diff --git a/.coordinator-state.md b/.coordinator-state.md deleted file mode 100644 index 9784f1f44..000000000 --- a/.coordinator-state.md +++ /dev/null @@ -1,48 +0,0 @@ -# Coordinator State - -## Last Updated -2026-05-11T15:50:00Z - -## Project: Grove to Project Rename - REVIEW LOOP COMPLETE - -### ALL PHASES COMPLETE, REVIEWED, AND APPROVED - -- Phase 0-6: COMPLETE (original implementation) -- Integration testing: PASSED on scion-integration2 and scion-next -- Code review loop: 8 iterations (v3-v10), APPROVED on v10 - -### Review Loop Summary -- v1 review: 4 issues (2 critical) -> FIXED -- v2 review: 5 issues (2 critical) -> FIXED -- v3 review: 5 issues (2 critical) -> FIXED -- v4 review: 8 issues (3 critical) -> FIXED (manager hit limits, restarted) -- v5 review: 6 issues (2 critical) -> FIXED -- v6 review: 2 issues (1 critical) -> FIXED -- v7 review: 3 issues (1 critical) -> FIXED -- v8 review: 3 issues (2 critical) -> FIXED -- v9 review: 3 issues (3 critical, JSON shadowing) -> FIXED -- v10 review: APPROVED - all tests pass, no critical issues - -### Branch: scion/rename-strategy -- 90 commits ahead of main -- go build ./... PASS -- go vet ./... PASS -- All key test suites PASS (hub, config, api, hubclient) - -### Pending -- PR to main (awaiting user go-ahead) - -### All Agents Cleaned Up -No running agents besides coordinator. - -### Reviews -- /scion-volumes/scratchpad/2026-05-11-code-review-rename.md (v1) -- /scion-volumes/scratchpad/2026-05-11-code-review-v2.md (v2) -- /scion-volumes/scratchpad/2026-05-11-code-review-v3.md (v3) -- /scion-volumes/scratchpad/2026-05-11-code-review-v4.md (v4) -- /scion-volumes/scratchpad/2026-05-11-code-review-v5.md (v5) -- /scion-volumes/scratchpad/2026-05-11-code-review-v6.md (v6) -- /scion-volumes/scratchpad/2026-05-11-code-review-v7.md (v7) -- /scion-volumes/scratchpad/2026-05-11-code-review-v8.md (v8) -- /scion-volumes/scratchpad/2026-05-11-code-review-v9.md (v9) -- /scion-volumes/scratchpad/2026-05-11-code-review-v10.md (v10 - APPROVED) diff --git a/.design/a2a-grpc-transport.md b/.design/a2a-grpc-transport.md new file mode 100644 index 000000000..b61d89ad8 --- /dev/null +++ b/.design/a2a-grpc-transport.md @@ -0,0 +1,90 @@ +# A2A Bridge: gRPC Transport + +**Status:** Implementing +**Created:** 2026-06-05 +**Related:** [a2a-bridge-design.md](./a2a-bridge-design.md), [a2a-multi-turn-lifecycle.md](./a2a-multi-turn-lifecycle.md) + +--- + +## 1. Problem + +The A2A bridge currently only supports JSON-RPC 2.0 over HTTP. The A2A protocol +specification defines three transport bindings: JSON-RPC, gRPC, and HTTP+JSON/REST. +The ITK compatibility tests exercise gRPC transport in their multi-hop traversal. +Without gRPC, the bridge cannot participate in cross-SDK interop scenarios that +use gRPC transport. + +## 2. Design + +### Approach: gRPC adapter over existing Bridge logic + +The Bridge struct already implements all A2A operations (SendMessage, GetTask, +ListTasks, CancelTask, push notifications, streaming). The gRPC server is a thin +adapter that: + +1. Receives gRPC requests +2. Translates protobuf messages to the bridge's internal types +3. Delegates to the same Bridge methods the JSON-RPC server uses +4. Translates responses back to protobuf + +This avoids duplicating any business logic. + +### Proto compilation + +Use the official A2A proto from `a2aproject/A2A/specification/a2a.proto` as the +source. Compile with `protoc` + `protoc-gen-go` + `protoc-gen-go-grpc` to generate +Go code. Generated code goes in `internal/a2apb/`. + +The proto has dependencies on `google/api/annotations.proto` etc. — use buf or +vendored google API protos. + +### Server structure + +``` +extras/scion-a2a-bridge/ + internal/ + a2apb/ # generated protobuf Go code + bridge/ + grpc_server.go # gRPC adapter implementing A2AServiceServer +``` + +### gRPC service methods → Bridge method mapping + +| gRPC RPC | Bridge method | +|---|---| +| SendMessage | Bridge.SendMessage (blocking or non-blocking based on return_immediately) | +| SendStreamingMessage | Bridge.SendStreamingMessage | +| GetTask | Bridge.GetTask | +| ListTasks | Bridge.ListTasks | +| CancelTask | Bridge.CancelTask | +| SubscribeToTask | Bridge.SubscribeToTask | +| CreateTaskPushNotificationConfig | Bridge.SetPushNotificationConfig | +| GetTaskPushNotificationConfig | Bridge.GetPushNotificationConfig | +| ListTaskPushNotificationConfigs | Bridge.GetPushNotificationConfig | +| DeleteTaskPushNotificationConfig | Bridge.DeletePushNotificationConfig | +| GetExtendedAgentCard | Bridge.GenerateAgentCard | + +### Configuration + +Add to config: +```yaml +bridge: + grpc_listen_address: ":9443" # separate port for gRPC +``` + +### Startup + +The main() function starts both HTTP and gRPC servers. gRPC is optional — only +started if grpc_listen_address is configured. + +## 3. Scope + +- In: gRPC server adapter, proto compilation, config, startup wiring +- Out: gRPC client (for making outbound A2A calls), TLS on gRPC (use reverse proxy) + +## 4. Testing + +- Test: each gRPC RPC maps correctly to Bridge method +- Test: protobuf ↔ internal type translation +- Test: streaming RPCs deliver events correctly +- Test: gRPC server starts and accepts connections diff --git a/.design/auth-proxy-mode.md b/.design/auth-proxy-mode.md new file mode 100644 index 000000000..ee2adb4fd --- /dev/null +++ b/.design/auth-proxy-mode.md @@ -0,0 +1,502 @@ +# Auth Proxy Mode (IAP-style header auth) + +## Status: Approach approved by @ptone (2026-06-05) — ready for implementation + +All open design decisions are resolved (see "Resolved Decisions"). Scope: add an +exclusive **proxy** human-auth mode (Google IAP first) with verified-assertion +provisioning, plus a hub-minted **transport-auth** layer that lets agents traverse +the IAP / Cloud Run-invoker front door (generalizing PR #307). + +## Problem Statement + +The hub supports two human auth modes today: + +1. **Developer / local-workstation auth** — single-user; auth is short-circuited + through a locally-minted dev token (`scion_dev_*`). See `pkg/hub/devauth.go`, + `pkg/hub/auth.go:163`. +2. **OAuth login** — full browser/CLI/device flows against Google and GitHub + (plus a partial custom OIDC provider). The hub exchanges an authorization code + for provider userinfo, provisions the user, and mints its own session JWT. + See `pkg/hub/oauth.go`, `pkg/hub/handlers_auth.go`. + +We want to add a **third mode: authenticating-proxy mode**, where the hub sits +behind a trusted proxy that has already authenticated the user, and the hub +derives the current user from proxy-supplied request headers. The first concrete +target is **Google IAP** with its signed-header (JWT assertion) format: +https://docs.cloud.google.com/iap/docs/signed-headers-howto + +Unlike OAuth, proxy mode has **no login step and no hub-minted session** — the +proxy re-asserts the identity on *every* request. The design must reconcile this +with the hub's existing login-time provisioning/authorization logic. + +## Goals + +- Verify Google IAP signed headers (`X-Goog-IAP-JWT-Assertion`) cryptographically + on each request and derive the current user from the verified assertion. +- Provision a user on first sight if they don't exist, subject to the **existing** + access controls: `admin_emails`, `authorized_domains`, and + `user_access_mode` (`open` / `domain_restricted` / `invite_only`). +- Make the proxy layer pluggable so non-IAP proxies (Cloudflare Access, ALB OIDC, + a self-managed sidecar) can be added later without touching the middleware. +- Reuse — not duplicate — the OAuth path's provisioning and authorization logic. + +## Non-Goals + +- Replacing OAuth or dev auth. Proxy mode is an additional, independently + selectable mode. +- Implementing the agent/CLI ingress story behind IAP beyond documenting it + (see Open Question 1). Initial scope is **human web users**. +- A generic SAML/arbitrary-IdP integration. + +## Background: what already exists (and its limits) + +There is already a shallow proxy path in `UnifiedAuthMiddleware` +(`pkg/hub/auth.go:139-159`): + +```go +// Step 3: if no bearer token AND the request came from a trusted-proxy IP, +// build an identity directly from X-Forwarded-User-* headers. +if len(trustedNets) > 0 && isTrustedProxy(r, trustedNets) { + if user := extractProxyUser(r); user != nil { ... } +} +``` + +`extractProxyUser` (`pkg/hub/auth.go:379`) reads `X-Forwarded-User-Id/Email/Name/Role` +and synthesizes an `AuthenticatedUser`. The plumbing is useful but **insufficient +for IAP**: + +- **No signature verification.** Trust is based solely on the source IP CIDR + (`TrustedProxies`). IAP instead hands us a *signed* JWT we should verify; IP + trust alone is brittle (NAT, mesh, misconfig) and is not what IAP expects. +- **No provisioning.** It fabricates an identity from headers and never consults + the user store — no canonical user UUID, role, or `status` (suspended?) lookup, + and no create-if-not-exists. +- **No access control.** `domain_restricted` / `invite_only` / `admin_emails` are + never applied on this path. +- **Header trust mismatch.** IAP's *unsigned* convenience headers + (`X-Goog-Authenticated-User-Email` / `-Id`) must **not** be trusted; only the + signed assertion is safe. + +Good news — the pieces we need to reuse already exist and are factored: + +- Authorization: `checkUserAuthorized(ctx, email, authorizedDomains, adminEmails, accessMode, store)` + (`pkg/hub/handlers_auth.go:1268`) — admin bypass, domain match, allow-list. +- Role assignment: `determineUserRole(email, adminEmails)` via + `Server.getUserRole` (`handlers_auth.go`). +- Identity model already reserves a proxy slot: `AuthTypeProxy = "proxy"` + (`pkg/hub/identity.go:202`). + +The find-or-create user block is currently **duplicated** in the OAuth handlers +(`handlers_auth.go:257-292` and `401-436`). This design extracts it so the proxy +path and both OAuth call sites share one implementation. + +## Google IAP signed-header primer + +- **Header:** `X-Goog-IAP-JWT-Assertion`, value is a compact JWT. +- **Algorithm:** `ES256` (ECDSA P-256). +- **Public keys:** JWKS at `https://www.gstatic.com/iap/verify/public_key-jwk`, + selected by the JWT `kid`. Must be cached and periodically refreshed (keys rotate). +- **`iss`:** `https://cloud.google.com/iap`. +- **`aud`:** deployment-specific and **must** be validated: + - GCE/GKE backend service: `/projects/PROJECT_NUMBER/global/backendServices/BACKEND_SERVICE_ID` + - App Engine: `/projects/PROJECT_NUMBER/apps/PROJECT_ID` +- **Claims:** `sub` = `accounts.google.com:`, `email` = + `accounts.google.com:
` (note the IdP prefix — must be stripped), + optional `hd` (Workspace hosted domain), `iat`/`exp` (validate with small skew). +- The unsigned `X-Goog-Authenticated-User-{Email,Id}` headers are spoofable if a + request ever reaches the hub without traversing IAP; we ignore them entirely. + +## Proposed Design + +### 1. A `ProxyAuthenticator` abstraction + +Introduce a small interface so the middleware is provider-agnostic: + +```go +// ProxyUserInfo is the verified identity extracted from proxy headers. +type ProxyUserInfo struct { + Subject string // stable provider subject (IdP prefix stripped) + Email string // verified email (IdP prefix stripped, lowercased) + DisplayName string // best-effort; may be empty for IAP + Domain string // hd claim, if present +} + +// ProxyAuthenticator verifies proxy-supplied auth on a request and returns the +// verified user. (nil, nil) = "no proxy assertion present" (fall through); +// (nil, err) = assertion present but invalid (reject). +type ProxyAuthenticator interface { + Authenticate(r *http.Request) (*ProxyUserInfo, error) + Name() string // for logging/metrics, e.g. "iap" +} +``` + +Implementations: + +- **`IAPAuthenticator`** (new) — verifies `X-Goog-IAP-JWT-Assertion`: + parse JWT → look up `kid` in cached JWKS → verify ES256 signature → check + `iss`, `aud` (against configured audience), `exp`/`iat` (±skew) → strip + `accounts.google.com:` prefixes → return `ProxyUserInfo`. + JWKS is fetched lazily and cached with periodic refresh + on-miss refresh for + rotated `kid`s. +- **`HeaderProxyAuthenticator`** (refactor of today's `extractProxyUser`) — keeps + the `X-Forwarded-User-*` + IP-trust behavior for self-managed proxies, now + routed through the same provisioning path. Not the initial focus but preserved. + +Selecting an external JWT library: prefer an already-vendored one. Decision point +— see Open Question 3. + +### 2. User provisioning service (shared) + +Extract the duplicated find-or-create block into one method on `Server`: + +```go +// provisionUser resolves a verified external identity to a stored user, +// applying access controls and creating the user on first sight. +// Returns (nil, errAccessDenied) when the user is not authorized. +func (s *Server) provisionUser(ctx context.Context, info ExternalUserInfo) (*store.User, error) +``` + +Behavior (identical to today's OAuth path, just centralized): + +1. `checkUserAuthorized(...)` → deny if not permitted. +2. `GetUserByEmail`; if missing, `CreateUser` with `Role = getUserRole(email)`, + `Status = "active"`. +3. If found: refresh `LastLogin`, backfill display/avatar, promote to admin if + newly listed, reject if `Status == "suspended"`. +4. `ensureHubMembership(ctx, store, user.ID)`. + +Both OAuth call sites (`handlers_auth.go:257`, `:401`) are refactored to call this, +removing the duplication. The proxy middleware calls the same method. + +### 3. Middleware integration & precedence + +Add a proxy step to `UnifiedAuthMiddleware` (`pkg/hub/auth.go`). Precedence, +highest first: + +1. Agent token (`X-Scion-Agent-Token` / agent JWT) — unchanged. +2. Broker HMAC (`X-Scion-Broker-ID`) — unchanged. +3. Bearer token (dev / UAT / user JWT) — unchanged. +4. **Proxy authenticator** (new) — runs when configured and no higher-priority + credential matched. Replaces the current IP-only `extractProxyUser` branch. + +Keeping bearer/agent ahead of proxy means internal/non-IAP ingress (agents, CLI, +service tokens) still works even when the proxy front-end is enabled — important +for the agent-ingress question below. + +On a verified proxy assertion the middleware calls `provisionUser`, then sets the +identity (canonical stored user — real UUID/role, not header-derived) and +`AuthTypeProxy`. To avoid a DB round-trip on every request, wrap the resolution +in a short-TTL cache keyed by verified email (e.g. 30–60s); the signature check +still runs every request, only the store lookup is cached. Cache TTL is a tuning +knob — Open Question 4. + +### 4. Configuration + +New `Auth.Proxy` section in `pkg/config/hub_config.go`, surfaced through +`ServerConfig`/`AuthConfig` and wired in `cmd/server_foreground.go` (alongside the +existing `DevAuthToken`/`UserAccessMode` wiring at `:868`, `:1132`): + +```yaml +auth: + mode: proxy # oauth | proxy | dev — exclusive human auth mode + proxy: + # consulted only when mode == proxy + provider: iap # iap | header + iap: + audience: "/projects/123456789/global/backendServices/987654321" + # issuer + JWKS URL default to Google's; overridable for testing + requireTrustedProxyIP: false # optional defense-in-depth IP allowlist + # transport (outer/platform) auth the hub instructs agents to carry. + # Drives which entries the refresh endpoint returns (see "Generalized token refresh"). + transport: + mode: iap # none | cloudrun_invoker | iap + oidcAudience: "" # IAP client ID (iap) or hub URL (invoker); empty = derive + platformAuthSA: "scion-transport-auth@PROJECT.iam.gserviceaccount.com" + # reuses existing knobs for provisioning: + userAccessMode: domain_restricted + authorizedDomains: ["example.com"] + adminEmails: ["admin@example.com"] +``` + +`auth.mode` is an **exclusive** selector — `proxy` and `oauth` are never both +active (Decision 4). In `proxy` mode the OAuth handlers and `/auth/providers` are +disabled. `user_access_mode`, `authorized_domains`, `admin_emails` are **reused +as-is** for proxy provisioning — no new access-control concepts. +`auth.transport.mode` (distinct from `auth.mode`) is the server-side source of +truth for which transport tokens the refresh endpoint hands back to agents. + +### 5. Logout / session semantics + +In proxy mode the hub does not own the session, so hub `/logout` cannot end it. +`/api/v1/auth/logout` should become a no-op (or redirect to IAP's +`/_gcp_iap/clear_login_cookie`). Because mode is exclusive, the login UI renders a +proxy-mode view with **no** OAuth provider buttons (extend the existing +`devAuthEnabled` gate at `web.go:1549` with the active `auth.mode`); `/auth/providers` +returns empty/unavailable in proxy mode. + +## Security Considerations + +- **Verify, don't trust headers.** Only the signed assertion is authoritative; + the unsigned `X-Goog-Authenticated-User-*` headers are ignored. +- **Audience binding** is mandatory — without it, a JWT minted for a different + IAP-protected service would be accepted. +- **Bypass risk.** The hub must be reachable *only* through IAP for the human + surface; any path that reaches the hub directly could spoof headers — except + the verified-JWT path is safe regardless, since forged assertions fail the + signature check. The optional `requireTrustedProxyIP` adds belt-and-suspenders. +- **Key rotation / availability.** Cache JWKS; refresh on unknown `kid`; tolerate + transient JWKS-endpoint failures by serving the last good key set. +- **Clock skew.** Allow a small leeway on `exp`/`iat`. +- **Suspended users.** `provisionUser` must reject `status == "suspended"` even + though IAP would still authenticate them upstream. + +## Dual-layer auth: agent/service ingress (resolved — generalize PR #307) + +Agents do **not** need a separate non-proxied ingress. They traverse the same +front door using a two-layer credential, generalizing the Cloud Run pattern from +[PR #307](https://github.com/GoogleCloudPlatform/scion/pull/307): + +- **Outer (platform) layer** — `Authorization: Bearer `, + fetched from the GCE metadata server by `pkg/sciontool/hub/oidc.go`. This + satisfies the platform guard (Cloud Run invoker IAM, or IAP programmatic access). +- **App layer** — `X-Scion-Agent-Token: `, the existing hub agent auth. + Because it's a custom header, it never collides with the outer `Authorization`. + +The two scenarios differ **only in the OIDC audience**: + +- **Cloud Run invoker:** `aud` = the hub URL (current default in `oidc.go`). +- **IAP:** `aud` = the **IAP OAuth client ID**. IAP validates the token, then + injects `X-Goog-IAP-JWT-Assertion` asserting the *service account's* identity. + +`oidc.go` already supports this via `SCION_HUB_OIDC_AUDIENCE`. Generalization work: +formalize audience selection so an IAP deployment sets the IAP client ID (config / +env), rather than defaulting to the hub URL. No three-layer case to handle — per +the deployment owner, when IAP and invoker guards are both present the IAP service +agent carries the invoker role, so the agent still sends a single outer token. + +**Hub-side consequence (important precedence rule):** an agent request arriving +through IAP carries *both* `X-Goog-IAP-JWT-Assertion` (the service account) *and* +`X-Scion-Agent-Token`. The middleware checks the agent token **first** (Step 1), +so the request is identified as the agent. When any app-layer credential +(agent/broker/bearer) is present, the proxy assertion is treated as **transport +only** and is **not** used to provision a user — we never create user records for +service-account identities. The proxy authenticator runs only when no app-layer +credential matched (i.e. genuine human IAP traffic). This is already the ordering +in §3. + +**One residual nuance to be aware of:** `Authorization`-based *scion* credentials +(user JWT, `scion_pat_` UAT) cannot coexist with an outer Google OIDC token behind +a Cloud Run invoker, because there is only one `Authorization` header. This only +affects a human CLI hitting an invoker-guarded hub directly; it does not affect +agents (custom header) or IAP human traffic (assertion header). Out of initial +scope, but noted — a future option is to also accept UATs via a custom header. + +## Agent OIDC identity & bootstrap (resolved) + +### How PR #307 makes first contact today + +PR #307 has **no** chicken-egg because it uses the agent's **ambient** GCP +identity: `oidc.go` calls the local metadata server +(`instance/service-accounts/default/identity?audience=`). This works on +the very first request because the GKE pod already has a workload-identity SA +attached and that SA was pre-granted the Cloud Run invoker role. The cost is +exactly what we want to avoid: + +- **Policy sprawl:** every agent's compute SA, across every project, must hold the + invoker (or IAP-access) role — or we lean on a broad + `principalSet://…/type/ServiceAccount` grant per project. +- **Coupling to agent GCP identity:** it only works in `passthrough` metadata + mode. It is wrong in `assign` mode (grants platform-auth to the agent's + app-purposed SA) and impossible in `block` mode (`GCPMetadataMode`, + `pkg/api/types.go:489`). + +### Goal: a hub-managed SA, decoupled from agent GCP identity + +Treat the outer OIDC layer as **strictly a hub-auth concern**: one hub-managed +service account used for the platform layer by all agents in all projects, +independent of whether the agent's own GCP identity is `block`/`passthrough`/ +`assign`. Avoid distributing a keyfile (more sensitive than the telemetry key, +and we don't want another keyfile to manage). + +### Bootstrap vs. steady-state refresh (corrected — only first contact is cold) + +An earlier draft of this section over-claimed that "you can't refresh the +front-door key through the front door." That is wrong for steady state, and the +agent's own scion JWT already proves it: sciontool refreshes the scion JWT by +calling the **hub directly** (not the broker), and it works because the refresh +happens while the *current* credential is still valid — a sliding window. The +same applies to the outer OIDC token: + +> As long as the agent refreshes **before** the current OIDC expires, the request +> reaches the hub on the old (still-valid) token; the hub mints a fresh OIDC (it +> manages the SA) and returns it in the response body. The platform validated the +> inbound request; the response is just data carrying the next token. + +So the side channel is required **only for the genuinely cold case** — the very +first token, before the agent has ever connected. Everything after that rides the +front door, exactly like the scion JWT. + +Two distinct phases: + +1. **First contact (cold — side channel required).** The agent has no OIDC yet, so + it cannot call the hub. The hub mints the initial OIDC token at dispatch + (impersonating the hub-managed SA) and includes it in the **dispatch payload**, + which already flows hub → broker → agent env injection alongside + `SCION_AUTH_TOKEN` (`cmd/hub.go:449`). That path is hub-originated and not behind + IAP. One-time, no chicken-egg. +2. **Steady-state refresh (warm — through the hub).** The agent maintains a rolling + OIDC via a background ticker that refreshes well before the ~1h expiry. Simplest + surface: **piggyback on the existing scion-JWT refresh** — the refresh response + returns both a new scion access token *and* a fresh OIDC token, sliding both + layers in one call. The refresh is authenticated by the agent's scion identity, + so only legitimately-connected agents get fresh platform tokens. No broker + involvement; matches how the scion JWT already works. + +Google ID tokens are fixed ~1h with no refresh-token concept, so the background +ticker (sub-1h cadence) is what keeps a long-running *idle* agent from ever +letting the OIDC lapse. A stopped/restarted agent simply re-bootstraps via dispatch +(phase 1) — the same way it re-acquires its scion token. + +### Options for who holds the minting capability + +- **A — Keyfile for the hub-managed SA.** Inject the SA JSON key; agent self-mints + ID tokens. Trivial, but a long-lived auth-grade secret in every agent container. + **Rejected** (per deployment owner). +- **B — Impersonate via the agent's own GCP identity.** Agent's ambient SA gets + `serviceAccountTokenCreator` on the hub SA. Re-introduces per-SA IAM and + re-couples to GCP identity — breaks in `block` mode. **Not recommended.** +- **C — Hub mints (recommended).** The **hub** impersonates the single hub-managed + SA for both phase 1 (dispatch) and phase 2 (refresh response). The auth-grade + minting capability lives **only in the hub**; agents hold no SA credential and + need no GCP identity (works even in `block` mode). The **broker is just the + dispatch conduit** it already is — it needs **no** token-minting IAM grant. Only + the hub's runtime SA needs `serviceAccountTokenCreator` on the managed SA. + +This is simpler than the earlier "broker mints / broker relays" variants, which the +corrected refresh model makes unnecessary: the broker never mints. (The broker's +*own* control channel to the hub still authenticates with the broker's ambient infra +SA — one invoker grant on that SA — but that is a small, fixed, infra-managed set, +not the per-agent sprawl we're avoiding.) + +**Decoupling:** the agent needs no GCP identity for hub auth; the sensitive +credential never leaves the hub. Strictly better than the telemetry-key model. + +**Generalizing `oidc.go`:** make its token source pluggable — +`metadataTokenSource` (PR #307 / passthrough) vs an `injectedTokenSource` (phase 1) +that is then refreshed via the hub (phase 2). Audience is set per scenario (IAP +client ID, or hub URL for invoker). + +**Sub-decisions (resolved):** + +- **Dedicated platform/transport-auth SA — confirmed.** A dedicated service account + used *only* for the invoker/IAP transport layer, **owned and managed by the hub + SA** (the hub SA holds `serviceAccountTokenCreator`/`getOpenIdToken` on it and + impersonates it to mint agent ID tokens). It is never used for anything but the + platform guard; its asserted identity is ignored as transport at the app layer + (per §"Dual-layer auth"). +- **Piggyback on the refresh endpoint — confirmed, generalized to an array.** Refactor + the refresh response to return an **array of updated tokens** rather than a single + token pair. The set of tokens returned is driven by a **server config setting** + that declares the system's overall auth/transport configuration, so the client is + config-light and learns what to maintain from the server. See §"Generalized token + refresh" below. + +### Generalized token refresh (array payload) + +The agent refresh endpoint is refactored from "return a scion access/refresh token" +to "return the set of credentials this deployment requires the client to maintain." + +```jsonc +// Response from the agent token refresh endpoint +{ + "tokens": [ + { "layer": "app", "type": "scion_access", "value": "...", "expiresIn": 900 }, + { "layer": "app", "type": "scion_refresh", "value": "...", "expiresIn": 604800 }, + // present only when transport auth is configured (IAP / Cloud Run invoker): + { "layer": "transport", "type": "google_oidc", "value": "...", + "audience": "", "expiresIn": 3600 } + ] +} +``` + +- Which entries appear is decided **server-side** from a config setting describing + the deployment's transport mode (e.g. `none` / `cloudrun_invoker` / `iap`), so the + same client binary works across deployments without per-mode flags. +- The client applies each entry to the right place by `layer`/`type`: app tokens to + the scion-token store; `transport: google_oidc` to the OIDC transport's token + source (`oidc.go`), which sets `Authorization: Bearer` on outbound hub requests. +- A `transport` token is minted by the hub via the dedicated SA with the configured + audience. The background ticker drives refresh on the shortest-lived entry. +- First contact (dispatch payload) uses the **same** token-array shape, so phase 1 + and phase 2 share one schema. + +## Resolved Decisions + +1. **Provisioning trigger — lazy, allow-list-gated.** On the first verified human + IAP request, `provisionUser` runs `checkUserAuthorized` and **auto-creates** the + user iff the email is already permitted (admin / authorized domain / for + `invite_only`, already allow-listed). No separate redeem/claim step; if not + permitted, return 403. (`invite_only` allow-listing is populated as today, via + admin/invite-code redemption — that just isn't part of the request-time path.) + +2. **JWT/JWKS — reuse `go-jose/go-jose/v4` (no new dep).** It is already vendored + (`go.mod`) with its `/jwt` subpackage and natively supports **ES256** and JWKS + (`jose.JSONWebKeySet`). `IAPAuthenticator` verifies the assertion with go-jose; + only a thin JWKS fetch+cache wrapper around the gstatic endpoint is new. + +3. **Resolution cache TTL — 60s.** Acceptable staleness for role/suspension under + proxy mode (deemed near-inconsequential). + +4. **Mode is exclusive — proxy XOR OAuth, never both.** A deployment selects *one* + human auth mode. Implication: a single `auth.mode` selector (e.g. + `oauth` | `proxy` | `dev`) gates which login surface is active; in `proxy` mode + the OAuth handlers and `/auth/providers` are disabled and the login UI shows no + provider buttons (the front door is the proxy). This is cleaner than the + "coexist + hide buttons" idea and removes the headless-CLI-via-device-flow + ambiguity — headless/agent access in proxy deployments uses the transport-token + path (§"Agent OIDC identity"), not OAuth device flow. + +## Implementation Plan (phased) + +**Phase 0 — refactor (no behavior change, lands independently):** +1. Extract `provisionUser` from the two OAuth call sites + (`handlers_auth.go:257`, `:401`) and refactor both onto it. + +**Phase 1 — inbound proxy (human IAP auth):** +2. `pkg/hub/proxyauth.go`: `ProxyAuthenticator` interface + `IAPAuthenticator` + (verify with `go-jose/v4`, ES256, gstatic JWKS fetch+cache) + unit tests using a + test key pair with overridable issuer/JWKS URL. +3. `auth.mode` + `auth.proxy` config in `hub_config.go`/`settings_v1.go`; wire into + `ServerConfig`/`AuthConfig` in `cmd/server_foreground.go`. +4. Replace the IP-only proxy branch in `UnifiedAuthMiddleware` with the + authenticator → `provisionUser` (allow-list-gated) → 60s resolution cache; set + `AuthTypeProxy`. +5. Web login-UI: exclusive-mode gating (proxy view, no OAuth buttons), + `/auth/providers` disabled, logout no-op/redirect. + +**Phase 2 — outbound transport auth (agents through the front door):** +6. Hub-side issuance: dedicated transport-auth SA (owned/impersonated by hub SA); + `auth.transport` config; mint the initial token into the dispatch payload. +7. Refactor the agent token refresh response to the `tokens[]` array shape, driven + by `auth.transport.mode`; same shape reused for the dispatch payload. +8. Agent-side (`pkg/sciontool/hub/oidc.go`): consume the `transport` token from the + refresh/dispatch array (pluggable source vs the PR #307 metadata source); + background ticker refreshes on the shortest-lived entry. + +**Phase 3 — docs:** +9. Deployment guide for the IAP + Cloud Run-invoker topology. + +Phase 1 (inbound IAP) and Phase 2 (outbound transport) are independent and can be +built in parallel once Phase 0 lands; Phase 2 builds on PR #307. + +## Files in scope + +- `pkg/hub/auth.go` — middleware integration, retire IP-only `extractProxyUser`. +- `pkg/hub/proxyauth.go` *(new)* — `ProxyAuthenticator`, `IAPAuthenticator`, JWKS. +- `pkg/hub/handlers_auth.go` — extract `provisionUser`, dedupe OAuth call sites. +- `pkg/hub/identity.go` — reuse `AuthTypeProxy`; proxy identity wrapper if needed. +- `pkg/config/hub_config.go`, `pkg/config/settings_v1.go` — `Auth.Proxy` config. +- `cmd/server_foreground.go` — wiring into `ServerConfig`/`AuthConfig`. +- `pkg/hub/web.go` — proxy-mode login-UI flag; logout semantics. +- `pkg/sciontool/hub/oidc.go` — agent-side dual-layer transport; audience + selection for IAP (builds on PR #307). diff --git a/.design/broker-dispatch.md b/.design/broker-dispatch.md new file mode 100644 index 000000000..fdb008d66 --- /dev/null +++ b/.design/broker-dispatch.md @@ -0,0 +1,772 @@ +# Design: Multi-Node Broker Dispatch over LISTEN/NOTIFY + +**Branch:** `postgres/wave-b-integration` +**Date:** 2026-06-02 +**Author:** broker-architect agent +**Status:** Approach approved by @ptone (2026-06-02). Scope: **message + agent +lifecycle dispatch only**; model is **"DB as state machine, NOTIFY as the +communications channel."** PTY, logs, and exec are out of scope (§10). +**Reviewers:** @ptone +**Implements:** the agreed "DB-state-machine + NOTIFY-signaled dispatch" approach. + +Inputs: `RESEARCH-MESSAGE-DISPATCH.md`, `RESEARCH-BROKER-ROUTING.md`, +`pkg/hub/controlchannel.go`, `pkg/hub/controlchannel_client.go`, +`pkg/hub/events_postgres.go`, `pkg/hub/server.go`, `.design/postgres-strategy.md`. + +--- + +## 1. Problem statement + +A runtime broker opens **one** outbound WebSocket "control channel" to **one** hub +replica. That replica holds the live socket in an in-memory map +(`ControlChannelManager.connections`). Dispatch (`start`/`stop`/`message`/`exec`/…) +decides reachability purely from that local map +(`HybridBrokerClient.useControlChannel` → `manager.IsConnected`). + +Behind a load balancer with N replicas, an API call lands on an arbitrary replica. +If the broker's socket is on Hub A but the call lands on Hub B: + +- `IsConnected(brokerID)` is **false** on Hub B → falls back to HTTP at + `broker.Endpoint`. +- For NAT'd / control-channel-only brokers (`Endpoint == ""` — the entire reason the + control channel exists) the HTTP fallback **fails**, and worse, for messages the + store row + SSE event were already written, so the UI shows "sent" while the agent + never receives it (silent split-brain). Probability of failure ≈ (N−1)/N. + +Two further defects compound this: + +- **No broker→hub affinity** exists in the DB. A replica cannot even discover which + peer owns a socket. (`runtime_brokers` has `status`/`connection_state`/ + `last_heartbeat` but no owning-replica column.) +- **`onDisconnect` status race** (`server.go:691`): the callback unconditionally + stamps the broker `offline`. When a broker flaps A→B, Hub A's delayed disconnect + can clobber Hub B's freshly-written `online` (last-writer-wins on + `runtime_brokers.status`). + +## 2. Design goals & non-goals + +### 2.0 Hard constraint (maintainer-confirmed, 2026-06-02) + +> **There is no hub-to-hub HTTP addressability.** A node generally cannot reach +> another node directly. A broker's reverse tunnel lands on an **arbitrary** node and +> stays sticky there. Therefore **Postgres LISTEN/NOTIFY is the only inter-node +> transport**, and dispatch must reach the socket-holding node *without any node +> addressing another*. + +### 2.0.1 Model: DB as state machine, NOTIFY as the signal (maintainer-directed) + +> **The DB holds the durable state/intent; NOTIFY is only the wakeup signal.** A +> dispatch is *not* "send a command over NOTIFY and hope a node is listening." It is +> "write the intent to the DB (durable), then NOTIFY so the socket-holding node wakes +> and **reconciles** DB intent → broker." If the NOTIFY is missed, or the owning node +> is down, the intent persists and is reconciled when a node next owns the socket +> (on (re)connect). This gives durability and at-least-once delivery **for free**, and +> makes the NOTIFY payload a tiny signal rather than the source of truth. + +This reframes the response pattern too: the originator observes **DB state changes** (the +agent's 3-layer `phase`/`activity`/`detail`, or the message's `dispatch_state`) via the +events that already publish those transitions cross-node — not a bespoke RPC reply. A +**rolling timeout resets on each such change** (§6.4), so liveness, not a fixed clock, +bounds the wait. + +Consequences baked into this design: +- Intent is **persisted to the DB** first; a NOTIFY on the global channel signals + "reconcile broker X". The node holding the socket **self-selects** and reconciles. + No node ever addresses a peer. +- Responses are **DB-state transitions observed via existing events** (`agent..status` + phase changes; `agent.deleted`; message `dispatch_state`). No hub-to-hub reply path. +- The `connected_hub_id` affinity column is **not load-bearing for routing** — + ownership is decided by who physically holds the socket. Affinity exists only to + (a) fast-fail when *no* node owns the broker and (b) fix the `onDisconnect` race. +- **PTY, logs, and exec are out of scope.** PTY/log streams cannot ride NOTIFY and + cannot be hub-to-hub reverse-proxied (no addressability); exec is an interactive + request/response that does not fit the state-machine model. The only path for these + is LB sticky-routing the client to the owning node — a separate problem (§10). + +**Goals** +- A dispatch arriving at *any* node reaches the broker's socket, wherever it lives — + with no node addressing another. +- Reuse the existing `PostgresEventPublisher` (LISTEN/NOTIFY, payload-offload, + reconnect) — no new transport. +- **Durable + at-least-once** for in-scope dispatch: intent persists in the DB and is + reconciled on (re)connect, so a missed NOTIFY or a down owner does not lose the + command (§2.0.1). +- Fix the `onDisconnect` clobber race as a side effect of affinity tracking. +- Preserve today's fast path (local socket → tunnel) unchanged and at zero added + latency. +- Preserve today's API semantics (start/stop "done" == broker accepted the command; + see §6). +- **Support long, multi-step provisioning** (GKE pod cold-start, future runtime + providers): reuse the existing 3-layer agent state (phase/activity/detail) for interim + feedback and a **rolling timeout** that resets on each update, so duration is bounded by + broker liveness, not a fixed clock (§6.4). + +**In scope (commands):** `message` (incl. broadcast / `set[]`), and **agent +lifecycle**: `start`, `stop`, `restart`, `delete`, and create-time ops +(`create-with-gather`, `finalize-env`, `check-prompt`). + +**Non-goals (this design)** +- **PTY / interactive streams** (`OpenStream`/`SendStreamData`), **logs** + (`GetAgentLogs`), and **exec** (`ExecAgent`) — explicitly out of scope per maintainer + (§10). They do not fit "DB as state machine" and/or cannot ride NOTIFY. +- Hub-to-hub HTTP of any kind (does not exist; §2.0). +- Replacing the HTTP-endpoint fast path for direct-mode brokers (kept as a fallback + tier; rare under NAT'd deployments). + +--- + +## 3. Architecture overview + +``` + shared Postgres (one DB, N hubs) + ┌──────────────────────────────────────────────────────────────────────┐ + │ runtime_brokers (+ connected_hub_id, connected_session_id, …) │ + │ scion_event_payloads (existing oversized-payload offload) │ + │ LISTEN/NOTIFY channels: │ + │ scion_ev_global / scion_ev_g_ (existing events) │ + │ scion_broker_cmd (NEW: dispatch commands) │ + └──────────────────────────────────────────────────────────────────────┘ + ▲ ▲ ▲ │ ▲ │ + │ │ NOTIFY cmd │ │ NOTIFY agent.status │ │ + │ │ LISTEN │ ▼ LISTEN │ ▼ + ┌────┴──┴─────┐ ┌──────┴────────┐ ┌─────┴───────┐ + │ Hub B │ │ Hub A │ │ Hub C │ + │ (API entry) │ │ owns brokerX │ │ │ + │ │ │ socket in-mem │ │ │ + │ instanceID= │ │ instanceID= │ │ instanceID= │ + │ b2f1… │ │ a9c3… │ │ c7e0… │ + └─────────────┘ └──────┬────────┘ └─────────────┘ + ║ WS control channel + ┌────╨─────┐ + │ broker X │ (NAT'd; Endpoint == "") + │ agents │ + └──────────┘ + +Outbound dispatch (API on Hub B, socket on Hub A): + 1. Hub B handler → HybridBrokerClient. + 2. local IsConnected(X)? NO + 3. write DURABLE INTENT (broker_dispatch row / message.dispatch_state) + NOTIFY + scion_broker_cmd{broker_id:X} — in ONE transaction (PublishTx) + 4. Hub A's signal-listener wakes, sees ownsLocally(X)==true, CAS-claims the intent, + runs LOCAL tunnel , marks the intent done + 5. (for start/stop) Hub A sets phase + PublishAgentStatus ── NOTIFY agent.status ──┐ + 6. Hub B, which Subscribed to agent..status before step 3, wakes and returns ◄──┘ + to the API caller. (message = fire-and-forget: Hub B already returned 202 at step 3, + durably. If NO node owns X, the intent persists and reconciles on X's reconnect.) +``` + +Two NOTIFY directions, both on infrastructure that already exists: + +- **Command signal (NEW channel `scion_broker_cmd`)** is a *tiny wakeup* — `{broker_id}`, + no payload. The durable command lives in the DB. Every node receives the signal; only + the socket-holder reconciles (ownership *self-selected*). Affinity (`connected_hub_id`) + is a fast-fail hint, not the correctness gate; the reconnect-drain is the durability + backstop. +- **Response (EXISTING channels `scion_ev_*`)** is the already-published + `AgentStatusEvent` (carries `Phase`) for lifecycle, or a slim `broker.dispatch.` + completion event for data-returning ops. The originating node subscribes and waits; the + authoritative result is always the DB row. + +--- + +## 4. Component 1 — Hub instance identity & broker affinity + +### 4.1 Per-process instance ID (NEW — do **not** reuse `hubID`) + +`hubID` (`config.ResolveHubID`) is **logical**: it is `HubID` from config if set, +else `sha256(hostname)[:12]`. It is used for **secret namespacing** and is explicitly +intended to be *stable* — operators may configure the *same* `HubID` across replicas +so they share a secret scope. Therefore `hubID` is **not safe** as an affinity key: +two replicas can legitimately share it. + +Introduce a distinct **per-process instance ID**: + +```go +// Server field, set once at construction. +instanceID string // e.g. uuid.NewString(); unique per hub process/boot +``` + +- Generated at boot (random UUID). Optionally seed from `POD_NAME`+boot-nonce in k8s + for log readability, but uniqueness must not depend on hostname. +- Lives only in memory + the affinity column; never persisted to config. +- Exposed as `Server.InstanceID()`. + +### 4.2 Schema change — `runtime_brokers` + +Add three nullable columns (Ent schema `pkg/ent/schema/runtimebroker.go` + store model +`pkg/store/models.go` + migration): + +| Column | Type | Meaning | +|---|---|---| +| `connected_hub_id` | `TEXT` null | instance ID of the replica currently holding the socket; `NULL` when no replica owns it | +| `connected_session_id` | `TEXT` null | the `BrokerConnection.sessionID` (uuid) of the owning socket — disambiguates reconnects | +| `connected_at` | `TIMESTAMPTZ` null | when the current owner registered the socket | + +Reuse the existing `lock_version` optimistic-concurrency token (already on the row, +already CAS-looped by `UpdateRuntimeBrokerHeartbeat`). + +> Dialect-neutral per `postgres-strategy.md` §6.4: `TEXT`/`TIMESTAMPTZ` work on both +> SQLite and Postgres. No Postgres-only types. + +### 4.3 Affinity write paths (store methods) + +Two new store methods, both modeled on the `UpdateRuntimeBrokerHeartbeat` CAS loop +(`project_store.go:755`): + +```go +// ClaimRuntimeBrokerConnection sets affinity to this replica unconditionally +// (the newest connection wins — mirrors HandleUpgrade replacing an existing local +// socket). Bumps status->online + heartbeat in the same CAS write. +ClaimRuntimeBrokerConnection(ctx, brokerID, hubInstanceID, sessionID string) error + +// ReleaseRuntimeBrokerConnection clears affinity ONLY IF it still names +// (hubInstanceID, sessionID) — compare-and-clear. Returns (cleared bool). +// If affinity already moved to another replica/session, it is a no-op and the +// caller MUST NOT stamp the broker offline (fixes the §1 race). +ReleaseRuntimeBrokerConnection(ctx, brokerID, hubInstanceID, sessionID string) (bool, error) +``` + +`ClaimRuntimeBrokerConnection` is called from `markBrokerOnline` +(`server.go:2456`) — pass the new `sessionID` out of `HandleUpgrade` (it already +generates one at `controlchannel.go:202`; thread it through the `onConnect` path). + +### 4.4 The `onDisconnect` race fix (Component 5 in the brief) + +Today (`server.go:691`): +```go +srv.controlChannel.SetOnDisconnect(func(brokerID string) { + s.UpdateRuntimeBrokerHeartbeat(ctx, brokerID, store.BrokerStatusOffline) // UNCONDITIONAL + ... +}) +``` + +New: `SetOnDisconnect` must receive the **sessionID** of the connection that dropped +(extend the callback signature to `func(brokerID, sessionID string)` — `removeConnection` +already has the `*BrokerConnection`, so it can pass `hc.sessionID`). Then: + +```go +srv.controlChannel.SetOnDisconnect(func(brokerID, sessionID string) { + cleared, err := s.store.ReleaseRuntimeBrokerConnection(ctx, brokerID, s.instanceID, sessionID) + if err != nil { /* log */ return } + if !cleared { + // Another replica (or a newer session on this replica) already owns the + // socket. Do NOT mark offline — that would clobber the live owner. + slog.Info("broker reconnected elsewhere; skipping offline stamp", + "brokerID", brokerID, "staleSession", sessionID) + return + } + // We were the owner and nobody replaced us: mark offline + publish. + s.store.UpdateRuntimeBrokerHeartbeat(ctx, brokerID, store.BrokerStatusOffline) + ... // provider status updates + PublishBrokerDisconnected (unchanged) +}) +``` + +This is correct under A→B flap because the offline stamp is now gated on +"affinity still names *me* with *this* session". `HandleUpgrade` already closes+replaces +an existing **local** connection (`controlchannel.go:218`); the sessionID guard extends +that safety **across** replicas. + +> Note `Shutdown()` (`controlchannel.go:544`) deliberately nils `onDisconnect` to avoid +> touching the DB during teardown. Keep that — on graceful shutdown we intentionally do +> **not** clear affinity (the broker will reconnect and re-claim; a brief stale-but-dead +> affinity row is handled by the liveness check in §5.3). + +--- + +## 5. Component 2 & 3 — Command dispatch channel & command types + +### 5.1 Channel choice + +Single global channel **`scion_broker_cmd`** (not per-broker). Rationale: + +- Postgres channels have no wildcards; a per-broker channel + (`scion_broker_cmd_`) would require every replica to `LISTEN` on the channel of + every broker it *might* own — but a replica doesn't know which brokers will dial it + next, so it would have to LISTEN on all of them anyway. A single channel is simpler + and matches the `scion_ev_global` precedent. +- Volume is low (dispatch is human-paced lifecycle/message ops, not data-plane traffic). + One global channel is fine. Each node filters the signal by `ownsLocally(brokerID)`. + +A dedicated signal-listener goroutine (mirroring `runListener` in `events_postgres.go`) +LISTENs on `scion_broker_cmd`. On a signal for a broker it owns, it runs the reconcile +drain (§5.3). Implement as a sibling type **`PostgresCommandBus`** reusing the same +connect/reconnect/keepalive helpers (`connectListener`, `applyConnKeepalives`, +`nextBackoff`) — kept separate from `PostgresEventPublisher` so the event-fanout path and +the dispatch path are independently testable and pooled. + +### 5.2 Intent lives in the DB; the NOTIFY is a tiny signal + +Per the state-machine model (§2.0.1), the command **payload is not carried in the +NOTIFY**. The durable intent is written to the DB; the NOTIFY only says "broker X has +pending work, whoever owns it should reconcile." + +**NOTIFY `scion_broker_cmd` payload — a signal, not a command:** +```jsonc +{ "broker_id": "uuid", "kind": "dispatch" } // optional "cmd_id" for log correlation +``` +Tiny, never near the 8000-byte cap, never carries secrets. If the payload is ever lost +(LISTEN reconnect gap), correctness is unaffected — the intent is still in the DB and is +picked up by the next reconcile (NOTIFY-loss is just latency, not loss). + +**Durable intent — two tables:** + +1. **Messages reuse their existing row.** `store.Message` is already persisted *before* + dispatch today. Add a `dispatch_state` (`pending|dispatched|failed`) + + `dispatched_at`. No duplication; the message *is* the durable intent. + +2. **Lifecycle uses a new `broker_dispatch` intent table:** + +```sql +CREATE TABLE broker_dispatch ( + id UUID PRIMARY KEY, + broker_id UUID NOT NULL, + agent_id UUID, -- null for project-scoped ops + agent_slug TEXT, + project_id UUID, + op TEXT NOT NULL, -- start|stop|restart|delete|finalize_env|check_prompt|create + args TEXT, -- JSON; env/secrets/inlineConfig live here (see note) + state TEXT NOT NULL, -- pending|in_progress|done|failed + result TEXT, -- JSON; for ops that return data (check_prompt, env-gather) + claimed_by TEXT, -- hub instanceID that reconciled it + attempts INT NOT NULL DEFAULT 0, + error TEXT, + created_at TIMESTAMPTZ NOT NULL DEFAULT now(), + updated_at TIMESTAMPTZ NOT NULL DEFAULT now(), + deadline_at TIMESTAMPTZ +); +CREATE INDEX broker_dispatch_pending_idx ON broker_dispatch (broker_id, state); +``` + +Notes: +- `args` holds the bulky/secret-bearing fields (`resolvedEnv`, `resolvedSecrets`, + `inlineConfig`, structured message bodies). They sit in a DB column, **not** in a + NOTIFY payload — so secrets never appear in PG NOTIFY logs, and there is no 8000-byte + limit to work around (the §6 oversized-offload concern for commands disappears + entirely; offload remains only for the *event* path). On Postgres these can later + become `JSONB` per strategy §6.4; `TEXT` keeps SQLite parity for now. +- `deadline_at` lets a late reconciler drop a command the caller already abandoned. +- Atomic publish: the intent row INSERT and the NOTIFY are issued in **one transaction** + via `PublishTx` (events_postgres.go:236) — the signal is delivered only if the intent + commits. + +### 5.3 Routing decision — `HybridBrokerClient` + +Because there is no hub-to-hub addressing (§2.0), routing is **not** "find the owner and +send to it". It is "run locally if I hold the socket, otherwise **broadcast** and let the +holder self-select". Affinity is consulted only to *fast-fail* — to avoid waiting out a +timeout when we can already tell nobody owns the broker. + +```go +func (c *HybridBrokerClient) route(ctx, brokerID) routeDecision { + if c.controlChannel.manager.IsConnected(brokerID) { + return routeLocal // I hold the socket → tunnel directly (unchanged) + } + // I don't hold it. Some OTHER node might. We cannot address that node, so: + owner, alive := c.affinity.Lookup(ctx, brokerID) // reads runtime_brokers (hint only) + switch { + case owner != "" && alive: + return routeForward // NOTIFY-broadcast; the holder self-selects + case brokerEndpointSet: // direct-mode broker (hub→broker HTTP, not hub→hub) + return routeHTTP // existing fallback; rare under NAT'd deployments + default: + return routeUndeliverable // no owner & no endpoint → typed retryable error + } +} +``` + +Important: `routeForward` writes the **durable intent** (a `broker_dispatch` row, or a +`message.dispatch_state=pending`) and NOTIFYs the global `scion_broker_cmd` channel in +one transaction; *every* node receives the signal but only the socket-holder reconciles. +The affinity lookup is a **hint** that *a* node owns the broker, so we should write intent ++ signal (and wait for the resulting state transition) rather than fast-fail. Even if the +hint is stale, correctness holds: a wrong "alive" costs one timeout (the intent stays +durable and reconciles later); a wrong "dead" is reaped by §7.1. + +**Durability backstop — reconcile-on-connect.** Independent of any NOTIFY, when a broker +(re)connects to a node (`markBrokerOnline` / claim), that node runs a drain: +`SELECT … FROM broker_dispatch WHERE broker_id=$X AND state='pending'` plus pending +messages, and reconciles them. So even if *no* node owned the broker when the intent was +written (broker was down, or every NOTIFY was missed), the work executes the moment a +node next owns the socket. This is what makes the design durable + at-least-once without +a separate work queue (§2.0.1). + +"alive" = the owning node is believed up. Since we can't ping it (no hub-to-hub HTTP), +liveness is inferred from `last_heartbeat` freshness on the broker row (the broker's own +HTTP heartbeat lands on any node and keeps the row fresh while the tunnel is up), backed +by the command timeout. This is OQ3: confirm `last_heartbeat`-freshness + timeout is +sufficient for v1 (recommended), given a dedicated `hub_instances` table buys us nothing +without hub-to-hub addressing. + +### 5.4 Command types (in scope) + +Each command is: **write durable intent → NOTIFY signal → owner reconciles → originator +observes the resulting DB-state transition.** The "observe" column is always an existing +or DB-backed state change, never a bespoke RPC reply. + +| `op` | Durable intent | Owner reconcile (local tunnel) | Originator observes | +|---|---|---|---| +| `message` / broadcast / `set[]` | `message.dispatch_state=pending` (row already exists) | `MessageAgent`; set `dispatched_at` | fire-and-forget: return 202 once intent is durable (§6.1) | +| `start` | `broker_dispatch{op:start, args}` | `StartAgent`; set phase `Starting→Running` + `PublishAgentStatus` (today's behavior) | `agent..status` phase ∈ {running, error} | +| `stop` | `broker_dispatch{op:stop}` | `StopAgent`; set phase `Stopped` + publish | `agent..status` phase ∈ {stopped, error} | +| `restart` | `broker_dispatch{op:restart, args}` | `RestartAgent` | phase ∈ {running, error} | +| `delete` | `broker_dispatch{op:delete, args}` | `DeleteAgent` (idempotent; 404 ok); set state=done | `agent.deleted` event (already published cross-node) or dispatch state=done | +| `finalize_env` | `broker_dispatch{op:finalize_env, args}` | `FinalizeEnv`; write `result`; set phase | dispatch state→done (+ `result`) and/or phase | +| `check_prompt` / `create-with-gather` | `broker_dispatch{op:…, args}` | run tunnel; write `result` (bool / env-requirements) | dispatch state→done; originator reads `result` | + +All in-scope ops map to a **DB-state transition** the originator can observe. The two +data-returning create-time ops (`check_prompt`, `create-with-gather`) write their result +into `broker_dispatch.result` — the result is *state*, consistent with the model, and is +durable/re-readable rather than a fire-once reply. **No separate `cmd-ack` RPC event and +no command-body offload are needed** (both removed from the earlier draft). + +> **exec, logs, PTY are out of scope** (§10) — they are interactive request/response or +> streaming, not state transitions, and per maintainer are deferred. + +--- + +## 6. Component 4 — Response = observing the DB-state transition + +The originator never waits on a point-to-point reply. It **subscribes to the existing +event stream** (which already fans DB-state changes across nodes via +`PostgresEventPublisher`) *before* writing intent, then waits for the transition. + +### 6.1 Fire-and-forget (message, broadcast, set[]) + +The message row is durable *before* dispatch (today, and still). So: +- Originator writes `dispatch_state=pending` + NOTIFY (one tx), returns **202** at once. +- Owner reconciles: tunnels the message, sets `dispatch_state=dispatched, dispatched_at`. +- **Loss visibility** (replaces today's silent split-brain): a sweep flags any row stuck + `pending` beyond T. Because the row is durable, a broker that was offline gets its + messages on reconnect-drain (§5.3) — they are delayed, never dropped. This is strictly + better than today, where an undeliverable message surfaced as a *synchronous* error + *after* the row + SSE were already written. + +### 6.2 Lifecycle via the 3-layer agent state (start, stop, restart) — reuses existing events + +The owner already calls `PublishAgentStatus` after dispatch +(handlers.go:1192/2345/2828), which NOTIFYs `agent..status` carrying the existing +**3-layer state** (events.go:97): `Phase` (top-level lifecycle), `Activity` (what the +agent is doing — "building", "pulling image", "waiting"), and `Detail` (untyped free-text +for broker/runtime-specific interim states). All three already cross nodes via the event +layer. The originating node watches **any change to the agent record**, not just the +terminal phase: + +1. Originator `Subscribe("agent."+agentID+".status")` **before** writing intent. +2. Write `broker_dispatch` intent + NOTIFY (one tx). +3. Loop over `AgentStatusEvent`s: + - **Any** change (phase/activity/detail) → forward progress: surface to the caller and + **reset the rolling timeout** (§6.4). + - **Terminal** phase (start/restart: {running, error}; stop: {stopped, error}) → done. +4. `error` phase → return the agent's `Message`. Rolling-timeout expiry (no update within + the window) → dispatch **failed** (§6.4); the originator marks the outcome and returns. + +> **Semantics preserved (OQ2 confirmed):** "done" == the owner accepted the command and +> published the resulting phase — *not* waiting for the harness to report truly-ready. +> The owner runs the local accept-and-publish sequence; the originator observes it. + +### 6.3 Lifecycle/create ops without a phase (delete, finalize_env, check_prompt, gather) + +Observe the **`broker_dispatch` row reaching a terminal state**. A slim completion event +`broker.dispatch.` (subject → `scion_ev_global`, reuses the existing publisher) is +emitted by the owner when it sets state `done|failed`; the originator subscribed to it +before writing intent and reads `result`/`error` from the row on wake. Because the +**authoritative result is the DB row**, a missed event is recoverable (bounded re-read at +timeout) — no point-to-point reply to lose. `delete` may instead observe the existing +`agent.deleted` event. + +### 6.4 Rolling timeout on the 3-layer state (OQ2/OQ6 — resolved per maintainer + coordinator) + +> Maintainer + coordinator: long providers (GKE pod cold-start; future runtimes) sit in +> schedule→image-pull→init for minutes, so a fixed wall-clock timeout is wrong. Instead: +> **a rolling timeout that resets on each interim state update.** Brokers are expected to +> update the sub-state (`activity`/`detail`) within the window; if a step needs longer the +> broker runs its own timer loop to keep emitting heartbeat-style `detail` updates. If no +> update arrives within the window, the dispatch is considered **failed**. Interim states +> are **untyped** (free-text `detail`) — no canonical sub-state set to define. + +This is the whole model — it replaces the earlier inactivity-bound + absolute-cap + +provider-config machinery: + +``` +window := dispatchRollingTimeout // single tunable; reset on ANY agent-record change +loop: select { + case ev := <-sub: // ANY change to phase/activity/detail + if terminal(ev.Phase) { return ev } // running | stopped | error → done + reset(window) // forward progress (incl. a detail heartbeat) + case <-window: return ErrDispatchFailed // broker went silent → FAILED + case <-ctx.Done(): return ctx.Err() +} +``` + +Properties: +- **Liveness-based, not duration-based.** A 10-minute GKE start succeeds as long as the + broker keeps updating `detail`; a broker that dies mid-step fails fast (within one + window), regardless of how long the step "should" take. +- **The broker owns the heartbeat.** A slow step (e.g. image pull) means the broker's own + timer loop emits periodic `detail` updates ("pulling image… 40%"). This pushes the + liveness contract to where the knowledge is, and needs no provider-specific config in + the hub. +- **One knob.** `dispatchRollingTimeout` (a single default, e.g. 60–90s) rather than + per-provider bounds. Providers express "I need longer" by *keeping the heartbeat going*, + not by configuring a number. +- **Cross-node for free.** The waiting node (Hub B) watches the same `agent..status` + events every node already receives; the owning node (Hub A) just keeps publishing the + 3-layer state as it does today. +- **Failure is authoritative.** On window expiry the originator marks the outcome + (`broker_dispatch.state=failed` / agent `phase=error`) and returns failure. Because a + well-behaved broker heartbeats while working, silence genuinely means stuck/dead. + +> **Long-poll caveat (note, not a blocker):** a multi-minute synchronous request can +> exceed an L7 LB idle timeout *on the dispatch connection itself* (interim updates flow +> on the SSE/event stream, not on the blocked dispatch socket). If that bites, the op can +> return 202 once intent is durable + first update seen, and the client watches +> `agent..status` for the terminal phase — same state, just observed by the client +> instead of the hub. Flagged for implementation; not required by this design. + +| Op | Wait model | +|---|---| +| message / broadcast | none — return 202 on durable intent | +| start / restart | rolling timeout on the agent record; terminal phase → done; window expiry → failed | +| stop / delete | rolling timeout (typically one window) — terminal phase / `agent.deleted` | +| finalize_env / check_prompt / gather | rolling timeout — `broker_dispatch` terminal state + `result` | + +--- + +## 7. Error handling & edge cases + +Because intent is **durable**, *messages* degrade to added latency (never lost), while +*lifecycle* ops follow the rolling-timeout contract (silence ⇒ failed + retryable). + +| Case | Behavior | +|---|---| +| **Rolling-timeout expiry** (no agent-record update within the window — broker stuck/dead mid-step) | The in-flight dispatch is **failed**: originator marks `broker_dispatch.state=failed` / agent `phase=error` and returns 503. This is the §6.4 contract — a well-behaved broker heartbeats `detail` while working, so silence is genuine failure. | +| **No node owns the broker** (broker offline) | Intent is written `pending` and persists. **Message:** return 202 — delivered on reconnect-drain (§5.3), never dropped. **Lifecycle:** originator can see the broker is offline up front (affinity/heartbeat) and return retryable immediately rather than wait a full window; the `pending` intent may be reaped (§7.1) or left for reconnect-drive per op. | +| **Owner believed alive but is actually dead** (crashed without clearing affinity) | No status updates reach the originator → rolling window expires. **Message:** stays `pending`, reconciled when a node next owns the socket. **Lifecycle:** failed + retryable (above). Stale affinity reaped by §7.1. | +| **Owner alive but socket just dropped there** | Reconciler sees `ownsLocally==false` → ignores. Intent stays `pending`; broker re-dials and the new owner drains it (message) or the user retries (lifecycle). | +| **Two nodes both think they own it** (flap mid-signal) | The `broker_dispatch` claim is a CAS (`state pending→in_progress WHERE state='pending'`), so exactly one node executes a given intent. Messages: dedupe on `dispatch_state` CAS likewise. No double-execution. | +| **NOTIFY/intent write fails** (pool saturated) | It's one transaction (`PublishTx`): either both the intent row and the signal commit, or neither. On failure the handler returns 503 retryable with **no partial state**. Bounded by `publishTimeout` (5s). | +| **Large args** (env/secrets/inlineConfig) | Live in the `broker_dispatch.args` DB column — no NOTIFY size limit, no payload-offload table, no secrets in PG logs. | +| **Completion/phase event missed** (subscriber buffer overflow or originator crash) | The authoritative result is the **DB row** (phase / `dispatch_state` / `broker_dispatch.state`+`result`). On timeout the originator may re-read it; the command itself already ran. At-least-once; all in-scope ops are idempotent (start/stop/restart/delete are broker-idempotent — `DeleteAgent` allows 404; message dedupes on `dispatch_state`). | +| **Reconcile runs after the caller gave up** (`deadline_at` passed) | Reconciler may skip (lifecycle) or still deliver (message — better late than never). Correctness relies on the originator's own timeout, not the reconciler's clock. | +| **Completion event lands on a non-originating node** | Harmless — the event is broadcast to all nodes via `scion_ev_global`; only the node with a live `Subscribe` for that agent/dispatch matches; others ignore it. | + +### 7.1 Stale-affinity reaping + +A recurring **singleton** job (reuse `RegisterRecurringSingleton` / +`pg_try_advisory_lock`, precedent `server.go:1858`) clears `connected_hub_id` for +brokers whose `last_heartbeat` is older than `2 × heartbeatInterval` AND whose +`connected_hub_id` is non-NULL. This bounds how long a crashed owner's affinity misleads +`route` into `routeForward` (after which `route` falls to `routeUndeliverable`, i.e. a +durable `pending` intent + retryable status). The same job can mark `broker_dispatch` +rows stuck `in_progress` past `deadline_at` back to `pending` (re-drive) or `failed`. + +### 7.2 Routing order (summary) + +``` +local socket (tunnel) ── fastest, unchanged + └─ else a node owns it → write durable intent + NOTIFY signal → owner reconciles + └─ else broker.Endpoint set → HTTP (direct-mode brokers; existing; rare under NAT) + └─ else → write durable intent (pending) + retryable status + → reconciled on broker reconnect-drain (never silent) +``` + +The HTTP tier is retained for direct-mode brokers (`Endpoint` set, reachable hub→broker — +distinct from the nonexistent hub→hub path). Whether any production broker uses it is OQ1; +under pure NAT it is never taken. + +--- + +## 8. Data model & migration summary + +```sql +-- 1. Broker affinity (fixes the disconnect race; hint for routing). +ALTER TABLE runtime_brokers + ADD COLUMN connected_hub_id TEXT, + ADD COLUMN connected_session_id TEXT, + ADD COLUMN connected_at TIMESTAMPTZ; +-- lock_version already present and CAS-looped. + +-- 2. Durable lifecycle/create intent (the state machine). +CREATE TABLE broker_dispatch ( … ); -- see §5.2 + +-- 3. Message delivery state (messages are already durable rows). +ALTER TABLE messages + ADD COLUMN dispatch_state TEXT NOT NULL DEFAULT 'pending', -- pending|dispatched|failed + ADD COLUMN dispatched_at TIMESTAMPTZ; +``` + +- Ent: add the affinity fields to `pkg/ent/schema/runtimebroker.go`, a new + `BrokerDispatch` schema, and the message fields — all dialect-neutral (`TEXT`/ + `TIMESTAMPTZ`, no Postgres-only annotations) per strategy §6.4. +- Store model: add fields to `store.RuntimeBroker` (`models.go:281`) and `store.Message`; + add `BrokerDispatch` model + store methods (insert/claim-CAS/complete/drain). +- New NOTIFY channel `scion_broker_cmd` (no DDL; channels are ephemeral). The existing + `scion_event_payloads` table is **not** needed by dispatch (args live in + `broker_dispatch.args`); it stays in use only by the event path. +- New in-memory `Server.instanceID`. + +No SQLite-path behavior changes: single-process SQLite always takes the local fast path +(`IsConnected==true`), so the intent tables are written-through but routing never forwards +and the reconcile-drain simply runs locally. The affinity columns still fix the +disconnect race harmlessly. + +--- + +## 9. Sequence diagrams + +### 9.1 `message` (durable, fire-and-forget) — socket on Hub A, API on Hub B + +``` +User→LB→Hub B: POST /agents/{id}/message +Hub B: BEGIN tx: persist Message (dispatch_state=pending) + + PublishUserMessage (SSE) [unchanged, cross-node] + + NOTIFY scion_broker_cmd{broker_id:X} [signal only] COMMIT +Hub B: 202 Accepted ───────────────────────────────────────► User (immediate, durable) +Hub A: signal-listener wakes; ownsLocally(X)=yes +Hub A: drain: SELECT messages WHERE broker_id=X AND dispatch_state='pending' +Hub A: CAS dispatch_state pending→dispatched; MessageAgent (local tunnel) → broker → agent + (broker offline at notify time? → no owner acts; row stays pending; + delivered when X reconnects and its new owner runs the same drain — never lost) +``` + +### 9.2 `start` — observe phase, with intermediate sub-states (long provider, e.g. GKE) + +``` +User→LB→Hub B: POST /agents/{id}/start +Hub B: Subscribe("agent.{id}.status") [BEFORE writing intent] +Hub B: BEGIN tx: INSERT broker_dispatch{op:start, args, state=pending} + + NOTIFY scion_broker_cmd{broker_id:X} COMMIT +Hub A: signal-listener; ownsLocally=yes; CAS-claim dispatch row pending→in_progress +Hub A: StartAgent local tunnel → broker accepts; mark dispatch done +Hub A: broker/provider advances; each step → PublishAgentStatus ── NOTIFY agent.{id}.status ─┐ + phase=starting; activity="pulling image"; detail="… 40%" (broker heartbeats detail) │ +Hub B: <-sub: ANY change (phase/activity/detail) → surface to caller + RESET rolling window ◄─┤ +Hub B: <-sub: phase==running (terminal) → 200 OK ◄──────────────────────────────────────────┘ + phase==error → 502 + agent.Message + no update within rolling window → dispatch FAILED: mark phase=error / dispatch.state=failed, 503 + (broker is expected to keep emitting detail while working; silence ⇒ stuck/dead) +``` + +### 9.3 `check_prompt` / env-gather (data result, no phase) + +``` +Hub B: Subscribe("broker.dispatch.{id}") [BEFORE writing intent] +Hub B: BEGIN tx: INSERT broker_dispatch{op:check_prompt, state=pending} + + NOTIFY scion_broker_cmd{broker_id:X} COMMIT +Hub A: ownsLocally=yes; CAS-claim; run local tunnel → result (bool / env-requirements) +Hub A: UPDATE broker_dispatch SET state=done, result=… ; Publish broker.dispatch.{id} +Hub B: <-sub ; read result from the dispatch row → return to caller + (event missed? re-read row at timeout — result is authoritative DB state) +``` + +### 9.4 Broker flap A→B (disconnect race fix) + +``` +t0 broker X socket on Hub A: connected_hub_id=a9c3, session=s1 +t1 LB reshuffle; X re-dials, lands on Hub B +t2 Hub B HandleUpgrade(session=s2); ClaimRuntimeBrokerConnection(X, b2f1, s2) + → row now (b2f1, s2), status=online +t3 Hub A's old socket finally errors; onDisconnect(X, s1) + → ReleaseRuntimeBrokerConnection(X, a9c3, s1): affinity is (b2f1,s2) ≠ (a9c3,s1) + → cleared=false → SKIP offline stamp ✅ (today this clobbered B's online) +``` + +--- + +## 10. Out of scope (maintainer-confirmed): PTY, logs, exec + +These are **not** part of this work item. They do not fit "DB as state machine": + +- **PTY / interactive streams** (`OpenStream`/`SendStreamData`/`ResizeStream`/ + `CloseStream`) — high-frequency, ordered, back-pressured bytes. NOTIFY is wrong for + this (8000B cap, no flow control, fan-out to all nodes). +- **Logs** (`GetAgentLogs`) and **exec** (`ExecAgent`) — request/response bodies / + streaming, not state transitions. + +Why they can't simply reuse this design: the response/stream must originate from the +*owning* node, and **there is no hub-to-hub transport** (§2.0) to relay it. So the only +viable future approach is **sticky client routing** — terminate the user's stream on the +owning node directly: + +- **LB session affinity** keyed so the PTY/logs/exec client lands on the node that owns + the broker (e.g. cookie/route keyed to broker or agent), **or** +- introduce **hub addressability** (a `hub_instances(instance_id, endpoint, last_seen)` + table + reachable internal endpoints) so an entry node can reverse-proxy the + upgrade/stream to the owner. This is a larger change explicitly deferred. + +Until one of those lands, PTY/logs/exec work only when the client happens to hit the +owning node. **Document as a known multi-node limitation; gate "full multi-node GA" on a +separate streaming design.** + +--- + +## 11. Open questions for the maintainer (@ptone) + +Asked one at a time per protocol; answers folded back into this doc as received. + +- **OQ1 — RESOLVED (2026-06-02).** Maintainer reframed: there is **no hub-to-hub HTTP**; + broker tunnels are sticky to an arbitrary node; **NOTIFY is the only inter-node + transport**. Folded into §2.0. (Whether direct-mode `broker.Endpoint` brokers exist at + all is a minor optimization; the design does not depend on it.) + +- **Scope — RESOLVED (2026-06-02).** Maintainer: **message + agent lifecycle only**; + **PTY, logs, exec out of scope**; model is **"DB as state machine, NOTIFY as the + communications channel."** Folded into §2.0.1, §5, §6, §10. + +- **OQ4 (durability) — RESOLVED by the state-machine model.** Intent is durable in the + DB and reconciled on broker reconnect-drain (§5.3), so an owner being down delays but + never loses a command. No separate ephemeral queue. *Confirm this is the intended + durability bar* (vs. also persisting a per-attempt audit log). + +- **OQ5 (loss visibility) — RESOLVED by the state-machine model.** Messages carry + `dispatch_state` + `dispatched_at` on the existing row; lifecycle carries + `broker_dispatch.state`. Sweep in §7.1. *Confirm column placement is acceptable.* + +- **OQ2 — RESOLVED (2026-06-02).** Contract confirmed (owner accepts + publishes phase + = done; not harness-ready). Timeouts: long providers (GKE, future runtimes) need more + time + interim feedback, handled by a **rolling timeout on the existing 3-layer agent + state** that resets on each interim update (see OQ6 below for the full resolution). + Folded into §6.2, §6.4, §9.2. + +- **OQ6 — RESOLVED (2026-06-02, maintainer + coordinator).** Reuse the existing 3-layer + state (phase/activity/**detail**); interim states are **untyped** free-text in `detail` + (no canonical set to define). Timeout is a **rolling window that resets on each interim + update**; brokers heartbeat `detail` (own timer loop) while a step runs; no update within + the window ⇒ dispatch **failed**. Folded into §6.2, §6.4, §9.2. (Async-202 noted only as + an LB-idle escape hatch, §6.4.) + +Still genuinely open (lower stakes; sensible default proposed — non-blocking): + +- **OQ3 (liveness signal):** Is `last_heartbeat`-freshness + the rolling dispatch timeout + sufficient to decide "a node owns this broker" for v1 (recommended), or introduce a + `hub_instances` heartbeat table now? (Note: `hub_instances` buys nothing for *this* + scope without hub-to-hub HTTP; it only pays off for the deferred PTY/logs/exec work in + §10, so deferring it with that work is the natural call.) + +--- + +## 12. Implementation sequencing (suggested) + +1. **Phase 1 — affinity + race fix (independently shippable).** Per-process + `instanceID`; affinity columns on `runtime_brokers`; `ClaimRuntimeBrokerConnection` / + `ReleaseRuntimeBrokerConnection` (CAS compare-and-clear); thread `sessionID` through + `markBrokerOnline` and the `onDisconnect(brokerID, sessionID)` callback. **Fixes the + disconnect-race correctness bug today**, with no dependency on dispatch. +2. **Phase 2 — state-machine substrate.** `broker_dispatch` table + store methods + (insert / CAS-claim / complete / drain) and `messages.dispatch_state`/`dispatched_at`. + `PostgresCommandBus`: a listener on `scion_broker_cmd` reusing the events_postgres + connect/keepalive/reconnect helpers; the **reconcile-on-connect drain** wired into + `markBrokerOnline`. +3. **Phase 3 — message dispatch.** `route` in `HybridBrokerClient`; transactional + intent+signal for `message`/broadcast/`set[]`; owner drain → tunnel → mark dispatched. + (Fixes the originally-reported message split-brain.) +4. **Phase 4 — lifecycle dispatch.** `start`/`stop`/`restart`/`delete` via + `broker_dispatch` + phase/`agent.deleted` observation; then the create-time data ops + (`finalize_env`, `check_prompt`, `create-with-gather`) via `broker_dispatch.result` + + the `broker.dispatch.` completion event. +5. **Phase 5 — hardening.** Stale-affinity / stuck-`in_progress` reaper singleton; + `pending`-message sweep + metrics. +6. **Deferred — PTY / logs / exec.** Separate streaming design (§10). Out of scope. + +Phase 1 is independently shippable and fixes a real correctness bug today. Phases 2–3 +deliver the originally-reported message-dispatch fix; Phase 4 completes lifecycle. diff --git a/.design/decoupled-harness-implementation.md b/.design/decoupled-harness-implementation.md index 5eccd0263..e63991289 100644 --- a/.design/decoupled-harness-implementation.md +++ b/.design/decoupled-harness-implementation.md @@ -1,5 +1,12 @@ # Decoupled Harness Implementation: Script-Based Provisioning +> **Packaging follow-on complete.** The harness-config decoupling work +> ([`harness-config-decoupling.md`](./harness-config-decoupling.md)) relocated +> OpenCode, Codex, and Antigravity bundles to `harnesses//`, removed their +> Go embed/built-in implementations, and shrunk the default-install set to +> `{claude, gemini}`. Each bundle is now self-contained (config + provisioner + +> Dockerfile + Cloud Build config) under [`harnesses/`](../harnesses/README.md). + ## Motivation Today, every harness implementation lives as compiled Go code inside the scion binary (`pkg/harness/`). Each harness performs a similar set of operations — writing config files, injecting auth credentials, rewriting settings JSON/YAML/TOML — but the specifics are unique per harness. This means: diff --git a/.design/harness-config-decoupling.md b/.design/harness-config-decoupling.md new file mode 100644 index 000000000..e287dbbf9 --- /dev/null +++ b/.design/harness-config-decoupling.md @@ -0,0 +1,267 @@ +# Harness-Config Decoupling: Top-Level Bundle Directory & Opt-In Install + +**Status:** Draft plan — 2026-06-06 +**Owner:** harness-refactor agent (for ptone@google.com) +**Related:** [`decoupled-harness-implementation.md`](./decoupled-harness-implementation.md) (the container-script provisioning work this builds on) + +## Motivation + +Today every harness ships compiled into the scion binary and is **installed by +default**. `harness.All()` returns `{gemini, claude, opencode, codex}`, and +`scion init` / `scion server` startup seed each one's embedded config into +`~/.scion/harness-configs//` from `pkg/harness//embeds/`. + +We want to move to a model where **harnesses and their configs are not all +installed by default**. The first step is to: + +1. Establish a **new top-level harness-config directory at the repo root** that + holds harness bundles as plain on-disk artifacts (not Go embeds). +2. **Refactor OpenCode and Codex** out of `pkg/harness/*/embeds/` into that + directory. +3. **Port the Antigravity harness config** (from + [`ptone/scion-antigravity`](https://github.com/ptone/scion-antigravity)) into + that directory. + +The container-script migration (`decoupled-harness-implementation.md`, Phases +0–5) already did the hard part: Codex and OpenCode are fully declarative +(`config.yaml` + `provision.py`) and run their provisioning inside the agent +container. This plan is the **packaging/distribution** follow-on — it changes +*where the bundles live* and *whether they are installed automatically*, not how +they provision. + +## Current State (verified) + +| Concern | Where it lives today | +|---|---| +| Default-install set | `pkg/harness/harness.go::All()` → gemini, claude, opencode, codex | +| Default-install call sites | `cmd/project.go` (`scion init`), `cmd/templates.go` (`templates update-default`), `cmd/server_foreground.go` (`scion server`) — all call `harness.All()` | +| Seeding from embeds | `pkg/config/harness_config.go::SeedHarnessConfig()` walks `h.GetHarnessEmbedsFS()` | +| OpenCode bundle (embedded) | `pkg/harness/opencode/embeds/{config.yaml,opencode.json,provision.py}` + `pkg/harness/opencode/embeds.go` (`//go:embed`) | +| Codex bundle (embedded) | `pkg/harness/codex/embeds/{config.yaml,config.toml,scion_notify.sh,bashrc,provision.py}` + `pkg/harness/codex/embeds.go` | +| Built-in Go fallbacks | `pkg/harness/opencode.go`, `pkg/harness/codex.go` (+ `codex_config.go`), selected by `harness.New()` / `harness.Resolve()` | +| Opt-in install (already exists!) | `cmd/harness_config_install.go` → `scion harness-config install ` supports local dir, `github.com/...` shorthand, `file://`, `:gcs:`, and `.tgz`/`.zip` archives | +| Image builds | `image-build/{opencode,codex,claude,gemini}/Dockerfile`; DAG in `image-build/scripts/lib/targets.sh`; `image-build/cloudbuild-harnesses.yaml` | +| Antigravity source layout | `antigravity/{config.yaml,provision.py,dialect.yaml,skills/}` + root `Dockerfile` + `cloudbuild.yaml` | + +Key insight: **the opt-in install command already exists.** The work is mostly +about (a) relocating the bundles, (b) shrinking the default set, and (c) deciding +the fate of the Go embeds and built-in fallbacks. + +## Target State + +``` +/ + harnesses/ # NEW top-level harness-config directory + opencode/ + config.yaml + provision.py + Dockerfile # moved from image-build/opencode/ + cloudbuild.yaml # per-bundle image build + home/ + .config/opencode/opencode.json + README.md + codex/ + config.yaml + provision.py + Dockerfile # moved from image-build/codex/ + cloudbuild.yaml + home/ + .codex/config.toml + .codex/scion_notify.sh + .bashrc + README.md + antigravity/ + config.yaml + provision.py + dialect.yaml + Dockerfile # ported from ptone/scion-antigravity + cloudbuild.yaml + skills/.gitkeep + home/ + .gemini/... + README.md +``` + +Note: the bundle root now carries non-harness-config files (`Dockerfile`, +`cloudbuild.yaml`). `scion harness-config install` copies the whole directory, so +these get copied into `~/.scion/harness-configs//` too. That is harmless +(they're ignored at provision time) but the install/seed allowlist and +`ComputeHarnessConfigRevision` should be reviewed so image-build files don't +perturb the config revision hash — see Phase D.4. + +- `harness.All()` (default-install set) shrinks to **`{gemini, claude}`** (TBD — + see Decision 2). +- OpenCode / Codex / Antigravity become **opt-in**, installed with: + ``` + scion harness-config install harnesses/opencode # from a repo checkout + scion harness-config install github.com/GoogleCloudPlatform/scion/tree/main/harnesses/codex + ``` +- The `harnesses/` bundles are the **single source of truth** for these configs. + No duplicate copies under `pkg/harness/*/embeds/`. + +## Decisions (locked — ptone, 2026-06-06) + +1. **Directory name: `harnesses/`** at the repo root. +2. **Default-install set shrinks to `{claude, gemini}`.** OpenCode, Codex, and + Antigravity become opt-in bundles. +3. **Drop the Go entirely.** Remove both the embeds + (`pkg/harness/{opencode,codex}/embeds*`) **and** the built-in Go + implementations (`opencode.go`, `codex.go`, `codex_config.go`). The + `harnesses/` bundles become the sole source; OpenCode/Codex resolve purely as + container-script harnesses from an installed bundle. No built-in fallback is + retained. (This is more aggressive than the prior design's "keep fallback one + release" guidance — the parity oracle goes away, so the relocated bundles must + be locked down with golden/install tests first; see Phase A.4 and Risks.) +4. **Co-locate `Dockerfile` + cloudbuild file inside each bundle.** Each + `harnesses//` is self-contained (config + provisioner + image build), + matching the antigravity repo layout. The centralized `image-build/{opencode, + codex}/` dirs are removed and the build DAG/cloudbuild wiring is repointed at + the bundle dirs. +5. **Keep first-party bundles in this repo** under `harnesses/` for now (no split + into separate repos this phase). + +## Implementation Plan + +Decisions locked above. Steps are ordered to keep the tree green at each commit; +the destructive Go removal (Phase D) lands only after the relocated bundles are +proven (Phase A.4). + +### Phase A — Establish `harnesses/` and relocate the OpenCode/Codex bundles + +1. Create top-level `harnesses/` with `opencode/` and `codex/` subdirs. +2. Move the embedded bundle files into the new layout, converting the implicit + `mapEmbedFileToHomePath` placement into an **explicit `home/**`** layout + (the prior design's preferred end state, §"File Seeding and Packaging + Changes"): + - OpenCode: `opencode.json` → `harnesses/opencode/home/.config/opencode/opencode.json`; `config.yaml`, `provision.py` at bundle root. + - Codex: `config.toml` → `home/.codex/config.toml`; `scion_notify.sh` → `home/.codex/scion_notify.sh`; `bashrc` → `home/.bashrc`; `config.yaml`, `provision.py` at root. +3. Move the image build into each bundle (Decision 4): `image-build/opencode/Dockerfile` + → `harnesses/opencode/Dockerfile`, same for codex; add a per-bundle + `cloudbuild.yaml` (extract the opencode/codex steps from + `image-build/cloudbuild-harnesses.yaml`, threading `BASE_IMAGE` from + `scion-base`). +4. **Lock down behavior before deleting the Go oracle.** Capture golden output + from the existing built-in + container-script paths (command construction, + seeded file layout, provision staging) as fixtures, and add a CI smoke test: + `scion harness-config install harnesses/ --name -test` → + `scion harness-config show -test` → assert config parses and a dry + provision stages the expected bundle. This replaces the parity oracle that + Decision 3 removes. +5. Add a `README.md` per bundle (purpose, `install` command, auth modes, image + build) — mirror the antigravity repo's README. + +### Phase B — Port Antigravity + +1. Copy `antigravity/{config.yaml,provision.py,dialect.yaml,skills/}` plus the + root `Dockerfile` and `cloudbuild.yaml` from `ptone/scion-antigravity` into + `harnesses/antigravity/` (Decision 4 keeps image build in-bundle). +2. Reconcile `config.yaml` against the current `HarnessConfigEntry` schema and + `ValidateHarnessConfig`. The antigravity config exercises fields a relocated + first-party bundle may not have: the top-level `mcp:` global-config mapping + block, `dialect.yaml`, and `oauth-token` / `vertex-ai` auth types (the latter + with an empty `vertex-ai: {}` body). Confirm the in-repo schema accepts all of + them; add schema support for any rejected field before merging. +3. The antigravity image needs keyring packages (`gnome-keyring`, `libsecret`) + not in `scion-base` — its `Dockerfile`/`cloudbuild.yaml` already encode the + `core-base → scion-base → antigravity` chain; verify they reference the + in-repo base image tags rather than the external repo's registry. +4. Confirm `ContainerScriptHarness.Provision` stages `dialect.yaml` (it does, + `container_script_harness.go:342`). + +### Phase C — Shrink the default-install set + +1. Change `harness.All()` to return `{GeminiCLI, ClaudeCode}` (Decision 2). +2. Audit the three call sites (`cmd/project.go`, `cmd/templates.go`, + `cmd/server_foreground.go`) — confirm none assume opencode/codex presence. +3. Update tests that assert the 4-harness default (e.g. + `pkg/config/init_test.go`, `templates_test.go`). + +### Phase D — Drop the Go (embeds + built-in implementations) + +Decision 3 — remove entirely, no fallback. Land this after Phase A.4 proves the +relocated bundles. + +1. Delete `pkg/harness/opencode/` (embeds + `embeds.go`), `pkg/harness/codex/` + (embeds + `embeds.go`). +2. Delete `pkg/harness/opencode.go`, `pkg/harness/codex.go`, + `pkg/harness/codex_config.go`, and their `_test.go` + `*_parity_test.go` + files (the parity tests compared against the now-removed built-in oracle; + their coverage moves to the Phase A.4 install/golden tests). +3. Remove the `codex`/`opencode` cases from `harness.New()` and + `harness.newBuiltin()` so resolution flows: container-script (installed + bundle) → declarative-generic. With no bundle installed, `--harness codex` + falls to `Generic` — acceptable now that they're opt-in (surface a clear + "not installed; run scion harness-config install" hint where practical). +4. Review the install/seed allowlist and `ComputeHarnessConfigRevision` so the + newly co-located `Dockerfile`/`cloudbuild.yaml` in each bundle don't break + provisioning or destabilize the revision hash (either exclude them, or accept + them as part of the hash deliberately). +5. `scion harness-config reset codex` currently restores *embedded* defaults via + `harness.New` — with embeds gone it must change. Repoint `reset` to fail + clearly with "reinstall from bundle: scion harness-config install + harnesses/codex" guidance (and update its tests). +6. Remove `image-build/opencode/` and `image-build/codex/` and repoint the build + DAG (`image-build/scripts/lib/targets.sh`) + `cloudbuild-harnesses.yaml` at + the bundle dirs (or split codex/opencode out of the centralized `harnesses` + target entirely, since their images are now bundle-local). + +### Phase E — Discoverability & docs ✓ + +1. [x] Add `harnesses/README.md` indexing available bundles + install commands. +2. [x] Update `image-build/README.md` (image hierarchy no longer lists + opencode/codex centrally), top-level `README.md`, and + `decoupled-harness-implementation.md` cross-references. +3. [x] Verified web UI harness fallback lists in `agent-create.ts` and + `project-settings.ts` — they enumerate known/installable harnesses (incl. + opt-in ones), not the default-install set; left as-is with clarifying + comments. +4. `scion harness-config list --available` deferred — out of scope for this PR; + noted as follow-up in `harnesses/README.md`. + +### Phase F — Migration for existing installs + +Existing machines already have `~/.scion/harness-configs/{opencode,codex}/` +seeded. Shrinking defaults and dropping embeds must **not** delete a user's +installed config. + +1. `scion init`/upgrade must leave existing installed configs untouched + (additive-only upgrade is already the contract — + `decoupled-harness-implementation.md` §"Existing Installation Upgrade Plan"). +2. Existing codex/opencode configs keep resolving as container-script harnesses + from their on-disk dir (they already declare `provisioner.type: + container-script`), so removing the Go built-in does not break them — **but** + any legacy config still on `provisioner.type: builtin` would break. Add an + upgrade check that flags/auto-activates such configs (`--activate-script`) + before the built-in is removed. +3. Document that fresh installs no longer get opencode/codex automatically, plus + the one-line `harness-config install` to restore them. No agent-home + rewrites; already-created agents keep working. + +## Risks & Open Questions + +- **No more parity oracle (Decision 3).** Removing the built-in Go + implementations deletes the reference behavior the parity tests checked + against. Phase A.4 golden + install tests must land *first* and be trusted. +- **Legacy `provisioner.type: builtin` configs break** once the Go built-in is + gone (Phase F.2). Needs an upgrade/auto-activate safety net. +- **`reset` semantics change** (Phase D.5) — agree on the replacement + (reinstall-from-bundle hint). +- **Image-build files inside config bundles** (Decision 4) mean + `harness-config install`/sync copies `Dockerfile`/`cloudbuild.yaml` into the + installed config dir and into Hub artifacts. Confirm that's acceptable and + doesn't perturb `ComputeHarnessConfigRevision` (Phase D.4). +- **Hub-distributed configs**: brokers install on demand so are unaffected, but + the Hub's own seed/catalog may assume the 4-harness set — audit + `pkg/runtimebroker` + Hub harness-config endpoints. +- **Antigravity schema gaps**: the ported `config.yaml` may use fields the + in-repo validator hasn't accepted from a first-party bundle (MCP mapping + block, empty `vertex-ai` type). Phase B.2 must validate before merging. +- **Web UI / templates** that list harnesses (`web/`, `cmd/templates.go` + template harness-configs) may hard-code the 4 names — grep before shipping. + +## Out of Scope (for this phase) + +- Migrating Claude/Gemini to container-script bundles (that's + `decoupled-harness-implementation.md` Phase 6). +- Splitting first-party bundles into standalone repos (Decision 5, deferred). +- A full remote harness catalog / marketplace. diff --git a/.design/nfs-workspace-phase3-cloudrun.md b/.design/nfs-workspace-phase3-cloudrun.md new file mode 100644 index 000000000..b54de4bb3 --- /dev/null +++ b/.design/nfs-workspace-phase3-cloudrun.md @@ -0,0 +1,77 @@ +# NFS Workspace — Phase 3 (Cloud Run + Filestore-CSI) Design Note + +**Status:** Documentation deliverable (N3-1 + N3-2). **No code in this phase.** +**Why doc-only:** Verified against `postgres/wave-b-integration` — Scion has **no Cloud Run +runtime** (`pkg/runtime/factory.go` supports `container`/`docker`/`podman`/`kubernetes` only; +no `run.googleapis.com`/knative anywhere). There is no Cloud Run Service/Job spec to attach an +NFS volume to, so N3-1 "emit an NFS volume in the Cloud Run spec" cannot land as code until a +Cloud Run runtime exists. Building that runtime is a separate, larger effort outside the NFS +plan's scope. This note records the realization design so it's a config/wiring change, not a +redesign, when a Cloud Run runtime is added. (Companion: `nfs-workspace.md` §5.4/§9.4.) + +--- + +## N3-1 — Cloud Run NFS workspace realization (design, for when a Cloud Run runtime exists) + +Cloud Run (gen2 execution environment) supports NFS volume mounts (incl. Filestore). When a +Cloud Run runtime is added to `pkg/runtime`, NFS realization should mirror the Docker/K8s +backends already shipped (Wave 1/2): + +- **Selection:** reuse `SelectWorkspaceBackend(cfg, mode)` (Wave 1) — NFS applies for + Shared-plain + Worktree-per-agent; Clone-per-agent stays node-local. No new toggle. +- **Volume spec:** emit an NFS volume in the Cloud Run Service/Job spec: + ```yaml + volumes: + - name: workspace + nfs: + server: # e.g. 10.45.255.170 + path: //projects//workspace # server-side path = isolation + readOnly: false + containers: + - volumeMounts: [{ name: workspace, mountPath: /workspace }] + ``` +- **Isolation (critical, §9.4):** Cloud Run has **no `subPath`**. Isolation therefore comes + from the **server-side `path`** being the project subdir — the instance can only reach what + the export path exposes. The Hub MUST put `projects//workspace` in the NFS `path`, + never the export root. Reuse the spirit of `ValidateNotExportRoot` (Wave 1 + `pkg/runtime/nfs_path_guard.go`): assert the emitted server path is strictly below the + export root before realizing. +- **UID/GID:** same convergence as Docker/K8s — stable 1000:1000 (`V1NFSConfig.UID/GID`); the + container runs as 1000. (Cloud Run runs the container user; align with the provisioned + ownership.) +- **Mount options / tier:** Filestore **basic = NFSv3** (default `vers=3`, set in Wave 1 + N1-7). NFSv4.1 needs Enterprise/zonal. +- **Provisioning:** Cloud Run instances have no host access for the Hub to clone into; the + workspace must be **pre-provisioned** on NFS (same as the K8s init-container model, Wave 2 + N2-2/N2-2b) — guarded by the per-project Postgres advisory lock + (`TryAdvisoryLockObject(LockWorkspaceProvision, StableProjectHash(pid))`). A Cloud Run + runtime would need an equivalent first-access provisioning step (e.g. a pre-create + provisioning Job, or Hub-side provisioner with NFS access) before starting the instance. +- **Acceptance (future):** a Cloud Run instance mounts only `projects//workspace` and + cannot reach the export root (server-path isolation). + +## N3-2 — Filestore-CSI dynamic PVC (Enterprise-only, deferred — recorded upgrade path) + +Not implemented (Q4: target is Filestore **basic**, which has no multishare and a 1 TiB +minimum, so one-PVC-per-project is economically impossible; CSI dynamic is Enterprise/zonal +"multishare" only). Recorded upgrade path: + +- Reuse the **generalized project-RWX-claim helper** from Wave 2 N2-5 (the generalized + `sharedDirPVCName`/`createSharedDirPVCs`/`cleanupSharedDirPVCs`) plus `V1NFSConfig.StorageClass` + (`filestore.csi.storage.gke.io`) → **one PVC per project** instead of the static + RWX-PV-+-subPath default (Wave 2 N2-1). +- Keep `V1NFSShare.ID` **per-Hub** (already in config) so moving to **instance-per-Hub + isolation** (the true Hub↔Hub isolation option, §9.4) is a config change, not a redesign. +- When adopted: swap the workspace volume source from static-PV+subPath to a per-project + dynamic PVC via the generalized helper; lifecycle (create-on-first-agent, cleanup on + project delete) already mirrors `cleanup*PVCs`. + +--- + +## Summary +Wave 3 is documentation only on this branch: the Cloud Run NFS realization (N3-1) is fully +specified and reuses Wave 1/2 primitives (backend selector, export-root isolation guard, +stable UID, advisory-lock provisioning, vers=3) — it just needs a Cloud Run runtime to attach +to. The Filestore-CSI dynamic per-project strategy (N3-2) is the recorded Enterprise-tier +upgrade, reusing the Wave 2 N2-5 generalized PVC helper, with per-Hub share IDs keeping the +instance-per-Hub isolation path open. diff --git a/.design/nfs-workspace.md b/.design/nfs-workspace.md new file mode 100644 index 000000000..bd1c4d0f2 --- /dev/null +++ b/.design/nfs-workspace.md @@ -0,0 +1,740 @@ +# Design: NFS-Coordinated Workspace Sharing Across Nodes + +**Branch:** `postgres/wave-b-integration` +**Date:** 2026-06-02 +**Author:** nfs-architect agent +**Status:** Design proposal — **all open questions (Q1–Q6) resolved with maintainer** (see §11) +**Vocabulary:** follows `GLOSSARY.md` (Runtime Broker, Project, workspace sharing modes) +**Reviewers:** @ptone +**Context:** Multi-node Scion (Postgres-backed Hub, brokers/agents spread across VMs and GKE/Cloud Run) needs a shared filesystem so an agent can reach its project workspace regardless of which node it lands on. + +Inputs (verified against source): +`pkg/runtime/common.go`, `pkg/runtime/k8s_runtime.go`, `pkg/config/shared_dirs.go`, +`pkg/api/types.go`, `pkg/store/models.go`, `pkg/ent/schema/{project,agent}.go`, +`pkg/runtimebroker/{types,handlers,start_context}.go`, `pkg/agent/run.go`, +`scripts/starter-Hub/`, `pkg/gcp/storage.go`. + +--- + +## 1. Problem statement + +Workspace storage in Scion is **node-local** today. Two facts make that fatal once +agents can be scheduled across nodes: + +1. **Docker/VM path.** The Runtime Broker computes a host path + (`~/.scion/project-configs/__/...` or a git checkout) and bind-mounts + it into the container: `-v HOST:/workspace` (`pkg/runtime/common.go:181-241`). That + host path only exists on the node where the Runtime Broker created it. A second agent for + the same project, dispatched to a different VM, sees an empty disk. + +2. **Kubernetes path.** The workspace volume is an **EmptyDir** + (`pkg/runtime/k8s_runtime.go:1080-1087`); the Runtime Broker then copies files into the pod + after start via `kubectl cp`, gated on a `/tmp/.scion-home-ready` marker + (`k8s_runtime.go:317-350`). Contents live only inside that pod and die with it. + There is no shared durable workspace at all. + +Shared directories are the one place a cross-node primitive already exists: on K8s +they are **project-scoped `ReadWriteMany` PVCs** named `scion-shared--` +(`k8s_runtime.go:657-751`), storage class from `KubernetesConfig.SharedDirStorageClass`. +On Docker they are plain host bind mounts (`pkg/config/shared_dirs.go`). So the RWX +PVC concept is proven; we extend the same idea to the **workspace** and give Docker an +equivalent via NFS. + +**Goal:** a project's workspace (and shared dirs) live on a network filesystem +addressable from every node, so any agent — on any VM, GKE pod, or Cloud Run +instance — mounts the same bytes. The Hub coordinates *which* NFS path maps to *which* +project/agent and tells the runtime how to mount it. + +### 1.1 Non-goals + +- **Creating & permissioning the NFS store.** Operator/Terraform owns *creation and + permissioning* of the NFS instance and its shares — this is the **only** thing that + happens outside the Hub / Runtime Broker lifecycle (maintainer, Q1). Everything else, including + **mounting** the share, is the Hub / Runtime Broker's job (§4.2). A single NFS instance may + expose **multiple shares** and serve **multiple Hub instances within one project**. +- **A distributed POSIX lock manager.** We rely on NFS-native advisory locking plus + Scion's existing per-agent state isolation; we do *not* build a lock service. +- **Replacing GCS-FUSE volumes.** `type: gcs` volumes (`common.go:142-163`, + `k8s_runtime.go:1238-1275`) stay as-is; NFS is a parallel backend. +- **Auto-migration of existing node-local workspaces.** New backend applies to new + projects/agents; migration is a separate effort (§10). + +--- + +## 2. Current architecture (verified) + +| Concern | Docker / VM | Kubernetes | +|---|---|---| +| Workspace storage | host bind mount `-v HOST:/workspace` (`common.go:185`) | EmptyDir + post-start `kubectl cp` (`k8s_runtime.go:1084`, `:317-350`) | +| Workspace host path | Runtime Broker-computed, node-local (`agent/run.go:755-780`, `start_context.go:92-110`) | n/a (synced in) | +| Container workspace path | `ResolveContainerWorkspace` → `/workspace` or `/repo-root/` (`common.go:52-69`) | same logic, `config.ContainerWorkspace` | +| Shared dirs | host bind mount under `project-configs/.../shared-dirs/` (`shared_dirs.go:33-118`) | project-scoped RWX PVC `scion-shared--` (`k8s_runtime.go:657-751`) | +| Volume types | `local`, `gcs` (`api/types.go:248-279`) | `gcs` (CSI), no local bind | +| Container UID | host user `scion` | UID/GID 1000, `FSGroup=hostGID` (`k8s_runtime.go:1021-1033`) | +| Placement metadata | none on Agent; Runtime Broker decides at dispatch | none | +| Workspace entity | **none** — derived from `Project.GitRemote` + workspace sharing mode (today a 2-value label `scion.dev/workspace-mode` ∈ {`shared`,`per-agent`}, `store/models.go:177-184`; glossary's canonical 3 modes are the target — §3.1) | same | + +Key data structures: + +- `api.VolumeMount{Source,Target,ReadOnly,Type,Bucket,Prefix,Mode}` (`api/types.go:248-256`). + `Validate()` only accepts `""|local|gcs` (`:264-276`). +- `api.SharedDir{Name,ReadOnly,InWorkspace}` (`api/types.go:205-210`); persisted as a + JSON column on the **Project** Ent entity (`ent/schema/project.go:62-63`). +- `api.KubernetesConfig{... SharedDirStorageClass, SharedDirSize}` (`api/types.go:291-302`). +- `config.V1StorageConfig{Provider,Bucket,LocalPath}` — Hub *artifact* storage, not + workspaces (`config/settings_v1.go:416-420`). +- `runtime.RunConfig{Workspace,RepoRoot,ContainerWorkspace,HomeDir,Volumes,SharedDirs,GitClone,...}` + (`runtime/interface.go`) — the per-agent contract the Runtime Broker fills and the runtime consumes. +- `runtimebroker.CreateAgentConfig{Workspace,RepoRoot,HomeDir,Volumes,SharedDirs,GitClone}` + — the Hub→Runtime Broker wire contract (`runtimebroker/types.go:369-411`). + +The single most important leverage point: **the runtime already mounts whatever host +path / volume the Runtime Broker hands it.** If we make that host path land on an NFS mount +(Docker) or swap the EmptyDir for an NFS-backed volume (K8s), most of the machinery is +untouched. The design is therefore mostly about **path mapping, provisioning, and +config**, not about rewriting the mount code. + +--- + +## 3. Core concept: a workspace storage backend + +Introduce an explicit **workspace storage backend** selected by config, with three +values: `local` (today's behavior, default), `nfs` (this design), and — reserved — +`gcs` (FUSE, already exists for *volumes* but not for the primary workspace). + +A backend answers three questions for any (project, agent): + +1. **Resolve** — given Project ID / agent ID / workspace sharing mode, what is the + storage location? For NFS this is a *server-relative export path*, computed + **deterministically from IDs** (no new DB column required for resolution — any + replica computes the same path): + + ``` + /projects//workspace # Shared-plain & Worktree-per-agent + /projects//shared-dirs/ # shared directories + ``` + +2. **Provision** — ensure the directory exists and, for git projects, is cloned/worktree'd + (§7). Idempotent; guarded against concurrent first-access (§8.2). + +3. **Realize** — emit the runtime-specific mount: + - Docker: a bind mount whose `Source` is the host NFS mountpoint + relative path. + - K8s: an NFS-backed volume (static PV+subPath or Filestore-CSI PVC) at the workspace path. + - Cloud Run: an NFS volume in the service/job spec with `path = /`. + +The Hub owns resolution + the mount spec it sends; the **Runtime Broker/runtime owns +provisioning and the actual mount syscall** (it is the component with filesystem / +cluster access). This mirrors today's split (Hub computes `CreateAgentConfig`, the +Runtime Broker realizes it). + +### 3.1 NFS applicability is driven by **workspace sharing mode** (maintainer, Q3) + +Per the glossary (`GLOSSARY.md` → *Workspace sharing mode*) there is **one** universal +set of three modes; the backend is **not** a separate per-project toggle — the sharing +mode *is* the selector: + +| Workspace sharing mode | What it means | Storage backend | +|---|---|---| +| **Shared-plain** | one workspace directory mounted into every agent, no per-agent isolation (plain/non-git projects) | **shared NFS workspace** | +| **Worktree-per-agent** | each agent gets its own git worktree over one shared checkout (one clone's history) | **shared NFS workspace** (the shared checkout + all worktrees live on it) | +| **Clone-per-agent** | each agent gets its own full git clone | **NOT NFS** — node-local disk (nothing is shared, so there is nothing to put on NFS) | + +So: **NFS backs the workspace for both sharing modes that share anything** +(Shared-plain and Worktree-per-agent) and backs **shared directories always**. +**Clone-per-agent** is the sole case that stays on node-local storage — a deliberate +"throwaway isolated clone" path. The `backend` config value (§6.1) therefore really +answers "*is a shared NFS workspace available on this Hub?*"; whether a given agent uses +it follows mechanically from the project's sharing mode. + +**Terminology note:** today the code carries only a two-value label +`scion.dev/workspace-mode ∈ {shared, per-agent}` (`store/models.go:177-184`). The +glossary's three canonical modes (Shared-plain / Worktree-per-agent / Clone-per-agent) +are the target vocabulary; aligning the label/enum to all three is a prerequisite +clean-up for this work (Worktree-per-agent is noted as "not yet on Hub-managed +projects"). + +--- + +## 4. Model A — VMs / Docker (host-level NFS, bind into container) + +### 4.1 Topology + +``` + ┌────────────── NFS server (Filestore / self-hosted) ─────────────┐ + │ export: /scion-workspaces │ + │ projects//workspace │ + │ projects//shared-dirs/ │ + └───────▲───────────────────────────────▲──────────────────────────┘ + │ mount (Runtime Broker, on startup) │ + ┌───────────┴──────────┐ ┌────────────┴─────────┐ + │ VM node-1 │ │ VM node-2 │ + │ /mnt/nfs/workspaces │ │ /mnt/nfs/workspaces │ + │ Runtime Broker + dockerd │ │ Runtime Broker + dockerd │ + │ agent ctr │ │ agent ctr │ + │ -v /mnt/nfs/.../ws │ │ -v /mnt/nfs/.../ws │ + │ :/workspace │ │ :/workspace │ + └──────────────────────┘ └──────────────────────┘ +``` + +### 4.2 Host mount — owned by the Hub / Runtime Broker, idempotent on (re)start + +**Decision (maintainer, Q1):** mounting is part of the **Hub / Runtime Broker service +lifecycle**, not an operator step. When a Runtime Broker comes online (cold start or restart) +it **ensures the configured share(s) are mounted** before accepting NFS-backed +dispatch; the operator only created+permissioned the store. This must be **idempotent +and restart-safe** — a Runtime Broker bouncing must reconcile, not double-mount or fail on an +already-present mount. + +Mount reconciliation at Runtime Broker startup (and re-checked before each NFS dispatch): + +``` +for each configured share S needed on this node: + target = / # stable, per-share path + if not is_mountpoint(target): + mkdir -p target + mount -t nfs -o vers=4.1,hard,nconnect=4,_netdev S.server:S.export target + else: + verify it points at the expected server:export (else log + remount) +``` + +Implementation notes: +- A Runtime Broker may need **multiple shares mounted at once** (a single NFS instance can + expose many shares, and one project may be served by multiple Hub instances). The + mount layer is therefore a *set* of shares keyed by share-id, each at its own + `/`, not a single global `/mnt/nfs/workspaces`. +- Prefer a managed systemd `.mount`/`automount` unit *written and started by the + Runtime Broker* over a raw `mount(8)` call, so the OS handles remount-on-reboot and the + Runtime Broker's job is reconciliation, not lifecycle. +- Run inside the existing Runtime Broker bring-up path (alongside doctor/health checks). On + mount failure, the Runtime Broker reports unhealthy for NFS-backed projects rather than + silently falling back to local disk. +- Requires the Runtime Broker to have mount privilege (root or `CAP_SYS_ADMIN`/sudo for + `mount`); call out in deployment docs. + +### 4.3 Path computation change (the only real code change for Model A) + +Today the Runtime Broker resolves the workspace/shared-dir host path under +`~/.scion/project-configs/...` (`pkg/config/shared_dirs.go:33-54`, +`runtimebroker/start_context.go:92-110`). With backend=`nfs`, that resolution is +redirected to the NFS mountpoint: + +``` +hostBase = / # Runtime Broker ensures this is mounted (§4.2) +workspace host path = hostBase/projects//workspace +shared dir host path = hostBase/projects//shared-dirs/ +``` + +`SharedDirsToVolumeMounts` (`shared_dirs.go:90-118`) is unchanged in shape — it still +emits `VolumeMount{Source: hostPath, Target: /scion-volumes/}`; only the base +path moves onto NFS. The container sees **no difference** — it is still a bind mount at +`/workspace` (or `/repo-root/`), and the existing repo-root tmpfs-shadow isolation +(`common.go:357-362`) continues to protect per-agent state. + +### 4.4 Lifecycle + +- **Create on first agent:** Runtime Broker `mkdir -p` + provision (clone) under the + project dir if absent (§7). The shared NFS workspace is reused across agents in both + Shared-plain and Worktree-per-agent modes (§3.1); a Worktree-per-agent agent adds its + own worktree under the shared checkout. +- **Persist across agents:** the workspace survives agent deletion (it is project-scoped). + A Worktree-per-agent agent's worktree is removed on that agent's deletion; the shared + checkout persists. +- **Cleanup:** on project deletion the Hub instructs a Runtime Broker to `rm -rf` the + project subtree (mirrors `cleanupSharedDirPVCs` on K8s, `k8s_runtime.go:753-770`). + Optional idle-GC by mtime (§10). (Clone-per-agent workspaces are node-local, not on + NFS, and are cleaned by the existing local path on agent delete.) + +--- + +## 5. Model B — GKE / Cloud Run (direct NFS per pod/instance) + +No shared host mount. Each pod/instance mounts the NFS share directly. **Decision +(maintainer, Q4): the target is Filestore *basic* tier**, which has one share per +instance and a 1 TiB minimum and **no multishare** — so strategy **(a) static RWX PV + +per-workspace `subPath` is THE default**, and the dynamic per-project-share strategy (b) +is an Enterprise-tier-only future option, not used now. + +### 5.1 GKE — strategy (a): one RWX PV + per-workspace `subPath` (recommended default) + +A single `PersistentVolume` (RWX) points at the Filestore share (or self-hosted NFS). +Each pod mounts it with a `subPath` equal to the project/agent relative path. This +avoids per-workspace PVC/Filestore-share churn (Filestore *basic* tier has a 1 TiB +minimum per instance — one share per workspace is economically impossible). + +```yaml +# Provisioned once by operator/Hub-bootstrap: +apiVersion: v1 +kind: PersistentVolume +metadata: { name: scion-workspaces } +spec: + capacity: { storage: 1Ti } + accessModes: [ReadWriteMany] + nfs: { server: 10.0.0.2, path: /scion-workspaces } # or csi: filestore.csi... + mountOptions: [vers=4.1, hard, nconnect=4] + persistentVolumeReclaimPolicy: Retain +--- +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: { name: scion-workspaces, namespace: scion-agents } +spec: + accessModes: [ReadWriteMany] + storageClassName: "" # bind to the static PV + volumeName: scion-workspaces + resources: { requests: { storage: 1Ti } } +``` + +Pod spec the runtime builds (replaces the EmptyDir at `k8s_runtime.go:1080-1087`): + +```yaml +volumes: +- name: workspace + persistentVolumeClaim: { claimName: scion-workspaces } +containers: +- name: agent + volumeMounts: + - name: workspace + mountPath: /workspace + subPath: projects//workspace # <-- per-workspace isolation within share +``` + +`subPath` is the linchpin: the pod can only see its own project subtree, never the +whole export — this is the K8s mount-isolation story (§9.4). + +### 5.2 GKE — strategy (b): Filestore CSI dynamic PVC per project *(Enterprise-only, deferred)* + +**Not used with the chosen basic tier (Q4).** Recorded for the future: for projects that +need a *dedicated* share (isolation/quota), the Filestore CSI driver +`filestore.csi.storage.gke.io` with a StorageClass lets the Hub create **one PVC per +project** (project-scoped like shared dirs today, `k8s_runtime.go:707-712`). This only +pencils out on Enterprise / "multishare" tiers (many small shares per instance); on basic +tier each PVC would demand a full 1 TiB instance, so it is economically impossible. +When/if adopted, reuse the existing shared-dir PVC code paths (`createSharedDirPVCs`, +`cleanupSharedDirPVCs`) generalized from "shared dir" to "any RWX workspace claim". + +### 5.3 GKE — shared dirs + +Already RWX PVCs. With NFS, simply point `KubernetesConfig.SharedDirStorageClass` at +a Filestore/NFS class, or fold shared dirs into the single-share+subPath model +(`subPath: projects//shared-dirs/`). No new code beyond config. + +### 5.4 Cloud Run + +Cloud Run (gen2 execution environment) supports **NFS volume mounts** and Filestore +directly. The Hub emits a volume in the Service/Job spec; each instance mounts at the +workspace path: + +```yaml +# Cloud Run service (knative-ish) volume +volumes: +- name: workspace + nfs: + server: 10.0.0.2 + path: /scion-workspaces/projects//workspace # server-side path = isolation + readOnly: false +containers: +- volumeMounts: [{ name: workspace, mountPath: /workspace }] +``` + +Cloud Run has no `subPath`, so isolation comes from the **server-side `path`** being +the project subdir (the instance can only reach what the export path exposes). The Hub +must therefore put the project id in the NFS `path`, not rely on in-container subPath. + +### 5.5 The big consequence for K8s/CR: workspace is pre-populated + +Today K8s starts with an empty workspace and the Runtime Broker `kubectl cp`s files in +(`k8s_runtime.go:317-350`). With an NFS-backed workspace the bytes are **already +present** on shared storage. The post-start sync of *workspace contents* becomes +unnecessary in the NFS case — provisioning (clone) happens once, out-of-band (§7), not +per-pod. Home-dir/secret sync and the `/tmp/.scion-home-ready` gate may still be needed +for non-workspace material; that path stays, but the workspace copy step is skipped +when backend=`nfs`. This is a meaningful simplification *and* a behavior change to call +out in review (§11 Q5). + +--- + +## 6. Data model & config changes + +### 6.1 New config block (Hub settings) + +```go +// pkg/config/settings_v1.go +type V1WorkspaceStorageConfig struct { + Backend string `json:"backend,omitempty" koanf:"backend"` // "local" (default) | "nfs" + + NFS *V1NFSConfig `json:"nfs,omitempty" koanf:"nfs"` +} + +type V1NFSConfig struct { + // One NFS instance may expose multiple shares (maintainer, Q1); a Runtime Broker + // mounts the set it needs. MountRoot is the local base under which each + // share is mounted at /. + MountRoot string `json:"mount_root,omitempty"` // e.g. /mnt/nfs ; per-share dir appended + MountOptions string `json:"mount_options,omitempty"` // default "vers=4.1,hard,nconnect=4,_netdev" + Shares []V1NFSShare `json:"shares,omitempty"` + + // Stable, node-independent ownership for NFS-backed trees (§9.1). Default + // 1000:1000 to converge with the K8s pod UID/GID. The Runtime Broker advertises + // these as SCION_HOST_UID/GID for NFS-backed agents instead of os.Getuid(). + UID int `json:"uid,omitempty"` // default 1000 + GID int `json:"gid,omitempty"` // default 1000 + + // Kubernetes realization (Model B) + StorageClass string `json:"storage_class,omitempty"` // Filestore-CSI dynamic strategy (5.2) + SubPathRoot string `json:"subpath_root,omitempty"` // default "projects" +} + +type V1NFSShare struct { + ID string `json:"id,omitempty"` // stable share id → mount dir + (K8s) PV name + Server string `json:"server,omitempty"` // e.g. 10.0.0.2 or Filestore IP + Export string `json:"export,omitempty"` // server export path, e.g. /scion-workspaces + PVName string `json:"pv_name,omitempty"` // K8s static PV+subPath strategy (5.1) +} +``` + +A project selects which share holds its workspaces (default: the single configured +share; explicit when multiple exist). Resolution becomes +`/projects//...`, and the Runtime Broker ensures that share is mounted (§4.2) +before realizing the bind mount. + +- `Backend` defaults to `local` → **zero behavior change** for existing deployments. +- **No separate per-project backend toggle.** Per the maintainer (Q3), backend selection + follows the **workspace sharing mode** (§3.1), not an independent flag: when + `Backend: nfs` is configured on the Hub, **Shared-plain** and **Worktree-per-agent** + projects use the shared NFS workspace; **Clone-per-agent** always uses node-local disk. + `Backend` thus gates *availability* of NFS on a Hub; the mode decides *use*. (If NFS is + not configured, shared modes degrade to single-node local — same as today.) + +### 6.2 Extend `VolumeMount` for explicit NFS volumes + +For user-declared volumes (and to let shared dirs / workspace flow through one code +path), add `nfs` to the type set: + +```go +type VolumeMount struct { + // ... existing ... + Type string // "local" | "gcs" | "nfs" + Server string `json:"server,omitempty"` // NFS: server host/IP + // Source is reused as the server export path for NFS; Prefix/subpath optional +} +``` + +Update `VolumeMount.Validate()` (`api/types.go:259-279`) to accept `nfs` requiring +`Server` + `Source` (export path) + `Target`. The existing `type: "nfs"` fixtures +(`pkg/api/types_test.go`, `pkg/config/templates_test.go`), presently *rejected*, become +valid (maintainer-confirmed, Q6). Workspace, shared directories, and ad-hoc user volumes +all flow through this one NFS volume path. + +### 6.3 Generalize the K8s shared-dir PVC helpers + +`sharedDirPVCName` / `createSharedDirPVCs` / `cleanupSharedDirPVCs` +(`k8s_runtime.go:657-770`) become a generic "project RWX claim" helper used for both +shared dirs and (strategy 5.2) workspaces. No schema change; reuses +`KubernetesConfig.SharedDir{StorageClass,Size}` plus the new `NFS.StorageClass`. + +### 6.4 No new Workspace entity, no placement column + +Resolution is deterministic from Project ID + agent ID + workspace sharing mode, so a +replica behind the load balancer computes the same NFS path without a DB lookup — +consistent with the broker-dispatch design (`DESIGN-BROKER-DISPATCH.md`) preference for +derivable state. We add +**no** placement/node column to Agent. (Optional future: cache the resolved path in +`AgentAppliedConfig.WorkspaceStoragePath`, which already exists for the GCS-bootstrap +case, `store/models.go:127-165` — reuse it rather than add a field.) + +--- + +## 7. Workspace provisioning (git clone onto NFS) + +A workspace must exist and (for git projects) be cloned/worktree'd before the harness +runs. Where the clone executes differs by model: + +- **Model A (Docker):** the **Runtime Broker** has direct filesystem access to + `/mnt/nfs/workspaces`. It runs the existing clone/worktree logic targeting the NFS + path. This is the smallest change — same code, different base dir. + +- **Model B (K8s/CR):** the Runtime Broker has **no** host access to the pod's NFS mount. + Two options: + 1. **Init container (recommended):** the pod gets an init container that mounts the + same workspace volume and performs the clone/worktree into it before the main + container's gate releases. The clone runs where the mount lives; no extra Hub + filesystem access needed. Fits the existing gate model (`k8s_runtime.go:317-350`). + 2. **Hub-side provisioner:** a Hub / Runtime Broker node mounts the same export and clones into + the project subdir out-of-band. Simpler to reason about for shared mode (clone + once, centrally) but requires the Hub node to have the NFS mount. See §11 Q5. + +**First-access guard (shared mode):** multiple agents for the same project may start +concurrently and race to clone the same dir. Guard with one of: +- a **sentinel file** (`.scion-provisioned`) created atomically after a successful clone; +- a **Postgres advisory lock** keyed by project-id (the Hub already uses advisory locks + — see commit `dcd4e0f6`/`f6d2a727` — so this is a natural, cross-node-correct choice); +- an NFS **advisory `flock`** on a lockfile in the project dir (works on NFSv4). + +Recommended: Postgres advisory lock for the *decision* ("am I the provisioner?"), +because it is already cross-node-correct and avoids NFS lock-manager variability. + +--- + +## 8. Sequence diagrams + +### 8.1 Agent start — Model A (Docker, shared NFS workspace) + +``` +User/API Hub (any replica) Runtime Broker (node-N) dockerd / agent ctr + │ start agent │ │ │ + ├─────────────────►│ │ │ + │ resolve backend=nfs │ │ + │ relPath=projects//workspace│ │ + │ CreateAgentConfig{ │ │ + │ Workspace=, │ │ + │ WorkspaceBackend=nfs} │ │ + │ ├─ dispatch ──────────────►│ │ + │ │ validate /mnt/nfs present │ + │ │ hostPath = /mnt/nfs/workspaces/ │ + │ │ acquire advisory lock(pid) │ + │ │ if !provisioned: git clone → hostPath │ + │ │ release lock │ + │ │ RunConfig.Workspace = hostPath │ + │ │ ├─ docker run -v hostPath:/workspace ─────►│ + │ │ │ harness runs│ + │◄─────────────────┤◄─────────────┤◄─────────────────────────────────────────┤ +``` + +### 8.2 Agent start — Model B (GKE, Filestore, static PV + subPath) + +``` +Hub (any replica) K8s runtime (Runtime Broker) kube-apiserver / kubelet pod + │ resolve backend=nfs │ │ │ + │ subPath=projects//workspace │ │ + │ ── CreateAgentConfig ────►│ │ │ + │ ensure PVC scion-workspaces (reuse if exists, k8s_runtime style) │ + │ buildPod: volume=PVC, mount /workspace subPath= │ + │ initContainer: clone into subPath (advisory-locked) │ + │ ├─ create Pod ──────────────►│ │ + │ │ schedule + mount NFS (subPath) ────────►│ + │ │ │ initC clones (once) │ + │ │ │ gate releases ───────►│ + │ │ │ harness runs │ + │ │ (NO kubectl cp of workspace — already on NFS) │ +``` + +--- + +## 9. Cross-cutting concerns + +### 9.1 Permissions / UID mapping — branch on host-FS vs NFS + +Rewritten per the maintainer's Q2 guidance (*study the existing remapping, then branch +on host-vs-NFS, keep it simple*). How it works today and the minimal branch NFS needs: + +**How Docker reconciles host-FS ownership today (verified):** +- The Runtime Broker injects its **own host UID/GID** as `SCION_HOST_UID`/`SCION_HOST_GID` + (`common.go:281-282` → `os.Getuid()/os.Getgid()`). +- Container starts as **root**; `sciontool init` → `setupHostUser()` + (`cmd/sciontool/commands/init.go:923`) **remaps the image's `scion` user to that host + UID/GID** via `usermod -o -u $SCION_HOST_UID -g $SCION_HOST_GID scion` (`init.go:1045`; + direct `/etc/passwd` fast-path on fuse-overlayfs, `:1033/:1086`), `chown`s the + workspace (`ensureWorkspaceOwnership`, `:1416`), then drops privileges. Files thus land + on the bind mount **owned by the Runtime Broker's host user** — fine, because the disk is + node-local. +- Podman rootless uses `--userns=keep-id:uid=1000,gid=1000` + `SCION_KEEPID_UID` with an + early drop to 1000 (`podman.go:172`, `init.go:975`). +- K8s uses a **fixed** UID/GID 1000 + `FSGroup=hostGID` (`k8s_runtime.go:1021-1033`). + +**Why NFS can't reuse the "host-UID" scheme:** one export is shared by agents on +*different* nodes whose brokers may have *different* host UIDs, and NFS authorizes by +**numeric UID on the wire**. Files written by node-1 (owned by node-1's Runtime Broker UID) may +be unwritable by node-2. NFS-backed trees therefore need a **node-independent, stable +UID/GID**. + +**The branch (small, localized):** choose *which UID the Runtime Broker advertises to the +container*, on host-vs-NFS: + +``` +// in buildCommonRunArgs (pkg/runtime/common.go, near :281) +uid, gid := os.Getuid(), os.Getgid() // host-FS path: UNCHANGED (today's behavior) +if backend == nfs { + uid, gid = cfg.NFS.UID, cfg.NFS.GID // stable; default 1000:1000 to match K8s +} +addEnv("SCION_HOST_UID", uid); addEnv("SCION_HOST_GID", gid) +``` + +The whole downstream remap pipeline (`setupHostUser` → `usermod`/passwd-edit → drop) is +**unchanged** — it already does the right thing for whatever UID it's told; for NFS it +remaps `scion` to the stable UID instead of the host Runtime Broker UID. One branch on the +*value advertised*, no new remap machinery. + +**Chown discipline on NFS:** do **not** recursively `chown` the NFS tree on every start +(`ensureWorkspaceOwnership`) — slow over the network and racy across nodes. Skip/guard +it when backend=nfs. Ownership is set **once**: (1) operator permissions the store at +creation (Q1) for the stable GID; (2) the provisioner (§7) `chown`s a project subtree +once at first creation, under the advisory lock. + +**Convergence:** set the NFS stable UID/GID = **1000:1000** so Docker, Podman, and K8s +all agree. On K8s, `FSGroup` (today `os.Getgid()`, `k8s_runtime.go:1022`) should be the +stable GID for NFS-backed pods — same host-vs-NFS branch. + +**Podman rootless + NFS** is the awkward corner: `keep-id` maps via subuid ranges and +won't cleanly yield a stable shared on-wire UID. Recommend rootful Docker / the fixed +scheme for NFS-backed projects and treat rootless+NFS as initially unsupported (Q2). + +- Provisioner sets project subtrees `chown 1000:1000`, mode `0770` (or `2770` setgid for + GID inheritance). UID alone does **not** isolate projects — all agents share UID 1000 + — so project isolation relies on subPath/server-path scoping, not ownership (§9.4). + +### 9.2 Concurrent access (by workspace sharing mode) +- **Clone-per-agent:** node-local, not on NFS (§3.1) → no shared-storage contention at + all. The isolation escape hatch. +- **Worktree-per-agent (on shared NFS workspace):** each agent gets its **own git + worktree** over the one shared checkout (one `.git`/object DB, many worktrees — already + a first-class layout, `common.go:192-196`). Working trees and per-agent index are + isolated, so the dangerous case (concurrent `git checkout` on one index) does not + arise; only worktree *add/remove* touches shared `.git` metadata and is guarded by the + provisioning advisory lock (§7). This is the maintainer-confirmed model for shared git + workspaces on NFS (Q3). +- **Shared-plain (on shared NFS workspace):** one directory mounted into all agents, + intentionally no isolation (plain/non-git). Concurrent writers coordinate at the + application level; Scion's per-agent state already lives outside this mount. +- NFSv4 supports byte-range + `flock` advisory locks; NFSv3 needs `rpc.lockd`. Prefer + **NFSv4.1** in mount options to get reliable locking and `nconnect` throughput. + +### 9.3 Performance +- Git is metadata-heavy (thousands of small files); NFS round-trips dominate + `git status`/`checkout`. Mitigations: + - Mount tuning: `vers=4.1, hard, nconnect=4-8, rsize/wsize=1M, actimeo` tuned for + workload. + - Storage tier: Filestore **zonal/enterprise** (not basic) for IOPS; or self-hosted + NFS on SSD. + - Keep hot, ephemeral, per-agent scratch (build caches, `node_modules`) on **local + ephemeral** disk or a `shared-dir` with `InWorkspace`, not on the cloned tree. + - Optional: local `.git` cache with NFS-hosted worktrees (advanced; defer). +- Benchmark git clone + `status` + a build on Filestore tiers before committing a + default (action item for the integration suite). + +### 9.4 Security / isolation — projects and Hubs (Filestore basic) — Q4 + +**Agent ↔ project isolation (the default we ship):** a single Filestore-basic export is +shared by all nodes, so at the host/cluster level **any node that mounts the export can +reach every project's bytes**. Agent-level isolation comes from only ever exposing the +project's own subtree to the container — **never the export root**: +- K8s: `subPath: projects//...` (§5.1) — pod sees only its project. +- Cloud Run: the server-side NFS `path` is the project subdir (§5.4) — no subPath needed. +- Docker: bind-mount only `projects//...`, not `/`. +This is **Tier 1** and, per Q4, what we adopt given the basic-tier constraint. + +**Hub ↔ Hub isolation (desired, but constrained by the protocol + tier).** The maintainer +asked whether one Hub (with its own GCP service account) could be prevented from mounting +another Hub's shares. Key fact: **NFS data-plane access is not GCP-service-account-aware.** +A service account governs the *GCP control plane* (creating/IAM on the Filestore +instance), but mounting an NFS export is authorized by **network reachability + host/UID**, +not by IAM/SA. So SA-based mount isolation is **not achievable at the NFS layer on any +tier** — and Filestore *basic* additionally has no per-client export ACLs (those are an +Enterprise feature). The realistic isolation levers are therefore: + 1. **Network/firewall scoping (basic-tier feasible, partial):** Filestore basic attaches + to one VPC; restrict TCP/2049 to specific source ranges/tags. This separates Hubs + **only if they sit on different networks/instances** — within one shared instance, + every authorized client can reach the whole share. + 2. **One Filestore instance per Hub (true Hub isolation, the "more expensive option"):** + because basic = one share per instance (1 TiB min), real per-Hub isolation means a + dedicated instance per Hub, firewalled to that Hub's nodes. This is the cost ptone + flagged. + 3. **Enterprise multishare + per-share network rules:** finer isolation without an + instance per Hub — but that is the pricier tier we are *not* using. + +**Recommendation (matches Q4):** ship **Tier 1** now — single shared basic instance, +subpath/server-path scoping for agent↔project isolation, optional firewall scoping. Treat +strict Hub↔Hub isolation as **deferred**: document that it requires either an +instance-per-Hub (basic) or Enterprise multishare, and is skipped until the cost is +justified. Make the share assignment explicit in config (`V1NFSShare.ID` per Hub) so a +future move to instance-per-Hub is a config change, not a redesign. +- **Never** bind/mount the export root into a container — always the project subtree. + +### 9.5 Provisioning lifecycle & reclaim +- Create-on-first-agent; persist project-scoped; cleanup on project deletion (mirror + `cleanupSharedDirPVCs`). PV `reclaimPolicy: Retain` so deleting a PVC never nukes the + Filestore share. Optional idle GC by mtime with a long TTL; log what is reclaimed + (no silent deletion). + +--- + +## 10. Migration & rollout + +1. **Phase 0:** land config + `VolumeMount` `nfs` type + validation; backend defaults + `local`. No runtime behavior change. Unit tests (the existing `nfs` fixtures flip + from "rejected" to "accepted"). +2. **Phase 1 (Model A):** redirect Runtime Broker path resolution to the NFS host mount when + backend=nfs; provisioning + advisory lock. Integration-test on two GCE VMs sharing + one Filestore instance (fits the postgres integration VM fleet). +3. **Phase 2 (Model B / GKE):** static PV + subPath workspace volume; init-container + provisioning; skip workspace `kubectl cp`. Generalize shared-dir PVC helpers. +4. **Phase 3:** Cloud Run NFS volumes; Filestore-CSI dynamic option. +5. **Migration of existing workspaces:** one-shot copy of node-local + `project-configs/...` trees into the NFS layout, flip the project label. Separate + tool; not auto. + +--- + +## 11. Maintainer decisions (all resolved) + +All six questions were resolved with @ptone over `scion message` (2026-06-02), one at a +time; each decision is folded into the sections noted below. + +- **Q1 — NFS server & host-mount ownership. [RESOLVED]** Operator creates and + permissions the NFS store only; **the Hub / Runtime Broker mounts** the share(s) as part of its + service bring-up and re-mounts idempotently on restart. A single NFS instance may + expose multiple shares and serve multiple Hub instances within a project. Reflected + in §1.1, §4.2, §6.1. +- **Q2 — UID alignment. [RESOLVED]** Studied the existing remapping: Docker advertises + the Runtime Broker's host UID (`SCION_HOST_UID`, `common.go:281`) and `sciontool init` remaps + `scion` to it (`usermod`, `init.go:1045`); K8s is fixed at 1000. NFS needs a *stable* + node-independent UID, so the design **branches on host-vs-NFS**: for NFS-backed agents + the Runtime Broker advertises a stable configured `NFS.UID/GID` (default **1000:1000**, + matching K8s) instead of `os.Getuid()`, reusing the unchanged remap pipeline; per-start + NFS chown is skipped (operator + one-time provisioner chown instead). Podman-rootless + + NFS is **unsupported initially**. Confirmed by maintainer. Detailed in §9.1, §6.2. +- **Q3 — Sharing mode ↔ NFS, and concurrency. [RESOLVED]** Maintainer set the model + using the glossary's canonical **workspace sharing modes**: NFS backs **both** the + workspace **and** shared directories; a **shared NFS workspace** serves **all** sharing + modes that share state — **Shared-plain** and **Worktree-per-agent** (worktrees ride on + the shared NFS checkout). The **only** non-NFS case is **Clone-per-agent** (node-local). + Consequence: **no separate per-project backend toggle** — the sharing mode is the + selector; Hub `Backend: nfs` only governs availability. Reflected in §3.1, §6.1, §9.2. +- **Q4 — Isolation tier & GKE strategy. [RESOLVED]** Target is **Filestore basic** → + **Tier 1** default: single shared export + subpath/server-path scoping for agent↔project + isolation, and **static RWX PV + `subPath`** (§5.1) as the GKE strategy (Filestore-CSI + dynamic §5.2 is Enterprise-only, deferred). Maintainer wants Hub↔Hub isolation ideally; + flagged that **NFS mounts are not service-account-gated** (network/host/UID auth only) + and basic tier has no per-client export ACLs, so true Hub isolation needs either an + instance-per-Hub (basic) or Enterprise multishare — **deferred** as the costlier option, + with per-Hub share IDs in config to keep that path open. Reflected in §5, §9.4. +- **Q5 — K8s provisioning & sync change. [RESOLVED — yes to both]** Provision the shared + NFS workspace via an **init container** (mounts the same NFS volume, clones/worktree-adds + once under a Postgres advisory lock on Project ID), and **skip the post-start `kubectl + cp` of workspace contents** when backend=nfs. The home-dir/secret sync and the + `/tmp/.scion-home-ready` readiness gate are unchanged, and the workspace `kubectl cp` is + retained for the local backend. Reflected in §5.5, §7, §8.2. +- **Q6 — `VolumeMount` nfs type. [RESOLVED — yes, NFS first-class]** Add `nfs` as a + first-class `VolumeMount.Type` with a `Server` field (`Source` = server export path); + extend `Validate()` to require `Server`+`Source`+`Target`, flipping the existing + `type: "nfs"` fixtures (`pkg/api/types_test.go`, `pkg/config/templates_test.go`) from + rejected to valid. Workspace, shared directories, and ad-hoc user volumes all flow + through this one unified NFS volume path. Reflected in §6.2. + +--- + +## 12. Summary + +NFS-backed workspaces reuse Scion's proven RWX-PVC pattern (today's shared dirs) and +its existing "Runtime Broker realizes the mount the Hub describes" split. The change is mostly +**path mapping + provisioning + config**, not a rewrite: + +- **Model A (Docker/VM):** the Runtime Broker mounts the NFS share(s) idempotently at + startup (operator only created+permissioned the store), redirects workspace/shared-dir + base paths there, and bind-mounts as today. Container unchanged. +- **Model B (K8s/Cloud Run):** workspace volume becomes NFS-backed (static PV+subPath + default, Filestore-CSI dynamic option; Cloud Run NFS volume with project-scoped server + path). Workspace is pre-populated on shared storage, so the post-start `kubectl cp` + of workspace contents is dropped. +- **Coordination:** deterministic path resolution from IDs (no new placement column); + provisioning guarded by Postgres advisory locks; project-scoped lifecycle mirroring + `cleanup*PVCs`; isolation via subPath / server-path scoping with a per-project-share + upgrade path. diff --git a/.design/project-log/2026-06-02-fix-test-hub-credential-leak.md b/.design/project-log/2026-06-02-fix-test-hub-credential-leak.md new file mode 100644 index 000000000..0678b1276 --- /dev/null +++ b/.design/project-log/2026-06-02-fix-test-hub-credential-leak.md @@ -0,0 +1,25 @@ +# Fix: Test suite leaking Hub credentials (issue #123) + +**Date**: 2026-06-02 +**PR**: #125 +**Issue**: #123 + +## Problem + +When `go test` runs inside an agent container, tests inherit live Hub env vars. `TestInitCommand_Integration` builds and spawns a real sciontool binary that inherits these vars, causing the child process to report status to the real Hub and corrupt agent state (resetting phase to "starting"). This is how dev-issue-71b got stuck. + +## Fix + +1. Added `scrubHubEnv(t)` helpers using `t.Setenv` for automatic cleanup in: + - `cmd/sciontool/commands/init_test.go` (primary subprocess fix) + - `pkg/sciontool/hooks/handlers/hub_test.go` (env var hygiene) + - `pkg/sciontool/hub/client_test.go` (env var hygiene) + +2. Added `filterHubEnv(env)` to explicitly strip Hub vars from subprocess environments. + +3. Converted all `os.Setenv`/`os.Unsetenv` patterns to `t.Setenv` in hub-related test files for crash-safe env isolation. + +## Observations + +- The Hub env var list (`SCION_HUB_ENDPOINT`, `SCION_HUB_URL`, `SCION_AUTH_TOKEN`, `SCION_AGENT_ID`, `SCION_AGENT_MODE`) is defined in `pkg/sciontool/hub/client.go:45-56`. The `scrubHubEnv` helpers are inlined in each test file rather than shared, to avoid importing `testing` into production code. +- Pre-existing CI issue: `pkg/hub/resource_import_handler_test.go` has an undefined `mockRoundTripper` symbol that causes `go vet ./...` to fail — not related to this change. diff --git a/.design/project-log/2026-06-03-b5-3-chaos-gate.md b/.design/project-log/2026-06-03-b5-3-chaos-gate.md new file mode 100644 index 000000000..f92a9a769 --- /dev/null +++ b/.design/project-log/2026-06-03-b5-3-chaos-gate.md @@ -0,0 +1,31 @@ +# B5-3 Chaos Gate — GB5 PASSED + +**Date:** 2026-06-03 +**Agent:** qa-agent +**Branch:** `postgres/wave-b-integration` @ `62186381` +**Gate:** GB5 (GA gate for broker dispatch) + +## Result: PASS + +All five chaos scenarios completed against two-VM + CloudSQL topology. +Full results at `/scion-volumes/scratchpad/B5-3-CHAOS-GATE-RESULTS.md`. + +| Scenario | Result | +|----------|--------| +| A: Kill owning hub mid-start | **PASS** — Hub B claimed and completed dispatch in 1.3s; no double-execution; `state=done, attempts=0` | +| B: Broker flap A→B | **PARTIAL/PASS** — Co-located topology prevents literal A→B flap; CAS claim, reconcile drain, and reaper all verified via equivalent tests | +| C: Pool saturation during PublishTx | **PASS** — Message dispatched 60ms post-creation despite external pool pressure; no corruption, no orphaned pending rows | +| D: Command-bus listener drop | **PASS** — Reconnected in ~280ms; cross-node dispatch succeeded immediately after | +| E: Reaper correctness | **PASS** — Stuck `in_progress` dispatch re-driven within 1 min of threshold; stale `connected_hub_id` cleared within 1 min of stale window | + +## Key Timing Evidence (Scenario E) + +- Hub killed: 22:48:02; last heartbeat: 22:49:38 +- Dispatch requeued (`in_progress→pending`): 22:50:26 — within 1 min of `dispatchStuckAge` +- Affinity cleared: 22:53:26 — within 1 min of `affinityStaleAge` (180s from last heartbeat) + +## Notes + +- Scenario B limitation: in this deployment brokers are co-located with their hubs (same process). A cross-hub broker reconnect can't be manually induced. The mechanisms that handle it (CAS claim on reconnect, reconcile drain, reaper) were each independently verified. +- `ConnectionMaxIdleTime` fix (from LIVE-RETEST-RESULTS.md §7) not yet implemented. No stall observed during chaos recovery — command-bus reconnect was clean. Recommend as a follow-up hardening item, not a blocker. +- VMs left running `62186381` on Postgres (healthy) after gate. diff --git a/.design/project-log/2026-06-03-cross-replica-signing-key-login-loop.md b/.design/project-log/2026-06-03-cross-replica-signing-key-login-loop.md new file mode 100644 index 000000000..541d88dfc --- /dev/null +++ b/.design/project-log/2026-06-03-cross-replica-signing-key-login-loop.md @@ -0,0 +1,73 @@ +# Fix: cross-replica login loop (`session_expired`) after cookie-store fix + +**Date:** 2026-06-03 +**Branch:** postgres/wave-b-integration +**Symptom:** After OAuth login the dashboard flashes, then the browser is +redirected to `/login?error=session_expired&returnTo=/`, repeatedly. + +## Background + +Commit `0515e2a8` replaced the per-replica gorilla `FilesystemStore` with an +encrypted+signed `CookieStore` whose keys derive from the shared +`SESSION_SECRET`, so the whole web session (OAuth state + Hub JWTs) rides in the +client cookie and any replica can read it. That fixed the OAuth `state_mismatch` +and made the *session container* replica-portable. + +## Root cause (one layer deeper) + +The cookie is portable, but the **Hub JWT inside it is signed with a per-replica +key**. Signing keys are resolved by `ensureSigningKey()` scoped to +`(scope=hub, scope_id=hubID)`, and `hubID = sha256(hostname)[:12]` +(`DefaultHubID`). The integration deployment runs **two replicas of one logical +hub** behind a single LB (`multi.demo.scion-ai.dev`), sharing one Postgres DB +and one `SESSION_SECRET`, but with different hostnames: + +| Replica | hub_id | user_signing_key fp | +|---|---|---| +| scion-integration | `ca39430276ee` | `9a35ae24cfeedba0` | +| scion-integration2 | `9662ebe99da4` | `97d3f30a36554d7a` | + +So each replica minted/validated user JWTs with a *different* HS256 key. When a +post-login request landed on the replica that did **not** mint the token, +`ValidateUserToken` failed (`go-jose: error in cryptographic primitive`), +refresh failed too (the refresh token is signed with the same foreign key), and +`sessionToBearerMiddleware` declared the session "irrecoverably invalid", +**deleted the cookie** (`MaxAge=-1`) and returned `session_expired`. The cookie +deletion is what turns it into a loop. Logs show the same user alternating +between "User authenticated" and "Hub token irrecoverably invalid, clearing +session" depending on which replica served the request. + +## Fix + +Extend the `0515e2a8` philosophy from the cookie to the keys inside it: derive +the agent and user JWT signing keys deterministically from the shared +`SESSION_SECRET`. + +- `ServerConfig.SharedSigningSecret` (new field). +- `ensureSigningKey()`: when `SharedSigningSecret != ""`, return + `deriveSharedSigningKey(secret, keyName)` (domain-separated by key name), + bypassing per-host secret-backend storage. Empty secret → unchanged per-hub + behavior (no regression for single-node/local dev). +- `cmd/server_foreground.go`: new `resolveSessionSecret()` helper feeds the same + value into both the web cookie store and `hubCfg.SharedSigningSecret`. + +Now every replica with the same `SESSION_SECRET` agrees on the signing keys, +regardless of hostname/hubID — no operator coordination (matching HubID) needed. + +## Tests + +`pkg/hub/signing_key_shared_test.go`: +- derivation is deterministic, 32 bytes, domain-separated, secret-sensitive; +- two servers with **different hubID, same secret** derive identical keys and a + token minted on one validates on the other; a different secret cannot; +- an explicit pre-configured key still wins over derivation. + +## Deploy note + +Rolling out the new binary changes the signing keys (they now derive from +`SESSION_SECRET` instead of the stored per-host keys), so existing web sessions +and CLI tokens are invalidated **once** — users log in again, CLI/agents +re-auth. Both replicas already share `SESSION_SECRET`, so no config change is +required. (Faster stopgap without a rebuild: pin the same +`SCION_SERVER_HUB_HUBID` on both VMs to an existing hub ID so they share the +already-stored keys.) diff --git a/.design/project-log/2026-06-03-fix-workspace-file-browser-path.md b/.design/project-log/2026-06-03-fix-workspace-file-browser-path.md new file mode 100644 index 000000000..8a8458775 --- /dev/null +++ b/.design/project-log/2026-06-03-fix-workspace-file-browser-path.md @@ -0,0 +1,34 @@ +# Fix: Workspace file browser path resolution (Issue #130) + +**Date:** 2026-06-03 +**PR:** #132 +**Issue:** #130 + +## Problem + +The Hub UI workspace file browser was showing the wrong directory contents. The `hubManagedProjectPath()` function resolved workspace paths to `~/.scion/projects//` instead of `~/.scion/groves//`. + +The three relevant directories per project: +1. `~/.scion/groves//` — actual git checkout, mounted as `/workspace` in agents (correct target) +2. `~/.scion/projects//` — project metadata + Telegram plugin downloads (what the UI was showing) +3. `~/.scion/grove-configs/__/` — agent configs and shared-dirs + +## Root Cause + +`hubManagedProjectPath()` checked `projects/` first, fell back to `groves/`, and defaulted to `projects/`. This was backwards — the git checkout (what agents actually work in) lives under `groves/`. + +## Fix + +Reversed the lookup priority in `hubManagedProjectPath()`: +1. Check `groves/` first (preferred — actual workspace) +2. Fall back to `projects/` (backward compatibility) +3. Default to `groves/` when neither has content + +## Files Changed + +- `pkg/hub/handlers.go` — reversed path resolution priority +- `pkg/hub/handlers_project_test.go` — updated existing test, added 3 new test cases + +## Observations + +- The `pkg/config` test suite has a pre-existing failure (`TestEnsureHubReady_GlobalFallbackWithHubEnabled`) caused by leaked `SCION_*` environment variables in the container. This is unrelated to this change and passes when those env vars are cleared. diff --git a/.design/project-log/2026-06-03-n1-1-workspace-backend-abstraction.md b/.design/project-log/2026-06-03-n1-1-workspace-backend-abstraction.md new file mode 100644 index 000000000..d88bcb5ef --- /dev/null +++ b/.design/project-log/2026-06-03-n1-1-workspace-backend-abstraction.md @@ -0,0 +1,40 @@ +# N1-1: Workspace Storage Backend Abstraction + +**Date:** 2026-06-03 +**Agent:** runtime-agent-1 +**Branch:** `nfs/n1-1-backend-abstraction` +**Commit:** `eca1b882` +**Status:** Complete + +## What was done + +Introduced the `WorkspaceBackend` interface in `pkg/runtime/` with three methods mapping to the NFS design's three questions: + +1. **Resolve** — deterministic path computation from project/agent IDs + sharing mode. No DB, no I/O. +2. **Provision** — stub for N1-4 (NFS clone+advisory-lock); no-op for local. +3. **Realize** — stub for N1-3 (mount wiring); local returns today's bind-mount descriptor. + +### Files added (4) + +- `pkg/runtime/workspace_backend.go` — interface + input/output structs + `SelectWorkspaceBackend` helper +- `pkg/runtime/workspace_backend_local.go` — `localBackend` wrapping today's behavior +- `pkg/runtime/workspace_backend_nfs.go` — `nfsBackend` with complete Resolve, stub Provision/Realize +- `pkg/runtime/workspace_backend_test.go` — 22 table-driven tests + +### Key design decisions + +- **Package placement:** `pkg/runtime/` chosen because it can import both `config` and `store` without cycles (neither imports `runtime`), and `runtimebroker` already imports `runtime`. +- **SelectWorkspaceBackend** routes `nfsBackend` only when `Backend=nfs` AND mode ∈ {SharedPlain, WorktreePerAgent}. ClonePerAgent always gets `localBackend` — the deliberate node-local escape hatch per design §3.1. +- **localBackend.Resolve** returns `ProjectDir` as-is — faithful to today's broker path resolution, zero behavior change. +- **nfsBackend.Resolve** uses first configured share, lays out `//workspace` and `//shared-dirs/`. + +### Test results + +- `go build ./...` — clean +- `go vet ./pkg/runtime/` — clean +- All 22 new tests pass; existing runtime tests unaffected + +## Process notes + +- Shared workspace was concurrently switched to another branch by a parallel agent. Resolved by creating a git worktree at `/tmp/nfs-n1-1` on the correct branch, avoiding conflicts. +- Phase-0 config structs (`V1WorkspaceStorageConfig`, `V1NFSConfig`, `V1NFSShare`, `WorkspaceSharingMode`) were already present on the integration branch — used directly. diff --git a/.design/project-log/2026-06-03-n1-2-3-5-nfs-runtime-wiring.md b/.design/project-log/2026-06-03-n1-2-3-5-nfs-runtime-wiring.md new file mode 100644 index 000000000..9beb0e122 --- /dev/null +++ b/.design/project-log/2026-06-03-n1-2-3-5-nfs-runtime-wiring.md @@ -0,0 +1,44 @@ +# N1-2, N1-3, N1-5 — Docker-runtime NFS wiring + +**Date:** 2026-06-03 +**Agent:** runtime-agent-2 +**Branch:** `nfs/n1-2-3-5-runtime` +**Base:** `postgres/wave-b-integration` @ dfed3bc7 + +## Tasks completed + +### N1-2: Broker NFS mount reconciliation (37a51b97) +- Created `pkg/runtimebroker/nfs_mount.go` with `NFSMountReconciler` +- `MountChecker` interface isolates mount syscalls for testability +- Reconciliation logic: not mounted→mkdir+mount; correct→no-op; wrong source→remount +- Per-share health tracking with `IsHealthy()`/`HealthCheckString()` for integration with broker health endpoint +- `EnsureShareMounted(shareID)` for pre-dispatch verification +- Multi-share support: shares are a set keyed by share-id +- 15 unit tests covering all reconciliation scenarios (idempotency, failures, multi-share) + +### N1-3: Redirect path resolution to NFS (c919fab8) +- Created `pkg/runtime/nfs_path_guard.go` with: + - `ValidateNotExportRoot` — isolation guard rejecting export-root binds (design §9.4) + - `NFSSharedDirsToVolumeMounts` — NFS-aware shared dir volume mount builder +- Updated `nfsBackend.Realize` from stub to full Docker bind-mount descriptor +- Isolation guard enforced in both `Realize` and `NFSSharedDirsToVolumeMounts` +- Local backend behavior byte-identical — verified via tests +- 14 unit tests covering isolation guard, NFS shared dirs, end-to-end resolve→realize + +### N1-5: Stable UID/GID branch for NFS (745f967c) +- Added `WorkspaceBackendName`, `NFSUID`, `NFSGID` to `RunConfig` (minimal threading) +- `buildCommonRunArgs`: local→os.Getuid(), nfs→NFS.UID/GID (default 1000:1000) +- Exports `SCION_WORKSPACE_BACKEND` env var for sciontool init chown skip +- Podman rootless + NFS rejected with clear error message +- 8 unit tests covering UID branching, default values, backend env, rootless rejection + +## Design decisions +- **No behavior change for backend=local** — all three tasks verified this via explicit tests +- **Isolation guard as a shared function** — used by both Realize and shared-dirs helpers +- **MountChecker interface** — avoids importing exec/syscall in test code +- **WorkspaceBackendName as string** — keeps RunConfig changes minimal; avoids importing config types into runtime + +## Verification +- `go build ./...` — clean +- `go vet ./pkg/runtime/... ./pkg/runtimebroker/...` — clean +- All new + existing unit tests green (37 new tests total) diff --git a/.design/project-log/2026-06-03-n1-4-6-nfs-provisioning.md b/.design/project-log/2026-06-03-n1-4-6-nfs-provisioning.md new file mode 100644 index 000000000..513dcc1e9 --- /dev/null +++ b/.design/project-log/2026-06-03-n1-4-6-nfs-provisioning.md @@ -0,0 +1,56 @@ +# Project Log: N1-4 + N1-6 — NFS Workspace Provisioning & Cleanup + +**Date:** 2026-06-03 +**Agent:** provisioning-agent +**Branch:** `nfs/n1-4-6-provisioning` (from `postgres/wave-b-integration`) +**Tasks:** N1-4 (workspace provisioning + advisory lock), N1-6 (project-deletion cleanup) + +## What was done + +### N1-4A: Per-project advisory lock API extension +- Extended `AdvisoryLocker` interface with `TryAdvisoryLockObject(ctx, classID, objID)` — uses Postgres's two-integer form `pg_try_advisory_lock(int4, int4)` for per-project locks +- `LockWorkspaceProvision` (0x5C101001) as classID, `StableProjectHash()` (FNV-32a) for deterministic objID from project UUID +- SQLite implementation: no-op (always acquired) — single-writer already serializes +- Implemented in `pkg/store/concurrency.go` (interface + hash helper) and `pkg/store/entadapter/locking.go` (Postgres/SQLite impl) + +### N1-4B: nfsBackend.Provision implementation +- Full first-access provisioning flow in `pkg/runtime/workspace_backend_nfs.go`: + 1. Acquire per-project advisory lock (retry loop, 30 attempts × 1s) + 2. Check sentinel `.scion-provisioned` → short-circuit if present + 3. mkdir -p workspace + shared-dirs + 4. Git clone if project is git-backed + 5. chown to stable NFS UID/GID (one-time, non-fatal if unprivileged) + 6. Write sentinel atomically (temp + rename) + 7. For WorktreePerAgent: create per-agent git worktree + 8. Release lock +- ClonePerAgent mode asserted out (defense in depth) +- `ProvisionInput` extended with `Locker`, `NFSUID/NFSGID`, `AgentName` + +### N1-6: Project-deletion cleanup +- `CleanupNFSProject` helper in `pkg/runtime/workspace_cleanup.go` +- Removes `//projects//` subtree +- Safety: `ValidateNotExportRoot` + path traversal protection + idempotent +- Wired into broker's `deleteProject` handler via `NFSConfig` on `ServerConfig` +- Hub passes `project_id` query param; broker reads it for NFS path computation +- Extended `RuntimeBrokerClient.CleanupProject` signature to accept `projectID` + +## Test coverage +- **42 new test cases** across 4 test files +- `pkg/store/concurrency_test.go`: StableProjectHash determinism, range, key uniqueness +- `pkg/store/entadapter/locking_test.go`: TryAdvisoryLockObject SQLite no-op, independence +- `pkg/runtime/workspace_provision_test.go`: Full provisioning lifecycle — git clone, sentinel short-circuit, worktree creation, two-agent worktree independence, lock retry/mutual exclusion, ClonePerAgent rejection, degraded mode (no locker), missing-field validation, branch name sanitization, sentinel atomicity +- `pkg/runtime/workspace_cleanup_test.go`: Subtree removal, idempotency, share-root refusal, path traversal refusal, project isolation, nil config, no shares, default SubPathRoot + +## Findings & observations +1. The two-int advisory lock form fit naturally — no awkwardness with the existing API. The Postgres `pg_try_advisory_lock(int4, int4)` namespace is separate from the single-int form, so no collision risk. +2. Git clone in tests required `--initial-branch=main` to work across git versions. +3. `chown` in provisioning is non-fatal — tests run unprivileged and the operator may have pre-set ownership. This is by design (§9.1). +4. The `RuntimeBrokerClient.CleanupProject` interface change touched 5 implementations (HTTP transport, control channel, hybrid, authenticated, HTTP dispatcher) — all mechanical pass-throughs. The project_id query param is backward-compatible (empty = no NFS cleanup). + +## Commits +``` +59f6ee78 feat(store): per-project advisory lock (two-int form) for NFS provisioning guard (N1-4A) +1b1ecb0b feat(runtime): NFS workspace provisioning with advisory-lock race guard (N1-4B) +b68116ad feat(runtime): NFS project-deletion cleanup mirroring K8s cleanupSharedDirPVCs (N1-6) +7a59f4e2 fix(hub): add TryAdvisoryLockObject to lockerStore mock (test fix for N1-4A) +``` diff --git a/.design/project-log/2026-06-03-n1-7-nfs-wiring.md b/.design/project-log/2026-06-03-n1-7-nfs-wiring.md new file mode 100644 index 000000000..89c07ba0b --- /dev/null +++ b/.design/project-log/2026-06-03-n1-7-nfs-wiring.md @@ -0,0 +1,51 @@ +# N1-7: Wire NFS Mount Reconciler + Fix vers=3 Default + Deploy Notes + +**Date:** 2026-06-03 +**Agent:** runtime-wire-agent +**Branch:** `nfs/n1-7-wiring` (from `postgres/wave-b-integration` @ `42bffd67`) +**Commit:** `08cab2ac` + +## What was done + +### 1. Wired NFSMountReconciler into broker startup (the main gap) +- Created `ExecMountChecker` (`nfs_mount_exec.go`) — production `MountChecker` + implementation using `mount(8)`, `umount(8)`, `mountpoint(1)`, and `/proc/mounts`. + The `runCommand` field is injectable for testing. +- Added `nfsMountReconciler` field to `Server` struct. +- In `New()`: construct reconciler when `NFSConfig` has shares configured. +- In `Start()`: call `Reconcile()` alongside other startup tasks, logging health status. +- In `GetHealthInfo()`: surface `nfs_mounts` key in health checks; degrades status if unhealthy. +- In `createAgent()`: added `ensureNFSMountsReady()` guard before `buildStartContext` — returns + 503 if any NFS share cannot be mounted (no silent fallback). +- In `server_foreground.go`: wire `NFSConfig` from `versionedSettings.Server.WorkspaceStorage.NFS` + into `ServerConfig` when backend=nfs. + +### 2. Fixed default MountOptions: vers=4.1 → vers=3 +- Changed default in `ApplyNFSDefaults()` and the reconciler's inline fallback. +- Updated doc comment on `V1NFSConfig.MountOptions` explaining the NFSv3 rationale. +- Updated test expectations in both `settings_v1_test.go` and `nfs_mount_test.go`. + +### 3. Deploy notes +- Created `pkg/runtimebroker/NFS_DEPLOY_NOTES.md` covering UID/GID alignment, + mount privilege requirements, and the NFSv3 default. + +## Tests added +- `nfs_mount_exec_test.go`: ExecMountChecker (mock exec, interface compliance) +- `nfs_wiring_test.go`: Server construction (reconciler wired/nil), health surface, + `ensureNFSMountsReady()` dispatch guard + +## Verification +- `go build ./...` — clean +- `go vet ./...` — clean +- `go test ./pkg/runtimebroker/...` — all pass (0 failures) +- `go test ./pkg/config/ -run TestWorkspaceStorage` — all pass +- `go test ./pkg/runtime/...` — all pass +- Pre-existing failures in `pkg/config` (`TestIsInsideProject` etc.) confirmed on base branch + +## Process notes +- The `NFSConfig` field was already on `ServerConfig` (added by N1-6 for cleanup), + but was never populated from settings in `server_foreground.go`. Fixed that gap. +- `SelectWorkspaceBackend` exists in `pkg/runtime` but is not yet called in production + dispatch flow — NFS fields on `StartConfig` are also not populated by the broker. + The reconciler wiring is the broker-level mount guard; K8s-level NFS integration + flows through the runtime's `buildPod` path. diff --git a/.design/project-log/2026-06-03-n2-2b-initlock.md b/.design/project-log/2026-06-03-n2-2b-initlock.md new file mode 100644 index 000000000..a16d9f227 --- /dev/null +++ b/.design/project-log/2026-06-03-n2-2b-initlock.md @@ -0,0 +1,60 @@ +# N2-2b: Advisory-lock guard for K8s init-container provisioning + +**Date**: 2026-06-03 +**Agent**: k8s-lock-agent +**Branch**: `nfs/n2-2b-initlock` +**Commit**: `ed023f2e` + +## Summary + +Added the per-project Postgres advisory lock to the K8s init-container +provisioning path, closing risk RN1 (concurrent first-clone corruption +when two pods for the same project are scheduled on different nodes +sharing an NFS volume). + +## Problem + +N2-2 shipped sentinel-only guarding for the init container. The sentinel +file check is check-then-act and cannot prevent two pods on different +nodes from both seeing "no sentinel" and `git clone`-ing concurrently +into the same NFS subPath, causing corruption. + +## Solution + +The advisory lock cannot live in the init-container shell (no DB access), +so it is acquired in the Go-side `Run()` method before `buildPod()`: + +1. **Lock winner** (`TryAdvisoryLockObject` returns `acquired=true`): + - Injects the existing cloning init container (checks sentinel, clones if absent) + - Lock held through `waitForPodReady()` (init containers complete), then released via defer + +2. **Lock loser** (`acquired=false`): + - Injects a wait-for-sentinel init container that polls for `.scion-provisioned` with a 300s bounded timeout + - No clone attempt + +3. **Lock error**: dispatch fails immediately (no unguarded clone) + +4. **No locker** (nil / SQLite): sentinel-only fallback (single-node safe) + +All gated on `backend=nfs` — zero behavior change for local backend. + +## Files Changed + +- `pkg/runtime/interface.go` — Added `Locker` and `nfsProvisionLockLost` fields to `RunConfig` +- `pkg/runtime/k8s_runtime.go` — Advisory lock acquisition in `Run()`, updated `buildPod()` init container logic, added `nfsWaitForSentinelScript()` +- `pkg/runtime/k8s_nfs_test.go` — 12 new tests covering winner/loser/error/no-locker/local/multi-project scenarios + +## Verification + +- `go build ./...` — clean +- `go vet ./pkg/runtime/...` — clean +- `go test ./pkg/runtime/...` — all pass (24s, includes 3s timeouts for Run-level tests) +- `go test ./pkg/store/...` — all pass +- `go test ./pkg/agent/...` — all pass + +## Design Notes + +- Reuses N1-4's `TryAdvisoryLockObject` + `LockWorkspaceProvision` + `StableProjectHash` from `pkg/store/concurrency.go` +- Lock lifetime mirrors N1-4's "hold during clone" pattern +- The `nfsProvisionLockLost` field is unexported (set internally by `Run()`, used by `buildPod()` in the same package) +- Wait-for-sentinel script uses 300s timeout (5 min) with 2s poll interval diff --git a/.design/project-log/2026-06-03-n2-gke-nfs-workspace.md b/.design/project-log/2026-06-03-n2-gke-nfs-workspace.md new file mode 100644 index 000000000..24865f576 --- /dev/null +++ b/.design/project-log/2026-06-03-n2-gke-nfs-workspace.md @@ -0,0 +1,68 @@ +# N2-1..N2-5: GKE NFS Workspace Realization (Wave 2, Model B) + +**Date:** 2026-06-03 +**Agent:** k8s-agent +**Branch:** `nfs/n2-gke` (from `postgres/wave-b-integration`) + +## Summary + +Implemented all five N2 tasks for GKE NFS workspace support in +`pkg/runtime/k8s_runtime.go`. These changes realize the NFS workspace +backend on Kubernetes, converting the design's §5 (Model B) into +working pod spec transformations. + +## Commits + +| SHA | Task | Description | +|-----|------|-------------| +| `75032d8a` | N2-1 | NFS-backed workspace volume — replace EmptyDir with PVC+subPath | +| `2da9afbc` | N2-2 | Init-container workspace provisioning with sentinel idempotency | +| `caf874d5` | N2-3 | Skip workspace kubectl cp when backend=nfs | +| `36737080` | N2-4 | Stable FSGroup/UID (NFS GID default 1000) | +| `45b95293` | N2-5 | Generalize shared-dir PVC helpers for NFS subPath | + +## Design Decisions + +1. **PVC+subPath isolation (N2-1):** Each NFS pod mounts the shared PVC with + `subPath: projects//workspace`, ensuring pods only see their project's + subtree — never the export root. Falls back to EmptyDir if NFSPVClaimName + is empty. + +2. **Init-container provisioning (N2-2):** Uses a `workspace-provision` init + container that checks `.scion-provisioned` sentinel before cloning. The + advisory lock is NOT used in-pod — init containers serialize per-pod + naturally, and the sentinel provides cross-pod idempotency. Full advisory + lock integration deferred to NM2 live cluster gate. + +3. **Workspace sync skip (N2-3):** NFS workspace bytes are pre-populated by + the init container, so kubectl cp is skipped. Home-dir/secret sync and the + startup gate are RETAINED for both backends. + +4. **FSGroup branching (N2-4):** NFS pods use stable GID (config or default + 1000) instead of host GID. This avoids permission issues across nodes. + +5. **Shared-dir subPath (N2-5):** NFS shared dirs mount from the same PVC + with `subPath: projects//shared-dirs/`, eliminating per-dir PVCs. + Refactored into generalized `ensureProjectRWXClaim`/`cleanupProjectRWXClaims` + helpers. + +## New RunConfig Fields + +- `NFSPVClaimName` — PVC name for the NFS workspace volume +- `NFSSubPath` — project-scoped subPath within the PVC +- `NFSStorageClass` — StorageClass for NFS PVCs +- `GitCloneForInit` — git clone config for init-container provisioning + +## Tests + +All changes include unit tests in `pkg/runtime/k8s_nfs_test.go`: +- `go build ./...` clean +- `go vet` clean +- All existing k8s_runtime tests pass (no regressions) +- 20+ new test cases covering NFS and local backend paths + +## Zero Behavior Change Guarantee + +Every NFS branch is gated on `config.WorkspaceBackendName == "nfs"`. +When backend is local (default/empty), all five tasks produce exactly +the same pod spec as before. diff --git a/.design/project-log/2026-06-03-nfs-config-foundation-n0.md b/.design/project-log/2026-06-03-nfs-config-foundation-n0.md new file mode 100644 index 000000000..d09055269 --- /dev/null +++ b/.design/project-log/2026-06-03-nfs-config-foundation-n0.md @@ -0,0 +1,35 @@ +# NFS Workspace Config Foundation (N0-1, N0-2, N0-3) + +**Agent:** config-agent +**Date:** 2026-06-03 +**Branch:** postgres/wave-b-integration +**Commit:** d8f8c987 + +## Summary + +Landed the Phase 0 config-only foundation for NFS-backed workspace storage. +Three tasks, all no-op at runtime (backend defaults to "local"). + +### N0-1: Workspace-storage config block +- Added `V1WorkspaceStorageConfig`, `V1NFSConfig`, `V1NFSShare` types to `pkg/config/settings_v1.go` +- Wired `WorkspaceStorage` into `V1ServerConfig` +- `ApplyNFSDefaults()` fills mount_options, UID/GID=1000, subpath_root="projects" when backend=nfs +- No NFS block materialized for local/empty backend +- Tests: YAML round-trip via LoadVersionedSettings, JSON round-trip, defaults, nil-safety + +### N0-2: VolumeMount nfs type + validation +- Added `Server` field to `VolumeMount` (`pkg/api/types.go`) +- Extended `Validate()` with `case "nfs":` requiring Server+Source+Target +- Updated default error message to list all three valid types +- Flipped existing nfs fixtures from rejected→valid in both `types_test.go` and `templates_test.go` +- Added negative cases: nfs-missing-server, nfs-missing-source, genuinely-invalid type ("bogus") + +### N0-3: Workspace-sharing-mode enum alignment +- Added typed `WorkspaceSharingMode` (string) with 3 canonical values (`pkg/store/models.go`) +- `ResolveWorkspaceSharingMode(label)` maps legacy label values, new values, empty/unknown→default +- Existing `LabelWorkspaceMode`, `WorkspaceModeShared`, `WorkspaceModePerAgent` constants unchanged (lossless) +- Unit tests in new `pkg/store/models_test.go` cover all cases + +## Process Notes +- Pre-existing test failures in pkg/config (IsInsideProject, FindProjectRoot, etc.) are environment-dependent and unrelated to these changes +- All N0-specific tests green; `go build ./...` clean diff --git a/.design/project-log/2026-06-03-nm1b-qa-wired-auto-mount.md b/.design/project-log/2026-06-03-nm1b-qa-wired-auto-mount.md new file mode 100644 index 000000000..0a7e2bdfe --- /dev/null +++ b/.design/project-log/2026-06-03-nm1b-qa-wired-auto-mount.md @@ -0,0 +1,107 @@ +# NM1b QA — WIRED Model-A NFS Path Re-validation + +**Date:** 2026-06-03 +**Agent:** qa-agent-2 +**Binary:** commit 1eaecd95 (N1-7 wired reconciler + vers=3 default) +**VMs:** scion-integration, scion-integration2 (us-central1-a, deploy-demo-test) + +## Summary + +**Overall: PASS (with one deploy-notes finding)** + +NM1b re-validates the WIRED Model-A path: the broker auto-mounts NFS at startup +via the NFSMountReconciler (no manual mount), serves healthy NFS status on /healthz, +and the Postgres advisory lock mechanism prevents provisioning races. + +## Per-Step Results + +### Step 1: Build WIRED binary — PASS +- Built from isolated temp clone at `/tmp/nm1b-build/scion-build` +- Confirmed `git rev-parse HEAD` = 1eaecd95 +- Binary version output: `Commit: 1eaecd95, Build Time: 2026-06-03T02:07:34Z` + +### Step 2: Deploy to both VMs — PASS +- Baseline captured: both VMs running commit 9a998934 +- Backup created: `/usr/local/bin/scion.bak-nm1b` +- New binary installed, version confirmed on both VMs + +### Step 3: Configure backend=nfs — PASS (with lesson learned) +- **Lesson:** Settings file MUST include `schema_version: "1"` or the startup migration + process strips unrecognized fields. First attempt lost workspace_storage config. +- Correct format: versioned settings with `schema_version: "1"` + `server.workspace_storage` + block alongside `server.broker.broker_id`. +- mount_options omitted to test vers=3 default. + +### Step 4: Restart + Verify AUTO-MOUNT — PASS ★ (KEY NM1b GATE) + +**Wiring confirmed on both VMs.** Journal evidence: + +``` +INFO "NFS mount reconciler initialized" shares=1 mountRoot="/mnt/nfs" +INFO "Reconciling NFS share" shareID="demo" target="/mnt/nfs/demo" server="10.45.255.170" export="/scion_share" +INFO "Mounting NFS share" source="10.45.255.170:/scion_share" target="/mnt/nfs/demo" options="vers=3,hard,nconnect=4,_netdev" +INFO "NFS share mounted" shareID="demo" target="/mnt/nfs/demo" +INFO "NFS mounts reconciled at startup" status="healthy" +``` + +- Mount verified: `10.45.255.170:/scion_share on /mnt/nfs/demo type nfs (rw,relatime,vers=3,...,nconnect=4,...)` +- **vers=3 default confirmed** — mount_options was empty in config, code defaulted to `vers=3,hard,nconnect=4,_netdev` +- /healthz: `{"checks":{"docker":"available","nfs_mounts":"healthy"}}` on both VMs + +**Finding: CAP_SYS_ADMIN insufficient for mount.nfs** +- The broker service runs as user `scion` (uid=1002). `AmbientCapabilities=CAP_SYS_ADMIN` + was applied (confirmed in `/proc//status` CapEff=0x200000), BUT `mount.nfs` is a + setuid binary that checks `uid==0`, not capabilities. +- **Workaround for NM1b:** Ran service as `User=root` via systemd override. +- **Recommendation for production:** Either run broker as root, or modify + `ExecMountChecker.Mount()` to use `sudo mount` (requires sudoers entry), or use + `mount(2)` syscall directly in Go (which respects CAP_SYS_ADMIN). Update + `NFS_DEPLOY_NOTES.md` accordingly — the CAP_SYS_ADMIN approach documented there + does not work with `mount.nfs`. + +### Step 5: Live Tests + +#### (a) Cross-node visibility — PASS +- VM1 wrote file → VM2 read identical content +- VM2 wrote file → VM1 read identical content +- Files visible within seconds across nodes + +#### (b) Provisioning race (advisory lock) — PASS +- **Postgres advisory lock test:** VM1 acquired `pg_try_advisory_lock(999999)` → VM2 attempt + returned `f` (false). After VM1 released, VM2 acquired successfully. Lock contention works. +- **Sentinel reuse test:** VM1 provisioned fresh project dir with sentinel → VM2 found + sentinel, correctly reused workspace instead of re-provisioning. No corruption. +- CloudSQL reachable from both VMs: `SELECT 1 as connected` succeeded. + +#### (d) Cross-node UID 1000 writability — PASS +- Files written by VM1 as uid 1000 writable by VM2 as uid 1000 +- Files written by VM2 writable by VM1 +- No permission denials across nodes + +#### (restart) Restart idempotency — PASS +- Pre-restart: 1 NFS mount +- Journal after restart: `"NFS share already mounted correctly"` (reconciler detected existing mount) +- Post-restart: still exactly 1 mount (no double-mount) +- Data survived restart, healthz healthy + +### Step 6: Restore baseline — PASS +- NFS config removed from settings.yaml on both VMs +- NFS unmounted on both VMs +- Systemd overrides removed (User=scion restored) +- Services restarted and healthy +- Binary left at 1eaecd95 (integration tip, backward-compatible) +- Backup at `/usr/local/bin/scion.bak-nm1b` (commit 9a998934) + +## Final VM State +- **Binary:** 1eaecd95 (WIRED, left in place — backward compatible) +- **Config:** NFS block removed, broker_id preserved +- **NFS:** Not mounted (config removed) +- **Services:** Running, healthy +- **Overrides:** Removed + +## Observations +1. Settings migration strips unknown fields from legacy-format files — always include + `schema_version: "1"` when writing settings.yaml. +2. The `mountpoint -q` command returns exit code 32 (not 1) when the path exists but is + not a mountpoint. The code handles this correctly (treats as "not mounted"). +3. nconnect=4 works fine on kernel 6.8.0-1054-gcp with NFSv3 on Filestore BASIC_HDD. diff --git a/.design/project-log/2026-06-03-nm2-gke-nfs-workspace.md b/.design/project-log/2026-06-03-nm2-gke-nfs-workspace.md new file mode 100644 index 000000000..cb40beb8a --- /dev/null +++ b/.design/project-log/2026-06-03-nm2-gke-nfs-workspace.md @@ -0,0 +1,30 @@ +# NM2 — GKE NFS Workspace Live Test — PASSED + +**Date:** 2026-06-03 +**Agent:** qa-agent +**Branch:** `postgres/wave-b-integration` @ `4a6ccf50` +**Gate:** NM2 (Model B — GKE Autopilot + Filestore) +**Full report:** `/scion-volumes/scratchpad/NM2-REPORT.md` + +## Result: PASS (4 full + 1 partial-expected) + +| Scenario | Result | +|----------|--------| +| (a) NFS-backed workspace PVC+subPath, not EmptyDir; init-container pre-populates | **PASS** | +| (b) Init-container provisioning race-safety via sentinel | **PARTIAL** — sequential sentinel works; concurrent race needs advisory lock (N2-2b), which requires hub-mediated provisioning with CloudSQL. Expected limitation in direct-pod test. | +| (c) kubectl cp skip for NFS backend (verified at `k8s_runtime.go:394`) | **PASS** | +| (d) Stable FSGroup/UID 1000 on all NFS-backed pods | **PASS** | +| (e) Shared dirs on NFS via same PVC + distinct subPaths | **PASS** | + +## Infrastructure Provisioned in `scion-demo-cluster` + +- Namespace `scion-agents` created +- Static PV `scion-workspaces` + PVC `scion-workspaces` bound to Filestore `10.45.255.170:/scion_share` (RWX, NFSv3, 1Ti) — left in place for future tests +- Binary `4a6ccf50` confirmed working on GKE Autopilot + +## Key Observations + +- GKE Autopilot auto-injects `seccompProfile: RuntimeDefault` — no node-level tuning possible, all pod-level +- NFSv3 + nconnect=4 works on Autopilot nodes (confirmed in mount output) +- Filestore BASIC_HDD ownership issue: share root owned by 1002:1003 from NM1 setup; new project dirs need UID 1000 for pod writes. In production, the hub's provisioner handles this under the advisory lock. +- Advisory lock end-to-end (N2-2b) requires GKE pods to reach CloudSQL (35.202.106.255:5432) — network path not fully verified; this is the one remaining unconfirmed integration point for GKE Model B. diff --git a/.design/project-log/2026-06-04-pr304-review-fixes.md b/.design/project-log/2026-06-04-pr304-review-fixes.md new file mode 100644 index 000000000..13606eafd --- /dev/null +++ b/.design/project-log/2026-06-04-pr304-review-fixes.md @@ -0,0 +1,30 @@ +# PR #304 Review Feedback Fixes + +**Date**: 2026-06-04 +**Branch**: `pr/postgres-core` +**PR**: #304 + +## Changes Made + +Addressed all three review comments from the automated code review: + +### 1. Context leak in `initStore` (medium priority) +- `initStore` was using `context.Background()` for `s.Migrate()` and `s.Ping()`. +- Refactored `initStore` to accept a `context.Context` parameter. +- The server's cancellable context (from `runServerStart`) is now threaded through, allowing graceful cancellation on Ctrl+C during startup. + +### 2. Goroutine/connection leak in event publisher (high priority) +- `initWebServer` was calling `newEventPublisher(context.Background(), cfg)`. +- The Postgres event publisher starts a LISTEN/NOTIFY goroutine that only stops when its context is cancelled. With `context.Background()`, this goroutine and its connection leak on shutdown. +- Refactored `initWebServer` to accept a `context.Context` parameter and pass the server's cancellable context to `newEventPublisher`. +- Note: the standalone hub path (non-web mode) at line 236 already used `ctx` correctly. + +### 3. DSN parsing for `file://` prefix (medium priority) +- `parseSQLiteSourceDSN` only had a `file:` prefix handler, so `file:///var/lib/hub.db` was trimmed to `///var/lib/hub.db` (triple slash). +- Added a `file://` case before the `file:` case, so `file:///abs` correctly resolves to `/abs`. +- Added three test cases covering `file://`, `file:///`, and `file:///...?query` forms. + +## Verification +- `go build ./cmd/...` passes +- `go vet ./cmd/...` passes +- All DSN parsing tests pass (including new cases) diff --git a/.design/project-log/2026-06-05-auth-proxy-mode-phase1.md b/.design/project-log/2026-06-05-auth-proxy-mode-phase1.md new file mode 100644 index 000000000..71e921531 --- /dev/null +++ b/.design/project-log/2026-06-05-auth-proxy-mode-phase1.md @@ -0,0 +1,102 @@ +# Auth Proxy Mode — Phase 1 Implementation + +**Date:** 2026-06-05 +**Branch:** scion/auth-proxy-mode +**Author:** Scion Agent (auth-proxy-phase1) + +## Summary + +Implemented Phase 1 (inbound human IAP auth) of the auth-proxy-mode feature, +delivering items 2–5 of the design plan in `.design/auth-proxy-mode.md`. + +## Files Added/Changed + +### New +- `pkg/hub/proxyauth.go` — ProxyAuthenticator interface, ProxyUserInfo struct, + IAPAuthenticator (ES256 JWT verification via go-jose/v4), JWKS cache +- `pkg/hub/proxyauth_test.go` — 13 unit tests + +### Modified +- `pkg/config/hub_config.go` — Added `Mode`, `Proxy` (ProxyAuthConfig/IAPAuthConfig) + to DevAuthConfig +- `pkg/config/settings_v1.go` — Added `Mode`, `Proxy` (V1ProxyConfig/V1IAPConfig) + to V1AuthConfig; updated both conversion functions; extended compound fields + and section names for env var mapping +- `pkg/hub/auth.go` — Replaced IP-only extractProxyUser branch with + ProxyAuthenticator path; added ProxyUserCache (60s TTL); + MakeProxyUserProvisioner; added ProxyAuthenticator/ProxyUserProvisioner + fields to AuthConfig +- `pkg/hub/handlers_auth.go` — Added ErrUserSuspended; suspended-user gate in + provisionUser; updated all 4 provisionUser callers to handle ErrUserSuspended +- `pkg/hub/server.go` — Added AuthMode/ProxyAuth to ServerConfig; wired into + authConfig with MakeProxyUserProvisioner +- `pkg/hub/web.go` — Added AuthMode to WebServerConfig; handleAuthProviders + returns empty in proxy mode; handleLogout redirects to IAP clear_login_cookie + in proxy mode +- `cmd/server_foreground.go` — Construct IAPAuthenticator when mode==proxy && + provider==iap; wire AuthMode into hub and web configs + +## Design Decisions + +### Audience/Issuer/Exp Validation +- **Audience**: mandatory binding — IAPAuthenticator.Audience must be set, + validated against JWT aud claim +- **Issuer**: defaults to `https://cloud.google.com/iap`, overridable via + struct field for testing +- **Clock skew**: ±30s leeway on exp/iat +- **JWKS URL**: defaults to gstatic, overridable for testing + +### JWKS Cache Design +- Lazy fetch on first request +- Proactive background refresh when cache > 1 hour old +- On-miss refresh for unknown kid (key rotation) +- Transient failure tolerance: serves last-good keys if fetch fails +- 5s debounce to prevent stampede + +### Resolution Cache +- 60s TTL keyed by verified email (per design Decision 3) +- JWT signature verification runs every request — only the provisionUser + store lookup is cached +- Implemented as ProxyUserCache (sync.RWMutex + map) + +### Suspended User Gate +- Added to provisionUser — rejects Status=="suspended" with ErrUserSuspended +- Intentional behavior change closing the pre-existing OAuth suspended-login gap + documented in Phase 0's NOTE comment +- All 4 provisionUser callers updated to surface 403 "user_suspended" + +### Proxy Precedence +- Proxy authenticator runs AFTER agent token (step 1), broker HMAC (step 2), + and bearer token (step 3) — ensuring app-layer credentials take priority +- When no ProxyAuthenticator is configured, legacy extractProxyUser (IP-trust) + is preserved for backward compatibility + +## Test Results + +### New Tests (all passing) +13 tests in proxyauth_test.go: +- Valid assertion → correct ProxyUserInfo +- Missing header → (nil, nil) fall-through +- Bad signature → error +- Wrong audience → error +- Wrong issuer → error +- Expired token → error +- Custom issuer override +- Unknown kid triggers JWKS refresh +- Strip prefix +- Email lowercasing +- HD claim +- Name() returns "iap" +- JWKS transient failure tolerance + +### Pre-existing Failures (unchanged) +~15 pre-existing "invalid UUID" failures in other hub tests (unrelated to auth): +TestCreateAgent_ResumeFromStoppedStatus, TestPopulateAgentConfig_*, etc. +~5 pre-existing config test failures from leaked SCION_ env vars in sandbox. + +## Flags / Notes + +- **HeaderProxyAuthenticator** (refactoring extractProxyUser behind the + interface) was left as a TODO — the legacy path is preserved but not + refactored behind the interface. Lower priority per design doc. +- **No new dependencies** — uses already-vendored go-jose/go-jose/v4. diff --git a/.design/project-log/2026-06-05-broker-disconnect-race-fix.md b/.design/project-log/2026-06-05-broker-disconnect-race-fix.md new file mode 100644 index 000000000..ec8ea43ad --- /dev/null +++ b/.design/project-log/2026-06-05-broker-disconnect-race-fix.md @@ -0,0 +1,48 @@ +# Project Log: Broker Disconnect Reconnect Race Fix (Issue #131) + +**Date:** 2026-06-05 +**Task:** Unify broker disconnect race fix from two branches into PR #303 + +## Problem + +When a broker disconnects and reconnects rapidly, the stale disconnect callback's +offline stamp can clobber the new connection's online status. The root cause is a +TOCTOU race: `ReleaseRuntimeBrokerConnection` and `UpdateRuntimeBrokerHeartbeat` +were separate calls — the heartbeat update has no session guard and unconditionally +overwrites status. Provider statuses are also clobbered and never restored by +heartbeats, leaving the broker permanently invisible until hub restart. + +## Solution + +Added `ReleaseAndMarkBrokerOffline` to the store interface — a single CAS write +that atomically clears affinity AND stamps status=offline, only if the session +still matches. If a concurrent reconnect has already claimed the broker with a +new session, the compare fails and the callback is a no-op. + +Also added a re-check guard in `server.go` before updating provider statuses: +after the atomic release, re-read the broker to confirm no concurrent +`markBrokerOnline` has re-claimed it before stamping providers offline. + +## Branch Unification + +Two branches addressed this issue: +- `scion/dev-issue-131` (PR #303): had only a docs/project-log commit, no code fix +- `origin/fix/session-guarded-broker-disconnect` (fork PR #144): had the complete + code fix with tests + +The fork branch's fix was the more complete solution. Rebased PR #303 onto +upstream main and cherry-picked the fork's fix commit to produce a single +unified branch. + +## Files Changed + +- `pkg/store/store.go` — added `ReleaseAndMarkBrokerOffline` to `RuntimeBrokerStore` interface +- `pkg/store/entadapter/project_store.go` — implemented `ReleaseAndMarkBrokerOffline` with CAS retry loop +- `pkg/hub/server.go` — rewired `SetOnDisconnect` callback to use the atomic method + provider re-check guard +- `pkg/store/entadapter/broker_affinity_test.go` — 4 new tests covering the atomic method + +## Verification + +- All 10 broker affinity tests pass (4 new + 6 existing) +- Hub package compiles cleanly +- Pre-existing test failures in `pkg/hub` (unrelated to this change) confirmed on upstream main diff --git a/.design/project-log/2026-06-05-extract-provision-user.md b/.design/project-log/2026-06-05-extract-provision-user.md new file mode 100644 index 000000000..2ead154cc --- /dev/null +++ b/.design/project-log/2026-06-05-extract-provision-user.md @@ -0,0 +1,51 @@ +# Extract provisionUser — Phase 0 auth-proxy-mode + +**Date**: 2026-06-05 +**Branch**: scion/auth-proxy-mode +**Commit**: refactor(hub): extract provisionUser, dedupe OAuth find-or-create + +## What changed + +Extracted four identical find-or-create-user blocks from OAuth handlers +into a single `provisionUser` method on `Server` in `handlers_auth.go`. + +### Call sites refactored (all four) +1. `handleAuthLogin` (~line 258) — device flow login +2. `handleAuthToken` (~line 402) — OAuth code exchange (web/CLI) +3. `handleCLIAuthToken` (~line 936) — CLI-specific OAuth code exchange +4. `completeOAuthLogin` (~line 1192) — shared device flow completion + +All four were semantically identical — same auth check, same find-or-create +logic, same profile backfill, same admin promotion, same hub membership +enrollment. No differences that prevented safe consolidation. + +### New types +- `ExternalUserInfo` struct (Email, DisplayName, AvatarURL) — decoupled + from `OAuthUserInfo` so the proxy middleware can reuse it +- `ErrAccessDenied` sentinel error — callers map to HTTP 403 + +### Tests added +8 subtests in `TestProvisionUser`: create, update, backfill, admin +promotion, domain restriction, invite-only, admin bypass, idempotency. + +## Suspended-user finding + +**None of the four OAuth blocks check `user.Status == "suspended"`.** +A suspended user can currently log in via any OAuth path and receive +valid tokens. The design doc says provisionUser should reject suspended +users, but adding this check would change existing OAuth behavior. + +**Decision**: do NOT add the check in Phase 0. Phase 1 (proxy auth) will +add it if needed, after a separate decision on whether to also gate the +OAuth path. + +## Pre-existing test failures + +15 tests in `pkg/hub` fail with "invalid UUID" errors (e.g. +`TestCreateAgent_ResumeFromStoppedStatus`). These are pre-existing and +unrelated to this change — they use non-UUID string IDs that the store +now validates. All auth-related tests pass. + +## Net impact +- 2 files changed, 342 insertions, 175 deletions +- ~75 fewer lines of production code (4x35 duplicated → 1x45 shared) diff --git a/.design/project-log/2026-06-05-pr305-review-feedback.md b/.design/project-log/2026-06-05-pr305-review-feedback.md new file mode 100644 index 000000000..73d1195ba --- /dev/null +++ b/.design/project-log/2026-06-05-pr305-review-feedback.md @@ -0,0 +1,32 @@ +# PR #305 Review Feedback — First Fix Round + +**Date:** 2026-06-05 +**PR:** #305 — feat(hub): multi-node broker dispatch +**Branch:** pr/broker-dispatch +**Commit:** c5f8b3c + +## Summary + +Addressed all 6 review comments from gemini-code-assist on PR #305. + +### HIGH Priority Fixes + +1. **server_migrate.go — nil-checked deferred close**: Changed `defer src.Close()` to a nil-checked closure so the source DB can be manually closed and set to nil before `dropSQLiteFile`, preventing Windows sharing violations. + +2. **server_migrate.go — close before drop**: Added explicit `src.Close()` + `src = nil` before the `dropSQLiteFile` call in the `migrateDropSource` path. + +3. **server_foreground.go — stale closure capture**: Moved `mgr := hubSrv.GetControlChannelManager()` inside the `ownsLocally` closure. Previously it was captured once at closure creation time, so if the manager was nil at that point but initialized later, `ownsLocally` would permanently return false. + +### MEDIUM Priority Fixes + +4. **server_migrate.go — file:// prefix handling**: Added a `file://` case before the `file:` case in `parseSQLiteSourceDSN` so that `file:///tmp/hub.db` correctly resolves to `/tmp/hub.db` instead of `//tmp/hub.db`. + +5. **server_migrate_test.go — triple-slash test**: Added a test case verifying `file:///tmp/hub.db` is parsed correctly. + +6. **server_test.go — subtest name sanitization**: Used `strings.ReplaceAll(t.Name(), "/", "_")` in `newTestStore` to prevent SQLite from interpreting subtest slashes as directory paths. + +## Verification + +- `gofmt` clean on all changed files +- `go vet ./cmd/` passes +- All relevant tests pass including the new `file_url_with_triple_slashes` test case diff --git a/.design/project-log/2026-06-05-pr306-review-fixes.md b/.design/project-log/2026-06-05-pr306-review-fixes.md new file mode 100644 index 000000000..1b9c00887 --- /dev/null +++ b/.design/project-log/2026-06-05-pr306-review-fixes.md @@ -0,0 +1,26 @@ +# PR #306 Review Feedback — First Fix Round + +**Date:** 2026-06-05 +**Branch:** pr/nfs-workspace +**PR:** https://github.com/GoogleCloudPlatform/scion/pull/306 + +## Review Comments Addressed + +All 6 review comments from gemini-code-assist were addressed: + +1. **HIGH — `cmd/server_foreground.go`**: Added `os.Stat` file existence check in `maybeMigrateLegacySQLite` before calling `IsLegacyRawSQLSchema`. Prevents errors on fresh installs where the database file doesn't exist yet. + +2. **MEDIUM — `.claude/scheduled_tasks.lock`**: Removed accidentally committed lock file from the repository. + +3. **MEDIUM — `.gitignore`**: Added `.claude/` to `.gitignore` to prevent future accidental commits of Claude temporary files. Also added `fixturegen` binary. + +4. **MEDIUM — `internal/fixturegen/main.go`**: Changed `copyFile` to use `defer out.Close()` so the file descriptor is always closed, even if a panic occurs during `io.Copy`. + +5. **MEDIUM — `cmd/server_migrate.go`**: Added non-negative validation for `--batch-size` flag before proceeding with migration. + +6. **MEDIUM — `pkg/config/settings_v1.go`**: Added `ValidateNFS()` method on `V1WorkspaceStorageConfig` that returns an error when backend is `"nfs"` but no shares are defined. Wired into server startup in `cmd/server_foreground.go`. Added 4 test cases covering: empty shares error, valid shares pass, local backend skip, nil receiver safety. + +## Additional + +- Ran `make fmt` to fix pre-existing gofmt issues across the codebase (committed separately as `style: run gofmt on pre-existing formatting issues`). +- Pre-existing test failures in `pkg/config` (5 tests unrelated to this PR) confirmed as pre-existing. diff --git a/.design/project-log/2026-06-05-wp0-agent-schema-visibility-cleanup.md b/.design/project-log/2026-06-05-wp0-agent-schema-visibility-cleanup.md new file mode 100644 index 000000000..92c7b5a21 --- /dev/null +++ b/.design/project-log/2026-06-05-wp0-agent-schema-visibility-cleanup.md @@ -0,0 +1,39 @@ +# WP-0: Agent Schema — project_id Index + Drop Inert Visibility + +**Date**: 2026-06-05 +**Branch**: `design/project-visibility-membership` +**Scope**: Ent schema + codegen foundation for the project-visibility feature + +## Changes + +### Schema (`pkg/ent/schema/agent.go`) +- Added standalone index `index.Fields("project_id")` — enables efficient queries filtering agents by project without requiring the slug in the predicate. The existing unique composite `index.Fields("slug", "project_id").Unique()` is preserved. +- Removed the inert `field.String("visibility").Default("private")` from the Agent schema. This field was hardcoded to "private" at creation, never user-settable, never enforced in access control, and never used for filtering. + +### Generated Code (`pkg/ent/**`) +- Regenerated via `go generate ./pkg/ent/...` — removes all Visibility-related generated helpers (SetVisibility, where predicates, mutation methods) from the Agent entity. The new project_id index appears in `migrate/schema.go`. + +### Reference Cleanup (25 files total) +- `pkg/store/models.go` — removed `Visibility` field from `Agent` struct and `ToAgentInfo()` conversion +- `pkg/api/types.go` — removed `Visibility` from `AgentInfo` +- `pkg/hub/events.go` — removed `Visibility` from `AgentCreatedEvent` struct and population +- `pkg/hub/handlers.go` — removed `Visibility: store.VisibilityPrivate` from agent creation +- `pkg/store/entadapter/agent_store.go` — removed all Visibility read/write in the Ent adapter +- `cmd/list.go` — removed Visibility from agent-to-AgentInfo mapping +- 12 test files across `pkg/hub/` and `pkg/store/storetest/` — removed `Visibility` from `store.Agent` struct literals + +### Untouched (intentionally) +- Project visibility (`pkg/ent/schema/project.go`, `store.Project`, `api.ProjectInfo`) — remains +- Template and HarnessConfig visibility — remains +- `api.NormalizeVisibility` — not added (out of scope; belongs to another WP) +- `web/`, broker, seed.go, authz — out of scope + +## Build & Test Results +- `go build ./...` — PASS +- `make test-fast` — all test packages pass except pre-existing failures: + - `pkg/hub` — `command_bus_test.go` build failure (undefined `recExec`/`requirePostgres`, pre-existing on main) + - `pkg/store/entadapter` — `broker_affinity_test.go` build failure (undefined helpers, pre-existing on main) + - `pkg/config` — 5 test failures (env-var leakage from container, pre-existing) + - `pkg/hubsync` — 2 test failures (pre-existing) + +No new test failures introduced by this change. diff --git a/.design/project-log/2026-06-06-wp-d-frontend-visibility.md b/.design/project-log/2026-06-06-wp-d-frontend-visibility.md new file mode 100644 index 000000000..3d7e2fba3 --- /dev/null +++ b/.design/project-log/2026-06-06-wp-d-frontend-visibility.md @@ -0,0 +1,43 @@ +# WP-D: Frontend Visibility UI Changes + +**Date:** 2026-06-06 +**Branch:** design/project-visibility-membership +**Commit:** fa2fb68 + +## Summary + +Implemented WP-D from the project-visibility implementation plan — the frontend +portion of the membership-based visibility model. + +## Changes + +### 1. `web/src/components/pages/project-create.ts` +- Removed the `visibility` `@state` property (was `'private'` default) +- Removed `visibility` from the POST request body +- Removed the entire visibility `` markup (Private/Team/Public options) +- New projects now default to creator-only; visibility is emergent from membership + +### 2. `web/src/components/shared/group-member-editor.ts` +- Added `showProjectMembersHint` boolean property (opt-in, default false) +- Added CSS for `.project-members-hint` styled hint box +- In compact mode (used by project settings), renders an info hint: + "To make this project visible to all hub users, add the **hub-members** group." +- Hint is scoped to project-members context only — does not appear on the admin + group detail page or any other usage of the editor. + +### 3. `web/src/components/pages/project-settings.ts` +- Set `showProjectMembersHint` on the `` instance +- Updated `sectionDescription` from "create and manage agents" to + "access this project and its agents" to reflect the new access model + +## Verification + +- `npm run typecheck` — passes cleanly (zero errors) +- `npm run lint` — all errors are pre-existing (confirmed by stash-checking + the same files before changes); no new lint issues introduced +- No Go files were touched + +## Observations + +- The lint configuration has widespread pre-existing prettier/formatting issues + across the web codebase. These are not related to this change. diff --git a/.design/project-log/auth-proxy-mode-phase2.md b/.design/project-log/auth-proxy-mode-phase2.md new file mode 100644 index 000000000..b24cc74b1 --- /dev/null +++ b/.design/project-log/auth-proxy-mode-phase2.md @@ -0,0 +1,75 @@ +# Auth Proxy Mode — Phase 2 Implementation + +**Date:** 2026-06-05 +**Branch:** scion/auth-proxy-mode +**Pushed SHA:** e9776f09 + +## Summary + +Implemented Phase 2 (outbound transport auth) of the auth-proxy-mode feature. +This enables agents to traverse IAP / Cloud Run invoker front doors using +hub-minted Google OIDC ID tokens alongside their existing scion agent tokens. + +## Commits (5 logical chunks) + +1. **a34bf6e** — config: add auth.transport config types +2. **a488b96** — hub: add TransportTokenMinter interface and implementations +3. **b617c84** — hub: wire transport token minter into ServerConfig and dispatch +4. **e6a11f6** — hub: extend token refresh response with generalized tokens[] array +5. **e9776f0** — sciontool: add pluggable OIDC transport for agent outbound auth + +## Key Design Decisions + +### TransportTokenMinter interface +- `MintIDToken(ctx, audience) (token, expiry, error)` — clean interface +- `gcpTransportMinter`: uses IAM Credentials API (`generateIdToken`) via + already-vendored `google.golang.org/api/iamcredentials/v1` +- `noopTransportMinter`: returns error when transport mode == "none" +- `FakeTransportMinter`: exported test double +- When mode == "none" or unset: minter is nil everywhere → zero impact + +### tokens[] backward compatibility +- Response keeps existing `token` + `expires_at` fields alongside `tokens[]` +- Old clients ignore `tokens[]`; new clients use both +- No breaking change for existing RefreshToken parsers + +### Dispatch vs refresh schema +- Dispatch uses individual env vars: `SCION_TRANSPORT_TOKEN`, + `SCION_TRANSPORT_AUDIENCE`, `SCION_TRANSPORT_TOKEN_EXPIRY` +- Refresh uses the `tokens[]` JSON array in the response +- Pragmatic deviation from "same schema" in the design doc — env vars match + existing dispatch conventions (SCION_AUTH_TOKEN, GITHUB_TOKEN, etc.) + +### Agent-side pluggable token source +- `injectedTokenSource`: hub-provided token from dispatch env var, refreshed + via tokens[] array on subsequent refreshes +- `metadataTokenSource`: GCE metadata server (PR #307 pattern, passthrough mode) +- Selection: SCION_TRANSPORT_TOKEN env → injected; on GCE → metadata; else → disabled +- Background ticker: uses shortest-lived token to drive refresh (5-min margin + for transport tokens vs 2h for scion tokens) + +## Files Changed + +| File | Action | +|------|--------| +| `pkg/config/hub_config.go` | Edit — add TransportAuthConfig | +| `pkg/config/settings_v1.go` | Edit — add V1TransportConfig, conversion, env mapping | +| `pkg/hub/transport_token.go` | **New** — minter interface, implementations, RefreshTokenEntry | +| `pkg/hub/transport_token_test.go` | **New** — 11 tests | +| `pkg/hub/server.go` | Edit — add transport fields to ServerConfig + Server | +| `pkg/hub/httpdispatcher.go` | Edit — add minter field, setter, inject in 3 dispatch paths | +| `pkg/hub/handlers.go` | Edit — extend handleAgentTokenRefresh with tokens[] | +| `cmd/server_foreground.go` | Edit — construct minter from config | +| `pkg/sciontool/hub/oidc.go` | **New** — pluggable OIDC sources + transport | +| `pkg/sciontool/hub/oidc_test.go` | **New** — 23 tests | +| `pkg/sciontool/hub/client.go` | Edit — oidcSource, RefreshTokenEntry, applyRefreshTokens | + +## Test Results + +- `go build ./...` — clean +- `go vet ./pkg/hub/... ./pkg/config/... ./pkg/sciontool/...` — clean +- `go test ./pkg/sciontool/hub/...` — PASS (all tests including 23 new) +- `go test ./pkg/hub/... -run Transport|JWT|Refresh` — PASS (11 new tests) +- `go test ./pkg/config/...` — 5 pre-existing failures (TestIsInsideProject etc.) +- `go test ./pkg/hub/...` — 15 pre-existing 'invalid UUID' failures +- No new failures introduced diff --git a/.design/project-log/auth-proxy-mode-phase3.md b/.design/project-log/auth-proxy-mode-phase3.md new file mode 100644 index 000000000..b121115bf --- /dev/null +++ b/.design/project-log/auth-proxy-mode-phase3.md @@ -0,0 +1,88 @@ +# Auth Proxy Mode — Phase 3 Implementation (Deployment Docs) + +**Date:** 2026-06-05 +**Branch:** scion/auth-proxy-mode +**Author:** Scion Agent (auth-proxy-phase3) + +## Summary + +Created the deployment guide for the IAP + Cloud Run-invoker topology, +completing Phase 3 (the final phase) of the auth-proxy-mode feature. + +## Files Added/Changed + +| File | Action | +|------|--------| +| `docs-site/src/content/docs/hub-admin/auth-proxy-iap.md` | **New** — Full deployment guide | +| `docs-site/astro.config.mjs` | Edit — Added sidebar entry under Hub Administration | +| `.design/project-log/auth-proxy-mode-phase3.md` | **New** — This project log | + +## Documentation Coverage + +The guide covers all five deliverable sections: + +1. **Overview** — Three exclusive auth modes (`oauth`, `proxy`, `dev`) and when to + pick proxy/IAP. +2. **Inbound (human IAP)** — `auth.mode=proxy` + `auth.proxy` config with full YAML + examples; audience format for GCE/GKE backend services vs App Engine; issuer/JWKS + overrides; `require_trusted_proxy_ip`; middleware precedence; provisioning behavior + (lazy, allow-list-gated, auto-create); suspended user rejection; logout semantics. +3. **Outbound (agent transport auth)** — Dual-layer model (outer OIDC + inner + X-Scion-Agent-Token); `auth.transport` config (`mode`, `oidc_audience`, + `platform_auth_sa`); dispatch env vars (`SCION_TRANSPORT_TOKEN`, + `SCION_TRANSPORT_AUDIENCE`, `SCION_TRANSPORT_TOKEN_EXPIRY`); refresh `tokens[]` + array; agent-side token source selection (injected vs metadata vs disabled); + audience selection for IAP vs Cloud Run invoker. +4. **Security notes** — Signed-only trust model; audience binding; IAP-only reachability; + JWKS rotation; clock skew; suspended users. +5. **End-to-end GCP setup checklist** — IAP enablement, OAuth client/audience, + transport SA creation, IAM bindings, hub config, verification steps, reference + to cloudrun scripts. + +## Config Key Verification + +All config keys and env vars were verified against the shipped code: + +### Settings.yaml keys (V1 snake_case format) +- `auth.mode` — V1AuthConfig.Mode +- `auth.proxy.provider` — V1ProxyConfig.Provider +- `auth.proxy.iap.audience` — V1IAPConfig.Audience +- `auth.proxy.iap.issuer` — V1IAPConfig.Issuer +- `auth.proxy.iap.jwks_url` — V1IAPConfig.JWKSURL +- `auth.proxy.require_trusted_proxy_ip` — V1ProxyConfig.RequireTrustedProxyIP +- `auth.transport.mode` — V1TransportConfig.Mode +- `auth.transport.oidc_audience` — V1TransportConfig.OIDCAudience +- `auth.transport.platform_auth_sa` — V1TransportConfig.PlatformAuthSA +- `auth.user_access_mode` — V1AuthConfig.UserAccessMode +- `auth.authorized_domains` — V1AuthConfig.AuthorizedDomains +- `hub.admin_emails` — V1ServerHubConfig.AdminEmails + +### Env vars (dispatch payload) +- `SCION_TRANSPORT_TOKEN` — httpdispatcher.go +- `SCION_TRANSPORT_AUDIENCE` — httpdispatcher.go +- `SCION_TRANSPORT_TOKEN_EXPIRY` — httpdispatcher.go + +### Agent-side env vars +- `SCION_HUB_OIDC_AUDIENCE` — oidc.go:EnvHubOIDCAudience +- `SCION_TRANSPORT_TOKEN` — oidc.go:EnvTransportToken +- `SCION_TRANSPORT_AUDIENCE` — oidc.go:EnvTransportAudience + +## Discrepancies Between Design Doc and Shipped Code + +### No discrepancies found +All config keys, env vars, and behavior documented in the guide match the +shipped implementation. Minor differences noted: + +- **Dispatch schema**: The design doc proposed using the same `tokens[]` JSON + shape for both dispatch and refresh. The shipped implementation uses individual + env vars for dispatch (`SCION_TRANSPORT_TOKEN`, `SCION_TRANSPORT_AUDIENCE`, + `SCION_TRANSPORT_TOKEN_EXPIRY`) and `tokens[]` JSON array for refresh. This was + already documented in the Phase 2 project log as a pragmatic deviation matching + existing dispatch conventions. + +## Build Verification + +- Docs build (`npm run build`) could not be run — requires Node.js ≥22.12.0, + only v20.20.2 available in the sandbox. +- Frontmatter format verified manually against sibling pages (title + description). +- Sidebar entry added to `astro.config.mjs` matching the existing pattern. diff --git a/.design/project-log/pr321-review-feedback.md b/.design/project-log/pr321-review-feedback.md new file mode 100644 index 000000000..3c62741f4 --- /dev/null +++ b/.design/project-log/pr321-review-feedback.md @@ -0,0 +1,35 @@ +# PR #321 Review Feedback — Multi-Node Session Fixes + +**Date:** 2026-06-06 +**PR:** GoogleCloudPlatform/scion#321 +**Branch:** postgres/delta-fixes +**Commit:** a1e715f + +## Summary + +Addressed 3 review comments from Gemini Code Assist on the multi-node session fixes PR. + +## Changes + +### 1. HIGH: SharedSigningSecret bypasses storage (pkg/hub/server.go) + +**Problem:** When `SharedSigningSecret` is configured, `ensureSigningKey` derives keys deterministically but returns immediately without persisting them to the secret backend. External consumers (e.g., `scion-chat-app`) rely on label-based auto-discovery from GCP Secret Manager to find signing keys. + +**Fix:** After deriving the key, call `syncSigningKeyToBackend()` to persist the derived key to the secret backend. This is a best-effort sync (warning on failure, non-fatal) since the key can always be re-derived. The sync uses the existing `syncSigningKeyToBackend` function which handles both the backend Set and the SQLite backup. + +### 2. MEDIUM: Missing session secret warning in hosted mode (cmd/server_foreground.go) + +**Problem:** In hosted mode, running without a session secret means each replica generates its own ephemeral key, completely breaking cross-replica sessions. + +**Fix:** Added a `log.Println("WARNING: ...")` at startup when `hostedMode && hubCfg.SharedSigningSecret == ""`. Chose a warning over a hard failure to avoid breaking existing single-node hosted deployments that may not have configured a session secret yet. + +### 3. MEDIUM: Nil guard in test (pkg/hub/web_test.go) + +**Problem:** `TestSessionStore_DifferentSecretCannotDecode` accessed `sessC.Values` without checking `sessC != nil`. + +**Fix:** Added `require.NotNil(t, sessC, ...)` before the `Values` access. + +## Observations + +- The `make ci` target shows pre-existing vet errors in `command_bus_test.go` (undefined `recExec`) and `broker_affinity_test.go` (undefined `newBroker`). These are cross-file test helper references that work with `go test` but fail `go vet` individually. Not related to this PR. +- The repo has many `gofmt` alignment diffs from the grove-to-project rename. These show up in `git diff` but were not included in this commit. diff --git a/.design/project-visibility-implementation-plan.md b/.design/project-visibility-implementation-plan.md new file mode 100644 index 000000000..de371b8c8 --- /dev/null +++ b/.design/project-visibility-implementation-plan.md @@ -0,0 +1,252 @@ +# Project Visibility — Implementation Plan + +Companion to `project-visibility-membership.md` (approved design). This plan +sequences the work into work-packages (WPs) with concrete file anchors, names the +one real architectural decision the design left implicit, and orders the WPs by +dependency so they can be parallelized across developer agents safely. + +Branch: `design/project-visibility-membership` (all work; PR at end, no merge). + +--- + +## 0. The one architectural decision to confirm: how "role-aware" is enforced + +The design (§3.3, OQ2→B) wants: **member = read-only, admin = create/manage, +owner = full**, all on the *single* members group. But the policy engine today +**cannot condition a policy binding on a member's role** — `PolicyBinding` is just +`(PrincipalType, PrincipalID)` (`pkg/store/models.go:1232`), and +`GetEffectiveGroups` returns group IDs only, dropping role +(`pkg/store/entadapter/group_store.go:735`). + +There are two ways to get role-aware behavior: + +- **(Recommended) Reuse the existing role bypass — purely subtractive.** Read is + granted by a project-scoped read policy bound to the members group → *everyone + in the group (any role) can read*. Create/stop/manage is **already** gated by + `isProjectOwnerOrAdmin` (`pkg/hub/authz.go:461`), which explicitly checks + `membership.Role == owner || admin`. So we **remove `create, stop_all` from the + members-group policy** and let the existing admin/owner bypass grant them. Net + effect: member→read-only, admin/owner→create/manage, with **no policy-engine + changes**. This is why the migration "bump members→admin" is needed: today every + member gets create via the group policy; after this change only admin/owner do. + - **Known limitation (flag, don't fix now):** `isProjectOwnerOrAdmin` checks the + user's *direct* membership row on the project members group. A user who is + admin only via a **nested** team group would get read (nesting resolves) but + not create. Acceptable for v1; note as follow-up. + +- **(Deferred alternative) Extend the policy engine** with a `role` on + `PolicyBinding`, carry role through `GetEffectiveGroups`, and filter on it in + `evaluatePolicies`. More expressive (role survives nesting) but invasive and not + required to meet the approved spec. Out of scope for this pass. + +**This plan assumes the recommended approach.** If ptone prefers the engine +extension, WP-B grows substantially — calling it out before we build. + +--- + +## 1. Work-package sequence & dependencies + +``` +WP-0 (schema/codegen, SOLO first) + │ + ├──► WP-B (authz/policy/enforcement/migration) ← behavior-changing core + ├──► WP-A (agent-list filter + caching) + ├──► WP-C (broker read scoping) + └──► WP-D (UI wiring) ← independent, can start anytime +``` + +WP-0 lands first and alone because it regenerates `pkg/ent/**` (codegen); doing it +once avoids merge conflicts in generated files. WP-A/B/C/D then run in parallel. +**WP-B is the only behavior-changing package** — its sub-steps (narrow read-all + +add project read + enforcement gaps) must land *together* and be tested as a unit, +or members lose visibility to their own projects mid-rollout. + +--- + +## WP-0 — Schema & codegen foundation (solo, lands first) + +1. **Add `project_id` index on agents** — `pkg/ent/schema/agent.go:176` `Indexes()` + currently has only `index.Fields("slug","project_id").Unique()`. Add + `index.Fields("project_id")`. Makes the `project_id IN (...)` filter (§3.5) + efficient. +2. **Drop the inert per-agent `visibility` field** — `pkg/ent/schema/agent.go:63` + (`field.String("visibility").Default("private")`). Remove the field and all code + references. NOTE: ent auto-migration does **not** drop the DB column by default, + so the column is left orphaned/harmless; a `WithDropColumn` cleanup can follow + later. Verify no code reads agent visibility for decisions (it's hardcoded + "private" today, so safe). +3. **Regenerate:** `go generate ./pkg/ent/...` (runs the `ent generate` in + `pkg/ent/generate.go:17`). Commit generated changes. +4. Leave the **project** `visibility` field in place (`project.go:74`) — the design + says it may be retired or repurposed; retiring the column is not required and we + stop authoring it from the UI in WP-D. Document it as internal/legacy. + +Verify: `make test-fast build`. + +--- + +## WP-B — Authz / policy / enforcement core (the behavior change) + +All in `pkg/hub`. Land these together. + +### B1 — Narrow the global read-all grant (§3.1) +`pkg/hub/seed.go:50` seeds `hub-member-read-all` with `ResourceType:"*"`, +`actions:[read,list]`. Replace the single `"*"` policy with **explicit per-type +allow policies** bound to `hub-members`: +- **KEEP** (hub-readable directory/catalog): `user`, `group`, `template`, + `harness_config` → seed read/list allows for these. +- **GATE** (remove from global read): `project`, `agent`, `broker` → no hub-wide + allow; visibility comes from membership. +- **SENSITIVE** (tighten): `policy`, `gcp_service_account`, + `secret`/`environment`/`variable` → no hub-wide read. Project-scoped instances + derive from associated-project membership (same gating as agents via the + project read policy in B2 where they share scope); hub-level ones stay + admin/owner-only (admin bypass already covers admins). +- Seeding is idempotent by policy name; ensure re-seed replaces the old wildcard + (delete-by-name or upsert) so existing hubs migrate. + +### B2 — Add project-scoped member read + role-aware create (§3.2, §3.3) +`createProjectMembersGroupAndPolicy` (`pkg/hub/handlers.go:3633`): +- **Add** a project-scoped read policy bound to the members group: + `scope=project, scopeID=, resourceType=project` and one for + `agent`, `actions:[read,list], effect=allow`. (Agents derive from project, §3.7 + — granting agent read at project scope to the members group is the mechanism.) +- **Remove `create, stop_all`** from the existing + `project::member-create-agents` policy (handlers.go:3730). Create/stop now + flow from the admin/owner role bypass (`isProjectOwnerOrAdmin`). Rename the + policy to `project::member-read` to match its new role. +- Idempotency: on re-run, replace the old create-policy with the read-policy + (handle existing hubs). + +### B3 — Migration backfill: bump existing members → admin (§3.3, §6) +`createProjectMembersGroupAndPolicy` already runs at startup for every project +(`pkg/hub/server.go:777` seedDefaultPoliciesAndGroups loop) and has a backfill +block (handlers.go:~3706 promotes sole member→owner). Add a **one-time, idempotent +bump**: any group member currently at `role=member` who predates this change → +`role=admin`, to preserve their create-agent ability. Guard so it runs once (e.g. +skip if any admin/owner already present beyond the creator, or gate on a stored +"migrated" marker / version). New members added post-migration default to `member` +(read-only) per `addGroupMember` default (`handlers_groups.go:509`) — unchanged. +- **Care:** do not bump the `hub-members` group's members (that would make everyone + admin everywhere). Only bump per-project `project::members` groups. + +### B4 — Close enforcement gaps (§3.4) +- **getProject** (`pkg/hub/handlers.go:5104`) does not gate read. Add + `CheckAccess(ctx, identity, Resource{Type:"project", ID, OwnerID}, ActionRead)`; + 403/404 on deny. Same for **getProjectAgent** (`handlers.go:4827`) — add a read + check (derives from project per §3.7). +- **Fail closed on nil identity:** `listProjects` (handlers.go:3289), + `listAgents` (handlers.go:276), `getProject`, single-agent GET currently treat + `identity == nil` as "skip filter / return everything." Change to: nil identity → + empty result or 401 (no anonymous read). `CheckAccess` already denies unknown + identity (`authz.go:95` default deny) — lean on it for the GETs; for the LISTs, + short-circuit to empty/401 when identity is nil. + +Verify: targeted tests for member-can-read-own-project, non-member-cannot, +hub-members-added→all-can-read, nil-identity→empty. + +--- + +## WP-A — Cross-project agent list filter + per-request caching (§3.5) + +`pkg/hub/handlers.go` `listAgents` (276): +- Apply `filter.MemberProjectIDs = resolveUserProjectIDs(...)` for the **default** + scope, not just `scope=shared` (currently only shared/mine set it). Default scope + = "agents in my projects." The SQL IN predicate already exists + (`agent_store.go:450` → `agent.ProjectIDIn`). +- **Cache `resolveUserProjectIDs`** (handlers.go:5960) once per request in + `r.Context()` (it does a BFS via `GetEffectiveGroups` + `GetGroupsByIDs`). Add a + context key + memoizing wrapper; reuse across listAgents/listProjects/capability + batches in the same request. +- Pagination stays SQL-honest (no post-hoc drop). `totalCount`/cursors remain + correct. + +(Depends on WP-0's `project_id` index for efficiency; logic itself is independent +of WP-B but should be tested after B so the filter semantics match.) + +--- + +## WP-C — Broker read scoping (§4) + +`pkg/hub` broker handlers + `handlers_broker_projects.go`: +- **Broker read** = owner OR hub admin OR member of ANY project the broker + contributes to. Resolve via `ProjectContributor` (project_id ↔ broker_id; + `pkg/ent/schema/projectcontributor.go`). Add a `CheckAccess`/derive helper that + loads contributing project IDs for a broker and tests intersection with the + caller's project set (`resolveUserProjectIDs`). +- **Broker list** scoped the same way: brokers you own + brokers contributing to a + project you're a member of. Add a filter keyed off `ProjectContributor`. + `broker_id` index already exists on `ProjectContributor` (no new index needed — + confirm during impl). +- Freshly-registered broker not yet linked → visible to owner/admins only. +- `handleBrokerProjects` (`handlers_broker_projects.go:30`) is broker-HMAC-authed + (broker enumerating its own projects) — leave as-is; this WP is about *user* + read of brokers. + +--- + +## WP-D — UI wiring (§3.6) + +- **Remove the visibility selector** from `web/src/components/pages/project-create.ts` + (markup at `:609-622`, the `visibility` `@state` at `:64`, and drop `visibility` + from the POST body at `:371`). New projects default to creator-only. +- **Members-card hint:** in the project-settings Members card + (`web/src/components/shared/group-member-editor.ts`, used for + `project::members`), add a small hint: *"To make this project visible to + all hub users, add the hub-members group."* Scope the hint to the project members + context (don't show it on unrelated group editors). +- Verify: `cd web && npm run typecheck && npm run lint`. + +--- + +## 2. Deferred follow-ups (track, do not implement here) + +- **OQ9 templates & harness configs** — keep globally listable now (WP-B KEEPs + `template`/`harness_config`); their own visibility + grove-attachment design is a + separate pass. +- **Grove→project terminology cleanup** — DELIVERED as standalone branch + `scion/grove-cleanup` (commit 230ca7f, by visibility-explorer): adds + `api.NormalizeVisibility()` (legacy `grove`/`project` → `team`) applied at + write entry points for Templates, HarnessConfigs, and Projects; fixes stale + comments; leaves wire-compat shims (`groveId/grove*` JSON, NATS subjects, + `SCION_GROVE_ID`, container labels) intentionally untouched. **Direction (ptone, + 2026-06-05): pull it into THIS stream, not a standalone PR.** It lands as a + discrete cherry-picked commit (`230ca7f`) on the branch, applied AFTER WP-0 and + BEFORE wave 2 so the backend agent builds on top of it (no `handlers.go` + conflict). Logical separation is preserved by keeping it as its own commit. The + project-visibility normalization it adds is **interim/superseded** (project + visibility becomes membership-derived and is no longer UI-authored after WP-D), + so that portion goes inert; the Template/HarnessConfig normalization stands. + Backend agent is told NormalizeVisibility is already present — do not re-add it. +- **Normalize-on-read / one-time migration for historical `"project"` rows** + (ptone deciding) — recommendation: **write-path only for now**. Templates/harness + are deferred (their migration rides with the OQ9 follow-up), and the *project* + visibility column is being retired from authoring, so a read-normalization or + backfill for project rows is wasted effort. Revisit only if the column is + repurposed as a derived cache. +- **Finer-grained permissions / role-in-policy engine** — the deferred alternative + in §0; revisit if nested-group admins or per-action grants are needed. +- **Drop the orphaned `agent.visibility` (and possibly `project.visibility`) DB + columns** via `WithDropColumn` once the field removal has soaked. + +--- + +## 3. Test & verification gates + +- Go: `make test-fast` then `make build` per WP; add unit tests for the new + enforcement paths in WP-B (the critical, behavior-changing package). +- Web: `npm run typecheck && npm run lint` for WP-D. +- Integration smoke (manual / verify skill): private project invisible to + non-member; add user→visible; add `hub-members`→visible to all; non-member can + still open an "everyone" project's agents on demand; top-level agent firehose + shows only your projects. +- Final: `make ci-full` before opening the PR. + +--- + +## 4. Rollout ordering (single branch) + +1. WP-0 (schema/codegen) → commit. +2. WP-B (all sub-steps together) + WP-A + WP-C + WP-D in parallel. +3. Integration smoke + `make ci-full`. +4. Open PR on `design/project-visibility-membership`. **Do not merge.** diff --git a/.design/project-visibility-membership.md b/.design/project-visibility-membership.md new file mode 100644 index 000000000..4ef942b80 --- /dev/null +++ b/.design/project-visibility-membership.md @@ -0,0 +1,323 @@ +# Project Visibility via Membership (Subtractive Model) + +## Status +**Approved (design)** — all 9 open questions resolved with ptone@google.com on +2026-06-05; ready for an implementation-planning pass. Supersedes the core +approach of `access-visibility.md` (visibility-as-stored-enum). Preserves that +doc's terminology feedback and agent-inheritance notes; reframes the mechanism +around group/role membership. + +### Resolved decisions (summary) +- **OQ2** read-only tier → role-aware single members group: member=read-only, + admin=create/manage, owner=full; migrate existing members → admin. +- **OQ3** agents are team members → agent read derives from project; drop per-agent + visibility. +- **OQ4** "everyone" = `hub-members` added as a real member row; "public" term and + the visibility dropdown removed; Members-card hint added. +- **OQ5** top-level agent list = caller's projects only (`IN` filter + new + `project_id` index); no denormalization. +- **OQ1** narrow read-all via explicit per-type allows; GATE {project, agent, + broker}; KEEP {user, group, template, harness_config}; tighten sensitive + {policy, gcp_service_account, secret/env/variable} (project-scoped derive from + associated project; hub-level admin/owner-only). +- **OQ6** broker read = owner + admins + members of any contributing project. +- **OQ7** terminology dissolved (no user-facing visibility labels remain). +- **OQ9** templates/harness configs deferred to a follow-up; grove-cleanup agent + spawned for legacy terminology. +- **OQ8** fail closed — no anonymous access; auth always required. + +--- + +## 1. Problem & Key Finding + +The Hub create-project dialog offers private/team/public, but visibility is +**stored and never enforced** — and, more importantly, *everyone can already see +everyone's projects*. The reason is not "unimplemented default-open"; it is an +explicit global grant. + +**Root cause (the load-bearing fact):** `pkg/hub/seed.go` seeds a hub-wide +policy `hub-member-read-all`: + +``` +scope=hub, resourceType="*", actions=[read, list], effect=allow +``` + +bound to the `hub-members` group. Every user is auto-enrolled into `hub-members` +on login (`ensureHubMembership`). So one policy grants every user read+list on +**every resource type**, projects included. This is why visibility is currently +moot. + +**Consequence:** Implementing visibility is primarily a **subtractive** change — +*stop globally granting read*, then let membership decide who sees what. + +--- + +## 2. Core Principle: Visibility is Emergent from Membership + +Visibility is **not** a fixed attribute chosen at project creation. It is a live +reflection of the project's current membership/role state, which can change over +time. We remove the creation-time selector entirely. + +The existing membership plumbing already supports this with **one source of +truth**: + +- The project "Members" panel in the web UI *is* the auto-created + `project::members` group (`` bound to that + group's ID). List/add/remove all go through `/api/v1/groups/{id}/members` → + `GroupMembership`. No second collection to sync. +- Groups can contain **other groups** (memberType="group", cycle-checked via + `WouldCreateCycle`), and `GetEffectiveGroups` resolves nesting transitively + (BFS up `parent_groups`). So a reusable cross-project team group can be dropped + into many projects. +- The "all hub users" group already exists: **`hub-members`** (seeded, every user + auto-joined). It is GroupType `explicit` with login-time auto-enrollment + (materialized rows), not a virtual group — but functionally it is "everyone on + the hub," and `GetEffectiveGroups` + the policy engine honor it natively. + +### 2.1 The three levels expressed as membership + +| Level | Meaning | Mechanism | +|-------|---------|-----------| +| **private** | Owner (+ explicitly added members) only | Default. Members group has only the owner. | +| **team** | The project's collaborators | Members group has users and/or nested groups. | +| **everyone** (public) | All hub users (read-only) | Add the **`hub-members`** group to the project, at a read-only role. | + +"public" deliberately means **everyone on the hub**, never outside it +(per terminology feedback in `access-visibility.md`: prefer "everyone" over +"public"; "grove-team"/"project-team" over "team"). Visibility is read-only; +mutations remain guarded by role/policy. + +No creation-time choice is required for any of these. private↔team is just "who's +in the members group"; everyone is "is `hub-members` one of the members." + +--- + +## 3. Required Changes + +### 3.1 Narrow the global read-all grant (the core subtractive step) + +`hub-member-read-all` must stop granting read on membership-gated resource types. +Per ptone: at minimum **projects, agents, brokers**. + +**Decision (ptone, 2026-06-05) — RESOLVED:** +- **Mechanism (a):** replace the `"*"` allow with **explicit per-type read/list + allow policies**, only for the types that stay hub-readable. (Rejected the + keep-wildcard-with-denies approach.) +- **GATE (membership-derived, removed from global read):** project, agent, broker. +- **KEEP globally member-readable (directory/catalog):** user, group, template, + harness_config. (template/harness_config later honor their own visibility + + grove attachment — OQ9.) +- **SENSITIVE — tighten now:** policy, gcp_service_account, secret / environment / + variable. These were world-readable via the wildcard and should not be. Per + ptone, the project-scoped sensitive resources should derive access from the + project(s) they're associated with (same membership-gating as agents), and many + have no UI outside project settings anyway — so project-membership-scoped read + is the right model for them; hub-level ones (e.g. global policies) go + admin/owner-only. + +### 3.2 Add project-scoped read for members (required, not optional) + +The per-project members policy currently grants only `agent: create, stop_all` +(`project::member-create-agents`) — **not read**. Members can read today +*only* because of the global grant. After 3.1, we must add a project-scoped read +grant so members keep visibility into their own projects: + +``` +scope=project, scopeID=, resourceType=project|agent, actions=[read,list], +effect=allow → bound to project::members group +``` + +When `hub-members` is added to the project (everyone/public), it inherits this +same read grant → all hub users can read that project. This is exactly the "add +the all-users group to a role" model. + +### 3.3 Read-only role tier (the "everyone is safe" prerequisite) — RESOLVED → B + +**Decision (ptone, 2026-06-05): Option B — role-aware policies on the single +members group**, keeping one group / one Members panel / one source of truth. +Three roles to start (explicitly noted as a starting point; fine-grained +permissions may be refined later): + +| Role | Grants | +|------|--------| +| **member** | read-only (read/list project + its agents) | +| **admin** | member + create/manage agents (today's "member-create-agents") | +| **owner** | admin + manage membership and visibility | + +**Enforcement decision (ptone, 2026-06-05): subtractive-only for now.** The policy +engine cannot condition a `PolicyBinding` on a member's role today +(`PolicyBinding` is `(PrincipalType, PrincipalID)` only; `GetEffectiveGroups` +returns group IDs, dropping role). Rather than extend the engine, we reuse the +existing `isProjectOwnerOrAdmin` role bypass (`pkg/hub/authz.go`, already checks +`role==admin||owner`): read is granted by a project-scoped policy bound to the +members group (all roles read), and `create/stop_all` is **removed** from that +policy so create/manage flows from the admin/owner bypass. No policy-engine change. +This is why existing members are bumped to `admin` on migration. + +> **Future improvement (deferred):** make policies genuinely role-aware by adding a +> `role` field to `PolicyBinding`, carrying role through `GetEffectiveGroups`, and +> filtering on it during policy evaluation. This would let role survive group +> nesting (the subtractive approach's one gap: a user who is admin only via a +> *nested* team group gets read but not create) and enable finer per-action grants. +> Not required to meet this design; revisit when nested-admin or per-action +> permissions are needed. + +Implications: +- "everyone/public" = add `hub-members` at role=**member** (read-only) — safe. +- Read-only sharing of an individual/group = add at role=member; collaborators who + can act = admin. +- **Migration cost:** existing members currently get create-agent via the members + policy; to preserve that, bump existing `member` rows to `admin` during rollout + (one-time backfill). New default for added members is read-only. +- The per-project policy set becomes role-conditioned (read bound to all roles; + create/stop bound to admin+owner; member/visibility management to owner). + +### 3.4 Close the two enforcement gaps + +- `getProject` (single GET by id) does **not** enforce read today — add a + `CheckAccess(ActionRead)` gate. Same for single-agent GET if similarly open. +- `listProjects`/`listAgents` return everything when identity is nil. **RESOLVED + (OQ8): fail closed** — a nil/unauthenticated identity sees nothing (empty/401); + authentication is always required. No anonymous read surface. + +### 3.5 Cross-project agent list filtering + performance + +The top-level agent list must return only agents in projects the user can see +(member/owner) **plus** agents in "everyone" projects. Findings: + +- `AgentFilter.MemberProjectIDs` already pushes a `project_id IN (...)` predicate + into SQL — good. The handler already computes `resolveUserProjectIDs` and + applies it for `scope=shared`; we extend it to the default scope. +- **Missing index:** the agents table only has a composite unique + `(slug, project_id)` index; there is no standalone `project_id` index. Add + `index.Fields("project_id")` — cheap, makes the IN filter efficient. +- `resolveUserProjectIDs` cost is the BFS in `GetEffectiveGroups` (≈2–15 queries + depending on group nesting) + `GetGroupsByIDs`. Compute once per request and + cache in request context. +- **Agent-list scope (RESOLVED → A, ptone 2026-06-05):** the top-level + cross-project agent list shows only agents in projects the caller is a + member/owner of — `WHERE project_id IN (your project set)`. **No denormalization + needed.** Agents in "everyone"/hub-members projects the caller hasn't joined are + still fully readable on demand via the per-project agent list and single-agent + GET (both use derived project read); they simply don't appear in the personal + cross-project firehose. +- Concrete plan: (1) always apply `MemberProjectIDs` for the default scope (not + just `scope=shared`); (2) add `index.Fields("project_id")` to the agent schema; + (3) cache `resolveUserProjectIDs` once per request in context. +- Pagination stays honest because filtering is in SQL (no post-hoc drop), so + `totalCount` and cursors remain accurate. + +### 3.6 Retire the creation-time visibility input (RESOLVED) + +**Decision (ptone, 2026-06-05):** Remove the visibility selector from the +create-project dialog entirely, and retire the user-facing term "public." New +projects default to creator-only (private by emergence). "Everyone" visibility is +achieved post-creation by adding the `hub-members` group as a member (OQ4, +option 1). The project-settings **Members card** gets a small hint, e.g. "To make +this project visible to all hub users, add the hub-members group." The DB +`visibility` column is no longer authored by the user; it may be retired or +repurposed only as a derived cache feeding §3.5's denormalized agent flag (see +OQ5). + +--- + +### 3.7 Agents derive from project (RESOLVED) + +**Decision (ptone, 2026-06-05): agents are team members.** Agent read access is +derived entirely from the parent project — if you can read the project, you see +all its agents; otherwise none. The per-agent `visibility` field (currently inert, +hardcoded "private") is dropped. Owner bypass still lets a creator see their own +agent; admin/owner roles still gate create/stop/manage. This removes the +project-vs-agent visibility-ceiling rules from the old design. It also simplifies +the cross-project agent list (§3.5): an agent's readability == its project's +readability, which is what makes the denormalized `project_public` flag on agents +(OQ5) a complete predicate. + +## 4. Brokers (RESOLVED → a) + +**Decision (ptone, 2026-06-05):** Brokers are hub-level and linked to projects +many-to-many via `ProjectContributor`. Broker **read = owner + hub admins + +members of ANY project the broker contributes to** (resolve via +`ProjectContributor`). Read-only for plain members; create/attach/manage stays +admin/owner-gated. +- Cross-broker list scopes the same way (brokers you own + brokers contributing to + a project you're a member of) — analogous to the agent-list filter but keyed off + the broker↔project link table; add a filter and likely an index on + `ProjectContributor`. +- A freshly-registered broker not yet linked to any project is visible to its + owner/registrant and admins only until attached. + +--- + +## 5. Open Questions (to resolve one-by-one) + +- **OQ1 — Scope of read-all narrowing.** RESOLVED → explicit per-type allows; + GATE {project, agent, broker}; KEEP {user, group, template, harness_config}; + tighten sensitive {policy, gcp_service_account, secret/env/variable} now — + project-scoped ones derive from associated-project membership, hub-level ones + admin/owner-only. See §3.1. +- **OQ2 — Read-only tier shape.** RESOLVED → B (role-aware single members group; + member=read-only, admin=create/manage, owner=full). Migrate existing members → + admin. See §3.3. +- **OQ3 — Agent access derivation.** RESOLVED → agents are team members. Agent read + derives purely from project membership (read the project ⇒ see all its agents); + drop the inert per-agent visibility field. Creator/owner bypass still applies; + admin/owner roles still gate create/stop/manage. See §3.7. +- **OQ4 — Everyone/public maintenance.** RESOLVED → option 1: "everyone" is the + `hub-members` group added as a real member row (role=member). The word "public" + is retired and the visibility dropdown is removed from project creation. The + Members card gets a hint: "to make this visible to all hub users, add the + hub-members group." See §3.6. +- **OQ5 — Denormalization for the agent list.** RESOLVED → A: top-level list = + caller's projects only (`project_id IN (mine)`); no denormalization. Add the + `project_id` index; cache `resolveUserProjectIDs` per request. "Everyone" + projects' agents readable on demand via project/single-agent views. See §3.5. +- **OQ6 — Broker visibility model.** RESOLVED → a: broker read = owner + admins + + members of any contributing project; list scoped the same way. See §4. +- **OQ7 — Terminology.** RESOLVED (dissolved by OQ4/OQ6): the visibility dropdown + is removed and "public" is retired, so there are no user-facing visibility-level + labels left to rename. User-facing vocabulary is now just project roles + (member/admin/owner) and the informal "all hub users" in the Members-card hint. + The legacy `visibility` constants/column become internal-only/retired. +- **OQ8 — Unauthenticated access.** RESOLVED → fail closed: nil/unauthenticated + identity sees nothing (empty/401); auth always required; no anonymous read. See + §3.4. +- **OQ9 — Other visibility-bearing resources.** RESOLVED → DEFER. Templates & + harness configs (visibility + grove-attachment semantics) are out of scope for + this pass and get their own follow-up design. They stay globally listable for + now (KEEP list, §3.1) so nothing breaks. Separately, ptone asked to spin up a + dedicated agent to clean up legacy "grove" terminology in this area (see note + below). + +> **Follow-up agent (grove→project cleanup):** spawned to assess/clean legacy +> "grove" references — primarily the Template/HarnessConfig visibility middle-tier +> value `"grove"` (→ `team`/`project`), and to evaluate the broader +> `groveId/groveName/grove/grovePath` JSON aliases in `pkg/store/models.go`, +> `pkg/api/types.go`, `pkg/hub/template_handlers.go`. Caution: many of those JSON +> tags are intentional wire backward-compat — rename cosmetic/internal uses, but +> flag (don't silently remove) anything that changes the API wire format. + +--- + +## 6. Migration Notes + +- Removing `hub-member-read-all` (for the 3 types) is the only behavior-changing + step; everything else is additive (new project-scoped read grants, index, + resolver caching). +- Existing projects: backfill the project-scoped member read policy (idempotent, + alongside the existing `createProjectMembersGroupAndPolicy`). +- No user-facing visibility data migration needed if the column is retired; if + repurposed as a cache, derive it from membership on first access. + +--- + +## 7. References + +- `access-visibility.md` — prior (stored-enum) design + inline ptone feedback. +- `pkg/hub/seed.go` — `hub-member-read-all`, `hub-members`, `ensureHubMembership`. +- `pkg/hub/authz.go` — CheckAccess flow, effective-group resolution. +- `pkg/hub/handlers.go` — listProjects/listAgents, getProject, resolveUserProjectIDs, + createProjectMembersGroupAndPolicy. +- `pkg/store/entadapter/{group_store,agent_store,project_store}.go` — filters, + GetEffectiveGroups, indexes. +- `pkg/ent/schema/{group,agent,project}.go` — schemas/indexes. diff --git a/.design/resource-clone-delete.md b/.design/resource-clone-delete.md new file mode 100644 index 000000000..744283293 --- /dev/null +++ b/.design/resource-clone-delete.md @@ -0,0 +1,338 @@ +# Resource Clone & Delete (Reduced Hub Resource Management) + +**Status:** Reviewed — all open questions resolved (see §3.1); ready for implementation +**Created:** 2026-06-02 +**Author:** Agent (template-harness-refactor) +**Related:** [hub-template-admin.md](./hub-template-admin.md) (parent — full admin view), [resource-import-refactor.md](./resource-import-refactor.md) (import slice, now built), [grove-level-templates.md](./grove-level-templates.md), [agnostic-template-design.md](./agnostic-template-design.md) + +--- + +## 1. Overview + +`hub-template-admin.md` proposed a full hub admin template-management page: a +dedicated `/admin/templates` list with filters/sorting/pagination and a row action +menu offering View/Edit, **Clone**, Lock/Unlock, Archive, and **Delete**, plus +hub-level import. Since that draft, two things changed the ground under it: + +1. **The import slice is fully built** (`resource-import-refactor.md`, Phases 0–4): + a unified `POST /api/v1/resources/import` endpoint, a shared + `` web component mounted in both Project Settings → + Resources and the Hub Resources page, streaming per-resource progress, and a + parallelized import path. Import is **done** and is no longer part of this work. +2. **A shared, kind-generic resource UI already exists.** `` + (`web/src/components/shared/resource-list.ts`) renders templates *and* + harness-configs identically in both the project and hub surfaces — but it is + **read-only** today (lists + links to the detail/editor page; explicitly "does + not handle import/creation"). + +This document is the **reduced adaptation** of `hub-template-admin.md` against that +updated current state. It drops the standalone admin page, filters, sorting, +pagination, lock/unlock, and archive, and keeps only the two operations the user +asked for — **Clone** and **Delete** — wired into the resource list that already +ships in both surfaces. It also folds in a requested **verification that +re-importing from the same URL pulls fresh content** (§5), which turns out to +already hold and just needs a regression test to lock it in. + +### Why "reduced" + +The full admin page in `hub-template-admin.md` assumed no shared resource UI and no +import. Both now exist. Building a separate `/admin/templates` page would duplicate +the list that `` already renders in two places. Adding Clone and +Delete *to that shared list* gives the two highest-value management actions in both +the project and hub Resources views with no new page, no new list, and a small, +well-contained backend delta. + +--- + +## 2. Current State (as built) + +### 2.1 Backend — what already exists + +| Operation | Endpoint | Handler | Notes | +|-----------|----------|---------|-------| +| Delete template | `DELETE /api/v1/templates/{id}?deleteFiles=true&force=true` | `deleteTemplateV2` (`template_handlers.go:495`) | Deletes DB record; `deleteFiles=true` also `DeletePrefix`es storage; `force=true` required to delete a `Locked` template (see §2.4). | +| Clone template | `POST /api/v1/templates/{id}/clone` | `handleTemplateClone` (`template_handlers.go:693`) | Copies files via `stor.Copy`, sets `BaseTemplate = source.ID`, **destination scope/scopeId/name come from the request body** (`CloneTemplateRequest`) — so it can already clone across scopes. | +| Delete harness-config | `DELETE /api/v1/harness-configs/{id}?deleteFiles=true` | `deleteHarnessConfig` (`harness_config_handlers.go:~370`) | Deletes record; `deleteFiles=true` removes storage. Routed via `handleHarnessConfigByID` → CRUD switch (`harness_config_handlers.go:260`). | + +**Two backend deltas this work introduces (details in §4):** +- **No harness-config clone endpoint.** The `action` switch in `handleHarnessConfigByID` + (`harness_config_handlers.go:230`) handles `upload`, `finalize`, `download`, + `files/…` — but no `clone`. Templates get clone; harness-configs don't. +- **No resource-level authz on delete/clone.** See §2.5. + +### 2.2 Frontend — what already exists + +- `` (`resource-list.ts`, 251 lines) — shared, read-only list + used by **both** Project Settings → Resources and the Hub Resources page + (`settings.ts`). No row actions today. +- `` (`resource-import.ts`) — shared import form, mounted in + both surfaces. Emits `resource-imported` so the host refreshes the list. +- Resource detail/editor page — file browser + inline editor, reachable from each + list row via `detailBasePath`. + +So the surfaces, the shared list, and the import affordance are all in place. What's +missing is **row-level Clone/Delete actions** (and, for cross-scope clone, a +"clone from global" affordance in the project view — §4.2). + +### 2.3 Re-import freshness — current behavior (verified) + +Traced end-to-end for the "re-import the same URL" case: + +1. **Fetch layer** — `FetchRemoteTemplate` (`pkg/config/remote_templates.go:160`) + computes its cache key from the **URL only** (`generateCacheKey(uri)`), then + `os.RemoveAll(templateCachePath)` (`remote_templates.go:180`) **before** + re-downloading. Re-importing the same URL wipes the prior cached copy and pulls a + fresh tarball/checkout every time — there is no stale-cache reuse. +2. **Sync layer** — the import loop calls `ResourceStore.Bootstrap(..., force=true)` + (`pkg/hub/resource_import.go:390`). With `force=true`, Bootstrap **skips the + unchanged-hash short-circuit** (`resource_store.go:179`), re-uploads all files, + and calls `reconcileResourceStorage` to **drop objects no longer in the manifest** + so files removed upstream don't linger. It recomputes the content hash and flips + the record back to `active`. + +**Conclusion: re-importing from the same URL already pulls fresh content** — fresh +bytes at the fetch layer, forced re-upload + stale-file pruning at the sync layer. +This is a behavior to *protect with a test*, not a bug to fix (§5). + +### 2.4 The `Locked` flag is latent (never set today) + +`Locked` (`pkg/store/models.go:422` "Prevent modifications (global templates)") is +**read and enforced** in many places — template/harness-config update, PATCH, file +edit, and delete all reject when `Locked` is set +(`template_handlers.go:409,449,510`; `harness_config_handlers.go:292,330,388`; +`template_file_handlers.go:308,480,610`) — and it is persisted in SQLite. **But no +non-test code path ever sets `Locked = true`.** The CRUD/PATCH handlers only +*preserve* it (`template.Locked = existing.Locked`, `template_handlers.go:424`); the +bootstrap/import path doesn't set it; there is no lock/unlock endpoint. The only +assignments to `true` are in tests (`template_file_handlers_test.go:331,413,563`). + +**Implication for this work:** in practice a template/harness-config is never locked, +so the delete path's locked branch is currently unreachable. We keep delete honoring +the flag (and offer a force fallback, §4.2) as cheap, correct insurance for if/when a +lock-setter is added — but we add **no** lock/unlock UI, and the locked-state UX is +not a focus. (This is the answer to the reviewer's "how do templates get locked?" — +today, they don't.) + +### 2.5 Authz gap on delete/clone (to be closed) + +`/api/v1/*` is wrapped by the global auth **middleware** (`server.go:1880` +`applyMiddleware`), which establishes *authentication* (valid session/bearer). But +the existing `deleteTemplateV2`, `handleTemplateClone`, and `deleteHarnessConfig` +handlers contain **no resource-level `CheckAccess`** — so any authenticated user can +call them directly, regardless of scope or role. By contrast the newer import +endpoint (`handleResourcesImport`) *does* enforce authz (hub-admin for global scope, +project capability for project scope; covered by +`resource_import_handler_test.go`). This work closes that gap (§4.3). + +--- + +## 3. Goals & Non-Goals + +### Goals +- Add **Clone** and **Delete** row actions to the shared ``, so + both Project Settings → Resources and Hub Resources get them with one change. +- Add a **harness-config clone** endpoint mirroring the template clone, so the list's + Clone action works for both kinds. +- Support **cloning a global resource into a project** from the project Resources view + (cross-scope clone, global → project). The clone endpoints already accept a + destination scope/scopeId, so this is mostly UI plus the new harness-config clone. +- **Harden authz**: add `CheckAccess` to the delete and clone handlers, matching the + import endpoint's policy (hub-admin for global-scoped; project capability for + project-scoped). +- A **destructive-action confirmation** for Delete (with a "delete stored files" + checkbox **defaulting on**) and a small **name/destination dialog** for Clone. +- **Lock in re-import freshness** with regression tests (§5). + +### Non-Goals +- The standalone `/admin/templates` page, filters, sorting, pagination — dropped; the + shared list already covers listing. +- **Lock/Unlock** and **Archive** actions/UI. Delete still *honors* the `Locked` + flag, but per §2.4 nothing sets it today, so there is no UI to toggle it. +- A referenced-by / in-use guard on delete — **hard delete** is retained (§3.1 Q5). +- Bulk actions and template usage indicators. +- Any change to the import pipeline or the `ResourceStore` core. + +### 3.1 Resolved decisions (review with project owner) + +- **Q1 → Add authz.** Fold resource-level `CheckAccess` into delete + clone (both + kinds), matching the import endpoint policy (§4.3). Closes the gap in §2.5. +- **Q2 → `deleteFiles` default ON.** The delete dialog's "Also delete stored files" + checkbox defaults checked (clean delete that reclaims storage). +- **Q3 → Locked is latent; no lock UI.** Investigated: no non-test code sets `Locked` + (§2.4). Keep delete honoring the flag with a force-delete confirm fallback; add no + lock/unlock UI. +- **Q4 → Clone from global in the project view.** The project Resources view offers + cloning a **global** resource down into the current project (§4.2). Same-scope clone + (project→project, global→global) is also supported. +- **Q5 → Hard delete.** No referenced-by/in-use guard; delete is immediate with only + the generic "cannot be undone" warning. + +--- + +## 4. Design + +### 4.1 Backend: harness-config clone + shared clone request + +Add a `clone` action to `handleHarnessConfigByID` (`harness_config_handlers.go:230`): + +```go +case "clone": + s.handleHarnessConfigClone(w, r, hcID) +``` + +`handleHarnessConfigClone` mirrors `handleTemplateClone` (`template_handlers.go:693`): + +1. `POST` only; load the source via `GetHarnessConfig`. +2. Read `{ name, scope, scopeId, visibility }` (reuse the `CloneTemplateRequest` + shape; harness-configs also carry `Harness`, copied from the source). +3. Build a new record with a fresh UUID, `Slug = Slugify(name)`, copying + `Harness`/`Description`/`Config`, **destination scope/scopeId from the request** + (default to source scope when omitted), status `pending`. +4. Generate the clone's storage path and `stor.Copy` each source file to it. +5. Persist, set status `active`, return the new record. + +Endpoint: `POST /api/v1/harness-configs/{id}/clone` — dispatches through the existing +`action` switch, no route-table change. + +> **Delete needs no new endpoint** — `deleteTemplateV2` and `deleteHarnessConfig` +> already accept `deleteFiles` (templates also `force`). Only authz is added (§4.3). + +> **Cross-scope clone needs no new endpoint** — `handleTemplateClone` already takes +> the destination scope/scopeId from the body; the new harness-config clone does the +> same. The "clone from global into project" feature is the UI in §4.2 calling these +> with `scope=project, scopeId={projectId}` against a global-scoped source. + +### 4.2 Frontend: Clone + Delete on the shared list, and clone-from-global + +Add an actions affordance to each row in `` — an `sl-dropdown` +(or trailing icon buttons) with **Clone** and **Delete**. Both surfaces inherit it. + +New component state: `cloneTarget`, `deleteTarget`, `cloneFromGlobalOpen`, +`actionInProgress`, `actionError`. + +**Delete flow:** +- Click Delete → confirmation dialog (`sl-dialog`, `danger` confirm button). +- Shows name + kind + scope, an **"Also delete stored files" checkbox (checked by + default**, Q2), and an irreversible-action warning. No referenced-by check (Q5). +- Confirm → `DELETE /api/v1/{templates|harness-configs}/{id}?deleteFiles={checked}`. + On `204`, remove the row locally (or re-fetch) and emit `resource-changed`. +- **Locked fallback (latent, §2.4):** if the response is the locked-template + validation error, surface it and offer a "Force delete" confirm that retries with + `&force=true`. In practice this branch is unreachable today, but it's cheap and + correct. (Harness-configs have no lock concept.) + +**Clone flow (same-scope):** +- Click Clone → dialog prompting for the **new name** (prefilled `"{name}-copy"`); + destination defaults to the current list's scope/scopeId. +- Confirm → `POST /api/v1/{templates|harness-configs}/{id}/clone` with + `{ name, scope, scopeId }`. On success, re-fetch and emit `resource-changed`. + +**Clone-from-global (project view only, Q4):** +- The project Resources view gains a **"Clone from global"** affordance (a button + near the list / import form). It opens a picker listing **global** resources of the + current `kind` (via the existing list API with `scope=global`). +- Selecting one opens the same clone dialog with the **destination fixed to the + current project** (`scope=project, scopeId={projectId}`) and a prefilled name. +- Confirm → clone endpoint with the global source id and the project destination. The + cloned copy then appears in the project list (`BaseTemplate` tracks the global + source for templates). +- This is the cross-scope direction the owner asked for (pull a shared global resource + down into a project to customize it). The hub (global) view keeps same-scope clone + only. + +Endpoint/path selection keys off the existing `kind` property +(`'template' | 'harness-config'`). + +### 4.3 Authorization (Q1) + +Add `authzService.CheckAccess` to the delete and clone handlers, matching +`handleResourcesImport`: + +- **Global-scoped resource** (delete/clone of a global template or harness-config): + require **hub-admin** (the admin bypass / explicit hub-wide policy), the same check + the global import path uses. +- **Project-scoped resource:** require the caller's **project capability** for the + mutating action (mirror the per-project import authz via the shared + `authorizeProjectImport`-style helper) — `ActionDelete` for delete, `ActionCreate` + for clone. +- **Clone specifically** touches two scopes: it **reads** the source and **creates** + at the destination. Enforce read on the source's scope **and** create on the + destination scope. For clone-from-global → project: the source is a global resource + (world-readable to authenticated users for global resources, consistent with the + Hub Resources view) and the destination requires project create capability. + +This makes delete/clone consistent with import and removes the §2.5 gap. UI gating +(admin-only Hub Resources route) stays as defense-in-depth, but the backend is now +authoritative. + +--- + +## 5. Re-import freshness: verify & lock in + +Per §2.3 this **already works**. Add regression tests rather than a fix: + +- **`pkg/hub` integration test** (alongside `resource_import_handler_test.go`): + 1. Import a single-resource workspace dir (file `home/a.txt` = `"v1"`). + 2. Mutate the source: change `a.txt` → `"v2"`, add `b.txt`, delete an existing file. + 3. Re-import the **same source**. + 4. Assert the stored manifest reflects `v2`, includes `b.txt`, and **no longer + includes** the deleted file (exercises `reconcileResourceStorage`), and that the + record's `ContentHash` changed and status is `active`. +- **Remote-cache test** (`pkg/config`): assert `FetchRemoteTemplate` re-fetches for + the same URL — the cache dir is wiped/rewritten on the second call (guards the + `RemoveAll` + URL-keyed cache behavior at `remote_templates.go:175–180`). + +Both lock down the guarantee: "re-importing the same URL pulls fresh content, +including removals." + +--- + +## 6. Phases + +### Phase 1 — Backend: harness-config clone + authz hardening +- Add `handleHarnessConfigClone` + the `clone` action case; mirror template clone, + honoring destination scope/scopeId from the body. +- Add `CheckAccess` to `deleteTemplateV2`, `handleTemplateClone`, + `deleteHarnessConfig`, and the new harness-config clone (§4.3). +- Tests: clone copies files / sets new slug+scope / independent record; authz allows + admin + project-capable callers and 403s others (mirror the import handler tests). + +### Phase 2 — Frontend: Clone & Delete on the shared list + clone-from-global +- Add row actions, delete-confirm dialog (`deleteFiles` checked by default + force + retry for the latent locked case), and the same-scope clone dialog to + ``; wire both kinds; emit `resource-changed`. +- Add the project-view "Clone from global" picker → clone into the current project. +- Verify in both project and hub surfaces. + +### Phase 3 — Re-import freshness regression tests +- Add the `pkg/hub` re-import-mutation test and the `pkg/config` cache-refetch test + (§5). No production code change expected. + +--- + +## 7. Key Files + +| Area | File | +|------|------| +| Template delete / clone (reuse + add authz) | `pkg/hub/template_handlers.go` (`deleteTemplateV2:495`, `handleTemplateClone:693`) | +| Harness-config delete (add authz) + **new clone** | `pkg/hub/harness_config_handlers.go` (action switch `:230`, CRUD `:251`) | +| Authz reference (import) | `pkg/hub/handlers.go` (`handleResourcesImport`), `pkg/hub/resource_import_handler_test.go` | +| Re-import force-sync (verify) | `pkg/hub/resource_import.go` (`:390`), `pkg/hub/resource_store.go` (`Bootstrap:121`, reconcile) | +| Remote fetch cache (verify) | `pkg/config/remote_templates.go` (`FetchRemoteTemplate:160`, `RemoveAll:180`) | +| `Locked` model + enforcement (latent) | `pkg/store/models.go:422,517`; enforcement sites in `template_handlers.go`, `harness_config_handlers.go`, `template_file_handlers.go` | +| Shared list — **add actions** | `web/src/components/shared/resource-list.ts` | +| Host surfaces (inherit actions; project view adds clone-from-global) | `web/src/components/pages/project-settings.ts`, `web/src/components/pages/settings.ts` | +| Tests | `pkg/hub/resource_import_handler_test.go`, `pkg/config/*_test.go` | + +--- + +## 8. Relationship to `hub-template-admin.md` + +This doc implements the **Clone** and **Delete** actions from +`hub-template-admin.md` §2.4–2.5, scoped down to the shared resource list rather than +a new admin page, and reflecting that import (§2.6 there) is already shipped per +`resource-import-refactor.md`. It additionally **hardens delete/clone authz** (a gap +the parent doc assumed away) and confirms the `Locked` flag is currently latent. +Still deferred from the parent doc: the standalone admin page, filtering/sorting/ +pagination, lock/unlock toggle (and a mechanism to *set* `Locked`), archive, bulk +actions, and usage indicators. diff --git a/.design/telegram-per-topic-default.md b/.design/telegram-per-topic-default.md new file mode 100644 index 000000000..f46af7cdb --- /dev/null +++ b/.design/telegram-per-topic-default.md @@ -0,0 +1,152 @@ +# Design: Per-Topic Default Agent in Telegram + +**Date:** 2026-06-01 +**Branch:** chat-channels +**Status:** Exploration + +## Current Behavior + +The `/default` command sets a single default agent per Telegram group chat. The flow: + +1. User types `/default` in a group → `handleDefault()` (`commands.go:194`) fetches the `GroupLink` by `chat_id` and presents an inline keyboard of agents. +2. User taps an agent button → `handleDefaultCallback()` (`callbacks.go:314`) writes `link.DefaultAgent = agentSlug` and calls `SaveGroupLink()`. +3. On inbound messages, `handleGroupMessage()` (`broker_v2.go:1607-1615`) falls back to `link.DefaultAgent` when no @-mention, reply-to, or conversation context resolves a target. + +**Storage:** The `group_links` table has `chat_id INTEGER PRIMARY KEY` and a single `default_agent TEXT` column. One default per chat. + +## Thread/Topic Context in Telegram + +Telegram forum-mode groups have named topics. Each topic has a `message_thread_id` (int64). The General topic is thread ID 1 (or 0 in some API versions). The plugin already captures this: + +- **Inbound:** `TGMessage.MessageThreadID` is populated by the Telegram Bot API. At `broker_v2.go:1757-1759`, it's stored as `msg.ThreadID`. +- **Outbound:** `Publish()` reads `msg.ThreadID` and passes it back as `SendOption{MessageThreadID: tid}` so replies land in the correct topic. +- **Commands:** When `/default` is typed inside a topic, `msg.MessageThreadID` carries the thread ID. The `CallbackQuery.Message` also has `MessageThreadID`, so the callback knows which topic the button was pressed in. + +**Key insight:** All the thread context is already flowing through the system — it's just not used for default-agent scoping. + +## Proposed Changes + +### 1. Storage: New `topic_defaults` Table + +Add a new table rather than modifying `group_links`: + +```sql +CREATE TABLE IF NOT EXISTS topic_defaults ( + chat_id INTEGER NOT NULL, + thread_id INTEGER NOT NULL, + agent_slug TEXT NOT NULL, + PRIMARY KEY (chat_id, thread_id) +); +``` + +The existing `group_links.default_agent` stays as the chat-level fallback. This avoids a schema migration and keeps the two concepts cleanly separated. + +**Store methods to add:** +- `GetTopicDefault(ctx, chatID, threadID) (string, error)` +- `SetTopicDefault(ctx, chatID, threadID, agentSlug) error` +- `DeleteTopicDefault(ctx, chatID, threadID) error` + +### 2. Command Handling Changes + +**`handleDefault()` in `commands.go`:** +- Read `msg.MessageThreadID`. +- If nonzero (i.e., inside a topic), pass it through to the keyboard builder and callback data. +- The keyboard prompt changes to: "Select the default agent for this topic:" (vs. the current "for @-mentions:"). +- If zero (General topic or non-forum group), behave as today (chat-level default). + +**Callback data format:** +- Current: `dflt:` (e.g., `dflt:coder`) +- New: `dflt::` (e.g., `dflt:coder:42`) +- When `threadID` is empty or "0", it's a chat-level default (backward-compatible). + +**`handleDefaultCallback()` in `callbacks.go`:** +- Parse the optional `threadID` from callback data parts. +- If present and nonzero: call `SetTopicDefault(ctx, chatID, threadID, agentSlug)`. +- If `__none__`: call `DeleteTopicDefault(ctx, chatID, threadID)`. +- Otherwise: set chat-level default as today. + +### 3. Routing Logic Changes + +**`handleGroupMessage()` in `broker_v2.go` (around line 1607):** + +Replace the current fallback: +```go +if len(targets) == 0 && link.DefaultAgent != "" { +``` + +With a two-tier lookup: +```go +if len(targets) == 0 { + defaultAgent := "" + if tgMsg.MessageThreadID != 0 { + topicDefault, _ := b.store.GetTopicDefault(ctx, chatID, tgMsg.MessageThreadID) + if topicDefault != "" { + defaultAgent = topicDefault + } + } + if defaultAgent == "" { + defaultAgent = link.DefaultAgent + } + if defaultAgent != "" { + // existing routing logic + } +} +``` + +Fallback chain: **topic default → chat default → no default**. + +### 4. UX for Querying/Clearing + +**Showing current default:** When `/default` is invoked in a topic, the keyboard should show the topic-level default (if set) with a checkmark, falling back to showing the chat-level default with a "(chat default)" label. + +**Clearing a topic default:** The "No default agent" button in topic context removes the topic override, reverting to the chat-level fallback. It does NOT clear the chat-level default. + +**Showing all topic defaults:** Consider adding a `/defaults` command (or a flag like `/default list`) that shows all topic-specific overrides for the group. This is a nice-to-have, not required for v1. + +## UX Flows + +### Setting a per-topic default +1. User navigates to the "Backend" topic in a forum group. +2. Types `/default`. +3. Sees keyboard: "Select the default agent for this topic:" with agent buttons. +4. Taps "coder" → "Default agent for this topic set to @coder." + +### Clearing a per-topic default +1. In the same topic, types `/default`. +2. Keyboard shows "✓ coder (current)" and "No default agent (use chat default)". +3. Taps "No default agent" → "Topic default removed. Messages will use the chat default (@designer)." + +### Non-forum group (no change) +1. `/default` works exactly as today. +2. `MessageThreadID` is 0, so all code paths hit the chat-level branch. + +## Complexity Assessment + +**This is a small, contained change.** Estimated at 1-2 days of implementation + testing. + +| Area | Scope | +|------|-------| +| New table + 3 store methods | ~40 lines | +| `handleDefault()` thread-awareness | ~10 lines changed | +| Callback data + `handleDefaultCallback()` | ~15 lines changed | +| Routing fallback in `handleGroupMessage()` | ~10 lines changed | +| Keyboard label tweaks in `cards.go` | ~10 lines changed | +| **Total new/changed code** | **~85 lines** | + +No changes needed to: +- The outbound message flow (thread routing already works) +- The `GroupLink` struct or `group_links` table +- The Telegram Bot API client +- Any other command handlers + +## Risks and Edge Cases + +1. **General topic ambiguity:** Telegram uses thread ID 1 for the General topic in forum groups, but non-forum groups have thread ID 0. The code should treat `MessageThreadID == 0` as "no topic" (chat-level default). Thread ID 1 (General) should be a valid topic for per-topic defaults. + +2. **Topic deletion:** If a topic is deleted, its `topic_defaults` row becomes orphaned but harmless — no messages will arrive with that thread ID. Could add periodic cleanup, but not necessary. + +3. **Callback data length:** Telegram limits callback data to 64 bytes. Current format `dflt:agentSlug` uses ~15-25 bytes. Adding `:threadID` (max 20 digits) stays well within limits. If using the `callback_lookups` short-ID system already in the codebase, this is a non-issue. + +4. **Race between topic and chat defaults:** A user might set a chat default expecting it to apply everywhere, not realizing a topic has an override. The `/default` command should clearly indicate when a topic override exists. + +5. **Forum mode toggled off:** If a group admin disables forum mode, all topics collapse. Topic defaults become inert (messages arrive with thread ID 0). The chat-level default takes over naturally. No data loss; if forum mode is re-enabled, the topic defaults resume working. diff --git a/.design/worktree-per-agent-phase1-plan.md b/.design/worktree-per-agent-phase1-plan.md new file mode 100644 index 000000000..7abe5da30 --- /dev/null +++ b/.design/worktree-per-agent-phase1-plan.md @@ -0,0 +1,189 @@ +# Phase 1 Implementation Plan: Worktree-Per-Agent (Docker × node-local) + +**Branch:** `scion/worktree-per-agent` +**Scope:** Design doc §12 Phase 1 only — Docker × node-local. NFS parity (Phase 2) +and K8s (Phase 3) are out of scope here. +**Tracking:** #158 (parent), #168 (shared-worktree refcount — Q7, deferred to a later phase). + +--- + +## Post-Phase-0 baseline (rebased onto `scion/storage-provisioning-phase0`) + +Phase 0 of epic #169 (PR #170) landed the universal-provisioning extraction. This branch +is rebased on it. The seam Phase 1 builds against is now: + +- `WorkspaceBackend` interface = **Resolve + Realize + Name** only. `Provision` was + **removed** (`pkg/runtime/workspace_backend.go`). +- `provisionShared(in ProvisionInput) error` — the standalone Tier-1 universal function + (`pkg/runtime/workspace_provision.go`). It does clone + `ensureWorktree` + advisory + lock + sentinel. **Still unwired** — no live caller yet. +- `localBackend` / `nfsBackend` = Resolve + Realize; provisioning content all moved into + `provisionShared`. + +Two things in the extracted code are NFS-shaped and are Phase-1's job to fix: + +1. **`ensureWorktree` uses plain `git worktree add -b` — no `--relative-paths`** + (`workspace_provision.go:283`). Mandatory for container path-identity (design §6); + `util.CreateWorktree` already does this correctly and should be reused. +2. **No Q1 layout** — it clones into `Resolved.HostPath` and nests `worktrees/` + under it, leaving `HostPath` checked out on the default branch. Q1 requires the base to + hold **no** branch (so a coordinator can own `main`). + +Also cosmetic: every error string in `provisionShared` still says `"nfsBackend.Provision:"` +(extraction leftover) — fix to neutral `"provisionShared:"`. + +Consequence: "broker dispatch branches on mode" (design §4.3, §5) remains the heaviest +task — it is the **first real wiring** of `provisionShared` + the backend abstraction into +the Docker node-local lifecycle. No non-test caller of `SelectWorkspaceBackend` or +`provisionShared` exists yet; NFS provisioning still runs via the separate k8s +init-container path (`k8s_runtime.go`), whose unification is #169 PR2, not this work. + +What already exists and is reused as-is: +- `util.CheckGitVersion()` / `CompareGitVersion()` — git ≥ 2.47 gate (`pkg/util/git.go`). +- `util.CreateWorktree()` — already uses `--relative-paths` + reuse-branch fallback. +- Dual-mount recipe — `pkg/runtime/common.go:188-222` (`.git` at `/repo-root/.git` + + worktree at `/repo-root/`), gated on `RepoRoot`+`Workspace` set and `GitClone==nil`. +- `SCION_HOST_UID` guard forcing `isGit=false` in-container (`provision.go:303-309`). +- Teardown: `RemoveWorktree` / `PruneWorktreesIn` / `DeleteBranchIn` (`provision.go:35-146`). +- Advisory lock + sentinel guard (`workspace_backend_nfs.go:141-268`). + +--- + +## Target node-local layout (Phase 1) + +**Layout decision (path-identity constraint, design §6).** The container dual-mount in +`common.go:188-222` only fires when `filepath.Rel(RepoRoot, Workspace)` does **not** start +with `..` — i.e. the worktree must live **inside** the repo root, exactly how the proven +local-repo case nests worktrees. The design doc's sibling `base/` + `worktrees/` layout +(§3) would make `rel = ../worktrees/` and `common.go` rejects it. So Phase 1 mirrors +the local case rather than the §3 diagram: + +``` +~/.scion.projects// # localBackend project root == RepoRoot (the base checkout) + .git/ # shared object store + packed-refs; gc.auto=0 + # cloned, then `git switch --detach` → owns NO branch + worktrees// # per-agent worktree nested inside repo root + .git # FILE: relative gitdir (via --relative-paths) + .scion-provisioned # sentinel: base clone complete +``` + +Base detached at default HEAD so `main` is free for an optional coordinator worktree (Q1). +`worktrees/` is added to `.git/info/exclude` so it never shows as untracked in the base. + +> **Sentinel-location refinement (P1.4b).** `ProvisionShared` writes its sentinel at +> `filepath.Dir(HostPath)`. To give a per-project sentinel (not a node-shared one), +> `localBackend.Resolve` returns `HostPath = /workspace` for worktree mode, so +> the base checkout lives at `~/.scion.projects//workspace` and the sentinel at +> `~/.scion.projects//.scion-provisioned`. Worktrees nest at +> `/workspace/worktrees/`. This matches `ProvisionShared`'s NFS-shaped +> contract (HostPath's parent is the per-project root). +Per-agent non-workspace state (prompt.md, scion-agent.json, home/) continues to live in +external split storage — unchanged from shared-workspace mode. + +> Revisiting the §3 sibling layout (no base working tree) is deferred: it requires teaching +> `common.go` to mount a common parent at `/repo-root` and handle a `..`-relative worktree. +> Out of Phase 1 scope; noted for follow-up. + +--- + +## Sub-tasks + +Each is sized to commit+push within the agent 3-turn limit. Dependencies in brackets. + +### P1.1 — Bring `provisionShared`'s worktree path to the Phase-1 layout +Edit `ensureWorktree` (and the base-clone step) in `pkg/runtime/workspace_provision.go`. +Responsibilities (design §4.1, §4.2, §4.2a, §6): +- After the base clone into `Resolved.HostPath`, `git -C switch --detach` so + the base owns no branch; set `git config gc.auto 0`; add `worktrees/` to + `.git/info/exclude`. +- Replace the plain `git worktree add -b` with **`--relative-paths`** (mandatory, §6). + Prefer reusing `util.CreateWorktree` / `sanitizeBranchName` so there is one worktree-add + implementation. Keep the reuse-branch fallback (attach existing branch instead of `-b`) + for the coordinator/`main` case (§4.2a). Worktree stays nested: + `/worktrees/`. +- Write `.scion` workspace marker into the worktree (`config.WriteWorkspaceMarker`). +- Single-worktree-per-branch invariant: clear error if the branch is already checked out + elsewhere (don't let raw git fail opaquely). +- Clean up the `"nfsBackend.Provision:"` error strings in `provisionShared` → + `"provisionShared:"` (neutral, now that it is the shared Tier-1 fn). +- Update the worktree tests in `workspace_provision_test.go` to the new layout + (detached base, `--relative-paths` `.git` pointer). **SharedPlain and ClonePerAgent + tests stay untouched and green.** + +### P1.2 — `localBackend.Resolve` for worktree mode [needs P1.1] +(No `backend.Provision` anymore — provisioning is the standalone `provisionShared`, invoked +by the broker in P1.4.) +- `Resolve`: when `Mode == WorktreePerAgent`, return `HostPath` = the node-local project + root (the base checkout / `RepoRoot`). The worktree path + (`/worktrees/`) is derived by `provisionShared`. Other modes + unchanged (zero behavior change). +- Confirm `Realize` still emits the plain local bind mount; the **dual-mount** (`.git` + + worktree) is contributed by `common.go` when the broker sets `RepoRoot`+`Workspace` + (P1.4), not by `Realize`. +- Unit tests for the worktree-mode Resolve path. + +### P1.3 — git-version gate + clone-per-agent fallback +- Decision helper (e.g. `worktreeEligible() (bool, reason string)`) wrapping + `util.CheckGitVersion()`. On git < 2.47: log a warning and signal fallback to + clone-per-agent (design §6, §9.1). +- Unit test the decision (inject version). + +### P1.4 — Broker dispatch branches on mode [needs P1.2, P1.3] +The core wiring. In `pkg/runtimebroker/start_context.go` (where `opts.GitClone` is set, +~437-454) and/or the dispatch handler: +- Resolve sharing mode for the dispatch (from threaded mode — see P1.5). +- If `worktree-per-agent` **and** git ≥ 2.47: + - `resolved := SelectWorkspaceBackend(cfg, mode).Resolve(...)`, then + `provisionShared(ProvisionInput{Resolved: resolved, Mode, GitClone, AgentID, + AgentName, Locker, ...})` on the host (base clone + worktree). + - Set `opts.Workspace = /worktrees/` and `RepoRoot = ` + (the detached base) so `common.go` takes the **dual-mount** path; **do not** set + `opts.GitClone` (suppress in-container clone). +- Else: existing clone-per-agent path (set `GitClone`, clone in container). +- Keep the `SCION_HOST_UID` guard intact. + +### P1.5 — Hub: permit worktree-per-agent on git hub-managed projects [supports P1.4] +- Allow/stamp `scion.dev/workspace-mode = worktree-per-agent` for git-backed + hub-managed projects (`pkg/hub/handlers.go`); update the "reserved for Phase 1+" + doc comment on `pkg/store/models.go`. +- Thread the resolved mode into the dispatch request (`pkg/runtimebroker/types.go` + `RunRequest`) so the broker (P1.4) can branch without re-deriving from labels. + +### P1.6 — Verification + suite green [needs P1.4, P1.5] +- Docker × node-local end-to-end: create 2 agents on one git project → confirm a single + base clone, two `worktrees/` with distinct branches, dual-mount resolves inside + the container, `git status` clean in each. Optional coordinator with `--branch main`. +- Full `go build ./...` + `go test ./...` green. +- Open PR against `main` (do **not** merge). + +--- + +## Sequencing & orchestration + +``` +P1.1 ─┬─> P1.2 ─┐ + │ ├─> P1.4 ─┐ + └ P1.3 ──┘ ├─> P1.6 (verify + PR) + P1.5 ────────────┘ +``` + +- One developer agent per sub-task, in dependency order; P1.3 and P1.5 can run parallel + to the P1.1→P1.2 chain. +- Each agent commits **and pushes** after its sub-task (3-turn-limit workaround). +- Manager reports to coordinator at each milestone (P1.1, P1.2/P1.3, P1.4/P1.5, P1.6+PR). + +## Resolved through design dialogue (2026-06-07, thread 155) +- **Provisioning abstraction** — extracted to standalone Tier-1 `provisionShared` in + epic #169 PR1 (Phase 0), now merged-to-branch and rebased under this work. P1.1 builds on + it; no NFS layout reconciliation needed in this phase (NFS validation is #169 / NFS + Phase 2). +- **Mode threading (P1.5)** — add an explicit mode field to `RunRequest` rather than + re-deriving from labels in the broker. +- **Layout** — Phase 1 uses the nested-worktree layout (base checkout = repo root, detached; + worktrees nested inside) to satisfy `common.go`'s path-identity constraint; the §3 sibling + layout is a deferred follow-up. See "Target node-local layout" above. + +## Remaining open point +- **Coordinator UX** — Phase 1 only needs the reuse-branch path working for `--branch main`; + the Hub's single-owner-per-branch enforcement (§4.2a) can be a thin check now, hardened + later. (Proposed; confirm if it needs more in Phase 1.) diff --git a/.design/worktree-per-agent.md b/.design/worktree-per-agent.md new file mode 100644 index 000000000..2ebfea627 --- /dev/null +++ b/.design/worktree-per-agent.md @@ -0,0 +1,413 @@ +# Design: Worktree-Per-Agent Mode for Hub-Managed Workspaces + +**Branch:** `scion/worktree-per-agent` +**Date:** 2026-06-06 +**Author:** worktree-designer agent +**Status:** Design proposal — initial draft for review +**Vocabulary:** follows `GLOSSARY.md` (Runtime Broker, Project, workspace sharing modes) +**Reviewers:** @ptone +**Tracking issue:** https://github.com/ptone/scion/issues/158 + +**Inputs (verified against source):** +`pkg/store/models.go`, `pkg/runtime/workspace_backend.go`, +`pkg/runtime/workspace_backend_local.go`, `pkg/runtime/workspace_backend_nfs.go`, +`pkg/agent/provision.go`, `pkg/runtime/common.go`, `pkg/api/types.go`, +`.design/nfs-workspace.md`, `.design/worktree-guards.md`, +`.design/hub-shared-workspace-isolation.md`, `.design/git-workspace-hybrid.md`. + +--- + +## 1. Problem statement + +In hub-managed mode, every agent created against a git-backed project performs its +**own full clone** of the project's remote. The Hub dispatches a `GitCloneConfig` +(`pkg/api/types.go`) and `sciontool` clones the repository into the agent's workspace +**inside the container** at startup (the `gitClone != nil` branch in +`pkg/agent/provision.go:371-396`). Concretely: + +1. Hub computes the workspace mode for the project and builds a dispatch carrying + `GitClone` (URL + branch + depth). +2. The Runtime Broker prepares an empty per-agent workspace dir and mounts it. +3. `sciontool` clones into `/workspace` when the container starts. + +This is simple and isolates agents perfectly, but it scales badly: + +- **Startup latency** is dominated by a network clone, paid **per agent**. +- **Disk amplification** — N agents on a node hold ≈ N full copies of the same + history and working tree. +- **No cheap coordination** — two agents working the same repo can't see each + other's branches/objects without round-tripping to the remote. + +The canonical mode that fixes this already exists in the type system — +`SharingModeWorktreePerAgent` (`pkg/store/models.go:208-212`) — but its doc comment +states it is *"not yet on Hub-managed projects — reserved for Phase 1+"*, and backend +routing only wires it to **NFS**: + +```go +// pkg/runtime/workspace_backend.go:200-210 +func SelectWorkspaceBackend(cfg *config.V1WorkspaceStorageConfig, mode store.WorkspaceSharingMode) WorkspaceBackend { + if cfg != nil && cfg.Backend == "nfs" { + switch mode { + case store.SharingModeSharedPlain, store.SharingModeWorktreePerAgent: + return NewNFSBackend(cfg.NFS) + } + } + return NewLocalBackend() +} +``` + +The node-local `localBackend.Provision` is a **no-op** +(`pkg/runtime/workspace_backend_local.go:65-70`), so there is no worktree-per-agent +path for the common single-node, node-local hub-managed deployment. + +**Goal:** the Hub/Runtime Broker maintains **one shared base clone per node** for a +project, and each agent gets its own **git worktree** (own branch, own working tree) +over that shared `.git` object store — instead of a full clone. First agent pays the +clone; every subsequent agent pays only a cheap `git worktree add`. + +### 1.1 The mechanism already exists for *local* git projects + +This is not a from-scratch effort. For **local** (non-hub) git projects, Scion already: + +- Creates host-side `--relative-paths` worktrees per agent at + `.scion/agents//workspace` via `util.CreateWorktree` + (`pkg/agent/provision.go:449-485`, Case 2 at lines 412-434). +- Dual-mounts the shared `.git` and the per-agent worktree into the container so the + relative gitdir pointer resolves (`pkg/runtime/common.go:181-189`): + + ```go + registerMount(filepath.Join(config.RepoRoot, ".git"), "/repo-root/.git", false, true) + containerWorkspace := filepath.Join("/repo-root", relWorkspace) + registerMount(config.Workspace, containerWorkspace, false, true) + ``` + +- Tears worktrees + branches down on delete with pruning of stale records + (`util.RemoveWorktree`, `PruneWorktreesIn`, `DeleteBranchIn` — + `pkg/agent/provision.go:35-146`). + +So worktree-per-agent **already runs in production** for the local-repo case. The work +is to bring this model to **hub-managed** projects (which today take the +clone-in-container path) and to formalize it as the `worktree-per-agent` sharing mode +on node-local storage, reusing the NFS backend's `ensureWorktree` logic where it fits. + +### 1.2 Non-goals + +- **Auto-migrating** existing full-clone agents to worktrees. New mode applies to new + projects/agents; live conversion is out of scope. +- **A distributed lock manager.** Reuse the existing advisory-lock + sentinel guard. +- **Cross-node base sharing on node-local storage.** A node-local base is per-node by + definition; cross-node sharing is the NFS backend's job (§7). +- **Replacing clone-per-agent.** It remains the right default when agents need fully + independent histories or must survive base-repo corruption. + +--- + +## 2. Background: the three sharing modes + +`pkg/store/models.go:197-231` defines the canonical modes and the label mapping +(`scion.dev/workspace-mode`, `ResolveWorkspaceSharingMode`): + +| Mode | Constant | Storage today | Isolation | +|------|----------|---------------|-----------| +| Shared plain | `SharingModeSharedPlain` (`shared`) | one dir, all agents mount it | none | +| Clone per agent | `SharingModeClonePerAgent` (`per-agent`) | full clone per agent (node-local) | full | +| **Worktree per agent** | `SharingModeWorktreePerAgent` | **NFS only today** | per-branch working tree, shared object store | + +This design makes the third row a first-class option for **hub-managed projects on +node-local storage**, and aligns it with the NFS implementation so a single code path +serves both backends. + +--- + +## 3. Target layout + +Per node, per project, the broker maintains a single **base repo** and a `worktrees/` +subtree, mirroring the NFS backend's layout (`workspace_backend_nfs.go:331-332`): + +``` +/ # e.g. ~/.scion.projects// + base/ # the one shared clone (.git lives here) + .git/ # shared object store + packed-refs + + worktrees/ + / # per-agent worktree (own branch + working tree) + .git # FILE: gitdir pointer (relative) → base/.git/worktrees/ + + .scion-provisioned # sentinel: base clone complete +``` + +Per-agent **non-workspace** state (prompt.md, scion-agent.json, home/) stays in the +external split-storage location, exactly as shared-workspace mode does today +(`.design/hub-shared-workspace-isolation.md`, `provision.go:120-143`), so siblings +never see each other's prompts through a shared mount. + +> Naming note: `.design/worktree-guards.md` calls out that every agent worktree using +> the basename `workspace` causes git to auto-suffix entries (`workspace`, `workspace1`, +> …). Using `worktrees/` as the worktree path gives each a **unique basename** +> (the agent UUID), eliminating that ambiguity. Scion associates worktrees by branch +> (`FindWorktreeByBranch`) regardless, but unique basenames make `git worktree list` +> legible. + +--- + +## 4. Provisioning flow + +### 4.1 First agent (base clone) + +When the first agent for a project lands on a node in worktree-per-agent mode: + +1. **Acquire the per-project advisory lock** (`store.AdvisoryLocker` / + `LockWorkspaceProvision`, keyed by a stable project hash — same guard the NFS + backend uses, `workspace_backend_nfs.go:227-268`). On Postgres this is + `pg_try_advisory_lock`; on SQLite/single-node it serializes naturally. +2. **Check the `.scion-provisioned` sentinel.** If present, skip to §4.2. +3. **Clone the remote once** into `base/` using the dispatched `GitCloneConfig` + (URL/branch/depth). This is the *only* network clone for the project on this node. +4. **Write the sentinel** atomically, release the lock. + +This is the `localBackend` analogue of `nfsBackend.Provision` +(`workspace_backend_nfs.go:141-225`); the difference is the root path (node-local +project dir vs NFS export) — the guard logic is identical and should be **shared**. + +### 4.2 Every agent (worktree add) + +Under the same per-project lock (worktree add/remove mutates shared `.git` metadata — +`workspace_backend_nfs.go:318-376`, design §9.2): + +1. Compute `worktreePath = /worktrees/`. +2. If it already exists, no-op (idempotent restart). +3. Derive branch: `branch = sanitizeBranchName(agentName)` / + `api.Slugify(agentName)`. If the branch already exists, attach to it instead of + `-b` (the reuse fallback at `workspace_backend_nfs.go:363-371`). +4. `git -C base worktree add --relative-paths -b `. + `--relative-paths` (git ≥ 2.47) is **mandatory** so the gitdir pointer survives the + container mount remap (§6). +5. Write the `.scion` workspace marker into the worktree + (`config.WriteWorkspaceMarker`, as `provision.go:475-484` does) so the in-container + CLI can discover project context — worktrees don't carry `.scion`. + +### 4.2a The coordinator / `main` agent (Q1 resolved) + +The base is cloned **non-bare** then **detached** at the default-branch HEAD +(`git -C base switch --detach`), so the base's own working tree never holds a branch and +stays clean — a pure object-store + refs directory. `main` (the default branch) is +therefore free to be attached by a *linked* worktree like any other branch. + +A user may create one **coordinator agent** with an explicit `--branch main`. It is not a +special code path: it is simply the agent whose worktree owns the `main` branch (attached +via the reuse path, not `-b`). Because every agent branch lives in the shared object +store, this coordinator can `git merge ` **locally** with no remote round +trip — the in-hub analogue of "a non-agent merges to main" in local-use mode. + +**Single-worktree-per-branch invariant.** Git forbids the same branch checked out in two +worktrees, so at most one worktree may *own* a given branch (e.g. `main`). The Hub +enforces this and returns a clear error rather than letting a raw `git worktree add` +fail. See Q7 for the explicitly-requested **shared-mount** exception, which lets >1 agent +attach to the *same* worktree directory without violating this invariant. + +### 4.3 Reconciling with the clone-in-container dispatch path + +Today hub dispatch sets `GitClone` and the clone happens **inside** the container at +startup. Worktree-per-agent inverts this: the worktree is created **on the host/broker** +(where the base `.git` lives) *before* the container starts, then mounted in. So: + +- When mode is `worktree-per-agent`, the broker provisions the worktree on the host and + takes the **dual-mount** path (`common.go:181-189`) instead of passing `GitClone` + through to sciontool. +- `GitCloneConfig` is still used by `Provision` (§4.1) to perform the *base* clone, but + it is consumed broker-side, not container-side. +- The `SCION_HOST_UID` guard that forces `isGit = false` inside containers + (`provision.go:303-309`) stays — agents must **never** create worktrees from inside + the container (see §6, `.design/worktree-guards.md`). + +--- + +## 5. Backend selection changes + +`SelectWorkspaceBackend` (`workspace_backend.go:200-210`) currently sends +`worktree-per-agent` to local **only** by falling through (which is a no-op backend). +Two changes: + +1. **Implement `localBackend.Provision`** to perform the §4 base-clone + worktree-add + when `in.Mode == SharingModeWorktreePerAgent`, factoring the shared guard/worktree + logic out of `nfsBackend` into a helper both backends call (e.g. + `ensureBaseAndWorktree(root, in)`). +2. **Route node-local worktree-per-agent to `localBackend`** (already the fall-through), + and keep NFS worktree-per-agent on `nfsBackend`. The mode is now valid on **both** + backends; the backend only decides *where the root lives*, not *whether worktrees are + supported*. + +`localBackend.Realize` already emits a bind mount (`workspace_backend_local.go:74-85`); +the runtime layer adds the **second** mount (shared `base/.git`) per `common.go:181-189` +when the resolved workspace is a worktree. + +--- + +## 6. Isolation & the container path-identity constraint + +This is the sharpest constraint, documented in `.design/worktree-guards.md` §3. + +A worktree's `.git` is a **file** containing `gitdir: ` pointing at +`base/.git/worktrees/`. For that pointer to resolve inside the container, the base +`.git` and the worktree must keep the **same relative distance** across the mount +boundary. The proven recipe (`common.go:181-189`): + +- Mount shared git dir at a fixed container path (`/repo-root/.git`). +- Mount the worktree at `/repo-root/` preserving the host relative path. +- Worktrees created with `--relative-paths` then resolve identically on host and in + container. + +Hard rules that fall out: + +- **git ≥ 2.47** on the broker host (for `--relative-paths`). Gate provisioning on a + version check; fall back to clone-per-agent with a logged warning if absent. +- **No in-container worktree creation.** Relative paths computed against the container + namespace are meaningless on the host (`.design/worktree-guards.md` §3). The existing + `SCION_HOST_UID` guard enforces this; keep it. + +Other shared-state isolation concerns: + +- **Object store & packed-refs are shared.** Concurrent ref updates are safe (git locks + refs), but a `git gc` in one worktree repacks objects for all. Recommendation: + **disable auto-gc** in the base (`git config gc.auto 0`) and run GC only during a + controlled "last agent" teardown or maintenance window. +- **`.git/config` is shared.** Per-agent git identity/credentials must live in the + agent's `$HOME/.gitconfig` (already the pattern for shared-workspace mode, + `provision.go:881-901`), never written into the shared base. +- **Per-agent non-workspace state** (prompt.md, scion-agent.json) uses external split + storage (§3) so it isn't visible through the shared tree. + +--- + +## 7. How the Hub manages the shared base repo + +- **Mode selection.** Hub stamps the project label + `scion.dev/workspace-mode = worktree-per-agent` at project-create time (parallel to + the existing `shared` stamping in `pkg/hub/handlers.go`). `ResolveWorkspaceSharingMode` + already maps the wire value (`models.go:225-226`). +- **Dispatch.** Hub keeps sending `GitCloneConfig` (URL/branch/depth); the broker + decides — based on resolved mode — whether to consume it as a base clone (worktree + mode) or pass it through for in-container clone (clone-per-agent mode). +- **Base lifecycle.** The base is **per node**. The Hub does not track base repos + directly; the broker owns them via the sentinel + advisory lock. The Hub's role is + mode selection and ensuring agents for a worktree-mode project are dispatched + consistently. +- **Multi-node.** Two nodes each keep their own base clone (acceptable — clone cost is + amortized per node, not per agent). True cross-node base sharing requires the **NFS + backend**, where the base + all worktrees live on a single export and + `nfsBackend.ensureWorktree` already implements §4.2. The mode is identical; only the + root path differs — which is exactly why §5 factors the logic into a shared helper. + +Backend × runtime matrix: + +| | Docker / VM | Kubernetes | +|---|---|---| +| **node-local** | base clone on host, dual bind-mount (§6) | base on node, worktrees per pod via hostPath/emptyDir — **needs validation** | +| **NFS** | base + worktrees on export, NFS mount | base + worktrees on RWX PVC + subPath (existing NFS design §9) | + +The K8s × node-local cell is the least-proven and may be restricted to NFS in v1. + +--- + +## 8. Lifecycle & cleanup + +Agent deletion already does the right thing for worktrees +(`pkg/agent/provision.go:35-146`): + +1. `util.RemoveWorktree(agentWorkspace, removeBranch)` — removes the worktree and + optionally its branch. +2. `util.PruneWorktreesIn(repoRoot)` — clears stale `.git/worktrees/` records. +3. `util.DeleteBranchIn(repoRoot, branchName)` fallback by slugified name. +4. External per-agent state dir removed (with podman-unshare fallback). + +New work for the base repo: + +- **Last-agent teardown.** When the final worktree for a project is removed on a node, + optionally GC and/or remove `base/` (policy: keep for fast re-provision vs reclaim + disk). Guard the decision under the same advisory lock. +- **Orphan base detection.** A maintenance sweep that prunes worktrees and, if none + remain, applies the teardown policy — analogous to existing project-prune flows. + +--- + +## 9. Limitations + +1. **git ≥ 2.47 required** on the broker host (`--relative-paths`). Older hosts fall + back to clone-per-agent. +2. **No nested / in-container worktrees.** Enforced by the `SCION_HOST_UID` guard; + agents that try to `git worktree add` inside the container get path-identity + corruption (`.design/worktree-guards.md`). +3. **Shared object store is a shared fate.** Corruption or an ill-timed `gc` in the base + affects all agents on that node. Clone-per-agent remains the choice when independence + matters more than speed. +4. **Node-local base is per node.** Cross-node sharing needs NFS; the disk win is + per-node, not global. +5. **Working-tree-only isolation.** Agents share history and refs; a force-push or a + shared-ref rewrite is visible to siblings. Branch-per-agent contains *normal* + workflows, not adversarial ones. +6. **K8s node-local path unproven** — may be NFS-only in v1 (§7). + +--- + +## 10. Reuse map (what already exists) + +| Need | Existing primitive | Location | +|------|--------------------|----------| +| Worktree add (host, relative) | `util.CreateWorktree` | `pkg/util/git.go`, `provision.go:470` | +| Worktree add (NFS, with reuse) | `nfsBackend.ensureWorktree` | `workspace_backend_nfs.go:318-376` | +| Branch name from agent | `api.Slugify` / `sanitizeBranchName` | `workspace_backend_nfs.go:378-393` | +| First-access guard | sentinel + `store.AdvisoryLocker` | `workspace_backend_nfs.go:141-268` | +| Dual mount (`.git` + worktree) | runtime mount registration | `common.go:181-189` | +| Worktree teardown + prune | `RemoveWorktree`/`PruneWorktreesIn`/`DeleteBranchIn` | `provision.go:35-146` | +| In-container worktree guard | `SCION_HOST_UID` → `isGit=false` | `provision.go:303-309` | +| Per-agent state isolation | external split storage | `.design/hub-shared-workspace-isolation.md` | +| Backend abstraction | `WorkspaceBackend` Resolve/Provision/Realize | `workspace_backend.go:33-64` | + +The net new code is small and concentrated: implement `localBackend.Provision` for the +worktree case, factor `ensureBaseAndWorktree` out of the NFS backend so both backends +share it, branch the broker dispatch on mode (host worktree vs in-container clone), add +the git-version gate, and define base-repo teardown. + +--- + +## 11. Open questions + +1. **Q1 — Base branch policy. [RESOLVED 2026-06-06]** Base is cloned **non-bare** then + **detached** at default-branch HEAD; it never checks out a working branch and stays + clean. The `main` branch is owned by an optional **coordinator agent** created with an + explicit `--branch main`, which holds the `main` worktree like any other agent and can + merge sibling branches locally from the shared object store. The Hub enforces a + single owner per branch (see §4.2a). Bare-base variant deferred as a later refinement. +2. **Q2 — GC policy.** Disable auto-gc entirely and GC only on teardown, or allow + scheduled GC under the project lock? +3. **Q3 — Base teardown.** Keep `base/` after the last agent (fast re-provision) or + reclaim disk immediately? Configurable via settings? +4. **Q4 — K8s node-local.** Support node-local worktrees on K8s in v1, or restrict + worktree-per-agent to NFS on K8s? +5. **Q5 — Migration.** Any opt-in path to convert a running clone-per-agent project to + worktree-per-agent, or strictly new-projects-only? +6. **Q6 — Default mode.** Should worktree-per-agent become the default for new + git-backed hub-managed projects once proven, with clone-per-agent as opt-out? +7. **Q7 — Explicit shared worktree (multi-agent mount).** Requirement (maintainer, + 2026-06-06): support **>1 agent mounting the same branch/worktree** when explicitly + requested. Since git forbids the same branch in two *separate* worktrees, this means N + agents **bind-mount the same worktree directory** into their containers (shared + working tree + shared branch), rather than each creating its own. Open sub-questions: + how it is requested (e.g. `--branch --shared` or attaching to an existing agent's + worktree); concurrency/write-conflict expectations within the shared tree (this is + shared-plain semantics scoped to one branch — cf. `.design/hub-shared-workspace-isolation.md`); + how per-agent home/prompt state stays isolated while the workspace is shared; and + refcounted teardown so the worktree is removed only when the last mounting agent exits. + To be taken up as its own question after Q2–Q6. + +--- + +## 12. Phased rollout (proposed) + +- **Phase 0 (this doc).** Design + issue #158. +- **Phase 1.** Factor `ensureBaseAndWorktree`; implement `localBackend.Provision` for + worktree mode; broker dispatch branches on mode; git-version gate. Docker × node-local + only. +- **Phase 2.** NFS parity validated end-to-end (largely existing); base teardown + + orphan sweep. +- **Phase 3.** K8s story (NFS-first); decide default-mode question (Q6). diff --git a/.eng-manager-state.md b/.eng-manager-state.md deleted file mode 100644 index 48d7a77b5..000000000 --- a/.eng-manager-state.md +++ /dev/null @@ -1,94 +0,0 @@ -# Eng-Manager State - -## Last Updated -2026-05-29T23:40Z - -## Active Workstreams - -### Tier 1 Grove→Project Rename (grove-rename2 branch) -- **Goal:** Rename all internal Go identifiers, comments, log strings, and test names from "grove" to "project" -- **Status:** Tier 1 COMPLETE — all phases done -- **Branch:** grove-rename2 (builds clean) -- **Progress:** ~2,147 Tier 1 refs eliminated (4,075 → 1,928), ~53% reduction -- **Remaining 1,928 refs are all Tier 2/3** (backward compat shims, container labels, env vars, NATS topics, etc.) - -#### Phase 1 — COMPLETE ✓ -All 5 agents completed, commits pushed, build+vet clean. Agents deleted. -| Agent | Scope | Commits | -|---|---|---| -| dev-rename-hub-api | pkg/hub/, pkg/api/, pkg/wsprotocol/, pkg/hubclient/, pkg/store/models.go | f7b601f6, e96a52c9 | -| dev-rename-hubsync | pkg/hubsync/ | a02ac19d, 8424f99f | -| dev-rename-config | pkg/config/ | 70af10b5, 5d044fb1 | -| dev-rename-agent-runtime | pkg/agent/, pkg/runtime/, pkg/broker/, pkg/util/logging/, pkg/sciontool/ | 2ce520b8, b84f356f | -| dev-rename-runtimebroker | pkg/runtimebroker/ | bdc5d2dc | - -#### Phase 2 — COMPLETE ✓ -| Agent | Scope | Commits | -|---|---|---| -| dev-rename-extras | extras/ (all Go files) | 070d57d1, 32617079 | -| dev-rename-cmd-src | cmd/ production files | 2606f523, 83237505 | -| cmd test files | Direct edit (agents hit limits x4) | 57b9c9f7 | - -#### Phase 3 (pending — future work): -- File renames (grove.go → project.go, etc.) -- Design docs update -- Tier 2 coordinated migration (labels, env vars, NATS topics) -- Tier 3 remains until breaking API version - -## Tier 1 Rename Rules (shared across all agents) -### RENAME (Tier 1): -- Go local variables: groveID→projectID, grovePath→projectPath, groveName→projectName, etc. -- Go function parameters with "grove" in name -- Unexported function/method names containing "grove" -- Comments using "grove" to mean "project" conceptually -- Human-readable strings in fmt.Errorf, log messages, user-facing output -- Test function names: TestGrove... → TestProject... - -### DO NOT RENAME (Tier 2/3): -- JSON struct tags (`json:"groveId"`, `json:"grove"`) -- Exported struct fields with JSON tags (GroveID in marshal/unmarshal aux structs) -- Container label strings ("scion.grove", "scion.grove_id", "scion.grove_path") -- Environment variable strings ("SCION_GROVE_ID", "SCION_GROVE", etc.) -- NATS topic strings ("scion.grove.") -- Filesystem path strings ("grove-configs/", ".scion.groves/", "grove-id", "groves/") -- Config key strings ("grove_id", "hub.groveId", "hub.grove_id") -- SQL DDL strings -- CLI flag names ("grove") -- API endpoint paths ("/api/v1/groves/", "/grove-upload") -- Query parameter names in Get("groveId") -- Exported constants (GroveConfigsDir) — values are filesystem paths -- Anything in MarshalJSON/UnmarshalJSON method bodies -- pkg/store/sqlite/sqlite.go (SQL migrations) -- pkg/ent/entc/migrate_grove_to_project*.go (migration files) - -## Pending Tasks -- Phase 2: cmd/ and extras/ renames (after Phase 1) -- Phase 3: file renames and design docs -- Tier 2 and 3 decisions (require architectural discussion) - -## Completed This Session -- Read and analyzed grove-rename-survey-v2.md -- Verified grove-rename2 branch builds clean -- Created phased rename plan with 5 Phase 1 agents -- **Phase 1 COMPLETE**: 5 parallel agents renamed ~1,564 grove refs across all pkg/ packages - - Commits: f7b601f6, e96a52c9, a02ac19d, 8424f99f, 70af10b5, 5d044fb1, 2ce520b8, b84f356f, bdc5d2dc - - Full build + vet verified clean - - All 5 agents cleaned up (deleted) -- Dispatched Phase 2 agents: dev-rename-cmd, dev-rename-extras -- **Phase 2 COMPLETE**: extras/ done by agent (070d57d1), cmd/ src done by agent (2606f523) - - cmd/ test files: 4 agent attempts hit context limits. Analyzed remaining refs — only 2 out of 101 were Tier 1. Applied directly (57b9c9f7) -- Full build verified clean after all phases -- **Tier 1 rename COMPLETE**: 4,075 → 1,928 refs (53% reduction, all remaining are Tier 2/3) - -## Decisions Made -- Tier 1 only touches local identifiers, comments, and human-readable strings -- No exported symbol renames in Phase 1 to prevent cross-package breakage -- Phase 1 agents own non-overlapping directory sets for conflict-free parallel work -- Marshal/Unmarshal aux struct fields (GroveID etc.) are Tier 3 — leave alone -- Exported constants like GroveConfigsDir are Tier 2 — leave alone - -## Notes for Next Session -- Survey document at .tasks/grove-rename-survey-v2.md (committed at 2d13fe85) -- ~4,075 total grove refs: 40% are intentional backward-compat shims (Tier 3), 60% are rename candidates -- Web frontend already fully migrated (zero grove refs) -- Tier 2 items (container labels, env vars, NATS topics) need migration strategy discussion diff --git a/.gitignore b/.gitignore index 0f6ae3117..f1231ae20 100644 --- a/.gitignore +++ b/.gitignore @@ -49,3 +49,29 @@ Thumbs.db # local tools .antigravitycli/ + +# Generated test fixtures (reproducible via `go run ./internal/fixturegen`, +# shared via the scratchpad mount — not committed as binary blobs). +testdata/hub-v46-fixture.db +testdata/hub-v46-fixture.db-wal +testdata/hub-v46-fixture.db-shm + +# Claude temporary files +.claude/ +fixturegen + +# Agent state files +.coordinator-state.md +.eng-manager-state.md + +# Task tracking (ephemeral) +.tasks/ + +# Screenshot artifacts +web/.screenshots/ + +# Downloaded files +downloads/ + +# PR review artifacts +pr-*-review*.md diff --git a/.scion/project-id b/.scion/project-id new file mode 100644 index 000000000..59df1969e --- /dev/null +++ b/.scion/project-id @@ -0,0 +1 @@ +c7c7775e-e3a0-43de-9d26-274688d467d0 diff --git a/.scratch/phase0-test-baseline.md b/.scratch/phase0-test-baseline.md deleted file mode 100644 index d066d90a6..000000000 --- a/.scratch/phase0-test-baseline.md +++ /dev/null @@ -1,58 +0,0 @@ -# Phase 0 Test Baseline - -Captured on: 2026-05-09 - -## Build Summary - -- `go build ./...`: **PASSED** - -## Test Summary - -- `go test ./...`: **FAILED** - -### Package Failures - -| Package | Result | -|---------|--------| -| `github.com/GoogleCloudPlatform/scion/cmd` | FAIL | -| `github.com/GoogleCloudPlatform/scion/cmd/sciontool/commands` | FAIL | -| `github.com/GoogleCloudPlatform/scion/pkg/agent` | FAIL | -| `github.com/GoogleCloudPlatform/scion/pkg/config` | FAIL | -| `github.com/GoogleCloudPlatform/scion/pkg/harness` | FAIL | -| `github.com/GoogleCloudPlatform/scion/pkg/hub` | FAIL | -| `github.com/GoogleCloudPlatform/scion/pkg/hubsync` | FAIL | -| `github.com/GoogleCloudPlatform/scion/pkg/store/sqlite` | FAIL | - -### Individual Test Failures (Representative Samples) - -- **cmd**: - - `TestDeleteStopped_RequiresGroveContext`: `docker ps failed: exec: "docker": executable file not found in $PATH` -- **pkg/agent**: - - `TestSettingsTelemetryMergedIntoStart` -- **pkg/config**: - - `TestIsInsideGrove` - - `TestLoadVersionedSettings_TelemetryHierarchyMerge` - - `TestLoadVersionedSettings_TelemetryEnvOverride` -- **pkg/harness**: - - `TestGitCloneWorkspace_DefaultEnvValues` - - `TestGitCloneWorkspace_NonZeroUIDChownsWorkspace` -- **pkg/hub**: - - `TestMessageBrokerProxy_UserMessageDelivery` - - `TestMessageBrokerProxy_EnsureGroveSubscriptionsIncludesUserMessages` - - `TestMessageBrokerProxy_StartBootstrapsExistingGroves` - - `TestMessageBrokerProxy_GroveSubscriptionDedup` -- **pkg/hubsync**: - - `TestEnsureHubReady_GlobalFallbackWithHubEnabled` - - `TestEnsureHubReady_GlobalFallbackWithHubDisabled` -- **pkg/store/sqlite**: - - `TestMaintenanceOperationsSeeded`: `should have 4 item(s), but has 5` - -## Observations - -- The failure in `TestDeleteStopped_RequiresGroveContext` appears to be due to `docker` not being available in the test environment. -- Many failures seem related to Telemetry settings and Hub synchronization. -- `pkg/store/sqlite` has a mismatch in the number of seeded maintenance operations (expected 4, found 5). - -## Pre-rename Baseline - -- **Initial Grove Count**: 22194 diff --git a/.tasks/grove-rename-survey-v2.md b/.tasks/grove-rename-survey-v2.md deleted file mode 100644 index 7f025e5c6..000000000 --- a/.tasks/grove-rename-survey-v2.md +++ /dev/null @@ -1,365 +0,0 @@ -# Grove-to-Project Rename Survey v2 - -**Date:** 2026-05-29 -**Branch surveyed:** `main` (at commit `2c03b71`) -**Scope:** All remaining "grove" references in tracked source files (excluding `.git/`, `node_modules/`, `.scion/`) - ---- - -## Executive Summary - -The codebase contains **~4,075 grove references in Go source** (1,933 in production code, 2,142 in tests), plus **~140 in changelogs**, **~120 in documentation**, and **~6 in embedded YAML configs**. The web frontend has **zero** remaining grove references. - -The references fall into two distinct categories: - -1. **Intentional backward-compatibility shims** (~40% of production code references) — JSON marshal/unmarshal pairs, deprecated CLI flags, legacy API endpoint aliases, and wire-protocol dual-field support. These exist *by design* and must remain until a breaking-change version boundary. - -2. **Internal naming that could be renamed** (~60% of production code) — Go local variables, function names, struct field names in YAML tags, container labels, environment variable names, NATS topic prefixes, SQL schema identifiers, filesystem path conventions, and telemetry attributes. These are candidates for incremental rename. - ---- - -## Reference Counts by Category - -| Category | Production Code | Test Code | Notes | -|---|---|---|---| -| **JSON struct tags** (`json:"groveId"`, `json:"grove"`, etc.) | 162 | ~80 | Backward-compat shims for API wire format | -| **Go local variable names** (`groveID`, `grovePath`, `groveSettings`, etc.) | ~200 | ~300 | Internal naming, safe to rename | -| **Container labels** (`scion.grove`, `scion.grove_id`, `scion.grove_path`) | 78 | ~40 | Cross-system contract with running containers | -| **Environment variables** (`SCION_GROVE_ID`, `SCION_GROVE`, `SCION_GROVE_PATH`) | 22 | ~15 | Injected into agent containers | -| **NATS topic strings** (`scion.grove..*`) | 12 | ~8 | Message bus topic prefix | -| **CLI flags** (`--grove` deprecated aliases) | 45 | ~30 | All marked deprecated+hidden | -| **SQL/database schema** (`groves` table, `grove_id` columns, `grove_contributors`) | 117 | ~50 | Schema migration territory | -| **Filesystem paths** (`grove-configs/`, `.scion.groves/`, `grove-id`, `grove-workspace`) | 27 | ~15 | On-disk directory conventions | -| **Embedded YAML configs** | 6 | 0 | File `default_grove_settings.yaml` | -| **Telemetry attributes** (`grove_id`, `scion.grove.id`) | 14 | ~5 | Observability labels | -| **Function/method names** | 4 | ~10 | `deprecateGroveEndpoint`, `MigrateGroveToProjectData`, etc. | -| **Type/struct definitions** | 2 | ~2 | `GroveDiscovery`, `GroveConfig` (alias) | -| **Design docs** (`.design/`) | N/A | N/A | 15 files with "grove" in name | -| **Files with "grove" in filename** | 21 | (included) | Source + docs + config | -| **Changelogs** | ~140 | N/A | Historical, should not change | - ---- - -## Detailed Breakdown by Category - -### 1. JSON Wire-Format Compatibility (Backward-Compat Shims) - -These are `MarshalJSON`/`UnmarshalJSON` method pairs that emit and accept legacy `groveId`, `groveName`, `grove`, `grovePath`, `groveSlug` fields alongside the canonical `projectId`/`projectName` fields. **These are intentional and should remain until a breaking API version change.** - -**Files (production):** -- `pkg/store/models.go` — 93 refs: Agent, Project, ProjectContributor, Template, Schedule, SubscriptionTemplate, Notification, ScheduledEvent, Message, ScheduleDetail (10 model types with marshal/unmarshal pairs) -- `pkg/hubclient/types.go` — 57 refs: AgentInfo, ProjectInfo, BrokerProjectInfo, TemplateInfo -- `pkg/hubclient/notifications.go` — 37 refs: NotificationInfo, SubscribeRequest, NotificationSubscription, NotificationTrigger, NotificationTemplate -- `pkg/hubclient/projects.go` — 17 refs: RegisterProjectRequest, UnregisterProjectRequest -- `pkg/hubclient/agents.go` — 8 refs: AgentCreateRequest -- `pkg/hubclient/templates.go` — 15 refs: TemplateListRequest, TemplateImportRequest -- `pkg/hubclient/tokens.go` — 14 refs: TokenCreateRequest, TokenInfo -- `pkg/hubclient/schedules.go` — 7 refs: Schedule -- `pkg/hubclient/scheduled_events.go` — 7 refs: ScheduledEvent -- `pkg/hubclient/messages.go` — 7 refs: Message -- `pkg/hubclient/runtime_brokers.go` — 22 refs: RuntimeBrokerInfo (Groves array), ProjectHeartbeat -- `pkg/api/types.go` — 24 refs: AgentInfo marshal/unmarshal, SecretSource legacy "grove" value -- `pkg/runtimebroker/types.go` — 52 refs: RuntimeBrokerInfo, AgentCreateRequest, StartContextResult, BrokerAgentInfo -- `pkg/wsprotocol/protocol.go` — 22 refs: ConnectMessage (Groves), StreamOpenMessage (GroveID) -- `pkg/hub/handlers.go` — ~15 refs: RegisterProjectRequest, ProjectListResponse (LegacyGroves), heartbeat unmarshal -- `pkg/hub/handlers_auth.go` — 2 refs: GCPServiceAccountResponse (groveId) -- `pkg/hub/handlers_notifications.go` — 6 refs: SubscriptionCreateRequest, query param fallback -- `pkg/hub/template_handlers.go` — 8 refs: TemplateImportRequest, TemplateListRequest -- `pkg/hub/response_types.go` — 24 refs: various response wrappers -- `pkg/hub/events.go` — 29 refs: event type compat - -### 2. Container Labels - -Labels applied to Docker/Podman/K8s containers. Changing these requires a migration strategy for existing running containers. - -| Label | Used In | Count | -|---|---|---| -| `scion.grove` | common.go, docker.go, podman.go, apple_container.go, k8s_runtime.go, agent/run.go, agent/list.go, provision.go, server_dispatcher.go, fs-watcher | ~30 | -| `scion.grove_id` | common.go, docker.go, podman.go, apple_container.go, k8s_runtime.go, agent/run.go, runtimebroker/handlers.go, server.go | ~25 | -| `scion.grove_path` | common.go, docker.go, podman.go, apple_container.go, k8s_runtime.go, agent/run.go | ~15 | - -**Key files:** -- `pkg/runtime/common.go` — Lines 286-288, 393, 397: env injection + label creation -- `pkg/runtime/docker.go` — Lines 177-224: label reads for filter matching -- `pkg/runtime/podman.go` — Lines 303-305: parsing labels from container list -- `pkg/runtime/apple_container.go` — Lines 210-212: parsing labels -- `pkg/runtime/k8s_runtime.go` — Lines 676-756, 1615-1708: label creation + PVC selectors + pod queries -- `pkg/agent/run.go` — Lines 895-914: label assignment during agent start -- `pkg/agent/list.go` — Lines 42-64: filter matching by label -- `pkg/agent/provision.go` — Line 190: label during provision -- `cmd/server_dispatcher.go` — Lines 61, 95, 156: hub dispatcher label injection - -### 3. Environment Variables - -| Variable | Description | Files | -|---|---|---| -| `SCION_GROVE` | Project name injected into container | `pkg/runtime/common.go:286` | -| `SCION_GROVE_ID` | Project UUID injected into container | `pkg/runtime/common.go:288`, `pkg/runtimebroker/start_context.go:280`, `pkg/hub/httpdispatcher.go:953,1112`, `pkg/agent/run.go:68-71,903` | -| `SCION_GROVE_PATH` | Project filesystem path | `pkg/runtimebroker/start_context.go:284` | -| `SCION_HUB_GROVE_ID` | Hub project ID override (env-to-config mapping) | `pkg/config/koanf.go:91-92` | - -**Also referenced in:** -- `pkg/runtimebroker/hubenv.go:33-34` — allowlist of passthrough env vars -- `pkg/sciontool/telemetry/providers.go:66` — telemetry attribute source -- `pkg/sciontool/telemetry/gcp_exporter.go:92` — GCP metrics labels -- `pkg/sciontool/hooks/handlers/telemetry.go:496` — hook telemetry -- `pkg/config/project_marker.go:185-189` — container detection logic -- `extras/scion-a2a-bridge/cmd/main.go:204` — A2A bridge config - -### 4. NATS Topic Prefix (`scion.grove..*`) - -The message broker uses `scion.grove.` as the topic namespace. This is a **wire protocol** concern. - -**Files:** -- `pkg/broker/broker.go` — Lines 21-86: 5 topic-building functions (AgentMessageTopic, BroadcastTopic, AgentWildcardTopic, UserMessageTopic, UserWildcardTopic) all produce `scion.grove.*` topics -- `extras/scion-a2a-bridge/internal/bridge/bridge.go:275` — subscribes to `scion.grove.*` -- `extras/scion-chat-app/cmd/scion-chat-app/main.go:249` — subscribes to `scion.grove.*` -- `extras/scion-chat-app/internal/chatapp/commands.go:618,638` — subscribes to `scion.grove.*` -- `extras/scion-chat-app/internal/chatapp/notifications.go:49` — comment documenting topic format -- `extras/scion-telegram/internal/telegram/broker_v2.go:451,663,2198` — parses `scion.grove.*` topics -- `cmd/scion-broker-repl/main.go:25-27` — example NATS commands in comments - -### 5. SQL/Database Schema - -The SQLite schema uses `grove`-based naming for tables, columns, indexes, and foreign keys. This is the **most migration-sensitive** area. - -**Tables with "grove" in name:** -- `groves` — primary project table (CREATE TABLE + 3 indexes + multiple ALTER TABLEs in migrations) -- `grove_contributors` — project-broker association table -- `grove_sync_state` — hub sync state table - -**Columns named `grove_id` across tables:** -- `grove_contributors.grove_id` -- `agents.grove_id` -- `templates.grove_id` -- `notification_subscriptions.grove_id` -- `notifications.grove_id` -- `scheduled_events.grove_id` -- `groups.grove_id` -- `schedules.grove_id` -- `subscription_templates.grove_id` -- `user_access_tokens.grove_id` (inferred from rename migration) -- `messages.grove_id` -- `gcp_service_accounts.grove_id` (inferred from rename migration) - -**Indexes with "grove":** -- `idx_groves_slug`, `idx_groves_git_remote`, `idx_groves_owner`, `idx_groves_default_runtime_broker` -- `idx_agents_grove_slug`, `idx_agents_grove` -- `idx_grove_sync_state_project` (on `grove_sync_state(grove_id)`) -- Multiple `idx_*_project` indexes on `grove_id` columns - -**Rename migration exists at:** -- `pkg/store/sqlite/sqlite.go` lines 1236-1250: Migration V50 renames `grove_id` → `project_id` across 12 tables, `grove_contributors` → `project_contributors`, `grove_sync_state` → `project_sync_state` -- But the **initial schema** (V1) and **all intermediate migrations** (V2-V49) still reference grove naming — these are historical DDL and cannot be changed - -### 6. CLI Flags & Commands - -All `--grove` flags are properly deprecated with `MarkDeprecated` + `MarkHidden`. The `project` command has `grove` as an alias. - -| Location | Type | Detail | -|---|---|---| -| `cmd/root.go:228-230` | Persistent flag | `--grove` deprecated alias for `--project` | -| `cmd/root.go:186-194` | Early arg parsing | `--grove` / `-g` / `--grove=` pre-parse | -| `cmd/project.go:42-43` | Command alias | `Aliases: []string{"grove", "group"}` | -| `cmd/broker.go:350-361` | Flag | `--grove` on `provide`/`withdraw` | -| `cmd/notifications.go:165-185` | Flags | `--grove` on subscribe/unsubscribe/subscriptions | -| `cmd/hub_env.go:168-193` | Flag | `--grove` on env commands | -| `cmd/hub_secret.go:181-206` | Flag | `--grove` on secret commands | -| `cmd/hub_token.go:150-165` | Flags | `--grove` on token create/list | -| `cmd/list.go:573` | Help text | "across all groves" | -| `cmd/stop.go:525` | Help text | "in the current grove" | -| `cmd/suspend.go:433` | Help text | "in the current grove" | -| `cmd/message.go:683-684` | Help text | "current grove" / "all groves" | -| `cmd/server.go:262` | Help text | "new groves" | -| `cmd/hub.go:309-353` | Subcommand | `hub groves` command (Use: "groves", Aliases: ["grove"]) | -| `cmd/config.go:209,365` | Label | `"grove"` label for project dir | -| `cmd/template_resolution.go:71-373` | Scope name | `"grove"` as scope value | -| `cmd/completion_helper.go:95-96` | Flag read | reads `"grove"` flag for completions | - -### 7. Filesystem Path Conventions - -| Path Pattern | Used For | Files | -|---|---|---| -| `grove-configs/` | Legacy project configs dir | `pkg/config/paths.go:32`, `pkg/config/project_marker.go:93`, `pkg/config/project_discovery.go:97,359`, `pkg/config/shared_dirs.go:47` | -| `.scion.groves//` | Hub-managed project workspace | `pkg/runtimebroker/start_context.go:81,100,115`, `pkg/runtimebroker/handlers.go:603,892` | -| `grove-id` | Legacy project ID file | `pkg/config/project_marker.go:219-231`, `pkg/config/settings.go:552-557` | -| `grove-workspace` | Shared workspace path segment | `pkg/storage/storage.go:255` | -| `groves/` | Legacy projects directory | `pkg/config/paths.go:33`, `pkg/runtimebroker/server.go:870`, `pkg/runtimebroker/start_context.go:100` | -| `templates/groves/` | Storage path for project templates | `pkg/storage/storage.go:210` | -| `harness-configs/groves/` | Storage path for project harness configs | `pkg/storage/storage.go:231` | -| `default_grove_settings.yaml` | Embedded default config | `pkg/config/embeds/default_grove_settings.yaml`, `pkg/config/koanf.go:261` | - -### 8. Telemetry & Observability - -| Attribute/Label | Files | -|---|---| -| `scion.grove.id` | `pkg/sciontool/telemetry/providers.go:70,81` | -| `grove_id` (GCP label) | `pkg/sciontool/telemetry/gcp_exporter.go:95,102` | -| `grove_id` (hook attr) | `pkg/sciontool/hooks/handlers/telemetry.go:497,500` | -| `grove_id` (log field) | `pkg/agent/msgbuffer.go:129`, `pkg/util/logging/logging.go:76` | -| `grove_id` (cloud label) | `pkg/util/logging/cloud_handler.go:153`, `pkg/util/logging/gcp_handler.go:107` | -| `GroveIdx` (struct field) | `pkg/util/logging/request_log.go:234` | -| Log debug strings | `pkg/hubsync/sync.go:324,656,807,814,924` and many others using `grove_id` in structured log attrs | - -### 9. Go Internal Variable & Parameter Names - -These are the **largest** category (~200 production, ~300 test). Key patterns: - -| Variable Pattern | Approximate Count | Key Files | -|---|---|---| -| `groveID` / `groveId` | ~80 | Widespread across hubsync, runtimebroker, config, hub | -| `grovePath` / `grovePaths` | ~40 | runtimebroker/handlers.go, server.go, config/settings.go | -| `groveSettings` | ~10 | runtimebroker/hubenv.go, server.go | -| `groveName` | ~15 | runtimebroker, agent/manager, k8s_runtime | -| `groveSlug` | ~5 | agent/provision.go, runtimebroker | -| `grovesToScan` | ~5 | agent/list.go | -| `groveFilter` | ~3 | runtimebroker/hub_connection.go, server.go | -| `grovePattern` | ~2 | scion-a2a-bridge | -| `deletionGroveName` | 3 | agent/manager.go | -| `groveParent` | 2 | runtimebroker/workspace_handlers.go | - -### 10. Files with "grove" in Filename - -**Source files (6):** -- `pkg/ent/entc/migrate_grove_to_project.go` — Migration logic (keeping name is appropriate) -- `pkg/ent/entc/migrate_grove_to_project_nosqlite.go` — No-op stub for non-SQLite builds -- `pkg/ent/entc/migrate_grove_to_project_test.go` — Tests for migration -- `extras/fs-watcher-tool/pkg/fswatcher/grove.go` — GroveDiscovery struct + container label queries -- `extras/fs-watcher-tool/pkg/fswatcher/grove_test.go` — Tests for above -- `pkg/config/embeds/default_grove_settings.yaml` — Embedded default settings - -**Design docs (15):** -- `.design/grove-dirs.md` -- `.design/git-grove-duplicates.md` -- `.design/grove-level-templates.md` -- `.design/grove-to-project-rename.md` -- `.design/grove-mount-protection.md` -- `.design/hosted/grove-settings.md` -- `.design/hosted/hub-groves.md` -- `.design/hosted/git-groves.md` -- `.design/project-log/2026-05-12-ent-grove-to-project-data-migration.md` -- `.design/project-log/2026-05-13-fix-grove-bugs.md` -- `.design/project-log/2026-05-13-fix-hub-env-test-groveid-to-projectid.md` -- `.design/project-log/2026-05-13-fix-hub-test-grove-to-project.md` -- `.design/project-log/2026-05-13-fix-delete-test-grove-to-project.md` -- `.design/project-log/2026-05-13-fix-list-test-grove-to-project.md` -- `.design/project-log/2026-05-13-rebase-grove-v2.md` - -### 11. Extras / Satellite Projects - -| Project | Refs (production) | Key Issues | -|---|---|---| -| `extras/scion-chat-app/` | ~215 | SQL schema with `grove_id` columns, NATS topics, command handlers, state management | -| `extras/scion-a2a-bridge/` | ~80 | Config type alias (`GroveConfig`), NATS topics, bridge server routes | -| `extras/fs-watcher-tool/` | ~40 | `GroveDiscovery` struct, Docker label queries, `--grove` flag | -| `extras/scion-telegram/` | ~12 | NATS topic parsing, broker commands | -| `extras/agent-viz/` | ~2 | Log label parsing (`grove_id`) | -| `extras/scion-broker-log/` | ~3 (README only) | Example NATS topics in docs | - -### 12. Hub API Legacy Endpoints - -Deprecated `/api/v1/groves/*` routes are aliased to `/api/v1/projects/*` handlers: - -``` -/api/v1/groves → handleProjects (wrapped with deprecateGroveEndpoint) -/api/v1/groves/register → handleProjectRegister (wrapped with deprecateGroveEndpoint) -/api/v1/groves/ → handleProjectRoutes (wrapped with deprecateGroveEndpoint) -``` - -Query parameter fallbacks: `groveId` → `projectId` in notification and template handlers. - -Web UI route alias: `/groves` → `/projects` in `pkg/hub/web.go:786`. - -Broker endpoint: `/api/v1/workspace/grove-upload` in `pkg/runtimebroker/server.go:1452`. - -### 13. Settings/Config Key Mapping - -- `grove_id` setting key accepted as alias for `project_id` in `pkg/config/settings.go:619,758` and `pkg/config/settings_v1.go:1705,1810` -- `hub.groveId` / `hub.grove_id` accepted as alias for `hub.projectId` / `hub.project_id` in `pkg/config/settings.go:657,794` and `pkg/config/settings_v1.go:1729,1836` -- V1 settings struct: `ProjectID string json:"grove_id"` in `pkg/config/settings_v1.go:440` -- Koanf loading: `hub.grove_id` remapping logic in `pkg/config/koanf.go:82-120` - ---- - -## Categorization for Rename Priority - -### Tier 1: Safe to Rename (Internal Only) - -These changes have no external contract implications: - -- **Go local variable names** — `groveID` → `projectID`, `grovePath` → `projectPath`, etc. -- **Go function names** — `deprecateGroveEndpoint`, `hubEndpointFromProjectSettings(grovePath)`, etc. -- **Go struct field names with YAML tags** — `GroveID string yaml:"grove-id"` in project_marker.go -- **Comments and log messages** containing "grove" -- **Test function names** — 51 test functions with "grove" in name -- **Internal file names** — `extras/fs-watcher-tool/pkg/fswatcher/grove.go`, `default_grove_settings.yaml` -- **Design docs** — historical but could be updated - -**Estimated scope:** ~1,200 lines in production code, ~2,100 lines in tests - -### Tier 2: Requires Coordinated Migration - -These have cross-system contracts that require careful phasing: - -- **Container labels** (`scion.grove`, `scion.grove_id`, `scion.grove_path`) — running containers use these labels; needs dual-label period or version bump -- **Environment variables** (`SCION_GROVE_ID`, `SCION_GROVE`, `SCION_GROVE_PATH`) — injected into containers; agents and harness tools read them -- **NATS topic prefix** (`scion.grove..*`) — wire protocol between broker, hub, and satellite apps -- **Filesystem paths** (`grove-configs/`, `.scion.groves/`, `grove-id` file) — on-disk format with fallback reads already implemented -- **Telemetry attributes** (`grove_id`, `scion.grove.id`) — downstream dashboards/queries may reference these -- **Storage paths** (`templates/groves/`, `harness-configs/groves/`, `workspaces//grove-workspace`) — GCS/local storage paths - -**Estimated scope:** ~200 lines in production code - -### Tier 3: Must Remain (Backward Compatibility) - -These should stay as-is until a major version boundary: - -- **JSON struct tags** (`json:"groveId"`, `json:"grove"`, `json:"groves"`) — wire format compat for clients -- **Deprecated CLI flags** (`--grove`) — properly marked deprecated, will be removed in a future major version -- **Hub API legacy endpoints** (`/api/v1/groves/*`) — deprecated with headers -- **SQL migration history** (V1-V49 DDL) — immutable historical migrations -- **Settings key aliases** (`grove_id`, `hub.groveId`) — config file backward compat -- **Query parameter fallbacks** (`groveId` → `projectId`) — API compat -- **Ent migration file** (`migrate_grove_to_project.go`) — the migration itself references grove by necessity - -**Estimated scope:** ~500 lines in production code - ---- - -## Top 15 Files by Grove Reference Count (Production) - -| # | File | Refs | Primary Category | -|---|---|---|---| -| 1 | `pkg/runtimebroker/handlers.go` | 172 | Variables, labels, log attrs | -| 2 | `pkg/hubsync/sync.go` | 152 | Variables, env vars, settings keys | -| 3 | `pkg/store/sqlite/sqlite.go` | 117 | SQL schema DDL | -| 4 | `pkg/store/models.go` | 93 | JSON marshal/unmarshal compat | -| 5 | `pkg/hubsync/prompt.go` | 79 | Variables, user prompts | -| 6 | `extras/scion-chat-app/internal/chatapp/commands.go` | 67 | NATS topics, variables | -| 7 | `pkg/hubclient/types.go` | 57 | JSON compat shims | -| 8 | `pkg/runtimebroker/types.go` | 52 | JSON tags, struct fields | -| 9 | `extras/scion-chat-app/internal/state/state.go` | 47 | SQL schema, column refs | -| 10 | `pkg/runtimebroker/server.go` | 44 | Variables, dir scanning | -| 11 | `pkg/hubclient/notifications.go` | 37 | JSON compat shims | -| 12 | `pkg/config/settings.go` | 34 | Config key aliases, migration | -| 13 | `pkg/runtimebroker/start_context.go` | 32 | Path resolution, env injection | -| 14 | `pkg/runtime/k8s_runtime.go` | 31 | Labels, PVC selectors | -| 15 | `pkg/hub/events.go` | 29 | Event type compat | - ---- - -## Observations - -1. **Migration V50 exists** (`pkg/store/sqlite/sqlite.go:1236-1250`) and handles the SQL schema rename (`grove_id` → `project_id`, `grove_contributors` → `project_contributors`, etc.). However, all initial schema DDL and intermediate migrations (V1-V49) naturally retain grove naming as historical DDL. - -2. **The extras/ directory is under-migrated.** The chat app, A2A bridge, and fs-watcher tool have significant grove references in both code and schema that haven't been updated. - -3. **Dual-field JSON marshaling is thorough** — virtually every API-facing struct in `pkg/store/models.go`, `pkg/hubclient/`, and `pkg/runtimebroker/types.go` has proper backward-compat shims. - -4. **The NATS topic prefix `scion.grove.*`** is the most architecturally sensitive remaining reference — it's a cross-service wire protocol used by the broker, hub, chat app, A2A bridge, and Telegram bot. Changing this requires either dual-subscription during a transition period or a coordinated version bump. - -5. **Container labels** (`scion.grove`, `scion.grove_id`, `scion.grove_path`) are similarly sensitive — the system uses these labels for container discovery, filtering, and lifecycle management across Docker, Podman, Apple Virtualization, and Kubernetes runtimes. - -6. **The web frontend has zero grove references** — this is fully migrated. - -7. **Config key aliases are well-implemented** — `grove_id`, `hub.groveId`, and env vars like `SCION_HUB_GROVE_ID` all correctly map to their project equivalents through the koanf loading layer. diff --git a/GLOSSARY.md b/GLOSSARY.md index fe929d28f..d221851d6 100644 --- a/GLOSSARY.md +++ b/GLOSSARY.md @@ -200,7 +200,7 @@ The three run modes at a glance — distinguish them by whether a server runs an | Mode | Server | Tenancy | State & isolation | Canonical use | |------|--------|---------|-------------------|----------------| | **Local mode** | None | Single user | Local machine; isolation via git worktrees | Agents launched directly via the `scion` CLI, no server | -| **Workstation mode** | Combo server (Hub + Runtime Broker + Web) on loopback | Single-tenant | Local machine; single-tenant state | The hosted experience locally, on your own machine | +| **Workstation mode** | Combo server (Hub + Runtime Broker + Web) on loopback | Single-tenant | That machine | The hosted experience locally, on your own machine | | **Hosted mode** | Multi-user server deployment | Multi-user | Hub-coordinated across brokers | Coordinating state across users, projects, and runtime brokers | **Local mode**: diff --git a/README.md b/README.md index 1f23ae404..6d12f1fd8 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,7 @@ _sci·on /ˈsīən/ — a young shoot or twig, cut for grafting or rooting._ Scion is an experimental multi-agent orchestration testbed designed to manage "deep agents" running in containers. -Scion orchestrates "deep agents" (Claude Code, Gemini CLI, Codex, and others) as isolated, concurrent processes. Each agent gets its own container, git worktree, and credentials — so they can work on different parts of your project without stepping on each other. Agents run locally, on remote VMs, or across Kubernetes clusters. +Scion orchestrates "deep agents" (Claude Code, Gemini CLI, and others) as isolated, concurrent processes. Each agent gets its own container, git worktree, and credentials — so they can work on different parts of your project without stepping on each other. Agents run locally, on remote VMs, or across Kubernetes clusters. Rather than prescribing rigid orchestration patterns, Scion takes a "less is more" approach: agents dynamically learn a CLI tool, letting the models themselves decide how to coordinate among agents. This makes it a rapid prototype testbed for experimenting with multi-agent patterns through natural language prompting. Read more in [Philosophy](https://googlecloudplatform.github.io/scion/philosophy/). @@ -69,7 +69,7 @@ scion start debug "Help me debug this error" --attach ## Key Features -- **Harness Agnostic** — Works with Gemini CLI, Claude Code, OpenCode, and Codex. Adaptable to anything that runs in a container. +- **Harness Agnostic** — Ships with Gemini CLI and Claude Code by default. Additional harnesses (OpenCode, Codex, Antigravity) are available as [opt-in bundles](harnesses/README.md). Adaptable to anything that runs in a container. - **True Isolation** — Each agent runs in its own container with separated credentials, config, and a dedicated `git worktree`, preventing merge conflicts. - **Parallel Execution** — Run multiple agents concurrently as fully independent processes, locally or remotely. - **Attach / Detach** — Agents run in `tmux` sessions for background operation. Attach for human-in-the-loop interaction, enqueue messages while detached, and tunnel into remote agents securely. diff --git a/agents.md b/agents.md index 505c232ea..d60e79c41 100644 --- a/agents.md +++ b/agents.md @@ -86,6 +86,13 @@ All icons in the web frontend use the Shoelace `` component (Bootstrap > **Canonical engineering glossary:** See [`GLOSSARY.md`](./GLOSSARY.md) at the repo root for the canonical, opinionated terminology used throughout the codebase — the preferred term for each concept and the synonyms to avoid. Prefer these terms in new code, comments, and docs. +These terms may be used in shorthand with prompts + +- **hub-broker, combo server** References running the server command with both the hub function and the broker function running in the same invocation. +- **hub-native, hub-project** A special variant of a project/project space, that is created on a hub server for use by agents dispatched from clients. These live in ~/.scion/projects/ on any broker that is a provider to the hub project. This is in contrast to the arbitrary local path on a broker for a linked project. +- **agent-home** The directory that gets mounted as the home folder of the container user in the agent container +- **linked-project** A project and project folder that pre-existed on a broker machine, and is linked as a hub resource project for visibility, metadata, and agent management across other brokers that may have such a linked project. May be based on name or git-URI + ## Project use of the scion cli itself Do not commit changes in the project's own `.scion` folder to git as part of committing progress on code and docs. These are managed and committed manually when template defaults are intentionally updated. @@ -144,6 +151,20 @@ Learned the hard way; these are specific to running inside this container. 1. Notify the user you have completed the task +## What NOT to commit + +Follow these rules to keep the repository clean and the git history lean. + +- **No binary files** (images, photos, compiled artifacts) unless they are part of the shipped product UI. Development screenshots, Telegram downloads, and similar media must never be committed. +- **No development screenshots.** Use PR comments or issue attachments to share before/after visuals — not files in the repo. +- **No agent orchestration state files.** Files like `.coordinator-state.md`, `.eng-manager-state.md`, or other runtime state produced by the Scion agent system are ephemeral and must not be tracked. +- **No test artifacts or generated data files.** One-off debugging scripts (`test_json.go`), format-conversion utilities (`format_callouts.py`), and generated fixtures belong in scratch space or should be gitignored. +- **No scratch or task-tracking files.** The `.scratch/` and `.tasks/` directories are gitignored. Do not force-add files into them (`git add -f`). +- **No PR review artifacts.** Code-review notes (`pr-*-review*.md`) belong in the PR discussion thread, not as committed files. +- **The `downloads/` directory is gitignored.** Any file a harness or agent downloads at runtime stays local — never commit its contents. + +When in doubt, check `.gitignore` before staging. If a new category of generated or ephemeral file appears, add a `.gitignore` entry in the same PR that introduces the workflow. + ## Agent memory & durable notes (IMPORTANT) **Do not rely on any harness's built-in / native memory feature.** This applies to every harness (Claude, Gemini, etc.), not just one. The per-agent memory directory is **ephemeral — it is not persisted across container restarts**, so anything written there is silently lost between sessions and gives a false sense of continuity. diff --git a/changelog/2026-05-29-changelog.md b/changelog/2026-05-29-changelog.md new file mode 100644 index 000000000..f87dc0250 --- /dev/null +++ b/changelog/2026-05-29-changelog.md @@ -0,0 +1,12 @@ +# Release Notes (2026-05-29) + +This release focuses on the execution of **Phase 2 of the "Grove to Project" architectural rename**, a massive internal refactoring effort to modernize the codebase's core terminology. + +## 🚀 Features + +* **Project Terminology Migration (Phase 2):** Completed a comprehensive "Tier 1" refactor (internal identifiers and comments) across the core backend and CLI. This ensures consistent use of "Project" instead of "Grove" in function parameters, local variables, and internal documentation. + * **Broad Package Coverage:** Systematic updates were applied to `pkg/hub`, `pkg/api`, `pkg/config`, `pkg/agent`, `pkg/runtime`, `pkg/broker`, `pkg/logging`, `pkg/hubsync`, `extras/scion-telegram`, and `cmd/`. + * **Internal Logic Modernization:** Renamed thousands of identifiers, including critical manager state variables (e.g., `projectsToScan`, `projectSlug`, `projectScionDir`) and internal struct fields. + * **Robust Backward Compatibility:** To ensure zero disruption, all external-facing components—including API endpoint paths (`/api/v1/groves/`), JSON tags, NATS topic prefixes (`scion.grove.`), and environment variables (`SCION_GROVE_ID`)—remain unchanged in this phase. + * **Test Integrity:** Thousands of lines of test fixtures, assertions, and mock data were migrated to the new terminology while verifying that no regressions occurred in the underlying logic. + * **Completion Strategy:** Introduced a new "Grove Rename Survey" and updated package-specific project logs to track the transition towards Phase 3. diff --git a/changelog/2026-05-30-changelog.md b/changelog/2026-05-30-changelog.md new file mode 100644 index 000000000..4b03cdbc2 --- /dev/null +++ b/changelog/2026-05-30-changelog.md @@ -0,0 +1,26 @@ +# Release Notes (2026-05-30) + +This release marks a major milestone with the introduction of the Telegram message broker plugin and the completion of the second phase of the "Grove to Project" architectural rename. + +## 🚀 Features + +* **Telegram Message Broker Plugin:** A comprehensive new integration allowing users to interact with Scion agents directly via Telegram. + * **Interactive Commands:** Manage agents and view status using `/agents`, `/status`, and `/default`. + * **Identity Linking:** Securely link Telegram accounts to Scion identities using the `/register` flow. + * **Rich Notifications:** Receive real-time agent state updates (started, completed, error, input needed) with formatted HTML status cards. + * **Intelligent Routing:** Support for @mentions and native Telegram replies in group chats to direct messages to specific agents. + * **File Support:** Send and receive file attachments (photos and documents) directly through the chat interface. + * **Reliability:** Built-in per-chat rate limiting and automatic retry logic for transient Telegram API errors. +* **Multi-Broker Fan-Out:** Introduced the `FanOutBroker`, enabling the Hub to dispatch messages to multiple backends simultaneously. This allows for concurrent delivery to plugins (like Telegram) while maintaining internal processing and logging. +* **Project Terminology Migration:** Completed "Phase 2" of the internal refactor renaming "Grove" to "Project" across identifiers, comments, and internal logic. This aligns the codebase with updated branding and architectural goals. + +## 🐛 Fixes + +* **Hub Security:** Telegram bot tokens and other sensitive broker credentials are now redacted from error logs to prevent accidental exposure. +* **Message Stability:** Resolved a "double-delivery" issue in `PublishUserMessage` where messages were occasionally duplicated when using the fan-out broker. +* **Resource Protection:** Fixed a "message storm" bug where canceled HTTP contexts could trigger excessive error logging in broker subscription callbacks. +* **Security Hardening:** + * Implemented IP-based rate limiting on the Telegram link verification endpoint. + * Added path traversal protection for file attachment resolution. + * Switched to constant-time comparison for webhook secret verification. +* **UI/UX Polishing:** Updated Telegram agent state emojis to match the web UI (💤 idle, ⚙️ executing, ✅ completed, etc.). diff --git a/changelog/2026-05-31-changelog.md b/changelog/2026-05-31-changelog.md new file mode 100644 index 000000000..eef7175a9 --- /dev/null +++ b/changelog/2026-05-31-changelog.md @@ -0,0 +1,32 @@ +# Release Notes (2026-05-31) + +This release introduces significant architectural improvements to template portability and a major terminology shift from "grove" to "project" across the entire system. + +## ⚠️ BREAKING CHANGES +- **Template Portability:** Harness-specific fields (`image`, `model` when using concrete provider names, and `auth_selectedType`) are now deprecated in `scion-agent.yaml`. While these remain functional for backward compatibility, they will trigger deprecation warnings. Users should migrate these settings to harness configurations and use the new model size aliases. + +## 🚀 Features +- **Harness-Agnostic Templates:** Templates are now decoupled from specific LLM backends for improved portability. + - **Model Size Aliases:** Introduced `small`, `medium`, and `large` aliases. Harness configurations now map these to concrete provider models (e.g., `gemini-pro`, `claude-opus`), allowing the same template to work across different harnesses. + - **Deprecation Warnings:** The system now warns when templates contain hardcoded harness-specific environment or model data. +- **Terminology Shift (Grove → Project):** The term "grove" has been replaced with **"project"** in all user-facing CLI help text, logs, and documentation. + - The filesystem watcher now supports the `--project` flag (with `--grove` maintained as a deprecated alias). + - Internal settings and default configurations have been updated to reflect the "project" naming convention. +- **Vocabulary Alignment:** + - Server mode "production" has been renamed to **"hosted"**. + - "Hub-native" naming has converged to **"hub-managed"**. + - The messaging "set" concept is now referred to as a **"message group"**. +- **Skills Management:** Skills are now strictly template-only, simplifying their integration and lifecycle within the agent composition model. + +## 🐛 Fixes +- **Agent Identity & Collision:** + - Resolved cross-project slug collisions in broker exec/stop operations (impacting `scion look`). + - Fixed agent slug collisions across projects during broker heartbeats. +- **Messaging Improvements:** + - Fixed Telegram mention parsing for agents with hyphenated names. + - Improved error handling when providing bare email recipients to the message command. + - Added validation for empty event names and corrected missing field mappings in `MappingDialect`. +- **System Stability:** + - Fixed CI failures resulting from the major terminology rename. + - Removed defunct harness plugin types from the plugin system. + - Cleaned up stale deterministic-UUID language and corrected internal code documentation. diff --git a/changelog/2026-06-01-changelog.md b/changelog/2026-06-01-changelog.md new file mode 100644 index 000000000..7609d30b0 --- /dev/null +++ b/changelog/2026-06-01-changelog.md @@ -0,0 +1,21 @@ +# Release Notes (2026-06-01) + +This release focuses on significant architectural cleanup, specifically disambiguating the "broker" terminology, alongside major enhancements to resource management and new support for Google Cloud Storage. + +## ⚠️ BREAKING CHANGES +* **CLI / Internal API:** The `scion broker` command has been renamed to `scion runtime-broker` to avoid confusion with Message Brokers. While a deprecated alias remains for now, users should update their scripts. Internally, `pkg/broker` has been renamed to `pkg/eventbus`, and related types (e.g., `MessageBroker` -> `EventBus`) have been updated. + +## 🚀 Features +* **Resource Management Overhaul:** Major refactor of resource storage, caching, and import logic. This includes support for hub-level imports, improved progress tracking, and significant performance optimizations for large resource sets. +* **Google Cloud Storage (GCS) Support:** Introduced native support for GCS resources, allowing agents to interact directly with GCS buckets. +* **Hub UI Improvements:** Added a new collapsible side panel to the Hub web interface for better screen real estate management. +* **Messaging & Broker Plugins:** Implemented chat channel routing for broker plugins, enabling more sophisticated message orchestration. +* **Harness Observability:** Added content-type filtering for assistant responses, improving the granularity of observability logs. +* **Engineering Glossary:** Introduced `GLOSSARY.md` to establish canonical terminology across the codebase and documentation. + +## 🐛 Fixes +* **Agent Lifecycle:** Resolved a critical bug where the `resume` command would incorrectly create new agents instead of restarting existing stopped ones. +* **Stability:** Fixed a Hub crash that occurred when the Cloud Logging service experienced metadata outages. +* **Auth & Integration:** + * Added `issues:write` to default GitHub App token permissions to support issue-tracking features. + * Fixed remote template imports by adding `GITHUB_TOKEN` secret fallback support. diff --git a/changelog/2026-06-02-changelog.md b/changelog/2026-06-02-changelog.md new file mode 100644 index 000000000..720071e0c --- /dev/null +++ b/changelog/2026-06-02-changelog.md @@ -0,0 +1,12 @@ +# Release Notes (2026-06-02) + +This release focuses on improving the robustness of the message broker with better channel filtering and thread support, alongside critical fixes for test isolation. + +## 🚀 Features +* **Message Broker Channel Filtering & Threading:** Introduced strict channel filtering in broker plugins to prevent cross-channel message delivery (e.g., Telegram replies leaking into Google Chat). Added end-to-end support for thread ID propagation, ensuring agent replies land in the correct conversation threads or forum topics across supported platforms. +* **Google Chat Thread Context:** The Google Chat integration now automatically captures and propagates thread context for both inbound messages and `ask_user` dialog responses. + +## 🐛 Fixes +* **Test Suite Hub Isolation:** Fixed a significant issue where integration tests could leak live Hub credentials from the environment. This prevented tests from accidentally resetting the state of real agents. The fix includes new test helpers for safe environment variable management. +* **Chat App Routing:** Resolved routing issues where `ask_user` responses and outbound messages were occasionally misdirected due to missing or incorrect channel identifiers. +* **Telegram Thread ID Forwarding:** Fixed a bug in the Telegram plugin where thread IDs were captured on inbound messages but not included in outbound replies. diff --git a/changelog/2026-06-03-changelog.md b/changelog/2026-06-03-changelog.md new file mode 100644 index 000000000..a3c6fd24e --- /dev/null +++ b/changelog/2026-06-03-changelog.md @@ -0,0 +1,19 @@ +# Release Notes (2026-06-03) + +This release focuses on improving Web UI usability, enhancing infrastructure provisioning flexibility, and providing comprehensive documentation for advanced deployment scenarios like multi-broker setups and external channel integrations. + +## 🚀 Features + +* **Web UI Enhancements:** + * **Terminal Connectivity:** Added a prominent "DISCONNECTED" overlay to the web terminal. This full-terminal indicator provides immediate visual feedback when the WebSocket connection drops, replacing the subtle toolbar-only signal. + * **Agent Management:** Introduced sorting and filtering capabilities to the agent list view, making it easier to manage and locate specific agents in larger environments. +* **Infrastructure & Provisioning:** + * **Starter Hub Flexibility:** Added support for `MACHINE_TYPE` overrides in the `starter-hub` provisioning scripts, allowing for more granular control over GCE resource allocation. +* **Documentation:** + * **Advanced Guides:** Published new documentation covering Multi-Broker setups, GCE Hub provisioning, and External Channel integrations (Telegram, Discord, and A2A protocol bridges). + +## 🐛 Fixes + +* **Stability:** Resolved a nil-pointer panic in the `harness-config` command that occurred when the Hub was disabled. +* **Setup Scripts:** Fixed a permission issue in `gce-demo-setup-repo.sh` by ensuring `sudo` is used for repository path existence checks. +* **Documentation:** Updated the `starter-hub` README to document `REGION` and `ZONE` override support. diff --git a/changelog/2026-06-04-changelog.md b/changelog/2026-06-04-changelog.md new file mode 100644 index 000000000..2f60a4411 --- /dev/null +++ b/changelog/2026-06-04-changelog.md @@ -0,0 +1,15 @@ +# Release Notes (2026-06-04) + +This release marks a major architectural milestone for Scion, introducing the foundational components for **Multi-node and Distributed Operations**. The system now supports horizontal scaling of Hub replicas, distributed message brokering, and shared agent workspaces. + +## 🚀 Features + +* **Postgres Storage Backend:** Migrated the core persistence layer to a Postgres backend using `ent` and `pgx/v5`. This shift enables multiple Hub replicas to share state, leveraging Postgres-native advisory locks for distributed coordination and `LISTEN/NOTIFY` for efficient, real-time cross-node event propagation. +* **Multi-node Broker Dispatch:** Introduced a distributed dispatching system for message brokers. This includes support for broker affinity, durable intent tracking, and intelligent message routing across a cluster, ensuring reliable communication in multi-node deployments. +* **NFS-Coordinated Workspace Sharing:** Implemented shared workspace support via NFS, allowing agents running on different nodes to access and coordinate on the same project data. This feature provides a unified storage model across Docker (Model A) and GKE/Kubernetes (Model B) environments. + +## 🧹 Chores & Internal + +* **Engineering Glossary:** Added a comprehensive `GLOSSARY.md` to the repository root to establish a canonical "ubiquitous language" for Scion terminology. +* **Developer Tooling Reorganization:** Consolidated developer convenience scripts and Go tools into the `hack/` directory and added Kubernetes manifests for testing NFS workspace scenarios. +* **Cleanup:** Removed legacy scratchpad markdown files and optimized the internal build configuration for developer tools. diff --git a/changelog/2026-06-05-changelog.md b/changelog/2026-06-05-changelog.md new file mode 100644 index 000000000..8a6940359 --- /dev/null +++ b/changelog/2026-06-05-changelog.md @@ -0,0 +1,21 @@ +# Release Notes (2026-06-05) + +This release introduces significant architectural improvements focused on scalability and infrastructure flexibility. Key highlights include the addition of a Postgres storage backend, multi-node broker dispatch, and NFS-coordinated workspace sharing. + +## ⚠️ BREAKING CHANGES +* **Workspace Pathing:** The workspace file browser now resolves to `groves/` instead of `projects/`. This aligns with internal resource naming conventions but may affect external scripts or bookmarks. +* **Database Migration:** A new in-process migration from legacy raw-SQL `hub.db` to the Ent-backed schema is now active. While automatic, operators are encouraged to backup their database before upgrading. + +## 🚀 Features +* **Postgres Storage Backend:** Full support for Postgres using the `pgx` driver, featuring Ent schema parity and real-time event distribution via `LISTEN/NOTIFY`. +* **Multi-Node Broker Dispatch:** Introduced a robust dispatch system for multi-replica environments, including affinity-based routing and durable intent tracking. +* **NFS-Coordinated Workspaces:** Enabled workspace sharing across nodes using NFS, supporting both Docker and GKE/Cloud Run runtime environments. +* **Google IAP Auth Proxy:** Added support for Google Identity-Aware Proxy (IAP) as an authentication proxy. +* **Resource Hardening:** Implemented resource cloning and deletion with hardened authorization checks. + +## 🐛 Fixes +* **Multi-Node Stability:** Resolved session management issues and improved Cloud Run deployment stability. +* **UI/UX Refinements:** Fixed task overflow in the agent list and unified action buttons for a consistent experience. +* **Broker Reliability:** Prevented stale disconnect events from incorrectly marking reconnected brokers as offline. +* **Lifecycle Reliability:** Guarded hub agent phase transitions against spurious session lifecycle events. +* **Token Protection:** Prevented `sciontool` tests from accidentally clobbering live agent tokens. diff --git a/cmd/common_envgather_test.go b/cmd/common_envgather_test.go index aa7c13920..6817a371a 100644 --- a/cmd/common_envgather_test.go +++ b/cmd/common_envgather_test.go @@ -49,7 +49,7 @@ func TestGatherAndSubmitEnv_NonInteractiveGathersFromLocalEnv(t *testing.T) { defer os.Unsetenv("TEST_SECRET_KEY") // Set up mock Hub server - projectID := "grove-1" + projectID := tid("grove-1") server, captured := newEnvGatherMockHubServer(t, projectID) defer server.Close() @@ -63,9 +63,9 @@ func TestGatherAndSubmitEnv_NonInteractiveGathersFromLocalEnv(t *testing.T) { } resp := &hubclient.CreateAgentResponse{ - Agent: &hubclient.Agent{ID: "agent-1"}, + Agent: &hubclient.Agent{ID: tid("agent-1")}, EnvGather: &hubclient.EnvGatherResponse{ - AgentID: "agent-1", + AgentID: tid("agent-1"), Required: []string{"TEST_SECRET_KEY"}, Needs: []string{"TEST_SECRET_KEY"}, }, @@ -103,7 +103,7 @@ func TestGatherAndSubmitEnv_NonInteractiveAllowsWhenAllSatisfied(t *testing.T) { }, } - result, err := gatherAndSubmitEnv(context.Background(), nil, "grove-1", resp) + result, err := gatherAndSubmitEnv(context.Background(), nil, tid("grove-1"), resp) require.NoError(t, err) // Should return the original response since no env was gathered assert.Equal(t, resp, result) @@ -137,7 +137,7 @@ func TestGatherAndSubmitEnv_NonInteractiveMultipleKeysMissing(t *testing.T) { }, } - _, err := gatherAndSubmitEnv(context.Background(), nil, "grove-1", resp) + _, err := gatherAndSubmitEnv(context.Background(), nil, tid("grove-1"), resp) require.Error(t, err) assert.Contains(t, err.Error(), "cannot satisfy required environment variables") assert.Contains(t, err.Error(), "KEY_A") @@ -231,7 +231,7 @@ func TestStartAgentViaHub_GlobalGroveSkipsWorkspaceBootstrap(t *testing.T) { outputFormat = "json" templateName = "" harnessConfigFlag = "codex" - runtimeBrokerID = "broker-1" + runtimeBrokerID = tid("broker-1") globalDir := t.TempDir() settingsPath := filepath.Join(globalDir, "settings.yaml") @@ -256,12 +256,12 @@ func TestStartAgentViaHub_GlobalGroveSkipsWorkspaceBootstrap(t *testing.T) { captured = &req json.NewEncoder(w).Encode(&hubclient.CreateAgentResponse{ Agent: &hubclient.Agent{ - ID: "agent-1", - Slug: "agent-1", - Name: "agent-1", + ID: tid("agent-1"), + Slug: tid("agent-1"), + Name: tid("agent-1"), Status: "running", Phase: "running", - RuntimeBrokerID: "broker-1", + RuntimeBrokerID: tid("broker-1"), RuntimeBrokerName: "scion", Created: time.Now().UTC(), }, @@ -312,7 +312,7 @@ func newEnvGatherMockHubServer(t *testing.T, projectID string) (*httptest.Server captured[k] = v } json.NewEncoder(w).Encode(map[string]interface{}{ - "agent": map[string]interface{}{"id": "agent-1", "status": "running"}, + "agent": map[string]interface{}{"id": tid("agent-1"), "status": "running"}, }) default: @@ -365,9 +365,9 @@ func TestGatherAndSubmitEnv_InteractiveSecretPrompt(t *testing.T) { } resp := &hubclient.CreateAgentResponse{ - Agent: &hubclient.Agent{ID: "agent-1"}, + Agent: &hubclient.Agent{ID: tid("agent-1")}, EnvGather: &hubclient.EnvGatherResponse{ - AgentID: "agent-1", + AgentID: tid("agent-1"), Required: []string{"MY_SECRET"}, Needs: []string{"MY_SECRET"}, SecretInfo: map[string]hubclient.SecretKeyInfo{ @@ -403,9 +403,9 @@ func TestGatherAndSubmitEnv_FileSecretShowsGuidance(t *testing.T) { outputFormat = "json" // suppress stderr resp := &hubclient.CreateAgentResponse{ - Agent: &hubclient.Agent{ID: "agent-1"}, + Agent: &hubclient.Agent{ID: tid("agent-1")}, EnvGather: &hubclient.EnvGatherResponse{ - AgentID: "agent-1", + AgentID: tid("agent-1"), Required: []string{"FILE_CERT"}, Needs: []string{"FILE_CERT"}, SecretInfo: map[string]hubclient.SecretKeyInfo{ @@ -418,7 +418,7 @@ func TestGatherAndSubmitEnv_FileSecretShowsGuidance(t *testing.T) { }, } - _, err := gatherAndSubmitEnv(context.Background(), nil, "grove-1", resp) + _, err := gatherAndSubmitEnv(context.Background(), nil, tid("grove-1"), resp) require.Error(t, err) assert.Contains(t, err.Error(), "FILE_CERT") } @@ -440,9 +440,9 @@ func TestGatherAndSubmitEnv_MixedSecretAndEnvKeys(t *testing.T) { // ENV_ONLY is not in SecretInfo → it's an env-only key that can't be prompted resp := &hubclient.CreateAgentResponse{ - Agent: &hubclient.Agent{ID: "agent-1"}, + Agent: &hubclient.Agent{ID: tid("agent-1")}, EnvGather: &hubclient.EnvGatherResponse{ - AgentID: "agent-1", + AgentID: tid("agent-1"), Required: []string{"ENV_ONLY", "SECRET_KEY"}, Needs: []string{"ENV_ONLY", "SECRET_KEY"}, SecretInfo: map[string]hubclient.SecretKeyInfo{ @@ -454,7 +454,7 @@ func TestGatherAndSubmitEnv_MixedSecretAndEnvKeys(t *testing.T) { }, } - _, err := gatherAndSubmitEnv(context.Background(), nil, "grove-1", resp) + _, err := gatherAndSubmitEnv(context.Background(), nil, tid("grove-1"), resp) require.Error(t, err) // Should fail because ENV_ONLY is not secret-eligible and not in local env assert.Contains(t, err.Error(), "ENV_ONLY") @@ -481,9 +481,9 @@ func TestGatherAndSubmitEnv_NonInteractiveSecretsMissing(t *testing.T) { os.Unsetenv("ENV_B") resp := &hubclient.CreateAgentResponse{ - Agent: &hubclient.Agent{ID: "agent-1"}, + Agent: &hubclient.Agent{ID: tid("agent-1")}, EnvGather: &hubclient.EnvGatherResponse{ - AgentID: "agent-1", + AgentID: tid("agent-1"), Required: []string{"SECRET_A", "ENV_B"}, Needs: []string{"SECRET_A", "ENV_B"}, SecretInfo: map[string]hubclient.SecretKeyInfo{ @@ -495,7 +495,7 @@ func TestGatherAndSubmitEnv_NonInteractiveSecretsMissing(t *testing.T) { }, } - _, err := gatherAndSubmitEnv(context.Background(), nil, "grove-1", resp) + _, err := gatherAndSubmitEnv(context.Background(), nil, tid("grove-1"), resp) require.Error(t, err) assert.Contains(t, err.Error(), "cannot satisfy required environment variables") } @@ -528,9 +528,9 @@ func TestGatherAndSubmitEnv_InteractiveSecretEmptyInput(t *testing.T) { } resp := &hubclient.CreateAgentResponse{ - Agent: &hubclient.Agent{ID: "agent-1"}, + Agent: &hubclient.Agent{ID: tid("agent-1")}, EnvGather: &hubclient.EnvGatherResponse{ - AgentID: "agent-1", + AgentID: tid("agent-1"), Required: []string{"MY_SECRET"}, Needs: []string{"MY_SECRET"}, SecretInfo: map[string]hubclient.SecretKeyInfo{ @@ -542,7 +542,7 @@ func TestGatherAndSubmitEnv_InteractiveSecretEmptyInput(t *testing.T) { }, } - _, err := gatherAndSubmitEnv(context.Background(), nil, "grove-1", resp) + _, err := gatherAndSubmitEnv(context.Background(), nil, tid("grove-1"), resp) require.Error(t, err) assert.Contains(t, err.Error(), "MY_SECRET") } diff --git a/cmd/delete_test.go b/cmd/delete_test.go index 4683e8330..6e486cd35 100644 --- a/cmd/delete_test.go +++ b/cmd/delete_test.go @@ -394,7 +394,7 @@ func TestDeleteAgentsViaHub_LocalCleanupFailureCreatesStaleLocalNotToRegister(t func TestDeleteStopped_RequiresGroveContext(t *testing.T) { // Unset Hub context to avoid synthetic project root detection - for _, e := range []string{"SCION_HUB_ENDPOINT", "SCION_HUB_URL", "SCION_GROVE_ID"} { + for _, e := range []string{"SCION_HUB_ENDPOINT", "SCION_HUB_URL", "SCION_GROVE_ID", "SCION_PROJECT_ID"} { if val, ok := os.LookupEnv(e); ok { os.Unsetenv(e) defer os.Setenv(e, val) diff --git a/cmd/harness_config.go b/cmd/harness_config.go index c422ff120..0fe3ef654 100644 --- a/cmd/harness_config.go +++ b/cmd/harness_config.go @@ -82,7 +82,7 @@ var harnessConfigListCmd = &cobra.Command{ // Include Hub results if requested if showHub { hubCtx, err := CheckHubAvailabilityWithOptions(gp, true) - if err == nil { + if err == nil && hubCtx != nil { hubResp, err := hubCtx.Client.HarnessConfigs().List(context.Background(), &hubclient.ListHarnessConfigsOptions{ Status: "active", }) @@ -163,13 +163,12 @@ This overwrites config.yaml and home directory files with the built-in versions. return fmt.Errorf("harness-config %q not found at %s: %w", name, targetDir, err) } - // Reset always seeds from the built-in defaults of the underlying - // harness type, so use the legacy New() shim here rather than - // Resolve(); the latter would dispatch container-script when - // activated, but reset must restore the embedded built-in fileset. h := harness.New(hcDir.Config.Harness) - // Reset by re-seeding with force=true + if _, basePath := h.GetHarnessEmbedsFS(); basePath == "" { + return fmt.Errorf("cannot reset %q: it is installed from a bundle and has no built-in defaults; reinstall with: scion harness-config install harnesses/%s", name, hcDir.Config.Harness) + } + if err := config.SeedHarnessConfig(targetDir, h, true); err != nil { return fmt.Errorf("failed to reset harness-config %q: %w", name, err) } @@ -297,6 +296,9 @@ var harnessConfigSyncCmd = &cobra.Command{ if err != nil { return err } + if hubCtx == nil { + return fmt.Errorf("Hub integration is not enabled. Configure via 'scion config set hub.enabled true' and 'scion config set hub.endpoint '") + } PrintUsingHub(hubCtx.Endpoint) @@ -339,6 +341,9 @@ var harnessConfigPullCmd = &cobra.Command{ if err != nil { return err } + if hubCtx == nil { + return fmt.Errorf("Hub integration is not enabled. Configure via 'scion config set hub.enabled true' and 'scion config set hub.endpoint '") + } PrintUsingHub(hubCtx.Endpoint) @@ -414,31 +419,31 @@ var harnessConfigShowCmd = &cobra.Command{ return fmt.Errorf("harness-config %q not found locally and Hub unavailable: %w", name, err) } - ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) - defer cancel() + if hubCtx != nil { + ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) + defer cancel() - resp, err := hubCtx.Client.HarnessConfigs().List(ctx, &hubclient.ListHarnessConfigsOptions{ - Name: name, - Status: "active", - }) - if err != nil { - return fmt.Errorf("failed to search Hub: %w", err) - } - - for _, hc := range resp.HarnessConfigs { - if hc.Name == name || hc.Slug == name { - if isJSONOutput() { - return outputJSON(hc) + resp, err := hubCtx.Client.HarnessConfigs().List(ctx, &hubclient.ListHarnessConfigsOptions{ + Name: name, + Status: "active", + }) + if err == nil { + for _, hc := range resp.HarnessConfigs { + if hc.Name == name || hc.Slug == name { + if isJSONOutput() { + return outputJSON(hc) + } + fmt.Printf("Name: %s\n", hc.Name) + fmt.Printf("Source: hub\n") + fmt.Printf("ID: %s\n", hc.ID) + fmt.Printf("Harness: %s\n", hc.Harness) + fmt.Printf("Status: %s\n", hc.Status) + fmt.Printf("Content Hash: %s\n", truncateHash(hc.ContentHash)) + fmt.Printf("Scope: %s\n", hc.Scope) + fmt.Printf("Files: %d\n", len(hc.Files)) + return nil + } } - fmt.Printf("Name: %s\n", hc.Name) - fmt.Printf("Source: hub\n") - fmt.Printf("ID: %s\n", hc.ID) - fmt.Printf("Harness: %s\n", hc.Harness) - fmt.Printf("Status: %s\n", hc.Status) - fmt.Printf("Content Hash: %s\n", truncateHash(hc.ContentHash)) - fmt.Printf("Scope: %s\n", hc.Scope) - fmt.Printf("Files: %d\n", len(hc.Files)) - return nil } } @@ -467,6 +472,9 @@ var harnessConfigDeleteCmd = &cobra.Command{ if err != nil { return err } + if hubCtx == nil { + return fmt.Errorf("Hub integration is not enabled. Configure via 'scion config set hub.enabled true' and 'scion config set hub.endpoint '") + } PrintUsingHub(hubCtx.Endpoint) diff --git a/cmd/harness_config_test.go b/cmd/harness_config_test.go index 693f65c4e..4e8e22f42 100644 --- a/cmd/harness_config_test.go +++ b/cmd/harness_config_test.go @@ -15,6 +15,7 @@ package cmd import ( + "fmt" "os" "path/filepath" "testing" @@ -34,9 +35,7 @@ func TestHarnessConfigList(t *testing.T) { ) defer restore() - origHome := os.Getenv("HOME") - defer os.Setenv("HOME", origHome) - os.Setenv("HOME", tmpDir) + t.Setenv("HOME", tmpDir) // Seed harness-configs via InitMachine harnesses := harness.All() @@ -71,9 +70,7 @@ func TestHarnessConfigReset(t *testing.T) { ) defer restore() - origHome := os.Getenv("HOME") - defer os.Setenv("HOME", origHome) - os.Setenv("HOME", tmpDir) + t.Setenv("HOME", tmpDir) // Seed harness-configs via InitMachine harnesses := harness.All() @@ -113,3 +110,27 @@ func TestHarnessConfigReset(t *testing.T) { assert.NotEqual(t, "CORRUPTED", string(restoredData)) assert.Equal(t, string(originalData), string(restoredData)) } + +func TestHarnessConfigReset_BundleHarnessReturnsError(t *testing.T) { + tmpDir := t.TempDir() + + t.Setenv("HOME", tmpDir) + + globalDir, err := config.GetGlobalDir() + require.NoError(t, err) + + // Create a harness-config for an opt-in harness (opencode resolves to Generic) + hcDir := filepath.Join(globalDir, "harness-configs", "opencode") + require.NoError(t, os.MkdirAll(hcDir, 0755)) + require.NoError(t, os.WriteFile(filepath.Join(hcDir, "config.yaml"), []byte("harness: opencode\nimage: scion-opencode:latest\nuser: scion\n"), 0644)) + + // harness.New("opencode") returns &Generic{} which has no embeds + h := harness.New("opencode") + _, basePath := h.GetHarnessEmbedsFS() + assert.Equal(t, "", basePath, "opencode should have no embedded defaults") + + // Verify the error message mentions reinstall + err = fmt.Errorf("cannot reset %q: it is installed from a bundle and has no built-in defaults; reinstall with: scion harness-config install harnesses/%s", "opencode", "opencode") + assert.Contains(t, err.Error(), "installed from a bundle") + assert.Contains(t, err.Error(), "harnesses/opencode") +} diff --git a/cmd/hub_secret_migrate.go b/cmd/hub_secret_migrate.go index 20721d762..f2913504d 100644 --- a/cmd/hub_secret_migrate.go +++ b/cmd/hub_secret_migrate.go @@ -23,9 +23,10 @@ import ( "time" "github.com/GoogleCloudPlatform/scion/pkg/config" + "github.com/GoogleCloudPlatform/scion/pkg/ent/entc" "github.com/GoogleCloudPlatform/scion/pkg/secret" "github.com/GoogleCloudPlatform/scion/pkg/store" - "github.com/GoogleCloudPlatform/scion/pkg/store/sqlite" + "github.com/GoogleCloudPlatform/scion/pkg/store/entadapter" "github.com/spf13/cobra" ) @@ -89,11 +90,13 @@ func runSecretMigrate(cmd *cobra.Command, args []string) error { return fmt.Errorf("failed to load config: %w", err) } - // Open database - db, err := sqlite.New(cfg.Database.URL) + // Open database (single Ent-backed store) + entClient, err := entc.OpenSQLite("file:"+cfg.Database.URL+"?cache=shared", entc.PoolConfig{}) if err != nil { return fmt.Errorf("failed to open database: %w", err) } + db := entadapter.NewCompositeStore(entClient) + defer db.Close() if err := db.Migrate(ctx); err != nil { return fmt.Errorf("failed to migrate database: %w", err) } diff --git a/cmd/list.go b/cmd/list.go index 9bb59bb56..27ff5f402 100644 --- a/cmd/list.go +++ b/cmd/list.go @@ -20,6 +20,7 @@ import ( "fmt" "os" "sort" + "strings" "text/tabwriter" "time" @@ -35,18 +36,31 @@ import ( ) var ( - listAll bool - listDeleted bool - listRunning bool - sortByTime bool + listAll bool + listDeleted bool + listRunning bool + sortByTime bool + filterPhase string + filterActivity string + filterTemplate string + sortField string + sortReverse bool ) +var validSortFields = map[string]bool{ + "name": true, "phase": true, "created": true, "updated": true, "last-seen": true, +} + // listCmd represents the list command var listCmd = &cobra.Command{ Use: "list", Aliases: []string{"ls"}, Short: "List running scion agents", RunE: func(cmd *cobra.Command, args []string) error { + if err := validateListFlags(); err != nil { + return err + } + // Check if Hub should be used hubCtx, err := CheckHubAvailability(projectPath) if err != nil { @@ -108,6 +122,7 @@ func listAgentsViaHub(hubCtx *HubContext) error { opts := &hubclient.ListAgentsOptions{ IncludeDeleted: listDeleted, + Phase: filterPhase, } agentSvc := hubCtx.Client.Agents() @@ -269,6 +284,93 @@ func filterRunningAgents(agents []api.AgentInfo) []api.AgentInfo { return filtered } +// validateListFlags checks that filter and sort flag values are valid. +func validateListFlags() error { + if filterPhase != "" { + filterPhase = strings.ToLower(filterPhase) + if !state.Phase(filterPhase).IsValid() { + valid := make([]string, 0, len(state.Phases())) + for _, p := range state.Phases() { + valid = append(valid, string(p)) + } + return fmt.Errorf("invalid phase %q; valid values: %s", filterPhase, strings.Join(valid, ", ")) + } + } + if filterActivity != "" { + filterActivity = strings.ToLower(filterActivity) + if !state.Activity(filterActivity).IsValid() { + valid := make([]string, 0, len(state.Activities())) + for _, a := range state.Activities() { + valid = append(valid, string(a)) + } + return fmt.Errorf("invalid activity %q; valid values: %s", filterActivity, strings.Join(valid, ", ")) + } + } + if sortField != "" { + sortField = strings.ToLower(sortField) + if !validSortFields[sortField] { + valid := make([]string, 0, len(validSortFields)) + for k := range validSortFields { + valid = append(valid, k) + } + sort.Strings(valid) + return fmt.Errorf("invalid sort field %q; valid values: %s", sortField, strings.Join(valid, ", ")) + } + } + return nil +} + +// filterAgentsByFlags applies --phase, --activity, and --template filters. +func filterAgentsByFlags(agents []api.AgentInfo) []api.AgentInfo { + if filterPhase == "" && filterActivity == "" && filterTemplate == "" { + return agents + } + filtered := make([]api.AgentInfo, 0, len(agents)) + for _, a := range agents { + if filterPhase != "" && !strings.EqualFold(a.Phase, filterPhase) { + continue + } + if filterActivity != "" && !strings.EqualFold(a.Activity, filterActivity) { + continue + } + if filterTemplate != "" && !strings.EqualFold(a.Template, filterTemplate) { + continue + } + filtered = append(filtered, a) + } + return filtered +} + +// sortAgentsByField sorts agents by the --sort field. +func sortAgentsByField(agents []api.AgentInfo) { + if sortField == "" { + return + } + sort.SliceStable(agents, func(i, j int) bool { + var less bool + switch sortField { + case "name": + less = strings.ToLower(agents[i].Name) < strings.ToLower(agents[j].Name) + case "phase": + less = agents[i].Phase < agents[j].Phase + case "created": + less = agents[i].Created.Before(agents[j].Created) + case "updated": + less = agents[i].Updated.Before(agents[j].Updated) + case "last-seen": + less = agents[i].LastSeen.Before(agents[j].LastSeen) + default: + return false + } + // Timestamps default to descending (newest first); name/phase default to ascending + descByDefault := sortField == "created" || sortField == "updated" || sortField == "last-seen" + if descByDefault != sortReverse { + return !less + } + return less + }) +} + func displayAgents(agents []api.AgentInfo, all bool, hubMode bool) error { if listRunning { agents = filterRunningAgents(agents) @@ -281,7 +383,12 @@ func displayAgents(agents []api.AgentInfo, all bool, hubMode bool) error { agents[i].Template = config.FriendlyTemplateName(agents[i].Template) } - if sortByTime { + // Apply --phase, --activity, --template filters + agents = filterAgentsByFlags(agents) + + if sortField != "" { + sortAgentsByField(agents) + } else if sortByTime { sort.Slice(agents, func(i, j int) bool { return agents[i].LastSeen.After(agents[j].LastSeen) }) @@ -574,4 +681,9 @@ func init() { listCmd.Flags().BoolVar(&listDeleted, "deleted", false, "Include soft-deleted agents in listing") listCmd.Flags().BoolVarP(&listRunning, "running", "r", false, "Only show agents that are not stopped or errored") listCmd.Flags().BoolVarP(&sortByTime, "time", "t", false, "Sort by last activity, most recent first") + listCmd.Flags().StringVar(&filterPhase, "phase", "", "Filter by lifecycle phase (running, stopped, error, ...)") + listCmd.Flags().StringVar(&filterActivity, "activity", "", "Filter by runtime activity (thinking, waiting_for_input, ...)") + listCmd.Flags().StringVar(&filterTemplate, "template", "", "Filter by template name") + listCmd.Flags().StringVar(&sortField, "sort", "", "Sort by field (name, phase, created, updated, last-seen)") + listCmd.Flags().BoolVar(&sortReverse, "reverse", false, "Reverse sort order") } diff --git a/cmd/list_test.go b/cmd/list_test.go index e28bad216..0c9a89aab 100644 --- a/cmd/list_test.go +++ b/cmd/list_test.go @@ -101,7 +101,7 @@ func TestFormatLastActivity(t *testing.T) { func TestDisplayAgentsLocalMode(t *testing.T) { agents := []api.AgentInfo{ { - Name: "agent-1", + Name: tid("agent-1"), Template: "default", HarnessConfig: "claude", Runtime: "docker", @@ -458,7 +458,7 @@ func TestHubAgentToAgentInfo_PhaseFromStatusFallback(t *testing.T) { func TestHubAgentToAgentInfo_HarnessConfigFromTopLevel(t *testing.T) { // When the Hub returns harnessConfig at the top level, use it directly a := hubclient.Agent{ - ID: "agent-1", + ID: tid("agent-1"), Name: "test-agent", HarnessConfig: "gemini", } @@ -580,3 +580,352 @@ func TestHubAgentToAgentInfo_HarnessConfigTopLevelTakesPrecedence(t *testing.T) t.Errorf("HarnessConfig = %q, want %q (top-level should take precedence)", info.HarnessConfig, "gemini") } } + +func TestFilterAgentsByPhase(t *testing.T) { + agents := []api.AgentInfo{ + {Name: "running-1", Phase: "running", Template: "default", Runtime: "docker", Project: "p"}, + {Name: "stopped-1", Phase: "stopped", Template: "default", Runtime: "docker", Project: "p"}, + {Name: "running-2", Phase: "running", Template: "claude", Runtime: "docker", Project: "p"}, + {Name: "error-1", Phase: "error", Template: "default", Runtime: "docker", Project: "p"}, + } + + filterPhase = "running" + defer func() { filterPhase = "" }() + + old := os.Stdout + r, w, _ := os.Pipe() + os.Stdout = w + + err := displayAgents(agents, false, false) + w.Close() + os.Stdout = old + + if err != nil { + t.Fatalf("displayAgents returned error: %v", err) + } + + var buf bytes.Buffer + buf.ReadFrom(r) + output := buf.String() + + if !strings.Contains(output, "running-1") { + t.Errorf("output should contain 'running-1': %s", output) + } + if !strings.Contains(output, "running-2") { + t.Errorf("output should contain 'running-2': %s", output) + } + if strings.Contains(output, "stopped-1") { + t.Errorf("output should NOT contain 'stopped-1': %s", output) + } + if strings.Contains(output, "error-1") { + t.Errorf("output should NOT contain 'error-1': %s", output) + } +} + +func TestFilterAgentsByActivity(t *testing.T) { + agents := []api.AgentInfo{ + {Name: "thinking-agent", Phase: "running", Activity: "thinking", Template: "default", Runtime: "docker", Project: "p"}, + {Name: "waiting-agent", Phase: "running", Activity: "waiting_for_input", Template: "default", Runtime: "docker", Project: "p"}, + {Name: "no-activity", Phase: "stopped", Template: "default", Runtime: "docker", Project: "p"}, + } + + filterActivity = "thinking" + defer func() { filterActivity = "" }() + + old := os.Stdout + r, w, _ := os.Pipe() + os.Stdout = w + + err := displayAgents(agents, false, false) + w.Close() + os.Stdout = old + + if err != nil { + t.Fatalf("displayAgents returned error: %v", err) + } + + var buf bytes.Buffer + buf.ReadFrom(r) + output := buf.String() + + if !strings.Contains(output, "thinking-agent") { + t.Errorf("output should contain 'thinking-agent': %s", output) + } + if strings.Contains(output, "waiting-agent") { + t.Errorf("output should NOT contain 'waiting-agent': %s", output) + } + if strings.Contains(output, "no-activity") { + t.Errorf("output should NOT contain 'no-activity': %s", output) + } +} + +func TestFilterAgentsByTemplate(t *testing.T) { + agents := []api.AgentInfo{ + {Name: "claude-agent", Phase: "running", Template: "claude", Runtime: "docker", Project: "p"}, + {Name: "gemini-agent", Phase: "running", Template: "gemini", Runtime: "docker", Project: "p"}, + } + + filterTemplate = "claude" + defer func() { filterTemplate = "" }() + + old := os.Stdout + r, w, _ := os.Pipe() + os.Stdout = w + + err := displayAgents(agents, false, false) + w.Close() + os.Stdout = old + + if err != nil { + t.Fatalf("displayAgents returned error: %v", err) + } + + var buf bytes.Buffer + buf.ReadFrom(r) + output := buf.String() + + if !strings.Contains(output, "claude-agent") { + t.Errorf("output should contain 'claude-agent': %s", output) + } + if strings.Contains(output, "gemini-agent") { + t.Errorf("output should NOT contain 'gemini-agent': %s", output) + } +} + +func TestFilterAgentsCombined(t *testing.T) { + agents := []api.AgentInfo{ + {Name: "match", Phase: "running", Activity: "thinking", Template: "claude", Runtime: "docker", Project: "p"}, + {Name: "wrong-phase", Phase: "stopped", Activity: "thinking", Template: "claude", Runtime: "docker", Project: "p"}, + {Name: "wrong-activity", Phase: "running", Activity: "executing", Template: "claude", Runtime: "docker", Project: "p"}, + {Name: "wrong-template", Phase: "running", Activity: "thinking", Template: "gemini", Runtime: "docker", Project: "p"}, + } + + filterPhase = "running" + filterActivity = "thinking" + filterTemplate = "claude" + defer func() { filterPhase = ""; filterActivity = ""; filterTemplate = "" }() + + old := os.Stdout + r, w, _ := os.Pipe() + os.Stdout = w + + err := displayAgents(agents, false, false) + w.Close() + os.Stdout = old + + if err != nil { + t.Fatalf("displayAgents returned error: %v", err) + } + + var buf bytes.Buffer + buf.ReadFrom(r) + output := buf.String() + + lines := strings.Split(strings.TrimSpace(output), "\n") + if len(lines) != 2 { + t.Fatalf("expected 2 lines (header + 1 agent), got %d: %s", len(lines), output) + } + if !strings.Contains(lines[1], "match") { + t.Errorf("only 'match' agent should appear: %s", lines[1]) + } +} + +func TestSortAgentsByName(t *testing.T) { + agents := []api.AgentInfo{ + {Name: "charlie", Template: "default", Runtime: "docker", Project: "p", Phase: "running"}, + {Name: "alice", Template: "default", Runtime: "docker", Project: "p", Phase: "running"}, + {Name: "bob", Template: "default", Runtime: "docker", Project: "p", Phase: "running"}, + } + + sortField = "name" + defer func() { sortField = "" }() + + old := os.Stdout + r, w, _ := os.Pipe() + os.Stdout = w + + err := displayAgents(agents, false, false) + w.Close() + os.Stdout = old + + if err != nil { + t.Fatalf("displayAgents returned error: %v", err) + } + + var buf bytes.Buffer + buf.ReadFrom(r) + output := buf.String() + + lines := strings.Split(strings.TrimSpace(output), "\n") + if len(lines) < 4 { + t.Fatalf("expected 4 lines, got %d: %s", len(lines), output) + } + if !strings.Contains(lines[1], "alice") { + t.Errorf("first agent should be 'alice': %s", lines[1]) + } + if !strings.Contains(lines[2], "bob") { + t.Errorf("second agent should be 'bob': %s", lines[2]) + } + if !strings.Contains(lines[3], "charlie") { + t.Errorf("third agent should be 'charlie': %s", lines[3]) + } +} + +func TestSortAgentsByCreated(t *testing.T) { + now := time.Now() + agents := []api.AgentInfo{ + {Name: "oldest", Template: "default", Runtime: "docker", Project: "p", Phase: "running", Created: now.Add(-3 * time.Hour)}, + {Name: "newest", Template: "default", Runtime: "docker", Project: "p", Phase: "running", Created: now.Add(-1 * time.Hour)}, + {Name: "middle", Template: "default", Runtime: "docker", Project: "p", Phase: "running", Created: now.Add(-2 * time.Hour)}, + } + + sortField = "created" + defer func() { sortField = "" }() + + old := os.Stdout + r, w, _ := os.Pipe() + os.Stdout = w + + err := displayAgents(agents, false, false) + w.Close() + os.Stdout = old + + if err != nil { + t.Fatalf("displayAgents returned error: %v", err) + } + + var buf bytes.Buffer + buf.ReadFrom(r) + output := buf.String() + + lines := strings.Split(strings.TrimSpace(output), "\n") + if len(lines) < 4 { + t.Fatalf("expected 4 lines, got %d: %s", len(lines), output) + } + // Timestamps default to descending (newest first) + if !strings.Contains(lines[1], "newest") { + t.Errorf("first agent should be 'newest': %s", lines[1]) + } + if !strings.Contains(lines[2], "middle") { + t.Errorf("second agent should be 'middle': %s", lines[2]) + } + if !strings.Contains(lines[3], "oldest") { + t.Errorf("third agent should be 'oldest': %s", lines[3]) + } +} + +func TestSortAgentsReverse(t *testing.T) { + now := time.Now() + agents := []api.AgentInfo{ + {Name: "oldest", Template: "default", Runtime: "docker", Project: "p", Phase: "running", Created: now.Add(-3 * time.Hour)}, + {Name: "newest", Template: "default", Runtime: "docker", Project: "p", Phase: "running", Created: now.Add(-1 * time.Hour)}, + {Name: "middle", Template: "default", Runtime: "docker", Project: "p", Phase: "running", Created: now.Add(-2 * time.Hour)}, + } + + sortField = "created" + sortReverse = true + defer func() { sortField = ""; sortReverse = false }() + + old := os.Stdout + r, w, _ := os.Pipe() + os.Stdout = w + + err := displayAgents(agents, false, false) + w.Close() + os.Stdout = old + + if err != nil { + t.Fatalf("displayAgents returned error: %v", err) + } + + var buf bytes.Buffer + buf.ReadFrom(r) + output := buf.String() + + lines := strings.Split(strings.TrimSpace(output), "\n") + if len(lines) < 4 { + t.Fatalf("expected 4 lines, got %d: %s", len(lines), output) + } + // --reverse on timestamp: ascending (oldest first) + if !strings.Contains(lines[1], "oldest") { + t.Errorf("first agent should be 'oldest': %s", lines[1]) + } + if !strings.Contains(lines[2], "middle") { + t.Errorf("second agent should be 'middle': %s", lines[2]) + } + if !strings.Contains(lines[3], "newest") { + t.Errorf("third agent should be 'newest': %s", lines[3]) + } +} + +func TestDisplayAgentsFilteredEmpty(t *testing.T) { + agents := []api.AgentInfo{ + {Name: "running-agent", Phase: "running", Template: "default", Runtime: "docker", Project: "p"}, + } + + filterPhase = "error" + defer func() { filterPhase = "" }() + + old := os.Stdout + r, w, _ := os.Pipe() + os.Stdout = w + + err := displayAgents(agents, false, false) + w.Close() + os.Stdout = old + + if err != nil { + t.Fatalf("displayAgents returned error: %v", err) + } + + var buf bytes.Buffer + buf.ReadFrom(r) + output := buf.String() + + if !strings.Contains(output, "No active agents") { + t.Errorf("expected empty message when filter matches nothing, got: %s", output) + } + if strings.Contains(output, "running-agent") { + t.Errorf("output should NOT contain filtered-out agent: %s", output) + } +} + +func TestValidateListFlags(t *testing.T) { + tests := []struct { + name string + phase string + activity string + sort string + wantErr bool + errContain string + }{ + {"valid phase", "running", "", "", false, ""}, + {"valid activity", "", "thinking", "", false, ""}, + {"valid sort", "", "", "name", false, ""}, + {"invalid phase", "bogus", "", "", true, "invalid phase"}, + {"invalid activity", "", "bogus", "", true, "invalid activity"}, + {"invalid sort", "", "", "bogus", true, "invalid sort field"}, + {"all empty", "", "", "", false, ""}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + filterPhase = tt.phase + filterActivity = tt.activity + sortField = tt.sort + defer func() { filterPhase = ""; filterActivity = ""; sortField = "" }() + + err := validateListFlags() + if tt.wantErr { + if err == nil { + t.Fatalf("expected error, got nil") + } + if !strings.Contains(err.Error(), tt.errContain) { + t.Errorf("error %q should contain %q", err.Error(), tt.errContain) + } + } else if err != nil { + t.Fatalf("unexpected error: %v", err) + } + }) + } +} diff --git a/cmd/message.go b/cmd/message.go index ca802e759..51cb15d13 100644 --- a/cmd/message.go +++ b/cmd/message.go @@ -93,9 +93,7 @@ Examples: } else if strings.HasPrefix(recipient, "user:") { userRecipient = recipient } else if strings.Contains(recipient, "@") && !strings.HasPrefix(recipient, "agent:") { - // Bare email address without user: prefix — return a clear error - // instead of silently converting, which can lead to undelivered messages. - return fmt.Errorf("recipient %q looks like an email address but is missing the \"user:\" prefix.\n\nDid you mean?\n scion message user:%s %q", recipient, recipient, message) + userRecipient = "user:" + recipient } else { // Strip optional "agent:" prefix for backwards compatibility agentName = api.Slugify(strings.TrimPrefix(recipient, "agent:")) diff --git a/cmd/message_test.go b/cmd/message_test.go index 6126a5f4e..7ced8b21e 100644 --- a/cmd/message_test.go +++ b/cmd/message_test.go @@ -197,7 +197,7 @@ func TestSendMessageViaHub_Broadcast(t *testing.T) { projectID := "grove-msg-broadcast" agents := []hubclient.Agent{ - {Name: "agent-1", Status: "running"}, + {Name: tid("agent-1"), Status: "running"}, {Name: "agent-2", Status: "running"}, {Name: "agent-3", Status: "running"}, } @@ -230,7 +230,7 @@ func TestSendMessageViaHub_Broadcast(t *testing.T) { require.NotNil(t, s.StructuredMsg) assert.True(t, s.StructuredMsg.Broadcasted) } - assert.ElementsMatch(t, []string{"agent-1", "agent-2", "agent-3"}, names) + assert.ElementsMatch(t, []string{tid("agent-1"), "agent-2", "agent-3"}, names) } func TestSendMessageViaHub_BroadcastNoAgents(t *testing.T) { @@ -981,29 +981,22 @@ func TestSendMessageViaHub_WakePassedThrough(t *testing.T) { mu.Unlock() } -func TestBareEmailRecipientReturnsError(t *testing.T) { +func TestBareEmailRecipientAutoPrefix(t *testing.T) { tests := []struct { - name string - args []string - wantErr string - wantSuggest string + name string + args []string }{ { - name: "bare email returns error with suggestion", - args: []string{"alice@example.com", "hello"}, - wantErr: `looks like an email address but is missing the "user:" prefix`, - wantSuggest: "user:alice@example.com", + name: "bare email is accepted without user: prefix", + args: []string{"alice@example.com", "hello"}, }, { - name: "bare email with subdomain", - args: []string{"bob@corp.example.com", "check this out"}, - wantErr: `looks like an email address but is missing the "user:" prefix`, - wantSuggest: "user:bob@corp.example.com", + name: "bare email with subdomain is accepted", + args: []string{"bob@corp.example.com", "check this out"}, }, { - name: "user-prefixed email is accepted (no error at parse stage)", - args: []string{"user:alice@example.com", "hello"}, - wantErr: "", // no parse error — fails later for other reasons (Hub, etc.) + name: "user-prefixed email is still accepted", + args: []string{"user:alice@example.com", "hello"}, }, } @@ -1032,19 +1025,12 @@ func TestBareEmailRecipientReturnsError(t *testing.T) { err := messageCmd.RunE(messageCmd, tc.args) - if tc.wantErr == "" { - // We expect no error at the recipient parsing stage. - // The command will still fail (Hub not configured, etc.) - // but NOT with our email-specific error. - if err != nil { - assert.NotContains(t, err.Error(), "looks like an email address") - } - } else { - require.Error(t, err) - assert.Contains(t, err.Error(), tc.wantErr) - if tc.wantSuggest != "" { - assert.Contains(t, err.Error(), tc.wantSuggest) - } + // No error at the recipient parsing stage. + // The command may still fail (Hub not configured, etc.) + // but NOT with an email-specific error. + if err != nil { + assert.NotContains(t, err.Error(), "looks like an email address") + assert.NotContains(t, err.Error(), "missing the \"user:\" prefix") } }) } diff --git a/cmd/project_rename.go b/cmd/project_rename.go new file mode 100644 index 000000000..5be3aa25f --- /dev/null +++ b/cmd/project_rename.go @@ -0,0 +1,116 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package cmd + +import ( + "context" + "fmt" + "time" + + "github.com/GoogleCloudPlatform/scion/pkg/api" + "github.com/GoogleCloudPlatform/scion/pkg/config" + "github.com/GoogleCloudPlatform/scion/pkg/hubclient" + "github.com/spf13/cobra" +) + +var projectRenameCmd = &cobra.Command{ + Use: "rename ", + Short: "Rename a project", + Long: `Rename a project's display name and slug. + +The argument can be a project name, slug, or ID. The +argument becomes both the new display name and the basis for the new slug +(generated by converting the name to a URL-safe form). + +Renaming a project updates its slug, which affects: + - Filesystem paths for hub-managed project workspaces + - Group identifiers (project:slug:agents, project:slug:members) + +Renaming does NOT affect: + - The project's unique ID (agents reference projects by ID) + - Git remote configuration + - Agent records or running agents + +Running agents retain the old slug until restarted. New agents will use the +updated slug immediately. + +Requires Hub connectivity.`, + Args: cobra.ExactArgs(2), + RunE: func(cmd *cobra.Command, args []string) error { + projectRef := args[0] + newName := args[1] + + newSlug := api.Slugify(newName) + if newSlug == "" { + return fmt.Errorf("invalid new name: must contain at least one alphanumeric character") + } + + resolvedPath, _, err := config.ResolveProjectPath(projectPath) + if err != nil { + return fmt.Errorf("failed to resolve project path: %w", err) + } + + settings, err := config.LoadSettings(resolvedPath) + if err != nil { + return fmt.Errorf("failed to load settings: %w", err) + } + + client, err := getHubClient(settings) + if err != nil { + return err + } + + ctx, cancel := context.WithTimeout(cmd.Context(), 30*time.Second) + defer cancel() + + project, err := resolveProjectByNameOrID(ctx, client, projectRef) + if err != nil { + return fmt.Errorf("failed to find project: %w", err) + } + + updated, err := client.Projects().Update(ctx, project.ID, &hubclient.UpdateProjectRequest{ + Name: newName, + Slug: newSlug, + }) + if err != nil { + return fmt.Errorf("failed to rename project: %w", err) + } + + if isJSONOutput() { + return outputJSON(ActionResult{ + Status: "success", + Command: "project rename", + Message: fmt.Sprintf("Project renamed from %q to %q", project.Name, updated.Name), + Details: map[string]interface{}{ + "id": updated.ID, + "name": updated.Name, + "slug": updated.Slug, + "old_name": project.Name, + "old_slug": project.Slug, + }, + }) + } + + fmt.Printf("Project renamed: %s → %s\n", project.Name, updated.Name) + if project.Slug != updated.Slug { + fmt.Printf("Slug updated: %s → %s\n", project.Slug, updated.Slug) + } + return nil + }, +} + +func init() { + projectCmd.AddCommand(projectRenameCmd) +} diff --git a/cmd/reset_auth.go b/cmd/reset_auth.go new file mode 100644 index 000000000..cd31f9ace --- /dev/null +++ b/cmd/reset_auth.go @@ -0,0 +1,79 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package cmd + +import ( + "context" + "fmt" + "time" + + "github.com/GoogleCloudPlatform/scion/pkg/api" + "github.com/spf13/cobra" +) + +var resetAuthCmd = &cobra.Command{ + Use: "reset-auth ", + Short: "Reset authentication for a running agent", + Long: `Inject a fresh Hub token into a running agent without restarting it. + +This is useful when an agent's token has expired and cannot be refreshed +(e.g., after hub signing key rotation). The command generates a new token +on the Hub, pushes it into the agent's container, and signals the agent +to restart its token refresh loop. + +The agent must be running — stopped agents get a fresh token on next start.`, + Args: cobra.ExactArgs(1), + ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { + return getAgentNames(cmd, args, toComplete) + }, + RunE: func(cmd *cobra.Command, args []string) error { + agentName := api.Slugify(args[0]) + + hubCtx, err := CheckHubAvailability(projectPath) + if err != nil { + return err + } + if hubCtx == nil { + return fmt.Errorf("reset-auth requires Hub connectivity (hub not configured)") + } + + return resetAuthViaHub(hubCtx, agentName) + }, +} + +func init() { + rootCmd.AddCommand(resetAuthCmd) +} + +func resetAuthViaHub(hubCtx *HubContext, agentName string) error { + PrintUsingHub(hubCtx.Endpoint) + statusf("Resetting auth for agent '%s'...\n", agentName) + + ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second) + defer cancel() + + projectID, err := GetProjectID(hubCtx) + if err != nil { + return wrapHubError(err) + } + + agentSvc := hubCtx.Client.ProjectAgents(projectID) + if err := agentSvc.ResetAuth(ctx, agentName); err != nil { + return wrapHubError(fmt.Errorf("failed to reset auth via Hub: %w", err)) + } + + statusf("Auth reset dispatched for agent '%s'. The agent will pick up the new token shortly.\n", agentName) + return nil +} diff --git a/cmd/sciontool/commands/doctor.go b/cmd/sciontool/commands/doctor.go new file mode 100644 index 000000000..1e2597a2f --- /dev/null +++ b/cmd/sciontool/commands/doctor.go @@ -0,0 +1,407 @@ +/* +Copyright 2026 The Scion Authors. +*/ + +package commands + +import ( + "bytes" + "encoding/base64" + "encoding/json" + "fmt" + "io" + "net/http" + "os" + "strings" + "time" + + "github.com/spf13/cobra" + + "github.com/GoogleCloudPlatform/scion/pkg/sciontool/hub" +) + +var doctorCmd = &cobra.Command{ + Use: "doctor", + Short: "Diagnose agent health, auth, and hub connectivity", + Long: `Runs a series of diagnostic checks on the agent's environment, +authentication tokens, hub connectivity, and ancillary services. + +Checks performed: + - Environment variables (SCION_HUB_ENDPOINT, SCION_AGENT_ID, etc.) + - Token file presence, format, and expiry + - Hub reachability (unauthenticated health check) + - Token validity (authenticated status update) + - Token refresh capability + - GCP metadata server (if configured) + - GitHub App token (if configured) + +Exit code 0 means all critical checks passed; non-zero means at least one failed.`, + Run: func(cmd *cobra.Command, args []string) { + os.Exit(runDoctor()) + }, +} + +func init() { + rootCmd.AddCommand(doctorCmd) +} + +func runDoctor() int { + failures := 0 + + fmt.Println("=== Scion Agent Doctor ===") + + // --- Environment --- + failures += checkEnvironment() + + // --- Token --- + tokenExpiry, tokenSubject := checkToken() + + // --- Hub Connectivity --- + hubURL := resolveHubURL() + hubReachable := false + if hubURL != "" { + hubReachable = checkHubConnectivity(hubURL) + } + + // --- Authentication --- + tokenValid := false + if hubURL != "" && hubReachable { + tokenValid = checkAuthentication(hubURL, &failures) + } + + // --- GCP Metadata --- + checkGCPMetadata(&failures) + + // --- GitHub Token --- + checkGitHubToken(&failures) + + // --- Remediation --- + printRemediation(tokenExpiry, tokenSubject, tokenValid) + + if failures > 0 { + fmt.Printf("\n[RESULT] %d check(s) FAILED\n", failures) + return 1 + } + fmt.Println("\n[RESULT] All checks passed") + return 0 +} + +func checkEnvironment() int { + failures := 0 + fmt.Println("\n--- Environment ---") + + envVars := []struct { + name string + required bool + fallback string + }{ + {"SCION_HUB_ENDPOINT", true, "SCION_HUB_URL"}, + {"SCION_AGENT_ID", true, ""}, + {"SCION_AGENT_MODE", false, ""}, + } + + for _, ev := range envVars { + val := os.Getenv(ev.name) + if val == "" && ev.fallback != "" { + val = os.Getenv(ev.fallback) + if val != "" { + fmt.Printf("[ OK ] %s = %s (via %s)\n", ev.name, val, ev.fallback) + continue + } + } + if val == "" { + if ev.required { + fmt.Printf("[FAIL] %s is not set\n", ev.name) + failures++ + } else { + fmt.Printf("[INFO] %s is not set\n", ev.name) + } + } else { + fmt.Printf("[ OK ] %s = %s\n", ev.name, val) + } + } + + mode := hub.OperatingMode() + fmt.Printf("[INFO] Operating mode: %s\n", mode) + + return failures +} + +// checkToken inspects the token file and returns (expiry, subject). +// Expiry is zero-value if the token can't be parsed. +func checkToken() (time.Time, string) { + fmt.Println("\n--- Token ---") + + tokenPath := hub.TokenFilePath() + data, err := os.ReadFile(tokenPath) + if err != nil { + fmt.Printf("[FAIL] Token file not found: %s\n", tokenPath) + return time.Time{}, "" + } + + token := strings.TrimSpace(string(data)) + if token == "" { + fmt.Printf("[FAIL] Token file is empty: %s\n", tokenPath) + return time.Time{}, "" + } + + fmt.Printf("[ OK ] Token file: %s (%d bytes)\n", tokenPath, len(token)) + + // Parse JWT claims + claims, err := parseJWTClaims(token) + if err != nil { + fmt.Printf("[WARN] Cannot parse token as JWT: %v\n", err) + return time.Time{}, "" + } + + subject, _ := claims["sub"].(string) + if subject != "" { + fmt.Printf("[INFO] Subject: %s\n", subject) + } + + if iat, ok := claims["iat"].(float64); ok { + issuedAt := time.Unix(int64(iat), 0) + fmt.Printf("[INFO] Issued: %s\n", issuedAt.Format(time.RFC3339)) + } + + exp, ok := claims["exp"].(float64) + if !ok { + fmt.Println("[WARN] Token has no expiry claim") + return time.Time{}, subject + } + + expiry := time.Unix(int64(exp), 0) + now := time.Now() + + if now.After(expiry) { + since := now.Sub(expiry).Truncate(time.Second) + fmt.Printf("[FAIL] Token EXPIRED at %s (%s ago)\n", expiry.Format(time.RFC3339), since) + } else { + until := expiry.Sub(now).Truncate(time.Second) + refreshWindow := expiry.Add(-2 * time.Hour) + if now.After(refreshWindow) { + fmt.Printf("[WARN] Token expires at %s (in %s, within refresh window)\n", expiry.Format(time.RFC3339), until) + } else { + fmt.Printf("[ OK ] Token expires at %s (in %s)\n", expiry.Format(time.RFC3339), until) + } + } + + return expiry, subject +} + +func resolveHubURL() string { + hubURL := os.Getenv("SCION_HUB_ENDPOINT") + if hubURL == "" { + hubURL = os.Getenv("SCION_HUB_URL") + } + return hubURL +} + +func checkHubConnectivity(hubURL string) bool { + fmt.Println("\n--- Hub Connectivity ---") + + client := &http.Client{Timeout: 5 * time.Second} + healthURL := strings.TrimSuffix(hubURL, "/") + "/healthz" + + resp, err := client.Get(healthURL) + if err != nil { + fmt.Printf("[FAIL] Hub unreachable at %s: %v\n", hubURL, err) + return false + } + resp.Body.Close() + + if resp.StatusCode < 400 { + fmt.Printf("[ OK ] Hub reachable at %s\n", hubURL) + return true + } + + fmt.Printf("[WARN] Hub returned %d at %s\n", resp.StatusCode, healthURL) + return true +} + +func checkAuthentication(hubURL string, failures *int) bool { + fmt.Println("\n--- Authentication ---") + + agentID := os.Getenv("SCION_AGENT_ID") + token := hub.ReadTokenFile() + + if token == "" || agentID == "" { + fmt.Println("[FAIL] Cannot test auth: missing token or agent ID") + *failures++ + return false + } + + client := &http.Client{Timeout: 5 * time.Second} + + // Test with a heartbeat (least disruptive authenticated call) + statusURL := fmt.Sprintf("%s/api/v1/agents/%s/status", + strings.TrimSuffix(hubURL, "/"), agentID) + body, _ := json.Marshal(map[string]interface{}{ + "heartbeat": true, + }) + + req, _ := http.NewRequest("POST", statusURL, bytes.NewReader(body)) + req.Header.Set("Content-Type", "application/json") + req.Header.Set("X-Scion-Agent-Token", token) + + resp, err := client.Do(req) + if err != nil { + fmt.Printf("[FAIL] Auth check failed: %v\n", err) + *failures++ + return false + } + respBody, _ := io.ReadAll(resp.Body) + resp.Body.Close() + + if resp.StatusCode < 400 { + fmt.Println("[ OK ] Authenticated successfully (heartbeat accepted)") + } else if resp.StatusCode == 401 || resp.StatusCode == 403 { + fmt.Printf("[FAIL] Token rejected by hub (%d): %s\n", resp.StatusCode, doctorTruncate(string(respBody), 120)) + *failures++ + } else { + fmt.Printf("[WARN] Hub returned %d: %s\n", resp.StatusCode, doctorTruncate(string(respBody), 120)) + } + + // Test token refresh + refreshURL := fmt.Sprintf("%s/api/v1/agents/%s/token/refresh", + strings.TrimSuffix(hubURL, "/"), agentID) + + req, _ = http.NewRequest("POST", refreshURL, nil) + req.Header.Set("X-Scion-Agent-Token", token) + + resp, err = client.Do(req) + if err != nil { + fmt.Printf("[FAIL] Token refresh check failed: %v\n", err) + *failures++ + return false + } + respBody, _ = io.ReadAll(resp.Body) + resp.Body.Close() + + if resp.StatusCode == 200 { + fmt.Println("[ OK ] Token refresh works") + return true + } else if resp.StatusCode == 401 || resp.StatusCode == 403 { + fmt.Printf("[FAIL] Token refresh rejected (%d): %s\n", resp.StatusCode, doctorTruncate(string(respBody), 120)) + *failures++ + return false + } + fmt.Printf("[WARN] Token refresh returned %d: %s\n", resp.StatusCode, doctorTruncate(string(respBody), 120)) + return false +} + +func checkGCPMetadata(failures *int) { + mode := os.Getenv("SCION_METADATA_MODE") + if mode == "" { + return + } + + fmt.Println("\n--- GCP Metadata ---") + + port := 18380 + if p := os.Getenv("SCION_METADATA_PORT"); p != "" { + fmt.Sscanf(p, "%d", &port) + } + + client := &http.Client{Timeout: 2 * time.Second} + addr := fmt.Sprintf("http://127.0.0.1:%d/", port) + + resp, err := client.Get(addr) + if err != nil { + fmt.Printf("[FAIL] Metadata server unreachable at %s: %v\n", addr, err) + *failures++ + return + } + resp.Body.Close() + + if resp.StatusCode == http.StatusOK { + fmt.Printf("[ OK ] Metadata server healthy at %s (mode=%s)\n", addr, mode) + } else { + fmt.Printf("[FAIL] Metadata server returned %d\n", resp.StatusCode) + *failures++ + } +} + +func checkGitHubToken(failures *int) { + if os.Getenv("SCION_GITHUB_APP_ENABLED") != "true" { + return + } + + fmt.Println("\n--- GitHub Token ---") + + tokenPath := hub.GitHubTokenPath() + token := hub.ReadGitHubTokenFile(tokenPath) + if token == "" { + fmt.Printf("[FAIL] GitHub token file missing or empty: %s\n", tokenPath) + *failures++ + return + } + fmt.Printf("[ OK ] GitHub token file present: %s\n", tokenPath) + + if hub.IsGitHubTokenExpired(tokenPath) { + expiry, err := hub.ReadGitHubTokenExpiry(tokenPath) + if err != nil { + fmt.Println("[FAIL] GitHub token expired (expiry file unreadable)") + } else { + fmt.Printf("[FAIL] GitHub token expired at %s\n", expiry.Format(time.RFC3339)) + } + *failures++ + } else { + expiry, err := hub.ReadGitHubTokenExpiry(tokenPath) + if err != nil { + fmt.Println("[ OK ] GitHub token present (expiry unknown)") + } else { + fmt.Printf("[ OK ] GitHub token valid until %s\n", expiry.Format(time.RFC3339)) + } + } +} + +func printRemediation(tokenExpiry time.Time, tokenSubject string, tokenValid bool) { + now := time.Now() + + // Only print remediation if there's a problem + expired := !tokenExpiry.IsZero() && now.After(tokenExpiry) + if !expired && tokenValid { + return + } + + fmt.Println("\n--- Remediation ---") + + if expired && !tokenValid { + fmt.Println("[!] Token is expired and cannot be refreshed.") + fmt.Println("[!] Run from the host: scion agent reset-auth ") + fmt.Println("[!] Or restart agent: scion agent restart ") + } else if !tokenValid { + fmt.Println("[!] Token is rejected by the hub (signing key may have changed).") + fmt.Println("[!] Run from the host: scion agent reset-auth ") + fmt.Println("[!] Or restart agent: scion agent restart ") + } +} + +// parseJWTClaims extracts claims from a JWT without validating the signature. +func parseJWTClaims(token string) (map[string]interface{}, error) { + parts := strings.Split(token, ".") + if len(parts) != 3 { + return nil, fmt.Errorf("invalid JWT format: expected 3 parts, got %d", len(parts)) + } + + payload, err := base64.RawURLEncoding.DecodeString(parts[1]) + if err != nil { + return nil, fmt.Errorf("failed to decode JWT payload: %w", err) + } + + var claims map[string]interface{} + if err := json.Unmarshal(payload, &claims); err != nil { + return nil, fmt.Errorf("failed to parse JWT claims: %w", err) + } + + return claims, nil +} + +func doctorTruncate(s string, maxLen int) string { + s = strings.TrimSpace(s) + if len(s) <= maxLen { + return s + } + return s[:maxLen] + "..." +} diff --git a/cmd/sciontool/commands/hook_test.go b/cmd/sciontool/commands/hook_test.go index cfe42ed71..bd402d596 100644 --- a/cmd/sciontool/commands/hook_test.go +++ b/cmd/sciontool/commands/hook_test.go @@ -15,12 +15,35 @@ import ( "github.com/stretchr/testify/require" ) +// scrubScionEnv clears all Hub and telemetry environment variables for the +// duration of the test, preventing accidental communication with a real Hub +// or telemetry backend when tests run inside an agent container. +func scrubScionEnv(t *testing.T) { + t.Helper() + for _, key := range []string{ + "SCION_HUB_ENDPOINT", + "SCION_HUB_URL", + "SCION_AUTH_TOKEN", + "SCION_AGENT_ID", + "SCION_AGENT_MODE", + "SCION_TELEMETRY_ENABLED", + "SCION_TELEMETRY_CLOUD_ENABLED", + "SCION_OTEL_ENDPOINT", + "SCION_OTEL_GCP_CREDENTIALS", + "SCION_GCP_PROJECT_ID", + "OTEL_EXPORTER_OTLP_ENDPOINT", + } { + t.Setenv(key, "") + } +} + func TestProcessHookData_Claude(t *testing.T) { // Set up temp home directory for status/log files tmpDir := t.TempDir() oldHome := os.Getenv("HOME") os.Setenv("HOME", tmpDir) defer os.Setenv("HOME", oldHome) + scrubScionEnv(t) log.SetLogPath(filepath.Join(tmpDir, "agent.log")) hookDialect = "claude" @@ -58,6 +81,7 @@ func TestProcessHookData_Gemini(t *testing.T) { oldHome := os.Getenv("HOME") os.Setenv("HOME", tmpDir) defer os.Setenv("HOME", oldHome) + scrubScionEnv(t) log.SetLogPath(filepath.Join(tmpDir, "agent.log")) hookDialect = "gemini" @@ -88,6 +112,7 @@ func TestProcessHookData_SessionEvents(t *testing.T) { oldHome := os.Getenv("HOME") os.Setenv("HOME", tmpDir) defer os.Setenv("HOME", oldHome) + scrubScionEnv(t) log.SetLogPath(filepath.Join(tmpDir, "agent.log")) hookDialect = "claude" @@ -130,6 +155,7 @@ func TestProcessHookData_CodexCompletion(t *testing.T) { oldHome := os.Getenv("HOME") os.Setenv("HOME", tmpDir) defer os.Setenv("HOME", oldHome) + scrubScionEnv(t) log.SetLogPath(filepath.Join(tmpDir, "agent.log")) hookDialect = "codex" diff --git a/cmd/sciontool/commands/init.go b/cmd/sciontool/commands/init.go index 8e620cca3..97f5b1357 100644 --- a/cmd/sciontool/commands/init.go +++ b/cmd/sciontool/commands/init.go @@ -429,7 +429,8 @@ func runInit(args []string) int { }{code, err} }() - // Heartbeat and token refresh control variables - declared here so they're accessible during shutdown + // Heartbeat and token refresh control variables - declared here so they're accessible during shutdown and auth reset + var hubClient *hub.Client var heartbeatCancel context.CancelFunc var heartbeatDone <-chan struct{} var tokenRefreshCancel context.CancelFunc @@ -458,7 +459,7 @@ func runInit(args []string) int { } // Report running status to Hub if in hosted mode - hubClient := hub.NewClient() + hubClient = hub.NewClient() log.Debug("Hub client check: client=%v, configured=%v", hubClient != nil, hubClient != nil && hubClient.IsConfigured()) log.Debug("Hub env: SCION_HUB_ENDPOINT=%q, SCION_HUB_URL=%q, token_file=%v, SCION_AGENT_ID=%q", os.Getenv("SCION_HUB_ENDPOINT"), os.Getenv("SCION_HUB_URL"), hub.ReadTokenFile() != "", os.Getenv("SCION_AGENT_ID")) @@ -626,6 +627,7 @@ func runInit(args []string) int { ChownGID: targetGID, OnRefreshed: func(newToken string, newExpiry time.Time) { log.Info("GitHub token refreshed, new expiry: %s", newExpiry.Format(time.RFC3339)) + writeEnvFile(agentHome, targetUID, targetGID) }, OnError: func(err error) { log.Error("GitHub token refresh failed: %v", err) @@ -645,6 +647,13 @@ func runInit(args []string) int { signal.Notify(usr1Chan, syscall.SIGUSR1) defer signal.Stop(usr1Chan) + // Set up SIGUSR2 handler for auth reset. When the broker writes a fresh + // token to ~/.scion/scion-token and sends SIGUSR2, init re-reads the + // token, updates the hub client, and restarts the token refresh loop. + usr2Chan := make(chan os.Signal, 1) + signal.Notify(usr2Chan, syscall.SIGUSR2) + defer signal.Stop(usr2Chan) + // Set up duration timer if max_duration is configured var durationTimer <-chan time.Time maxDurStr := os.Getenv("SCION_MAX_DURATION") @@ -688,37 +697,48 @@ func runInit(args []string) int { go watchLimitsTriggerFile(triggerCtx, triggerChan) } - // Wait for child to exit, duration limit, SIGUSR1, or trigger file + // Wait for child to exit, duration limit, SIGUSR1, SIGUSR2, or trigger file. + // The loop allows SIGUSR2 (auth reset) to be handled without terminating. var result struct { code int err error } limitsExceeded := false - select { - case r := <-exitChan: - result = r - case <-durationTimer: - limitsExceeded = true - handleLimitsExceeded(sup, "duration", fmt.Sprintf("max_duration of %s exceeded", maxDurStr)) - result = <-exitChan - case <-usr1Chan: - // SIGUSR1 received from hook handler - limits already set in agent-info.json - limitsExceeded = true - log.TaggedInfo("LIMITS_EXCEEDED", "Received SIGUSR1: limit exceeded, initiating shutdown") - // Initiate graceful shutdown of the child process - if err := sup.Signal(syscall.SIGTERM); err != nil { - log.Error("Failed to send SIGTERM to child: %v", err) - } - result = <-exitChan - case <-triggerChan: - // Trigger file detected from hook handler - limits already set in agent-info.json - limitsExceeded = true - log.TaggedInfo("LIMITS_EXCEEDED", "Trigger file detected: limit exceeded, initiating shutdown") - if err := sup.Signal(syscall.SIGTERM); err != nil { - log.Error("Failed to send SIGTERM to child: %v", err) +waitLoop: + for { + select { + case r := <-exitChan: + result = r + break waitLoop + case <-durationTimer: + limitsExceeded = true + handleLimitsExceeded(sup, "duration", fmt.Sprintf("max_duration of %s exceeded", maxDurStr)) + result = <-exitChan + break waitLoop + case <-usr1Chan: + // SIGUSR1 received from hook handler - limits already set in agent-info.json + limitsExceeded = true + log.TaggedInfo("LIMITS_EXCEEDED", "Received SIGUSR1: limit exceeded, initiating shutdown") + if err := sup.Signal(syscall.SIGTERM); err != nil { + log.Error("Failed to send SIGTERM to child: %v", err) + } + result = <-exitChan + break waitLoop + case <-usr2Chan: + // SIGUSR2: auth reset — re-read token file and restart refresh loop. + handleAuthReset(hubClient, &tokenRefreshCancel, &tokenRefreshDone, statusHandler, targetUID, targetGID) + // Continue waiting — this is non-terminal. + case <-triggerChan: + // Trigger file detected from hook handler - limits already set in agent-info.json + limitsExceeded = true + log.TaggedInfo("LIMITS_EXCEEDED", "Trigger file detected: limit exceeded, initiating shutdown") + if err := sup.Signal(syscall.SIGTERM); err != nil { + log.Error("Failed to send SIGTERM to child: %v", err) + } + result = <-exitChan + break waitLoop } - result = <-exitChan } // Stop token refresh loops and heartbeat before reporting shutdown status to prevent races @@ -889,6 +909,93 @@ func handleLimitsExceeded(sup *supervisor.Supervisor, limitType, message string) } } +// handleAuthReset re-reads the token file, updates the hub client, and +// restarts the token refresh loop. Called when SIGUSR2 is received from the +// broker's reset-auth handler. +func handleAuthReset(hubClient *hub.Client, tokenRefreshCancel *context.CancelFunc, tokenRefreshDone *<-chan struct{}, statusHandler *handlers.StatusHandler, targetUID, targetGID int) { + log.TaggedInfo("AUTH_RESET", "Received SIGUSR2: auth reset requested") + + if hubClient == nil { + log.Error("AUTH_RESET: Hub client is not configured, cannot reset auth") + return + } + + newToken := hub.ReadTokenFile() + if newToken == "" { + log.Error("AUTH_RESET: Token file is empty after SIGUSR2, cannot reset auth") + return + } + + tokenExpiry, err := hub.ParseTokenExpiry(newToken) + if err != nil { + log.Error("AUTH_RESET: Cannot parse new token expiry: %v", err) + return + } + + // Cancel the existing token refresh loop if running. + if *tokenRefreshCancel != nil { + (*tokenRefreshCancel)() + if *tokenRefreshDone != nil { + <-*tokenRefreshDone + } + } + + // Update the hub client's in-memory token. + if hubClient != nil { + hubClient.SetToken(newToken) + } + + // Clear any AUTH_LOST message from agent-info.json. + statusHandler.SetMessage("") + + // Schedule refresh 2 hours before the new token's expiry. + refreshAt := tokenExpiry.Add(-2 * time.Hour) + if refreshAt.Before(time.Now()) { + if time.Now().Before(tokenExpiry) { + refreshAt = time.Now().Add(1 * time.Minute) + } else { + log.Error("AUTH_RESET: New token is already expired at %s", tokenExpiry.Format(time.RFC3339)) + return + } + } + + // Start a new token refresh loop. + var tokenRefreshCtx context.Context + var cancel context.CancelFunc + tokenRefreshCtx, cancel = context.WithCancel(context.Background()) + *tokenRefreshCancel = cancel + *tokenRefreshDone = hubClient.StartTokenRefresh(tokenRefreshCtx, &hub.TokenRefreshConfig{ + RefreshAt: refreshAt, + ChownUID: targetUID, + ChownGID: targetGID, + OnRefreshed: func(newExpiry time.Time) { + log.Info("Token refreshed successfully, new expiry: %s", newExpiry.Format(time.RFC3339)) + }, + OnError: func(err error) { + log.Error("Token refresh failed: %v", err) + }, + OnAuthLost: func() { + log.Error("AUTH_LOST: Agent token has expired and could not be refreshed - hub communication is no longer possible") + log.Error("AUTH_LOST: Agent limits (max-duration, max-turns, max-model-calls) are enforced locally and remain active") + statusHandler.SetMessage("AUTH_LOST: Hub token expired and could not be refreshed") + }, + }) + + log.TaggedInfo("AUTH_RESET", "Auth reset complete — new token expires %s, refresh at %s", + tokenExpiry.Format(time.RFC3339), refreshAt.Format(time.RFC3339)) + + // Send an immediate heartbeat with the new token. + if hubClient != nil && hubClient.IsConfigured() { + hubCtx, hubCancel := context.WithTimeout(context.Background(), 5*time.Second) + if err := hubClient.Heartbeat(hubCtx); err != nil { + log.Error("AUTH_RESET: Post-reset heartbeat failed: %v", err) + } else { + log.Info("AUTH_RESET: Post-reset heartbeat sent successfully") + } + hubCancel() + } +} + // extractChildCommand extracts the command arguments. // Cobra handles -- separator, so args contains everything after --. func extractChildCommand(args []string) []string { @@ -1624,8 +1731,14 @@ func writeEnvFile(agentHome string, uid, gid int) { } envPath := filepath.Join(scionDir, "scion-env") - if err := os.WriteFile(envPath, []byte(strings.Join(lines, "\n")+"\n"), 0644); err != nil { - log.Error("Failed to write scion-env file: %v", err) + tmpPath := envPath + ".tmp" + if err := os.WriteFile(tmpPath, []byte(strings.Join(lines, "\n")+"\n"), 0644); err != nil { + log.Error("Failed to write temporary scion-env file: %v", err) + return + } + if err := os.Rename(tmpPath, envPath); err != nil { + log.Error("Failed to atomically rename scion-env file: %v", err) + os.Remove(tmpPath) return } diff --git a/cmd/sciontool/commands/init_test.go b/cmd/sciontool/commands/init_test.go index e24da9ac3..14f1a4aab 100644 --- a/cmd/sciontool/commands/init_test.go +++ b/cmd/sciontool/commands/init_test.go @@ -15,6 +15,41 @@ import ( "testing" ) +// hubEnvVars lists the environment variables used by the Hub client. +// Leaking these to a subprocess (e.g., sciontool init) causes the child +// to talk to the real Hub and corrupt agent state. See issue #123. +var hubEnvVars = []string{ + "SCION_HUB_ENDPOINT", + "SCION_HUB_URL", + "SCION_AUTH_TOKEN", + "SCION_AGENT_ID", + "SCION_AGENT_MODE", +} + +// scrubHubEnv clears all Hub-related environment variables for the +// duration of the test, preventing accidental communication with a +// real Hub when tests run inside an agent container. +func scrubHubEnv(t *testing.T) { + t.Helper() + for _, key := range hubEnvVars { + t.Setenv(key, "") + } +} + +// filterHubEnv returns a copy of the environment with all Hub-related +// variables removed. Use when constructing exec.Cmd.Env to prevent +// credential leakage to child processes. +func filterHubEnv(env []string) []string { + var filtered []string + for _, e := range env { + key, _, _ := strings.Cut(e, "=") + if !slices.Contains(hubEnvVars, key) { + filtered = append(filtered, e) + } + } + return filtered +} + // TestInitProjectDataIsolation is a canary test that verifies sciontool source code // does NOT import the pkg/config package, which contains project path resolution logic. // This is a compile-time guarantee that in-container code cannot access project data paths. @@ -135,14 +170,21 @@ func TestInitCommand_Integration(t *testing.T) { t.Skip("skipping integration test in short mode") } + // Clear Hub env vars so the subprocess cannot talk to the real Hub + // and corrupt agent state. See issue #123. + scrubHubEnv(t) + // Build sciontool if needed for integration testing - cmd := exec.Command("go", "build", "-buildvcs=false", "-o", "/tmp/sciontool-test", "../") + binPath := filepath.Join(t.TempDir(), "sciontool-test") + cmd := exec.Command("go", "build", "-buildvcs=false", "-o", binPath, "../") if err := cmd.Run(); err != nil { t.Skipf("failed to build sciontool for integration test: %v", err) } - // Test running a simple command - testCmd := exec.Command("/tmp/sciontool-test", "init", "--", "echo", "hello") + // Test running a simple command — filter Hub env vars from the + // subprocess environment as belt-and-suspenders protection. + testCmd := exec.Command(binPath, "init", "--", "echo", "hello") + testCmd.Env = filterHubEnv(os.Environ()) output, err := testCmd.CombinedOutput() if err != nil { t.Errorf("init command failed: %v\nOutput: %s", err, output) @@ -692,6 +734,41 @@ func TestWriteEnvFile_IncludesGitHubToken(t *testing.T) { } } +func TestWriteEnvFile_ReflectsUpdatedGitHubToken(t *testing.T) { + tmpHome := t.TempDir() + + t.Setenv("GITHUB_TOKEN", "ghs_initial_token_abc123") + + writeEnvFile(tmpHome, 0, 0) + + envPath := filepath.Join(tmpHome, ".scion", "scion-env") + data, err := os.ReadFile(envPath) + if err != nil { + t.Fatalf("failed to read scion-env file: %v", err) + } + if !strings.Contains(string(data), `export GITHUB_TOKEN="ghs_initial_token_abc123"`) { + t.Fatalf("expected initial GITHUB_TOKEN in env file, got:\n%s", string(data)) + } + + // Simulate what StartGitHubTokenRefresh does: os.Setenv then OnRefreshed calls writeEnvFile + t.Setenv("GITHUB_TOKEN", "ghs_refreshed_token_xyz789") + + writeEnvFile(tmpHome, 0, 0) + + data, err = os.ReadFile(envPath) + if err != nil { + t.Fatalf("failed to read scion-env file after refresh: %v", err) + } + content := string(data) + + if !strings.Contains(content, `export GITHUB_TOKEN="ghs_refreshed_token_xyz789"`) { + t.Errorf("expected refreshed GITHUB_TOKEN in env file, got:\n%s", content) + } + if strings.Contains(content, "ghs_initial_token_abc123") { + t.Errorf("stale initial token should not appear in env file after refresh") + } +} + func TestGitCloneWorkspace_DefaultEnvValues(t *testing.T) { // Set SCION_GIT_CLONE_URL to trigger the clone path, but use a URL // that will cause a predictable early failure (non-existent host). diff --git a/cmd/sciontool/commands/provision.go b/cmd/sciontool/commands/provision.go new file mode 100644 index 000000000..a15c9db8c --- /dev/null +++ b/cmd/sciontool/commands/provision.go @@ -0,0 +1,146 @@ +/* +Copyright 2026 The Scion Authors. +*/ +package commands + +import ( + "context" + "fmt" + "os" + "path/filepath" + "time" + + "github.com/GoogleCloudPlatform/scion/pkg/api" + "github.com/GoogleCloudPlatform/scion/pkg/provision" + "github.com/GoogleCloudPlatform/scion/pkg/sciontool/log" + "github.com/GoogleCloudPlatform/scion/pkg/store" + "github.com/spf13/cobra" +) + +var ( + provisionWorkspace string + provisionMode string + provisionDepth int + provisionUID int + provisionGID int + provisionWaitSentinel bool + provisionTimeout int + provisionPollInterval int +) + +var provisionCmd = &cobra.Command{ + Use: "provision", + Short: "Provision an NFS workspace (clone or wait for sentinel)", + Long: `Provision a shared workspace in an NFS-backed init container. + +In default (clone) mode, reads SCION_CLONE_URL and SCION_CLONE_BRANCH from +the environment and invokes the shared provisioning function. The sentinel +file (.scion-provisioned) is placed inside the workspace directory itself +because the init container's PVC subPath mount only exposes the workspace +dir, not its parent. + +In --wait-for-sentinel mode, polls for the sentinel file written by the +winning node's init container and exits 0 when found or non-zero on timeout. + +URL and branch are ALWAYS read from environment variables (never from flags) +to prevent shell injection via crafted values.`, + SilenceErrors: true, + SilenceUsage: true, + RunE: func(cmd *cobra.Command, args []string) error { + if provisionWaitSentinel { + return runWaitForSentinel(cmd.Context()) + } + return runProvision(cmd.Context()) + }, +} + +func init() { + rootCmd.AddCommand(provisionCmd) + + provisionCmd.Flags().StringVar(&provisionWorkspace, "workspace", "/workspace", + "Path to the workspace directory") + provisionCmd.Flags().StringVar(&provisionMode, "mode", "shared-plain", + "Workspace sharing mode (shared-plain, worktree-per-agent)") + provisionCmd.Flags().IntVar(&provisionDepth, "depth", 1, + "Git clone depth (1=shallow, 0=full, -1=no depth flag)") + provisionCmd.Flags().IntVar(&provisionUID, "uid", 1000, + "UID for chown of provisioned files") + provisionCmd.Flags().IntVar(&provisionGID, "gid", 1000, + "GID for chown of provisioned files") + provisionCmd.Flags().BoolVar(&provisionWaitSentinel, "wait-for-sentinel", false, + "Poll for sentinel file instead of provisioning (lock-loser mode)") + provisionCmd.Flags().IntVar(&provisionTimeout, "timeout", 300, + "Timeout in seconds for --wait-for-sentinel mode") + provisionCmd.Flags().IntVar(&provisionPollInterval, "poll-interval", 2, + "Poll interval in seconds for --wait-for-sentinel mode") +} + +func runProvision(ctx context.Context) error { + cloneURL := os.Getenv("SCION_CLONE_URL") + cloneBranch := os.Getenv("SCION_CLONE_BRANCH") + projectID := os.Getenv("SCION_PROJECT_ID") + if projectID == "" { + projectID = "unknown" + } + + var gc *api.GitCloneConfig + if cloneURL != "" { + gc = &api.GitCloneConfig{ + URL: cloneURL, + Branch: cloneBranch, + Depth: provisionDepth, + } + } + + mode := store.ResolveWorkspaceSharingMode(provisionMode) + + in := provision.ProvisionInput{ + Ctx: ctx, + Resolved: provision.ResolvedWorkspace{ + HostPath: provisionWorkspace, + }, + ProjectID: projectID, + Mode: mode, + GitClone: gc, + Locker: nil, // no advisory locker in init container + NFSUID: provisionUID, + NFSGID: provisionGID, + SentinelDir: provisionWorkspace, + } + + log.Info("Provisioning workspace at %s (mode=%s, project=%s)", provisionWorkspace, mode, projectID) + if err := provision.ProvisionShared(in); err != nil { + return fmt.Errorf("provision failed: %w", err) + } + log.Info("Workspace provisioned successfully") + return nil +} + +func runWaitForSentinel(ctx context.Context) error { + sentinelPath := filepath.Join(provisionWorkspace, provision.ProvisionSentinelFile) + timeout := time.Duration(provisionTimeout) * time.Second + interval := time.Duration(provisionPollInterval) * time.Second + start := time.Now() + deadline := start.Add(timeout) + + log.Info("Waiting for sentinel %s (timeout=%s, interval=%s)", sentinelPath, timeout, interval) + + for { + if _, err := os.Stat(sentinelPath); err == nil { + log.Info("Sentinel found after %s", time.Since(start).Truncate(time.Second)) + return nil + } + + if time.Now().After(deadline) { + return fmt.Errorf("timed out waiting for sentinel %s after %s", sentinelPath, timeout) + } + + // Sleep for the poll interval, but wake immediately on cancellation + // (SIGTERM/SIGINT) so the init container exits promptly. + select { + case <-ctx.Done(): + return fmt.Errorf("cancelled while waiting for sentinel %s: %w", sentinelPath, ctx.Err()) + case <-time.After(interval): + } + } +} diff --git a/cmd/sciontool/commands/provision_test.go b/cmd/sciontool/commands/provision_test.go new file mode 100644 index 000000000..2ff3c6df1 --- /dev/null +++ b/cmd/sciontool/commands/provision_test.go @@ -0,0 +1,217 @@ +/* +Copyright 2026 The Scion Authors. +*/ +package commands + +import ( + "context" + "os" + "path/filepath" + "testing" + "time" +) + +func TestProvisionCmd_WaitForSentinel_Found(t *testing.T) { + dir := t.TempDir() + + sentinelPath := filepath.Join(dir, ".scion-provisioned") + if err := os.WriteFile(sentinelPath, []byte("provisioned_at=test\n"), 0644); err != nil { + t.Fatal(err) + } + + oldWorkspace := provisionWorkspace + oldWait := provisionWaitSentinel + oldTimeout := provisionTimeout + oldInterval := provisionPollInterval + defer func() { + provisionWorkspace = oldWorkspace + provisionWaitSentinel = oldWait + provisionTimeout = oldTimeout + provisionPollInterval = oldInterval + }() + + provisionWorkspace = dir + provisionWaitSentinel = true + provisionTimeout = 5 + provisionPollInterval = 1 + + if err := runWaitForSentinel(context.Background()); err != nil { + t.Fatalf("expected success when sentinel exists, got: %v", err) + } +} + +func TestProvisionCmd_WaitForSentinel_Timeout(t *testing.T) { + dir := t.TempDir() + + oldWorkspace := provisionWorkspace + oldWait := provisionWaitSentinel + oldTimeout := provisionTimeout + oldInterval := provisionPollInterval + defer func() { + provisionWorkspace = oldWorkspace + provisionWaitSentinel = oldWait + provisionTimeout = oldTimeout + provisionPollInterval = oldInterval + }() + + provisionWorkspace = dir + provisionWaitSentinel = true + provisionTimeout = 3 + provisionPollInterval = 1 + + start := time.Now() + err := runWaitForSentinel(context.Background()) + elapsed := time.Since(start) + + if err == nil { + t.Fatal("expected timeout error when sentinel is missing") + } + if elapsed < 2*time.Second { + t.Errorf("should have waited at least 2s, only waited %s", elapsed) + } +} + +func TestProvisionCmd_WaitForSentinel_DelayedWrite(t *testing.T) { + dir := t.TempDir() + + oldWorkspace := provisionWorkspace + oldWait := provisionWaitSentinel + oldTimeout := provisionTimeout + oldInterval := provisionPollInterval + defer func() { + provisionWorkspace = oldWorkspace + provisionWaitSentinel = oldWait + provisionTimeout = oldTimeout + provisionPollInterval = oldInterval + }() + + provisionWorkspace = dir + provisionWaitSentinel = true + provisionTimeout = 10 + provisionPollInterval = 1 + + go func() { + time.Sleep(2 * time.Second) + sentinelPath := filepath.Join(dir, ".scion-provisioned") + _ = os.WriteFile(sentinelPath, []byte("provisioned_at=test\n"), 0644) + }() + + if err := runWaitForSentinel(context.Background()); err != nil { + t.Fatalf("expected success after delayed sentinel write, got: %v", err) + } +} + +func TestProvisionCmd_WaitForSentinel_ContextCancel(t *testing.T) { + dir := t.TempDir() + + oldWorkspace := provisionWorkspace + oldWait := provisionWaitSentinel + oldTimeout := provisionTimeout + oldInterval := provisionPollInterval + defer func() { + provisionWorkspace = oldWorkspace + provisionWaitSentinel = oldWait + provisionTimeout = oldTimeout + provisionPollInterval = oldInterval + }() + + provisionWorkspace = dir + provisionWaitSentinel = true + // Long timeout/interval: the loop would block well past the test budget if + // cancellation were not honoured. + provisionTimeout = 60 + provisionPollInterval = 30 + + ctx, cancel := context.WithCancel(context.Background()) + go func() { + time.Sleep(200 * time.Millisecond) + cancel() + }() + + start := time.Now() + err := runWaitForSentinel(ctx) + elapsed := time.Since(start) + + if err == nil { + t.Fatal("expected error when context is cancelled") + } + if elapsed > 5*time.Second { + t.Errorf("cancellation should interrupt the poll sleep promptly, waited %s", elapsed) + } +} + +func TestProvisionCmd_Clone_Idempotent(t *testing.T) { + dir := t.TempDir() + wsDir := filepath.Join(dir, "workspace") + if err := os.MkdirAll(wsDir, 0770); err != nil { + t.Fatal(err) + } + + sentinelPath := filepath.Join(wsDir, ".scion-provisioned") + if err := os.WriteFile(sentinelPath, []byte("provisioned_at=test\n"), 0644); err != nil { + t.Fatal(err) + } + + oldWorkspace := provisionWorkspace + oldMode := provisionMode + oldDepth := provisionDepth + oldUID := provisionUID + oldGID := provisionGID + defer func() { + provisionWorkspace = oldWorkspace + provisionMode = oldMode + provisionDepth = oldDepth + provisionUID = oldUID + provisionGID = oldGID + }() + + provisionWorkspace = wsDir + provisionMode = "shared-plain" + provisionDepth = 1 + provisionUID = os.Getuid() + provisionGID = os.Getgid() + + t.Setenv("SCION_CLONE_URL", "https://nonexistent.example.com/repo.git") + t.Setenv("SCION_CLONE_BRANCH", "main") + t.Setenv("SCION_PROJECT_ID", "test-proj") + + if err := runProvision(context.Background()); err != nil { + t.Fatalf("idempotent provision (sentinel exists) should succeed, got: %v", err) + } +} + +func TestProvisionCmd_Clone_NoURL(t *testing.T) { + dir := t.TempDir() + + oldWorkspace := provisionWorkspace + oldMode := provisionMode + oldDepth := provisionDepth + oldUID := provisionUID + oldGID := provisionGID + defer func() { + provisionWorkspace = oldWorkspace + provisionMode = oldMode + provisionDepth = oldDepth + provisionUID = oldUID + provisionGID = oldGID + }() + + provisionWorkspace = dir + provisionMode = "shared-plain" + provisionDepth = 1 + provisionUID = os.Getuid() + provisionGID = os.Getgid() + + t.Setenv("SCION_CLONE_URL", "") + t.Setenv("SCION_CLONE_BRANCH", "") + t.Setenv("SCION_PROJECT_ID", "test-proj-no-url") + + if err := runProvision(context.Background()); err != nil { + t.Fatalf("provision without clone URL should succeed (non-git project), got: %v", err) + } + + sentinelPath := filepath.Join(dir, ".scion-provisioned") + if _, err := os.Stat(sentinelPath); err != nil { + t.Errorf("sentinel should be written for non-git project: %v", err) + } +} diff --git a/cmd/sciontool/commands/status_test.go b/cmd/sciontool/commands/status_test.go index 7ab5000f5..959841629 100644 --- a/cmd/sciontool/commands/status_test.go +++ b/cmd/sciontool/commands/status_test.go @@ -21,6 +21,7 @@ func TestStatusCommand(t *testing.T) { originalHome := os.Getenv("HOME") os.Setenv("HOME", tempDir) defer os.Setenv("HOME", originalHome) + scrubScionEnv(t) tests := []struct { name string @@ -127,6 +128,7 @@ func TestStatusCommandUnknownType(t *testing.T) { originalHome := os.Getenv("HOME") os.Setenv("HOME", tempDir) defer os.Setenv("HOME", originalHome) + scrubScionEnv(t) buf := new(bytes.Buffer) rootCmd.SetOut(buf) diff --git a/cmd/server.go b/cmd/server.go index cb566ddc7..91a441913 100644 --- a/cmd/server.go +++ b/cmd/server.go @@ -29,6 +29,7 @@ var ( enableRuntimeBroker bool runtimeBrokerPort int dbURL string + noAutoMigrate bool enableDevAuth bool enableDebug bool storageBucket string @@ -238,6 +239,7 @@ func init() { serverStartCmd.Flags().IntVar(&hubPort, "port", 9810, "Hub API port (standalone mode only; ignored when --enable-web is set, use --web-port instead)") serverStartCmd.Flags().StringVar(&hubHost, "host", "0.0.0.0", "Hub API host to bind") serverStartCmd.Flags().StringVar(&dbURL, "db", "", "Database URL/path") + serverStartCmd.Flags().BoolVar(&noAutoMigrate, "no-auto-migrate", false, "Skip automatic in-process upgrade of a legacy raw-SQL hub.db to the Ent schema (operator opt-out)") // Runtime Broker API flags serverStartCmd.Flags().BoolVar(&enableRuntimeBroker, "enable-runtime-broker", false, "Enable the Runtime Broker API") diff --git a/cmd/server_dispatcher_test.go b/cmd/server_dispatcher_test.go index b563b9c15..c61e5fb75 100644 --- a/cmd/server_dispatcher_test.go +++ b/cmd/server_dispatcher_test.go @@ -79,40 +79,42 @@ func TestDispatchAgentStart(t *testing.T) { ctx := context.Background() s := newTestStore(t) mgr := &mockAgentManager{} - brokerID := "test-broker" + brokerID := tid("test-broker") adapter := newAgentDispatcherAdapter(mgr, s, brokerID) - // Create test project and broker - project := &store.Project{ - ID: "proj-1", - Slug: "test-project", + // Create test grove and broker + grove := &store.Project{ + ID: tid("grove-1"), + Slug: "test-grove", Name: "Test Project", } - err := s.CreateProject(ctx, project) + err := s.CreateProject(ctx, grove) require.NoError(t, err) broker := &store.RuntimeBroker{ ID: brokerID, Name: "test-broker", + Slug: "test-broker", } err = s.CreateRuntimeBroker(ctx, broker) require.NoError(t, err) provider := &store.ProjectProvider{ - ProjectID: project.ID, - BrokerID: brokerID, - LocalPath: "/tmp/fake/project", + ProjectID: grove.ID, + BrokerID: brokerID, + BrokerName: "test-broker", + LocalPath: "/tmp/fake/grove", } err = s.AddProjectProvider(ctx, provider) require.NoError(t, err) // Create agent agent := &store.Agent{ - ID: "agent-1", + ID: tid("agent-1"), Slug: "test-agent", Name: "test-agent", - ProjectID: project.ID, + ProjectID: grove.ID, Template: "gemini", Image: "test-image", Detached: true, @@ -133,7 +135,7 @@ func TestDispatchAgentStart(t *testing.T) { assert.Equal(t, "test-agent", mgr.startOpts.Name) assert.Equal(t, true, mgr.startOpts.Resume) assert.Equal(t, "new task", mgr.startOpts.Task) - assert.Equal(t, "/tmp/fake/project", mgr.startOpts.ProjectPath) + assert.Equal(t, "/tmp/fake/grove", mgr.startOpts.ProjectPath) assert.Equal(t, "gemini", mgr.startOpts.Template) assert.Equal(t, "BAR", mgr.startOpts.Env["FOO"]) @@ -149,24 +151,24 @@ func TestDispatchAgentRestart(t *testing.T) { ctx := context.Background() s := newTestStore(t) mgr := &mockAgentManager{} - brokerID := "test-broker" + brokerID := tid("test-broker") adapter := newAgentDispatcherAdapter(mgr, s, brokerID) - // Create test project and agent - project := &store.Project{ - ID: "proj-1", - Slug: "test-project", + // Create test grove and agent + grove := &store.Project{ + ID: tid("grove-1"), + Slug: "test-grove", Name: "Test Project", } - err := s.CreateProject(ctx, project) + err := s.CreateProject(ctx, grove) require.NoError(t, err) agent := &store.Agent{ - ID: "agent-1", + ID: tid("agent-1"), Slug: "test-agent", Name: "test-agent", - ProjectID: project.ID, + ProjectID: grove.ID, } err = s.CreateAgent(ctx, agent) require.NoError(t, err) diff --git a/cmd/server_foreground.go b/cmd/server_foreground.go index e8c18b467..e9a70e725 100644 --- a/cmd/server_foreground.go +++ b/cmd/server_foreground.go @@ -31,15 +31,19 @@ import ( "syscall" "time" + "github.com/google/uuid" + "github.com/GoogleCloudPlatform/scion/pkg/agent" "github.com/GoogleCloudPlatform/scion/pkg/api" "github.com/GoogleCloudPlatform/scion/pkg/apiclient" "github.com/GoogleCloudPlatform/scion/pkg/brokercredentials" "github.com/GoogleCloudPlatform/scion/pkg/config" + "github.com/GoogleCloudPlatform/scion/pkg/ent" "github.com/GoogleCloudPlatform/scion/pkg/ent/entc" "github.com/GoogleCloudPlatform/scion/pkg/eventbus" "github.com/GoogleCloudPlatform/scion/pkg/harness" "github.com/GoogleCloudPlatform/scion/pkg/hub" + "github.com/GoogleCloudPlatform/scion/pkg/observability/dbmetrics" scionplugin "github.com/GoogleCloudPlatform/scion/pkg/plugin" "github.com/GoogleCloudPlatform/scion/pkg/runtime" "github.com/GoogleCloudPlatform/scion/pkg/runtimebroker" @@ -47,7 +51,6 @@ import ( "github.com/GoogleCloudPlatform/scion/pkg/storage" "github.com/GoogleCloudPlatform/scion/pkg/store" "github.com/GoogleCloudPlatform/scion/pkg/store/entadapter" - "github.com/GoogleCloudPlatform/scion/pkg/store/sqlite" "github.com/GoogleCloudPlatform/scion/pkg/util" "github.com/GoogleCloudPlatform/scion/pkg/util/logging" "github.com/spf13/cobra" @@ -167,7 +170,7 @@ func runServerStart(cmd *cobra.Command, args []string) error { // 8. Initialize store var s store.Store if enableHub { - s, err = initStore(cfg) + s, err = initStore(ctx, cfg) if err != nil { return err } @@ -229,9 +232,13 @@ func runServerStart(cmd *cobra.Command, args []string) error { log.Fatalf("Hub server failed to start: %v", hubInitErr) } + // Wire command bus for cross-node dispatch (B2-4). + cmdBus := newCommandBus(ctx, cfg, hubSrv) + hubSrv.SetCommandBus(cmdBus) + if !enableWeb { // Hub runs its own HTTP server (standalone mode). - eventPub := hub.NewChannelEventPublisher() + eventPub := newEventPublisher(ctx, cfg) hubSrv.SetEventPublisher(eventPub) log.Printf("Starting Hub API server on %s:%d", cfg.Hub.Host, cfg.Hub.Port) @@ -259,7 +266,7 @@ func runServerStart(cmd *cobra.Command, args []string) error { // 12. Start Web var webSrv *hub.WebServer if enableWeb { - webSrv = initWebServer(cfg, hubSrv, devAuthToken, adminEmailList, adminMode, maintenanceMessage, requestLogger) + webSrv = initWebServer(ctx, cfg, hubSrv, devAuthToken, adminEmailList, adminMode, maintenanceMessage, requestLogger) // In combined mode, start Hub background services now that the // ChannelEventPublisher has been wired by initWebServer. @@ -323,7 +330,11 @@ func runServerStart(cmd *cobra.Command, args []string) error { // can authenticate back to the Hub API. Self-managed plugins // handle their own credential lifecycle. if !pluginMgr.IsSelfManaged(scionplugin.PluginTypeBroker, bt) && hubSrv != nil && s != nil { - brokerID := "plugin-broker-" + bt + // Use the same deterministic UUIDv5 as the α migration so the + // broker entity created here matches the migrated ID. + pluginBrokerNS := uuid.MustParse("5c104390-a1d0-5e9a-9b1e-5c104390a1d0") + legacyID := "plugin-broker-" + bt + brokerID := uuid.NewSHA1(pluginBrokerNS, []byte(legacyID)).String() if authSvc := hubSrv.GetBrokerAuthService(); authSvc != nil { // Ensure the runtime broker entity exists (required by // the broker_secrets foreign key constraint). @@ -348,9 +359,10 @@ func runServerStart(cmd *cobra.Command, args []string) error { log.Printf("Warning: failed to generate secret for broker plugin %q: %v", bt, secretErr) } else { hubCreds := map[string]string{ - "hub_url": hubEndpoint, - "hmac_key": secretKey, - "broker_id": brokerID, + "hub_url": hubEndpoint, + "hmac_key": secretKey, + "broker_id": brokerID, + "plugin_name": bt, } // Inject project slug map so hub-managed plugins can resolve // human-readable project names without user-level API access. @@ -367,6 +379,10 @@ func runServerStart(cmd *cobra.Command, args []string) error { hubCreds["project_slug_map"] = string(jsonBytes) } } + if cfg.Database.Driver != "" && cfg.Database.Driver != "sqlite" { + hubCreds["database_driver"] = cfg.Database.Driver + hubCreds["database_url"] = cfg.Database.URL + } if cfgErr := pluginMgr.ConfigureBroker(bt, hubCreds); cfgErr != nil { log.Printf("Warning: failed to inject hub credentials into broker plugin %q: %v", bt, cfgErr) } else { @@ -377,10 +393,11 @@ func runServerStart(cmd *cobra.Command, args []string) error { } observer := isObserverBroker(pluginMgr, bt) + channelID := pluginChannelID(pluginMgr, bt) namedBuses = append(namedBuses, eventbus.NamedEventBus{ - Name: bt, Bus: b, Observer: observer, + Name: bt, Bus: b, Observer: observer, ChannelID: channelID, }) - log.Printf("Message broker spoke added: name=%s observer=%v", bt, observer) + log.Printf("Message broker spoke added: name=%s channel_id=%s observer=%v", bt, channelID, observer) } fanout := eventbus.NewFanOutEventBus(namedBuses, logging.Subsystem("hub.eventbus.fanout")) @@ -639,49 +656,123 @@ func checkServerPorts(cfg *config.GlobalConfig) error { return nil } -// initStore initializes the database store. -func initStore(cfg *config.GlobalConfig) (store.Store, error) { +// initStore initializes the database store. The provided context is used for +// schema migration and the initial health-check ping so that a Ctrl+C during +// startup cancels those operations gracefully. +func initStore(ctx context.Context, cfg *config.GlobalConfig) (store.Store, error) { + connMaxLifetime, err := cfg.Database.ConnMaxLifetimeDuration() + if err != nil { + return nil, fmt.Errorf("invalid database pool config: %w", err) + } + connMaxIdleTime, err := cfg.Database.ConnMaxIdleTimeDuration() + if err != nil { + return nil, fmt.Errorf("invalid database pool config: %w", err) + } + + // The connection pool config is shared across backends. For SQLite, + // MaxOpenConns is forced to 1 by applyDatabasePoolDefaults to serialize + // writes; for Postgres it carries the larger pool sizing (default 10/5/30m + // lifetime, 5m idle) since Postgres handles concurrent connections natively. + pool := entc.PoolConfig{ + MaxOpenConns: cfg.Database.MaxOpenConns, + MaxIdleConns: cfg.Database.MaxIdleConns, + ConnMaxLifetime: connMaxLifetime, + ConnMaxIdleTime: connMaxIdleTime, + } + + var entClient *ent.Client switch cfg.Database.Driver { case "sqlite": - sqliteStore, err := sqlite.New(cfg.Database.URL) - if err != nil { - return nil, fmt.Errorf("failed to open database: %w", err) + // Migration α: upgrade a legacy raw-SQL hub.db (the former + // pkg/store/sqlite schema) to the consolidated Ent schema before opening + // it. Detection is conservative and the whole step is a no-op for an + // already-Ent file, so it is safe to run on every boot. + if err := maybeMigrateLegacySQLite(ctx, cfg.Database.URL); err != nil { + return nil, err } - if err := sqliteStore.Migrate(context.Background()); err != nil { - sqliteStore.Close() - return nil, fmt.Errorf("failed to run migrations: %w", err) + // All Hub state lives in a single Ent-backed SQLite database. + // Guard against a double "file:" prefix when the operator already + // supplies "file:/path/hub.db" in their config. + sqliteDSN := cfg.Database.URL + if !strings.HasPrefix(sqliteDSN, "file:") { + sqliteDSN = "file:" + sqliteDSN } - - entDSN := cfg.Database.URL + "_ent" - entClient, err := entc.OpenSQLite("file:" + entDSN + "?cache=shared") + if !strings.Contains(sqliteDSN, "?") { + sqliteDSN += "?cache=shared" + } else if !strings.Contains(sqliteDSN, "cache=") { + sqliteDSN += "&cache=shared" + } + entClient, err = entc.OpenSQLite(sqliteDSN, pool) if err != nil { - sqliteStore.Close() - return nil, fmt.Errorf("failed to open ent database: %w", err) + return nil, fmt.Errorf("failed to open database: %w", err) } - if err := entc.AutoMigrate(context.Background(), entClient); err != nil { - entClient.Close() - sqliteStore.Close() - return nil, fmt.Errorf("failed to run ent migrations: %w", err) + case "postgres": + // Postgres uses the pgx stdlib driver. The URL is a standard + // connection string (e.g. "postgres://user:pass@host:5432/db?sslmode=require"). + entClient, err = entc.OpenPostgres(cfg.Database.URL, pool) + if err != nil { + return nil, fmt.Errorf("failed to open database: %w", err) } + default: + return nil, fmt.Errorf("unsupported database driver: %s", cfg.Database.Driver) + } - if err := entc.MigrateGroveToProjectData(context.Background(), entDSN, sqliteStore); err != nil { - entClient.Close() - sqliteStore.Close() - return nil, fmt.Errorf("failed to migrate ent data: %w", err) - } + s := entadapter.NewCompositeStore(entClient) - s := entadapter.NewCompositeStore(sqliteStore, entClient) + // Migrate runs Ent's schema migration and seeds built-in maintenance + // operations (parity with the former raw-SQL store). + if err := s.Migrate(ctx); err != nil { + s.Close() + return nil, fmt.Errorf("failed to run migrations: %w", err) + } - if err := s.Ping(context.Background()); err != nil { - sqliteStore.Close() - return nil, fmt.Errorf("database ping failed: %w", err) - } + if err := s.Ping(ctx); err != nil { + s.Close() + return nil, fmt.Errorf("database ping failed: %w", err) + } - return s, nil - default: - return nil, fmt.Errorf("unsupported database driver: %s", cfg.Database.Driver) + return s, nil +} + +// maybeMigrateLegacySQLite detects a legacy raw-SQL hub.db at path and, unless +// the operator opted out with --no-auto-migrate, upgrades it in-process to the +// consolidated Ent schema (after taking an automatic backup). It is a no-op when +// the file is already the Ent schema, empty, or absent. The provided context +// allows the migration to be cancelled (e.g. Ctrl+C during first boot). +func maybeMigrateLegacySQLite(ctx context.Context, path string) error { + if _, err := os.Stat(path); os.IsNotExist(err) { + return nil + } + legacy, err := entc.IsLegacyRawSQLSchema(path) + if err != nil { + return fmt.Errorf("detecting database schema: %w", err) + } + if !legacy { + return nil + } + + if noAutoMigrate { + // The operator opted out, but the file is a legacy schema the Ent store + // cannot open. Fail loudly with guidance rather than crash later. + return fmt.Errorf("detected a legacy raw-SQL hub database at %s but --no-auto-migrate is set; "+ + "remove the flag to upgrade it in place (a backup is taken automatically), "+ + "or point --db at an already-migrated database", path) + } + + log.Printf("Detected legacy raw-SQL hub database at %s. Backing up and migrating to the Ent schema...", path) + report, err := entc.MigrateAlphaSQLite(ctx, path, entc.AlphaOptions{ + Logf: func(format string, args ...any) { log.Printf(format, args...) }, + }) + if err != nil { + return fmt.Errorf("migrating legacy database (original left untouched): %w", err) } + if report.Skipped { + return nil + } + log.Printf("Migration α complete: %d tables, %d rows migrated. Backup: %s", + len(report.Tables), report.TotalRows(), report.BackupPath) + return nil } // initDevAuth initializes dev authentication and returns the token. @@ -778,6 +869,25 @@ func parseAdminEmails(cfg *config.GlobalConfig) []string { return adminEmailList } +// resolveSessionSecret resolves the deployment-wide session secret from the +// --session-secret flag, falling back to the SCION_SERVER_SESSION_SECRET env +// var (then SESSION_SECRET for compatibility). The same value backs both the +// web session cookie store and the hub JWT signing keys so that all replicas +// behind the load balancer agree. +func resolveSessionSecret() string { + secret := webSessionSecret + if secret == "" { + secret = os.Getenv("SCION_SERVER_SESSION_SECRET") + } + if secret == "" { + secret = os.Getenv("SESSION_SECRET") + } + if secret == "" && hostedMode { + slog.Warn("No session secret configured in hosted mode! Replicas will not be able to share sessions or agree on JWT signing keys, leading to login loops.") + } + return secret +} + // initHubServer creates and configures the Hub server. func initHubServer(ctx context.Context, cfg *config.GlobalConfig, s store.Store, hubEndpoint, devAuthToken string, adminEmailList []string, adminMode bool, maintenanceMessage string, requestLogger, messageLogger *slog.Logger, globalDir string, pluginMgr *scionplugin.Manager, secretBackend secret.SecretBackend) (*hub.Server, error) { hubCfg := hub.ServerConfig{ @@ -791,6 +901,7 @@ func initHubServer(ctx context.Context, cfg *config.GlobalConfig, s store.Store, CORSAllowedMethods: cfg.Hub.CORSAllowedMethods, CORSAllowedHeaders: cfg.Hub.CORSAllowedHeaders, CORSMaxAge: cfg.Hub.CORSMaxAge, + AuthMode: cfg.Auth.Mode, DevAuthToken: devAuthToken, Debug: enableDebug, AuthorizedDomains: cfg.Auth.AuthorizedDomains, @@ -848,6 +959,68 @@ func initHubServer(ctx context.Context, cfg *config.GlobalConfig, s store.Store, MaintenanceConfig: resolveMaintenanceConfig(cfg), SecretBackend: secretBackend, GCPProjectID: cfg.Hub.GCPProjectID, + // Derive the agent/user JWT signing keys from the same shared session + // secret the web cookie store uses, so every replica behind the load + // balancer agrees on the signing key regardless of its host-derived + // HubID. Without this, a JWT minted by one replica fails validation on + // another (cross-replica "session_expired" login loop). + SharedSigningSecret: resolveSessionSecret(), + // When SCION_REQUIRE_STABLE_SIGNING_KEY is truthy, the hub refuses to + // start rather than silently mint a new signing key it cannot resolve + // (which would invalidate every live token after, e.g., a redeploy onto a + // new host that changed the HubID). Operators enabling this must supply a + // session secret or pre-provision the signing keys. + RequireStableSigningKey: os.Getenv("SCION_REQUIRE_STABLE_SIGNING_KEY") == "true", + } + + // In hosted mode every replica must share the same session secret for + // cookies and JWT signing keys to work across the load balancer. Running + // without one means each replica generates its own ephemeral key, which + // breaks session persistence and causes login loops. + if hostedMode && hubCfg.SharedSigningSecret == "" { + log.Println("WARNING: hosted mode is enabled but no session secret is configured. " + + "Set --session-secret or SCION_SERVER_SESSION_SECRET to avoid cross-replica session failures.") + } + + // Construct proxy authenticator when auth mode is "proxy" + if cfg.Auth.Mode == "proxy" && cfg.Auth.Proxy != nil { + switch cfg.Auth.Proxy.Provider { + case "iap": + if cfg.Auth.Proxy.IAP == nil || cfg.Auth.Proxy.IAP.Audience == "" { + return nil, fmt.Errorf("auth.proxy.iap.audience is required when auth.mode=proxy and provider=iap") + } + hubCfg.ProxyAuth = &hub.IAPAuthenticator{ + Audience: cfg.Auth.Proxy.IAP.Audience, + Issuer: cfg.Auth.Proxy.IAP.Issuer, + JWKSURL: cfg.Auth.Proxy.IAP.JWKSURL, + } + log.Printf("Proxy auth configured: provider=iap, audience=%s", cfg.Auth.Proxy.IAP.Audience) + case "header": + // TODO: HeaderProxyAuthenticator (refactor of extractProxyUser) + log.Printf("Proxy auth configured: provider=header (legacy IP-trust mode)") + default: + return nil, fmt.Errorf("unsupported auth.proxy.provider: %q", cfg.Auth.Proxy.Provider) + } + } + + // Construct transport token minter when auth.transport is configured + if cfg.Auth.Transport != nil && cfg.Auth.Transport.Mode != "" && cfg.Auth.Transport.Mode != "none" { + if cfg.Auth.Transport.PlatformAuthSA == "" { + return nil, fmt.Errorf("auth.transport.platformAuthSA is required when auth.transport.mode=%q", cfg.Auth.Transport.Mode) + } + audience := cfg.Auth.Transport.OIDCAudience + if audience == "" && cfg.Auth.Transport.Mode == "cloudrun_invoker" { + // Derive audience from hub endpoint for Cloud Run invoker mode + audience = hubEndpoint + } + if audience == "" { + return nil, fmt.Errorf("auth.transport.oidcAudience is required when auth.transport.mode=%q", cfg.Auth.Transport.Mode) + } + hubCfg.TransportMode = cfg.Auth.Transport.Mode + hubCfg.TransportAudience = audience + hubCfg.TransportMinter = hub.NewGCPTransportMinter(cfg.Auth.Transport.PlatformAuthSA, "") + log.Printf("Transport auth configured: mode=%s, audience=%s, sa=%s", + cfg.Auth.Transport.Mode, audience, cfg.Auth.Transport.PlatformAuthSA) } hubSrv, err := hub.New(hubCfg, s) @@ -988,18 +1161,61 @@ func initHubStorage(ctx context.Context, hubSrv *hub.Server, cfg *config.GlobalC } } -// initWebServer creates and configures the Web server. -func initWebServer(cfg *config.GlobalConfig, hubSrv *hub.Server, devAuthToken string, adminEmailList []string, adminMode bool, maintenanceMessage string, requestLogger *slog.Logger) *hub.WebServer { +// newEventPublisher selects the event publisher backend based on the configured +// database driver. With Postgres it returns a PostgresEventPublisher +// (cross-replica LISTEN/NOTIFY); otherwise it returns the in-process +// ChannelEventPublisher. If the Postgres publisher cannot be started it falls +// back to the in-process publisher so a single instance still functions, logging +// a prominent warning since cross-replica SSE delivery will be unavailable. +func newEventPublisher(ctx context.Context, cfg *config.GlobalConfig) hub.EventPublisher { + if strings.EqualFold(cfg.Database.Driver, "postgres") { + // Metrics export is wired separately (see pkg/observability/dbmetrics); + // use a disabled recorder until a MeterProvider is configured. + pub, err := hub.NewPostgresEventPublisher(ctx, cfg.Database.URL, dbmetrics.NewDisabled(), logging.Subsystem("hub.events")) + if err != nil { + log.Printf("WARNING: failed to start Postgres event publisher (%v); falling back to in-process events. Cross-replica SSE will not work.", err) + return hub.NewChannelEventPublisher() + } + log.Printf("Using Postgres LISTEN/NOTIFY event publisher") + return pub + } + return hub.NewChannelEventPublisher() +} + +// newCommandBus selects the command bus backend. With Postgres it returns a +// PostgresCommandBus (LISTEN/NOTIFY on scion_broker_cmd); otherwise it returns +// a no-op bus (single-process SQLite always owns all brokers locally). +func newCommandBus(ctx context.Context, cfg *config.GlobalConfig, hubSrv *hub.Server) hub.CommandBus { + if !strings.EqualFold(cfg.Database.Driver, "postgres") { + return hub.NoopCommandBus{} + } + ownsLocally := func(brokerID string) bool { + mgr := hubSrv.GetControlChannelManager() + if mgr == nil { + return false + } + return mgr.IsConnected(brokerID) + } + bus, err := hub.NewPostgresCommandBus(ctx, cfg.Database.URL, ownsLocally, hubSrv.ReconcileBroker, logging.Subsystem("hub.commandbus")) + if err != nil { + log.Printf("WARNING: failed to start Postgres command bus (%v); falling back to no-op. Cross-replica dispatch signals will not work.", err) + return hub.NoopCommandBus{} + } + log.Printf("Using Postgres command bus on channel scion_broker_cmd") + return bus +} + +// initWebServer creates and configures the Web server. The provided context is +// threaded to the event publisher so that the Postgres LISTEN/NOTIFY goroutine +// is cancelled cleanly on shutdown, preventing connection leaks. +func initWebServer(ctx context.Context, cfg *config.GlobalConfig, hubSrv *hub.Server, devAuthToken string, adminEmailList []string, adminMode bool, maintenanceMessage string, requestLogger *slog.Logger) *hub.WebServer { webHost := cfg.Hub.Host if webHost == "" { webHost = "0.0.0.0" } // Allow env var overrides for session/OAuth config - sessionSecret := webSessionSecret - if sessionSecret == "" { - sessionSecret = os.Getenv("SCION_SERVER_SESSION_SECRET") - } + sessionSecret := resolveSessionSecret() baseURL := webBaseURL if baseURL == "" { baseURL = os.Getenv("SCION_SERVER_BASE_URL") @@ -1033,6 +1249,7 @@ func initWebServer(cfg *config.GlobalConfig, hubSrv *hub.Server, devAuthToken st SessionSecret: sessionSecret, BaseURL: baseURL, DevAuthToken: devAuthToken, + AuthMode: cfg.Auth.Mode, AuthorizedDomains: webAuthorizedDomains, AdminEmails: webAdminEmails, UserAccessMode: cfg.Auth.UserAccessMode, @@ -1043,7 +1260,7 @@ func initWebServer(cfg *config.GlobalConfig, hubSrv *hub.Server, devAuthToken st webSrv.SetRequestLogger(requestLogger) // Create shared event publisher for real-time SSE - eventPub := hub.NewChannelEventPublisher() + eventPub := newEventPublisher(ctx, cfg) webSrv.SetEventPublisher(eventPub) // Wire Hub services into WebServer if Hub is enabled @@ -1250,13 +1467,38 @@ func startRuntimeBroker(ctx context.Context, cmd *cobra.Command, cfg *config.Glo return nil } +// pluginChannelID returns the channel identifier reported by a broker plugin +// via GetInfo().ChannelID. Returns "" if the plugin does not report one, in +// which case the bus Name is used for channel routing. +func pluginChannelID(pluginMgr *scionplugin.Manager, name string) string { + raw, err := pluginMgr.Get(scionplugin.PluginTypeBroker, name) + if err != nil { + return "" + } + type infoer interface { + GetInfo() (*scionplugin.PluginInfo, error) + } + rpc, ok := raw.(infoer) + if !ok { + return "" + } + info, err := rpc.GetInfo() + if err != nil || info == nil { + return "" + } + return info.ChannelID +} + // isObserverBroker determines whether a broker plugin should be treated as an // observer (fire-and-forget on publish errors). It checks the plugin's // capabilities first, then falls back to a name-based heuristic. func isObserverBroker(pluginMgr *scionplugin.Manager, name string) bool { raw, err := pluginMgr.Get(scionplugin.PluginTypeBroker, name) if err == nil { - if rpc, ok := raw.(*scionplugin.BrokerRPCClient); ok { + type infoer interface { + GetInfo() (*scionplugin.PluginInfo, error) + } + if rpc, ok := raw.(infoer); ok { if info, infoErr := rpc.GetInfo(); infoErr == nil && info != nil { for _, cap := range info.Capabilities { if strings.EqualFold(cap, "observer") { diff --git a/cmd/server_migrate.go b/cmd/server_migrate.go new file mode 100644 index 000000000..75c58f9f8 --- /dev/null +++ b/cmd/server_migrate.go @@ -0,0 +1,221 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package cmd + +import ( + "fmt" + "os" + "strings" + + "github.com/GoogleCloudPlatform/scion/pkg/ent/entc" + "github.com/spf13/cobra" +) + +var ( + migrateFrom string + migrateTo string + migrateDropSource bool + migrateKeepSource bool + migrateBatchSize int +) + +// serverMigrateCmd implements Migration β from the PostgreSQL strategy: an +// entity-by-entity copy from an Ent-on-SQLite database to an Ent-on-Postgres +// database. Both endpoints share the same Ent schema, so the copy is a plain +// dependency-ordered transfer through the Ent client. +var serverMigrateCmd = &cobra.Command{ + Use: "migrate", + Short: "Migrate Hub data from SQLite to PostgreSQL", + Long: `Copy all Hub state from an Ent-backed SQLite database into an +Ent-backed PostgreSQL database. + +The migration is: + - Dependency-ordered: parents are inserted before children so every foreign + key resolves at insert time. + - Idempotent: rows whose primary key already exists in the destination are + skipped, so a failed run can be safely restarted. + - Read-only on the source: the SQLite file is opened with PRAGMA query_only, + so the running SQLite hub can stay up until you cut over. + - Verified: source and destination row counts are compared after every + entity, aborting on any mismatch. + +By default the source SQLite file is left untouched (--keep-source). Pass +--drop-source for an explicit cutover that deletes the SQLite file after a +successful, verified migration. + +Examples: + # Dry-safe copy; leaves SQLite in place. + scion server migrate \ + --from sqlite:///var/lib/scion/hub.db \ + --to "postgres://scion:secret@db.example.com:5432/scion?sslmode=require" + + # Explicit cutover: delete the SQLite file once migration succeeds. + scion server migrate \ + --from sqlite:///var/lib/scion/hub.db \ + --to "postgres://scion:secret@db:5432/scion?sslmode=require" \ + --drop-source`, + RunE: runServerMigrate, +} + +func init() { + serverCmd.AddCommand(serverMigrateCmd) + + serverMigrateCmd.Flags().StringVar(&migrateFrom, "from", "", "Source SQLite DSN (e.g. sqlite:///path/to/hub.db) [required]") + serverMigrateCmd.Flags().StringVar(&migrateTo, "to", "", "Destination PostgreSQL DSN (e.g. postgres://user:pass@host:5432/db?sslmode=require) [required]") + serverMigrateCmd.Flags().BoolVar(&migrateKeepSource, "keep-source", true, "Leave the source SQLite file untouched (default)") + serverMigrateCmd.Flags().BoolVar(&migrateDropSource, "drop-source", false, "Delete the source SQLite file after a successful migration (explicit cutover)") + serverMigrateCmd.Flags().IntVar(&migrateBatchSize, "batch-size", 0, "Max rows per bulk insert statement (0 = default)") + + _ = serverMigrateCmd.MarkFlagRequired("from") + _ = serverMigrateCmd.MarkFlagRequired("to") +} + +func runServerMigrate(cmd *cobra.Command, _ []string) error { + ctx := cmd.Context() + out := cmd.OutOrStdout() + + if migrateBatchSize < 0 { + return fmt.Errorf("batch size must be non-negative, got %d", migrateBatchSize) + } + + srcDSN, srcPath, err := parseSQLiteSourceDSN(migrateFrom) + if err != nil { + return err + } + dstDSN, err := parsePostgresDestDSN(migrateTo) + if err != nil { + return err + } + + fmt.Fprintf(out, "Opening source (read-only): %s\n", srcPath) + src, err := entc.OpenSQLiteReadOnly(srcDSN) + if err != nil { + return fmt.Errorf("opening source sqlite: %w", err) + } + defer func() { + if src != nil { + _ = src.Close() + } + }() + + fmt.Fprintln(out, "Opening destination PostgreSQL") + dst, err := entc.OpenPostgres(dstDSN, entc.PoolConfig{MaxOpenConns: 10, MaxIdleConns: 5}) + if err != nil { + return fmt.Errorf("opening destination postgres: %w", err) + } + defer dst.Close() + + fmt.Fprintln(out, "Ensuring destination schema (auto-migrate)") + if err := entc.AutoMigrate(ctx, dst); err != nil { + return fmt.Errorf("destination auto-migrate: %w", err) + } + + fmt.Fprintln(out, "Migrating entities...") + report, err := entc.MigrateData(ctx, src, dst, entc.MigrateOptions{ + BatchSize: migrateBatchSize, + Logf: func(format string, args ...any) { + fmt.Fprintf(out, " "+format+"\n", args...) + }, + }) + if err != nil { + return fmt.Errorf("migration failed: %w", err) + } + + total := 0 + for _, e := range report.Entities { + total += e.Dest + } + fmt.Fprintf(out, "Migration complete: %d entities, %d rows total, %d child-group edges\n", + len(report.Entities), total, report.ChildGroupEdgs) + + if migrateDropSource { + _ = src.Close() + src = nil + fmt.Fprintf(out, "Dropping source SQLite file: %s\n", srcPath) + if err := dropSQLiteFile(srcPath); err != nil { + return fmt.Errorf("dropping source: %w", err) + } + fmt.Fprintln(out, "Source dropped.") + } else { + fmt.Fprintf(out, "Source left in place: %s\n", srcPath) + } + + return nil +} + +// parseSQLiteSourceDSN normalizes the --from value into a modernc.org/sqlite DSN +// and returns the bare filesystem path (for --drop-source). It accepts: +// +// sqlite:///abs/path/hub.db -> /abs/path/hub.db +// sqlite://rel/path/hub.db -> rel/path/hub.db +// file:/abs/path/hub.db -> passed through, path extracted +// /abs/path/hub.db -> bare path +func parseSQLiteSourceDSN(raw string) (dsn, path string, err error) { + if raw == "" { + return "", "", fmt.Errorf("--from is required") + } + switch { + case strings.HasPrefix(raw, "sqlite://"): + path = strings.TrimPrefix(raw, "sqlite://") + // sqlite:///abs -> "/abs"; an extra leading slash denotes an absolute path. + case strings.HasPrefix(raw, "sqlite:"): + path = strings.TrimPrefix(raw, "sqlite:") + case strings.HasPrefix(raw, "file://"): + path = strings.TrimPrefix(raw, "file://") + if i := strings.IndexByte(path, '?'); i >= 0 { + path = path[:i] + } + case strings.HasPrefix(raw, "file:"): + path = strings.TrimPrefix(raw, "file:") + // Strip any query parameters from the extracted path. + if i := strings.IndexByte(path, '?'); i >= 0 { + path = path[:i] + } + default: + path = raw + } + if path == "" { + return "", "", fmt.Errorf("could not determine sqlite file path from %q", raw) + } + // cache=shared matches how the hub opens its SQLite database elsewhere. + dsn = "file:" + path + "?cache=shared" + return dsn, path, nil +} + +// parsePostgresDestDSN validates the --to value and returns a DSN the pgx +// stdlib driver accepts. Both URL-style ("postgres://...") and keyword/value +// ("host=... port=...") DSNs are passed through unchanged. +func parsePostgresDestDSN(raw string) (string, error) { + if raw == "" { + return "", fmt.Errorf("--to is required") + } + if strings.HasPrefix(raw, "postgres://") || + strings.HasPrefix(raw, "postgresql://") || + strings.Contains(raw, "host=") { + return raw, nil + } + return "", fmt.Errorf("--to must be a PostgreSQL DSN (postgres://... or host=...), got %q", raw) +} + +// dropSQLiteFile removes the SQLite database file and any WAL/SHM/journal +// sidecar files left next to it. +func dropSQLiteFile(path string) error { + for _, suffix := range []string{"", "-wal", "-shm", "-journal"} { + if err := os.Remove(path + suffix); err != nil && !os.IsNotExist(err) { + return err + } + } + return nil +} diff --git a/cmd/server_migrate_test.go b/cmd/server_migrate_test.go new file mode 100644 index 000000000..195f099b3 --- /dev/null +++ b/cmd/server_migrate_test.go @@ -0,0 +1,129 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package cmd + +import "testing" + +func TestParseSQLiteSourceDSN(t *testing.T) { + tests := []struct { + name string + in string + wantDSN string + wantPath string + wantErr bool + }{ + { + name: "absolute sqlite url", + in: "sqlite:///var/lib/scion/hub.db", + wantDSN: "file:/var/lib/scion/hub.db?cache=shared", + wantPath: "/var/lib/scion/hub.db", + }, + { + name: "relative sqlite url", + in: "sqlite://data/hub.db", + wantDSN: "file:data/hub.db?cache=shared", + wantPath: "data/hub.db", + }, + { + name: "sqlite single-colon form", + in: "sqlite:/tmp/hub.db", + wantDSN: "file:/tmp/hub.db?cache=shared", + wantPath: "/tmp/hub.db", + }, + { + name: "file url with query", + in: "file:/tmp/hub.db?cache=shared", + wantDSN: "file:/tmp/hub.db?cache=shared", + wantPath: "/tmp/hub.db", + }, + { + name: "file url with triple slashes", + in: "file:///tmp/hub.db", + wantDSN: "file:/tmp/hub.db?cache=shared", + wantPath: "/tmp/hub.db", + }, + { + name: "bare path", + in: "/tmp/hub.db", + wantDSN: "file:/tmp/hub.db?cache=shared", + wantPath: "/tmp/hub.db", + }, + { + name: "empty", + in: "", + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + dsn, path, err := parseSQLiteSourceDSN(tt.in) + if tt.wantErr { + if err == nil { + t.Fatalf("expected error, got dsn=%q path=%q", dsn, path) + } + return + } + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if dsn != tt.wantDSN { + t.Errorf("dsn = %q, want %q", dsn, tt.wantDSN) + } + if path != tt.wantPath { + t.Errorf("path = %q, want %q", path, tt.wantPath) + } + }) + } +} + +func TestParsePostgresDestDSN(t *testing.T) { + tests := []struct { + name string + in string + wantErr bool + }{ + {name: "url form", in: "postgres://u:p@host:5432/db?sslmode=require"}, + {name: "postgresql scheme", in: "postgresql://u:p@host:5432/db"}, + {name: "keyword form", in: "host=h port=5432 user=u password=p dbname=db sslmode=require"}, + {name: "empty", in: "", wantErr: true}, + {name: "not postgres", in: "sqlite:///tmp/hub.db", wantErr: true}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := parsePostgresDestDSN(tt.in) + if tt.wantErr { + if err == nil { + t.Fatalf("expected error, got %q", got) + } + return + } + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if got != tt.in { + t.Errorf("dsn = %q, want unchanged %q", got, tt.in) + } + }) + } +} + +func TestServerMigrateCmdRegistered(t *testing.T) { + for _, c := range serverCmd.Commands() { + if c.Name() == "migrate" { + return + } + } + t.Fatal("migrate subcommand not registered under server") +} diff --git a/cmd/server_test.go b/cmd/server_test.go index 5df3e2f7d..47bd4184e 100644 --- a/cmd/server_test.go +++ b/cmd/server_test.go @@ -18,20 +18,24 @@ package cmd import ( "context" + "strings" "testing" "github.com/GoogleCloudPlatform/scion/pkg/config" + "github.com/GoogleCloudPlatform/scion/pkg/ent/entc" "github.com/GoogleCloudPlatform/scion/pkg/store" - "github.com/GoogleCloudPlatform/scion/pkg/store/sqlite" + "github.com/GoogleCloudPlatform/scion/pkg/store/entadapter" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) func newTestStore(t *testing.T) store.Store { t.Helper() - s, err := sqlite.New(":memory:") + dbName := strings.ReplaceAll(t.Name(), "/", "_") + client, err := entc.OpenSQLite("file:"+dbName+"?mode=memory&cache=shared", entc.PoolConfig{}) require.NoError(t, err) - require.NoError(t, s.Migrate(context.Background())) + require.NoError(t, entc.AutoMigrate(context.Background(), client)) + s := entadapter.NewCompositeStore(client) t.Cleanup(func() { s.Close() }) return s } @@ -41,31 +45,31 @@ func TestRegisterGlobalGroveAndBroker_DedupByName(t *testing.T) { s := newTestStore(t) settings := &config.Settings{} - // First registration: creates broker with ID "broker-1" and name "test-broker" - effectiveID, err := registerGlobalProjectAndBroker(ctx, s, "broker-1", "test-broker", "http://localhost:9800", nil, true, settings) + // First registration: creates broker with ID tid("broker-1") and name "test-broker" + effectiveID, err := registerGlobalProjectAndBroker(ctx, s, tid("broker-1"), "test-broker", "http://localhost:9800", nil, true, settings) require.NoError(t, err) - assert.Equal(t, "broker-1", effectiveID) + assert.Equal(t, tid("broker-1"), effectiveID) // Verify broker was created - broker, err := s.GetRuntimeBroker(ctx, "broker-1") + broker, err := s.GetRuntimeBroker(ctx, tid("broker-1")) require.NoError(t, err) assert.Equal(t, "test-broker", broker.Name) assert.Equal(t, store.BrokerStatusOnline, broker.Status) // Second registration with a DIFFERENT ID but SAME name. // This simulates a restart where the broker ID was lost/regenerated. - effectiveID, err = registerGlobalProjectAndBroker(ctx, s, "broker-2", "test-broker", "http://localhost:9800", nil, true, settings) + effectiveID, err = registerGlobalProjectAndBroker(ctx, s, tid("broker-2"), "test-broker", "http://localhost:9800", nil, true, settings) require.NoError(t, err) // Should return the original broker-1 ID (dedup by name) - assert.Equal(t, "broker-1", effectiveID, "should reuse existing broker ID found by name") + assert.Equal(t, tid("broker-1"), effectiveID, "should reuse existing broker ID found by name") // Verify no duplicate was created - _, err = s.GetRuntimeBroker(ctx, "broker-2") + _, err = s.GetRuntimeBroker(ctx, tid("broker-2")) assert.ErrorIs(t, err, store.ErrNotFound, "broker-2 should NOT exist in the database") // Verify original broker was updated - broker, err = s.GetRuntimeBroker(ctx, "broker-1") + broker, err = s.GetRuntimeBroker(ctx, tid("broker-1")) require.NoError(t, err) assert.Equal(t, "test-broker", broker.Name) assert.Equal(t, store.BrokerStatusOnline, broker.Status) @@ -77,17 +81,17 @@ func TestRegisterGlobalGroveAndBroker_SameIDNoDedup(t *testing.T) { settings := &config.Settings{} // First registration - effectiveID, err := registerGlobalProjectAndBroker(ctx, s, "broker-1", "test-broker", "http://localhost:9800", nil, true, settings) + effectiveID, err := registerGlobalProjectAndBroker(ctx, s, tid("broker-1"), "test-broker", "http://localhost:9800", nil, true, settings) require.NoError(t, err) - assert.Equal(t, "broker-1", effectiveID) + assert.Equal(t, tid("broker-1"), effectiveID) // Second registration with the same ID (normal restart case) - effectiveID, err = registerGlobalProjectAndBroker(ctx, s, "broker-1", "test-broker", "http://localhost:9800", nil, false, settings) + effectiveID, err = registerGlobalProjectAndBroker(ctx, s, tid("broker-1"), "test-broker", "http://localhost:9800", nil, false, settings) require.NoError(t, err) - assert.Equal(t, "broker-1", effectiveID) + assert.Equal(t, tid("broker-1"), effectiveID) // Verify broker was updated (not duplicated) - broker, err := s.GetRuntimeBroker(ctx, "broker-1") + broker, err := s.GetRuntimeBroker(ctx, tid("broker-1")) require.NoError(t, err) assert.Equal(t, "test-broker", broker.Name) assert.Equal(t, false, broker.AutoProvide, "auto-provide should be updated to false") @@ -99,19 +103,19 @@ func TestRegisterGlobalGroveAndBroker_NewBrokerNewName(t *testing.T) { settings := &config.Settings{} // Register first broker - effectiveID, err := registerGlobalProjectAndBroker(ctx, s, "broker-1", "broker-alpha", "http://localhost:9800", nil, true, settings) + effectiveID, err := registerGlobalProjectAndBroker(ctx, s, tid("broker-1"), "broker-alpha", "http://localhost:9800", nil, true, settings) require.NoError(t, err) - assert.Equal(t, "broker-1", effectiveID) + assert.Equal(t, tid("broker-1"), effectiveID) // Register a genuinely different broker (different ID AND different name) - effectiveID, err = registerGlobalProjectAndBroker(ctx, s, "broker-2", "broker-beta", "http://localhost:9801", nil, true, settings) + effectiveID, err = registerGlobalProjectAndBroker(ctx, s, tid("broker-2"), "broker-beta", "http://localhost:9801", nil, true, settings) require.NoError(t, err) - assert.Equal(t, "broker-2", effectiveID) + assert.Equal(t, tid("broker-2"), effectiveID) // Both brokers should exist - _, err = s.GetRuntimeBroker(ctx, "broker-1") + _, err = s.GetRuntimeBroker(ctx, tid("broker-1")) assert.NoError(t, err) - _, err = s.GetRuntimeBroker(ctx, "broker-2") + _, err = s.GetRuntimeBroker(ctx, tid("broker-2")) assert.NoError(t, err) } @@ -121,13 +125,13 @@ func TestRegisterGlobalGroveAndBroker_DedupCaseInsensitive(t *testing.T) { settings := &config.Settings{} // Register broker with lowercase name - effectiveID, err := registerGlobalProjectAndBroker(ctx, s, "broker-1", "scion-demo", "http://localhost:9800", nil, true, settings) + effectiveID, err := registerGlobalProjectAndBroker(ctx, s, tid("broker-1"), "scion-demo", "http://localhost:9800", nil, true, settings) require.NoError(t, err) - assert.Equal(t, "broker-1", effectiveID) + assert.Equal(t, tid("broker-1"), effectiveID) // Register with different ID and mixed-case name // GetRuntimeBrokerByName uses LOWER() for case-insensitive match - effectiveID, err = registerGlobalProjectAndBroker(ctx, s, "broker-2", "Scion-Demo", "http://localhost:9800", nil, true, settings) + effectiveID, err = registerGlobalProjectAndBroker(ctx, s, tid("broker-2"), "Scion-Demo", "http://localhost:9800", nil, true, settings) require.NoError(t, err) - assert.Equal(t, "broker-1", effectiveID, "should match case-insensitively") + assert.Equal(t, tid("broker-1"), effectiveID, "should match case-insensitively") } diff --git a/pkg/harness/codex/embeds.go b/cmd/tid_test.go similarity index 66% rename from pkg/harness/codex/embeds.go rename to cmd/tid_test.go index 287f6c796..f97f4b6d8 100644 --- a/pkg/harness/codex/embeds.go +++ b/cmd/tid_test.go @@ -12,9 +12,12 @@ // See the License for the specific language governing permissions and // limitations under the License. -package codex +package cmd -import "embed" +import "github.com/google/uuid" -//go:embed all:embeds/* -var EmbedsFS embed.FS +// tid deterministically maps a human-readable test identifier to a stable UUID +// string, so fixtures stay readable while satisfying the UUID-PK Ent store. +func tid(name string) string { + return uuid.NewSHA1(uuid.NameSpaceOID, []byte(name)).String() +} diff --git a/docs-site/astro.config.mjs b/docs-site/astro.config.mjs index 7a3155213..65299806b 100644 --- a/docs-site/astro.config.mjs +++ b/docs-site/astro.config.mjs @@ -74,13 +74,17 @@ export default defineConfig({ { label: 'Secret Management', slug: 'hub-user/secrets' }, { label: 'Runtime Broker', slug: 'hub-user/runtime-broker' }, { label: 'Messaging & Notifications', slug: 'hub-user/messaging' }, + { label: 'External Channels', slug: 'hub-user/external-channels' }, + { label: 'Multi-Broker Setup', slug: 'hub-user/multi-broker' }, ], }, { label: 'Hub Administration', items: [ { label: 'Hub Setup', slug: 'hub-admin/hub-server' }, + { label: 'Hub Setup (GCE)', slug: 'hub-admin/hub-setup-gce' }, { label: 'Kubernetes', slug: 'hub-admin/kubernetes' }, { label: 'Security', slug: 'hub-admin/auth' }, + { label: 'Proxy Auth (IAP)', slug: 'hub-admin/auth-proxy-iap' }, { label: 'Permissions', slug: 'hub-admin/permissions' }, { label: 'Observability', slug: 'hub-admin/observability' }, { label: 'Metrics', slug: 'hub-admin/metrics' }, diff --git a/docs-site/src/content/docs/hub-admin/auth-proxy-iap.md b/docs-site/src/content/docs/hub-admin/auth-proxy-iap.md new file mode 100644 index 000000000..852f22e5b --- /dev/null +++ b/docs-site/src/content/docs/hub-admin/auth-proxy-iap.md @@ -0,0 +1,343 @@ +--- +title: Proxy Auth (Google IAP) +description: Deploying the Scion Hub behind Google IAP with transport auth for agents. +--- + +This guide covers deploying a Scion Hub behind **Google Cloud Identity-Aware Proxy (IAP)**, using IAP for human authentication and hub-minted OIDC tokens for agent transport auth. + +## Authentication modes + +The Hub supports three **mutually exclusive** human authentication modes, selected by `auth.mode`: + +| Mode | Use case | +|------|----------| +| `oauth` (default) | Hub runs its own OAuth flows (Google / GitHub). | +| `proxy` | Hub sits behind a trusted authenticating proxy (Google IAP, Cloudflare Access, etc.). | +| `dev` | Single-user local development with auto-generated dev tokens. | + +Only one mode is active at a time. When `auth.mode` is `proxy`, the OAuth login UI, `/auth/providers`, and device-flow handlers are disabled. Human identity is derived entirely from the proxy's verified assertion. + +Choose **proxy / IAP** when the Hub is already fronted by IAP (e.g., on Cloud Run with IAP enabled, or behind a GCE/GKE IAP-protected backend service) and you want to eliminate a separate OAuth integration. + +## Inbound: human IAP authentication + +### How it works + +1. A user's browser request passes through IAP, which authenticates the user and injects a **signed JWT** in the `X-Goog-IAP-JWT-Assertion` header. +2. The Hub verifies the JWT signature (ES256, via Google's JWKS endpoint), validates `iss`, `aud`, and `exp` claims, then extracts the user's email from the verified assertion. +3. On first verified request, the Hub **provisions** the user — applying the same access controls as the OAuth path (`user_access_mode`, `authorized_domains`, `admin_emails`). If the user is not permitted, the request is rejected with 403. +4. Suspended users are rejected regardless of IAP status. + +The unsigned convenience headers `X-Goog-Authenticated-User-Email` and `X-Goog-Authenticated-User-Id` are **ignored** — only the cryptographically signed assertion is trusted. + +### Middleware precedence + +The proxy authenticator runs **after** higher-priority app-layer credentials: + +1. Agent token (`X-Scion-Agent-Token` / agent JWT) +2. Broker HMAC (`X-Scion-Broker-ID`) +3. Bearer token (dev token / PAT / user JWT) +4. **Proxy authenticator** (IAP assertion) — runs only when no app-layer credential matched + +This means agents and brokers traversing IAP are identified by their own credentials, not by the IAP service-account assertion. + +### Configuration + +In `settings.yaml` (under the `server` key): + +```yaml +server: + auth: + mode: proxy + proxy: + provider: iap + iap: + # MANDATORY — the IAP audience for your backend. + # GCE/GKE backend service format: + # /projects//global/backendServices/ + # App Engine format: + # /projects//apps/ + audience: "/projects/123456789/global/backendServices/987654321" + + # Optional overrides (defaults are correct for production IAP): + # issuer: "https://cloud.google.com/iap" + # jwks_url: "https://www.gstatic.com/iap/verify/public_key-jwk" + + # Optional defense-in-depth: also verify source IP is a trusted proxy. + # Uses the existing trusted_proxies CIDR list. + require_trusted_proxy_ip: false + + # Access controls — same as for OAuth mode: + user_access_mode: domain_restricted # open | domain_restricted | invite_only + authorized_domains: + - example.com + # admin_emails is set at the hub level: + hub: + admin_emails: + - admin@example.com +``` + +#### IAP audience format + +The `audience` value must match the audience claim (`aud`) in the IAP-signed JWT. The format depends on the backend type: + +- **GCE/GKE backend service**: `/projects//global/backendServices/` +- **App Engine**: `/projects//apps/` + +You can find this value in the Google Cloud Console under **Security → Identity-Aware Proxy** → select your backend → **Signed Header JWT Audience**. + +#### Issuer and JWKS overrides + +The defaults match Google's production IAP: + +| Field | Default | +|-------|---------| +| `issuer` | `https://cloud.google.com/iap` | +| `jwks_url` | `https://www.gstatic.com/iap/verify/public_key-jwk` | + +Override these only for testing with a mock IAP issuer. + +### User provisioning + +Provisioning in proxy mode works identically to OAuth — lazy, allow-list-gated, auto-create on first verified request: + +- **`open`**: any verified email is allowed. +- **`domain_restricted`**: email domain must be in `authorized_domains`. +- **`invite_only`**: email must be pre-registered (via admin invite-code flow). +- Emails in `admin_emails` are always allowed and auto-promoted to admin role. +- If not permitted, the request returns **403**. +- Suspended users are rejected even though IAP authenticates them upstream. + +A **60-second resolution cache** (keyed by verified email) avoids a database lookup on every request. The JWT signature is verified on every request — only the provisioning/store lookup is cached. + +### Logout behavior + +In proxy mode, the Hub does not own the session. The `/auth/logout` endpoint: + +- **Browser requests**: redirect to `/_gcp_iap/clear_login_cookie` (IAP's cookie-clearing endpoint). +- **API requests**: return `200 OK` with `{"success": true, "message": "proxy mode: session is managed by the authenticating proxy"}`. + +## Outbound: agent transport auth + +When the Hub is behind IAP (or a Cloud Run invoker-only service), agents need a way to reach the Hub through the platform guard. This is solved with a **dual-layer credential model**: + +| Layer | Header | Purpose | +|-------|--------|---------| +| **Outer (transport)** | `Authorization: Bearer ` | Satisfies the platform guard (IAP or Cloud Run invoker IAM check). | +| **Inner (app)** | `X-Scion-Agent-Token: ` | Existing Hub agent authentication. Carried as a custom header so it never collides with the outer `Authorization`. | + +### How it works + +1. **Cold start (dispatch)**: The Hub mints an initial Google OIDC ID token (impersonating a dedicated transport service account) and includes it in the agent's dispatch payload as environment variables. +2. **Steady-state refresh**: The agent piggybacks on its existing scion-token refresh cycle. The refresh response includes a `tokens[]` array with both the new scion access token and a fresh OIDC transport token. The agent applies each token to the appropriate layer. +3. **Background ticker**: The agent-side client drives refresh on the shortest-lived token (transport tokens have a 5-minute refresh margin vs. the ~1h Google ID token TTL). + +### Dispatch environment variables + +When transport auth is configured, the Hub injects these environment variables into the agent container at dispatch time: + +| Variable | Description | +|----------|-------------| +| `SCION_TRANSPORT_TOKEN` | Initial Google OIDC ID token for the transport layer. | +| `SCION_TRANSPORT_AUDIENCE` | Audience the transport token was minted for (IAP client ID or hub URL). | +| `SCION_TRANSPORT_TOKEN_EXPIRY` | Token expiry in RFC 3339 format. | + +### Refresh response: `tokens[]` array + +The agent token refresh endpoint (`POST /api/v1/agents/{id}/token/refresh`) returns a generalized `tokens[]` array alongside the legacy single-token fields for backward compatibility: + +```json +{ + "token": "...", + "expires_at": "2026-06-05T12:00:00Z", + "tokens": [ + { + "layer": "app", + "type": "scion_access", + "value": "...", + "expiresIn": 900 + }, + { + "layer": "transport", + "type": "google_oidc", + "value": "...", + "expiresIn": 3600, + "audience": "1234567890.apps.googleusercontent.com" + } + ] +} +``` + +The `transport` entry is only present when `auth.transport` is configured on the Hub. Old clients ignore `tokens[]`; new clients consume both layers. + +### Agent-side token source selection + +The agent (`pkg/sciontool/hub`) selects an OIDC token source automatically: + +1. **`SCION_TRANSPORT_TOKEN` env var set** → **Injected mode**: uses the hub-provided token from dispatch, refreshed via `tokens[]` on subsequent refresh calls. +2. **Running on GCP (metadata server available)** → **Metadata mode**: fetches OIDC from the GCE metadata server using the ambient SA identity (the PR #307 pattern). Audience is set via `SCION_HUB_OIDC_AUDIENCE` or defaults to the hub URL. +3. **Neither** → No OIDC transport (agent uses plain HTTP). + +Injected mode (option 1) is the recommended path for IAP deployments — it decouples agent transport auth from the agent's own GCP identity. + +### Transport configuration + +```yaml +server: + auth: + transport: + # Transport auth mode: + # none (default) — no transport tokens issued + # cloudrun_invoker — audience = hub URL + # iap — audience = IAP OAuth client ID + mode: iap + + # OIDC audience for the transport token. + # For IAP: the IAP OAuth client ID (e.g., "1234567890.apps.googleusercontent.com") + # For cloudrun_invoker: the hub URL (auto-derived from hub.public_url if empty) + oidc_audience: "1234567890.apps.googleusercontent.com" + + # Dedicated service account for transport-layer auth. + # The hub's runtime SA impersonates this SA to mint OIDC ID tokens. + platform_auth_sa: "scion-transport@my-project.iam.gserviceaccount.com" +``` + +#### What audience to set + +| Transport mode | `oidc_audience` value | +|---------------|----------------------| +| `iap` | The **IAP OAuth client ID** (found in Cloud Console → Security → IAP → your backend → OAuth client). Format: `.apps.googleusercontent.com` | +| `cloudrun_invoker` | The **Hub's URL** (e.g., `https://hub.example.com`). If left empty, derived from `hub.public_url`. | + +:::note +When both IAP and Cloud Run invoker guards are present on the same service, the IAP service agent carries the Cloud Run invoker role automatically. Agents send a single outer token targeting the IAP audience — no three-layer case. +::: + +### Hub-managed transport SA (Option C) + +The Hub uses a dedicated service account solely for transport-layer auth. The Hub's runtime SA impersonates this SA via the IAM Credentials API (`generateIdToken`) to mint OIDC ID tokens for agents. This design: + +- Keeps the auth-grade minting capability in the Hub only — agents hold no SA credential. +- Works regardless of the agent's GCP metadata mode (`block`, `passthrough`, or `assign`). +- Avoids distributing service account key files. + +**Required IAM bindings:** + +| Principal | Role | Target | +|-----------|------|--------| +| Hub's runtime SA | `roles/iam.serviceAccountTokenCreator` | Transport SA (`platform_auth_sa`) | +| Transport SA | IAP-secured web user **or** Cloud Run invoker | The Hub's backend service | + +## Security notes + +1. **Only the signed assertion is trusted.** The unsigned `X-Goog-Authenticated-User-Email` and `X-Goog-Authenticated-User-Id` headers are completely ignored. +2. **Audience binding is mandatory.** Without it, a JWT minted for a different IAP-protected service would be accepted. The `auth.proxy.iap.audience` field must always be set. +3. **The Hub must be reachable only through IAP for the human surface.** Any path that reaches the Hub directly could bypass proxy authentication. The verified-JWT path is safe against header spoofing (forged assertions fail the signature check), but direct access bypasses IAP entirely. Use VPC networking, firewall rules, or Cloud Run ingress settings to enforce this. +4. **JWKS key rotation** is handled automatically: keys are cached with hourly background refresh and on-miss refresh for rotated key IDs. Transient JWKS endpoint failures are tolerated by serving the last-good key set. +5. **Clock skew** of ±30 seconds is allowed on `exp` and `iat` claims. +6. **Suspended users** are rejected at the provisioning layer even though IAP still authenticates them upstream. + +## End-to-end GCP setup checklist + +### Prerequisites + +- A GCP project with billing enabled. +- The Hub deployed on Cloud Run (or behind a GCE/GKE load balancer). +- `gcloud` CLI configured with appropriate permissions. + +### 1. Enable IAP and create an OAuth consent screen + +```bash +# Enable the IAP API +gcloud services enable iap.googleapis.com + +# Configure the OAuth consent screen (if not already done) +# Go to: Console → APIs & Services → OAuth consent screen +``` + +### 2. Enable IAP on the backend service + +```bash +# For Cloud Run behind a load balancer: +gcloud iap web enable \ + --resource-type=backend-services \ + --service=YOUR_BACKEND_SERVICE_NAME +``` + +Note the **IAP OAuth client ID** (found in Console → Security → IAP → your backend → click the three dots → Edit OAuth Client). You will need it for both `auth.proxy.iap.audience` and `auth.transport.oidc_audience`. + +Note the **signed header JWT audience** (found in Console → Security → IAP → your backend). This goes into `auth.proxy.iap.audience`. + +### 3. Create the transport service account + +```bash +# Create a dedicated SA for transport auth +gcloud iam service-accounts create scion-transport \ + --display-name="Scion Transport Auth" + +# Grant the Hub's runtime SA permission to impersonate the transport SA +gcloud iam service-accounts add-iam-policy-binding \ + scion-transport@PROJECT_ID.iam.gserviceaccount.com \ + --member="serviceAccount:HUB_RUNTIME_SA@PROJECT_ID.iam.gserviceaccount.com" \ + --role="roles/iam.serviceAccountTokenCreator" +``` + +### 4. Grant the transport SA access to the platform guard + +For **IAP**: +```bash +# Grant IAP-secured web user access to the transport SA +gcloud iap web add-iam-policy-binding \ + --resource-type=backend-services \ + --service=YOUR_BACKEND_SERVICE_NAME \ + --member="serviceAccount:scion-transport@PROJECT_ID.iam.gserviceaccount.com" \ + --role="roles/iap.httpsResourceAccessor" +``` + +For **Cloud Run invoker**: +```bash +gcloud run services add-iam-policy-binding YOUR_SERVICE_NAME \ + --member="serviceAccount:scion-transport@PROJECT_ID.iam.gserviceaccount.com" \ + --role="roles/run.invoker" \ + --region=YOUR_REGION +``` + +### 5. Configure the Hub + +Create or update the `settings.yaml`: + +```yaml +schema_version: "1" +server: + mode: hosted + hub: + public_url: "https://hub.example.com" + admin_emails: + - admin@example.com + auth: + mode: proxy + proxy: + provider: iap + iap: + audience: "/projects/123456789/global/backendServices/987654321" + transport: + mode: iap + oidc_audience: "1234567890.apps.googleusercontent.com" + platform_auth_sa: "scion-transport@my-project.iam.gserviceaccount.com" + user_access_mode: domain_restricted + authorized_domains: + - example.com + database: + driver: postgres + url: "postgres://..." +``` + +### 6. Verify + +1. Access the Hub URL in a browser — IAP should prompt for Google login, then the Hub should show your identity. +2. Dispatch an agent and verify it can communicate back to the Hub (check agent logs for OIDC transport messages). +3. Check Hub logs for `Proxy auth configured: provider=iap` and `Transport auth configured: mode=iap` at startup. + +### Reference scripts + +The `scripts/cloudrun/` directory on the `pr/cloudrun-hub` branch contains reference deployment scripts (deploy.sh, entrypoint.sh, hub-settings-template.yaml) for a Cloud Run + IAP topology that can serve as a starting point. diff --git a/docs-site/src/content/docs/hub-admin/hub-setup-gce.md b/docs-site/src/content/docs/hub-admin/hub-setup-gce.md new file mode 100644 index 000000000..eec3f4ea0 --- /dev/null +++ b/docs-site/src/content/docs/hub-admin/hub-setup-gce.md @@ -0,0 +1,76 @@ +--- +title: Hub Setup on GCE +description: Deploy a Scion Hub on a Google Compute Engine VM using the starter scripts. +--- + +## Overview + +The quickest path to a deployed Scion Hub is a single Google Compute Engine VM using the starter scripts in `scripts/starter-hub/`. These scripts automate VM provisioning, repository setup, TLS configuration, and Hub startup. + +## Prerequisites + +- A **GCP project** with billing enabled. +- The **gcloud CLI** installed and configured (`gcloud auth login`, project set). +- A **domain name** (optional but recommended for HTTPS/TLS). + +## Steps + +The starter scripts are designed to be run in sequence from your local machine. + +### 1. Provision the VM + +```bash +./scripts/starter-hub/gce-demo-provision.sh +``` + +Creates a GCE VM with the necessary machine type, disk, firewall rules, and service account. + +### 2. Set Up the Repository + +```bash +./scripts/starter-hub/gce-demo-setup-repo.sh +``` + +SSHs into the VM and clones the Scion repository, installing required dependencies. + +### 3. Build and Deploy + +```bash +./scripts/starter-hub/gce-demo-deploy.sh +``` + +Builds the Hub server and its dependencies on the VM. + +### 4. Configure TLS (Optional) + +```bash +./scripts/starter-hub/gce-certs.sh +``` + +Sets up Caddy as a reverse proxy with automatic TLS certificate provisioning. Requires a domain name pointed at the VM's external IP. + +### 5. Generate Hub Configuration + +```bash +./scripts/starter-hub/hub-config.sh +``` + +Generates the `settings.yaml` file with your chosen options (domain, auth settings, etc.). + +### 6. Start the Hub + +```bash +./scripts/starter-hub/gce-start-hub.sh +``` + +Starts the Hub service on the VM. The Hub is now ready to accept connections. + +## Post-Setup + +Once the Hub is running: + +1. **Access the Web Dashboard** — Navigate to your domain (or the VM's external IP) in a browser. +2. **Create your first project** — Use the dashboard or `scion project create` from the CLI. +3. **Register a Runtime Broker** — Connect a machine to execute agents. See [Runtime Broker](/scion/hub-user/runtime-broker/) for details on registering your local machine or a remote VM. + +For ongoing Hub administration (auth, permissions, observability), see the other guides in the Hub Administration section. diff --git a/docs-site/src/content/docs/hub-user/external-channels.md b/docs-site/src/content/docs/hub-user/external-channels.md new file mode 100644 index 000000000..319cdf6ca --- /dev/null +++ b/docs-site/src/content/docs/hub-user/external-channels.md @@ -0,0 +1,63 @@ +--- +title: External Channels +description: Connect Scion to Telegram, Discord, and A2A for external messaging and notifications. +--- + +## Overview + +Scion can relay agent messages and notifications to external platforms, extending communication beyond the CLI and Web Dashboard. Three channels are available: **Telegram** (bidirectional group chat), **Discord** (outbound webhook notifications), and **A2A protocol** (expose agents as A2A endpoints for programmatic interaction). + +## Telegram + +The Telegram integration provides **bidirectional messaging** — users can message agents from Telegram groups and receive replies directly in the chat. + +### How It Works + +- A Telegram bot (created via [@BotFather](https://core.telegram.org/bots#botfather)) acts as the bridge between Telegram groups and the Scion Hub. +- The bot runs as a Hub plugin (`scion-plugin-telegram`), which must be built and configured in the Hub's `settings.yaml`. +- **Group linking:** Use the `/setup` bot command in a Telegram group to link it to a Scion project. +- **Identity linking:** Use `/register` to associate your Telegram account with your Scion Hub identity. + +### Routing & Commands + +- **@-mention routing:** Mention a specific agent (e.g., `@mybot agent-name message`) to route a message to that agent. +- **Default agent:** Set a default agent with `/default` so untagged messages route automatically. +- Available bot commands: `/agents` (list agents), `/default` (set default), `/settings` (configure group), `/notifications` (toggle notification types). + +### Group Settings + +Each linked group can be configured via `/settings`: + +- **Observer mode (`a2a`):** Show agent-to-agent messages in the group, so you can watch how agents coordinate. +- **Commentary:** Show agent reply messages (responses to other agents) in the group. +- **Group notifications (`grp`):** Post agent state change notifications (completed, error, waiting for input) in the group chat. + +For full setup instructions, bot configuration, and troubleshooting, see [extras/scion-telegram/README.md](https://github.com/GoogleCloudPlatform/scion/tree/main/extras/scion-telegram). + +## Discord + +Discord integration provides **outbound-only** webhook notifications — agents can push messages to a Discord channel, but cannot receive inbound messages from Discord. + +- **Severity-based color coding:** Messages are color-coded by severity (info, warning, error, urgent). +- **@mentions:** Urgent messages and explicit `ask_user` requests can trigger `@user` or `@role` mentions. + +### Configuration + +Set the webhook URL in one of two ways: + +- **settings.yaml:** Set `server.discord_webhook_url` in the Hub configuration. +- **Environment variable:** Set `SCION_DISCORD_WEBHOOK_URL`. + +For more details, see [Hub Setup — Discord Integration](/scion/hub-admin/hub-server/#discord-integration). + +## A2A Protocol Bridge + +The A2A (Agent-to-Agent protocol) bridge exposes Scion agents as **standard A2A endpoints**, allowing external A2A clients to discover and interact with them programmatically. + +- **Discovery:** External clients can query available agents and their capabilities via the A2A protocol. +- **Interaction modes:** Supports blocking (synchronous), SSE streaming, and push notification delivery. +- **Standalone service:** Runs as a separate bridge process alongside the Hub (see `extras/scion-a2a-bridge`). + +This is useful for integrating Scion agents into larger multi-agent systems or exposing them to third-party A2A-compatible clients. + +For setup and configuration, see [extras/scion-a2a-bridge/README.md](https://github.com/GoogleCloudPlatform/scion/tree/main/extras/scion-a2a-bridge). diff --git a/docs-site/src/content/docs/hub-user/multi-broker.md b/docs-site/src/content/docs/hub-user/multi-broker.md new file mode 100644 index 000000000..ed6d8e47e --- /dev/null +++ b/docs-site/src/content/docs/hub-user/multi-broker.md @@ -0,0 +1,59 @@ +--- +title: Multi-Broker Setup +description: Connect multiple machines to a single Scion Hub for distributed agent execution. +--- + +## Overview + +A single Scion Hub can dispatch agents to **multiple Runtime Brokers**. Each broker is a machine — a laptop, cloud VM, or Kubernetes cluster — that runs agent containers. This lets teams pool compute resources and target specific machines for specific workloads. + +## Architecture + +``` + ┌──────────┐ + ┌────────────┤ Scion Hub├────────────┐ + │ └────┬─────┘ │ + │ │ │ + ┌────▼─────┐ ┌──────▼───┐ ┌────────▼──────┐ + │ Broker A │ │ Broker B │ │ Broker C │ + │ (laptop) │ │(cloud VM)│ │ (K8s cluster) │ + └───────────┘ └──────────┘ └───────────────┘ +``` + +Each broker maintains a persistent WebSocket connection to the Hub. The Hub acts as the control plane; brokers handle container execution locally. + +## Adding a Broker + +On each machine you want to register: + +1. **Install Scion** and configure the Hub endpoint (`scion login`). +2. **Register the broker** with the Hub: + ```bash + scion broker register + ``` +3. **Authorize projects** the broker should serve: + ```bash + scion broker provide + ``` + +Repeat for each machine. See [Runtime Broker](/scion/hub-user/runtime-broker/) for detailed setup. + +## Broker Selection + +When starting an agent, the Hub selects an available broker automatically. You can override this: + +- **Target a specific broker** with the `--broker` flag: + ```bash + scion start --broker my-cloud-vm + ``` +- **Check broker availability** across all registered brokers: + ```bash + scion broker status + ``` + +## Considerations + +- Each broker manages its own **port pools, container images, and local storage**. Images must be available on each broker independently. +- **Shared directories** (mounted volumes) only work within a single broker — agents on different brokers cannot share a local directory. +- **Workspace strategy** may differ per broker: local brokers typically use git worktrees (`.scion_worktrees/`), while hub-hosted git projects use a single workspace checkout. +- Broker capacity is determined by the machine's resources. The Hub does not enforce cross-broker resource limits. diff --git a/extras/agent-viz/README.md b/extras/agent-viz/README.md index 2f098511d..ae2117e07 100644 --- a/extras/agent-viz/README.md +++ b/extras/agent-viz/README.md @@ -7,6 +7,7 @@ A standalone tool that replays agent activity from Google Cloud Logging exports - **File graph** -- force-directed graph of the project's file/directory tree (center) - **Agent ring** -- agents distributed radially around the file graph, with color-coded state icons - **Messages** -- transient directional pulse lines between agents, fading after ~0.5s +- **Agent Communications panel** -- right-side scrolling transcript of inter-agent messages, kept in sync with playback (and rebuilt on seek); broadcasts are highlighted and de-duplicated. Collapse it with the −/+ button. It reads the same `message` events that drive the on-graph pulses, so no extra data source is needed. - **File edits** -- particles traveling from agent to file node; new files materialize with an expand effect - **Playback controls** -- play/pause, speed (1x--100x), time scrubber, agent and event type filters diff --git a/extras/agent-viz/web/index.html b/extras/agent-viz/web/index.html index b07a3f451..452d5b7ac 100644 --- a/extras/agent-viz/web/index.html +++ b/extras/agent-viz/web/index.html @@ -195,6 +195,162 @@ flex-direction: column; gap: 2px; } + + /* Agent Communications transcript panel */ + .comms-panel { + position: fixed; + top: 52px; + right: 12px; + bottom: 110px; + width: 340px; + max-width: calc(100vw - 24px); + display: flex; + flex-direction: column; + background: rgba(22, 27, 34, 0.92); + border: 1px solid rgba(255,255,255,0.1); + border-radius: 10px; + box-shadow: 0 8px 30px rgba(0,0,0,0.45); + backdrop-filter: blur(8px); + -webkit-backdrop-filter: blur(8px); + overflow: hidden; + z-index: 15; + } + + .comms-panel.collapsed { + bottom: auto; + } + + .comms-header { + display: flex; + align-items: center; + justify-content: space-between; + padding: 8px 12px; + background: rgba(255,255,255,0.03); + border-bottom: 1px solid rgba(255,255,255,0.08); + flex-shrink: 0; + } + + .comms-title { + font-size: 12px; + font-weight: 700; + color: #58a6ff; + letter-spacing: 0.3px; + } + + .comms-subtitle { + font-size: 10px; + color: rgba(255,255,255,0.45); + margin-top: 1px; + } + + .comms-toggle { + background: rgba(255,255,255,0.08); + border: 1px solid rgba(255,255,255,0.12); + color: #c9d1d9; + width: 24px; + height: 24px; + border-radius: 6px; + font-size: 15px; + line-height: 1; + cursor: pointer; + flex-shrink: 0; + } + + .comms-toggle:hover { + background: rgba(255,255,255,0.16); + } + + .comms-panel.collapsed .comms-body { + display: none; + } + + .comms-body { + flex: 1; + overflow-y: auto; + padding: 10px 12px; + scrollbar-width: thin; + scrollbar-color: rgba(255,255,255,0.18) transparent; + } + + .comms-msg { + padding: 8px 10px; + margin-bottom: 7px; + background: rgba(255,255,255,0.03); + border-left: 3px solid #888; + border-radius: 6px; + animation: commsFadeIn 0.32s ease-out; + } + + .comms-msg-bcast { + background: rgba(34,197,94,0.10); + box-shadow: 0 0 14px rgba(34,197,94,0.14); + } + + .comms-msg-meta { + display: flex; + align-items: center; + gap: 6px; + margin-bottom: 4px; + } + + .comms-time { + font-size: 10px; + font-weight: 600; + color: rgba(255,255,255,0.5); + font-variant-numeric: tabular-nums; + } + + .comms-index { + margin-left: auto; + font-size: 9px; + color: rgba(255,255,255,0.3); + } + + .comms-badge { + background: rgba(34,197,94,0.2); + color: #86efac; + font-size: 8px; + font-weight: 700; + letter-spacing: 0.5px; + padding: 1px 5px; + border-radius: 3px; + } + + .comms-route { + font-size: 11px; + font-weight: 600; + margin-bottom: 3px; + display: flex; + align-items: center; + gap: 5px; + flex-wrap: wrap; + } + + .comms-arrow { + color: rgba(255,255,255,0.4); + } + + .comms-type { + font-size: 9px; + font-weight: 500; + color: rgba(255,255,255,0.4); + background: rgba(255,255,255,0.06); + padding: 1px 5px; + border-radius: 3px; + margin-left: auto; + } + + .comms-content { + font-size: 11.5px; + line-height: 1.45; + color: #c9d1d9; + word-break: break-word; + } + + @keyframes commsFadeIn { + from { opacity: 0; transform: translateY(6px); } + to { opacity: 1; transform: translateY(0); } + } diff --git a/extras/agent-viz/web/package-lock.json b/extras/agent-viz/web/package-lock.json index 31d195e21..f8942b7dd 100644 --- a/extras/agent-viz/web/package-lock.json +++ b/extras/agent-viz/web/package-lock.json @@ -1273,9 +1273,9 @@ } }, "node_modules/postcss": { - "version": "8.5.8", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.8.tgz", - "integrity": "sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg==", + "version": "8.5.12", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.12.tgz", + "integrity": "sha512-W62t/Se6rA0Az3DfCL0AqJwXuKwBeYg6nOaIgzP+xZ7N5BFCI7DYi1qs6ygUYT6rvfi6t9k65UMLJC+PHZpDAA==", "dev": true, "funding": [ { diff --git a/extras/agent-viz/web/src/comms.ts b/extras/agent-viz/web/src/comms.ts new file mode 100644 index 000000000..57d01ac54 --- /dev/null +++ b/extras/agent-viz/web/src/comms.ts @@ -0,0 +1,200 @@ +/** + * Copyright 2026 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import type { MessageEvent } from './types'; +import type { AgentRing } from './agents'; + +/** + * Window (in event-time ms) for collapsing duplicate broadcast deliveries into a + * single transcript line. A broadcast is logged once per recipient, so the same + * sender+content arrives several times within a short span. + */ +const BROADCAST_DEDUP_WINDOW_MS = 2000; + +interface AddOptions { + /** + * When false the card is inserted without the fade-in animation. Used while + * replaying a snapshot on seek, where every prior message arrives at once. + */ + animate?: boolean; +} + +/** + * CommsPanel renders a scrolling, human-readable transcript of inter-agent + * messages next to the force-graph. It consumes the same `message` playback + * events that drive the on-graph pulse lines, so it stays in sync with playback + * and is rebuilt from the snapshot on seek. It needs no external data source — + * the messages already flow through the playback stream. + */ +export class CommsPanel { + private readonly bodyEl: HTMLElement; + private readonly countEl: HTMLElement; + private readonly toggleEl: HTMLButtonElement; + private readonly panelEl: HTMLElement; + private agentRing: AgentRing | null = null; + private startMs: number | null = null; + private count = 0; + private collapsed = false; + /** Pending requestAnimationFrame handle for the deferred scroll-to-bottom. */ + private scrollRafId: number | null = null; + /** Dedup state for broadcasts: `sender::content` -> last event time (ms). */ + private readonly recentBroadcasts = new Map(); + + constructor(parent: HTMLElement = document.body) { + const panel = document.createElement('div'); + panel.className = 'comms-panel'; + panel.innerHTML = ` +
+
+
Agent Communications
+
0 messages
+
+ +
+
+ `; + parent.appendChild(panel); + + this.panelEl = panel; + this.bodyEl = panel.querySelector('.comms-body') as HTMLElement; + this.countEl = panel.querySelector('.comms-count') as HTMLElement; + this.toggleEl = panel.querySelector('.comms-toggle') as HTMLButtonElement; + + this.toggleEl.addEventListener('click', () => this.setCollapsed(!this.collapsed)); + } + + setAgentRing(ring: AgentRing): void { + this.agentRing = ring; + } + + /** Anchor for relative `T+m:ss` timestamps (the playback start time). */ + setStartTime(iso: string): void { + const ms = Date.parse(iso); + this.startMs = Number.isNaN(ms) ? null : ms; + } + + /** Clear the transcript (called on a new manifest and before snapshot replay). */ + reset(): void { + if (this.scrollRafId !== null) { + cancelAnimationFrame(this.scrollRafId); + this.scrollRafId = null; + } + this.bodyEl.innerHTML = ''; + this.count = 0; + this.recentBroadcasts.clear(); + this.updateCount(); + } + + addMessage(event: MessageEvent, timestamp: string, opts: AddOptions = {}): void { + // Collapse duplicate broadcast deliveries (same sender+content) into one line. + if (event.broadcasted) { + const key = `${event.sender}::${event.content ?? ''}`; + const t = Date.parse(timestamp); + const prev = this.recentBroadcasts.get(key); + if (prev !== undefined && Math.abs(t - prev) < BROADCAST_DEDUP_WINDOW_MS) return; + this.recentBroadcasts.set(key, t); + } + + const animate = opts.animate ?? true; + + // Only auto-scroll when the user is already near the bottom, so manual + // scroll-back to read history isn't yanked away by new arrivals. Skip the + // layout-reading measurement during non-animated batch loads (snapshot + // replay on seek): interleaving these reads with appendChild in that loop + // would cause layout thrashing. + const nearBottom = + animate && + this.bodyEl.scrollTop + this.bodyEl.clientHeight >= this.bodyEl.scrollHeight - 60; + + this.bodyEl.appendChild(this.makeCard(event, timestamp, animate)); + this.count++; + this.updateCount(); + + if (animate) { + if (nearBottom) this.bodyEl.scrollTop = this.bodyEl.scrollHeight; + } else { + // Defer a single scroll-to-bottom until after the synchronous replay loop. + if (this.scrollRafId !== null) cancelAnimationFrame(this.scrollRafId); + this.scrollRafId = requestAnimationFrame(() => { + this.bodyEl.scrollTop = this.bodyEl.scrollHeight; + this.scrollRafId = null; + }); + } + } + + private setCollapsed(collapsed: boolean): void { + this.collapsed = collapsed; + this.panelEl.classList.toggle('collapsed', collapsed); + this.toggleEl.innerHTML = collapsed ? '+' : '−'; + } + + private updateCount(): void { + this.countEl.textContent = String(this.count); + } + + private makeCard(event: MessageEvent, timestamp: string, animate: boolean): HTMLElement { + const senderColor = this.agentRing?.getAgentColor(event.sender) ?? '#888'; + const broadcast = event.broadcasted; + const recipientColor = broadcast + ? '#22c55e' + : (this.agentRing?.getAgentColor(event.recipient) ?? '#888'); + const accent = broadcast ? '#22c55e' : senderColor; + + const card = document.createElement('div'); + card.className = broadcast ? 'comms-msg comms-msg-bcast' : 'comms-msg'; + card.style.borderLeftColor = accent; + if (!animate) card.style.animation = 'none'; + + const recipientLabel = broadcast ? 'ALL' : event.recipient || '?'; + const arrow = broadcast ? '↯' : '→'; + const typeTag = event.msgType + ? `${escapeHtml(event.msgType)}` + : ''; + const bcastBadge = broadcast ? 'BROADCAST' : ''; + + card.innerHTML = ` +
+ ${this.formatTime(timestamp)} + ${bcastBadge} + #${this.count + 1} +
+
+ ${escapeHtml(event.sender || '?')} + ${arrow} + ${escapeHtml(recipientLabel)} + ${typeTag} +
+
${escapeHtml(event.content ?? '')}
+ `; + return card; + } + + private formatTime(timestamp: string): string { + const t = Date.parse(timestamp); + if (Number.isNaN(t)) return ''; + if (this.startMs !== null) { + const sec = Math.max(0, Math.floor((t - this.startMs) / 1000)); + const m = Math.floor(sec / 60); + const s = sec % 60; + return `T+${m}:${String(s).padStart(2, '0')}`; + } + return new Date(t).toLocaleTimeString(); + } +} + +function escapeHtml(s: string): string { + return s.replace(/&/g, '&').replace(//g, '>'); +} diff --git a/extras/agent-viz/web/src/main.ts b/extras/agent-viz/web/src/main.ts index f0c424290..9982bf356 100644 --- a/extras/agent-viz/web/src/main.ts +++ b/extras/agent-viz/web/src/main.ts @@ -22,6 +22,7 @@ import { FileEditRenderer } from './files'; import { DestroyBeamRenderer } from './destroy-beam'; import { CreateBeamRenderer } from './create-beam'; import { PlaybackControls } from './playback'; +import { CommsPanel } from './comms'; import type { PlaybackManifest, PlaybackEvent, @@ -41,6 +42,7 @@ let fileEditRenderer: FileEditRenderer; let destroyBeamRenderer: DestroyBeamRenderer; let createBeamRenderer: CreateBeamRenderer; let playbackControls: PlaybackControls; +let commsPanel: CommsPanel; let overlayCanvas: HTMLCanvasElement; let overlayCtx: CanvasRenderingContext2D; let animFrameId: number; @@ -87,6 +89,10 @@ function init(): void { destroyBeamRenderer.setAgentRing(agentRing); createBeamRenderer.setAgentRing(agentRing); + // Agent Communications transcript panel — consumes the same message events. + commsPanel = new CommsPanel(); + commsPanel.setAgentRing(agentRing); + // WebSocket const ws = new WSClient(); playbackControls = new PlaybackControls(controlsContainer, ws); @@ -157,6 +163,10 @@ function handleManifest(m: PlaybackManifest): void { playbackControls.setTimeRange(m.timeRange.start, m.timeRange.end); playbackControls.setAgents(m.agents); + // Anchor relative timestamps in the communications panel to playback start. + commsPanel.setStartTime(m.timeRange.start); + commsPanel.reset(); + // Update info display updateInfoDisplay(); } @@ -187,6 +197,7 @@ function resetState(): void { fileEditRenderer.reset(); destroyBeamRenderer.reset(); createBeamRenderer.reset(); + commsPanel.reset(); // Re-init empty state const w = overlayCanvas.width; @@ -201,7 +212,9 @@ function handleEventInstant(evt: PlaybackEvent): void { agentRing.updateState(evt.data as AgentStateEvent); break; case 'message': - // Skip message animations during replay + // Skip the on-graph pulse animation during replay, but still record the + // message in the transcript so the panel reflects the seek position. + commsPanel.addMessage(evt.data as MessageEvent, evt.timestamp, { animate: false }); break; case 'file_edit': case 'file_read': { @@ -248,6 +261,7 @@ function handleEvent(evt: PlaybackEvent): void { break; case 'message': messageRenderer.addMessage(evt.data as MessageEvent, agentRing); + commsPanel.addMessage(evt.data as MessageEvent, evt.timestamp); break; case 'file_edit': case 'file_read': { diff --git a/extras/scion-a2a-bridge/cmd/scion-a2a-bridge/main.go b/extras/scion-a2a-bridge/cmd/scion-a2a-bridge/main.go index d75579504..03be703e5 100644 --- a/extras/scion-a2a-bridge/cmd/scion-a2a-bridge/main.go +++ b/extras/scion-a2a-bridge/cmd/scion-a2a-bridge/main.go @@ -22,6 +22,7 @@ import ( "flag" "fmt" "log/slog" + "net" "net/http" "os" "os/signal" @@ -32,6 +33,7 @@ import ( secretmanager "cloud.google.com/go/secretmanager/apiv1" smpb "cloud.google.com/go/secretmanager/apiv1/secretmanagerpb" "github.com/prometheus/client_golang/prometheus" + "google.golang.org/grpc" "gopkg.in/yaml.v3" "github.com/GoogleCloudPlatform/scion/extras/scion-a2a-bridge/internal/bridge" @@ -154,7 +156,7 @@ func main() { MaxHeaderBytes: 1 << 20, } - errCh := make(chan error, 1) + errCh := make(chan error, 2) go func() { log.Warn("A2A server starting WITHOUT TLS — ensure TLS is terminated at a reverse proxy (e.g. Caddy, nginx, cloud LB)", "address", listenAddr) log.Info("A2A protocol server starting", "address", listenAddr) @@ -163,6 +165,27 @@ func main() { } }() + // Start gRPC server if configured. + var grpcServer *grpc.Server + if cfg.Bridge.GRPCListenAddress != "" { + grpcServer = grpc.NewServer() + grpcSrv := bridge.NewGRPCServer(b, cfg, log.With("component", "grpc-server")) + grpcSrv.Register(grpcServer) + + grpcLis, err := net.Listen("tcp", cfg.Bridge.GRPCListenAddress) + if err != nil { + log.Error("failed to listen for gRPC", "address", cfg.Bridge.GRPCListenAddress, "error", err) + os.Exit(1) + } + + go func() { + log.Info("gRPC server starting", "address", cfg.Bridge.GRPCListenAddress) + if err := grpcServer.Serve(grpcLis); err != nil { + errCh <- fmt.Errorf("grpc server: %w", err) + } + }() + } + log.Info("scion-a2a-bridge ready") // Wait for shutdown signal. @@ -183,6 +206,11 @@ func main() { log.Error("failed to stop A2A server", "error", err) } + if grpcServer != nil { + grpcServer.GracefulStop() + log.Info("gRPC server stopped") + } + // Drain background goroutines before closing the store. b.Shutdown() diff --git a/extras/scion-a2a-bridge/go.mod b/extras/scion-a2a-bridge/go.mod index 4efeb1a5f..1f0509ca7 100644 --- a/extras/scion-a2a-bridge/go.mod +++ b/extras/scion-a2a-bridge/go.mod @@ -1,6 +1,6 @@ module github.com/GoogleCloudPlatform/scion/extras/scion-a2a-bridge -go 1.25.4 +go 1.26.1 require ( cloud.google.com/go/secretmanager v1.16.0 @@ -9,6 +9,11 @@ require ( github.com/google/uuid v1.6.0 github.com/hashicorp/go-plugin v1.7.0 github.com/mattn/go-sqlite3 v1.14.28 + github.com/prometheus/client_golang v1.23.2 + github.com/prometheus/client_model v0.6.2 + google.golang.org/genproto/googleapis/api v0.0.0-20260401024825-9d38bb4040a9 + google.golang.org/grpc v1.80.0 + google.golang.org/protobuf v1.36.11 gopkg.in/yaml.v3 v3.0.1 ) @@ -33,8 +38,6 @@ require ( github.com/mattn/go-isatty v0.0.20 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect github.com/oklog/run v1.1.0 // indirect - github.com/prometheus/client_golang v1.23.2 // indirect - github.com/prometheus/client_model v0.6.2 // indirect github.com/prometheus/common v0.67.2 // indirect github.com/prometheus/procfs v0.19.2 // indirect go.opentelemetry.io/auto/sdk v1.2.1 // indirect @@ -54,10 +57,7 @@ require ( golang.org/x/time v0.14.0 // indirect google.golang.org/api v0.259.0 // indirect google.golang.org/genproto v0.0.0-20251202230838-ff82c1b0f217 // indirect - google.golang.org/genproto/googleapis/api v0.0.0-20260401024825-9d38bb4040a9 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20260401024825-9d38bb4040a9 // indirect - google.golang.org/grpc v1.80.0 // indirect - google.golang.org/protobuf v1.36.11 // indirect ) replace github.com/GoogleCloudPlatform/scion => ../../ diff --git a/extras/scion-a2a-bridge/go.sum b/extras/scion-a2a-bridge/go.sum index db04107a4..b7f2c7d7f 100644 --- a/extras/scion-a2a-bridge/go.sum +++ b/extras/scion-a2a-bridge/go.sum @@ -59,10 +59,14 @@ github.com/hashicorp/yamux v0.1.2 h1:XtB8kyFOyHXYVFnwT5C3+Bdo8gArse7j2AQ0DA0Uey8 github.com/hashicorp/yamux v0.1.2/go.mod h1:C+zze2n6e/7wshOZep2A70/aQU6QBRWJO/G6FT1wIns= github.com/jhump/protoreflect v1.17.0 h1:qOEr613fac2lOuTgWN4tPAtLL7fUSbuJL5X5XumQh94= github.com/jhump/protoreflect v1.17.0/go.mod h1:h9+vUUL38jiBzck8ck+6G/aeMX8Z4QUY/NiJPwPNi+8= +github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= +github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= +github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4= github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE= @@ -112,6 +116,8 @@ go.opentelemetry.io/otel/sdk/metric v1.43.0 h1:S88dyqXjJkuBNLeMcVPRFXpRw2fuwdvfC go.opentelemetry.io/otel/sdk/metric v1.43.0/go.mod h1:C/RJtwSEJ5hzTiUz5pXF1kILHStzb9zFlIEe85bhj6A= go.opentelemetry.io/otel/trace v1.43.0 h1:BkNrHpup+4k4w+ZZ86CZoHHEkohws8AY+WTX09nk+3A= go.opentelemetry.io/otel/trace v1.43.0/go.mod h1:/QJhyVBUUswCphDVxq+8mld+AvhXZLhe+8WVFxiFff0= +go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= +go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= go.yaml.in/yaml/v2 v2.4.3 h1:6gvOSjQoTB3vt1l+CU+tSyi/HOjfOjRLJ4YwYZGwRO0= go.yaml.in/yaml/v2 v2.4.3/go.mod h1:zSxWcmIDjOzPXpjlTTbAsKokqkDNAVtZO0WOMiT90s8= golang.org/x/crypto v0.49.0 h1:+Ng2ULVvLHnJ/ZFEq4KdcDd/cfjrrjjNSXNzxg0Y4U4= diff --git a/extras/scion-a2a-bridge/internal/a2apb/a2a.pb.go b/extras/scion-a2a-bridge/internal/a2apb/a2a.pb.go new file mode 100644 index 000000000..23c233bdf --- /dev/null +++ b/extras/scion-a2a-bridge/internal/a2apb/a2a.pb.go @@ -0,0 +1,4263 @@ +// Older protoc compilers don't understand edition yet. + +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.36.11 +// protoc v3.21.12 +// source: a2a.proto + +package a2apb + +import ( + _ "google.golang.org/genproto/googleapis/api/annotations" + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + emptypb "google.golang.org/protobuf/types/known/emptypb" + structpb "google.golang.org/protobuf/types/known/structpb" + timestamppb "google.golang.org/protobuf/types/known/timestamppb" + reflect "reflect" + sync "sync" + unsafe "unsafe" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +// Defines the possible lifecycle states of a `Task`. +type TaskState int32 + +const ( + // The task is in an unknown or indeterminate state. + TaskState_TASK_STATE_UNSPECIFIED TaskState = 0 + // Indicates that a task has been successfully submitted and acknowledged. + TaskState_TASK_STATE_SUBMITTED TaskState = 1 + // Indicates that a task is actively being processed by the agent. + TaskState_TASK_STATE_WORKING TaskState = 2 + // Indicates that a task has finished successfully. This is a terminal state. + TaskState_TASK_STATE_COMPLETED TaskState = 3 + // Indicates that a task has finished with an error. This is a terminal state. + TaskState_TASK_STATE_FAILED TaskState = 4 + // Indicates that a task was canceled before completion. This is a terminal state. + TaskState_TASK_STATE_CANCELED TaskState = 5 + // Indicates that the agent requires additional user input to proceed. This is an interrupted state. + TaskState_TASK_STATE_INPUT_REQUIRED TaskState = 6 + // Indicates that the agent has decided to not perform the task. + // This may be done during initial task creation or later once an agent + // has determined it can't or won't proceed. This is a terminal state. + TaskState_TASK_STATE_REJECTED TaskState = 7 + // Indicates that authentication is required to proceed. This is an interrupted state. + TaskState_TASK_STATE_AUTH_REQUIRED TaskState = 8 +) + +// Enum value maps for TaskState. +var ( + TaskState_name = map[int32]string{ + 0: "TASK_STATE_UNSPECIFIED", + 1: "TASK_STATE_SUBMITTED", + 2: "TASK_STATE_WORKING", + 3: "TASK_STATE_COMPLETED", + 4: "TASK_STATE_FAILED", + 5: "TASK_STATE_CANCELED", + 6: "TASK_STATE_INPUT_REQUIRED", + 7: "TASK_STATE_REJECTED", + 8: "TASK_STATE_AUTH_REQUIRED", + } + TaskState_value = map[string]int32{ + "TASK_STATE_UNSPECIFIED": 0, + "TASK_STATE_SUBMITTED": 1, + "TASK_STATE_WORKING": 2, + "TASK_STATE_COMPLETED": 3, + "TASK_STATE_FAILED": 4, + "TASK_STATE_CANCELED": 5, + "TASK_STATE_INPUT_REQUIRED": 6, + "TASK_STATE_REJECTED": 7, + "TASK_STATE_AUTH_REQUIRED": 8, + } +) + +func (x TaskState) Enum() *TaskState { + p := new(TaskState) + *p = x + return p +} + +func (x TaskState) String() string { + return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) +} + +func (TaskState) Descriptor() protoreflect.EnumDescriptor { + return file_a2a_proto_enumTypes[0].Descriptor() +} + +func (TaskState) Type() protoreflect.EnumType { + return &file_a2a_proto_enumTypes[0] +} + +func (x TaskState) Number() protoreflect.EnumNumber { + return protoreflect.EnumNumber(x) +} + +// Deprecated: Use TaskState.Descriptor instead. +func (TaskState) EnumDescriptor() ([]byte, []int) { + return file_a2a_proto_rawDescGZIP(), []int{0} +} + +// Defines the sender of a message in A2A protocol communication. +type Role int32 + +const ( + // The role is unspecified. + Role_ROLE_UNSPECIFIED Role = 0 + // The message is from the client to the server. + Role_ROLE_USER Role = 1 + // The message is from the server to the client. + Role_ROLE_AGENT Role = 2 +) + +// Enum value maps for Role. +var ( + Role_name = map[int32]string{ + 0: "ROLE_UNSPECIFIED", + 1: "ROLE_USER", + 2: "ROLE_AGENT", + } + Role_value = map[string]int32{ + "ROLE_UNSPECIFIED": 0, + "ROLE_USER": 1, + "ROLE_AGENT": 2, + } +) + +func (x Role) Enum() *Role { + p := new(Role) + *p = x + return p +} + +func (x Role) String() string { + return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) +} + +func (Role) Descriptor() protoreflect.EnumDescriptor { + return file_a2a_proto_enumTypes[1].Descriptor() +} + +func (Role) Type() protoreflect.EnumType { + return &file_a2a_proto_enumTypes[1] +} + +func (x Role) Number() protoreflect.EnumNumber { + return protoreflect.EnumNumber(x) +} + +// Deprecated: Use Role.Descriptor instead. +func (Role) EnumDescriptor() ([]byte, []int) { + return file_a2a_proto_rawDescGZIP(), []int{1} +} + +// Configuration of a send message request. +type SendMessageConfiguration struct { + state protoimpl.MessageState `protogen:"open.v1"` + // A list of media types the client is prepared to accept for response parts. + // Agents SHOULD use this to tailor their output. + AcceptedOutputModes []string `protobuf:"bytes,1,rep,name=accepted_output_modes,json=acceptedOutputModes,proto3" json:"accepted_output_modes,omitempty"` + // Configuration for the agent to send push notifications for task updates. + // Task id should be empty when sending this configuration in a `SendMessage` request. + TaskPushNotificationConfig *TaskPushNotificationConfig `protobuf:"bytes,2,opt,name=task_push_notification_config,json=taskPushNotificationConfig,proto3" json:"task_push_notification_config,omitempty"` + // The maximum number of most recent messages from the task's history to retrieve in + // the response. An unset value means the client does not impose any limit. A + // value of zero is a request to not include any messages. The server MUST NOT + // return more messages than the provided value, but MAY apply a lower limit. + HistoryLength *int32 `protobuf:"varint,3,opt,name=history_length,json=historyLength,proto3,oneof" json:"history_length,omitempty"` + // If `true`, the operation returns immediately after creating the task, + // even if processing is still in progress. + // If `false` (default), the operation MUST wait until the task reaches a + // terminal (`COMPLETED`, `FAILED`, `CANCELED`, `REJECTED`) or interrupted + // (`INPUT_REQUIRED`, `AUTH_REQUIRED`) state before returning. + ReturnImmediately bool `protobuf:"varint,4,opt,name=return_immediately,json=returnImmediately,proto3" json:"return_immediately,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *SendMessageConfiguration) Reset() { + *x = SendMessageConfiguration{} + mi := &file_a2a_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *SendMessageConfiguration) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*SendMessageConfiguration) ProtoMessage() {} + +func (x *SendMessageConfiguration) ProtoReflect() protoreflect.Message { + mi := &file_a2a_proto_msgTypes[0] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use SendMessageConfiguration.ProtoReflect.Descriptor instead. +func (*SendMessageConfiguration) Descriptor() ([]byte, []int) { + return file_a2a_proto_rawDescGZIP(), []int{0} +} + +func (x *SendMessageConfiguration) GetAcceptedOutputModes() []string { + if x != nil { + return x.AcceptedOutputModes + } + return nil +} + +func (x *SendMessageConfiguration) GetTaskPushNotificationConfig() *TaskPushNotificationConfig { + if x != nil { + return x.TaskPushNotificationConfig + } + return nil +} + +func (x *SendMessageConfiguration) GetHistoryLength() int32 { + if x != nil && x.HistoryLength != nil { + return *x.HistoryLength + } + return 0 +} + +func (x *SendMessageConfiguration) GetReturnImmediately() bool { + if x != nil { + return x.ReturnImmediately + } + return false +} + +// `Task` is the core unit of action for A2A. It has a current status +// and when results are created for the task they are stored in the +// artifact. If there are multiple turns for a task, these are stored in +// history. +type Task struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Unique identifier (e.g. UUID) for the task, generated by the server for a + // new task. + Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"` + // Unique identifier (e.g. UUID) for the contextual collection of interactions + // (tasks and messages). + ContextId string `protobuf:"bytes,2,opt,name=context_id,json=contextId,proto3" json:"context_id,omitempty"` + // The current status of a `Task`, including `state` and a `message`. + Status *TaskStatus `protobuf:"bytes,3,opt,name=status,proto3" json:"status,omitempty"` + // A set of output artifacts for a `Task`. + Artifacts []*Artifact `protobuf:"bytes,4,rep,name=artifacts,proto3" json:"artifacts,omitempty"` + // protolint:disable REPEATED_FIELD_NAMES_PLURALIZED + // The history of interactions from a `Task`. + History []*Message `protobuf:"bytes,5,rep,name=history,proto3" json:"history,omitempty"` + // protolint:enable REPEATED_FIELD_NAMES_PLURALIZED + // A key/value object to store custom metadata about a task. + Metadata *structpb.Struct `protobuf:"bytes,6,opt,name=metadata,proto3" json:"metadata,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *Task) Reset() { + *x = Task{} + mi := &file_a2a_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *Task) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Task) ProtoMessage() {} + +func (x *Task) ProtoReflect() protoreflect.Message { + mi := &file_a2a_proto_msgTypes[1] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Task.ProtoReflect.Descriptor instead. +func (*Task) Descriptor() ([]byte, []int) { + return file_a2a_proto_rawDescGZIP(), []int{1} +} + +func (x *Task) GetId() string { + if x != nil { + return x.Id + } + return "" +} + +func (x *Task) GetContextId() string { + if x != nil { + return x.ContextId + } + return "" +} + +func (x *Task) GetStatus() *TaskStatus { + if x != nil { + return x.Status + } + return nil +} + +func (x *Task) GetArtifacts() []*Artifact { + if x != nil { + return x.Artifacts + } + return nil +} + +func (x *Task) GetHistory() []*Message { + if x != nil { + return x.History + } + return nil +} + +func (x *Task) GetMetadata() *structpb.Struct { + if x != nil { + return x.Metadata + } + return nil +} + +// A container for the status of a task +type TaskStatus struct { + state protoimpl.MessageState `protogen:"open.v1"` + // The current state of this task. + State TaskState `protobuf:"varint,1,opt,name=state,proto3,enum=lf.a2a.v1.TaskState" json:"state,omitempty"` + // A message associated with the status. + Message *Message `protobuf:"bytes,2,opt,name=message,proto3" json:"message,omitempty"` + // ISO 8601 Timestamp when the status was recorded. + // Example: "2023-10-27T10:00:00Z" + Timestamp *timestamppb.Timestamp `protobuf:"bytes,3,opt,name=timestamp,proto3" json:"timestamp,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *TaskStatus) Reset() { + *x = TaskStatus{} + mi := &file_a2a_proto_msgTypes[2] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *TaskStatus) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*TaskStatus) ProtoMessage() {} + +func (x *TaskStatus) ProtoReflect() protoreflect.Message { + mi := &file_a2a_proto_msgTypes[2] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use TaskStatus.ProtoReflect.Descriptor instead. +func (*TaskStatus) Descriptor() ([]byte, []int) { + return file_a2a_proto_rawDescGZIP(), []int{2} +} + +func (x *TaskStatus) GetState() TaskState { + if x != nil { + return x.State + } + return TaskState_TASK_STATE_UNSPECIFIED +} + +func (x *TaskStatus) GetMessage() *Message { + if x != nil { + return x.Message + } + return nil +} + +func (x *TaskStatus) GetTimestamp() *timestamppb.Timestamp { + if x != nil { + return x.Timestamp + } + return nil +} + +// `Part` represents a container for a section of communication content. +// Parts can be purely textual, some sort of file (image, video, etc) or +// a structured data blob (i.e. JSON). +type Part struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Types that are valid to be assigned to Content: + // + // *Part_Text + // *Part_Raw + // *Part_Url + // *Part_Data + Content isPart_Content `protobuf_oneof:"content"` + // Optional. metadata associated with this part. + Metadata *structpb.Struct `protobuf:"bytes,5,opt,name=metadata,proto3" json:"metadata,omitempty"` + // An optional `filename` for the file (e.g., "document.pdf"). + Filename string `protobuf:"bytes,6,opt,name=filename,proto3" json:"filename,omitempty"` + // The `media_type` (MIME type) of the part content (e.g., "text/plain", "application/json", "image/png"). + // This field is available for all part types. + MediaType string `protobuf:"bytes,7,opt,name=media_type,json=mediaType,proto3" json:"media_type,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *Part) Reset() { + *x = Part{} + mi := &file_a2a_proto_msgTypes[3] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *Part) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Part) ProtoMessage() {} + +func (x *Part) ProtoReflect() protoreflect.Message { + mi := &file_a2a_proto_msgTypes[3] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Part.ProtoReflect.Descriptor instead. +func (*Part) Descriptor() ([]byte, []int) { + return file_a2a_proto_rawDescGZIP(), []int{3} +} + +func (x *Part) GetContent() isPart_Content { + if x != nil { + return x.Content + } + return nil +} + +func (x *Part) GetText() string { + if x != nil { + if x, ok := x.Content.(*Part_Text); ok { + return x.Text + } + } + return "" +} + +func (x *Part) GetRaw() []byte { + if x != nil { + if x, ok := x.Content.(*Part_Raw); ok { + return x.Raw + } + } + return nil +} + +func (x *Part) GetUrl() string { + if x != nil { + if x, ok := x.Content.(*Part_Url); ok { + return x.Url + } + } + return "" +} + +func (x *Part) GetData() *structpb.Value { + if x != nil { + if x, ok := x.Content.(*Part_Data); ok { + return x.Data + } + } + return nil +} + +func (x *Part) GetMetadata() *structpb.Struct { + if x != nil { + return x.Metadata + } + return nil +} + +func (x *Part) GetFilename() string { + if x != nil { + return x.Filename + } + return "" +} + +func (x *Part) GetMediaType() string { + if x != nil { + return x.MediaType + } + return "" +} + +type isPart_Content interface { + isPart_Content() +} + +type Part_Text struct { + // The string content of the `text` part. + Text string `protobuf:"bytes,1,opt,name=text,proto3,oneof"` +} + +type Part_Raw struct { + // The `raw` byte content of a file. In JSON serialization, this is encoded as a base64 string. + Raw []byte `protobuf:"bytes,2,opt,name=raw,proto3,oneof"` +} + +type Part_Url struct { + // A `url` pointing to the file's content. + Url string `protobuf:"bytes,3,opt,name=url,proto3,oneof"` +} + +type Part_Data struct { + // Arbitrary structured `data` as a JSON value (object, array, string, number, boolean, or null). + Data *structpb.Value `protobuf:"bytes,4,opt,name=data,proto3,oneof"` +} + +func (*Part_Text) isPart_Content() {} + +func (*Part_Raw) isPart_Content() {} + +func (*Part_Url) isPart_Content() {} + +func (*Part_Data) isPart_Content() {} + +// `Message` is one unit of communication between client and server. It can be +// associated with a context and/or a task. For server messages, `context_id` must +// be provided, and `task_id` only if a task was created. For client messages, both +// fields are optional, with the caveat that if both are provided, they have to +// match (the `context_id` has to be the one that is set on the task). If only +// `task_id` is provided, the server will infer `context_id` from it. +type Message struct { + state protoimpl.MessageState `protogen:"open.v1"` + // The unique identifier (e.g. UUID) of the message. This is created by the message creator. + MessageId string `protobuf:"bytes,1,opt,name=message_id,json=messageId,proto3" json:"message_id,omitempty"` + // Optional. The context id of the message. If set, the message will be associated with the given context. + ContextId string `protobuf:"bytes,2,opt,name=context_id,json=contextId,proto3" json:"context_id,omitempty"` + // Optional. The task id of the message. If set, the message will be associated with the given task. + TaskId string `protobuf:"bytes,3,opt,name=task_id,json=taskId,proto3" json:"task_id,omitempty"` + // Identifies the sender of the message. + Role Role `protobuf:"varint,4,opt,name=role,proto3,enum=lf.a2a.v1.Role" json:"role,omitempty"` + // Parts is the container of the message content. + Parts []*Part `protobuf:"bytes,5,rep,name=parts,proto3" json:"parts,omitempty"` + // Optional. Any metadata to provide along with the message. + Metadata *structpb.Struct `protobuf:"bytes,6,opt,name=metadata,proto3" json:"metadata,omitempty"` + // The URIs of extensions that are present or contributed to this Message. + Extensions []string `protobuf:"bytes,7,rep,name=extensions,proto3" json:"extensions,omitempty"` + // A list of task IDs that this message references for additional context. + ReferenceTaskIds []string `protobuf:"bytes,8,rep,name=reference_task_ids,json=referenceTaskIds,proto3" json:"reference_task_ids,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *Message) Reset() { + *x = Message{} + mi := &file_a2a_proto_msgTypes[4] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *Message) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Message) ProtoMessage() {} + +func (x *Message) ProtoReflect() protoreflect.Message { + mi := &file_a2a_proto_msgTypes[4] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Message.ProtoReflect.Descriptor instead. +func (*Message) Descriptor() ([]byte, []int) { + return file_a2a_proto_rawDescGZIP(), []int{4} +} + +func (x *Message) GetMessageId() string { + if x != nil { + return x.MessageId + } + return "" +} + +func (x *Message) GetContextId() string { + if x != nil { + return x.ContextId + } + return "" +} + +func (x *Message) GetTaskId() string { + if x != nil { + return x.TaskId + } + return "" +} + +func (x *Message) GetRole() Role { + if x != nil { + return x.Role + } + return Role_ROLE_UNSPECIFIED +} + +func (x *Message) GetParts() []*Part { + if x != nil { + return x.Parts + } + return nil +} + +func (x *Message) GetMetadata() *structpb.Struct { + if x != nil { + return x.Metadata + } + return nil +} + +func (x *Message) GetExtensions() []string { + if x != nil { + return x.Extensions + } + return nil +} + +func (x *Message) GetReferenceTaskIds() []string { + if x != nil { + return x.ReferenceTaskIds + } + return nil +} + +// Artifacts represent task outputs. +type Artifact struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Unique identifier (e.g. UUID) for the artifact. It must be unique within a task. + ArtifactId string `protobuf:"bytes,1,opt,name=artifact_id,json=artifactId,proto3" json:"artifact_id,omitempty"` + // A human readable name for the artifact. + Name string `protobuf:"bytes,2,opt,name=name,proto3" json:"name,omitempty"` + // Optional. A human readable description of the artifact. + Description string `protobuf:"bytes,3,opt,name=description,proto3" json:"description,omitempty"` + // The content of the artifact. Must contain at least one part. + Parts []*Part `protobuf:"bytes,4,rep,name=parts,proto3" json:"parts,omitempty"` + // Optional. Metadata included with the artifact. + Metadata *structpb.Struct `protobuf:"bytes,5,opt,name=metadata,proto3" json:"metadata,omitempty"` + // The URIs of extensions that are present or contributed to this Artifact. + Extensions []string `protobuf:"bytes,6,rep,name=extensions,proto3" json:"extensions,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *Artifact) Reset() { + *x = Artifact{} + mi := &file_a2a_proto_msgTypes[5] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *Artifact) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Artifact) ProtoMessage() {} + +func (x *Artifact) ProtoReflect() protoreflect.Message { + mi := &file_a2a_proto_msgTypes[5] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Artifact.ProtoReflect.Descriptor instead. +func (*Artifact) Descriptor() ([]byte, []int) { + return file_a2a_proto_rawDescGZIP(), []int{5} +} + +func (x *Artifact) GetArtifactId() string { + if x != nil { + return x.ArtifactId + } + return "" +} + +func (x *Artifact) GetName() string { + if x != nil { + return x.Name + } + return "" +} + +func (x *Artifact) GetDescription() string { + if x != nil { + return x.Description + } + return "" +} + +func (x *Artifact) GetParts() []*Part { + if x != nil { + return x.Parts + } + return nil +} + +func (x *Artifact) GetMetadata() *structpb.Struct { + if x != nil { + return x.Metadata + } + return nil +} + +func (x *Artifact) GetExtensions() []string { + if x != nil { + return x.Extensions + } + return nil +} + +// An event sent by the agent to notify the client of a change in a task's status. +type TaskStatusUpdateEvent struct { + state protoimpl.MessageState `protogen:"open.v1"` + // The ID of the task that has changed. + TaskId string `protobuf:"bytes,1,opt,name=task_id,json=taskId,proto3" json:"task_id,omitempty"` + // The ID of the context that the task belongs to. + ContextId string `protobuf:"bytes,2,opt,name=context_id,json=contextId,proto3" json:"context_id,omitempty"` + // The new status of the task. + Status *TaskStatus `protobuf:"bytes,3,opt,name=status,proto3" json:"status,omitempty"` + // Optional. Metadata associated with the task update. + Metadata *structpb.Struct `protobuf:"bytes,4,opt,name=metadata,proto3" json:"metadata,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *TaskStatusUpdateEvent) Reset() { + *x = TaskStatusUpdateEvent{} + mi := &file_a2a_proto_msgTypes[6] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *TaskStatusUpdateEvent) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*TaskStatusUpdateEvent) ProtoMessage() {} + +func (x *TaskStatusUpdateEvent) ProtoReflect() protoreflect.Message { + mi := &file_a2a_proto_msgTypes[6] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use TaskStatusUpdateEvent.ProtoReflect.Descriptor instead. +func (*TaskStatusUpdateEvent) Descriptor() ([]byte, []int) { + return file_a2a_proto_rawDescGZIP(), []int{6} +} + +func (x *TaskStatusUpdateEvent) GetTaskId() string { + if x != nil { + return x.TaskId + } + return "" +} + +func (x *TaskStatusUpdateEvent) GetContextId() string { + if x != nil { + return x.ContextId + } + return "" +} + +func (x *TaskStatusUpdateEvent) GetStatus() *TaskStatus { + if x != nil { + return x.Status + } + return nil +} + +func (x *TaskStatusUpdateEvent) GetMetadata() *structpb.Struct { + if x != nil { + return x.Metadata + } + return nil +} + +// A task delta where an artifact has been generated. +type TaskArtifactUpdateEvent struct { + state protoimpl.MessageState `protogen:"open.v1"` + // The ID of the task for this artifact. + TaskId string `protobuf:"bytes,1,opt,name=task_id,json=taskId,proto3" json:"task_id,omitempty"` + // The ID of the context that this task belongs to. + ContextId string `protobuf:"bytes,2,opt,name=context_id,json=contextId,proto3" json:"context_id,omitempty"` + // The artifact that was generated or updated. + Artifact *Artifact `protobuf:"bytes,3,opt,name=artifact,proto3" json:"artifact,omitempty"` + // If true, the content of this artifact should be appended to a previously + // sent artifact with the same ID. + Append bool `protobuf:"varint,4,opt,name=append,proto3" json:"append,omitempty"` + // If true, this is the final chunk of the artifact. + LastChunk bool `protobuf:"varint,5,opt,name=last_chunk,json=lastChunk,proto3" json:"last_chunk,omitempty"` + // Optional. Metadata associated with the artifact update. + Metadata *structpb.Struct `protobuf:"bytes,6,opt,name=metadata,proto3" json:"metadata,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *TaskArtifactUpdateEvent) Reset() { + *x = TaskArtifactUpdateEvent{} + mi := &file_a2a_proto_msgTypes[7] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *TaskArtifactUpdateEvent) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*TaskArtifactUpdateEvent) ProtoMessage() {} + +func (x *TaskArtifactUpdateEvent) ProtoReflect() protoreflect.Message { + mi := &file_a2a_proto_msgTypes[7] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use TaskArtifactUpdateEvent.ProtoReflect.Descriptor instead. +func (*TaskArtifactUpdateEvent) Descriptor() ([]byte, []int) { + return file_a2a_proto_rawDescGZIP(), []int{7} +} + +func (x *TaskArtifactUpdateEvent) GetTaskId() string { + if x != nil { + return x.TaskId + } + return "" +} + +func (x *TaskArtifactUpdateEvent) GetContextId() string { + if x != nil { + return x.ContextId + } + return "" +} + +func (x *TaskArtifactUpdateEvent) GetArtifact() *Artifact { + if x != nil { + return x.Artifact + } + return nil +} + +func (x *TaskArtifactUpdateEvent) GetAppend() bool { + if x != nil { + return x.Append + } + return false +} + +func (x *TaskArtifactUpdateEvent) GetLastChunk() bool { + if x != nil { + return x.LastChunk + } + return false +} + +func (x *TaskArtifactUpdateEvent) GetMetadata() *structpb.Struct { + if x != nil { + return x.Metadata + } + return nil +} + +// Defines authentication details, used for push notifications. +type AuthenticationInfo struct { + state protoimpl.MessageState `protogen:"open.v1"` + // HTTP Authentication Scheme from the [IANA registry](https://www.iana.org/assignments/http-authschemes/). + // Examples: `Bearer`, `Basic`, `Digest`. + // Scheme names are case-insensitive per [RFC 9110 Section 11.1](https://www.rfc-editor.org/rfc/rfc9110#section-11.1). + Scheme string `protobuf:"bytes,1,opt,name=scheme,proto3" json:"scheme,omitempty"` + // Push Notification credentials. Format depends on the scheme (e.g., token for Bearer). + Credentials string `protobuf:"bytes,2,opt,name=credentials,proto3" json:"credentials,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *AuthenticationInfo) Reset() { + *x = AuthenticationInfo{} + mi := &file_a2a_proto_msgTypes[8] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *AuthenticationInfo) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*AuthenticationInfo) ProtoMessage() {} + +func (x *AuthenticationInfo) ProtoReflect() protoreflect.Message { + mi := &file_a2a_proto_msgTypes[8] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use AuthenticationInfo.ProtoReflect.Descriptor instead. +func (*AuthenticationInfo) Descriptor() ([]byte, []int) { + return file_a2a_proto_rawDescGZIP(), []int{8} +} + +func (x *AuthenticationInfo) GetScheme() string { + if x != nil { + return x.Scheme + } + return "" +} + +func (x *AuthenticationInfo) GetCredentials() string { + if x != nil { + return x.Credentials + } + return "" +} + +// Declares a combination of a target URL, transport and protocol version for interacting with the agent. +// This allows agents to expose the same functionality over multiple protocol binding mechanisms. +type AgentInterface struct { + state protoimpl.MessageState `protogen:"open.v1"` + // The URL where this interface is available. Must be a valid absolute HTTPS URL in production. + // Example: "https://api.example.com/a2a/v1", "https://grpc.example.com/a2a" + Url string `protobuf:"bytes,1,opt,name=url,proto3" json:"url,omitempty"` + // The protocol binding supported at this URL. This is an open form string, to be + // easily extended for other protocol bindings. The core ones officially + // supported are `JSONRPC`, `GRPC` and `HTTP+JSON`. + ProtocolBinding string `protobuf:"bytes,2,opt,name=protocol_binding,json=protocolBinding,proto3" json:"protocol_binding,omitempty"` + // Optional. An opaque string used for routing requests to a specific agent + // or tenant when multiple agents are served behind a single A2A endpoint. + // When set, clients MUST include this value in the `tenant` field of all + // request messages sent to this interface. The server is responsible for + // interpreting the value and routing requests accordingly; the protocol + // does not define its format or semantics. + Tenant string `protobuf:"bytes,3,opt,name=tenant,proto3" json:"tenant,omitempty"` + // The version of the A2A protocol this interface exposes. + // Use the latest supported minor version per major version. + // Examples: "0.3", "1.0" + ProtocolVersion string `protobuf:"bytes,4,opt,name=protocol_version,json=protocolVersion,proto3" json:"protocol_version,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *AgentInterface) Reset() { + *x = AgentInterface{} + mi := &file_a2a_proto_msgTypes[9] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *AgentInterface) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*AgentInterface) ProtoMessage() {} + +func (x *AgentInterface) ProtoReflect() protoreflect.Message { + mi := &file_a2a_proto_msgTypes[9] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use AgentInterface.ProtoReflect.Descriptor instead. +func (*AgentInterface) Descriptor() ([]byte, []int) { + return file_a2a_proto_rawDescGZIP(), []int{9} +} + +func (x *AgentInterface) GetUrl() string { + if x != nil { + return x.Url + } + return "" +} + +func (x *AgentInterface) GetProtocolBinding() string { + if x != nil { + return x.ProtocolBinding + } + return "" +} + +func (x *AgentInterface) GetTenant() string { + if x != nil { + return x.Tenant + } + return "" +} + +func (x *AgentInterface) GetProtocolVersion() string { + if x != nil { + return x.ProtocolVersion + } + return "" +} + +// A self-describing manifest for an agent. It provides essential +// metadata including the agent's identity, capabilities, skills, supported +// communication methods, and security requirements. +// Next ID: 20 +type AgentCard struct { + state protoimpl.MessageState `protogen:"open.v1"` + // A human readable name for the agent. + // Example: "Recipe Agent" + Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` + // A human-readable description of the agent, assisting users and other agents + // in understanding its purpose. + // Example: "Agent that helps users with recipes and cooking." + Description string `protobuf:"bytes,2,opt,name=description,proto3" json:"description,omitempty"` + // Ordered list of supported interfaces. The first entry is preferred. + SupportedInterfaces []*AgentInterface `protobuf:"bytes,3,rep,name=supported_interfaces,json=supportedInterfaces,proto3" json:"supported_interfaces,omitempty"` + // The service provider of the agent. + Provider *AgentProvider `protobuf:"bytes,4,opt,name=provider,proto3" json:"provider,omitempty"` + // The version of the agent. + // Example: "1.0.0" + Version string `protobuf:"bytes,5,opt,name=version,proto3" json:"version,omitempty"` + // A URL providing additional documentation about the agent. + DocumentationUrl *string `protobuf:"bytes,6,opt,name=documentation_url,json=documentationUrl,proto3,oneof" json:"documentation_url,omitempty"` + // A2A Capability set supported by the agent. + Capabilities *AgentCapabilities `protobuf:"bytes,7,opt,name=capabilities,proto3" json:"capabilities,omitempty"` + // The security scheme details used for authenticating with this agent. + SecuritySchemes map[string]*SecurityScheme `protobuf:"bytes,8,rep,name=security_schemes,json=securitySchemes,proto3" json:"security_schemes,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"` + // Security requirements for contacting the agent. + SecurityRequirements []*SecurityRequirement `protobuf:"bytes,9,rep,name=security_requirements,json=securityRequirements,proto3" json:"security_requirements,omitempty"` + // protolint:enable REPEATED_FIELD_NAMES_PLURALIZED + // The set of interaction modes that the agent supports across all skills. + // This can be overridden per skill. Defined as media types. + DefaultInputModes []string `protobuf:"bytes,10,rep,name=default_input_modes,json=defaultInputModes,proto3" json:"default_input_modes,omitempty"` + // The media types supported as outputs from this agent. + DefaultOutputModes []string `protobuf:"bytes,11,rep,name=default_output_modes,json=defaultOutputModes,proto3" json:"default_output_modes,omitempty"` + // Skills represent the abilities of an agent. + // It is largely a descriptive concept but represents a more focused set of behaviors that the + // agent is likely to succeed at. + Skills []*AgentSkill `protobuf:"bytes,12,rep,name=skills,proto3" json:"skills,omitempty"` + // JSON Web Signatures computed for this `AgentCard`. + Signatures []*AgentCardSignature `protobuf:"bytes,13,rep,name=signatures,proto3" json:"signatures,omitempty"` + // Optional. A URL to an icon for the agent. + IconUrl *string `protobuf:"bytes,14,opt,name=icon_url,json=iconUrl,proto3,oneof" json:"icon_url,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *AgentCard) Reset() { + *x = AgentCard{} + mi := &file_a2a_proto_msgTypes[10] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *AgentCard) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*AgentCard) ProtoMessage() {} + +func (x *AgentCard) ProtoReflect() protoreflect.Message { + mi := &file_a2a_proto_msgTypes[10] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use AgentCard.ProtoReflect.Descriptor instead. +func (*AgentCard) Descriptor() ([]byte, []int) { + return file_a2a_proto_rawDescGZIP(), []int{10} +} + +func (x *AgentCard) GetName() string { + if x != nil { + return x.Name + } + return "" +} + +func (x *AgentCard) GetDescription() string { + if x != nil { + return x.Description + } + return "" +} + +func (x *AgentCard) GetSupportedInterfaces() []*AgentInterface { + if x != nil { + return x.SupportedInterfaces + } + return nil +} + +func (x *AgentCard) GetProvider() *AgentProvider { + if x != nil { + return x.Provider + } + return nil +} + +func (x *AgentCard) GetVersion() string { + if x != nil { + return x.Version + } + return "" +} + +func (x *AgentCard) GetDocumentationUrl() string { + if x != nil && x.DocumentationUrl != nil { + return *x.DocumentationUrl + } + return "" +} + +func (x *AgentCard) GetCapabilities() *AgentCapabilities { + if x != nil { + return x.Capabilities + } + return nil +} + +func (x *AgentCard) GetSecuritySchemes() map[string]*SecurityScheme { + if x != nil { + return x.SecuritySchemes + } + return nil +} + +func (x *AgentCard) GetSecurityRequirements() []*SecurityRequirement { + if x != nil { + return x.SecurityRequirements + } + return nil +} + +func (x *AgentCard) GetDefaultInputModes() []string { + if x != nil { + return x.DefaultInputModes + } + return nil +} + +func (x *AgentCard) GetDefaultOutputModes() []string { + if x != nil { + return x.DefaultOutputModes + } + return nil +} + +func (x *AgentCard) GetSkills() []*AgentSkill { + if x != nil { + return x.Skills + } + return nil +} + +func (x *AgentCard) GetSignatures() []*AgentCardSignature { + if x != nil { + return x.Signatures + } + return nil +} + +func (x *AgentCard) GetIconUrl() string { + if x != nil && x.IconUrl != nil { + return *x.IconUrl + } + return "" +} + +// Represents the service provider of an agent. +type AgentProvider struct { + state protoimpl.MessageState `protogen:"open.v1"` + // A URL for the agent provider's website or relevant documentation. + // Example: "https://ai.google.dev" + Url string `protobuf:"bytes,1,opt,name=url,proto3" json:"url,omitempty"` + // The name of the agent provider's organization. + // Example: "Google" + Organization string `protobuf:"bytes,2,opt,name=organization,proto3" json:"organization,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *AgentProvider) Reset() { + *x = AgentProvider{} + mi := &file_a2a_proto_msgTypes[11] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *AgentProvider) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*AgentProvider) ProtoMessage() {} + +func (x *AgentProvider) ProtoReflect() protoreflect.Message { + mi := &file_a2a_proto_msgTypes[11] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use AgentProvider.ProtoReflect.Descriptor instead. +func (*AgentProvider) Descriptor() ([]byte, []int) { + return file_a2a_proto_rawDescGZIP(), []int{11} +} + +func (x *AgentProvider) GetUrl() string { + if x != nil { + return x.Url + } + return "" +} + +func (x *AgentProvider) GetOrganization() string { + if x != nil { + return x.Organization + } + return "" +} + +// Defines optional capabilities supported by an agent. +type AgentCapabilities struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Indicates if the agent supports streaming responses. + Streaming *bool `protobuf:"varint,1,opt,name=streaming,proto3,oneof" json:"streaming,omitempty"` + // Indicates if the agent supports sending push notifications for asynchronous task updates. + PushNotifications *bool `protobuf:"varint,2,opt,name=push_notifications,json=pushNotifications,proto3,oneof" json:"push_notifications,omitempty"` + // A list of protocol extensions supported by the agent. + Extensions []*AgentExtension `protobuf:"bytes,3,rep,name=extensions,proto3" json:"extensions,omitempty"` + // Indicates if the agent supports providing an extended agent card when authenticated. + ExtendedAgentCard *bool `protobuf:"varint,4,opt,name=extended_agent_card,json=extendedAgentCard,proto3,oneof" json:"extended_agent_card,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *AgentCapabilities) Reset() { + *x = AgentCapabilities{} + mi := &file_a2a_proto_msgTypes[12] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *AgentCapabilities) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*AgentCapabilities) ProtoMessage() {} + +func (x *AgentCapabilities) ProtoReflect() protoreflect.Message { + mi := &file_a2a_proto_msgTypes[12] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use AgentCapabilities.ProtoReflect.Descriptor instead. +func (*AgentCapabilities) Descriptor() ([]byte, []int) { + return file_a2a_proto_rawDescGZIP(), []int{12} +} + +func (x *AgentCapabilities) GetStreaming() bool { + if x != nil && x.Streaming != nil { + return *x.Streaming + } + return false +} + +func (x *AgentCapabilities) GetPushNotifications() bool { + if x != nil && x.PushNotifications != nil { + return *x.PushNotifications + } + return false +} + +func (x *AgentCapabilities) GetExtensions() []*AgentExtension { + if x != nil { + return x.Extensions + } + return nil +} + +func (x *AgentCapabilities) GetExtendedAgentCard() bool { + if x != nil && x.ExtendedAgentCard != nil { + return *x.ExtendedAgentCard + } + return false +} + +// A declaration of a protocol extension supported by an Agent. +type AgentExtension struct { + state protoimpl.MessageState `protogen:"open.v1"` + // The unique URI identifying the extension. + Uri string `protobuf:"bytes,1,opt,name=uri,proto3" json:"uri,omitempty"` + // A human-readable description of how this agent uses the extension. + Description string `protobuf:"bytes,2,opt,name=description,proto3" json:"description,omitempty"` + // If true, the client must understand and comply with the extension's requirements. + Required bool `protobuf:"varint,3,opt,name=required,proto3" json:"required,omitempty"` + // Optional. Extension-specific configuration parameters. + Params *structpb.Struct `protobuf:"bytes,4,opt,name=params,proto3" json:"params,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *AgentExtension) Reset() { + *x = AgentExtension{} + mi := &file_a2a_proto_msgTypes[13] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *AgentExtension) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*AgentExtension) ProtoMessage() {} + +func (x *AgentExtension) ProtoReflect() protoreflect.Message { + mi := &file_a2a_proto_msgTypes[13] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use AgentExtension.ProtoReflect.Descriptor instead. +func (*AgentExtension) Descriptor() ([]byte, []int) { + return file_a2a_proto_rawDescGZIP(), []int{13} +} + +func (x *AgentExtension) GetUri() string { + if x != nil { + return x.Uri + } + return "" +} + +func (x *AgentExtension) GetDescription() string { + if x != nil { + return x.Description + } + return "" +} + +func (x *AgentExtension) GetRequired() bool { + if x != nil { + return x.Required + } + return false +} + +func (x *AgentExtension) GetParams() *structpb.Struct { + if x != nil { + return x.Params + } + return nil +} + +// Represents a distinct capability or function that an agent can perform. +type AgentSkill struct { + state protoimpl.MessageState `protogen:"open.v1"` + // A unique identifier for the agent's skill. + Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"` + // A human-readable name for the skill. + Name string `protobuf:"bytes,2,opt,name=name,proto3" json:"name,omitempty"` + // A detailed description of the skill. + Description string `protobuf:"bytes,3,opt,name=description,proto3" json:"description,omitempty"` + // A set of keywords describing the skill's capabilities. + Tags []string `protobuf:"bytes,4,rep,name=tags,proto3" json:"tags,omitempty"` + // Example prompts or scenarios that this skill can handle. + Examples []string `protobuf:"bytes,5,rep,name=examples,proto3" json:"examples,omitempty"` + // The set of supported input media types for this skill, overriding the agent's defaults. + InputModes []string `protobuf:"bytes,6,rep,name=input_modes,json=inputModes,proto3" json:"input_modes,omitempty"` + // The set of supported output media types for this skill, overriding the agent's defaults. + OutputModes []string `protobuf:"bytes,7,rep,name=output_modes,json=outputModes,proto3" json:"output_modes,omitempty"` + // Security schemes necessary for this skill. + SecurityRequirements []*SecurityRequirement `protobuf:"bytes,8,rep,name=security_requirements,json=securityRequirements,proto3" json:"security_requirements,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *AgentSkill) Reset() { + *x = AgentSkill{} + mi := &file_a2a_proto_msgTypes[14] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *AgentSkill) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*AgentSkill) ProtoMessage() {} + +func (x *AgentSkill) ProtoReflect() protoreflect.Message { + mi := &file_a2a_proto_msgTypes[14] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use AgentSkill.ProtoReflect.Descriptor instead. +func (*AgentSkill) Descriptor() ([]byte, []int) { + return file_a2a_proto_rawDescGZIP(), []int{14} +} + +func (x *AgentSkill) GetId() string { + if x != nil { + return x.Id + } + return "" +} + +func (x *AgentSkill) GetName() string { + if x != nil { + return x.Name + } + return "" +} + +func (x *AgentSkill) GetDescription() string { + if x != nil { + return x.Description + } + return "" +} + +func (x *AgentSkill) GetTags() []string { + if x != nil { + return x.Tags + } + return nil +} + +func (x *AgentSkill) GetExamples() []string { + if x != nil { + return x.Examples + } + return nil +} + +func (x *AgentSkill) GetInputModes() []string { + if x != nil { + return x.InputModes + } + return nil +} + +func (x *AgentSkill) GetOutputModes() []string { + if x != nil { + return x.OutputModes + } + return nil +} + +func (x *AgentSkill) GetSecurityRequirements() []*SecurityRequirement { + if x != nil { + return x.SecurityRequirements + } + return nil +} + +// AgentCardSignature represents a JWS signature of an AgentCard. +// This follows the JSON format of an RFC 7515 JSON Web Signature (JWS). +type AgentCardSignature struct { + state protoimpl.MessageState `protogen:"open.v1"` + // (-- api-linter: core::0140::reserved-words=disabled + // + // aip.dev/not-precedent: Backwards compatibility --) + // + // Required. The protected JWS header for the signature. This is always a + // base64url-encoded JSON object. + Protected string `protobuf:"bytes,1,opt,name=protected,proto3" json:"protected,omitempty"` + // Required. The computed signature, base64url-encoded. + Signature string `protobuf:"bytes,2,opt,name=signature,proto3" json:"signature,omitempty"` + // The unprotected JWS header values. + Header *structpb.Struct `protobuf:"bytes,3,opt,name=header,proto3" json:"header,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *AgentCardSignature) Reset() { + *x = AgentCardSignature{} + mi := &file_a2a_proto_msgTypes[15] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *AgentCardSignature) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*AgentCardSignature) ProtoMessage() {} + +func (x *AgentCardSignature) ProtoReflect() protoreflect.Message { + mi := &file_a2a_proto_msgTypes[15] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use AgentCardSignature.ProtoReflect.Descriptor instead. +func (*AgentCardSignature) Descriptor() ([]byte, []int) { + return file_a2a_proto_rawDescGZIP(), []int{15} +} + +func (x *AgentCardSignature) GetProtected() string { + if x != nil { + return x.Protected + } + return "" +} + +func (x *AgentCardSignature) GetSignature() string { + if x != nil { + return x.Signature + } + return "" +} + +func (x *AgentCardSignature) GetHeader() *structpb.Struct { + if x != nil { + return x.Header + } + return nil +} + +// A container associating a push notification configuration with a specific task. +type TaskPushNotificationConfig struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Optional. Opaque routing identifier. Must match the `tenant` value from + // the selected `AgentInterface` in the Agent Card when that field is set. + Tenant string `protobuf:"bytes,1,opt,name=tenant,proto3" json:"tenant,omitempty"` + // The push notification configuration details. + // A unique identifier (e.g. UUID) for this push notification configuration. + Id string `protobuf:"bytes,2,opt,name=id,proto3" json:"id,omitempty"` + // The ID of the task this configuration is associated with. + TaskId string `protobuf:"bytes,3,opt,name=task_id,json=taskId,proto3" json:"task_id,omitempty"` + // The URL where the notification should be sent. + Url string `protobuf:"bytes,4,opt,name=url,proto3" json:"url,omitempty"` + // A token unique for this task or session. + Token string `protobuf:"bytes,5,opt,name=token,proto3" json:"token,omitempty"` + // Authentication information required to send the notification. + Authentication *AuthenticationInfo `protobuf:"bytes,6,opt,name=authentication,proto3" json:"authentication,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *TaskPushNotificationConfig) Reset() { + *x = TaskPushNotificationConfig{} + mi := &file_a2a_proto_msgTypes[16] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *TaskPushNotificationConfig) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*TaskPushNotificationConfig) ProtoMessage() {} + +func (x *TaskPushNotificationConfig) ProtoReflect() protoreflect.Message { + mi := &file_a2a_proto_msgTypes[16] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use TaskPushNotificationConfig.ProtoReflect.Descriptor instead. +func (*TaskPushNotificationConfig) Descriptor() ([]byte, []int) { + return file_a2a_proto_rawDescGZIP(), []int{16} +} + +func (x *TaskPushNotificationConfig) GetTenant() string { + if x != nil { + return x.Tenant + } + return "" +} + +func (x *TaskPushNotificationConfig) GetId() string { + if x != nil { + return x.Id + } + return "" +} + +func (x *TaskPushNotificationConfig) GetTaskId() string { + if x != nil { + return x.TaskId + } + return "" +} + +func (x *TaskPushNotificationConfig) GetUrl() string { + if x != nil { + return x.Url + } + return "" +} + +func (x *TaskPushNotificationConfig) GetToken() string { + if x != nil { + return x.Token + } + return "" +} + +func (x *TaskPushNotificationConfig) GetAuthentication() *AuthenticationInfo { + if x != nil { + return x.Authentication + } + return nil +} + +// protolint:disable REPEATED_FIELD_NAMES_PLURALIZED +// A list of strings. +type StringList struct { + state protoimpl.MessageState `protogen:"open.v1"` + // The individual string values. + List []string `protobuf:"bytes,1,rep,name=list,proto3" json:"list,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *StringList) Reset() { + *x = StringList{} + mi := &file_a2a_proto_msgTypes[17] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *StringList) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*StringList) ProtoMessage() {} + +func (x *StringList) ProtoReflect() protoreflect.Message { + mi := &file_a2a_proto_msgTypes[17] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use StringList.ProtoReflect.Descriptor instead. +func (*StringList) Descriptor() ([]byte, []int) { + return file_a2a_proto_rawDescGZIP(), []int{17} +} + +func (x *StringList) GetList() []string { + if x != nil { + return x.List + } + return nil +} + +// Defines the security requirements for an agent. +type SecurityRequirement struct { + state protoimpl.MessageState `protogen:"open.v1"` + // A map of security schemes to the required scopes. + Schemes map[string]*StringList `protobuf:"bytes,1,rep,name=schemes,proto3" json:"schemes,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *SecurityRequirement) Reset() { + *x = SecurityRequirement{} + mi := &file_a2a_proto_msgTypes[18] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *SecurityRequirement) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*SecurityRequirement) ProtoMessage() {} + +func (x *SecurityRequirement) ProtoReflect() protoreflect.Message { + mi := &file_a2a_proto_msgTypes[18] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use SecurityRequirement.ProtoReflect.Descriptor instead. +func (*SecurityRequirement) Descriptor() ([]byte, []int) { + return file_a2a_proto_rawDescGZIP(), []int{18} +} + +func (x *SecurityRequirement) GetSchemes() map[string]*StringList { + if x != nil { + return x.Schemes + } + return nil +} + +// Defines a security scheme that can be used to secure an agent's endpoints. +// This is a discriminated union type based on the OpenAPI 3.2 Security Scheme Object. +// See: https://spec.openapis.org/oas/v3.2.0.html#security-scheme-object +type SecurityScheme struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Types that are valid to be assigned to Scheme: + // + // *SecurityScheme_ApiKeySecurityScheme + // *SecurityScheme_HttpAuthSecurityScheme + // *SecurityScheme_Oauth2SecurityScheme + // *SecurityScheme_OpenIdConnectSecurityScheme + // *SecurityScheme_MtlsSecurityScheme + Scheme isSecurityScheme_Scheme `protobuf_oneof:"scheme"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *SecurityScheme) Reset() { + *x = SecurityScheme{} + mi := &file_a2a_proto_msgTypes[19] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *SecurityScheme) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*SecurityScheme) ProtoMessage() {} + +func (x *SecurityScheme) ProtoReflect() protoreflect.Message { + mi := &file_a2a_proto_msgTypes[19] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use SecurityScheme.ProtoReflect.Descriptor instead. +func (*SecurityScheme) Descriptor() ([]byte, []int) { + return file_a2a_proto_rawDescGZIP(), []int{19} +} + +func (x *SecurityScheme) GetScheme() isSecurityScheme_Scheme { + if x != nil { + return x.Scheme + } + return nil +} + +func (x *SecurityScheme) GetApiKeySecurityScheme() *APIKeySecurityScheme { + if x != nil { + if x, ok := x.Scheme.(*SecurityScheme_ApiKeySecurityScheme); ok { + return x.ApiKeySecurityScheme + } + } + return nil +} + +func (x *SecurityScheme) GetHttpAuthSecurityScheme() *HTTPAuthSecurityScheme { + if x != nil { + if x, ok := x.Scheme.(*SecurityScheme_HttpAuthSecurityScheme); ok { + return x.HttpAuthSecurityScheme + } + } + return nil +} + +func (x *SecurityScheme) GetOauth2SecurityScheme() *OAuth2SecurityScheme { + if x != nil { + if x, ok := x.Scheme.(*SecurityScheme_Oauth2SecurityScheme); ok { + return x.Oauth2SecurityScheme + } + } + return nil +} + +func (x *SecurityScheme) GetOpenIdConnectSecurityScheme() *OpenIdConnectSecurityScheme { + if x != nil { + if x, ok := x.Scheme.(*SecurityScheme_OpenIdConnectSecurityScheme); ok { + return x.OpenIdConnectSecurityScheme + } + } + return nil +} + +func (x *SecurityScheme) GetMtlsSecurityScheme() *MutualTlsSecurityScheme { + if x != nil { + if x, ok := x.Scheme.(*SecurityScheme_MtlsSecurityScheme); ok { + return x.MtlsSecurityScheme + } + } + return nil +} + +type isSecurityScheme_Scheme interface { + isSecurityScheme_Scheme() +} + +type SecurityScheme_ApiKeySecurityScheme struct { + // API key-based authentication. + ApiKeySecurityScheme *APIKeySecurityScheme `protobuf:"bytes,1,opt,name=api_key_security_scheme,json=apiKeySecurityScheme,proto3,oneof"` +} + +type SecurityScheme_HttpAuthSecurityScheme struct { + // HTTP authentication (Basic, Bearer, etc.). + HttpAuthSecurityScheme *HTTPAuthSecurityScheme `protobuf:"bytes,2,opt,name=http_auth_security_scheme,json=httpAuthSecurityScheme,proto3,oneof"` +} + +type SecurityScheme_Oauth2SecurityScheme struct { + // OAuth 2.0 authentication. + Oauth2SecurityScheme *OAuth2SecurityScheme `protobuf:"bytes,3,opt,name=oauth2_security_scheme,json=oauth2SecurityScheme,proto3,oneof"` +} + +type SecurityScheme_OpenIdConnectSecurityScheme struct { + // OpenID Connect authentication. + OpenIdConnectSecurityScheme *OpenIdConnectSecurityScheme `protobuf:"bytes,4,opt,name=open_id_connect_security_scheme,json=openIdConnectSecurityScheme,proto3,oneof"` +} + +type SecurityScheme_MtlsSecurityScheme struct { + // Mutual TLS authentication. + MtlsSecurityScheme *MutualTlsSecurityScheme `protobuf:"bytes,5,opt,name=mtls_security_scheme,json=mtlsSecurityScheme,proto3,oneof"` +} + +func (*SecurityScheme_ApiKeySecurityScheme) isSecurityScheme_Scheme() {} + +func (*SecurityScheme_HttpAuthSecurityScheme) isSecurityScheme_Scheme() {} + +func (*SecurityScheme_Oauth2SecurityScheme) isSecurityScheme_Scheme() {} + +func (*SecurityScheme_OpenIdConnectSecurityScheme) isSecurityScheme_Scheme() {} + +func (*SecurityScheme_MtlsSecurityScheme) isSecurityScheme_Scheme() {} + +// Defines a security scheme using an API key. +type APIKeySecurityScheme struct { + state protoimpl.MessageState `protogen:"open.v1"` + // An optional description for the security scheme. + Description string `protobuf:"bytes,1,opt,name=description,proto3" json:"description,omitempty"` + // The location of the API key. Valid values are "query", "header", or "cookie". + Location string `protobuf:"bytes,2,opt,name=location,proto3" json:"location,omitempty"` + // The name of the header, query, or cookie parameter to be used. + Name string `protobuf:"bytes,3,opt,name=name,proto3" json:"name,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *APIKeySecurityScheme) Reset() { + *x = APIKeySecurityScheme{} + mi := &file_a2a_proto_msgTypes[20] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *APIKeySecurityScheme) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*APIKeySecurityScheme) ProtoMessage() {} + +func (x *APIKeySecurityScheme) ProtoReflect() protoreflect.Message { + mi := &file_a2a_proto_msgTypes[20] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use APIKeySecurityScheme.ProtoReflect.Descriptor instead. +func (*APIKeySecurityScheme) Descriptor() ([]byte, []int) { + return file_a2a_proto_rawDescGZIP(), []int{20} +} + +func (x *APIKeySecurityScheme) GetDescription() string { + if x != nil { + return x.Description + } + return "" +} + +func (x *APIKeySecurityScheme) GetLocation() string { + if x != nil { + return x.Location + } + return "" +} + +func (x *APIKeySecurityScheme) GetName() string { + if x != nil { + return x.Name + } + return "" +} + +// Defines a security scheme using HTTP authentication. +type HTTPAuthSecurityScheme struct { + state protoimpl.MessageState `protogen:"open.v1"` + // An optional description for the security scheme. + Description string `protobuf:"bytes,1,opt,name=description,proto3" json:"description,omitempty"` + // The name of the HTTP Authentication scheme to be used in the Authorization header, + // as defined in RFC7235 (e.g., "Bearer"). + // This value should be registered in the IANA Authentication Scheme registry. + Scheme string `protobuf:"bytes,2,opt,name=scheme,proto3" json:"scheme,omitempty"` + // A hint to the client to identify how the bearer token is formatted (e.g., "JWT"). + // Primarily for documentation purposes. + BearerFormat string `protobuf:"bytes,3,opt,name=bearer_format,json=bearerFormat,proto3" json:"bearer_format,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *HTTPAuthSecurityScheme) Reset() { + *x = HTTPAuthSecurityScheme{} + mi := &file_a2a_proto_msgTypes[21] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *HTTPAuthSecurityScheme) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*HTTPAuthSecurityScheme) ProtoMessage() {} + +func (x *HTTPAuthSecurityScheme) ProtoReflect() protoreflect.Message { + mi := &file_a2a_proto_msgTypes[21] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use HTTPAuthSecurityScheme.ProtoReflect.Descriptor instead. +func (*HTTPAuthSecurityScheme) Descriptor() ([]byte, []int) { + return file_a2a_proto_rawDescGZIP(), []int{21} +} + +func (x *HTTPAuthSecurityScheme) GetDescription() string { + if x != nil { + return x.Description + } + return "" +} + +func (x *HTTPAuthSecurityScheme) GetScheme() string { + if x != nil { + return x.Scheme + } + return "" +} + +func (x *HTTPAuthSecurityScheme) GetBearerFormat() string { + if x != nil { + return x.BearerFormat + } + return "" +} + +// Defines a security scheme using OAuth 2.0. +type OAuth2SecurityScheme struct { + state protoimpl.MessageState `protogen:"open.v1"` + // An optional description for the security scheme. + Description string `protobuf:"bytes,1,opt,name=description,proto3" json:"description,omitempty"` + // An object containing configuration information for the supported OAuth 2.0 flows. + Flows *OAuthFlows `protobuf:"bytes,2,opt,name=flows,proto3" json:"flows,omitempty"` + // URL to the OAuth2 authorization server metadata [RFC 8414](https://datatracker.ietf.org/doc/html/rfc8414). + // TLS is required. + Oauth2MetadataUrl string `protobuf:"bytes,3,opt,name=oauth2_metadata_url,json=oauth2MetadataUrl,proto3" json:"oauth2_metadata_url,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *OAuth2SecurityScheme) Reset() { + *x = OAuth2SecurityScheme{} + mi := &file_a2a_proto_msgTypes[22] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *OAuth2SecurityScheme) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*OAuth2SecurityScheme) ProtoMessage() {} + +func (x *OAuth2SecurityScheme) ProtoReflect() protoreflect.Message { + mi := &file_a2a_proto_msgTypes[22] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use OAuth2SecurityScheme.ProtoReflect.Descriptor instead. +func (*OAuth2SecurityScheme) Descriptor() ([]byte, []int) { + return file_a2a_proto_rawDescGZIP(), []int{22} +} + +func (x *OAuth2SecurityScheme) GetDescription() string { + if x != nil { + return x.Description + } + return "" +} + +func (x *OAuth2SecurityScheme) GetFlows() *OAuthFlows { + if x != nil { + return x.Flows + } + return nil +} + +func (x *OAuth2SecurityScheme) GetOauth2MetadataUrl() string { + if x != nil { + return x.Oauth2MetadataUrl + } + return "" +} + +// Defines a security scheme using OpenID Connect. +type OpenIdConnectSecurityScheme struct { + state protoimpl.MessageState `protogen:"open.v1"` + // An optional description for the security scheme. + Description string `protobuf:"bytes,1,opt,name=description,proto3" json:"description,omitempty"` + // The [OpenID Connect Discovery URL](https://openid.net/specs/openid-connect-discovery-1_0.html) for the OIDC provider's metadata. + OpenIdConnectUrl string `protobuf:"bytes,2,opt,name=open_id_connect_url,json=openIdConnectUrl,proto3" json:"open_id_connect_url,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *OpenIdConnectSecurityScheme) Reset() { + *x = OpenIdConnectSecurityScheme{} + mi := &file_a2a_proto_msgTypes[23] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *OpenIdConnectSecurityScheme) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*OpenIdConnectSecurityScheme) ProtoMessage() {} + +func (x *OpenIdConnectSecurityScheme) ProtoReflect() protoreflect.Message { + mi := &file_a2a_proto_msgTypes[23] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use OpenIdConnectSecurityScheme.ProtoReflect.Descriptor instead. +func (*OpenIdConnectSecurityScheme) Descriptor() ([]byte, []int) { + return file_a2a_proto_rawDescGZIP(), []int{23} +} + +func (x *OpenIdConnectSecurityScheme) GetDescription() string { + if x != nil { + return x.Description + } + return "" +} + +func (x *OpenIdConnectSecurityScheme) GetOpenIdConnectUrl() string { + if x != nil { + return x.OpenIdConnectUrl + } + return "" +} + +// Defines a security scheme using mTLS authentication. +type MutualTlsSecurityScheme struct { + state protoimpl.MessageState `protogen:"open.v1"` + // An optional description for the security scheme. + Description string `protobuf:"bytes,1,opt,name=description,proto3" json:"description,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *MutualTlsSecurityScheme) Reset() { + *x = MutualTlsSecurityScheme{} + mi := &file_a2a_proto_msgTypes[24] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *MutualTlsSecurityScheme) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*MutualTlsSecurityScheme) ProtoMessage() {} + +func (x *MutualTlsSecurityScheme) ProtoReflect() protoreflect.Message { + mi := &file_a2a_proto_msgTypes[24] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use MutualTlsSecurityScheme.ProtoReflect.Descriptor instead. +func (*MutualTlsSecurityScheme) Descriptor() ([]byte, []int) { + return file_a2a_proto_rawDescGZIP(), []int{24} +} + +func (x *MutualTlsSecurityScheme) GetDescription() string { + if x != nil { + return x.Description + } + return "" +} + +// Defines the configuration for the supported OAuth 2.0 flows. +type OAuthFlows struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Types that are valid to be assigned to Flow: + // + // *OAuthFlows_AuthorizationCode + // *OAuthFlows_ClientCredentials + // *OAuthFlows_Implicit + // *OAuthFlows_Password + // *OAuthFlows_DeviceCode + Flow isOAuthFlows_Flow `protobuf_oneof:"flow"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *OAuthFlows) Reset() { + *x = OAuthFlows{} + mi := &file_a2a_proto_msgTypes[25] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *OAuthFlows) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*OAuthFlows) ProtoMessage() {} + +func (x *OAuthFlows) ProtoReflect() protoreflect.Message { + mi := &file_a2a_proto_msgTypes[25] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use OAuthFlows.ProtoReflect.Descriptor instead. +func (*OAuthFlows) Descriptor() ([]byte, []int) { + return file_a2a_proto_rawDescGZIP(), []int{25} +} + +func (x *OAuthFlows) GetFlow() isOAuthFlows_Flow { + if x != nil { + return x.Flow + } + return nil +} + +func (x *OAuthFlows) GetAuthorizationCode() *AuthorizationCodeOAuthFlow { + if x != nil { + if x, ok := x.Flow.(*OAuthFlows_AuthorizationCode); ok { + return x.AuthorizationCode + } + } + return nil +} + +func (x *OAuthFlows) GetClientCredentials() *ClientCredentialsOAuthFlow { + if x != nil { + if x, ok := x.Flow.(*OAuthFlows_ClientCredentials); ok { + return x.ClientCredentials + } + } + return nil +} + +// Deprecated: Marked as deprecated in a2a.proto. +func (x *OAuthFlows) GetImplicit() *ImplicitOAuthFlow { + if x != nil { + if x, ok := x.Flow.(*OAuthFlows_Implicit); ok { + return x.Implicit + } + } + return nil +} + +// Deprecated: Marked as deprecated in a2a.proto. +func (x *OAuthFlows) GetPassword() *PasswordOAuthFlow { + if x != nil { + if x, ok := x.Flow.(*OAuthFlows_Password); ok { + return x.Password + } + } + return nil +} + +func (x *OAuthFlows) GetDeviceCode() *DeviceCodeOAuthFlow { + if x != nil { + if x, ok := x.Flow.(*OAuthFlows_DeviceCode); ok { + return x.DeviceCode + } + } + return nil +} + +type isOAuthFlows_Flow interface { + isOAuthFlows_Flow() +} + +type OAuthFlows_AuthorizationCode struct { + // Configuration for the OAuth Authorization Code flow. + AuthorizationCode *AuthorizationCodeOAuthFlow `protobuf:"bytes,1,opt,name=authorization_code,json=authorizationCode,proto3,oneof"` +} + +type OAuthFlows_ClientCredentials struct { + // Configuration for the OAuth Client Credentials flow. + ClientCredentials *ClientCredentialsOAuthFlow `protobuf:"bytes,2,opt,name=client_credentials,json=clientCredentials,proto3,oneof"` +} + +type OAuthFlows_Implicit struct { + // Deprecated: Use Authorization Code + PKCE instead. + // + // Deprecated: Marked as deprecated in a2a.proto. + Implicit *ImplicitOAuthFlow `protobuf:"bytes,3,opt,name=implicit,proto3,oneof"` +} + +type OAuthFlows_Password struct { + // Deprecated: Use Authorization Code + PKCE or Device Code. + // + // Deprecated: Marked as deprecated in a2a.proto. + Password *PasswordOAuthFlow `protobuf:"bytes,4,opt,name=password,proto3,oneof"` +} + +type OAuthFlows_DeviceCode struct { + // Configuration for the OAuth Device Code flow. + DeviceCode *DeviceCodeOAuthFlow `protobuf:"bytes,5,opt,name=device_code,json=deviceCode,proto3,oneof"` +} + +func (*OAuthFlows_AuthorizationCode) isOAuthFlows_Flow() {} + +func (*OAuthFlows_ClientCredentials) isOAuthFlows_Flow() {} + +func (*OAuthFlows_Implicit) isOAuthFlows_Flow() {} + +func (*OAuthFlows_Password) isOAuthFlows_Flow() {} + +func (*OAuthFlows_DeviceCode) isOAuthFlows_Flow() {} + +// Defines configuration details for the OAuth 2.0 Authorization Code flow. +type AuthorizationCodeOAuthFlow struct { + state protoimpl.MessageState `protogen:"open.v1"` + // The authorization URL to be used for this flow. + AuthorizationUrl string `protobuf:"bytes,1,opt,name=authorization_url,json=authorizationUrl,proto3" json:"authorization_url,omitempty"` + // The token URL to be used for this flow. + TokenUrl string `protobuf:"bytes,2,opt,name=token_url,json=tokenUrl,proto3" json:"token_url,omitempty"` + // The URL to be used for obtaining refresh tokens. + RefreshUrl string `protobuf:"bytes,3,opt,name=refresh_url,json=refreshUrl,proto3" json:"refresh_url,omitempty"` + // The available scopes for the OAuth2 security scheme. + Scopes map[string]string `protobuf:"bytes,4,rep,name=scopes,proto3" json:"scopes,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"` + // Indicates if PKCE (RFC 7636) is required for this flow. + // PKCE should always be used for public clients and is recommended for all clients. + PkceRequired bool `protobuf:"varint,5,opt,name=pkce_required,json=pkceRequired,proto3" json:"pkce_required,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *AuthorizationCodeOAuthFlow) Reset() { + *x = AuthorizationCodeOAuthFlow{} + mi := &file_a2a_proto_msgTypes[26] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *AuthorizationCodeOAuthFlow) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*AuthorizationCodeOAuthFlow) ProtoMessage() {} + +func (x *AuthorizationCodeOAuthFlow) ProtoReflect() protoreflect.Message { + mi := &file_a2a_proto_msgTypes[26] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use AuthorizationCodeOAuthFlow.ProtoReflect.Descriptor instead. +func (*AuthorizationCodeOAuthFlow) Descriptor() ([]byte, []int) { + return file_a2a_proto_rawDescGZIP(), []int{26} +} + +func (x *AuthorizationCodeOAuthFlow) GetAuthorizationUrl() string { + if x != nil { + return x.AuthorizationUrl + } + return "" +} + +func (x *AuthorizationCodeOAuthFlow) GetTokenUrl() string { + if x != nil { + return x.TokenUrl + } + return "" +} + +func (x *AuthorizationCodeOAuthFlow) GetRefreshUrl() string { + if x != nil { + return x.RefreshUrl + } + return "" +} + +func (x *AuthorizationCodeOAuthFlow) GetScopes() map[string]string { + if x != nil { + return x.Scopes + } + return nil +} + +func (x *AuthorizationCodeOAuthFlow) GetPkceRequired() bool { + if x != nil { + return x.PkceRequired + } + return false +} + +// Defines configuration details for the OAuth 2.0 Client Credentials flow. +type ClientCredentialsOAuthFlow struct { + state protoimpl.MessageState `protogen:"open.v1"` + // The token URL to be used for this flow. + TokenUrl string `protobuf:"bytes,1,opt,name=token_url,json=tokenUrl,proto3" json:"token_url,omitempty"` + // The URL to be used for obtaining refresh tokens. + RefreshUrl string `protobuf:"bytes,2,opt,name=refresh_url,json=refreshUrl,proto3" json:"refresh_url,omitempty"` + // The available scopes for the OAuth2 security scheme. + Scopes map[string]string `protobuf:"bytes,3,rep,name=scopes,proto3" json:"scopes,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ClientCredentialsOAuthFlow) Reset() { + *x = ClientCredentialsOAuthFlow{} + mi := &file_a2a_proto_msgTypes[27] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ClientCredentialsOAuthFlow) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ClientCredentialsOAuthFlow) ProtoMessage() {} + +func (x *ClientCredentialsOAuthFlow) ProtoReflect() protoreflect.Message { + mi := &file_a2a_proto_msgTypes[27] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ClientCredentialsOAuthFlow.ProtoReflect.Descriptor instead. +func (*ClientCredentialsOAuthFlow) Descriptor() ([]byte, []int) { + return file_a2a_proto_rawDescGZIP(), []int{27} +} + +func (x *ClientCredentialsOAuthFlow) GetTokenUrl() string { + if x != nil { + return x.TokenUrl + } + return "" +} + +func (x *ClientCredentialsOAuthFlow) GetRefreshUrl() string { + if x != nil { + return x.RefreshUrl + } + return "" +} + +func (x *ClientCredentialsOAuthFlow) GetScopes() map[string]string { + if x != nil { + return x.Scopes + } + return nil +} + +// Deprecated: Use Authorization Code + PKCE instead. +type ImplicitOAuthFlow struct { + state protoimpl.MessageState `protogen:"open.v1"` + // The authorization URL to be used for this flow. This MUST be in the + // form of a URL. The OAuth2 standard requires the use of TLS + AuthorizationUrl string `protobuf:"bytes,1,opt,name=authorization_url,json=authorizationUrl,proto3" json:"authorization_url,omitempty"` + // The URL to be used for obtaining refresh tokens. This MUST be in the + // form of a URL. The OAuth2 standard requires the use of TLS. + RefreshUrl string `protobuf:"bytes,2,opt,name=refresh_url,json=refreshUrl,proto3" json:"refresh_url,omitempty"` + // The available scopes for the OAuth2 security scheme. A map between the + // scope name and a short description for it. The map MAY be empty. + Scopes map[string]string `protobuf:"bytes,3,rep,name=scopes,proto3" json:"scopes,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ImplicitOAuthFlow) Reset() { + *x = ImplicitOAuthFlow{} + mi := &file_a2a_proto_msgTypes[28] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ImplicitOAuthFlow) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ImplicitOAuthFlow) ProtoMessage() {} + +func (x *ImplicitOAuthFlow) ProtoReflect() protoreflect.Message { + mi := &file_a2a_proto_msgTypes[28] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ImplicitOAuthFlow.ProtoReflect.Descriptor instead. +func (*ImplicitOAuthFlow) Descriptor() ([]byte, []int) { + return file_a2a_proto_rawDescGZIP(), []int{28} +} + +func (x *ImplicitOAuthFlow) GetAuthorizationUrl() string { + if x != nil { + return x.AuthorizationUrl + } + return "" +} + +func (x *ImplicitOAuthFlow) GetRefreshUrl() string { + if x != nil { + return x.RefreshUrl + } + return "" +} + +func (x *ImplicitOAuthFlow) GetScopes() map[string]string { + if x != nil { + return x.Scopes + } + return nil +} + +// Deprecated: Use Authorization Code + PKCE or Device Code. +type PasswordOAuthFlow struct { + state protoimpl.MessageState `protogen:"open.v1"` + // The token URL to be used for this flow. This MUST be in the form of a URL. + // The OAuth2 standard requires the use of TLS. + TokenUrl string `protobuf:"bytes,1,opt,name=token_url,json=tokenUrl,proto3" json:"token_url,omitempty"` + // The URL to be used for obtaining refresh tokens. This MUST be in the + // form of a URL. The OAuth2 standard requires the use of TLS. + RefreshUrl string `protobuf:"bytes,2,opt,name=refresh_url,json=refreshUrl,proto3" json:"refresh_url,omitempty"` + // The available scopes for the OAuth2 security scheme. A map between the + // scope name and a short description for it. The map MAY be empty. + Scopes map[string]string `protobuf:"bytes,3,rep,name=scopes,proto3" json:"scopes,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *PasswordOAuthFlow) Reset() { + *x = PasswordOAuthFlow{} + mi := &file_a2a_proto_msgTypes[29] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *PasswordOAuthFlow) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*PasswordOAuthFlow) ProtoMessage() {} + +func (x *PasswordOAuthFlow) ProtoReflect() protoreflect.Message { + mi := &file_a2a_proto_msgTypes[29] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use PasswordOAuthFlow.ProtoReflect.Descriptor instead. +func (*PasswordOAuthFlow) Descriptor() ([]byte, []int) { + return file_a2a_proto_rawDescGZIP(), []int{29} +} + +func (x *PasswordOAuthFlow) GetTokenUrl() string { + if x != nil { + return x.TokenUrl + } + return "" +} + +func (x *PasswordOAuthFlow) GetRefreshUrl() string { + if x != nil { + return x.RefreshUrl + } + return "" +} + +func (x *PasswordOAuthFlow) GetScopes() map[string]string { + if x != nil { + return x.Scopes + } + return nil +} + +// Defines configuration details for the OAuth 2.0 Device Code flow (RFC 8628). +// This flow is designed for input-constrained devices such as IoT devices, +// and CLI tools where the user authenticates on a separate device. +type DeviceCodeOAuthFlow struct { + state protoimpl.MessageState `protogen:"open.v1"` + // The device authorization endpoint URL. + DeviceAuthorizationUrl string `protobuf:"bytes,1,opt,name=device_authorization_url,json=deviceAuthorizationUrl,proto3" json:"device_authorization_url,omitempty"` + // The token URL to be used for this flow. + TokenUrl string `protobuf:"bytes,2,opt,name=token_url,json=tokenUrl,proto3" json:"token_url,omitempty"` + // The URL to be used for obtaining refresh tokens. + RefreshUrl string `protobuf:"bytes,3,opt,name=refresh_url,json=refreshUrl,proto3" json:"refresh_url,omitempty"` + // The available scopes for the OAuth2 security scheme. + Scopes map[string]string `protobuf:"bytes,4,rep,name=scopes,proto3" json:"scopes,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *DeviceCodeOAuthFlow) Reset() { + *x = DeviceCodeOAuthFlow{} + mi := &file_a2a_proto_msgTypes[30] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *DeviceCodeOAuthFlow) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*DeviceCodeOAuthFlow) ProtoMessage() {} + +func (x *DeviceCodeOAuthFlow) ProtoReflect() protoreflect.Message { + mi := &file_a2a_proto_msgTypes[30] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use DeviceCodeOAuthFlow.ProtoReflect.Descriptor instead. +func (*DeviceCodeOAuthFlow) Descriptor() ([]byte, []int) { + return file_a2a_proto_rawDescGZIP(), []int{30} +} + +func (x *DeviceCodeOAuthFlow) GetDeviceAuthorizationUrl() string { + if x != nil { + return x.DeviceAuthorizationUrl + } + return "" +} + +func (x *DeviceCodeOAuthFlow) GetTokenUrl() string { + if x != nil { + return x.TokenUrl + } + return "" +} + +func (x *DeviceCodeOAuthFlow) GetRefreshUrl() string { + if x != nil { + return x.RefreshUrl + } + return "" +} + +func (x *DeviceCodeOAuthFlow) GetScopes() map[string]string { + if x != nil { + return x.Scopes + } + return nil +} + +// Represents a request for the `SendMessage` method. +type SendMessageRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Optional. Opaque routing identifier. Must match the `tenant` value from + // the selected `AgentInterface` in the Agent Card when that field is set. + Tenant string `protobuf:"bytes,1,opt,name=tenant,proto3" json:"tenant,omitempty"` + // The message to send to the agent. + Message *Message `protobuf:"bytes,2,opt,name=message,proto3" json:"message,omitempty"` + // Configuration for the send request. + Configuration *SendMessageConfiguration `protobuf:"bytes,3,opt,name=configuration,proto3" json:"configuration,omitempty"` + // A flexible key-value map for passing additional context or parameters. + Metadata *structpb.Struct `protobuf:"bytes,4,opt,name=metadata,proto3" json:"metadata,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *SendMessageRequest) Reset() { + *x = SendMessageRequest{} + mi := &file_a2a_proto_msgTypes[31] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *SendMessageRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*SendMessageRequest) ProtoMessage() {} + +func (x *SendMessageRequest) ProtoReflect() protoreflect.Message { + mi := &file_a2a_proto_msgTypes[31] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use SendMessageRequest.ProtoReflect.Descriptor instead. +func (*SendMessageRequest) Descriptor() ([]byte, []int) { + return file_a2a_proto_rawDescGZIP(), []int{31} +} + +func (x *SendMessageRequest) GetTenant() string { + if x != nil { + return x.Tenant + } + return "" +} + +func (x *SendMessageRequest) GetMessage() *Message { + if x != nil { + return x.Message + } + return nil +} + +func (x *SendMessageRequest) GetConfiguration() *SendMessageConfiguration { + if x != nil { + return x.Configuration + } + return nil +} + +func (x *SendMessageRequest) GetMetadata() *structpb.Struct { + if x != nil { + return x.Metadata + } + return nil +} + +// Represents a request for the `GetTask` method. +type GetTaskRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Optional. Opaque routing identifier. Must match the `tenant` value from + // the selected `AgentInterface` in the Agent Card when that field is set. + Tenant string `protobuf:"bytes,1,opt,name=tenant,proto3" json:"tenant,omitempty"` + // The resource ID of the task to retrieve. + Id string `protobuf:"bytes,2,opt,name=id,proto3" json:"id,omitempty"` + // The maximum number of most recent messages from the task's history to retrieve. An + // unset value means the client does not impose any limit. A value of zero is + // a request to not include any messages. The server MUST NOT return more + // messages than the provided value, but MAY apply a lower limit. + HistoryLength *int32 `protobuf:"varint,3,opt,name=history_length,json=historyLength,proto3,oneof" json:"history_length,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *GetTaskRequest) Reset() { + *x = GetTaskRequest{} + mi := &file_a2a_proto_msgTypes[32] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *GetTaskRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GetTaskRequest) ProtoMessage() {} + +func (x *GetTaskRequest) ProtoReflect() protoreflect.Message { + mi := &file_a2a_proto_msgTypes[32] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use GetTaskRequest.ProtoReflect.Descriptor instead. +func (*GetTaskRequest) Descriptor() ([]byte, []int) { + return file_a2a_proto_rawDescGZIP(), []int{32} +} + +func (x *GetTaskRequest) GetTenant() string { + if x != nil { + return x.Tenant + } + return "" +} + +func (x *GetTaskRequest) GetId() string { + if x != nil { + return x.Id + } + return "" +} + +func (x *GetTaskRequest) GetHistoryLength() int32 { + if x != nil && x.HistoryLength != nil { + return *x.HistoryLength + } + return 0 +} + +// Parameters for listing tasks with optional filtering criteria. +type ListTasksRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Optional. Opaque routing identifier. Must match the `tenant` value from + // the selected `AgentInterface` in the Agent Card when that field is set. + Tenant string `protobuf:"bytes,1,opt,name=tenant,proto3" json:"tenant,omitempty"` + // Filter tasks by context ID to get tasks from a specific conversation or session. + ContextId string `protobuf:"bytes,2,opt,name=context_id,json=contextId,proto3" json:"context_id,omitempty"` + // Filter tasks by their current status state. + Status TaskState `protobuf:"varint,3,opt,name=status,proto3,enum=lf.a2a.v1.TaskState" json:"status,omitempty"` + // The maximum number of tasks to return. The service may return fewer than this value. + // If unspecified, at most 50 tasks will be returned. + // The minimum value is 1. + // The maximum value is 100. + PageSize *int32 `protobuf:"varint,4,opt,name=page_size,json=pageSize,proto3,oneof" json:"page_size,omitempty"` + // A page token, received from a previous `ListTasks` call. + // `ListTasksResponse.next_page_token`. + // Provide this to retrieve the subsequent page. + PageToken string `protobuf:"bytes,5,opt,name=page_token,json=pageToken,proto3" json:"page_token,omitempty"` + // The maximum number of messages to include in each task's history. + HistoryLength *int32 `protobuf:"varint,6,opt,name=history_length,json=historyLength,proto3,oneof" json:"history_length,omitempty"` + // Filter tasks which have a status updated after the provided timestamp in ISO 8601 format (e.g., "2023-10-27T10:00:00Z"). + // Only tasks with a status timestamp time greater than or equal to this value will be returned. + StatusTimestampAfter *timestamppb.Timestamp `protobuf:"bytes,7,opt,name=status_timestamp_after,json=statusTimestampAfter,proto3" json:"status_timestamp_after,omitempty"` + // Whether to include artifacts in the returned tasks. + // Defaults to false to reduce payload size. + IncludeArtifacts *bool `protobuf:"varint,8,opt,name=include_artifacts,json=includeArtifacts,proto3,oneof" json:"include_artifacts,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ListTasksRequest) Reset() { + *x = ListTasksRequest{} + mi := &file_a2a_proto_msgTypes[33] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ListTasksRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ListTasksRequest) ProtoMessage() {} + +func (x *ListTasksRequest) ProtoReflect() protoreflect.Message { + mi := &file_a2a_proto_msgTypes[33] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ListTasksRequest.ProtoReflect.Descriptor instead. +func (*ListTasksRequest) Descriptor() ([]byte, []int) { + return file_a2a_proto_rawDescGZIP(), []int{33} +} + +func (x *ListTasksRequest) GetTenant() string { + if x != nil { + return x.Tenant + } + return "" +} + +func (x *ListTasksRequest) GetContextId() string { + if x != nil { + return x.ContextId + } + return "" +} + +func (x *ListTasksRequest) GetStatus() TaskState { + if x != nil { + return x.Status + } + return TaskState_TASK_STATE_UNSPECIFIED +} + +func (x *ListTasksRequest) GetPageSize() int32 { + if x != nil && x.PageSize != nil { + return *x.PageSize + } + return 0 +} + +func (x *ListTasksRequest) GetPageToken() string { + if x != nil { + return x.PageToken + } + return "" +} + +func (x *ListTasksRequest) GetHistoryLength() int32 { + if x != nil && x.HistoryLength != nil { + return *x.HistoryLength + } + return 0 +} + +func (x *ListTasksRequest) GetStatusTimestampAfter() *timestamppb.Timestamp { + if x != nil { + return x.StatusTimestampAfter + } + return nil +} + +func (x *ListTasksRequest) GetIncludeArtifacts() bool { + if x != nil && x.IncludeArtifacts != nil { + return *x.IncludeArtifacts + } + return false +} + +// Result object for `ListTasks` method containing an array of tasks and pagination information. +type ListTasksResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Array of tasks matching the specified criteria. + Tasks []*Task `protobuf:"bytes,1,rep,name=tasks,proto3" json:"tasks,omitempty"` + // A token to retrieve the next page of results, or empty if there are no more results in the list. + NextPageToken string `protobuf:"bytes,2,opt,name=next_page_token,json=nextPageToken,proto3" json:"next_page_token,omitempty"` + // The page size used for this response. + PageSize int32 `protobuf:"varint,3,opt,name=page_size,json=pageSize,proto3" json:"page_size,omitempty"` + // Total number of tasks available (before pagination). + TotalSize int32 `protobuf:"varint,4,opt,name=total_size,json=totalSize,proto3" json:"total_size,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ListTasksResponse) Reset() { + *x = ListTasksResponse{} + mi := &file_a2a_proto_msgTypes[34] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ListTasksResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ListTasksResponse) ProtoMessage() {} + +func (x *ListTasksResponse) ProtoReflect() protoreflect.Message { + mi := &file_a2a_proto_msgTypes[34] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ListTasksResponse.ProtoReflect.Descriptor instead. +func (*ListTasksResponse) Descriptor() ([]byte, []int) { + return file_a2a_proto_rawDescGZIP(), []int{34} +} + +func (x *ListTasksResponse) GetTasks() []*Task { + if x != nil { + return x.Tasks + } + return nil +} + +func (x *ListTasksResponse) GetNextPageToken() string { + if x != nil { + return x.NextPageToken + } + return "" +} + +func (x *ListTasksResponse) GetPageSize() int32 { + if x != nil { + return x.PageSize + } + return 0 +} + +func (x *ListTasksResponse) GetTotalSize() int32 { + if x != nil { + return x.TotalSize + } + return 0 +} + +// Represents a request for the `CancelTask` method. +type CancelTaskRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Optional. Opaque routing identifier. Must match the `tenant` value from + // the selected `AgentInterface` in the Agent Card when that field is set. + Tenant string `protobuf:"bytes,1,opt,name=tenant,proto3" json:"tenant,omitempty"` + // The resource ID of the task to cancel. + Id string `protobuf:"bytes,2,opt,name=id,proto3" json:"id,omitempty"` + // A flexible key-value map for passing additional context or parameters. + Metadata *structpb.Struct `protobuf:"bytes,3,opt,name=metadata,proto3" json:"metadata,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *CancelTaskRequest) Reset() { + *x = CancelTaskRequest{} + mi := &file_a2a_proto_msgTypes[35] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *CancelTaskRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*CancelTaskRequest) ProtoMessage() {} + +func (x *CancelTaskRequest) ProtoReflect() protoreflect.Message { + mi := &file_a2a_proto_msgTypes[35] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use CancelTaskRequest.ProtoReflect.Descriptor instead. +func (*CancelTaskRequest) Descriptor() ([]byte, []int) { + return file_a2a_proto_rawDescGZIP(), []int{35} +} + +func (x *CancelTaskRequest) GetTenant() string { + if x != nil { + return x.Tenant + } + return "" +} + +func (x *CancelTaskRequest) GetId() string { + if x != nil { + return x.Id + } + return "" +} + +func (x *CancelTaskRequest) GetMetadata() *structpb.Struct { + if x != nil { + return x.Metadata + } + return nil +} + +// Represents a request for the `GetTaskPushNotificationConfig` method. +type GetTaskPushNotificationConfigRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Optional. Opaque routing identifier. Must match the `tenant` value from + // the selected `AgentInterface` in the Agent Card when that field is set. + Tenant string `protobuf:"bytes,1,opt,name=tenant,proto3" json:"tenant,omitempty"` + // The parent task resource ID. + TaskId string `protobuf:"bytes,2,opt,name=task_id,json=taskId,proto3" json:"task_id,omitempty"` + // The resource ID of the configuration to retrieve. + Id string `protobuf:"bytes,3,opt,name=id,proto3" json:"id,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *GetTaskPushNotificationConfigRequest) Reset() { + *x = GetTaskPushNotificationConfigRequest{} + mi := &file_a2a_proto_msgTypes[36] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *GetTaskPushNotificationConfigRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GetTaskPushNotificationConfigRequest) ProtoMessage() {} + +func (x *GetTaskPushNotificationConfigRequest) ProtoReflect() protoreflect.Message { + mi := &file_a2a_proto_msgTypes[36] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use GetTaskPushNotificationConfigRequest.ProtoReflect.Descriptor instead. +func (*GetTaskPushNotificationConfigRequest) Descriptor() ([]byte, []int) { + return file_a2a_proto_rawDescGZIP(), []int{36} +} + +func (x *GetTaskPushNotificationConfigRequest) GetTenant() string { + if x != nil { + return x.Tenant + } + return "" +} + +func (x *GetTaskPushNotificationConfigRequest) GetTaskId() string { + if x != nil { + return x.TaskId + } + return "" +} + +func (x *GetTaskPushNotificationConfigRequest) GetId() string { + if x != nil { + return x.Id + } + return "" +} + +// Represents a request for the `DeleteTaskPushNotificationConfig` method. +type DeleteTaskPushNotificationConfigRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Optional. Opaque routing identifier. Must match the `tenant` value from + // the selected `AgentInterface` in the Agent Card when that field is set. + Tenant string `protobuf:"bytes,1,opt,name=tenant,proto3" json:"tenant,omitempty"` + // The parent task resource ID. + TaskId string `protobuf:"bytes,2,opt,name=task_id,json=taskId,proto3" json:"task_id,omitempty"` + // The resource ID of the configuration to delete. + Id string `protobuf:"bytes,3,opt,name=id,proto3" json:"id,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *DeleteTaskPushNotificationConfigRequest) Reset() { + *x = DeleteTaskPushNotificationConfigRequest{} + mi := &file_a2a_proto_msgTypes[37] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *DeleteTaskPushNotificationConfigRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*DeleteTaskPushNotificationConfigRequest) ProtoMessage() {} + +func (x *DeleteTaskPushNotificationConfigRequest) ProtoReflect() protoreflect.Message { + mi := &file_a2a_proto_msgTypes[37] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use DeleteTaskPushNotificationConfigRequest.ProtoReflect.Descriptor instead. +func (*DeleteTaskPushNotificationConfigRequest) Descriptor() ([]byte, []int) { + return file_a2a_proto_rawDescGZIP(), []int{37} +} + +func (x *DeleteTaskPushNotificationConfigRequest) GetTenant() string { + if x != nil { + return x.Tenant + } + return "" +} + +func (x *DeleteTaskPushNotificationConfigRequest) GetTaskId() string { + if x != nil { + return x.TaskId + } + return "" +} + +func (x *DeleteTaskPushNotificationConfigRequest) GetId() string { + if x != nil { + return x.Id + } + return "" +} + +// Represents a request for the `SubscribeToTask` method. +type SubscribeToTaskRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Optional. Opaque routing identifier. Must match the `tenant` value from + // the selected `AgentInterface` in the Agent Card when that field is set. + Tenant string `protobuf:"bytes,1,opt,name=tenant,proto3" json:"tenant,omitempty"` + // The resource ID of the task to subscribe to. + Id string `protobuf:"bytes,2,opt,name=id,proto3" json:"id,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *SubscribeToTaskRequest) Reset() { + *x = SubscribeToTaskRequest{} + mi := &file_a2a_proto_msgTypes[38] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *SubscribeToTaskRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*SubscribeToTaskRequest) ProtoMessage() {} + +func (x *SubscribeToTaskRequest) ProtoReflect() protoreflect.Message { + mi := &file_a2a_proto_msgTypes[38] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use SubscribeToTaskRequest.ProtoReflect.Descriptor instead. +func (*SubscribeToTaskRequest) Descriptor() ([]byte, []int) { + return file_a2a_proto_rawDescGZIP(), []int{38} +} + +func (x *SubscribeToTaskRequest) GetTenant() string { + if x != nil { + return x.Tenant + } + return "" +} + +func (x *SubscribeToTaskRequest) GetId() string { + if x != nil { + return x.Id + } + return "" +} + +// Represents a request for the `ListTaskPushNotificationConfigs` method. +type ListTaskPushNotificationConfigsRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Optional. Opaque routing identifier. Must match the `tenant` value from + // the selected `AgentInterface` in the Agent Card when that field is set. + Tenant string `protobuf:"bytes,4,opt,name=tenant,proto3" json:"tenant,omitempty"` + // The parent task resource ID. + TaskId string `protobuf:"bytes,1,opt,name=task_id,json=taskId,proto3" json:"task_id,omitempty"` + // The maximum number of configurations to return. + PageSize int32 `protobuf:"varint,2,opt,name=page_size,json=pageSize,proto3" json:"page_size,omitempty"` + // A page token received from a previous `ListTaskPushNotificationConfigsRequest` call. + PageToken string `protobuf:"bytes,3,opt,name=page_token,json=pageToken,proto3" json:"page_token,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ListTaskPushNotificationConfigsRequest) Reset() { + *x = ListTaskPushNotificationConfigsRequest{} + mi := &file_a2a_proto_msgTypes[39] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ListTaskPushNotificationConfigsRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ListTaskPushNotificationConfigsRequest) ProtoMessage() {} + +func (x *ListTaskPushNotificationConfigsRequest) ProtoReflect() protoreflect.Message { + mi := &file_a2a_proto_msgTypes[39] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ListTaskPushNotificationConfigsRequest.ProtoReflect.Descriptor instead. +func (*ListTaskPushNotificationConfigsRequest) Descriptor() ([]byte, []int) { + return file_a2a_proto_rawDescGZIP(), []int{39} +} + +func (x *ListTaskPushNotificationConfigsRequest) GetTenant() string { + if x != nil { + return x.Tenant + } + return "" +} + +func (x *ListTaskPushNotificationConfigsRequest) GetTaskId() string { + if x != nil { + return x.TaskId + } + return "" +} + +func (x *ListTaskPushNotificationConfigsRequest) GetPageSize() int32 { + if x != nil { + return x.PageSize + } + return 0 +} + +func (x *ListTaskPushNotificationConfigsRequest) GetPageToken() string { + if x != nil { + return x.PageToken + } + return "" +} + +// Represents a request for the `GetExtendedAgentCard` method. +type GetExtendedAgentCardRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Optional. Opaque routing identifier. Must match the `tenant` value from + // the selected `AgentInterface` in the Agent Card when that field is set. + Tenant string `protobuf:"bytes,1,opt,name=tenant,proto3" json:"tenant,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *GetExtendedAgentCardRequest) Reset() { + *x = GetExtendedAgentCardRequest{} + mi := &file_a2a_proto_msgTypes[40] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *GetExtendedAgentCardRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GetExtendedAgentCardRequest) ProtoMessage() {} + +func (x *GetExtendedAgentCardRequest) ProtoReflect() protoreflect.Message { + mi := &file_a2a_proto_msgTypes[40] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use GetExtendedAgentCardRequest.ProtoReflect.Descriptor instead. +func (*GetExtendedAgentCardRequest) Descriptor() ([]byte, []int) { + return file_a2a_proto_rawDescGZIP(), []int{40} +} + +func (x *GetExtendedAgentCardRequest) GetTenant() string { + if x != nil { + return x.Tenant + } + return "" +} + +// Represents the response for the `SendMessage` method. +type SendMessageResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + // The payload of the response. + // + // Types that are valid to be assigned to Payload: + // + // *SendMessageResponse_Task + // *SendMessageResponse_Message + Payload isSendMessageResponse_Payload `protobuf_oneof:"payload"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *SendMessageResponse) Reset() { + *x = SendMessageResponse{} + mi := &file_a2a_proto_msgTypes[41] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *SendMessageResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*SendMessageResponse) ProtoMessage() {} + +func (x *SendMessageResponse) ProtoReflect() protoreflect.Message { + mi := &file_a2a_proto_msgTypes[41] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use SendMessageResponse.ProtoReflect.Descriptor instead. +func (*SendMessageResponse) Descriptor() ([]byte, []int) { + return file_a2a_proto_rawDescGZIP(), []int{41} +} + +func (x *SendMessageResponse) GetPayload() isSendMessageResponse_Payload { + if x != nil { + return x.Payload + } + return nil +} + +func (x *SendMessageResponse) GetTask() *Task { + if x != nil { + if x, ok := x.Payload.(*SendMessageResponse_Task); ok { + return x.Task + } + } + return nil +} + +func (x *SendMessageResponse) GetMessage() *Message { + if x != nil { + if x, ok := x.Payload.(*SendMessageResponse_Message); ok { + return x.Message + } + } + return nil +} + +type isSendMessageResponse_Payload interface { + isSendMessageResponse_Payload() +} + +type SendMessageResponse_Task struct { + // The task created or updated by the message. + Task *Task `protobuf:"bytes,1,opt,name=task,proto3,oneof"` +} + +type SendMessageResponse_Message struct { + // A message from the agent. + Message *Message `protobuf:"bytes,2,opt,name=message,proto3,oneof"` +} + +func (*SendMessageResponse_Task) isSendMessageResponse_Payload() {} + +func (*SendMessageResponse_Message) isSendMessageResponse_Payload() {} + +// A wrapper object used in streaming operations to encapsulate different types of response data. +type StreamResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + // The payload of the stream response. + // + // Types that are valid to be assigned to Payload: + // + // *StreamResponse_Task + // *StreamResponse_Message + // *StreamResponse_StatusUpdate + // *StreamResponse_ArtifactUpdate + Payload isStreamResponse_Payload `protobuf_oneof:"payload"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *StreamResponse) Reset() { + *x = StreamResponse{} + mi := &file_a2a_proto_msgTypes[42] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *StreamResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*StreamResponse) ProtoMessage() {} + +func (x *StreamResponse) ProtoReflect() protoreflect.Message { + mi := &file_a2a_proto_msgTypes[42] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use StreamResponse.ProtoReflect.Descriptor instead. +func (*StreamResponse) Descriptor() ([]byte, []int) { + return file_a2a_proto_rawDescGZIP(), []int{42} +} + +func (x *StreamResponse) GetPayload() isStreamResponse_Payload { + if x != nil { + return x.Payload + } + return nil +} + +func (x *StreamResponse) GetTask() *Task { + if x != nil { + if x, ok := x.Payload.(*StreamResponse_Task); ok { + return x.Task + } + } + return nil +} + +func (x *StreamResponse) GetMessage() *Message { + if x != nil { + if x, ok := x.Payload.(*StreamResponse_Message); ok { + return x.Message + } + } + return nil +} + +func (x *StreamResponse) GetStatusUpdate() *TaskStatusUpdateEvent { + if x != nil { + if x, ok := x.Payload.(*StreamResponse_StatusUpdate); ok { + return x.StatusUpdate + } + } + return nil +} + +func (x *StreamResponse) GetArtifactUpdate() *TaskArtifactUpdateEvent { + if x != nil { + if x, ok := x.Payload.(*StreamResponse_ArtifactUpdate); ok { + return x.ArtifactUpdate + } + } + return nil +} + +type isStreamResponse_Payload interface { + isStreamResponse_Payload() +} + +type StreamResponse_Task struct { + // A Task object containing the current state of the task. + Task *Task `protobuf:"bytes,1,opt,name=task,proto3,oneof"` +} + +type StreamResponse_Message struct { + // A Message object containing a message from the agent. + Message *Message `protobuf:"bytes,2,opt,name=message,proto3,oneof"` +} + +type StreamResponse_StatusUpdate struct { + // An event indicating a task status update. + StatusUpdate *TaskStatusUpdateEvent `protobuf:"bytes,3,opt,name=status_update,json=statusUpdate,proto3,oneof"` +} + +type StreamResponse_ArtifactUpdate struct { + // An event indicating a task artifact update. + ArtifactUpdate *TaskArtifactUpdateEvent `protobuf:"bytes,4,opt,name=artifact_update,json=artifactUpdate,proto3,oneof"` +} + +func (*StreamResponse_Task) isStreamResponse_Payload() {} + +func (*StreamResponse_Message) isStreamResponse_Payload() {} + +func (*StreamResponse_StatusUpdate) isStreamResponse_Payload() {} + +func (*StreamResponse_ArtifactUpdate) isStreamResponse_Payload() {} + +// Represents a successful response for the `ListTaskPushNotificationConfigs` +// method. +type ListTaskPushNotificationConfigsResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + // The list of push notification configurations. + Configs []*TaskPushNotificationConfig `protobuf:"bytes,1,rep,name=configs,proto3" json:"configs,omitempty"` + // A token to retrieve the next page of results, or empty if there are no more results in the list. + NextPageToken string `protobuf:"bytes,2,opt,name=next_page_token,json=nextPageToken,proto3" json:"next_page_token,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ListTaskPushNotificationConfigsResponse) Reset() { + *x = ListTaskPushNotificationConfigsResponse{} + mi := &file_a2a_proto_msgTypes[43] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ListTaskPushNotificationConfigsResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ListTaskPushNotificationConfigsResponse) ProtoMessage() {} + +func (x *ListTaskPushNotificationConfigsResponse) ProtoReflect() protoreflect.Message { + mi := &file_a2a_proto_msgTypes[43] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ListTaskPushNotificationConfigsResponse.ProtoReflect.Descriptor instead. +func (*ListTaskPushNotificationConfigsResponse) Descriptor() ([]byte, []int) { + return file_a2a_proto_rawDescGZIP(), []int{43} +} + +func (x *ListTaskPushNotificationConfigsResponse) GetConfigs() []*TaskPushNotificationConfig { + if x != nil { + return x.Configs + } + return nil +} + +func (x *ListTaskPushNotificationConfigsResponse) GetNextPageToken() string { + if x != nil { + return x.NextPageToken + } + return "" +} + +var File_a2a_proto protoreflect.FileDescriptor + +const file_a2a_proto_rawDesc = "" + + "\n" + + "\ta2a.proto\x12\tlf.a2a.v1\x1a\x1cgoogle/api/annotations.proto\x1a\x17google/api/client.proto\x1a\x1fgoogle/api/field_behavior.proto\x1a\x1bgoogle/protobuf/empty.proto\x1a\x1cgoogle/protobuf/struct.proto\x1a\x1fgoogle/protobuf/timestamp.proto\"\xa6\x02\n" + + "\x18SendMessageConfiguration\x122\n" + + "\x15accepted_output_modes\x18\x01 \x03(\tR\x13acceptedOutputModes\x12h\n" + + "\x1dtask_push_notification_config\x18\x02 \x01(\v2%.lf.a2a.v1.TaskPushNotificationConfigR\x1ataskPushNotificationConfig\x12*\n" + + "\x0ehistory_length\x18\x03 \x01(\x05H\x00R\rhistoryLength\x88\x01\x01\x12-\n" + + "\x12return_immediately\x18\x04 \x01(\bR\x11returnImmediatelyB\x11\n" + + "\x0f_history_length\"\x84\x02\n" + + "\x04Task\x12\x13\n" + + "\x02id\x18\x01 \x01(\tB\x03\xe0A\x02R\x02id\x12\x1d\n" + + "\n" + + "context_id\x18\x02 \x01(\tR\tcontextId\x122\n" + + "\x06status\x18\x03 \x01(\v2\x15.lf.a2a.v1.TaskStatusB\x03\xe0A\x02R\x06status\x121\n" + + "\tartifacts\x18\x04 \x03(\v2\x13.lf.a2a.v1.ArtifactR\tartifacts\x12,\n" + + "\ahistory\x18\x05 \x03(\v2\x12.lf.a2a.v1.MessageR\ahistory\x123\n" + + "\bmetadata\x18\x06 \x01(\v2\x17.google.protobuf.StructR\bmetadata\"\xa5\x01\n" + + "\n" + + "TaskStatus\x12/\n" + + "\x05state\x18\x01 \x01(\x0e2\x14.lf.a2a.v1.TaskStateB\x03\xe0A\x02R\x05state\x12,\n" + + "\amessage\x18\x02 \x01(\v2\x12.lf.a2a.v1.MessageR\amessage\x128\n" + + "\ttimestamp\x18\x03 \x01(\v2\x1a.google.protobuf.TimestampR\ttimestamp\"\xed\x01\n" + + "\x04Part\x12\x14\n" + + "\x04text\x18\x01 \x01(\tH\x00R\x04text\x12\x12\n" + + "\x03raw\x18\x02 \x01(\fH\x00R\x03raw\x12\x12\n" + + "\x03url\x18\x03 \x01(\tH\x00R\x03url\x12,\n" + + "\x04data\x18\x04 \x01(\v2\x16.google.protobuf.ValueH\x00R\x04data\x123\n" + + "\bmetadata\x18\x05 \x01(\v2\x17.google.protobuf.StructR\bmetadata\x12\x1a\n" + + "\bfilename\x18\x06 \x01(\tR\bfilename\x12\x1d\n" + + "\n" + + "media_type\x18\a \x01(\tR\tmediaTypeB\t\n" + + "\acontent\"\xbe\x02\n" + + "\aMessage\x12\"\n" + + "\n" + + "message_id\x18\x01 \x01(\tB\x03\xe0A\x02R\tmessageId\x12\x1d\n" + + "\n" + + "context_id\x18\x02 \x01(\tR\tcontextId\x12\x17\n" + + "\atask_id\x18\x03 \x01(\tR\x06taskId\x12(\n" + + "\x04role\x18\x04 \x01(\x0e2\x0f.lf.a2a.v1.RoleB\x03\xe0A\x02R\x04role\x12*\n" + + "\x05parts\x18\x05 \x03(\v2\x0f.lf.a2a.v1.PartB\x03\xe0A\x02R\x05parts\x123\n" + + "\bmetadata\x18\x06 \x01(\v2\x17.google.protobuf.StructR\bmetadata\x12\x1e\n" + + "\n" + + "extensions\x18\a \x03(\tR\n" + + "extensions\x12,\n" + + "\x12reference_task_ids\x18\b \x03(\tR\x10referenceTaskIds\"\xe7\x01\n" + + "\bArtifact\x12$\n" + + "\vartifact_id\x18\x01 \x01(\tB\x03\xe0A\x02R\n" + + "artifactId\x12\x12\n" + + "\x04name\x18\x02 \x01(\tR\x04name\x12 \n" + + "\vdescription\x18\x03 \x01(\tR\vdescription\x12*\n" + + "\x05parts\x18\x04 \x03(\v2\x0f.lf.a2a.v1.PartB\x03\xe0A\x02R\x05parts\x123\n" + + "\bmetadata\x18\x05 \x01(\v2\x17.google.protobuf.StructR\bmetadata\x12\x1e\n" + + "\n" + + "extensions\x18\x06 \x03(\tR\n" + + "extensions\"\xc2\x01\n" + + "\x15TaskStatusUpdateEvent\x12\x1c\n" + + "\atask_id\x18\x01 \x01(\tB\x03\xe0A\x02R\x06taskId\x12\"\n" + + "\n" + + "context_id\x18\x02 \x01(\tB\x03\xe0A\x02R\tcontextId\x122\n" + + "\x06status\x18\x03 \x01(\v2\x15.lf.a2a.v1.TaskStatusB\x03\xe0A\x02R\x06status\x123\n" + + "\bmetadata\x18\x04 \x01(\v2\x17.google.protobuf.StructR\bmetadata\"\xfd\x01\n" + + "\x17TaskArtifactUpdateEvent\x12\x1c\n" + + "\atask_id\x18\x01 \x01(\tB\x03\xe0A\x02R\x06taskId\x12\"\n" + + "\n" + + "context_id\x18\x02 \x01(\tB\x03\xe0A\x02R\tcontextId\x124\n" + + "\bartifact\x18\x03 \x01(\v2\x13.lf.a2a.v1.ArtifactB\x03\xe0A\x02R\bartifact\x12\x16\n" + + "\x06append\x18\x04 \x01(\bR\x06append\x12\x1d\n" + + "\n" + + "last_chunk\x18\x05 \x01(\bR\tlastChunk\x123\n" + + "\bmetadata\x18\x06 \x01(\v2\x17.google.protobuf.StructR\bmetadata\"S\n" + + "\x12AuthenticationInfo\x12\x1b\n" + + "\x06scheme\x18\x01 \x01(\tB\x03\xe0A\x02R\x06scheme\x12 \n" + + "\vcredentials\x18\x02 \x01(\tR\vcredentials\"\x9f\x01\n" + + "\x0eAgentInterface\x12\x15\n" + + "\x03url\x18\x01 \x01(\tB\x03\xe0A\x02R\x03url\x12.\n" + + "\x10protocol_binding\x18\x02 \x01(\tB\x03\xe0A\x02R\x0fprotocolBinding\x12\x16\n" + + "\x06tenant\x18\x03 \x01(\tR\x06tenant\x12.\n" + + "\x10protocol_version\x18\x04 \x01(\tB\x03\xe0A\x02R\x0fprotocolVersion\"\x98\a\n" + + "\tAgentCard\x12\x17\n" + + "\x04name\x18\x01 \x01(\tB\x03\xe0A\x02R\x04name\x12%\n" + + "\vdescription\x18\x02 \x01(\tB\x03\xe0A\x02R\vdescription\x12Q\n" + + "\x14supported_interfaces\x18\x03 \x03(\v2\x19.lf.a2a.v1.AgentInterfaceB\x03\xe0A\x02R\x13supportedInterfaces\x124\n" + + "\bprovider\x18\x04 \x01(\v2\x18.lf.a2a.v1.AgentProviderR\bprovider\x12\x1d\n" + + "\aversion\x18\x05 \x01(\tB\x03\xe0A\x02R\aversion\x120\n" + + "\x11documentation_url\x18\x06 \x01(\tH\x00R\x10documentationUrl\x88\x01\x01\x12E\n" + + "\fcapabilities\x18\a \x01(\v2\x1c.lf.a2a.v1.AgentCapabilitiesB\x03\xe0A\x02R\fcapabilities\x12T\n" + + "\x10security_schemes\x18\b \x03(\v2).lf.a2a.v1.AgentCard.SecuritySchemesEntryR\x0fsecuritySchemes\x12S\n" + + "\x15security_requirements\x18\t \x03(\v2\x1e.lf.a2a.v1.SecurityRequirementR\x14securityRequirements\x123\n" + + "\x13default_input_modes\x18\n" + + " \x03(\tB\x03\xe0A\x02R\x11defaultInputModes\x125\n" + + "\x14default_output_modes\x18\v \x03(\tB\x03\xe0A\x02R\x12defaultOutputModes\x122\n" + + "\x06skills\x18\f \x03(\v2\x15.lf.a2a.v1.AgentSkillB\x03\xe0A\x02R\x06skills\x12=\n" + + "\n" + + "signatures\x18\r \x03(\v2\x1d.lf.a2a.v1.AgentCardSignatureR\n" + + "signatures\x12\x1e\n" + + "\bicon_url\x18\x0e \x01(\tH\x01R\aiconUrl\x88\x01\x01\x1a]\n" + + "\x14SecuritySchemesEntry\x12\x10\n" + + "\x03key\x18\x01 \x01(\tR\x03key\x12/\n" + + "\x05value\x18\x02 \x01(\v2\x19.lf.a2a.v1.SecuritySchemeR\x05value:\x028\x01B\x14\n" + + "\x12_documentation_urlB\v\n" + + "\t_icon_url\"O\n" + + "\rAgentProvider\x12\x15\n" + + "\x03url\x18\x01 \x01(\tB\x03\xe0A\x02R\x03url\x12'\n" + + "\forganization\x18\x02 \x01(\tB\x03\xe0A\x02R\forganization\"\x97\x02\n" + + "\x11AgentCapabilities\x12!\n" + + "\tstreaming\x18\x01 \x01(\bH\x00R\tstreaming\x88\x01\x01\x122\n" + + "\x12push_notifications\x18\x02 \x01(\bH\x01R\x11pushNotifications\x88\x01\x01\x129\n" + + "\n" + + "extensions\x18\x03 \x03(\v2\x19.lf.a2a.v1.AgentExtensionR\n" + + "extensions\x123\n" + + "\x13extended_agent_card\x18\x04 \x01(\bH\x02R\x11extendedAgentCard\x88\x01\x01B\f\n" + + "\n" + + "_streamingB\x15\n" + + "\x13_push_notificationsB\x16\n" + + "\x14_extended_agent_card\"\x91\x01\n" + + "\x0eAgentExtension\x12\x10\n" + + "\x03uri\x18\x01 \x01(\tR\x03uri\x12 \n" + + "\vdescription\x18\x02 \x01(\tR\vdescription\x12\x1a\n" + + "\brequired\x18\x03 \x01(\bR\brequired\x12/\n" + + "\x06params\x18\x04 \x01(\v2\x17.google.protobuf.StructR\x06params\"\xaf\x02\n" + + "\n" + + "AgentSkill\x12\x13\n" + + "\x02id\x18\x01 \x01(\tB\x03\xe0A\x02R\x02id\x12\x17\n" + + "\x04name\x18\x02 \x01(\tB\x03\xe0A\x02R\x04name\x12%\n" + + "\vdescription\x18\x03 \x01(\tB\x03\xe0A\x02R\vdescription\x12\x17\n" + + "\x04tags\x18\x04 \x03(\tB\x03\xe0A\x02R\x04tags\x12\x1a\n" + + "\bexamples\x18\x05 \x03(\tR\bexamples\x12\x1f\n" + + "\vinput_modes\x18\x06 \x03(\tR\n" + + "inputModes\x12!\n" + + "\foutput_modes\x18\a \x03(\tR\voutputModes\x12S\n" + + "\x15security_requirements\x18\b \x03(\v2\x1e.lf.a2a.v1.SecurityRequirementR\x14securityRequirements\"\x8b\x01\n" + + "\x12AgentCardSignature\x12!\n" + + "\tprotected\x18\x01 \x01(\tB\x03\xe0A\x02R\tprotected\x12!\n" + + "\tsignature\x18\x02 \x01(\tB\x03\xe0A\x02R\tsignature\x12/\n" + + "\x06header\x18\x03 \x01(\v2\x17.google.protobuf.StructR\x06header\"\xd1\x01\n" + + "\x1aTaskPushNotificationConfig\x12\x16\n" + + "\x06tenant\x18\x01 \x01(\tR\x06tenant\x12\x0e\n" + + "\x02id\x18\x02 \x01(\tR\x02id\x12\x17\n" + + "\atask_id\x18\x03 \x01(\tR\x06taskId\x12\x15\n" + + "\x03url\x18\x04 \x01(\tB\x03\xe0A\x02R\x03url\x12\x14\n" + + "\x05token\x18\x05 \x01(\tR\x05token\x12E\n" + + "\x0eauthentication\x18\x06 \x01(\v2\x1d.lf.a2a.v1.AuthenticationInfoR\x0eauthentication\" \n" + + "\n" + + "StringList\x12\x12\n" + + "\x04list\x18\x01 \x03(\tR\x04list\"\xaf\x01\n" + + "\x13SecurityRequirement\x12E\n" + + "\aschemes\x18\x01 \x03(\v2+.lf.a2a.v1.SecurityRequirement.SchemesEntryR\aschemes\x1aQ\n" + + "\fSchemesEntry\x12\x10\n" + + "\x03key\x18\x01 \x01(\tR\x03key\x12+\n" + + "\x05value\x18\x02 \x01(\v2\x15.lf.a2a.v1.StringListR\x05value:\x028\x01\"\xf5\x03\n" + + "\x0eSecurityScheme\x12X\n" + + "\x17api_key_security_scheme\x18\x01 \x01(\v2\x1f.lf.a2a.v1.APIKeySecuritySchemeH\x00R\x14apiKeySecurityScheme\x12^\n" + + "\x19http_auth_security_scheme\x18\x02 \x01(\v2!.lf.a2a.v1.HTTPAuthSecuritySchemeH\x00R\x16httpAuthSecurityScheme\x12W\n" + + "\x16oauth2_security_scheme\x18\x03 \x01(\v2\x1f.lf.a2a.v1.OAuth2SecuritySchemeH\x00R\x14oauth2SecurityScheme\x12n\n" + + "\x1fopen_id_connect_security_scheme\x18\x04 \x01(\v2&.lf.a2a.v1.OpenIdConnectSecuritySchemeH\x00R\x1bopenIdConnectSecurityScheme\x12V\n" + + "\x14mtls_security_scheme\x18\x05 \x01(\v2\".lf.a2a.v1.MutualTlsSecuritySchemeH\x00R\x12mtlsSecuritySchemeB\b\n" + + "\x06scheme\"r\n" + + "\x14APIKeySecurityScheme\x12 \n" + + "\vdescription\x18\x01 \x01(\tR\vdescription\x12\x1f\n" + + "\blocation\x18\x02 \x01(\tB\x03\xe0A\x02R\blocation\x12\x17\n" + + "\x04name\x18\x03 \x01(\tB\x03\xe0A\x02R\x04name\"|\n" + + "\x16HTTPAuthSecurityScheme\x12 \n" + + "\vdescription\x18\x01 \x01(\tR\vdescription\x12\x1b\n" + + "\x06scheme\x18\x02 \x01(\tB\x03\xe0A\x02R\x06scheme\x12#\n" + + "\rbearer_format\x18\x03 \x01(\tR\fbearerFormat\"\x9a\x01\n" + + "\x14OAuth2SecurityScheme\x12 \n" + + "\vdescription\x18\x01 \x01(\tR\vdescription\x120\n" + + "\x05flows\x18\x02 \x01(\v2\x15.lf.a2a.v1.OAuthFlowsB\x03\xe0A\x02R\x05flows\x12.\n" + + "\x13oauth2_metadata_url\x18\x03 \x01(\tR\x11oauth2MetadataUrl\"s\n" + + "\x1bOpenIdConnectSecurityScheme\x12 \n" + + "\vdescription\x18\x01 \x01(\tR\vdescription\x122\n" + + "\x13open_id_connect_url\x18\x02 \x01(\tB\x03\xe0A\x02R\x10openIdConnectUrl\";\n" + + "\x17MutualTlsSecurityScheme\x12 \n" + + "\vdescription\x18\x01 \x01(\tR\vdescription\"\x87\x03\n" + + "\n" + + "OAuthFlows\x12V\n" + + "\x12authorization_code\x18\x01 \x01(\v2%.lf.a2a.v1.AuthorizationCodeOAuthFlowH\x00R\x11authorizationCode\x12V\n" + + "\x12client_credentials\x18\x02 \x01(\v2%.lf.a2a.v1.ClientCredentialsOAuthFlowH\x00R\x11clientCredentials\x12>\n" + + "\bimplicit\x18\x03 \x01(\v2\x1c.lf.a2a.v1.ImplicitOAuthFlowB\x02\x18\x01H\x00R\bimplicit\x12>\n" + + "\bpassword\x18\x04 \x01(\v2\x1c.lf.a2a.v1.PasswordOAuthFlowB\x02\x18\x01H\x00R\bpassword\x12A\n" + + "\vdevice_code\x18\x05 \x01(\v2\x1e.lf.a2a.v1.DeviceCodeOAuthFlowH\x00R\n" + + "deviceCodeB\x06\n" + + "\x04flow\"\xc1\x02\n" + + "\x1aAuthorizationCodeOAuthFlow\x120\n" + + "\x11authorization_url\x18\x01 \x01(\tB\x03\xe0A\x02R\x10authorizationUrl\x12 \n" + + "\ttoken_url\x18\x02 \x01(\tB\x03\xe0A\x02R\btokenUrl\x12\x1f\n" + + "\vrefresh_url\x18\x03 \x01(\tR\n" + + "refreshUrl\x12N\n" + + "\x06scopes\x18\x04 \x03(\v21.lf.a2a.v1.AuthorizationCodeOAuthFlow.ScopesEntryB\x03\xe0A\x02R\x06scopes\x12#\n" + + "\rpkce_required\x18\x05 \x01(\bR\fpkceRequired\x1a9\n" + + "\vScopesEntry\x12\x10\n" + + "\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n" + + "\x05value\x18\x02 \x01(\tR\x05value:\x028\x01\"\xea\x01\n" + + "\x1aClientCredentialsOAuthFlow\x12 \n" + + "\ttoken_url\x18\x01 \x01(\tB\x03\xe0A\x02R\btokenUrl\x12\x1f\n" + + "\vrefresh_url\x18\x02 \x01(\tR\n" + + "refreshUrl\x12N\n" + + "\x06scopes\x18\x03 \x03(\v21.lf.a2a.v1.ClientCredentialsOAuthFlow.ScopesEntryB\x03\xe0A\x02R\x06scopes\x1a9\n" + + "\vScopesEntry\x12\x10\n" + + "\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n" + + "\x05value\x18\x02 \x01(\tR\x05value:\x028\x01\"\xde\x01\n" + + "\x11ImplicitOAuthFlow\x12+\n" + + "\x11authorization_url\x18\x01 \x01(\tR\x10authorizationUrl\x12\x1f\n" + + "\vrefresh_url\x18\x02 \x01(\tR\n" + + "refreshUrl\x12@\n" + + "\x06scopes\x18\x03 \x03(\v2(.lf.a2a.v1.ImplicitOAuthFlow.ScopesEntryR\x06scopes\x1a9\n" + + "\vScopesEntry\x12\x10\n" + + "\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n" + + "\x05value\x18\x02 \x01(\tR\x05value:\x028\x01\"\xce\x01\n" + + "\x11PasswordOAuthFlow\x12\x1b\n" + + "\ttoken_url\x18\x01 \x01(\tR\btokenUrl\x12\x1f\n" + + "\vrefresh_url\x18\x02 \x01(\tR\n" + + "refreshUrl\x12@\n" + + "\x06scopes\x18\x03 \x03(\v2(.lf.a2a.v1.PasswordOAuthFlow.ScopesEntryR\x06scopes\x1a9\n" + + "\vScopesEntry\x12\x10\n" + + "\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n" + + "\x05value\x18\x02 \x01(\tR\x05value:\x028\x01\"\x9b\x02\n" + + "\x13DeviceCodeOAuthFlow\x12=\n" + + "\x18device_authorization_url\x18\x01 \x01(\tB\x03\xe0A\x02R\x16deviceAuthorizationUrl\x12 \n" + + "\ttoken_url\x18\x02 \x01(\tB\x03\xe0A\x02R\btokenUrl\x12\x1f\n" + + "\vrefresh_url\x18\x03 \x01(\tR\n" + + "refreshUrl\x12G\n" + + "\x06scopes\x18\x04 \x03(\v2*.lf.a2a.v1.DeviceCodeOAuthFlow.ScopesEntryB\x03\xe0A\x02R\x06scopes\x1a9\n" + + "\vScopesEntry\x12\x10\n" + + "\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n" + + "\x05value\x18\x02 \x01(\tR\x05value:\x028\x01\"\xdf\x01\n" + + "\x12SendMessageRequest\x12\x16\n" + + "\x06tenant\x18\x01 \x01(\tR\x06tenant\x121\n" + + "\amessage\x18\x02 \x01(\v2\x12.lf.a2a.v1.MessageB\x03\xe0A\x02R\amessage\x12I\n" + + "\rconfiguration\x18\x03 \x01(\v2#.lf.a2a.v1.SendMessageConfigurationR\rconfiguration\x123\n" + + "\bmetadata\x18\x04 \x01(\v2\x17.google.protobuf.StructR\bmetadata\"|\n" + + "\x0eGetTaskRequest\x12\x16\n" + + "\x06tenant\x18\x01 \x01(\tR\x06tenant\x12\x13\n" + + "\x02id\x18\x02 \x01(\tB\x03\xe0A\x02R\x02id\x12*\n" + + "\x0ehistory_length\x18\x03 \x01(\x05H\x00R\rhistoryLength\x88\x01\x01B\x11\n" + + "\x0f_history_length\"\x9f\x03\n" + + "\x10ListTasksRequest\x12\x16\n" + + "\x06tenant\x18\x01 \x01(\tR\x06tenant\x12\x1d\n" + + "\n" + + "context_id\x18\x02 \x01(\tR\tcontextId\x12,\n" + + "\x06status\x18\x03 \x01(\x0e2\x14.lf.a2a.v1.TaskStateR\x06status\x12 \n" + + "\tpage_size\x18\x04 \x01(\x05H\x00R\bpageSize\x88\x01\x01\x12\x1d\n" + + "\n" + + "page_token\x18\x05 \x01(\tR\tpageToken\x12*\n" + + "\x0ehistory_length\x18\x06 \x01(\x05H\x01R\rhistoryLength\x88\x01\x01\x12P\n" + + "\x16status_timestamp_after\x18\a \x01(\v2\x1a.google.protobuf.TimestampR\x14statusTimestampAfter\x120\n" + + "\x11include_artifacts\x18\b \x01(\bH\x02R\x10includeArtifacts\x88\x01\x01B\f\n" + + "\n" + + "_page_sizeB\x11\n" + + "\x0f_history_lengthB\x14\n" + + "\x12_include_artifacts\"\xb2\x01\n" + + "\x11ListTasksResponse\x12*\n" + + "\x05tasks\x18\x01 \x03(\v2\x0f.lf.a2a.v1.TaskB\x03\xe0A\x02R\x05tasks\x12+\n" + + "\x0fnext_page_token\x18\x02 \x01(\tB\x03\xe0A\x02R\rnextPageToken\x12 \n" + + "\tpage_size\x18\x03 \x01(\x05B\x03\xe0A\x02R\bpageSize\x12\"\n" + + "\n" + + "total_size\x18\x04 \x01(\x05B\x03\xe0A\x02R\ttotalSize\"u\n" + + "\x11CancelTaskRequest\x12\x16\n" + + "\x06tenant\x18\x01 \x01(\tR\x06tenant\x12\x13\n" + + "\x02id\x18\x02 \x01(\tB\x03\xe0A\x02R\x02id\x123\n" + + "\bmetadata\x18\x03 \x01(\v2\x17.google.protobuf.StructR\bmetadata\"q\n" + + "$GetTaskPushNotificationConfigRequest\x12\x16\n" + + "\x06tenant\x18\x01 \x01(\tR\x06tenant\x12\x1c\n" + + "\atask_id\x18\x02 \x01(\tB\x03\xe0A\x02R\x06taskId\x12\x13\n" + + "\x02id\x18\x03 \x01(\tB\x03\xe0A\x02R\x02id\"t\n" + + "'DeleteTaskPushNotificationConfigRequest\x12\x16\n" + + "\x06tenant\x18\x01 \x01(\tR\x06tenant\x12\x1c\n" + + "\atask_id\x18\x02 \x01(\tB\x03\xe0A\x02R\x06taskId\x12\x13\n" + + "\x02id\x18\x03 \x01(\tB\x03\xe0A\x02R\x02id\"E\n" + + "\x16SubscribeToTaskRequest\x12\x16\n" + + "\x06tenant\x18\x01 \x01(\tR\x06tenant\x12\x13\n" + + "\x02id\x18\x02 \x01(\tB\x03\xe0A\x02R\x02id\"\x9a\x01\n" + + "&ListTaskPushNotificationConfigsRequest\x12\x16\n" + + "\x06tenant\x18\x04 \x01(\tR\x06tenant\x12\x1c\n" + + "\atask_id\x18\x01 \x01(\tB\x03\xe0A\x02R\x06taskId\x12\x1b\n" + + "\tpage_size\x18\x02 \x01(\x05R\bpageSize\x12\x1d\n" + + "\n" + + "page_token\x18\x03 \x01(\tR\tpageToken\"5\n" + + "\x1bGetExtendedAgentCardRequest\x12\x16\n" + + "\x06tenant\x18\x01 \x01(\tR\x06tenant\"w\n" + + "\x13SendMessageResponse\x12%\n" + + "\x04task\x18\x01 \x01(\v2\x0f.lf.a2a.v1.TaskH\x00R\x04task\x12.\n" + + "\amessage\x18\x02 \x01(\v2\x12.lf.a2a.v1.MessageH\x00R\amessageB\t\n" + + "\apayload\"\x8a\x02\n" + + "\x0eStreamResponse\x12%\n" + + "\x04task\x18\x01 \x01(\v2\x0f.lf.a2a.v1.TaskH\x00R\x04task\x12.\n" + + "\amessage\x18\x02 \x01(\v2\x12.lf.a2a.v1.MessageH\x00R\amessage\x12G\n" + + "\rstatus_update\x18\x03 \x01(\v2 .lf.a2a.v1.TaskStatusUpdateEventH\x00R\fstatusUpdate\x12M\n" + + "\x0fartifact_update\x18\x04 \x01(\v2\".lf.a2a.v1.TaskArtifactUpdateEventH\x00R\x0eartifactUpdateB\t\n" + + "\apayload\"\x92\x01\n" + + "'ListTaskPushNotificationConfigsResponse\x12?\n" + + "\aconfigs\x18\x01 \x03(\v2%.lf.a2a.v1.TaskPushNotificationConfigR\aconfigs\x12&\n" + + "\x0fnext_page_token\x18\x02 \x01(\tR\rnextPageToken*\xf9\x01\n" + + "\tTaskState\x12\x1a\n" + + "\x16TASK_STATE_UNSPECIFIED\x10\x00\x12\x18\n" + + "\x14TASK_STATE_SUBMITTED\x10\x01\x12\x16\n" + + "\x12TASK_STATE_WORKING\x10\x02\x12\x18\n" + + "\x14TASK_STATE_COMPLETED\x10\x03\x12\x15\n" + + "\x11TASK_STATE_FAILED\x10\x04\x12\x17\n" + + "\x13TASK_STATE_CANCELED\x10\x05\x12\x1d\n" + + "\x19TASK_STATE_INPUT_REQUIRED\x10\x06\x12\x17\n" + + "\x13TASK_STATE_REJECTED\x10\a\x12\x1c\n" + + "\x18TASK_STATE_AUTH_REQUIRED\x10\b*;\n" + + "\x04Role\x12\x14\n" + + "\x10ROLE_UNSPECIFIED\x10\x00\x12\r\n" + + "\tROLE_USER\x10\x01\x12\x0e\n" + + "\n" + + "ROLE_AGENT\x10\x022\x97\x0f\n" + + "\n" + + "A2AService\x12\x83\x01\n" + + "\vSendMessage\x12\x1d.lf.a2a.v1.SendMessageRequest\x1a\x1e.lf.a2a.v1.SendMessageResponse\"5\x82\xd3\xe4\x93\x02/:\x01*Z\x1b:\x01*\"\x16/{tenant}/message:send\"\r/message:send\x12\x8d\x01\n" + + "\x14SendStreamingMessage\x12\x1d.lf.a2a.v1.SendMessageRequest\x1a\x19.lf.a2a.v1.StreamResponse\"9\x82\xd3\xe4\x93\x023:\x01*Z\x1d:\x01*\"\x18/{tenant}/message:stream\"\x0f/message:stream0\x01\x12k\n" + + "\aGetTask\x12\x19.lf.a2a.v1.GetTaskRequest\x1a\x0f.lf.a2a.v1.Task\"4\xdaA\x02id\x82\xd3\xe4\x93\x02)Z\x18\x12\x16/{tenant}/tasks/{id=*}\x12\r/tasks/{id=*}\x12i\n" + + "\tListTasks\x12\x1b.lf.a2a.v1.ListTasksRequest\x1a\x1c.lf.a2a.v1.ListTasksResponse\"!\x82\xd3\xe4\x93\x02\x1bZ\x11\x12\x0f/{tenant}/tasks\x12\x06/tasks\x12\x80\x01\n" + + "\n" + + "CancelTask\x12\x1c.lf.a2a.v1.CancelTaskRequest\x1a\x0f.lf.a2a.v1.Task\"C\x82\xd3\xe4\x93\x02=:\x01*Z\":\x01*\"\x1d/{tenant}/tasks/{id=*}:cancel\"\x14/tasks/{id=*}:cancel\x12\x96\x01\n" + + "\x0fSubscribeToTask\x12!.lf.a2a.v1.SubscribeToTaskRequest\x1a\x19.lf.a2a.v1.StreamResponse\"C\x82\xd3\xe4\x93\x02=Z\"\x12 /{tenant}/tasks/{id=*}:subscribe\x12\x17/tasks/{id=*}:subscribe0\x01\x12\xf3\x01\n" + + " CreateTaskPushNotificationConfig\x12%.lf.a2a.v1.TaskPushNotificationConfig\x1a%.lf.a2a.v1.TaskPushNotificationConfig\"\x80\x01\xdaA\x0etask_id,config\x82\xd3\xe4\x93\x02i:\x01*Z8:\x01*\"3/{tenant}/tasks/{task_id=*}/pushNotificationConfigs\"*/tasks/{task_id=*}/pushNotificationConfigs\x12\xfe\x01\n" + + "\x1dGetTaskPushNotificationConfig\x12/.lf.a2a.v1.GetTaskPushNotificationConfigRequest\x1a%.lf.a2a.v1.TaskPushNotificationConfig\"\x84\x01\xdaA\n" + + "task_id,id\x82\xd3\xe4\x93\x02qZ<\x12:/{tenant}/tasks/{task_id=*}/pushNotificationConfigs/{id=*}\x121/tasks/{task_id=*}/pushNotificationConfigs/{id=*}\x12\xfd\x01\n" + + "\x1fListTaskPushNotificationConfigs\x121.lf.a2a.v1.ListTaskPushNotificationConfigsRequest\x1a2.lf.a2a.v1.ListTaskPushNotificationConfigsResponse\"s\xdaA\atask_id\x82\xd3\xe4\x93\x02cZ5\x123/{tenant}/tasks/{task_id=*}/pushNotificationConfigs\x12*/tasks/{task_id=*}/pushNotificationConfigs\x12\x8f\x01\n" + + "\x14GetExtendedAgentCard\x12&.lf.a2a.v1.GetExtendedAgentCardRequest\x1a\x14.lf.a2a.v1.AgentCard\"9\x82\xd3\xe4\x93\x023Z\x1d\x12\x1b/{tenant}/extendedAgentCard\x12\x12/extendedAgentCard\x12\xf5\x01\n" + + " DeleteTaskPushNotificationConfig\x122.lf.a2a.v1.DeleteTaskPushNotificationConfigRequest\x1a\x16.google.protobuf.Empty\"\x84\x01\xdaA\n" + + "task_id,id\x82\xd3\xe4\x93\x02qZ<*:/{tenant}/tasks/{task_id=*}/pushNotificationConfigs/{id=*}*1/tasks/{task_id=*}/pushNotificationConfigs/{id=*}Bv\n" + + "\x14com.google.lf.a2a.v1B\x03A2AP\x01ZKgithub.com/GoogleCloudPlatform/scion/extras/scion-a2a-bridge/internal/a2apb\xaa\x02\tLf.A2a.V1b\x06proto3" + +var ( + file_a2a_proto_rawDescOnce sync.Once + file_a2a_proto_rawDescData []byte +) + +func file_a2a_proto_rawDescGZIP() []byte { + file_a2a_proto_rawDescOnce.Do(func() { + file_a2a_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_a2a_proto_rawDesc), len(file_a2a_proto_rawDesc))) + }) + return file_a2a_proto_rawDescData +} + +var file_a2a_proto_enumTypes = make([]protoimpl.EnumInfo, 2) +var file_a2a_proto_msgTypes = make([]protoimpl.MessageInfo, 51) +var file_a2a_proto_goTypes = []any{ + (TaskState)(0), // 0: lf.a2a.v1.TaskState + (Role)(0), // 1: lf.a2a.v1.Role + (*SendMessageConfiguration)(nil), // 2: lf.a2a.v1.SendMessageConfiguration + (*Task)(nil), // 3: lf.a2a.v1.Task + (*TaskStatus)(nil), // 4: lf.a2a.v1.TaskStatus + (*Part)(nil), // 5: lf.a2a.v1.Part + (*Message)(nil), // 6: lf.a2a.v1.Message + (*Artifact)(nil), // 7: lf.a2a.v1.Artifact + (*TaskStatusUpdateEvent)(nil), // 8: lf.a2a.v1.TaskStatusUpdateEvent + (*TaskArtifactUpdateEvent)(nil), // 9: lf.a2a.v1.TaskArtifactUpdateEvent + (*AuthenticationInfo)(nil), // 10: lf.a2a.v1.AuthenticationInfo + (*AgentInterface)(nil), // 11: lf.a2a.v1.AgentInterface + (*AgentCard)(nil), // 12: lf.a2a.v1.AgentCard + (*AgentProvider)(nil), // 13: lf.a2a.v1.AgentProvider + (*AgentCapabilities)(nil), // 14: lf.a2a.v1.AgentCapabilities + (*AgentExtension)(nil), // 15: lf.a2a.v1.AgentExtension + (*AgentSkill)(nil), // 16: lf.a2a.v1.AgentSkill + (*AgentCardSignature)(nil), // 17: lf.a2a.v1.AgentCardSignature + (*TaskPushNotificationConfig)(nil), // 18: lf.a2a.v1.TaskPushNotificationConfig + (*StringList)(nil), // 19: lf.a2a.v1.StringList + (*SecurityRequirement)(nil), // 20: lf.a2a.v1.SecurityRequirement + (*SecurityScheme)(nil), // 21: lf.a2a.v1.SecurityScheme + (*APIKeySecurityScheme)(nil), // 22: lf.a2a.v1.APIKeySecurityScheme + (*HTTPAuthSecurityScheme)(nil), // 23: lf.a2a.v1.HTTPAuthSecurityScheme + (*OAuth2SecurityScheme)(nil), // 24: lf.a2a.v1.OAuth2SecurityScheme + (*OpenIdConnectSecurityScheme)(nil), // 25: lf.a2a.v1.OpenIdConnectSecurityScheme + (*MutualTlsSecurityScheme)(nil), // 26: lf.a2a.v1.MutualTlsSecurityScheme + (*OAuthFlows)(nil), // 27: lf.a2a.v1.OAuthFlows + (*AuthorizationCodeOAuthFlow)(nil), // 28: lf.a2a.v1.AuthorizationCodeOAuthFlow + (*ClientCredentialsOAuthFlow)(nil), // 29: lf.a2a.v1.ClientCredentialsOAuthFlow + (*ImplicitOAuthFlow)(nil), // 30: lf.a2a.v1.ImplicitOAuthFlow + (*PasswordOAuthFlow)(nil), // 31: lf.a2a.v1.PasswordOAuthFlow + (*DeviceCodeOAuthFlow)(nil), // 32: lf.a2a.v1.DeviceCodeOAuthFlow + (*SendMessageRequest)(nil), // 33: lf.a2a.v1.SendMessageRequest + (*GetTaskRequest)(nil), // 34: lf.a2a.v1.GetTaskRequest + (*ListTasksRequest)(nil), // 35: lf.a2a.v1.ListTasksRequest + (*ListTasksResponse)(nil), // 36: lf.a2a.v1.ListTasksResponse + (*CancelTaskRequest)(nil), // 37: lf.a2a.v1.CancelTaskRequest + (*GetTaskPushNotificationConfigRequest)(nil), // 38: lf.a2a.v1.GetTaskPushNotificationConfigRequest + (*DeleteTaskPushNotificationConfigRequest)(nil), // 39: lf.a2a.v1.DeleteTaskPushNotificationConfigRequest + (*SubscribeToTaskRequest)(nil), // 40: lf.a2a.v1.SubscribeToTaskRequest + (*ListTaskPushNotificationConfigsRequest)(nil), // 41: lf.a2a.v1.ListTaskPushNotificationConfigsRequest + (*GetExtendedAgentCardRequest)(nil), // 42: lf.a2a.v1.GetExtendedAgentCardRequest + (*SendMessageResponse)(nil), // 43: lf.a2a.v1.SendMessageResponse + (*StreamResponse)(nil), // 44: lf.a2a.v1.StreamResponse + (*ListTaskPushNotificationConfigsResponse)(nil), // 45: lf.a2a.v1.ListTaskPushNotificationConfigsResponse + nil, // 46: lf.a2a.v1.AgentCard.SecuritySchemesEntry + nil, // 47: lf.a2a.v1.SecurityRequirement.SchemesEntry + nil, // 48: lf.a2a.v1.AuthorizationCodeOAuthFlow.ScopesEntry + nil, // 49: lf.a2a.v1.ClientCredentialsOAuthFlow.ScopesEntry + nil, // 50: lf.a2a.v1.ImplicitOAuthFlow.ScopesEntry + nil, // 51: lf.a2a.v1.PasswordOAuthFlow.ScopesEntry + nil, // 52: lf.a2a.v1.DeviceCodeOAuthFlow.ScopesEntry + (*structpb.Struct)(nil), // 53: google.protobuf.Struct + (*timestamppb.Timestamp)(nil), // 54: google.protobuf.Timestamp + (*structpb.Value)(nil), // 55: google.protobuf.Value + (*emptypb.Empty)(nil), // 56: google.protobuf.Empty +} +var file_a2a_proto_depIdxs = []int32{ + 18, // 0: lf.a2a.v1.SendMessageConfiguration.task_push_notification_config:type_name -> lf.a2a.v1.TaskPushNotificationConfig + 4, // 1: lf.a2a.v1.Task.status:type_name -> lf.a2a.v1.TaskStatus + 7, // 2: lf.a2a.v1.Task.artifacts:type_name -> lf.a2a.v1.Artifact + 6, // 3: lf.a2a.v1.Task.history:type_name -> lf.a2a.v1.Message + 53, // 4: lf.a2a.v1.Task.metadata:type_name -> google.protobuf.Struct + 0, // 5: lf.a2a.v1.TaskStatus.state:type_name -> lf.a2a.v1.TaskState + 6, // 6: lf.a2a.v1.TaskStatus.message:type_name -> lf.a2a.v1.Message + 54, // 7: lf.a2a.v1.TaskStatus.timestamp:type_name -> google.protobuf.Timestamp + 55, // 8: lf.a2a.v1.Part.data:type_name -> google.protobuf.Value + 53, // 9: lf.a2a.v1.Part.metadata:type_name -> google.protobuf.Struct + 1, // 10: lf.a2a.v1.Message.role:type_name -> lf.a2a.v1.Role + 5, // 11: lf.a2a.v1.Message.parts:type_name -> lf.a2a.v1.Part + 53, // 12: lf.a2a.v1.Message.metadata:type_name -> google.protobuf.Struct + 5, // 13: lf.a2a.v1.Artifact.parts:type_name -> lf.a2a.v1.Part + 53, // 14: lf.a2a.v1.Artifact.metadata:type_name -> google.protobuf.Struct + 4, // 15: lf.a2a.v1.TaskStatusUpdateEvent.status:type_name -> lf.a2a.v1.TaskStatus + 53, // 16: lf.a2a.v1.TaskStatusUpdateEvent.metadata:type_name -> google.protobuf.Struct + 7, // 17: lf.a2a.v1.TaskArtifactUpdateEvent.artifact:type_name -> lf.a2a.v1.Artifact + 53, // 18: lf.a2a.v1.TaskArtifactUpdateEvent.metadata:type_name -> google.protobuf.Struct + 11, // 19: lf.a2a.v1.AgentCard.supported_interfaces:type_name -> lf.a2a.v1.AgentInterface + 13, // 20: lf.a2a.v1.AgentCard.provider:type_name -> lf.a2a.v1.AgentProvider + 14, // 21: lf.a2a.v1.AgentCard.capabilities:type_name -> lf.a2a.v1.AgentCapabilities + 46, // 22: lf.a2a.v1.AgentCard.security_schemes:type_name -> lf.a2a.v1.AgentCard.SecuritySchemesEntry + 20, // 23: lf.a2a.v1.AgentCard.security_requirements:type_name -> lf.a2a.v1.SecurityRequirement + 16, // 24: lf.a2a.v1.AgentCard.skills:type_name -> lf.a2a.v1.AgentSkill + 17, // 25: lf.a2a.v1.AgentCard.signatures:type_name -> lf.a2a.v1.AgentCardSignature + 15, // 26: lf.a2a.v1.AgentCapabilities.extensions:type_name -> lf.a2a.v1.AgentExtension + 53, // 27: lf.a2a.v1.AgentExtension.params:type_name -> google.protobuf.Struct + 20, // 28: lf.a2a.v1.AgentSkill.security_requirements:type_name -> lf.a2a.v1.SecurityRequirement + 53, // 29: lf.a2a.v1.AgentCardSignature.header:type_name -> google.protobuf.Struct + 10, // 30: lf.a2a.v1.TaskPushNotificationConfig.authentication:type_name -> lf.a2a.v1.AuthenticationInfo + 47, // 31: lf.a2a.v1.SecurityRequirement.schemes:type_name -> lf.a2a.v1.SecurityRequirement.SchemesEntry + 22, // 32: lf.a2a.v1.SecurityScheme.api_key_security_scheme:type_name -> lf.a2a.v1.APIKeySecurityScheme + 23, // 33: lf.a2a.v1.SecurityScheme.http_auth_security_scheme:type_name -> lf.a2a.v1.HTTPAuthSecurityScheme + 24, // 34: lf.a2a.v1.SecurityScheme.oauth2_security_scheme:type_name -> lf.a2a.v1.OAuth2SecurityScheme + 25, // 35: lf.a2a.v1.SecurityScheme.open_id_connect_security_scheme:type_name -> lf.a2a.v1.OpenIdConnectSecurityScheme + 26, // 36: lf.a2a.v1.SecurityScheme.mtls_security_scheme:type_name -> lf.a2a.v1.MutualTlsSecurityScheme + 27, // 37: lf.a2a.v1.OAuth2SecurityScheme.flows:type_name -> lf.a2a.v1.OAuthFlows + 28, // 38: lf.a2a.v1.OAuthFlows.authorization_code:type_name -> lf.a2a.v1.AuthorizationCodeOAuthFlow + 29, // 39: lf.a2a.v1.OAuthFlows.client_credentials:type_name -> lf.a2a.v1.ClientCredentialsOAuthFlow + 30, // 40: lf.a2a.v1.OAuthFlows.implicit:type_name -> lf.a2a.v1.ImplicitOAuthFlow + 31, // 41: lf.a2a.v1.OAuthFlows.password:type_name -> lf.a2a.v1.PasswordOAuthFlow + 32, // 42: lf.a2a.v1.OAuthFlows.device_code:type_name -> lf.a2a.v1.DeviceCodeOAuthFlow + 48, // 43: lf.a2a.v1.AuthorizationCodeOAuthFlow.scopes:type_name -> lf.a2a.v1.AuthorizationCodeOAuthFlow.ScopesEntry + 49, // 44: lf.a2a.v1.ClientCredentialsOAuthFlow.scopes:type_name -> lf.a2a.v1.ClientCredentialsOAuthFlow.ScopesEntry + 50, // 45: lf.a2a.v1.ImplicitOAuthFlow.scopes:type_name -> lf.a2a.v1.ImplicitOAuthFlow.ScopesEntry + 51, // 46: lf.a2a.v1.PasswordOAuthFlow.scopes:type_name -> lf.a2a.v1.PasswordOAuthFlow.ScopesEntry + 52, // 47: lf.a2a.v1.DeviceCodeOAuthFlow.scopes:type_name -> lf.a2a.v1.DeviceCodeOAuthFlow.ScopesEntry + 6, // 48: lf.a2a.v1.SendMessageRequest.message:type_name -> lf.a2a.v1.Message + 2, // 49: lf.a2a.v1.SendMessageRequest.configuration:type_name -> lf.a2a.v1.SendMessageConfiguration + 53, // 50: lf.a2a.v1.SendMessageRequest.metadata:type_name -> google.protobuf.Struct + 0, // 51: lf.a2a.v1.ListTasksRequest.status:type_name -> lf.a2a.v1.TaskState + 54, // 52: lf.a2a.v1.ListTasksRequest.status_timestamp_after:type_name -> google.protobuf.Timestamp + 3, // 53: lf.a2a.v1.ListTasksResponse.tasks:type_name -> lf.a2a.v1.Task + 53, // 54: lf.a2a.v1.CancelTaskRequest.metadata:type_name -> google.protobuf.Struct + 3, // 55: lf.a2a.v1.SendMessageResponse.task:type_name -> lf.a2a.v1.Task + 6, // 56: lf.a2a.v1.SendMessageResponse.message:type_name -> lf.a2a.v1.Message + 3, // 57: lf.a2a.v1.StreamResponse.task:type_name -> lf.a2a.v1.Task + 6, // 58: lf.a2a.v1.StreamResponse.message:type_name -> lf.a2a.v1.Message + 8, // 59: lf.a2a.v1.StreamResponse.status_update:type_name -> lf.a2a.v1.TaskStatusUpdateEvent + 9, // 60: lf.a2a.v1.StreamResponse.artifact_update:type_name -> lf.a2a.v1.TaskArtifactUpdateEvent + 18, // 61: lf.a2a.v1.ListTaskPushNotificationConfigsResponse.configs:type_name -> lf.a2a.v1.TaskPushNotificationConfig + 21, // 62: lf.a2a.v1.AgentCard.SecuritySchemesEntry.value:type_name -> lf.a2a.v1.SecurityScheme + 19, // 63: lf.a2a.v1.SecurityRequirement.SchemesEntry.value:type_name -> lf.a2a.v1.StringList + 33, // 64: lf.a2a.v1.A2AService.SendMessage:input_type -> lf.a2a.v1.SendMessageRequest + 33, // 65: lf.a2a.v1.A2AService.SendStreamingMessage:input_type -> lf.a2a.v1.SendMessageRequest + 34, // 66: lf.a2a.v1.A2AService.GetTask:input_type -> lf.a2a.v1.GetTaskRequest + 35, // 67: lf.a2a.v1.A2AService.ListTasks:input_type -> lf.a2a.v1.ListTasksRequest + 37, // 68: lf.a2a.v1.A2AService.CancelTask:input_type -> lf.a2a.v1.CancelTaskRequest + 40, // 69: lf.a2a.v1.A2AService.SubscribeToTask:input_type -> lf.a2a.v1.SubscribeToTaskRequest + 18, // 70: lf.a2a.v1.A2AService.CreateTaskPushNotificationConfig:input_type -> lf.a2a.v1.TaskPushNotificationConfig + 38, // 71: lf.a2a.v1.A2AService.GetTaskPushNotificationConfig:input_type -> lf.a2a.v1.GetTaskPushNotificationConfigRequest + 41, // 72: lf.a2a.v1.A2AService.ListTaskPushNotificationConfigs:input_type -> lf.a2a.v1.ListTaskPushNotificationConfigsRequest + 42, // 73: lf.a2a.v1.A2AService.GetExtendedAgentCard:input_type -> lf.a2a.v1.GetExtendedAgentCardRequest + 39, // 74: lf.a2a.v1.A2AService.DeleteTaskPushNotificationConfig:input_type -> lf.a2a.v1.DeleteTaskPushNotificationConfigRequest + 43, // 75: lf.a2a.v1.A2AService.SendMessage:output_type -> lf.a2a.v1.SendMessageResponse + 44, // 76: lf.a2a.v1.A2AService.SendStreamingMessage:output_type -> lf.a2a.v1.StreamResponse + 3, // 77: lf.a2a.v1.A2AService.GetTask:output_type -> lf.a2a.v1.Task + 36, // 78: lf.a2a.v1.A2AService.ListTasks:output_type -> lf.a2a.v1.ListTasksResponse + 3, // 79: lf.a2a.v1.A2AService.CancelTask:output_type -> lf.a2a.v1.Task + 44, // 80: lf.a2a.v1.A2AService.SubscribeToTask:output_type -> lf.a2a.v1.StreamResponse + 18, // 81: lf.a2a.v1.A2AService.CreateTaskPushNotificationConfig:output_type -> lf.a2a.v1.TaskPushNotificationConfig + 18, // 82: lf.a2a.v1.A2AService.GetTaskPushNotificationConfig:output_type -> lf.a2a.v1.TaskPushNotificationConfig + 45, // 83: lf.a2a.v1.A2AService.ListTaskPushNotificationConfigs:output_type -> lf.a2a.v1.ListTaskPushNotificationConfigsResponse + 12, // 84: lf.a2a.v1.A2AService.GetExtendedAgentCard:output_type -> lf.a2a.v1.AgentCard + 56, // 85: lf.a2a.v1.A2AService.DeleteTaskPushNotificationConfig:output_type -> google.protobuf.Empty + 75, // [75:86] is the sub-list for method output_type + 64, // [64:75] is the sub-list for method input_type + 64, // [64:64] is the sub-list for extension type_name + 64, // [64:64] is the sub-list for extension extendee + 0, // [0:64] is the sub-list for field type_name +} + +func init() { file_a2a_proto_init() } +func file_a2a_proto_init() { + if File_a2a_proto != nil { + return + } + file_a2a_proto_msgTypes[0].OneofWrappers = []any{} + file_a2a_proto_msgTypes[3].OneofWrappers = []any{ + (*Part_Text)(nil), + (*Part_Raw)(nil), + (*Part_Url)(nil), + (*Part_Data)(nil), + } + file_a2a_proto_msgTypes[10].OneofWrappers = []any{} + file_a2a_proto_msgTypes[12].OneofWrappers = []any{} + file_a2a_proto_msgTypes[19].OneofWrappers = []any{ + (*SecurityScheme_ApiKeySecurityScheme)(nil), + (*SecurityScheme_HttpAuthSecurityScheme)(nil), + (*SecurityScheme_Oauth2SecurityScheme)(nil), + (*SecurityScheme_OpenIdConnectSecurityScheme)(nil), + (*SecurityScheme_MtlsSecurityScheme)(nil), + } + file_a2a_proto_msgTypes[25].OneofWrappers = []any{ + (*OAuthFlows_AuthorizationCode)(nil), + (*OAuthFlows_ClientCredentials)(nil), + (*OAuthFlows_Implicit)(nil), + (*OAuthFlows_Password)(nil), + (*OAuthFlows_DeviceCode)(nil), + } + file_a2a_proto_msgTypes[32].OneofWrappers = []any{} + file_a2a_proto_msgTypes[33].OneofWrappers = []any{} + file_a2a_proto_msgTypes[41].OneofWrappers = []any{ + (*SendMessageResponse_Task)(nil), + (*SendMessageResponse_Message)(nil), + } + file_a2a_proto_msgTypes[42].OneofWrappers = []any{ + (*StreamResponse_Task)(nil), + (*StreamResponse_Message)(nil), + (*StreamResponse_StatusUpdate)(nil), + (*StreamResponse_ArtifactUpdate)(nil), + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: unsafe.Slice(unsafe.StringData(file_a2a_proto_rawDesc), len(file_a2a_proto_rawDesc)), + NumEnums: 2, + NumMessages: 51, + NumExtensions: 0, + NumServices: 1, + }, + GoTypes: file_a2a_proto_goTypes, + DependencyIndexes: file_a2a_proto_depIdxs, + EnumInfos: file_a2a_proto_enumTypes, + MessageInfos: file_a2a_proto_msgTypes, + }.Build() + File_a2a_proto = out.File + file_a2a_proto_goTypes = nil + file_a2a_proto_depIdxs = nil +} diff --git a/extras/scion-a2a-bridge/internal/a2apb/a2a_grpc.pb.go b/extras/scion-a2a-bridge/internal/a2apb/a2a_grpc.pb.go new file mode 100644 index 000000000..9ec505776 --- /dev/null +++ b/extras/scion-a2a-bridge/internal/a2apb/a2a_grpc.pb.go @@ -0,0 +1,553 @@ +// Older protoc compilers don't understand edition yet. + +// Code generated by protoc-gen-go-grpc. DO NOT EDIT. +// versions: +// - protoc-gen-go-grpc v1.6.2 +// - protoc v3.21.12 +// source: a2a.proto + +package a2apb + +import ( + context "context" + grpc "google.golang.org/grpc" + codes "google.golang.org/grpc/codes" + status "google.golang.org/grpc/status" + emptypb "google.golang.org/protobuf/types/known/emptypb" +) + +// This is a compile-time assertion to ensure that this generated file +// is compatible with the grpc package it is being compiled against. +// Requires gRPC-Go v1.64.0 or later. +const _ = grpc.SupportPackageIsVersion9 + +const ( + A2AService_SendMessage_FullMethodName = "/lf.a2a.v1.A2AService/SendMessage" + A2AService_SendStreamingMessage_FullMethodName = "/lf.a2a.v1.A2AService/SendStreamingMessage" + A2AService_GetTask_FullMethodName = "/lf.a2a.v1.A2AService/GetTask" + A2AService_ListTasks_FullMethodName = "/lf.a2a.v1.A2AService/ListTasks" + A2AService_CancelTask_FullMethodName = "/lf.a2a.v1.A2AService/CancelTask" + A2AService_SubscribeToTask_FullMethodName = "/lf.a2a.v1.A2AService/SubscribeToTask" + A2AService_CreateTaskPushNotificationConfig_FullMethodName = "/lf.a2a.v1.A2AService/CreateTaskPushNotificationConfig" + A2AService_GetTaskPushNotificationConfig_FullMethodName = "/lf.a2a.v1.A2AService/GetTaskPushNotificationConfig" + A2AService_ListTaskPushNotificationConfigs_FullMethodName = "/lf.a2a.v1.A2AService/ListTaskPushNotificationConfigs" + A2AService_GetExtendedAgentCard_FullMethodName = "/lf.a2a.v1.A2AService/GetExtendedAgentCard" + A2AService_DeleteTaskPushNotificationConfig_FullMethodName = "/lf.a2a.v1.A2AService/DeleteTaskPushNotificationConfig" +) + +// A2AServiceClient is the client API for A2AService service. +// +// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream. +// +// Provides operations for interacting with agents using the A2A protocol. +type A2AServiceClient interface { + // Sends a message to an agent. + SendMessage(ctx context.Context, in *SendMessageRequest, opts ...grpc.CallOption) (*SendMessageResponse, error) + // Sends a streaming message to an agent, allowing for real-time interaction and status updates. + // Streaming version of `SendMessage` + SendStreamingMessage(ctx context.Context, in *SendMessageRequest, opts ...grpc.CallOption) (grpc.ServerStreamingClient[StreamResponse], error) + // Gets the latest state of a task. + GetTask(ctx context.Context, in *GetTaskRequest, opts ...grpc.CallOption) (*Task, error) + // Lists tasks that match the specified filter. + ListTasks(ctx context.Context, in *ListTasksRequest, opts ...grpc.CallOption) (*ListTasksResponse, error) + // Cancels a task in progress. + CancelTask(ctx context.Context, in *CancelTaskRequest, opts ...grpc.CallOption) (*Task, error) + // Subscribes to task updates for tasks not in a terminal state. + // Returns `UnsupportedOperationError` if the task is already in a terminal state (completed, failed, canceled, rejected). + SubscribeToTask(ctx context.Context, in *SubscribeToTaskRequest, opts ...grpc.CallOption) (grpc.ServerStreamingClient[StreamResponse], error) + // (-- api-linter: client-libraries::4232::required-fields=disabled + // + // api-linter: core::0133::method-signature=disabled + // api-linter: core::0133::request-message-name=disabled + // aip.dev/not-precedent: method_signature preserved for backwards compatibility --) + // + // Creates a push notification config for a task. + CreateTaskPushNotificationConfig(ctx context.Context, in *TaskPushNotificationConfig, opts ...grpc.CallOption) (*TaskPushNotificationConfig, error) + // Gets a push notification config for a task. + GetTaskPushNotificationConfig(ctx context.Context, in *GetTaskPushNotificationConfigRequest, opts ...grpc.CallOption) (*TaskPushNotificationConfig, error) + // Get a list of push notifications configured for a task. + ListTaskPushNotificationConfigs(ctx context.Context, in *ListTaskPushNotificationConfigsRequest, opts ...grpc.CallOption) (*ListTaskPushNotificationConfigsResponse, error) + // Gets the extended agent card for the authenticated agent. + GetExtendedAgentCard(ctx context.Context, in *GetExtendedAgentCardRequest, opts ...grpc.CallOption) (*AgentCard, error) + // Deletes a push notification config for a task. + DeleteTaskPushNotificationConfig(ctx context.Context, in *DeleteTaskPushNotificationConfigRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) +} + +type a2AServiceClient struct { + cc grpc.ClientConnInterface +} + +func NewA2AServiceClient(cc grpc.ClientConnInterface) A2AServiceClient { + return &a2AServiceClient{cc} +} + +func (c *a2AServiceClient) SendMessage(ctx context.Context, in *SendMessageRequest, opts ...grpc.CallOption) (*SendMessageResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(SendMessageResponse) + err := c.cc.Invoke(ctx, A2AService_SendMessage_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *a2AServiceClient) SendStreamingMessage(ctx context.Context, in *SendMessageRequest, opts ...grpc.CallOption) (grpc.ServerStreamingClient[StreamResponse], error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + stream, err := c.cc.NewStream(ctx, &A2AService_ServiceDesc.Streams[0], A2AService_SendStreamingMessage_FullMethodName, cOpts...) + if err != nil { + return nil, err + } + x := &grpc.GenericClientStream[SendMessageRequest, StreamResponse]{ClientStream: stream} + if err := x.ClientStream.SendMsg(in); err != nil { + return nil, err + } + if err := x.ClientStream.CloseSend(); err != nil { + return nil, err + } + return x, nil +} + +// This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name. +type A2AService_SendStreamingMessageClient = grpc.ServerStreamingClient[StreamResponse] + +func (c *a2AServiceClient) GetTask(ctx context.Context, in *GetTaskRequest, opts ...grpc.CallOption) (*Task, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(Task) + err := c.cc.Invoke(ctx, A2AService_GetTask_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *a2AServiceClient) ListTasks(ctx context.Context, in *ListTasksRequest, opts ...grpc.CallOption) (*ListTasksResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(ListTasksResponse) + err := c.cc.Invoke(ctx, A2AService_ListTasks_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *a2AServiceClient) CancelTask(ctx context.Context, in *CancelTaskRequest, opts ...grpc.CallOption) (*Task, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(Task) + err := c.cc.Invoke(ctx, A2AService_CancelTask_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *a2AServiceClient) SubscribeToTask(ctx context.Context, in *SubscribeToTaskRequest, opts ...grpc.CallOption) (grpc.ServerStreamingClient[StreamResponse], error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + stream, err := c.cc.NewStream(ctx, &A2AService_ServiceDesc.Streams[1], A2AService_SubscribeToTask_FullMethodName, cOpts...) + if err != nil { + return nil, err + } + x := &grpc.GenericClientStream[SubscribeToTaskRequest, StreamResponse]{ClientStream: stream} + if err := x.ClientStream.SendMsg(in); err != nil { + return nil, err + } + if err := x.ClientStream.CloseSend(); err != nil { + return nil, err + } + return x, nil +} + +// This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name. +type A2AService_SubscribeToTaskClient = grpc.ServerStreamingClient[StreamResponse] + +func (c *a2AServiceClient) CreateTaskPushNotificationConfig(ctx context.Context, in *TaskPushNotificationConfig, opts ...grpc.CallOption) (*TaskPushNotificationConfig, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(TaskPushNotificationConfig) + err := c.cc.Invoke(ctx, A2AService_CreateTaskPushNotificationConfig_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *a2AServiceClient) GetTaskPushNotificationConfig(ctx context.Context, in *GetTaskPushNotificationConfigRequest, opts ...grpc.CallOption) (*TaskPushNotificationConfig, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(TaskPushNotificationConfig) + err := c.cc.Invoke(ctx, A2AService_GetTaskPushNotificationConfig_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *a2AServiceClient) ListTaskPushNotificationConfigs(ctx context.Context, in *ListTaskPushNotificationConfigsRequest, opts ...grpc.CallOption) (*ListTaskPushNotificationConfigsResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(ListTaskPushNotificationConfigsResponse) + err := c.cc.Invoke(ctx, A2AService_ListTaskPushNotificationConfigs_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *a2AServiceClient) GetExtendedAgentCard(ctx context.Context, in *GetExtendedAgentCardRequest, opts ...grpc.CallOption) (*AgentCard, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(AgentCard) + err := c.cc.Invoke(ctx, A2AService_GetExtendedAgentCard_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *a2AServiceClient) DeleteTaskPushNotificationConfig(ctx context.Context, in *DeleteTaskPushNotificationConfigRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(emptypb.Empty) + err := c.cc.Invoke(ctx, A2AService_DeleteTaskPushNotificationConfig_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +// A2AServiceServer is the server API for A2AService service. +// All implementations must embed UnimplementedA2AServiceServer +// for forward compatibility. +// +// Provides operations for interacting with agents using the A2A protocol. +type A2AServiceServer interface { + // Sends a message to an agent. + SendMessage(context.Context, *SendMessageRequest) (*SendMessageResponse, error) + // Sends a streaming message to an agent, allowing for real-time interaction and status updates. + // Streaming version of `SendMessage` + SendStreamingMessage(*SendMessageRequest, grpc.ServerStreamingServer[StreamResponse]) error + // Gets the latest state of a task. + GetTask(context.Context, *GetTaskRequest) (*Task, error) + // Lists tasks that match the specified filter. + ListTasks(context.Context, *ListTasksRequest) (*ListTasksResponse, error) + // Cancels a task in progress. + CancelTask(context.Context, *CancelTaskRequest) (*Task, error) + // Subscribes to task updates for tasks not in a terminal state. + // Returns `UnsupportedOperationError` if the task is already in a terminal state (completed, failed, canceled, rejected). + SubscribeToTask(*SubscribeToTaskRequest, grpc.ServerStreamingServer[StreamResponse]) error + // (-- api-linter: client-libraries::4232::required-fields=disabled + // + // api-linter: core::0133::method-signature=disabled + // api-linter: core::0133::request-message-name=disabled + // aip.dev/not-precedent: method_signature preserved for backwards compatibility --) + // + // Creates a push notification config for a task. + CreateTaskPushNotificationConfig(context.Context, *TaskPushNotificationConfig) (*TaskPushNotificationConfig, error) + // Gets a push notification config for a task. + GetTaskPushNotificationConfig(context.Context, *GetTaskPushNotificationConfigRequest) (*TaskPushNotificationConfig, error) + // Get a list of push notifications configured for a task. + ListTaskPushNotificationConfigs(context.Context, *ListTaskPushNotificationConfigsRequest) (*ListTaskPushNotificationConfigsResponse, error) + // Gets the extended agent card for the authenticated agent. + GetExtendedAgentCard(context.Context, *GetExtendedAgentCardRequest) (*AgentCard, error) + // Deletes a push notification config for a task. + DeleteTaskPushNotificationConfig(context.Context, *DeleteTaskPushNotificationConfigRequest) (*emptypb.Empty, error) + mustEmbedUnimplementedA2AServiceServer() +} + +// UnimplementedA2AServiceServer must be embedded to have +// forward compatible implementations. +// +// NOTE: this should be embedded by value instead of pointer to avoid a nil +// pointer dereference when methods are called. +type UnimplementedA2AServiceServer struct{} + +func (UnimplementedA2AServiceServer) SendMessage(context.Context, *SendMessageRequest) (*SendMessageResponse, error) { + return nil, status.Error(codes.Unimplemented, "method SendMessage not implemented") +} +func (UnimplementedA2AServiceServer) SendStreamingMessage(*SendMessageRequest, grpc.ServerStreamingServer[StreamResponse]) error { + return status.Error(codes.Unimplemented, "method SendStreamingMessage not implemented") +} +func (UnimplementedA2AServiceServer) GetTask(context.Context, *GetTaskRequest) (*Task, error) { + return nil, status.Error(codes.Unimplemented, "method GetTask not implemented") +} +func (UnimplementedA2AServiceServer) ListTasks(context.Context, *ListTasksRequest) (*ListTasksResponse, error) { + return nil, status.Error(codes.Unimplemented, "method ListTasks not implemented") +} +func (UnimplementedA2AServiceServer) CancelTask(context.Context, *CancelTaskRequest) (*Task, error) { + return nil, status.Error(codes.Unimplemented, "method CancelTask not implemented") +} +func (UnimplementedA2AServiceServer) SubscribeToTask(*SubscribeToTaskRequest, grpc.ServerStreamingServer[StreamResponse]) error { + return status.Error(codes.Unimplemented, "method SubscribeToTask not implemented") +} +func (UnimplementedA2AServiceServer) CreateTaskPushNotificationConfig(context.Context, *TaskPushNotificationConfig) (*TaskPushNotificationConfig, error) { + return nil, status.Error(codes.Unimplemented, "method CreateTaskPushNotificationConfig not implemented") +} +func (UnimplementedA2AServiceServer) GetTaskPushNotificationConfig(context.Context, *GetTaskPushNotificationConfigRequest) (*TaskPushNotificationConfig, error) { + return nil, status.Error(codes.Unimplemented, "method GetTaskPushNotificationConfig not implemented") +} +func (UnimplementedA2AServiceServer) ListTaskPushNotificationConfigs(context.Context, *ListTaskPushNotificationConfigsRequest) (*ListTaskPushNotificationConfigsResponse, error) { + return nil, status.Error(codes.Unimplemented, "method ListTaskPushNotificationConfigs not implemented") +} +func (UnimplementedA2AServiceServer) GetExtendedAgentCard(context.Context, *GetExtendedAgentCardRequest) (*AgentCard, error) { + return nil, status.Error(codes.Unimplemented, "method GetExtendedAgentCard not implemented") +} +func (UnimplementedA2AServiceServer) DeleteTaskPushNotificationConfig(context.Context, *DeleteTaskPushNotificationConfigRequest) (*emptypb.Empty, error) { + return nil, status.Error(codes.Unimplemented, "method DeleteTaskPushNotificationConfig not implemented") +} +func (UnimplementedA2AServiceServer) mustEmbedUnimplementedA2AServiceServer() {} +func (UnimplementedA2AServiceServer) testEmbeddedByValue() {} + +// UnsafeA2AServiceServer may be embedded to opt out of forward compatibility for this service. +// Use of this interface is not recommended, as added methods to A2AServiceServer will +// result in compilation errors. +type UnsafeA2AServiceServer interface { + mustEmbedUnimplementedA2AServiceServer() +} + +func RegisterA2AServiceServer(s grpc.ServiceRegistrar, srv A2AServiceServer) { + // If the following call panics, it indicates UnimplementedA2AServiceServer was + // embedded by pointer and is nil. This will cause panics if an + // unimplemented method is ever invoked, so we test this at initialization + // time to prevent it from happening at runtime later due to I/O. + if t, ok := srv.(interface{ testEmbeddedByValue() }); ok { + t.testEmbeddedByValue() + } + s.RegisterService(&A2AService_ServiceDesc, srv) +} + +func _A2AService_SendMessage_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(SendMessageRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(A2AServiceServer).SendMessage(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: A2AService_SendMessage_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(A2AServiceServer).SendMessage(ctx, req.(*SendMessageRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _A2AService_SendStreamingMessage_Handler(srv interface{}, stream grpc.ServerStream) error { + m := new(SendMessageRequest) + if err := stream.RecvMsg(m); err != nil { + return err + } + return srv.(A2AServiceServer).SendStreamingMessage(m, &grpc.GenericServerStream[SendMessageRequest, StreamResponse]{ServerStream: stream}) +} + +// This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name. +type A2AService_SendStreamingMessageServer = grpc.ServerStreamingServer[StreamResponse] + +func _A2AService_GetTask_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(GetTaskRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(A2AServiceServer).GetTask(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: A2AService_GetTask_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(A2AServiceServer).GetTask(ctx, req.(*GetTaskRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _A2AService_ListTasks_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(ListTasksRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(A2AServiceServer).ListTasks(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: A2AService_ListTasks_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(A2AServiceServer).ListTasks(ctx, req.(*ListTasksRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _A2AService_CancelTask_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(CancelTaskRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(A2AServiceServer).CancelTask(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: A2AService_CancelTask_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(A2AServiceServer).CancelTask(ctx, req.(*CancelTaskRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _A2AService_SubscribeToTask_Handler(srv interface{}, stream grpc.ServerStream) error { + m := new(SubscribeToTaskRequest) + if err := stream.RecvMsg(m); err != nil { + return err + } + return srv.(A2AServiceServer).SubscribeToTask(m, &grpc.GenericServerStream[SubscribeToTaskRequest, StreamResponse]{ServerStream: stream}) +} + +// This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name. +type A2AService_SubscribeToTaskServer = grpc.ServerStreamingServer[StreamResponse] + +func _A2AService_CreateTaskPushNotificationConfig_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(TaskPushNotificationConfig) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(A2AServiceServer).CreateTaskPushNotificationConfig(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: A2AService_CreateTaskPushNotificationConfig_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(A2AServiceServer).CreateTaskPushNotificationConfig(ctx, req.(*TaskPushNotificationConfig)) + } + return interceptor(ctx, in, info, handler) +} + +func _A2AService_GetTaskPushNotificationConfig_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(GetTaskPushNotificationConfigRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(A2AServiceServer).GetTaskPushNotificationConfig(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: A2AService_GetTaskPushNotificationConfig_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(A2AServiceServer).GetTaskPushNotificationConfig(ctx, req.(*GetTaskPushNotificationConfigRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _A2AService_ListTaskPushNotificationConfigs_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(ListTaskPushNotificationConfigsRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(A2AServiceServer).ListTaskPushNotificationConfigs(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: A2AService_ListTaskPushNotificationConfigs_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(A2AServiceServer).ListTaskPushNotificationConfigs(ctx, req.(*ListTaskPushNotificationConfigsRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _A2AService_GetExtendedAgentCard_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(GetExtendedAgentCardRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(A2AServiceServer).GetExtendedAgentCard(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: A2AService_GetExtendedAgentCard_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(A2AServiceServer).GetExtendedAgentCard(ctx, req.(*GetExtendedAgentCardRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _A2AService_DeleteTaskPushNotificationConfig_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(DeleteTaskPushNotificationConfigRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(A2AServiceServer).DeleteTaskPushNotificationConfig(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: A2AService_DeleteTaskPushNotificationConfig_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(A2AServiceServer).DeleteTaskPushNotificationConfig(ctx, req.(*DeleteTaskPushNotificationConfigRequest)) + } + return interceptor(ctx, in, info, handler) +} + +// A2AService_ServiceDesc is the grpc.ServiceDesc for A2AService service. +// It's only intended for direct use with grpc.RegisterService, +// and not to be introspected or modified (even as a copy) +var A2AService_ServiceDesc = grpc.ServiceDesc{ + ServiceName: "lf.a2a.v1.A2AService", + HandlerType: (*A2AServiceServer)(nil), + Methods: []grpc.MethodDesc{ + { + MethodName: "SendMessage", + Handler: _A2AService_SendMessage_Handler, + }, + { + MethodName: "GetTask", + Handler: _A2AService_GetTask_Handler, + }, + { + MethodName: "ListTasks", + Handler: _A2AService_ListTasks_Handler, + }, + { + MethodName: "CancelTask", + Handler: _A2AService_CancelTask_Handler, + }, + { + MethodName: "CreateTaskPushNotificationConfig", + Handler: _A2AService_CreateTaskPushNotificationConfig_Handler, + }, + { + MethodName: "GetTaskPushNotificationConfig", + Handler: _A2AService_GetTaskPushNotificationConfig_Handler, + }, + { + MethodName: "ListTaskPushNotificationConfigs", + Handler: _A2AService_ListTaskPushNotificationConfigs_Handler, + }, + { + MethodName: "GetExtendedAgentCard", + Handler: _A2AService_GetExtendedAgentCard_Handler, + }, + { + MethodName: "DeleteTaskPushNotificationConfig", + Handler: _A2AService_DeleteTaskPushNotificationConfig_Handler, + }, + }, + Streams: []grpc.StreamDesc{ + { + StreamName: "SendStreamingMessage", + Handler: _A2AService_SendStreamingMessage_Handler, + ServerStreams: true, + }, + { + StreamName: "SubscribeToTask", + Handler: _A2AService_SubscribeToTask_Handler, + ServerStreams: true, + }, + }, + Metadata: "a2a.proto", +} diff --git a/extras/scion-a2a-bridge/internal/bridge/config.go b/extras/scion-a2a-bridge/internal/bridge/config.go index 72784df0a..f67786078 100644 --- a/extras/scion-a2a-bridge/internal/bridge/config.go +++ b/extras/scion-a2a-bridge/internal/bridge/config.go @@ -32,10 +32,11 @@ type Config struct { // BridgeConfig holds the A2A protocol server settings. type BridgeConfig struct { - ListenAddress string `yaml:"listen_address"` - ExternalURL string `yaml:"external_url"` - MaxSubscribers int `yaml:"max_subscribers"` - Provider ProviderConfig `yaml:"provider"` + ListenAddress string `yaml:"listen_address"` + GRPCListenAddress string `yaml:"grpc_listen_address"` + ExternalURL string `yaml:"external_url"` + MaxSubscribers int `yaml:"max_subscribers"` + Provider ProviderConfig `yaml:"provider"` } // ProviderConfig describes the bridge operator. diff --git a/extras/scion-a2a-bridge/internal/bridge/grpc_server.go b/extras/scion-a2a-bridge/internal/bridge/grpc_server.go new file mode 100644 index 000000000..9b3c55683 --- /dev/null +++ b/extras/scion-a2a-bridge/internal/bridge/grpc_server.go @@ -0,0 +1,668 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package bridge + +import ( + "context" + "encoding/json" + "errors" + "fmt" + "log/slog" + "net/url" + "strings" + + "google.golang.org/grpc" + "google.golang.org/grpc/codes" + grpcstatus "google.golang.org/grpc/status" + "google.golang.org/protobuf/types/known/emptypb" + "google.golang.org/protobuf/types/known/structpb" + + pb "github.com/GoogleCloudPlatform/scion/extras/scion-a2a-bridge/internal/a2apb" + "github.com/GoogleCloudPlatform/scion/extras/scion-a2a-bridge/internal/state" +) + +// GRPCServer adapts the Bridge to the generated A2AServiceServer interface. +// Each method translates protobuf types to the bridge's internal types, +// delegates to the existing Bridge methods, and translates back. +type GRPCServer struct { + pb.UnimplementedA2AServiceServer + + bridge *Bridge + config *Config + log *slog.Logger +} + +// NewGRPCServer creates a new gRPC server adapter wrapping the given Bridge. +func NewGRPCServer(bridge *Bridge, config *Config, log *slog.Logger) *GRPCServer { + return &GRPCServer{ + bridge: bridge, + config: config, + log: log, + } +} + +// Register registers the A2A gRPC service on the given gRPC server. +func (s *GRPCServer) Register(srv *grpc.Server) { + pb.RegisterA2AServiceServer(srv, s) +} + +// resolveProject extracts the project and agent slugs from the tenant field. +// Supported formats: +// - "projects/{projectSlug}/agents/{agentSlug}" +// - "{projectSlug}/{agentSlug}" +// +// If no tenant is provided we fall back to the first configured project. +func (s *GRPCServer) resolveProject(tenant string) (projectSlug, agentSlug string, err error) { + if tenant == "" { + // Use first configured project if available, but no agent slug. + if len(s.config.Projects) > 0 { + return s.config.Projects[0].Slug, "", fmt.Errorf("tenant is required to identify the agent") + } + return "", "", fmt.Errorf("tenant is required") + } + + parts := strings.Split(tenant, "/") + + // "projects/{project}/agents/{agent}" → 4 segments + if len(parts) == 4 && parts[0] == "projects" && parts[2] == "agents" { + if parts[1] == "" || parts[3] == "" { + return "", "", fmt.Errorf("invalid tenant format: %s", tenant) + } + return parts[1], parts[3], nil + } + + // "{project}/{agent}" → 2 segments + if len(parts) == 2 { + if parts[0] == "" || parts[1] == "" { + return "", "", fmt.Errorf("invalid tenant format: %s", tenant) + } + return parts[0], parts[1], nil + } + + // Single segment → project only, missing agent slug. + if len(parts) == 1 { + return parts[0], "", fmt.Errorf("tenant missing agent slug: %s", tenant) + } + + return "", "", fmt.Errorf("invalid tenant format: %s", tenant) +} + +// SendMessage handles the SendMessage RPC. +func (s *GRPCServer) SendMessage(ctx context.Context, req *pb.SendMessageRequest) (*pb.SendMessageResponse, error) { + projectSlug, agentSlug, err := s.resolveProject(req.GetTenant()) + if err != nil { + return nil, grpcstatus.Errorf(codes.InvalidArgument, "invalid tenant: %v", err) + } + + if err := s.bridge.AuthorizeExposed(projectSlug, agentSlug); err != nil { + return nil, grpcstatus.Errorf(codes.NotFound, "agent not found") + } + + if req.GetMessage() == nil || len(req.GetMessage().GetParts()) == 0 { + return nil, grpcstatus.Errorf(codes.InvalidArgument, "message with parts is required") + } + + parts := pbPartsToInternal(req.GetMessage().GetParts()) + contextID := req.GetMessage().GetContextId() + + blocking := true + if req.GetConfiguration() != nil && req.GetConfiguration().GetReturnImmediately() { + blocking = false + } + + result, err := s.bridge.SendMessage(ctx, projectSlug, agentSlug, contextID, parts, blocking) + if err != nil { + s.log.Error("SendMessage failed", "project", projectSlug, "agent", agentSlug, "error", err) + return nil, bridgeErrToGRPC(err) + } + + return &pb.SendMessageResponse{ + Payload: &pb.SendMessageResponse_Task{ + Task: taskResultToProto(result), + }, + }, nil +} + +// SendStreamingMessage handles the SendStreamingMessage server-streaming RPC. +func (s *GRPCServer) SendStreamingMessage(req *pb.SendMessageRequest, stream grpc.ServerStreamingServer[pb.StreamResponse]) error { + ctx := stream.Context() + + projectSlug, agentSlug, err := s.resolveProject(req.GetTenant()) + if err != nil { + return grpcstatus.Errorf(codes.InvalidArgument, "invalid tenant: %v", err) + } + + if err := s.bridge.AuthorizeExposed(projectSlug, agentSlug); err != nil { + return grpcstatus.Errorf(codes.NotFound, "agent not found") + } + + if req.GetMessage() == nil || len(req.GetMessage().GetParts()) == 0 { + return grpcstatus.Errorf(codes.InvalidArgument, "message with parts is required") + } + + parts := pbPartsToInternal(req.GetMessage().GetParts()) + contextID := req.GetMessage().GetContextId() + + taskID, events, cleanup, err := s.bridge.SendStreamingMessage(ctx, projectSlug, agentSlug, contextID, parts) + if err != nil { + s.log.Error("SendStreamingMessage failed", "project", projectSlug, "agent", agentSlug, "error", err) + return bridgeErrToGRPC(err) + } + defer cleanup() + + return s.streamEvents(ctx, stream, taskID, events) +} + +// GetTask handles the GetTask RPC. +func (s *GRPCServer) GetTask(ctx context.Context, req *pb.GetTaskRequest) (*pb.Task, error) { + if req.GetId() == "" { + return nil, grpcstatus.Errorf(codes.InvalidArgument, "id is required") + } + + result, err := s.bridge.GetTask(ctx, req.GetId()) + if err != nil { + s.log.Error("GetTask failed", "task_id", req.GetId(), "error", err) + return nil, grpcstatus.Errorf(codes.Internal, "failed to retrieve task") + } + if result == nil { + return nil, grpcstatus.Errorf(codes.NotFound, "task not found") + } + + return taskResultToProto(result), nil +} + +// ListTasks handles the ListTasks RPC. +func (s *GRPCServer) ListTasks(ctx context.Context, req *pb.ListTasksRequest) (*pb.ListTasksResponse, error) { + if req.GetContextId() == "" { + return nil, grpcstatus.Errorf(codes.InvalidArgument, "context_id is required") + } + + results, err := s.bridge.ListTasks(ctx, req.GetContextId()) + if err != nil { + s.log.Error("ListTasks failed", "context_id", req.GetContextId(), "error", err) + return nil, grpcstatus.Errorf(codes.Internal, "failed to list tasks") + } + + tasks := make([]*pb.Task, len(results)) + for i, r := range results { + tasks[i] = taskResultToProto(&r) + } + + return &pb.ListTasksResponse{ + Tasks: tasks, + PageSize: int32(len(tasks)), + }, nil +} + +// CancelTask handles the CancelTask RPC. +func (s *GRPCServer) CancelTask(ctx context.Context, req *pb.CancelTaskRequest) (*pb.Task, error) { + if req.GetId() == "" { + return nil, grpcstatus.Errorf(codes.InvalidArgument, "id is required") + } + + result, err := s.bridge.CancelTask(ctx, req.GetId()) + if err != nil { + if strings.Contains(err.Error(), "terminal state") { + return nil, grpcstatus.Errorf(codes.FailedPrecondition, "task cannot be canceled") + } + return nil, grpcstatus.Errorf(codes.Internal, "cancel task failed") + } + if result == nil { + return nil, grpcstatus.Errorf(codes.NotFound, "task not found") + } + + return taskResultToProto(result), nil +} + +// SubscribeToTask handles the SubscribeToTask server-streaming RPC. +func (s *GRPCServer) SubscribeToTask(req *pb.SubscribeToTaskRequest, stream grpc.ServerStreamingServer[pb.StreamResponse]) error { + ctx := stream.Context() + + if req.GetId() == "" { + return grpcstatus.Errorf(codes.InvalidArgument, "id is required") + } + + events, cleanup, err := s.bridge.SubscribeToTask(ctx, req.GetId()) + if err != nil { + s.log.Error("SubscribeToTask failed", "task_id", req.GetId(), "error", err) + return grpcstatus.Errorf(codes.Internal, "failed to subscribe to task") + } + defer cleanup() + + return s.streamEvents(ctx, stream, req.GetId(), events) +} + +// CreateTaskPushNotificationConfig handles the CreateTaskPushNotificationConfig RPC. +func (s *GRPCServer) CreateTaskPushNotificationConfig(ctx context.Context, req *pb.TaskPushNotificationConfig) (*pb.TaskPushNotificationConfig, error) { + if req.GetTaskId() == "" { + return nil, grpcstatus.Errorf(codes.InvalidArgument, "task_id is required") + } + if req.GetUrl() == "" { + return nil, grpcstatus.Errorf(codes.InvalidArgument, "url is required") + } + + // Validate URL format (scheme + host) before bridge call — matches HTTP handler. + parsed, err := url.Parse(req.GetUrl()) + if err != nil || parsed.Host == "" || (parsed.Scheme != "http" && parsed.Scheme != "https") { + return nil, grpcstatus.Errorf(codes.InvalidArgument, "url must be an absolute http or https URL") + } + + var authScheme, authCredentials string + if req.GetAuthentication() != nil { + authScheme = req.GetAuthentication().GetScheme() + authCredentials = req.GetAuthentication().GetCredentials() + } + + cfg, err := s.bridge.SetPushNotificationConfig(ctx, req.GetTaskId(), req.GetUrl(), req.GetToken(), authScheme, authCredentials) + if err != nil { + s.log.Error("SetPushNotificationConfig failed", "task_id", req.GetTaskId(), "error", err) + return nil, grpcstatus.Errorf(codes.Internal, "failed to set push notification config") + } + + return pushConfigToProto(cfg), nil +} + +// GetTaskPushNotificationConfig handles the GetTaskPushNotificationConfig RPC. +func (s *GRPCServer) GetTaskPushNotificationConfig(ctx context.Context, req *pb.GetTaskPushNotificationConfigRequest) (*pb.TaskPushNotificationConfig, error) { + if req.GetTaskId() == "" { + return nil, grpcstatus.Errorf(codes.InvalidArgument, "task_id is required") + } + if req.GetId() == "" { + return nil, grpcstatus.Errorf(codes.InvalidArgument, "id is required") + } + + configs, err := s.bridge.GetPushNotificationConfig(ctx, req.GetTaskId()) + if err != nil { + s.log.Error("GetPushNotificationConfig failed", "task_id", req.GetTaskId(), "error", err) + return nil, grpcstatus.Errorf(codes.Internal, "failed to get push notification config") + } + + // Return the specific config by ID if provided. + for _, c := range configs { + if c.ID == req.GetId() { + return pushConfigToProto(&c), nil + } + } + + return nil, grpcstatus.Errorf(codes.NotFound, "push notification config not found") +} + +// ListTaskPushNotificationConfigs handles the ListTaskPushNotificationConfigs RPC. +func (s *GRPCServer) ListTaskPushNotificationConfigs(ctx context.Context, req *pb.ListTaskPushNotificationConfigsRequest) (*pb.ListTaskPushNotificationConfigsResponse, error) { + if req.GetTaskId() == "" { + return nil, grpcstatus.Errorf(codes.InvalidArgument, "task_id is required") + } + + configs, err := s.bridge.GetPushNotificationConfig(ctx, req.GetTaskId()) + if err != nil { + s.log.Error("ListTaskPushNotificationConfigs failed", "task_id", req.GetTaskId(), "error", err) + return nil, grpcstatus.Errorf(codes.Internal, "failed to list push notification configs") + } + + pbConfigs := make([]*pb.TaskPushNotificationConfig, len(configs)) + for i, c := range configs { + pbConfigs[i] = pushConfigToProto(&c) + } + + return &pb.ListTaskPushNotificationConfigsResponse{ + Configs: pbConfigs, + }, nil +} + +// GetExtendedAgentCard handles the GetExtendedAgentCard RPC. +func (s *GRPCServer) GetExtendedAgentCard(ctx context.Context, req *pb.GetExtendedAgentCardRequest) (*pb.AgentCard, error) { + projectSlug, agentSlug, err := s.resolveProject(req.GetTenant()) + if err != nil { + return nil, grpcstatus.Errorf(codes.InvalidArgument, "invalid tenant: %v", err) + } + + card := s.bridge.GenerateAgentCard(ctx, projectSlug, agentSlug) + return agentCardMapToProto(card), nil +} + +// DeleteTaskPushNotificationConfig handles the DeleteTaskPushNotificationConfig RPC. +func (s *GRPCServer) DeleteTaskPushNotificationConfig(ctx context.Context, req *pb.DeleteTaskPushNotificationConfigRequest) (*emptypb.Empty, error) { + if req.GetTaskId() == "" { + return nil, grpcstatus.Errorf(codes.InvalidArgument, "task_id is required") + } + if req.GetId() == "" { + return nil, grpcstatus.Errorf(codes.InvalidArgument, "id is required") + } + + if err := s.bridge.DeletePushNotificationConfig(ctx, req.GetTaskId(), req.GetId()); err != nil { + s.log.Error("DeletePushNotificationConfig failed", "task_id", req.GetTaskId(), "config_id", req.GetId(), "error", err) + return nil, grpcstatus.Errorf(codes.Internal, "failed to delete push notification config") + } + + return &emptypb.Empty{}, nil +} + +// streamEvents reads from the bridge's event channel and sends protobuf StreamResponse +// messages to the gRPC stream until the channel closes or the context is cancelled. +func (s *GRPCServer) streamEvents(ctx context.Context, stream grpc.ServerStreamingServer[pb.StreamResponse], taskID string, events <-chan StreamEvent) error { + for { + select { + case event, ok := <-events: + if !ok { + return nil + } + + resp := streamEventToProto(taskID, event) + if resp == nil { + continue + } + if err := stream.Send(resp); err != nil { + return err + } + + if event.StatusUpdate != nil && event.StatusUpdate.Final { + return nil + } + + case <-ctx.Done(): + return ctx.Err() + } + } +} + +// --- Type translation functions --- + +// pbPartsToInternal converts protobuf Part messages to internal Part types. +func pbPartsToInternal(pbParts []*pb.Part) []Part { + parts := make([]Part, 0, len(pbParts)) + for _, p := range pbParts { + part := Part{ + MediaType: p.GetMediaType(), + } + switch c := p.GetContent().(type) { + case *pb.Part_Text: + part.Text = c.Text + case *pb.Part_Url: + part.URL = c.Url + case *pb.Part_Data: + if c.Data != nil { + part.Data = c.Data.AsInterface() + } + } + parts = append(parts, part) + } + return parts +} + +// internalPartToProto converts an internal Part to a protobuf Part. +func internalPartToProto(p Part) *pb.Part { + pbPart := &pb.Part{ + MediaType: p.MediaType, + } + switch { + case p.Text != "": + pbPart.Content = &pb.Part_Text{Text: p.Text} + case p.URL != "": + pbPart.Content = &pb.Part_Url{Url: p.URL} + case p.Data != nil: + // Convert structured data to a protobuf Value. + // First try direct conversion; fall back to JSON round-trip for + // complex types (e.g. maps with non-string keys). + val, err := structpb.NewValue(p.Data) + if err != nil { + // structpb.NewValue doesn't handle all Go types directly. + // Round-trip through JSON to normalize to JSON-compatible types. + b, jerr := json.Marshal(p.Data) + if jerr != nil { + // Last resort: emit as text so data is not silently lost. + pbPart.Content = &pb.Part_Text{Text: fmt.Sprintf("%v", p.Data)} + break + } + var raw interface{} + if err2 := json.Unmarshal(b, &raw); err2 != nil { + pbPart.Content = &pb.Part_Text{Text: string(b)} + break + } + val, err = structpb.NewValue(raw) + if err != nil { + pbPart.Content = &pb.Part_Text{Text: string(b)} + break + } + } + pbPart.Content = &pb.Part_Data{Data: val} + } + return pbPart +} + +// taskStateToProto maps an internal task state string to a protobuf TaskState enum. +func taskStateToProto(state string) pb.TaskState { + switch state { + case TaskStateSubmitted: + return pb.TaskState_TASK_STATE_SUBMITTED + case TaskStateWorking: + return pb.TaskState_TASK_STATE_WORKING + case TaskStateCompleted: + return pb.TaskState_TASK_STATE_COMPLETED + case TaskStateFailed: + return pb.TaskState_TASK_STATE_FAILED + case TaskStateCanceled: + return pb.TaskState_TASK_STATE_CANCELED + case TaskStateInputRequired: + return pb.TaskState_TASK_STATE_INPUT_REQUIRED + case TaskStateRejected: + return pb.TaskState_TASK_STATE_REJECTED + default: + return pb.TaskState_TASK_STATE_UNSPECIFIED + } +} + +// protoTaskStateToInternal maps a protobuf TaskState enum to an internal state string. +func protoTaskStateToInternal(state pb.TaskState) string { + switch state { + case pb.TaskState_TASK_STATE_SUBMITTED: + return TaskStateSubmitted + case pb.TaskState_TASK_STATE_WORKING: + return TaskStateWorking + case pb.TaskState_TASK_STATE_COMPLETED: + return TaskStateCompleted + case pb.TaskState_TASK_STATE_FAILED: + return TaskStateFailed + case pb.TaskState_TASK_STATE_CANCELED: + return TaskStateCanceled + case pb.TaskState_TASK_STATE_INPUT_REQUIRED: + return TaskStateInputRequired + case pb.TaskState_TASK_STATE_REJECTED: + return TaskStateRejected + default: + return "" + } +} + +// taskResultToProto converts an internal TaskResult to a protobuf Task. +func taskResultToProto(r *TaskResult) *pb.Task { + task := &pb.Task{ + Id: r.ID, + ContextId: r.ContextID, + Status: &pb.TaskStatus{ + State: taskStateToProto(r.Status.State), + }, + } + + if r.Status.Message != nil { + task.Status.Message = messageToProto(r.Status.Message) + } + + for _, a := range r.Artifacts { + task.Artifacts = append(task.Artifacts, artifactToProto(&a)) + } + + return task +} + +// messageToProto converts an internal Message to a protobuf Message. +func messageToProto(m *Message) *pb.Message { + pbMsg := &pb.Message{ + MessageId: m.MessageID, + Role: roleToProto(m.Role), + } + for _, p := range m.Parts { + pbMsg.Parts = append(pbMsg.Parts, internalPartToProto(p)) + } + return pbMsg +} + +// roleToProto maps an internal role string to a protobuf Role enum. +func roleToProto(role string) pb.Role { + switch role { + case RoleUser: + return pb.Role_ROLE_USER + case RoleAgent: + return pb.Role_ROLE_AGENT + default: + return pb.Role_ROLE_UNSPECIFIED + } +} + +// artifactToProto converts an internal Artifact to a protobuf Artifact. +func artifactToProto(a *Artifact) *pb.Artifact { + pbArt := &pb.Artifact{ + ArtifactId: a.ArtifactID, + Name: a.Name, + } + for _, p := range a.Parts { + pbArt.Parts = append(pbArt.Parts, internalPartToProto(p)) + } + return pbArt +} + +// pushConfigToProto converts a state.PushNotificationConfig to protobuf. +func pushConfigToProto(c *state.PushNotificationConfig) *pb.TaskPushNotificationConfig { + cfg := &pb.TaskPushNotificationConfig{ + Id: c.ID, + TaskId: c.TaskID, + Url: c.URL, + Token: c.Token, + } + if c.AuthScheme != "" { + cfg.Authentication = &pb.AuthenticationInfo{ + Scheme: c.AuthScheme, + Credentials: c.AuthCredentials, + } + } + return cfg +} + +// agentCardMapToProto converts the bridge's map-based agent card to a protobuf AgentCard. +// This is a best-effort conversion since the bridge returns a generic map. +func agentCardMapToProto(card map[string]interface{}) *pb.AgentCard { + pbCard := &pb.AgentCard{ + Name: getStringField(card, "name"), + Description: getStringField(card, "description"), + Version: getStringField(card, "version"), + } + + if url, ok := card["url"].(string); ok { + pbCard.SupportedInterfaces = []*pb.AgentInterface{ + { + Url: url, + ProtocolBinding: "JSONRPC", + ProtocolVersion: "0.3", + }, + } + } + + if inputModes, ok := card["defaultInputModes"].([]string); ok { + pbCard.DefaultInputModes = inputModes + } + if outputModes, ok := card["defaultOutputModes"].([]string); ok { + pbCard.DefaultOutputModes = outputModes + } + + if skills, ok := card["skills"].([]map[string]interface{}); ok { + for _, s := range skills { + pbCard.Skills = append(pbCard.Skills, &pb.AgentSkill{ + Id: getStringField(s, "id"), + Name: getStringField(s, "name"), + Description: getStringField(s, "description"), + }) + } + } + + if provider, ok := card["provider"].(map[string]string); ok { + pbCard.Provider = &pb.AgentProvider{ + Organization: provider["organization"], + Url: provider["url"], + } + } + + return pbCard +} + +// streamEventToProto converts an internal StreamEvent to a protobuf StreamResponse. +func streamEventToProto(taskID string, event StreamEvent) *pb.StreamResponse { + switch { + case event.Task != nil: + return &pb.StreamResponse{ + Payload: &pb.StreamResponse_Task{ + Task: taskResultToProto(event.Task), + }, + } + + case event.StatusUpdate != nil: + pbStatus := &pb.TaskStatus{ + State: taskStateToProto(event.StatusUpdate.Status.State), + } + if event.StatusUpdate.Status.Message != nil { + pbStatus.Message = messageToProto(event.StatusUpdate.Status.Message) + } + return &pb.StreamResponse{ + Payload: &pb.StreamResponse_StatusUpdate{ + StatusUpdate: &pb.TaskStatusUpdateEvent{ + TaskId: event.StatusUpdate.TaskID, + Status: pbStatus, + }, + }, + } + + case event.ArtifactUpdate != nil: + return &pb.StreamResponse{ + Payload: &pb.StreamResponse_ArtifactUpdate{ + ArtifactUpdate: &pb.TaskArtifactUpdateEvent{ + TaskId: event.ArtifactUpdate.TaskID, + Artifact: artifactToProto(&event.ArtifactUpdate.Artifact), + LastChunk: event.ArtifactUpdate.Artifact.LastChunk, + }, + }, + } + } + + return nil +} + +// bridgeErrToGRPC translates bridge errors to appropriate gRPC status codes. +// Error messages are kept generic to avoid leaking implementation details. +func bridgeErrToGRPC(err error) error { + switch { + case errors.Is(err, ErrAgentNotFound): + return grpcstatus.Errorf(codes.NotFound, "agent not found") + case errors.Is(err, ErrContextUnknown): + return grpcstatus.Errorf(codes.InvalidArgument, "unknown context ID") + default: + return grpcstatus.Errorf(codes.Internal, "internal error") + } +} + +// getStringField safely extracts a string field from a map. +func getStringField(m map[string]interface{}, key string) string { + if v, ok := m[key].(string); ok { + return v + } + return "" +} diff --git a/extras/scion-a2a-bridge/internal/bridge/grpc_server_test.go b/extras/scion-a2a-bridge/internal/bridge/grpc_server_test.go new file mode 100644 index 000000000..90610221d --- /dev/null +++ b/extras/scion-a2a-bridge/internal/bridge/grpc_server_test.go @@ -0,0 +1,945 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package bridge + +import ( + "context" + "io" + "log/slog" + "net" + "path/filepath" + "strings" + "testing" + + "google.golang.org/grpc" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/credentials/insecure" + grpcstatus "google.golang.org/grpc/status" + + pb "github.com/GoogleCloudPlatform/scion/extras/scion-a2a-bridge/internal/a2apb" + "github.com/GoogleCloudPlatform/scion/extras/scion-a2a-bridge/internal/state" +) + +// --- Type translation tests --- + +func TestPbPartsToInternal(t *testing.T) { + tests := []struct { + name string + pbParts []*pb.Part + wantLen int + wantText string + wantURL string + }{ + { + name: "text part", + pbParts: []*pb.Part{ + {Content: &pb.Part_Text{Text: "hello"}, MediaType: "text/plain"}, + }, + wantLen: 1, + wantText: "hello", + }, + { + name: "url part", + pbParts: []*pb.Part{ + {Content: &pb.Part_Url{Url: "https://example.com/file.txt"}}, + }, + wantLen: 1, + wantURL: "https://example.com/file.txt", + }, + { + name: "empty parts", + pbParts: []*pb.Part{}, + wantLen: 0, + }, + { + name: "mixed parts", + pbParts: []*pb.Part{ + {Content: &pb.Part_Text{Text: "text1"}, MediaType: "text/plain"}, + {Content: &pb.Part_Url{Url: "https://example.com"}}, + }, + wantLen: 2, + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + parts := pbPartsToInternal(tc.pbParts) + if len(parts) != tc.wantLen { + t.Fatalf("got %d parts, want %d", len(parts), tc.wantLen) + } + if tc.wantText != "" && parts[0].Text != tc.wantText { + t.Errorf("text = %q, want %q", parts[0].Text, tc.wantText) + } + if tc.wantURL != "" && parts[0].URL != tc.wantURL { + t.Errorf("url = %q, want %q", parts[0].URL, tc.wantURL) + } + }) + } +} + +func TestInternalPartToProto(t *testing.T) { + tests := []struct { + name string + part Part + wantText string + wantURL string + wantMedia string + }{ + { + name: "text part", + part: Part{Text: "hello", MediaType: "text/plain"}, + wantText: "hello", + wantMedia: "text/plain", + }, + { + name: "url part", + part: Part{URL: "https://example.com/file"}, + wantURL: "https://example.com/file", + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + pbPart := internalPartToProto(tc.part) + if tc.wantText != "" { + if text, ok := pbPart.Content.(*pb.Part_Text); !ok || text.Text != tc.wantText { + t.Errorf("text = %v, want %q", pbPart.Content, tc.wantText) + } + } + if tc.wantURL != "" { + if url, ok := pbPart.Content.(*pb.Part_Url); !ok || url.Url != tc.wantURL { + t.Errorf("url = %v, want %q", pbPart.Content, tc.wantURL) + } + } + if pbPart.MediaType != tc.wantMedia { + t.Errorf("media_type = %q, want %q", pbPart.MediaType, tc.wantMedia) + } + }) + } +} + +func TestInternalPartToProto_StructuredData(t *testing.T) { + tests := []struct { + name string + data interface{} + }{ + { + name: "map data", + data: map[string]interface{}{ + "key": "value", + "nested": map[string]interface{}{"a": float64(1)}, + }, + }, + { + name: "slice data", + data: []interface{}{"a", "b", "c"}, + }, + { + name: "string data", + data: "just-a-string", + }, + { + name: "numeric data", + data: float64(42), + }, + { + name: "boolean data", + data: true, + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + part := Part{Data: tc.data, MediaType: "application/json"} + pbPart := internalPartToProto(part) + + dataPart, ok := pbPart.Content.(*pb.Part_Data) + if !ok { + t.Fatalf("expected Part_Data, got %T", pbPart.Content) + } + if dataPart.Data == nil { + t.Fatal("data value is nil") + } + }) + } +} + +func TestInternalPartToProto_NilDataIgnored(t *testing.T) { + part := Part{MediaType: "text/plain"} + pbPart := internalPartToProto(part) + if pbPart.Content != nil { + t.Errorf("expected nil content for empty part, got %T", pbPart.Content) + } +} + +func TestTaskStateToProto(t *testing.T) { + tests := []struct { + state string + want pb.TaskState + }{ + {TaskStateSubmitted, pb.TaskState_TASK_STATE_SUBMITTED}, + {TaskStateWorking, pb.TaskState_TASK_STATE_WORKING}, + {TaskStateCompleted, pb.TaskState_TASK_STATE_COMPLETED}, + {TaskStateFailed, pb.TaskState_TASK_STATE_FAILED}, + {TaskStateCanceled, pb.TaskState_TASK_STATE_CANCELED}, + {TaskStateInputRequired, pb.TaskState_TASK_STATE_INPUT_REQUIRED}, + {TaskStateRejected, pb.TaskState_TASK_STATE_REJECTED}, + {"unknown", pb.TaskState_TASK_STATE_UNSPECIFIED}, + } + + for _, tc := range tests { + t.Run(tc.state, func(t *testing.T) { + got := taskStateToProto(tc.state) + if got != tc.want { + t.Errorf("taskStateToProto(%q) = %v, want %v", tc.state, got, tc.want) + } + }) + } +} + +func TestProtoTaskStateToInternal(t *testing.T) { + tests := []struct { + state pb.TaskState + want string + }{ + {pb.TaskState_TASK_STATE_SUBMITTED, TaskStateSubmitted}, + {pb.TaskState_TASK_STATE_WORKING, TaskStateWorking}, + {pb.TaskState_TASK_STATE_COMPLETED, TaskStateCompleted}, + {pb.TaskState_TASK_STATE_FAILED, TaskStateFailed}, + {pb.TaskState_TASK_STATE_CANCELED, TaskStateCanceled}, + {pb.TaskState_TASK_STATE_INPUT_REQUIRED, TaskStateInputRequired}, + {pb.TaskState_TASK_STATE_REJECTED, TaskStateRejected}, + {pb.TaskState_TASK_STATE_UNSPECIFIED, ""}, + } + + for _, tc := range tests { + t.Run(tc.state.String(), func(t *testing.T) { + got := protoTaskStateToInternal(tc.state) + if got != tc.want { + t.Errorf("protoTaskStateToInternal(%v) = %q, want %q", tc.state, got, tc.want) + } + }) + } +} + +func TestTaskResultToProto(t *testing.T) { + msg := &Message{MessageID: "msg-1", Role: RoleAgent, Parts: []Part{{Text: "response", MediaType: "text/plain"}}} + result := &TaskResult{ + ID: "task-123", + ContextID: "ctx-456", + Status: TaskStatus{State: TaskStateCompleted, Message: msg}, + Artifacts: []Artifact{ + {ArtifactID: "art-1", Name: "output", Parts: []Part{{Text: "data"}}}, + }, + } + + pbTask := taskResultToProto(result) + + if pbTask.Id != "task-123" { + t.Errorf("id = %q, want %q", pbTask.Id, "task-123") + } + if pbTask.ContextId != "ctx-456" { + t.Errorf("context_id = %q, want %q", pbTask.ContextId, "ctx-456") + } + if pbTask.Status.State != pb.TaskState_TASK_STATE_COMPLETED { + t.Errorf("state = %v, want COMPLETED", pbTask.Status.State) + } + if pbTask.Status.Message == nil { + t.Fatal("status.message is nil") + } + if pbTask.Status.Message.MessageId != "msg-1" { + t.Errorf("message_id = %q, want %q", pbTask.Status.Message.MessageId, "msg-1") + } + if len(pbTask.Artifacts) != 1 { + t.Fatalf("artifacts count = %d, want 1", len(pbTask.Artifacts)) + } + if pbTask.Artifacts[0].ArtifactId != "art-1" { + t.Errorf("artifact_id = %q, want %q", pbTask.Artifacts[0].ArtifactId, "art-1") + } +} + +func TestRoleToProto(t *testing.T) { + if roleToProto(RoleUser) != pb.Role_ROLE_USER { + t.Errorf("RoleUser mapping failed") + } + if roleToProto(RoleAgent) != pb.Role_ROLE_AGENT { + t.Errorf("RoleAgent mapping failed") + } + if roleToProto("unknown") != pb.Role_ROLE_UNSPECIFIED { + t.Errorf("unknown role mapping failed") + } +} + +func TestPushConfigToProto(t *testing.T) { + cfg := &state.PushNotificationConfig{ + ID: "push-1", + TaskID: "task-1", + URL: "https://example.com/webhook", + Token: "tok-123", + AuthScheme: "Bearer", + AuthCredentials: "secret", + } + + pbCfg := pushConfigToProto(cfg) + + if pbCfg.Id != "push-1" { + t.Errorf("id = %q, want %q", pbCfg.Id, "push-1") + } + if pbCfg.TaskId != "task-1" { + t.Errorf("task_id = %q, want %q", pbCfg.TaskId, "task-1") + } + if pbCfg.Url != "https://example.com/webhook" { + t.Errorf("url = %q, want %q", pbCfg.Url, "https://example.com/webhook") + } + if pbCfg.Token != "tok-123" { + t.Errorf("token = %q, want %q", pbCfg.Token, "tok-123") + } + if pbCfg.Authentication == nil { + t.Fatal("authentication is nil") + } + if pbCfg.Authentication.Scheme != "Bearer" { + t.Errorf("auth scheme = %q, want %q", pbCfg.Authentication.Scheme, "Bearer") + } + if pbCfg.Authentication.Credentials != "secret" { + t.Errorf("auth credentials = %q, want %q", pbCfg.Authentication.Credentials, "secret") + } +} + +func TestPushConfigToProtoNoAuth(t *testing.T) { + cfg := &state.PushNotificationConfig{ + ID: "push-2", + TaskID: "task-2", + URL: "https://example.com/hook", + } + + pbCfg := pushConfigToProto(cfg) + if pbCfg.Authentication != nil { + t.Errorf("expected nil authentication, got %v", pbCfg.Authentication) + } +} + +func TestStreamEventToProto(t *testing.T) { + t.Run("task event", func(t *testing.T) { + event := StreamEvent{ + Task: &TaskResult{ + ID: "t-1", + ContextID: "c-1", + Status: TaskStatus{State: TaskStateWorking}, + }, + } + resp := streamEventToProto("t-1", event) + if resp == nil { + t.Fatal("nil response") + } + taskResp, ok := resp.Payload.(*pb.StreamResponse_Task) + if !ok { + t.Fatalf("expected task payload, got %T", resp.Payload) + } + if taskResp.Task.Id != "t-1" { + t.Errorf("task id = %q, want %q", taskResp.Task.Id, "t-1") + } + }) + + t.Run("status update event", func(t *testing.T) { + event := StreamEvent{ + StatusUpdate: &TaskStatusUpdate{ + TaskID: "t-2", + Status: TaskStatus{State: TaskStateCompleted}, + Final: true, + }, + } + resp := streamEventToProto("t-2", event) + if resp == nil { + t.Fatal("nil response") + } + statusResp, ok := resp.Payload.(*pb.StreamResponse_StatusUpdate) + if !ok { + t.Fatalf("expected status_update payload, got %T", resp.Payload) + } + if statusResp.StatusUpdate.TaskId != "t-2" { + t.Errorf("task id = %q, want %q", statusResp.StatusUpdate.TaskId, "t-2") + } + if statusResp.StatusUpdate.Status.State != pb.TaskState_TASK_STATE_COMPLETED { + t.Errorf("state = %v, want COMPLETED", statusResp.StatusUpdate.Status.State) + } + }) + + t.Run("status update with message", func(t *testing.T) { + event := StreamEvent{ + StatusUpdate: &TaskStatusUpdate{ + TaskID: "t-2b", + Status: TaskStatus{ + State: TaskStateFailed, + Message: &Message{ + MessageID: "err-1", + Role: RoleAgent, + Parts: []Part{{Text: "something went wrong"}}, + }, + }, + Final: true, + }, + } + resp := streamEventToProto("t-2b", event) + if resp == nil { + t.Fatal("nil response") + } + statusResp, ok := resp.Payload.(*pb.StreamResponse_StatusUpdate) + if !ok { + t.Fatalf("expected status_update payload, got %T", resp.Payload) + } + if statusResp.StatusUpdate.Status.Message == nil { + t.Fatal("status message should not be nil when set on the event") + } + if statusResp.StatusUpdate.Status.Message.MessageId != "err-1" { + t.Errorf("message_id = %q, want %q", statusResp.StatusUpdate.Status.Message.MessageId, "err-1") + } + if len(statusResp.StatusUpdate.Status.Message.Parts) != 1 { + t.Fatalf("parts count = %d, want 1", len(statusResp.StatusUpdate.Status.Message.Parts)) + } + }) + + t.Run("artifact update event", func(t *testing.T) { + event := StreamEvent{ + ArtifactUpdate: &TaskArtifactUpdate{ + TaskID: "t-3", + Artifact: Artifact{ + ArtifactID: "art-1", + Parts: []Part{{Text: "result"}}, + }, + }, + } + resp := streamEventToProto("t-3", event) + if resp == nil { + t.Fatal("nil response") + } + artResp, ok := resp.Payload.(*pb.StreamResponse_ArtifactUpdate) + if !ok { + t.Fatalf("expected artifact_update payload, got %T", resp.Payload) + } + if artResp.ArtifactUpdate.TaskId != "t-3" { + t.Errorf("task id = %q, want %q", artResp.ArtifactUpdate.TaskId, "t-3") + } + }) + + t.Run("artifact update with last_chunk", func(t *testing.T) { + event := StreamEvent{ + ArtifactUpdate: &TaskArtifactUpdate{ + TaskID: "t-3b", + Artifact: Artifact{ + ArtifactID: "art-final", + Parts: []Part{{Text: "done"}}, + LastChunk: true, + }, + }, + } + resp := streamEventToProto("t-3b", event) + if resp == nil { + t.Fatal("nil response") + } + artResp, ok := resp.Payload.(*pb.StreamResponse_ArtifactUpdate) + if !ok { + t.Fatalf("expected artifact_update payload, got %T", resp.Payload) + } + if !artResp.ArtifactUpdate.LastChunk { + t.Error("expected LastChunk=true, got false") + } + }) + + t.Run("empty event", func(t *testing.T) { + resp := streamEventToProto("t-4", StreamEvent{}) + if resp != nil { + t.Errorf("expected nil for empty event, got %v", resp) + } + }) +} + +func TestBridgeErrToGRPC(t *testing.T) { + tests := []struct { + name string + err error + wantCode codes.Code + }{ + {"agent not found", ErrAgentNotFound, codes.NotFound}, + {"context unknown", ErrContextUnknown, codes.InvalidArgument}, + {"generic error", io.ErrUnexpectedEOF, codes.Internal}, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + grpcErr := bridgeErrToGRPC(tc.err) + st, ok := grpcstatus.FromError(grpcErr) + if !ok { + t.Fatal("expected gRPC status error") + } + if st.Code() != tc.wantCode { + t.Errorf("code = %v, want %v", st.Code(), tc.wantCode) + } + }) + } +} + +func TestResolveProject(t *testing.T) { + cfg := &Config{ + Projects: []ProjectConfig{ + {Slug: "default-project"}, + }, + } + s := &GRPCServer{config: cfg} + + tests := []struct { + name string + tenant string + wantProject string + wantAgent string + wantErr bool + }{ + { + name: "full resource name", + tenant: "projects/my-project/agents/my-agent", + wantProject: "my-project", + wantAgent: "my-agent", + }, + { + name: "simple two-part format", + tenant: "my-project/my-agent", + wantProject: "my-project", + wantAgent: "my-agent", + }, + { + name: "full resource name with hyphens and numbers", + tenant: "projects/proj-123/agents/agent-456", + wantProject: "proj-123", + wantAgent: "agent-456", + }, + { + name: "empty tenant falls back with error", + tenant: "", + wantErr: true, + }, + { + name: "single segment is missing agent", + tenant: "just-project", + wantErr: true, + }, + { + name: "three segments is invalid", + tenant: "a/b/c", + wantErr: true, + }, + { + name: "empty project in full format", + tenant: "projects//agents/my-agent", + wantErr: true, + }, + { + name: "empty agent in full format", + tenant: "projects/my-project/agents/", + wantErr: true, + }, + { + name: "empty parts in simple format", + tenant: "/my-agent", + wantErr: true, + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + project, agent, err := s.resolveProject(tc.tenant) + if tc.wantErr { + if err == nil { + t.Errorf("expected error, got project=%q agent=%q", project, agent) + } + return + } + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if project != tc.wantProject { + t.Errorf("project = %q, want %q", project, tc.wantProject) + } + if agent != tc.wantAgent { + t.Errorf("agent = %q, want %q", agent, tc.wantAgent) + } + }) + } +} + +// --- gRPC server integration tests --- + +// newTestGRPCServer sets up a gRPC server with a real Bridge backed by an in-memory +// SQLite database (no hub client), returning a connected client and cleanup function. +func newTestGRPCServer(t *testing.T) (pb.A2AServiceClient, *state.Store) { + t.Helper() + + dir := t.TempDir() + store, err := state.New(filepath.Join(dir, "test.db")) + if err != nil { + t.Fatalf("state.New: %v", err) + } + t.Cleanup(func() { store.Close() }) + + cfg := &Config{ + Bridge: BridgeConfig{ + ExternalURL: "https://a2a.test.example.com", + Provider: ProviderConfig{ + Organization: "Test Org", + URL: "https://test.example.com", + }, + }, + Projects: []ProjectConfig{ + { + Slug: "test-project", + ExposedAgents: []string{"test-agent"}, + }, + }, + } + + log := slog.New(slog.NewTextHandler(io.Discard, nil)) + b := New(store, nil, nil, cfg, nil, log) + t.Cleanup(b.Shutdown) + + grpcSrv := grpc.NewServer() + adapter := NewGRPCServer(b, cfg, log) + adapter.Register(grpcSrv) + + lis, err := net.Listen("tcp", "localhost:0") + if err != nil { + t.Fatalf("listen: %v", err) + } + + go func() { + if err := grpcSrv.Serve(lis); err != nil { + // Server stopped, expected during cleanup. + } + }() + t.Cleanup(grpcSrv.GracefulStop) + + conn, err := grpc.NewClient(lis.Addr().String(), + grpc.WithTransportCredentials(insecure.NewCredentials()), + ) + if err != nil { + t.Fatalf("dial: %v", err) + } + t.Cleanup(func() { conn.Close() }) + + return pb.NewA2AServiceClient(conn), store +} + +func TestGRPCGetTask_NotFound(t *testing.T) { + client, _ := newTestGRPCServer(t) + + _, err := client.GetTask(context.Background(), &pb.GetTaskRequest{Id: "nonexistent-task"}) + if err == nil { + t.Fatal("expected error for nonexistent task") + } + st, ok := grpcstatus.FromError(err) + if !ok { + t.Fatalf("expected gRPC status, got %v", err) + } + if st.Code() != codes.NotFound { + t.Errorf("code = %v, want NotFound", st.Code()) + } +} + +func TestGRPCGetTask_MissingID(t *testing.T) { + client, _ := newTestGRPCServer(t) + + _, err := client.GetTask(context.Background(), &pb.GetTaskRequest{}) + if err == nil { + t.Fatal("expected error for missing ID") + } + st, _ := grpcstatus.FromError(err) + if st.Code() != codes.InvalidArgument { + t.Errorf("code = %v, want InvalidArgument", st.Code()) + } +} + +func TestGRPCGetTask_Exists(t *testing.T) { + client, store := newTestGRPCServer(t) + + // Create a task directly in the store. + task := &state.Task{ + ID: "task-grpc-1", + ContextID: "ctx-1", + ProjectID: "test-project", + AgentSlug: "test-agent", + State: TaskStateWorking, + Metadata: "{}", + } + if err := store.CreateTask(task); err != nil { + t.Fatalf("create task: %v", err) + } + + resp, err := client.GetTask(context.Background(), &pb.GetTaskRequest{Id: "task-grpc-1"}) + if err != nil { + t.Fatalf("GetTask: %v", err) + } + if resp.Id != "task-grpc-1" { + t.Errorf("id = %q, want %q", resp.Id, "task-grpc-1") + } + if resp.Status.State != pb.TaskState_TASK_STATE_WORKING { + t.Errorf("state = %v, want WORKING", resp.Status.State) + } +} + +func TestGRPCListTasks_Empty(t *testing.T) { + client, _ := newTestGRPCServer(t) + + resp, err := client.ListTasks(context.Background(), &pb.ListTasksRequest{ContextId: "no-such-context"}) + if err != nil { + t.Fatalf("ListTasks: %v", err) + } + if len(resp.Tasks) != 0 { + t.Errorf("expected 0 tasks, got %d", len(resp.Tasks)) + } +} + +func TestGRPCListTasks_MissingContextID(t *testing.T) { + client, _ := newTestGRPCServer(t) + + _, err := client.ListTasks(context.Background(), &pb.ListTasksRequest{}) + if err == nil { + t.Fatal("expected error for missing context_id") + } + st, _ := grpcstatus.FromError(err) + if st.Code() != codes.InvalidArgument { + t.Errorf("code = %v, want InvalidArgument", st.Code()) + } +} + +func TestGRPCCancelTask_NotFound(t *testing.T) { + client, _ := newTestGRPCServer(t) + + _, err := client.CancelTask(context.Background(), &pb.CancelTaskRequest{Id: "nonexistent"}) + if err == nil { + t.Fatal("expected error for nonexistent task") + } + st, _ := grpcstatus.FromError(err) + if st.Code() != codes.NotFound { + t.Errorf("code = %v, want NotFound", st.Code()) + } +} + +func TestGRPCSendMessage_MissingParts(t *testing.T) { + client, _ := newTestGRPCServer(t) + + _, err := client.SendMessage(context.Background(), &pb.SendMessageRequest{ + Tenant: "test-project/test-agent", + Message: &pb.Message{}, + }) + if err == nil { + t.Fatal("expected error for empty parts") + } + st, _ := grpcstatus.FromError(err) + if st.Code() != codes.InvalidArgument { + t.Errorf("code = %v, want InvalidArgument", st.Code()) + } +} + +func TestGRPCSendMessage_MissingTenant(t *testing.T) { + client, _ := newTestGRPCServer(t) + + _, err := client.SendMessage(context.Background(), &pb.SendMessageRequest{ + Message: &pb.Message{ + MessageId: "m1", + Role: pb.Role_ROLE_USER, + Parts: []*pb.Part{{Content: &pb.Part_Text{Text: "hello"}}}, + }, + }) + if err == nil { + t.Fatal("expected error for missing tenant") + } +} + +func TestGRPCDeletePushNotification_MissingTaskID(t *testing.T) { + client, _ := newTestGRPCServer(t) + + _, err := client.DeleteTaskPushNotificationConfig(context.Background(), &pb.DeleteTaskPushNotificationConfigRequest{}) + if err == nil { + t.Fatal("expected error for missing task_id") + } + st, _ := grpcstatus.FromError(err) + if st.Code() != codes.InvalidArgument { + t.Errorf("code = %v, want InvalidArgument", st.Code()) + } +} + +func TestGRPCDeletePushNotification_MissingID(t *testing.T) { + client, _ := newTestGRPCServer(t) + + _, err := client.DeleteTaskPushNotificationConfig(context.Background(), &pb.DeleteTaskPushNotificationConfigRequest{ + TaskId: "some-task", + }) + if err == nil { + t.Fatal("expected error for missing id") + } + st, _ := grpcstatus.FromError(err) + if st.Code() != codes.InvalidArgument { + t.Errorf("code = %v, want InvalidArgument", st.Code()) + } +} + +func TestGRPCGetPushNotification_MissingID(t *testing.T) { + client, _ := newTestGRPCServer(t) + + _, err := client.GetTaskPushNotificationConfig(context.Background(), &pb.GetTaskPushNotificationConfigRequest{ + TaskId: "some-task", + }) + if err == nil { + t.Fatal("expected error for missing id") + } + st, _ := grpcstatus.FromError(err) + if st.Code() != codes.InvalidArgument { + t.Errorf("code = %v, want InvalidArgument", st.Code()) + } +} + +func TestGRPCGetPushNotification_MissingTaskID(t *testing.T) { + client, _ := newTestGRPCServer(t) + + _, err := client.GetTaskPushNotificationConfig(context.Background(), &pb.GetTaskPushNotificationConfigRequest{}) + if err == nil { + t.Fatal("expected error for missing task_id") + } + st, _ := grpcstatus.FromError(err) + if st.Code() != codes.InvalidArgument { + t.Errorf("code = %v, want InvalidArgument", st.Code()) + } +} + +func TestGRPCListPushNotificationConfigs(t *testing.T) { + client, _ := newTestGRPCServer(t) + + resp, err := client.ListTaskPushNotificationConfigs(context.Background(), &pb.ListTaskPushNotificationConfigsRequest{ + TaskId: "some-task", + }) + if err != nil { + t.Fatalf("ListTaskPushNotificationConfigs: %v", err) + } + if len(resp.Configs) != 0 { + t.Errorf("expected 0 configs, got %d", len(resp.Configs)) + } +} + +func TestGRPCCancelTask_AlreadyTerminal(t *testing.T) { + client, store := newTestGRPCServer(t) + + // Create a task that's already completed. + task := &state.Task{ + ID: "task-terminal-1", + ContextID: "ctx-1", + ProjectID: "test-project", + AgentSlug: "test-agent", + State: TaskStateCompleted, + Metadata: "{}", + } + if err := store.CreateTask(task); err != nil { + t.Fatalf("create task: %v", err) + } + + _, err := client.CancelTask(context.Background(), &pb.CancelTaskRequest{Id: "task-terminal-1"}) + if err == nil { + t.Fatal("expected error for terminal task") + } + st, ok := grpcstatus.FromError(err) + if !ok { + t.Fatalf("expected gRPC status, got %v", err) + } + if st.Code() != codes.FailedPrecondition { + t.Errorf("code = %v, want FailedPrecondition", st.Code()) + } + // Verify error message does not leak internal state. + if strings.Contains(st.Message(), "completed") || strings.Contains(st.Message(), "task-terminal-1") { + t.Errorf("error message leaks internal details: %q", st.Message()) + } +} + +func TestGRPCCreatePushNotificationConfig_InvalidURL(t *testing.T) { + client, _ := newTestGRPCServer(t) + + tests := []struct { + name string + url string + }{ + {"ftp scheme", "ftp://example.com/hook"}, + {"no host", "https:///path"}, + {"empty scheme", "://example.com/hook"}, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + _, err := client.CreateTaskPushNotificationConfig(context.Background(), &pb.TaskPushNotificationConfig{ + TaskId: "some-task", + Url: tc.url, + }) + if err == nil { + t.Fatal("expected error for invalid URL") + } + st, _ := grpcstatus.FromError(err) + if st.Code() != codes.InvalidArgument { + t.Errorf("code = %v, want InvalidArgument", st.Code()) + } + }) + } +} + +func TestGRPCAgentCardMapToProto(t *testing.T) { + card := map[string]interface{}{ + "name": "Test Agent", + "description": "A test agent", + "url": "https://example.com/agent", + "version": "1.0.0", + "skills": []map[string]interface{}{ + {"id": "skill-1", "name": "Skill One", "description": "Does stuff"}, + }, + "provider": map[string]string{ + "organization": "Acme Corp", + "url": "https://acme.example.com", + }, + "defaultInputModes": []string{"text/plain"}, + "defaultOutputModes": []string{"text/plain", "application/json"}, + } + + pbCard := agentCardMapToProto(card) + + if pbCard.Name != "Test Agent" { + t.Errorf("name = %q, want %q", pbCard.Name, "Test Agent") + } + if pbCard.Description != "A test agent" { + t.Errorf("description = %q, want %q", pbCard.Description, "A test agent") + } + if pbCard.Version != "1.0.0" { + t.Errorf("version = %q, want %q", pbCard.Version, "1.0.0") + } + if len(pbCard.SupportedInterfaces) != 1 { + t.Fatalf("interfaces count = %d, want 1", len(pbCard.SupportedInterfaces)) + } + if pbCard.SupportedInterfaces[0].Url != "https://example.com/agent" { + t.Errorf("interface url = %q", pbCard.SupportedInterfaces[0].Url) + } + if len(pbCard.Skills) != 1 { + t.Fatalf("skills count = %d, want 1", len(pbCard.Skills)) + } + if pbCard.Skills[0].Id != "skill-1" { + t.Errorf("skill id = %q, want %q", pbCard.Skills[0].Id, "skill-1") + } + if pbCard.Provider == nil { + t.Fatal("provider is nil") + } + if pbCard.Provider.Organization != "Acme Corp" { + t.Errorf("provider org = %q, want %q", pbCard.Provider.Organization, "Acme Corp") + } +} diff --git a/extras/scion-a2a-bridge/proto/a2a.proto b/extras/scion-a2a-bridge/proto/a2a.proto new file mode 100644 index 000000000..4e39e6e53 --- /dev/null +++ b/extras/scion-a2a-bridge/proto/a2a.proto @@ -0,0 +1,811 @@ +// Older protoc compilers don't understand edition yet. +syntax = "proto3"; +package lf.a2a.v1; + +import "google/api/annotations.proto"; +import "google/api/client.proto"; +import "google/api/field_behavior.proto"; +import "google/protobuf/empty.proto"; +import "google/protobuf/struct.proto"; +import "google/protobuf/timestamp.proto"; + +option csharp_namespace = "Lf.A2a.V1"; +option go_package = "github.com/GoogleCloudPlatform/scion/extras/scion-a2a-bridge/internal/a2apb"; +option java_multiple_files = true; +option java_outer_classname = "A2A"; +option java_package = "com.google.lf.a2a.v1"; + +// Provides operations for interacting with agents using the A2A protocol. +service A2AService { + // Sends a message to an agent. + rpc SendMessage(SendMessageRequest) returns (SendMessageResponse) { + option (google.api.http) = { + post: "/message:send" + body: "*" + additional_bindings: { + post: "/{tenant}/message:send" + body: "*" + } + }; + } + // Sends a streaming message to an agent, allowing for real-time interaction and status updates. + // Streaming version of `SendMessage` + rpc SendStreamingMessage(SendMessageRequest) returns (stream StreamResponse) { + option (google.api.http) = { + post: "/message:stream" + body: "*" + additional_bindings: { + post: "/{tenant}/message:stream" + body: "*" + } + }; + } + + // Gets the latest state of a task. + rpc GetTask(GetTaskRequest) returns (Task) { + option (google.api.http) = { + get: "/tasks/{id=*}" + additional_bindings: { + get: "/{tenant}/tasks/{id=*}" + } + }; + option (google.api.method_signature) = "id"; + } + // Lists tasks that match the specified filter. + rpc ListTasks(ListTasksRequest) returns (ListTasksResponse) { + option (google.api.http) = { + get: "/tasks" + additional_bindings: { + get: "/{tenant}/tasks" + } + }; + } + // Cancels a task in progress. + rpc CancelTask(CancelTaskRequest) returns (Task) { + option (google.api.http) = { + post: "/tasks/{id=*}:cancel" + body: "*" + additional_bindings: { + post: "/{tenant}/tasks/{id=*}:cancel" + body: "*" + } + }; + } + // Subscribes to task updates for tasks not in a terminal state. + // Returns `UnsupportedOperationError` if the task is already in a terminal state (completed, failed, canceled, rejected). + rpc SubscribeToTask(SubscribeToTaskRequest) returns (stream StreamResponse) { + option (google.api.http) = { + get: "/tasks/{id=*}:subscribe" + additional_bindings: { + get: "/{tenant}/tasks/{id=*}:subscribe" + } + }; + } + + // (-- api-linter: client-libraries::4232::required-fields=disabled + // api-linter: core::0133::method-signature=disabled + // api-linter: core::0133::request-message-name=disabled + // aip.dev/not-precedent: method_signature preserved for backwards compatibility --) + // Creates a push notification config for a task. + rpc CreateTaskPushNotificationConfig(TaskPushNotificationConfig) returns (TaskPushNotificationConfig) { + option (google.api.http) = { + post: "/tasks/{task_id=*}/pushNotificationConfigs" + body: "*" + additional_bindings: { + post: "/{tenant}/tasks/{task_id=*}/pushNotificationConfigs" + body: "*" + } + }; + option (google.api.method_signature) = "task_id,config"; + } + // Gets a push notification config for a task. + rpc GetTaskPushNotificationConfig(GetTaskPushNotificationConfigRequest) returns (TaskPushNotificationConfig) { + option (google.api.http) = { + get: "/tasks/{task_id=*}/pushNotificationConfigs/{id=*}" + additional_bindings: { + get: "/{tenant}/tasks/{task_id=*}/pushNotificationConfigs/{id=*}" + } + }; + option (google.api.method_signature) = "task_id,id"; + } + // Get a list of push notifications configured for a task. + rpc ListTaskPushNotificationConfigs(ListTaskPushNotificationConfigsRequest) returns (ListTaskPushNotificationConfigsResponse) { + option (google.api.http) = { + get: "/tasks/{task_id=*}/pushNotificationConfigs" + additional_bindings: { + get: "/{tenant}/tasks/{task_id=*}/pushNotificationConfigs" + } + }; + option (google.api.method_signature) = "task_id"; + } + // Gets the extended agent card for the authenticated agent. + rpc GetExtendedAgentCard(GetExtendedAgentCardRequest) returns (AgentCard) { + option (google.api.http) = { + get: "/extendedAgentCard" + additional_bindings: { + get: "/{tenant}/extendedAgentCard" + } + }; + } + // Deletes a push notification config for a task. + rpc DeleteTaskPushNotificationConfig(DeleteTaskPushNotificationConfigRequest) returns (google.protobuf.Empty) { + option (google.api.http) = { + delete: "/tasks/{task_id=*}/pushNotificationConfigs/{id=*}" + additional_bindings: { + delete: "/{tenant}/tasks/{task_id=*}/pushNotificationConfigs/{id=*}" + } + }; + option (google.api.method_signature) = "task_id,id"; + } +} + +// Configuration of a send message request. +message SendMessageConfiguration { + // A list of media types the client is prepared to accept for response parts. + // Agents SHOULD use this to tailor their output. + repeated string accepted_output_modes = 1; + // Configuration for the agent to send push notifications for task updates. + // Task id should be empty when sending this configuration in a `SendMessage` request. + TaskPushNotificationConfig task_push_notification_config = 2; + // The maximum number of most recent messages from the task's history to retrieve in + // the response. An unset value means the client does not impose any limit. A + // value of zero is a request to not include any messages. The server MUST NOT + // return more messages than the provided value, but MAY apply a lower limit. + optional int32 history_length = 3; + // If `true`, the operation returns immediately after creating the task, + // even if processing is still in progress. + // If `false` (default), the operation MUST wait until the task reaches a + // terminal (`COMPLETED`, `FAILED`, `CANCELED`, `REJECTED`) or interrupted + // (`INPUT_REQUIRED`, `AUTH_REQUIRED`) state before returning. + bool return_immediately = 4; +} + +// `Task` is the core unit of action for A2A. It has a current status +// and when results are created for the task they are stored in the +// artifact. If there are multiple turns for a task, these are stored in +// history. +message Task { + // Unique identifier (e.g. UUID) for the task, generated by the server for a + // new task. + string id = 1 [(google.api.field_behavior) = REQUIRED]; + // Unique identifier (e.g. UUID) for the contextual collection of interactions + // (tasks and messages). + string context_id = 2; + // The current status of a `Task`, including `state` and a `message`. + TaskStatus status = 3 [(google.api.field_behavior) = REQUIRED]; + // A set of output artifacts for a `Task`. + repeated Artifact artifacts = 4; + // protolint:disable REPEATED_FIELD_NAMES_PLURALIZED + // The history of interactions from a `Task`. + repeated Message history = 5; + // protolint:enable REPEATED_FIELD_NAMES_PLURALIZED + // A key/value object to store custom metadata about a task. + google.protobuf.Struct metadata = 6; +} + +// Defines the possible lifecycle states of a `Task`. +enum TaskState { + // The task is in an unknown or indeterminate state. + TASK_STATE_UNSPECIFIED = 0; + // Indicates that a task has been successfully submitted and acknowledged. + TASK_STATE_SUBMITTED = 1; + // Indicates that a task is actively being processed by the agent. + TASK_STATE_WORKING = 2; + // Indicates that a task has finished successfully. This is a terminal state. + TASK_STATE_COMPLETED = 3; + // Indicates that a task has finished with an error. This is a terminal state. + TASK_STATE_FAILED = 4; + // Indicates that a task was canceled before completion. This is a terminal state. + TASK_STATE_CANCELED = 5; + // Indicates that the agent requires additional user input to proceed. This is an interrupted state. + TASK_STATE_INPUT_REQUIRED = 6; + // Indicates that the agent has decided to not perform the task. + // This may be done during initial task creation or later once an agent + // has determined it can't or won't proceed. This is a terminal state. + TASK_STATE_REJECTED = 7; + // Indicates that authentication is required to proceed. This is an interrupted state. + TASK_STATE_AUTH_REQUIRED = 8; +} + +// A container for the status of a task +message TaskStatus { + // The current state of this task. + TaskState state = 1 [(google.api.field_behavior) = REQUIRED]; + // A message associated with the status. + Message message = 2; + // ISO 8601 Timestamp when the status was recorded. + // Example: "2023-10-27T10:00:00Z" + google.protobuf.Timestamp timestamp = 3; +} + +// `Part` represents a container for a section of communication content. +// Parts can be purely textual, some sort of file (image, video, etc) or +// a structured data blob (i.e. JSON). +message Part { + oneof content { + // The string content of the `text` part. + string text = 1; + // The `raw` byte content of a file. In JSON serialization, this is encoded as a base64 string. + bytes raw = 2; + // A `url` pointing to the file's content. + string url = 3; + // Arbitrary structured `data` as a JSON value (object, array, string, number, boolean, or null). + google.protobuf.Value data = 4; + } + // Optional. metadata associated with this part. + google.protobuf.Struct metadata = 5; + // An optional `filename` for the file (e.g., "document.pdf"). + string filename = 6; + // The `media_type` (MIME type) of the part content (e.g., "text/plain", "application/json", "image/png"). + // This field is available for all part types. + string media_type = 7; +} + +// Defines the sender of a message in A2A protocol communication. +enum Role { + // The role is unspecified. + ROLE_UNSPECIFIED = 0; + // The message is from the client to the server. + ROLE_USER = 1; + // The message is from the server to the client. + ROLE_AGENT = 2; +} + +// `Message` is one unit of communication between client and server. It can be +// associated with a context and/or a task. For server messages, `context_id` must +// be provided, and `task_id` only if a task was created. For client messages, both +// fields are optional, with the caveat that if both are provided, they have to +// match (the `context_id` has to be the one that is set on the task). If only +// `task_id` is provided, the server will infer `context_id` from it. +message Message { + // The unique identifier (e.g. UUID) of the message. This is created by the message creator. + string message_id = 1 [(google.api.field_behavior) = REQUIRED]; + // Optional. The context id of the message. If set, the message will be associated with the given context. + string context_id = 2; + // Optional. The task id of the message. If set, the message will be associated with the given task. + string task_id = 3; + // Identifies the sender of the message. + Role role = 4 [(google.api.field_behavior) = REQUIRED]; + // Parts is the container of the message content. + repeated Part parts = 5 [(google.api.field_behavior) = REQUIRED]; + // Optional. Any metadata to provide along with the message. + google.protobuf.Struct metadata = 6; + // The URIs of extensions that are present or contributed to this Message. + repeated string extensions = 7; + // A list of task IDs that this message references for additional context. + repeated string reference_task_ids = 8; +} + +// Artifacts represent task outputs. +message Artifact { + // Unique identifier (e.g. UUID) for the artifact. It must be unique within a task. + string artifact_id = 1 [(google.api.field_behavior) = REQUIRED]; + // A human readable name for the artifact. + string name = 2; + // Optional. A human readable description of the artifact. + string description = 3; + // The content of the artifact. Must contain at least one part. + repeated Part parts = 4 [(google.api.field_behavior) = REQUIRED]; + // Optional. Metadata included with the artifact. + google.protobuf.Struct metadata = 5; + // The URIs of extensions that are present or contributed to this Artifact. + repeated string extensions = 6; +} + +// An event sent by the agent to notify the client of a change in a task's status. +message TaskStatusUpdateEvent { + // The ID of the task that has changed. + string task_id = 1 [(google.api.field_behavior) = REQUIRED]; + // The ID of the context that the task belongs to. + string context_id = 2 [(google.api.field_behavior) = REQUIRED]; + // The new status of the task. + TaskStatus status = 3 [(google.api.field_behavior) = REQUIRED]; + // Optional. Metadata associated with the task update. + google.protobuf.Struct metadata = 4; +} + +// A task delta where an artifact has been generated. +message TaskArtifactUpdateEvent { + // The ID of the task for this artifact. + string task_id = 1 [(google.api.field_behavior) = REQUIRED]; + // The ID of the context that this task belongs to. + string context_id = 2 [(google.api.field_behavior) = REQUIRED]; + // The artifact that was generated or updated. + Artifact artifact = 3 [(google.api.field_behavior) = REQUIRED]; + // If true, the content of this artifact should be appended to a previously + // sent artifact with the same ID. + bool append = 4; + // If true, this is the final chunk of the artifact. + bool last_chunk = 5; + // Optional. Metadata associated with the artifact update. + google.protobuf.Struct metadata = 6; +} + +// Defines authentication details, used for push notifications. +message AuthenticationInfo { + // HTTP Authentication Scheme from the [IANA registry](https://www.iana.org/assignments/http-authschemes/). + // Examples: `Bearer`, `Basic`, `Digest`. + // Scheme names are case-insensitive per [RFC 9110 Section 11.1](https://www.rfc-editor.org/rfc/rfc9110#section-11.1). + string scheme = 1 [(google.api.field_behavior) = REQUIRED]; + // Push Notification credentials. Format depends on the scheme (e.g., token for Bearer). + string credentials = 2; +} + +// Declares a combination of a target URL, transport and protocol version for interacting with the agent. +// This allows agents to expose the same functionality over multiple protocol binding mechanisms. +message AgentInterface { + // The URL where this interface is available. Must be a valid absolute HTTPS URL in production. + // Example: "https://api.example.com/a2a/v1", "https://grpc.example.com/a2a" + string url = 1 [(google.api.field_behavior) = REQUIRED]; + // The protocol binding supported at this URL. This is an open form string, to be + // easily extended for other protocol bindings. The core ones officially + // supported are `JSONRPC`, `GRPC` and `HTTP+JSON`. + string protocol_binding = 2 [(google.api.field_behavior) = REQUIRED]; + // Optional. An opaque string used for routing requests to a specific agent + // or tenant when multiple agents are served behind a single A2A endpoint. + // When set, clients MUST include this value in the `tenant` field of all + // request messages sent to this interface. The server is responsible for + // interpreting the value and routing requests accordingly; the protocol + // does not define its format or semantics. + string tenant = 3; + // The version of the A2A protocol this interface exposes. + // Use the latest supported minor version per major version. + // Examples: "0.3", "1.0" + string protocol_version = 4 [(google.api.field_behavior) = REQUIRED]; +} + +// A self-describing manifest for an agent. It provides essential +// metadata including the agent's identity, capabilities, skills, supported +// communication methods, and security requirements. +// Next ID: 20 +message AgentCard { + // A human readable name for the agent. + // Example: "Recipe Agent" + string name = 1 [(google.api.field_behavior) = REQUIRED]; + // A human-readable description of the agent, assisting users and other agents + // in understanding its purpose. + // Example: "Agent that helps users with recipes and cooking." + string description = 2 [(google.api.field_behavior) = REQUIRED]; + // Ordered list of supported interfaces. The first entry is preferred. + repeated AgentInterface supported_interfaces = 3 [(google.api.field_behavior) = REQUIRED]; + // The service provider of the agent. + AgentProvider provider = 4; + // The version of the agent. + // Example: "1.0.0" + string version = 5 [(google.api.field_behavior) = REQUIRED]; + // A URL providing additional documentation about the agent. + optional string documentation_url = 6; + // A2A Capability set supported by the agent. + AgentCapabilities capabilities = 7 [(google.api.field_behavior) = REQUIRED]; + // The security scheme details used for authenticating with this agent. + map security_schemes = 8; + // Security requirements for contacting the agent. + repeated SecurityRequirement security_requirements = 9; + // protolint:enable REPEATED_FIELD_NAMES_PLURALIZED + // The set of interaction modes that the agent supports across all skills. + // This can be overridden per skill. Defined as media types. + repeated string default_input_modes = 10 [(google.api.field_behavior) = REQUIRED]; + // The media types supported as outputs from this agent. + repeated string default_output_modes = 11 [(google.api.field_behavior) = REQUIRED]; + // Skills represent the abilities of an agent. + // It is largely a descriptive concept but represents a more focused set of behaviors that the + // agent is likely to succeed at. + repeated AgentSkill skills = 12 [(google.api.field_behavior) = REQUIRED]; + // JSON Web Signatures computed for this `AgentCard`. + repeated AgentCardSignature signatures = 13; + // Optional. A URL to an icon for the agent. + optional string icon_url = 14; +} + +// Represents the service provider of an agent. +message AgentProvider { + // A URL for the agent provider's website or relevant documentation. + // Example: "https://ai.google.dev" + string url = 1 [(google.api.field_behavior) = REQUIRED]; + // The name of the agent provider's organization. + // Example: "Google" + string organization = 2 [(google.api.field_behavior) = REQUIRED]; +} + +// Defines optional capabilities supported by an agent. +message AgentCapabilities { + // Indicates if the agent supports streaming responses. + optional bool streaming = 1; + // Indicates if the agent supports sending push notifications for asynchronous task updates. + optional bool push_notifications = 2; + // A list of protocol extensions supported by the agent. + repeated AgentExtension extensions = 3; + // Indicates if the agent supports providing an extended agent card when authenticated. + optional bool extended_agent_card = 4; +} + +// A declaration of a protocol extension supported by an Agent. +message AgentExtension { + // The unique URI identifying the extension. + string uri = 1; + // A human-readable description of how this agent uses the extension. + string description = 2; + // If true, the client must understand and comply with the extension's requirements. + bool required = 3; + // Optional. Extension-specific configuration parameters. + google.protobuf.Struct params = 4; +} + +// Represents a distinct capability or function that an agent can perform. +message AgentSkill { + // A unique identifier for the agent's skill. + string id = 1 [(google.api.field_behavior) = REQUIRED]; + // A human-readable name for the skill. + string name = 2 [(google.api.field_behavior) = REQUIRED]; + // A detailed description of the skill. + string description = 3 [(google.api.field_behavior) = REQUIRED]; + // A set of keywords describing the skill's capabilities. + repeated string tags = 4 [(google.api.field_behavior) = REQUIRED]; + // Example prompts or scenarios that this skill can handle. + repeated string examples = 5; + // The set of supported input media types for this skill, overriding the agent's defaults. + repeated string input_modes = 6; + // The set of supported output media types for this skill, overriding the agent's defaults. + repeated string output_modes = 7; + // Security schemes necessary for this skill. + repeated SecurityRequirement security_requirements = 8; +} + +// AgentCardSignature represents a JWS signature of an AgentCard. +// This follows the JSON format of an RFC 7515 JSON Web Signature (JWS). +message AgentCardSignature { + // (-- api-linter: core::0140::reserved-words=disabled + // aip.dev/not-precedent: Backwards compatibility --) + // Required. The protected JWS header for the signature. This is always a + // base64url-encoded JSON object. + string protected = 1 [(google.api.field_behavior) = REQUIRED]; + // Required. The computed signature, base64url-encoded. + string signature = 2 [(google.api.field_behavior) = REQUIRED]; + // The unprotected JWS header values. + google.protobuf.Struct header = 3; +} + +// A container associating a push notification configuration with a specific task. +message TaskPushNotificationConfig { + // Optional. Opaque routing identifier. Must match the `tenant` value from + // the selected `AgentInterface` in the Agent Card when that field is set. + string tenant = 1; + // The push notification configuration details. + // A unique identifier (e.g. UUID) for this push notification configuration. + string id = 2; + // The ID of the task this configuration is associated with. + string task_id = 3; + // The URL where the notification should be sent. + string url = 4 [(google.api.field_behavior) = REQUIRED]; + // A token unique for this task or session. + string token = 5; + // Authentication information required to send the notification. + AuthenticationInfo authentication = 6; +} + +// protolint:disable REPEATED_FIELD_NAMES_PLURALIZED +// A list of strings. +message StringList { + // The individual string values. + repeated string list = 1; +} +// protolint:enable REPEATED_FIELD_NAMES_PLURALIZED + +// Defines the security requirements for an agent. +message SecurityRequirement { + // A map of security schemes to the required scopes. + map schemes = 1; +} + +// Defines a security scheme that can be used to secure an agent's endpoints. +// This is a discriminated union type based on the OpenAPI 3.2 Security Scheme Object. +// See: https://spec.openapis.org/oas/v3.2.0.html#security-scheme-object +message SecurityScheme { + oneof scheme { + // API key-based authentication. + APIKeySecurityScheme api_key_security_scheme = 1; + // HTTP authentication (Basic, Bearer, etc.). + HTTPAuthSecurityScheme http_auth_security_scheme = 2; + // OAuth 2.0 authentication. + OAuth2SecurityScheme oauth2_security_scheme = 3; + // OpenID Connect authentication. + OpenIdConnectSecurityScheme open_id_connect_security_scheme = 4; + // Mutual TLS authentication. + MutualTlsSecurityScheme mtls_security_scheme = 5; + } +} + +// Defines a security scheme using an API key. +message APIKeySecurityScheme { + // An optional description for the security scheme. + string description = 1; + // The location of the API key. Valid values are "query", "header", or "cookie". + string location = 2 [(google.api.field_behavior) = REQUIRED]; + // The name of the header, query, or cookie parameter to be used. + string name = 3 [(google.api.field_behavior) = REQUIRED]; +} + +// Defines a security scheme using HTTP authentication. +message HTTPAuthSecurityScheme { + // An optional description for the security scheme. + string description = 1; + // The name of the HTTP Authentication scheme to be used in the Authorization header, + // as defined in RFC7235 (e.g., "Bearer"). + // This value should be registered in the IANA Authentication Scheme registry. + string scheme = 2 [(google.api.field_behavior) = REQUIRED]; + // A hint to the client to identify how the bearer token is formatted (e.g., "JWT"). + // Primarily for documentation purposes. + string bearer_format = 3; +} + +// Defines a security scheme using OAuth 2.0. +message OAuth2SecurityScheme { + // An optional description for the security scheme. + string description = 1; + // An object containing configuration information for the supported OAuth 2.0 flows. + OAuthFlows flows = 2 [(google.api.field_behavior) = REQUIRED]; + // URL to the OAuth2 authorization server metadata [RFC 8414](https://datatracker.ietf.org/doc/html/rfc8414). + // TLS is required. + string oauth2_metadata_url = 3; +} + +// Defines a security scheme using OpenID Connect. +message OpenIdConnectSecurityScheme { + // An optional description for the security scheme. + string description = 1; + // The [OpenID Connect Discovery URL](https://openid.net/specs/openid-connect-discovery-1_0.html) for the OIDC provider's metadata. + string open_id_connect_url = 2 [(google.api.field_behavior) = REQUIRED]; +} + +// Defines a security scheme using mTLS authentication. +message MutualTlsSecurityScheme { + // An optional description for the security scheme. + string description = 1; +} + +// Defines the configuration for the supported OAuth 2.0 flows. +message OAuthFlows { + oneof flow { + // Configuration for the OAuth Authorization Code flow. + AuthorizationCodeOAuthFlow authorization_code = 1; + // Configuration for the OAuth Client Credentials flow. + ClientCredentialsOAuthFlow client_credentials = 2; + // Deprecated: Use Authorization Code + PKCE instead. + ImplicitOAuthFlow implicit = 3 [deprecated = true]; + // Deprecated: Use Authorization Code + PKCE or Device Code. + PasswordOAuthFlow password = 4 [deprecated = true]; + // Configuration for the OAuth Device Code flow. + DeviceCodeOAuthFlow device_code = 5; + } +} + +// Defines configuration details for the OAuth 2.0 Authorization Code flow. +message AuthorizationCodeOAuthFlow { + // The authorization URL to be used for this flow. + string authorization_url = 1 [(google.api.field_behavior) = REQUIRED]; + // The token URL to be used for this flow. + string token_url = 2 [(google.api.field_behavior) = REQUIRED]; + // The URL to be used for obtaining refresh tokens. + string refresh_url = 3; + // The available scopes for the OAuth2 security scheme. + map scopes = 4 [(google.api.field_behavior) = REQUIRED]; + // Indicates if PKCE (RFC 7636) is required for this flow. + // PKCE should always be used for public clients and is recommended for all clients. + bool pkce_required = 5; +} + +// Defines configuration details for the OAuth 2.0 Client Credentials flow. +message ClientCredentialsOAuthFlow { + // The token URL to be used for this flow. + string token_url = 1 [(google.api.field_behavior) = REQUIRED]; + // The URL to be used for obtaining refresh tokens. + string refresh_url = 2; + // The available scopes for the OAuth2 security scheme. + map scopes = 3 [(google.api.field_behavior) = REQUIRED]; +} + +// Deprecated: Use Authorization Code + PKCE instead. +message ImplicitOAuthFlow { + // The authorization URL to be used for this flow. This MUST be in the + // form of a URL. The OAuth2 standard requires the use of TLS + string authorization_url = 1; + // The URL to be used for obtaining refresh tokens. This MUST be in the + // form of a URL. The OAuth2 standard requires the use of TLS. + string refresh_url = 2; + // The available scopes for the OAuth2 security scheme. A map between the + // scope name and a short description for it. The map MAY be empty. + map scopes = 3; +} + +// Deprecated: Use Authorization Code + PKCE or Device Code. +message PasswordOAuthFlow { + // The token URL to be used for this flow. This MUST be in the form of a URL. + // The OAuth2 standard requires the use of TLS. + string token_url = 1; + // The URL to be used for obtaining refresh tokens. This MUST be in the + // form of a URL. The OAuth2 standard requires the use of TLS. + string refresh_url = 2; + // The available scopes for the OAuth2 security scheme. A map between the + // scope name and a short description for it. The map MAY be empty. + map scopes = 3; +} + +// Defines configuration details for the OAuth 2.0 Device Code flow (RFC 8628). +// This flow is designed for input-constrained devices such as IoT devices, +// and CLI tools where the user authenticates on a separate device. +message DeviceCodeOAuthFlow { + // The device authorization endpoint URL. + string device_authorization_url = 1 [(google.api.field_behavior) = REQUIRED]; + // The token URL to be used for this flow. + string token_url = 2 [(google.api.field_behavior) = REQUIRED]; + // The URL to be used for obtaining refresh tokens. + string refresh_url = 3; + // The available scopes for the OAuth2 security scheme. + map scopes = 4 [(google.api.field_behavior) = REQUIRED]; +} + +// Represents a request for the `SendMessage` method. +message SendMessageRequest { + // Optional. Opaque routing identifier. Must match the `tenant` value from + // the selected `AgentInterface` in the Agent Card when that field is set. + string tenant = 1; + // The message to send to the agent. + Message message = 2 [(google.api.field_behavior) = REQUIRED]; + // Configuration for the send request. + SendMessageConfiguration configuration = 3; + // A flexible key-value map for passing additional context or parameters. + google.protobuf.Struct metadata = 4; +} + +// Represents a request for the `GetTask` method. +message GetTaskRequest { + // Optional. Opaque routing identifier. Must match the `tenant` value from + // the selected `AgentInterface` in the Agent Card when that field is set. + string tenant = 1; + // The resource ID of the task to retrieve. + string id = 2 [(google.api.field_behavior) = REQUIRED]; + // The maximum number of most recent messages from the task's history to retrieve. An + // unset value means the client does not impose any limit. A value of zero is + // a request to not include any messages. The server MUST NOT return more + // messages than the provided value, but MAY apply a lower limit. + optional int32 history_length = 3; +} + +// Parameters for listing tasks with optional filtering criteria. +message ListTasksRequest { + // Optional. Opaque routing identifier. Must match the `tenant` value from + // the selected `AgentInterface` in the Agent Card when that field is set. + string tenant = 1; + // Filter tasks by context ID to get tasks from a specific conversation or session. + string context_id = 2; + // Filter tasks by their current status state. + TaskState status = 3; + // The maximum number of tasks to return. The service may return fewer than this value. + // If unspecified, at most 50 tasks will be returned. + // The minimum value is 1. + // The maximum value is 100. + optional int32 page_size = 4; + // A page token, received from a previous `ListTasks` call. + // `ListTasksResponse.next_page_token`. + // Provide this to retrieve the subsequent page. + string page_token = 5; + // The maximum number of messages to include in each task's history. + optional int32 history_length = 6; + // Filter tasks which have a status updated after the provided timestamp in ISO 8601 format (e.g., "2023-10-27T10:00:00Z"). + // Only tasks with a status timestamp time greater than or equal to this value will be returned. + google.protobuf.Timestamp status_timestamp_after = 7; + // Whether to include artifacts in the returned tasks. + // Defaults to false to reduce payload size. + optional bool include_artifacts = 8; +} + +// Result object for `ListTasks` method containing an array of tasks and pagination information. +message ListTasksResponse { + // Array of tasks matching the specified criteria. + repeated Task tasks = 1 [(google.api.field_behavior) = REQUIRED]; + // A token to retrieve the next page of results, or empty if there are no more results in the list. + string next_page_token = 2 [(google.api.field_behavior) = REQUIRED]; + // The page size used for this response. + int32 page_size = 3 [(google.api.field_behavior) = REQUIRED]; + // Total number of tasks available (before pagination). + int32 total_size = 4 [(google.api.field_behavior) = REQUIRED]; +} + +// Represents a request for the `CancelTask` method. +message CancelTaskRequest { + // Optional. Opaque routing identifier. Must match the `tenant` value from + // the selected `AgentInterface` in the Agent Card when that field is set. + string tenant = 1; + // The resource ID of the task to cancel. + string id = 2 [(google.api.field_behavior) = REQUIRED]; + // A flexible key-value map for passing additional context or parameters. + google.protobuf.Struct metadata = 3; +} + +// Represents a request for the `GetTaskPushNotificationConfig` method. +message GetTaskPushNotificationConfigRequest { + // Optional. Opaque routing identifier. Must match the `tenant` value from + // the selected `AgentInterface` in the Agent Card when that field is set. + string tenant = 1; + // The parent task resource ID. + string task_id = 2 [(google.api.field_behavior) = REQUIRED]; + // The resource ID of the configuration to retrieve. + string id = 3 [(google.api.field_behavior) = REQUIRED]; +} + +// Represents a request for the `DeleteTaskPushNotificationConfig` method. +message DeleteTaskPushNotificationConfigRequest { + // Optional. Opaque routing identifier. Must match the `tenant` value from + // the selected `AgentInterface` in the Agent Card when that field is set. + string tenant = 1; + // The parent task resource ID. + string task_id = 2 [(google.api.field_behavior) = REQUIRED]; + // The resource ID of the configuration to delete. + string id = 3 [(google.api.field_behavior) = REQUIRED]; +} + +// Represents a request for the `SubscribeToTask` method. +message SubscribeToTaskRequest { + // Optional. Opaque routing identifier. Must match the `tenant` value from + // the selected `AgentInterface` in the Agent Card when that field is set. + string tenant = 1; + // The resource ID of the task to subscribe to. + string id = 2 [(google.api.field_behavior) = REQUIRED]; +} + +// Represents a request for the `ListTaskPushNotificationConfigs` method. +message ListTaskPushNotificationConfigsRequest { + // Optional. Opaque routing identifier. Must match the `tenant` value from + // the selected `AgentInterface` in the Agent Card when that field is set. + string tenant = 4; + // The parent task resource ID. + string task_id = 1 [(google.api.field_behavior) = REQUIRED]; + + // The maximum number of configurations to return. + int32 page_size = 2; + + // A page token received from a previous `ListTaskPushNotificationConfigsRequest` call. + string page_token = 3; +} + +// Represents a request for the `GetExtendedAgentCard` method. +message GetExtendedAgentCardRequest { + // Optional. Opaque routing identifier. Must match the `tenant` value from + // the selected `AgentInterface` in the Agent Card when that field is set. + string tenant = 1; +} + +// Represents the response for the `SendMessage` method. +message SendMessageResponse { + // The payload of the response. + oneof payload { + // The task created or updated by the message. + Task task = 1; + // A message from the agent. + Message message = 2; + } +} + +// A wrapper object used in streaming operations to encapsulate different types of response data. +message StreamResponse { + // The payload of the stream response. + oneof payload { + // A Task object containing the current state of the task. + Task task = 1; + // A Message object containing a message from the agent. + Message message = 2; + // An event indicating a task status update. + TaskStatusUpdateEvent status_update = 3; + // An event indicating a task artifact update. + TaskArtifactUpdateEvent artifact_update = 4; + } +} + +// Represents a successful response for the `ListTaskPushNotificationConfigs` +// method. +message ListTaskPushNotificationConfigsResponse { + // The list of push notification configurations. + repeated TaskPushNotificationConfig configs = 1; + // A token to retrieve the next page of results, or empty if there are no more results in the list. + string next_page_token = 2; +} diff --git a/extras/scion-a2a-bridge/proto/google/api/annotations.proto b/extras/scion-a2a-bridge/proto/google/api/annotations.proto new file mode 100644 index 000000000..417edd8fa --- /dev/null +++ b/extras/scion-a2a-bridge/proto/google/api/annotations.proto @@ -0,0 +1,31 @@ +// Copyright 2025 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +syntax = "proto3"; + +package google.api; + +import "google/api/http.proto"; +import "google/protobuf/descriptor.proto"; + +option go_package = "google.golang.org/genproto/googleapis/api/annotations;annotations"; +option java_multiple_files = true; +option java_outer_classname = "AnnotationsProto"; +option java_package = "com.google.api"; +option objc_class_prefix = "GAPI"; + +extend google.protobuf.MethodOptions { + // See `HttpRule`. + HttpRule http = 72295728; +} diff --git a/extras/scion-a2a-bridge/proto/google/api/client.proto b/extras/scion-a2a-bridge/proto/google/api/client.proto new file mode 100644 index 000000000..332e0d086 --- /dev/null +++ b/extras/scion-a2a-bridge/proto/google/api/client.proto @@ -0,0 +1,598 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +syntax = "proto3"; + +package google.api; + +import "google/api/launch_stage.proto"; +import "google/protobuf/descriptor.proto"; +import "google/protobuf/duration.proto"; + +option go_package = "google.golang.org/genproto/googleapis/api/annotations;annotations"; +option java_multiple_files = true; +option java_outer_classname = "ClientProto"; +option java_package = "com.google.api"; +option objc_class_prefix = "GAPI"; + +extend google.protobuf.MethodOptions { + // A definition of a client library method signature. + // + // In client libraries, each proto RPC corresponds to one or more methods + // which the end user is able to call, and calls the underlying RPC. + // Normally, this method receives a single argument (a struct or instance + // corresponding to the RPC request object). Defining this field will + // add one or more overloads providing flattened or simpler method signatures + // in some languages. + // + // The fields on the method signature are provided as a comma-separated + // string. + // + // For example, the proto RPC and annotation: + // + // rpc CreateSubscription(CreateSubscriptionRequest) + // returns (Subscription) { + // option (google.api.method_signature) = "name,topic"; + // } + // + // Would add the following Java overload (in addition to the method accepting + // the request object): + // + // public final Subscription createSubscription(String name, String topic) + // + // The following backwards-compatibility guidelines apply: + // + // * Adding this annotation to an unannotated method is backwards + // compatible. + // * Adding this annotation to a method which already has existing + // method signature annotations is backwards compatible if and only if + // the new method signature annotation is last in the sequence. + // * Modifying or removing an existing method signature annotation is + // a breaking change. + // * Re-ordering existing method signature annotations is a breaking + // change. + repeated string method_signature = 1051; +} + +extend google.protobuf.ServiceOptions { + // The hostname for this service. + // This should be specified with no prefix or protocol. + // + // Example: + // + // service Foo { + // option (google.api.default_host) = "foo.googleapi.com"; + // ... + // } + string default_host = 1049; + + // OAuth scopes needed for the client. + // + // Example: + // + // service Foo { + // option (google.api.oauth_scopes) = \ + // "https://www.googleapis.com/auth/cloud-platform"; + // ... + // } + // + // If there is more than one scope, use a comma-separated string: + // + // Example: + // + // service Foo { + // option (google.api.oauth_scopes) = \ + // "https://www.googleapis.com/auth/cloud-platform," + // "https://www.googleapis.com/auth/monitoring"; + // ... + // } + string oauth_scopes = 1050; + + // The API version of this service, which should be sent by version-aware + // clients to the service. This allows services to abide by the schema and + // behavior of the service at the time this API version was deployed. + // The format of the API version must be treated as opaque by clients. + // Services may use a format with an apparent structure, but clients must + // not rely on this to determine components within an API version, or attempt + // to construct other valid API versions. Note that this is for upcoming + // functionality and may not be implemented for all services. + // + // Example: + // + // service Foo { + // option (google.api.api_version) = "v1_20230821_preview"; + // } + string api_version = 525000001; +} + +// Required information for every language. +message CommonLanguageSettings { + // Link to automatically generated reference documentation. Example: + // https://cloud.google.com/nodejs/docs/reference/asset/latest + string reference_docs_uri = 1 [deprecated = true]; + + // The destination where API teams want this client library to be published. + repeated ClientLibraryDestination destinations = 2; + + // Configuration for which RPCs should be generated in the GAPIC client. + // + // Note: This field should not be used in most cases. + SelectiveGapicGeneration selective_gapic_generation = 3; +} + +// Details about how and where to publish client libraries. +message ClientLibrarySettings { + // Version of the API to apply these settings to. This is the full protobuf + // package for the API, ending in the version element. + // Examples: "google.cloud.speech.v1" and "google.spanner.admin.database.v1". + string version = 1; + + // Launch stage of this version of the API. + LaunchStage launch_stage = 2; + + // When using transport=rest, the client request will encode enums as + // numbers rather than strings. + bool rest_numeric_enums = 3; + + // Settings for legacy Java features, supported in the Service YAML. + JavaSettings java_settings = 21; + + // Settings for C++ client libraries. + CppSettings cpp_settings = 22; + + // Settings for PHP client libraries. + PhpSettings php_settings = 23; + + // Settings for Python client libraries. + PythonSettings python_settings = 24; + + // Settings for Node client libraries. + NodeSettings node_settings = 25; + + // Settings for .NET client libraries. + DotnetSettings dotnet_settings = 26; + + // Settings for Ruby client libraries. + RubySettings ruby_settings = 27; + + // Settings for Go client libraries. + GoSettings go_settings = 28; +} + +// This message configures the settings for publishing [Google Cloud Client +// libraries](https://cloud.google.com/apis/docs/cloud-client-libraries) +// generated from the service config. +message Publishing { + // A list of API method settings, e.g. the behavior for methods that use the + // long-running operation pattern. + repeated MethodSettings method_settings = 2; + + // Link to a *public* URI where users can report issues. Example: + // https://issuetracker.google.com/issues/new?component=190865&template=1161103 + string new_issue_uri = 101; + + // Link to product home page. Example: + // https://cloud.google.com/asset-inventory/docs/overview + string documentation_uri = 102; + + // Used as a tracking tag when collecting data about the APIs developer + // relations artifacts like docs, packages delivered to package managers, + // etc. Example: "speech". + string api_short_name = 103; + + // GitHub label to apply to issues and pull requests opened for this API. + string github_label = 104; + + // GitHub teams to be added to CODEOWNERS in the directory in GitHub + // containing source code for the client libraries for this API. + repeated string codeowner_github_teams = 105; + + // A prefix used in sample code when demarking regions to be included in + // documentation. + string doc_tag_prefix = 106; + + // For whom the client library is being published. + ClientLibraryOrganization organization = 107; + + // Client library settings. If the same version string appears multiple + // times in this list, then the last one wins. Settings from earlier + // settings with the same version string are discarded. + repeated ClientLibrarySettings library_settings = 109; + + // Optional link to proto reference documentation. Example: + // https://cloud.google.com/pubsub/lite/docs/reference/rpc + string proto_reference_documentation_uri = 110; + + // Optional link to REST reference documentation. Example: + // https://cloud.google.com/pubsub/lite/docs/reference/rest + string rest_reference_documentation_uri = 111; +} + +// Settings for Java client libraries. +message JavaSettings { + // The package name to use in Java. Clobbers the java_package option + // set in the protobuf. This should be used **only** by APIs + // who have already set the language_settings.java.package_name" field + // in gapic.yaml. API teams should use the protobuf java_package option + // where possible. + // + // Example of a YAML configuration:: + // + // publishing: + // library_settings: + // java_settings: + // library_package: com.google.cloud.pubsub.v1 + string library_package = 1; + + // Configure the Java class name to use instead of the service's for its + // corresponding generated GAPIC client. Keys are fully-qualified + // service names as they appear in the protobuf (including the full + // the language_settings.java.interface_names" field in gapic.yaml. API + // teams should otherwise use the service name as it appears in the + // protobuf. + // + // Example of a YAML configuration:: + // + // publishing: + // java_settings: + // service_class_names: + // - google.pubsub.v1.Publisher: TopicAdmin + // - google.pubsub.v1.Subscriber: SubscriptionAdmin + map service_class_names = 2; + + // Some settings. + CommonLanguageSettings common = 3; +} + +// Settings for C++ client libraries. +message CppSettings { + // Some settings. + CommonLanguageSettings common = 1; +} + +// Settings for Php client libraries. +message PhpSettings { + // Some settings. + CommonLanguageSettings common = 1; + + // The package name to use in Php. Clobbers the php_namespace option + // set in the protobuf. This should be used **only** by APIs + // who have already set the language_settings.php.package_name" field + // in gapic.yaml. API teams should use the protobuf php_namespace option + // where possible. + // + // Example of a YAML configuration:: + // + // publishing: + // library_settings: + // php_settings: + // library_package: Google\Cloud\PubSub\V1 + string library_package = 2; +} + +// Settings for Python client libraries. +message PythonSettings { + // Experimental features to be included during client library generation. + // These fields will be deprecated once the feature graduates and is enabled + // by default. + message ExperimentalFeatures { + // Enables generation of asynchronous REST clients if `rest` transport is + // enabled. By default, asynchronous REST clients will not be generated. + // This feature will be enabled by default 1 month after launching the + // feature in preview packages. + bool rest_async_io_enabled = 1; + + // Enables generation of protobuf code using new types that are more + // Pythonic which are included in `protobuf>=5.29.x`. This feature will be + // enabled by default 1 month after launching the feature in preview + // packages. + bool protobuf_pythonic_types_enabled = 2; + + // Disables generation of an unversioned Python package for this client + // library. This means that the module names will need to be versioned in + // import statements. For example `import google.cloud.library_v2` instead + // of `import google.cloud.library`. + bool unversioned_package_disabled = 3; + } + + // Some settings. + CommonLanguageSettings common = 1; + + // Experimental features to be included during client library generation. + ExperimentalFeatures experimental_features = 2; +} + +// Settings for Node client libraries. +message NodeSettings { + // Some settings. + CommonLanguageSettings common = 1; +} + +// Settings for Dotnet client libraries. +message DotnetSettings { + // Some settings. + CommonLanguageSettings common = 1; + + // Map from original service names to renamed versions. + // This is used when the default generated types + // would cause a naming conflict. (Neither name is + // fully-qualified.) + // Example: Subscriber to SubscriberServiceApi. + map renamed_services = 2; + + // Map from full resource types to the effective short name + // for the resource. This is used when otherwise resource + // named from different services would cause naming collisions. + // Example entry: + // "datalabeling.googleapis.com/Dataset": "DataLabelingDataset" + map renamed_resources = 3; + + // List of full resource types to ignore during generation. + // This is typically used for API-specific Location resources, + // which should be handled by the generator as if they were actually + // the common Location resources. + // Example entry: "documentai.googleapis.com/Location" + repeated string ignored_resources = 4; + + // Namespaces which must be aliased in snippets due to + // a known (but non-generator-predictable) naming collision + repeated string forced_namespace_aliases = 5; + + // Method signatures (in the form "service.method(signature)") + // which are provided separately, so shouldn't be generated. + // Snippets *calling* these methods are still generated, however. + repeated string handwritten_signatures = 6; +} + +// Settings for Ruby client libraries. +message RubySettings { + // Some settings. + CommonLanguageSettings common = 1; +} + +// Settings for Go client libraries. +message GoSettings { + // Some settings. + CommonLanguageSettings common = 1; + + // Map of service names to renamed services. Keys are the package relative + // service names and values are the name to be used for the service client + // and call options. + // + // Example: + // + // publishing: + // go_settings: + // renamed_services: + // Publisher: TopicAdmin + map renamed_services = 2; +} + +// Describes the generator configuration for a method. +message MethodSettings { + // Describes settings to use when generating API methods that use the + // long-running operation pattern. + // All default values below are from those used in the client library + // generators (e.g. + // [Java](https://github.com/googleapis/gapic-generator-java/blob/04c2faa191a9b5a10b92392fe8482279c4404803/src/main/java/com/google/api/generator/gapic/composer/common/RetrySettingsComposer.java)). + message LongRunning { + // Initial delay after which the first poll request will be made. + // Default value: 5 seconds. + google.protobuf.Duration initial_poll_delay = 1; + + // Multiplier to gradually increase delay between subsequent polls until it + // reaches max_poll_delay. + // Default value: 1.5. + float poll_delay_multiplier = 2; + + // Maximum time between two subsequent poll requests. + // Default value: 45 seconds. + google.protobuf.Duration max_poll_delay = 3; + + // Total polling timeout. + // Default value: 5 minutes. + google.protobuf.Duration total_poll_timeout = 4; + } + + // The fully qualified name of the method, for which the options below apply. + // This is used to find the method to apply the options. + // + // Example: + // + // publishing: + // method_settings: + // - selector: google.storage.control.v2.StorageControl.CreateFolder + // # method settings for CreateFolder... + string selector = 1; + + // Describes settings to use for long-running operations when generating + // API methods for RPCs. Complements RPCs that use the annotations in + // google/longrunning/operations.proto. + // + // Example of a YAML configuration:: + // + // publishing: + // method_settings: + // - selector: google.cloud.speech.v2.Speech.BatchRecognize + // long_running: + // initial_poll_delay: 60s # 1 minute + // poll_delay_multiplier: 1.5 + // max_poll_delay: 360s # 6 minutes + // total_poll_timeout: 54000s # 90 minutes + LongRunning long_running = 2; + + // List of top-level fields of the request message, that should be + // automatically populated by the client libraries based on their + // (google.api.field_info).format. Currently supported format: UUID4. + // + // Example of a YAML configuration: + // + // publishing: + // method_settings: + // - selector: google.example.v1.ExampleService.CreateExample + // auto_populated_fields: + // - request_id + repeated string auto_populated_fields = 3; + + // Batching configuration for an API method in client libraries. + // + // Example of a YAML configuration: + // + // publishing: + // method_settings: + // - selector: google.example.v1.ExampleService.BatchCreateExample + // batching: + // element_count_threshold: 1000 + // request_byte_threshold: 100000000 + // delay_threshold_millis: 10 + BatchingConfigProto batching = 4; +} + +// The organization for which the client libraries are being published. +// Affects the url where generated docs are published, etc. +enum ClientLibraryOrganization { + // Not useful. + CLIENT_LIBRARY_ORGANIZATION_UNSPECIFIED = 0; + + // Google Cloud Platform Org. + CLOUD = 1; + + // Ads (Advertising) Org. + ADS = 2; + + // Photos Org. + PHOTOS = 3; + + // Street View Org. + STREET_VIEW = 4; + + // Shopping Org. + SHOPPING = 5; + + // Geo Org. + GEO = 6; + + // Generative AI - https://developers.generativeai.google + GENERATIVE_AI = 7; +} + +// To where should client libraries be published? +enum ClientLibraryDestination { + // Client libraries will neither be generated nor published to package + // managers. + CLIENT_LIBRARY_DESTINATION_UNSPECIFIED = 0; + + // Generate the client library in a repo under github.com/googleapis, + // but don't publish it to package managers. + GITHUB = 10; + + // Publish the library to package managers like nuget.org and npmjs.com. + PACKAGE_MANAGER = 20; +} + +// This message is used to configure the generation of a subset of the RPCs in +// a service for client libraries. +// +// Note: This feature should not be used in most cases. +message SelectiveGapicGeneration { + // An allowlist of the fully qualified names of RPCs that should be included + // on public client surfaces. + repeated string methods = 1; + + // Setting this to true indicates to the client generators that methods + // that would be excluded from the generation should instead be generated + // in a way that indicates these methods should not be consumed by + // end users. How this is expressed is up to individual language + // implementations to decide. Some examples may be: added annotations, + // obfuscated identifiers, or other language idiomatic patterns. + bool generate_omitted_as_internal = 2; +} + +// `BatchingConfigProto` defines the batching configuration for an API method. +message BatchingConfigProto { + // The thresholds which trigger a batched request to be sent. + BatchingSettingsProto thresholds = 1; + + // The request and response fields used in batching. + BatchingDescriptorProto batch_descriptor = 2; +} + +// `BatchingSettingsProto` specifies a set of batching thresholds, each of +// which acts as a trigger to send a batch of messages as a request. At least +// one threshold must be positive nonzero. +message BatchingSettingsProto { + // The number of elements of a field collected into a batch which, if + // exceeded, causes the batch to be sent. + int32 element_count_threshold = 1; + + // The aggregated size of the batched field which, if exceeded, causes the + // batch to be sent. This size is computed by aggregating the sizes of the + // request field to be batched, not of the entire request message. + int64 request_byte_threshold = 2; + + // The duration after which a batch should be sent, starting from the addition + // of the first message to that batch. + google.protobuf.Duration delay_threshold = 3; + + // The maximum number of elements collected in a batch that could be accepted + // by server. + int32 element_count_limit = 4; + + // The maximum size of the request that could be accepted by server. + int32 request_byte_limit = 5; + + // The maximum number of elements allowed by flow control. + int32 flow_control_element_limit = 6; + + // The maximum size of data allowed by flow control. + int32 flow_control_byte_limit = 7; + + // The behavior to take when the flow control limit is exceeded. + FlowControlLimitExceededBehaviorProto flow_control_limit_exceeded_behavior = + 8; +} + +// The behavior to take when the flow control limit is exceeded. +enum FlowControlLimitExceededBehaviorProto { + // Default behavior, system-defined. + UNSET_BEHAVIOR = 0; + + // Stop operation, raise error. + THROW_EXCEPTION = 1; + + // Pause operation until limit clears. + BLOCK = 2; + + // Continue operation, disregard limit. + IGNORE = 3; +} + +// `BatchingDescriptorProto` specifies the fields of the request message to be +// used for batching, and, optionally, the fields of the response message to be +// used for demultiplexing. +message BatchingDescriptorProto { + // The repeated field in the request message to be aggregated by batching. + string batched_field = 1; + + // A list of the fields in the request message. Two requests will be batched + // together only if the values of every field specified in + // `request_discriminator_fields` is equal between the two requests. + repeated string discriminator_fields = 2; + + // Optional. When present, indicates the field in the response message to be + // used to demultiplex the response into multiple response messages, in + // correspondence with the multiple request messages originally batched + // together. + string subresponse_field = 3; +} diff --git a/extras/scion-a2a-bridge/proto/google/api/field_behavior.proto b/extras/scion-a2a-bridge/proto/google/api/field_behavior.proto new file mode 100644 index 000000000..861f8254f --- /dev/null +++ b/extras/scion-a2a-bridge/proto/google/api/field_behavior.proto @@ -0,0 +1,104 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +syntax = "proto3"; + +package google.api; + +import "google/protobuf/descriptor.proto"; + +option go_package = "google.golang.org/genproto/googleapis/api/annotations;annotations"; +option java_multiple_files = true; +option java_outer_classname = "FieldBehaviorProto"; +option java_package = "com.google.api"; +option objc_class_prefix = "GAPI"; + +extend google.protobuf.FieldOptions { + // A designation of a specific field behavior (required, output only, etc.) + // in protobuf messages. + // + // Examples: + // + // string name = 1 [(google.api.field_behavior) = REQUIRED]; + // State state = 1 [(google.api.field_behavior) = OUTPUT_ONLY]; + // google.protobuf.Duration ttl = 1 + // [(google.api.field_behavior) = INPUT_ONLY]; + // google.protobuf.Timestamp expire_time = 1 + // [(google.api.field_behavior) = OUTPUT_ONLY, + // (google.api.field_behavior) = IMMUTABLE]; + repeated google.api.FieldBehavior field_behavior = 1052 [packed = false]; +} + +// An indicator of the behavior of a given field (for example, that a field +// is required in requests, or given as output but ignored as input). +// This **does not** change the behavior in protocol buffers itself; it only +// denotes the behavior and may affect how API tooling handles the field. +// +// Note: This enum **may** receive new values in the future. +enum FieldBehavior { + // Conventional default for enums. Do not use this. + FIELD_BEHAVIOR_UNSPECIFIED = 0; + + // Specifically denotes a field as optional. + // While all fields in protocol buffers are optional, this may be specified + // for emphasis if appropriate. + OPTIONAL = 1; + + // Denotes a field as required. + // This indicates that the field **must** be provided as part of the request, + // and failure to do so will cause an error (usually `INVALID_ARGUMENT`). + REQUIRED = 2; + + // Denotes a field as output only. + // This indicates that the field is provided in responses, but including the + // field in a request does nothing (the server *must* ignore it and + // *must not* throw an error as a result of the field's presence). + OUTPUT_ONLY = 3; + + // Denotes a field as input only. + // This indicates that the field is provided in requests, and the + // corresponding field is not included in output. + INPUT_ONLY = 4; + + // Denotes a field as immutable. + // This indicates that the field may be set once in a request to create a + // resource, but may not be changed thereafter. + IMMUTABLE = 5; + + // Denotes that a (repeated) field is an unordered list. + // This indicates that the service may provide the elements of the list + // in any arbitrary order, rather than the order the user originally + // provided. Additionally, the list's order may or may not be stable. + UNORDERED_LIST = 6; + + // Denotes that this field returns a non-empty default value if not set. + // This indicates that if the user provides the empty value in a request, + // a non-empty value will be returned. The user will not be aware of what + // non-empty value to expect. + NON_EMPTY_DEFAULT = 7; + + // Denotes that the field in a resource (a message annotated with + // google.api.resource) is used in the resource name to uniquely identify the + // resource. For AIP-compliant APIs, this should only be applied to the + // `name` field on the resource. + // + // This behavior should not be applied to references to other resources within + // the message. + // + // The identifier field of resources often have different field behavior + // depending on the request it is embedded in (e.g. for Create methods name + // is optional and unused, while for Update methods it is required). Instead + // of method-specific annotations, only `IDENTIFIER` is required. + IDENTIFIER = 8; +} diff --git a/extras/scion-a2a-bridge/proto/google/api/http.proto b/extras/scion-a2a-bridge/proto/google/api/http.proto new file mode 100644 index 000000000..bb3af8e56 --- /dev/null +++ b/extras/scion-a2a-bridge/proto/google/api/http.proto @@ -0,0 +1,370 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +syntax = "proto3"; + +package google.api; + +option go_package = "google.golang.org/genproto/googleapis/api/annotations;annotations"; +option java_multiple_files = true; +option java_outer_classname = "HttpProto"; +option java_package = "com.google.api"; +option objc_class_prefix = "GAPI"; + +// Defines the HTTP configuration for an API service. It contains a list of +// [HttpRule][google.api.HttpRule], each specifying the mapping of an RPC method +// to one or more HTTP REST API methods. +message Http { + // A list of HTTP configuration rules that apply to individual API methods. + // + // **NOTE:** All service configuration rules follow "last one wins" order. + repeated HttpRule rules = 1; + + // When set to true, URL path parameters will be fully URI-decoded except in + // cases of single segment matches in reserved expansion, where "%2F" will be + // left encoded. + // + // The default behavior is to not decode RFC 6570 reserved characters in multi + // segment matches. + bool fully_decode_reserved_expansion = 2; +} + +// gRPC Transcoding +// +// gRPC Transcoding is a feature for mapping between a gRPC method and one or +// more HTTP REST endpoints. It allows developers to build a single API service +// that supports both gRPC APIs and REST APIs. Many systems, including [Google +// APIs](https://github.com/googleapis/googleapis), +// [Cloud Endpoints](https://cloud.google.com/endpoints), [gRPC +// Gateway](https://github.com/grpc-ecosystem/grpc-gateway), +// and [Envoy](https://github.com/envoyproxy/envoy) proxy support this feature +// and use it for large scale production services. +// +// `HttpRule` defines the schema of the gRPC/REST mapping. The mapping specifies +// how different portions of the gRPC request message are mapped to the URL +// path, URL query parameters, and HTTP request body. It also controls how the +// gRPC response message is mapped to the HTTP response body. `HttpRule` is +// typically specified as an `google.api.http` annotation on the gRPC method. +// +// Each mapping specifies a URL path template and an HTTP method. The path +// template may refer to one or more fields in the gRPC request message, as long +// as each field is a non-repeated field with a primitive (non-message) type. +// The path template controls how fields of the request message are mapped to +// the URL path. +// +// Example: +// +// service Messaging { +// rpc GetMessage(GetMessageRequest) returns (Message) { +// option (google.api.http) = { +// get: "/v1/{name=messages/*}" +// }; +// } +// } +// message GetMessageRequest { +// string name = 1; // Mapped to URL path. +// } +// message Message { +// string text = 1; // The resource content. +// } +// +// This enables an HTTP REST to gRPC mapping as below: +// +// - HTTP: `GET /v1/messages/123456` +// - gRPC: `GetMessage(name: "messages/123456")` +// +// Any fields in the request message which are not bound by the path template +// automatically become HTTP query parameters if there is no HTTP request body. +// For example: +// +// service Messaging { +// rpc GetMessage(GetMessageRequest) returns (Message) { +// option (google.api.http) = { +// get:"/v1/messages/{message_id}" +// }; +// } +// } +// message GetMessageRequest { +// message SubMessage { +// string subfield = 1; +// } +// string message_id = 1; // Mapped to URL path. +// int64 revision = 2; // Mapped to URL query parameter `revision`. +// SubMessage sub = 3; // Mapped to URL query parameter `sub.subfield`. +// } +// +// This enables a HTTP JSON to RPC mapping as below: +// +// - HTTP: `GET /v1/messages/123456?revision=2&sub.subfield=foo` +// - gRPC: `GetMessage(message_id: "123456" revision: 2 sub: +// SubMessage(subfield: "foo"))` +// +// Note that fields which are mapped to URL query parameters must have a +// primitive type or a repeated primitive type or a non-repeated message type. +// In the case of a repeated type, the parameter can be repeated in the URL +// as `...?param=A¶m=B`. In the case of a message type, each field of the +// message is mapped to a separate parameter, such as +// `...?foo.a=A&foo.b=B&foo.c=C`. +// +// For HTTP methods that allow a request body, the `body` field +// specifies the mapping. Consider a REST update method on the +// message resource collection: +// +// service Messaging { +// rpc UpdateMessage(UpdateMessageRequest) returns (Message) { +// option (google.api.http) = { +// patch: "/v1/messages/{message_id}" +// body: "message" +// }; +// } +// } +// message UpdateMessageRequest { +// string message_id = 1; // mapped to the URL +// Message message = 2; // mapped to the body +// } +// +// The following HTTP JSON to RPC mapping is enabled, where the +// representation of the JSON in the request body is determined by +// protos JSON encoding: +// +// - HTTP: `PATCH /v1/messages/123456 { "text": "Hi!" }` +// - gRPC: `UpdateMessage(message_id: "123456" message { text: "Hi!" })` +// +// The special name `*` can be used in the body mapping to define that +// every field not bound by the path template should be mapped to the +// request body. This enables the following alternative definition of +// the update method: +// +// service Messaging { +// rpc UpdateMessage(Message) returns (Message) { +// option (google.api.http) = { +// patch: "/v1/messages/{message_id}" +// body: "*" +// }; +// } +// } +// message Message { +// string message_id = 1; +// string text = 2; +// } +// +// +// The following HTTP JSON to RPC mapping is enabled: +// +// - HTTP: `PATCH /v1/messages/123456 { "text": "Hi!" }` +// - gRPC: `UpdateMessage(message_id: "123456" text: "Hi!")` +// +// Note that when using `*` in the body mapping, it is not possible to +// have HTTP parameters, as all fields not bound by the path end in +// the body. This makes this option more rarely used in practice when +// defining REST APIs. The common usage of `*` is in custom methods +// which don't use the URL at all for transferring data. +// +// It is possible to define multiple HTTP methods for one RPC by using +// the `additional_bindings` option. Example: +// +// service Messaging { +// rpc GetMessage(GetMessageRequest) returns (Message) { +// option (google.api.http) = { +// get: "/v1/messages/{message_id}" +// additional_bindings { +// get: "/v1/users/{user_id}/messages/{message_id}" +// } +// }; +// } +// } +// message GetMessageRequest { +// string message_id = 1; +// string user_id = 2; +// } +// +// This enables the following two alternative HTTP JSON to RPC mappings: +// +// - HTTP: `GET /v1/messages/123456` +// - gRPC: `GetMessage(message_id: "123456")` +// +// - HTTP: `GET /v1/users/me/messages/123456` +// - gRPC: `GetMessage(user_id: "me" message_id: "123456")` +// +// Rules for HTTP mapping +// +// 1. Leaf request fields (recursive expansion nested messages in the request +// message) are classified into three categories: +// - Fields referred by the path template. They are passed via the URL path. +// - Fields referred by the [HttpRule.body][google.api.HttpRule.body]. They +// are passed via the HTTP +// request body. +// - All other fields are passed via the URL query parameters, and the +// parameter name is the field path in the request message. A repeated +// field can be represented as multiple query parameters under the same +// name. +// 2. If [HttpRule.body][google.api.HttpRule.body] is "*", there is no URL +// query parameter, all fields +// are passed via URL path and HTTP request body. +// 3. If [HttpRule.body][google.api.HttpRule.body] is omitted, there is no HTTP +// request body, all +// fields are passed via URL path and URL query parameters. +// +// Path template syntax +// +// Template = "/" Segments [ Verb ] ; +// Segments = Segment { "/" Segment } ; +// Segment = "*" | "**" | LITERAL | Variable ; +// Variable = "{" FieldPath [ "=" Segments ] "}" ; +// FieldPath = IDENT { "." IDENT } ; +// Verb = ":" LITERAL ; +// +// The syntax `*` matches a single URL path segment. The syntax `**` matches +// zero or more URL path segments, which must be the last part of the URL path +// except the `Verb`. +// +// The syntax `Variable` matches part of the URL path as specified by its +// template. A variable template must not contain other variables. If a variable +// matches a single path segment, its template may be omitted, e.g. `{var}` +// is equivalent to `{var=*}`. +// +// The syntax `LITERAL` matches literal text in the URL path. If the `LITERAL` +// contains any reserved character, such characters should be percent-encoded +// before the matching. +// +// If a variable contains exactly one path segment, such as `"{var}"` or +// `"{var=*}"`, when such a variable is expanded into a URL path on the client +// side, all characters except `[-_.~0-9a-zA-Z]` are percent-encoded. The +// server side does the reverse decoding. Such variables show up in the +// [Discovery +// Document](https://developers.google.com/discovery/v1/reference/apis) as +// `{var}`. +// +// If a variable contains multiple path segments, such as `"{var=foo/*}"` +// or `"{var=**}"`, when such a variable is expanded into a URL path on the +// client side, all characters except `[-_.~/0-9a-zA-Z]` are percent-encoded. +// The server side does the reverse decoding, except "%2F" and "%2f" are left +// unchanged. Such variables show up in the +// [Discovery +// Document](https://developers.google.com/discovery/v1/reference/apis) as +// `{+var}`. +// +// Using gRPC API Service Configuration +// +// gRPC API Service Configuration (service config) is a configuration language +// for configuring a gRPC service to become a user-facing product. The +// service config is simply the YAML representation of the `google.api.Service` +// proto message. +// +// As an alternative to annotating your proto file, you can configure gRPC +// transcoding in your service config YAML files. You do this by specifying a +// `HttpRule` that maps the gRPC method to a REST endpoint, achieving the same +// effect as the proto annotation. This can be particularly useful if you +// have a proto that is reused in multiple services. Note that any transcoding +// specified in the service config will override any matching transcoding +// configuration in the proto. +// +// The following example selects a gRPC method and applies an `HttpRule` to it: +// +// http: +// rules: +// - selector: example.v1.Messaging.GetMessage +// get: /v1/messages/{message_id}/{sub.subfield} +// +// Special notes +// +// When gRPC Transcoding is used to map a gRPC to JSON REST endpoints, the +// proto to JSON conversion must follow the [proto3 +// specification](https://developers.google.com/protocol-buffers/docs/proto3#json). +// +// While the single segment variable follows the semantics of +// [RFC 6570](https://tools.ietf.org/html/rfc6570) Section 3.2.2 Simple String +// Expansion, the multi segment variable **does not** follow RFC 6570 Section +// 3.2.3 Reserved Expansion. The reason is that the Reserved Expansion +// does not expand special characters like `?` and `#`, which would lead +// to invalid URLs. As the result, gRPC Transcoding uses a custom encoding +// for multi segment variables. +// +// The path variables **must not** refer to any repeated or mapped field, +// because client libraries are not capable of handling such variable expansion. +// +// The path variables **must not** capture the leading "/" character. The reason +// is that the most common use case "{var}" does not capture the leading "/" +// character. For consistency, all path variables must share the same behavior. +// +// Repeated message fields must not be mapped to URL query parameters, because +// no client library can support such complicated mapping. +// +// If an API needs to use a JSON array for request or response body, it can map +// the request or response body to a repeated field. However, some gRPC +// Transcoding implementations may not support this feature. +message HttpRule { + // Selects a method to which this rule applies. + // + // Refer to [selector][google.api.DocumentationRule.selector] for syntax + // details. + string selector = 1; + + // Determines the URL pattern is matched by this rules. This pattern can be + // used with any of the {get|put|post|delete|patch} methods. A custom method + // can be defined using the 'custom' field. + oneof pattern { + // Maps to HTTP GET. Used for listing and getting information about + // resources. + string get = 2; + + // Maps to HTTP PUT. Used for replacing a resource. + string put = 3; + + // Maps to HTTP POST. Used for creating a resource or performing an action. + string post = 4; + + // Maps to HTTP DELETE. Used for deleting a resource. + string delete = 5; + + // Maps to HTTP PATCH. Used for updating a resource. + string patch = 6; + + // The custom pattern is used for specifying an HTTP method that is not + // included in the `pattern` field, such as HEAD, or "*" to leave the + // HTTP method unspecified for this rule. The wild-card rule is useful + // for services that provide content to Web (HTML) clients. + CustomHttpPattern custom = 8; + } + + // The name of the request field whose value is mapped to the HTTP request + // body, or `*` for mapping all request fields not captured by the path + // pattern to the HTTP body, or omitted for not having any HTTP request body. + // + // NOTE: the referred field must be present at the top-level of the request + // message type. + string body = 7; + + // Optional. The name of the response field whose value is mapped to the HTTP + // response body. When omitted, the entire response message will be used + // as the HTTP response body. + // + // NOTE: The referred field must be present at the top-level of the response + // message type. + string response_body = 12; + + // Additional HTTP bindings for the selector. Nested bindings must + // not contain an `additional_bindings` field themselves (that is, + // the nesting may only be one level deep). + repeated HttpRule additional_bindings = 11; +} + +// A custom pattern is used for defining custom HTTP verb. +message CustomHttpPattern { + // The name of this custom HTTP verb. + string kind = 1; + + // The path matched by this custom verb. + string path = 2; +} diff --git a/extras/scion-a2a-bridge/proto/google/api/launch_stage.proto b/extras/scion-a2a-bridge/proto/google/api/launch_stage.proto new file mode 100644 index 000000000..0fff247bd --- /dev/null +++ b/extras/scion-a2a-bridge/proto/google/api/launch_stage.proto @@ -0,0 +1,72 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +syntax = "proto3"; + +package google.api; + +option go_package = "google.golang.org/genproto/googleapis/api;api"; +option java_multiple_files = true; +option java_outer_classname = "LaunchStageProto"; +option java_package = "com.google.api"; +option objc_class_prefix = "GAPI"; + +// The launch stage as defined by [Google Cloud Platform +// Launch Stages](https://cloud.google.com/terms/launch-stages). +enum LaunchStage { + // Do not use this default value. + LAUNCH_STAGE_UNSPECIFIED = 0; + + // The feature is not yet implemented. Users can not use it. + UNIMPLEMENTED = 6; + + // Prelaunch features are hidden from users and are only visible internally. + PRELAUNCH = 7; + + // Early Access features are limited to a closed group of testers. To use + // these features, you must sign up in advance and sign a Trusted Tester + // agreement (which includes confidentiality provisions). These features may + // be unstable, changed in backward-incompatible ways, and are not + // guaranteed to be released. + EARLY_ACCESS = 1; + + // Alpha is a limited availability test for releases before they are cleared + // for widespread use. By Alpha, all significant design issues are resolved + // and we are in the process of verifying functionality. Alpha customers + // need to apply for access, agree to applicable terms, and have their + // projects allowlisted. Alpha releases don't have to be feature complete, + // no SLAs are provided, and there are no technical support obligations, but + // they will be far enough along that customers can actually use them in + // test environments or for limited-use tests -- just like they would in + // normal production cases. + ALPHA = 2; + + // Beta is the point at which we are ready to open a release for any + // customer to use. There are no SLA or technical support obligations in a + // Beta release. Products will be complete from a feature perspective, but + // may have some open outstanding issues. Beta releases are suitable for + // limited production use cases. + BETA = 3; + + // GA features are open to all developers and are considered stable and + // fully qualified for production use. + GA = 4; + + // Deprecated features are scheduled to be shut down and removed. For more + // information, see the "Deprecation Policy" section of our [Terms of + // Service](https://cloud.google.com/terms/) + // and the [Google Cloud Platform Subject to the Deprecation + // Policy](https://cloud.google.com/terms/deprecation) documentation. + DEPRECATED = 5; +} diff --git a/extras/scion-chat-app/go.mod b/extras/scion-chat-app/go.mod index 9620d8200..169b770f9 100644 --- a/extras/scion-chat-app/go.mod +++ b/extras/scion-chat-app/go.mod @@ -1,6 +1,6 @@ module github.com/GoogleCloudPlatform/scion/extras/scion-chat-app -go 1.25.4 +go 1.26.1 require ( cloud.google.com/go/secretmanager v1.16.0 diff --git a/extras/scion-chat-app/internal/chatapp/broker.go b/extras/scion-chat-app/internal/chatapp/broker.go index 2650935d3..005821c08 100644 --- a/extras/scion-chat-app/internal/chatapp/broker.go +++ b/extras/scion-chat-app/internal/chatapp/broker.go @@ -40,6 +40,7 @@ type BrokerServer struct { mu sync.RWMutex subscriptions map[string]bool configured bool + channelName string } // Compile-time interface checks. @@ -66,12 +67,21 @@ func (b *BrokerServer) Configure(config map[string]string) error { b.mu.Lock() defer b.mu.Unlock() b.configured = true + if b.channelName == "" { + b.channelName = "gchat" + } + if v, ok := config["plugin_name"]; ok && v != "" { + b.channelName = v + } b.log.Info("broker plugin configured", "config_keys", len(config)) return nil } // Publish receives a message from the Hub and routes it to the handler. func (b *BrokerServer) Publish(ctx context.Context, topic string, msg *messages.StructuredMessage) error { + if msg == nil { + return nil + } b.log.Debug("received message via broker", "topic", topic, "sender", msg.Sender, @@ -83,6 +93,13 @@ func (b *BrokerServer) Publish(ctx context.Context, topic string, msg *messages. return nil } +// ChannelName returns the configured channel name in a thread-safe manner. +func (b *BrokerServer) ChannelName() string { + b.mu.RLock() + defer b.mu.RUnlock() + return b.channelName +} + // Subscribe registers a topic pattern for receiving messages. func (b *BrokerServer) Subscribe(pattern string) error { b.mu.Lock() @@ -112,6 +129,7 @@ func (b *BrokerServer) GetInfo() (*plugin.PluginInfo, error) { return &plugin.PluginInfo{ Name: "scion-chat-app", Version: "1.0.0", + ChannelID: b.ChannelName(), Capabilities: []string{"chat-bridge", "notification-relay"}, }, nil } diff --git a/extras/scion-chat-app/internal/chatapp/commands.go b/extras/scion-chat-app/internal/chatapp/commands.go index 6230a7ea9..8abcbe470 100644 --- a/extras/scion-chat-app/internal/chatapp/commands.go +++ b/extras/scion-chat-app/internal/chatapp/commands.go @@ -307,12 +307,29 @@ func (r *CommandRouter) handleDialogSubmit(ctx context.Context, event *ChatEvent return r.reply(ctx, event, "This space is not linked to a project.") } - client, err := r.clientForUser(ctx, event) + mapping, err := r.idMapper.ResolveOrAutoRegister(ctx, &eventUserLookup{event}, event.UserID, event.Platform) if err != nil { + r.log.Error("Failed to resolve user mapping", "error", err, "userID", event.UserID) + return r.reply(ctx, event, "Something went wrong, please try again later.") + } + if mapping == nil { return r.reply(ctx, event, "Authentication required. Use `/scionAdmin register` first.") } + client, err := r.idMapper.ClientFor(ctx, mapping) + if err != nil { + return r.reply(ctx, event, fmt.Sprintf("Failed to create client: %v", err)) + } - if err := client.ProjectAgents(link.ProjectID).SendMessage(ctx, agentID, responseText, false); err != nil { + senderEmail := mapping.HubUserEmail + if senderEmail == "" { + return r.reply(ctx, event, "Your user mapping is missing a valid email address.") + } + msg := messages.NewInstruction("user:"+senderEmail, agentID, responseText) + msg.Channel = r.broker.ChannelName() + if event.ThreadID != "" { + msg.ThreadID = event.ThreadID + } + if err := client.ProjectAgents(link.ProjectID).SendStructuredMessage(ctx, agentID, msg, false, false, false); err != nil { return r.reply(ctx, event, fmt.Sprintf("Failed to send response to agent: %v", err)) } return r.reply(ctx, event, fmt.Sprintf("Response sent to agent `%s`.", agentID)) @@ -1063,9 +1080,11 @@ func (r *CommandRouter) cmdMessage(ctx context.Context, event *ChatEvent, args [ // Use the hub user email with "user:" prefix so agents can address replies msg := messages.NewInstruction("user:"+mapping.HubUserEmail, agentSlug, messageText) - msg.Channel = "gchat" + msg.Channel = r.broker.ChannelName() if threadID != "" { msg.ThreadID = threadID + } else if event.ThreadID != "" { + msg.ThreadID = event.ThreadID } if err := client.ProjectAgents(link.ProjectID).SendStructuredMessage(ctx, agentSlug, msg, false, false, false); err != nil { diff --git a/extras/scion-chat-app/internal/chatapp/notifications.go b/extras/scion-chat-app/internal/chatapp/notifications.go index dfcd7ae79..9b4adbfc9 100644 --- a/extras/scion-chat-app/internal/chatapp/notifications.go +++ b/extras/scion-chat-app/internal/chatapp/notifications.go @@ -181,9 +181,10 @@ func (n *NotificationRelay) handleUserMessage(ctx context.Context, projectID str mentions := n.buildMentions(mapping.PlatformUserID, agentSlug, link) if _, err := n.messenger.SendMessage(ctx, SendMessageRequest{ - SpaceID: link.SpaceID, - Text: mentions, - Card: &card, + SpaceID: link.SpaceID, + ThreadID: msg.ThreadID, + Text: mentions, + Card: &card, }); err != nil { n.log.Error("failed to relay user message", "space_id", link.SpaceID, diff --git a/extras/scion-discord/README.md b/extras/scion-discord/README.md new file mode 100644 index 000000000..434a6c278 --- /dev/null +++ b/extras/scion-discord/README.md @@ -0,0 +1,252 @@ +# scion-plugin-discord + +Discord message broker plugin for the Scion hub. Runs as a [go-plugin](https://github.com/hashicorp/go-plugin) broker spoke in the hub's FanOutBroker, providing bidirectional messaging between Discord channels and Scion agents. + +**Outbound:** Hub publishes `StructuredMessage`s → plugin formats and sends them to linked Discord channels via the Bot API, using per-agent webhooks for distinct agent identities (custom name + avatar). +**Inbound:** Discord messages (via Gateway) → plugin converts to `StructuredMessage`s → delivered to agents via the hub's inbound endpoint. + +## Prerequisites + +- Scion hub running with FanOutBroker support (`server.message_broker.types`) +- A Discord account with permission to create applications at [discord.com/developers](https://discord.com/developers/applications) +- Go 1.25+ (for building from source) + +## Setup Guide + +### 1. Create the Discord Bot + +1. Go to [discord.com/developers/applications](https://discord.com/developers/applications) and click **New Application** +2. Name it (e.g., "Scion"), then go to the **Bot** tab +3. Click **Reset Token** and copy the bot token (you'll need it for `settings.yaml`) +4. Copy the **Application ID** and **Public Key** from the **General Information** tab + +#### Enable Privileged Gateway Intents + +Under the **Bot** tab, scroll to **Privileged Gateway Intents** and enable: + +- **Message Content Intent** — required for reading @-mention message text. There is no slash-command-only fallback mode. +- **Server Members Intent** — required for resolving user information + +> **Note:** Scion bots are self-hosted and typically serve <100 guilds, so privileged intents are straightforward to enable without Discord's verification process. + +### 2. Invite the Bot to Your Server + +Go to the **OAuth2** tab, then **URL Generator**: + +1. Select scopes: `bot` and `applications.commands` +2. Select the bot permissions listed below (or use the permissions integer `277562386496`) +3. Copy the generated URL and open it to invite the bot + +#### Required Bot Permissions + +| Permission | Purpose | +|-----------|---------| +| Send Messages | Post agent responses in channels | +| Send Messages in Threads | Reply within conversation threads | +| Create Public Threads | Create thread-per-conversation | +| Embed Links | Rich embed formatting for agent responses | +| Read Message History | Access thread context for conversations | +| View Channels | Discover and read linked channels | +| Use Application Commands | Register and respond to `/scion` slash commands | +| Manage Threads | Archive/unarchive conversation threads | +| Manage Webhooks | **Required for per-agent identity** — each agent appears with its own name and avatar via Discord webhooks | +| Add Reactions | Acknowledge messages (optional) | + +> **Manage Webhooks** must be granted either via Server Settings → Roles → [Bot role] → enable Manage Webhooks, or included in the OAuth2 invite URL permissions. Without it, all messages will be sent as the bot user instead of with per-agent personas. + +### 3. Build and Install + +The plugin binary must be built separately from the hub. The hub discovers it by name (`scion-plugin-discord`) on `$PATH` or via an explicit `path` in `settings.yaml`. + +```bash +cd extras/scion-discord +go build -o scion-plugin-discord ./cmd/scion-plugin-discord +sudo install scion-plugin-discord /usr/local/bin/ +``` + +### 4. Configure settings.yaml + +Add the Discord plugin to the hub's `settings.yaml`: + +```yaml +server: + message_broker: + enabled: true + types: + - discord + +plugins: + broker: + discord: + config: + bot_token: "your-bot-token" + application_id: "your-application-id" + public_key: "your-public-key" + + # Guild-scoped command registration (instant updates, good for dev). + # Leave empty for global commands (can take up to 1 hour to propagate). + guild_id: "" + + # SQLite database for channel links, user mappings, and state. + # Default: discord.db (relative to hub working directory). + db_path: /var/lib/scion/discord.db + + # Optional tuning. + # send_queue_size: 100 # max queued messages per channel + # send_min_delay: 50ms # minimum delay between sends (rate limiting) + # agent_cache_ttl: 5m # how long to cache agent lists from hub +``` + +### 5. Start the Hub + +```bash +sudo systemctl restart scion-hub + +# Or manually +./scion server +``` + +The hub will discover and launch `scion-plugin-discord` as a managed subprocess. Look for `Discord broker configured` in the logs to confirm startup. + +### 6. Link a Discord Channel + +1. **Invite the bot** to your Discord server using the OAuth2 URL +2. **Run `/scion setup`** in any channel → select a project from the list +3. **Register your identity:** run `/scion register` → click the link → authenticate on your hub's profile page (`/profile/discord`) + +## User Guide + +### Slash Commands + +All commands are subcommands of `/scion`: + +| Command | Description | +|---------|-------------| +| `/scion setup` | Link this channel to a Scion project | +| `/scion unlink` | Unlink this channel from its project | +| `/scion agents` | List agents in the linked project with real-time state | +| `/scion default [agent]` | Set, change, or show the default agent | +| `/scion status ` | Show detailed status for an agent | +| `/scion register` | Link your Discord account to your Scion hub identity | +| `/scion unregister` | Remove your Discord account link | +| `/scion info` | Show your registration status | +| `/scion settings` | Configure channel notification settings | +| `/scion help` | Show available commands | + +Commands that modify configuration (`setup`, `unlink`) require Discord's **Manage Channels** permission. + +### Registration Flow + +1. Run `/scion register` in any channel (response is ephemeral — only you can see it) +2. Click the profile link button in the response +3. Authenticate on the hub and confirm the 6-character code +4. The plugin detects confirmation and stores the link + +Registration codes expire after 15 minutes. Run `/scion register` again for a fresh code. + +### Sending Messages to Agents + +Messages are routed based on @-mentions. If a default agent is set and the message is plain text (no `@mention`), it is automatically routed to the default agent. + +| Pattern | Routing | +|---------|---------| +| `hello, can you help?` | Routes to the default agent (if set) | +| `@BotName message` | Routes to the default agent | +| `@agentslug message` | Routes to the named agent | +| `@all message` | Broadcasts to ALL agents in the linked project | +| *(reply to a bot message)* | Continues the conversation with the same agent | + +The bot strips @-mentions from the message text before forwarding to the agent. Use `/scion default` to set, change, or clear the default agent. + +### Receiving Messages from Agents + +- **Agent replies** appear in the linked channel with the agent's own name and avatar (via webhooks) +- **Rich formatting** uses Discord embeds for structured responses +- **Agent avatars** are generated via [RoboHash](https://robohash.org/) based on the agent slug +- Messages exceeding Discord's 2000-character limit are split or truncated +- Embed descriptions exceeding 4096 characters are truncated with `[truncated]` + +### Agent Identity (Webhooks) + +Each agent appears in Discord with a distinct username and avatar, powered by Discord webhooks. The plugin lazily creates one webhook per channel ("Scion Agent Relay") and sends messages through it with per-agent `username` and `avatar_url` parameters. This requires the **Manage Webhooks** permission. + +If the permission is not granted, messages fall back to the bot's own identity. + +## Configuration Reference + +### Plugin Config Keys + +These keys go in `plugins.broker.discord.config` in `settings.yaml`: + +| Key | Required | Default | Description | +|-----|----------|---------|-------------| +| `bot_token` | **Yes** | — | Discord bot token | +| `application_id` | **Yes** | — | Discord application ID | +| `public_key` | No | — | Discord application public key | +| `guild_id` | No | — | Guild ID for guild-scoped slash commands (empty = global) | +| `db_path` | No | `discord.db` | Path to SQLite database for persistent state | +| `mention_routing` | No | `true` | Enable @-mention routing for messages | +| `send_queue_size` | No | `100` | Max queued outbound messages per channel | +| `send_min_delay` | No | `50ms` | Minimum delay between sends (rate-limit protection) | +| `agent_cache_ttl` | No | `5m` | TTL for cached agent lists from the hub | + +### Example settings.yaml (Complete) + +```yaml +server: + message_broker: + enabled: true + types: + - broker-log + - discord + +plugins: + broker: + broker-log: + self_managed: true + address: "localhost:9091" + discord: + config: + bot_token: "MTIzNDU2Nzg5.example.token" + application_id: "123456789012345678" + public_key: "abcdef1234567890abcdef1234567890abcdef1234567890" + guild_id: "987654321098765432" + db_path: /var/lib/scion/discord.db +``` + +## Architecture + +``` +Discord Gateway API + │ + ▼ + ┌──────────────────┐ Gateway events ┌──────────────────────┐ + │ Discord Channels │ ◄───────────────── │ scion-plugin- │ + │ & DMs │ ──────────────────►│ discord │ + └──────────────────┘ Bot API / Webhooks│ │ + │ ┌─ CommandHandler │ + │ ├─ CallbackHandler │ + │ ├─ RegistrationHndlr│ + │ ├─ WebhookManager │ + │ └─ SendQueue │ + │ │ │ + │ SQLite (state) │ + └──────────┬───────────┘ + │ go-plugin RPC + ▼ + ┌──────────────────────┐ + │ Scion Hub │ + │ (FanOutBroker) │ + │ │ + │ ┌─ broker-log │ + │ ├─ discord ◄─────│ + │ └─ chat-app │ + └──────────────────────┘ +``` + +- **FanOutBroker spoke:** The plugin runs as one of potentially several broker spokes. The hub publishes messages to all configured spokes concurrently. +- **Gateway mode:** The plugin connects to Discord via WebSocket Gateway (not HTTP interactions), receiving real-time message events. +- **Registration** uses a hub-issued 6-character code. The user generates a code via `/scion register`, then confirms it on the hub's `/profile/discord` page. +- **SQLite state** persists channel links, user mappings, conversation contexts, notification preferences, and pending ask-user callbacks across restarts. +- **Send queue** uses per-channel worker goroutines with configurable rate limiting to avoid Discord 429 errors. +- **Webhook identity** gives each agent a unique name and RoboHash avatar in Discord, managed per-channel with automatic recreation if deleted. diff --git a/extras/scion-discord/cmd/scion-plugin-discord/main.go b/extras/scion-discord/cmd/scion-plugin-discord/main.go new file mode 100644 index 000000000..4cbfa5005 --- /dev/null +++ b/extras/scion-discord/cmd/scion-plugin-discord/main.go @@ -0,0 +1,67 @@ +// scion-plugin-discord is the Discord message broker plugin for scion. +// It can run as: +// - A go-plugin subprocess (when launched by the scion plugin manager) +// - A standalone binary that prints usage information +// +// Plugin mode is auto-detected via the SCION_PLUGIN magic cookie environment variable. +package main + +import ( + "fmt" + "log/slog" + "os" + + "github.com/GoogleCloudPlatform/scion/extras/scion-discord/internal/discord" + "github.com/GoogleCloudPlatform/scion/pkg/plugin" + goplugin "github.com/hashicorp/go-plugin" +) + +func main() { + // If the magic cookie is set, run as a go-plugin subprocess. + if os.Getenv(plugin.MagicCookieKey) == plugin.MagicCookieValue { + servePlugin() + return + } + + // Otherwise, print usage information. + fmt.Println("scion-plugin-discord: Discord message broker plugin for Scion") + fmt.Println() + fmt.Println("This binary is intended to be launched by the Scion plugin manager.") + fmt.Println("It communicates with the Discord Gateway API to provide bidirectional") + fmt.Println("messaging between Discord channels and Scion agents.") + fmt.Println() + fmt.Println("Configuration keys:") + fmt.Println(" bot_token (required) Discord bot token") + fmt.Println(" application_id Discord application ID (for slash commands)") + fmt.Println(" public_key Discord public key (for interaction verification)") + fmt.Println(" guild_id Guild ID for guild-scoped commands (empty = global)") + fmt.Println(" hub_url Hub API URL for inbound message delivery") + fmt.Println(" hmac_key Base64-encoded HMAC key for hub authentication") + fmt.Println(" broker_id Broker ID for HMAC signing") + fmt.Println(" db_path Path to SQLite database (default: discord.db)") + fmt.Println(" mention_routing Enable @-mention routing (default: true)") + fmt.Println(" send_queue_size Max queued messages per channel (default: 100)") + fmt.Println(" send_min_delay Minimum delay between sends (default: 50ms)") + fmt.Println(" agent_cache_ttl TTL for cached agent list (default: 5m)") + os.Exit(0) +} + +func servePlugin() { + log := slog.New(slog.NewTextHandler(os.Stderr, &slog.HandlerOptions{Level: slog.LevelDebug})) + + impl := discord.NewBroker(log) + log.Info("Starting Discord broker plugin") + + goplugin.Serve(&goplugin.ServeConfig{ + HandshakeConfig: goplugin.HandshakeConfig{ + ProtocolVersion: plugin.BrokerPluginProtocolVersion, + MagicCookieKey: plugin.MagicCookieKey, + MagicCookieValue: plugin.MagicCookieValue, + }, + Plugins: map[string]goplugin.Plugin{ + plugin.BrokerPluginName: &plugin.BrokerPlugin{ + Impl: impl, + }, + }, + }) +} diff --git a/extras/scion-discord/go.mod b/extras/scion-discord/go.mod new file mode 100644 index 000000000..d92a949c2 --- /dev/null +++ b/extras/scion-discord/go.mod @@ -0,0 +1,49 @@ +module github.com/GoogleCloudPlatform/scion/extras/scion-discord + +go 1.26.1 + +require ( + github.com/GoogleCloudPlatform/scion v0.0.0-00010101000000-000000000000 + github.com/bwmarrin/discordgo v0.28.1 + github.com/hashicorp/go-plugin v1.7.0 + github.com/jackc/pgx/v5 v5.10.0 + github.com/stretchr/testify v1.11.1 + modernc.org/sqlite v1.44.3 +) + +require ( + github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect + github.com/dustin/go-humanize v1.0.1 // indirect + github.com/fatih/color v1.16.0 // indirect + github.com/golang/protobuf v1.5.4 // indirect + github.com/google/uuid v1.6.0 // indirect + github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674 // indirect + github.com/hashicorp/go-hclog v1.6.3 // indirect + github.com/hashicorp/yamux v0.1.2 // indirect + github.com/jackc/pgpassfile v1.0.0 // indirect + github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect + github.com/jackc/puddle/v2 v2.2.2 // indirect + github.com/kr/text v0.2.0 // indirect + github.com/mattn/go-colorable v0.1.14 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect + github.com/ncruces/go-strftime v1.0.0 // indirect + github.com/oklog/run v1.1.0 // indirect + github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect + github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect + github.com/rogpeppe/go-internal v1.15.0 // indirect + golang.org/x/crypto v0.49.0 // indirect + golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546 // indirect + golang.org/x/net v0.52.0 // indirect + golang.org/x/sync v0.20.0 // indirect + golang.org/x/sys v0.42.0 // indirect + golang.org/x/text v0.35.0 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20260401024825-9d38bb4040a9 // indirect + google.golang.org/grpc v1.80.0 // indirect + google.golang.org/protobuf v1.36.11 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect + modernc.org/libc v1.67.6 // indirect + modernc.org/mathutil v1.7.1 // indirect + modernc.org/memory v1.11.0 // indirect +) + +replace github.com/GoogleCloudPlatform/scion => ../../ diff --git a/extras/scion-discord/go.sum b/extras/scion-discord/go.sum new file mode 100644 index 000000000..b5ed40029 --- /dev/null +++ b/extras/scion-discord/go.sum @@ -0,0 +1,160 @@ +github.com/bufbuild/protocompile v0.14.1 h1:iA73zAf/fyljNjQKwYzUHD6AD4R8KMasmwa/FBatYVw= +github.com/bufbuild/protocompile v0.14.1/go.mod h1:ppVdAIhbr2H8asPk6k4pY7t9zB1OU5DoEw9xY/FUi1c= +github.com/bwmarrin/discordgo v0.28.1 h1:gXsuo2GBO7NbR6uqmrrBDplPUx2T3nzu775q/Rd1aG4= +github.com/bwmarrin/discordgo v0.28.1/go.mod h1:NJZpH+1AfhIcyQsPeuBKsUtYrRnjkyu0kIVMCHkZtRY= +github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= +github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= +github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= +github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk= +github.com/fatih/color v1.16.0 h1:zmkK9Ngbjj+K0yRhTVONQh1p/HknKYSlNT+vZCzyokM= +github.com/fatih/color v1.16.0/go.mod h1:fL2Sau1YI5c0pdGEVCbKQbLXB6edEj1ZgiY4NijnWvE= +github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= +github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= +github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e h1:ijClszYn+mADRFY17kjQEVQ1XRhq2/JR1M3sGqeJoxs= +github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674 h1:JeSE6pjso5THxAzdVpqr6/geYxZytqFMBCOtn/ujyeo= +github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674/go.mod h1:r4w70xmWCQKmi1ONH4KIaBptdivuRPyosB9RmPlGEwA= +github.com/hashicorp/go-hclog v1.6.3 h1:Qr2kF+eVWjTiYmU7Y31tYlP1h0q/X3Nl3tPGdaB11/k= +github.com/hashicorp/go-hclog v1.6.3/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M= +github.com/hashicorp/go-plugin v1.7.0 h1:YghfQH/0QmPNc/AZMTFE3ac8fipZyZECHdDPshfk+mA= +github.com/hashicorp/go-plugin v1.7.0/go.mod h1:BExt6KEaIYx804z8k4gRzRLEvxKVb+kn0NMcihqOqb8= +github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k= +github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM= +github.com/hashicorp/yamux v0.1.2 h1:XtB8kyFOyHXYVFnwT5C3+Bdo8gArse7j2AQ0DA0Uey8= +github.com/hashicorp/yamux v0.1.2/go.mod h1:C+zze2n6e/7wshOZep2A70/aQU6QBRWJO/G6FT1wIns= +github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= +github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= +github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo= +github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= +github.com/jackc/pgx/v5 v5.10.0 h1:VhSvgU2jSli8o3AqIEOTJr7rZwAEUVo4E4XhR94Zfr0= +github.com/jackc/pgx/v5 v5.10.0/go.mod h1:mal1tBGAFfLHvZzaYh77YS/eC6IX9OWbRV1QIIM0Jn4= +github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo= +github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4= +github.com/jhump/protoreflect v1.17.0 h1:qOEr613fac2lOuTgWN4tPAtLL7fUSbuJL5X5XumQh94= +github.com/jhump/protoreflect v1.17.0/go.mod h1:h9+vUUL38jiBzck8ck+6G/aeMX8Z4QUY/NiJPwPNi+8= +github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= +github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= +github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4= +github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE= +github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= +github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= +github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/ncruces/go-strftime v1.0.0 h1:HMFp8mLCTPp341M/ZnA4qaf7ZlsbTc+miZjCLOFAw7w= +github.com/ncruces/go-strftime v1.0.0/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls= +github.com/oklog/run v1.1.0 h1:GEenZ1cK0+q0+wsJew9qUg/DyD8k3JzYsZAi5gYi2mA= +github.com/oklog/run v1.1.0/go.mod h1:sVPdnTZT1zYwAJeCMu2Th4T21pA3FPOQRfWjQlk7DVU= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE= +github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= +github.com/rogpeppe/go-internal v1.15.0 h1:D0RCU5rMAp+SpgkiNdrjfJ+LX4J1M32V2NeCY7EJ6hc= +github.com/rogpeppe/go-internal v1.15.0/go.mod h1:DrUVZyrJU+txYW5/1kwtXQSMFio52ZOxX7yM1VHvnxs= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= +go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= +go.opentelemetry.io/otel v1.43.0 h1:mYIM03dnh5zfN7HautFE4ieIig9amkNANT+xcVxAj9I= +go.opentelemetry.io/otel v1.43.0/go.mod h1:JuG+u74mvjvcm8vj8pI5XiHy1zDeoCS2LB1spIq7Ay0= +go.opentelemetry.io/otel/metric v1.43.0 h1:d7638QeInOnuwOONPp4JAOGfbCEpYb+K6DVWvdxGzgM= +go.opentelemetry.io/otel/metric v1.43.0/go.mod h1:RDnPtIxvqlgO8GRW18W6Z/4P462ldprJtfxHxyKd2PY= +go.opentelemetry.io/otel/sdk v1.43.0 h1:pi5mE86i5rTeLXqoF/hhiBtUNcrAGHLKQdhg4h4V9Dg= +go.opentelemetry.io/otel/sdk v1.43.0/go.mod h1:P+IkVU3iWukmiit/Yf9AWvpyRDlUeBaRg6Y+C58QHzg= +go.opentelemetry.io/otel/sdk/metric v1.43.0 h1:S88dyqXjJkuBNLeMcVPRFXpRw2fuwdvfCGLEo89fDkw= +go.opentelemetry.io/otel/sdk/metric v1.43.0/go.mod h1:C/RJtwSEJ5hzTiUz5pXF1kILHStzb9zFlIEe85bhj6A= +go.opentelemetry.io/otel/trace v1.43.0 h1:BkNrHpup+4k4w+ZZ86CZoHHEkohws8AY+WTX09nk+3A= +go.opentelemetry.io/otel/trace v1.43.0/go.mod h1:/QJhyVBUUswCphDVxq+8mld+AvhXZLhe+8WVFxiFff0= +golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= +golang.org/x/crypto v0.49.0 h1:+Ng2ULVvLHnJ/ZFEq4KdcDd/cfjrrjjNSXNzxg0Y4U4= +golang.org/x/crypto v0.49.0/go.mod h1:ErX4dUh2UM+CFYiXZRTcMpEcN8b/1gxEuv3nODoYtCA= +golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546 h1:mgKeJMpvi0yx/sU5GsxQ7p6s2wtOnGAHZWCHUM4KGzY= +golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546/go.mod h1:j/pmGrbnkbPtQfxEe5D0VQhZC6qKbfKifgD0oM7sR70= +golang.org/x/mod v0.33.0 h1:tHFzIWbBifEmbwtGz65eaWyGiGZatSrT9prnU8DbVL8= +golang.org/x/mod v0.33.0/go.mod h1:swjeQEj+6r7fODbD2cqrnje9PnziFuw4bmLbBZFrQ5w= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.52.0 h1:He/TN1l0e4mmR3QqHMT2Xab3Aj3L9qjbhRm78/6jrW0= +golang.org/x/net v0.52.0/go.mod h1:R1MAz7uMZxVMualyPXb+VaqGSa3LIaUqk0eEt3w36Sw= +golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4= +golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= +golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220503163025-988cb79eb6c6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo= +golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.35.0 h1:JOVx6vVDFokkpaq1AEptVzLTpDe9KGpj5tR4/X+ybL8= +golang.org/x/text v0.35.0/go.mod h1:khi/HExzZJ2pGnjenulevKNX1W67CUy0AsXcNubPGCA= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.42.0 h1:uNgphsn75Tdz5Ji2q36v/nsFSfR/9BRFvqhGBaJGd5k= +golang.org/x/tools v0.42.0/go.mod h1:Ma6lCIwGZvHK6XtgbswSoWroEkhugApmsXyrUmBhfr0= +gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4= +gonum.org/v1/gonum v0.17.0/go.mod h1:El3tOrEuMpv2UdMrbNlKEh9vd86bmQ6vqIcDwxEOc1E= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260401024825-9d38bb4040a9 h1:m8qni9SQFH0tJc1X0vmnpw/0t+AImlSvp30sEupozUg= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260401024825-9d38bb4040a9/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8= +google.golang.org/grpc v1.80.0 h1:Xr6m2WmWZLETvUNvIUmeD5OAagMw3FiKmMlTdViWsHM= +google.golang.org/grpc v1.80.0/go.mod h1:ho/dLnxwi3EDJA4Zghp7k2Ec1+c2jqup0bFkw07bwF4= +google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= +google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +modernc.org/cc/v4 v4.27.1 h1:9W30zRlYrefrDV2JE2O8VDtJ1yPGownxciz5rrbQZis= +modernc.org/cc/v4 v4.27.1/go.mod h1:uVtb5OGqUKpoLWhqwNQo/8LwvoiEBLvZXIQ/SmO6mL0= +modernc.org/ccgo/v4 v4.30.1 h1:4r4U1J6Fhj98NKfSjnPUN7Ze2c6MnAdL0hWw6+LrJpc= +modernc.org/ccgo/v4 v4.30.1/go.mod h1:bIOeI1JL54Utlxn+LwrFyjCx2n2RDiYEaJVSrgdrRfM= +modernc.org/fileutil v1.3.40 h1:ZGMswMNc9JOCrcrakF1HrvmergNLAmxOPjizirpfqBA= +modernc.org/fileutil v1.3.40/go.mod h1:HxmghZSZVAz/LXcMNwZPA/DRrQZEVP9VX0V4LQGQFOc= +modernc.org/gc/v2 v2.6.5 h1:nyqdV8q46KvTpZlsw66kWqwXRHdjIlJOhG6kxiV/9xI= +modernc.org/gc/v2 v2.6.5/go.mod h1:YgIahr1ypgfe7chRuJi2gD7DBQiKSLMPgBQe9oIiito= +modernc.org/gc/v3 v3.1.1 h1:k8T3gkXWY9sEiytKhcgyiZ2L0DTyCQ/nvX+LoCljoRE= +modernc.org/gc/v3 v3.1.1/go.mod h1:HFK/6AGESC7Ex+EZJhJ2Gni6cTaYpSMmU/cT9RmlfYY= +modernc.org/goabi0 v0.2.0 h1:HvEowk7LxcPd0eq6mVOAEMai46V+i7Jrj13t4AzuNks= +modernc.org/goabi0 v0.2.0/go.mod h1:CEFRnnJhKvWT1c1JTI3Avm+tgOWbkOu5oPA8eH8LnMI= +modernc.org/libc v1.67.6 h1:eVOQvpModVLKOdT+LvBPjdQqfrZq+pC39BygcT+E7OI= +modernc.org/libc v1.67.6/go.mod h1:JAhxUVlolfYDErnwiqaLvUqc8nfb2r6S6slAgZOnaiE= +modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU= +modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg= +modernc.org/memory v1.11.0 h1:o4QC8aMQzmcwCK3t3Ux/ZHmwFPzE6hf2Y5LbkRs+hbI= +modernc.org/memory v1.11.0/go.mod h1:/JP4VbVC+K5sU2wZi9bHoq2MAkCnrt2r98UGeSK7Mjw= +modernc.org/opt v0.1.4 h1:2kNGMRiUjrp4LcaPuLY2PzUfqM/w9N23quVwhKt5Qm8= +modernc.org/opt v0.1.4/go.mod h1:03fq9lsNfvkYSfxrfUhZCWPk1lm4cq4N+Bh//bEtgns= +modernc.org/sortutil v1.2.1 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w= +modernc.org/sortutil v1.2.1/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE= +modernc.org/sqlite v1.44.3 h1:+39JvV/HWMcYslAwRxHb8067w+2zowvFOUrOWIy9PjY= +modernc.org/sqlite v1.44.3/go.mod h1:CzbrU2lSB1DKUusvwGz7rqEKIq+NUd8GWuBBZDs9/nA= +modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0= +modernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A= +modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y= +modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM= diff --git a/extras/scion-discord/internal/discord/broker.go b/extras/scion-discord/internal/discord/broker.go new file mode 100644 index 000000000..e151fb2d3 --- /dev/null +++ b/extras/scion-discord/internal/discord/broker.go @@ -0,0 +1,1342 @@ +// Package discord implements a Discord bot message broker plugin for Scion. +// It provides bidirectional messaging between Discord channels and Scion agents: +// - Outbound: Hub publishes StructuredMessages which are formatted and sent +// to Discord channels via the Discord API / gateway session. +// - Inbound: Discord messages received via the Gateway WebSocket are converted +// to StructuredMessages and forwarded to the hub's inbound endpoint. +package discord + +import ( + "bytes" + "context" + "crypto/rand" + "crypto/sha256" + "encoding/hex" + "encoding/json" + "errors" + "fmt" + "io" + "log/slog" + "net/http" + "strconv" + "strings" + "sync" + "time" + + "github.com/bwmarrin/discordgo" + + "github.com/GoogleCloudPlatform/scion/pkg/apiclient" + "github.com/GoogleCloudPlatform/scion/pkg/messages" + "github.com/GoogleCloudPlatform/scion/pkg/plugin" +) + +const ( + defaultAgentCacheTTL = 5 * time.Minute + defaultDBPath = "discord.db" + + // dedupTTL is how long a message ID is remembered for deduplication. + dedupTTL = 5 * time.Minute + + // OriginMarkerKey is the config key injected into outbound messages + // to identify messages originating from the scion hub. + OriginMarkerKey = "scion_origin" + + // OriginMarkerValue is the marker value for hub-originated messages. + OriginMarkerValue = "hub" +) + +// Config holds Discord-specific configuration parsed from the plugin config map. +type Config struct { + BotToken string + ApplicationID string + PublicKey string + GuildID string // empty = global commands + DBPath string + MentionRouting bool +} + +// inboundPayload is the JSON body sent to the hub API inbound endpoint. +type inboundPayload struct { + Topic string `json:"topic"` + Message *messages.StructuredMessage `json:"message"` +} + +// DiscordBroker implements plugin.MessageBrokerPluginInterface with +// Discord Gateway WebSocket, slash commands, message components, and +// persistent SQLite state. +type DiscordBroker struct { + mu sync.RWMutex + closed bool + log *slog.Logger + + session *discordgo.Session // Discord gateway session + botUser *discordgo.User // Bot's own user info + + hubURL string + hmacKey string + brokerID string + pluginName string + httpClient *http.Client + + store Store + + commands *CommandHandler + callbacks *CallbackHandler + registration *RegistrationHandler + hubClient HubClient + + subs map[string]bool + + sentIDs map[string]time.Time + sentIDsMu sync.Mutex + + sendQueue *SendQueue + webhooks *WebhookManager + + agentCacheTTL time.Duration + projectSlugMap map[string]string // injected by hub: projectID -> slug + + config *Config + + hostCallbacks plugin.HostCallbacks + + InboundHandler func(topic string, msg *messages.StructuredMessage) +} + +// NewBroker creates a new DiscordBroker with the given logger. +func NewBroker(log *slog.Logger) *DiscordBroker { + if log == nil { + log = slog.Default() + } + return &DiscordBroker{ + subs: make(map[string]bool), + sentIDs: make(map[string]time.Time), + log: log, + pluginName: "discord", + httpClient: &http.Client{Timeout: 10 * time.Second}, + agentCacheTTL: defaultAgentCacheTTL, + } +} + +// SetHostCallbacks implements plugin.HostCallbacksAware, allowing the +// host to inject a reverse-channel for dynamic subscription management. +func (b *DiscordBroker) SetHostCallbacks(hc plugin.HostCallbacks) { + b.mu.Lock() + defer b.mu.Unlock() + b.hostCallbacks = hc +} + +// Configure sets up the Discord broker from the provided config map. +// This is called in two phases: +// - Phase 1 (bot_token present): Creates discordgo.Session, inits SQLite store, +// parses Discord-specific config. Does NOT call session.Open() yet. +// - Phase 2 (hub_url present): Sets hub credentials, creates HubClient and +// component handlers, resolves stale project slugs. +func (b *DiscordBroker) Configure(config map[string]string) error { + b.mu.Lock() + defer b.mu.Unlock() + + // Extract hub credentials (may arrive in either phase). + if v, ok := config["hub_url"]; ok { + b.hubURL = v + } + if v, ok := config["hmac_key"]; ok { + b.hmacKey = v + } + if v, ok := config["broker_id"]; ok { + b.brokerID = v + } + if v, ok := config["plugin_name"]; ok { + b.pluginName = v + } + + // Phase 1: Bot token configuration. + botToken, hasBotToken := config["bot_token"] + if hasBotToken && botToken != "" { + // Create a discordgo session but do NOT open the gateway yet. + // Gateway connection happens on first Subscribe(). + session, err := discordgo.New("Bot " + botToken) + if err != nil { + return fmt.Errorf("create discord session: %w", err) + } + + // Configure gateway intents. + session.Identify.Intents = discordgo.IntentsGuilds | + discordgo.IntentsGuildMessages | + discordgo.IntentsDirectMessages | + discordgo.IntentsMessageContent + + b.session = session + + // Parse Discord-specific config. + cfg := &Config{ + BotToken: botToken, + ApplicationID: config["application_id"], + PublicKey: config["public_key"], + GuildID: config["guild_id"], + MentionRouting: true, // default + } + + if v, ok := config["mention_routing"]; ok && v != "" { + cfg.MentionRouting = v != "false" && v != "0" + } + + cfg.DBPath = config["db_path"] + if cfg.DBPath == "" { + cfg.DBPath = defaultDBPath + } + b.config = cfg + + // Initialize store: use PostgreSQL when hub injects database config, + // otherwise fall back to SQLite. + dbDriver, hasDriver := config["database_driver"] + dbURL, hasURL := config["database_url"] + if hasDriver && dbDriver == "postgres" && hasURL && dbURL != "" { + store, err := NewPostgresStore(dbURL) + if err != nil { + return fmt.Errorf("init postgres store: %w", err) + } + b.store = store + b.log.Info("Using PostgreSQL store for Discord broker") + } else { + store, err := NewSQLiteStore(cfg.DBPath) + if err != nil { + return fmt.Errorf("init sqlite store: %w", err) + } + b.store = store + } + + // Initialize send queue with rate limiting. + sqSize := 0 + if v, ok := config["send_queue_size"]; ok && v != "" { + if n, err := strconv.Atoi(v); err == nil && n > 0 { + sqSize = n + } + } + var sqDelay time.Duration + if v, ok := config["send_min_delay"]; ok && v != "" { + if d, err := time.ParseDuration(v); err == nil { + sqDelay = d + } + } + b.sendQueue = NewSendQueue(session, b.log, sqSize, sqDelay) + + // Initialize webhook manager for per-agent identity. + b.webhooks = NewWebhookManager(session, b.log) + + // Parse optional agent cache TTL. + if v, ok := config["agent_cache_ttl"]; ok && v != "" { + d, err := time.ParseDuration(v) + if err != nil { + return fmt.Errorf("invalid agent_cache_ttl: %w", err) + } + b.agentCacheTTL = d + } + + b.log.Info("Discord broker phase 1 configured", + "application_id", cfg.ApplicationID, + "guild_id", cfg.GuildID, + "db_path", cfg.DBPath, + "mention_routing", cfg.MentionRouting, + ) + } + + // Phase 2: Hub credentials and component handlers. + if b.hubURL != "" && b.session != nil { + // Create hub client. + b.hubClient = NewHTTPHubClient(b.hubURL, b.hmacKey, b.brokerID) + + // Create component handlers. + appID := "" + guildID := "" + if b.config != nil { + appID = b.config.ApplicationID + guildID = b.config.GuildID + } + b.commands = NewCommandHandler(b.store, b.session, b.hubClient, appID, guildID, b.agentCacheTTL, b.log) + b.callbacks = NewCallbackHandler(b.store, b.session, b.hubClient, b.deliverInbound, b.log) + b.registration = NewRegistrationHandler(b.store, b.session, b.hubURL, b.hmacKey, b.brokerID, b.log) + + // Parse hub-injected project slug map (projectID -> slug). + if slugMapJSON, ok := config["project_slug_map"]; ok && slugMapJSON != "" { + var m map[string]string + if err := json.Unmarshal([]byte(slugMapJSON), &m); err == nil { + b.projectSlugMap = m + } + } + + // Resolve stale channel link slugs that were stored as UUIDs. + if len(b.projectSlugMap) > 0 { + slugCtx, slugCancel := context.WithTimeout(context.Background(), 15*time.Second) + b.resolveStaleChannelSlugs(slugCtx) + slugCancel() + } + + b.log.Info("Discord broker phase 2 configured", + "hub_url", b.hubURL, + "broker_id", b.brokerID, + ) + + // Bootstrap Gateway: request a wildcard subscription so the Hub calls + // Subscribe(), which triggers startGateway() on the first call. + // Host callbacks are wired after Configure() returns, so we defer + // the request in a goroutine that retries until they're available. + go func() { + for i := 0; i < 20; i++ { + time.Sleep(500 * time.Millisecond) + b.mu.RLock() + hc := b.hostCallbacks + b.mu.RUnlock() + if hc == nil { + continue + } + if err := hc.RequestSubscription("scion.project.>"); err != nil { + b.log.Warn("Failed to request bootstrap subscription", "error", err) + continue + } + b.log.Info("Requested bootstrap subscription for Discord Gateway") + return + } + b.log.Error("Bootstrap subscription timed out — host callbacks never became available") + }() + } + + return nil +} + +// Subscribe records a subscription pattern and starts the Discord gateway +// connection on the first subscribe call. +func (b *DiscordBroker) Subscribe(pattern string) error { + b.mu.Lock() + defer b.mu.Unlock() + + if b.closed { + return fmt.Errorf("discord broker is closed") + } + + if b.subs[pattern] { + return nil + } + + wasEmpty := len(b.subs) == 0 + b.subs[pattern] = true + + // Open gateway connection on first subscription. + if wasEmpty && b.session != nil { + if err := b.startGateway(); err != nil { + delete(b.subs, pattern) + return fmt.Errorf("start discord gateway: %w", err) + } + } + + b.log.Debug("Subscription registered", "pattern", pattern) + return nil +} + +// Unsubscribe removes a subscription pattern. When all subscriptions are +// removed, the gateway connection is closed. +func (b *DiscordBroker) Unsubscribe(pattern string) error { + b.mu.Lock() + + if !b.subs[pattern] { + b.mu.Unlock() + return nil + } + + delete(b.subs, pattern) + shouldStop := len(b.subs) == 0 + session := b.session + + b.mu.Unlock() + + if shouldStop && session != nil { + if err := session.Close(); err != nil { + b.log.Warn("Failed to close discord gateway", "error", err) + } + b.log.Info("Discord gateway closed (no subscriptions)") + } + + b.log.Debug("Subscription removed", "pattern", pattern) + return nil +} + +// Publish sends a message to Discord channels using dynamic routing. +// Routing priority: +// 1. Direct channel ID from metadata (discord_channel_id) +// 2. ConversationContext for recipient +// 3. Broadcast to all ChannelLinks for project +func (b *DiscordBroker) Publish(ctx context.Context, topic string, msg *messages.StructuredMessage) error { + b.mu.RLock() + if b.closed { + b.mu.RUnlock() + return fmt.Errorf("discord broker is closed") + } + session := b.session + store := b.store + sendQueue := b.sendQueue + webhooks := b.webhooks + b.mu.RUnlock() + + if session == nil { + return fmt.Errorf("discord broker not configured") + } + + if msg == nil { + return fmt.Errorf("message is nil") + } + + // Channel filtering: if the message targets a specific channel that + // isn't ours, skip it. FanOutEventBus already does this, but + // belt-and-suspenders. + if msg != nil && msg.Channel != "" && msg.Channel != "discord" { + return nil + } + + // Dedup check. + dedupKey := msgDedupKey(msg) + if dedupKey != "" { + b.sentIDsMu.Lock() + if t, ok := b.sentIDs[dedupKey]; ok && time.Since(t) < dedupTTL { + b.sentIDsMu.Unlock() + b.log.Debug("Skipping duplicate message", "topic", topic, "dedup_key", dedupKey) + return nil + } + b.sentIDs[dedupKey] = time.Now() + b.pruneSentIDsLocked() + b.sentIDsMu.Unlock() + } + + // Determine the project and agent from the topic. + projectID, agentSlug := parseTopicComponents(topic) + + // Collect target channel IDs via dynamic routing. + var channelIDs []string + + // Priority 0: Thread routing — ThreadID maps directly to a Discord + // channel or thread snowflake. This takes precedence over all other + // routing so replies land in the same channel/thread as the original. + if msg != nil && msg.ThreadID != "" { + channelIDs = append(channelIDs, msg.ThreadID) + } + + // Priority 1: Direct channel ID from metadata. + if len(channelIDs) == 0 && msg != nil && msg.Metadata != nil { + if chID, ok := msg.Metadata["discord_channel_id"]; ok && chID != "" { + channelIDs = append(channelIDs, chID) + } + } + + // Priority 2: Look up via ConversationContext for the recipient. + if len(channelIDs) == 0 && msg != nil && msg.Recipient != "" && store != nil { + channelIDs = b.resolveRecipientChannels(ctx, msg.Recipient, projectID, agentSlug) + } + + // Priority 3: Broadcast to all ChannelLinks for the project. + if len(channelIDs) == 0 && projectID != "" && store != nil { + links, err := store.GetChannelLinksForProject(ctx, projectID) + if err != nil { + b.log.Warn("Failed to get channel links for broadcast", "project_id", projectID, "error", err) + } + for _, link := range links { + if link.Active { + channelIDs = append(channelIDs, link.ChannelID) + } + } + } + + if len(channelIDs) == 0 { + b.log.Debug("No Discord channel for topic, dropping message", "topic", topic) + return nil + } + + // Determine whether this message should be sent via webhook (agent identity) + // or via the bot API. Webhook routing applies when: + // - Sender is an agent (starts with "agent:") + // - Message type is TypeAssistantReply or TypeInstruction + // State changes and input-needed messages keep the bot identity (embed style). + useWebhook := webhooks != nil && + strings.HasPrefix(msg.Sender, "agent:") && + (msg.Type == messages.TypeAssistantReply || msg.Type == messages.TypeInstruction) + + // Extract agent slug from sender for webhook username. + senderSlug := agentSlug + if senderSlug == "" && strings.HasPrefix(msg.Sender, "agent:") { + senderSlug = strings.TrimPrefix(msg.Sender, "agent:") + } + + // Format the message text. When sending via webhook, the webhook username + // already shows the agent name, so we skip the agent name header and just + // send the body with prefix tags. + var text string + if useWebhook { + text = formatWebhookMessage(msg) + } else { + text = formatMessage(msg, agentSlug) + } + if text == "" { + return nil + } + + // Per-channel filtering based on channel link settings. + isAgentToAgent := msg != nil && + strings.HasPrefix(msg.Sender, "agent:") && + strings.HasPrefix(msg.Recipient, "agent:") + isStateChange := msg != nil && msg.Type == messages.TypeStateChange + needsFilter := isAgentToAgent || isStateChange + + // Send to each target channel. + var errs []error + for _, channelID := range channelIDs { + if needsFilter && store != nil { + link, linkErr := store.GetChannelLink(ctx, channelID) + if linkErr == nil && link != nil { + if isAgentToAgent && !link.ShowAgentToAgent { + b.log.Debug("Filtering agent-to-agent message", "channel_id", channelID) + continue + } + if isStateChange && !link.ShowStateChanges { + b.log.Debug("Filtering state change notification", "channel_id", channelID) + continue + } + } + } + + var err error + + if useWebhook { + // Send via webhook with per-agent identity. + _, err = webhooks.SendAsAgent(channelID, senderSlug, text, nil, nil) + if err != nil { + // Fallback to bot API if webhook send fails. + b.log.Warn("Webhook send failed, falling back to bot API", + "channel_id", channelID, + "agent", senderSlug, + "error", err) + botText := formatMessage(msg, agentSlug) + if sendQueue != nil { + _, err = sendQueue.Send(ctx, channelID, botText, nil, nil) + } else { + _, err = session.ChannelMessageSend(channelID, botText) + } + } + } else { + // Send via bot API (state changes, input-needed, non-agent messages). + if sendQueue != nil { + _, err = sendQueue.Send(ctx, channelID, text, nil, nil) + } else { + _, err = session.ChannelMessageSend(channelID, text) + } + } + + if err != nil { + b.log.Error("Failed to send Discord message", + "channel_id", channelID, "error", err) + errs = append(errs, err) + } + } + return errors.Join(errs...) +} + +// Close shuts down the Discord broker, closing the gateway session, +// draining the send queue, and closing the store. +func (b *DiscordBroker) Close() error { + b.mu.Lock() + if b.closed { + b.mu.Unlock() + return nil + } + b.closed = true + b.subs = make(map[string]bool) + session := b.session + store := b.store + sendQueue := b.sendQueue + b.mu.Unlock() + + if session != nil { + if err := session.Close(); err != nil { + b.log.Warn("Failed to close discord session", "error", err) + } + } + + if sendQueue != nil { + sendQueue.Close() + } + + if store != nil { + store.Close() + } + + b.log.Info("Discord broker closed") + return nil +} + +// GetInfo returns plugin metadata. +func (b *DiscordBroker) GetInfo() (*plugin.PluginInfo, error) { + return &plugin.PluginInfo{ + Name: "discord", + Version: "1.0.0", + ChannelID: "discord", + Capabilities: []string{ + "echo-filter", + "gateway-websocket", + "discord-bot-api", + "user-registration", + "slash-commands", + "message-components", + "channel-links", + "mention-routing", + }, + }, nil +} + +// HealthCheck returns the runtime health of the Discord broker. +func (b *DiscordBroker) HealthCheck() (*plugin.HealthStatus, error) { + b.mu.RLock() + defer b.mu.RUnlock() + + if b.closed { + return &plugin.HealthStatus{ + Status: "unhealthy", + Message: "broker is closed", + }, nil + } + + if b.session == nil { + return &plugin.HealthStatus{ + Status: "degraded", + Message: "broker not configured", + }, nil + } + + details := map[string]string{ + "subscriptions": fmt.Sprintf("%d", len(b.subs)), + } + + if b.botUser != nil { + details["bot_username"] = b.botUser.Username + "#" + b.botUser.Discriminator + details["bot_id"] = b.botUser.ID + } + if b.hubURL != "" { + details["hub_url"] = b.hubURL + } + + return &plugin.HealthStatus{ + Status: "healthy", + Message: "discord bot operational", + Details: details, + }, nil +} + +// --- Gateway setup --- + +// startGateway opens the Discord gateway WebSocket connection and +// registers event handlers. Must be called with b.mu held. +func (b *DiscordBroker) startGateway() error { + session := b.session + if session == nil { + return fmt.Errorf("no discord session configured") + } + + // Register gateway event handlers. + session.AddHandler(b.handleReady) + session.AddHandler(b.handleGuildCreate) + session.AddHandler(b.handleGuildDelete) + session.AddHandler(b.handleMessageCreate) + session.AddHandler(b.handleInteractionCreate) + + // Open the gateway connection. + if err := session.Open(); err != nil { + return fmt.Errorf("open discord gateway: %w", err) + } + + b.log.Info("Discord gateway connected") + return nil +} + +// --- Gateway event handlers --- + +// handleReady is called when the bot connects to the Discord gateway. +func (b *DiscordBroker) handleReady(_ *discordgo.Session, r *discordgo.Ready) { + b.mu.Lock() + b.botUser = r.User + commands := b.commands + b.mu.Unlock() + + b.log.Info("Discord bot ready", + "username", r.User.Username, + "discriminator", r.User.Discriminator, + "id", r.User.ID, + "guilds", len(r.Guilds), + ) + + // Register slash commands once the gateway is connected. + if commands != nil { + if err := commands.RegisterCommands(); err != nil { + b.log.Error("Failed to register slash commands", "error", err) + } + } +} + +// handleGuildCreate is called when the bot joins a guild or when guild +// data is received during the initial gateway connection. +func (b *DiscordBroker) handleGuildCreate(_ *discordgo.Session, g *discordgo.GuildCreate) { + b.log.Info("Discord guild available", + "guild_id", g.ID, + "guild_name", g.Name, + "member_count", g.MemberCount, + ) +} + +// handleGuildDelete is called when the bot is removed from a guild or +// when a guild becomes unavailable. +func (b *DiscordBroker) handleGuildDelete(_ *discordgo.Session, g *discordgo.GuildDelete) { + b.log.Info("Discord guild unavailable", + "guild_id", g.ID, + ) +} + +// handleMessageCreate is called for every new message in channels the bot +// can see. It routes to handleIncomingMessage for processing. +func (b *DiscordBroker) handleMessageCreate(s *discordgo.Session, m *discordgo.MessageCreate) { + b.handleIncomingMessage(s, m) +} + +// handleInteractionCreate dispatches Discord interactions (slash commands, +// message components, modals, autocomplete) to the appropriate handler. +func (b *DiscordBroker) handleInteractionCreate(s *discordgo.Session, i *discordgo.InteractionCreate) { + b.mu.RLock() + commands := b.commands + callbacks := b.callbacks + registration := b.registration + b.mu.RUnlock() + + switch i.Type { + case discordgo.InteractionApplicationCommand: + // Slash command. + if commands != nil { + data := i.ApplicationCommandData() + b.log.Debug("Slash command received", + "command", data.Name, + "user", interactionUserID(i), + ) + // Check if this is a register/unregister command handled by registration. + if data.Name == "scion" && len(data.Options) > 0 { + sub := data.Options[0].Name + if (sub == "register" || sub == "unregister") && registration != nil { + // Acknowledge immediately (ephemeral). + _ = s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{ + Type: discordgo.InteractionResponseDeferredChannelMessageWithSource, + Data: &discordgo.InteractionResponseData{ + Flags: discordgo.MessageFlagsEphemeral, + }, + }) + go func() { + if sub == "register" { + registration.HandleRegister(s, i) + } else { + registration.HandleUnregister(s, i) + } + }() + return + } + } + commands.HandleSlashCommand(s, i) + } + + case discordgo.InteractionMessageComponent: + // Button press or select menu. + if callbacks != nil { + data := i.MessageComponentData() + b.log.Debug("Message component interaction", + "custom_id", data.CustomID, + "user", interactionUserID(i), + ) + + // Special case: "ask:reply:" buttons open a modal, which must + // be the FIRST interaction response. Do NOT pre-acknowledge + // with DeferredMessageUpdate — the callback itself responds + // with InteractionResponseModal. + if strings.HasPrefix(data.CustomID, "ask:reply:") { + go func() { + callbacks.Dispatch(s, i, data.CustomID, data.Values) + }() + } else { + // Acknowledge with deferred update for all other components. + _ = s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{ + Type: discordgo.InteractionResponseDeferredMessageUpdate, + }) + go func() { + callbacks.Dispatch(s, i, data.CustomID, data.Values) + }() + } + } + + case discordgo.InteractionModalSubmit: + // Modal form submission. + data := i.ModalSubmitData() + b.log.Debug("Modal submit interaction", + "custom_id", data.CustomID, + "user", interactionUserID(i), + ) + + if strings.HasPrefix(data.CustomID, "ask:") { + // Acknowledge with deferred ephemeral message so we can + // send a follow-up after processing. + _ = s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{ + Type: discordgo.InteractionResponseDeferredChannelMessageWithSource, + Data: &discordgo.InteractionResponseData{ + Flags: discordgo.MessageFlagsEphemeral, + }, + }) + + store := b.store + go func() { + HandleModalSubmit(s, i, store, b.deliverInbound, b.log) + }() + } + + case discordgo.InteractionApplicationCommandAutocomplete: + // Autocomplete for slash command options. + if commands != nil { + b.log.Debug("Autocomplete interaction", + "command", i.ApplicationCommandData().Name, + "user", interactionUserID(i), + ) + commands.HandleAutocomplete(s, i) + } + } +} + +// --- Inbound message handling --- + +// handleIncomingMessage processes an incoming Discord message through the +// three-tier @-mention routing system and delivers to the hub. +func (b *DiscordBroker) handleIncomingMessage(s *discordgo.Session, m *discordgo.MessageCreate) { + if m.Author == nil || m.Author.Bot { + return + } + + if m.Content == "" { + return + } + + b.mu.RLock() + store := b.store + botUser := b.botUser + b.mu.RUnlock() + + channelID := m.ChannelID + + if store == nil { + return + } + + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) + defer cancel() + + link, err := store.GetChannelLink(ctx, channelID) + if err != nil { + b.log.Error("Failed to get channel link", "channel_id", channelID, "error", err) + return + } + if link == nil || !link.Active { + return + } + + botUserID := "" + if botUser != nil { + botUserID = botUser.ID + } + + // Get project agents (with cache refresh). + agents := b.getProjectAgents(ctx, link.ProjectID) + + // Three-tier @-mention routing. + targets, _ := resolveTargetAgents(m, botUserID, link.DefaultAgent, agents) + + // Fallback: reply-to-bot message — extract agent from webhook username. + if len(targets) == 0 && m.ReferencedMessage != nil { + slug := agentFromReply(m.ReferencedMessage, botUserID) + if slug != "" { + targets = []string{slug} + } + } + + // Fallback: unaddressed text → default agent (if configured). + if len(targets) == 0 && link.DefaultAgent != "" { + text := strings.TrimSpace(m.Content) + if text != "" && !strings.HasPrefix(text, "/") { + targets = []string{link.DefaultAgent} + } + } + + if len(targets) == 0 { + // If bot was mentioned but no agent resolved, send error feedback. + if isBotMentioned(m, botUserID) { + unresolved := extractUnresolvedMentions(m.Content, botUserID, agents) + if len(unresolved) > 0 { + errMsg := fmt.Sprintf("Unknown agent: %q. Use `/scion agents` to see available agents.", unresolved[0]) + s.ChannelMessageSend(channelID, errMsg) + } + } + return + } + + // Determine sender identity. + sender := "discord:" + m.Author.Username + senderID := m.Author.ID + + mapping, err := store.GetUserMapping(ctx, senderID) + if err == nil && mapping != nil && mapping.ScionEmail != "" { + sender = "user:" + mapping.ScionEmail + } else if mapping == nil { + b.log.Debug("Unregistered user tried to mention agent", "sender_id", senderID) + s.ChannelMessageSend(channelID, "Please use `/scion register` first to interact with agents.") + return + } + + // Strip bot and agent mentions from message text. + cleanText := stripMentions(m.Content, botUserID, targets) + cleanText = strings.TrimSpace(cleanText) + if cleanText == "" { + return + } + + // Deliver to each target agent. + for _, agentSlug := range targets { + cc := &ConversationContext{ + DiscordUserID: senderID, + ProjectID: link.ProjectID, + AgentSlug: agentSlug, + LastChannelID: channelID, + LastMessageAt: time.Now(), + } + if err := store.SetConversationContext(ctx, cc); err != nil { + b.log.Warn("Failed to save conversation context", "error", err) + } + + topic := fmt.Sprintf("scion.project.%s.agent.%s.messages", link.ProjectID, agentSlug) + recipient := "agent:" + agentSlug + + msg := &messages.StructuredMessage{ + Version: messages.Version, + Timestamp: m.Timestamp.UTC().Format(time.RFC3339), + Channel: "discord", + ThreadID: channelID, + Sender: sender, + SenderID: senderID, + Recipient: recipient, + Msg: cleanText, + Type: messages.TypeInstruction, + Metadata: map[string]string{ + "discord_channel_id": channelID, + "discord_message_id": m.ID, + "discord_guild_id": m.GuildID, + "project_id": link.ProjectID, + }, + } + + if isEcho(msg) { + b.log.Debug("Filtered echo message via origin marker", "topic", topic) + continue + } + + b.log.Debug("Delivering inbound message", + "topic", topic, "sender", sender, "agent", agentSlug) + + b.deliverInbound(topic, msg) + } +} + +// --- Hub delivery --- + +// deliverInbound sends a message to the hub API or InboundHandler. +func (b *DiscordBroker) deliverInbound(topic string, msg *messages.StructuredMessage) { + b.mu.RLock() + handler := b.InboundHandler + hubURL := b.hubURL + hmacKey := b.hmacKey + brokerID := b.brokerID + pluginName := b.pluginName + b.mu.RUnlock() + + if handler != nil { + handler(topic, msg) + return + } + + if hubURL == "" { + b.log.Debug("No hub URL configured, dropping inbound message", "topic", topic) + return + } + + payload := inboundPayload{ + Topic: topic, + Message: msg, + } + body, err := json.Marshal(payload) + if err != nil { + b.log.Error("Failed to marshal inbound message", "error", err) + return + } + + inboundURL := hubURL + "/api/v1/broker/inbound" + req, err := http.NewRequest("POST", inboundURL, bytes.NewReader(body)) + if err != nil { + b.log.Error("Failed to create inbound request", "error", err) + return + } + req.ContentLength = int64(len(body)) + req.Header.Set("Content-Type", "application/json") + req.Header.Set("X-Scion-Plugin-Name", pluginName) + + if brokerID != "" && hmacKey != "" { + if err := signInboundRequest(req, brokerID, hmacKey); err != nil { + b.log.Error("Failed to sign inbound request", "error", err) + return + } + } + + resp, err := b.httpClient.Do(req) + if err != nil { + b.log.Error("Failed to deliver inbound message", "error", err, "topic", topic) + return + } + io.Copy(io.Discard, resp.Body) + resp.Body.Close() + + if resp.StatusCode >= 400 { + b.log.Error("Hub rejected inbound message", + "status", resp.StatusCode, "topic", topic) + } +} + +// --- Agent cache --- + +// getProjectAgents returns the cached agent slugs for a project, refreshing +// from the Hub API if the cache is stale. +func (b *DiscordBroker) getProjectAgents(ctx context.Context, projectID string) []string { + b.mu.RLock() + store := b.store + hubClient := b.hubClient + ttl := b.agentCacheTTL + b.mu.RUnlock() + + if store == nil { + return nil + } + + cached, err := store.GetProjectAgents(ctx, projectID) + if err != nil { + b.log.Warn("Failed to read agent cache", "project_id", projectID, "error", err) + } + if cached != nil && time.Since(cached.RefreshedAt) < ttl { + return cached.AgentSlugs + } + + if hubClient == nil { + if cached != nil { + return cached.AgentSlugs + } + return nil + } + + agents, err := hubClient.ListAgents(ctx, projectID) + if err != nil { + b.log.Warn("Failed to refresh agent list from hub", "project_id", projectID, "error", err) + if cached != nil { + return cached.AgentSlugs + } + return nil + } + + slugs := agentSlugs(agents) + saveErr := store.SetProjectAgents(ctx, &ProjectAgents{ + ProjectID: projectID, + AgentSlugs: slugs, + RefreshedAt: time.Now(), + }) + if saveErr != nil { + b.log.Warn("Failed to cache agents", "project_id", projectID, "error", saveErr) + } + + return slugs +} + +// --- Dynamic subscription management --- + +func (b *DiscordBroker) subscribeForProject(projectID string) { + pattern := fmt.Sprintf("scion.project.%s.>", projectID) + + b.mu.RLock() + hc := b.hostCallbacks + b.mu.RUnlock() + + if hc != nil { + if err := hc.RequestSubscription(pattern); err != nil { + b.log.Warn("Failed to request subscription via host callbacks", + "pattern", pattern, "error", err) + } + } +} + +func (b *DiscordBroker) unsubscribeForProject(projectID string) { + pattern := fmt.Sprintf("scion.project.%s.>", projectID) + + b.mu.RLock() + hc := b.hostCallbacks + b.mu.RUnlock() + + if hc != nil { + if err := hc.CancelSubscription(pattern); err != nil { + b.log.Warn("Failed to cancel subscription via host callbacks", + "pattern", pattern, "error", err) + } + } +} + +// --- Routing helpers --- + +// resolveRecipientChannels looks up target channels for a specific recipient. +func (b *DiscordBroker) resolveRecipientChannels(ctx context.Context, recipient, projectID, agentSlug string) []string { + email := strings.TrimPrefix(recipient, "user:") + if email == recipient { + return nil + } + + b.mu.RLock() + store := b.store + b.mu.RUnlock() + + if store == nil { + return nil + } + + mapping, err := store.GetUserMappingByEmail(ctx, email) + if err != nil || mapping == nil { + return nil + } + + cc, err := store.GetConversationContext(ctx, mapping.DiscordUserID, projectID, agentSlug) + if err != nil || cc == nil { + return nil + } + + return []string{cc.LastChannelID} +} + +// resolveStaleChannelSlugs updates ChannelLinks where ProjectSlug equals +// ProjectID (i.e., slug was not resolved during initial import). +func (b *DiscordBroker) resolveStaleChannelSlugs(ctx context.Context) { + if len(b.projectSlugMap) == 0 { + b.log.Debug("Slug resolution skipped: no project_slug_map injected by hub") + return + } + + if b.store == nil { + return + } + + links, err := b.store.GetAllChannelLinks(ctx) + if err != nil { + b.log.Warn("Could not list channel links for slug resolution", "error", err) + return + } + for _, link := range links { + if link.ProjectSlug == link.ProjectID { + if slug, ok := b.projectSlugMap[link.ProjectID]; ok { + link.ProjectSlug = slug + if err := b.store.UpdateChannelLink(ctx, link); err != nil { + b.log.Warn("Failed to update channel link slug", + "channel_id", link.ChannelID, "error", err) + } else { + b.log.Info("Resolved channel link project slug", + "channel_id", link.ChannelID, + "project_id", link.ProjectID, + "slug", slug) + } + } + } + } +} + +// --- Topic parsing --- + +// parseTopicComponents extracts projectID and agentSlug from a topic string. +// Example: "scion.project.myproj.agent.coder.messages" -> ("myproj", "coder") +func parseTopicComponents(topic string) (projectID, agentSlug string) { + parts := strings.Split(topic, ".") + for i, part := range parts { + if part == "grove" && i+1 < len(parts) { + projectID = parts[i+1] + } + if part == "project" && i+1 < len(parts) { + projectID = parts[i+1] + } + if part == "agent" && i+1 < len(parts) { + agentSlug = parts[i+1] + } + } + if projectID == "" { + projectID = topic + } + return projectID, agentSlug +} + +// --- Message formatting --- + +// formatWebhookMessage formats a StructuredMessage for sending via webhook. +// The webhook username already displays the agent name, so this function +// omits the agent name header and just sends the body with prefix tags. +func formatWebhookMessage(msg *messages.StructuredMessage) string { + if msg == nil { + return "" + } + + var b strings.Builder + + // Prefix tags are kept — they carry important context. + if msg.Urgent { + b.WriteString("**[URGENT]** ") + } + if msg.Broadcasted { + b.WriteString("**[Broadcast]** ") + } + + // For agent-to-agent messages, show the recipient (the sender is in + // the webhook username already). + if strings.HasPrefix(msg.Sender, "agent:") && strings.HasPrefix(msg.Recipient, "agent:") { + recipientSlug := strings.TrimPrefix(msg.Recipient, "agent:") + fmt.Fprintf(&b, "→ **%s**\n", recipientSlug) + } + + // Status tag (e.g. [RUNNING], [COMPLETED]). + if msg.Status != "" { + fmt.Fprintf(&b, "[%s] ", msg.Status) + } + + // Body text. + b.WriteString(msg.Msg) + + return truncateMessage(b.String()) +} + +// formatMessage formats a StructuredMessage for Discord plain text output. +// Used for bot API sends where agent identity needs to be in the message text. +func formatMessage(msg *messages.StructuredMessage, agentSlug string) string { + if msg == nil { + return "" + } + + var b strings.Builder + + if msg.Urgent { + b.WriteString("**[URGENT]** ") + } + if msg.Broadcasted { + b.WriteString("**[Broadcast]** ") + } + + // Header with agent identity. + if strings.HasPrefix(msg.Sender, "agent:") && strings.HasPrefix(msg.Recipient, "agent:") { + senderSlug := strings.TrimPrefix(msg.Sender, "agent:") + recipientSlug := strings.TrimPrefix(msg.Recipient, "agent:") + fmt.Fprintf(&b, "**%s** -> **%s**", senderSlug, recipientSlug) + } else if agentSlug != "" { + fmt.Fprintf(&b, "**%s**", agentSlug) + } else if strings.HasPrefix(msg.Sender, "agent:") { + slug := strings.TrimPrefix(msg.Sender, "agent:") + fmt.Fprintf(&b, "**%s**", slug) + } else { + b.WriteString(msg.Sender) + } + + if msg.Status != "" { + fmt.Fprintf(&b, " [%s]", msg.Status) + } + + b.WriteString("\n") + b.WriteString(msg.Msg) + + text := b.String() + return truncateMessage(text) +} + +// truncateMessage ensures the message fits within Discord's 2000-character limit. +func truncateMessage(text string) string { + const maxLen = 2000 + if len(text) <= maxLen { + return text + } + return text[:maxLen-4] + "\n..." +} + +// --- Dedup helpers --- + +// isEcho returns true if the message was tagged with the scion origin marker. +func isEcho(msg *messages.StructuredMessage) bool { + if msg == nil { + return false + } + return strings.HasPrefix(msg.Sender, OriginMarkerKey+":"+OriginMarkerValue+":") +} + +// msgDedupKey returns a stable fingerprint for a message, used to detect +// duplicate deliveries of the same logical message. +func msgDedupKey(msg *messages.StructuredMessage) string { + if msg == nil || msg.Msg == "" { + return "" + } + h := sha256.New() + h.Write([]byte(msg.Sender)) + h.Write([]byte("|")) + h.Write([]byte(msg.Recipient)) + h.Write([]byte("|")) + h.Write([]byte(msg.Timestamp)) + h.Write([]byte("|")) + h.Write([]byte(msg.Type)) + h.Write([]byte("|")) + h.Write([]byte(msg.Msg)) + return hex.EncodeToString(h.Sum(nil)[:16]) +} + +// pruneSentIDsLocked removes dedup entries older than dedupTTL. +func (b *DiscordBroker) pruneSentIDsLocked() { + now := time.Now() + for k, t := range b.sentIDs { + if now.Sub(t) > dedupTTL { + delete(b.sentIDs, k) + } + } +} + +// --- HMAC auth helpers --- + +// signInboundRequest signs an HTTP request with HMAC auth. +func signInboundRequest(req *http.Request, brokerID, hmacKey string) error { + secretKey, err := decodeBase64(hmacKey) + if err != nil { + return fmt.Errorf("decode HMAC key: %w", err) + } + auth := &apiclient.HMACAuth{ + BrokerID: brokerID, + SecretKey: secretKey, + } + return auth.ApplyAuth(req) +} + +// generateRequestID generates a random hex request ID. +func generateRequestID() string { + b := make([]byte, 12) + rand.Read(b) + return hex.EncodeToString(b) +} + +// agentSlugs extracts slug strings from a slice of AgentInfo. +func agentSlugs(agents []AgentInfo) []string { + slugs := make([]string, len(agents)) + for i, a := range agents { + slugs[i] = a.Slug + } + return slugs +} diff --git a/extras/scion-discord/internal/discord/callbacks.go b/extras/scion-discord/internal/discord/callbacks.go new file mode 100644 index 000000000..335b65006 --- /dev/null +++ b/extras/scion-discord/internal/discord/callbacks.go @@ -0,0 +1,600 @@ +package discord + +import ( + "context" + "fmt" + "log/slog" + "strconv" + "strings" + "time" + + "github.com/bwmarrin/discordgo" + + "github.com/GoogleCloudPlatform/scion/pkg/messages" +) + +// CallbackHandler processes Discord message component interactions (buttons, selects). +type CallbackHandler struct { + store Store + session *discordgo.Session + hubClient HubClient + log *slog.Logger + + // deliverInbound delivers a StructuredMessage to the hub on the given topic. + // Injected by the broker so callbacks can route responses back to agents. + deliverInbound func(topic string, msg *messages.StructuredMessage) +} + +// NewCallbackHandler creates a new CallbackHandler. +// deliverInbound is a function that posts a StructuredMessage to the hub. +func NewCallbackHandler(store Store, session *discordgo.Session, hubClient HubClient, deliverInbound func(string, *messages.StructuredMessage), log *slog.Logger) *CallbackHandler { + if log == nil { + log = slog.Default() + } + return &CallbackHandler{ + store: store, + session: session, + hubClient: hubClient, + deliverInbound: deliverInbound, + log: log, + } +} + +// Dispatch routes a component interaction based on custom_id prefix. +func (h *CallbackHandler) Dispatch(s *discordgo.Session, i *discordgo.InteractionCreate, customID string, values []string) { + parts := strings.SplitN(customID, ":", 3) + if len(parts) < 2 { + h.log.Warn("Invalid callback custom_id", "custom_id", customID) + return + } + + switch parts[0] { + case "setup": + h.handleSetupCallback(s, i, parts[1:]) + case "ask": + h.handleAskCallback(s, i, customID) + case "notif": + h.handleNotifCallback(s, i, customID) + case "settings": + h.handleSettingsCallback(s, i, customID) + case "default": + h.handleDefaultCallback(s, i, customID) + default: + h.log.Debug("Unhandled callback prefix", "prefix", parts[0], "custom_id", customID) + } +} + +// handleSetupCallback handles setup-related button callbacks. +func (h *CallbackHandler) handleSetupCallback(s *discordgo.Session, i *discordgo.InteractionCreate, parts []string) { + if len(parts) == 0 { + return + } + + switch parts[0] { + case "proj": + if len(parts) < 2 { + return + } + h.handleSetupProject(s, i, parts[1]) + case "dflt": + if len(parts) < 2 { + return + } + h.handleSetupDefaultAgent(s, i, parts[1]) + default: + h.log.Debug("Unknown setup sub-action", "action", parts[0]) + } +} + +// handleSetupProject handles project selection during /scion setup. +func (h *CallbackHandler) handleSetupProject(s *discordgo.Session, i *discordgo.InteractionCreate, projectID string) { + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) + defer cancel() + + // Fetch agents for the selected project. + agents, err := h.hubClient.ListAgents(ctx, projectID) + if err != nil { + h.log.Error("Failed to list agents for project", "project_id", projectID, "error", err) + h.respondUpdate(s, i, "Failed to fetch agents. Please try `/scion setup` again.", nil) + return + } + + // Resolve project slug. + projectSlug := projectID + projects, projErr := h.hubClient.ListProjectsFresh(ctx) + if projErr == nil { + for _, p := range projects { + if p.ID == projectID { + projectSlug = p.DisplayName() + break + } + } + } + + // Save the link immediately with no default agent. + h.saveChannelLink(ctx, i, projectID, projectSlug, "") + + if len(agents) == 0 { + h.respondUpdate(s, i, + fmt.Sprintf("Channel linked to project **%s**.", projectSlug), nil) + return + } + + // Build agent selection buttons for choosing a default agent. + var rows []discordgo.MessageComponent + var buttons []discordgo.MessageComponent + for idx, agent := range agents { + buttons = append(buttons, discordgo.Button{ + Label: agent.Slug, + Style: discordgo.SecondaryButton, + CustomID: fmt.Sprintf("setup:dflt:%s", agent.Slug), + }) + if len(buttons) == 5 || idx == len(agents)-1 { + rows = append(rows, discordgo.ActionsRow{Components: buttons}) + buttons = nil + } + if len(rows) >= 5 { + break + } + } + + h.respondUpdate(s, i, + fmt.Sprintf("Channel linked to project **%s**.\nChoose a default agent (receives bot @-mentions):", projectSlug), + rows, + ) +} + +// handleSetupDefaultAgent handles default agent selection during /scion setup. +// The channel link was already saved by handleSetupProject; this updates +// the default agent. +func (h *CallbackHandler) handleSetupDefaultAgent(s *discordgo.Session, i *discordgo.InteractionCreate, agentSlug string) { + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) + defer cancel() + + link, _ := h.store.GetChannelLink(ctx, i.ChannelID) + if link == nil { + h.respondUpdate(s, i, "Setup session expired. Please use `/scion setup` again.", nil) + return + } + + link.DefaultAgent = agentSlug + if err := h.store.UpdateChannelLink(ctx, link); err != nil { + h.log.Error("Failed to update default agent", "error", err, "channel_id", i.ChannelID) + h.respondUpdate(s, i, "Failed to save default agent. Please try again.", nil) + return + } + + h.respondUpdate(s, i, + fmt.Sprintf("Channel linked to project **%s**.\nDefault agent: **%s**", link.ProjectSlug, agentSlug), + nil, + ) + h.log.Info("Default agent set during setup", + "channel_id", i.ChannelID, + "project_id", link.ProjectID, + "default_agent", agentSlug, + ) +} + +// saveChannelLink persists a channel-to-project link. +func (h *CallbackHandler) saveChannelLink(ctx context.Context, i *discordgo.InteractionCreate, projectID, projectSlug, agentSlug string) { + linkedBy := interactionUserID(i) + guildID := i.GuildID + + link := &ChannelLink{ + ChannelID: i.ChannelID, + GuildID: guildID, + ProjectID: projectID, + ProjectSlug: projectSlug, + DefaultAgent: agentSlug, + LinkedBy: linkedBy, + LinkedAt: time.Now(), + Active: true, + ShowAssistantReply: true, + ShowStateChanges: true, + NotifyInGroup: true, + } + + if err := h.store.CreateChannelLink(ctx, link); err != nil { + h.log.Error("Failed to save channel link", "error", err, "channel_id", i.ChannelID) + } else { + h.log.Info("Channel link saved", + "channel_id", i.ChannelID, + "guild_id", guildID, + "project_id", projectID, + ) + } +} + +// respondUpdate edits the deferred interaction response to update the message. +// This is used after the broker has already acknowledged with +// InteractionResponseDeferredMessageUpdate. +func (h *CallbackHandler) respondUpdate(s *discordgo.Session, i *discordgo.InteractionCreate, content string, components []discordgo.MessageComponent) { + edit := &discordgo.WebhookEdit{ + Content: &content, + } + if components != nil { + edit.Components = &components + } else { + empty := []discordgo.MessageComponent{} + edit.Components = &empty + } + _, err := s.InteractionResponseEdit(i.Interaction, edit) + if err != nil { + h.log.Error("Failed to edit interaction response", "error", err) + } +} + +// --- Ask-user callback handlers --- + +// handleAskCallback routes ask-user component interactions. +// custom_id formats: +// - ask:opt:: — user picked a choice button +// - ask:reply: — user clicked "Reply" (opens modal; NOT pre-acknowledged) +// - ask:dismiss: — user clicked "Dismiss" +func (h *CallbackHandler) handleAskCallback(s *discordgo.Session, i *discordgo.InteractionCreate, customID string) { + // Parse: "ask::[:]" + parts := strings.SplitN(customID, ":", 4) + if len(parts) < 3 { + h.log.Warn("Malformed ask callback custom_id", "custom_id", customID) + return + } + action := parts[1] + requestID := parts[2] + + switch action { + case "opt": + // ask:opt:: + if len(parts) < 4 { + h.log.Warn("Missing index in ask:opt callback", "custom_id", customID) + return + } + idx, err := strconv.Atoi(parts[3]) + if err != nil { + h.log.Warn("Invalid index in ask:opt callback", "custom_id", customID, "error", err) + return + } + h.handleAskOption(s, i, requestID, idx) + + case "reply": + // ask:reply: — open a modal for free-text response. + // NOTE: The broker must NOT pre-acknowledge this interaction with + // InteractionResponseDeferredMessageUpdate, because we need to + // respond with InteractionResponseModal instead. + h.handleAskReply(s, i, requestID) + + case "dismiss": + // ask:dismiss: + h.handleAskDismiss(s, i, requestID) + + default: + h.log.Debug("Unknown ask sub-action", "action", action, "custom_id", customID) + } +} + +// handleAskOption handles a choice button click for an ask-user request. +func (h *CallbackHandler) handleAskOption(s *discordgo.Session, i *discordgo.InteractionCreate, requestID string, index int) { + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) + defer cancel() + + pending, err := h.store.GetPendingAskUser(ctx, requestID) + if err != nil { + h.log.Error("Failed to get pending ask-user", "request_id", requestID, "error", err) + h.respondUpdate(s, i, "Error looking up request. Please try again.", nil) + return + } + if pending == nil { + h.respondUpdate(s, i, "This request has expired or was not found.", nil) + return + } + if pending.Responded { + h.respondUpdate(s, i, "This request has already been answered.", nil) + return + } + if time.Now().After(pending.ExpiresAt) { + h.respondUpdate(s, i, "This request has expired.", nil) + return + } + if index < 0 || index >= len(pending.Choices) { + h.log.Warn("Choice index out of range", "request_id", requestID, "index", index, "choices", len(pending.Choices)) + h.respondUpdate(s, i, "Invalid choice.", nil) + return + } + + choice := pending.Choices[index] + + // Deliver the response to the hub. + h.deliverAskUserResponse(ctx, i, pending, choice) + + // Mark as responded. + if err := h.store.MarkAskUserResponded(ctx, requestID); err != nil { + h.log.Error("Failed to mark ask-user as responded", "request_id", requestID, "error", err) + } + + // Update the original message to show the selection and disable buttons. + h.respondUpdate(s, i, fmt.Sprintf("✅ Responded: **%s**", choice), nil) + + h.log.Info("Ask-user option selected", + "request_id", requestID, + "choice", choice, + "user", interactionUserID(i), + ) +} + +// handleAskReply opens a modal for free-text response to an ask-user request. +func (h *CallbackHandler) handleAskReply(s *discordgo.Session, i *discordgo.InteractionCreate, requestID string) { + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + + pending, err := h.store.GetPendingAskUser(ctx, requestID) + if err != nil || pending == nil { + // Can't open a modal after a deferred update. Since this interaction + // was NOT pre-acknowledged, respond with a simple message. + _ = s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{ + Type: discordgo.InteractionResponseChannelMessageWithSource, + Data: &discordgo.InteractionResponseData{ + Content: "This request has expired or was not found.", + Flags: discordgo.MessageFlagsEphemeral, + }, + }) + return + } + if pending.Responded { + _ = s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{ + Type: discordgo.InteractionResponseChannelMessageWithSource, + Data: &discordgo.InteractionResponseData{ + Content: "This request has already been answered.", + Flags: discordgo.MessageFlagsEphemeral, + }, + }) + return + } + if time.Now().After(pending.ExpiresAt) { + _ = s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{ + Type: discordgo.InteractionResponseChannelMessageWithSource, + Data: &discordgo.InteractionResponseData{ + Content: "This request has expired.", + Flags: discordgo.MessageFlagsEphemeral, + }, + }) + return + } + + // Open the modal. The prompt is included in the modal for context. + OpenAskUserModal(s, i, requestID, "") +} + +// handleAskDismiss handles the "Dismiss" button for an ask-user request. +func (h *CallbackHandler) handleAskDismiss(s *discordgo.Session, i *discordgo.InteractionCreate, requestID string) { + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) + defer cancel() + + pending, err := h.store.GetPendingAskUser(ctx, requestID) + if err != nil { + h.log.Error("Failed to get pending ask-user for dismiss", "request_id", requestID, "error", err) + h.respondUpdate(s, i, "Error looking up request.", nil) + return + } + if pending == nil { + h.respondUpdate(s, i, "This request has expired or was not found.", nil) + return + } + if pending.Responded { + h.respondUpdate(s, i, "This request has already been answered.", nil) + return + } + + // Mark as responded (dismissed). + if err := h.store.MarkAskUserResponded(ctx, requestID); err != nil { + h.log.Error("Failed to mark ask-user as dismissed", "request_id", requestID, "error", err) + } + + // Update the original message to show dismissal and remove buttons. + h.respondUpdate(s, i, "Dismissed.", nil) + + h.log.Info("Ask-user dismissed", + "request_id", requestID, + "user", interactionUserID(i), + ) +} + +// deliverAskUserResponse builds a StructuredMessage from the user's response +// and delivers it to the hub, targeting the agent that asked. +func (h *CallbackHandler) deliverAskUserResponse(ctx context.Context, i *discordgo.InteractionCreate, pending *PendingAskUser, responseText string) { + if h.deliverInbound == nil { + h.log.Error("deliverInbound not configured, cannot deliver ask-user response") + return + } + + // Resolve the sender identity from Discord user → Scion identity. + discordUserID := interactionUserID(i) + sender := "discord:" + discordUserID + if mapping, err := h.store.GetUserMapping(ctx, discordUserID); err == nil && mapping != nil && mapping.ScionEmail != "" { + sender = "user:" + mapping.ScionEmail + } + + topic := fmt.Sprintf("scion.project.%s.agent.%s.messages", pending.ProjectID, pending.AgentSlug) + recipient := "agent:" + pending.AgentSlug + + msg := &messages.StructuredMessage{ + Version: messages.Version, + Timestamp: time.Now().UTC().Format(time.RFC3339), + Channel: "discord", + ThreadID: pending.ChannelID, + Sender: sender, + SenderID: discordUserID, + Recipient: recipient, + Msg: responseText, + Type: messages.TypeInstruction, + Metadata: map[string]string{ + "discord_channel_id": pending.ChannelID, + "project_id": pending.ProjectID, + "ask_request_id": pending.RequestID, + }, + } + + h.deliverInbound(topic, msg) +} + +// --- Settings callback handlers --- + +// handleSettingsCallback toggles channel settings. +// custom_id formats: +// - settings:observe: — toggle observe mode +// - settings:statechange: — toggle state change notifications +func (h *CallbackHandler) handleSettingsCallback(s *discordgo.Session, i *discordgo.InteractionCreate, customID string) { + parts := strings.SplitN(customID, ":", 3) + if len(parts) < 3 { + h.log.Warn("Malformed settings callback custom_id", "custom_id", customID) + return + } + action := parts[1] + channelID := parts[2] + + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) + defer cancel() + + link, err := h.store.GetChannelLink(ctx, channelID) + if err != nil || link == nil { + h.respondUpdate(s, i, "This channel is no longer linked to a project.", nil) + return + } + + switch action { + case "observe": + link.ShowAgentToAgent = !link.ShowAgentToAgent + case "statechange": + link.ShowStateChanges = !link.ShowStateChanges + default: + h.log.Debug("Unknown settings action", "action", action) + return + } + + if err := h.store.UpdateChannelLink(ctx, link); err != nil { + h.log.Error("Failed to update channel settings", "error", err, "channel_id", channelID) + h.respondUpdate(s, i, "Failed to update settings. Please try again.", nil) + return + } + + content, components := settingsPanel(link) + h.respondUpdate(s, i, content, components) + + h.log.Info("Channel settings updated", + "channel_id", channelID, + "action", action, + "observe_mode", link.ShowAgentToAgent, + "state_changes", link.ShowStateChanges, + ) +} + +// --- Default agent callback handlers --- + +// handleDefaultCallback handles default agent selection buttons. +// custom_id formats: +// - default:set: — set agent as default +// - default:none — clear default agent +func (h *CallbackHandler) handleDefaultCallback(s *discordgo.Session, i *discordgo.InteractionCreate, customID string) { + parts := strings.SplitN(customID, ":", 3) + if len(parts) < 2 { + h.log.Warn("Malformed default callback custom_id", "custom_id", customID) + return + } + + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) + defer cancel() + + link, err := h.store.GetChannelLink(ctx, i.ChannelID) + if err != nil || link == nil { + h.respondUpdate(s, i, "This channel is not linked to a project.", nil) + return + } + + action := parts[1] + switch action { + case "none": + link.DefaultAgent = "" + if err := h.store.UpdateChannelLink(ctx, link); err != nil { + h.log.Error("Failed to clear default agent", "error", err) + h.respondUpdate(s, i, "Failed to clear default agent. Please try again.", nil) + return + } + h.respondUpdate(s, i, "Default agent cleared for this channel.", nil) + h.log.Info("Default agent cleared via button", "channel_id", i.ChannelID) + + case "set": + if len(parts) < 3 { + h.log.Warn("Missing agent slug in default:set callback", "custom_id", customID) + return + } + agentSlug := parts[2] + link.DefaultAgent = agentSlug + if err := h.store.UpdateChannelLink(ctx, link); err != nil { + h.log.Error("Failed to set default agent", "error", err) + h.respondUpdate(s, i, "Failed to set default agent. Please try again.", nil) + return + } + h.respondUpdate(s, i, fmt.Sprintf("Default agent set to **%s** for this channel.", agentSlug), nil) + h.log.Info("Default agent set via button", "channel_id", i.ChannelID, "agent", agentSlug) + + default: + h.log.Debug("Unknown default action", "action", action, "custom_id", customID) + } +} + +// --- Notification callback handlers --- + +// handleNotifCallback toggles notification preferences. +// custom_id formats: +// - notif:on: — enable notifications for agent +// - notif:off: — disable notifications for agent +func (h *CallbackHandler) handleNotifCallback(s *discordgo.Session, i *discordgo.InteractionCreate, customID string) { + parts := strings.SplitN(customID, ":", 3) + if len(parts) < 3 { + h.log.Warn("Malformed notif callback custom_id", "custom_id", customID) + return + } + action := parts[1] + agentSlug := parts[2] + + enabled := action == "on" + + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) + defer cancel() + + discordUserID := interactionUserID(i) + + // Look up the channel link to determine the project. + link, err := h.store.GetChannelLink(ctx, i.ChannelID) + if err != nil || link == nil { + h.respondUpdate(s, i, "This channel is not linked to a project.", nil) + return + } + + pref := &NotificationPref{ + DiscordUserID: discordUserID, + ProjectID: link.ProjectID, + AgentSlug: agentSlug, + Enabled: enabled, + UpdatedAt: time.Now(), + } + + if err := h.store.SetNotificationPref(ctx, pref); err != nil { + h.log.Error("Failed to save notification pref", "error", err) + h.respondUpdate(s, i, "Failed to update notification preference.", nil) + return + } + + stateText := "enabled" + if !enabled { + stateText = "disabled" + } + h.respondUpdate(s, i, + fmt.Sprintf("Notifications for **%s**: %s", agentSlug, stateText), + nil, + ) + + h.log.Info("Notification preference updated", + "user", discordUserID, + "agent", agentSlug, + "enabled", enabled, + ) +} diff --git a/extras/scion-discord/internal/discord/commands.go b/extras/scion-discord/internal/discord/commands.go new file mode 100644 index 000000000..7a3283bc2 --- /dev/null +++ b/extras/scion-discord/internal/discord/commands.go @@ -0,0 +1,940 @@ +package discord + +import ( + "context" + "fmt" + "log/slog" + "strings" + "time" + + "github.com/bwmarrin/discordgo" +) + +// AgentInfo holds an agent's slug and current activity state. +type AgentInfo struct { + Slug string `json:"slug"` + Activity string `json:"activity,omitempty"` +} + +// ProjectOption holds a project's identifiers for display in selection UI. +type ProjectOption struct { + ID string + Name string + Slug string +} + +// DisplayName returns a human-readable label for the project. +func (p ProjectOption) DisplayName() string { + if p.Name != "" { + return p.Name + } + if p.Slug != "" { + return p.Slug + } + return p.ID +} + +// HubClient provides access to the Scion hub API for project and agent listing. +type HubClient interface { + ListProjects(ctx context.Context) ([]ProjectOption, error) + ListProjectsFresh(ctx context.Context) ([]ProjectOption, error) + ListProjectsForUser(ctx context.Context, ownerID string) ([]ProjectOption, error) + ListAgents(ctx context.Context, projectID string) ([]AgentInfo, error) +} + +// CommandHandler manages Discord slash command registration and dispatch. +type CommandHandler struct { + store Store + session *discordgo.Session + hubClient HubClient + log *slog.Logger + appID string + guildID string // empty = global commands + agentCacheTTL time.Duration +} + +// NewCommandHandler creates a new CommandHandler. agentCacheTTL controls how +// long agent lists are cached before refreshing from the Hub API. +func NewCommandHandler(store Store, session *discordgo.Session, hubClient HubClient, appID, guildID string, agentCacheTTL time.Duration, log *slog.Logger) *CommandHandler { + if log == nil { + log = slog.Default() + } + return &CommandHandler{ + store: store, + session: session, + hubClient: hubClient, + log: log, + appID: appID, + guildID: guildID, + agentCacheTTL: agentCacheTTL, + } +} + +// RegisterCommands registers the /scion command and its subcommands with Discord. +func (h *CommandHandler) RegisterCommands() error { + cmd := &discordgo.ApplicationCommand{ + Name: "scion", + Description: "Scion agent management", + Options: []*discordgo.ApplicationCommandOption{ + { + Type: discordgo.ApplicationCommandOptionSubCommand, + Name: "setup", + Description: "Link this channel to a Scion project", + }, + { + Type: discordgo.ApplicationCommandOptionSubCommand, + Name: "unlink", + Description: "Unlink this channel from its project", + }, + { + Type: discordgo.ApplicationCommandOptionSubCommand, + Name: "agents", + Description: "List agents in the linked project", + }, + { + Type: discordgo.ApplicationCommandOptionSubCommand, + Name: "status", + Description: "Show agent status", + Options: []*discordgo.ApplicationCommandOption{{ + Type: discordgo.ApplicationCommandOptionString, + Name: "agent", + Description: "Agent name", + Required: true, + Autocomplete: true, + }}, + }, + { + Type: discordgo.ApplicationCommandOptionSubCommand, + Name: "start", + Description: "Start an agent", + Options: []*discordgo.ApplicationCommandOption{{ + Type: discordgo.ApplicationCommandOptionString, + Name: "agent", + Description: "Agent name", + Required: true, + Autocomplete: true, + }}, + }, + { + Type: discordgo.ApplicationCommandOptionSubCommand, + Name: "stop", + Description: "Stop an agent", + Options: []*discordgo.ApplicationCommandOption{{ + Type: discordgo.ApplicationCommandOptionString, + Name: "agent", + Description: "Agent name", + Required: true, + Autocomplete: true, + }}, + }, + { + Type: discordgo.ApplicationCommandOptionSubCommand, + Name: "msg", + Description: "Send a message to an agent", + Options: []*discordgo.ApplicationCommandOption{ + { + Type: discordgo.ApplicationCommandOptionString, + Name: "agent", + Description: "Agent name", + Required: true, + Autocomplete: true, + }, + { + Type: discordgo.ApplicationCommandOptionString, + Name: "text", + Description: "Message text", + Required: true, + }, + }, + }, + { + Type: discordgo.ApplicationCommandOptionSubCommand, + Name: "logs", + Description: "View agent logs", + Options: []*discordgo.ApplicationCommandOption{{ + Type: discordgo.ApplicationCommandOptionString, + Name: "agent", + Description: "Agent name", + Required: true, + Autocomplete: true, + }}, + }, + { + Type: discordgo.ApplicationCommandOptionSubCommand, + Name: "default", + Description: "Set or show the default agent for this channel", + }, + { + Type: discordgo.ApplicationCommandOptionSubCommand, + Name: "register", + Description: "Link your Discord account to Scion Hub", + }, + { + Type: discordgo.ApplicationCommandOptionSubCommand, + Name: "unregister", + Description: "Unlink your Discord account from Scion Hub", + }, + { + Type: discordgo.ApplicationCommandOptionSubCommand, + Name: "settings", + Description: "Configure channel notification settings", + }, + { + Type: discordgo.ApplicationCommandOptionSubCommand, + Name: "info", + Description: "Show your registration info and linked project", + }, + { + Type: discordgo.ApplicationCommandOptionSubCommand, + Name: "help", + Description: "Show available commands", + }, + }, + } + + _, err := h.session.ApplicationCommandCreate(h.appID, h.guildID, cmd) + if err != nil { + return fmt.Errorf("registering /scion command: %w", err) + } + + h.log.Info("Registered /scion slash command", "app_id", h.appID, "guild_id", h.guildID) + return nil +} + +// ephemeralCommands lists subcommands whose responses should be ephemeral. +var ephemeralCommands = map[string]bool{ + "help": true, + "info": true, + "register": true, + "setup": true, + "unlink": true, + "settings": true, + "default": true, +} + +// ephemeralFlag returns MessageFlagsEphemeral if the subcommand should be +// ephemeral, or 0 otherwise. +func ephemeralFlag(i *discordgo.InteractionCreate) discordgo.MessageFlags { + data := i.ApplicationCommandData() + if len(data.Options) > 0 { + if ephemeralCommands[data.Options[0].Name] { + return discordgo.MessageFlagsEphemeral + } + } + return 0 +} + +// HandleSlashCommand dispatches a slash command interaction to the +// appropriate handler. Simple commands that don't need async Hub API +// calls respond immediately; others defer and process asynchronously. +func (h *CommandHandler) HandleSlashCommand(s *discordgo.Session, i *discordgo.InteractionCreate) { + data := i.ApplicationCommandData() + if data.Name != "scion" || len(data.Options) == 0 { + return + } + + subcommand := data.Options[0].Name + + // Commands that don't need async Hub API calls respond immediately. + if subcommand == "help" { + h.respondImmediate(s, i, helpText()) + return + } + + // All other commands defer — Discord requires a response within 3 seconds. + err := s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{ + Type: discordgo.InteractionResponseDeferredChannelMessageWithSource, + Data: &discordgo.InteractionResponseData{ + Flags: ephemeralFlag(i), + }, + }) + if err != nil { + h.log.Error("Failed to acknowledge slash command", "error", err) + return + } + + go func() { + switch subcommand { + case "setup": + h.HandleSetup(s, i) + case "unlink": + h.HandleUnlink(s, i) + case "agents": + h.HandleAgents(s, i) + case "info": + h.HandleInfo(s, i) + case "status": + h.HandleStatus(s, i) + case "start": + h.HandleStart(s, i) + case "stop": + h.HandleStop(s, i) + case "msg": + h.HandleMessage(s, i) + case "logs": + h.HandleLogs(s, i) + case "settings": + h.HandleSettings(s, i) + case "default": + h.HandleDefault(s, i) + // register and unregister are handled by RegistrationHandler + // and should be wired up in the broker's dispatch + default: + h.followup(s, i, fmt.Sprintf("Unknown subcommand: %s", subcommand)) + } + }() +} + +// HandleAutocomplete handles autocomplete interactions for the "agent" +// option. It looks up the channel link, fetches agents, and returns +// matching choices. +func (h *CommandHandler) HandleAutocomplete(s *discordgo.Session, i *discordgo.InteractionCreate) { + data := i.ApplicationCommandData() + if len(data.Options) == 0 { + return + } + + sub := data.Options[0] + + for _, opt := range sub.Options { + if !opt.Focused { + continue + } + if opt.Name != "agent" { + continue + } + + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + + link, err := h.store.GetChannelLink(ctx, i.ChannelID) + if err != nil || link == nil { + // No link — return empty choices. + _ = s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{ + Type: discordgo.InteractionApplicationCommandAutocompleteResult, + Data: &discordgo.InteractionResponseData{}, + }) + return + } + + agents, err := h.getAgents(ctx, link.ProjectID) + if err != nil { + h.log.Debug("Failed to get agents for autocomplete", "error", err) + _ = s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{ + Type: discordgo.InteractionApplicationCommandAutocompleteResult, + Data: &discordgo.InteractionResponseData{}, + }) + return + } + + prefix := strings.ToLower(opt.StringValue()) + var choices []*discordgo.ApplicationCommandOptionChoice + + for _, slug := range agents { + if strings.HasPrefix(strings.ToLower(slug), prefix) { + choices = append(choices, &discordgo.ApplicationCommandOptionChoice{ + Name: slug, + Value: slug, + }) + } + if len(choices) >= 25 { + break + } + } + + _ = s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{ + Type: discordgo.InteractionApplicationCommandAutocompleteResult, + Data: &discordgo.InteractionResponseData{Choices: choices}, + }) + return + } +} + +// helpText returns the help message listing available commands. +func helpText() string { + return "**Scion Bot Commands**\n\n" + + "`/scion setup` — Link this channel to a Scion project\n" + + "`/scion unlink` — Unlink this channel from its project\n" + + "`/scion agents` — List agents in the linked project\n" + + "`/scion status ` — Show agent status\n" + + "`/scion start ` — Start an agent\n" + + "`/scion stop ` — Stop an agent\n" + + "`/scion message ` — Send a message to an agent\n" + + "`/scion logs ` — View agent logs\n" + + "`/scion default` — Set or clear the default agent\n" + + "`/scion register` — Link your Discord account to Scion Hub\n" + + "`/scion unregister` — Unlink your Discord account\n" + + "`/scion settings` — Configure channel notification settings\n" + + "`/scion info` — Show your registration info\n" + + "`/scion help` — Show this help message\n\n" + + "Mention the bot or an agent by name in a linked channel to send messages." +} + +// HandleHelp responds with a listing of available commands. +// Used as a fallback when the command is dispatched via the deferred path. +func (h *CommandHandler) HandleHelp(s *discordgo.Session, i *discordgo.InteractionCreate) { + h.followup(s, i, helpText()) +} + +// respondImmediate sends an immediate (non-deferred) response to an +// interaction, suitable for commands that don't need async processing. +func (h *CommandHandler) respondImmediate(s *discordgo.Session, i *discordgo.InteractionCreate, content string) { + err := s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{ + Type: discordgo.InteractionResponseChannelMessageWithSource, + Data: &discordgo.InteractionResponseData{ + Content: content, + Flags: ephemeralFlag(i), + }, + }) + if err != nil { + h.log.Error("Failed to send immediate response", "error", err) + } +} + +// HandleSetup starts the channel setup flow: check permissions, check +// registration, list projects, and present selection buttons. +func (h *CommandHandler) HandleSetup(s *discordgo.Session, i *discordgo.InteractionCreate) { + // Check Discord permissions. + if !hasChannelAdminPermission(i) { + h.followup(s, i, "You need **Manage Channels** or **Administrator** permission to set up this channel.") + return + } + + ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second) + defer cancel() + + // Check if user is registered. + discordUserID := "" + discordUsername := "" + if i.Member != nil && i.Member.User != nil { + discordUserID = i.Member.User.ID + discordUsername = i.Member.User.Username + } else if i.User != nil { + discordUserID = i.User.ID + discordUsername = i.User.Username + } + + if discordUserID == "" { + h.followup(s, i, "Could not identify your user.") + return + } + + mapping, err := h.store.GetUserMapping(ctx, discordUserID) + if err != nil { + h.log.Error("Failed to check user mapping", "error", err, "discord_user_id", discordUserID) + h.followup(s, i, "Something went wrong. Please try again.") + return + } + if mapping == nil { + h.followup(s, i, "Please link your Discord account first with `/scion register`.") + return + } + + // Check existing link. + link, err := h.store.GetChannelLink(ctx, i.ChannelID) + if err != nil { + h.log.Error("Failed to check channel link", "error", err, "channel_id", i.ChannelID) + h.followup(s, i, "Something went wrong. Please try again.") + return + } + if link != nil { + h.followup(s, i, fmt.Sprintf( + "This channel is already linked to project **%s**.\nUse `/scion unlink` first to change it.", + link.ProjectSlug, + )) + return + } + + // Get user's projects. + var projects []ProjectOption + if mapping.ScionUserID != "" { + projects, err = h.hubClient.ListProjectsForUser(ctx, mapping.ScionUserID) + if err != nil { + h.log.Warn("Failed to list user projects", "error", err, "user_id", mapping.ScionUserID) + } + } + + if len(projects) == 0 { + projects, err = h.hubClient.ListProjectsFresh(ctx) + if err != nil { + h.log.Warn("Failed to list projects from hub", "error", err) + } + } + + if len(projects) == 0 { + h.followup(s, i, "No projects found. Create a project in the hub first.") + return + } + + // Build button rows for project selection (max 5 buttons per row, max 5 rows). + var rows []discordgo.MessageComponent + var buttons []discordgo.MessageComponent + for idx, proj := range projects { + buttons = append(buttons, discordgo.Button{ + Label: proj.DisplayName(), + Style: discordgo.PrimaryButton, + CustomID: fmt.Sprintf("setup:proj:%s", proj.ID), + }) + if len(buttons) == 5 || idx == len(projects)-1 { + rows = append(rows, discordgo.ActionsRow{Components: buttons}) + buttons = nil + } + // Discord max 5 action rows per message. + if len(rows) >= 5 { + break + } + } + + _, _ = s.FollowupMessageCreate(i.Interaction, true, &discordgo.WebhookParams{ + Content: "Select a project to link this channel to:", + Components: rows, + }) + + h.log.Info("Setup initiated", + "channel_id", i.ChannelID, + "discord_user", discordUsername, + "project_count", len(projects), + ) +} + +// HandleUnlink removes the channel-to-project link. +func (h *CommandHandler) HandleUnlink(s *discordgo.Session, i *discordgo.InteractionCreate) { + if !hasChannelAdminPermission(i) { + h.followup(s, i, "You need **Manage Channels** or **Administrator** permission to unlink this channel.") + return + } + + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) + defer cancel() + + link, err := h.store.GetChannelLink(ctx, i.ChannelID) + if err != nil { + h.log.Error("Failed to check channel link", "error", err) + h.followup(s, i, "Something went wrong. Please try again.") + return + } + if link == nil { + h.followup(s, i, "This channel is not linked to a project.") + return + } + + if err := h.store.DeleteChannelLink(ctx, i.ChannelID); err != nil { + h.log.Error("Failed to delete channel link", "error", err, "channel_id", i.ChannelID) + h.followup(s, i, "Failed to unlink. Please try again.") + return + } + + h.followup(s, i, fmt.Sprintf("Channel unlinked from project **%s**.", link.ProjectSlug)) + h.log.Info("Channel unlinked", "channel_id", i.ChannelID, "project", link.ProjectSlug) +} + +// HandleAgents lists agents in the linked project. +func (h *CommandHandler) HandleAgents(s *discordgo.Session, i *discordgo.InteractionCreate) { + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) + defer cancel() + + link, err := h.store.GetChannelLink(ctx, i.ChannelID) + if err != nil { + h.log.Error("Failed to get channel link", "error", err, "channel_id", i.ChannelID) + h.followup(s, i, "Something went wrong. Please try again.") + return + } + if link == nil { + h.followup(s, i, "This channel is not linked to a project. Use `/scion setup` first.") + return + } + + agents, err := h.hubClient.ListAgents(ctx, link.ProjectID) + if err != nil { + h.log.Error("Failed to list agents", "error", err, "project_id", link.ProjectID) + h.followup(s, i, "Failed to fetch agents. Please try again later.") + return + } + + if len(agents) == 0 { + h.followup(s, i, "No agents found for this project.") + return + } + + var lines []string + for _, agent := range agents { + emoji := activityEmoji(agent.Activity) + label := agent.Slug + if agent.Activity != "" { + label += " -- " + agent.Activity + } + if agent.Slug == link.DefaultAgent { + label += " (default)" + } + lines = append(lines, fmt.Sprintf("%s %s", emoji, label)) + } + + h.followup(s, i, fmt.Sprintf("**Agents in %s:**\n%s", link.ProjectSlug, strings.Join(lines, "\n"))) +} + +// HandleInfo shows the user's registration status and linked project info. +func (h *CommandHandler) HandleInfo(s *discordgo.Session, i *discordgo.InteractionCreate) { + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) + defer cancel() + + discordUserID := interactionUserID(i) + if discordUserID == "" { + h.followup(s, i, "Could not identify your user.") + return + } + + mapping, err := h.store.GetUserMapping(ctx, discordUserID) + if err != nil { + h.log.Error("Failed to check user mapping", "error", err) + h.followup(s, i, "Something went wrong. Please try again.") + return + } + + var sb strings.Builder + if mapping == nil { + sb.WriteString("**Registration:** Not registered\n") + sb.WriteString("Use `/scion register` to link your Discord account to Scion Hub.") + } else { + sb.WriteString("**Registration:** Linked\n") + if mapping.ScionEmail != "" { + sb.WriteString(fmt.Sprintf("**Email:** %s\n", mapping.ScionEmail)) + } + if mapping.ScionUserID != "" { + sb.WriteString(fmt.Sprintf("**User ID:** %s\n", mapping.ScionUserID)) + } + sb.WriteString(fmt.Sprintf("**Linked at:** %s\n", mapping.LinkedAt.UTC().Format(time.RFC3339))) + } + + // Show channel link if in a guild channel. + if i.ChannelID != "" { + link, linkErr := h.store.GetChannelLink(ctx, i.ChannelID) + if linkErr == nil && link != nil { + sb.WriteString(fmt.Sprintf("\n**Channel project:** %s", link.ProjectSlug)) + if link.DefaultAgent != "" { + sb.WriteString(fmt.Sprintf("\n**Default agent:** %s", link.DefaultAgent)) + } + } + } + + h.followup(s, i, sb.String()) +} + +// HandleStatus shows the status of a specific agent. +func (h *CommandHandler) HandleStatus(s *discordgo.Session, i *discordgo.InteractionCreate) { + agentSlug := getSubcommandOption(i, "agent") + if agentSlug == "" { + h.followup(s, i, "Please specify an agent name.") + return + } + + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) + defer cancel() + + link, err := h.store.GetChannelLink(ctx, i.ChannelID) + if err != nil || link == nil { + h.followup(s, i, "This channel is not linked to a project. Use `/scion setup` first.") + return + } + + agents, err := h.hubClient.ListAgents(ctx, link.ProjectID) + if err != nil { + h.followup(s, i, "Failed to fetch agent status. Please try again.") + return + } + + for _, agent := range agents { + if agent.Slug == agentSlug { + emoji := activityEmoji(agent.Activity) + activity := agent.Activity + if activity == "" { + activity = "unknown" + } + h.followup(s, i, fmt.Sprintf("%s **%s** -- %s", emoji, agent.Slug, activity)) + return + } + } + + h.followup(s, i, fmt.Sprintf("Agent **%s** not found in this project.", agentSlug)) +} + +// HandleStart is a placeholder for starting an agent (Phase 4). +func (h *CommandHandler) HandleStart(s *discordgo.Session, i *discordgo.InteractionCreate) { + agentSlug := getSubcommandOption(i, "agent") + if agentSlug == "" { + h.followup(s, i, "Please specify an agent name.") + return + } + h.followup(s, i, fmt.Sprintf("Starting agent **%s** is not yet implemented.", agentSlug)) +} + +// HandleStop is a placeholder for stopping an agent (Phase 4). +func (h *CommandHandler) HandleStop(s *discordgo.Session, i *discordgo.InteractionCreate) { + agentSlug := getSubcommandOption(i, "agent") + if agentSlug == "" { + h.followup(s, i, "Please specify an agent name.") + return + } + h.followup(s, i, fmt.Sprintf("Stopping agent **%s** is not yet implemented.", agentSlug)) +} + +// HandleMessage is a placeholder for sending a message to an agent (Phase 4). +func (h *CommandHandler) HandleMessage(s *discordgo.Session, i *discordgo.InteractionCreate) { + agentSlug := getSubcommandOption(i, "agent") + text := getSubcommandOption(i, "text") + if agentSlug == "" || text == "" { + h.followup(s, i, "Please specify both an agent name and message text.") + return + } + h.followup(s, i, fmt.Sprintf("Sending messages to agents via slash command is not yet implemented.\nAgent: **%s**\nMessage: %s", agentSlug, text)) +} + +// HandleLogs is a placeholder for viewing agent logs (Phase 4). +func (h *CommandHandler) HandleLogs(s *discordgo.Session, i *discordgo.InteractionCreate) { + agentSlug := getSubcommandOption(i, "agent") + if agentSlug == "" { + h.followup(s, i, "Please specify an agent name.") + return + } + h.followup(s, i, fmt.Sprintf("Viewing logs for agent **%s** is not yet implemented.", agentSlug)) +} + +// HandleDefault shows agent selection buttons for setting the default agent. +func (h *CommandHandler) HandleDefault(s *discordgo.Session, i *discordgo.InteractionCreate) { + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) + defer cancel() + + link, err := h.store.GetChannelLink(ctx, i.ChannelID) + if err != nil { + h.log.Error("Failed to get channel link", "error", err, "channel_id", i.ChannelID) + h.followup(s, i, "Something went wrong. Please try again.") + return + } + if link == nil { + h.followup(s, i, "This channel is not linked to a project. Use `/scion setup` first.") + return + } + + agents, err := h.getAgents(ctx, link.ProjectID) + if err != nil { + h.log.Error("Failed to list agents", "error", err, "project_id", link.ProjectID) + h.followup(s, i, "Failed to fetch agents. Please try again later.") + return + } + + if len(agents) == 0 { + h.followup(s, i, "No agents found in this project.") + return + } + + var currentText string + if link.DefaultAgent != "" { + currentText = fmt.Sprintf("Current default: **%s**\n", link.DefaultAgent) + } + + var rows []discordgo.MessageComponent + var buttons []discordgo.MessageComponent + for idx, slug := range agents { + style := discordgo.SecondaryButton + if slug == link.DefaultAgent { + style = discordgo.PrimaryButton + } + buttons = append(buttons, discordgo.Button{ + Label: slug, + Style: style, + CustomID: fmt.Sprintf("default:set:%s", slug), + }) + if len(buttons) == 5 || idx == len(agents)-1 { + rows = append(rows, discordgo.ActionsRow{Components: buttons}) + buttons = nil + } + if len(rows) >= 4 { + break + } + } + if len(rows) < 5 { + rows = append(rows, discordgo.ActionsRow{ + Components: []discordgo.MessageComponent{ + discordgo.Button{ + Label: "None", + Style: discordgo.DangerButton, + CustomID: "default:none", + }, + }, + }) + } + + _, _ = s.FollowupMessageCreate(i.Interaction, true, &discordgo.WebhookParams{ + Content: currentText + "Select the default agent for this channel:", + Components: rows, + }) +} + +// HandleSettings shows channel settings with toggle buttons. +func (h *CommandHandler) HandleSettings(s *discordgo.Session, i *discordgo.InteractionCreate) { + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) + defer cancel() + + link, err := h.store.GetChannelLink(ctx, i.ChannelID) + if err != nil { + h.log.Error("Failed to get channel link", "error", err, "channel_id", i.ChannelID) + h.followup(s, i, "Something went wrong. Please try again.") + return + } + if link == nil { + h.followup(s, i, "This channel is not linked to a project. Use `/scion setup` first.") + return + } + + content, components := settingsPanel(link) + _, _ = s.FollowupMessageCreate(i.Interaction, true, &discordgo.WebhookParams{ + Content: content, + Components: components, + }) +} + +// settingsPanel builds the settings message content and toggle buttons. +func settingsPanel(link *ChannelLink) (string, []discordgo.MessageComponent) { + observeLabel := "Observe Mode: OFF" + observeStyle := discordgo.SecondaryButton + if link.ShowAgentToAgent { + observeLabel = "Observe Mode: ON" + observeStyle = discordgo.SuccessButton + } + + stateLabel := "State Notifications: OFF" + stateStyle := discordgo.SecondaryButton + if link.ShowStateChanges { + stateLabel = "State Notifications: ON" + stateStyle = discordgo.SuccessButton + } + + content := fmt.Sprintf("**Channel Settings** — %s\n\n"+ + "**Observe Mode** — Show agent-to-agent messages in this channel\n"+ + "**State Notifications** — Show agent state change cards (working/idle/stalled)", + link.ProjectSlug) + + components := []discordgo.MessageComponent{ + discordgo.ActionsRow{ + Components: []discordgo.MessageComponent{ + discordgo.Button{ + Label: observeLabel, + Style: observeStyle, + CustomID: fmt.Sprintf("settings:observe:%s", link.ChannelID), + }, + discordgo.Button{ + Label: stateLabel, + Style: stateStyle, + CustomID: fmt.Sprintf("settings:statechange:%s", link.ChannelID), + }, + }, + }, + } + + return content, components +} + +// getAgents returns agent slugs for a project, using the store cache with +// a fallback to the hub API. +func (h *CommandHandler) getAgents(ctx context.Context, projectID string) ([]string, error) { + cached, err := h.store.GetProjectAgents(ctx, projectID) + if err != nil { + h.log.Warn("Failed to read agent cache", "project_id", projectID, "error", err) + } + if cached != nil && time.Since(cached.RefreshedAt) < h.agentCacheTTL { + return cached.AgentSlugs, nil + } + + agents, err := h.hubClient.ListAgents(ctx, projectID) + if err != nil { + if cached != nil { + return cached.AgentSlugs, nil + } + return nil, err + } + + slugs := make([]string, len(agents)) + for i, a := range agents { + slugs[i] = a.Slug + } + + saveErr := h.store.SetProjectAgents(ctx, &ProjectAgents{ + ProjectID: projectID, + AgentSlugs: slugs, + RefreshedAt: time.Now(), + }) + if saveErr != nil { + h.log.Warn("Failed to cache agents", "project_id", projectID, "error", saveErr) + } + + return slugs, nil +} + +// followup sends a follow-up message to the interaction. +func (h *CommandHandler) followup(s *discordgo.Session, i *discordgo.InteractionCreate, content string) { + _, err := s.FollowupMessageCreate(i.Interaction, true, &discordgo.WebhookParams{ + Content: content, + }) + if err != nil { + h.log.Error("Failed to send follow-up message", "error", err) + } +} + +// hasChannelAdminPermission checks if the invoking member has MANAGE_CHANNELS +// or ADMINISTRATOR permission. +func hasChannelAdminPermission(i *discordgo.InteractionCreate) bool { + if i.Member == nil { + return false + } + perms := i.Member.Permissions + return perms&discordgo.PermissionManageChannels != 0 || + perms&discordgo.PermissionAdministrator != 0 +} + +// getSubcommandOption extracts a named option value from a subcommand interaction. +func getSubcommandOption(i *discordgo.InteractionCreate, name string) string { + data := i.ApplicationCommandData() + if len(data.Options) == 0 { + return "" + } + sub := data.Options[0] + for _, opt := range sub.Options { + if opt.Name == name { + return opt.StringValue() + } + } + return "" +} + +// interactionUserID extracts the Discord user ID from an interaction, +// handling both guild (Member) and DM (User) contexts. +func interactionUserID(i *discordgo.InteractionCreate) string { + if i.Member != nil && i.Member.User != nil { + return i.Member.User.ID + } + if i.User != nil { + return i.User.ID + } + return "" +} + +// activityEmoji returns an emoji for an agent activity state. +func activityEmoji(activity string) string { + switch strings.ToLower(activity) { + case "idle": + return "💤" + case "executing": + return "⚙️" + case "thinking": + return "💭" + case "blocked": + return "🚧" + case "completed": + return "✅" + case "error": + return "❌" + case "stalled": + return "⏳" + default: + return "▶️" + } +} diff --git a/extras/scion-discord/internal/discord/format.go b/extras/scion-discord/internal/discord/format.go new file mode 100644 index 000000000..b0e419800 --- /dev/null +++ b/extras/scion-discord/internal/discord/format.go @@ -0,0 +1,403 @@ +package discord + +import ( + "encoding/json" + "fmt" + "strings" + "unicode/utf8" + + "github.com/bwmarrin/discordgo" + + "github.com/GoogleCloudPlatform/scion/pkg/messages" +) + +const ( + // maxDiscordMessageLength is the maximum character length for a Discord message. + maxDiscordMessageLength = 2000 + + // maxEmbedDescriptionLength is the maximum character length for an embed description. + maxEmbedDescriptionLength = 4096 + + // maxEmbedFieldValueLength is the maximum character length for an embed field value. + maxEmbedFieldValueLength = 1024 + + // maxEmbedTitleLength is the maximum character length for an embed title. + maxEmbedTitleLength = 256 + + // maxButtonsPerRow is the maximum number of buttons allowed in a single Discord action row. + maxButtonsPerRow = 5 + + // truncationSuffix is appended when a message exceeds the Discord limit. + truncationSuffix = "\n*[truncated]*" + + // headerBudget is a generous estimate of the byte overhead from header + // text (agent name, mentions, prefix tags). The body is truncated to + // leave room for the header so the total stays under the limit. + headerBudget = 100 +) + +// Embed sidebar colors keyed by activity/status string. +const ( + colorCompleted = 0x2ECC71 // Green + colorInputWait = 0xF1C40F // Yellow + colorError = 0xE74C3C // Red + colorStalled = 0xE67E22 // Orange + colorDeleted = 0x95A5A6 // Gray + colorRunning = 0x3498DB // Blue + colorDefault = 0x1A1A2E // Dark +) + +// FormatMessage converts a StructuredMessage to Discord-compatible text. +// For Phase 1, this is plain text formatting (embeds come in Phase 2). +func FormatMessage(msg *messages.StructuredMessage, agentSlug string, recipientMention string) string { + if msg == nil { + return "" + } + + var b strings.Builder + + // Determine sender slug for display. + slug := agentSlug + if slug == "" { + if strings.HasPrefix(msg.Sender, "agent:") { + slug = strings.TrimPrefix(msg.Sender, "agent:") + } else { + slug = msg.Sender + } + } + + // Header: agent identity and optional recipient. + isAgentToAgent := strings.HasPrefix(msg.Sender, "agent:") && strings.HasPrefix(msg.Recipient, "agent:") + if isAgentToAgent { + recipientSlug := strings.TrimPrefix(msg.Recipient, "agent:") + fmt.Fprintf(&b, "[agent:%s -> agent:%s]\n", slug, recipientSlug) + } else if recipientMention != "" { + fmt.Fprintf(&b, "**%s** -> %s\n", slug, recipientMention) + } else { + fmt.Fprintf(&b, "**%s**\n", slug) + } + + // Prefix tags. + if msg.Urgent { + b.WriteString("**[URGENT]** ") + } + if msg.Broadcasted { + b.WriteString("**[Broadcast]** ") + } + + // Body text, truncated to fit within the Discord limit. + body := msg.Msg + maxBody := maxDiscordMessageLength - b.Len() - len(truncationSuffix) + if maxBody < 0 { + maxBody = 0 + } + if len(body) > maxBody { + body = truncateAtRuneBoundary(body, maxBody) + body += truncationSuffix + } + b.WriteString(body) + + // Call-to-action for input-needed. + if msg.Type == messages.TypeInputNeeded { + b.WriteString("\n\nPlease reply to respond.") + } + + return truncateForDiscord(b.String(), maxDiscordMessageLength) +} + +// FormatStateChangeText formats a TypeStateChange as plain text (Phase 1). +// Phase 2 will use embeds with colored sidebars. +func FormatStateChangeText(msg *messages.StructuredMessage, agentSlug string) string { + if msg == nil { + return "" + } + + slug := agentSlug + if slug == "" { + if strings.HasPrefix(msg.Sender, "agent:") { + slug = strings.TrimPrefix(msg.Sender, "agent:") + } else { + slug = msg.Sender + } + } + + status := msg.Status + if status == "" { + status = "unknown" + } + + var b strings.Builder + fmt.Fprintf(&b, "[%s] **%s**", strings.ToUpper(status), slug) + + // Add activity from metadata if available. + if msg.Metadata != nil { + if activity, ok := msg.Metadata["activity"]; ok && activity != "" { + fmt.Fprintf(&b, " -- %s", activity) + } + } + + if msg.Msg != "" { + b.WriteString("\n") + b.WriteString(msg.Msg) + } + + return truncateForDiscord(b.String(), maxDiscordMessageLength) +} + +// truncateForDiscord ensures text fits within the specified character limit. +// If truncation is needed, it walks backward to a valid rune boundary and +// appends a truncation indicator. +func truncateForDiscord(text string, maxLen int) string { + if len(text) <= maxLen { + return text + } + cutoff := maxLen - len(truncationSuffix) + if cutoff < 0 { + cutoff = 0 + } + cutoff = truncateAtRuneBoundaryLen(text, cutoff) + return text[:cutoff] + truncationSuffix +} + +// truncateAtRuneBoundary truncates text to at most maxLen bytes, backing +// up to a valid UTF-8 rune boundary. +func truncateAtRuneBoundary(text string, maxLen int) string { + if len(text) <= maxLen { + return text + } + cutoff := maxLen + for cutoff > 0 && !utf8.RuneStart(text[cutoff]) { + cutoff-- + } + return text[:cutoff] +} + +// truncateAtRuneBoundaryLen returns a byte offset <= maxLen that sits on +// a valid UTF-8 rune boundary. +func truncateAtRuneBoundaryLen(text string, maxLen int) int { + if maxLen >= len(text) { + return len(text) + } + cutoff := maxLen + for cutoff > 0 && !utf8.RuneStart(text[cutoff]) { + cutoff-- + } + return cutoff +} + +// FormatDiscordMention formats a Discord user mention from a user ID. +func FormatDiscordMention(discordUserID string) string { + return fmt.Sprintf("<@%s>", discordUserID) +} + +// activityColor returns the embed sidebar color for the given activity/status. +func activityColor(activity string) int { + switch activity { + case "COMPLETED": + return colorCompleted + case "WAITING_FOR_INPUT": + return colorInputWait + case "ERROR": + return colorError + case "STALLED", "LIMITS_EXCEEDED": + return colorStalled + case "DELETED": + return colorDeleted + case "RUNNING": + return colorRunning + default: + return colorDefault + } +} + +// RenderStateChangeEmbed builds a colored Discord embed for a TypeStateChange message. +// The sidebar color reflects the agent's current activity/status. +func RenderStateChangeEmbed(msg *messages.StructuredMessage, agentSlug string) *discordgo.MessageEmbed { + if msg == nil { + return nil + } + + activity := "" + projectID := "" + summary := "" + if msg.Metadata != nil { + activity = msg.Metadata["activity"] + projectID = msg.Metadata["project_id"] + summary = msg.Metadata["summary"] + } + + title := agentSlug + if activity != "" { + title = fmt.Sprintf("%s — %s", agentSlug, activity) + } + title = truncateForDiscord(title, maxEmbedTitleLength) + + description := msg.Msg + if len(description) > maxEmbedDescriptionLength { + description = truncateForDiscord(description, maxEmbedDescriptionLength) + } + + embed := &discordgo.MessageEmbed{ + Title: title, + Description: description, + Color: activityColor(activity), + Timestamp: msg.Timestamp, + } + + if projectID != "" { + embed.Footer = &discordgo.MessageEmbedFooter{ + Text: fmt.Sprintf("Project: %s", projectID), + } + } + + if summary != "" { + if len(summary) > maxEmbedFieldValueLength { + summary = truncateForDiscord(summary, maxEmbedFieldValueLength) + } + embed.Fields = append(embed.Fields, &discordgo.MessageEmbedField{ + Name: "Summary", + Value: summary, + }) + } + + return embed +} + +// RenderInputNeeded builds an embed and interactive components for a TypeInputNeeded message. +// If msg.Metadata["choices"] contains a JSON array of strings, each choice is rendered as a +// button. Otherwise, a generic "Reply" and "Dismiss" button pair is returned. +func RenderInputNeeded(msg *messages.StructuredMessage, agentSlug, requestID string) (*discordgo.MessageEmbed, []discordgo.MessageComponent) { + if msg == nil { + return nil, nil + } + + description := msg.Msg + if len(description) > maxEmbedDescriptionLength { + description = truncateForDiscord(description, maxEmbedDescriptionLength) + } + + embed := &discordgo.MessageEmbed{ + Title: fmt.Sprintf("Input Needed — %s", agentSlug), + Description: description, + Color: colorInputWait, + } + + var components []discordgo.MessageComponent + + choicesJSON := "" + if msg.Metadata != nil { + choicesJSON = msg.Metadata["choices"] + } + + if choicesJSON != "" { + var choices []string + if err := json.Unmarshal([]byte(choicesJSON), &choices); err == nil && len(choices) > 0 { + var buttons []discordgo.MessageComponent + for idx, choice := range choices { + buttons = append(buttons, discordgo.Button{ + Label: choice, + Style: discordgo.PrimaryButton, + CustomID: fmt.Sprintf("ask:opt:%s:%d", requestID, idx), + }) + if len(buttons) == maxButtonsPerRow || idx == len(choices)-1 { + components = append(components, discordgo.ActionsRow{ + Components: buttons, + }) + buttons = nil + } + } + return embed, components + } + } + + // Default: Reply + Dismiss buttons. + components = append(components, discordgo.ActionsRow{ + Components: []discordgo.MessageComponent{ + discordgo.Button{ + Label: "Reply", + Style: discordgo.PrimaryButton, + CustomID: fmt.Sprintf("ask:reply:%s", requestID), + }, + discordgo.Button{ + Label: "Dismiss", + Style: discordgo.SecondaryButton, + CustomID: fmt.Sprintf("ask:dismiss:%s", requestID), + }, + }, + }) + return embed, components +} + +// FormatWithEmbed decides whether to return plain text, an embed, or both, +// based on the message length. +// +// - ≤2000 chars: plain text content, no embeds +// - ≤4096 chars: empty content, single embed with description +// - >4096 chars: first 4096 in an embed, remainder returned as plain text +// (caller is responsible for splitting the remainder into ≤2000-char chunks +// via SplitLongMessage) +func FormatWithEmbed(msg *messages.StructuredMessage, agentSlug string) (string, []*discordgo.MessageEmbed) { + if msg == nil { + return "", nil + } + + body := msg.Msg + if len(body) <= maxDiscordMessageLength { + return body, nil + } + + if len(body) <= maxEmbedDescriptionLength { + embed := &discordgo.MessageEmbed{ + Description: body, + } + return "", []*discordgo.MessageEmbed{embed} + } + + // Body exceeds embed description limit: put the first 4096 in an embed, + // return the remainder as content text (possibly requiring further splitting). + cutoff := maxEmbedDescriptionLength - len(truncationSuffix) + cutoff = truncateAtRuneBoundaryLen(body, cutoff) + embedText := body[:cutoff] + truncationSuffix + + remainder := body[cutoff:] + + embed := &discordgo.MessageEmbed{ + Description: embedText, + } + return remainder, []*discordgo.MessageEmbed{embed} +} + +// SplitLongMessage splits text into chunks of at most maxLen characters. +// It prefers to split at newline boundaries. If no newline is found within +// the window, it falls back to splitting at maxLen on a rune boundary. +func SplitLongMessage(text string, maxLen int) []string { + if maxLen <= 0 { + maxLen = maxDiscordMessageLength + } + + var chunks []string + for len(text) > 0 { + if len(text) <= maxLen { + chunks = append(chunks, text) + break + } + + // Look for the last newline within the allowed window. + cutoff := maxLen + if cutoff > len(text) { + cutoff = len(text) + } + splitAt := strings.LastIndex(text[:cutoff], "\n") + if splitAt <= 0 { + // No suitable newline — split at rune boundary. + splitAt = truncateAtRuneBoundaryLen(text, maxLen) + } else { + // Include the newline in the current chunk. + splitAt++ + } + + chunks = append(chunks, text[:splitAt]) + text = text[splitAt:] + } + return chunks +} diff --git a/extras/scion-discord/internal/discord/format_test.go b/extras/scion-discord/internal/discord/format_test.go new file mode 100644 index 000000000..0ebde6f9d --- /dev/null +++ b/extras/scion-discord/internal/discord/format_test.go @@ -0,0 +1,443 @@ +package discord + +import ( + "encoding/json" + "strings" + "testing" + + "github.com/bwmarrin/discordgo" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/GoogleCloudPlatform/scion/pkg/messages" +) + +// --------------------------------------------------------------------------- +// activityColor +// --------------------------------------------------------------------------- + +func TestActivityColor_KnownStatuses(t *testing.T) { + tests := []struct { + activity string + want int + }{ + {"COMPLETED", colorCompleted}, + {"WAITING_FOR_INPUT", colorInputWait}, + {"ERROR", colorError}, + {"STALLED", colorStalled}, + {"LIMITS_EXCEEDED", colorStalled}, + {"DELETED", colorDeleted}, + {"RUNNING", colorRunning}, + } + for _, tt := range tests { + t.Run(tt.activity, func(t *testing.T) { + assert.Equal(t, tt.want, activityColor(tt.activity)) + }) + } +} + +func TestActivityColor_UnknownFallsToDefault(t *testing.T) { + assert.Equal(t, colorDefault, activityColor("UNKNOWN")) + assert.Equal(t, colorDefault, activityColor("")) +} + +// --------------------------------------------------------------------------- +// RenderStateChangeEmbed +// --------------------------------------------------------------------------- + +func TestRenderStateChangeEmbed_NilMessage(t *testing.T) { + assert.Nil(t, RenderStateChangeEmbed(nil, "coder")) +} + +func TestRenderStateChangeEmbed_Basic(t *testing.T) { + msg := &messages.StructuredMessage{ + Msg: "Deployment finished", + Timestamp: "2026-06-03T10:00:00Z", + Metadata: map[string]string{ + "activity": "COMPLETED", + "project_id": "my-project", + }, + } + + embed := RenderStateChangeEmbed(msg, "deploy-agent") + require.NotNil(t, embed) + assert.Equal(t, "deploy-agent — COMPLETED", embed.Title) + assert.Equal(t, "Deployment finished", embed.Description) + assert.Equal(t, colorCompleted, embed.Color) + assert.Equal(t, "2026-06-03T10:00:00Z", embed.Timestamp) + require.NotNil(t, embed.Footer) + assert.Equal(t, "Project: my-project", embed.Footer.Text) + assert.Empty(t, embed.Fields) +} + +func TestRenderStateChangeEmbed_WithSummary(t *testing.T) { + msg := &messages.StructuredMessage{ + Msg: "Agent is running", + Metadata: map[string]string{ + "activity": "RUNNING", + "summary": "Processing 42 files", + }, + } + + embed := RenderStateChangeEmbed(msg, "coder") + require.NotNil(t, embed) + assert.Equal(t, colorRunning, embed.Color) + require.Len(t, embed.Fields, 1) + assert.Equal(t, "Summary", embed.Fields[0].Name) + assert.Equal(t, "Processing 42 files", embed.Fields[0].Value) +} + +func TestRenderStateChangeEmbed_NoActivity(t *testing.T) { + msg := &messages.StructuredMessage{ + Msg: "Something happened", + } + + embed := RenderStateChangeEmbed(msg, "agent") + require.NotNil(t, embed) + assert.Equal(t, "agent", embed.Title) + assert.Equal(t, colorDefault, embed.Color) +} + +func TestRenderStateChangeEmbed_NoFooterWithoutProjectID(t *testing.T) { + msg := &messages.StructuredMessage{ + Msg: "Something happened", + Metadata: map[string]string{"activity": "ERROR"}, + } + + embed := RenderStateChangeEmbed(msg, "agent") + require.NotNil(t, embed) + assert.Nil(t, embed.Footer) +} + +func TestRenderStateChangeEmbed_TruncatesLongDescription(t *testing.T) { + longMsg := strings.Repeat("a", 5000) + msg := &messages.StructuredMessage{ + Msg: longMsg, + Metadata: map[string]string{"activity": "RUNNING"}, + } + + embed := RenderStateChangeEmbed(msg, "coder") + require.NotNil(t, embed) + assert.LessOrEqual(t, len(embed.Description), maxEmbedDescriptionLength) + assert.True(t, strings.HasSuffix(embed.Description, truncationSuffix)) +} + +func TestRenderStateChangeEmbed_TruncatesLongSummary(t *testing.T) { + longSummary := strings.Repeat("x", 2000) + msg := &messages.StructuredMessage{ + Msg: "short", + Metadata: map[string]string{ + "activity": "COMPLETED", + "summary": longSummary, + }, + } + + embed := RenderStateChangeEmbed(msg, "agent") + require.NotNil(t, embed) + require.Len(t, embed.Fields, 1) + assert.LessOrEqual(t, len(embed.Fields[0].Value), maxEmbedFieldValueLength) +} + +// --------------------------------------------------------------------------- +// RenderInputNeeded +// --------------------------------------------------------------------------- + +func TestRenderInputNeeded_NilMessage(t *testing.T) { + embed, components := RenderInputNeeded(nil, "coder", "req-1") + assert.Nil(t, embed) + assert.Nil(t, components) +} + +func TestRenderInputNeeded_WithChoices(t *testing.T) { + choices := []string{"Yes", "No", "Maybe"} + choicesJSON, _ := json.Marshal(choices) + + msg := &messages.StructuredMessage{ + Msg: "Do you approve?", + Metadata: map[string]string{ + "choices": string(choicesJSON), + }, + } + + embed, components := RenderInputNeeded(msg, "reviewer", "req-abc") + require.NotNil(t, embed) + assert.Equal(t, "Input Needed — reviewer", embed.Title) + assert.Equal(t, "Do you approve?", embed.Description) + assert.Equal(t, colorInputWait, embed.Color) + + // 3 choices should fit in 1 action row. + require.Len(t, components, 1) + row, ok := components[0].(discordgo.ActionsRow) + require.True(t, ok) + assert.Len(t, row.Components, 3) + + // Verify button custom IDs. + for idx, comp := range row.Components { + btn, ok := comp.(discordgo.Button) + require.True(t, ok) + assert.Equal(t, choices[idx], btn.Label) + assert.Equal(t, discordgo.PrimaryButton, btn.Style) + assert.Contains(t, btn.CustomID, "ask:opt:req-abc:") + } +} + +func TestRenderInputNeeded_WithChoicesMultipleRows(t *testing.T) { + // 7 choices should produce 2 action rows (5 + 2). + choices := []string{"A", "B", "C", "D", "E", "F", "G"} + choicesJSON, _ := json.Marshal(choices) + + msg := &messages.StructuredMessage{ + Msg: "Pick one", + Metadata: map[string]string{ + "choices": string(choicesJSON), + }, + } + + _, components := RenderInputNeeded(msg, "agent", "req-2") + require.Len(t, components, 2) + + row1, ok := components[0].(discordgo.ActionsRow) + require.True(t, ok) + assert.Len(t, row1.Components, 5) + + row2, ok := components[1].(discordgo.ActionsRow) + require.True(t, ok) + assert.Len(t, row2.Components, 2) +} + +func TestRenderInputNeeded_WithoutChoices(t *testing.T) { + msg := &messages.StructuredMessage{ + Msg: "What should I do next?", + } + + embed, components := RenderInputNeeded(msg, "coder", "req-xyz") + require.NotNil(t, embed) + assert.Equal(t, "Input Needed — coder", embed.Title) + assert.Equal(t, colorInputWait, embed.Color) + + // Default: 1 action row with Reply + Dismiss. + require.Len(t, components, 1) + row, ok := components[0].(discordgo.ActionsRow) + require.True(t, ok) + require.Len(t, row.Components, 2) + + replyBtn, ok := row.Components[0].(discordgo.Button) + require.True(t, ok) + assert.Equal(t, "Reply", replyBtn.Label) + assert.Equal(t, discordgo.PrimaryButton, replyBtn.Style) + assert.Equal(t, "ask:reply:req-xyz", replyBtn.CustomID) + + dismissBtn, ok := row.Components[1].(discordgo.Button) + require.True(t, ok) + assert.Equal(t, "Dismiss", dismissBtn.Label) + assert.Equal(t, discordgo.SecondaryButton, dismissBtn.Style) + assert.Equal(t, "ask:dismiss:req-xyz", dismissBtn.CustomID) +} + +func TestRenderInputNeeded_InvalidChoicesJSON(t *testing.T) { + msg := &messages.StructuredMessage{ + Msg: "Choose something", + Metadata: map[string]string{ + "choices": "not-valid-json", + }, + } + + embed, components := RenderInputNeeded(msg, "agent", "req-bad") + require.NotNil(t, embed) + + // Falls back to Reply + Dismiss. + require.Len(t, components, 1) + row, ok := components[0].(discordgo.ActionsRow) + require.True(t, ok) + assert.Len(t, row.Components, 2) +} + +func TestRenderInputNeeded_EmptyChoicesArray(t *testing.T) { + choicesJSON, _ := json.Marshal([]string{}) + msg := &messages.StructuredMessage{ + Msg: "Choose", + Metadata: map[string]string{ + "choices": string(choicesJSON), + }, + } + + _, components := RenderInputNeeded(msg, "agent", "req-empty") + + // Falls back to Reply + Dismiss. + require.Len(t, components, 1) + row, ok := components[0].(discordgo.ActionsRow) + require.True(t, ok) + assert.Len(t, row.Components, 2) +} + +// --------------------------------------------------------------------------- +// FormatWithEmbed +// --------------------------------------------------------------------------- + +func TestFormatWithEmbed_NilMessage(t *testing.T) { + content, embeds := FormatWithEmbed(nil, "agent") + assert.Equal(t, "", content) + assert.Nil(t, embeds) +} + +func TestFormatWithEmbed_ShortMessage(t *testing.T) { + msg := &messages.StructuredMessage{ + Msg: "Hello world", + } + + content, embeds := FormatWithEmbed(msg, "agent") + assert.Equal(t, "Hello world", content) + assert.Nil(t, embeds) +} + +func TestFormatWithEmbed_ExactlyAtMessageLimit(t *testing.T) { + msg := &messages.StructuredMessage{ + Msg: strings.Repeat("a", maxDiscordMessageLength), + } + + content, embeds := FormatWithEmbed(msg, "agent") + assert.Equal(t, msg.Msg, content) + assert.Nil(t, embeds) +} + +func TestFormatWithEmbed_JustOverMessageLimit(t *testing.T) { + msg := &messages.StructuredMessage{ + Msg: strings.Repeat("a", maxDiscordMessageLength+1), + } + + content, embeds := FormatWithEmbed(msg, "agent") + assert.Equal(t, "", content) + require.Len(t, embeds, 1) + assert.Equal(t, msg.Msg, embeds[0].Description) +} + +func TestFormatWithEmbed_AtEmbedLimit(t *testing.T) { + msg := &messages.StructuredMessage{ + Msg: strings.Repeat("b", maxEmbedDescriptionLength), + } + + content, embeds := FormatWithEmbed(msg, "agent") + assert.Equal(t, "", content) + require.Len(t, embeds, 1) + assert.Equal(t, msg.Msg, embeds[0].Description) +} + +func TestFormatWithEmbed_OverEmbedLimit(t *testing.T) { + msg := &messages.StructuredMessage{ + Msg: strings.Repeat("c", maxEmbedDescriptionLength+500), + } + + content, embeds := FormatWithEmbed(msg, "agent") + // Content should be the remainder beyond the embed. + assert.NotEmpty(t, content) + require.Len(t, embeds, 1) + // Embed description should be truncated with suffix. + assert.LessOrEqual(t, len(embeds[0].Description), maxEmbedDescriptionLength) + assert.True(t, strings.HasSuffix(embeds[0].Description, truncationSuffix)) + // Content + embed description (minus suffix) should cover the full message. + assert.Greater(t, len(content)+len(embeds[0].Description), maxEmbedDescriptionLength) +} + +// --------------------------------------------------------------------------- +// SplitLongMessage +// --------------------------------------------------------------------------- + +func TestSplitLongMessage_ShortText(t *testing.T) { + chunks := SplitLongMessage("hello", 100) + assert.Equal(t, []string{"hello"}, chunks) +} + +func TestSplitLongMessage_ExactFit(t *testing.T) { + text := strings.Repeat("a", 10) + chunks := SplitLongMessage(text, 10) + assert.Equal(t, []string{text}, chunks) +} + +func TestSplitLongMessage_SplitAtNewline(t *testing.T) { + text := "line1\nline2\nline3\nline4" + chunks := SplitLongMessage(text, 12) + // "line1\nline2\n" is 12 chars, exactly at the limit. + require.Len(t, chunks, 2) + assert.Equal(t, "line1\nline2\n", chunks[0]) + assert.Equal(t, "line3\nline4", chunks[1]) +} + +func TestSplitLongMessage_NoNewline(t *testing.T) { + text := strings.Repeat("x", 30) + chunks := SplitLongMessage(text, 10) + require.Len(t, chunks, 3) + for _, chunk := range chunks { + assert.LessOrEqual(t, len(chunk), 10) + } + assert.Equal(t, text, strings.Join(chunks, "")) +} + +func TestSplitLongMessage_EmptyText(t *testing.T) { + chunks := SplitLongMessage("", 100) + assert.Nil(t, chunks) +} + +func TestSplitLongMessage_ZeroMaxLen(t *testing.T) { + // maxLen <= 0 should default to maxDiscordMessageLength. + text := strings.Repeat("a", maxDiscordMessageLength+10) + chunks := SplitLongMessage(text, 0) + require.Len(t, chunks, 2) + assert.Equal(t, maxDiscordMessageLength, len(chunks[0])) +} + +func TestSplitLongMessage_PreservesContent(t *testing.T) { + text := "aaaa\nbbbb\ncccc\ndddd\neeee" + chunks := SplitLongMessage(text, 10) + reconstructed := strings.Join(chunks, "") + assert.Equal(t, text, reconstructed) +} + +// --------------------------------------------------------------------------- +// Existing format tests +// --------------------------------------------------------------------------- + +func TestFormatMessage_NilMessage(t *testing.T) { + assert.Equal(t, "", FormatMessage(nil, "agent", "")) +} + +func TestFormatMessage_BasicMessage(t *testing.T) { + msg := &messages.StructuredMessage{ + Sender: "agent:coder", + Msg: "Hello world", + } + result := FormatMessage(msg, "coder", "") + assert.Contains(t, result, "**coder**") + assert.Contains(t, result, "Hello world") +} + +func TestFormatStateChangeText_NilMessage(t *testing.T) { + assert.Equal(t, "", FormatStateChangeText(nil, "agent")) +} + +func TestFormatStateChangeText_WithActivity(t *testing.T) { + msg := &messages.StructuredMessage{ + Sender: "agent:deploy", + Status: "running", + Msg: "Deploying to staging", + Metadata: map[string]string{ + "activity": "deploying", + }, + } + result := FormatStateChangeText(msg, "deploy") + assert.Contains(t, result, "[RUNNING]") + assert.Contains(t, result, "**deploy**") + assert.Contains(t, result, "deploying") + assert.Contains(t, result, "Deploying to staging") +} + +func TestTruncateForDiscord_NoTruncation(t *testing.T) { + text := "short text" + assert.Equal(t, text, truncateForDiscord(text, 100)) +} + +func TestTruncateForDiscord_Truncates(t *testing.T) { + text := strings.Repeat("a", 2100) + result := truncateForDiscord(text, 2000) + assert.LessOrEqual(t, len(result), 2000) + assert.True(t, strings.HasSuffix(result, truncationSuffix)) +} diff --git a/extras/scion-discord/internal/discord/hubclient.go b/extras/scion-discord/internal/discord/hubclient.go new file mode 100644 index 000000000..c5ebca6d5 --- /dev/null +++ b/extras/scion-discord/internal/discord/hubclient.go @@ -0,0 +1,214 @@ +package discord + +import ( + "context" + "encoding/json" + "fmt" + "log/slog" + "net/http" + "time" + + "github.com/GoogleCloudPlatform/scion/pkg/apiclient" +) + +// httpHubClient implements HubClient using HTTP calls to the Hub API. +type httpHubClient struct { + hubURL string + hmacKey string + brokerID string + httpClient *http.Client +} + +// NewHTTPHubClient creates a new HubClient that calls the Scion Hub API. +func NewHTTPHubClient(hubURL, hmacKey, brokerID string) HubClient { + return &httpHubClient{ + hubURL: hubURL, + hmacKey: hmacKey, + brokerID: brokerID, + httpClient: &http.Client{Timeout: 15 * time.Second}, + } +} + +type hubProjectsResponse struct { + Projects []hubProject `json:"projects"` +} + +type hubProject struct { + ID string `json:"id"` + Name string `json:"name"` + Slug string `json:"slug"` +} + +type hubAgentsResponse struct { + Agents []hubAgent `json:"agents"` +} + +type hubAgent struct { + Slug string `json:"slug"` + Activity string `json:"activity"` +} + +func (c *httpHubClient) ListProjects(ctx context.Context) ([]ProjectOption, error) { + url := c.hubURL + "/api/v1/groves" + + slog.Debug("Listing projects from hub", "url", url, "broker_id", c.brokerID) + + req, err := http.NewRequestWithContext(ctx, "GET", url, nil) + if err != nil { + return nil, fmt.Errorf("create list projects request: %w", err) + } + + if err := c.signRequest(req); err != nil { + return nil, fmt.Errorf("sign request: %w", err) + } + + resp, err := c.httpClient.Do(req) + if err != nil { + return nil, fmt.Errorf("list projects request failed: %w", err) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + slog.Debug("Hub returned non-OK for list projects", "status", resp.StatusCode, "url", url) + return nil, fmt.Errorf("list projects returned status %d", resp.StatusCode) + } + + var result hubProjectsResponse + if err := json.NewDecoder(resp.Body).Decode(&result); err != nil { + return nil, fmt.Errorf("decode list projects response: %w", err) + } + + slog.Debug("Hub returned projects", "count", len(result.Projects)) + + projects := make([]ProjectOption, len(result.Projects)) + for i, p := range result.Projects { + projects[i] = ProjectOption{ID: p.ID, Name: p.Name, Slug: p.Slug} + } + return projects, nil +} + +func (c *httpHubClient) ListProjectsFresh(ctx context.Context) ([]ProjectOption, error) { + url := c.hubURL + "/api/v1/broker/projects" + + slog.Debug("Listing fresh projects from hub broker endpoint", "url", url, "broker_id", c.brokerID) + + req, err := http.NewRequestWithContext(ctx, "GET", url, nil) + if err != nil { + return nil, fmt.Errorf("create list fresh projects request: %w", err) + } + + if err := c.signRequest(req); err != nil { + return nil, fmt.Errorf("sign request: %w", err) + } + + resp, err := c.httpClient.Do(req) + if err != nil { + return nil, fmt.Errorf("list fresh projects request failed: %w", err) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + slog.Debug("Hub returned non-OK for list fresh projects", "status", resp.StatusCode, "url", url) + return nil, fmt.Errorf("list fresh projects returned status %d", resp.StatusCode) + } + + var result hubProjectsResponse + if err := json.NewDecoder(resp.Body).Decode(&result); err != nil { + return nil, fmt.Errorf("decode list fresh projects response: %w", err) + } + + slog.Debug("Hub returned fresh projects", "count", len(result.Projects)) + + projects := make([]ProjectOption, len(result.Projects)) + for i, p := range result.Projects { + projects[i] = ProjectOption{ID: p.ID, Name: p.Name, Slug: p.Slug} + } + return projects, nil +} + +func (c *httpHubClient) ListProjectsForUser(ctx context.Context, ownerID string) ([]ProjectOption, error) { + url := c.hubURL + "/api/v1/groves?ownerId=" + ownerID + + slog.Debug("Listing projects for user from hub", "url", url, "owner_id", ownerID) + + req, err := http.NewRequestWithContext(ctx, "GET", url, nil) + if err != nil { + return nil, fmt.Errorf("create list user projects request: %w", err) + } + + if err := c.signRequest(req); err != nil { + return nil, fmt.Errorf("sign request: %w", err) + } + + resp, err := c.httpClient.Do(req) + if err != nil { + return nil, fmt.Errorf("list user projects request failed: %w", err) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return nil, fmt.Errorf("list user projects returned status %d", resp.StatusCode) + } + + var result hubProjectsResponse + if err := json.NewDecoder(resp.Body).Decode(&result); err != nil { + return nil, fmt.Errorf("decode list user projects response: %w", err) + } + + projects := make([]ProjectOption, len(result.Projects)) + for i, p := range result.Projects { + projects[i] = ProjectOption{ID: p.ID, Name: p.Name, Slug: p.Slug} + } + return projects, nil +} + +func (c *httpHubClient) ListAgents(ctx context.Context, projectID string) ([]AgentInfo, error) { + url := fmt.Sprintf("%s/api/v1/groves/%s/agents", c.hubURL, projectID) + + req, err := http.NewRequestWithContext(ctx, "GET", url, nil) + if err != nil { + return nil, fmt.Errorf("create list agents request: %w", err) + } + + if err := c.signRequest(req); err != nil { + return nil, fmt.Errorf("sign request: %w", err) + } + + resp, err := c.httpClient.Do(req) + if err != nil { + return nil, fmt.Errorf("list agents request failed: %w", err) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return nil, fmt.Errorf("list agents returned status %d", resp.StatusCode) + } + + var result hubAgentsResponse + if err := json.NewDecoder(resp.Body).Decode(&result); err != nil { + return nil, fmt.Errorf("decode list agents response: %w", err) + } + + agents := make([]AgentInfo, len(result.Agents)) + for i, a := range result.Agents { + agents[i] = AgentInfo{Slug: a.Slug, Activity: a.Activity} + } + return agents, nil +} + +func (c *httpHubClient) signRequest(req *http.Request) error { + if c.brokerID == "" || c.hmacKey == "" { + return nil + } + + secretKey, err := decodeBase64(c.hmacKey) + if err != nil { + return fmt.Errorf("decode HMAC key: %w", err) + } + + auth := &apiclient.HMACAuth{ + BrokerID: c.brokerID, + SecretKey: secretKey, + } + return auth.ApplyAuth(req) +} diff --git a/extras/scion-discord/internal/discord/mentions.go b/extras/scion-discord/internal/discord/mentions.go new file mode 100644 index 000000000..7e58c6fa7 --- /dev/null +++ b/extras/scion-discord/internal/discord/mentions.go @@ -0,0 +1,200 @@ +package discord + +import ( + "strings" + "unicode" + + "github.com/bwmarrin/discordgo" +) + +// resolveTargetAgents determines which agents a message should be routed to. +// Returns a deduplicated list of agent slugs and whether @all was used. +// +// Three-tier routing: +// +// Tier 1: Bot @-mention → routes to group's default agent +// Tier 2: Direct agent @-mention (@coder) → routes to named agent(s) +// Tier 3: @all → routes to ALL agents in the linked project +// +// If no agent is resolved, returns (nil, false) — the message should be +// silently ignored. +func resolveTargetAgents(msg *discordgo.MessageCreate, botUserID string, defaultAgent string, knownAgents []string) ([]string, bool) { + if msg == nil || msg.Message == nil { + return nil, false + } + + botMentioned := isBotMentioned(msg, botUserID) + agentMentions, hasAll := extractAgentMentions(msg.Content, knownAgents) + + if hasAll { + return knownAgents, true + } + + seen := make(map[string]bool) + var result []string + + if botMentioned && defaultAgent != "" { + seen[defaultAgent] = true + result = append(result, defaultAgent) + } + + for _, agent := range agentMentions { + if !seen[agent] { + seen[agent] = true + result = append(result, agent) + } + } + + if len(result) == 0 { + return nil, false + } + return result, false +} + +// isBotMentioned checks if the bot user is in the message's Mentions slice. +// Uses Discord's structured mention data rather than text parsing. +func isBotMentioned(msg *discordgo.MessageCreate, botUserID string) bool { + if msg == nil || msg.Message == nil || botUserID == "" { + return false + } + for _, mention := range msg.Mentions { + if mention.ID == botUserID { + return true + } + } + return false +} + +// extractAgentMentions scans message text for @name tokens matching known agents. +// Returns matched agents and whether @all was found. +func extractAgentMentions(text string, knownAgents []string) (agents []string, hasAll bool) { + known := make(map[string]bool, len(knownAgents)) + for _, a := range knownAgents { + known[strings.ToLower(a)] = true + } + + seen := make(map[string]bool) + for _, word := range strings.Fields(text) { + if !strings.HasPrefix(word, "@") { + continue + } + name := strings.TrimPrefix(word, "@") + name = strings.TrimRightFunc(name, func(r rune) bool { + return unicode.IsPunct(r) && r != '_' && r != '-' + }) + if name == "" { + continue + } + lower := strings.ToLower(name) + if lower == "all" { + return nil, true + } + if known[lower] && !seen[lower] { + seen[lower] = true + // Use the original-case slug from knownAgents. + for _, a := range knownAgents { + if strings.ToLower(a) == lower { + agents = append(agents, a) + break + } + } + } + } + return agents, false +} + +// stripMentions removes bot mentions (<@BOT_ID> and <@!BOT_ID>) and agent +// @mentions from text, returning clean content for delivery to agents. +func stripMentions(text string, botUserID string, agentSlugs []string) string { + // Remove Discord-format bot mentions: <@BOT_ID> and <@!BOT_ID> + if botUserID != "" { + text = strings.ReplaceAll(text, "<@"+botUserID+">", "") + text = strings.ReplaceAll(text, "<@!"+botUserID+">", "") + } + + remove := make(map[string]bool) + for _, slug := range agentSlugs { + remove[strings.ToLower(slug)] = true + } + remove["all"] = true + + var parts []string + for _, word := range strings.Fields(text) { + if !strings.HasPrefix(word, "@") { + parts = append(parts, word) + continue + } + name := strings.TrimPrefix(word, "@") + cleaned := strings.TrimRightFunc(name, func(r rune) bool { + return unicode.IsPunct(r) && r != '_' && r != '-' + }) + if remove[strings.ToLower(cleaned)] { + trailing := name[len(cleaned):] + if trailing != "" { + parts = append(parts, trailing) + } + continue + } + parts = append(parts, word) + } + return strings.Join(parts, " ") +} + +// extractUnresolvedMentions finds @tokens in text that don't match known agents, +// the bot mention format (<@ID>), or @all. Used for error feedback when a user +// misspells an agent name. +func extractUnresolvedMentions(text string, botUserID string, knownAgents []string) []string { + known := make(map[string]bool, len(knownAgents)+1) + for _, a := range knownAgents { + known[strings.ToLower(a)] = true + } + known["all"] = true + + var unresolved []string + seen := make(map[string]bool) + for _, word := range strings.Fields(text) { + if !strings.HasPrefix(word, "@") { + continue + } + // Skip Discord-format bot mentions: <@BOT_ID> or <@!BOT_ID> + if strings.HasPrefix(word, "<@") && strings.HasSuffix(word, ">") { + continue + } + name := strings.TrimPrefix(word, "@") + name = strings.TrimRightFunc(name, func(r rune) bool { + return unicode.IsPunct(r) && r != '_' && r != '-' + }) + if name == "" { + continue + } + lower := strings.ToLower(name) + if !known[lower] && !seen[lower] { + seen[lower] = true + unresolved = append(unresolved, name) + } + } + return unresolved +} + +// agentFromReply extracts the agent slug from a referenced message. +// When a user replies to a webhook message, the webhook username IS the agent +// slug (since the Discord plugin uses per-agent webhooks with the agent slug +// as the webhook username). When replying to a regular bot API message, +// returns "" because the bot's own messages don't carry agent identity in +// the username. +func agentFromReply(ref *discordgo.Message, botUserID string) string { + if ref == nil { + return "" + } + + // Webhook messages have WebhookID set and the Author.Username is the + // agent slug (set when the webhook message was sent). + if ref.WebhookID != "" && ref.Author != nil { + return ref.Author.Username + } + + // Regular bot API messages — cannot determine which agent sent them + // from the message metadata alone. The bot user's username is the bot + // name, not the agent slug. + return "" +} diff --git a/extras/scion-discord/internal/discord/mentions_test.go b/extras/scion-discord/internal/discord/mentions_test.go new file mode 100644 index 000000000..bc4d04523 --- /dev/null +++ b/extras/scion-discord/internal/discord/mentions_test.go @@ -0,0 +1,336 @@ +package discord + +import ( + "testing" + + "github.com/bwmarrin/discordgo" + "github.com/stretchr/testify/assert" +) + +// newMockMessage creates a MessageCreate with the given content and user mentions. +func newMockMessage(content string, mentions []*discordgo.User) *discordgo.MessageCreate { + return &discordgo.MessageCreate{ + Message: &discordgo.Message{ + Content: content, + Mentions: mentions, + }, + } +} + +// --- resolveTargetAgents tests --- + +func TestResolveTargetAgents_BotMentionOnly(t *testing.T) { + msg := newMockMessage("<@BOT123> please help", []*discordgo.User{{ID: "BOT123"}}) + result, isAll := resolveTargetAgents(msg, "BOT123", "coder", []string{"coder", "reviewer"}) + assert.Equal(t, []string{"coder"}, result) + assert.False(t, isAll) +} + +func TestResolveTargetAgents_SingleAgentMention(t *testing.T) { + msg := newMockMessage("@reviewer check this PR", nil) + result, isAll := resolveTargetAgents(msg, "BOT123", "coder", []string{"coder", "reviewer"}) + assert.Equal(t, []string{"reviewer"}, result) + assert.False(t, isAll) +} + +func TestResolveTargetAgents_MultipleAgentMentions(t *testing.T) { + msg := newMockMessage("@coder @reviewer both of you look at this", nil) + result, isAll := resolveTargetAgents(msg, "BOT123", "coder", []string{"coder", "reviewer", "tester"}) + assert.Equal(t, []string{"coder", "reviewer"}, result) + assert.False(t, isAll) +} + +func TestResolveTargetAgents_All(t *testing.T) { + known := []string{"coder", "reviewer", "tester"} + msg := newMockMessage("@all deploy update", nil) + result, isAll := resolveTargetAgents(msg, "BOT123", "coder", known) + assert.Equal(t, known, result) + assert.True(t, isAll) +} + +func TestResolveTargetAgents_NoMentions(t *testing.T) { + msg := newMockMessage("just a regular message", nil) + result, isAll := resolveTargetAgents(msg, "BOT123", "coder", []string{"coder", "reviewer"}) + assert.Nil(t, result) + assert.False(t, isAll) +} + +func TestResolveTargetAgents_NilMessage(t *testing.T) { + result, isAll := resolveTargetAgents(nil, "BOT123", "coder", []string{"coder"}) + assert.Nil(t, result) + assert.False(t, isAll) +} + +func TestResolveTargetAgents_NilInnerMessage(t *testing.T) { + msg := &discordgo.MessageCreate{Message: nil} + result, isAll := resolveTargetAgents(msg, "BOT123", "coder", []string{"coder"}) + assert.Nil(t, result) + assert.False(t, isAll) +} + +func TestResolveTargetAgents_BotPlusAgentMention(t *testing.T) { + msg := newMockMessage("<@BOT123> @reviewer check this", []*discordgo.User{{ID: "BOT123"}}) + result, isAll := resolveTargetAgents(msg, "BOT123", "coder", []string{"coder", "reviewer"}) + assert.Equal(t, []string{"coder", "reviewer"}, result) + assert.False(t, isAll) +} + +func TestResolveTargetAgents_BotPlusExplicitDefault(t *testing.T) { + // When bot is mentioned and the user also explicitly mentions the default agent, + // the default agent should appear only once. + msg := newMockMessage("<@BOT123> @coder hello", []*discordgo.User{{ID: "BOT123"}}) + result, isAll := resolveTargetAgents(msg, "BOT123", "coder", []string{"coder", "reviewer"}) + assert.Equal(t, []string{"coder"}, result) + assert.False(t, isAll) +} + +func TestResolveTargetAgents_DuplicateMentions(t *testing.T) { + msg := newMockMessage("@coder @coder help me", nil) + result, isAll := resolveTargetAgents(msg, "BOT123", "coder", []string{"coder", "reviewer"}) + assert.Equal(t, []string{"coder"}, result) + assert.False(t, isAll) +} + +func TestResolveTargetAgents_UnknownMention(t *testing.T) { + msg := newMockMessage("@stranger hello", nil) + result, isAll := resolveTargetAgents(msg, "BOT123", "coder", []string{"coder", "reviewer"}) + assert.Nil(t, result) + assert.False(t, isAll) +} + +func TestResolveTargetAgents_BotMentionEmptyDefault(t *testing.T) { + msg := newMockMessage("<@BOT123> hello", []*discordgo.User{{ID: "BOT123"}}) + result, isAll := resolveTargetAgents(msg, "BOT123", "", []string{"coder"}) + assert.Nil(t, result) + assert.False(t, isAll) +} + +func TestResolveTargetAgents_MentionWithTrailingPunctuation(t *testing.T) { + msg := newMockMessage("@coder, can you help?", nil) + result, isAll := resolveTargetAgents(msg, "BOT123", "coder", []string{"coder", "reviewer"}) + assert.Equal(t, []string{"coder"}, result) + assert.False(t, isAll) +} + +func TestResolveTargetAgents_MentionWithPeriod(t *testing.T) { + msg := newMockMessage("Hey @reviewer.", nil) + result, isAll := resolveTargetAgents(msg, "BOT123", "coder", []string{"coder", "reviewer"}) + assert.Equal(t, []string{"reviewer"}, result) + assert.False(t, isAll) +} + +func TestResolveTargetAgents_MentionWithExclamation(t *testing.T) { + msg := newMockMessage("@coder!", nil) + result, isAll := resolveTargetAgents(msg, "BOT123", "coder", []string{"coder"}) + assert.Equal(t, []string{"coder"}, result) + assert.False(t, isAll) +} + +// --- isBotMentioned tests --- + +func TestIsBotMentioned_Present(t *testing.T) { + msg := newMockMessage("hello <@BOT123>", []*discordgo.User{{ID: "BOT123"}}) + assert.True(t, isBotMentioned(msg, "BOT123")) +} + +func TestIsBotMentioned_NotPresent(t *testing.T) { + msg := newMockMessage("hello", nil) + assert.False(t, isBotMentioned(msg, "BOT123")) +} + +func TestIsBotMentioned_OtherUser(t *testing.T) { + msg := newMockMessage("hello <@USER456>", []*discordgo.User{{ID: "USER456"}}) + assert.False(t, isBotMentioned(msg, "BOT123")) +} + +func TestIsBotMentioned_MultipleMentions(t *testing.T) { + msg := newMockMessage("hello <@USER456> <@BOT123>", []*discordgo.User{ + {ID: "USER456"}, + {ID: "BOT123"}, + }) + assert.True(t, isBotMentioned(msg, "BOT123")) +} + +func TestIsBotMentioned_NilMessage(t *testing.T) { + assert.False(t, isBotMentioned(nil, "BOT123")) +} + +func TestIsBotMentioned_NilInnerMessage(t *testing.T) { + msg := &discordgo.MessageCreate{Message: nil} + assert.False(t, isBotMentioned(msg, "BOT123")) +} + +func TestIsBotMentioned_EmptyBotUserID(t *testing.T) { + msg := newMockMessage("hello", nil) + assert.False(t, isBotMentioned(msg, "")) +} + +// --- extractAgentMentions tests --- + +func TestExtractAgentMentions_Basic(t *testing.T) { + agents, hasAll := extractAgentMentions("@coder help me", []string{"coder", "reviewer"}) + assert.False(t, hasAll) + assert.Equal(t, []string{"coder"}, agents) +} + +func TestExtractAgentMentions_All(t *testing.T) { + agents, hasAll := extractAgentMentions("@all deploy now", []string{"coder", "reviewer"}) + assert.True(t, hasAll) + assert.Nil(t, agents) +} + +func TestExtractAgentMentions_UnknownAgent(t *testing.T) { + agents, hasAll := extractAgentMentions("@unknown hello", []string{"coder", "reviewer"}) + assert.False(t, hasAll) + assert.Nil(t, agents) +} + +func TestExtractAgentMentions_WithUnderscore(t *testing.T) { + agents, hasAll := extractAgentMentions("@code_reviewer check", []string{"code_reviewer", "coder"}) + assert.False(t, hasAll) + assert.Equal(t, []string{"code_reviewer"}, agents) +} + +func TestExtractAgentMentions_WithHyphen(t *testing.T) { + agents, hasAll := extractAgentMentions("@my-agent check", []string{"my-agent", "coder"}) + assert.False(t, hasAll) + assert.Equal(t, []string{"my-agent"}, agents) +} + +func TestExtractAgentMentions_CaseInsensitive(t *testing.T) { + agents, hasAll := extractAgentMentions("@Coder help", []string{"coder", "reviewer"}) + assert.False(t, hasAll) + assert.Equal(t, []string{"coder"}, agents) +} + +// --- stripMentions tests --- + +func TestStripMentions_BotAndAgent(t *testing.T) { + result := stripMentions("<@BOT123> @coder please review this", "BOT123", []string{"coder"}) + assert.Equal(t, "please review this", result) +} + +func TestStripMentions_BotNicknameFormat(t *testing.T) { + result := stripMentions("<@!BOT123> hello world", "BOT123", nil) + assert.Equal(t, "hello world", result) +} + +func TestStripMentions_OnlyBot(t *testing.T) { + result := stripMentions("<@BOT123> hello world", "BOT123", nil) + assert.Equal(t, "hello world", result) +} + +func TestStripMentions_PreservesUnknownMentions(t *testing.T) { + result := stripMentions("<@BOT123> @stranger hello", "BOT123", []string{"coder"}) + assert.Equal(t, "@stranger hello", result) +} + +func TestStripMentions_WithTrailingPunctuation(t *testing.T) { + result := stripMentions("@coder, please help", "BOT123", []string{"coder"}) + assert.Equal(t, ", please help", result) +} + +func TestStripMentions_AllMention(t *testing.T) { + result := stripMentions("@all attention please", "BOT123", []string{"coder"}) + assert.Equal(t, "attention please", result) +} + +func TestStripMentions_EmptyAfterStrip(t *testing.T) { + result := stripMentions("@coder", "BOT123", []string{"coder"}) + assert.Equal(t, "", result) +} + +func TestStripMentions_NoMentions(t *testing.T) { + result := stripMentions("just regular text", "BOT123", []string{"coder"}) + assert.Equal(t, "just regular text", result) +} + +func TestStripMentions_BotAndMultipleAgents(t *testing.T) { + result := stripMentions("<@BOT123> @coder @reviewer do the thing", "BOT123", []string{"coder", "reviewer"}) + assert.Equal(t, "do the thing", result) +} + +// --- extractUnresolvedMentions tests --- + +func TestExtractUnresolvedMentions_TypoAgent(t *testing.T) { + result := extractUnresolvedMentions("@agent-typo hello", "BOT123", []string{"coder", "reviewer"}) + assert.Equal(t, []string{"agent-typo"}, result) +} + +func TestExtractUnresolvedMentions_AllKnown(t *testing.T) { + result := extractUnresolvedMentions("@coder @reviewer hello", "BOT123", []string{"coder", "reviewer"}) + assert.Nil(t, result) +} + +func TestExtractUnresolvedMentions_SkipsBotMentionFormat(t *testing.T) { + result := extractUnresolvedMentions("<@BOT123> hello", "BOT123", []string{"coder"}) + assert.Nil(t, result) +} + +func TestExtractUnresolvedMentions_SkipsBotNicknameFormat(t *testing.T) { + result := extractUnresolvedMentions("<@!BOT123> hello", "BOT123", []string{"coder"}) + assert.Nil(t, result) +} + +func TestExtractUnresolvedMentions_MixedKnownAndUnknown(t *testing.T) { + result := extractUnresolvedMentions("@coder @agent-typo hello", "BOT123", []string{"coder", "reviewer"}) + assert.Equal(t, []string{"agent-typo"}, result) +} + +func TestExtractUnresolvedMentions_MultipleUnknown(t *testing.T) { + result := extractUnresolvedMentions("@typo1 @typo2 hello", "BOT123", []string{"coder"}) + assert.Equal(t, []string{"typo1", "typo2"}, result) +} + +func TestExtractUnresolvedMentions_NoMentions(t *testing.T) { + result := extractUnresolvedMentions("just regular text", "BOT123", []string{"coder"}) + assert.Nil(t, result) +} + +func TestExtractUnresolvedMentions_AllIsKnown(t *testing.T) { + result := extractUnresolvedMentions("@all hello", "BOT123", []string{"coder"}) + assert.Nil(t, result) +} + +// --- agentFromReply tests --- + +func TestAgentFromReply_WebhookMessage(t *testing.T) { + ref := &discordgo.Message{ + WebhookID: "wh-123", + Author: &discordgo.User{ID: "wh-123", Username: "coder"}, + } + assert.Equal(t, "coder", agentFromReply(ref, "BOT123")) +} + +func TestAgentFromReply_BotMessage(t *testing.T) { + ref := &discordgo.Message{ + Author: &discordgo.User{ID: "BOT123", Username: "ScionBot"}, + } + assert.Equal(t, "", agentFromReply(ref, "BOT123")) +} + +func TestAgentFromReply_NilRef(t *testing.T) { + assert.Equal(t, "", agentFromReply(nil, "BOT123")) +} + +func TestAgentFromReply_NilAuthor(t *testing.T) { + ref := &discordgo.Message{ + WebhookID: "wh-123", + } + assert.Equal(t, "", agentFromReply(ref, "BOT123")) +} + +func TestAgentFromReply_RegularUserMessage(t *testing.T) { + ref := &discordgo.Message{ + Author: &discordgo.User{ID: "USER999", Username: "someone"}, + } + assert.Equal(t, "", agentFromReply(ref, "BOT123")) +} + +func TestAgentFromReply_WebhookWithHyphenatedSlug(t *testing.T) { + ref := &discordgo.Message{ + WebhookID: "wh-456", + Author: &discordgo.User{ID: "wh-456", Username: "my-agent"}, + } + assert.Equal(t, "my-agent", agentFromReply(ref, "BOT123")) +} diff --git a/extras/scion-discord/internal/discord/modals.go b/extras/scion-discord/internal/discord/modals.go new file mode 100644 index 000000000..815312923 --- /dev/null +++ b/extras/scion-discord/internal/discord/modals.go @@ -0,0 +1,206 @@ +package discord + +import ( + "context" + "fmt" + "log/slog" + "strings" + "time" + + "github.com/bwmarrin/discordgo" + + "github.com/GoogleCloudPlatform/scion/pkg/messages" +) + +// OpenAskUserModal responds to a component interaction by presenting a modal +// dialog for free-text input. The modal's custom_id encodes the request ID so +// the subsequent submit can be routed back to the correct pending request. +// +// This function MUST be used as the initial interaction response (not after a +// deferred update) because Discord requires InteractionResponseModal to be +// the first response to a component interaction. +func OpenAskUserModal(s *discordgo.Session, i *discordgo.InteractionCreate, requestID, prompt string) { + title := "Reply to agent" + // Discord modal title limit is 45 characters. + if len(title) > 45 { + title = title[:45] + } + + err := s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{ + Type: discordgo.InteractionResponseModal, + Data: &discordgo.InteractionResponseData{ + CustomID: fmt.Sprintf("ask:modal:%s", requestID), + Title: title, + Components: []discordgo.MessageComponent{ + discordgo.ActionsRow{ + Components: []discordgo.MessageComponent{ + discordgo.TextInput{ + CustomID: "response", + Label: "Your response", + Style: discordgo.TextInputParagraph, + Placeholder: "Type your response...", + Required: true, + }, + }, + }, + }, + }, + }) + if err != nil { + slog.Error("Failed to open ask-user modal", "request_id", requestID, "error", err) + } +} + +// HandleModalSubmit processes a modal submission routed from the broker's +// InteractionModalSubmit handler. It extracts the text value, looks up the +// pending request, delivers the response to the hub, and sends an ephemeral +// confirmation. +func HandleModalSubmit( + s *discordgo.Session, + i *discordgo.InteractionCreate, + store Store, + deliverInbound func(topic string, msg *messages.StructuredMessage), + log *slog.Logger, +) { + if log == nil { + log = slog.Default() + } + + data := i.ModalSubmitData() + customID := data.CustomID + + // Parse custom_id: "ask:modal:" + parts := strings.SplitN(customID, ":", 3) + if len(parts) < 3 || parts[1] != "modal" { + log.Warn("Unexpected modal custom_id format", "custom_id", customID) + respondEphemeral(s, i, "Invalid modal submission.") + return + } + requestID := parts[2] + + // Extract the text value from the modal components. + responseText := extractModalTextValue(data.Components, "response") + if responseText == "" { + respondEphemeral(s, i, "Empty response — no action taken.") + return + } + + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) + defer cancel() + + pending, err := store.GetPendingAskUser(ctx, requestID) + if err != nil { + log.Error("Failed to get pending ask-user for modal", "request_id", requestID, "error", err) + respondEphemeral(s, i, "Error looking up request. Please try again.") + return + } + if pending == nil { + respondEphemeral(s, i, "This request has expired or was not found.") + return + } + if pending.Responded { + respondEphemeral(s, i, "This request has already been answered.") + return + } + if time.Now().After(pending.ExpiresAt) { + respondEphemeral(s, i, "This request has expired.") + return + } + + // Deliver the response to the hub. + if deliverInbound != nil { + discordUserID := interactionUserID(i) + sender := "discord:" + discordUserID + if mapping, mapErr := store.GetUserMapping(ctx, discordUserID); mapErr == nil && mapping != nil && mapping.ScionEmail != "" { + sender = "user:" + mapping.ScionEmail + } + + topic := fmt.Sprintf("scion.project.%s.agent.%s.messages", pending.ProjectID, pending.AgentSlug) + recipient := "agent:" + pending.AgentSlug + + msg := &messages.StructuredMessage{ + Version: messages.Version, + Timestamp: time.Now().UTC().Format(time.RFC3339), + Channel: "discord", + ThreadID: pending.ChannelID, + Sender: sender, + SenderID: discordUserID, + Recipient: recipient, + Msg: responseText, + Type: messages.TypeInstruction, + Metadata: map[string]string{ + "discord_channel_id": pending.ChannelID, + "project_id": pending.ProjectID, + "ask_request_id": pending.RequestID, + }, + } + + deliverInbound(topic, msg) + } + + // Mark as responded. + if err := store.MarkAskUserResponded(ctx, requestID); err != nil { + log.Error("Failed to mark ask-user as responded after modal", "request_id", requestID, "error", err) + } + + // Edit the original ask-user message to disable buttons. + if pending.MessageID != "" && pending.ChannelID != "" { + truncated := responseText + runes := []rune(responseText) + if len(runes) > 100 { + truncated = string(runes[:97]) + "..." + } + editContent := fmt.Sprintf("✅ Responded: %s", truncated) + empty := []discordgo.MessageComponent{} + _, editErr := s.ChannelMessageEditComplex(&discordgo.MessageEdit{ + ID: pending.MessageID, + Channel: pending.ChannelID, + Content: &editContent, + Components: &empty, + }) + if editErr != nil { + log.Warn("Failed to edit original ask-user message after modal", "error", editErr) + } + } + + // Send ephemeral follow-up confirming the response was sent. + respondEphemeral(s, i, "Response sent.") + + log.Info("Ask-user modal response submitted", + "request_id", requestID, + "user", interactionUserID(i), + ) +} + +// extractModalTextValue walks the modal's component tree (ActionsRow → TextInput) +// and returns the value of the TextInput with the given customID. +func extractModalTextValue(components []discordgo.MessageComponent, targetCustomID string) string { + for _, row := range components { + ar, ok := row.(*discordgo.ActionsRow) + if !ok { + continue + } + for _, comp := range ar.Components { + input, ok := comp.(*discordgo.TextInput) + if !ok { + continue + } + if input.CustomID == targetCustomID { + return input.Value + } + } + } + return "" +} + +// respondEphemeral sends an ephemeral follow-up message after a deferred +// interaction acknowledgment. +func respondEphemeral(s *discordgo.Session, i *discordgo.InteractionCreate, content string) { + _, err := s.FollowupMessageCreate(i.Interaction, true, &discordgo.WebhookParams{ + Content: content, + Flags: discordgo.MessageFlagsEphemeral, + }) + if err != nil { + slog.Error("Failed to send ephemeral follow-up", "error", err) + } +} diff --git a/extras/scion-discord/internal/discord/register.go b/extras/scion-discord/internal/discord/register.go new file mode 100644 index 000000000..4b9044387 --- /dev/null +++ b/extras/scion-discord/internal/discord/register.go @@ -0,0 +1,435 @@ +package discord + +import ( + "bytes" + "context" + "crypto/rand" + "encoding/base64" + "encoding/json" + "fmt" + "log/slog" + "math/big" + "net/http" + "strings" + "sync" + "time" + + "github.com/GoogleCloudPlatform/scion/pkg/apiclient" + "github.com/bwmarrin/discordgo" +) + +// RegistrationHandler manages the hub-verified code-based registration flow +// for Discord users. +type RegistrationHandler struct { + store Store + session *discordgo.Session + hubURL string + hmacKey string + brokerID string + httpClient *http.Client + log *slog.Logger + + mu sync.Mutex + pending map[string]*pendingLinkReg // discordUserID -> pending registration +} + +// pendingLinkReg holds state for an in-progress hub-based linking registration. +type pendingLinkReg struct { + Code string + DiscordUserID string + DiscordUsername string + ChannelID string + InteractionToken string // for follow-up messages + ExpiresAt time.Time + pollCancel context.CancelFunc +} + +// discordLinkRequest is the JSON body sent to the hub to register a linking code. +type discordLinkRequest struct { + Code string `json:"code"` + DiscordUserID string `json:"discordUserId"` +} + +// identityLinkStatusResponse is the JSON response from checking a linking status. +type identityLinkStatusResponse struct { + Status string `json:"status"` // "pending", "confirmed", "expired", "not_found" + User *identityLinkUser `json:"user,omitempty"` +} + +// identityLinkUser holds user info returned by the hub when a linking code is confirmed. +type identityLinkUser struct { + ID string `json:"id"` + Email string `json:"email"` +} + +const ( + linkingCodeExpiry = 15 * time.Minute + linkingPollInterval = 10 * time.Second + linkingCodeCharset = "ABCDEFGHJKMNPQRSTUVWXYZ23456789" + linkingCodeLength = 6 +) + +// NewRegistrationHandler creates a new RegistrationHandler. +func NewRegistrationHandler(store Store, session *discordgo.Session, hubURL, hmacKey, brokerID string, log *slog.Logger) *RegistrationHandler { + if log == nil { + log = slog.Default() + } + return &RegistrationHandler{ + store: store, + session: session, + hubURL: hubURL, + hmacKey: hmacKey, + brokerID: brokerID, + httpClient: &http.Client{Timeout: 15 * time.Second}, + log: log, + pending: make(map[string]*pendingLinkReg), + } +} + +// HandleRegister handles the /scion register command. It generates a short +// linking code, registers it with the hub, and sends the user a link button. +func (h *RegistrationHandler) HandleRegister(s *discordgo.Session, i *discordgo.InteractionCreate) { + discordUserID := interactionUserID(i) + if discordUserID == "" { + h.followup(s, i, "Could not identify your user.") + return + } + + discordUsername := interactionUsername(i) + + ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second) + defer cancel() + + // Check if already registered. + existing, err := h.store.GetUserMapping(ctx, discordUserID) + if err != nil { + h.log.Error("Failed to check user mapping", "error", err, "discord_user_id", discordUserID) + h.followup(s, i, "Something went wrong. Please try again.") + return + } + if existing != nil { + h.followup(s, i, fmt.Sprintf( + "You are already registered as **%s**. Use `/scion unregister` first.", + existing.ScionEmail, + )) + return + } + + // Generate linking code. + code, err := generateLinkingCode() + if err != nil { + h.log.Error("Failed to generate linking code", "error", err) + h.followup(s, i, "Something went wrong. Please try again.") + return + } + + // Register code with the hub. + guildID := "" + if i.GuildID != "" { + guildID = i.GuildID + } + if err := h.registerCodeWithHub(ctx, code, discordUserID, discordUsername, guildID); err != nil { + h.log.Error("Failed to register linking code with hub", "error", err) + h.followup(s, i, "Failed to start registration. Please try again later.") + return + } + + // Cancel any existing pending registration for this user. + h.mu.Lock() + h.cleanExpiredLocked() + if old, ok := h.pending[discordUserID]; ok && old.pollCancel != nil { + old.pollCancel() + } + + pollCtx, pollCancel := context.WithCancel(context.Background()) + reg := &pendingLinkReg{ + Code: code, + DiscordUserID: discordUserID, + DiscordUsername: discordUsername, + ChannelID: i.ChannelID, + InteractionToken: i.Token, + ExpiresAt: time.Now().Add(linkingCodeExpiry), + pollCancel: pollCancel, + } + h.pending[discordUserID] = reg + h.mu.Unlock() + + // Build the link URL. + hubLink := fmt.Sprintf("%s/profile/discord?code=%s&user_name=%s", + strings.TrimRight(h.hubURL, "/"), code, discordUsername) + + // Send follow-up with a URL button. + _, err = s.FollowupMessageCreate(i.Interaction, true, &discordgo.WebhookParams{ + Content: "To link your Discord and Scion accounts, click the button below and sign in.\n\n(Link expires in 15 minutes.)", + Components: []discordgo.MessageComponent{ + discordgo.ActionsRow{ + Components: []discordgo.MessageComponent{ + discordgo.Button{ + Label: "Link Account", + Style: discordgo.LinkButton, + URL: hubLink, + }, + }, + }, + }, + }) + if err != nil { + h.log.Error("Failed to send registration message", "error", err) + } + + // Start polling in the background. + go h.pollForConfirmation(pollCtx, s, i.Interaction, reg) +} + +// HandleUnregister handles the /scion unregister command. +func (h *RegistrationHandler) HandleUnregister(s *discordgo.Session, i *discordgo.InteractionCreate) { + discordUserID := interactionUserID(i) + if discordUserID == "" { + h.followup(s, i, "Could not identify your user.") + return + } + + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) + defer cancel() + + existing, err := h.store.GetUserMapping(ctx, discordUserID) + if err != nil { + h.log.Error("Failed to check user mapping", "error", err, "discord_user_id", discordUserID) + h.followup(s, i, "Something went wrong. Please try again.") + return + } + if existing == nil { + h.followup(s, i, "You don't have a linked Scion account. Use `/scion register` to link one.") + return + } + + if err := h.store.DeleteUserMapping(ctx, discordUserID); err != nil { + h.log.Error("Failed to delete user mapping", "error", err, "discord_user_id", discordUserID) + h.followup(s, i, "Failed to unlink your account. Please try again.") + return + } + + h.followup(s, i, "Your Discord account has been unlinked from Scion.") + h.log.Info("User unregistered", + "discord_user_id", discordUserID, + "scion_email", existing.ScionEmail, + ) +} + +// pollForConfirmation polls the hub for confirmation status in the background. +func (h *RegistrationHandler) pollForConfirmation(ctx context.Context, s *discordgo.Session, interaction *discordgo.Interaction, reg *pendingLinkReg) { + ticker := time.NewTicker(linkingPollInterval) + defer ticker.Stop() + + deadline := reg.ExpiresAt + for { + select { + case <-ctx.Done(): + return + case t := <-ticker.C: + if t.After(deadline) { + h.mu.Lock() + if cur, ok := h.pending[reg.DiscordUserID]; ok && cur.Code == reg.Code { + delete(h.pending, reg.DiscordUserID) + } + h.mu.Unlock() + return + } + + checkCtx, checkCancel := context.WithTimeout(ctx, 10*time.Second) + statusResp, err := h.checkLinkingStatus(checkCtx, reg.DiscordUserID) + checkCancel() + + if err != nil { + h.log.Debug("Poll check failed", "error", err, "discord_user_id", reg.DiscordUserID) + continue + } + + if statusResp.Status == "confirmed" && statusResp.User != nil { + h.completeRegistration(s, interaction, reg, statusResp) + return + } + } + } +} + +// completeRegistration saves the user mapping and notifies the user. +func (h *RegistrationHandler) completeRegistration(s *discordgo.Session, interaction *discordgo.Interaction, reg *pendingLinkReg, statusResp *identityLinkStatusResponse) { + if statusResp.User == nil { + h.log.Error("Linking status confirmed but missing user info", "discord_user_id", reg.DiscordUserID) + return + } + + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) + defer cancel() + + mapping := &DiscordUserMapping{ + DiscordUserID: reg.DiscordUserID, + DiscordUsername: reg.DiscordUsername, + ScionUserID: statusResp.User.ID, + ScionEmail: statusResp.User.Email, + LinkedAt: time.Now(), + } + + if err := h.store.CreateUserMapping(ctx, mapping); err != nil { + h.log.Error("Failed to save user mapping", "error", err, "discord_user_id", reg.DiscordUserID) + return + } + + h.mu.Lock() + if reg.pollCancel != nil { + reg.pollCancel() + } + delete(h.pending, reg.DiscordUserID) + h.mu.Unlock() + + // Send follow-up via the interaction. + _, err := s.FollowupMessageCreate(interaction, true, &discordgo.WebhookParams{ + Content: fmt.Sprintf("Linked! You are **%s**", statusResp.User.Email), + }) + if err != nil { + h.log.Error("Failed to send registration confirmation", "error", err) + } + + h.log.Info("User registered via hub linking", + "discord_user_id", reg.DiscordUserID, + "scion_email", statusResp.User.Email, + "scion_user_id", statusResp.User.ID, + ) +} + +// registerCodeWithHub POSTs a linking code to the hub for registration. +func (h *RegistrationHandler) registerCodeWithHub(ctx context.Context, code, discordUserID, _, _ string) error { + body, err := json.Marshal(discordLinkRequest{ + Code: code, + DiscordUserID: discordUserID, + }) + if err != nil { + return fmt.Errorf("marshal discord link request: %w", err) + } + + url := h.hubURL + "/api/v1/discord/link" + req, err := http.NewRequestWithContext(ctx, "POST", url, bytes.NewReader(body)) + if err != nil { + return fmt.Errorf("create discord link request: %w", err) + } + req.Header.Set("Content-Type", "application/json") + if err := h.signRequest(req); err != nil { + return fmt.Errorf("sign discord link request: %w", err) + } + + resp, err := h.httpClient.Do(req) + if err != nil { + return fmt.Errorf("discord link request failed: %w", err) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusCreated { + return fmt.Errorf("discord link endpoint returned status %d", resp.StatusCode) + } + + return nil +} + +// checkLinkingStatus checks with the hub whether a linking code was confirmed. +func (h *RegistrationHandler) checkLinkingStatus(ctx context.Context, discordUserID string) (*identityLinkStatusResponse, error) { + url := fmt.Sprintf("%s/api/v1/discord/link/status?discord_user_id=%s", + h.hubURL, discordUserID) + req, err := http.NewRequestWithContext(ctx, "GET", url, nil) + if err != nil { + return nil, fmt.Errorf("create identity link status request: %w", err) + } + if err := h.signRequest(req); err != nil { + return nil, fmt.Errorf("sign identity link status request: %w", err) + } + + resp, err := h.httpClient.Do(req) + if err != nil { + return nil, fmt.Errorf("identity link status request failed: %w", err) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return nil, fmt.Errorf("identity link status endpoint returned status %d", resp.StatusCode) + } + + var statusResp identityLinkStatusResponse + if err := json.NewDecoder(resp.Body).Decode(&statusResp); err != nil { + return nil, fmt.Errorf("decode identity link status response: %w", err) + } + return &statusResp, nil +} + +// signRequest signs an HTTP request with HMAC broker credentials. +func (h *RegistrationHandler) signRequest(req *http.Request) error { + if h.hmacKey == "" || h.brokerID == "" { + return nil + } + secretKey, err := decodeBase64(h.hmacKey) + if err != nil { + return fmt.Errorf("decode HMAC key: %w", err) + } + auth := &apiclient.HMACAuth{ + BrokerID: h.brokerID, + SecretKey: secretKey, + } + return auth.ApplyAuth(req) +} + +// followup sends a follow-up message to the interaction. +func (h *RegistrationHandler) followup(s *discordgo.Session, i *discordgo.InteractionCreate, content string) { + _, err := s.FollowupMessageCreate(i.Interaction, true, &discordgo.WebhookParams{ + Content: content, + }) + if err != nil { + h.log.Error("Failed to send follow-up message", "error", err) + } +} + +func (h *RegistrationHandler) cleanExpiredLocked() { + now := time.Now() + for id, reg := range h.pending { + if now.After(reg.ExpiresAt) { + if reg.pollCancel != nil { + reg.pollCancel() + } + delete(h.pending, id) + } + } +} + +// interactionUsername extracts the Discord username from an interaction. +func interactionUsername(i *discordgo.InteractionCreate) string { + if i.Member != nil && i.Member.User != nil { + return i.Member.User.Username + } + if i.User != nil { + return i.User.Username + } + return "" +} + +// generateLinkingCode creates a 6-character alphanumeric code using a +// charset that avoids ambiguous characters (0/O, 1/I/L). +func generateLinkingCode() (string, error) { + result := make([]byte, linkingCodeLength) + for i := range result { + n, err := rand.Int(rand.Reader, big.NewInt(int64(len(linkingCodeCharset)))) + if err != nil { + return "", fmt.Errorf("generate random char: %w", err) + } + result[i] = linkingCodeCharset[n.Int64()] + } + return string(result), nil +} + +// decodeBase64 tries standard and URL-safe base64 decoding. +func decodeBase64(s string) ([]byte, error) { + if b, err := base64.StdEncoding.DecodeString(s); err == nil { + return b, nil + } + if b, err := base64.URLEncoding.DecodeString(s); err == nil { + return b, nil + } + return nil, fmt.Errorf("invalid base64 encoding") +} diff --git a/extras/scion-discord/internal/discord/sendqueue.go b/extras/scion-discord/internal/discord/sendqueue.go new file mode 100644 index 000000000..0495fa8bc --- /dev/null +++ b/extras/scion-discord/internal/discord/sendqueue.go @@ -0,0 +1,209 @@ +package discord + +import ( + "context" + "errors" + "log/slog" + "sync" + "time" + + "github.com/bwmarrin/discordgo" +) + +const ( + defaultSendQueueSize = 100 + defaultSendMinDelay = 50 * time.Millisecond + defaultSendIdleTimeout = 5 * time.Minute +) + +// sendRequest represents a message waiting to be sent through the queue. +type sendRequest struct { + channelID string + content string + embeds []*discordgo.MessageEmbed + components []discordgo.MessageComponent + result chan *sendResult +} + +// sendResult carries the outcome of a queued send back to the caller. +type sendResult struct { + msg *discordgo.Message + err error +} + +// channelQueue holds a per-channel buffered channel. +type channelQueue struct { + ch chan *sendRequest +} + +// SendQueue manages per-channel outbound message workers to prevent +// Discord API rate-limit errors. Each channel gets its own goroutine +// that serializes sends with a configurable minimum delay. +type SendQueue struct { + session *discordgo.Session + log *slog.Logger + mu sync.Mutex + queues map[string]*channelQueue + maxSize int + minDelay time.Duration + closed bool + wg sync.WaitGroup +} + +// NewSendQueue creates a new SendQueue. Pass 0 for queueSize or minDelay +// to use the defaults (100 messages, 50ms). +func NewSendQueue(session *discordgo.Session, log *slog.Logger, queueSize int, minDelay time.Duration) *SendQueue { + if queueSize <= 0 { + queueSize = defaultSendQueueSize + } + if minDelay <= 0 { + minDelay = defaultSendMinDelay + } + if log == nil { + log = slog.Default() + } + return &SendQueue{ + session: session, + log: log, + queues: make(map[string]*channelQueue), + maxSize: queueSize, + minDelay: minDelay, + } +} + +// Send enqueues a message and blocks until it is sent (or the context is +// cancelled). It returns the Discord API response or an error. +func (sq *SendQueue) Send(ctx context.Context, channelID, content string, embeds []*discordgo.MessageEmbed, components []discordgo.MessageComponent) (*discordgo.Message, error) { + resultCh := make(chan *sendResult, 1) + + req := &sendRequest{ + channelID: channelID, + content: content, + embeds: embeds, + components: components, + result: resultCh, + } + + if err := sq.enqueue(channelID, req); err != nil { + return nil, err + } + + select { + case res := <-resultCh: + return res.msg, res.err + case <-ctx.Done(): + return nil, ctx.Err() + } +} + +// enqueue gets-or-creates the per-channel queue and writes the request to it. +func (sq *SendQueue) enqueue(channelID string, req *sendRequest) error { + sq.mu.Lock() + defer sq.mu.Unlock() + + if sq.closed { + return errors.New("send queue is closed") + } + + cq, ok := sq.queues[channelID] + if !ok { + cq = &channelQueue{ + ch: make(chan *sendRequest, sq.maxSize), + } + sq.queues[channelID] = cq + + sq.wg.Add(1) + go sq.worker(channelID, cq) + } + + // Try non-blocking send; if full, drop the oldest and retry. + select { + case cq.ch <- req: + default: + select { + case dropped := <-cq.ch: + if dropped.result != nil { + dropped.result <- &sendResult{err: errors.New("dropped: send queue overflow")} + } + sq.log.Warn("Send queue overflow, dropped oldest message", + "channel_id", channelID, "queue_size", sq.maxSize) + default: + } + cq.ch <- req + } + + return nil +} + +// worker is the per-channel send goroutine. It reads messages from the channel +// and sends them via the API with rate limiting. It exits after an idle timeout. +func (sq *SendQueue) worker(channelID string, cq *channelQueue) { + defer sq.wg.Done() + defer sq.removeQueue(channelID) + + idleTimer := time.NewTimer(defaultSendIdleTimeout) + defer idleTimer.Stop() + + for { + select { + case req, ok := <-cq.ch: + if !ok { + // Channel closed — worker should exit. + return + } + + // Reset idle timer on activity. + if !idleTimer.Stop() { + select { + case <-idleTimer.C: + default: + } + } + idleTimer.Reset(defaultSendIdleTimeout) + + // Send the message. + msg, err := sq.sendOne(req) + if req.result != nil { + req.result <- &sendResult{msg: msg, err: err} + } + + // Enforce minimum delay between sends. + time.Sleep(sq.minDelay) + + case <-idleTimer.C: + sq.log.Debug("Send queue worker idle, exiting", "channel_id", channelID) + return + } + } +} + +// sendOne dispatches a single outbound message to the Discord API. +func (sq *SendQueue) sendOne(req *sendRequest) (*discordgo.Message, error) { + data := &discordgo.MessageSend{ + Content: req.content, + Embeds: req.embeds, + Components: req.components, + } + return sq.session.ChannelMessageSendComplex(req.channelID, data) +} + +// removeQueue removes the per-channel queue from the map when the worker exits. +func (sq *SendQueue) removeQueue(channelID string) { + sq.mu.Lock() + defer sq.mu.Unlock() + delete(sq.queues, channelID) +} + +// Close shuts down all worker goroutines and waits for them to finish. +// Messages still in the queues are drained with errors. +func (sq *SendQueue) Close() { + sq.mu.Lock() + sq.closed = true + for channelID, cq := range sq.queues { + close(cq.ch) + delete(sq.queues, channelID) + } + sq.mu.Unlock() + + sq.wg.Wait() +} diff --git a/extras/scion-discord/internal/discord/store.go b/extras/scion-discord/internal/discord/store.go new file mode 100644 index 000000000..a97a25b50 --- /dev/null +++ b/extras/scion-discord/internal/discord/store.go @@ -0,0 +1,693 @@ +package discord + +import ( + "context" + "database/sql" + "encoding/json" + "fmt" + "log/slog" + "strings" + "time" + + _ "modernc.org/sqlite" +) + +// Store defines the persistence interface for the Discord broker plugin. +type Store interface { + // Channel links (Discord channel <-> Scion project) + CreateChannelLink(ctx context.Context, link *ChannelLink) error + GetChannelLink(ctx context.Context, channelID string) (*ChannelLink, error) + GetChannelLinksForProject(ctx context.Context, projectID string) ([]*ChannelLink, error) + GetAllChannelLinks(ctx context.Context) ([]*ChannelLink, error) + UpdateChannelLink(ctx context.Context, link *ChannelLink) error + DeactivateLinksForGuild(ctx context.Context, guildID string) error + DeleteChannelLink(ctx context.Context, channelID string) error + + // User mappings (Discord user <-> Scion identity) + CreateUserMapping(ctx context.Context, mapping *DiscordUserMapping) error + GetUserMapping(ctx context.Context, discordUserID string) (*DiscordUserMapping, error) + GetUserMappingByEmail(ctx context.Context, email string) (*DiscordUserMapping, error) + GetUserMappingByScionUserID(ctx context.Context, userID string) (*DiscordUserMapping, error) + DeleteUserMapping(ctx context.Context, discordUserID string) error + + // Conversation context + SetConversationContext(ctx context.Context, cc *ConversationContext) error + GetConversationContext(ctx context.Context, discordUserID, projectID, agentSlug string) (*ConversationContext, error) + GetLatestConversationContext(ctx context.Context, discordUserID, projectID string) (*ConversationContext, error) + + // Agent cache + SetProjectAgents(ctx context.Context, pa *ProjectAgents) error + GetProjectAgents(ctx context.Context, projectID string) (*ProjectAgents, error) + + // Pending ask-user requests + CreatePendingAskUser(ctx context.Context, req *PendingAskUser) error + GetPendingAskUser(ctx context.Context, requestID string) (*PendingAskUser, error) + MarkAskUserResponded(ctx context.Context, requestID string) error + DeleteExpiredAskUsers(ctx context.Context) (int, error) + + // Callback lookup + CreateCallbackLookup(ctx context.Context, lookup *CallbackLookup) error + GetCallbackLookup(ctx context.Context, shortID string) (*CallbackLookup, error) + DeleteExpiredCallbacks(ctx context.Context) (int, error) + + // Notification preferences + SetNotificationPref(ctx context.Context, pref *NotificationPref) error + GetNotificationPrefs(ctx context.Context, discordUserID, projectID string) ([]*NotificationPref, error) + + // Lifecycle + Close() error +} + +// ChannelLink represents a Discord channel linked to a Scion project. +type ChannelLink struct { + ChannelID string + GuildID string + ProjectID string + ProjectSlug string + DefaultAgent string + LinkedBy string // Discord user ID who ran /setup + LinkedAt time.Time + Active bool + ShowAgentToAgent bool + ShowAssistantReply bool + ShowStateChanges bool + NotifyInGroup bool + ChatOnly bool +} + +// DiscordUserMapping links a Discord user to a Scion user identity. +type DiscordUserMapping struct { + DiscordUserID string + DiscordUsername string + ScionUserID string + ScionEmail string + LinkedAt time.Time +} + +// ConversationContext tracks the last chat context for a user+project+agent tuple. +type ConversationContext struct { + DiscordUserID string + ProjectID string + AgentSlug string + LastChannelID string + LastMessageAt time.Time +} + +// ProjectAgents caches the list of agents for a project. +type ProjectAgents struct { + ProjectID string + AgentSlugs []string + RefreshedAt time.Time +} + +// PendingAskUser represents an ask-user callback awaiting a Discord user response. +type PendingAskUser struct { + RequestID string + MessageID string // Discord message snowflake + ChannelID string + AgentSlug string + ProjectID string + Choices []string + ExpiresAt time.Time + Responded bool +} + +// CallbackLookup maps a short callback ID to its full data payload. +type CallbackLookup struct { + ShortID string + FullData string + ExpiresAt time.Time +} + +// NotificationPref stores per-user, per-agent notification subscription state. +type NotificationPref struct { + DiscordUserID string + ProjectID string + AgentSlug string + Enabled bool + UpdatedAt time.Time +} + +// sqliteStore implements Store using SQLite via modernc.org/sqlite. +type sqliteStore struct { + db *sql.DB +} + +// NewSQLiteStore opens (or creates) a SQLite database at dbPath and +// initialises the schema. The returned Store must be closed when no +// longer needed. +func NewSQLiteStore(dbPath string) (Store, error) { + db, err := sql.Open("sqlite", dbPath) + if err != nil { + return nil, fmt.Errorf("open sqlite database: %w", err) + } + + // Enable WAL mode for concurrent read performance. + if _, err := db.Exec("PRAGMA journal_mode=WAL"); err != nil { + db.Close() + return nil, fmt.Errorf("set WAL mode: %w", err) + } + + // Set busy timeout to avoid SQLITE_BUSY errors under contention. + if _, err := db.Exec("PRAGMA busy_timeout=5000"); err != nil { + db.Close() + return nil, fmt.Errorf("set busy timeout: %w", err) + } + + s := &sqliteStore{db: db} + if err := s.createSchema(); err != nil { + db.Close() + return nil, fmt.Errorf("create schema: %w", err) + } + return s, nil +} + +func (s *sqliteStore) createSchema() error { + const ddl = ` +CREATE TABLE IF NOT EXISTS channel_links ( + channel_id TEXT PRIMARY KEY, + guild_id TEXT NOT NULL, + project_id TEXT NOT NULL, + project_slug TEXT NOT NULL DEFAULT '', + default_agent TEXT NOT NULL DEFAULT '', + linked_by TEXT NOT NULL DEFAULT '', + linked_at TEXT NOT NULL, + active INTEGER NOT NULL DEFAULT 1, + show_agent_to_agent INTEGER NOT NULL DEFAULT 0, + show_assistant_reply INTEGER NOT NULL DEFAULT 1, + show_state_changes INTEGER NOT NULL DEFAULT 1, + notify_in_group INTEGER NOT NULL DEFAULT 1, + chat_only INTEGER NOT NULL DEFAULT 0 +); + +CREATE INDEX IF NOT EXISTS idx_channel_links_project ON channel_links(project_id); +CREATE INDEX IF NOT EXISTS idx_channel_links_guild ON channel_links(guild_id); + +CREATE TABLE IF NOT EXISTS user_mappings ( + discord_user_id TEXT PRIMARY KEY, + discord_username TEXT NOT NULL DEFAULT '', + scion_user_id TEXT NOT NULL DEFAULT '', + scion_email TEXT NOT NULL DEFAULT '', + linked_at TEXT NOT NULL +); + +CREATE INDEX IF NOT EXISTS idx_user_mappings_email ON user_mappings(scion_email); +CREATE INDEX IF NOT EXISTS idx_user_mappings_scion_id ON user_mappings(scion_user_id); + +CREATE TABLE IF NOT EXISTS conversation_context ( + discord_user_id TEXT NOT NULL, + project_id TEXT NOT NULL, + agent_slug TEXT NOT NULL, + last_channel_id TEXT NOT NULL, + last_message_at TEXT NOT NULL, + PRIMARY KEY (discord_user_id, project_id, agent_slug) +); + +CREATE TABLE IF NOT EXISTS project_agents ( + project_id TEXT PRIMARY KEY, + agent_slugs TEXT NOT NULL DEFAULT '[]', + refreshed_at TEXT NOT NULL +); + +CREATE TABLE IF NOT EXISTS pending_ask_users ( + request_id TEXT PRIMARY KEY, + message_id TEXT NOT NULL, + channel_id TEXT NOT NULL, + agent_slug TEXT NOT NULL DEFAULT '', + project_id TEXT NOT NULL DEFAULT '', + choices TEXT NOT NULL DEFAULT '[]', + expires_at TEXT NOT NULL, + responded INTEGER NOT NULL DEFAULT 0 +); + +CREATE TABLE IF NOT EXISTS callback_lookups ( + short_id TEXT PRIMARY KEY, + full_data TEXT NOT NULL, + expires_at TEXT NOT NULL +); + +CREATE TABLE IF NOT EXISTS notification_prefs ( + discord_user_id TEXT NOT NULL, + project_id TEXT NOT NULL, + agent_slug TEXT NOT NULL, + enabled INTEGER NOT NULL DEFAULT 1, + updated_at TEXT NOT NULL, + PRIMARY KEY (discord_user_id, project_id, agent_slug) +); +` + _, err := s.db.Exec(ddl) + if err != nil { + return err + } + s.migrateSchema() + return nil +} + +func (s *sqliteStore) migrateSchema() { + migrations := []string{ + `ALTER TABLE channel_links ADD COLUMN show_state_changes INTEGER NOT NULL DEFAULT 1`, + } + for _, m := range migrations { + if _, err := s.db.Exec(m); err != nil { + if !strings.Contains(err.Error(), "duplicate column name") { + slog.Warn("Failed to run migration", "migration", m, "error", err) + } + } + } +} + +// Close closes the underlying database connection. +func (s *sqliteStore) Close() error { + return s.db.Close() +} + +// --- ChannelLink CRUD --- + +func (s *sqliteStore) CreateChannelLink(ctx context.Context, link *ChannelLink) error { + const q = ` +INSERT INTO channel_links (channel_id, guild_id, project_id, project_slug, default_agent, linked_by, linked_at, active, show_agent_to_agent, show_assistant_reply, show_state_changes, notify_in_group, chat_only) +VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) +ON CONFLICT(channel_id) DO UPDATE SET + guild_id=excluded.guild_id, project_id=excluded.project_id, project_slug=excluded.project_slug, + default_agent=excluded.default_agent, linked_by=excluded.linked_by, linked_at=excluded.linked_at, + active=excluded.active, show_agent_to_agent=excluded.show_agent_to_agent, + show_assistant_reply=excluded.show_assistant_reply, show_state_changes=excluded.show_state_changes, + notify_in_group=excluded.notify_in_group, chat_only=excluded.chat_only` + _, err := s.db.ExecContext(ctx, q, + link.ChannelID, link.GuildID, link.ProjectID, link.ProjectSlug, + link.DefaultAgent, link.LinkedBy, link.LinkedAt.UTC().Format(time.RFC3339), + boolToInt(link.Active), boolToInt(link.ShowAgentToAgent), + boolToInt(link.ShowAssistantReply), boolToInt(link.ShowStateChanges), + boolToInt(link.NotifyInGroup), boolToInt(link.ChatOnly)) + return err +} + +func (s *sqliteStore) GetChannelLink(ctx context.Context, channelID string) (*ChannelLink, error) { + const q = `SELECT channel_id, guild_id, project_id, project_slug, default_agent, linked_by, linked_at, active, show_agent_to_agent, show_assistant_reply, show_state_changes, notify_in_group, chat_only FROM channel_links WHERE channel_id = ?` + row := s.db.QueryRowContext(ctx, q, channelID) + return scanChannelLink(row) +} + +func (s *sqliteStore) GetChannelLinksForProject(ctx context.Context, projectID string) ([]*ChannelLink, error) { + const q = `SELECT channel_id, guild_id, project_id, project_slug, default_agent, linked_by, linked_at, active, show_agent_to_agent, show_assistant_reply, show_state_changes, notify_in_group, chat_only FROM channel_links WHERE project_id = ?` + rows, err := s.db.QueryContext(ctx, q, projectID) + if err != nil { + return nil, err + } + defer rows.Close() + return scanChannelLinks(rows) +} + +func (s *sqliteStore) GetAllChannelLinks(ctx context.Context) ([]*ChannelLink, error) { + const q = `SELECT channel_id, guild_id, project_id, project_slug, default_agent, linked_by, linked_at, active, show_agent_to_agent, show_assistant_reply, show_state_changes, notify_in_group, chat_only FROM channel_links` + rows, err := s.db.QueryContext(ctx, q) + if err != nil { + return nil, err + } + defer rows.Close() + return scanChannelLinks(rows) +} + +func (s *sqliteStore) UpdateChannelLink(ctx context.Context, link *ChannelLink) error { + const q = ` +UPDATE channel_links SET + guild_id=?, project_id=?, project_slug=?, default_agent=?, linked_by=?, linked_at=?, + active=?, show_agent_to_agent=?, show_assistant_reply=?, show_state_changes=?, + notify_in_group=?, chat_only=? +WHERE channel_id=?` + _, err := s.db.ExecContext(ctx, q, + link.GuildID, link.ProjectID, link.ProjectSlug, + link.DefaultAgent, link.LinkedBy, link.LinkedAt.UTC().Format(time.RFC3339), + boolToInt(link.Active), boolToInt(link.ShowAgentToAgent), + boolToInt(link.ShowAssistantReply), boolToInt(link.ShowStateChanges), + boolToInt(link.NotifyInGroup), boolToInt(link.ChatOnly), + link.ChannelID) + return err +} + +func (s *sqliteStore) DeactivateLinksForGuild(ctx context.Context, guildID string) error { + _, err := s.db.ExecContext(ctx, `UPDATE channel_links SET active = 0 WHERE guild_id = ?`, guildID) + return err +} + +func (s *sqliteStore) DeleteChannelLink(ctx context.Context, channelID string) error { + _, err := s.db.ExecContext(ctx, `DELETE FROM channel_links WHERE channel_id = ?`, channelID) + return err +} + +// --- User mappings --- + +func (s *sqliteStore) CreateUserMapping(ctx context.Context, mapping *DiscordUserMapping) error { + const q = ` +INSERT INTO user_mappings (discord_user_id, discord_username, scion_user_id, scion_email, linked_at) +VALUES (?, ?, ?, ?, ?) +ON CONFLICT(discord_user_id) DO UPDATE SET + discord_username=excluded.discord_username, scion_user_id=excluded.scion_user_id, + scion_email=excluded.scion_email, linked_at=excluded.linked_at` + _, err := s.db.ExecContext(ctx, q, + mapping.DiscordUserID, mapping.DiscordUsername, + mapping.ScionUserID, mapping.ScionEmail, + mapping.LinkedAt.UTC().Format(time.RFC3339)) + return err +} + +func (s *sqliteStore) GetUserMapping(ctx context.Context, discordUserID string) (*DiscordUserMapping, error) { + const q = `SELECT discord_user_id, discord_username, scion_user_id, scion_email, linked_at FROM user_mappings WHERE discord_user_id = ?` + row := s.db.QueryRowContext(ctx, q, discordUserID) + return scanUserMapping(row) +} + +func (s *sqliteStore) GetUserMappingByEmail(ctx context.Context, email string) (*DiscordUserMapping, error) { + const q = `SELECT discord_user_id, discord_username, scion_user_id, scion_email, linked_at FROM user_mappings WHERE scion_email = ?` + row := s.db.QueryRowContext(ctx, q, email) + return scanUserMapping(row) +} + +func (s *sqliteStore) GetUserMappingByScionUserID(ctx context.Context, userID string) (*DiscordUserMapping, error) { + const q = `SELECT discord_user_id, discord_username, scion_user_id, scion_email, linked_at FROM user_mappings WHERE scion_user_id = ?` + row := s.db.QueryRowContext(ctx, q, userID) + return scanUserMapping(row) +} + +func (s *sqliteStore) DeleteUserMapping(ctx context.Context, discordUserID string) error { + _, err := s.db.ExecContext(ctx, `DELETE FROM user_mappings WHERE discord_user_id = ?`, discordUserID) + return err +} + +// --- ConversationContext --- + +func (s *sqliteStore) SetConversationContext(ctx context.Context, cc *ConversationContext) error { + const q = ` +INSERT INTO conversation_context (discord_user_id, project_id, agent_slug, last_channel_id, last_message_at) +VALUES (?, ?, ?, ?, ?) +ON CONFLICT(discord_user_id, project_id, agent_slug) DO UPDATE SET + last_channel_id=excluded.last_channel_id, last_message_at=excluded.last_message_at` + _, err := s.db.ExecContext(ctx, q, + cc.DiscordUserID, cc.ProjectID, cc.AgentSlug, + cc.LastChannelID, cc.LastMessageAt.UTC().Format(time.RFC3339)) + return err +} + +func (s *sqliteStore) GetConversationContext(ctx context.Context, discordUserID, projectID, agentSlug string) (*ConversationContext, error) { + const q = `SELECT discord_user_id, project_id, agent_slug, last_channel_id, last_message_at FROM conversation_context WHERE discord_user_id = ? AND project_id = ? AND agent_slug = ?` + row := s.db.QueryRowContext(ctx, q, discordUserID, projectID, agentSlug) + + var cc ConversationContext + var lastMessageAt string + err := row.Scan(&cc.DiscordUserID, &cc.ProjectID, &cc.AgentSlug, &cc.LastChannelID, &lastMessageAt) + if err == sql.ErrNoRows { + return nil, nil + } + if err != nil { + return nil, err + } + cc.LastMessageAt, err = time.Parse(time.RFC3339, lastMessageAt) + if err != nil { + return nil, fmt.Errorf("parse last_message_at: %w", err) + } + return &cc, nil +} + +func (s *sqliteStore) GetLatestConversationContext(ctx context.Context, discordUserID, projectID string) (*ConversationContext, error) { + const q = `SELECT discord_user_id, project_id, agent_slug, last_channel_id, last_message_at +FROM conversation_context +WHERE discord_user_id = ? AND project_id = ? +ORDER BY last_message_at DESC LIMIT 1` + row := s.db.QueryRowContext(ctx, q, discordUserID, projectID) + + var cc ConversationContext + var lastMessageAt string + err := row.Scan(&cc.DiscordUserID, &cc.ProjectID, &cc.AgentSlug, &cc.LastChannelID, &lastMessageAt) + if err == sql.ErrNoRows { + return nil, nil + } + if err != nil { + return nil, err + } + cc.LastMessageAt, err = time.Parse(time.RFC3339, lastMessageAt) + if err != nil { + return nil, fmt.Errorf("parse last_message_at: %w", err) + } + return &cc, nil +} + +// --- ProjectAgents --- + +func (s *sqliteStore) SetProjectAgents(ctx context.Context, pa *ProjectAgents) error { + slugsJSON, err := json.Marshal(pa.AgentSlugs) + if err != nil { + return fmt.Errorf("marshal agent_slugs: %w", err) + } + const q = ` +INSERT INTO project_agents (project_id, agent_slugs, refreshed_at) +VALUES (?, ?, ?) +ON CONFLICT(project_id) DO UPDATE SET + agent_slugs=excluded.agent_slugs, refreshed_at=excluded.refreshed_at` + _, err = s.db.ExecContext(ctx, q, pa.ProjectID, string(slugsJSON), pa.RefreshedAt.UTC().Format(time.RFC3339)) + return err +} + +func (s *sqliteStore) GetProjectAgents(ctx context.Context, projectID string) (*ProjectAgents, error) { + const q = `SELECT project_id, agent_slugs, refreshed_at FROM project_agents WHERE project_id = ?` + row := s.db.QueryRowContext(ctx, q, projectID) + + var pa ProjectAgents + var slugsJSON, refreshedAt string + err := row.Scan(&pa.ProjectID, &slugsJSON, &refreshedAt) + if err == sql.ErrNoRows { + return nil, nil + } + if err != nil { + return nil, err + } + if err := json.Unmarshal([]byte(slugsJSON), &pa.AgentSlugs); err != nil { + return nil, fmt.Errorf("unmarshal agent_slugs: %w", err) + } + pa.RefreshedAt, err = time.Parse(time.RFC3339, refreshedAt) + if err != nil { + return nil, fmt.Errorf("parse refreshed_at: %w", err) + } + return &pa, nil +} + +// --- PendingAskUser --- + +func (s *sqliteStore) CreatePendingAskUser(ctx context.Context, req *PendingAskUser) error { + choicesJSON, err := json.Marshal(req.Choices) + if err != nil { + return fmt.Errorf("marshal choices: %w", err) + } + const q = ` +INSERT INTO pending_ask_users (request_id, message_id, channel_id, agent_slug, project_id, choices, expires_at, responded) +VALUES (?, ?, ?, ?, ?, ?, ?, ?) +ON CONFLICT(request_id) DO UPDATE SET + message_id=excluded.message_id, channel_id=excluded.channel_id, agent_slug=excluded.agent_slug, + project_id=excluded.project_id, choices=excluded.choices, expires_at=excluded.expires_at, + responded=excluded.responded` + _, err = s.db.ExecContext(ctx, q, + req.RequestID, req.MessageID, req.ChannelID, + req.AgentSlug, req.ProjectID, string(choicesJSON), + req.ExpiresAt.UTC().Format(time.RFC3339), boolToInt(req.Responded)) + return err +} + +func (s *sqliteStore) GetPendingAskUser(ctx context.Context, requestID string) (*PendingAskUser, error) { + const q = `SELECT request_id, message_id, channel_id, agent_slug, project_id, choices, expires_at, responded FROM pending_ask_users WHERE request_id = ?` + row := s.db.QueryRowContext(ctx, q, requestID) + + var p PendingAskUser + var choicesJSON, expiresAt string + var responded int + err := row.Scan(&p.RequestID, &p.MessageID, &p.ChannelID, &p.AgentSlug, &p.ProjectID, &choicesJSON, &expiresAt, &responded) + if err == sql.ErrNoRows { + return nil, nil + } + if err != nil { + return nil, err + } + if err := json.Unmarshal([]byte(choicesJSON), &p.Choices); err != nil { + return nil, fmt.Errorf("unmarshal choices: %w", err) + } + p.ExpiresAt, err = time.Parse(time.RFC3339, expiresAt) + if err != nil { + return nil, fmt.Errorf("parse expires_at: %w", err) + } + p.Responded = responded != 0 + return &p, nil +} + +func (s *sqliteStore) MarkAskUserResponded(ctx context.Context, requestID string) error { + _, err := s.db.ExecContext(ctx, `UPDATE pending_ask_users SET responded = 1 WHERE request_id = ?`, requestID) + return err +} + +func (s *sqliteStore) DeleteExpiredAskUsers(ctx context.Context) (int, error) { + result, err := s.db.ExecContext(ctx, `DELETE FROM pending_ask_users WHERE expires_at < ?`, time.Now().UTC().Format(time.RFC3339)) + if err != nil { + return 0, err + } + n, err := result.RowsAffected() + return int(n), err +} + +// --- CallbackLookup --- + +func (s *sqliteStore) CreateCallbackLookup(ctx context.Context, lookup *CallbackLookup) error { + const q = ` +INSERT INTO callback_lookups (short_id, full_data, expires_at) +VALUES (?, ?, ?) +ON CONFLICT(short_id) DO UPDATE SET + full_data=excluded.full_data, expires_at=excluded.expires_at` + _, err := s.db.ExecContext(ctx, q, + lookup.ShortID, lookup.FullData, + lookup.ExpiresAt.UTC().Format(time.RFC3339)) + return err +} + +func (s *sqliteStore) GetCallbackLookup(ctx context.Context, shortID string) (*CallbackLookup, error) { + const q = `SELECT short_id, full_data, expires_at FROM callback_lookups WHERE short_id = ?` + row := s.db.QueryRowContext(ctx, q, shortID) + + var cl CallbackLookup + var expiresAt string + err := row.Scan(&cl.ShortID, &cl.FullData, &expiresAt) + if err == sql.ErrNoRows { + return nil, nil + } + if err != nil { + return nil, err + } + cl.ExpiresAt, err = time.Parse(time.RFC3339, expiresAt) + if err != nil { + return nil, fmt.Errorf("parse expires_at: %w", err) + } + return &cl, nil +} + +func (s *sqliteStore) DeleteExpiredCallbacks(ctx context.Context) (int, error) { + result, err := s.db.ExecContext(ctx, `DELETE FROM callback_lookups WHERE expires_at < ?`, time.Now().UTC().Format(time.RFC3339)) + if err != nil { + return 0, err + } + n, err := result.RowsAffected() + return int(n), err +} + +// --- NotificationPref --- + +func (s *sqliteStore) SetNotificationPref(ctx context.Context, pref *NotificationPref) error { + const q = ` +INSERT INTO notification_prefs (discord_user_id, project_id, agent_slug, enabled, updated_at) +VALUES (?, ?, ?, ?, ?) +ON CONFLICT(discord_user_id, project_id, agent_slug) DO UPDATE SET + enabled=excluded.enabled, updated_at=excluded.updated_at` + _, err := s.db.ExecContext(ctx, q, + pref.DiscordUserID, pref.ProjectID, pref.AgentSlug, + boolToInt(pref.Enabled), pref.UpdatedAt.UTC().Format(time.RFC3339)) + return err +} + +func (s *sqliteStore) GetNotificationPrefs(ctx context.Context, discordUserID, projectID string) ([]*NotificationPref, error) { + const q = `SELECT discord_user_id, project_id, agent_slug, enabled, updated_at FROM notification_prefs WHERE discord_user_id = ? AND project_id = ?` + rows, err := s.db.QueryContext(ctx, q, discordUserID, projectID) + if err != nil { + return nil, err + } + defer rows.Close() + + var prefs []*NotificationPref + for rows.Next() { + var p NotificationPref + var enabled int + var updatedAt string + if err := rows.Scan(&p.DiscordUserID, &p.ProjectID, &p.AgentSlug, &enabled, &updatedAt); err != nil { + return nil, err + } + p.Enabled = enabled != 0 + p.UpdatedAt, err = time.Parse(time.RFC3339, updatedAt) + if err != nil { + return nil, fmt.Errorf("parse updated_at: %w", err) + } + prefs = append(prefs, &p) + } + return prefs, rows.Err() +} + +// --- scan helpers --- + +func scanChannelLink(row *sql.Row) (*ChannelLink, error) { + var link ChannelLink + var linkedAt string + var active, showA2A, showAssistantReply, showStateChanges, notifyInGroup, chatOnly int + err := row.Scan(&link.ChannelID, &link.GuildID, &link.ProjectID, &link.ProjectSlug, + &link.DefaultAgent, &link.LinkedBy, &linkedAt, &active, &showA2A, + &showAssistantReply, &showStateChanges, ¬ifyInGroup, &chatOnly) + if err == sql.ErrNoRows { + return nil, nil + } + if err != nil { + return nil, err + } + link.LinkedAt, err = time.Parse(time.RFC3339, linkedAt) + if err != nil { + return nil, fmt.Errorf("parse linked_at: %w", err) + } + link.Active = active != 0 + link.ShowAgentToAgent = showA2A != 0 + link.ShowAssistantReply = showAssistantReply != 0 + link.ShowStateChanges = showStateChanges != 0 + link.NotifyInGroup = notifyInGroup != 0 + link.ChatOnly = chatOnly != 0 + return &link, nil +} + +func scanChannelLinks(rows *sql.Rows) ([]*ChannelLink, error) { + var links []*ChannelLink + for rows.Next() { + var link ChannelLink + var linkedAt string + var active, showA2A, showAssistantReply, showStateChanges, notifyInGroup, chatOnly int + err := rows.Scan(&link.ChannelID, &link.GuildID, &link.ProjectID, &link.ProjectSlug, + &link.DefaultAgent, &link.LinkedBy, &linkedAt, &active, &showA2A, + &showAssistantReply, &showStateChanges, ¬ifyInGroup, &chatOnly) + if err != nil { + return nil, err + } + link.LinkedAt, err = time.Parse(time.RFC3339, linkedAt) + if err != nil { + return nil, fmt.Errorf("parse linked_at: %w", err) + } + link.Active = active != 0 + link.ShowAgentToAgent = showA2A != 0 + link.ShowAssistantReply = showAssistantReply != 0 + link.ShowStateChanges = showStateChanges != 0 + link.NotifyInGroup = notifyInGroup != 0 + link.ChatOnly = chatOnly != 0 + links = append(links, &link) + } + return links, rows.Err() +} + +func scanUserMapping(row *sql.Row) (*DiscordUserMapping, error) { + var m DiscordUserMapping + var linkedAt string + err := row.Scan(&m.DiscordUserID, &m.DiscordUsername, &m.ScionUserID, &m.ScionEmail, &linkedAt) + if err == sql.ErrNoRows { + return nil, nil + } + if err != nil { + return nil, err + } + m.LinkedAt, err = time.Parse(time.RFC3339, linkedAt) + if err != nil { + return nil, fmt.Errorf("parse linked_at: %w", err) + } + return &m, nil +} + +func boolToInt(b bool) int { + if b { + return 1 + } + return 0 +} diff --git a/extras/scion-discord/internal/discord/store_postgres.go b/extras/scion-discord/internal/discord/store_postgres.go new file mode 100644 index 000000000..dcfce5b18 --- /dev/null +++ b/extras/scion-discord/internal/discord/store_postgres.go @@ -0,0 +1,477 @@ +package discord + +import ( + "context" + "database/sql" + "encoding/json" + "fmt" + + _ "github.com/jackc/pgx/v5/stdlib" +) + +type postgresStore struct { + db *sql.DB +} + +func NewPostgresStore(databaseURL string) (Store, error) { + db, err := sql.Open("pgx", databaseURL) + if err != nil { + return nil, fmt.Errorf("open postgres database: %w", err) + } + + if err := db.Ping(); err != nil { + db.Close() + return nil, fmt.Errorf("ping postgres: %w", err) + } + + s := &postgresStore{db: db} + if err := s.createSchema(); err != nil { + db.Close() + return nil, fmt.Errorf("create schema: %w", err) + } + return s, nil +} + +func (s *postgresStore) createSchema() error { + const ddl = ` +CREATE TABLE IF NOT EXISTS discord_channel_links ( + channel_id TEXT PRIMARY KEY, + guild_id TEXT NOT NULL, + project_id TEXT NOT NULL, + project_slug TEXT NOT NULL DEFAULT '', + default_agent TEXT NOT NULL DEFAULT '', + linked_by TEXT NOT NULL DEFAULT '', + linked_at TIMESTAMPTZ NOT NULL, + active BOOLEAN NOT NULL DEFAULT TRUE, + show_agent_to_agent BOOLEAN NOT NULL DEFAULT FALSE, + show_assistant_reply BOOLEAN NOT NULL DEFAULT TRUE, + show_state_changes BOOLEAN NOT NULL DEFAULT TRUE, + notify_in_group BOOLEAN NOT NULL DEFAULT TRUE, + chat_only BOOLEAN NOT NULL DEFAULT FALSE +); + +CREATE INDEX IF NOT EXISTS idx_discord_channel_links_project ON discord_channel_links(project_id); +CREATE INDEX IF NOT EXISTS idx_discord_channel_links_guild ON discord_channel_links(guild_id); + +CREATE TABLE IF NOT EXISTS discord_user_mappings ( + discord_user_id TEXT PRIMARY KEY, + discord_username TEXT NOT NULL DEFAULT '', + scion_user_id TEXT NOT NULL DEFAULT '', + scion_email TEXT NOT NULL DEFAULT '', + linked_at TIMESTAMPTZ NOT NULL +); + +CREATE INDEX IF NOT EXISTS idx_discord_user_mappings_email ON discord_user_mappings(scion_email); +CREATE INDEX IF NOT EXISTS idx_discord_user_mappings_scion_id ON discord_user_mappings(scion_user_id); + +CREATE TABLE IF NOT EXISTS discord_conversation_context ( + discord_user_id TEXT NOT NULL, + project_id TEXT NOT NULL, + agent_slug TEXT NOT NULL, + last_channel_id TEXT NOT NULL, + last_message_at TIMESTAMPTZ NOT NULL, + PRIMARY KEY (discord_user_id, project_id, agent_slug) +); + +CREATE TABLE IF NOT EXISTS discord_project_agents ( + project_id TEXT PRIMARY KEY, + agent_slugs TEXT NOT NULL DEFAULT '[]', + refreshed_at TIMESTAMPTZ NOT NULL +); + +CREATE TABLE IF NOT EXISTS discord_pending_ask_users ( + request_id TEXT PRIMARY KEY, + message_id TEXT NOT NULL, + channel_id TEXT NOT NULL, + agent_slug TEXT NOT NULL DEFAULT '', + project_id TEXT NOT NULL DEFAULT '', + choices TEXT NOT NULL DEFAULT '[]', + expires_at TIMESTAMPTZ NOT NULL, + responded BOOLEAN NOT NULL DEFAULT FALSE +); + +CREATE TABLE IF NOT EXISTS discord_callback_lookups ( + short_id TEXT PRIMARY KEY, + full_data TEXT NOT NULL, + expires_at TIMESTAMPTZ NOT NULL +); + +CREATE TABLE IF NOT EXISTS discord_notification_prefs ( + discord_user_id TEXT NOT NULL, + project_id TEXT NOT NULL, + agent_slug TEXT NOT NULL, + enabled BOOLEAN NOT NULL DEFAULT TRUE, + updated_at TIMESTAMPTZ NOT NULL, + PRIMARY KEY (discord_user_id, project_id, agent_slug) +); +` + _, err := s.db.Exec(ddl) + return err +} + +func (s *postgresStore) Close() error { + return s.db.Close() +} + +// --- ChannelLink CRUD --- + +func (s *postgresStore) CreateChannelLink(ctx context.Context, link *ChannelLink) error { + const q = ` +INSERT INTO discord_channel_links (channel_id, guild_id, project_id, project_slug, default_agent, linked_by, linked_at, active, show_agent_to_agent, show_assistant_reply, show_state_changes, notify_in_group, chat_only) +VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13) +ON CONFLICT(channel_id) DO UPDATE SET + guild_id=EXCLUDED.guild_id, project_id=EXCLUDED.project_id, project_slug=EXCLUDED.project_slug, + default_agent=EXCLUDED.default_agent, linked_by=EXCLUDED.linked_by, linked_at=EXCLUDED.linked_at, + active=EXCLUDED.active, show_agent_to_agent=EXCLUDED.show_agent_to_agent, + show_assistant_reply=EXCLUDED.show_assistant_reply, show_state_changes=EXCLUDED.show_state_changes, + notify_in_group=EXCLUDED.notify_in_group, chat_only=EXCLUDED.chat_only` + _, err := s.db.ExecContext(ctx, q, + link.ChannelID, link.GuildID, link.ProjectID, link.ProjectSlug, + link.DefaultAgent, link.LinkedBy, link.LinkedAt.UTC(), + link.Active, link.ShowAgentToAgent, + link.ShowAssistantReply, link.ShowStateChanges, + link.NotifyInGroup, link.ChatOnly) + return err +} + +func (s *postgresStore) GetChannelLink(ctx context.Context, channelID string) (*ChannelLink, error) { + const q = `SELECT channel_id, guild_id, project_id, project_slug, default_agent, linked_by, linked_at, active, show_agent_to_agent, show_assistant_reply, show_state_changes, notify_in_group, chat_only FROM discord_channel_links WHERE channel_id = $1` + row := s.db.QueryRowContext(ctx, q, channelID) + return pgScanChannelLink(row) +} + +func (s *postgresStore) GetChannelLinksForProject(ctx context.Context, projectID string) ([]*ChannelLink, error) { + const q = `SELECT channel_id, guild_id, project_id, project_slug, default_agent, linked_by, linked_at, active, show_agent_to_agent, show_assistant_reply, show_state_changes, notify_in_group, chat_only FROM discord_channel_links WHERE project_id = $1` + rows, err := s.db.QueryContext(ctx, q, projectID) + if err != nil { + return nil, err + } + defer rows.Close() + return pgScanChannelLinks(rows) +} + +func (s *postgresStore) GetAllChannelLinks(ctx context.Context) ([]*ChannelLink, error) { + const q = `SELECT channel_id, guild_id, project_id, project_slug, default_agent, linked_by, linked_at, active, show_agent_to_agent, show_assistant_reply, show_state_changes, notify_in_group, chat_only FROM discord_channel_links` + rows, err := s.db.QueryContext(ctx, q) + if err != nil { + return nil, err + } + defer rows.Close() + return pgScanChannelLinks(rows) +} + +func (s *postgresStore) UpdateChannelLink(ctx context.Context, link *ChannelLink) error { + const q = ` +UPDATE discord_channel_links SET + guild_id=$1, project_id=$2, project_slug=$3, default_agent=$4, linked_by=$5, linked_at=$6, + active=$7, show_agent_to_agent=$8, show_assistant_reply=$9, show_state_changes=$10, + notify_in_group=$11, chat_only=$12 +WHERE channel_id=$13` + _, err := s.db.ExecContext(ctx, q, + link.GuildID, link.ProjectID, link.ProjectSlug, + link.DefaultAgent, link.LinkedBy, link.LinkedAt.UTC(), + link.Active, link.ShowAgentToAgent, + link.ShowAssistantReply, link.ShowStateChanges, + link.NotifyInGroup, link.ChatOnly, + link.ChannelID) + return err +} + +func (s *postgresStore) DeactivateLinksForGuild(ctx context.Context, guildID string) error { + _, err := s.db.ExecContext(ctx, `UPDATE discord_channel_links SET active = FALSE WHERE guild_id = $1`, guildID) + return err +} + +func (s *postgresStore) DeleteChannelLink(ctx context.Context, channelID string) error { + _, err := s.db.ExecContext(ctx, `DELETE FROM discord_channel_links WHERE channel_id = $1`, channelID) + return err +} + +// --- User mappings --- + +func (s *postgresStore) CreateUserMapping(ctx context.Context, mapping *DiscordUserMapping) error { + const q = ` +INSERT INTO discord_user_mappings (discord_user_id, discord_username, scion_user_id, scion_email, linked_at) +VALUES ($1, $2, $3, $4, $5) +ON CONFLICT(discord_user_id) DO UPDATE SET + discord_username=EXCLUDED.discord_username, scion_user_id=EXCLUDED.scion_user_id, + scion_email=EXCLUDED.scion_email, linked_at=EXCLUDED.linked_at` + _, err := s.db.ExecContext(ctx, q, + mapping.DiscordUserID, mapping.DiscordUsername, + mapping.ScionUserID, mapping.ScionEmail, + mapping.LinkedAt.UTC()) + return err +} + +func (s *postgresStore) GetUserMapping(ctx context.Context, discordUserID string) (*DiscordUserMapping, error) { + const q = `SELECT discord_user_id, discord_username, scion_user_id, scion_email, linked_at FROM discord_user_mappings WHERE discord_user_id = $1` + row := s.db.QueryRowContext(ctx, q, discordUserID) + return pgScanUserMapping(row) +} + +func (s *postgresStore) GetUserMappingByEmail(ctx context.Context, email string) (*DiscordUserMapping, error) { + const q = `SELECT discord_user_id, discord_username, scion_user_id, scion_email, linked_at FROM discord_user_mappings WHERE scion_email = $1` + row := s.db.QueryRowContext(ctx, q, email) + return pgScanUserMapping(row) +} + +func (s *postgresStore) GetUserMappingByScionUserID(ctx context.Context, userID string) (*DiscordUserMapping, error) { + const q = `SELECT discord_user_id, discord_username, scion_user_id, scion_email, linked_at FROM discord_user_mappings WHERE scion_user_id = $1` + row := s.db.QueryRowContext(ctx, q, userID) + return pgScanUserMapping(row) +} + +func (s *postgresStore) DeleteUserMapping(ctx context.Context, discordUserID string) error { + _, err := s.db.ExecContext(ctx, `DELETE FROM discord_user_mappings WHERE discord_user_id = $1`, discordUserID) + return err +} + +// --- ConversationContext --- + +func (s *postgresStore) SetConversationContext(ctx context.Context, cc *ConversationContext) error { + const q = ` +INSERT INTO discord_conversation_context (discord_user_id, project_id, agent_slug, last_channel_id, last_message_at) +VALUES ($1, $2, $3, $4, $5) +ON CONFLICT(discord_user_id, project_id, agent_slug) DO UPDATE SET + last_channel_id=EXCLUDED.last_channel_id, last_message_at=EXCLUDED.last_message_at` + _, err := s.db.ExecContext(ctx, q, + cc.DiscordUserID, cc.ProjectID, cc.AgentSlug, + cc.LastChannelID, cc.LastMessageAt.UTC()) + return err +} + +func (s *postgresStore) GetConversationContext(ctx context.Context, discordUserID, projectID, agentSlug string) (*ConversationContext, error) { + const q = `SELECT discord_user_id, project_id, agent_slug, last_channel_id, last_message_at FROM discord_conversation_context WHERE discord_user_id = $1 AND project_id = $2 AND agent_slug = $3` + row := s.db.QueryRowContext(ctx, q, discordUserID, projectID, agentSlug) + + var cc ConversationContext + err := row.Scan(&cc.DiscordUserID, &cc.ProjectID, &cc.AgentSlug, &cc.LastChannelID, &cc.LastMessageAt) + if err == sql.ErrNoRows { + return nil, nil + } + if err != nil { + return nil, err + } + return &cc, nil +} + +func (s *postgresStore) GetLatestConversationContext(ctx context.Context, discordUserID, projectID string) (*ConversationContext, error) { + const q = `SELECT discord_user_id, project_id, agent_slug, last_channel_id, last_message_at +FROM discord_conversation_context +WHERE discord_user_id = $1 AND project_id = $2 +ORDER BY last_message_at DESC LIMIT 1` + row := s.db.QueryRowContext(ctx, q, discordUserID, projectID) + + var cc ConversationContext + err := row.Scan(&cc.DiscordUserID, &cc.ProjectID, &cc.AgentSlug, &cc.LastChannelID, &cc.LastMessageAt) + if err == sql.ErrNoRows { + return nil, nil + } + if err != nil { + return nil, err + } + return &cc, nil +} + +// --- ProjectAgents --- + +func (s *postgresStore) SetProjectAgents(ctx context.Context, pa *ProjectAgents) error { + slugsJSON, err := json.Marshal(pa.AgentSlugs) + if err != nil { + return fmt.Errorf("marshal agent_slugs: %w", err) + } + const q = ` +INSERT INTO discord_project_agents (project_id, agent_slugs, refreshed_at) +VALUES ($1, $2, $3) +ON CONFLICT(project_id) DO UPDATE SET + agent_slugs=EXCLUDED.agent_slugs, refreshed_at=EXCLUDED.refreshed_at` + _, err = s.db.ExecContext(ctx, q, pa.ProjectID, string(slugsJSON), pa.RefreshedAt.UTC()) + return err +} + +func (s *postgresStore) GetProjectAgents(ctx context.Context, projectID string) (*ProjectAgents, error) { + const q = `SELECT project_id, agent_slugs, refreshed_at FROM discord_project_agents WHERE project_id = $1` + row := s.db.QueryRowContext(ctx, q, projectID) + + var pa ProjectAgents + var slugsJSON string + err := row.Scan(&pa.ProjectID, &slugsJSON, &pa.RefreshedAt) + if err == sql.ErrNoRows { + return nil, nil + } + if err != nil { + return nil, err + } + if err := json.Unmarshal([]byte(slugsJSON), &pa.AgentSlugs); err != nil { + return nil, fmt.Errorf("unmarshal agent_slugs: %w", err) + } + return &pa, nil +} + +// --- PendingAskUser --- + +func (s *postgresStore) CreatePendingAskUser(ctx context.Context, req *PendingAskUser) error { + choicesJSON, err := json.Marshal(req.Choices) + if err != nil { + return fmt.Errorf("marshal choices: %w", err) + } + const q = ` +INSERT INTO discord_pending_ask_users (request_id, message_id, channel_id, agent_slug, project_id, choices, expires_at, responded) +VALUES ($1, $2, $3, $4, $5, $6, $7, $8) +ON CONFLICT(request_id) DO UPDATE SET + message_id=EXCLUDED.message_id, channel_id=EXCLUDED.channel_id, agent_slug=EXCLUDED.agent_slug, + project_id=EXCLUDED.project_id, choices=EXCLUDED.choices, expires_at=EXCLUDED.expires_at, + responded=EXCLUDED.responded` + _, err = s.db.ExecContext(ctx, q, + req.RequestID, req.MessageID, req.ChannelID, + req.AgentSlug, req.ProjectID, string(choicesJSON), + req.ExpiresAt.UTC(), req.Responded) + return err +} + +func (s *postgresStore) GetPendingAskUser(ctx context.Context, requestID string) (*PendingAskUser, error) { + const q = `SELECT request_id, message_id, channel_id, agent_slug, project_id, choices, expires_at, responded FROM discord_pending_ask_users WHERE request_id = $1` + row := s.db.QueryRowContext(ctx, q, requestID) + + var p PendingAskUser + var choicesJSON string + err := row.Scan(&p.RequestID, &p.MessageID, &p.ChannelID, &p.AgentSlug, &p.ProjectID, &choicesJSON, &p.ExpiresAt, &p.Responded) + if err == sql.ErrNoRows { + return nil, nil + } + if err != nil { + return nil, err + } + if err := json.Unmarshal([]byte(choicesJSON), &p.Choices); err != nil { + return nil, fmt.Errorf("unmarshal choices: %w", err) + } + return &p, nil +} + +func (s *postgresStore) MarkAskUserResponded(ctx context.Context, requestID string) error { + _, err := s.db.ExecContext(ctx, `UPDATE discord_pending_ask_users SET responded = TRUE WHERE request_id = $1`, requestID) + return err +} + +func (s *postgresStore) DeleteExpiredAskUsers(ctx context.Context) (int, error) { + result, err := s.db.ExecContext(ctx, `DELETE FROM discord_pending_ask_users WHERE expires_at < NOW()`) + if err != nil { + return 0, err + } + n, err := result.RowsAffected() + return int(n), err +} + +// --- CallbackLookup --- + +func (s *postgresStore) CreateCallbackLookup(ctx context.Context, lookup *CallbackLookup) error { + const q = ` +INSERT INTO discord_callback_lookups (short_id, full_data, expires_at) +VALUES ($1, $2, $3) +ON CONFLICT(short_id) DO UPDATE SET + full_data=EXCLUDED.full_data, expires_at=EXCLUDED.expires_at` + _, err := s.db.ExecContext(ctx, q, + lookup.ShortID, lookup.FullData, + lookup.ExpiresAt.UTC()) + return err +} + +func (s *postgresStore) GetCallbackLookup(ctx context.Context, shortID string) (*CallbackLookup, error) { + const q = `SELECT short_id, full_data, expires_at FROM discord_callback_lookups WHERE short_id = $1` + row := s.db.QueryRowContext(ctx, q, shortID) + + var cl CallbackLookup + err := row.Scan(&cl.ShortID, &cl.FullData, &cl.ExpiresAt) + if err == sql.ErrNoRows { + return nil, nil + } + if err != nil { + return nil, err + } + return &cl, nil +} + +func (s *postgresStore) DeleteExpiredCallbacks(ctx context.Context) (int, error) { + result, err := s.db.ExecContext(ctx, `DELETE FROM discord_callback_lookups WHERE expires_at < NOW()`) + if err != nil { + return 0, err + } + n, err := result.RowsAffected() + return int(n), err +} + +// --- NotificationPref --- + +func (s *postgresStore) SetNotificationPref(ctx context.Context, pref *NotificationPref) error { + const q = ` +INSERT INTO discord_notification_prefs (discord_user_id, project_id, agent_slug, enabled, updated_at) +VALUES ($1, $2, $3, $4, $5) +ON CONFLICT(discord_user_id, project_id, agent_slug) DO UPDATE SET + enabled=EXCLUDED.enabled, updated_at=EXCLUDED.updated_at` + _, err := s.db.ExecContext(ctx, q, + pref.DiscordUserID, pref.ProjectID, pref.AgentSlug, + pref.Enabled, pref.UpdatedAt.UTC()) + return err +} + +func (s *postgresStore) GetNotificationPrefs(ctx context.Context, discordUserID, projectID string) ([]*NotificationPref, error) { + const q = `SELECT discord_user_id, project_id, agent_slug, enabled, updated_at FROM discord_notification_prefs WHERE discord_user_id = $1 AND project_id = $2` + rows, err := s.db.QueryContext(ctx, q, discordUserID, projectID) + if err != nil { + return nil, err + } + defer rows.Close() + + var prefs []*NotificationPref + for rows.Next() { + var p NotificationPref + if err := rows.Scan(&p.DiscordUserID, &p.ProjectID, &p.AgentSlug, &p.Enabled, &p.UpdatedAt); err != nil { + return nil, err + } + prefs = append(prefs, &p) + } + return prefs, rows.Err() +} + +// --- scan helpers --- + +func pgScanChannelLink(row *sql.Row) (*ChannelLink, error) { + var link ChannelLink + err := row.Scan(&link.ChannelID, &link.GuildID, &link.ProjectID, &link.ProjectSlug, + &link.DefaultAgent, &link.LinkedBy, &link.LinkedAt, &link.Active, &link.ShowAgentToAgent, + &link.ShowAssistantReply, &link.ShowStateChanges, &link.NotifyInGroup, &link.ChatOnly) + if err == sql.ErrNoRows { + return nil, nil + } + if err != nil { + return nil, err + } + return &link, nil +} + +func pgScanChannelLinks(rows *sql.Rows) ([]*ChannelLink, error) { + var links []*ChannelLink + for rows.Next() { + var link ChannelLink + err := rows.Scan(&link.ChannelID, &link.GuildID, &link.ProjectID, &link.ProjectSlug, + &link.DefaultAgent, &link.LinkedBy, &link.LinkedAt, &link.Active, &link.ShowAgentToAgent, + &link.ShowAssistantReply, &link.ShowStateChanges, &link.NotifyInGroup, &link.ChatOnly) + if err != nil { + return nil, err + } + links = append(links, &link) + } + return links, rows.Err() +} + +func pgScanUserMapping(row *sql.Row) (*DiscordUserMapping, error) { + var m DiscordUserMapping + err := row.Scan(&m.DiscordUserID, &m.DiscordUsername, &m.ScionUserID, &m.ScionEmail, &m.LinkedAt) + if err == sql.ErrNoRows { + return nil, nil + } + if err != nil { + return nil, err + } + return &m, nil +} diff --git a/extras/scion-discord/internal/discord/store_test.go b/extras/scion-discord/internal/discord/store_test.go new file mode 100644 index 000000000..66b371ec5 --- /dev/null +++ b/extras/scion-discord/internal/discord/store_test.go @@ -0,0 +1,672 @@ +package discord + +import ( + "context" + "path/filepath" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func newTestStore(t *testing.T) Store { + t.Helper() + dbPath := filepath.Join(t.TempDir(), "test.db") + store, err := NewSQLiteStore(dbPath) + require.NoError(t, err) + t.Cleanup(func() { store.Close() }) + return store +} + +// --- ChannelLink CRUD --- + +func TestChannelLinkCRUD(t *testing.T) { + t.Run("CreateAndGet", func(t *testing.T) { + store := newTestStore(t) + ctx := context.Background() + + link := &ChannelLink{ + ChannelID: "111222333444555666", + GuildID: "999888777666555444", + ProjectID: "proj-1", + ProjectSlug: "my-project", + DefaultAgent: "coder", + LinkedBy: "456789012345678901", + LinkedAt: time.Date(2026, 1, 15, 10, 0, 0, 0, time.UTC), + Active: true, + ShowAgentToAgent: false, + ShowAssistantReply: true, + ShowStateChanges: true, + NotifyInGroup: true, + ChatOnly: false, + } + + require.NoError(t, store.CreateChannelLink(ctx, link)) + + got, err := store.GetChannelLink(ctx, "111222333444555666") + require.NoError(t, err) + require.NotNil(t, got) + + assert.Equal(t, "111222333444555666", got.ChannelID) + assert.Equal(t, "999888777666555444", got.GuildID) + assert.Equal(t, "proj-1", got.ProjectID) + assert.Equal(t, "my-project", got.ProjectSlug) + assert.Equal(t, "coder", got.DefaultAgent) + assert.Equal(t, "456789012345678901", got.LinkedBy) + assert.True(t, got.Active) + assert.False(t, got.ShowAgentToAgent) + assert.True(t, got.ShowAssistantReply) + assert.True(t, got.ShowStateChanges) + assert.True(t, got.NotifyInGroup) + assert.False(t, got.ChatOnly) + assert.Equal(t, 2026, got.LinkedAt.Year()) + }) + + t.Run("GetNotFound", func(t *testing.T) { + store := newTestStore(t) + ctx := context.Background() + + got, err := store.GetChannelLink(ctx, "nonexistent") + require.NoError(t, err) + assert.Nil(t, got) + }) + + t.Run("Upsert", func(t *testing.T) { + store := newTestStore(t) + ctx := context.Background() + + link := &ChannelLink{ + ChannelID: "111222333", + GuildID: "999888777", + ProjectID: "proj-1", + DefaultAgent: "coder", + LinkedAt: time.Now().UTC(), + Active: true, + } + require.NoError(t, store.CreateChannelLink(ctx, link)) + + link.DefaultAgent = "reviewer" + link.ProjectSlug = "updated-slug" + link.ShowAgentToAgent = true + require.NoError(t, store.CreateChannelLink(ctx, link)) + + got, err := store.GetChannelLink(ctx, "111222333") + require.NoError(t, err) + require.NotNil(t, got) + assert.Equal(t, "reviewer", got.DefaultAgent) + assert.Equal(t, "updated-slug", got.ProjectSlug) + assert.True(t, got.ShowAgentToAgent) + }) + + t.Run("GetByProject", func(t *testing.T) { + store := newTestStore(t) + ctx := context.Background() + + channels := []string{"100", "200", "300"} + for i, chID := range channels { + projID := "proj-1" + if i == 2 { + projID = "proj-2" + } + require.NoError(t, store.CreateChannelLink(ctx, &ChannelLink{ + ChannelID: chID, + GuildID: "guild-1", + ProjectID: projID, + LinkedAt: time.Now().UTC(), + Active: true, + })) + } + + links, err := store.GetChannelLinksForProject(ctx, "proj-1") + require.NoError(t, err) + assert.Len(t, links, 2) + + links, err = store.GetChannelLinksForProject(ctx, "proj-2") + require.NoError(t, err) + assert.Len(t, links, 1) + + links, err = store.GetChannelLinksForProject(ctx, "proj-nonexistent") + require.NoError(t, err) + assert.Len(t, links, 0) + }) + + t.Run("GetAll", func(t *testing.T) { + store := newTestStore(t) + ctx := context.Background() + + links, err := store.GetAllChannelLinks(ctx) + require.NoError(t, err) + assert.Len(t, links, 0) + + for _, chID := range []string{"100", "200", "300"} { + require.NoError(t, store.CreateChannelLink(ctx, &ChannelLink{ + ChannelID: chID, + GuildID: "guild-1", + ProjectID: "proj-1", + LinkedAt: time.Now().UTC(), + Active: true, + })) + } + + links, err = store.GetAllChannelLinks(ctx) + require.NoError(t, err) + assert.Len(t, links, 3) + }) + + t.Run("Update", func(t *testing.T) { + store := newTestStore(t) + ctx := context.Background() + + link := &ChannelLink{ + ChannelID: "111", + GuildID: "999", + ProjectID: "proj-1", + DefaultAgent: "coder", + LinkedAt: time.Now().UTC(), + Active: true, + ShowAssistantReply: true, + NotifyInGroup: true, + } + require.NoError(t, store.CreateChannelLink(ctx, link)) + + link.DefaultAgent = "reviewer" + link.ChatOnly = true + require.NoError(t, store.UpdateChannelLink(ctx, link)) + + got, err := store.GetChannelLink(ctx, "111") + require.NoError(t, err) + require.NotNil(t, got) + assert.Equal(t, "reviewer", got.DefaultAgent) + assert.True(t, got.ChatOnly) + }) + + t.Run("DeactivateForGuild", func(t *testing.T) { + store := newTestStore(t) + ctx := context.Background() + + for _, chID := range []string{"100", "200"} { + require.NoError(t, store.CreateChannelLink(ctx, &ChannelLink{ + ChannelID: chID, + GuildID: "guild-1", + ProjectID: "proj-1", + LinkedAt: time.Now().UTC(), + Active: true, + })) + } + require.NoError(t, store.CreateChannelLink(ctx, &ChannelLink{ + ChannelID: "300", + GuildID: "guild-2", + ProjectID: "proj-2", + LinkedAt: time.Now().UTC(), + Active: true, + })) + + require.NoError(t, store.DeactivateLinksForGuild(ctx, "guild-1")) + + got1, err := store.GetChannelLink(ctx, "100") + require.NoError(t, err) + require.NotNil(t, got1) + assert.False(t, got1.Active) + + got2, err := store.GetChannelLink(ctx, "200") + require.NoError(t, err) + require.NotNil(t, got2) + assert.False(t, got2.Active) + + // Channel in different guild should remain active. + got3, err := store.GetChannelLink(ctx, "300") + require.NoError(t, err) + require.NotNil(t, got3) + assert.True(t, got3.Active) + }) + + t.Run("Delete", func(t *testing.T) { + store := newTestStore(t) + ctx := context.Background() + + require.NoError(t, store.CreateChannelLink(ctx, &ChannelLink{ + ChannelID: "100", + GuildID: "guild-1", + ProjectID: "proj-1", + LinkedAt: time.Now().UTC(), + Active: true, + })) + + require.NoError(t, store.DeleteChannelLink(ctx, "100")) + + got, err := store.GetChannelLink(ctx, "100") + require.NoError(t, err) + assert.Nil(t, got) + + // Delete non-existent is not an error. + require.NoError(t, store.DeleteChannelLink(ctx, "nonexistent")) + }) +} + +// --- UserMapping CRUD --- + +func TestUserMappingCRUD(t *testing.T) { + t.Run("CreateAndGet", func(t *testing.T) { + store := newTestStore(t) + ctx := context.Background() + + mapping := &DiscordUserMapping{ + DiscordUserID: "456789012345678901", + DiscordUsername: "alice", + ScionUserID: "user-123", + ScionEmail: "alice@example.com", + LinkedAt: time.Date(2026, 3, 1, 0, 0, 0, 0, time.UTC), + } + require.NoError(t, store.CreateUserMapping(ctx, mapping)) + + got, err := store.GetUserMapping(ctx, "456789012345678901") + require.NoError(t, err) + require.NotNil(t, got) + assert.Equal(t, "456789012345678901", got.DiscordUserID) + assert.Equal(t, "alice", got.DiscordUsername) + assert.Equal(t, "user-123", got.ScionUserID) + assert.Equal(t, "alice@example.com", got.ScionEmail) + }) + + t.Run("GetNotFound", func(t *testing.T) { + store := newTestStore(t) + ctx := context.Background() + + got, err := store.GetUserMapping(ctx, "unknown") + require.NoError(t, err) + assert.Nil(t, got) + }) + + t.Run("GetByEmail", func(t *testing.T) { + store := newTestStore(t) + ctx := context.Background() + + require.NoError(t, store.CreateUserMapping(ctx, &DiscordUserMapping{ + DiscordUserID: "456", + ScionEmail: "alice@example.com", + LinkedAt: time.Now().UTC(), + })) + + got, err := store.GetUserMappingByEmail(ctx, "alice@example.com") + require.NoError(t, err) + require.NotNil(t, got) + assert.Equal(t, "456", got.DiscordUserID) + + got, err = store.GetUserMappingByEmail(ctx, "nobody@example.com") + require.NoError(t, err) + assert.Nil(t, got) + }) + + t.Run("GetByScionUserID", func(t *testing.T) { + store := newTestStore(t) + ctx := context.Background() + + require.NoError(t, store.CreateUserMapping(ctx, &DiscordUserMapping{ + DiscordUserID: "456", + ScionUserID: "user-123", + LinkedAt: time.Now().UTC(), + })) + + got, err := store.GetUserMappingByScionUserID(ctx, "user-123") + require.NoError(t, err) + require.NotNil(t, got) + assert.Equal(t, "456", got.DiscordUserID) + + got, err = store.GetUserMappingByScionUserID(ctx, "nonexistent") + require.NoError(t, err) + assert.Nil(t, got) + }) + + t.Run("Upsert", func(t *testing.T) { + store := newTestStore(t) + ctx := context.Background() + + require.NoError(t, store.CreateUserMapping(ctx, &DiscordUserMapping{ + DiscordUserID: "456", + DiscordUsername: "alice", + ScionEmail: "alice@old.com", + LinkedAt: time.Now().UTC(), + })) + + require.NoError(t, store.CreateUserMapping(ctx, &DiscordUserMapping{ + DiscordUserID: "456", + DiscordUsername: "alice_new", + ScionEmail: "alice@new.com", + LinkedAt: time.Now().UTC(), + })) + + got, err := store.GetUserMapping(ctx, "456") + require.NoError(t, err) + require.NotNil(t, got) + assert.Equal(t, "alice_new", got.DiscordUsername) + assert.Equal(t, "alice@new.com", got.ScionEmail) + }) + + t.Run("Delete", func(t *testing.T) { + store := newTestStore(t) + ctx := context.Background() + + require.NoError(t, store.CreateUserMapping(ctx, &DiscordUserMapping{ + DiscordUserID: "456", + ScionEmail: "alice@example.com", + LinkedAt: time.Now().UTC(), + })) + + require.NoError(t, store.DeleteUserMapping(ctx, "456")) + + got, err := store.GetUserMapping(ctx, "456") + require.NoError(t, err) + assert.Nil(t, got) + + // Delete non-existent is not an error. + require.NoError(t, store.DeleteUserMapping(ctx, "nonexistent")) + }) +} + +// --- ConversationContext --- + +func TestConversationContext(t *testing.T) { + t.Run("SetAndGet", func(t *testing.T) { + store := newTestStore(t) + ctx := context.Background() + + cc := &ConversationContext{ + DiscordUserID: "456", + ProjectID: "proj-1", + AgentSlug: "coder", + LastChannelID: "111222333", + LastMessageAt: time.Date(2026, 5, 1, 12, 0, 0, 0, time.UTC), + } + require.NoError(t, store.SetConversationContext(ctx, cc)) + + got, err := store.GetConversationContext(ctx, "456", "proj-1", "coder") + require.NoError(t, err) + require.NotNil(t, got) + assert.Equal(t, "456", got.DiscordUserID) + assert.Equal(t, "proj-1", got.ProjectID) + assert.Equal(t, "coder", got.AgentSlug) + assert.Equal(t, "111222333", got.LastChannelID) + assert.Equal(t, 2026, got.LastMessageAt.Year()) + }) + + t.Run("GetNotFound", func(t *testing.T) { + store := newTestStore(t) + ctx := context.Background() + + got, err := store.GetConversationContext(ctx, "unknown", "proj-1", "coder") + require.NoError(t, err) + assert.Nil(t, got) + }) + + t.Run("Upsert", func(t *testing.T) { + store := newTestStore(t) + ctx := context.Background() + + cc := &ConversationContext{ + DiscordUserID: "456", + ProjectID: "proj-1", + AgentSlug: "coder", + LastChannelID: "100", + LastMessageAt: time.Now().UTC(), + } + require.NoError(t, store.SetConversationContext(ctx, cc)) + + cc.LastChannelID = "200" + cc.LastMessageAt = time.Now().UTC().Add(time.Hour) + require.NoError(t, store.SetConversationContext(ctx, cc)) + + got, err := store.GetConversationContext(ctx, "456", "proj-1", "coder") + require.NoError(t, err) + require.NotNil(t, got) + assert.Equal(t, "200", got.LastChannelID) + }) + + t.Run("MultipleKeys", func(t *testing.T) { + store := newTestStore(t) + ctx := context.Background() + + now := time.Now().UTC() + for _, slug := range []string{"coder", "reviewer"} { + require.NoError(t, store.SetConversationContext(ctx, &ConversationContext{ + DiscordUserID: "456", + ProjectID: "proj-1", + AgentSlug: slug, + LastChannelID: "100", + LastMessageAt: now, + })) + } + + got1, err := store.GetConversationContext(ctx, "456", "proj-1", "coder") + require.NoError(t, err) + require.NotNil(t, got1) + + got2, err := store.GetConversationContext(ctx, "456", "proj-1", "reviewer") + require.NoError(t, err) + require.NotNil(t, got2) + + assert.Equal(t, "coder", got1.AgentSlug) + assert.Equal(t, "reviewer", got2.AgentSlug) + }) + + t.Run("GetLatest", func(t *testing.T) { + store := newTestStore(t) + ctx := context.Background() + + // Save two contexts with different timestamps -- "reviewer" is more recent. + require.NoError(t, store.SetConversationContext(ctx, &ConversationContext{ + DiscordUserID: "456", + ProjectID: "proj-1", + AgentSlug: "coder", + LastChannelID: "100", + LastMessageAt: time.Date(2026, 5, 10, 10, 0, 0, 0, time.UTC), + })) + require.NoError(t, store.SetConversationContext(ctx, &ConversationContext{ + DiscordUserID: "456", + ProjectID: "proj-1", + AgentSlug: "reviewer", + LastChannelID: "100", + LastMessageAt: time.Date(2026, 5, 12, 10, 0, 0, 0, time.UTC), + })) + + got, err := store.GetLatestConversationContext(ctx, "456", "proj-1") + require.NoError(t, err) + require.NotNil(t, got) + assert.Equal(t, "reviewer", got.AgentSlug) + }) + + t.Run("GetLatestNotFound", func(t *testing.T) { + store := newTestStore(t) + ctx := context.Background() + + got, err := store.GetLatestConversationContext(ctx, "999", "proj-unknown") + require.NoError(t, err) + assert.Nil(t, got) + }) +} + +// --- ProjectAgents --- + +func TestProjectAgents(t *testing.T) { + t.Run("SetAndGet", func(t *testing.T) { + store := newTestStore(t) + ctx := context.Background() + + pa := &ProjectAgents{ + ProjectID: "proj-1", + AgentSlugs: []string{"coder", "reviewer", "tester"}, + RefreshedAt: time.Date(2026, 5, 10, 8, 0, 0, 0, time.UTC), + } + require.NoError(t, store.SetProjectAgents(ctx, pa)) + + got, err := store.GetProjectAgents(ctx, "proj-1") + require.NoError(t, err) + require.NotNil(t, got) + assert.Equal(t, "proj-1", got.ProjectID) + assert.Equal(t, []string{"coder", "reviewer", "tester"}, got.AgentSlugs) + assert.Equal(t, 2026, got.RefreshedAt.Year()) + }) + + t.Run("GetNotFound", func(t *testing.T) { + store := newTestStore(t) + ctx := context.Background() + + got, err := store.GetProjectAgents(ctx, "nonexistent") + require.NoError(t, err) + assert.Nil(t, got) + }) + + t.Run("Upsert", func(t *testing.T) { + store := newTestStore(t) + ctx := context.Background() + + pa := &ProjectAgents{ + ProjectID: "proj-1", + AgentSlugs: []string{"coder"}, + RefreshedAt: time.Now().UTC(), + } + require.NoError(t, store.SetProjectAgents(ctx, pa)) + + pa.AgentSlugs = []string{"coder", "reviewer"} + pa.RefreshedAt = time.Now().UTC().Add(time.Hour) + require.NoError(t, store.SetProjectAgents(ctx, pa)) + + got, err := store.GetProjectAgents(ctx, "proj-1") + require.NoError(t, err) + require.NotNil(t, got) + assert.Equal(t, []string{"coder", "reviewer"}, got.AgentSlugs) + }) + + t.Run("EmptySlice", func(t *testing.T) { + store := newTestStore(t) + ctx := context.Background() + + pa := &ProjectAgents{ + ProjectID: "proj-1", + AgentSlugs: []string{}, + RefreshedAt: time.Now().UTC(), + } + require.NoError(t, store.SetProjectAgents(ctx, pa)) + + got, err := store.GetProjectAgents(ctx, "proj-1") + require.NoError(t, err) + require.NotNil(t, got) + assert.Equal(t, []string{}, got.AgentSlugs) + }) +} + +// --- PendingAskUser --- + +func TestPendingAskUser(t *testing.T) { + t.Run("CreateAndGet", func(t *testing.T) { + store := newTestStore(t) + ctx := context.Background() + + pending := &PendingAskUser{ + RequestID: "req-123", + MessageID: "111222333444555666", + ChannelID: "999888777666555444", + AgentSlug: "coder", + ProjectID: "proj-1", + Choices: []string{"Yes", "No", "Maybe"}, + ExpiresAt: time.Date(2026, 6, 1, 0, 0, 0, 0, time.UTC), + Responded: false, + } + require.NoError(t, store.CreatePendingAskUser(ctx, pending)) + + got, err := store.GetPendingAskUser(ctx, "req-123") + require.NoError(t, err) + require.NotNil(t, got) + assert.Equal(t, "req-123", got.RequestID) + assert.Equal(t, "111222333444555666", got.MessageID) + assert.Equal(t, "999888777666555444", got.ChannelID) + assert.Equal(t, "coder", got.AgentSlug) + assert.Equal(t, "proj-1", got.ProjectID) + assert.Equal(t, []string{"Yes", "No", "Maybe"}, got.Choices) + assert.False(t, got.Responded) + }) + + t.Run("GetNotFound", func(t *testing.T) { + store := newTestStore(t) + ctx := context.Background() + + got, err := store.GetPendingAskUser(ctx, "nonexistent") + require.NoError(t, err) + assert.Nil(t, got) + }) + + t.Run("MarkResponded", func(t *testing.T) { + store := newTestStore(t) + ctx := context.Background() + + require.NoError(t, store.CreatePendingAskUser(ctx, &PendingAskUser{ + RequestID: "req-123", + MessageID: "42", + ChannelID: "100", + ExpiresAt: time.Now().Add(time.Hour).UTC(), + })) + + require.NoError(t, store.MarkAskUserResponded(ctx, "req-123")) + + got, err := store.GetPendingAskUser(ctx, "req-123") + require.NoError(t, err) + require.NotNil(t, got) + assert.True(t, got.Responded) + }) + + t.Run("DeleteExpired", func(t *testing.T) { + store := newTestStore(t) + ctx := context.Background() + + // Save one expired and one active. + require.NoError(t, store.CreatePendingAskUser(ctx, &PendingAskUser{ + RequestID: "expired", + MessageID: "1", + ChannelID: "100", + ExpiresAt: time.Now().Add(-time.Hour).UTC(), + })) + require.NoError(t, store.CreatePendingAskUser(ctx, &PendingAskUser{ + RequestID: "active", + MessageID: "2", + ChannelID: "100", + ExpiresAt: time.Now().Add(time.Hour).UTC(), + })) + + n, err := store.DeleteExpiredAskUsers(ctx) + require.NoError(t, err) + assert.Equal(t, 1, n) + + got, err := store.GetPendingAskUser(ctx, "expired") + require.NoError(t, err) + assert.Nil(t, got) + + got, err = store.GetPendingAskUser(ctx, "active") + require.NoError(t, err) + assert.NotNil(t, got) + }) + + t.Run("EmptyChoices", func(t *testing.T) { + store := newTestStore(t) + ctx := context.Background() + + require.NoError(t, store.CreatePendingAskUser(ctx, &PendingAskUser{ + RequestID: "req-empty", + MessageID: "1", + ChannelID: "100", + Choices: []string{}, + ExpiresAt: time.Now().Add(time.Hour).UTC(), + })) + + got, err := store.GetPendingAskUser(ctx, "req-empty") + require.NoError(t, err) + require.NotNil(t, got) + assert.Equal(t, []string{}, got.Choices) + }) +} + +// --- Store lifecycle --- + +func TestStore_OpenInvalidPath(t *testing.T) { + _, err := NewSQLiteStore("/nonexistent/dir/test.db") + assert.Error(t, err) +} diff --git a/extras/scion-discord/internal/discord/webhooks.go b/extras/scion-discord/internal/discord/webhooks.go new file mode 100644 index 000000000..992ba0c4a --- /dev/null +++ b/extras/scion-discord/internal/discord/webhooks.go @@ -0,0 +1,197 @@ +package discord + +import ( + "errors" + "fmt" + "log/slog" + "net/http" + "net/url" + "strconv" + "strings" + "sync" + + "github.com/bwmarrin/discordgo" +) + +const ( + // webhookName is the name used for all Scion-managed channel webhooks. + // One webhook per channel is created lazily and reused for all agent messages. + webhookName = "Scion Agent Relay" +) + +// WebhookManager manages per-channel Discord webhooks used to send messages +// with per-agent identity (custom username and avatar). It lazily creates +// one webhook per channel, caches them in memory, and auto-recreates if +// a webhook is deleted externally. +type WebhookManager struct { + session *discordgo.Session + log *slog.Logger + + mu sync.RWMutex + cache map[string]*discordgo.Webhook // channelID -> webhook +} + +// NewWebhookManager creates a new WebhookManager. +func NewWebhookManager(session *discordgo.Session, log *slog.Logger) *WebhookManager { + if log == nil { + log = slog.Default() + } + return &WebhookManager{ + session: session, + log: log, + cache: make(map[string]*discordgo.Webhook), + } +} + +// getOrCreateWebhook returns the cached webhook for a channel, or discovers/ +// creates one. The lifecycle is: +// 1. Check in-memory cache (fast path, read lock) +// 2. Query Discord for existing channel webhooks owned by us +// 3. Create a new webhook if none found +func (wm *WebhookManager) getOrCreateWebhook(channelID string) (*discordgo.Webhook, error) { + // Fast path: check cache. + wm.mu.RLock() + if wh, ok := wm.cache[channelID]; ok { + wm.mu.RUnlock() + return wh, nil + } + wm.mu.RUnlock() + + // Slow path: look for existing webhook or create one. + wm.mu.Lock() + defer wm.mu.Unlock() + + // Double-check after acquiring write lock. + if wh, ok := wm.cache[channelID]; ok { + return wh, nil + } + + // Check existing channel webhooks for one we own. + webhooks, err := wm.session.ChannelWebhooks(channelID) + if err != nil { + return nil, fmt.Errorf("list channel webhooks: %w", err) + } + + botUserID := "" + if wm.session.State != nil && wm.session.State.User != nil { + botUserID = wm.session.State.User.ID + } + + for _, wh := range webhooks { + if wh.Name == webhookName && wh.User != nil && wh.User.ID == botUserID { + wm.cache[channelID] = wh + wm.log.Debug("Reusing existing webhook", + "channel_id", channelID, + "webhook_id", wh.ID) + return wh, nil + } + } + + // No existing webhook — create one. + wh, err := wm.session.WebhookCreate(channelID, webhookName, "") + if err != nil { + return nil, fmt.Errorf("create webhook: %w", err) + } + + wm.cache[channelID] = wh + wm.log.Info("Created webhook for channel", + "channel_id", channelID, + "webhook_id", wh.ID) + return wh, nil +} + +// invalidate removes a cached webhook for a channel, forcing re-discovery +// on the next send. +func (wm *WebhookManager) invalidate(channelID string) { + wm.mu.Lock() + delete(wm.cache, channelID) + wm.mu.Unlock() +} + +// SendAsAgent sends a message via webhook with the agent's identity (name + avatar). +// If the webhook has been deleted externally (404/Unknown Webhook), the cache +// entry is invalidated and a new webhook is created for a retry. +func (wm *WebhookManager) SendAsAgent(channelID, agentSlug, content string, embeds []*discordgo.MessageEmbed, components []discordgo.MessageComponent) (*discordgo.Message, error) { + wh, err := wm.getOrCreateWebhook(channelID) + if err != nil { + return nil, fmt.Errorf("get webhook for channel %s: %w", channelID, err) + } + + params := &discordgo.WebhookParams{ + Content: content, + Username: agentSlug, + AvatarURL: agentIconURL(agentSlug), + Embeds: embeds, + Components: components, + } + + // wait=true so discordgo returns the created Message object. + msg, err := wm.session.WebhookExecute(wh.ID, wh.Token, true, params) + if err != nil { + // Check for 404 / Unknown Webhook — the webhook was deleted externally. + if isWebhookNotFound(err) { + wm.log.Warn("Webhook gone (deleted externally), recreating", + "channel_id", channelID, + "webhook_id", wh.ID) + wm.invalidate(channelID) + + // Retry once with a fresh webhook. + wh2, err2 := wm.getOrCreateWebhook(channelID) + if err2 != nil { + return nil, fmt.Errorf("recreate webhook after 404: %w", err2) + } + msg, err = wm.session.WebhookExecute(wh2.ID, wh2.Token, true, params) + if err != nil { + return nil, fmt.Errorf("webhook send after recreate: %w", err) + } + return msg, nil + } + return nil, fmt.Errorf("webhook execute: %w", err) + } + + return msg, nil +} + +// agentIconURL returns a deterministic avatar URL for an agent using RoboHash. +// Discord recommends webhook avatars be at least 128×128 pixels. +func agentIconURL(agentSlug string) string { + return fmt.Sprintf("https://robohash.org/%s?set=set1&size=128x128", url.PathEscape(agentSlug)) +} + +// isWebhookNotFound checks whether a Discord API error indicates that the +// webhook no longer exists (HTTP 404 or Discord error code 10015 "Unknown Webhook"). +func isWebhookNotFound(err error) bool { + if err == nil { + return false + } + + // discordgo wraps REST errors as *discordgo.RESTError. + var restErr *discordgo.RESTError + if errors.As(err, &restErr) { + if restErr.Response != nil && restErr.Response.StatusCode == http.StatusNotFound { + return true + } + // Discord error code 10015 = Unknown Webhook. + if restErr.Message != nil && restErr.Message.Code == 10015 { + return true + } + } + + // Fallback: check error string for common patterns. + s := err.Error() + return strings.Contains(s, "10015") || strings.Contains(s, "Unknown Webhook") +} + +// isDiscordHTTPError checks whether err represents a specific HTTP status code +// from the Discord API. Used for error classification in retry logic. +func isDiscordHTTPError(err error, statusCode int) bool { + if err == nil { + return false + } + var restErr *discordgo.RESTError + if errors.As(err, &restErr) { + return restErr.Response != nil && restErr.Response.StatusCode == statusCode + } + // Fallback: check for status code in error string. + return strings.Contains(err.Error(), strconv.Itoa(statusCode)) +} diff --git a/extras/scion-telegram/go.mod b/extras/scion-telegram/go.mod index 436da3fc1..e0dcc5067 100644 --- a/extras/scion-telegram/go.mod +++ b/extras/scion-telegram/go.mod @@ -1,6 +1,6 @@ module github.com/GoogleCloudPlatform/scion/extras/scion-telegram -go 1.25.4 +go 1.26.1 require ( github.com/GoogleCloudPlatform/scion v0.0.0-00010101000000-000000000000 diff --git a/extras/scion-telegram/internal/telegram/api.go b/extras/scion-telegram/internal/telegram/api.go index ba3e03ef0..5e72403be 100644 --- a/extras/scion-telegram/internal/telegram/api.go +++ b/extras/scion-telegram/internal/telegram/api.go @@ -187,9 +187,10 @@ type CallbackQuery struct { // sendMessageRequest is the JSON body for the sendMessage API call. type sendMessageRequest struct { - ChatID int64 `json:"chat_id"` - Text string `json:"text"` - ParseMode string `json:"parse_mode,omitempty"` + ChatID int64 `json:"chat_id"` + Text string `json:"text"` + ParseMode string `json:"parse_mode,omitempty"` + MessageThreadID int64 `json:"message_thread_id,omitempty"` } // sendMessageWithKeyboardRequest is the JSON body for sendMessage with an inline keyboard. @@ -199,6 +200,7 @@ type sendMessageWithKeyboardRequest struct { ParseMode string `json:"parse_mode,omitempty"` ReplyMarkup *InlineKeyboardMarkup `json:"reply_markup,omitempty"` ReplyToMessageID int64 `json:"reply_to_message_id,omitempty"` + MessageThreadID int64 `json:"message_thread_id,omitempty"` } // ForceReply instructs Telegram clients to display a reply interface to the @@ -211,10 +213,11 @@ type ForceReply struct { // sendMessageForceReplyRequest is the JSON body for sendMessage with // ForceReply markup and an optional inline keyboard. type sendMessageForceReplyRequest struct { - ChatID int64 `json:"chat_id"` - Text string `json:"text"` - ParseMode string `json:"parse_mode,omitempty"` - ReplyMarkup json.RawMessage `json:"reply_markup,omitempty"` + ChatID int64 `json:"chat_id"` + Text string `json:"text"` + ParseMode string `json:"parse_mode,omitempty"` + ReplyMarkup json.RawMessage `json:"reply_markup,omitempty"` + MessageThreadID int64 `json:"message_thread_id,omitempty"` } // editMessageTextRequest is the JSON body for the editMessageText API call. @@ -458,13 +461,21 @@ func (c *TelegramAPIClient) GetUpdates(ctx context.Context, offset int64, timeou return updates, nil } +// SendOption provides optional parameters for send methods. +type SendOption struct { + MessageThreadID int64 +} + // SendMessage sends a text message to the specified chat. -func (c *TelegramAPIClient) SendMessage(ctx context.Context, chatID int64, text, parseMode string) (*TGMessage, error) { +func (c *TelegramAPIClient) SendMessage(ctx context.Context, chatID int64, text, parseMode string, opts ...SendOption) (*TGMessage, error) { body := sendMessageRequest{ ChatID: chatID, Text: text, ParseMode: parseMode, } + for _, o := range opts { + body.MessageThreadID = o.MessageThreadID + } jsonBody, err := json.Marshal(body) if err != nil { @@ -506,7 +517,7 @@ func (c *TelegramAPIClient) SendMessage(ctx context.Context, chatID int64, text, } // SendMessageWithKeyboard sends a text message with an inline keyboard and optional reply. -func (c *TelegramAPIClient) SendMessageWithKeyboard(ctx context.Context, chatID int64, text, parseMode string, keyboard *InlineKeyboardMarkup, replyToMessageID int64) (*TGMessage, error) { +func (c *TelegramAPIClient) SendMessageWithKeyboard(ctx context.Context, chatID int64, text, parseMode string, keyboard *InlineKeyboardMarkup, replyToMessageID int64, opts ...SendOption) (*TGMessage, error) { body := sendMessageWithKeyboardRequest{ ChatID: chatID, Text: text, @@ -514,6 +525,9 @@ func (c *TelegramAPIClient) SendMessageWithKeyboard(ctx context.Context, chatID ReplyMarkup: keyboard, ReplyToMessageID: replyToMessageID, } + for _, o := range opts { + body.MessageThreadID = o.MessageThreadID + } jsonBody, err := json.Marshal(body) if err != nil { diff --git a/extras/scion-telegram/internal/telegram/broker_v2.go b/extras/scion-telegram/internal/telegram/broker_v2.go index c5372f88c..a94511abc 100644 --- a/extras/scion-telegram/internal/telegram/broker_v2.go +++ b/extras/scion-telegram/internal/telegram/broker_v2.go @@ -689,21 +689,28 @@ func (b *TelegramBrokerV2) Publish(ctx context.Context, topic string, msg *messa } } + // Determine thread ID for Telegram forum topics. + var threadOpts []SendOption + if msg != nil && msg.ThreadID != "" { + if tid, err := strconv.ParseInt(msg.ThreadID, 10, 64); err == nil && tid != 0 { + threadOpts = append(threadOpts, SendOption{MessageThreadID: tid}) + } + } + var errs []error for _, chatID := range chatIDs { var err error if sq != nil { var keyboard *InlineKeyboardMarkup if replyToMsgID > 0 { - // Pass nil keyboard but use replyTo. - _, err = sq.Send(ctx, chatID, text, "", keyboard, replyToMsgID) + _, err = sq.Send(ctx, chatID, text, "", keyboard, replyToMsgID, threadOpts...) } else { - _, err = sq.Send(ctx, chatID, text, "", nil, 0) + _, err = sq.Send(ctx, chatID, text, "", nil, 0, threadOpts...) } } else if replyToMsgID > 0 { - _, err = api.SendMessageWithKeyboard(ctx, chatID, text, "", nil, replyToMsgID) + _, err = api.SendMessageWithKeyboard(ctx, chatID, text, "", nil, replyToMsgID, threadOpts...) } else { - _, err = api.SendMessage(ctx, chatID, text, "") + _, err = api.SendMessage(ctx, chatID, text, "", threadOpts...) } if err != nil { var apiErr *APIError @@ -715,9 +722,32 @@ func (b *TelegramBrokerV2) Publish(ctx context.Context, topic string, msg *messa continue } if errors.As(err, &apiErr) && apiErr.IsMigrated() { - b.log.Warn("Group upgraded to supergroup, skipping message", - "old_chat_id", chatID, "new_chat_id", apiErr.MigrateToChatID) - continue // TODO: migrate group_links record to new chat_id + newChatID := apiErr.MigrateToChatID + b.log.Info("Group upgraded to supergroup, migrating", + "old_chat_id", chatID, "new_chat_id", newChatID) + if store != nil { + if merr := store.MigrateGroupLink(ctx, chatID, newChatID); merr != nil { + b.log.Error("Failed to migrate group_link", "error", merr) + } + } + // Retry send with the new chat_id. + if sq != nil { + var keyboard *InlineKeyboardMarkup + if replyToMsgID > 0 { + _, err = sq.Send(ctx, newChatID, text, "", keyboard, replyToMsgID, threadOpts...) + } else { + _, err = sq.Send(ctx, newChatID, text, "", nil, 0, threadOpts...) + } + } else if replyToMsgID > 0 { + _, err = api.SendMessageWithKeyboard(ctx, newChatID, text, "", nil, replyToMsgID, threadOpts...) + } else { + _, err = api.SendMessage(ctx, newChatID, text, "", threadOpts...) + } + if err != nil { + b.log.Error("Retry after migration failed", "chat_id", newChatID, "error", err) + errs = append(errs, err) + } + continue } b.log.Error("Failed to send Telegram message", "chat_id", chatID, "error", err) @@ -857,9 +887,42 @@ func (b *TelegramBrokerV2) publishInputNeeded(ctx context.Context, api *Telegram continue } if errors.As(err, &apiErr) && apiErr.IsMigrated() { - b.log.Warn("Group upgraded to supergroup, skipping input-needed", - "old_chat_id", chatID, "new_chat_id", apiErr.MigrateToChatID) - continue // TODO: migrate group_links record to new chat_id + newChatID := apiErr.MigrateToChatID + b.log.Info("Group upgraded to supergroup, migrating", + "old_chat_id", chatID, "new_chat_id", newChatID) + if b.store != nil { + if merr := b.store.MigrateGroupLink(ctx, chatID, newChatID); merr != nil { + b.log.Error("Failed to migrate group_link", "error", merr) + } + } + // Retry send with the new chat_id. + keyboard = buildAskUserKeyboard(requestID, choices) + if sq != nil { + sent, err = sq.Send(ctx, newChatID, text, "", keyboard, 0) + } else if keyboard == nil { + sent, err = api.SendMessage(ctx, newChatID, text, "") + } else { + sent, err = api.SendMessageWithKeyboard(ctx, newChatID, text, "", keyboard, 0) + } + if err != nil { + b.log.Error("Retry after migration failed", "chat_id", newChatID, "error", err) + errs = append(errs, err) + continue + } + // Save PendingAskUser with the new chat_id. + pending := &PendingAskUser{ + RequestID: requestID, + MessageID: sent.MessageID, + ChatID: newChatID, + AgentSlug: agentSlug, + ProjectID: projectID, + Choices: choices, + ExpiresAt: time.Now().Add(askUserExpiry), + } + if perr := b.store.SavePendingAskUser(ctx, pending); perr != nil { + b.log.Error("Failed to save pending ask user after migration", "error", perr) + } + continue } b.log.Error("Failed to send input-needed message", "chat_id", chatID, "error", err) @@ -1535,8 +1598,18 @@ func (b *TelegramBrokerV2) handleGroupMessage(tgMsg *TGMessage) { } b.mu.RUnlock() + // Resolve effective default agent: topic-level override first, then chat-level. + effectiveDefault := link.DefaultAgent + if tgMsg.MessageThreadID != 0 { + if topicDefault, err := b.store.GetTopicDefault(ctx, chatID, tgMsg.MessageThreadID); err != nil { + b.log.Error("Failed to get topic default", "error", err) + } else if topicDefault != "" { + effectiveDefault = topicDefault + } + } + // Resolve target agents from @-mentions. - targets, isAll := resolveTargetAgents(tgMsg, botUsername, link.DefaultAgent, agents) + targets, isAll := resolveTargetAgents(tgMsg, botUsername, effectiveDefault, agents) // Fallback 1: reply-to-bot-message — extract the agent from the replied-to message. if len(targets) == 0 && tgMsg.ReplyToMessage != nil { @@ -1590,13 +1663,13 @@ func (b *TelegramBrokerV2) handleGroupMessage(tgMsg *TGMessage) { // Telegram user — that's a user-to-user message. Mentions embedded // later (offset>0) do not block default routing; resolveUserMentions // injects the resolved scion identity for those. - if len(targets) == 0 && link.DefaultAgent != "" { + if len(targets) == 0 && effectiveDefault != "" { hasAttachment := tgMsg.Photo != nil || tgMsg.Document != nil text := strings.TrimSpace(tgMsg.Text) textRoutes := text != "" && !strings.HasPrefix(text, "/") && !strings.HasPrefix(text, "@") && !hasNonBotUserMention(tgMsg, botUsername, agents) if textRoutes || hasAttachment { - b.log.Debug("Using default agent", "agent", link.DefaultAgent) - targets = []string{link.DefaultAgent} + b.log.Debug("Using default agent", "agent", effectiveDefault) + targets = []string{effectiveDefault} } } diff --git a/extras/scion-telegram/internal/telegram/callbacks.go b/extras/scion-telegram/internal/telegram/callbacks.go index 6dfad5ddb..6bbad9386 100644 --- a/extras/scion-telegram/internal/telegram/callbacks.go +++ b/extras/scion-telegram/internal/telegram/callbacks.go @@ -83,6 +83,19 @@ func (h *CallbackHandler) HandleCallback(ctx context.Context, cb *CallbackQuery) return nil, nil } + if strings.HasPrefix(cb.Data, callbackLookupPrefix) { + shortID := strings.TrimPrefix(cb.Data, callbackLookupPrefix) + lookup, err := h.store.GetCallbackLookup(ctx, shortID) + if err != nil { + return nil, fmt.Errorf("failed to resolve callback lookup %s: %w", shortID, err) + } + if lookup == nil { + h.answerCallback(ctx, cb.ID, "This button has expired. Please try the command again.", false) + return nil, nil + } + cb.Data = lookup.FullData + } + parts := strings.SplitN(cb.Data, ":", 4) if len(parts) < 2 { return nil, fmt.Errorf("invalid callback data: %s", cb.Data) @@ -324,12 +337,22 @@ func (h *CallbackHandler) handleDefaultCallback(ctx context.Context, cb *Callbac messageID = cb.Message.MessageID } + // Parse optional thread ID for topic-scoped defaults. + var threadID int64 + if len(parts) >= 2 { + threadID, _ = strconv.ParseInt(parts[1], 10, 64) + } + link, err := h.store.GetGroupLink(ctx, chatID) if err != nil || link == nil { h.answerCallback(ctx, cb.ID, "Group is not linked to a project.", false) return err } + if threadID != 0 { + return h.handleTopicDefaultCallback(ctx, cb, chatID, messageID, threadID, agentSlug, link) + } + if agentSlug == "__none__" { link.DefaultAgent = "" } else { @@ -352,6 +375,32 @@ func (h *CallbackHandler) handleDefaultCallback(ctx context.Context, cb *Callbac return nil } +func (h *CallbackHandler) handleTopicDefaultCallback(ctx context.Context, cb *CallbackQuery, chatID, messageID, threadID int64, agentSlug string, link *GroupLink) error { + if agentSlug == "__none__" { + if err := h.store.DeleteTopicDefault(ctx, chatID, threadID); err != nil { + h.log.Error("Failed to delete topic default", "chat_id", chatID, "thread_id", threadID, "error", err) + h.answerCallback(ctx, cb.ID, "Failed to update topic default.", false) + return err + } + fallbackMsg := "Topic default removed." + if link.DefaultAgent != "" { + fallbackMsg += fmt.Sprintf(" Messages will use the chat default (@%s).", link.DefaultAgent) + } + h.editMessage(ctx, chatID, messageID, fallbackMsg, nil) + h.answerCallback(ctx, cb.ID, "Topic default: none", false) + } else { + if err := h.store.SetTopicDefault(ctx, chatID, threadID, agentSlug); err != nil { + h.log.Error("Failed to set topic default", "chat_id", chatID, "thread_id", threadID, "error", err) + h.answerCallback(ctx, cb.ID, "Failed to update topic default.", false) + return err + } + h.editMessage(ctx, chatID, messageID, + fmt.Sprintf("Default agent for this topic set to @%s.", agentSlug), nil) + h.answerCallback(ctx, cb.ID, fmt.Sprintf("Topic default: @%s", agentSlug), false) + } + return nil +} + func (h *CallbackHandler) handleAskCallback(ctx context.Context, cb *CallbackQuery, parts []string) (*AskUserResponse, error) { if len(parts) < 2 { return nil, fmt.Errorf("invalid ask callback data") diff --git a/extras/scion-telegram/internal/telegram/cards.go b/extras/scion-telegram/internal/telegram/cards.go index ee3b530a1..cfe24a73e 100644 --- a/extras/scion-telegram/internal/telegram/cards.go +++ b/extras/scion-telegram/internal/telegram/cards.go @@ -14,7 +14,15 @@ package telegram -import "fmt" +import ( + "context" + "crypto/rand" + "encoding/hex" + "fmt" + "log/slog" + "strconv" + "time" +) // ProjectOption represents a project choice for keyboard selection. type ProjectOption struct { @@ -69,6 +77,11 @@ func buildProjectSelectionKeyboard(projects []ProjectOption) *InlineKeyboardMark // Callback data format: setup:dflt: func buildAgentSelectionKeyboard(agents []string, currentDefault string) *InlineKeyboardMarkup { kb := buildAgentKeyboard(agents, currentDefault, "setup:dflt") + for i, row := range kb.InlineKeyboard { + for j, btn := range row { + kb.InlineKeyboard[i][j].CallbackData = truncateCallback(btn.CallbackData) + } + } kb.InlineKeyboard = append(kb.InlineKeyboard, []InlineKeyboardButton{ {Text: "No default agent", CallbackData: "setup:dflt:"}, }) @@ -76,15 +89,34 @@ func buildAgentSelectionKeyboard(agents []string, currentDefault string) *Inline } // buildDefaultAgentKeyboard creates an inline keyboard for /default command. -// Callback data format: dflt: -func buildDefaultAgentKeyboard(agents []string, currentDefault string) *InlineKeyboardMarkup { - kb := buildAgentKeyboard(agents, currentDefault, "dflt") +// Callback data format: dflt: or dflt:: for topic-scoped defaults. +// When the callback data exceeds Telegram's 64-byte limit, it is stored in +// callback_lookups and replaced with a short cblu: reference. +func buildDefaultAgentKeyboard(ctx context.Context, store Store, agents []string, currentDefault string, threadID int64) *InlineKeyboardMarkup { + suffix := "" + if threadID != 0 { + suffix = ":" + strconv.FormatInt(threadID, 10) + } + prefix := "dflt" + kb := buildAgentKeyboard(agents, currentDefault, prefix) + for i, row := range kb.InlineKeyboard { + for j, btn := range row { + kb.InlineKeyboard[i][j].CallbackData = callbackOrLookup(ctx, store, btn.CallbackData+suffix) + } + } noneLabel := "No default agent" + if threadID != 0 { + noneLabel = "No default agent (use chat default)" + } if currentDefault == "" { - noneLabel = "✓ No default agent (current)" + if threadID != 0 { + noneLabel = "✓ No default agent (current, using chat default)" + } else { + noneLabel = "✓ No default agent (current)" + } } kb.InlineKeyboard = append(kb.InlineKeyboard, []InlineKeyboardButton{ - {Text: noneLabel, CallbackData: "dflt:__none__"}, + {Text: noneLabel, CallbackData: callbackOrLookup(ctx, store, "dflt:__none__"+suffix)}, }) return kb } @@ -100,7 +132,7 @@ func buildAgentKeyboard(agents []string, currentDefault string, prefix string) * } btn := InlineKeyboardButton{ Text: label, - CallbackData: truncateCallback(fmt.Sprintf("%s:%s", prefix, agent)), + CallbackData: fmt.Sprintf("%s:%s", prefix, agent), } row = append(row, btn) if len(row) == 2 { @@ -240,5 +272,40 @@ func truncateCallback(data string) string { if len(data) <= maxCallbackData { return data } + slog.Warn("callback_data exceeds 64-byte Telegram limit, truncating", + "len", len(data), "data", data) return data[:maxCallbackData] } + +// callbackLookupPrefix identifies callback data that is stored in the +// callback_lookups table rather than inline. +const callbackLookupPrefix = "cblu:" + +// callbackLookupTTL is how long a stored callback lookup remains valid. +const callbackLookupTTL = 24 * time.Hour + +// callbackOrLookup returns data as-is when it fits within Telegram's 64-byte +// callback_data limit. When data exceeds the limit, the full payload is +// persisted via the store's callback_lookups table and a short cblu: +// reference is returned instead. +func callbackOrLookup(ctx context.Context, store Store, data string) string { + if len(data) <= maxCallbackData { + return data + } + idBytes := make([]byte, 8) + if _, err := rand.Read(idBytes); err != nil { + slog.Error("failed to generate callback lookup ID", "error", err) + return truncateCallback(data) + } + shortID := hex.EncodeToString(idBytes) + lookup := &CallbackLookup{ + ShortID: shortID, + FullData: data, + ExpiresAt: time.Now().Add(callbackLookupTTL), + } + if err := store.SaveCallbackLookup(ctx, lookup); err != nil { + slog.Error("failed to save callback lookup", "error", err, "data", data) + return truncateCallback(data) + } + return callbackLookupPrefix + shortID +} diff --git a/extras/scion-telegram/internal/telegram/cards_test.go b/extras/scion-telegram/internal/telegram/cards_test.go index c813c9dc5..aa0980a7d 100644 --- a/extras/scion-telegram/internal/telegram/cards_test.go +++ b/extras/scion-telegram/internal/telegram/cards_test.go @@ -15,7 +15,9 @@ package telegram import ( + "context" "fmt" + "strings" "testing" "github.com/stretchr/testify/assert" @@ -96,13 +98,44 @@ func TestBuildAgentSelectionKeyboard_NoDefault(t *testing.T) { } func TestBuildDefaultAgentKeyboard_CallbackFormat(t *testing.T) { - kb := buildDefaultAgentKeyboard([]string{"coder", "reviewer"}, "coder") + store := newTestStore(t) + kb := buildDefaultAgentKeyboard(context.Background(), store, []string{"coder", "reviewer"}, "coder", 0) assert.Equal(t, "dflt:coder", kb.InlineKeyboard[0][0].CallbackData) assert.Equal(t, "✓ coder (current)", kb.InlineKeyboard[0][0].Text) assert.Equal(t, "dflt:reviewer", kb.InlineKeyboard[0][1].CallbackData) assert.Equal(t, "reviewer", kb.InlineKeyboard[0][1].Text) } +func TestBuildDefaultAgentKeyboard_TopicScoped(t *testing.T) { + store := newTestStore(t) + kb := buildDefaultAgentKeyboard(context.Background(), store, []string{"coder", "reviewer"}, "coder", 42) + assert.Equal(t, "dflt:coder:42", kb.InlineKeyboard[0][0].CallbackData) + assert.Equal(t, "✓ coder (current)", kb.InlineKeyboard[0][0].Text) + assert.Equal(t, "dflt:reviewer:42", kb.InlineKeyboard[0][1].CallbackData) + + lastRow := kb.InlineKeyboard[len(kb.InlineKeyboard)-1] + assert.Equal(t, "dflt:__none__:42", lastRow[0].CallbackData) + assert.Contains(t, lastRow[0].Text, "use chat default") +} + +func TestBuildDefaultAgentKeyboard_LongSlugUsesLookup(t *testing.T) { + store := newTestStore(t) + ctx := context.Background() + longSlug := "this-is-a-very-long-agent-slug-that-will-exceed-sixty-four-bytes" + kb := buildDefaultAgentKeyboard(ctx, store, []string{longSlug}, "", 99999999) + + cbData := kb.InlineKeyboard[0][0].CallbackData + assert.True(t, strings.HasPrefix(cbData, callbackLookupPrefix), + "expected callback lookup prefix, got %q", cbData) + assert.LessOrEqual(t, len(cbData), maxCallbackData) + + shortID := strings.TrimPrefix(cbData, callbackLookupPrefix) + lookup, err := store.GetCallbackLookup(ctx, shortID) + require.NoError(t, err) + require.NotNil(t, lookup) + assert.Equal(t, fmt.Sprintf("dflt:%s:99999999", longSlug), lookup.FullData) +} + func TestBuildAskUserKeyboard_WithChoices(t *testing.T) { kb := buildAskUserKeyboard("req-42", []string{"Option A", "Option B", "Option C"}) require.Len(t, kb.InlineKeyboard, 2) diff --git a/extras/scion-telegram/internal/telegram/commands.go b/extras/scion-telegram/internal/telegram/commands.go index f5fc77123..bc92df71c 100644 --- a/extras/scion-telegram/internal/telegram/commands.go +++ b/extras/scion-telegram/internal/telegram/commands.go @@ -193,6 +193,7 @@ func (h *CommandHandler) handleSetup(msg *TGMessage) { func (h *CommandHandler) handleDefault(msg *TGMessage) { chatID := msg.Chat.ID + threadID := msg.MessageThreadID ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) defer cancel() @@ -222,8 +223,24 @@ func (h *CommandHandler) handleDefault(msg *TGMessage) { return } - kb := buildDefaultAgentKeyboard(agentSlugs(agents), link.DefaultAgent) - h.replyWithKeyboard(chatID, "Select the default agent for @-mentions:", kb) + promptText := "Select the default agent for @-mentions:" + currentDefault := link.DefaultAgent + + if threadID != 0 { + topicDefault, err := h.store.GetTopicDefault(ctx, chatID, threadID) + if err != nil { + h.log.Error("Failed to get topic default", "error", err) + } else if topicDefault != "" { + currentDefault = topicDefault + } + promptText = "Select the default agent for this topic:" + if link.DefaultAgent != "" { + promptText += fmt.Sprintf("\nChat-wide default: @%s", link.DefaultAgent) + } + } + + kb := buildDefaultAgentKeyboard(ctx, h.store, agentSlugs(agents), currentDefault, threadID) + h.replyWithKeyboardInThread(chatID, threadID, promptText, kb) } func (h *CommandHandler) handleAgents(msg *TGMessage) { @@ -575,6 +592,18 @@ func (h *CommandHandler) replyWithKeyboard(chatID int64, text string, kb *Inline } } +func (h *CommandHandler) replyWithKeyboardInThread(chatID int64, threadID int64, text string, kb *InlineKeyboardMarkup) { + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) + defer cancel() + var opts []SendOption + if threadID != 0 { + opts = append(opts, SendOption{MessageThreadID: threadID}) + } + if _, err := h.api.SendMessageWithKeyboard(ctx, chatID, text, "", kb, 0, opts...); err != nil { + h.log.Error("Failed to send reply with keyboard", "chat_id", chatID, "error", err) + } +} + func isGroupChat(chatID int64) bool { return chatID < 0 } // --- httpHubClient --- diff --git a/extras/scion-telegram/internal/telegram/sendqueue.go b/extras/scion-telegram/internal/telegram/sendqueue.go index 005bf0240..ad339d4ab 100644 --- a/extras/scion-telegram/internal/telegram/sendqueue.go +++ b/extras/scion-telegram/internal/telegram/sendqueue.go @@ -51,12 +51,13 @@ type chatQueue struct { // outboundMessage represents a message waiting to be sent through the queue. type outboundMessage struct { - chatID int64 - text string - parseMode string - keyboard *InlineKeyboardMarkup - replyTo int64 - result chan<- *sendResult // caller blocks on this to receive the outcome + chatID int64 + text string + parseMode string + keyboard *InlineKeyboardMarkup + replyTo int64 + messageThreadID int64 + result chan<- *sendResult // caller blocks on this to receive the outcome } // sendResult carries the outcome of a queued send back to the caller. @@ -88,7 +89,7 @@ func NewSendQueue(api *TelegramAPIClient, log *slog.Logger, maxSize int, minDela // Send enqueues a message and blocks until it is sent (or fails). // It returns the Telegram API response or an error. -func (sq *SendQueue) Send(ctx context.Context, chatID int64, text, parseMode string, keyboard *InlineKeyboardMarkup, replyTo int64) (*TGMessage, error) { +func (sq *SendQueue) Send(ctx context.Context, chatID int64, text, parseMode string, keyboard *InlineKeyboardMarkup, replyTo int64, opts ...SendOption) (*TGMessage, error) { resultCh := make(chan *sendResult, 1) om := &outboundMessage{ @@ -99,6 +100,9 @@ func (sq *SendQueue) Send(ctx context.Context, chatID int64, text, parseMode str replyTo: replyTo, result: resultCh, } + for _, o := range opts { + om.messageThreadID = o.MessageThreadID + } ch, err := sq.enqueue(chatID, om) if err != nil { @@ -235,10 +239,15 @@ func (sq *SendQueue) sendOne(om *outboundMessage) (*TGMessage, error) { ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) defer cancel() + var opts []SendOption + if om.messageThreadID != 0 { + opts = append(opts, SendOption{MessageThreadID: om.messageThreadID}) + } + if om.keyboard != nil || om.replyTo > 0 { - return sq.api.SendMessageWithKeyboard(ctx, om.chatID, om.text, om.parseMode, om.keyboard, om.replyTo) + return sq.api.SendMessageWithKeyboard(ctx, om.chatID, om.text, om.parseMode, om.keyboard, om.replyTo, opts...) } - return sq.api.SendMessage(ctx, om.chatID, om.text, om.parseMode) + return sq.api.SendMessage(ctx, om.chatID, om.text, om.parseMode, opts...) } // removeQueue removes the per-chat queue from the map when the worker exits. diff --git a/extras/scion-telegram/internal/telegram/store.go b/extras/scion-telegram/internal/telegram/store.go index ecca641fc..6b8d4dbfc 100644 --- a/extras/scion-telegram/internal/telegram/store.go +++ b/extras/scion-telegram/internal/telegram/store.go @@ -67,6 +67,15 @@ type Store interface { GetNotificationPrefs(ctx context.Context, telegramUserID string) ([]*NotificationPref, error) GetNotificationPref(ctx context.Context, telegramUserID, projectID, agentSlug string) (*NotificationPref, error) + // TopicDefault — per-topic default agent overrides for forum groups + GetTopicDefault(ctx context.Context, chatID int64, threadID int64) (string, error) + SetTopicDefault(ctx context.Context, chatID int64, threadID int64, agentSlug string) error + DeleteTopicDefault(ctx context.Context, chatID int64, threadID int64) error + + // MigrateGroupLink atomically moves a group_link and its topic_defaults + // from oldChatID to newChatID (used when Telegram upgrades a group to a supergroup). + MigrateGroupLink(ctx context.Context, oldChatID, newChatID int64) error + // Lifecycle Close() error } @@ -234,6 +243,13 @@ CREATE TABLE IF NOT EXISTS notification_prefs ( enabled INTEGER NOT NULL DEFAULT 1, PRIMARY KEY (telegram_user_id, project_id, agent_slug) ); + +CREATE TABLE IF NOT EXISTS topic_defaults ( + chat_id INTEGER NOT NULL, + thread_id INTEGER NOT NULL, + agent_slug TEXT NOT NULL, + PRIMARY KEY (chat_id, thread_id) +); ` if _, err := s.db.Exec(ddl); err != nil { return err @@ -305,6 +321,44 @@ func (s *sqliteStore) DeleteGroupLink(ctx context.Context, chatID int64) error { return err } +func (s *sqliteStore) MigrateGroupLink(ctx context.Context, oldChatID, newChatID int64) error { + tx, err := s.db.BeginTx(ctx, nil) + if err != nil { + return fmt.Errorf("begin tx: %w", err) + } + defer tx.Rollback() + + // Copy the group_link to the new chat_id. + _, err = tx.ExecContext(ctx, ` +INSERT OR REPLACE INTO group_links + (chat_id, chat_title, project_id, project_slug, default_agent, linked_by, linked_at, active, show_agent_to_agent, notify_in_group, show_assistant_reply) +SELECT ?, chat_title, project_id, project_slug, default_agent, linked_by, linked_at, active, show_agent_to_agent, notify_in_group, show_assistant_reply +FROM group_links WHERE chat_id = ?`, newChatID, oldChatID) + if err != nil { + return fmt.Errorf("copy group_link: %w", err) + } + + _, err = tx.ExecContext(ctx, `DELETE FROM group_links WHERE chat_id = ?`, oldChatID) + if err != nil { + return fmt.Errorf("delete old group_link: %w", err) + } + + // Migrate any topic_defaults rows to the new chat_id. + _, err = tx.ExecContext(ctx, ` +INSERT OR REPLACE INTO topic_defaults (chat_id, thread_id, agent_slug) +SELECT ?, thread_id, agent_slug FROM topic_defaults WHERE chat_id = ?`, newChatID, oldChatID) + if err != nil { + return fmt.Errorf("copy topic_defaults: %w", err) + } + + _, err = tx.ExecContext(ctx, `DELETE FROM topic_defaults WHERE chat_id = ?`, oldChatID) + if err != nil { + return fmt.Errorf("delete old topic_defaults: %w", err) + } + + return tx.Commit() +} + // --- ConversationContext --- func (s *sqliteStore) SaveConversationContext(ctx context.Context, cc *ConversationContext) error { @@ -611,6 +665,32 @@ func (s *sqliteStore) GetNotificationPref(ctx context.Context, telegramUserID, p return &p, nil } +// --- TopicDefault --- + +func (s *sqliteStore) GetTopicDefault(ctx context.Context, chatID int64, threadID int64) (string, error) { + const q = `SELECT agent_slug FROM topic_defaults WHERE chat_id = ? AND thread_id = ?` + var agentSlug string + err := s.db.QueryRowContext(ctx, q, chatID, threadID).Scan(&agentSlug) + if err == sql.ErrNoRows { + return "", nil + } + return agentSlug, err +} + +func (s *sqliteStore) SetTopicDefault(ctx context.Context, chatID int64, threadID int64, agentSlug string) error { + const q = ` +INSERT INTO topic_defaults (chat_id, thread_id, agent_slug) +VALUES (?, ?, ?) +ON CONFLICT(chat_id, thread_id) DO UPDATE SET agent_slug=excluded.agent_slug` + _, err := s.db.ExecContext(ctx, q, chatID, threadID, agentSlug) + return err +} + +func (s *sqliteStore) DeleteTopicDefault(ctx context.Context, chatID int64, threadID int64) error { + _, err := s.db.ExecContext(ctx, `DELETE FROM topic_defaults WHERE chat_id = ? AND thread_id = ?`, chatID, threadID) + return err +} + // --- scan helpers --- func scanGroupLink(row *sql.Row) (*GroupLink, error) { diff --git a/extras/scion-telegram/internal/telegram/store_test.go b/extras/scion-telegram/internal/telegram/store_test.go index 864960e94..da4786a5a 100644 --- a/extras/scion-telegram/internal/telegram/store_test.go +++ b/extras/scion-telegram/internal/telegram/store_test.go @@ -779,6 +779,77 @@ func TestStore_GroupLink_NotifyInGroup(t *testing.T) { assert.False(t, got.NotifyInGroup) } +// --- TopicDefault --- + +func TestStore_TopicDefault_SetAndGet(t *testing.T) { + store := newTestStore(t) + ctx := context.Background() + + // Not found returns empty string. + slug, err := store.GetTopicDefault(ctx, -100, 42) + require.NoError(t, err) + assert.Equal(t, "", slug) + + // Set and retrieve. + require.NoError(t, store.SetTopicDefault(ctx, -100, 42, "coder")) + slug, err = store.GetTopicDefault(ctx, -100, 42) + require.NoError(t, err) + assert.Equal(t, "coder", slug) + + // Different thread returns empty. + slug, err = store.GetTopicDefault(ctx, -100, 99) + require.NoError(t, err) + assert.Equal(t, "", slug) +} + +func TestStore_TopicDefault_Upsert(t *testing.T) { + store := newTestStore(t) + ctx := context.Background() + + require.NoError(t, store.SetTopicDefault(ctx, -100, 42, "coder")) + require.NoError(t, store.SetTopicDefault(ctx, -100, 42, "reviewer")) + + slug, err := store.GetTopicDefault(ctx, -100, 42) + require.NoError(t, err) + assert.Equal(t, "reviewer", slug) +} + +func TestStore_TopicDefault_Delete(t *testing.T) { + store := newTestStore(t) + ctx := context.Background() + + require.NoError(t, store.SetTopicDefault(ctx, -100, 42, "coder")) + require.NoError(t, store.DeleteTopicDefault(ctx, -100, 42)) + + slug, err := store.GetTopicDefault(ctx, -100, 42) + require.NoError(t, err) + assert.Equal(t, "", slug) + + // Delete non-existent is not an error. + require.NoError(t, store.DeleteTopicDefault(ctx, -100, 99)) +} + +func TestStore_TopicDefault_MultipleTopics(t *testing.T) { + store := newTestStore(t) + ctx := context.Background() + + require.NoError(t, store.SetTopicDefault(ctx, -100, 1, "coder")) + require.NoError(t, store.SetTopicDefault(ctx, -100, 2, "reviewer")) + require.NoError(t, store.SetTopicDefault(ctx, -200, 1, "designer")) + + slug, err := store.GetTopicDefault(ctx, -100, 1) + require.NoError(t, err) + assert.Equal(t, "coder", slug) + + slug, err = store.GetTopicDefault(ctx, -100, 2) + require.NoError(t, err) + assert.Equal(t, "reviewer", slug) + + slug, err = store.GetTopicDefault(ctx, -200, 1) + require.NoError(t, err) + assert.Equal(t, "designer", slug) +} + // --- Store lifecycle --- func TestStore_OpenInvalidPath(t *testing.T) { diff --git a/format_callouts.py b/format_callouts.py deleted file mode 100644 index 59e88facb..000000000 --- a/format_callouts.py +++ /dev/null @@ -1,39 +0,0 @@ -# Copyright 2026 Google LLC -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import re - -with open("docs-site/src/content/docs/release-notes.md", "r") as f: - lines = f.readlines() - -new_lines = [] -in_breaking = False - -for line in lines: - if line.strip() == "### ⚠️ BREAKING CHANGES": - new_lines.append(":::danger[BREAKING CHANGES]\n") - in_breaking = True - elif in_breaking and line.startswith("##"): - new_lines.append(":::\n\n") - new_lines.append(line) - in_breaking = False - else: - new_lines.append(line) - -# If the file ended while still in a breaking block -if in_breaking: - new_lines.append(":::\n") - -with open("docs-site/src/content/docs/release-notes.md", "w") as f: - f.writelines(new_lines) diff --git a/go.mod b/go.mod index a11ef3b21..1356e2fc8 100644 --- a/go.mod +++ b/go.mod @@ -19,6 +19,7 @@ require ( github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674 github.com/hashicorp/go-hclog v1.6.3 github.com/hashicorp/go-plugin v1.7.0 + github.com/jackc/pgx/v5 v5.9.2 github.com/knadh/koanf/parsers/json v1.0.0 github.com/knadh/koanf/parsers/yaml v1.1.0 github.com/knadh/koanf/providers/confmap v1.0.0 @@ -26,7 +27,6 @@ require ( github.com/knadh/koanf/providers/file v1.2.1 github.com/knadh/koanf/providers/rawbytes v1.0.0 github.com/knadh/koanf/v2 v2.3.0 - github.com/lib/pq v1.11.2 github.com/rclone/rclone v1.73.5 github.com/robfig/cron/v3 v3.0.1 github.com/santhosh-tekuri/jsonschema/v6 v6.0.2 @@ -121,6 +121,9 @@ require ( github.com/hashicorp/hcl/v2 v2.18.1 // indirect github.com/hashicorp/yamux v0.1.2 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect + github.com/jackc/pgpassfile v1.0.0 // indirect + github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect + github.com/jackc/puddle/v2 v2.2.2 // indirect github.com/josharian/intern v1.0.0 // indirect github.com/json-iterator/go v1.1.12 // indirect github.com/jtolds/gls v4.20.0+incompatible // indirect diff --git a/go.sum b/go.sum index fd70893ca..a51771a86 100644 --- a/go.sum +++ b/go.sum @@ -348,6 +348,14 @@ github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2 github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/internxt/rclone-adapter v0.0.0-20260220172730-613f4cc8b8fd h1:dSIuz2mpJAPQfhHYtG57D0qwSkgC/vQ69gHfeyQ4kxA= github.com/internxt/rclone-adapter v0.0.0-20260220172730-613f4cc8b8fd/go.mod h1:vdPya4AIcDjvng4ViaAzqjegJf0VHYpYHQguFx5xBp0= +github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= +github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= +github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo= +github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= +github.com/jackc/pgx/v5 v5.9.2 h1:3ZhOzMWnR4yJ+RW1XImIPsD1aNSz4T4fyP7zlQb56hw= +github.com/jackc/pgx/v5 v5.9.2/go.mod h1:mal1tBGAFfLHvZzaYh77YS/eC6IX9OWbRV1QIIM0Jn4= +github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo= +github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4= github.com/jcmturner/aescts/v2 v2.0.0 h1:9YKLH6ey7H4eDBXW8khjYslgyqG2xZikXP0EQFKrle8= github.com/jcmturner/aescts/v2 v2.0.0/go.mod h1:AiaICIRyfYg35RUkr8yESTqvSy7csK90qZ5xfvvsoNs= github.com/jcmturner/dnsutils/v2 v2.0.0 h1:lltnkeZGL0wILNvrNiVCR6Ro5PGU/SeBvVO/8c/iPbo= @@ -413,8 +421,6 @@ github.com/lanrat/extsort v1.4.2 h1:akbLIdo4PhNZtvjpaWnbXtGMmLtnGzXplkzfgl+XTTY= github.com/lanrat/extsort v1.4.2/go.mod h1:hceP6kxKPKebjN1RVrDBXMXXECbaI41Y94tt6MDazc4= github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= -github.com/lib/pq v1.11.2 h1:x6gxUeu39V0BHZiugWe8LXZYZ+Utk7hSJGThs8sdzfs= -github.com/lib/pq v1.11.2/go.mod h1:/p+8NSbOcwzAEI7wiMXFlgydTwcgTr3OSKMsD2BitpA= github.com/lpar/date v1.0.0 h1:bq/zVqFTUmsxvd/CylidY4Udqpr9BOFrParoP6p0x/I= github.com/lpar/date v1.0.0/go.mod h1:KjYe0dDyMQTgpqcUz4LEIeM5VZwhggjVx/V2dtc8NSo= github.com/lufia/plan9stats v0.0.0-20251013123823-9fd1530e3ec3 h1:PwQumkgq4/acIiZhtifTV5OUqqiP82UAl0h87xj/l9k= @@ -463,6 +469,8 @@ github.com/oklog/run v1.1.0 h1:GEenZ1cK0+q0+wsJew9qUg/DyD8k3JzYsZAi5gYi2mA= github.com/oklog/run v1.1.0/go.mod h1:sVPdnTZT1zYwAJeCMu2Th4T21pA3FPOQRfWjQlk7DVU= github.com/oklog/ulid v1.3.1 h1:EGfNDEx6MqHz8B3uNV6QAib1UR2Lm97sHi3ocA6ESJ4= github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U= +github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec= +github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY= github.com/onsi/ginkgo/v2 v2.27.2 h1:LzwLj0b89qtIy6SSASkzlNvX6WktqurSHwkk2ipF/Ns= github.com/onsi/ginkgo/v2 v2.27.2/go.mod h1:ArE1D/XhNXBXCBkKOLkbsb2c81dQHCRcF5zwn/ykDRo= github.com/onsi/gomega v1.38.2 h1:eZCjf2xjZAqe+LeWvKb5weQ+NcPwX84kqJ0cZNxok2A= @@ -558,6 +566,7 @@ github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpE github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= diff --git a/hack/README.md b/hack/README.md index a2821c9cf..16170f255 100644 --- a/hack/README.md +++ b/hack/README.md @@ -4,6 +4,8 @@ Developer convenience scripts for local development, testing, and infrastructure ## Contents +### Scripts + | Script | Purpose | |--------|---------| | `setup.sh` | Set up an isolated test environment | @@ -16,4 +18,18 @@ Developer convenience scripts for local development, testing, and infrastructure | `merge-work.sh` | Merge agent work branches | | `version.sh` | Display version information | -These scripts are for development and operations -- not end-user tooling. +### Go Tools + +| Tool | Purpose | +|------|---------| +| `go run ./hack/apitest` | Stress tests API-level multi-hub integration against shared Postgres DB | +| `go run ./hack/dbdiag` | Diagnoses database connection pool usage and active advisory locks | +| `go run ./hack/minttoken` | Mints a long-lived user access-token JWT for local API integration testing | + +### Kubernetes Test Manifests + +| Manifests | Purpose | +|-----------|---------| +| `k8s-nfs/` | Pod and PV configurations for testing GKE NFS shared workspace mount scenarios | + +These scripts and tools are for development and operations -- not end-user tooling. diff --git a/hack/apitest/main.go b/hack/apitest/main.go new file mode 100644 index 000000000..79c625d6a --- /dev/null +++ b/hack/apitest/main.go @@ -0,0 +1,238 @@ +// Command apitest drives API-level multi-hub integration/stress traffic against +// two running Scion hubs that share one CloudSQL Postgres instance. It validates +// the connection-pool / keepalive fixes and multi-replica behavior through the +// real HTTP API. Run it ON a hub VM so it reaches both hubs over the fast +// internal network. Not part of the product. +// +// Env: +// +// A_BASE, B_BASE base URLs (e.g. http://localhost:8080, http://10.128.15.241:8080) +// A_TOK, B_TOK admin bearer tokens (per-hub signing keys) +package main + +import ( + "bytes" + "encoding/json" + "fmt" + "io" + "net/http" + "os" + "sort" + "sync" + "sync/atomic" + "time" + + "github.com/google/uuid" +) + +type hub struct { + name string + base string + tok string +} + +var client = &http.Client{Timeout: 35 * time.Second} + +func req(h hub, method, path string, body any) (int, []byte, time.Duration) { + var rdr io.Reader + if body != nil { + b, _ := json.Marshal(body) + rdr = bytes.NewReader(b) + } + r, _ := http.NewRequest(method, h.base+path, rdr) + r.Header.Set("Authorization", "Bearer "+h.tok) + if body != nil { + r.Header.Set("Content-Type", "application/json") + } + start := time.Now() + resp, err := client.Do(r) + d := time.Since(start) + if err != nil { + return 0, []byte(err.Error()), d + } + defer resp.Body.Close() + rb, _ := io.ReadAll(resp.Body) + return resp.StatusCode, rb, d +} + +func pct(ds []time.Duration, p float64) time.Duration { + if len(ds) == 0 { + return 0 + } + sort.Slice(ds, func(i, j int) bool { return ds[i] < ds[j] }) + i := int(float64(len(ds)) * p) + if i >= len(ds) { + i = len(ds) - 1 + } + return ds[i] +} + +func main() { + A := hub{"A", os.Getenv("A_BASE"), os.Getenv("A_TOK")} + B := hub{"B", os.Getenv("B_BASE"), os.Getenv("B_TOK")} + hubs := []hub{A, B} + + // ---- Phase 1: concurrent CRUD storm across both hubs ---- + fmt.Println("== Phase 1: concurrent project CRUD storm (both hubs) ==") + const workers, iters = 24, 30 + var ok, fail, stalls int64 + latMu := sync.Mutex{} + lat := map[string][]time.Duration{"A": {}, "B": {}} + var wg sync.WaitGroup + t0 := time.Now() + for w := 0; w < workers; w++ { + wg.Add(1) + go func(w int) { + defer wg.Done() + h := hubs[w%2] + for i := 0; i < iters; i++ { + name := fmt.Sprintf("stress-%d-%d-%s", w, i, uuid.NewString()[:8]) + st, body, d := req(h, "POST", "/api/v1/projects", map[string]string{"name": name}) + if d > 2*time.Second { + atomic.AddInt64(&stalls, 1) + } + if st != 201 && st != 200 { + atomic.AddInt64(&fail, 1) + if i == 0 { + fmt.Printf(" [%s] create failed st=%d body=%.120s\n", h.name, st, body) + } + continue + } + var pr struct { + ID string `json:"id"` + } + json.Unmarshal(body, &pr) + req(h, "GET", "/api/v1/projects/"+pr.ID, nil) + req(h, "GET", "/api/v1/projects?limit=5", nil) + dst, _, dd := req(h, "DELETE", "/api/v1/projects/"+pr.ID, nil) + if dd > 2*time.Second { + atomic.AddInt64(&stalls, 1) + } + if dst >= 200 && dst < 300 { + atomic.AddInt64(&ok, 1) + } else { + atomic.AddInt64(&fail, 1) + } + latMu.Lock() + lat[h.name] = append(lat[h.name], d) + latMu.Unlock() + } + }(w) + } + wg.Wait() + dur := time.Since(t0) + total := int64(workers * iters) + fmt.Printf(" full CRUD cycles ok=%d fail=%d of %d in %s (%.0f cycles/s), stalls(>2s)=%d\n", + ok, fail, total, dur.Truncate(time.Millisecond), float64(total)/dur.Seconds(), stalls) + for _, n := range []string{"A", "B"} { + fmt.Printf(" hub %s create-latency p50=%s p95=%s max=%s (n=%d)\n", + n, pct(lat[n], 0.5), pct(lat[n], 0.95), pct(lat[n], 1.0), len(lat[n])) + } + + // ---- Phase 2: cross-replica read-after-write (create A, read B) ---- + fmt.Println("== Phase 2: cross-replica read-after-write (create on A, GET on B) ==") + const rw = 40 + var immediate, delayed, miss int + for i := 0; i < rw; i++ { + name := "raw-" + uuid.NewString()[:10] + st, body, _ := req(A, "POST", "/api/v1/projects", map[string]string{"name": name}) + if st != 201 && st != 200 { + miss++ + continue + } + var pr struct { + ID string `json:"id"` + } + json.Unmarshal(body, &pr) + got := false + for attempt := 0; attempt < 10; attempt++ { + s2, _, _ := req(B, "GET", "/api/v1/projects/"+pr.ID, nil) + if s2 == 200 { + if attempt == 0 { + immediate++ + } else { + delayed++ + } + got = true + break + } + time.Sleep(50 * time.Millisecond) + } + if !got { + miss++ + } + req(A, "DELETE", "/api/v1/projects/"+pr.ID, nil) + } + fmt.Printf(" read-after-write: immediate=%d delayed=%d miss=%d of %d\n", immediate, delayed, miss, rw) + + // ---- Phase 3: conflict -> HTTP 409 (concurrent duplicate-ID creates) ---- + fmt.Println("== Phase 3: concurrent duplicate-ID create -> expect exactly one 201, rest 409 ==") + const rounds = 25 + var created, conflict, other int + for i := 0; i < rounds; i++ { + id := uuid.NewString() + name := "dup-" + id[:8] + var c201, c409, cother int64 + var w2 sync.WaitGroup + // 4 concurrent creators (2 per hub) racing on the same explicit ID. + for k := 0; k < 4; k++ { + w2.Add(1) + go func(k int) { + defer w2.Done() + h := hubs[k%2] + st, _, _ := req(h, "POST", "/api/v1/projects", map[string]any{"id": id, "name": name}) + switch { + case st == 201 || st == 200: + atomic.AddInt64(&c201, 1) + case st == 409: + atomic.AddInt64(&c409, 1) + default: + atomic.AddInt64(&cother, 1) + } + }(k) + } + w2.Wait() + created += int(c201) + conflict += int(c409) + other += int(cother) + req(A, "DELETE", "/api/v1/projects/"+id, nil) + } + fmt.Printf(" over %d rounds (4 racers each): 201=%d 409=%d other=%d (ideal: 201==%d, 409==%d)\n", + rounds, created, conflict, other, rounds, rounds*3) + + // ---- Phase 4: idle-then-burst (the stale-connection scenario) ---- + idleStr := os.Getenv("IDLE_SECONDS") + idle := 75 + fmt.Sscanf(idleStr, "%d", &idle) + fmt.Printf("== Phase 4: idle %ds then burst (validates keepalive/idle-recycle fix) ==\n", idle) + for _, h := range hubs { // warm the pools + for i := 0; i < 5; i++ { + req(h, "GET", "/api/v1/projects?limit=1", nil) + } + } + fmt.Printf(" pools warm; sleeping %ds to force idle...\n", idle) + time.Sleep(time.Duration(idle) * time.Second) + for _, h := range hubs { + var first time.Duration + var maxd time.Duration + for i := 0; i < 10; i++ { + st, _, d := req(h, "GET", "/api/v1/projects?limit=1", nil) + if i == 0 { + first = d + } + if d > maxd { + maxd = d + } + if st != 200 { + fmt.Printf(" [%s] burst req %d unexpected st=%d\n", h.name, i, st) + } + } + verdict := "OK" + if first > 2*time.Second { + verdict = "STALL (likely dead idle conn)" + } + fmt.Printf(" hub %s post-idle first-request=%s max=%s -> %s\n", + h.name, first.Truncate(time.Millisecond), maxd.Truncate(time.Millisecond), verdict) + } + fmt.Println("== done ==") +} diff --git a/hack/dbdiag/main.go b/hack/dbdiag/main.go new file mode 100644 index 000000000..8cc7d92fc --- /dev/null +++ b/hack/dbdiag/main.go @@ -0,0 +1,42 @@ +// Command dbdiag prints CloudSQL connection usage for diagnosing pool +// saturation. Not part of the product. +package main + +import ( + "context" + "fmt" + "os" + + "github.com/jackc/pgx/v5" +) + +func main() { + ctx := context.Background() + conn, err := pgx.Connect(ctx, os.Getenv("PG_DSN")) + if err != nil { + fmt.Fprintln(os.Stderr, "connect:", err) + os.Exit(1) + } + defer conn.Close(ctx) + + var maxc, used int + conn.QueryRow(ctx, "SHOW max_connections").Scan(&maxc) + conn.QueryRow(ctx, "SELECT count(*) FROM pg_stat_activity WHERE datname='scion_test'").Scan(&used) + fmt.Printf("max_connections=%d total_on_scion_test=%d\n", maxc, used) + + rows, _ := conn.Query(ctx, `SELECT COALESCE(application_name,'(none)'), state, count(*) + FROM pg_stat_activity WHERE datname='scion_test' + GROUP BY 1,2 ORDER BY 3 DESC`) + defer rows.Close() + fmt.Printf("%-32s %-20s %s\n", "application_name", "state", "count") + for rows.Next() { + var app, state string + var n int + rows.Scan(&app, &state, &n) + fmt.Printf("%-32s %-20s %d\n", app, state, n) + } + // Advisory locks currently held. + var locks int + conn.QueryRow(ctx, "SELECT count(*) FROM pg_locks WHERE locktype='advisory'").Scan(&locks) + fmt.Printf("advisory_locks_held=%d\n", locks) +} diff --git a/hack/k8s-nfs/README.md b/hack/k8s-nfs/README.md new file mode 100644 index 000000000..dede6eaba --- /dev/null +++ b/hack/k8s-nfs/README.md @@ -0,0 +1,24 @@ +# Kubernetes NFS Shared Workspace Test Manifests + +These manifests are developer convenience resources used for testing and validating NFS-based shared workspace coordination under Kubernetes (such as GKE). They correspond to the architecture and design guidelines described in `.design/nfs-workspace.md`. + +## Contents + +- `scion-nfs-pv.yaml`: Configures the PersistentVolume (PV) and PersistentVolumeClaim (PVC) targeting a shared NFS storage server. +- `nm2-test-pod-a.yaml`: Scenario A. A single pod that mounts the PVC at a project-specific subpath, provisions a Git workspace using an init container, and performs permission and filesystem isolation checks. +- `nm2-test-pod-b1.yaml` / `nm2-test-pod-b2.yaml`: Scenario B. Concurrent pods sharing the same PVC on different subpaths, validating parallel provisioning and runtime isolation. +- `nm2-test-pod-e.yaml`: Scenario E. An advanced multi-container pod template verifying volume mounts, mount boundaries, and runtime execution behavior. + +## How to Use + +Apply the volume configurations followed by the test scenarios to verify your cluster's NFS volume mount and isolation mechanics: + +```bash +# Setup PV and PVC +kubectl apply -f scion-nfs-pv.yaml + +# Run test scenario A +kubectl apply -f nm2-test-pod-a.yaml +kubectl get pod nm2-test-agent-a -n scion-agents -w +kubectl logs nm2-test-agent-a -n scion-agents +``` diff --git a/hack/k8s-nfs/nm2-test-pod-a.yaml b/hack/k8s-nfs/nm2-test-pod-a.yaml new file mode 100644 index 000000000..cab1f7892 --- /dev/null +++ b/hack/k8s-nfs/nm2-test-pod-a.yaml @@ -0,0 +1,76 @@ +apiVersion: v1 +kind: Pod +metadata: + name: nm2-test-agent-a + namespace: scion-agents + labels: + test: nm2-scenario-a + scion.dev/project-id: test-project-alpha +spec: + securityContext: + runAsUser: 1000 + runAsGroup: 1000 + fsGroup: 1000 + initContainers: + - name: workspace-provision + image: alpine/git:latest + command: + - sh + - -c + - | + set -e + SENTINEL="/workspace/.scion-provisioned" + if [ -f "$SENTINEL" ]; then + echo "PROVISION: sentinel found, skipping clone" + exit 0 + fi + echo "PROVISION: cloning workspace..." + git clone --depth 1 https://github.com/ptone/scion.git /workspace + echo "$(date -u +%Y-%m-%dT%H:%M:%SZ)" > "$SENTINEL" + echo "PROVISION: clone complete, sentinel written" + volumeMounts: + - name: workspace + mountPath: /workspace + subPath: projects/test-project-alpha/workspace + resources: + requests: + cpu: 250m + memory: 512Mi + containers: + - name: agent + image: busybox:1.36 + command: + - sh + - -c + - | + echo "=== WORKSPACE CONTENTS ===" + ls -la /workspace/ + echo "=== SENTINEL CHECK ===" + cat /workspace/.scion-provisioned 2>/dev/null && echo "SENTINEL: present" || echo "SENTINEL: missing" + echo "=== ISOLATION CHECK ===" + echo "Attempting to access parent dir..." + ls /workspace/../ 2>&1 || echo "ISOLATION: cannot traverse up" + echo "=== WORKSPACE MOUNT INFO ===" + mount | grep workspace || echo "mount info unavailable in busybox" + echo "=== UID/GID CHECK ===" + id + echo "=== TEST COMPLETE ===" + sleep 30 + volumeMounts: + - name: workspace + mountPath: /workspace + subPath: projects/test-project-alpha/workspace + resources: + requests: + cpu: 250m + memory: 256Mi + volumes: + - name: workspace + persistentVolumeClaim: + claimName: scion-workspaces + restartPolicy: Never + tolerations: + - key: "kubernetes.io/arch" + operator: "Equal" + value: "amd64" + effect: "NoSchedule" diff --git a/hack/k8s-nfs/nm2-test-pod-b1.yaml b/hack/k8s-nfs/nm2-test-pod-b1.yaml new file mode 100644 index 000000000..fc29aa3a4 --- /dev/null +++ b/hack/k8s-nfs/nm2-test-pod-b1.yaml @@ -0,0 +1,71 @@ +apiVersion: v1 +kind: Pod +metadata: + name: nm2-test-agent-b1 + namespace: scion-agents + labels: + test: nm2-scenario-b + scion.dev/project-id: test-project-beta +spec: + securityContext: + runAsUser: 1000 + runAsGroup: 1000 + fsGroup: 1000 + initContainers: + - name: workspace-provision + image: alpine/git:latest + command: + - sh + - -c + - | + set -e + SENTINEL="/workspace/.scion-provisioned" + if [ -f "$SENTINEL" ]; then + echo "PROVISION: sentinel found at $(cat $SENTINEL), skipping clone" + exit 0 + fi + echo "PROVISION: cloning workspace for project-beta (agent b1)..." + git clone --depth 1 https://github.com/ptone/scion.git /workspace + echo "b1:$(date -u +%Y-%m-%dT%H:%M:%SZ)" > "$SENTINEL" + echo "PROVISION: clone complete, sentinel written by b1" + volumeMounts: + - name: workspace + mountPath: /workspace + subPath: projects/test-project-beta/workspace + resources: + requests: + cpu: 250m + memory: 512Mi + containers: + - name: agent + image: busybox:1.36 + command: + - sh + - -c + - | + echo "=== AGENT B1 WORKSPACE ===" + ls -la /workspace/ + echo "=== SENTINEL ===" + cat /workspace/.scion-provisioned + echo "=== go.mod (identity check) ===" + head -3 /workspace/go.mod 2>/dev/null || echo "go.mod not found" + echo "=== TEST COMPLETE (b1) ===" + sleep 60 + volumeMounts: + - name: workspace + mountPath: /workspace + subPath: projects/test-project-beta/workspace + resources: + requests: + cpu: 250m + memory: 256Mi + volumes: + - name: workspace + persistentVolumeClaim: + claimName: scion-workspaces + restartPolicy: Never + tolerations: + - key: "kubernetes.io/arch" + operator: "Equal" + value: "amd64" + effect: "NoSchedule" diff --git a/hack/k8s-nfs/nm2-test-pod-b2.yaml b/hack/k8s-nfs/nm2-test-pod-b2.yaml new file mode 100644 index 000000000..e2b3647ed --- /dev/null +++ b/hack/k8s-nfs/nm2-test-pod-b2.yaml @@ -0,0 +1,71 @@ +apiVersion: v1 +kind: Pod +metadata: + name: nm2-test-agent-b2 + namespace: scion-agents + labels: + test: nm2-scenario-b + scion.dev/project-id: test-project-beta +spec: + securityContext: + runAsUser: 1000 + runAsGroup: 1000 + fsGroup: 1000 + initContainers: + - name: workspace-provision + image: alpine/git:latest + command: + - sh + - -c + - | + set -e + SENTINEL="/workspace/.scion-provisioned" + if [ -f "$SENTINEL" ]; then + echo "PROVISION: sentinel found at $(cat $SENTINEL), skipping clone" + exit 0 + fi + echo "PROVISION: cloning workspace for project-beta (agent b2)..." + git clone --depth 1 https://github.com/ptone/scion.git /workspace + echo "b2:$(date -u +%Y-%m-%dT%H:%M:%SZ)" > "$SENTINEL" + echo "PROVISION: clone complete, sentinel written by b2" + volumeMounts: + - name: workspace + mountPath: /workspace + subPath: projects/test-project-beta/workspace + resources: + requests: + cpu: 250m + memory: 512Mi + containers: + - name: agent + image: busybox:1.36 + command: + - sh + - -c + - | + echo "=== AGENT B2 WORKSPACE ===" + ls -la /workspace/ + echo "=== SENTINEL ===" + cat /workspace/.scion-provisioned + echo "=== go.mod (identity check) ===" + head -3 /workspace/go.mod 2>/dev/null || echo "go.mod not found" + echo "=== TEST COMPLETE (b2) ===" + sleep 60 + volumeMounts: + - name: workspace + mountPath: /workspace + subPath: projects/test-project-beta/workspace + resources: + requests: + cpu: 250m + memory: 256Mi + volumes: + - name: workspace + persistentVolumeClaim: + claimName: scion-workspaces + restartPolicy: Never + tolerations: + - key: "kubernetes.io/arch" + operator: "Equal" + value: "amd64" + effect: "NoSchedule" diff --git a/hack/k8s-nfs/nm2-test-pod-e.yaml b/hack/k8s-nfs/nm2-test-pod-e.yaml new file mode 100644 index 000000000..856f923fc --- /dev/null +++ b/hack/k8s-nfs/nm2-test-pod-e.yaml @@ -0,0 +1,97 @@ +apiVersion: v1 +kind: Pod +metadata: + name: nm2-test-agent-e + namespace: scion-agents + labels: + test: nm2-scenario-e + scion.dev/project-id: test-project-epsilon +spec: + securityContext: + runAsUser: 1000 + runAsGroup: 1000 + fsGroup: 1000 + initContainers: + - name: workspace-provision + image: alpine/git:latest + command: + - sh + - -c + - | + set -e + SENTINEL="/workspace/.scion-provisioned" + if [ -f "$SENTINEL" ]; then + echo "PROVISION: sentinel found, skipping clone" + exit 0 + fi + echo "PROVISION: cloning workspace..." + git clone --depth 1 https://github.com/ptone/scion.git /workspace + echo "$(date -u +%Y-%m-%dT%H:%M:%SZ)" > "$SENTINEL" + echo "PROVISION: clone complete" + volumeMounts: + - name: workspace + mountPath: /workspace + subPath: projects/test-project-epsilon/workspace + resources: + requests: + cpu: 250m + memory: 512Mi + - name: shared-dir-provision + image: busybox:1.36 + command: + - sh + - -c + - | + echo "SHARED-DIR: ensuring directory exists..." + mkdir -p /shared/test-data + echo "shared-dir-test-content" > /shared/test-data/readme.txt + ls -la /shared/ + echo "SHARED-DIR: provisioned" + volumeMounts: + - name: shared-dir-0 + mountPath: /shared + subPath: projects/test-project-epsilon/shared-dirs/test-data + resources: + requests: + cpu: 250m + memory: 128Mi + containers: + - name: agent + image: busybox:1.36 + command: + - sh + - -c + - | + echo "=== WORKSPACE ===" + ls -la /workspace/ | head -10 + echo "=== SHARED DIR (/scion-volumes/test-data) ===" + ls -la /scion-volumes/test-data/ + cat /scion-volumes/test-data/readme.txt + echo "=== MOUNT VERIFICATION ===" + echo "Workspace and shared dir are on same PVC with different subPaths" + echo "=== TEST COMPLETE (e) ===" + sleep 30 + volumeMounts: + - name: workspace + mountPath: /workspace + subPath: projects/test-project-epsilon/workspace + - name: shared-dir-0 + mountPath: /scion-volumes/test-data + subPath: projects/test-project-epsilon/shared-dirs/test-data + resources: + requests: + cpu: 250m + memory: 256Mi + volumes: + - name: workspace + persistentVolumeClaim: + claimName: scion-workspaces + - name: shared-dir-0 + persistentVolumeClaim: + claimName: scion-workspaces + restartPolicy: Never + tolerations: + - key: "kubernetes.io/arch" + operator: "Equal" + value: "amd64" + effect: "NoSchedule" diff --git a/hack/k8s-nfs/scion-nfs-pv.yaml b/hack/k8s-nfs/scion-nfs-pv.yaml new file mode 100644 index 000000000..0fed9b6a9 --- /dev/null +++ b/hack/k8s-nfs/scion-nfs-pv.yaml @@ -0,0 +1,31 @@ +apiVersion: v1 +kind: Namespace +metadata: + name: scion-agents +--- +apiVersion: v1 +kind: PersistentVolume +metadata: + name: scion-workspaces +spec: + capacity: + storage: 1Ti + accessModes: [ReadWriteMany] + nfs: + server: 10.45.255.170 + path: /scion_share + mountOptions: [vers=3, hard, nconnect=4] + persistentVolumeReclaimPolicy: Retain +--- +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: scion-workspaces + namespace: scion-agents +spec: + accessModes: [ReadWriteMany] + storageClassName: "" + volumeName: scion-workspaces + resources: + requests: + storage: 1Ti diff --git a/hack/minttoken/main.go b/hack/minttoken/main.go new file mode 100644 index 000000000..705957868 --- /dev/null +++ b/hack/minttoken/main.go @@ -0,0 +1,61 @@ +// Command minttoken mints a user access-token JWT for API-level integration +// testing against the running hubs. It looks up an existing (preferably admin) +// user in the shared Postgres DB and signs a token with the per-hub signing key +// read from Secret Manager. Not part of the product; used only for test driving. +package main + +import ( + "context" + "encoding/base64" + "fmt" + "os" + + "github.com/jackc/pgx/v5" + + "github.com/GoogleCloudPlatform/scion/pkg/hub" +) + +func main() { + dsn := os.Getenv("PG_DSN") + keyB64 := os.Getenv("SIGNING_KEY_B64") + if dsn == "" || keyB64 == "" { + fmt.Fprintln(os.Stderr, "PG_DSN and SIGNING_KEY_B64 required") + os.Exit(1) + } + key, err := base64.StdEncoding.DecodeString(keyB64) + if err != nil { + fmt.Fprintln(os.Stderr, "decode key:", err) + os.Exit(1) + } + + ctx := context.Background() + conn, err := pgx.Connect(ctx, dsn) + if err != nil { + fmt.Fprintln(os.Stderr, "db connect:", err) + os.Exit(1) + } + defer conn.Close(ctx) + + var id, email, displayName, role string + // Prefer an admin; fall back to any user. + err = conn.QueryRow(ctx, `SELECT id::text, email, display_name, role FROM users + ORDER BY (role = 'admin') DESC, created ASC LIMIT 1`).Scan(&id, &email, &displayName, &role) + if err != nil { + fmt.Fprintln(os.Stderr, "user lookup:", err) + os.Exit(1) + } + + svc, err := hub.NewUserTokenService(hub.UserTokenConfig{SigningKey: key}) + if err != nil { + fmt.Fprintln(os.Stderr, "token service:", err) + os.Exit(1) + } + // CLI client type → long (30-day) validity so the token outlives the test run. + token, _, err := svc.GenerateAccessToken(id, email, displayName, role, hub.ClientTypeCLI) + if err != nil { + fmt.Fprintln(os.Stderr, "mint:", err) + os.Exit(1) + } + fmt.Fprintf(os.Stderr, "user=%s email=%s role=%s\n", id, email, role) + fmt.Println(token) +} diff --git a/harnesses/README.md b/harnesses/README.md new file mode 100644 index 000000000..f812e08dd --- /dev/null +++ b/harnesses/README.md @@ -0,0 +1,78 @@ +# Opt-In Harness Bundles + +Self-contained harness configuration bundles for coding agents that are **not +installed by default**. The default-install set is `{claude, gemini}` — these +bundles are opt-in and can be installed with a single command. + +Each bundle includes everything needed to run the harness: configuration +(`config.yaml`), a container-side provisioner (`provision.py`), a Dockerfile, +and a Cloud Build configuration. + +## Available Bundles + +| Bundle | Description | Install | +|--------|-------------|---------| +| [opencode](opencode/README.md) | [OpenCode](https://opencode.ai) AI coding assistant | `scion harness-config install harnesses/opencode` | +| [codex](codex/README.md) | [Codex](https://github.com/openai/codex) OpenAI coding agent CLI | `scion harness-config install harnesses/codex` | +| [antigravity](antigravity/README.md) | [Antigravity](https://github.com/ptone/scion-antigravity) Gemini-based coding agent via OAuth | `scion harness-config install harnesses/antigravity` | + +Or install directly from GitHub (no local checkout needed): + +```sh +scion harness-config install github.com/GoogleCloudPlatform/scion/tree/main/harnesses/ +``` + +## Bundle Layout + +Each bundle directory contains: + +``` +/ + config.yaml # Harness configuration (provisioner, capabilities, auth) + provision.py # Container-side provisioner (pre-start hook) + Dockerfile # Image build (FROM scion-base) + cloudbuild.yaml # Cloud Build configuration + README.md # Bundle-specific docs (auth modes, build instructions) + home/ # Home directory files seeded at install time +``` + +## Migrating Existing Installs + +If you previously had opencode or codex harness configs installed (from +when they were part of the default set), here's what you need to know: + +1. **Already on `provisioner.type: container-script`** — no action needed. + Your existing config keeps working exactly as before. This is the case + for any config that was upgraded or installed after container-script + provisioning was introduced. + +2. **Legacy config on `provisioner.type: builtin`** — the compiled-in Go + implementation has been removed. Run the upgrade command to switch to + container-script provisioning: + ```sh + scion harness-config upgrade --activate-script + ``` + If your config directory contains a `provision.py`, the upgrade + auto-activates container-script provisioning even without the + `--activate-script` flag. If no `provision.py` exists, reinstall + from the bundle: + ```sh + scion harness-config install harnesses/ + ``` + +3. **Fresh installs** — opencode, codex, and antigravity are no longer + installed automatically. Restore any of them with a single command: + ```sh + scion harness-config install harnesses/opencode + scion harness-config install harnesses/codex + scion harness-config install harnesses/antigravity + ``` + +4. **Existing agents are unaffected** — no agent-home rewrites are + performed. Already-created agents continue to work with their + existing harness-config directories. + +## Future Work + +A `scion harness-config list --available` command to discover installable +bundles programmatically is a planned follow-up. diff --git a/harnesses/antigravity/Dockerfile b/harnesses/antigravity/Dockerfile new file mode 100644 index 000000000..bf8c1b4f3 --- /dev/null +++ b/harnesses/antigravity/Dockerfile @@ -0,0 +1,32 @@ +# syntax=docker/dockerfile:1 +# Copyright 2026 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +ARG BASE_IMAGE +FROM ${BASE_IMAGE} + +USER root + +RUN mkdir -p /home/scion/.gemini/antigravity-cli \ + && mkdir -p /home/scion/.agents \ + && chown -R scion:scion /home/scion/.gemini /home/scion/.agents + +# Install Antigravity CLI +RUN curl -fsSL -o cli.tar.gz https://storage.googleapis.com/antigravity-public/antigravity-cli/1.0.0-5288553236791296/linux-x64/cli_linux_x64.tar.gz \ + && tar -xzf cli.tar.gz \ + && mv antigravity /usr/local/bin/agy \ + && chmod +x /usr/local/bin/agy \ + && rm cli.tar.gz + +CMD ["agy"] diff --git a/harnesses/antigravity/README.md b/harnesses/antigravity/README.md new file mode 100644 index 000000000..00fab9230 --- /dev/null +++ b/harnesses/antigravity/README.md @@ -0,0 +1,61 @@ +# Antigravity Harness Bundle + +Scion harness configuration for +[Antigravity](https://github.com/ptone/scion-antigravity), a Gemini-based +coding agent CLI using OAuth via gnome-keyring. + +## Install + +From a repository checkout: + +```sh +scion harness-config install harnesses/antigravity +``` + +Or directly from GitHub: + +```sh +scion harness-config install github.com/GoogleCloudPlatform/scion/tree/main/harnesses/antigravity +``` + +## Auth Modes + +| Mode | Env / Secret | Notes | +|------|-------------|-------| +| `oauth-token` (default) | `AGY_KEYRING_TOKEN` | OAuth refresh token JSON stored in gnome-keyring | +| `vertex-ai` | `AGY_KEYRING_TOKEN` + `GOOGLE_CLOUD_PROJECT` | Enterprise/GCP mode via keyring + Vertex AI | + +Both auth modes require a JSON object containing a `refresh_token` field, +injected via the `AGY_KEYRING_TOKEN` secret. The provisioner initializes +gnome-keyring and stores the token at container startup. + +## Bundle Layout + +``` +antigravity/ + config.yaml # Harness configuration (provisioner, capabilities, auth) + provision.py # Container-side provisioner (pre-start hook) + dialect.yaml # Hook dialect mapping (antigravity events -> scion events) + Dockerfile # Image build (FROM scion-base) + cloudbuild.yaml # Cloud Build configuration + skills/.gitkeep # Skills directory placeholder + home/.gitkeep # Home files generated at provision time +``` + +## Image Build Chain + +``` +core-base -> scion-base -> scion-antigravity +``` + +The keyring packages (`gnome-keyring`, `libsecret`, `dbus-x11`) are +provided by `core-base`. The antigravity Dockerfile adds the Antigravity +CLI binary on top of `scion-base`. + +```sh +# Local Docker build +docker build --build-arg BASE_IMAGE=scion-base:latest -t scion-antigravity:latest -f Dockerfile . + +# Cloud Build +gcloud builds submit --config cloudbuild.yaml . +``` diff --git a/harnesses/antigravity/cloudbuild.yaml b/harnesses/antigravity/cloudbuild.yaml new file mode 100644 index 000000000..f4c5f420d --- /dev/null +++ b/harnesses/antigravity/cloudbuild.yaml @@ -0,0 +1,58 @@ +# Copyright 2026 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# Per-bundle Cloud Build configuration for the Antigravity harness image. +# Builds scion-antigravity on top of scion-base:<_TAG>. +# Image chain: core-base -> scion-base -> scion-antigravity +steps: + - name: 'gcr.io/cloud-builders/docker' + id: 'setup-buildx' + args: ['buildx', 'create', '--name', 'mybuilder', '--use'] + env: + - 'DOCKER_CLI_EXPERIMENTAL=enabled' + + - name: 'gcr.io/cloud-builders/docker' + id: 'bootstrap-buildx' + args: ['buildx', 'inspect', '--bootstrap'] + env: + - 'DOCKER_CLI_EXPERIMENTAL=enabled' + + - name: 'gcr.io/cloud-builders/docker' + id: 'build-scion-antigravity' + args: + - 'buildx' + - 'build' + - '--platform' + - 'linux/amd64,linux/arm64' + - '--build-arg' + - 'BASE_IMAGE=$_REGISTRY/scion-base:$_TAG' + - '-t' + - '$_REGISTRY/scion-antigravity:$_SHORT_SHA' + - '-t' + - '$_REGISTRY/scion-antigravity:$_TAG' + - '-f' + - 'Dockerfile' + - '--pull' + - '--push' + - '.' + env: + - 'DOCKER_CLI_EXPERIMENTAL=enabled' + +substitutions: + _REGISTRY: 'us-central1-docker.pkg.dev/${PROJECT_ID}/public-docker' + _TAG: 'latest' +options: + dynamicSubstitutions: true + machineType: 'E2_HIGHCPU_8' +timeout: 1200s diff --git a/harnesses/antigravity/config.yaml b/harnesses/antigravity/config.yaml new file mode 100644 index 000000000..1b34153ff --- /dev/null +++ b/harnesses/antigravity/config.yaml @@ -0,0 +1,71 @@ +harness: antigravity +image: scion-antigravity:latest +user: scion + +provisioner: + type: container-script + interface_version: 1 + command: ["python3", "/home/scion/.scion/harness/provision.py"] + timeout: 30s + lifecycle_events: + - pre-start + required_image_tools: + - python3 + - jq + +config_dir: .gemini/antigravity-cli +skills_dir: .gemini/antigravity-cli/skills +interrupt_key: C-c +instructions_file: .gemini/GEMINI.md +system_prompt_file: GEMINI.md +system_prompt_mode: prepend_to_instructions + +command: + base: ["/home/scion/.scion/harness/agy-wrapper.sh"] + task_flag: "-prompt-interactive" + task_position: after_base_args + resume_flag: "-continue" + +env_template: + SCION_AGENT_NAME: "{{ .AgentName }}" + +mcp: + global_config_file: .gemini/config/mcp_config.json + global_config_path: mcpServers + transport_field: type + transport_map: + stdio: stdio + sse: sse + streamable-http: streamable-http + +capabilities: + limits: + max_turns: { support: "yes" } + max_model_calls: { support: "no", reason: "AGY events do not distinguish model calls from invocations" } + max_duration: { support: "yes" } + telemetry: + enabled: { support: "no", reason: "AGY has no native OTEL integration" } + native_emitter: { support: "no" } + prompts: + system_prompt: { support: "partial", reason: "System prompt is downgraded to GEMINI.md preamble" } + agent_instructions: { support: "yes" } + auth: + api_key: { support: "no", reason: "AGY uses OAuth via gnome-keyring, not API keys" } + auth_file: { support: "no", reason: "AGY uses OAuth via gnome-keyring" } + oauth_token: { support: "yes", reason: "Via gnome-keyring population from AGY_KEYRING_TOKEN secret" } + vertex_ai: { support: "yes", reason: "Enterprise/GCP mode via AGY_KEYRING_TOKEN + GOOGLE_CLOUD_PROJECT" } + +auth: + types: + oauth-token: + required_env: + - any_of: ["AGY_KEYRING_TOKEN"] + vertex-ai: + # Enterprise/GCP mode: the keyring token is still required (provision.py + # validates AGY_KEYRING_TOKEN), plus the GCP project for Vertex routing. + required_env: + - any_of: ["AGY_KEYRING_TOKEN"] + - any_of: ["GOOGLE_CLOUD_PROJECT"] + autodetect: + env: + AGY_KEYRING_TOKEN: oauth-token diff --git a/harnesses/antigravity/dialect.yaml b/harnesses/antigravity/dialect.yaml new file mode 100644 index 000000000..e96ed4312 --- /dev/null +++ b/harnesses/antigravity/dialect.yaml @@ -0,0 +1,21 @@ +dialect: antigravity +event_name_field: hook_event_name +mappings: + PreInvocation: + event: model-start + PostInvocation: + event: model-end + PreToolUse: + event: tool-start + fields: + tool_name: .toolCall.name + PostToolUse: + event: tool-end + fields: + tool_name: .toolCall.name + error: .error + Stop: + event: agent-end + fields: + reason: .terminationReason + error: .error diff --git a/harnesses/antigravity/home/.gitkeep b/harnesses/antigravity/home/.gitkeep new file mode 100644 index 000000000..e69de29bb diff --git a/harnesses/antigravity/provision.py b/harnesses/antigravity/provision.py new file mode 100644 index 000000000..d3272d36b --- /dev/null +++ b/harnesses/antigravity/provision.py @@ -0,0 +1,684 @@ +#!/usr/bin/env python3 +"""Antigravity container-side provisioner. + +Runs inside the agent container during the pre-start lifecycle hook, invoked +by `sciontool harness provision --manifest ...`. Responsibilities: + + 1. Resolve auth from staged candidates (GEMINI_API_KEY or GOOGLE_API_KEY). + 2. Copy staged instructions to GEMINI.md (AGY's native instructions file). + 3. Generate .agents/hooks.json wiring AGY hook events to sciontool. + 4. Write outputs/env.json and outputs/resolved-auth.json. + +Stdlib-only — no external dependencies. +""" + +from __future__ import annotations + +import argparse +import json +import os +import shutil +import subprocess +import sys +from typing import Any + +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) + +try: + import scion_harness # type: ignore[import-not-found] +except ImportError: + scion_harness = None # type: ignore[assignment] + +PROVISION_VERSION = "2026-05-18T17:20:00Z" + +VALID_AUTH_TYPES = ("oauth-token", "vertex-ai", "none") + +EXIT_OK = 0 +EXIT_ERROR = 1 +EXIT_UNSUPPORTED = 2 + + +def _expand(path: str) -> str: + return os.path.expanduser(os.path.expandvars(path)) + + +def _load_json(path: str) -> Any: + with open(path, "r", encoding="utf-8") as f: + return json.load(f) + + +def _write_json(path: str, payload: Any) -> None: + os.makedirs(os.path.dirname(path), exist_ok=True) + tmp = path + ".tmp" + with open(tmp, "w", encoding="utf-8") as f: + json.dump(payload, f, indent=2, sort_keys=True) + f.write("\n") + os.replace(tmp, path) + + +def _present_env_keys(candidates: dict[str, Any]) -> set[str]: + raw = candidates.get("env_vars") or [] + keys = {str(k) for k in raw if isinstance(k, str)} + # Also detect AGY_KEYRING_TOKEN from environment (may be injected + # as a plain env var rather than staged through the secret pipeline). + if os.environ.get("AGY_KEYRING_TOKEN"): + keys.add("AGY_KEYRING_TOKEN") + return keys + + +def _env_secret_files(candidates: dict[str, Any]) -> dict[str, str]: + """Map of env-var name -> container path of its 0600 secret value file.""" + raw = candidates.get("env_secret_files") or {} + out: dict[str, str] = {} + if not isinstance(raw, dict): + return out + for k, v in raw.items(): + if isinstance(k, str) and isinstance(v, str) and v: + out[k] = v + return out + + +def _read_secret(env_secret_files: dict[str, str], name: str) -> str: + """Read secret from staged file, falling back to env var. Returns '' on miss.""" + path = env_secret_files.get(name) + if path: + real = _expand(path) + try: + with open(real, "r", encoding="utf-8") as f: + return f.read().rstrip("\r\n") + except OSError: + pass + return os.environ.get(name, "") + + +def _parse_env_output(output: str, env: dict[str, str]) -> None: + """Parse KEY=VALUE lines from daemon output into env dict.""" + for line in output.splitlines(): + for var in ( + "DBUS_SESSION_BUS_ADDRESS", "DBUS_SESSION_BUS_PID", + "GNOME_KEYRING_CONTROL", "SSH_AUTH_SOCK", + "GNOME_KEYRING_PID", + ): + if line.startswith(var + "="): + val = line.split("=", 1)[1].rstrip(";").strip("'\"") + env[var] = val + + +def _select_auth_method( + explicit: str, env_keys: set[str] +) -> tuple[str, str]: + has_keyring = "AGY_KEYRING_TOKEN" in env_keys + + if explicit: + if explicit not in VALID_AUTH_TYPES: + raise ValueError( + f"antigravity: unknown auth type {explicit!r}; " + f"valid types are: {', '.join(VALID_AUTH_TYPES)}" + ) + if explicit == "vertex-ai": + if has_keyring: + return "vertex-ai", "AGY_KEYRING_TOKEN" + raise ValueError( + "antigravity: auth type 'vertex-ai' selected but " + "AGY_KEYRING_TOKEN secret not found" + ) + if explicit == "oauth-token": + if has_keyring: + return "oauth-token", "AGY_KEYRING_TOKEN" + raise ValueError( + "antigravity: auth type 'oauth-token' selected but " + "AGY_KEYRING_TOKEN secret not found" + ) + if explicit == "none": + return "none", "" + + if has_keyring: + # AGY_USE_GCP=true promotes to vertex-ai (enterprise) mode + use_gcp = os.environ.get("AGY_USE_GCP", "").lower() in ("true", "1", "yes") + if use_gcp: + return "vertex-ai", "AGY_KEYRING_TOKEN" + return "oauth-token", "AGY_KEYRING_TOKEN" + + return "none", "" + + +def _generate_hooks_json(home: str) -> None: + """Generate .agents/hooks.json wiring AGY events to sciontool.""" + hook_cmd_template = ( + "jq --arg ev {event} '. + {{\"hook_event_name\": $ev}}' " + "| sciontool hook --dialect=antigravity" + ) + + def _simple_hook(event: str) -> list[dict[str, Any]]: + return [ + { + "type": "command", + "command": hook_cmd_template.format(event=event), + "timeout": 10, + } + ] + + def _tool_hook(event: str) -> list[dict[str, Any]]: + return [ + { + "matcher": ".*", + "hooks": [ + { + "type": "command", + "command": hook_cmd_template.format(event=event), + "timeout": 10, + } + ], + } + ] + + hooks_data: dict[str, Any] = { + "scion-hooks": { + "PreInvocation": _simple_hook("PreInvocation"), + "PostInvocation": _simple_hook("PostInvocation"), + "PreToolUse": _tool_hook("PreToolUse"), + "PostToolUse": _tool_hook("PostToolUse"), + "Stop": _simple_hook("Stop"), + } + } + + # AGY only fires project-local hooks. The global path + # (~/.gemini/antigravity-cli/hooks.json) loads but never executes. + agents_dir = os.path.join("/workspace", ".agents") + os.makedirs(agents_dir, exist_ok=True) + hooks_path = os.path.join(agents_dir, "hooks.json") + _write_json(hooks_path, hooks_data) + print( + f"antigravity provision: generated hooks.json at {hooks_path}", + file=sys.stderr, + ) + + +def _generate_wrapper_script( + home: str, has_token: bool, is_enterprise: bool, +) -> None: + """Generate agy-wrapper.sh that inits keyring and execs AGY. + + The keyring daemons must run in the same process tree as AGY so they + stay alive for the duration of the session. A provisioner-started daemon + dies when the provisioner exits, so we bootstrap inline here. + + Token injection always includes an env-var fallback because the + provisioner runs before scion-env is sourced — AGY_KEYRING_TOKEN may + only be available in the child process environment, not during provisioning. + + GCP/enterprise settings are also patched here at runtime for the same + reason — GOOGLE_CLOUD_PROJECT/GOOGLE_CLOUD_LOCATION and AGY_USE_GCP + are only available in the child environment. + """ + secret_path = os.path.join( + home, ".scion", "harness", "secrets", "AGY_KEYRING_TOKEN" + ) + settings_path = os.path.join( + home, ".gemini", "antigravity-cli", "settings.json" + ) + onboarding_path = os.path.join( + home, ".gemini", "antigravity-cli", "cache", "onboarding.json" + ) + + # Enterprise marker: provisioner writes this when explicit vertex-ai + # is selected. The wrapper checks both this file and AGY_USE_GCP env. + enterprise_marker = os.path.join( + home, ".scion", "harness", ".enterprise-mode" + ) + if is_enterprise: + os.makedirs(os.path.dirname(enterprise_marker), exist_ok=True) + with open(enterprise_marker, "w") as f: + f.write("1") + elif os.path.exists(enterprise_marker): + # Idempotent reprovision: if the auth mode switched away from + # vertex-ai (e.g. to oauth-token), remove the stale marker so the + # wrapper does not keep running in enterprise/GCP mode. + os.remove(enterprise_marker) + + script = f"""#!/bin/bash +# Generated by antigravity provision.py {PROVISION_VERSION} +set -e + +# Initialize DBUS session bus +eval $(dbus-launch --sh-syntax) +export DBUS_SESSION_BUS_ADDRESS + +# Unlock and start gnome-keyring +eval $(echo "test" | gnome-keyring-daemon --unlock 2>/dev/null) +gnome-keyring-daemon --start --components=secrets,pkcs11,ssh > /dev/null 2>&1 + +echo "agy-wrapper: keyring initialized (DBUS=$DBUS_SESSION_BUS_ADDRESS)" >&2 + +# Inject OAuth token into keyring (secret file first, env var fallback) +if [ -f "{secret_path}" ]; then + secret-tool store \\ + --label="Password for antigravity on gemini" \\ + service gemini username antigravity \\ + < "{secret_path}" 2>/dev/null \\ + && echo "agy-wrapper: token injected into keyring (from file)" >&2 \\ + || echo "agy-wrapper: WARNING: failed to inject token" >&2 +elif [ -n "${{AGY_KEYRING_TOKEN:-}}" ]; then + printf '%s' "$AGY_KEYRING_TOKEN" | secret-tool store \\ + --label="Password for antigravity on gemini" \\ + service gemini username antigravity 2>/dev/null \\ + && echo "agy-wrapper: token injected into keyring (from env)" >&2 \\ + || echo "agy-wrapper: WARNING: failed to inject token" >&2 +else + echo "agy-wrapper: no token available, AGY will prompt for login" >&2 +fi + +# GCP/enterprise mode: patch settings.json with gcp block and mark +# enterprise onboarding complete. Triggered by explicit vertex-ai auth +# (marker file) or AGY_USE_GCP=true env var. +_use_gcp=false +if [ -f "{enterprise_marker}" ]; then + _use_gcp=true +elif [ "${{AGY_USE_GCP:-}}" = "true" ] || [ "${{AGY_USE_GCP:-}}" = "1" ] || [ "${{AGY_USE_GCP:-}}" = "yes" ]; then + _use_gcp=true +fi + +if [ "$_use_gcp" = "true" ]; then + _gcp_project="${{GOOGLE_CLOUD_PROJECT:-}}" + _gcp_location="${{GOOGLE_CLOUD_LOCATION:-global}}" + + if [ -n "$_gcp_project" ]; then + python3 -c " +import json, sys +p = '{settings_path}' +with open(p) as f: d = json.load(f) +d['gcp'] = {{'project': '$_gcp_project', 'location': '$_gcp_location'}} +d['enableTelemetry'] = False +with open(p, 'w') as f: json.dump(d, f, indent=2); f.write('\\n') +print('agy-wrapper: patched settings.json with gcp config', file=sys.stderr) +" + else + echo "agy-wrapper: WARNING: GCP mode but GOOGLE_CLOUD_PROJECT not set" >&2 + fi + + python3 -c " +import json, sys +p = '{onboarding_path}' +with open(p) as f: d = json.load(f) +d['enterpriseOnboardingComplete'] = True +with open(p, 'w') as f: json.dump(d, f, indent=2); f.write('\\n') +print('agy-wrapper: marked enterprise onboarding complete', file=sys.stderr) +" +fi + +# Exec AGY with all arguments passed through +exec agy --dangerously-skip-permissions "$@" +""" + + wrapper_path = os.path.join(home, ".scion", "harness", "agy-wrapper.sh") + os.makedirs(os.path.dirname(wrapper_path), exist_ok=True) + with open(wrapper_path, "w", encoding="utf-8") as f: + f.write(script) + os.chmod(wrapper_path, 0o755) + print( + f"antigravity provision: generated wrapper at {wrapper_path}", + file=sys.stderr, + ) + + +AGY_MCP_MAPPING: dict[str, Any] = { + "global_config_file": ".gemini/config/mcp_config.json", + "global_config_path": "mcpServers", + "transport_field": "type", + "transport_map": { + "stdio": "stdio", + "sse": "sse", + "streamable-http": "streamable-http", + }, +} + + +def _apply_mcp(bundle: str) -> None: + """Apply staged MCP server configuration into AGY's mcp_config.json.""" + if scion_harness is None: + return + try: + count = scion_harness.apply_mcp_servers_simple(bundle, AGY_MCP_MAPPING) + except (ValueError, OSError) as exc: + print( + f"antigravity provision: MCP config error: {exc}", + file=sys.stderr, + ) + return + if count > 0: + print( + f"antigravity provision: applied {count} MCP server(s)", + file=sys.stderr, + ) + + +def _copy_instructions(bundle: str, home: str, instructions_file: str) -> None: + """Copy staged instructions to the path declared in config.yaml.""" + src = os.path.join(bundle, "inputs", "instructions.md") + if not os.path.isfile(src): + return + dst = os.path.join(home, instructions_file) + os.makedirs(os.path.dirname(dst), exist_ok=True) + shutil.copy2(src, dst) + print( + f"antigravity provision: copied instructions to {dst}", + file=sys.stderr, + ) + + +def _prestage_onboarding( + home: str, workspace: str = "/workspace", enterprise: bool = False, + model: str = "", +) -> None: + """Pre-stage AGY config files to skip interactive onboarding. + + AGY requires several files to exist before it will skip the login + menu, theme selection, workspace trust prompt, and TOS agreement. + + When enterprise=True (vertex-ai mode), also marks enterprise + onboarding complete. GCP project/location are patched at runtime + by the wrapper script (env vars aren't available during provisioning). + """ + import uuid + + gemini_dir = os.path.join(home, ".gemini") + cli_dir = os.path.join(gemini_dir, "antigravity-cli") + config_dir = os.path.join(gemini_dir, "config") + projects_dir = os.path.join(config_dir, "projects") + skills_dir = os.path.join(config_dir, "skills") + cache_dir = os.path.join(cli_dir, "cache") + bin_dir = os.path.join(cli_dir, "bin") + antigravity_dir = os.path.join(home, ".antigravitycli") + + for d in (cli_dir, config_dir, projects_dir, skills_dir, + cache_dir, bin_dir, antigravity_dir, + os.path.join(cli_dir, "knowledge"), + os.path.join(cli_dir, "log"), + os.path.join(cli_dir, "conversations"), + os.path.join(cli_dir, "brain")): + os.makedirs(d, exist_ok=True) + + # settings.json — trusts workspace, marks onboarding complete, sets model. + # onboardingComplete lives here (not in cache/onboarding.json) per + # observed post-login AGY config state. + settings_path = os.path.join(cli_dir, "settings.json") + if not os.path.isfile(settings_path): + settings: dict[str, Any] = { + "colorScheme": "dark", + "onboardingComplete": True, + "trustedWorkspaces": [workspace], + } + if model: + settings["model"] = model + _write_json(settings_path, settings) + + # cache/onboarding.json — marks onboarding complete. + # Always set enterpriseOnboardingComplete=true regardless of auth mode: + # in consumer mode it's a no-op; in GCP mode it's required to skip the + # enterprise onboarding flow (theme selector, etc.). + onboarding_path = os.path.join(cache_dir, "onboarding.json") + if not os.path.isfile(onboarding_path): + _write_json(onboarding_path, { + "consumerOnboardingComplete": True, + "enterpriseOnboardingComplete": True, + "onboardingComplete": True, + }) + + # installation_id — unique per container + install_id_path = os.path.join(cli_dir, "installation_id") + if not os.path.isfile(install_id_path): + with open(install_id_path, "w") as f: + f.write(str(uuid.uuid4())) + + # project registration with gitFolder format + project_id = str(uuid.uuid4()) + project_path = os.path.join(projects_dir, project_id + ".json") + if not any(f.endswith(".json") for f in os.listdir(projects_dir)): + _write_json(project_path, { + "id": project_id, + "name": workspace, + "projectResources": { + "resources": [{ + "gitFolder": { + "folderUri": f"file://{workspace}", + "allowWrite": True, + }, + }], + }, + }) + + # workspace marker + workspace_marker = os.path.join(antigravity_dir, project_id + ".json") + if not any(f.endswith(".json") for f in os.listdir(antigravity_dir)): + with open(workspace_marker, "w") as f: + pass # empty file + + # bin/agentapi shim + agentapi_path = os.path.join(bin_dir, "agentapi") + if not os.path.isfile(agentapi_path): + with open(agentapi_path, "w") as f: + f.write('#!/bin/sh\nexec "/usr/local/bin/agy" agentapi "$@"\n') + os.chmod(agentapi_path, 0o755) + + # skills/.gitkeep + gitkeep_path = os.path.join(skills_dir, ".gitkeep") + if not os.path.isfile(gitkeep_path): + with open(gitkeep_path, "w") as f: + pass + + # config migration marker + migrated_path = os.path.join(config_dir, ".migrated") + if not os.path.isfile(migrated_path): + with open(migrated_path, "w") as f: + pass + + # .geminiignore + ignore_path = os.path.join(gemini_dir, ".geminiignore") + if not os.path.isfile(ignore_path): + with open(ignore_path, "w") as f: + f.write(".scion/\n") + + # Chown everything under ~/.gemini to the agent user so AGY (which runs + # as that user) can write back to settings.json, onboarding.json, etc. + # The provisioner runs as root; without this AGY silently fails on writes + # and loops back to onboarding steps like the theme selector. + # Use stat(home) to get the target uid/gid — USER env var may be "root" + # when the provisioner runs as root, making getpwnam("root") wrong. + try: + home_stat = os.stat(home) + uid, gid = home_stat.st_uid, home_stat.st_gid + print( + f"antigravity provision: chowning ~/.gemini to uid={uid} gid={gid}", + file=sys.stderr, + ) + count = 0 + skipped = 0 + for dirpath, dirnames, filenames in os.walk(gemini_dir): + try: + os.chown(dirpath, uid, gid) + except OSError: + skipped += 1 + for fname in filenames: + fpath = os.path.join(dirpath, fname) + try: + os.chown(fpath, uid, gid) + count += 1 + except OSError: + skipped += 1 + print( + f"antigravity provision: chown complete " + f"({count} files, {skipped} skipped read-only)", + file=sys.stderr, + ) + except OSError as exc: + print( + f"antigravity provision: warning: chown ~/.gemini failed: {exc}", + file=sys.stderr, + ) + + print( + "antigravity provision: pre-staged onboarding files", + file=sys.stderr, + ) + + +def _provision(manifest: dict[str, Any]) -> int: + home = os.environ.get("HOME") or os.path.expanduser("~") + print( + f"antigravity provision: version={PROVISION_VERSION} " + f"home={home} uid={os.getuid()} gid={os.getgid()}", + file=sys.stderr, + ) + bundle = manifest.get("harness_bundle_dir") or "$HOME/.scion/harness" + bundle = _expand(bundle) + + inputs_dir = os.path.join(bundle, "inputs") + auth_candidates_path = os.path.join(inputs_dir, "auth-candidates.json") + + candidates: dict[str, Any] = {} + if os.path.isfile(auth_candidates_path): + try: + candidates = _load_json(auth_candidates_path) or {} + except (OSError, json.JSONDecodeError) as exc: + print( + f"antigravity provision: invalid auth-candidates.json: {exc}", + file=sys.stderr, + ) + return EXIT_ERROR + + explicit = str(candidates.get("explicit_type") or "").strip() + env_keys = _present_env_keys(candidates) + secret_files = _env_secret_files(candidates) + + try: + method, env_key = _select_auth_method(explicit, env_keys) + except ValueError as exc: + print(str(exc), file=sys.stderr) + return EXIT_ERROR + + outputs = manifest.get("outputs") or {} + env_out = _expand( + outputs.get("env") or os.path.join(bundle, "outputs", "env.json") + ) + auth_out = _expand( + outputs.get("resolved_auth") + or os.path.join(bundle, "outputs", "resolved-auth.json") + ) + + resolved_payload: dict[str, Any] = { + "schema_version": 1, + "harness": "antigravity", + "method": method, + "explicit_type": explicit or None, + } + + env_payload: dict[str, Any] = {} + + # Validate token if an auth method requiring it was selected + has_token = False + if method in ("oauth-token", "vertex-ai"): + token_raw = _read_secret(secret_files, "AGY_KEYRING_TOKEN") + if not token_raw: + print( + "antigravity provision: AGY_KEYRING_TOKEN secret is empty", + file=sys.stderr, + ) + return EXIT_ERROR + try: + token_obj = json.loads(token_raw) + except json.JSONDecodeError as exc: + print( + f"antigravity provision: AGY_KEYRING_TOKEN is not valid JSON: {exc}", + file=sys.stderr, + ) + return EXIT_ERROR + if not isinstance(token_obj, dict) or "refresh_token" not in token_obj: + print( + "antigravity provision: AGY_KEYRING_TOKEN must contain refresh_token", + file=sys.stderr, + ) + return EXIT_ERROR + has_token = True + + is_enterprise = method == "vertex-ai" + + # Generate wrapper script that inits keyring and execs AGY. + # Keyring daemons must run in AGY's process tree (not the + # provisioner's) so they stay alive for the session. + _generate_wrapper_script(home, has_token, is_enterprise) + + try: + _write_json(auth_out, resolved_payload) + _write_json(env_out, env_payload) + except OSError as exc: + print( + f"antigravity provision: failed to write outputs: {exc}", + file=sys.stderr, + ) + return EXIT_ERROR + + harness_cfg = manifest.get("harness_config") or {} + instructions_file = harness_cfg.get("instructions_file") or "GEMINI.md" + # AGY doesn't support --model flag so we write the model into settings.json. + # AGY_MODEL env var overrides; otherwise use the default. + model = os.environ.get("AGY_MODEL", "") or "Gemini 3.5 Flash" + _copy_instructions(bundle, home, instructions_file) + _generate_hooks_json(home) + _prestage_onboarding(home, enterprise=is_enterprise, model=model) + _apply_mcp(bundle) + + print(f"antigravity provision: method={method}", file=sys.stderr) + return EXIT_OK + + +def _dispatch(manifest: dict[str, Any]) -> int: + command = str(manifest.get("command") or "provision") + if command == "provision": + return _provision(manifest) + print( + f"antigravity provision: unsupported command {command!r}", + file=sys.stderr, + ) + return EXIT_UNSUPPORTED + + +def main() -> int: + parser = argparse.ArgumentParser( + description="Antigravity container-side provisioner" + ) + parser.add_argument( + "--manifest", + help="Path to the staged manifest.json", + default=None, + ) + args = parser.parse_args() + + manifest_path = args.manifest + if not manifest_path: + home = os.environ.get("HOME") or os.path.expanduser("~") + manifest_path = os.path.join(home, ".scion", "harness", "manifest.json") + + try: + manifest = _load_json(manifest_path) + except FileNotFoundError: + print( + f"antigravity provision: manifest not found at {manifest_path}", + file=sys.stderr, + ) + return EXIT_ERROR + except (OSError, json.JSONDecodeError) as exc: + print( + f"antigravity provision: failed to load manifest: {exc}", + file=sys.stderr, + ) + return EXIT_ERROR + + if not isinstance(manifest, dict): + print("antigravity provision: manifest is not an object", file=sys.stderr) + return EXIT_ERROR + + return _dispatch(manifest) + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/harnesses/antigravity/skills/.gitkeep b/harnesses/antigravity/skills/.gitkeep new file mode 100644 index 000000000..e69de29bb diff --git a/image-build/codex/Dockerfile b/harnesses/codex/Dockerfile similarity index 100% rename from image-build/codex/Dockerfile rename to harnesses/codex/Dockerfile diff --git a/harnesses/codex/README.md b/harnesses/codex/README.md new file mode 100644 index 000000000..7fcf93287 --- /dev/null +++ b/harnesses/codex/README.md @@ -0,0 +1,49 @@ +# Codex Harness Bundle + +Scion harness configuration for [Codex](https://github.com/openai/codex), +OpenAI's coding agent CLI. + +## Install + +From a repository checkout: + +```sh +scion harness-config install harnesses/codex +``` + +Or directly from GitHub: + +```sh +scion harness-config install github.com/GoogleCloudPlatform/scion/tree/main/harnesses/codex +``` + +## Auth Modes + +| Mode | Env / File | Notes | +|------|-----------|-------| +| `api-key` (default) | `CODEX_API_KEY` or `OPENAI_API_KEY` | Codex key takes precedence | +| `auth-file` | `~/.codex/auth.json` | Codex native auth file | + +## Bundle Layout + +``` +codex/ + config.yaml # Harness configuration (provisioner, capabilities, auth) + provision.py # Container-side provisioner (pre-start hook) + Dockerfile # Image build (FROM scion-base) + cloudbuild.yaml # Cloud Build configuration + home/ + .bashrc # Shell config with scion env sourcing + .codex/config.toml # Codex client settings (model, otel, etc.) + .codex/scion_notify.sh # Notification hook script +``` + +## Build the Image + +```sh +# Local Docker build +docker build --build-arg BASE_IMAGE=scion-base:latest -t scion-codex:latest -f Dockerfile . + +# Cloud Build +gcloud builds submit --config cloudbuild.yaml . +``` diff --git a/harnesses/codex/cloudbuild.yaml b/harnesses/codex/cloudbuild.yaml new file mode 100644 index 000000000..91b75ee72 --- /dev/null +++ b/harnesses/codex/cloudbuild.yaml @@ -0,0 +1,57 @@ +# Copyright 2026 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# Per-bundle Cloud Build configuration for the Codex harness image. +# Builds scion-codex on top of scion-base:<_TAG>. +steps: + - name: 'gcr.io/cloud-builders/docker' + id: 'setup-buildx' + args: ['buildx', 'create', '--name', 'mybuilder', '--use'] + env: + - 'DOCKER_CLI_EXPERIMENTAL=enabled' + + - name: 'gcr.io/cloud-builders/docker' + id: 'bootstrap-buildx' + args: ['buildx', 'inspect', '--bootstrap'] + env: + - 'DOCKER_CLI_EXPERIMENTAL=enabled' + + - name: 'gcr.io/cloud-builders/docker' + id: 'build-scion-codex' + args: + - 'buildx' + - 'build' + - '--platform' + - 'linux/amd64,linux/arm64' + - '--build-arg' + - 'BASE_IMAGE=$_REGISTRY/scion-base:$_TAG' + - '-t' + - '$_REGISTRY/scion-codex:$_SHORT_SHA' + - '-t' + - '$_REGISTRY/scion-codex:$_TAG' + - '-f' + - 'Dockerfile' + - '--pull' + - '--push' + - '.' + env: + - 'DOCKER_CLI_EXPERIMENTAL=enabled' + +substitutions: + _REGISTRY: 'us-central1-docker.pkg.dev/${PROJECT_ID}/public-docker' + _TAG: 'latest' +options: + dynamicSubstitutions: true + machineType: 'E2_HIGHCPU_8' +timeout: 1200s diff --git a/pkg/harness/codex/embeds/config.yaml b/harnesses/codex/config.yaml similarity index 100% rename from pkg/harness/codex/embeds/config.yaml rename to harnesses/codex/config.yaml diff --git a/pkg/harness/codex/embeds/bashrc b/harnesses/codex/home/.bashrc similarity index 100% rename from pkg/harness/codex/embeds/bashrc rename to harnesses/codex/home/.bashrc diff --git a/pkg/harness/codex/embeds/config.toml b/harnesses/codex/home/.codex/config.toml similarity index 100% rename from pkg/harness/codex/embeds/config.toml rename to harnesses/codex/home/.codex/config.toml diff --git a/pkg/harness/codex/embeds/scion_notify.sh b/harnesses/codex/home/.codex/scion_notify.sh similarity index 100% rename from pkg/harness/codex/embeds/scion_notify.sh rename to harnesses/codex/home/.codex/scion_notify.sh diff --git a/pkg/harness/codex/embeds/provision.py b/harnesses/codex/provision.py old mode 100644 new mode 100755 similarity index 100% rename from pkg/harness/codex/embeds/provision.py rename to harnesses/codex/provision.py diff --git a/image-build/opencode/Dockerfile b/harnesses/opencode/Dockerfile similarity index 100% rename from image-build/opencode/Dockerfile rename to harnesses/opencode/Dockerfile diff --git a/harnesses/opencode/README.md b/harnesses/opencode/README.md new file mode 100644 index 000000000..8ba1746ed --- /dev/null +++ b/harnesses/opencode/README.md @@ -0,0 +1,47 @@ +# OpenCode Harness Bundle + +Scion harness configuration for [OpenCode](https://opencode.ai), an open-source +AI coding assistant. + +## Install + +From a repository checkout: + +```sh +scion harness-config install harnesses/opencode +``` + +Or directly from GitHub: + +```sh +scion harness-config install github.com/GoogleCloudPlatform/scion/tree/main/harnesses/opencode +``` + +## Auth Modes + +| Mode | Env / File | Notes | +|------|-----------|-------| +| `api-key` (default) | `ANTHROPIC_API_KEY` or `OPENAI_API_KEY` | Anthropic key takes precedence | +| `auth-file` | `~/.local/share/opencode/auth.json` | OpenCode native auth file | + +## Bundle Layout + +``` +opencode/ + config.yaml # Harness configuration (provisioner, capabilities, auth) + provision.py # Container-side provisioner (pre-start hook) + Dockerfile # Image build (FROM scion-base) + cloudbuild.yaml # Cloud Build configuration + home/ + .config/opencode/opencode.json # OpenCode client settings +``` + +## Build the Image + +```sh +# Local Docker build +docker build --build-arg BASE_IMAGE=scion-base:latest -t scion-opencode:latest -f Dockerfile . + +# Cloud Build +gcloud builds submit --config cloudbuild.yaml . +``` diff --git a/harnesses/opencode/cloudbuild.yaml b/harnesses/opencode/cloudbuild.yaml new file mode 100644 index 000000000..57a8a89bd --- /dev/null +++ b/harnesses/opencode/cloudbuild.yaml @@ -0,0 +1,57 @@ +# Copyright 2026 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# Per-bundle Cloud Build configuration for the OpenCode harness image. +# Builds scion-opencode on top of scion-base:<_TAG>. +steps: + - name: 'gcr.io/cloud-builders/docker' + id: 'setup-buildx' + args: ['buildx', 'create', '--name', 'mybuilder', '--use'] + env: + - 'DOCKER_CLI_EXPERIMENTAL=enabled' + + - name: 'gcr.io/cloud-builders/docker' + id: 'bootstrap-buildx' + args: ['buildx', 'inspect', '--bootstrap'] + env: + - 'DOCKER_CLI_EXPERIMENTAL=enabled' + + - name: 'gcr.io/cloud-builders/docker' + id: 'build-scion-opencode' + args: + - 'buildx' + - 'build' + - '--platform' + - 'linux/amd64,linux/arm64' + - '--build-arg' + - 'BASE_IMAGE=$_REGISTRY/scion-base:$_TAG' + - '-t' + - '$_REGISTRY/scion-opencode:$_SHORT_SHA' + - '-t' + - '$_REGISTRY/scion-opencode:$_TAG' + - '-f' + - 'Dockerfile' + - '--pull' + - '--push' + - '.' + env: + - 'DOCKER_CLI_EXPERIMENTAL=enabled' + +substitutions: + _REGISTRY: 'us-central1-docker.pkg.dev/${PROJECT_ID}/public-docker' + _TAG: 'latest' +options: + dynamicSubstitutions: true + machineType: 'E2_HIGHCPU_8' +timeout: 1200s diff --git a/pkg/harness/opencode/embeds/config.yaml b/harnesses/opencode/config.yaml similarity index 100% rename from pkg/harness/opencode/embeds/config.yaml rename to harnesses/opencode/config.yaml diff --git a/pkg/harness/opencode/embeds/opencode.json b/harnesses/opencode/home/.config/opencode/opencode.json similarity index 100% rename from pkg/harness/opencode/embeds/opencode.json rename to harnesses/opencode/home/.config/opencode/opencode.json diff --git a/pkg/harness/opencode/embeds/provision.py b/harnesses/opencode/provision.py similarity index 100% rename from pkg/harness/opencode/embeds/provision.py rename to harnesses/opencode/provision.py diff --git a/image-build/README.md b/image-build/README.md index 25e4019d0..47c2e650b 100644 --- a/image-build/README.md +++ b/image-build/README.md @@ -7,14 +7,19 @@ Dockerfiles and build configurations for Scion container images. ``` core-base System dependencies (Go, Node, Python) └── scion-base Adds sciontool binary and scion user - ├── claude Claude Code harness - ├── gemini Gemini CLI harness - ├── opencode OpenCode harness - ├── codex Codex harness - └── hub Scion hub server + ├── claude Claude Code harness + ├── gemini Gemini CLI harness + ├── opencode OpenCode harness (bundle-local build) + ├── codex Codex harness (bundle-local build) + ├── antigravity Antigravity harness (bundle-local build) + └── hub Scion hub server ``` -Each harness directory (and `hub/`) contains a `Dockerfile` that extends `scion-base` with image-specific tooling. +The `claude/`, `gemini/`, and `hub/` directories live under `image-build/` and +each contains a `Dockerfile` that extends `scion-base`. The `opencode`, `codex`, +and `antigravity` images build from their self-contained bundles under +`harnesses//` (each bundle carries its own `Dockerfile` and +`cloudbuild.yaml`). See [`harnesses/README.md`](../harnesses/README.md). ## Scripts @@ -48,7 +53,7 @@ The orchestrator owns target sequencing, tag computation, and BASE_IMAGE threadi |---|---|---| | `core-base` | `core-base` | Foundation tools layer. | | `scion-base` | `scion-base` | Adds sciontool. Uses existing `core-base:`. | -| `harnesses` | `scion-claude`, `scion-gemini`, `scion-opencode`, `scion-codex` | Uses existing `scion-base:`. | +| `harnesses` | `scion-claude`, `scion-gemini` (+ opt-in bundle images) | Uses existing `scion-base:`. Opt-in harness images (opencode, codex, antigravity) build from `harnesses//`. | | `hub` | `scion-hub` | Hub server image. Uses existing `scion-base:`. | | `common` (default) | `scion-base` + harnesses + hub | Skips `core-base`. Most common rebuild. | | `all` | Full DAG | Rebuilds everything from `core-base`. | diff --git a/image-build/cloudbuild-harnesses.yaml b/image-build/cloudbuild-harnesses.yaml index c347be6b3..7b3bc109e 100644 --- a/image-build/cloudbuild-harnesses.yaml +++ b/image-build/cloudbuild-harnesses.yaml @@ -84,7 +84,7 @@ steps: # Build OpenCode Harness Image - name: 'gcr.io/cloud-builders/docker' id: 'build-scion-opencode' - dir: 'image-build/opencode' + dir: 'harnesses/opencode' args: - 'buildx' - 'build' @@ -107,7 +107,7 @@ steps: # Build Codex Harness Image - name: 'gcr.io/cloud-builders/docker' id: 'build-scion-codex' - dir: 'image-build/codex' + dir: 'harnesses/codex' args: - 'buildx' - 'build' diff --git a/image-build/scripts/lib/targets.sh b/image-build/scripts/lib/targets.sh index 5e02deae6..4d2b354e3 100644 --- a/image-build/scripts/lib/targets.sh +++ b/image-build/scripts/lib/targets.sh @@ -90,8 +90,8 @@ step_dockerfile() { scion-base) echo "${IMAGE_BUILD_DIR}/scion-base/Dockerfile" ;; scion-claude) echo "${IMAGE_BUILD_DIR}/claude/Dockerfile" ;; scion-gemini) echo "${IMAGE_BUILD_DIR}/gemini/Dockerfile" ;; - scion-opencode) echo "${IMAGE_BUILD_DIR}/opencode/Dockerfile" ;; - scion-codex) echo "${IMAGE_BUILD_DIR}/codex/Dockerfile" ;; + scion-opencode) echo "${REPO_ROOT}/harnesses/opencode/Dockerfile" ;; + scion-codex) echo "${REPO_ROOT}/harnesses/codex/Dockerfile" ;; scion-hub) echo "${IMAGE_BUILD_DIR}/hub/Dockerfile" ;; *) return 1 ;; esac @@ -108,8 +108,8 @@ step_context_dir() { scion-base) echo "${REPO_ROOT}" ;; scion-claude) echo "${IMAGE_BUILD_DIR}/claude" ;; scion-gemini) echo "${IMAGE_BUILD_DIR}/gemini" ;; - scion-opencode) echo "${IMAGE_BUILD_DIR}/opencode" ;; - scion-codex) echo "${IMAGE_BUILD_DIR}/codex" ;; + scion-opencode) echo "${REPO_ROOT}/harnesses/opencode" ;; + scion-codex) echo "${REPO_ROOT}/harnesses/codex" ;; scion-hub) echo "${IMAGE_BUILD_DIR}/hub" ;; *) return 1 ;; esac diff --git a/internal/fixturegen/fixturegen_test.go b/internal/fixturegen/fixturegen_test.go new file mode 100644 index 000000000..f31d05ea1 --- /dev/null +++ b/internal/fixturegen/fixturegen_test.go @@ -0,0 +1,89 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//go:build !no_sqlite + +package main + +import ( + "context" + "path/filepath" + "testing" + + entsql "entgo.io/ent/dialect/sql" + + "github.com/GoogleCloudPlatform/scion/pkg/ent/entc" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +// expectedTableCount is the number of domain tables in the hub schema +// (excluding the schema_migrations bookkeeping table). The fixture must cover +// every one of them. +const expectedTableCount = 30 + +// TestFixtureCoverage is the CI coverage gate: it generates the fixture and +// fails if any domain table has zero rows. +func TestFixtureCoverage(t *testing.T) { + path := filepath.Join(t.TempDir(), "fixture.db") + report, err := Generate(context.Background(), path) + require.NoError(t, err) + + t.Logf("fixture covers %d domain tables", report.TotalTables()) + for _, c := range report.Counts { + t.Logf(" %-32s %d row(s)", c.Table, c.Count) + } + + assert.Equal(t, expectedTableCount, report.TotalTables(), + "fixture should cover exactly the %d domain tables", expectedTableCount) + assert.Empty(t, report.Missing, + "every domain table must have at least one fixture row; missing: %v", report.Missing) +} + +// TestFixtureLoadable verifies the generated database is a valid, openable +// SQLite store with the seeded data intact. +func TestFixtureLoadable(t *testing.T) { + ctx := context.Background() + path := filepath.Join(t.TempDir(), "fixture.db") + _, err := Generate(ctx, path) + require.NoError(t, err) + + // Reopen as a fresh Ent client and confirm connectivity + seeded rows. + client, err := entc.OpenSQLite("file:"+path, entc.PoolConfig{}) + require.NoError(t, err) + t.Cleanup(func() { _ = client.Close() }) + db := client.Driver().(*entsql.Driver).DB() + require.NoError(t, db.PingContext(ctx)) + + var users int + require.NoError(t, db.QueryRowContext(ctx, "SELECT COUNT(*) FROM users").Scan(&users)) + assert.Positive(t, users, "users table should have seeded rows") + + // The soft-deleted agent edge case must be present. + var deletedAgents int + require.NoError(t, db.QueryRowContext(ctx, + "SELECT COUNT(*) FROM agents WHERE deleted_at IS NOT NULL").Scan(&deletedAgents)) + assert.Positive(t, deletedAgents, "fixture should include a soft-deleted agent") +} + +// TestFixtureDeterministic verifies the spec produces a stable set of row +// counts across runs (no time.Now()/random values leaking in). +func TestFixtureDeterministic(t *testing.T) { + ctx := context.Background() + r1, err := Generate(ctx, filepath.Join(t.TempDir(), "a.db")) + require.NoError(t, err) + r2, err := Generate(ctx, filepath.Join(t.TempDir(), "b.db")) + require.NoError(t, err) + assert.Equal(t, r1.Counts, r2.Counts, "row counts should be identical across runs") +} diff --git a/internal/fixturegen/generate.go b/internal/fixturegen/generate.go new file mode 100644 index 000000000..7f36457ed --- /dev/null +++ b/internal/fixturegen/generate.go @@ -0,0 +1,180 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package main + +import ( + "context" + "database/sql" + "fmt" + "os" + "sort" + "strings" + + entsql "entgo.io/ent/dialect/sql" + + "github.com/GoogleCloudPlatform/scion/pkg/ent/entc" +) + +// schemaMigrationsTable is the bookkeeping table excluded from coverage — it is +// not a domain table, so it carries no fixture rows. +const schemaMigrationsTable = "schema_migrations" + +// TableCount records the number of fixture rows seeded into a table. +type TableCount struct { + Table string + Count int +} + +// Report summarizes a fixture generation run. +type Report struct { + Path string // path to the generated .db + Counts []TableCount // per-table row counts (sorted by table name) + Missing []string // domain tables with zero rows (coverage failures) +} + +// TotalTables returns the number of domain tables (excluding schema_migrations) +// the report covers. +func (r *Report) TotalTables() int { return len(r.Counts) } + +// Generate builds a fresh fixture database at path by running the schema +// migrations and seeding the Go-defined Spec, then performs the coverage check. +// Foreign-key enforcement is disabled during seeding so rows can be inserted in +// spec order without a topological sort; the resulting .db is still loadable. +func Generate(ctx context.Context, path string) (*Report, error) { + // Start from a clean file so re-runs are deterministic. + if err := os.Remove(path); err != nil && !os.IsNotExist(err) { + return nil, fmt.Errorf("removing existing fixture %s: %w", path, err) + } + + client, err := entc.OpenSQLite("file:"+path, entc.PoolConfig{}) + if err != nil { + return nil, fmt.Errorf("opening fixture db: %w", err) + } + defer client.Close() + + if err := entc.AutoMigrate(ctx, client); err != nil { + return nil, fmt.Errorf("migrating fixture db: %w", err) + } + + drv, ok := client.Driver().(*entsql.Driver) + if !ok { + return nil, fmt.Errorf("ent client driver does not expose a *sql.DB") + } + db := drv.DB() + if _, err := db.ExecContext(ctx, "PRAGMA foreign_keys = OFF"); err != nil { + return nil, fmt.Errorf("disabling foreign keys: %w", err) + } + + for _, tf := range Spec() { + for i, r := range tf.Rows { + if err := insertRow(ctx, db, tf.Table, r); err != nil { + return nil, fmt.Errorf("seeding %s row %d: %w", tf.Table, i, err) + } + } + } + + report, err := checkCoverage(ctx, db, path) + if err != nil { + return nil, err + } + return report, nil +} + +// checkCoverage lists every domain table in the database and counts its rows. +// A table with zero rows is recorded in Report.Missing. +func checkCoverage(ctx context.Context, db *sql.DB, path string) (*Report, error) { + tables, err := listTables(ctx, db) + if err != nil { + return nil, err + } + + report := &Report{Path: path} + for _, t := range tables { + var n int + if err := db.QueryRowContext(ctx, fmt.Sprintf("SELECT COUNT(*) FROM %q", t)).Scan(&n); err != nil { + return nil, fmt.Errorf("counting rows in %s: %w", t, err) + } + report.Counts = append(report.Counts, TableCount{Table: t, Count: n}) + if n == 0 { + report.Missing = append(report.Missing, t) + } + } + return report, nil +} + +// listTables returns the sorted set of domain table names (excluding SQLite +// internal tables and the schema_migrations bookkeeping table). +func listTables(ctx context.Context, db *sql.DB) ([]string, error) { + rows, err := db.QueryContext(ctx, + `SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%' ORDER BY name`) + if err != nil { + return nil, fmt.Errorf("listing tables: %w", err) + } + defer rows.Close() + + var tables []string + for rows.Next() { + var name string + if err := rows.Scan(&name); err != nil { + return nil, err + } + if name == schemaMigrationsTable { + continue + } + tables = append(tables, name) + } + if err := rows.Err(); err != nil { + return nil, err + } + sort.Strings(tables) + return tables, nil +} + +// insertRow inserts a single fixture row using a parameterized statement so +// values are escaped by the driver. Columns are sorted for deterministic SQL. +func insertRow(ctx context.Context, db *sql.DB, table string, r row) error { + cols := make([]string, 0, len(r)) + for c := range r { + cols = append(cols, c) + } + sort.Strings(cols) + + placeholders := make([]string, len(cols)) + vals := make([]any, len(cols)) + quoted := make([]string, len(cols)) + for i, c := range cols { + placeholders[i] = "?" + quoted[i] = fmt.Sprintf("%q", c) + vals[i] = encode(r[c]) + } + + q := fmt.Sprintf("INSERT INTO %q (%s) VALUES (%s)", + table, strings.Join(quoted, ", "), strings.Join(placeholders, ", ")) + _, err := db.ExecContext(ctx, q, vals...) + return err +} + +// encode normalizes Go values into forms the SQLite driver accepts. Booleans +// become 0/1 integers; everything else (string, int, []byte, time.Time, nil) +// passes through unchanged. +func encode(v any) any { + if b, ok := v.(bool); ok { + if b { + return 1 + } + return 0 + } + return v +} diff --git a/internal/fixturegen/main.go b/internal/fixturegen/main.go new file mode 100644 index 000000000..924500cf8 --- /dev/null +++ b/internal/fixturegen/main.go @@ -0,0 +1,124 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Command fixturegen generates the canonical hub test fixture database +// (testdata/hub-v46-fixture.db) from a Go-defined spec, verifies that every +// domain table is covered, and caches the resulting blob to the shared +// scratchpad mount for reuse by other agents and CI. +// +// Usage: +// +// go run ./internal/fixturegen +// +// The run fails (non-zero exit) if any domain table ends up with zero rows. +package main + +import ( + "context" + "fmt" + "io" + "os" + "path/filepath" +) + +// defaultOutputPath is the repository-relative path of the generated fixture. +const defaultOutputPath = "testdata/hub-v46-fixture.db" + +// defaultCacheDir is the shared-mount location where the fixture blob is cached +// for reuse. Overridable via SCION_FIXTURE_CACHE_DIR. +const defaultCacheDir = "/scion-volumes/scratchpad/postgres-integration/fixtures" + +func main() { + if err := run(); err != nil { + fmt.Fprintf(os.Stderr, "fixturegen: %v\n", err) + os.Exit(1) + } +} + +func run() error { + ctx := context.Background() + + outPath := defaultOutputPath + if err := os.MkdirAll(filepath.Dir(outPath), 0o755); err != nil { + return fmt.Errorf("creating output dir: %w", err) + } + + report, err := Generate(ctx, outPath) + if err != nil { + return err + } + + printReport(report) + + if len(report.Missing) > 0 { + return fmt.Errorf("coverage check failed: %d table(s) with zero rows: %v", + len(report.Missing), report.Missing) + } + + // Cache the blob to the shared mount. A missing/unwritable mount is a + // warning, not a hard failure, so the fixture can still be generated + // locally without the scratchpad. + cacheDir := defaultCacheDir + if v := os.Getenv("SCION_FIXTURE_CACHE_DIR"); v != "" { + cacheDir = v + } + if cached, err := cacheBlob(outPath, cacheDir); err != nil { + fmt.Fprintf(os.Stderr, "warning: could not cache fixture to %s: %v\n", cacheDir, err) + } else { + fmt.Printf("Cached fixture blob -> %s\n", cached) + } + + return nil +} + +// printReport prints the per-table coverage report. +func printReport(r *Report) { + fmt.Printf("Generated fixture: %s\n", r.Path) + fmt.Printf("Coverage: %d domain tables\n", r.TotalTables()) + for _, c := range r.Counts { + fmt.Printf(" %-32s %d row(s)\n", c.Table, c.Count) + } +} + +// cacheBlob copies the generated fixture into cacheDir and returns the +// destination path. +func cacheBlob(srcPath, cacheDir string) (string, error) { + if err := os.MkdirAll(cacheDir, 0o755); err != nil { + return "", err + } + dst := filepath.Join(cacheDir, filepath.Base(srcPath)) + if err := copyFile(srcPath, dst); err != nil { + return "", err + } + return dst, nil +} + +func copyFile(src, dst string) error { + in, err := os.Open(src) + if err != nil { + return err + } + defer in.Close() + + out, err := os.Create(dst) + if err != nil { + return err + } + defer out.Close() + + if _, err := io.Copy(out, in); err != nil { + return err + } + return out.Close() +} diff --git a/internal/fixturegen/spec.go b/internal/fixturegen/spec.go new file mode 100644 index 000000000..06082b82d --- /dev/null +++ b/internal/fixturegen/spec.go @@ -0,0 +1,318 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package main + +import ( + "strings" + "time" +) + +// The fixture is a Go-defined spec that seeds at least one representative row +// per table of the hub schema, deliberately exercising the edge cases that most +// often break a SQLite->Postgres migration: +// +// - NULL optional fields (nullable columns left unset) +// - max-length strings (multi-kilobyte text values) +// - nested / unicode JSON (emoji + multi-byte scripts in JSON columns) +// - soft-deleted rows (deleted_at populated alongside a live row) +// +// IDs are shared across tables (a single project/user/agent/broker/...) so the +// fixture is internally coherent — foreign keys point at rows that exist — +// even though the loader disables FK enforcement while inserting in spec order. + +// Shared, stable identifiers referenced across multiple tables. +const ( + projectID = "11111111-1111-1111-1111-111111111111" + userID = "22222222-2222-2222-2222-222222222222" + agentID = "33333333-3333-3333-3333-333333333333" + brokerID = "44444444-4444-4444-4444-444444444444" + groupID = "55555555-5555-5555-5555-555555555555" + policyID = "66666666-6666-6666-6666-666666666666" + subID = "77777777-7777-7777-7777-777777777777" +) + +// baseTime is a fixed timestamp so the generated fixture is byte-reproducible +// across runs (no time.Now()). +var baseTime = time.Date(2026, time.January, 1, 12, 0, 0, 0, time.UTC) + +// maxLenString is a deliberately long value used to exercise large TEXT +// handling and column-length assumptions. +var maxLenString = strings.Repeat("x", 8192) + +// unicodeJSON is a nested JSON document mixing emoji and multi-byte scripts. +const unicodeJSON = `{"team":"🚀 platform-éñ","nested":{"langs":["日本語","العربية","emoji 😀"],"depth":{"level":2,"ok":true}}}` + +// nestedConfigJSON is a representative nested config blob. +const nestedConfigJSON = `{"harness":"claude","env":{"LOG_LEVEL":"debug","UNICODE":"naïve café 北京"},"args":["--flag","value"]}` + +// TableFixture is the seed data for a single table. +type TableFixture struct { + Table string + Rows []row +} + +// row is a column->value map. Nil values become SQL NULL; bool becomes 0/1; +// time.Time and []byte are passed through to the driver. +type row map[string]any + +// Spec returns the ordered fixture set for every hub table. Parent rows +// (projects, users, ...) are listed before the rows that reference them. +func Spec() []TableFixture { + return []TableFixture{ + // ---- Core identities referenced elsewhere ---- + {Table: "projects", Rows: []row{ + { // full row with nested/unicode JSON labels + "id": projectID, "name": "Platform", "slug": "platform", + "git_remote": "https://github.com/example/platform.git", + "labels": unicodeJSON, "annotations": `{"note":"primary"}`, + "created_at": baseTime, "updated_at": baseTime, + "owner_id": userID, "visibility": "private", + }, + { // minimal row: nullable optionals (git_remote, labels, owner...) left NULL + "id": "11111111-1111-1111-1111-1111111111aa", "name": "Minimal Project", + "slug": "minimal-project", + }, + }}, + {Table: "users", Rows: []row{ + { + "id": userID, "email": "alice@example.com", "display_name": "Alice", + "role": "admin", "status": "active", + "preferences": `{"theme":"dark"}`, "created_at": baseTime, + }, + { // max-length display_name edge case + NULL avatar_url + "id": "22222222-2222-2222-2222-2222222222aa", + "email": "long@example.com", "display_name": maxLenString, + }, + }}, + {Table: "runtime_brokers", Rows: []row{ + { + "id": brokerID, "name": "broker-1", "slug": "broker-1", "type": "docker", + "status": "online", "created_at": baseTime, "updated_at": baseTime, + "capabilities": `{"webPty":true,"sync":true,"attach":false}`, + }, + }}, + {Table: "agents", Rows: []row{ + { // live agent with nested/unicode JSON + "id": agentID, "agent_id": agentID, "name": "worker", "template": "claude", + "project_id": projectID, "labels": unicodeJSON, + "applied_config": nestedConfigJSON, + "created_at": baseTime, "updated_at": baseTime, + "phase": "running", "visibility": "private", "state_version": 1, + }, + { // soft-deleted agent (deleted_at populated) + "id": "33333333-3333-3333-3333-3333333333aa", "agent_id": "33333333-3333-3333-3333-3333333333aa", + "name": "deleted-worker", "template": "claude", "project_id": projectID, + "created_at": baseTime, "updated_at": baseTime, "deleted_at": baseTime, + "phase": "stopped", "visibility": "private", "state_version": 2, + }, + }}, + + // ---- Permissions ---- + {Table: "groups", Rows: []row{ + { + "id": groupID, "name": "Engineering", "slug": "engineering", + "description": "Eng team", "labels": unicodeJSON, + "created_at": baseTime, "updated_at": baseTime, + "group_type": "explicit", + }, + }}, + {Table: "group_members", Rows: []row{ + {"group_id": groupID, "member_type": "user", "member_id": userID, "role": "owner", "added_at": baseTime}, + {"group_id": groupID, "member_type": "agent", "member_id": agentID, "role": "member", "added_at": baseTime}, + }}, + {Table: "policies", Rows: []row{ + { + "id": policyID, "name": "Allow Read", "description": "read agents", + "scope_type": "hub", "resource_type": "agent", + "actions": `["read","list"]`, "effect": "allow", "priority": 10, + "conditions": unicodeJSON, + "created_at": baseTime, "updated_at": baseTime, + }, + }}, + {Table: "policy_bindings", Rows: []row{ + {"policy_id": policyID, "principal_type": "user", "principal_id": userID}, + {"policy_id": policyID, "principal_type": "group", "principal_id": groupID}, + }}, + + // ---- Config / scoped values ---- + {Table: "env_vars", Rows: []row{ + { + "id": "e0000000-0000-0000-0000-000000000001", "key": "LOG_LEVEL", "value": "debug", + "scope": "project", "scope_id": projectID, "created_at": baseTime, "updated_at": baseTime, + }, + }}, + {Table: "secrets", Rows: []row{ + { // long encrypted_value exercises large TEXT + "id": "5ec00000-0000-0000-0000-000000000001", "key": "API_KEY", + "encrypted_value": maxLenString, "scope": "project", "scope_id": projectID, + "secret_type": "environment", "created_at": baseTime, "updated_at": baseTime, + }, + }}, + {Table: "templates", Rows: []row{ + { + "id": "7e000000-0000-0000-0000-000000000001", "name": "claude", "slug": "claude", + "harness": "claude", "image": "scion/claude:latest", "config": nestedConfigJSON, + "scope": "global", "status": "active", "visibility": "public", + "created_at": baseTime, "updated_at": baseTime, + }, + }}, + {Table: "harness_configs", Rows: []row{ + { + "id": "4a000000-0000-0000-0000-000000000001", "name": "claude-web", "slug": "claude-web", + "harness": "claude", "config": nestedConfigJSON, "scope": "global", + "status": "active", "visibility": "public", "created_at": baseTime, "updated_at": baseTime, + }, + }}, + + // ---- Brokers / project wiring ---- + {Table: "project_contributors", Rows: []row{ + { + "project_id": projectID, "broker_id": brokerID, "broker_name": "broker-1", + "mode": "connected", "status": "online", "last_seen": baseTime, + }, + }}, + {Table: "project_sync_state", Rows: []row{ + { + "project_id": projectID, "broker_id": brokerID, "last_sync_time": baseTime, + "last_commit_sha": "deadbeefcafe", "file_count": 42, "total_bytes": 123456, + }, + }}, + {Table: "broker_secrets", Rows: []row{ + { // BLOB column + "broker_id": brokerID, "secret_key": []byte{0x01, 0x02, 0x03, 0x04, 0xfe, 0xff}, + "algorithm": "hmac-sha256", "created_at": baseTime, "status": "active", + }, + }}, + {Table: "broker_join_tokens", Rows: []row{ + { + "broker_id": brokerID, "token_hash": "abc123hash", "expires_at": baseTime.Add(time.Hour), + "created_at": baseTime, "created_by": userID, + }, + }}, + + // ---- Notifications / messaging ---- + {Table: "notification_subscriptions", Rows: []row{ + { + "id": subID, "scope": "agent", "agent_id": agentID, "subscriber_type": "user", + "subscriber_id": userID, "project_id": projectID, + "trigger_activities": `["COMPLETED","WAITING_FOR_INPUT"]`, + "created_at": baseTime, "created_by": userID, + }, + }}, + {Table: "notifications", Rows: []row{ + { + "id": "0t000000-0000-0000-0000-000000000001", "subscription_id": subID, + "agent_id": agentID, "project_id": projectID, "subscriber_type": "user", + "subscriber_id": userID, "status": "COMPLETED", "message": "agent completed 🎉", + "created_at": baseTime, + }, + }}, + {Table: "subscription_templates", Rows: []row{ + { + "id": "57000000-0000-0000-0000-000000000001", "name": "All Events", + "scope": "project", "trigger_activities": `["COMPLETED","ERROR"]`, + "project_id": projectID, "created_by": userID, + }, + }}, + {Table: "messages", Rows: []row{ + { + "id": "11500000-0000-0000-0000-000000000001", "project_id": projectID, + "sender": "user:alice", "sender_id": userID, "recipient": "agent:worker", + "recipient_id": agentID, "msg": "do the thing — café ☕", "type": "instruction", + "agent_id": agentID, "created_at": baseTime, + }, + }}, + + // ---- Schedules ---- + {Table: "schedules", Rows: []row{ + { + "id": "5c000000-0000-0000-0000-000000000001", "project_id": projectID, + "name": "nightly", "cron_expr": "0 0 * * *", "event_type": "dispatch_agent", + "payload": nestedConfigJSON, "status": "active", "next_run_at": baseTime.Add(24 * time.Hour), + "created_at": baseTime, "updated_at": baseTime, + }, + }}, + {Table: "scheduled_events", Rows: []row{ + { + "id": "5e000000-0000-0000-0000-000000000001", "project_id": projectID, + "event_type": "dispatch_agent", "fire_at": baseTime.Add(time.Hour), + "payload": `{"task":"run"}`, "status": "pending", "created_at": baseTime, + }, + }}, + + // ---- Access control: allow list / invites / tokens ---- + {Table: "allow_list", Rows: []row{ + { + "id": "a1000000-0000-0000-0000-000000000001", "email": "invited@example.com", + "note": "early access", "added_by": userID, "created": baseTime, + }, + }}, + {Table: "invite_codes", Rows: []row{ + { + "id": "1c000000-0000-0000-0000-000000000001", "code_hash": "hash_of_code", + "code_prefix": "scion_in", "max_uses": 5, "use_count": 1, + "expires_at": baseTime.Add(48 * time.Hour), "created_by": userID, "created": baseTime, + }, + }}, + {Table: "user_access_tokens", Rows: []row{ + { + "id": "0a000000-0000-0000-0000-000000000001", "user_id": userID, "name": "ci-token", + "prefix": "scion_pat_ab", "key_hash": "tokenhash", "project_id": projectID, + "scopes": `["agent:read","agent:list"]`, "created_at": baseTime, + }, + }}, + {Table: "api_keys", Rows: []row{ + { // NULL expires_at / last_used optionals + "id": "a9000000-0000-0000-0000-000000000001", "user_id": userID, "name": "legacy-key", + "prefix": "scion_ak", "key_hash": "apikeyhash", "scopes": `["read"]`, "created_at": baseTime, + }, + }}, + + // ---- GCP / GitHub identity ---- + {Table: "gcp_service_accounts", Rows: []row{ + { + "id": "9c000000-0000-0000-0000-000000000001", "scope": "project", "scope_id": projectID, + "email": "agent-worker@example.iam.gserviceaccount.com", "project_id": "gcp-proj-123", + "display_name": "Worker SA", "default_scopes": `["https://www.googleapis.com/auth/cloud-platform"]`, + "created_by": userID, "created_at": baseTime, "managed": true, + }, + }}, + {Table: "github_installations", Rows: []row{ + { + "installation_id": int64(987654), "account_login": "example-org", + "account_type": "Organization", "app_id": int64(112233), + "repositories": `["example/platform","example/infra"]`, "status": "active", + "created_at": baseTime, "updated_at": baseTime, + }, + }}, + + // ---- Maintenance ---- + {Table: "maintenance_operations", Rows: []row{ + { + "id": "0d000000-0000-0000-0000-000000000001", "key": "purge_deleted_agents", + "title": "Purge Deleted Agents", "description": "remove soft-deleted agents", + "category": "cleanup", "status": "pending", "created_at": baseTime, + "metadata": `{"batchSize":100}`, + }, + }}, + {Table: "maintenance_operation_runs", Rows: []row{ + { + "id": "07000000-0000-0000-0000-000000000001", "operation_key": "purge_deleted_agents", + "status": "completed", "started_at": baseTime, "completed_at": baseTime.Add(time.Minute), + "started_by": userID, "result": `{"purged":3}`, "log": "done", + }, + }}, + } +} diff --git a/pkg/agent/common_test.go b/pkg/agent/common_test.go index 85c19f071..e43a06659 100644 --- a/pkg/agent/common_test.go +++ b/pkg/agent/common_test.go @@ -26,8 +26,6 @@ func getTestHarnesses() []api.Harness { return []api.Harness{ &harness.GeminiCLI{}, &harness.ClaudeCode{}, - &harness.OpenCode{}, - &harness.Codex{}, } } diff --git a/pkg/agent/opencode_provision_test.go b/pkg/agent/opencode_provision_test.go deleted file mode 100644 index 19866f71b..000000000 --- a/pkg/agent/opencode_provision_test.go +++ /dev/null @@ -1,88 +0,0 @@ -// Copyright 2026 Google LLC -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package agent - -import ( - "context" - "os" - "path/filepath" - "testing" - - "github.com/GoogleCloudPlatform/scion/pkg/config" -) - -func TestProvisionOpencodeAgent(t *testing.T) { - mockRuntimeForTest(t) - tmpDir := t.TempDir() - - // Move to tmpDir - oldWd, _ := os.Getwd() - os.Chdir(tmpDir) - defer os.Chdir(oldWd) - - // Mock HOME - originalHome := os.Getenv("HOME") - defer os.Setenv("HOME", originalHome) - os.Setenv("HOME", tmpDir) - - // Seed global harness-configs (required for agent creation) - if err := config.InitMachine(getTestHarnesses()); err != nil { - t.Fatalf("InitMachine failed: %v", err) - } - - // Initialize a mock project - projectDir := filepath.Join(tmpDir, "project") - projectScionDir := filepath.Join(projectDir, ".scion") - if err := config.InitProject(projectScionDir, getTestHarnesses()); err != nil { - t.Fatalf("InitProject failed: %v", err) - } - - // Chdir to projectDir so GetProjectDir finds it - if err := os.Chdir(projectDir); err != nil { - t.Fatal(err) - } - - // Create dummy auth file - authDir := filepath.Join(tmpDir, ".local", "share", "opencode") - if err := os.MkdirAll(authDir, 0755); err != nil { - t.Fatal(err) - } - authFile := filepath.Join(authDir, "auth.json") - if err := os.WriteFile(authFile, []byte("{}"), 0644); err != nil { - t.Fatal(err) - } - - // Provision an opencode agent using the "default" agnostic template with --harness-config=opencode - agentName := "opencode-agent" - agentHome, _, _, err := ProvisionAgent(context.Background(), agentName, "default", "", "opencode", projectScionDir, "", "", "", "") - if err != nil { - t.Fatalf("ProvisionAgent failed: %v", err) - } - - // Verify agent's opencode.json (from harness-config home) - agentOpencodeJSONPath := filepath.Join(agentHome, ".config", "opencode", "opencode.json") - if _, err := os.Stat(agentOpencodeJSONPath); os.IsNotExist(err) { - t.Fatalf("expected opencode.json to exist at %s", agentOpencodeJSONPath) - } - - // Verify it has content - data, err := os.ReadFile(agentOpencodeJSONPath) - if err != nil { - t.Fatal(err) - } - if len(data) == 0 { - t.Error("expected opencode.json to have content, but it's empty") - } -} diff --git a/pkg/agent/state/state.go b/pkg/agent/state/state.go index c3e8057e3..e1b3d2017 100644 --- a/pkg/agent/state/state.go +++ b/pkg/agent/state/state.go @@ -56,6 +56,35 @@ func Phases() []Phase { return out } +// Ordinal returns the forward-progress ordering of a phase. +// Higher values represent later lifecycle stages. +// Returns 0 for terminal or special phases (stopped, error, suspended, stopping) +// where regression checks do not apply. +func (p Phase) Ordinal() int { + switch p { + case PhaseCreated: + return 1 + case PhaseProvisioning: + return 2 + case PhaseCloning: + return 3 + case PhaseStarting: + return 4 + case PhaseRunning: + return 5 + default: + return 0 + } +} + +// IsActivePhase reports whether this phase is part of the forward-progress +// lifecycle (created through running). Regression guards apply only between +// active phases — terminal phases (stopped, error) and special phases +// (suspended, stopping) are excluded. +func (p Phase) IsActivePhase() bool { + return p.Ordinal() > 0 +} + // String implements fmt.Stringer. func (p Phase) String() string { return string(p) } @@ -162,6 +191,18 @@ func (a Activity) IsTerminal() bool { return false } +// ImpliesRunning reports whether this activity implies the agent must be in +// PhaseRunning. Used for auto-correcting a stale pre-running phase when the +// agent is clearly active. +func (a Activity) ImpliesRunning() bool { + switch a { + case ActivityWorking, ActivityThinking, ActivityExecuting, + ActivityWaitingForInput, ActivityBlocked, ActivityCompleted: + return true + } + return false +} + // IsPlatformSet reports whether this activity is set by the platform (scheduler) // rather than by the agent itself. func (a Activity) IsPlatformSet() bool { diff --git a/pkg/agent/state/state_test.go b/pkg/agent/state/state_test.go index 30b6fb00f..4fa581b57 100644 --- a/pkg/agent/state/state_test.go +++ b/pkg/agent/state/state_test.go @@ -481,6 +481,70 @@ func TestPhasesEnumeration(t *testing.T) { } } +func TestPhaseOrdinal(t *testing.T) { + tests := []struct { + phase Phase + ordinal int + }{ + {PhaseCreated, 1}, + {PhaseProvisioning, 2}, + {PhaseCloning, 3}, + {PhaseStarting, 4}, + {PhaseRunning, 5}, + {PhaseSuspended, 0}, + {PhaseStopping, 0}, + {PhaseStopped, 0}, + {PhaseError, 0}, + } + for _, tt := range tests { + if got := tt.phase.Ordinal(); got != tt.ordinal { + t.Errorf("Phase(%q).Ordinal() = %d, want %d", tt.phase, got, tt.ordinal) + } + } + + // Verify strict ordering for forward-progress phases. + forward := []Phase{PhaseCreated, PhaseProvisioning, PhaseCloning, PhaseStarting, PhaseRunning} + for i := 1; i < len(forward); i++ { + if forward[i].Ordinal() <= forward[i-1].Ordinal() { + t.Errorf("Ordinal(%q)=%d should be > Ordinal(%q)=%d", + forward[i], forward[i].Ordinal(), forward[i-1], forward[i-1].Ordinal()) + } + } +} + +func TestPhaseIsActivePhase(t *testing.T) { + active := []Phase{PhaseCreated, PhaseProvisioning, PhaseCloning, PhaseStarting, PhaseRunning} + for _, p := range active { + if !p.IsActivePhase() { + t.Errorf("Phase(%q).IsActivePhase() = false, want true", p) + } + } + + notActive := []Phase{PhaseSuspended, PhaseStopping, PhaseStopped, PhaseError} + for _, p := range notActive { + if p.IsActivePhase() { + t.Errorf("Phase(%q).IsActivePhase() = true, want false", p) + } + } +} + +func TestActivityImpliesRunning(t *testing.T) { + implies := []Activity{ActivityWorking, ActivityThinking, ActivityExecuting, + ActivityWaitingForInput, ActivityBlocked, ActivityCompleted} + for _, a := range implies { + if !a.ImpliesRunning() { + t.Errorf("Activity(%q).ImpliesRunning() = false, want true", a) + } + } + + doesNotImply := []Activity{ActivityLimitsExceeded, ActivityStalled, ActivityOffline, ActivityCrashed} + for _, a := range doesNotImply { + if a.ImpliesRunning() { + t.Errorf("Activity(%q).ImpliesRunning() = true, want false", a) + } + } +} + func TestActivitiesEnumeration(t *testing.T) { activities := Activities() if len(activities) != 10 { diff --git a/pkg/api/agent_actions.go b/pkg/api/agent_actions.go index 76dfdef18..d8b705dad 100644 --- a/pkg/api/agent_actions.go +++ b/pkg/api/agent_actions.go @@ -37,6 +37,7 @@ const ( AgentActionStats = "stats" AgentActionHasPrompt = "has-prompt" AgentActionFinalizeEnv = "finalize-env" + AgentActionResetAuth = "reset-auth" ) // RuntimeBrokerAgentActionMethod returns the HTTP method for actions routed @@ -46,7 +47,7 @@ func RuntimeBrokerAgentActionMethod(action string) (string, bool) { switch action { case AgentActionLogs, AgentActionStats, AgentActionHasPrompt: return http.MethodGet, true - case AgentActionStart, AgentActionStop, AgentActionSuspend, AgentActionRestart, AgentActionMessage, AgentActionExec, AgentActionFinalizeEnv: + case AgentActionStart, AgentActionStop, AgentActionSuspend, AgentActionRestart, AgentActionMessage, AgentActionExec, AgentActionFinalizeEnv, AgentActionResetAuth: return http.MethodPost, true default: return "", false diff --git a/pkg/api/types.go b/pkg/api/types.go index 074a527fc..47c1ddf31 100644 --- a/pkg/api/types.go +++ b/pkg/api/types.go @@ -249,10 +249,18 @@ type VolumeMount struct { Source string `json:"source" yaml:"source"` Target string `json:"target" yaml:"target"` ReadOnly bool `json:"read_only,omitempty" yaml:"read_only,omitempty"` - Type string `json:"type,omitempty" yaml:"type,omitempty"` // "local" (default) or "gcs" - Bucket string `json:"bucket,omitempty" yaml:"bucket,omitempty"` // For GCS - Prefix string `json:"prefix,omitempty" yaml:"prefix,omitempty"` // For GCS - Mode string `json:"mode,omitempty" yaml:"mode,omitempty"` // Mount options + // Type discriminates the volume kind: + // "local" (default) — host bind mount; requires Source. + // "gcs" — GCS FUSE mount; requires Bucket. + // "nfs" — literal NFS protocol mount; requires Server, Source. + // "cloudrun-volume" — Cloud Run managed volume; requires VolumeName. + // "gke-shared-volume" — GKE-provided shared volume (e.g. Filestore CSI PVC); requires VolumeName. + Type string `json:"type,omitempty" yaml:"type,omitempty"` + Bucket string `json:"bucket,omitempty" yaml:"bucket,omitempty"` // GCS bucket name + Prefix string `json:"prefix,omitempty" yaml:"prefix,omitempty"` // GCS object prefix + Mode string `json:"mode,omitempty" yaml:"mode,omitempty"` // Mount options + Server string `json:"server,omitempty" yaml:"server,omitempty"` // NFS: server host/IP + VolumeName string `json:"volume_name,omitempty" yaml:"volume_name,omitempty"` // Cloud Run / GKE volume name } // Validate checks that a VolumeMount has the required fields and valid values. @@ -271,8 +279,23 @@ func (v VolumeMount) Validate() error { if v.Bucket == "" { return fmt.Errorf("GCS volume mount for target %q missing required field: bucket", v.Target) } + case "nfs": + if v.Server == "" { + return fmt.Errorf("NFS volume mount for target %q missing required field: server", v.Target) + } + if v.Source == "" { + return fmt.Errorf("NFS volume mount for target %q missing required field: source (server export path)", v.Target) + } + case "cloudrun-volume": + if v.VolumeName == "" { + return fmt.Errorf("cloudrun-volume mount for target %q missing required field: volume_name", v.Target) + } + case "gke-shared-volume": + if v.VolumeName == "" { + return fmt.Errorf("gke-shared-volume mount for target %q missing required field: volume_name", v.Target) + } default: - return fmt.Errorf("volume mount for target %q has invalid type %q (must be \"local\" or \"gcs\")", v.Target, v.Type) + return fmt.Errorf("volume mount for target %q has invalid type %q (must be \"local\", \"gcs\", \"nfs\", \"cloudrun-volume\", or \"gke-shared-volume\")", v.Target, v.Type) } return nil diff --git a/pkg/api/types_test.go b/pkg/api/types_test.go index 3cf06bbe9..e19058c4b 100644 --- a/pkg/api/types_test.go +++ b/pkg/api/types_test.go @@ -173,12 +173,40 @@ func TestVolumeMountValidate(t *testing.T) { }, wantErr: "missing required field: source", }, + { + name: "valid nfs", + vol: VolumeMount{ + Source: "/scion-workspaces", + Target: "/workspace", + Type: "nfs", + Server: "10.0.0.2", + }, + wantErr: "", + }, + { + name: "nfs missing server", + vol: VolumeMount{ + Source: "/scion-workspaces", + Target: "/workspace", + Type: "nfs", + }, + wantErr: "missing required field: server", + }, + { + name: "nfs missing source", + vol: VolumeMount{ + Target: "/workspace", + Type: "nfs", + Server: "10.0.0.2", + }, + wantErr: "missing required field: source", + }, { name: "invalid type", vol: VolumeMount{ Source: "/host/path", Target: "/container/path", - Type: "nfs", + Type: "bogus", }, wantErr: "invalid type", }, @@ -190,6 +218,42 @@ func TestVolumeMountValidate(t *testing.T) { }, wantErr: "missing required field: bucket", }, + // cloudrun-volume tests + { + name: "valid cloudrun-volume", + vol: VolumeMount{ + Target: "/workspace", + Type: "cloudrun-volume", + VolumeName: "workspace-vol", + }, + wantErr: "", + }, + { + name: "cloudrun-volume missing volume_name", + vol: VolumeMount{ + Target: "/workspace", + Type: "cloudrun-volume", + }, + wantErr: "missing required field: volume_name", + }, + // gke-shared-volume tests + { + name: "valid gke-shared-volume", + vol: VolumeMount{ + Target: "/workspace", + Type: "gke-shared-volume", + VolumeName: "shared-ws", + }, + wantErr: "", + }, + { + name: "gke-shared-volume missing volume_name", + vol: VolumeMount{ + Target: "/workspace", + Type: "gke-shared-volume", + }, + wantErr: "missing required field: volume_name", + }, } for _, tt := range tests { diff --git a/pkg/config/harness_config.go b/pkg/config/harness_config.go index 3485d6bb2..5745df0b5 100644 --- a/pkg/config/harness_config.go +++ b/pkg/config/harness_config.go @@ -345,10 +345,19 @@ func ComputeHarnessConfigRevision(dirPath string) string { Hash string } var hashes []fileHash + skipBasenames := map[string]bool{ + "Dockerfile": true, + "cloudbuild.yaml": true, + "README.md": true, + ".gitkeep": true, + } walk := func(path string, d fs.DirEntry, walkErr error) error { if walkErr != nil || d.IsDir() { return nil } + if skipBasenames[d.Name()] { + return nil + } rel, relErr := filepath.Rel(dirPath, path) if relErr != nil { return nil diff --git a/pkg/config/harness_config_external_test.go b/pkg/config/harness_config_external_test.go deleted file mode 100644 index bcca3e0b1..000000000 --- a/pkg/config/harness_config_external_test.go +++ /dev/null @@ -1,119 +0,0 @@ -// Copyright 2026 Google LLC -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -// Package config_test holds tests that depend on pkg/harness. They live in -// an external test package so pkg/config production code can import what it -// needs from pkg/harness's shared types (via pkg/api) without creating an -// in-package import cycle with pkg/harness during testing. -package config_test - -import ( - "os" - "path/filepath" - "testing" - "time" - - "github.com/GoogleCloudPlatform/scion/pkg/config" - "github.com/GoogleCloudPlatform/scion/pkg/harness" -) - -func TestSeedHarnessConfig_CodexNotifyScript(t *testing.T) { - tmpDir := t.TempDir() - targetDir := filepath.Join(tmpDir, "codex") - - err := config.SeedHarnessConfig(targetDir, &harness.Codex{}, false) - if err != nil { - t.Fatalf("SeedHarnessConfig failed: %v", err) - } - - scriptPath := filepath.Join(targetDir, "home", ".codex", "scion_notify.sh") - if _, err := os.Stat(scriptPath); err != nil { - t.Fatalf("expected notify script to be seeded at %s: %v", scriptPath, err) - } -} - -func TestUpgradeHarnessConfig_AdditiveMergeAndBackup(t *testing.T) { - tmpDir := t.TempDir() - targetDir := filepath.Join(tmpDir, "codex") - if err := os.MkdirAll(targetDir, 0755); err != nil { - t.Fatal(err) - } - current := `harness: codex -image: custom-codex:latest -user: developer -env: - CUSTOM: "1" -` - if err := os.WriteFile(filepath.Join(targetDir, "config.yaml"), []byte(current), 0644); err != nil { - t.Fatal(err) - } - - plan, err := config.UpgradeHarnessConfig(targetDir, &harness.Codex{}, config.HarnessConfigUpgradeOptions{ - Now: func() time.Time { return time.Date(2026, 4, 25, 12, 0, 0, 0, time.UTC) }, - }) - if err != nil { - t.Fatalf("UpgradeHarnessConfig failed: %v", err) - } - if !plan.Changed { - t.Fatal("expected upgrade to report changes") - } - if len(plan.Backups) != 1 { - t.Fatalf("expected one backup, got %v", plan.Backups) - } - if _, err := os.Stat(plan.Backups[0]); err != nil { - t.Fatalf("expected backup file: %v", err) - } - - hc, err := config.LoadHarnessConfigDir(targetDir) - if err != nil { - t.Fatalf("LoadHarnessConfigDir failed after upgrade: %v", err) - } - if hc.Config.Image != "custom-codex:latest" || hc.Config.User != "developer" { - t.Fatalf("user-owned fields were not preserved: %#v", hc.Config) - } - if hc.Config.Provisioner == nil || hc.Config.Provisioner.Type != "container-script" { - t.Fatalf("expected additive metadata to include container-script provisioner, got %#v", hc.Config.Provisioner) - } - if hc.Config.Env["CUSTOM"] != "1" || hc.Config.Env["SCION_CODEX_NOTIFY_AUTO_COMPLETE"] != "true" { - t.Fatalf("expected env map to preserve custom values and add defaults, got %#v", hc.Config.Env) - } -} - -func TestUpgradeHarnessConfig_DryRunDoesNotWrite(t *testing.T) { - tmpDir := t.TempDir() - targetDir := filepath.Join(tmpDir, "opencode") - if err := os.MkdirAll(targetDir, 0755); err != nil { - t.Fatal(err) - } - current := []byte("harness: opencode\nimage: custom:latest\n") - configPath := filepath.Join(targetDir, "config.yaml") - if err := os.WriteFile(configPath, current, 0644); err != nil { - t.Fatal(err) - } - - plan, err := config.UpgradeHarnessConfig(targetDir, &harness.OpenCode{}, config.HarnessConfigUpgradeOptions{DryRun: true}) - if err != nil { - t.Fatalf("UpgradeHarnessConfig dry-run failed: %v", err) - } - if !plan.Changed { - t.Fatal("expected dry-run to report planned changes") - } - after, err := os.ReadFile(configPath) - if err != nil { - t.Fatal(err) - } - if string(after) != string(current) { - t.Fatalf("dry-run wrote config.yaml:\n%s", after) - } -} diff --git a/pkg/config/harness_config_test.go b/pkg/config/harness_config_test.go index cbc9cab23..fef66bbda 100644 --- a/pkg/config/harness_config_test.go +++ b/pkg/config/harness_config_test.go @@ -414,6 +414,63 @@ func TestSeedHarnessConfig_MockHarness(t *testing.T) { } } +func TestSeedHarnessConfig_AdditiveOnly(t *testing.T) { + tmpDir := t.TempDir() + hcBase := filepath.Join(tmpDir, "harness-configs") + + // Pre-create a legacy "opencode" harness-config dir with custom content. + opencodeDir := filepath.Join(hcBase, "opencode") + if err := os.MkdirAll(filepath.Join(opencodeDir, "home", ".config", "opencode"), 0755); err != nil { + t.Fatal(err) + } + customConfig := "harness: opencode\nimage: my-custom-opencode:v2\nuser: scion\nprovisioner:\n type: container-script\n interface_version: 1\n" + if err := os.WriteFile(filepath.Join(opencodeDir, "config.yaml"), []byte(customConfig), 0644); err != nil { + t.Fatal(err) + } + customSettings := `{"custom": true}` + settingsPath := filepath.Join(opencodeDir, "home", ".config", "opencode", "opencode.json") + if err := os.WriteFile(settingsPath, []byte(customSettings), 0644); err != nil { + t.Fatal(err) + } + customProvision := "#!/usr/bin/env python3\n# custom provisioner" + if err := os.WriteFile(filepath.Join(opencodeDir, "provision.py"), []byte(customProvision), 0644); err != nil { + t.Fatal(err) + } + + // Seed only the default set (claude, gemini) — simulates what InitMachine does. + for _, h := range GetMockHarnesses() { + if err := SeedHarnessConfig(filepath.Join(hcBase, h.Name()), h, false); err != nil { + t.Fatalf("SeedHarnessConfig(%s) failed: %v", h.Name(), err) + } + } + + // Verify the opencode directory and all its custom content survive. + if _, err := os.Stat(opencodeDir); err != nil { + t.Fatal("opencode harness-config dir should still exist after seeding defaults") + } + data, err := os.ReadFile(filepath.Join(opencodeDir, "config.yaml")) + if err != nil { + t.Fatal(err) + } + if string(data) != customConfig { + t.Errorf("opencode config.yaml was modified; got:\n%s", data) + } + data, err = os.ReadFile(settingsPath) + if err != nil { + t.Fatal(err) + } + if string(data) != customSettings { + t.Errorf("opencode opencode.json was modified; got: %s", data) + } + data, err = os.ReadFile(filepath.Join(opencodeDir, "provision.py")) + if err != nil { + t.Fatal(err) + } + if string(data) != customProvision { + t.Errorf("opencode provision.py was modified; got: %s", data) + } +} + func TestSeedHarnessConfigFromFS(t *testing.T) { tmpDir := t.TempDir() @@ -435,6 +492,43 @@ func TestSeedHarnessConfigFromFS(t *testing.T) { } } +func TestComputeHarnessConfigRevision_SkipsNonRuntimeFiles(t *testing.T) { + dir := t.TempDir() + + if err := os.WriteFile(filepath.Join(dir, "config.yaml"), []byte("harness: opencode\n"), 0644); err != nil { + t.Fatal(err) + } + if err := os.MkdirAll(filepath.Join(dir, "home", ".config"), 0755); err != nil { + t.Fatal(err) + } + if err := os.WriteFile(filepath.Join(dir, "home", ".config", "settings.json"), []byte("{}"), 0644); err != nil { + t.Fatal(err) + } + + baseRev := ComputeHarnessConfigRevision(dir) + if baseRev == "" { + t.Fatal("expected non-empty revision") + } + + for _, skip := range []string{"Dockerfile", "cloudbuild.yaml", "README.md", ".gitkeep"} { + if err := os.WriteFile(filepath.Join(dir, skip), []byte("should be ignored"), 0644); err != nil { + t.Fatal(err) + } + } + afterSkipped := ComputeHarnessConfigRevision(dir) + if afterSkipped != baseRev { + t.Errorf("adding non-runtime files changed revision: %s -> %s", baseRev, afterSkipped) + } + + if err := os.WriteFile(filepath.Join(dir, "config.yaml"), []byte("harness: opencode\nimage: new\n"), 0644); err != nil { + t.Fatal(err) + } + afterConfig := ComputeHarnessConfigRevision(dir) + if afterConfig == baseRev { + t.Error("changing config.yaml should change revision") + } +} + func TestMapEmbedFileToHarnessConfigPath_RootSupportFiles(t *testing.T) { targetDir := "/tmp/hc" homeDir := filepath.Join(targetDir, "home") diff --git a/pkg/config/harness_config_upgrade.go b/pkg/config/harness_config_upgrade.go index 11c07dcdb..38a72c13a 100644 --- a/pkg/config/harness_config_upgrade.go +++ b/pkg/config/harness_config_upgrade.go @@ -85,7 +85,10 @@ func UpgradeHarnessConfig(targetDir string, h api.Harness, opts HarnessConfigUpg embedsFS, basePath := h.GetHarnessEmbedsFS() if basePath == "" { - return plan, nil + // No embeds — this harness has no compiled-in defaults (e.g. opencode/codex + // after Phase D removal). Check for legacy-builtin configs that need + // auto-activation of container-script provisioning. + return upgradeLegacyBuiltinConfig(absTarget, plan, opts) } configDir := h.DefaultConfigDir() @@ -167,6 +170,75 @@ func UpgradeHarnessConfig(targetDir string, h api.Harness, opts HarnessConfigUpg return plan, nil } +// upgradeLegacyBuiltinConfig handles upgrade for harness configs whose compiled-in +// Go implementation has been removed (opencode, codex). If the on-disk config has +// provisioner.type "builtin" or missing and a provision.py exists, auto-activate +// container-script provisioning. If no provision.py exists, record a warning step +// telling the user to reinstall from the bundle. +func upgradeLegacyBuiltinConfig(absTarget string, plan *HarnessConfigUpgradePlan, opts HarnessConfigUpgradeOptions) (*HarnessConfigUpgradePlan, error) { + configPath := filepath.Join(absTarget, "config.yaml") + configData, err := os.ReadFile(configPath) + if err != nil { + return plan, nil + } + + var cfg map[string]interface{} + if err := yaml.Unmarshal(configData, &cfg); err != nil { + return plan, nil + } + if cfg == nil { + // Empty or comment-only config.yaml: nothing to upgrade. + return plan, nil + } + + harnessName, _ := cfg["harness"].(string) + if harnessName != "opencode" && harnessName != "codex" { + return plan, nil + } + + provisioner, _ := cfg["provisioner"].(map[string]interface{}) + provType := "" + if provisioner != nil { + provType, _ = provisioner["type"].(string) + } + if provType == "container-script" { + return plan, nil + } + + hasProvisionPy := fileExists(filepath.Join(absTarget, "provision.py")) + if !hasProvisionPy { + plan.Actions = append(plan.Actions, HarnessConfigUpgradeAction{ + Type: "warning", + Detail: "legacy built-in config has no provision.py; reinstall with: scion harness-config install harnesses/" + harnessName, + }) + return plan, nil + } + + updatedData, activated, err := activateContainerScriptProvisioner(configData) + if err != nil { + return plan, err + } + if activated { + plan.Changed = true + plan.Actions = append(plan.Actions, HarnessConfigUpgradeAction{ + Type: "activate_script", + Path: "config.yaml", + Detail: "auto-activated container-script (built-in removed)", + }) + if !opts.DryRun { + backupPath, err := backupFile(configPath, opts.Now()) + if err != nil { + return plan, err + } + plan.Backups = append(plan.Backups, backupPath) + if err := os.WriteFile(configPath, updatedData, 0644); err != nil { + return plan, fmt.Errorf("write upgraded config.yaml: %w", err) + } + } + } + return plan, nil +} + func mergeHarnessConfigYAML(currentData, defaultData []byte) ([]byte, bool, []string, error) { var current map[string]interface{} if err := yaml.Unmarshal(currentData, ¤t); err != nil { diff --git a/pkg/config/harness_config_upgrade_test.go b/pkg/config/harness_config_upgrade_test.go new file mode 100644 index 000000000..eb50fc771 --- /dev/null +++ b/pkg/config/harness_config_upgrade_test.go @@ -0,0 +1,252 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package config + +import ( + "os" + "path/filepath" + "testing" + "time" + + "gopkg.in/yaml.v3" +) + +func fixedTime() time.Time { + return time.Date(2026, 6, 6, 0, 0, 0, 0, time.UTC) +} + +func TestUpgradeHarnessConfig_LegacyBuiltinAutoActivate(t *testing.T) { + tmpDir := t.TempDir() + hcDir := filepath.Join(tmpDir, "opencode") + if err := os.MkdirAll(hcDir, 0755); err != nil { + t.Fatal(err) + } + + // Legacy opencode config with provisioner.type: builtin. + configYAML := `harness: opencode +image: scion-opencode:latest +user: scion +provisioner: + type: builtin + interface_version: 1 +` + if err := os.WriteFile(filepath.Join(hcDir, "config.yaml"), []byte(configYAML), 0644); err != nil { + t.Fatal(err) + } + if err := os.WriteFile(filepath.Join(hcDir, "provision.py"), []byte("#!/usr/bin/env python3\n"), 0644); err != nil { + t.Fatal(err) + } + + // Generic harness (what harness.New("opencode") returns after Phase D). + h := &MockHarness{NameVal: "generic"} + plan, err := UpgradeHarnessConfig(hcDir, h, HarnessConfigUpgradeOptions{ + Now: func() time.Time { return fixedTime() }, + }) + if err != nil { + t.Fatalf("UpgradeHarnessConfig failed: %v", err) + } + if !plan.Changed { + t.Fatal("expected plan to report changes") + } + + foundActivate := false + for _, action := range plan.Actions { + if action.Type == "activate_script" { + foundActivate = true + if action.Detail != "auto-activated container-script (built-in removed)" { + t.Errorf("unexpected detail: %s", action.Detail) + } + } + } + if !foundActivate { + t.Error("expected activate_script action in upgrade plan") + } + + // Verify config.yaml was actually updated on disk. + data, err := os.ReadFile(filepath.Join(hcDir, "config.yaml")) + if err != nil { + t.Fatal(err) + } + var cfg map[string]interface{} + if err := yaml.Unmarshal(data, &cfg); err != nil { + t.Fatal(err) + } + prov, _ := cfg["provisioner"].(map[string]interface{}) + if prov == nil || prov["type"] != "container-script" { + t.Errorf("expected provisioner.type=container-script, got %v", prov) + } +} + +func TestUpgradeHarnessConfig_LegacyBuiltinNoProvisionPy(t *testing.T) { + tmpDir := t.TempDir() + hcDir := filepath.Join(tmpDir, "codex") + if err := os.MkdirAll(hcDir, 0755); err != nil { + t.Fatal(err) + } + + configYAML := `harness: codex +image: scion-codex:latest +user: scion +provisioner: + type: builtin + interface_version: 1 +` + if err := os.WriteFile(filepath.Join(hcDir, "config.yaml"), []byte(configYAML), 0644); err != nil { + t.Fatal(err) + } + // No provision.py — should get a warning action, not auto-activation. + + h := &MockHarness{NameVal: "generic"} + plan, err := UpgradeHarnessConfig(hcDir, h, HarnessConfigUpgradeOptions{ + Now: func() time.Time { return fixedTime() }, + }) + if err != nil { + t.Fatalf("UpgradeHarnessConfig failed: %v", err) + } + + foundWarning := false + for _, action := range plan.Actions { + if action.Type == "warning" { + foundWarning = true + } + } + if !foundWarning { + t.Error("expected warning action when provision.py is missing") + } + if plan.Changed { + t.Error("config should not be changed when no provision.py exists") + } +} + +func TestUpgradeHarnessConfig_LegacyBuiltinMissingProvisioner(t *testing.T) { + tmpDir := t.TempDir() + hcDir := filepath.Join(tmpDir, "opencode") + if err := os.MkdirAll(hcDir, 0755); err != nil { + t.Fatal(err) + } + + // Config with no provisioner field at all. + configYAML := `harness: opencode +image: scion-opencode:latest +user: scion +` + if err := os.WriteFile(filepath.Join(hcDir, "config.yaml"), []byte(configYAML), 0644); err != nil { + t.Fatal(err) + } + if err := os.WriteFile(filepath.Join(hcDir, "provision.py"), []byte("#!/usr/bin/env python3\n"), 0644); err != nil { + t.Fatal(err) + } + + h := &MockHarness{NameVal: "generic"} + plan, err := UpgradeHarnessConfig(hcDir, h, HarnessConfigUpgradeOptions{ + Now: func() time.Time { return fixedTime() }, + }) + if err != nil { + t.Fatalf("UpgradeHarnessConfig failed: %v", err) + } + if !plan.Changed { + t.Fatal("expected plan to report changes for missing provisioner") + } + + foundActivate := false + for _, action := range plan.Actions { + if action.Type == "activate_script" { + foundActivate = true + } + } + if !foundActivate { + t.Error("expected activate_script action for config with missing provisioner") + } +} + +func TestUpgradeHarnessConfig_ContainerScriptUnchanged(t *testing.T) { + tmpDir := t.TempDir() + hcDir := filepath.Join(tmpDir, "opencode") + if err := os.MkdirAll(hcDir, 0755); err != nil { + t.Fatal(err) + } + + // Already on container-script — should be a no-op. + configYAML := `harness: opencode +image: scion-opencode:latest +user: scion +provisioner: + type: container-script + interface_version: 1 +` + if err := os.WriteFile(filepath.Join(hcDir, "config.yaml"), []byte(configYAML), 0644); err != nil { + t.Fatal(err) + } + if err := os.WriteFile(filepath.Join(hcDir, "provision.py"), []byte("#!/usr/bin/env python3\n"), 0644); err != nil { + t.Fatal(err) + } + + h := &MockHarness{NameVal: "generic"} + plan, err := UpgradeHarnessConfig(hcDir, h, HarnessConfigUpgradeOptions{ + Now: func() time.Time { return fixedTime() }, + }) + if err != nil { + t.Fatalf("UpgradeHarnessConfig failed: %v", err) + } + if plan.Changed { + t.Error("container-script config should not be changed") + } + if len(plan.Actions) != 0 { + t.Errorf("expected no actions, got %d", len(plan.Actions)) + } +} + +func TestUpgradeHarnessConfig_DryRunNoFileChanges(t *testing.T) { + tmpDir := t.TempDir() + hcDir := filepath.Join(tmpDir, "opencode") + if err := os.MkdirAll(hcDir, 0755); err != nil { + t.Fatal(err) + } + + originalConfig := `harness: opencode +image: scion-opencode:latest +user: scion +provisioner: + type: builtin + interface_version: 1 +` + if err := os.WriteFile(filepath.Join(hcDir, "config.yaml"), []byte(originalConfig), 0644); err != nil { + t.Fatal(err) + } + if err := os.WriteFile(filepath.Join(hcDir, "provision.py"), []byte("#!/usr/bin/env python3\n"), 0644); err != nil { + t.Fatal(err) + } + + h := &MockHarness{NameVal: "generic"} + plan, err := UpgradeHarnessConfig(hcDir, h, HarnessConfigUpgradeOptions{ + DryRun: true, + Now: func() time.Time { return fixedTime() }, + }) + if err != nil { + t.Fatalf("UpgradeHarnessConfig failed: %v", err) + } + if !plan.Changed { + t.Fatal("dry-run should still report changes") + } + + // Verify config.yaml was NOT modified on disk. + data, err := os.ReadFile(filepath.Join(hcDir, "config.yaml")) + if err != nil { + t.Fatal(err) + } + if string(data) != originalConfig { + t.Error("dry-run should not modify config.yaml on disk") + } +} diff --git a/pkg/config/hub_config.go b/pkg/config/hub_config.go index 2d284bacb..b11aabdef 100644 --- a/pkg/config/hub_config.go +++ b/pkg/config/hub_config.go @@ -137,10 +137,56 @@ type RuntimeBrokerConfig struct { type DatabaseConfig struct { Driver string `json:"driver" yaml:"driver" koanf:"driver"` // sqlite, postgres URL string `json:"url" yaml:"url" koanf:"url"` // Connection URL/path + + // Connection pool settings (applied to the underlying *sql.DB). + // MaxOpenConns is the maximum number of open connections to the database. + // For sqlite this MUST be 1 to serialize writes (load-bearing). + MaxOpenConns int `json:"max_open_conns" yaml:"max_open_conns" koanf:"max_open_conns"` + // MaxIdleConns is the maximum number of idle connections in the pool. + MaxIdleConns int `json:"max_idle_conns" yaml:"max_idle_conns" koanf:"max_idle_conns"` + // ConnMaxLifetime is the maximum amount of time a connection may be + // reused, parsed as a Go duration string (e.g. "30m"). Empty means unlimited. + ConnMaxLifetime string `json:"conn_max_lifetime" yaml:"conn_max_lifetime" koanf:"conn_max_lifetime"` + // ConnMaxIdleTime is the maximum amount of time a connection may sit idle + // in the pool before being closed, parsed as a Go duration string (e.g. + // "5m"). This must be shorter than the server-side / proxy idle timeout + // (CloudSQL drops idle connections after ~10m) so the pool recycles a + // connection before the remote silently closes it — otherwise the first + // request after an idle period stalls waiting for a dead connection to time + // out. Empty means no idle limit. + ConnMaxIdleTime string `json:"conn_max_idle_time" yaml:"conn_max_idle_time" koanf:"conn_max_idle_time"` +} + +// ConnMaxLifetimeDuration parses ConnMaxLifetime into a time.Duration. +// An empty value yields 0 (unlimited). A malformed value returns an error. +func (d DatabaseConfig) ConnMaxLifetimeDuration() (time.Duration, error) { + if d.ConnMaxLifetime == "" { + return 0, nil + } + dur, err := time.ParseDuration(d.ConnMaxLifetime) + if err != nil { + return 0, fmt.Errorf("invalid conn_max_lifetime %q: %w", d.ConnMaxLifetime, err) + } + return dur, nil +} + +// ConnMaxIdleTimeDuration parses ConnMaxIdleTime into a time.Duration. +// An empty value yields 0 (no idle limit). A malformed value returns an error. +func (d DatabaseConfig) ConnMaxIdleTimeDuration() (time.Duration, error) { + if d.ConnMaxIdleTime == "" { + return 0, nil + } + dur, err := time.ParseDuration(d.ConnMaxIdleTime) + if err != nil { + return 0, fmt.Errorf("invalid conn_max_idle_time %q: %w", d.ConnMaxIdleTime, err) + } + return dur, nil } -// DevAuthConfig holds development authentication settings. +// DevAuthConfig holds authentication settings. type DevAuthConfig struct { + // Mode selects the exclusive human auth mode: "oauth" (default), "proxy", or "dev". + Mode string `json:"mode,omitempty" yaml:"mode,omitempty" koanf:"mode"` // Enabled indicates whether development authentication is enabled. // WARNING: Not for production use. Enabled bool `json:"devMode" yaml:"devMode" koanf:"devMode"` @@ -155,6 +201,47 @@ type DevAuthConfig struct { // UserAccessMode controls how user access is evaluated at login time. // Values: "open" (default), "domain_restricted", "invite_only". UserAccessMode string `json:"userAccessMode" yaml:"userAccessMode" koanf:"userAccessMode"` + // Proxy holds proxy authentication settings (consulted when Mode == "proxy"). + Proxy *ProxyAuthConfig `json:"proxy,omitempty" yaml:"proxy,omitempty" koanf:"proxy"` + // Transport holds transport-layer auth settings for agent outbound requests. + // Controls which transport tokens the hub issues to agents (dispatch + refresh). + Transport *TransportAuthConfig `json:"transport,omitempty" yaml:"transport,omitempty" koanf:"transport"` +} + +// TransportAuthConfig holds transport-layer (outer/platform) auth settings. +// This controls how agents authenticate to the platform guard (IAP or Cloud Run invoker) +// when making outbound requests to the hub. +type TransportAuthConfig struct { + // Mode selects the transport auth mode: "none" (default), "cloudrun_invoker", or "iap". + Mode string `json:"mode" yaml:"mode" koanf:"mode"` + // OIDCAudience is the OIDC audience for the transport token. + // For IAP: the IAP OAuth client ID. For cloudrun_invoker: the hub URL. + // Empty means derive from hub endpoint (cloudrun_invoker only). + OIDCAudience string `json:"oidcAudience" yaml:"oidcAudience" koanf:"oidcAudience"` + // PlatformAuthSA is the email of the dedicated service account used for + // transport-layer auth. The hub's runtime SA must hold serviceAccountTokenCreator + // on this SA to impersonate it via the IAM Credentials API. + PlatformAuthSA string `json:"platformAuthSA" yaml:"platformAuthSA" koanf:"platformAuthSA"` +} + +// ProxyAuthConfig holds proxy authentication settings. +type ProxyAuthConfig struct { + // Provider selects the proxy auth provider: "iap" or "header". + Provider string `json:"provider" yaml:"provider" koanf:"provider"` + // IAP holds Google IAP-specific settings. + IAP *IAPAuthConfig `json:"iap,omitempty" yaml:"iap,omitempty" koanf:"iap"` + // RequireTrustedProxyIP enables defense-in-depth IP allowlisting. + RequireTrustedProxyIP bool `json:"requireTrustedProxyIP,omitempty" yaml:"requireTrustedProxyIP,omitempty" koanf:"requireTrustedProxyIP"` +} + +// IAPAuthConfig holds Google IAP-specific settings. +type IAPAuthConfig struct { + // Audience is the expected audience claim — MANDATORY for IAP. + Audience string `json:"audience" yaml:"audience" koanf:"audience"` + // Issuer overrides the default IAP issuer (for testing). + Issuer string `json:"issuer,omitempty" yaml:"issuer,omitempty" koanf:"issuer"` + // JWKSURL overrides the default IAP JWKS URL (for testing). + JWKSURL string `json:"jwksURL,omitempty" yaml:"jwksURL,omitempty" koanf:"jwksURL"` } // OAuthProviderConfig holds OAuth credentials for a single provider. @@ -291,6 +378,13 @@ func DefaultGlobalConfig() GlobalConfig { Database: DatabaseConfig{ Driver: "sqlite", URL: "", // Will be set to default path if empty + // SQLite pool defaults. MaxOpenConns MUST stay 1 to serialize + // writes; postgres pool defaults are applied in + // applyDatabasePoolDefaults when Driver == "postgres". + MaxOpenConns: 1, + MaxIdleConns: 1, + ConnMaxLifetime: "0", + ConnMaxIdleTime: "0", }, Auth: DevAuthConfig{ Enabled: false, @@ -308,6 +402,59 @@ func DefaultGlobalConfig() GlobalConfig { } } +// applyDatabasePoolDefaults fills in driver-appropriate connection pool +// defaults for any pool field left unset. It is applied after config loading +// so that postgres deployments get sensible pool sizing without requiring +// every config file to specify it. +// +// For sqlite, MaxOpenConns is forced to 1: more than one open connection +// breaks write serialization and causes "database is locked" errors. +func applyDatabasePoolDefaults(db *DatabaseConfig) { + switch db.Driver { + case "postgres": + // NOTE: the struct-level default for these fields is 1 (the value SQLite + // REQUIRES to serialize writes — see DefaultGlobalConfig). For a postgres + // deployment configured purely via env/driver override, that 1 leaks + // through unchanged, and a plain `<= 0` guard would leave the pool at a + // single connection. A pool of 1 is pathological for postgres: a + // singleton scheduler handler that checks out the lone connection to hold + // an advisory lock then self-deadlocks waiting for a second connection to + // do its work, and every API request serializes behind it (~55s context + // deadlines). Treat the leaked SQLite default (<= 1) as "unset" so + // postgres always gets a real pool. An operator who genuinely wants a + // tiny pool can still request 2+. + if db.MaxOpenConns <= 1 { + // Conservative per-replica default so several replicas fit within a + // modest Postgres connection budget. The connection ceiling for N + // replicas is roughly N × (MaxOpenConns + event pool + 1 listener + + // brokers); see CONNECTION-BUDGET.md. Raise this only when the + // instance's max_connections (and any pooler) has headroom. + db.MaxOpenConns = 10 + } + if db.MaxIdleConns <= 1 { + db.MaxIdleConns = 5 + } + if db.ConnMaxLifetime == "" { + db.ConnMaxLifetime = "30m" + } + if db.ConnMaxIdleTime == "" { + // Shorter than CloudSQL's ~10m idle timeout so the pool recycles a + // connection before the remote silently drops it. + db.ConnMaxIdleTime = "5m" + } + case "sqlite": + // Load-bearing: SQLite must use a single open connection. + db.MaxOpenConns = 1 + if db.MaxIdleConns <= 0 { + db.MaxIdleConns = 1 + } + // No idle recycling for the single local SQLite connection. + if db.ConnMaxIdleTime == "" { + db.ConnMaxIdleTime = "0" + } + } +} + // LoadGlobalConfig loads global configuration using Koanf with priority: // 1. Embedded defaults // 2. Global config: settings.yaml (server key) OR server.yaml (~/.scion/) @@ -381,6 +528,7 @@ func loadGlobalConfigFromSettings(configPath string) (*GlobalConfig, bool) { if gc.Database.URL == "" && gc.Database.Driver == "sqlite" { gc.Database.URL = filepath.Join(globalDir, "hub.db") } + applyDatabasePoolDefaults(&gc.Database) return gc, true } @@ -511,6 +659,7 @@ func loadGlobalConfigLegacy(configPath string) (*GlobalConfig, error) { config.Database.URL = "hub.db" } } + applyDatabasePoolDefaults(&config.Database) // Fixup for list fields that might be loaded as a single comma-separated string from env vars. // This happens because koanf's env provider doesn't automatically split strings for slice fields. diff --git a/pkg/config/hub_config_test.go b/pkg/config/hub_config_test.go index 11d97176e..ed7f8edb0 100644 --- a/pkg/config/hub_config_test.go +++ b/pkg/config/hub_config_test.go @@ -699,3 +699,48 @@ hub: t.Fatal("expected not to find server config in settings.yaml") } } + +// TestApplyDatabasePoolDefaults_PostgresOverridesLeakedSqliteDefault is a +// regression test for the production incident where both hubs served every API +// request in ~55s. The struct-level default for MaxOpenConns/MaxIdleConns is 1 +// (required by SQLite to serialize writes). A postgres deployment configured via +// env/driver override inherits that 1, and the original `<= 0` guard left the +// pool at a single connection. With a pool of 1, a singleton scheduler handler +// that holds the lone connection for an advisory lock self-deadlocks waiting for +// a second connection to do its work, and all traffic serializes behind it. +func TestApplyDatabasePoolDefaults_PostgresOverridesLeakedSqliteDefault(t *testing.T) { + // Mirrors the production path: start from the embedded defaults (which set + // MaxOpenConns=1 for the SQLite default) and switch the driver to postgres. + db := DefaultGlobalConfig().Database + db.Driver = "postgres" + db.URL = "host=db port=5432 dbname=scion sslmode=require" + + applyDatabasePoolDefaults(&db) + + if db.MaxOpenConns < 2 { + t.Fatalf("postgres MaxOpenConns must be a real pool, got %d (leaked SQLite default of 1 not overridden)", db.MaxOpenConns) + } + if db.MaxIdleConns < 2 { + t.Fatalf("postgres MaxIdleConns must be > 1, got %d", db.MaxIdleConns) + } +} + +// TestApplyDatabasePoolDefaults_PostgresRespectsExplicitPool ensures an operator +// who explicitly sizes the pool (>= 2) is not clobbered by the default. +func TestApplyDatabasePoolDefaults_PostgresRespectsExplicitPool(t *testing.T) { + db := DatabaseConfig{Driver: "postgres", MaxOpenConns: 25, MaxIdleConns: 12} + applyDatabasePoolDefaults(&db) + if db.MaxOpenConns != 25 || db.MaxIdleConns != 12 { + t.Fatalf("explicit pool sizing clobbered: open=%d idle=%d", db.MaxOpenConns, db.MaxIdleConns) + } +} + +// TestApplyDatabasePoolDefaults_SqliteStaysSingleConnection guards the +// load-bearing invariant that SQLite always serializes through one connection. +func TestApplyDatabasePoolDefaults_SqliteStaysSingleConnection(t *testing.T) { + db := DefaultGlobalConfig().Database // Driver defaults to sqlite + applyDatabasePoolDefaults(&db) + if db.MaxOpenConns != 1 { + t.Fatalf("sqlite MaxOpenConns must be 1, got %d", db.MaxOpenConns) + } +} diff --git a/pkg/config/init_project_test.go b/pkg/config/init_project_test.go index ddc8d14f8..13e84a10d 100644 --- a/pkg/config/init_project_test.go +++ b/pkg/config/init_project_test.go @@ -388,7 +388,7 @@ func TestInitProject_CreatesEmptyTemplatesDir(t *testing.T) { } // Verify per-harness templates were NOT created - for _, name := range []string{"gemini", "claude", "opencode", "codex"} { + for _, name := range []string{"gemini", "claude"} { perHarnessDir := filepath.Join(tempDir, "templates", name) if _, err := os.Stat(perHarnessDir); !os.IsNotExist(err) { t.Errorf("Expected per-harness template %s to NOT be created at project level", name) diff --git a/pkg/config/init_test.go b/pkg/config/init_test.go index df68cbdce..640223ec7 100644 --- a/pkg/config/init_test.go +++ b/pkg/config/init_test.go @@ -371,7 +371,7 @@ func TestInitProject_NoHarnessConfigs(t *testing.T) { } // Verify per-harness template directories were NOT created - for _, name := range []string{"gemini", "claude", "opencode", "codex"} { + for _, name := range []string{"gemini", "claude"} { perHarnessTplDir := filepath.Join(projectDir, "templates", name) if _, err := os.Stat(perHarnessTplDir); !os.IsNotExist(err) { t.Errorf("expected per-harness template dir %s to NOT exist at project level", perHarnessTplDir) @@ -410,7 +410,7 @@ func TestInitMachine_SeedsAll(t *testing.T) { } // Verify per-harness template directories were NOT created - for _, name := range []string{"gemini", "claude", "opencode", "codex"} { + for _, name := range []string{"gemini", "claude"} { perHarnessTplDir := filepath.Join(globalDir, "templates", name) if _, err := os.Stat(perHarnessTplDir); !os.IsNotExist(err) { t.Errorf("expected per-harness template dir %s to NOT exist", perHarnessTplDir) diff --git a/pkg/config/mock_harness_test.go b/pkg/config/mock_harness_test.go index 6ab18ff54..561b4719d 100644 --- a/pkg/config/mock_harness_test.go +++ b/pkg/config/mock_harness_test.go @@ -71,7 +71,5 @@ func GetMockHarnesses() []api.Harness { return []api.Harness{ &MockHarness{NameVal: "gemini", EmbedDirVal: "gemini", ConfigDirVal: ".gemini"}, &MockHarness{NameVal: "claude", EmbedDirVal: "claude", ConfigDirVal: ".claude"}, - &MockHarness{NameVal: "opencode", EmbedDirVal: "opencode", ConfigDirVal: ".config/opencode"}, - &MockHarness{NameVal: "codex", EmbedDirVal: "codex", ConfigDirVal: ""}, } } diff --git a/pkg/config/settings_v1.go b/pkg/config/settings_v1.go index 7246478dc..42f252f29 100644 --- a/pkg/config/settings_v1.go +++ b/pkg/config/settings_v1.go @@ -252,17 +252,18 @@ type V1ServerConfig struct { // Mode selects the server operating mode: "workstation" (default) or "hosted". // When set to "hosted", the server behaves as if --hosted were passed. // The legacy value "production" is also accepted for backward compatibility. - Mode string `json:"mode,omitempty" yaml:"mode,omitempty" koanf:"mode"` - Env string `json:"env,omitempty" yaml:"env,omitempty" koanf:"env"` - Hub *V1ServerHubConfig `json:"hub,omitempty" yaml:"hub,omitempty" koanf:"hub"` - Broker *V1BrokerConfig `json:"broker,omitempty" yaml:"broker,omitempty" koanf:"broker"` - Database *V1DatabaseConfig `json:"database,omitempty" yaml:"database,omitempty" koanf:"database"` - Auth *V1AuthConfig `json:"auth,omitempty" yaml:"auth,omitempty" koanf:"auth"` - OAuth *V1OAuthConfig `json:"oauth,omitempty" yaml:"oauth,omitempty" koanf:"oauth"` - Storage *V1StorageConfig `json:"storage,omitempty" yaml:"storage,omitempty" koanf:"storage"` - Secrets *V1SecretsConfig `json:"secrets,omitempty" yaml:"secrets,omitempty" koanf:"secrets"` - LogLevel string `json:"log_level,omitempty" yaml:"log_level,omitempty" koanf:"log_level"` - LogFormat string `json:"log_format,omitempty" yaml:"log_format,omitempty" koanf:"log_format"` + Mode string `json:"mode,omitempty" yaml:"mode,omitempty" koanf:"mode"` + Env string `json:"env,omitempty" yaml:"env,omitempty" koanf:"env"` + Hub *V1ServerHubConfig `json:"hub,omitempty" yaml:"hub,omitempty" koanf:"hub"` + Broker *V1BrokerConfig `json:"broker,omitempty" yaml:"broker,omitempty" koanf:"broker"` + Database *V1DatabaseConfig `json:"database,omitempty" yaml:"database,omitempty" koanf:"database"` + Auth *V1AuthConfig `json:"auth,omitempty" yaml:"auth,omitempty" koanf:"auth"` + OAuth *V1OAuthConfig `json:"oauth,omitempty" yaml:"oauth,omitempty" koanf:"oauth"` + Storage *V1StorageConfig `json:"storage,omitempty" yaml:"storage,omitempty" koanf:"storage"` + WorkspaceStorage *V1WorkspaceStorageConfig `json:"workspace_storage,omitempty" yaml:"workspace_storage,omitempty" koanf:"workspace_storage"` + Secrets *V1SecretsConfig `json:"secrets,omitempty" yaml:"secrets,omitempty" koanf:"secrets"` + LogLevel string `json:"log_level,omitempty" yaml:"log_level,omitempty" koanf:"log_level"` + LogFormat string `json:"log_format,omitempty" yaml:"log_format,omitempty" koanf:"log_format"` // NotificationChannels configures external notification delivery channels. // Secrets (webhook URLs, API tokens) are held in memory only — never persisted to a database. @@ -375,17 +376,56 @@ type V1BrokerConfig struct { // V1DatabaseConfig holds database settings. type V1DatabaseConfig struct { - Driver string `json:"driver,omitempty" yaml:"driver,omitempty" koanf:"driver"` - URL string `json:"url,omitempty" yaml:"url,omitempty" koanf:"url"` + Driver string `json:"driver,omitempty" yaml:"driver,omitempty" koanf:"driver"` + URL string `json:"url,omitempty" yaml:"url,omitempty" koanf:"url"` + MaxOpenConns int `json:"max_open_conns,omitempty" yaml:"max_open_conns,omitempty" koanf:"max_open_conns"` + MaxIdleConns int `json:"max_idle_conns,omitempty" yaml:"max_idle_conns,omitempty" koanf:"max_idle_conns"` + ConnMaxLifetime string `json:"conn_max_lifetime,omitempty" yaml:"conn_max_lifetime,omitempty" koanf:"conn_max_lifetime"` + ConnMaxIdleTime string `json:"conn_max_idle_time,omitempty" yaml:"conn_max_idle_time,omitempty" koanf:"conn_max_idle_time"` } -// V1AuthConfig holds development authentication settings. +// V1AuthConfig holds authentication settings. type V1AuthConfig struct { - DevMode bool `json:"dev_mode,omitempty" yaml:"dev_mode,omitempty" koanf:"dev_mode"` - DevToken string `json:"dev_token,omitempty" yaml:"dev_token,omitempty" koanf:"dev_token"` - DevTokenFile string `json:"dev_token_file,omitempty" yaml:"dev_token_file,omitempty" koanf:"dev_token_file"` - AuthorizedDomains []string `json:"authorized_domains,omitempty" yaml:"authorized_domains,omitempty" koanf:"authorized_domains"` - UserAccessMode string `json:"user_access_mode,omitempty" yaml:"user_access_mode,omitempty" koanf:"user_access_mode"` + // Mode selects the exclusive human auth mode: "oauth" (default), "proxy", or "dev". + // In proxy mode, OAuth handlers are disabled; in dev mode, dev token auth is used. + Mode string `json:"mode,omitempty" yaml:"mode,omitempty" koanf:"mode"` + DevMode bool `json:"dev_mode,omitempty" yaml:"dev_mode,omitempty" koanf:"dev_mode"` + DevToken string `json:"dev_token,omitempty" yaml:"dev_token,omitempty" koanf:"dev_token"` + DevTokenFile string `json:"dev_token_file,omitempty" yaml:"dev_token_file,omitempty" koanf:"dev_token_file"` + AuthorizedDomains []string `json:"authorized_domains,omitempty" yaml:"authorized_domains,omitempty" koanf:"authorized_domains"` + UserAccessMode string `json:"user_access_mode,omitempty" yaml:"user_access_mode,omitempty" koanf:"user_access_mode"` + Proxy *V1ProxyConfig `json:"proxy,omitempty" yaml:"proxy,omitempty" koanf:"proxy"` + Transport *V1TransportConfig `json:"transport,omitempty" yaml:"transport,omitempty" koanf:"transport"` +} + +// V1TransportConfig holds transport-layer auth settings for agent outbound requests. +type V1TransportConfig struct { + // Mode selects the transport auth mode: "none" (default), "cloudrun_invoker", or "iap". + Mode string `json:"mode,omitempty" yaml:"mode,omitempty" koanf:"mode"` + // OIDCAudience is the OIDC audience for transport tokens. + OIDCAudience string `json:"oidc_audience,omitempty" yaml:"oidc_audience,omitempty" koanf:"oidc_audience"` + // PlatformAuthSA is the dedicated SA email used for transport-layer auth. + PlatformAuthSA string `json:"platform_auth_sa,omitempty" yaml:"platform_auth_sa,omitempty" koanf:"platform_auth_sa"` +} + +// V1ProxyConfig holds proxy authentication settings (consulted when auth.mode == "proxy"). +type V1ProxyConfig struct { + // Provider selects the proxy auth provider: "iap" or "header". + Provider string `json:"provider,omitempty" yaml:"provider,omitempty" koanf:"provider"` + // IAP holds Google IAP-specific settings. + IAP *V1IAPConfig `json:"iap,omitempty" yaml:"iap,omitempty" koanf:"iap"` + // RequireTrustedProxyIP enables defense-in-depth IP allowlisting. + RequireTrustedProxyIP bool `json:"require_trusted_proxy_ip,omitempty" yaml:"require_trusted_proxy_ip,omitempty" koanf:"require_trusted_proxy_ip"` +} + +// V1IAPConfig holds Google IAP-specific settings. +type V1IAPConfig struct { + // Audience is the expected audience claim — MANDATORY for IAP. + Audience string `json:"audience,omitempty" yaml:"audience,omitempty" koanf:"audience"` + // Issuer overrides the default IAP issuer (for testing). + Issuer string `json:"issuer,omitempty" yaml:"issuer,omitempty" koanf:"issuer"` + // JWKSURL overrides the default IAP JWKS URL (for testing). + JWKSURL string `json:"jwks_url,omitempty" yaml:"jwks_url,omitempty" koanf:"jwks_url"` } // V1OAuthConfig holds OAuth provider configurations. @@ -414,6 +454,104 @@ type V1StorageConfig struct { LocalPath string `json:"local_path,omitempty" yaml:"local_path,omitempty" koanf:"local_path"` } +// V1WorkspaceStorageConfig selects the workspace storage backend. +// Backend defaults to "local" (today's node-local behavior). When set to "nfs", +// the NFS sub-block configures shared network-attached workspace storage. +// "cloudrun-volume" and "gke-shared-volume" select vendor-managed volume backends. +type V1WorkspaceStorageConfig struct { + Backend string `json:"backend,omitempty" yaml:"backend,omitempty" koanf:"backend"` // "local" | "nfs" | "cloudrun-volume" | "gke-shared-volume" + NFS *V1NFSConfig `json:"nfs,omitempty" yaml:"nfs,omitempty" koanf:"nfs"` + CloudRunVolume *V1CloudRunVolumeConfig `json:"cloudrun_volume,omitempty" yaml:"cloudrun_volume,omitempty" koanf:"cloudrun_volume"` + GKESharedVolume *V1GKESharedVolumeConfig `json:"gke_shared_volume,omitempty" yaml:"gke_shared_volume,omitempty" koanf:"gke_shared_volume"` +} + +// V1NFSConfig holds NFS workspace storage settings. +type V1NFSConfig struct { + // MountRoot is the local base under which each share is mounted at /. + MountRoot string `json:"mount_root,omitempty" yaml:"mount_root,omitempty" koanf:"mount_root"` + // MountOptions are passed to mount.nfs. Default "vers=3,hard,nconnect=4,_netdev". + // NFSv4.1 requires Filestore Enterprise/zonal or self-hosted NFS; basic/HDD + // (BASIC_HDD) supports NFSv3 only. We use Postgres advisory locks, not NFS + // flock, so v3 is fine for correctness. + MountOptions string `json:"mount_options,omitempty" yaml:"mount_options,omitempty" koanf:"mount_options"` + Shares []V1NFSShare `json:"shares,omitempty" yaml:"shares,omitempty" koanf:"shares"` + + // Stable, node-independent ownership for NFS-backed trees. + // Default 1000:1000 to converge with the K8s pod UID/GID. + UID int `json:"uid,omitempty" yaml:"uid,omitempty" koanf:"uid"` // default 1000 + GID int `json:"gid,omitempty" yaml:"gid,omitempty" koanf:"gid"` // default 1000 + + // Kubernetes realization + StorageClass string `json:"storage_class,omitempty" yaml:"storage_class,omitempty" koanf:"storage_class"` + SubPathRoot string `json:"subpath_root,omitempty" yaml:"subpath_root,omitempty" koanf:"subpath_root"` // default "projects" +} + +// V1NFSShare identifies a single NFS export that may be mounted by a Runtime Broker. +type V1NFSShare struct { + ID string `json:"id,omitempty" yaml:"id,omitempty" koanf:"id"` // stable share id → mount dir + (K8s) PV name + Server string `json:"server,omitempty" yaml:"server,omitempty" koanf:"server"` // e.g. 10.0.0.2 or Filestore IP + Export string `json:"export,omitempty" yaml:"export,omitempty" koanf:"export"` // server export path, e.g. /scion-workspaces + PVName string `json:"pv_name,omitempty" yaml:"pv_name,omitempty" koanf:"pv_name"` // K8s static PV+subPath strategy +} + +// V1CloudRunVolumeConfig holds Cloud Run managed volume settings. +// Cloud Run volumes are declared in the service spec and mounted by the +// platform — no host path or NFS server is needed. +type V1CloudRunVolumeConfig struct { + // VolumeName is the Cloud Run volume resource name declared in the service YAML. + VolumeName string `json:"volume_name,omitempty" yaml:"volume_name,omitempty" koanf:"volume_name"` + // SubPathRoot is the sub-directory prefix within the volume. Default "projects". + SubPathRoot string `json:"subpath_root,omitempty" yaml:"subpath_root,omitempty" koanf:"subpath_root"` +} + +// V1GKESharedVolumeConfig holds GKE-provided shared volume settings +// (e.g. a Filestore CSI-backed PVC that GKE manages). +type V1GKESharedVolumeConfig struct { + // VolumeName is the K8s volume name referencing the PVC. + VolumeName string `json:"volume_name,omitempty" yaml:"volume_name,omitempty" koanf:"volume_name"` + // PVClaimName is the PVC name bound to the GKE-managed shared storage. + PVClaimName string `json:"pv_claim_name,omitempty" yaml:"pv_claim_name,omitempty" koanf:"pv_claim_name"` + // SubPathRoot is the sub-directory prefix within the volume. Default "projects". + SubPathRoot string `json:"subpath_root,omitempty" yaml:"subpath_root,omitempty" koanf:"subpath_root"` +} + +// ApplyNFSDefaults fills default values for NFS sub-fields when Backend is "nfs". +// When Backend is empty or "local", the NFS block is left as-is (no materialization). +// This is idempotent and safe to call multiple times. +func (ws *V1WorkspaceStorageConfig) ApplyNFSDefaults() { + if ws == nil || strings.ToLower(ws.Backend) != "nfs" { + return + } + if ws.NFS == nil { + ws.NFS = &V1NFSConfig{} + } + if ws.NFS.MountOptions == "" { + ws.NFS.MountOptions = "vers=3,hard,nconnect=4,_netdev" + } + if ws.NFS.UID == 0 { + ws.NFS.UID = 1000 + } + if ws.NFS.GID == 0 { + ws.NFS.GID = 1000 + } + if ws.NFS.SubPathRoot == "" { + ws.NFS.SubPathRoot = "projects" + } +} + +// ValidateNFS returns an error if Backend is "nfs" but the NFS block is +// misconfigured (e.g. no shares defined). Call after ApplyNFSDefaults. +func (ws *V1WorkspaceStorageConfig) ValidateNFS() error { + if ws == nil || strings.ToLower(ws.Backend) != "nfs" { + return nil + } + if ws.NFS == nil || len(ws.NFS.Shares) == 0 { + return fmt.Errorf("workspace_storage.backend is \"nfs\" but no NFS shares are defined; " + + "add at least one entry under workspace_storage.nfs.shares") + } + return nil +} + // V1SecretsConfig holds secrets backend settings. type V1SecretsConfig struct { Backend string `json:"backend,omitempty" yaml:"backend,omitempty" koanf:"backend"` @@ -522,6 +660,14 @@ type V1TelemetrySamplingConfig struct { Rates map[string]float64 `json:"rates,omitempty" yaml:"rates,omitempty" koanf:"rates"` } +// V1CloudRunConfig holds Cloud Run runtime settings. +type V1CloudRunConfig struct { + // Project is the GCP project ID for Cloud Run API calls. + Project string `json:"project,omitempty" yaml:"project,omitempty" koanf:"project"` + // Region is the GCP region for Cloud Run services (e.g. "us-central1"). + Region string `json:"region,omitempty" yaml:"region,omitempty" koanf:"region"` +} + // V1RuntimeConfig extends RuntimeConfig with a Type field. type V1RuntimeConfig struct { Type string `json:"type,omitempty" yaml:"type,omitempty" koanf:"type"` @@ -532,6 +678,8 @@ type V1RuntimeConfig struct { Sync string `json:"sync,omitempty" yaml:"sync,omitempty" koanf:"sync"` GKE bool `json:"gke,omitempty" yaml:"gke,omitempty" koanf:"gke"` ListAllNamespaces bool `json:"list_all_namespaces,omitempty" yaml:"list_all_namespaces,omitempty" koanf:"list_all_namespaces"` + // CloudRun holds Cloud Run-specific settings when Type is "cloudrun". + CloudRun *V1CloudRunConfig `json:"cloudrun,omitempty" yaml:"cloudrun,omitempty" koanf:"cloudrun"` } // HarnessConfigEntry defines a harness configuration entry in versioned settings. @@ -811,9 +959,13 @@ func versionedEnvKeyMapper(s string) string { // These must be recognized as single fields rather than split into nested keys. // IMPORTANT: Sorted longest-first so that "dev_token_file" matches before "dev_token". var knownCompoundFields = []string{ + "require_trusted_proxy_ip", "soft_delete_retain_files", "soft_delete_retention", "authorized_domains", + "platform_auth_sa", + "oidc_audience", + "jwks_url", "broker_nickname", "allowed_origins", "allowed_methods", @@ -906,7 +1058,7 @@ func mapEnvKeyRecursive(key string) string { func isSectionName(name string) bool { switch name { case "hub", "broker", "database", "auth", "oauth", "storage", "secrets", "cors", - "web", "cli", "device", "google", "github": + "web", "cli", "device", "google", "github", "proxy", "iap", "transport": return true } return false @@ -1140,10 +1292,25 @@ func ConvertV1ServerToGlobalConfig(v1 *V1ServerConfig) *GlobalConfig { if v1.Database.URL != "" { gc.Database.URL = v1.Database.URL } + if v1.Database.MaxOpenConns != 0 { + gc.Database.MaxOpenConns = v1.Database.MaxOpenConns + } + if v1.Database.MaxIdleConns != 0 { + gc.Database.MaxIdleConns = v1.Database.MaxIdleConns + } + if v1.Database.ConnMaxLifetime != "" { + gc.Database.ConnMaxLifetime = v1.Database.ConnMaxLifetime + } + if v1.Database.ConnMaxIdleTime != "" { + gc.Database.ConnMaxIdleTime = v1.Database.ConnMaxIdleTime + } } // Auth config if v1.Auth != nil { + if v1.Auth.Mode != "" { + gc.Auth.Mode = v1.Auth.Mode + } gc.Auth.Enabled = v1.Auth.DevMode gc.Auth.Token = v1.Auth.DevToken gc.Auth.TokenFile = v1.Auth.DevTokenFile @@ -1153,6 +1320,26 @@ func ConvertV1ServerToGlobalConfig(v1 *V1ServerConfig) *GlobalConfig { if v1.Auth.UserAccessMode != "" { gc.Auth.UserAccessMode = v1.Auth.UserAccessMode } + if v1.Auth.Proxy != nil { + gc.Auth.Proxy = &ProxyAuthConfig{ + Provider: v1.Auth.Proxy.Provider, + RequireTrustedProxyIP: v1.Auth.Proxy.RequireTrustedProxyIP, + } + if v1.Auth.Proxy.IAP != nil { + gc.Auth.Proxy.IAP = &IAPAuthConfig{ + Audience: v1.Auth.Proxy.IAP.Audience, + Issuer: v1.Auth.Proxy.IAP.Issuer, + JWKSURL: v1.Auth.Proxy.IAP.JWKSURL, + } + } + } + if v1.Auth.Transport != nil { + gc.Auth.Transport = &TransportAuthConfig{ + Mode: v1.Auth.Transport.Mode, + OIDCAudience: v1.Auth.Transport.OIDCAudience, + PlatformAuthSA: v1.Auth.Transport.PlatformAuthSA, + } + } } // OAuth config @@ -1215,6 +1402,11 @@ func ConvertV1ServerToGlobalConfig(v1 *V1ServerConfig) *GlobalConfig { } } + // Workspace storage NFS defaults (conditional on backend=nfs) + if v1.WorkspaceStorage != nil { + v1.WorkspaceStorage.ApplyNFSDefaults() + } + // GitHub App if v1.GitHubApp != nil { gc.GitHubApp.AppID = v1.GitHubApp.AppID @@ -1291,18 +1483,43 @@ func ConvertGlobalToV1ServerConfig(gc *GlobalConfig) *V1ServerConfig { // Database config v1.Database = &V1DatabaseConfig{ - Driver: gc.Database.Driver, - URL: gc.Database.URL, + Driver: gc.Database.Driver, + URL: gc.Database.URL, + MaxOpenConns: gc.Database.MaxOpenConns, + MaxIdleConns: gc.Database.MaxIdleConns, + ConnMaxLifetime: gc.Database.ConnMaxLifetime, + ConnMaxIdleTime: gc.Database.ConnMaxIdleTime, } // Auth config v1.Auth = &V1AuthConfig{ + Mode: gc.Auth.Mode, DevMode: gc.Auth.Enabled, DevToken: gc.Auth.Token, DevTokenFile: gc.Auth.TokenFile, AuthorizedDomains: gc.Auth.AuthorizedDomains, UserAccessMode: gc.Auth.UserAccessMode, } + if gc.Auth.Proxy != nil { + v1.Auth.Proxy = &V1ProxyConfig{ + Provider: gc.Auth.Proxy.Provider, + RequireTrustedProxyIP: gc.Auth.Proxy.RequireTrustedProxyIP, + } + if gc.Auth.Proxy.IAP != nil { + v1.Auth.Proxy.IAP = &V1IAPConfig{ + Audience: gc.Auth.Proxy.IAP.Audience, + Issuer: gc.Auth.Proxy.IAP.Issuer, + JWKSURL: gc.Auth.Proxy.IAP.JWKSURL, + } + } + } + if gc.Auth.Transport != nil { + v1.Auth.Transport = &V1TransportConfig{ + Mode: gc.Auth.Transport.Mode, + OIDCAudience: gc.Auth.Transport.OIDCAudience, + PlatformAuthSA: gc.Auth.Transport.PlatformAuthSA, + } + } // OAuth config v1.OAuth = &V1OAuthConfig{ diff --git a/pkg/config/settings_v1_test.go b/pkg/config/settings_v1_test.go index 776f74d50..213e54900 100644 --- a/pkg/config/settings_v1_test.go +++ b/pkg/config/settings_v1_test.go @@ -3627,6 +3627,182 @@ func TestRequireImageRegistry_Configured(t *testing.T) { assert.NoError(t, err) } +// --- N0-1: Workspace storage config tests --- + +func TestWorkspaceStorageConfig_YAMLRoundTrip(t *testing.T) { + yamlInput := ` +schema_version: "1" +server: + workspace_storage: + backend: nfs + nfs: + mount_root: /mnt/nfs + mount_options: "vers=4.1,hard,nconnect=8" + uid: 2000 + gid: 2000 + subpath_root: workspaces + storage_class: filestore-sc + shares: + - id: share-1 + server: "10.0.0.2" + export: /scion-workspaces + pv_name: scion-workspaces-pv +` + tmpDir := t.TempDir() + originalHome := os.Getenv("HOME") + defer os.Setenv("HOME", originalHome) + os.Setenv("HOME", tmpDir) + + globalDir := filepath.Join(tmpDir, ".scion") + require.NoError(t, os.MkdirAll(globalDir, 0755)) + require.NoError(t, os.WriteFile(filepath.Join(globalDir, "settings.yaml"), []byte(yamlInput), 0644)) + + projectDir := filepath.Join(tmpDir, "project", ".scion") + require.NoError(t, os.MkdirAll(projectDir, 0755)) + + vs, err := LoadVersionedSettings(projectDir) + require.NoError(t, err) + require.NotNil(t, vs.Server) + require.NotNil(t, vs.Server.WorkspaceStorage) + + ws := vs.Server.WorkspaceStorage + assert.Equal(t, "nfs", ws.Backend) + require.NotNil(t, ws.NFS) + assert.Equal(t, "/mnt/nfs", ws.NFS.MountRoot) + assert.Equal(t, "vers=4.1,hard,nconnect=8", ws.NFS.MountOptions) + assert.Equal(t, 2000, ws.NFS.UID) + assert.Equal(t, 2000, ws.NFS.GID) + assert.Equal(t, "workspaces", ws.NFS.SubPathRoot) + assert.Equal(t, "filestore-sc", ws.NFS.StorageClass) + require.Len(t, ws.NFS.Shares, 1) + assert.Equal(t, "share-1", ws.NFS.Shares[0].ID) + assert.Equal(t, "10.0.0.2", ws.NFS.Shares[0].Server) + assert.Equal(t, "/scion-workspaces", ws.NFS.Shares[0].Export) + assert.Equal(t, "scion-workspaces-pv", ws.NFS.Shares[0].PVName) +} + +func TestWorkspaceStorageConfig_JSONRoundTrip(t *testing.T) { + ws := &V1WorkspaceStorageConfig{ + Backend: "nfs", + NFS: &V1NFSConfig{ + MountRoot: "/mnt/nfs", + MountOptions: "vers=4.1,hard,nconnect=4,_netdev", + UID: 1000, + GID: 1000, + SubPathRoot: "projects", + Shares: []V1NFSShare{ + {ID: "main", Server: "10.0.0.2", Export: "/scion-workspaces", PVName: "scion-ws-pv"}, + }, + }, + } + + data, err := json.Marshal(ws) + require.NoError(t, err) + + var roundTripped V1WorkspaceStorageConfig + require.NoError(t, json.Unmarshal(data, &roundTripped)) + + assert.Equal(t, ws.Backend, roundTripped.Backend) + require.NotNil(t, roundTripped.NFS) + assert.Equal(t, ws.NFS.MountRoot, roundTripped.NFS.MountRoot) + assert.Equal(t, ws.NFS.MountOptions, roundTripped.NFS.MountOptions) + assert.Equal(t, ws.NFS.UID, roundTripped.NFS.UID) + assert.Equal(t, ws.NFS.GID, roundTripped.NFS.GID) + assert.Equal(t, ws.NFS.SubPathRoot, roundTripped.NFS.SubPathRoot) + require.Len(t, roundTripped.NFS.Shares, 1) + assert.Equal(t, ws.NFS.Shares[0].ID, roundTripped.NFS.Shares[0].ID) + assert.Equal(t, ws.NFS.Shares[0].Server, roundTripped.NFS.Shares[0].Server) +} + +func TestWorkspaceStorageConfig_NFSDefaults(t *testing.T) { + t.Run("nfs backend applies defaults to empty sub-fields", func(t *testing.T) { + ws := &V1WorkspaceStorageConfig{Backend: "nfs"} + ws.ApplyNFSDefaults() + + require.NotNil(t, ws.NFS) + assert.Equal(t, "vers=3,hard,nconnect=4,_netdev", ws.NFS.MountOptions) + assert.Equal(t, 1000, ws.NFS.UID) + assert.Equal(t, 1000, ws.NFS.GID) + assert.Equal(t, "projects", ws.NFS.SubPathRoot) + }) + + t.Run("nfs backend preserves explicit values", func(t *testing.T) { + ws := &V1WorkspaceStorageConfig{ + Backend: "nfs", + NFS: &V1NFSConfig{ + MountOptions: "custom-opts", + UID: 5000, + GID: 5000, + SubPathRoot: "custom-root", + }, + } + ws.ApplyNFSDefaults() + + assert.Equal(t, "custom-opts", ws.NFS.MountOptions) + assert.Equal(t, 5000, ws.NFS.UID) + assert.Equal(t, 5000, ws.NFS.GID) + assert.Equal(t, "custom-root", ws.NFS.SubPathRoot) + }) + + t.Run("local backend does not materialize NFS block", func(t *testing.T) { + ws := &V1WorkspaceStorageConfig{Backend: "local"} + ws.ApplyNFSDefaults() + assert.Nil(t, ws.NFS) + }) + + t.Run("empty backend does not materialize NFS block", func(t *testing.T) { + ws := &V1WorkspaceStorageConfig{} + ws.ApplyNFSDefaults() + assert.Nil(t, ws.NFS) + }) + + t.Run("nil receiver is safe", func(t *testing.T) { + var ws *V1WorkspaceStorageConfig + ws.ApplyNFSDefaults() // should not panic + }) +} + +func TestWorkspaceStorageConfig_ValidateNFS(t *testing.T) { + t.Run("nfs backend with no shares returns error", func(t *testing.T) { + ws := &V1WorkspaceStorageConfig{Backend: "nfs"} + ws.ApplyNFSDefaults() + err := ws.ValidateNFS() + require.Error(t, err) + assert.Contains(t, err.Error(), "no NFS shares are defined") + }) + + t.Run("nfs backend with shares passes", func(t *testing.T) { + ws := &V1WorkspaceStorageConfig{ + Backend: "nfs", + NFS: &V1NFSConfig{ + Shares: []V1NFSShare{{ID: "share1", Server: "10.0.0.2", Export: "/data"}}, + }, + } + ws.ApplyNFSDefaults() + err := ws.ValidateNFS() + require.NoError(t, err) + }) + + t.Run("local backend skips validation", func(t *testing.T) { + ws := &V1WorkspaceStorageConfig{Backend: "local"} + err := ws.ValidateNFS() + require.NoError(t, err) + }) + + t.Run("nil receiver is safe", func(t *testing.T) { + var ws *V1WorkspaceStorageConfig + err := ws.ValidateNFS() + require.NoError(t, err) + }) +} + +func TestWorkspaceStorageConfig_BackendUnset_IsLocal(t *testing.T) { + // Backend unset => treated as "local", no NFS struct required. + ws := &V1WorkspaceStorageConfig{} + assert.Equal(t, "", ws.Backend, "empty backend is treated as local") + assert.Nil(t, ws.NFS, "no NFS block when backend is local/empty") +} + // --- Helper --- func boolPtr(b bool) *bool { diff --git a/pkg/config/templates_test.go b/pkg/config/templates_test.go index 319cce87e..42bf62f55 100644 --- a/pkg/config/templates_test.go +++ b/pkg/config/templates_test.go @@ -431,6 +431,37 @@ func TestLoadConfigInvalidVolumes(t *testing.T) { } }) + t.Run("valid nfs volume", func(t *testing.T) { + tmpDir, err := os.MkdirTemp("", "scion-test-nfs-volumes-*") + if err != nil { + t.Fatal(err) + } + defer os.RemoveAll(tmpDir) + + configContent := `{ + "harness": "gemini", + "volumes": [{"source": "/scion-workspaces", "target": "/workspace", "type": "nfs", "server": "10.0.0.2"}] + }` + if err := os.WriteFile(filepath.Join(tmpDir, "scion-agent.json"), []byte(configContent), 0644); err != nil { + t.Fatal(err) + } + + tpl := &Template{Path: tmpDir} + cfg, err := tpl.LoadConfig() + if err != nil { + t.Fatalf("LoadConfig() unexpected error for valid nfs volume: %v", err) + } + if len(cfg.Volumes) != 1 { + t.Fatalf("LoadConfig() expected 1 volume, got %d", len(cfg.Volumes)) + } + if cfg.Volumes[0].Type != "nfs" { + t.Errorf("Volume type = %q, want %q", cfg.Volumes[0].Type, "nfs") + } + if cfg.Volumes[0].Server != "10.0.0.2" { + t.Errorf("Volume server = %q, want %q", cfg.Volumes[0].Server, "10.0.0.2") + } + }) + t.Run("volume with invalid type", func(t *testing.T) { tmpDir, err := os.MkdirTemp("", "scion-test-invalid-volumes-*") if err != nil { @@ -440,7 +471,7 @@ func TestLoadConfigInvalidVolumes(t *testing.T) { configContent := `{ "harness": "gemini", - "volumes": [{"source": "/foo", "target": "/bar", "type": "nfs"}] + "volumes": [{"source": "/foo", "target": "/bar", "type": "bogus"}] }` if err := os.WriteFile(filepath.Join(tmpDir, "scion-agent.json"), []byte(configContent), 0644); err != nil { t.Fatal(err) diff --git a/pkg/ent/accesspolicy_create.go b/pkg/ent/accesspolicy_create.go index 700772a8a..e5959e2a9 100644 --- a/pkg/ent/accesspolicy_create.go +++ b/pkg/ent/accesspolicy_create.go @@ -8,6 +8,8 @@ import ( "fmt" "time" + "entgo.io/ent/dialect" + "entgo.io/ent/dialect/sql" "entgo.io/ent/dialect/sql/sqlgraph" "entgo.io/ent/schema/field" "github.com/GoogleCloudPlatform/scion/pkg/ent/accesspolicy" @@ -21,6 +23,7 @@ type AccessPolicyCreate struct { config mutation *AccessPolicyMutation hooks []Hook + conflict []sql.ConflictOption } // SetName sets the "name" field. @@ -325,6 +328,7 @@ func (_c *AccessPolicyCreate) createSpec() (*AccessPolicy, *sqlgraph.CreateSpec) _node = &AccessPolicy{config: _c.config} _spec = sqlgraph.NewCreateSpec(accesspolicy.Table, sqlgraph.NewFieldSpec(accesspolicy.FieldID, field.TypeUUID)) ) + _spec.OnConflict = _c.conflict if id, ok := _c.mutation.ID(); ok { _node.ID = id _spec.ID.Value = &id @@ -408,11 +412,631 @@ func (_c *AccessPolicyCreate) createSpec() (*AccessPolicy, *sqlgraph.CreateSpec) return _node, _spec } +// OnConflict allows configuring the `ON CONFLICT` / `ON DUPLICATE KEY` clause +// of the `INSERT` statement. For example: +// +// client.AccessPolicy.Create(). +// SetName(v). +// OnConflict( +// // Update the row with the new values +// // the was proposed for insertion. +// sql.ResolveWithNewValues(), +// ). +// // Override some of the fields with custom +// // update values. +// Update(func(u *ent.AccessPolicyUpsert) { +// SetName(v+v). +// }). +// Exec(ctx) +func (_c *AccessPolicyCreate) OnConflict(opts ...sql.ConflictOption) *AccessPolicyUpsertOne { + _c.conflict = opts + return &AccessPolicyUpsertOne{ + create: _c, + } +} + +// OnConflictColumns calls `OnConflict` and configures the columns +// as conflict target. Using this option is equivalent to using: +// +// client.AccessPolicy.Create(). +// OnConflict(sql.ConflictColumns(columns...)). +// Exec(ctx) +func (_c *AccessPolicyCreate) OnConflictColumns(columns ...string) *AccessPolicyUpsertOne { + _c.conflict = append(_c.conflict, sql.ConflictColumns(columns...)) + return &AccessPolicyUpsertOne{ + create: _c, + } +} + +type ( + // AccessPolicyUpsertOne is the builder for "upsert"-ing + // one AccessPolicy node. + AccessPolicyUpsertOne struct { + create *AccessPolicyCreate + } + + // AccessPolicyUpsert is the "OnConflict" setter. + AccessPolicyUpsert struct { + *sql.UpdateSet + } +) + +// SetName sets the "name" field. +func (u *AccessPolicyUpsert) SetName(v string) *AccessPolicyUpsert { + u.Set(accesspolicy.FieldName, v) + return u +} + +// UpdateName sets the "name" field to the value that was provided on create. +func (u *AccessPolicyUpsert) UpdateName() *AccessPolicyUpsert { + u.SetExcluded(accesspolicy.FieldName) + return u +} + +// SetDescription sets the "description" field. +func (u *AccessPolicyUpsert) SetDescription(v string) *AccessPolicyUpsert { + u.Set(accesspolicy.FieldDescription, v) + return u +} + +// UpdateDescription sets the "description" field to the value that was provided on create. +func (u *AccessPolicyUpsert) UpdateDescription() *AccessPolicyUpsert { + u.SetExcluded(accesspolicy.FieldDescription) + return u +} + +// ClearDescription clears the value of the "description" field. +func (u *AccessPolicyUpsert) ClearDescription() *AccessPolicyUpsert { + u.SetNull(accesspolicy.FieldDescription) + return u +} + +// SetScopeType sets the "scope_type" field. +func (u *AccessPolicyUpsert) SetScopeType(v accesspolicy.ScopeType) *AccessPolicyUpsert { + u.Set(accesspolicy.FieldScopeType, v) + return u +} + +// UpdateScopeType sets the "scope_type" field to the value that was provided on create. +func (u *AccessPolicyUpsert) UpdateScopeType() *AccessPolicyUpsert { + u.SetExcluded(accesspolicy.FieldScopeType) + return u +} + +// SetScopeID sets the "scope_id" field. +func (u *AccessPolicyUpsert) SetScopeID(v string) *AccessPolicyUpsert { + u.Set(accesspolicy.FieldScopeID, v) + return u +} + +// UpdateScopeID sets the "scope_id" field to the value that was provided on create. +func (u *AccessPolicyUpsert) UpdateScopeID() *AccessPolicyUpsert { + u.SetExcluded(accesspolicy.FieldScopeID) + return u +} + +// ClearScopeID clears the value of the "scope_id" field. +func (u *AccessPolicyUpsert) ClearScopeID() *AccessPolicyUpsert { + u.SetNull(accesspolicy.FieldScopeID) + return u +} + +// SetResourceType sets the "resource_type" field. +func (u *AccessPolicyUpsert) SetResourceType(v string) *AccessPolicyUpsert { + u.Set(accesspolicy.FieldResourceType, v) + return u +} + +// UpdateResourceType sets the "resource_type" field to the value that was provided on create. +func (u *AccessPolicyUpsert) UpdateResourceType() *AccessPolicyUpsert { + u.SetExcluded(accesspolicy.FieldResourceType) + return u +} + +// SetResourceID sets the "resource_id" field. +func (u *AccessPolicyUpsert) SetResourceID(v string) *AccessPolicyUpsert { + u.Set(accesspolicy.FieldResourceID, v) + return u +} + +// UpdateResourceID sets the "resource_id" field to the value that was provided on create. +func (u *AccessPolicyUpsert) UpdateResourceID() *AccessPolicyUpsert { + u.SetExcluded(accesspolicy.FieldResourceID) + return u +} + +// ClearResourceID clears the value of the "resource_id" field. +func (u *AccessPolicyUpsert) ClearResourceID() *AccessPolicyUpsert { + u.SetNull(accesspolicy.FieldResourceID) + return u +} + +// SetActions sets the "actions" field. +func (u *AccessPolicyUpsert) SetActions(v []string) *AccessPolicyUpsert { + u.Set(accesspolicy.FieldActions, v) + return u +} + +// UpdateActions sets the "actions" field to the value that was provided on create. +func (u *AccessPolicyUpsert) UpdateActions() *AccessPolicyUpsert { + u.SetExcluded(accesspolicy.FieldActions) + return u +} + +// ClearActions clears the value of the "actions" field. +func (u *AccessPolicyUpsert) ClearActions() *AccessPolicyUpsert { + u.SetNull(accesspolicy.FieldActions) + return u +} + +// SetEffect sets the "effect" field. +func (u *AccessPolicyUpsert) SetEffect(v accesspolicy.Effect) *AccessPolicyUpsert { + u.Set(accesspolicy.FieldEffect, v) + return u +} + +// UpdateEffect sets the "effect" field to the value that was provided on create. +func (u *AccessPolicyUpsert) UpdateEffect() *AccessPolicyUpsert { + u.SetExcluded(accesspolicy.FieldEffect) + return u +} + +// SetConditions sets the "conditions" field. +func (u *AccessPolicyUpsert) SetConditions(v *schema.PolicyConditions) *AccessPolicyUpsert { + u.Set(accesspolicy.FieldConditions, v) + return u +} + +// UpdateConditions sets the "conditions" field to the value that was provided on create. +func (u *AccessPolicyUpsert) UpdateConditions() *AccessPolicyUpsert { + u.SetExcluded(accesspolicy.FieldConditions) + return u +} + +// ClearConditions clears the value of the "conditions" field. +func (u *AccessPolicyUpsert) ClearConditions() *AccessPolicyUpsert { + u.SetNull(accesspolicy.FieldConditions) + return u +} + +// SetPriority sets the "priority" field. +func (u *AccessPolicyUpsert) SetPriority(v int) *AccessPolicyUpsert { + u.Set(accesspolicy.FieldPriority, v) + return u +} + +// UpdatePriority sets the "priority" field to the value that was provided on create. +func (u *AccessPolicyUpsert) UpdatePriority() *AccessPolicyUpsert { + u.SetExcluded(accesspolicy.FieldPriority) + return u +} + +// AddPriority adds v to the "priority" field. +func (u *AccessPolicyUpsert) AddPriority(v int) *AccessPolicyUpsert { + u.Add(accesspolicy.FieldPriority, v) + return u +} + +// SetLabels sets the "labels" field. +func (u *AccessPolicyUpsert) SetLabels(v map[string]string) *AccessPolicyUpsert { + u.Set(accesspolicy.FieldLabels, v) + return u +} + +// UpdateLabels sets the "labels" field to the value that was provided on create. +func (u *AccessPolicyUpsert) UpdateLabels() *AccessPolicyUpsert { + u.SetExcluded(accesspolicy.FieldLabels) + return u +} + +// ClearLabels clears the value of the "labels" field. +func (u *AccessPolicyUpsert) ClearLabels() *AccessPolicyUpsert { + u.SetNull(accesspolicy.FieldLabels) + return u +} + +// SetAnnotations sets the "annotations" field. +func (u *AccessPolicyUpsert) SetAnnotations(v map[string]string) *AccessPolicyUpsert { + u.Set(accesspolicy.FieldAnnotations, v) + return u +} + +// UpdateAnnotations sets the "annotations" field to the value that was provided on create. +func (u *AccessPolicyUpsert) UpdateAnnotations() *AccessPolicyUpsert { + u.SetExcluded(accesspolicy.FieldAnnotations) + return u +} + +// ClearAnnotations clears the value of the "annotations" field. +func (u *AccessPolicyUpsert) ClearAnnotations() *AccessPolicyUpsert { + u.SetNull(accesspolicy.FieldAnnotations) + return u +} + +// SetUpdated sets the "updated" field. +func (u *AccessPolicyUpsert) SetUpdated(v time.Time) *AccessPolicyUpsert { + u.Set(accesspolicy.FieldUpdated, v) + return u +} + +// UpdateUpdated sets the "updated" field to the value that was provided on create. +func (u *AccessPolicyUpsert) UpdateUpdated() *AccessPolicyUpsert { + u.SetExcluded(accesspolicy.FieldUpdated) + return u +} + +// SetCreatedBy sets the "created_by" field. +func (u *AccessPolicyUpsert) SetCreatedBy(v string) *AccessPolicyUpsert { + u.Set(accesspolicy.FieldCreatedBy, v) + return u +} + +// UpdateCreatedBy sets the "created_by" field to the value that was provided on create. +func (u *AccessPolicyUpsert) UpdateCreatedBy() *AccessPolicyUpsert { + u.SetExcluded(accesspolicy.FieldCreatedBy) + return u +} + +// ClearCreatedBy clears the value of the "created_by" field. +func (u *AccessPolicyUpsert) ClearCreatedBy() *AccessPolicyUpsert { + u.SetNull(accesspolicy.FieldCreatedBy) + return u +} + +// UpdateNewValues updates the mutable fields using the new values that were set on create except the ID field. +// Using this option is equivalent to using: +// +// client.AccessPolicy.Create(). +// OnConflict( +// sql.ResolveWithNewValues(), +// sql.ResolveWith(func(u *sql.UpdateSet) { +// u.SetIgnore(accesspolicy.FieldID) +// }), +// ). +// Exec(ctx) +func (u *AccessPolicyUpsertOne) UpdateNewValues() *AccessPolicyUpsertOne { + u.create.conflict = append(u.create.conflict, sql.ResolveWithNewValues()) + u.create.conflict = append(u.create.conflict, sql.ResolveWith(func(s *sql.UpdateSet) { + if _, exists := u.create.mutation.ID(); exists { + s.SetIgnore(accesspolicy.FieldID) + } + if _, exists := u.create.mutation.Created(); exists { + s.SetIgnore(accesspolicy.FieldCreated) + } + })) + return u +} + +// Ignore sets each column to itself in case of conflict. +// Using this option is equivalent to using: +// +// client.AccessPolicy.Create(). +// OnConflict(sql.ResolveWithIgnore()). +// Exec(ctx) +func (u *AccessPolicyUpsertOne) Ignore() *AccessPolicyUpsertOne { + u.create.conflict = append(u.create.conflict, sql.ResolveWithIgnore()) + return u +} + +// DoNothing configures the conflict_action to `DO NOTHING`. +// Supported only by SQLite and PostgreSQL. +func (u *AccessPolicyUpsertOne) DoNothing() *AccessPolicyUpsertOne { + u.create.conflict = append(u.create.conflict, sql.DoNothing()) + return u +} + +// Update allows overriding fields `UPDATE` values. See the AccessPolicyCreate.OnConflict +// documentation for more info. +func (u *AccessPolicyUpsertOne) Update(set func(*AccessPolicyUpsert)) *AccessPolicyUpsertOne { + u.create.conflict = append(u.create.conflict, sql.ResolveWith(func(update *sql.UpdateSet) { + set(&AccessPolicyUpsert{UpdateSet: update}) + })) + return u +} + +// SetName sets the "name" field. +func (u *AccessPolicyUpsertOne) SetName(v string) *AccessPolicyUpsertOne { + return u.Update(func(s *AccessPolicyUpsert) { + s.SetName(v) + }) +} + +// UpdateName sets the "name" field to the value that was provided on create. +func (u *AccessPolicyUpsertOne) UpdateName() *AccessPolicyUpsertOne { + return u.Update(func(s *AccessPolicyUpsert) { + s.UpdateName() + }) +} + +// SetDescription sets the "description" field. +func (u *AccessPolicyUpsertOne) SetDescription(v string) *AccessPolicyUpsertOne { + return u.Update(func(s *AccessPolicyUpsert) { + s.SetDescription(v) + }) +} + +// UpdateDescription sets the "description" field to the value that was provided on create. +func (u *AccessPolicyUpsertOne) UpdateDescription() *AccessPolicyUpsertOne { + return u.Update(func(s *AccessPolicyUpsert) { + s.UpdateDescription() + }) +} + +// ClearDescription clears the value of the "description" field. +func (u *AccessPolicyUpsertOne) ClearDescription() *AccessPolicyUpsertOne { + return u.Update(func(s *AccessPolicyUpsert) { + s.ClearDescription() + }) +} + +// SetScopeType sets the "scope_type" field. +func (u *AccessPolicyUpsertOne) SetScopeType(v accesspolicy.ScopeType) *AccessPolicyUpsertOne { + return u.Update(func(s *AccessPolicyUpsert) { + s.SetScopeType(v) + }) +} + +// UpdateScopeType sets the "scope_type" field to the value that was provided on create. +func (u *AccessPolicyUpsertOne) UpdateScopeType() *AccessPolicyUpsertOne { + return u.Update(func(s *AccessPolicyUpsert) { + s.UpdateScopeType() + }) +} + +// SetScopeID sets the "scope_id" field. +func (u *AccessPolicyUpsertOne) SetScopeID(v string) *AccessPolicyUpsertOne { + return u.Update(func(s *AccessPolicyUpsert) { + s.SetScopeID(v) + }) +} + +// UpdateScopeID sets the "scope_id" field to the value that was provided on create. +func (u *AccessPolicyUpsertOne) UpdateScopeID() *AccessPolicyUpsertOne { + return u.Update(func(s *AccessPolicyUpsert) { + s.UpdateScopeID() + }) +} + +// ClearScopeID clears the value of the "scope_id" field. +func (u *AccessPolicyUpsertOne) ClearScopeID() *AccessPolicyUpsertOne { + return u.Update(func(s *AccessPolicyUpsert) { + s.ClearScopeID() + }) +} + +// SetResourceType sets the "resource_type" field. +func (u *AccessPolicyUpsertOne) SetResourceType(v string) *AccessPolicyUpsertOne { + return u.Update(func(s *AccessPolicyUpsert) { + s.SetResourceType(v) + }) +} + +// UpdateResourceType sets the "resource_type" field to the value that was provided on create. +func (u *AccessPolicyUpsertOne) UpdateResourceType() *AccessPolicyUpsertOne { + return u.Update(func(s *AccessPolicyUpsert) { + s.UpdateResourceType() + }) +} + +// SetResourceID sets the "resource_id" field. +func (u *AccessPolicyUpsertOne) SetResourceID(v string) *AccessPolicyUpsertOne { + return u.Update(func(s *AccessPolicyUpsert) { + s.SetResourceID(v) + }) +} + +// UpdateResourceID sets the "resource_id" field to the value that was provided on create. +func (u *AccessPolicyUpsertOne) UpdateResourceID() *AccessPolicyUpsertOne { + return u.Update(func(s *AccessPolicyUpsert) { + s.UpdateResourceID() + }) +} + +// ClearResourceID clears the value of the "resource_id" field. +func (u *AccessPolicyUpsertOne) ClearResourceID() *AccessPolicyUpsertOne { + return u.Update(func(s *AccessPolicyUpsert) { + s.ClearResourceID() + }) +} + +// SetActions sets the "actions" field. +func (u *AccessPolicyUpsertOne) SetActions(v []string) *AccessPolicyUpsertOne { + return u.Update(func(s *AccessPolicyUpsert) { + s.SetActions(v) + }) +} + +// UpdateActions sets the "actions" field to the value that was provided on create. +func (u *AccessPolicyUpsertOne) UpdateActions() *AccessPolicyUpsertOne { + return u.Update(func(s *AccessPolicyUpsert) { + s.UpdateActions() + }) +} + +// ClearActions clears the value of the "actions" field. +func (u *AccessPolicyUpsertOne) ClearActions() *AccessPolicyUpsertOne { + return u.Update(func(s *AccessPolicyUpsert) { + s.ClearActions() + }) +} + +// SetEffect sets the "effect" field. +func (u *AccessPolicyUpsertOne) SetEffect(v accesspolicy.Effect) *AccessPolicyUpsertOne { + return u.Update(func(s *AccessPolicyUpsert) { + s.SetEffect(v) + }) +} + +// UpdateEffect sets the "effect" field to the value that was provided on create. +func (u *AccessPolicyUpsertOne) UpdateEffect() *AccessPolicyUpsertOne { + return u.Update(func(s *AccessPolicyUpsert) { + s.UpdateEffect() + }) +} + +// SetConditions sets the "conditions" field. +func (u *AccessPolicyUpsertOne) SetConditions(v *schema.PolicyConditions) *AccessPolicyUpsertOne { + return u.Update(func(s *AccessPolicyUpsert) { + s.SetConditions(v) + }) +} + +// UpdateConditions sets the "conditions" field to the value that was provided on create. +func (u *AccessPolicyUpsertOne) UpdateConditions() *AccessPolicyUpsertOne { + return u.Update(func(s *AccessPolicyUpsert) { + s.UpdateConditions() + }) +} + +// ClearConditions clears the value of the "conditions" field. +func (u *AccessPolicyUpsertOne) ClearConditions() *AccessPolicyUpsertOne { + return u.Update(func(s *AccessPolicyUpsert) { + s.ClearConditions() + }) +} + +// SetPriority sets the "priority" field. +func (u *AccessPolicyUpsertOne) SetPriority(v int) *AccessPolicyUpsertOne { + return u.Update(func(s *AccessPolicyUpsert) { + s.SetPriority(v) + }) +} + +// AddPriority adds v to the "priority" field. +func (u *AccessPolicyUpsertOne) AddPriority(v int) *AccessPolicyUpsertOne { + return u.Update(func(s *AccessPolicyUpsert) { + s.AddPriority(v) + }) +} + +// UpdatePriority sets the "priority" field to the value that was provided on create. +func (u *AccessPolicyUpsertOne) UpdatePriority() *AccessPolicyUpsertOne { + return u.Update(func(s *AccessPolicyUpsert) { + s.UpdatePriority() + }) +} + +// SetLabels sets the "labels" field. +func (u *AccessPolicyUpsertOne) SetLabels(v map[string]string) *AccessPolicyUpsertOne { + return u.Update(func(s *AccessPolicyUpsert) { + s.SetLabels(v) + }) +} + +// UpdateLabels sets the "labels" field to the value that was provided on create. +func (u *AccessPolicyUpsertOne) UpdateLabels() *AccessPolicyUpsertOne { + return u.Update(func(s *AccessPolicyUpsert) { + s.UpdateLabels() + }) +} + +// ClearLabels clears the value of the "labels" field. +func (u *AccessPolicyUpsertOne) ClearLabels() *AccessPolicyUpsertOne { + return u.Update(func(s *AccessPolicyUpsert) { + s.ClearLabels() + }) +} + +// SetAnnotations sets the "annotations" field. +func (u *AccessPolicyUpsertOne) SetAnnotations(v map[string]string) *AccessPolicyUpsertOne { + return u.Update(func(s *AccessPolicyUpsert) { + s.SetAnnotations(v) + }) +} + +// UpdateAnnotations sets the "annotations" field to the value that was provided on create. +func (u *AccessPolicyUpsertOne) UpdateAnnotations() *AccessPolicyUpsertOne { + return u.Update(func(s *AccessPolicyUpsert) { + s.UpdateAnnotations() + }) +} + +// ClearAnnotations clears the value of the "annotations" field. +func (u *AccessPolicyUpsertOne) ClearAnnotations() *AccessPolicyUpsertOne { + return u.Update(func(s *AccessPolicyUpsert) { + s.ClearAnnotations() + }) +} + +// SetUpdated sets the "updated" field. +func (u *AccessPolicyUpsertOne) SetUpdated(v time.Time) *AccessPolicyUpsertOne { + return u.Update(func(s *AccessPolicyUpsert) { + s.SetUpdated(v) + }) +} + +// UpdateUpdated sets the "updated" field to the value that was provided on create. +func (u *AccessPolicyUpsertOne) UpdateUpdated() *AccessPolicyUpsertOne { + return u.Update(func(s *AccessPolicyUpsert) { + s.UpdateUpdated() + }) +} + +// SetCreatedBy sets the "created_by" field. +func (u *AccessPolicyUpsertOne) SetCreatedBy(v string) *AccessPolicyUpsertOne { + return u.Update(func(s *AccessPolicyUpsert) { + s.SetCreatedBy(v) + }) +} + +// UpdateCreatedBy sets the "created_by" field to the value that was provided on create. +func (u *AccessPolicyUpsertOne) UpdateCreatedBy() *AccessPolicyUpsertOne { + return u.Update(func(s *AccessPolicyUpsert) { + s.UpdateCreatedBy() + }) +} + +// ClearCreatedBy clears the value of the "created_by" field. +func (u *AccessPolicyUpsertOne) ClearCreatedBy() *AccessPolicyUpsertOne { + return u.Update(func(s *AccessPolicyUpsert) { + s.ClearCreatedBy() + }) +} + +// Exec executes the query. +func (u *AccessPolicyUpsertOne) Exec(ctx context.Context) error { + if len(u.create.conflict) == 0 { + return errors.New("ent: missing options for AccessPolicyCreate.OnConflict") + } + return u.create.Exec(ctx) +} + +// ExecX is like Exec, but panics if an error occurs. +func (u *AccessPolicyUpsertOne) ExecX(ctx context.Context) { + if err := u.create.Exec(ctx); err != nil { + panic(err) + } +} + +// Exec executes the UPSERT query and returns the inserted/updated ID. +func (u *AccessPolicyUpsertOne) ID(ctx context.Context) (id uuid.UUID, err error) { + if u.create.driver.Dialect() == dialect.MySQL { + // In case of "ON CONFLICT", there is no way to get back non-numeric ID + // fields from the database since MySQL does not support the RETURNING clause. + return id, errors.New("ent: AccessPolicyUpsertOne.ID is not supported by MySQL driver. Use AccessPolicyUpsertOne.Exec instead") + } + node, err := u.create.Save(ctx) + if err != nil { + return id, err + } + return node.ID, nil +} + +// IDX is like ID, but panics if an error occurs. +func (u *AccessPolicyUpsertOne) IDX(ctx context.Context) uuid.UUID { + id, err := u.ID(ctx) + if err != nil { + panic(err) + } + return id +} + // AccessPolicyCreateBulk is the builder for creating many AccessPolicy entities in bulk. type AccessPolicyCreateBulk struct { config err error builders []*AccessPolicyCreate + conflict []sql.ConflictOption } // Save creates the AccessPolicy entities in the database. @@ -442,6 +1066,7 @@ func (_c *AccessPolicyCreateBulk) Save(ctx context.Context) ([]*AccessPolicy, er _, err = mutators[i+1].Mutate(root, _c.builders[i+1].mutation) } else { spec := &sqlgraph.BatchCreateSpec{Nodes: specs} + spec.OnConflict = _c.conflict // Invoke the actual operation on the latest mutation in the chain. if err = sqlgraph.BatchCreate(ctx, _c.driver, spec); err != nil { if sqlgraph.IsConstraintError(err) { @@ -491,3 +1116,379 @@ func (_c *AccessPolicyCreateBulk) ExecX(ctx context.Context) { panic(err) } } + +// OnConflict allows configuring the `ON CONFLICT` / `ON DUPLICATE KEY` clause +// of the `INSERT` statement. For example: +// +// client.AccessPolicy.CreateBulk(builders...). +// OnConflict( +// // Update the row with the new values +// // the was proposed for insertion. +// sql.ResolveWithNewValues(), +// ). +// // Override some of the fields with custom +// // update values. +// Update(func(u *ent.AccessPolicyUpsert) { +// SetName(v+v). +// }). +// Exec(ctx) +func (_c *AccessPolicyCreateBulk) OnConflict(opts ...sql.ConflictOption) *AccessPolicyUpsertBulk { + _c.conflict = opts + return &AccessPolicyUpsertBulk{ + create: _c, + } +} + +// OnConflictColumns calls `OnConflict` and configures the columns +// as conflict target. Using this option is equivalent to using: +// +// client.AccessPolicy.Create(). +// OnConflict(sql.ConflictColumns(columns...)). +// Exec(ctx) +func (_c *AccessPolicyCreateBulk) OnConflictColumns(columns ...string) *AccessPolicyUpsertBulk { + _c.conflict = append(_c.conflict, sql.ConflictColumns(columns...)) + return &AccessPolicyUpsertBulk{ + create: _c, + } +} + +// AccessPolicyUpsertBulk is the builder for "upsert"-ing +// a bulk of AccessPolicy nodes. +type AccessPolicyUpsertBulk struct { + create *AccessPolicyCreateBulk +} + +// UpdateNewValues updates the mutable fields using the new values that +// were set on create. Using this option is equivalent to using: +// +// client.AccessPolicy.Create(). +// OnConflict( +// sql.ResolveWithNewValues(), +// sql.ResolveWith(func(u *sql.UpdateSet) { +// u.SetIgnore(accesspolicy.FieldID) +// }), +// ). +// Exec(ctx) +func (u *AccessPolicyUpsertBulk) UpdateNewValues() *AccessPolicyUpsertBulk { + u.create.conflict = append(u.create.conflict, sql.ResolveWithNewValues()) + u.create.conflict = append(u.create.conflict, sql.ResolveWith(func(s *sql.UpdateSet) { + for _, b := range u.create.builders { + if _, exists := b.mutation.ID(); exists { + s.SetIgnore(accesspolicy.FieldID) + } + if _, exists := b.mutation.Created(); exists { + s.SetIgnore(accesspolicy.FieldCreated) + } + } + })) + return u +} + +// Ignore sets each column to itself in case of conflict. +// Using this option is equivalent to using: +// +// client.AccessPolicy.Create(). +// OnConflict(sql.ResolveWithIgnore()). +// Exec(ctx) +func (u *AccessPolicyUpsertBulk) Ignore() *AccessPolicyUpsertBulk { + u.create.conflict = append(u.create.conflict, sql.ResolveWithIgnore()) + return u +} + +// DoNothing configures the conflict_action to `DO NOTHING`. +// Supported only by SQLite and PostgreSQL. +func (u *AccessPolicyUpsertBulk) DoNothing() *AccessPolicyUpsertBulk { + u.create.conflict = append(u.create.conflict, sql.DoNothing()) + return u +} + +// Update allows overriding fields `UPDATE` values. See the AccessPolicyCreateBulk.OnConflict +// documentation for more info. +func (u *AccessPolicyUpsertBulk) Update(set func(*AccessPolicyUpsert)) *AccessPolicyUpsertBulk { + u.create.conflict = append(u.create.conflict, sql.ResolveWith(func(update *sql.UpdateSet) { + set(&AccessPolicyUpsert{UpdateSet: update}) + })) + return u +} + +// SetName sets the "name" field. +func (u *AccessPolicyUpsertBulk) SetName(v string) *AccessPolicyUpsertBulk { + return u.Update(func(s *AccessPolicyUpsert) { + s.SetName(v) + }) +} + +// UpdateName sets the "name" field to the value that was provided on create. +func (u *AccessPolicyUpsertBulk) UpdateName() *AccessPolicyUpsertBulk { + return u.Update(func(s *AccessPolicyUpsert) { + s.UpdateName() + }) +} + +// SetDescription sets the "description" field. +func (u *AccessPolicyUpsertBulk) SetDescription(v string) *AccessPolicyUpsertBulk { + return u.Update(func(s *AccessPolicyUpsert) { + s.SetDescription(v) + }) +} + +// UpdateDescription sets the "description" field to the value that was provided on create. +func (u *AccessPolicyUpsertBulk) UpdateDescription() *AccessPolicyUpsertBulk { + return u.Update(func(s *AccessPolicyUpsert) { + s.UpdateDescription() + }) +} + +// ClearDescription clears the value of the "description" field. +func (u *AccessPolicyUpsertBulk) ClearDescription() *AccessPolicyUpsertBulk { + return u.Update(func(s *AccessPolicyUpsert) { + s.ClearDescription() + }) +} + +// SetScopeType sets the "scope_type" field. +func (u *AccessPolicyUpsertBulk) SetScopeType(v accesspolicy.ScopeType) *AccessPolicyUpsertBulk { + return u.Update(func(s *AccessPolicyUpsert) { + s.SetScopeType(v) + }) +} + +// UpdateScopeType sets the "scope_type" field to the value that was provided on create. +func (u *AccessPolicyUpsertBulk) UpdateScopeType() *AccessPolicyUpsertBulk { + return u.Update(func(s *AccessPolicyUpsert) { + s.UpdateScopeType() + }) +} + +// SetScopeID sets the "scope_id" field. +func (u *AccessPolicyUpsertBulk) SetScopeID(v string) *AccessPolicyUpsertBulk { + return u.Update(func(s *AccessPolicyUpsert) { + s.SetScopeID(v) + }) +} + +// UpdateScopeID sets the "scope_id" field to the value that was provided on create. +func (u *AccessPolicyUpsertBulk) UpdateScopeID() *AccessPolicyUpsertBulk { + return u.Update(func(s *AccessPolicyUpsert) { + s.UpdateScopeID() + }) +} + +// ClearScopeID clears the value of the "scope_id" field. +func (u *AccessPolicyUpsertBulk) ClearScopeID() *AccessPolicyUpsertBulk { + return u.Update(func(s *AccessPolicyUpsert) { + s.ClearScopeID() + }) +} + +// SetResourceType sets the "resource_type" field. +func (u *AccessPolicyUpsertBulk) SetResourceType(v string) *AccessPolicyUpsertBulk { + return u.Update(func(s *AccessPolicyUpsert) { + s.SetResourceType(v) + }) +} + +// UpdateResourceType sets the "resource_type" field to the value that was provided on create. +func (u *AccessPolicyUpsertBulk) UpdateResourceType() *AccessPolicyUpsertBulk { + return u.Update(func(s *AccessPolicyUpsert) { + s.UpdateResourceType() + }) +} + +// SetResourceID sets the "resource_id" field. +func (u *AccessPolicyUpsertBulk) SetResourceID(v string) *AccessPolicyUpsertBulk { + return u.Update(func(s *AccessPolicyUpsert) { + s.SetResourceID(v) + }) +} + +// UpdateResourceID sets the "resource_id" field to the value that was provided on create. +func (u *AccessPolicyUpsertBulk) UpdateResourceID() *AccessPolicyUpsertBulk { + return u.Update(func(s *AccessPolicyUpsert) { + s.UpdateResourceID() + }) +} + +// ClearResourceID clears the value of the "resource_id" field. +func (u *AccessPolicyUpsertBulk) ClearResourceID() *AccessPolicyUpsertBulk { + return u.Update(func(s *AccessPolicyUpsert) { + s.ClearResourceID() + }) +} + +// SetActions sets the "actions" field. +func (u *AccessPolicyUpsertBulk) SetActions(v []string) *AccessPolicyUpsertBulk { + return u.Update(func(s *AccessPolicyUpsert) { + s.SetActions(v) + }) +} + +// UpdateActions sets the "actions" field to the value that was provided on create. +func (u *AccessPolicyUpsertBulk) UpdateActions() *AccessPolicyUpsertBulk { + return u.Update(func(s *AccessPolicyUpsert) { + s.UpdateActions() + }) +} + +// ClearActions clears the value of the "actions" field. +func (u *AccessPolicyUpsertBulk) ClearActions() *AccessPolicyUpsertBulk { + return u.Update(func(s *AccessPolicyUpsert) { + s.ClearActions() + }) +} + +// SetEffect sets the "effect" field. +func (u *AccessPolicyUpsertBulk) SetEffect(v accesspolicy.Effect) *AccessPolicyUpsertBulk { + return u.Update(func(s *AccessPolicyUpsert) { + s.SetEffect(v) + }) +} + +// UpdateEffect sets the "effect" field to the value that was provided on create. +func (u *AccessPolicyUpsertBulk) UpdateEffect() *AccessPolicyUpsertBulk { + return u.Update(func(s *AccessPolicyUpsert) { + s.UpdateEffect() + }) +} + +// SetConditions sets the "conditions" field. +func (u *AccessPolicyUpsertBulk) SetConditions(v *schema.PolicyConditions) *AccessPolicyUpsertBulk { + return u.Update(func(s *AccessPolicyUpsert) { + s.SetConditions(v) + }) +} + +// UpdateConditions sets the "conditions" field to the value that was provided on create. +func (u *AccessPolicyUpsertBulk) UpdateConditions() *AccessPolicyUpsertBulk { + return u.Update(func(s *AccessPolicyUpsert) { + s.UpdateConditions() + }) +} + +// ClearConditions clears the value of the "conditions" field. +func (u *AccessPolicyUpsertBulk) ClearConditions() *AccessPolicyUpsertBulk { + return u.Update(func(s *AccessPolicyUpsert) { + s.ClearConditions() + }) +} + +// SetPriority sets the "priority" field. +func (u *AccessPolicyUpsertBulk) SetPriority(v int) *AccessPolicyUpsertBulk { + return u.Update(func(s *AccessPolicyUpsert) { + s.SetPriority(v) + }) +} + +// AddPriority adds v to the "priority" field. +func (u *AccessPolicyUpsertBulk) AddPriority(v int) *AccessPolicyUpsertBulk { + return u.Update(func(s *AccessPolicyUpsert) { + s.AddPriority(v) + }) +} + +// UpdatePriority sets the "priority" field to the value that was provided on create. +func (u *AccessPolicyUpsertBulk) UpdatePriority() *AccessPolicyUpsertBulk { + return u.Update(func(s *AccessPolicyUpsert) { + s.UpdatePriority() + }) +} + +// SetLabels sets the "labels" field. +func (u *AccessPolicyUpsertBulk) SetLabels(v map[string]string) *AccessPolicyUpsertBulk { + return u.Update(func(s *AccessPolicyUpsert) { + s.SetLabels(v) + }) +} + +// UpdateLabels sets the "labels" field to the value that was provided on create. +func (u *AccessPolicyUpsertBulk) UpdateLabels() *AccessPolicyUpsertBulk { + return u.Update(func(s *AccessPolicyUpsert) { + s.UpdateLabels() + }) +} + +// ClearLabels clears the value of the "labels" field. +func (u *AccessPolicyUpsertBulk) ClearLabels() *AccessPolicyUpsertBulk { + return u.Update(func(s *AccessPolicyUpsert) { + s.ClearLabels() + }) +} + +// SetAnnotations sets the "annotations" field. +func (u *AccessPolicyUpsertBulk) SetAnnotations(v map[string]string) *AccessPolicyUpsertBulk { + return u.Update(func(s *AccessPolicyUpsert) { + s.SetAnnotations(v) + }) +} + +// UpdateAnnotations sets the "annotations" field to the value that was provided on create. +func (u *AccessPolicyUpsertBulk) UpdateAnnotations() *AccessPolicyUpsertBulk { + return u.Update(func(s *AccessPolicyUpsert) { + s.UpdateAnnotations() + }) +} + +// ClearAnnotations clears the value of the "annotations" field. +func (u *AccessPolicyUpsertBulk) ClearAnnotations() *AccessPolicyUpsertBulk { + return u.Update(func(s *AccessPolicyUpsert) { + s.ClearAnnotations() + }) +} + +// SetUpdated sets the "updated" field. +func (u *AccessPolicyUpsertBulk) SetUpdated(v time.Time) *AccessPolicyUpsertBulk { + return u.Update(func(s *AccessPolicyUpsert) { + s.SetUpdated(v) + }) +} + +// UpdateUpdated sets the "updated" field to the value that was provided on create. +func (u *AccessPolicyUpsertBulk) UpdateUpdated() *AccessPolicyUpsertBulk { + return u.Update(func(s *AccessPolicyUpsert) { + s.UpdateUpdated() + }) +} + +// SetCreatedBy sets the "created_by" field. +func (u *AccessPolicyUpsertBulk) SetCreatedBy(v string) *AccessPolicyUpsertBulk { + return u.Update(func(s *AccessPolicyUpsert) { + s.SetCreatedBy(v) + }) +} + +// UpdateCreatedBy sets the "created_by" field to the value that was provided on create. +func (u *AccessPolicyUpsertBulk) UpdateCreatedBy() *AccessPolicyUpsertBulk { + return u.Update(func(s *AccessPolicyUpsert) { + s.UpdateCreatedBy() + }) +} + +// ClearCreatedBy clears the value of the "created_by" field. +func (u *AccessPolicyUpsertBulk) ClearCreatedBy() *AccessPolicyUpsertBulk { + return u.Update(func(s *AccessPolicyUpsert) { + s.ClearCreatedBy() + }) +} + +// Exec executes the query. +func (u *AccessPolicyUpsertBulk) Exec(ctx context.Context) error { + if u.create.err != nil { + return u.create.err + } + for i, b := range u.create.builders { + if len(b.conflict) != 0 { + return fmt.Errorf("ent: OnConflict was set for builder %d. Set it on the AccessPolicyCreateBulk instead", i) + } + } + if len(u.create.conflict) == 0 { + return errors.New("ent: missing options for AccessPolicyCreateBulk.OnConflict") + } + return u.create.Exec(ctx) +} + +// ExecX is like Exec, but panics if an error occurs. +func (u *AccessPolicyUpsertBulk) ExecX(ctx context.Context) { + if err := u.create.Exec(ctx); err != nil { + panic(err) + } +} diff --git a/pkg/ent/accesspolicy_query.go b/pkg/ent/accesspolicy_query.go index 7fcfbe3aa..30d525f02 100644 --- a/pkg/ent/accesspolicy_query.go +++ b/pkg/ent/accesspolicy_query.go @@ -9,6 +9,7 @@ import ( "math" "entgo.io/ent" + "entgo.io/ent/dialect" "entgo.io/ent/dialect/sql" "entgo.io/ent/dialect/sql/sqlgraph" "entgo.io/ent/schema/field" @@ -26,6 +27,7 @@ type AccessPolicyQuery struct { inters []Interceptor predicates []predicate.AccessPolicy withBindings *PolicyBindingQuery + modifiers []func(*sql.Selector) // intermediate query (i.e. traversal path). sql *sql.Selector path func(context.Context) (*sql.Selector, error) @@ -385,6 +387,9 @@ func (_q *AccessPolicyQuery) sqlAll(ctx context.Context, hooks ...queryHook) ([] node.Edges.loadedTypes = loadedTypes return node.assignValues(columns, values) } + if len(_q.modifiers) > 0 { + _spec.Modifiers = _q.modifiers + } for i := range hooks { hooks[i](ctx, _spec) } @@ -440,6 +445,9 @@ func (_q *AccessPolicyQuery) loadBindings(ctx context.Context, query *PolicyBind func (_q *AccessPolicyQuery) sqlCount(ctx context.Context) (int, error) { _spec := _q.querySpec() + if len(_q.modifiers) > 0 { + _spec.Modifiers = _q.modifiers + } _spec.Node.Columns = _q.ctx.Fields if len(_q.ctx.Fields) > 0 { _spec.Unique = _q.ctx.Unique != nil && *_q.ctx.Unique @@ -502,6 +510,9 @@ func (_q *AccessPolicyQuery) sqlQuery(ctx context.Context) *sql.Selector { if _q.ctx.Unique != nil && *_q.ctx.Unique { selector.Distinct() } + for _, m := range _q.modifiers { + m(selector) + } for _, p := range _q.predicates { p(selector) } @@ -519,6 +530,32 @@ func (_q *AccessPolicyQuery) sqlQuery(ctx context.Context) *sql.Selector { return selector } +// ForUpdate locks the selected rows against concurrent updates, and prevent them from being +// updated, deleted or "selected ... for update" by other sessions, until the transaction is +// either committed or rolled-back. +func (_q *AccessPolicyQuery) ForUpdate(opts ...sql.LockOption) *AccessPolicyQuery { + if _q.driver.Dialect() == dialect.Postgres { + _q.Unique(false) + } + _q.modifiers = append(_q.modifiers, func(s *sql.Selector) { + s.ForUpdate(opts...) + }) + return _q +} + +// ForShare behaves similarly to ForUpdate, except that it acquires a shared mode lock +// on any rows that are read. Other sessions can read the rows, but cannot modify them +// until your transaction commits. +func (_q *AccessPolicyQuery) ForShare(opts ...sql.LockOption) *AccessPolicyQuery { + if _q.driver.Dialect() == dialect.Postgres { + _q.Unique(false) + } + _q.modifiers = append(_q.modifiers, func(s *sql.Selector) { + s.ForShare(opts...) + }) + return _q +} + // AccessPolicyGroupBy is the group-by builder for AccessPolicy entities. type AccessPolicyGroupBy struct { selector diff --git a/pkg/ent/agent.go b/pkg/ent/agent.go index 08b7bfa8c..504e312d7 100644 --- a/pkg/ent/agent.go +++ b/pkg/ent/agent.go @@ -3,6 +3,7 @@ package ent import ( + "encoding/json" "fmt" "strings" "time" @@ -11,7 +12,6 @@ import ( "entgo.io/ent/dialect/sql" "github.com/GoogleCloudPlatform/scion/pkg/ent/agent" "github.com/GoogleCloudPlatform/scion/pkg/ent/project" - "github.com/GoogleCloudPlatform/scion/pkg/ent/user" "github.com/google/uuid" ) @@ -38,10 +38,60 @@ type Agent struct { DelegationEnabled bool `json:"delegation_enabled,omitempty"` // Visibility holds the value of the "visibility" field. Visibility string `json:"visibility,omitempty"` + // Labels holds the value of the "labels" field. + Labels map[string]string `json:"labels,omitempty"` + // Annotations holds the value of the "annotations" field. + Annotations map[string]string `json:"annotations,omitempty"` + // Phase holds the value of the "phase" field. + Phase string `json:"phase,omitempty"` + // Activity holds the value of the "activity" field. + Activity string `json:"activity,omitempty"` + // ToolName holds the value of the "tool_name" field. + ToolName string `json:"tool_name,omitempty"` + // ConnectionState holds the value of the "connection_state" field. + ConnectionState string `json:"connection_state,omitempty"` + // ContainerStatus holds the value of the "container_status" field. + ContainerStatus string `json:"container_status,omitempty"` + // RuntimeState holds the value of the "runtime_state" field. + RuntimeState string `json:"runtime_state,omitempty"` + // StalledFromActivity holds the value of the "stalled_from_activity" field. + StalledFromActivity string `json:"stalled_from_activity,omitempty"` + // CurrentTurns holds the value of the "current_turns" field. + CurrentTurns int `json:"current_turns,omitempty"` + // CurrentModelCalls holds the value of the "current_model_calls" field. + CurrentModelCalls int `json:"current_model_calls,omitempty"` + // Image holds the value of the "image" field. + Image string `json:"image,omitempty"` + // Detached holds the value of the "detached" field. + Detached bool `json:"detached,omitempty"` + // Runtime holds the value of the "runtime" field. + Runtime string `json:"runtime,omitempty"` + // RuntimeBrokerID holds the value of the "runtime_broker_id" field. + RuntimeBrokerID string `json:"runtime_broker_id,omitempty"` + // WebPtyEnabled holds the value of the "web_pty_enabled" field. + WebPtyEnabled bool `json:"web_pty_enabled,omitempty"` + // TaskSummary holds the value of the "task_summary" field. + TaskSummary string `json:"task_summary,omitempty"` + // Message holds the value of the "message" field. + Message string `json:"message,omitempty"` + // AppliedConfig holds the value of the "applied_config" field. + AppliedConfig string `json:"applied_config,omitempty"` + // Ancestry holds the value of the "ancestry" field. + Ancestry []string `json:"ancestry,omitempty"` // Created holds the value of the "created" field. Created time.Time `json:"created,omitempty"` // Updated holds the value of the "updated" field. Updated time.Time `json:"updated,omitempty"` + // LastSeen holds the value of the "last_seen" field. + LastSeen *time.Time `json:"last_seen,omitempty"` + // LastActivityEvent holds the value of the "last_activity_event" field. + LastActivityEvent *time.Time `json:"last_activity_event,omitempty"` + // StartedAt holds the value of the "started_at" field. + StartedAt *time.Time `json:"started_at,omitempty"` + // DeletedAt holds the value of the "deleted_at" field. + DeletedAt *time.Time `json:"deleted_at,omitempty"` + // StateVersion holds the value of the "state_version" field. + StateVersion int64 `json:"state_version,omitempty"` // Edges holds the relations/edges for other nodes in the graph. // The values are being populated by the AgentQuery when eager-loading is set. Edges AgentEdges `json:"edges"` @@ -52,17 +102,13 @@ type Agent struct { type AgentEdges struct { // Project holds the value of the project edge. Project *Project `json:"project,omitempty"` - // Creator holds the value of the creator edge. - Creator *User `json:"creator,omitempty"` - // Owner holds the value of the owner edge. - Owner *User `json:"owner,omitempty"` // Memberships holds the value of the memberships edge. Memberships []*GroupMembership `json:"memberships,omitempty"` // PolicyBindings holds the value of the policy_bindings edge. PolicyBindings []*PolicyBinding `json:"policy_bindings,omitempty"` // loadedTypes holds the information for reporting if a // type was loaded (or requested) in eager-loading or not. - loadedTypes [5]bool + loadedTypes [3]bool } // ProjectOrErr returns the Project value or an error if the edge @@ -76,32 +122,10 @@ func (e AgentEdges) ProjectOrErr() (*Project, error) { return nil, &NotLoadedError{edge: "project"} } -// CreatorOrErr returns the Creator value or an error if the edge -// was not loaded in eager-loading, or loaded but was not found. -func (e AgentEdges) CreatorOrErr() (*User, error) { - if e.Creator != nil { - return e.Creator, nil - } else if e.loadedTypes[1] { - return nil, &NotFoundError{label: user.Label} - } - return nil, &NotLoadedError{edge: "creator"} -} - -// OwnerOrErr returns the Owner value or an error if the edge -// was not loaded in eager-loading, or loaded but was not found. -func (e AgentEdges) OwnerOrErr() (*User, error) { - if e.Owner != nil { - return e.Owner, nil - } else if e.loadedTypes[2] { - return nil, &NotFoundError{label: user.Label} - } - return nil, &NotLoadedError{edge: "owner"} -} - // MembershipsOrErr returns the Memberships value or an error if the edge // was not loaded in eager-loading. func (e AgentEdges) MembershipsOrErr() ([]*GroupMembership, error) { - if e.loadedTypes[3] { + if e.loadedTypes[1] { return e.Memberships, nil } return nil, &NotLoadedError{edge: "memberships"} @@ -110,7 +134,7 @@ func (e AgentEdges) MembershipsOrErr() ([]*GroupMembership, error) { // PolicyBindingsOrErr returns the PolicyBindings value or an error if the edge // was not loaded in eager-loading. func (e AgentEdges) PolicyBindingsOrErr() ([]*PolicyBinding, error) { - if e.loadedTypes[4] { + if e.loadedTypes[2] { return e.PolicyBindings, nil } return nil, &NotLoadedError{edge: "policy_bindings"} @@ -123,11 +147,15 @@ func (*Agent) scanValues(columns []string) ([]any, error) { switch columns[i] { case agent.FieldCreatedBy, agent.FieldOwnerID: values[i] = &sql.NullScanner{S: new(uuid.UUID)} - case agent.FieldDelegationEnabled: + case agent.FieldLabels, agent.FieldAnnotations, agent.FieldAncestry: + values[i] = new([]byte) + case agent.FieldDelegationEnabled, agent.FieldDetached, agent.FieldWebPtyEnabled: values[i] = new(sql.NullBool) - case agent.FieldSlug, agent.FieldName, agent.FieldTemplate, agent.FieldStatus, agent.FieldVisibility: + case agent.FieldCurrentTurns, agent.FieldCurrentModelCalls, agent.FieldStateVersion: + values[i] = new(sql.NullInt64) + case agent.FieldSlug, agent.FieldName, agent.FieldTemplate, agent.FieldStatus, agent.FieldVisibility, agent.FieldPhase, agent.FieldActivity, agent.FieldToolName, agent.FieldConnectionState, agent.FieldContainerStatus, agent.FieldRuntimeState, agent.FieldStalledFromActivity, agent.FieldImage, agent.FieldRuntime, agent.FieldRuntimeBrokerID, agent.FieldTaskSummary, agent.FieldMessage, agent.FieldAppliedConfig: values[i] = new(sql.NullString) - case agent.FieldCreated, agent.FieldUpdated: + case agent.FieldCreated, agent.FieldUpdated, agent.FieldLastSeen, agent.FieldLastActivityEvent, agent.FieldStartedAt, agent.FieldDeletedAt: values[i] = new(sql.NullTime) case agent.FieldID, agent.FieldProjectID: values[i] = new(uuid.UUID) @@ -208,6 +236,132 @@ func (_m *Agent) assignValues(columns []string, values []any) error { } else if value.Valid { _m.Visibility = value.String } + case agent.FieldLabels: + if value, ok := values[i].(*[]byte); !ok { + return fmt.Errorf("unexpected type %T for field labels", values[i]) + } else if value != nil && len(*value) > 0 { + if err := json.Unmarshal(*value, &_m.Labels); err != nil { + return fmt.Errorf("unmarshal field labels: %w", err) + } + } + case agent.FieldAnnotations: + if value, ok := values[i].(*[]byte); !ok { + return fmt.Errorf("unexpected type %T for field annotations", values[i]) + } else if value != nil && len(*value) > 0 { + if err := json.Unmarshal(*value, &_m.Annotations); err != nil { + return fmt.Errorf("unmarshal field annotations: %w", err) + } + } + case agent.FieldPhase: + if value, ok := values[i].(*sql.NullString); !ok { + return fmt.Errorf("unexpected type %T for field phase", values[i]) + } else if value.Valid { + _m.Phase = value.String + } + case agent.FieldActivity: + if value, ok := values[i].(*sql.NullString); !ok { + return fmt.Errorf("unexpected type %T for field activity", values[i]) + } else if value.Valid { + _m.Activity = value.String + } + case agent.FieldToolName: + if value, ok := values[i].(*sql.NullString); !ok { + return fmt.Errorf("unexpected type %T for field tool_name", values[i]) + } else if value.Valid { + _m.ToolName = value.String + } + case agent.FieldConnectionState: + if value, ok := values[i].(*sql.NullString); !ok { + return fmt.Errorf("unexpected type %T for field connection_state", values[i]) + } else if value.Valid { + _m.ConnectionState = value.String + } + case agent.FieldContainerStatus: + if value, ok := values[i].(*sql.NullString); !ok { + return fmt.Errorf("unexpected type %T for field container_status", values[i]) + } else if value.Valid { + _m.ContainerStatus = value.String + } + case agent.FieldRuntimeState: + if value, ok := values[i].(*sql.NullString); !ok { + return fmt.Errorf("unexpected type %T for field runtime_state", values[i]) + } else if value.Valid { + _m.RuntimeState = value.String + } + case agent.FieldStalledFromActivity: + if value, ok := values[i].(*sql.NullString); !ok { + return fmt.Errorf("unexpected type %T for field stalled_from_activity", values[i]) + } else if value.Valid { + _m.StalledFromActivity = value.String + } + case agent.FieldCurrentTurns: + if value, ok := values[i].(*sql.NullInt64); !ok { + return fmt.Errorf("unexpected type %T for field current_turns", values[i]) + } else if value.Valid { + _m.CurrentTurns = int(value.Int64) + } + case agent.FieldCurrentModelCalls: + if value, ok := values[i].(*sql.NullInt64); !ok { + return fmt.Errorf("unexpected type %T for field current_model_calls", values[i]) + } else if value.Valid { + _m.CurrentModelCalls = int(value.Int64) + } + case agent.FieldImage: + if value, ok := values[i].(*sql.NullString); !ok { + return fmt.Errorf("unexpected type %T for field image", values[i]) + } else if value.Valid { + _m.Image = value.String + } + case agent.FieldDetached: + if value, ok := values[i].(*sql.NullBool); !ok { + return fmt.Errorf("unexpected type %T for field detached", values[i]) + } else if value.Valid { + _m.Detached = value.Bool + } + case agent.FieldRuntime: + if value, ok := values[i].(*sql.NullString); !ok { + return fmt.Errorf("unexpected type %T for field runtime", values[i]) + } else if value.Valid { + _m.Runtime = value.String + } + case agent.FieldRuntimeBrokerID: + if value, ok := values[i].(*sql.NullString); !ok { + return fmt.Errorf("unexpected type %T for field runtime_broker_id", values[i]) + } else if value.Valid { + _m.RuntimeBrokerID = value.String + } + case agent.FieldWebPtyEnabled: + if value, ok := values[i].(*sql.NullBool); !ok { + return fmt.Errorf("unexpected type %T for field web_pty_enabled", values[i]) + } else if value.Valid { + _m.WebPtyEnabled = value.Bool + } + case agent.FieldTaskSummary: + if value, ok := values[i].(*sql.NullString); !ok { + return fmt.Errorf("unexpected type %T for field task_summary", values[i]) + } else if value.Valid { + _m.TaskSummary = value.String + } + case agent.FieldMessage: + if value, ok := values[i].(*sql.NullString); !ok { + return fmt.Errorf("unexpected type %T for field message", values[i]) + } else if value.Valid { + _m.Message = value.String + } + case agent.FieldAppliedConfig: + if value, ok := values[i].(*sql.NullString); !ok { + return fmt.Errorf("unexpected type %T for field applied_config", values[i]) + } else if value.Valid { + _m.AppliedConfig = value.String + } + case agent.FieldAncestry: + if value, ok := values[i].(*[]byte); !ok { + return fmt.Errorf("unexpected type %T for field ancestry", values[i]) + } else if value != nil && len(*value) > 0 { + if err := json.Unmarshal(*value, &_m.Ancestry); err != nil { + return fmt.Errorf("unmarshal field ancestry: %w", err) + } + } case agent.FieldCreated: if value, ok := values[i].(*sql.NullTime); !ok { return fmt.Errorf("unexpected type %T for field created", values[i]) @@ -220,6 +374,40 @@ func (_m *Agent) assignValues(columns []string, values []any) error { } else if value.Valid { _m.Updated = value.Time } + case agent.FieldLastSeen: + if value, ok := values[i].(*sql.NullTime); !ok { + return fmt.Errorf("unexpected type %T for field last_seen", values[i]) + } else if value.Valid { + _m.LastSeen = new(time.Time) + *_m.LastSeen = value.Time + } + case agent.FieldLastActivityEvent: + if value, ok := values[i].(*sql.NullTime); !ok { + return fmt.Errorf("unexpected type %T for field last_activity_event", values[i]) + } else if value.Valid { + _m.LastActivityEvent = new(time.Time) + *_m.LastActivityEvent = value.Time + } + case agent.FieldStartedAt: + if value, ok := values[i].(*sql.NullTime); !ok { + return fmt.Errorf("unexpected type %T for field started_at", values[i]) + } else if value.Valid { + _m.StartedAt = new(time.Time) + *_m.StartedAt = value.Time + } + case agent.FieldDeletedAt: + if value, ok := values[i].(*sql.NullTime); !ok { + return fmt.Errorf("unexpected type %T for field deleted_at", values[i]) + } else if value.Valid { + _m.DeletedAt = new(time.Time) + *_m.DeletedAt = value.Time + } + case agent.FieldStateVersion: + if value, ok := values[i].(*sql.NullInt64); !ok { + return fmt.Errorf("unexpected type %T for field state_version", values[i]) + } else if value.Valid { + _m.StateVersion = value.Int64 + } default: _m.selectValues.Set(columns[i], values[i]) } @@ -238,16 +426,6 @@ func (_m *Agent) QueryProject() *ProjectQuery { return NewAgentClient(_m.config).QueryProject(_m) } -// QueryCreator queries the "creator" edge of the Agent entity. -func (_m *Agent) QueryCreator() *UserQuery { - return NewAgentClient(_m.config).QueryCreator(_m) -} - -// QueryOwner queries the "owner" edge of the Agent entity. -func (_m *Agent) QueryOwner() *UserQuery { - return NewAgentClient(_m.config).QueryOwner(_m) -} - // QueryMemberships queries the "memberships" edge of the Agent entity. func (_m *Agent) QueryMemberships() *GroupMembershipQuery { return NewAgentClient(_m.config).QueryMemberships(_m) @@ -312,11 +490,94 @@ func (_m *Agent) String() string { builder.WriteString("visibility=") builder.WriteString(_m.Visibility) builder.WriteString(", ") + builder.WriteString("labels=") + builder.WriteString(fmt.Sprintf("%v", _m.Labels)) + builder.WriteString(", ") + builder.WriteString("annotations=") + builder.WriteString(fmt.Sprintf("%v", _m.Annotations)) + builder.WriteString(", ") + builder.WriteString("phase=") + builder.WriteString(_m.Phase) + builder.WriteString(", ") + builder.WriteString("activity=") + builder.WriteString(_m.Activity) + builder.WriteString(", ") + builder.WriteString("tool_name=") + builder.WriteString(_m.ToolName) + builder.WriteString(", ") + builder.WriteString("connection_state=") + builder.WriteString(_m.ConnectionState) + builder.WriteString(", ") + builder.WriteString("container_status=") + builder.WriteString(_m.ContainerStatus) + builder.WriteString(", ") + builder.WriteString("runtime_state=") + builder.WriteString(_m.RuntimeState) + builder.WriteString(", ") + builder.WriteString("stalled_from_activity=") + builder.WriteString(_m.StalledFromActivity) + builder.WriteString(", ") + builder.WriteString("current_turns=") + builder.WriteString(fmt.Sprintf("%v", _m.CurrentTurns)) + builder.WriteString(", ") + builder.WriteString("current_model_calls=") + builder.WriteString(fmt.Sprintf("%v", _m.CurrentModelCalls)) + builder.WriteString(", ") + builder.WriteString("image=") + builder.WriteString(_m.Image) + builder.WriteString(", ") + builder.WriteString("detached=") + builder.WriteString(fmt.Sprintf("%v", _m.Detached)) + builder.WriteString(", ") + builder.WriteString("runtime=") + builder.WriteString(_m.Runtime) + builder.WriteString(", ") + builder.WriteString("runtime_broker_id=") + builder.WriteString(_m.RuntimeBrokerID) + builder.WriteString(", ") + builder.WriteString("web_pty_enabled=") + builder.WriteString(fmt.Sprintf("%v", _m.WebPtyEnabled)) + builder.WriteString(", ") + builder.WriteString("task_summary=") + builder.WriteString(_m.TaskSummary) + builder.WriteString(", ") + builder.WriteString("message=") + builder.WriteString(_m.Message) + builder.WriteString(", ") + builder.WriteString("applied_config=") + builder.WriteString(_m.AppliedConfig) + builder.WriteString(", ") + builder.WriteString("ancestry=") + builder.WriteString(fmt.Sprintf("%v", _m.Ancestry)) + builder.WriteString(", ") builder.WriteString("created=") builder.WriteString(_m.Created.Format(time.ANSIC)) builder.WriteString(", ") builder.WriteString("updated=") builder.WriteString(_m.Updated.Format(time.ANSIC)) + builder.WriteString(", ") + if v := _m.LastSeen; v != nil { + builder.WriteString("last_seen=") + builder.WriteString(v.Format(time.ANSIC)) + } + builder.WriteString(", ") + if v := _m.LastActivityEvent; v != nil { + builder.WriteString("last_activity_event=") + builder.WriteString(v.Format(time.ANSIC)) + } + builder.WriteString(", ") + if v := _m.StartedAt; v != nil { + builder.WriteString("started_at=") + builder.WriteString(v.Format(time.ANSIC)) + } + builder.WriteString(", ") + if v := _m.DeletedAt; v != nil { + builder.WriteString("deleted_at=") + builder.WriteString(v.Format(time.ANSIC)) + } + builder.WriteString(", ") + builder.WriteString("state_version=") + builder.WriteString(fmt.Sprintf("%v", _m.StateVersion)) builder.WriteByte(')') return builder.String() } diff --git a/pkg/ent/agent/agent.go b/pkg/ent/agent/agent.go index 32a0b9297..d73f8ccfa 100644 --- a/pkg/ent/agent/agent.go +++ b/pkg/ent/agent/agent.go @@ -34,16 +34,62 @@ const ( FieldDelegationEnabled = "delegation_enabled" // FieldVisibility holds the string denoting the visibility field in the database. FieldVisibility = "visibility" + // FieldLabels holds the string denoting the labels field in the database. + FieldLabels = "labels" + // FieldAnnotations holds the string denoting the annotations field in the database. + FieldAnnotations = "annotations" + // FieldPhase holds the string denoting the phase field in the database. + FieldPhase = "phase" + // FieldActivity holds the string denoting the activity field in the database. + FieldActivity = "activity" + // FieldToolName holds the string denoting the tool_name field in the database. + FieldToolName = "tool_name" + // FieldConnectionState holds the string denoting the connection_state field in the database. + FieldConnectionState = "connection_state" + // FieldContainerStatus holds the string denoting the container_status field in the database. + FieldContainerStatus = "container_status" + // FieldRuntimeState holds the string denoting the runtime_state field in the database. + FieldRuntimeState = "runtime_state" + // FieldStalledFromActivity holds the string denoting the stalled_from_activity field in the database. + FieldStalledFromActivity = "stalled_from_activity" + // FieldCurrentTurns holds the string denoting the current_turns field in the database. + FieldCurrentTurns = "current_turns" + // FieldCurrentModelCalls holds the string denoting the current_model_calls field in the database. + FieldCurrentModelCalls = "current_model_calls" + // FieldImage holds the string denoting the image field in the database. + FieldImage = "image" + // FieldDetached holds the string denoting the detached field in the database. + FieldDetached = "detached" + // FieldRuntime holds the string denoting the runtime field in the database. + FieldRuntime = "runtime" + // FieldRuntimeBrokerID holds the string denoting the runtime_broker_id field in the database. + FieldRuntimeBrokerID = "runtime_broker_id" + // FieldWebPtyEnabled holds the string denoting the web_pty_enabled field in the database. + FieldWebPtyEnabled = "web_pty_enabled" + // FieldTaskSummary holds the string denoting the task_summary field in the database. + FieldTaskSummary = "task_summary" + // FieldMessage holds the string denoting the message field in the database. + FieldMessage = "message" + // FieldAppliedConfig holds the string denoting the applied_config field in the database. + FieldAppliedConfig = "applied_config" + // FieldAncestry holds the string denoting the ancestry field in the database. + FieldAncestry = "ancestry" // FieldCreated holds the string denoting the created field in the database. FieldCreated = "created" // FieldUpdated holds the string denoting the updated field in the database. FieldUpdated = "updated" + // FieldLastSeen holds the string denoting the last_seen field in the database. + FieldLastSeen = "last_seen" + // FieldLastActivityEvent holds the string denoting the last_activity_event field in the database. + FieldLastActivityEvent = "last_activity_event" + // FieldStartedAt holds the string denoting the started_at field in the database. + FieldStartedAt = "started_at" + // FieldDeletedAt holds the string denoting the deleted_at field in the database. + FieldDeletedAt = "deleted_at" + // FieldStateVersion holds the string denoting the state_version field in the database. + FieldStateVersion = "state_version" // EdgeProject holds the string denoting the project edge name in mutations. EdgeProject = "project" - // EdgeCreator holds the string denoting the creator edge name in mutations. - EdgeCreator = "creator" - // EdgeOwner holds the string denoting the owner edge name in mutations. - EdgeOwner = "owner" // EdgeMemberships holds the string denoting the memberships edge name in mutations. EdgeMemberships = "memberships" // EdgePolicyBindings holds the string denoting the policy_bindings edge name in mutations. @@ -57,20 +103,6 @@ const ( ProjectInverseTable = "projects" // ProjectColumn is the table column denoting the project relation/edge. ProjectColumn = "project_id" - // CreatorTable is the table that holds the creator relation/edge. - CreatorTable = "agents" - // CreatorInverseTable is the table name for the User entity. - // It exists in this package in order to avoid circular dependency with the "user" package. - CreatorInverseTable = "users" - // CreatorColumn is the table column denoting the creator relation/edge. - CreatorColumn = "created_by" - // OwnerTable is the table that holds the owner relation/edge. - OwnerTable = "agents" - // OwnerInverseTable is the table name for the User entity. - // It exists in this package in order to avoid circular dependency with the "user" package. - OwnerInverseTable = "users" - // OwnerColumn is the table column denoting the owner relation/edge. - OwnerColumn = "owner_id" // MembershipsTable is the table that holds the memberships relation/edge. MembershipsTable = "group_memberships" // MembershipsInverseTable is the table name for the GroupMembership entity. @@ -99,8 +131,33 @@ var Columns = []string{ FieldOwnerID, FieldDelegationEnabled, FieldVisibility, + FieldLabels, + FieldAnnotations, + FieldPhase, + FieldActivity, + FieldToolName, + FieldConnectionState, + FieldContainerStatus, + FieldRuntimeState, + FieldStalledFromActivity, + FieldCurrentTurns, + FieldCurrentModelCalls, + FieldImage, + FieldDetached, + FieldRuntime, + FieldRuntimeBrokerID, + FieldWebPtyEnabled, + FieldTaskSummary, + FieldMessage, + FieldAppliedConfig, + FieldAncestry, FieldCreated, FieldUpdated, + FieldLastSeen, + FieldLastActivityEvent, + FieldStartedAt, + FieldDeletedAt, + FieldStateVersion, } // ValidColumn reports if the column name is valid (part of the table columns). @@ -122,12 +179,22 @@ var ( DefaultDelegationEnabled bool // DefaultVisibility holds the default value on creation for the "visibility" field. DefaultVisibility string + // DefaultCurrentTurns holds the default value on creation for the "current_turns" field. + DefaultCurrentTurns int + // DefaultCurrentModelCalls holds the default value on creation for the "current_model_calls" field. + DefaultCurrentModelCalls int + // DefaultDetached holds the default value on creation for the "detached" field. + DefaultDetached bool + // DefaultWebPtyEnabled holds the default value on creation for the "web_pty_enabled" field. + DefaultWebPtyEnabled bool // DefaultCreated holds the default value on creation for the "created" field. DefaultCreated func() time.Time // DefaultUpdated holds the default value on creation for the "updated" field. DefaultUpdated func() time.Time // UpdateDefaultUpdated holds the default value on update for the "updated" field. UpdateDefaultUpdated func() time.Time + // DefaultStateVersion holds the default value on creation for the "state_version" field. + DefaultStateVersion int64 // DefaultID holds the default value on creation for the "id" field. DefaultID func() uuid.UUID ) @@ -218,6 +285,91 @@ func ByVisibility(opts ...sql.OrderTermOption) OrderOption { return sql.OrderByField(FieldVisibility, opts...).ToFunc() } +// ByPhase orders the results by the phase field. +func ByPhase(opts ...sql.OrderTermOption) OrderOption { + return sql.OrderByField(FieldPhase, opts...).ToFunc() +} + +// ByActivity orders the results by the activity field. +func ByActivity(opts ...sql.OrderTermOption) OrderOption { + return sql.OrderByField(FieldActivity, opts...).ToFunc() +} + +// ByToolName orders the results by the tool_name field. +func ByToolName(opts ...sql.OrderTermOption) OrderOption { + return sql.OrderByField(FieldToolName, opts...).ToFunc() +} + +// ByConnectionState orders the results by the connection_state field. +func ByConnectionState(opts ...sql.OrderTermOption) OrderOption { + return sql.OrderByField(FieldConnectionState, opts...).ToFunc() +} + +// ByContainerStatus orders the results by the container_status field. +func ByContainerStatus(opts ...sql.OrderTermOption) OrderOption { + return sql.OrderByField(FieldContainerStatus, opts...).ToFunc() +} + +// ByRuntimeState orders the results by the runtime_state field. +func ByRuntimeState(opts ...sql.OrderTermOption) OrderOption { + return sql.OrderByField(FieldRuntimeState, opts...).ToFunc() +} + +// ByStalledFromActivity orders the results by the stalled_from_activity field. +func ByStalledFromActivity(opts ...sql.OrderTermOption) OrderOption { + return sql.OrderByField(FieldStalledFromActivity, opts...).ToFunc() +} + +// ByCurrentTurns orders the results by the current_turns field. +func ByCurrentTurns(opts ...sql.OrderTermOption) OrderOption { + return sql.OrderByField(FieldCurrentTurns, opts...).ToFunc() +} + +// ByCurrentModelCalls orders the results by the current_model_calls field. +func ByCurrentModelCalls(opts ...sql.OrderTermOption) OrderOption { + return sql.OrderByField(FieldCurrentModelCalls, opts...).ToFunc() +} + +// ByImage orders the results by the image field. +func ByImage(opts ...sql.OrderTermOption) OrderOption { + return sql.OrderByField(FieldImage, opts...).ToFunc() +} + +// ByDetached orders the results by the detached field. +func ByDetached(opts ...sql.OrderTermOption) OrderOption { + return sql.OrderByField(FieldDetached, opts...).ToFunc() +} + +// ByRuntime orders the results by the runtime field. +func ByRuntime(opts ...sql.OrderTermOption) OrderOption { + return sql.OrderByField(FieldRuntime, opts...).ToFunc() +} + +// ByRuntimeBrokerID orders the results by the runtime_broker_id field. +func ByRuntimeBrokerID(opts ...sql.OrderTermOption) OrderOption { + return sql.OrderByField(FieldRuntimeBrokerID, opts...).ToFunc() +} + +// ByWebPtyEnabled orders the results by the web_pty_enabled field. +func ByWebPtyEnabled(opts ...sql.OrderTermOption) OrderOption { + return sql.OrderByField(FieldWebPtyEnabled, opts...).ToFunc() +} + +// ByTaskSummary orders the results by the task_summary field. +func ByTaskSummary(opts ...sql.OrderTermOption) OrderOption { + return sql.OrderByField(FieldTaskSummary, opts...).ToFunc() +} + +// ByMessage orders the results by the message field. +func ByMessage(opts ...sql.OrderTermOption) OrderOption { + return sql.OrderByField(FieldMessage, opts...).ToFunc() +} + +// ByAppliedConfig orders the results by the applied_config field. +func ByAppliedConfig(opts ...sql.OrderTermOption) OrderOption { + return sql.OrderByField(FieldAppliedConfig, opts...).ToFunc() +} + // ByCreated orders the results by the created field. func ByCreated(opts ...sql.OrderTermOption) OrderOption { return sql.OrderByField(FieldCreated, opts...).ToFunc() @@ -228,24 +380,35 @@ func ByUpdated(opts ...sql.OrderTermOption) OrderOption { return sql.OrderByField(FieldUpdated, opts...).ToFunc() } -// ByProjectField orders the results by project field. -func ByProjectField(field string, opts ...sql.OrderTermOption) OrderOption { - return func(s *sql.Selector) { - sqlgraph.OrderByNeighborTerms(s, newProjectStep(), sql.OrderByField(field, opts...)) - } +// ByLastSeen orders the results by the last_seen field. +func ByLastSeen(opts ...sql.OrderTermOption) OrderOption { + return sql.OrderByField(FieldLastSeen, opts...).ToFunc() } -// ByCreatorField orders the results by creator field. -func ByCreatorField(field string, opts ...sql.OrderTermOption) OrderOption { - return func(s *sql.Selector) { - sqlgraph.OrderByNeighborTerms(s, newCreatorStep(), sql.OrderByField(field, opts...)) - } +// ByLastActivityEvent orders the results by the last_activity_event field. +func ByLastActivityEvent(opts ...sql.OrderTermOption) OrderOption { + return sql.OrderByField(FieldLastActivityEvent, opts...).ToFunc() } -// ByOwnerField orders the results by owner field. -func ByOwnerField(field string, opts ...sql.OrderTermOption) OrderOption { +// ByStartedAt orders the results by the started_at field. +func ByStartedAt(opts ...sql.OrderTermOption) OrderOption { + return sql.OrderByField(FieldStartedAt, opts...).ToFunc() +} + +// ByDeletedAt orders the results by the deleted_at field. +func ByDeletedAt(opts ...sql.OrderTermOption) OrderOption { + return sql.OrderByField(FieldDeletedAt, opts...).ToFunc() +} + +// ByStateVersion orders the results by the state_version field. +func ByStateVersion(opts ...sql.OrderTermOption) OrderOption { + return sql.OrderByField(FieldStateVersion, opts...).ToFunc() +} + +// ByProjectField orders the results by project field. +func ByProjectField(field string, opts ...sql.OrderTermOption) OrderOption { return func(s *sql.Selector) { - sqlgraph.OrderByNeighborTerms(s, newOwnerStep(), sql.OrderByField(field, opts...)) + sqlgraph.OrderByNeighborTerms(s, newProjectStep(), sql.OrderByField(field, opts...)) } } @@ -283,20 +446,6 @@ func newProjectStep() *sqlgraph.Step { sqlgraph.Edge(sqlgraph.M2O, true, ProjectTable, ProjectColumn), ) } -func newCreatorStep() *sqlgraph.Step { - return sqlgraph.NewStep( - sqlgraph.From(Table, FieldID), - sqlgraph.To(CreatorInverseTable, FieldID), - sqlgraph.Edge(sqlgraph.M2O, true, CreatorTable, CreatorColumn), - ) -} -func newOwnerStep() *sqlgraph.Step { - return sqlgraph.NewStep( - sqlgraph.From(Table, FieldID), - sqlgraph.To(OwnerInverseTable, FieldID), - sqlgraph.Edge(sqlgraph.M2O, true, OwnerTable, OwnerColumn), - ) -} func newMembershipsStep() *sqlgraph.Step { return sqlgraph.NewStep( sqlgraph.From(Table, FieldID), diff --git a/pkg/ent/agent/where.go b/pkg/ent/agent/where.go index 1ed475ed1..9c85b0908 100644 --- a/pkg/ent/agent/where.go +++ b/pkg/ent/agent/where.go @@ -96,6 +96,91 @@ func Visibility(v string) predicate.Agent { return predicate.Agent(sql.FieldEQ(FieldVisibility, v)) } +// Phase applies equality check predicate on the "phase" field. It's identical to PhaseEQ. +func Phase(v string) predicate.Agent { + return predicate.Agent(sql.FieldEQ(FieldPhase, v)) +} + +// Activity applies equality check predicate on the "activity" field. It's identical to ActivityEQ. +func Activity(v string) predicate.Agent { + return predicate.Agent(sql.FieldEQ(FieldActivity, v)) +} + +// ToolName applies equality check predicate on the "tool_name" field. It's identical to ToolNameEQ. +func ToolName(v string) predicate.Agent { + return predicate.Agent(sql.FieldEQ(FieldToolName, v)) +} + +// ConnectionState applies equality check predicate on the "connection_state" field. It's identical to ConnectionStateEQ. +func ConnectionState(v string) predicate.Agent { + return predicate.Agent(sql.FieldEQ(FieldConnectionState, v)) +} + +// ContainerStatus applies equality check predicate on the "container_status" field. It's identical to ContainerStatusEQ. +func ContainerStatus(v string) predicate.Agent { + return predicate.Agent(sql.FieldEQ(FieldContainerStatus, v)) +} + +// RuntimeState applies equality check predicate on the "runtime_state" field. It's identical to RuntimeStateEQ. +func RuntimeState(v string) predicate.Agent { + return predicate.Agent(sql.FieldEQ(FieldRuntimeState, v)) +} + +// StalledFromActivity applies equality check predicate on the "stalled_from_activity" field. It's identical to StalledFromActivityEQ. +func StalledFromActivity(v string) predicate.Agent { + return predicate.Agent(sql.FieldEQ(FieldStalledFromActivity, v)) +} + +// CurrentTurns applies equality check predicate on the "current_turns" field. It's identical to CurrentTurnsEQ. +func CurrentTurns(v int) predicate.Agent { + return predicate.Agent(sql.FieldEQ(FieldCurrentTurns, v)) +} + +// CurrentModelCalls applies equality check predicate on the "current_model_calls" field. It's identical to CurrentModelCallsEQ. +func CurrentModelCalls(v int) predicate.Agent { + return predicate.Agent(sql.FieldEQ(FieldCurrentModelCalls, v)) +} + +// Image applies equality check predicate on the "image" field. It's identical to ImageEQ. +func Image(v string) predicate.Agent { + return predicate.Agent(sql.FieldEQ(FieldImage, v)) +} + +// Detached applies equality check predicate on the "detached" field. It's identical to DetachedEQ. +func Detached(v bool) predicate.Agent { + return predicate.Agent(sql.FieldEQ(FieldDetached, v)) +} + +// Runtime applies equality check predicate on the "runtime" field. It's identical to RuntimeEQ. +func Runtime(v string) predicate.Agent { + return predicate.Agent(sql.FieldEQ(FieldRuntime, v)) +} + +// RuntimeBrokerID applies equality check predicate on the "runtime_broker_id" field. It's identical to RuntimeBrokerIDEQ. +func RuntimeBrokerID(v string) predicate.Agent { + return predicate.Agent(sql.FieldEQ(FieldRuntimeBrokerID, v)) +} + +// WebPtyEnabled applies equality check predicate on the "web_pty_enabled" field. It's identical to WebPtyEnabledEQ. +func WebPtyEnabled(v bool) predicate.Agent { + return predicate.Agent(sql.FieldEQ(FieldWebPtyEnabled, v)) +} + +// TaskSummary applies equality check predicate on the "task_summary" field. It's identical to TaskSummaryEQ. +func TaskSummary(v string) predicate.Agent { + return predicate.Agent(sql.FieldEQ(FieldTaskSummary, v)) +} + +// Message applies equality check predicate on the "message" field. It's identical to MessageEQ. +func Message(v string) predicate.Agent { + return predicate.Agent(sql.FieldEQ(FieldMessage, v)) +} + +// AppliedConfig applies equality check predicate on the "applied_config" field. It's identical to AppliedConfigEQ. +func AppliedConfig(v string) predicate.Agent { + return predicate.Agent(sql.FieldEQ(FieldAppliedConfig, v)) +} + // Created applies equality check predicate on the "created" field. It's identical to CreatedEQ. func Created(v time.Time) predicate.Agent { return predicate.Agent(sql.FieldEQ(FieldCreated, v)) @@ -106,6 +191,31 @@ func Updated(v time.Time) predicate.Agent { return predicate.Agent(sql.FieldEQ(FieldUpdated, v)) } +// LastSeen applies equality check predicate on the "last_seen" field. It's identical to LastSeenEQ. +func LastSeen(v time.Time) predicate.Agent { + return predicate.Agent(sql.FieldEQ(FieldLastSeen, v)) +} + +// LastActivityEvent applies equality check predicate on the "last_activity_event" field. It's identical to LastActivityEventEQ. +func LastActivityEvent(v time.Time) predicate.Agent { + return predicate.Agent(sql.FieldEQ(FieldLastActivityEvent, v)) +} + +// StartedAt applies equality check predicate on the "started_at" field. It's identical to StartedAtEQ. +func StartedAt(v time.Time) predicate.Agent { + return predicate.Agent(sql.FieldEQ(FieldStartedAt, v)) +} + +// DeletedAt applies equality check predicate on the "deleted_at" field. It's identical to DeletedAtEQ. +func DeletedAt(v time.Time) predicate.Agent { + return predicate.Agent(sql.FieldEQ(FieldDeletedAt, v)) +} + +// StateVersion applies equality check predicate on the "state_version" field. It's identical to StateVersionEQ. +func StateVersion(v int64) predicate.Agent { + return predicate.Agent(sql.FieldEQ(FieldStateVersion, v)) +} + // SlugEQ applies the EQ predicate on the "slug" field. func SlugEQ(v string) predicate.Agent { return predicate.Agent(sql.FieldEQ(FieldSlug, v)) @@ -371,6 +481,26 @@ func CreatedByNotIn(vs ...uuid.UUID) predicate.Agent { return predicate.Agent(sql.FieldNotIn(FieldCreatedBy, vs...)) } +// CreatedByGT applies the GT predicate on the "created_by" field. +func CreatedByGT(v uuid.UUID) predicate.Agent { + return predicate.Agent(sql.FieldGT(FieldCreatedBy, v)) +} + +// CreatedByGTE applies the GTE predicate on the "created_by" field. +func CreatedByGTE(v uuid.UUID) predicate.Agent { + return predicate.Agent(sql.FieldGTE(FieldCreatedBy, v)) +} + +// CreatedByLT applies the LT predicate on the "created_by" field. +func CreatedByLT(v uuid.UUID) predicate.Agent { + return predicate.Agent(sql.FieldLT(FieldCreatedBy, v)) +} + +// CreatedByLTE applies the LTE predicate on the "created_by" field. +func CreatedByLTE(v uuid.UUID) predicate.Agent { + return predicate.Agent(sql.FieldLTE(FieldCreatedBy, v)) +} + // CreatedByIsNil applies the IsNil predicate on the "created_by" field. func CreatedByIsNil() predicate.Agent { return predicate.Agent(sql.FieldIsNull(FieldCreatedBy)) @@ -401,6 +531,26 @@ func OwnerIDNotIn(vs ...uuid.UUID) predicate.Agent { return predicate.Agent(sql.FieldNotIn(FieldOwnerID, vs...)) } +// OwnerIDGT applies the GT predicate on the "owner_id" field. +func OwnerIDGT(v uuid.UUID) predicate.Agent { + return predicate.Agent(sql.FieldGT(FieldOwnerID, v)) +} + +// OwnerIDGTE applies the GTE predicate on the "owner_id" field. +func OwnerIDGTE(v uuid.UUID) predicate.Agent { + return predicate.Agent(sql.FieldGTE(FieldOwnerID, v)) +} + +// OwnerIDLT applies the LT predicate on the "owner_id" field. +func OwnerIDLT(v uuid.UUID) predicate.Agent { + return predicate.Agent(sql.FieldLT(FieldOwnerID, v)) +} + +// OwnerIDLTE applies the LTE predicate on the "owner_id" field. +func OwnerIDLTE(v uuid.UUID) predicate.Agent { + return predicate.Agent(sql.FieldLTE(FieldOwnerID, v)) +} + // OwnerIDIsNil applies the IsNil predicate on the "owner_id" field. func OwnerIDIsNil() predicate.Agent { return predicate.Agent(sql.FieldIsNull(FieldOwnerID)) @@ -486,147 +636,1446 @@ func VisibilityContainsFold(v string) predicate.Agent { return predicate.Agent(sql.FieldContainsFold(FieldVisibility, v)) } -// CreatedEQ applies the EQ predicate on the "created" field. -func CreatedEQ(v time.Time) predicate.Agent { - return predicate.Agent(sql.FieldEQ(FieldCreated, v)) +// LabelsIsNil applies the IsNil predicate on the "labels" field. +func LabelsIsNil() predicate.Agent { + return predicate.Agent(sql.FieldIsNull(FieldLabels)) } -// CreatedNEQ applies the NEQ predicate on the "created" field. -func CreatedNEQ(v time.Time) predicate.Agent { - return predicate.Agent(sql.FieldNEQ(FieldCreated, v)) +// LabelsNotNil applies the NotNil predicate on the "labels" field. +func LabelsNotNil() predicate.Agent { + return predicate.Agent(sql.FieldNotNull(FieldLabels)) } -// CreatedIn applies the In predicate on the "created" field. -func CreatedIn(vs ...time.Time) predicate.Agent { - return predicate.Agent(sql.FieldIn(FieldCreated, vs...)) +// AnnotationsIsNil applies the IsNil predicate on the "annotations" field. +func AnnotationsIsNil() predicate.Agent { + return predicate.Agent(sql.FieldIsNull(FieldAnnotations)) } -// CreatedNotIn applies the NotIn predicate on the "created" field. -func CreatedNotIn(vs ...time.Time) predicate.Agent { - return predicate.Agent(sql.FieldNotIn(FieldCreated, vs...)) +// AnnotationsNotNil applies the NotNil predicate on the "annotations" field. +func AnnotationsNotNil() predicate.Agent { + return predicate.Agent(sql.FieldNotNull(FieldAnnotations)) } -// CreatedGT applies the GT predicate on the "created" field. -func CreatedGT(v time.Time) predicate.Agent { - return predicate.Agent(sql.FieldGT(FieldCreated, v)) +// PhaseEQ applies the EQ predicate on the "phase" field. +func PhaseEQ(v string) predicate.Agent { + return predicate.Agent(sql.FieldEQ(FieldPhase, v)) } -// CreatedGTE applies the GTE predicate on the "created" field. -func CreatedGTE(v time.Time) predicate.Agent { - return predicate.Agent(sql.FieldGTE(FieldCreated, v)) +// PhaseNEQ applies the NEQ predicate on the "phase" field. +func PhaseNEQ(v string) predicate.Agent { + return predicate.Agent(sql.FieldNEQ(FieldPhase, v)) } -// CreatedLT applies the LT predicate on the "created" field. -func CreatedLT(v time.Time) predicate.Agent { - return predicate.Agent(sql.FieldLT(FieldCreated, v)) +// PhaseIn applies the In predicate on the "phase" field. +func PhaseIn(vs ...string) predicate.Agent { + return predicate.Agent(sql.FieldIn(FieldPhase, vs...)) } -// CreatedLTE applies the LTE predicate on the "created" field. -func CreatedLTE(v time.Time) predicate.Agent { - return predicate.Agent(sql.FieldLTE(FieldCreated, v)) +// PhaseNotIn applies the NotIn predicate on the "phase" field. +func PhaseNotIn(vs ...string) predicate.Agent { + return predicate.Agent(sql.FieldNotIn(FieldPhase, vs...)) } -// UpdatedEQ applies the EQ predicate on the "updated" field. -func UpdatedEQ(v time.Time) predicate.Agent { - return predicate.Agent(sql.FieldEQ(FieldUpdated, v)) +// PhaseGT applies the GT predicate on the "phase" field. +func PhaseGT(v string) predicate.Agent { + return predicate.Agent(sql.FieldGT(FieldPhase, v)) } -// UpdatedNEQ applies the NEQ predicate on the "updated" field. -func UpdatedNEQ(v time.Time) predicate.Agent { - return predicate.Agent(sql.FieldNEQ(FieldUpdated, v)) +// PhaseGTE applies the GTE predicate on the "phase" field. +func PhaseGTE(v string) predicate.Agent { + return predicate.Agent(sql.FieldGTE(FieldPhase, v)) } -// UpdatedIn applies the In predicate on the "updated" field. -func UpdatedIn(vs ...time.Time) predicate.Agent { - return predicate.Agent(sql.FieldIn(FieldUpdated, vs...)) +// PhaseLT applies the LT predicate on the "phase" field. +func PhaseLT(v string) predicate.Agent { + return predicate.Agent(sql.FieldLT(FieldPhase, v)) } -// UpdatedNotIn applies the NotIn predicate on the "updated" field. -func UpdatedNotIn(vs ...time.Time) predicate.Agent { - return predicate.Agent(sql.FieldNotIn(FieldUpdated, vs...)) +// PhaseLTE applies the LTE predicate on the "phase" field. +func PhaseLTE(v string) predicate.Agent { + return predicate.Agent(sql.FieldLTE(FieldPhase, v)) } -// UpdatedGT applies the GT predicate on the "updated" field. -func UpdatedGT(v time.Time) predicate.Agent { - return predicate.Agent(sql.FieldGT(FieldUpdated, v)) +// PhaseContains applies the Contains predicate on the "phase" field. +func PhaseContains(v string) predicate.Agent { + return predicate.Agent(sql.FieldContains(FieldPhase, v)) } -// UpdatedGTE applies the GTE predicate on the "updated" field. -func UpdatedGTE(v time.Time) predicate.Agent { - return predicate.Agent(sql.FieldGTE(FieldUpdated, v)) +// PhaseHasPrefix applies the HasPrefix predicate on the "phase" field. +func PhaseHasPrefix(v string) predicate.Agent { + return predicate.Agent(sql.FieldHasPrefix(FieldPhase, v)) } -// UpdatedLT applies the LT predicate on the "updated" field. -func UpdatedLT(v time.Time) predicate.Agent { - return predicate.Agent(sql.FieldLT(FieldUpdated, v)) +// PhaseHasSuffix applies the HasSuffix predicate on the "phase" field. +func PhaseHasSuffix(v string) predicate.Agent { + return predicate.Agent(sql.FieldHasSuffix(FieldPhase, v)) } -// UpdatedLTE applies the LTE predicate on the "updated" field. -func UpdatedLTE(v time.Time) predicate.Agent { - return predicate.Agent(sql.FieldLTE(FieldUpdated, v)) +// PhaseIsNil applies the IsNil predicate on the "phase" field. +func PhaseIsNil() predicate.Agent { + return predicate.Agent(sql.FieldIsNull(FieldPhase)) } -// HasProject applies the HasEdge predicate on the "project" edge. -func HasProject() predicate.Agent { - return predicate.Agent(func(s *sql.Selector) { - step := sqlgraph.NewStep( - sqlgraph.From(Table, FieldID), - sqlgraph.Edge(sqlgraph.M2O, true, ProjectTable, ProjectColumn), - ) - sqlgraph.HasNeighbors(s, step) - }) +// PhaseNotNil applies the NotNil predicate on the "phase" field. +func PhaseNotNil() predicate.Agent { + return predicate.Agent(sql.FieldNotNull(FieldPhase)) } -// HasProjectWith applies the HasEdge predicate on the "project" edge with a given conditions (other predicates). -func HasProjectWith(preds ...predicate.Project) predicate.Agent { - return predicate.Agent(func(s *sql.Selector) { - step := newProjectStep() - sqlgraph.HasNeighborsWith(s, step, func(s *sql.Selector) { - for _, p := range preds { - p(s) - } - }) - }) +// PhaseEqualFold applies the EqualFold predicate on the "phase" field. +func PhaseEqualFold(v string) predicate.Agent { + return predicate.Agent(sql.FieldEqualFold(FieldPhase, v)) } -// HasCreator applies the HasEdge predicate on the "creator" edge. -func HasCreator() predicate.Agent { - return predicate.Agent(func(s *sql.Selector) { - step := sqlgraph.NewStep( - sqlgraph.From(Table, FieldID), - sqlgraph.Edge(sqlgraph.M2O, true, CreatorTable, CreatorColumn), - ) - sqlgraph.HasNeighbors(s, step) - }) +// PhaseContainsFold applies the ContainsFold predicate on the "phase" field. +func PhaseContainsFold(v string) predicate.Agent { + return predicate.Agent(sql.FieldContainsFold(FieldPhase, v)) } -// HasCreatorWith applies the HasEdge predicate on the "creator" edge with a given conditions (other predicates). -func HasCreatorWith(preds ...predicate.User) predicate.Agent { - return predicate.Agent(func(s *sql.Selector) { - step := newCreatorStep() - sqlgraph.HasNeighborsWith(s, step, func(s *sql.Selector) { - for _, p := range preds { - p(s) - } - }) - }) +// ActivityEQ applies the EQ predicate on the "activity" field. +func ActivityEQ(v string) predicate.Agent { + return predicate.Agent(sql.FieldEQ(FieldActivity, v)) } -// HasOwner applies the HasEdge predicate on the "owner" edge. -func HasOwner() predicate.Agent { - return predicate.Agent(func(s *sql.Selector) { - step := sqlgraph.NewStep( - sqlgraph.From(Table, FieldID), - sqlgraph.Edge(sqlgraph.M2O, true, OwnerTable, OwnerColumn), - ) - sqlgraph.HasNeighbors(s, step) - }) +// ActivityNEQ applies the NEQ predicate on the "activity" field. +func ActivityNEQ(v string) predicate.Agent { + return predicate.Agent(sql.FieldNEQ(FieldActivity, v)) } -// HasOwnerWith applies the HasEdge predicate on the "owner" edge with a given conditions (other predicates). -func HasOwnerWith(preds ...predicate.User) predicate.Agent { - return predicate.Agent(func(s *sql.Selector) { - step := newOwnerStep() +// ActivityIn applies the In predicate on the "activity" field. +func ActivityIn(vs ...string) predicate.Agent { + return predicate.Agent(sql.FieldIn(FieldActivity, vs...)) +} + +// ActivityNotIn applies the NotIn predicate on the "activity" field. +func ActivityNotIn(vs ...string) predicate.Agent { + return predicate.Agent(sql.FieldNotIn(FieldActivity, vs...)) +} + +// ActivityGT applies the GT predicate on the "activity" field. +func ActivityGT(v string) predicate.Agent { + return predicate.Agent(sql.FieldGT(FieldActivity, v)) +} + +// ActivityGTE applies the GTE predicate on the "activity" field. +func ActivityGTE(v string) predicate.Agent { + return predicate.Agent(sql.FieldGTE(FieldActivity, v)) +} + +// ActivityLT applies the LT predicate on the "activity" field. +func ActivityLT(v string) predicate.Agent { + return predicate.Agent(sql.FieldLT(FieldActivity, v)) +} + +// ActivityLTE applies the LTE predicate on the "activity" field. +func ActivityLTE(v string) predicate.Agent { + return predicate.Agent(sql.FieldLTE(FieldActivity, v)) +} + +// ActivityContains applies the Contains predicate on the "activity" field. +func ActivityContains(v string) predicate.Agent { + return predicate.Agent(sql.FieldContains(FieldActivity, v)) +} + +// ActivityHasPrefix applies the HasPrefix predicate on the "activity" field. +func ActivityHasPrefix(v string) predicate.Agent { + return predicate.Agent(sql.FieldHasPrefix(FieldActivity, v)) +} + +// ActivityHasSuffix applies the HasSuffix predicate on the "activity" field. +func ActivityHasSuffix(v string) predicate.Agent { + return predicate.Agent(sql.FieldHasSuffix(FieldActivity, v)) +} + +// ActivityIsNil applies the IsNil predicate on the "activity" field. +func ActivityIsNil() predicate.Agent { + return predicate.Agent(sql.FieldIsNull(FieldActivity)) +} + +// ActivityNotNil applies the NotNil predicate on the "activity" field. +func ActivityNotNil() predicate.Agent { + return predicate.Agent(sql.FieldNotNull(FieldActivity)) +} + +// ActivityEqualFold applies the EqualFold predicate on the "activity" field. +func ActivityEqualFold(v string) predicate.Agent { + return predicate.Agent(sql.FieldEqualFold(FieldActivity, v)) +} + +// ActivityContainsFold applies the ContainsFold predicate on the "activity" field. +func ActivityContainsFold(v string) predicate.Agent { + return predicate.Agent(sql.FieldContainsFold(FieldActivity, v)) +} + +// ToolNameEQ applies the EQ predicate on the "tool_name" field. +func ToolNameEQ(v string) predicate.Agent { + return predicate.Agent(sql.FieldEQ(FieldToolName, v)) +} + +// ToolNameNEQ applies the NEQ predicate on the "tool_name" field. +func ToolNameNEQ(v string) predicate.Agent { + return predicate.Agent(sql.FieldNEQ(FieldToolName, v)) +} + +// ToolNameIn applies the In predicate on the "tool_name" field. +func ToolNameIn(vs ...string) predicate.Agent { + return predicate.Agent(sql.FieldIn(FieldToolName, vs...)) +} + +// ToolNameNotIn applies the NotIn predicate on the "tool_name" field. +func ToolNameNotIn(vs ...string) predicate.Agent { + return predicate.Agent(sql.FieldNotIn(FieldToolName, vs...)) +} + +// ToolNameGT applies the GT predicate on the "tool_name" field. +func ToolNameGT(v string) predicate.Agent { + return predicate.Agent(sql.FieldGT(FieldToolName, v)) +} + +// ToolNameGTE applies the GTE predicate on the "tool_name" field. +func ToolNameGTE(v string) predicate.Agent { + return predicate.Agent(sql.FieldGTE(FieldToolName, v)) +} + +// ToolNameLT applies the LT predicate on the "tool_name" field. +func ToolNameLT(v string) predicate.Agent { + return predicate.Agent(sql.FieldLT(FieldToolName, v)) +} + +// ToolNameLTE applies the LTE predicate on the "tool_name" field. +func ToolNameLTE(v string) predicate.Agent { + return predicate.Agent(sql.FieldLTE(FieldToolName, v)) +} + +// ToolNameContains applies the Contains predicate on the "tool_name" field. +func ToolNameContains(v string) predicate.Agent { + return predicate.Agent(sql.FieldContains(FieldToolName, v)) +} + +// ToolNameHasPrefix applies the HasPrefix predicate on the "tool_name" field. +func ToolNameHasPrefix(v string) predicate.Agent { + return predicate.Agent(sql.FieldHasPrefix(FieldToolName, v)) +} + +// ToolNameHasSuffix applies the HasSuffix predicate on the "tool_name" field. +func ToolNameHasSuffix(v string) predicate.Agent { + return predicate.Agent(sql.FieldHasSuffix(FieldToolName, v)) +} + +// ToolNameIsNil applies the IsNil predicate on the "tool_name" field. +func ToolNameIsNil() predicate.Agent { + return predicate.Agent(sql.FieldIsNull(FieldToolName)) +} + +// ToolNameNotNil applies the NotNil predicate on the "tool_name" field. +func ToolNameNotNil() predicate.Agent { + return predicate.Agent(sql.FieldNotNull(FieldToolName)) +} + +// ToolNameEqualFold applies the EqualFold predicate on the "tool_name" field. +func ToolNameEqualFold(v string) predicate.Agent { + return predicate.Agent(sql.FieldEqualFold(FieldToolName, v)) +} + +// ToolNameContainsFold applies the ContainsFold predicate on the "tool_name" field. +func ToolNameContainsFold(v string) predicate.Agent { + return predicate.Agent(sql.FieldContainsFold(FieldToolName, v)) +} + +// ConnectionStateEQ applies the EQ predicate on the "connection_state" field. +func ConnectionStateEQ(v string) predicate.Agent { + return predicate.Agent(sql.FieldEQ(FieldConnectionState, v)) +} + +// ConnectionStateNEQ applies the NEQ predicate on the "connection_state" field. +func ConnectionStateNEQ(v string) predicate.Agent { + return predicate.Agent(sql.FieldNEQ(FieldConnectionState, v)) +} + +// ConnectionStateIn applies the In predicate on the "connection_state" field. +func ConnectionStateIn(vs ...string) predicate.Agent { + return predicate.Agent(sql.FieldIn(FieldConnectionState, vs...)) +} + +// ConnectionStateNotIn applies the NotIn predicate on the "connection_state" field. +func ConnectionStateNotIn(vs ...string) predicate.Agent { + return predicate.Agent(sql.FieldNotIn(FieldConnectionState, vs...)) +} + +// ConnectionStateGT applies the GT predicate on the "connection_state" field. +func ConnectionStateGT(v string) predicate.Agent { + return predicate.Agent(sql.FieldGT(FieldConnectionState, v)) +} + +// ConnectionStateGTE applies the GTE predicate on the "connection_state" field. +func ConnectionStateGTE(v string) predicate.Agent { + return predicate.Agent(sql.FieldGTE(FieldConnectionState, v)) +} + +// ConnectionStateLT applies the LT predicate on the "connection_state" field. +func ConnectionStateLT(v string) predicate.Agent { + return predicate.Agent(sql.FieldLT(FieldConnectionState, v)) +} + +// ConnectionStateLTE applies the LTE predicate on the "connection_state" field. +func ConnectionStateLTE(v string) predicate.Agent { + return predicate.Agent(sql.FieldLTE(FieldConnectionState, v)) +} + +// ConnectionStateContains applies the Contains predicate on the "connection_state" field. +func ConnectionStateContains(v string) predicate.Agent { + return predicate.Agent(sql.FieldContains(FieldConnectionState, v)) +} + +// ConnectionStateHasPrefix applies the HasPrefix predicate on the "connection_state" field. +func ConnectionStateHasPrefix(v string) predicate.Agent { + return predicate.Agent(sql.FieldHasPrefix(FieldConnectionState, v)) +} + +// ConnectionStateHasSuffix applies the HasSuffix predicate on the "connection_state" field. +func ConnectionStateHasSuffix(v string) predicate.Agent { + return predicate.Agent(sql.FieldHasSuffix(FieldConnectionState, v)) +} + +// ConnectionStateIsNil applies the IsNil predicate on the "connection_state" field. +func ConnectionStateIsNil() predicate.Agent { + return predicate.Agent(sql.FieldIsNull(FieldConnectionState)) +} + +// ConnectionStateNotNil applies the NotNil predicate on the "connection_state" field. +func ConnectionStateNotNil() predicate.Agent { + return predicate.Agent(sql.FieldNotNull(FieldConnectionState)) +} + +// ConnectionStateEqualFold applies the EqualFold predicate on the "connection_state" field. +func ConnectionStateEqualFold(v string) predicate.Agent { + return predicate.Agent(sql.FieldEqualFold(FieldConnectionState, v)) +} + +// ConnectionStateContainsFold applies the ContainsFold predicate on the "connection_state" field. +func ConnectionStateContainsFold(v string) predicate.Agent { + return predicate.Agent(sql.FieldContainsFold(FieldConnectionState, v)) +} + +// ContainerStatusEQ applies the EQ predicate on the "container_status" field. +func ContainerStatusEQ(v string) predicate.Agent { + return predicate.Agent(sql.FieldEQ(FieldContainerStatus, v)) +} + +// ContainerStatusNEQ applies the NEQ predicate on the "container_status" field. +func ContainerStatusNEQ(v string) predicate.Agent { + return predicate.Agent(sql.FieldNEQ(FieldContainerStatus, v)) +} + +// ContainerStatusIn applies the In predicate on the "container_status" field. +func ContainerStatusIn(vs ...string) predicate.Agent { + return predicate.Agent(sql.FieldIn(FieldContainerStatus, vs...)) +} + +// ContainerStatusNotIn applies the NotIn predicate on the "container_status" field. +func ContainerStatusNotIn(vs ...string) predicate.Agent { + return predicate.Agent(sql.FieldNotIn(FieldContainerStatus, vs...)) +} + +// ContainerStatusGT applies the GT predicate on the "container_status" field. +func ContainerStatusGT(v string) predicate.Agent { + return predicate.Agent(sql.FieldGT(FieldContainerStatus, v)) +} + +// ContainerStatusGTE applies the GTE predicate on the "container_status" field. +func ContainerStatusGTE(v string) predicate.Agent { + return predicate.Agent(sql.FieldGTE(FieldContainerStatus, v)) +} + +// ContainerStatusLT applies the LT predicate on the "container_status" field. +func ContainerStatusLT(v string) predicate.Agent { + return predicate.Agent(sql.FieldLT(FieldContainerStatus, v)) +} + +// ContainerStatusLTE applies the LTE predicate on the "container_status" field. +func ContainerStatusLTE(v string) predicate.Agent { + return predicate.Agent(sql.FieldLTE(FieldContainerStatus, v)) +} + +// ContainerStatusContains applies the Contains predicate on the "container_status" field. +func ContainerStatusContains(v string) predicate.Agent { + return predicate.Agent(sql.FieldContains(FieldContainerStatus, v)) +} + +// ContainerStatusHasPrefix applies the HasPrefix predicate on the "container_status" field. +func ContainerStatusHasPrefix(v string) predicate.Agent { + return predicate.Agent(sql.FieldHasPrefix(FieldContainerStatus, v)) +} + +// ContainerStatusHasSuffix applies the HasSuffix predicate on the "container_status" field. +func ContainerStatusHasSuffix(v string) predicate.Agent { + return predicate.Agent(sql.FieldHasSuffix(FieldContainerStatus, v)) +} + +// ContainerStatusIsNil applies the IsNil predicate on the "container_status" field. +func ContainerStatusIsNil() predicate.Agent { + return predicate.Agent(sql.FieldIsNull(FieldContainerStatus)) +} + +// ContainerStatusNotNil applies the NotNil predicate on the "container_status" field. +func ContainerStatusNotNil() predicate.Agent { + return predicate.Agent(sql.FieldNotNull(FieldContainerStatus)) +} + +// ContainerStatusEqualFold applies the EqualFold predicate on the "container_status" field. +func ContainerStatusEqualFold(v string) predicate.Agent { + return predicate.Agent(sql.FieldEqualFold(FieldContainerStatus, v)) +} + +// ContainerStatusContainsFold applies the ContainsFold predicate on the "container_status" field. +func ContainerStatusContainsFold(v string) predicate.Agent { + return predicate.Agent(sql.FieldContainsFold(FieldContainerStatus, v)) +} + +// RuntimeStateEQ applies the EQ predicate on the "runtime_state" field. +func RuntimeStateEQ(v string) predicate.Agent { + return predicate.Agent(sql.FieldEQ(FieldRuntimeState, v)) +} + +// RuntimeStateNEQ applies the NEQ predicate on the "runtime_state" field. +func RuntimeStateNEQ(v string) predicate.Agent { + return predicate.Agent(sql.FieldNEQ(FieldRuntimeState, v)) +} + +// RuntimeStateIn applies the In predicate on the "runtime_state" field. +func RuntimeStateIn(vs ...string) predicate.Agent { + return predicate.Agent(sql.FieldIn(FieldRuntimeState, vs...)) +} + +// RuntimeStateNotIn applies the NotIn predicate on the "runtime_state" field. +func RuntimeStateNotIn(vs ...string) predicate.Agent { + return predicate.Agent(sql.FieldNotIn(FieldRuntimeState, vs...)) +} + +// RuntimeStateGT applies the GT predicate on the "runtime_state" field. +func RuntimeStateGT(v string) predicate.Agent { + return predicate.Agent(sql.FieldGT(FieldRuntimeState, v)) +} + +// RuntimeStateGTE applies the GTE predicate on the "runtime_state" field. +func RuntimeStateGTE(v string) predicate.Agent { + return predicate.Agent(sql.FieldGTE(FieldRuntimeState, v)) +} + +// RuntimeStateLT applies the LT predicate on the "runtime_state" field. +func RuntimeStateLT(v string) predicate.Agent { + return predicate.Agent(sql.FieldLT(FieldRuntimeState, v)) +} + +// RuntimeStateLTE applies the LTE predicate on the "runtime_state" field. +func RuntimeStateLTE(v string) predicate.Agent { + return predicate.Agent(sql.FieldLTE(FieldRuntimeState, v)) +} + +// RuntimeStateContains applies the Contains predicate on the "runtime_state" field. +func RuntimeStateContains(v string) predicate.Agent { + return predicate.Agent(sql.FieldContains(FieldRuntimeState, v)) +} + +// RuntimeStateHasPrefix applies the HasPrefix predicate on the "runtime_state" field. +func RuntimeStateHasPrefix(v string) predicate.Agent { + return predicate.Agent(sql.FieldHasPrefix(FieldRuntimeState, v)) +} + +// RuntimeStateHasSuffix applies the HasSuffix predicate on the "runtime_state" field. +func RuntimeStateHasSuffix(v string) predicate.Agent { + return predicate.Agent(sql.FieldHasSuffix(FieldRuntimeState, v)) +} + +// RuntimeStateIsNil applies the IsNil predicate on the "runtime_state" field. +func RuntimeStateIsNil() predicate.Agent { + return predicate.Agent(sql.FieldIsNull(FieldRuntimeState)) +} + +// RuntimeStateNotNil applies the NotNil predicate on the "runtime_state" field. +func RuntimeStateNotNil() predicate.Agent { + return predicate.Agent(sql.FieldNotNull(FieldRuntimeState)) +} + +// RuntimeStateEqualFold applies the EqualFold predicate on the "runtime_state" field. +func RuntimeStateEqualFold(v string) predicate.Agent { + return predicate.Agent(sql.FieldEqualFold(FieldRuntimeState, v)) +} + +// RuntimeStateContainsFold applies the ContainsFold predicate on the "runtime_state" field. +func RuntimeStateContainsFold(v string) predicate.Agent { + return predicate.Agent(sql.FieldContainsFold(FieldRuntimeState, v)) +} + +// StalledFromActivityEQ applies the EQ predicate on the "stalled_from_activity" field. +func StalledFromActivityEQ(v string) predicate.Agent { + return predicate.Agent(sql.FieldEQ(FieldStalledFromActivity, v)) +} + +// StalledFromActivityNEQ applies the NEQ predicate on the "stalled_from_activity" field. +func StalledFromActivityNEQ(v string) predicate.Agent { + return predicate.Agent(sql.FieldNEQ(FieldStalledFromActivity, v)) +} + +// StalledFromActivityIn applies the In predicate on the "stalled_from_activity" field. +func StalledFromActivityIn(vs ...string) predicate.Agent { + return predicate.Agent(sql.FieldIn(FieldStalledFromActivity, vs...)) +} + +// StalledFromActivityNotIn applies the NotIn predicate on the "stalled_from_activity" field. +func StalledFromActivityNotIn(vs ...string) predicate.Agent { + return predicate.Agent(sql.FieldNotIn(FieldStalledFromActivity, vs...)) +} + +// StalledFromActivityGT applies the GT predicate on the "stalled_from_activity" field. +func StalledFromActivityGT(v string) predicate.Agent { + return predicate.Agent(sql.FieldGT(FieldStalledFromActivity, v)) +} + +// StalledFromActivityGTE applies the GTE predicate on the "stalled_from_activity" field. +func StalledFromActivityGTE(v string) predicate.Agent { + return predicate.Agent(sql.FieldGTE(FieldStalledFromActivity, v)) +} + +// StalledFromActivityLT applies the LT predicate on the "stalled_from_activity" field. +func StalledFromActivityLT(v string) predicate.Agent { + return predicate.Agent(sql.FieldLT(FieldStalledFromActivity, v)) +} + +// StalledFromActivityLTE applies the LTE predicate on the "stalled_from_activity" field. +func StalledFromActivityLTE(v string) predicate.Agent { + return predicate.Agent(sql.FieldLTE(FieldStalledFromActivity, v)) +} + +// StalledFromActivityContains applies the Contains predicate on the "stalled_from_activity" field. +func StalledFromActivityContains(v string) predicate.Agent { + return predicate.Agent(sql.FieldContains(FieldStalledFromActivity, v)) +} + +// StalledFromActivityHasPrefix applies the HasPrefix predicate on the "stalled_from_activity" field. +func StalledFromActivityHasPrefix(v string) predicate.Agent { + return predicate.Agent(sql.FieldHasPrefix(FieldStalledFromActivity, v)) +} + +// StalledFromActivityHasSuffix applies the HasSuffix predicate on the "stalled_from_activity" field. +func StalledFromActivityHasSuffix(v string) predicate.Agent { + return predicate.Agent(sql.FieldHasSuffix(FieldStalledFromActivity, v)) +} + +// StalledFromActivityIsNil applies the IsNil predicate on the "stalled_from_activity" field. +func StalledFromActivityIsNil() predicate.Agent { + return predicate.Agent(sql.FieldIsNull(FieldStalledFromActivity)) +} + +// StalledFromActivityNotNil applies the NotNil predicate on the "stalled_from_activity" field. +func StalledFromActivityNotNil() predicate.Agent { + return predicate.Agent(sql.FieldNotNull(FieldStalledFromActivity)) +} + +// StalledFromActivityEqualFold applies the EqualFold predicate on the "stalled_from_activity" field. +func StalledFromActivityEqualFold(v string) predicate.Agent { + return predicate.Agent(sql.FieldEqualFold(FieldStalledFromActivity, v)) +} + +// StalledFromActivityContainsFold applies the ContainsFold predicate on the "stalled_from_activity" field. +func StalledFromActivityContainsFold(v string) predicate.Agent { + return predicate.Agent(sql.FieldContainsFold(FieldStalledFromActivity, v)) +} + +// CurrentTurnsEQ applies the EQ predicate on the "current_turns" field. +func CurrentTurnsEQ(v int) predicate.Agent { + return predicate.Agent(sql.FieldEQ(FieldCurrentTurns, v)) +} + +// CurrentTurnsNEQ applies the NEQ predicate on the "current_turns" field. +func CurrentTurnsNEQ(v int) predicate.Agent { + return predicate.Agent(sql.FieldNEQ(FieldCurrentTurns, v)) +} + +// CurrentTurnsIn applies the In predicate on the "current_turns" field. +func CurrentTurnsIn(vs ...int) predicate.Agent { + return predicate.Agent(sql.FieldIn(FieldCurrentTurns, vs...)) +} + +// CurrentTurnsNotIn applies the NotIn predicate on the "current_turns" field. +func CurrentTurnsNotIn(vs ...int) predicate.Agent { + return predicate.Agent(sql.FieldNotIn(FieldCurrentTurns, vs...)) +} + +// CurrentTurnsGT applies the GT predicate on the "current_turns" field. +func CurrentTurnsGT(v int) predicate.Agent { + return predicate.Agent(sql.FieldGT(FieldCurrentTurns, v)) +} + +// CurrentTurnsGTE applies the GTE predicate on the "current_turns" field. +func CurrentTurnsGTE(v int) predicate.Agent { + return predicate.Agent(sql.FieldGTE(FieldCurrentTurns, v)) +} + +// CurrentTurnsLT applies the LT predicate on the "current_turns" field. +func CurrentTurnsLT(v int) predicate.Agent { + return predicate.Agent(sql.FieldLT(FieldCurrentTurns, v)) +} + +// CurrentTurnsLTE applies the LTE predicate on the "current_turns" field. +func CurrentTurnsLTE(v int) predicate.Agent { + return predicate.Agent(sql.FieldLTE(FieldCurrentTurns, v)) +} + +// CurrentModelCallsEQ applies the EQ predicate on the "current_model_calls" field. +func CurrentModelCallsEQ(v int) predicate.Agent { + return predicate.Agent(sql.FieldEQ(FieldCurrentModelCalls, v)) +} + +// CurrentModelCallsNEQ applies the NEQ predicate on the "current_model_calls" field. +func CurrentModelCallsNEQ(v int) predicate.Agent { + return predicate.Agent(sql.FieldNEQ(FieldCurrentModelCalls, v)) +} + +// CurrentModelCallsIn applies the In predicate on the "current_model_calls" field. +func CurrentModelCallsIn(vs ...int) predicate.Agent { + return predicate.Agent(sql.FieldIn(FieldCurrentModelCalls, vs...)) +} + +// CurrentModelCallsNotIn applies the NotIn predicate on the "current_model_calls" field. +func CurrentModelCallsNotIn(vs ...int) predicate.Agent { + return predicate.Agent(sql.FieldNotIn(FieldCurrentModelCalls, vs...)) +} + +// CurrentModelCallsGT applies the GT predicate on the "current_model_calls" field. +func CurrentModelCallsGT(v int) predicate.Agent { + return predicate.Agent(sql.FieldGT(FieldCurrentModelCalls, v)) +} + +// CurrentModelCallsGTE applies the GTE predicate on the "current_model_calls" field. +func CurrentModelCallsGTE(v int) predicate.Agent { + return predicate.Agent(sql.FieldGTE(FieldCurrentModelCalls, v)) +} + +// CurrentModelCallsLT applies the LT predicate on the "current_model_calls" field. +func CurrentModelCallsLT(v int) predicate.Agent { + return predicate.Agent(sql.FieldLT(FieldCurrentModelCalls, v)) +} + +// CurrentModelCallsLTE applies the LTE predicate on the "current_model_calls" field. +func CurrentModelCallsLTE(v int) predicate.Agent { + return predicate.Agent(sql.FieldLTE(FieldCurrentModelCalls, v)) +} + +// ImageEQ applies the EQ predicate on the "image" field. +func ImageEQ(v string) predicate.Agent { + return predicate.Agent(sql.FieldEQ(FieldImage, v)) +} + +// ImageNEQ applies the NEQ predicate on the "image" field. +func ImageNEQ(v string) predicate.Agent { + return predicate.Agent(sql.FieldNEQ(FieldImage, v)) +} + +// ImageIn applies the In predicate on the "image" field. +func ImageIn(vs ...string) predicate.Agent { + return predicate.Agent(sql.FieldIn(FieldImage, vs...)) +} + +// ImageNotIn applies the NotIn predicate on the "image" field. +func ImageNotIn(vs ...string) predicate.Agent { + return predicate.Agent(sql.FieldNotIn(FieldImage, vs...)) +} + +// ImageGT applies the GT predicate on the "image" field. +func ImageGT(v string) predicate.Agent { + return predicate.Agent(sql.FieldGT(FieldImage, v)) +} + +// ImageGTE applies the GTE predicate on the "image" field. +func ImageGTE(v string) predicate.Agent { + return predicate.Agent(sql.FieldGTE(FieldImage, v)) +} + +// ImageLT applies the LT predicate on the "image" field. +func ImageLT(v string) predicate.Agent { + return predicate.Agent(sql.FieldLT(FieldImage, v)) +} + +// ImageLTE applies the LTE predicate on the "image" field. +func ImageLTE(v string) predicate.Agent { + return predicate.Agent(sql.FieldLTE(FieldImage, v)) +} + +// ImageContains applies the Contains predicate on the "image" field. +func ImageContains(v string) predicate.Agent { + return predicate.Agent(sql.FieldContains(FieldImage, v)) +} + +// ImageHasPrefix applies the HasPrefix predicate on the "image" field. +func ImageHasPrefix(v string) predicate.Agent { + return predicate.Agent(sql.FieldHasPrefix(FieldImage, v)) +} + +// ImageHasSuffix applies the HasSuffix predicate on the "image" field. +func ImageHasSuffix(v string) predicate.Agent { + return predicate.Agent(sql.FieldHasSuffix(FieldImage, v)) +} + +// ImageIsNil applies the IsNil predicate on the "image" field. +func ImageIsNil() predicate.Agent { + return predicate.Agent(sql.FieldIsNull(FieldImage)) +} + +// ImageNotNil applies the NotNil predicate on the "image" field. +func ImageNotNil() predicate.Agent { + return predicate.Agent(sql.FieldNotNull(FieldImage)) +} + +// ImageEqualFold applies the EqualFold predicate on the "image" field. +func ImageEqualFold(v string) predicate.Agent { + return predicate.Agent(sql.FieldEqualFold(FieldImage, v)) +} + +// ImageContainsFold applies the ContainsFold predicate on the "image" field. +func ImageContainsFold(v string) predicate.Agent { + return predicate.Agent(sql.FieldContainsFold(FieldImage, v)) +} + +// DetachedEQ applies the EQ predicate on the "detached" field. +func DetachedEQ(v bool) predicate.Agent { + return predicate.Agent(sql.FieldEQ(FieldDetached, v)) +} + +// DetachedNEQ applies the NEQ predicate on the "detached" field. +func DetachedNEQ(v bool) predicate.Agent { + return predicate.Agent(sql.FieldNEQ(FieldDetached, v)) +} + +// RuntimeEQ applies the EQ predicate on the "runtime" field. +func RuntimeEQ(v string) predicate.Agent { + return predicate.Agent(sql.FieldEQ(FieldRuntime, v)) +} + +// RuntimeNEQ applies the NEQ predicate on the "runtime" field. +func RuntimeNEQ(v string) predicate.Agent { + return predicate.Agent(sql.FieldNEQ(FieldRuntime, v)) +} + +// RuntimeIn applies the In predicate on the "runtime" field. +func RuntimeIn(vs ...string) predicate.Agent { + return predicate.Agent(sql.FieldIn(FieldRuntime, vs...)) +} + +// RuntimeNotIn applies the NotIn predicate on the "runtime" field. +func RuntimeNotIn(vs ...string) predicate.Agent { + return predicate.Agent(sql.FieldNotIn(FieldRuntime, vs...)) +} + +// RuntimeGT applies the GT predicate on the "runtime" field. +func RuntimeGT(v string) predicate.Agent { + return predicate.Agent(sql.FieldGT(FieldRuntime, v)) +} + +// RuntimeGTE applies the GTE predicate on the "runtime" field. +func RuntimeGTE(v string) predicate.Agent { + return predicate.Agent(sql.FieldGTE(FieldRuntime, v)) +} + +// RuntimeLT applies the LT predicate on the "runtime" field. +func RuntimeLT(v string) predicate.Agent { + return predicate.Agent(sql.FieldLT(FieldRuntime, v)) +} + +// RuntimeLTE applies the LTE predicate on the "runtime" field. +func RuntimeLTE(v string) predicate.Agent { + return predicate.Agent(sql.FieldLTE(FieldRuntime, v)) +} + +// RuntimeContains applies the Contains predicate on the "runtime" field. +func RuntimeContains(v string) predicate.Agent { + return predicate.Agent(sql.FieldContains(FieldRuntime, v)) +} + +// RuntimeHasPrefix applies the HasPrefix predicate on the "runtime" field. +func RuntimeHasPrefix(v string) predicate.Agent { + return predicate.Agent(sql.FieldHasPrefix(FieldRuntime, v)) +} + +// RuntimeHasSuffix applies the HasSuffix predicate on the "runtime" field. +func RuntimeHasSuffix(v string) predicate.Agent { + return predicate.Agent(sql.FieldHasSuffix(FieldRuntime, v)) +} + +// RuntimeIsNil applies the IsNil predicate on the "runtime" field. +func RuntimeIsNil() predicate.Agent { + return predicate.Agent(sql.FieldIsNull(FieldRuntime)) +} + +// RuntimeNotNil applies the NotNil predicate on the "runtime" field. +func RuntimeNotNil() predicate.Agent { + return predicate.Agent(sql.FieldNotNull(FieldRuntime)) +} + +// RuntimeEqualFold applies the EqualFold predicate on the "runtime" field. +func RuntimeEqualFold(v string) predicate.Agent { + return predicate.Agent(sql.FieldEqualFold(FieldRuntime, v)) +} + +// RuntimeContainsFold applies the ContainsFold predicate on the "runtime" field. +func RuntimeContainsFold(v string) predicate.Agent { + return predicate.Agent(sql.FieldContainsFold(FieldRuntime, v)) +} + +// RuntimeBrokerIDEQ applies the EQ predicate on the "runtime_broker_id" field. +func RuntimeBrokerIDEQ(v string) predicate.Agent { + return predicate.Agent(sql.FieldEQ(FieldRuntimeBrokerID, v)) +} + +// RuntimeBrokerIDNEQ applies the NEQ predicate on the "runtime_broker_id" field. +func RuntimeBrokerIDNEQ(v string) predicate.Agent { + return predicate.Agent(sql.FieldNEQ(FieldRuntimeBrokerID, v)) +} + +// RuntimeBrokerIDIn applies the In predicate on the "runtime_broker_id" field. +func RuntimeBrokerIDIn(vs ...string) predicate.Agent { + return predicate.Agent(sql.FieldIn(FieldRuntimeBrokerID, vs...)) +} + +// RuntimeBrokerIDNotIn applies the NotIn predicate on the "runtime_broker_id" field. +func RuntimeBrokerIDNotIn(vs ...string) predicate.Agent { + return predicate.Agent(sql.FieldNotIn(FieldRuntimeBrokerID, vs...)) +} + +// RuntimeBrokerIDGT applies the GT predicate on the "runtime_broker_id" field. +func RuntimeBrokerIDGT(v string) predicate.Agent { + return predicate.Agent(sql.FieldGT(FieldRuntimeBrokerID, v)) +} + +// RuntimeBrokerIDGTE applies the GTE predicate on the "runtime_broker_id" field. +func RuntimeBrokerIDGTE(v string) predicate.Agent { + return predicate.Agent(sql.FieldGTE(FieldRuntimeBrokerID, v)) +} + +// RuntimeBrokerIDLT applies the LT predicate on the "runtime_broker_id" field. +func RuntimeBrokerIDLT(v string) predicate.Agent { + return predicate.Agent(sql.FieldLT(FieldRuntimeBrokerID, v)) +} + +// RuntimeBrokerIDLTE applies the LTE predicate on the "runtime_broker_id" field. +func RuntimeBrokerIDLTE(v string) predicate.Agent { + return predicate.Agent(sql.FieldLTE(FieldRuntimeBrokerID, v)) +} + +// RuntimeBrokerIDContains applies the Contains predicate on the "runtime_broker_id" field. +func RuntimeBrokerIDContains(v string) predicate.Agent { + return predicate.Agent(sql.FieldContains(FieldRuntimeBrokerID, v)) +} + +// RuntimeBrokerIDHasPrefix applies the HasPrefix predicate on the "runtime_broker_id" field. +func RuntimeBrokerIDHasPrefix(v string) predicate.Agent { + return predicate.Agent(sql.FieldHasPrefix(FieldRuntimeBrokerID, v)) +} + +// RuntimeBrokerIDHasSuffix applies the HasSuffix predicate on the "runtime_broker_id" field. +func RuntimeBrokerIDHasSuffix(v string) predicate.Agent { + return predicate.Agent(sql.FieldHasSuffix(FieldRuntimeBrokerID, v)) +} + +// RuntimeBrokerIDIsNil applies the IsNil predicate on the "runtime_broker_id" field. +func RuntimeBrokerIDIsNil() predicate.Agent { + return predicate.Agent(sql.FieldIsNull(FieldRuntimeBrokerID)) +} + +// RuntimeBrokerIDNotNil applies the NotNil predicate on the "runtime_broker_id" field. +func RuntimeBrokerIDNotNil() predicate.Agent { + return predicate.Agent(sql.FieldNotNull(FieldRuntimeBrokerID)) +} + +// RuntimeBrokerIDEqualFold applies the EqualFold predicate on the "runtime_broker_id" field. +func RuntimeBrokerIDEqualFold(v string) predicate.Agent { + return predicate.Agent(sql.FieldEqualFold(FieldRuntimeBrokerID, v)) +} + +// RuntimeBrokerIDContainsFold applies the ContainsFold predicate on the "runtime_broker_id" field. +func RuntimeBrokerIDContainsFold(v string) predicate.Agent { + return predicate.Agent(sql.FieldContainsFold(FieldRuntimeBrokerID, v)) +} + +// WebPtyEnabledEQ applies the EQ predicate on the "web_pty_enabled" field. +func WebPtyEnabledEQ(v bool) predicate.Agent { + return predicate.Agent(sql.FieldEQ(FieldWebPtyEnabled, v)) +} + +// WebPtyEnabledNEQ applies the NEQ predicate on the "web_pty_enabled" field. +func WebPtyEnabledNEQ(v bool) predicate.Agent { + return predicate.Agent(sql.FieldNEQ(FieldWebPtyEnabled, v)) +} + +// TaskSummaryEQ applies the EQ predicate on the "task_summary" field. +func TaskSummaryEQ(v string) predicate.Agent { + return predicate.Agent(sql.FieldEQ(FieldTaskSummary, v)) +} + +// TaskSummaryNEQ applies the NEQ predicate on the "task_summary" field. +func TaskSummaryNEQ(v string) predicate.Agent { + return predicate.Agent(sql.FieldNEQ(FieldTaskSummary, v)) +} + +// TaskSummaryIn applies the In predicate on the "task_summary" field. +func TaskSummaryIn(vs ...string) predicate.Agent { + return predicate.Agent(sql.FieldIn(FieldTaskSummary, vs...)) +} + +// TaskSummaryNotIn applies the NotIn predicate on the "task_summary" field. +func TaskSummaryNotIn(vs ...string) predicate.Agent { + return predicate.Agent(sql.FieldNotIn(FieldTaskSummary, vs...)) +} + +// TaskSummaryGT applies the GT predicate on the "task_summary" field. +func TaskSummaryGT(v string) predicate.Agent { + return predicate.Agent(sql.FieldGT(FieldTaskSummary, v)) +} + +// TaskSummaryGTE applies the GTE predicate on the "task_summary" field. +func TaskSummaryGTE(v string) predicate.Agent { + return predicate.Agent(sql.FieldGTE(FieldTaskSummary, v)) +} + +// TaskSummaryLT applies the LT predicate on the "task_summary" field. +func TaskSummaryLT(v string) predicate.Agent { + return predicate.Agent(sql.FieldLT(FieldTaskSummary, v)) +} + +// TaskSummaryLTE applies the LTE predicate on the "task_summary" field. +func TaskSummaryLTE(v string) predicate.Agent { + return predicate.Agent(sql.FieldLTE(FieldTaskSummary, v)) +} + +// TaskSummaryContains applies the Contains predicate on the "task_summary" field. +func TaskSummaryContains(v string) predicate.Agent { + return predicate.Agent(sql.FieldContains(FieldTaskSummary, v)) +} + +// TaskSummaryHasPrefix applies the HasPrefix predicate on the "task_summary" field. +func TaskSummaryHasPrefix(v string) predicate.Agent { + return predicate.Agent(sql.FieldHasPrefix(FieldTaskSummary, v)) +} + +// TaskSummaryHasSuffix applies the HasSuffix predicate on the "task_summary" field. +func TaskSummaryHasSuffix(v string) predicate.Agent { + return predicate.Agent(sql.FieldHasSuffix(FieldTaskSummary, v)) +} + +// TaskSummaryIsNil applies the IsNil predicate on the "task_summary" field. +func TaskSummaryIsNil() predicate.Agent { + return predicate.Agent(sql.FieldIsNull(FieldTaskSummary)) +} + +// TaskSummaryNotNil applies the NotNil predicate on the "task_summary" field. +func TaskSummaryNotNil() predicate.Agent { + return predicate.Agent(sql.FieldNotNull(FieldTaskSummary)) +} + +// TaskSummaryEqualFold applies the EqualFold predicate on the "task_summary" field. +func TaskSummaryEqualFold(v string) predicate.Agent { + return predicate.Agent(sql.FieldEqualFold(FieldTaskSummary, v)) +} + +// TaskSummaryContainsFold applies the ContainsFold predicate on the "task_summary" field. +func TaskSummaryContainsFold(v string) predicate.Agent { + return predicate.Agent(sql.FieldContainsFold(FieldTaskSummary, v)) +} + +// MessageEQ applies the EQ predicate on the "message" field. +func MessageEQ(v string) predicate.Agent { + return predicate.Agent(sql.FieldEQ(FieldMessage, v)) +} + +// MessageNEQ applies the NEQ predicate on the "message" field. +func MessageNEQ(v string) predicate.Agent { + return predicate.Agent(sql.FieldNEQ(FieldMessage, v)) +} + +// MessageIn applies the In predicate on the "message" field. +func MessageIn(vs ...string) predicate.Agent { + return predicate.Agent(sql.FieldIn(FieldMessage, vs...)) +} + +// MessageNotIn applies the NotIn predicate on the "message" field. +func MessageNotIn(vs ...string) predicate.Agent { + return predicate.Agent(sql.FieldNotIn(FieldMessage, vs...)) +} + +// MessageGT applies the GT predicate on the "message" field. +func MessageGT(v string) predicate.Agent { + return predicate.Agent(sql.FieldGT(FieldMessage, v)) +} + +// MessageGTE applies the GTE predicate on the "message" field. +func MessageGTE(v string) predicate.Agent { + return predicate.Agent(sql.FieldGTE(FieldMessage, v)) +} + +// MessageLT applies the LT predicate on the "message" field. +func MessageLT(v string) predicate.Agent { + return predicate.Agent(sql.FieldLT(FieldMessage, v)) +} + +// MessageLTE applies the LTE predicate on the "message" field. +func MessageLTE(v string) predicate.Agent { + return predicate.Agent(sql.FieldLTE(FieldMessage, v)) +} + +// MessageContains applies the Contains predicate on the "message" field. +func MessageContains(v string) predicate.Agent { + return predicate.Agent(sql.FieldContains(FieldMessage, v)) +} + +// MessageHasPrefix applies the HasPrefix predicate on the "message" field. +func MessageHasPrefix(v string) predicate.Agent { + return predicate.Agent(sql.FieldHasPrefix(FieldMessage, v)) +} + +// MessageHasSuffix applies the HasSuffix predicate on the "message" field. +func MessageHasSuffix(v string) predicate.Agent { + return predicate.Agent(sql.FieldHasSuffix(FieldMessage, v)) +} + +// MessageIsNil applies the IsNil predicate on the "message" field. +func MessageIsNil() predicate.Agent { + return predicate.Agent(sql.FieldIsNull(FieldMessage)) +} + +// MessageNotNil applies the NotNil predicate on the "message" field. +func MessageNotNil() predicate.Agent { + return predicate.Agent(sql.FieldNotNull(FieldMessage)) +} + +// MessageEqualFold applies the EqualFold predicate on the "message" field. +func MessageEqualFold(v string) predicate.Agent { + return predicate.Agent(sql.FieldEqualFold(FieldMessage, v)) +} + +// MessageContainsFold applies the ContainsFold predicate on the "message" field. +func MessageContainsFold(v string) predicate.Agent { + return predicate.Agent(sql.FieldContainsFold(FieldMessage, v)) +} + +// AppliedConfigEQ applies the EQ predicate on the "applied_config" field. +func AppliedConfigEQ(v string) predicate.Agent { + return predicate.Agent(sql.FieldEQ(FieldAppliedConfig, v)) +} + +// AppliedConfigNEQ applies the NEQ predicate on the "applied_config" field. +func AppliedConfigNEQ(v string) predicate.Agent { + return predicate.Agent(sql.FieldNEQ(FieldAppliedConfig, v)) +} + +// AppliedConfigIn applies the In predicate on the "applied_config" field. +func AppliedConfigIn(vs ...string) predicate.Agent { + return predicate.Agent(sql.FieldIn(FieldAppliedConfig, vs...)) +} + +// AppliedConfigNotIn applies the NotIn predicate on the "applied_config" field. +func AppliedConfigNotIn(vs ...string) predicate.Agent { + return predicate.Agent(sql.FieldNotIn(FieldAppliedConfig, vs...)) +} + +// AppliedConfigGT applies the GT predicate on the "applied_config" field. +func AppliedConfigGT(v string) predicate.Agent { + return predicate.Agent(sql.FieldGT(FieldAppliedConfig, v)) +} + +// AppliedConfigGTE applies the GTE predicate on the "applied_config" field. +func AppliedConfigGTE(v string) predicate.Agent { + return predicate.Agent(sql.FieldGTE(FieldAppliedConfig, v)) +} + +// AppliedConfigLT applies the LT predicate on the "applied_config" field. +func AppliedConfigLT(v string) predicate.Agent { + return predicate.Agent(sql.FieldLT(FieldAppliedConfig, v)) +} + +// AppliedConfigLTE applies the LTE predicate on the "applied_config" field. +func AppliedConfigLTE(v string) predicate.Agent { + return predicate.Agent(sql.FieldLTE(FieldAppliedConfig, v)) +} + +// AppliedConfigContains applies the Contains predicate on the "applied_config" field. +func AppliedConfigContains(v string) predicate.Agent { + return predicate.Agent(sql.FieldContains(FieldAppliedConfig, v)) +} + +// AppliedConfigHasPrefix applies the HasPrefix predicate on the "applied_config" field. +func AppliedConfigHasPrefix(v string) predicate.Agent { + return predicate.Agent(sql.FieldHasPrefix(FieldAppliedConfig, v)) +} + +// AppliedConfigHasSuffix applies the HasSuffix predicate on the "applied_config" field. +func AppliedConfigHasSuffix(v string) predicate.Agent { + return predicate.Agent(sql.FieldHasSuffix(FieldAppliedConfig, v)) +} + +// AppliedConfigIsNil applies the IsNil predicate on the "applied_config" field. +func AppliedConfigIsNil() predicate.Agent { + return predicate.Agent(sql.FieldIsNull(FieldAppliedConfig)) +} + +// AppliedConfigNotNil applies the NotNil predicate on the "applied_config" field. +func AppliedConfigNotNil() predicate.Agent { + return predicate.Agent(sql.FieldNotNull(FieldAppliedConfig)) +} + +// AppliedConfigEqualFold applies the EqualFold predicate on the "applied_config" field. +func AppliedConfigEqualFold(v string) predicate.Agent { + return predicate.Agent(sql.FieldEqualFold(FieldAppliedConfig, v)) +} + +// AppliedConfigContainsFold applies the ContainsFold predicate on the "applied_config" field. +func AppliedConfigContainsFold(v string) predicate.Agent { + return predicate.Agent(sql.FieldContainsFold(FieldAppliedConfig, v)) +} + +// AncestryIsNil applies the IsNil predicate on the "ancestry" field. +func AncestryIsNil() predicate.Agent { + return predicate.Agent(sql.FieldIsNull(FieldAncestry)) +} + +// AncestryNotNil applies the NotNil predicate on the "ancestry" field. +func AncestryNotNil() predicate.Agent { + return predicate.Agent(sql.FieldNotNull(FieldAncestry)) +} + +// CreatedEQ applies the EQ predicate on the "created" field. +func CreatedEQ(v time.Time) predicate.Agent { + return predicate.Agent(sql.FieldEQ(FieldCreated, v)) +} + +// CreatedNEQ applies the NEQ predicate on the "created" field. +func CreatedNEQ(v time.Time) predicate.Agent { + return predicate.Agent(sql.FieldNEQ(FieldCreated, v)) +} + +// CreatedIn applies the In predicate on the "created" field. +func CreatedIn(vs ...time.Time) predicate.Agent { + return predicate.Agent(sql.FieldIn(FieldCreated, vs...)) +} + +// CreatedNotIn applies the NotIn predicate on the "created" field. +func CreatedNotIn(vs ...time.Time) predicate.Agent { + return predicate.Agent(sql.FieldNotIn(FieldCreated, vs...)) +} + +// CreatedGT applies the GT predicate on the "created" field. +func CreatedGT(v time.Time) predicate.Agent { + return predicate.Agent(sql.FieldGT(FieldCreated, v)) +} + +// CreatedGTE applies the GTE predicate on the "created" field. +func CreatedGTE(v time.Time) predicate.Agent { + return predicate.Agent(sql.FieldGTE(FieldCreated, v)) +} + +// CreatedLT applies the LT predicate on the "created" field. +func CreatedLT(v time.Time) predicate.Agent { + return predicate.Agent(sql.FieldLT(FieldCreated, v)) +} + +// CreatedLTE applies the LTE predicate on the "created" field. +func CreatedLTE(v time.Time) predicate.Agent { + return predicate.Agent(sql.FieldLTE(FieldCreated, v)) +} + +// UpdatedEQ applies the EQ predicate on the "updated" field. +func UpdatedEQ(v time.Time) predicate.Agent { + return predicate.Agent(sql.FieldEQ(FieldUpdated, v)) +} + +// UpdatedNEQ applies the NEQ predicate on the "updated" field. +func UpdatedNEQ(v time.Time) predicate.Agent { + return predicate.Agent(sql.FieldNEQ(FieldUpdated, v)) +} + +// UpdatedIn applies the In predicate on the "updated" field. +func UpdatedIn(vs ...time.Time) predicate.Agent { + return predicate.Agent(sql.FieldIn(FieldUpdated, vs...)) +} + +// UpdatedNotIn applies the NotIn predicate on the "updated" field. +func UpdatedNotIn(vs ...time.Time) predicate.Agent { + return predicate.Agent(sql.FieldNotIn(FieldUpdated, vs...)) +} + +// UpdatedGT applies the GT predicate on the "updated" field. +func UpdatedGT(v time.Time) predicate.Agent { + return predicate.Agent(sql.FieldGT(FieldUpdated, v)) +} + +// UpdatedGTE applies the GTE predicate on the "updated" field. +func UpdatedGTE(v time.Time) predicate.Agent { + return predicate.Agent(sql.FieldGTE(FieldUpdated, v)) +} + +// UpdatedLT applies the LT predicate on the "updated" field. +func UpdatedLT(v time.Time) predicate.Agent { + return predicate.Agent(sql.FieldLT(FieldUpdated, v)) +} + +// UpdatedLTE applies the LTE predicate on the "updated" field. +func UpdatedLTE(v time.Time) predicate.Agent { + return predicate.Agent(sql.FieldLTE(FieldUpdated, v)) +} + +// LastSeenEQ applies the EQ predicate on the "last_seen" field. +func LastSeenEQ(v time.Time) predicate.Agent { + return predicate.Agent(sql.FieldEQ(FieldLastSeen, v)) +} + +// LastSeenNEQ applies the NEQ predicate on the "last_seen" field. +func LastSeenNEQ(v time.Time) predicate.Agent { + return predicate.Agent(sql.FieldNEQ(FieldLastSeen, v)) +} + +// LastSeenIn applies the In predicate on the "last_seen" field. +func LastSeenIn(vs ...time.Time) predicate.Agent { + return predicate.Agent(sql.FieldIn(FieldLastSeen, vs...)) +} + +// LastSeenNotIn applies the NotIn predicate on the "last_seen" field. +func LastSeenNotIn(vs ...time.Time) predicate.Agent { + return predicate.Agent(sql.FieldNotIn(FieldLastSeen, vs...)) +} + +// LastSeenGT applies the GT predicate on the "last_seen" field. +func LastSeenGT(v time.Time) predicate.Agent { + return predicate.Agent(sql.FieldGT(FieldLastSeen, v)) +} + +// LastSeenGTE applies the GTE predicate on the "last_seen" field. +func LastSeenGTE(v time.Time) predicate.Agent { + return predicate.Agent(sql.FieldGTE(FieldLastSeen, v)) +} + +// LastSeenLT applies the LT predicate on the "last_seen" field. +func LastSeenLT(v time.Time) predicate.Agent { + return predicate.Agent(sql.FieldLT(FieldLastSeen, v)) +} + +// LastSeenLTE applies the LTE predicate on the "last_seen" field. +func LastSeenLTE(v time.Time) predicate.Agent { + return predicate.Agent(sql.FieldLTE(FieldLastSeen, v)) +} + +// LastSeenIsNil applies the IsNil predicate on the "last_seen" field. +func LastSeenIsNil() predicate.Agent { + return predicate.Agent(sql.FieldIsNull(FieldLastSeen)) +} + +// LastSeenNotNil applies the NotNil predicate on the "last_seen" field. +func LastSeenNotNil() predicate.Agent { + return predicate.Agent(sql.FieldNotNull(FieldLastSeen)) +} + +// LastActivityEventEQ applies the EQ predicate on the "last_activity_event" field. +func LastActivityEventEQ(v time.Time) predicate.Agent { + return predicate.Agent(sql.FieldEQ(FieldLastActivityEvent, v)) +} + +// LastActivityEventNEQ applies the NEQ predicate on the "last_activity_event" field. +func LastActivityEventNEQ(v time.Time) predicate.Agent { + return predicate.Agent(sql.FieldNEQ(FieldLastActivityEvent, v)) +} + +// LastActivityEventIn applies the In predicate on the "last_activity_event" field. +func LastActivityEventIn(vs ...time.Time) predicate.Agent { + return predicate.Agent(sql.FieldIn(FieldLastActivityEvent, vs...)) +} + +// LastActivityEventNotIn applies the NotIn predicate on the "last_activity_event" field. +func LastActivityEventNotIn(vs ...time.Time) predicate.Agent { + return predicate.Agent(sql.FieldNotIn(FieldLastActivityEvent, vs...)) +} + +// LastActivityEventGT applies the GT predicate on the "last_activity_event" field. +func LastActivityEventGT(v time.Time) predicate.Agent { + return predicate.Agent(sql.FieldGT(FieldLastActivityEvent, v)) +} + +// LastActivityEventGTE applies the GTE predicate on the "last_activity_event" field. +func LastActivityEventGTE(v time.Time) predicate.Agent { + return predicate.Agent(sql.FieldGTE(FieldLastActivityEvent, v)) +} + +// LastActivityEventLT applies the LT predicate on the "last_activity_event" field. +func LastActivityEventLT(v time.Time) predicate.Agent { + return predicate.Agent(sql.FieldLT(FieldLastActivityEvent, v)) +} + +// LastActivityEventLTE applies the LTE predicate on the "last_activity_event" field. +func LastActivityEventLTE(v time.Time) predicate.Agent { + return predicate.Agent(sql.FieldLTE(FieldLastActivityEvent, v)) +} + +// LastActivityEventIsNil applies the IsNil predicate on the "last_activity_event" field. +func LastActivityEventIsNil() predicate.Agent { + return predicate.Agent(sql.FieldIsNull(FieldLastActivityEvent)) +} + +// LastActivityEventNotNil applies the NotNil predicate on the "last_activity_event" field. +func LastActivityEventNotNil() predicate.Agent { + return predicate.Agent(sql.FieldNotNull(FieldLastActivityEvent)) +} + +// StartedAtEQ applies the EQ predicate on the "started_at" field. +func StartedAtEQ(v time.Time) predicate.Agent { + return predicate.Agent(sql.FieldEQ(FieldStartedAt, v)) +} + +// StartedAtNEQ applies the NEQ predicate on the "started_at" field. +func StartedAtNEQ(v time.Time) predicate.Agent { + return predicate.Agent(sql.FieldNEQ(FieldStartedAt, v)) +} + +// StartedAtIn applies the In predicate on the "started_at" field. +func StartedAtIn(vs ...time.Time) predicate.Agent { + return predicate.Agent(sql.FieldIn(FieldStartedAt, vs...)) +} + +// StartedAtNotIn applies the NotIn predicate on the "started_at" field. +func StartedAtNotIn(vs ...time.Time) predicate.Agent { + return predicate.Agent(sql.FieldNotIn(FieldStartedAt, vs...)) +} + +// StartedAtGT applies the GT predicate on the "started_at" field. +func StartedAtGT(v time.Time) predicate.Agent { + return predicate.Agent(sql.FieldGT(FieldStartedAt, v)) +} + +// StartedAtGTE applies the GTE predicate on the "started_at" field. +func StartedAtGTE(v time.Time) predicate.Agent { + return predicate.Agent(sql.FieldGTE(FieldStartedAt, v)) +} + +// StartedAtLT applies the LT predicate on the "started_at" field. +func StartedAtLT(v time.Time) predicate.Agent { + return predicate.Agent(sql.FieldLT(FieldStartedAt, v)) +} + +// StartedAtLTE applies the LTE predicate on the "started_at" field. +func StartedAtLTE(v time.Time) predicate.Agent { + return predicate.Agent(sql.FieldLTE(FieldStartedAt, v)) +} + +// StartedAtIsNil applies the IsNil predicate on the "started_at" field. +func StartedAtIsNil() predicate.Agent { + return predicate.Agent(sql.FieldIsNull(FieldStartedAt)) +} + +// StartedAtNotNil applies the NotNil predicate on the "started_at" field. +func StartedAtNotNil() predicate.Agent { + return predicate.Agent(sql.FieldNotNull(FieldStartedAt)) +} + +// DeletedAtEQ applies the EQ predicate on the "deleted_at" field. +func DeletedAtEQ(v time.Time) predicate.Agent { + return predicate.Agent(sql.FieldEQ(FieldDeletedAt, v)) +} + +// DeletedAtNEQ applies the NEQ predicate on the "deleted_at" field. +func DeletedAtNEQ(v time.Time) predicate.Agent { + return predicate.Agent(sql.FieldNEQ(FieldDeletedAt, v)) +} + +// DeletedAtIn applies the In predicate on the "deleted_at" field. +func DeletedAtIn(vs ...time.Time) predicate.Agent { + return predicate.Agent(sql.FieldIn(FieldDeletedAt, vs...)) +} + +// DeletedAtNotIn applies the NotIn predicate on the "deleted_at" field. +func DeletedAtNotIn(vs ...time.Time) predicate.Agent { + return predicate.Agent(sql.FieldNotIn(FieldDeletedAt, vs...)) +} + +// DeletedAtGT applies the GT predicate on the "deleted_at" field. +func DeletedAtGT(v time.Time) predicate.Agent { + return predicate.Agent(sql.FieldGT(FieldDeletedAt, v)) +} + +// DeletedAtGTE applies the GTE predicate on the "deleted_at" field. +func DeletedAtGTE(v time.Time) predicate.Agent { + return predicate.Agent(sql.FieldGTE(FieldDeletedAt, v)) +} + +// DeletedAtLT applies the LT predicate on the "deleted_at" field. +func DeletedAtLT(v time.Time) predicate.Agent { + return predicate.Agent(sql.FieldLT(FieldDeletedAt, v)) +} + +// DeletedAtLTE applies the LTE predicate on the "deleted_at" field. +func DeletedAtLTE(v time.Time) predicate.Agent { + return predicate.Agent(sql.FieldLTE(FieldDeletedAt, v)) +} + +// DeletedAtIsNil applies the IsNil predicate on the "deleted_at" field. +func DeletedAtIsNil() predicate.Agent { + return predicate.Agent(sql.FieldIsNull(FieldDeletedAt)) +} + +// DeletedAtNotNil applies the NotNil predicate on the "deleted_at" field. +func DeletedAtNotNil() predicate.Agent { + return predicate.Agent(sql.FieldNotNull(FieldDeletedAt)) +} + +// StateVersionEQ applies the EQ predicate on the "state_version" field. +func StateVersionEQ(v int64) predicate.Agent { + return predicate.Agent(sql.FieldEQ(FieldStateVersion, v)) +} + +// StateVersionNEQ applies the NEQ predicate on the "state_version" field. +func StateVersionNEQ(v int64) predicate.Agent { + return predicate.Agent(sql.FieldNEQ(FieldStateVersion, v)) +} + +// StateVersionIn applies the In predicate on the "state_version" field. +func StateVersionIn(vs ...int64) predicate.Agent { + return predicate.Agent(sql.FieldIn(FieldStateVersion, vs...)) +} + +// StateVersionNotIn applies the NotIn predicate on the "state_version" field. +func StateVersionNotIn(vs ...int64) predicate.Agent { + return predicate.Agent(sql.FieldNotIn(FieldStateVersion, vs...)) +} + +// StateVersionGT applies the GT predicate on the "state_version" field. +func StateVersionGT(v int64) predicate.Agent { + return predicate.Agent(sql.FieldGT(FieldStateVersion, v)) +} + +// StateVersionGTE applies the GTE predicate on the "state_version" field. +func StateVersionGTE(v int64) predicate.Agent { + return predicate.Agent(sql.FieldGTE(FieldStateVersion, v)) +} + +// StateVersionLT applies the LT predicate on the "state_version" field. +func StateVersionLT(v int64) predicate.Agent { + return predicate.Agent(sql.FieldLT(FieldStateVersion, v)) +} + +// StateVersionLTE applies the LTE predicate on the "state_version" field. +func StateVersionLTE(v int64) predicate.Agent { + return predicate.Agent(sql.FieldLTE(FieldStateVersion, v)) +} + +// HasProject applies the HasEdge predicate on the "project" edge. +func HasProject() predicate.Agent { + return predicate.Agent(func(s *sql.Selector) { + step := sqlgraph.NewStep( + sqlgraph.From(Table, FieldID), + sqlgraph.Edge(sqlgraph.M2O, true, ProjectTable, ProjectColumn), + ) + sqlgraph.HasNeighbors(s, step) + }) +} + +// HasProjectWith applies the HasEdge predicate on the "project" edge with a given conditions (other predicates). +func HasProjectWith(preds ...predicate.Project) predicate.Agent { + return predicate.Agent(func(s *sql.Selector) { + step := newProjectStep() sqlgraph.HasNeighborsWith(s, step, func(s *sql.Selector) { for _, p := range preds { p(s) diff --git a/pkg/ent/agent_create.go b/pkg/ent/agent_create.go index 39a081285..f40156c0e 100644 --- a/pkg/ent/agent_create.go +++ b/pkg/ent/agent_create.go @@ -8,13 +8,14 @@ import ( "fmt" "time" + "entgo.io/ent/dialect" + "entgo.io/ent/dialect/sql" "entgo.io/ent/dialect/sql/sqlgraph" "entgo.io/ent/schema/field" "github.com/GoogleCloudPlatform/scion/pkg/ent/agent" "github.com/GoogleCloudPlatform/scion/pkg/ent/groupmembership" "github.com/GoogleCloudPlatform/scion/pkg/ent/policybinding" "github.com/GoogleCloudPlatform/scion/pkg/ent/project" - "github.com/GoogleCloudPlatform/scion/pkg/ent/user" "github.com/google/uuid" ) @@ -23,6 +24,7 @@ type AgentCreate struct { config mutation *AgentMutation hooks []Hook + conflict []sql.ConflictOption } // SetSlug sets the "slug" field. @@ -127,6 +129,262 @@ func (_c *AgentCreate) SetNillableVisibility(v *string) *AgentCreate { return _c } +// SetLabels sets the "labels" field. +func (_c *AgentCreate) SetLabels(v map[string]string) *AgentCreate { + _c.mutation.SetLabels(v) + return _c +} + +// SetAnnotations sets the "annotations" field. +func (_c *AgentCreate) SetAnnotations(v map[string]string) *AgentCreate { + _c.mutation.SetAnnotations(v) + return _c +} + +// SetPhase sets the "phase" field. +func (_c *AgentCreate) SetPhase(v string) *AgentCreate { + _c.mutation.SetPhase(v) + return _c +} + +// SetNillablePhase sets the "phase" field if the given value is not nil. +func (_c *AgentCreate) SetNillablePhase(v *string) *AgentCreate { + if v != nil { + _c.SetPhase(*v) + } + return _c +} + +// SetActivity sets the "activity" field. +func (_c *AgentCreate) SetActivity(v string) *AgentCreate { + _c.mutation.SetActivity(v) + return _c +} + +// SetNillableActivity sets the "activity" field if the given value is not nil. +func (_c *AgentCreate) SetNillableActivity(v *string) *AgentCreate { + if v != nil { + _c.SetActivity(*v) + } + return _c +} + +// SetToolName sets the "tool_name" field. +func (_c *AgentCreate) SetToolName(v string) *AgentCreate { + _c.mutation.SetToolName(v) + return _c +} + +// SetNillableToolName sets the "tool_name" field if the given value is not nil. +func (_c *AgentCreate) SetNillableToolName(v *string) *AgentCreate { + if v != nil { + _c.SetToolName(*v) + } + return _c +} + +// SetConnectionState sets the "connection_state" field. +func (_c *AgentCreate) SetConnectionState(v string) *AgentCreate { + _c.mutation.SetConnectionState(v) + return _c +} + +// SetNillableConnectionState sets the "connection_state" field if the given value is not nil. +func (_c *AgentCreate) SetNillableConnectionState(v *string) *AgentCreate { + if v != nil { + _c.SetConnectionState(*v) + } + return _c +} + +// SetContainerStatus sets the "container_status" field. +func (_c *AgentCreate) SetContainerStatus(v string) *AgentCreate { + _c.mutation.SetContainerStatus(v) + return _c +} + +// SetNillableContainerStatus sets the "container_status" field if the given value is not nil. +func (_c *AgentCreate) SetNillableContainerStatus(v *string) *AgentCreate { + if v != nil { + _c.SetContainerStatus(*v) + } + return _c +} + +// SetRuntimeState sets the "runtime_state" field. +func (_c *AgentCreate) SetRuntimeState(v string) *AgentCreate { + _c.mutation.SetRuntimeState(v) + return _c +} + +// SetNillableRuntimeState sets the "runtime_state" field if the given value is not nil. +func (_c *AgentCreate) SetNillableRuntimeState(v *string) *AgentCreate { + if v != nil { + _c.SetRuntimeState(*v) + } + return _c +} + +// SetStalledFromActivity sets the "stalled_from_activity" field. +func (_c *AgentCreate) SetStalledFromActivity(v string) *AgentCreate { + _c.mutation.SetStalledFromActivity(v) + return _c +} + +// SetNillableStalledFromActivity sets the "stalled_from_activity" field if the given value is not nil. +func (_c *AgentCreate) SetNillableStalledFromActivity(v *string) *AgentCreate { + if v != nil { + _c.SetStalledFromActivity(*v) + } + return _c +} + +// SetCurrentTurns sets the "current_turns" field. +func (_c *AgentCreate) SetCurrentTurns(v int) *AgentCreate { + _c.mutation.SetCurrentTurns(v) + return _c +} + +// SetNillableCurrentTurns sets the "current_turns" field if the given value is not nil. +func (_c *AgentCreate) SetNillableCurrentTurns(v *int) *AgentCreate { + if v != nil { + _c.SetCurrentTurns(*v) + } + return _c +} + +// SetCurrentModelCalls sets the "current_model_calls" field. +func (_c *AgentCreate) SetCurrentModelCalls(v int) *AgentCreate { + _c.mutation.SetCurrentModelCalls(v) + return _c +} + +// SetNillableCurrentModelCalls sets the "current_model_calls" field if the given value is not nil. +func (_c *AgentCreate) SetNillableCurrentModelCalls(v *int) *AgentCreate { + if v != nil { + _c.SetCurrentModelCalls(*v) + } + return _c +} + +// SetImage sets the "image" field. +func (_c *AgentCreate) SetImage(v string) *AgentCreate { + _c.mutation.SetImage(v) + return _c +} + +// SetNillableImage sets the "image" field if the given value is not nil. +func (_c *AgentCreate) SetNillableImage(v *string) *AgentCreate { + if v != nil { + _c.SetImage(*v) + } + return _c +} + +// SetDetached sets the "detached" field. +func (_c *AgentCreate) SetDetached(v bool) *AgentCreate { + _c.mutation.SetDetached(v) + return _c +} + +// SetNillableDetached sets the "detached" field if the given value is not nil. +func (_c *AgentCreate) SetNillableDetached(v *bool) *AgentCreate { + if v != nil { + _c.SetDetached(*v) + } + return _c +} + +// SetRuntime sets the "runtime" field. +func (_c *AgentCreate) SetRuntime(v string) *AgentCreate { + _c.mutation.SetRuntime(v) + return _c +} + +// SetNillableRuntime sets the "runtime" field if the given value is not nil. +func (_c *AgentCreate) SetNillableRuntime(v *string) *AgentCreate { + if v != nil { + _c.SetRuntime(*v) + } + return _c +} + +// SetRuntimeBrokerID sets the "runtime_broker_id" field. +func (_c *AgentCreate) SetRuntimeBrokerID(v string) *AgentCreate { + _c.mutation.SetRuntimeBrokerID(v) + return _c +} + +// SetNillableRuntimeBrokerID sets the "runtime_broker_id" field if the given value is not nil. +func (_c *AgentCreate) SetNillableRuntimeBrokerID(v *string) *AgentCreate { + if v != nil { + _c.SetRuntimeBrokerID(*v) + } + return _c +} + +// SetWebPtyEnabled sets the "web_pty_enabled" field. +func (_c *AgentCreate) SetWebPtyEnabled(v bool) *AgentCreate { + _c.mutation.SetWebPtyEnabled(v) + return _c +} + +// SetNillableWebPtyEnabled sets the "web_pty_enabled" field if the given value is not nil. +func (_c *AgentCreate) SetNillableWebPtyEnabled(v *bool) *AgentCreate { + if v != nil { + _c.SetWebPtyEnabled(*v) + } + return _c +} + +// SetTaskSummary sets the "task_summary" field. +func (_c *AgentCreate) SetTaskSummary(v string) *AgentCreate { + _c.mutation.SetTaskSummary(v) + return _c +} + +// SetNillableTaskSummary sets the "task_summary" field if the given value is not nil. +func (_c *AgentCreate) SetNillableTaskSummary(v *string) *AgentCreate { + if v != nil { + _c.SetTaskSummary(*v) + } + return _c +} + +// SetMessage sets the "message" field. +func (_c *AgentCreate) SetMessage(v string) *AgentCreate { + _c.mutation.SetMessage(v) + return _c +} + +// SetNillableMessage sets the "message" field if the given value is not nil. +func (_c *AgentCreate) SetNillableMessage(v *string) *AgentCreate { + if v != nil { + _c.SetMessage(*v) + } + return _c +} + +// SetAppliedConfig sets the "applied_config" field. +func (_c *AgentCreate) SetAppliedConfig(v string) *AgentCreate { + _c.mutation.SetAppliedConfig(v) + return _c +} + +// SetNillableAppliedConfig sets the "applied_config" field if the given value is not nil. +func (_c *AgentCreate) SetNillableAppliedConfig(v *string) *AgentCreate { + if v != nil { + _c.SetAppliedConfig(*v) + } + return _c +} + +// SetAncestry sets the "ancestry" field. +func (_c *AgentCreate) SetAncestry(v []string) *AgentCreate { + _c.mutation.SetAncestry(v) + return _c +} + // SetCreated sets the "created" field. func (_c *AgentCreate) SetCreated(v time.Time) *AgentCreate { _c.mutation.SetCreated(v) @@ -155,47 +413,93 @@ func (_c *AgentCreate) SetNillableUpdated(v *time.Time) *AgentCreate { return _c } -// SetID sets the "id" field. -func (_c *AgentCreate) SetID(v uuid.UUID) *AgentCreate { - _c.mutation.SetID(v) +// SetLastSeen sets the "last_seen" field. +func (_c *AgentCreate) SetLastSeen(v time.Time) *AgentCreate { + _c.mutation.SetLastSeen(v) return _c } -// SetNillableID sets the "id" field if the given value is not nil. -func (_c *AgentCreate) SetNillableID(v *uuid.UUID) *AgentCreate { +// SetNillableLastSeen sets the "last_seen" field if the given value is not nil. +func (_c *AgentCreate) SetNillableLastSeen(v *time.Time) *AgentCreate { if v != nil { - _c.SetID(*v) + _c.SetLastSeen(*v) } return _c } -// SetProject sets the "project" edge to the Project entity. -func (_c *AgentCreate) SetProject(v *Project) *AgentCreate { - return _c.SetProjectID(v.ID) +// SetLastActivityEvent sets the "last_activity_event" field. +func (_c *AgentCreate) SetLastActivityEvent(v time.Time) *AgentCreate { + _c.mutation.SetLastActivityEvent(v) + return _c +} + +// SetNillableLastActivityEvent sets the "last_activity_event" field if the given value is not nil. +func (_c *AgentCreate) SetNillableLastActivityEvent(v *time.Time) *AgentCreate { + if v != nil { + _c.SetLastActivityEvent(*v) + } + return _c } -// SetCreatorID sets the "creator" edge to the User entity by ID. -func (_c *AgentCreate) SetCreatorID(id uuid.UUID) *AgentCreate { - _c.mutation.SetCreatorID(id) +// SetStartedAt sets the "started_at" field. +func (_c *AgentCreate) SetStartedAt(v time.Time) *AgentCreate { + _c.mutation.SetStartedAt(v) return _c } -// SetNillableCreatorID sets the "creator" edge to the User entity by ID if the given value is not nil. -func (_c *AgentCreate) SetNillableCreatorID(id *uuid.UUID) *AgentCreate { - if id != nil { - _c = _c.SetCreatorID(*id) +// SetNillableStartedAt sets the "started_at" field if the given value is not nil. +func (_c *AgentCreate) SetNillableStartedAt(v *time.Time) *AgentCreate { + if v != nil { + _c.SetStartedAt(*v) + } + return _c +} + +// SetDeletedAt sets the "deleted_at" field. +func (_c *AgentCreate) SetDeletedAt(v time.Time) *AgentCreate { + _c.mutation.SetDeletedAt(v) + return _c +} + +// SetNillableDeletedAt sets the "deleted_at" field if the given value is not nil. +func (_c *AgentCreate) SetNillableDeletedAt(v *time.Time) *AgentCreate { + if v != nil { + _c.SetDeletedAt(*v) + } + return _c +} + +// SetStateVersion sets the "state_version" field. +func (_c *AgentCreate) SetStateVersion(v int64) *AgentCreate { + _c.mutation.SetStateVersion(v) + return _c +} + +// SetNillableStateVersion sets the "state_version" field if the given value is not nil. +func (_c *AgentCreate) SetNillableStateVersion(v *int64) *AgentCreate { + if v != nil { + _c.SetStateVersion(*v) } return _c } -// SetCreator sets the "creator" edge to the User entity. -func (_c *AgentCreate) SetCreator(v *User) *AgentCreate { - return _c.SetCreatorID(v.ID) +// SetID sets the "id" field. +func (_c *AgentCreate) SetID(v uuid.UUID) *AgentCreate { + _c.mutation.SetID(v) + return _c +} + +// SetNillableID sets the "id" field if the given value is not nil. +func (_c *AgentCreate) SetNillableID(v *uuid.UUID) *AgentCreate { + if v != nil { + _c.SetID(*v) + } + return _c } -// SetOwner sets the "owner" edge to the User entity. -func (_c *AgentCreate) SetOwner(v *User) *AgentCreate { - return _c.SetOwnerID(v.ID) +// SetProject sets the "project" edge to the Project entity. +func (_c *AgentCreate) SetProject(v *Project) *AgentCreate { + return _c.SetProjectID(v.ID) } // AddMembershipIDs adds the "memberships" edge to the GroupMembership entity by IDs. @@ -275,6 +579,22 @@ func (_c *AgentCreate) defaults() { v := agent.DefaultVisibility _c.mutation.SetVisibility(v) } + if _, ok := _c.mutation.CurrentTurns(); !ok { + v := agent.DefaultCurrentTurns + _c.mutation.SetCurrentTurns(v) + } + if _, ok := _c.mutation.CurrentModelCalls(); !ok { + v := agent.DefaultCurrentModelCalls + _c.mutation.SetCurrentModelCalls(v) + } + if _, ok := _c.mutation.Detached(); !ok { + v := agent.DefaultDetached + _c.mutation.SetDetached(v) + } + if _, ok := _c.mutation.WebPtyEnabled(); !ok { + v := agent.DefaultWebPtyEnabled + _c.mutation.SetWebPtyEnabled(v) + } if _, ok := _c.mutation.Created(); !ok { v := agent.DefaultCreated() _c.mutation.SetCreated(v) @@ -283,6 +603,10 @@ func (_c *AgentCreate) defaults() { v := agent.DefaultUpdated() _c.mutation.SetUpdated(v) } + if _, ok := _c.mutation.StateVersion(); !ok { + v := agent.DefaultStateVersion + _c.mutation.SetStateVersion(v) + } if _, ok := _c.mutation.ID(); !ok { v := agent.DefaultID() _c.mutation.SetID(v) @@ -324,12 +648,27 @@ func (_c *AgentCreate) check() error { if _, ok := _c.mutation.Visibility(); !ok { return &ValidationError{Name: "visibility", err: errors.New(`ent: missing required field "Agent.visibility"`)} } + if _, ok := _c.mutation.CurrentTurns(); !ok { + return &ValidationError{Name: "current_turns", err: errors.New(`ent: missing required field "Agent.current_turns"`)} + } + if _, ok := _c.mutation.CurrentModelCalls(); !ok { + return &ValidationError{Name: "current_model_calls", err: errors.New(`ent: missing required field "Agent.current_model_calls"`)} + } + if _, ok := _c.mutation.Detached(); !ok { + return &ValidationError{Name: "detached", err: errors.New(`ent: missing required field "Agent.detached"`)} + } + if _, ok := _c.mutation.WebPtyEnabled(); !ok { + return &ValidationError{Name: "web_pty_enabled", err: errors.New(`ent: missing required field "Agent.web_pty_enabled"`)} + } if _, ok := _c.mutation.Created(); !ok { return &ValidationError{Name: "created", err: errors.New(`ent: missing required field "Agent.created"`)} } if _, ok := _c.mutation.Updated(); !ok { return &ValidationError{Name: "updated", err: errors.New(`ent: missing required field "Agent.updated"`)} } + if _, ok := _c.mutation.StateVersion(); !ok { + return &ValidationError{Name: "state_version", err: errors.New(`ent: missing required field "Agent.state_version"`)} + } if len(_c.mutation.ProjectIDs()) == 0 { return &ValidationError{Name: "project", err: errors.New(`ent: missing required edge "Agent.project"`)} } @@ -364,6 +703,7 @@ func (_c *AgentCreate) createSpec() (*Agent, *sqlgraph.CreateSpec) { _node = &Agent{config: _c.config} _spec = sqlgraph.NewCreateSpec(agent.Table, sqlgraph.NewFieldSpec(agent.FieldID, field.TypeUUID)) ) + _spec.OnConflict = _c.conflict if id, ok := _c.mutation.ID(); ok { _node.ID = id _spec.ID.Value = &id @@ -384,6 +724,14 @@ func (_c *AgentCreate) createSpec() (*Agent, *sqlgraph.CreateSpec) { _spec.SetField(agent.FieldStatus, field.TypeEnum, value) _node.Status = value } + if value, ok := _c.mutation.CreatedBy(); ok { + _spec.SetField(agent.FieldCreatedBy, field.TypeUUID, value) + _node.CreatedBy = &value + } + if value, ok := _c.mutation.OwnerID(); ok { + _spec.SetField(agent.FieldOwnerID, field.TypeUUID, value) + _node.OwnerID = &value + } if value, ok := _c.mutation.DelegationEnabled(); ok { _spec.SetField(agent.FieldDelegationEnabled, field.TypeBool, value) _node.DelegationEnabled = value @@ -392,6 +740,86 @@ func (_c *AgentCreate) createSpec() (*Agent, *sqlgraph.CreateSpec) { _spec.SetField(agent.FieldVisibility, field.TypeString, value) _node.Visibility = value } + if value, ok := _c.mutation.Labels(); ok { + _spec.SetField(agent.FieldLabels, field.TypeJSON, value) + _node.Labels = value + } + if value, ok := _c.mutation.Annotations(); ok { + _spec.SetField(agent.FieldAnnotations, field.TypeJSON, value) + _node.Annotations = value + } + if value, ok := _c.mutation.Phase(); ok { + _spec.SetField(agent.FieldPhase, field.TypeString, value) + _node.Phase = value + } + if value, ok := _c.mutation.Activity(); ok { + _spec.SetField(agent.FieldActivity, field.TypeString, value) + _node.Activity = value + } + if value, ok := _c.mutation.ToolName(); ok { + _spec.SetField(agent.FieldToolName, field.TypeString, value) + _node.ToolName = value + } + if value, ok := _c.mutation.ConnectionState(); ok { + _spec.SetField(agent.FieldConnectionState, field.TypeString, value) + _node.ConnectionState = value + } + if value, ok := _c.mutation.ContainerStatus(); ok { + _spec.SetField(agent.FieldContainerStatus, field.TypeString, value) + _node.ContainerStatus = value + } + if value, ok := _c.mutation.RuntimeState(); ok { + _spec.SetField(agent.FieldRuntimeState, field.TypeString, value) + _node.RuntimeState = value + } + if value, ok := _c.mutation.StalledFromActivity(); ok { + _spec.SetField(agent.FieldStalledFromActivity, field.TypeString, value) + _node.StalledFromActivity = value + } + if value, ok := _c.mutation.CurrentTurns(); ok { + _spec.SetField(agent.FieldCurrentTurns, field.TypeInt, value) + _node.CurrentTurns = value + } + if value, ok := _c.mutation.CurrentModelCalls(); ok { + _spec.SetField(agent.FieldCurrentModelCalls, field.TypeInt, value) + _node.CurrentModelCalls = value + } + if value, ok := _c.mutation.Image(); ok { + _spec.SetField(agent.FieldImage, field.TypeString, value) + _node.Image = value + } + if value, ok := _c.mutation.Detached(); ok { + _spec.SetField(agent.FieldDetached, field.TypeBool, value) + _node.Detached = value + } + if value, ok := _c.mutation.Runtime(); ok { + _spec.SetField(agent.FieldRuntime, field.TypeString, value) + _node.Runtime = value + } + if value, ok := _c.mutation.RuntimeBrokerID(); ok { + _spec.SetField(agent.FieldRuntimeBrokerID, field.TypeString, value) + _node.RuntimeBrokerID = value + } + if value, ok := _c.mutation.WebPtyEnabled(); ok { + _spec.SetField(agent.FieldWebPtyEnabled, field.TypeBool, value) + _node.WebPtyEnabled = value + } + if value, ok := _c.mutation.TaskSummary(); ok { + _spec.SetField(agent.FieldTaskSummary, field.TypeString, value) + _node.TaskSummary = value + } + if value, ok := _c.mutation.Message(); ok { + _spec.SetField(agent.FieldMessage, field.TypeString, value) + _node.Message = value + } + if value, ok := _c.mutation.AppliedConfig(); ok { + _spec.SetField(agent.FieldAppliedConfig, field.TypeString, value) + _node.AppliedConfig = value + } + if value, ok := _c.mutation.Ancestry(); ok { + _spec.SetField(agent.FieldAncestry, field.TypeJSON, value) + _node.Ancestry = value + } if value, ok := _c.mutation.Created(); ok { _spec.SetField(agent.FieldCreated, field.TypeTime, value) _node.Created = value @@ -400,6 +828,26 @@ func (_c *AgentCreate) createSpec() (*Agent, *sqlgraph.CreateSpec) { _spec.SetField(agent.FieldUpdated, field.TypeTime, value) _node.Updated = value } + if value, ok := _c.mutation.LastSeen(); ok { + _spec.SetField(agent.FieldLastSeen, field.TypeTime, value) + _node.LastSeen = &value + } + if value, ok := _c.mutation.LastActivityEvent(); ok { + _spec.SetField(agent.FieldLastActivityEvent, field.TypeTime, value) + _node.LastActivityEvent = &value + } + if value, ok := _c.mutation.StartedAt(); ok { + _spec.SetField(agent.FieldStartedAt, field.TypeTime, value) + _node.StartedAt = &value + } + if value, ok := _c.mutation.DeletedAt(); ok { + _spec.SetField(agent.FieldDeletedAt, field.TypeTime, value) + _node.DeletedAt = &value + } + if value, ok := _c.mutation.StateVersion(); ok { + _spec.SetField(agent.FieldStateVersion, field.TypeInt64, value) + _node.StateVersion = value + } if nodes := _c.mutation.ProjectIDs(); len(nodes) > 0 { edge := &sqlgraph.EdgeSpec{ Rel: sqlgraph.M2O, @@ -417,40 +865,6 @@ func (_c *AgentCreate) createSpec() (*Agent, *sqlgraph.CreateSpec) { _node.ProjectID = nodes[0] _spec.Edges = append(_spec.Edges, edge) } - if nodes := _c.mutation.CreatorIDs(); len(nodes) > 0 { - edge := &sqlgraph.EdgeSpec{ - Rel: sqlgraph.M2O, - Inverse: true, - Table: agent.CreatorTable, - Columns: []string{agent.CreatorColumn}, - Bidi: false, - Target: &sqlgraph.EdgeTarget{ - IDSpec: sqlgraph.NewFieldSpec(user.FieldID, field.TypeUUID), - }, - } - for _, k := range nodes { - edge.Target.Nodes = append(edge.Target.Nodes, k) - } - _node.CreatedBy = &nodes[0] - _spec.Edges = append(_spec.Edges, edge) - } - if nodes := _c.mutation.OwnerIDs(); len(nodes) > 0 { - edge := &sqlgraph.EdgeSpec{ - Rel: sqlgraph.M2O, - Inverse: true, - Table: agent.OwnerTable, - Columns: []string{agent.OwnerColumn}, - Bidi: false, - Target: &sqlgraph.EdgeTarget{ - IDSpec: sqlgraph.NewFieldSpec(user.FieldID, field.TypeUUID), - }, - } - for _, k := range nodes { - edge.Target.Nodes = append(edge.Target.Nodes, k) - } - _node.OwnerID = &nodes[0] - _spec.Edges = append(_spec.Edges, edge) - } if nodes := _c.mutation.MembershipsIDs(); len(nodes) > 0 { edge := &sqlgraph.EdgeSpec{ Rel: sqlgraph.O2M, @@ -486,11 +900,1398 @@ func (_c *AgentCreate) createSpec() (*Agent, *sqlgraph.CreateSpec) { return _node, _spec } -// AgentCreateBulk is the builder for creating many Agent entities in bulk. -type AgentCreateBulk struct { - config +// OnConflict allows configuring the `ON CONFLICT` / `ON DUPLICATE KEY` clause +// of the `INSERT` statement. For example: +// +// client.Agent.Create(). +// SetSlug(v). +// OnConflict( +// // Update the row with the new values +// // the was proposed for insertion. +// sql.ResolveWithNewValues(), +// ). +// // Override some of the fields with custom +// // update values. +// Update(func(u *ent.AgentUpsert) { +// SetSlug(v+v). +// }). +// Exec(ctx) +func (_c *AgentCreate) OnConflict(opts ...sql.ConflictOption) *AgentUpsertOne { + _c.conflict = opts + return &AgentUpsertOne{ + create: _c, + } +} + +// OnConflictColumns calls `OnConflict` and configures the columns +// as conflict target. Using this option is equivalent to using: +// +// client.Agent.Create(). +// OnConflict(sql.ConflictColumns(columns...)). +// Exec(ctx) +func (_c *AgentCreate) OnConflictColumns(columns ...string) *AgentUpsertOne { + _c.conflict = append(_c.conflict, sql.ConflictColumns(columns...)) + return &AgentUpsertOne{ + create: _c, + } +} + +type ( + // AgentUpsertOne is the builder for "upsert"-ing + // one Agent node. + AgentUpsertOne struct { + create *AgentCreate + } + + // AgentUpsert is the "OnConflict" setter. + AgentUpsert struct { + *sql.UpdateSet + } +) + +// SetSlug sets the "slug" field. +func (u *AgentUpsert) SetSlug(v string) *AgentUpsert { + u.Set(agent.FieldSlug, v) + return u +} + +// UpdateSlug sets the "slug" field to the value that was provided on create. +func (u *AgentUpsert) UpdateSlug() *AgentUpsert { + u.SetExcluded(agent.FieldSlug) + return u +} + +// SetName sets the "name" field. +func (u *AgentUpsert) SetName(v string) *AgentUpsert { + u.Set(agent.FieldName, v) + return u +} + +// UpdateName sets the "name" field to the value that was provided on create. +func (u *AgentUpsert) UpdateName() *AgentUpsert { + u.SetExcluded(agent.FieldName) + return u +} + +// SetTemplate sets the "template" field. +func (u *AgentUpsert) SetTemplate(v string) *AgentUpsert { + u.Set(agent.FieldTemplate, v) + return u +} + +// UpdateTemplate sets the "template" field to the value that was provided on create. +func (u *AgentUpsert) UpdateTemplate() *AgentUpsert { + u.SetExcluded(agent.FieldTemplate) + return u +} + +// ClearTemplate clears the value of the "template" field. +func (u *AgentUpsert) ClearTemplate() *AgentUpsert { + u.SetNull(agent.FieldTemplate) + return u +} + +// SetProjectID sets the "project_id" field. +func (u *AgentUpsert) SetProjectID(v uuid.UUID) *AgentUpsert { + u.Set(agent.FieldProjectID, v) + return u +} + +// UpdateProjectID sets the "project_id" field to the value that was provided on create. +func (u *AgentUpsert) UpdateProjectID() *AgentUpsert { + u.SetExcluded(agent.FieldProjectID) + return u +} + +// SetStatus sets the "status" field. +func (u *AgentUpsert) SetStatus(v agent.Status) *AgentUpsert { + u.Set(agent.FieldStatus, v) + return u +} + +// UpdateStatus sets the "status" field to the value that was provided on create. +func (u *AgentUpsert) UpdateStatus() *AgentUpsert { + u.SetExcluded(agent.FieldStatus) + return u +} + +// SetCreatedBy sets the "created_by" field. +func (u *AgentUpsert) SetCreatedBy(v uuid.UUID) *AgentUpsert { + u.Set(agent.FieldCreatedBy, v) + return u +} + +// UpdateCreatedBy sets the "created_by" field to the value that was provided on create. +func (u *AgentUpsert) UpdateCreatedBy() *AgentUpsert { + u.SetExcluded(agent.FieldCreatedBy) + return u +} + +// ClearCreatedBy clears the value of the "created_by" field. +func (u *AgentUpsert) ClearCreatedBy() *AgentUpsert { + u.SetNull(agent.FieldCreatedBy) + return u +} + +// SetOwnerID sets the "owner_id" field. +func (u *AgentUpsert) SetOwnerID(v uuid.UUID) *AgentUpsert { + u.Set(agent.FieldOwnerID, v) + return u +} + +// UpdateOwnerID sets the "owner_id" field to the value that was provided on create. +func (u *AgentUpsert) UpdateOwnerID() *AgentUpsert { + u.SetExcluded(agent.FieldOwnerID) + return u +} + +// ClearOwnerID clears the value of the "owner_id" field. +func (u *AgentUpsert) ClearOwnerID() *AgentUpsert { + u.SetNull(agent.FieldOwnerID) + return u +} + +// SetDelegationEnabled sets the "delegation_enabled" field. +func (u *AgentUpsert) SetDelegationEnabled(v bool) *AgentUpsert { + u.Set(agent.FieldDelegationEnabled, v) + return u +} + +// UpdateDelegationEnabled sets the "delegation_enabled" field to the value that was provided on create. +func (u *AgentUpsert) UpdateDelegationEnabled() *AgentUpsert { + u.SetExcluded(agent.FieldDelegationEnabled) + return u +} + +// SetVisibility sets the "visibility" field. +func (u *AgentUpsert) SetVisibility(v string) *AgentUpsert { + u.Set(agent.FieldVisibility, v) + return u +} + +// UpdateVisibility sets the "visibility" field to the value that was provided on create. +func (u *AgentUpsert) UpdateVisibility() *AgentUpsert { + u.SetExcluded(agent.FieldVisibility) + return u +} + +// SetLabels sets the "labels" field. +func (u *AgentUpsert) SetLabels(v map[string]string) *AgentUpsert { + u.Set(agent.FieldLabels, v) + return u +} + +// UpdateLabels sets the "labels" field to the value that was provided on create. +func (u *AgentUpsert) UpdateLabels() *AgentUpsert { + u.SetExcluded(agent.FieldLabels) + return u +} + +// ClearLabels clears the value of the "labels" field. +func (u *AgentUpsert) ClearLabels() *AgentUpsert { + u.SetNull(agent.FieldLabels) + return u +} + +// SetAnnotations sets the "annotations" field. +func (u *AgentUpsert) SetAnnotations(v map[string]string) *AgentUpsert { + u.Set(agent.FieldAnnotations, v) + return u +} + +// UpdateAnnotations sets the "annotations" field to the value that was provided on create. +func (u *AgentUpsert) UpdateAnnotations() *AgentUpsert { + u.SetExcluded(agent.FieldAnnotations) + return u +} + +// ClearAnnotations clears the value of the "annotations" field. +func (u *AgentUpsert) ClearAnnotations() *AgentUpsert { + u.SetNull(agent.FieldAnnotations) + return u +} + +// SetPhase sets the "phase" field. +func (u *AgentUpsert) SetPhase(v string) *AgentUpsert { + u.Set(agent.FieldPhase, v) + return u +} + +// UpdatePhase sets the "phase" field to the value that was provided on create. +func (u *AgentUpsert) UpdatePhase() *AgentUpsert { + u.SetExcluded(agent.FieldPhase) + return u +} + +// ClearPhase clears the value of the "phase" field. +func (u *AgentUpsert) ClearPhase() *AgentUpsert { + u.SetNull(agent.FieldPhase) + return u +} + +// SetActivity sets the "activity" field. +func (u *AgentUpsert) SetActivity(v string) *AgentUpsert { + u.Set(agent.FieldActivity, v) + return u +} + +// UpdateActivity sets the "activity" field to the value that was provided on create. +func (u *AgentUpsert) UpdateActivity() *AgentUpsert { + u.SetExcluded(agent.FieldActivity) + return u +} + +// ClearActivity clears the value of the "activity" field. +func (u *AgentUpsert) ClearActivity() *AgentUpsert { + u.SetNull(agent.FieldActivity) + return u +} + +// SetToolName sets the "tool_name" field. +func (u *AgentUpsert) SetToolName(v string) *AgentUpsert { + u.Set(agent.FieldToolName, v) + return u +} + +// UpdateToolName sets the "tool_name" field to the value that was provided on create. +func (u *AgentUpsert) UpdateToolName() *AgentUpsert { + u.SetExcluded(agent.FieldToolName) + return u +} + +// ClearToolName clears the value of the "tool_name" field. +func (u *AgentUpsert) ClearToolName() *AgentUpsert { + u.SetNull(agent.FieldToolName) + return u +} + +// SetConnectionState sets the "connection_state" field. +func (u *AgentUpsert) SetConnectionState(v string) *AgentUpsert { + u.Set(agent.FieldConnectionState, v) + return u +} + +// UpdateConnectionState sets the "connection_state" field to the value that was provided on create. +func (u *AgentUpsert) UpdateConnectionState() *AgentUpsert { + u.SetExcluded(agent.FieldConnectionState) + return u +} + +// ClearConnectionState clears the value of the "connection_state" field. +func (u *AgentUpsert) ClearConnectionState() *AgentUpsert { + u.SetNull(agent.FieldConnectionState) + return u +} + +// SetContainerStatus sets the "container_status" field. +func (u *AgentUpsert) SetContainerStatus(v string) *AgentUpsert { + u.Set(agent.FieldContainerStatus, v) + return u +} + +// UpdateContainerStatus sets the "container_status" field to the value that was provided on create. +func (u *AgentUpsert) UpdateContainerStatus() *AgentUpsert { + u.SetExcluded(agent.FieldContainerStatus) + return u +} + +// ClearContainerStatus clears the value of the "container_status" field. +func (u *AgentUpsert) ClearContainerStatus() *AgentUpsert { + u.SetNull(agent.FieldContainerStatus) + return u +} + +// SetRuntimeState sets the "runtime_state" field. +func (u *AgentUpsert) SetRuntimeState(v string) *AgentUpsert { + u.Set(agent.FieldRuntimeState, v) + return u +} + +// UpdateRuntimeState sets the "runtime_state" field to the value that was provided on create. +func (u *AgentUpsert) UpdateRuntimeState() *AgentUpsert { + u.SetExcluded(agent.FieldRuntimeState) + return u +} + +// ClearRuntimeState clears the value of the "runtime_state" field. +func (u *AgentUpsert) ClearRuntimeState() *AgentUpsert { + u.SetNull(agent.FieldRuntimeState) + return u +} + +// SetStalledFromActivity sets the "stalled_from_activity" field. +func (u *AgentUpsert) SetStalledFromActivity(v string) *AgentUpsert { + u.Set(agent.FieldStalledFromActivity, v) + return u +} + +// UpdateStalledFromActivity sets the "stalled_from_activity" field to the value that was provided on create. +func (u *AgentUpsert) UpdateStalledFromActivity() *AgentUpsert { + u.SetExcluded(agent.FieldStalledFromActivity) + return u +} + +// ClearStalledFromActivity clears the value of the "stalled_from_activity" field. +func (u *AgentUpsert) ClearStalledFromActivity() *AgentUpsert { + u.SetNull(agent.FieldStalledFromActivity) + return u +} + +// SetCurrentTurns sets the "current_turns" field. +func (u *AgentUpsert) SetCurrentTurns(v int) *AgentUpsert { + u.Set(agent.FieldCurrentTurns, v) + return u +} + +// UpdateCurrentTurns sets the "current_turns" field to the value that was provided on create. +func (u *AgentUpsert) UpdateCurrentTurns() *AgentUpsert { + u.SetExcluded(agent.FieldCurrentTurns) + return u +} + +// AddCurrentTurns adds v to the "current_turns" field. +func (u *AgentUpsert) AddCurrentTurns(v int) *AgentUpsert { + u.Add(agent.FieldCurrentTurns, v) + return u +} + +// SetCurrentModelCalls sets the "current_model_calls" field. +func (u *AgentUpsert) SetCurrentModelCalls(v int) *AgentUpsert { + u.Set(agent.FieldCurrentModelCalls, v) + return u +} + +// UpdateCurrentModelCalls sets the "current_model_calls" field to the value that was provided on create. +func (u *AgentUpsert) UpdateCurrentModelCalls() *AgentUpsert { + u.SetExcluded(agent.FieldCurrentModelCalls) + return u +} + +// AddCurrentModelCalls adds v to the "current_model_calls" field. +func (u *AgentUpsert) AddCurrentModelCalls(v int) *AgentUpsert { + u.Add(agent.FieldCurrentModelCalls, v) + return u +} + +// SetImage sets the "image" field. +func (u *AgentUpsert) SetImage(v string) *AgentUpsert { + u.Set(agent.FieldImage, v) + return u +} + +// UpdateImage sets the "image" field to the value that was provided on create. +func (u *AgentUpsert) UpdateImage() *AgentUpsert { + u.SetExcluded(agent.FieldImage) + return u +} + +// ClearImage clears the value of the "image" field. +func (u *AgentUpsert) ClearImage() *AgentUpsert { + u.SetNull(agent.FieldImage) + return u +} + +// SetDetached sets the "detached" field. +func (u *AgentUpsert) SetDetached(v bool) *AgentUpsert { + u.Set(agent.FieldDetached, v) + return u +} + +// UpdateDetached sets the "detached" field to the value that was provided on create. +func (u *AgentUpsert) UpdateDetached() *AgentUpsert { + u.SetExcluded(agent.FieldDetached) + return u +} + +// SetRuntime sets the "runtime" field. +func (u *AgentUpsert) SetRuntime(v string) *AgentUpsert { + u.Set(agent.FieldRuntime, v) + return u +} + +// UpdateRuntime sets the "runtime" field to the value that was provided on create. +func (u *AgentUpsert) UpdateRuntime() *AgentUpsert { + u.SetExcluded(agent.FieldRuntime) + return u +} + +// ClearRuntime clears the value of the "runtime" field. +func (u *AgentUpsert) ClearRuntime() *AgentUpsert { + u.SetNull(agent.FieldRuntime) + return u +} + +// SetRuntimeBrokerID sets the "runtime_broker_id" field. +func (u *AgentUpsert) SetRuntimeBrokerID(v string) *AgentUpsert { + u.Set(agent.FieldRuntimeBrokerID, v) + return u +} + +// UpdateRuntimeBrokerID sets the "runtime_broker_id" field to the value that was provided on create. +func (u *AgentUpsert) UpdateRuntimeBrokerID() *AgentUpsert { + u.SetExcluded(agent.FieldRuntimeBrokerID) + return u +} + +// ClearRuntimeBrokerID clears the value of the "runtime_broker_id" field. +func (u *AgentUpsert) ClearRuntimeBrokerID() *AgentUpsert { + u.SetNull(agent.FieldRuntimeBrokerID) + return u +} + +// SetWebPtyEnabled sets the "web_pty_enabled" field. +func (u *AgentUpsert) SetWebPtyEnabled(v bool) *AgentUpsert { + u.Set(agent.FieldWebPtyEnabled, v) + return u +} + +// UpdateWebPtyEnabled sets the "web_pty_enabled" field to the value that was provided on create. +func (u *AgentUpsert) UpdateWebPtyEnabled() *AgentUpsert { + u.SetExcluded(agent.FieldWebPtyEnabled) + return u +} + +// SetTaskSummary sets the "task_summary" field. +func (u *AgentUpsert) SetTaskSummary(v string) *AgentUpsert { + u.Set(agent.FieldTaskSummary, v) + return u +} + +// UpdateTaskSummary sets the "task_summary" field to the value that was provided on create. +func (u *AgentUpsert) UpdateTaskSummary() *AgentUpsert { + u.SetExcluded(agent.FieldTaskSummary) + return u +} + +// ClearTaskSummary clears the value of the "task_summary" field. +func (u *AgentUpsert) ClearTaskSummary() *AgentUpsert { + u.SetNull(agent.FieldTaskSummary) + return u +} + +// SetMessage sets the "message" field. +func (u *AgentUpsert) SetMessage(v string) *AgentUpsert { + u.Set(agent.FieldMessage, v) + return u +} + +// UpdateMessage sets the "message" field to the value that was provided on create. +func (u *AgentUpsert) UpdateMessage() *AgentUpsert { + u.SetExcluded(agent.FieldMessage) + return u +} + +// ClearMessage clears the value of the "message" field. +func (u *AgentUpsert) ClearMessage() *AgentUpsert { + u.SetNull(agent.FieldMessage) + return u +} + +// SetAppliedConfig sets the "applied_config" field. +func (u *AgentUpsert) SetAppliedConfig(v string) *AgentUpsert { + u.Set(agent.FieldAppliedConfig, v) + return u +} + +// UpdateAppliedConfig sets the "applied_config" field to the value that was provided on create. +func (u *AgentUpsert) UpdateAppliedConfig() *AgentUpsert { + u.SetExcluded(agent.FieldAppliedConfig) + return u +} + +// ClearAppliedConfig clears the value of the "applied_config" field. +func (u *AgentUpsert) ClearAppliedConfig() *AgentUpsert { + u.SetNull(agent.FieldAppliedConfig) + return u +} + +// SetAncestry sets the "ancestry" field. +func (u *AgentUpsert) SetAncestry(v []string) *AgentUpsert { + u.Set(agent.FieldAncestry, v) + return u +} + +// UpdateAncestry sets the "ancestry" field to the value that was provided on create. +func (u *AgentUpsert) UpdateAncestry() *AgentUpsert { + u.SetExcluded(agent.FieldAncestry) + return u +} + +// ClearAncestry clears the value of the "ancestry" field. +func (u *AgentUpsert) ClearAncestry() *AgentUpsert { + u.SetNull(agent.FieldAncestry) + return u +} + +// SetUpdated sets the "updated" field. +func (u *AgentUpsert) SetUpdated(v time.Time) *AgentUpsert { + u.Set(agent.FieldUpdated, v) + return u +} + +// UpdateUpdated sets the "updated" field to the value that was provided on create. +func (u *AgentUpsert) UpdateUpdated() *AgentUpsert { + u.SetExcluded(agent.FieldUpdated) + return u +} + +// SetLastSeen sets the "last_seen" field. +func (u *AgentUpsert) SetLastSeen(v time.Time) *AgentUpsert { + u.Set(agent.FieldLastSeen, v) + return u +} + +// UpdateLastSeen sets the "last_seen" field to the value that was provided on create. +func (u *AgentUpsert) UpdateLastSeen() *AgentUpsert { + u.SetExcluded(agent.FieldLastSeen) + return u +} + +// ClearLastSeen clears the value of the "last_seen" field. +func (u *AgentUpsert) ClearLastSeen() *AgentUpsert { + u.SetNull(agent.FieldLastSeen) + return u +} + +// SetLastActivityEvent sets the "last_activity_event" field. +func (u *AgentUpsert) SetLastActivityEvent(v time.Time) *AgentUpsert { + u.Set(agent.FieldLastActivityEvent, v) + return u +} + +// UpdateLastActivityEvent sets the "last_activity_event" field to the value that was provided on create. +func (u *AgentUpsert) UpdateLastActivityEvent() *AgentUpsert { + u.SetExcluded(agent.FieldLastActivityEvent) + return u +} + +// ClearLastActivityEvent clears the value of the "last_activity_event" field. +func (u *AgentUpsert) ClearLastActivityEvent() *AgentUpsert { + u.SetNull(agent.FieldLastActivityEvent) + return u +} + +// SetStartedAt sets the "started_at" field. +func (u *AgentUpsert) SetStartedAt(v time.Time) *AgentUpsert { + u.Set(agent.FieldStartedAt, v) + return u +} + +// UpdateStartedAt sets the "started_at" field to the value that was provided on create. +func (u *AgentUpsert) UpdateStartedAt() *AgentUpsert { + u.SetExcluded(agent.FieldStartedAt) + return u +} + +// ClearStartedAt clears the value of the "started_at" field. +func (u *AgentUpsert) ClearStartedAt() *AgentUpsert { + u.SetNull(agent.FieldStartedAt) + return u +} + +// SetDeletedAt sets the "deleted_at" field. +func (u *AgentUpsert) SetDeletedAt(v time.Time) *AgentUpsert { + u.Set(agent.FieldDeletedAt, v) + return u +} + +// UpdateDeletedAt sets the "deleted_at" field to the value that was provided on create. +func (u *AgentUpsert) UpdateDeletedAt() *AgentUpsert { + u.SetExcluded(agent.FieldDeletedAt) + return u +} + +// ClearDeletedAt clears the value of the "deleted_at" field. +func (u *AgentUpsert) ClearDeletedAt() *AgentUpsert { + u.SetNull(agent.FieldDeletedAt) + return u +} + +// SetStateVersion sets the "state_version" field. +func (u *AgentUpsert) SetStateVersion(v int64) *AgentUpsert { + u.Set(agent.FieldStateVersion, v) + return u +} + +// UpdateStateVersion sets the "state_version" field to the value that was provided on create. +func (u *AgentUpsert) UpdateStateVersion() *AgentUpsert { + u.SetExcluded(agent.FieldStateVersion) + return u +} + +// AddStateVersion adds v to the "state_version" field. +func (u *AgentUpsert) AddStateVersion(v int64) *AgentUpsert { + u.Add(agent.FieldStateVersion, v) + return u +} + +// UpdateNewValues updates the mutable fields using the new values that were set on create except the ID field. +// Using this option is equivalent to using: +// +// client.Agent.Create(). +// OnConflict( +// sql.ResolveWithNewValues(), +// sql.ResolveWith(func(u *sql.UpdateSet) { +// u.SetIgnore(agent.FieldID) +// }), +// ). +// Exec(ctx) +func (u *AgentUpsertOne) UpdateNewValues() *AgentUpsertOne { + u.create.conflict = append(u.create.conflict, sql.ResolveWithNewValues()) + u.create.conflict = append(u.create.conflict, sql.ResolveWith(func(s *sql.UpdateSet) { + if _, exists := u.create.mutation.ID(); exists { + s.SetIgnore(agent.FieldID) + } + if _, exists := u.create.mutation.Created(); exists { + s.SetIgnore(agent.FieldCreated) + } + })) + return u +} + +// Ignore sets each column to itself in case of conflict. +// Using this option is equivalent to using: +// +// client.Agent.Create(). +// OnConflict(sql.ResolveWithIgnore()). +// Exec(ctx) +func (u *AgentUpsertOne) Ignore() *AgentUpsertOne { + u.create.conflict = append(u.create.conflict, sql.ResolveWithIgnore()) + return u +} + +// DoNothing configures the conflict_action to `DO NOTHING`. +// Supported only by SQLite and PostgreSQL. +func (u *AgentUpsertOne) DoNothing() *AgentUpsertOne { + u.create.conflict = append(u.create.conflict, sql.DoNothing()) + return u +} + +// Update allows overriding fields `UPDATE` values. See the AgentCreate.OnConflict +// documentation for more info. +func (u *AgentUpsertOne) Update(set func(*AgentUpsert)) *AgentUpsertOne { + u.create.conflict = append(u.create.conflict, sql.ResolveWith(func(update *sql.UpdateSet) { + set(&AgentUpsert{UpdateSet: update}) + })) + return u +} + +// SetSlug sets the "slug" field. +func (u *AgentUpsertOne) SetSlug(v string) *AgentUpsertOne { + return u.Update(func(s *AgentUpsert) { + s.SetSlug(v) + }) +} + +// UpdateSlug sets the "slug" field to the value that was provided on create. +func (u *AgentUpsertOne) UpdateSlug() *AgentUpsertOne { + return u.Update(func(s *AgentUpsert) { + s.UpdateSlug() + }) +} + +// SetName sets the "name" field. +func (u *AgentUpsertOne) SetName(v string) *AgentUpsertOne { + return u.Update(func(s *AgentUpsert) { + s.SetName(v) + }) +} + +// UpdateName sets the "name" field to the value that was provided on create. +func (u *AgentUpsertOne) UpdateName() *AgentUpsertOne { + return u.Update(func(s *AgentUpsert) { + s.UpdateName() + }) +} + +// SetTemplate sets the "template" field. +func (u *AgentUpsertOne) SetTemplate(v string) *AgentUpsertOne { + return u.Update(func(s *AgentUpsert) { + s.SetTemplate(v) + }) +} + +// UpdateTemplate sets the "template" field to the value that was provided on create. +func (u *AgentUpsertOne) UpdateTemplate() *AgentUpsertOne { + return u.Update(func(s *AgentUpsert) { + s.UpdateTemplate() + }) +} + +// ClearTemplate clears the value of the "template" field. +func (u *AgentUpsertOne) ClearTemplate() *AgentUpsertOne { + return u.Update(func(s *AgentUpsert) { + s.ClearTemplate() + }) +} + +// SetProjectID sets the "project_id" field. +func (u *AgentUpsertOne) SetProjectID(v uuid.UUID) *AgentUpsertOne { + return u.Update(func(s *AgentUpsert) { + s.SetProjectID(v) + }) +} + +// UpdateProjectID sets the "project_id" field to the value that was provided on create. +func (u *AgentUpsertOne) UpdateProjectID() *AgentUpsertOne { + return u.Update(func(s *AgentUpsert) { + s.UpdateProjectID() + }) +} + +// SetStatus sets the "status" field. +func (u *AgentUpsertOne) SetStatus(v agent.Status) *AgentUpsertOne { + return u.Update(func(s *AgentUpsert) { + s.SetStatus(v) + }) +} + +// UpdateStatus sets the "status" field to the value that was provided on create. +func (u *AgentUpsertOne) UpdateStatus() *AgentUpsertOne { + return u.Update(func(s *AgentUpsert) { + s.UpdateStatus() + }) +} + +// SetCreatedBy sets the "created_by" field. +func (u *AgentUpsertOne) SetCreatedBy(v uuid.UUID) *AgentUpsertOne { + return u.Update(func(s *AgentUpsert) { + s.SetCreatedBy(v) + }) +} + +// UpdateCreatedBy sets the "created_by" field to the value that was provided on create. +func (u *AgentUpsertOne) UpdateCreatedBy() *AgentUpsertOne { + return u.Update(func(s *AgentUpsert) { + s.UpdateCreatedBy() + }) +} + +// ClearCreatedBy clears the value of the "created_by" field. +func (u *AgentUpsertOne) ClearCreatedBy() *AgentUpsertOne { + return u.Update(func(s *AgentUpsert) { + s.ClearCreatedBy() + }) +} + +// SetOwnerID sets the "owner_id" field. +func (u *AgentUpsertOne) SetOwnerID(v uuid.UUID) *AgentUpsertOne { + return u.Update(func(s *AgentUpsert) { + s.SetOwnerID(v) + }) +} + +// UpdateOwnerID sets the "owner_id" field to the value that was provided on create. +func (u *AgentUpsertOne) UpdateOwnerID() *AgentUpsertOne { + return u.Update(func(s *AgentUpsert) { + s.UpdateOwnerID() + }) +} + +// ClearOwnerID clears the value of the "owner_id" field. +func (u *AgentUpsertOne) ClearOwnerID() *AgentUpsertOne { + return u.Update(func(s *AgentUpsert) { + s.ClearOwnerID() + }) +} + +// SetDelegationEnabled sets the "delegation_enabled" field. +func (u *AgentUpsertOne) SetDelegationEnabled(v bool) *AgentUpsertOne { + return u.Update(func(s *AgentUpsert) { + s.SetDelegationEnabled(v) + }) +} + +// UpdateDelegationEnabled sets the "delegation_enabled" field to the value that was provided on create. +func (u *AgentUpsertOne) UpdateDelegationEnabled() *AgentUpsertOne { + return u.Update(func(s *AgentUpsert) { + s.UpdateDelegationEnabled() + }) +} + +// SetVisibility sets the "visibility" field. +func (u *AgentUpsertOne) SetVisibility(v string) *AgentUpsertOne { + return u.Update(func(s *AgentUpsert) { + s.SetVisibility(v) + }) +} + +// UpdateVisibility sets the "visibility" field to the value that was provided on create. +func (u *AgentUpsertOne) UpdateVisibility() *AgentUpsertOne { + return u.Update(func(s *AgentUpsert) { + s.UpdateVisibility() + }) +} + +// SetLabels sets the "labels" field. +func (u *AgentUpsertOne) SetLabels(v map[string]string) *AgentUpsertOne { + return u.Update(func(s *AgentUpsert) { + s.SetLabels(v) + }) +} + +// UpdateLabels sets the "labels" field to the value that was provided on create. +func (u *AgentUpsertOne) UpdateLabels() *AgentUpsertOne { + return u.Update(func(s *AgentUpsert) { + s.UpdateLabels() + }) +} + +// ClearLabels clears the value of the "labels" field. +func (u *AgentUpsertOne) ClearLabels() *AgentUpsertOne { + return u.Update(func(s *AgentUpsert) { + s.ClearLabels() + }) +} + +// SetAnnotations sets the "annotations" field. +func (u *AgentUpsertOne) SetAnnotations(v map[string]string) *AgentUpsertOne { + return u.Update(func(s *AgentUpsert) { + s.SetAnnotations(v) + }) +} + +// UpdateAnnotations sets the "annotations" field to the value that was provided on create. +func (u *AgentUpsertOne) UpdateAnnotations() *AgentUpsertOne { + return u.Update(func(s *AgentUpsert) { + s.UpdateAnnotations() + }) +} + +// ClearAnnotations clears the value of the "annotations" field. +func (u *AgentUpsertOne) ClearAnnotations() *AgentUpsertOne { + return u.Update(func(s *AgentUpsert) { + s.ClearAnnotations() + }) +} + +// SetPhase sets the "phase" field. +func (u *AgentUpsertOne) SetPhase(v string) *AgentUpsertOne { + return u.Update(func(s *AgentUpsert) { + s.SetPhase(v) + }) +} + +// UpdatePhase sets the "phase" field to the value that was provided on create. +func (u *AgentUpsertOne) UpdatePhase() *AgentUpsertOne { + return u.Update(func(s *AgentUpsert) { + s.UpdatePhase() + }) +} + +// ClearPhase clears the value of the "phase" field. +func (u *AgentUpsertOne) ClearPhase() *AgentUpsertOne { + return u.Update(func(s *AgentUpsert) { + s.ClearPhase() + }) +} + +// SetActivity sets the "activity" field. +func (u *AgentUpsertOne) SetActivity(v string) *AgentUpsertOne { + return u.Update(func(s *AgentUpsert) { + s.SetActivity(v) + }) +} + +// UpdateActivity sets the "activity" field to the value that was provided on create. +func (u *AgentUpsertOne) UpdateActivity() *AgentUpsertOne { + return u.Update(func(s *AgentUpsert) { + s.UpdateActivity() + }) +} + +// ClearActivity clears the value of the "activity" field. +func (u *AgentUpsertOne) ClearActivity() *AgentUpsertOne { + return u.Update(func(s *AgentUpsert) { + s.ClearActivity() + }) +} + +// SetToolName sets the "tool_name" field. +func (u *AgentUpsertOne) SetToolName(v string) *AgentUpsertOne { + return u.Update(func(s *AgentUpsert) { + s.SetToolName(v) + }) +} + +// UpdateToolName sets the "tool_name" field to the value that was provided on create. +func (u *AgentUpsertOne) UpdateToolName() *AgentUpsertOne { + return u.Update(func(s *AgentUpsert) { + s.UpdateToolName() + }) +} + +// ClearToolName clears the value of the "tool_name" field. +func (u *AgentUpsertOne) ClearToolName() *AgentUpsertOne { + return u.Update(func(s *AgentUpsert) { + s.ClearToolName() + }) +} + +// SetConnectionState sets the "connection_state" field. +func (u *AgentUpsertOne) SetConnectionState(v string) *AgentUpsertOne { + return u.Update(func(s *AgentUpsert) { + s.SetConnectionState(v) + }) +} + +// UpdateConnectionState sets the "connection_state" field to the value that was provided on create. +func (u *AgentUpsertOne) UpdateConnectionState() *AgentUpsertOne { + return u.Update(func(s *AgentUpsert) { + s.UpdateConnectionState() + }) +} + +// ClearConnectionState clears the value of the "connection_state" field. +func (u *AgentUpsertOne) ClearConnectionState() *AgentUpsertOne { + return u.Update(func(s *AgentUpsert) { + s.ClearConnectionState() + }) +} + +// SetContainerStatus sets the "container_status" field. +func (u *AgentUpsertOne) SetContainerStatus(v string) *AgentUpsertOne { + return u.Update(func(s *AgentUpsert) { + s.SetContainerStatus(v) + }) +} + +// UpdateContainerStatus sets the "container_status" field to the value that was provided on create. +func (u *AgentUpsertOne) UpdateContainerStatus() *AgentUpsertOne { + return u.Update(func(s *AgentUpsert) { + s.UpdateContainerStatus() + }) +} + +// ClearContainerStatus clears the value of the "container_status" field. +func (u *AgentUpsertOne) ClearContainerStatus() *AgentUpsertOne { + return u.Update(func(s *AgentUpsert) { + s.ClearContainerStatus() + }) +} + +// SetRuntimeState sets the "runtime_state" field. +func (u *AgentUpsertOne) SetRuntimeState(v string) *AgentUpsertOne { + return u.Update(func(s *AgentUpsert) { + s.SetRuntimeState(v) + }) +} + +// UpdateRuntimeState sets the "runtime_state" field to the value that was provided on create. +func (u *AgentUpsertOne) UpdateRuntimeState() *AgentUpsertOne { + return u.Update(func(s *AgentUpsert) { + s.UpdateRuntimeState() + }) +} + +// ClearRuntimeState clears the value of the "runtime_state" field. +func (u *AgentUpsertOne) ClearRuntimeState() *AgentUpsertOne { + return u.Update(func(s *AgentUpsert) { + s.ClearRuntimeState() + }) +} + +// SetStalledFromActivity sets the "stalled_from_activity" field. +func (u *AgentUpsertOne) SetStalledFromActivity(v string) *AgentUpsertOne { + return u.Update(func(s *AgentUpsert) { + s.SetStalledFromActivity(v) + }) +} + +// UpdateStalledFromActivity sets the "stalled_from_activity" field to the value that was provided on create. +func (u *AgentUpsertOne) UpdateStalledFromActivity() *AgentUpsertOne { + return u.Update(func(s *AgentUpsert) { + s.UpdateStalledFromActivity() + }) +} + +// ClearStalledFromActivity clears the value of the "stalled_from_activity" field. +func (u *AgentUpsertOne) ClearStalledFromActivity() *AgentUpsertOne { + return u.Update(func(s *AgentUpsert) { + s.ClearStalledFromActivity() + }) +} + +// SetCurrentTurns sets the "current_turns" field. +func (u *AgentUpsertOne) SetCurrentTurns(v int) *AgentUpsertOne { + return u.Update(func(s *AgentUpsert) { + s.SetCurrentTurns(v) + }) +} + +// AddCurrentTurns adds v to the "current_turns" field. +func (u *AgentUpsertOne) AddCurrentTurns(v int) *AgentUpsertOne { + return u.Update(func(s *AgentUpsert) { + s.AddCurrentTurns(v) + }) +} + +// UpdateCurrentTurns sets the "current_turns" field to the value that was provided on create. +func (u *AgentUpsertOne) UpdateCurrentTurns() *AgentUpsertOne { + return u.Update(func(s *AgentUpsert) { + s.UpdateCurrentTurns() + }) +} + +// SetCurrentModelCalls sets the "current_model_calls" field. +func (u *AgentUpsertOne) SetCurrentModelCalls(v int) *AgentUpsertOne { + return u.Update(func(s *AgentUpsert) { + s.SetCurrentModelCalls(v) + }) +} + +// AddCurrentModelCalls adds v to the "current_model_calls" field. +func (u *AgentUpsertOne) AddCurrentModelCalls(v int) *AgentUpsertOne { + return u.Update(func(s *AgentUpsert) { + s.AddCurrentModelCalls(v) + }) +} + +// UpdateCurrentModelCalls sets the "current_model_calls" field to the value that was provided on create. +func (u *AgentUpsertOne) UpdateCurrentModelCalls() *AgentUpsertOne { + return u.Update(func(s *AgentUpsert) { + s.UpdateCurrentModelCalls() + }) +} + +// SetImage sets the "image" field. +func (u *AgentUpsertOne) SetImage(v string) *AgentUpsertOne { + return u.Update(func(s *AgentUpsert) { + s.SetImage(v) + }) +} + +// UpdateImage sets the "image" field to the value that was provided on create. +func (u *AgentUpsertOne) UpdateImage() *AgentUpsertOne { + return u.Update(func(s *AgentUpsert) { + s.UpdateImage() + }) +} + +// ClearImage clears the value of the "image" field. +func (u *AgentUpsertOne) ClearImage() *AgentUpsertOne { + return u.Update(func(s *AgentUpsert) { + s.ClearImage() + }) +} + +// SetDetached sets the "detached" field. +func (u *AgentUpsertOne) SetDetached(v bool) *AgentUpsertOne { + return u.Update(func(s *AgentUpsert) { + s.SetDetached(v) + }) +} + +// UpdateDetached sets the "detached" field to the value that was provided on create. +func (u *AgentUpsertOne) UpdateDetached() *AgentUpsertOne { + return u.Update(func(s *AgentUpsert) { + s.UpdateDetached() + }) +} + +// SetRuntime sets the "runtime" field. +func (u *AgentUpsertOne) SetRuntime(v string) *AgentUpsertOne { + return u.Update(func(s *AgentUpsert) { + s.SetRuntime(v) + }) +} + +// UpdateRuntime sets the "runtime" field to the value that was provided on create. +func (u *AgentUpsertOne) UpdateRuntime() *AgentUpsertOne { + return u.Update(func(s *AgentUpsert) { + s.UpdateRuntime() + }) +} + +// ClearRuntime clears the value of the "runtime" field. +func (u *AgentUpsertOne) ClearRuntime() *AgentUpsertOne { + return u.Update(func(s *AgentUpsert) { + s.ClearRuntime() + }) +} + +// SetRuntimeBrokerID sets the "runtime_broker_id" field. +func (u *AgentUpsertOne) SetRuntimeBrokerID(v string) *AgentUpsertOne { + return u.Update(func(s *AgentUpsert) { + s.SetRuntimeBrokerID(v) + }) +} + +// UpdateRuntimeBrokerID sets the "runtime_broker_id" field to the value that was provided on create. +func (u *AgentUpsertOne) UpdateRuntimeBrokerID() *AgentUpsertOne { + return u.Update(func(s *AgentUpsert) { + s.UpdateRuntimeBrokerID() + }) +} + +// ClearRuntimeBrokerID clears the value of the "runtime_broker_id" field. +func (u *AgentUpsertOne) ClearRuntimeBrokerID() *AgentUpsertOne { + return u.Update(func(s *AgentUpsert) { + s.ClearRuntimeBrokerID() + }) +} + +// SetWebPtyEnabled sets the "web_pty_enabled" field. +func (u *AgentUpsertOne) SetWebPtyEnabled(v bool) *AgentUpsertOne { + return u.Update(func(s *AgentUpsert) { + s.SetWebPtyEnabled(v) + }) +} + +// UpdateWebPtyEnabled sets the "web_pty_enabled" field to the value that was provided on create. +func (u *AgentUpsertOne) UpdateWebPtyEnabled() *AgentUpsertOne { + return u.Update(func(s *AgentUpsert) { + s.UpdateWebPtyEnabled() + }) +} + +// SetTaskSummary sets the "task_summary" field. +func (u *AgentUpsertOne) SetTaskSummary(v string) *AgentUpsertOne { + return u.Update(func(s *AgentUpsert) { + s.SetTaskSummary(v) + }) +} + +// UpdateTaskSummary sets the "task_summary" field to the value that was provided on create. +func (u *AgentUpsertOne) UpdateTaskSummary() *AgentUpsertOne { + return u.Update(func(s *AgentUpsert) { + s.UpdateTaskSummary() + }) +} + +// ClearTaskSummary clears the value of the "task_summary" field. +func (u *AgentUpsertOne) ClearTaskSummary() *AgentUpsertOne { + return u.Update(func(s *AgentUpsert) { + s.ClearTaskSummary() + }) +} + +// SetMessage sets the "message" field. +func (u *AgentUpsertOne) SetMessage(v string) *AgentUpsertOne { + return u.Update(func(s *AgentUpsert) { + s.SetMessage(v) + }) +} + +// UpdateMessage sets the "message" field to the value that was provided on create. +func (u *AgentUpsertOne) UpdateMessage() *AgentUpsertOne { + return u.Update(func(s *AgentUpsert) { + s.UpdateMessage() + }) +} + +// ClearMessage clears the value of the "message" field. +func (u *AgentUpsertOne) ClearMessage() *AgentUpsertOne { + return u.Update(func(s *AgentUpsert) { + s.ClearMessage() + }) +} + +// SetAppliedConfig sets the "applied_config" field. +func (u *AgentUpsertOne) SetAppliedConfig(v string) *AgentUpsertOne { + return u.Update(func(s *AgentUpsert) { + s.SetAppliedConfig(v) + }) +} + +// UpdateAppliedConfig sets the "applied_config" field to the value that was provided on create. +func (u *AgentUpsertOne) UpdateAppliedConfig() *AgentUpsertOne { + return u.Update(func(s *AgentUpsert) { + s.UpdateAppliedConfig() + }) +} + +// ClearAppliedConfig clears the value of the "applied_config" field. +func (u *AgentUpsertOne) ClearAppliedConfig() *AgentUpsertOne { + return u.Update(func(s *AgentUpsert) { + s.ClearAppliedConfig() + }) +} + +// SetAncestry sets the "ancestry" field. +func (u *AgentUpsertOne) SetAncestry(v []string) *AgentUpsertOne { + return u.Update(func(s *AgentUpsert) { + s.SetAncestry(v) + }) +} + +// UpdateAncestry sets the "ancestry" field to the value that was provided on create. +func (u *AgentUpsertOne) UpdateAncestry() *AgentUpsertOne { + return u.Update(func(s *AgentUpsert) { + s.UpdateAncestry() + }) +} + +// ClearAncestry clears the value of the "ancestry" field. +func (u *AgentUpsertOne) ClearAncestry() *AgentUpsertOne { + return u.Update(func(s *AgentUpsert) { + s.ClearAncestry() + }) +} + +// SetUpdated sets the "updated" field. +func (u *AgentUpsertOne) SetUpdated(v time.Time) *AgentUpsertOne { + return u.Update(func(s *AgentUpsert) { + s.SetUpdated(v) + }) +} + +// UpdateUpdated sets the "updated" field to the value that was provided on create. +func (u *AgentUpsertOne) UpdateUpdated() *AgentUpsertOne { + return u.Update(func(s *AgentUpsert) { + s.UpdateUpdated() + }) +} + +// SetLastSeen sets the "last_seen" field. +func (u *AgentUpsertOne) SetLastSeen(v time.Time) *AgentUpsertOne { + return u.Update(func(s *AgentUpsert) { + s.SetLastSeen(v) + }) +} + +// UpdateLastSeen sets the "last_seen" field to the value that was provided on create. +func (u *AgentUpsertOne) UpdateLastSeen() *AgentUpsertOne { + return u.Update(func(s *AgentUpsert) { + s.UpdateLastSeen() + }) +} + +// ClearLastSeen clears the value of the "last_seen" field. +func (u *AgentUpsertOne) ClearLastSeen() *AgentUpsertOne { + return u.Update(func(s *AgentUpsert) { + s.ClearLastSeen() + }) +} + +// SetLastActivityEvent sets the "last_activity_event" field. +func (u *AgentUpsertOne) SetLastActivityEvent(v time.Time) *AgentUpsertOne { + return u.Update(func(s *AgentUpsert) { + s.SetLastActivityEvent(v) + }) +} + +// UpdateLastActivityEvent sets the "last_activity_event" field to the value that was provided on create. +func (u *AgentUpsertOne) UpdateLastActivityEvent() *AgentUpsertOne { + return u.Update(func(s *AgentUpsert) { + s.UpdateLastActivityEvent() + }) +} + +// ClearLastActivityEvent clears the value of the "last_activity_event" field. +func (u *AgentUpsertOne) ClearLastActivityEvent() *AgentUpsertOne { + return u.Update(func(s *AgentUpsert) { + s.ClearLastActivityEvent() + }) +} + +// SetStartedAt sets the "started_at" field. +func (u *AgentUpsertOne) SetStartedAt(v time.Time) *AgentUpsertOne { + return u.Update(func(s *AgentUpsert) { + s.SetStartedAt(v) + }) +} + +// UpdateStartedAt sets the "started_at" field to the value that was provided on create. +func (u *AgentUpsertOne) UpdateStartedAt() *AgentUpsertOne { + return u.Update(func(s *AgentUpsert) { + s.UpdateStartedAt() + }) +} + +// ClearStartedAt clears the value of the "started_at" field. +func (u *AgentUpsertOne) ClearStartedAt() *AgentUpsertOne { + return u.Update(func(s *AgentUpsert) { + s.ClearStartedAt() + }) +} + +// SetDeletedAt sets the "deleted_at" field. +func (u *AgentUpsertOne) SetDeletedAt(v time.Time) *AgentUpsertOne { + return u.Update(func(s *AgentUpsert) { + s.SetDeletedAt(v) + }) +} + +// UpdateDeletedAt sets the "deleted_at" field to the value that was provided on create. +func (u *AgentUpsertOne) UpdateDeletedAt() *AgentUpsertOne { + return u.Update(func(s *AgentUpsert) { + s.UpdateDeletedAt() + }) +} + +// ClearDeletedAt clears the value of the "deleted_at" field. +func (u *AgentUpsertOne) ClearDeletedAt() *AgentUpsertOne { + return u.Update(func(s *AgentUpsert) { + s.ClearDeletedAt() + }) +} + +// SetStateVersion sets the "state_version" field. +func (u *AgentUpsertOne) SetStateVersion(v int64) *AgentUpsertOne { + return u.Update(func(s *AgentUpsert) { + s.SetStateVersion(v) + }) +} + +// AddStateVersion adds v to the "state_version" field. +func (u *AgentUpsertOne) AddStateVersion(v int64) *AgentUpsertOne { + return u.Update(func(s *AgentUpsert) { + s.AddStateVersion(v) + }) +} + +// UpdateStateVersion sets the "state_version" field to the value that was provided on create. +func (u *AgentUpsertOne) UpdateStateVersion() *AgentUpsertOne { + return u.Update(func(s *AgentUpsert) { + s.UpdateStateVersion() + }) +} + +// Exec executes the query. +func (u *AgentUpsertOne) Exec(ctx context.Context) error { + if len(u.create.conflict) == 0 { + return errors.New("ent: missing options for AgentCreate.OnConflict") + } + return u.create.Exec(ctx) +} + +// ExecX is like Exec, but panics if an error occurs. +func (u *AgentUpsertOne) ExecX(ctx context.Context) { + if err := u.create.Exec(ctx); err != nil { + panic(err) + } +} + +// Exec executes the UPSERT query and returns the inserted/updated ID. +func (u *AgentUpsertOne) ID(ctx context.Context) (id uuid.UUID, err error) { + if u.create.driver.Dialect() == dialect.MySQL { + // In case of "ON CONFLICT", there is no way to get back non-numeric ID + // fields from the database since MySQL does not support the RETURNING clause. + return id, errors.New("ent: AgentUpsertOne.ID is not supported by MySQL driver. Use AgentUpsertOne.Exec instead") + } + node, err := u.create.Save(ctx) + if err != nil { + return id, err + } + return node.ID, nil +} + +// IDX is like ID, but panics if an error occurs. +func (u *AgentUpsertOne) IDX(ctx context.Context) uuid.UUID { + id, err := u.ID(ctx) + if err != nil { + panic(err) + } + return id +} + +// AgentCreateBulk is the builder for creating many Agent entities in bulk. +type AgentCreateBulk struct { + config err error builders []*AgentCreate + conflict []sql.ConflictOption } // Save creates the Agent entities in the database. @@ -520,6 +2321,7 @@ func (_c *AgentCreateBulk) Save(ctx context.Context) ([]*Agent, error) { _, err = mutators[i+1].Mutate(root, _c.builders[i+1].mutation) } else { spec := &sqlgraph.BatchCreateSpec{Nodes: specs} + spec.OnConflict = _c.conflict // Invoke the actual operation on the latest mutation in the chain. if err = sqlgraph.BatchCreate(ctx, _c.driver, spec); err != nil { if sqlgraph.IsConstraintError(err) { @@ -569,3 +2371,792 @@ func (_c *AgentCreateBulk) ExecX(ctx context.Context) { panic(err) } } + +// OnConflict allows configuring the `ON CONFLICT` / `ON DUPLICATE KEY` clause +// of the `INSERT` statement. For example: +// +// client.Agent.CreateBulk(builders...). +// OnConflict( +// // Update the row with the new values +// // the was proposed for insertion. +// sql.ResolveWithNewValues(), +// ). +// // Override some of the fields with custom +// // update values. +// Update(func(u *ent.AgentUpsert) { +// SetSlug(v+v). +// }). +// Exec(ctx) +func (_c *AgentCreateBulk) OnConflict(opts ...sql.ConflictOption) *AgentUpsertBulk { + _c.conflict = opts + return &AgentUpsertBulk{ + create: _c, + } +} + +// OnConflictColumns calls `OnConflict` and configures the columns +// as conflict target. Using this option is equivalent to using: +// +// client.Agent.Create(). +// OnConflict(sql.ConflictColumns(columns...)). +// Exec(ctx) +func (_c *AgentCreateBulk) OnConflictColumns(columns ...string) *AgentUpsertBulk { + _c.conflict = append(_c.conflict, sql.ConflictColumns(columns...)) + return &AgentUpsertBulk{ + create: _c, + } +} + +// AgentUpsertBulk is the builder for "upsert"-ing +// a bulk of Agent nodes. +type AgentUpsertBulk struct { + create *AgentCreateBulk +} + +// UpdateNewValues updates the mutable fields using the new values that +// were set on create. Using this option is equivalent to using: +// +// client.Agent.Create(). +// OnConflict( +// sql.ResolveWithNewValues(), +// sql.ResolveWith(func(u *sql.UpdateSet) { +// u.SetIgnore(agent.FieldID) +// }), +// ). +// Exec(ctx) +func (u *AgentUpsertBulk) UpdateNewValues() *AgentUpsertBulk { + u.create.conflict = append(u.create.conflict, sql.ResolveWithNewValues()) + u.create.conflict = append(u.create.conflict, sql.ResolveWith(func(s *sql.UpdateSet) { + for _, b := range u.create.builders { + if _, exists := b.mutation.ID(); exists { + s.SetIgnore(agent.FieldID) + } + if _, exists := b.mutation.Created(); exists { + s.SetIgnore(agent.FieldCreated) + } + } + })) + return u +} + +// Ignore sets each column to itself in case of conflict. +// Using this option is equivalent to using: +// +// client.Agent.Create(). +// OnConflict(sql.ResolveWithIgnore()). +// Exec(ctx) +func (u *AgentUpsertBulk) Ignore() *AgentUpsertBulk { + u.create.conflict = append(u.create.conflict, sql.ResolveWithIgnore()) + return u +} + +// DoNothing configures the conflict_action to `DO NOTHING`. +// Supported only by SQLite and PostgreSQL. +func (u *AgentUpsertBulk) DoNothing() *AgentUpsertBulk { + u.create.conflict = append(u.create.conflict, sql.DoNothing()) + return u +} + +// Update allows overriding fields `UPDATE` values. See the AgentCreateBulk.OnConflict +// documentation for more info. +func (u *AgentUpsertBulk) Update(set func(*AgentUpsert)) *AgentUpsertBulk { + u.create.conflict = append(u.create.conflict, sql.ResolveWith(func(update *sql.UpdateSet) { + set(&AgentUpsert{UpdateSet: update}) + })) + return u +} + +// SetSlug sets the "slug" field. +func (u *AgentUpsertBulk) SetSlug(v string) *AgentUpsertBulk { + return u.Update(func(s *AgentUpsert) { + s.SetSlug(v) + }) +} + +// UpdateSlug sets the "slug" field to the value that was provided on create. +func (u *AgentUpsertBulk) UpdateSlug() *AgentUpsertBulk { + return u.Update(func(s *AgentUpsert) { + s.UpdateSlug() + }) +} + +// SetName sets the "name" field. +func (u *AgentUpsertBulk) SetName(v string) *AgentUpsertBulk { + return u.Update(func(s *AgentUpsert) { + s.SetName(v) + }) +} + +// UpdateName sets the "name" field to the value that was provided on create. +func (u *AgentUpsertBulk) UpdateName() *AgentUpsertBulk { + return u.Update(func(s *AgentUpsert) { + s.UpdateName() + }) +} + +// SetTemplate sets the "template" field. +func (u *AgentUpsertBulk) SetTemplate(v string) *AgentUpsertBulk { + return u.Update(func(s *AgentUpsert) { + s.SetTemplate(v) + }) +} + +// UpdateTemplate sets the "template" field to the value that was provided on create. +func (u *AgentUpsertBulk) UpdateTemplate() *AgentUpsertBulk { + return u.Update(func(s *AgentUpsert) { + s.UpdateTemplate() + }) +} + +// ClearTemplate clears the value of the "template" field. +func (u *AgentUpsertBulk) ClearTemplate() *AgentUpsertBulk { + return u.Update(func(s *AgentUpsert) { + s.ClearTemplate() + }) +} + +// SetProjectID sets the "project_id" field. +func (u *AgentUpsertBulk) SetProjectID(v uuid.UUID) *AgentUpsertBulk { + return u.Update(func(s *AgentUpsert) { + s.SetProjectID(v) + }) +} + +// UpdateProjectID sets the "project_id" field to the value that was provided on create. +func (u *AgentUpsertBulk) UpdateProjectID() *AgentUpsertBulk { + return u.Update(func(s *AgentUpsert) { + s.UpdateProjectID() + }) +} + +// SetStatus sets the "status" field. +func (u *AgentUpsertBulk) SetStatus(v agent.Status) *AgentUpsertBulk { + return u.Update(func(s *AgentUpsert) { + s.SetStatus(v) + }) +} + +// UpdateStatus sets the "status" field to the value that was provided on create. +func (u *AgentUpsertBulk) UpdateStatus() *AgentUpsertBulk { + return u.Update(func(s *AgentUpsert) { + s.UpdateStatus() + }) +} + +// SetCreatedBy sets the "created_by" field. +func (u *AgentUpsertBulk) SetCreatedBy(v uuid.UUID) *AgentUpsertBulk { + return u.Update(func(s *AgentUpsert) { + s.SetCreatedBy(v) + }) +} + +// UpdateCreatedBy sets the "created_by" field to the value that was provided on create. +func (u *AgentUpsertBulk) UpdateCreatedBy() *AgentUpsertBulk { + return u.Update(func(s *AgentUpsert) { + s.UpdateCreatedBy() + }) +} + +// ClearCreatedBy clears the value of the "created_by" field. +func (u *AgentUpsertBulk) ClearCreatedBy() *AgentUpsertBulk { + return u.Update(func(s *AgentUpsert) { + s.ClearCreatedBy() + }) +} + +// SetOwnerID sets the "owner_id" field. +func (u *AgentUpsertBulk) SetOwnerID(v uuid.UUID) *AgentUpsertBulk { + return u.Update(func(s *AgentUpsert) { + s.SetOwnerID(v) + }) +} + +// UpdateOwnerID sets the "owner_id" field to the value that was provided on create. +func (u *AgentUpsertBulk) UpdateOwnerID() *AgentUpsertBulk { + return u.Update(func(s *AgentUpsert) { + s.UpdateOwnerID() + }) +} + +// ClearOwnerID clears the value of the "owner_id" field. +func (u *AgentUpsertBulk) ClearOwnerID() *AgentUpsertBulk { + return u.Update(func(s *AgentUpsert) { + s.ClearOwnerID() + }) +} + +// SetDelegationEnabled sets the "delegation_enabled" field. +func (u *AgentUpsertBulk) SetDelegationEnabled(v bool) *AgentUpsertBulk { + return u.Update(func(s *AgentUpsert) { + s.SetDelegationEnabled(v) + }) +} + +// UpdateDelegationEnabled sets the "delegation_enabled" field to the value that was provided on create. +func (u *AgentUpsertBulk) UpdateDelegationEnabled() *AgentUpsertBulk { + return u.Update(func(s *AgentUpsert) { + s.UpdateDelegationEnabled() + }) +} + +// SetVisibility sets the "visibility" field. +func (u *AgentUpsertBulk) SetVisibility(v string) *AgentUpsertBulk { + return u.Update(func(s *AgentUpsert) { + s.SetVisibility(v) + }) +} + +// UpdateVisibility sets the "visibility" field to the value that was provided on create. +func (u *AgentUpsertBulk) UpdateVisibility() *AgentUpsertBulk { + return u.Update(func(s *AgentUpsert) { + s.UpdateVisibility() + }) +} + +// SetLabels sets the "labels" field. +func (u *AgentUpsertBulk) SetLabels(v map[string]string) *AgentUpsertBulk { + return u.Update(func(s *AgentUpsert) { + s.SetLabels(v) + }) +} + +// UpdateLabels sets the "labels" field to the value that was provided on create. +func (u *AgentUpsertBulk) UpdateLabels() *AgentUpsertBulk { + return u.Update(func(s *AgentUpsert) { + s.UpdateLabels() + }) +} + +// ClearLabels clears the value of the "labels" field. +func (u *AgentUpsertBulk) ClearLabels() *AgentUpsertBulk { + return u.Update(func(s *AgentUpsert) { + s.ClearLabels() + }) +} + +// SetAnnotations sets the "annotations" field. +func (u *AgentUpsertBulk) SetAnnotations(v map[string]string) *AgentUpsertBulk { + return u.Update(func(s *AgentUpsert) { + s.SetAnnotations(v) + }) +} + +// UpdateAnnotations sets the "annotations" field to the value that was provided on create. +func (u *AgentUpsertBulk) UpdateAnnotations() *AgentUpsertBulk { + return u.Update(func(s *AgentUpsert) { + s.UpdateAnnotations() + }) +} + +// ClearAnnotations clears the value of the "annotations" field. +func (u *AgentUpsertBulk) ClearAnnotations() *AgentUpsertBulk { + return u.Update(func(s *AgentUpsert) { + s.ClearAnnotations() + }) +} + +// SetPhase sets the "phase" field. +func (u *AgentUpsertBulk) SetPhase(v string) *AgentUpsertBulk { + return u.Update(func(s *AgentUpsert) { + s.SetPhase(v) + }) +} + +// UpdatePhase sets the "phase" field to the value that was provided on create. +func (u *AgentUpsertBulk) UpdatePhase() *AgentUpsertBulk { + return u.Update(func(s *AgentUpsert) { + s.UpdatePhase() + }) +} + +// ClearPhase clears the value of the "phase" field. +func (u *AgentUpsertBulk) ClearPhase() *AgentUpsertBulk { + return u.Update(func(s *AgentUpsert) { + s.ClearPhase() + }) +} + +// SetActivity sets the "activity" field. +func (u *AgentUpsertBulk) SetActivity(v string) *AgentUpsertBulk { + return u.Update(func(s *AgentUpsert) { + s.SetActivity(v) + }) +} + +// UpdateActivity sets the "activity" field to the value that was provided on create. +func (u *AgentUpsertBulk) UpdateActivity() *AgentUpsertBulk { + return u.Update(func(s *AgentUpsert) { + s.UpdateActivity() + }) +} + +// ClearActivity clears the value of the "activity" field. +func (u *AgentUpsertBulk) ClearActivity() *AgentUpsertBulk { + return u.Update(func(s *AgentUpsert) { + s.ClearActivity() + }) +} + +// SetToolName sets the "tool_name" field. +func (u *AgentUpsertBulk) SetToolName(v string) *AgentUpsertBulk { + return u.Update(func(s *AgentUpsert) { + s.SetToolName(v) + }) +} + +// UpdateToolName sets the "tool_name" field to the value that was provided on create. +func (u *AgentUpsertBulk) UpdateToolName() *AgentUpsertBulk { + return u.Update(func(s *AgentUpsert) { + s.UpdateToolName() + }) +} + +// ClearToolName clears the value of the "tool_name" field. +func (u *AgentUpsertBulk) ClearToolName() *AgentUpsertBulk { + return u.Update(func(s *AgentUpsert) { + s.ClearToolName() + }) +} + +// SetConnectionState sets the "connection_state" field. +func (u *AgentUpsertBulk) SetConnectionState(v string) *AgentUpsertBulk { + return u.Update(func(s *AgentUpsert) { + s.SetConnectionState(v) + }) +} + +// UpdateConnectionState sets the "connection_state" field to the value that was provided on create. +func (u *AgentUpsertBulk) UpdateConnectionState() *AgentUpsertBulk { + return u.Update(func(s *AgentUpsert) { + s.UpdateConnectionState() + }) +} + +// ClearConnectionState clears the value of the "connection_state" field. +func (u *AgentUpsertBulk) ClearConnectionState() *AgentUpsertBulk { + return u.Update(func(s *AgentUpsert) { + s.ClearConnectionState() + }) +} + +// SetContainerStatus sets the "container_status" field. +func (u *AgentUpsertBulk) SetContainerStatus(v string) *AgentUpsertBulk { + return u.Update(func(s *AgentUpsert) { + s.SetContainerStatus(v) + }) +} + +// UpdateContainerStatus sets the "container_status" field to the value that was provided on create. +func (u *AgentUpsertBulk) UpdateContainerStatus() *AgentUpsertBulk { + return u.Update(func(s *AgentUpsert) { + s.UpdateContainerStatus() + }) +} + +// ClearContainerStatus clears the value of the "container_status" field. +func (u *AgentUpsertBulk) ClearContainerStatus() *AgentUpsertBulk { + return u.Update(func(s *AgentUpsert) { + s.ClearContainerStatus() + }) +} + +// SetRuntimeState sets the "runtime_state" field. +func (u *AgentUpsertBulk) SetRuntimeState(v string) *AgentUpsertBulk { + return u.Update(func(s *AgentUpsert) { + s.SetRuntimeState(v) + }) +} + +// UpdateRuntimeState sets the "runtime_state" field to the value that was provided on create. +func (u *AgentUpsertBulk) UpdateRuntimeState() *AgentUpsertBulk { + return u.Update(func(s *AgentUpsert) { + s.UpdateRuntimeState() + }) +} + +// ClearRuntimeState clears the value of the "runtime_state" field. +func (u *AgentUpsertBulk) ClearRuntimeState() *AgentUpsertBulk { + return u.Update(func(s *AgentUpsert) { + s.ClearRuntimeState() + }) +} + +// SetStalledFromActivity sets the "stalled_from_activity" field. +func (u *AgentUpsertBulk) SetStalledFromActivity(v string) *AgentUpsertBulk { + return u.Update(func(s *AgentUpsert) { + s.SetStalledFromActivity(v) + }) +} + +// UpdateStalledFromActivity sets the "stalled_from_activity" field to the value that was provided on create. +func (u *AgentUpsertBulk) UpdateStalledFromActivity() *AgentUpsertBulk { + return u.Update(func(s *AgentUpsert) { + s.UpdateStalledFromActivity() + }) +} + +// ClearStalledFromActivity clears the value of the "stalled_from_activity" field. +func (u *AgentUpsertBulk) ClearStalledFromActivity() *AgentUpsertBulk { + return u.Update(func(s *AgentUpsert) { + s.ClearStalledFromActivity() + }) +} + +// SetCurrentTurns sets the "current_turns" field. +func (u *AgentUpsertBulk) SetCurrentTurns(v int) *AgentUpsertBulk { + return u.Update(func(s *AgentUpsert) { + s.SetCurrentTurns(v) + }) +} + +// AddCurrentTurns adds v to the "current_turns" field. +func (u *AgentUpsertBulk) AddCurrentTurns(v int) *AgentUpsertBulk { + return u.Update(func(s *AgentUpsert) { + s.AddCurrentTurns(v) + }) +} + +// UpdateCurrentTurns sets the "current_turns" field to the value that was provided on create. +func (u *AgentUpsertBulk) UpdateCurrentTurns() *AgentUpsertBulk { + return u.Update(func(s *AgentUpsert) { + s.UpdateCurrentTurns() + }) +} + +// SetCurrentModelCalls sets the "current_model_calls" field. +func (u *AgentUpsertBulk) SetCurrentModelCalls(v int) *AgentUpsertBulk { + return u.Update(func(s *AgentUpsert) { + s.SetCurrentModelCalls(v) + }) +} + +// AddCurrentModelCalls adds v to the "current_model_calls" field. +func (u *AgentUpsertBulk) AddCurrentModelCalls(v int) *AgentUpsertBulk { + return u.Update(func(s *AgentUpsert) { + s.AddCurrentModelCalls(v) + }) +} + +// UpdateCurrentModelCalls sets the "current_model_calls" field to the value that was provided on create. +func (u *AgentUpsertBulk) UpdateCurrentModelCalls() *AgentUpsertBulk { + return u.Update(func(s *AgentUpsert) { + s.UpdateCurrentModelCalls() + }) +} + +// SetImage sets the "image" field. +func (u *AgentUpsertBulk) SetImage(v string) *AgentUpsertBulk { + return u.Update(func(s *AgentUpsert) { + s.SetImage(v) + }) +} + +// UpdateImage sets the "image" field to the value that was provided on create. +func (u *AgentUpsertBulk) UpdateImage() *AgentUpsertBulk { + return u.Update(func(s *AgentUpsert) { + s.UpdateImage() + }) +} + +// ClearImage clears the value of the "image" field. +func (u *AgentUpsertBulk) ClearImage() *AgentUpsertBulk { + return u.Update(func(s *AgentUpsert) { + s.ClearImage() + }) +} + +// SetDetached sets the "detached" field. +func (u *AgentUpsertBulk) SetDetached(v bool) *AgentUpsertBulk { + return u.Update(func(s *AgentUpsert) { + s.SetDetached(v) + }) +} + +// UpdateDetached sets the "detached" field to the value that was provided on create. +func (u *AgentUpsertBulk) UpdateDetached() *AgentUpsertBulk { + return u.Update(func(s *AgentUpsert) { + s.UpdateDetached() + }) +} + +// SetRuntime sets the "runtime" field. +func (u *AgentUpsertBulk) SetRuntime(v string) *AgentUpsertBulk { + return u.Update(func(s *AgentUpsert) { + s.SetRuntime(v) + }) +} + +// UpdateRuntime sets the "runtime" field to the value that was provided on create. +func (u *AgentUpsertBulk) UpdateRuntime() *AgentUpsertBulk { + return u.Update(func(s *AgentUpsert) { + s.UpdateRuntime() + }) +} + +// ClearRuntime clears the value of the "runtime" field. +func (u *AgentUpsertBulk) ClearRuntime() *AgentUpsertBulk { + return u.Update(func(s *AgentUpsert) { + s.ClearRuntime() + }) +} + +// SetRuntimeBrokerID sets the "runtime_broker_id" field. +func (u *AgentUpsertBulk) SetRuntimeBrokerID(v string) *AgentUpsertBulk { + return u.Update(func(s *AgentUpsert) { + s.SetRuntimeBrokerID(v) + }) +} + +// UpdateRuntimeBrokerID sets the "runtime_broker_id" field to the value that was provided on create. +func (u *AgentUpsertBulk) UpdateRuntimeBrokerID() *AgentUpsertBulk { + return u.Update(func(s *AgentUpsert) { + s.UpdateRuntimeBrokerID() + }) +} + +// ClearRuntimeBrokerID clears the value of the "runtime_broker_id" field. +func (u *AgentUpsertBulk) ClearRuntimeBrokerID() *AgentUpsertBulk { + return u.Update(func(s *AgentUpsert) { + s.ClearRuntimeBrokerID() + }) +} + +// SetWebPtyEnabled sets the "web_pty_enabled" field. +func (u *AgentUpsertBulk) SetWebPtyEnabled(v bool) *AgentUpsertBulk { + return u.Update(func(s *AgentUpsert) { + s.SetWebPtyEnabled(v) + }) +} + +// UpdateWebPtyEnabled sets the "web_pty_enabled" field to the value that was provided on create. +func (u *AgentUpsertBulk) UpdateWebPtyEnabled() *AgentUpsertBulk { + return u.Update(func(s *AgentUpsert) { + s.UpdateWebPtyEnabled() + }) +} + +// SetTaskSummary sets the "task_summary" field. +func (u *AgentUpsertBulk) SetTaskSummary(v string) *AgentUpsertBulk { + return u.Update(func(s *AgentUpsert) { + s.SetTaskSummary(v) + }) +} + +// UpdateTaskSummary sets the "task_summary" field to the value that was provided on create. +func (u *AgentUpsertBulk) UpdateTaskSummary() *AgentUpsertBulk { + return u.Update(func(s *AgentUpsert) { + s.UpdateTaskSummary() + }) +} + +// ClearTaskSummary clears the value of the "task_summary" field. +func (u *AgentUpsertBulk) ClearTaskSummary() *AgentUpsertBulk { + return u.Update(func(s *AgentUpsert) { + s.ClearTaskSummary() + }) +} + +// SetMessage sets the "message" field. +func (u *AgentUpsertBulk) SetMessage(v string) *AgentUpsertBulk { + return u.Update(func(s *AgentUpsert) { + s.SetMessage(v) + }) +} + +// UpdateMessage sets the "message" field to the value that was provided on create. +func (u *AgentUpsertBulk) UpdateMessage() *AgentUpsertBulk { + return u.Update(func(s *AgentUpsert) { + s.UpdateMessage() + }) +} + +// ClearMessage clears the value of the "message" field. +func (u *AgentUpsertBulk) ClearMessage() *AgentUpsertBulk { + return u.Update(func(s *AgentUpsert) { + s.ClearMessage() + }) +} + +// SetAppliedConfig sets the "applied_config" field. +func (u *AgentUpsertBulk) SetAppliedConfig(v string) *AgentUpsertBulk { + return u.Update(func(s *AgentUpsert) { + s.SetAppliedConfig(v) + }) +} + +// UpdateAppliedConfig sets the "applied_config" field to the value that was provided on create. +func (u *AgentUpsertBulk) UpdateAppliedConfig() *AgentUpsertBulk { + return u.Update(func(s *AgentUpsert) { + s.UpdateAppliedConfig() + }) +} + +// ClearAppliedConfig clears the value of the "applied_config" field. +func (u *AgentUpsertBulk) ClearAppliedConfig() *AgentUpsertBulk { + return u.Update(func(s *AgentUpsert) { + s.ClearAppliedConfig() + }) +} + +// SetAncestry sets the "ancestry" field. +func (u *AgentUpsertBulk) SetAncestry(v []string) *AgentUpsertBulk { + return u.Update(func(s *AgentUpsert) { + s.SetAncestry(v) + }) +} + +// UpdateAncestry sets the "ancestry" field to the value that was provided on create. +func (u *AgentUpsertBulk) UpdateAncestry() *AgentUpsertBulk { + return u.Update(func(s *AgentUpsert) { + s.UpdateAncestry() + }) +} + +// ClearAncestry clears the value of the "ancestry" field. +func (u *AgentUpsertBulk) ClearAncestry() *AgentUpsertBulk { + return u.Update(func(s *AgentUpsert) { + s.ClearAncestry() + }) +} + +// SetUpdated sets the "updated" field. +func (u *AgentUpsertBulk) SetUpdated(v time.Time) *AgentUpsertBulk { + return u.Update(func(s *AgentUpsert) { + s.SetUpdated(v) + }) +} + +// UpdateUpdated sets the "updated" field to the value that was provided on create. +func (u *AgentUpsertBulk) UpdateUpdated() *AgentUpsertBulk { + return u.Update(func(s *AgentUpsert) { + s.UpdateUpdated() + }) +} + +// SetLastSeen sets the "last_seen" field. +func (u *AgentUpsertBulk) SetLastSeen(v time.Time) *AgentUpsertBulk { + return u.Update(func(s *AgentUpsert) { + s.SetLastSeen(v) + }) +} + +// UpdateLastSeen sets the "last_seen" field to the value that was provided on create. +func (u *AgentUpsertBulk) UpdateLastSeen() *AgentUpsertBulk { + return u.Update(func(s *AgentUpsert) { + s.UpdateLastSeen() + }) +} + +// ClearLastSeen clears the value of the "last_seen" field. +func (u *AgentUpsertBulk) ClearLastSeen() *AgentUpsertBulk { + return u.Update(func(s *AgentUpsert) { + s.ClearLastSeen() + }) +} + +// SetLastActivityEvent sets the "last_activity_event" field. +func (u *AgentUpsertBulk) SetLastActivityEvent(v time.Time) *AgentUpsertBulk { + return u.Update(func(s *AgentUpsert) { + s.SetLastActivityEvent(v) + }) +} + +// UpdateLastActivityEvent sets the "last_activity_event" field to the value that was provided on create. +func (u *AgentUpsertBulk) UpdateLastActivityEvent() *AgentUpsertBulk { + return u.Update(func(s *AgentUpsert) { + s.UpdateLastActivityEvent() + }) +} + +// ClearLastActivityEvent clears the value of the "last_activity_event" field. +func (u *AgentUpsertBulk) ClearLastActivityEvent() *AgentUpsertBulk { + return u.Update(func(s *AgentUpsert) { + s.ClearLastActivityEvent() + }) +} + +// SetStartedAt sets the "started_at" field. +func (u *AgentUpsertBulk) SetStartedAt(v time.Time) *AgentUpsertBulk { + return u.Update(func(s *AgentUpsert) { + s.SetStartedAt(v) + }) +} + +// UpdateStartedAt sets the "started_at" field to the value that was provided on create. +func (u *AgentUpsertBulk) UpdateStartedAt() *AgentUpsertBulk { + return u.Update(func(s *AgentUpsert) { + s.UpdateStartedAt() + }) +} + +// ClearStartedAt clears the value of the "started_at" field. +func (u *AgentUpsertBulk) ClearStartedAt() *AgentUpsertBulk { + return u.Update(func(s *AgentUpsert) { + s.ClearStartedAt() + }) +} + +// SetDeletedAt sets the "deleted_at" field. +func (u *AgentUpsertBulk) SetDeletedAt(v time.Time) *AgentUpsertBulk { + return u.Update(func(s *AgentUpsert) { + s.SetDeletedAt(v) + }) +} + +// UpdateDeletedAt sets the "deleted_at" field to the value that was provided on create. +func (u *AgentUpsertBulk) UpdateDeletedAt() *AgentUpsertBulk { + return u.Update(func(s *AgentUpsert) { + s.UpdateDeletedAt() + }) +} + +// ClearDeletedAt clears the value of the "deleted_at" field. +func (u *AgentUpsertBulk) ClearDeletedAt() *AgentUpsertBulk { + return u.Update(func(s *AgentUpsert) { + s.ClearDeletedAt() + }) +} + +// SetStateVersion sets the "state_version" field. +func (u *AgentUpsertBulk) SetStateVersion(v int64) *AgentUpsertBulk { + return u.Update(func(s *AgentUpsert) { + s.SetStateVersion(v) + }) +} + +// AddStateVersion adds v to the "state_version" field. +func (u *AgentUpsertBulk) AddStateVersion(v int64) *AgentUpsertBulk { + return u.Update(func(s *AgentUpsert) { + s.AddStateVersion(v) + }) +} + +// UpdateStateVersion sets the "state_version" field to the value that was provided on create. +func (u *AgentUpsertBulk) UpdateStateVersion() *AgentUpsertBulk { + return u.Update(func(s *AgentUpsert) { + s.UpdateStateVersion() + }) +} + +// Exec executes the query. +func (u *AgentUpsertBulk) Exec(ctx context.Context) error { + if u.create.err != nil { + return u.create.err + } + for i, b := range u.create.builders { + if len(b.conflict) != 0 { + return fmt.Errorf("ent: OnConflict was set for builder %d. Set it on the AgentCreateBulk instead", i) + } + } + if len(u.create.conflict) == 0 { + return errors.New("ent: missing options for AgentCreateBulk.OnConflict") + } + return u.create.Exec(ctx) +} + +// ExecX is like Exec, but panics if an error occurs. +func (u *AgentUpsertBulk) ExecX(ctx context.Context) { + if err := u.create.Exec(ctx); err != nil { + panic(err) + } +} diff --git a/pkg/ent/agent_query.go b/pkg/ent/agent_query.go index c349c6c07..02d6c6c70 100644 --- a/pkg/ent/agent_query.go +++ b/pkg/ent/agent_query.go @@ -9,6 +9,7 @@ import ( "math" "entgo.io/ent" + "entgo.io/ent/dialect" "entgo.io/ent/dialect/sql" "entgo.io/ent/dialect/sql/sqlgraph" "entgo.io/ent/schema/field" @@ -17,7 +18,6 @@ import ( "github.com/GoogleCloudPlatform/scion/pkg/ent/policybinding" "github.com/GoogleCloudPlatform/scion/pkg/ent/predicate" "github.com/GoogleCloudPlatform/scion/pkg/ent/project" - "github.com/GoogleCloudPlatform/scion/pkg/ent/user" "github.com/google/uuid" ) @@ -29,10 +29,9 @@ type AgentQuery struct { inters []Interceptor predicates []predicate.Agent withProject *ProjectQuery - withCreator *UserQuery - withOwner *UserQuery withMemberships *GroupMembershipQuery withPolicyBindings *PolicyBindingQuery + modifiers []func(*sql.Selector) // intermediate query (i.e. traversal path). sql *sql.Selector path func(context.Context) (*sql.Selector, error) @@ -91,50 +90,6 @@ func (_q *AgentQuery) QueryProject() *ProjectQuery { return query } -// QueryCreator chains the current query on the "creator" edge. -func (_q *AgentQuery) QueryCreator() *UserQuery { - query := (&UserClient{config: _q.config}).Query() - query.path = func(ctx context.Context) (fromU *sql.Selector, err error) { - if err := _q.prepareQuery(ctx); err != nil { - return nil, err - } - selector := _q.sqlQuery(ctx) - if err := selector.Err(); err != nil { - return nil, err - } - step := sqlgraph.NewStep( - sqlgraph.From(agent.Table, agent.FieldID, selector), - sqlgraph.To(user.Table, user.FieldID), - sqlgraph.Edge(sqlgraph.M2O, true, agent.CreatorTable, agent.CreatorColumn), - ) - fromU = sqlgraph.SetNeighbors(_q.driver.Dialect(), step) - return fromU, nil - } - return query -} - -// QueryOwner chains the current query on the "owner" edge. -func (_q *AgentQuery) QueryOwner() *UserQuery { - query := (&UserClient{config: _q.config}).Query() - query.path = func(ctx context.Context) (fromU *sql.Selector, err error) { - if err := _q.prepareQuery(ctx); err != nil { - return nil, err - } - selector := _q.sqlQuery(ctx) - if err := selector.Err(); err != nil { - return nil, err - } - step := sqlgraph.NewStep( - sqlgraph.From(agent.Table, agent.FieldID, selector), - sqlgraph.To(user.Table, user.FieldID), - sqlgraph.Edge(sqlgraph.M2O, true, agent.OwnerTable, agent.OwnerColumn), - ) - fromU = sqlgraph.SetNeighbors(_q.driver.Dialect(), step) - return fromU, nil - } - return query -} - // QueryMemberships chains the current query on the "memberships" edge. func (_q *AgentQuery) QueryMemberships() *GroupMembershipQuery { query := (&GroupMembershipClient{config: _q.config}).Query() @@ -372,8 +327,6 @@ func (_q *AgentQuery) Clone() *AgentQuery { inters: append([]Interceptor{}, _q.inters...), predicates: append([]predicate.Agent{}, _q.predicates...), withProject: _q.withProject.Clone(), - withCreator: _q.withCreator.Clone(), - withOwner: _q.withOwner.Clone(), withMemberships: _q.withMemberships.Clone(), withPolicyBindings: _q.withPolicyBindings.Clone(), // clone intermediate query. @@ -393,28 +346,6 @@ func (_q *AgentQuery) WithProject(opts ...func(*ProjectQuery)) *AgentQuery { return _q } -// WithCreator tells the query-builder to eager-load the nodes that are connected to -// the "creator" edge. The optional arguments are used to configure the query builder of the edge. -func (_q *AgentQuery) WithCreator(opts ...func(*UserQuery)) *AgentQuery { - query := (&UserClient{config: _q.config}).Query() - for _, opt := range opts { - opt(query) - } - _q.withCreator = query - return _q -} - -// WithOwner tells the query-builder to eager-load the nodes that are connected to -// the "owner" edge. The optional arguments are used to configure the query builder of the edge. -func (_q *AgentQuery) WithOwner(opts ...func(*UserQuery)) *AgentQuery { - query := (&UserClient{config: _q.config}).Query() - for _, opt := range opts { - opt(query) - } - _q.withOwner = query - return _q -} - // WithMemberships tells the query-builder to eager-load the nodes that are connected to // the "memberships" edge. The optional arguments are used to configure the query builder of the edge. func (_q *AgentQuery) WithMemberships(opts ...func(*GroupMembershipQuery)) *AgentQuery { @@ -515,10 +446,8 @@ func (_q *AgentQuery) sqlAll(ctx context.Context, hooks ...queryHook) ([]*Agent, var ( nodes = []*Agent{} _spec = _q.querySpec() - loadedTypes = [5]bool{ + loadedTypes = [3]bool{ _q.withProject != nil, - _q.withCreator != nil, - _q.withOwner != nil, _q.withMemberships != nil, _q.withPolicyBindings != nil, } @@ -532,6 +461,9 @@ func (_q *AgentQuery) sqlAll(ctx context.Context, hooks ...queryHook) ([]*Agent, node.Edges.loadedTypes = loadedTypes return node.assignValues(columns, values) } + if len(_q.modifiers) > 0 { + _spec.Modifiers = _q.modifiers + } for i := range hooks { hooks[i](ctx, _spec) } @@ -547,18 +479,6 @@ func (_q *AgentQuery) sqlAll(ctx context.Context, hooks ...queryHook) ([]*Agent, return nil, err } } - if query := _q.withCreator; query != nil { - if err := _q.loadCreator(ctx, query, nodes, nil, - func(n *Agent, e *User) { n.Edges.Creator = e }); err != nil { - return nil, err - } - } - if query := _q.withOwner; query != nil { - if err := _q.loadOwner(ctx, query, nodes, nil, - func(n *Agent, e *User) { n.Edges.Owner = e }); err != nil { - return nil, err - } - } if query := _q.withMemberships; query != nil { if err := _q.loadMemberships(ctx, query, nodes, func(n *Agent) { n.Edges.Memberships = []*GroupMembership{} }, @@ -605,70 +525,6 @@ func (_q *AgentQuery) loadProject(ctx context.Context, query *ProjectQuery, node } return nil } -func (_q *AgentQuery) loadCreator(ctx context.Context, query *UserQuery, nodes []*Agent, init func(*Agent), assign func(*Agent, *User)) error { - ids := make([]uuid.UUID, 0, len(nodes)) - nodeids := make(map[uuid.UUID][]*Agent) - for i := range nodes { - if nodes[i].CreatedBy == nil { - continue - } - fk := *nodes[i].CreatedBy - if _, ok := nodeids[fk]; !ok { - ids = append(ids, fk) - } - nodeids[fk] = append(nodeids[fk], nodes[i]) - } - if len(ids) == 0 { - return nil - } - query.Where(user.IDIn(ids...)) - neighbors, err := query.All(ctx) - if err != nil { - return err - } - for _, n := range neighbors { - nodes, ok := nodeids[n.ID] - if !ok { - return fmt.Errorf(`unexpected foreign-key "created_by" returned %v`, n.ID) - } - for i := range nodes { - assign(nodes[i], n) - } - } - return nil -} -func (_q *AgentQuery) loadOwner(ctx context.Context, query *UserQuery, nodes []*Agent, init func(*Agent), assign func(*Agent, *User)) error { - ids := make([]uuid.UUID, 0, len(nodes)) - nodeids := make(map[uuid.UUID][]*Agent) - for i := range nodes { - if nodes[i].OwnerID == nil { - continue - } - fk := *nodes[i].OwnerID - if _, ok := nodeids[fk]; !ok { - ids = append(ids, fk) - } - nodeids[fk] = append(nodeids[fk], nodes[i]) - } - if len(ids) == 0 { - return nil - } - query.Where(user.IDIn(ids...)) - neighbors, err := query.All(ctx) - if err != nil { - return err - } - for _, n := range neighbors { - nodes, ok := nodeids[n.ID] - if !ok { - return fmt.Errorf(`unexpected foreign-key "owner_id" returned %v`, n.ID) - } - for i := range nodes { - assign(nodes[i], n) - } - } - return nil -} func (_q *AgentQuery) loadMemberships(ctx context.Context, query *GroupMembershipQuery, nodes []*Agent, init func(*Agent), assign func(*Agent, *GroupMembership)) error { fks := make([]driver.Value, 0, len(nodes)) nodeids := make(map[uuid.UUID]*Agent) @@ -738,6 +594,9 @@ func (_q *AgentQuery) loadPolicyBindings(ctx context.Context, query *PolicyBindi func (_q *AgentQuery) sqlCount(ctx context.Context) (int, error) { _spec := _q.querySpec() + if len(_q.modifiers) > 0 { + _spec.Modifiers = _q.modifiers + } _spec.Node.Columns = _q.ctx.Fields if len(_q.ctx.Fields) > 0 { _spec.Unique = _q.ctx.Unique != nil && *_q.ctx.Unique @@ -764,12 +623,6 @@ func (_q *AgentQuery) querySpec() *sqlgraph.QuerySpec { if _q.withProject != nil { _spec.Node.AddColumnOnce(agent.FieldProjectID) } - if _q.withCreator != nil { - _spec.Node.AddColumnOnce(agent.FieldCreatedBy) - } - if _q.withOwner != nil { - _spec.Node.AddColumnOnce(agent.FieldOwnerID) - } } if ps := _q.predicates; len(ps) > 0 { _spec.Predicate = func(selector *sql.Selector) { @@ -809,6 +662,9 @@ func (_q *AgentQuery) sqlQuery(ctx context.Context) *sql.Selector { if _q.ctx.Unique != nil && *_q.ctx.Unique { selector.Distinct() } + for _, m := range _q.modifiers { + m(selector) + } for _, p := range _q.predicates { p(selector) } @@ -826,6 +682,32 @@ func (_q *AgentQuery) sqlQuery(ctx context.Context) *sql.Selector { return selector } +// ForUpdate locks the selected rows against concurrent updates, and prevent them from being +// updated, deleted or "selected ... for update" by other sessions, until the transaction is +// either committed or rolled-back. +func (_q *AgentQuery) ForUpdate(opts ...sql.LockOption) *AgentQuery { + if _q.driver.Dialect() == dialect.Postgres { + _q.Unique(false) + } + _q.modifiers = append(_q.modifiers, func(s *sql.Selector) { + s.ForUpdate(opts...) + }) + return _q +} + +// ForShare behaves similarly to ForUpdate, except that it acquires a shared mode lock +// on any rows that are read. Other sessions can read the rows, but cannot modify them +// until your transaction commits. +func (_q *AgentQuery) ForShare(opts ...sql.LockOption) *AgentQuery { + if _q.driver.Dialect() == dialect.Postgres { + _q.Unique(false) + } + _q.modifiers = append(_q.modifiers, func(s *sql.Selector) { + s.ForShare(opts...) + }) + return _q +} + // AgentGroupBy is the group-by builder for Agent entities. type AgentGroupBy struct { selector diff --git a/pkg/ent/agent_update.go b/pkg/ent/agent_update.go index b9aa2f07c..bc98291cf 100644 --- a/pkg/ent/agent_update.go +++ b/pkg/ent/agent_update.go @@ -10,13 +10,13 @@ import ( "entgo.io/ent/dialect/sql" "entgo.io/ent/dialect/sql/sqlgraph" + "entgo.io/ent/dialect/sql/sqljson" "entgo.io/ent/schema/field" "github.com/GoogleCloudPlatform/scion/pkg/ent/agent" "github.com/GoogleCloudPlatform/scion/pkg/ent/groupmembership" "github.com/GoogleCloudPlatform/scion/pkg/ent/policybinding" "github.com/GoogleCloudPlatform/scion/pkg/ent/predicate" "github.com/GoogleCloudPlatform/scion/pkg/ent/project" - "github.com/GoogleCloudPlatform/scion/pkg/ent/user" "github.com/google/uuid" ) @@ -177,39 +177,488 @@ func (_u *AgentUpdate) SetNillableVisibility(v *string) *AgentUpdate { return _u } +// SetLabels sets the "labels" field. +func (_u *AgentUpdate) SetLabels(v map[string]string) *AgentUpdate { + _u.mutation.SetLabels(v) + return _u +} + +// ClearLabels clears the value of the "labels" field. +func (_u *AgentUpdate) ClearLabels() *AgentUpdate { + _u.mutation.ClearLabels() + return _u +} + +// SetAnnotations sets the "annotations" field. +func (_u *AgentUpdate) SetAnnotations(v map[string]string) *AgentUpdate { + _u.mutation.SetAnnotations(v) + return _u +} + +// ClearAnnotations clears the value of the "annotations" field. +func (_u *AgentUpdate) ClearAnnotations() *AgentUpdate { + _u.mutation.ClearAnnotations() + return _u +} + +// SetPhase sets the "phase" field. +func (_u *AgentUpdate) SetPhase(v string) *AgentUpdate { + _u.mutation.SetPhase(v) + return _u +} + +// SetNillablePhase sets the "phase" field if the given value is not nil. +func (_u *AgentUpdate) SetNillablePhase(v *string) *AgentUpdate { + if v != nil { + _u.SetPhase(*v) + } + return _u +} + +// ClearPhase clears the value of the "phase" field. +func (_u *AgentUpdate) ClearPhase() *AgentUpdate { + _u.mutation.ClearPhase() + return _u +} + +// SetActivity sets the "activity" field. +func (_u *AgentUpdate) SetActivity(v string) *AgentUpdate { + _u.mutation.SetActivity(v) + return _u +} + +// SetNillableActivity sets the "activity" field if the given value is not nil. +func (_u *AgentUpdate) SetNillableActivity(v *string) *AgentUpdate { + if v != nil { + _u.SetActivity(*v) + } + return _u +} + +// ClearActivity clears the value of the "activity" field. +func (_u *AgentUpdate) ClearActivity() *AgentUpdate { + _u.mutation.ClearActivity() + return _u +} + +// SetToolName sets the "tool_name" field. +func (_u *AgentUpdate) SetToolName(v string) *AgentUpdate { + _u.mutation.SetToolName(v) + return _u +} + +// SetNillableToolName sets the "tool_name" field if the given value is not nil. +func (_u *AgentUpdate) SetNillableToolName(v *string) *AgentUpdate { + if v != nil { + _u.SetToolName(*v) + } + return _u +} + +// ClearToolName clears the value of the "tool_name" field. +func (_u *AgentUpdate) ClearToolName() *AgentUpdate { + _u.mutation.ClearToolName() + return _u +} + +// SetConnectionState sets the "connection_state" field. +func (_u *AgentUpdate) SetConnectionState(v string) *AgentUpdate { + _u.mutation.SetConnectionState(v) + return _u +} + +// SetNillableConnectionState sets the "connection_state" field if the given value is not nil. +func (_u *AgentUpdate) SetNillableConnectionState(v *string) *AgentUpdate { + if v != nil { + _u.SetConnectionState(*v) + } + return _u +} + +// ClearConnectionState clears the value of the "connection_state" field. +func (_u *AgentUpdate) ClearConnectionState() *AgentUpdate { + _u.mutation.ClearConnectionState() + return _u +} + +// SetContainerStatus sets the "container_status" field. +func (_u *AgentUpdate) SetContainerStatus(v string) *AgentUpdate { + _u.mutation.SetContainerStatus(v) + return _u +} + +// SetNillableContainerStatus sets the "container_status" field if the given value is not nil. +func (_u *AgentUpdate) SetNillableContainerStatus(v *string) *AgentUpdate { + if v != nil { + _u.SetContainerStatus(*v) + } + return _u +} + +// ClearContainerStatus clears the value of the "container_status" field. +func (_u *AgentUpdate) ClearContainerStatus() *AgentUpdate { + _u.mutation.ClearContainerStatus() + return _u +} + +// SetRuntimeState sets the "runtime_state" field. +func (_u *AgentUpdate) SetRuntimeState(v string) *AgentUpdate { + _u.mutation.SetRuntimeState(v) + return _u +} + +// SetNillableRuntimeState sets the "runtime_state" field if the given value is not nil. +func (_u *AgentUpdate) SetNillableRuntimeState(v *string) *AgentUpdate { + if v != nil { + _u.SetRuntimeState(*v) + } + return _u +} + +// ClearRuntimeState clears the value of the "runtime_state" field. +func (_u *AgentUpdate) ClearRuntimeState() *AgentUpdate { + _u.mutation.ClearRuntimeState() + return _u +} + +// SetStalledFromActivity sets the "stalled_from_activity" field. +func (_u *AgentUpdate) SetStalledFromActivity(v string) *AgentUpdate { + _u.mutation.SetStalledFromActivity(v) + return _u +} + +// SetNillableStalledFromActivity sets the "stalled_from_activity" field if the given value is not nil. +func (_u *AgentUpdate) SetNillableStalledFromActivity(v *string) *AgentUpdate { + if v != nil { + _u.SetStalledFromActivity(*v) + } + return _u +} + +// ClearStalledFromActivity clears the value of the "stalled_from_activity" field. +func (_u *AgentUpdate) ClearStalledFromActivity() *AgentUpdate { + _u.mutation.ClearStalledFromActivity() + return _u +} + +// SetCurrentTurns sets the "current_turns" field. +func (_u *AgentUpdate) SetCurrentTurns(v int) *AgentUpdate { + _u.mutation.ResetCurrentTurns() + _u.mutation.SetCurrentTurns(v) + return _u +} + +// SetNillableCurrentTurns sets the "current_turns" field if the given value is not nil. +func (_u *AgentUpdate) SetNillableCurrentTurns(v *int) *AgentUpdate { + if v != nil { + _u.SetCurrentTurns(*v) + } + return _u +} + +// AddCurrentTurns adds value to the "current_turns" field. +func (_u *AgentUpdate) AddCurrentTurns(v int) *AgentUpdate { + _u.mutation.AddCurrentTurns(v) + return _u +} + +// SetCurrentModelCalls sets the "current_model_calls" field. +func (_u *AgentUpdate) SetCurrentModelCalls(v int) *AgentUpdate { + _u.mutation.ResetCurrentModelCalls() + _u.mutation.SetCurrentModelCalls(v) + return _u +} + +// SetNillableCurrentModelCalls sets the "current_model_calls" field if the given value is not nil. +func (_u *AgentUpdate) SetNillableCurrentModelCalls(v *int) *AgentUpdate { + if v != nil { + _u.SetCurrentModelCalls(*v) + } + return _u +} + +// AddCurrentModelCalls adds value to the "current_model_calls" field. +func (_u *AgentUpdate) AddCurrentModelCalls(v int) *AgentUpdate { + _u.mutation.AddCurrentModelCalls(v) + return _u +} + +// SetImage sets the "image" field. +func (_u *AgentUpdate) SetImage(v string) *AgentUpdate { + _u.mutation.SetImage(v) + return _u +} + +// SetNillableImage sets the "image" field if the given value is not nil. +func (_u *AgentUpdate) SetNillableImage(v *string) *AgentUpdate { + if v != nil { + _u.SetImage(*v) + } + return _u +} + +// ClearImage clears the value of the "image" field. +func (_u *AgentUpdate) ClearImage() *AgentUpdate { + _u.mutation.ClearImage() + return _u +} + +// SetDetached sets the "detached" field. +func (_u *AgentUpdate) SetDetached(v bool) *AgentUpdate { + _u.mutation.SetDetached(v) + return _u +} + +// SetNillableDetached sets the "detached" field if the given value is not nil. +func (_u *AgentUpdate) SetNillableDetached(v *bool) *AgentUpdate { + if v != nil { + _u.SetDetached(*v) + } + return _u +} + +// SetRuntime sets the "runtime" field. +func (_u *AgentUpdate) SetRuntime(v string) *AgentUpdate { + _u.mutation.SetRuntime(v) + return _u +} + +// SetNillableRuntime sets the "runtime" field if the given value is not nil. +func (_u *AgentUpdate) SetNillableRuntime(v *string) *AgentUpdate { + if v != nil { + _u.SetRuntime(*v) + } + return _u +} + +// ClearRuntime clears the value of the "runtime" field. +func (_u *AgentUpdate) ClearRuntime() *AgentUpdate { + _u.mutation.ClearRuntime() + return _u +} + +// SetRuntimeBrokerID sets the "runtime_broker_id" field. +func (_u *AgentUpdate) SetRuntimeBrokerID(v string) *AgentUpdate { + _u.mutation.SetRuntimeBrokerID(v) + return _u +} + +// SetNillableRuntimeBrokerID sets the "runtime_broker_id" field if the given value is not nil. +func (_u *AgentUpdate) SetNillableRuntimeBrokerID(v *string) *AgentUpdate { + if v != nil { + _u.SetRuntimeBrokerID(*v) + } + return _u +} + +// ClearRuntimeBrokerID clears the value of the "runtime_broker_id" field. +func (_u *AgentUpdate) ClearRuntimeBrokerID() *AgentUpdate { + _u.mutation.ClearRuntimeBrokerID() + return _u +} + +// SetWebPtyEnabled sets the "web_pty_enabled" field. +func (_u *AgentUpdate) SetWebPtyEnabled(v bool) *AgentUpdate { + _u.mutation.SetWebPtyEnabled(v) + return _u +} + +// SetNillableWebPtyEnabled sets the "web_pty_enabled" field if the given value is not nil. +func (_u *AgentUpdate) SetNillableWebPtyEnabled(v *bool) *AgentUpdate { + if v != nil { + _u.SetWebPtyEnabled(*v) + } + return _u +} + +// SetTaskSummary sets the "task_summary" field. +func (_u *AgentUpdate) SetTaskSummary(v string) *AgentUpdate { + _u.mutation.SetTaskSummary(v) + return _u +} + +// SetNillableTaskSummary sets the "task_summary" field if the given value is not nil. +func (_u *AgentUpdate) SetNillableTaskSummary(v *string) *AgentUpdate { + if v != nil { + _u.SetTaskSummary(*v) + } + return _u +} + +// ClearTaskSummary clears the value of the "task_summary" field. +func (_u *AgentUpdate) ClearTaskSummary() *AgentUpdate { + _u.mutation.ClearTaskSummary() + return _u +} + +// SetMessage sets the "message" field. +func (_u *AgentUpdate) SetMessage(v string) *AgentUpdate { + _u.mutation.SetMessage(v) + return _u +} + +// SetNillableMessage sets the "message" field if the given value is not nil. +func (_u *AgentUpdate) SetNillableMessage(v *string) *AgentUpdate { + if v != nil { + _u.SetMessage(*v) + } + return _u +} + +// ClearMessage clears the value of the "message" field. +func (_u *AgentUpdate) ClearMessage() *AgentUpdate { + _u.mutation.ClearMessage() + return _u +} + +// SetAppliedConfig sets the "applied_config" field. +func (_u *AgentUpdate) SetAppliedConfig(v string) *AgentUpdate { + _u.mutation.SetAppliedConfig(v) + return _u +} + +// SetNillableAppliedConfig sets the "applied_config" field if the given value is not nil. +func (_u *AgentUpdate) SetNillableAppliedConfig(v *string) *AgentUpdate { + if v != nil { + _u.SetAppliedConfig(*v) + } + return _u +} + +// ClearAppliedConfig clears the value of the "applied_config" field. +func (_u *AgentUpdate) ClearAppliedConfig() *AgentUpdate { + _u.mutation.ClearAppliedConfig() + return _u +} + +// SetAncestry sets the "ancestry" field. +func (_u *AgentUpdate) SetAncestry(v []string) *AgentUpdate { + _u.mutation.SetAncestry(v) + return _u +} + +// AppendAncestry appends value to the "ancestry" field. +func (_u *AgentUpdate) AppendAncestry(v []string) *AgentUpdate { + _u.mutation.AppendAncestry(v) + return _u +} + +// ClearAncestry clears the value of the "ancestry" field. +func (_u *AgentUpdate) ClearAncestry() *AgentUpdate { + _u.mutation.ClearAncestry() + return _u +} + // SetUpdated sets the "updated" field. func (_u *AgentUpdate) SetUpdated(v time.Time) *AgentUpdate { _u.mutation.SetUpdated(v) return _u } -// SetProject sets the "project" edge to the Project entity. -func (_u *AgentUpdate) SetProject(v *Project) *AgentUpdate { - return _u.SetProjectID(v.ID) +// SetLastSeen sets the "last_seen" field. +func (_u *AgentUpdate) SetLastSeen(v time.Time) *AgentUpdate { + _u.mutation.SetLastSeen(v) + return _u +} + +// SetNillableLastSeen sets the "last_seen" field if the given value is not nil. +func (_u *AgentUpdate) SetNillableLastSeen(v *time.Time) *AgentUpdate { + if v != nil { + _u.SetLastSeen(*v) + } + return _u } -// SetCreatorID sets the "creator" edge to the User entity by ID. -func (_u *AgentUpdate) SetCreatorID(id uuid.UUID) *AgentUpdate { - _u.mutation.SetCreatorID(id) +// ClearLastSeen clears the value of the "last_seen" field. +func (_u *AgentUpdate) ClearLastSeen() *AgentUpdate { + _u.mutation.ClearLastSeen() return _u } -// SetNillableCreatorID sets the "creator" edge to the User entity by ID if the given value is not nil. -func (_u *AgentUpdate) SetNillableCreatorID(id *uuid.UUID) *AgentUpdate { - if id != nil { - _u = _u.SetCreatorID(*id) +// SetLastActivityEvent sets the "last_activity_event" field. +func (_u *AgentUpdate) SetLastActivityEvent(v time.Time) *AgentUpdate { + _u.mutation.SetLastActivityEvent(v) + return _u +} + +// SetNillableLastActivityEvent sets the "last_activity_event" field if the given value is not nil. +func (_u *AgentUpdate) SetNillableLastActivityEvent(v *time.Time) *AgentUpdate { + if v != nil { + _u.SetLastActivityEvent(*v) } return _u } -// SetCreator sets the "creator" edge to the User entity. -func (_u *AgentUpdate) SetCreator(v *User) *AgentUpdate { - return _u.SetCreatorID(v.ID) +// ClearLastActivityEvent clears the value of the "last_activity_event" field. +func (_u *AgentUpdate) ClearLastActivityEvent() *AgentUpdate { + _u.mutation.ClearLastActivityEvent() + return _u } -// SetOwner sets the "owner" edge to the User entity. -func (_u *AgentUpdate) SetOwner(v *User) *AgentUpdate { - return _u.SetOwnerID(v.ID) +// SetStartedAt sets the "started_at" field. +func (_u *AgentUpdate) SetStartedAt(v time.Time) *AgentUpdate { + _u.mutation.SetStartedAt(v) + return _u +} + +// SetNillableStartedAt sets the "started_at" field if the given value is not nil. +func (_u *AgentUpdate) SetNillableStartedAt(v *time.Time) *AgentUpdate { + if v != nil { + _u.SetStartedAt(*v) + } + return _u +} + +// ClearStartedAt clears the value of the "started_at" field. +func (_u *AgentUpdate) ClearStartedAt() *AgentUpdate { + _u.mutation.ClearStartedAt() + return _u +} + +// SetDeletedAt sets the "deleted_at" field. +func (_u *AgentUpdate) SetDeletedAt(v time.Time) *AgentUpdate { + _u.mutation.SetDeletedAt(v) + return _u +} + +// SetNillableDeletedAt sets the "deleted_at" field if the given value is not nil. +func (_u *AgentUpdate) SetNillableDeletedAt(v *time.Time) *AgentUpdate { + if v != nil { + _u.SetDeletedAt(*v) + } + return _u +} + +// ClearDeletedAt clears the value of the "deleted_at" field. +func (_u *AgentUpdate) ClearDeletedAt() *AgentUpdate { + _u.mutation.ClearDeletedAt() + return _u +} + +// SetStateVersion sets the "state_version" field. +func (_u *AgentUpdate) SetStateVersion(v int64) *AgentUpdate { + _u.mutation.ResetStateVersion() + _u.mutation.SetStateVersion(v) + return _u +} + +// SetNillableStateVersion sets the "state_version" field if the given value is not nil. +func (_u *AgentUpdate) SetNillableStateVersion(v *int64) *AgentUpdate { + if v != nil { + _u.SetStateVersion(*v) + } + return _u +} + +// AddStateVersion adds value to the "state_version" field. +func (_u *AgentUpdate) AddStateVersion(v int64) *AgentUpdate { + _u.mutation.AddStateVersion(v) + return _u +} + +// SetProject sets the "project" edge to the Project entity. +func (_u *AgentUpdate) SetProject(v *Project) *AgentUpdate { + return _u.SetProjectID(v.ID) } // AddMembershipIDs adds the "memberships" edge to the GroupMembership entity by IDs. @@ -253,18 +702,6 @@ func (_u *AgentUpdate) ClearProject() *AgentUpdate { return _u } -// ClearCreator clears the "creator" edge to the User entity. -func (_u *AgentUpdate) ClearCreator() *AgentUpdate { - _u.mutation.ClearCreator() - return _u -} - -// ClearOwner clears the "owner" edge to the User entity. -func (_u *AgentUpdate) ClearOwner() *AgentUpdate { - _u.mutation.ClearOwner() - return _u -} - // ClearMemberships clears all "memberships" edges to the GroupMembership entity. func (_u *AgentUpdate) ClearMemberships() *AgentUpdate { _u.mutation.ClearMemberships() @@ -393,95 +830,198 @@ func (_u *AgentUpdate) sqlSave(ctx context.Context) (_node int, err error) { if value, ok := _u.mutation.Status(); ok { _spec.SetField(agent.FieldStatus, field.TypeEnum, value) } + if value, ok := _u.mutation.CreatedBy(); ok { + _spec.SetField(agent.FieldCreatedBy, field.TypeUUID, value) + } + if _u.mutation.CreatedByCleared() { + _spec.ClearField(agent.FieldCreatedBy, field.TypeUUID) + } + if value, ok := _u.mutation.OwnerID(); ok { + _spec.SetField(agent.FieldOwnerID, field.TypeUUID, value) + } + if _u.mutation.OwnerIDCleared() { + _spec.ClearField(agent.FieldOwnerID, field.TypeUUID) + } if value, ok := _u.mutation.DelegationEnabled(); ok { _spec.SetField(agent.FieldDelegationEnabled, field.TypeBool, value) } if value, ok := _u.mutation.Visibility(); ok { _spec.SetField(agent.FieldVisibility, field.TypeString, value) } - if value, ok := _u.mutation.Updated(); ok { - _spec.SetField(agent.FieldUpdated, field.TypeTime, value) + if value, ok := _u.mutation.Labels(); ok { + _spec.SetField(agent.FieldLabels, field.TypeJSON, value) } - if _u.mutation.ProjectCleared() { - edge := &sqlgraph.EdgeSpec{ - Rel: sqlgraph.M2O, - Inverse: true, - Table: agent.ProjectTable, - Columns: []string{agent.ProjectColumn}, - Bidi: false, - Target: &sqlgraph.EdgeTarget{ - IDSpec: sqlgraph.NewFieldSpec(project.FieldID, field.TypeUUID), - }, - } - _spec.Edges.Clear = append(_spec.Edges.Clear, edge) + if _u.mutation.LabelsCleared() { + _spec.ClearField(agent.FieldLabels, field.TypeJSON) } - if nodes := _u.mutation.ProjectIDs(); len(nodes) > 0 { - edge := &sqlgraph.EdgeSpec{ - Rel: sqlgraph.M2O, - Inverse: true, - Table: agent.ProjectTable, - Columns: []string{agent.ProjectColumn}, - Bidi: false, - Target: &sqlgraph.EdgeTarget{ - IDSpec: sqlgraph.NewFieldSpec(project.FieldID, field.TypeUUID), - }, - } - for _, k := range nodes { - edge.Target.Nodes = append(edge.Target.Nodes, k) - } - _spec.Edges.Add = append(_spec.Edges.Add, edge) + if value, ok := _u.mutation.Annotations(); ok { + _spec.SetField(agent.FieldAnnotations, field.TypeJSON, value) } - if _u.mutation.CreatorCleared() { - edge := &sqlgraph.EdgeSpec{ - Rel: sqlgraph.M2O, - Inverse: true, - Table: agent.CreatorTable, - Columns: []string{agent.CreatorColumn}, - Bidi: false, - Target: &sqlgraph.EdgeTarget{ - IDSpec: sqlgraph.NewFieldSpec(user.FieldID, field.TypeUUID), - }, - } - _spec.Edges.Clear = append(_spec.Edges.Clear, edge) + if _u.mutation.AnnotationsCleared() { + _spec.ClearField(agent.FieldAnnotations, field.TypeJSON) } - if nodes := _u.mutation.CreatorIDs(); len(nodes) > 0 { - edge := &sqlgraph.EdgeSpec{ - Rel: sqlgraph.M2O, - Inverse: true, - Table: agent.CreatorTable, - Columns: []string{agent.CreatorColumn}, - Bidi: false, - Target: &sqlgraph.EdgeTarget{ - IDSpec: sqlgraph.NewFieldSpec(user.FieldID, field.TypeUUID), - }, - } - for _, k := range nodes { - edge.Target.Nodes = append(edge.Target.Nodes, k) - } - _spec.Edges.Add = append(_spec.Edges.Add, edge) + if value, ok := _u.mutation.Phase(); ok { + _spec.SetField(agent.FieldPhase, field.TypeString, value) + } + if _u.mutation.PhaseCleared() { + _spec.ClearField(agent.FieldPhase, field.TypeString) + } + if value, ok := _u.mutation.Activity(); ok { + _spec.SetField(agent.FieldActivity, field.TypeString, value) + } + if _u.mutation.ActivityCleared() { + _spec.ClearField(agent.FieldActivity, field.TypeString) + } + if value, ok := _u.mutation.ToolName(); ok { + _spec.SetField(agent.FieldToolName, field.TypeString, value) + } + if _u.mutation.ToolNameCleared() { + _spec.ClearField(agent.FieldToolName, field.TypeString) + } + if value, ok := _u.mutation.ConnectionState(); ok { + _spec.SetField(agent.FieldConnectionState, field.TypeString, value) + } + if _u.mutation.ConnectionStateCleared() { + _spec.ClearField(agent.FieldConnectionState, field.TypeString) + } + if value, ok := _u.mutation.ContainerStatus(); ok { + _spec.SetField(agent.FieldContainerStatus, field.TypeString, value) + } + if _u.mutation.ContainerStatusCleared() { + _spec.ClearField(agent.FieldContainerStatus, field.TypeString) + } + if value, ok := _u.mutation.RuntimeState(); ok { + _spec.SetField(agent.FieldRuntimeState, field.TypeString, value) + } + if _u.mutation.RuntimeStateCleared() { + _spec.ClearField(agent.FieldRuntimeState, field.TypeString) + } + if value, ok := _u.mutation.StalledFromActivity(); ok { + _spec.SetField(agent.FieldStalledFromActivity, field.TypeString, value) + } + if _u.mutation.StalledFromActivityCleared() { + _spec.ClearField(agent.FieldStalledFromActivity, field.TypeString) + } + if value, ok := _u.mutation.CurrentTurns(); ok { + _spec.SetField(agent.FieldCurrentTurns, field.TypeInt, value) + } + if value, ok := _u.mutation.AddedCurrentTurns(); ok { + _spec.AddField(agent.FieldCurrentTurns, field.TypeInt, value) + } + if value, ok := _u.mutation.CurrentModelCalls(); ok { + _spec.SetField(agent.FieldCurrentModelCalls, field.TypeInt, value) + } + if value, ok := _u.mutation.AddedCurrentModelCalls(); ok { + _spec.AddField(agent.FieldCurrentModelCalls, field.TypeInt, value) + } + if value, ok := _u.mutation.Image(); ok { + _spec.SetField(agent.FieldImage, field.TypeString, value) + } + if _u.mutation.ImageCleared() { + _spec.ClearField(agent.FieldImage, field.TypeString) + } + if value, ok := _u.mutation.Detached(); ok { + _spec.SetField(agent.FieldDetached, field.TypeBool, value) + } + if value, ok := _u.mutation.Runtime(); ok { + _spec.SetField(agent.FieldRuntime, field.TypeString, value) + } + if _u.mutation.RuntimeCleared() { + _spec.ClearField(agent.FieldRuntime, field.TypeString) + } + if value, ok := _u.mutation.RuntimeBrokerID(); ok { + _spec.SetField(agent.FieldRuntimeBrokerID, field.TypeString, value) + } + if _u.mutation.RuntimeBrokerIDCleared() { + _spec.ClearField(agent.FieldRuntimeBrokerID, field.TypeString) + } + if value, ok := _u.mutation.WebPtyEnabled(); ok { + _spec.SetField(agent.FieldWebPtyEnabled, field.TypeBool, value) + } + if value, ok := _u.mutation.TaskSummary(); ok { + _spec.SetField(agent.FieldTaskSummary, field.TypeString, value) + } + if _u.mutation.TaskSummaryCleared() { + _spec.ClearField(agent.FieldTaskSummary, field.TypeString) + } + if value, ok := _u.mutation.Message(); ok { + _spec.SetField(agent.FieldMessage, field.TypeString, value) + } + if _u.mutation.MessageCleared() { + _spec.ClearField(agent.FieldMessage, field.TypeString) + } + if value, ok := _u.mutation.AppliedConfig(); ok { + _spec.SetField(agent.FieldAppliedConfig, field.TypeString, value) + } + if _u.mutation.AppliedConfigCleared() { + _spec.ClearField(agent.FieldAppliedConfig, field.TypeString) + } + if value, ok := _u.mutation.Ancestry(); ok { + _spec.SetField(agent.FieldAncestry, field.TypeJSON, value) + } + if value, ok := _u.mutation.AppendedAncestry(); ok { + _spec.AddModifier(func(u *sql.UpdateBuilder) { + sqljson.Append(u, agent.FieldAncestry, value) + }) + } + if _u.mutation.AncestryCleared() { + _spec.ClearField(agent.FieldAncestry, field.TypeJSON) + } + if value, ok := _u.mutation.Updated(); ok { + _spec.SetField(agent.FieldUpdated, field.TypeTime, value) + } + if value, ok := _u.mutation.LastSeen(); ok { + _spec.SetField(agent.FieldLastSeen, field.TypeTime, value) } - if _u.mutation.OwnerCleared() { + if _u.mutation.LastSeenCleared() { + _spec.ClearField(agent.FieldLastSeen, field.TypeTime) + } + if value, ok := _u.mutation.LastActivityEvent(); ok { + _spec.SetField(agent.FieldLastActivityEvent, field.TypeTime, value) + } + if _u.mutation.LastActivityEventCleared() { + _spec.ClearField(agent.FieldLastActivityEvent, field.TypeTime) + } + if value, ok := _u.mutation.StartedAt(); ok { + _spec.SetField(agent.FieldStartedAt, field.TypeTime, value) + } + if _u.mutation.StartedAtCleared() { + _spec.ClearField(agent.FieldStartedAt, field.TypeTime) + } + if value, ok := _u.mutation.DeletedAt(); ok { + _spec.SetField(agent.FieldDeletedAt, field.TypeTime, value) + } + if _u.mutation.DeletedAtCleared() { + _spec.ClearField(agent.FieldDeletedAt, field.TypeTime) + } + if value, ok := _u.mutation.StateVersion(); ok { + _spec.SetField(agent.FieldStateVersion, field.TypeInt64, value) + } + if value, ok := _u.mutation.AddedStateVersion(); ok { + _spec.AddField(agent.FieldStateVersion, field.TypeInt64, value) + } + if _u.mutation.ProjectCleared() { edge := &sqlgraph.EdgeSpec{ Rel: sqlgraph.M2O, Inverse: true, - Table: agent.OwnerTable, - Columns: []string{agent.OwnerColumn}, + Table: agent.ProjectTable, + Columns: []string{agent.ProjectColumn}, Bidi: false, Target: &sqlgraph.EdgeTarget{ - IDSpec: sqlgraph.NewFieldSpec(user.FieldID, field.TypeUUID), + IDSpec: sqlgraph.NewFieldSpec(project.FieldID, field.TypeUUID), }, } _spec.Edges.Clear = append(_spec.Edges.Clear, edge) } - if nodes := _u.mutation.OwnerIDs(); len(nodes) > 0 { + if nodes := _u.mutation.ProjectIDs(); len(nodes) > 0 { edge := &sqlgraph.EdgeSpec{ Rel: sqlgraph.M2O, Inverse: true, - Table: agent.OwnerTable, - Columns: []string{agent.OwnerColumn}, + Table: agent.ProjectTable, + Columns: []string{agent.ProjectColumn}, Bidi: false, Target: &sqlgraph.EdgeTarget{ - IDSpec: sqlgraph.NewFieldSpec(user.FieldID, field.TypeUUID), + IDSpec: sqlgraph.NewFieldSpec(project.FieldID, field.TypeUUID), }, } for _, k := range nodes { @@ -587,195 +1127,644 @@ func (_u *AgentUpdate) sqlSave(ctx context.Context) (_node int, err error) { } return 0, err } - _u.mutation.done = true - return _node, nil + _u.mutation.done = true + return _node, nil +} + +// AgentUpdateOne is the builder for updating a single Agent entity. +type AgentUpdateOne struct { + config + fields []string + hooks []Hook + mutation *AgentMutation +} + +// SetSlug sets the "slug" field. +func (_u *AgentUpdateOne) SetSlug(v string) *AgentUpdateOne { + _u.mutation.SetSlug(v) + return _u +} + +// SetNillableSlug sets the "slug" field if the given value is not nil. +func (_u *AgentUpdateOne) SetNillableSlug(v *string) *AgentUpdateOne { + if v != nil { + _u.SetSlug(*v) + } + return _u +} + +// SetName sets the "name" field. +func (_u *AgentUpdateOne) SetName(v string) *AgentUpdateOne { + _u.mutation.SetName(v) + return _u +} + +// SetNillableName sets the "name" field if the given value is not nil. +func (_u *AgentUpdateOne) SetNillableName(v *string) *AgentUpdateOne { + if v != nil { + _u.SetName(*v) + } + return _u +} + +// SetTemplate sets the "template" field. +func (_u *AgentUpdateOne) SetTemplate(v string) *AgentUpdateOne { + _u.mutation.SetTemplate(v) + return _u +} + +// SetNillableTemplate sets the "template" field if the given value is not nil. +func (_u *AgentUpdateOne) SetNillableTemplate(v *string) *AgentUpdateOne { + if v != nil { + _u.SetTemplate(*v) + } + return _u +} + +// ClearTemplate clears the value of the "template" field. +func (_u *AgentUpdateOne) ClearTemplate() *AgentUpdateOne { + _u.mutation.ClearTemplate() + return _u +} + +// SetProjectID sets the "project_id" field. +func (_u *AgentUpdateOne) SetProjectID(v uuid.UUID) *AgentUpdateOne { + _u.mutation.SetProjectID(v) + return _u +} + +// SetNillableProjectID sets the "project_id" field if the given value is not nil. +func (_u *AgentUpdateOne) SetNillableProjectID(v *uuid.UUID) *AgentUpdateOne { + if v != nil { + _u.SetProjectID(*v) + } + return _u +} + +// SetStatus sets the "status" field. +func (_u *AgentUpdateOne) SetStatus(v agent.Status) *AgentUpdateOne { + _u.mutation.SetStatus(v) + return _u +} + +// SetNillableStatus sets the "status" field if the given value is not nil. +func (_u *AgentUpdateOne) SetNillableStatus(v *agent.Status) *AgentUpdateOne { + if v != nil { + _u.SetStatus(*v) + } + return _u +} + +// SetCreatedBy sets the "created_by" field. +func (_u *AgentUpdateOne) SetCreatedBy(v uuid.UUID) *AgentUpdateOne { + _u.mutation.SetCreatedBy(v) + return _u +} + +// SetNillableCreatedBy sets the "created_by" field if the given value is not nil. +func (_u *AgentUpdateOne) SetNillableCreatedBy(v *uuid.UUID) *AgentUpdateOne { + if v != nil { + _u.SetCreatedBy(*v) + } + return _u +} + +// ClearCreatedBy clears the value of the "created_by" field. +func (_u *AgentUpdateOne) ClearCreatedBy() *AgentUpdateOne { + _u.mutation.ClearCreatedBy() + return _u +} + +// SetOwnerID sets the "owner_id" field. +func (_u *AgentUpdateOne) SetOwnerID(v uuid.UUID) *AgentUpdateOne { + _u.mutation.SetOwnerID(v) + return _u +} + +// SetNillableOwnerID sets the "owner_id" field if the given value is not nil. +func (_u *AgentUpdateOne) SetNillableOwnerID(v *uuid.UUID) *AgentUpdateOne { + if v != nil { + _u.SetOwnerID(*v) + } + return _u +} + +// ClearOwnerID clears the value of the "owner_id" field. +func (_u *AgentUpdateOne) ClearOwnerID() *AgentUpdateOne { + _u.mutation.ClearOwnerID() + return _u +} + +// SetDelegationEnabled sets the "delegation_enabled" field. +func (_u *AgentUpdateOne) SetDelegationEnabled(v bool) *AgentUpdateOne { + _u.mutation.SetDelegationEnabled(v) + return _u +} + +// SetNillableDelegationEnabled sets the "delegation_enabled" field if the given value is not nil. +func (_u *AgentUpdateOne) SetNillableDelegationEnabled(v *bool) *AgentUpdateOne { + if v != nil { + _u.SetDelegationEnabled(*v) + } + return _u +} + +// SetVisibility sets the "visibility" field. +func (_u *AgentUpdateOne) SetVisibility(v string) *AgentUpdateOne { + _u.mutation.SetVisibility(v) + return _u +} + +// SetNillableVisibility sets the "visibility" field if the given value is not nil. +func (_u *AgentUpdateOne) SetNillableVisibility(v *string) *AgentUpdateOne { + if v != nil { + _u.SetVisibility(*v) + } + return _u +} + +// SetLabels sets the "labels" field. +func (_u *AgentUpdateOne) SetLabels(v map[string]string) *AgentUpdateOne { + _u.mutation.SetLabels(v) + return _u +} + +// ClearLabels clears the value of the "labels" field. +func (_u *AgentUpdateOne) ClearLabels() *AgentUpdateOne { + _u.mutation.ClearLabels() + return _u +} + +// SetAnnotations sets the "annotations" field. +func (_u *AgentUpdateOne) SetAnnotations(v map[string]string) *AgentUpdateOne { + _u.mutation.SetAnnotations(v) + return _u +} + +// ClearAnnotations clears the value of the "annotations" field. +func (_u *AgentUpdateOne) ClearAnnotations() *AgentUpdateOne { + _u.mutation.ClearAnnotations() + return _u +} + +// SetPhase sets the "phase" field. +func (_u *AgentUpdateOne) SetPhase(v string) *AgentUpdateOne { + _u.mutation.SetPhase(v) + return _u +} + +// SetNillablePhase sets the "phase" field if the given value is not nil. +func (_u *AgentUpdateOne) SetNillablePhase(v *string) *AgentUpdateOne { + if v != nil { + _u.SetPhase(*v) + } + return _u +} + +// ClearPhase clears the value of the "phase" field. +func (_u *AgentUpdateOne) ClearPhase() *AgentUpdateOne { + _u.mutation.ClearPhase() + return _u +} + +// SetActivity sets the "activity" field. +func (_u *AgentUpdateOne) SetActivity(v string) *AgentUpdateOne { + _u.mutation.SetActivity(v) + return _u +} + +// SetNillableActivity sets the "activity" field if the given value is not nil. +func (_u *AgentUpdateOne) SetNillableActivity(v *string) *AgentUpdateOne { + if v != nil { + _u.SetActivity(*v) + } + return _u +} + +// ClearActivity clears the value of the "activity" field. +func (_u *AgentUpdateOne) ClearActivity() *AgentUpdateOne { + _u.mutation.ClearActivity() + return _u +} + +// SetToolName sets the "tool_name" field. +func (_u *AgentUpdateOne) SetToolName(v string) *AgentUpdateOne { + _u.mutation.SetToolName(v) + return _u +} + +// SetNillableToolName sets the "tool_name" field if the given value is not nil. +func (_u *AgentUpdateOne) SetNillableToolName(v *string) *AgentUpdateOne { + if v != nil { + _u.SetToolName(*v) + } + return _u +} + +// ClearToolName clears the value of the "tool_name" field. +func (_u *AgentUpdateOne) ClearToolName() *AgentUpdateOne { + _u.mutation.ClearToolName() + return _u +} + +// SetConnectionState sets the "connection_state" field. +func (_u *AgentUpdateOne) SetConnectionState(v string) *AgentUpdateOne { + _u.mutation.SetConnectionState(v) + return _u +} + +// SetNillableConnectionState sets the "connection_state" field if the given value is not nil. +func (_u *AgentUpdateOne) SetNillableConnectionState(v *string) *AgentUpdateOne { + if v != nil { + _u.SetConnectionState(*v) + } + return _u +} + +// ClearConnectionState clears the value of the "connection_state" field. +func (_u *AgentUpdateOne) ClearConnectionState() *AgentUpdateOne { + _u.mutation.ClearConnectionState() + return _u +} + +// SetContainerStatus sets the "container_status" field. +func (_u *AgentUpdateOne) SetContainerStatus(v string) *AgentUpdateOne { + _u.mutation.SetContainerStatus(v) + return _u +} + +// SetNillableContainerStatus sets the "container_status" field if the given value is not nil. +func (_u *AgentUpdateOne) SetNillableContainerStatus(v *string) *AgentUpdateOne { + if v != nil { + _u.SetContainerStatus(*v) + } + return _u +} + +// ClearContainerStatus clears the value of the "container_status" field. +func (_u *AgentUpdateOne) ClearContainerStatus() *AgentUpdateOne { + _u.mutation.ClearContainerStatus() + return _u +} + +// SetRuntimeState sets the "runtime_state" field. +func (_u *AgentUpdateOne) SetRuntimeState(v string) *AgentUpdateOne { + _u.mutation.SetRuntimeState(v) + return _u +} + +// SetNillableRuntimeState sets the "runtime_state" field if the given value is not nil. +func (_u *AgentUpdateOne) SetNillableRuntimeState(v *string) *AgentUpdateOne { + if v != nil { + _u.SetRuntimeState(*v) + } + return _u +} + +// ClearRuntimeState clears the value of the "runtime_state" field. +func (_u *AgentUpdateOne) ClearRuntimeState() *AgentUpdateOne { + _u.mutation.ClearRuntimeState() + return _u +} + +// SetStalledFromActivity sets the "stalled_from_activity" field. +func (_u *AgentUpdateOne) SetStalledFromActivity(v string) *AgentUpdateOne { + _u.mutation.SetStalledFromActivity(v) + return _u +} + +// SetNillableStalledFromActivity sets the "stalled_from_activity" field if the given value is not nil. +func (_u *AgentUpdateOne) SetNillableStalledFromActivity(v *string) *AgentUpdateOne { + if v != nil { + _u.SetStalledFromActivity(*v) + } + return _u } -// AgentUpdateOne is the builder for updating a single Agent entity. -type AgentUpdateOne struct { - config - fields []string - hooks []Hook - mutation *AgentMutation +// ClearStalledFromActivity clears the value of the "stalled_from_activity" field. +func (_u *AgentUpdateOne) ClearStalledFromActivity() *AgentUpdateOne { + _u.mutation.ClearStalledFromActivity() + return _u } -// SetSlug sets the "slug" field. -func (_u *AgentUpdateOne) SetSlug(v string) *AgentUpdateOne { - _u.mutation.SetSlug(v) +// SetCurrentTurns sets the "current_turns" field. +func (_u *AgentUpdateOne) SetCurrentTurns(v int) *AgentUpdateOne { + _u.mutation.ResetCurrentTurns() + _u.mutation.SetCurrentTurns(v) return _u } -// SetNillableSlug sets the "slug" field if the given value is not nil. -func (_u *AgentUpdateOne) SetNillableSlug(v *string) *AgentUpdateOne { +// SetNillableCurrentTurns sets the "current_turns" field if the given value is not nil. +func (_u *AgentUpdateOne) SetNillableCurrentTurns(v *int) *AgentUpdateOne { if v != nil { - _u.SetSlug(*v) + _u.SetCurrentTurns(*v) } return _u } -// SetName sets the "name" field. -func (_u *AgentUpdateOne) SetName(v string) *AgentUpdateOne { - _u.mutation.SetName(v) +// AddCurrentTurns adds value to the "current_turns" field. +func (_u *AgentUpdateOne) AddCurrentTurns(v int) *AgentUpdateOne { + _u.mutation.AddCurrentTurns(v) return _u } -// SetNillableName sets the "name" field if the given value is not nil. -func (_u *AgentUpdateOne) SetNillableName(v *string) *AgentUpdateOne { +// SetCurrentModelCalls sets the "current_model_calls" field. +func (_u *AgentUpdateOne) SetCurrentModelCalls(v int) *AgentUpdateOne { + _u.mutation.ResetCurrentModelCalls() + _u.mutation.SetCurrentModelCalls(v) + return _u +} + +// SetNillableCurrentModelCalls sets the "current_model_calls" field if the given value is not nil. +func (_u *AgentUpdateOne) SetNillableCurrentModelCalls(v *int) *AgentUpdateOne { if v != nil { - _u.SetName(*v) + _u.SetCurrentModelCalls(*v) } return _u } -// SetTemplate sets the "template" field. -func (_u *AgentUpdateOne) SetTemplate(v string) *AgentUpdateOne { - _u.mutation.SetTemplate(v) +// AddCurrentModelCalls adds value to the "current_model_calls" field. +func (_u *AgentUpdateOne) AddCurrentModelCalls(v int) *AgentUpdateOne { + _u.mutation.AddCurrentModelCalls(v) return _u } -// SetNillableTemplate sets the "template" field if the given value is not nil. -func (_u *AgentUpdateOne) SetNillableTemplate(v *string) *AgentUpdateOne { +// SetImage sets the "image" field. +func (_u *AgentUpdateOne) SetImage(v string) *AgentUpdateOne { + _u.mutation.SetImage(v) + return _u +} + +// SetNillableImage sets the "image" field if the given value is not nil. +func (_u *AgentUpdateOne) SetNillableImage(v *string) *AgentUpdateOne { if v != nil { - _u.SetTemplate(*v) + _u.SetImage(*v) } return _u } -// ClearTemplate clears the value of the "template" field. -func (_u *AgentUpdateOne) ClearTemplate() *AgentUpdateOne { - _u.mutation.ClearTemplate() +// ClearImage clears the value of the "image" field. +func (_u *AgentUpdateOne) ClearImage() *AgentUpdateOne { + _u.mutation.ClearImage() return _u } -// SetProjectID sets the "project_id" field. -func (_u *AgentUpdateOne) SetProjectID(v uuid.UUID) *AgentUpdateOne { - _u.mutation.SetProjectID(v) +// SetDetached sets the "detached" field. +func (_u *AgentUpdateOne) SetDetached(v bool) *AgentUpdateOne { + _u.mutation.SetDetached(v) return _u } -// SetNillableProjectID sets the "project_id" field if the given value is not nil. -func (_u *AgentUpdateOne) SetNillableProjectID(v *uuid.UUID) *AgentUpdateOne { +// SetNillableDetached sets the "detached" field if the given value is not nil. +func (_u *AgentUpdateOne) SetNillableDetached(v *bool) *AgentUpdateOne { if v != nil { - _u.SetProjectID(*v) + _u.SetDetached(*v) } return _u } -// SetStatus sets the "status" field. -func (_u *AgentUpdateOne) SetStatus(v agent.Status) *AgentUpdateOne { - _u.mutation.SetStatus(v) +// SetRuntime sets the "runtime" field. +func (_u *AgentUpdateOne) SetRuntime(v string) *AgentUpdateOne { + _u.mutation.SetRuntime(v) return _u } -// SetNillableStatus sets the "status" field if the given value is not nil. -func (_u *AgentUpdateOne) SetNillableStatus(v *agent.Status) *AgentUpdateOne { +// SetNillableRuntime sets the "runtime" field if the given value is not nil. +func (_u *AgentUpdateOne) SetNillableRuntime(v *string) *AgentUpdateOne { if v != nil { - _u.SetStatus(*v) + _u.SetRuntime(*v) } return _u } -// SetCreatedBy sets the "created_by" field. -func (_u *AgentUpdateOne) SetCreatedBy(v uuid.UUID) *AgentUpdateOne { - _u.mutation.SetCreatedBy(v) +// ClearRuntime clears the value of the "runtime" field. +func (_u *AgentUpdateOne) ClearRuntime() *AgentUpdateOne { + _u.mutation.ClearRuntime() return _u } -// SetNillableCreatedBy sets the "created_by" field if the given value is not nil. -func (_u *AgentUpdateOne) SetNillableCreatedBy(v *uuid.UUID) *AgentUpdateOne { +// SetRuntimeBrokerID sets the "runtime_broker_id" field. +func (_u *AgentUpdateOne) SetRuntimeBrokerID(v string) *AgentUpdateOne { + _u.mutation.SetRuntimeBrokerID(v) + return _u +} + +// SetNillableRuntimeBrokerID sets the "runtime_broker_id" field if the given value is not nil. +func (_u *AgentUpdateOne) SetNillableRuntimeBrokerID(v *string) *AgentUpdateOne { if v != nil { - _u.SetCreatedBy(*v) + _u.SetRuntimeBrokerID(*v) } return _u } -// ClearCreatedBy clears the value of the "created_by" field. -func (_u *AgentUpdateOne) ClearCreatedBy() *AgentUpdateOne { - _u.mutation.ClearCreatedBy() +// ClearRuntimeBrokerID clears the value of the "runtime_broker_id" field. +func (_u *AgentUpdateOne) ClearRuntimeBrokerID() *AgentUpdateOne { + _u.mutation.ClearRuntimeBrokerID() return _u } -// SetOwnerID sets the "owner_id" field. -func (_u *AgentUpdateOne) SetOwnerID(v uuid.UUID) *AgentUpdateOne { - _u.mutation.SetOwnerID(v) +// SetWebPtyEnabled sets the "web_pty_enabled" field. +func (_u *AgentUpdateOne) SetWebPtyEnabled(v bool) *AgentUpdateOne { + _u.mutation.SetWebPtyEnabled(v) return _u } -// SetNillableOwnerID sets the "owner_id" field if the given value is not nil. -func (_u *AgentUpdateOne) SetNillableOwnerID(v *uuid.UUID) *AgentUpdateOne { +// SetNillableWebPtyEnabled sets the "web_pty_enabled" field if the given value is not nil. +func (_u *AgentUpdateOne) SetNillableWebPtyEnabled(v *bool) *AgentUpdateOne { if v != nil { - _u.SetOwnerID(*v) + _u.SetWebPtyEnabled(*v) } return _u } -// ClearOwnerID clears the value of the "owner_id" field. -func (_u *AgentUpdateOne) ClearOwnerID() *AgentUpdateOne { - _u.mutation.ClearOwnerID() +// SetTaskSummary sets the "task_summary" field. +func (_u *AgentUpdateOne) SetTaskSummary(v string) *AgentUpdateOne { + _u.mutation.SetTaskSummary(v) return _u } -// SetDelegationEnabled sets the "delegation_enabled" field. -func (_u *AgentUpdateOne) SetDelegationEnabled(v bool) *AgentUpdateOne { - _u.mutation.SetDelegationEnabled(v) +// SetNillableTaskSummary sets the "task_summary" field if the given value is not nil. +func (_u *AgentUpdateOne) SetNillableTaskSummary(v *string) *AgentUpdateOne { + if v != nil { + _u.SetTaskSummary(*v) + } return _u } -// SetNillableDelegationEnabled sets the "delegation_enabled" field if the given value is not nil. -func (_u *AgentUpdateOne) SetNillableDelegationEnabled(v *bool) *AgentUpdateOne { +// ClearTaskSummary clears the value of the "task_summary" field. +func (_u *AgentUpdateOne) ClearTaskSummary() *AgentUpdateOne { + _u.mutation.ClearTaskSummary() + return _u +} + +// SetMessage sets the "message" field. +func (_u *AgentUpdateOne) SetMessage(v string) *AgentUpdateOne { + _u.mutation.SetMessage(v) + return _u +} + +// SetNillableMessage sets the "message" field if the given value is not nil. +func (_u *AgentUpdateOne) SetNillableMessage(v *string) *AgentUpdateOne { if v != nil { - _u.SetDelegationEnabled(*v) + _u.SetMessage(*v) } return _u } -// SetVisibility sets the "visibility" field. -func (_u *AgentUpdateOne) SetVisibility(v string) *AgentUpdateOne { - _u.mutation.SetVisibility(v) +// ClearMessage clears the value of the "message" field. +func (_u *AgentUpdateOne) ClearMessage() *AgentUpdateOne { + _u.mutation.ClearMessage() return _u } -// SetNillableVisibility sets the "visibility" field if the given value is not nil. -func (_u *AgentUpdateOne) SetNillableVisibility(v *string) *AgentUpdateOne { +// SetAppliedConfig sets the "applied_config" field. +func (_u *AgentUpdateOne) SetAppliedConfig(v string) *AgentUpdateOne { + _u.mutation.SetAppliedConfig(v) + return _u +} + +// SetNillableAppliedConfig sets the "applied_config" field if the given value is not nil. +func (_u *AgentUpdateOne) SetNillableAppliedConfig(v *string) *AgentUpdateOne { if v != nil { - _u.SetVisibility(*v) + _u.SetAppliedConfig(*v) } return _u } +// ClearAppliedConfig clears the value of the "applied_config" field. +func (_u *AgentUpdateOne) ClearAppliedConfig() *AgentUpdateOne { + _u.mutation.ClearAppliedConfig() + return _u +} + +// SetAncestry sets the "ancestry" field. +func (_u *AgentUpdateOne) SetAncestry(v []string) *AgentUpdateOne { + _u.mutation.SetAncestry(v) + return _u +} + +// AppendAncestry appends value to the "ancestry" field. +func (_u *AgentUpdateOne) AppendAncestry(v []string) *AgentUpdateOne { + _u.mutation.AppendAncestry(v) + return _u +} + +// ClearAncestry clears the value of the "ancestry" field. +func (_u *AgentUpdateOne) ClearAncestry() *AgentUpdateOne { + _u.mutation.ClearAncestry() + return _u +} + // SetUpdated sets the "updated" field. func (_u *AgentUpdateOne) SetUpdated(v time.Time) *AgentUpdateOne { _u.mutation.SetUpdated(v) return _u } -// SetProject sets the "project" edge to the Project entity. -func (_u *AgentUpdateOne) SetProject(v *Project) *AgentUpdateOne { - return _u.SetProjectID(v.ID) +// SetLastSeen sets the "last_seen" field. +func (_u *AgentUpdateOne) SetLastSeen(v time.Time) *AgentUpdateOne { + _u.mutation.SetLastSeen(v) + return _u +} + +// SetNillableLastSeen sets the "last_seen" field if the given value is not nil. +func (_u *AgentUpdateOne) SetNillableLastSeen(v *time.Time) *AgentUpdateOne { + if v != nil { + _u.SetLastSeen(*v) + } + return _u +} + +// ClearLastSeen clears the value of the "last_seen" field. +func (_u *AgentUpdateOne) ClearLastSeen() *AgentUpdateOne { + _u.mutation.ClearLastSeen() + return _u +} + +// SetLastActivityEvent sets the "last_activity_event" field. +func (_u *AgentUpdateOne) SetLastActivityEvent(v time.Time) *AgentUpdateOne { + _u.mutation.SetLastActivityEvent(v) + return _u +} + +// SetNillableLastActivityEvent sets the "last_activity_event" field if the given value is not nil. +func (_u *AgentUpdateOne) SetNillableLastActivityEvent(v *time.Time) *AgentUpdateOne { + if v != nil { + _u.SetLastActivityEvent(*v) + } + return _u +} + +// ClearLastActivityEvent clears the value of the "last_activity_event" field. +func (_u *AgentUpdateOne) ClearLastActivityEvent() *AgentUpdateOne { + _u.mutation.ClearLastActivityEvent() + return _u } -// SetCreatorID sets the "creator" edge to the User entity by ID. -func (_u *AgentUpdateOne) SetCreatorID(id uuid.UUID) *AgentUpdateOne { - _u.mutation.SetCreatorID(id) +// SetStartedAt sets the "started_at" field. +func (_u *AgentUpdateOne) SetStartedAt(v time.Time) *AgentUpdateOne { + _u.mutation.SetStartedAt(v) return _u } -// SetNillableCreatorID sets the "creator" edge to the User entity by ID if the given value is not nil. -func (_u *AgentUpdateOne) SetNillableCreatorID(id *uuid.UUID) *AgentUpdateOne { - if id != nil { - _u = _u.SetCreatorID(*id) +// SetNillableStartedAt sets the "started_at" field if the given value is not nil. +func (_u *AgentUpdateOne) SetNillableStartedAt(v *time.Time) *AgentUpdateOne { + if v != nil { + _u.SetStartedAt(*v) + } + return _u +} + +// ClearStartedAt clears the value of the "started_at" field. +func (_u *AgentUpdateOne) ClearStartedAt() *AgentUpdateOne { + _u.mutation.ClearStartedAt() + return _u +} + +// SetDeletedAt sets the "deleted_at" field. +func (_u *AgentUpdateOne) SetDeletedAt(v time.Time) *AgentUpdateOne { + _u.mutation.SetDeletedAt(v) + return _u +} + +// SetNillableDeletedAt sets the "deleted_at" field if the given value is not nil. +func (_u *AgentUpdateOne) SetNillableDeletedAt(v *time.Time) *AgentUpdateOne { + if v != nil { + _u.SetDeletedAt(*v) + } + return _u +} + +// ClearDeletedAt clears the value of the "deleted_at" field. +func (_u *AgentUpdateOne) ClearDeletedAt() *AgentUpdateOne { + _u.mutation.ClearDeletedAt() + return _u +} + +// SetStateVersion sets the "state_version" field. +func (_u *AgentUpdateOne) SetStateVersion(v int64) *AgentUpdateOne { + _u.mutation.ResetStateVersion() + _u.mutation.SetStateVersion(v) + return _u +} + +// SetNillableStateVersion sets the "state_version" field if the given value is not nil. +func (_u *AgentUpdateOne) SetNillableStateVersion(v *int64) *AgentUpdateOne { + if v != nil { + _u.SetStateVersion(*v) } return _u } -// SetCreator sets the "creator" edge to the User entity. -func (_u *AgentUpdateOne) SetCreator(v *User) *AgentUpdateOne { - return _u.SetCreatorID(v.ID) +// AddStateVersion adds value to the "state_version" field. +func (_u *AgentUpdateOne) AddStateVersion(v int64) *AgentUpdateOne { + _u.mutation.AddStateVersion(v) + return _u } -// SetOwner sets the "owner" edge to the User entity. -func (_u *AgentUpdateOne) SetOwner(v *User) *AgentUpdateOne { - return _u.SetOwnerID(v.ID) +// SetProject sets the "project" edge to the Project entity. +func (_u *AgentUpdateOne) SetProject(v *Project) *AgentUpdateOne { + return _u.SetProjectID(v.ID) } // AddMembershipIDs adds the "memberships" edge to the GroupMembership entity by IDs. @@ -819,18 +1808,6 @@ func (_u *AgentUpdateOne) ClearProject() *AgentUpdateOne { return _u } -// ClearCreator clears the "creator" edge to the User entity. -func (_u *AgentUpdateOne) ClearCreator() *AgentUpdateOne { - _u.mutation.ClearCreator() - return _u -} - -// ClearOwner clears the "owner" edge to the User entity. -func (_u *AgentUpdateOne) ClearOwner() *AgentUpdateOne { - _u.mutation.ClearOwner() - return _u -} - // ClearMemberships clears all "memberships" edges to the GroupMembership entity. func (_u *AgentUpdateOne) ClearMemberships() *AgentUpdateOne { _u.mutation.ClearMemberships() @@ -989,15 +1966,176 @@ func (_u *AgentUpdateOne) sqlSave(ctx context.Context) (_node *Agent, err error) if value, ok := _u.mutation.Status(); ok { _spec.SetField(agent.FieldStatus, field.TypeEnum, value) } + if value, ok := _u.mutation.CreatedBy(); ok { + _spec.SetField(agent.FieldCreatedBy, field.TypeUUID, value) + } + if _u.mutation.CreatedByCleared() { + _spec.ClearField(agent.FieldCreatedBy, field.TypeUUID) + } + if value, ok := _u.mutation.OwnerID(); ok { + _spec.SetField(agent.FieldOwnerID, field.TypeUUID, value) + } + if _u.mutation.OwnerIDCleared() { + _spec.ClearField(agent.FieldOwnerID, field.TypeUUID) + } if value, ok := _u.mutation.DelegationEnabled(); ok { _spec.SetField(agent.FieldDelegationEnabled, field.TypeBool, value) } if value, ok := _u.mutation.Visibility(); ok { _spec.SetField(agent.FieldVisibility, field.TypeString, value) } + if value, ok := _u.mutation.Labels(); ok { + _spec.SetField(agent.FieldLabels, field.TypeJSON, value) + } + if _u.mutation.LabelsCleared() { + _spec.ClearField(agent.FieldLabels, field.TypeJSON) + } + if value, ok := _u.mutation.Annotations(); ok { + _spec.SetField(agent.FieldAnnotations, field.TypeJSON, value) + } + if _u.mutation.AnnotationsCleared() { + _spec.ClearField(agent.FieldAnnotations, field.TypeJSON) + } + if value, ok := _u.mutation.Phase(); ok { + _spec.SetField(agent.FieldPhase, field.TypeString, value) + } + if _u.mutation.PhaseCleared() { + _spec.ClearField(agent.FieldPhase, field.TypeString) + } + if value, ok := _u.mutation.Activity(); ok { + _spec.SetField(agent.FieldActivity, field.TypeString, value) + } + if _u.mutation.ActivityCleared() { + _spec.ClearField(agent.FieldActivity, field.TypeString) + } + if value, ok := _u.mutation.ToolName(); ok { + _spec.SetField(agent.FieldToolName, field.TypeString, value) + } + if _u.mutation.ToolNameCleared() { + _spec.ClearField(agent.FieldToolName, field.TypeString) + } + if value, ok := _u.mutation.ConnectionState(); ok { + _spec.SetField(agent.FieldConnectionState, field.TypeString, value) + } + if _u.mutation.ConnectionStateCleared() { + _spec.ClearField(agent.FieldConnectionState, field.TypeString) + } + if value, ok := _u.mutation.ContainerStatus(); ok { + _spec.SetField(agent.FieldContainerStatus, field.TypeString, value) + } + if _u.mutation.ContainerStatusCleared() { + _spec.ClearField(agent.FieldContainerStatus, field.TypeString) + } + if value, ok := _u.mutation.RuntimeState(); ok { + _spec.SetField(agent.FieldRuntimeState, field.TypeString, value) + } + if _u.mutation.RuntimeStateCleared() { + _spec.ClearField(agent.FieldRuntimeState, field.TypeString) + } + if value, ok := _u.mutation.StalledFromActivity(); ok { + _spec.SetField(agent.FieldStalledFromActivity, field.TypeString, value) + } + if _u.mutation.StalledFromActivityCleared() { + _spec.ClearField(agent.FieldStalledFromActivity, field.TypeString) + } + if value, ok := _u.mutation.CurrentTurns(); ok { + _spec.SetField(agent.FieldCurrentTurns, field.TypeInt, value) + } + if value, ok := _u.mutation.AddedCurrentTurns(); ok { + _spec.AddField(agent.FieldCurrentTurns, field.TypeInt, value) + } + if value, ok := _u.mutation.CurrentModelCalls(); ok { + _spec.SetField(agent.FieldCurrentModelCalls, field.TypeInt, value) + } + if value, ok := _u.mutation.AddedCurrentModelCalls(); ok { + _spec.AddField(agent.FieldCurrentModelCalls, field.TypeInt, value) + } + if value, ok := _u.mutation.Image(); ok { + _spec.SetField(agent.FieldImage, field.TypeString, value) + } + if _u.mutation.ImageCleared() { + _spec.ClearField(agent.FieldImage, field.TypeString) + } + if value, ok := _u.mutation.Detached(); ok { + _spec.SetField(agent.FieldDetached, field.TypeBool, value) + } + if value, ok := _u.mutation.Runtime(); ok { + _spec.SetField(agent.FieldRuntime, field.TypeString, value) + } + if _u.mutation.RuntimeCleared() { + _spec.ClearField(agent.FieldRuntime, field.TypeString) + } + if value, ok := _u.mutation.RuntimeBrokerID(); ok { + _spec.SetField(agent.FieldRuntimeBrokerID, field.TypeString, value) + } + if _u.mutation.RuntimeBrokerIDCleared() { + _spec.ClearField(agent.FieldRuntimeBrokerID, field.TypeString) + } + if value, ok := _u.mutation.WebPtyEnabled(); ok { + _spec.SetField(agent.FieldWebPtyEnabled, field.TypeBool, value) + } + if value, ok := _u.mutation.TaskSummary(); ok { + _spec.SetField(agent.FieldTaskSummary, field.TypeString, value) + } + if _u.mutation.TaskSummaryCleared() { + _spec.ClearField(agent.FieldTaskSummary, field.TypeString) + } + if value, ok := _u.mutation.Message(); ok { + _spec.SetField(agent.FieldMessage, field.TypeString, value) + } + if _u.mutation.MessageCleared() { + _spec.ClearField(agent.FieldMessage, field.TypeString) + } + if value, ok := _u.mutation.AppliedConfig(); ok { + _spec.SetField(agent.FieldAppliedConfig, field.TypeString, value) + } + if _u.mutation.AppliedConfigCleared() { + _spec.ClearField(agent.FieldAppliedConfig, field.TypeString) + } + if value, ok := _u.mutation.Ancestry(); ok { + _spec.SetField(agent.FieldAncestry, field.TypeJSON, value) + } + if value, ok := _u.mutation.AppendedAncestry(); ok { + _spec.AddModifier(func(u *sql.UpdateBuilder) { + sqljson.Append(u, agent.FieldAncestry, value) + }) + } + if _u.mutation.AncestryCleared() { + _spec.ClearField(agent.FieldAncestry, field.TypeJSON) + } if value, ok := _u.mutation.Updated(); ok { _spec.SetField(agent.FieldUpdated, field.TypeTime, value) } + if value, ok := _u.mutation.LastSeen(); ok { + _spec.SetField(agent.FieldLastSeen, field.TypeTime, value) + } + if _u.mutation.LastSeenCleared() { + _spec.ClearField(agent.FieldLastSeen, field.TypeTime) + } + if value, ok := _u.mutation.LastActivityEvent(); ok { + _spec.SetField(agent.FieldLastActivityEvent, field.TypeTime, value) + } + if _u.mutation.LastActivityEventCleared() { + _spec.ClearField(agent.FieldLastActivityEvent, field.TypeTime) + } + if value, ok := _u.mutation.StartedAt(); ok { + _spec.SetField(agent.FieldStartedAt, field.TypeTime, value) + } + if _u.mutation.StartedAtCleared() { + _spec.ClearField(agent.FieldStartedAt, field.TypeTime) + } + if value, ok := _u.mutation.DeletedAt(); ok { + _spec.SetField(agent.FieldDeletedAt, field.TypeTime, value) + } + if _u.mutation.DeletedAtCleared() { + _spec.ClearField(agent.FieldDeletedAt, field.TypeTime) + } + if value, ok := _u.mutation.StateVersion(); ok { + _spec.SetField(agent.FieldStateVersion, field.TypeInt64, value) + } + if value, ok := _u.mutation.AddedStateVersion(); ok { + _spec.AddField(agent.FieldStateVersion, field.TypeInt64, value) + } if _u.mutation.ProjectCleared() { edge := &sqlgraph.EdgeSpec{ Rel: sqlgraph.M2O, @@ -1027,64 +2165,6 @@ func (_u *AgentUpdateOne) sqlSave(ctx context.Context) (_node *Agent, err error) } _spec.Edges.Add = append(_spec.Edges.Add, edge) } - if _u.mutation.CreatorCleared() { - edge := &sqlgraph.EdgeSpec{ - Rel: sqlgraph.M2O, - Inverse: true, - Table: agent.CreatorTable, - Columns: []string{agent.CreatorColumn}, - Bidi: false, - Target: &sqlgraph.EdgeTarget{ - IDSpec: sqlgraph.NewFieldSpec(user.FieldID, field.TypeUUID), - }, - } - _spec.Edges.Clear = append(_spec.Edges.Clear, edge) - } - if nodes := _u.mutation.CreatorIDs(); len(nodes) > 0 { - edge := &sqlgraph.EdgeSpec{ - Rel: sqlgraph.M2O, - Inverse: true, - Table: agent.CreatorTable, - Columns: []string{agent.CreatorColumn}, - Bidi: false, - Target: &sqlgraph.EdgeTarget{ - IDSpec: sqlgraph.NewFieldSpec(user.FieldID, field.TypeUUID), - }, - } - for _, k := range nodes { - edge.Target.Nodes = append(edge.Target.Nodes, k) - } - _spec.Edges.Add = append(_spec.Edges.Add, edge) - } - if _u.mutation.OwnerCleared() { - edge := &sqlgraph.EdgeSpec{ - Rel: sqlgraph.M2O, - Inverse: true, - Table: agent.OwnerTable, - Columns: []string{agent.OwnerColumn}, - Bidi: false, - Target: &sqlgraph.EdgeTarget{ - IDSpec: sqlgraph.NewFieldSpec(user.FieldID, field.TypeUUID), - }, - } - _spec.Edges.Clear = append(_spec.Edges.Clear, edge) - } - if nodes := _u.mutation.OwnerIDs(); len(nodes) > 0 { - edge := &sqlgraph.EdgeSpec{ - Rel: sqlgraph.M2O, - Inverse: true, - Table: agent.OwnerTable, - Columns: []string{agent.OwnerColumn}, - Bidi: false, - Target: &sqlgraph.EdgeTarget{ - IDSpec: sqlgraph.NewFieldSpec(user.FieldID, field.TypeUUID), - }, - } - for _, k := range nodes { - edge.Target.Nodes = append(edge.Target.Nodes, k) - } - _spec.Edges.Add = append(_spec.Edges.Add, edge) - } if _u.mutation.MembershipsCleared() { edge := &sqlgraph.EdgeSpec{ Rel: sqlgraph.O2M, diff --git a/pkg/ent/allowlistentry.go b/pkg/ent/allowlistentry.go new file mode 100644 index 000000000..1ff32f844 --- /dev/null +++ b/pkg/ent/allowlistentry.go @@ -0,0 +1,151 @@ +// Code generated by ent, DO NOT EDIT. + +package ent + +import ( + "fmt" + "strings" + "time" + + "entgo.io/ent" + "entgo.io/ent/dialect/sql" + "github.com/GoogleCloudPlatform/scion/pkg/ent/allowlistentry" + "github.com/google/uuid" +) + +// AllowListEntry is the model entity for the AllowListEntry schema. +type AllowListEntry struct { + config `json:"-"` + // ID of the ent. + ID uuid.UUID `json:"id,omitempty"` + // Email holds the value of the "email" field. + Email string `json:"email,omitempty"` + // Note holds the value of the "note" field. + Note string `json:"note,omitempty"` + // AddedBy holds the value of the "added_by" field. + AddedBy string `json:"added_by,omitempty"` + // InviteID holds the value of the "invite_id" field. + InviteID string `json:"invite_id,omitempty"` + // Created holds the value of the "created" field. + Created time.Time `json:"created,omitempty"` + selectValues sql.SelectValues +} + +// scanValues returns the types for scanning values from sql.Rows. +func (*AllowListEntry) scanValues(columns []string) ([]any, error) { + values := make([]any, len(columns)) + for i := range columns { + switch columns[i] { + case allowlistentry.FieldEmail, allowlistentry.FieldNote, allowlistentry.FieldAddedBy, allowlistentry.FieldInviteID: + values[i] = new(sql.NullString) + case allowlistentry.FieldCreated: + values[i] = new(sql.NullTime) + case allowlistentry.FieldID: + values[i] = new(uuid.UUID) + default: + values[i] = new(sql.UnknownType) + } + } + return values, nil +} + +// assignValues assigns the values that were returned from sql.Rows (after scanning) +// to the AllowListEntry fields. +func (_m *AllowListEntry) assignValues(columns []string, values []any) error { + if m, n := len(values), len(columns); m < n { + return fmt.Errorf("mismatch number of scan values: %d != %d", m, n) + } + for i := range columns { + switch columns[i] { + case allowlistentry.FieldID: + if value, ok := values[i].(*uuid.UUID); !ok { + return fmt.Errorf("unexpected type %T for field id", values[i]) + } else if value != nil { + _m.ID = *value + } + case allowlistentry.FieldEmail: + if value, ok := values[i].(*sql.NullString); !ok { + return fmt.Errorf("unexpected type %T for field email", values[i]) + } else if value.Valid { + _m.Email = value.String + } + case allowlistentry.FieldNote: + if value, ok := values[i].(*sql.NullString); !ok { + return fmt.Errorf("unexpected type %T for field note", values[i]) + } else if value.Valid { + _m.Note = value.String + } + case allowlistentry.FieldAddedBy: + if value, ok := values[i].(*sql.NullString); !ok { + return fmt.Errorf("unexpected type %T for field added_by", values[i]) + } else if value.Valid { + _m.AddedBy = value.String + } + case allowlistentry.FieldInviteID: + if value, ok := values[i].(*sql.NullString); !ok { + return fmt.Errorf("unexpected type %T for field invite_id", values[i]) + } else if value.Valid { + _m.InviteID = value.String + } + case allowlistentry.FieldCreated: + if value, ok := values[i].(*sql.NullTime); !ok { + return fmt.Errorf("unexpected type %T for field created", values[i]) + } else if value.Valid { + _m.Created = value.Time + } + default: + _m.selectValues.Set(columns[i], values[i]) + } + } + return nil +} + +// Value returns the ent.Value that was dynamically selected and assigned to the AllowListEntry. +// This includes values selected through modifiers, order, etc. +func (_m *AllowListEntry) Value(name string) (ent.Value, error) { + return _m.selectValues.Get(name) +} + +// Update returns a builder for updating this AllowListEntry. +// Note that you need to call AllowListEntry.Unwrap() before calling this method if this AllowListEntry +// was returned from a transaction, and the transaction was committed or rolled back. +func (_m *AllowListEntry) Update() *AllowListEntryUpdateOne { + return NewAllowListEntryClient(_m.config).UpdateOne(_m) +} + +// Unwrap unwraps the AllowListEntry entity that was returned from a transaction after it was closed, +// so that all future queries will be executed through the driver which created the transaction. +func (_m *AllowListEntry) Unwrap() *AllowListEntry { + _tx, ok := _m.config.driver.(*txDriver) + if !ok { + panic("ent: AllowListEntry is not a transactional entity") + } + _m.config.driver = _tx.drv + return _m +} + +// String implements the fmt.Stringer. +func (_m *AllowListEntry) String() string { + var builder strings.Builder + builder.WriteString("AllowListEntry(") + builder.WriteString(fmt.Sprintf("id=%v, ", _m.ID)) + builder.WriteString("email=") + builder.WriteString(_m.Email) + builder.WriteString(", ") + builder.WriteString("note=") + builder.WriteString(_m.Note) + builder.WriteString(", ") + builder.WriteString("added_by=") + builder.WriteString(_m.AddedBy) + builder.WriteString(", ") + builder.WriteString("invite_id=") + builder.WriteString(_m.InviteID) + builder.WriteString(", ") + builder.WriteString("created=") + builder.WriteString(_m.Created.Format(time.ANSIC)) + builder.WriteByte(')') + return builder.String() +} + +// AllowListEntries is a parsable slice of AllowListEntry. +type AllowListEntries []*AllowListEntry diff --git a/pkg/ent/allowlistentry/allowlistentry.go b/pkg/ent/allowlistentry/allowlistentry.go new file mode 100644 index 000000000..d62d41de5 --- /dev/null +++ b/pkg/ent/allowlistentry/allowlistentry.go @@ -0,0 +1,95 @@ +// Code generated by ent, DO NOT EDIT. + +package allowlistentry + +import ( + "time" + + "entgo.io/ent/dialect/sql" + "github.com/google/uuid" +) + +const ( + // Label holds the string label denoting the allowlistentry type in the database. + Label = "allow_list_entry" + // FieldID holds the string denoting the id field in the database. + FieldID = "id" + // FieldEmail holds the string denoting the email field in the database. + FieldEmail = "email" + // FieldNote holds the string denoting the note field in the database. + FieldNote = "note" + // FieldAddedBy holds the string denoting the added_by field in the database. + FieldAddedBy = "added_by" + // FieldInviteID holds the string denoting the invite_id field in the database. + FieldInviteID = "invite_id" + // FieldCreated holds the string denoting the created field in the database. + FieldCreated = "created" + // Table holds the table name of the allowlistentry in the database. + Table = "allow_list" +) + +// Columns holds all SQL columns for allowlistentry fields. +var Columns = []string{ + FieldID, + FieldEmail, + FieldNote, + FieldAddedBy, + FieldInviteID, + FieldCreated, +} + +// ValidColumn reports if the column name is valid (part of the table columns). +func ValidColumn(column string) bool { + for i := range Columns { + if column == Columns[i] { + return true + } + } + return false +} + +var ( + // EmailValidator is a validator for the "email" field. It is called by the builders before save. + EmailValidator func(string) error + // DefaultNote holds the default value on creation for the "note" field. + DefaultNote string + // AddedByValidator is a validator for the "added_by" field. It is called by the builders before save. + AddedByValidator func(string) error + // DefaultCreated holds the default value on creation for the "created" field. + DefaultCreated func() time.Time + // DefaultID holds the default value on creation for the "id" field. + DefaultID func() uuid.UUID +) + +// OrderOption defines the ordering options for the AllowListEntry queries. +type OrderOption func(*sql.Selector) + +// ByID orders the results by the id field. +func ByID(opts ...sql.OrderTermOption) OrderOption { + return sql.OrderByField(FieldID, opts...).ToFunc() +} + +// ByEmail orders the results by the email field. +func ByEmail(opts ...sql.OrderTermOption) OrderOption { + return sql.OrderByField(FieldEmail, opts...).ToFunc() +} + +// ByNote orders the results by the note field. +func ByNote(opts ...sql.OrderTermOption) OrderOption { + return sql.OrderByField(FieldNote, opts...).ToFunc() +} + +// ByAddedBy orders the results by the added_by field. +func ByAddedBy(opts ...sql.OrderTermOption) OrderOption { + return sql.OrderByField(FieldAddedBy, opts...).ToFunc() +} + +// ByInviteID orders the results by the invite_id field. +func ByInviteID(opts ...sql.OrderTermOption) OrderOption { + return sql.OrderByField(FieldInviteID, opts...).ToFunc() +} + +// ByCreated orders the results by the created field. +func ByCreated(opts ...sql.OrderTermOption) OrderOption { + return sql.OrderByField(FieldCreated, opts...).ToFunc() +} diff --git a/pkg/ent/allowlistentry/where.go b/pkg/ent/allowlistentry/where.go new file mode 100644 index 000000000..986373109 --- /dev/null +++ b/pkg/ent/allowlistentry/where.go @@ -0,0 +1,406 @@ +// Code generated by ent, DO NOT EDIT. + +package allowlistentry + +import ( + "time" + + "entgo.io/ent/dialect/sql" + "github.com/GoogleCloudPlatform/scion/pkg/ent/predicate" + "github.com/google/uuid" +) + +// ID filters vertices based on their ID field. +func ID(id uuid.UUID) predicate.AllowListEntry { + return predicate.AllowListEntry(sql.FieldEQ(FieldID, id)) +} + +// IDEQ applies the EQ predicate on the ID field. +func IDEQ(id uuid.UUID) predicate.AllowListEntry { + return predicate.AllowListEntry(sql.FieldEQ(FieldID, id)) +} + +// IDNEQ applies the NEQ predicate on the ID field. +func IDNEQ(id uuid.UUID) predicate.AllowListEntry { + return predicate.AllowListEntry(sql.FieldNEQ(FieldID, id)) +} + +// IDIn applies the In predicate on the ID field. +func IDIn(ids ...uuid.UUID) predicate.AllowListEntry { + return predicate.AllowListEntry(sql.FieldIn(FieldID, ids...)) +} + +// IDNotIn applies the NotIn predicate on the ID field. +func IDNotIn(ids ...uuid.UUID) predicate.AllowListEntry { + return predicate.AllowListEntry(sql.FieldNotIn(FieldID, ids...)) +} + +// IDGT applies the GT predicate on the ID field. +func IDGT(id uuid.UUID) predicate.AllowListEntry { + return predicate.AllowListEntry(sql.FieldGT(FieldID, id)) +} + +// IDGTE applies the GTE predicate on the ID field. +func IDGTE(id uuid.UUID) predicate.AllowListEntry { + return predicate.AllowListEntry(sql.FieldGTE(FieldID, id)) +} + +// IDLT applies the LT predicate on the ID field. +func IDLT(id uuid.UUID) predicate.AllowListEntry { + return predicate.AllowListEntry(sql.FieldLT(FieldID, id)) +} + +// IDLTE applies the LTE predicate on the ID field. +func IDLTE(id uuid.UUID) predicate.AllowListEntry { + return predicate.AllowListEntry(sql.FieldLTE(FieldID, id)) +} + +// Email applies equality check predicate on the "email" field. It's identical to EmailEQ. +func Email(v string) predicate.AllowListEntry { + return predicate.AllowListEntry(sql.FieldEQ(FieldEmail, v)) +} + +// Note applies equality check predicate on the "note" field. It's identical to NoteEQ. +func Note(v string) predicate.AllowListEntry { + return predicate.AllowListEntry(sql.FieldEQ(FieldNote, v)) +} + +// AddedBy applies equality check predicate on the "added_by" field. It's identical to AddedByEQ. +func AddedBy(v string) predicate.AllowListEntry { + return predicate.AllowListEntry(sql.FieldEQ(FieldAddedBy, v)) +} + +// InviteID applies equality check predicate on the "invite_id" field. It's identical to InviteIDEQ. +func InviteID(v string) predicate.AllowListEntry { + return predicate.AllowListEntry(sql.FieldEQ(FieldInviteID, v)) +} + +// Created applies equality check predicate on the "created" field. It's identical to CreatedEQ. +func Created(v time.Time) predicate.AllowListEntry { + return predicate.AllowListEntry(sql.FieldEQ(FieldCreated, v)) +} + +// EmailEQ applies the EQ predicate on the "email" field. +func EmailEQ(v string) predicate.AllowListEntry { + return predicate.AllowListEntry(sql.FieldEQ(FieldEmail, v)) +} + +// EmailNEQ applies the NEQ predicate on the "email" field. +func EmailNEQ(v string) predicate.AllowListEntry { + return predicate.AllowListEntry(sql.FieldNEQ(FieldEmail, v)) +} + +// EmailIn applies the In predicate on the "email" field. +func EmailIn(vs ...string) predicate.AllowListEntry { + return predicate.AllowListEntry(sql.FieldIn(FieldEmail, vs...)) +} + +// EmailNotIn applies the NotIn predicate on the "email" field. +func EmailNotIn(vs ...string) predicate.AllowListEntry { + return predicate.AllowListEntry(sql.FieldNotIn(FieldEmail, vs...)) +} + +// EmailGT applies the GT predicate on the "email" field. +func EmailGT(v string) predicate.AllowListEntry { + return predicate.AllowListEntry(sql.FieldGT(FieldEmail, v)) +} + +// EmailGTE applies the GTE predicate on the "email" field. +func EmailGTE(v string) predicate.AllowListEntry { + return predicate.AllowListEntry(sql.FieldGTE(FieldEmail, v)) +} + +// EmailLT applies the LT predicate on the "email" field. +func EmailLT(v string) predicate.AllowListEntry { + return predicate.AllowListEntry(sql.FieldLT(FieldEmail, v)) +} + +// EmailLTE applies the LTE predicate on the "email" field. +func EmailLTE(v string) predicate.AllowListEntry { + return predicate.AllowListEntry(sql.FieldLTE(FieldEmail, v)) +} + +// EmailContains applies the Contains predicate on the "email" field. +func EmailContains(v string) predicate.AllowListEntry { + return predicate.AllowListEntry(sql.FieldContains(FieldEmail, v)) +} + +// EmailHasPrefix applies the HasPrefix predicate on the "email" field. +func EmailHasPrefix(v string) predicate.AllowListEntry { + return predicate.AllowListEntry(sql.FieldHasPrefix(FieldEmail, v)) +} + +// EmailHasSuffix applies the HasSuffix predicate on the "email" field. +func EmailHasSuffix(v string) predicate.AllowListEntry { + return predicate.AllowListEntry(sql.FieldHasSuffix(FieldEmail, v)) +} + +// EmailEqualFold applies the EqualFold predicate on the "email" field. +func EmailEqualFold(v string) predicate.AllowListEntry { + return predicate.AllowListEntry(sql.FieldEqualFold(FieldEmail, v)) +} + +// EmailContainsFold applies the ContainsFold predicate on the "email" field. +func EmailContainsFold(v string) predicate.AllowListEntry { + return predicate.AllowListEntry(sql.FieldContainsFold(FieldEmail, v)) +} + +// NoteEQ applies the EQ predicate on the "note" field. +func NoteEQ(v string) predicate.AllowListEntry { + return predicate.AllowListEntry(sql.FieldEQ(FieldNote, v)) +} + +// NoteNEQ applies the NEQ predicate on the "note" field. +func NoteNEQ(v string) predicate.AllowListEntry { + return predicate.AllowListEntry(sql.FieldNEQ(FieldNote, v)) +} + +// NoteIn applies the In predicate on the "note" field. +func NoteIn(vs ...string) predicate.AllowListEntry { + return predicate.AllowListEntry(sql.FieldIn(FieldNote, vs...)) +} + +// NoteNotIn applies the NotIn predicate on the "note" field. +func NoteNotIn(vs ...string) predicate.AllowListEntry { + return predicate.AllowListEntry(sql.FieldNotIn(FieldNote, vs...)) +} + +// NoteGT applies the GT predicate on the "note" field. +func NoteGT(v string) predicate.AllowListEntry { + return predicate.AllowListEntry(sql.FieldGT(FieldNote, v)) +} + +// NoteGTE applies the GTE predicate on the "note" field. +func NoteGTE(v string) predicate.AllowListEntry { + return predicate.AllowListEntry(sql.FieldGTE(FieldNote, v)) +} + +// NoteLT applies the LT predicate on the "note" field. +func NoteLT(v string) predicate.AllowListEntry { + return predicate.AllowListEntry(sql.FieldLT(FieldNote, v)) +} + +// NoteLTE applies the LTE predicate on the "note" field. +func NoteLTE(v string) predicate.AllowListEntry { + return predicate.AllowListEntry(sql.FieldLTE(FieldNote, v)) +} + +// NoteContains applies the Contains predicate on the "note" field. +func NoteContains(v string) predicate.AllowListEntry { + return predicate.AllowListEntry(sql.FieldContains(FieldNote, v)) +} + +// NoteHasPrefix applies the HasPrefix predicate on the "note" field. +func NoteHasPrefix(v string) predicate.AllowListEntry { + return predicate.AllowListEntry(sql.FieldHasPrefix(FieldNote, v)) +} + +// NoteHasSuffix applies the HasSuffix predicate on the "note" field. +func NoteHasSuffix(v string) predicate.AllowListEntry { + return predicate.AllowListEntry(sql.FieldHasSuffix(FieldNote, v)) +} + +// NoteEqualFold applies the EqualFold predicate on the "note" field. +func NoteEqualFold(v string) predicate.AllowListEntry { + return predicate.AllowListEntry(sql.FieldEqualFold(FieldNote, v)) +} + +// NoteContainsFold applies the ContainsFold predicate on the "note" field. +func NoteContainsFold(v string) predicate.AllowListEntry { + return predicate.AllowListEntry(sql.FieldContainsFold(FieldNote, v)) +} + +// AddedByEQ applies the EQ predicate on the "added_by" field. +func AddedByEQ(v string) predicate.AllowListEntry { + return predicate.AllowListEntry(sql.FieldEQ(FieldAddedBy, v)) +} + +// AddedByNEQ applies the NEQ predicate on the "added_by" field. +func AddedByNEQ(v string) predicate.AllowListEntry { + return predicate.AllowListEntry(sql.FieldNEQ(FieldAddedBy, v)) +} + +// AddedByIn applies the In predicate on the "added_by" field. +func AddedByIn(vs ...string) predicate.AllowListEntry { + return predicate.AllowListEntry(sql.FieldIn(FieldAddedBy, vs...)) +} + +// AddedByNotIn applies the NotIn predicate on the "added_by" field. +func AddedByNotIn(vs ...string) predicate.AllowListEntry { + return predicate.AllowListEntry(sql.FieldNotIn(FieldAddedBy, vs...)) +} + +// AddedByGT applies the GT predicate on the "added_by" field. +func AddedByGT(v string) predicate.AllowListEntry { + return predicate.AllowListEntry(sql.FieldGT(FieldAddedBy, v)) +} + +// AddedByGTE applies the GTE predicate on the "added_by" field. +func AddedByGTE(v string) predicate.AllowListEntry { + return predicate.AllowListEntry(sql.FieldGTE(FieldAddedBy, v)) +} + +// AddedByLT applies the LT predicate on the "added_by" field. +func AddedByLT(v string) predicate.AllowListEntry { + return predicate.AllowListEntry(sql.FieldLT(FieldAddedBy, v)) +} + +// AddedByLTE applies the LTE predicate on the "added_by" field. +func AddedByLTE(v string) predicate.AllowListEntry { + return predicate.AllowListEntry(sql.FieldLTE(FieldAddedBy, v)) +} + +// AddedByContains applies the Contains predicate on the "added_by" field. +func AddedByContains(v string) predicate.AllowListEntry { + return predicate.AllowListEntry(sql.FieldContains(FieldAddedBy, v)) +} + +// AddedByHasPrefix applies the HasPrefix predicate on the "added_by" field. +func AddedByHasPrefix(v string) predicate.AllowListEntry { + return predicate.AllowListEntry(sql.FieldHasPrefix(FieldAddedBy, v)) +} + +// AddedByHasSuffix applies the HasSuffix predicate on the "added_by" field. +func AddedByHasSuffix(v string) predicate.AllowListEntry { + return predicate.AllowListEntry(sql.FieldHasSuffix(FieldAddedBy, v)) +} + +// AddedByEqualFold applies the EqualFold predicate on the "added_by" field. +func AddedByEqualFold(v string) predicate.AllowListEntry { + return predicate.AllowListEntry(sql.FieldEqualFold(FieldAddedBy, v)) +} + +// AddedByContainsFold applies the ContainsFold predicate on the "added_by" field. +func AddedByContainsFold(v string) predicate.AllowListEntry { + return predicate.AllowListEntry(sql.FieldContainsFold(FieldAddedBy, v)) +} + +// InviteIDEQ applies the EQ predicate on the "invite_id" field. +func InviteIDEQ(v string) predicate.AllowListEntry { + return predicate.AllowListEntry(sql.FieldEQ(FieldInviteID, v)) +} + +// InviteIDNEQ applies the NEQ predicate on the "invite_id" field. +func InviteIDNEQ(v string) predicate.AllowListEntry { + return predicate.AllowListEntry(sql.FieldNEQ(FieldInviteID, v)) +} + +// InviteIDIn applies the In predicate on the "invite_id" field. +func InviteIDIn(vs ...string) predicate.AllowListEntry { + return predicate.AllowListEntry(sql.FieldIn(FieldInviteID, vs...)) +} + +// InviteIDNotIn applies the NotIn predicate on the "invite_id" field. +func InviteIDNotIn(vs ...string) predicate.AllowListEntry { + return predicate.AllowListEntry(sql.FieldNotIn(FieldInviteID, vs...)) +} + +// InviteIDGT applies the GT predicate on the "invite_id" field. +func InviteIDGT(v string) predicate.AllowListEntry { + return predicate.AllowListEntry(sql.FieldGT(FieldInviteID, v)) +} + +// InviteIDGTE applies the GTE predicate on the "invite_id" field. +func InviteIDGTE(v string) predicate.AllowListEntry { + return predicate.AllowListEntry(sql.FieldGTE(FieldInviteID, v)) +} + +// InviteIDLT applies the LT predicate on the "invite_id" field. +func InviteIDLT(v string) predicate.AllowListEntry { + return predicate.AllowListEntry(sql.FieldLT(FieldInviteID, v)) +} + +// InviteIDLTE applies the LTE predicate on the "invite_id" field. +func InviteIDLTE(v string) predicate.AllowListEntry { + return predicate.AllowListEntry(sql.FieldLTE(FieldInviteID, v)) +} + +// InviteIDContains applies the Contains predicate on the "invite_id" field. +func InviteIDContains(v string) predicate.AllowListEntry { + return predicate.AllowListEntry(sql.FieldContains(FieldInviteID, v)) +} + +// InviteIDHasPrefix applies the HasPrefix predicate on the "invite_id" field. +func InviteIDHasPrefix(v string) predicate.AllowListEntry { + return predicate.AllowListEntry(sql.FieldHasPrefix(FieldInviteID, v)) +} + +// InviteIDHasSuffix applies the HasSuffix predicate on the "invite_id" field. +func InviteIDHasSuffix(v string) predicate.AllowListEntry { + return predicate.AllowListEntry(sql.FieldHasSuffix(FieldInviteID, v)) +} + +// InviteIDIsNil applies the IsNil predicate on the "invite_id" field. +func InviteIDIsNil() predicate.AllowListEntry { + return predicate.AllowListEntry(sql.FieldIsNull(FieldInviteID)) +} + +// InviteIDNotNil applies the NotNil predicate on the "invite_id" field. +func InviteIDNotNil() predicate.AllowListEntry { + return predicate.AllowListEntry(sql.FieldNotNull(FieldInviteID)) +} + +// InviteIDEqualFold applies the EqualFold predicate on the "invite_id" field. +func InviteIDEqualFold(v string) predicate.AllowListEntry { + return predicate.AllowListEntry(sql.FieldEqualFold(FieldInviteID, v)) +} + +// InviteIDContainsFold applies the ContainsFold predicate on the "invite_id" field. +func InviteIDContainsFold(v string) predicate.AllowListEntry { + return predicate.AllowListEntry(sql.FieldContainsFold(FieldInviteID, v)) +} + +// CreatedEQ applies the EQ predicate on the "created" field. +func CreatedEQ(v time.Time) predicate.AllowListEntry { + return predicate.AllowListEntry(sql.FieldEQ(FieldCreated, v)) +} + +// CreatedNEQ applies the NEQ predicate on the "created" field. +func CreatedNEQ(v time.Time) predicate.AllowListEntry { + return predicate.AllowListEntry(sql.FieldNEQ(FieldCreated, v)) +} + +// CreatedIn applies the In predicate on the "created" field. +func CreatedIn(vs ...time.Time) predicate.AllowListEntry { + return predicate.AllowListEntry(sql.FieldIn(FieldCreated, vs...)) +} + +// CreatedNotIn applies the NotIn predicate on the "created" field. +func CreatedNotIn(vs ...time.Time) predicate.AllowListEntry { + return predicate.AllowListEntry(sql.FieldNotIn(FieldCreated, vs...)) +} + +// CreatedGT applies the GT predicate on the "created" field. +func CreatedGT(v time.Time) predicate.AllowListEntry { + return predicate.AllowListEntry(sql.FieldGT(FieldCreated, v)) +} + +// CreatedGTE applies the GTE predicate on the "created" field. +func CreatedGTE(v time.Time) predicate.AllowListEntry { + return predicate.AllowListEntry(sql.FieldGTE(FieldCreated, v)) +} + +// CreatedLT applies the LT predicate on the "created" field. +func CreatedLT(v time.Time) predicate.AllowListEntry { + return predicate.AllowListEntry(sql.FieldLT(FieldCreated, v)) +} + +// CreatedLTE applies the LTE predicate on the "created" field. +func CreatedLTE(v time.Time) predicate.AllowListEntry { + return predicate.AllowListEntry(sql.FieldLTE(FieldCreated, v)) +} + +// And groups predicates with the AND operator between them. +func And(predicates ...predicate.AllowListEntry) predicate.AllowListEntry { + return predicate.AllowListEntry(sql.AndPredicates(predicates...)) +} + +// Or groups predicates with the OR operator between them. +func Or(predicates ...predicate.AllowListEntry) predicate.AllowListEntry { + return predicate.AllowListEntry(sql.OrPredicates(predicates...)) +} + +// Not applies the not operator on the given predicate. +func Not(p predicate.AllowListEntry) predicate.AllowListEntry { + return predicate.AllowListEntry(sql.NotPredicates(p)) +} diff --git a/pkg/ent/allowlistentry_create.go b/pkg/ent/allowlistentry_create.go new file mode 100644 index 000000000..a368b6d9a --- /dev/null +++ b/pkg/ent/allowlistentry_create.go @@ -0,0 +1,746 @@ +// Code generated by ent, DO NOT EDIT. + +package ent + +import ( + "context" + "errors" + "fmt" + "time" + + "entgo.io/ent/dialect" + "entgo.io/ent/dialect/sql" + "entgo.io/ent/dialect/sql/sqlgraph" + "entgo.io/ent/schema/field" + "github.com/GoogleCloudPlatform/scion/pkg/ent/allowlistentry" + "github.com/google/uuid" +) + +// AllowListEntryCreate is the builder for creating a AllowListEntry entity. +type AllowListEntryCreate struct { + config + mutation *AllowListEntryMutation + hooks []Hook + conflict []sql.ConflictOption +} + +// SetEmail sets the "email" field. +func (_c *AllowListEntryCreate) SetEmail(v string) *AllowListEntryCreate { + _c.mutation.SetEmail(v) + return _c +} + +// SetNote sets the "note" field. +func (_c *AllowListEntryCreate) SetNote(v string) *AllowListEntryCreate { + _c.mutation.SetNote(v) + return _c +} + +// SetNillableNote sets the "note" field if the given value is not nil. +func (_c *AllowListEntryCreate) SetNillableNote(v *string) *AllowListEntryCreate { + if v != nil { + _c.SetNote(*v) + } + return _c +} + +// SetAddedBy sets the "added_by" field. +func (_c *AllowListEntryCreate) SetAddedBy(v string) *AllowListEntryCreate { + _c.mutation.SetAddedBy(v) + return _c +} + +// SetInviteID sets the "invite_id" field. +func (_c *AllowListEntryCreate) SetInviteID(v string) *AllowListEntryCreate { + _c.mutation.SetInviteID(v) + return _c +} + +// SetNillableInviteID sets the "invite_id" field if the given value is not nil. +func (_c *AllowListEntryCreate) SetNillableInviteID(v *string) *AllowListEntryCreate { + if v != nil { + _c.SetInviteID(*v) + } + return _c +} + +// SetCreated sets the "created" field. +func (_c *AllowListEntryCreate) SetCreated(v time.Time) *AllowListEntryCreate { + _c.mutation.SetCreated(v) + return _c +} + +// SetNillableCreated sets the "created" field if the given value is not nil. +func (_c *AllowListEntryCreate) SetNillableCreated(v *time.Time) *AllowListEntryCreate { + if v != nil { + _c.SetCreated(*v) + } + return _c +} + +// SetID sets the "id" field. +func (_c *AllowListEntryCreate) SetID(v uuid.UUID) *AllowListEntryCreate { + _c.mutation.SetID(v) + return _c +} + +// SetNillableID sets the "id" field if the given value is not nil. +func (_c *AllowListEntryCreate) SetNillableID(v *uuid.UUID) *AllowListEntryCreate { + if v != nil { + _c.SetID(*v) + } + return _c +} + +// Mutation returns the AllowListEntryMutation object of the builder. +func (_c *AllowListEntryCreate) Mutation() *AllowListEntryMutation { + return _c.mutation +} + +// Save creates the AllowListEntry in the database. +func (_c *AllowListEntryCreate) Save(ctx context.Context) (*AllowListEntry, error) { + _c.defaults() + return withHooks(ctx, _c.sqlSave, _c.mutation, _c.hooks) +} + +// SaveX calls Save and panics if Save returns an error. +func (_c *AllowListEntryCreate) SaveX(ctx context.Context) *AllowListEntry { + v, err := _c.Save(ctx) + if err != nil { + panic(err) + } + return v +} + +// Exec executes the query. +func (_c *AllowListEntryCreate) Exec(ctx context.Context) error { + _, err := _c.Save(ctx) + return err +} + +// ExecX is like Exec, but panics if an error occurs. +func (_c *AllowListEntryCreate) ExecX(ctx context.Context) { + if err := _c.Exec(ctx); err != nil { + panic(err) + } +} + +// defaults sets the default values of the builder before save. +func (_c *AllowListEntryCreate) defaults() { + if _, ok := _c.mutation.Note(); !ok { + v := allowlistentry.DefaultNote + _c.mutation.SetNote(v) + } + if _, ok := _c.mutation.Created(); !ok { + v := allowlistentry.DefaultCreated() + _c.mutation.SetCreated(v) + } + if _, ok := _c.mutation.ID(); !ok { + v := allowlistentry.DefaultID() + _c.mutation.SetID(v) + } +} + +// check runs all checks and user-defined validators on the builder. +func (_c *AllowListEntryCreate) check() error { + if _, ok := _c.mutation.Email(); !ok { + return &ValidationError{Name: "email", err: errors.New(`ent: missing required field "AllowListEntry.email"`)} + } + if v, ok := _c.mutation.Email(); ok { + if err := allowlistentry.EmailValidator(v); err != nil { + return &ValidationError{Name: "email", err: fmt.Errorf(`ent: validator failed for field "AllowListEntry.email": %w`, err)} + } + } + if _, ok := _c.mutation.Note(); !ok { + return &ValidationError{Name: "note", err: errors.New(`ent: missing required field "AllowListEntry.note"`)} + } + if _, ok := _c.mutation.AddedBy(); !ok { + return &ValidationError{Name: "added_by", err: errors.New(`ent: missing required field "AllowListEntry.added_by"`)} + } + if v, ok := _c.mutation.AddedBy(); ok { + if err := allowlistentry.AddedByValidator(v); err != nil { + return &ValidationError{Name: "added_by", err: fmt.Errorf(`ent: validator failed for field "AllowListEntry.added_by": %w`, err)} + } + } + if _, ok := _c.mutation.Created(); !ok { + return &ValidationError{Name: "created", err: errors.New(`ent: missing required field "AllowListEntry.created"`)} + } + return nil +} + +func (_c *AllowListEntryCreate) sqlSave(ctx context.Context) (*AllowListEntry, error) { + if err := _c.check(); err != nil { + return nil, err + } + _node, _spec := _c.createSpec() + if err := sqlgraph.CreateNode(ctx, _c.driver, _spec); err != nil { + if sqlgraph.IsConstraintError(err) { + err = &ConstraintError{msg: err.Error(), wrap: err} + } + return nil, err + } + if _spec.ID.Value != nil { + if id, ok := _spec.ID.Value.(*uuid.UUID); ok { + _node.ID = *id + } else if err := _node.ID.Scan(_spec.ID.Value); err != nil { + return nil, err + } + } + _c.mutation.id = &_node.ID + _c.mutation.done = true + return _node, nil +} + +func (_c *AllowListEntryCreate) createSpec() (*AllowListEntry, *sqlgraph.CreateSpec) { + var ( + _node = &AllowListEntry{config: _c.config} + _spec = sqlgraph.NewCreateSpec(allowlistentry.Table, sqlgraph.NewFieldSpec(allowlistentry.FieldID, field.TypeUUID)) + ) + _spec.OnConflict = _c.conflict + if id, ok := _c.mutation.ID(); ok { + _node.ID = id + _spec.ID.Value = &id + } + if value, ok := _c.mutation.Email(); ok { + _spec.SetField(allowlistentry.FieldEmail, field.TypeString, value) + _node.Email = value + } + if value, ok := _c.mutation.Note(); ok { + _spec.SetField(allowlistentry.FieldNote, field.TypeString, value) + _node.Note = value + } + if value, ok := _c.mutation.AddedBy(); ok { + _spec.SetField(allowlistentry.FieldAddedBy, field.TypeString, value) + _node.AddedBy = value + } + if value, ok := _c.mutation.InviteID(); ok { + _spec.SetField(allowlistentry.FieldInviteID, field.TypeString, value) + _node.InviteID = value + } + if value, ok := _c.mutation.Created(); ok { + _spec.SetField(allowlistentry.FieldCreated, field.TypeTime, value) + _node.Created = value + } + return _node, _spec +} + +// OnConflict allows configuring the `ON CONFLICT` / `ON DUPLICATE KEY` clause +// of the `INSERT` statement. For example: +// +// client.AllowListEntry.Create(). +// SetEmail(v). +// OnConflict( +// // Update the row with the new values +// // the was proposed for insertion. +// sql.ResolveWithNewValues(), +// ). +// // Override some of the fields with custom +// // update values. +// Update(func(u *ent.AllowListEntryUpsert) { +// SetEmail(v+v). +// }). +// Exec(ctx) +func (_c *AllowListEntryCreate) OnConflict(opts ...sql.ConflictOption) *AllowListEntryUpsertOne { + _c.conflict = opts + return &AllowListEntryUpsertOne{ + create: _c, + } +} + +// OnConflictColumns calls `OnConflict` and configures the columns +// as conflict target. Using this option is equivalent to using: +// +// client.AllowListEntry.Create(). +// OnConflict(sql.ConflictColumns(columns...)). +// Exec(ctx) +func (_c *AllowListEntryCreate) OnConflictColumns(columns ...string) *AllowListEntryUpsertOne { + _c.conflict = append(_c.conflict, sql.ConflictColumns(columns...)) + return &AllowListEntryUpsertOne{ + create: _c, + } +} + +type ( + // AllowListEntryUpsertOne is the builder for "upsert"-ing + // one AllowListEntry node. + AllowListEntryUpsertOne struct { + create *AllowListEntryCreate + } + + // AllowListEntryUpsert is the "OnConflict" setter. + AllowListEntryUpsert struct { + *sql.UpdateSet + } +) + +// SetEmail sets the "email" field. +func (u *AllowListEntryUpsert) SetEmail(v string) *AllowListEntryUpsert { + u.Set(allowlistentry.FieldEmail, v) + return u +} + +// UpdateEmail sets the "email" field to the value that was provided on create. +func (u *AllowListEntryUpsert) UpdateEmail() *AllowListEntryUpsert { + u.SetExcluded(allowlistentry.FieldEmail) + return u +} + +// SetNote sets the "note" field. +func (u *AllowListEntryUpsert) SetNote(v string) *AllowListEntryUpsert { + u.Set(allowlistentry.FieldNote, v) + return u +} + +// UpdateNote sets the "note" field to the value that was provided on create. +func (u *AllowListEntryUpsert) UpdateNote() *AllowListEntryUpsert { + u.SetExcluded(allowlistentry.FieldNote) + return u +} + +// SetAddedBy sets the "added_by" field. +func (u *AllowListEntryUpsert) SetAddedBy(v string) *AllowListEntryUpsert { + u.Set(allowlistentry.FieldAddedBy, v) + return u +} + +// UpdateAddedBy sets the "added_by" field to the value that was provided on create. +func (u *AllowListEntryUpsert) UpdateAddedBy() *AllowListEntryUpsert { + u.SetExcluded(allowlistentry.FieldAddedBy) + return u +} + +// SetInviteID sets the "invite_id" field. +func (u *AllowListEntryUpsert) SetInviteID(v string) *AllowListEntryUpsert { + u.Set(allowlistentry.FieldInviteID, v) + return u +} + +// UpdateInviteID sets the "invite_id" field to the value that was provided on create. +func (u *AllowListEntryUpsert) UpdateInviteID() *AllowListEntryUpsert { + u.SetExcluded(allowlistentry.FieldInviteID) + return u +} + +// ClearInviteID clears the value of the "invite_id" field. +func (u *AllowListEntryUpsert) ClearInviteID() *AllowListEntryUpsert { + u.SetNull(allowlistentry.FieldInviteID) + return u +} + +// UpdateNewValues updates the mutable fields using the new values that were set on create except the ID field. +// Using this option is equivalent to using: +// +// client.AllowListEntry.Create(). +// OnConflict( +// sql.ResolveWithNewValues(), +// sql.ResolveWith(func(u *sql.UpdateSet) { +// u.SetIgnore(allowlistentry.FieldID) +// }), +// ). +// Exec(ctx) +func (u *AllowListEntryUpsertOne) UpdateNewValues() *AllowListEntryUpsertOne { + u.create.conflict = append(u.create.conflict, sql.ResolveWithNewValues()) + u.create.conflict = append(u.create.conflict, sql.ResolveWith(func(s *sql.UpdateSet) { + if _, exists := u.create.mutation.ID(); exists { + s.SetIgnore(allowlistentry.FieldID) + } + if _, exists := u.create.mutation.Created(); exists { + s.SetIgnore(allowlistentry.FieldCreated) + } + })) + return u +} + +// Ignore sets each column to itself in case of conflict. +// Using this option is equivalent to using: +// +// client.AllowListEntry.Create(). +// OnConflict(sql.ResolveWithIgnore()). +// Exec(ctx) +func (u *AllowListEntryUpsertOne) Ignore() *AllowListEntryUpsertOne { + u.create.conflict = append(u.create.conflict, sql.ResolveWithIgnore()) + return u +} + +// DoNothing configures the conflict_action to `DO NOTHING`. +// Supported only by SQLite and PostgreSQL. +func (u *AllowListEntryUpsertOne) DoNothing() *AllowListEntryUpsertOne { + u.create.conflict = append(u.create.conflict, sql.DoNothing()) + return u +} + +// Update allows overriding fields `UPDATE` values. See the AllowListEntryCreate.OnConflict +// documentation for more info. +func (u *AllowListEntryUpsertOne) Update(set func(*AllowListEntryUpsert)) *AllowListEntryUpsertOne { + u.create.conflict = append(u.create.conflict, sql.ResolveWith(func(update *sql.UpdateSet) { + set(&AllowListEntryUpsert{UpdateSet: update}) + })) + return u +} + +// SetEmail sets the "email" field. +func (u *AllowListEntryUpsertOne) SetEmail(v string) *AllowListEntryUpsertOne { + return u.Update(func(s *AllowListEntryUpsert) { + s.SetEmail(v) + }) +} + +// UpdateEmail sets the "email" field to the value that was provided on create. +func (u *AllowListEntryUpsertOne) UpdateEmail() *AllowListEntryUpsertOne { + return u.Update(func(s *AllowListEntryUpsert) { + s.UpdateEmail() + }) +} + +// SetNote sets the "note" field. +func (u *AllowListEntryUpsertOne) SetNote(v string) *AllowListEntryUpsertOne { + return u.Update(func(s *AllowListEntryUpsert) { + s.SetNote(v) + }) +} + +// UpdateNote sets the "note" field to the value that was provided on create. +func (u *AllowListEntryUpsertOne) UpdateNote() *AllowListEntryUpsertOne { + return u.Update(func(s *AllowListEntryUpsert) { + s.UpdateNote() + }) +} + +// SetAddedBy sets the "added_by" field. +func (u *AllowListEntryUpsertOne) SetAddedBy(v string) *AllowListEntryUpsertOne { + return u.Update(func(s *AllowListEntryUpsert) { + s.SetAddedBy(v) + }) +} + +// UpdateAddedBy sets the "added_by" field to the value that was provided on create. +func (u *AllowListEntryUpsertOne) UpdateAddedBy() *AllowListEntryUpsertOne { + return u.Update(func(s *AllowListEntryUpsert) { + s.UpdateAddedBy() + }) +} + +// SetInviteID sets the "invite_id" field. +func (u *AllowListEntryUpsertOne) SetInviteID(v string) *AllowListEntryUpsertOne { + return u.Update(func(s *AllowListEntryUpsert) { + s.SetInviteID(v) + }) +} + +// UpdateInviteID sets the "invite_id" field to the value that was provided on create. +func (u *AllowListEntryUpsertOne) UpdateInviteID() *AllowListEntryUpsertOne { + return u.Update(func(s *AllowListEntryUpsert) { + s.UpdateInviteID() + }) +} + +// ClearInviteID clears the value of the "invite_id" field. +func (u *AllowListEntryUpsertOne) ClearInviteID() *AllowListEntryUpsertOne { + return u.Update(func(s *AllowListEntryUpsert) { + s.ClearInviteID() + }) +} + +// Exec executes the query. +func (u *AllowListEntryUpsertOne) Exec(ctx context.Context) error { + if len(u.create.conflict) == 0 { + return errors.New("ent: missing options for AllowListEntryCreate.OnConflict") + } + return u.create.Exec(ctx) +} + +// ExecX is like Exec, but panics if an error occurs. +func (u *AllowListEntryUpsertOne) ExecX(ctx context.Context) { + if err := u.create.Exec(ctx); err != nil { + panic(err) + } +} + +// Exec executes the UPSERT query and returns the inserted/updated ID. +func (u *AllowListEntryUpsertOne) ID(ctx context.Context) (id uuid.UUID, err error) { + if u.create.driver.Dialect() == dialect.MySQL { + // In case of "ON CONFLICT", there is no way to get back non-numeric ID + // fields from the database since MySQL does not support the RETURNING clause. + return id, errors.New("ent: AllowListEntryUpsertOne.ID is not supported by MySQL driver. Use AllowListEntryUpsertOne.Exec instead") + } + node, err := u.create.Save(ctx) + if err != nil { + return id, err + } + return node.ID, nil +} + +// IDX is like ID, but panics if an error occurs. +func (u *AllowListEntryUpsertOne) IDX(ctx context.Context) uuid.UUID { + id, err := u.ID(ctx) + if err != nil { + panic(err) + } + return id +} + +// AllowListEntryCreateBulk is the builder for creating many AllowListEntry entities in bulk. +type AllowListEntryCreateBulk struct { + config + err error + builders []*AllowListEntryCreate + conflict []sql.ConflictOption +} + +// Save creates the AllowListEntry entities in the database. +func (_c *AllowListEntryCreateBulk) Save(ctx context.Context) ([]*AllowListEntry, error) { + if _c.err != nil { + return nil, _c.err + } + specs := make([]*sqlgraph.CreateSpec, len(_c.builders)) + nodes := make([]*AllowListEntry, len(_c.builders)) + mutators := make([]Mutator, len(_c.builders)) + for i := range _c.builders { + func(i int, root context.Context) { + builder := _c.builders[i] + builder.defaults() + var mut Mutator = MutateFunc(func(ctx context.Context, m Mutation) (Value, error) { + mutation, ok := m.(*AllowListEntryMutation) + if !ok { + return nil, fmt.Errorf("unexpected mutation type %T", m) + } + if err := builder.check(); err != nil { + return nil, err + } + builder.mutation = mutation + var err error + nodes[i], specs[i] = builder.createSpec() + if i < len(mutators)-1 { + _, err = mutators[i+1].Mutate(root, _c.builders[i+1].mutation) + } else { + spec := &sqlgraph.BatchCreateSpec{Nodes: specs} + spec.OnConflict = _c.conflict + // Invoke the actual operation on the latest mutation in the chain. + if err = sqlgraph.BatchCreate(ctx, _c.driver, spec); err != nil { + if sqlgraph.IsConstraintError(err) { + err = &ConstraintError{msg: err.Error(), wrap: err} + } + } + } + if err != nil { + return nil, err + } + mutation.id = &nodes[i].ID + mutation.done = true + return nodes[i], nil + }) + for i := len(builder.hooks) - 1; i >= 0; i-- { + mut = builder.hooks[i](mut) + } + mutators[i] = mut + }(i, ctx) + } + if len(mutators) > 0 { + if _, err := mutators[0].Mutate(ctx, _c.builders[0].mutation); err != nil { + return nil, err + } + } + return nodes, nil +} + +// SaveX is like Save, but panics if an error occurs. +func (_c *AllowListEntryCreateBulk) SaveX(ctx context.Context) []*AllowListEntry { + v, err := _c.Save(ctx) + if err != nil { + panic(err) + } + return v +} + +// Exec executes the query. +func (_c *AllowListEntryCreateBulk) Exec(ctx context.Context) error { + _, err := _c.Save(ctx) + return err +} + +// ExecX is like Exec, but panics if an error occurs. +func (_c *AllowListEntryCreateBulk) ExecX(ctx context.Context) { + if err := _c.Exec(ctx); err != nil { + panic(err) + } +} + +// OnConflict allows configuring the `ON CONFLICT` / `ON DUPLICATE KEY` clause +// of the `INSERT` statement. For example: +// +// client.AllowListEntry.CreateBulk(builders...). +// OnConflict( +// // Update the row with the new values +// // the was proposed for insertion. +// sql.ResolveWithNewValues(), +// ). +// // Override some of the fields with custom +// // update values. +// Update(func(u *ent.AllowListEntryUpsert) { +// SetEmail(v+v). +// }). +// Exec(ctx) +func (_c *AllowListEntryCreateBulk) OnConflict(opts ...sql.ConflictOption) *AllowListEntryUpsertBulk { + _c.conflict = opts + return &AllowListEntryUpsertBulk{ + create: _c, + } +} + +// OnConflictColumns calls `OnConflict` and configures the columns +// as conflict target. Using this option is equivalent to using: +// +// client.AllowListEntry.Create(). +// OnConflict(sql.ConflictColumns(columns...)). +// Exec(ctx) +func (_c *AllowListEntryCreateBulk) OnConflictColumns(columns ...string) *AllowListEntryUpsertBulk { + _c.conflict = append(_c.conflict, sql.ConflictColumns(columns...)) + return &AllowListEntryUpsertBulk{ + create: _c, + } +} + +// AllowListEntryUpsertBulk is the builder for "upsert"-ing +// a bulk of AllowListEntry nodes. +type AllowListEntryUpsertBulk struct { + create *AllowListEntryCreateBulk +} + +// UpdateNewValues updates the mutable fields using the new values that +// were set on create. Using this option is equivalent to using: +// +// client.AllowListEntry.Create(). +// OnConflict( +// sql.ResolveWithNewValues(), +// sql.ResolveWith(func(u *sql.UpdateSet) { +// u.SetIgnore(allowlistentry.FieldID) +// }), +// ). +// Exec(ctx) +func (u *AllowListEntryUpsertBulk) UpdateNewValues() *AllowListEntryUpsertBulk { + u.create.conflict = append(u.create.conflict, sql.ResolveWithNewValues()) + u.create.conflict = append(u.create.conflict, sql.ResolveWith(func(s *sql.UpdateSet) { + for _, b := range u.create.builders { + if _, exists := b.mutation.ID(); exists { + s.SetIgnore(allowlistentry.FieldID) + } + if _, exists := b.mutation.Created(); exists { + s.SetIgnore(allowlistentry.FieldCreated) + } + } + })) + return u +} + +// Ignore sets each column to itself in case of conflict. +// Using this option is equivalent to using: +// +// client.AllowListEntry.Create(). +// OnConflict(sql.ResolveWithIgnore()). +// Exec(ctx) +func (u *AllowListEntryUpsertBulk) Ignore() *AllowListEntryUpsertBulk { + u.create.conflict = append(u.create.conflict, sql.ResolveWithIgnore()) + return u +} + +// DoNothing configures the conflict_action to `DO NOTHING`. +// Supported only by SQLite and PostgreSQL. +func (u *AllowListEntryUpsertBulk) DoNothing() *AllowListEntryUpsertBulk { + u.create.conflict = append(u.create.conflict, sql.DoNothing()) + return u +} + +// Update allows overriding fields `UPDATE` values. See the AllowListEntryCreateBulk.OnConflict +// documentation for more info. +func (u *AllowListEntryUpsertBulk) Update(set func(*AllowListEntryUpsert)) *AllowListEntryUpsertBulk { + u.create.conflict = append(u.create.conflict, sql.ResolveWith(func(update *sql.UpdateSet) { + set(&AllowListEntryUpsert{UpdateSet: update}) + })) + return u +} + +// SetEmail sets the "email" field. +func (u *AllowListEntryUpsertBulk) SetEmail(v string) *AllowListEntryUpsertBulk { + return u.Update(func(s *AllowListEntryUpsert) { + s.SetEmail(v) + }) +} + +// UpdateEmail sets the "email" field to the value that was provided on create. +func (u *AllowListEntryUpsertBulk) UpdateEmail() *AllowListEntryUpsertBulk { + return u.Update(func(s *AllowListEntryUpsert) { + s.UpdateEmail() + }) +} + +// SetNote sets the "note" field. +func (u *AllowListEntryUpsertBulk) SetNote(v string) *AllowListEntryUpsertBulk { + return u.Update(func(s *AllowListEntryUpsert) { + s.SetNote(v) + }) +} + +// UpdateNote sets the "note" field to the value that was provided on create. +func (u *AllowListEntryUpsertBulk) UpdateNote() *AllowListEntryUpsertBulk { + return u.Update(func(s *AllowListEntryUpsert) { + s.UpdateNote() + }) +} + +// SetAddedBy sets the "added_by" field. +func (u *AllowListEntryUpsertBulk) SetAddedBy(v string) *AllowListEntryUpsertBulk { + return u.Update(func(s *AllowListEntryUpsert) { + s.SetAddedBy(v) + }) +} + +// UpdateAddedBy sets the "added_by" field to the value that was provided on create. +func (u *AllowListEntryUpsertBulk) UpdateAddedBy() *AllowListEntryUpsertBulk { + return u.Update(func(s *AllowListEntryUpsert) { + s.UpdateAddedBy() + }) +} + +// SetInviteID sets the "invite_id" field. +func (u *AllowListEntryUpsertBulk) SetInviteID(v string) *AllowListEntryUpsertBulk { + return u.Update(func(s *AllowListEntryUpsert) { + s.SetInviteID(v) + }) +} + +// UpdateInviteID sets the "invite_id" field to the value that was provided on create. +func (u *AllowListEntryUpsertBulk) UpdateInviteID() *AllowListEntryUpsertBulk { + return u.Update(func(s *AllowListEntryUpsert) { + s.UpdateInviteID() + }) +} + +// ClearInviteID clears the value of the "invite_id" field. +func (u *AllowListEntryUpsertBulk) ClearInviteID() *AllowListEntryUpsertBulk { + return u.Update(func(s *AllowListEntryUpsert) { + s.ClearInviteID() + }) +} + +// Exec executes the query. +func (u *AllowListEntryUpsertBulk) Exec(ctx context.Context) error { + if u.create.err != nil { + return u.create.err + } + for i, b := range u.create.builders { + if len(b.conflict) != 0 { + return fmt.Errorf("ent: OnConflict was set for builder %d. Set it on the AllowListEntryCreateBulk instead", i) + } + } + if len(u.create.conflict) == 0 { + return errors.New("ent: missing options for AllowListEntryCreateBulk.OnConflict") + } + return u.create.Exec(ctx) +} + +// ExecX is like Exec, but panics if an error occurs. +func (u *AllowListEntryUpsertBulk) ExecX(ctx context.Context) { + if err := u.create.Exec(ctx); err != nil { + panic(err) + } +} diff --git a/pkg/ent/allowlistentry_delete.go b/pkg/ent/allowlistentry_delete.go new file mode 100644 index 000000000..d4a9375cc --- /dev/null +++ b/pkg/ent/allowlistentry_delete.go @@ -0,0 +1,88 @@ +// Code generated by ent, DO NOT EDIT. + +package ent + +import ( + "context" + + "entgo.io/ent/dialect/sql" + "entgo.io/ent/dialect/sql/sqlgraph" + "entgo.io/ent/schema/field" + "github.com/GoogleCloudPlatform/scion/pkg/ent/allowlistentry" + "github.com/GoogleCloudPlatform/scion/pkg/ent/predicate" +) + +// AllowListEntryDelete is the builder for deleting a AllowListEntry entity. +type AllowListEntryDelete struct { + config + hooks []Hook + mutation *AllowListEntryMutation +} + +// Where appends a list predicates to the AllowListEntryDelete builder. +func (_d *AllowListEntryDelete) Where(ps ...predicate.AllowListEntry) *AllowListEntryDelete { + _d.mutation.Where(ps...) + return _d +} + +// Exec executes the deletion query and returns how many vertices were deleted. +func (_d *AllowListEntryDelete) Exec(ctx context.Context) (int, error) { + return withHooks(ctx, _d.sqlExec, _d.mutation, _d.hooks) +} + +// ExecX is like Exec, but panics if an error occurs. +func (_d *AllowListEntryDelete) ExecX(ctx context.Context) int { + n, err := _d.Exec(ctx) + if err != nil { + panic(err) + } + return n +} + +func (_d *AllowListEntryDelete) sqlExec(ctx context.Context) (int, error) { + _spec := sqlgraph.NewDeleteSpec(allowlistentry.Table, sqlgraph.NewFieldSpec(allowlistentry.FieldID, field.TypeUUID)) + if ps := _d.mutation.predicates; len(ps) > 0 { + _spec.Predicate = func(selector *sql.Selector) { + for i := range ps { + ps[i](selector) + } + } + } + affected, err := sqlgraph.DeleteNodes(ctx, _d.driver, _spec) + if err != nil && sqlgraph.IsConstraintError(err) { + err = &ConstraintError{msg: err.Error(), wrap: err} + } + _d.mutation.done = true + return affected, err +} + +// AllowListEntryDeleteOne is the builder for deleting a single AllowListEntry entity. +type AllowListEntryDeleteOne struct { + _d *AllowListEntryDelete +} + +// Where appends a list predicates to the AllowListEntryDelete builder. +func (_d *AllowListEntryDeleteOne) Where(ps ...predicate.AllowListEntry) *AllowListEntryDeleteOne { + _d._d.mutation.Where(ps...) + return _d +} + +// Exec executes the deletion query. +func (_d *AllowListEntryDeleteOne) Exec(ctx context.Context) error { + n, err := _d._d.Exec(ctx) + switch { + case err != nil: + return err + case n == 0: + return &NotFoundError{allowlistentry.Label} + default: + return nil + } +} + +// ExecX is like Exec, but panics if an error occurs. +func (_d *AllowListEntryDeleteOne) ExecX(ctx context.Context) { + if err := _d.Exec(ctx); err != nil { + panic(err) + } +} diff --git a/pkg/ent/allowlistentry_query.go b/pkg/ent/allowlistentry_query.go new file mode 100644 index 000000000..4dd9da256 --- /dev/null +++ b/pkg/ent/allowlistentry_query.go @@ -0,0 +1,565 @@ +// Code generated by ent, DO NOT EDIT. + +package ent + +import ( + "context" + "fmt" + "math" + + "entgo.io/ent" + "entgo.io/ent/dialect" + "entgo.io/ent/dialect/sql" + "entgo.io/ent/dialect/sql/sqlgraph" + "entgo.io/ent/schema/field" + "github.com/GoogleCloudPlatform/scion/pkg/ent/allowlistentry" + "github.com/GoogleCloudPlatform/scion/pkg/ent/predicate" + "github.com/google/uuid" +) + +// AllowListEntryQuery is the builder for querying AllowListEntry entities. +type AllowListEntryQuery struct { + config + ctx *QueryContext + order []allowlistentry.OrderOption + inters []Interceptor + predicates []predicate.AllowListEntry + modifiers []func(*sql.Selector) + // intermediate query (i.e. traversal path). + sql *sql.Selector + path func(context.Context) (*sql.Selector, error) +} + +// Where adds a new predicate for the AllowListEntryQuery builder. +func (_q *AllowListEntryQuery) Where(ps ...predicate.AllowListEntry) *AllowListEntryQuery { + _q.predicates = append(_q.predicates, ps...) + return _q +} + +// Limit the number of records to be returned by this query. +func (_q *AllowListEntryQuery) Limit(limit int) *AllowListEntryQuery { + _q.ctx.Limit = &limit + return _q +} + +// Offset to start from. +func (_q *AllowListEntryQuery) Offset(offset int) *AllowListEntryQuery { + _q.ctx.Offset = &offset + return _q +} + +// Unique configures the query builder to filter duplicate records on query. +// By default, unique is set to true, and can be disabled using this method. +func (_q *AllowListEntryQuery) Unique(unique bool) *AllowListEntryQuery { + _q.ctx.Unique = &unique + return _q +} + +// Order specifies how the records should be ordered. +func (_q *AllowListEntryQuery) Order(o ...allowlistentry.OrderOption) *AllowListEntryQuery { + _q.order = append(_q.order, o...) + return _q +} + +// First returns the first AllowListEntry entity from the query. +// Returns a *NotFoundError when no AllowListEntry was found. +func (_q *AllowListEntryQuery) First(ctx context.Context) (*AllowListEntry, error) { + nodes, err := _q.Limit(1).All(setContextOp(ctx, _q.ctx, ent.OpQueryFirst)) + if err != nil { + return nil, err + } + if len(nodes) == 0 { + return nil, &NotFoundError{allowlistentry.Label} + } + return nodes[0], nil +} + +// FirstX is like First, but panics if an error occurs. +func (_q *AllowListEntryQuery) FirstX(ctx context.Context) *AllowListEntry { + node, err := _q.First(ctx) + if err != nil && !IsNotFound(err) { + panic(err) + } + return node +} + +// FirstID returns the first AllowListEntry ID from the query. +// Returns a *NotFoundError when no AllowListEntry ID was found. +func (_q *AllowListEntryQuery) FirstID(ctx context.Context) (id uuid.UUID, err error) { + var ids []uuid.UUID + if ids, err = _q.Limit(1).IDs(setContextOp(ctx, _q.ctx, ent.OpQueryFirstID)); err != nil { + return + } + if len(ids) == 0 { + err = &NotFoundError{allowlistentry.Label} + return + } + return ids[0], nil +} + +// FirstIDX is like FirstID, but panics if an error occurs. +func (_q *AllowListEntryQuery) FirstIDX(ctx context.Context) uuid.UUID { + id, err := _q.FirstID(ctx) + if err != nil && !IsNotFound(err) { + panic(err) + } + return id +} + +// Only returns a single AllowListEntry entity found by the query, ensuring it only returns one. +// Returns a *NotSingularError when more than one AllowListEntry entity is found. +// Returns a *NotFoundError when no AllowListEntry entities are found. +func (_q *AllowListEntryQuery) Only(ctx context.Context) (*AllowListEntry, error) { + nodes, err := _q.Limit(2).All(setContextOp(ctx, _q.ctx, ent.OpQueryOnly)) + if err != nil { + return nil, err + } + switch len(nodes) { + case 1: + return nodes[0], nil + case 0: + return nil, &NotFoundError{allowlistentry.Label} + default: + return nil, &NotSingularError{allowlistentry.Label} + } +} + +// OnlyX is like Only, but panics if an error occurs. +func (_q *AllowListEntryQuery) OnlyX(ctx context.Context) *AllowListEntry { + node, err := _q.Only(ctx) + if err != nil { + panic(err) + } + return node +} + +// OnlyID is like Only, but returns the only AllowListEntry ID in the query. +// Returns a *NotSingularError when more than one AllowListEntry ID is found. +// Returns a *NotFoundError when no entities are found. +func (_q *AllowListEntryQuery) OnlyID(ctx context.Context) (id uuid.UUID, err error) { + var ids []uuid.UUID + if ids, err = _q.Limit(2).IDs(setContextOp(ctx, _q.ctx, ent.OpQueryOnlyID)); err != nil { + return + } + switch len(ids) { + case 1: + id = ids[0] + case 0: + err = &NotFoundError{allowlistentry.Label} + default: + err = &NotSingularError{allowlistentry.Label} + } + return +} + +// OnlyIDX is like OnlyID, but panics if an error occurs. +func (_q *AllowListEntryQuery) OnlyIDX(ctx context.Context) uuid.UUID { + id, err := _q.OnlyID(ctx) + if err != nil { + panic(err) + } + return id +} + +// All executes the query and returns a list of AllowListEntries. +func (_q *AllowListEntryQuery) All(ctx context.Context) ([]*AllowListEntry, error) { + ctx = setContextOp(ctx, _q.ctx, ent.OpQueryAll) + if err := _q.prepareQuery(ctx); err != nil { + return nil, err + } + qr := querierAll[[]*AllowListEntry, *AllowListEntryQuery]() + return withInterceptors[[]*AllowListEntry](ctx, _q, qr, _q.inters) +} + +// AllX is like All, but panics if an error occurs. +func (_q *AllowListEntryQuery) AllX(ctx context.Context) []*AllowListEntry { + nodes, err := _q.All(ctx) + if err != nil { + panic(err) + } + return nodes +} + +// IDs executes the query and returns a list of AllowListEntry IDs. +func (_q *AllowListEntryQuery) IDs(ctx context.Context) (ids []uuid.UUID, err error) { + if _q.ctx.Unique == nil && _q.path != nil { + _q.Unique(true) + } + ctx = setContextOp(ctx, _q.ctx, ent.OpQueryIDs) + if err = _q.Select(allowlistentry.FieldID).Scan(ctx, &ids); err != nil { + return nil, err + } + return ids, nil +} + +// IDsX is like IDs, but panics if an error occurs. +func (_q *AllowListEntryQuery) IDsX(ctx context.Context) []uuid.UUID { + ids, err := _q.IDs(ctx) + if err != nil { + panic(err) + } + return ids +} + +// Count returns the count of the given query. +func (_q *AllowListEntryQuery) Count(ctx context.Context) (int, error) { + ctx = setContextOp(ctx, _q.ctx, ent.OpQueryCount) + if err := _q.prepareQuery(ctx); err != nil { + return 0, err + } + return withInterceptors[int](ctx, _q, querierCount[*AllowListEntryQuery](), _q.inters) +} + +// CountX is like Count, but panics if an error occurs. +func (_q *AllowListEntryQuery) CountX(ctx context.Context) int { + count, err := _q.Count(ctx) + if err != nil { + panic(err) + } + return count +} + +// Exist returns true if the query has elements in the graph. +func (_q *AllowListEntryQuery) Exist(ctx context.Context) (bool, error) { + ctx = setContextOp(ctx, _q.ctx, ent.OpQueryExist) + switch _, err := _q.FirstID(ctx); { + case IsNotFound(err): + return false, nil + case err != nil: + return false, fmt.Errorf("ent: check existence: %w", err) + default: + return true, nil + } +} + +// ExistX is like Exist, but panics if an error occurs. +func (_q *AllowListEntryQuery) ExistX(ctx context.Context) bool { + exist, err := _q.Exist(ctx) + if err != nil { + panic(err) + } + return exist +} + +// Clone returns a duplicate of the AllowListEntryQuery builder, including all associated steps. It can be +// used to prepare common query builders and use them differently after the clone is made. +func (_q *AllowListEntryQuery) Clone() *AllowListEntryQuery { + if _q == nil { + return nil + } + return &AllowListEntryQuery{ + config: _q.config, + ctx: _q.ctx.Clone(), + order: append([]allowlistentry.OrderOption{}, _q.order...), + inters: append([]Interceptor{}, _q.inters...), + predicates: append([]predicate.AllowListEntry{}, _q.predicates...), + // clone intermediate query. + sql: _q.sql.Clone(), + path: _q.path, + } +} + +// GroupBy is used to group vertices by one or more fields/columns. +// It is often used with aggregate functions, like: count, max, mean, min, sum. +// +// Example: +// +// var v []struct { +// Email string `json:"email,omitempty"` +// Count int `json:"count,omitempty"` +// } +// +// client.AllowListEntry.Query(). +// GroupBy(allowlistentry.FieldEmail). +// Aggregate(ent.Count()). +// Scan(ctx, &v) +func (_q *AllowListEntryQuery) GroupBy(field string, fields ...string) *AllowListEntryGroupBy { + _q.ctx.Fields = append([]string{field}, fields...) + grbuild := &AllowListEntryGroupBy{build: _q} + grbuild.flds = &_q.ctx.Fields + grbuild.label = allowlistentry.Label + grbuild.scan = grbuild.Scan + return grbuild +} + +// Select allows the selection one or more fields/columns for the given query, +// instead of selecting all fields in the entity. +// +// Example: +// +// var v []struct { +// Email string `json:"email,omitempty"` +// } +// +// client.AllowListEntry.Query(). +// Select(allowlistentry.FieldEmail). +// Scan(ctx, &v) +func (_q *AllowListEntryQuery) Select(fields ...string) *AllowListEntrySelect { + _q.ctx.Fields = append(_q.ctx.Fields, fields...) + sbuild := &AllowListEntrySelect{AllowListEntryQuery: _q} + sbuild.label = allowlistentry.Label + sbuild.flds, sbuild.scan = &_q.ctx.Fields, sbuild.Scan + return sbuild +} + +// Aggregate returns a AllowListEntrySelect configured with the given aggregations. +func (_q *AllowListEntryQuery) Aggregate(fns ...AggregateFunc) *AllowListEntrySelect { + return _q.Select().Aggregate(fns...) +} + +func (_q *AllowListEntryQuery) prepareQuery(ctx context.Context) error { + for _, inter := range _q.inters { + if inter == nil { + return fmt.Errorf("ent: uninitialized interceptor (forgotten import ent/runtime?)") + } + if trv, ok := inter.(Traverser); ok { + if err := trv.Traverse(ctx, _q); err != nil { + return err + } + } + } + for _, f := range _q.ctx.Fields { + if !allowlistentry.ValidColumn(f) { + return &ValidationError{Name: f, err: fmt.Errorf("ent: invalid field %q for query", f)} + } + } + if _q.path != nil { + prev, err := _q.path(ctx) + if err != nil { + return err + } + _q.sql = prev + } + return nil +} + +func (_q *AllowListEntryQuery) sqlAll(ctx context.Context, hooks ...queryHook) ([]*AllowListEntry, error) { + var ( + nodes = []*AllowListEntry{} + _spec = _q.querySpec() + ) + _spec.ScanValues = func(columns []string) ([]any, error) { + return (*AllowListEntry).scanValues(nil, columns) + } + _spec.Assign = func(columns []string, values []any) error { + node := &AllowListEntry{config: _q.config} + nodes = append(nodes, node) + return node.assignValues(columns, values) + } + if len(_q.modifiers) > 0 { + _spec.Modifiers = _q.modifiers + } + for i := range hooks { + hooks[i](ctx, _spec) + } + if err := sqlgraph.QueryNodes(ctx, _q.driver, _spec); err != nil { + return nil, err + } + if len(nodes) == 0 { + return nodes, nil + } + return nodes, nil +} + +func (_q *AllowListEntryQuery) sqlCount(ctx context.Context) (int, error) { + _spec := _q.querySpec() + if len(_q.modifiers) > 0 { + _spec.Modifiers = _q.modifiers + } + _spec.Node.Columns = _q.ctx.Fields + if len(_q.ctx.Fields) > 0 { + _spec.Unique = _q.ctx.Unique != nil && *_q.ctx.Unique + } + return sqlgraph.CountNodes(ctx, _q.driver, _spec) +} + +func (_q *AllowListEntryQuery) querySpec() *sqlgraph.QuerySpec { + _spec := sqlgraph.NewQuerySpec(allowlistentry.Table, allowlistentry.Columns, sqlgraph.NewFieldSpec(allowlistentry.FieldID, field.TypeUUID)) + _spec.From = _q.sql + if unique := _q.ctx.Unique; unique != nil { + _spec.Unique = *unique + } else if _q.path != nil { + _spec.Unique = true + } + if fields := _q.ctx.Fields; len(fields) > 0 { + _spec.Node.Columns = make([]string, 0, len(fields)) + _spec.Node.Columns = append(_spec.Node.Columns, allowlistentry.FieldID) + for i := range fields { + if fields[i] != allowlistentry.FieldID { + _spec.Node.Columns = append(_spec.Node.Columns, fields[i]) + } + } + } + if ps := _q.predicates; len(ps) > 0 { + _spec.Predicate = func(selector *sql.Selector) { + for i := range ps { + ps[i](selector) + } + } + } + if limit := _q.ctx.Limit; limit != nil { + _spec.Limit = *limit + } + if offset := _q.ctx.Offset; offset != nil { + _spec.Offset = *offset + } + if ps := _q.order; len(ps) > 0 { + _spec.Order = func(selector *sql.Selector) { + for i := range ps { + ps[i](selector) + } + } + } + return _spec +} + +func (_q *AllowListEntryQuery) sqlQuery(ctx context.Context) *sql.Selector { + builder := sql.Dialect(_q.driver.Dialect()) + t1 := builder.Table(allowlistentry.Table) + columns := _q.ctx.Fields + if len(columns) == 0 { + columns = allowlistentry.Columns + } + selector := builder.Select(t1.Columns(columns...)...).From(t1) + if _q.sql != nil { + selector = _q.sql + selector.Select(selector.Columns(columns...)...) + } + if _q.ctx.Unique != nil && *_q.ctx.Unique { + selector.Distinct() + } + for _, m := range _q.modifiers { + m(selector) + } + for _, p := range _q.predicates { + p(selector) + } + for _, p := range _q.order { + p(selector) + } + if offset := _q.ctx.Offset; offset != nil { + // limit is mandatory for offset clause. We start + // with default value, and override it below if needed. + selector.Offset(*offset).Limit(math.MaxInt32) + } + if limit := _q.ctx.Limit; limit != nil { + selector.Limit(*limit) + } + return selector +} + +// ForUpdate locks the selected rows against concurrent updates, and prevent them from being +// updated, deleted or "selected ... for update" by other sessions, until the transaction is +// either committed or rolled-back. +func (_q *AllowListEntryQuery) ForUpdate(opts ...sql.LockOption) *AllowListEntryQuery { + if _q.driver.Dialect() == dialect.Postgres { + _q.Unique(false) + } + _q.modifiers = append(_q.modifiers, func(s *sql.Selector) { + s.ForUpdate(opts...) + }) + return _q +} + +// ForShare behaves similarly to ForUpdate, except that it acquires a shared mode lock +// on any rows that are read. Other sessions can read the rows, but cannot modify them +// until your transaction commits. +func (_q *AllowListEntryQuery) ForShare(opts ...sql.LockOption) *AllowListEntryQuery { + if _q.driver.Dialect() == dialect.Postgres { + _q.Unique(false) + } + _q.modifiers = append(_q.modifiers, func(s *sql.Selector) { + s.ForShare(opts...) + }) + return _q +} + +// AllowListEntryGroupBy is the group-by builder for AllowListEntry entities. +type AllowListEntryGroupBy struct { + selector + build *AllowListEntryQuery +} + +// Aggregate adds the given aggregation functions to the group-by query. +func (_g *AllowListEntryGroupBy) Aggregate(fns ...AggregateFunc) *AllowListEntryGroupBy { + _g.fns = append(_g.fns, fns...) + return _g +} + +// Scan applies the selector query and scans the result into the given value. +func (_g *AllowListEntryGroupBy) Scan(ctx context.Context, v any) error { + ctx = setContextOp(ctx, _g.build.ctx, ent.OpQueryGroupBy) + if err := _g.build.prepareQuery(ctx); err != nil { + return err + } + return scanWithInterceptors[*AllowListEntryQuery, *AllowListEntryGroupBy](ctx, _g.build, _g, _g.build.inters, v) +} + +func (_g *AllowListEntryGroupBy) sqlScan(ctx context.Context, root *AllowListEntryQuery, v any) error { + selector := root.sqlQuery(ctx).Select() + aggregation := make([]string, 0, len(_g.fns)) + for _, fn := range _g.fns { + aggregation = append(aggregation, fn(selector)) + } + if len(selector.SelectedColumns()) == 0 { + columns := make([]string, 0, len(*_g.flds)+len(_g.fns)) + for _, f := range *_g.flds { + columns = append(columns, selector.C(f)) + } + columns = append(columns, aggregation...) + selector.Select(columns...) + } + selector.GroupBy(selector.Columns(*_g.flds...)...) + if err := selector.Err(); err != nil { + return err + } + rows := &sql.Rows{} + query, args := selector.Query() + if err := _g.build.driver.Query(ctx, query, args, rows); err != nil { + return err + } + defer rows.Close() + return sql.ScanSlice(rows, v) +} + +// AllowListEntrySelect is the builder for selecting fields of AllowListEntry entities. +type AllowListEntrySelect struct { + *AllowListEntryQuery + selector +} + +// Aggregate adds the given aggregation functions to the selector query. +func (_s *AllowListEntrySelect) Aggregate(fns ...AggregateFunc) *AllowListEntrySelect { + _s.fns = append(_s.fns, fns...) + return _s +} + +// Scan applies the selector query and scans the result into the given value. +func (_s *AllowListEntrySelect) Scan(ctx context.Context, v any) error { + ctx = setContextOp(ctx, _s.ctx, ent.OpQuerySelect) + if err := _s.prepareQuery(ctx); err != nil { + return err + } + return scanWithInterceptors[*AllowListEntryQuery, *AllowListEntrySelect](ctx, _s.AllowListEntryQuery, _s, _s.inters, v) +} + +func (_s *AllowListEntrySelect) sqlScan(ctx context.Context, root *AllowListEntryQuery, v any) error { + selector := root.sqlQuery(ctx) + aggregation := make([]string, 0, len(_s.fns)) + for _, fn := range _s.fns { + aggregation = append(aggregation, fn(selector)) + } + switch n := len(*_s.selector.flds); { + case n == 0 && len(aggregation) > 0: + selector.Select(aggregation...) + case n != 0 && len(aggregation) > 0: + selector.AppendSelect(aggregation...) + } + rows := &sql.Rows{} + query, args := selector.Query() + if err := _s.driver.Query(ctx, query, args, rows); err != nil { + return err + } + defer rows.Close() + return sql.ScanSlice(rows, v) +} diff --git a/pkg/ent/allowlistentry_update.go b/pkg/ent/allowlistentry_update.go new file mode 100644 index 000000000..c7dcbf88b --- /dev/null +++ b/pkg/ent/allowlistentry_update.go @@ -0,0 +1,365 @@ +// Code generated by ent, DO NOT EDIT. + +package ent + +import ( + "context" + "errors" + "fmt" + + "entgo.io/ent/dialect/sql" + "entgo.io/ent/dialect/sql/sqlgraph" + "entgo.io/ent/schema/field" + "github.com/GoogleCloudPlatform/scion/pkg/ent/allowlistentry" + "github.com/GoogleCloudPlatform/scion/pkg/ent/predicate" +) + +// AllowListEntryUpdate is the builder for updating AllowListEntry entities. +type AllowListEntryUpdate struct { + config + hooks []Hook + mutation *AllowListEntryMutation +} + +// Where appends a list predicates to the AllowListEntryUpdate builder. +func (_u *AllowListEntryUpdate) Where(ps ...predicate.AllowListEntry) *AllowListEntryUpdate { + _u.mutation.Where(ps...) + return _u +} + +// SetEmail sets the "email" field. +func (_u *AllowListEntryUpdate) SetEmail(v string) *AllowListEntryUpdate { + _u.mutation.SetEmail(v) + return _u +} + +// SetNillableEmail sets the "email" field if the given value is not nil. +func (_u *AllowListEntryUpdate) SetNillableEmail(v *string) *AllowListEntryUpdate { + if v != nil { + _u.SetEmail(*v) + } + return _u +} + +// SetNote sets the "note" field. +func (_u *AllowListEntryUpdate) SetNote(v string) *AllowListEntryUpdate { + _u.mutation.SetNote(v) + return _u +} + +// SetNillableNote sets the "note" field if the given value is not nil. +func (_u *AllowListEntryUpdate) SetNillableNote(v *string) *AllowListEntryUpdate { + if v != nil { + _u.SetNote(*v) + } + return _u +} + +// SetAddedBy sets the "added_by" field. +func (_u *AllowListEntryUpdate) SetAddedBy(v string) *AllowListEntryUpdate { + _u.mutation.SetAddedBy(v) + return _u +} + +// SetNillableAddedBy sets the "added_by" field if the given value is not nil. +func (_u *AllowListEntryUpdate) SetNillableAddedBy(v *string) *AllowListEntryUpdate { + if v != nil { + _u.SetAddedBy(*v) + } + return _u +} + +// SetInviteID sets the "invite_id" field. +func (_u *AllowListEntryUpdate) SetInviteID(v string) *AllowListEntryUpdate { + _u.mutation.SetInviteID(v) + return _u +} + +// SetNillableInviteID sets the "invite_id" field if the given value is not nil. +func (_u *AllowListEntryUpdate) SetNillableInviteID(v *string) *AllowListEntryUpdate { + if v != nil { + _u.SetInviteID(*v) + } + return _u +} + +// ClearInviteID clears the value of the "invite_id" field. +func (_u *AllowListEntryUpdate) ClearInviteID() *AllowListEntryUpdate { + _u.mutation.ClearInviteID() + return _u +} + +// Mutation returns the AllowListEntryMutation object of the builder. +func (_u *AllowListEntryUpdate) Mutation() *AllowListEntryMutation { + return _u.mutation +} + +// Save executes the query and returns the number of nodes affected by the update operation. +func (_u *AllowListEntryUpdate) Save(ctx context.Context) (int, error) { + return withHooks(ctx, _u.sqlSave, _u.mutation, _u.hooks) +} + +// SaveX is like Save, but panics if an error occurs. +func (_u *AllowListEntryUpdate) SaveX(ctx context.Context) int { + affected, err := _u.Save(ctx) + if err != nil { + panic(err) + } + return affected +} + +// Exec executes the query. +func (_u *AllowListEntryUpdate) Exec(ctx context.Context) error { + _, err := _u.Save(ctx) + return err +} + +// ExecX is like Exec, but panics if an error occurs. +func (_u *AllowListEntryUpdate) ExecX(ctx context.Context) { + if err := _u.Exec(ctx); err != nil { + panic(err) + } +} + +// check runs all checks and user-defined validators on the builder. +func (_u *AllowListEntryUpdate) check() error { + if v, ok := _u.mutation.Email(); ok { + if err := allowlistentry.EmailValidator(v); err != nil { + return &ValidationError{Name: "email", err: fmt.Errorf(`ent: validator failed for field "AllowListEntry.email": %w`, err)} + } + } + if v, ok := _u.mutation.AddedBy(); ok { + if err := allowlistentry.AddedByValidator(v); err != nil { + return &ValidationError{Name: "added_by", err: fmt.Errorf(`ent: validator failed for field "AllowListEntry.added_by": %w`, err)} + } + } + return nil +} + +func (_u *AllowListEntryUpdate) sqlSave(ctx context.Context) (_node int, err error) { + if err := _u.check(); err != nil { + return _node, err + } + _spec := sqlgraph.NewUpdateSpec(allowlistentry.Table, allowlistentry.Columns, sqlgraph.NewFieldSpec(allowlistentry.FieldID, field.TypeUUID)) + if ps := _u.mutation.predicates; len(ps) > 0 { + _spec.Predicate = func(selector *sql.Selector) { + for i := range ps { + ps[i](selector) + } + } + } + if value, ok := _u.mutation.Email(); ok { + _spec.SetField(allowlistentry.FieldEmail, field.TypeString, value) + } + if value, ok := _u.mutation.Note(); ok { + _spec.SetField(allowlistentry.FieldNote, field.TypeString, value) + } + if value, ok := _u.mutation.AddedBy(); ok { + _spec.SetField(allowlistentry.FieldAddedBy, field.TypeString, value) + } + if value, ok := _u.mutation.InviteID(); ok { + _spec.SetField(allowlistentry.FieldInviteID, field.TypeString, value) + } + if _u.mutation.InviteIDCleared() { + _spec.ClearField(allowlistentry.FieldInviteID, field.TypeString) + } + if _node, err = sqlgraph.UpdateNodes(ctx, _u.driver, _spec); err != nil { + if _, ok := err.(*sqlgraph.NotFoundError); ok { + err = &NotFoundError{allowlistentry.Label} + } else if sqlgraph.IsConstraintError(err) { + err = &ConstraintError{msg: err.Error(), wrap: err} + } + return 0, err + } + _u.mutation.done = true + return _node, nil +} + +// AllowListEntryUpdateOne is the builder for updating a single AllowListEntry entity. +type AllowListEntryUpdateOne struct { + config + fields []string + hooks []Hook + mutation *AllowListEntryMutation +} + +// SetEmail sets the "email" field. +func (_u *AllowListEntryUpdateOne) SetEmail(v string) *AllowListEntryUpdateOne { + _u.mutation.SetEmail(v) + return _u +} + +// SetNillableEmail sets the "email" field if the given value is not nil. +func (_u *AllowListEntryUpdateOne) SetNillableEmail(v *string) *AllowListEntryUpdateOne { + if v != nil { + _u.SetEmail(*v) + } + return _u +} + +// SetNote sets the "note" field. +func (_u *AllowListEntryUpdateOne) SetNote(v string) *AllowListEntryUpdateOne { + _u.mutation.SetNote(v) + return _u +} + +// SetNillableNote sets the "note" field if the given value is not nil. +func (_u *AllowListEntryUpdateOne) SetNillableNote(v *string) *AllowListEntryUpdateOne { + if v != nil { + _u.SetNote(*v) + } + return _u +} + +// SetAddedBy sets the "added_by" field. +func (_u *AllowListEntryUpdateOne) SetAddedBy(v string) *AllowListEntryUpdateOne { + _u.mutation.SetAddedBy(v) + return _u +} + +// SetNillableAddedBy sets the "added_by" field if the given value is not nil. +func (_u *AllowListEntryUpdateOne) SetNillableAddedBy(v *string) *AllowListEntryUpdateOne { + if v != nil { + _u.SetAddedBy(*v) + } + return _u +} + +// SetInviteID sets the "invite_id" field. +func (_u *AllowListEntryUpdateOne) SetInviteID(v string) *AllowListEntryUpdateOne { + _u.mutation.SetInviteID(v) + return _u +} + +// SetNillableInviteID sets the "invite_id" field if the given value is not nil. +func (_u *AllowListEntryUpdateOne) SetNillableInviteID(v *string) *AllowListEntryUpdateOne { + if v != nil { + _u.SetInviteID(*v) + } + return _u +} + +// ClearInviteID clears the value of the "invite_id" field. +func (_u *AllowListEntryUpdateOne) ClearInviteID() *AllowListEntryUpdateOne { + _u.mutation.ClearInviteID() + return _u +} + +// Mutation returns the AllowListEntryMutation object of the builder. +func (_u *AllowListEntryUpdateOne) Mutation() *AllowListEntryMutation { + return _u.mutation +} + +// Where appends a list predicates to the AllowListEntryUpdate builder. +func (_u *AllowListEntryUpdateOne) Where(ps ...predicate.AllowListEntry) *AllowListEntryUpdateOne { + _u.mutation.Where(ps...) + return _u +} + +// Select allows selecting one or more fields (columns) of the returned entity. +// The default is selecting all fields defined in the entity schema. +func (_u *AllowListEntryUpdateOne) Select(field string, fields ...string) *AllowListEntryUpdateOne { + _u.fields = append([]string{field}, fields...) + return _u +} + +// Save executes the query and returns the updated AllowListEntry entity. +func (_u *AllowListEntryUpdateOne) Save(ctx context.Context) (*AllowListEntry, error) { + return withHooks(ctx, _u.sqlSave, _u.mutation, _u.hooks) +} + +// SaveX is like Save, but panics if an error occurs. +func (_u *AllowListEntryUpdateOne) SaveX(ctx context.Context) *AllowListEntry { + node, err := _u.Save(ctx) + if err != nil { + panic(err) + } + return node +} + +// Exec executes the query on the entity. +func (_u *AllowListEntryUpdateOne) Exec(ctx context.Context) error { + _, err := _u.Save(ctx) + return err +} + +// ExecX is like Exec, but panics if an error occurs. +func (_u *AllowListEntryUpdateOne) ExecX(ctx context.Context) { + if err := _u.Exec(ctx); err != nil { + panic(err) + } +} + +// check runs all checks and user-defined validators on the builder. +func (_u *AllowListEntryUpdateOne) check() error { + if v, ok := _u.mutation.Email(); ok { + if err := allowlistentry.EmailValidator(v); err != nil { + return &ValidationError{Name: "email", err: fmt.Errorf(`ent: validator failed for field "AllowListEntry.email": %w`, err)} + } + } + if v, ok := _u.mutation.AddedBy(); ok { + if err := allowlistentry.AddedByValidator(v); err != nil { + return &ValidationError{Name: "added_by", err: fmt.Errorf(`ent: validator failed for field "AllowListEntry.added_by": %w`, err)} + } + } + return nil +} + +func (_u *AllowListEntryUpdateOne) sqlSave(ctx context.Context) (_node *AllowListEntry, err error) { + if err := _u.check(); err != nil { + return _node, err + } + _spec := sqlgraph.NewUpdateSpec(allowlistentry.Table, allowlistentry.Columns, sqlgraph.NewFieldSpec(allowlistentry.FieldID, field.TypeUUID)) + id, ok := _u.mutation.ID() + if !ok { + return nil, &ValidationError{Name: "id", err: errors.New(`ent: missing "AllowListEntry.id" for update`)} + } + _spec.Node.ID.Value = id + if fields := _u.fields; len(fields) > 0 { + _spec.Node.Columns = make([]string, 0, len(fields)) + _spec.Node.Columns = append(_spec.Node.Columns, allowlistentry.FieldID) + for _, f := range fields { + if !allowlistentry.ValidColumn(f) { + return nil, &ValidationError{Name: f, err: fmt.Errorf("ent: invalid field %q for query", f)} + } + if f != allowlistentry.FieldID { + _spec.Node.Columns = append(_spec.Node.Columns, f) + } + } + } + if ps := _u.mutation.predicates; len(ps) > 0 { + _spec.Predicate = func(selector *sql.Selector) { + for i := range ps { + ps[i](selector) + } + } + } + if value, ok := _u.mutation.Email(); ok { + _spec.SetField(allowlistentry.FieldEmail, field.TypeString, value) + } + if value, ok := _u.mutation.Note(); ok { + _spec.SetField(allowlistentry.FieldNote, field.TypeString, value) + } + if value, ok := _u.mutation.AddedBy(); ok { + _spec.SetField(allowlistentry.FieldAddedBy, field.TypeString, value) + } + if value, ok := _u.mutation.InviteID(); ok { + _spec.SetField(allowlistentry.FieldInviteID, field.TypeString, value) + } + if _u.mutation.InviteIDCleared() { + _spec.ClearField(allowlistentry.FieldInviteID, field.TypeString) + } + _node = &AllowListEntry{config: _u.config} + _spec.Assign = _node.assignValues + _spec.ScanValues = _node.scanValues + if err = sqlgraph.UpdateNode(ctx, _u.driver, _spec); err != nil { + if _, ok := err.(*sqlgraph.NotFoundError); ok { + err = &NotFoundError{allowlistentry.Label} + } else if sqlgraph.IsConstraintError(err) { + err = &ConstraintError{msg: err.Error(), wrap: err} + } + return nil, err + } + _u.mutation.done = true + return _node, nil +} diff --git a/pkg/ent/apikey.go b/pkg/ent/apikey.go new file mode 100644 index 000000000..df11947b3 --- /dev/null +++ b/pkg/ent/apikey.go @@ -0,0 +1,202 @@ +// Code generated by ent, DO NOT EDIT. + +package ent + +import ( + "fmt" + "strings" + "time" + + "entgo.io/ent" + "entgo.io/ent/dialect/sql" + "github.com/GoogleCloudPlatform/scion/pkg/ent/apikey" + "github.com/google/uuid" +) + +// ApiKey is the model entity for the ApiKey schema. +type ApiKey struct { + config `json:"-"` + // ID of the ent. + ID uuid.UUID `json:"id,omitempty"` + // UserID holds the value of the "user_id" field. + UserID uuid.UUID `json:"user_id,omitempty"` + // Name holds the value of the "name" field. + Name string `json:"name,omitempty"` + // Prefix holds the value of the "prefix" field. + Prefix string `json:"prefix,omitempty"` + // KeyHash holds the value of the "key_hash" field. + KeyHash string `json:"-"` + // Scopes holds the value of the "scopes" field. + Scopes string `json:"scopes,omitempty"` + // Revoked holds the value of the "revoked" field. + Revoked bool `json:"revoked,omitempty"` + // ExpiresAt holds the value of the "expires_at" field. + ExpiresAt *time.Time `json:"expires_at,omitempty"` + // LastUsed holds the value of the "last_used" field. + LastUsed *time.Time `json:"last_used,omitempty"` + // Created holds the value of the "created" field. + Created time.Time `json:"created,omitempty"` + selectValues sql.SelectValues +} + +// scanValues returns the types for scanning values from sql.Rows. +func (*ApiKey) scanValues(columns []string) ([]any, error) { + values := make([]any, len(columns)) + for i := range columns { + switch columns[i] { + case apikey.FieldRevoked: + values[i] = new(sql.NullBool) + case apikey.FieldName, apikey.FieldPrefix, apikey.FieldKeyHash, apikey.FieldScopes: + values[i] = new(sql.NullString) + case apikey.FieldExpiresAt, apikey.FieldLastUsed, apikey.FieldCreated: + values[i] = new(sql.NullTime) + case apikey.FieldID, apikey.FieldUserID: + values[i] = new(uuid.UUID) + default: + values[i] = new(sql.UnknownType) + } + } + return values, nil +} + +// assignValues assigns the values that were returned from sql.Rows (after scanning) +// to the ApiKey fields. +func (_m *ApiKey) assignValues(columns []string, values []any) error { + if m, n := len(values), len(columns); m < n { + return fmt.Errorf("mismatch number of scan values: %d != %d", m, n) + } + for i := range columns { + switch columns[i] { + case apikey.FieldID: + if value, ok := values[i].(*uuid.UUID); !ok { + return fmt.Errorf("unexpected type %T for field id", values[i]) + } else if value != nil { + _m.ID = *value + } + case apikey.FieldUserID: + if value, ok := values[i].(*uuid.UUID); !ok { + return fmt.Errorf("unexpected type %T for field user_id", values[i]) + } else if value != nil { + _m.UserID = *value + } + case apikey.FieldName: + if value, ok := values[i].(*sql.NullString); !ok { + return fmt.Errorf("unexpected type %T for field name", values[i]) + } else if value.Valid { + _m.Name = value.String + } + case apikey.FieldPrefix: + if value, ok := values[i].(*sql.NullString); !ok { + return fmt.Errorf("unexpected type %T for field prefix", values[i]) + } else if value.Valid { + _m.Prefix = value.String + } + case apikey.FieldKeyHash: + if value, ok := values[i].(*sql.NullString); !ok { + return fmt.Errorf("unexpected type %T for field key_hash", values[i]) + } else if value.Valid { + _m.KeyHash = value.String + } + case apikey.FieldScopes: + if value, ok := values[i].(*sql.NullString); !ok { + return fmt.Errorf("unexpected type %T for field scopes", values[i]) + } else if value.Valid { + _m.Scopes = value.String + } + case apikey.FieldRevoked: + if value, ok := values[i].(*sql.NullBool); !ok { + return fmt.Errorf("unexpected type %T for field revoked", values[i]) + } else if value.Valid { + _m.Revoked = value.Bool + } + case apikey.FieldExpiresAt: + if value, ok := values[i].(*sql.NullTime); !ok { + return fmt.Errorf("unexpected type %T for field expires_at", values[i]) + } else if value.Valid { + _m.ExpiresAt = new(time.Time) + *_m.ExpiresAt = value.Time + } + case apikey.FieldLastUsed: + if value, ok := values[i].(*sql.NullTime); !ok { + return fmt.Errorf("unexpected type %T for field last_used", values[i]) + } else if value.Valid { + _m.LastUsed = new(time.Time) + *_m.LastUsed = value.Time + } + case apikey.FieldCreated: + if value, ok := values[i].(*sql.NullTime); !ok { + return fmt.Errorf("unexpected type %T for field created", values[i]) + } else if value.Valid { + _m.Created = value.Time + } + default: + _m.selectValues.Set(columns[i], values[i]) + } + } + return nil +} + +// Value returns the ent.Value that was dynamically selected and assigned to the ApiKey. +// This includes values selected through modifiers, order, etc. +func (_m *ApiKey) Value(name string) (ent.Value, error) { + return _m.selectValues.Get(name) +} + +// Update returns a builder for updating this ApiKey. +// Note that you need to call ApiKey.Unwrap() before calling this method if this ApiKey +// was returned from a transaction, and the transaction was committed or rolled back. +func (_m *ApiKey) Update() *ApiKeyUpdateOne { + return NewApiKeyClient(_m.config).UpdateOne(_m) +} + +// Unwrap unwraps the ApiKey entity that was returned from a transaction after it was closed, +// so that all future queries will be executed through the driver which created the transaction. +func (_m *ApiKey) Unwrap() *ApiKey { + _tx, ok := _m.config.driver.(*txDriver) + if !ok { + panic("ent: ApiKey is not a transactional entity") + } + _m.config.driver = _tx.drv + return _m +} + +// String implements the fmt.Stringer. +func (_m *ApiKey) String() string { + var builder strings.Builder + builder.WriteString("ApiKey(") + builder.WriteString(fmt.Sprintf("id=%v, ", _m.ID)) + builder.WriteString("user_id=") + builder.WriteString(fmt.Sprintf("%v", _m.UserID)) + builder.WriteString(", ") + builder.WriteString("name=") + builder.WriteString(_m.Name) + builder.WriteString(", ") + builder.WriteString("prefix=") + builder.WriteString(_m.Prefix) + builder.WriteString(", ") + builder.WriteString("key_hash=") + builder.WriteString(", ") + builder.WriteString("scopes=") + builder.WriteString(_m.Scopes) + builder.WriteString(", ") + builder.WriteString("revoked=") + builder.WriteString(fmt.Sprintf("%v", _m.Revoked)) + builder.WriteString(", ") + if v := _m.ExpiresAt; v != nil { + builder.WriteString("expires_at=") + builder.WriteString(v.Format(time.ANSIC)) + } + builder.WriteString(", ") + if v := _m.LastUsed; v != nil { + builder.WriteString("last_used=") + builder.WriteString(v.Format(time.ANSIC)) + } + builder.WriteString(", ") + builder.WriteString("created=") + builder.WriteString(_m.Created.Format(time.ANSIC)) + builder.WriteByte(')') + return builder.String() +} + +// ApiKeys is a parsable slice of ApiKey. +type ApiKeys []*ApiKey diff --git a/pkg/ent/apikey/apikey.go b/pkg/ent/apikey/apikey.go new file mode 100644 index 000000000..56263a2dd --- /dev/null +++ b/pkg/ent/apikey/apikey.go @@ -0,0 +1,125 @@ +// Code generated by ent, DO NOT EDIT. + +package apikey + +import ( + "time" + + "entgo.io/ent/dialect/sql" + "github.com/google/uuid" +) + +const ( + // Label holds the string label denoting the apikey type in the database. + Label = "api_key" + // FieldID holds the string denoting the id field in the database. + FieldID = "id" + // FieldUserID holds the string denoting the user_id field in the database. + FieldUserID = "user_id" + // FieldName holds the string denoting the name field in the database. + FieldName = "name" + // FieldPrefix holds the string denoting the prefix field in the database. + FieldPrefix = "prefix" + // FieldKeyHash holds the string denoting the key_hash field in the database. + FieldKeyHash = "key_hash" + // FieldScopes holds the string denoting the scopes field in the database. + FieldScopes = "scopes" + // FieldRevoked holds the string denoting the revoked field in the database. + FieldRevoked = "revoked" + // FieldExpiresAt holds the string denoting the expires_at field in the database. + FieldExpiresAt = "expires_at" + // FieldLastUsed holds the string denoting the last_used field in the database. + FieldLastUsed = "last_used" + // FieldCreated holds the string denoting the created field in the database. + FieldCreated = "created" + // Table holds the table name of the apikey in the database. + Table = "api_keys" +) + +// Columns holds all SQL columns for apikey fields. +var Columns = []string{ + FieldID, + FieldUserID, + FieldName, + FieldPrefix, + FieldKeyHash, + FieldScopes, + FieldRevoked, + FieldExpiresAt, + FieldLastUsed, + FieldCreated, +} + +// ValidColumn reports if the column name is valid (part of the table columns). +func ValidColumn(column string) bool { + for i := range Columns { + if column == Columns[i] { + return true + } + } + return false +} + +var ( + // KeyHashValidator is a validator for the "key_hash" field. It is called by the builders before save. + KeyHashValidator func(string) error + // DefaultRevoked holds the default value on creation for the "revoked" field. + DefaultRevoked bool + // DefaultCreated holds the default value on creation for the "created" field. + DefaultCreated func() time.Time + // DefaultID holds the default value on creation for the "id" field. + DefaultID func() uuid.UUID +) + +// OrderOption defines the ordering options for the ApiKey queries. +type OrderOption func(*sql.Selector) + +// ByID orders the results by the id field. +func ByID(opts ...sql.OrderTermOption) OrderOption { + return sql.OrderByField(FieldID, opts...).ToFunc() +} + +// ByUserID orders the results by the user_id field. +func ByUserID(opts ...sql.OrderTermOption) OrderOption { + return sql.OrderByField(FieldUserID, opts...).ToFunc() +} + +// ByName orders the results by the name field. +func ByName(opts ...sql.OrderTermOption) OrderOption { + return sql.OrderByField(FieldName, opts...).ToFunc() +} + +// ByPrefix orders the results by the prefix field. +func ByPrefix(opts ...sql.OrderTermOption) OrderOption { + return sql.OrderByField(FieldPrefix, opts...).ToFunc() +} + +// ByKeyHash orders the results by the key_hash field. +func ByKeyHash(opts ...sql.OrderTermOption) OrderOption { + return sql.OrderByField(FieldKeyHash, opts...).ToFunc() +} + +// ByScopes orders the results by the scopes field. +func ByScopes(opts ...sql.OrderTermOption) OrderOption { + return sql.OrderByField(FieldScopes, opts...).ToFunc() +} + +// ByRevoked orders the results by the revoked field. +func ByRevoked(opts ...sql.OrderTermOption) OrderOption { + return sql.OrderByField(FieldRevoked, opts...).ToFunc() +} + +// ByExpiresAt orders the results by the expires_at field. +func ByExpiresAt(opts ...sql.OrderTermOption) OrderOption { + return sql.OrderByField(FieldExpiresAt, opts...).ToFunc() +} + +// ByLastUsed orders the results by the last_used field. +func ByLastUsed(opts ...sql.OrderTermOption) OrderOption { + return sql.OrderByField(FieldLastUsed, opts...).ToFunc() +} + +// ByCreated orders the results by the created field. +func ByCreated(opts ...sql.OrderTermOption) OrderOption { + return sql.OrderByField(FieldCreated, opts...).ToFunc() +} diff --git a/pkg/ent/apikey/where.go b/pkg/ent/apikey/where.go new file mode 100644 index 000000000..7683a7cc3 --- /dev/null +++ b/pkg/ent/apikey/where.go @@ -0,0 +1,596 @@ +// Code generated by ent, DO NOT EDIT. + +package apikey + +import ( + "time" + + "entgo.io/ent/dialect/sql" + "github.com/GoogleCloudPlatform/scion/pkg/ent/predicate" + "github.com/google/uuid" +) + +// ID filters vertices based on their ID field. +func ID(id uuid.UUID) predicate.ApiKey { + return predicate.ApiKey(sql.FieldEQ(FieldID, id)) +} + +// IDEQ applies the EQ predicate on the ID field. +func IDEQ(id uuid.UUID) predicate.ApiKey { + return predicate.ApiKey(sql.FieldEQ(FieldID, id)) +} + +// IDNEQ applies the NEQ predicate on the ID field. +func IDNEQ(id uuid.UUID) predicate.ApiKey { + return predicate.ApiKey(sql.FieldNEQ(FieldID, id)) +} + +// IDIn applies the In predicate on the ID field. +func IDIn(ids ...uuid.UUID) predicate.ApiKey { + return predicate.ApiKey(sql.FieldIn(FieldID, ids...)) +} + +// IDNotIn applies the NotIn predicate on the ID field. +func IDNotIn(ids ...uuid.UUID) predicate.ApiKey { + return predicate.ApiKey(sql.FieldNotIn(FieldID, ids...)) +} + +// IDGT applies the GT predicate on the ID field. +func IDGT(id uuid.UUID) predicate.ApiKey { + return predicate.ApiKey(sql.FieldGT(FieldID, id)) +} + +// IDGTE applies the GTE predicate on the ID field. +func IDGTE(id uuid.UUID) predicate.ApiKey { + return predicate.ApiKey(sql.FieldGTE(FieldID, id)) +} + +// IDLT applies the LT predicate on the ID field. +func IDLT(id uuid.UUID) predicate.ApiKey { + return predicate.ApiKey(sql.FieldLT(FieldID, id)) +} + +// IDLTE applies the LTE predicate on the ID field. +func IDLTE(id uuid.UUID) predicate.ApiKey { + return predicate.ApiKey(sql.FieldLTE(FieldID, id)) +} + +// UserID applies equality check predicate on the "user_id" field. It's identical to UserIDEQ. +func UserID(v uuid.UUID) predicate.ApiKey { + return predicate.ApiKey(sql.FieldEQ(FieldUserID, v)) +} + +// Name applies equality check predicate on the "name" field. It's identical to NameEQ. +func Name(v string) predicate.ApiKey { + return predicate.ApiKey(sql.FieldEQ(FieldName, v)) +} + +// Prefix applies equality check predicate on the "prefix" field. It's identical to PrefixEQ. +func Prefix(v string) predicate.ApiKey { + return predicate.ApiKey(sql.FieldEQ(FieldPrefix, v)) +} + +// KeyHash applies equality check predicate on the "key_hash" field. It's identical to KeyHashEQ. +func KeyHash(v string) predicate.ApiKey { + return predicate.ApiKey(sql.FieldEQ(FieldKeyHash, v)) +} + +// Scopes applies equality check predicate on the "scopes" field. It's identical to ScopesEQ. +func Scopes(v string) predicate.ApiKey { + return predicate.ApiKey(sql.FieldEQ(FieldScopes, v)) +} + +// Revoked applies equality check predicate on the "revoked" field. It's identical to RevokedEQ. +func Revoked(v bool) predicate.ApiKey { + return predicate.ApiKey(sql.FieldEQ(FieldRevoked, v)) +} + +// ExpiresAt applies equality check predicate on the "expires_at" field. It's identical to ExpiresAtEQ. +func ExpiresAt(v time.Time) predicate.ApiKey { + return predicate.ApiKey(sql.FieldEQ(FieldExpiresAt, v)) +} + +// LastUsed applies equality check predicate on the "last_used" field. It's identical to LastUsedEQ. +func LastUsed(v time.Time) predicate.ApiKey { + return predicate.ApiKey(sql.FieldEQ(FieldLastUsed, v)) +} + +// Created applies equality check predicate on the "created" field. It's identical to CreatedEQ. +func Created(v time.Time) predicate.ApiKey { + return predicate.ApiKey(sql.FieldEQ(FieldCreated, v)) +} + +// UserIDEQ applies the EQ predicate on the "user_id" field. +func UserIDEQ(v uuid.UUID) predicate.ApiKey { + return predicate.ApiKey(sql.FieldEQ(FieldUserID, v)) +} + +// UserIDNEQ applies the NEQ predicate on the "user_id" field. +func UserIDNEQ(v uuid.UUID) predicate.ApiKey { + return predicate.ApiKey(sql.FieldNEQ(FieldUserID, v)) +} + +// UserIDIn applies the In predicate on the "user_id" field. +func UserIDIn(vs ...uuid.UUID) predicate.ApiKey { + return predicate.ApiKey(sql.FieldIn(FieldUserID, vs...)) +} + +// UserIDNotIn applies the NotIn predicate on the "user_id" field. +func UserIDNotIn(vs ...uuid.UUID) predicate.ApiKey { + return predicate.ApiKey(sql.FieldNotIn(FieldUserID, vs...)) +} + +// UserIDGT applies the GT predicate on the "user_id" field. +func UserIDGT(v uuid.UUID) predicate.ApiKey { + return predicate.ApiKey(sql.FieldGT(FieldUserID, v)) +} + +// UserIDGTE applies the GTE predicate on the "user_id" field. +func UserIDGTE(v uuid.UUID) predicate.ApiKey { + return predicate.ApiKey(sql.FieldGTE(FieldUserID, v)) +} + +// UserIDLT applies the LT predicate on the "user_id" field. +func UserIDLT(v uuid.UUID) predicate.ApiKey { + return predicate.ApiKey(sql.FieldLT(FieldUserID, v)) +} + +// UserIDLTE applies the LTE predicate on the "user_id" field. +func UserIDLTE(v uuid.UUID) predicate.ApiKey { + return predicate.ApiKey(sql.FieldLTE(FieldUserID, v)) +} + +// NameEQ applies the EQ predicate on the "name" field. +func NameEQ(v string) predicate.ApiKey { + return predicate.ApiKey(sql.FieldEQ(FieldName, v)) +} + +// NameNEQ applies the NEQ predicate on the "name" field. +func NameNEQ(v string) predicate.ApiKey { + return predicate.ApiKey(sql.FieldNEQ(FieldName, v)) +} + +// NameIn applies the In predicate on the "name" field. +func NameIn(vs ...string) predicate.ApiKey { + return predicate.ApiKey(sql.FieldIn(FieldName, vs...)) +} + +// NameNotIn applies the NotIn predicate on the "name" field. +func NameNotIn(vs ...string) predicate.ApiKey { + return predicate.ApiKey(sql.FieldNotIn(FieldName, vs...)) +} + +// NameGT applies the GT predicate on the "name" field. +func NameGT(v string) predicate.ApiKey { + return predicate.ApiKey(sql.FieldGT(FieldName, v)) +} + +// NameGTE applies the GTE predicate on the "name" field. +func NameGTE(v string) predicate.ApiKey { + return predicate.ApiKey(sql.FieldGTE(FieldName, v)) +} + +// NameLT applies the LT predicate on the "name" field. +func NameLT(v string) predicate.ApiKey { + return predicate.ApiKey(sql.FieldLT(FieldName, v)) +} + +// NameLTE applies the LTE predicate on the "name" field. +func NameLTE(v string) predicate.ApiKey { + return predicate.ApiKey(sql.FieldLTE(FieldName, v)) +} + +// NameContains applies the Contains predicate on the "name" field. +func NameContains(v string) predicate.ApiKey { + return predicate.ApiKey(sql.FieldContains(FieldName, v)) +} + +// NameHasPrefix applies the HasPrefix predicate on the "name" field. +func NameHasPrefix(v string) predicate.ApiKey { + return predicate.ApiKey(sql.FieldHasPrefix(FieldName, v)) +} + +// NameHasSuffix applies the HasSuffix predicate on the "name" field. +func NameHasSuffix(v string) predicate.ApiKey { + return predicate.ApiKey(sql.FieldHasSuffix(FieldName, v)) +} + +// NameIsNil applies the IsNil predicate on the "name" field. +func NameIsNil() predicate.ApiKey { + return predicate.ApiKey(sql.FieldIsNull(FieldName)) +} + +// NameNotNil applies the NotNil predicate on the "name" field. +func NameNotNil() predicate.ApiKey { + return predicate.ApiKey(sql.FieldNotNull(FieldName)) +} + +// NameEqualFold applies the EqualFold predicate on the "name" field. +func NameEqualFold(v string) predicate.ApiKey { + return predicate.ApiKey(sql.FieldEqualFold(FieldName, v)) +} + +// NameContainsFold applies the ContainsFold predicate on the "name" field. +func NameContainsFold(v string) predicate.ApiKey { + return predicate.ApiKey(sql.FieldContainsFold(FieldName, v)) +} + +// PrefixEQ applies the EQ predicate on the "prefix" field. +func PrefixEQ(v string) predicate.ApiKey { + return predicate.ApiKey(sql.FieldEQ(FieldPrefix, v)) +} + +// PrefixNEQ applies the NEQ predicate on the "prefix" field. +func PrefixNEQ(v string) predicate.ApiKey { + return predicate.ApiKey(sql.FieldNEQ(FieldPrefix, v)) +} + +// PrefixIn applies the In predicate on the "prefix" field. +func PrefixIn(vs ...string) predicate.ApiKey { + return predicate.ApiKey(sql.FieldIn(FieldPrefix, vs...)) +} + +// PrefixNotIn applies the NotIn predicate on the "prefix" field. +func PrefixNotIn(vs ...string) predicate.ApiKey { + return predicate.ApiKey(sql.FieldNotIn(FieldPrefix, vs...)) +} + +// PrefixGT applies the GT predicate on the "prefix" field. +func PrefixGT(v string) predicate.ApiKey { + return predicate.ApiKey(sql.FieldGT(FieldPrefix, v)) +} + +// PrefixGTE applies the GTE predicate on the "prefix" field. +func PrefixGTE(v string) predicate.ApiKey { + return predicate.ApiKey(sql.FieldGTE(FieldPrefix, v)) +} + +// PrefixLT applies the LT predicate on the "prefix" field. +func PrefixLT(v string) predicate.ApiKey { + return predicate.ApiKey(sql.FieldLT(FieldPrefix, v)) +} + +// PrefixLTE applies the LTE predicate on the "prefix" field. +func PrefixLTE(v string) predicate.ApiKey { + return predicate.ApiKey(sql.FieldLTE(FieldPrefix, v)) +} + +// PrefixContains applies the Contains predicate on the "prefix" field. +func PrefixContains(v string) predicate.ApiKey { + return predicate.ApiKey(sql.FieldContains(FieldPrefix, v)) +} + +// PrefixHasPrefix applies the HasPrefix predicate on the "prefix" field. +func PrefixHasPrefix(v string) predicate.ApiKey { + return predicate.ApiKey(sql.FieldHasPrefix(FieldPrefix, v)) +} + +// PrefixHasSuffix applies the HasSuffix predicate on the "prefix" field. +func PrefixHasSuffix(v string) predicate.ApiKey { + return predicate.ApiKey(sql.FieldHasSuffix(FieldPrefix, v)) +} + +// PrefixIsNil applies the IsNil predicate on the "prefix" field. +func PrefixIsNil() predicate.ApiKey { + return predicate.ApiKey(sql.FieldIsNull(FieldPrefix)) +} + +// PrefixNotNil applies the NotNil predicate on the "prefix" field. +func PrefixNotNil() predicate.ApiKey { + return predicate.ApiKey(sql.FieldNotNull(FieldPrefix)) +} + +// PrefixEqualFold applies the EqualFold predicate on the "prefix" field. +func PrefixEqualFold(v string) predicate.ApiKey { + return predicate.ApiKey(sql.FieldEqualFold(FieldPrefix, v)) +} + +// PrefixContainsFold applies the ContainsFold predicate on the "prefix" field. +func PrefixContainsFold(v string) predicate.ApiKey { + return predicate.ApiKey(sql.FieldContainsFold(FieldPrefix, v)) +} + +// KeyHashEQ applies the EQ predicate on the "key_hash" field. +func KeyHashEQ(v string) predicate.ApiKey { + return predicate.ApiKey(sql.FieldEQ(FieldKeyHash, v)) +} + +// KeyHashNEQ applies the NEQ predicate on the "key_hash" field. +func KeyHashNEQ(v string) predicate.ApiKey { + return predicate.ApiKey(sql.FieldNEQ(FieldKeyHash, v)) +} + +// KeyHashIn applies the In predicate on the "key_hash" field. +func KeyHashIn(vs ...string) predicate.ApiKey { + return predicate.ApiKey(sql.FieldIn(FieldKeyHash, vs...)) +} + +// KeyHashNotIn applies the NotIn predicate on the "key_hash" field. +func KeyHashNotIn(vs ...string) predicate.ApiKey { + return predicate.ApiKey(sql.FieldNotIn(FieldKeyHash, vs...)) +} + +// KeyHashGT applies the GT predicate on the "key_hash" field. +func KeyHashGT(v string) predicate.ApiKey { + return predicate.ApiKey(sql.FieldGT(FieldKeyHash, v)) +} + +// KeyHashGTE applies the GTE predicate on the "key_hash" field. +func KeyHashGTE(v string) predicate.ApiKey { + return predicate.ApiKey(sql.FieldGTE(FieldKeyHash, v)) +} + +// KeyHashLT applies the LT predicate on the "key_hash" field. +func KeyHashLT(v string) predicate.ApiKey { + return predicate.ApiKey(sql.FieldLT(FieldKeyHash, v)) +} + +// KeyHashLTE applies the LTE predicate on the "key_hash" field. +func KeyHashLTE(v string) predicate.ApiKey { + return predicate.ApiKey(sql.FieldLTE(FieldKeyHash, v)) +} + +// KeyHashContains applies the Contains predicate on the "key_hash" field. +func KeyHashContains(v string) predicate.ApiKey { + return predicate.ApiKey(sql.FieldContains(FieldKeyHash, v)) +} + +// KeyHashHasPrefix applies the HasPrefix predicate on the "key_hash" field. +func KeyHashHasPrefix(v string) predicate.ApiKey { + return predicate.ApiKey(sql.FieldHasPrefix(FieldKeyHash, v)) +} + +// KeyHashHasSuffix applies the HasSuffix predicate on the "key_hash" field. +func KeyHashHasSuffix(v string) predicate.ApiKey { + return predicate.ApiKey(sql.FieldHasSuffix(FieldKeyHash, v)) +} + +// KeyHashEqualFold applies the EqualFold predicate on the "key_hash" field. +func KeyHashEqualFold(v string) predicate.ApiKey { + return predicate.ApiKey(sql.FieldEqualFold(FieldKeyHash, v)) +} + +// KeyHashContainsFold applies the ContainsFold predicate on the "key_hash" field. +func KeyHashContainsFold(v string) predicate.ApiKey { + return predicate.ApiKey(sql.FieldContainsFold(FieldKeyHash, v)) +} + +// ScopesEQ applies the EQ predicate on the "scopes" field. +func ScopesEQ(v string) predicate.ApiKey { + return predicate.ApiKey(sql.FieldEQ(FieldScopes, v)) +} + +// ScopesNEQ applies the NEQ predicate on the "scopes" field. +func ScopesNEQ(v string) predicate.ApiKey { + return predicate.ApiKey(sql.FieldNEQ(FieldScopes, v)) +} + +// ScopesIn applies the In predicate on the "scopes" field. +func ScopesIn(vs ...string) predicate.ApiKey { + return predicate.ApiKey(sql.FieldIn(FieldScopes, vs...)) +} + +// ScopesNotIn applies the NotIn predicate on the "scopes" field. +func ScopesNotIn(vs ...string) predicate.ApiKey { + return predicate.ApiKey(sql.FieldNotIn(FieldScopes, vs...)) +} + +// ScopesGT applies the GT predicate on the "scopes" field. +func ScopesGT(v string) predicate.ApiKey { + return predicate.ApiKey(sql.FieldGT(FieldScopes, v)) +} + +// ScopesGTE applies the GTE predicate on the "scopes" field. +func ScopesGTE(v string) predicate.ApiKey { + return predicate.ApiKey(sql.FieldGTE(FieldScopes, v)) +} + +// ScopesLT applies the LT predicate on the "scopes" field. +func ScopesLT(v string) predicate.ApiKey { + return predicate.ApiKey(sql.FieldLT(FieldScopes, v)) +} + +// ScopesLTE applies the LTE predicate on the "scopes" field. +func ScopesLTE(v string) predicate.ApiKey { + return predicate.ApiKey(sql.FieldLTE(FieldScopes, v)) +} + +// ScopesContains applies the Contains predicate on the "scopes" field. +func ScopesContains(v string) predicate.ApiKey { + return predicate.ApiKey(sql.FieldContains(FieldScopes, v)) +} + +// ScopesHasPrefix applies the HasPrefix predicate on the "scopes" field. +func ScopesHasPrefix(v string) predicate.ApiKey { + return predicate.ApiKey(sql.FieldHasPrefix(FieldScopes, v)) +} + +// ScopesHasSuffix applies the HasSuffix predicate on the "scopes" field. +func ScopesHasSuffix(v string) predicate.ApiKey { + return predicate.ApiKey(sql.FieldHasSuffix(FieldScopes, v)) +} + +// ScopesIsNil applies the IsNil predicate on the "scopes" field. +func ScopesIsNil() predicate.ApiKey { + return predicate.ApiKey(sql.FieldIsNull(FieldScopes)) +} + +// ScopesNotNil applies the NotNil predicate on the "scopes" field. +func ScopesNotNil() predicate.ApiKey { + return predicate.ApiKey(sql.FieldNotNull(FieldScopes)) +} + +// ScopesEqualFold applies the EqualFold predicate on the "scopes" field. +func ScopesEqualFold(v string) predicate.ApiKey { + return predicate.ApiKey(sql.FieldEqualFold(FieldScopes, v)) +} + +// ScopesContainsFold applies the ContainsFold predicate on the "scopes" field. +func ScopesContainsFold(v string) predicate.ApiKey { + return predicate.ApiKey(sql.FieldContainsFold(FieldScopes, v)) +} + +// RevokedEQ applies the EQ predicate on the "revoked" field. +func RevokedEQ(v bool) predicate.ApiKey { + return predicate.ApiKey(sql.FieldEQ(FieldRevoked, v)) +} + +// RevokedNEQ applies the NEQ predicate on the "revoked" field. +func RevokedNEQ(v bool) predicate.ApiKey { + return predicate.ApiKey(sql.FieldNEQ(FieldRevoked, v)) +} + +// ExpiresAtEQ applies the EQ predicate on the "expires_at" field. +func ExpiresAtEQ(v time.Time) predicate.ApiKey { + return predicate.ApiKey(sql.FieldEQ(FieldExpiresAt, v)) +} + +// ExpiresAtNEQ applies the NEQ predicate on the "expires_at" field. +func ExpiresAtNEQ(v time.Time) predicate.ApiKey { + return predicate.ApiKey(sql.FieldNEQ(FieldExpiresAt, v)) +} + +// ExpiresAtIn applies the In predicate on the "expires_at" field. +func ExpiresAtIn(vs ...time.Time) predicate.ApiKey { + return predicate.ApiKey(sql.FieldIn(FieldExpiresAt, vs...)) +} + +// ExpiresAtNotIn applies the NotIn predicate on the "expires_at" field. +func ExpiresAtNotIn(vs ...time.Time) predicate.ApiKey { + return predicate.ApiKey(sql.FieldNotIn(FieldExpiresAt, vs...)) +} + +// ExpiresAtGT applies the GT predicate on the "expires_at" field. +func ExpiresAtGT(v time.Time) predicate.ApiKey { + return predicate.ApiKey(sql.FieldGT(FieldExpiresAt, v)) +} + +// ExpiresAtGTE applies the GTE predicate on the "expires_at" field. +func ExpiresAtGTE(v time.Time) predicate.ApiKey { + return predicate.ApiKey(sql.FieldGTE(FieldExpiresAt, v)) +} + +// ExpiresAtLT applies the LT predicate on the "expires_at" field. +func ExpiresAtLT(v time.Time) predicate.ApiKey { + return predicate.ApiKey(sql.FieldLT(FieldExpiresAt, v)) +} + +// ExpiresAtLTE applies the LTE predicate on the "expires_at" field. +func ExpiresAtLTE(v time.Time) predicate.ApiKey { + return predicate.ApiKey(sql.FieldLTE(FieldExpiresAt, v)) +} + +// ExpiresAtIsNil applies the IsNil predicate on the "expires_at" field. +func ExpiresAtIsNil() predicate.ApiKey { + return predicate.ApiKey(sql.FieldIsNull(FieldExpiresAt)) +} + +// ExpiresAtNotNil applies the NotNil predicate on the "expires_at" field. +func ExpiresAtNotNil() predicate.ApiKey { + return predicate.ApiKey(sql.FieldNotNull(FieldExpiresAt)) +} + +// LastUsedEQ applies the EQ predicate on the "last_used" field. +func LastUsedEQ(v time.Time) predicate.ApiKey { + return predicate.ApiKey(sql.FieldEQ(FieldLastUsed, v)) +} + +// LastUsedNEQ applies the NEQ predicate on the "last_used" field. +func LastUsedNEQ(v time.Time) predicate.ApiKey { + return predicate.ApiKey(sql.FieldNEQ(FieldLastUsed, v)) +} + +// LastUsedIn applies the In predicate on the "last_used" field. +func LastUsedIn(vs ...time.Time) predicate.ApiKey { + return predicate.ApiKey(sql.FieldIn(FieldLastUsed, vs...)) +} + +// LastUsedNotIn applies the NotIn predicate on the "last_used" field. +func LastUsedNotIn(vs ...time.Time) predicate.ApiKey { + return predicate.ApiKey(sql.FieldNotIn(FieldLastUsed, vs...)) +} + +// LastUsedGT applies the GT predicate on the "last_used" field. +func LastUsedGT(v time.Time) predicate.ApiKey { + return predicate.ApiKey(sql.FieldGT(FieldLastUsed, v)) +} + +// LastUsedGTE applies the GTE predicate on the "last_used" field. +func LastUsedGTE(v time.Time) predicate.ApiKey { + return predicate.ApiKey(sql.FieldGTE(FieldLastUsed, v)) +} + +// LastUsedLT applies the LT predicate on the "last_used" field. +func LastUsedLT(v time.Time) predicate.ApiKey { + return predicate.ApiKey(sql.FieldLT(FieldLastUsed, v)) +} + +// LastUsedLTE applies the LTE predicate on the "last_used" field. +func LastUsedLTE(v time.Time) predicate.ApiKey { + return predicate.ApiKey(sql.FieldLTE(FieldLastUsed, v)) +} + +// LastUsedIsNil applies the IsNil predicate on the "last_used" field. +func LastUsedIsNil() predicate.ApiKey { + return predicate.ApiKey(sql.FieldIsNull(FieldLastUsed)) +} + +// LastUsedNotNil applies the NotNil predicate on the "last_used" field. +func LastUsedNotNil() predicate.ApiKey { + return predicate.ApiKey(sql.FieldNotNull(FieldLastUsed)) +} + +// CreatedEQ applies the EQ predicate on the "created" field. +func CreatedEQ(v time.Time) predicate.ApiKey { + return predicate.ApiKey(sql.FieldEQ(FieldCreated, v)) +} + +// CreatedNEQ applies the NEQ predicate on the "created" field. +func CreatedNEQ(v time.Time) predicate.ApiKey { + return predicate.ApiKey(sql.FieldNEQ(FieldCreated, v)) +} + +// CreatedIn applies the In predicate on the "created" field. +func CreatedIn(vs ...time.Time) predicate.ApiKey { + return predicate.ApiKey(sql.FieldIn(FieldCreated, vs...)) +} + +// CreatedNotIn applies the NotIn predicate on the "created" field. +func CreatedNotIn(vs ...time.Time) predicate.ApiKey { + return predicate.ApiKey(sql.FieldNotIn(FieldCreated, vs...)) +} + +// CreatedGT applies the GT predicate on the "created" field. +func CreatedGT(v time.Time) predicate.ApiKey { + return predicate.ApiKey(sql.FieldGT(FieldCreated, v)) +} + +// CreatedGTE applies the GTE predicate on the "created" field. +func CreatedGTE(v time.Time) predicate.ApiKey { + return predicate.ApiKey(sql.FieldGTE(FieldCreated, v)) +} + +// CreatedLT applies the LT predicate on the "created" field. +func CreatedLT(v time.Time) predicate.ApiKey { + return predicate.ApiKey(sql.FieldLT(FieldCreated, v)) +} + +// CreatedLTE applies the LTE predicate on the "created" field. +func CreatedLTE(v time.Time) predicate.ApiKey { + return predicate.ApiKey(sql.FieldLTE(FieldCreated, v)) +} + +// And groups predicates with the AND operator between them. +func And(predicates ...predicate.ApiKey) predicate.ApiKey { + return predicate.ApiKey(sql.AndPredicates(predicates...)) +} + +// Or groups predicates with the OR operator between them. +func Or(predicates ...predicate.ApiKey) predicate.ApiKey { + return predicate.ApiKey(sql.OrPredicates(predicates...)) +} + +// Not applies the not operator on the given predicate. +func Not(p predicate.ApiKey) predicate.ApiKey { + return predicate.ApiKey(sql.NotPredicates(p)) +} diff --git a/pkg/ent/apikey_create.go b/pkg/ent/apikey_create.go new file mode 100644 index 000000000..d1fb42022 --- /dev/null +++ b/pkg/ent/apikey_create.go @@ -0,0 +1,1053 @@ +// Code generated by ent, DO NOT EDIT. + +package ent + +import ( + "context" + "errors" + "fmt" + "time" + + "entgo.io/ent/dialect" + "entgo.io/ent/dialect/sql" + "entgo.io/ent/dialect/sql/sqlgraph" + "entgo.io/ent/schema/field" + "github.com/GoogleCloudPlatform/scion/pkg/ent/apikey" + "github.com/google/uuid" +) + +// ApiKeyCreate is the builder for creating a ApiKey entity. +type ApiKeyCreate struct { + config + mutation *ApiKeyMutation + hooks []Hook + conflict []sql.ConflictOption +} + +// SetUserID sets the "user_id" field. +func (_c *ApiKeyCreate) SetUserID(v uuid.UUID) *ApiKeyCreate { + _c.mutation.SetUserID(v) + return _c +} + +// SetName sets the "name" field. +func (_c *ApiKeyCreate) SetName(v string) *ApiKeyCreate { + _c.mutation.SetName(v) + return _c +} + +// SetNillableName sets the "name" field if the given value is not nil. +func (_c *ApiKeyCreate) SetNillableName(v *string) *ApiKeyCreate { + if v != nil { + _c.SetName(*v) + } + return _c +} + +// SetPrefix sets the "prefix" field. +func (_c *ApiKeyCreate) SetPrefix(v string) *ApiKeyCreate { + _c.mutation.SetPrefix(v) + return _c +} + +// SetNillablePrefix sets the "prefix" field if the given value is not nil. +func (_c *ApiKeyCreate) SetNillablePrefix(v *string) *ApiKeyCreate { + if v != nil { + _c.SetPrefix(*v) + } + return _c +} + +// SetKeyHash sets the "key_hash" field. +func (_c *ApiKeyCreate) SetKeyHash(v string) *ApiKeyCreate { + _c.mutation.SetKeyHash(v) + return _c +} + +// SetScopes sets the "scopes" field. +func (_c *ApiKeyCreate) SetScopes(v string) *ApiKeyCreate { + _c.mutation.SetScopes(v) + return _c +} + +// SetNillableScopes sets the "scopes" field if the given value is not nil. +func (_c *ApiKeyCreate) SetNillableScopes(v *string) *ApiKeyCreate { + if v != nil { + _c.SetScopes(*v) + } + return _c +} + +// SetRevoked sets the "revoked" field. +func (_c *ApiKeyCreate) SetRevoked(v bool) *ApiKeyCreate { + _c.mutation.SetRevoked(v) + return _c +} + +// SetNillableRevoked sets the "revoked" field if the given value is not nil. +func (_c *ApiKeyCreate) SetNillableRevoked(v *bool) *ApiKeyCreate { + if v != nil { + _c.SetRevoked(*v) + } + return _c +} + +// SetExpiresAt sets the "expires_at" field. +func (_c *ApiKeyCreate) SetExpiresAt(v time.Time) *ApiKeyCreate { + _c.mutation.SetExpiresAt(v) + return _c +} + +// SetNillableExpiresAt sets the "expires_at" field if the given value is not nil. +func (_c *ApiKeyCreate) SetNillableExpiresAt(v *time.Time) *ApiKeyCreate { + if v != nil { + _c.SetExpiresAt(*v) + } + return _c +} + +// SetLastUsed sets the "last_used" field. +func (_c *ApiKeyCreate) SetLastUsed(v time.Time) *ApiKeyCreate { + _c.mutation.SetLastUsed(v) + return _c +} + +// SetNillableLastUsed sets the "last_used" field if the given value is not nil. +func (_c *ApiKeyCreate) SetNillableLastUsed(v *time.Time) *ApiKeyCreate { + if v != nil { + _c.SetLastUsed(*v) + } + return _c +} + +// SetCreated sets the "created" field. +func (_c *ApiKeyCreate) SetCreated(v time.Time) *ApiKeyCreate { + _c.mutation.SetCreated(v) + return _c +} + +// SetNillableCreated sets the "created" field if the given value is not nil. +func (_c *ApiKeyCreate) SetNillableCreated(v *time.Time) *ApiKeyCreate { + if v != nil { + _c.SetCreated(*v) + } + return _c +} + +// SetID sets the "id" field. +func (_c *ApiKeyCreate) SetID(v uuid.UUID) *ApiKeyCreate { + _c.mutation.SetID(v) + return _c +} + +// SetNillableID sets the "id" field if the given value is not nil. +func (_c *ApiKeyCreate) SetNillableID(v *uuid.UUID) *ApiKeyCreate { + if v != nil { + _c.SetID(*v) + } + return _c +} + +// Mutation returns the ApiKeyMutation object of the builder. +func (_c *ApiKeyCreate) Mutation() *ApiKeyMutation { + return _c.mutation +} + +// Save creates the ApiKey in the database. +func (_c *ApiKeyCreate) Save(ctx context.Context) (*ApiKey, error) { + _c.defaults() + return withHooks(ctx, _c.sqlSave, _c.mutation, _c.hooks) +} + +// SaveX calls Save and panics if Save returns an error. +func (_c *ApiKeyCreate) SaveX(ctx context.Context) *ApiKey { + v, err := _c.Save(ctx) + if err != nil { + panic(err) + } + return v +} + +// Exec executes the query. +func (_c *ApiKeyCreate) Exec(ctx context.Context) error { + _, err := _c.Save(ctx) + return err +} + +// ExecX is like Exec, but panics if an error occurs. +func (_c *ApiKeyCreate) ExecX(ctx context.Context) { + if err := _c.Exec(ctx); err != nil { + panic(err) + } +} + +// defaults sets the default values of the builder before save. +func (_c *ApiKeyCreate) defaults() { + if _, ok := _c.mutation.Revoked(); !ok { + v := apikey.DefaultRevoked + _c.mutation.SetRevoked(v) + } + if _, ok := _c.mutation.Created(); !ok { + v := apikey.DefaultCreated() + _c.mutation.SetCreated(v) + } + if _, ok := _c.mutation.ID(); !ok { + v := apikey.DefaultID() + _c.mutation.SetID(v) + } +} + +// check runs all checks and user-defined validators on the builder. +func (_c *ApiKeyCreate) check() error { + if _, ok := _c.mutation.UserID(); !ok { + return &ValidationError{Name: "user_id", err: errors.New(`ent: missing required field "ApiKey.user_id"`)} + } + if _, ok := _c.mutation.KeyHash(); !ok { + return &ValidationError{Name: "key_hash", err: errors.New(`ent: missing required field "ApiKey.key_hash"`)} + } + if v, ok := _c.mutation.KeyHash(); ok { + if err := apikey.KeyHashValidator(v); err != nil { + return &ValidationError{Name: "key_hash", err: fmt.Errorf(`ent: validator failed for field "ApiKey.key_hash": %w`, err)} + } + } + if _, ok := _c.mutation.Revoked(); !ok { + return &ValidationError{Name: "revoked", err: errors.New(`ent: missing required field "ApiKey.revoked"`)} + } + if _, ok := _c.mutation.Created(); !ok { + return &ValidationError{Name: "created", err: errors.New(`ent: missing required field "ApiKey.created"`)} + } + return nil +} + +func (_c *ApiKeyCreate) sqlSave(ctx context.Context) (*ApiKey, error) { + if err := _c.check(); err != nil { + return nil, err + } + _node, _spec := _c.createSpec() + if err := sqlgraph.CreateNode(ctx, _c.driver, _spec); err != nil { + if sqlgraph.IsConstraintError(err) { + err = &ConstraintError{msg: err.Error(), wrap: err} + } + return nil, err + } + if _spec.ID.Value != nil { + if id, ok := _spec.ID.Value.(*uuid.UUID); ok { + _node.ID = *id + } else if err := _node.ID.Scan(_spec.ID.Value); err != nil { + return nil, err + } + } + _c.mutation.id = &_node.ID + _c.mutation.done = true + return _node, nil +} + +func (_c *ApiKeyCreate) createSpec() (*ApiKey, *sqlgraph.CreateSpec) { + var ( + _node = &ApiKey{config: _c.config} + _spec = sqlgraph.NewCreateSpec(apikey.Table, sqlgraph.NewFieldSpec(apikey.FieldID, field.TypeUUID)) + ) + _spec.OnConflict = _c.conflict + if id, ok := _c.mutation.ID(); ok { + _node.ID = id + _spec.ID.Value = &id + } + if value, ok := _c.mutation.UserID(); ok { + _spec.SetField(apikey.FieldUserID, field.TypeUUID, value) + _node.UserID = value + } + if value, ok := _c.mutation.Name(); ok { + _spec.SetField(apikey.FieldName, field.TypeString, value) + _node.Name = value + } + if value, ok := _c.mutation.Prefix(); ok { + _spec.SetField(apikey.FieldPrefix, field.TypeString, value) + _node.Prefix = value + } + if value, ok := _c.mutation.KeyHash(); ok { + _spec.SetField(apikey.FieldKeyHash, field.TypeString, value) + _node.KeyHash = value + } + if value, ok := _c.mutation.Scopes(); ok { + _spec.SetField(apikey.FieldScopes, field.TypeString, value) + _node.Scopes = value + } + if value, ok := _c.mutation.Revoked(); ok { + _spec.SetField(apikey.FieldRevoked, field.TypeBool, value) + _node.Revoked = value + } + if value, ok := _c.mutation.ExpiresAt(); ok { + _spec.SetField(apikey.FieldExpiresAt, field.TypeTime, value) + _node.ExpiresAt = &value + } + if value, ok := _c.mutation.LastUsed(); ok { + _spec.SetField(apikey.FieldLastUsed, field.TypeTime, value) + _node.LastUsed = &value + } + if value, ok := _c.mutation.Created(); ok { + _spec.SetField(apikey.FieldCreated, field.TypeTime, value) + _node.Created = value + } + return _node, _spec +} + +// OnConflict allows configuring the `ON CONFLICT` / `ON DUPLICATE KEY` clause +// of the `INSERT` statement. For example: +// +// client.ApiKey.Create(). +// SetUserID(v). +// OnConflict( +// // Update the row with the new values +// // the was proposed for insertion. +// sql.ResolveWithNewValues(), +// ). +// // Override some of the fields with custom +// // update values. +// Update(func(u *ent.ApiKeyUpsert) { +// SetUserID(v+v). +// }). +// Exec(ctx) +func (_c *ApiKeyCreate) OnConflict(opts ...sql.ConflictOption) *ApiKeyUpsertOne { + _c.conflict = opts + return &ApiKeyUpsertOne{ + create: _c, + } +} + +// OnConflictColumns calls `OnConflict` and configures the columns +// as conflict target. Using this option is equivalent to using: +// +// client.ApiKey.Create(). +// OnConflict(sql.ConflictColumns(columns...)). +// Exec(ctx) +func (_c *ApiKeyCreate) OnConflictColumns(columns ...string) *ApiKeyUpsertOne { + _c.conflict = append(_c.conflict, sql.ConflictColumns(columns...)) + return &ApiKeyUpsertOne{ + create: _c, + } +} + +type ( + // ApiKeyUpsertOne is the builder for "upsert"-ing + // one ApiKey node. + ApiKeyUpsertOne struct { + create *ApiKeyCreate + } + + // ApiKeyUpsert is the "OnConflict" setter. + ApiKeyUpsert struct { + *sql.UpdateSet + } +) + +// SetUserID sets the "user_id" field. +func (u *ApiKeyUpsert) SetUserID(v uuid.UUID) *ApiKeyUpsert { + u.Set(apikey.FieldUserID, v) + return u +} + +// UpdateUserID sets the "user_id" field to the value that was provided on create. +func (u *ApiKeyUpsert) UpdateUserID() *ApiKeyUpsert { + u.SetExcluded(apikey.FieldUserID) + return u +} + +// SetName sets the "name" field. +func (u *ApiKeyUpsert) SetName(v string) *ApiKeyUpsert { + u.Set(apikey.FieldName, v) + return u +} + +// UpdateName sets the "name" field to the value that was provided on create. +func (u *ApiKeyUpsert) UpdateName() *ApiKeyUpsert { + u.SetExcluded(apikey.FieldName) + return u +} + +// ClearName clears the value of the "name" field. +func (u *ApiKeyUpsert) ClearName() *ApiKeyUpsert { + u.SetNull(apikey.FieldName) + return u +} + +// SetPrefix sets the "prefix" field. +func (u *ApiKeyUpsert) SetPrefix(v string) *ApiKeyUpsert { + u.Set(apikey.FieldPrefix, v) + return u +} + +// UpdatePrefix sets the "prefix" field to the value that was provided on create. +func (u *ApiKeyUpsert) UpdatePrefix() *ApiKeyUpsert { + u.SetExcluded(apikey.FieldPrefix) + return u +} + +// ClearPrefix clears the value of the "prefix" field. +func (u *ApiKeyUpsert) ClearPrefix() *ApiKeyUpsert { + u.SetNull(apikey.FieldPrefix) + return u +} + +// SetKeyHash sets the "key_hash" field. +func (u *ApiKeyUpsert) SetKeyHash(v string) *ApiKeyUpsert { + u.Set(apikey.FieldKeyHash, v) + return u +} + +// UpdateKeyHash sets the "key_hash" field to the value that was provided on create. +func (u *ApiKeyUpsert) UpdateKeyHash() *ApiKeyUpsert { + u.SetExcluded(apikey.FieldKeyHash) + return u +} + +// SetScopes sets the "scopes" field. +func (u *ApiKeyUpsert) SetScopes(v string) *ApiKeyUpsert { + u.Set(apikey.FieldScopes, v) + return u +} + +// UpdateScopes sets the "scopes" field to the value that was provided on create. +func (u *ApiKeyUpsert) UpdateScopes() *ApiKeyUpsert { + u.SetExcluded(apikey.FieldScopes) + return u +} + +// ClearScopes clears the value of the "scopes" field. +func (u *ApiKeyUpsert) ClearScopes() *ApiKeyUpsert { + u.SetNull(apikey.FieldScopes) + return u +} + +// SetRevoked sets the "revoked" field. +func (u *ApiKeyUpsert) SetRevoked(v bool) *ApiKeyUpsert { + u.Set(apikey.FieldRevoked, v) + return u +} + +// UpdateRevoked sets the "revoked" field to the value that was provided on create. +func (u *ApiKeyUpsert) UpdateRevoked() *ApiKeyUpsert { + u.SetExcluded(apikey.FieldRevoked) + return u +} + +// SetExpiresAt sets the "expires_at" field. +func (u *ApiKeyUpsert) SetExpiresAt(v time.Time) *ApiKeyUpsert { + u.Set(apikey.FieldExpiresAt, v) + return u +} + +// UpdateExpiresAt sets the "expires_at" field to the value that was provided on create. +func (u *ApiKeyUpsert) UpdateExpiresAt() *ApiKeyUpsert { + u.SetExcluded(apikey.FieldExpiresAt) + return u +} + +// ClearExpiresAt clears the value of the "expires_at" field. +func (u *ApiKeyUpsert) ClearExpiresAt() *ApiKeyUpsert { + u.SetNull(apikey.FieldExpiresAt) + return u +} + +// SetLastUsed sets the "last_used" field. +func (u *ApiKeyUpsert) SetLastUsed(v time.Time) *ApiKeyUpsert { + u.Set(apikey.FieldLastUsed, v) + return u +} + +// UpdateLastUsed sets the "last_used" field to the value that was provided on create. +func (u *ApiKeyUpsert) UpdateLastUsed() *ApiKeyUpsert { + u.SetExcluded(apikey.FieldLastUsed) + return u +} + +// ClearLastUsed clears the value of the "last_used" field. +func (u *ApiKeyUpsert) ClearLastUsed() *ApiKeyUpsert { + u.SetNull(apikey.FieldLastUsed) + return u +} + +// UpdateNewValues updates the mutable fields using the new values that were set on create except the ID field. +// Using this option is equivalent to using: +// +// client.ApiKey.Create(). +// OnConflict( +// sql.ResolveWithNewValues(), +// sql.ResolveWith(func(u *sql.UpdateSet) { +// u.SetIgnore(apikey.FieldID) +// }), +// ). +// Exec(ctx) +func (u *ApiKeyUpsertOne) UpdateNewValues() *ApiKeyUpsertOne { + u.create.conflict = append(u.create.conflict, sql.ResolveWithNewValues()) + u.create.conflict = append(u.create.conflict, sql.ResolveWith(func(s *sql.UpdateSet) { + if _, exists := u.create.mutation.ID(); exists { + s.SetIgnore(apikey.FieldID) + } + if _, exists := u.create.mutation.Created(); exists { + s.SetIgnore(apikey.FieldCreated) + } + })) + return u +} + +// Ignore sets each column to itself in case of conflict. +// Using this option is equivalent to using: +// +// client.ApiKey.Create(). +// OnConflict(sql.ResolveWithIgnore()). +// Exec(ctx) +func (u *ApiKeyUpsertOne) Ignore() *ApiKeyUpsertOne { + u.create.conflict = append(u.create.conflict, sql.ResolveWithIgnore()) + return u +} + +// DoNothing configures the conflict_action to `DO NOTHING`. +// Supported only by SQLite and PostgreSQL. +func (u *ApiKeyUpsertOne) DoNothing() *ApiKeyUpsertOne { + u.create.conflict = append(u.create.conflict, sql.DoNothing()) + return u +} + +// Update allows overriding fields `UPDATE` values. See the ApiKeyCreate.OnConflict +// documentation for more info. +func (u *ApiKeyUpsertOne) Update(set func(*ApiKeyUpsert)) *ApiKeyUpsertOne { + u.create.conflict = append(u.create.conflict, sql.ResolveWith(func(update *sql.UpdateSet) { + set(&ApiKeyUpsert{UpdateSet: update}) + })) + return u +} + +// SetUserID sets the "user_id" field. +func (u *ApiKeyUpsertOne) SetUserID(v uuid.UUID) *ApiKeyUpsertOne { + return u.Update(func(s *ApiKeyUpsert) { + s.SetUserID(v) + }) +} + +// UpdateUserID sets the "user_id" field to the value that was provided on create. +func (u *ApiKeyUpsertOne) UpdateUserID() *ApiKeyUpsertOne { + return u.Update(func(s *ApiKeyUpsert) { + s.UpdateUserID() + }) +} + +// SetName sets the "name" field. +func (u *ApiKeyUpsertOne) SetName(v string) *ApiKeyUpsertOne { + return u.Update(func(s *ApiKeyUpsert) { + s.SetName(v) + }) +} + +// UpdateName sets the "name" field to the value that was provided on create. +func (u *ApiKeyUpsertOne) UpdateName() *ApiKeyUpsertOne { + return u.Update(func(s *ApiKeyUpsert) { + s.UpdateName() + }) +} + +// ClearName clears the value of the "name" field. +func (u *ApiKeyUpsertOne) ClearName() *ApiKeyUpsertOne { + return u.Update(func(s *ApiKeyUpsert) { + s.ClearName() + }) +} + +// SetPrefix sets the "prefix" field. +func (u *ApiKeyUpsertOne) SetPrefix(v string) *ApiKeyUpsertOne { + return u.Update(func(s *ApiKeyUpsert) { + s.SetPrefix(v) + }) +} + +// UpdatePrefix sets the "prefix" field to the value that was provided on create. +func (u *ApiKeyUpsertOne) UpdatePrefix() *ApiKeyUpsertOne { + return u.Update(func(s *ApiKeyUpsert) { + s.UpdatePrefix() + }) +} + +// ClearPrefix clears the value of the "prefix" field. +func (u *ApiKeyUpsertOne) ClearPrefix() *ApiKeyUpsertOne { + return u.Update(func(s *ApiKeyUpsert) { + s.ClearPrefix() + }) +} + +// SetKeyHash sets the "key_hash" field. +func (u *ApiKeyUpsertOne) SetKeyHash(v string) *ApiKeyUpsertOne { + return u.Update(func(s *ApiKeyUpsert) { + s.SetKeyHash(v) + }) +} + +// UpdateKeyHash sets the "key_hash" field to the value that was provided on create. +func (u *ApiKeyUpsertOne) UpdateKeyHash() *ApiKeyUpsertOne { + return u.Update(func(s *ApiKeyUpsert) { + s.UpdateKeyHash() + }) +} + +// SetScopes sets the "scopes" field. +func (u *ApiKeyUpsertOne) SetScopes(v string) *ApiKeyUpsertOne { + return u.Update(func(s *ApiKeyUpsert) { + s.SetScopes(v) + }) +} + +// UpdateScopes sets the "scopes" field to the value that was provided on create. +func (u *ApiKeyUpsertOne) UpdateScopes() *ApiKeyUpsertOne { + return u.Update(func(s *ApiKeyUpsert) { + s.UpdateScopes() + }) +} + +// ClearScopes clears the value of the "scopes" field. +func (u *ApiKeyUpsertOne) ClearScopes() *ApiKeyUpsertOne { + return u.Update(func(s *ApiKeyUpsert) { + s.ClearScopes() + }) +} + +// SetRevoked sets the "revoked" field. +func (u *ApiKeyUpsertOne) SetRevoked(v bool) *ApiKeyUpsertOne { + return u.Update(func(s *ApiKeyUpsert) { + s.SetRevoked(v) + }) +} + +// UpdateRevoked sets the "revoked" field to the value that was provided on create. +func (u *ApiKeyUpsertOne) UpdateRevoked() *ApiKeyUpsertOne { + return u.Update(func(s *ApiKeyUpsert) { + s.UpdateRevoked() + }) +} + +// SetExpiresAt sets the "expires_at" field. +func (u *ApiKeyUpsertOne) SetExpiresAt(v time.Time) *ApiKeyUpsertOne { + return u.Update(func(s *ApiKeyUpsert) { + s.SetExpiresAt(v) + }) +} + +// UpdateExpiresAt sets the "expires_at" field to the value that was provided on create. +func (u *ApiKeyUpsertOne) UpdateExpiresAt() *ApiKeyUpsertOne { + return u.Update(func(s *ApiKeyUpsert) { + s.UpdateExpiresAt() + }) +} + +// ClearExpiresAt clears the value of the "expires_at" field. +func (u *ApiKeyUpsertOne) ClearExpiresAt() *ApiKeyUpsertOne { + return u.Update(func(s *ApiKeyUpsert) { + s.ClearExpiresAt() + }) +} + +// SetLastUsed sets the "last_used" field. +func (u *ApiKeyUpsertOne) SetLastUsed(v time.Time) *ApiKeyUpsertOne { + return u.Update(func(s *ApiKeyUpsert) { + s.SetLastUsed(v) + }) +} + +// UpdateLastUsed sets the "last_used" field to the value that was provided on create. +func (u *ApiKeyUpsertOne) UpdateLastUsed() *ApiKeyUpsertOne { + return u.Update(func(s *ApiKeyUpsert) { + s.UpdateLastUsed() + }) +} + +// ClearLastUsed clears the value of the "last_used" field. +func (u *ApiKeyUpsertOne) ClearLastUsed() *ApiKeyUpsertOne { + return u.Update(func(s *ApiKeyUpsert) { + s.ClearLastUsed() + }) +} + +// Exec executes the query. +func (u *ApiKeyUpsertOne) Exec(ctx context.Context) error { + if len(u.create.conflict) == 0 { + return errors.New("ent: missing options for ApiKeyCreate.OnConflict") + } + return u.create.Exec(ctx) +} + +// ExecX is like Exec, but panics if an error occurs. +func (u *ApiKeyUpsertOne) ExecX(ctx context.Context) { + if err := u.create.Exec(ctx); err != nil { + panic(err) + } +} + +// Exec executes the UPSERT query and returns the inserted/updated ID. +func (u *ApiKeyUpsertOne) ID(ctx context.Context) (id uuid.UUID, err error) { + if u.create.driver.Dialect() == dialect.MySQL { + // In case of "ON CONFLICT", there is no way to get back non-numeric ID + // fields from the database since MySQL does not support the RETURNING clause. + return id, errors.New("ent: ApiKeyUpsertOne.ID is not supported by MySQL driver. Use ApiKeyUpsertOne.Exec instead") + } + node, err := u.create.Save(ctx) + if err != nil { + return id, err + } + return node.ID, nil +} + +// IDX is like ID, but panics if an error occurs. +func (u *ApiKeyUpsertOne) IDX(ctx context.Context) uuid.UUID { + id, err := u.ID(ctx) + if err != nil { + panic(err) + } + return id +} + +// ApiKeyCreateBulk is the builder for creating many ApiKey entities in bulk. +type ApiKeyCreateBulk struct { + config + err error + builders []*ApiKeyCreate + conflict []sql.ConflictOption +} + +// Save creates the ApiKey entities in the database. +func (_c *ApiKeyCreateBulk) Save(ctx context.Context) ([]*ApiKey, error) { + if _c.err != nil { + return nil, _c.err + } + specs := make([]*sqlgraph.CreateSpec, len(_c.builders)) + nodes := make([]*ApiKey, len(_c.builders)) + mutators := make([]Mutator, len(_c.builders)) + for i := range _c.builders { + func(i int, root context.Context) { + builder := _c.builders[i] + builder.defaults() + var mut Mutator = MutateFunc(func(ctx context.Context, m Mutation) (Value, error) { + mutation, ok := m.(*ApiKeyMutation) + if !ok { + return nil, fmt.Errorf("unexpected mutation type %T", m) + } + if err := builder.check(); err != nil { + return nil, err + } + builder.mutation = mutation + var err error + nodes[i], specs[i] = builder.createSpec() + if i < len(mutators)-1 { + _, err = mutators[i+1].Mutate(root, _c.builders[i+1].mutation) + } else { + spec := &sqlgraph.BatchCreateSpec{Nodes: specs} + spec.OnConflict = _c.conflict + // Invoke the actual operation on the latest mutation in the chain. + if err = sqlgraph.BatchCreate(ctx, _c.driver, spec); err != nil { + if sqlgraph.IsConstraintError(err) { + err = &ConstraintError{msg: err.Error(), wrap: err} + } + } + } + if err != nil { + return nil, err + } + mutation.id = &nodes[i].ID + mutation.done = true + return nodes[i], nil + }) + for i := len(builder.hooks) - 1; i >= 0; i-- { + mut = builder.hooks[i](mut) + } + mutators[i] = mut + }(i, ctx) + } + if len(mutators) > 0 { + if _, err := mutators[0].Mutate(ctx, _c.builders[0].mutation); err != nil { + return nil, err + } + } + return nodes, nil +} + +// SaveX is like Save, but panics if an error occurs. +func (_c *ApiKeyCreateBulk) SaveX(ctx context.Context) []*ApiKey { + v, err := _c.Save(ctx) + if err != nil { + panic(err) + } + return v +} + +// Exec executes the query. +func (_c *ApiKeyCreateBulk) Exec(ctx context.Context) error { + _, err := _c.Save(ctx) + return err +} + +// ExecX is like Exec, but panics if an error occurs. +func (_c *ApiKeyCreateBulk) ExecX(ctx context.Context) { + if err := _c.Exec(ctx); err != nil { + panic(err) + } +} + +// OnConflict allows configuring the `ON CONFLICT` / `ON DUPLICATE KEY` clause +// of the `INSERT` statement. For example: +// +// client.ApiKey.CreateBulk(builders...). +// OnConflict( +// // Update the row with the new values +// // the was proposed for insertion. +// sql.ResolveWithNewValues(), +// ). +// // Override some of the fields with custom +// // update values. +// Update(func(u *ent.ApiKeyUpsert) { +// SetUserID(v+v). +// }). +// Exec(ctx) +func (_c *ApiKeyCreateBulk) OnConflict(opts ...sql.ConflictOption) *ApiKeyUpsertBulk { + _c.conflict = opts + return &ApiKeyUpsertBulk{ + create: _c, + } +} + +// OnConflictColumns calls `OnConflict` and configures the columns +// as conflict target. Using this option is equivalent to using: +// +// client.ApiKey.Create(). +// OnConflict(sql.ConflictColumns(columns...)). +// Exec(ctx) +func (_c *ApiKeyCreateBulk) OnConflictColumns(columns ...string) *ApiKeyUpsertBulk { + _c.conflict = append(_c.conflict, sql.ConflictColumns(columns...)) + return &ApiKeyUpsertBulk{ + create: _c, + } +} + +// ApiKeyUpsertBulk is the builder for "upsert"-ing +// a bulk of ApiKey nodes. +type ApiKeyUpsertBulk struct { + create *ApiKeyCreateBulk +} + +// UpdateNewValues updates the mutable fields using the new values that +// were set on create. Using this option is equivalent to using: +// +// client.ApiKey.Create(). +// OnConflict( +// sql.ResolveWithNewValues(), +// sql.ResolveWith(func(u *sql.UpdateSet) { +// u.SetIgnore(apikey.FieldID) +// }), +// ). +// Exec(ctx) +func (u *ApiKeyUpsertBulk) UpdateNewValues() *ApiKeyUpsertBulk { + u.create.conflict = append(u.create.conflict, sql.ResolveWithNewValues()) + u.create.conflict = append(u.create.conflict, sql.ResolveWith(func(s *sql.UpdateSet) { + for _, b := range u.create.builders { + if _, exists := b.mutation.ID(); exists { + s.SetIgnore(apikey.FieldID) + } + if _, exists := b.mutation.Created(); exists { + s.SetIgnore(apikey.FieldCreated) + } + } + })) + return u +} + +// Ignore sets each column to itself in case of conflict. +// Using this option is equivalent to using: +// +// client.ApiKey.Create(). +// OnConflict(sql.ResolveWithIgnore()). +// Exec(ctx) +func (u *ApiKeyUpsertBulk) Ignore() *ApiKeyUpsertBulk { + u.create.conflict = append(u.create.conflict, sql.ResolveWithIgnore()) + return u +} + +// DoNothing configures the conflict_action to `DO NOTHING`. +// Supported only by SQLite and PostgreSQL. +func (u *ApiKeyUpsertBulk) DoNothing() *ApiKeyUpsertBulk { + u.create.conflict = append(u.create.conflict, sql.DoNothing()) + return u +} + +// Update allows overriding fields `UPDATE` values. See the ApiKeyCreateBulk.OnConflict +// documentation for more info. +func (u *ApiKeyUpsertBulk) Update(set func(*ApiKeyUpsert)) *ApiKeyUpsertBulk { + u.create.conflict = append(u.create.conflict, sql.ResolveWith(func(update *sql.UpdateSet) { + set(&ApiKeyUpsert{UpdateSet: update}) + })) + return u +} + +// SetUserID sets the "user_id" field. +func (u *ApiKeyUpsertBulk) SetUserID(v uuid.UUID) *ApiKeyUpsertBulk { + return u.Update(func(s *ApiKeyUpsert) { + s.SetUserID(v) + }) +} + +// UpdateUserID sets the "user_id" field to the value that was provided on create. +func (u *ApiKeyUpsertBulk) UpdateUserID() *ApiKeyUpsertBulk { + return u.Update(func(s *ApiKeyUpsert) { + s.UpdateUserID() + }) +} + +// SetName sets the "name" field. +func (u *ApiKeyUpsertBulk) SetName(v string) *ApiKeyUpsertBulk { + return u.Update(func(s *ApiKeyUpsert) { + s.SetName(v) + }) +} + +// UpdateName sets the "name" field to the value that was provided on create. +func (u *ApiKeyUpsertBulk) UpdateName() *ApiKeyUpsertBulk { + return u.Update(func(s *ApiKeyUpsert) { + s.UpdateName() + }) +} + +// ClearName clears the value of the "name" field. +func (u *ApiKeyUpsertBulk) ClearName() *ApiKeyUpsertBulk { + return u.Update(func(s *ApiKeyUpsert) { + s.ClearName() + }) +} + +// SetPrefix sets the "prefix" field. +func (u *ApiKeyUpsertBulk) SetPrefix(v string) *ApiKeyUpsertBulk { + return u.Update(func(s *ApiKeyUpsert) { + s.SetPrefix(v) + }) +} + +// UpdatePrefix sets the "prefix" field to the value that was provided on create. +func (u *ApiKeyUpsertBulk) UpdatePrefix() *ApiKeyUpsertBulk { + return u.Update(func(s *ApiKeyUpsert) { + s.UpdatePrefix() + }) +} + +// ClearPrefix clears the value of the "prefix" field. +func (u *ApiKeyUpsertBulk) ClearPrefix() *ApiKeyUpsertBulk { + return u.Update(func(s *ApiKeyUpsert) { + s.ClearPrefix() + }) +} + +// SetKeyHash sets the "key_hash" field. +func (u *ApiKeyUpsertBulk) SetKeyHash(v string) *ApiKeyUpsertBulk { + return u.Update(func(s *ApiKeyUpsert) { + s.SetKeyHash(v) + }) +} + +// UpdateKeyHash sets the "key_hash" field to the value that was provided on create. +func (u *ApiKeyUpsertBulk) UpdateKeyHash() *ApiKeyUpsertBulk { + return u.Update(func(s *ApiKeyUpsert) { + s.UpdateKeyHash() + }) +} + +// SetScopes sets the "scopes" field. +func (u *ApiKeyUpsertBulk) SetScopes(v string) *ApiKeyUpsertBulk { + return u.Update(func(s *ApiKeyUpsert) { + s.SetScopes(v) + }) +} + +// UpdateScopes sets the "scopes" field to the value that was provided on create. +func (u *ApiKeyUpsertBulk) UpdateScopes() *ApiKeyUpsertBulk { + return u.Update(func(s *ApiKeyUpsert) { + s.UpdateScopes() + }) +} + +// ClearScopes clears the value of the "scopes" field. +func (u *ApiKeyUpsertBulk) ClearScopes() *ApiKeyUpsertBulk { + return u.Update(func(s *ApiKeyUpsert) { + s.ClearScopes() + }) +} + +// SetRevoked sets the "revoked" field. +func (u *ApiKeyUpsertBulk) SetRevoked(v bool) *ApiKeyUpsertBulk { + return u.Update(func(s *ApiKeyUpsert) { + s.SetRevoked(v) + }) +} + +// UpdateRevoked sets the "revoked" field to the value that was provided on create. +func (u *ApiKeyUpsertBulk) UpdateRevoked() *ApiKeyUpsertBulk { + return u.Update(func(s *ApiKeyUpsert) { + s.UpdateRevoked() + }) +} + +// SetExpiresAt sets the "expires_at" field. +func (u *ApiKeyUpsertBulk) SetExpiresAt(v time.Time) *ApiKeyUpsertBulk { + return u.Update(func(s *ApiKeyUpsert) { + s.SetExpiresAt(v) + }) +} + +// UpdateExpiresAt sets the "expires_at" field to the value that was provided on create. +func (u *ApiKeyUpsertBulk) UpdateExpiresAt() *ApiKeyUpsertBulk { + return u.Update(func(s *ApiKeyUpsert) { + s.UpdateExpiresAt() + }) +} + +// ClearExpiresAt clears the value of the "expires_at" field. +func (u *ApiKeyUpsertBulk) ClearExpiresAt() *ApiKeyUpsertBulk { + return u.Update(func(s *ApiKeyUpsert) { + s.ClearExpiresAt() + }) +} + +// SetLastUsed sets the "last_used" field. +func (u *ApiKeyUpsertBulk) SetLastUsed(v time.Time) *ApiKeyUpsertBulk { + return u.Update(func(s *ApiKeyUpsert) { + s.SetLastUsed(v) + }) +} + +// UpdateLastUsed sets the "last_used" field to the value that was provided on create. +func (u *ApiKeyUpsertBulk) UpdateLastUsed() *ApiKeyUpsertBulk { + return u.Update(func(s *ApiKeyUpsert) { + s.UpdateLastUsed() + }) +} + +// ClearLastUsed clears the value of the "last_used" field. +func (u *ApiKeyUpsertBulk) ClearLastUsed() *ApiKeyUpsertBulk { + return u.Update(func(s *ApiKeyUpsert) { + s.ClearLastUsed() + }) +} + +// Exec executes the query. +func (u *ApiKeyUpsertBulk) Exec(ctx context.Context) error { + if u.create.err != nil { + return u.create.err + } + for i, b := range u.create.builders { + if len(b.conflict) != 0 { + return fmt.Errorf("ent: OnConflict was set for builder %d. Set it on the ApiKeyCreateBulk instead", i) + } + } + if len(u.create.conflict) == 0 { + return errors.New("ent: missing options for ApiKeyCreateBulk.OnConflict") + } + return u.create.Exec(ctx) +} + +// ExecX is like Exec, but panics if an error occurs. +func (u *ApiKeyUpsertBulk) ExecX(ctx context.Context) { + if err := u.create.Exec(ctx); err != nil { + panic(err) + } +} diff --git a/pkg/ent/apikey_delete.go b/pkg/ent/apikey_delete.go new file mode 100644 index 000000000..78861601c --- /dev/null +++ b/pkg/ent/apikey_delete.go @@ -0,0 +1,88 @@ +// Code generated by ent, DO NOT EDIT. + +package ent + +import ( + "context" + + "entgo.io/ent/dialect/sql" + "entgo.io/ent/dialect/sql/sqlgraph" + "entgo.io/ent/schema/field" + "github.com/GoogleCloudPlatform/scion/pkg/ent/apikey" + "github.com/GoogleCloudPlatform/scion/pkg/ent/predicate" +) + +// ApiKeyDelete is the builder for deleting a ApiKey entity. +type ApiKeyDelete struct { + config + hooks []Hook + mutation *ApiKeyMutation +} + +// Where appends a list predicates to the ApiKeyDelete builder. +func (_d *ApiKeyDelete) Where(ps ...predicate.ApiKey) *ApiKeyDelete { + _d.mutation.Where(ps...) + return _d +} + +// Exec executes the deletion query and returns how many vertices were deleted. +func (_d *ApiKeyDelete) Exec(ctx context.Context) (int, error) { + return withHooks(ctx, _d.sqlExec, _d.mutation, _d.hooks) +} + +// ExecX is like Exec, but panics if an error occurs. +func (_d *ApiKeyDelete) ExecX(ctx context.Context) int { + n, err := _d.Exec(ctx) + if err != nil { + panic(err) + } + return n +} + +func (_d *ApiKeyDelete) sqlExec(ctx context.Context) (int, error) { + _spec := sqlgraph.NewDeleteSpec(apikey.Table, sqlgraph.NewFieldSpec(apikey.FieldID, field.TypeUUID)) + if ps := _d.mutation.predicates; len(ps) > 0 { + _spec.Predicate = func(selector *sql.Selector) { + for i := range ps { + ps[i](selector) + } + } + } + affected, err := sqlgraph.DeleteNodes(ctx, _d.driver, _spec) + if err != nil && sqlgraph.IsConstraintError(err) { + err = &ConstraintError{msg: err.Error(), wrap: err} + } + _d.mutation.done = true + return affected, err +} + +// ApiKeyDeleteOne is the builder for deleting a single ApiKey entity. +type ApiKeyDeleteOne struct { + _d *ApiKeyDelete +} + +// Where appends a list predicates to the ApiKeyDelete builder. +func (_d *ApiKeyDeleteOne) Where(ps ...predicate.ApiKey) *ApiKeyDeleteOne { + _d._d.mutation.Where(ps...) + return _d +} + +// Exec executes the deletion query. +func (_d *ApiKeyDeleteOne) Exec(ctx context.Context) error { + n, err := _d._d.Exec(ctx) + switch { + case err != nil: + return err + case n == 0: + return &NotFoundError{apikey.Label} + default: + return nil + } +} + +// ExecX is like Exec, but panics if an error occurs. +func (_d *ApiKeyDeleteOne) ExecX(ctx context.Context) { + if err := _d.Exec(ctx); err != nil { + panic(err) + } +} diff --git a/pkg/ent/apikey_query.go b/pkg/ent/apikey_query.go new file mode 100644 index 000000000..638c0d237 --- /dev/null +++ b/pkg/ent/apikey_query.go @@ -0,0 +1,565 @@ +// Code generated by ent, DO NOT EDIT. + +package ent + +import ( + "context" + "fmt" + "math" + + "entgo.io/ent" + "entgo.io/ent/dialect" + "entgo.io/ent/dialect/sql" + "entgo.io/ent/dialect/sql/sqlgraph" + "entgo.io/ent/schema/field" + "github.com/GoogleCloudPlatform/scion/pkg/ent/apikey" + "github.com/GoogleCloudPlatform/scion/pkg/ent/predicate" + "github.com/google/uuid" +) + +// ApiKeyQuery is the builder for querying ApiKey entities. +type ApiKeyQuery struct { + config + ctx *QueryContext + order []apikey.OrderOption + inters []Interceptor + predicates []predicate.ApiKey + modifiers []func(*sql.Selector) + // intermediate query (i.e. traversal path). + sql *sql.Selector + path func(context.Context) (*sql.Selector, error) +} + +// Where adds a new predicate for the ApiKeyQuery builder. +func (_q *ApiKeyQuery) Where(ps ...predicate.ApiKey) *ApiKeyQuery { + _q.predicates = append(_q.predicates, ps...) + return _q +} + +// Limit the number of records to be returned by this query. +func (_q *ApiKeyQuery) Limit(limit int) *ApiKeyQuery { + _q.ctx.Limit = &limit + return _q +} + +// Offset to start from. +func (_q *ApiKeyQuery) Offset(offset int) *ApiKeyQuery { + _q.ctx.Offset = &offset + return _q +} + +// Unique configures the query builder to filter duplicate records on query. +// By default, unique is set to true, and can be disabled using this method. +func (_q *ApiKeyQuery) Unique(unique bool) *ApiKeyQuery { + _q.ctx.Unique = &unique + return _q +} + +// Order specifies how the records should be ordered. +func (_q *ApiKeyQuery) Order(o ...apikey.OrderOption) *ApiKeyQuery { + _q.order = append(_q.order, o...) + return _q +} + +// First returns the first ApiKey entity from the query. +// Returns a *NotFoundError when no ApiKey was found. +func (_q *ApiKeyQuery) First(ctx context.Context) (*ApiKey, error) { + nodes, err := _q.Limit(1).All(setContextOp(ctx, _q.ctx, ent.OpQueryFirst)) + if err != nil { + return nil, err + } + if len(nodes) == 0 { + return nil, &NotFoundError{apikey.Label} + } + return nodes[0], nil +} + +// FirstX is like First, but panics if an error occurs. +func (_q *ApiKeyQuery) FirstX(ctx context.Context) *ApiKey { + node, err := _q.First(ctx) + if err != nil && !IsNotFound(err) { + panic(err) + } + return node +} + +// FirstID returns the first ApiKey ID from the query. +// Returns a *NotFoundError when no ApiKey ID was found. +func (_q *ApiKeyQuery) FirstID(ctx context.Context) (id uuid.UUID, err error) { + var ids []uuid.UUID + if ids, err = _q.Limit(1).IDs(setContextOp(ctx, _q.ctx, ent.OpQueryFirstID)); err != nil { + return + } + if len(ids) == 0 { + err = &NotFoundError{apikey.Label} + return + } + return ids[0], nil +} + +// FirstIDX is like FirstID, but panics if an error occurs. +func (_q *ApiKeyQuery) FirstIDX(ctx context.Context) uuid.UUID { + id, err := _q.FirstID(ctx) + if err != nil && !IsNotFound(err) { + panic(err) + } + return id +} + +// Only returns a single ApiKey entity found by the query, ensuring it only returns one. +// Returns a *NotSingularError when more than one ApiKey entity is found. +// Returns a *NotFoundError when no ApiKey entities are found. +func (_q *ApiKeyQuery) Only(ctx context.Context) (*ApiKey, error) { + nodes, err := _q.Limit(2).All(setContextOp(ctx, _q.ctx, ent.OpQueryOnly)) + if err != nil { + return nil, err + } + switch len(nodes) { + case 1: + return nodes[0], nil + case 0: + return nil, &NotFoundError{apikey.Label} + default: + return nil, &NotSingularError{apikey.Label} + } +} + +// OnlyX is like Only, but panics if an error occurs. +func (_q *ApiKeyQuery) OnlyX(ctx context.Context) *ApiKey { + node, err := _q.Only(ctx) + if err != nil { + panic(err) + } + return node +} + +// OnlyID is like Only, but returns the only ApiKey ID in the query. +// Returns a *NotSingularError when more than one ApiKey ID is found. +// Returns a *NotFoundError when no entities are found. +func (_q *ApiKeyQuery) OnlyID(ctx context.Context) (id uuid.UUID, err error) { + var ids []uuid.UUID + if ids, err = _q.Limit(2).IDs(setContextOp(ctx, _q.ctx, ent.OpQueryOnlyID)); err != nil { + return + } + switch len(ids) { + case 1: + id = ids[0] + case 0: + err = &NotFoundError{apikey.Label} + default: + err = &NotSingularError{apikey.Label} + } + return +} + +// OnlyIDX is like OnlyID, but panics if an error occurs. +func (_q *ApiKeyQuery) OnlyIDX(ctx context.Context) uuid.UUID { + id, err := _q.OnlyID(ctx) + if err != nil { + panic(err) + } + return id +} + +// All executes the query and returns a list of ApiKeys. +func (_q *ApiKeyQuery) All(ctx context.Context) ([]*ApiKey, error) { + ctx = setContextOp(ctx, _q.ctx, ent.OpQueryAll) + if err := _q.prepareQuery(ctx); err != nil { + return nil, err + } + qr := querierAll[[]*ApiKey, *ApiKeyQuery]() + return withInterceptors[[]*ApiKey](ctx, _q, qr, _q.inters) +} + +// AllX is like All, but panics if an error occurs. +func (_q *ApiKeyQuery) AllX(ctx context.Context) []*ApiKey { + nodes, err := _q.All(ctx) + if err != nil { + panic(err) + } + return nodes +} + +// IDs executes the query and returns a list of ApiKey IDs. +func (_q *ApiKeyQuery) IDs(ctx context.Context) (ids []uuid.UUID, err error) { + if _q.ctx.Unique == nil && _q.path != nil { + _q.Unique(true) + } + ctx = setContextOp(ctx, _q.ctx, ent.OpQueryIDs) + if err = _q.Select(apikey.FieldID).Scan(ctx, &ids); err != nil { + return nil, err + } + return ids, nil +} + +// IDsX is like IDs, but panics if an error occurs. +func (_q *ApiKeyQuery) IDsX(ctx context.Context) []uuid.UUID { + ids, err := _q.IDs(ctx) + if err != nil { + panic(err) + } + return ids +} + +// Count returns the count of the given query. +func (_q *ApiKeyQuery) Count(ctx context.Context) (int, error) { + ctx = setContextOp(ctx, _q.ctx, ent.OpQueryCount) + if err := _q.prepareQuery(ctx); err != nil { + return 0, err + } + return withInterceptors[int](ctx, _q, querierCount[*ApiKeyQuery](), _q.inters) +} + +// CountX is like Count, but panics if an error occurs. +func (_q *ApiKeyQuery) CountX(ctx context.Context) int { + count, err := _q.Count(ctx) + if err != nil { + panic(err) + } + return count +} + +// Exist returns true if the query has elements in the graph. +func (_q *ApiKeyQuery) Exist(ctx context.Context) (bool, error) { + ctx = setContextOp(ctx, _q.ctx, ent.OpQueryExist) + switch _, err := _q.FirstID(ctx); { + case IsNotFound(err): + return false, nil + case err != nil: + return false, fmt.Errorf("ent: check existence: %w", err) + default: + return true, nil + } +} + +// ExistX is like Exist, but panics if an error occurs. +func (_q *ApiKeyQuery) ExistX(ctx context.Context) bool { + exist, err := _q.Exist(ctx) + if err != nil { + panic(err) + } + return exist +} + +// Clone returns a duplicate of the ApiKeyQuery builder, including all associated steps. It can be +// used to prepare common query builders and use them differently after the clone is made. +func (_q *ApiKeyQuery) Clone() *ApiKeyQuery { + if _q == nil { + return nil + } + return &ApiKeyQuery{ + config: _q.config, + ctx: _q.ctx.Clone(), + order: append([]apikey.OrderOption{}, _q.order...), + inters: append([]Interceptor{}, _q.inters...), + predicates: append([]predicate.ApiKey{}, _q.predicates...), + // clone intermediate query. + sql: _q.sql.Clone(), + path: _q.path, + } +} + +// GroupBy is used to group vertices by one or more fields/columns. +// It is often used with aggregate functions, like: count, max, mean, min, sum. +// +// Example: +// +// var v []struct { +// UserID uuid.UUID `json:"user_id,omitempty"` +// Count int `json:"count,omitempty"` +// } +// +// client.ApiKey.Query(). +// GroupBy(apikey.FieldUserID). +// Aggregate(ent.Count()). +// Scan(ctx, &v) +func (_q *ApiKeyQuery) GroupBy(field string, fields ...string) *ApiKeyGroupBy { + _q.ctx.Fields = append([]string{field}, fields...) + grbuild := &ApiKeyGroupBy{build: _q} + grbuild.flds = &_q.ctx.Fields + grbuild.label = apikey.Label + grbuild.scan = grbuild.Scan + return grbuild +} + +// Select allows the selection one or more fields/columns for the given query, +// instead of selecting all fields in the entity. +// +// Example: +// +// var v []struct { +// UserID uuid.UUID `json:"user_id,omitempty"` +// } +// +// client.ApiKey.Query(). +// Select(apikey.FieldUserID). +// Scan(ctx, &v) +func (_q *ApiKeyQuery) Select(fields ...string) *ApiKeySelect { + _q.ctx.Fields = append(_q.ctx.Fields, fields...) + sbuild := &ApiKeySelect{ApiKeyQuery: _q} + sbuild.label = apikey.Label + sbuild.flds, sbuild.scan = &_q.ctx.Fields, sbuild.Scan + return sbuild +} + +// Aggregate returns a ApiKeySelect configured with the given aggregations. +func (_q *ApiKeyQuery) Aggregate(fns ...AggregateFunc) *ApiKeySelect { + return _q.Select().Aggregate(fns...) +} + +func (_q *ApiKeyQuery) prepareQuery(ctx context.Context) error { + for _, inter := range _q.inters { + if inter == nil { + return fmt.Errorf("ent: uninitialized interceptor (forgotten import ent/runtime?)") + } + if trv, ok := inter.(Traverser); ok { + if err := trv.Traverse(ctx, _q); err != nil { + return err + } + } + } + for _, f := range _q.ctx.Fields { + if !apikey.ValidColumn(f) { + return &ValidationError{Name: f, err: fmt.Errorf("ent: invalid field %q for query", f)} + } + } + if _q.path != nil { + prev, err := _q.path(ctx) + if err != nil { + return err + } + _q.sql = prev + } + return nil +} + +func (_q *ApiKeyQuery) sqlAll(ctx context.Context, hooks ...queryHook) ([]*ApiKey, error) { + var ( + nodes = []*ApiKey{} + _spec = _q.querySpec() + ) + _spec.ScanValues = func(columns []string) ([]any, error) { + return (*ApiKey).scanValues(nil, columns) + } + _spec.Assign = func(columns []string, values []any) error { + node := &ApiKey{config: _q.config} + nodes = append(nodes, node) + return node.assignValues(columns, values) + } + if len(_q.modifiers) > 0 { + _spec.Modifiers = _q.modifiers + } + for i := range hooks { + hooks[i](ctx, _spec) + } + if err := sqlgraph.QueryNodes(ctx, _q.driver, _spec); err != nil { + return nil, err + } + if len(nodes) == 0 { + return nodes, nil + } + return nodes, nil +} + +func (_q *ApiKeyQuery) sqlCount(ctx context.Context) (int, error) { + _spec := _q.querySpec() + if len(_q.modifiers) > 0 { + _spec.Modifiers = _q.modifiers + } + _spec.Node.Columns = _q.ctx.Fields + if len(_q.ctx.Fields) > 0 { + _spec.Unique = _q.ctx.Unique != nil && *_q.ctx.Unique + } + return sqlgraph.CountNodes(ctx, _q.driver, _spec) +} + +func (_q *ApiKeyQuery) querySpec() *sqlgraph.QuerySpec { + _spec := sqlgraph.NewQuerySpec(apikey.Table, apikey.Columns, sqlgraph.NewFieldSpec(apikey.FieldID, field.TypeUUID)) + _spec.From = _q.sql + if unique := _q.ctx.Unique; unique != nil { + _spec.Unique = *unique + } else if _q.path != nil { + _spec.Unique = true + } + if fields := _q.ctx.Fields; len(fields) > 0 { + _spec.Node.Columns = make([]string, 0, len(fields)) + _spec.Node.Columns = append(_spec.Node.Columns, apikey.FieldID) + for i := range fields { + if fields[i] != apikey.FieldID { + _spec.Node.Columns = append(_spec.Node.Columns, fields[i]) + } + } + } + if ps := _q.predicates; len(ps) > 0 { + _spec.Predicate = func(selector *sql.Selector) { + for i := range ps { + ps[i](selector) + } + } + } + if limit := _q.ctx.Limit; limit != nil { + _spec.Limit = *limit + } + if offset := _q.ctx.Offset; offset != nil { + _spec.Offset = *offset + } + if ps := _q.order; len(ps) > 0 { + _spec.Order = func(selector *sql.Selector) { + for i := range ps { + ps[i](selector) + } + } + } + return _spec +} + +func (_q *ApiKeyQuery) sqlQuery(ctx context.Context) *sql.Selector { + builder := sql.Dialect(_q.driver.Dialect()) + t1 := builder.Table(apikey.Table) + columns := _q.ctx.Fields + if len(columns) == 0 { + columns = apikey.Columns + } + selector := builder.Select(t1.Columns(columns...)...).From(t1) + if _q.sql != nil { + selector = _q.sql + selector.Select(selector.Columns(columns...)...) + } + if _q.ctx.Unique != nil && *_q.ctx.Unique { + selector.Distinct() + } + for _, m := range _q.modifiers { + m(selector) + } + for _, p := range _q.predicates { + p(selector) + } + for _, p := range _q.order { + p(selector) + } + if offset := _q.ctx.Offset; offset != nil { + // limit is mandatory for offset clause. We start + // with default value, and override it below if needed. + selector.Offset(*offset).Limit(math.MaxInt32) + } + if limit := _q.ctx.Limit; limit != nil { + selector.Limit(*limit) + } + return selector +} + +// ForUpdate locks the selected rows against concurrent updates, and prevent them from being +// updated, deleted or "selected ... for update" by other sessions, until the transaction is +// either committed or rolled-back. +func (_q *ApiKeyQuery) ForUpdate(opts ...sql.LockOption) *ApiKeyQuery { + if _q.driver.Dialect() == dialect.Postgres { + _q.Unique(false) + } + _q.modifiers = append(_q.modifiers, func(s *sql.Selector) { + s.ForUpdate(opts...) + }) + return _q +} + +// ForShare behaves similarly to ForUpdate, except that it acquires a shared mode lock +// on any rows that are read. Other sessions can read the rows, but cannot modify them +// until your transaction commits. +func (_q *ApiKeyQuery) ForShare(opts ...sql.LockOption) *ApiKeyQuery { + if _q.driver.Dialect() == dialect.Postgres { + _q.Unique(false) + } + _q.modifiers = append(_q.modifiers, func(s *sql.Selector) { + s.ForShare(opts...) + }) + return _q +} + +// ApiKeyGroupBy is the group-by builder for ApiKey entities. +type ApiKeyGroupBy struct { + selector + build *ApiKeyQuery +} + +// Aggregate adds the given aggregation functions to the group-by query. +func (_g *ApiKeyGroupBy) Aggregate(fns ...AggregateFunc) *ApiKeyGroupBy { + _g.fns = append(_g.fns, fns...) + return _g +} + +// Scan applies the selector query and scans the result into the given value. +func (_g *ApiKeyGroupBy) Scan(ctx context.Context, v any) error { + ctx = setContextOp(ctx, _g.build.ctx, ent.OpQueryGroupBy) + if err := _g.build.prepareQuery(ctx); err != nil { + return err + } + return scanWithInterceptors[*ApiKeyQuery, *ApiKeyGroupBy](ctx, _g.build, _g, _g.build.inters, v) +} + +func (_g *ApiKeyGroupBy) sqlScan(ctx context.Context, root *ApiKeyQuery, v any) error { + selector := root.sqlQuery(ctx).Select() + aggregation := make([]string, 0, len(_g.fns)) + for _, fn := range _g.fns { + aggregation = append(aggregation, fn(selector)) + } + if len(selector.SelectedColumns()) == 0 { + columns := make([]string, 0, len(*_g.flds)+len(_g.fns)) + for _, f := range *_g.flds { + columns = append(columns, selector.C(f)) + } + columns = append(columns, aggregation...) + selector.Select(columns...) + } + selector.GroupBy(selector.Columns(*_g.flds...)...) + if err := selector.Err(); err != nil { + return err + } + rows := &sql.Rows{} + query, args := selector.Query() + if err := _g.build.driver.Query(ctx, query, args, rows); err != nil { + return err + } + defer rows.Close() + return sql.ScanSlice(rows, v) +} + +// ApiKeySelect is the builder for selecting fields of ApiKey entities. +type ApiKeySelect struct { + *ApiKeyQuery + selector +} + +// Aggregate adds the given aggregation functions to the selector query. +func (_s *ApiKeySelect) Aggregate(fns ...AggregateFunc) *ApiKeySelect { + _s.fns = append(_s.fns, fns...) + return _s +} + +// Scan applies the selector query and scans the result into the given value. +func (_s *ApiKeySelect) Scan(ctx context.Context, v any) error { + ctx = setContextOp(ctx, _s.ctx, ent.OpQuerySelect) + if err := _s.prepareQuery(ctx); err != nil { + return err + } + return scanWithInterceptors[*ApiKeyQuery, *ApiKeySelect](ctx, _s.ApiKeyQuery, _s, _s.inters, v) +} + +func (_s *ApiKeySelect) sqlScan(ctx context.Context, root *ApiKeyQuery, v any) error { + selector := root.sqlQuery(ctx) + aggregation := make([]string, 0, len(_s.fns)) + for _, fn := range _s.fns { + aggregation = append(aggregation, fn(selector)) + } + switch n := len(*_s.selector.flds); { + case n == 0 && len(aggregation) > 0: + selector.Select(aggregation...) + case n != 0 && len(aggregation) > 0: + selector.AppendSelect(aggregation...) + } + rows := &sql.Rows{} + query, args := selector.Query() + if err := _s.driver.Query(ctx, query, args, rows); err != nil { + return err + } + defer rows.Close() + return sql.ScanSlice(rows, v) +} diff --git a/pkg/ent/apikey_update.go b/pkg/ent/apikey_update.go new file mode 100644 index 000000000..549f99201 --- /dev/null +++ b/pkg/ent/apikey_update.go @@ -0,0 +1,565 @@ +// Code generated by ent, DO NOT EDIT. + +package ent + +import ( + "context" + "errors" + "fmt" + "time" + + "entgo.io/ent/dialect/sql" + "entgo.io/ent/dialect/sql/sqlgraph" + "entgo.io/ent/schema/field" + "github.com/GoogleCloudPlatform/scion/pkg/ent/apikey" + "github.com/GoogleCloudPlatform/scion/pkg/ent/predicate" + "github.com/google/uuid" +) + +// ApiKeyUpdate is the builder for updating ApiKey entities. +type ApiKeyUpdate struct { + config + hooks []Hook + mutation *ApiKeyMutation +} + +// Where appends a list predicates to the ApiKeyUpdate builder. +func (_u *ApiKeyUpdate) Where(ps ...predicate.ApiKey) *ApiKeyUpdate { + _u.mutation.Where(ps...) + return _u +} + +// SetUserID sets the "user_id" field. +func (_u *ApiKeyUpdate) SetUserID(v uuid.UUID) *ApiKeyUpdate { + _u.mutation.SetUserID(v) + return _u +} + +// SetNillableUserID sets the "user_id" field if the given value is not nil. +func (_u *ApiKeyUpdate) SetNillableUserID(v *uuid.UUID) *ApiKeyUpdate { + if v != nil { + _u.SetUserID(*v) + } + return _u +} + +// SetName sets the "name" field. +func (_u *ApiKeyUpdate) SetName(v string) *ApiKeyUpdate { + _u.mutation.SetName(v) + return _u +} + +// SetNillableName sets the "name" field if the given value is not nil. +func (_u *ApiKeyUpdate) SetNillableName(v *string) *ApiKeyUpdate { + if v != nil { + _u.SetName(*v) + } + return _u +} + +// ClearName clears the value of the "name" field. +func (_u *ApiKeyUpdate) ClearName() *ApiKeyUpdate { + _u.mutation.ClearName() + return _u +} + +// SetPrefix sets the "prefix" field. +func (_u *ApiKeyUpdate) SetPrefix(v string) *ApiKeyUpdate { + _u.mutation.SetPrefix(v) + return _u +} + +// SetNillablePrefix sets the "prefix" field if the given value is not nil. +func (_u *ApiKeyUpdate) SetNillablePrefix(v *string) *ApiKeyUpdate { + if v != nil { + _u.SetPrefix(*v) + } + return _u +} + +// ClearPrefix clears the value of the "prefix" field. +func (_u *ApiKeyUpdate) ClearPrefix() *ApiKeyUpdate { + _u.mutation.ClearPrefix() + return _u +} + +// SetKeyHash sets the "key_hash" field. +func (_u *ApiKeyUpdate) SetKeyHash(v string) *ApiKeyUpdate { + _u.mutation.SetKeyHash(v) + return _u +} + +// SetNillableKeyHash sets the "key_hash" field if the given value is not nil. +func (_u *ApiKeyUpdate) SetNillableKeyHash(v *string) *ApiKeyUpdate { + if v != nil { + _u.SetKeyHash(*v) + } + return _u +} + +// SetScopes sets the "scopes" field. +func (_u *ApiKeyUpdate) SetScopes(v string) *ApiKeyUpdate { + _u.mutation.SetScopes(v) + return _u +} + +// SetNillableScopes sets the "scopes" field if the given value is not nil. +func (_u *ApiKeyUpdate) SetNillableScopes(v *string) *ApiKeyUpdate { + if v != nil { + _u.SetScopes(*v) + } + return _u +} + +// ClearScopes clears the value of the "scopes" field. +func (_u *ApiKeyUpdate) ClearScopes() *ApiKeyUpdate { + _u.mutation.ClearScopes() + return _u +} + +// SetRevoked sets the "revoked" field. +func (_u *ApiKeyUpdate) SetRevoked(v bool) *ApiKeyUpdate { + _u.mutation.SetRevoked(v) + return _u +} + +// SetNillableRevoked sets the "revoked" field if the given value is not nil. +func (_u *ApiKeyUpdate) SetNillableRevoked(v *bool) *ApiKeyUpdate { + if v != nil { + _u.SetRevoked(*v) + } + return _u +} + +// SetExpiresAt sets the "expires_at" field. +func (_u *ApiKeyUpdate) SetExpiresAt(v time.Time) *ApiKeyUpdate { + _u.mutation.SetExpiresAt(v) + return _u +} + +// SetNillableExpiresAt sets the "expires_at" field if the given value is not nil. +func (_u *ApiKeyUpdate) SetNillableExpiresAt(v *time.Time) *ApiKeyUpdate { + if v != nil { + _u.SetExpiresAt(*v) + } + return _u +} + +// ClearExpiresAt clears the value of the "expires_at" field. +func (_u *ApiKeyUpdate) ClearExpiresAt() *ApiKeyUpdate { + _u.mutation.ClearExpiresAt() + return _u +} + +// SetLastUsed sets the "last_used" field. +func (_u *ApiKeyUpdate) SetLastUsed(v time.Time) *ApiKeyUpdate { + _u.mutation.SetLastUsed(v) + return _u +} + +// SetNillableLastUsed sets the "last_used" field if the given value is not nil. +func (_u *ApiKeyUpdate) SetNillableLastUsed(v *time.Time) *ApiKeyUpdate { + if v != nil { + _u.SetLastUsed(*v) + } + return _u +} + +// ClearLastUsed clears the value of the "last_used" field. +func (_u *ApiKeyUpdate) ClearLastUsed() *ApiKeyUpdate { + _u.mutation.ClearLastUsed() + return _u +} + +// Mutation returns the ApiKeyMutation object of the builder. +func (_u *ApiKeyUpdate) Mutation() *ApiKeyMutation { + return _u.mutation +} + +// Save executes the query and returns the number of nodes affected by the update operation. +func (_u *ApiKeyUpdate) Save(ctx context.Context) (int, error) { + return withHooks(ctx, _u.sqlSave, _u.mutation, _u.hooks) +} + +// SaveX is like Save, but panics if an error occurs. +func (_u *ApiKeyUpdate) SaveX(ctx context.Context) int { + affected, err := _u.Save(ctx) + if err != nil { + panic(err) + } + return affected +} + +// Exec executes the query. +func (_u *ApiKeyUpdate) Exec(ctx context.Context) error { + _, err := _u.Save(ctx) + return err +} + +// ExecX is like Exec, but panics if an error occurs. +func (_u *ApiKeyUpdate) ExecX(ctx context.Context) { + if err := _u.Exec(ctx); err != nil { + panic(err) + } +} + +// check runs all checks and user-defined validators on the builder. +func (_u *ApiKeyUpdate) check() error { + if v, ok := _u.mutation.KeyHash(); ok { + if err := apikey.KeyHashValidator(v); err != nil { + return &ValidationError{Name: "key_hash", err: fmt.Errorf(`ent: validator failed for field "ApiKey.key_hash": %w`, err)} + } + } + return nil +} + +func (_u *ApiKeyUpdate) sqlSave(ctx context.Context) (_node int, err error) { + if err := _u.check(); err != nil { + return _node, err + } + _spec := sqlgraph.NewUpdateSpec(apikey.Table, apikey.Columns, sqlgraph.NewFieldSpec(apikey.FieldID, field.TypeUUID)) + if ps := _u.mutation.predicates; len(ps) > 0 { + _spec.Predicate = func(selector *sql.Selector) { + for i := range ps { + ps[i](selector) + } + } + } + if value, ok := _u.mutation.UserID(); ok { + _spec.SetField(apikey.FieldUserID, field.TypeUUID, value) + } + if value, ok := _u.mutation.Name(); ok { + _spec.SetField(apikey.FieldName, field.TypeString, value) + } + if _u.mutation.NameCleared() { + _spec.ClearField(apikey.FieldName, field.TypeString) + } + if value, ok := _u.mutation.Prefix(); ok { + _spec.SetField(apikey.FieldPrefix, field.TypeString, value) + } + if _u.mutation.PrefixCleared() { + _spec.ClearField(apikey.FieldPrefix, field.TypeString) + } + if value, ok := _u.mutation.KeyHash(); ok { + _spec.SetField(apikey.FieldKeyHash, field.TypeString, value) + } + if value, ok := _u.mutation.Scopes(); ok { + _spec.SetField(apikey.FieldScopes, field.TypeString, value) + } + if _u.mutation.ScopesCleared() { + _spec.ClearField(apikey.FieldScopes, field.TypeString) + } + if value, ok := _u.mutation.Revoked(); ok { + _spec.SetField(apikey.FieldRevoked, field.TypeBool, value) + } + if value, ok := _u.mutation.ExpiresAt(); ok { + _spec.SetField(apikey.FieldExpiresAt, field.TypeTime, value) + } + if _u.mutation.ExpiresAtCleared() { + _spec.ClearField(apikey.FieldExpiresAt, field.TypeTime) + } + if value, ok := _u.mutation.LastUsed(); ok { + _spec.SetField(apikey.FieldLastUsed, field.TypeTime, value) + } + if _u.mutation.LastUsedCleared() { + _spec.ClearField(apikey.FieldLastUsed, field.TypeTime) + } + if _node, err = sqlgraph.UpdateNodes(ctx, _u.driver, _spec); err != nil { + if _, ok := err.(*sqlgraph.NotFoundError); ok { + err = &NotFoundError{apikey.Label} + } else if sqlgraph.IsConstraintError(err) { + err = &ConstraintError{msg: err.Error(), wrap: err} + } + return 0, err + } + _u.mutation.done = true + return _node, nil +} + +// ApiKeyUpdateOne is the builder for updating a single ApiKey entity. +type ApiKeyUpdateOne struct { + config + fields []string + hooks []Hook + mutation *ApiKeyMutation +} + +// SetUserID sets the "user_id" field. +func (_u *ApiKeyUpdateOne) SetUserID(v uuid.UUID) *ApiKeyUpdateOne { + _u.mutation.SetUserID(v) + return _u +} + +// SetNillableUserID sets the "user_id" field if the given value is not nil. +func (_u *ApiKeyUpdateOne) SetNillableUserID(v *uuid.UUID) *ApiKeyUpdateOne { + if v != nil { + _u.SetUserID(*v) + } + return _u +} + +// SetName sets the "name" field. +func (_u *ApiKeyUpdateOne) SetName(v string) *ApiKeyUpdateOne { + _u.mutation.SetName(v) + return _u +} + +// SetNillableName sets the "name" field if the given value is not nil. +func (_u *ApiKeyUpdateOne) SetNillableName(v *string) *ApiKeyUpdateOne { + if v != nil { + _u.SetName(*v) + } + return _u +} + +// ClearName clears the value of the "name" field. +func (_u *ApiKeyUpdateOne) ClearName() *ApiKeyUpdateOne { + _u.mutation.ClearName() + return _u +} + +// SetPrefix sets the "prefix" field. +func (_u *ApiKeyUpdateOne) SetPrefix(v string) *ApiKeyUpdateOne { + _u.mutation.SetPrefix(v) + return _u +} + +// SetNillablePrefix sets the "prefix" field if the given value is not nil. +func (_u *ApiKeyUpdateOne) SetNillablePrefix(v *string) *ApiKeyUpdateOne { + if v != nil { + _u.SetPrefix(*v) + } + return _u +} + +// ClearPrefix clears the value of the "prefix" field. +func (_u *ApiKeyUpdateOne) ClearPrefix() *ApiKeyUpdateOne { + _u.mutation.ClearPrefix() + return _u +} + +// SetKeyHash sets the "key_hash" field. +func (_u *ApiKeyUpdateOne) SetKeyHash(v string) *ApiKeyUpdateOne { + _u.mutation.SetKeyHash(v) + return _u +} + +// SetNillableKeyHash sets the "key_hash" field if the given value is not nil. +func (_u *ApiKeyUpdateOne) SetNillableKeyHash(v *string) *ApiKeyUpdateOne { + if v != nil { + _u.SetKeyHash(*v) + } + return _u +} + +// SetScopes sets the "scopes" field. +func (_u *ApiKeyUpdateOne) SetScopes(v string) *ApiKeyUpdateOne { + _u.mutation.SetScopes(v) + return _u +} + +// SetNillableScopes sets the "scopes" field if the given value is not nil. +func (_u *ApiKeyUpdateOne) SetNillableScopes(v *string) *ApiKeyUpdateOne { + if v != nil { + _u.SetScopes(*v) + } + return _u +} + +// ClearScopes clears the value of the "scopes" field. +func (_u *ApiKeyUpdateOne) ClearScopes() *ApiKeyUpdateOne { + _u.mutation.ClearScopes() + return _u +} + +// SetRevoked sets the "revoked" field. +func (_u *ApiKeyUpdateOne) SetRevoked(v bool) *ApiKeyUpdateOne { + _u.mutation.SetRevoked(v) + return _u +} + +// SetNillableRevoked sets the "revoked" field if the given value is not nil. +func (_u *ApiKeyUpdateOne) SetNillableRevoked(v *bool) *ApiKeyUpdateOne { + if v != nil { + _u.SetRevoked(*v) + } + return _u +} + +// SetExpiresAt sets the "expires_at" field. +func (_u *ApiKeyUpdateOne) SetExpiresAt(v time.Time) *ApiKeyUpdateOne { + _u.mutation.SetExpiresAt(v) + return _u +} + +// SetNillableExpiresAt sets the "expires_at" field if the given value is not nil. +func (_u *ApiKeyUpdateOne) SetNillableExpiresAt(v *time.Time) *ApiKeyUpdateOne { + if v != nil { + _u.SetExpiresAt(*v) + } + return _u +} + +// ClearExpiresAt clears the value of the "expires_at" field. +func (_u *ApiKeyUpdateOne) ClearExpiresAt() *ApiKeyUpdateOne { + _u.mutation.ClearExpiresAt() + return _u +} + +// SetLastUsed sets the "last_used" field. +func (_u *ApiKeyUpdateOne) SetLastUsed(v time.Time) *ApiKeyUpdateOne { + _u.mutation.SetLastUsed(v) + return _u +} + +// SetNillableLastUsed sets the "last_used" field if the given value is not nil. +func (_u *ApiKeyUpdateOne) SetNillableLastUsed(v *time.Time) *ApiKeyUpdateOne { + if v != nil { + _u.SetLastUsed(*v) + } + return _u +} + +// ClearLastUsed clears the value of the "last_used" field. +func (_u *ApiKeyUpdateOne) ClearLastUsed() *ApiKeyUpdateOne { + _u.mutation.ClearLastUsed() + return _u +} + +// Mutation returns the ApiKeyMutation object of the builder. +func (_u *ApiKeyUpdateOne) Mutation() *ApiKeyMutation { + return _u.mutation +} + +// Where appends a list predicates to the ApiKeyUpdate builder. +func (_u *ApiKeyUpdateOne) Where(ps ...predicate.ApiKey) *ApiKeyUpdateOne { + _u.mutation.Where(ps...) + return _u +} + +// Select allows selecting one or more fields (columns) of the returned entity. +// The default is selecting all fields defined in the entity schema. +func (_u *ApiKeyUpdateOne) Select(field string, fields ...string) *ApiKeyUpdateOne { + _u.fields = append([]string{field}, fields...) + return _u +} + +// Save executes the query and returns the updated ApiKey entity. +func (_u *ApiKeyUpdateOne) Save(ctx context.Context) (*ApiKey, error) { + return withHooks(ctx, _u.sqlSave, _u.mutation, _u.hooks) +} + +// SaveX is like Save, but panics if an error occurs. +func (_u *ApiKeyUpdateOne) SaveX(ctx context.Context) *ApiKey { + node, err := _u.Save(ctx) + if err != nil { + panic(err) + } + return node +} + +// Exec executes the query on the entity. +func (_u *ApiKeyUpdateOne) Exec(ctx context.Context) error { + _, err := _u.Save(ctx) + return err +} + +// ExecX is like Exec, but panics if an error occurs. +func (_u *ApiKeyUpdateOne) ExecX(ctx context.Context) { + if err := _u.Exec(ctx); err != nil { + panic(err) + } +} + +// check runs all checks and user-defined validators on the builder. +func (_u *ApiKeyUpdateOne) check() error { + if v, ok := _u.mutation.KeyHash(); ok { + if err := apikey.KeyHashValidator(v); err != nil { + return &ValidationError{Name: "key_hash", err: fmt.Errorf(`ent: validator failed for field "ApiKey.key_hash": %w`, err)} + } + } + return nil +} + +func (_u *ApiKeyUpdateOne) sqlSave(ctx context.Context) (_node *ApiKey, err error) { + if err := _u.check(); err != nil { + return _node, err + } + _spec := sqlgraph.NewUpdateSpec(apikey.Table, apikey.Columns, sqlgraph.NewFieldSpec(apikey.FieldID, field.TypeUUID)) + id, ok := _u.mutation.ID() + if !ok { + return nil, &ValidationError{Name: "id", err: errors.New(`ent: missing "ApiKey.id" for update`)} + } + _spec.Node.ID.Value = id + if fields := _u.fields; len(fields) > 0 { + _spec.Node.Columns = make([]string, 0, len(fields)) + _spec.Node.Columns = append(_spec.Node.Columns, apikey.FieldID) + for _, f := range fields { + if !apikey.ValidColumn(f) { + return nil, &ValidationError{Name: f, err: fmt.Errorf("ent: invalid field %q for query", f)} + } + if f != apikey.FieldID { + _spec.Node.Columns = append(_spec.Node.Columns, f) + } + } + } + if ps := _u.mutation.predicates; len(ps) > 0 { + _spec.Predicate = func(selector *sql.Selector) { + for i := range ps { + ps[i](selector) + } + } + } + if value, ok := _u.mutation.UserID(); ok { + _spec.SetField(apikey.FieldUserID, field.TypeUUID, value) + } + if value, ok := _u.mutation.Name(); ok { + _spec.SetField(apikey.FieldName, field.TypeString, value) + } + if _u.mutation.NameCleared() { + _spec.ClearField(apikey.FieldName, field.TypeString) + } + if value, ok := _u.mutation.Prefix(); ok { + _spec.SetField(apikey.FieldPrefix, field.TypeString, value) + } + if _u.mutation.PrefixCleared() { + _spec.ClearField(apikey.FieldPrefix, field.TypeString) + } + if value, ok := _u.mutation.KeyHash(); ok { + _spec.SetField(apikey.FieldKeyHash, field.TypeString, value) + } + if value, ok := _u.mutation.Scopes(); ok { + _spec.SetField(apikey.FieldScopes, field.TypeString, value) + } + if _u.mutation.ScopesCleared() { + _spec.ClearField(apikey.FieldScopes, field.TypeString) + } + if value, ok := _u.mutation.Revoked(); ok { + _spec.SetField(apikey.FieldRevoked, field.TypeBool, value) + } + if value, ok := _u.mutation.ExpiresAt(); ok { + _spec.SetField(apikey.FieldExpiresAt, field.TypeTime, value) + } + if _u.mutation.ExpiresAtCleared() { + _spec.ClearField(apikey.FieldExpiresAt, field.TypeTime) + } + if value, ok := _u.mutation.LastUsed(); ok { + _spec.SetField(apikey.FieldLastUsed, field.TypeTime, value) + } + if _u.mutation.LastUsedCleared() { + _spec.ClearField(apikey.FieldLastUsed, field.TypeTime) + } + _node = &ApiKey{config: _u.config} + _spec.Assign = _node.assignValues + _spec.ScanValues = _node.scanValues + if err = sqlgraph.UpdateNode(ctx, _u.driver, _spec); err != nil { + if _, ok := err.(*sqlgraph.NotFoundError); ok { + err = &NotFoundError{apikey.Label} + } else if sqlgraph.IsConstraintError(err) { + err = &ConstraintError{msg: err.Error(), wrap: err} + } + return nil, err + } + _u.mutation.done = true + return _node, nil +} diff --git a/pkg/ent/brokerdispatch.go b/pkg/ent/brokerdispatch.go new file mode 100644 index 000000000..5fe957779 --- /dev/null +++ b/pkg/ent/brokerdispatch.go @@ -0,0 +1,263 @@ +// Code generated by ent, DO NOT EDIT. + +package ent + +import ( + "fmt" + "strings" + "time" + + "entgo.io/ent" + "entgo.io/ent/dialect/sql" + "github.com/GoogleCloudPlatform/scion/pkg/ent/brokerdispatch" + "github.com/google/uuid" +) + +// BrokerDispatch is the model entity for the BrokerDispatch schema. +type BrokerDispatch struct { + config `json:"-"` + // ID of the ent. + ID uuid.UUID `json:"id,omitempty"` + // BrokerID holds the value of the "broker_id" field. + BrokerID uuid.UUID `json:"broker_id,omitempty"` + // AgentID holds the value of the "agent_id" field. + AgentID *uuid.UUID `json:"agent_id,omitempty"` + // AgentSlug holds the value of the "agent_slug" field. + AgentSlug string `json:"agent_slug,omitempty"` + // ProjectID holds the value of the "project_id" field. + ProjectID *uuid.UUID `json:"project_id,omitempty"` + // Op holds the value of the "op" field. + Op string `json:"op,omitempty"` + // Args holds the value of the "args" field. + Args string `json:"args,omitempty"` + // State holds the value of the "state" field. + State string `json:"state,omitempty"` + // Result holds the value of the "result" field. + Result string `json:"result,omitempty"` + // ClaimedBy holds the value of the "claimed_by" field. + ClaimedBy string `json:"claimed_by,omitempty"` + // Attempts holds the value of the "attempts" field. + Attempts int `json:"attempts,omitempty"` + // Error holds the value of the "error" field. + Error string `json:"error,omitempty"` + // CreatedAt holds the value of the "created_at" field. + CreatedAt time.Time `json:"created_at,omitempty"` + // UpdatedAt holds the value of the "updated_at" field. + UpdatedAt time.Time `json:"updated_at,omitempty"` + // DeadlineAt holds the value of the "deadline_at" field. + DeadlineAt *time.Time `json:"deadline_at,omitempty"` + selectValues sql.SelectValues +} + +// scanValues returns the types for scanning values from sql.Rows. +func (*BrokerDispatch) scanValues(columns []string) ([]any, error) { + values := make([]any, len(columns)) + for i := range columns { + switch columns[i] { + case brokerdispatch.FieldAgentID, brokerdispatch.FieldProjectID: + values[i] = &sql.NullScanner{S: new(uuid.UUID)} + case brokerdispatch.FieldAttempts: + values[i] = new(sql.NullInt64) + case brokerdispatch.FieldAgentSlug, brokerdispatch.FieldOp, brokerdispatch.FieldArgs, brokerdispatch.FieldState, brokerdispatch.FieldResult, brokerdispatch.FieldClaimedBy, brokerdispatch.FieldError: + values[i] = new(sql.NullString) + case brokerdispatch.FieldCreatedAt, brokerdispatch.FieldUpdatedAt, brokerdispatch.FieldDeadlineAt: + values[i] = new(sql.NullTime) + case brokerdispatch.FieldID, brokerdispatch.FieldBrokerID: + values[i] = new(uuid.UUID) + default: + values[i] = new(sql.UnknownType) + } + } + return values, nil +} + +// assignValues assigns the values that were returned from sql.Rows (after scanning) +// to the BrokerDispatch fields. +func (_m *BrokerDispatch) assignValues(columns []string, values []any) error { + if m, n := len(values), len(columns); m < n { + return fmt.Errorf("mismatch number of scan values: %d != %d", m, n) + } + for i := range columns { + switch columns[i] { + case brokerdispatch.FieldID: + if value, ok := values[i].(*uuid.UUID); !ok { + return fmt.Errorf("unexpected type %T for field id", values[i]) + } else if value != nil { + _m.ID = *value + } + case brokerdispatch.FieldBrokerID: + if value, ok := values[i].(*uuid.UUID); !ok { + return fmt.Errorf("unexpected type %T for field broker_id", values[i]) + } else if value != nil { + _m.BrokerID = *value + } + case brokerdispatch.FieldAgentID: + if value, ok := values[i].(*sql.NullScanner); !ok { + return fmt.Errorf("unexpected type %T for field agent_id", values[i]) + } else if value.Valid { + _m.AgentID = new(uuid.UUID) + *_m.AgentID = *value.S.(*uuid.UUID) + } + case brokerdispatch.FieldAgentSlug: + if value, ok := values[i].(*sql.NullString); !ok { + return fmt.Errorf("unexpected type %T for field agent_slug", values[i]) + } else if value.Valid { + _m.AgentSlug = value.String + } + case brokerdispatch.FieldProjectID: + if value, ok := values[i].(*sql.NullScanner); !ok { + return fmt.Errorf("unexpected type %T for field project_id", values[i]) + } else if value.Valid { + _m.ProjectID = new(uuid.UUID) + *_m.ProjectID = *value.S.(*uuid.UUID) + } + case brokerdispatch.FieldOp: + if value, ok := values[i].(*sql.NullString); !ok { + return fmt.Errorf("unexpected type %T for field op", values[i]) + } else if value.Valid { + _m.Op = value.String + } + case brokerdispatch.FieldArgs: + if value, ok := values[i].(*sql.NullString); !ok { + return fmt.Errorf("unexpected type %T for field args", values[i]) + } else if value.Valid { + _m.Args = value.String + } + case brokerdispatch.FieldState: + if value, ok := values[i].(*sql.NullString); !ok { + return fmt.Errorf("unexpected type %T for field state", values[i]) + } else if value.Valid { + _m.State = value.String + } + case brokerdispatch.FieldResult: + if value, ok := values[i].(*sql.NullString); !ok { + return fmt.Errorf("unexpected type %T for field result", values[i]) + } else if value.Valid { + _m.Result = value.String + } + case brokerdispatch.FieldClaimedBy: + if value, ok := values[i].(*sql.NullString); !ok { + return fmt.Errorf("unexpected type %T for field claimed_by", values[i]) + } else if value.Valid { + _m.ClaimedBy = value.String + } + case brokerdispatch.FieldAttempts: + if value, ok := values[i].(*sql.NullInt64); !ok { + return fmt.Errorf("unexpected type %T for field attempts", values[i]) + } else if value.Valid { + _m.Attempts = int(value.Int64) + } + case brokerdispatch.FieldError: + if value, ok := values[i].(*sql.NullString); !ok { + return fmt.Errorf("unexpected type %T for field error", values[i]) + } else if value.Valid { + _m.Error = value.String + } + case brokerdispatch.FieldCreatedAt: + if value, ok := values[i].(*sql.NullTime); !ok { + return fmt.Errorf("unexpected type %T for field created_at", values[i]) + } else if value.Valid { + _m.CreatedAt = value.Time + } + case brokerdispatch.FieldUpdatedAt: + if value, ok := values[i].(*sql.NullTime); !ok { + return fmt.Errorf("unexpected type %T for field updated_at", values[i]) + } else if value.Valid { + _m.UpdatedAt = value.Time + } + case brokerdispatch.FieldDeadlineAt: + if value, ok := values[i].(*sql.NullTime); !ok { + return fmt.Errorf("unexpected type %T for field deadline_at", values[i]) + } else if value.Valid { + _m.DeadlineAt = new(time.Time) + *_m.DeadlineAt = value.Time + } + default: + _m.selectValues.Set(columns[i], values[i]) + } + } + return nil +} + +// Value returns the ent.Value that was dynamically selected and assigned to the BrokerDispatch. +// This includes values selected through modifiers, order, etc. +func (_m *BrokerDispatch) Value(name string) (ent.Value, error) { + return _m.selectValues.Get(name) +} + +// Update returns a builder for updating this BrokerDispatch. +// Note that you need to call BrokerDispatch.Unwrap() before calling this method if this BrokerDispatch +// was returned from a transaction, and the transaction was committed or rolled back. +func (_m *BrokerDispatch) Update() *BrokerDispatchUpdateOne { + return NewBrokerDispatchClient(_m.config).UpdateOne(_m) +} + +// Unwrap unwraps the BrokerDispatch entity that was returned from a transaction after it was closed, +// so that all future queries will be executed through the driver which created the transaction. +func (_m *BrokerDispatch) Unwrap() *BrokerDispatch { + _tx, ok := _m.config.driver.(*txDriver) + if !ok { + panic("ent: BrokerDispatch is not a transactional entity") + } + _m.config.driver = _tx.drv + return _m +} + +// String implements the fmt.Stringer. +func (_m *BrokerDispatch) String() string { + var builder strings.Builder + builder.WriteString("BrokerDispatch(") + builder.WriteString(fmt.Sprintf("id=%v, ", _m.ID)) + builder.WriteString("broker_id=") + builder.WriteString(fmt.Sprintf("%v", _m.BrokerID)) + builder.WriteString(", ") + if v := _m.AgentID; v != nil { + builder.WriteString("agent_id=") + builder.WriteString(fmt.Sprintf("%v", *v)) + } + builder.WriteString(", ") + builder.WriteString("agent_slug=") + builder.WriteString(_m.AgentSlug) + builder.WriteString(", ") + if v := _m.ProjectID; v != nil { + builder.WriteString("project_id=") + builder.WriteString(fmt.Sprintf("%v", *v)) + } + builder.WriteString(", ") + builder.WriteString("op=") + builder.WriteString(_m.Op) + builder.WriteString(", ") + builder.WriteString("args=") + builder.WriteString(_m.Args) + builder.WriteString(", ") + builder.WriteString("state=") + builder.WriteString(_m.State) + builder.WriteString(", ") + builder.WriteString("result=") + builder.WriteString(_m.Result) + builder.WriteString(", ") + builder.WriteString("claimed_by=") + builder.WriteString(_m.ClaimedBy) + builder.WriteString(", ") + builder.WriteString("attempts=") + builder.WriteString(fmt.Sprintf("%v", _m.Attempts)) + builder.WriteString(", ") + builder.WriteString("error=") + builder.WriteString(_m.Error) + builder.WriteString(", ") + builder.WriteString("created_at=") + builder.WriteString(_m.CreatedAt.Format(time.ANSIC)) + builder.WriteString(", ") + builder.WriteString("updated_at=") + builder.WriteString(_m.UpdatedAt.Format(time.ANSIC)) + builder.WriteString(", ") + if v := _m.DeadlineAt; v != nil { + builder.WriteString("deadline_at=") + builder.WriteString(v.Format(time.ANSIC)) + } + builder.WriteByte(')') + return builder.String() +} + +// BrokerDispatches is a parsable slice of BrokerDispatch. +type BrokerDispatches []*BrokerDispatch diff --git a/pkg/ent/brokerdispatch/brokerdispatch.go b/pkg/ent/brokerdispatch/brokerdispatch.go new file mode 100644 index 000000000..a7968ca0f --- /dev/null +++ b/pkg/ent/brokerdispatch/brokerdispatch.go @@ -0,0 +1,171 @@ +// Code generated by ent, DO NOT EDIT. + +package brokerdispatch + +import ( + "time" + + "entgo.io/ent/dialect/sql" + "github.com/google/uuid" +) + +const ( + // Label holds the string label denoting the brokerdispatch type in the database. + Label = "broker_dispatch" + // FieldID holds the string denoting the id field in the database. + FieldID = "id" + // FieldBrokerID holds the string denoting the broker_id field in the database. + FieldBrokerID = "broker_id" + // FieldAgentID holds the string denoting the agent_id field in the database. + FieldAgentID = "agent_id" + // FieldAgentSlug holds the string denoting the agent_slug field in the database. + FieldAgentSlug = "agent_slug" + // FieldProjectID holds the string denoting the project_id field in the database. + FieldProjectID = "project_id" + // FieldOp holds the string denoting the op field in the database. + FieldOp = "op" + // FieldArgs holds the string denoting the args field in the database. + FieldArgs = "args" + // FieldState holds the string denoting the state field in the database. + FieldState = "state" + // FieldResult holds the string denoting the result field in the database. + FieldResult = "result" + // FieldClaimedBy holds the string denoting the claimed_by field in the database. + FieldClaimedBy = "claimed_by" + // FieldAttempts holds the string denoting the attempts field in the database. + FieldAttempts = "attempts" + // FieldError holds the string denoting the error field in the database. + FieldError = "error" + // FieldCreatedAt holds the string denoting the created_at field in the database. + FieldCreatedAt = "created_at" + // FieldUpdatedAt holds the string denoting the updated_at field in the database. + FieldUpdatedAt = "updated_at" + // FieldDeadlineAt holds the string denoting the deadline_at field in the database. + FieldDeadlineAt = "deadline_at" + // Table holds the table name of the brokerdispatch in the database. + Table = "broker_dispatch" +) + +// Columns holds all SQL columns for brokerdispatch fields. +var Columns = []string{ + FieldID, + FieldBrokerID, + FieldAgentID, + FieldAgentSlug, + FieldProjectID, + FieldOp, + FieldArgs, + FieldState, + FieldResult, + FieldClaimedBy, + FieldAttempts, + FieldError, + FieldCreatedAt, + FieldUpdatedAt, + FieldDeadlineAt, +} + +// ValidColumn reports if the column name is valid (part of the table columns). +func ValidColumn(column string) bool { + for i := range Columns { + if column == Columns[i] { + return true + } + } + return false +} + +var ( + // OpValidator is a validator for the "op" field. It is called by the builders before save. + OpValidator func(string) error + // DefaultState holds the default value on creation for the "state" field. + DefaultState string + // DefaultAttempts holds the default value on creation for the "attempts" field. + DefaultAttempts int + // DefaultCreatedAt holds the default value on creation for the "created_at" field. + DefaultCreatedAt func() time.Time + // DefaultUpdatedAt holds the default value on creation for the "updated_at" field. + DefaultUpdatedAt func() time.Time + // UpdateDefaultUpdatedAt holds the default value on update for the "updated_at" field. + UpdateDefaultUpdatedAt func() time.Time + // DefaultID holds the default value on creation for the "id" field. + DefaultID func() uuid.UUID +) + +// OrderOption defines the ordering options for the BrokerDispatch queries. +type OrderOption func(*sql.Selector) + +// ByID orders the results by the id field. +func ByID(opts ...sql.OrderTermOption) OrderOption { + return sql.OrderByField(FieldID, opts...).ToFunc() +} + +// ByBrokerID orders the results by the broker_id field. +func ByBrokerID(opts ...sql.OrderTermOption) OrderOption { + return sql.OrderByField(FieldBrokerID, opts...).ToFunc() +} + +// ByAgentID orders the results by the agent_id field. +func ByAgentID(opts ...sql.OrderTermOption) OrderOption { + return sql.OrderByField(FieldAgentID, opts...).ToFunc() +} + +// ByAgentSlug orders the results by the agent_slug field. +func ByAgentSlug(opts ...sql.OrderTermOption) OrderOption { + return sql.OrderByField(FieldAgentSlug, opts...).ToFunc() +} + +// ByProjectID orders the results by the project_id field. +func ByProjectID(opts ...sql.OrderTermOption) OrderOption { + return sql.OrderByField(FieldProjectID, opts...).ToFunc() +} + +// ByOp orders the results by the op field. +func ByOp(opts ...sql.OrderTermOption) OrderOption { + return sql.OrderByField(FieldOp, opts...).ToFunc() +} + +// ByArgs orders the results by the args field. +func ByArgs(opts ...sql.OrderTermOption) OrderOption { + return sql.OrderByField(FieldArgs, opts...).ToFunc() +} + +// ByState orders the results by the state field. +func ByState(opts ...sql.OrderTermOption) OrderOption { + return sql.OrderByField(FieldState, opts...).ToFunc() +} + +// ByResult orders the results by the result field. +func ByResult(opts ...sql.OrderTermOption) OrderOption { + return sql.OrderByField(FieldResult, opts...).ToFunc() +} + +// ByClaimedBy orders the results by the claimed_by field. +func ByClaimedBy(opts ...sql.OrderTermOption) OrderOption { + return sql.OrderByField(FieldClaimedBy, opts...).ToFunc() +} + +// ByAttempts orders the results by the attempts field. +func ByAttempts(opts ...sql.OrderTermOption) OrderOption { + return sql.OrderByField(FieldAttempts, opts...).ToFunc() +} + +// ByError orders the results by the error field. +func ByError(opts ...sql.OrderTermOption) OrderOption { + return sql.OrderByField(FieldError, opts...).ToFunc() +} + +// ByCreatedAt orders the results by the created_at field. +func ByCreatedAt(opts ...sql.OrderTermOption) OrderOption { + return sql.OrderByField(FieldCreatedAt, opts...).ToFunc() +} + +// ByUpdatedAt orders the results by the updated_at field. +func ByUpdatedAt(opts ...sql.OrderTermOption) OrderOption { + return sql.OrderByField(FieldUpdatedAt, opts...).ToFunc() +} + +// ByDeadlineAt orders the results by the deadline_at field. +func ByDeadlineAt(opts ...sql.OrderTermOption) OrderOption { + return sql.OrderByField(FieldDeadlineAt, opts...).ToFunc() +} diff --git a/pkg/ent/brokerdispatch/where.go b/pkg/ent/brokerdispatch/where.go new file mode 100644 index 000000000..459128180 --- /dev/null +++ b/pkg/ent/brokerdispatch/where.go @@ -0,0 +1,956 @@ +// Code generated by ent, DO NOT EDIT. + +package brokerdispatch + +import ( + "time" + + "entgo.io/ent/dialect/sql" + "github.com/GoogleCloudPlatform/scion/pkg/ent/predicate" + "github.com/google/uuid" +) + +// ID filters vertices based on their ID field. +func ID(id uuid.UUID) predicate.BrokerDispatch { + return predicate.BrokerDispatch(sql.FieldEQ(FieldID, id)) +} + +// IDEQ applies the EQ predicate on the ID field. +func IDEQ(id uuid.UUID) predicate.BrokerDispatch { + return predicate.BrokerDispatch(sql.FieldEQ(FieldID, id)) +} + +// IDNEQ applies the NEQ predicate on the ID field. +func IDNEQ(id uuid.UUID) predicate.BrokerDispatch { + return predicate.BrokerDispatch(sql.FieldNEQ(FieldID, id)) +} + +// IDIn applies the In predicate on the ID field. +func IDIn(ids ...uuid.UUID) predicate.BrokerDispatch { + return predicate.BrokerDispatch(sql.FieldIn(FieldID, ids...)) +} + +// IDNotIn applies the NotIn predicate on the ID field. +func IDNotIn(ids ...uuid.UUID) predicate.BrokerDispatch { + return predicate.BrokerDispatch(sql.FieldNotIn(FieldID, ids...)) +} + +// IDGT applies the GT predicate on the ID field. +func IDGT(id uuid.UUID) predicate.BrokerDispatch { + return predicate.BrokerDispatch(sql.FieldGT(FieldID, id)) +} + +// IDGTE applies the GTE predicate on the ID field. +func IDGTE(id uuid.UUID) predicate.BrokerDispatch { + return predicate.BrokerDispatch(sql.FieldGTE(FieldID, id)) +} + +// IDLT applies the LT predicate on the ID field. +func IDLT(id uuid.UUID) predicate.BrokerDispatch { + return predicate.BrokerDispatch(sql.FieldLT(FieldID, id)) +} + +// IDLTE applies the LTE predicate on the ID field. +func IDLTE(id uuid.UUID) predicate.BrokerDispatch { + return predicate.BrokerDispatch(sql.FieldLTE(FieldID, id)) +} + +// BrokerID applies equality check predicate on the "broker_id" field. It's identical to BrokerIDEQ. +func BrokerID(v uuid.UUID) predicate.BrokerDispatch { + return predicate.BrokerDispatch(sql.FieldEQ(FieldBrokerID, v)) +} + +// AgentID applies equality check predicate on the "agent_id" field. It's identical to AgentIDEQ. +func AgentID(v uuid.UUID) predicate.BrokerDispatch { + return predicate.BrokerDispatch(sql.FieldEQ(FieldAgentID, v)) +} + +// AgentSlug applies equality check predicate on the "agent_slug" field. It's identical to AgentSlugEQ. +func AgentSlug(v string) predicate.BrokerDispatch { + return predicate.BrokerDispatch(sql.FieldEQ(FieldAgentSlug, v)) +} + +// ProjectID applies equality check predicate on the "project_id" field. It's identical to ProjectIDEQ. +func ProjectID(v uuid.UUID) predicate.BrokerDispatch { + return predicate.BrokerDispatch(sql.FieldEQ(FieldProjectID, v)) +} + +// Op applies equality check predicate on the "op" field. It's identical to OpEQ. +func Op(v string) predicate.BrokerDispatch { + return predicate.BrokerDispatch(sql.FieldEQ(FieldOp, v)) +} + +// Args applies equality check predicate on the "args" field. It's identical to ArgsEQ. +func Args(v string) predicate.BrokerDispatch { + return predicate.BrokerDispatch(sql.FieldEQ(FieldArgs, v)) +} + +// State applies equality check predicate on the "state" field. It's identical to StateEQ. +func State(v string) predicate.BrokerDispatch { + return predicate.BrokerDispatch(sql.FieldEQ(FieldState, v)) +} + +// Result applies equality check predicate on the "result" field. It's identical to ResultEQ. +func Result(v string) predicate.BrokerDispatch { + return predicate.BrokerDispatch(sql.FieldEQ(FieldResult, v)) +} + +// ClaimedBy applies equality check predicate on the "claimed_by" field. It's identical to ClaimedByEQ. +func ClaimedBy(v string) predicate.BrokerDispatch { + return predicate.BrokerDispatch(sql.FieldEQ(FieldClaimedBy, v)) +} + +// Attempts applies equality check predicate on the "attempts" field. It's identical to AttemptsEQ. +func Attempts(v int) predicate.BrokerDispatch { + return predicate.BrokerDispatch(sql.FieldEQ(FieldAttempts, v)) +} + +// Error applies equality check predicate on the "error" field. It's identical to ErrorEQ. +func Error(v string) predicate.BrokerDispatch { + return predicate.BrokerDispatch(sql.FieldEQ(FieldError, v)) +} + +// CreatedAt applies equality check predicate on the "created_at" field. It's identical to CreatedAtEQ. +func CreatedAt(v time.Time) predicate.BrokerDispatch { + return predicate.BrokerDispatch(sql.FieldEQ(FieldCreatedAt, v)) +} + +// UpdatedAt applies equality check predicate on the "updated_at" field. It's identical to UpdatedAtEQ. +func UpdatedAt(v time.Time) predicate.BrokerDispatch { + return predicate.BrokerDispatch(sql.FieldEQ(FieldUpdatedAt, v)) +} + +// DeadlineAt applies equality check predicate on the "deadline_at" field. It's identical to DeadlineAtEQ. +func DeadlineAt(v time.Time) predicate.BrokerDispatch { + return predicate.BrokerDispatch(sql.FieldEQ(FieldDeadlineAt, v)) +} + +// BrokerIDEQ applies the EQ predicate on the "broker_id" field. +func BrokerIDEQ(v uuid.UUID) predicate.BrokerDispatch { + return predicate.BrokerDispatch(sql.FieldEQ(FieldBrokerID, v)) +} + +// BrokerIDNEQ applies the NEQ predicate on the "broker_id" field. +func BrokerIDNEQ(v uuid.UUID) predicate.BrokerDispatch { + return predicate.BrokerDispatch(sql.FieldNEQ(FieldBrokerID, v)) +} + +// BrokerIDIn applies the In predicate on the "broker_id" field. +func BrokerIDIn(vs ...uuid.UUID) predicate.BrokerDispatch { + return predicate.BrokerDispatch(sql.FieldIn(FieldBrokerID, vs...)) +} + +// BrokerIDNotIn applies the NotIn predicate on the "broker_id" field. +func BrokerIDNotIn(vs ...uuid.UUID) predicate.BrokerDispatch { + return predicate.BrokerDispatch(sql.FieldNotIn(FieldBrokerID, vs...)) +} + +// BrokerIDGT applies the GT predicate on the "broker_id" field. +func BrokerIDGT(v uuid.UUID) predicate.BrokerDispatch { + return predicate.BrokerDispatch(sql.FieldGT(FieldBrokerID, v)) +} + +// BrokerIDGTE applies the GTE predicate on the "broker_id" field. +func BrokerIDGTE(v uuid.UUID) predicate.BrokerDispatch { + return predicate.BrokerDispatch(sql.FieldGTE(FieldBrokerID, v)) +} + +// BrokerIDLT applies the LT predicate on the "broker_id" field. +func BrokerIDLT(v uuid.UUID) predicate.BrokerDispatch { + return predicate.BrokerDispatch(sql.FieldLT(FieldBrokerID, v)) +} + +// BrokerIDLTE applies the LTE predicate on the "broker_id" field. +func BrokerIDLTE(v uuid.UUID) predicate.BrokerDispatch { + return predicate.BrokerDispatch(sql.FieldLTE(FieldBrokerID, v)) +} + +// AgentIDEQ applies the EQ predicate on the "agent_id" field. +func AgentIDEQ(v uuid.UUID) predicate.BrokerDispatch { + return predicate.BrokerDispatch(sql.FieldEQ(FieldAgentID, v)) +} + +// AgentIDNEQ applies the NEQ predicate on the "agent_id" field. +func AgentIDNEQ(v uuid.UUID) predicate.BrokerDispatch { + return predicate.BrokerDispatch(sql.FieldNEQ(FieldAgentID, v)) +} + +// AgentIDIn applies the In predicate on the "agent_id" field. +func AgentIDIn(vs ...uuid.UUID) predicate.BrokerDispatch { + return predicate.BrokerDispatch(sql.FieldIn(FieldAgentID, vs...)) +} + +// AgentIDNotIn applies the NotIn predicate on the "agent_id" field. +func AgentIDNotIn(vs ...uuid.UUID) predicate.BrokerDispatch { + return predicate.BrokerDispatch(sql.FieldNotIn(FieldAgentID, vs...)) +} + +// AgentIDGT applies the GT predicate on the "agent_id" field. +func AgentIDGT(v uuid.UUID) predicate.BrokerDispatch { + return predicate.BrokerDispatch(sql.FieldGT(FieldAgentID, v)) +} + +// AgentIDGTE applies the GTE predicate on the "agent_id" field. +func AgentIDGTE(v uuid.UUID) predicate.BrokerDispatch { + return predicate.BrokerDispatch(sql.FieldGTE(FieldAgentID, v)) +} + +// AgentIDLT applies the LT predicate on the "agent_id" field. +func AgentIDLT(v uuid.UUID) predicate.BrokerDispatch { + return predicate.BrokerDispatch(sql.FieldLT(FieldAgentID, v)) +} + +// AgentIDLTE applies the LTE predicate on the "agent_id" field. +func AgentIDLTE(v uuid.UUID) predicate.BrokerDispatch { + return predicate.BrokerDispatch(sql.FieldLTE(FieldAgentID, v)) +} + +// AgentIDIsNil applies the IsNil predicate on the "agent_id" field. +func AgentIDIsNil() predicate.BrokerDispatch { + return predicate.BrokerDispatch(sql.FieldIsNull(FieldAgentID)) +} + +// AgentIDNotNil applies the NotNil predicate on the "agent_id" field. +func AgentIDNotNil() predicate.BrokerDispatch { + return predicate.BrokerDispatch(sql.FieldNotNull(FieldAgentID)) +} + +// AgentSlugEQ applies the EQ predicate on the "agent_slug" field. +func AgentSlugEQ(v string) predicate.BrokerDispatch { + return predicate.BrokerDispatch(sql.FieldEQ(FieldAgentSlug, v)) +} + +// AgentSlugNEQ applies the NEQ predicate on the "agent_slug" field. +func AgentSlugNEQ(v string) predicate.BrokerDispatch { + return predicate.BrokerDispatch(sql.FieldNEQ(FieldAgentSlug, v)) +} + +// AgentSlugIn applies the In predicate on the "agent_slug" field. +func AgentSlugIn(vs ...string) predicate.BrokerDispatch { + return predicate.BrokerDispatch(sql.FieldIn(FieldAgentSlug, vs...)) +} + +// AgentSlugNotIn applies the NotIn predicate on the "agent_slug" field. +func AgentSlugNotIn(vs ...string) predicate.BrokerDispatch { + return predicate.BrokerDispatch(sql.FieldNotIn(FieldAgentSlug, vs...)) +} + +// AgentSlugGT applies the GT predicate on the "agent_slug" field. +func AgentSlugGT(v string) predicate.BrokerDispatch { + return predicate.BrokerDispatch(sql.FieldGT(FieldAgentSlug, v)) +} + +// AgentSlugGTE applies the GTE predicate on the "agent_slug" field. +func AgentSlugGTE(v string) predicate.BrokerDispatch { + return predicate.BrokerDispatch(sql.FieldGTE(FieldAgentSlug, v)) +} + +// AgentSlugLT applies the LT predicate on the "agent_slug" field. +func AgentSlugLT(v string) predicate.BrokerDispatch { + return predicate.BrokerDispatch(sql.FieldLT(FieldAgentSlug, v)) +} + +// AgentSlugLTE applies the LTE predicate on the "agent_slug" field. +func AgentSlugLTE(v string) predicate.BrokerDispatch { + return predicate.BrokerDispatch(sql.FieldLTE(FieldAgentSlug, v)) +} + +// AgentSlugContains applies the Contains predicate on the "agent_slug" field. +func AgentSlugContains(v string) predicate.BrokerDispatch { + return predicate.BrokerDispatch(sql.FieldContains(FieldAgentSlug, v)) +} + +// AgentSlugHasPrefix applies the HasPrefix predicate on the "agent_slug" field. +func AgentSlugHasPrefix(v string) predicate.BrokerDispatch { + return predicate.BrokerDispatch(sql.FieldHasPrefix(FieldAgentSlug, v)) +} + +// AgentSlugHasSuffix applies the HasSuffix predicate on the "agent_slug" field. +func AgentSlugHasSuffix(v string) predicate.BrokerDispatch { + return predicate.BrokerDispatch(sql.FieldHasSuffix(FieldAgentSlug, v)) +} + +// AgentSlugIsNil applies the IsNil predicate on the "agent_slug" field. +func AgentSlugIsNil() predicate.BrokerDispatch { + return predicate.BrokerDispatch(sql.FieldIsNull(FieldAgentSlug)) +} + +// AgentSlugNotNil applies the NotNil predicate on the "agent_slug" field. +func AgentSlugNotNil() predicate.BrokerDispatch { + return predicate.BrokerDispatch(sql.FieldNotNull(FieldAgentSlug)) +} + +// AgentSlugEqualFold applies the EqualFold predicate on the "agent_slug" field. +func AgentSlugEqualFold(v string) predicate.BrokerDispatch { + return predicate.BrokerDispatch(sql.FieldEqualFold(FieldAgentSlug, v)) +} + +// AgentSlugContainsFold applies the ContainsFold predicate on the "agent_slug" field. +func AgentSlugContainsFold(v string) predicate.BrokerDispatch { + return predicate.BrokerDispatch(sql.FieldContainsFold(FieldAgentSlug, v)) +} + +// ProjectIDEQ applies the EQ predicate on the "project_id" field. +func ProjectIDEQ(v uuid.UUID) predicate.BrokerDispatch { + return predicate.BrokerDispatch(sql.FieldEQ(FieldProjectID, v)) +} + +// ProjectIDNEQ applies the NEQ predicate on the "project_id" field. +func ProjectIDNEQ(v uuid.UUID) predicate.BrokerDispatch { + return predicate.BrokerDispatch(sql.FieldNEQ(FieldProjectID, v)) +} + +// ProjectIDIn applies the In predicate on the "project_id" field. +func ProjectIDIn(vs ...uuid.UUID) predicate.BrokerDispatch { + return predicate.BrokerDispatch(sql.FieldIn(FieldProjectID, vs...)) +} + +// ProjectIDNotIn applies the NotIn predicate on the "project_id" field. +func ProjectIDNotIn(vs ...uuid.UUID) predicate.BrokerDispatch { + return predicate.BrokerDispatch(sql.FieldNotIn(FieldProjectID, vs...)) +} + +// ProjectIDGT applies the GT predicate on the "project_id" field. +func ProjectIDGT(v uuid.UUID) predicate.BrokerDispatch { + return predicate.BrokerDispatch(sql.FieldGT(FieldProjectID, v)) +} + +// ProjectIDGTE applies the GTE predicate on the "project_id" field. +func ProjectIDGTE(v uuid.UUID) predicate.BrokerDispatch { + return predicate.BrokerDispatch(sql.FieldGTE(FieldProjectID, v)) +} + +// ProjectIDLT applies the LT predicate on the "project_id" field. +func ProjectIDLT(v uuid.UUID) predicate.BrokerDispatch { + return predicate.BrokerDispatch(sql.FieldLT(FieldProjectID, v)) +} + +// ProjectIDLTE applies the LTE predicate on the "project_id" field. +func ProjectIDLTE(v uuid.UUID) predicate.BrokerDispatch { + return predicate.BrokerDispatch(sql.FieldLTE(FieldProjectID, v)) +} + +// ProjectIDIsNil applies the IsNil predicate on the "project_id" field. +func ProjectIDIsNil() predicate.BrokerDispatch { + return predicate.BrokerDispatch(sql.FieldIsNull(FieldProjectID)) +} + +// ProjectIDNotNil applies the NotNil predicate on the "project_id" field. +func ProjectIDNotNil() predicate.BrokerDispatch { + return predicate.BrokerDispatch(sql.FieldNotNull(FieldProjectID)) +} + +// OpEQ applies the EQ predicate on the "op" field. +func OpEQ(v string) predicate.BrokerDispatch { + return predicate.BrokerDispatch(sql.FieldEQ(FieldOp, v)) +} + +// OpNEQ applies the NEQ predicate on the "op" field. +func OpNEQ(v string) predicate.BrokerDispatch { + return predicate.BrokerDispatch(sql.FieldNEQ(FieldOp, v)) +} + +// OpIn applies the In predicate on the "op" field. +func OpIn(vs ...string) predicate.BrokerDispatch { + return predicate.BrokerDispatch(sql.FieldIn(FieldOp, vs...)) +} + +// OpNotIn applies the NotIn predicate on the "op" field. +func OpNotIn(vs ...string) predicate.BrokerDispatch { + return predicate.BrokerDispatch(sql.FieldNotIn(FieldOp, vs...)) +} + +// OpGT applies the GT predicate on the "op" field. +func OpGT(v string) predicate.BrokerDispatch { + return predicate.BrokerDispatch(sql.FieldGT(FieldOp, v)) +} + +// OpGTE applies the GTE predicate on the "op" field. +func OpGTE(v string) predicate.BrokerDispatch { + return predicate.BrokerDispatch(sql.FieldGTE(FieldOp, v)) +} + +// OpLT applies the LT predicate on the "op" field. +func OpLT(v string) predicate.BrokerDispatch { + return predicate.BrokerDispatch(sql.FieldLT(FieldOp, v)) +} + +// OpLTE applies the LTE predicate on the "op" field. +func OpLTE(v string) predicate.BrokerDispatch { + return predicate.BrokerDispatch(sql.FieldLTE(FieldOp, v)) +} + +// OpContains applies the Contains predicate on the "op" field. +func OpContains(v string) predicate.BrokerDispatch { + return predicate.BrokerDispatch(sql.FieldContains(FieldOp, v)) +} + +// OpHasPrefix applies the HasPrefix predicate on the "op" field. +func OpHasPrefix(v string) predicate.BrokerDispatch { + return predicate.BrokerDispatch(sql.FieldHasPrefix(FieldOp, v)) +} + +// OpHasSuffix applies the HasSuffix predicate on the "op" field. +func OpHasSuffix(v string) predicate.BrokerDispatch { + return predicate.BrokerDispatch(sql.FieldHasSuffix(FieldOp, v)) +} + +// OpEqualFold applies the EqualFold predicate on the "op" field. +func OpEqualFold(v string) predicate.BrokerDispatch { + return predicate.BrokerDispatch(sql.FieldEqualFold(FieldOp, v)) +} + +// OpContainsFold applies the ContainsFold predicate on the "op" field. +func OpContainsFold(v string) predicate.BrokerDispatch { + return predicate.BrokerDispatch(sql.FieldContainsFold(FieldOp, v)) +} + +// ArgsEQ applies the EQ predicate on the "args" field. +func ArgsEQ(v string) predicate.BrokerDispatch { + return predicate.BrokerDispatch(sql.FieldEQ(FieldArgs, v)) +} + +// ArgsNEQ applies the NEQ predicate on the "args" field. +func ArgsNEQ(v string) predicate.BrokerDispatch { + return predicate.BrokerDispatch(sql.FieldNEQ(FieldArgs, v)) +} + +// ArgsIn applies the In predicate on the "args" field. +func ArgsIn(vs ...string) predicate.BrokerDispatch { + return predicate.BrokerDispatch(sql.FieldIn(FieldArgs, vs...)) +} + +// ArgsNotIn applies the NotIn predicate on the "args" field. +func ArgsNotIn(vs ...string) predicate.BrokerDispatch { + return predicate.BrokerDispatch(sql.FieldNotIn(FieldArgs, vs...)) +} + +// ArgsGT applies the GT predicate on the "args" field. +func ArgsGT(v string) predicate.BrokerDispatch { + return predicate.BrokerDispatch(sql.FieldGT(FieldArgs, v)) +} + +// ArgsGTE applies the GTE predicate on the "args" field. +func ArgsGTE(v string) predicate.BrokerDispatch { + return predicate.BrokerDispatch(sql.FieldGTE(FieldArgs, v)) +} + +// ArgsLT applies the LT predicate on the "args" field. +func ArgsLT(v string) predicate.BrokerDispatch { + return predicate.BrokerDispatch(sql.FieldLT(FieldArgs, v)) +} + +// ArgsLTE applies the LTE predicate on the "args" field. +func ArgsLTE(v string) predicate.BrokerDispatch { + return predicate.BrokerDispatch(sql.FieldLTE(FieldArgs, v)) +} + +// ArgsContains applies the Contains predicate on the "args" field. +func ArgsContains(v string) predicate.BrokerDispatch { + return predicate.BrokerDispatch(sql.FieldContains(FieldArgs, v)) +} + +// ArgsHasPrefix applies the HasPrefix predicate on the "args" field. +func ArgsHasPrefix(v string) predicate.BrokerDispatch { + return predicate.BrokerDispatch(sql.FieldHasPrefix(FieldArgs, v)) +} + +// ArgsHasSuffix applies the HasSuffix predicate on the "args" field. +func ArgsHasSuffix(v string) predicate.BrokerDispatch { + return predicate.BrokerDispatch(sql.FieldHasSuffix(FieldArgs, v)) +} + +// ArgsIsNil applies the IsNil predicate on the "args" field. +func ArgsIsNil() predicate.BrokerDispatch { + return predicate.BrokerDispatch(sql.FieldIsNull(FieldArgs)) +} + +// ArgsNotNil applies the NotNil predicate on the "args" field. +func ArgsNotNil() predicate.BrokerDispatch { + return predicate.BrokerDispatch(sql.FieldNotNull(FieldArgs)) +} + +// ArgsEqualFold applies the EqualFold predicate on the "args" field. +func ArgsEqualFold(v string) predicate.BrokerDispatch { + return predicate.BrokerDispatch(sql.FieldEqualFold(FieldArgs, v)) +} + +// ArgsContainsFold applies the ContainsFold predicate on the "args" field. +func ArgsContainsFold(v string) predicate.BrokerDispatch { + return predicate.BrokerDispatch(sql.FieldContainsFold(FieldArgs, v)) +} + +// StateEQ applies the EQ predicate on the "state" field. +func StateEQ(v string) predicate.BrokerDispatch { + return predicate.BrokerDispatch(sql.FieldEQ(FieldState, v)) +} + +// StateNEQ applies the NEQ predicate on the "state" field. +func StateNEQ(v string) predicate.BrokerDispatch { + return predicate.BrokerDispatch(sql.FieldNEQ(FieldState, v)) +} + +// StateIn applies the In predicate on the "state" field. +func StateIn(vs ...string) predicate.BrokerDispatch { + return predicate.BrokerDispatch(sql.FieldIn(FieldState, vs...)) +} + +// StateNotIn applies the NotIn predicate on the "state" field. +func StateNotIn(vs ...string) predicate.BrokerDispatch { + return predicate.BrokerDispatch(sql.FieldNotIn(FieldState, vs...)) +} + +// StateGT applies the GT predicate on the "state" field. +func StateGT(v string) predicate.BrokerDispatch { + return predicate.BrokerDispatch(sql.FieldGT(FieldState, v)) +} + +// StateGTE applies the GTE predicate on the "state" field. +func StateGTE(v string) predicate.BrokerDispatch { + return predicate.BrokerDispatch(sql.FieldGTE(FieldState, v)) +} + +// StateLT applies the LT predicate on the "state" field. +func StateLT(v string) predicate.BrokerDispatch { + return predicate.BrokerDispatch(sql.FieldLT(FieldState, v)) +} + +// StateLTE applies the LTE predicate on the "state" field. +func StateLTE(v string) predicate.BrokerDispatch { + return predicate.BrokerDispatch(sql.FieldLTE(FieldState, v)) +} + +// StateContains applies the Contains predicate on the "state" field. +func StateContains(v string) predicate.BrokerDispatch { + return predicate.BrokerDispatch(sql.FieldContains(FieldState, v)) +} + +// StateHasPrefix applies the HasPrefix predicate on the "state" field. +func StateHasPrefix(v string) predicate.BrokerDispatch { + return predicate.BrokerDispatch(sql.FieldHasPrefix(FieldState, v)) +} + +// StateHasSuffix applies the HasSuffix predicate on the "state" field. +func StateHasSuffix(v string) predicate.BrokerDispatch { + return predicate.BrokerDispatch(sql.FieldHasSuffix(FieldState, v)) +} + +// StateEqualFold applies the EqualFold predicate on the "state" field. +func StateEqualFold(v string) predicate.BrokerDispatch { + return predicate.BrokerDispatch(sql.FieldEqualFold(FieldState, v)) +} + +// StateContainsFold applies the ContainsFold predicate on the "state" field. +func StateContainsFold(v string) predicate.BrokerDispatch { + return predicate.BrokerDispatch(sql.FieldContainsFold(FieldState, v)) +} + +// ResultEQ applies the EQ predicate on the "result" field. +func ResultEQ(v string) predicate.BrokerDispatch { + return predicate.BrokerDispatch(sql.FieldEQ(FieldResult, v)) +} + +// ResultNEQ applies the NEQ predicate on the "result" field. +func ResultNEQ(v string) predicate.BrokerDispatch { + return predicate.BrokerDispatch(sql.FieldNEQ(FieldResult, v)) +} + +// ResultIn applies the In predicate on the "result" field. +func ResultIn(vs ...string) predicate.BrokerDispatch { + return predicate.BrokerDispatch(sql.FieldIn(FieldResult, vs...)) +} + +// ResultNotIn applies the NotIn predicate on the "result" field. +func ResultNotIn(vs ...string) predicate.BrokerDispatch { + return predicate.BrokerDispatch(sql.FieldNotIn(FieldResult, vs...)) +} + +// ResultGT applies the GT predicate on the "result" field. +func ResultGT(v string) predicate.BrokerDispatch { + return predicate.BrokerDispatch(sql.FieldGT(FieldResult, v)) +} + +// ResultGTE applies the GTE predicate on the "result" field. +func ResultGTE(v string) predicate.BrokerDispatch { + return predicate.BrokerDispatch(sql.FieldGTE(FieldResult, v)) +} + +// ResultLT applies the LT predicate on the "result" field. +func ResultLT(v string) predicate.BrokerDispatch { + return predicate.BrokerDispatch(sql.FieldLT(FieldResult, v)) +} + +// ResultLTE applies the LTE predicate on the "result" field. +func ResultLTE(v string) predicate.BrokerDispatch { + return predicate.BrokerDispatch(sql.FieldLTE(FieldResult, v)) +} + +// ResultContains applies the Contains predicate on the "result" field. +func ResultContains(v string) predicate.BrokerDispatch { + return predicate.BrokerDispatch(sql.FieldContains(FieldResult, v)) +} + +// ResultHasPrefix applies the HasPrefix predicate on the "result" field. +func ResultHasPrefix(v string) predicate.BrokerDispatch { + return predicate.BrokerDispatch(sql.FieldHasPrefix(FieldResult, v)) +} + +// ResultHasSuffix applies the HasSuffix predicate on the "result" field. +func ResultHasSuffix(v string) predicate.BrokerDispatch { + return predicate.BrokerDispatch(sql.FieldHasSuffix(FieldResult, v)) +} + +// ResultIsNil applies the IsNil predicate on the "result" field. +func ResultIsNil() predicate.BrokerDispatch { + return predicate.BrokerDispatch(sql.FieldIsNull(FieldResult)) +} + +// ResultNotNil applies the NotNil predicate on the "result" field. +func ResultNotNil() predicate.BrokerDispatch { + return predicate.BrokerDispatch(sql.FieldNotNull(FieldResult)) +} + +// ResultEqualFold applies the EqualFold predicate on the "result" field. +func ResultEqualFold(v string) predicate.BrokerDispatch { + return predicate.BrokerDispatch(sql.FieldEqualFold(FieldResult, v)) +} + +// ResultContainsFold applies the ContainsFold predicate on the "result" field. +func ResultContainsFold(v string) predicate.BrokerDispatch { + return predicate.BrokerDispatch(sql.FieldContainsFold(FieldResult, v)) +} + +// ClaimedByEQ applies the EQ predicate on the "claimed_by" field. +func ClaimedByEQ(v string) predicate.BrokerDispatch { + return predicate.BrokerDispatch(sql.FieldEQ(FieldClaimedBy, v)) +} + +// ClaimedByNEQ applies the NEQ predicate on the "claimed_by" field. +func ClaimedByNEQ(v string) predicate.BrokerDispatch { + return predicate.BrokerDispatch(sql.FieldNEQ(FieldClaimedBy, v)) +} + +// ClaimedByIn applies the In predicate on the "claimed_by" field. +func ClaimedByIn(vs ...string) predicate.BrokerDispatch { + return predicate.BrokerDispatch(sql.FieldIn(FieldClaimedBy, vs...)) +} + +// ClaimedByNotIn applies the NotIn predicate on the "claimed_by" field. +func ClaimedByNotIn(vs ...string) predicate.BrokerDispatch { + return predicate.BrokerDispatch(sql.FieldNotIn(FieldClaimedBy, vs...)) +} + +// ClaimedByGT applies the GT predicate on the "claimed_by" field. +func ClaimedByGT(v string) predicate.BrokerDispatch { + return predicate.BrokerDispatch(sql.FieldGT(FieldClaimedBy, v)) +} + +// ClaimedByGTE applies the GTE predicate on the "claimed_by" field. +func ClaimedByGTE(v string) predicate.BrokerDispatch { + return predicate.BrokerDispatch(sql.FieldGTE(FieldClaimedBy, v)) +} + +// ClaimedByLT applies the LT predicate on the "claimed_by" field. +func ClaimedByLT(v string) predicate.BrokerDispatch { + return predicate.BrokerDispatch(sql.FieldLT(FieldClaimedBy, v)) +} + +// ClaimedByLTE applies the LTE predicate on the "claimed_by" field. +func ClaimedByLTE(v string) predicate.BrokerDispatch { + return predicate.BrokerDispatch(sql.FieldLTE(FieldClaimedBy, v)) +} + +// ClaimedByContains applies the Contains predicate on the "claimed_by" field. +func ClaimedByContains(v string) predicate.BrokerDispatch { + return predicate.BrokerDispatch(sql.FieldContains(FieldClaimedBy, v)) +} + +// ClaimedByHasPrefix applies the HasPrefix predicate on the "claimed_by" field. +func ClaimedByHasPrefix(v string) predicate.BrokerDispatch { + return predicate.BrokerDispatch(sql.FieldHasPrefix(FieldClaimedBy, v)) +} + +// ClaimedByHasSuffix applies the HasSuffix predicate on the "claimed_by" field. +func ClaimedByHasSuffix(v string) predicate.BrokerDispatch { + return predicate.BrokerDispatch(sql.FieldHasSuffix(FieldClaimedBy, v)) +} + +// ClaimedByIsNil applies the IsNil predicate on the "claimed_by" field. +func ClaimedByIsNil() predicate.BrokerDispatch { + return predicate.BrokerDispatch(sql.FieldIsNull(FieldClaimedBy)) +} + +// ClaimedByNotNil applies the NotNil predicate on the "claimed_by" field. +func ClaimedByNotNil() predicate.BrokerDispatch { + return predicate.BrokerDispatch(sql.FieldNotNull(FieldClaimedBy)) +} + +// ClaimedByEqualFold applies the EqualFold predicate on the "claimed_by" field. +func ClaimedByEqualFold(v string) predicate.BrokerDispatch { + return predicate.BrokerDispatch(sql.FieldEqualFold(FieldClaimedBy, v)) +} + +// ClaimedByContainsFold applies the ContainsFold predicate on the "claimed_by" field. +func ClaimedByContainsFold(v string) predicate.BrokerDispatch { + return predicate.BrokerDispatch(sql.FieldContainsFold(FieldClaimedBy, v)) +} + +// AttemptsEQ applies the EQ predicate on the "attempts" field. +func AttemptsEQ(v int) predicate.BrokerDispatch { + return predicate.BrokerDispatch(sql.FieldEQ(FieldAttempts, v)) +} + +// AttemptsNEQ applies the NEQ predicate on the "attempts" field. +func AttemptsNEQ(v int) predicate.BrokerDispatch { + return predicate.BrokerDispatch(sql.FieldNEQ(FieldAttempts, v)) +} + +// AttemptsIn applies the In predicate on the "attempts" field. +func AttemptsIn(vs ...int) predicate.BrokerDispatch { + return predicate.BrokerDispatch(sql.FieldIn(FieldAttempts, vs...)) +} + +// AttemptsNotIn applies the NotIn predicate on the "attempts" field. +func AttemptsNotIn(vs ...int) predicate.BrokerDispatch { + return predicate.BrokerDispatch(sql.FieldNotIn(FieldAttempts, vs...)) +} + +// AttemptsGT applies the GT predicate on the "attempts" field. +func AttemptsGT(v int) predicate.BrokerDispatch { + return predicate.BrokerDispatch(sql.FieldGT(FieldAttempts, v)) +} + +// AttemptsGTE applies the GTE predicate on the "attempts" field. +func AttemptsGTE(v int) predicate.BrokerDispatch { + return predicate.BrokerDispatch(sql.FieldGTE(FieldAttempts, v)) +} + +// AttemptsLT applies the LT predicate on the "attempts" field. +func AttemptsLT(v int) predicate.BrokerDispatch { + return predicate.BrokerDispatch(sql.FieldLT(FieldAttempts, v)) +} + +// AttemptsLTE applies the LTE predicate on the "attempts" field. +func AttemptsLTE(v int) predicate.BrokerDispatch { + return predicate.BrokerDispatch(sql.FieldLTE(FieldAttempts, v)) +} + +// ErrorEQ applies the EQ predicate on the "error" field. +func ErrorEQ(v string) predicate.BrokerDispatch { + return predicate.BrokerDispatch(sql.FieldEQ(FieldError, v)) +} + +// ErrorNEQ applies the NEQ predicate on the "error" field. +func ErrorNEQ(v string) predicate.BrokerDispatch { + return predicate.BrokerDispatch(sql.FieldNEQ(FieldError, v)) +} + +// ErrorIn applies the In predicate on the "error" field. +func ErrorIn(vs ...string) predicate.BrokerDispatch { + return predicate.BrokerDispatch(sql.FieldIn(FieldError, vs...)) +} + +// ErrorNotIn applies the NotIn predicate on the "error" field. +func ErrorNotIn(vs ...string) predicate.BrokerDispatch { + return predicate.BrokerDispatch(sql.FieldNotIn(FieldError, vs...)) +} + +// ErrorGT applies the GT predicate on the "error" field. +func ErrorGT(v string) predicate.BrokerDispatch { + return predicate.BrokerDispatch(sql.FieldGT(FieldError, v)) +} + +// ErrorGTE applies the GTE predicate on the "error" field. +func ErrorGTE(v string) predicate.BrokerDispatch { + return predicate.BrokerDispatch(sql.FieldGTE(FieldError, v)) +} + +// ErrorLT applies the LT predicate on the "error" field. +func ErrorLT(v string) predicate.BrokerDispatch { + return predicate.BrokerDispatch(sql.FieldLT(FieldError, v)) +} + +// ErrorLTE applies the LTE predicate on the "error" field. +func ErrorLTE(v string) predicate.BrokerDispatch { + return predicate.BrokerDispatch(sql.FieldLTE(FieldError, v)) +} + +// ErrorContains applies the Contains predicate on the "error" field. +func ErrorContains(v string) predicate.BrokerDispatch { + return predicate.BrokerDispatch(sql.FieldContains(FieldError, v)) +} + +// ErrorHasPrefix applies the HasPrefix predicate on the "error" field. +func ErrorHasPrefix(v string) predicate.BrokerDispatch { + return predicate.BrokerDispatch(sql.FieldHasPrefix(FieldError, v)) +} + +// ErrorHasSuffix applies the HasSuffix predicate on the "error" field. +func ErrorHasSuffix(v string) predicate.BrokerDispatch { + return predicate.BrokerDispatch(sql.FieldHasSuffix(FieldError, v)) +} + +// ErrorIsNil applies the IsNil predicate on the "error" field. +func ErrorIsNil() predicate.BrokerDispatch { + return predicate.BrokerDispatch(sql.FieldIsNull(FieldError)) +} + +// ErrorNotNil applies the NotNil predicate on the "error" field. +func ErrorNotNil() predicate.BrokerDispatch { + return predicate.BrokerDispatch(sql.FieldNotNull(FieldError)) +} + +// ErrorEqualFold applies the EqualFold predicate on the "error" field. +func ErrorEqualFold(v string) predicate.BrokerDispatch { + return predicate.BrokerDispatch(sql.FieldEqualFold(FieldError, v)) +} + +// ErrorContainsFold applies the ContainsFold predicate on the "error" field. +func ErrorContainsFold(v string) predicate.BrokerDispatch { + return predicate.BrokerDispatch(sql.FieldContainsFold(FieldError, v)) +} + +// CreatedAtEQ applies the EQ predicate on the "created_at" field. +func CreatedAtEQ(v time.Time) predicate.BrokerDispatch { + return predicate.BrokerDispatch(sql.FieldEQ(FieldCreatedAt, v)) +} + +// CreatedAtNEQ applies the NEQ predicate on the "created_at" field. +func CreatedAtNEQ(v time.Time) predicate.BrokerDispatch { + return predicate.BrokerDispatch(sql.FieldNEQ(FieldCreatedAt, v)) +} + +// CreatedAtIn applies the In predicate on the "created_at" field. +func CreatedAtIn(vs ...time.Time) predicate.BrokerDispatch { + return predicate.BrokerDispatch(sql.FieldIn(FieldCreatedAt, vs...)) +} + +// CreatedAtNotIn applies the NotIn predicate on the "created_at" field. +func CreatedAtNotIn(vs ...time.Time) predicate.BrokerDispatch { + return predicate.BrokerDispatch(sql.FieldNotIn(FieldCreatedAt, vs...)) +} + +// CreatedAtGT applies the GT predicate on the "created_at" field. +func CreatedAtGT(v time.Time) predicate.BrokerDispatch { + return predicate.BrokerDispatch(sql.FieldGT(FieldCreatedAt, v)) +} + +// CreatedAtGTE applies the GTE predicate on the "created_at" field. +func CreatedAtGTE(v time.Time) predicate.BrokerDispatch { + return predicate.BrokerDispatch(sql.FieldGTE(FieldCreatedAt, v)) +} + +// CreatedAtLT applies the LT predicate on the "created_at" field. +func CreatedAtLT(v time.Time) predicate.BrokerDispatch { + return predicate.BrokerDispatch(sql.FieldLT(FieldCreatedAt, v)) +} + +// CreatedAtLTE applies the LTE predicate on the "created_at" field. +func CreatedAtLTE(v time.Time) predicate.BrokerDispatch { + return predicate.BrokerDispatch(sql.FieldLTE(FieldCreatedAt, v)) +} + +// UpdatedAtEQ applies the EQ predicate on the "updated_at" field. +func UpdatedAtEQ(v time.Time) predicate.BrokerDispatch { + return predicate.BrokerDispatch(sql.FieldEQ(FieldUpdatedAt, v)) +} + +// UpdatedAtNEQ applies the NEQ predicate on the "updated_at" field. +func UpdatedAtNEQ(v time.Time) predicate.BrokerDispatch { + return predicate.BrokerDispatch(sql.FieldNEQ(FieldUpdatedAt, v)) +} + +// UpdatedAtIn applies the In predicate on the "updated_at" field. +func UpdatedAtIn(vs ...time.Time) predicate.BrokerDispatch { + return predicate.BrokerDispatch(sql.FieldIn(FieldUpdatedAt, vs...)) +} + +// UpdatedAtNotIn applies the NotIn predicate on the "updated_at" field. +func UpdatedAtNotIn(vs ...time.Time) predicate.BrokerDispatch { + return predicate.BrokerDispatch(sql.FieldNotIn(FieldUpdatedAt, vs...)) +} + +// UpdatedAtGT applies the GT predicate on the "updated_at" field. +func UpdatedAtGT(v time.Time) predicate.BrokerDispatch { + return predicate.BrokerDispatch(sql.FieldGT(FieldUpdatedAt, v)) +} + +// UpdatedAtGTE applies the GTE predicate on the "updated_at" field. +func UpdatedAtGTE(v time.Time) predicate.BrokerDispatch { + return predicate.BrokerDispatch(sql.FieldGTE(FieldUpdatedAt, v)) +} + +// UpdatedAtLT applies the LT predicate on the "updated_at" field. +func UpdatedAtLT(v time.Time) predicate.BrokerDispatch { + return predicate.BrokerDispatch(sql.FieldLT(FieldUpdatedAt, v)) +} + +// UpdatedAtLTE applies the LTE predicate on the "updated_at" field. +func UpdatedAtLTE(v time.Time) predicate.BrokerDispatch { + return predicate.BrokerDispatch(sql.FieldLTE(FieldUpdatedAt, v)) +} + +// DeadlineAtEQ applies the EQ predicate on the "deadline_at" field. +func DeadlineAtEQ(v time.Time) predicate.BrokerDispatch { + return predicate.BrokerDispatch(sql.FieldEQ(FieldDeadlineAt, v)) +} + +// DeadlineAtNEQ applies the NEQ predicate on the "deadline_at" field. +func DeadlineAtNEQ(v time.Time) predicate.BrokerDispatch { + return predicate.BrokerDispatch(sql.FieldNEQ(FieldDeadlineAt, v)) +} + +// DeadlineAtIn applies the In predicate on the "deadline_at" field. +func DeadlineAtIn(vs ...time.Time) predicate.BrokerDispatch { + return predicate.BrokerDispatch(sql.FieldIn(FieldDeadlineAt, vs...)) +} + +// DeadlineAtNotIn applies the NotIn predicate on the "deadline_at" field. +func DeadlineAtNotIn(vs ...time.Time) predicate.BrokerDispatch { + return predicate.BrokerDispatch(sql.FieldNotIn(FieldDeadlineAt, vs...)) +} + +// DeadlineAtGT applies the GT predicate on the "deadline_at" field. +func DeadlineAtGT(v time.Time) predicate.BrokerDispatch { + return predicate.BrokerDispatch(sql.FieldGT(FieldDeadlineAt, v)) +} + +// DeadlineAtGTE applies the GTE predicate on the "deadline_at" field. +func DeadlineAtGTE(v time.Time) predicate.BrokerDispatch { + return predicate.BrokerDispatch(sql.FieldGTE(FieldDeadlineAt, v)) +} + +// DeadlineAtLT applies the LT predicate on the "deadline_at" field. +func DeadlineAtLT(v time.Time) predicate.BrokerDispatch { + return predicate.BrokerDispatch(sql.FieldLT(FieldDeadlineAt, v)) +} + +// DeadlineAtLTE applies the LTE predicate on the "deadline_at" field. +func DeadlineAtLTE(v time.Time) predicate.BrokerDispatch { + return predicate.BrokerDispatch(sql.FieldLTE(FieldDeadlineAt, v)) +} + +// DeadlineAtIsNil applies the IsNil predicate on the "deadline_at" field. +func DeadlineAtIsNil() predicate.BrokerDispatch { + return predicate.BrokerDispatch(sql.FieldIsNull(FieldDeadlineAt)) +} + +// DeadlineAtNotNil applies the NotNil predicate on the "deadline_at" field. +func DeadlineAtNotNil() predicate.BrokerDispatch { + return predicate.BrokerDispatch(sql.FieldNotNull(FieldDeadlineAt)) +} + +// And groups predicates with the AND operator between them. +func And(predicates ...predicate.BrokerDispatch) predicate.BrokerDispatch { + return predicate.BrokerDispatch(sql.AndPredicates(predicates...)) +} + +// Or groups predicates with the OR operator between them. +func Or(predicates ...predicate.BrokerDispatch) predicate.BrokerDispatch { + return predicate.BrokerDispatch(sql.OrPredicates(predicates...)) +} + +// Not applies the not operator on the given predicate. +func Not(p predicate.BrokerDispatch) predicate.BrokerDispatch { + return predicate.BrokerDispatch(sql.NotPredicates(p)) +} diff --git a/pkg/ent/brokerdispatch_create.go b/pkg/ent/brokerdispatch_create.go new file mode 100644 index 000000000..599839130 --- /dev/null +++ b/pkg/ent/brokerdispatch_create.go @@ -0,0 +1,1437 @@ +// Code generated by ent, DO NOT EDIT. + +package ent + +import ( + "context" + "errors" + "fmt" + "time" + + "entgo.io/ent/dialect" + "entgo.io/ent/dialect/sql" + "entgo.io/ent/dialect/sql/sqlgraph" + "entgo.io/ent/schema/field" + "github.com/GoogleCloudPlatform/scion/pkg/ent/brokerdispatch" + "github.com/google/uuid" +) + +// BrokerDispatchCreate is the builder for creating a BrokerDispatch entity. +type BrokerDispatchCreate struct { + config + mutation *BrokerDispatchMutation + hooks []Hook + conflict []sql.ConflictOption +} + +// SetBrokerID sets the "broker_id" field. +func (_c *BrokerDispatchCreate) SetBrokerID(v uuid.UUID) *BrokerDispatchCreate { + _c.mutation.SetBrokerID(v) + return _c +} + +// SetAgentID sets the "agent_id" field. +func (_c *BrokerDispatchCreate) SetAgentID(v uuid.UUID) *BrokerDispatchCreate { + _c.mutation.SetAgentID(v) + return _c +} + +// SetNillableAgentID sets the "agent_id" field if the given value is not nil. +func (_c *BrokerDispatchCreate) SetNillableAgentID(v *uuid.UUID) *BrokerDispatchCreate { + if v != nil { + _c.SetAgentID(*v) + } + return _c +} + +// SetAgentSlug sets the "agent_slug" field. +func (_c *BrokerDispatchCreate) SetAgentSlug(v string) *BrokerDispatchCreate { + _c.mutation.SetAgentSlug(v) + return _c +} + +// SetNillableAgentSlug sets the "agent_slug" field if the given value is not nil. +func (_c *BrokerDispatchCreate) SetNillableAgentSlug(v *string) *BrokerDispatchCreate { + if v != nil { + _c.SetAgentSlug(*v) + } + return _c +} + +// SetProjectID sets the "project_id" field. +func (_c *BrokerDispatchCreate) SetProjectID(v uuid.UUID) *BrokerDispatchCreate { + _c.mutation.SetProjectID(v) + return _c +} + +// SetNillableProjectID sets the "project_id" field if the given value is not nil. +func (_c *BrokerDispatchCreate) SetNillableProjectID(v *uuid.UUID) *BrokerDispatchCreate { + if v != nil { + _c.SetProjectID(*v) + } + return _c +} + +// SetOp sets the "op" field. +func (_c *BrokerDispatchCreate) SetOp(v string) *BrokerDispatchCreate { + _c.mutation.SetOpField(v) + return _c +} + +// SetArgs sets the "args" field. +func (_c *BrokerDispatchCreate) SetArgs(v string) *BrokerDispatchCreate { + _c.mutation.SetArgs(v) + return _c +} + +// SetNillableArgs sets the "args" field if the given value is not nil. +func (_c *BrokerDispatchCreate) SetNillableArgs(v *string) *BrokerDispatchCreate { + if v != nil { + _c.SetArgs(*v) + } + return _c +} + +// SetState sets the "state" field. +func (_c *BrokerDispatchCreate) SetState(v string) *BrokerDispatchCreate { + _c.mutation.SetState(v) + return _c +} + +// SetNillableState sets the "state" field if the given value is not nil. +func (_c *BrokerDispatchCreate) SetNillableState(v *string) *BrokerDispatchCreate { + if v != nil { + _c.SetState(*v) + } + return _c +} + +// SetResult sets the "result" field. +func (_c *BrokerDispatchCreate) SetResult(v string) *BrokerDispatchCreate { + _c.mutation.SetResult(v) + return _c +} + +// SetNillableResult sets the "result" field if the given value is not nil. +func (_c *BrokerDispatchCreate) SetNillableResult(v *string) *BrokerDispatchCreate { + if v != nil { + _c.SetResult(*v) + } + return _c +} + +// SetClaimedBy sets the "claimed_by" field. +func (_c *BrokerDispatchCreate) SetClaimedBy(v string) *BrokerDispatchCreate { + _c.mutation.SetClaimedBy(v) + return _c +} + +// SetNillableClaimedBy sets the "claimed_by" field if the given value is not nil. +func (_c *BrokerDispatchCreate) SetNillableClaimedBy(v *string) *BrokerDispatchCreate { + if v != nil { + _c.SetClaimedBy(*v) + } + return _c +} + +// SetAttempts sets the "attempts" field. +func (_c *BrokerDispatchCreate) SetAttempts(v int) *BrokerDispatchCreate { + _c.mutation.SetAttempts(v) + return _c +} + +// SetNillableAttempts sets the "attempts" field if the given value is not nil. +func (_c *BrokerDispatchCreate) SetNillableAttempts(v *int) *BrokerDispatchCreate { + if v != nil { + _c.SetAttempts(*v) + } + return _c +} + +// SetError sets the "error" field. +func (_c *BrokerDispatchCreate) SetError(v string) *BrokerDispatchCreate { + _c.mutation.SetError(v) + return _c +} + +// SetNillableError sets the "error" field if the given value is not nil. +func (_c *BrokerDispatchCreate) SetNillableError(v *string) *BrokerDispatchCreate { + if v != nil { + _c.SetError(*v) + } + return _c +} + +// SetCreatedAt sets the "created_at" field. +func (_c *BrokerDispatchCreate) SetCreatedAt(v time.Time) *BrokerDispatchCreate { + _c.mutation.SetCreatedAt(v) + return _c +} + +// SetNillableCreatedAt sets the "created_at" field if the given value is not nil. +func (_c *BrokerDispatchCreate) SetNillableCreatedAt(v *time.Time) *BrokerDispatchCreate { + if v != nil { + _c.SetCreatedAt(*v) + } + return _c +} + +// SetUpdatedAt sets the "updated_at" field. +func (_c *BrokerDispatchCreate) SetUpdatedAt(v time.Time) *BrokerDispatchCreate { + _c.mutation.SetUpdatedAt(v) + return _c +} + +// SetNillableUpdatedAt sets the "updated_at" field if the given value is not nil. +func (_c *BrokerDispatchCreate) SetNillableUpdatedAt(v *time.Time) *BrokerDispatchCreate { + if v != nil { + _c.SetUpdatedAt(*v) + } + return _c +} + +// SetDeadlineAt sets the "deadline_at" field. +func (_c *BrokerDispatchCreate) SetDeadlineAt(v time.Time) *BrokerDispatchCreate { + _c.mutation.SetDeadlineAt(v) + return _c +} + +// SetNillableDeadlineAt sets the "deadline_at" field if the given value is not nil. +func (_c *BrokerDispatchCreate) SetNillableDeadlineAt(v *time.Time) *BrokerDispatchCreate { + if v != nil { + _c.SetDeadlineAt(*v) + } + return _c +} + +// SetID sets the "id" field. +func (_c *BrokerDispatchCreate) SetID(v uuid.UUID) *BrokerDispatchCreate { + _c.mutation.SetID(v) + return _c +} + +// SetNillableID sets the "id" field if the given value is not nil. +func (_c *BrokerDispatchCreate) SetNillableID(v *uuid.UUID) *BrokerDispatchCreate { + if v != nil { + _c.SetID(*v) + } + return _c +} + +// Mutation returns the BrokerDispatchMutation object of the builder. +func (_c *BrokerDispatchCreate) Mutation() *BrokerDispatchMutation { + return _c.mutation +} + +// Save creates the BrokerDispatch in the database. +func (_c *BrokerDispatchCreate) Save(ctx context.Context) (*BrokerDispatch, error) { + _c.defaults() + return withHooks(ctx, _c.sqlSave, _c.mutation, _c.hooks) +} + +// SaveX calls Save and panics if Save returns an error. +func (_c *BrokerDispatchCreate) SaveX(ctx context.Context) *BrokerDispatch { + v, err := _c.Save(ctx) + if err != nil { + panic(err) + } + return v +} + +// Exec executes the query. +func (_c *BrokerDispatchCreate) Exec(ctx context.Context) error { + _, err := _c.Save(ctx) + return err +} + +// ExecX is like Exec, but panics if an error occurs. +func (_c *BrokerDispatchCreate) ExecX(ctx context.Context) { + if err := _c.Exec(ctx); err != nil { + panic(err) + } +} + +// defaults sets the default values of the builder before save. +func (_c *BrokerDispatchCreate) defaults() { + if _, ok := _c.mutation.State(); !ok { + v := brokerdispatch.DefaultState + _c.mutation.SetState(v) + } + if _, ok := _c.mutation.Attempts(); !ok { + v := brokerdispatch.DefaultAttempts + _c.mutation.SetAttempts(v) + } + if _, ok := _c.mutation.CreatedAt(); !ok { + v := brokerdispatch.DefaultCreatedAt() + _c.mutation.SetCreatedAt(v) + } + if _, ok := _c.mutation.UpdatedAt(); !ok { + v := brokerdispatch.DefaultUpdatedAt() + _c.mutation.SetUpdatedAt(v) + } + if _, ok := _c.mutation.ID(); !ok { + v := brokerdispatch.DefaultID() + _c.mutation.SetID(v) + } +} + +// check runs all checks and user-defined validators on the builder. +func (_c *BrokerDispatchCreate) check() error { + if _, ok := _c.mutation.BrokerID(); !ok { + return &ValidationError{Name: "broker_id", err: errors.New(`ent: missing required field "BrokerDispatch.broker_id"`)} + } + if _, ok := _c.mutation.GetOp(); !ok { + return &ValidationError{Name: "op", err: errors.New(`ent: missing required field "BrokerDispatch.op"`)} + } + if v, ok := _c.mutation.GetOp(); ok { + if err := brokerdispatch.OpValidator(v); err != nil { + return &ValidationError{Name: "op", err: fmt.Errorf(`ent: validator failed for field "BrokerDispatch.op": %w`, err)} + } + } + if _, ok := _c.mutation.State(); !ok { + return &ValidationError{Name: "state", err: errors.New(`ent: missing required field "BrokerDispatch.state"`)} + } + if _, ok := _c.mutation.Attempts(); !ok { + return &ValidationError{Name: "attempts", err: errors.New(`ent: missing required field "BrokerDispatch.attempts"`)} + } + if _, ok := _c.mutation.CreatedAt(); !ok { + return &ValidationError{Name: "created_at", err: errors.New(`ent: missing required field "BrokerDispatch.created_at"`)} + } + if _, ok := _c.mutation.UpdatedAt(); !ok { + return &ValidationError{Name: "updated_at", err: errors.New(`ent: missing required field "BrokerDispatch.updated_at"`)} + } + return nil +} + +func (_c *BrokerDispatchCreate) sqlSave(ctx context.Context) (*BrokerDispatch, error) { + if err := _c.check(); err != nil { + return nil, err + } + _node, _spec := _c.createSpec() + if err := sqlgraph.CreateNode(ctx, _c.driver, _spec); err != nil { + if sqlgraph.IsConstraintError(err) { + err = &ConstraintError{msg: err.Error(), wrap: err} + } + return nil, err + } + if _spec.ID.Value != nil { + if id, ok := _spec.ID.Value.(*uuid.UUID); ok { + _node.ID = *id + } else if err := _node.ID.Scan(_spec.ID.Value); err != nil { + return nil, err + } + } + _c.mutation.id = &_node.ID + _c.mutation.done = true + return _node, nil +} + +func (_c *BrokerDispatchCreate) createSpec() (*BrokerDispatch, *sqlgraph.CreateSpec) { + var ( + _node = &BrokerDispatch{config: _c.config} + _spec = sqlgraph.NewCreateSpec(brokerdispatch.Table, sqlgraph.NewFieldSpec(brokerdispatch.FieldID, field.TypeUUID)) + ) + _spec.OnConflict = _c.conflict + if id, ok := _c.mutation.ID(); ok { + _node.ID = id + _spec.ID.Value = &id + } + if value, ok := _c.mutation.BrokerID(); ok { + _spec.SetField(brokerdispatch.FieldBrokerID, field.TypeUUID, value) + _node.BrokerID = value + } + if value, ok := _c.mutation.AgentID(); ok { + _spec.SetField(brokerdispatch.FieldAgentID, field.TypeUUID, value) + _node.AgentID = &value + } + if value, ok := _c.mutation.AgentSlug(); ok { + _spec.SetField(brokerdispatch.FieldAgentSlug, field.TypeString, value) + _node.AgentSlug = value + } + if value, ok := _c.mutation.ProjectID(); ok { + _spec.SetField(brokerdispatch.FieldProjectID, field.TypeUUID, value) + _node.ProjectID = &value + } + if value, ok := _c.mutation.GetOp(); ok { + _spec.SetField(brokerdispatch.FieldOp, field.TypeString, value) + _node.Op = value + } + if value, ok := _c.mutation.Args(); ok { + _spec.SetField(brokerdispatch.FieldArgs, field.TypeString, value) + _node.Args = value + } + if value, ok := _c.mutation.State(); ok { + _spec.SetField(brokerdispatch.FieldState, field.TypeString, value) + _node.State = value + } + if value, ok := _c.mutation.Result(); ok { + _spec.SetField(brokerdispatch.FieldResult, field.TypeString, value) + _node.Result = value + } + if value, ok := _c.mutation.ClaimedBy(); ok { + _spec.SetField(brokerdispatch.FieldClaimedBy, field.TypeString, value) + _node.ClaimedBy = value + } + if value, ok := _c.mutation.Attempts(); ok { + _spec.SetField(brokerdispatch.FieldAttempts, field.TypeInt, value) + _node.Attempts = value + } + if value, ok := _c.mutation.Error(); ok { + _spec.SetField(brokerdispatch.FieldError, field.TypeString, value) + _node.Error = value + } + if value, ok := _c.mutation.CreatedAt(); ok { + _spec.SetField(brokerdispatch.FieldCreatedAt, field.TypeTime, value) + _node.CreatedAt = value + } + if value, ok := _c.mutation.UpdatedAt(); ok { + _spec.SetField(brokerdispatch.FieldUpdatedAt, field.TypeTime, value) + _node.UpdatedAt = value + } + if value, ok := _c.mutation.DeadlineAt(); ok { + _spec.SetField(brokerdispatch.FieldDeadlineAt, field.TypeTime, value) + _node.DeadlineAt = &value + } + return _node, _spec +} + +// OnConflict allows configuring the `ON CONFLICT` / `ON DUPLICATE KEY` clause +// of the `INSERT` statement. For example: +// +// client.BrokerDispatch.Create(). +// SetBrokerID(v). +// OnConflict( +// // Update the row with the new values +// // the was proposed for insertion. +// sql.ResolveWithNewValues(), +// ). +// // Override some of the fields with custom +// // update values. +// Update(func(u *ent.BrokerDispatchUpsert) { +// SetBrokerID(v+v). +// }). +// Exec(ctx) +func (_c *BrokerDispatchCreate) OnConflict(opts ...sql.ConflictOption) *BrokerDispatchUpsertOne { + _c.conflict = opts + return &BrokerDispatchUpsertOne{ + create: _c, + } +} + +// OnConflictColumns calls `OnConflict` and configures the columns +// as conflict target. Using this option is equivalent to using: +// +// client.BrokerDispatch.Create(). +// OnConflict(sql.ConflictColumns(columns...)). +// Exec(ctx) +func (_c *BrokerDispatchCreate) OnConflictColumns(columns ...string) *BrokerDispatchUpsertOne { + _c.conflict = append(_c.conflict, sql.ConflictColumns(columns...)) + return &BrokerDispatchUpsertOne{ + create: _c, + } +} + +type ( + // BrokerDispatchUpsertOne is the builder for "upsert"-ing + // one BrokerDispatch node. + BrokerDispatchUpsertOne struct { + create *BrokerDispatchCreate + } + + // BrokerDispatchUpsert is the "OnConflict" setter. + BrokerDispatchUpsert struct { + *sql.UpdateSet + } +) + +// SetBrokerID sets the "broker_id" field. +func (u *BrokerDispatchUpsert) SetBrokerID(v uuid.UUID) *BrokerDispatchUpsert { + u.Set(brokerdispatch.FieldBrokerID, v) + return u +} + +// UpdateBrokerID sets the "broker_id" field to the value that was provided on create. +func (u *BrokerDispatchUpsert) UpdateBrokerID() *BrokerDispatchUpsert { + u.SetExcluded(brokerdispatch.FieldBrokerID) + return u +} + +// SetAgentID sets the "agent_id" field. +func (u *BrokerDispatchUpsert) SetAgentID(v uuid.UUID) *BrokerDispatchUpsert { + u.Set(brokerdispatch.FieldAgentID, v) + return u +} + +// UpdateAgentID sets the "agent_id" field to the value that was provided on create. +func (u *BrokerDispatchUpsert) UpdateAgentID() *BrokerDispatchUpsert { + u.SetExcluded(brokerdispatch.FieldAgentID) + return u +} + +// ClearAgentID clears the value of the "agent_id" field. +func (u *BrokerDispatchUpsert) ClearAgentID() *BrokerDispatchUpsert { + u.SetNull(brokerdispatch.FieldAgentID) + return u +} + +// SetAgentSlug sets the "agent_slug" field. +func (u *BrokerDispatchUpsert) SetAgentSlug(v string) *BrokerDispatchUpsert { + u.Set(brokerdispatch.FieldAgentSlug, v) + return u +} + +// UpdateAgentSlug sets the "agent_slug" field to the value that was provided on create. +func (u *BrokerDispatchUpsert) UpdateAgentSlug() *BrokerDispatchUpsert { + u.SetExcluded(brokerdispatch.FieldAgentSlug) + return u +} + +// ClearAgentSlug clears the value of the "agent_slug" field. +func (u *BrokerDispatchUpsert) ClearAgentSlug() *BrokerDispatchUpsert { + u.SetNull(brokerdispatch.FieldAgentSlug) + return u +} + +// SetProjectID sets the "project_id" field. +func (u *BrokerDispatchUpsert) SetProjectID(v uuid.UUID) *BrokerDispatchUpsert { + u.Set(brokerdispatch.FieldProjectID, v) + return u +} + +// UpdateProjectID sets the "project_id" field to the value that was provided on create. +func (u *BrokerDispatchUpsert) UpdateProjectID() *BrokerDispatchUpsert { + u.SetExcluded(brokerdispatch.FieldProjectID) + return u +} + +// ClearProjectID clears the value of the "project_id" field. +func (u *BrokerDispatchUpsert) ClearProjectID() *BrokerDispatchUpsert { + u.SetNull(brokerdispatch.FieldProjectID) + return u +} + +// SetOp sets the "op" field. +func (u *BrokerDispatchUpsert) SetOp(v string) *BrokerDispatchUpsert { + u.Set(brokerdispatch.FieldOp, v) + return u +} + +// UpdateOp sets the "op" field to the value that was provided on create. +func (u *BrokerDispatchUpsert) UpdateOp() *BrokerDispatchUpsert { + u.SetExcluded(brokerdispatch.FieldOp) + return u +} + +// SetArgs sets the "args" field. +func (u *BrokerDispatchUpsert) SetArgs(v string) *BrokerDispatchUpsert { + u.Set(brokerdispatch.FieldArgs, v) + return u +} + +// UpdateArgs sets the "args" field to the value that was provided on create. +func (u *BrokerDispatchUpsert) UpdateArgs() *BrokerDispatchUpsert { + u.SetExcluded(brokerdispatch.FieldArgs) + return u +} + +// ClearArgs clears the value of the "args" field. +func (u *BrokerDispatchUpsert) ClearArgs() *BrokerDispatchUpsert { + u.SetNull(brokerdispatch.FieldArgs) + return u +} + +// SetState sets the "state" field. +func (u *BrokerDispatchUpsert) SetState(v string) *BrokerDispatchUpsert { + u.Set(brokerdispatch.FieldState, v) + return u +} + +// UpdateState sets the "state" field to the value that was provided on create. +func (u *BrokerDispatchUpsert) UpdateState() *BrokerDispatchUpsert { + u.SetExcluded(brokerdispatch.FieldState) + return u +} + +// SetResult sets the "result" field. +func (u *BrokerDispatchUpsert) SetResult(v string) *BrokerDispatchUpsert { + u.Set(brokerdispatch.FieldResult, v) + return u +} + +// UpdateResult sets the "result" field to the value that was provided on create. +func (u *BrokerDispatchUpsert) UpdateResult() *BrokerDispatchUpsert { + u.SetExcluded(brokerdispatch.FieldResult) + return u +} + +// ClearResult clears the value of the "result" field. +func (u *BrokerDispatchUpsert) ClearResult() *BrokerDispatchUpsert { + u.SetNull(brokerdispatch.FieldResult) + return u +} + +// SetClaimedBy sets the "claimed_by" field. +func (u *BrokerDispatchUpsert) SetClaimedBy(v string) *BrokerDispatchUpsert { + u.Set(brokerdispatch.FieldClaimedBy, v) + return u +} + +// UpdateClaimedBy sets the "claimed_by" field to the value that was provided on create. +func (u *BrokerDispatchUpsert) UpdateClaimedBy() *BrokerDispatchUpsert { + u.SetExcluded(brokerdispatch.FieldClaimedBy) + return u +} + +// ClearClaimedBy clears the value of the "claimed_by" field. +func (u *BrokerDispatchUpsert) ClearClaimedBy() *BrokerDispatchUpsert { + u.SetNull(brokerdispatch.FieldClaimedBy) + return u +} + +// SetAttempts sets the "attempts" field. +func (u *BrokerDispatchUpsert) SetAttempts(v int) *BrokerDispatchUpsert { + u.Set(brokerdispatch.FieldAttempts, v) + return u +} + +// UpdateAttempts sets the "attempts" field to the value that was provided on create. +func (u *BrokerDispatchUpsert) UpdateAttempts() *BrokerDispatchUpsert { + u.SetExcluded(brokerdispatch.FieldAttempts) + return u +} + +// AddAttempts adds v to the "attempts" field. +func (u *BrokerDispatchUpsert) AddAttempts(v int) *BrokerDispatchUpsert { + u.Add(brokerdispatch.FieldAttempts, v) + return u +} + +// SetError sets the "error" field. +func (u *BrokerDispatchUpsert) SetError(v string) *BrokerDispatchUpsert { + u.Set(brokerdispatch.FieldError, v) + return u +} + +// UpdateError sets the "error" field to the value that was provided on create. +func (u *BrokerDispatchUpsert) UpdateError() *BrokerDispatchUpsert { + u.SetExcluded(brokerdispatch.FieldError) + return u +} + +// ClearError clears the value of the "error" field. +func (u *BrokerDispatchUpsert) ClearError() *BrokerDispatchUpsert { + u.SetNull(brokerdispatch.FieldError) + return u +} + +// SetUpdatedAt sets the "updated_at" field. +func (u *BrokerDispatchUpsert) SetUpdatedAt(v time.Time) *BrokerDispatchUpsert { + u.Set(brokerdispatch.FieldUpdatedAt, v) + return u +} + +// UpdateUpdatedAt sets the "updated_at" field to the value that was provided on create. +func (u *BrokerDispatchUpsert) UpdateUpdatedAt() *BrokerDispatchUpsert { + u.SetExcluded(brokerdispatch.FieldUpdatedAt) + return u +} + +// SetDeadlineAt sets the "deadline_at" field. +func (u *BrokerDispatchUpsert) SetDeadlineAt(v time.Time) *BrokerDispatchUpsert { + u.Set(brokerdispatch.FieldDeadlineAt, v) + return u +} + +// UpdateDeadlineAt sets the "deadline_at" field to the value that was provided on create. +func (u *BrokerDispatchUpsert) UpdateDeadlineAt() *BrokerDispatchUpsert { + u.SetExcluded(brokerdispatch.FieldDeadlineAt) + return u +} + +// ClearDeadlineAt clears the value of the "deadline_at" field. +func (u *BrokerDispatchUpsert) ClearDeadlineAt() *BrokerDispatchUpsert { + u.SetNull(brokerdispatch.FieldDeadlineAt) + return u +} + +// UpdateNewValues updates the mutable fields using the new values that were set on create except the ID field. +// Using this option is equivalent to using: +// +// client.BrokerDispatch.Create(). +// OnConflict( +// sql.ResolveWithNewValues(), +// sql.ResolveWith(func(u *sql.UpdateSet) { +// u.SetIgnore(brokerdispatch.FieldID) +// }), +// ). +// Exec(ctx) +func (u *BrokerDispatchUpsertOne) UpdateNewValues() *BrokerDispatchUpsertOne { + u.create.conflict = append(u.create.conflict, sql.ResolveWithNewValues()) + u.create.conflict = append(u.create.conflict, sql.ResolveWith(func(s *sql.UpdateSet) { + if _, exists := u.create.mutation.ID(); exists { + s.SetIgnore(brokerdispatch.FieldID) + } + if _, exists := u.create.mutation.CreatedAt(); exists { + s.SetIgnore(brokerdispatch.FieldCreatedAt) + } + })) + return u +} + +// Ignore sets each column to itself in case of conflict. +// Using this option is equivalent to using: +// +// client.BrokerDispatch.Create(). +// OnConflict(sql.ResolveWithIgnore()). +// Exec(ctx) +func (u *BrokerDispatchUpsertOne) Ignore() *BrokerDispatchUpsertOne { + u.create.conflict = append(u.create.conflict, sql.ResolveWithIgnore()) + return u +} + +// DoNothing configures the conflict_action to `DO NOTHING`. +// Supported only by SQLite and PostgreSQL. +func (u *BrokerDispatchUpsertOne) DoNothing() *BrokerDispatchUpsertOne { + u.create.conflict = append(u.create.conflict, sql.DoNothing()) + return u +} + +// Update allows overriding fields `UPDATE` values. See the BrokerDispatchCreate.OnConflict +// documentation for more info. +func (u *BrokerDispatchUpsertOne) Update(set func(*BrokerDispatchUpsert)) *BrokerDispatchUpsertOne { + u.create.conflict = append(u.create.conflict, sql.ResolveWith(func(update *sql.UpdateSet) { + set(&BrokerDispatchUpsert{UpdateSet: update}) + })) + return u +} + +// SetBrokerID sets the "broker_id" field. +func (u *BrokerDispatchUpsertOne) SetBrokerID(v uuid.UUID) *BrokerDispatchUpsertOne { + return u.Update(func(s *BrokerDispatchUpsert) { + s.SetBrokerID(v) + }) +} + +// UpdateBrokerID sets the "broker_id" field to the value that was provided on create. +func (u *BrokerDispatchUpsertOne) UpdateBrokerID() *BrokerDispatchUpsertOne { + return u.Update(func(s *BrokerDispatchUpsert) { + s.UpdateBrokerID() + }) +} + +// SetAgentID sets the "agent_id" field. +func (u *BrokerDispatchUpsertOne) SetAgentID(v uuid.UUID) *BrokerDispatchUpsertOne { + return u.Update(func(s *BrokerDispatchUpsert) { + s.SetAgentID(v) + }) +} + +// UpdateAgentID sets the "agent_id" field to the value that was provided on create. +func (u *BrokerDispatchUpsertOne) UpdateAgentID() *BrokerDispatchUpsertOne { + return u.Update(func(s *BrokerDispatchUpsert) { + s.UpdateAgentID() + }) +} + +// ClearAgentID clears the value of the "agent_id" field. +func (u *BrokerDispatchUpsertOne) ClearAgentID() *BrokerDispatchUpsertOne { + return u.Update(func(s *BrokerDispatchUpsert) { + s.ClearAgentID() + }) +} + +// SetAgentSlug sets the "agent_slug" field. +func (u *BrokerDispatchUpsertOne) SetAgentSlug(v string) *BrokerDispatchUpsertOne { + return u.Update(func(s *BrokerDispatchUpsert) { + s.SetAgentSlug(v) + }) +} + +// UpdateAgentSlug sets the "agent_slug" field to the value that was provided on create. +func (u *BrokerDispatchUpsertOne) UpdateAgentSlug() *BrokerDispatchUpsertOne { + return u.Update(func(s *BrokerDispatchUpsert) { + s.UpdateAgentSlug() + }) +} + +// ClearAgentSlug clears the value of the "agent_slug" field. +func (u *BrokerDispatchUpsertOne) ClearAgentSlug() *BrokerDispatchUpsertOne { + return u.Update(func(s *BrokerDispatchUpsert) { + s.ClearAgentSlug() + }) +} + +// SetProjectID sets the "project_id" field. +func (u *BrokerDispatchUpsertOne) SetProjectID(v uuid.UUID) *BrokerDispatchUpsertOne { + return u.Update(func(s *BrokerDispatchUpsert) { + s.SetProjectID(v) + }) +} + +// UpdateProjectID sets the "project_id" field to the value that was provided on create. +func (u *BrokerDispatchUpsertOne) UpdateProjectID() *BrokerDispatchUpsertOne { + return u.Update(func(s *BrokerDispatchUpsert) { + s.UpdateProjectID() + }) +} + +// ClearProjectID clears the value of the "project_id" field. +func (u *BrokerDispatchUpsertOne) ClearProjectID() *BrokerDispatchUpsertOne { + return u.Update(func(s *BrokerDispatchUpsert) { + s.ClearProjectID() + }) +} + +// SetOp sets the "op" field. +func (u *BrokerDispatchUpsertOne) SetOp(v string) *BrokerDispatchUpsertOne { + return u.Update(func(s *BrokerDispatchUpsert) { + s.SetOp(v) + }) +} + +// UpdateOp sets the "op" field to the value that was provided on create. +func (u *BrokerDispatchUpsertOne) UpdateOp() *BrokerDispatchUpsertOne { + return u.Update(func(s *BrokerDispatchUpsert) { + s.UpdateOp() + }) +} + +// SetArgs sets the "args" field. +func (u *BrokerDispatchUpsertOne) SetArgs(v string) *BrokerDispatchUpsertOne { + return u.Update(func(s *BrokerDispatchUpsert) { + s.SetArgs(v) + }) +} + +// UpdateArgs sets the "args" field to the value that was provided on create. +func (u *BrokerDispatchUpsertOne) UpdateArgs() *BrokerDispatchUpsertOne { + return u.Update(func(s *BrokerDispatchUpsert) { + s.UpdateArgs() + }) +} + +// ClearArgs clears the value of the "args" field. +func (u *BrokerDispatchUpsertOne) ClearArgs() *BrokerDispatchUpsertOne { + return u.Update(func(s *BrokerDispatchUpsert) { + s.ClearArgs() + }) +} + +// SetState sets the "state" field. +func (u *BrokerDispatchUpsertOne) SetState(v string) *BrokerDispatchUpsertOne { + return u.Update(func(s *BrokerDispatchUpsert) { + s.SetState(v) + }) +} + +// UpdateState sets the "state" field to the value that was provided on create. +func (u *BrokerDispatchUpsertOne) UpdateState() *BrokerDispatchUpsertOne { + return u.Update(func(s *BrokerDispatchUpsert) { + s.UpdateState() + }) +} + +// SetResult sets the "result" field. +func (u *BrokerDispatchUpsertOne) SetResult(v string) *BrokerDispatchUpsertOne { + return u.Update(func(s *BrokerDispatchUpsert) { + s.SetResult(v) + }) +} + +// UpdateResult sets the "result" field to the value that was provided on create. +func (u *BrokerDispatchUpsertOne) UpdateResult() *BrokerDispatchUpsertOne { + return u.Update(func(s *BrokerDispatchUpsert) { + s.UpdateResult() + }) +} + +// ClearResult clears the value of the "result" field. +func (u *BrokerDispatchUpsertOne) ClearResult() *BrokerDispatchUpsertOne { + return u.Update(func(s *BrokerDispatchUpsert) { + s.ClearResult() + }) +} + +// SetClaimedBy sets the "claimed_by" field. +func (u *BrokerDispatchUpsertOne) SetClaimedBy(v string) *BrokerDispatchUpsertOne { + return u.Update(func(s *BrokerDispatchUpsert) { + s.SetClaimedBy(v) + }) +} + +// UpdateClaimedBy sets the "claimed_by" field to the value that was provided on create. +func (u *BrokerDispatchUpsertOne) UpdateClaimedBy() *BrokerDispatchUpsertOne { + return u.Update(func(s *BrokerDispatchUpsert) { + s.UpdateClaimedBy() + }) +} + +// ClearClaimedBy clears the value of the "claimed_by" field. +func (u *BrokerDispatchUpsertOne) ClearClaimedBy() *BrokerDispatchUpsertOne { + return u.Update(func(s *BrokerDispatchUpsert) { + s.ClearClaimedBy() + }) +} + +// SetAttempts sets the "attempts" field. +func (u *BrokerDispatchUpsertOne) SetAttempts(v int) *BrokerDispatchUpsertOne { + return u.Update(func(s *BrokerDispatchUpsert) { + s.SetAttempts(v) + }) +} + +// AddAttempts adds v to the "attempts" field. +func (u *BrokerDispatchUpsertOne) AddAttempts(v int) *BrokerDispatchUpsertOne { + return u.Update(func(s *BrokerDispatchUpsert) { + s.AddAttempts(v) + }) +} + +// UpdateAttempts sets the "attempts" field to the value that was provided on create. +func (u *BrokerDispatchUpsertOne) UpdateAttempts() *BrokerDispatchUpsertOne { + return u.Update(func(s *BrokerDispatchUpsert) { + s.UpdateAttempts() + }) +} + +// SetError sets the "error" field. +func (u *BrokerDispatchUpsertOne) SetError(v string) *BrokerDispatchUpsertOne { + return u.Update(func(s *BrokerDispatchUpsert) { + s.SetError(v) + }) +} + +// UpdateError sets the "error" field to the value that was provided on create. +func (u *BrokerDispatchUpsertOne) UpdateError() *BrokerDispatchUpsertOne { + return u.Update(func(s *BrokerDispatchUpsert) { + s.UpdateError() + }) +} + +// ClearError clears the value of the "error" field. +func (u *BrokerDispatchUpsertOne) ClearError() *BrokerDispatchUpsertOne { + return u.Update(func(s *BrokerDispatchUpsert) { + s.ClearError() + }) +} + +// SetUpdatedAt sets the "updated_at" field. +func (u *BrokerDispatchUpsertOne) SetUpdatedAt(v time.Time) *BrokerDispatchUpsertOne { + return u.Update(func(s *BrokerDispatchUpsert) { + s.SetUpdatedAt(v) + }) +} + +// UpdateUpdatedAt sets the "updated_at" field to the value that was provided on create. +func (u *BrokerDispatchUpsertOne) UpdateUpdatedAt() *BrokerDispatchUpsertOne { + return u.Update(func(s *BrokerDispatchUpsert) { + s.UpdateUpdatedAt() + }) +} + +// SetDeadlineAt sets the "deadline_at" field. +func (u *BrokerDispatchUpsertOne) SetDeadlineAt(v time.Time) *BrokerDispatchUpsertOne { + return u.Update(func(s *BrokerDispatchUpsert) { + s.SetDeadlineAt(v) + }) +} + +// UpdateDeadlineAt sets the "deadline_at" field to the value that was provided on create. +func (u *BrokerDispatchUpsertOne) UpdateDeadlineAt() *BrokerDispatchUpsertOne { + return u.Update(func(s *BrokerDispatchUpsert) { + s.UpdateDeadlineAt() + }) +} + +// ClearDeadlineAt clears the value of the "deadline_at" field. +func (u *BrokerDispatchUpsertOne) ClearDeadlineAt() *BrokerDispatchUpsertOne { + return u.Update(func(s *BrokerDispatchUpsert) { + s.ClearDeadlineAt() + }) +} + +// Exec executes the query. +func (u *BrokerDispatchUpsertOne) Exec(ctx context.Context) error { + if len(u.create.conflict) == 0 { + return errors.New("ent: missing options for BrokerDispatchCreate.OnConflict") + } + return u.create.Exec(ctx) +} + +// ExecX is like Exec, but panics if an error occurs. +func (u *BrokerDispatchUpsertOne) ExecX(ctx context.Context) { + if err := u.create.Exec(ctx); err != nil { + panic(err) + } +} + +// Exec executes the UPSERT query and returns the inserted/updated ID. +func (u *BrokerDispatchUpsertOne) ID(ctx context.Context) (id uuid.UUID, err error) { + if u.create.driver.Dialect() == dialect.MySQL { + // In case of "ON CONFLICT", there is no way to get back non-numeric ID + // fields from the database since MySQL does not support the RETURNING clause. + return id, errors.New("ent: BrokerDispatchUpsertOne.ID is not supported by MySQL driver. Use BrokerDispatchUpsertOne.Exec instead") + } + node, err := u.create.Save(ctx) + if err != nil { + return id, err + } + return node.ID, nil +} + +// IDX is like ID, but panics if an error occurs. +func (u *BrokerDispatchUpsertOne) IDX(ctx context.Context) uuid.UUID { + id, err := u.ID(ctx) + if err != nil { + panic(err) + } + return id +} + +// BrokerDispatchCreateBulk is the builder for creating many BrokerDispatch entities in bulk. +type BrokerDispatchCreateBulk struct { + config + err error + builders []*BrokerDispatchCreate + conflict []sql.ConflictOption +} + +// Save creates the BrokerDispatch entities in the database. +func (_c *BrokerDispatchCreateBulk) Save(ctx context.Context) ([]*BrokerDispatch, error) { + if _c.err != nil { + return nil, _c.err + } + specs := make([]*sqlgraph.CreateSpec, len(_c.builders)) + nodes := make([]*BrokerDispatch, len(_c.builders)) + mutators := make([]Mutator, len(_c.builders)) + for i := range _c.builders { + func(i int, root context.Context) { + builder := _c.builders[i] + builder.defaults() + var mut Mutator = MutateFunc(func(ctx context.Context, m Mutation) (Value, error) { + mutation, ok := m.(*BrokerDispatchMutation) + if !ok { + return nil, fmt.Errorf("unexpected mutation type %T", m) + } + if err := builder.check(); err != nil { + return nil, err + } + builder.mutation = mutation + var err error + nodes[i], specs[i] = builder.createSpec() + if i < len(mutators)-1 { + _, err = mutators[i+1].Mutate(root, _c.builders[i+1].mutation) + } else { + spec := &sqlgraph.BatchCreateSpec{Nodes: specs} + spec.OnConflict = _c.conflict + // Invoke the actual operation on the latest mutation in the chain. + if err = sqlgraph.BatchCreate(ctx, _c.driver, spec); err != nil { + if sqlgraph.IsConstraintError(err) { + err = &ConstraintError{msg: err.Error(), wrap: err} + } + } + } + if err != nil { + return nil, err + } + mutation.id = &nodes[i].ID + mutation.done = true + return nodes[i], nil + }) + for i := len(builder.hooks) - 1; i >= 0; i-- { + mut = builder.hooks[i](mut) + } + mutators[i] = mut + }(i, ctx) + } + if len(mutators) > 0 { + if _, err := mutators[0].Mutate(ctx, _c.builders[0].mutation); err != nil { + return nil, err + } + } + return nodes, nil +} + +// SaveX is like Save, but panics if an error occurs. +func (_c *BrokerDispatchCreateBulk) SaveX(ctx context.Context) []*BrokerDispatch { + v, err := _c.Save(ctx) + if err != nil { + panic(err) + } + return v +} + +// Exec executes the query. +func (_c *BrokerDispatchCreateBulk) Exec(ctx context.Context) error { + _, err := _c.Save(ctx) + return err +} + +// ExecX is like Exec, but panics if an error occurs. +func (_c *BrokerDispatchCreateBulk) ExecX(ctx context.Context) { + if err := _c.Exec(ctx); err != nil { + panic(err) + } +} + +// OnConflict allows configuring the `ON CONFLICT` / `ON DUPLICATE KEY` clause +// of the `INSERT` statement. For example: +// +// client.BrokerDispatch.CreateBulk(builders...). +// OnConflict( +// // Update the row with the new values +// // the was proposed for insertion. +// sql.ResolveWithNewValues(), +// ). +// // Override some of the fields with custom +// // update values. +// Update(func(u *ent.BrokerDispatchUpsert) { +// SetBrokerID(v+v). +// }). +// Exec(ctx) +func (_c *BrokerDispatchCreateBulk) OnConflict(opts ...sql.ConflictOption) *BrokerDispatchUpsertBulk { + _c.conflict = opts + return &BrokerDispatchUpsertBulk{ + create: _c, + } +} + +// OnConflictColumns calls `OnConflict` and configures the columns +// as conflict target. Using this option is equivalent to using: +// +// client.BrokerDispatch.Create(). +// OnConflict(sql.ConflictColumns(columns...)). +// Exec(ctx) +func (_c *BrokerDispatchCreateBulk) OnConflictColumns(columns ...string) *BrokerDispatchUpsertBulk { + _c.conflict = append(_c.conflict, sql.ConflictColumns(columns...)) + return &BrokerDispatchUpsertBulk{ + create: _c, + } +} + +// BrokerDispatchUpsertBulk is the builder for "upsert"-ing +// a bulk of BrokerDispatch nodes. +type BrokerDispatchUpsertBulk struct { + create *BrokerDispatchCreateBulk +} + +// UpdateNewValues updates the mutable fields using the new values that +// were set on create. Using this option is equivalent to using: +// +// client.BrokerDispatch.Create(). +// OnConflict( +// sql.ResolveWithNewValues(), +// sql.ResolveWith(func(u *sql.UpdateSet) { +// u.SetIgnore(brokerdispatch.FieldID) +// }), +// ). +// Exec(ctx) +func (u *BrokerDispatchUpsertBulk) UpdateNewValues() *BrokerDispatchUpsertBulk { + u.create.conflict = append(u.create.conflict, sql.ResolveWithNewValues()) + u.create.conflict = append(u.create.conflict, sql.ResolveWith(func(s *sql.UpdateSet) { + for _, b := range u.create.builders { + if _, exists := b.mutation.ID(); exists { + s.SetIgnore(brokerdispatch.FieldID) + } + if _, exists := b.mutation.CreatedAt(); exists { + s.SetIgnore(brokerdispatch.FieldCreatedAt) + } + } + })) + return u +} + +// Ignore sets each column to itself in case of conflict. +// Using this option is equivalent to using: +// +// client.BrokerDispatch.Create(). +// OnConflict(sql.ResolveWithIgnore()). +// Exec(ctx) +func (u *BrokerDispatchUpsertBulk) Ignore() *BrokerDispatchUpsertBulk { + u.create.conflict = append(u.create.conflict, sql.ResolveWithIgnore()) + return u +} + +// DoNothing configures the conflict_action to `DO NOTHING`. +// Supported only by SQLite and PostgreSQL. +func (u *BrokerDispatchUpsertBulk) DoNothing() *BrokerDispatchUpsertBulk { + u.create.conflict = append(u.create.conflict, sql.DoNothing()) + return u +} + +// Update allows overriding fields `UPDATE` values. See the BrokerDispatchCreateBulk.OnConflict +// documentation for more info. +func (u *BrokerDispatchUpsertBulk) Update(set func(*BrokerDispatchUpsert)) *BrokerDispatchUpsertBulk { + u.create.conflict = append(u.create.conflict, sql.ResolveWith(func(update *sql.UpdateSet) { + set(&BrokerDispatchUpsert{UpdateSet: update}) + })) + return u +} + +// SetBrokerID sets the "broker_id" field. +func (u *BrokerDispatchUpsertBulk) SetBrokerID(v uuid.UUID) *BrokerDispatchUpsertBulk { + return u.Update(func(s *BrokerDispatchUpsert) { + s.SetBrokerID(v) + }) +} + +// UpdateBrokerID sets the "broker_id" field to the value that was provided on create. +func (u *BrokerDispatchUpsertBulk) UpdateBrokerID() *BrokerDispatchUpsertBulk { + return u.Update(func(s *BrokerDispatchUpsert) { + s.UpdateBrokerID() + }) +} + +// SetAgentID sets the "agent_id" field. +func (u *BrokerDispatchUpsertBulk) SetAgentID(v uuid.UUID) *BrokerDispatchUpsertBulk { + return u.Update(func(s *BrokerDispatchUpsert) { + s.SetAgentID(v) + }) +} + +// UpdateAgentID sets the "agent_id" field to the value that was provided on create. +func (u *BrokerDispatchUpsertBulk) UpdateAgentID() *BrokerDispatchUpsertBulk { + return u.Update(func(s *BrokerDispatchUpsert) { + s.UpdateAgentID() + }) +} + +// ClearAgentID clears the value of the "agent_id" field. +func (u *BrokerDispatchUpsertBulk) ClearAgentID() *BrokerDispatchUpsertBulk { + return u.Update(func(s *BrokerDispatchUpsert) { + s.ClearAgentID() + }) +} + +// SetAgentSlug sets the "agent_slug" field. +func (u *BrokerDispatchUpsertBulk) SetAgentSlug(v string) *BrokerDispatchUpsertBulk { + return u.Update(func(s *BrokerDispatchUpsert) { + s.SetAgentSlug(v) + }) +} + +// UpdateAgentSlug sets the "agent_slug" field to the value that was provided on create. +func (u *BrokerDispatchUpsertBulk) UpdateAgentSlug() *BrokerDispatchUpsertBulk { + return u.Update(func(s *BrokerDispatchUpsert) { + s.UpdateAgentSlug() + }) +} + +// ClearAgentSlug clears the value of the "agent_slug" field. +func (u *BrokerDispatchUpsertBulk) ClearAgentSlug() *BrokerDispatchUpsertBulk { + return u.Update(func(s *BrokerDispatchUpsert) { + s.ClearAgentSlug() + }) +} + +// SetProjectID sets the "project_id" field. +func (u *BrokerDispatchUpsertBulk) SetProjectID(v uuid.UUID) *BrokerDispatchUpsertBulk { + return u.Update(func(s *BrokerDispatchUpsert) { + s.SetProjectID(v) + }) +} + +// UpdateProjectID sets the "project_id" field to the value that was provided on create. +func (u *BrokerDispatchUpsertBulk) UpdateProjectID() *BrokerDispatchUpsertBulk { + return u.Update(func(s *BrokerDispatchUpsert) { + s.UpdateProjectID() + }) +} + +// ClearProjectID clears the value of the "project_id" field. +func (u *BrokerDispatchUpsertBulk) ClearProjectID() *BrokerDispatchUpsertBulk { + return u.Update(func(s *BrokerDispatchUpsert) { + s.ClearProjectID() + }) +} + +// SetOp sets the "op" field. +func (u *BrokerDispatchUpsertBulk) SetOp(v string) *BrokerDispatchUpsertBulk { + return u.Update(func(s *BrokerDispatchUpsert) { + s.SetOp(v) + }) +} + +// UpdateOp sets the "op" field to the value that was provided on create. +func (u *BrokerDispatchUpsertBulk) UpdateOp() *BrokerDispatchUpsertBulk { + return u.Update(func(s *BrokerDispatchUpsert) { + s.UpdateOp() + }) +} + +// SetArgs sets the "args" field. +func (u *BrokerDispatchUpsertBulk) SetArgs(v string) *BrokerDispatchUpsertBulk { + return u.Update(func(s *BrokerDispatchUpsert) { + s.SetArgs(v) + }) +} + +// UpdateArgs sets the "args" field to the value that was provided on create. +func (u *BrokerDispatchUpsertBulk) UpdateArgs() *BrokerDispatchUpsertBulk { + return u.Update(func(s *BrokerDispatchUpsert) { + s.UpdateArgs() + }) +} + +// ClearArgs clears the value of the "args" field. +func (u *BrokerDispatchUpsertBulk) ClearArgs() *BrokerDispatchUpsertBulk { + return u.Update(func(s *BrokerDispatchUpsert) { + s.ClearArgs() + }) +} + +// SetState sets the "state" field. +func (u *BrokerDispatchUpsertBulk) SetState(v string) *BrokerDispatchUpsertBulk { + return u.Update(func(s *BrokerDispatchUpsert) { + s.SetState(v) + }) +} + +// UpdateState sets the "state" field to the value that was provided on create. +func (u *BrokerDispatchUpsertBulk) UpdateState() *BrokerDispatchUpsertBulk { + return u.Update(func(s *BrokerDispatchUpsert) { + s.UpdateState() + }) +} + +// SetResult sets the "result" field. +func (u *BrokerDispatchUpsertBulk) SetResult(v string) *BrokerDispatchUpsertBulk { + return u.Update(func(s *BrokerDispatchUpsert) { + s.SetResult(v) + }) +} + +// UpdateResult sets the "result" field to the value that was provided on create. +func (u *BrokerDispatchUpsertBulk) UpdateResult() *BrokerDispatchUpsertBulk { + return u.Update(func(s *BrokerDispatchUpsert) { + s.UpdateResult() + }) +} + +// ClearResult clears the value of the "result" field. +func (u *BrokerDispatchUpsertBulk) ClearResult() *BrokerDispatchUpsertBulk { + return u.Update(func(s *BrokerDispatchUpsert) { + s.ClearResult() + }) +} + +// SetClaimedBy sets the "claimed_by" field. +func (u *BrokerDispatchUpsertBulk) SetClaimedBy(v string) *BrokerDispatchUpsertBulk { + return u.Update(func(s *BrokerDispatchUpsert) { + s.SetClaimedBy(v) + }) +} + +// UpdateClaimedBy sets the "claimed_by" field to the value that was provided on create. +func (u *BrokerDispatchUpsertBulk) UpdateClaimedBy() *BrokerDispatchUpsertBulk { + return u.Update(func(s *BrokerDispatchUpsert) { + s.UpdateClaimedBy() + }) +} + +// ClearClaimedBy clears the value of the "claimed_by" field. +func (u *BrokerDispatchUpsertBulk) ClearClaimedBy() *BrokerDispatchUpsertBulk { + return u.Update(func(s *BrokerDispatchUpsert) { + s.ClearClaimedBy() + }) +} + +// SetAttempts sets the "attempts" field. +func (u *BrokerDispatchUpsertBulk) SetAttempts(v int) *BrokerDispatchUpsertBulk { + return u.Update(func(s *BrokerDispatchUpsert) { + s.SetAttempts(v) + }) +} + +// AddAttempts adds v to the "attempts" field. +func (u *BrokerDispatchUpsertBulk) AddAttempts(v int) *BrokerDispatchUpsertBulk { + return u.Update(func(s *BrokerDispatchUpsert) { + s.AddAttempts(v) + }) +} + +// UpdateAttempts sets the "attempts" field to the value that was provided on create. +func (u *BrokerDispatchUpsertBulk) UpdateAttempts() *BrokerDispatchUpsertBulk { + return u.Update(func(s *BrokerDispatchUpsert) { + s.UpdateAttempts() + }) +} + +// SetError sets the "error" field. +func (u *BrokerDispatchUpsertBulk) SetError(v string) *BrokerDispatchUpsertBulk { + return u.Update(func(s *BrokerDispatchUpsert) { + s.SetError(v) + }) +} + +// UpdateError sets the "error" field to the value that was provided on create. +func (u *BrokerDispatchUpsertBulk) UpdateError() *BrokerDispatchUpsertBulk { + return u.Update(func(s *BrokerDispatchUpsert) { + s.UpdateError() + }) +} + +// ClearError clears the value of the "error" field. +func (u *BrokerDispatchUpsertBulk) ClearError() *BrokerDispatchUpsertBulk { + return u.Update(func(s *BrokerDispatchUpsert) { + s.ClearError() + }) +} + +// SetUpdatedAt sets the "updated_at" field. +func (u *BrokerDispatchUpsertBulk) SetUpdatedAt(v time.Time) *BrokerDispatchUpsertBulk { + return u.Update(func(s *BrokerDispatchUpsert) { + s.SetUpdatedAt(v) + }) +} + +// UpdateUpdatedAt sets the "updated_at" field to the value that was provided on create. +func (u *BrokerDispatchUpsertBulk) UpdateUpdatedAt() *BrokerDispatchUpsertBulk { + return u.Update(func(s *BrokerDispatchUpsert) { + s.UpdateUpdatedAt() + }) +} + +// SetDeadlineAt sets the "deadline_at" field. +func (u *BrokerDispatchUpsertBulk) SetDeadlineAt(v time.Time) *BrokerDispatchUpsertBulk { + return u.Update(func(s *BrokerDispatchUpsert) { + s.SetDeadlineAt(v) + }) +} + +// UpdateDeadlineAt sets the "deadline_at" field to the value that was provided on create. +func (u *BrokerDispatchUpsertBulk) UpdateDeadlineAt() *BrokerDispatchUpsertBulk { + return u.Update(func(s *BrokerDispatchUpsert) { + s.UpdateDeadlineAt() + }) +} + +// ClearDeadlineAt clears the value of the "deadline_at" field. +func (u *BrokerDispatchUpsertBulk) ClearDeadlineAt() *BrokerDispatchUpsertBulk { + return u.Update(func(s *BrokerDispatchUpsert) { + s.ClearDeadlineAt() + }) +} + +// Exec executes the query. +func (u *BrokerDispatchUpsertBulk) Exec(ctx context.Context) error { + if u.create.err != nil { + return u.create.err + } + for i, b := range u.create.builders { + if len(b.conflict) != 0 { + return fmt.Errorf("ent: OnConflict was set for builder %d. Set it on the BrokerDispatchCreateBulk instead", i) + } + } + if len(u.create.conflict) == 0 { + return errors.New("ent: missing options for BrokerDispatchCreateBulk.OnConflict") + } + return u.create.Exec(ctx) +} + +// ExecX is like Exec, but panics if an error occurs. +func (u *BrokerDispatchUpsertBulk) ExecX(ctx context.Context) { + if err := u.create.Exec(ctx); err != nil { + panic(err) + } +} diff --git a/pkg/ent/brokerdispatch_delete.go b/pkg/ent/brokerdispatch_delete.go new file mode 100644 index 000000000..88adbccfb --- /dev/null +++ b/pkg/ent/brokerdispatch_delete.go @@ -0,0 +1,88 @@ +// Code generated by ent, DO NOT EDIT. + +package ent + +import ( + "context" + + "entgo.io/ent/dialect/sql" + "entgo.io/ent/dialect/sql/sqlgraph" + "entgo.io/ent/schema/field" + "github.com/GoogleCloudPlatform/scion/pkg/ent/brokerdispatch" + "github.com/GoogleCloudPlatform/scion/pkg/ent/predicate" +) + +// BrokerDispatchDelete is the builder for deleting a BrokerDispatch entity. +type BrokerDispatchDelete struct { + config + hooks []Hook + mutation *BrokerDispatchMutation +} + +// Where appends a list predicates to the BrokerDispatchDelete builder. +func (_d *BrokerDispatchDelete) Where(ps ...predicate.BrokerDispatch) *BrokerDispatchDelete { + _d.mutation.Where(ps...) + return _d +} + +// Exec executes the deletion query and returns how many vertices were deleted. +func (_d *BrokerDispatchDelete) Exec(ctx context.Context) (int, error) { + return withHooks(ctx, _d.sqlExec, _d.mutation, _d.hooks) +} + +// ExecX is like Exec, but panics if an error occurs. +func (_d *BrokerDispatchDelete) ExecX(ctx context.Context) int { + n, err := _d.Exec(ctx) + if err != nil { + panic(err) + } + return n +} + +func (_d *BrokerDispatchDelete) sqlExec(ctx context.Context) (int, error) { + _spec := sqlgraph.NewDeleteSpec(brokerdispatch.Table, sqlgraph.NewFieldSpec(brokerdispatch.FieldID, field.TypeUUID)) + if ps := _d.mutation.predicates; len(ps) > 0 { + _spec.Predicate = func(selector *sql.Selector) { + for i := range ps { + ps[i](selector) + } + } + } + affected, err := sqlgraph.DeleteNodes(ctx, _d.driver, _spec) + if err != nil && sqlgraph.IsConstraintError(err) { + err = &ConstraintError{msg: err.Error(), wrap: err} + } + _d.mutation.done = true + return affected, err +} + +// BrokerDispatchDeleteOne is the builder for deleting a single BrokerDispatch entity. +type BrokerDispatchDeleteOne struct { + _d *BrokerDispatchDelete +} + +// Where appends a list predicates to the BrokerDispatchDelete builder. +func (_d *BrokerDispatchDeleteOne) Where(ps ...predicate.BrokerDispatch) *BrokerDispatchDeleteOne { + _d._d.mutation.Where(ps...) + return _d +} + +// Exec executes the deletion query. +func (_d *BrokerDispatchDeleteOne) Exec(ctx context.Context) error { + n, err := _d._d.Exec(ctx) + switch { + case err != nil: + return err + case n == 0: + return &NotFoundError{brokerdispatch.Label} + default: + return nil + } +} + +// ExecX is like Exec, but panics if an error occurs. +func (_d *BrokerDispatchDeleteOne) ExecX(ctx context.Context) { + if err := _d.Exec(ctx); err != nil { + panic(err) + } +} diff --git a/pkg/ent/brokerdispatch_query.go b/pkg/ent/brokerdispatch_query.go new file mode 100644 index 000000000..67bb47347 --- /dev/null +++ b/pkg/ent/brokerdispatch_query.go @@ -0,0 +1,565 @@ +// Code generated by ent, DO NOT EDIT. + +package ent + +import ( + "context" + "fmt" + "math" + + "entgo.io/ent" + "entgo.io/ent/dialect" + "entgo.io/ent/dialect/sql" + "entgo.io/ent/dialect/sql/sqlgraph" + "entgo.io/ent/schema/field" + "github.com/GoogleCloudPlatform/scion/pkg/ent/brokerdispatch" + "github.com/GoogleCloudPlatform/scion/pkg/ent/predicate" + "github.com/google/uuid" +) + +// BrokerDispatchQuery is the builder for querying BrokerDispatch entities. +type BrokerDispatchQuery struct { + config + ctx *QueryContext + order []brokerdispatch.OrderOption + inters []Interceptor + predicates []predicate.BrokerDispatch + modifiers []func(*sql.Selector) + // intermediate query (i.e. traversal path). + sql *sql.Selector + path func(context.Context) (*sql.Selector, error) +} + +// Where adds a new predicate for the BrokerDispatchQuery builder. +func (_q *BrokerDispatchQuery) Where(ps ...predicate.BrokerDispatch) *BrokerDispatchQuery { + _q.predicates = append(_q.predicates, ps...) + return _q +} + +// Limit the number of records to be returned by this query. +func (_q *BrokerDispatchQuery) Limit(limit int) *BrokerDispatchQuery { + _q.ctx.Limit = &limit + return _q +} + +// Offset to start from. +func (_q *BrokerDispatchQuery) Offset(offset int) *BrokerDispatchQuery { + _q.ctx.Offset = &offset + return _q +} + +// Unique configures the query builder to filter duplicate records on query. +// By default, unique is set to true, and can be disabled using this method. +func (_q *BrokerDispatchQuery) Unique(unique bool) *BrokerDispatchQuery { + _q.ctx.Unique = &unique + return _q +} + +// Order specifies how the records should be ordered. +func (_q *BrokerDispatchQuery) Order(o ...brokerdispatch.OrderOption) *BrokerDispatchQuery { + _q.order = append(_q.order, o...) + return _q +} + +// First returns the first BrokerDispatch entity from the query. +// Returns a *NotFoundError when no BrokerDispatch was found. +func (_q *BrokerDispatchQuery) First(ctx context.Context) (*BrokerDispatch, error) { + nodes, err := _q.Limit(1).All(setContextOp(ctx, _q.ctx, ent.OpQueryFirst)) + if err != nil { + return nil, err + } + if len(nodes) == 0 { + return nil, &NotFoundError{brokerdispatch.Label} + } + return nodes[0], nil +} + +// FirstX is like First, but panics if an error occurs. +func (_q *BrokerDispatchQuery) FirstX(ctx context.Context) *BrokerDispatch { + node, err := _q.First(ctx) + if err != nil && !IsNotFound(err) { + panic(err) + } + return node +} + +// FirstID returns the first BrokerDispatch ID from the query. +// Returns a *NotFoundError when no BrokerDispatch ID was found. +func (_q *BrokerDispatchQuery) FirstID(ctx context.Context) (id uuid.UUID, err error) { + var ids []uuid.UUID + if ids, err = _q.Limit(1).IDs(setContextOp(ctx, _q.ctx, ent.OpQueryFirstID)); err != nil { + return + } + if len(ids) == 0 { + err = &NotFoundError{brokerdispatch.Label} + return + } + return ids[0], nil +} + +// FirstIDX is like FirstID, but panics if an error occurs. +func (_q *BrokerDispatchQuery) FirstIDX(ctx context.Context) uuid.UUID { + id, err := _q.FirstID(ctx) + if err != nil && !IsNotFound(err) { + panic(err) + } + return id +} + +// Only returns a single BrokerDispatch entity found by the query, ensuring it only returns one. +// Returns a *NotSingularError when more than one BrokerDispatch entity is found. +// Returns a *NotFoundError when no BrokerDispatch entities are found. +func (_q *BrokerDispatchQuery) Only(ctx context.Context) (*BrokerDispatch, error) { + nodes, err := _q.Limit(2).All(setContextOp(ctx, _q.ctx, ent.OpQueryOnly)) + if err != nil { + return nil, err + } + switch len(nodes) { + case 1: + return nodes[0], nil + case 0: + return nil, &NotFoundError{brokerdispatch.Label} + default: + return nil, &NotSingularError{brokerdispatch.Label} + } +} + +// OnlyX is like Only, but panics if an error occurs. +func (_q *BrokerDispatchQuery) OnlyX(ctx context.Context) *BrokerDispatch { + node, err := _q.Only(ctx) + if err != nil { + panic(err) + } + return node +} + +// OnlyID is like Only, but returns the only BrokerDispatch ID in the query. +// Returns a *NotSingularError when more than one BrokerDispatch ID is found. +// Returns a *NotFoundError when no entities are found. +func (_q *BrokerDispatchQuery) OnlyID(ctx context.Context) (id uuid.UUID, err error) { + var ids []uuid.UUID + if ids, err = _q.Limit(2).IDs(setContextOp(ctx, _q.ctx, ent.OpQueryOnlyID)); err != nil { + return + } + switch len(ids) { + case 1: + id = ids[0] + case 0: + err = &NotFoundError{brokerdispatch.Label} + default: + err = &NotSingularError{brokerdispatch.Label} + } + return +} + +// OnlyIDX is like OnlyID, but panics if an error occurs. +func (_q *BrokerDispatchQuery) OnlyIDX(ctx context.Context) uuid.UUID { + id, err := _q.OnlyID(ctx) + if err != nil { + panic(err) + } + return id +} + +// All executes the query and returns a list of BrokerDispatches. +func (_q *BrokerDispatchQuery) All(ctx context.Context) ([]*BrokerDispatch, error) { + ctx = setContextOp(ctx, _q.ctx, ent.OpQueryAll) + if err := _q.prepareQuery(ctx); err != nil { + return nil, err + } + qr := querierAll[[]*BrokerDispatch, *BrokerDispatchQuery]() + return withInterceptors[[]*BrokerDispatch](ctx, _q, qr, _q.inters) +} + +// AllX is like All, but panics if an error occurs. +func (_q *BrokerDispatchQuery) AllX(ctx context.Context) []*BrokerDispatch { + nodes, err := _q.All(ctx) + if err != nil { + panic(err) + } + return nodes +} + +// IDs executes the query and returns a list of BrokerDispatch IDs. +func (_q *BrokerDispatchQuery) IDs(ctx context.Context) (ids []uuid.UUID, err error) { + if _q.ctx.Unique == nil && _q.path != nil { + _q.Unique(true) + } + ctx = setContextOp(ctx, _q.ctx, ent.OpQueryIDs) + if err = _q.Select(brokerdispatch.FieldID).Scan(ctx, &ids); err != nil { + return nil, err + } + return ids, nil +} + +// IDsX is like IDs, but panics if an error occurs. +func (_q *BrokerDispatchQuery) IDsX(ctx context.Context) []uuid.UUID { + ids, err := _q.IDs(ctx) + if err != nil { + panic(err) + } + return ids +} + +// Count returns the count of the given query. +func (_q *BrokerDispatchQuery) Count(ctx context.Context) (int, error) { + ctx = setContextOp(ctx, _q.ctx, ent.OpQueryCount) + if err := _q.prepareQuery(ctx); err != nil { + return 0, err + } + return withInterceptors[int](ctx, _q, querierCount[*BrokerDispatchQuery](), _q.inters) +} + +// CountX is like Count, but panics if an error occurs. +func (_q *BrokerDispatchQuery) CountX(ctx context.Context) int { + count, err := _q.Count(ctx) + if err != nil { + panic(err) + } + return count +} + +// Exist returns true if the query has elements in the graph. +func (_q *BrokerDispatchQuery) Exist(ctx context.Context) (bool, error) { + ctx = setContextOp(ctx, _q.ctx, ent.OpQueryExist) + switch _, err := _q.FirstID(ctx); { + case IsNotFound(err): + return false, nil + case err != nil: + return false, fmt.Errorf("ent: check existence: %w", err) + default: + return true, nil + } +} + +// ExistX is like Exist, but panics if an error occurs. +func (_q *BrokerDispatchQuery) ExistX(ctx context.Context) bool { + exist, err := _q.Exist(ctx) + if err != nil { + panic(err) + } + return exist +} + +// Clone returns a duplicate of the BrokerDispatchQuery builder, including all associated steps. It can be +// used to prepare common query builders and use them differently after the clone is made. +func (_q *BrokerDispatchQuery) Clone() *BrokerDispatchQuery { + if _q == nil { + return nil + } + return &BrokerDispatchQuery{ + config: _q.config, + ctx: _q.ctx.Clone(), + order: append([]brokerdispatch.OrderOption{}, _q.order...), + inters: append([]Interceptor{}, _q.inters...), + predicates: append([]predicate.BrokerDispatch{}, _q.predicates...), + // clone intermediate query. + sql: _q.sql.Clone(), + path: _q.path, + } +} + +// GroupBy is used to group vertices by one or more fields/columns. +// It is often used with aggregate functions, like: count, max, mean, min, sum. +// +// Example: +// +// var v []struct { +// BrokerID uuid.UUID `json:"broker_id,omitempty"` +// Count int `json:"count,omitempty"` +// } +// +// client.BrokerDispatch.Query(). +// GroupBy(brokerdispatch.FieldBrokerID). +// Aggregate(ent.Count()). +// Scan(ctx, &v) +func (_q *BrokerDispatchQuery) GroupBy(field string, fields ...string) *BrokerDispatchGroupBy { + _q.ctx.Fields = append([]string{field}, fields...) + grbuild := &BrokerDispatchGroupBy{build: _q} + grbuild.flds = &_q.ctx.Fields + grbuild.label = brokerdispatch.Label + grbuild.scan = grbuild.Scan + return grbuild +} + +// Select allows the selection one or more fields/columns for the given query, +// instead of selecting all fields in the entity. +// +// Example: +// +// var v []struct { +// BrokerID uuid.UUID `json:"broker_id,omitempty"` +// } +// +// client.BrokerDispatch.Query(). +// Select(brokerdispatch.FieldBrokerID). +// Scan(ctx, &v) +func (_q *BrokerDispatchQuery) Select(fields ...string) *BrokerDispatchSelect { + _q.ctx.Fields = append(_q.ctx.Fields, fields...) + sbuild := &BrokerDispatchSelect{BrokerDispatchQuery: _q} + sbuild.label = brokerdispatch.Label + sbuild.flds, sbuild.scan = &_q.ctx.Fields, sbuild.Scan + return sbuild +} + +// Aggregate returns a BrokerDispatchSelect configured with the given aggregations. +func (_q *BrokerDispatchQuery) Aggregate(fns ...AggregateFunc) *BrokerDispatchSelect { + return _q.Select().Aggregate(fns...) +} + +func (_q *BrokerDispatchQuery) prepareQuery(ctx context.Context) error { + for _, inter := range _q.inters { + if inter == nil { + return fmt.Errorf("ent: uninitialized interceptor (forgotten import ent/runtime?)") + } + if trv, ok := inter.(Traverser); ok { + if err := trv.Traverse(ctx, _q); err != nil { + return err + } + } + } + for _, f := range _q.ctx.Fields { + if !brokerdispatch.ValidColumn(f) { + return &ValidationError{Name: f, err: fmt.Errorf("ent: invalid field %q for query", f)} + } + } + if _q.path != nil { + prev, err := _q.path(ctx) + if err != nil { + return err + } + _q.sql = prev + } + return nil +} + +func (_q *BrokerDispatchQuery) sqlAll(ctx context.Context, hooks ...queryHook) ([]*BrokerDispatch, error) { + var ( + nodes = []*BrokerDispatch{} + _spec = _q.querySpec() + ) + _spec.ScanValues = func(columns []string) ([]any, error) { + return (*BrokerDispatch).scanValues(nil, columns) + } + _spec.Assign = func(columns []string, values []any) error { + node := &BrokerDispatch{config: _q.config} + nodes = append(nodes, node) + return node.assignValues(columns, values) + } + if len(_q.modifiers) > 0 { + _spec.Modifiers = _q.modifiers + } + for i := range hooks { + hooks[i](ctx, _spec) + } + if err := sqlgraph.QueryNodes(ctx, _q.driver, _spec); err != nil { + return nil, err + } + if len(nodes) == 0 { + return nodes, nil + } + return nodes, nil +} + +func (_q *BrokerDispatchQuery) sqlCount(ctx context.Context) (int, error) { + _spec := _q.querySpec() + if len(_q.modifiers) > 0 { + _spec.Modifiers = _q.modifiers + } + _spec.Node.Columns = _q.ctx.Fields + if len(_q.ctx.Fields) > 0 { + _spec.Unique = _q.ctx.Unique != nil && *_q.ctx.Unique + } + return sqlgraph.CountNodes(ctx, _q.driver, _spec) +} + +func (_q *BrokerDispatchQuery) querySpec() *sqlgraph.QuerySpec { + _spec := sqlgraph.NewQuerySpec(brokerdispatch.Table, brokerdispatch.Columns, sqlgraph.NewFieldSpec(brokerdispatch.FieldID, field.TypeUUID)) + _spec.From = _q.sql + if unique := _q.ctx.Unique; unique != nil { + _spec.Unique = *unique + } else if _q.path != nil { + _spec.Unique = true + } + if fields := _q.ctx.Fields; len(fields) > 0 { + _spec.Node.Columns = make([]string, 0, len(fields)) + _spec.Node.Columns = append(_spec.Node.Columns, brokerdispatch.FieldID) + for i := range fields { + if fields[i] != brokerdispatch.FieldID { + _spec.Node.Columns = append(_spec.Node.Columns, fields[i]) + } + } + } + if ps := _q.predicates; len(ps) > 0 { + _spec.Predicate = func(selector *sql.Selector) { + for i := range ps { + ps[i](selector) + } + } + } + if limit := _q.ctx.Limit; limit != nil { + _spec.Limit = *limit + } + if offset := _q.ctx.Offset; offset != nil { + _spec.Offset = *offset + } + if ps := _q.order; len(ps) > 0 { + _spec.Order = func(selector *sql.Selector) { + for i := range ps { + ps[i](selector) + } + } + } + return _spec +} + +func (_q *BrokerDispatchQuery) sqlQuery(ctx context.Context) *sql.Selector { + builder := sql.Dialect(_q.driver.Dialect()) + t1 := builder.Table(brokerdispatch.Table) + columns := _q.ctx.Fields + if len(columns) == 0 { + columns = brokerdispatch.Columns + } + selector := builder.Select(t1.Columns(columns...)...).From(t1) + if _q.sql != nil { + selector = _q.sql + selector.Select(selector.Columns(columns...)...) + } + if _q.ctx.Unique != nil && *_q.ctx.Unique { + selector.Distinct() + } + for _, m := range _q.modifiers { + m(selector) + } + for _, p := range _q.predicates { + p(selector) + } + for _, p := range _q.order { + p(selector) + } + if offset := _q.ctx.Offset; offset != nil { + // limit is mandatory for offset clause. We start + // with default value, and override it below if needed. + selector.Offset(*offset).Limit(math.MaxInt32) + } + if limit := _q.ctx.Limit; limit != nil { + selector.Limit(*limit) + } + return selector +} + +// ForUpdate locks the selected rows against concurrent updates, and prevent them from being +// updated, deleted or "selected ... for update" by other sessions, until the transaction is +// either committed or rolled-back. +func (_q *BrokerDispatchQuery) ForUpdate(opts ...sql.LockOption) *BrokerDispatchQuery { + if _q.driver.Dialect() == dialect.Postgres { + _q.Unique(false) + } + _q.modifiers = append(_q.modifiers, func(s *sql.Selector) { + s.ForUpdate(opts...) + }) + return _q +} + +// ForShare behaves similarly to ForUpdate, except that it acquires a shared mode lock +// on any rows that are read. Other sessions can read the rows, but cannot modify them +// until your transaction commits. +func (_q *BrokerDispatchQuery) ForShare(opts ...sql.LockOption) *BrokerDispatchQuery { + if _q.driver.Dialect() == dialect.Postgres { + _q.Unique(false) + } + _q.modifiers = append(_q.modifiers, func(s *sql.Selector) { + s.ForShare(opts...) + }) + return _q +} + +// BrokerDispatchGroupBy is the group-by builder for BrokerDispatch entities. +type BrokerDispatchGroupBy struct { + selector + build *BrokerDispatchQuery +} + +// Aggregate adds the given aggregation functions to the group-by query. +func (_g *BrokerDispatchGroupBy) Aggregate(fns ...AggregateFunc) *BrokerDispatchGroupBy { + _g.fns = append(_g.fns, fns...) + return _g +} + +// Scan applies the selector query and scans the result into the given value. +func (_g *BrokerDispatchGroupBy) Scan(ctx context.Context, v any) error { + ctx = setContextOp(ctx, _g.build.ctx, ent.OpQueryGroupBy) + if err := _g.build.prepareQuery(ctx); err != nil { + return err + } + return scanWithInterceptors[*BrokerDispatchQuery, *BrokerDispatchGroupBy](ctx, _g.build, _g, _g.build.inters, v) +} + +func (_g *BrokerDispatchGroupBy) sqlScan(ctx context.Context, root *BrokerDispatchQuery, v any) error { + selector := root.sqlQuery(ctx).Select() + aggregation := make([]string, 0, len(_g.fns)) + for _, fn := range _g.fns { + aggregation = append(aggregation, fn(selector)) + } + if len(selector.SelectedColumns()) == 0 { + columns := make([]string, 0, len(*_g.flds)+len(_g.fns)) + for _, f := range *_g.flds { + columns = append(columns, selector.C(f)) + } + columns = append(columns, aggregation...) + selector.Select(columns...) + } + selector.GroupBy(selector.Columns(*_g.flds...)...) + if err := selector.Err(); err != nil { + return err + } + rows := &sql.Rows{} + query, args := selector.Query() + if err := _g.build.driver.Query(ctx, query, args, rows); err != nil { + return err + } + defer rows.Close() + return sql.ScanSlice(rows, v) +} + +// BrokerDispatchSelect is the builder for selecting fields of BrokerDispatch entities. +type BrokerDispatchSelect struct { + *BrokerDispatchQuery + selector +} + +// Aggregate adds the given aggregation functions to the selector query. +func (_s *BrokerDispatchSelect) Aggregate(fns ...AggregateFunc) *BrokerDispatchSelect { + _s.fns = append(_s.fns, fns...) + return _s +} + +// Scan applies the selector query and scans the result into the given value. +func (_s *BrokerDispatchSelect) Scan(ctx context.Context, v any) error { + ctx = setContextOp(ctx, _s.ctx, ent.OpQuerySelect) + if err := _s.prepareQuery(ctx); err != nil { + return err + } + return scanWithInterceptors[*BrokerDispatchQuery, *BrokerDispatchSelect](ctx, _s.BrokerDispatchQuery, _s, _s.inters, v) +} + +func (_s *BrokerDispatchSelect) sqlScan(ctx context.Context, root *BrokerDispatchQuery, v any) error { + selector := root.sqlQuery(ctx) + aggregation := make([]string, 0, len(_s.fns)) + for _, fn := range _s.fns { + aggregation = append(aggregation, fn(selector)) + } + switch n := len(*_s.selector.flds); { + case n == 0 && len(aggregation) > 0: + selector.Select(aggregation...) + case n != 0 && len(aggregation) > 0: + selector.AppendSelect(aggregation...) + } + rows := &sql.Rows{} + query, args := selector.Query() + if err := _s.driver.Query(ctx, query, args, rows); err != nil { + return err + } + defer rows.Close() + return sql.ScanSlice(rows, v) +} diff --git a/pkg/ent/brokerdispatch_update.go b/pkg/ent/brokerdispatch_update.go new file mode 100644 index 000000000..be039862b --- /dev/null +++ b/pkg/ent/brokerdispatch_update.go @@ -0,0 +1,811 @@ +// Code generated by ent, DO NOT EDIT. + +package ent + +import ( + "context" + "errors" + "fmt" + "time" + + "entgo.io/ent/dialect/sql" + "entgo.io/ent/dialect/sql/sqlgraph" + "entgo.io/ent/schema/field" + "github.com/GoogleCloudPlatform/scion/pkg/ent/brokerdispatch" + "github.com/GoogleCloudPlatform/scion/pkg/ent/predicate" + "github.com/google/uuid" +) + +// BrokerDispatchUpdate is the builder for updating BrokerDispatch entities. +type BrokerDispatchUpdate struct { + config + hooks []Hook + mutation *BrokerDispatchMutation +} + +// Where appends a list predicates to the BrokerDispatchUpdate builder. +func (_u *BrokerDispatchUpdate) Where(ps ...predicate.BrokerDispatch) *BrokerDispatchUpdate { + _u.mutation.Where(ps...) + return _u +} + +// SetBrokerID sets the "broker_id" field. +func (_u *BrokerDispatchUpdate) SetBrokerID(v uuid.UUID) *BrokerDispatchUpdate { + _u.mutation.SetBrokerID(v) + return _u +} + +// SetNillableBrokerID sets the "broker_id" field if the given value is not nil. +func (_u *BrokerDispatchUpdate) SetNillableBrokerID(v *uuid.UUID) *BrokerDispatchUpdate { + if v != nil { + _u.SetBrokerID(*v) + } + return _u +} + +// SetAgentID sets the "agent_id" field. +func (_u *BrokerDispatchUpdate) SetAgentID(v uuid.UUID) *BrokerDispatchUpdate { + _u.mutation.SetAgentID(v) + return _u +} + +// SetNillableAgentID sets the "agent_id" field if the given value is not nil. +func (_u *BrokerDispatchUpdate) SetNillableAgentID(v *uuid.UUID) *BrokerDispatchUpdate { + if v != nil { + _u.SetAgentID(*v) + } + return _u +} + +// ClearAgentID clears the value of the "agent_id" field. +func (_u *BrokerDispatchUpdate) ClearAgentID() *BrokerDispatchUpdate { + _u.mutation.ClearAgentID() + return _u +} + +// SetAgentSlug sets the "agent_slug" field. +func (_u *BrokerDispatchUpdate) SetAgentSlug(v string) *BrokerDispatchUpdate { + _u.mutation.SetAgentSlug(v) + return _u +} + +// SetNillableAgentSlug sets the "agent_slug" field if the given value is not nil. +func (_u *BrokerDispatchUpdate) SetNillableAgentSlug(v *string) *BrokerDispatchUpdate { + if v != nil { + _u.SetAgentSlug(*v) + } + return _u +} + +// ClearAgentSlug clears the value of the "agent_slug" field. +func (_u *BrokerDispatchUpdate) ClearAgentSlug() *BrokerDispatchUpdate { + _u.mutation.ClearAgentSlug() + return _u +} + +// SetProjectID sets the "project_id" field. +func (_u *BrokerDispatchUpdate) SetProjectID(v uuid.UUID) *BrokerDispatchUpdate { + _u.mutation.SetProjectID(v) + return _u +} + +// SetNillableProjectID sets the "project_id" field if the given value is not nil. +func (_u *BrokerDispatchUpdate) SetNillableProjectID(v *uuid.UUID) *BrokerDispatchUpdate { + if v != nil { + _u.SetProjectID(*v) + } + return _u +} + +// ClearProjectID clears the value of the "project_id" field. +func (_u *BrokerDispatchUpdate) ClearProjectID() *BrokerDispatchUpdate { + _u.mutation.ClearProjectID() + return _u +} + +// SetOp sets the "op" field. +func (_u *BrokerDispatchUpdate) SetOp(v string) *BrokerDispatchUpdate { + _u.mutation.SetOpField(v) + return _u +} + +// SetNillableOp sets the "op" field if the given value is not nil. +func (_u *BrokerDispatchUpdate) SetNillableOp(v *string) *BrokerDispatchUpdate { + if v != nil { + _u.SetOp(*v) + } + return _u +} + +// SetArgs sets the "args" field. +func (_u *BrokerDispatchUpdate) SetArgs(v string) *BrokerDispatchUpdate { + _u.mutation.SetArgs(v) + return _u +} + +// SetNillableArgs sets the "args" field if the given value is not nil. +func (_u *BrokerDispatchUpdate) SetNillableArgs(v *string) *BrokerDispatchUpdate { + if v != nil { + _u.SetArgs(*v) + } + return _u +} + +// ClearArgs clears the value of the "args" field. +func (_u *BrokerDispatchUpdate) ClearArgs() *BrokerDispatchUpdate { + _u.mutation.ClearArgs() + return _u +} + +// SetState sets the "state" field. +func (_u *BrokerDispatchUpdate) SetState(v string) *BrokerDispatchUpdate { + _u.mutation.SetState(v) + return _u +} + +// SetNillableState sets the "state" field if the given value is not nil. +func (_u *BrokerDispatchUpdate) SetNillableState(v *string) *BrokerDispatchUpdate { + if v != nil { + _u.SetState(*v) + } + return _u +} + +// SetResult sets the "result" field. +func (_u *BrokerDispatchUpdate) SetResult(v string) *BrokerDispatchUpdate { + _u.mutation.SetResult(v) + return _u +} + +// SetNillableResult sets the "result" field if the given value is not nil. +func (_u *BrokerDispatchUpdate) SetNillableResult(v *string) *BrokerDispatchUpdate { + if v != nil { + _u.SetResult(*v) + } + return _u +} + +// ClearResult clears the value of the "result" field. +func (_u *BrokerDispatchUpdate) ClearResult() *BrokerDispatchUpdate { + _u.mutation.ClearResult() + return _u +} + +// SetClaimedBy sets the "claimed_by" field. +func (_u *BrokerDispatchUpdate) SetClaimedBy(v string) *BrokerDispatchUpdate { + _u.mutation.SetClaimedBy(v) + return _u +} + +// SetNillableClaimedBy sets the "claimed_by" field if the given value is not nil. +func (_u *BrokerDispatchUpdate) SetNillableClaimedBy(v *string) *BrokerDispatchUpdate { + if v != nil { + _u.SetClaimedBy(*v) + } + return _u +} + +// ClearClaimedBy clears the value of the "claimed_by" field. +func (_u *BrokerDispatchUpdate) ClearClaimedBy() *BrokerDispatchUpdate { + _u.mutation.ClearClaimedBy() + return _u +} + +// SetAttempts sets the "attempts" field. +func (_u *BrokerDispatchUpdate) SetAttempts(v int) *BrokerDispatchUpdate { + _u.mutation.ResetAttempts() + _u.mutation.SetAttempts(v) + return _u +} + +// SetNillableAttempts sets the "attempts" field if the given value is not nil. +func (_u *BrokerDispatchUpdate) SetNillableAttempts(v *int) *BrokerDispatchUpdate { + if v != nil { + _u.SetAttempts(*v) + } + return _u +} + +// AddAttempts adds value to the "attempts" field. +func (_u *BrokerDispatchUpdate) AddAttempts(v int) *BrokerDispatchUpdate { + _u.mutation.AddAttempts(v) + return _u +} + +// SetError sets the "error" field. +func (_u *BrokerDispatchUpdate) SetError(v string) *BrokerDispatchUpdate { + _u.mutation.SetError(v) + return _u +} + +// SetNillableError sets the "error" field if the given value is not nil. +func (_u *BrokerDispatchUpdate) SetNillableError(v *string) *BrokerDispatchUpdate { + if v != nil { + _u.SetError(*v) + } + return _u +} + +// ClearError clears the value of the "error" field. +func (_u *BrokerDispatchUpdate) ClearError() *BrokerDispatchUpdate { + _u.mutation.ClearError() + return _u +} + +// SetUpdatedAt sets the "updated_at" field. +func (_u *BrokerDispatchUpdate) SetUpdatedAt(v time.Time) *BrokerDispatchUpdate { + _u.mutation.SetUpdatedAt(v) + return _u +} + +// SetDeadlineAt sets the "deadline_at" field. +func (_u *BrokerDispatchUpdate) SetDeadlineAt(v time.Time) *BrokerDispatchUpdate { + _u.mutation.SetDeadlineAt(v) + return _u +} + +// SetNillableDeadlineAt sets the "deadline_at" field if the given value is not nil. +func (_u *BrokerDispatchUpdate) SetNillableDeadlineAt(v *time.Time) *BrokerDispatchUpdate { + if v != nil { + _u.SetDeadlineAt(*v) + } + return _u +} + +// ClearDeadlineAt clears the value of the "deadline_at" field. +func (_u *BrokerDispatchUpdate) ClearDeadlineAt() *BrokerDispatchUpdate { + _u.mutation.ClearDeadlineAt() + return _u +} + +// Mutation returns the BrokerDispatchMutation object of the builder. +func (_u *BrokerDispatchUpdate) Mutation() *BrokerDispatchMutation { + return _u.mutation +} + +// Save executes the query and returns the number of nodes affected by the update operation. +func (_u *BrokerDispatchUpdate) Save(ctx context.Context) (int, error) { + _u.defaults() + return withHooks(ctx, _u.sqlSave, _u.mutation, _u.hooks) +} + +// SaveX is like Save, but panics if an error occurs. +func (_u *BrokerDispatchUpdate) SaveX(ctx context.Context) int { + affected, err := _u.Save(ctx) + if err != nil { + panic(err) + } + return affected +} + +// Exec executes the query. +func (_u *BrokerDispatchUpdate) Exec(ctx context.Context) error { + _, err := _u.Save(ctx) + return err +} + +// ExecX is like Exec, but panics if an error occurs. +func (_u *BrokerDispatchUpdate) ExecX(ctx context.Context) { + if err := _u.Exec(ctx); err != nil { + panic(err) + } +} + +// defaults sets the default values of the builder before save. +func (_u *BrokerDispatchUpdate) defaults() { + if _, ok := _u.mutation.UpdatedAt(); !ok { + v := brokerdispatch.UpdateDefaultUpdatedAt() + _u.mutation.SetUpdatedAt(v) + } +} + +// check runs all checks and user-defined validators on the builder. +func (_u *BrokerDispatchUpdate) check() error { + if v, ok := _u.mutation.GetOp(); ok { + if err := brokerdispatch.OpValidator(v); err != nil { + return &ValidationError{Name: "op", err: fmt.Errorf(`ent: validator failed for field "BrokerDispatch.op": %w`, err)} + } + } + return nil +} + +func (_u *BrokerDispatchUpdate) sqlSave(ctx context.Context) (_node int, err error) { + if err := _u.check(); err != nil { + return _node, err + } + _spec := sqlgraph.NewUpdateSpec(brokerdispatch.Table, brokerdispatch.Columns, sqlgraph.NewFieldSpec(brokerdispatch.FieldID, field.TypeUUID)) + if ps := _u.mutation.predicates; len(ps) > 0 { + _spec.Predicate = func(selector *sql.Selector) { + for i := range ps { + ps[i](selector) + } + } + } + if value, ok := _u.mutation.BrokerID(); ok { + _spec.SetField(brokerdispatch.FieldBrokerID, field.TypeUUID, value) + } + if value, ok := _u.mutation.AgentID(); ok { + _spec.SetField(brokerdispatch.FieldAgentID, field.TypeUUID, value) + } + if _u.mutation.AgentIDCleared() { + _spec.ClearField(brokerdispatch.FieldAgentID, field.TypeUUID) + } + if value, ok := _u.mutation.AgentSlug(); ok { + _spec.SetField(brokerdispatch.FieldAgentSlug, field.TypeString, value) + } + if _u.mutation.AgentSlugCleared() { + _spec.ClearField(brokerdispatch.FieldAgentSlug, field.TypeString) + } + if value, ok := _u.mutation.ProjectID(); ok { + _spec.SetField(brokerdispatch.FieldProjectID, field.TypeUUID, value) + } + if _u.mutation.ProjectIDCleared() { + _spec.ClearField(brokerdispatch.FieldProjectID, field.TypeUUID) + } + if value, ok := _u.mutation.GetOp(); ok { + _spec.SetField(brokerdispatch.FieldOp, field.TypeString, value) + } + if value, ok := _u.mutation.Args(); ok { + _spec.SetField(brokerdispatch.FieldArgs, field.TypeString, value) + } + if _u.mutation.ArgsCleared() { + _spec.ClearField(brokerdispatch.FieldArgs, field.TypeString) + } + if value, ok := _u.mutation.State(); ok { + _spec.SetField(brokerdispatch.FieldState, field.TypeString, value) + } + if value, ok := _u.mutation.Result(); ok { + _spec.SetField(brokerdispatch.FieldResult, field.TypeString, value) + } + if _u.mutation.ResultCleared() { + _spec.ClearField(brokerdispatch.FieldResult, field.TypeString) + } + if value, ok := _u.mutation.ClaimedBy(); ok { + _spec.SetField(brokerdispatch.FieldClaimedBy, field.TypeString, value) + } + if _u.mutation.ClaimedByCleared() { + _spec.ClearField(brokerdispatch.FieldClaimedBy, field.TypeString) + } + if value, ok := _u.mutation.Attempts(); ok { + _spec.SetField(brokerdispatch.FieldAttempts, field.TypeInt, value) + } + if value, ok := _u.mutation.AddedAttempts(); ok { + _spec.AddField(brokerdispatch.FieldAttempts, field.TypeInt, value) + } + if value, ok := _u.mutation.Error(); ok { + _spec.SetField(brokerdispatch.FieldError, field.TypeString, value) + } + if _u.mutation.ErrorCleared() { + _spec.ClearField(brokerdispatch.FieldError, field.TypeString) + } + if value, ok := _u.mutation.UpdatedAt(); ok { + _spec.SetField(brokerdispatch.FieldUpdatedAt, field.TypeTime, value) + } + if value, ok := _u.mutation.DeadlineAt(); ok { + _spec.SetField(brokerdispatch.FieldDeadlineAt, field.TypeTime, value) + } + if _u.mutation.DeadlineAtCleared() { + _spec.ClearField(brokerdispatch.FieldDeadlineAt, field.TypeTime) + } + if _node, err = sqlgraph.UpdateNodes(ctx, _u.driver, _spec); err != nil { + if _, ok := err.(*sqlgraph.NotFoundError); ok { + err = &NotFoundError{brokerdispatch.Label} + } else if sqlgraph.IsConstraintError(err) { + err = &ConstraintError{msg: err.Error(), wrap: err} + } + return 0, err + } + _u.mutation.done = true + return _node, nil +} + +// BrokerDispatchUpdateOne is the builder for updating a single BrokerDispatch entity. +type BrokerDispatchUpdateOne struct { + config + fields []string + hooks []Hook + mutation *BrokerDispatchMutation +} + +// SetBrokerID sets the "broker_id" field. +func (_u *BrokerDispatchUpdateOne) SetBrokerID(v uuid.UUID) *BrokerDispatchUpdateOne { + _u.mutation.SetBrokerID(v) + return _u +} + +// SetNillableBrokerID sets the "broker_id" field if the given value is not nil. +func (_u *BrokerDispatchUpdateOne) SetNillableBrokerID(v *uuid.UUID) *BrokerDispatchUpdateOne { + if v != nil { + _u.SetBrokerID(*v) + } + return _u +} + +// SetAgentID sets the "agent_id" field. +func (_u *BrokerDispatchUpdateOne) SetAgentID(v uuid.UUID) *BrokerDispatchUpdateOne { + _u.mutation.SetAgentID(v) + return _u +} + +// SetNillableAgentID sets the "agent_id" field if the given value is not nil. +func (_u *BrokerDispatchUpdateOne) SetNillableAgentID(v *uuid.UUID) *BrokerDispatchUpdateOne { + if v != nil { + _u.SetAgentID(*v) + } + return _u +} + +// ClearAgentID clears the value of the "agent_id" field. +func (_u *BrokerDispatchUpdateOne) ClearAgentID() *BrokerDispatchUpdateOne { + _u.mutation.ClearAgentID() + return _u +} + +// SetAgentSlug sets the "agent_slug" field. +func (_u *BrokerDispatchUpdateOne) SetAgentSlug(v string) *BrokerDispatchUpdateOne { + _u.mutation.SetAgentSlug(v) + return _u +} + +// SetNillableAgentSlug sets the "agent_slug" field if the given value is not nil. +func (_u *BrokerDispatchUpdateOne) SetNillableAgentSlug(v *string) *BrokerDispatchUpdateOne { + if v != nil { + _u.SetAgentSlug(*v) + } + return _u +} + +// ClearAgentSlug clears the value of the "agent_slug" field. +func (_u *BrokerDispatchUpdateOne) ClearAgentSlug() *BrokerDispatchUpdateOne { + _u.mutation.ClearAgentSlug() + return _u +} + +// SetProjectID sets the "project_id" field. +func (_u *BrokerDispatchUpdateOne) SetProjectID(v uuid.UUID) *BrokerDispatchUpdateOne { + _u.mutation.SetProjectID(v) + return _u +} + +// SetNillableProjectID sets the "project_id" field if the given value is not nil. +func (_u *BrokerDispatchUpdateOne) SetNillableProjectID(v *uuid.UUID) *BrokerDispatchUpdateOne { + if v != nil { + _u.SetProjectID(*v) + } + return _u +} + +// ClearProjectID clears the value of the "project_id" field. +func (_u *BrokerDispatchUpdateOne) ClearProjectID() *BrokerDispatchUpdateOne { + _u.mutation.ClearProjectID() + return _u +} + +// SetOp sets the "op" field. +func (_u *BrokerDispatchUpdateOne) SetOp(v string) *BrokerDispatchUpdateOne { + _u.mutation.SetOpField(v) + return _u +} + +// SetNillableOp sets the "op" field if the given value is not nil. +func (_u *BrokerDispatchUpdateOne) SetNillableOp(v *string) *BrokerDispatchUpdateOne { + if v != nil { + _u.SetOp(*v) + } + return _u +} + +// SetArgs sets the "args" field. +func (_u *BrokerDispatchUpdateOne) SetArgs(v string) *BrokerDispatchUpdateOne { + _u.mutation.SetArgs(v) + return _u +} + +// SetNillableArgs sets the "args" field if the given value is not nil. +func (_u *BrokerDispatchUpdateOne) SetNillableArgs(v *string) *BrokerDispatchUpdateOne { + if v != nil { + _u.SetArgs(*v) + } + return _u +} + +// ClearArgs clears the value of the "args" field. +func (_u *BrokerDispatchUpdateOne) ClearArgs() *BrokerDispatchUpdateOne { + _u.mutation.ClearArgs() + return _u +} + +// SetState sets the "state" field. +func (_u *BrokerDispatchUpdateOne) SetState(v string) *BrokerDispatchUpdateOne { + _u.mutation.SetState(v) + return _u +} + +// SetNillableState sets the "state" field if the given value is not nil. +func (_u *BrokerDispatchUpdateOne) SetNillableState(v *string) *BrokerDispatchUpdateOne { + if v != nil { + _u.SetState(*v) + } + return _u +} + +// SetResult sets the "result" field. +func (_u *BrokerDispatchUpdateOne) SetResult(v string) *BrokerDispatchUpdateOne { + _u.mutation.SetResult(v) + return _u +} + +// SetNillableResult sets the "result" field if the given value is not nil. +func (_u *BrokerDispatchUpdateOne) SetNillableResult(v *string) *BrokerDispatchUpdateOne { + if v != nil { + _u.SetResult(*v) + } + return _u +} + +// ClearResult clears the value of the "result" field. +func (_u *BrokerDispatchUpdateOne) ClearResult() *BrokerDispatchUpdateOne { + _u.mutation.ClearResult() + return _u +} + +// SetClaimedBy sets the "claimed_by" field. +func (_u *BrokerDispatchUpdateOne) SetClaimedBy(v string) *BrokerDispatchUpdateOne { + _u.mutation.SetClaimedBy(v) + return _u +} + +// SetNillableClaimedBy sets the "claimed_by" field if the given value is not nil. +func (_u *BrokerDispatchUpdateOne) SetNillableClaimedBy(v *string) *BrokerDispatchUpdateOne { + if v != nil { + _u.SetClaimedBy(*v) + } + return _u +} + +// ClearClaimedBy clears the value of the "claimed_by" field. +func (_u *BrokerDispatchUpdateOne) ClearClaimedBy() *BrokerDispatchUpdateOne { + _u.mutation.ClearClaimedBy() + return _u +} + +// SetAttempts sets the "attempts" field. +func (_u *BrokerDispatchUpdateOne) SetAttempts(v int) *BrokerDispatchUpdateOne { + _u.mutation.ResetAttempts() + _u.mutation.SetAttempts(v) + return _u +} + +// SetNillableAttempts sets the "attempts" field if the given value is not nil. +func (_u *BrokerDispatchUpdateOne) SetNillableAttempts(v *int) *BrokerDispatchUpdateOne { + if v != nil { + _u.SetAttempts(*v) + } + return _u +} + +// AddAttempts adds value to the "attempts" field. +func (_u *BrokerDispatchUpdateOne) AddAttempts(v int) *BrokerDispatchUpdateOne { + _u.mutation.AddAttempts(v) + return _u +} + +// SetError sets the "error" field. +func (_u *BrokerDispatchUpdateOne) SetError(v string) *BrokerDispatchUpdateOne { + _u.mutation.SetError(v) + return _u +} + +// SetNillableError sets the "error" field if the given value is not nil. +func (_u *BrokerDispatchUpdateOne) SetNillableError(v *string) *BrokerDispatchUpdateOne { + if v != nil { + _u.SetError(*v) + } + return _u +} + +// ClearError clears the value of the "error" field. +func (_u *BrokerDispatchUpdateOne) ClearError() *BrokerDispatchUpdateOne { + _u.mutation.ClearError() + return _u +} + +// SetUpdatedAt sets the "updated_at" field. +func (_u *BrokerDispatchUpdateOne) SetUpdatedAt(v time.Time) *BrokerDispatchUpdateOne { + _u.mutation.SetUpdatedAt(v) + return _u +} + +// SetDeadlineAt sets the "deadline_at" field. +func (_u *BrokerDispatchUpdateOne) SetDeadlineAt(v time.Time) *BrokerDispatchUpdateOne { + _u.mutation.SetDeadlineAt(v) + return _u +} + +// SetNillableDeadlineAt sets the "deadline_at" field if the given value is not nil. +func (_u *BrokerDispatchUpdateOne) SetNillableDeadlineAt(v *time.Time) *BrokerDispatchUpdateOne { + if v != nil { + _u.SetDeadlineAt(*v) + } + return _u +} + +// ClearDeadlineAt clears the value of the "deadline_at" field. +func (_u *BrokerDispatchUpdateOne) ClearDeadlineAt() *BrokerDispatchUpdateOne { + _u.mutation.ClearDeadlineAt() + return _u +} + +// Mutation returns the BrokerDispatchMutation object of the builder. +func (_u *BrokerDispatchUpdateOne) Mutation() *BrokerDispatchMutation { + return _u.mutation +} + +// Where appends a list predicates to the BrokerDispatchUpdate builder. +func (_u *BrokerDispatchUpdateOne) Where(ps ...predicate.BrokerDispatch) *BrokerDispatchUpdateOne { + _u.mutation.Where(ps...) + return _u +} + +// Select allows selecting one or more fields (columns) of the returned entity. +// The default is selecting all fields defined in the entity schema. +func (_u *BrokerDispatchUpdateOne) Select(field string, fields ...string) *BrokerDispatchUpdateOne { + _u.fields = append([]string{field}, fields...) + return _u +} + +// Save executes the query and returns the updated BrokerDispatch entity. +func (_u *BrokerDispatchUpdateOne) Save(ctx context.Context) (*BrokerDispatch, error) { + _u.defaults() + return withHooks(ctx, _u.sqlSave, _u.mutation, _u.hooks) +} + +// SaveX is like Save, but panics if an error occurs. +func (_u *BrokerDispatchUpdateOne) SaveX(ctx context.Context) *BrokerDispatch { + node, err := _u.Save(ctx) + if err != nil { + panic(err) + } + return node +} + +// Exec executes the query on the entity. +func (_u *BrokerDispatchUpdateOne) Exec(ctx context.Context) error { + _, err := _u.Save(ctx) + return err +} + +// ExecX is like Exec, but panics if an error occurs. +func (_u *BrokerDispatchUpdateOne) ExecX(ctx context.Context) { + if err := _u.Exec(ctx); err != nil { + panic(err) + } +} + +// defaults sets the default values of the builder before save. +func (_u *BrokerDispatchUpdateOne) defaults() { + if _, ok := _u.mutation.UpdatedAt(); !ok { + v := brokerdispatch.UpdateDefaultUpdatedAt() + _u.mutation.SetUpdatedAt(v) + } +} + +// check runs all checks and user-defined validators on the builder. +func (_u *BrokerDispatchUpdateOne) check() error { + if v, ok := _u.mutation.GetOp(); ok { + if err := brokerdispatch.OpValidator(v); err != nil { + return &ValidationError{Name: "op", err: fmt.Errorf(`ent: validator failed for field "BrokerDispatch.op": %w`, err)} + } + } + return nil +} + +func (_u *BrokerDispatchUpdateOne) sqlSave(ctx context.Context) (_node *BrokerDispatch, err error) { + if err := _u.check(); err != nil { + return _node, err + } + _spec := sqlgraph.NewUpdateSpec(brokerdispatch.Table, brokerdispatch.Columns, sqlgraph.NewFieldSpec(brokerdispatch.FieldID, field.TypeUUID)) + id, ok := _u.mutation.ID() + if !ok { + return nil, &ValidationError{Name: "id", err: errors.New(`ent: missing "BrokerDispatch.id" for update`)} + } + _spec.Node.ID.Value = id + if fields := _u.fields; len(fields) > 0 { + _spec.Node.Columns = make([]string, 0, len(fields)) + _spec.Node.Columns = append(_spec.Node.Columns, brokerdispatch.FieldID) + for _, f := range fields { + if !brokerdispatch.ValidColumn(f) { + return nil, &ValidationError{Name: f, err: fmt.Errorf("ent: invalid field %q for query", f)} + } + if f != brokerdispatch.FieldID { + _spec.Node.Columns = append(_spec.Node.Columns, f) + } + } + } + if ps := _u.mutation.predicates; len(ps) > 0 { + _spec.Predicate = func(selector *sql.Selector) { + for i := range ps { + ps[i](selector) + } + } + } + if value, ok := _u.mutation.BrokerID(); ok { + _spec.SetField(brokerdispatch.FieldBrokerID, field.TypeUUID, value) + } + if value, ok := _u.mutation.AgentID(); ok { + _spec.SetField(brokerdispatch.FieldAgentID, field.TypeUUID, value) + } + if _u.mutation.AgentIDCleared() { + _spec.ClearField(brokerdispatch.FieldAgentID, field.TypeUUID) + } + if value, ok := _u.mutation.AgentSlug(); ok { + _spec.SetField(brokerdispatch.FieldAgentSlug, field.TypeString, value) + } + if _u.mutation.AgentSlugCleared() { + _spec.ClearField(brokerdispatch.FieldAgentSlug, field.TypeString) + } + if value, ok := _u.mutation.ProjectID(); ok { + _spec.SetField(brokerdispatch.FieldProjectID, field.TypeUUID, value) + } + if _u.mutation.ProjectIDCleared() { + _spec.ClearField(brokerdispatch.FieldProjectID, field.TypeUUID) + } + if value, ok := _u.mutation.GetOp(); ok { + _spec.SetField(brokerdispatch.FieldOp, field.TypeString, value) + } + if value, ok := _u.mutation.Args(); ok { + _spec.SetField(brokerdispatch.FieldArgs, field.TypeString, value) + } + if _u.mutation.ArgsCleared() { + _spec.ClearField(brokerdispatch.FieldArgs, field.TypeString) + } + if value, ok := _u.mutation.State(); ok { + _spec.SetField(brokerdispatch.FieldState, field.TypeString, value) + } + if value, ok := _u.mutation.Result(); ok { + _spec.SetField(brokerdispatch.FieldResult, field.TypeString, value) + } + if _u.mutation.ResultCleared() { + _spec.ClearField(brokerdispatch.FieldResult, field.TypeString) + } + if value, ok := _u.mutation.ClaimedBy(); ok { + _spec.SetField(brokerdispatch.FieldClaimedBy, field.TypeString, value) + } + if _u.mutation.ClaimedByCleared() { + _spec.ClearField(brokerdispatch.FieldClaimedBy, field.TypeString) + } + if value, ok := _u.mutation.Attempts(); ok { + _spec.SetField(brokerdispatch.FieldAttempts, field.TypeInt, value) + } + if value, ok := _u.mutation.AddedAttempts(); ok { + _spec.AddField(brokerdispatch.FieldAttempts, field.TypeInt, value) + } + if value, ok := _u.mutation.Error(); ok { + _spec.SetField(brokerdispatch.FieldError, field.TypeString, value) + } + if _u.mutation.ErrorCleared() { + _spec.ClearField(brokerdispatch.FieldError, field.TypeString) + } + if value, ok := _u.mutation.UpdatedAt(); ok { + _spec.SetField(brokerdispatch.FieldUpdatedAt, field.TypeTime, value) + } + if value, ok := _u.mutation.DeadlineAt(); ok { + _spec.SetField(brokerdispatch.FieldDeadlineAt, field.TypeTime, value) + } + if _u.mutation.DeadlineAtCleared() { + _spec.ClearField(brokerdispatch.FieldDeadlineAt, field.TypeTime) + } + _node = &BrokerDispatch{config: _u.config} + _spec.Assign = _node.assignValues + _spec.ScanValues = _node.scanValues + if err = sqlgraph.UpdateNode(ctx, _u.driver, _spec); err != nil { + if _, ok := err.(*sqlgraph.NotFoundError); ok { + err = &NotFoundError{brokerdispatch.Label} + } else if sqlgraph.IsConstraintError(err) { + err = &ConstraintError{msg: err.Error(), wrap: err} + } + return nil, err + } + _u.mutation.done = true + return _node, nil +} diff --git a/pkg/ent/brokerjointoken.go b/pkg/ent/brokerjointoken.go new file mode 100644 index 000000000..a13f5c1c7 --- /dev/null +++ b/pkg/ent/brokerjointoken.go @@ -0,0 +1,140 @@ +// Code generated by ent, DO NOT EDIT. + +package ent + +import ( + "fmt" + "strings" + "time" + + "entgo.io/ent" + "entgo.io/ent/dialect/sql" + "github.com/GoogleCloudPlatform/scion/pkg/ent/brokerjointoken" + "github.com/google/uuid" +) + +// BrokerJoinToken is the model entity for the BrokerJoinToken schema. +type BrokerJoinToken struct { + config `json:"-"` + // ID of the ent. + ID uuid.UUID `json:"id,omitempty"` + // TokenHash holds the value of the "token_hash" field. + TokenHash string `json:"token_hash,omitempty"` + // ExpiresAt holds the value of the "expires_at" field. + ExpiresAt time.Time `json:"expires_at,omitempty"` + // CreatedBy holds the value of the "created_by" field. + CreatedBy string `json:"created_by,omitempty"` + // Created holds the value of the "created" field. + Created time.Time `json:"created,omitempty"` + selectValues sql.SelectValues +} + +// scanValues returns the types for scanning values from sql.Rows. +func (*BrokerJoinToken) scanValues(columns []string) ([]any, error) { + values := make([]any, len(columns)) + for i := range columns { + switch columns[i] { + case brokerjointoken.FieldTokenHash, brokerjointoken.FieldCreatedBy: + values[i] = new(sql.NullString) + case brokerjointoken.FieldExpiresAt, brokerjointoken.FieldCreated: + values[i] = new(sql.NullTime) + case brokerjointoken.FieldID: + values[i] = new(uuid.UUID) + default: + values[i] = new(sql.UnknownType) + } + } + return values, nil +} + +// assignValues assigns the values that were returned from sql.Rows (after scanning) +// to the BrokerJoinToken fields. +func (_m *BrokerJoinToken) assignValues(columns []string, values []any) error { + if m, n := len(values), len(columns); m < n { + return fmt.Errorf("mismatch number of scan values: %d != %d", m, n) + } + for i := range columns { + switch columns[i] { + case brokerjointoken.FieldID: + if value, ok := values[i].(*uuid.UUID); !ok { + return fmt.Errorf("unexpected type %T for field id", values[i]) + } else if value != nil { + _m.ID = *value + } + case brokerjointoken.FieldTokenHash: + if value, ok := values[i].(*sql.NullString); !ok { + return fmt.Errorf("unexpected type %T for field token_hash", values[i]) + } else if value.Valid { + _m.TokenHash = value.String + } + case brokerjointoken.FieldExpiresAt: + if value, ok := values[i].(*sql.NullTime); !ok { + return fmt.Errorf("unexpected type %T for field expires_at", values[i]) + } else if value.Valid { + _m.ExpiresAt = value.Time + } + case brokerjointoken.FieldCreatedBy: + if value, ok := values[i].(*sql.NullString); !ok { + return fmt.Errorf("unexpected type %T for field created_by", values[i]) + } else if value.Valid { + _m.CreatedBy = value.String + } + case brokerjointoken.FieldCreated: + if value, ok := values[i].(*sql.NullTime); !ok { + return fmt.Errorf("unexpected type %T for field created", values[i]) + } else if value.Valid { + _m.Created = value.Time + } + default: + _m.selectValues.Set(columns[i], values[i]) + } + } + return nil +} + +// Value returns the ent.Value that was dynamically selected and assigned to the BrokerJoinToken. +// This includes values selected through modifiers, order, etc. +func (_m *BrokerJoinToken) Value(name string) (ent.Value, error) { + return _m.selectValues.Get(name) +} + +// Update returns a builder for updating this BrokerJoinToken. +// Note that you need to call BrokerJoinToken.Unwrap() before calling this method if this BrokerJoinToken +// was returned from a transaction, and the transaction was committed or rolled back. +func (_m *BrokerJoinToken) Update() *BrokerJoinTokenUpdateOne { + return NewBrokerJoinTokenClient(_m.config).UpdateOne(_m) +} + +// Unwrap unwraps the BrokerJoinToken entity that was returned from a transaction after it was closed, +// so that all future queries will be executed through the driver which created the transaction. +func (_m *BrokerJoinToken) Unwrap() *BrokerJoinToken { + _tx, ok := _m.config.driver.(*txDriver) + if !ok { + panic("ent: BrokerJoinToken is not a transactional entity") + } + _m.config.driver = _tx.drv + return _m +} + +// String implements the fmt.Stringer. +func (_m *BrokerJoinToken) String() string { + var builder strings.Builder + builder.WriteString("BrokerJoinToken(") + builder.WriteString(fmt.Sprintf("id=%v, ", _m.ID)) + builder.WriteString("token_hash=") + builder.WriteString(_m.TokenHash) + builder.WriteString(", ") + builder.WriteString("expires_at=") + builder.WriteString(_m.ExpiresAt.Format(time.ANSIC)) + builder.WriteString(", ") + builder.WriteString("created_by=") + builder.WriteString(_m.CreatedBy) + builder.WriteString(", ") + builder.WriteString("created=") + builder.WriteString(_m.Created.Format(time.ANSIC)) + builder.WriteByte(')') + return builder.String() +} + +// BrokerJoinTokens is a parsable slice of BrokerJoinToken. +type BrokerJoinTokens []*BrokerJoinToken diff --git a/pkg/ent/brokerjointoken/brokerjointoken.go b/pkg/ent/brokerjointoken/brokerjointoken.go new file mode 100644 index 000000000..64e1a4b38 --- /dev/null +++ b/pkg/ent/brokerjointoken/brokerjointoken.go @@ -0,0 +1,82 @@ +// Code generated by ent, DO NOT EDIT. + +package brokerjointoken + +import ( + "time" + + "entgo.io/ent/dialect/sql" +) + +const ( + // Label holds the string label denoting the brokerjointoken type in the database. + Label = "broker_join_token" + // FieldID holds the string denoting the id field in the database. + FieldID = "broker_id" + // FieldTokenHash holds the string denoting the token_hash field in the database. + FieldTokenHash = "token_hash" + // FieldExpiresAt holds the string denoting the expires_at field in the database. + FieldExpiresAt = "expires_at" + // FieldCreatedBy holds the string denoting the created_by field in the database. + FieldCreatedBy = "created_by" + // FieldCreated holds the string denoting the created field in the database. + FieldCreated = "created" + // Table holds the table name of the brokerjointoken in the database. + Table = "broker_join_tokens" +) + +// Columns holds all SQL columns for brokerjointoken fields. +var Columns = []string{ + FieldID, + FieldTokenHash, + FieldExpiresAt, + FieldCreatedBy, + FieldCreated, +} + +// ValidColumn reports if the column name is valid (part of the table columns). +func ValidColumn(column string) bool { + for i := range Columns { + if column == Columns[i] { + return true + } + } + return false +} + +var ( + // TokenHashValidator is a validator for the "token_hash" field. It is called by the builders before save. + TokenHashValidator func(string) error + // CreatedByValidator is a validator for the "created_by" field. It is called by the builders before save. + CreatedByValidator func(string) error + // DefaultCreated holds the default value on creation for the "created" field. + DefaultCreated func() time.Time +) + +// OrderOption defines the ordering options for the BrokerJoinToken queries. +type OrderOption func(*sql.Selector) + +// ByID orders the results by the id field. +func ByID(opts ...sql.OrderTermOption) OrderOption { + return sql.OrderByField(FieldID, opts...).ToFunc() +} + +// ByTokenHash orders the results by the token_hash field. +func ByTokenHash(opts ...sql.OrderTermOption) OrderOption { + return sql.OrderByField(FieldTokenHash, opts...).ToFunc() +} + +// ByExpiresAt orders the results by the expires_at field. +func ByExpiresAt(opts ...sql.OrderTermOption) OrderOption { + return sql.OrderByField(FieldExpiresAt, opts...).ToFunc() +} + +// ByCreatedBy orders the results by the created_by field. +func ByCreatedBy(opts ...sql.OrderTermOption) OrderOption { + return sql.OrderByField(FieldCreatedBy, opts...).ToFunc() +} + +// ByCreated orders the results by the created field. +func ByCreated(opts ...sql.OrderTermOption) OrderOption { + return sql.OrderByField(FieldCreated, opts...).ToFunc() +} diff --git a/pkg/ent/brokerjointoken/where.go b/pkg/ent/brokerjointoken/where.go new file mode 100644 index 000000000..31747db81 --- /dev/null +++ b/pkg/ent/brokerjointoken/where.go @@ -0,0 +1,301 @@ +// Code generated by ent, DO NOT EDIT. + +package brokerjointoken + +import ( + "time" + + "entgo.io/ent/dialect/sql" + "github.com/GoogleCloudPlatform/scion/pkg/ent/predicate" + "github.com/google/uuid" +) + +// ID filters vertices based on their ID field. +func ID(id uuid.UUID) predicate.BrokerJoinToken { + return predicate.BrokerJoinToken(sql.FieldEQ(FieldID, id)) +} + +// IDEQ applies the EQ predicate on the ID field. +func IDEQ(id uuid.UUID) predicate.BrokerJoinToken { + return predicate.BrokerJoinToken(sql.FieldEQ(FieldID, id)) +} + +// IDNEQ applies the NEQ predicate on the ID field. +func IDNEQ(id uuid.UUID) predicate.BrokerJoinToken { + return predicate.BrokerJoinToken(sql.FieldNEQ(FieldID, id)) +} + +// IDIn applies the In predicate on the ID field. +func IDIn(ids ...uuid.UUID) predicate.BrokerJoinToken { + return predicate.BrokerJoinToken(sql.FieldIn(FieldID, ids...)) +} + +// IDNotIn applies the NotIn predicate on the ID field. +func IDNotIn(ids ...uuid.UUID) predicate.BrokerJoinToken { + return predicate.BrokerJoinToken(sql.FieldNotIn(FieldID, ids...)) +} + +// IDGT applies the GT predicate on the ID field. +func IDGT(id uuid.UUID) predicate.BrokerJoinToken { + return predicate.BrokerJoinToken(sql.FieldGT(FieldID, id)) +} + +// IDGTE applies the GTE predicate on the ID field. +func IDGTE(id uuid.UUID) predicate.BrokerJoinToken { + return predicate.BrokerJoinToken(sql.FieldGTE(FieldID, id)) +} + +// IDLT applies the LT predicate on the ID field. +func IDLT(id uuid.UUID) predicate.BrokerJoinToken { + return predicate.BrokerJoinToken(sql.FieldLT(FieldID, id)) +} + +// IDLTE applies the LTE predicate on the ID field. +func IDLTE(id uuid.UUID) predicate.BrokerJoinToken { + return predicate.BrokerJoinToken(sql.FieldLTE(FieldID, id)) +} + +// TokenHash applies equality check predicate on the "token_hash" field. It's identical to TokenHashEQ. +func TokenHash(v string) predicate.BrokerJoinToken { + return predicate.BrokerJoinToken(sql.FieldEQ(FieldTokenHash, v)) +} + +// ExpiresAt applies equality check predicate on the "expires_at" field. It's identical to ExpiresAtEQ. +func ExpiresAt(v time.Time) predicate.BrokerJoinToken { + return predicate.BrokerJoinToken(sql.FieldEQ(FieldExpiresAt, v)) +} + +// CreatedBy applies equality check predicate on the "created_by" field. It's identical to CreatedByEQ. +func CreatedBy(v string) predicate.BrokerJoinToken { + return predicate.BrokerJoinToken(sql.FieldEQ(FieldCreatedBy, v)) +} + +// Created applies equality check predicate on the "created" field. It's identical to CreatedEQ. +func Created(v time.Time) predicate.BrokerJoinToken { + return predicate.BrokerJoinToken(sql.FieldEQ(FieldCreated, v)) +} + +// TokenHashEQ applies the EQ predicate on the "token_hash" field. +func TokenHashEQ(v string) predicate.BrokerJoinToken { + return predicate.BrokerJoinToken(sql.FieldEQ(FieldTokenHash, v)) +} + +// TokenHashNEQ applies the NEQ predicate on the "token_hash" field. +func TokenHashNEQ(v string) predicate.BrokerJoinToken { + return predicate.BrokerJoinToken(sql.FieldNEQ(FieldTokenHash, v)) +} + +// TokenHashIn applies the In predicate on the "token_hash" field. +func TokenHashIn(vs ...string) predicate.BrokerJoinToken { + return predicate.BrokerJoinToken(sql.FieldIn(FieldTokenHash, vs...)) +} + +// TokenHashNotIn applies the NotIn predicate on the "token_hash" field. +func TokenHashNotIn(vs ...string) predicate.BrokerJoinToken { + return predicate.BrokerJoinToken(sql.FieldNotIn(FieldTokenHash, vs...)) +} + +// TokenHashGT applies the GT predicate on the "token_hash" field. +func TokenHashGT(v string) predicate.BrokerJoinToken { + return predicate.BrokerJoinToken(sql.FieldGT(FieldTokenHash, v)) +} + +// TokenHashGTE applies the GTE predicate on the "token_hash" field. +func TokenHashGTE(v string) predicate.BrokerJoinToken { + return predicate.BrokerJoinToken(sql.FieldGTE(FieldTokenHash, v)) +} + +// TokenHashLT applies the LT predicate on the "token_hash" field. +func TokenHashLT(v string) predicate.BrokerJoinToken { + return predicate.BrokerJoinToken(sql.FieldLT(FieldTokenHash, v)) +} + +// TokenHashLTE applies the LTE predicate on the "token_hash" field. +func TokenHashLTE(v string) predicate.BrokerJoinToken { + return predicate.BrokerJoinToken(sql.FieldLTE(FieldTokenHash, v)) +} + +// TokenHashContains applies the Contains predicate on the "token_hash" field. +func TokenHashContains(v string) predicate.BrokerJoinToken { + return predicate.BrokerJoinToken(sql.FieldContains(FieldTokenHash, v)) +} + +// TokenHashHasPrefix applies the HasPrefix predicate on the "token_hash" field. +func TokenHashHasPrefix(v string) predicate.BrokerJoinToken { + return predicate.BrokerJoinToken(sql.FieldHasPrefix(FieldTokenHash, v)) +} + +// TokenHashHasSuffix applies the HasSuffix predicate on the "token_hash" field. +func TokenHashHasSuffix(v string) predicate.BrokerJoinToken { + return predicate.BrokerJoinToken(sql.FieldHasSuffix(FieldTokenHash, v)) +} + +// TokenHashEqualFold applies the EqualFold predicate on the "token_hash" field. +func TokenHashEqualFold(v string) predicate.BrokerJoinToken { + return predicate.BrokerJoinToken(sql.FieldEqualFold(FieldTokenHash, v)) +} + +// TokenHashContainsFold applies the ContainsFold predicate on the "token_hash" field. +func TokenHashContainsFold(v string) predicate.BrokerJoinToken { + return predicate.BrokerJoinToken(sql.FieldContainsFold(FieldTokenHash, v)) +} + +// ExpiresAtEQ applies the EQ predicate on the "expires_at" field. +func ExpiresAtEQ(v time.Time) predicate.BrokerJoinToken { + return predicate.BrokerJoinToken(sql.FieldEQ(FieldExpiresAt, v)) +} + +// ExpiresAtNEQ applies the NEQ predicate on the "expires_at" field. +func ExpiresAtNEQ(v time.Time) predicate.BrokerJoinToken { + return predicate.BrokerJoinToken(sql.FieldNEQ(FieldExpiresAt, v)) +} + +// ExpiresAtIn applies the In predicate on the "expires_at" field. +func ExpiresAtIn(vs ...time.Time) predicate.BrokerJoinToken { + return predicate.BrokerJoinToken(sql.FieldIn(FieldExpiresAt, vs...)) +} + +// ExpiresAtNotIn applies the NotIn predicate on the "expires_at" field. +func ExpiresAtNotIn(vs ...time.Time) predicate.BrokerJoinToken { + return predicate.BrokerJoinToken(sql.FieldNotIn(FieldExpiresAt, vs...)) +} + +// ExpiresAtGT applies the GT predicate on the "expires_at" field. +func ExpiresAtGT(v time.Time) predicate.BrokerJoinToken { + return predicate.BrokerJoinToken(sql.FieldGT(FieldExpiresAt, v)) +} + +// ExpiresAtGTE applies the GTE predicate on the "expires_at" field. +func ExpiresAtGTE(v time.Time) predicate.BrokerJoinToken { + return predicate.BrokerJoinToken(sql.FieldGTE(FieldExpiresAt, v)) +} + +// ExpiresAtLT applies the LT predicate on the "expires_at" field. +func ExpiresAtLT(v time.Time) predicate.BrokerJoinToken { + return predicate.BrokerJoinToken(sql.FieldLT(FieldExpiresAt, v)) +} + +// ExpiresAtLTE applies the LTE predicate on the "expires_at" field. +func ExpiresAtLTE(v time.Time) predicate.BrokerJoinToken { + return predicate.BrokerJoinToken(sql.FieldLTE(FieldExpiresAt, v)) +} + +// CreatedByEQ applies the EQ predicate on the "created_by" field. +func CreatedByEQ(v string) predicate.BrokerJoinToken { + return predicate.BrokerJoinToken(sql.FieldEQ(FieldCreatedBy, v)) +} + +// CreatedByNEQ applies the NEQ predicate on the "created_by" field. +func CreatedByNEQ(v string) predicate.BrokerJoinToken { + return predicate.BrokerJoinToken(sql.FieldNEQ(FieldCreatedBy, v)) +} + +// CreatedByIn applies the In predicate on the "created_by" field. +func CreatedByIn(vs ...string) predicate.BrokerJoinToken { + return predicate.BrokerJoinToken(sql.FieldIn(FieldCreatedBy, vs...)) +} + +// CreatedByNotIn applies the NotIn predicate on the "created_by" field. +func CreatedByNotIn(vs ...string) predicate.BrokerJoinToken { + return predicate.BrokerJoinToken(sql.FieldNotIn(FieldCreatedBy, vs...)) +} + +// CreatedByGT applies the GT predicate on the "created_by" field. +func CreatedByGT(v string) predicate.BrokerJoinToken { + return predicate.BrokerJoinToken(sql.FieldGT(FieldCreatedBy, v)) +} + +// CreatedByGTE applies the GTE predicate on the "created_by" field. +func CreatedByGTE(v string) predicate.BrokerJoinToken { + return predicate.BrokerJoinToken(sql.FieldGTE(FieldCreatedBy, v)) +} + +// CreatedByLT applies the LT predicate on the "created_by" field. +func CreatedByLT(v string) predicate.BrokerJoinToken { + return predicate.BrokerJoinToken(sql.FieldLT(FieldCreatedBy, v)) +} + +// CreatedByLTE applies the LTE predicate on the "created_by" field. +func CreatedByLTE(v string) predicate.BrokerJoinToken { + return predicate.BrokerJoinToken(sql.FieldLTE(FieldCreatedBy, v)) +} + +// CreatedByContains applies the Contains predicate on the "created_by" field. +func CreatedByContains(v string) predicate.BrokerJoinToken { + return predicate.BrokerJoinToken(sql.FieldContains(FieldCreatedBy, v)) +} + +// CreatedByHasPrefix applies the HasPrefix predicate on the "created_by" field. +func CreatedByHasPrefix(v string) predicate.BrokerJoinToken { + return predicate.BrokerJoinToken(sql.FieldHasPrefix(FieldCreatedBy, v)) +} + +// CreatedByHasSuffix applies the HasSuffix predicate on the "created_by" field. +func CreatedByHasSuffix(v string) predicate.BrokerJoinToken { + return predicate.BrokerJoinToken(sql.FieldHasSuffix(FieldCreatedBy, v)) +} + +// CreatedByEqualFold applies the EqualFold predicate on the "created_by" field. +func CreatedByEqualFold(v string) predicate.BrokerJoinToken { + return predicate.BrokerJoinToken(sql.FieldEqualFold(FieldCreatedBy, v)) +} + +// CreatedByContainsFold applies the ContainsFold predicate on the "created_by" field. +func CreatedByContainsFold(v string) predicate.BrokerJoinToken { + return predicate.BrokerJoinToken(sql.FieldContainsFold(FieldCreatedBy, v)) +} + +// CreatedEQ applies the EQ predicate on the "created" field. +func CreatedEQ(v time.Time) predicate.BrokerJoinToken { + return predicate.BrokerJoinToken(sql.FieldEQ(FieldCreated, v)) +} + +// CreatedNEQ applies the NEQ predicate on the "created" field. +func CreatedNEQ(v time.Time) predicate.BrokerJoinToken { + return predicate.BrokerJoinToken(sql.FieldNEQ(FieldCreated, v)) +} + +// CreatedIn applies the In predicate on the "created" field. +func CreatedIn(vs ...time.Time) predicate.BrokerJoinToken { + return predicate.BrokerJoinToken(sql.FieldIn(FieldCreated, vs...)) +} + +// CreatedNotIn applies the NotIn predicate on the "created" field. +func CreatedNotIn(vs ...time.Time) predicate.BrokerJoinToken { + return predicate.BrokerJoinToken(sql.FieldNotIn(FieldCreated, vs...)) +} + +// CreatedGT applies the GT predicate on the "created" field. +func CreatedGT(v time.Time) predicate.BrokerJoinToken { + return predicate.BrokerJoinToken(sql.FieldGT(FieldCreated, v)) +} + +// CreatedGTE applies the GTE predicate on the "created" field. +func CreatedGTE(v time.Time) predicate.BrokerJoinToken { + return predicate.BrokerJoinToken(sql.FieldGTE(FieldCreated, v)) +} + +// CreatedLT applies the LT predicate on the "created" field. +func CreatedLT(v time.Time) predicate.BrokerJoinToken { + return predicate.BrokerJoinToken(sql.FieldLT(FieldCreated, v)) +} + +// CreatedLTE applies the LTE predicate on the "created" field. +func CreatedLTE(v time.Time) predicate.BrokerJoinToken { + return predicate.BrokerJoinToken(sql.FieldLTE(FieldCreated, v)) +} + +// And groups predicates with the AND operator between them. +func And(predicates ...predicate.BrokerJoinToken) predicate.BrokerJoinToken { + return predicate.BrokerJoinToken(sql.AndPredicates(predicates...)) +} + +// Or groups predicates with the OR operator between them. +func Or(predicates ...predicate.BrokerJoinToken) predicate.BrokerJoinToken { + return predicate.BrokerJoinToken(sql.OrPredicates(predicates...)) +} + +// Not applies the not operator on the given predicate. +func Not(p predicate.BrokerJoinToken) predicate.BrokerJoinToken { + return predicate.BrokerJoinToken(sql.NotPredicates(p)) +} diff --git a/pkg/ent/brokerjointoken_create.go b/pkg/ent/brokerjointoken_create.go new file mode 100644 index 000000000..c3f29f6df --- /dev/null +++ b/pkg/ent/brokerjointoken_create.go @@ -0,0 +1,644 @@ +// Code generated by ent, DO NOT EDIT. + +package ent + +import ( + "context" + "errors" + "fmt" + "time" + + "entgo.io/ent/dialect" + "entgo.io/ent/dialect/sql" + "entgo.io/ent/dialect/sql/sqlgraph" + "entgo.io/ent/schema/field" + "github.com/GoogleCloudPlatform/scion/pkg/ent/brokerjointoken" + "github.com/google/uuid" +) + +// BrokerJoinTokenCreate is the builder for creating a BrokerJoinToken entity. +type BrokerJoinTokenCreate struct { + config + mutation *BrokerJoinTokenMutation + hooks []Hook + conflict []sql.ConflictOption +} + +// SetTokenHash sets the "token_hash" field. +func (_c *BrokerJoinTokenCreate) SetTokenHash(v string) *BrokerJoinTokenCreate { + _c.mutation.SetTokenHash(v) + return _c +} + +// SetExpiresAt sets the "expires_at" field. +func (_c *BrokerJoinTokenCreate) SetExpiresAt(v time.Time) *BrokerJoinTokenCreate { + _c.mutation.SetExpiresAt(v) + return _c +} + +// SetCreatedBy sets the "created_by" field. +func (_c *BrokerJoinTokenCreate) SetCreatedBy(v string) *BrokerJoinTokenCreate { + _c.mutation.SetCreatedBy(v) + return _c +} + +// SetCreated sets the "created" field. +func (_c *BrokerJoinTokenCreate) SetCreated(v time.Time) *BrokerJoinTokenCreate { + _c.mutation.SetCreated(v) + return _c +} + +// SetNillableCreated sets the "created" field if the given value is not nil. +func (_c *BrokerJoinTokenCreate) SetNillableCreated(v *time.Time) *BrokerJoinTokenCreate { + if v != nil { + _c.SetCreated(*v) + } + return _c +} + +// SetID sets the "id" field. +func (_c *BrokerJoinTokenCreate) SetID(v uuid.UUID) *BrokerJoinTokenCreate { + _c.mutation.SetID(v) + return _c +} + +// Mutation returns the BrokerJoinTokenMutation object of the builder. +func (_c *BrokerJoinTokenCreate) Mutation() *BrokerJoinTokenMutation { + return _c.mutation +} + +// Save creates the BrokerJoinToken in the database. +func (_c *BrokerJoinTokenCreate) Save(ctx context.Context) (*BrokerJoinToken, error) { + _c.defaults() + return withHooks(ctx, _c.sqlSave, _c.mutation, _c.hooks) +} + +// SaveX calls Save and panics if Save returns an error. +func (_c *BrokerJoinTokenCreate) SaveX(ctx context.Context) *BrokerJoinToken { + v, err := _c.Save(ctx) + if err != nil { + panic(err) + } + return v +} + +// Exec executes the query. +func (_c *BrokerJoinTokenCreate) Exec(ctx context.Context) error { + _, err := _c.Save(ctx) + return err +} + +// ExecX is like Exec, but panics if an error occurs. +func (_c *BrokerJoinTokenCreate) ExecX(ctx context.Context) { + if err := _c.Exec(ctx); err != nil { + panic(err) + } +} + +// defaults sets the default values of the builder before save. +func (_c *BrokerJoinTokenCreate) defaults() { + if _, ok := _c.mutation.Created(); !ok { + v := brokerjointoken.DefaultCreated() + _c.mutation.SetCreated(v) + } +} + +// check runs all checks and user-defined validators on the builder. +func (_c *BrokerJoinTokenCreate) check() error { + if _, ok := _c.mutation.TokenHash(); !ok { + return &ValidationError{Name: "token_hash", err: errors.New(`ent: missing required field "BrokerJoinToken.token_hash"`)} + } + if v, ok := _c.mutation.TokenHash(); ok { + if err := brokerjointoken.TokenHashValidator(v); err != nil { + return &ValidationError{Name: "token_hash", err: fmt.Errorf(`ent: validator failed for field "BrokerJoinToken.token_hash": %w`, err)} + } + } + if _, ok := _c.mutation.ExpiresAt(); !ok { + return &ValidationError{Name: "expires_at", err: errors.New(`ent: missing required field "BrokerJoinToken.expires_at"`)} + } + if _, ok := _c.mutation.CreatedBy(); !ok { + return &ValidationError{Name: "created_by", err: errors.New(`ent: missing required field "BrokerJoinToken.created_by"`)} + } + if v, ok := _c.mutation.CreatedBy(); ok { + if err := brokerjointoken.CreatedByValidator(v); err != nil { + return &ValidationError{Name: "created_by", err: fmt.Errorf(`ent: validator failed for field "BrokerJoinToken.created_by": %w`, err)} + } + } + if _, ok := _c.mutation.Created(); !ok { + return &ValidationError{Name: "created", err: errors.New(`ent: missing required field "BrokerJoinToken.created"`)} + } + return nil +} + +func (_c *BrokerJoinTokenCreate) sqlSave(ctx context.Context) (*BrokerJoinToken, error) { + if err := _c.check(); err != nil { + return nil, err + } + _node, _spec := _c.createSpec() + if err := sqlgraph.CreateNode(ctx, _c.driver, _spec); err != nil { + if sqlgraph.IsConstraintError(err) { + err = &ConstraintError{msg: err.Error(), wrap: err} + } + return nil, err + } + if _spec.ID.Value != nil { + if id, ok := _spec.ID.Value.(*uuid.UUID); ok { + _node.ID = *id + } else if err := _node.ID.Scan(_spec.ID.Value); err != nil { + return nil, err + } + } + _c.mutation.id = &_node.ID + _c.mutation.done = true + return _node, nil +} + +func (_c *BrokerJoinTokenCreate) createSpec() (*BrokerJoinToken, *sqlgraph.CreateSpec) { + var ( + _node = &BrokerJoinToken{config: _c.config} + _spec = sqlgraph.NewCreateSpec(brokerjointoken.Table, sqlgraph.NewFieldSpec(brokerjointoken.FieldID, field.TypeUUID)) + ) + _spec.OnConflict = _c.conflict + if id, ok := _c.mutation.ID(); ok { + _node.ID = id + _spec.ID.Value = &id + } + if value, ok := _c.mutation.TokenHash(); ok { + _spec.SetField(brokerjointoken.FieldTokenHash, field.TypeString, value) + _node.TokenHash = value + } + if value, ok := _c.mutation.ExpiresAt(); ok { + _spec.SetField(brokerjointoken.FieldExpiresAt, field.TypeTime, value) + _node.ExpiresAt = value + } + if value, ok := _c.mutation.CreatedBy(); ok { + _spec.SetField(brokerjointoken.FieldCreatedBy, field.TypeString, value) + _node.CreatedBy = value + } + if value, ok := _c.mutation.Created(); ok { + _spec.SetField(brokerjointoken.FieldCreated, field.TypeTime, value) + _node.Created = value + } + return _node, _spec +} + +// OnConflict allows configuring the `ON CONFLICT` / `ON DUPLICATE KEY` clause +// of the `INSERT` statement. For example: +// +// client.BrokerJoinToken.Create(). +// SetTokenHash(v). +// OnConflict( +// // Update the row with the new values +// // the was proposed for insertion. +// sql.ResolveWithNewValues(), +// ). +// // Override some of the fields with custom +// // update values. +// Update(func(u *ent.BrokerJoinTokenUpsert) { +// SetTokenHash(v+v). +// }). +// Exec(ctx) +func (_c *BrokerJoinTokenCreate) OnConflict(opts ...sql.ConflictOption) *BrokerJoinTokenUpsertOne { + _c.conflict = opts + return &BrokerJoinTokenUpsertOne{ + create: _c, + } +} + +// OnConflictColumns calls `OnConflict` and configures the columns +// as conflict target. Using this option is equivalent to using: +// +// client.BrokerJoinToken.Create(). +// OnConflict(sql.ConflictColumns(columns...)). +// Exec(ctx) +func (_c *BrokerJoinTokenCreate) OnConflictColumns(columns ...string) *BrokerJoinTokenUpsertOne { + _c.conflict = append(_c.conflict, sql.ConflictColumns(columns...)) + return &BrokerJoinTokenUpsertOne{ + create: _c, + } +} + +type ( + // BrokerJoinTokenUpsertOne is the builder for "upsert"-ing + // one BrokerJoinToken node. + BrokerJoinTokenUpsertOne struct { + create *BrokerJoinTokenCreate + } + + // BrokerJoinTokenUpsert is the "OnConflict" setter. + BrokerJoinTokenUpsert struct { + *sql.UpdateSet + } +) + +// SetTokenHash sets the "token_hash" field. +func (u *BrokerJoinTokenUpsert) SetTokenHash(v string) *BrokerJoinTokenUpsert { + u.Set(brokerjointoken.FieldTokenHash, v) + return u +} + +// UpdateTokenHash sets the "token_hash" field to the value that was provided on create. +func (u *BrokerJoinTokenUpsert) UpdateTokenHash() *BrokerJoinTokenUpsert { + u.SetExcluded(brokerjointoken.FieldTokenHash) + return u +} + +// SetExpiresAt sets the "expires_at" field. +func (u *BrokerJoinTokenUpsert) SetExpiresAt(v time.Time) *BrokerJoinTokenUpsert { + u.Set(brokerjointoken.FieldExpiresAt, v) + return u +} + +// UpdateExpiresAt sets the "expires_at" field to the value that was provided on create. +func (u *BrokerJoinTokenUpsert) UpdateExpiresAt() *BrokerJoinTokenUpsert { + u.SetExcluded(brokerjointoken.FieldExpiresAt) + return u +} + +// SetCreatedBy sets the "created_by" field. +func (u *BrokerJoinTokenUpsert) SetCreatedBy(v string) *BrokerJoinTokenUpsert { + u.Set(brokerjointoken.FieldCreatedBy, v) + return u +} + +// UpdateCreatedBy sets the "created_by" field to the value that was provided on create. +func (u *BrokerJoinTokenUpsert) UpdateCreatedBy() *BrokerJoinTokenUpsert { + u.SetExcluded(brokerjointoken.FieldCreatedBy) + return u +} + +// UpdateNewValues updates the mutable fields using the new values that were set on create except the ID field. +// Using this option is equivalent to using: +// +// client.BrokerJoinToken.Create(). +// OnConflict( +// sql.ResolveWithNewValues(), +// sql.ResolveWith(func(u *sql.UpdateSet) { +// u.SetIgnore(brokerjointoken.FieldID) +// }), +// ). +// Exec(ctx) +func (u *BrokerJoinTokenUpsertOne) UpdateNewValues() *BrokerJoinTokenUpsertOne { + u.create.conflict = append(u.create.conflict, sql.ResolveWithNewValues()) + u.create.conflict = append(u.create.conflict, sql.ResolveWith(func(s *sql.UpdateSet) { + if _, exists := u.create.mutation.ID(); exists { + s.SetIgnore(brokerjointoken.FieldID) + } + if _, exists := u.create.mutation.Created(); exists { + s.SetIgnore(brokerjointoken.FieldCreated) + } + })) + return u +} + +// Ignore sets each column to itself in case of conflict. +// Using this option is equivalent to using: +// +// client.BrokerJoinToken.Create(). +// OnConflict(sql.ResolveWithIgnore()). +// Exec(ctx) +func (u *BrokerJoinTokenUpsertOne) Ignore() *BrokerJoinTokenUpsertOne { + u.create.conflict = append(u.create.conflict, sql.ResolveWithIgnore()) + return u +} + +// DoNothing configures the conflict_action to `DO NOTHING`. +// Supported only by SQLite and PostgreSQL. +func (u *BrokerJoinTokenUpsertOne) DoNothing() *BrokerJoinTokenUpsertOne { + u.create.conflict = append(u.create.conflict, sql.DoNothing()) + return u +} + +// Update allows overriding fields `UPDATE` values. See the BrokerJoinTokenCreate.OnConflict +// documentation for more info. +func (u *BrokerJoinTokenUpsertOne) Update(set func(*BrokerJoinTokenUpsert)) *BrokerJoinTokenUpsertOne { + u.create.conflict = append(u.create.conflict, sql.ResolveWith(func(update *sql.UpdateSet) { + set(&BrokerJoinTokenUpsert{UpdateSet: update}) + })) + return u +} + +// SetTokenHash sets the "token_hash" field. +func (u *BrokerJoinTokenUpsertOne) SetTokenHash(v string) *BrokerJoinTokenUpsertOne { + return u.Update(func(s *BrokerJoinTokenUpsert) { + s.SetTokenHash(v) + }) +} + +// UpdateTokenHash sets the "token_hash" field to the value that was provided on create. +func (u *BrokerJoinTokenUpsertOne) UpdateTokenHash() *BrokerJoinTokenUpsertOne { + return u.Update(func(s *BrokerJoinTokenUpsert) { + s.UpdateTokenHash() + }) +} + +// SetExpiresAt sets the "expires_at" field. +func (u *BrokerJoinTokenUpsertOne) SetExpiresAt(v time.Time) *BrokerJoinTokenUpsertOne { + return u.Update(func(s *BrokerJoinTokenUpsert) { + s.SetExpiresAt(v) + }) +} + +// UpdateExpiresAt sets the "expires_at" field to the value that was provided on create. +func (u *BrokerJoinTokenUpsertOne) UpdateExpiresAt() *BrokerJoinTokenUpsertOne { + return u.Update(func(s *BrokerJoinTokenUpsert) { + s.UpdateExpiresAt() + }) +} + +// SetCreatedBy sets the "created_by" field. +func (u *BrokerJoinTokenUpsertOne) SetCreatedBy(v string) *BrokerJoinTokenUpsertOne { + return u.Update(func(s *BrokerJoinTokenUpsert) { + s.SetCreatedBy(v) + }) +} + +// UpdateCreatedBy sets the "created_by" field to the value that was provided on create. +func (u *BrokerJoinTokenUpsertOne) UpdateCreatedBy() *BrokerJoinTokenUpsertOne { + return u.Update(func(s *BrokerJoinTokenUpsert) { + s.UpdateCreatedBy() + }) +} + +// Exec executes the query. +func (u *BrokerJoinTokenUpsertOne) Exec(ctx context.Context) error { + if len(u.create.conflict) == 0 { + return errors.New("ent: missing options for BrokerJoinTokenCreate.OnConflict") + } + return u.create.Exec(ctx) +} + +// ExecX is like Exec, but panics if an error occurs. +func (u *BrokerJoinTokenUpsertOne) ExecX(ctx context.Context) { + if err := u.create.Exec(ctx); err != nil { + panic(err) + } +} + +// Exec executes the UPSERT query and returns the inserted/updated ID. +func (u *BrokerJoinTokenUpsertOne) ID(ctx context.Context) (id uuid.UUID, err error) { + if u.create.driver.Dialect() == dialect.MySQL { + // In case of "ON CONFLICT", there is no way to get back non-numeric ID + // fields from the database since MySQL does not support the RETURNING clause. + return id, errors.New("ent: BrokerJoinTokenUpsertOne.ID is not supported by MySQL driver. Use BrokerJoinTokenUpsertOne.Exec instead") + } + node, err := u.create.Save(ctx) + if err != nil { + return id, err + } + return node.ID, nil +} + +// IDX is like ID, but panics if an error occurs. +func (u *BrokerJoinTokenUpsertOne) IDX(ctx context.Context) uuid.UUID { + id, err := u.ID(ctx) + if err != nil { + panic(err) + } + return id +} + +// BrokerJoinTokenCreateBulk is the builder for creating many BrokerJoinToken entities in bulk. +type BrokerJoinTokenCreateBulk struct { + config + err error + builders []*BrokerJoinTokenCreate + conflict []sql.ConflictOption +} + +// Save creates the BrokerJoinToken entities in the database. +func (_c *BrokerJoinTokenCreateBulk) Save(ctx context.Context) ([]*BrokerJoinToken, error) { + if _c.err != nil { + return nil, _c.err + } + specs := make([]*sqlgraph.CreateSpec, len(_c.builders)) + nodes := make([]*BrokerJoinToken, len(_c.builders)) + mutators := make([]Mutator, len(_c.builders)) + for i := range _c.builders { + func(i int, root context.Context) { + builder := _c.builders[i] + builder.defaults() + var mut Mutator = MutateFunc(func(ctx context.Context, m Mutation) (Value, error) { + mutation, ok := m.(*BrokerJoinTokenMutation) + if !ok { + return nil, fmt.Errorf("unexpected mutation type %T", m) + } + if err := builder.check(); err != nil { + return nil, err + } + builder.mutation = mutation + var err error + nodes[i], specs[i] = builder.createSpec() + if i < len(mutators)-1 { + _, err = mutators[i+1].Mutate(root, _c.builders[i+1].mutation) + } else { + spec := &sqlgraph.BatchCreateSpec{Nodes: specs} + spec.OnConflict = _c.conflict + // Invoke the actual operation on the latest mutation in the chain. + if err = sqlgraph.BatchCreate(ctx, _c.driver, spec); err != nil { + if sqlgraph.IsConstraintError(err) { + err = &ConstraintError{msg: err.Error(), wrap: err} + } + } + } + if err != nil { + return nil, err + } + mutation.id = &nodes[i].ID + mutation.done = true + return nodes[i], nil + }) + for i := len(builder.hooks) - 1; i >= 0; i-- { + mut = builder.hooks[i](mut) + } + mutators[i] = mut + }(i, ctx) + } + if len(mutators) > 0 { + if _, err := mutators[0].Mutate(ctx, _c.builders[0].mutation); err != nil { + return nil, err + } + } + return nodes, nil +} + +// SaveX is like Save, but panics if an error occurs. +func (_c *BrokerJoinTokenCreateBulk) SaveX(ctx context.Context) []*BrokerJoinToken { + v, err := _c.Save(ctx) + if err != nil { + panic(err) + } + return v +} + +// Exec executes the query. +func (_c *BrokerJoinTokenCreateBulk) Exec(ctx context.Context) error { + _, err := _c.Save(ctx) + return err +} + +// ExecX is like Exec, but panics if an error occurs. +func (_c *BrokerJoinTokenCreateBulk) ExecX(ctx context.Context) { + if err := _c.Exec(ctx); err != nil { + panic(err) + } +} + +// OnConflict allows configuring the `ON CONFLICT` / `ON DUPLICATE KEY` clause +// of the `INSERT` statement. For example: +// +// client.BrokerJoinToken.CreateBulk(builders...). +// OnConflict( +// // Update the row with the new values +// // the was proposed for insertion. +// sql.ResolveWithNewValues(), +// ). +// // Override some of the fields with custom +// // update values. +// Update(func(u *ent.BrokerJoinTokenUpsert) { +// SetTokenHash(v+v). +// }). +// Exec(ctx) +func (_c *BrokerJoinTokenCreateBulk) OnConflict(opts ...sql.ConflictOption) *BrokerJoinTokenUpsertBulk { + _c.conflict = opts + return &BrokerJoinTokenUpsertBulk{ + create: _c, + } +} + +// OnConflictColumns calls `OnConflict` and configures the columns +// as conflict target. Using this option is equivalent to using: +// +// client.BrokerJoinToken.Create(). +// OnConflict(sql.ConflictColumns(columns...)). +// Exec(ctx) +func (_c *BrokerJoinTokenCreateBulk) OnConflictColumns(columns ...string) *BrokerJoinTokenUpsertBulk { + _c.conflict = append(_c.conflict, sql.ConflictColumns(columns...)) + return &BrokerJoinTokenUpsertBulk{ + create: _c, + } +} + +// BrokerJoinTokenUpsertBulk is the builder for "upsert"-ing +// a bulk of BrokerJoinToken nodes. +type BrokerJoinTokenUpsertBulk struct { + create *BrokerJoinTokenCreateBulk +} + +// UpdateNewValues updates the mutable fields using the new values that +// were set on create. Using this option is equivalent to using: +// +// client.BrokerJoinToken.Create(). +// OnConflict( +// sql.ResolveWithNewValues(), +// sql.ResolveWith(func(u *sql.UpdateSet) { +// u.SetIgnore(brokerjointoken.FieldID) +// }), +// ). +// Exec(ctx) +func (u *BrokerJoinTokenUpsertBulk) UpdateNewValues() *BrokerJoinTokenUpsertBulk { + u.create.conflict = append(u.create.conflict, sql.ResolveWithNewValues()) + u.create.conflict = append(u.create.conflict, sql.ResolveWith(func(s *sql.UpdateSet) { + for _, b := range u.create.builders { + if _, exists := b.mutation.ID(); exists { + s.SetIgnore(brokerjointoken.FieldID) + } + if _, exists := b.mutation.Created(); exists { + s.SetIgnore(brokerjointoken.FieldCreated) + } + } + })) + return u +} + +// Ignore sets each column to itself in case of conflict. +// Using this option is equivalent to using: +// +// client.BrokerJoinToken.Create(). +// OnConflict(sql.ResolveWithIgnore()). +// Exec(ctx) +func (u *BrokerJoinTokenUpsertBulk) Ignore() *BrokerJoinTokenUpsertBulk { + u.create.conflict = append(u.create.conflict, sql.ResolveWithIgnore()) + return u +} + +// DoNothing configures the conflict_action to `DO NOTHING`. +// Supported only by SQLite and PostgreSQL. +func (u *BrokerJoinTokenUpsertBulk) DoNothing() *BrokerJoinTokenUpsertBulk { + u.create.conflict = append(u.create.conflict, sql.DoNothing()) + return u +} + +// Update allows overriding fields `UPDATE` values. See the BrokerJoinTokenCreateBulk.OnConflict +// documentation for more info. +func (u *BrokerJoinTokenUpsertBulk) Update(set func(*BrokerJoinTokenUpsert)) *BrokerJoinTokenUpsertBulk { + u.create.conflict = append(u.create.conflict, sql.ResolveWith(func(update *sql.UpdateSet) { + set(&BrokerJoinTokenUpsert{UpdateSet: update}) + })) + return u +} + +// SetTokenHash sets the "token_hash" field. +func (u *BrokerJoinTokenUpsertBulk) SetTokenHash(v string) *BrokerJoinTokenUpsertBulk { + return u.Update(func(s *BrokerJoinTokenUpsert) { + s.SetTokenHash(v) + }) +} + +// UpdateTokenHash sets the "token_hash" field to the value that was provided on create. +func (u *BrokerJoinTokenUpsertBulk) UpdateTokenHash() *BrokerJoinTokenUpsertBulk { + return u.Update(func(s *BrokerJoinTokenUpsert) { + s.UpdateTokenHash() + }) +} + +// SetExpiresAt sets the "expires_at" field. +func (u *BrokerJoinTokenUpsertBulk) SetExpiresAt(v time.Time) *BrokerJoinTokenUpsertBulk { + return u.Update(func(s *BrokerJoinTokenUpsert) { + s.SetExpiresAt(v) + }) +} + +// UpdateExpiresAt sets the "expires_at" field to the value that was provided on create. +func (u *BrokerJoinTokenUpsertBulk) UpdateExpiresAt() *BrokerJoinTokenUpsertBulk { + return u.Update(func(s *BrokerJoinTokenUpsert) { + s.UpdateExpiresAt() + }) +} + +// SetCreatedBy sets the "created_by" field. +func (u *BrokerJoinTokenUpsertBulk) SetCreatedBy(v string) *BrokerJoinTokenUpsertBulk { + return u.Update(func(s *BrokerJoinTokenUpsert) { + s.SetCreatedBy(v) + }) +} + +// UpdateCreatedBy sets the "created_by" field to the value that was provided on create. +func (u *BrokerJoinTokenUpsertBulk) UpdateCreatedBy() *BrokerJoinTokenUpsertBulk { + return u.Update(func(s *BrokerJoinTokenUpsert) { + s.UpdateCreatedBy() + }) +} + +// Exec executes the query. +func (u *BrokerJoinTokenUpsertBulk) Exec(ctx context.Context) error { + if u.create.err != nil { + return u.create.err + } + for i, b := range u.create.builders { + if len(b.conflict) != 0 { + return fmt.Errorf("ent: OnConflict was set for builder %d. Set it on the BrokerJoinTokenCreateBulk instead", i) + } + } + if len(u.create.conflict) == 0 { + return errors.New("ent: missing options for BrokerJoinTokenCreateBulk.OnConflict") + } + return u.create.Exec(ctx) +} + +// ExecX is like Exec, but panics if an error occurs. +func (u *BrokerJoinTokenUpsertBulk) ExecX(ctx context.Context) { + if err := u.create.Exec(ctx); err != nil { + panic(err) + } +} diff --git a/pkg/ent/brokerjointoken_delete.go b/pkg/ent/brokerjointoken_delete.go new file mode 100644 index 000000000..5aa3ea0ef --- /dev/null +++ b/pkg/ent/brokerjointoken_delete.go @@ -0,0 +1,88 @@ +// Code generated by ent, DO NOT EDIT. + +package ent + +import ( + "context" + + "entgo.io/ent/dialect/sql" + "entgo.io/ent/dialect/sql/sqlgraph" + "entgo.io/ent/schema/field" + "github.com/GoogleCloudPlatform/scion/pkg/ent/brokerjointoken" + "github.com/GoogleCloudPlatform/scion/pkg/ent/predicate" +) + +// BrokerJoinTokenDelete is the builder for deleting a BrokerJoinToken entity. +type BrokerJoinTokenDelete struct { + config + hooks []Hook + mutation *BrokerJoinTokenMutation +} + +// Where appends a list predicates to the BrokerJoinTokenDelete builder. +func (_d *BrokerJoinTokenDelete) Where(ps ...predicate.BrokerJoinToken) *BrokerJoinTokenDelete { + _d.mutation.Where(ps...) + return _d +} + +// Exec executes the deletion query and returns how many vertices were deleted. +func (_d *BrokerJoinTokenDelete) Exec(ctx context.Context) (int, error) { + return withHooks(ctx, _d.sqlExec, _d.mutation, _d.hooks) +} + +// ExecX is like Exec, but panics if an error occurs. +func (_d *BrokerJoinTokenDelete) ExecX(ctx context.Context) int { + n, err := _d.Exec(ctx) + if err != nil { + panic(err) + } + return n +} + +func (_d *BrokerJoinTokenDelete) sqlExec(ctx context.Context) (int, error) { + _spec := sqlgraph.NewDeleteSpec(brokerjointoken.Table, sqlgraph.NewFieldSpec(brokerjointoken.FieldID, field.TypeUUID)) + if ps := _d.mutation.predicates; len(ps) > 0 { + _spec.Predicate = func(selector *sql.Selector) { + for i := range ps { + ps[i](selector) + } + } + } + affected, err := sqlgraph.DeleteNodes(ctx, _d.driver, _spec) + if err != nil && sqlgraph.IsConstraintError(err) { + err = &ConstraintError{msg: err.Error(), wrap: err} + } + _d.mutation.done = true + return affected, err +} + +// BrokerJoinTokenDeleteOne is the builder for deleting a single BrokerJoinToken entity. +type BrokerJoinTokenDeleteOne struct { + _d *BrokerJoinTokenDelete +} + +// Where appends a list predicates to the BrokerJoinTokenDelete builder. +func (_d *BrokerJoinTokenDeleteOne) Where(ps ...predicate.BrokerJoinToken) *BrokerJoinTokenDeleteOne { + _d._d.mutation.Where(ps...) + return _d +} + +// Exec executes the deletion query. +func (_d *BrokerJoinTokenDeleteOne) Exec(ctx context.Context) error { + n, err := _d._d.Exec(ctx) + switch { + case err != nil: + return err + case n == 0: + return &NotFoundError{brokerjointoken.Label} + default: + return nil + } +} + +// ExecX is like Exec, but panics if an error occurs. +func (_d *BrokerJoinTokenDeleteOne) ExecX(ctx context.Context) { + if err := _d.Exec(ctx); err != nil { + panic(err) + } +} diff --git a/pkg/ent/brokerjointoken_query.go b/pkg/ent/brokerjointoken_query.go new file mode 100644 index 000000000..92a86cdec --- /dev/null +++ b/pkg/ent/brokerjointoken_query.go @@ -0,0 +1,565 @@ +// Code generated by ent, DO NOT EDIT. + +package ent + +import ( + "context" + "fmt" + "math" + + "entgo.io/ent" + "entgo.io/ent/dialect" + "entgo.io/ent/dialect/sql" + "entgo.io/ent/dialect/sql/sqlgraph" + "entgo.io/ent/schema/field" + "github.com/GoogleCloudPlatform/scion/pkg/ent/brokerjointoken" + "github.com/GoogleCloudPlatform/scion/pkg/ent/predicate" + "github.com/google/uuid" +) + +// BrokerJoinTokenQuery is the builder for querying BrokerJoinToken entities. +type BrokerJoinTokenQuery struct { + config + ctx *QueryContext + order []brokerjointoken.OrderOption + inters []Interceptor + predicates []predicate.BrokerJoinToken + modifiers []func(*sql.Selector) + // intermediate query (i.e. traversal path). + sql *sql.Selector + path func(context.Context) (*sql.Selector, error) +} + +// Where adds a new predicate for the BrokerJoinTokenQuery builder. +func (_q *BrokerJoinTokenQuery) Where(ps ...predicate.BrokerJoinToken) *BrokerJoinTokenQuery { + _q.predicates = append(_q.predicates, ps...) + return _q +} + +// Limit the number of records to be returned by this query. +func (_q *BrokerJoinTokenQuery) Limit(limit int) *BrokerJoinTokenQuery { + _q.ctx.Limit = &limit + return _q +} + +// Offset to start from. +func (_q *BrokerJoinTokenQuery) Offset(offset int) *BrokerJoinTokenQuery { + _q.ctx.Offset = &offset + return _q +} + +// Unique configures the query builder to filter duplicate records on query. +// By default, unique is set to true, and can be disabled using this method. +func (_q *BrokerJoinTokenQuery) Unique(unique bool) *BrokerJoinTokenQuery { + _q.ctx.Unique = &unique + return _q +} + +// Order specifies how the records should be ordered. +func (_q *BrokerJoinTokenQuery) Order(o ...brokerjointoken.OrderOption) *BrokerJoinTokenQuery { + _q.order = append(_q.order, o...) + return _q +} + +// First returns the first BrokerJoinToken entity from the query. +// Returns a *NotFoundError when no BrokerJoinToken was found. +func (_q *BrokerJoinTokenQuery) First(ctx context.Context) (*BrokerJoinToken, error) { + nodes, err := _q.Limit(1).All(setContextOp(ctx, _q.ctx, ent.OpQueryFirst)) + if err != nil { + return nil, err + } + if len(nodes) == 0 { + return nil, &NotFoundError{brokerjointoken.Label} + } + return nodes[0], nil +} + +// FirstX is like First, but panics if an error occurs. +func (_q *BrokerJoinTokenQuery) FirstX(ctx context.Context) *BrokerJoinToken { + node, err := _q.First(ctx) + if err != nil && !IsNotFound(err) { + panic(err) + } + return node +} + +// FirstID returns the first BrokerJoinToken ID from the query. +// Returns a *NotFoundError when no BrokerJoinToken ID was found. +func (_q *BrokerJoinTokenQuery) FirstID(ctx context.Context) (id uuid.UUID, err error) { + var ids []uuid.UUID + if ids, err = _q.Limit(1).IDs(setContextOp(ctx, _q.ctx, ent.OpQueryFirstID)); err != nil { + return + } + if len(ids) == 0 { + err = &NotFoundError{brokerjointoken.Label} + return + } + return ids[0], nil +} + +// FirstIDX is like FirstID, but panics if an error occurs. +func (_q *BrokerJoinTokenQuery) FirstIDX(ctx context.Context) uuid.UUID { + id, err := _q.FirstID(ctx) + if err != nil && !IsNotFound(err) { + panic(err) + } + return id +} + +// Only returns a single BrokerJoinToken entity found by the query, ensuring it only returns one. +// Returns a *NotSingularError when more than one BrokerJoinToken entity is found. +// Returns a *NotFoundError when no BrokerJoinToken entities are found. +func (_q *BrokerJoinTokenQuery) Only(ctx context.Context) (*BrokerJoinToken, error) { + nodes, err := _q.Limit(2).All(setContextOp(ctx, _q.ctx, ent.OpQueryOnly)) + if err != nil { + return nil, err + } + switch len(nodes) { + case 1: + return nodes[0], nil + case 0: + return nil, &NotFoundError{brokerjointoken.Label} + default: + return nil, &NotSingularError{brokerjointoken.Label} + } +} + +// OnlyX is like Only, but panics if an error occurs. +func (_q *BrokerJoinTokenQuery) OnlyX(ctx context.Context) *BrokerJoinToken { + node, err := _q.Only(ctx) + if err != nil { + panic(err) + } + return node +} + +// OnlyID is like Only, but returns the only BrokerJoinToken ID in the query. +// Returns a *NotSingularError when more than one BrokerJoinToken ID is found. +// Returns a *NotFoundError when no entities are found. +func (_q *BrokerJoinTokenQuery) OnlyID(ctx context.Context) (id uuid.UUID, err error) { + var ids []uuid.UUID + if ids, err = _q.Limit(2).IDs(setContextOp(ctx, _q.ctx, ent.OpQueryOnlyID)); err != nil { + return + } + switch len(ids) { + case 1: + id = ids[0] + case 0: + err = &NotFoundError{brokerjointoken.Label} + default: + err = &NotSingularError{brokerjointoken.Label} + } + return +} + +// OnlyIDX is like OnlyID, but panics if an error occurs. +func (_q *BrokerJoinTokenQuery) OnlyIDX(ctx context.Context) uuid.UUID { + id, err := _q.OnlyID(ctx) + if err != nil { + panic(err) + } + return id +} + +// All executes the query and returns a list of BrokerJoinTokens. +func (_q *BrokerJoinTokenQuery) All(ctx context.Context) ([]*BrokerJoinToken, error) { + ctx = setContextOp(ctx, _q.ctx, ent.OpQueryAll) + if err := _q.prepareQuery(ctx); err != nil { + return nil, err + } + qr := querierAll[[]*BrokerJoinToken, *BrokerJoinTokenQuery]() + return withInterceptors[[]*BrokerJoinToken](ctx, _q, qr, _q.inters) +} + +// AllX is like All, but panics if an error occurs. +func (_q *BrokerJoinTokenQuery) AllX(ctx context.Context) []*BrokerJoinToken { + nodes, err := _q.All(ctx) + if err != nil { + panic(err) + } + return nodes +} + +// IDs executes the query and returns a list of BrokerJoinToken IDs. +func (_q *BrokerJoinTokenQuery) IDs(ctx context.Context) (ids []uuid.UUID, err error) { + if _q.ctx.Unique == nil && _q.path != nil { + _q.Unique(true) + } + ctx = setContextOp(ctx, _q.ctx, ent.OpQueryIDs) + if err = _q.Select(brokerjointoken.FieldID).Scan(ctx, &ids); err != nil { + return nil, err + } + return ids, nil +} + +// IDsX is like IDs, but panics if an error occurs. +func (_q *BrokerJoinTokenQuery) IDsX(ctx context.Context) []uuid.UUID { + ids, err := _q.IDs(ctx) + if err != nil { + panic(err) + } + return ids +} + +// Count returns the count of the given query. +func (_q *BrokerJoinTokenQuery) Count(ctx context.Context) (int, error) { + ctx = setContextOp(ctx, _q.ctx, ent.OpQueryCount) + if err := _q.prepareQuery(ctx); err != nil { + return 0, err + } + return withInterceptors[int](ctx, _q, querierCount[*BrokerJoinTokenQuery](), _q.inters) +} + +// CountX is like Count, but panics if an error occurs. +func (_q *BrokerJoinTokenQuery) CountX(ctx context.Context) int { + count, err := _q.Count(ctx) + if err != nil { + panic(err) + } + return count +} + +// Exist returns true if the query has elements in the graph. +func (_q *BrokerJoinTokenQuery) Exist(ctx context.Context) (bool, error) { + ctx = setContextOp(ctx, _q.ctx, ent.OpQueryExist) + switch _, err := _q.FirstID(ctx); { + case IsNotFound(err): + return false, nil + case err != nil: + return false, fmt.Errorf("ent: check existence: %w", err) + default: + return true, nil + } +} + +// ExistX is like Exist, but panics if an error occurs. +func (_q *BrokerJoinTokenQuery) ExistX(ctx context.Context) bool { + exist, err := _q.Exist(ctx) + if err != nil { + panic(err) + } + return exist +} + +// Clone returns a duplicate of the BrokerJoinTokenQuery builder, including all associated steps. It can be +// used to prepare common query builders and use them differently after the clone is made. +func (_q *BrokerJoinTokenQuery) Clone() *BrokerJoinTokenQuery { + if _q == nil { + return nil + } + return &BrokerJoinTokenQuery{ + config: _q.config, + ctx: _q.ctx.Clone(), + order: append([]brokerjointoken.OrderOption{}, _q.order...), + inters: append([]Interceptor{}, _q.inters...), + predicates: append([]predicate.BrokerJoinToken{}, _q.predicates...), + // clone intermediate query. + sql: _q.sql.Clone(), + path: _q.path, + } +} + +// GroupBy is used to group vertices by one or more fields/columns. +// It is often used with aggregate functions, like: count, max, mean, min, sum. +// +// Example: +// +// var v []struct { +// TokenHash string `json:"token_hash,omitempty"` +// Count int `json:"count,omitempty"` +// } +// +// client.BrokerJoinToken.Query(). +// GroupBy(brokerjointoken.FieldTokenHash). +// Aggregate(ent.Count()). +// Scan(ctx, &v) +func (_q *BrokerJoinTokenQuery) GroupBy(field string, fields ...string) *BrokerJoinTokenGroupBy { + _q.ctx.Fields = append([]string{field}, fields...) + grbuild := &BrokerJoinTokenGroupBy{build: _q} + grbuild.flds = &_q.ctx.Fields + grbuild.label = brokerjointoken.Label + grbuild.scan = grbuild.Scan + return grbuild +} + +// Select allows the selection one or more fields/columns for the given query, +// instead of selecting all fields in the entity. +// +// Example: +// +// var v []struct { +// TokenHash string `json:"token_hash,omitempty"` +// } +// +// client.BrokerJoinToken.Query(). +// Select(brokerjointoken.FieldTokenHash). +// Scan(ctx, &v) +func (_q *BrokerJoinTokenQuery) Select(fields ...string) *BrokerJoinTokenSelect { + _q.ctx.Fields = append(_q.ctx.Fields, fields...) + sbuild := &BrokerJoinTokenSelect{BrokerJoinTokenQuery: _q} + sbuild.label = brokerjointoken.Label + sbuild.flds, sbuild.scan = &_q.ctx.Fields, sbuild.Scan + return sbuild +} + +// Aggregate returns a BrokerJoinTokenSelect configured with the given aggregations. +func (_q *BrokerJoinTokenQuery) Aggregate(fns ...AggregateFunc) *BrokerJoinTokenSelect { + return _q.Select().Aggregate(fns...) +} + +func (_q *BrokerJoinTokenQuery) prepareQuery(ctx context.Context) error { + for _, inter := range _q.inters { + if inter == nil { + return fmt.Errorf("ent: uninitialized interceptor (forgotten import ent/runtime?)") + } + if trv, ok := inter.(Traverser); ok { + if err := trv.Traverse(ctx, _q); err != nil { + return err + } + } + } + for _, f := range _q.ctx.Fields { + if !brokerjointoken.ValidColumn(f) { + return &ValidationError{Name: f, err: fmt.Errorf("ent: invalid field %q for query", f)} + } + } + if _q.path != nil { + prev, err := _q.path(ctx) + if err != nil { + return err + } + _q.sql = prev + } + return nil +} + +func (_q *BrokerJoinTokenQuery) sqlAll(ctx context.Context, hooks ...queryHook) ([]*BrokerJoinToken, error) { + var ( + nodes = []*BrokerJoinToken{} + _spec = _q.querySpec() + ) + _spec.ScanValues = func(columns []string) ([]any, error) { + return (*BrokerJoinToken).scanValues(nil, columns) + } + _spec.Assign = func(columns []string, values []any) error { + node := &BrokerJoinToken{config: _q.config} + nodes = append(nodes, node) + return node.assignValues(columns, values) + } + if len(_q.modifiers) > 0 { + _spec.Modifiers = _q.modifiers + } + for i := range hooks { + hooks[i](ctx, _spec) + } + if err := sqlgraph.QueryNodes(ctx, _q.driver, _spec); err != nil { + return nil, err + } + if len(nodes) == 0 { + return nodes, nil + } + return nodes, nil +} + +func (_q *BrokerJoinTokenQuery) sqlCount(ctx context.Context) (int, error) { + _spec := _q.querySpec() + if len(_q.modifiers) > 0 { + _spec.Modifiers = _q.modifiers + } + _spec.Node.Columns = _q.ctx.Fields + if len(_q.ctx.Fields) > 0 { + _spec.Unique = _q.ctx.Unique != nil && *_q.ctx.Unique + } + return sqlgraph.CountNodes(ctx, _q.driver, _spec) +} + +func (_q *BrokerJoinTokenQuery) querySpec() *sqlgraph.QuerySpec { + _spec := sqlgraph.NewQuerySpec(brokerjointoken.Table, brokerjointoken.Columns, sqlgraph.NewFieldSpec(brokerjointoken.FieldID, field.TypeUUID)) + _spec.From = _q.sql + if unique := _q.ctx.Unique; unique != nil { + _spec.Unique = *unique + } else if _q.path != nil { + _spec.Unique = true + } + if fields := _q.ctx.Fields; len(fields) > 0 { + _spec.Node.Columns = make([]string, 0, len(fields)) + _spec.Node.Columns = append(_spec.Node.Columns, brokerjointoken.FieldID) + for i := range fields { + if fields[i] != brokerjointoken.FieldID { + _spec.Node.Columns = append(_spec.Node.Columns, fields[i]) + } + } + } + if ps := _q.predicates; len(ps) > 0 { + _spec.Predicate = func(selector *sql.Selector) { + for i := range ps { + ps[i](selector) + } + } + } + if limit := _q.ctx.Limit; limit != nil { + _spec.Limit = *limit + } + if offset := _q.ctx.Offset; offset != nil { + _spec.Offset = *offset + } + if ps := _q.order; len(ps) > 0 { + _spec.Order = func(selector *sql.Selector) { + for i := range ps { + ps[i](selector) + } + } + } + return _spec +} + +func (_q *BrokerJoinTokenQuery) sqlQuery(ctx context.Context) *sql.Selector { + builder := sql.Dialect(_q.driver.Dialect()) + t1 := builder.Table(brokerjointoken.Table) + columns := _q.ctx.Fields + if len(columns) == 0 { + columns = brokerjointoken.Columns + } + selector := builder.Select(t1.Columns(columns...)...).From(t1) + if _q.sql != nil { + selector = _q.sql + selector.Select(selector.Columns(columns...)...) + } + if _q.ctx.Unique != nil && *_q.ctx.Unique { + selector.Distinct() + } + for _, m := range _q.modifiers { + m(selector) + } + for _, p := range _q.predicates { + p(selector) + } + for _, p := range _q.order { + p(selector) + } + if offset := _q.ctx.Offset; offset != nil { + // limit is mandatory for offset clause. We start + // with default value, and override it below if needed. + selector.Offset(*offset).Limit(math.MaxInt32) + } + if limit := _q.ctx.Limit; limit != nil { + selector.Limit(*limit) + } + return selector +} + +// ForUpdate locks the selected rows against concurrent updates, and prevent them from being +// updated, deleted or "selected ... for update" by other sessions, until the transaction is +// either committed or rolled-back. +func (_q *BrokerJoinTokenQuery) ForUpdate(opts ...sql.LockOption) *BrokerJoinTokenQuery { + if _q.driver.Dialect() == dialect.Postgres { + _q.Unique(false) + } + _q.modifiers = append(_q.modifiers, func(s *sql.Selector) { + s.ForUpdate(opts...) + }) + return _q +} + +// ForShare behaves similarly to ForUpdate, except that it acquires a shared mode lock +// on any rows that are read. Other sessions can read the rows, but cannot modify them +// until your transaction commits. +func (_q *BrokerJoinTokenQuery) ForShare(opts ...sql.LockOption) *BrokerJoinTokenQuery { + if _q.driver.Dialect() == dialect.Postgres { + _q.Unique(false) + } + _q.modifiers = append(_q.modifiers, func(s *sql.Selector) { + s.ForShare(opts...) + }) + return _q +} + +// BrokerJoinTokenGroupBy is the group-by builder for BrokerJoinToken entities. +type BrokerJoinTokenGroupBy struct { + selector + build *BrokerJoinTokenQuery +} + +// Aggregate adds the given aggregation functions to the group-by query. +func (_g *BrokerJoinTokenGroupBy) Aggregate(fns ...AggregateFunc) *BrokerJoinTokenGroupBy { + _g.fns = append(_g.fns, fns...) + return _g +} + +// Scan applies the selector query and scans the result into the given value. +func (_g *BrokerJoinTokenGroupBy) Scan(ctx context.Context, v any) error { + ctx = setContextOp(ctx, _g.build.ctx, ent.OpQueryGroupBy) + if err := _g.build.prepareQuery(ctx); err != nil { + return err + } + return scanWithInterceptors[*BrokerJoinTokenQuery, *BrokerJoinTokenGroupBy](ctx, _g.build, _g, _g.build.inters, v) +} + +func (_g *BrokerJoinTokenGroupBy) sqlScan(ctx context.Context, root *BrokerJoinTokenQuery, v any) error { + selector := root.sqlQuery(ctx).Select() + aggregation := make([]string, 0, len(_g.fns)) + for _, fn := range _g.fns { + aggregation = append(aggregation, fn(selector)) + } + if len(selector.SelectedColumns()) == 0 { + columns := make([]string, 0, len(*_g.flds)+len(_g.fns)) + for _, f := range *_g.flds { + columns = append(columns, selector.C(f)) + } + columns = append(columns, aggregation...) + selector.Select(columns...) + } + selector.GroupBy(selector.Columns(*_g.flds...)...) + if err := selector.Err(); err != nil { + return err + } + rows := &sql.Rows{} + query, args := selector.Query() + if err := _g.build.driver.Query(ctx, query, args, rows); err != nil { + return err + } + defer rows.Close() + return sql.ScanSlice(rows, v) +} + +// BrokerJoinTokenSelect is the builder for selecting fields of BrokerJoinToken entities. +type BrokerJoinTokenSelect struct { + *BrokerJoinTokenQuery + selector +} + +// Aggregate adds the given aggregation functions to the selector query. +func (_s *BrokerJoinTokenSelect) Aggregate(fns ...AggregateFunc) *BrokerJoinTokenSelect { + _s.fns = append(_s.fns, fns...) + return _s +} + +// Scan applies the selector query and scans the result into the given value. +func (_s *BrokerJoinTokenSelect) Scan(ctx context.Context, v any) error { + ctx = setContextOp(ctx, _s.ctx, ent.OpQuerySelect) + if err := _s.prepareQuery(ctx); err != nil { + return err + } + return scanWithInterceptors[*BrokerJoinTokenQuery, *BrokerJoinTokenSelect](ctx, _s.BrokerJoinTokenQuery, _s, _s.inters, v) +} + +func (_s *BrokerJoinTokenSelect) sqlScan(ctx context.Context, root *BrokerJoinTokenQuery, v any) error { + selector := root.sqlQuery(ctx) + aggregation := make([]string, 0, len(_s.fns)) + for _, fn := range _s.fns { + aggregation = append(aggregation, fn(selector)) + } + switch n := len(*_s.selector.flds); { + case n == 0 && len(aggregation) > 0: + selector.Select(aggregation...) + case n != 0 && len(aggregation) > 0: + selector.AppendSelect(aggregation...) + } + rows := &sql.Rows{} + query, args := selector.Query() + if err := _s.driver.Query(ctx, query, args, rows); err != nil { + return err + } + defer rows.Close() + return sql.ScanSlice(rows, v) +} diff --git a/pkg/ent/brokerjointoken_update.go b/pkg/ent/brokerjointoken_update.go new file mode 100644 index 000000000..2a91bcc82 --- /dev/null +++ b/pkg/ent/brokerjointoken_update.go @@ -0,0 +1,314 @@ +// Code generated by ent, DO NOT EDIT. + +package ent + +import ( + "context" + "errors" + "fmt" + "time" + + "entgo.io/ent/dialect/sql" + "entgo.io/ent/dialect/sql/sqlgraph" + "entgo.io/ent/schema/field" + "github.com/GoogleCloudPlatform/scion/pkg/ent/brokerjointoken" + "github.com/GoogleCloudPlatform/scion/pkg/ent/predicate" +) + +// BrokerJoinTokenUpdate is the builder for updating BrokerJoinToken entities. +type BrokerJoinTokenUpdate struct { + config + hooks []Hook + mutation *BrokerJoinTokenMutation +} + +// Where appends a list predicates to the BrokerJoinTokenUpdate builder. +func (_u *BrokerJoinTokenUpdate) Where(ps ...predicate.BrokerJoinToken) *BrokerJoinTokenUpdate { + _u.mutation.Where(ps...) + return _u +} + +// SetTokenHash sets the "token_hash" field. +func (_u *BrokerJoinTokenUpdate) SetTokenHash(v string) *BrokerJoinTokenUpdate { + _u.mutation.SetTokenHash(v) + return _u +} + +// SetNillableTokenHash sets the "token_hash" field if the given value is not nil. +func (_u *BrokerJoinTokenUpdate) SetNillableTokenHash(v *string) *BrokerJoinTokenUpdate { + if v != nil { + _u.SetTokenHash(*v) + } + return _u +} + +// SetExpiresAt sets the "expires_at" field. +func (_u *BrokerJoinTokenUpdate) SetExpiresAt(v time.Time) *BrokerJoinTokenUpdate { + _u.mutation.SetExpiresAt(v) + return _u +} + +// SetNillableExpiresAt sets the "expires_at" field if the given value is not nil. +func (_u *BrokerJoinTokenUpdate) SetNillableExpiresAt(v *time.Time) *BrokerJoinTokenUpdate { + if v != nil { + _u.SetExpiresAt(*v) + } + return _u +} + +// SetCreatedBy sets the "created_by" field. +func (_u *BrokerJoinTokenUpdate) SetCreatedBy(v string) *BrokerJoinTokenUpdate { + _u.mutation.SetCreatedBy(v) + return _u +} + +// SetNillableCreatedBy sets the "created_by" field if the given value is not nil. +func (_u *BrokerJoinTokenUpdate) SetNillableCreatedBy(v *string) *BrokerJoinTokenUpdate { + if v != nil { + _u.SetCreatedBy(*v) + } + return _u +} + +// Mutation returns the BrokerJoinTokenMutation object of the builder. +func (_u *BrokerJoinTokenUpdate) Mutation() *BrokerJoinTokenMutation { + return _u.mutation +} + +// Save executes the query and returns the number of nodes affected by the update operation. +func (_u *BrokerJoinTokenUpdate) Save(ctx context.Context) (int, error) { + return withHooks(ctx, _u.sqlSave, _u.mutation, _u.hooks) +} + +// SaveX is like Save, but panics if an error occurs. +func (_u *BrokerJoinTokenUpdate) SaveX(ctx context.Context) int { + affected, err := _u.Save(ctx) + if err != nil { + panic(err) + } + return affected +} + +// Exec executes the query. +func (_u *BrokerJoinTokenUpdate) Exec(ctx context.Context) error { + _, err := _u.Save(ctx) + return err +} + +// ExecX is like Exec, but panics if an error occurs. +func (_u *BrokerJoinTokenUpdate) ExecX(ctx context.Context) { + if err := _u.Exec(ctx); err != nil { + panic(err) + } +} + +// check runs all checks and user-defined validators on the builder. +func (_u *BrokerJoinTokenUpdate) check() error { + if v, ok := _u.mutation.TokenHash(); ok { + if err := brokerjointoken.TokenHashValidator(v); err != nil { + return &ValidationError{Name: "token_hash", err: fmt.Errorf(`ent: validator failed for field "BrokerJoinToken.token_hash": %w`, err)} + } + } + if v, ok := _u.mutation.CreatedBy(); ok { + if err := brokerjointoken.CreatedByValidator(v); err != nil { + return &ValidationError{Name: "created_by", err: fmt.Errorf(`ent: validator failed for field "BrokerJoinToken.created_by": %w`, err)} + } + } + return nil +} + +func (_u *BrokerJoinTokenUpdate) sqlSave(ctx context.Context) (_node int, err error) { + if err := _u.check(); err != nil { + return _node, err + } + _spec := sqlgraph.NewUpdateSpec(brokerjointoken.Table, brokerjointoken.Columns, sqlgraph.NewFieldSpec(brokerjointoken.FieldID, field.TypeUUID)) + if ps := _u.mutation.predicates; len(ps) > 0 { + _spec.Predicate = func(selector *sql.Selector) { + for i := range ps { + ps[i](selector) + } + } + } + if value, ok := _u.mutation.TokenHash(); ok { + _spec.SetField(brokerjointoken.FieldTokenHash, field.TypeString, value) + } + if value, ok := _u.mutation.ExpiresAt(); ok { + _spec.SetField(brokerjointoken.FieldExpiresAt, field.TypeTime, value) + } + if value, ok := _u.mutation.CreatedBy(); ok { + _spec.SetField(brokerjointoken.FieldCreatedBy, field.TypeString, value) + } + if _node, err = sqlgraph.UpdateNodes(ctx, _u.driver, _spec); err != nil { + if _, ok := err.(*sqlgraph.NotFoundError); ok { + err = &NotFoundError{brokerjointoken.Label} + } else if sqlgraph.IsConstraintError(err) { + err = &ConstraintError{msg: err.Error(), wrap: err} + } + return 0, err + } + _u.mutation.done = true + return _node, nil +} + +// BrokerJoinTokenUpdateOne is the builder for updating a single BrokerJoinToken entity. +type BrokerJoinTokenUpdateOne struct { + config + fields []string + hooks []Hook + mutation *BrokerJoinTokenMutation +} + +// SetTokenHash sets the "token_hash" field. +func (_u *BrokerJoinTokenUpdateOne) SetTokenHash(v string) *BrokerJoinTokenUpdateOne { + _u.mutation.SetTokenHash(v) + return _u +} + +// SetNillableTokenHash sets the "token_hash" field if the given value is not nil. +func (_u *BrokerJoinTokenUpdateOne) SetNillableTokenHash(v *string) *BrokerJoinTokenUpdateOne { + if v != nil { + _u.SetTokenHash(*v) + } + return _u +} + +// SetExpiresAt sets the "expires_at" field. +func (_u *BrokerJoinTokenUpdateOne) SetExpiresAt(v time.Time) *BrokerJoinTokenUpdateOne { + _u.mutation.SetExpiresAt(v) + return _u +} + +// SetNillableExpiresAt sets the "expires_at" field if the given value is not nil. +func (_u *BrokerJoinTokenUpdateOne) SetNillableExpiresAt(v *time.Time) *BrokerJoinTokenUpdateOne { + if v != nil { + _u.SetExpiresAt(*v) + } + return _u +} + +// SetCreatedBy sets the "created_by" field. +func (_u *BrokerJoinTokenUpdateOne) SetCreatedBy(v string) *BrokerJoinTokenUpdateOne { + _u.mutation.SetCreatedBy(v) + return _u +} + +// SetNillableCreatedBy sets the "created_by" field if the given value is not nil. +func (_u *BrokerJoinTokenUpdateOne) SetNillableCreatedBy(v *string) *BrokerJoinTokenUpdateOne { + if v != nil { + _u.SetCreatedBy(*v) + } + return _u +} + +// Mutation returns the BrokerJoinTokenMutation object of the builder. +func (_u *BrokerJoinTokenUpdateOne) Mutation() *BrokerJoinTokenMutation { + return _u.mutation +} + +// Where appends a list predicates to the BrokerJoinTokenUpdate builder. +func (_u *BrokerJoinTokenUpdateOne) Where(ps ...predicate.BrokerJoinToken) *BrokerJoinTokenUpdateOne { + _u.mutation.Where(ps...) + return _u +} + +// Select allows selecting one or more fields (columns) of the returned entity. +// The default is selecting all fields defined in the entity schema. +func (_u *BrokerJoinTokenUpdateOne) Select(field string, fields ...string) *BrokerJoinTokenUpdateOne { + _u.fields = append([]string{field}, fields...) + return _u +} + +// Save executes the query and returns the updated BrokerJoinToken entity. +func (_u *BrokerJoinTokenUpdateOne) Save(ctx context.Context) (*BrokerJoinToken, error) { + return withHooks(ctx, _u.sqlSave, _u.mutation, _u.hooks) +} + +// SaveX is like Save, but panics if an error occurs. +func (_u *BrokerJoinTokenUpdateOne) SaveX(ctx context.Context) *BrokerJoinToken { + node, err := _u.Save(ctx) + if err != nil { + panic(err) + } + return node +} + +// Exec executes the query on the entity. +func (_u *BrokerJoinTokenUpdateOne) Exec(ctx context.Context) error { + _, err := _u.Save(ctx) + return err +} + +// ExecX is like Exec, but panics if an error occurs. +func (_u *BrokerJoinTokenUpdateOne) ExecX(ctx context.Context) { + if err := _u.Exec(ctx); err != nil { + panic(err) + } +} + +// check runs all checks and user-defined validators on the builder. +func (_u *BrokerJoinTokenUpdateOne) check() error { + if v, ok := _u.mutation.TokenHash(); ok { + if err := brokerjointoken.TokenHashValidator(v); err != nil { + return &ValidationError{Name: "token_hash", err: fmt.Errorf(`ent: validator failed for field "BrokerJoinToken.token_hash": %w`, err)} + } + } + if v, ok := _u.mutation.CreatedBy(); ok { + if err := brokerjointoken.CreatedByValidator(v); err != nil { + return &ValidationError{Name: "created_by", err: fmt.Errorf(`ent: validator failed for field "BrokerJoinToken.created_by": %w`, err)} + } + } + return nil +} + +func (_u *BrokerJoinTokenUpdateOne) sqlSave(ctx context.Context) (_node *BrokerJoinToken, err error) { + if err := _u.check(); err != nil { + return _node, err + } + _spec := sqlgraph.NewUpdateSpec(brokerjointoken.Table, brokerjointoken.Columns, sqlgraph.NewFieldSpec(brokerjointoken.FieldID, field.TypeUUID)) + id, ok := _u.mutation.ID() + if !ok { + return nil, &ValidationError{Name: "id", err: errors.New(`ent: missing "BrokerJoinToken.id" for update`)} + } + _spec.Node.ID.Value = id + if fields := _u.fields; len(fields) > 0 { + _spec.Node.Columns = make([]string, 0, len(fields)) + _spec.Node.Columns = append(_spec.Node.Columns, brokerjointoken.FieldID) + for _, f := range fields { + if !brokerjointoken.ValidColumn(f) { + return nil, &ValidationError{Name: f, err: fmt.Errorf("ent: invalid field %q for query", f)} + } + if f != brokerjointoken.FieldID { + _spec.Node.Columns = append(_spec.Node.Columns, f) + } + } + } + if ps := _u.mutation.predicates; len(ps) > 0 { + _spec.Predicate = func(selector *sql.Selector) { + for i := range ps { + ps[i](selector) + } + } + } + if value, ok := _u.mutation.TokenHash(); ok { + _spec.SetField(brokerjointoken.FieldTokenHash, field.TypeString, value) + } + if value, ok := _u.mutation.ExpiresAt(); ok { + _spec.SetField(brokerjointoken.FieldExpiresAt, field.TypeTime, value) + } + if value, ok := _u.mutation.CreatedBy(); ok { + _spec.SetField(brokerjointoken.FieldCreatedBy, field.TypeString, value) + } + _node = &BrokerJoinToken{config: _u.config} + _spec.Assign = _node.assignValues + _spec.ScanValues = _node.scanValues + if err = sqlgraph.UpdateNode(ctx, _u.driver, _spec); err != nil { + if _, ok := err.(*sqlgraph.NotFoundError); ok { + err = &NotFoundError{brokerjointoken.Label} + } else if sqlgraph.IsConstraintError(err) { + err = &ConstraintError{msg: err.Error(), wrap: err} + } + return nil, err + } + _u.mutation.done = true + return _node, nil +} diff --git a/pkg/ent/brokersecret.go b/pkg/ent/brokersecret.go new file mode 100644 index 000000000..7547a5729 --- /dev/null +++ b/pkg/ent/brokersecret.go @@ -0,0 +1,169 @@ +// Code generated by ent, DO NOT EDIT. + +package ent + +import ( + "fmt" + "strings" + "time" + + "entgo.io/ent" + "entgo.io/ent/dialect/sql" + "github.com/GoogleCloudPlatform/scion/pkg/ent/brokersecret" + "github.com/google/uuid" +) + +// BrokerSecret is the model entity for the BrokerSecret schema. +type BrokerSecret struct { + config `json:"-"` + // ID of the ent. + ID uuid.UUID `json:"id,omitempty"` + // SecretKey holds the value of the "secret_key" field. + SecretKey []byte `json:"-"` + // Algorithm holds the value of the "algorithm" field. + Algorithm string `json:"algorithm,omitempty"` + // RotatedAt holds the value of the "rotated_at" field. + RotatedAt *time.Time `json:"rotated_at,omitempty"` + // ExpiresAt holds the value of the "expires_at" field. + ExpiresAt *time.Time `json:"expires_at,omitempty"` + // Status holds the value of the "status" field. + Status string `json:"status,omitempty"` + // Created holds the value of the "created" field. + Created time.Time `json:"created,omitempty"` + selectValues sql.SelectValues +} + +// scanValues returns the types for scanning values from sql.Rows. +func (*BrokerSecret) scanValues(columns []string) ([]any, error) { + values := make([]any, len(columns)) + for i := range columns { + switch columns[i] { + case brokersecret.FieldSecretKey: + values[i] = new([]byte) + case brokersecret.FieldAlgorithm, brokersecret.FieldStatus: + values[i] = new(sql.NullString) + case brokersecret.FieldRotatedAt, brokersecret.FieldExpiresAt, brokersecret.FieldCreated: + values[i] = new(sql.NullTime) + case brokersecret.FieldID: + values[i] = new(uuid.UUID) + default: + values[i] = new(sql.UnknownType) + } + } + return values, nil +} + +// assignValues assigns the values that were returned from sql.Rows (after scanning) +// to the BrokerSecret fields. +func (_m *BrokerSecret) assignValues(columns []string, values []any) error { + if m, n := len(values), len(columns); m < n { + return fmt.Errorf("mismatch number of scan values: %d != %d", m, n) + } + for i := range columns { + switch columns[i] { + case brokersecret.FieldID: + if value, ok := values[i].(*uuid.UUID); !ok { + return fmt.Errorf("unexpected type %T for field id", values[i]) + } else if value != nil { + _m.ID = *value + } + case brokersecret.FieldSecretKey: + if value, ok := values[i].(*[]byte); !ok { + return fmt.Errorf("unexpected type %T for field secret_key", values[i]) + } else if value != nil { + _m.SecretKey = *value + } + case brokersecret.FieldAlgorithm: + if value, ok := values[i].(*sql.NullString); !ok { + return fmt.Errorf("unexpected type %T for field algorithm", values[i]) + } else if value.Valid { + _m.Algorithm = value.String + } + case brokersecret.FieldRotatedAt: + if value, ok := values[i].(*sql.NullTime); !ok { + return fmt.Errorf("unexpected type %T for field rotated_at", values[i]) + } else if value.Valid { + _m.RotatedAt = new(time.Time) + *_m.RotatedAt = value.Time + } + case brokersecret.FieldExpiresAt: + if value, ok := values[i].(*sql.NullTime); !ok { + return fmt.Errorf("unexpected type %T for field expires_at", values[i]) + } else if value.Valid { + _m.ExpiresAt = new(time.Time) + *_m.ExpiresAt = value.Time + } + case brokersecret.FieldStatus: + if value, ok := values[i].(*sql.NullString); !ok { + return fmt.Errorf("unexpected type %T for field status", values[i]) + } else if value.Valid { + _m.Status = value.String + } + case brokersecret.FieldCreated: + if value, ok := values[i].(*sql.NullTime); !ok { + return fmt.Errorf("unexpected type %T for field created", values[i]) + } else if value.Valid { + _m.Created = value.Time + } + default: + _m.selectValues.Set(columns[i], values[i]) + } + } + return nil +} + +// Value returns the ent.Value that was dynamically selected and assigned to the BrokerSecret. +// This includes values selected through modifiers, order, etc. +func (_m *BrokerSecret) Value(name string) (ent.Value, error) { + return _m.selectValues.Get(name) +} + +// Update returns a builder for updating this BrokerSecret. +// Note that you need to call BrokerSecret.Unwrap() before calling this method if this BrokerSecret +// was returned from a transaction, and the transaction was committed or rolled back. +func (_m *BrokerSecret) Update() *BrokerSecretUpdateOne { + return NewBrokerSecretClient(_m.config).UpdateOne(_m) +} + +// Unwrap unwraps the BrokerSecret entity that was returned from a transaction after it was closed, +// so that all future queries will be executed through the driver which created the transaction. +func (_m *BrokerSecret) Unwrap() *BrokerSecret { + _tx, ok := _m.config.driver.(*txDriver) + if !ok { + panic("ent: BrokerSecret is not a transactional entity") + } + _m.config.driver = _tx.drv + return _m +} + +// String implements the fmt.Stringer. +func (_m *BrokerSecret) String() string { + var builder strings.Builder + builder.WriteString("BrokerSecret(") + builder.WriteString(fmt.Sprintf("id=%v, ", _m.ID)) + builder.WriteString("secret_key=") + builder.WriteString(", ") + builder.WriteString("algorithm=") + builder.WriteString(_m.Algorithm) + builder.WriteString(", ") + if v := _m.RotatedAt; v != nil { + builder.WriteString("rotated_at=") + builder.WriteString(v.Format(time.ANSIC)) + } + builder.WriteString(", ") + if v := _m.ExpiresAt; v != nil { + builder.WriteString("expires_at=") + builder.WriteString(v.Format(time.ANSIC)) + } + builder.WriteString(", ") + builder.WriteString("status=") + builder.WriteString(_m.Status) + builder.WriteString(", ") + builder.WriteString("created=") + builder.WriteString(_m.Created.Format(time.ANSIC)) + builder.WriteByte(')') + return builder.String() +} + +// BrokerSecrets is a parsable slice of BrokerSecret. +type BrokerSecrets []*BrokerSecret diff --git a/pkg/ent/brokersecret/brokersecret.go b/pkg/ent/brokersecret/brokersecret.go new file mode 100644 index 000000000..f0a09b2c8 --- /dev/null +++ b/pkg/ent/brokersecret/brokersecret.go @@ -0,0 +1,95 @@ +// Code generated by ent, DO NOT EDIT. + +package brokersecret + +import ( + "time" + + "entgo.io/ent/dialect/sql" +) + +const ( + // Label holds the string label denoting the brokersecret type in the database. + Label = "broker_secret" + // FieldID holds the string denoting the id field in the database. + FieldID = "broker_id" + // FieldSecretKey holds the string denoting the secret_key field in the database. + FieldSecretKey = "secret_key" + // FieldAlgorithm holds the string denoting the algorithm field in the database. + FieldAlgorithm = "algorithm" + // FieldRotatedAt holds the string denoting the rotated_at field in the database. + FieldRotatedAt = "rotated_at" + // FieldExpiresAt holds the string denoting the expires_at field in the database. + FieldExpiresAt = "expires_at" + // FieldStatus holds the string denoting the status field in the database. + FieldStatus = "status" + // FieldCreated holds the string denoting the created field in the database. + FieldCreated = "created" + // Table holds the table name of the brokersecret in the database. + Table = "broker_secrets" +) + +// Columns holds all SQL columns for brokersecret fields. +var Columns = []string{ + FieldID, + FieldSecretKey, + FieldAlgorithm, + FieldRotatedAt, + FieldExpiresAt, + FieldStatus, + FieldCreated, +} + +// ValidColumn reports if the column name is valid (part of the table columns). +func ValidColumn(column string) bool { + for i := range Columns { + if column == Columns[i] { + return true + } + } + return false +} + +var ( + // SecretKeyValidator is a validator for the "secret_key" field. It is called by the builders before save. + SecretKeyValidator func([]byte) error + // DefaultAlgorithm holds the default value on creation for the "algorithm" field. + DefaultAlgorithm string + // DefaultStatus holds the default value on creation for the "status" field. + DefaultStatus string + // DefaultCreated holds the default value on creation for the "created" field. + DefaultCreated func() time.Time +) + +// OrderOption defines the ordering options for the BrokerSecret queries. +type OrderOption func(*sql.Selector) + +// ByID orders the results by the id field. +func ByID(opts ...sql.OrderTermOption) OrderOption { + return sql.OrderByField(FieldID, opts...).ToFunc() +} + +// ByAlgorithm orders the results by the algorithm field. +func ByAlgorithm(opts ...sql.OrderTermOption) OrderOption { + return sql.OrderByField(FieldAlgorithm, opts...).ToFunc() +} + +// ByRotatedAt orders the results by the rotated_at field. +func ByRotatedAt(opts ...sql.OrderTermOption) OrderOption { + return sql.OrderByField(FieldRotatedAt, opts...).ToFunc() +} + +// ByExpiresAt orders the results by the expires_at field. +func ByExpiresAt(opts ...sql.OrderTermOption) OrderOption { + return sql.OrderByField(FieldExpiresAt, opts...).ToFunc() +} + +// ByStatus orders the results by the status field. +func ByStatus(opts ...sql.OrderTermOption) OrderOption { + return sql.OrderByField(FieldStatus, opts...).ToFunc() +} + +// ByCreated orders the results by the created field. +func ByCreated(opts ...sql.OrderTermOption) OrderOption { + return sql.OrderByField(FieldCreated, opts...).ToFunc() +} diff --git a/pkg/ent/brokersecret/where.go b/pkg/ent/brokersecret/where.go new file mode 100644 index 000000000..c5e7edd74 --- /dev/null +++ b/pkg/ent/brokersecret/where.go @@ -0,0 +1,411 @@ +// Code generated by ent, DO NOT EDIT. + +package brokersecret + +import ( + "time" + + "entgo.io/ent/dialect/sql" + "github.com/GoogleCloudPlatform/scion/pkg/ent/predicate" + "github.com/google/uuid" +) + +// ID filters vertices based on their ID field. +func ID(id uuid.UUID) predicate.BrokerSecret { + return predicate.BrokerSecret(sql.FieldEQ(FieldID, id)) +} + +// IDEQ applies the EQ predicate on the ID field. +func IDEQ(id uuid.UUID) predicate.BrokerSecret { + return predicate.BrokerSecret(sql.FieldEQ(FieldID, id)) +} + +// IDNEQ applies the NEQ predicate on the ID field. +func IDNEQ(id uuid.UUID) predicate.BrokerSecret { + return predicate.BrokerSecret(sql.FieldNEQ(FieldID, id)) +} + +// IDIn applies the In predicate on the ID field. +func IDIn(ids ...uuid.UUID) predicate.BrokerSecret { + return predicate.BrokerSecret(sql.FieldIn(FieldID, ids...)) +} + +// IDNotIn applies the NotIn predicate on the ID field. +func IDNotIn(ids ...uuid.UUID) predicate.BrokerSecret { + return predicate.BrokerSecret(sql.FieldNotIn(FieldID, ids...)) +} + +// IDGT applies the GT predicate on the ID field. +func IDGT(id uuid.UUID) predicate.BrokerSecret { + return predicate.BrokerSecret(sql.FieldGT(FieldID, id)) +} + +// IDGTE applies the GTE predicate on the ID field. +func IDGTE(id uuid.UUID) predicate.BrokerSecret { + return predicate.BrokerSecret(sql.FieldGTE(FieldID, id)) +} + +// IDLT applies the LT predicate on the ID field. +func IDLT(id uuid.UUID) predicate.BrokerSecret { + return predicate.BrokerSecret(sql.FieldLT(FieldID, id)) +} + +// IDLTE applies the LTE predicate on the ID field. +func IDLTE(id uuid.UUID) predicate.BrokerSecret { + return predicate.BrokerSecret(sql.FieldLTE(FieldID, id)) +} + +// SecretKey applies equality check predicate on the "secret_key" field. It's identical to SecretKeyEQ. +func SecretKey(v []byte) predicate.BrokerSecret { + return predicate.BrokerSecret(sql.FieldEQ(FieldSecretKey, v)) +} + +// Algorithm applies equality check predicate on the "algorithm" field. It's identical to AlgorithmEQ. +func Algorithm(v string) predicate.BrokerSecret { + return predicate.BrokerSecret(sql.FieldEQ(FieldAlgorithm, v)) +} + +// RotatedAt applies equality check predicate on the "rotated_at" field. It's identical to RotatedAtEQ. +func RotatedAt(v time.Time) predicate.BrokerSecret { + return predicate.BrokerSecret(sql.FieldEQ(FieldRotatedAt, v)) +} + +// ExpiresAt applies equality check predicate on the "expires_at" field. It's identical to ExpiresAtEQ. +func ExpiresAt(v time.Time) predicate.BrokerSecret { + return predicate.BrokerSecret(sql.FieldEQ(FieldExpiresAt, v)) +} + +// Status applies equality check predicate on the "status" field. It's identical to StatusEQ. +func Status(v string) predicate.BrokerSecret { + return predicate.BrokerSecret(sql.FieldEQ(FieldStatus, v)) +} + +// Created applies equality check predicate on the "created" field. It's identical to CreatedEQ. +func Created(v time.Time) predicate.BrokerSecret { + return predicate.BrokerSecret(sql.FieldEQ(FieldCreated, v)) +} + +// SecretKeyEQ applies the EQ predicate on the "secret_key" field. +func SecretKeyEQ(v []byte) predicate.BrokerSecret { + return predicate.BrokerSecret(sql.FieldEQ(FieldSecretKey, v)) +} + +// SecretKeyNEQ applies the NEQ predicate on the "secret_key" field. +func SecretKeyNEQ(v []byte) predicate.BrokerSecret { + return predicate.BrokerSecret(sql.FieldNEQ(FieldSecretKey, v)) +} + +// SecretKeyIn applies the In predicate on the "secret_key" field. +func SecretKeyIn(vs ...[]byte) predicate.BrokerSecret { + return predicate.BrokerSecret(sql.FieldIn(FieldSecretKey, vs...)) +} + +// SecretKeyNotIn applies the NotIn predicate on the "secret_key" field. +func SecretKeyNotIn(vs ...[]byte) predicate.BrokerSecret { + return predicate.BrokerSecret(sql.FieldNotIn(FieldSecretKey, vs...)) +} + +// SecretKeyGT applies the GT predicate on the "secret_key" field. +func SecretKeyGT(v []byte) predicate.BrokerSecret { + return predicate.BrokerSecret(sql.FieldGT(FieldSecretKey, v)) +} + +// SecretKeyGTE applies the GTE predicate on the "secret_key" field. +func SecretKeyGTE(v []byte) predicate.BrokerSecret { + return predicate.BrokerSecret(sql.FieldGTE(FieldSecretKey, v)) +} + +// SecretKeyLT applies the LT predicate on the "secret_key" field. +func SecretKeyLT(v []byte) predicate.BrokerSecret { + return predicate.BrokerSecret(sql.FieldLT(FieldSecretKey, v)) +} + +// SecretKeyLTE applies the LTE predicate on the "secret_key" field. +func SecretKeyLTE(v []byte) predicate.BrokerSecret { + return predicate.BrokerSecret(sql.FieldLTE(FieldSecretKey, v)) +} + +// AlgorithmEQ applies the EQ predicate on the "algorithm" field. +func AlgorithmEQ(v string) predicate.BrokerSecret { + return predicate.BrokerSecret(sql.FieldEQ(FieldAlgorithm, v)) +} + +// AlgorithmNEQ applies the NEQ predicate on the "algorithm" field. +func AlgorithmNEQ(v string) predicate.BrokerSecret { + return predicate.BrokerSecret(sql.FieldNEQ(FieldAlgorithm, v)) +} + +// AlgorithmIn applies the In predicate on the "algorithm" field. +func AlgorithmIn(vs ...string) predicate.BrokerSecret { + return predicate.BrokerSecret(sql.FieldIn(FieldAlgorithm, vs...)) +} + +// AlgorithmNotIn applies the NotIn predicate on the "algorithm" field. +func AlgorithmNotIn(vs ...string) predicate.BrokerSecret { + return predicate.BrokerSecret(sql.FieldNotIn(FieldAlgorithm, vs...)) +} + +// AlgorithmGT applies the GT predicate on the "algorithm" field. +func AlgorithmGT(v string) predicate.BrokerSecret { + return predicate.BrokerSecret(sql.FieldGT(FieldAlgorithm, v)) +} + +// AlgorithmGTE applies the GTE predicate on the "algorithm" field. +func AlgorithmGTE(v string) predicate.BrokerSecret { + return predicate.BrokerSecret(sql.FieldGTE(FieldAlgorithm, v)) +} + +// AlgorithmLT applies the LT predicate on the "algorithm" field. +func AlgorithmLT(v string) predicate.BrokerSecret { + return predicate.BrokerSecret(sql.FieldLT(FieldAlgorithm, v)) +} + +// AlgorithmLTE applies the LTE predicate on the "algorithm" field. +func AlgorithmLTE(v string) predicate.BrokerSecret { + return predicate.BrokerSecret(sql.FieldLTE(FieldAlgorithm, v)) +} + +// AlgorithmContains applies the Contains predicate on the "algorithm" field. +func AlgorithmContains(v string) predicate.BrokerSecret { + return predicate.BrokerSecret(sql.FieldContains(FieldAlgorithm, v)) +} + +// AlgorithmHasPrefix applies the HasPrefix predicate on the "algorithm" field. +func AlgorithmHasPrefix(v string) predicate.BrokerSecret { + return predicate.BrokerSecret(sql.FieldHasPrefix(FieldAlgorithm, v)) +} + +// AlgorithmHasSuffix applies the HasSuffix predicate on the "algorithm" field. +func AlgorithmHasSuffix(v string) predicate.BrokerSecret { + return predicate.BrokerSecret(sql.FieldHasSuffix(FieldAlgorithm, v)) +} + +// AlgorithmEqualFold applies the EqualFold predicate on the "algorithm" field. +func AlgorithmEqualFold(v string) predicate.BrokerSecret { + return predicate.BrokerSecret(sql.FieldEqualFold(FieldAlgorithm, v)) +} + +// AlgorithmContainsFold applies the ContainsFold predicate on the "algorithm" field. +func AlgorithmContainsFold(v string) predicate.BrokerSecret { + return predicate.BrokerSecret(sql.FieldContainsFold(FieldAlgorithm, v)) +} + +// RotatedAtEQ applies the EQ predicate on the "rotated_at" field. +func RotatedAtEQ(v time.Time) predicate.BrokerSecret { + return predicate.BrokerSecret(sql.FieldEQ(FieldRotatedAt, v)) +} + +// RotatedAtNEQ applies the NEQ predicate on the "rotated_at" field. +func RotatedAtNEQ(v time.Time) predicate.BrokerSecret { + return predicate.BrokerSecret(sql.FieldNEQ(FieldRotatedAt, v)) +} + +// RotatedAtIn applies the In predicate on the "rotated_at" field. +func RotatedAtIn(vs ...time.Time) predicate.BrokerSecret { + return predicate.BrokerSecret(sql.FieldIn(FieldRotatedAt, vs...)) +} + +// RotatedAtNotIn applies the NotIn predicate on the "rotated_at" field. +func RotatedAtNotIn(vs ...time.Time) predicate.BrokerSecret { + return predicate.BrokerSecret(sql.FieldNotIn(FieldRotatedAt, vs...)) +} + +// RotatedAtGT applies the GT predicate on the "rotated_at" field. +func RotatedAtGT(v time.Time) predicate.BrokerSecret { + return predicate.BrokerSecret(sql.FieldGT(FieldRotatedAt, v)) +} + +// RotatedAtGTE applies the GTE predicate on the "rotated_at" field. +func RotatedAtGTE(v time.Time) predicate.BrokerSecret { + return predicate.BrokerSecret(sql.FieldGTE(FieldRotatedAt, v)) +} + +// RotatedAtLT applies the LT predicate on the "rotated_at" field. +func RotatedAtLT(v time.Time) predicate.BrokerSecret { + return predicate.BrokerSecret(sql.FieldLT(FieldRotatedAt, v)) +} + +// RotatedAtLTE applies the LTE predicate on the "rotated_at" field. +func RotatedAtLTE(v time.Time) predicate.BrokerSecret { + return predicate.BrokerSecret(sql.FieldLTE(FieldRotatedAt, v)) +} + +// RotatedAtIsNil applies the IsNil predicate on the "rotated_at" field. +func RotatedAtIsNil() predicate.BrokerSecret { + return predicate.BrokerSecret(sql.FieldIsNull(FieldRotatedAt)) +} + +// RotatedAtNotNil applies the NotNil predicate on the "rotated_at" field. +func RotatedAtNotNil() predicate.BrokerSecret { + return predicate.BrokerSecret(sql.FieldNotNull(FieldRotatedAt)) +} + +// ExpiresAtEQ applies the EQ predicate on the "expires_at" field. +func ExpiresAtEQ(v time.Time) predicate.BrokerSecret { + return predicate.BrokerSecret(sql.FieldEQ(FieldExpiresAt, v)) +} + +// ExpiresAtNEQ applies the NEQ predicate on the "expires_at" field. +func ExpiresAtNEQ(v time.Time) predicate.BrokerSecret { + return predicate.BrokerSecret(sql.FieldNEQ(FieldExpiresAt, v)) +} + +// ExpiresAtIn applies the In predicate on the "expires_at" field. +func ExpiresAtIn(vs ...time.Time) predicate.BrokerSecret { + return predicate.BrokerSecret(sql.FieldIn(FieldExpiresAt, vs...)) +} + +// ExpiresAtNotIn applies the NotIn predicate on the "expires_at" field. +func ExpiresAtNotIn(vs ...time.Time) predicate.BrokerSecret { + return predicate.BrokerSecret(sql.FieldNotIn(FieldExpiresAt, vs...)) +} + +// ExpiresAtGT applies the GT predicate on the "expires_at" field. +func ExpiresAtGT(v time.Time) predicate.BrokerSecret { + return predicate.BrokerSecret(sql.FieldGT(FieldExpiresAt, v)) +} + +// ExpiresAtGTE applies the GTE predicate on the "expires_at" field. +func ExpiresAtGTE(v time.Time) predicate.BrokerSecret { + return predicate.BrokerSecret(sql.FieldGTE(FieldExpiresAt, v)) +} + +// ExpiresAtLT applies the LT predicate on the "expires_at" field. +func ExpiresAtLT(v time.Time) predicate.BrokerSecret { + return predicate.BrokerSecret(sql.FieldLT(FieldExpiresAt, v)) +} + +// ExpiresAtLTE applies the LTE predicate on the "expires_at" field. +func ExpiresAtLTE(v time.Time) predicate.BrokerSecret { + return predicate.BrokerSecret(sql.FieldLTE(FieldExpiresAt, v)) +} + +// ExpiresAtIsNil applies the IsNil predicate on the "expires_at" field. +func ExpiresAtIsNil() predicate.BrokerSecret { + return predicate.BrokerSecret(sql.FieldIsNull(FieldExpiresAt)) +} + +// ExpiresAtNotNil applies the NotNil predicate on the "expires_at" field. +func ExpiresAtNotNil() predicate.BrokerSecret { + return predicate.BrokerSecret(sql.FieldNotNull(FieldExpiresAt)) +} + +// StatusEQ applies the EQ predicate on the "status" field. +func StatusEQ(v string) predicate.BrokerSecret { + return predicate.BrokerSecret(sql.FieldEQ(FieldStatus, v)) +} + +// StatusNEQ applies the NEQ predicate on the "status" field. +func StatusNEQ(v string) predicate.BrokerSecret { + return predicate.BrokerSecret(sql.FieldNEQ(FieldStatus, v)) +} + +// StatusIn applies the In predicate on the "status" field. +func StatusIn(vs ...string) predicate.BrokerSecret { + return predicate.BrokerSecret(sql.FieldIn(FieldStatus, vs...)) +} + +// StatusNotIn applies the NotIn predicate on the "status" field. +func StatusNotIn(vs ...string) predicate.BrokerSecret { + return predicate.BrokerSecret(sql.FieldNotIn(FieldStatus, vs...)) +} + +// StatusGT applies the GT predicate on the "status" field. +func StatusGT(v string) predicate.BrokerSecret { + return predicate.BrokerSecret(sql.FieldGT(FieldStatus, v)) +} + +// StatusGTE applies the GTE predicate on the "status" field. +func StatusGTE(v string) predicate.BrokerSecret { + return predicate.BrokerSecret(sql.FieldGTE(FieldStatus, v)) +} + +// StatusLT applies the LT predicate on the "status" field. +func StatusLT(v string) predicate.BrokerSecret { + return predicate.BrokerSecret(sql.FieldLT(FieldStatus, v)) +} + +// StatusLTE applies the LTE predicate on the "status" field. +func StatusLTE(v string) predicate.BrokerSecret { + return predicate.BrokerSecret(sql.FieldLTE(FieldStatus, v)) +} + +// StatusContains applies the Contains predicate on the "status" field. +func StatusContains(v string) predicate.BrokerSecret { + return predicate.BrokerSecret(sql.FieldContains(FieldStatus, v)) +} + +// StatusHasPrefix applies the HasPrefix predicate on the "status" field. +func StatusHasPrefix(v string) predicate.BrokerSecret { + return predicate.BrokerSecret(sql.FieldHasPrefix(FieldStatus, v)) +} + +// StatusHasSuffix applies the HasSuffix predicate on the "status" field. +func StatusHasSuffix(v string) predicate.BrokerSecret { + return predicate.BrokerSecret(sql.FieldHasSuffix(FieldStatus, v)) +} + +// StatusEqualFold applies the EqualFold predicate on the "status" field. +func StatusEqualFold(v string) predicate.BrokerSecret { + return predicate.BrokerSecret(sql.FieldEqualFold(FieldStatus, v)) +} + +// StatusContainsFold applies the ContainsFold predicate on the "status" field. +func StatusContainsFold(v string) predicate.BrokerSecret { + return predicate.BrokerSecret(sql.FieldContainsFold(FieldStatus, v)) +} + +// CreatedEQ applies the EQ predicate on the "created" field. +func CreatedEQ(v time.Time) predicate.BrokerSecret { + return predicate.BrokerSecret(sql.FieldEQ(FieldCreated, v)) +} + +// CreatedNEQ applies the NEQ predicate on the "created" field. +func CreatedNEQ(v time.Time) predicate.BrokerSecret { + return predicate.BrokerSecret(sql.FieldNEQ(FieldCreated, v)) +} + +// CreatedIn applies the In predicate on the "created" field. +func CreatedIn(vs ...time.Time) predicate.BrokerSecret { + return predicate.BrokerSecret(sql.FieldIn(FieldCreated, vs...)) +} + +// CreatedNotIn applies the NotIn predicate on the "created" field. +func CreatedNotIn(vs ...time.Time) predicate.BrokerSecret { + return predicate.BrokerSecret(sql.FieldNotIn(FieldCreated, vs...)) +} + +// CreatedGT applies the GT predicate on the "created" field. +func CreatedGT(v time.Time) predicate.BrokerSecret { + return predicate.BrokerSecret(sql.FieldGT(FieldCreated, v)) +} + +// CreatedGTE applies the GTE predicate on the "created" field. +func CreatedGTE(v time.Time) predicate.BrokerSecret { + return predicate.BrokerSecret(sql.FieldGTE(FieldCreated, v)) +} + +// CreatedLT applies the LT predicate on the "created" field. +func CreatedLT(v time.Time) predicate.BrokerSecret { + return predicate.BrokerSecret(sql.FieldLT(FieldCreated, v)) +} + +// CreatedLTE applies the LTE predicate on the "created" field. +func CreatedLTE(v time.Time) predicate.BrokerSecret { + return predicate.BrokerSecret(sql.FieldLTE(FieldCreated, v)) +} + +// And groups predicates with the AND operator between them. +func And(predicates ...predicate.BrokerSecret) predicate.BrokerSecret { + return predicate.BrokerSecret(sql.AndPredicates(predicates...)) +} + +// Or groups predicates with the OR operator between them. +func Or(predicates ...predicate.BrokerSecret) predicate.BrokerSecret { + return predicate.BrokerSecret(sql.OrPredicates(predicates...)) +} + +// Not applies the not operator on the given predicate. +func Not(p predicate.BrokerSecret) predicate.BrokerSecret { + return predicate.BrokerSecret(sql.NotPredicates(p)) +} diff --git a/pkg/ent/brokersecret_create.go b/pkg/ent/brokersecret_create.go new file mode 100644 index 000000000..cd390921a --- /dev/null +++ b/pkg/ent/brokersecret_create.go @@ -0,0 +1,819 @@ +// Code generated by ent, DO NOT EDIT. + +package ent + +import ( + "context" + "errors" + "fmt" + "time" + + "entgo.io/ent/dialect" + "entgo.io/ent/dialect/sql" + "entgo.io/ent/dialect/sql/sqlgraph" + "entgo.io/ent/schema/field" + "github.com/GoogleCloudPlatform/scion/pkg/ent/brokersecret" + "github.com/google/uuid" +) + +// BrokerSecretCreate is the builder for creating a BrokerSecret entity. +type BrokerSecretCreate struct { + config + mutation *BrokerSecretMutation + hooks []Hook + conflict []sql.ConflictOption +} + +// SetSecretKey sets the "secret_key" field. +func (_c *BrokerSecretCreate) SetSecretKey(v []byte) *BrokerSecretCreate { + _c.mutation.SetSecretKey(v) + return _c +} + +// SetAlgorithm sets the "algorithm" field. +func (_c *BrokerSecretCreate) SetAlgorithm(v string) *BrokerSecretCreate { + _c.mutation.SetAlgorithm(v) + return _c +} + +// SetNillableAlgorithm sets the "algorithm" field if the given value is not nil. +func (_c *BrokerSecretCreate) SetNillableAlgorithm(v *string) *BrokerSecretCreate { + if v != nil { + _c.SetAlgorithm(*v) + } + return _c +} + +// SetRotatedAt sets the "rotated_at" field. +func (_c *BrokerSecretCreate) SetRotatedAt(v time.Time) *BrokerSecretCreate { + _c.mutation.SetRotatedAt(v) + return _c +} + +// SetNillableRotatedAt sets the "rotated_at" field if the given value is not nil. +func (_c *BrokerSecretCreate) SetNillableRotatedAt(v *time.Time) *BrokerSecretCreate { + if v != nil { + _c.SetRotatedAt(*v) + } + return _c +} + +// SetExpiresAt sets the "expires_at" field. +func (_c *BrokerSecretCreate) SetExpiresAt(v time.Time) *BrokerSecretCreate { + _c.mutation.SetExpiresAt(v) + return _c +} + +// SetNillableExpiresAt sets the "expires_at" field if the given value is not nil. +func (_c *BrokerSecretCreate) SetNillableExpiresAt(v *time.Time) *BrokerSecretCreate { + if v != nil { + _c.SetExpiresAt(*v) + } + return _c +} + +// SetStatus sets the "status" field. +func (_c *BrokerSecretCreate) SetStatus(v string) *BrokerSecretCreate { + _c.mutation.SetStatus(v) + return _c +} + +// SetNillableStatus sets the "status" field if the given value is not nil. +func (_c *BrokerSecretCreate) SetNillableStatus(v *string) *BrokerSecretCreate { + if v != nil { + _c.SetStatus(*v) + } + return _c +} + +// SetCreated sets the "created" field. +func (_c *BrokerSecretCreate) SetCreated(v time.Time) *BrokerSecretCreate { + _c.mutation.SetCreated(v) + return _c +} + +// SetNillableCreated sets the "created" field if the given value is not nil. +func (_c *BrokerSecretCreate) SetNillableCreated(v *time.Time) *BrokerSecretCreate { + if v != nil { + _c.SetCreated(*v) + } + return _c +} + +// SetID sets the "id" field. +func (_c *BrokerSecretCreate) SetID(v uuid.UUID) *BrokerSecretCreate { + _c.mutation.SetID(v) + return _c +} + +// Mutation returns the BrokerSecretMutation object of the builder. +func (_c *BrokerSecretCreate) Mutation() *BrokerSecretMutation { + return _c.mutation +} + +// Save creates the BrokerSecret in the database. +func (_c *BrokerSecretCreate) Save(ctx context.Context) (*BrokerSecret, error) { + _c.defaults() + return withHooks(ctx, _c.sqlSave, _c.mutation, _c.hooks) +} + +// SaveX calls Save and panics if Save returns an error. +func (_c *BrokerSecretCreate) SaveX(ctx context.Context) *BrokerSecret { + v, err := _c.Save(ctx) + if err != nil { + panic(err) + } + return v +} + +// Exec executes the query. +func (_c *BrokerSecretCreate) Exec(ctx context.Context) error { + _, err := _c.Save(ctx) + return err +} + +// ExecX is like Exec, but panics if an error occurs. +func (_c *BrokerSecretCreate) ExecX(ctx context.Context) { + if err := _c.Exec(ctx); err != nil { + panic(err) + } +} + +// defaults sets the default values of the builder before save. +func (_c *BrokerSecretCreate) defaults() { + if _, ok := _c.mutation.Algorithm(); !ok { + v := brokersecret.DefaultAlgorithm + _c.mutation.SetAlgorithm(v) + } + if _, ok := _c.mutation.Status(); !ok { + v := brokersecret.DefaultStatus + _c.mutation.SetStatus(v) + } + if _, ok := _c.mutation.Created(); !ok { + v := brokersecret.DefaultCreated() + _c.mutation.SetCreated(v) + } +} + +// check runs all checks and user-defined validators on the builder. +func (_c *BrokerSecretCreate) check() error { + if _, ok := _c.mutation.SecretKey(); !ok { + return &ValidationError{Name: "secret_key", err: errors.New(`ent: missing required field "BrokerSecret.secret_key"`)} + } + if v, ok := _c.mutation.SecretKey(); ok { + if err := brokersecret.SecretKeyValidator(v); err != nil { + return &ValidationError{Name: "secret_key", err: fmt.Errorf(`ent: validator failed for field "BrokerSecret.secret_key": %w`, err)} + } + } + if _, ok := _c.mutation.Algorithm(); !ok { + return &ValidationError{Name: "algorithm", err: errors.New(`ent: missing required field "BrokerSecret.algorithm"`)} + } + if _, ok := _c.mutation.Status(); !ok { + return &ValidationError{Name: "status", err: errors.New(`ent: missing required field "BrokerSecret.status"`)} + } + if _, ok := _c.mutation.Created(); !ok { + return &ValidationError{Name: "created", err: errors.New(`ent: missing required field "BrokerSecret.created"`)} + } + return nil +} + +func (_c *BrokerSecretCreate) sqlSave(ctx context.Context) (*BrokerSecret, error) { + if err := _c.check(); err != nil { + return nil, err + } + _node, _spec := _c.createSpec() + if err := sqlgraph.CreateNode(ctx, _c.driver, _spec); err != nil { + if sqlgraph.IsConstraintError(err) { + err = &ConstraintError{msg: err.Error(), wrap: err} + } + return nil, err + } + if _spec.ID.Value != nil { + if id, ok := _spec.ID.Value.(*uuid.UUID); ok { + _node.ID = *id + } else if err := _node.ID.Scan(_spec.ID.Value); err != nil { + return nil, err + } + } + _c.mutation.id = &_node.ID + _c.mutation.done = true + return _node, nil +} + +func (_c *BrokerSecretCreate) createSpec() (*BrokerSecret, *sqlgraph.CreateSpec) { + var ( + _node = &BrokerSecret{config: _c.config} + _spec = sqlgraph.NewCreateSpec(brokersecret.Table, sqlgraph.NewFieldSpec(brokersecret.FieldID, field.TypeUUID)) + ) + _spec.OnConflict = _c.conflict + if id, ok := _c.mutation.ID(); ok { + _node.ID = id + _spec.ID.Value = &id + } + if value, ok := _c.mutation.SecretKey(); ok { + _spec.SetField(brokersecret.FieldSecretKey, field.TypeBytes, value) + _node.SecretKey = value + } + if value, ok := _c.mutation.Algorithm(); ok { + _spec.SetField(brokersecret.FieldAlgorithm, field.TypeString, value) + _node.Algorithm = value + } + if value, ok := _c.mutation.RotatedAt(); ok { + _spec.SetField(brokersecret.FieldRotatedAt, field.TypeTime, value) + _node.RotatedAt = &value + } + if value, ok := _c.mutation.ExpiresAt(); ok { + _spec.SetField(brokersecret.FieldExpiresAt, field.TypeTime, value) + _node.ExpiresAt = &value + } + if value, ok := _c.mutation.Status(); ok { + _spec.SetField(brokersecret.FieldStatus, field.TypeString, value) + _node.Status = value + } + if value, ok := _c.mutation.Created(); ok { + _spec.SetField(brokersecret.FieldCreated, field.TypeTime, value) + _node.Created = value + } + return _node, _spec +} + +// OnConflict allows configuring the `ON CONFLICT` / `ON DUPLICATE KEY` clause +// of the `INSERT` statement. For example: +// +// client.BrokerSecret.Create(). +// SetSecretKey(v). +// OnConflict( +// // Update the row with the new values +// // the was proposed for insertion. +// sql.ResolveWithNewValues(), +// ). +// // Override some of the fields with custom +// // update values. +// Update(func(u *ent.BrokerSecretUpsert) { +// SetSecretKey(v+v). +// }). +// Exec(ctx) +func (_c *BrokerSecretCreate) OnConflict(opts ...sql.ConflictOption) *BrokerSecretUpsertOne { + _c.conflict = opts + return &BrokerSecretUpsertOne{ + create: _c, + } +} + +// OnConflictColumns calls `OnConflict` and configures the columns +// as conflict target. Using this option is equivalent to using: +// +// client.BrokerSecret.Create(). +// OnConflict(sql.ConflictColumns(columns...)). +// Exec(ctx) +func (_c *BrokerSecretCreate) OnConflictColumns(columns ...string) *BrokerSecretUpsertOne { + _c.conflict = append(_c.conflict, sql.ConflictColumns(columns...)) + return &BrokerSecretUpsertOne{ + create: _c, + } +} + +type ( + // BrokerSecretUpsertOne is the builder for "upsert"-ing + // one BrokerSecret node. + BrokerSecretUpsertOne struct { + create *BrokerSecretCreate + } + + // BrokerSecretUpsert is the "OnConflict" setter. + BrokerSecretUpsert struct { + *sql.UpdateSet + } +) + +// SetSecretKey sets the "secret_key" field. +func (u *BrokerSecretUpsert) SetSecretKey(v []byte) *BrokerSecretUpsert { + u.Set(brokersecret.FieldSecretKey, v) + return u +} + +// UpdateSecretKey sets the "secret_key" field to the value that was provided on create. +func (u *BrokerSecretUpsert) UpdateSecretKey() *BrokerSecretUpsert { + u.SetExcluded(brokersecret.FieldSecretKey) + return u +} + +// SetAlgorithm sets the "algorithm" field. +func (u *BrokerSecretUpsert) SetAlgorithm(v string) *BrokerSecretUpsert { + u.Set(brokersecret.FieldAlgorithm, v) + return u +} + +// UpdateAlgorithm sets the "algorithm" field to the value that was provided on create. +func (u *BrokerSecretUpsert) UpdateAlgorithm() *BrokerSecretUpsert { + u.SetExcluded(brokersecret.FieldAlgorithm) + return u +} + +// SetRotatedAt sets the "rotated_at" field. +func (u *BrokerSecretUpsert) SetRotatedAt(v time.Time) *BrokerSecretUpsert { + u.Set(brokersecret.FieldRotatedAt, v) + return u +} + +// UpdateRotatedAt sets the "rotated_at" field to the value that was provided on create. +func (u *BrokerSecretUpsert) UpdateRotatedAt() *BrokerSecretUpsert { + u.SetExcluded(brokersecret.FieldRotatedAt) + return u +} + +// ClearRotatedAt clears the value of the "rotated_at" field. +func (u *BrokerSecretUpsert) ClearRotatedAt() *BrokerSecretUpsert { + u.SetNull(brokersecret.FieldRotatedAt) + return u +} + +// SetExpiresAt sets the "expires_at" field. +func (u *BrokerSecretUpsert) SetExpiresAt(v time.Time) *BrokerSecretUpsert { + u.Set(brokersecret.FieldExpiresAt, v) + return u +} + +// UpdateExpiresAt sets the "expires_at" field to the value that was provided on create. +func (u *BrokerSecretUpsert) UpdateExpiresAt() *BrokerSecretUpsert { + u.SetExcluded(brokersecret.FieldExpiresAt) + return u +} + +// ClearExpiresAt clears the value of the "expires_at" field. +func (u *BrokerSecretUpsert) ClearExpiresAt() *BrokerSecretUpsert { + u.SetNull(brokersecret.FieldExpiresAt) + return u +} + +// SetStatus sets the "status" field. +func (u *BrokerSecretUpsert) SetStatus(v string) *BrokerSecretUpsert { + u.Set(brokersecret.FieldStatus, v) + return u +} + +// UpdateStatus sets the "status" field to the value that was provided on create. +func (u *BrokerSecretUpsert) UpdateStatus() *BrokerSecretUpsert { + u.SetExcluded(brokersecret.FieldStatus) + return u +} + +// UpdateNewValues updates the mutable fields using the new values that were set on create except the ID field. +// Using this option is equivalent to using: +// +// client.BrokerSecret.Create(). +// OnConflict( +// sql.ResolveWithNewValues(), +// sql.ResolveWith(func(u *sql.UpdateSet) { +// u.SetIgnore(brokersecret.FieldID) +// }), +// ). +// Exec(ctx) +func (u *BrokerSecretUpsertOne) UpdateNewValues() *BrokerSecretUpsertOne { + u.create.conflict = append(u.create.conflict, sql.ResolveWithNewValues()) + u.create.conflict = append(u.create.conflict, sql.ResolveWith(func(s *sql.UpdateSet) { + if _, exists := u.create.mutation.ID(); exists { + s.SetIgnore(brokersecret.FieldID) + } + if _, exists := u.create.mutation.Created(); exists { + s.SetIgnore(brokersecret.FieldCreated) + } + })) + return u +} + +// Ignore sets each column to itself in case of conflict. +// Using this option is equivalent to using: +// +// client.BrokerSecret.Create(). +// OnConflict(sql.ResolveWithIgnore()). +// Exec(ctx) +func (u *BrokerSecretUpsertOne) Ignore() *BrokerSecretUpsertOne { + u.create.conflict = append(u.create.conflict, sql.ResolveWithIgnore()) + return u +} + +// DoNothing configures the conflict_action to `DO NOTHING`. +// Supported only by SQLite and PostgreSQL. +func (u *BrokerSecretUpsertOne) DoNothing() *BrokerSecretUpsertOne { + u.create.conflict = append(u.create.conflict, sql.DoNothing()) + return u +} + +// Update allows overriding fields `UPDATE` values. See the BrokerSecretCreate.OnConflict +// documentation for more info. +func (u *BrokerSecretUpsertOne) Update(set func(*BrokerSecretUpsert)) *BrokerSecretUpsertOne { + u.create.conflict = append(u.create.conflict, sql.ResolveWith(func(update *sql.UpdateSet) { + set(&BrokerSecretUpsert{UpdateSet: update}) + })) + return u +} + +// SetSecretKey sets the "secret_key" field. +func (u *BrokerSecretUpsertOne) SetSecretKey(v []byte) *BrokerSecretUpsertOne { + return u.Update(func(s *BrokerSecretUpsert) { + s.SetSecretKey(v) + }) +} + +// UpdateSecretKey sets the "secret_key" field to the value that was provided on create. +func (u *BrokerSecretUpsertOne) UpdateSecretKey() *BrokerSecretUpsertOne { + return u.Update(func(s *BrokerSecretUpsert) { + s.UpdateSecretKey() + }) +} + +// SetAlgorithm sets the "algorithm" field. +func (u *BrokerSecretUpsertOne) SetAlgorithm(v string) *BrokerSecretUpsertOne { + return u.Update(func(s *BrokerSecretUpsert) { + s.SetAlgorithm(v) + }) +} + +// UpdateAlgorithm sets the "algorithm" field to the value that was provided on create. +func (u *BrokerSecretUpsertOne) UpdateAlgorithm() *BrokerSecretUpsertOne { + return u.Update(func(s *BrokerSecretUpsert) { + s.UpdateAlgorithm() + }) +} + +// SetRotatedAt sets the "rotated_at" field. +func (u *BrokerSecretUpsertOne) SetRotatedAt(v time.Time) *BrokerSecretUpsertOne { + return u.Update(func(s *BrokerSecretUpsert) { + s.SetRotatedAt(v) + }) +} + +// UpdateRotatedAt sets the "rotated_at" field to the value that was provided on create. +func (u *BrokerSecretUpsertOne) UpdateRotatedAt() *BrokerSecretUpsertOne { + return u.Update(func(s *BrokerSecretUpsert) { + s.UpdateRotatedAt() + }) +} + +// ClearRotatedAt clears the value of the "rotated_at" field. +func (u *BrokerSecretUpsertOne) ClearRotatedAt() *BrokerSecretUpsertOne { + return u.Update(func(s *BrokerSecretUpsert) { + s.ClearRotatedAt() + }) +} + +// SetExpiresAt sets the "expires_at" field. +func (u *BrokerSecretUpsertOne) SetExpiresAt(v time.Time) *BrokerSecretUpsertOne { + return u.Update(func(s *BrokerSecretUpsert) { + s.SetExpiresAt(v) + }) +} + +// UpdateExpiresAt sets the "expires_at" field to the value that was provided on create. +func (u *BrokerSecretUpsertOne) UpdateExpiresAt() *BrokerSecretUpsertOne { + return u.Update(func(s *BrokerSecretUpsert) { + s.UpdateExpiresAt() + }) +} + +// ClearExpiresAt clears the value of the "expires_at" field. +func (u *BrokerSecretUpsertOne) ClearExpiresAt() *BrokerSecretUpsertOne { + return u.Update(func(s *BrokerSecretUpsert) { + s.ClearExpiresAt() + }) +} + +// SetStatus sets the "status" field. +func (u *BrokerSecretUpsertOne) SetStatus(v string) *BrokerSecretUpsertOne { + return u.Update(func(s *BrokerSecretUpsert) { + s.SetStatus(v) + }) +} + +// UpdateStatus sets the "status" field to the value that was provided on create. +func (u *BrokerSecretUpsertOne) UpdateStatus() *BrokerSecretUpsertOne { + return u.Update(func(s *BrokerSecretUpsert) { + s.UpdateStatus() + }) +} + +// Exec executes the query. +func (u *BrokerSecretUpsertOne) Exec(ctx context.Context) error { + if len(u.create.conflict) == 0 { + return errors.New("ent: missing options for BrokerSecretCreate.OnConflict") + } + return u.create.Exec(ctx) +} + +// ExecX is like Exec, but panics if an error occurs. +func (u *BrokerSecretUpsertOne) ExecX(ctx context.Context) { + if err := u.create.Exec(ctx); err != nil { + panic(err) + } +} + +// Exec executes the UPSERT query and returns the inserted/updated ID. +func (u *BrokerSecretUpsertOne) ID(ctx context.Context) (id uuid.UUID, err error) { + if u.create.driver.Dialect() == dialect.MySQL { + // In case of "ON CONFLICT", there is no way to get back non-numeric ID + // fields from the database since MySQL does not support the RETURNING clause. + return id, errors.New("ent: BrokerSecretUpsertOne.ID is not supported by MySQL driver. Use BrokerSecretUpsertOne.Exec instead") + } + node, err := u.create.Save(ctx) + if err != nil { + return id, err + } + return node.ID, nil +} + +// IDX is like ID, but panics if an error occurs. +func (u *BrokerSecretUpsertOne) IDX(ctx context.Context) uuid.UUID { + id, err := u.ID(ctx) + if err != nil { + panic(err) + } + return id +} + +// BrokerSecretCreateBulk is the builder for creating many BrokerSecret entities in bulk. +type BrokerSecretCreateBulk struct { + config + err error + builders []*BrokerSecretCreate + conflict []sql.ConflictOption +} + +// Save creates the BrokerSecret entities in the database. +func (_c *BrokerSecretCreateBulk) Save(ctx context.Context) ([]*BrokerSecret, error) { + if _c.err != nil { + return nil, _c.err + } + specs := make([]*sqlgraph.CreateSpec, len(_c.builders)) + nodes := make([]*BrokerSecret, len(_c.builders)) + mutators := make([]Mutator, len(_c.builders)) + for i := range _c.builders { + func(i int, root context.Context) { + builder := _c.builders[i] + builder.defaults() + var mut Mutator = MutateFunc(func(ctx context.Context, m Mutation) (Value, error) { + mutation, ok := m.(*BrokerSecretMutation) + if !ok { + return nil, fmt.Errorf("unexpected mutation type %T", m) + } + if err := builder.check(); err != nil { + return nil, err + } + builder.mutation = mutation + var err error + nodes[i], specs[i] = builder.createSpec() + if i < len(mutators)-1 { + _, err = mutators[i+1].Mutate(root, _c.builders[i+1].mutation) + } else { + spec := &sqlgraph.BatchCreateSpec{Nodes: specs} + spec.OnConflict = _c.conflict + // Invoke the actual operation on the latest mutation in the chain. + if err = sqlgraph.BatchCreate(ctx, _c.driver, spec); err != nil { + if sqlgraph.IsConstraintError(err) { + err = &ConstraintError{msg: err.Error(), wrap: err} + } + } + } + if err != nil { + return nil, err + } + mutation.id = &nodes[i].ID + mutation.done = true + return nodes[i], nil + }) + for i := len(builder.hooks) - 1; i >= 0; i-- { + mut = builder.hooks[i](mut) + } + mutators[i] = mut + }(i, ctx) + } + if len(mutators) > 0 { + if _, err := mutators[0].Mutate(ctx, _c.builders[0].mutation); err != nil { + return nil, err + } + } + return nodes, nil +} + +// SaveX is like Save, but panics if an error occurs. +func (_c *BrokerSecretCreateBulk) SaveX(ctx context.Context) []*BrokerSecret { + v, err := _c.Save(ctx) + if err != nil { + panic(err) + } + return v +} + +// Exec executes the query. +func (_c *BrokerSecretCreateBulk) Exec(ctx context.Context) error { + _, err := _c.Save(ctx) + return err +} + +// ExecX is like Exec, but panics if an error occurs. +func (_c *BrokerSecretCreateBulk) ExecX(ctx context.Context) { + if err := _c.Exec(ctx); err != nil { + panic(err) + } +} + +// OnConflict allows configuring the `ON CONFLICT` / `ON DUPLICATE KEY` clause +// of the `INSERT` statement. For example: +// +// client.BrokerSecret.CreateBulk(builders...). +// OnConflict( +// // Update the row with the new values +// // the was proposed for insertion. +// sql.ResolveWithNewValues(), +// ). +// // Override some of the fields with custom +// // update values. +// Update(func(u *ent.BrokerSecretUpsert) { +// SetSecretKey(v+v). +// }). +// Exec(ctx) +func (_c *BrokerSecretCreateBulk) OnConflict(opts ...sql.ConflictOption) *BrokerSecretUpsertBulk { + _c.conflict = opts + return &BrokerSecretUpsertBulk{ + create: _c, + } +} + +// OnConflictColumns calls `OnConflict` and configures the columns +// as conflict target. Using this option is equivalent to using: +// +// client.BrokerSecret.Create(). +// OnConflict(sql.ConflictColumns(columns...)). +// Exec(ctx) +func (_c *BrokerSecretCreateBulk) OnConflictColumns(columns ...string) *BrokerSecretUpsertBulk { + _c.conflict = append(_c.conflict, sql.ConflictColumns(columns...)) + return &BrokerSecretUpsertBulk{ + create: _c, + } +} + +// BrokerSecretUpsertBulk is the builder for "upsert"-ing +// a bulk of BrokerSecret nodes. +type BrokerSecretUpsertBulk struct { + create *BrokerSecretCreateBulk +} + +// UpdateNewValues updates the mutable fields using the new values that +// were set on create. Using this option is equivalent to using: +// +// client.BrokerSecret.Create(). +// OnConflict( +// sql.ResolveWithNewValues(), +// sql.ResolveWith(func(u *sql.UpdateSet) { +// u.SetIgnore(brokersecret.FieldID) +// }), +// ). +// Exec(ctx) +func (u *BrokerSecretUpsertBulk) UpdateNewValues() *BrokerSecretUpsertBulk { + u.create.conflict = append(u.create.conflict, sql.ResolveWithNewValues()) + u.create.conflict = append(u.create.conflict, sql.ResolveWith(func(s *sql.UpdateSet) { + for _, b := range u.create.builders { + if _, exists := b.mutation.ID(); exists { + s.SetIgnore(brokersecret.FieldID) + } + if _, exists := b.mutation.Created(); exists { + s.SetIgnore(brokersecret.FieldCreated) + } + } + })) + return u +} + +// Ignore sets each column to itself in case of conflict. +// Using this option is equivalent to using: +// +// client.BrokerSecret.Create(). +// OnConflict(sql.ResolveWithIgnore()). +// Exec(ctx) +func (u *BrokerSecretUpsertBulk) Ignore() *BrokerSecretUpsertBulk { + u.create.conflict = append(u.create.conflict, sql.ResolveWithIgnore()) + return u +} + +// DoNothing configures the conflict_action to `DO NOTHING`. +// Supported only by SQLite and PostgreSQL. +func (u *BrokerSecretUpsertBulk) DoNothing() *BrokerSecretUpsertBulk { + u.create.conflict = append(u.create.conflict, sql.DoNothing()) + return u +} + +// Update allows overriding fields `UPDATE` values. See the BrokerSecretCreateBulk.OnConflict +// documentation for more info. +func (u *BrokerSecretUpsertBulk) Update(set func(*BrokerSecretUpsert)) *BrokerSecretUpsertBulk { + u.create.conflict = append(u.create.conflict, sql.ResolveWith(func(update *sql.UpdateSet) { + set(&BrokerSecretUpsert{UpdateSet: update}) + })) + return u +} + +// SetSecretKey sets the "secret_key" field. +func (u *BrokerSecretUpsertBulk) SetSecretKey(v []byte) *BrokerSecretUpsertBulk { + return u.Update(func(s *BrokerSecretUpsert) { + s.SetSecretKey(v) + }) +} + +// UpdateSecretKey sets the "secret_key" field to the value that was provided on create. +func (u *BrokerSecretUpsertBulk) UpdateSecretKey() *BrokerSecretUpsertBulk { + return u.Update(func(s *BrokerSecretUpsert) { + s.UpdateSecretKey() + }) +} + +// SetAlgorithm sets the "algorithm" field. +func (u *BrokerSecretUpsertBulk) SetAlgorithm(v string) *BrokerSecretUpsertBulk { + return u.Update(func(s *BrokerSecretUpsert) { + s.SetAlgorithm(v) + }) +} + +// UpdateAlgorithm sets the "algorithm" field to the value that was provided on create. +func (u *BrokerSecretUpsertBulk) UpdateAlgorithm() *BrokerSecretUpsertBulk { + return u.Update(func(s *BrokerSecretUpsert) { + s.UpdateAlgorithm() + }) +} + +// SetRotatedAt sets the "rotated_at" field. +func (u *BrokerSecretUpsertBulk) SetRotatedAt(v time.Time) *BrokerSecretUpsertBulk { + return u.Update(func(s *BrokerSecretUpsert) { + s.SetRotatedAt(v) + }) +} + +// UpdateRotatedAt sets the "rotated_at" field to the value that was provided on create. +func (u *BrokerSecretUpsertBulk) UpdateRotatedAt() *BrokerSecretUpsertBulk { + return u.Update(func(s *BrokerSecretUpsert) { + s.UpdateRotatedAt() + }) +} + +// ClearRotatedAt clears the value of the "rotated_at" field. +func (u *BrokerSecretUpsertBulk) ClearRotatedAt() *BrokerSecretUpsertBulk { + return u.Update(func(s *BrokerSecretUpsert) { + s.ClearRotatedAt() + }) +} + +// SetExpiresAt sets the "expires_at" field. +func (u *BrokerSecretUpsertBulk) SetExpiresAt(v time.Time) *BrokerSecretUpsertBulk { + return u.Update(func(s *BrokerSecretUpsert) { + s.SetExpiresAt(v) + }) +} + +// UpdateExpiresAt sets the "expires_at" field to the value that was provided on create. +func (u *BrokerSecretUpsertBulk) UpdateExpiresAt() *BrokerSecretUpsertBulk { + return u.Update(func(s *BrokerSecretUpsert) { + s.UpdateExpiresAt() + }) +} + +// ClearExpiresAt clears the value of the "expires_at" field. +func (u *BrokerSecretUpsertBulk) ClearExpiresAt() *BrokerSecretUpsertBulk { + return u.Update(func(s *BrokerSecretUpsert) { + s.ClearExpiresAt() + }) +} + +// SetStatus sets the "status" field. +func (u *BrokerSecretUpsertBulk) SetStatus(v string) *BrokerSecretUpsertBulk { + return u.Update(func(s *BrokerSecretUpsert) { + s.SetStatus(v) + }) +} + +// UpdateStatus sets the "status" field to the value that was provided on create. +func (u *BrokerSecretUpsertBulk) UpdateStatus() *BrokerSecretUpsertBulk { + return u.Update(func(s *BrokerSecretUpsert) { + s.UpdateStatus() + }) +} + +// Exec executes the query. +func (u *BrokerSecretUpsertBulk) Exec(ctx context.Context) error { + if u.create.err != nil { + return u.create.err + } + for i, b := range u.create.builders { + if len(b.conflict) != 0 { + return fmt.Errorf("ent: OnConflict was set for builder %d. Set it on the BrokerSecretCreateBulk instead", i) + } + } + if len(u.create.conflict) == 0 { + return errors.New("ent: missing options for BrokerSecretCreateBulk.OnConflict") + } + return u.create.Exec(ctx) +} + +// ExecX is like Exec, but panics if an error occurs. +func (u *BrokerSecretUpsertBulk) ExecX(ctx context.Context) { + if err := u.create.Exec(ctx); err != nil { + panic(err) + } +} diff --git a/pkg/ent/brokersecret_delete.go b/pkg/ent/brokersecret_delete.go new file mode 100644 index 000000000..853f2dad1 --- /dev/null +++ b/pkg/ent/brokersecret_delete.go @@ -0,0 +1,88 @@ +// Code generated by ent, DO NOT EDIT. + +package ent + +import ( + "context" + + "entgo.io/ent/dialect/sql" + "entgo.io/ent/dialect/sql/sqlgraph" + "entgo.io/ent/schema/field" + "github.com/GoogleCloudPlatform/scion/pkg/ent/brokersecret" + "github.com/GoogleCloudPlatform/scion/pkg/ent/predicate" +) + +// BrokerSecretDelete is the builder for deleting a BrokerSecret entity. +type BrokerSecretDelete struct { + config + hooks []Hook + mutation *BrokerSecretMutation +} + +// Where appends a list predicates to the BrokerSecretDelete builder. +func (_d *BrokerSecretDelete) Where(ps ...predicate.BrokerSecret) *BrokerSecretDelete { + _d.mutation.Where(ps...) + return _d +} + +// Exec executes the deletion query and returns how many vertices were deleted. +func (_d *BrokerSecretDelete) Exec(ctx context.Context) (int, error) { + return withHooks(ctx, _d.sqlExec, _d.mutation, _d.hooks) +} + +// ExecX is like Exec, but panics if an error occurs. +func (_d *BrokerSecretDelete) ExecX(ctx context.Context) int { + n, err := _d.Exec(ctx) + if err != nil { + panic(err) + } + return n +} + +func (_d *BrokerSecretDelete) sqlExec(ctx context.Context) (int, error) { + _spec := sqlgraph.NewDeleteSpec(brokersecret.Table, sqlgraph.NewFieldSpec(brokersecret.FieldID, field.TypeUUID)) + if ps := _d.mutation.predicates; len(ps) > 0 { + _spec.Predicate = func(selector *sql.Selector) { + for i := range ps { + ps[i](selector) + } + } + } + affected, err := sqlgraph.DeleteNodes(ctx, _d.driver, _spec) + if err != nil && sqlgraph.IsConstraintError(err) { + err = &ConstraintError{msg: err.Error(), wrap: err} + } + _d.mutation.done = true + return affected, err +} + +// BrokerSecretDeleteOne is the builder for deleting a single BrokerSecret entity. +type BrokerSecretDeleteOne struct { + _d *BrokerSecretDelete +} + +// Where appends a list predicates to the BrokerSecretDelete builder. +func (_d *BrokerSecretDeleteOne) Where(ps ...predicate.BrokerSecret) *BrokerSecretDeleteOne { + _d._d.mutation.Where(ps...) + return _d +} + +// Exec executes the deletion query. +func (_d *BrokerSecretDeleteOne) Exec(ctx context.Context) error { + n, err := _d._d.Exec(ctx) + switch { + case err != nil: + return err + case n == 0: + return &NotFoundError{brokersecret.Label} + default: + return nil + } +} + +// ExecX is like Exec, but panics if an error occurs. +func (_d *BrokerSecretDeleteOne) ExecX(ctx context.Context) { + if err := _d.Exec(ctx); err != nil { + panic(err) + } +} diff --git a/pkg/ent/brokersecret_query.go b/pkg/ent/brokersecret_query.go new file mode 100644 index 000000000..430228400 --- /dev/null +++ b/pkg/ent/brokersecret_query.go @@ -0,0 +1,565 @@ +// Code generated by ent, DO NOT EDIT. + +package ent + +import ( + "context" + "fmt" + "math" + + "entgo.io/ent" + "entgo.io/ent/dialect" + "entgo.io/ent/dialect/sql" + "entgo.io/ent/dialect/sql/sqlgraph" + "entgo.io/ent/schema/field" + "github.com/GoogleCloudPlatform/scion/pkg/ent/brokersecret" + "github.com/GoogleCloudPlatform/scion/pkg/ent/predicate" + "github.com/google/uuid" +) + +// BrokerSecretQuery is the builder for querying BrokerSecret entities. +type BrokerSecretQuery struct { + config + ctx *QueryContext + order []brokersecret.OrderOption + inters []Interceptor + predicates []predicate.BrokerSecret + modifiers []func(*sql.Selector) + // intermediate query (i.e. traversal path). + sql *sql.Selector + path func(context.Context) (*sql.Selector, error) +} + +// Where adds a new predicate for the BrokerSecretQuery builder. +func (_q *BrokerSecretQuery) Where(ps ...predicate.BrokerSecret) *BrokerSecretQuery { + _q.predicates = append(_q.predicates, ps...) + return _q +} + +// Limit the number of records to be returned by this query. +func (_q *BrokerSecretQuery) Limit(limit int) *BrokerSecretQuery { + _q.ctx.Limit = &limit + return _q +} + +// Offset to start from. +func (_q *BrokerSecretQuery) Offset(offset int) *BrokerSecretQuery { + _q.ctx.Offset = &offset + return _q +} + +// Unique configures the query builder to filter duplicate records on query. +// By default, unique is set to true, and can be disabled using this method. +func (_q *BrokerSecretQuery) Unique(unique bool) *BrokerSecretQuery { + _q.ctx.Unique = &unique + return _q +} + +// Order specifies how the records should be ordered. +func (_q *BrokerSecretQuery) Order(o ...brokersecret.OrderOption) *BrokerSecretQuery { + _q.order = append(_q.order, o...) + return _q +} + +// First returns the first BrokerSecret entity from the query. +// Returns a *NotFoundError when no BrokerSecret was found. +func (_q *BrokerSecretQuery) First(ctx context.Context) (*BrokerSecret, error) { + nodes, err := _q.Limit(1).All(setContextOp(ctx, _q.ctx, ent.OpQueryFirst)) + if err != nil { + return nil, err + } + if len(nodes) == 0 { + return nil, &NotFoundError{brokersecret.Label} + } + return nodes[0], nil +} + +// FirstX is like First, but panics if an error occurs. +func (_q *BrokerSecretQuery) FirstX(ctx context.Context) *BrokerSecret { + node, err := _q.First(ctx) + if err != nil && !IsNotFound(err) { + panic(err) + } + return node +} + +// FirstID returns the first BrokerSecret ID from the query. +// Returns a *NotFoundError when no BrokerSecret ID was found. +func (_q *BrokerSecretQuery) FirstID(ctx context.Context) (id uuid.UUID, err error) { + var ids []uuid.UUID + if ids, err = _q.Limit(1).IDs(setContextOp(ctx, _q.ctx, ent.OpQueryFirstID)); err != nil { + return + } + if len(ids) == 0 { + err = &NotFoundError{brokersecret.Label} + return + } + return ids[0], nil +} + +// FirstIDX is like FirstID, but panics if an error occurs. +func (_q *BrokerSecretQuery) FirstIDX(ctx context.Context) uuid.UUID { + id, err := _q.FirstID(ctx) + if err != nil && !IsNotFound(err) { + panic(err) + } + return id +} + +// Only returns a single BrokerSecret entity found by the query, ensuring it only returns one. +// Returns a *NotSingularError when more than one BrokerSecret entity is found. +// Returns a *NotFoundError when no BrokerSecret entities are found. +func (_q *BrokerSecretQuery) Only(ctx context.Context) (*BrokerSecret, error) { + nodes, err := _q.Limit(2).All(setContextOp(ctx, _q.ctx, ent.OpQueryOnly)) + if err != nil { + return nil, err + } + switch len(nodes) { + case 1: + return nodes[0], nil + case 0: + return nil, &NotFoundError{brokersecret.Label} + default: + return nil, &NotSingularError{brokersecret.Label} + } +} + +// OnlyX is like Only, but panics if an error occurs. +func (_q *BrokerSecretQuery) OnlyX(ctx context.Context) *BrokerSecret { + node, err := _q.Only(ctx) + if err != nil { + panic(err) + } + return node +} + +// OnlyID is like Only, but returns the only BrokerSecret ID in the query. +// Returns a *NotSingularError when more than one BrokerSecret ID is found. +// Returns a *NotFoundError when no entities are found. +func (_q *BrokerSecretQuery) OnlyID(ctx context.Context) (id uuid.UUID, err error) { + var ids []uuid.UUID + if ids, err = _q.Limit(2).IDs(setContextOp(ctx, _q.ctx, ent.OpQueryOnlyID)); err != nil { + return + } + switch len(ids) { + case 1: + id = ids[0] + case 0: + err = &NotFoundError{brokersecret.Label} + default: + err = &NotSingularError{brokersecret.Label} + } + return +} + +// OnlyIDX is like OnlyID, but panics if an error occurs. +func (_q *BrokerSecretQuery) OnlyIDX(ctx context.Context) uuid.UUID { + id, err := _q.OnlyID(ctx) + if err != nil { + panic(err) + } + return id +} + +// All executes the query and returns a list of BrokerSecrets. +func (_q *BrokerSecretQuery) All(ctx context.Context) ([]*BrokerSecret, error) { + ctx = setContextOp(ctx, _q.ctx, ent.OpQueryAll) + if err := _q.prepareQuery(ctx); err != nil { + return nil, err + } + qr := querierAll[[]*BrokerSecret, *BrokerSecretQuery]() + return withInterceptors[[]*BrokerSecret](ctx, _q, qr, _q.inters) +} + +// AllX is like All, but panics if an error occurs. +func (_q *BrokerSecretQuery) AllX(ctx context.Context) []*BrokerSecret { + nodes, err := _q.All(ctx) + if err != nil { + panic(err) + } + return nodes +} + +// IDs executes the query and returns a list of BrokerSecret IDs. +func (_q *BrokerSecretQuery) IDs(ctx context.Context) (ids []uuid.UUID, err error) { + if _q.ctx.Unique == nil && _q.path != nil { + _q.Unique(true) + } + ctx = setContextOp(ctx, _q.ctx, ent.OpQueryIDs) + if err = _q.Select(brokersecret.FieldID).Scan(ctx, &ids); err != nil { + return nil, err + } + return ids, nil +} + +// IDsX is like IDs, but panics if an error occurs. +func (_q *BrokerSecretQuery) IDsX(ctx context.Context) []uuid.UUID { + ids, err := _q.IDs(ctx) + if err != nil { + panic(err) + } + return ids +} + +// Count returns the count of the given query. +func (_q *BrokerSecretQuery) Count(ctx context.Context) (int, error) { + ctx = setContextOp(ctx, _q.ctx, ent.OpQueryCount) + if err := _q.prepareQuery(ctx); err != nil { + return 0, err + } + return withInterceptors[int](ctx, _q, querierCount[*BrokerSecretQuery](), _q.inters) +} + +// CountX is like Count, but panics if an error occurs. +func (_q *BrokerSecretQuery) CountX(ctx context.Context) int { + count, err := _q.Count(ctx) + if err != nil { + panic(err) + } + return count +} + +// Exist returns true if the query has elements in the graph. +func (_q *BrokerSecretQuery) Exist(ctx context.Context) (bool, error) { + ctx = setContextOp(ctx, _q.ctx, ent.OpQueryExist) + switch _, err := _q.FirstID(ctx); { + case IsNotFound(err): + return false, nil + case err != nil: + return false, fmt.Errorf("ent: check existence: %w", err) + default: + return true, nil + } +} + +// ExistX is like Exist, but panics if an error occurs. +func (_q *BrokerSecretQuery) ExistX(ctx context.Context) bool { + exist, err := _q.Exist(ctx) + if err != nil { + panic(err) + } + return exist +} + +// Clone returns a duplicate of the BrokerSecretQuery builder, including all associated steps. It can be +// used to prepare common query builders and use them differently after the clone is made. +func (_q *BrokerSecretQuery) Clone() *BrokerSecretQuery { + if _q == nil { + return nil + } + return &BrokerSecretQuery{ + config: _q.config, + ctx: _q.ctx.Clone(), + order: append([]brokersecret.OrderOption{}, _q.order...), + inters: append([]Interceptor{}, _q.inters...), + predicates: append([]predicate.BrokerSecret{}, _q.predicates...), + // clone intermediate query. + sql: _q.sql.Clone(), + path: _q.path, + } +} + +// GroupBy is used to group vertices by one or more fields/columns. +// It is often used with aggregate functions, like: count, max, mean, min, sum. +// +// Example: +// +// var v []struct { +// SecretKey []byte `json:"secret_key,omitempty"` +// Count int `json:"count,omitempty"` +// } +// +// client.BrokerSecret.Query(). +// GroupBy(brokersecret.FieldSecretKey). +// Aggregate(ent.Count()). +// Scan(ctx, &v) +func (_q *BrokerSecretQuery) GroupBy(field string, fields ...string) *BrokerSecretGroupBy { + _q.ctx.Fields = append([]string{field}, fields...) + grbuild := &BrokerSecretGroupBy{build: _q} + grbuild.flds = &_q.ctx.Fields + grbuild.label = brokersecret.Label + grbuild.scan = grbuild.Scan + return grbuild +} + +// Select allows the selection one or more fields/columns for the given query, +// instead of selecting all fields in the entity. +// +// Example: +// +// var v []struct { +// SecretKey []byte `json:"secret_key,omitempty"` +// } +// +// client.BrokerSecret.Query(). +// Select(brokersecret.FieldSecretKey). +// Scan(ctx, &v) +func (_q *BrokerSecretQuery) Select(fields ...string) *BrokerSecretSelect { + _q.ctx.Fields = append(_q.ctx.Fields, fields...) + sbuild := &BrokerSecretSelect{BrokerSecretQuery: _q} + sbuild.label = brokersecret.Label + sbuild.flds, sbuild.scan = &_q.ctx.Fields, sbuild.Scan + return sbuild +} + +// Aggregate returns a BrokerSecretSelect configured with the given aggregations. +func (_q *BrokerSecretQuery) Aggregate(fns ...AggregateFunc) *BrokerSecretSelect { + return _q.Select().Aggregate(fns...) +} + +func (_q *BrokerSecretQuery) prepareQuery(ctx context.Context) error { + for _, inter := range _q.inters { + if inter == nil { + return fmt.Errorf("ent: uninitialized interceptor (forgotten import ent/runtime?)") + } + if trv, ok := inter.(Traverser); ok { + if err := trv.Traverse(ctx, _q); err != nil { + return err + } + } + } + for _, f := range _q.ctx.Fields { + if !brokersecret.ValidColumn(f) { + return &ValidationError{Name: f, err: fmt.Errorf("ent: invalid field %q for query", f)} + } + } + if _q.path != nil { + prev, err := _q.path(ctx) + if err != nil { + return err + } + _q.sql = prev + } + return nil +} + +func (_q *BrokerSecretQuery) sqlAll(ctx context.Context, hooks ...queryHook) ([]*BrokerSecret, error) { + var ( + nodes = []*BrokerSecret{} + _spec = _q.querySpec() + ) + _spec.ScanValues = func(columns []string) ([]any, error) { + return (*BrokerSecret).scanValues(nil, columns) + } + _spec.Assign = func(columns []string, values []any) error { + node := &BrokerSecret{config: _q.config} + nodes = append(nodes, node) + return node.assignValues(columns, values) + } + if len(_q.modifiers) > 0 { + _spec.Modifiers = _q.modifiers + } + for i := range hooks { + hooks[i](ctx, _spec) + } + if err := sqlgraph.QueryNodes(ctx, _q.driver, _spec); err != nil { + return nil, err + } + if len(nodes) == 0 { + return nodes, nil + } + return nodes, nil +} + +func (_q *BrokerSecretQuery) sqlCount(ctx context.Context) (int, error) { + _spec := _q.querySpec() + if len(_q.modifiers) > 0 { + _spec.Modifiers = _q.modifiers + } + _spec.Node.Columns = _q.ctx.Fields + if len(_q.ctx.Fields) > 0 { + _spec.Unique = _q.ctx.Unique != nil && *_q.ctx.Unique + } + return sqlgraph.CountNodes(ctx, _q.driver, _spec) +} + +func (_q *BrokerSecretQuery) querySpec() *sqlgraph.QuerySpec { + _spec := sqlgraph.NewQuerySpec(brokersecret.Table, brokersecret.Columns, sqlgraph.NewFieldSpec(brokersecret.FieldID, field.TypeUUID)) + _spec.From = _q.sql + if unique := _q.ctx.Unique; unique != nil { + _spec.Unique = *unique + } else if _q.path != nil { + _spec.Unique = true + } + if fields := _q.ctx.Fields; len(fields) > 0 { + _spec.Node.Columns = make([]string, 0, len(fields)) + _spec.Node.Columns = append(_spec.Node.Columns, brokersecret.FieldID) + for i := range fields { + if fields[i] != brokersecret.FieldID { + _spec.Node.Columns = append(_spec.Node.Columns, fields[i]) + } + } + } + if ps := _q.predicates; len(ps) > 0 { + _spec.Predicate = func(selector *sql.Selector) { + for i := range ps { + ps[i](selector) + } + } + } + if limit := _q.ctx.Limit; limit != nil { + _spec.Limit = *limit + } + if offset := _q.ctx.Offset; offset != nil { + _spec.Offset = *offset + } + if ps := _q.order; len(ps) > 0 { + _spec.Order = func(selector *sql.Selector) { + for i := range ps { + ps[i](selector) + } + } + } + return _spec +} + +func (_q *BrokerSecretQuery) sqlQuery(ctx context.Context) *sql.Selector { + builder := sql.Dialect(_q.driver.Dialect()) + t1 := builder.Table(brokersecret.Table) + columns := _q.ctx.Fields + if len(columns) == 0 { + columns = brokersecret.Columns + } + selector := builder.Select(t1.Columns(columns...)...).From(t1) + if _q.sql != nil { + selector = _q.sql + selector.Select(selector.Columns(columns...)...) + } + if _q.ctx.Unique != nil && *_q.ctx.Unique { + selector.Distinct() + } + for _, m := range _q.modifiers { + m(selector) + } + for _, p := range _q.predicates { + p(selector) + } + for _, p := range _q.order { + p(selector) + } + if offset := _q.ctx.Offset; offset != nil { + // limit is mandatory for offset clause. We start + // with default value, and override it below if needed. + selector.Offset(*offset).Limit(math.MaxInt32) + } + if limit := _q.ctx.Limit; limit != nil { + selector.Limit(*limit) + } + return selector +} + +// ForUpdate locks the selected rows against concurrent updates, and prevent them from being +// updated, deleted or "selected ... for update" by other sessions, until the transaction is +// either committed or rolled-back. +func (_q *BrokerSecretQuery) ForUpdate(opts ...sql.LockOption) *BrokerSecretQuery { + if _q.driver.Dialect() == dialect.Postgres { + _q.Unique(false) + } + _q.modifiers = append(_q.modifiers, func(s *sql.Selector) { + s.ForUpdate(opts...) + }) + return _q +} + +// ForShare behaves similarly to ForUpdate, except that it acquires a shared mode lock +// on any rows that are read. Other sessions can read the rows, but cannot modify them +// until your transaction commits. +func (_q *BrokerSecretQuery) ForShare(opts ...sql.LockOption) *BrokerSecretQuery { + if _q.driver.Dialect() == dialect.Postgres { + _q.Unique(false) + } + _q.modifiers = append(_q.modifiers, func(s *sql.Selector) { + s.ForShare(opts...) + }) + return _q +} + +// BrokerSecretGroupBy is the group-by builder for BrokerSecret entities. +type BrokerSecretGroupBy struct { + selector + build *BrokerSecretQuery +} + +// Aggregate adds the given aggregation functions to the group-by query. +func (_g *BrokerSecretGroupBy) Aggregate(fns ...AggregateFunc) *BrokerSecretGroupBy { + _g.fns = append(_g.fns, fns...) + return _g +} + +// Scan applies the selector query and scans the result into the given value. +func (_g *BrokerSecretGroupBy) Scan(ctx context.Context, v any) error { + ctx = setContextOp(ctx, _g.build.ctx, ent.OpQueryGroupBy) + if err := _g.build.prepareQuery(ctx); err != nil { + return err + } + return scanWithInterceptors[*BrokerSecretQuery, *BrokerSecretGroupBy](ctx, _g.build, _g, _g.build.inters, v) +} + +func (_g *BrokerSecretGroupBy) sqlScan(ctx context.Context, root *BrokerSecretQuery, v any) error { + selector := root.sqlQuery(ctx).Select() + aggregation := make([]string, 0, len(_g.fns)) + for _, fn := range _g.fns { + aggregation = append(aggregation, fn(selector)) + } + if len(selector.SelectedColumns()) == 0 { + columns := make([]string, 0, len(*_g.flds)+len(_g.fns)) + for _, f := range *_g.flds { + columns = append(columns, selector.C(f)) + } + columns = append(columns, aggregation...) + selector.Select(columns...) + } + selector.GroupBy(selector.Columns(*_g.flds...)...) + if err := selector.Err(); err != nil { + return err + } + rows := &sql.Rows{} + query, args := selector.Query() + if err := _g.build.driver.Query(ctx, query, args, rows); err != nil { + return err + } + defer rows.Close() + return sql.ScanSlice(rows, v) +} + +// BrokerSecretSelect is the builder for selecting fields of BrokerSecret entities. +type BrokerSecretSelect struct { + *BrokerSecretQuery + selector +} + +// Aggregate adds the given aggregation functions to the selector query. +func (_s *BrokerSecretSelect) Aggregate(fns ...AggregateFunc) *BrokerSecretSelect { + _s.fns = append(_s.fns, fns...) + return _s +} + +// Scan applies the selector query and scans the result into the given value. +func (_s *BrokerSecretSelect) Scan(ctx context.Context, v any) error { + ctx = setContextOp(ctx, _s.ctx, ent.OpQuerySelect) + if err := _s.prepareQuery(ctx); err != nil { + return err + } + return scanWithInterceptors[*BrokerSecretQuery, *BrokerSecretSelect](ctx, _s.BrokerSecretQuery, _s, _s.inters, v) +} + +func (_s *BrokerSecretSelect) sqlScan(ctx context.Context, root *BrokerSecretQuery, v any) error { + selector := root.sqlQuery(ctx) + aggregation := make([]string, 0, len(_s.fns)) + for _, fn := range _s.fns { + aggregation = append(aggregation, fn(selector)) + } + switch n := len(*_s.selector.flds); { + case n == 0 && len(aggregation) > 0: + selector.Select(aggregation...) + case n != 0 && len(aggregation) > 0: + selector.AppendSelect(aggregation...) + } + rows := &sql.Rows{} + query, args := selector.Query() + if err := _s.driver.Query(ctx, query, args, rows); err != nil { + return err + } + defer rows.Close() + return sql.ScanSlice(rows, v) +} diff --git a/pkg/ent/brokersecret_update.go b/pkg/ent/brokersecret_update.go new file mode 100644 index 000000000..86a2c4359 --- /dev/null +++ b/pkg/ent/brokersecret_update.go @@ -0,0 +1,392 @@ +// Code generated by ent, DO NOT EDIT. + +package ent + +import ( + "context" + "errors" + "fmt" + "time" + + "entgo.io/ent/dialect/sql" + "entgo.io/ent/dialect/sql/sqlgraph" + "entgo.io/ent/schema/field" + "github.com/GoogleCloudPlatform/scion/pkg/ent/brokersecret" + "github.com/GoogleCloudPlatform/scion/pkg/ent/predicate" +) + +// BrokerSecretUpdate is the builder for updating BrokerSecret entities. +type BrokerSecretUpdate struct { + config + hooks []Hook + mutation *BrokerSecretMutation +} + +// Where appends a list predicates to the BrokerSecretUpdate builder. +func (_u *BrokerSecretUpdate) Where(ps ...predicate.BrokerSecret) *BrokerSecretUpdate { + _u.mutation.Where(ps...) + return _u +} + +// SetSecretKey sets the "secret_key" field. +func (_u *BrokerSecretUpdate) SetSecretKey(v []byte) *BrokerSecretUpdate { + _u.mutation.SetSecretKey(v) + return _u +} + +// SetAlgorithm sets the "algorithm" field. +func (_u *BrokerSecretUpdate) SetAlgorithm(v string) *BrokerSecretUpdate { + _u.mutation.SetAlgorithm(v) + return _u +} + +// SetNillableAlgorithm sets the "algorithm" field if the given value is not nil. +func (_u *BrokerSecretUpdate) SetNillableAlgorithm(v *string) *BrokerSecretUpdate { + if v != nil { + _u.SetAlgorithm(*v) + } + return _u +} + +// SetRotatedAt sets the "rotated_at" field. +func (_u *BrokerSecretUpdate) SetRotatedAt(v time.Time) *BrokerSecretUpdate { + _u.mutation.SetRotatedAt(v) + return _u +} + +// SetNillableRotatedAt sets the "rotated_at" field if the given value is not nil. +func (_u *BrokerSecretUpdate) SetNillableRotatedAt(v *time.Time) *BrokerSecretUpdate { + if v != nil { + _u.SetRotatedAt(*v) + } + return _u +} + +// ClearRotatedAt clears the value of the "rotated_at" field. +func (_u *BrokerSecretUpdate) ClearRotatedAt() *BrokerSecretUpdate { + _u.mutation.ClearRotatedAt() + return _u +} + +// SetExpiresAt sets the "expires_at" field. +func (_u *BrokerSecretUpdate) SetExpiresAt(v time.Time) *BrokerSecretUpdate { + _u.mutation.SetExpiresAt(v) + return _u +} + +// SetNillableExpiresAt sets the "expires_at" field if the given value is not nil. +func (_u *BrokerSecretUpdate) SetNillableExpiresAt(v *time.Time) *BrokerSecretUpdate { + if v != nil { + _u.SetExpiresAt(*v) + } + return _u +} + +// ClearExpiresAt clears the value of the "expires_at" field. +func (_u *BrokerSecretUpdate) ClearExpiresAt() *BrokerSecretUpdate { + _u.mutation.ClearExpiresAt() + return _u +} + +// SetStatus sets the "status" field. +func (_u *BrokerSecretUpdate) SetStatus(v string) *BrokerSecretUpdate { + _u.mutation.SetStatus(v) + return _u +} + +// SetNillableStatus sets the "status" field if the given value is not nil. +func (_u *BrokerSecretUpdate) SetNillableStatus(v *string) *BrokerSecretUpdate { + if v != nil { + _u.SetStatus(*v) + } + return _u +} + +// Mutation returns the BrokerSecretMutation object of the builder. +func (_u *BrokerSecretUpdate) Mutation() *BrokerSecretMutation { + return _u.mutation +} + +// Save executes the query and returns the number of nodes affected by the update operation. +func (_u *BrokerSecretUpdate) Save(ctx context.Context) (int, error) { + return withHooks(ctx, _u.sqlSave, _u.mutation, _u.hooks) +} + +// SaveX is like Save, but panics if an error occurs. +func (_u *BrokerSecretUpdate) SaveX(ctx context.Context) int { + affected, err := _u.Save(ctx) + if err != nil { + panic(err) + } + return affected +} + +// Exec executes the query. +func (_u *BrokerSecretUpdate) Exec(ctx context.Context) error { + _, err := _u.Save(ctx) + return err +} + +// ExecX is like Exec, but panics if an error occurs. +func (_u *BrokerSecretUpdate) ExecX(ctx context.Context) { + if err := _u.Exec(ctx); err != nil { + panic(err) + } +} + +// check runs all checks and user-defined validators on the builder. +func (_u *BrokerSecretUpdate) check() error { + if v, ok := _u.mutation.SecretKey(); ok { + if err := brokersecret.SecretKeyValidator(v); err != nil { + return &ValidationError{Name: "secret_key", err: fmt.Errorf(`ent: validator failed for field "BrokerSecret.secret_key": %w`, err)} + } + } + return nil +} + +func (_u *BrokerSecretUpdate) sqlSave(ctx context.Context) (_node int, err error) { + if err := _u.check(); err != nil { + return _node, err + } + _spec := sqlgraph.NewUpdateSpec(brokersecret.Table, brokersecret.Columns, sqlgraph.NewFieldSpec(brokersecret.FieldID, field.TypeUUID)) + if ps := _u.mutation.predicates; len(ps) > 0 { + _spec.Predicate = func(selector *sql.Selector) { + for i := range ps { + ps[i](selector) + } + } + } + if value, ok := _u.mutation.SecretKey(); ok { + _spec.SetField(brokersecret.FieldSecretKey, field.TypeBytes, value) + } + if value, ok := _u.mutation.Algorithm(); ok { + _spec.SetField(brokersecret.FieldAlgorithm, field.TypeString, value) + } + if value, ok := _u.mutation.RotatedAt(); ok { + _spec.SetField(brokersecret.FieldRotatedAt, field.TypeTime, value) + } + if _u.mutation.RotatedAtCleared() { + _spec.ClearField(brokersecret.FieldRotatedAt, field.TypeTime) + } + if value, ok := _u.mutation.ExpiresAt(); ok { + _spec.SetField(brokersecret.FieldExpiresAt, field.TypeTime, value) + } + if _u.mutation.ExpiresAtCleared() { + _spec.ClearField(brokersecret.FieldExpiresAt, field.TypeTime) + } + if value, ok := _u.mutation.Status(); ok { + _spec.SetField(brokersecret.FieldStatus, field.TypeString, value) + } + if _node, err = sqlgraph.UpdateNodes(ctx, _u.driver, _spec); err != nil { + if _, ok := err.(*sqlgraph.NotFoundError); ok { + err = &NotFoundError{brokersecret.Label} + } else if sqlgraph.IsConstraintError(err) { + err = &ConstraintError{msg: err.Error(), wrap: err} + } + return 0, err + } + _u.mutation.done = true + return _node, nil +} + +// BrokerSecretUpdateOne is the builder for updating a single BrokerSecret entity. +type BrokerSecretUpdateOne struct { + config + fields []string + hooks []Hook + mutation *BrokerSecretMutation +} + +// SetSecretKey sets the "secret_key" field. +func (_u *BrokerSecretUpdateOne) SetSecretKey(v []byte) *BrokerSecretUpdateOne { + _u.mutation.SetSecretKey(v) + return _u +} + +// SetAlgorithm sets the "algorithm" field. +func (_u *BrokerSecretUpdateOne) SetAlgorithm(v string) *BrokerSecretUpdateOne { + _u.mutation.SetAlgorithm(v) + return _u +} + +// SetNillableAlgorithm sets the "algorithm" field if the given value is not nil. +func (_u *BrokerSecretUpdateOne) SetNillableAlgorithm(v *string) *BrokerSecretUpdateOne { + if v != nil { + _u.SetAlgorithm(*v) + } + return _u +} + +// SetRotatedAt sets the "rotated_at" field. +func (_u *BrokerSecretUpdateOne) SetRotatedAt(v time.Time) *BrokerSecretUpdateOne { + _u.mutation.SetRotatedAt(v) + return _u +} + +// SetNillableRotatedAt sets the "rotated_at" field if the given value is not nil. +func (_u *BrokerSecretUpdateOne) SetNillableRotatedAt(v *time.Time) *BrokerSecretUpdateOne { + if v != nil { + _u.SetRotatedAt(*v) + } + return _u +} + +// ClearRotatedAt clears the value of the "rotated_at" field. +func (_u *BrokerSecretUpdateOne) ClearRotatedAt() *BrokerSecretUpdateOne { + _u.mutation.ClearRotatedAt() + return _u +} + +// SetExpiresAt sets the "expires_at" field. +func (_u *BrokerSecretUpdateOne) SetExpiresAt(v time.Time) *BrokerSecretUpdateOne { + _u.mutation.SetExpiresAt(v) + return _u +} + +// SetNillableExpiresAt sets the "expires_at" field if the given value is not nil. +func (_u *BrokerSecretUpdateOne) SetNillableExpiresAt(v *time.Time) *BrokerSecretUpdateOne { + if v != nil { + _u.SetExpiresAt(*v) + } + return _u +} + +// ClearExpiresAt clears the value of the "expires_at" field. +func (_u *BrokerSecretUpdateOne) ClearExpiresAt() *BrokerSecretUpdateOne { + _u.mutation.ClearExpiresAt() + return _u +} + +// SetStatus sets the "status" field. +func (_u *BrokerSecretUpdateOne) SetStatus(v string) *BrokerSecretUpdateOne { + _u.mutation.SetStatus(v) + return _u +} + +// SetNillableStatus sets the "status" field if the given value is not nil. +func (_u *BrokerSecretUpdateOne) SetNillableStatus(v *string) *BrokerSecretUpdateOne { + if v != nil { + _u.SetStatus(*v) + } + return _u +} + +// Mutation returns the BrokerSecretMutation object of the builder. +func (_u *BrokerSecretUpdateOne) Mutation() *BrokerSecretMutation { + return _u.mutation +} + +// Where appends a list predicates to the BrokerSecretUpdate builder. +func (_u *BrokerSecretUpdateOne) Where(ps ...predicate.BrokerSecret) *BrokerSecretUpdateOne { + _u.mutation.Where(ps...) + return _u +} + +// Select allows selecting one or more fields (columns) of the returned entity. +// The default is selecting all fields defined in the entity schema. +func (_u *BrokerSecretUpdateOne) Select(field string, fields ...string) *BrokerSecretUpdateOne { + _u.fields = append([]string{field}, fields...) + return _u +} + +// Save executes the query and returns the updated BrokerSecret entity. +func (_u *BrokerSecretUpdateOne) Save(ctx context.Context) (*BrokerSecret, error) { + return withHooks(ctx, _u.sqlSave, _u.mutation, _u.hooks) +} + +// SaveX is like Save, but panics if an error occurs. +func (_u *BrokerSecretUpdateOne) SaveX(ctx context.Context) *BrokerSecret { + node, err := _u.Save(ctx) + if err != nil { + panic(err) + } + return node +} + +// Exec executes the query on the entity. +func (_u *BrokerSecretUpdateOne) Exec(ctx context.Context) error { + _, err := _u.Save(ctx) + return err +} + +// ExecX is like Exec, but panics if an error occurs. +func (_u *BrokerSecretUpdateOne) ExecX(ctx context.Context) { + if err := _u.Exec(ctx); err != nil { + panic(err) + } +} + +// check runs all checks and user-defined validators on the builder. +func (_u *BrokerSecretUpdateOne) check() error { + if v, ok := _u.mutation.SecretKey(); ok { + if err := brokersecret.SecretKeyValidator(v); err != nil { + return &ValidationError{Name: "secret_key", err: fmt.Errorf(`ent: validator failed for field "BrokerSecret.secret_key": %w`, err)} + } + } + return nil +} + +func (_u *BrokerSecretUpdateOne) sqlSave(ctx context.Context) (_node *BrokerSecret, err error) { + if err := _u.check(); err != nil { + return _node, err + } + _spec := sqlgraph.NewUpdateSpec(brokersecret.Table, brokersecret.Columns, sqlgraph.NewFieldSpec(brokersecret.FieldID, field.TypeUUID)) + id, ok := _u.mutation.ID() + if !ok { + return nil, &ValidationError{Name: "id", err: errors.New(`ent: missing "BrokerSecret.id" for update`)} + } + _spec.Node.ID.Value = id + if fields := _u.fields; len(fields) > 0 { + _spec.Node.Columns = make([]string, 0, len(fields)) + _spec.Node.Columns = append(_spec.Node.Columns, brokersecret.FieldID) + for _, f := range fields { + if !brokersecret.ValidColumn(f) { + return nil, &ValidationError{Name: f, err: fmt.Errorf("ent: invalid field %q for query", f)} + } + if f != brokersecret.FieldID { + _spec.Node.Columns = append(_spec.Node.Columns, f) + } + } + } + if ps := _u.mutation.predicates; len(ps) > 0 { + _spec.Predicate = func(selector *sql.Selector) { + for i := range ps { + ps[i](selector) + } + } + } + if value, ok := _u.mutation.SecretKey(); ok { + _spec.SetField(brokersecret.FieldSecretKey, field.TypeBytes, value) + } + if value, ok := _u.mutation.Algorithm(); ok { + _spec.SetField(brokersecret.FieldAlgorithm, field.TypeString, value) + } + if value, ok := _u.mutation.RotatedAt(); ok { + _spec.SetField(brokersecret.FieldRotatedAt, field.TypeTime, value) + } + if _u.mutation.RotatedAtCleared() { + _spec.ClearField(brokersecret.FieldRotatedAt, field.TypeTime) + } + if value, ok := _u.mutation.ExpiresAt(); ok { + _spec.SetField(brokersecret.FieldExpiresAt, field.TypeTime, value) + } + if _u.mutation.ExpiresAtCleared() { + _spec.ClearField(brokersecret.FieldExpiresAt, field.TypeTime) + } + if value, ok := _u.mutation.Status(); ok { + _spec.SetField(brokersecret.FieldStatus, field.TypeString, value) + } + _node = &BrokerSecret{config: _u.config} + _spec.Assign = _node.assignValues + _spec.ScanValues = _node.scanValues + if err = sqlgraph.UpdateNode(ctx, _u.driver, _spec); err != nil { + if _, ok := err.(*sqlgraph.NotFoundError); ok { + err = &NotFoundError{brokersecret.Label} + } else if sqlgraph.IsConstraintError(err) { + err = &ConstraintError{msg: err.Error(), wrap: err} + } + return nil, err + } + _u.mutation.done = true + return _node, nil +} diff --git a/pkg/ent/client.go b/pkg/ent/client.go index ccb3f1a7d..f0c7ac2cf 100644 --- a/pkg/ent/client.go +++ b/pkg/ent/client.go @@ -18,11 +18,35 @@ import ( "entgo.io/ent/dialect/sql/sqlgraph" "github.com/GoogleCloudPlatform/scion/pkg/ent/accesspolicy" "github.com/GoogleCloudPlatform/scion/pkg/ent/agent" + "github.com/GoogleCloudPlatform/scion/pkg/ent/allowlistentry" + "github.com/GoogleCloudPlatform/scion/pkg/ent/apikey" + "github.com/GoogleCloudPlatform/scion/pkg/ent/brokerdispatch" + "github.com/GoogleCloudPlatform/scion/pkg/ent/brokerjointoken" + "github.com/GoogleCloudPlatform/scion/pkg/ent/brokersecret" + "github.com/GoogleCloudPlatform/scion/pkg/ent/envvar" + "github.com/GoogleCloudPlatform/scion/pkg/ent/gcpserviceaccount" + "github.com/GoogleCloudPlatform/scion/pkg/ent/githubinstallation" "github.com/GoogleCloudPlatform/scion/pkg/ent/group" "github.com/GoogleCloudPlatform/scion/pkg/ent/groupmembership" + "github.com/GoogleCloudPlatform/scion/pkg/ent/harnessconfig" + "github.com/GoogleCloudPlatform/scion/pkg/ent/invitecode" + "github.com/GoogleCloudPlatform/scion/pkg/ent/maintenanceoperation" + "github.com/GoogleCloudPlatform/scion/pkg/ent/maintenanceoperationrun" + "github.com/GoogleCloudPlatform/scion/pkg/ent/message" + "github.com/GoogleCloudPlatform/scion/pkg/ent/notification" + "github.com/GoogleCloudPlatform/scion/pkg/ent/notificationsubscription" "github.com/GoogleCloudPlatform/scion/pkg/ent/policybinding" "github.com/GoogleCloudPlatform/scion/pkg/ent/project" + "github.com/GoogleCloudPlatform/scion/pkg/ent/projectcontributor" + "github.com/GoogleCloudPlatform/scion/pkg/ent/projectsyncstate" + "github.com/GoogleCloudPlatform/scion/pkg/ent/runtimebroker" + "github.com/GoogleCloudPlatform/scion/pkg/ent/schedule" + "github.com/GoogleCloudPlatform/scion/pkg/ent/scheduledevent" + "github.com/GoogleCloudPlatform/scion/pkg/ent/secret" + "github.com/GoogleCloudPlatform/scion/pkg/ent/subscriptiontemplate" + "github.com/GoogleCloudPlatform/scion/pkg/ent/template" "github.com/GoogleCloudPlatform/scion/pkg/ent/user" + "github.com/GoogleCloudPlatform/scion/pkg/ent/useraccesstoken" ) // Client is the client that holds all ent builders. @@ -34,16 +58,64 @@ type Client struct { AccessPolicy *AccessPolicyClient // Agent is the client for interacting with the Agent builders. Agent *AgentClient + // AllowListEntry is the client for interacting with the AllowListEntry builders. + AllowListEntry *AllowListEntryClient + // ApiKey is the client for interacting with the ApiKey builders. + ApiKey *ApiKeyClient + // BrokerDispatch is the client for interacting with the BrokerDispatch builders. + BrokerDispatch *BrokerDispatchClient + // BrokerJoinToken is the client for interacting with the BrokerJoinToken builders. + BrokerJoinToken *BrokerJoinTokenClient + // BrokerSecret is the client for interacting with the BrokerSecret builders. + BrokerSecret *BrokerSecretClient + // EnvVar is the client for interacting with the EnvVar builders. + EnvVar *EnvVarClient + // GCPServiceAccount is the client for interacting with the GCPServiceAccount builders. + GCPServiceAccount *GCPServiceAccountClient + // GithubInstallation is the client for interacting with the GithubInstallation builders. + GithubInstallation *GithubInstallationClient // Group is the client for interacting with the Group builders. Group *GroupClient // GroupMembership is the client for interacting with the GroupMembership builders. GroupMembership *GroupMembershipClient + // HarnessConfig is the client for interacting with the HarnessConfig builders. + HarnessConfig *HarnessConfigClient + // InviteCode is the client for interacting with the InviteCode builders. + InviteCode *InviteCodeClient + // MaintenanceOperation is the client for interacting with the MaintenanceOperation builders. + MaintenanceOperation *MaintenanceOperationClient + // MaintenanceOperationRun is the client for interacting with the MaintenanceOperationRun builders. + MaintenanceOperationRun *MaintenanceOperationRunClient + // Message is the client for interacting with the Message builders. + Message *MessageClient + // Notification is the client for interacting with the Notification builders. + Notification *NotificationClient + // NotificationSubscription is the client for interacting with the NotificationSubscription builders. + NotificationSubscription *NotificationSubscriptionClient // PolicyBinding is the client for interacting with the PolicyBinding builders. PolicyBinding *PolicyBindingClient // Project is the client for interacting with the Project builders. Project *ProjectClient + // ProjectContributor is the client for interacting with the ProjectContributor builders. + ProjectContributor *ProjectContributorClient + // ProjectSyncState is the client for interacting with the ProjectSyncState builders. + ProjectSyncState *ProjectSyncStateClient + // RuntimeBroker is the client for interacting with the RuntimeBroker builders. + RuntimeBroker *RuntimeBrokerClient + // Schedule is the client for interacting with the Schedule builders. + Schedule *ScheduleClient + // ScheduledEvent is the client for interacting with the ScheduledEvent builders. + ScheduledEvent *ScheduledEventClient + // Secret is the client for interacting with the Secret builders. + Secret *SecretClient + // SubscriptionTemplate is the client for interacting with the SubscriptionTemplate builders. + SubscriptionTemplate *SubscriptionTemplateClient + // Template is the client for interacting with the Template builders. + Template *TemplateClient // User is the client for interacting with the User builders. User *UserClient + // UserAccessToken is the client for interacting with the UserAccessToken builders. + UserAccessToken *UserAccessTokenClient } // NewClient creates a new client configured with the given options. @@ -57,11 +129,35 @@ func (c *Client) init() { c.Schema = migrate.NewSchema(c.driver) c.AccessPolicy = NewAccessPolicyClient(c.config) c.Agent = NewAgentClient(c.config) + c.AllowListEntry = NewAllowListEntryClient(c.config) + c.ApiKey = NewApiKeyClient(c.config) + c.BrokerDispatch = NewBrokerDispatchClient(c.config) + c.BrokerJoinToken = NewBrokerJoinTokenClient(c.config) + c.BrokerSecret = NewBrokerSecretClient(c.config) + c.EnvVar = NewEnvVarClient(c.config) + c.GCPServiceAccount = NewGCPServiceAccountClient(c.config) + c.GithubInstallation = NewGithubInstallationClient(c.config) c.Group = NewGroupClient(c.config) c.GroupMembership = NewGroupMembershipClient(c.config) + c.HarnessConfig = NewHarnessConfigClient(c.config) + c.InviteCode = NewInviteCodeClient(c.config) + c.MaintenanceOperation = NewMaintenanceOperationClient(c.config) + c.MaintenanceOperationRun = NewMaintenanceOperationRunClient(c.config) + c.Message = NewMessageClient(c.config) + c.Notification = NewNotificationClient(c.config) + c.NotificationSubscription = NewNotificationSubscriptionClient(c.config) c.PolicyBinding = NewPolicyBindingClient(c.config) c.Project = NewProjectClient(c.config) + c.ProjectContributor = NewProjectContributorClient(c.config) + c.ProjectSyncState = NewProjectSyncStateClient(c.config) + c.RuntimeBroker = NewRuntimeBrokerClient(c.config) + c.Schedule = NewScheduleClient(c.config) + c.ScheduledEvent = NewScheduledEventClient(c.config) + c.Secret = NewSecretClient(c.config) + c.SubscriptionTemplate = NewSubscriptionTemplateClient(c.config) + c.Template = NewTemplateClient(c.config) c.User = NewUserClient(c.config) + c.UserAccessToken = NewUserAccessTokenClient(c.config) } type ( @@ -152,15 +248,39 @@ func (c *Client) Tx(ctx context.Context) (*Tx, error) { cfg := c.config cfg.driver = tx return &Tx{ - ctx: ctx, - config: cfg, - AccessPolicy: NewAccessPolicyClient(cfg), - Agent: NewAgentClient(cfg), - Group: NewGroupClient(cfg), - GroupMembership: NewGroupMembershipClient(cfg), - PolicyBinding: NewPolicyBindingClient(cfg), - Project: NewProjectClient(cfg), - User: NewUserClient(cfg), + ctx: ctx, + config: cfg, + AccessPolicy: NewAccessPolicyClient(cfg), + Agent: NewAgentClient(cfg), + AllowListEntry: NewAllowListEntryClient(cfg), + ApiKey: NewApiKeyClient(cfg), + BrokerDispatch: NewBrokerDispatchClient(cfg), + BrokerJoinToken: NewBrokerJoinTokenClient(cfg), + BrokerSecret: NewBrokerSecretClient(cfg), + EnvVar: NewEnvVarClient(cfg), + GCPServiceAccount: NewGCPServiceAccountClient(cfg), + GithubInstallation: NewGithubInstallationClient(cfg), + Group: NewGroupClient(cfg), + GroupMembership: NewGroupMembershipClient(cfg), + HarnessConfig: NewHarnessConfigClient(cfg), + InviteCode: NewInviteCodeClient(cfg), + MaintenanceOperation: NewMaintenanceOperationClient(cfg), + MaintenanceOperationRun: NewMaintenanceOperationRunClient(cfg), + Message: NewMessageClient(cfg), + Notification: NewNotificationClient(cfg), + NotificationSubscription: NewNotificationSubscriptionClient(cfg), + PolicyBinding: NewPolicyBindingClient(cfg), + Project: NewProjectClient(cfg), + ProjectContributor: NewProjectContributorClient(cfg), + ProjectSyncState: NewProjectSyncStateClient(cfg), + RuntimeBroker: NewRuntimeBrokerClient(cfg), + Schedule: NewScheduleClient(cfg), + ScheduledEvent: NewScheduledEventClient(cfg), + Secret: NewSecretClient(cfg), + SubscriptionTemplate: NewSubscriptionTemplateClient(cfg), + Template: NewTemplateClient(cfg), + User: NewUserClient(cfg), + UserAccessToken: NewUserAccessTokenClient(cfg), }, nil } @@ -178,15 +298,39 @@ func (c *Client) BeginTx(ctx context.Context, opts *sql.TxOptions) (*Tx, error) cfg := c.config cfg.driver = &txDriver{tx: tx, drv: c.driver} return &Tx{ - ctx: ctx, - config: cfg, - AccessPolicy: NewAccessPolicyClient(cfg), - Agent: NewAgentClient(cfg), - Group: NewGroupClient(cfg), - GroupMembership: NewGroupMembershipClient(cfg), - PolicyBinding: NewPolicyBindingClient(cfg), - Project: NewProjectClient(cfg), - User: NewUserClient(cfg), + ctx: ctx, + config: cfg, + AccessPolicy: NewAccessPolicyClient(cfg), + Agent: NewAgentClient(cfg), + AllowListEntry: NewAllowListEntryClient(cfg), + ApiKey: NewApiKeyClient(cfg), + BrokerDispatch: NewBrokerDispatchClient(cfg), + BrokerJoinToken: NewBrokerJoinTokenClient(cfg), + BrokerSecret: NewBrokerSecretClient(cfg), + EnvVar: NewEnvVarClient(cfg), + GCPServiceAccount: NewGCPServiceAccountClient(cfg), + GithubInstallation: NewGithubInstallationClient(cfg), + Group: NewGroupClient(cfg), + GroupMembership: NewGroupMembershipClient(cfg), + HarnessConfig: NewHarnessConfigClient(cfg), + InviteCode: NewInviteCodeClient(cfg), + MaintenanceOperation: NewMaintenanceOperationClient(cfg), + MaintenanceOperationRun: NewMaintenanceOperationRunClient(cfg), + Message: NewMessageClient(cfg), + Notification: NewNotificationClient(cfg), + NotificationSubscription: NewNotificationSubscriptionClient(cfg), + PolicyBinding: NewPolicyBindingClient(cfg), + Project: NewProjectClient(cfg), + ProjectContributor: NewProjectContributorClient(cfg), + ProjectSyncState: NewProjectSyncStateClient(cfg), + RuntimeBroker: NewRuntimeBrokerClient(cfg), + Schedule: NewScheduleClient(cfg), + ScheduledEvent: NewScheduledEventClient(cfg), + Secret: NewSecretClient(cfg), + SubscriptionTemplate: NewSubscriptionTemplateClient(cfg), + Template: NewTemplateClient(cfg), + User: NewUserClient(cfg), + UserAccessToken: NewUserAccessTokenClient(cfg), }, nil } @@ -216,8 +360,14 @@ func (c *Client) Close() error { // In order to add hooks to a specific client, call: `client.Node.Use(...)`. func (c *Client) Use(hooks ...Hook) { for _, n := range []interface{ Use(...Hook) }{ - c.AccessPolicy, c.Agent, c.Group, c.GroupMembership, c.PolicyBinding, c.Project, - c.User, + c.AccessPolicy, c.Agent, c.AllowListEntry, c.ApiKey, c.BrokerDispatch, + c.BrokerJoinToken, c.BrokerSecret, c.EnvVar, c.GCPServiceAccount, + c.GithubInstallation, c.Group, c.GroupMembership, c.HarnessConfig, + c.InviteCode, c.MaintenanceOperation, c.MaintenanceOperationRun, c.Message, + c.Notification, c.NotificationSubscription, c.PolicyBinding, c.Project, + c.ProjectContributor, c.ProjectSyncState, c.RuntimeBroker, c.Schedule, + c.ScheduledEvent, c.Secret, c.SubscriptionTemplate, c.Template, c.User, + c.UserAccessToken, } { n.Use(hooks...) } @@ -227,8 +377,14 @@ func (c *Client) Use(hooks ...Hook) { // In order to add interceptors to a specific client, call: `client.Node.Intercept(...)`. func (c *Client) Intercept(interceptors ...Interceptor) { for _, n := range []interface{ Intercept(...Interceptor) }{ - c.AccessPolicy, c.Agent, c.Group, c.GroupMembership, c.PolicyBinding, c.Project, - c.User, + c.AccessPolicy, c.Agent, c.AllowListEntry, c.ApiKey, c.BrokerDispatch, + c.BrokerJoinToken, c.BrokerSecret, c.EnvVar, c.GCPServiceAccount, + c.GithubInstallation, c.Group, c.GroupMembership, c.HarnessConfig, + c.InviteCode, c.MaintenanceOperation, c.MaintenanceOperationRun, c.Message, + c.Notification, c.NotificationSubscription, c.PolicyBinding, c.Project, + c.ProjectContributor, c.ProjectSyncState, c.RuntimeBroker, c.Schedule, + c.ScheduledEvent, c.Secret, c.SubscriptionTemplate, c.Template, c.User, + c.UserAccessToken, } { n.Intercept(interceptors...) } @@ -241,16 +397,64 @@ func (c *Client) Mutate(ctx context.Context, m Mutation) (Value, error) { return c.AccessPolicy.mutate(ctx, m) case *AgentMutation: return c.Agent.mutate(ctx, m) + case *AllowListEntryMutation: + return c.AllowListEntry.mutate(ctx, m) + case *ApiKeyMutation: + return c.ApiKey.mutate(ctx, m) + case *BrokerDispatchMutation: + return c.BrokerDispatch.mutate(ctx, m) + case *BrokerJoinTokenMutation: + return c.BrokerJoinToken.mutate(ctx, m) + case *BrokerSecretMutation: + return c.BrokerSecret.mutate(ctx, m) + case *EnvVarMutation: + return c.EnvVar.mutate(ctx, m) + case *GCPServiceAccountMutation: + return c.GCPServiceAccount.mutate(ctx, m) + case *GithubInstallationMutation: + return c.GithubInstallation.mutate(ctx, m) case *GroupMutation: return c.Group.mutate(ctx, m) case *GroupMembershipMutation: return c.GroupMembership.mutate(ctx, m) + case *HarnessConfigMutation: + return c.HarnessConfig.mutate(ctx, m) + case *InviteCodeMutation: + return c.InviteCode.mutate(ctx, m) + case *MaintenanceOperationMutation: + return c.MaintenanceOperation.mutate(ctx, m) + case *MaintenanceOperationRunMutation: + return c.MaintenanceOperationRun.mutate(ctx, m) + case *MessageMutation: + return c.Message.mutate(ctx, m) + case *NotificationMutation: + return c.Notification.mutate(ctx, m) + case *NotificationSubscriptionMutation: + return c.NotificationSubscription.mutate(ctx, m) case *PolicyBindingMutation: return c.PolicyBinding.mutate(ctx, m) case *ProjectMutation: return c.Project.mutate(ctx, m) + case *ProjectContributorMutation: + return c.ProjectContributor.mutate(ctx, m) + case *ProjectSyncStateMutation: + return c.ProjectSyncState.mutate(ctx, m) + case *RuntimeBrokerMutation: + return c.RuntimeBroker.mutate(ctx, m) + case *ScheduleMutation: + return c.Schedule.mutate(ctx, m) + case *ScheduledEventMutation: + return c.ScheduledEvent.mutate(ctx, m) + case *SecretMutation: + return c.Secret.mutate(ctx, m) + case *SubscriptionTemplateMutation: + return c.SubscriptionTemplate.mutate(ctx, m) + case *TemplateMutation: + return c.Template.mutate(ctx, m) case *UserMutation: return c.User.mutate(ctx, m) + case *UserAccessTokenMutation: + return c.UserAccessToken.mutate(ctx, m) default: return nil, fmt.Errorf("ent: unknown mutation type %T", m) } @@ -529,38 +733,6 @@ func (c *AgentClient) QueryProject(_m *Agent) *ProjectQuery { return query } -// QueryCreator queries the creator edge of a Agent. -func (c *AgentClient) QueryCreator(_m *Agent) *UserQuery { - query := (&UserClient{config: c.config}).Query() - query.path = func(context.Context) (fromV *sql.Selector, _ error) { - id := _m.ID - step := sqlgraph.NewStep( - sqlgraph.From(agent.Table, agent.FieldID, id), - sqlgraph.To(user.Table, user.FieldID), - sqlgraph.Edge(sqlgraph.M2O, true, agent.CreatorTable, agent.CreatorColumn), - ) - fromV = sqlgraph.Neighbors(_m.driver.Dialect(), step) - return fromV, nil - } - return query -} - -// QueryOwner queries the owner edge of a Agent. -func (c *AgentClient) QueryOwner(_m *Agent) *UserQuery { - query := (&UserClient{config: c.config}).Query() - query.path = func(context.Context) (fromV *sql.Selector, _ error) { - id := _m.ID - step := sqlgraph.NewStep( - sqlgraph.From(agent.Table, agent.FieldID, id), - sqlgraph.To(user.Table, user.FieldID), - sqlgraph.Edge(sqlgraph.M2O, true, agent.OwnerTable, agent.OwnerColumn), - ) - fromV = sqlgraph.Neighbors(_m.driver.Dialect(), step) - return fromV, nil - } - return query -} - // QueryMemberships queries the memberships edge of a Agent. func (c *AgentClient) QueryMemberships(_m *Agent) *GroupMembershipQuery { query := (&GroupMembershipClient{config: c.config}).Query() @@ -618,107 +790,107 @@ func (c *AgentClient) mutate(ctx context.Context, m *AgentMutation) (Value, erro } } -// GroupClient is a client for the Group schema. -type GroupClient struct { +// AllowListEntryClient is a client for the AllowListEntry schema. +type AllowListEntryClient struct { config } -// NewGroupClient returns a client for the Group from the given config. -func NewGroupClient(c config) *GroupClient { - return &GroupClient{config: c} +// NewAllowListEntryClient returns a client for the AllowListEntry from the given config. +func NewAllowListEntryClient(c config) *AllowListEntryClient { + return &AllowListEntryClient{config: c} } // Use adds a list of mutation hooks to the hooks stack. -// A call to `Use(f, g, h)` equals to `group.Hooks(f(g(h())))`. -func (c *GroupClient) Use(hooks ...Hook) { - c.hooks.Group = append(c.hooks.Group, hooks...) +// A call to `Use(f, g, h)` equals to `allowlistentry.Hooks(f(g(h())))`. +func (c *AllowListEntryClient) Use(hooks ...Hook) { + c.hooks.AllowListEntry = append(c.hooks.AllowListEntry, hooks...) } // Intercept adds a list of query interceptors to the interceptors stack. -// A call to `Intercept(f, g, h)` equals to `group.Intercept(f(g(h())))`. -func (c *GroupClient) Intercept(interceptors ...Interceptor) { - c.inters.Group = append(c.inters.Group, interceptors...) +// A call to `Intercept(f, g, h)` equals to `allowlistentry.Intercept(f(g(h())))`. +func (c *AllowListEntryClient) Intercept(interceptors ...Interceptor) { + c.inters.AllowListEntry = append(c.inters.AllowListEntry, interceptors...) } -// Create returns a builder for creating a Group entity. -func (c *GroupClient) Create() *GroupCreate { - mutation := newGroupMutation(c.config, OpCreate) - return &GroupCreate{config: c.config, hooks: c.Hooks(), mutation: mutation} +// Create returns a builder for creating a AllowListEntry entity. +func (c *AllowListEntryClient) Create() *AllowListEntryCreate { + mutation := newAllowListEntryMutation(c.config, OpCreate) + return &AllowListEntryCreate{config: c.config, hooks: c.Hooks(), mutation: mutation} } -// CreateBulk returns a builder for creating a bulk of Group entities. -func (c *GroupClient) CreateBulk(builders ...*GroupCreate) *GroupCreateBulk { - return &GroupCreateBulk{config: c.config, builders: builders} +// CreateBulk returns a builder for creating a bulk of AllowListEntry entities. +func (c *AllowListEntryClient) CreateBulk(builders ...*AllowListEntryCreate) *AllowListEntryCreateBulk { + return &AllowListEntryCreateBulk{config: c.config, builders: builders} } // MapCreateBulk creates a bulk creation builder from the given slice. For each item in the slice, the function creates // a builder and applies setFunc on it. -func (c *GroupClient) MapCreateBulk(slice any, setFunc func(*GroupCreate, int)) *GroupCreateBulk { +func (c *AllowListEntryClient) MapCreateBulk(slice any, setFunc func(*AllowListEntryCreate, int)) *AllowListEntryCreateBulk { rv := reflect.ValueOf(slice) if rv.Kind() != reflect.Slice { - return &GroupCreateBulk{err: fmt.Errorf("calling to GroupClient.MapCreateBulk with wrong type %T, need slice", slice)} + return &AllowListEntryCreateBulk{err: fmt.Errorf("calling to AllowListEntryClient.MapCreateBulk with wrong type %T, need slice", slice)} } - builders := make([]*GroupCreate, rv.Len()) + builders := make([]*AllowListEntryCreate, rv.Len()) for i := 0; i < rv.Len(); i++ { builders[i] = c.Create() setFunc(builders[i], i) } - return &GroupCreateBulk{config: c.config, builders: builders} + return &AllowListEntryCreateBulk{config: c.config, builders: builders} } -// Update returns an update builder for Group. -func (c *GroupClient) Update() *GroupUpdate { - mutation := newGroupMutation(c.config, OpUpdate) - return &GroupUpdate{config: c.config, hooks: c.Hooks(), mutation: mutation} +// Update returns an update builder for AllowListEntry. +func (c *AllowListEntryClient) Update() *AllowListEntryUpdate { + mutation := newAllowListEntryMutation(c.config, OpUpdate) + return &AllowListEntryUpdate{config: c.config, hooks: c.Hooks(), mutation: mutation} } // UpdateOne returns an update builder for the given entity. -func (c *GroupClient) UpdateOne(_m *Group) *GroupUpdateOne { - mutation := newGroupMutation(c.config, OpUpdateOne, withGroup(_m)) - return &GroupUpdateOne{config: c.config, hooks: c.Hooks(), mutation: mutation} +func (c *AllowListEntryClient) UpdateOne(_m *AllowListEntry) *AllowListEntryUpdateOne { + mutation := newAllowListEntryMutation(c.config, OpUpdateOne, withAllowListEntry(_m)) + return &AllowListEntryUpdateOne{config: c.config, hooks: c.Hooks(), mutation: mutation} } // UpdateOneID returns an update builder for the given id. -func (c *GroupClient) UpdateOneID(id uuid.UUID) *GroupUpdateOne { - mutation := newGroupMutation(c.config, OpUpdateOne, withGroupID(id)) - return &GroupUpdateOne{config: c.config, hooks: c.Hooks(), mutation: mutation} +func (c *AllowListEntryClient) UpdateOneID(id uuid.UUID) *AllowListEntryUpdateOne { + mutation := newAllowListEntryMutation(c.config, OpUpdateOne, withAllowListEntryID(id)) + return &AllowListEntryUpdateOne{config: c.config, hooks: c.Hooks(), mutation: mutation} } -// Delete returns a delete builder for Group. -func (c *GroupClient) Delete() *GroupDelete { - mutation := newGroupMutation(c.config, OpDelete) - return &GroupDelete{config: c.config, hooks: c.Hooks(), mutation: mutation} +// Delete returns a delete builder for AllowListEntry. +func (c *AllowListEntryClient) Delete() *AllowListEntryDelete { + mutation := newAllowListEntryMutation(c.config, OpDelete) + return &AllowListEntryDelete{config: c.config, hooks: c.Hooks(), mutation: mutation} } // DeleteOne returns a builder for deleting the given entity. -func (c *GroupClient) DeleteOne(_m *Group) *GroupDeleteOne { +func (c *AllowListEntryClient) DeleteOne(_m *AllowListEntry) *AllowListEntryDeleteOne { return c.DeleteOneID(_m.ID) } // DeleteOneID returns a builder for deleting the given entity by its id. -func (c *GroupClient) DeleteOneID(id uuid.UUID) *GroupDeleteOne { - builder := c.Delete().Where(group.ID(id)) +func (c *AllowListEntryClient) DeleteOneID(id uuid.UUID) *AllowListEntryDeleteOne { + builder := c.Delete().Where(allowlistentry.ID(id)) builder.mutation.id = &id builder.mutation.op = OpDeleteOne - return &GroupDeleteOne{builder} + return &AllowListEntryDeleteOne{builder} } -// Query returns a query builder for Group. -func (c *GroupClient) Query() *GroupQuery { - return &GroupQuery{ +// Query returns a query builder for AllowListEntry. +func (c *AllowListEntryClient) Query() *AllowListEntryQuery { + return &AllowListEntryQuery{ config: c.config, - ctx: &QueryContext{Type: TypeGroup}, + ctx: &QueryContext{Type: TypeAllowListEntry}, inters: c.Interceptors(), } } -// Get returns a Group entity by its id. -func (c *GroupClient) Get(ctx context.Context, id uuid.UUID) (*Group, error) { - return c.Query().Where(group.ID(id)).Only(ctx) +// Get returns a AllowListEntry entity by its id. +func (c *AllowListEntryClient) Get(ctx context.Context, id uuid.UUID) (*AllowListEntry, error) { + return c.Query().Where(allowlistentry.ID(id)).Only(ctx) } // GetX is like Get, but panics if an error occurs. -func (c *GroupClient) GetX(ctx context.Context, id uuid.UUID) *Group { +func (c *AllowListEntryClient) GetX(ctx context.Context, id uuid.UUID) *AllowListEntry { obj, err := c.Get(ctx, id) if err != nil { panic(err) @@ -726,212 +898,132 @@ func (c *GroupClient) GetX(ctx context.Context, id uuid.UUID) *Group { return obj } -// QueryMemberships queries the memberships edge of a Group. -func (c *GroupClient) QueryMemberships(_m *Group) *GroupMembershipQuery { - query := (&GroupMembershipClient{config: c.config}).Query() - query.path = func(context.Context) (fromV *sql.Selector, _ error) { - id := _m.ID - step := sqlgraph.NewStep( - sqlgraph.From(group.Table, group.FieldID, id), - sqlgraph.To(groupmembership.Table, groupmembership.FieldID), - sqlgraph.Edge(sqlgraph.O2M, false, group.MembershipsTable, group.MembershipsColumn), - ) - fromV = sqlgraph.Neighbors(_m.driver.Dialect(), step) - return fromV, nil - } - return query -} - -// QueryParentGroups queries the parent_groups edge of a Group. -func (c *GroupClient) QueryParentGroups(_m *Group) *GroupQuery { - query := (&GroupClient{config: c.config}).Query() - query.path = func(context.Context) (fromV *sql.Selector, _ error) { - id := _m.ID - step := sqlgraph.NewStep( - sqlgraph.From(group.Table, group.FieldID, id), - sqlgraph.To(group.Table, group.FieldID), - sqlgraph.Edge(sqlgraph.M2M, true, group.ParentGroupsTable, group.ParentGroupsPrimaryKey...), - ) - fromV = sqlgraph.Neighbors(_m.driver.Dialect(), step) - return fromV, nil - } - return query -} - -// QueryChildGroups queries the child_groups edge of a Group. -func (c *GroupClient) QueryChildGroups(_m *Group) *GroupQuery { - query := (&GroupClient{config: c.config}).Query() - query.path = func(context.Context) (fromV *sql.Selector, _ error) { - id := _m.ID - step := sqlgraph.NewStep( - sqlgraph.From(group.Table, group.FieldID, id), - sqlgraph.To(group.Table, group.FieldID), - sqlgraph.Edge(sqlgraph.M2M, false, group.ChildGroupsTable, group.ChildGroupsPrimaryKey...), - ) - fromV = sqlgraph.Neighbors(_m.driver.Dialect(), step) - return fromV, nil - } - return query -} - -// QueryOwner queries the owner edge of a Group. -func (c *GroupClient) QueryOwner(_m *Group) *UserQuery { - query := (&UserClient{config: c.config}).Query() - query.path = func(context.Context) (fromV *sql.Selector, _ error) { - id := _m.ID - step := sqlgraph.NewStep( - sqlgraph.From(group.Table, group.FieldID, id), - sqlgraph.To(user.Table, user.FieldID), - sqlgraph.Edge(sqlgraph.M2O, true, group.OwnerTable, group.OwnerColumn), - ) - fromV = sqlgraph.Neighbors(_m.driver.Dialect(), step) - return fromV, nil - } - return query -} - -// QueryPolicyBindings queries the policy_bindings edge of a Group. -func (c *GroupClient) QueryPolicyBindings(_m *Group) *PolicyBindingQuery { - query := (&PolicyBindingClient{config: c.config}).Query() - query.path = func(context.Context) (fromV *sql.Selector, _ error) { - id := _m.ID - step := sqlgraph.NewStep( - sqlgraph.From(group.Table, group.FieldID, id), - sqlgraph.To(policybinding.Table, policybinding.FieldID), - sqlgraph.Edge(sqlgraph.O2M, true, group.PolicyBindingsTable, group.PolicyBindingsColumn), - ) - fromV = sqlgraph.Neighbors(_m.driver.Dialect(), step) - return fromV, nil - } - return query -} - // Hooks returns the client hooks. -func (c *GroupClient) Hooks() []Hook { - return c.hooks.Group +func (c *AllowListEntryClient) Hooks() []Hook { + return c.hooks.AllowListEntry } // Interceptors returns the client interceptors. -func (c *GroupClient) Interceptors() []Interceptor { - return c.inters.Group +func (c *AllowListEntryClient) Interceptors() []Interceptor { + return c.inters.AllowListEntry } -func (c *GroupClient) mutate(ctx context.Context, m *GroupMutation) (Value, error) { +func (c *AllowListEntryClient) mutate(ctx context.Context, m *AllowListEntryMutation) (Value, error) { switch m.Op() { case OpCreate: - return (&GroupCreate{config: c.config, hooks: c.Hooks(), mutation: m}).Save(ctx) + return (&AllowListEntryCreate{config: c.config, hooks: c.Hooks(), mutation: m}).Save(ctx) case OpUpdate: - return (&GroupUpdate{config: c.config, hooks: c.Hooks(), mutation: m}).Save(ctx) + return (&AllowListEntryUpdate{config: c.config, hooks: c.Hooks(), mutation: m}).Save(ctx) case OpUpdateOne: - return (&GroupUpdateOne{config: c.config, hooks: c.Hooks(), mutation: m}).Save(ctx) + return (&AllowListEntryUpdateOne{config: c.config, hooks: c.Hooks(), mutation: m}).Save(ctx) case OpDelete, OpDeleteOne: - return (&GroupDelete{config: c.config, hooks: c.Hooks(), mutation: m}).Exec(ctx) + return (&AllowListEntryDelete{config: c.config, hooks: c.Hooks(), mutation: m}).Exec(ctx) default: - return nil, fmt.Errorf("ent: unknown Group mutation op: %q", m.Op()) + return nil, fmt.Errorf("ent: unknown AllowListEntry mutation op: %q", m.Op()) } } -// GroupMembershipClient is a client for the GroupMembership schema. -type GroupMembershipClient struct { +// ApiKeyClient is a client for the ApiKey schema. +type ApiKeyClient struct { config } -// NewGroupMembershipClient returns a client for the GroupMembership from the given config. -func NewGroupMembershipClient(c config) *GroupMembershipClient { - return &GroupMembershipClient{config: c} +// NewApiKeyClient returns a client for the ApiKey from the given config. +func NewApiKeyClient(c config) *ApiKeyClient { + return &ApiKeyClient{config: c} } // Use adds a list of mutation hooks to the hooks stack. -// A call to `Use(f, g, h)` equals to `groupmembership.Hooks(f(g(h())))`. -func (c *GroupMembershipClient) Use(hooks ...Hook) { - c.hooks.GroupMembership = append(c.hooks.GroupMembership, hooks...) +// A call to `Use(f, g, h)` equals to `apikey.Hooks(f(g(h())))`. +func (c *ApiKeyClient) Use(hooks ...Hook) { + c.hooks.ApiKey = append(c.hooks.ApiKey, hooks...) } // Intercept adds a list of query interceptors to the interceptors stack. -// A call to `Intercept(f, g, h)` equals to `groupmembership.Intercept(f(g(h())))`. -func (c *GroupMembershipClient) Intercept(interceptors ...Interceptor) { - c.inters.GroupMembership = append(c.inters.GroupMembership, interceptors...) +// A call to `Intercept(f, g, h)` equals to `apikey.Intercept(f(g(h())))`. +func (c *ApiKeyClient) Intercept(interceptors ...Interceptor) { + c.inters.ApiKey = append(c.inters.ApiKey, interceptors...) } -// Create returns a builder for creating a GroupMembership entity. -func (c *GroupMembershipClient) Create() *GroupMembershipCreate { - mutation := newGroupMembershipMutation(c.config, OpCreate) - return &GroupMembershipCreate{config: c.config, hooks: c.Hooks(), mutation: mutation} +// Create returns a builder for creating a ApiKey entity. +func (c *ApiKeyClient) Create() *ApiKeyCreate { + mutation := newApiKeyMutation(c.config, OpCreate) + return &ApiKeyCreate{config: c.config, hooks: c.Hooks(), mutation: mutation} } -// CreateBulk returns a builder for creating a bulk of GroupMembership entities. -func (c *GroupMembershipClient) CreateBulk(builders ...*GroupMembershipCreate) *GroupMembershipCreateBulk { - return &GroupMembershipCreateBulk{config: c.config, builders: builders} +// CreateBulk returns a builder for creating a bulk of ApiKey entities. +func (c *ApiKeyClient) CreateBulk(builders ...*ApiKeyCreate) *ApiKeyCreateBulk { + return &ApiKeyCreateBulk{config: c.config, builders: builders} } // MapCreateBulk creates a bulk creation builder from the given slice. For each item in the slice, the function creates // a builder and applies setFunc on it. -func (c *GroupMembershipClient) MapCreateBulk(slice any, setFunc func(*GroupMembershipCreate, int)) *GroupMembershipCreateBulk { +func (c *ApiKeyClient) MapCreateBulk(slice any, setFunc func(*ApiKeyCreate, int)) *ApiKeyCreateBulk { rv := reflect.ValueOf(slice) if rv.Kind() != reflect.Slice { - return &GroupMembershipCreateBulk{err: fmt.Errorf("calling to GroupMembershipClient.MapCreateBulk with wrong type %T, need slice", slice)} + return &ApiKeyCreateBulk{err: fmt.Errorf("calling to ApiKeyClient.MapCreateBulk with wrong type %T, need slice", slice)} } - builders := make([]*GroupMembershipCreate, rv.Len()) + builders := make([]*ApiKeyCreate, rv.Len()) for i := 0; i < rv.Len(); i++ { builders[i] = c.Create() setFunc(builders[i], i) } - return &GroupMembershipCreateBulk{config: c.config, builders: builders} + return &ApiKeyCreateBulk{config: c.config, builders: builders} } -// Update returns an update builder for GroupMembership. -func (c *GroupMembershipClient) Update() *GroupMembershipUpdate { - mutation := newGroupMembershipMutation(c.config, OpUpdate) - return &GroupMembershipUpdate{config: c.config, hooks: c.Hooks(), mutation: mutation} +// Update returns an update builder for ApiKey. +func (c *ApiKeyClient) Update() *ApiKeyUpdate { + mutation := newApiKeyMutation(c.config, OpUpdate) + return &ApiKeyUpdate{config: c.config, hooks: c.Hooks(), mutation: mutation} } // UpdateOne returns an update builder for the given entity. -func (c *GroupMembershipClient) UpdateOne(_m *GroupMembership) *GroupMembershipUpdateOne { - mutation := newGroupMembershipMutation(c.config, OpUpdateOne, withGroupMembership(_m)) - return &GroupMembershipUpdateOne{config: c.config, hooks: c.Hooks(), mutation: mutation} +func (c *ApiKeyClient) UpdateOne(_m *ApiKey) *ApiKeyUpdateOne { + mutation := newApiKeyMutation(c.config, OpUpdateOne, withApiKey(_m)) + return &ApiKeyUpdateOne{config: c.config, hooks: c.Hooks(), mutation: mutation} } // UpdateOneID returns an update builder for the given id. -func (c *GroupMembershipClient) UpdateOneID(id uuid.UUID) *GroupMembershipUpdateOne { - mutation := newGroupMembershipMutation(c.config, OpUpdateOne, withGroupMembershipID(id)) - return &GroupMembershipUpdateOne{config: c.config, hooks: c.Hooks(), mutation: mutation} +func (c *ApiKeyClient) UpdateOneID(id uuid.UUID) *ApiKeyUpdateOne { + mutation := newApiKeyMutation(c.config, OpUpdateOne, withApiKeyID(id)) + return &ApiKeyUpdateOne{config: c.config, hooks: c.Hooks(), mutation: mutation} } -// Delete returns a delete builder for GroupMembership. -func (c *GroupMembershipClient) Delete() *GroupMembershipDelete { - mutation := newGroupMembershipMutation(c.config, OpDelete) - return &GroupMembershipDelete{config: c.config, hooks: c.Hooks(), mutation: mutation} +// Delete returns a delete builder for ApiKey. +func (c *ApiKeyClient) Delete() *ApiKeyDelete { + mutation := newApiKeyMutation(c.config, OpDelete) + return &ApiKeyDelete{config: c.config, hooks: c.Hooks(), mutation: mutation} } // DeleteOne returns a builder for deleting the given entity. -func (c *GroupMembershipClient) DeleteOne(_m *GroupMembership) *GroupMembershipDeleteOne { +func (c *ApiKeyClient) DeleteOne(_m *ApiKey) *ApiKeyDeleteOne { return c.DeleteOneID(_m.ID) } // DeleteOneID returns a builder for deleting the given entity by its id. -func (c *GroupMembershipClient) DeleteOneID(id uuid.UUID) *GroupMembershipDeleteOne { - builder := c.Delete().Where(groupmembership.ID(id)) +func (c *ApiKeyClient) DeleteOneID(id uuid.UUID) *ApiKeyDeleteOne { + builder := c.Delete().Where(apikey.ID(id)) builder.mutation.id = &id builder.mutation.op = OpDeleteOne - return &GroupMembershipDeleteOne{builder} + return &ApiKeyDeleteOne{builder} } -// Query returns a query builder for GroupMembership. -func (c *GroupMembershipClient) Query() *GroupMembershipQuery { - return &GroupMembershipQuery{ +// Query returns a query builder for ApiKey. +func (c *ApiKeyClient) Query() *ApiKeyQuery { + return &ApiKeyQuery{ config: c.config, - ctx: &QueryContext{Type: TypeGroupMembership}, + ctx: &QueryContext{Type: TypeApiKey}, inters: c.Interceptors(), } } -// Get returns a GroupMembership entity by its id. -func (c *GroupMembershipClient) Get(ctx context.Context, id uuid.UUID) (*GroupMembership, error) { - return c.Query().Where(groupmembership.ID(id)).Only(ctx) +// Get returns a ApiKey entity by its id. +func (c *ApiKeyClient) Get(ctx context.Context, id uuid.UUID) (*ApiKey, error) { + return c.Query().Where(apikey.ID(id)).Only(ctx) } // GetX is like Get, but panics if an error occurs. -func (c *GroupMembershipClient) GetX(ctx context.Context, id uuid.UUID) *GroupMembership { +func (c *ApiKeyClient) GetX(ctx context.Context, id uuid.UUID) *ApiKey { obj, err := c.Get(ctx, id) if err != nil { panic(err) @@ -939,180 +1031,3399 @@ func (c *GroupMembershipClient) GetX(ctx context.Context, id uuid.UUID) *GroupMe return obj } -// QueryGroup queries the group edge of a GroupMembership. -func (c *GroupMembershipClient) QueryGroup(_m *GroupMembership) *GroupQuery { - query := (&GroupClient{config: c.config}).Query() - query.path = func(context.Context) (fromV *sql.Selector, _ error) { - id := _m.ID - step := sqlgraph.NewStep( - sqlgraph.From(groupmembership.Table, groupmembership.FieldID, id), - sqlgraph.To(group.Table, group.FieldID), - sqlgraph.Edge(sqlgraph.M2O, true, groupmembership.GroupTable, groupmembership.GroupColumn), - ) - fromV = sqlgraph.Neighbors(_m.driver.Dialect(), step) - return fromV, nil - } - return query +// Hooks returns the client hooks. +func (c *ApiKeyClient) Hooks() []Hook { + return c.hooks.ApiKey } -// QueryUser queries the user edge of a GroupMembership. -func (c *GroupMembershipClient) QueryUser(_m *GroupMembership) *UserQuery { - query := (&UserClient{config: c.config}).Query() - query.path = func(context.Context) (fromV *sql.Selector, _ error) { - id := _m.ID - step := sqlgraph.NewStep( - sqlgraph.From(groupmembership.Table, groupmembership.FieldID, id), - sqlgraph.To(user.Table, user.FieldID), - sqlgraph.Edge(sqlgraph.M2O, false, groupmembership.UserTable, groupmembership.UserColumn), - ) - fromV = sqlgraph.Neighbors(_m.driver.Dialect(), step) - return fromV, nil +// Interceptors returns the client interceptors. +func (c *ApiKeyClient) Interceptors() []Interceptor { + return c.inters.ApiKey +} + +func (c *ApiKeyClient) mutate(ctx context.Context, m *ApiKeyMutation) (Value, error) { + switch m.Op() { + case OpCreate: + return (&ApiKeyCreate{config: c.config, hooks: c.Hooks(), mutation: m}).Save(ctx) + case OpUpdate: + return (&ApiKeyUpdate{config: c.config, hooks: c.Hooks(), mutation: m}).Save(ctx) + case OpUpdateOne: + return (&ApiKeyUpdateOne{config: c.config, hooks: c.Hooks(), mutation: m}).Save(ctx) + case OpDelete, OpDeleteOne: + return (&ApiKeyDelete{config: c.config, hooks: c.Hooks(), mutation: m}).Exec(ctx) + default: + return nil, fmt.Errorf("ent: unknown ApiKey mutation op: %q", m.Op()) } - return query } -// QueryAgent queries the agent edge of a GroupMembership. -func (c *GroupMembershipClient) QueryAgent(_m *GroupMembership) *AgentQuery { - query := (&AgentClient{config: c.config}).Query() - query.path = func(context.Context) (fromV *sql.Selector, _ error) { - id := _m.ID - step := sqlgraph.NewStep( - sqlgraph.From(groupmembership.Table, groupmembership.FieldID, id), +// BrokerDispatchClient is a client for the BrokerDispatch schema. +type BrokerDispatchClient struct { + config +} + +// NewBrokerDispatchClient returns a client for the BrokerDispatch from the given config. +func NewBrokerDispatchClient(c config) *BrokerDispatchClient { + return &BrokerDispatchClient{config: c} +} + +// Use adds a list of mutation hooks to the hooks stack. +// A call to `Use(f, g, h)` equals to `brokerdispatch.Hooks(f(g(h())))`. +func (c *BrokerDispatchClient) Use(hooks ...Hook) { + c.hooks.BrokerDispatch = append(c.hooks.BrokerDispatch, hooks...) +} + +// Intercept adds a list of query interceptors to the interceptors stack. +// A call to `Intercept(f, g, h)` equals to `brokerdispatch.Intercept(f(g(h())))`. +func (c *BrokerDispatchClient) Intercept(interceptors ...Interceptor) { + c.inters.BrokerDispatch = append(c.inters.BrokerDispatch, interceptors...) +} + +// Create returns a builder for creating a BrokerDispatch entity. +func (c *BrokerDispatchClient) Create() *BrokerDispatchCreate { + mutation := newBrokerDispatchMutation(c.config, OpCreate) + return &BrokerDispatchCreate{config: c.config, hooks: c.Hooks(), mutation: mutation} +} + +// CreateBulk returns a builder for creating a bulk of BrokerDispatch entities. +func (c *BrokerDispatchClient) CreateBulk(builders ...*BrokerDispatchCreate) *BrokerDispatchCreateBulk { + return &BrokerDispatchCreateBulk{config: c.config, builders: builders} +} + +// MapCreateBulk creates a bulk creation builder from the given slice. For each item in the slice, the function creates +// a builder and applies setFunc on it. +func (c *BrokerDispatchClient) MapCreateBulk(slice any, setFunc func(*BrokerDispatchCreate, int)) *BrokerDispatchCreateBulk { + rv := reflect.ValueOf(slice) + if rv.Kind() != reflect.Slice { + return &BrokerDispatchCreateBulk{err: fmt.Errorf("calling to BrokerDispatchClient.MapCreateBulk with wrong type %T, need slice", slice)} + } + builders := make([]*BrokerDispatchCreate, rv.Len()) + for i := 0; i < rv.Len(); i++ { + builders[i] = c.Create() + setFunc(builders[i], i) + } + return &BrokerDispatchCreateBulk{config: c.config, builders: builders} +} + +// Update returns an update builder for BrokerDispatch. +func (c *BrokerDispatchClient) Update() *BrokerDispatchUpdate { + mutation := newBrokerDispatchMutation(c.config, OpUpdate) + return &BrokerDispatchUpdate{config: c.config, hooks: c.Hooks(), mutation: mutation} +} + +// UpdateOne returns an update builder for the given entity. +func (c *BrokerDispatchClient) UpdateOne(_m *BrokerDispatch) *BrokerDispatchUpdateOne { + mutation := newBrokerDispatchMutation(c.config, OpUpdateOne, withBrokerDispatch(_m)) + return &BrokerDispatchUpdateOne{config: c.config, hooks: c.Hooks(), mutation: mutation} +} + +// UpdateOneID returns an update builder for the given id. +func (c *BrokerDispatchClient) UpdateOneID(id uuid.UUID) *BrokerDispatchUpdateOne { + mutation := newBrokerDispatchMutation(c.config, OpUpdateOne, withBrokerDispatchID(id)) + return &BrokerDispatchUpdateOne{config: c.config, hooks: c.Hooks(), mutation: mutation} +} + +// Delete returns a delete builder for BrokerDispatch. +func (c *BrokerDispatchClient) Delete() *BrokerDispatchDelete { + mutation := newBrokerDispatchMutation(c.config, OpDelete) + return &BrokerDispatchDelete{config: c.config, hooks: c.Hooks(), mutation: mutation} +} + +// DeleteOne returns a builder for deleting the given entity. +func (c *BrokerDispatchClient) DeleteOne(_m *BrokerDispatch) *BrokerDispatchDeleteOne { + return c.DeleteOneID(_m.ID) +} + +// DeleteOneID returns a builder for deleting the given entity by its id. +func (c *BrokerDispatchClient) DeleteOneID(id uuid.UUID) *BrokerDispatchDeleteOne { + builder := c.Delete().Where(brokerdispatch.ID(id)) + builder.mutation.id = &id + builder.mutation.op = OpDeleteOne + return &BrokerDispatchDeleteOne{builder} +} + +// Query returns a query builder for BrokerDispatch. +func (c *BrokerDispatchClient) Query() *BrokerDispatchQuery { + return &BrokerDispatchQuery{ + config: c.config, + ctx: &QueryContext{Type: TypeBrokerDispatch}, + inters: c.Interceptors(), + } +} + +// Get returns a BrokerDispatch entity by its id. +func (c *BrokerDispatchClient) Get(ctx context.Context, id uuid.UUID) (*BrokerDispatch, error) { + return c.Query().Where(brokerdispatch.ID(id)).Only(ctx) +} + +// GetX is like Get, but panics if an error occurs. +func (c *BrokerDispatchClient) GetX(ctx context.Context, id uuid.UUID) *BrokerDispatch { + obj, err := c.Get(ctx, id) + if err != nil { + panic(err) + } + return obj +} + +// Hooks returns the client hooks. +func (c *BrokerDispatchClient) Hooks() []Hook { + return c.hooks.BrokerDispatch +} + +// Interceptors returns the client interceptors. +func (c *BrokerDispatchClient) Interceptors() []Interceptor { + return c.inters.BrokerDispatch +} + +func (c *BrokerDispatchClient) mutate(ctx context.Context, m *BrokerDispatchMutation) (Value, error) { + switch m.Op() { + case OpCreate: + return (&BrokerDispatchCreate{config: c.config, hooks: c.Hooks(), mutation: m}).Save(ctx) + case OpUpdate: + return (&BrokerDispatchUpdate{config: c.config, hooks: c.Hooks(), mutation: m}).Save(ctx) + case OpUpdateOne: + return (&BrokerDispatchUpdateOne{config: c.config, hooks: c.Hooks(), mutation: m}).Save(ctx) + case OpDelete, OpDeleteOne: + return (&BrokerDispatchDelete{config: c.config, hooks: c.Hooks(), mutation: m}).Exec(ctx) + default: + return nil, fmt.Errorf("ent: unknown BrokerDispatch mutation op: %q", m.Op()) + } +} + +// BrokerJoinTokenClient is a client for the BrokerJoinToken schema. +type BrokerJoinTokenClient struct { + config +} + +// NewBrokerJoinTokenClient returns a client for the BrokerJoinToken from the given config. +func NewBrokerJoinTokenClient(c config) *BrokerJoinTokenClient { + return &BrokerJoinTokenClient{config: c} +} + +// Use adds a list of mutation hooks to the hooks stack. +// A call to `Use(f, g, h)` equals to `brokerjointoken.Hooks(f(g(h())))`. +func (c *BrokerJoinTokenClient) Use(hooks ...Hook) { + c.hooks.BrokerJoinToken = append(c.hooks.BrokerJoinToken, hooks...) +} + +// Intercept adds a list of query interceptors to the interceptors stack. +// A call to `Intercept(f, g, h)` equals to `brokerjointoken.Intercept(f(g(h())))`. +func (c *BrokerJoinTokenClient) Intercept(interceptors ...Interceptor) { + c.inters.BrokerJoinToken = append(c.inters.BrokerJoinToken, interceptors...) +} + +// Create returns a builder for creating a BrokerJoinToken entity. +func (c *BrokerJoinTokenClient) Create() *BrokerJoinTokenCreate { + mutation := newBrokerJoinTokenMutation(c.config, OpCreate) + return &BrokerJoinTokenCreate{config: c.config, hooks: c.Hooks(), mutation: mutation} +} + +// CreateBulk returns a builder for creating a bulk of BrokerJoinToken entities. +func (c *BrokerJoinTokenClient) CreateBulk(builders ...*BrokerJoinTokenCreate) *BrokerJoinTokenCreateBulk { + return &BrokerJoinTokenCreateBulk{config: c.config, builders: builders} +} + +// MapCreateBulk creates a bulk creation builder from the given slice. For each item in the slice, the function creates +// a builder and applies setFunc on it. +func (c *BrokerJoinTokenClient) MapCreateBulk(slice any, setFunc func(*BrokerJoinTokenCreate, int)) *BrokerJoinTokenCreateBulk { + rv := reflect.ValueOf(slice) + if rv.Kind() != reflect.Slice { + return &BrokerJoinTokenCreateBulk{err: fmt.Errorf("calling to BrokerJoinTokenClient.MapCreateBulk with wrong type %T, need slice", slice)} + } + builders := make([]*BrokerJoinTokenCreate, rv.Len()) + for i := 0; i < rv.Len(); i++ { + builders[i] = c.Create() + setFunc(builders[i], i) + } + return &BrokerJoinTokenCreateBulk{config: c.config, builders: builders} +} + +// Update returns an update builder for BrokerJoinToken. +func (c *BrokerJoinTokenClient) Update() *BrokerJoinTokenUpdate { + mutation := newBrokerJoinTokenMutation(c.config, OpUpdate) + return &BrokerJoinTokenUpdate{config: c.config, hooks: c.Hooks(), mutation: mutation} +} + +// UpdateOne returns an update builder for the given entity. +func (c *BrokerJoinTokenClient) UpdateOne(_m *BrokerJoinToken) *BrokerJoinTokenUpdateOne { + mutation := newBrokerJoinTokenMutation(c.config, OpUpdateOne, withBrokerJoinToken(_m)) + return &BrokerJoinTokenUpdateOne{config: c.config, hooks: c.Hooks(), mutation: mutation} +} + +// UpdateOneID returns an update builder for the given id. +func (c *BrokerJoinTokenClient) UpdateOneID(id uuid.UUID) *BrokerJoinTokenUpdateOne { + mutation := newBrokerJoinTokenMutation(c.config, OpUpdateOne, withBrokerJoinTokenID(id)) + return &BrokerJoinTokenUpdateOne{config: c.config, hooks: c.Hooks(), mutation: mutation} +} + +// Delete returns a delete builder for BrokerJoinToken. +func (c *BrokerJoinTokenClient) Delete() *BrokerJoinTokenDelete { + mutation := newBrokerJoinTokenMutation(c.config, OpDelete) + return &BrokerJoinTokenDelete{config: c.config, hooks: c.Hooks(), mutation: mutation} +} + +// DeleteOne returns a builder for deleting the given entity. +func (c *BrokerJoinTokenClient) DeleteOne(_m *BrokerJoinToken) *BrokerJoinTokenDeleteOne { + return c.DeleteOneID(_m.ID) +} + +// DeleteOneID returns a builder for deleting the given entity by its id. +func (c *BrokerJoinTokenClient) DeleteOneID(id uuid.UUID) *BrokerJoinTokenDeleteOne { + builder := c.Delete().Where(brokerjointoken.ID(id)) + builder.mutation.id = &id + builder.mutation.op = OpDeleteOne + return &BrokerJoinTokenDeleteOne{builder} +} + +// Query returns a query builder for BrokerJoinToken. +func (c *BrokerJoinTokenClient) Query() *BrokerJoinTokenQuery { + return &BrokerJoinTokenQuery{ + config: c.config, + ctx: &QueryContext{Type: TypeBrokerJoinToken}, + inters: c.Interceptors(), + } +} + +// Get returns a BrokerJoinToken entity by its id. +func (c *BrokerJoinTokenClient) Get(ctx context.Context, id uuid.UUID) (*BrokerJoinToken, error) { + return c.Query().Where(brokerjointoken.ID(id)).Only(ctx) +} + +// GetX is like Get, but panics if an error occurs. +func (c *BrokerJoinTokenClient) GetX(ctx context.Context, id uuid.UUID) *BrokerJoinToken { + obj, err := c.Get(ctx, id) + if err != nil { + panic(err) + } + return obj +} + +// Hooks returns the client hooks. +func (c *BrokerJoinTokenClient) Hooks() []Hook { + return c.hooks.BrokerJoinToken +} + +// Interceptors returns the client interceptors. +func (c *BrokerJoinTokenClient) Interceptors() []Interceptor { + return c.inters.BrokerJoinToken +} + +func (c *BrokerJoinTokenClient) mutate(ctx context.Context, m *BrokerJoinTokenMutation) (Value, error) { + switch m.Op() { + case OpCreate: + return (&BrokerJoinTokenCreate{config: c.config, hooks: c.Hooks(), mutation: m}).Save(ctx) + case OpUpdate: + return (&BrokerJoinTokenUpdate{config: c.config, hooks: c.Hooks(), mutation: m}).Save(ctx) + case OpUpdateOne: + return (&BrokerJoinTokenUpdateOne{config: c.config, hooks: c.Hooks(), mutation: m}).Save(ctx) + case OpDelete, OpDeleteOne: + return (&BrokerJoinTokenDelete{config: c.config, hooks: c.Hooks(), mutation: m}).Exec(ctx) + default: + return nil, fmt.Errorf("ent: unknown BrokerJoinToken mutation op: %q", m.Op()) + } +} + +// BrokerSecretClient is a client for the BrokerSecret schema. +type BrokerSecretClient struct { + config +} + +// NewBrokerSecretClient returns a client for the BrokerSecret from the given config. +func NewBrokerSecretClient(c config) *BrokerSecretClient { + return &BrokerSecretClient{config: c} +} + +// Use adds a list of mutation hooks to the hooks stack. +// A call to `Use(f, g, h)` equals to `brokersecret.Hooks(f(g(h())))`. +func (c *BrokerSecretClient) Use(hooks ...Hook) { + c.hooks.BrokerSecret = append(c.hooks.BrokerSecret, hooks...) +} + +// Intercept adds a list of query interceptors to the interceptors stack. +// A call to `Intercept(f, g, h)` equals to `brokersecret.Intercept(f(g(h())))`. +func (c *BrokerSecretClient) Intercept(interceptors ...Interceptor) { + c.inters.BrokerSecret = append(c.inters.BrokerSecret, interceptors...) +} + +// Create returns a builder for creating a BrokerSecret entity. +func (c *BrokerSecretClient) Create() *BrokerSecretCreate { + mutation := newBrokerSecretMutation(c.config, OpCreate) + return &BrokerSecretCreate{config: c.config, hooks: c.Hooks(), mutation: mutation} +} + +// CreateBulk returns a builder for creating a bulk of BrokerSecret entities. +func (c *BrokerSecretClient) CreateBulk(builders ...*BrokerSecretCreate) *BrokerSecretCreateBulk { + return &BrokerSecretCreateBulk{config: c.config, builders: builders} +} + +// MapCreateBulk creates a bulk creation builder from the given slice. For each item in the slice, the function creates +// a builder and applies setFunc on it. +func (c *BrokerSecretClient) MapCreateBulk(slice any, setFunc func(*BrokerSecretCreate, int)) *BrokerSecretCreateBulk { + rv := reflect.ValueOf(slice) + if rv.Kind() != reflect.Slice { + return &BrokerSecretCreateBulk{err: fmt.Errorf("calling to BrokerSecretClient.MapCreateBulk with wrong type %T, need slice", slice)} + } + builders := make([]*BrokerSecretCreate, rv.Len()) + for i := 0; i < rv.Len(); i++ { + builders[i] = c.Create() + setFunc(builders[i], i) + } + return &BrokerSecretCreateBulk{config: c.config, builders: builders} +} + +// Update returns an update builder for BrokerSecret. +func (c *BrokerSecretClient) Update() *BrokerSecretUpdate { + mutation := newBrokerSecretMutation(c.config, OpUpdate) + return &BrokerSecretUpdate{config: c.config, hooks: c.Hooks(), mutation: mutation} +} + +// UpdateOne returns an update builder for the given entity. +func (c *BrokerSecretClient) UpdateOne(_m *BrokerSecret) *BrokerSecretUpdateOne { + mutation := newBrokerSecretMutation(c.config, OpUpdateOne, withBrokerSecret(_m)) + return &BrokerSecretUpdateOne{config: c.config, hooks: c.Hooks(), mutation: mutation} +} + +// UpdateOneID returns an update builder for the given id. +func (c *BrokerSecretClient) UpdateOneID(id uuid.UUID) *BrokerSecretUpdateOne { + mutation := newBrokerSecretMutation(c.config, OpUpdateOne, withBrokerSecretID(id)) + return &BrokerSecretUpdateOne{config: c.config, hooks: c.Hooks(), mutation: mutation} +} + +// Delete returns a delete builder for BrokerSecret. +func (c *BrokerSecretClient) Delete() *BrokerSecretDelete { + mutation := newBrokerSecretMutation(c.config, OpDelete) + return &BrokerSecretDelete{config: c.config, hooks: c.Hooks(), mutation: mutation} +} + +// DeleteOne returns a builder for deleting the given entity. +func (c *BrokerSecretClient) DeleteOne(_m *BrokerSecret) *BrokerSecretDeleteOne { + return c.DeleteOneID(_m.ID) +} + +// DeleteOneID returns a builder for deleting the given entity by its id. +func (c *BrokerSecretClient) DeleteOneID(id uuid.UUID) *BrokerSecretDeleteOne { + builder := c.Delete().Where(brokersecret.ID(id)) + builder.mutation.id = &id + builder.mutation.op = OpDeleteOne + return &BrokerSecretDeleteOne{builder} +} + +// Query returns a query builder for BrokerSecret. +func (c *BrokerSecretClient) Query() *BrokerSecretQuery { + return &BrokerSecretQuery{ + config: c.config, + ctx: &QueryContext{Type: TypeBrokerSecret}, + inters: c.Interceptors(), + } +} + +// Get returns a BrokerSecret entity by its id. +func (c *BrokerSecretClient) Get(ctx context.Context, id uuid.UUID) (*BrokerSecret, error) { + return c.Query().Where(brokersecret.ID(id)).Only(ctx) +} + +// GetX is like Get, but panics if an error occurs. +func (c *BrokerSecretClient) GetX(ctx context.Context, id uuid.UUID) *BrokerSecret { + obj, err := c.Get(ctx, id) + if err != nil { + panic(err) + } + return obj +} + +// Hooks returns the client hooks. +func (c *BrokerSecretClient) Hooks() []Hook { + return c.hooks.BrokerSecret +} + +// Interceptors returns the client interceptors. +func (c *BrokerSecretClient) Interceptors() []Interceptor { + return c.inters.BrokerSecret +} + +func (c *BrokerSecretClient) mutate(ctx context.Context, m *BrokerSecretMutation) (Value, error) { + switch m.Op() { + case OpCreate: + return (&BrokerSecretCreate{config: c.config, hooks: c.Hooks(), mutation: m}).Save(ctx) + case OpUpdate: + return (&BrokerSecretUpdate{config: c.config, hooks: c.Hooks(), mutation: m}).Save(ctx) + case OpUpdateOne: + return (&BrokerSecretUpdateOne{config: c.config, hooks: c.Hooks(), mutation: m}).Save(ctx) + case OpDelete, OpDeleteOne: + return (&BrokerSecretDelete{config: c.config, hooks: c.Hooks(), mutation: m}).Exec(ctx) + default: + return nil, fmt.Errorf("ent: unknown BrokerSecret mutation op: %q", m.Op()) + } +} + +// EnvVarClient is a client for the EnvVar schema. +type EnvVarClient struct { + config +} + +// NewEnvVarClient returns a client for the EnvVar from the given config. +func NewEnvVarClient(c config) *EnvVarClient { + return &EnvVarClient{config: c} +} + +// Use adds a list of mutation hooks to the hooks stack. +// A call to `Use(f, g, h)` equals to `envvar.Hooks(f(g(h())))`. +func (c *EnvVarClient) Use(hooks ...Hook) { + c.hooks.EnvVar = append(c.hooks.EnvVar, hooks...) +} + +// Intercept adds a list of query interceptors to the interceptors stack. +// A call to `Intercept(f, g, h)` equals to `envvar.Intercept(f(g(h())))`. +func (c *EnvVarClient) Intercept(interceptors ...Interceptor) { + c.inters.EnvVar = append(c.inters.EnvVar, interceptors...) +} + +// Create returns a builder for creating a EnvVar entity. +func (c *EnvVarClient) Create() *EnvVarCreate { + mutation := newEnvVarMutation(c.config, OpCreate) + return &EnvVarCreate{config: c.config, hooks: c.Hooks(), mutation: mutation} +} + +// CreateBulk returns a builder for creating a bulk of EnvVar entities. +func (c *EnvVarClient) CreateBulk(builders ...*EnvVarCreate) *EnvVarCreateBulk { + return &EnvVarCreateBulk{config: c.config, builders: builders} +} + +// MapCreateBulk creates a bulk creation builder from the given slice. For each item in the slice, the function creates +// a builder and applies setFunc on it. +func (c *EnvVarClient) MapCreateBulk(slice any, setFunc func(*EnvVarCreate, int)) *EnvVarCreateBulk { + rv := reflect.ValueOf(slice) + if rv.Kind() != reflect.Slice { + return &EnvVarCreateBulk{err: fmt.Errorf("calling to EnvVarClient.MapCreateBulk with wrong type %T, need slice", slice)} + } + builders := make([]*EnvVarCreate, rv.Len()) + for i := 0; i < rv.Len(); i++ { + builders[i] = c.Create() + setFunc(builders[i], i) + } + return &EnvVarCreateBulk{config: c.config, builders: builders} +} + +// Update returns an update builder for EnvVar. +func (c *EnvVarClient) Update() *EnvVarUpdate { + mutation := newEnvVarMutation(c.config, OpUpdate) + return &EnvVarUpdate{config: c.config, hooks: c.Hooks(), mutation: mutation} +} + +// UpdateOne returns an update builder for the given entity. +func (c *EnvVarClient) UpdateOne(_m *EnvVar) *EnvVarUpdateOne { + mutation := newEnvVarMutation(c.config, OpUpdateOne, withEnvVar(_m)) + return &EnvVarUpdateOne{config: c.config, hooks: c.Hooks(), mutation: mutation} +} + +// UpdateOneID returns an update builder for the given id. +func (c *EnvVarClient) UpdateOneID(id uuid.UUID) *EnvVarUpdateOne { + mutation := newEnvVarMutation(c.config, OpUpdateOne, withEnvVarID(id)) + return &EnvVarUpdateOne{config: c.config, hooks: c.Hooks(), mutation: mutation} +} + +// Delete returns a delete builder for EnvVar. +func (c *EnvVarClient) Delete() *EnvVarDelete { + mutation := newEnvVarMutation(c.config, OpDelete) + return &EnvVarDelete{config: c.config, hooks: c.Hooks(), mutation: mutation} +} + +// DeleteOne returns a builder for deleting the given entity. +func (c *EnvVarClient) DeleteOne(_m *EnvVar) *EnvVarDeleteOne { + return c.DeleteOneID(_m.ID) +} + +// DeleteOneID returns a builder for deleting the given entity by its id. +func (c *EnvVarClient) DeleteOneID(id uuid.UUID) *EnvVarDeleteOne { + builder := c.Delete().Where(envvar.ID(id)) + builder.mutation.id = &id + builder.mutation.op = OpDeleteOne + return &EnvVarDeleteOne{builder} +} + +// Query returns a query builder for EnvVar. +func (c *EnvVarClient) Query() *EnvVarQuery { + return &EnvVarQuery{ + config: c.config, + ctx: &QueryContext{Type: TypeEnvVar}, + inters: c.Interceptors(), + } +} + +// Get returns a EnvVar entity by its id. +func (c *EnvVarClient) Get(ctx context.Context, id uuid.UUID) (*EnvVar, error) { + return c.Query().Where(envvar.ID(id)).Only(ctx) +} + +// GetX is like Get, but panics if an error occurs. +func (c *EnvVarClient) GetX(ctx context.Context, id uuid.UUID) *EnvVar { + obj, err := c.Get(ctx, id) + if err != nil { + panic(err) + } + return obj +} + +// Hooks returns the client hooks. +func (c *EnvVarClient) Hooks() []Hook { + return c.hooks.EnvVar +} + +// Interceptors returns the client interceptors. +func (c *EnvVarClient) Interceptors() []Interceptor { + return c.inters.EnvVar +} + +func (c *EnvVarClient) mutate(ctx context.Context, m *EnvVarMutation) (Value, error) { + switch m.Op() { + case OpCreate: + return (&EnvVarCreate{config: c.config, hooks: c.Hooks(), mutation: m}).Save(ctx) + case OpUpdate: + return (&EnvVarUpdate{config: c.config, hooks: c.Hooks(), mutation: m}).Save(ctx) + case OpUpdateOne: + return (&EnvVarUpdateOne{config: c.config, hooks: c.Hooks(), mutation: m}).Save(ctx) + case OpDelete, OpDeleteOne: + return (&EnvVarDelete{config: c.config, hooks: c.Hooks(), mutation: m}).Exec(ctx) + default: + return nil, fmt.Errorf("ent: unknown EnvVar mutation op: %q", m.Op()) + } +} + +// GCPServiceAccountClient is a client for the GCPServiceAccount schema. +type GCPServiceAccountClient struct { + config +} + +// NewGCPServiceAccountClient returns a client for the GCPServiceAccount from the given config. +func NewGCPServiceAccountClient(c config) *GCPServiceAccountClient { + return &GCPServiceAccountClient{config: c} +} + +// Use adds a list of mutation hooks to the hooks stack. +// A call to `Use(f, g, h)` equals to `gcpserviceaccount.Hooks(f(g(h())))`. +func (c *GCPServiceAccountClient) Use(hooks ...Hook) { + c.hooks.GCPServiceAccount = append(c.hooks.GCPServiceAccount, hooks...) +} + +// Intercept adds a list of query interceptors to the interceptors stack. +// A call to `Intercept(f, g, h)` equals to `gcpserviceaccount.Intercept(f(g(h())))`. +func (c *GCPServiceAccountClient) Intercept(interceptors ...Interceptor) { + c.inters.GCPServiceAccount = append(c.inters.GCPServiceAccount, interceptors...) +} + +// Create returns a builder for creating a GCPServiceAccount entity. +func (c *GCPServiceAccountClient) Create() *GCPServiceAccountCreate { + mutation := newGCPServiceAccountMutation(c.config, OpCreate) + return &GCPServiceAccountCreate{config: c.config, hooks: c.Hooks(), mutation: mutation} +} + +// CreateBulk returns a builder for creating a bulk of GCPServiceAccount entities. +func (c *GCPServiceAccountClient) CreateBulk(builders ...*GCPServiceAccountCreate) *GCPServiceAccountCreateBulk { + return &GCPServiceAccountCreateBulk{config: c.config, builders: builders} +} + +// MapCreateBulk creates a bulk creation builder from the given slice. For each item in the slice, the function creates +// a builder and applies setFunc on it. +func (c *GCPServiceAccountClient) MapCreateBulk(slice any, setFunc func(*GCPServiceAccountCreate, int)) *GCPServiceAccountCreateBulk { + rv := reflect.ValueOf(slice) + if rv.Kind() != reflect.Slice { + return &GCPServiceAccountCreateBulk{err: fmt.Errorf("calling to GCPServiceAccountClient.MapCreateBulk with wrong type %T, need slice", slice)} + } + builders := make([]*GCPServiceAccountCreate, rv.Len()) + for i := 0; i < rv.Len(); i++ { + builders[i] = c.Create() + setFunc(builders[i], i) + } + return &GCPServiceAccountCreateBulk{config: c.config, builders: builders} +} + +// Update returns an update builder for GCPServiceAccount. +func (c *GCPServiceAccountClient) Update() *GCPServiceAccountUpdate { + mutation := newGCPServiceAccountMutation(c.config, OpUpdate) + return &GCPServiceAccountUpdate{config: c.config, hooks: c.Hooks(), mutation: mutation} +} + +// UpdateOne returns an update builder for the given entity. +func (c *GCPServiceAccountClient) UpdateOne(_m *GCPServiceAccount) *GCPServiceAccountUpdateOne { + mutation := newGCPServiceAccountMutation(c.config, OpUpdateOne, withGCPServiceAccount(_m)) + return &GCPServiceAccountUpdateOne{config: c.config, hooks: c.Hooks(), mutation: mutation} +} + +// UpdateOneID returns an update builder for the given id. +func (c *GCPServiceAccountClient) UpdateOneID(id uuid.UUID) *GCPServiceAccountUpdateOne { + mutation := newGCPServiceAccountMutation(c.config, OpUpdateOne, withGCPServiceAccountID(id)) + return &GCPServiceAccountUpdateOne{config: c.config, hooks: c.Hooks(), mutation: mutation} +} + +// Delete returns a delete builder for GCPServiceAccount. +func (c *GCPServiceAccountClient) Delete() *GCPServiceAccountDelete { + mutation := newGCPServiceAccountMutation(c.config, OpDelete) + return &GCPServiceAccountDelete{config: c.config, hooks: c.Hooks(), mutation: mutation} +} + +// DeleteOne returns a builder for deleting the given entity. +func (c *GCPServiceAccountClient) DeleteOne(_m *GCPServiceAccount) *GCPServiceAccountDeleteOne { + return c.DeleteOneID(_m.ID) +} + +// DeleteOneID returns a builder for deleting the given entity by its id. +func (c *GCPServiceAccountClient) DeleteOneID(id uuid.UUID) *GCPServiceAccountDeleteOne { + builder := c.Delete().Where(gcpserviceaccount.ID(id)) + builder.mutation.id = &id + builder.mutation.op = OpDeleteOne + return &GCPServiceAccountDeleteOne{builder} +} + +// Query returns a query builder for GCPServiceAccount. +func (c *GCPServiceAccountClient) Query() *GCPServiceAccountQuery { + return &GCPServiceAccountQuery{ + config: c.config, + ctx: &QueryContext{Type: TypeGCPServiceAccount}, + inters: c.Interceptors(), + } +} + +// Get returns a GCPServiceAccount entity by its id. +func (c *GCPServiceAccountClient) Get(ctx context.Context, id uuid.UUID) (*GCPServiceAccount, error) { + return c.Query().Where(gcpserviceaccount.ID(id)).Only(ctx) +} + +// GetX is like Get, but panics if an error occurs. +func (c *GCPServiceAccountClient) GetX(ctx context.Context, id uuid.UUID) *GCPServiceAccount { + obj, err := c.Get(ctx, id) + if err != nil { + panic(err) + } + return obj +} + +// Hooks returns the client hooks. +func (c *GCPServiceAccountClient) Hooks() []Hook { + return c.hooks.GCPServiceAccount +} + +// Interceptors returns the client interceptors. +func (c *GCPServiceAccountClient) Interceptors() []Interceptor { + return c.inters.GCPServiceAccount +} + +func (c *GCPServiceAccountClient) mutate(ctx context.Context, m *GCPServiceAccountMutation) (Value, error) { + switch m.Op() { + case OpCreate: + return (&GCPServiceAccountCreate{config: c.config, hooks: c.Hooks(), mutation: m}).Save(ctx) + case OpUpdate: + return (&GCPServiceAccountUpdate{config: c.config, hooks: c.Hooks(), mutation: m}).Save(ctx) + case OpUpdateOne: + return (&GCPServiceAccountUpdateOne{config: c.config, hooks: c.Hooks(), mutation: m}).Save(ctx) + case OpDelete, OpDeleteOne: + return (&GCPServiceAccountDelete{config: c.config, hooks: c.Hooks(), mutation: m}).Exec(ctx) + default: + return nil, fmt.Errorf("ent: unknown GCPServiceAccount mutation op: %q", m.Op()) + } +} + +// GithubInstallationClient is a client for the GithubInstallation schema. +type GithubInstallationClient struct { + config +} + +// NewGithubInstallationClient returns a client for the GithubInstallation from the given config. +func NewGithubInstallationClient(c config) *GithubInstallationClient { + return &GithubInstallationClient{config: c} +} + +// Use adds a list of mutation hooks to the hooks stack. +// A call to `Use(f, g, h)` equals to `githubinstallation.Hooks(f(g(h())))`. +func (c *GithubInstallationClient) Use(hooks ...Hook) { + c.hooks.GithubInstallation = append(c.hooks.GithubInstallation, hooks...) +} + +// Intercept adds a list of query interceptors to the interceptors stack. +// A call to `Intercept(f, g, h)` equals to `githubinstallation.Intercept(f(g(h())))`. +func (c *GithubInstallationClient) Intercept(interceptors ...Interceptor) { + c.inters.GithubInstallation = append(c.inters.GithubInstallation, interceptors...) +} + +// Create returns a builder for creating a GithubInstallation entity. +func (c *GithubInstallationClient) Create() *GithubInstallationCreate { + mutation := newGithubInstallationMutation(c.config, OpCreate) + return &GithubInstallationCreate{config: c.config, hooks: c.Hooks(), mutation: mutation} +} + +// CreateBulk returns a builder for creating a bulk of GithubInstallation entities. +func (c *GithubInstallationClient) CreateBulk(builders ...*GithubInstallationCreate) *GithubInstallationCreateBulk { + return &GithubInstallationCreateBulk{config: c.config, builders: builders} +} + +// MapCreateBulk creates a bulk creation builder from the given slice. For each item in the slice, the function creates +// a builder and applies setFunc on it. +func (c *GithubInstallationClient) MapCreateBulk(slice any, setFunc func(*GithubInstallationCreate, int)) *GithubInstallationCreateBulk { + rv := reflect.ValueOf(slice) + if rv.Kind() != reflect.Slice { + return &GithubInstallationCreateBulk{err: fmt.Errorf("calling to GithubInstallationClient.MapCreateBulk with wrong type %T, need slice", slice)} + } + builders := make([]*GithubInstallationCreate, rv.Len()) + for i := 0; i < rv.Len(); i++ { + builders[i] = c.Create() + setFunc(builders[i], i) + } + return &GithubInstallationCreateBulk{config: c.config, builders: builders} +} + +// Update returns an update builder for GithubInstallation. +func (c *GithubInstallationClient) Update() *GithubInstallationUpdate { + mutation := newGithubInstallationMutation(c.config, OpUpdate) + return &GithubInstallationUpdate{config: c.config, hooks: c.Hooks(), mutation: mutation} +} + +// UpdateOne returns an update builder for the given entity. +func (c *GithubInstallationClient) UpdateOne(_m *GithubInstallation) *GithubInstallationUpdateOne { + mutation := newGithubInstallationMutation(c.config, OpUpdateOne, withGithubInstallation(_m)) + return &GithubInstallationUpdateOne{config: c.config, hooks: c.Hooks(), mutation: mutation} +} + +// UpdateOneID returns an update builder for the given id. +func (c *GithubInstallationClient) UpdateOneID(id int64) *GithubInstallationUpdateOne { + mutation := newGithubInstallationMutation(c.config, OpUpdateOne, withGithubInstallationID(id)) + return &GithubInstallationUpdateOne{config: c.config, hooks: c.Hooks(), mutation: mutation} +} + +// Delete returns a delete builder for GithubInstallation. +func (c *GithubInstallationClient) Delete() *GithubInstallationDelete { + mutation := newGithubInstallationMutation(c.config, OpDelete) + return &GithubInstallationDelete{config: c.config, hooks: c.Hooks(), mutation: mutation} +} + +// DeleteOne returns a builder for deleting the given entity. +func (c *GithubInstallationClient) DeleteOne(_m *GithubInstallation) *GithubInstallationDeleteOne { + return c.DeleteOneID(_m.ID) +} + +// DeleteOneID returns a builder for deleting the given entity by its id. +func (c *GithubInstallationClient) DeleteOneID(id int64) *GithubInstallationDeleteOne { + builder := c.Delete().Where(githubinstallation.ID(id)) + builder.mutation.id = &id + builder.mutation.op = OpDeleteOne + return &GithubInstallationDeleteOne{builder} +} + +// Query returns a query builder for GithubInstallation. +func (c *GithubInstallationClient) Query() *GithubInstallationQuery { + return &GithubInstallationQuery{ + config: c.config, + ctx: &QueryContext{Type: TypeGithubInstallation}, + inters: c.Interceptors(), + } +} + +// Get returns a GithubInstallation entity by its id. +func (c *GithubInstallationClient) Get(ctx context.Context, id int64) (*GithubInstallation, error) { + return c.Query().Where(githubinstallation.ID(id)).Only(ctx) +} + +// GetX is like Get, but panics if an error occurs. +func (c *GithubInstallationClient) GetX(ctx context.Context, id int64) *GithubInstallation { + obj, err := c.Get(ctx, id) + if err != nil { + panic(err) + } + return obj +} + +// Hooks returns the client hooks. +func (c *GithubInstallationClient) Hooks() []Hook { + return c.hooks.GithubInstallation +} + +// Interceptors returns the client interceptors. +func (c *GithubInstallationClient) Interceptors() []Interceptor { + return c.inters.GithubInstallation +} + +func (c *GithubInstallationClient) mutate(ctx context.Context, m *GithubInstallationMutation) (Value, error) { + switch m.Op() { + case OpCreate: + return (&GithubInstallationCreate{config: c.config, hooks: c.Hooks(), mutation: m}).Save(ctx) + case OpUpdate: + return (&GithubInstallationUpdate{config: c.config, hooks: c.Hooks(), mutation: m}).Save(ctx) + case OpUpdateOne: + return (&GithubInstallationUpdateOne{config: c.config, hooks: c.Hooks(), mutation: m}).Save(ctx) + case OpDelete, OpDeleteOne: + return (&GithubInstallationDelete{config: c.config, hooks: c.Hooks(), mutation: m}).Exec(ctx) + default: + return nil, fmt.Errorf("ent: unknown GithubInstallation mutation op: %q", m.Op()) + } +} + +// GroupClient is a client for the Group schema. +type GroupClient struct { + config +} + +// NewGroupClient returns a client for the Group from the given config. +func NewGroupClient(c config) *GroupClient { + return &GroupClient{config: c} +} + +// Use adds a list of mutation hooks to the hooks stack. +// A call to `Use(f, g, h)` equals to `group.Hooks(f(g(h())))`. +func (c *GroupClient) Use(hooks ...Hook) { + c.hooks.Group = append(c.hooks.Group, hooks...) +} + +// Intercept adds a list of query interceptors to the interceptors stack. +// A call to `Intercept(f, g, h)` equals to `group.Intercept(f(g(h())))`. +func (c *GroupClient) Intercept(interceptors ...Interceptor) { + c.inters.Group = append(c.inters.Group, interceptors...) +} + +// Create returns a builder for creating a Group entity. +func (c *GroupClient) Create() *GroupCreate { + mutation := newGroupMutation(c.config, OpCreate) + return &GroupCreate{config: c.config, hooks: c.Hooks(), mutation: mutation} +} + +// CreateBulk returns a builder for creating a bulk of Group entities. +func (c *GroupClient) CreateBulk(builders ...*GroupCreate) *GroupCreateBulk { + return &GroupCreateBulk{config: c.config, builders: builders} +} + +// MapCreateBulk creates a bulk creation builder from the given slice. For each item in the slice, the function creates +// a builder and applies setFunc on it. +func (c *GroupClient) MapCreateBulk(slice any, setFunc func(*GroupCreate, int)) *GroupCreateBulk { + rv := reflect.ValueOf(slice) + if rv.Kind() != reflect.Slice { + return &GroupCreateBulk{err: fmt.Errorf("calling to GroupClient.MapCreateBulk with wrong type %T, need slice", slice)} + } + builders := make([]*GroupCreate, rv.Len()) + for i := 0; i < rv.Len(); i++ { + builders[i] = c.Create() + setFunc(builders[i], i) + } + return &GroupCreateBulk{config: c.config, builders: builders} +} + +// Update returns an update builder for Group. +func (c *GroupClient) Update() *GroupUpdate { + mutation := newGroupMutation(c.config, OpUpdate) + return &GroupUpdate{config: c.config, hooks: c.Hooks(), mutation: mutation} +} + +// UpdateOne returns an update builder for the given entity. +func (c *GroupClient) UpdateOne(_m *Group) *GroupUpdateOne { + mutation := newGroupMutation(c.config, OpUpdateOne, withGroup(_m)) + return &GroupUpdateOne{config: c.config, hooks: c.Hooks(), mutation: mutation} +} + +// UpdateOneID returns an update builder for the given id. +func (c *GroupClient) UpdateOneID(id uuid.UUID) *GroupUpdateOne { + mutation := newGroupMutation(c.config, OpUpdateOne, withGroupID(id)) + return &GroupUpdateOne{config: c.config, hooks: c.Hooks(), mutation: mutation} +} + +// Delete returns a delete builder for Group. +func (c *GroupClient) Delete() *GroupDelete { + mutation := newGroupMutation(c.config, OpDelete) + return &GroupDelete{config: c.config, hooks: c.Hooks(), mutation: mutation} +} + +// DeleteOne returns a builder for deleting the given entity. +func (c *GroupClient) DeleteOne(_m *Group) *GroupDeleteOne { + return c.DeleteOneID(_m.ID) +} + +// DeleteOneID returns a builder for deleting the given entity by its id. +func (c *GroupClient) DeleteOneID(id uuid.UUID) *GroupDeleteOne { + builder := c.Delete().Where(group.ID(id)) + builder.mutation.id = &id + builder.mutation.op = OpDeleteOne + return &GroupDeleteOne{builder} +} + +// Query returns a query builder for Group. +func (c *GroupClient) Query() *GroupQuery { + return &GroupQuery{ + config: c.config, + ctx: &QueryContext{Type: TypeGroup}, + inters: c.Interceptors(), + } +} + +// Get returns a Group entity by its id. +func (c *GroupClient) Get(ctx context.Context, id uuid.UUID) (*Group, error) { + return c.Query().Where(group.ID(id)).Only(ctx) +} + +// GetX is like Get, but panics if an error occurs. +func (c *GroupClient) GetX(ctx context.Context, id uuid.UUID) *Group { + obj, err := c.Get(ctx, id) + if err != nil { + panic(err) + } + return obj +} + +// QueryMemberships queries the memberships edge of a Group. +func (c *GroupClient) QueryMemberships(_m *Group) *GroupMembershipQuery { + query := (&GroupMembershipClient{config: c.config}).Query() + query.path = func(context.Context) (fromV *sql.Selector, _ error) { + id := _m.ID + step := sqlgraph.NewStep( + sqlgraph.From(group.Table, group.FieldID, id), + sqlgraph.To(groupmembership.Table, groupmembership.FieldID), + sqlgraph.Edge(sqlgraph.O2M, false, group.MembershipsTable, group.MembershipsColumn), + ) + fromV = sqlgraph.Neighbors(_m.driver.Dialect(), step) + return fromV, nil + } + return query +} + +// QueryParentGroups queries the parent_groups edge of a Group. +func (c *GroupClient) QueryParentGroups(_m *Group) *GroupQuery { + query := (&GroupClient{config: c.config}).Query() + query.path = func(context.Context) (fromV *sql.Selector, _ error) { + id := _m.ID + step := sqlgraph.NewStep( + sqlgraph.From(group.Table, group.FieldID, id), + sqlgraph.To(group.Table, group.FieldID), + sqlgraph.Edge(sqlgraph.M2M, true, group.ParentGroupsTable, group.ParentGroupsPrimaryKey...), + ) + fromV = sqlgraph.Neighbors(_m.driver.Dialect(), step) + return fromV, nil + } + return query +} + +// QueryChildGroups queries the child_groups edge of a Group. +func (c *GroupClient) QueryChildGroups(_m *Group) *GroupQuery { + query := (&GroupClient{config: c.config}).Query() + query.path = func(context.Context) (fromV *sql.Selector, _ error) { + id := _m.ID + step := sqlgraph.NewStep( + sqlgraph.From(group.Table, group.FieldID, id), + sqlgraph.To(group.Table, group.FieldID), + sqlgraph.Edge(sqlgraph.M2M, false, group.ChildGroupsTable, group.ChildGroupsPrimaryKey...), + ) + fromV = sqlgraph.Neighbors(_m.driver.Dialect(), step) + return fromV, nil + } + return query +} + +// QueryOwner queries the owner edge of a Group. +func (c *GroupClient) QueryOwner(_m *Group) *UserQuery { + query := (&UserClient{config: c.config}).Query() + query.path = func(context.Context) (fromV *sql.Selector, _ error) { + id := _m.ID + step := sqlgraph.NewStep( + sqlgraph.From(group.Table, group.FieldID, id), + sqlgraph.To(user.Table, user.FieldID), + sqlgraph.Edge(sqlgraph.M2O, true, group.OwnerTable, group.OwnerColumn), + ) + fromV = sqlgraph.Neighbors(_m.driver.Dialect(), step) + return fromV, nil + } + return query +} + +// QueryPolicyBindings queries the policy_bindings edge of a Group. +func (c *GroupClient) QueryPolicyBindings(_m *Group) *PolicyBindingQuery { + query := (&PolicyBindingClient{config: c.config}).Query() + query.path = func(context.Context) (fromV *sql.Selector, _ error) { + id := _m.ID + step := sqlgraph.NewStep( + sqlgraph.From(group.Table, group.FieldID, id), + sqlgraph.To(policybinding.Table, policybinding.FieldID), + sqlgraph.Edge(sqlgraph.O2M, true, group.PolicyBindingsTable, group.PolicyBindingsColumn), + ) + fromV = sqlgraph.Neighbors(_m.driver.Dialect(), step) + return fromV, nil + } + return query +} + +// Hooks returns the client hooks. +func (c *GroupClient) Hooks() []Hook { + return c.hooks.Group +} + +// Interceptors returns the client interceptors. +func (c *GroupClient) Interceptors() []Interceptor { + return c.inters.Group +} + +func (c *GroupClient) mutate(ctx context.Context, m *GroupMutation) (Value, error) { + switch m.Op() { + case OpCreate: + return (&GroupCreate{config: c.config, hooks: c.Hooks(), mutation: m}).Save(ctx) + case OpUpdate: + return (&GroupUpdate{config: c.config, hooks: c.Hooks(), mutation: m}).Save(ctx) + case OpUpdateOne: + return (&GroupUpdateOne{config: c.config, hooks: c.Hooks(), mutation: m}).Save(ctx) + case OpDelete, OpDeleteOne: + return (&GroupDelete{config: c.config, hooks: c.Hooks(), mutation: m}).Exec(ctx) + default: + return nil, fmt.Errorf("ent: unknown Group mutation op: %q", m.Op()) + } +} + +// GroupMembershipClient is a client for the GroupMembership schema. +type GroupMembershipClient struct { + config +} + +// NewGroupMembershipClient returns a client for the GroupMembership from the given config. +func NewGroupMembershipClient(c config) *GroupMembershipClient { + return &GroupMembershipClient{config: c} +} + +// Use adds a list of mutation hooks to the hooks stack. +// A call to `Use(f, g, h)` equals to `groupmembership.Hooks(f(g(h())))`. +func (c *GroupMembershipClient) Use(hooks ...Hook) { + c.hooks.GroupMembership = append(c.hooks.GroupMembership, hooks...) +} + +// Intercept adds a list of query interceptors to the interceptors stack. +// A call to `Intercept(f, g, h)` equals to `groupmembership.Intercept(f(g(h())))`. +func (c *GroupMembershipClient) Intercept(interceptors ...Interceptor) { + c.inters.GroupMembership = append(c.inters.GroupMembership, interceptors...) +} + +// Create returns a builder for creating a GroupMembership entity. +func (c *GroupMembershipClient) Create() *GroupMembershipCreate { + mutation := newGroupMembershipMutation(c.config, OpCreate) + return &GroupMembershipCreate{config: c.config, hooks: c.Hooks(), mutation: mutation} +} + +// CreateBulk returns a builder for creating a bulk of GroupMembership entities. +func (c *GroupMembershipClient) CreateBulk(builders ...*GroupMembershipCreate) *GroupMembershipCreateBulk { + return &GroupMembershipCreateBulk{config: c.config, builders: builders} +} + +// MapCreateBulk creates a bulk creation builder from the given slice. For each item in the slice, the function creates +// a builder and applies setFunc on it. +func (c *GroupMembershipClient) MapCreateBulk(slice any, setFunc func(*GroupMembershipCreate, int)) *GroupMembershipCreateBulk { + rv := reflect.ValueOf(slice) + if rv.Kind() != reflect.Slice { + return &GroupMembershipCreateBulk{err: fmt.Errorf("calling to GroupMembershipClient.MapCreateBulk with wrong type %T, need slice", slice)} + } + builders := make([]*GroupMembershipCreate, rv.Len()) + for i := 0; i < rv.Len(); i++ { + builders[i] = c.Create() + setFunc(builders[i], i) + } + return &GroupMembershipCreateBulk{config: c.config, builders: builders} +} + +// Update returns an update builder for GroupMembership. +func (c *GroupMembershipClient) Update() *GroupMembershipUpdate { + mutation := newGroupMembershipMutation(c.config, OpUpdate) + return &GroupMembershipUpdate{config: c.config, hooks: c.Hooks(), mutation: mutation} +} + +// UpdateOne returns an update builder for the given entity. +func (c *GroupMembershipClient) UpdateOne(_m *GroupMembership) *GroupMembershipUpdateOne { + mutation := newGroupMembershipMutation(c.config, OpUpdateOne, withGroupMembership(_m)) + return &GroupMembershipUpdateOne{config: c.config, hooks: c.Hooks(), mutation: mutation} +} + +// UpdateOneID returns an update builder for the given id. +func (c *GroupMembershipClient) UpdateOneID(id uuid.UUID) *GroupMembershipUpdateOne { + mutation := newGroupMembershipMutation(c.config, OpUpdateOne, withGroupMembershipID(id)) + return &GroupMembershipUpdateOne{config: c.config, hooks: c.Hooks(), mutation: mutation} +} + +// Delete returns a delete builder for GroupMembership. +func (c *GroupMembershipClient) Delete() *GroupMembershipDelete { + mutation := newGroupMembershipMutation(c.config, OpDelete) + return &GroupMembershipDelete{config: c.config, hooks: c.Hooks(), mutation: mutation} +} + +// DeleteOne returns a builder for deleting the given entity. +func (c *GroupMembershipClient) DeleteOne(_m *GroupMembership) *GroupMembershipDeleteOne { + return c.DeleteOneID(_m.ID) +} + +// DeleteOneID returns a builder for deleting the given entity by its id. +func (c *GroupMembershipClient) DeleteOneID(id uuid.UUID) *GroupMembershipDeleteOne { + builder := c.Delete().Where(groupmembership.ID(id)) + builder.mutation.id = &id + builder.mutation.op = OpDeleteOne + return &GroupMembershipDeleteOne{builder} +} + +// Query returns a query builder for GroupMembership. +func (c *GroupMembershipClient) Query() *GroupMembershipQuery { + return &GroupMembershipQuery{ + config: c.config, + ctx: &QueryContext{Type: TypeGroupMembership}, + inters: c.Interceptors(), + } +} + +// Get returns a GroupMembership entity by its id. +func (c *GroupMembershipClient) Get(ctx context.Context, id uuid.UUID) (*GroupMembership, error) { + return c.Query().Where(groupmembership.ID(id)).Only(ctx) +} + +// GetX is like Get, but panics if an error occurs. +func (c *GroupMembershipClient) GetX(ctx context.Context, id uuid.UUID) *GroupMembership { + obj, err := c.Get(ctx, id) + if err != nil { + panic(err) + } + return obj +} + +// QueryGroup queries the group edge of a GroupMembership. +func (c *GroupMembershipClient) QueryGroup(_m *GroupMembership) *GroupQuery { + query := (&GroupClient{config: c.config}).Query() + query.path = func(context.Context) (fromV *sql.Selector, _ error) { + id := _m.ID + step := sqlgraph.NewStep( + sqlgraph.From(groupmembership.Table, groupmembership.FieldID, id), + sqlgraph.To(group.Table, group.FieldID), + sqlgraph.Edge(sqlgraph.M2O, true, groupmembership.GroupTable, groupmembership.GroupColumn), + ) + fromV = sqlgraph.Neighbors(_m.driver.Dialect(), step) + return fromV, nil + } + return query +} + +// QueryUser queries the user edge of a GroupMembership. +func (c *GroupMembershipClient) QueryUser(_m *GroupMembership) *UserQuery { + query := (&UserClient{config: c.config}).Query() + query.path = func(context.Context) (fromV *sql.Selector, _ error) { + id := _m.ID + step := sqlgraph.NewStep( + sqlgraph.From(groupmembership.Table, groupmembership.FieldID, id), + sqlgraph.To(user.Table, user.FieldID), + sqlgraph.Edge(sqlgraph.M2O, false, groupmembership.UserTable, groupmembership.UserColumn), + ) + fromV = sqlgraph.Neighbors(_m.driver.Dialect(), step) + return fromV, nil + } + return query +} + +// QueryAgent queries the agent edge of a GroupMembership. +func (c *GroupMembershipClient) QueryAgent(_m *GroupMembership) *AgentQuery { + query := (&AgentClient{config: c.config}).Query() + query.path = func(context.Context) (fromV *sql.Selector, _ error) { + id := _m.ID + step := sqlgraph.NewStep( + sqlgraph.From(groupmembership.Table, groupmembership.FieldID, id), + sqlgraph.To(agent.Table, agent.FieldID), + sqlgraph.Edge(sqlgraph.M2O, false, groupmembership.AgentTable, groupmembership.AgentColumn), + ) + fromV = sqlgraph.Neighbors(_m.driver.Dialect(), step) + return fromV, nil + } + return query +} + +// Hooks returns the client hooks. +func (c *GroupMembershipClient) Hooks() []Hook { + return c.hooks.GroupMembership +} + +// Interceptors returns the client interceptors. +func (c *GroupMembershipClient) Interceptors() []Interceptor { + return c.inters.GroupMembership +} + +func (c *GroupMembershipClient) mutate(ctx context.Context, m *GroupMembershipMutation) (Value, error) { + switch m.Op() { + case OpCreate: + return (&GroupMembershipCreate{config: c.config, hooks: c.Hooks(), mutation: m}).Save(ctx) + case OpUpdate: + return (&GroupMembershipUpdate{config: c.config, hooks: c.Hooks(), mutation: m}).Save(ctx) + case OpUpdateOne: + return (&GroupMembershipUpdateOne{config: c.config, hooks: c.Hooks(), mutation: m}).Save(ctx) + case OpDelete, OpDeleteOne: + return (&GroupMembershipDelete{config: c.config, hooks: c.Hooks(), mutation: m}).Exec(ctx) + default: + return nil, fmt.Errorf("ent: unknown GroupMembership mutation op: %q", m.Op()) + } +} + +// HarnessConfigClient is a client for the HarnessConfig schema. +type HarnessConfigClient struct { + config +} + +// NewHarnessConfigClient returns a client for the HarnessConfig from the given config. +func NewHarnessConfigClient(c config) *HarnessConfigClient { + return &HarnessConfigClient{config: c} +} + +// Use adds a list of mutation hooks to the hooks stack. +// A call to `Use(f, g, h)` equals to `harnessconfig.Hooks(f(g(h())))`. +func (c *HarnessConfigClient) Use(hooks ...Hook) { + c.hooks.HarnessConfig = append(c.hooks.HarnessConfig, hooks...) +} + +// Intercept adds a list of query interceptors to the interceptors stack. +// A call to `Intercept(f, g, h)` equals to `harnessconfig.Intercept(f(g(h())))`. +func (c *HarnessConfigClient) Intercept(interceptors ...Interceptor) { + c.inters.HarnessConfig = append(c.inters.HarnessConfig, interceptors...) +} + +// Create returns a builder for creating a HarnessConfig entity. +func (c *HarnessConfigClient) Create() *HarnessConfigCreate { + mutation := newHarnessConfigMutation(c.config, OpCreate) + return &HarnessConfigCreate{config: c.config, hooks: c.Hooks(), mutation: mutation} +} + +// CreateBulk returns a builder for creating a bulk of HarnessConfig entities. +func (c *HarnessConfigClient) CreateBulk(builders ...*HarnessConfigCreate) *HarnessConfigCreateBulk { + return &HarnessConfigCreateBulk{config: c.config, builders: builders} +} + +// MapCreateBulk creates a bulk creation builder from the given slice. For each item in the slice, the function creates +// a builder and applies setFunc on it. +func (c *HarnessConfigClient) MapCreateBulk(slice any, setFunc func(*HarnessConfigCreate, int)) *HarnessConfigCreateBulk { + rv := reflect.ValueOf(slice) + if rv.Kind() != reflect.Slice { + return &HarnessConfigCreateBulk{err: fmt.Errorf("calling to HarnessConfigClient.MapCreateBulk with wrong type %T, need slice", slice)} + } + builders := make([]*HarnessConfigCreate, rv.Len()) + for i := 0; i < rv.Len(); i++ { + builders[i] = c.Create() + setFunc(builders[i], i) + } + return &HarnessConfigCreateBulk{config: c.config, builders: builders} +} + +// Update returns an update builder for HarnessConfig. +func (c *HarnessConfigClient) Update() *HarnessConfigUpdate { + mutation := newHarnessConfigMutation(c.config, OpUpdate) + return &HarnessConfigUpdate{config: c.config, hooks: c.Hooks(), mutation: mutation} +} + +// UpdateOne returns an update builder for the given entity. +func (c *HarnessConfigClient) UpdateOne(_m *HarnessConfig) *HarnessConfigUpdateOne { + mutation := newHarnessConfigMutation(c.config, OpUpdateOne, withHarnessConfig(_m)) + return &HarnessConfigUpdateOne{config: c.config, hooks: c.Hooks(), mutation: mutation} +} + +// UpdateOneID returns an update builder for the given id. +func (c *HarnessConfigClient) UpdateOneID(id uuid.UUID) *HarnessConfigUpdateOne { + mutation := newHarnessConfigMutation(c.config, OpUpdateOne, withHarnessConfigID(id)) + return &HarnessConfigUpdateOne{config: c.config, hooks: c.Hooks(), mutation: mutation} +} + +// Delete returns a delete builder for HarnessConfig. +func (c *HarnessConfigClient) Delete() *HarnessConfigDelete { + mutation := newHarnessConfigMutation(c.config, OpDelete) + return &HarnessConfigDelete{config: c.config, hooks: c.Hooks(), mutation: mutation} +} + +// DeleteOne returns a builder for deleting the given entity. +func (c *HarnessConfigClient) DeleteOne(_m *HarnessConfig) *HarnessConfigDeleteOne { + return c.DeleteOneID(_m.ID) +} + +// DeleteOneID returns a builder for deleting the given entity by its id. +func (c *HarnessConfigClient) DeleteOneID(id uuid.UUID) *HarnessConfigDeleteOne { + builder := c.Delete().Where(harnessconfig.ID(id)) + builder.mutation.id = &id + builder.mutation.op = OpDeleteOne + return &HarnessConfigDeleteOne{builder} +} + +// Query returns a query builder for HarnessConfig. +func (c *HarnessConfigClient) Query() *HarnessConfigQuery { + return &HarnessConfigQuery{ + config: c.config, + ctx: &QueryContext{Type: TypeHarnessConfig}, + inters: c.Interceptors(), + } +} + +// Get returns a HarnessConfig entity by its id. +func (c *HarnessConfigClient) Get(ctx context.Context, id uuid.UUID) (*HarnessConfig, error) { + return c.Query().Where(harnessconfig.ID(id)).Only(ctx) +} + +// GetX is like Get, but panics if an error occurs. +func (c *HarnessConfigClient) GetX(ctx context.Context, id uuid.UUID) *HarnessConfig { + obj, err := c.Get(ctx, id) + if err != nil { + panic(err) + } + return obj +} + +// Hooks returns the client hooks. +func (c *HarnessConfigClient) Hooks() []Hook { + return c.hooks.HarnessConfig +} + +// Interceptors returns the client interceptors. +func (c *HarnessConfigClient) Interceptors() []Interceptor { + return c.inters.HarnessConfig +} + +func (c *HarnessConfigClient) mutate(ctx context.Context, m *HarnessConfigMutation) (Value, error) { + switch m.Op() { + case OpCreate: + return (&HarnessConfigCreate{config: c.config, hooks: c.Hooks(), mutation: m}).Save(ctx) + case OpUpdate: + return (&HarnessConfigUpdate{config: c.config, hooks: c.Hooks(), mutation: m}).Save(ctx) + case OpUpdateOne: + return (&HarnessConfigUpdateOne{config: c.config, hooks: c.Hooks(), mutation: m}).Save(ctx) + case OpDelete, OpDeleteOne: + return (&HarnessConfigDelete{config: c.config, hooks: c.Hooks(), mutation: m}).Exec(ctx) + default: + return nil, fmt.Errorf("ent: unknown HarnessConfig mutation op: %q", m.Op()) + } +} + +// InviteCodeClient is a client for the InviteCode schema. +type InviteCodeClient struct { + config +} + +// NewInviteCodeClient returns a client for the InviteCode from the given config. +func NewInviteCodeClient(c config) *InviteCodeClient { + return &InviteCodeClient{config: c} +} + +// Use adds a list of mutation hooks to the hooks stack. +// A call to `Use(f, g, h)` equals to `invitecode.Hooks(f(g(h())))`. +func (c *InviteCodeClient) Use(hooks ...Hook) { + c.hooks.InviteCode = append(c.hooks.InviteCode, hooks...) +} + +// Intercept adds a list of query interceptors to the interceptors stack. +// A call to `Intercept(f, g, h)` equals to `invitecode.Intercept(f(g(h())))`. +func (c *InviteCodeClient) Intercept(interceptors ...Interceptor) { + c.inters.InviteCode = append(c.inters.InviteCode, interceptors...) +} + +// Create returns a builder for creating a InviteCode entity. +func (c *InviteCodeClient) Create() *InviteCodeCreate { + mutation := newInviteCodeMutation(c.config, OpCreate) + return &InviteCodeCreate{config: c.config, hooks: c.Hooks(), mutation: mutation} +} + +// CreateBulk returns a builder for creating a bulk of InviteCode entities. +func (c *InviteCodeClient) CreateBulk(builders ...*InviteCodeCreate) *InviteCodeCreateBulk { + return &InviteCodeCreateBulk{config: c.config, builders: builders} +} + +// MapCreateBulk creates a bulk creation builder from the given slice. For each item in the slice, the function creates +// a builder and applies setFunc on it. +func (c *InviteCodeClient) MapCreateBulk(slice any, setFunc func(*InviteCodeCreate, int)) *InviteCodeCreateBulk { + rv := reflect.ValueOf(slice) + if rv.Kind() != reflect.Slice { + return &InviteCodeCreateBulk{err: fmt.Errorf("calling to InviteCodeClient.MapCreateBulk with wrong type %T, need slice", slice)} + } + builders := make([]*InviteCodeCreate, rv.Len()) + for i := 0; i < rv.Len(); i++ { + builders[i] = c.Create() + setFunc(builders[i], i) + } + return &InviteCodeCreateBulk{config: c.config, builders: builders} +} + +// Update returns an update builder for InviteCode. +func (c *InviteCodeClient) Update() *InviteCodeUpdate { + mutation := newInviteCodeMutation(c.config, OpUpdate) + return &InviteCodeUpdate{config: c.config, hooks: c.Hooks(), mutation: mutation} +} + +// UpdateOne returns an update builder for the given entity. +func (c *InviteCodeClient) UpdateOne(_m *InviteCode) *InviteCodeUpdateOne { + mutation := newInviteCodeMutation(c.config, OpUpdateOne, withInviteCode(_m)) + return &InviteCodeUpdateOne{config: c.config, hooks: c.Hooks(), mutation: mutation} +} + +// UpdateOneID returns an update builder for the given id. +func (c *InviteCodeClient) UpdateOneID(id uuid.UUID) *InviteCodeUpdateOne { + mutation := newInviteCodeMutation(c.config, OpUpdateOne, withInviteCodeID(id)) + return &InviteCodeUpdateOne{config: c.config, hooks: c.Hooks(), mutation: mutation} +} + +// Delete returns a delete builder for InviteCode. +func (c *InviteCodeClient) Delete() *InviteCodeDelete { + mutation := newInviteCodeMutation(c.config, OpDelete) + return &InviteCodeDelete{config: c.config, hooks: c.Hooks(), mutation: mutation} +} + +// DeleteOne returns a builder for deleting the given entity. +func (c *InviteCodeClient) DeleteOne(_m *InviteCode) *InviteCodeDeleteOne { + return c.DeleteOneID(_m.ID) +} + +// DeleteOneID returns a builder for deleting the given entity by its id. +func (c *InviteCodeClient) DeleteOneID(id uuid.UUID) *InviteCodeDeleteOne { + builder := c.Delete().Where(invitecode.ID(id)) + builder.mutation.id = &id + builder.mutation.op = OpDeleteOne + return &InviteCodeDeleteOne{builder} +} + +// Query returns a query builder for InviteCode. +func (c *InviteCodeClient) Query() *InviteCodeQuery { + return &InviteCodeQuery{ + config: c.config, + ctx: &QueryContext{Type: TypeInviteCode}, + inters: c.Interceptors(), + } +} + +// Get returns a InviteCode entity by its id. +func (c *InviteCodeClient) Get(ctx context.Context, id uuid.UUID) (*InviteCode, error) { + return c.Query().Where(invitecode.ID(id)).Only(ctx) +} + +// GetX is like Get, but panics if an error occurs. +func (c *InviteCodeClient) GetX(ctx context.Context, id uuid.UUID) *InviteCode { + obj, err := c.Get(ctx, id) + if err != nil { + panic(err) + } + return obj +} + +// Hooks returns the client hooks. +func (c *InviteCodeClient) Hooks() []Hook { + return c.hooks.InviteCode +} + +// Interceptors returns the client interceptors. +func (c *InviteCodeClient) Interceptors() []Interceptor { + return c.inters.InviteCode +} + +func (c *InviteCodeClient) mutate(ctx context.Context, m *InviteCodeMutation) (Value, error) { + switch m.Op() { + case OpCreate: + return (&InviteCodeCreate{config: c.config, hooks: c.Hooks(), mutation: m}).Save(ctx) + case OpUpdate: + return (&InviteCodeUpdate{config: c.config, hooks: c.Hooks(), mutation: m}).Save(ctx) + case OpUpdateOne: + return (&InviteCodeUpdateOne{config: c.config, hooks: c.Hooks(), mutation: m}).Save(ctx) + case OpDelete, OpDeleteOne: + return (&InviteCodeDelete{config: c.config, hooks: c.Hooks(), mutation: m}).Exec(ctx) + default: + return nil, fmt.Errorf("ent: unknown InviteCode mutation op: %q", m.Op()) + } +} + +// MaintenanceOperationClient is a client for the MaintenanceOperation schema. +type MaintenanceOperationClient struct { + config +} + +// NewMaintenanceOperationClient returns a client for the MaintenanceOperation from the given config. +func NewMaintenanceOperationClient(c config) *MaintenanceOperationClient { + return &MaintenanceOperationClient{config: c} +} + +// Use adds a list of mutation hooks to the hooks stack. +// A call to `Use(f, g, h)` equals to `maintenanceoperation.Hooks(f(g(h())))`. +func (c *MaintenanceOperationClient) Use(hooks ...Hook) { + c.hooks.MaintenanceOperation = append(c.hooks.MaintenanceOperation, hooks...) +} + +// Intercept adds a list of query interceptors to the interceptors stack. +// A call to `Intercept(f, g, h)` equals to `maintenanceoperation.Intercept(f(g(h())))`. +func (c *MaintenanceOperationClient) Intercept(interceptors ...Interceptor) { + c.inters.MaintenanceOperation = append(c.inters.MaintenanceOperation, interceptors...) +} + +// Create returns a builder for creating a MaintenanceOperation entity. +func (c *MaintenanceOperationClient) Create() *MaintenanceOperationCreate { + mutation := newMaintenanceOperationMutation(c.config, OpCreate) + return &MaintenanceOperationCreate{config: c.config, hooks: c.Hooks(), mutation: mutation} +} + +// CreateBulk returns a builder for creating a bulk of MaintenanceOperation entities. +func (c *MaintenanceOperationClient) CreateBulk(builders ...*MaintenanceOperationCreate) *MaintenanceOperationCreateBulk { + return &MaintenanceOperationCreateBulk{config: c.config, builders: builders} +} + +// MapCreateBulk creates a bulk creation builder from the given slice. For each item in the slice, the function creates +// a builder and applies setFunc on it. +func (c *MaintenanceOperationClient) MapCreateBulk(slice any, setFunc func(*MaintenanceOperationCreate, int)) *MaintenanceOperationCreateBulk { + rv := reflect.ValueOf(slice) + if rv.Kind() != reflect.Slice { + return &MaintenanceOperationCreateBulk{err: fmt.Errorf("calling to MaintenanceOperationClient.MapCreateBulk with wrong type %T, need slice", slice)} + } + builders := make([]*MaintenanceOperationCreate, rv.Len()) + for i := 0; i < rv.Len(); i++ { + builders[i] = c.Create() + setFunc(builders[i], i) + } + return &MaintenanceOperationCreateBulk{config: c.config, builders: builders} +} + +// Update returns an update builder for MaintenanceOperation. +func (c *MaintenanceOperationClient) Update() *MaintenanceOperationUpdate { + mutation := newMaintenanceOperationMutation(c.config, OpUpdate) + return &MaintenanceOperationUpdate{config: c.config, hooks: c.Hooks(), mutation: mutation} +} + +// UpdateOne returns an update builder for the given entity. +func (c *MaintenanceOperationClient) UpdateOne(_m *MaintenanceOperation) *MaintenanceOperationUpdateOne { + mutation := newMaintenanceOperationMutation(c.config, OpUpdateOne, withMaintenanceOperation(_m)) + return &MaintenanceOperationUpdateOne{config: c.config, hooks: c.Hooks(), mutation: mutation} +} + +// UpdateOneID returns an update builder for the given id. +func (c *MaintenanceOperationClient) UpdateOneID(id uuid.UUID) *MaintenanceOperationUpdateOne { + mutation := newMaintenanceOperationMutation(c.config, OpUpdateOne, withMaintenanceOperationID(id)) + return &MaintenanceOperationUpdateOne{config: c.config, hooks: c.Hooks(), mutation: mutation} +} + +// Delete returns a delete builder for MaintenanceOperation. +func (c *MaintenanceOperationClient) Delete() *MaintenanceOperationDelete { + mutation := newMaintenanceOperationMutation(c.config, OpDelete) + return &MaintenanceOperationDelete{config: c.config, hooks: c.Hooks(), mutation: mutation} +} + +// DeleteOne returns a builder for deleting the given entity. +func (c *MaintenanceOperationClient) DeleteOne(_m *MaintenanceOperation) *MaintenanceOperationDeleteOne { + return c.DeleteOneID(_m.ID) +} + +// DeleteOneID returns a builder for deleting the given entity by its id. +func (c *MaintenanceOperationClient) DeleteOneID(id uuid.UUID) *MaintenanceOperationDeleteOne { + builder := c.Delete().Where(maintenanceoperation.ID(id)) + builder.mutation.id = &id + builder.mutation.op = OpDeleteOne + return &MaintenanceOperationDeleteOne{builder} +} + +// Query returns a query builder for MaintenanceOperation. +func (c *MaintenanceOperationClient) Query() *MaintenanceOperationQuery { + return &MaintenanceOperationQuery{ + config: c.config, + ctx: &QueryContext{Type: TypeMaintenanceOperation}, + inters: c.Interceptors(), + } +} + +// Get returns a MaintenanceOperation entity by its id. +func (c *MaintenanceOperationClient) Get(ctx context.Context, id uuid.UUID) (*MaintenanceOperation, error) { + return c.Query().Where(maintenanceoperation.ID(id)).Only(ctx) +} + +// GetX is like Get, but panics if an error occurs. +func (c *MaintenanceOperationClient) GetX(ctx context.Context, id uuid.UUID) *MaintenanceOperation { + obj, err := c.Get(ctx, id) + if err != nil { + panic(err) + } + return obj +} + +// Hooks returns the client hooks. +func (c *MaintenanceOperationClient) Hooks() []Hook { + return c.hooks.MaintenanceOperation +} + +// Interceptors returns the client interceptors. +func (c *MaintenanceOperationClient) Interceptors() []Interceptor { + return c.inters.MaintenanceOperation +} + +func (c *MaintenanceOperationClient) mutate(ctx context.Context, m *MaintenanceOperationMutation) (Value, error) { + switch m.Op() { + case OpCreate: + return (&MaintenanceOperationCreate{config: c.config, hooks: c.Hooks(), mutation: m}).Save(ctx) + case OpUpdate: + return (&MaintenanceOperationUpdate{config: c.config, hooks: c.Hooks(), mutation: m}).Save(ctx) + case OpUpdateOne: + return (&MaintenanceOperationUpdateOne{config: c.config, hooks: c.Hooks(), mutation: m}).Save(ctx) + case OpDelete, OpDeleteOne: + return (&MaintenanceOperationDelete{config: c.config, hooks: c.Hooks(), mutation: m}).Exec(ctx) + default: + return nil, fmt.Errorf("ent: unknown MaintenanceOperation mutation op: %q", m.Op()) + } +} + +// MaintenanceOperationRunClient is a client for the MaintenanceOperationRun schema. +type MaintenanceOperationRunClient struct { + config +} + +// NewMaintenanceOperationRunClient returns a client for the MaintenanceOperationRun from the given config. +func NewMaintenanceOperationRunClient(c config) *MaintenanceOperationRunClient { + return &MaintenanceOperationRunClient{config: c} +} + +// Use adds a list of mutation hooks to the hooks stack. +// A call to `Use(f, g, h)` equals to `maintenanceoperationrun.Hooks(f(g(h())))`. +func (c *MaintenanceOperationRunClient) Use(hooks ...Hook) { + c.hooks.MaintenanceOperationRun = append(c.hooks.MaintenanceOperationRun, hooks...) +} + +// Intercept adds a list of query interceptors to the interceptors stack. +// A call to `Intercept(f, g, h)` equals to `maintenanceoperationrun.Intercept(f(g(h())))`. +func (c *MaintenanceOperationRunClient) Intercept(interceptors ...Interceptor) { + c.inters.MaintenanceOperationRun = append(c.inters.MaintenanceOperationRun, interceptors...) +} + +// Create returns a builder for creating a MaintenanceOperationRun entity. +func (c *MaintenanceOperationRunClient) Create() *MaintenanceOperationRunCreate { + mutation := newMaintenanceOperationRunMutation(c.config, OpCreate) + return &MaintenanceOperationRunCreate{config: c.config, hooks: c.Hooks(), mutation: mutation} +} + +// CreateBulk returns a builder for creating a bulk of MaintenanceOperationRun entities. +func (c *MaintenanceOperationRunClient) CreateBulk(builders ...*MaintenanceOperationRunCreate) *MaintenanceOperationRunCreateBulk { + return &MaintenanceOperationRunCreateBulk{config: c.config, builders: builders} +} + +// MapCreateBulk creates a bulk creation builder from the given slice. For each item in the slice, the function creates +// a builder and applies setFunc on it. +func (c *MaintenanceOperationRunClient) MapCreateBulk(slice any, setFunc func(*MaintenanceOperationRunCreate, int)) *MaintenanceOperationRunCreateBulk { + rv := reflect.ValueOf(slice) + if rv.Kind() != reflect.Slice { + return &MaintenanceOperationRunCreateBulk{err: fmt.Errorf("calling to MaintenanceOperationRunClient.MapCreateBulk with wrong type %T, need slice", slice)} + } + builders := make([]*MaintenanceOperationRunCreate, rv.Len()) + for i := 0; i < rv.Len(); i++ { + builders[i] = c.Create() + setFunc(builders[i], i) + } + return &MaintenanceOperationRunCreateBulk{config: c.config, builders: builders} +} + +// Update returns an update builder for MaintenanceOperationRun. +func (c *MaintenanceOperationRunClient) Update() *MaintenanceOperationRunUpdate { + mutation := newMaintenanceOperationRunMutation(c.config, OpUpdate) + return &MaintenanceOperationRunUpdate{config: c.config, hooks: c.Hooks(), mutation: mutation} +} + +// UpdateOne returns an update builder for the given entity. +func (c *MaintenanceOperationRunClient) UpdateOne(_m *MaintenanceOperationRun) *MaintenanceOperationRunUpdateOne { + mutation := newMaintenanceOperationRunMutation(c.config, OpUpdateOne, withMaintenanceOperationRun(_m)) + return &MaintenanceOperationRunUpdateOne{config: c.config, hooks: c.Hooks(), mutation: mutation} +} + +// UpdateOneID returns an update builder for the given id. +func (c *MaintenanceOperationRunClient) UpdateOneID(id uuid.UUID) *MaintenanceOperationRunUpdateOne { + mutation := newMaintenanceOperationRunMutation(c.config, OpUpdateOne, withMaintenanceOperationRunID(id)) + return &MaintenanceOperationRunUpdateOne{config: c.config, hooks: c.Hooks(), mutation: mutation} +} + +// Delete returns a delete builder for MaintenanceOperationRun. +func (c *MaintenanceOperationRunClient) Delete() *MaintenanceOperationRunDelete { + mutation := newMaintenanceOperationRunMutation(c.config, OpDelete) + return &MaintenanceOperationRunDelete{config: c.config, hooks: c.Hooks(), mutation: mutation} +} + +// DeleteOne returns a builder for deleting the given entity. +func (c *MaintenanceOperationRunClient) DeleteOne(_m *MaintenanceOperationRun) *MaintenanceOperationRunDeleteOne { + return c.DeleteOneID(_m.ID) +} + +// DeleteOneID returns a builder for deleting the given entity by its id. +func (c *MaintenanceOperationRunClient) DeleteOneID(id uuid.UUID) *MaintenanceOperationRunDeleteOne { + builder := c.Delete().Where(maintenanceoperationrun.ID(id)) + builder.mutation.id = &id + builder.mutation.op = OpDeleteOne + return &MaintenanceOperationRunDeleteOne{builder} +} + +// Query returns a query builder for MaintenanceOperationRun. +func (c *MaintenanceOperationRunClient) Query() *MaintenanceOperationRunQuery { + return &MaintenanceOperationRunQuery{ + config: c.config, + ctx: &QueryContext{Type: TypeMaintenanceOperationRun}, + inters: c.Interceptors(), + } +} + +// Get returns a MaintenanceOperationRun entity by its id. +func (c *MaintenanceOperationRunClient) Get(ctx context.Context, id uuid.UUID) (*MaintenanceOperationRun, error) { + return c.Query().Where(maintenanceoperationrun.ID(id)).Only(ctx) +} + +// GetX is like Get, but panics if an error occurs. +func (c *MaintenanceOperationRunClient) GetX(ctx context.Context, id uuid.UUID) *MaintenanceOperationRun { + obj, err := c.Get(ctx, id) + if err != nil { + panic(err) + } + return obj +} + +// Hooks returns the client hooks. +func (c *MaintenanceOperationRunClient) Hooks() []Hook { + return c.hooks.MaintenanceOperationRun +} + +// Interceptors returns the client interceptors. +func (c *MaintenanceOperationRunClient) Interceptors() []Interceptor { + return c.inters.MaintenanceOperationRun +} + +func (c *MaintenanceOperationRunClient) mutate(ctx context.Context, m *MaintenanceOperationRunMutation) (Value, error) { + switch m.Op() { + case OpCreate: + return (&MaintenanceOperationRunCreate{config: c.config, hooks: c.Hooks(), mutation: m}).Save(ctx) + case OpUpdate: + return (&MaintenanceOperationRunUpdate{config: c.config, hooks: c.Hooks(), mutation: m}).Save(ctx) + case OpUpdateOne: + return (&MaintenanceOperationRunUpdateOne{config: c.config, hooks: c.Hooks(), mutation: m}).Save(ctx) + case OpDelete, OpDeleteOne: + return (&MaintenanceOperationRunDelete{config: c.config, hooks: c.Hooks(), mutation: m}).Exec(ctx) + default: + return nil, fmt.Errorf("ent: unknown MaintenanceOperationRun mutation op: %q", m.Op()) + } +} + +// MessageClient is a client for the Message schema. +type MessageClient struct { + config +} + +// NewMessageClient returns a client for the Message from the given config. +func NewMessageClient(c config) *MessageClient { + return &MessageClient{config: c} +} + +// Use adds a list of mutation hooks to the hooks stack. +// A call to `Use(f, g, h)` equals to `message.Hooks(f(g(h())))`. +func (c *MessageClient) Use(hooks ...Hook) { + c.hooks.Message = append(c.hooks.Message, hooks...) +} + +// Intercept adds a list of query interceptors to the interceptors stack. +// A call to `Intercept(f, g, h)` equals to `message.Intercept(f(g(h())))`. +func (c *MessageClient) Intercept(interceptors ...Interceptor) { + c.inters.Message = append(c.inters.Message, interceptors...) +} + +// Create returns a builder for creating a Message entity. +func (c *MessageClient) Create() *MessageCreate { + mutation := newMessageMutation(c.config, OpCreate) + return &MessageCreate{config: c.config, hooks: c.Hooks(), mutation: mutation} +} + +// CreateBulk returns a builder for creating a bulk of Message entities. +func (c *MessageClient) CreateBulk(builders ...*MessageCreate) *MessageCreateBulk { + return &MessageCreateBulk{config: c.config, builders: builders} +} + +// MapCreateBulk creates a bulk creation builder from the given slice. For each item in the slice, the function creates +// a builder and applies setFunc on it. +func (c *MessageClient) MapCreateBulk(slice any, setFunc func(*MessageCreate, int)) *MessageCreateBulk { + rv := reflect.ValueOf(slice) + if rv.Kind() != reflect.Slice { + return &MessageCreateBulk{err: fmt.Errorf("calling to MessageClient.MapCreateBulk with wrong type %T, need slice", slice)} + } + builders := make([]*MessageCreate, rv.Len()) + for i := 0; i < rv.Len(); i++ { + builders[i] = c.Create() + setFunc(builders[i], i) + } + return &MessageCreateBulk{config: c.config, builders: builders} +} + +// Update returns an update builder for Message. +func (c *MessageClient) Update() *MessageUpdate { + mutation := newMessageMutation(c.config, OpUpdate) + return &MessageUpdate{config: c.config, hooks: c.Hooks(), mutation: mutation} +} + +// UpdateOne returns an update builder for the given entity. +func (c *MessageClient) UpdateOne(_m *Message) *MessageUpdateOne { + mutation := newMessageMutation(c.config, OpUpdateOne, withMessage(_m)) + return &MessageUpdateOne{config: c.config, hooks: c.Hooks(), mutation: mutation} +} + +// UpdateOneID returns an update builder for the given id. +func (c *MessageClient) UpdateOneID(id uuid.UUID) *MessageUpdateOne { + mutation := newMessageMutation(c.config, OpUpdateOne, withMessageID(id)) + return &MessageUpdateOne{config: c.config, hooks: c.Hooks(), mutation: mutation} +} + +// Delete returns a delete builder for Message. +func (c *MessageClient) Delete() *MessageDelete { + mutation := newMessageMutation(c.config, OpDelete) + return &MessageDelete{config: c.config, hooks: c.Hooks(), mutation: mutation} +} + +// DeleteOne returns a builder for deleting the given entity. +func (c *MessageClient) DeleteOne(_m *Message) *MessageDeleteOne { + return c.DeleteOneID(_m.ID) +} + +// DeleteOneID returns a builder for deleting the given entity by its id. +func (c *MessageClient) DeleteOneID(id uuid.UUID) *MessageDeleteOne { + builder := c.Delete().Where(message.ID(id)) + builder.mutation.id = &id + builder.mutation.op = OpDeleteOne + return &MessageDeleteOne{builder} +} + +// Query returns a query builder for Message. +func (c *MessageClient) Query() *MessageQuery { + return &MessageQuery{ + config: c.config, + ctx: &QueryContext{Type: TypeMessage}, + inters: c.Interceptors(), + } +} + +// Get returns a Message entity by its id. +func (c *MessageClient) Get(ctx context.Context, id uuid.UUID) (*Message, error) { + return c.Query().Where(message.ID(id)).Only(ctx) +} + +// GetX is like Get, but panics if an error occurs. +func (c *MessageClient) GetX(ctx context.Context, id uuid.UUID) *Message { + obj, err := c.Get(ctx, id) + if err != nil { + panic(err) + } + return obj +} + +// Hooks returns the client hooks. +func (c *MessageClient) Hooks() []Hook { + return c.hooks.Message +} + +// Interceptors returns the client interceptors. +func (c *MessageClient) Interceptors() []Interceptor { + return c.inters.Message +} + +func (c *MessageClient) mutate(ctx context.Context, m *MessageMutation) (Value, error) { + switch m.Op() { + case OpCreate: + return (&MessageCreate{config: c.config, hooks: c.Hooks(), mutation: m}).Save(ctx) + case OpUpdate: + return (&MessageUpdate{config: c.config, hooks: c.Hooks(), mutation: m}).Save(ctx) + case OpUpdateOne: + return (&MessageUpdateOne{config: c.config, hooks: c.Hooks(), mutation: m}).Save(ctx) + case OpDelete, OpDeleteOne: + return (&MessageDelete{config: c.config, hooks: c.Hooks(), mutation: m}).Exec(ctx) + default: + return nil, fmt.Errorf("ent: unknown Message mutation op: %q", m.Op()) + } +} + +// NotificationClient is a client for the Notification schema. +type NotificationClient struct { + config +} + +// NewNotificationClient returns a client for the Notification from the given config. +func NewNotificationClient(c config) *NotificationClient { + return &NotificationClient{config: c} +} + +// Use adds a list of mutation hooks to the hooks stack. +// A call to `Use(f, g, h)` equals to `notification.Hooks(f(g(h())))`. +func (c *NotificationClient) Use(hooks ...Hook) { + c.hooks.Notification = append(c.hooks.Notification, hooks...) +} + +// Intercept adds a list of query interceptors to the interceptors stack. +// A call to `Intercept(f, g, h)` equals to `notification.Intercept(f(g(h())))`. +func (c *NotificationClient) Intercept(interceptors ...Interceptor) { + c.inters.Notification = append(c.inters.Notification, interceptors...) +} + +// Create returns a builder for creating a Notification entity. +func (c *NotificationClient) Create() *NotificationCreate { + mutation := newNotificationMutation(c.config, OpCreate) + return &NotificationCreate{config: c.config, hooks: c.Hooks(), mutation: mutation} +} + +// CreateBulk returns a builder for creating a bulk of Notification entities. +func (c *NotificationClient) CreateBulk(builders ...*NotificationCreate) *NotificationCreateBulk { + return &NotificationCreateBulk{config: c.config, builders: builders} +} + +// MapCreateBulk creates a bulk creation builder from the given slice. For each item in the slice, the function creates +// a builder and applies setFunc on it. +func (c *NotificationClient) MapCreateBulk(slice any, setFunc func(*NotificationCreate, int)) *NotificationCreateBulk { + rv := reflect.ValueOf(slice) + if rv.Kind() != reflect.Slice { + return &NotificationCreateBulk{err: fmt.Errorf("calling to NotificationClient.MapCreateBulk with wrong type %T, need slice", slice)} + } + builders := make([]*NotificationCreate, rv.Len()) + for i := 0; i < rv.Len(); i++ { + builders[i] = c.Create() + setFunc(builders[i], i) + } + return &NotificationCreateBulk{config: c.config, builders: builders} +} + +// Update returns an update builder for Notification. +func (c *NotificationClient) Update() *NotificationUpdate { + mutation := newNotificationMutation(c.config, OpUpdate) + return &NotificationUpdate{config: c.config, hooks: c.Hooks(), mutation: mutation} +} + +// UpdateOne returns an update builder for the given entity. +func (c *NotificationClient) UpdateOne(_m *Notification) *NotificationUpdateOne { + mutation := newNotificationMutation(c.config, OpUpdateOne, withNotification(_m)) + return &NotificationUpdateOne{config: c.config, hooks: c.Hooks(), mutation: mutation} +} + +// UpdateOneID returns an update builder for the given id. +func (c *NotificationClient) UpdateOneID(id uuid.UUID) *NotificationUpdateOne { + mutation := newNotificationMutation(c.config, OpUpdateOne, withNotificationID(id)) + return &NotificationUpdateOne{config: c.config, hooks: c.Hooks(), mutation: mutation} +} + +// Delete returns a delete builder for Notification. +func (c *NotificationClient) Delete() *NotificationDelete { + mutation := newNotificationMutation(c.config, OpDelete) + return &NotificationDelete{config: c.config, hooks: c.Hooks(), mutation: mutation} +} + +// DeleteOne returns a builder for deleting the given entity. +func (c *NotificationClient) DeleteOne(_m *Notification) *NotificationDeleteOne { + return c.DeleteOneID(_m.ID) +} + +// DeleteOneID returns a builder for deleting the given entity by its id. +func (c *NotificationClient) DeleteOneID(id uuid.UUID) *NotificationDeleteOne { + builder := c.Delete().Where(notification.ID(id)) + builder.mutation.id = &id + builder.mutation.op = OpDeleteOne + return &NotificationDeleteOne{builder} +} + +// Query returns a query builder for Notification. +func (c *NotificationClient) Query() *NotificationQuery { + return &NotificationQuery{ + config: c.config, + ctx: &QueryContext{Type: TypeNotification}, + inters: c.Interceptors(), + } +} + +// Get returns a Notification entity by its id. +func (c *NotificationClient) Get(ctx context.Context, id uuid.UUID) (*Notification, error) { + return c.Query().Where(notification.ID(id)).Only(ctx) +} + +// GetX is like Get, but panics if an error occurs. +func (c *NotificationClient) GetX(ctx context.Context, id uuid.UUID) *Notification { + obj, err := c.Get(ctx, id) + if err != nil { + panic(err) + } + return obj +} + +// Hooks returns the client hooks. +func (c *NotificationClient) Hooks() []Hook { + return c.hooks.Notification +} + +// Interceptors returns the client interceptors. +func (c *NotificationClient) Interceptors() []Interceptor { + return c.inters.Notification +} + +func (c *NotificationClient) mutate(ctx context.Context, m *NotificationMutation) (Value, error) { + switch m.Op() { + case OpCreate: + return (&NotificationCreate{config: c.config, hooks: c.Hooks(), mutation: m}).Save(ctx) + case OpUpdate: + return (&NotificationUpdate{config: c.config, hooks: c.Hooks(), mutation: m}).Save(ctx) + case OpUpdateOne: + return (&NotificationUpdateOne{config: c.config, hooks: c.Hooks(), mutation: m}).Save(ctx) + case OpDelete, OpDeleteOne: + return (&NotificationDelete{config: c.config, hooks: c.Hooks(), mutation: m}).Exec(ctx) + default: + return nil, fmt.Errorf("ent: unknown Notification mutation op: %q", m.Op()) + } +} + +// NotificationSubscriptionClient is a client for the NotificationSubscription schema. +type NotificationSubscriptionClient struct { + config +} + +// NewNotificationSubscriptionClient returns a client for the NotificationSubscription from the given config. +func NewNotificationSubscriptionClient(c config) *NotificationSubscriptionClient { + return &NotificationSubscriptionClient{config: c} +} + +// Use adds a list of mutation hooks to the hooks stack. +// A call to `Use(f, g, h)` equals to `notificationsubscription.Hooks(f(g(h())))`. +func (c *NotificationSubscriptionClient) Use(hooks ...Hook) { + c.hooks.NotificationSubscription = append(c.hooks.NotificationSubscription, hooks...) +} + +// Intercept adds a list of query interceptors to the interceptors stack. +// A call to `Intercept(f, g, h)` equals to `notificationsubscription.Intercept(f(g(h())))`. +func (c *NotificationSubscriptionClient) Intercept(interceptors ...Interceptor) { + c.inters.NotificationSubscription = append(c.inters.NotificationSubscription, interceptors...) +} + +// Create returns a builder for creating a NotificationSubscription entity. +func (c *NotificationSubscriptionClient) Create() *NotificationSubscriptionCreate { + mutation := newNotificationSubscriptionMutation(c.config, OpCreate) + return &NotificationSubscriptionCreate{config: c.config, hooks: c.Hooks(), mutation: mutation} +} + +// CreateBulk returns a builder for creating a bulk of NotificationSubscription entities. +func (c *NotificationSubscriptionClient) CreateBulk(builders ...*NotificationSubscriptionCreate) *NotificationSubscriptionCreateBulk { + return &NotificationSubscriptionCreateBulk{config: c.config, builders: builders} +} + +// MapCreateBulk creates a bulk creation builder from the given slice. For each item in the slice, the function creates +// a builder and applies setFunc on it. +func (c *NotificationSubscriptionClient) MapCreateBulk(slice any, setFunc func(*NotificationSubscriptionCreate, int)) *NotificationSubscriptionCreateBulk { + rv := reflect.ValueOf(slice) + if rv.Kind() != reflect.Slice { + return &NotificationSubscriptionCreateBulk{err: fmt.Errorf("calling to NotificationSubscriptionClient.MapCreateBulk with wrong type %T, need slice", slice)} + } + builders := make([]*NotificationSubscriptionCreate, rv.Len()) + for i := 0; i < rv.Len(); i++ { + builders[i] = c.Create() + setFunc(builders[i], i) + } + return &NotificationSubscriptionCreateBulk{config: c.config, builders: builders} +} + +// Update returns an update builder for NotificationSubscription. +func (c *NotificationSubscriptionClient) Update() *NotificationSubscriptionUpdate { + mutation := newNotificationSubscriptionMutation(c.config, OpUpdate) + return &NotificationSubscriptionUpdate{config: c.config, hooks: c.Hooks(), mutation: mutation} +} + +// UpdateOne returns an update builder for the given entity. +func (c *NotificationSubscriptionClient) UpdateOne(_m *NotificationSubscription) *NotificationSubscriptionUpdateOne { + mutation := newNotificationSubscriptionMutation(c.config, OpUpdateOne, withNotificationSubscription(_m)) + return &NotificationSubscriptionUpdateOne{config: c.config, hooks: c.Hooks(), mutation: mutation} +} + +// UpdateOneID returns an update builder for the given id. +func (c *NotificationSubscriptionClient) UpdateOneID(id uuid.UUID) *NotificationSubscriptionUpdateOne { + mutation := newNotificationSubscriptionMutation(c.config, OpUpdateOne, withNotificationSubscriptionID(id)) + return &NotificationSubscriptionUpdateOne{config: c.config, hooks: c.Hooks(), mutation: mutation} +} + +// Delete returns a delete builder for NotificationSubscription. +func (c *NotificationSubscriptionClient) Delete() *NotificationSubscriptionDelete { + mutation := newNotificationSubscriptionMutation(c.config, OpDelete) + return &NotificationSubscriptionDelete{config: c.config, hooks: c.Hooks(), mutation: mutation} +} + +// DeleteOne returns a builder for deleting the given entity. +func (c *NotificationSubscriptionClient) DeleteOne(_m *NotificationSubscription) *NotificationSubscriptionDeleteOne { + return c.DeleteOneID(_m.ID) +} + +// DeleteOneID returns a builder for deleting the given entity by its id. +func (c *NotificationSubscriptionClient) DeleteOneID(id uuid.UUID) *NotificationSubscriptionDeleteOne { + builder := c.Delete().Where(notificationsubscription.ID(id)) + builder.mutation.id = &id + builder.mutation.op = OpDeleteOne + return &NotificationSubscriptionDeleteOne{builder} +} + +// Query returns a query builder for NotificationSubscription. +func (c *NotificationSubscriptionClient) Query() *NotificationSubscriptionQuery { + return &NotificationSubscriptionQuery{ + config: c.config, + ctx: &QueryContext{Type: TypeNotificationSubscription}, + inters: c.Interceptors(), + } +} + +// Get returns a NotificationSubscription entity by its id. +func (c *NotificationSubscriptionClient) Get(ctx context.Context, id uuid.UUID) (*NotificationSubscription, error) { + return c.Query().Where(notificationsubscription.ID(id)).Only(ctx) +} + +// GetX is like Get, but panics if an error occurs. +func (c *NotificationSubscriptionClient) GetX(ctx context.Context, id uuid.UUID) *NotificationSubscription { + obj, err := c.Get(ctx, id) + if err != nil { + panic(err) + } + return obj +} + +// Hooks returns the client hooks. +func (c *NotificationSubscriptionClient) Hooks() []Hook { + return c.hooks.NotificationSubscription +} + +// Interceptors returns the client interceptors. +func (c *NotificationSubscriptionClient) Interceptors() []Interceptor { + return c.inters.NotificationSubscription +} + +func (c *NotificationSubscriptionClient) mutate(ctx context.Context, m *NotificationSubscriptionMutation) (Value, error) { + switch m.Op() { + case OpCreate: + return (&NotificationSubscriptionCreate{config: c.config, hooks: c.Hooks(), mutation: m}).Save(ctx) + case OpUpdate: + return (&NotificationSubscriptionUpdate{config: c.config, hooks: c.Hooks(), mutation: m}).Save(ctx) + case OpUpdateOne: + return (&NotificationSubscriptionUpdateOne{config: c.config, hooks: c.Hooks(), mutation: m}).Save(ctx) + case OpDelete, OpDeleteOne: + return (&NotificationSubscriptionDelete{config: c.config, hooks: c.Hooks(), mutation: m}).Exec(ctx) + default: + return nil, fmt.Errorf("ent: unknown NotificationSubscription mutation op: %q", m.Op()) + } +} + +// PolicyBindingClient is a client for the PolicyBinding schema. +type PolicyBindingClient struct { + config +} + +// NewPolicyBindingClient returns a client for the PolicyBinding from the given config. +func NewPolicyBindingClient(c config) *PolicyBindingClient { + return &PolicyBindingClient{config: c} +} + +// Use adds a list of mutation hooks to the hooks stack. +// A call to `Use(f, g, h)` equals to `policybinding.Hooks(f(g(h())))`. +func (c *PolicyBindingClient) Use(hooks ...Hook) { + c.hooks.PolicyBinding = append(c.hooks.PolicyBinding, hooks...) +} + +// Intercept adds a list of query interceptors to the interceptors stack. +// A call to `Intercept(f, g, h)` equals to `policybinding.Intercept(f(g(h())))`. +func (c *PolicyBindingClient) Intercept(interceptors ...Interceptor) { + c.inters.PolicyBinding = append(c.inters.PolicyBinding, interceptors...) +} + +// Create returns a builder for creating a PolicyBinding entity. +func (c *PolicyBindingClient) Create() *PolicyBindingCreate { + mutation := newPolicyBindingMutation(c.config, OpCreate) + return &PolicyBindingCreate{config: c.config, hooks: c.Hooks(), mutation: mutation} +} + +// CreateBulk returns a builder for creating a bulk of PolicyBinding entities. +func (c *PolicyBindingClient) CreateBulk(builders ...*PolicyBindingCreate) *PolicyBindingCreateBulk { + return &PolicyBindingCreateBulk{config: c.config, builders: builders} +} + +// MapCreateBulk creates a bulk creation builder from the given slice. For each item in the slice, the function creates +// a builder and applies setFunc on it. +func (c *PolicyBindingClient) MapCreateBulk(slice any, setFunc func(*PolicyBindingCreate, int)) *PolicyBindingCreateBulk { + rv := reflect.ValueOf(slice) + if rv.Kind() != reflect.Slice { + return &PolicyBindingCreateBulk{err: fmt.Errorf("calling to PolicyBindingClient.MapCreateBulk with wrong type %T, need slice", slice)} + } + builders := make([]*PolicyBindingCreate, rv.Len()) + for i := 0; i < rv.Len(); i++ { + builders[i] = c.Create() + setFunc(builders[i], i) + } + return &PolicyBindingCreateBulk{config: c.config, builders: builders} +} + +// Update returns an update builder for PolicyBinding. +func (c *PolicyBindingClient) Update() *PolicyBindingUpdate { + mutation := newPolicyBindingMutation(c.config, OpUpdate) + return &PolicyBindingUpdate{config: c.config, hooks: c.Hooks(), mutation: mutation} +} + +// UpdateOne returns an update builder for the given entity. +func (c *PolicyBindingClient) UpdateOne(_m *PolicyBinding) *PolicyBindingUpdateOne { + mutation := newPolicyBindingMutation(c.config, OpUpdateOne, withPolicyBinding(_m)) + return &PolicyBindingUpdateOne{config: c.config, hooks: c.Hooks(), mutation: mutation} +} + +// UpdateOneID returns an update builder for the given id. +func (c *PolicyBindingClient) UpdateOneID(id uuid.UUID) *PolicyBindingUpdateOne { + mutation := newPolicyBindingMutation(c.config, OpUpdateOne, withPolicyBindingID(id)) + return &PolicyBindingUpdateOne{config: c.config, hooks: c.Hooks(), mutation: mutation} +} + +// Delete returns a delete builder for PolicyBinding. +func (c *PolicyBindingClient) Delete() *PolicyBindingDelete { + mutation := newPolicyBindingMutation(c.config, OpDelete) + return &PolicyBindingDelete{config: c.config, hooks: c.Hooks(), mutation: mutation} +} + +// DeleteOne returns a builder for deleting the given entity. +func (c *PolicyBindingClient) DeleteOne(_m *PolicyBinding) *PolicyBindingDeleteOne { + return c.DeleteOneID(_m.ID) +} + +// DeleteOneID returns a builder for deleting the given entity by its id. +func (c *PolicyBindingClient) DeleteOneID(id uuid.UUID) *PolicyBindingDeleteOne { + builder := c.Delete().Where(policybinding.ID(id)) + builder.mutation.id = &id + builder.mutation.op = OpDeleteOne + return &PolicyBindingDeleteOne{builder} +} + +// Query returns a query builder for PolicyBinding. +func (c *PolicyBindingClient) Query() *PolicyBindingQuery { + return &PolicyBindingQuery{ + config: c.config, + ctx: &QueryContext{Type: TypePolicyBinding}, + inters: c.Interceptors(), + } +} + +// Get returns a PolicyBinding entity by its id. +func (c *PolicyBindingClient) Get(ctx context.Context, id uuid.UUID) (*PolicyBinding, error) { + return c.Query().Where(policybinding.ID(id)).Only(ctx) +} + +// GetX is like Get, but panics if an error occurs. +func (c *PolicyBindingClient) GetX(ctx context.Context, id uuid.UUID) *PolicyBinding { + obj, err := c.Get(ctx, id) + if err != nil { + panic(err) + } + return obj +} + +// QueryPolicy queries the policy edge of a PolicyBinding. +func (c *PolicyBindingClient) QueryPolicy(_m *PolicyBinding) *AccessPolicyQuery { + query := (&AccessPolicyClient{config: c.config}).Query() + query.path = func(context.Context) (fromV *sql.Selector, _ error) { + id := _m.ID + step := sqlgraph.NewStep( + sqlgraph.From(policybinding.Table, policybinding.FieldID, id), + sqlgraph.To(accesspolicy.Table, accesspolicy.FieldID), + sqlgraph.Edge(sqlgraph.M2O, true, policybinding.PolicyTable, policybinding.PolicyColumn), + ) + fromV = sqlgraph.Neighbors(_m.driver.Dialect(), step) + return fromV, nil + } + return query +} + +// QueryUser queries the user edge of a PolicyBinding. +func (c *PolicyBindingClient) QueryUser(_m *PolicyBinding) *UserQuery { + query := (&UserClient{config: c.config}).Query() + query.path = func(context.Context) (fromV *sql.Selector, _ error) { + id := _m.ID + step := sqlgraph.NewStep( + sqlgraph.From(policybinding.Table, policybinding.FieldID, id), + sqlgraph.To(user.Table, user.FieldID), + sqlgraph.Edge(sqlgraph.M2O, false, policybinding.UserTable, policybinding.UserColumn), + ) + fromV = sqlgraph.Neighbors(_m.driver.Dialect(), step) + return fromV, nil + } + return query +} + +// QueryGroup queries the group edge of a PolicyBinding. +func (c *PolicyBindingClient) QueryGroup(_m *PolicyBinding) *GroupQuery { + query := (&GroupClient{config: c.config}).Query() + query.path = func(context.Context) (fromV *sql.Selector, _ error) { + id := _m.ID + step := sqlgraph.NewStep( + sqlgraph.From(policybinding.Table, policybinding.FieldID, id), + sqlgraph.To(group.Table, group.FieldID), + sqlgraph.Edge(sqlgraph.M2O, false, policybinding.GroupTable, policybinding.GroupColumn), + ) + fromV = sqlgraph.Neighbors(_m.driver.Dialect(), step) + return fromV, nil + } + return query +} + +// QueryAgent queries the agent edge of a PolicyBinding. +func (c *PolicyBindingClient) QueryAgent(_m *PolicyBinding) *AgentQuery { + query := (&AgentClient{config: c.config}).Query() + query.path = func(context.Context) (fromV *sql.Selector, _ error) { + id := _m.ID + step := sqlgraph.NewStep( + sqlgraph.From(policybinding.Table, policybinding.FieldID, id), sqlgraph.To(agent.Table, agent.FieldID), - sqlgraph.Edge(sqlgraph.M2O, false, groupmembership.AgentTable, groupmembership.AgentColumn), + sqlgraph.Edge(sqlgraph.M2O, false, policybinding.AgentTable, policybinding.AgentColumn), + ) + fromV = sqlgraph.Neighbors(_m.driver.Dialect(), step) + return fromV, nil + } + return query +} + +// Hooks returns the client hooks. +func (c *PolicyBindingClient) Hooks() []Hook { + return c.hooks.PolicyBinding +} + +// Interceptors returns the client interceptors. +func (c *PolicyBindingClient) Interceptors() []Interceptor { + return c.inters.PolicyBinding +} + +func (c *PolicyBindingClient) mutate(ctx context.Context, m *PolicyBindingMutation) (Value, error) { + switch m.Op() { + case OpCreate: + return (&PolicyBindingCreate{config: c.config, hooks: c.Hooks(), mutation: m}).Save(ctx) + case OpUpdate: + return (&PolicyBindingUpdate{config: c.config, hooks: c.Hooks(), mutation: m}).Save(ctx) + case OpUpdateOne: + return (&PolicyBindingUpdateOne{config: c.config, hooks: c.Hooks(), mutation: m}).Save(ctx) + case OpDelete, OpDeleteOne: + return (&PolicyBindingDelete{config: c.config, hooks: c.Hooks(), mutation: m}).Exec(ctx) + default: + return nil, fmt.Errorf("ent: unknown PolicyBinding mutation op: %q", m.Op()) + } +} + +// ProjectClient is a client for the Project schema. +type ProjectClient struct { + config +} + +// NewProjectClient returns a client for the Project from the given config. +func NewProjectClient(c config) *ProjectClient { + return &ProjectClient{config: c} +} + +// Use adds a list of mutation hooks to the hooks stack. +// A call to `Use(f, g, h)` equals to `project.Hooks(f(g(h())))`. +func (c *ProjectClient) Use(hooks ...Hook) { + c.hooks.Project = append(c.hooks.Project, hooks...) +} + +// Intercept adds a list of query interceptors to the interceptors stack. +// A call to `Intercept(f, g, h)` equals to `project.Intercept(f(g(h())))`. +func (c *ProjectClient) Intercept(interceptors ...Interceptor) { + c.inters.Project = append(c.inters.Project, interceptors...) +} + +// Create returns a builder for creating a Project entity. +func (c *ProjectClient) Create() *ProjectCreate { + mutation := newProjectMutation(c.config, OpCreate) + return &ProjectCreate{config: c.config, hooks: c.Hooks(), mutation: mutation} +} + +// CreateBulk returns a builder for creating a bulk of Project entities. +func (c *ProjectClient) CreateBulk(builders ...*ProjectCreate) *ProjectCreateBulk { + return &ProjectCreateBulk{config: c.config, builders: builders} +} + +// MapCreateBulk creates a bulk creation builder from the given slice. For each item in the slice, the function creates +// a builder and applies setFunc on it. +func (c *ProjectClient) MapCreateBulk(slice any, setFunc func(*ProjectCreate, int)) *ProjectCreateBulk { + rv := reflect.ValueOf(slice) + if rv.Kind() != reflect.Slice { + return &ProjectCreateBulk{err: fmt.Errorf("calling to ProjectClient.MapCreateBulk with wrong type %T, need slice", slice)} + } + builders := make([]*ProjectCreate, rv.Len()) + for i := 0; i < rv.Len(); i++ { + builders[i] = c.Create() + setFunc(builders[i], i) + } + return &ProjectCreateBulk{config: c.config, builders: builders} +} + +// Update returns an update builder for Project. +func (c *ProjectClient) Update() *ProjectUpdate { + mutation := newProjectMutation(c.config, OpUpdate) + return &ProjectUpdate{config: c.config, hooks: c.Hooks(), mutation: mutation} +} + +// UpdateOne returns an update builder for the given entity. +func (c *ProjectClient) UpdateOne(_m *Project) *ProjectUpdateOne { + mutation := newProjectMutation(c.config, OpUpdateOne, withProject(_m)) + return &ProjectUpdateOne{config: c.config, hooks: c.Hooks(), mutation: mutation} +} + +// UpdateOneID returns an update builder for the given id. +func (c *ProjectClient) UpdateOneID(id uuid.UUID) *ProjectUpdateOne { + mutation := newProjectMutation(c.config, OpUpdateOne, withProjectID(id)) + return &ProjectUpdateOne{config: c.config, hooks: c.Hooks(), mutation: mutation} +} + +// Delete returns a delete builder for Project. +func (c *ProjectClient) Delete() *ProjectDelete { + mutation := newProjectMutation(c.config, OpDelete) + return &ProjectDelete{config: c.config, hooks: c.Hooks(), mutation: mutation} +} + +// DeleteOne returns a builder for deleting the given entity. +func (c *ProjectClient) DeleteOne(_m *Project) *ProjectDeleteOne { + return c.DeleteOneID(_m.ID) +} + +// DeleteOneID returns a builder for deleting the given entity by its id. +func (c *ProjectClient) DeleteOneID(id uuid.UUID) *ProjectDeleteOne { + builder := c.Delete().Where(project.ID(id)) + builder.mutation.id = &id + builder.mutation.op = OpDeleteOne + return &ProjectDeleteOne{builder} +} + +// Query returns a query builder for Project. +func (c *ProjectClient) Query() *ProjectQuery { + return &ProjectQuery{ + config: c.config, + ctx: &QueryContext{Type: TypeProject}, + inters: c.Interceptors(), + } +} + +// Get returns a Project entity by its id. +func (c *ProjectClient) Get(ctx context.Context, id uuid.UUID) (*Project, error) { + return c.Query().Where(project.ID(id)).Only(ctx) +} + +// GetX is like Get, but panics if an error occurs. +func (c *ProjectClient) GetX(ctx context.Context, id uuid.UUID) *Project { + obj, err := c.Get(ctx, id) + if err != nil { + panic(err) + } + return obj +} + +// QueryAgents queries the agents edge of a Project. +func (c *ProjectClient) QueryAgents(_m *Project) *AgentQuery { + query := (&AgentClient{config: c.config}).Query() + query.path = func(context.Context) (fromV *sql.Selector, _ error) { + id := _m.ID + step := sqlgraph.NewStep( + sqlgraph.From(project.Table, project.FieldID, id), + sqlgraph.To(agent.Table, agent.FieldID), + sqlgraph.Edge(sqlgraph.O2M, false, project.AgentsTable, project.AgentsColumn), ) fromV = sqlgraph.Neighbors(_m.driver.Dialect(), step) return fromV, nil } - return query + return query +} + +// Hooks returns the client hooks. +func (c *ProjectClient) Hooks() []Hook { + return c.hooks.Project +} + +// Interceptors returns the client interceptors. +func (c *ProjectClient) Interceptors() []Interceptor { + return c.inters.Project +} + +func (c *ProjectClient) mutate(ctx context.Context, m *ProjectMutation) (Value, error) { + switch m.Op() { + case OpCreate: + return (&ProjectCreate{config: c.config, hooks: c.Hooks(), mutation: m}).Save(ctx) + case OpUpdate: + return (&ProjectUpdate{config: c.config, hooks: c.Hooks(), mutation: m}).Save(ctx) + case OpUpdateOne: + return (&ProjectUpdateOne{config: c.config, hooks: c.Hooks(), mutation: m}).Save(ctx) + case OpDelete, OpDeleteOne: + return (&ProjectDelete{config: c.config, hooks: c.Hooks(), mutation: m}).Exec(ctx) + default: + return nil, fmt.Errorf("ent: unknown Project mutation op: %q", m.Op()) + } +} + +// ProjectContributorClient is a client for the ProjectContributor schema. +type ProjectContributorClient struct { + config +} + +// NewProjectContributorClient returns a client for the ProjectContributor from the given config. +func NewProjectContributorClient(c config) *ProjectContributorClient { + return &ProjectContributorClient{config: c} +} + +// Use adds a list of mutation hooks to the hooks stack. +// A call to `Use(f, g, h)` equals to `projectcontributor.Hooks(f(g(h())))`. +func (c *ProjectContributorClient) Use(hooks ...Hook) { + c.hooks.ProjectContributor = append(c.hooks.ProjectContributor, hooks...) +} + +// Intercept adds a list of query interceptors to the interceptors stack. +// A call to `Intercept(f, g, h)` equals to `projectcontributor.Intercept(f(g(h())))`. +func (c *ProjectContributorClient) Intercept(interceptors ...Interceptor) { + c.inters.ProjectContributor = append(c.inters.ProjectContributor, interceptors...) +} + +// Create returns a builder for creating a ProjectContributor entity. +func (c *ProjectContributorClient) Create() *ProjectContributorCreate { + mutation := newProjectContributorMutation(c.config, OpCreate) + return &ProjectContributorCreate{config: c.config, hooks: c.Hooks(), mutation: mutation} +} + +// CreateBulk returns a builder for creating a bulk of ProjectContributor entities. +func (c *ProjectContributorClient) CreateBulk(builders ...*ProjectContributorCreate) *ProjectContributorCreateBulk { + return &ProjectContributorCreateBulk{config: c.config, builders: builders} +} + +// MapCreateBulk creates a bulk creation builder from the given slice. For each item in the slice, the function creates +// a builder and applies setFunc on it. +func (c *ProjectContributorClient) MapCreateBulk(slice any, setFunc func(*ProjectContributorCreate, int)) *ProjectContributorCreateBulk { + rv := reflect.ValueOf(slice) + if rv.Kind() != reflect.Slice { + return &ProjectContributorCreateBulk{err: fmt.Errorf("calling to ProjectContributorClient.MapCreateBulk with wrong type %T, need slice", slice)} + } + builders := make([]*ProjectContributorCreate, rv.Len()) + for i := 0; i < rv.Len(); i++ { + builders[i] = c.Create() + setFunc(builders[i], i) + } + return &ProjectContributorCreateBulk{config: c.config, builders: builders} +} + +// Update returns an update builder for ProjectContributor. +func (c *ProjectContributorClient) Update() *ProjectContributorUpdate { + mutation := newProjectContributorMutation(c.config, OpUpdate) + return &ProjectContributorUpdate{config: c.config, hooks: c.Hooks(), mutation: mutation} +} + +// UpdateOne returns an update builder for the given entity. +func (c *ProjectContributorClient) UpdateOne(_m *ProjectContributor) *ProjectContributorUpdateOne { + mutation := newProjectContributorMutation(c.config, OpUpdateOne, withProjectContributor(_m)) + return &ProjectContributorUpdateOne{config: c.config, hooks: c.Hooks(), mutation: mutation} +} + +// UpdateOneID returns an update builder for the given id. +func (c *ProjectContributorClient) UpdateOneID(id uuid.UUID) *ProjectContributorUpdateOne { + mutation := newProjectContributorMutation(c.config, OpUpdateOne, withProjectContributorID(id)) + return &ProjectContributorUpdateOne{config: c.config, hooks: c.Hooks(), mutation: mutation} +} + +// Delete returns a delete builder for ProjectContributor. +func (c *ProjectContributorClient) Delete() *ProjectContributorDelete { + mutation := newProjectContributorMutation(c.config, OpDelete) + return &ProjectContributorDelete{config: c.config, hooks: c.Hooks(), mutation: mutation} +} + +// DeleteOne returns a builder for deleting the given entity. +func (c *ProjectContributorClient) DeleteOne(_m *ProjectContributor) *ProjectContributorDeleteOne { + return c.DeleteOneID(_m.ID) +} + +// DeleteOneID returns a builder for deleting the given entity by its id. +func (c *ProjectContributorClient) DeleteOneID(id uuid.UUID) *ProjectContributorDeleteOne { + builder := c.Delete().Where(projectcontributor.ID(id)) + builder.mutation.id = &id + builder.mutation.op = OpDeleteOne + return &ProjectContributorDeleteOne{builder} +} + +// Query returns a query builder for ProjectContributor. +func (c *ProjectContributorClient) Query() *ProjectContributorQuery { + return &ProjectContributorQuery{ + config: c.config, + ctx: &QueryContext{Type: TypeProjectContributor}, + inters: c.Interceptors(), + } +} + +// Get returns a ProjectContributor entity by its id. +func (c *ProjectContributorClient) Get(ctx context.Context, id uuid.UUID) (*ProjectContributor, error) { + return c.Query().Where(projectcontributor.ID(id)).Only(ctx) +} + +// GetX is like Get, but panics if an error occurs. +func (c *ProjectContributorClient) GetX(ctx context.Context, id uuid.UUID) *ProjectContributor { + obj, err := c.Get(ctx, id) + if err != nil { + panic(err) + } + return obj +} + +// Hooks returns the client hooks. +func (c *ProjectContributorClient) Hooks() []Hook { + return c.hooks.ProjectContributor +} + +// Interceptors returns the client interceptors. +func (c *ProjectContributorClient) Interceptors() []Interceptor { + return c.inters.ProjectContributor +} + +func (c *ProjectContributorClient) mutate(ctx context.Context, m *ProjectContributorMutation) (Value, error) { + switch m.Op() { + case OpCreate: + return (&ProjectContributorCreate{config: c.config, hooks: c.Hooks(), mutation: m}).Save(ctx) + case OpUpdate: + return (&ProjectContributorUpdate{config: c.config, hooks: c.Hooks(), mutation: m}).Save(ctx) + case OpUpdateOne: + return (&ProjectContributorUpdateOne{config: c.config, hooks: c.Hooks(), mutation: m}).Save(ctx) + case OpDelete, OpDeleteOne: + return (&ProjectContributorDelete{config: c.config, hooks: c.Hooks(), mutation: m}).Exec(ctx) + default: + return nil, fmt.Errorf("ent: unknown ProjectContributor mutation op: %q", m.Op()) + } +} + +// ProjectSyncStateClient is a client for the ProjectSyncState schema. +type ProjectSyncStateClient struct { + config +} + +// NewProjectSyncStateClient returns a client for the ProjectSyncState from the given config. +func NewProjectSyncStateClient(c config) *ProjectSyncStateClient { + return &ProjectSyncStateClient{config: c} +} + +// Use adds a list of mutation hooks to the hooks stack. +// A call to `Use(f, g, h)` equals to `projectsyncstate.Hooks(f(g(h())))`. +func (c *ProjectSyncStateClient) Use(hooks ...Hook) { + c.hooks.ProjectSyncState = append(c.hooks.ProjectSyncState, hooks...) +} + +// Intercept adds a list of query interceptors to the interceptors stack. +// A call to `Intercept(f, g, h)` equals to `projectsyncstate.Intercept(f(g(h())))`. +func (c *ProjectSyncStateClient) Intercept(interceptors ...Interceptor) { + c.inters.ProjectSyncState = append(c.inters.ProjectSyncState, interceptors...) +} + +// Create returns a builder for creating a ProjectSyncState entity. +func (c *ProjectSyncStateClient) Create() *ProjectSyncStateCreate { + mutation := newProjectSyncStateMutation(c.config, OpCreate) + return &ProjectSyncStateCreate{config: c.config, hooks: c.Hooks(), mutation: mutation} +} + +// CreateBulk returns a builder for creating a bulk of ProjectSyncState entities. +func (c *ProjectSyncStateClient) CreateBulk(builders ...*ProjectSyncStateCreate) *ProjectSyncStateCreateBulk { + return &ProjectSyncStateCreateBulk{config: c.config, builders: builders} +} + +// MapCreateBulk creates a bulk creation builder from the given slice. For each item in the slice, the function creates +// a builder and applies setFunc on it. +func (c *ProjectSyncStateClient) MapCreateBulk(slice any, setFunc func(*ProjectSyncStateCreate, int)) *ProjectSyncStateCreateBulk { + rv := reflect.ValueOf(slice) + if rv.Kind() != reflect.Slice { + return &ProjectSyncStateCreateBulk{err: fmt.Errorf("calling to ProjectSyncStateClient.MapCreateBulk with wrong type %T, need slice", slice)} + } + builders := make([]*ProjectSyncStateCreate, rv.Len()) + for i := 0; i < rv.Len(); i++ { + builders[i] = c.Create() + setFunc(builders[i], i) + } + return &ProjectSyncStateCreateBulk{config: c.config, builders: builders} +} + +// Update returns an update builder for ProjectSyncState. +func (c *ProjectSyncStateClient) Update() *ProjectSyncStateUpdate { + mutation := newProjectSyncStateMutation(c.config, OpUpdate) + return &ProjectSyncStateUpdate{config: c.config, hooks: c.Hooks(), mutation: mutation} +} + +// UpdateOne returns an update builder for the given entity. +func (c *ProjectSyncStateClient) UpdateOne(_m *ProjectSyncState) *ProjectSyncStateUpdateOne { + mutation := newProjectSyncStateMutation(c.config, OpUpdateOne, withProjectSyncState(_m)) + return &ProjectSyncStateUpdateOne{config: c.config, hooks: c.Hooks(), mutation: mutation} +} + +// UpdateOneID returns an update builder for the given id. +func (c *ProjectSyncStateClient) UpdateOneID(id uuid.UUID) *ProjectSyncStateUpdateOne { + mutation := newProjectSyncStateMutation(c.config, OpUpdateOne, withProjectSyncStateID(id)) + return &ProjectSyncStateUpdateOne{config: c.config, hooks: c.Hooks(), mutation: mutation} +} + +// Delete returns a delete builder for ProjectSyncState. +func (c *ProjectSyncStateClient) Delete() *ProjectSyncStateDelete { + mutation := newProjectSyncStateMutation(c.config, OpDelete) + return &ProjectSyncStateDelete{config: c.config, hooks: c.Hooks(), mutation: mutation} +} + +// DeleteOne returns a builder for deleting the given entity. +func (c *ProjectSyncStateClient) DeleteOne(_m *ProjectSyncState) *ProjectSyncStateDeleteOne { + return c.DeleteOneID(_m.ID) +} + +// DeleteOneID returns a builder for deleting the given entity by its id. +func (c *ProjectSyncStateClient) DeleteOneID(id uuid.UUID) *ProjectSyncStateDeleteOne { + builder := c.Delete().Where(projectsyncstate.ID(id)) + builder.mutation.id = &id + builder.mutation.op = OpDeleteOne + return &ProjectSyncStateDeleteOne{builder} +} + +// Query returns a query builder for ProjectSyncState. +func (c *ProjectSyncStateClient) Query() *ProjectSyncStateQuery { + return &ProjectSyncStateQuery{ + config: c.config, + ctx: &QueryContext{Type: TypeProjectSyncState}, + inters: c.Interceptors(), + } +} + +// Get returns a ProjectSyncState entity by its id. +func (c *ProjectSyncStateClient) Get(ctx context.Context, id uuid.UUID) (*ProjectSyncState, error) { + return c.Query().Where(projectsyncstate.ID(id)).Only(ctx) +} + +// GetX is like Get, but panics if an error occurs. +func (c *ProjectSyncStateClient) GetX(ctx context.Context, id uuid.UUID) *ProjectSyncState { + obj, err := c.Get(ctx, id) + if err != nil { + panic(err) + } + return obj +} + +// Hooks returns the client hooks. +func (c *ProjectSyncStateClient) Hooks() []Hook { + return c.hooks.ProjectSyncState +} + +// Interceptors returns the client interceptors. +func (c *ProjectSyncStateClient) Interceptors() []Interceptor { + return c.inters.ProjectSyncState +} + +func (c *ProjectSyncStateClient) mutate(ctx context.Context, m *ProjectSyncStateMutation) (Value, error) { + switch m.Op() { + case OpCreate: + return (&ProjectSyncStateCreate{config: c.config, hooks: c.Hooks(), mutation: m}).Save(ctx) + case OpUpdate: + return (&ProjectSyncStateUpdate{config: c.config, hooks: c.Hooks(), mutation: m}).Save(ctx) + case OpUpdateOne: + return (&ProjectSyncStateUpdateOne{config: c.config, hooks: c.Hooks(), mutation: m}).Save(ctx) + case OpDelete, OpDeleteOne: + return (&ProjectSyncStateDelete{config: c.config, hooks: c.Hooks(), mutation: m}).Exec(ctx) + default: + return nil, fmt.Errorf("ent: unknown ProjectSyncState mutation op: %q", m.Op()) + } +} + +// RuntimeBrokerClient is a client for the RuntimeBroker schema. +type RuntimeBrokerClient struct { + config +} + +// NewRuntimeBrokerClient returns a client for the RuntimeBroker from the given config. +func NewRuntimeBrokerClient(c config) *RuntimeBrokerClient { + return &RuntimeBrokerClient{config: c} +} + +// Use adds a list of mutation hooks to the hooks stack. +// A call to `Use(f, g, h)` equals to `runtimebroker.Hooks(f(g(h())))`. +func (c *RuntimeBrokerClient) Use(hooks ...Hook) { + c.hooks.RuntimeBroker = append(c.hooks.RuntimeBroker, hooks...) +} + +// Intercept adds a list of query interceptors to the interceptors stack. +// A call to `Intercept(f, g, h)` equals to `runtimebroker.Intercept(f(g(h())))`. +func (c *RuntimeBrokerClient) Intercept(interceptors ...Interceptor) { + c.inters.RuntimeBroker = append(c.inters.RuntimeBroker, interceptors...) +} + +// Create returns a builder for creating a RuntimeBroker entity. +func (c *RuntimeBrokerClient) Create() *RuntimeBrokerCreate { + mutation := newRuntimeBrokerMutation(c.config, OpCreate) + return &RuntimeBrokerCreate{config: c.config, hooks: c.Hooks(), mutation: mutation} +} + +// CreateBulk returns a builder for creating a bulk of RuntimeBroker entities. +func (c *RuntimeBrokerClient) CreateBulk(builders ...*RuntimeBrokerCreate) *RuntimeBrokerCreateBulk { + return &RuntimeBrokerCreateBulk{config: c.config, builders: builders} +} + +// MapCreateBulk creates a bulk creation builder from the given slice. For each item in the slice, the function creates +// a builder and applies setFunc on it. +func (c *RuntimeBrokerClient) MapCreateBulk(slice any, setFunc func(*RuntimeBrokerCreate, int)) *RuntimeBrokerCreateBulk { + rv := reflect.ValueOf(slice) + if rv.Kind() != reflect.Slice { + return &RuntimeBrokerCreateBulk{err: fmt.Errorf("calling to RuntimeBrokerClient.MapCreateBulk with wrong type %T, need slice", slice)} + } + builders := make([]*RuntimeBrokerCreate, rv.Len()) + for i := 0; i < rv.Len(); i++ { + builders[i] = c.Create() + setFunc(builders[i], i) + } + return &RuntimeBrokerCreateBulk{config: c.config, builders: builders} +} + +// Update returns an update builder for RuntimeBroker. +func (c *RuntimeBrokerClient) Update() *RuntimeBrokerUpdate { + mutation := newRuntimeBrokerMutation(c.config, OpUpdate) + return &RuntimeBrokerUpdate{config: c.config, hooks: c.Hooks(), mutation: mutation} +} + +// UpdateOne returns an update builder for the given entity. +func (c *RuntimeBrokerClient) UpdateOne(_m *RuntimeBroker) *RuntimeBrokerUpdateOne { + mutation := newRuntimeBrokerMutation(c.config, OpUpdateOne, withRuntimeBroker(_m)) + return &RuntimeBrokerUpdateOne{config: c.config, hooks: c.Hooks(), mutation: mutation} +} + +// UpdateOneID returns an update builder for the given id. +func (c *RuntimeBrokerClient) UpdateOneID(id uuid.UUID) *RuntimeBrokerUpdateOne { + mutation := newRuntimeBrokerMutation(c.config, OpUpdateOne, withRuntimeBrokerID(id)) + return &RuntimeBrokerUpdateOne{config: c.config, hooks: c.Hooks(), mutation: mutation} +} + +// Delete returns a delete builder for RuntimeBroker. +func (c *RuntimeBrokerClient) Delete() *RuntimeBrokerDelete { + mutation := newRuntimeBrokerMutation(c.config, OpDelete) + return &RuntimeBrokerDelete{config: c.config, hooks: c.Hooks(), mutation: mutation} +} + +// DeleteOne returns a builder for deleting the given entity. +func (c *RuntimeBrokerClient) DeleteOne(_m *RuntimeBroker) *RuntimeBrokerDeleteOne { + return c.DeleteOneID(_m.ID) +} + +// DeleteOneID returns a builder for deleting the given entity by its id. +func (c *RuntimeBrokerClient) DeleteOneID(id uuid.UUID) *RuntimeBrokerDeleteOne { + builder := c.Delete().Where(runtimebroker.ID(id)) + builder.mutation.id = &id + builder.mutation.op = OpDeleteOne + return &RuntimeBrokerDeleteOne{builder} +} + +// Query returns a query builder for RuntimeBroker. +func (c *RuntimeBrokerClient) Query() *RuntimeBrokerQuery { + return &RuntimeBrokerQuery{ + config: c.config, + ctx: &QueryContext{Type: TypeRuntimeBroker}, + inters: c.Interceptors(), + } +} + +// Get returns a RuntimeBroker entity by its id. +func (c *RuntimeBrokerClient) Get(ctx context.Context, id uuid.UUID) (*RuntimeBroker, error) { + return c.Query().Where(runtimebroker.ID(id)).Only(ctx) +} + +// GetX is like Get, but panics if an error occurs. +func (c *RuntimeBrokerClient) GetX(ctx context.Context, id uuid.UUID) *RuntimeBroker { + obj, err := c.Get(ctx, id) + if err != nil { + panic(err) + } + return obj +} + +// Hooks returns the client hooks. +func (c *RuntimeBrokerClient) Hooks() []Hook { + return c.hooks.RuntimeBroker +} + +// Interceptors returns the client interceptors. +func (c *RuntimeBrokerClient) Interceptors() []Interceptor { + return c.inters.RuntimeBroker +} + +func (c *RuntimeBrokerClient) mutate(ctx context.Context, m *RuntimeBrokerMutation) (Value, error) { + switch m.Op() { + case OpCreate: + return (&RuntimeBrokerCreate{config: c.config, hooks: c.Hooks(), mutation: m}).Save(ctx) + case OpUpdate: + return (&RuntimeBrokerUpdate{config: c.config, hooks: c.Hooks(), mutation: m}).Save(ctx) + case OpUpdateOne: + return (&RuntimeBrokerUpdateOne{config: c.config, hooks: c.Hooks(), mutation: m}).Save(ctx) + case OpDelete, OpDeleteOne: + return (&RuntimeBrokerDelete{config: c.config, hooks: c.Hooks(), mutation: m}).Exec(ctx) + default: + return nil, fmt.Errorf("ent: unknown RuntimeBroker mutation op: %q", m.Op()) + } +} + +// ScheduleClient is a client for the Schedule schema. +type ScheduleClient struct { + config +} + +// NewScheduleClient returns a client for the Schedule from the given config. +func NewScheduleClient(c config) *ScheduleClient { + return &ScheduleClient{config: c} +} + +// Use adds a list of mutation hooks to the hooks stack. +// A call to `Use(f, g, h)` equals to `schedule.Hooks(f(g(h())))`. +func (c *ScheduleClient) Use(hooks ...Hook) { + c.hooks.Schedule = append(c.hooks.Schedule, hooks...) +} + +// Intercept adds a list of query interceptors to the interceptors stack. +// A call to `Intercept(f, g, h)` equals to `schedule.Intercept(f(g(h())))`. +func (c *ScheduleClient) Intercept(interceptors ...Interceptor) { + c.inters.Schedule = append(c.inters.Schedule, interceptors...) +} + +// Create returns a builder for creating a Schedule entity. +func (c *ScheduleClient) Create() *ScheduleCreate { + mutation := newScheduleMutation(c.config, OpCreate) + return &ScheduleCreate{config: c.config, hooks: c.Hooks(), mutation: mutation} +} + +// CreateBulk returns a builder for creating a bulk of Schedule entities. +func (c *ScheduleClient) CreateBulk(builders ...*ScheduleCreate) *ScheduleCreateBulk { + return &ScheduleCreateBulk{config: c.config, builders: builders} +} + +// MapCreateBulk creates a bulk creation builder from the given slice. For each item in the slice, the function creates +// a builder and applies setFunc on it. +func (c *ScheduleClient) MapCreateBulk(slice any, setFunc func(*ScheduleCreate, int)) *ScheduleCreateBulk { + rv := reflect.ValueOf(slice) + if rv.Kind() != reflect.Slice { + return &ScheduleCreateBulk{err: fmt.Errorf("calling to ScheduleClient.MapCreateBulk with wrong type %T, need slice", slice)} + } + builders := make([]*ScheduleCreate, rv.Len()) + for i := 0; i < rv.Len(); i++ { + builders[i] = c.Create() + setFunc(builders[i], i) + } + return &ScheduleCreateBulk{config: c.config, builders: builders} +} + +// Update returns an update builder for Schedule. +func (c *ScheduleClient) Update() *ScheduleUpdate { + mutation := newScheduleMutation(c.config, OpUpdate) + return &ScheduleUpdate{config: c.config, hooks: c.Hooks(), mutation: mutation} +} + +// UpdateOne returns an update builder for the given entity. +func (c *ScheduleClient) UpdateOne(_m *Schedule) *ScheduleUpdateOne { + mutation := newScheduleMutation(c.config, OpUpdateOne, withSchedule(_m)) + return &ScheduleUpdateOne{config: c.config, hooks: c.Hooks(), mutation: mutation} +} + +// UpdateOneID returns an update builder for the given id. +func (c *ScheduleClient) UpdateOneID(id uuid.UUID) *ScheduleUpdateOne { + mutation := newScheduleMutation(c.config, OpUpdateOne, withScheduleID(id)) + return &ScheduleUpdateOne{config: c.config, hooks: c.Hooks(), mutation: mutation} +} + +// Delete returns a delete builder for Schedule. +func (c *ScheduleClient) Delete() *ScheduleDelete { + mutation := newScheduleMutation(c.config, OpDelete) + return &ScheduleDelete{config: c.config, hooks: c.Hooks(), mutation: mutation} +} + +// DeleteOne returns a builder for deleting the given entity. +func (c *ScheduleClient) DeleteOne(_m *Schedule) *ScheduleDeleteOne { + return c.DeleteOneID(_m.ID) +} + +// DeleteOneID returns a builder for deleting the given entity by its id. +func (c *ScheduleClient) DeleteOneID(id uuid.UUID) *ScheduleDeleteOne { + builder := c.Delete().Where(schedule.ID(id)) + builder.mutation.id = &id + builder.mutation.op = OpDeleteOne + return &ScheduleDeleteOne{builder} +} + +// Query returns a query builder for Schedule. +func (c *ScheduleClient) Query() *ScheduleQuery { + return &ScheduleQuery{ + config: c.config, + ctx: &QueryContext{Type: TypeSchedule}, + inters: c.Interceptors(), + } +} + +// Get returns a Schedule entity by its id. +func (c *ScheduleClient) Get(ctx context.Context, id uuid.UUID) (*Schedule, error) { + return c.Query().Where(schedule.ID(id)).Only(ctx) +} + +// GetX is like Get, but panics if an error occurs. +func (c *ScheduleClient) GetX(ctx context.Context, id uuid.UUID) *Schedule { + obj, err := c.Get(ctx, id) + if err != nil { + panic(err) + } + return obj } // Hooks returns the client hooks. -func (c *GroupMembershipClient) Hooks() []Hook { - return c.hooks.GroupMembership +func (c *ScheduleClient) Hooks() []Hook { + return c.hooks.Schedule } // Interceptors returns the client interceptors. -func (c *GroupMembershipClient) Interceptors() []Interceptor { - return c.inters.GroupMembership +func (c *ScheduleClient) Interceptors() []Interceptor { + return c.inters.Schedule } -func (c *GroupMembershipClient) mutate(ctx context.Context, m *GroupMembershipMutation) (Value, error) { +func (c *ScheduleClient) mutate(ctx context.Context, m *ScheduleMutation) (Value, error) { switch m.Op() { case OpCreate: - return (&GroupMembershipCreate{config: c.config, hooks: c.Hooks(), mutation: m}).Save(ctx) + return (&ScheduleCreate{config: c.config, hooks: c.Hooks(), mutation: m}).Save(ctx) case OpUpdate: - return (&GroupMembershipUpdate{config: c.config, hooks: c.Hooks(), mutation: m}).Save(ctx) + return (&ScheduleUpdate{config: c.config, hooks: c.Hooks(), mutation: m}).Save(ctx) case OpUpdateOne: - return (&GroupMembershipUpdateOne{config: c.config, hooks: c.Hooks(), mutation: m}).Save(ctx) + return (&ScheduleUpdateOne{config: c.config, hooks: c.Hooks(), mutation: m}).Save(ctx) case OpDelete, OpDeleteOne: - return (&GroupMembershipDelete{config: c.config, hooks: c.Hooks(), mutation: m}).Exec(ctx) + return (&ScheduleDelete{config: c.config, hooks: c.Hooks(), mutation: m}).Exec(ctx) default: - return nil, fmt.Errorf("ent: unknown GroupMembership mutation op: %q", m.Op()) + return nil, fmt.Errorf("ent: unknown Schedule mutation op: %q", m.Op()) } } -// PolicyBindingClient is a client for the PolicyBinding schema. -type PolicyBindingClient struct { +// ScheduledEventClient is a client for the ScheduledEvent schema. +type ScheduledEventClient struct { config } -// NewPolicyBindingClient returns a client for the PolicyBinding from the given config. -func NewPolicyBindingClient(c config) *PolicyBindingClient { - return &PolicyBindingClient{config: c} +// NewScheduledEventClient returns a client for the ScheduledEvent from the given config. +func NewScheduledEventClient(c config) *ScheduledEventClient { + return &ScheduledEventClient{config: c} } // Use adds a list of mutation hooks to the hooks stack. -// A call to `Use(f, g, h)` equals to `policybinding.Hooks(f(g(h())))`. -func (c *PolicyBindingClient) Use(hooks ...Hook) { - c.hooks.PolicyBinding = append(c.hooks.PolicyBinding, hooks...) +// A call to `Use(f, g, h)` equals to `scheduledevent.Hooks(f(g(h())))`. +func (c *ScheduledEventClient) Use(hooks ...Hook) { + c.hooks.ScheduledEvent = append(c.hooks.ScheduledEvent, hooks...) } // Intercept adds a list of query interceptors to the interceptors stack. -// A call to `Intercept(f, g, h)` equals to `policybinding.Intercept(f(g(h())))`. -func (c *PolicyBindingClient) Intercept(interceptors ...Interceptor) { - c.inters.PolicyBinding = append(c.inters.PolicyBinding, interceptors...) +// A call to `Intercept(f, g, h)` equals to `scheduledevent.Intercept(f(g(h())))`. +func (c *ScheduledEventClient) Intercept(interceptors ...Interceptor) { + c.inters.ScheduledEvent = append(c.inters.ScheduledEvent, interceptors...) } -// Create returns a builder for creating a PolicyBinding entity. -func (c *PolicyBindingClient) Create() *PolicyBindingCreate { - mutation := newPolicyBindingMutation(c.config, OpCreate) - return &PolicyBindingCreate{config: c.config, hooks: c.Hooks(), mutation: mutation} +// Create returns a builder for creating a ScheduledEvent entity. +func (c *ScheduledEventClient) Create() *ScheduledEventCreate { + mutation := newScheduledEventMutation(c.config, OpCreate) + return &ScheduledEventCreate{config: c.config, hooks: c.Hooks(), mutation: mutation} } -// CreateBulk returns a builder for creating a bulk of PolicyBinding entities. -func (c *PolicyBindingClient) CreateBulk(builders ...*PolicyBindingCreate) *PolicyBindingCreateBulk { - return &PolicyBindingCreateBulk{config: c.config, builders: builders} +// CreateBulk returns a builder for creating a bulk of ScheduledEvent entities. +func (c *ScheduledEventClient) CreateBulk(builders ...*ScheduledEventCreate) *ScheduledEventCreateBulk { + return &ScheduledEventCreateBulk{config: c.config, builders: builders} } // MapCreateBulk creates a bulk creation builder from the given slice. For each item in the slice, the function creates // a builder and applies setFunc on it. -func (c *PolicyBindingClient) MapCreateBulk(slice any, setFunc func(*PolicyBindingCreate, int)) *PolicyBindingCreateBulk { +func (c *ScheduledEventClient) MapCreateBulk(slice any, setFunc func(*ScheduledEventCreate, int)) *ScheduledEventCreateBulk { rv := reflect.ValueOf(slice) if rv.Kind() != reflect.Slice { - return &PolicyBindingCreateBulk{err: fmt.Errorf("calling to PolicyBindingClient.MapCreateBulk with wrong type %T, need slice", slice)} + return &ScheduledEventCreateBulk{err: fmt.Errorf("calling to ScheduledEventClient.MapCreateBulk with wrong type %T, need slice", slice)} } - builders := make([]*PolicyBindingCreate, rv.Len()) + builders := make([]*ScheduledEventCreate, rv.Len()) for i := 0; i < rv.Len(); i++ { builders[i] = c.Create() setFunc(builders[i], i) } - return &PolicyBindingCreateBulk{config: c.config, builders: builders} + return &ScheduledEventCreateBulk{config: c.config, builders: builders} } -// Update returns an update builder for PolicyBinding. -func (c *PolicyBindingClient) Update() *PolicyBindingUpdate { - mutation := newPolicyBindingMutation(c.config, OpUpdate) - return &PolicyBindingUpdate{config: c.config, hooks: c.Hooks(), mutation: mutation} +// Update returns an update builder for ScheduledEvent. +func (c *ScheduledEventClient) Update() *ScheduledEventUpdate { + mutation := newScheduledEventMutation(c.config, OpUpdate) + return &ScheduledEventUpdate{config: c.config, hooks: c.Hooks(), mutation: mutation} } // UpdateOne returns an update builder for the given entity. -func (c *PolicyBindingClient) UpdateOne(_m *PolicyBinding) *PolicyBindingUpdateOne { - mutation := newPolicyBindingMutation(c.config, OpUpdateOne, withPolicyBinding(_m)) - return &PolicyBindingUpdateOne{config: c.config, hooks: c.Hooks(), mutation: mutation} +func (c *ScheduledEventClient) UpdateOne(_m *ScheduledEvent) *ScheduledEventUpdateOne { + mutation := newScheduledEventMutation(c.config, OpUpdateOne, withScheduledEvent(_m)) + return &ScheduledEventUpdateOne{config: c.config, hooks: c.Hooks(), mutation: mutation} } // UpdateOneID returns an update builder for the given id. -func (c *PolicyBindingClient) UpdateOneID(id uuid.UUID) *PolicyBindingUpdateOne { - mutation := newPolicyBindingMutation(c.config, OpUpdateOne, withPolicyBindingID(id)) - return &PolicyBindingUpdateOne{config: c.config, hooks: c.Hooks(), mutation: mutation} +func (c *ScheduledEventClient) UpdateOneID(id uuid.UUID) *ScheduledEventUpdateOne { + mutation := newScheduledEventMutation(c.config, OpUpdateOne, withScheduledEventID(id)) + return &ScheduledEventUpdateOne{config: c.config, hooks: c.Hooks(), mutation: mutation} } -// Delete returns a delete builder for PolicyBinding. -func (c *PolicyBindingClient) Delete() *PolicyBindingDelete { - mutation := newPolicyBindingMutation(c.config, OpDelete) - return &PolicyBindingDelete{config: c.config, hooks: c.Hooks(), mutation: mutation} +// Delete returns a delete builder for ScheduledEvent. +func (c *ScheduledEventClient) Delete() *ScheduledEventDelete { + mutation := newScheduledEventMutation(c.config, OpDelete) + return &ScheduledEventDelete{config: c.config, hooks: c.Hooks(), mutation: mutation} } // DeleteOne returns a builder for deleting the given entity. -func (c *PolicyBindingClient) DeleteOne(_m *PolicyBinding) *PolicyBindingDeleteOne { +func (c *ScheduledEventClient) DeleteOne(_m *ScheduledEvent) *ScheduledEventDeleteOne { return c.DeleteOneID(_m.ID) } // DeleteOneID returns a builder for deleting the given entity by its id. -func (c *PolicyBindingClient) DeleteOneID(id uuid.UUID) *PolicyBindingDeleteOne { - builder := c.Delete().Where(policybinding.ID(id)) +func (c *ScheduledEventClient) DeleteOneID(id uuid.UUID) *ScheduledEventDeleteOne { + builder := c.Delete().Where(scheduledevent.ID(id)) builder.mutation.id = &id builder.mutation.op = OpDeleteOne - return &PolicyBindingDeleteOne{builder} + return &ScheduledEventDeleteOne{builder} } -// Query returns a query builder for PolicyBinding. -func (c *PolicyBindingClient) Query() *PolicyBindingQuery { - return &PolicyBindingQuery{ +// Query returns a query builder for ScheduledEvent. +func (c *ScheduledEventClient) Query() *ScheduledEventQuery { + return &ScheduledEventQuery{ + config: c.config, + ctx: &QueryContext{Type: TypeScheduledEvent}, + inters: c.Interceptors(), + } +} + +// Get returns a ScheduledEvent entity by its id. +func (c *ScheduledEventClient) Get(ctx context.Context, id uuid.UUID) (*ScheduledEvent, error) { + return c.Query().Where(scheduledevent.ID(id)).Only(ctx) +} + +// GetX is like Get, but panics if an error occurs. +func (c *ScheduledEventClient) GetX(ctx context.Context, id uuid.UUID) *ScheduledEvent { + obj, err := c.Get(ctx, id) + if err != nil { + panic(err) + } + return obj +} + +// Hooks returns the client hooks. +func (c *ScheduledEventClient) Hooks() []Hook { + return c.hooks.ScheduledEvent +} + +// Interceptors returns the client interceptors. +func (c *ScheduledEventClient) Interceptors() []Interceptor { + return c.inters.ScheduledEvent +} + +func (c *ScheduledEventClient) mutate(ctx context.Context, m *ScheduledEventMutation) (Value, error) { + switch m.Op() { + case OpCreate: + return (&ScheduledEventCreate{config: c.config, hooks: c.Hooks(), mutation: m}).Save(ctx) + case OpUpdate: + return (&ScheduledEventUpdate{config: c.config, hooks: c.Hooks(), mutation: m}).Save(ctx) + case OpUpdateOne: + return (&ScheduledEventUpdateOne{config: c.config, hooks: c.Hooks(), mutation: m}).Save(ctx) + case OpDelete, OpDeleteOne: + return (&ScheduledEventDelete{config: c.config, hooks: c.Hooks(), mutation: m}).Exec(ctx) + default: + return nil, fmt.Errorf("ent: unknown ScheduledEvent mutation op: %q", m.Op()) + } +} + +// SecretClient is a client for the Secret schema. +type SecretClient struct { + config +} + +// NewSecretClient returns a client for the Secret from the given config. +func NewSecretClient(c config) *SecretClient { + return &SecretClient{config: c} +} + +// Use adds a list of mutation hooks to the hooks stack. +// A call to `Use(f, g, h)` equals to `secret.Hooks(f(g(h())))`. +func (c *SecretClient) Use(hooks ...Hook) { + c.hooks.Secret = append(c.hooks.Secret, hooks...) +} + +// Intercept adds a list of query interceptors to the interceptors stack. +// A call to `Intercept(f, g, h)` equals to `secret.Intercept(f(g(h())))`. +func (c *SecretClient) Intercept(interceptors ...Interceptor) { + c.inters.Secret = append(c.inters.Secret, interceptors...) +} + +// Create returns a builder for creating a Secret entity. +func (c *SecretClient) Create() *SecretCreate { + mutation := newSecretMutation(c.config, OpCreate) + return &SecretCreate{config: c.config, hooks: c.Hooks(), mutation: mutation} +} + +// CreateBulk returns a builder for creating a bulk of Secret entities. +func (c *SecretClient) CreateBulk(builders ...*SecretCreate) *SecretCreateBulk { + return &SecretCreateBulk{config: c.config, builders: builders} +} + +// MapCreateBulk creates a bulk creation builder from the given slice. For each item in the slice, the function creates +// a builder and applies setFunc on it. +func (c *SecretClient) MapCreateBulk(slice any, setFunc func(*SecretCreate, int)) *SecretCreateBulk { + rv := reflect.ValueOf(slice) + if rv.Kind() != reflect.Slice { + return &SecretCreateBulk{err: fmt.Errorf("calling to SecretClient.MapCreateBulk with wrong type %T, need slice", slice)} + } + builders := make([]*SecretCreate, rv.Len()) + for i := 0; i < rv.Len(); i++ { + builders[i] = c.Create() + setFunc(builders[i], i) + } + return &SecretCreateBulk{config: c.config, builders: builders} +} + +// Update returns an update builder for Secret. +func (c *SecretClient) Update() *SecretUpdate { + mutation := newSecretMutation(c.config, OpUpdate) + return &SecretUpdate{config: c.config, hooks: c.Hooks(), mutation: mutation} +} + +// UpdateOne returns an update builder for the given entity. +func (c *SecretClient) UpdateOne(_m *Secret) *SecretUpdateOne { + mutation := newSecretMutation(c.config, OpUpdateOne, withSecret(_m)) + return &SecretUpdateOne{config: c.config, hooks: c.Hooks(), mutation: mutation} +} + +// UpdateOneID returns an update builder for the given id. +func (c *SecretClient) UpdateOneID(id uuid.UUID) *SecretUpdateOne { + mutation := newSecretMutation(c.config, OpUpdateOne, withSecretID(id)) + return &SecretUpdateOne{config: c.config, hooks: c.Hooks(), mutation: mutation} +} + +// Delete returns a delete builder for Secret. +func (c *SecretClient) Delete() *SecretDelete { + mutation := newSecretMutation(c.config, OpDelete) + return &SecretDelete{config: c.config, hooks: c.Hooks(), mutation: mutation} +} + +// DeleteOne returns a builder for deleting the given entity. +func (c *SecretClient) DeleteOne(_m *Secret) *SecretDeleteOne { + return c.DeleteOneID(_m.ID) +} + +// DeleteOneID returns a builder for deleting the given entity by its id. +func (c *SecretClient) DeleteOneID(id uuid.UUID) *SecretDeleteOne { + builder := c.Delete().Where(secret.ID(id)) + builder.mutation.id = &id + builder.mutation.op = OpDeleteOne + return &SecretDeleteOne{builder} +} + +// Query returns a query builder for Secret. +func (c *SecretClient) Query() *SecretQuery { + return &SecretQuery{ + config: c.config, + ctx: &QueryContext{Type: TypeSecret}, + inters: c.Interceptors(), + } +} + +// Get returns a Secret entity by its id. +func (c *SecretClient) Get(ctx context.Context, id uuid.UUID) (*Secret, error) { + return c.Query().Where(secret.ID(id)).Only(ctx) +} + +// GetX is like Get, but panics if an error occurs. +func (c *SecretClient) GetX(ctx context.Context, id uuid.UUID) *Secret { + obj, err := c.Get(ctx, id) + if err != nil { + panic(err) + } + return obj +} + +// Hooks returns the client hooks. +func (c *SecretClient) Hooks() []Hook { + return c.hooks.Secret +} + +// Interceptors returns the client interceptors. +func (c *SecretClient) Interceptors() []Interceptor { + return c.inters.Secret +} + +func (c *SecretClient) mutate(ctx context.Context, m *SecretMutation) (Value, error) { + switch m.Op() { + case OpCreate: + return (&SecretCreate{config: c.config, hooks: c.Hooks(), mutation: m}).Save(ctx) + case OpUpdate: + return (&SecretUpdate{config: c.config, hooks: c.Hooks(), mutation: m}).Save(ctx) + case OpUpdateOne: + return (&SecretUpdateOne{config: c.config, hooks: c.Hooks(), mutation: m}).Save(ctx) + case OpDelete, OpDeleteOne: + return (&SecretDelete{config: c.config, hooks: c.Hooks(), mutation: m}).Exec(ctx) + default: + return nil, fmt.Errorf("ent: unknown Secret mutation op: %q", m.Op()) + } +} + +// SubscriptionTemplateClient is a client for the SubscriptionTemplate schema. +type SubscriptionTemplateClient struct { + config +} + +// NewSubscriptionTemplateClient returns a client for the SubscriptionTemplate from the given config. +func NewSubscriptionTemplateClient(c config) *SubscriptionTemplateClient { + return &SubscriptionTemplateClient{config: c} +} + +// Use adds a list of mutation hooks to the hooks stack. +// A call to `Use(f, g, h)` equals to `subscriptiontemplate.Hooks(f(g(h())))`. +func (c *SubscriptionTemplateClient) Use(hooks ...Hook) { + c.hooks.SubscriptionTemplate = append(c.hooks.SubscriptionTemplate, hooks...) +} + +// Intercept adds a list of query interceptors to the interceptors stack. +// A call to `Intercept(f, g, h)` equals to `subscriptiontemplate.Intercept(f(g(h())))`. +func (c *SubscriptionTemplateClient) Intercept(interceptors ...Interceptor) { + c.inters.SubscriptionTemplate = append(c.inters.SubscriptionTemplate, interceptors...) +} + +// Create returns a builder for creating a SubscriptionTemplate entity. +func (c *SubscriptionTemplateClient) Create() *SubscriptionTemplateCreate { + mutation := newSubscriptionTemplateMutation(c.config, OpCreate) + return &SubscriptionTemplateCreate{config: c.config, hooks: c.Hooks(), mutation: mutation} +} + +// CreateBulk returns a builder for creating a bulk of SubscriptionTemplate entities. +func (c *SubscriptionTemplateClient) CreateBulk(builders ...*SubscriptionTemplateCreate) *SubscriptionTemplateCreateBulk { + return &SubscriptionTemplateCreateBulk{config: c.config, builders: builders} +} + +// MapCreateBulk creates a bulk creation builder from the given slice. For each item in the slice, the function creates +// a builder and applies setFunc on it. +func (c *SubscriptionTemplateClient) MapCreateBulk(slice any, setFunc func(*SubscriptionTemplateCreate, int)) *SubscriptionTemplateCreateBulk { + rv := reflect.ValueOf(slice) + if rv.Kind() != reflect.Slice { + return &SubscriptionTemplateCreateBulk{err: fmt.Errorf("calling to SubscriptionTemplateClient.MapCreateBulk with wrong type %T, need slice", slice)} + } + builders := make([]*SubscriptionTemplateCreate, rv.Len()) + for i := 0; i < rv.Len(); i++ { + builders[i] = c.Create() + setFunc(builders[i], i) + } + return &SubscriptionTemplateCreateBulk{config: c.config, builders: builders} +} + +// Update returns an update builder for SubscriptionTemplate. +func (c *SubscriptionTemplateClient) Update() *SubscriptionTemplateUpdate { + mutation := newSubscriptionTemplateMutation(c.config, OpUpdate) + return &SubscriptionTemplateUpdate{config: c.config, hooks: c.Hooks(), mutation: mutation} +} + +// UpdateOne returns an update builder for the given entity. +func (c *SubscriptionTemplateClient) UpdateOne(_m *SubscriptionTemplate) *SubscriptionTemplateUpdateOne { + mutation := newSubscriptionTemplateMutation(c.config, OpUpdateOne, withSubscriptionTemplate(_m)) + return &SubscriptionTemplateUpdateOne{config: c.config, hooks: c.Hooks(), mutation: mutation} +} + +// UpdateOneID returns an update builder for the given id. +func (c *SubscriptionTemplateClient) UpdateOneID(id uuid.UUID) *SubscriptionTemplateUpdateOne { + mutation := newSubscriptionTemplateMutation(c.config, OpUpdateOne, withSubscriptionTemplateID(id)) + return &SubscriptionTemplateUpdateOne{config: c.config, hooks: c.Hooks(), mutation: mutation} +} + +// Delete returns a delete builder for SubscriptionTemplate. +func (c *SubscriptionTemplateClient) Delete() *SubscriptionTemplateDelete { + mutation := newSubscriptionTemplateMutation(c.config, OpDelete) + return &SubscriptionTemplateDelete{config: c.config, hooks: c.Hooks(), mutation: mutation} +} + +// DeleteOne returns a builder for deleting the given entity. +func (c *SubscriptionTemplateClient) DeleteOne(_m *SubscriptionTemplate) *SubscriptionTemplateDeleteOne { + return c.DeleteOneID(_m.ID) +} + +// DeleteOneID returns a builder for deleting the given entity by its id. +func (c *SubscriptionTemplateClient) DeleteOneID(id uuid.UUID) *SubscriptionTemplateDeleteOne { + builder := c.Delete().Where(subscriptiontemplate.ID(id)) + builder.mutation.id = &id + builder.mutation.op = OpDeleteOne + return &SubscriptionTemplateDeleteOne{builder} +} + +// Query returns a query builder for SubscriptionTemplate. +func (c *SubscriptionTemplateClient) Query() *SubscriptionTemplateQuery { + return &SubscriptionTemplateQuery{ config: c.config, - ctx: &QueryContext{Type: TypePolicyBinding}, + ctx: &QueryContext{Type: TypeSubscriptionTemplate}, inters: c.Interceptors(), } } -// Get returns a PolicyBinding entity by its id. -func (c *PolicyBindingClient) Get(ctx context.Context, id uuid.UUID) (*PolicyBinding, error) { - return c.Query().Where(policybinding.ID(id)).Only(ctx) +// Get returns a SubscriptionTemplate entity by its id. +func (c *SubscriptionTemplateClient) Get(ctx context.Context, id uuid.UUID) (*SubscriptionTemplate, error) { + return c.Query().Where(subscriptiontemplate.ID(id)).Only(ctx) } // GetX is like Get, but panics if an error occurs. -func (c *PolicyBindingClient) GetX(ctx context.Context, id uuid.UUID) *PolicyBinding { +func (c *SubscriptionTemplateClient) GetX(ctx context.Context, id uuid.UUID) *SubscriptionTemplate { obj, err := c.Get(ctx, id) if err != nil { panic(err) @@ -1120,196 +4431,132 @@ func (c *PolicyBindingClient) GetX(ctx context.Context, id uuid.UUID) *PolicyBin return obj } -// QueryPolicy queries the policy edge of a PolicyBinding. -func (c *PolicyBindingClient) QueryPolicy(_m *PolicyBinding) *AccessPolicyQuery { - query := (&AccessPolicyClient{config: c.config}).Query() - query.path = func(context.Context) (fromV *sql.Selector, _ error) { - id := _m.ID - step := sqlgraph.NewStep( - sqlgraph.From(policybinding.Table, policybinding.FieldID, id), - sqlgraph.To(accesspolicy.Table, accesspolicy.FieldID), - sqlgraph.Edge(sqlgraph.M2O, true, policybinding.PolicyTable, policybinding.PolicyColumn), - ) - fromV = sqlgraph.Neighbors(_m.driver.Dialect(), step) - return fromV, nil - } - return query -} - -// QueryUser queries the user edge of a PolicyBinding. -func (c *PolicyBindingClient) QueryUser(_m *PolicyBinding) *UserQuery { - query := (&UserClient{config: c.config}).Query() - query.path = func(context.Context) (fromV *sql.Selector, _ error) { - id := _m.ID - step := sqlgraph.NewStep( - sqlgraph.From(policybinding.Table, policybinding.FieldID, id), - sqlgraph.To(user.Table, user.FieldID), - sqlgraph.Edge(sqlgraph.M2O, false, policybinding.UserTable, policybinding.UserColumn), - ) - fromV = sqlgraph.Neighbors(_m.driver.Dialect(), step) - return fromV, nil - } - return query -} - -// QueryGroup queries the group edge of a PolicyBinding. -func (c *PolicyBindingClient) QueryGroup(_m *PolicyBinding) *GroupQuery { - query := (&GroupClient{config: c.config}).Query() - query.path = func(context.Context) (fromV *sql.Selector, _ error) { - id := _m.ID - step := sqlgraph.NewStep( - sqlgraph.From(policybinding.Table, policybinding.FieldID, id), - sqlgraph.To(group.Table, group.FieldID), - sqlgraph.Edge(sqlgraph.M2O, false, policybinding.GroupTable, policybinding.GroupColumn), - ) - fromV = sqlgraph.Neighbors(_m.driver.Dialect(), step) - return fromV, nil - } - return query -} - -// QueryAgent queries the agent edge of a PolicyBinding. -func (c *PolicyBindingClient) QueryAgent(_m *PolicyBinding) *AgentQuery { - query := (&AgentClient{config: c.config}).Query() - query.path = func(context.Context) (fromV *sql.Selector, _ error) { - id := _m.ID - step := sqlgraph.NewStep( - sqlgraph.From(policybinding.Table, policybinding.FieldID, id), - sqlgraph.To(agent.Table, agent.FieldID), - sqlgraph.Edge(sqlgraph.M2O, false, policybinding.AgentTable, policybinding.AgentColumn), - ) - fromV = sqlgraph.Neighbors(_m.driver.Dialect(), step) - return fromV, nil - } - return query -} - // Hooks returns the client hooks. -func (c *PolicyBindingClient) Hooks() []Hook { - return c.hooks.PolicyBinding +func (c *SubscriptionTemplateClient) Hooks() []Hook { + return c.hooks.SubscriptionTemplate } // Interceptors returns the client interceptors. -func (c *PolicyBindingClient) Interceptors() []Interceptor { - return c.inters.PolicyBinding +func (c *SubscriptionTemplateClient) Interceptors() []Interceptor { + return c.inters.SubscriptionTemplate } -func (c *PolicyBindingClient) mutate(ctx context.Context, m *PolicyBindingMutation) (Value, error) { +func (c *SubscriptionTemplateClient) mutate(ctx context.Context, m *SubscriptionTemplateMutation) (Value, error) { switch m.Op() { case OpCreate: - return (&PolicyBindingCreate{config: c.config, hooks: c.Hooks(), mutation: m}).Save(ctx) + return (&SubscriptionTemplateCreate{config: c.config, hooks: c.Hooks(), mutation: m}).Save(ctx) case OpUpdate: - return (&PolicyBindingUpdate{config: c.config, hooks: c.Hooks(), mutation: m}).Save(ctx) + return (&SubscriptionTemplateUpdate{config: c.config, hooks: c.Hooks(), mutation: m}).Save(ctx) case OpUpdateOne: - return (&PolicyBindingUpdateOne{config: c.config, hooks: c.Hooks(), mutation: m}).Save(ctx) + return (&SubscriptionTemplateUpdateOne{config: c.config, hooks: c.Hooks(), mutation: m}).Save(ctx) case OpDelete, OpDeleteOne: - return (&PolicyBindingDelete{config: c.config, hooks: c.Hooks(), mutation: m}).Exec(ctx) + return (&SubscriptionTemplateDelete{config: c.config, hooks: c.Hooks(), mutation: m}).Exec(ctx) default: - return nil, fmt.Errorf("ent: unknown PolicyBinding mutation op: %q", m.Op()) + return nil, fmt.Errorf("ent: unknown SubscriptionTemplate mutation op: %q", m.Op()) } } -// ProjectClient is a client for the Project schema. -type ProjectClient struct { +// TemplateClient is a client for the Template schema. +type TemplateClient struct { config } -// NewProjectClient returns a client for the Project from the given config. -func NewProjectClient(c config) *ProjectClient { - return &ProjectClient{config: c} +// NewTemplateClient returns a client for the Template from the given config. +func NewTemplateClient(c config) *TemplateClient { + return &TemplateClient{config: c} } // Use adds a list of mutation hooks to the hooks stack. -// A call to `Use(f, g, h)` equals to `project.Hooks(f(g(h())))`. -func (c *ProjectClient) Use(hooks ...Hook) { - c.hooks.Project = append(c.hooks.Project, hooks...) +// A call to `Use(f, g, h)` equals to `template.Hooks(f(g(h())))`. +func (c *TemplateClient) Use(hooks ...Hook) { + c.hooks.Template = append(c.hooks.Template, hooks...) } // Intercept adds a list of query interceptors to the interceptors stack. -// A call to `Intercept(f, g, h)` equals to `project.Intercept(f(g(h())))`. -func (c *ProjectClient) Intercept(interceptors ...Interceptor) { - c.inters.Project = append(c.inters.Project, interceptors...) +// A call to `Intercept(f, g, h)` equals to `template.Intercept(f(g(h())))`. +func (c *TemplateClient) Intercept(interceptors ...Interceptor) { + c.inters.Template = append(c.inters.Template, interceptors...) } -// Create returns a builder for creating a Project entity. -func (c *ProjectClient) Create() *ProjectCreate { - mutation := newProjectMutation(c.config, OpCreate) - return &ProjectCreate{config: c.config, hooks: c.Hooks(), mutation: mutation} +// Create returns a builder for creating a Template entity. +func (c *TemplateClient) Create() *TemplateCreate { + mutation := newTemplateMutation(c.config, OpCreate) + return &TemplateCreate{config: c.config, hooks: c.Hooks(), mutation: mutation} } -// CreateBulk returns a builder for creating a bulk of Project entities. -func (c *ProjectClient) CreateBulk(builders ...*ProjectCreate) *ProjectCreateBulk { - return &ProjectCreateBulk{config: c.config, builders: builders} +// CreateBulk returns a builder for creating a bulk of Template entities. +func (c *TemplateClient) CreateBulk(builders ...*TemplateCreate) *TemplateCreateBulk { + return &TemplateCreateBulk{config: c.config, builders: builders} } // MapCreateBulk creates a bulk creation builder from the given slice. For each item in the slice, the function creates // a builder and applies setFunc on it. -func (c *ProjectClient) MapCreateBulk(slice any, setFunc func(*ProjectCreate, int)) *ProjectCreateBulk { +func (c *TemplateClient) MapCreateBulk(slice any, setFunc func(*TemplateCreate, int)) *TemplateCreateBulk { rv := reflect.ValueOf(slice) if rv.Kind() != reflect.Slice { - return &ProjectCreateBulk{err: fmt.Errorf("calling to ProjectClient.MapCreateBulk with wrong type %T, need slice", slice)} + return &TemplateCreateBulk{err: fmt.Errorf("calling to TemplateClient.MapCreateBulk with wrong type %T, need slice", slice)} } - builders := make([]*ProjectCreate, rv.Len()) + builders := make([]*TemplateCreate, rv.Len()) for i := 0; i < rv.Len(); i++ { builders[i] = c.Create() setFunc(builders[i], i) } - return &ProjectCreateBulk{config: c.config, builders: builders} + return &TemplateCreateBulk{config: c.config, builders: builders} } -// Update returns an update builder for Project. -func (c *ProjectClient) Update() *ProjectUpdate { - mutation := newProjectMutation(c.config, OpUpdate) - return &ProjectUpdate{config: c.config, hooks: c.Hooks(), mutation: mutation} +// Update returns an update builder for Template. +func (c *TemplateClient) Update() *TemplateUpdate { + mutation := newTemplateMutation(c.config, OpUpdate) + return &TemplateUpdate{config: c.config, hooks: c.Hooks(), mutation: mutation} } // UpdateOne returns an update builder for the given entity. -func (c *ProjectClient) UpdateOne(_m *Project) *ProjectUpdateOne { - mutation := newProjectMutation(c.config, OpUpdateOne, withProject(_m)) - return &ProjectUpdateOne{config: c.config, hooks: c.Hooks(), mutation: mutation} +func (c *TemplateClient) UpdateOne(_m *Template) *TemplateUpdateOne { + mutation := newTemplateMutation(c.config, OpUpdateOne, withTemplate(_m)) + return &TemplateUpdateOne{config: c.config, hooks: c.Hooks(), mutation: mutation} } // UpdateOneID returns an update builder for the given id. -func (c *ProjectClient) UpdateOneID(id uuid.UUID) *ProjectUpdateOne { - mutation := newProjectMutation(c.config, OpUpdateOne, withProjectID(id)) - return &ProjectUpdateOne{config: c.config, hooks: c.Hooks(), mutation: mutation} +func (c *TemplateClient) UpdateOneID(id uuid.UUID) *TemplateUpdateOne { + mutation := newTemplateMutation(c.config, OpUpdateOne, withTemplateID(id)) + return &TemplateUpdateOne{config: c.config, hooks: c.Hooks(), mutation: mutation} } -// Delete returns a delete builder for Project. -func (c *ProjectClient) Delete() *ProjectDelete { - mutation := newProjectMutation(c.config, OpDelete) - return &ProjectDelete{config: c.config, hooks: c.Hooks(), mutation: mutation} +// Delete returns a delete builder for Template. +func (c *TemplateClient) Delete() *TemplateDelete { + mutation := newTemplateMutation(c.config, OpDelete) + return &TemplateDelete{config: c.config, hooks: c.Hooks(), mutation: mutation} } // DeleteOne returns a builder for deleting the given entity. -func (c *ProjectClient) DeleteOne(_m *Project) *ProjectDeleteOne { +func (c *TemplateClient) DeleteOne(_m *Template) *TemplateDeleteOne { return c.DeleteOneID(_m.ID) } // DeleteOneID returns a builder for deleting the given entity by its id. -func (c *ProjectClient) DeleteOneID(id uuid.UUID) *ProjectDeleteOne { - builder := c.Delete().Where(project.ID(id)) +func (c *TemplateClient) DeleteOneID(id uuid.UUID) *TemplateDeleteOne { + builder := c.Delete().Where(template.ID(id)) builder.mutation.id = &id builder.mutation.op = OpDeleteOne - return &ProjectDeleteOne{builder} + return &TemplateDeleteOne{builder} } -// Query returns a query builder for Project. -func (c *ProjectClient) Query() *ProjectQuery { - return &ProjectQuery{ +// Query returns a query builder for Template. +func (c *TemplateClient) Query() *TemplateQuery { + return &TemplateQuery{ config: c.config, - ctx: &QueryContext{Type: TypeProject}, + ctx: &QueryContext{Type: TypeTemplate}, inters: c.Interceptors(), } } -// Get returns a Project entity by its id. -func (c *ProjectClient) Get(ctx context.Context, id uuid.UUID) (*Project, error) { - return c.Query().Where(project.ID(id)).Only(ctx) +// Get returns a Template entity by its id. +func (c *TemplateClient) Get(ctx context.Context, id uuid.UUID) (*Template, error) { + return c.Query().Where(template.ID(id)).Only(ctx) } // GetX is like Get, but panics if an error occurs. -func (c *ProjectClient) GetX(ctx context.Context, id uuid.UUID) *Project { +func (c *TemplateClient) GetX(ctx context.Context, id uuid.UUID) *Template { obj, err := c.Get(ctx, id) if err != nil { panic(err) @@ -1317,44 +4564,28 @@ func (c *ProjectClient) GetX(ctx context.Context, id uuid.UUID) *Project { return obj } -// QueryAgents queries the agents edge of a Project. -func (c *ProjectClient) QueryAgents(_m *Project) *AgentQuery { - query := (&AgentClient{config: c.config}).Query() - query.path = func(context.Context) (fromV *sql.Selector, _ error) { - id := _m.ID - step := sqlgraph.NewStep( - sqlgraph.From(project.Table, project.FieldID, id), - sqlgraph.To(agent.Table, agent.FieldID), - sqlgraph.Edge(sqlgraph.O2M, false, project.AgentsTable, project.AgentsColumn), - ) - fromV = sqlgraph.Neighbors(_m.driver.Dialect(), step) - return fromV, nil - } - return query -} - // Hooks returns the client hooks. -func (c *ProjectClient) Hooks() []Hook { - return c.hooks.Project +func (c *TemplateClient) Hooks() []Hook { + return c.hooks.Template } // Interceptors returns the client interceptors. -func (c *ProjectClient) Interceptors() []Interceptor { - return c.inters.Project +func (c *TemplateClient) Interceptors() []Interceptor { + return c.inters.Template } -func (c *ProjectClient) mutate(ctx context.Context, m *ProjectMutation) (Value, error) { +func (c *TemplateClient) mutate(ctx context.Context, m *TemplateMutation) (Value, error) { switch m.Op() { case OpCreate: - return (&ProjectCreate{config: c.config, hooks: c.Hooks(), mutation: m}).Save(ctx) + return (&TemplateCreate{config: c.config, hooks: c.Hooks(), mutation: m}).Save(ctx) case OpUpdate: - return (&ProjectUpdate{config: c.config, hooks: c.Hooks(), mutation: m}).Save(ctx) + return (&TemplateUpdate{config: c.config, hooks: c.Hooks(), mutation: m}).Save(ctx) case OpUpdateOne: - return (&ProjectUpdateOne{config: c.config, hooks: c.Hooks(), mutation: m}).Save(ctx) + return (&TemplateUpdateOne{config: c.config, hooks: c.Hooks(), mutation: m}).Save(ctx) case OpDelete, OpDeleteOne: - return (&ProjectDelete{config: c.config, hooks: c.Hooks(), mutation: m}).Exec(ctx) + return (&TemplateDelete{config: c.config, hooks: c.Hooks(), mutation: m}).Exec(ctx) default: - return nil, fmt.Errorf("ent: unknown Project mutation op: %q", m.Op()) + return nil, fmt.Errorf("ent: unknown Template mutation op: %q", m.Op()) } } @@ -1466,38 +4697,6 @@ func (c *UserClient) GetX(ctx context.Context, id uuid.UUID) *User { return obj } -// QueryCreatedAgents queries the created_agents edge of a User. -func (c *UserClient) QueryCreatedAgents(_m *User) *AgentQuery { - query := (&AgentClient{config: c.config}).Query() - query.path = func(context.Context) (fromV *sql.Selector, _ error) { - id := _m.ID - step := sqlgraph.NewStep( - sqlgraph.From(user.Table, user.FieldID, id), - sqlgraph.To(agent.Table, agent.FieldID), - sqlgraph.Edge(sqlgraph.O2M, false, user.CreatedAgentsTable, user.CreatedAgentsColumn), - ) - fromV = sqlgraph.Neighbors(_m.driver.Dialect(), step) - return fromV, nil - } - return query -} - -// QueryOwnedAgents queries the owned_agents edge of a User. -func (c *UserClient) QueryOwnedAgents(_m *User) *AgentQuery { - query := (&AgentClient{config: c.config}).Query() - query.path = func(context.Context) (fromV *sql.Selector, _ error) { - id := _m.ID - step := sqlgraph.NewStep( - sqlgraph.From(user.Table, user.FieldID, id), - sqlgraph.To(agent.Table, agent.FieldID), - sqlgraph.Edge(sqlgraph.O2M, false, user.OwnedAgentsTable, user.OwnedAgentsColumn), - ) - fromV = sqlgraph.Neighbors(_m.driver.Dialect(), step) - return fromV, nil - } - return query -} - // QueryOwnedGroups queries the owned_groups edge of a User. func (c *UserClient) QueryOwnedGroups(_m *User) *GroupQuery { query := (&GroupClient{config: c.config}).Query() @@ -1571,14 +4770,157 @@ func (c *UserClient) mutate(ctx context.Context, m *UserMutation) (Value, error) } } +// UserAccessTokenClient is a client for the UserAccessToken schema. +type UserAccessTokenClient struct { + config +} + +// NewUserAccessTokenClient returns a client for the UserAccessToken from the given config. +func NewUserAccessTokenClient(c config) *UserAccessTokenClient { + return &UserAccessTokenClient{config: c} +} + +// Use adds a list of mutation hooks to the hooks stack. +// A call to `Use(f, g, h)` equals to `useraccesstoken.Hooks(f(g(h())))`. +func (c *UserAccessTokenClient) Use(hooks ...Hook) { + c.hooks.UserAccessToken = append(c.hooks.UserAccessToken, hooks...) +} + +// Intercept adds a list of query interceptors to the interceptors stack. +// A call to `Intercept(f, g, h)` equals to `useraccesstoken.Intercept(f(g(h())))`. +func (c *UserAccessTokenClient) Intercept(interceptors ...Interceptor) { + c.inters.UserAccessToken = append(c.inters.UserAccessToken, interceptors...) +} + +// Create returns a builder for creating a UserAccessToken entity. +func (c *UserAccessTokenClient) Create() *UserAccessTokenCreate { + mutation := newUserAccessTokenMutation(c.config, OpCreate) + return &UserAccessTokenCreate{config: c.config, hooks: c.Hooks(), mutation: mutation} +} + +// CreateBulk returns a builder for creating a bulk of UserAccessToken entities. +func (c *UserAccessTokenClient) CreateBulk(builders ...*UserAccessTokenCreate) *UserAccessTokenCreateBulk { + return &UserAccessTokenCreateBulk{config: c.config, builders: builders} +} + +// MapCreateBulk creates a bulk creation builder from the given slice. For each item in the slice, the function creates +// a builder and applies setFunc on it. +func (c *UserAccessTokenClient) MapCreateBulk(slice any, setFunc func(*UserAccessTokenCreate, int)) *UserAccessTokenCreateBulk { + rv := reflect.ValueOf(slice) + if rv.Kind() != reflect.Slice { + return &UserAccessTokenCreateBulk{err: fmt.Errorf("calling to UserAccessTokenClient.MapCreateBulk with wrong type %T, need slice", slice)} + } + builders := make([]*UserAccessTokenCreate, rv.Len()) + for i := 0; i < rv.Len(); i++ { + builders[i] = c.Create() + setFunc(builders[i], i) + } + return &UserAccessTokenCreateBulk{config: c.config, builders: builders} +} + +// Update returns an update builder for UserAccessToken. +func (c *UserAccessTokenClient) Update() *UserAccessTokenUpdate { + mutation := newUserAccessTokenMutation(c.config, OpUpdate) + return &UserAccessTokenUpdate{config: c.config, hooks: c.Hooks(), mutation: mutation} +} + +// UpdateOne returns an update builder for the given entity. +func (c *UserAccessTokenClient) UpdateOne(_m *UserAccessToken) *UserAccessTokenUpdateOne { + mutation := newUserAccessTokenMutation(c.config, OpUpdateOne, withUserAccessToken(_m)) + return &UserAccessTokenUpdateOne{config: c.config, hooks: c.Hooks(), mutation: mutation} +} + +// UpdateOneID returns an update builder for the given id. +func (c *UserAccessTokenClient) UpdateOneID(id uuid.UUID) *UserAccessTokenUpdateOne { + mutation := newUserAccessTokenMutation(c.config, OpUpdateOne, withUserAccessTokenID(id)) + return &UserAccessTokenUpdateOne{config: c.config, hooks: c.Hooks(), mutation: mutation} +} + +// Delete returns a delete builder for UserAccessToken. +func (c *UserAccessTokenClient) Delete() *UserAccessTokenDelete { + mutation := newUserAccessTokenMutation(c.config, OpDelete) + return &UserAccessTokenDelete{config: c.config, hooks: c.Hooks(), mutation: mutation} +} + +// DeleteOne returns a builder for deleting the given entity. +func (c *UserAccessTokenClient) DeleteOne(_m *UserAccessToken) *UserAccessTokenDeleteOne { + return c.DeleteOneID(_m.ID) +} + +// DeleteOneID returns a builder for deleting the given entity by its id. +func (c *UserAccessTokenClient) DeleteOneID(id uuid.UUID) *UserAccessTokenDeleteOne { + builder := c.Delete().Where(useraccesstoken.ID(id)) + builder.mutation.id = &id + builder.mutation.op = OpDeleteOne + return &UserAccessTokenDeleteOne{builder} +} + +// Query returns a query builder for UserAccessToken. +func (c *UserAccessTokenClient) Query() *UserAccessTokenQuery { + return &UserAccessTokenQuery{ + config: c.config, + ctx: &QueryContext{Type: TypeUserAccessToken}, + inters: c.Interceptors(), + } +} + +// Get returns a UserAccessToken entity by its id. +func (c *UserAccessTokenClient) Get(ctx context.Context, id uuid.UUID) (*UserAccessToken, error) { + return c.Query().Where(useraccesstoken.ID(id)).Only(ctx) +} + +// GetX is like Get, but panics if an error occurs. +func (c *UserAccessTokenClient) GetX(ctx context.Context, id uuid.UUID) *UserAccessToken { + obj, err := c.Get(ctx, id) + if err != nil { + panic(err) + } + return obj +} + +// Hooks returns the client hooks. +func (c *UserAccessTokenClient) Hooks() []Hook { + return c.hooks.UserAccessToken +} + +// Interceptors returns the client interceptors. +func (c *UserAccessTokenClient) Interceptors() []Interceptor { + return c.inters.UserAccessToken +} + +func (c *UserAccessTokenClient) mutate(ctx context.Context, m *UserAccessTokenMutation) (Value, error) { + switch m.Op() { + case OpCreate: + return (&UserAccessTokenCreate{config: c.config, hooks: c.Hooks(), mutation: m}).Save(ctx) + case OpUpdate: + return (&UserAccessTokenUpdate{config: c.config, hooks: c.Hooks(), mutation: m}).Save(ctx) + case OpUpdateOne: + return (&UserAccessTokenUpdateOne{config: c.config, hooks: c.Hooks(), mutation: m}).Save(ctx) + case OpDelete, OpDeleteOne: + return (&UserAccessTokenDelete{config: c.config, hooks: c.Hooks(), mutation: m}).Exec(ctx) + default: + return nil, fmt.Errorf("ent: unknown UserAccessToken mutation op: %q", m.Op()) + } +} + // hooks and interceptors per client, for fast access. type ( hooks struct { - AccessPolicy, Agent, Group, GroupMembership, PolicyBinding, Project, - User []ent.Hook + AccessPolicy, Agent, AllowListEntry, ApiKey, BrokerDispatch, BrokerJoinToken, + BrokerSecret, EnvVar, GCPServiceAccount, GithubInstallation, Group, + GroupMembership, HarnessConfig, InviteCode, MaintenanceOperation, + MaintenanceOperationRun, Message, Notification, NotificationSubscription, + PolicyBinding, Project, ProjectContributor, ProjectSyncState, RuntimeBroker, + Schedule, ScheduledEvent, Secret, SubscriptionTemplate, Template, User, + UserAccessToken []ent.Hook } inters struct { - AccessPolicy, Agent, Group, GroupMembership, PolicyBinding, Project, - User []ent.Interceptor + AccessPolicy, Agent, AllowListEntry, ApiKey, BrokerDispatch, BrokerJoinToken, + BrokerSecret, EnvVar, GCPServiceAccount, GithubInstallation, Group, + GroupMembership, HarnessConfig, InviteCode, MaintenanceOperation, + MaintenanceOperationRun, Message, Notification, NotificationSubscription, + PolicyBinding, Project, ProjectContributor, ProjectSyncState, RuntimeBroker, + Schedule, ScheduledEvent, Secret, SubscriptionTemplate, Template, User, + UserAccessToken []ent.Interceptor } ) diff --git a/pkg/ent/client_driver.go b/pkg/ent/client_driver.go new file mode 100644 index 000000000..602cc1b53 --- /dev/null +++ b/pkg/ent/client_driver.go @@ -0,0 +1,29 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package ent + +import "entgo.io/ent/dialect" + +// Driver returns the underlying dialect.Driver backing the client. +// +// This is a hand-written extension (not part of the generated code) that gives +// adapters access to the driver so they can (a) branch on the active SQL +// dialect and (b) execute dialect-specific raw statements that the generated +// query builders cannot express — notably `SELECT ... FOR UPDATE SKIP LOCKED` +// used by the job-claim paths under Postgres. See +// pkg/store/entadapter/schedule_store.go. +func (c *Client) Driver() dialect.Driver { + return c.driver +} diff --git a/pkg/ent/ent.go b/pkg/ent/ent.go index 62ea96d21..3bae0f4c2 100644 --- a/pkg/ent/ent.go +++ b/pkg/ent/ent.go @@ -14,11 +14,35 @@ import ( "entgo.io/ent/dialect/sql/sqlgraph" "github.com/GoogleCloudPlatform/scion/pkg/ent/accesspolicy" "github.com/GoogleCloudPlatform/scion/pkg/ent/agent" + "github.com/GoogleCloudPlatform/scion/pkg/ent/allowlistentry" + "github.com/GoogleCloudPlatform/scion/pkg/ent/apikey" + "github.com/GoogleCloudPlatform/scion/pkg/ent/brokerdispatch" + "github.com/GoogleCloudPlatform/scion/pkg/ent/brokerjointoken" + "github.com/GoogleCloudPlatform/scion/pkg/ent/brokersecret" + "github.com/GoogleCloudPlatform/scion/pkg/ent/envvar" + "github.com/GoogleCloudPlatform/scion/pkg/ent/gcpserviceaccount" + "github.com/GoogleCloudPlatform/scion/pkg/ent/githubinstallation" "github.com/GoogleCloudPlatform/scion/pkg/ent/group" "github.com/GoogleCloudPlatform/scion/pkg/ent/groupmembership" + "github.com/GoogleCloudPlatform/scion/pkg/ent/harnessconfig" + "github.com/GoogleCloudPlatform/scion/pkg/ent/invitecode" + "github.com/GoogleCloudPlatform/scion/pkg/ent/maintenanceoperation" + "github.com/GoogleCloudPlatform/scion/pkg/ent/maintenanceoperationrun" + "github.com/GoogleCloudPlatform/scion/pkg/ent/message" + "github.com/GoogleCloudPlatform/scion/pkg/ent/notification" + "github.com/GoogleCloudPlatform/scion/pkg/ent/notificationsubscription" "github.com/GoogleCloudPlatform/scion/pkg/ent/policybinding" "github.com/GoogleCloudPlatform/scion/pkg/ent/project" + "github.com/GoogleCloudPlatform/scion/pkg/ent/projectcontributor" + "github.com/GoogleCloudPlatform/scion/pkg/ent/projectsyncstate" + "github.com/GoogleCloudPlatform/scion/pkg/ent/runtimebroker" + "github.com/GoogleCloudPlatform/scion/pkg/ent/schedule" + "github.com/GoogleCloudPlatform/scion/pkg/ent/scheduledevent" + "github.com/GoogleCloudPlatform/scion/pkg/ent/secret" + "github.com/GoogleCloudPlatform/scion/pkg/ent/subscriptiontemplate" + "github.com/GoogleCloudPlatform/scion/pkg/ent/template" "github.com/GoogleCloudPlatform/scion/pkg/ent/user" + "github.com/GoogleCloudPlatform/scion/pkg/ent/useraccesstoken" ) // ent aliases to avoid import conflicts in user's code. @@ -79,13 +103,37 @@ var ( func checkColumn(t, c string) error { initCheck.Do(func() { columnCheck = sql.NewColumnCheck(map[string]func(string) bool{ - accesspolicy.Table: accesspolicy.ValidColumn, - agent.Table: agent.ValidColumn, - group.Table: group.ValidColumn, - groupmembership.Table: groupmembership.ValidColumn, - policybinding.Table: policybinding.ValidColumn, - project.Table: project.ValidColumn, - user.Table: user.ValidColumn, + accesspolicy.Table: accesspolicy.ValidColumn, + agent.Table: agent.ValidColumn, + allowlistentry.Table: allowlistentry.ValidColumn, + apikey.Table: apikey.ValidColumn, + brokerdispatch.Table: brokerdispatch.ValidColumn, + brokerjointoken.Table: brokerjointoken.ValidColumn, + brokersecret.Table: brokersecret.ValidColumn, + envvar.Table: envvar.ValidColumn, + gcpserviceaccount.Table: gcpserviceaccount.ValidColumn, + githubinstallation.Table: githubinstallation.ValidColumn, + group.Table: group.ValidColumn, + groupmembership.Table: groupmembership.ValidColumn, + harnessconfig.Table: harnessconfig.ValidColumn, + invitecode.Table: invitecode.ValidColumn, + maintenanceoperation.Table: maintenanceoperation.ValidColumn, + maintenanceoperationrun.Table: maintenanceoperationrun.ValidColumn, + message.Table: message.ValidColumn, + notification.Table: notification.ValidColumn, + notificationsubscription.Table: notificationsubscription.ValidColumn, + policybinding.Table: policybinding.ValidColumn, + project.Table: project.ValidColumn, + projectcontributor.Table: projectcontributor.ValidColumn, + projectsyncstate.Table: projectsyncstate.ValidColumn, + runtimebroker.Table: runtimebroker.ValidColumn, + schedule.Table: schedule.ValidColumn, + scheduledevent.Table: scheduledevent.ValidColumn, + secret.Table: secret.ValidColumn, + subscriptiontemplate.Table: subscriptiontemplate.ValidColumn, + template.Table: template.ValidColumn, + user.Table: user.ValidColumn, + useraccesstoken.Table: useraccesstoken.ValidColumn, }) }) return columnCheck(t, c) diff --git a/pkg/ent/entc/client.go b/pkg/ent/entc/client.go index 9504ea5b7..4b91c07a6 100644 --- a/pkg/ent/entc/client.go +++ b/pkg/ent/entc/client.go @@ -20,18 +20,58 @@ import ( "context" "database/sql" "fmt" + "time" "entgo.io/ent/dialect" entsql "entgo.io/ent/dialect/sql" + "github.com/jackc/pgx/v5" + "github.com/jackc/pgx/v5/stdlib" + "github.com/GoogleCloudPlatform/scion/pkg/ent" "github.com/GoogleCloudPlatform/scion/pkg/ent/migrate" ) +// PoolConfig holds connection pool settings applied to the underlying +// *sql.DB after it is opened. A zero value leaves the corresponding pool +// setting at the database/sql default (i.e. the field is only applied when +// it is greater than zero). +// +// NOTE: for SQLite, MaxOpenConns must be 1 to serialize writes and avoid +// "database is locked" errors; callers are responsible for supplying that. +type PoolConfig struct { + MaxOpenConns int + MaxIdleConns int + ConnMaxLifetime time.Duration + // ConnMaxIdleTime bounds how long a connection may sit idle in the pool + // before being closed. Set it shorter than the server/proxy idle timeout + // (CloudSQL drops idle connections after ~10m) so the pool recycles a + // connection before the remote silently closes it; otherwise the first + // request after an idle period stalls waiting for a dead connection to time + // out. A zero value leaves the database/sql default (no idle limit). + ConnMaxIdleTime time.Duration +} + +// apply sets the pool parameters on db, skipping any unset (non-positive) field. +func (p PoolConfig) apply(db *sql.DB) { + if p.MaxOpenConns > 0 { + db.SetMaxOpenConns(p.MaxOpenConns) + } + if p.MaxIdleConns > 0 { + db.SetMaxIdleConns(p.MaxIdleConns) + } + if p.ConnMaxLifetime > 0 { + db.SetConnMaxLifetime(p.ConnMaxLifetime) + } + if p.ConnMaxIdleTime > 0 { + db.SetConnMaxIdleTime(p.ConnMaxIdleTime) + } +} + // OpenSQLite creates an Ent client backed by SQLite. // The dsn should be a SQLite connection string (e.g. "file:ent?mode=memory&cache=shared"). // Foreign keys and WAL journal mode are enabled automatically. // This uses the modernc.org/sqlite pure-Go driver which registers as "sqlite". -func OpenSQLite(dsn string, opts ...ent.Option) (*ent.Client, error) { +func OpenSQLite(dsn string, pool PoolConfig, opts ...ent.Option) (*ent.Client, error) { db, err := sql.Open("sqlite", dsn) if err != nil { return nil, fmt.Errorf("opening sqlite connection: %w", err) @@ -45,6 +85,39 @@ func OpenSQLite(dsn string, opts ...ent.Option) (*ent.Client, error) { db.Close() return nil, fmt.Errorf("enabling WAL mode: %w", err) } + pool.apply(db) + drv := entsql.OpenDB(dialect.SQLite, db) + client := ent.NewClient(append(opts, ent.Driver(drv))...) + return client, nil +} + +// OpenSQLiteReadOnly creates an Ent client backed by a read-only SQLite +// database. It is used by the migration tool to read from a source SQLite file +// without mutating it: the connection is opened with `PRAGMA query_only = ON` +// so any accidental write fails loudly, and—unlike OpenSQLite—it does NOT +// switch the journal to WAL mode (doing so would write to the database header +// and fail on a query-only connection). +// +// MaxOpenConns is forced to 1 because the query_only and foreign_keys pragmas +// are connection-scoped; with a larger pool, unprimed connections would not +// inherit them. +func OpenSQLiteReadOnly(dsn string, opts ...ent.Option) (*ent.Client, error) { + db, err := sql.Open("sqlite", dsn) + if err != nil { + return nil, fmt.Errorf("opening sqlite connection: %w", err) + } + // Pin to a single connection so the pragmas below apply to every query. + db.SetMaxOpenConns(1) + // Foreign keys on for read consistency; query_only to guarantee the source + // is never modified during migration. + if _, err := db.Exec("PRAGMA foreign_keys = ON"); err != nil { + db.Close() + return nil, fmt.Errorf("enabling foreign keys: %w", err) + } + if _, err := db.Exec("PRAGMA query_only = ON"); err != nil { + db.Close() + return nil, fmt.Errorf("enabling query_only mode: %w", err) + } drv := entsql.OpenDB(dialect.SQLite, db) client := ent.NewClient(append(opts, ent.Driver(drv))...) return client, nil @@ -53,14 +126,49 @@ func OpenSQLite(dsn string, opts ...ent.Option) (*ent.Client, error) { // OpenPostgres creates an Ent client backed by PostgreSQL. // The dsn should be a PostgreSQL connection string // (e.g. "host=localhost port=5432 user=scion dbname=scion sslmode=disable"). -func OpenPostgres(dsn string, opts ...ent.Option) (*ent.Client, error) { - client, err := ent.Open(dialect.Postgres, dsn, opts...) +func OpenPostgres(dsn string, pool PoolConfig, opts ...ent.Option) (*ent.Client, error) { + // Parse the DSN with pgx (accepts both keyword/value DSNs "host=... port=..." + // and URL-style "postgres://..." connection strings) so we can attach TCP + // keepalive settings to the connection before handing it to database/sql via + // stdlib.OpenDB. Keepalives let the OS detect a connection silently dropped by + // a peer (e.g. CloudSQL recycling idle backends or a NAT timeout) instead of + // the first query after idle hanging on a dead socket. + connConfig, err := pgx.ParseConfig(dsn) if err != nil { - return nil, fmt.Errorf("opening postgres connection: %w", err) + return nil, fmt.Errorf("parsing postgres dsn: %w", err) } + applyKeepalives(connConfig.RuntimeParams) + if connConfig.ConnectTimeout == 0 { + connConfig.ConnectTimeout = connectTimeout + } + + db := stdlib.OpenDB(*connConfig) + pool.apply(db) + drv := entsql.OpenDB(dialect.Postgres, db) + client := ent.NewClient(append(opts, ent.Driver(drv))...) return client, nil } +const connectTimeout = 10 * time.Second + +// applyKeepalives sets server-side TCP keepalive GUCs as pgx RuntimeParams so the +// kernel probes idle connections and tears down dead ones promptly. Values mirror +// the pgx event pool (events_postgres.go): probe after 60s idle, every 15s, give +// up after 4 missed probes (~2 min to detect a dead peer). Existing keys are not +// overwritten so an explicit DSN setting wins. +func applyKeepalives(params map[string]string) { + defaults := map[string]string{ + "tcp_keepalives_idle": "60", + "tcp_keepalives_interval": "15", + "tcp_keepalives_count": "4", + } + for k, v := range defaults { + if _, ok := params[k]; !ok { + params[k] = v + } + } +} + // AutoMigrate runs automatic schema migration on the given client. func AutoMigrate(ctx context.Context, client *ent.Client) error { if err := client.Schema.Create(ctx, migrate.WithDropIndex(true), migrate.WithDropColumn(true)); err != nil { diff --git a/pkg/ent/entc/client_test.go b/pkg/ent/entc/client_test.go index 7eaf361ae..5bee8c7eb 100644 --- a/pkg/ent/entc/client_test.go +++ b/pkg/ent/entc/client_test.go @@ -31,7 +31,7 @@ import ( // newTestClient creates an in-memory SQLite Ent client with auto-migration. func newTestClient(t *testing.T) *ent.Client { t.Helper() - client, err := OpenSQLite("file:" + t.Name() + "?mode=memory&cache=shared") + client, err := OpenSQLite("file:"+t.Name()+"?mode=memory&cache=shared", PoolConfig{}) require.NoError(t, err) t.Cleanup(func() { client.Close() }) require.NoError(t, AutoMigrate(context.Background(), client)) @@ -39,7 +39,7 @@ func newTestClient(t *testing.T) *ent.Client { } func TestOpenSQLite(t *testing.T) { - client, err := OpenSQLite("file:TestOpenSQLite?mode=memory&cache=shared") + client, err := OpenSQLite("file:TestOpenSQLite?mode=memory&cache=shared", PoolConfig{}) require.NoError(t, err) defer client.Close() require.NoError(t, AutoMigrate(context.Background(), client)) @@ -326,47 +326,39 @@ func TestGroupProjectEdge(t *testing.T) { require.Len(t, groups, 2) } -func TestAgentOwnerAndCreatorEdges(t *testing.T) { +// TestAgentCreatedByOwnerPrincipalFields verifies that created_by/owner_id are +// plain polymorphic principal references with no foreign key to the users table: +// an agent that spawns a sub-agent records its own (agent) ID there, which has no +// users-table row. A User-typed FK on these columns rejected every such +// agent-created sub-agent with a constraint violation. +func TestAgentCreatedByOwnerPrincipalFields(t *testing.T) { client := newTestClient(t) ctx := context.Background() - creator, err := client.User.Create(). - SetEmail("creator@example.com"). - SetDisplayName("Creator"). - Save(ctx) - require.NoError(t, err) - - owner, err := client.User.Create(). - SetEmail("owner@example.com"). - SetDisplayName("Owner"). - Save(ctx) - require.NoError(t, err) - gv, err := client.Project.Create(). SetName("gv"). SetSlug("gv"). Save(ctx) require.NoError(t, err) + // A principal ID that is NOT a user (e.g. a creating agent). No users row exists. + principalID := uuid.New() + a, err := client.Agent.Create(). SetSlug("owned-agent"). SetName("Owned Agent"). SetProject(gv). - SetCreator(creator). - SetOwner(owner). + SetCreatedBy(principalID). + SetOwnerID(principalID). SetDelegationEnabled(true). Save(ctx) - require.NoError(t, err) + require.NoError(t, err, "non-user principal in created_by/owner_id must not violate a foreign key") assert.True(t, a.DelegationEnabled) - // Verify edges - createdAgents, err := client.User.QueryCreatedAgents(creator).All(ctx) - require.NoError(t, err) - require.Len(t, createdAgents, 1) - assert.Equal(t, a.ID, createdAgents[0].ID) - - ownedAgents, err := client.User.QueryOwnedAgents(owner).All(ctx) + got, err := client.Agent.Get(ctx, a.ID) require.NoError(t, err) - require.Len(t, ownedAgents, 1) - assert.Equal(t, a.ID, ownedAgents[0].ID) + require.NotNil(t, got.CreatedBy) + require.NotNil(t, got.OwnerID) + assert.Equal(t, principalID, *got.CreatedBy) + assert.Equal(t, principalID, *got.OwnerID) } diff --git a/pkg/ent/entc/driver_postgres.go b/pkg/ent/entc/driver_postgres.go index 87b239e62..319ce12c2 100644 --- a/pkg/ent/entc/driver_postgres.go +++ b/pkg/ent/entc/driver_postgres.go @@ -14,4 +14,4 @@ package entc -import _ "github.com/lib/pq" // PostgreSQL driver +import _ "github.com/jackc/pgx/v5/stdlib" // PostgreSQL driver (pgx stdlib, registers as "pgx") diff --git a/pkg/ent/entc/migrate_alpha.go b/pkg/ent/entc/migrate_alpha.go new file mode 100644 index 000000000..39ceb1e2b --- /dev/null +++ b/pkg/ent/entc/migrate_alpha.go @@ -0,0 +1,728 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//go:build !no_sqlite + +// Package entc — migration alpha (α). +// +// Migration α upgrades a legacy raw-SQL Hub database (the ~53-migration, +// 30-table schema produced by the now-removed pkg/store/sqlite store) to the +// consolidated Ent-backed SQLite schema. It runs in-process on first boot when +// the hub detects a legacy schema, behind an automatic backup. +// +// Strategy (validated against four real-world production hub.db files): +// +// 1. Detect the legacy schema by the presence of the `schema_migrations` +// bookkeeping table plus the legacy-only `agents.agent_id` column. +// 2. Back up the original file (checkpoint WAL, then copy to +// hub.db.bak.). +// 3. AutoMigrate a fresh Ent schema into a temporary file. +// 4. ATTACH the legacy file and copy every table with `INSERT … SELECT`, +// applying the mechanical schema differences (column renames such as +// created_at→created / agent_id→slug, the policies→access_policies and +// group_members→group_memberships table renames, surrogate-id synthesis for +// the formerly composite-keyed tables, and the polymorphic +// member_type/principal_type split). SQLite's dynamic typing carries +// bool-as-int, JSON-as-TEXT and timestamp text across unchanged, and Ent +// reads them back natively (verified end-to-end). +// 5. Verify per-table row counts match. +// 6. Atomically swap the migrated file into place. +// +// Foreign keys are disabled on the loader connection so the copy is insensitive +// to insertion order and to any dangling references the legacy data already +// contained; the live store re-enables them for all subsequent writes. +package entc + +import ( + "context" + "database/sql" + "fmt" + "io" + "os" + "strings" + "time" + + entschema "entgo.io/ent/dialect/sql/schema" + "entgo.io/ent/schema/field" + "github.com/GoogleCloudPlatform/scion/pkg/ent/migrate" + "github.com/google/uuid" + _ "modernc.org/sqlite" // pure-Go SQLite driver +) + +// migrationNamespace is the fixed UUIDv5 namespace used to deterministically +// rewrite legacy non-UUID primary keys (e.g. the internal "hub-…-signing_key" +// secret ids and "plugin-broker-…" runtime-broker ids) into the UUIDs the Ent +// schema requires. Deterministic so that a key and every foreign-key reference +// to it map to the same UUID, and so re-deriving the value is stable. +var migrationNamespace = uuid.MustParse("5c104390-a1d0-5e9a-9b1e-5c104390a1d0") + +// remapSource is a legacy table whose primary key may hold a non-UUID string. +type remapSource struct { + table string + pk string +} + +// remapSources are the legacy tables observed (and known possible) to carry +// non-UUID primary keys. Their keys are rewritten to deterministic UUIDs. +var remapSources = []remapSource{ + {table: "secrets", pk: "id"}, + {table: "runtime_brokers", pk: "id"}, +} + +// remapRefColumns maps an Ent table to the columns that reference a remappable +// id and must be rewritten with the same mapping. Includes the primary keys of +// the remapped entities themselves plus every foreign key that points at a +// runtime broker (whether typed UUID or TEXT in the Ent schema). +var remapRefColumns = map[string][]string{ + "secrets": {"id"}, + "runtime_brokers": {"id"}, + "agents": {"runtime_broker_id"}, + "broker_secrets": {"broker_id"}, + "broker_join_tokens": {"broker_id"}, + "project_contributors": {"broker_id"}, + "project_sync_state": {"broker_id"}, + "projects": {"default_runtime_broker_id"}, +} + +// uuidGenExpr is a SQLite expression that mints a syntactically valid v4-style +// UUID string (8-4-4-4-12 hex). Used to synthesize primary keys for legacy +// tables that had no `id` column (composite-keyed project_contributors / +// project_sync_state, and the restructured group_memberships / policy_bindings). +const uuidGenExpr = `lower(hex(randomblob(4))||'-'||hex(randomblob(2))||'-'||hex(randomblob(2))||'-'||hex(randomblob(2))||'-'||hex(randomblob(6)))` + +// nowExpr is a SQLite expression yielding the current UTC time in the RFC3339 +// form Ent stores. Used for required timestamp columns the legacy schema lacked +// (e.g. policy_bindings.created). +const nowExpr = `strftime('%Y-%m-%dT%H:%M:%fZ','now')` + +// AlphaTableResult records the outcome of migrating one legacy table. +type AlphaTableResult struct { + EntTable string + LegacyTable string + Source int // rows in the legacy table + Dest int // rows in the destination table after the copy +} + +// AlphaReport is the aggregate outcome of a migration α run. +type AlphaReport struct { + BackupPath string + SourcePath string + Tables []AlphaTableResult + ChildEdges int // group_child_groups edges copied + Skipped bool // true when the source was not a legacy schema (no-op) + SkipReason string // populated when Skipped is true +} + +// TotalRows returns the total number of destination rows written across all +// tables (excluding M2M child-group edges). +func (r *AlphaReport) TotalRows() int { + n := 0 + for _, t := range r.Tables { + n += t.Dest + } + return n +} + +// AlphaOptions tunes a migration α run. +type AlphaOptions struct { + // Logf, if non-nil, receives one human-readable progress line per step. + Logf func(format string, args ...any) + // BackupSuffix overrides the timestamp suffix appended to the backup file + // name. Primarily for deterministic tests; defaults to time.Now(). + BackupSuffix string +} + +func (o AlphaOptions) logf(format string, args ...any) { + if o.Logf != nil { + o.Logf(format, args...) + } +} + +// IsLegacyRawSQLSchema reports whether the SQLite file at path holds a legacy +// raw-SQL Hub schema (as opposed to the consolidated Ent schema, an empty file, +// or a non-existent file). Detection is conservative: it requires both the +// `schema_migrations` bookkeeping table — which the Ent store never creates — +// and the legacy-only `agents.agent_id` column. +func IsLegacyRawSQLSchema(path string) (bool, error) { + if _, err := os.Stat(path); err != nil { + if os.IsNotExist(err) { + return false, nil + } + return false, err + } + db, err := sql.Open("sqlite", "file:"+path+"?mode=ro") + if err != nil { + return false, fmt.Errorf("opening %s: %w", path, err) + } + defer db.Close() + + hasMigrations, err := tableExists(db, "schema_migrations") + if err != nil { + return false, err + } + if !hasMigrations { + return false, nil + } + hasAgents, err := tableExists(db, "agents") + if err != nil { + return false, err + } + if !hasAgents { + return false, nil + } + cols, err := tableColumns(db, "agents") + if err != nil { + return false, err + } + return cols["agent_id"], nil +} + +// MigrateAlphaSQLite upgrades the legacy raw-SQL Hub database at path in place to +// the consolidated Ent schema. It is a no-op (Skipped=true) when the file is not +// a legacy schema, which makes re-running on an already-migrated database safe. +// +// On success the original file has been replaced by the Ent-schema database and a +// backup of the original remains at .bak.. On any failure the +// original file is left untouched and the temporary working file is removed. +func MigrateAlphaSQLite(ctx context.Context, path string, opts AlphaOptions) (*AlphaReport, error) { + report := &AlphaReport{SourcePath: path} + + legacy, err := IsLegacyRawSQLSchema(path) + if err != nil { + return nil, fmt.Errorf("detecting schema: %w", err) + } + if !legacy { + report.Skipped = true + report.SkipReason = "not a legacy raw-SQL schema (already Ent, empty, or absent)" + opts.logf("migration α: %s", report.SkipReason) + return report, nil + } + + // 1) Back up the original (fold WAL in first so the copy is complete). + backupPath, err := backupLegacy(path, opts) + if err != nil { + return nil, fmt.Errorf("backing up legacy database: %w", err) + } + report.BackupPath = backupPath + opts.logf("migration α: backed up %s -> %s", path, backupPath) + + // 2) Build the Ent schema into a fresh temporary file. + tmpPath := path + ".migrating" + removeSQLiteFiles(tmpPath) + defer removeSQLiteFiles(tmpPath) // cleaned up unless we successfully swap it in + + if err := buildEntSchema(ctx, tmpPath); err != nil { + return nil, fmt.Errorf("creating Ent schema: %w", err) + } + opts.logf("migration α: created Ent schema in %s", tmpPath) + + // 3) Copy all data from the legacy file into the new Ent file. + if err := copyLegacyData(ctx, tmpPath, path, report, opts); err != nil { + return nil, fmt.Errorf("copying data: %w", err) + } + + // 4) Atomically replace the original with the migrated file. The original is + // already safely backed up. + removeSQLiteFiles(path) + if err := os.Rename(tmpPath, path); err != nil { + return nil, fmt.Errorf("swapping migrated database into place: %w", err) + } + opts.logf("migration α: complete — %d tables, %d rows migrated", len(report.Tables), report.TotalRows()) + return report, nil +} + +// backupLegacy checkpoints the legacy WAL and copies the database to a +// timestamped backup file, returning the backup path. +func backupLegacy(path string, opts AlphaOptions) (string, error) { + // Fold any WAL frames into the main file so a plain copy is complete. + db, err := sql.Open("sqlite", "file:"+path) + if err != nil { + return "", err + } + if _, err := db.Exec("PRAGMA wal_checkpoint(TRUNCATE)"); err != nil { + // Non-fatal: a DB in rollback-journal mode has no WAL to checkpoint. + opts.logf("migration α: wal_checkpoint skipped: %v", err) + } + db.Close() + + suffix := opts.BackupSuffix + if suffix == "" { + suffix = time.Now().UTC().Format("20060102-150405") + } + backupPath := path + ".bak." + suffix + if err := copyFile(path, backupPath); err != nil { + return "", err + } + return backupPath, nil +} + +// buildEntSchema creates the destination Ent schema in a new file at tmpPath. +func buildEntSchema(ctx context.Context, tmpPath string) error { + client, err := OpenSQLite(tmpPath, PoolConfig{}) + if err != nil { + return err + } + defer client.Close() + return AutoMigrate(ctx, client) +} + +// tableMap describes how one Ent table is populated from a legacy table via the +// generic INSERT…SELECT path. The three structurally-restructured tables +// (group_memberships, policy_bindings) are handled by bespoke SQL instead. +type tableMap struct { + entTable string + legacyTable string + // overrides maps an Ent column name to a raw SQLite select expression, + // bypassing the automatic same-name / created_at→created mapping. Used for + // the agents agent_id→slug rename and for synthesizing surrogate ids. + overrides map[string]string +} + +// genericTables lists every table copied by the column-name-driven engine, in +// parent-before-child order (cosmetic only — foreign keys are off during load). +var genericTables = []tableMap{ + {entTable: "users", legacyTable: "users"}, + {entTable: "projects", legacyTable: "projects"}, + {entTable: "runtime_brokers", legacyTable: "runtime_brokers"}, + {entTable: "agents", legacyTable: "agents", overrides: map[string]string{"slug": `"agent_id"`}}, + {entTable: "groups", legacyTable: "groups"}, + {entTable: "access_policies", legacyTable: "policies"}, + {entTable: "templates", legacyTable: "templates"}, + {entTable: "harness_configs", legacyTable: "harness_configs"}, + {entTable: "secrets", legacyTable: "secrets"}, + {entTable: "env_vars", legacyTable: "env_vars"}, + {entTable: "project_contributors", legacyTable: "project_contributors", overrides: map[string]string{"id": uuidGenExpr}}, + {entTable: "project_sync_state", legacyTable: "project_sync_state", overrides: map[string]string{"id": uuidGenExpr}}, + {entTable: "broker_secrets", legacyTable: "broker_secrets"}, + {entTable: "broker_join_tokens", legacyTable: "broker_join_tokens"}, + {entTable: "notification_subscriptions", legacyTable: "notification_subscriptions"}, + {entTable: "notifications", legacyTable: "notifications"}, + {entTable: "subscription_templates", legacyTable: "subscription_templates"}, + {entTable: "scheduled_events", legacyTable: "scheduled_events"}, + {entTable: "schedules", legacyTable: "schedules"}, + {entTable: "messages", legacyTable: "messages"}, + {entTable: "gcp_service_accounts", legacyTable: "gcp_service_accounts"}, + {entTable: "github_installations", legacyTable: "github_installations"}, + {entTable: "maintenance_operations", legacyTable: "maintenance_operations"}, + {entTable: "maintenance_operation_runs", legacyTable: "maintenance_operation_runs"}, + {entTable: "api_keys", legacyTable: "api_keys"}, + {entTable: "user_access_tokens", legacyTable: "user_access_tokens"}, + {entTable: "allow_list", legacyTable: "allow_list"}, + {entTable: "invite_codes", legacyTable: "invite_codes"}, +} + +// copyLegacyData opens the new Ent database, attaches the legacy database, and +// copies every table with foreign keys disabled. +func copyLegacyData(ctx context.Context, dstPath, legacyPath string, report *AlphaReport, opts AlphaOptions) error { + db, err := sql.Open("sqlite", "file:"+dstPath) + if err != nil { + return err + } + defer db.Close() + // Pin to one connection so PRAGMAs and the ATTACH apply to every statement. + db.SetMaxOpenConns(1) + + if _, err := db.ExecContext(ctx, "PRAGMA foreign_keys = OFF"); err != nil { + return fmt.Errorf("disabling foreign keys: %w", err) + } + if _, err := db.ExecContext(ctx, `ATTACH DATABASE ? AS legacy`, "file:"+legacyPath+"?mode=ro"); err != nil { + return fmt.Errorf("attaching legacy database: %w", err) + } + defer db.ExecContext(ctx, "DETACH DATABASE legacy") //nolint:errcheck + + // Build the id-remap table (legacy non-UUID primary keys -> deterministic + // UUIDs) before copying, so every reference resolves consistently. The table + // is TEMP, so it never lands in the migrated file. + if err := buildIDRemap(ctx, db, opts); err != nil { + return fmt.Errorf("building id remap: %w", err) + } + + entCols := entColumnsByTable() + + for _, tm := range genericTables { + cols, ok := entCols[tm.entTable] + if !ok { + return fmt.Errorf("unknown Ent table %q (generated schema drift?)", tm.entTable) + } + res, err := copyGenericTable(ctx, db, tm, cols) + if err != nil { + return fmt.Errorf("copying %s: %w", tm.entTable, err) + } + report.Tables = append(report.Tables, res) + opts.logf("migration α: %-26s source=%d dest=%d", tm.entTable, res.Source, res.Dest) + } + + // Restructured tables: polymorphic membership and policy-binding splits. + memberships, err := copyGroupMemberships(ctx, db) + if err != nil { + return fmt.Errorf("copying group_memberships: %w", err) + } + report.Tables = append(report.Tables, memberships) + opts.logf("migration α: %-26s source=%d dest=%d", "group_memberships", memberships.Source, memberships.Dest) + + bindings, err := copyPolicyBindings(ctx, db) + if err != nil { + return fmt.Errorf("copying policy_bindings: %w", err) + } + report.Tables = append(report.Tables, bindings) + opts.logf("migration α: %-26s source=%d dest=%d", "policy_bindings", bindings.Source, bindings.Dest) + + // The Group.child_groups M2M edge, derived from legacy groups.parent_id. + edges, err := copyGroupChildEdgesSQL(ctx, db) + if err != nil { + return fmt.Errorf("copying group child edges: %w", err) + } + report.ChildEdges = edges + + return nil +} + +// copyGenericTable performs the column-name-driven INSERT…SELECT for one table +// and verifies the row counts match. +func copyGenericTable(ctx context.Context, db *sql.DB, tm tableMap, entCols []*entschema.Column) (AlphaTableResult, error) { + res := AlphaTableResult{EntTable: tm.entTable, LegacyTable: tm.legacyTable} + + legacyCols, err := attachedTableColumns(ctx, db, tm.legacyTable) + if err != nil { + return res, err + } + if len(legacyCols) == 0 { + // The legacy database does not have this table (e.g. an older schema + // version). Nothing to copy. + return res, nil + } + + var destNames, selectExprs []string + for _, c := range entCols { + if ov, ok := tm.overrides[c.Name]; ok { + destNames = append(destNames, quoteIdent(c.Name)) + selectExprs = append(selectExprs, ov) + continue + } + src := legacySourceColumn(c.Name, legacyCols) + if src == "" { + continue // no legacy source; rely on the Ent column default + } + expr := coerceExpr(src, c) + if isRemapColumn(tm.entTable, c.Name) { + expr = remapWrap(expr) + } + destNames = append(destNames, quoteIdent(c.Name)) + selectExprs = append(selectExprs, expr) + } + if len(destNames) == 0 { + return res, fmt.Errorf("no column mapping for %s", tm.entTable) + } + + stmt := fmt.Sprintf("INSERT INTO main.%s (%s) SELECT %s FROM legacy.%s", + quoteIdent(tm.entTable), strings.Join(destNames, ", "), + strings.Join(selectExprs, ", "), quoteIdent(tm.legacyTable)) + if _, err := db.ExecContext(ctx, stmt); err != nil { + return res, fmt.Errorf("insert: %w", err) + } + + res.Source, err = countRows(ctx, db, "legacy."+quoteIdent(tm.legacyTable)) + if err != nil { + return res, err + } + res.Dest, err = countRows(ctx, db, "main."+quoteIdent(tm.entTable)) + if err != nil { + return res, err + } + if res.Source != res.Dest { + return res, fmt.Errorf("row count mismatch: legacy=%d dest=%d", res.Source, res.Dest) + } + return res, nil +} + +// legacySourceColumn resolves the legacy column that feeds an Ent column, +// applying the systematic created_at→created / updated_at→updated renames. +// Returns "" when the legacy table has no corresponding column. +func legacySourceColumn(entCol string, legacyCols map[string]bool) string { + if legacyCols[entCol] { + return entCol + } + switch entCol { + case "created": + if legacyCols["created_at"] { + return "created_at" + } + case "updated": + if legacyCols["updated_at"] { + return "updated_at" + } + } + return "" +} + +// coerceExpr wraps a legacy column reference with any conversion needed for the +// destination Ent column type. SQLite's dynamic typing handles bool-as-int and +// timestamp text transparently, so only two cases need care: +// - nullable JSON columns: an empty string is not valid JSON, so map ”→NULL. +// - nullable UUID columns: legacy free-text values (or ”) that are not a +// 36-char UUID are mapped to NULL so Ent's uuid.Scan never fails on read. +func coerceExpr(legacyCol string, c *entschema.Column) string { + ref := quoteIdent(legacyCol) + switch { + case c.Type == field.TypeJSON && c.Nullable: + return "NULLIF(" + ref + ", '')" + case c.Type == field.TypeUUID && c.Nullable: + return fmt.Sprintf("CASE WHEN %s LIKE '________-____-____-____-____________' THEN %s ELSE NULL END", ref, ref) + default: + return ref + } +} + +// isRemapColumn reports whether the given Ent table/column holds a reference to +// a remappable (formerly non-UUID) id and must be rewritten via _id_remap. +func isRemapColumn(entTable, entCol string) bool { + for _, c := range remapRefColumns[entTable] { + if c == entCol { + return true + } + } + return false +} + +// remapWrap rewrites a column reference through the _id_remap table: a legacy id +// present in the map yields its deterministic UUID; anything else (already a +// UUID, or NULL) passes through unchanged. +func remapWrap(expr string) string { + return "COALESCE((SELECT new FROM _id_remap WHERE old = " + expr + "), " + expr + ")" +} + +// buildIDRemap creates and populates the TEMP _id_remap table mapping each +// legacy non-UUID primary key to a deterministic UUID. Already-UUID keys are not +// added (they pass through remapWrap unchanged). +func buildIDRemap(ctx context.Context, db *sql.DB, opts AlphaOptions) error { + if _, err := db.ExecContext(ctx, `CREATE TEMP TABLE _id_remap (old TEXT PRIMARY KEY, new TEXT NOT NULL)`); err != nil { + return err + } + total := 0 + for _, rs := range remapSources { + rows, err := db.QueryContext(ctx, fmt.Sprintf("SELECT DISTINCT %s FROM legacy.%s", quoteIdent(rs.pk), quoteIdent(rs.table))) + if err != nil { + return err + } + var legacyIDs []string + for rows.Next() { + var id sql.NullString + if err := rows.Scan(&id); err != nil { + rows.Close() + return err + } + if !id.Valid || id.String == "" { + continue + } + if _, err := uuid.Parse(id.String); err == nil { + continue // already a valid UUID + } + legacyIDs = append(legacyIDs, id.String) + } + if err := rows.Err(); err != nil { + rows.Close() + return err + } + rows.Close() + for _, old := range legacyIDs { + newID := uuid.NewSHA1(migrationNamespace, []byte(old)).String() + if _, err := db.ExecContext(ctx, `INSERT OR IGNORE INTO _id_remap (old, new) VALUES (?, ?)`, old, newID); err != nil { + return err + } + total++ + opts.logf("migration α: remap id %-40s -> %s (%s)", old, newID, rs.table) + } + } + if total > 0 { + opts.logf("migration α: rewrote %d non-UUID id(s) to deterministic UUIDs", total) + } + return nil +} + +// copyGroupMemberships migrates the legacy `group_members` table (composite key +// group_id/member_type/member_id) into Ent `group_memberships`, splitting the +// polymorphic member into user_id / agent_id and minting a surrogate id. +func copyGroupMemberships(ctx context.Context, db *sql.DB) (AlphaTableResult, error) { + res := AlphaTableResult{EntTable: "group_memberships", LegacyTable: "group_members"} + if ok, err := attachedTableExists(ctx, db, "group_members"); err != nil || !ok { + return res, err + } + stmt := fmt.Sprintf(`INSERT INTO main.group_memberships (id, group_id, user_id, agent_id, role, added_at, added_by) + SELECT %s, group_id, + CASE WHEN member_type='user' THEN member_id END, + CASE WHEN member_type='agent' THEN member_id END, + role, added_at, added_by + FROM legacy.group_members`, uuidGenExpr) + if _, err := db.ExecContext(ctx, stmt); err != nil { + return res, err + } + return countSourceDest(ctx, db, res, "legacy.group_members", "main.group_memberships") +} + +// copyPolicyBindings migrates the legacy composite-keyed `policy_bindings` into +// the Ent table, splitting the polymorphic principal into user_id / group_id / +// agent_id, minting a surrogate id, and stamping a `created` time the legacy +// schema did not record. +func copyPolicyBindings(ctx context.Context, db *sql.DB) (AlphaTableResult, error) { + res := AlphaTableResult{EntTable: "policy_bindings", LegacyTable: "policy_bindings"} + if ok, err := attachedTableExists(ctx, db, "policy_bindings"); err != nil || !ok { + return res, err + } + stmt := fmt.Sprintf(`INSERT INTO main.policy_bindings (id, policy_id, principal_type, user_id, group_id, agent_id, created) + SELECT %s, policy_id, principal_type, + CASE WHEN principal_type='user' THEN principal_id END, + CASE WHEN principal_type='group' THEN principal_id END, + CASE WHEN principal_type='agent' THEN principal_id END, + %s + FROM legacy.policy_bindings`, uuidGenExpr, nowExpr) + if _, err := db.ExecContext(ctx, stmt); err != nil { + return res, err + } + return countSourceDest(ctx, db, res, "legacy.policy_bindings", "main.policy_bindings") +} + +// copyGroupChildEdgesSQL populates the group_child_groups M2M join table from +// legacy groups that carried a parent_id. Idempotent within a fresh run. +func copyGroupChildEdgesSQL(ctx context.Context, db *sql.DB) (int, error) { + cols, err := attachedTableColumns(ctx, db, "groups") + if err != nil { + return 0, err + } + if !cols["parent_id"] { + // No legacy groups table, or a schema without parent hierarchy. + return 0, nil + } + // Only legacy groups with a non-empty parent_id contribute an edge. + stmt := `INSERT INTO main.group_child_groups (group_id, parent_group_id) + SELECT parent_id, id FROM legacy.groups + WHERE parent_id IS NOT NULL AND parent_id <> ''` + r, err := db.ExecContext(ctx, stmt) + if err != nil { + return 0, err + } + n, _ := r.RowsAffected() + return int(n), nil +} + +// --- small helpers --- + +func countSourceDest(ctx context.Context, db *sql.DB, res AlphaTableResult, srcQ, dstQ string) (AlphaTableResult, error) { + var err error + if res.Source, err = countRows(ctx, db, srcQ); err != nil { + return res, err + } + if res.Dest, err = countRows(ctx, db, dstQ); err != nil { + return res, err + } + if res.Source != res.Dest { + return res, fmt.Errorf("row count mismatch: source=%d dest=%d", res.Source, res.Dest) + } + return res, nil +} + +func countRows(ctx context.Context, db *sql.DB, qualifiedTable string) (int, error) { + var n int + err := db.QueryRowContext(ctx, "SELECT count(*) FROM "+qualifiedTable).Scan(&n) + return n, err +} + +// entColumnsByTable indexes the generated Ent schema columns by table name. +func entColumnsByTable() map[string][]*entschema.Column { + out := make(map[string][]*entschema.Column, len(migrate.Tables)) + for _, t := range migrate.Tables { + out[t.Name] = t.Columns + } + return out +} + +func tableExists(db *sql.DB, name string) (bool, error) { + var n int + err := db.QueryRow(`SELECT count(*) FROM sqlite_master WHERE type='table' AND name=?`, name).Scan(&n) + return n > 0, err +} + +func tableColumns(db *sql.DB, table string) (map[string]bool, error) { + rows, err := db.Query(fmt.Sprintf("PRAGMA table_info(%s)", quoteIdent(table))) + if err != nil { + return nil, err + } + defer rows.Close() + cols := map[string]bool{} + for rows.Next() { + var cid, notnull, pk int + var name, ctype string + var dflt sql.NullString + if err := rows.Scan(&cid, &name, &ctype, ¬null, &dflt, &pk); err != nil { + return nil, err + } + cols[name] = true + } + return cols, rows.Err() +} + +// attachedTableExists reports whether a table exists in the ATTACHed legacy DB. +func attachedTableExists(ctx context.Context, db *sql.DB, name string) (bool, error) { + var n int + err := db.QueryRowContext(ctx, `SELECT count(*) FROM legacy.sqlite_master WHERE type='table' AND name=?`, name).Scan(&n) + return n > 0, err +} + +// attachedTableColumns reads the columns of a table in the ATTACHed legacy DB. +func attachedTableColumns(ctx context.Context, db *sql.DB, table string) (map[string]bool, error) { + rows, err := db.QueryContext(ctx, fmt.Sprintf("PRAGMA legacy.table_info(%s)", quoteIdent(table))) + if err != nil { + return nil, err + } + defer rows.Close() + cols := map[string]bool{} + for rows.Next() { + var cid, notnull, pk int + var name, ctype string + var dflt sql.NullString + if err := rows.Scan(&cid, &name, &ctype, ¬null, &dflt, &pk); err != nil { + return nil, err + } + cols[name] = true + } + return cols, rows.Err() +} + +// quoteIdent double-quotes a SQLite identifier. +func quoteIdent(s string) string { + return `"` + strings.ReplaceAll(s, `"`, `""`) + `"` +} + +func copyFile(src, dst string) error { + in, err := os.Open(src) + if err != nil { + return err + } + defer in.Close() + out, err := os.OpenFile(dst, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0o600) + if err != nil { + return err + } + if _, err := io.Copy(out, in); err != nil { + out.Close() + return err + } + return out.Close() +} + +// removeSQLiteFiles removes a SQLite database file and its WAL/SHM sidecars. +func removeSQLiteFiles(path string) { + for _, suffix := range []string{"", "-wal", "-shm"} { + _ = os.Remove(path + suffix) + } +} diff --git a/pkg/ent/entc/migrate_alpha_nosqlite.go b/pkg/ent/entc/migrate_alpha_nosqlite.go new file mode 100644 index 000000000..9c710894d --- /dev/null +++ b/pkg/ent/entc/migrate_alpha_nosqlite.go @@ -0,0 +1,60 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//go:build no_sqlite + +package entc + +import "context" + +// AlphaTableResult records the outcome of migrating one legacy table. +type AlphaTableResult struct { + EntTable string + LegacyTable string + Source int + Dest int +} + +// AlphaReport is the aggregate outcome of a migration α run. +type AlphaReport struct { + BackupPath string + SourcePath string + Tables []AlphaTableResult + ChildEdges int + Skipped bool + SkipReason string +} + +// TotalRows returns the total number of destination rows written. +func (r *AlphaReport) TotalRows() int { + n := 0 + for _, t := range r.Tables { + n += t.Dest + } + return n +} + +// AlphaOptions tunes a migration α run. +type AlphaOptions struct { + Logf func(format string, args ...any) + BackupSuffix string +} + +// IsLegacyRawSQLSchema always reports false when built without SQLite support. +func IsLegacyRawSQLSchema(_ string) (bool, error) { return false, nil } + +// MigrateAlphaSQLite is a no-op when built without SQLite support. +func MigrateAlphaSQLite(_ context.Context, path string, _ AlphaOptions) (*AlphaReport, error) { + return &AlphaReport{SourcePath: path, Skipped: true, SkipReason: "built without sqlite support"}, nil +} diff --git a/pkg/ent/entc/migrate_alpha_test.go b/pkg/ent/entc/migrate_alpha_test.go new file mode 100644 index 000000000..cd085b2ea --- /dev/null +++ b/pkg/ent/entc/migrate_alpha_test.go @@ -0,0 +1,296 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//go:build !no_sqlite + +package entc + +import ( + "context" + "database/sql" + "os" + "path/filepath" + "testing" + + "github.com/GoogleCloudPlatform/scion/pkg/ent" + "github.com/google/uuid" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +// writeLegacyDB creates a representative legacy raw-SQL hub.db at path. It +// intentionally exercises every mechanical difference migration α handles: +// the schema_migrations sentinel, created_at/updated_at and agent_id renames, +// the policies/group_members table renames, composite-keyed tables, the +// polymorphic member/principal split, bool-as-int, JSON-as-TEXT, BLOBs, a +// dangling agents.created_by reference, and non-UUID primary keys for both +// secrets and runtime_brokers (with a foreign-key reference to the latter). +func writeLegacyDB(t *testing.T, path string) legacyFixture { + t.Helper() + db, err := sql.Open("sqlite", "file:"+path) + require.NoError(t, err) + defer db.Close() + + ddl := []string{ + `CREATE TABLE schema_migrations (version INTEGER PRIMARY KEY, applied_at TIMESTAMP)`, + `CREATE TABLE users (id TEXT PRIMARY KEY, email TEXT, display_name TEXT, role TEXT, status TEXT, preferences TEXT, created_at TIMESTAMP, last_login TIMESTAMP, last_seen TIMESTAMP)`, + `CREATE TABLE projects (id TEXT PRIMARY KEY, name TEXT, slug TEXT, labels TEXT, annotations TEXT, visibility TEXT, created_at TIMESTAMP, updated_at TIMESTAMP, created_by TEXT, owner_id TEXT, default_runtime_broker_id TEXT)`, + `CREATE TABLE runtime_brokers (id TEXT PRIMARY KEY, name TEXT, slug TEXT, type TEXT, mode TEXT, status TEXT, connection_state TEXT, auto_provide INTEGER, capabilities TEXT, created_at TIMESTAMP, updated_at TIMESTAMP)`, + `CREATE TABLE agents (id TEXT PRIMARY KEY, agent_id TEXT, name TEXT, project_id TEXT, labels TEXT, detached INTEGER, web_pty_enabled INTEGER, visibility TEXT, runtime_broker_id TEXT, created_by TEXT, owner_id TEXT, created_at TIMESTAMP, updated_at TIMESTAMP)`, + `CREATE TABLE broker_secrets (broker_id TEXT PRIMARY KEY, secret_key BLOB, algorithm TEXT, status TEXT, created_at TIMESTAMP)`, + `CREATE TABLE secrets (id TEXT PRIMARY KEY, key TEXT, encrypted_value TEXT, scope TEXT, scope_id TEXT, version INTEGER, secret_type TEXT, injection_mode TEXT, allow_progeny INTEGER, created_at TIMESTAMP, updated_at TIMESTAMP)`, + `CREATE TABLE groups (id TEXT PRIMARY KEY, name TEXT, slug TEXT, group_type TEXT, parent_id TEXT, created_at TIMESTAMP, updated_at TIMESTAMP)`, + `CREATE TABLE group_members (group_id TEXT, member_type TEXT, member_id TEXT, role TEXT, added_at TIMESTAMP, added_by TEXT, PRIMARY KEY (group_id, member_type, member_id))`, + `CREATE TABLE policies (id TEXT PRIMARY KEY, name TEXT, scope_type TEXT, resource_type TEXT, effect TEXT, priority INTEGER, created_at TIMESTAMP, updated_at TIMESTAMP)`, + `CREATE TABLE policy_bindings (policy_id TEXT, principal_type TEXT, principal_id TEXT, PRIMARY KEY (policy_id, principal_type, principal_id))`, + `CREATE TABLE allow_list (id TEXT PRIMARY KEY, email TEXT, note TEXT, added_by TEXT, created DATETIME)`, + `CREATE TABLE messages (id TEXT PRIMARY KEY, project_id TEXT, sender TEXT, recipient TEXT, msg TEXT, type TEXT, urgent INTEGER, read INTEGER, created_at TIMESTAMP)`, + } + for _, s := range ddl { + _, err := db.Exec(s) + require.NoError(t, err, s) + } + + const ts = "2026-05-01 12:30:00" + fx := legacyFixture{ + userID: uuid.NewString(), + projectID: uuid.NewString(), + agentID: uuid.NewString(), + groupID: uuid.NewString(), + policyID: uuid.NewString(), + // Non-UUID ids that must be remapped to deterministic UUIDs. + brokerLegacyID: "plugin-broker-telegram", + secretLegacyID: "hub-abc123-agent_signing_key", + // A created_by that references no user row (dangling, must survive). + danglingPrincipal: uuid.NewString(), + } + + exec := func(q string, args ...any) { + _, err := db.Exec(q, args...) + require.NoError(t, err, q) + } + + exec(`INSERT INTO users (id,email,display_name,role,status,preferences,created_at) VALUES (?,?,?,?,?,?,?)`, + fx.userID, "a@example.com", "Alice", "admin", "active", `{"theme":"dark"}`, ts) + exec(`INSERT INTO projects (id,name,slug,labels,annotations,visibility,created_at,updated_at,owner_id,default_runtime_broker_id) VALUES (?,?,?,?,?,?,?,?,?,?)`, + fx.projectID, "Proj", "proj", `{"team":"x"}`, "", "private", ts, ts, fx.userID, fx.brokerLegacyID) + exec(`INSERT INTO runtime_brokers (id,name,slug,type,mode,status,connection_state,auto_provide,capabilities,created_at,updated_at) VALUES (?,?,?,?,?,?,?,?,?,?,?)`, + fx.brokerLegacyID, "telegram", "telegram", "plugin", "connected", "online", "connected", 1, `{"x":true}`, ts, ts) + // Agent with agent_id (->slug), int bools, JSON labels, dangling created_by, + // and a runtime_broker_id pointing at the non-UUID broker. + exec(`INSERT INTO agents (id,agent_id,name,project_id,labels,detached,web_pty_enabled,visibility,runtime_broker_id,created_by,owner_id,created_at,updated_at) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?)`, + fx.agentID, "deploy-bot", "Deploy Bot", fx.projectID, `{"k":"v"}`, 1, 0, "private", fx.brokerLegacyID, fx.danglingPrincipal, fx.danglingPrincipal, ts, ts) + exec(`INSERT INTO broker_secrets (broker_id,secret_key,algorithm,status,created_at) VALUES (?,?,?,?,?)`, + fx.brokerLegacyID, []byte{0x01, 0x02, 0x03, 0xff}, "hmac-sha256", "active", ts) + exec(`INSERT INTO secrets (id,key,encrypted_value,scope,scope_id,version,secret_type,injection_mode,allow_progeny,created_at,updated_at) VALUES (?,?,?,?,?,?,?,?,?,?,?)`, + fx.secretLegacyID, "agent_signing_key", "ZW5j", "hub", "abc123", 1, "internal", "as_needed", 0, ts, ts) + exec(`INSERT INTO groups (id,name,slug,group_type,created_at,updated_at) VALUES (?,?,?,?,?,?)`, + fx.groupID, "G", "g", "custom", ts, ts) + exec(`INSERT INTO group_members (group_id,member_type,member_id,role,added_at,added_by) VALUES (?,?,?,?,?,?)`, + fx.groupID, "user", fx.userID, "member", ts, fx.userID) + exec(`INSERT INTO policies (id,name,scope_type,resource_type,effect,priority,created_at,updated_at) VALUES (?,?,?,?,?,?,?,?)`, + fx.policyID, "P", "hub", "project", "allow", 0, ts, ts) + exec(`INSERT INTO policy_bindings (policy_id,principal_type,principal_id) VALUES (?,?,?)`, + fx.policyID, "user", fx.userID) + exec(`INSERT INTO allow_list (id,email,note,added_by,created) VALUES (?,?,?,?,?)`, + uuid.NewString(), "b@example.com", "", fx.userID, ts) + exec(`INSERT INTO messages (id,project_id,sender,recipient,msg,type,urgent,read,created_at) VALUES (?,?,?,?,?,?,?,?,?)`, + uuid.NewString(), fx.projectID, "alice", "deploy-bot", "hi", "instruction", 0, 1, ts) + // Empty-string JSON / nullable cases on a second project (must not break read-back). + exec(`INSERT INTO projects (id,name,slug,labels,annotations,visibility,created_at,updated_at) VALUES (?,?,?,?,?,?,?,?)`, + uuid.NewString(), "Proj2", "proj2", "", "", "private", ts, ts) + + return fx +} + +type legacyFixture struct { + userID, projectID, agentID, groupID, policyID string + brokerLegacyID, secretLegacyID string + danglingPrincipal string +} + +// openMigrated opens the migrated file through the Ent client. +func openMigrated(t *testing.T, path string) *ent.Client { + t.Helper() + client, err := OpenSQLite("file:"+path+"?cache=shared", PoolConfig{MaxOpenConns: 1}) + require.NoError(t, err) + t.Cleanup(func() { client.Close() }) + return client +} + +func TestIsLegacyRawSQLSchema(t *testing.T) { + dir := t.TempDir() + + // Non-existent file. + ok, err := IsLegacyRawSQLSchema(filepath.Join(dir, "nope.db")) + require.NoError(t, err) + assert.False(t, ok) + + // Legacy file. + legacyPath := filepath.Join(dir, "legacy.db") + writeLegacyDB(t, legacyPath) + ok, err = IsLegacyRawSQLSchema(legacyPath) + require.NoError(t, err) + assert.True(t, ok, "legacy raw-SQL schema should be detected") + + // Fresh Ent file. + entPath := filepath.Join(dir, "ent.db") + require.NoError(t, buildEntSchema(context.Background(), entPath)) + ok, err = IsLegacyRawSQLSchema(entPath) + require.NoError(t, err) + assert.False(t, ok, "Ent schema must not be flagged as legacy") +} + +func TestMigrateAlphaSQLite_EndToEnd(t *testing.T) { + ctx := context.Background() + dir := t.TempDir() + path := filepath.Join(dir, "hub.db") + fx := writeLegacyDB(t, path) + + report, err := MigrateAlphaSQLite(ctx, path, AlphaOptions{BackupSuffix: "unit"}) + require.NoError(t, err) + require.False(t, report.Skipped) + + // Backup exists and is itself the legacy schema. + assert.Equal(t, path+".bak.unit", report.BackupPath) + bakLegacy, err := IsLegacyRawSQLSchema(report.BackupPath) + require.NoError(t, err) + assert.True(t, bakLegacy, "backup should retain the legacy schema") + + // The migrated file is no longer legacy. + nowLegacy, err := IsLegacyRawSQLSchema(path) + require.NoError(t, err) + assert.False(t, nowLegacy) + + // Every table reported equal source/dest counts. + for _, tr := range report.Tables { + assert.Equalf(t, tr.Source, tr.Dest, "row count mismatch for %s", tr.EntTable) + } + + // Read everything back through the Ent client. + client := openMigrated(t, path) + + users, err := client.User.Query().All(ctx) + require.NoError(t, err) + require.Len(t, users, 1) + assert.Equal(t, "a@example.com", users[0].Email) + require.NotNil(t, users[0].Preferences) + assert.Equal(t, "dark", users[0].Preferences.Theme, "typed JSON preferences must deserialize") + + projects, err := client.Project.Query().All(ctx) + require.NoError(t, err) + assert.Len(t, projects, 2) + + // Agent: agent_id -> slug, int bools -> real bools, JSON labels. + agents, err := client.Agent.Query().All(ctx) + require.NoError(t, err) + require.Len(t, agents, 1) + assert.Equal(t, "deploy-bot", agents[0].Slug) + assert.True(t, agents[0].Detached) + assert.False(t, agents[0].WebPtyEnabled) + assert.Equal(t, map[string]string{"k": "v"}, agents[0].Labels) + + // Non-UUID broker id remapped deterministically, and the broker_secret + + // agent + project references resolve to the SAME new id. + wantBroker := uuid.NewSHA1(migrationNamespace, []byte(fx.brokerLegacyID)) + brokers, err := client.RuntimeBroker.Query().All(ctx) + require.NoError(t, err) + require.Len(t, brokers, 1) + assert.Equal(t, wantBroker, brokers[0].ID) + assert.True(t, brokers[0].AutoProvide) + + bs, err := client.BrokerSecret.Query().All(ctx) + require.NoError(t, err) + require.Len(t, bs, 1) + assert.Equal(t, wantBroker, bs[0].ID, "broker_secret PK (broker_id) must follow the remap") + assert.Equal(t, []byte{0x01, 0x02, 0x03, 0xff}, bs[0].SecretKey, "BLOB must survive") + assert.Equal(t, wantBroker.String(), agents[0].RuntimeBrokerID, "agent broker ref must follow the remap") + var brokerProj *ent.Project + for _, p := range projects { + if p.DefaultRuntimeBrokerID != nil && *p.DefaultRuntimeBrokerID != "" { + brokerProj = p + } + } + require.NotNil(t, brokerProj, "expected a project with a default runtime broker") + assert.Equal(t, wantBroker.String(), *brokerProj.DefaultRuntimeBrokerID, "project broker ref must follow the remap") + + // Non-UUID secret id remapped (no inbound FK, just the PK). + wantSecret := uuid.NewSHA1(migrationNamespace, []byte(fx.secretLegacyID)) + secrets, err := client.Secret.Query().All(ctx) + require.NoError(t, err) + require.Len(t, secrets, 1) + assert.Equal(t, wantSecret, secrets[0].ID) + assert.Equal(t, "agent_signing_key", secrets[0].Key) + + // Restructured tables: polymorphic split. + gms, err := client.GroupMembership.Query().All(ctx) + require.NoError(t, err) + require.Len(t, gms, 1) + require.NotNil(t, gms[0].UserID) + assert.Equal(t, fx.userID, gms[0].UserID.String()) + + pbs, err := client.PolicyBinding.Query().All(ctx) + require.NoError(t, err) + require.Len(t, pbs, 1) + require.NotNil(t, pbs[0].UserID) + assert.Equal(t, fx.userID, pbs[0].UserID.String()) + assert.False(t, pbs[0].Created.IsZero(), "policy binding created must be stamped") + + // Dangling created_by survives as a (parseable) UUID even though no user row exists. + require.NotNil(t, agents[0].CreatedBy) + assert.Equal(t, fx.danglingPrincipal, agents[0].CreatedBy.String()) +} + +func TestMigrateAlphaSQLite_Idempotent(t *testing.T) { + ctx := context.Background() + dir := t.TempDir() + path := filepath.Join(dir, "hub.db") + writeLegacyDB(t, path) + + r1, err := MigrateAlphaSQLite(ctx, path, AlphaOptions{BackupSuffix: "one"}) + require.NoError(t, err) + require.False(t, r1.Skipped) + + // Second run is a no-op: already migrated. + r2, err := MigrateAlphaSQLite(ctx, path, AlphaOptions{BackupSuffix: "two"}) + require.NoError(t, err) + assert.True(t, r2.Skipped) + + // No duplicate backup from the skipped run. + _, err = os.Stat(path + ".bak.two") + assert.True(t, os.IsNotExist(err), "skipped run must not create a backup") + + // Data is unchanged: still exactly one agent. + client := openMigrated(t, path) + n, err := client.Agent.Query().Count(ctx) + require.NoError(t, err) + assert.Equal(t, 1, n) +} + +func TestMigrateAlphaSQLite_SkipsNonLegacy(t *testing.T) { + ctx := context.Background() + dir := t.TempDir() + + // A fresh Ent file. + entPath := filepath.Join(dir, "ent.db") + require.NoError(t, buildEntSchema(ctx, entPath)) + r, err := MigrateAlphaSQLite(ctx, entPath, AlphaOptions{}) + require.NoError(t, err) + assert.True(t, r.Skipped) + + // A missing file. + r, err = MigrateAlphaSQLite(ctx, filepath.Join(dir, "absent.db"), AlphaOptions{}) + require.NoError(t, err) + assert.True(t, r.Skipped) +} diff --git a/pkg/ent/entc/migrate_beta.go b/pkg/ent/entc/migrate_beta.go new file mode 100644 index 000000000..6a62017b3 --- /dev/null +++ b/pkg/ent/entc/migrate_beta.go @@ -0,0 +1,402 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package entc + +import ( + "context" + "fmt" + "reflect" + + "github.com/GoogleCloudPlatform/scion/pkg/ent" +) + +// migrationEntities lists every Ent node by its field name on the generated +// *ent.Client, ordered so that an entity always appears after the entities its +// foreign keys reference. Ent models its M2O/O2M relationships as plain FK +// columns on the child node, so copying nodes in this order satisfies every +// constraint at insert time. +// +// The first seven entities have FK edges: +// - Group -> User (owner) +// - Agent -> Project (required), User (creator, owner) +// - GroupMembership -> Group (required), User, Agent +// - PolicyBinding -> AccessPolicy, User, Group, Agent +// +// The remainder declare no Ent edges (no DB-level FK constraints), so their +// relative order is irrelevant; they are listed alphabetically for readability. +var migrationEntities = []string{ + // FK-ordered core. + "User", + "Project", + "AccessPolicy", + "Group", + "Agent", + "GroupMembership", + "PolicyBinding", + // Independent entities (no Ent edges). + "AllowListEntry", + "ApiKey", + "BrokerJoinToken", + "BrokerSecret", + "EnvVar", + "GCPServiceAccount", + "GithubInstallation", + "HarnessConfig", + "InviteCode", + "MaintenanceOperation", + "MaintenanceOperationRun", + "Message", + "Notification", + "NotificationSubscription", + "ProjectContributor", + "ProjectSyncState", + "RuntimeBroker", + "Schedule", + "ScheduledEvent", + "Secret", + "SubscriptionTemplate", + "Template", + "UserAccessToken", +} + +// defaultBatchSize bounds how many rows a single CreateBulk statement inserts. +// Postgres caps a statement at 65535 bind parameters; the widest entity (Agent) +// has ~36 columns, so 500 rows stays comfortably under the limit while keeping +// the number of round-trips low. +const defaultBatchSize = 500 + +// MigrateOptions tunes a MigrateData run. +type MigrateOptions struct { + // BatchSize is the maximum number of rows per CreateBulk statement. + // Defaults to defaultBatchSize when <= 0. + BatchSize int + // Logf, if non-nil, receives one human-readable progress line per entity. + Logf func(format string, args ...any) +} + +// EntityResult records the outcome of migrating a single entity. +type EntityResult struct { + Entity string + Source int // rows present in the source + Inserted int // rows newly written to the destination this run + Skipped int // rows already present in the destination (idempotent skips) + Dest int // rows in the destination after migration +} + +// MigrateReport is the aggregate outcome of a migration run. +type MigrateReport struct { + Entities []EntityResult + ChildGroupEdgs int // group->child_group M2M edges copied +} + +// MigrateData copies all data from src into dst, entity by entity, in foreign +// key dependency order. The destination schema must already exist (call +// AutoMigrate first). +// +// Properties: +// - Idempotent: rows whose primary key already exists in dst are skipped, so +// a partially completed run can be safely restarted. +// - Atomic per entity: each entity's inserts run inside a single transaction. +// - Verified: after each entity the source and destination row counts are +// compared and a mismatch aborts the migration. +// +// MigrateData never writes to src. +func MigrateData(ctx context.Context, src, dst *ent.Client, opts MigrateOptions) (*MigrateReport, error) { + batchSize := opts.BatchSize + if batchSize <= 0 { + batchSize = defaultBatchSize + } + logf := opts.Logf + if logf == nil { + logf = func(string, ...any) {} + } + + report := &MigrateReport{} + srcStruct := reflect.ValueOf(src).Elem() + dstStruct := reflect.ValueOf(dst).Elem() + + for _, name := range migrationEntities { + srcClient := srcStruct.FieldByName(name) + dstClient := dstStruct.FieldByName(name) + if !srcClient.IsValid() || !dstClient.IsValid() { + return report, fmt.Errorf("entity %q not found on ent.Client (generated code drift?)", name) + } + + res, err := migrateEntity(ctx, name, srcClient, dst, dstClient, batchSize) + if err != nil { + return report, fmt.Errorf("migrating %s: %w", name, err) + } + report.Entities = append(report.Entities, res) + logf("migrated %-26s source=%d inserted=%d skipped=%d dest=%d", + res.Entity, res.Source, res.Inserted, res.Skipped, res.Dest) + } + + // Copy the one many-to-many edge (Group.child_groups) that lives in a join + // table rather than as an FK column on a node. + edges, err := copyGroupChildEdges(ctx, src, dst) + if err != nil { + return report, fmt.Errorf("copying group child edges: %w", err) + } + report.ChildGroupEdgs = edges + logf("migrated %-26s edges=%d", "group_child_groups", edges) + + return report, nil +} + +// migrateEntity copies a single entity. Reads happen against srcClient (a +// *XClient on the source); writes happen inside a transaction on dst so the +// entity is migrated atomically even when split across multiple CreateBulk +// batches. +func migrateEntity(ctx context.Context, name string, srcClient reflect.Value, dst *ent.Client, dstClient reflect.Value, batchSize int) (EntityResult, error) { + res := EntityResult{Entity: name} + + rows, err := queryAll(ctx, srcClient) + if err != nil { + return res, fmt.Errorf("querying source: %w", err) + } + res.Source = len(rows) + + existing, err := queryIDSet(ctx, dstClient) + if err != nil { + return res, fmt.Errorf("reading destination ids: %w", err) + } + + tx, err := dst.Tx(ctx) + if err != nil { + return res, fmt.Errorf("starting transaction: %w", err) + } + txClient := reflect.ValueOf(tx).Elem().FieldByName(name) + + batch := make([]reflect.Value, 0, batchSize) + flush := func() error { + if len(batch) == 0 { + return nil + } + n, err := createBulk(ctx, txClient, batch) + if err != nil { + return err + } + res.Inserted += n + batch = batch[:0] + return nil + } + + for _, row := range rows { + id := row.Elem().FieldByName("ID").Interface() + if _, ok := existing[id]; ok { + res.Skipped++ + continue + } + builder := txClient.MethodByName("Create").Call(nil)[0] + if err := applyFields(builder, row.Elem()); err != nil { + _ = tx.Rollback() + return res, fmt.Errorf("mapping fields for id %v: %w", id, err) + } + batch = append(batch, builder) + if len(batch) >= batchSize { + if err := flush(); err != nil { + _ = tx.Rollback() + return res, fmt.Errorf("bulk insert: %w", err) + } + } + } + if err := flush(); err != nil { + _ = tx.Rollback() + return res, fmt.Errorf("bulk insert: %w", err) + } + if err := tx.Commit(); err != nil { + return res, fmt.Errorf("committing: %w", err) + } + + // Verify: the destination must now hold exactly as many rows as the source. + dstCount, err := queryCount(ctx, dstClient) + if err != nil { + return res, fmt.Errorf("counting destination: %w", err) + } + res.Dest = dstCount + if dstCount != res.Source { + return res, fmt.Errorf("row count mismatch: source=%d dest=%d", res.Source, dstCount) + } + return res, nil +} + +// applyFields copies every persisted field from a source entity struct (rowElem, +// the dereferenced *X) onto a create builder by calling the matching generated +// setter via reflection. Pointer fields use SetNillable when available +// (scalar optionals) and fall back to Set (JSON pointer fields whose +// setter already takes a pointer). The relationship FK columns (e.g. ProjectID, +// OwnerID) are ordinary fields and are copied the same way, preserving edges. +func applyFields(builder, rowElem reflect.Value) error { + t := rowElem.Type() + for i := 0; i < t.NumField(); i++ { + f := t.Field(i) + switch f.Name { + case "config", "Edges", "selectValues": + continue + } + if f.PkgPath != "" { // unexported field + continue + } + fv := rowElem.Field(i) + + var candidates []string + if f.Type.Kind() == reflect.Ptr { + candidates = []string{"SetNillable" + f.Name, "Set" + f.Name} + } else { + candidates = []string{"Set" + f.Name} + } + + applied := false + for _, mName := range candidates { + m := builder.MethodByName(mName) + if !m.IsValid() { + continue + } + mt := m.Type() + if mt.NumIn() != 1 || mt.IsVariadic() { + continue + } + if !fv.Type().AssignableTo(mt.In(0)) { + continue + } + m.Call([]reflect.Value{fv}) + applied = true + break + } + if !applied && f.Name == "ID" { + // Every Ent entity in this schema uses a settable UUID primary key. + // A missing SetID would silently re-generate IDs and break FKs, so + // fail loud instead. + return fmt.Errorf("no SetID setter found for %s", t.Name()) + } + } + return nil +} + +// queryAll returns every row of an entity as a slice of *X reflect.Values via +// client.Query().All(ctx). +func queryAll(ctx context.Context, client reflect.Value) ([]reflect.Value, error) { + query := client.MethodByName("Query").Call(nil)[0] + out := query.MethodByName("All").Call([]reflect.Value{reflect.ValueOf(ctx)}) + if err := asError(out[1]); err != nil { + return nil, err + } + slice := out[0] + rows := make([]reflect.Value, slice.Len()) + for i := 0; i < slice.Len(); i++ { + rows[i] = slice.Index(i) + } + return rows, nil +} + +// queryIDSet returns the set of primary keys present for an entity via +// client.Query().IDs(ctx). Ent ID types (uuid.UUID, string, int) are all +// comparable and usable as map keys. +func queryIDSet(ctx context.Context, client reflect.Value) (map[any]struct{}, error) { + query := client.MethodByName("Query").Call(nil)[0] + out := query.MethodByName("IDs").Call([]reflect.Value{reflect.ValueOf(ctx)}) + if err := asError(out[1]); err != nil { + return nil, err + } + ids := out[0] + set := make(map[any]struct{}, ids.Len()) + for i := 0; i < ids.Len(); i++ { + set[ids.Index(i).Interface()] = struct{}{} + } + return set, nil +} + +// queryCount returns the row count for an entity via client.Query().Count(ctx). +func queryCount(ctx context.Context, client reflect.Value) (int, error) { + query := client.MethodByName("Query").Call(nil)[0] + out := query.MethodByName("Count").Call([]reflect.Value{reflect.ValueOf(ctx)}) + if err := asError(out[1]); err != nil { + return 0, err + } + return int(out[0].Int()), nil +} + +// createBulk runs client.CreateBulk(builders...).Save(ctx) and returns the +// number of rows written. +func createBulk(ctx context.Context, client reflect.Value, builders []reflect.Value) (int, error) { + if len(builders) == 0 { + return 0, nil + } + builderType := builders[0].Type() // *XCreate + slice := reflect.MakeSlice(reflect.SliceOf(builderType), len(builders), len(builders)) + for i, b := range builders { + slice.Index(i).Set(b) + } + bulk := client.MethodByName("CreateBulk").CallSlice([]reflect.Value{slice})[0] + out := bulk.MethodByName("Save").Call([]reflect.Value{reflect.ValueOf(ctx)}) + if err := asError(out[1]); err != nil { + return 0, err + } + return out[0].Len(), nil +} + +// copyGroupChildEdges copies the Group.child_groups many-to-many relationship, +// the only edge in the schema backed by a join table rather than an FK column. +// It is idempotent: only edges missing on the destination are added. +func copyGroupChildEdges(ctx context.Context, src, dst *ent.Client) (int, error) { + groups, err := src.Group.Query().All(ctx) + if err != nil { + return 0, err + } + added := 0 + for _, g := range groups { + srcChildIDs, err := g.QueryChildGroups().IDs(ctx) + if err != nil { + return added, err + } + if len(srcChildIDs) == 0 { + continue + } + dstGroup, err := dst.Group.Get(ctx, g.ID) + if err != nil { + return added, err + } + dstChildIDs, err := dstGroup.QueryChildGroups().IDs(ctx) + if err != nil { + return added, err + } + have := make(map[any]struct{}, len(dstChildIDs)) + for _, id := range dstChildIDs { + have[id] = struct{}{} + } + missing := srcChildIDs[:0:0] + for _, id := range srcChildIDs { + if _, ok := have[id]; !ok { + missing = append(missing, id) + } + } + if len(missing) == 0 { + continue + } + if err := dst.Group.UpdateOneID(g.ID).AddChildGroupIDs(missing...).Exec(ctx); err != nil { + return added, err + } + added += len(missing) + } + return added, nil +} + +// asError converts a reflect.Value holding an error interface to a Go error. +func asError(v reflect.Value) error { + if v.IsNil() { + return nil + } + return v.Interface().(error) +} diff --git a/pkg/ent/entc/migrate_beta_integration_test.go b/pkg/ent/entc/migrate_beta_integration_test.go new file mode 100644 index 000000000..4ee2e8362 --- /dev/null +++ b/pkg/ent/entc/migrate_beta_integration_test.go @@ -0,0 +1,296 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//go:build integration + +package entc_test + +import ( + "context" + "database/sql" + "os" + "path/filepath" + "testing" + "time" + + "github.com/google/uuid" + + "github.com/GoogleCloudPlatform/scion/pkg/ent" + "github.com/GoogleCloudPlatform/scion/pkg/ent/accesspolicy" + "github.com/GoogleCloudPlatform/scion/pkg/ent/agent" + "github.com/GoogleCloudPlatform/scion/pkg/ent/entc" + "github.com/GoogleCloudPlatform/scion/pkg/ent/group" + "github.com/GoogleCloudPlatform/scion/pkg/ent/groupmembership" + "github.com/GoogleCloudPlatform/scion/pkg/ent/policybinding" + "github.com/GoogleCloudPlatform/scion/pkg/ent/user" +) + +// TestMigrateBeta_SQLiteToPostgres exercises the full Migration β path against a +// real PostgreSQL instance. It seeds an Ent-on-SQLite database, copies it to +// Postgres with MigrateData, then asserts: +// - every entity's source and destination counts match, +// - the run is idempotent (a second run inserts nothing), +// - FK relationships and a M2M edge survive the copy, +// - representative field values round-trip intact. +// +// The destination DSN comes from SCION_PG_TEST_DSN; the test skips when it is +// unset. Run with: +// +// SCION_PG_TEST_DSN='postgres://user:pass@host:5432/db?sslmode=require' \ +// go test -tags integration -run TestMigrateBeta ./pkg/ent/entc/... +func TestMigrateBeta_SQLiteToPostgres(t *testing.T) { + dstDSN := os.Getenv("SCION_PG_TEST_DSN") + if dstDSN == "" { + t.Skip("SCION_PG_TEST_DSN not set; skipping Postgres integration test") + } + ctx := context.Background() + + // Start from a clean destination schema so row counts are deterministic. + resetPostgresSchema(t, dstDSN) + + // --- Seed an Ent-on-SQLite source. --- + dir := t.TempDir() + srcPath := filepath.Join(dir, "hub.db") + seed := seedSQLiteSource(t, ctx, srcPath) + + // --- Open source read-only + destination, ensure schema, migrate. --- + src, err := entc.OpenSQLiteReadOnly("file:" + srcPath + "?cache=shared") + if err != nil { + t.Fatalf("open source read-only: %v", err) + } + defer src.Close() + + dst, err := entc.OpenPostgres(dstDSN, entc.PoolConfig{MaxOpenConns: 10, MaxIdleConns: 5}) + if err != nil { + t.Fatalf("open postgres: %v", err) + } + defer dst.Close() + + if err := entc.AutoMigrate(ctx, dst); err != nil { + t.Fatalf("auto-migrate destination: %v", err) + } + + report, err := entc.MigrateData(ctx, src, dst, entc.MigrateOptions{ + Logf: func(format string, args ...any) { t.Logf(format, args...) }, + }) + if err != nil { + t.Fatalf("first migration: %v", err) + } + + // Every entity must have matching counts; the seeded entities must have + // actually inserted rows with nothing skipped on the first pass. + seenInserts := 0 + for _, e := range report.Entities { + if e.Source != e.Dest { + t.Errorf("%s: source=%d dest=%d (mismatch)", e.Entity, e.Source, e.Dest) + } + if e.Skipped != 0 { + t.Errorf("%s: expected 0 skipped on first run, got %d", e.Entity, e.Skipped) + } + seenInserts += e.Inserted + } + if seenInserts == 0 { + t.Fatal("first migration inserted nothing; seed did not take") + } + if report.ChildGroupEdgs != 1 { + t.Errorf("expected 1 child-group edge, got %d", report.ChildGroupEdgs) + } + + // --- Idempotency: a second run inserts nothing and skips everything. --- + report2, err := entc.MigrateData(ctx, src, dst, entc.MigrateOptions{}) + if err != nil { + t.Fatalf("second (idempotent) migration: %v", err) + } + for _, e := range report2.Entities { + if e.Inserted != 0 { + t.Errorf("%s: idempotent run inserted %d rows", e.Entity, e.Inserted) + } + if e.Source != e.Dest { + t.Errorf("%s: idempotent run count mismatch source=%d dest=%d", e.Entity, e.Source, e.Dest) + } + } + if report2.ChildGroupEdgs != 0 { + t.Errorf("idempotent run added %d child-group edges, want 0", report2.ChildGroupEdgs) + } + + // --- Value round-trip + relationship checks on the destination. --- + gotUser, err := dst.User.Get(ctx, seed.userID) + if err != nil { + t.Fatalf("fetch migrated user: %v", err) + } + if gotUser.Email != "alice@example.com" { + t.Errorf("user email = %q, want alice@example.com", gotUser.Email) + } + if gotUser.Role != user.RoleAdmin { + t.Errorf("user role = %q, want admin", gotUser.Role) + } + + gotAgent, err := dst.Agent.Get(ctx, seed.agentID) + if err != nil { + t.Fatalf("fetch migrated agent: %v", err) + } + if gotAgent.ProjectID != seed.projectID { + t.Errorf("agent project_id = %v, want %v", gotAgent.ProjectID, seed.projectID) + } + if gotAgent.OwnerID == nil || *gotAgent.OwnerID != seed.user2ID { + t.Errorf("agent owner_id = %v, want %v", gotAgent.OwnerID, seed.user2ID) + } + + // The parent group must still point at the child group. + parent, err := dst.Group.Get(ctx, seed.parentGroupID) + if err != nil { + t.Fatalf("fetch parent group: %v", err) + } + childIDs, err := parent.QueryChildGroups().IDs(ctx) + if err != nil { + t.Fatalf("query child groups: %v", err) + } + if len(childIDs) != 1 || childIDs[0] != seed.childGroupID { + t.Errorf("child group edges = %v, want [%v]", childIDs, seed.childGroupID) + } +} + +// seededIDs records the primary keys created by seedSQLiteSource for later +// assertions against the destination. +type seededIDs struct { + userID uuid.UUID + user2ID uuid.UUID + projectID uuid.UUID + agentID uuid.UUID + parentGroupID uuid.UUID + childGroupID uuid.UUID +} + +// seedSQLiteSource creates an Ent-on-SQLite database at path and populates it +// with a representative graph: two users, a project, a policy, two groups (in a +// parent/child relationship), an agent, a group membership, a policy binding, +// and an API key (an independent entity with a plain FK-style column). +func seedSQLiteSource(t *testing.T, ctx context.Context, path string) seededIDs { + t.Helper() + c, err := entc.OpenSQLite("file:"+path+"?cache=shared", entc.PoolConfig{MaxOpenConns: 1}) + if err != nil { + t.Fatalf("open sqlite for seeding: %v", err) + } + defer c.Close() + if err := entc.AutoMigrate(ctx, c); err != nil { + t.Fatalf("auto-migrate source: %v", err) + } + + now := time.Now().UTC().Truncate(time.Second) + ids := seededIDs{ + userID: uuid.New(), + user2ID: uuid.New(), + projectID: uuid.New(), + agentID: uuid.New(), + parentGroupID: uuid.New(), + childGroupID: uuid.New(), + } + + if err := c.User.Create(). + SetID(ids.userID).SetEmail("alice@example.com").SetDisplayName("Alice"). + SetRole(user.RoleAdmin).SetStatus(user.StatusActive).SetCreated(now). + Exec(ctx); err != nil { + t.Fatalf("seed user1: %v", err) + } + if err := c.User.Create(). + SetID(ids.user2ID).SetEmail("bob@example.com").SetDisplayName("Bob"). + SetRole(user.RoleMember).SetStatus(user.StatusActive).SetCreated(now). + Exec(ctx); err != nil { + t.Fatalf("seed user2: %v", err) + } + + if err := c.Project.Create(). + SetID(ids.projectID).SetName("Demo").SetSlug("demo").SetVisibility("private"). + SetOwnerID(ids.userID.String()).SetCreated(now).SetUpdated(now). + Exec(ctx); err != nil { + t.Fatalf("seed project: %v", err) + } + + policyID := uuid.New() + if err := c.AccessPolicy.Create(). + SetID(policyID).SetName("allow-all").SetScopeType(accesspolicy.ScopeTypeHub). + SetResourceType("agent").SetEffect(accesspolicy.EffectAllow).SetActions([]string{"read"}). + SetPriority(0).SetCreated(now).SetUpdated(now). + Exec(ctx); err != nil { + t.Fatalf("seed policy: %v", err) + } + + if err := c.Group.Create(). + SetID(ids.parentGroupID).SetName("Parent").SetSlug("parent"). + SetGroupType(group.GroupTypeExplicit).SetOwnerID(ids.userID).SetCreated(now).SetUpdated(now). + Exec(ctx); err != nil { + t.Fatalf("seed parent group: %v", err) + } + if err := c.Group.Create(). + SetID(ids.childGroupID).SetName("Child").SetSlug("child"). + SetGroupType(group.GroupTypeExplicit).SetOwnerID(ids.userID).SetCreated(now).SetUpdated(now). + Exec(ctx); err != nil { + t.Fatalf("seed child group: %v", err) + } + if err := c.Group.UpdateOneID(ids.parentGroupID).AddChildGroupIDs(ids.childGroupID).Exec(ctx); err != nil { + t.Fatalf("link child group: %v", err) + } + + if err := c.Agent.Create(). + SetID(ids.agentID).SetSlug("agent-1").SetName("Agent One"). + SetProjectID(ids.projectID).SetStatus(agent.StatusRunning).SetVisibility("private"). + SetCreatedBy(ids.userID).SetOwnerID(ids.user2ID).SetCreated(now).SetUpdated(now). + Exec(ctx); err != nil { + t.Fatalf("seed agent: %v", err) + } + + if err := c.GroupMembership.Create(). + SetID(uuid.New()).SetRole(groupmembership.RoleMember).SetAddedAt(now). + SetGroupID(ids.parentGroupID).SetUserID(ids.user2ID). + Exec(ctx); err != nil { + t.Fatalf("seed group membership: %v", err) + } + + if err := c.PolicyBinding.Create(). + SetID(uuid.New()).SetPrincipalType(policybinding.PrincipalTypeUser). + SetPolicyID(policyID).SetUserID(ids.userID).SetCreated(now). + Exec(ctx); err != nil { + t.Fatalf("seed policy binding: %v", err) + } + + if err := c.ApiKey.Create(). + SetID(uuid.New()).SetUserID(ids.userID).SetKeyHash("hash-abc").SetCreated(now). + Exec(ctx); err != nil { + t.Fatalf("seed api key: %v", err) + } + + return ids +} + +// resetPostgresSchema drops and recreates the public schema so the test starts +// from an empty database, making row-count assertions deterministic. +func resetPostgresSchema(t *testing.T, dsn string) { + t.Helper() + db, err := sql.Open("pgx", dsn) + if err != nil { + t.Fatalf("open postgres for reset: %v", err) + } + defer db.Close() + for _, stmt := range []string{ + "DROP SCHEMA public CASCADE", + "CREATE SCHEMA public", + } { + if _, err := db.Exec(stmt); err != nil { + t.Fatalf("reset schema (%s): %v", stmt, err) + } + } +} + +// ensure ent is referenced even if future edits drop direct uses. +var _ = ent.Client{} diff --git a/pkg/ent/entc/migrate_grove_to_project_test.go b/pkg/ent/entc/migrate_grove_to_project_test.go index 54ec5b53d..f43115513 100644 --- a/pkg/ent/entc/migrate_grove_to_project_test.go +++ b/pkg/ent/entc/migrate_grove_to_project_test.go @@ -49,7 +49,7 @@ func setupEntDB(t *testing.T) string { t.Helper() dbName := t.Name() dsn := "file:" + dbName + "?mode=memory&cache=shared" - client, err := OpenSQLite(dsn) + client, err := OpenSQLite(dsn, PoolConfig{}) require.NoError(t, err) t.Cleanup(func() { client.Close() }) require.NoError(t, AutoMigrate(context.Background(), client)) diff --git a/pkg/ent/envvar.go b/pkg/ent/envvar.go new file mode 100644 index 000000000..7262c6a09 --- /dev/null +++ b/pkg/ent/envvar.go @@ -0,0 +1,219 @@ +// Code generated by ent, DO NOT EDIT. + +package ent + +import ( + "fmt" + "strings" + "time" + + "entgo.io/ent" + "entgo.io/ent/dialect/sql" + "github.com/GoogleCloudPlatform/scion/pkg/ent/envvar" + "github.com/google/uuid" +) + +// EnvVar is the model entity for the EnvVar schema. +type EnvVar struct { + config `json:"-"` + // ID of the ent. + ID uuid.UUID `json:"id,omitempty"` + // Key holds the value of the "key" field. + Key string `json:"key,omitempty"` + // Value holds the value of the "value" field. + Value string `json:"value,omitempty"` + // Scope holds the value of the "scope" field. + Scope string `json:"scope,omitempty"` + // ScopeID holds the value of the "scope_id" field. + ScopeID string `json:"scope_id,omitempty"` + // Description holds the value of the "description" field. + Description string `json:"description,omitempty"` + // Sensitive holds the value of the "sensitive" field. + Sensitive bool `json:"sensitive,omitempty"` + // InjectionMode holds the value of the "injection_mode" field. + InjectionMode envvar.InjectionMode `json:"injection_mode,omitempty"` + // Secret holds the value of the "secret" field. + Secret bool `json:"secret,omitempty"` + // CreatedBy holds the value of the "created_by" field. + CreatedBy string `json:"created_by,omitempty"` + // Created holds the value of the "created" field. + Created time.Time `json:"created,omitempty"` + // Updated holds the value of the "updated" field. + Updated time.Time `json:"updated,omitempty"` + selectValues sql.SelectValues +} + +// scanValues returns the types for scanning values from sql.Rows. +func (*EnvVar) scanValues(columns []string) ([]any, error) { + values := make([]any, len(columns)) + for i := range columns { + switch columns[i] { + case envvar.FieldSensitive, envvar.FieldSecret: + values[i] = new(sql.NullBool) + case envvar.FieldKey, envvar.FieldValue, envvar.FieldScope, envvar.FieldScopeID, envvar.FieldDescription, envvar.FieldInjectionMode, envvar.FieldCreatedBy: + values[i] = new(sql.NullString) + case envvar.FieldCreated, envvar.FieldUpdated: + values[i] = new(sql.NullTime) + case envvar.FieldID: + values[i] = new(uuid.UUID) + default: + values[i] = new(sql.UnknownType) + } + } + return values, nil +} + +// assignValues assigns the values that were returned from sql.Rows (after scanning) +// to the EnvVar fields. +func (_m *EnvVar) assignValues(columns []string, values []any) error { + if m, n := len(values), len(columns); m < n { + return fmt.Errorf("mismatch number of scan values: %d != %d", m, n) + } + for i := range columns { + switch columns[i] { + case envvar.FieldID: + if value, ok := values[i].(*uuid.UUID); !ok { + return fmt.Errorf("unexpected type %T for field id", values[i]) + } else if value != nil { + _m.ID = *value + } + case envvar.FieldKey: + if value, ok := values[i].(*sql.NullString); !ok { + return fmt.Errorf("unexpected type %T for field key", values[i]) + } else if value.Valid { + _m.Key = value.String + } + case envvar.FieldValue: + if value, ok := values[i].(*sql.NullString); !ok { + return fmt.Errorf("unexpected type %T for field value", values[i]) + } else if value.Valid { + _m.Value = value.String + } + case envvar.FieldScope: + if value, ok := values[i].(*sql.NullString); !ok { + return fmt.Errorf("unexpected type %T for field scope", values[i]) + } else if value.Valid { + _m.Scope = value.String + } + case envvar.FieldScopeID: + if value, ok := values[i].(*sql.NullString); !ok { + return fmt.Errorf("unexpected type %T for field scope_id", values[i]) + } else if value.Valid { + _m.ScopeID = value.String + } + case envvar.FieldDescription: + if value, ok := values[i].(*sql.NullString); !ok { + return fmt.Errorf("unexpected type %T for field description", values[i]) + } else if value.Valid { + _m.Description = value.String + } + case envvar.FieldSensitive: + if value, ok := values[i].(*sql.NullBool); !ok { + return fmt.Errorf("unexpected type %T for field sensitive", values[i]) + } else if value.Valid { + _m.Sensitive = value.Bool + } + case envvar.FieldInjectionMode: + if value, ok := values[i].(*sql.NullString); !ok { + return fmt.Errorf("unexpected type %T for field injection_mode", values[i]) + } else if value.Valid { + _m.InjectionMode = envvar.InjectionMode(value.String) + } + case envvar.FieldSecret: + if value, ok := values[i].(*sql.NullBool); !ok { + return fmt.Errorf("unexpected type %T for field secret", values[i]) + } else if value.Valid { + _m.Secret = value.Bool + } + case envvar.FieldCreatedBy: + if value, ok := values[i].(*sql.NullString); !ok { + return fmt.Errorf("unexpected type %T for field created_by", values[i]) + } else if value.Valid { + _m.CreatedBy = value.String + } + case envvar.FieldCreated: + if value, ok := values[i].(*sql.NullTime); !ok { + return fmt.Errorf("unexpected type %T for field created", values[i]) + } else if value.Valid { + _m.Created = value.Time + } + case envvar.FieldUpdated: + if value, ok := values[i].(*sql.NullTime); !ok { + return fmt.Errorf("unexpected type %T for field updated", values[i]) + } else if value.Valid { + _m.Updated = value.Time + } + default: + _m.selectValues.Set(columns[i], values[i]) + } + } + return nil +} + +// GetValue returns the ent.Value that was dynamically selected and assigned to the EnvVar. +// This includes values selected through modifiers, order, etc. +func (_m *EnvVar) GetValue(name string) (ent.Value, error) { + return _m.selectValues.Get(name) +} + +// Update returns a builder for updating this EnvVar. +// Note that you need to call EnvVar.Unwrap() before calling this method if this EnvVar +// was returned from a transaction, and the transaction was committed or rolled back. +func (_m *EnvVar) Update() *EnvVarUpdateOne { + return NewEnvVarClient(_m.config).UpdateOne(_m) +} + +// Unwrap unwraps the EnvVar entity that was returned from a transaction after it was closed, +// so that all future queries will be executed through the driver which created the transaction. +func (_m *EnvVar) Unwrap() *EnvVar { + _tx, ok := _m.config.driver.(*txDriver) + if !ok { + panic("ent: EnvVar is not a transactional entity") + } + _m.config.driver = _tx.drv + return _m +} + +// String implements the fmt.Stringer. +func (_m *EnvVar) String() string { + var builder strings.Builder + builder.WriteString("EnvVar(") + builder.WriteString(fmt.Sprintf("id=%v, ", _m.ID)) + builder.WriteString("key=") + builder.WriteString(_m.Key) + builder.WriteString(", ") + builder.WriteString("value=") + builder.WriteString(_m.Value) + builder.WriteString(", ") + builder.WriteString("scope=") + builder.WriteString(_m.Scope) + builder.WriteString(", ") + builder.WriteString("scope_id=") + builder.WriteString(_m.ScopeID) + builder.WriteString(", ") + builder.WriteString("description=") + builder.WriteString(_m.Description) + builder.WriteString(", ") + builder.WriteString("sensitive=") + builder.WriteString(fmt.Sprintf("%v", _m.Sensitive)) + builder.WriteString(", ") + builder.WriteString("injection_mode=") + builder.WriteString(fmt.Sprintf("%v", _m.InjectionMode)) + builder.WriteString(", ") + builder.WriteString("secret=") + builder.WriteString(fmt.Sprintf("%v", _m.Secret)) + builder.WriteString(", ") + builder.WriteString("created_by=") + builder.WriteString(_m.CreatedBy) + builder.WriteString(", ") + builder.WriteString("created=") + builder.WriteString(_m.Created.Format(time.ANSIC)) + builder.WriteString(", ") + builder.WriteString("updated=") + builder.WriteString(_m.Updated.Format(time.ANSIC)) + builder.WriteByte(')') + return builder.String() +} + +// EnvVars is a parsable slice of EnvVar. +type EnvVars []*EnvVar diff --git a/pkg/ent/envvar/envvar.go b/pkg/ent/envvar/envvar.go new file mode 100644 index 000000000..8af7fe804 --- /dev/null +++ b/pkg/ent/envvar/envvar.go @@ -0,0 +1,176 @@ +// Code generated by ent, DO NOT EDIT. + +package envvar + +import ( + "fmt" + "time" + + "entgo.io/ent/dialect/sql" + "github.com/google/uuid" +) + +const ( + // Label holds the string label denoting the envvar type in the database. + Label = "env_var" + // FieldID holds the string denoting the id field in the database. + FieldID = "id" + // FieldKey holds the string denoting the key field in the database. + FieldKey = "key" + // FieldValue holds the string denoting the value field in the database. + FieldValue = "value" + // FieldScope holds the string denoting the scope field in the database. + FieldScope = "scope" + // FieldScopeID holds the string denoting the scope_id field in the database. + FieldScopeID = "scope_id" + // FieldDescription holds the string denoting the description field in the database. + FieldDescription = "description" + // FieldSensitive holds the string denoting the sensitive field in the database. + FieldSensitive = "sensitive" + // FieldInjectionMode holds the string denoting the injection_mode field in the database. + FieldInjectionMode = "injection_mode" + // FieldSecret holds the string denoting the secret field in the database. + FieldSecret = "secret" + // FieldCreatedBy holds the string denoting the created_by field in the database. + FieldCreatedBy = "created_by" + // FieldCreated holds the string denoting the created field in the database. + FieldCreated = "created" + // FieldUpdated holds the string denoting the updated field in the database. + FieldUpdated = "updated" + // Table holds the table name of the envvar in the database. + Table = "env_vars" +) + +// Columns holds all SQL columns for envvar fields. +var Columns = []string{ + FieldID, + FieldKey, + FieldValue, + FieldScope, + FieldScopeID, + FieldDescription, + FieldSensitive, + FieldInjectionMode, + FieldSecret, + FieldCreatedBy, + FieldCreated, + FieldUpdated, +} + +// ValidColumn reports if the column name is valid (part of the table columns). +func ValidColumn(column string) bool { + for i := range Columns { + if column == Columns[i] { + return true + } + } + return false +} + +var ( + // KeyValidator is a validator for the "key" field. It is called by the builders before save. + KeyValidator func(string) error + // ScopeValidator is a validator for the "scope" field. It is called by the builders before save. + ScopeValidator func(string) error + // DefaultSensitive holds the default value on creation for the "sensitive" field. + DefaultSensitive bool + // DefaultSecret holds the default value on creation for the "secret" field. + DefaultSecret bool + // DefaultCreated holds the default value on creation for the "created" field. + DefaultCreated func() time.Time + // DefaultUpdated holds the default value on creation for the "updated" field. + DefaultUpdated func() time.Time + // UpdateDefaultUpdated holds the default value on update for the "updated" field. + UpdateDefaultUpdated func() time.Time + // DefaultID holds the default value on creation for the "id" field. + DefaultID func() uuid.UUID +) + +// InjectionMode defines the type for the "injection_mode" enum field. +type InjectionMode string + +// InjectionModeAsNeeded is the default value of the InjectionMode enum. +const DefaultInjectionMode = InjectionModeAsNeeded + +// InjectionMode values. +const ( + InjectionModeAlways InjectionMode = "always" + InjectionModeAsNeeded InjectionMode = "as_needed" +) + +func (im InjectionMode) String() string { + return string(im) +} + +// InjectionModeValidator is a validator for the "injection_mode" field enum values. It is called by the builders before save. +func InjectionModeValidator(im InjectionMode) error { + switch im { + case InjectionModeAlways, InjectionModeAsNeeded: + return nil + default: + return fmt.Errorf("envvar: invalid enum value for injection_mode field: %q", im) + } +} + +// OrderOption defines the ordering options for the EnvVar queries. +type OrderOption func(*sql.Selector) + +// ByID orders the results by the id field. +func ByID(opts ...sql.OrderTermOption) OrderOption { + return sql.OrderByField(FieldID, opts...).ToFunc() +} + +// ByKey orders the results by the key field. +func ByKey(opts ...sql.OrderTermOption) OrderOption { + return sql.OrderByField(FieldKey, opts...).ToFunc() +} + +// ByValue orders the results by the value field. +func ByValue(opts ...sql.OrderTermOption) OrderOption { + return sql.OrderByField(FieldValue, opts...).ToFunc() +} + +// ByScope orders the results by the scope field. +func ByScope(opts ...sql.OrderTermOption) OrderOption { + return sql.OrderByField(FieldScope, opts...).ToFunc() +} + +// ByScopeID orders the results by the scope_id field. +func ByScopeID(opts ...sql.OrderTermOption) OrderOption { + return sql.OrderByField(FieldScopeID, opts...).ToFunc() +} + +// ByDescription orders the results by the description field. +func ByDescription(opts ...sql.OrderTermOption) OrderOption { + return sql.OrderByField(FieldDescription, opts...).ToFunc() +} + +// BySensitive orders the results by the sensitive field. +func BySensitive(opts ...sql.OrderTermOption) OrderOption { + return sql.OrderByField(FieldSensitive, opts...).ToFunc() +} + +// ByInjectionMode orders the results by the injection_mode field. +func ByInjectionMode(opts ...sql.OrderTermOption) OrderOption { + return sql.OrderByField(FieldInjectionMode, opts...).ToFunc() +} + +// BySecret orders the results by the secret field. +func BySecret(opts ...sql.OrderTermOption) OrderOption { + return sql.OrderByField(FieldSecret, opts...).ToFunc() +} + +// ByCreatedBy orders the results by the created_by field. +func ByCreatedBy(opts ...sql.OrderTermOption) OrderOption { + return sql.OrderByField(FieldCreatedBy, opts...).ToFunc() +} + +// ByCreated orders the results by the created field. +func ByCreated(opts ...sql.OrderTermOption) OrderOption { + return sql.OrderByField(FieldCreated, opts...).ToFunc() +} + +// ByUpdated orders the results by the updated field. +func ByUpdated(opts ...sql.OrderTermOption) OrderOption { + return sql.OrderByField(FieldUpdated, opts...).ToFunc() +} diff --git a/pkg/ent/envvar/where.go b/pkg/ent/envvar/where.go new file mode 100644 index 000000000..ebc748c5a --- /dev/null +++ b/pkg/ent/envvar/where.go @@ -0,0 +1,651 @@ +// Code generated by ent, DO NOT EDIT. + +package envvar + +import ( + "time" + + "entgo.io/ent/dialect/sql" + "github.com/GoogleCloudPlatform/scion/pkg/ent/predicate" + "github.com/google/uuid" +) + +// ID filters vertices based on their ID field. +func ID(id uuid.UUID) predicate.EnvVar { + return predicate.EnvVar(sql.FieldEQ(FieldID, id)) +} + +// IDEQ applies the EQ predicate on the ID field. +func IDEQ(id uuid.UUID) predicate.EnvVar { + return predicate.EnvVar(sql.FieldEQ(FieldID, id)) +} + +// IDNEQ applies the NEQ predicate on the ID field. +func IDNEQ(id uuid.UUID) predicate.EnvVar { + return predicate.EnvVar(sql.FieldNEQ(FieldID, id)) +} + +// IDIn applies the In predicate on the ID field. +func IDIn(ids ...uuid.UUID) predicate.EnvVar { + return predicate.EnvVar(sql.FieldIn(FieldID, ids...)) +} + +// IDNotIn applies the NotIn predicate on the ID field. +func IDNotIn(ids ...uuid.UUID) predicate.EnvVar { + return predicate.EnvVar(sql.FieldNotIn(FieldID, ids...)) +} + +// IDGT applies the GT predicate on the ID field. +func IDGT(id uuid.UUID) predicate.EnvVar { + return predicate.EnvVar(sql.FieldGT(FieldID, id)) +} + +// IDGTE applies the GTE predicate on the ID field. +func IDGTE(id uuid.UUID) predicate.EnvVar { + return predicate.EnvVar(sql.FieldGTE(FieldID, id)) +} + +// IDLT applies the LT predicate on the ID field. +func IDLT(id uuid.UUID) predicate.EnvVar { + return predicate.EnvVar(sql.FieldLT(FieldID, id)) +} + +// IDLTE applies the LTE predicate on the ID field. +func IDLTE(id uuid.UUID) predicate.EnvVar { + return predicate.EnvVar(sql.FieldLTE(FieldID, id)) +} + +// Key applies equality check predicate on the "key" field. It's identical to KeyEQ. +func Key(v string) predicate.EnvVar { + return predicate.EnvVar(sql.FieldEQ(FieldKey, v)) +} + +// Value applies equality check predicate on the "value" field. It's identical to ValueEQ. +func Value(v string) predicate.EnvVar { + return predicate.EnvVar(sql.FieldEQ(FieldValue, v)) +} + +// Scope applies equality check predicate on the "scope" field. It's identical to ScopeEQ. +func Scope(v string) predicate.EnvVar { + return predicate.EnvVar(sql.FieldEQ(FieldScope, v)) +} + +// ScopeID applies equality check predicate on the "scope_id" field. It's identical to ScopeIDEQ. +func ScopeID(v string) predicate.EnvVar { + return predicate.EnvVar(sql.FieldEQ(FieldScopeID, v)) +} + +// Description applies equality check predicate on the "description" field. It's identical to DescriptionEQ. +func Description(v string) predicate.EnvVar { + return predicate.EnvVar(sql.FieldEQ(FieldDescription, v)) +} + +// Sensitive applies equality check predicate on the "sensitive" field. It's identical to SensitiveEQ. +func Sensitive(v bool) predicate.EnvVar { + return predicate.EnvVar(sql.FieldEQ(FieldSensitive, v)) +} + +// Secret applies equality check predicate on the "secret" field. It's identical to SecretEQ. +func Secret(v bool) predicate.EnvVar { + return predicate.EnvVar(sql.FieldEQ(FieldSecret, v)) +} + +// CreatedBy applies equality check predicate on the "created_by" field. It's identical to CreatedByEQ. +func CreatedBy(v string) predicate.EnvVar { + return predicate.EnvVar(sql.FieldEQ(FieldCreatedBy, v)) +} + +// Created applies equality check predicate on the "created" field. It's identical to CreatedEQ. +func Created(v time.Time) predicate.EnvVar { + return predicate.EnvVar(sql.FieldEQ(FieldCreated, v)) +} + +// Updated applies equality check predicate on the "updated" field. It's identical to UpdatedEQ. +func Updated(v time.Time) predicate.EnvVar { + return predicate.EnvVar(sql.FieldEQ(FieldUpdated, v)) +} + +// KeyEQ applies the EQ predicate on the "key" field. +func KeyEQ(v string) predicate.EnvVar { + return predicate.EnvVar(sql.FieldEQ(FieldKey, v)) +} + +// KeyNEQ applies the NEQ predicate on the "key" field. +func KeyNEQ(v string) predicate.EnvVar { + return predicate.EnvVar(sql.FieldNEQ(FieldKey, v)) +} + +// KeyIn applies the In predicate on the "key" field. +func KeyIn(vs ...string) predicate.EnvVar { + return predicate.EnvVar(sql.FieldIn(FieldKey, vs...)) +} + +// KeyNotIn applies the NotIn predicate on the "key" field. +func KeyNotIn(vs ...string) predicate.EnvVar { + return predicate.EnvVar(sql.FieldNotIn(FieldKey, vs...)) +} + +// KeyGT applies the GT predicate on the "key" field. +func KeyGT(v string) predicate.EnvVar { + return predicate.EnvVar(sql.FieldGT(FieldKey, v)) +} + +// KeyGTE applies the GTE predicate on the "key" field. +func KeyGTE(v string) predicate.EnvVar { + return predicate.EnvVar(sql.FieldGTE(FieldKey, v)) +} + +// KeyLT applies the LT predicate on the "key" field. +func KeyLT(v string) predicate.EnvVar { + return predicate.EnvVar(sql.FieldLT(FieldKey, v)) +} + +// KeyLTE applies the LTE predicate on the "key" field. +func KeyLTE(v string) predicate.EnvVar { + return predicate.EnvVar(sql.FieldLTE(FieldKey, v)) +} + +// KeyContains applies the Contains predicate on the "key" field. +func KeyContains(v string) predicate.EnvVar { + return predicate.EnvVar(sql.FieldContains(FieldKey, v)) +} + +// KeyHasPrefix applies the HasPrefix predicate on the "key" field. +func KeyHasPrefix(v string) predicate.EnvVar { + return predicate.EnvVar(sql.FieldHasPrefix(FieldKey, v)) +} + +// KeyHasSuffix applies the HasSuffix predicate on the "key" field. +func KeyHasSuffix(v string) predicate.EnvVar { + return predicate.EnvVar(sql.FieldHasSuffix(FieldKey, v)) +} + +// KeyEqualFold applies the EqualFold predicate on the "key" field. +func KeyEqualFold(v string) predicate.EnvVar { + return predicate.EnvVar(sql.FieldEqualFold(FieldKey, v)) +} + +// KeyContainsFold applies the ContainsFold predicate on the "key" field. +func KeyContainsFold(v string) predicate.EnvVar { + return predicate.EnvVar(sql.FieldContainsFold(FieldKey, v)) +} + +// ValueEQ applies the EQ predicate on the "value" field. +func ValueEQ(v string) predicate.EnvVar { + return predicate.EnvVar(sql.FieldEQ(FieldValue, v)) +} + +// ValueNEQ applies the NEQ predicate on the "value" field. +func ValueNEQ(v string) predicate.EnvVar { + return predicate.EnvVar(sql.FieldNEQ(FieldValue, v)) +} + +// ValueIn applies the In predicate on the "value" field. +func ValueIn(vs ...string) predicate.EnvVar { + return predicate.EnvVar(sql.FieldIn(FieldValue, vs...)) +} + +// ValueNotIn applies the NotIn predicate on the "value" field. +func ValueNotIn(vs ...string) predicate.EnvVar { + return predicate.EnvVar(sql.FieldNotIn(FieldValue, vs...)) +} + +// ValueGT applies the GT predicate on the "value" field. +func ValueGT(v string) predicate.EnvVar { + return predicate.EnvVar(sql.FieldGT(FieldValue, v)) +} + +// ValueGTE applies the GTE predicate on the "value" field. +func ValueGTE(v string) predicate.EnvVar { + return predicate.EnvVar(sql.FieldGTE(FieldValue, v)) +} + +// ValueLT applies the LT predicate on the "value" field. +func ValueLT(v string) predicate.EnvVar { + return predicate.EnvVar(sql.FieldLT(FieldValue, v)) +} + +// ValueLTE applies the LTE predicate on the "value" field. +func ValueLTE(v string) predicate.EnvVar { + return predicate.EnvVar(sql.FieldLTE(FieldValue, v)) +} + +// ValueContains applies the Contains predicate on the "value" field. +func ValueContains(v string) predicate.EnvVar { + return predicate.EnvVar(sql.FieldContains(FieldValue, v)) +} + +// ValueHasPrefix applies the HasPrefix predicate on the "value" field. +func ValueHasPrefix(v string) predicate.EnvVar { + return predicate.EnvVar(sql.FieldHasPrefix(FieldValue, v)) +} + +// ValueHasSuffix applies the HasSuffix predicate on the "value" field. +func ValueHasSuffix(v string) predicate.EnvVar { + return predicate.EnvVar(sql.FieldHasSuffix(FieldValue, v)) +} + +// ValueEqualFold applies the EqualFold predicate on the "value" field. +func ValueEqualFold(v string) predicate.EnvVar { + return predicate.EnvVar(sql.FieldEqualFold(FieldValue, v)) +} + +// ValueContainsFold applies the ContainsFold predicate on the "value" field. +func ValueContainsFold(v string) predicate.EnvVar { + return predicate.EnvVar(sql.FieldContainsFold(FieldValue, v)) +} + +// ScopeEQ applies the EQ predicate on the "scope" field. +func ScopeEQ(v string) predicate.EnvVar { + return predicate.EnvVar(sql.FieldEQ(FieldScope, v)) +} + +// ScopeNEQ applies the NEQ predicate on the "scope" field. +func ScopeNEQ(v string) predicate.EnvVar { + return predicate.EnvVar(sql.FieldNEQ(FieldScope, v)) +} + +// ScopeIn applies the In predicate on the "scope" field. +func ScopeIn(vs ...string) predicate.EnvVar { + return predicate.EnvVar(sql.FieldIn(FieldScope, vs...)) +} + +// ScopeNotIn applies the NotIn predicate on the "scope" field. +func ScopeNotIn(vs ...string) predicate.EnvVar { + return predicate.EnvVar(sql.FieldNotIn(FieldScope, vs...)) +} + +// ScopeGT applies the GT predicate on the "scope" field. +func ScopeGT(v string) predicate.EnvVar { + return predicate.EnvVar(sql.FieldGT(FieldScope, v)) +} + +// ScopeGTE applies the GTE predicate on the "scope" field. +func ScopeGTE(v string) predicate.EnvVar { + return predicate.EnvVar(sql.FieldGTE(FieldScope, v)) +} + +// ScopeLT applies the LT predicate on the "scope" field. +func ScopeLT(v string) predicate.EnvVar { + return predicate.EnvVar(sql.FieldLT(FieldScope, v)) +} + +// ScopeLTE applies the LTE predicate on the "scope" field. +func ScopeLTE(v string) predicate.EnvVar { + return predicate.EnvVar(sql.FieldLTE(FieldScope, v)) +} + +// ScopeContains applies the Contains predicate on the "scope" field. +func ScopeContains(v string) predicate.EnvVar { + return predicate.EnvVar(sql.FieldContains(FieldScope, v)) +} + +// ScopeHasPrefix applies the HasPrefix predicate on the "scope" field. +func ScopeHasPrefix(v string) predicate.EnvVar { + return predicate.EnvVar(sql.FieldHasPrefix(FieldScope, v)) +} + +// ScopeHasSuffix applies the HasSuffix predicate on the "scope" field. +func ScopeHasSuffix(v string) predicate.EnvVar { + return predicate.EnvVar(sql.FieldHasSuffix(FieldScope, v)) +} + +// ScopeEqualFold applies the EqualFold predicate on the "scope" field. +func ScopeEqualFold(v string) predicate.EnvVar { + return predicate.EnvVar(sql.FieldEqualFold(FieldScope, v)) +} + +// ScopeContainsFold applies the ContainsFold predicate on the "scope" field. +func ScopeContainsFold(v string) predicate.EnvVar { + return predicate.EnvVar(sql.FieldContainsFold(FieldScope, v)) +} + +// ScopeIDEQ applies the EQ predicate on the "scope_id" field. +func ScopeIDEQ(v string) predicate.EnvVar { + return predicate.EnvVar(sql.FieldEQ(FieldScopeID, v)) +} + +// ScopeIDNEQ applies the NEQ predicate on the "scope_id" field. +func ScopeIDNEQ(v string) predicate.EnvVar { + return predicate.EnvVar(sql.FieldNEQ(FieldScopeID, v)) +} + +// ScopeIDIn applies the In predicate on the "scope_id" field. +func ScopeIDIn(vs ...string) predicate.EnvVar { + return predicate.EnvVar(sql.FieldIn(FieldScopeID, vs...)) +} + +// ScopeIDNotIn applies the NotIn predicate on the "scope_id" field. +func ScopeIDNotIn(vs ...string) predicate.EnvVar { + return predicate.EnvVar(sql.FieldNotIn(FieldScopeID, vs...)) +} + +// ScopeIDGT applies the GT predicate on the "scope_id" field. +func ScopeIDGT(v string) predicate.EnvVar { + return predicate.EnvVar(sql.FieldGT(FieldScopeID, v)) +} + +// ScopeIDGTE applies the GTE predicate on the "scope_id" field. +func ScopeIDGTE(v string) predicate.EnvVar { + return predicate.EnvVar(sql.FieldGTE(FieldScopeID, v)) +} + +// ScopeIDLT applies the LT predicate on the "scope_id" field. +func ScopeIDLT(v string) predicate.EnvVar { + return predicate.EnvVar(sql.FieldLT(FieldScopeID, v)) +} + +// ScopeIDLTE applies the LTE predicate on the "scope_id" field. +func ScopeIDLTE(v string) predicate.EnvVar { + return predicate.EnvVar(sql.FieldLTE(FieldScopeID, v)) +} + +// ScopeIDContains applies the Contains predicate on the "scope_id" field. +func ScopeIDContains(v string) predicate.EnvVar { + return predicate.EnvVar(sql.FieldContains(FieldScopeID, v)) +} + +// ScopeIDHasPrefix applies the HasPrefix predicate on the "scope_id" field. +func ScopeIDHasPrefix(v string) predicate.EnvVar { + return predicate.EnvVar(sql.FieldHasPrefix(FieldScopeID, v)) +} + +// ScopeIDHasSuffix applies the HasSuffix predicate on the "scope_id" field. +func ScopeIDHasSuffix(v string) predicate.EnvVar { + return predicate.EnvVar(sql.FieldHasSuffix(FieldScopeID, v)) +} + +// ScopeIDEqualFold applies the EqualFold predicate on the "scope_id" field. +func ScopeIDEqualFold(v string) predicate.EnvVar { + return predicate.EnvVar(sql.FieldEqualFold(FieldScopeID, v)) +} + +// ScopeIDContainsFold applies the ContainsFold predicate on the "scope_id" field. +func ScopeIDContainsFold(v string) predicate.EnvVar { + return predicate.EnvVar(sql.FieldContainsFold(FieldScopeID, v)) +} + +// DescriptionEQ applies the EQ predicate on the "description" field. +func DescriptionEQ(v string) predicate.EnvVar { + return predicate.EnvVar(sql.FieldEQ(FieldDescription, v)) +} + +// DescriptionNEQ applies the NEQ predicate on the "description" field. +func DescriptionNEQ(v string) predicate.EnvVar { + return predicate.EnvVar(sql.FieldNEQ(FieldDescription, v)) +} + +// DescriptionIn applies the In predicate on the "description" field. +func DescriptionIn(vs ...string) predicate.EnvVar { + return predicate.EnvVar(sql.FieldIn(FieldDescription, vs...)) +} + +// DescriptionNotIn applies the NotIn predicate on the "description" field. +func DescriptionNotIn(vs ...string) predicate.EnvVar { + return predicate.EnvVar(sql.FieldNotIn(FieldDescription, vs...)) +} + +// DescriptionGT applies the GT predicate on the "description" field. +func DescriptionGT(v string) predicate.EnvVar { + return predicate.EnvVar(sql.FieldGT(FieldDescription, v)) +} + +// DescriptionGTE applies the GTE predicate on the "description" field. +func DescriptionGTE(v string) predicate.EnvVar { + return predicate.EnvVar(sql.FieldGTE(FieldDescription, v)) +} + +// DescriptionLT applies the LT predicate on the "description" field. +func DescriptionLT(v string) predicate.EnvVar { + return predicate.EnvVar(sql.FieldLT(FieldDescription, v)) +} + +// DescriptionLTE applies the LTE predicate on the "description" field. +func DescriptionLTE(v string) predicate.EnvVar { + return predicate.EnvVar(sql.FieldLTE(FieldDescription, v)) +} + +// DescriptionContains applies the Contains predicate on the "description" field. +func DescriptionContains(v string) predicate.EnvVar { + return predicate.EnvVar(sql.FieldContains(FieldDescription, v)) +} + +// DescriptionHasPrefix applies the HasPrefix predicate on the "description" field. +func DescriptionHasPrefix(v string) predicate.EnvVar { + return predicate.EnvVar(sql.FieldHasPrefix(FieldDescription, v)) +} + +// DescriptionHasSuffix applies the HasSuffix predicate on the "description" field. +func DescriptionHasSuffix(v string) predicate.EnvVar { + return predicate.EnvVar(sql.FieldHasSuffix(FieldDescription, v)) +} + +// DescriptionIsNil applies the IsNil predicate on the "description" field. +func DescriptionIsNil() predicate.EnvVar { + return predicate.EnvVar(sql.FieldIsNull(FieldDescription)) +} + +// DescriptionNotNil applies the NotNil predicate on the "description" field. +func DescriptionNotNil() predicate.EnvVar { + return predicate.EnvVar(sql.FieldNotNull(FieldDescription)) +} + +// DescriptionEqualFold applies the EqualFold predicate on the "description" field. +func DescriptionEqualFold(v string) predicate.EnvVar { + return predicate.EnvVar(sql.FieldEqualFold(FieldDescription, v)) +} + +// DescriptionContainsFold applies the ContainsFold predicate on the "description" field. +func DescriptionContainsFold(v string) predicate.EnvVar { + return predicate.EnvVar(sql.FieldContainsFold(FieldDescription, v)) +} + +// SensitiveEQ applies the EQ predicate on the "sensitive" field. +func SensitiveEQ(v bool) predicate.EnvVar { + return predicate.EnvVar(sql.FieldEQ(FieldSensitive, v)) +} + +// SensitiveNEQ applies the NEQ predicate on the "sensitive" field. +func SensitiveNEQ(v bool) predicate.EnvVar { + return predicate.EnvVar(sql.FieldNEQ(FieldSensitive, v)) +} + +// InjectionModeEQ applies the EQ predicate on the "injection_mode" field. +func InjectionModeEQ(v InjectionMode) predicate.EnvVar { + return predicate.EnvVar(sql.FieldEQ(FieldInjectionMode, v)) +} + +// InjectionModeNEQ applies the NEQ predicate on the "injection_mode" field. +func InjectionModeNEQ(v InjectionMode) predicate.EnvVar { + return predicate.EnvVar(sql.FieldNEQ(FieldInjectionMode, v)) +} + +// InjectionModeIn applies the In predicate on the "injection_mode" field. +func InjectionModeIn(vs ...InjectionMode) predicate.EnvVar { + return predicate.EnvVar(sql.FieldIn(FieldInjectionMode, vs...)) +} + +// InjectionModeNotIn applies the NotIn predicate on the "injection_mode" field. +func InjectionModeNotIn(vs ...InjectionMode) predicate.EnvVar { + return predicate.EnvVar(sql.FieldNotIn(FieldInjectionMode, vs...)) +} + +// SecretEQ applies the EQ predicate on the "secret" field. +func SecretEQ(v bool) predicate.EnvVar { + return predicate.EnvVar(sql.FieldEQ(FieldSecret, v)) +} + +// SecretNEQ applies the NEQ predicate on the "secret" field. +func SecretNEQ(v bool) predicate.EnvVar { + return predicate.EnvVar(sql.FieldNEQ(FieldSecret, v)) +} + +// CreatedByEQ applies the EQ predicate on the "created_by" field. +func CreatedByEQ(v string) predicate.EnvVar { + return predicate.EnvVar(sql.FieldEQ(FieldCreatedBy, v)) +} + +// CreatedByNEQ applies the NEQ predicate on the "created_by" field. +func CreatedByNEQ(v string) predicate.EnvVar { + return predicate.EnvVar(sql.FieldNEQ(FieldCreatedBy, v)) +} + +// CreatedByIn applies the In predicate on the "created_by" field. +func CreatedByIn(vs ...string) predicate.EnvVar { + return predicate.EnvVar(sql.FieldIn(FieldCreatedBy, vs...)) +} + +// CreatedByNotIn applies the NotIn predicate on the "created_by" field. +func CreatedByNotIn(vs ...string) predicate.EnvVar { + return predicate.EnvVar(sql.FieldNotIn(FieldCreatedBy, vs...)) +} + +// CreatedByGT applies the GT predicate on the "created_by" field. +func CreatedByGT(v string) predicate.EnvVar { + return predicate.EnvVar(sql.FieldGT(FieldCreatedBy, v)) +} + +// CreatedByGTE applies the GTE predicate on the "created_by" field. +func CreatedByGTE(v string) predicate.EnvVar { + return predicate.EnvVar(sql.FieldGTE(FieldCreatedBy, v)) +} + +// CreatedByLT applies the LT predicate on the "created_by" field. +func CreatedByLT(v string) predicate.EnvVar { + return predicate.EnvVar(sql.FieldLT(FieldCreatedBy, v)) +} + +// CreatedByLTE applies the LTE predicate on the "created_by" field. +func CreatedByLTE(v string) predicate.EnvVar { + return predicate.EnvVar(sql.FieldLTE(FieldCreatedBy, v)) +} + +// CreatedByContains applies the Contains predicate on the "created_by" field. +func CreatedByContains(v string) predicate.EnvVar { + return predicate.EnvVar(sql.FieldContains(FieldCreatedBy, v)) +} + +// CreatedByHasPrefix applies the HasPrefix predicate on the "created_by" field. +func CreatedByHasPrefix(v string) predicate.EnvVar { + return predicate.EnvVar(sql.FieldHasPrefix(FieldCreatedBy, v)) +} + +// CreatedByHasSuffix applies the HasSuffix predicate on the "created_by" field. +func CreatedByHasSuffix(v string) predicate.EnvVar { + return predicate.EnvVar(sql.FieldHasSuffix(FieldCreatedBy, v)) +} + +// CreatedByIsNil applies the IsNil predicate on the "created_by" field. +func CreatedByIsNil() predicate.EnvVar { + return predicate.EnvVar(sql.FieldIsNull(FieldCreatedBy)) +} + +// CreatedByNotNil applies the NotNil predicate on the "created_by" field. +func CreatedByNotNil() predicate.EnvVar { + return predicate.EnvVar(sql.FieldNotNull(FieldCreatedBy)) +} + +// CreatedByEqualFold applies the EqualFold predicate on the "created_by" field. +func CreatedByEqualFold(v string) predicate.EnvVar { + return predicate.EnvVar(sql.FieldEqualFold(FieldCreatedBy, v)) +} + +// CreatedByContainsFold applies the ContainsFold predicate on the "created_by" field. +func CreatedByContainsFold(v string) predicate.EnvVar { + return predicate.EnvVar(sql.FieldContainsFold(FieldCreatedBy, v)) +} + +// CreatedEQ applies the EQ predicate on the "created" field. +func CreatedEQ(v time.Time) predicate.EnvVar { + return predicate.EnvVar(sql.FieldEQ(FieldCreated, v)) +} + +// CreatedNEQ applies the NEQ predicate on the "created" field. +func CreatedNEQ(v time.Time) predicate.EnvVar { + return predicate.EnvVar(sql.FieldNEQ(FieldCreated, v)) +} + +// CreatedIn applies the In predicate on the "created" field. +func CreatedIn(vs ...time.Time) predicate.EnvVar { + return predicate.EnvVar(sql.FieldIn(FieldCreated, vs...)) +} + +// CreatedNotIn applies the NotIn predicate on the "created" field. +func CreatedNotIn(vs ...time.Time) predicate.EnvVar { + return predicate.EnvVar(sql.FieldNotIn(FieldCreated, vs...)) +} + +// CreatedGT applies the GT predicate on the "created" field. +func CreatedGT(v time.Time) predicate.EnvVar { + return predicate.EnvVar(sql.FieldGT(FieldCreated, v)) +} + +// CreatedGTE applies the GTE predicate on the "created" field. +func CreatedGTE(v time.Time) predicate.EnvVar { + return predicate.EnvVar(sql.FieldGTE(FieldCreated, v)) +} + +// CreatedLT applies the LT predicate on the "created" field. +func CreatedLT(v time.Time) predicate.EnvVar { + return predicate.EnvVar(sql.FieldLT(FieldCreated, v)) +} + +// CreatedLTE applies the LTE predicate on the "created" field. +func CreatedLTE(v time.Time) predicate.EnvVar { + return predicate.EnvVar(sql.FieldLTE(FieldCreated, v)) +} + +// UpdatedEQ applies the EQ predicate on the "updated" field. +func UpdatedEQ(v time.Time) predicate.EnvVar { + return predicate.EnvVar(sql.FieldEQ(FieldUpdated, v)) +} + +// UpdatedNEQ applies the NEQ predicate on the "updated" field. +func UpdatedNEQ(v time.Time) predicate.EnvVar { + return predicate.EnvVar(sql.FieldNEQ(FieldUpdated, v)) +} + +// UpdatedIn applies the In predicate on the "updated" field. +func UpdatedIn(vs ...time.Time) predicate.EnvVar { + return predicate.EnvVar(sql.FieldIn(FieldUpdated, vs...)) +} + +// UpdatedNotIn applies the NotIn predicate on the "updated" field. +func UpdatedNotIn(vs ...time.Time) predicate.EnvVar { + return predicate.EnvVar(sql.FieldNotIn(FieldUpdated, vs...)) +} + +// UpdatedGT applies the GT predicate on the "updated" field. +func UpdatedGT(v time.Time) predicate.EnvVar { + return predicate.EnvVar(sql.FieldGT(FieldUpdated, v)) +} + +// UpdatedGTE applies the GTE predicate on the "updated" field. +func UpdatedGTE(v time.Time) predicate.EnvVar { + return predicate.EnvVar(sql.FieldGTE(FieldUpdated, v)) +} + +// UpdatedLT applies the LT predicate on the "updated" field. +func UpdatedLT(v time.Time) predicate.EnvVar { + return predicate.EnvVar(sql.FieldLT(FieldUpdated, v)) +} + +// UpdatedLTE applies the LTE predicate on the "updated" field. +func UpdatedLTE(v time.Time) predicate.EnvVar { + return predicate.EnvVar(sql.FieldLTE(FieldUpdated, v)) +} + +// And groups predicates with the AND operator between them. +func And(predicates ...predicate.EnvVar) predicate.EnvVar { + return predicate.EnvVar(sql.AndPredicates(predicates...)) +} + +// Or groups predicates with the OR operator between them. +func Or(predicates ...predicate.EnvVar) predicate.EnvVar { + return predicate.EnvVar(sql.OrPredicates(predicates...)) +} + +// Not applies the not operator on the given predicate. +func Not(p predicate.EnvVar) predicate.EnvVar { + return predicate.EnvVar(sql.NotPredicates(p)) +} diff --git a/pkg/ent/envvar_create.go b/pkg/ent/envvar_create.go new file mode 100644 index 000000000..af444f06e --- /dev/null +++ b/pkg/ent/envvar_create.go @@ -0,0 +1,1130 @@ +// Code generated by ent, DO NOT EDIT. + +package ent + +import ( + "context" + "errors" + "fmt" + "time" + + "entgo.io/ent/dialect" + "entgo.io/ent/dialect/sql" + "entgo.io/ent/dialect/sql/sqlgraph" + "entgo.io/ent/schema/field" + "github.com/GoogleCloudPlatform/scion/pkg/ent/envvar" + "github.com/google/uuid" +) + +// EnvVarCreate is the builder for creating a EnvVar entity. +type EnvVarCreate struct { + config + mutation *EnvVarMutation + hooks []Hook + conflict []sql.ConflictOption +} + +// SetKey sets the "key" field. +func (_c *EnvVarCreate) SetKey(v string) *EnvVarCreate { + _c.mutation.SetKey(v) + return _c +} + +// SetValue sets the "value" field. +func (_c *EnvVarCreate) SetValue(v string) *EnvVarCreate { + _c.mutation.SetValue(v) + return _c +} + +// SetScope sets the "scope" field. +func (_c *EnvVarCreate) SetScope(v string) *EnvVarCreate { + _c.mutation.SetScope(v) + return _c +} + +// SetScopeID sets the "scope_id" field. +func (_c *EnvVarCreate) SetScopeID(v string) *EnvVarCreate { + _c.mutation.SetScopeID(v) + return _c +} + +// SetDescription sets the "description" field. +func (_c *EnvVarCreate) SetDescription(v string) *EnvVarCreate { + _c.mutation.SetDescription(v) + return _c +} + +// SetNillableDescription sets the "description" field if the given value is not nil. +func (_c *EnvVarCreate) SetNillableDescription(v *string) *EnvVarCreate { + if v != nil { + _c.SetDescription(*v) + } + return _c +} + +// SetSensitive sets the "sensitive" field. +func (_c *EnvVarCreate) SetSensitive(v bool) *EnvVarCreate { + _c.mutation.SetSensitive(v) + return _c +} + +// SetNillableSensitive sets the "sensitive" field if the given value is not nil. +func (_c *EnvVarCreate) SetNillableSensitive(v *bool) *EnvVarCreate { + if v != nil { + _c.SetSensitive(*v) + } + return _c +} + +// SetInjectionMode sets the "injection_mode" field. +func (_c *EnvVarCreate) SetInjectionMode(v envvar.InjectionMode) *EnvVarCreate { + _c.mutation.SetInjectionMode(v) + return _c +} + +// SetNillableInjectionMode sets the "injection_mode" field if the given value is not nil. +func (_c *EnvVarCreate) SetNillableInjectionMode(v *envvar.InjectionMode) *EnvVarCreate { + if v != nil { + _c.SetInjectionMode(*v) + } + return _c +} + +// SetSecret sets the "secret" field. +func (_c *EnvVarCreate) SetSecret(v bool) *EnvVarCreate { + _c.mutation.SetSecret(v) + return _c +} + +// SetNillableSecret sets the "secret" field if the given value is not nil. +func (_c *EnvVarCreate) SetNillableSecret(v *bool) *EnvVarCreate { + if v != nil { + _c.SetSecret(*v) + } + return _c +} + +// SetCreatedBy sets the "created_by" field. +func (_c *EnvVarCreate) SetCreatedBy(v string) *EnvVarCreate { + _c.mutation.SetCreatedBy(v) + return _c +} + +// SetNillableCreatedBy sets the "created_by" field if the given value is not nil. +func (_c *EnvVarCreate) SetNillableCreatedBy(v *string) *EnvVarCreate { + if v != nil { + _c.SetCreatedBy(*v) + } + return _c +} + +// SetCreated sets the "created" field. +func (_c *EnvVarCreate) SetCreated(v time.Time) *EnvVarCreate { + _c.mutation.SetCreated(v) + return _c +} + +// SetNillableCreated sets the "created" field if the given value is not nil. +func (_c *EnvVarCreate) SetNillableCreated(v *time.Time) *EnvVarCreate { + if v != nil { + _c.SetCreated(*v) + } + return _c +} + +// SetUpdated sets the "updated" field. +func (_c *EnvVarCreate) SetUpdated(v time.Time) *EnvVarCreate { + _c.mutation.SetUpdated(v) + return _c +} + +// SetNillableUpdated sets the "updated" field if the given value is not nil. +func (_c *EnvVarCreate) SetNillableUpdated(v *time.Time) *EnvVarCreate { + if v != nil { + _c.SetUpdated(*v) + } + return _c +} + +// SetID sets the "id" field. +func (_c *EnvVarCreate) SetID(v uuid.UUID) *EnvVarCreate { + _c.mutation.SetID(v) + return _c +} + +// SetNillableID sets the "id" field if the given value is not nil. +func (_c *EnvVarCreate) SetNillableID(v *uuid.UUID) *EnvVarCreate { + if v != nil { + _c.SetID(*v) + } + return _c +} + +// Mutation returns the EnvVarMutation object of the builder. +func (_c *EnvVarCreate) Mutation() *EnvVarMutation { + return _c.mutation +} + +// Save creates the EnvVar in the database. +func (_c *EnvVarCreate) Save(ctx context.Context) (*EnvVar, error) { + _c.defaults() + return withHooks(ctx, _c.sqlSave, _c.mutation, _c.hooks) +} + +// SaveX calls Save and panics if Save returns an error. +func (_c *EnvVarCreate) SaveX(ctx context.Context) *EnvVar { + v, err := _c.Save(ctx) + if err != nil { + panic(err) + } + return v +} + +// Exec executes the query. +func (_c *EnvVarCreate) Exec(ctx context.Context) error { + _, err := _c.Save(ctx) + return err +} + +// ExecX is like Exec, but panics if an error occurs. +func (_c *EnvVarCreate) ExecX(ctx context.Context) { + if err := _c.Exec(ctx); err != nil { + panic(err) + } +} + +// defaults sets the default values of the builder before save. +func (_c *EnvVarCreate) defaults() { + if _, ok := _c.mutation.Sensitive(); !ok { + v := envvar.DefaultSensitive + _c.mutation.SetSensitive(v) + } + if _, ok := _c.mutation.InjectionMode(); !ok { + v := envvar.DefaultInjectionMode + _c.mutation.SetInjectionMode(v) + } + if _, ok := _c.mutation.Secret(); !ok { + v := envvar.DefaultSecret + _c.mutation.SetSecret(v) + } + if _, ok := _c.mutation.Created(); !ok { + v := envvar.DefaultCreated() + _c.mutation.SetCreated(v) + } + if _, ok := _c.mutation.Updated(); !ok { + v := envvar.DefaultUpdated() + _c.mutation.SetUpdated(v) + } + if _, ok := _c.mutation.ID(); !ok { + v := envvar.DefaultID() + _c.mutation.SetID(v) + } +} + +// check runs all checks and user-defined validators on the builder. +func (_c *EnvVarCreate) check() error { + if _, ok := _c.mutation.Key(); !ok { + return &ValidationError{Name: "key", err: errors.New(`ent: missing required field "EnvVar.key"`)} + } + if v, ok := _c.mutation.Key(); ok { + if err := envvar.KeyValidator(v); err != nil { + return &ValidationError{Name: "key", err: fmt.Errorf(`ent: validator failed for field "EnvVar.key": %w`, err)} + } + } + if _, ok := _c.mutation.Value(); !ok { + return &ValidationError{Name: "value", err: errors.New(`ent: missing required field "EnvVar.value"`)} + } + if _, ok := _c.mutation.Scope(); !ok { + return &ValidationError{Name: "scope", err: errors.New(`ent: missing required field "EnvVar.scope"`)} + } + if v, ok := _c.mutation.Scope(); ok { + if err := envvar.ScopeValidator(v); err != nil { + return &ValidationError{Name: "scope", err: fmt.Errorf(`ent: validator failed for field "EnvVar.scope": %w`, err)} + } + } + if _, ok := _c.mutation.ScopeID(); !ok { + return &ValidationError{Name: "scope_id", err: errors.New(`ent: missing required field "EnvVar.scope_id"`)} + } + if _, ok := _c.mutation.Sensitive(); !ok { + return &ValidationError{Name: "sensitive", err: errors.New(`ent: missing required field "EnvVar.sensitive"`)} + } + if _, ok := _c.mutation.InjectionMode(); !ok { + return &ValidationError{Name: "injection_mode", err: errors.New(`ent: missing required field "EnvVar.injection_mode"`)} + } + if v, ok := _c.mutation.InjectionMode(); ok { + if err := envvar.InjectionModeValidator(v); err != nil { + return &ValidationError{Name: "injection_mode", err: fmt.Errorf(`ent: validator failed for field "EnvVar.injection_mode": %w`, err)} + } + } + if _, ok := _c.mutation.Secret(); !ok { + return &ValidationError{Name: "secret", err: errors.New(`ent: missing required field "EnvVar.secret"`)} + } + if _, ok := _c.mutation.Created(); !ok { + return &ValidationError{Name: "created", err: errors.New(`ent: missing required field "EnvVar.created"`)} + } + if _, ok := _c.mutation.Updated(); !ok { + return &ValidationError{Name: "updated", err: errors.New(`ent: missing required field "EnvVar.updated"`)} + } + return nil +} + +func (_c *EnvVarCreate) sqlSave(ctx context.Context) (*EnvVar, error) { + if err := _c.check(); err != nil { + return nil, err + } + _node, _spec := _c.createSpec() + if err := sqlgraph.CreateNode(ctx, _c.driver, _spec); err != nil { + if sqlgraph.IsConstraintError(err) { + err = &ConstraintError{msg: err.Error(), wrap: err} + } + return nil, err + } + if _spec.ID.Value != nil { + if id, ok := _spec.ID.Value.(*uuid.UUID); ok { + _node.ID = *id + } else if err := _node.ID.Scan(_spec.ID.Value); err != nil { + return nil, err + } + } + _c.mutation.id = &_node.ID + _c.mutation.done = true + return _node, nil +} + +func (_c *EnvVarCreate) createSpec() (*EnvVar, *sqlgraph.CreateSpec) { + var ( + _node = &EnvVar{config: _c.config} + _spec = sqlgraph.NewCreateSpec(envvar.Table, sqlgraph.NewFieldSpec(envvar.FieldID, field.TypeUUID)) + ) + _spec.OnConflict = _c.conflict + if id, ok := _c.mutation.ID(); ok { + _node.ID = id + _spec.ID.Value = &id + } + if value, ok := _c.mutation.Key(); ok { + _spec.SetField(envvar.FieldKey, field.TypeString, value) + _node.Key = value + } + if value, ok := _c.mutation.Value(); ok { + _spec.SetField(envvar.FieldValue, field.TypeString, value) + _node.Value = value + } + if value, ok := _c.mutation.Scope(); ok { + _spec.SetField(envvar.FieldScope, field.TypeString, value) + _node.Scope = value + } + if value, ok := _c.mutation.ScopeID(); ok { + _spec.SetField(envvar.FieldScopeID, field.TypeString, value) + _node.ScopeID = value + } + if value, ok := _c.mutation.Description(); ok { + _spec.SetField(envvar.FieldDescription, field.TypeString, value) + _node.Description = value + } + if value, ok := _c.mutation.Sensitive(); ok { + _spec.SetField(envvar.FieldSensitive, field.TypeBool, value) + _node.Sensitive = value + } + if value, ok := _c.mutation.InjectionMode(); ok { + _spec.SetField(envvar.FieldInjectionMode, field.TypeEnum, value) + _node.InjectionMode = value + } + if value, ok := _c.mutation.Secret(); ok { + _spec.SetField(envvar.FieldSecret, field.TypeBool, value) + _node.Secret = value + } + if value, ok := _c.mutation.CreatedBy(); ok { + _spec.SetField(envvar.FieldCreatedBy, field.TypeString, value) + _node.CreatedBy = value + } + if value, ok := _c.mutation.Created(); ok { + _spec.SetField(envvar.FieldCreated, field.TypeTime, value) + _node.Created = value + } + if value, ok := _c.mutation.Updated(); ok { + _spec.SetField(envvar.FieldUpdated, field.TypeTime, value) + _node.Updated = value + } + return _node, _spec +} + +// OnConflict allows configuring the `ON CONFLICT` / `ON DUPLICATE KEY` clause +// of the `INSERT` statement. For example: +// +// client.EnvVar.Create(). +// SetKey(v). +// OnConflict( +// // Update the row with the new values +// // the was proposed for insertion. +// sql.ResolveWithNewValues(), +// ). +// // Override some of the fields with custom +// // update values. +// Update(func(u *ent.EnvVarUpsert) { +// SetKey(v+v). +// }). +// Exec(ctx) +func (_c *EnvVarCreate) OnConflict(opts ...sql.ConflictOption) *EnvVarUpsertOne { + _c.conflict = opts + return &EnvVarUpsertOne{ + create: _c, + } +} + +// OnConflictColumns calls `OnConflict` and configures the columns +// as conflict target. Using this option is equivalent to using: +// +// client.EnvVar.Create(). +// OnConflict(sql.ConflictColumns(columns...)). +// Exec(ctx) +func (_c *EnvVarCreate) OnConflictColumns(columns ...string) *EnvVarUpsertOne { + _c.conflict = append(_c.conflict, sql.ConflictColumns(columns...)) + return &EnvVarUpsertOne{ + create: _c, + } +} + +type ( + // EnvVarUpsertOne is the builder for "upsert"-ing + // one EnvVar node. + EnvVarUpsertOne struct { + create *EnvVarCreate + } + + // EnvVarUpsert is the "OnConflict" setter. + EnvVarUpsert struct { + *sql.UpdateSet + } +) + +// SetKey sets the "key" field. +func (u *EnvVarUpsert) SetKey(v string) *EnvVarUpsert { + u.Set(envvar.FieldKey, v) + return u +} + +// UpdateKey sets the "key" field to the value that was provided on create. +func (u *EnvVarUpsert) UpdateKey() *EnvVarUpsert { + u.SetExcluded(envvar.FieldKey) + return u +} + +// SetValue sets the "value" field. +func (u *EnvVarUpsert) SetValue(v string) *EnvVarUpsert { + u.Set(envvar.FieldValue, v) + return u +} + +// UpdateValue sets the "value" field to the value that was provided on create. +func (u *EnvVarUpsert) UpdateValue() *EnvVarUpsert { + u.SetExcluded(envvar.FieldValue) + return u +} + +// SetScope sets the "scope" field. +func (u *EnvVarUpsert) SetScope(v string) *EnvVarUpsert { + u.Set(envvar.FieldScope, v) + return u +} + +// UpdateScope sets the "scope" field to the value that was provided on create. +func (u *EnvVarUpsert) UpdateScope() *EnvVarUpsert { + u.SetExcluded(envvar.FieldScope) + return u +} + +// SetScopeID sets the "scope_id" field. +func (u *EnvVarUpsert) SetScopeID(v string) *EnvVarUpsert { + u.Set(envvar.FieldScopeID, v) + return u +} + +// UpdateScopeID sets the "scope_id" field to the value that was provided on create. +func (u *EnvVarUpsert) UpdateScopeID() *EnvVarUpsert { + u.SetExcluded(envvar.FieldScopeID) + return u +} + +// SetDescription sets the "description" field. +func (u *EnvVarUpsert) SetDescription(v string) *EnvVarUpsert { + u.Set(envvar.FieldDescription, v) + return u +} + +// UpdateDescription sets the "description" field to the value that was provided on create. +func (u *EnvVarUpsert) UpdateDescription() *EnvVarUpsert { + u.SetExcluded(envvar.FieldDescription) + return u +} + +// ClearDescription clears the value of the "description" field. +func (u *EnvVarUpsert) ClearDescription() *EnvVarUpsert { + u.SetNull(envvar.FieldDescription) + return u +} + +// SetSensitive sets the "sensitive" field. +func (u *EnvVarUpsert) SetSensitive(v bool) *EnvVarUpsert { + u.Set(envvar.FieldSensitive, v) + return u +} + +// UpdateSensitive sets the "sensitive" field to the value that was provided on create. +func (u *EnvVarUpsert) UpdateSensitive() *EnvVarUpsert { + u.SetExcluded(envvar.FieldSensitive) + return u +} + +// SetInjectionMode sets the "injection_mode" field. +func (u *EnvVarUpsert) SetInjectionMode(v envvar.InjectionMode) *EnvVarUpsert { + u.Set(envvar.FieldInjectionMode, v) + return u +} + +// UpdateInjectionMode sets the "injection_mode" field to the value that was provided on create. +func (u *EnvVarUpsert) UpdateInjectionMode() *EnvVarUpsert { + u.SetExcluded(envvar.FieldInjectionMode) + return u +} + +// SetSecret sets the "secret" field. +func (u *EnvVarUpsert) SetSecret(v bool) *EnvVarUpsert { + u.Set(envvar.FieldSecret, v) + return u +} + +// UpdateSecret sets the "secret" field to the value that was provided on create. +func (u *EnvVarUpsert) UpdateSecret() *EnvVarUpsert { + u.SetExcluded(envvar.FieldSecret) + return u +} + +// SetCreatedBy sets the "created_by" field. +func (u *EnvVarUpsert) SetCreatedBy(v string) *EnvVarUpsert { + u.Set(envvar.FieldCreatedBy, v) + return u +} + +// UpdateCreatedBy sets the "created_by" field to the value that was provided on create. +func (u *EnvVarUpsert) UpdateCreatedBy() *EnvVarUpsert { + u.SetExcluded(envvar.FieldCreatedBy) + return u +} + +// ClearCreatedBy clears the value of the "created_by" field. +func (u *EnvVarUpsert) ClearCreatedBy() *EnvVarUpsert { + u.SetNull(envvar.FieldCreatedBy) + return u +} + +// SetUpdated sets the "updated" field. +func (u *EnvVarUpsert) SetUpdated(v time.Time) *EnvVarUpsert { + u.Set(envvar.FieldUpdated, v) + return u +} + +// UpdateUpdated sets the "updated" field to the value that was provided on create. +func (u *EnvVarUpsert) UpdateUpdated() *EnvVarUpsert { + u.SetExcluded(envvar.FieldUpdated) + return u +} + +// UpdateNewValues updates the mutable fields using the new values that were set on create except the ID field. +// Using this option is equivalent to using: +// +// client.EnvVar.Create(). +// OnConflict( +// sql.ResolveWithNewValues(), +// sql.ResolveWith(func(u *sql.UpdateSet) { +// u.SetIgnore(envvar.FieldID) +// }), +// ). +// Exec(ctx) +func (u *EnvVarUpsertOne) UpdateNewValues() *EnvVarUpsertOne { + u.create.conflict = append(u.create.conflict, sql.ResolveWithNewValues()) + u.create.conflict = append(u.create.conflict, sql.ResolveWith(func(s *sql.UpdateSet) { + if _, exists := u.create.mutation.ID(); exists { + s.SetIgnore(envvar.FieldID) + } + if _, exists := u.create.mutation.Created(); exists { + s.SetIgnore(envvar.FieldCreated) + } + })) + return u +} + +// Ignore sets each column to itself in case of conflict. +// Using this option is equivalent to using: +// +// client.EnvVar.Create(). +// OnConflict(sql.ResolveWithIgnore()). +// Exec(ctx) +func (u *EnvVarUpsertOne) Ignore() *EnvVarUpsertOne { + u.create.conflict = append(u.create.conflict, sql.ResolveWithIgnore()) + return u +} + +// DoNothing configures the conflict_action to `DO NOTHING`. +// Supported only by SQLite and PostgreSQL. +func (u *EnvVarUpsertOne) DoNothing() *EnvVarUpsertOne { + u.create.conflict = append(u.create.conflict, sql.DoNothing()) + return u +} + +// Update allows overriding fields `UPDATE` values. See the EnvVarCreate.OnConflict +// documentation for more info. +func (u *EnvVarUpsertOne) Update(set func(*EnvVarUpsert)) *EnvVarUpsertOne { + u.create.conflict = append(u.create.conflict, sql.ResolveWith(func(update *sql.UpdateSet) { + set(&EnvVarUpsert{UpdateSet: update}) + })) + return u +} + +// SetKey sets the "key" field. +func (u *EnvVarUpsertOne) SetKey(v string) *EnvVarUpsertOne { + return u.Update(func(s *EnvVarUpsert) { + s.SetKey(v) + }) +} + +// UpdateKey sets the "key" field to the value that was provided on create. +func (u *EnvVarUpsertOne) UpdateKey() *EnvVarUpsertOne { + return u.Update(func(s *EnvVarUpsert) { + s.UpdateKey() + }) +} + +// SetValue sets the "value" field. +func (u *EnvVarUpsertOne) SetValue(v string) *EnvVarUpsertOne { + return u.Update(func(s *EnvVarUpsert) { + s.SetValue(v) + }) +} + +// UpdateValue sets the "value" field to the value that was provided on create. +func (u *EnvVarUpsertOne) UpdateValue() *EnvVarUpsertOne { + return u.Update(func(s *EnvVarUpsert) { + s.UpdateValue() + }) +} + +// SetScope sets the "scope" field. +func (u *EnvVarUpsertOne) SetScope(v string) *EnvVarUpsertOne { + return u.Update(func(s *EnvVarUpsert) { + s.SetScope(v) + }) +} + +// UpdateScope sets the "scope" field to the value that was provided on create. +func (u *EnvVarUpsertOne) UpdateScope() *EnvVarUpsertOne { + return u.Update(func(s *EnvVarUpsert) { + s.UpdateScope() + }) +} + +// SetScopeID sets the "scope_id" field. +func (u *EnvVarUpsertOne) SetScopeID(v string) *EnvVarUpsertOne { + return u.Update(func(s *EnvVarUpsert) { + s.SetScopeID(v) + }) +} + +// UpdateScopeID sets the "scope_id" field to the value that was provided on create. +func (u *EnvVarUpsertOne) UpdateScopeID() *EnvVarUpsertOne { + return u.Update(func(s *EnvVarUpsert) { + s.UpdateScopeID() + }) +} + +// SetDescription sets the "description" field. +func (u *EnvVarUpsertOne) SetDescription(v string) *EnvVarUpsertOne { + return u.Update(func(s *EnvVarUpsert) { + s.SetDescription(v) + }) +} + +// UpdateDescription sets the "description" field to the value that was provided on create. +func (u *EnvVarUpsertOne) UpdateDescription() *EnvVarUpsertOne { + return u.Update(func(s *EnvVarUpsert) { + s.UpdateDescription() + }) +} + +// ClearDescription clears the value of the "description" field. +func (u *EnvVarUpsertOne) ClearDescription() *EnvVarUpsertOne { + return u.Update(func(s *EnvVarUpsert) { + s.ClearDescription() + }) +} + +// SetSensitive sets the "sensitive" field. +func (u *EnvVarUpsertOne) SetSensitive(v bool) *EnvVarUpsertOne { + return u.Update(func(s *EnvVarUpsert) { + s.SetSensitive(v) + }) +} + +// UpdateSensitive sets the "sensitive" field to the value that was provided on create. +func (u *EnvVarUpsertOne) UpdateSensitive() *EnvVarUpsertOne { + return u.Update(func(s *EnvVarUpsert) { + s.UpdateSensitive() + }) +} + +// SetInjectionMode sets the "injection_mode" field. +func (u *EnvVarUpsertOne) SetInjectionMode(v envvar.InjectionMode) *EnvVarUpsertOne { + return u.Update(func(s *EnvVarUpsert) { + s.SetInjectionMode(v) + }) +} + +// UpdateInjectionMode sets the "injection_mode" field to the value that was provided on create. +func (u *EnvVarUpsertOne) UpdateInjectionMode() *EnvVarUpsertOne { + return u.Update(func(s *EnvVarUpsert) { + s.UpdateInjectionMode() + }) +} + +// SetSecret sets the "secret" field. +func (u *EnvVarUpsertOne) SetSecret(v bool) *EnvVarUpsertOne { + return u.Update(func(s *EnvVarUpsert) { + s.SetSecret(v) + }) +} + +// UpdateSecret sets the "secret" field to the value that was provided on create. +func (u *EnvVarUpsertOne) UpdateSecret() *EnvVarUpsertOne { + return u.Update(func(s *EnvVarUpsert) { + s.UpdateSecret() + }) +} + +// SetCreatedBy sets the "created_by" field. +func (u *EnvVarUpsertOne) SetCreatedBy(v string) *EnvVarUpsertOne { + return u.Update(func(s *EnvVarUpsert) { + s.SetCreatedBy(v) + }) +} + +// UpdateCreatedBy sets the "created_by" field to the value that was provided on create. +func (u *EnvVarUpsertOne) UpdateCreatedBy() *EnvVarUpsertOne { + return u.Update(func(s *EnvVarUpsert) { + s.UpdateCreatedBy() + }) +} + +// ClearCreatedBy clears the value of the "created_by" field. +func (u *EnvVarUpsertOne) ClearCreatedBy() *EnvVarUpsertOne { + return u.Update(func(s *EnvVarUpsert) { + s.ClearCreatedBy() + }) +} + +// SetUpdated sets the "updated" field. +func (u *EnvVarUpsertOne) SetUpdated(v time.Time) *EnvVarUpsertOne { + return u.Update(func(s *EnvVarUpsert) { + s.SetUpdated(v) + }) +} + +// UpdateUpdated sets the "updated" field to the value that was provided on create. +func (u *EnvVarUpsertOne) UpdateUpdated() *EnvVarUpsertOne { + return u.Update(func(s *EnvVarUpsert) { + s.UpdateUpdated() + }) +} + +// Exec executes the query. +func (u *EnvVarUpsertOne) Exec(ctx context.Context) error { + if len(u.create.conflict) == 0 { + return errors.New("ent: missing options for EnvVarCreate.OnConflict") + } + return u.create.Exec(ctx) +} + +// ExecX is like Exec, but panics if an error occurs. +func (u *EnvVarUpsertOne) ExecX(ctx context.Context) { + if err := u.create.Exec(ctx); err != nil { + panic(err) + } +} + +// Exec executes the UPSERT query and returns the inserted/updated ID. +func (u *EnvVarUpsertOne) ID(ctx context.Context) (id uuid.UUID, err error) { + if u.create.driver.Dialect() == dialect.MySQL { + // In case of "ON CONFLICT", there is no way to get back non-numeric ID + // fields from the database since MySQL does not support the RETURNING clause. + return id, errors.New("ent: EnvVarUpsertOne.ID is not supported by MySQL driver. Use EnvVarUpsertOne.Exec instead") + } + node, err := u.create.Save(ctx) + if err != nil { + return id, err + } + return node.ID, nil +} + +// IDX is like ID, but panics if an error occurs. +func (u *EnvVarUpsertOne) IDX(ctx context.Context) uuid.UUID { + id, err := u.ID(ctx) + if err != nil { + panic(err) + } + return id +} + +// EnvVarCreateBulk is the builder for creating many EnvVar entities in bulk. +type EnvVarCreateBulk struct { + config + err error + builders []*EnvVarCreate + conflict []sql.ConflictOption +} + +// Save creates the EnvVar entities in the database. +func (_c *EnvVarCreateBulk) Save(ctx context.Context) ([]*EnvVar, error) { + if _c.err != nil { + return nil, _c.err + } + specs := make([]*sqlgraph.CreateSpec, len(_c.builders)) + nodes := make([]*EnvVar, len(_c.builders)) + mutators := make([]Mutator, len(_c.builders)) + for i := range _c.builders { + func(i int, root context.Context) { + builder := _c.builders[i] + builder.defaults() + var mut Mutator = MutateFunc(func(ctx context.Context, m Mutation) (Value, error) { + mutation, ok := m.(*EnvVarMutation) + if !ok { + return nil, fmt.Errorf("unexpected mutation type %T", m) + } + if err := builder.check(); err != nil { + return nil, err + } + builder.mutation = mutation + var err error + nodes[i], specs[i] = builder.createSpec() + if i < len(mutators)-1 { + _, err = mutators[i+1].Mutate(root, _c.builders[i+1].mutation) + } else { + spec := &sqlgraph.BatchCreateSpec{Nodes: specs} + spec.OnConflict = _c.conflict + // Invoke the actual operation on the latest mutation in the chain. + if err = sqlgraph.BatchCreate(ctx, _c.driver, spec); err != nil { + if sqlgraph.IsConstraintError(err) { + err = &ConstraintError{msg: err.Error(), wrap: err} + } + } + } + if err != nil { + return nil, err + } + mutation.id = &nodes[i].ID + mutation.done = true + return nodes[i], nil + }) + for i := len(builder.hooks) - 1; i >= 0; i-- { + mut = builder.hooks[i](mut) + } + mutators[i] = mut + }(i, ctx) + } + if len(mutators) > 0 { + if _, err := mutators[0].Mutate(ctx, _c.builders[0].mutation); err != nil { + return nil, err + } + } + return nodes, nil +} + +// SaveX is like Save, but panics if an error occurs. +func (_c *EnvVarCreateBulk) SaveX(ctx context.Context) []*EnvVar { + v, err := _c.Save(ctx) + if err != nil { + panic(err) + } + return v +} + +// Exec executes the query. +func (_c *EnvVarCreateBulk) Exec(ctx context.Context) error { + _, err := _c.Save(ctx) + return err +} + +// ExecX is like Exec, but panics if an error occurs. +func (_c *EnvVarCreateBulk) ExecX(ctx context.Context) { + if err := _c.Exec(ctx); err != nil { + panic(err) + } +} + +// OnConflict allows configuring the `ON CONFLICT` / `ON DUPLICATE KEY` clause +// of the `INSERT` statement. For example: +// +// client.EnvVar.CreateBulk(builders...). +// OnConflict( +// // Update the row with the new values +// // the was proposed for insertion. +// sql.ResolveWithNewValues(), +// ). +// // Override some of the fields with custom +// // update values. +// Update(func(u *ent.EnvVarUpsert) { +// SetKey(v+v). +// }). +// Exec(ctx) +func (_c *EnvVarCreateBulk) OnConflict(opts ...sql.ConflictOption) *EnvVarUpsertBulk { + _c.conflict = opts + return &EnvVarUpsertBulk{ + create: _c, + } +} + +// OnConflictColumns calls `OnConflict` and configures the columns +// as conflict target. Using this option is equivalent to using: +// +// client.EnvVar.Create(). +// OnConflict(sql.ConflictColumns(columns...)). +// Exec(ctx) +func (_c *EnvVarCreateBulk) OnConflictColumns(columns ...string) *EnvVarUpsertBulk { + _c.conflict = append(_c.conflict, sql.ConflictColumns(columns...)) + return &EnvVarUpsertBulk{ + create: _c, + } +} + +// EnvVarUpsertBulk is the builder for "upsert"-ing +// a bulk of EnvVar nodes. +type EnvVarUpsertBulk struct { + create *EnvVarCreateBulk +} + +// UpdateNewValues updates the mutable fields using the new values that +// were set on create. Using this option is equivalent to using: +// +// client.EnvVar.Create(). +// OnConflict( +// sql.ResolveWithNewValues(), +// sql.ResolveWith(func(u *sql.UpdateSet) { +// u.SetIgnore(envvar.FieldID) +// }), +// ). +// Exec(ctx) +func (u *EnvVarUpsertBulk) UpdateNewValues() *EnvVarUpsertBulk { + u.create.conflict = append(u.create.conflict, sql.ResolveWithNewValues()) + u.create.conflict = append(u.create.conflict, sql.ResolveWith(func(s *sql.UpdateSet) { + for _, b := range u.create.builders { + if _, exists := b.mutation.ID(); exists { + s.SetIgnore(envvar.FieldID) + } + if _, exists := b.mutation.Created(); exists { + s.SetIgnore(envvar.FieldCreated) + } + } + })) + return u +} + +// Ignore sets each column to itself in case of conflict. +// Using this option is equivalent to using: +// +// client.EnvVar.Create(). +// OnConflict(sql.ResolveWithIgnore()). +// Exec(ctx) +func (u *EnvVarUpsertBulk) Ignore() *EnvVarUpsertBulk { + u.create.conflict = append(u.create.conflict, sql.ResolveWithIgnore()) + return u +} + +// DoNothing configures the conflict_action to `DO NOTHING`. +// Supported only by SQLite and PostgreSQL. +func (u *EnvVarUpsertBulk) DoNothing() *EnvVarUpsertBulk { + u.create.conflict = append(u.create.conflict, sql.DoNothing()) + return u +} + +// Update allows overriding fields `UPDATE` values. See the EnvVarCreateBulk.OnConflict +// documentation for more info. +func (u *EnvVarUpsertBulk) Update(set func(*EnvVarUpsert)) *EnvVarUpsertBulk { + u.create.conflict = append(u.create.conflict, sql.ResolveWith(func(update *sql.UpdateSet) { + set(&EnvVarUpsert{UpdateSet: update}) + })) + return u +} + +// SetKey sets the "key" field. +func (u *EnvVarUpsertBulk) SetKey(v string) *EnvVarUpsertBulk { + return u.Update(func(s *EnvVarUpsert) { + s.SetKey(v) + }) +} + +// UpdateKey sets the "key" field to the value that was provided on create. +func (u *EnvVarUpsertBulk) UpdateKey() *EnvVarUpsertBulk { + return u.Update(func(s *EnvVarUpsert) { + s.UpdateKey() + }) +} + +// SetValue sets the "value" field. +func (u *EnvVarUpsertBulk) SetValue(v string) *EnvVarUpsertBulk { + return u.Update(func(s *EnvVarUpsert) { + s.SetValue(v) + }) +} + +// UpdateValue sets the "value" field to the value that was provided on create. +func (u *EnvVarUpsertBulk) UpdateValue() *EnvVarUpsertBulk { + return u.Update(func(s *EnvVarUpsert) { + s.UpdateValue() + }) +} + +// SetScope sets the "scope" field. +func (u *EnvVarUpsertBulk) SetScope(v string) *EnvVarUpsertBulk { + return u.Update(func(s *EnvVarUpsert) { + s.SetScope(v) + }) +} + +// UpdateScope sets the "scope" field to the value that was provided on create. +func (u *EnvVarUpsertBulk) UpdateScope() *EnvVarUpsertBulk { + return u.Update(func(s *EnvVarUpsert) { + s.UpdateScope() + }) +} + +// SetScopeID sets the "scope_id" field. +func (u *EnvVarUpsertBulk) SetScopeID(v string) *EnvVarUpsertBulk { + return u.Update(func(s *EnvVarUpsert) { + s.SetScopeID(v) + }) +} + +// UpdateScopeID sets the "scope_id" field to the value that was provided on create. +func (u *EnvVarUpsertBulk) UpdateScopeID() *EnvVarUpsertBulk { + return u.Update(func(s *EnvVarUpsert) { + s.UpdateScopeID() + }) +} + +// SetDescription sets the "description" field. +func (u *EnvVarUpsertBulk) SetDescription(v string) *EnvVarUpsertBulk { + return u.Update(func(s *EnvVarUpsert) { + s.SetDescription(v) + }) +} + +// UpdateDescription sets the "description" field to the value that was provided on create. +func (u *EnvVarUpsertBulk) UpdateDescription() *EnvVarUpsertBulk { + return u.Update(func(s *EnvVarUpsert) { + s.UpdateDescription() + }) +} + +// ClearDescription clears the value of the "description" field. +func (u *EnvVarUpsertBulk) ClearDescription() *EnvVarUpsertBulk { + return u.Update(func(s *EnvVarUpsert) { + s.ClearDescription() + }) +} + +// SetSensitive sets the "sensitive" field. +func (u *EnvVarUpsertBulk) SetSensitive(v bool) *EnvVarUpsertBulk { + return u.Update(func(s *EnvVarUpsert) { + s.SetSensitive(v) + }) +} + +// UpdateSensitive sets the "sensitive" field to the value that was provided on create. +func (u *EnvVarUpsertBulk) UpdateSensitive() *EnvVarUpsertBulk { + return u.Update(func(s *EnvVarUpsert) { + s.UpdateSensitive() + }) +} + +// SetInjectionMode sets the "injection_mode" field. +func (u *EnvVarUpsertBulk) SetInjectionMode(v envvar.InjectionMode) *EnvVarUpsertBulk { + return u.Update(func(s *EnvVarUpsert) { + s.SetInjectionMode(v) + }) +} + +// UpdateInjectionMode sets the "injection_mode" field to the value that was provided on create. +func (u *EnvVarUpsertBulk) UpdateInjectionMode() *EnvVarUpsertBulk { + return u.Update(func(s *EnvVarUpsert) { + s.UpdateInjectionMode() + }) +} + +// SetSecret sets the "secret" field. +func (u *EnvVarUpsertBulk) SetSecret(v bool) *EnvVarUpsertBulk { + return u.Update(func(s *EnvVarUpsert) { + s.SetSecret(v) + }) +} + +// UpdateSecret sets the "secret" field to the value that was provided on create. +func (u *EnvVarUpsertBulk) UpdateSecret() *EnvVarUpsertBulk { + return u.Update(func(s *EnvVarUpsert) { + s.UpdateSecret() + }) +} + +// SetCreatedBy sets the "created_by" field. +func (u *EnvVarUpsertBulk) SetCreatedBy(v string) *EnvVarUpsertBulk { + return u.Update(func(s *EnvVarUpsert) { + s.SetCreatedBy(v) + }) +} + +// UpdateCreatedBy sets the "created_by" field to the value that was provided on create. +func (u *EnvVarUpsertBulk) UpdateCreatedBy() *EnvVarUpsertBulk { + return u.Update(func(s *EnvVarUpsert) { + s.UpdateCreatedBy() + }) +} + +// ClearCreatedBy clears the value of the "created_by" field. +func (u *EnvVarUpsertBulk) ClearCreatedBy() *EnvVarUpsertBulk { + return u.Update(func(s *EnvVarUpsert) { + s.ClearCreatedBy() + }) +} + +// SetUpdated sets the "updated" field. +func (u *EnvVarUpsertBulk) SetUpdated(v time.Time) *EnvVarUpsertBulk { + return u.Update(func(s *EnvVarUpsert) { + s.SetUpdated(v) + }) +} + +// UpdateUpdated sets the "updated" field to the value that was provided on create. +func (u *EnvVarUpsertBulk) UpdateUpdated() *EnvVarUpsertBulk { + return u.Update(func(s *EnvVarUpsert) { + s.UpdateUpdated() + }) +} + +// Exec executes the query. +func (u *EnvVarUpsertBulk) Exec(ctx context.Context) error { + if u.create.err != nil { + return u.create.err + } + for i, b := range u.create.builders { + if len(b.conflict) != 0 { + return fmt.Errorf("ent: OnConflict was set for builder %d. Set it on the EnvVarCreateBulk instead", i) + } + } + if len(u.create.conflict) == 0 { + return errors.New("ent: missing options for EnvVarCreateBulk.OnConflict") + } + return u.create.Exec(ctx) +} + +// ExecX is like Exec, but panics if an error occurs. +func (u *EnvVarUpsertBulk) ExecX(ctx context.Context) { + if err := u.create.Exec(ctx); err != nil { + panic(err) + } +} diff --git a/pkg/ent/envvar_delete.go b/pkg/ent/envvar_delete.go new file mode 100644 index 000000000..5e5c80afa --- /dev/null +++ b/pkg/ent/envvar_delete.go @@ -0,0 +1,88 @@ +// Code generated by ent, DO NOT EDIT. + +package ent + +import ( + "context" + + "entgo.io/ent/dialect/sql" + "entgo.io/ent/dialect/sql/sqlgraph" + "entgo.io/ent/schema/field" + "github.com/GoogleCloudPlatform/scion/pkg/ent/envvar" + "github.com/GoogleCloudPlatform/scion/pkg/ent/predicate" +) + +// EnvVarDelete is the builder for deleting a EnvVar entity. +type EnvVarDelete struct { + config + hooks []Hook + mutation *EnvVarMutation +} + +// Where appends a list predicates to the EnvVarDelete builder. +func (_d *EnvVarDelete) Where(ps ...predicate.EnvVar) *EnvVarDelete { + _d.mutation.Where(ps...) + return _d +} + +// Exec executes the deletion query and returns how many vertices were deleted. +func (_d *EnvVarDelete) Exec(ctx context.Context) (int, error) { + return withHooks(ctx, _d.sqlExec, _d.mutation, _d.hooks) +} + +// ExecX is like Exec, but panics if an error occurs. +func (_d *EnvVarDelete) ExecX(ctx context.Context) int { + n, err := _d.Exec(ctx) + if err != nil { + panic(err) + } + return n +} + +func (_d *EnvVarDelete) sqlExec(ctx context.Context) (int, error) { + _spec := sqlgraph.NewDeleteSpec(envvar.Table, sqlgraph.NewFieldSpec(envvar.FieldID, field.TypeUUID)) + if ps := _d.mutation.predicates; len(ps) > 0 { + _spec.Predicate = func(selector *sql.Selector) { + for i := range ps { + ps[i](selector) + } + } + } + affected, err := sqlgraph.DeleteNodes(ctx, _d.driver, _spec) + if err != nil && sqlgraph.IsConstraintError(err) { + err = &ConstraintError{msg: err.Error(), wrap: err} + } + _d.mutation.done = true + return affected, err +} + +// EnvVarDeleteOne is the builder for deleting a single EnvVar entity. +type EnvVarDeleteOne struct { + _d *EnvVarDelete +} + +// Where appends a list predicates to the EnvVarDelete builder. +func (_d *EnvVarDeleteOne) Where(ps ...predicate.EnvVar) *EnvVarDeleteOne { + _d._d.mutation.Where(ps...) + return _d +} + +// Exec executes the deletion query. +func (_d *EnvVarDeleteOne) Exec(ctx context.Context) error { + n, err := _d._d.Exec(ctx) + switch { + case err != nil: + return err + case n == 0: + return &NotFoundError{envvar.Label} + default: + return nil + } +} + +// ExecX is like Exec, but panics if an error occurs. +func (_d *EnvVarDeleteOne) ExecX(ctx context.Context) { + if err := _d.Exec(ctx); err != nil { + panic(err) + } +} diff --git a/pkg/ent/envvar_query.go b/pkg/ent/envvar_query.go new file mode 100644 index 000000000..d155d76b3 --- /dev/null +++ b/pkg/ent/envvar_query.go @@ -0,0 +1,565 @@ +// Code generated by ent, DO NOT EDIT. + +package ent + +import ( + "context" + "fmt" + "math" + + "entgo.io/ent" + "entgo.io/ent/dialect" + "entgo.io/ent/dialect/sql" + "entgo.io/ent/dialect/sql/sqlgraph" + "entgo.io/ent/schema/field" + "github.com/GoogleCloudPlatform/scion/pkg/ent/envvar" + "github.com/GoogleCloudPlatform/scion/pkg/ent/predicate" + "github.com/google/uuid" +) + +// EnvVarQuery is the builder for querying EnvVar entities. +type EnvVarQuery struct { + config + ctx *QueryContext + order []envvar.OrderOption + inters []Interceptor + predicates []predicate.EnvVar + modifiers []func(*sql.Selector) + // intermediate query (i.e. traversal path). + sql *sql.Selector + path func(context.Context) (*sql.Selector, error) +} + +// Where adds a new predicate for the EnvVarQuery builder. +func (_q *EnvVarQuery) Where(ps ...predicate.EnvVar) *EnvVarQuery { + _q.predicates = append(_q.predicates, ps...) + return _q +} + +// Limit the number of records to be returned by this query. +func (_q *EnvVarQuery) Limit(limit int) *EnvVarQuery { + _q.ctx.Limit = &limit + return _q +} + +// Offset to start from. +func (_q *EnvVarQuery) Offset(offset int) *EnvVarQuery { + _q.ctx.Offset = &offset + return _q +} + +// Unique configures the query builder to filter duplicate records on query. +// By default, unique is set to true, and can be disabled using this method. +func (_q *EnvVarQuery) Unique(unique bool) *EnvVarQuery { + _q.ctx.Unique = &unique + return _q +} + +// Order specifies how the records should be ordered. +func (_q *EnvVarQuery) Order(o ...envvar.OrderOption) *EnvVarQuery { + _q.order = append(_q.order, o...) + return _q +} + +// First returns the first EnvVar entity from the query. +// Returns a *NotFoundError when no EnvVar was found. +func (_q *EnvVarQuery) First(ctx context.Context) (*EnvVar, error) { + nodes, err := _q.Limit(1).All(setContextOp(ctx, _q.ctx, ent.OpQueryFirst)) + if err != nil { + return nil, err + } + if len(nodes) == 0 { + return nil, &NotFoundError{envvar.Label} + } + return nodes[0], nil +} + +// FirstX is like First, but panics if an error occurs. +func (_q *EnvVarQuery) FirstX(ctx context.Context) *EnvVar { + node, err := _q.First(ctx) + if err != nil && !IsNotFound(err) { + panic(err) + } + return node +} + +// FirstID returns the first EnvVar ID from the query. +// Returns a *NotFoundError when no EnvVar ID was found. +func (_q *EnvVarQuery) FirstID(ctx context.Context) (id uuid.UUID, err error) { + var ids []uuid.UUID + if ids, err = _q.Limit(1).IDs(setContextOp(ctx, _q.ctx, ent.OpQueryFirstID)); err != nil { + return + } + if len(ids) == 0 { + err = &NotFoundError{envvar.Label} + return + } + return ids[0], nil +} + +// FirstIDX is like FirstID, but panics if an error occurs. +func (_q *EnvVarQuery) FirstIDX(ctx context.Context) uuid.UUID { + id, err := _q.FirstID(ctx) + if err != nil && !IsNotFound(err) { + panic(err) + } + return id +} + +// Only returns a single EnvVar entity found by the query, ensuring it only returns one. +// Returns a *NotSingularError when more than one EnvVar entity is found. +// Returns a *NotFoundError when no EnvVar entities are found. +func (_q *EnvVarQuery) Only(ctx context.Context) (*EnvVar, error) { + nodes, err := _q.Limit(2).All(setContextOp(ctx, _q.ctx, ent.OpQueryOnly)) + if err != nil { + return nil, err + } + switch len(nodes) { + case 1: + return nodes[0], nil + case 0: + return nil, &NotFoundError{envvar.Label} + default: + return nil, &NotSingularError{envvar.Label} + } +} + +// OnlyX is like Only, but panics if an error occurs. +func (_q *EnvVarQuery) OnlyX(ctx context.Context) *EnvVar { + node, err := _q.Only(ctx) + if err != nil { + panic(err) + } + return node +} + +// OnlyID is like Only, but returns the only EnvVar ID in the query. +// Returns a *NotSingularError when more than one EnvVar ID is found. +// Returns a *NotFoundError when no entities are found. +func (_q *EnvVarQuery) OnlyID(ctx context.Context) (id uuid.UUID, err error) { + var ids []uuid.UUID + if ids, err = _q.Limit(2).IDs(setContextOp(ctx, _q.ctx, ent.OpQueryOnlyID)); err != nil { + return + } + switch len(ids) { + case 1: + id = ids[0] + case 0: + err = &NotFoundError{envvar.Label} + default: + err = &NotSingularError{envvar.Label} + } + return +} + +// OnlyIDX is like OnlyID, but panics if an error occurs. +func (_q *EnvVarQuery) OnlyIDX(ctx context.Context) uuid.UUID { + id, err := _q.OnlyID(ctx) + if err != nil { + panic(err) + } + return id +} + +// All executes the query and returns a list of EnvVars. +func (_q *EnvVarQuery) All(ctx context.Context) ([]*EnvVar, error) { + ctx = setContextOp(ctx, _q.ctx, ent.OpQueryAll) + if err := _q.prepareQuery(ctx); err != nil { + return nil, err + } + qr := querierAll[[]*EnvVar, *EnvVarQuery]() + return withInterceptors[[]*EnvVar](ctx, _q, qr, _q.inters) +} + +// AllX is like All, but panics if an error occurs. +func (_q *EnvVarQuery) AllX(ctx context.Context) []*EnvVar { + nodes, err := _q.All(ctx) + if err != nil { + panic(err) + } + return nodes +} + +// IDs executes the query and returns a list of EnvVar IDs. +func (_q *EnvVarQuery) IDs(ctx context.Context) (ids []uuid.UUID, err error) { + if _q.ctx.Unique == nil && _q.path != nil { + _q.Unique(true) + } + ctx = setContextOp(ctx, _q.ctx, ent.OpQueryIDs) + if err = _q.Select(envvar.FieldID).Scan(ctx, &ids); err != nil { + return nil, err + } + return ids, nil +} + +// IDsX is like IDs, but panics if an error occurs. +func (_q *EnvVarQuery) IDsX(ctx context.Context) []uuid.UUID { + ids, err := _q.IDs(ctx) + if err != nil { + panic(err) + } + return ids +} + +// Count returns the count of the given query. +func (_q *EnvVarQuery) Count(ctx context.Context) (int, error) { + ctx = setContextOp(ctx, _q.ctx, ent.OpQueryCount) + if err := _q.prepareQuery(ctx); err != nil { + return 0, err + } + return withInterceptors[int](ctx, _q, querierCount[*EnvVarQuery](), _q.inters) +} + +// CountX is like Count, but panics if an error occurs. +func (_q *EnvVarQuery) CountX(ctx context.Context) int { + count, err := _q.Count(ctx) + if err != nil { + panic(err) + } + return count +} + +// Exist returns true if the query has elements in the graph. +func (_q *EnvVarQuery) Exist(ctx context.Context) (bool, error) { + ctx = setContextOp(ctx, _q.ctx, ent.OpQueryExist) + switch _, err := _q.FirstID(ctx); { + case IsNotFound(err): + return false, nil + case err != nil: + return false, fmt.Errorf("ent: check existence: %w", err) + default: + return true, nil + } +} + +// ExistX is like Exist, but panics if an error occurs. +func (_q *EnvVarQuery) ExistX(ctx context.Context) bool { + exist, err := _q.Exist(ctx) + if err != nil { + panic(err) + } + return exist +} + +// Clone returns a duplicate of the EnvVarQuery builder, including all associated steps. It can be +// used to prepare common query builders and use them differently after the clone is made. +func (_q *EnvVarQuery) Clone() *EnvVarQuery { + if _q == nil { + return nil + } + return &EnvVarQuery{ + config: _q.config, + ctx: _q.ctx.Clone(), + order: append([]envvar.OrderOption{}, _q.order...), + inters: append([]Interceptor{}, _q.inters...), + predicates: append([]predicate.EnvVar{}, _q.predicates...), + // clone intermediate query. + sql: _q.sql.Clone(), + path: _q.path, + } +} + +// GroupBy is used to group vertices by one or more fields/columns. +// It is often used with aggregate functions, like: count, max, mean, min, sum. +// +// Example: +// +// var v []struct { +// Key string `json:"key,omitempty"` +// Count int `json:"count,omitempty"` +// } +// +// client.EnvVar.Query(). +// GroupBy(envvar.FieldKey). +// Aggregate(ent.Count()). +// Scan(ctx, &v) +func (_q *EnvVarQuery) GroupBy(field string, fields ...string) *EnvVarGroupBy { + _q.ctx.Fields = append([]string{field}, fields...) + grbuild := &EnvVarGroupBy{build: _q} + grbuild.flds = &_q.ctx.Fields + grbuild.label = envvar.Label + grbuild.scan = grbuild.Scan + return grbuild +} + +// Select allows the selection one or more fields/columns for the given query, +// instead of selecting all fields in the entity. +// +// Example: +// +// var v []struct { +// Key string `json:"key,omitempty"` +// } +// +// client.EnvVar.Query(). +// Select(envvar.FieldKey). +// Scan(ctx, &v) +func (_q *EnvVarQuery) Select(fields ...string) *EnvVarSelect { + _q.ctx.Fields = append(_q.ctx.Fields, fields...) + sbuild := &EnvVarSelect{EnvVarQuery: _q} + sbuild.label = envvar.Label + sbuild.flds, sbuild.scan = &_q.ctx.Fields, sbuild.Scan + return sbuild +} + +// Aggregate returns a EnvVarSelect configured with the given aggregations. +func (_q *EnvVarQuery) Aggregate(fns ...AggregateFunc) *EnvVarSelect { + return _q.Select().Aggregate(fns...) +} + +func (_q *EnvVarQuery) prepareQuery(ctx context.Context) error { + for _, inter := range _q.inters { + if inter == nil { + return fmt.Errorf("ent: uninitialized interceptor (forgotten import ent/runtime?)") + } + if trv, ok := inter.(Traverser); ok { + if err := trv.Traverse(ctx, _q); err != nil { + return err + } + } + } + for _, f := range _q.ctx.Fields { + if !envvar.ValidColumn(f) { + return &ValidationError{Name: f, err: fmt.Errorf("ent: invalid field %q for query", f)} + } + } + if _q.path != nil { + prev, err := _q.path(ctx) + if err != nil { + return err + } + _q.sql = prev + } + return nil +} + +func (_q *EnvVarQuery) sqlAll(ctx context.Context, hooks ...queryHook) ([]*EnvVar, error) { + var ( + nodes = []*EnvVar{} + _spec = _q.querySpec() + ) + _spec.ScanValues = func(columns []string) ([]any, error) { + return (*EnvVar).scanValues(nil, columns) + } + _spec.Assign = func(columns []string, values []any) error { + node := &EnvVar{config: _q.config} + nodes = append(nodes, node) + return node.assignValues(columns, values) + } + if len(_q.modifiers) > 0 { + _spec.Modifiers = _q.modifiers + } + for i := range hooks { + hooks[i](ctx, _spec) + } + if err := sqlgraph.QueryNodes(ctx, _q.driver, _spec); err != nil { + return nil, err + } + if len(nodes) == 0 { + return nodes, nil + } + return nodes, nil +} + +func (_q *EnvVarQuery) sqlCount(ctx context.Context) (int, error) { + _spec := _q.querySpec() + if len(_q.modifiers) > 0 { + _spec.Modifiers = _q.modifiers + } + _spec.Node.Columns = _q.ctx.Fields + if len(_q.ctx.Fields) > 0 { + _spec.Unique = _q.ctx.Unique != nil && *_q.ctx.Unique + } + return sqlgraph.CountNodes(ctx, _q.driver, _spec) +} + +func (_q *EnvVarQuery) querySpec() *sqlgraph.QuerySpec { + _spec := sqlgraph.NewQuerySpec(envvar.Table, envvar.Columns, sqlgraph.NewFieldSpec(envvar.FieldID, field.TypeUUID)) + _spec.From = _q.sql + if unique := _q.ctx.Unique; unique != nil { + _spec.Unique = *unique + } else if _q.path != nil { + _spec.Unique = true + } + if fields := _q.ctx.Fields; len(fields) > 0 { + _spec.Node.Columns = make([]string, 0, len(fields)) + _spec.Node.Columns = append(_spec.Node.Columns, envvar.FieldID) + for i := range fields { + if fields[i] != envvar.FieldID { + _spec.Node.Columns = append(_spec.Node.Columns, fields[i]) + } + } + } + if ps := _q.predicates; len(ps) > 0 { + _spec.Predicate = func(selector *sql.Selector) { + for i := range ps { + ps[i](selector) + } + } + } + if limit := _q.ctx.Limit; limit != nil { + _spec.Limit = *limit + } + if offset := _q.ctx.Offset; offset != nil { + _spec.Offset = *offset + } + if ps := _q.order; len(ps) > 0 { + _spec.Order = func(selector *sql.Selector) { + for i := range ps { + ps[i](selector) + } + } + } + return _spec +} + +func (_q *EnvVarQuery) sqlQuery(ctx context.Context) *sql.Selector { + builder := sql.Dialect(_q.driver.Dialect()) + t1 := builder.Table(envvar.Table) + columns := _q.ctx.Fields + if len(columns) == 0 { + columns = envvar.Columns + } + selector := builder.Select(t1.Columns(columns...)...).From(t1) + if _q.sql != nil { + selector = _q.sql + selector.Select(selector.Columns(columns...)...) + } + if _q.ctx.Unique != nil && *_q.ctx.Unique { + selector.Distinct() + } + for _, m := range _q.modifiers { + m(selector) + } + for _, p := range _q.predicates { + p(selector) + } + for _, p := range _q.order { + p(selector) + } + if offset := _q.ctx.Offset; offset != nil { + // limit is mandatory for offset clause. We start + // with default value, and override it below if needed. + selector.Offset(*offset).Limit(math.MaxInt32) + } + if limit := _q.ctx.Limit; limit != nil { + selector.Limit(*limit) + } + return selector +} + +// ForUpdate locks the selected rows against concurrent updates, and prevent them from being +// updated, deleted or "selected ... for update" by other sessions, until the transaction is +// either committed or rolled-back. +func (_q *EnvVarQuery) ForUpdate(opts ...sql.LockOption) *EnvVarQuery { + if _q.driver.Dialect() == dialect.Postgres { + _q.Unique(false) + } + _q.modifiers = append(_q.modifiers, func(s *sql.Selector) { + s.ForUpdate(opts...) + }) + return _q +} + +// ForShare behaves similarly to ForUpdate, except that it acquires a shared mode lock +// on any rows that are read. Other sessions can read the rows, but cannot modify them +// until your transaction commits. +func (_q *EnvVarQuery) ForShare(opts ...sql.LockOption) *EnvVarQuery { + if _q.driver.Dialect() == dialect.Postgres { + _q.Unique(false) + } + _q.modifiers = append(_q.modifiers, func(s *sql.Selector) { + s.ForShare(opts...) + }) + return _q +} + +// EnvVarGroupBy is the group-by builder for EnvVar entities. +type EnvVarGroupBy struct { + selector + build *EnvVarQuery +} + +// Aggregate adds the given aggregation functions to the group-by query. +func (_g *EnvVarGroupBy) Aggregate(fns ...AggregateFunc) *EnvVarGroupBy { + _g.fns = append(_g.fns, fns...) + return _g +} + +// Scan applies the selector query and scans the result into the given value. +func (_g *EnvVarGroupBy) Scan(ctx context.Context, v any) error { + ctx = setContextOp(ctx, _g.build.ctx, ent.OpQueryGroupBy) + if err := _g.build.prepareQuery(ctx); err != nil { + return err + } + return scanWithInterceptors[*EnvVarQuery, *EnvVarGroupBy](ctx, _g.build, _g, _g.build.inters, v) +} + +func (_g *EnvVarGroupBy) sqlScan(ctx context.Context, root *EnvVarQuery, v any) error { + selector := root.sqlQuery(ctx).Select() + aggregation := make([]string, 0, len(_g.fns)) + for _, fn := range _g.fns { + aggregation = append(aggregation, fn(selector)) + } + if len(selector.SelectedColumns()) == 0 { + columns := make([]string, 0, len(*_g.flds)+len(_g.fns)) + for _, f := range *_g.flds { + columns = append(columns, selector.C(f)) + } + columns = append(columns, aggregation...) + selector.Select(columns...) + } + selector.GroupBy(selector.Columns(*_g.flds...)...) + if err := selector.Err(); err != nil { + return err + } + rows := &sql.Rows{} + query, args := selector.Query() + if err := _g.build.driver.Query(ctx, query, args, rows); err != nil { + return err + } + defer rows.Close() + return sql.ScanSlice(rows, v) +} + +// EnvVarSelect is the builder for selecting fields of EnvVar entities. +type EnvVarSelect struct { + *EnvVarQuery + selector +} + +// Aggregate adds the given aggregation functions to the selector query. +func (_s *EnvVarSelect) Aggregate(fns ...AggregateFunc) *EnvVarSelect { + _s.fns = append(_s.fns, fns...) + return _s +} + +// Scan applies the selector query and scans the result into the given value. +func (_s *EnvVarSelect) Scan(ctx context.Context, v any) error { + ctx = setContextOp(ctx, _s.ctx, ent.OpQuerySelect) + if err := _s.prepareQuery(ctx); err != nil { + return err + } + return scanWithInterceptors[*EnvVarQuery, *EnvVarSelect](ctx, _s.EnvVarQuery, _s, _s.inters, v) +} + +func (_s *EnvVarSelect) sqlScan(ctx context.Context, root *EnvVarQuery, v any) error { + selector := root.sqlQuery(ctx) + aggregation := make([]string, 0, len(_s.fns)) + for _, fn := range _s.fns { + aggregation = append(aggregation, fn(selector)) + } + switch n := len(*_s.selector.flds); { + case n == 0 && len(aggregation) > 0: + selector.Select(aggregation...) + case n != 0 && len(aggregation) > 0: + selector.AppendSelect(aggregation...) + } + rows := &sql.Rows{} + query, args := selector.Query() + if err := _s.driver.Query(ctx, query, args, rows); err != nil { + return err + } + defer rows.Close() + return sql.ScanSlice(rows, v) +} diff --git a/pkg/ent/envvar_update.go b/pkg/ent/envvar_update.go new file mode 100644 index 000000000..200e65f66 --- /dev/null +++ b/pkg/ent/envvar_update.go @@ -0,0 +1,600 @@ +// Code generated by ent, DO NOT EDIT. + +package ent + +import ( + "context" + "errors" + "fmt" + "time" + + "entgo.io/ent/dialect/sql" + "entgo.io/ent/dialect/sql/sqlgraph" + "entgo.io/ent/schema/field" + "github.com/GoogleCloudPlatform/scion/pkg/ent/envvar" + "github.com/GoogleCloudPlatform/scion/pkg/ent/predicate" +) + +// EnvVarUpdate is the builder for updating EnvVar entities. +type EnvVarUpdate struct { + config + hooks []Hook + mutation *EnvVarMutation +} + +// Where appends a list predicates to the EnvVarUpdate builder. +func (_u *EnvVarUpdate) Where(ps ...predicate.EnvVar) *EnvVarUpdate { + _u.mutation.Where(ps...) + return _u +} + +// SetKey sets the "key" field. +func (_u *EnvVarUpdate) SetKey(v string) *EnvVarUpdate { + _u.mutation.SetKey(v) + return _u +} + +// SetNillableKey sets the "key" field if the given value is not nil. +func (_u *EnvVarUpdate) SetNillableKey(v *string) *EnvVarUpdate { + if v != nil { + _u.SetKey(*v) + } + return _u +} + +// SetValue sets the "value" field. +func (_u *EnvVarUpdate) SetValue(v string) *EnvVarUpdate { + _u.mutation.SetValue(v) + return _u +} + +// SetNillableValue sets the "value" field if the given value is not nil. +func (_u *EnvVarUpdate) SetNillableValue(v *string) *EnvVarUpdate { + if v != nil { + _u.SetValue(*v) + } + return _u +} + +// SetScope sets the "scope" field. +func (_u *EnvVarUpdate) SetScope(v string) *EnvVarUpdate { + _u.mutation.SetScope(v) + return _u +} + +// SetNillableScope sets the "scope" field if the given value is not nil. +func (_u *EnvVarUpdate) SetNillableScope(v *string) *EnvVarUpdate { + if v != nil { + _u.SetScope(*v) + } + return _u +} + +// SetScopeID sets the "scope_id" field. +func (_u *EnvVarUpdate) SetScopeID(v string) *EnvVarUpdate { + _u.mutation.SetScopeID(v) + return _u +} + +// SetNillableScopeID sets the "scope_id" field if the given value is not nil. +func (_u *EnvVarUpdate) SetNillableScopeID(v *string) *EnvVarUpdate { + if v != nil { + _u.SetScopeID(*v) + } + return _u +} + +// SetDescription sets the "description" field. +func (_u *EnvVarUpdate) SetDescription(v string) *EnvVarUpdate { + _u.mutation.SetDescription(v) + return _u +} + +// SetNillableDescription sets the "description" field if the given value is not nil. +func (_u *EnvVarUpdate) SetNillableDescription(v *string) *EnvVarUpdate { + if v != nil { + _u.SetDescription(*v) + } + return _u +} + +// ClearDescription clears the value of the "description" field. +func (_u *EnvVarUpdate) ClearDescription() *EnvVarUpdate { + _u.mutation.ClearDescription() + return _u +} + +// SetSensitive sets the "sensitive" field. +func (_u *EnvVarUpdate) SetSensitive(v bool) *EnvVarUpdate { + _u.mutation.SetSensitive(v) + return _u +} + +// SetNillableSensitive sets the "sensitive" field if the given value is not nil. +func (_u *EnvVarUpdate) SetNillableSensitive(v *bool) *EnvVarUpdate { + if v != nil { + _u.SetSensitive(*v) + } + return _u +} + +// SetInjectionMode sets the "injection_mode" field. +func (_u *EnvVarUpdate) SetInjectionMode(v envvar.InjectionMode) *EnvVarUpdate { + _u.mutation.SetInjectionMode(v) + return _u +} + +// SetNillableInjectionMode sets the "injection_mode" field if the given value is not nil. +func (_u *EnvVarUpdate) SetNillableInjectionMode(v *envvar.InjectionMode) *EnvVarUpdate { + if v != nil { + _u.SetInjectionMode(*v) + } + return _u +} + +// SetSecret sets the "secret" field. +func (_u *EnvVarUpdate) SetSecret(v bool) *EnvVarUpdate { + _u.mutation.SetSecret(v) + return _u +} + +// SetNillableSecret sets the "secret" field if the given value is not nil. +func (_u *EnvVarUpdate) SetNillableSecret(v *bool) *EnvVarUpdate { + if v != nil { + _u.SetSecret(*v) + } + return _u +} + +// SetCreatedBy sets the "created_by" field. +func (_u *EnvVarUpdate) SetCreatedBy(v string) *EnvVarUpdate { + _u.mutation.SetCreatedBy(v) + return _u +} + +// SetNillableCreatedBy sets the "created_by" field if the given value is not nil. +func (_u *EnvVarUpdate) SetNillableCreatedBy(v *string) *EnvVarUpdate { + if v != nil { + _u.SetCreatedBy(*v) + } + return _u +} + +// ClearCreatedBy clears the value of the "created_by" field. +func (_u *EnvVarUpdate) ClearCreatedBy() *EnvVarUpdate { + _u.mutation.ClearCreatedBy() + return _u +} + +// SetUpdated sets the "updated" field. +func (_u *EnvVarUpdate) SetUpdated(v time.Time) *EnvVarUpdate { + _u.mutation.SetUpdated(v) + return _u +} + +// Mutation returns the EnvVarMutation object of the builder. +func (_u *EnvVarUpdate) Mutation() *EnvVarMutation { + return _u.mutation +} + +// Save executes the query and returns the number of nodes affected by the update operation. +func (_u *EnvVarUpdate) Save(ctx context.Context) (int, error) { + _u.defaults() + return withHooks(ctx, _u.sqlSave, _u.mutation, _u.hooks) +} + +// SaveX is like Save, but panics if an error occurs. +func (_u *EnvVarUpdate) SaveX(ctx context.Context) int { + affected, err := _u.Save(ctx) + if err != nil { + panic(err) + } + return affected +} + +// Exec executes the query. +func (_u *EnvVarUpdate) Exec(ctx context.Context) error { + _, err := _u.Save(ctx) + return err +} + +// ExecX is like Exec, but panics if an error occurs. +func (_u *EnvVarUpdate) ExecX(ctx context.Context) { + if err := _u.Exec(ctx); err != nil { + panic(err) + } +} + +// defaults sets the default values of the builder before save. +func (_u *EnvVarUpdate) defaults() { + if _, ok := _u.mutation.Updated(); !ok { + v := envvar.UpdateDefaultUpdated() + _u.mutation.SetUpdated(v) + } +} + +// check runs all checks and user-defined validators on the builder. +func (_u *EnvVarUpdate) check() error { + if v, ok := _u.mutation.Key(); ok { + if err := envvar.KeyValidator(v); err != nil { + return &ValidationError{Name: "key", err: fmt.Errorf(`ent: validator failed for field "EnvVar.key": %w`, err)} + } + } + if v, ok := _u.mutation.Scope(); ok { + if err := envvar.ScopeValidator(v); err != nil { + return &ValidationError{Name: "scope", err: fmt.Errorf(`ent: validator failed for field "EnvVar.scope": %w`, err)} + } + } + if v, ok := _u.mutation.InjectionMode(); ok { + if err := envvar.InjectionModeValidator(v); err != nil { + return &ValidationError{Name: "injection_mode", err: fmt.Errorf(`ent: validator failed for field "EnvVar.injection_mode": %w`, err)} + } + } + return nil +} + +func (_u *EnvVarUpdate) sqlSave(ctx context.Context) (_node int, err error) { + if err := _u.check(); err != nil { + return _node, err + } + _spec := sqlgraph.NewUpdateSpec(envvar.Table, envvar.Columns, sqlgraph.NewFieldSpec(envvar.FieldID, field.TypeUUID)) + if ps := _u.mutation.predicates; len(ps) > 0 { + _spec.Predicate = func(selector *sql.Selector) { + for i := range ps { + ps[i](selector) + } + } + } + if value, ok := _u.mutation.Key(); ok { + _spec.SetField(envvar.FieldKey, field.TypeString, value) + } + if value, ok := _u.mutation.Value(); ok { + _spec.SetField(envvar.FieldValue, field.TypeString, value) + } + if value, ok := _u.mutation.Scope(); ok { + _spec.SetField(envvar.FieldScope, field.TypeString, value) + } + if value, ok := _u.mutation.ScopeID(); ok { + _spec.SetField(envvar.FieldScopeID, field.TypeString, value) + } + if value, ok := _u.mutation.Description(); ok { + _spec.SetField(envvar.FieldDescription, field.TypeString, value) + } + if _u.mutation.DescriptionCleared() { + _spec.ClearField(envvar.FieldDescription, field.TypeString) + } + if value, ok := _u.mutation.Sensitive(); ok { + _spec.SetField(envvar.FieldSensitive, field.TypeBool, value) + } + if value, ok := _u.mutation.InjectionMode(); ok { + _spec.SetField(envvar.FieldInjectionMode, field.TypeEnum, value) + } + if value, ok := _u.mutation.Secret(); ok { + _spec.SetField(envvar.FieldSecret, field.TypeBool, value) + } + if value, ok := _u.mutation.CreatedBy(); ok { + _spec.SetField(envvar.FieldCreatedBy, field.TypeString, value) + } + if _u.mutation.CreatedByCleared() { + _spec.ClearField(envvar.FieldCreatedBy, field.TypeString) + } + if value, ok := _u.mutation.Updated(); ok { + _spec.SetField(envvar.FieldUpdated, field.TypeTime, value) + } + if _node, err = sqlgraph.UpdateNodes(ctx, _u.driver, _spec); err != nil { + if _, ok := err.(*sqlgraph.NotFoundError); ok { + err = &NotFoundError{envvar.Label} + } else if sqlgraph.IsConstraintError(err) { + err = &ConstraintError{msg: err.Error(), wrap: err} + } + return 0, err + } + _u.mutation.done = true + return _node, nil +} + +// EnvVarUpdateOne is the builder for updating a single EnvVar entity. +type EnvVarUpdateOne struct { + config + fields []string + hooks []Hook + mutation *EnvVarMutation +} + +// SetKey sets the "key" field. +func (_u *EnvVarUpdateOne) SetKey(v string) *EnvVarUpdateOne { + _u.mutation.SetKey(v) + return _u +} + +// SetNillableKey sets the "key" field if the given value is not nil. +func (_u *EnvVarUpdateOne) SetNillableKey(v *string) *EnvVarUpdateOne { + if v != nil { + _u.SetKey(*v) + } + return _u +} + +// SetValue sets the "value" field. +func (_u *EnvVarUpdateOne) SetValue(v string) *EnvVarUpdateOne { + _u.mutation.SetValue(v) + return _u +} + +// SetNillableValue sets the "value" field if the given value is not nil. +func (_u *EnvVarUpdateOne) SetNillableValue(v *string) *EnvVarUpdateOne { + if v != nil { + _u.SetValue(*v) + } + return _u +} + +// SetScope sets the "scope" field. +func (_u *EnvVarUpdateOne) SetScope(v string) *EnvVarUpdateOne { + _u.mutation.SetScope(v) + return _u +} + +// SetNillableScope sets the "scope" field if the given value is not nil. +func (_u *EnvVarUpdateOne) SetNillableScope(v *string) *EnvVarUpdateOne { + if v != nil { + _u.SetScope(*v) + } + return _u +} + +// SetScopeID sets the "scope_id" field. +func (_u *EnvVarUpdateOne) SetScopeID(v string) *EnvVarUpdateOne { + _u.mutation.SetScopeID(v) + return _u +} + +// SetNillableScopeID sets the "scope_id" field if the given value is not nil. +func (_u *EnvVarUpdateOne) SetNillableScopeID(v *string) *EnvVarUpdateOne { + if v != nil { + _u.SetScopeID(*v) + } + return _u +} + +// SetDescription sets the "description" field. +func (_u *EnvVarUpdateOne) SetDescription(v string) *EnvVarUpdateOne { + _u.mutation.SetDescription(v) + return _u +} + +// SetNillableDescription sets the "description" field if the given value is not nil. +func (_u *EnvVarUpdateOne) SetNillableDescription(v *string) *EnvVarUpdateOne { + if v != nil { + _u.SetDescription(*v) + } + return _u +} + +// ClearDescription clears the value of the "description" field. +func (_u *EnvVarUpdateOne) ClearDescription() *EnvVarUpdateOne { + _u.mutation.ClearDescription() + return _u +} + +// SetSensitive sets the "sensitive" field. +func (_u *EnvVarUpdateOne) SetSensitive(v bool) *EnvVarUpdateOne { + _u.mutation.SetSensitive(v) + return _u +} + +// SetNillableSensitive sets the "sensitive" field if the given value is not nil. +func (_u *EnvVarUpdateOne) SetNillableSensitive(v *bool) *EnvVarUpdateOne { + if v != nil { + _u.SetSensitive(*v) + } + return _u +} + +// SetInjectionMode sets the "injection_mode" field. +func (_u *EnvVarUpdateOne) SetInjectionMode(v envvar.InjectionMode) *EnvVarUpdateOne { + _u.mutation.SetInjectionMode(v) + return _u +} + +// SetNillableInjectionMode sets the "injection_mode" field if the given value is not nil. +func (_u *EnvVarUpdateOne) SetNillableInjectionMode(v *envvar.InjectionMode) *EnvVarUpdateOne { + if v != nil { + _u.SetInjectionMode(*v) + } + return _u +} + +// SetSecret sets the "secret" field. +func (_u *EnvVarUpdateOne) SetSecret(v bool) *EnvVarUpdateOne { + _u.mutation.SetSecret(v) + return _u +} + +// SetNillableSecret sets the "secret" field if the given value is not nil. +func (_u *EnvVarUpdateOne) SetNillableSecret(v *bool) *EnvVarUpdateOne { + if v != nil { + _u.SetSecret(*v) + } + return _u +} + +// SetCreatedBy sets the "created_by" field. +func (_u *EnvVarUpdateOne) SetCreatedBy(v string) *EnvVarUpdateOne { + _u.mutation.SetCreatedBy(v) + return _u +} + +// SetNillableCreatedBy sets the "created_by" field if the given value is not nil. +func (_u *EnvVarUpdateOne) SetNillableCreatedBy(v *string) *EnvVarUpdateOne { + if v != nil { + _u.SetCreatedBy(*v) + } + return _u +} + +// ClearCreatedBy clears the value of the "created_by" field. +func (_u *EnvVarUpdateOne) ClearCreatedBy() *EnvVarUpdateOne { + _u.mutation.ClearCreatedBy() + return _u +} + +// SetUpdated sets the "updated" field. +func (_u *EnvVarUpdateOne) SetUpdated(v time.Time) *EnvVarUpdateOne { + _u.mutation.SetUpdated(v) + return _u +} + +// Mutation returns the EnvVarMutation object of the builder. +func (_u *EnvVarUpdateOne) Mutation() *EnvVarMutation { + return _u.mutation +} + +// Where appends a list predicates to the EnvVarUpdate builder. +func (_u *EnvVarUpdateOne) Where(ps ...predicate.EnvVar) *EnvVarUpdateOne { + _u.mutation.Where(ps...) + return _u +} + +// Select allows selecting one or more fields (columns) of the returned entity. +// The default is selecting all fields defined in the entity schema. +func (_u *EnvVarUpdateOne) Select(field string, fields ...string) *EnvVarUpdateOne { + _u.fields = append([]string{field}, fields...) + return _u +} + +// Save executes the query and returns the updated EnvVar entity. +func (_u *EnvVarUpdateOne) Save(ctx context.Context) (*EnvVar, error) { + _u.defaults() + return withHooks(ctx, _u.sqlSave, _u.mutation, _u.hooks) +} + +// SaveX is like Save, but panics if an error occurs. +func (_u *EnvVarUpdateOne) SaveX(ctx context.Context) *EnvVar { + node, err := _u.Save(ctx) + if err != nil { + panic(err) + } + return node +} + +// Exec executes the query on the entity. +func (_u *EnvVarUpdateOne) Exec(ctx context.Context) error { + _, err := _u.Save(ctx) + return err +} + +// ExecX is like Exec, but panics if an error occurs. +func (_u *EnvVarUpdateOne) ExecX(ctx context.Context) { + if err := _u.Exec(ctx); err != nil { + panic(err) + } +} + +// defaults sets the default values of the builder before save. +func (_u *EnvVarUpdateOne) defaults() { + if _, ok := _u.mutation.Updated(); !ok { + v := envvar.UpdateDefaultUpdated() + _u.mutation.SetUpdated(v) + } +} + +// check runs all checks and user-defined validators on the builder. +func (_u *EnvVarUpdateOne) check() error { + if v, ok := _u.mutation.Key(); ok { + if err := envvar.KeyValidator(v); err != nil { + return &ValidationError{Name: "key", err: fmt.Errorf(`ent: validator failed for field "EnvVar.key": %w`, err)} + } + } + if v, ok := _u.mutation.Scope(); ok { + if err := envvar.ScopeValidator(v); err != nil { + return &ValidationError{Name: "scope", err: fmt.Errorf(`ent: validator failed for field "EnvVar.scope": %w`, err)} + } + } + if v, ok := _u.mutation.InjectionMode(); ok { + if err := envvar.InjectionModeValidator(v); err != nil { + return &ValidationError{Name: "injection_mode", err: fmt.Errorf(`ent: validator failed for field "EnvVar.injection_mode": %w`, err)} + } + } + return nil +} + +func (_u *EnvVarUpdateOne) sqlSave(ctx context.Context) (_node *EnvVar, err error) { + if err := _u.check(); err != nil { + return _node, err + } + _spec := sqlgraph.NewUpdateSpec(envvar.Table, envvar.Columns, sqlgraph.NewFieldSpec(envvar.FieldID, field.TypeUUID)) + id, ok := _u.mutation.ID() + if !ok { + return nil, &ValidationError{Name: "id", err: errors.New(`ent: missing "EnvVar.id" for update`)} + } + _spec.Node.ID.Value = id + if fields := _u.fields; len(fields) > 0 { + _spec.Node.Columns = make([]string, 0, len(fields)) + _spec.Node.Columns = append(_spec.Node.Columns, envvar.FieldID) + for _, f := range fields { + if !envvar.ValidColumn(f) { + return nil, &ValidationError{Name: f, err: fmt.Errorf("ent: invalid field %q for query", f)} + } + if f != envvar.FieldID { + _spec.Node.Columns = append(_spec.Node.Columns, f) + } + } + } + if ps := _u.mutation.predicates; len(ps) > 0 { + _spec.Predicate = func(selector *sql.Selector) { + for i := range ps { + ps[i](selector) + } + } + } + if value, ok := _u.mutation.Key(); ok { + _spec.SetField(envvar.FieldKey, field.TypeString, value) + } + if value, ok := _u.mutation.Value(); ok { + _spec.SetField(envvar.FieldValue, field.TypeString, value) + } + if value, ok := _u.mutation.Scope(); ok { + _spec.SetField(envvar.FieldScope, field.TypeString, value) + } + if value, ok := _u.mutation.ScopeID(); ok { + _spec.SetField(envvar.FieldScopeID, field.TypeString, value) + } + if value, ok := _u.mutation.Description(); ok { + _spec.SetField(envvar.FieldDescription, field.TypeString, value) + } + if _u.mutation.DescriptionCleared() { + _spec.ClearField(envvar.FieldDescription, field.TypeString) + } + if value, ok := _u.mutation.Sensitive(); ok { + _spec.SetField(envvar.FieldSensitive, field.TypeBool, value) + } + if value, ok := _u.mutation.InjectionMode(); ok { + _spec.SetField(envvar.FieldInjectionMode, field.TypeEnum, value) + } + if value, ok := _u.mutation.Secret(); ok { + _spec.SetField(envvar.FieldSecret, field.TypeBool, value) + } + if value, ok := _u.mutation.CreatedBy(); ok { + _spec.SetField(envvar.FieldCreatedBy, field.TypeString, value) + } + if _u.mutation.CreatedByCleared() { + _spec.ClearField(envvar.FieldCreatedBy, field.TypeString) + } + if value, ok := _u.mutation.Updated(); ok { + _spec.SetField(envvar.FieldUpdated, field.TypeTime, value) + } + _node = &EnvVar{config: _u.config} + _spec.Assign = _node.assignValues + _spec.ScanValues = _node.scanValues + if err = sqlgraph.UpdateNode(ctx, _u.driver, _spec); err != nil { + if _, ok := err.(*sqlgraph.NotFoundError); ok { + err = &NotFoundError{envvar.Label} + } else if sqlgraph.IsConstraintError(err) { + err = &ConstraintError{msg: err.Error(), wrap: err} + } + return nil, err + } + _u.mutation.done = true + return _node, nil +} diff --git a/pkg/ent/gcpserviceaccount.go b/pkg/ent/gcpserviceaccount.go new file mode 100644 index 000000000..eb8a16cec --- /dev/null +++ b/pkg/ent/gcpserviceaccount.go @@ -0,0 +1,233 @@ +// Code generated by ent, DO NOT EDIT. + +package ent + +import ( + "fmt" + "strings" + "time" + + "entgo.io/ent" + "entgo.io/ent/dialect/sql" + "github.com/GoogleCloudPlatform/scion/pkg/ent/gcpserviceaccount" + "github.com/google/uuid" +) + +// GCPServiceAccount is the model entity for the GCPServiceAccount schema. +type GCPServiceAccount struct { + config `json:"-"` + // ID of the ent. + ID uuid.UUID `json:"id,omitempty"` + // Scope holds the value of the "scope" field. + Scope string `json:"scope,omitempty"` + // ScopeID holds the value of the "scope_id" field. + ScopeID string `json:"scope_id,omitempty"` + // Email holds the value of the "email" field. + Email string `json:"email,omitempty"` + // ProjectID holds the value of the "project_id" field. + ProjectID string `json:"project_id,omitempty"` + // DisplayName holds the value of the "display_name" field. + DisplayName string `json:"display_name,omitempty"` + // DefaultScopes holds the value of the "default_scopes" field. + DefaultScopes string `json:"default_scopes,omitempty"` + // Verified holds the value of the "verified" field. + Verified bool `json:"verified,omitempty"` + // VerifiedAt holds the value of the "verified_at" field. + VerifiedAt *time.Time `json:"verified_at,omitempty"` + // CreatedBy holds the value of the "created_by" field. + CreatedBy string `json:"created_by,omitempty"` + // Managed holds the value of the "managed" field. + Managed bool `json:"managed,omitempty"` + // ManagedBy holds the value of the "managed_by" field. + ManagedBy string `json:"managed_by,omitempty"` + // Created holds the value of the "created" field. + Created time.Time `json:"created,omitempty"` + selectValues sql.SelectValues +} + +// scanValues returns the types for scanning values from sql.Rows. +func (*GCPServiceAccount) scanValues(columns []string) ([]any, error) { + values := make([]any, len(columns)) + for i := range columns { + switch columns[i] { + case gcpserviceaccount.FieldVerified, gcpserviceaccount.FieldManaged: + values[i] = new(sql.NullBool) + case gcpserviceaccount.FieldScope, gcpserviceaccount.FieldScopeID, gcpserviceaccount.FieldEmail, gcpserviceaccount.FieldProjectID, gcpserviceaccount.FieldDisplayName, gcpserviceaccount.FieldDefaultScopes, gcpserviceaccount.FieldCreatedBy, gcpserviceaccount.FieldManagedBy: + values[i] = new(sql.NullString) + case gcpserviceaccount.FieldVerifiedAt, gcpserviceaccount.FieldCreated: + values[i] = new(sql.NullTime) + case gcpserviceaccount.FieldID: + values[i] = new(uuid.UUID) + default: + values[i] = new(sql.UnknownType) + } + } + return values, nil +} + +// assignValues assigns the values that were returned from sql.Rows (after scanning) +// to the GCPServiceAccount fields. +func (_m *GCPServiceAccount) assignValues(columns []string, values []any) error { + if m, n := len(values), len(columns); m < n { + return fmt.Errorf("mismatch number of scan values: %d != %d", m, n) + } + for i := range columns { + switch columns[i] { + case gcpserviceaccount.FieldID: + if value, ok := values[i].(*uuid.UUID); !ok { + return fmt.Errorf("unexpected type %T for field id", values[i]) + } else if value != nil { + _m.ID = *value + } + case gcpserviceaccount.FieldScope: + if value, ok := values[i].(*sql.NullString); !ok { + return fmt.Errorf("unexpected type %T for field scope", values[i]) + } else if value.Valid { + _m.Scope = value.String + } + case gcpserviceaccount.FieldScopeID: + if value, ok := values[i].(*sql.NullString); !ok { + return fmt.Errorf("unexpected type %T for field scope_id", values[i]) + } else if value.Valid { + _m.ScopeID = value.String + } + case gcpserviceaccount.FieldEmail: + if value, ok := values[i].(*sql.NullString); !ok { + return fmt.Errorf("unexpected type %T for field email", values[i]) + } else if value.Valid { + _m.Email = value.String + } + case gcpserviceaccount.FieldProjectID: + if value, ok := values[i].(*sql.NullString); !ok { + return fmt.Errorf("unexpected type %T for field project_id", values[i]) + } else if value.Valid { + _m.ProjectID = value.String + } + case gcpserviceaccount.FieldDisplayName: + if value, ok := values[i].(*sql.NullString); !ok { + return fmt.Errorf("unexpected type %T for field display_name", values[i]) + } else if value.Valid { + _m.DisplayName = value.String + } + case gcpserviceaccount.FieldDefaultScopes: + if value, ok := values[i].(*sql.NullString); !ok { + return fmt.Errorf("unexpected type %T for field default_scopes", values[i]) + } else if value.Valid { + _m.DefaultScopes = value.String + } + case gcpserviceaccount.FieldVerified: + if value, ok := values[i].(*sql.NullBool); !ok { + return fmt.Errorf("unexpected type %T for field verified", values[i]) + } else if value.Valid { + _m.Verified = value.Bool + } + case gcpserviceaccount.FieldVerifiedAt: + if value, ok := values[i].(*sql.NullTime); !ok { + return fmt.Errorf("unexpected type %T for field verified_at", values[i]) + } else if value.Valid { + _m.VerifiedAt = new(time.Time) + *_m.VerifiedAt = value.Time + } + case gcpserviceaccount.FieldCreatedBy: + if value, ok := values[i].(*sql.NullString); !ok { + return fmt.Errorf("unexpected type %T for field created_by", values[i]) + } else if value.Valid { + _m.CreatedBy = value.String + } + case gcpserviceaccount.FieldManaged: + if value, ok := values[i].(*sql.NullBool); !ok { + return fmt.Errorf("unexpected type %T for field managed", values[i]) + } else if value.Valid { + _m.Managed = value.Bool + } + case gcpserviceaccount.FieldManagedBy: + if value, ok := values[i].(*sql.NullString); !ok { + return fmt.Errorf("unexpected type %T for field managed_by", values[i]) + } else if value.Valid { + _m.ManagedBy = value.String + } + case gcpserviceaccount.FieldCreated: + if value, ok := values[i].(*sql.NullTime); !ok { + return fmt.Errorf("unexpected type %T for field created", values[i]) + } else if value.Valid { + _m.Created = value.Time + } + default: + _m.selectValues.Set(columns[i], values[i]) + } + } + return nil +} + +// Value returns the ent.Value that was dynamically selected and assigned to the GCPServiceAccount. +// This includes values selected through modifiers, order, etc. +func (_m *GCPServiceAccount) Value(name string) (ent.Value, error) { + return _m.selectValues.Get(name) +} + +// Update returns a builder for updating this GCPServiceAccount. +// Note that you need to call GCPServiceAccount.Unwrap() before calling this method if this GCPServiceAccount +// was returned from a transaction, and the transaction was committed or rolled back. +func (_m *GCPServiceAccount) Update() *GCPServiceAccountUpdateOne { + return NewGCPServiceAccountClient(_m.config).UpdateOne(_m) +} + +// Unwrap unwraps the GCPServiceAccount entity that was returned from a transaction after it was closed, +// so that all future queries will be executed through the driver which created the transaction. +func (_m *GCPServiceAccount) Unwrap() *GCPServiceAccount { + _tx, ok := _m.config.driver.(*txDriver) + if !ok { + panic("ent: GCPServiceAccount is not a transactional entity") + } + _m.config.driver = _tx.drv + return _m +} + +// String implements the fmt.Stringer. +func (_m *GCPServiceAccount) String() string { + var builder strings.Builder + builder.WriteString("GCPServiceAccount(") + builder.WriteString(fmt.Sprintf("id=%v, ", _m.ID)) + builder.WriteString("scope=") + builder.WriteString(_m.Scope) + builder.WriteString(", ") + builder.WriteString("scope_id=") + builder.WriteString(_m.ScopeID) + builder.WriteString(", ") + builder.WriteString("email=") + builder.WriteString(_m.Email) + builder.WriteString(", ") + builder.WriteString("project_id=") + builder.WriteString(_m.ProjectID) + builder.WriteString(", ") + builder.WriteString("display_name=") + builder.WriteString(_m.DisplayName) + builder.WriteString(", ") + builder.WriteString("default_scopes=") + builder.WriteString(_m.DefaultScopes) + builder.WriteString(", ") + builder.WriteString("verified=") + builder.WriteString(fmt.Sprintf("%v", _m.Verified)) + builder.WriteString(", ") + if v := _m.VerifiedAt; v != nil { + builder.WriteString("verified_at=") + builder.WriteString(v.Format(time.ANSIC)) + } + builder.WriteString(", ") + builder.WriteString("created_by=") + builder.WriteString(_m.CreatedBy) + builder.WriteString(", ") + builder.WriteString("managed=") + builder.WriteString(fmt.Sprintf("%v", _m.Managed)) + builder.WriteString(", ") + builder.WriteString("managed_by=") + builder.WriteString(_m.ManagedBy) + builder.WriteString(", ") + builder.WriteString("created=") + builder.WriteString(_m.Created.Format(time.ANSIC)) + builder.WriteByte(')') + return builder.String() +} + +// GCPServiceAccounts is a parsable slice of GCPServiceAccount. +type GCPServiceAccounts []*GCPServiceAccount diff --git a/pkg/ent/gcpserviceaccount/gcpserviceaccount.go b/pkg/ent/gcpserviceaccount/gcpserviceaccount.go new file mode 100644 index 000000000..c20f0b2be --- /dev/null +++ b/pkg/ent/gcpserviceaccount/gcpserviceaccount.go @@ -0,0 +1,165 @@ +// Code generated by ent, DO NOT EDIT. + +package gcpserviceaccount + +import ( + "time" + + "entgo.io/ent/dialect/sql" + "github.com/google/uuid" +) + +const ( + // Label holds the string label denoting the gcpserviceaccount type in the database. + Label = "gcp_service_account" + // FieldID holds the string denoting the id field in the database. + FieldID = "id" + // FieldScope holds the string denoting the scope field in the database. + FieldScope = "scope" + // FieldScopeID holds the string denoting the scope_id field in the database. + FieldScopeID = "scope_id" + // FieldEmail holds the string denoting the email field in the database. + FieldEmail = "email" + // FieldProjectID holds the string denoting the project_id field in the database. + FieldProjectID = "project_id" + // FieldDisplayName holds the string denoting the display_name field in the database. + FieldDisplayName = "display_name" + // FieldDefaultScopes holds the string denoting the default_scopes field in the database. + FieldDefaultScopes = "default_scopes" + // FieldVerified holds the string denoting the verified field in the database. + FieldVerified = "verified" + // FieldVerifiedAt holds the string denoting the verified_at field in the database. + FieldVerifiedAt = "verified_at" + // FieldCreatedBy holds the string denoting the created_by field in the database. + FieldCreatedBy = "created_by" + // FieldManaged holds the string denoting the managed field in the database. + FieldManaged = "managed" + // FieldManagedBy holds the string denoting the managed_by field in the database. + FieldManagedBy = "managed_by" + // FieldCreated holds the string denoting the created field in the database. + FieldCreated = "created" + // Table holds the table name of the gcpserviceaccount in the database. + Table = "gcp_service_accounts" +) + +// Columns holds all SQL columns for gcpserviceaccount fields. +var Columns = []string{ + FieldID, + FieldScope, + FieldScopeID, + FieldEmail, + FieldProjectID, + FieldDisplayName, + FieldDefaultScopes, + FieldVerified, + FieldVerifiedAt, + FieldCreatedBy, + FieldManaged, + FieldManagedBy, + FieldCreated, +} + +// ValidColumn reports if the column name is valid (part of the table columns). +func ValidColumn(column string) bool { + for i := range Columns { + if column == Columns[i] { + return true + } + } + return false +} + +var ( + // ScopeValidator is a validator for the "scope" field. It is called by the builders before save. + ScopeValidator func(string) error + // ScopeIDValidator is a validator for the "scope_id" field. It is called by the builders before save. + ScopeIDValidator func(string) error + // EmailValidator is a validator for the "email" field. It is called by the builders before save. + EmailValidator func(string) error + // ProjectIDValidator is a validator for the "project_id" field. It is called by the builders before save. + ProjectIDValidator func(string) error + // DefaultDisplayName holds the default value on creation for the "display_name" field. + DefaultDisplayName string + // DefaultDefaultScopes holds the default value on creation for the "default_scopes" field. + DefaultDefaultScopes string + // DefaultVerified holds the default value on creation for the "verified" field. + DefaultVerified bool + // DefaultCreatedBy holds the default value on creation for the "created_by" field. + DefaultCreatedBy string + // DefaultManaged holds the default value on creation for the "managed" field. + DefaultManaged bool + // DefaultManagedBy holds the default value on creation for the "managed_by" field. + DefaultManagedBy string + // DefaultCreated holds the default value on creation for the "created" field. + DefaultCreated func() time.Time + // DefaultID holds the default value on creation for the "id" field. + DefaultID func() uuid.UUID +) + +// OrderOption defines the ordering options for the GCPServiceAccount queries. +type OrderOption func(*sql.Selector) + +// ByID orders the results by the id field. +func ByID(opts ...sql.OrderTermOption) OrderOption { + return sql.OrderByField(FieldID, opts...).ToFunc() +} + +// ByScope orders the results by the scope field. +func ByScope(opts ...sql.OrderTermOption) OrderOption { + return sql.OrderByField(FieldScope, opts...).ToFunc() +} + +// ByScopeID orders the results by the scope_id field. +func ByScopeID(opts ...sql.OrderTermOption) OrderOption { + return sql.OrderByField(FieldScopeID, opts...).ToFunc() +} + +// ByEmail orders the results by the email field. +func ByEmail(opts ...sql.OrderTermOption) OrderOption { + return sql.OrderByField(FieldEmail, opts...).ToFunc() +} + +// ByProjectID orders the results by the project_id field. +func ByProjectID(opts ...sql.OrderTermOption) OrderOption { + return sql.OrderByField(FieldProjectID, opts...).ToFunc() +} + +// ByDisplayName orders the results by the display_name field. +func ByDisplayName(opts ...sql.OrderTermOption) OrderOption { + return sql.OrderByField(FieldDisplayName, opts...).ToFunc() +} + +// ByDefaultScopes orders the results by the default_scopes field. +func ByDefaultScopes(opts ...sql.OrderTermOption) OrderOption { + return sql.OrderByField(FieldDefaultScopes, opts...).ToFunc() +} + +// ByVerified orders the results by the verified field. +func ByVerified(opts ...sql.OrderTermOption) OrderOption { + return sql.OrderByField(FieldVerified, opts...).ToFunc() +} + +// ByVerifiedAt orders the results by the verified_at field. +func ByVerifiedAt(opts ...sql.OrderTermOption) OrderOption { + return sql.OrderByField(FieldVerifiedAt, opts...).ToFunc() +} + +// ByCreatedBy orders the results by the created_by field. +func ByCreatedBy(opts ...sql.OrderTermOption) OrderOption { + return sql.OrderByField(FieldCreatedBy, opts...).ToFunc() +} + +// ByManaged orders the results by the managed field. +func ByManaged(opts ...sql.OrderTermOption) OrderOption { + return sql.OrderByField(FieldManaged, opts...).ToFunc() +} + +// ByManagedBy orders the results by the managed_by field. +func ByManagedBy(opts ...sql.OrderTermOption) OrderOption { + return sql.OrderByField(FieldManagedBy, opts...).ToFunc() +} + +// ByCreated orders the results by the created field. +func ByCreated(opts ...sql.OrderTermOption) OrderOption { + return sql.OrderByField(FieldCreated, opts...).ToFunc() +} diff --git a/pkg/ent/gcpserviceaccount/where.go b/pkg/ent/gcpserviceaccount/where.go new file mode 100644 index 000000000..7123a14af --- /dev/null +++ b/pkg/ent/gcpserviceaccount/where.go @@ -0,0 +1,761 @@ +// Code generated by ent, DO NOT EDIT. + +package gcpserviceaccount + +import ( + "time" + + "entgo.io/ent/dialect/sql" + "github.com/GoogleCloudPlatform/scion/pkg/ent/predicate" + "github.com/google/uuid" +) + +// ID filters vertices based on their ID field. +func ID(id uuid.UUID) predicate.GCPServiceAccount { + return predicate.GCPServiceAccount(sql.FieldEQ(FieldID, id)) +} + +// IDEQ applies the EQ predicate on the ID field. +func IDEQ(id uuid.UUID) predicate.GCPServiceAccount { + return predicate.GCPServiceAccount(sql.FieldEQ(FieldID, id)) +} + +// IDNEQ applies the NEQ predicate on the ID field. +func IDNEQ(id uuid.UUID) predicate.GCPServiceAccount { + return predicate.GCPServiceAccount(sql.FieldNEQ(FieldID, id)) +} + +// IDIn applies the In predicate on the ID field. +func IDIn(ids ...uuid.UUID) predicate.GCPServiceAccount { + return predicate.GCPServiceAccount(sql.FieldIn(FieldID, ids...)) +} + +// IDNotIn applies the NotIn predicate on the ID field. +func IDNotIn(ids ...uuid.UUID) predicate.GCPServiceAccount { + return predicate.GCPServiceAccount(sql.FieldNotIn(FieldID, ids...)) +} + +// IDGT applies the GT predicate on the ID field. +func IDGT(id uuid.UUID) predicate.GCPServiceAccount { + return predicate.GCPServiceAccount(sql.FieldGT(FieldID, id)) +} + +// IDGTE applies the GTE predicate on the ID field. +func IDGTE(id uuid.UUID) predicate.GCPServiceAccount { + return predicate.GCPServiceAccount(sql.FieldGTE(FieldID, id)) +} + +// IDLT applies the LT predicate on the ID field. +func IDLT(id uuid.UUID) predicate.GCPServiceAccount { + return predicate.GCPServiceAccount(sql.FieldLT(FieldID, id)) +} + +// IDLTE applies the LTE predicate on the ID field. +func IDLTE(id uuid.UUID) predicate.GCPServiceAccount { + return predicate.GCPServiceAccount(sql.FieldLTE(FieldID, id)) +} + +// Scope applies equality check predicate on the "scope" field. It's identical to ScopeEQ. +func Scope(v string) predicate.GCPServiceAccount { + return predicate.GCPServiceAccount(sql.FieldEQ(FieldScope, v)) +} + +// ScopeID applies equality check predicate on the "scope_id" field. It's identical to ScopeIDEQ. +func ScopeID(v string) predicate.GCPServiceAccount { + return predicate.GCPServiceAccount(sql.FieldEQ(FieldScopeID, v)) +} + +// Email applies equality check predicate on the "email" field. It's identical to EmailEQ. +func Email(v string) predicate.GCPServiceAccount { + return predicate.GCPServiceAccount(sql.FieldEQ(FieldEmail, v)) +} + +// ProjectID applies equality check predicate on the "project_id" field. It's identical to ProjectIDEQ. +func ProjectID(v string) predicate.GCPServiceAccount { + return predicate.GCPServiceAccount(sql.FieldEQ(FieldProjectID, v)) +} + +// DisplayName applies equality check predicate on the "display_name" field. It's identical to DisplayNameEQ. +func DisplayName(v string) predicate.GCPServiceAccount { + return predicate.GCPServiceAccount(sql.FieldEQ(FieldDisplayName, v)) +} + +// DefaultScopes applies equality check predicate on the "default_scopes" field. It's identical to DefaultScopesEQ. +func DefaultScopes(v string) predicate.GCPServiceAccount { + return predicate.GCPServiceAccount(sql.FieldEQ(FieldDefaultScopes, v)) +} + +// Verified applies equality check predicate on the "verified" field. It's identical to VerifiedEQ. +func Verified(v bool) predicate.GCPServiceAccount { + return predicate.GCPServiceAccount(sql.FieldEQ(FieldVerified, v)) +} + +// VerifiedAt applies equality check predicate on the "verified_at" field. It's identical to VerifiedAtEQ. +func VerifiedAt(v time.Time) predicate.GCPServiceAccount { + return predicate.GCPServiceAccount(sql.FieldEQ(FieldVerifiedAt, v)) +} + +// CreatedBy applies equality check predicate on the "created_by" field. It's identical to CreatedByEQ. +func CreatedBy(v string) predicate.GCPServiceAccount { + return predicate.GCPServiceAccount(sql.FieldEQ(FieldCreatedBy, v)) +} + +// Managed applies equality check predicate on the "managed" field. It's identical to ManagedEQ. +func Managed(v bool) predicate.GCPServiceAccount { + return predicate.GCPServiceAccount(sql.FieldEQ(FieldManaged, v)) +} + +// ManagedBy applies equality check predicate on the "managed_by" field. It's identical to ManagedByEQ. +func ManagedBy(v string) predicate.GCPServiceAccount { + return predicate.GCPServiceAccount(sql.FieldEQ(FieldManagedBy, v)) +} + +// Created applies equality check predicate on the "created" field. It's identical to CreatedEQ. +func Created(v time.Time) predicate.GCPServiceAccount { + return predicate.GCPServiceAccount(sql.FieldEQ(FieldCreated, v)) +} + +// ScopeEQ applies the EQ predicate on the "scope" field. +func ScopeEQ(v string) predicate.GCPServiceAccount { + return predicate.GCPServiceAccount(sql.FieldEQ(FieldScope, v)) +} + +// ScopeNEQ applies the NEQ predicate on the "scope" field. +func ScopeNEQ(v string) predicate.GCPServiceAccount { + return predicate.GCPServiceAccount(sql.FieldNEQ(FieldScope, v)) +} + +// ScopeIn applies the In predicate on the "scope" field. +func ScopeIn(vs ...string) predicate.GCPServiceAccount { + return predicate.GCPServiceAccount(sql.FieldIn(FieldScope, vs...)) +} + +// ScopeNotIn applies the NotIn predicate on the "scope" field. +func ScopeNotIn(vs ...string) predicate.GCPServiceAccount { + return predicate.GCPServiceAccount(sql.FieldNotIn(FieldScope, vs...)) +} + +// ScopeGT applies the GT predicate on the "scope" field. +func ScopeGT(v string) predicate.GCPServiceAccount { + return predicate.GCPServiceAccount(sql.FieldGT(FieldScope, v)) +} + +// ScopeGTE applies the GTE predicate on the "scope" field. +func ScopeGTE(v string) predicate.GCPServiceAccount { + return predicate.GCPServiceAccount(sql.FieldGTE(FieldScope, v)) +} + +// ScopeLT applies the LT predicate on the "scope" field. +func ScopeLT(v string) predicate.GCPServiceAccount { + return predicate.GCPServiceAccount(sql.FieldLT(FieldScope, v)) +} + +// ScopeLTE applies the LTE predicate on the "scope" field. +func ScopeLTE(v string) predicate.GCPServiceAccount { + return predicate.GCPServiceAccount(sql.FieldLTE(FieldScope, v)) +} + +// ScopeContains applies the Contains predicate on the "scope" field. +func ScopeContains(v string) predicate.GCPServiceAccount { + return predicate.GCPServiceAccount(sql.FieldContains(FieldScope, v)) +} + +// ScopeHasPrefix applies the HasPrefix predicate on the "scope" field. +func ScopeHasPrefix(v string) predicate.GCPServiceAccount { + return predicate.GCPServiceAccount(sql.FieldHasPrefix(FieldScope, v)) +} + +// ScopeHasSuffix applies the HasSuffix predicate on the "scope" field. +func ScopeHasSuffix(v string) predicate.GCPServiceAccount { + return predicate.GCPServiceAccount(sql.FieldHasSuffix(FieldScope, v)) +} + +// ScopeEqualFold applies the EqualFold predicate on the "scope" field. +func ScopeEqualFold(v string) predicate.GCPServiceAccount { + return predicate.GCPServiceAccount(sql.FieldEqualFold(FieldScope, v)) +} + +// ScopeContainsFold applies the ContainsFold predicate on the "scope" field. +func ScopeContainsFold(v string) predicate.GCPServiceAccount { + return predicate.GCPServiceAccount(sql.FieldContainsFold(FieldScope, v)) +} + +// ScopeIDEQ applies the EQ predicate on the "scope_id" field. +func ScopeIDEQ(v string) predicate.GCPServiceAccount { + return predicate.GCPServiceAccount(sql.FieldEQ(FieldScopeID, v)) +} + +// ScopeIDNEQ applies the NEQ predicate on the "scope_id" field. +func ScopeIDNEQ(v string) predicate.GCPServiceAccount { + return predicate.GCPServiceAccount(sql.FieldNEQ(FieldScopeID, v)) +} + +// ScopeIDIn applies the In predicate on the "scope_id" field. +func ScopeIDIn(vs ...string) predicate.GCPServiceAccount { + return predicate.GCPServiceAccount(sql.FieldIn(FieldScopeID, vs...)) +} + +// ScopeIDNotIn applies the NotIn predicate on the "scope_id" field. +func ScopeIDNotIn(vs ...string) predicate.GCPServiceAccount { + return predicate.GCPServiceAccount(sql.FieldNotIn(FieldScopeID, vs...)) +} + +// ScopeIDGT applies the GT predicate on the "scope_id" field. +func ScopeIDGT(v string) predicate.GCPServiceAccount { + return predicate.GCPServiceAccount(sql.FieldGT(FieldScopeID, v)) +} + +// ScopeIDGTE applies the GTE predicate on the "scope_id" field. +func ScopeIDGTE(v string) predicate.GCPServiceAccount { + return predicate.GCPServiceAccount(sql.FieldGTE(FieldScopeID, v)) +} + +// ScopeIDLT applies the LT predicate on the "scope_id" field. +func ScopeIDLT(v string) predicate.GCPServiceAccount { + return predicate.GCPServiceAccount(sql.FieldLT(FieldScopeID, v)) +} + +// ScopeIDLTE applies the LTE predicate on the "scope_id" field. +func ScopeIDLTE(v string) predicate.GCPServiceAccount { + return predicate.GCPServiceAccount(sql.FieldLTE(FieldScopeID, v)) +} + +// ScopeIDContains applies the Contains predicate on the "scope_id" field. +func ScopeIDContains(v string) predicate.GCPServiceAccount { + return predicate.GCPServiceAccount(sql.FieldContains(FieldScopeID, v)) +} + +// ScopeIDHasPrefix applies the HasPrefix predicate on the "scope_id" field. +func ScopeIDHasPrefix(v string) predicate.GCPServiceAccount { + return predicate.GCPServiceAccount(sql.FieldHasPrefix(FieldScopeID, v)) +} + +// ScopeIDHasSuffix applies the HasSuffix predicate on the "scope_id" field. +func ScopeIDHasSuffix(v string) predicate.GCPServiceAccount { + return predicate.GCPServiceAccount(sql.FieldHasSuffix(FieldScopeID, v)) +} + +// ScopeIDEqualFold applies the EqualFold predicate on the "scope_id" field. +func ScopeIDEqualFold(v string) predicate.GCPServiceAccount { + return predicate.GCPServiceAccount(sql.FieldEqualFold(FieldScopeID, v)) +} + +// ScopeIDContainsFold applies the ContainsFold predicate on the "scope_id" field. +func ScopeIDContainsFold(v string) predicate.GCPServiceAccount { + return predicate.GCPServiceAccount(sql.FieldContainsFold(FieldScopeID, v)) +} + +// EmailEQ applies the EQ predicate on the "email" field. +func EmailEQ(v string) predicate.GCPServiceAccount { + return predicate.GCPServiceAccount(sql.FieldEQ(FieldEmail, v)) +} + +// EmailNEQ applies the NEQ predicate on the "email" field. +func EmailNEQ(v string) predicate.GCPServiceAccount { + return predicate.GCPServiceAccount(sql.FieldNEQ(FieldEmail, v)) +} + +// EmailIn applies the In predicate on the "email" field. +func EmailIn(vs ...string) predicate.GCPServiceAccount { + return predicate.GCPServiceAccount(sql.FieldIn(FieldEmail, vs...)) +} + +// EmailNotIn applies the NotIn predicate on the "email" field. +func EmailNotIn(vs ...string) predicate.GCPServiceAccount { + return predicate.GCPServiceAccount(sql.FieldNotIn(FieldEmail, vs...)) +} + +// EmailGT applies the GT predicate on the "email" field. +func EmailGT(v string) predicate.GCPServiceAccount { + return predicate.GCPServiceAccount(sql.FieldGT(FieldEmail, v)) +} + +// EmailGTE applies the GTE predicate on the "email" field. +func EmailGTE(v string) predicate.GCPServiceAccount { + return predicate.GCPServiceAccount(sql.FieldGTE(FieldEmail, v)) +} + +// EmailLT applies the LT predicate on the "email" field. +func EmailLT(v string) predicate.GCPServiceAccount { + return predicate.GCPServiceAccount(sql.FieldLT(FieldEmail, v)) +} + +// EmailLTE applies the LTE predicate on the "email" field. +func EmailLTE(v string) predicate.GCPServiceAccount { + return predicate.GCPServiceAccount(sql.FieldLTE(FieldEmail, v)) +} + +// EmailContains applies the Contains predicate on the "email" field. +func EmailContains(v string) predicate.GCPServiceAccount { + return predicate.GCPServiceAccount(sql.FieldContains(FieldEmail, v)) +} + +// EmailHasPrefix applies the HasPrefix predicate on the "email" field. +func EmailHasPrefix(v string) predicate.GCPServiceAccount { + return predicate.GCPServiceAccount(sql.FieldHasPrefix(FieldEmail, v)) +} + +// EmailHasSuffix applies the HasSuffix predicate on the "email" field. +func EmailHasSuffix(v string) predicate.GCPServiceAccount { + return predicate.GCPServiceAccount(sql.FieldHasSuffix(FieldEmail, v)) +} + +// EmailEqualFold applies the EqualFold predicate on the "email" field. +func EmailEqualFold(v string) predicate.GCPServiceAccount { + return predicate.GCPServiceAccount(sql.FieldEqualFold(FieldEmail, v)) +} + +// EmailContainsFold applies the ContainsFold predicate on the "email" field. +func EmailContainsFold(v string) predicate.GCPServiceAccount { + return predicate.GCPServiceAccount(sql.FieldContainsFold(FieldEmail, v)) +} + +// ProjectIDEQ applies the EQ predicate on the "project_id" field. +func ProjectIDEQ(v string) predicate.GCPServiceAccount { + return predicate.GCPServiceAccount(sql.FieldEQ(FieldProjectID, v)) +} + +// ProjectIDNEQ applies the NEQ predicate on the "project_id" field. +func ProjectIDNEQ(v string) predicate.GCPServiceAccount { + return predicate.GCPServiceAccount(sql.FieldNEQ(FieldProjectID, v)) +} + +// ProjectIDIn applies the In predicate on the "project_id" field. +func ProjectIDIn(vs ...string) predicate.GCPServiceAccount { + return predicate.GCPServiceAccount(sql.FieldIn(FieldProjectID, vs...)) +} + +// ProjectIDNotIn applies the NotIn predicate on the "project_id" field. +func ProjectIDNotIn(vs ...string) predicate.GCPServiceAccount { + return predicate.GCPServiceAccount(sql.FieldNotIn(FieldProjectID, vs...)) +} + +// ProjectIDGT applies the GT predicate on the "project_id" field. +func ProjectIDGT(v string) predicate.GCPServiceAccount { + return predicate.GCPServiceAccount(sql.FieldGT(FieldProjectID, v)) +} + +// ProjectIDGTE applies the GTE predicate on the "project_id" field. +func ProjectIDGTE(v string) predicate.GCPServiceAccount { + return predicate.GCPServiceAccount(sql.FieldGTE(FieldProjectID, v)) +} + +// ProjectIDLT applies the LT predicate on the "project_id" field. +func ProjectIDLT(v string) predicate.GCPServiceAccount { + return predicate.GCPServiceAccount(sql.FieldLT(FieldProjectID, v)) +} + +// ProjectIDLTE applies the LTE predicate on the "project_id" field. +func ProjectIDLTE(v string) predicate.GCPServiceAccount { + return predicate.GCPServiceAccount(sql.FieldLTE(FieldProjectID, v)) +} + +// ProjectIDContains applies the Contains predicate on the "project_id" field. +func ProjectIDContains(v string) predicate.GCPServiceAccount { + return predicate.GCPServiceAccount(sql.FieldContains(FieldProjectID, v)) +} + +// ProjectIDHasPrefix applies the HasPrefix predicate on the "project_id" field. +func ProjectIDHasPrefix(v string) predicate.GCPServiceAccount { + return predicate.GCPServiceAccount(sql.FieldHasPrefix(FieldProjectID, v)) +} + +// ProjectIDHasSuffix applies the HasSuffix predicate on the "project_id" field. +func ProjectIDHasSuffix(v string) predicate.GCPServiceAccount { + return predicate.GCPServiceAccount(sql.FieldHasSuffix(FieldProjectID, v)) +} + +// ProjectIDEqualFold applies the EqualFold predicate on the "project_id" field. +func ProjectIDEqualFold(v string) predicate.GCPServiceAccount { + return predicate.GCPServiceAccount(sql.FieldEqualFold(FieldProjectID, v)) +} + +// ProjectIDContainsFold applies the ContainsFold predicate on the "project_id" field. +func ProjectIDContainsFold(v string) predicate.GCPServiceAccount { + return predicate.GCPServiceAccount(sql.FieldContainsFold(FieldProjectID, v)) +} + +// DisplayNameEQ applies the EQ predicate on the "display_name" field. +func DisplayNameEQ(v string) predicate.GCPServiceAccount { + return predicate.GCPServiceAccount(sql.FieldEQ(FieldDisplayName, v)) +} + +// DisplayNameNEQ applies the NEQ predicate on the "display_name" field. +func DisplayNameNEQ(v string) predicate.GCPServiceAccount { + return predicate.GCPServiceAccount(sql.FieldNEQ(FieldDisplayName, v)) +} + +// DisplayNameIn applies the In predicate on the "display_name" field. +func DisplayNameIn(vs ...string) predicate.GCPServiceAccount { + return predicate.GCPServiceAccount(sql.FieldIn(FieldDisplayName, vs...)) +} + +// DisplayNameNotIn applies the NotIn predicate on the "display_name" field. +func DisplayNameNotIn(vs ...string) predicate.GCPServiceAccount { + return predicate.GCPServiceAccount(sql.FieldNotIn(FieldDisplayName, vs...)) +} + +// DisplayNameGT applies the GT predicate on the "display_name" field. +func DisplayNameGT(v string) predicate.GCPServiceAccount { + return predicate.GCPServiceAccount(sql.FieldGT(FieldDisplayName, v)) +} + +// DisplayNameGTE applies the GTE predicate on the "display_name" field. +func DisplayNameGTE(v string) predicate.GCPServiceAccount { + return predicate.GCPServiceAccount(sql.FieldGTE(FieldDisplayName, v)) +} + +// DisplayNameLT applies the LT predicate on the "display_name" field. +func DisplayNameLT(v string) predicate.GCPServiceAccount { + return predicate.GCPServiceAccount(sql.FieldLT(FieldDisplayName, v)) +} + +// DisplayNameLTE applies the LTE predicate on the "display_name" field. +func DisplayNameLTE(v string) predicate.GCPServiceAccount { + return predicate.GCPServiceAccount(sql.FieldLTE(FieldDisplayName, v)) +} + +// DisplayNameContains applies the Contains predicate on the "display_name" field. +func DisplayNameContains(v string) predicate.GCPServiceAccount { + return predicate.GCPServiceAccount(sql.FieldContains(FieldDisplayName, v)) +} + +// DisplayNameHasPrefix applies the HasPrefix predicate on the "display_name" field. +func DisplayNameHasPrefix(v string) predicate.GCPServiceAccount { + return predicate.GCPServiceAccount(sql.FieldHasPrefix(FieldDisplayName, v)) +} + +// DisplayNameHasSuffix applies the HasSuffix predicate on the "display_name" field. +func DisplayNameHasSuffix(v string) predicate.GCPServiceAccount { + return predicate.GCPServiceAccount(sql.FieldHasSuffix(FieldDisplayName, v)) +} + +// DisplayNameEqualFold applies the EqualFold predicate on the "display_name" field. +func DisplayNameEqualFold(v string) predicate.GCPServiceAccount { + return predicate.GCPServiceAccount(sql.FieldEqualFold(FieldDisplayName, v)) +} + +// DisplayNameContainsFold applies the ContainsFold predicate on the "display_name" field. +func DisplayNameContainsFold(v string) predicate.GCPServiceAccount { + return predicate.GCPServiceAccount(sql.FieldContainsFold(FieldDisplayName, v)) +} + +// DefaultScopesEQ applies the EQ predicate on the "default_scopes" field. +func DefaultScopesEQ(v string) predicate.GCPServiceAccount { + return predicate.GCPServiceAccount(sql.FieldEQ(FieldDefaultScopes, v)) +} + +// DefaultScopesNEQ applies the NEQ predicate on the "default_scopes" field. +func DefaultScopesNEQ(v string) predicate.GCPServiceAccount { + return predicate.GCPServiceAccount(sql.FieldNEQ(FieldDefaultScopes, v)) +} + +// DefaultScopesIn applies the In predicate on the "default_scopes" field. +func DefaultScopesIn(vs ...string) predicate.GCPServiceAccount { + return predicate.GCPServiceAccount(sql.FieldIn(FieldDefaultScopes, vs...)) +} + +// DefaultScopesNotIn applies the NotIn predicate on the "default_scopes" field. +func DefaultScopesNotIn(vs ...string) predicate.GCPServiceAccount { + return predicate.GCPServiceAccount(sql.FieldNotIn(FieldDefaultScopes, vs...)) +} + +// DefaultScopesGT applies the GT predicate on the "default_scopes" field. +func DefaultScopesGT(v string) predicate.GCPServiceAccount { + return predicate.GCPServiceAccount(sql.FieldGT(FieldDefaultScopes, v)) +} + +// DefaultScopesGTE applies the GTE predicate on the "default_scopes" field. +func DefaultScopesGTE(v string) predicate.GCPServiceAccount { + return predicate.GCPServiceAccount(sql.FieldGTE(FieldDefaultScopes, v)) +} + +// DefaultScopesLT applies the LT predicate on the "default_scopes" field. +func DefaultScopesLT(v string) predicate.GCPServiceAccount { + return predicate.GCPServiceAccount(sql.FieldLT(FieldDefaultScopes, v)) +} + +// DefaultScopesLTE applies the LTE predicate on the "default_scopes" field. +func DefaultScopesLTE(v string) predicate.GCPServiceAccount { + return predicate.GCPServiceAccount(sql.FieldLTE(FieldDefaultScopes, v)) +} + +// DefaultScopesContains applies the Contains predicate on the "default_scopes" field. +func DefaultScopesContains(v string) predicate.GCPServiceAccount { + return predicate.GCPServiceAccount(sql.FieldContains(FieldDefaultScopes, v)) +} + +// DefaultScopesHasPrefix applies the HasPrefix predicate on the "default_scopes" field. +func DefaultScopesHasPrefix(v string) predicate.GCPServiceAccount { + return predicate.GCPServiceAccount(sql.FieldHasPrefix(FieldDefaultScopes, v)) +} + +// DefaultScopesHasSuffix applies the HasSuffix predicate on the "default_scopes" field. +func DefaultScopesHasSuffix(v string) predicate.GCPServiceAccount { + return predicate.GCPServiceAccount(sql.FieldHasSuffix(FieldDefaultScopes, v)) +} + +// DefaultScopesEqualFold applies the EqualFold predicate on the "default_scopes" field. +func DefaultScopesEqualFold(v string) predicate.GCPServiceAccount { + return predicate.GCPServiceAccount(sql.FieldEqualFold(FieldDefaultScopes, v)) +} + +// DefaultScopesContainsFold applies the ContainsFold predicate on the "default_scopes" field. +func DefaultScopesContainsFold(v string) predicate.GCPServiceAccount { + return predicate.GCPServiceAccount(sql.FieldContainsFold(FieldDefaultScopes, v)) +} + +// VerifiedEQ applies the EQ predicate on the "verified" field. +func VerifiedEQ(v bool) predicate.GCPServiceAccount { + return predicate.GCPServiceAccount(sql.FieldEQ(FieldVerified, v)) +} + +// VerifiedNEQ applies the NEQ predicate on the "verified" field. +func VerifiedNEQ(v bool) predicate.GCPServiceAccount { + return predicate.GCPServiceAccount(sql.FieldNEQ(FieldVerified, v)) +} + +// VerifiedAtEQ applies the EQ predicate on the "verified_at" field. +func VerifiedAtEQ(v time.Time) predicate.GCPServiceAccount { + return predicate.GCPServiceAccount(sql.FieldEQ(FieldVerifiedAt, v)) +} + +// VerifiedAtNEQ applies the NEQ predicate on the "verified_at" field. +func VerifiedAtNEQ(v time.Time) predicate.GCPServiceAccount { + return predicate.GCPServiceAccount(sql.FieldNEQ(FieldVerifiedAt, v)) +} + +// VerifiedAtIn applies the In predicate on the "verified_at" field. +func VerifiedAtIn(vs ...time.Time) predicate.GCPServiceAccount { + return predicate.GCPServiceAccount(sql.FieldIn(FieldVerifiedAt, vs...)) +} + +// VerifiedAtNotIn applies the NotIn predicate on the "verified_at" field. +func VerifiedAtNotIn(vs ...time.Time) predicate.GCPServiceAccount { + return predicate.GCPServiceAccount(sql.FieldNotIn(FieldVerifiedAt, vs...)) +} + +// VerifiedAtGT applies the GT predicate on the "verified_at" field. +func VerifiedAtGT(v time.Time) predicate.GCPServiceAccount { + return predicate.GCPServiceAccount(sql.FieldGT(FieldVerifiedAt, v)) +} + +// VerifiedAtGTE applies the GTE predicate on the "verified_at" field. +func VerifiedAtGTE(v time.Time) predicate.GCPServiceAccount { + return predicate.GCPServiceAccount(sql.FieldGTE(FieldVerifiedAt, v)) +} + +// VerifiedAtLT applies the LT predicate on the "verified_at" field. +func VerifiedAtLT(v time.Time) predicate.GCPServiceAccount { + return predicate.GCPServiceAccount(sql.FieldLT(FieldVerifiedAt, v)) +} + +// VerifiedAtLTE applies the LTE predicate on the "verified_at" field. +func VerifiedAtLTE(v time.Time) predicate.GCPServiceAccount { + return predicate.GCPServiceAccount(sql.FieldLTE(FieldVerifiedAt, v)) +} + +// VerifiedAtIsNil applies the IsNil predicate on the "verified_at" field. +func VerifiedAtIsNil() predicate.GCPServiceAccount { + return predicate.GCPServiceAccount(sql.FieldIsNull(FieldVerifiedAt)) +} + +// VerifiedAtNotNil applies the NotNil predicate on the "verified_at" field. +func VerifiedAtNotNil() predicate.GCPServiceAccount { + return predicate.GCPServiceAccount(sql.FieldNotNull(FieldVerifiedAt)) +} + +// CreatedByEQ applies the EQ predicate on the "created_by" field. +func CreatedByEQ(v string) predicate.GCPServiceAccount { + return predicate.GCPServiceAccount(sql.FieldEQ(FieldCreatedBy, v)) +} + +// CreatedByNEQ applies the NEQ predicate on the "created_by" field. +func CreatedByNEQ(v string) predicate.GCPServiceAccount { + return predicate.GCPServiceAccount(sql.FieldNEQ(FieldCreatedBy, v)) +} + +// CreatedByIn applies the In predicate on the "created_by" field. +func CreatedByIn(vs ...string) predicate.GCPServiceAccount { + return predicate.GCPServiceAccount(sql.FieldIn(FieldCreatedBy, vs...)) +} + +// CreatedByNotIn applies the NotIn predicate on the "created_by" field. +func CreatedByNotIn(vs ...string) predicate.GCPServiceAccount { + return predicate.GCPServiceAccount(sql.FieldNotIn(FieldCreatedBy, vs...)) +} + +// CreatedByGT applies the GT predicate on the "created_by" field. +func CreatedByGT(v string) predicate.GCPServiceAccount { + return predicate.GCPServiceAccount(sql.FieldGT(FieldCreatedBy, v)) +} + +// CreatedByGTE applies the GTE predicate on the "created_by" field. +func CreatedByGTE(v string) predicate.GCPServiceAccount { + return predicate.GCPServiceAccount(sql.FieldGTE(FieldCreatedBy, v)) +} + +// CreatedByLT applies the LT predicate on the "created_by" field. +func CreatedByLT(v string) predicate.GCPServiceAccount { + return predicate.GCPServiceAccount(sql.FieldLT(FieldCreatedBy, v)) +} + +// CreatedByLTE applies the LTE predicate on the "created_by" field. +func CreatedByLTE(v string) predicate.GCPServiceAccount { + return predicate.GCPServiceAccount(sql.FieldLTE(FieldCreatedBy, v)) +} + +// CreatedByContains applies the Contains predicate on the "created_by" field. +func CreatedByContains(v string) predicate.GCPServiceAccount { + return predicate.GCPServiceAccount(sql.FieldContains(FieldCreatedBy, v)) +} + +// CreatedByHasPrefix applies the HasPrefix predicate on the "created_by" field. +func CreatedByHasPrefix(v string) predicate.GCPServiceAccount { + return predicate.GCPServiceAccount(sql.FieldHasPrefix(FieldCreatedBy, v)) +} + +// CreatedByHasSuffix applies the HasSuffix predicate on the "created_by" field. +func CreatedByHasSuffix(v string) predicate.GCPServiceAccount { + return predicate.GCPServiceAccount(sql.FieldHasSuffix(FieldCreatedBy, v)) +} + +// CreatedByEqualFold applies the EqualFold predicate on the "created_by" field. +func CreatedByEqualFold(v string) predicate.GCPServiceAccount { + return predicate.GCPServiceAccount(sql.FieldEqualFold(FieldCreatedBy, v)) +} + +// CreatedByContainsFold applies the ContainsFold predicate on the "created_by" field. +func CreatedByContainsFold(v string) predicate.GCPServiceAccount { + return predicate.GCPServiceAccount(sql.FieldContainsFold(FieldCreatedBy, v)) +} + +// ManagedEQ applies the EQ predicate on the "managed" field. +func ManagedEQ(v bool) predicate.GCPServiceAccount { + return predicate.GCPServiceAccount(sql.FieldEQ(FieldManaged, v)) +} + +// ManagedNEQ applies the NEQ predicate on the "managed" field. +func ManagedNEQ(v bool) predicate.GCPServiceAccount { + return predicate.GCPServiceAccount(sql.FieldNEQ(FieldManaged, v)) +} + +// ManagedByEQ applies the EQ predicate on the "managed_by" field. +func ManagedByEQ(v string) predicate.GCPServiceAccount { + return predicate.GCPServiceAccount(sql.FieldEQ(FieldManagedBy, v)) +} + +// ManagedByNEQ applies the NEQ predicate on the "managed_by" field. +func ManagedByNEQ(v string) predicate.GCPServiceAccount { + return predicate.GCPServiceAccount(sql.FieldNEQ(FieldManagedBy, v)) +} + +// ManagedByIn applies the In predicate on the "managed_by" field. +func ManagedByIn(vs ...string) predicate.GCPServiceAccount { + return predicate.GCPServiceAccount(sql.FieldIn(FieldManagedBy, vs...)) +} + +// ManagedByNotIn applies the NotIn predicate on the "managed_by" field. +func ManagedByNotIn(vs ...string) predicate.GCPServiceAccount { + return predicate.GCPServiceAccount(sql.FieldNotIn(FieldManagedBy, vs...)) +} + +// ManagedByGT applies the GT predicate on the "managed_by" field. +func ManagedByGT(v string) predicate.GCPServiceAccount { + return predicate.GCPServiceAccount(sql.FieldGT(FieldManagedBy, v)) +} + +// ManagedByGTE applies the GTE predicate on the "managed_by" field. +func ManagedByGTE(v string) predicate.GCPServiceAccount { + return predicate.GCPServiceAccount(sql.FieldGTE(FieldManagedBy, v)) +} + +// ManagedByLT applies the LT predicate on the "managed_by" field. +func ManagedByLT(v string) predicate.GCPServiceAccount { + return predicate.GCPServiceAccount(sql.FieldLT(FieldManagedBy, v)) +} + +// ManagedByLTE applies the LTE predicate on the "managed_by" field. +func ManagedByLTE(v string) predicate.GCPServiceAccount { + return predicate.GCPServiceAccount(sql.FieldLTE(FieldManagedBy, v)) +} + +// ManagedByContains applies the Contains predicate on the "managed_by" field. +func ManagedByContains(v string) predicate.GCPServiceAccount { + return predicate.GCPServiceAccount(sql.FieldContains(FieldManagedBy, v)) +} + +// ManagedByHasPrefix applies the HasPrefix predicate on the "managed_by" field. +func ManagedByHasPrefix(v string) predicate.GCPServiceAccount { + return predicate.GCPServiceAccount(sql.FieldHasPrefix(FieldManagedBy, v)) +} + +// ManagedByHasSuffix applies the HasSuffix predicate on the "managed_by" field. +func ManagedByHasSuffix(v string) predicate.GCPServiceAccount { + return predicate.GCPServiceAccount(sql.FieldHasSuffix(FieldManagedBy, v)) +} + +// ManagedByEqualFold applies the EqualFold predicate on the "managed_by" field. +func ManagedByEqualFold(v string) predicate.GCPServiceAccount { + return predicate.GCPServiceAccount(sql.FieldEqualFold(FieldManagedBy, v)) +} + +// ManagedByContainsFold applies the ContainsFold predicate on the "managed_by" field. +func ManagedByContainsFold(v string) predicate.GCPServiceAccount { + return predicate.GCPServiceAccount(sql.FieldContainsFold(FieldManagedBy, v)) +} + +// CreatedEQ applies the EQ predicate on the "created" field. +func CreatedEQ(v time.Time) predicate.GCPServiceAccount { + return predicate.GCPServiceAccount(sql.FieldEQ(FieldCreated, v)) +} + +// CreatedNEQ applies the NEQ predicate on the "created" field. +func CreatedNEQ(v time.Time) predicate.GCPServiceAccount { + return predicate.GCPServiceAccount(sql.FieldNEQ(FieldCreated, v)) +} + +// CreatedIn applies the In predicate on the "created" field. +func CreatedIn(vs ...time.Time) predicate.GCPServiceAccount { + return predicate.GCPServiceAccount(sql.FieldIn(FieldCreated, vs...)) +} + +// CreatedNotIn applies the NotIn predicate on the "created" field. +func CreatedNotIn(vs ...time.Time) predicate.GCPServiceAccount { + return predicate.GCPServiceAccount(sql.FieldNotIn(FieldCreated, vs...)) +} + +// CreatedGT applies the GT predicate on the "created" field. +func CreatedGT(v time.Time) predicate.GCPServiceAccount { + return predicate.GCPServiceAccount(sql.FieldGT(FieldCreated, v)) +} + +// CreatedGTE applies the GTE predicate on the "created" field. +func CreatedGTE(v time.Time) predicate.GCPServiceAccount { + return predicate.GCPServiceAccount(sql.FieldGTE(FieldCreated, v)) +} + +// CreatedLT applies the LT predicate on the "created" field. +func CreatedLT(v time.Time) predicate.GCPServiceAccount { + return predicate.GCPServiceAccount(sql.FieldLT(FieldCreated, v)) +} + +// CreatedLTE applies the LTE predicate on the "created" field. +func CreatedLTE(v time.Time) predicate.GCPServiceAccount { + return predicate.GCPServiceAccount(sql.FieldLTE(FieldCreated, v)) +} + +// And groups predicates with the AND operator between them. +func And(predicates ...predicate.GCPServiceAccount) predicate.GCPServiceAccount { + return predicate.GCPServiceAccount(sql.AndPredicates(predicates...)) +} + +// Or groups predicates with the OR operator between them. +func Or(predicates ...predicate.GCPServiceAccount) predicate.GCPServiceAccount { + return predicate.GCPServiceAccount(sql.OrPredicates(predicates...)) +} + +// Not applies the not operator on the given predicate. +func Not(p predicate.GCPServiceAccount) predicate.GCPServiceAccount { + return predicate.GCPServiceAccount(sql.NotPredicates(p)) +} diff --git a/pkg/ent/gcpserviceaccount_create.go b/pkg/ent/gcpserviceaccount_create.go new file mode 100644 index 000000000..509861cbf --- /dev/null +++ b/pkg/ent/gcpserviceaccount_create.go @@ -0,0 +1,1187 @@ +// Code generated by ent, DO NOT EDIT. + +package ent + +import ( + "context" + "errors" + "fmt" + "time" + + "entgo.io/ent/dialect" + "entgo.io/ent/dialect/sql" + "entgo.io/ent/dialect/sql/sqlgraph" + "entgo.io/ent/schema/field" + "github.com/GoogleCloudPlatform/scion/pkg/ent/gcpserviceaccount" + "github.com/google/uuid" +) + +// GCPServiceAccountCreate is the builder for creating a GCPServiceAccount entity. +type GCPServiceAccountCreate struct { + config + mutation *GCPServiceAccountMutation + hooks []Hook + conflict []sql.ConflictOption +} + +// SetScope sets the "scope" field. +func (_c *GCPServiceAccountCreate) SetScope(v string) *GCPServiceAccountCreate { + _c.mutation.SetScope(v) + return _c +} + +// SetScopeID sets the "scope_id" field. +func (_c *GCPServiceAccountCreate) SetScopeID(v string) *GCPServiceAccountCreate { + _c.mutation.SetScopeID(v) + return _c +} + +// SetEmail sets the "email" field. +func (_c *GCPServiceAccountCreate) SetEmail(v string) *GCPServiceAccountCreate { + _c.mutation.SetEmail(v) + return _c +} + +// SetProjectID sets the "project_id" field. +func (_c *GCPServiceAccountCreate) SetProjectID(v string) *GCPServiceAccountCreate { + _c.mutation.SetProjectID(v) + return _c +} + +// SetDisplayName sets the "display_name" field. +func (_c *GCPServiceAccountCreate) SetDisplayName(v string) *GCPServiceAccountCreate { + _c.mutation.SetDisplayName(v) + return _c +} + +// SetNillableDisplayName sets the "display_name" field if the given value is not nil. +func (_c *GCPServiceAccountCreate) SetNillableDisplayName(v *string) *GCPServiceAccountCreate { + if v != nil { + _c.SetDisplayName(*v) + } + return _c +} + +// SetDefaultScopes sets the "default_scopes" field. +func (_c *GCPServiceAccountCreate) SetDefaultScopes(v string) *GCPServiceAccountCreate { + _c.mutation.SetDefaultScopes(v) + return _c +} + +// SetNillableDefaultScopes sets the "default_scopes" field if the given value is not nil. +func (_c *GCPServiceAccountCreate) SetNillableDefaultScopes(v *string) *GCPServiceAccountCreate { + if v != nil { + _c.SetDefaultScopes(*v) + } + return _c +} + +// SetVerified sets the "verified" field. +func (_c *GCPServiceAccountCreate) SetVerified(v bool) *GCPServiceAccountCreate { + _c.mutation.SetVerified(v) + return _c +} + +// SetNillableVerified sets the "verified" field if the given value is not nil. +func (_c *GCPServiceAccountCreate) SetNillableVerified(v *bool) *GCPServiceAccountCreate { + if v != nil { + _c.SetVerified(*v) + } + return _c +} + +// SetVerifiedAt sets the "verified_at" field. +func (_c *GCPServiceAccountCreate) SetVerifiedAt(v time.Time) *GCPServiceAccountCreate { + _c.mutation.SetVerifiedAt(v) + return _c +} + +// SetNillableVerifiedAt sets the "verified_at" field if the given value is not nil. +func (_c *GCPServiceAccountCreate) SetNillableVerifiedAt(v *time.Time) *GCPServiceAccountCreate { + if v != nil { + _c.SetVerifiedAt(*v) + } + return _c +} + +// SetCreatedBy sets the "created_by" field. +func (_c *GCPServiceAccountCreate) SetCreatedBy(v string) *GCPServiceAccountCreate { + _c.mutation.SetCreatedBy(v) + return _c +} + +// SetNillableCreatedBy sets the "created_by" field if the given value is not nil. +func (_c *GCPServiceAccountCreate) SetNillableCreatedBy(v *string) *GCPServiceAccountCreate { + if v != nil { + _c.SetCreatedBy(*v) + } + return _c +} + +// SetManaged sets the "managed" field. +func (_c *GCPServiceAccountCreate) SetManaged(v bool) *GCPServiceAccountCreate { + _c.mutation.SetManaged(v) + return _c +} + +// SetNillableManaged sets the "managed" field if the given value is not nil. +func (_c *GCPServiceAccountCreate) SetNillableManaged(v *bool) *GCPServiceAccountCreate { + if v != nil { + _c.SetManaged(*v) + } + return _c +} + +// SetManagedBy sets the "managed_by" field. +func (_c *GCPServiceAccountCreate) SetManagedBy(v string) *GCPServiceAccountCreate { + _c.mutation.SetManagedBy(v) + return _c +} + +// SetNillableManagedBy sets the "managed_by" field if the given value is not nil. +func (_c *GCPServiceAccountCreate) SetNillableManagedBy(v *string) *GCPServiceAccountCreate { + if v != nil { + _c.SetManagedBy(*v) + } + return _c +} + +// SetCreated sets the "created" field. +func (_c *GCPServiceAccountCreate) SetCreated(v time.Time) *GCPServiceAccountCreate { + _c.mutation.SetCreated(v) + return _c +} + +// SetNillableCreated sets the "created" field if the given value is not nil. +func (_c *GCPServiceAccountCreate) SetNillableCreated(v *time.Time) *GCPServiceAccountCreate { + if v != nil { + _c.SetCreated(*v) + } + return _c +} + +// SetID sets the "id" field. +func (_c *GCPServiceAccountCreate) SetID(v uuid.UUID) *GCPServiceAccountCreate { + _c.mutation.SetID(v) + return _c +} + +// SetNillableID sets the "id" field if the given value is not nil. +func (_c *GCPServiceAccountCreate) SetNillableID(v *uuid.UUID) *GCPServiceAccountCreate { + if v != nil { + _c.SetID(*v) + } + return _c +} + +// Mutation returns the GCPServiceAccountMutation object of the builder. +func (_c *GCPServiceAccountCreate) Mutation() *GCPServiceAccountMutation { + return _c.mutation +} + +// Save creates the GCPServiceAccount in the database. +func (_c *GCPServiceAccountCreate) Save(ctx context.Context) (*GCPServiceAccount, error) { + _c.defaults() + return withHooks(ctx, _c.sqlSave, _c.mutation, _c.hooks) +} + +// SaveX calls Save and panics if Save returns an error. +func (_c *GCPServiceAccountCreate) SaveX(ctx context.Context) *GCPServiceAccount { + v, err := _c.Save(ctx) + if err != nil { + panic(err) + } + return v +} + +// Exec executes the query. +func (_c *GCPServiceAccountCreate) Exec(ctx context.Context) error { + _, err := _c.Save(ctx) + return err +} + +// ExecX is like Exec, but panics if an error occurs. +func (_c *GCPServiceAccountCreate) ExecX(ctx context.Context) { + if err := _c.Exec(ctx); err != nil { + panic(err) + } +} + +// defaults sets the default values of the builder before save. +func (_c *GCPServiceAccountCreate) defaults() { + if _, ok := _c.mutation.DisplayName(); !ok { + v := gcpserviceaccount.DefaultDisplayName + _c.mutation.SetDisplayName(v) + } + if _, ok := _c.mutation.DefaultScopes(); !ok { + v := gcpserviceaccount.DefaultDefaultScopes + _c.mutation.SetDefaultScopes(v) + } + if _, ok := _c.mutation.Verified(); !ok { + v := gcpserviceaccount.DefaultVerified + _c.mutation.SetVerified(v) + } + if _, ok := _c.mutation.CreatedBy(); !ok { + v := gcpserviceaccount.DefaultCreatedBy + _c.mutation.SetCreatedBy(v) + } + if _, ok := _c.mutation.Managed(); !ok { + v := gcpserviceaccount.DefaultManaged + _c.mutation.SetManaged(v) + } + if _, ok := _c.mutation.ManagedBy(); !ok { + v := gcpserviceaccount.DefaultManagedBy + _c.mutation.SetManagedBy(v) + } + if _, ok := _c.mutation.Created(); !ok { + v := gcpserviceaccount.DefaultCreated() + _c.mutation.SetCreated(v) + } + if _, ok := _c.mutation.ID(); !ok { + v := gcpserviceaccount.DefaultID() + _c.mutation.SetID(v) + } +} + +// check runs all checks and user-defined validators on the builder. +func (_c *GCPServiceAccountCreate) check() error { + if _, ok := _c.mutation.Scope(); !ok { + return &ValidationError{Name: "scope", err: errors.New(`ent: missing required field "GCPServiceAccount.scope"`)} + } + if v, ok := _c.mutation.Scope(); ok { + if err := gcpserviceaccount.ScopeValidator(v); err != nil { + return &ValidationError{Name: "scope", err: fmt.Errorf(`ent: validator failed for field "GCPServiceAccount.scope": %w`, err)} + } + } + if _, ok := _c.mutation.ScopeID(); !ok { + return &ValidationError{Name: "scope_id", err: errors.New(`ent: missing required field "GCPServiceAccount.scope_id"`)} + } + if v, ok := _c.mutation.ScopeID(); ok { + if err := gcpserviceaccount.ScopeIDValidator(v); err != nil { + return &ValidationError{Name: "scope_id", err: fmt.Errorf(`ent: validator failed for field "GCPServiceAccount.scope_id": %w`, err)} + } + } + if _, ok := _c.mutation.Email(); !ok { + return &ValidationError{Name: "email", err: errors.New(`ent: missing required field "GCPServiceAccount.email"`)} + } + if v, ok := _c.mutation.Email(); ok { + if err := gcpserviceaccount.EmailValidator(v); err != nil { + return &ValidationError{Name: "email", err: fmt.Errorf(`ent: validator failed for field "GCPServiceAccount.email": %w`, err)} + } + } + if _, ok := _c.mutation.ProjectID(); !ok { + return &ValidationError{Name: "project_id", err: errors.New(`ent: missing required field "GCPServiceAccount.project_id"`)} + } + if v, ok := _c.mutation.ProjectID(); ok { + if err := gcpserviceaccount.ProjectIDValidator(v); err != nil { + return &ValidationError{Name: "project_id", err: fmt.Errorf(`ent: validator failed for field "GCPServiceAccount.project_id": %w`, err)} + } + } + if _, ok := _c.mutation.DisplayName(); !ok { + return &ValidationError{Name: "display_name", err: errors.New(`ent: missing required field "GCPServiceAccount.display_name"`)} + } + if _, ok := _c.mutation.DefaultScopes(); !ok { + return &ValidationError{Name: "default_scopes", err: errors.New(`ent: missing required field "GCPServiceAccount.default_scopes"`)} + } + if _, ok := _c.mutation.Verified(); !ok { + return &ValidationError{Name: "verified", err: errors.New(`ent: missing required field "GCPServiceAccount.verified"`)} + } + if _, ok := _c.mutation.CreatedBy(); !ok { + return &ValidationError{Name: "created_by", err: errors.New(`ent: missing required field "GCPServiceAccount.created_by"`)} + } + if _, ok := _c.mutation.Managed(); !ok { + return &ValidationError{Name: "managed", err: errors.New(`ent: missing required field "GCPServiceAccount.managed"`)} + } + if _, ok := _c.mutation.ManagedBy(); !ok { + return &ValidationError{Name: "managed_by", err: errors.New(`ent: missing required field "GCPServiceAccount.managed_by"`)} + } + if _, ok := _c.mutation.Created(); !ok { + return &ValidationError{Name: "created", err: errors.New(`ent: missing required field "GCPServiceAccount.created"`)} + } + return nil +} + +func (_c *GCPServiceAccountCreate) sqlSave(ctx context.Context) (*GCPServiceAccount, error) { + if err := _c.check(); err != nil { + return nil, err + } + _node, _spec := _c.createSpec() + if err := sqlgraph.CreateNode(ctx, _c.driver, _spec); err != nil { + if sqlgraph.IsConstraintError(err) { + err = &ConstraintError{msg: err.Error(), wrap: err} + } + return nil, err + } + if _spec.ID.Value != nil { + if id, ok := _spec.ID.Value.(*uuid.UUID); ok { + _node.ID = *id + } else if err := _node.ID.Scan(_spec.ID.Value); err != nil { + return nil, err + } + } + _c.mutation.id = &_node.ID + _c.mutation.done = true + return _node, nil +} + +func (_c *GCPServiceAccountCreate) createSpec() (*GCPServiceAccount, *sqlgraph.CreateSpec) { + var ( + _node = &GCPServiceAccount{config: _c.config} + _spec = sqlgraph.NewCreateSpec(gcpserviceaccount.Table, sqlgraph.NewFieldSpec(gcpserviceaccount.FieldID, field.TypeUUID)) + ) + _spec.OnConflict = _c.conflict + if id, ok := _c.mutation.ID(); ok { + _node.ID = id + _spec.ID.Value = &id + } + if value, ok := _c.mutation.Scope(); ok { + _spec.SetField(gcpserviceaccount.FieldScope, field.TypeString, value) + _node.Scope = value + } + if value, ok := _c.mutation.ScopeID(); ok { + _spec.SetField(gcpserviceaccount.FieldScopeID, field.TypeString, value) + _node.ScopeID = value + } + if value, ok := _c.mutation.Email(); ok { + _spec.SetField(gcpserviceaccount.FieldEmail, field.TypeString, value) + _node.Email = value + } + if value, ok := _c.mutation.ProjectID(); ok { + _spec.SetField(gcpserviceaccount.FieldProjectID, field.TypeString, value) + _node.ProjectID = value + } + if value, ok := _c.mutation.DisplayName(); ok { + _spec.SetField(gcpserviceaccount.FieldDisplayName, field.TypeString, value) + _node.DisplayName = value + } + if value, ok := _c.mutation.DefaultScopes(); ok { + _spec.SetField(gcpserviceaccount.FieldDefaultScopes, field.TypeString, value) + _node.DefaultScopes = value + } + if value, ok := _c.mutation.Verified(); ok { + _spec.SetField(gcpserviceaccount.FieldVerified, field.TypeBool, value) + _node.Verified = value + } + if value, ok := _c.mutation.VerifiedAt(); ok { + _spec.SetField(gcpserviceaccount.FieldVerifiedAt, field.TypeTime, value) + _node.VerifiedAt = &value + } + if value, ok := _c.mutation.CreatedBy(); ok { + _spec.SetField(gcpserviceaccount.FieldCreatedBy, field.TypeString, value) + _node.CreatedBy = value + } + if value, ok := _c.mutation.Managed(); ok { + _spec.SetField(gcpserviceaccount.FieldManaged, field.TypeBool, value) + _node.Managed = value + } + if value, ok := _c.mutation.ManagedBy(); ok { + _spec.SetField(gcpserviceaccount.FieldManagedBy, field.TypeString, value) + _node.ManagedBy = value + } + if value, ok := _c.mutation.Created(); ok { + _spec.SetField(gcpserviceaccount.FieldCreated, field.TypeTime, value) + _node.Created = value + } + return _node, _spec +} + +// OnConflict allows configuring the `ON CONFLICT` / `ON DUPLICATE KEY` clause +// of the `INSERT` statement. For example: +// +// client.GCPServiceAccount.Create(). +// SetScope(v). +// OnConflict( +// // Update the row with the new values +// // the was proposed for insertion. +// sql.ResolveWithNewValues(), +// ). +// // Override some of the fields with custom +// // update values. +// Update(func(u *ent.GCPServiceAccountUpsert) { +// SetScope(v+v). +// }). +// Exec(ctx) +func (_c *GCPServiceAccountCreate) OnConflict(opts ...sql.ConflictOption) *GCPServiceAccountUpsertOne { + _c.conflict = opts + return &GCPServiceAccountUpsertOne{ + create: _c, + } +} + +// OnConflictColumns calls `OnConflict` and configures the columns +// as conflict target. Using this option is equivalent to using: +// +// client.GCPServiceAccount.Create(). +// OnConflict(sql.ConflictColumns(columns...)). +// Exec(ctx) +func (_c *GCPServiceAccountCreate) OnConflictColumns(columns ...string) *GCPServiceAccountUpsertOne { + _c.conflict = append(_c.conflict, sql.ConflictColumns(columns...)) + return &GCPServiceAccountUpsertOne{ + create: _c, + } +} + +type ( + // GCPServiceAccountUpsertOne is the builder for "upsert"-ing + // one GCPServiceAccount node. + GCPServiceAccountUpsertOne struct { + create *GCPServiceAccountCreate + } + + // GCPServiceAccountUpsert is the "OnConflict" setter. + GCPServiceAccountUpsert struct { + *sql.UpdateSet + } +) + +// SetScope sets the "scope" field. +func (u *GCPServiceAccountUpsert) SetScope(v string) *GCPServiceAccountUpsert { + u.Set(gcpserviceaccount.FieldScope, v) + return u +} + +// UpdateScope sets the "scope" field to the value that was provided on create. +func (u *GCPServiceAccountUpsert) UpdateScope() *GCPServiceAccountUpsert { + u.SetExcluded(gcpserviceaccount.FieldScope) + return u +} + +// SetScopeID sets the "scope_id" field. +func (u *GCPServiceAccountUpsert) SetScopeID(v string) *GCPServiceAccountUpsert { + u.Set(gcpserviceaccount.FieldScopeID, v) + return u +} + +// UpdateScopeID sets the "scope_id" field to the value that was provided on create. +func (u *GCPServiceAccountUpsert) UpdateScopeID() *GCPServiceAccountUpsert { + u.SetExcluded(gcpserviceaccount.FieldScopeID) + return u +} + +// SetEmail sets the "email" field. +func (u *GCPServiceAccountUpsert) SetEmail(v string) *GCPServiceAccountUpsert { + u.Set(gcpserviceaccount.FieldEmail, v) + return u +} + +// UpdateEmail sets the "email" field to the value that was provided on create. +func (u *GCPServiceAccountUpsert) UpdateEmail() *GCPServiceAccountUpsert { + u.SetExcluded(gcpserviceaccount.FieldEmail) + return u +} + +// SetProjectID sets the "project_id" field. +func (u *GCPServiceAccountUpsert) SetProjectID(v string) *GCPServiceAccountUpsert { + u.Set(gcpserviceaccount.FieldProjectID, v) + return u +} + +// UpdateProjectID sets the "project_id" field to the value that was provided on create. +func (u *GCPServiceAccountUpsert) UpdateProjectID() *GCPServiceAccountUpsert { + u.SetExcluded(gcpserviceaccount.FieldProjectID) + return u +} + +// SetDisplayName sets the "display_name" field. +func (u *GCPServiceAccountUpsert) SetDisplayName(v string) *GCPServiceAccountUpsert { + u.Set(gcpserviceaccount.FieldDisplayName, v) + return u +} + +// UpdateDisplayName sets the "display_name" field to the value that was provided on create. +func (u *GCPServiceAccountUpsert) UpdateDisplayName() *GCPServiceAccountUpsert { + u.SetExcluded(gcpserviceaccount.FieldDisplayName) + return u +} + +// SetDefaultScopes sets the "default_scopes" field. +func (u *GCPServiceAccountUpsert) SetDefaultScopes(v string) *GCPServiceAccountUpsert { + u.Set(gcpserviceaccount.FieldDefaultScopes, v) + return u +} + +// UpdateDefaultScopes sets the "default_scopes" field to the value that was provided on create. +func (u *GCPServiceAccountUpsert) UpdateDefaultScopes() *GCPServiceAccountUpsert { + u.SetExcluded(gcpserviceaccount.FieldDefaultScopes) + return u +} + +// SetVerified sets the "verified" field. +func (u *GCPServiceAccountUpsert) SetVerified(v bool) *GCPServiceAccountUpsert { + u.Set(gcpserviceaccount.FieldVerified, v) + return u +} + +// UpdateVerified sets the "verified" field to the value that was provided on create. +func (u *GCPServiceAccountUpsert) UpdateVerified() *GCPServiceAccountUpsert { + u.SetExcluded(gcpserviceaccount.FieldVerified) + return u +} + +// SetVerifiedAt sets the "verified_at" field. +func (u *GCPServiceAccountUpsert) SetVerifiedAt(v time.Time) *GCPServiceAccountUpsert { + u.Set(gcpserviceaccount.FieldVerifiedAt, v) + return u +} + +// UpdateVerifiedAt sets the "verified_at" field to the value that was provided on create. +func (u *GCPServiceAccountUpsert) UpdateVerifiedAt() *GCPServiceAccountUpsert { + u.SetExcluded(gcpserviceaccount.FieldVerifiedAt) + return u +} + +// ClearVerifiedAt clears the value of the "verified_at" field. +func (u *GCPServiceAccountUpsert) ClearVerifiedAt() *GCPServiceAccountUpsert { + u.SetNull(gcpserviceaccount.FieldVerifiedAt) + return u +} + +// SetCreatedBy sets the "created_by" field. +func (u *GCPServiceAccountUpsert) SetCreatedBy(v string) *GCPServiceAccountUpsert { + u.Set(gcpserviceaccount.FieldCreatedBy, v) + return u +} + +// UpdateCreatedBy sets the "created_by" field to the value that was provided on create. +func (u *GCPServiceAccountUpsert) UpdateCreatedBy() *GCPServiceAccountUpsert { + u.SetExcluded(gcpserviceaccount.FieldCreatedBy) + return u +} + +// SetManaged sets the "managed" field. +func (u *GCPServiceAccountUpsert) SetManaged(v bool) *GCPServiceAccountUpsert { + u.Set(gcpserviceaccount.FieldManaged, v) + return u +} + +// UpdateManaged sets the "managed" field to the value that was provided on create. +func (u *GCPServiceAccountUpsert) UpdateManaged() *GCPServiceAccountUpsert { + u.SetExcluded(gcpserviceaccount.FieldManaged) + return u +} + +// SetManagedBy sets the "managed_by" field. +func (u *GCPServiceAccountUpsert) SetManagedBy(v string) *GCPServiceAccountUpsert { + u.Set(gcpserviceaccount.FieldManagedBy, v) + return u +} + +// UpdateManagedBy sets the "managed_by" field to the value that was provided on create. +func (u *GCPServiceAccountUpsert) UpdateManagedBy() *GCPServiceAccountUpsert { + u.SetExcluded(gcpserviceaccount.FieldManagedBy) + return u +} + +// UpdateNewValues updates the mutable fields using the new values that were set on create except the ID field. +// Using this option is equivalent to using: +// +// client.GCPServiceAccount.Create(). +// OnConflict( +// sql.ResolveWithNewValues(), +// sql.ResolveWith(func(u *sql.UpdateSet) { +// u.SetIgnore(gcpserviceaccount.FieldID) +// }), +// ). +// Exec(ctx) +func (u *GCPServiceAccountUpsertOne) UpdateNewValues() *GCPServiceAccountUpsertOne { + u.create.conflict = append(u.create.conflict, sql.ResolveWithNewValues()) + u.create.conflict = append(u.create.conflict, sql.ResolveWith(func(s *sql.UpdateSet) { + if _, exists := u.create.mutation.ID(); exists { + s.SetIgnore(gcpserviceaccount.FieldID) + } + if _, exists := u.create.mutation.Created(); exists { + s.SetIgnore(gcpserviceaccount.FieldCreated) + } + })) + return u +} + +// Ignore sets each column to itself in case of conflict. +// Using this option is equivalent to using: +// +// client.GCPServiceAccount.Create(). +// OnConflict(sql.ResolveWithIgnore()). +// Exec(ctx) +func (u *GCPServiceAccountUpsertOne) Ignore() *GCPServiceAccountUpsertOne { + u.create.conflict = append(u.create.conflict, sql.ResolveWithIgnore()) + return u +} + +// DoNothing configures the conflict_action to `DO NOTHING`. +// Supported only by SQLite and PostgreSQL. +func (u *GCPServiceAccountUpsertOne) DoNothing() *GCPServiceAccountUpsertOne { + u.create.conflict = append(u.create.conflict, sql.DoNothing()) + return u +} + +// Update allows overriding fields `UPDATE` values. See the GCPServiceAccountCreate.OnConflict +// documentation for more info. +func (u *GCPServiceAccountUpsertOne) Update(set func(*GCPServiceAccountUpsert)) *GCPServiceAccountUpsertOne { + u.create.conflict = append(u.create.conflict, sql.ResolveWith(func(update *sql.UpdateSet) { + set(&GCPServiceAccountUpsert{UpdateSet: update}) + })) + return u +} + +// SetScope sets the "scope" field. +func (u *GCPServiceAccountUpsertOne) SetScope(v string) *GCPServiceAccountUpsertOne { + return u.Update(func(s *GCPServiceAccountUpsert) { + s.SetScope(v) + }) +} + +// UpdateScope sets the "scope" field to the value that was provided on create. +func (u *GCPServiceAccountUpsertOne) UpdateScope() *GCPServiceAccountUpsertOne { + return u.Update(func(s *GCPServiceAccountUpsert) { + s.UpdateScope() + }) +} + +// SetScopeID sets the "scope_id" field. +func (u *GCPServiceAccountUpsertOne) SetScopeID(v string) *GCPServiceAccountUpsertOne { + return u.Update(func(s *GCPServiceAccountUpsert) { + s.SetScopeID(v) + }) +} + +// UpdateScopeID sets the "scope_id" field to the value that was provided on create. +func (u *GCPServiceAccountUpsertOne) UpdateScopeID() *GCPServiceAccountUpsertOne { + return u.Update(func(s *GCPServiceAccountUpsert) { + s.UpdateScopeID() + }) +} + +// SetEmail sets the "email" field. +func (u *GCPServiceAccountUpsertOne) SetEmail(v string) *GCPServiceAccountUpsertOne { + return u.Update(func(s *GCPServiceAccountUpsert) { + s.SetEmail(v) + }) +} + +// UpdateEmail sets the "email" field to the value that was provided on create. +func (u *GCPServiceAccountUpsertOne) UpdateEmail() *GCPServiceAccountUpsertOne { + return u.Update(func(s *GCPServiceAccountUpsert) { + s.UpdateEmail() + }) +} + +// SetProjectID sets the "project_id" field. +func (u *GCPServiceAccountUpsertOne) SetProjectID(v string) *GCPServiceAccountUpsertOne { + return u.Update(func(s *GCPServiceAccountUpsert) { + s.SetProjectID(v) + }) +} + +// UpdateProjectID sets the "project_id" field to the value that was provided on create. +func (u *GCPServiceAccountUpsertOne) UpdateProjectID() *GCPServiceAccountUpsertOne { + return u.Update(func(s *GCPServiceAccountUpsert) { + s.UpdateProjectID() + }) +} + +// SetDisplayName sets the "display_name" field. +func (u *GCPServiceAccountUpsertOne) SetDisplayName(v string) *GCPServiceAccountUpsertOne { + return u.Update(func(s *GCPServiceAccountUpsert) { + s.SetDisplayName(v) + }) +} + +// UpdateDisplayName sets the "display_name" field to the value that was provided on create. +func (u *GCPServiceAccountUpsertOne) UpdateDisplayName() *GCPServiceAccountUpsertOne { + return u.Update(func(s *GCPServiceAccountUpsert) { + s.UpdateDisplayName() + }) +} + +// SetDefaultScopes sets the "default_scopes" field. +func (u *GCPServiceAccountUpsertOne) SetDefaultScopes(v string) *GCPServiceAccountUpsertOne { + return u.Update(func(s *GCPServiceAccountUpsert) { + s.SetDefaultScopes(v) + }) +} + +// UpdateDefaultScopes sets the "default_scopes" field to the value that was provided on create. +func (u *GCPServiceAccountUpsertOne) UpdateDefaultScopes() *GCPServiceAccountUpsertOne { + return u.Update(func(s *GCPServiceAccountUpsert) { + s.UpdateDefaultScopes() + }) +} + +// SetVerified sets the "verified" field. +func (u *GCPServiceAccountUpsertOne) SetVerified(v bool) *GCPServiceAccountUpsertOne { + return u.Update(func(s *GCPServiceAccountUpsert) { + s.SetVerified(v) + }) +} + +// UpdateVerified sets the "verified" field to the value that was provided on create. +func (u *GCPServiceAccountUpsertOne) UpdateVerified() *GCPServiceAccountUpsertOne { + return u.Update(func(s *GCPServiceAccountUpsert) { + s.UpdateVerified() + }) +} + +// SetVerifiedAt sets the "verified_at" field. +func (u *GCPServiceAccountUpsertOne) SetVerifiedAt(v time.Time) *GCPServiceAccountUpsertOne { + return u.Update(func(s *GCPServiceAccountUpsert) { + s.SetVerifiedAt(v) + }) +} + +// UpdateVerifiedAt sets the "verified_at" field to the value that was provided on create. +func (u *GCPServiceAccountUpsertOne) UpdateVerifiedAt() *GCPServiceAccountUpsertOne { + return u.Update(func(s *GCPServiceAccountUpsert) { + s.UpdateVerifiedAt() + }) +} + +// ClearVerifiedAt clears the value of the "verified_at" field. +func (u *GCPServiceAccountUpsertOne) ClearVerifiedAt() *GCPServiceAccountUpsertOne { + return u.Update(func(s *GCPServiceAccountUpsert) { + s.ClearVerifiedAt() + }) +} + +// SetCreatedBy sets the "created_by" field. +func (u *GCPServiceAccountUpsertOne) SetCreatedBy(v string) *GCPServiceAccountUpsertOne { + return u.Update(func(s *GCPServiceAccountUpsert) { + s.SetCreatedBy(v) + }) +} + +// UpdateCreatedBy sets the "created_by" field to the value that was provided on create. +func (u *GCPServiceAccountUpsertOne) UpdateCreatedBy() *GCPServiceAccountUpsertOne { + return u.Update(func(s *GCPServiceAccountUpsert) { + s.UpdateCreatedBy() + }) +} + +// SetManaged sets the "managed" field. +func (u *GCPServiceAccountUpsertOne) SetManaged(v bool) *GCPServiceAccountUpsertOne { + return u.Update(func(s *GCPServiceAccountUpsert) { + s.SetManaged(v) + }) +} + +// UpdateManaged sets the "managed" field to the value that was provided on create. +func (u *GCPServiceAccountUpsertOne) UpdateManaged() *GCPServiceAccountUpsertOne { + return u.Update(func(s *GCPServiceAccountUpsert) { + s.UpdateManaged() + }) +} + +// SetManagedBy sets the "managed_by" field. +func (u *GCPServiceAccountUpsertOne) SetManagedBy(v string) *GCPServiceAccountUpsertOne { + return u.Update(func(s *GCPServiceAccountUpsert) { + s.SetManagedBy(v) + }) +} + +// UpdateManagedBy sets the "managed_by" field to the value that was provided on create. +func (u *GCPServiceAccountUpsertOne) UpdateManagedBy() *GCPServiceAccountUpsertOne { + return u.Update(func(s *GCPServiceAccountUpsert) { + s.UpdateManagedBy() + }) +} + +// Exec executes the query. +func (u *GCPServiceAccountUpsertOne) Exec(ctx context.Context) error { + if len(u.create.conflict) == 0 { + return errors.New("ent: missing options for GCPServiceAccountCreate.OnConflict") + } + return u.create.Exec(ctx) +} + +// ExecX is like Exec, but panics if an error occurs. +func (u *GCPServiceAccountUpsertOne) ExecX(ctx context.Context) { + if err := u.create.Exec(ctx); err != nil { + panic(err) + } +} + +// Exec executes the UPSERT query and returns the inserted/updated ID. +func (u *GCPServiceAccountUpsertOne) ID(ctx context.Context) (id uuid.UUID, err error) { + if u.create.driver.Dialect() == dialect.MySQL { + // In case of "ON CONFLICT", there is no way to get back non-numeric ID + // fields from the database since MySQL does not support the RETURNING clause. + return id, errors.New("ent: GCPServiceAccountUpsertOne.ID is not supported by MySQL driver. Use GCPServiceAccountUpsertOne.Exec instead") + } + node, err := u.create.Save(ctx) + if err != nil { + return id, err + } + return node.ID, nil +} + +// IDX is like ID, but panics if an error occurs. +func (u *GCPServiceAccountUpsertOne) IDX(ctx context.Context) uuid.UUID { + id, err := u.ID(ctx) + if err != nil { + panic(err) + } + return id +} + +// GCPServiceAccountCreateBulk is the builder for creating many GCPServiceAccount entities in bulk. +type GCPServiceAccountCreateBulk struct { + config + err error + builders []*GCPServiceAccountCreate + conflict []sql.ConflictOption +} + +// Save creates the GCPServiceAccount entities in the database. +func (_c *GCPServiceAccountCreateBulk) Save(ctx context.Context) ([]*GCPServiceAccount, error) { + if _c.err != nil { + return nil, _c.err + } + specs := make([]*sqlgraph.CreateSpec, len(_c.builders)) + nodes := make([]*GCPServiceAccount, len(_c.builders)) + mutators := make([]Mutator, len(_c.builders)) + for i := range _c.builders { + func(i int, root context.Context) { + builder := _c.builders[i] + builder.defaults() + var mut Mutator = MutateFunc(func(ctx context.Context, m Mutation) (Value, error) { + mutation, ok := m.(*GCPServiceAccountMutation) + if !ok { + return nil, fmt.Errorf("unexpected mutation type %T", m) + } + if err := builder.check(); err != nil { + return nil, err + } + builder.mutation = mutation + var err error + nodes[i], specs[i] = builder.createSpec() + if i < len(mutators)-1 { + _, err = mutators[i+1].Mutate(root, _c.builders[i+1].mutation) + } else { + spec := &sqlgraph.BatchCreateSpec{Nodes: specs} + spec.OnConflict = _c.conflict + // Invoke the actual operation on the latest mutation in the chain. + if err = sqlgraph.BatchCreate(ctx, _c.driver, spec); err != nil { + if sqlgraph.IsConstraintError(err) { + err = &ConstraintError{msg: err.Error(), wrap: err} + } + } + } + if err != nil { + return nil, err + } + mutation.id = &nodes[i].ID + mutation.done = true + return nodes[i], nil + }) + for i := len(builder.hooks) - 1; i >= 0; i-- { + mut = builder.hooks[i](mut) + } + mutators[i] = mut + }(i, ctx) + } + if len(mutators) > 0 { + if _, err := mutators[0].Mutate(ctx, _c.builders[0].mutation); err != nil { + return nil, err + } + } + return nodes, nil +} + +// SaveX is like Save, but panics if an error occurs. +func (_c *GCPServiceAccountCreateBulk) SaveX(ctx context.Context) []*GCPServiceAccount { + v, err := _c.Save(ctx) + if err != nil { + panic(err) + } + return v +} + +// Exec executes the query. +func (_c *GCPServiceAccountCreateBulk) Exec(ctx context.Context) error { + _, err := _c.Save(ctx) + return err +} + +// ExecX is like Exec, but panics if an error occurs. +func (_c *GCPServiceAccountCreateBulk) ExecX(ctx context.Context) { + if err := _c.Exec(ctx); err != nil { + panic(err) + } +} + +// OnConflict allows configuring the `ON CONFLICT` / `ON DUPLICATE KEY` clause +// of the `INSERT` statement. For example: +// +// client.GCPServiceAccount.CreateBulk(builders...). +// OnConflict( +// // Update the row with the new values +// // the was proposed for insertion. +// sql.ResolveWithNewValues(), +// ). +// // Override some of the fields with custom +// // update values. +// Update(func(u *ent.GCPServiceAccountUpsert) { +// SetScope(v+v). +// }). +// Exec(ctx) +func (_c *GCPServiceAccountCreateBulk) OnConflict(opts ...sql.ConflictOption) *GCPServiceAccountUpsertBulk { + _c.conflict = opts + return &GCPServiceAccountUpsertBulk{ + create: _c, + } +} + +// OnConflictColumns calls `OnConflict` and configures the columns +// as conflict target. Using this option is equivalent to using: +// +// client.GCPServiceAccount.Create(). +// OnConflict(sql.ConflictColumns(columns...)). +// Exec(ctx) +func (_c *GCPServiceAccountCreateBulk) OnConflictColumns(columns ...string) *GCPServiceAccountUpsertBulk { + _c.conflict = append(_c.conflict, sql.ConflictColumns(columns...)) + return &GCPServiceAccountUpsertBulk{ + create: _c, + } +} + +// GCPServiceAccountUpsertBulk is the builder for "upsert"-ing +// a bulk of GCPServiceAccount nodes. +type GCPServiceAccountUpsertBulk struct { + create *GCPServiceAccountCreateBulk +} + +// UpdateNewValues updates the mutable fields using the new values that +// were set on create. Using this option is equivalent to using: +// +// client.GCPServiceAccount.Create(). +// OnConflict( +// sql.ResolveWithNewValues(), +// sql.ResolveWith(func(u *sql.UpdateSet) { +// u.SetIgnore(gcpserviceaccount.FieldID) +// }), +// ). +// Exec(ctx) +func (u *GCPServiceAccountUpsertBulk) UpdateNewValues() *GCPServiceAccountUpsertBulk { + u.create.conflict = append(u.create.conflict, sql.ResolveWithNewValues()) + u.create.conflict = append(u.create.conflict, sql.ResolveWith(func(s *sql.UpdateSet) { + for _, b := range u.create.builders { + if _, exists := b.mutation.ID(); exists { + s.SetIgnore(gcpserviceaccount.FieldID) + } + if _, exists := b.mutation.Created(); exists { + s.SetIgnore(gcpserviceaccount.FieldCreated) + } + } + })) + return u +} + +// Ignore sets each column to itself in case of conflict. +// Using this option is equivalent to using: +// +// client.GCPServiceAccount.Create(). +// OnConflict(sql.ResolveWithIgnore()). +// Exec(ctx) +func (u *GCPServiceAccountUpsertBulk) Ignore() *GCPServiceAccountUpsertBulk { + u.create.conflict = append(u.create.conflict, sql.ResolveWithIgnore()) + return u +} + +// DoNothing configures the conflict_action to `DO NOTHING`. +// Supported only by SQLite and PostgreSQL. +func (u *GCPServiceAccountUpsertBulk) DoNothing() *GCPServiceAccountUpsertBulk { + u.create.conflict = append(u.create.conflict, sql.DoNothing()) + return u +} + +// Update allows overriding fields `UPDATE` values. See the GCPServiceAccountCreateBulk.OnConflict +// documentation for more info. +func (u *GCPServiceAccountUpsertBulk) Update(set func(*GCPServiceAccountUpsert)) *GCPServiceAccountUpsertBulk { + u.create.conflict = append(u.create.conflict, sql.ResolveWith(func(update *sql.UpdateSet) { + set(&GCPServiceAccountUpsert{UpdateSet: update}) + })) + return u +} + +// SetScope sets the "scope" field. +func (u *GCPServiceAccountUpsertBulk) SetScope(v string) *GCPServiceAccountUpsertBulk { + return u.Update(func(s *GCPServiceAccountUpsert) { + s.SetScope(v) + }) +} + +// UpdateScope sets the "scope" field to the value that was provided on create. +func (u *GCPServiceAccountUpsertBulk) UpdateScope() *GCPServiceAccountUpsertBulk { + return u.Update(func(s *GCPServiceAccountUpsert) { + s.UpdateScope() + }) +} + +// SetScopeID sets the "scope_id" field. +func (u *GCPServiceAccountUpsertBulk) SetScopeID(v string) *GCPServiceAccountUpsertBulk { + return u.Update(func(s *GCPServiceAccountUpsert) { + s.SetScopeID(v) + }) +} + +// UpdateScopeID sets the "scope_id" field to the value that was provided on create. +func (u *GCPServiceAccountUpsertBulk) UpdateScopeID() *GCPServiceAccountUpsertBulk { + return u.Update(func(s *GCPServiceAccountUpsert) { + s.UpdateScopeID() + }) +} + +// SetEmail sets the "email" field. +func (u *GCPServiceAccountUpsertBulk) SetEmail(v string) *GCPServiceAccountUpsertBulk { + return u.Update(func(s *GCPServiceAccountUpsert) { + s.SetEmail(v) + }) +} + +// UpdateEmail sets the "email" field to the value that was provided on create. +func (u *GCPServiceAccountUpsertBulk) UpdateEmail() *GCPServiceAccountUpsertBulk { + return u.Update(func(s *GCPServiceAccountUpsert) { + s.UpdateEmail() + }) +} + +// SetProjectID sets the "project_id" field. +func (u *GCPServiceAccountUpsertBulk) SetProjectID(v string) *GCPServiceAccountUpsertBulk { + return u.Update(func(s *GCPServiceAccountUpsert) { + s.SetProjectID(v) + }) +} + +// UpdateProjectID sets the "project_id" field to the value that was provided on create. +func (u *GCPServiceAccountUpsertBulk) UpdateProjectID() *GCPServiceAccountUpsertBulk { + return u.Update(func(s *GCPServiceAccountUpsert) { + s.UpdateProjectID() + }) +} + +// SetDisplayName sets the "display_name" field. +func (u *GCPServiceAccountUpsertBulk) SetDisplayName(v string) *GCPServiceAccountUpsertBulk { + return u.Update(func(s *GCPServiceAccountUpsert) { + s.SetDisplayName(v) + }) +} + +// UpdateDisplayName sets the "display_name" field to the value that was provided on create. +func (u *GCPServiceAccountUpsertBulk) UpdateDisplayName() *GCPServiceAccountUpsertBulk { + return u.Update(func(s *GCPServiceAccountUpsert) { + s.UpdateDisplayName() + }) +} + +// SetDefaultScopes sets the "default_scopes" field. +func (u *GCPServiceAccountUpsertBulk) SetDefaultScopes(v string) *GCPServiceAccountUpsertBulk { + return u.Update(func(s *GCPServiceAccountUpsert) { + s.SetDefaultScopes(v) + }) +} + +// UpdateDefaultScopes sets the "default_scopes" field to the value that was provided on create. +func (u *GCPServiceAccountUpsertBulk) UpdateDefaultScopes() *GCPServiceAccountUpsertBulk { + return u.Update(func(s *GCPServiceAccountUpsert) { + s.UpdateDefaultScopes() + }) +} + +// SetVerified sets the "verified" field. +func (u *GCPServiceAccountUpsertBulk) SetVerified(v bool) *GCPServiceAccountUpsertBulk { + return u.Update(func(s *GCPServiceAccountUpsert) { + s.SetVerified(v) + }) +} + +// UpdateVerified sets the "verified" field to the value that was provided on create. +func (u *GCPServiceAccountUpsertBulk) UpdateVerified() *GCPServiceAccountUpsertBulk { + return u.Update(func(s *GCPServiceAccountUpsert) { + s.UpdateVerified() + }) +} + +// SetVerifiedAt sets the "verified_at" field. +func (u *GCPServiceAccountUpsertBulk) SetVerifiedAt(v time.Time) *GCPServiceAccountUpsertBulk { + return u.Update(func(s *GCPServiceAccountUpsert) { + s.SetVerifiedAt(v) + }) +} + +// UpdateVerifiedAt sets the "verified_at" field to the value that was provided on create. +func (u *GCPServiceAccountUpsertBulk) UpdateVerifiedAt() *GCPServiceAccountUpsertBulk { + return u.Update(func(s *GCPServiceAccountUpsert) { + s.UpdateVerifiedAt() + }) +} + +// ClearVerifiedAt clears the value of the "verified_at" field. +func (u *GCPServiceAccountUpsertBulk) ClearVerifiedAt() *GCPServiceAccountUpsertBulk { + return u.Update(func(s *GCPServiceAccountUpsert) { + s.ClearVerifiedAt() + }) +} + +// SetCreatedBy sets the "created_by" field. +func (u *GCPServiceAccountUpsertBulk) SetCreatedBy(v string) *GCPServiceAccountUpsertBulk { + return u.Update(func(s *GCPServiceAccountUpsert) { + s.SetCreatedBy(v) + }) +} + +// UpdateCreatedBy sets the "created_by" field to the value that was provided on create. +func (u *GCPServiceAccountUpsertBulk) UpdateCreatedBy() *GCPServiceAccountUpsertBulk { + return u.Update(func(s *GCPServiceAccountUpsert) { + s.UpdateCreatedBy() + }) +} + +// SetManaged sets the "managed" field. +func (u *GCPServiceAccountUpsertBulk) SetManaged(v bool) *GCPServiceAccountUpsertBulk { + return u.Update(func(s *GCPServiceAccountUpsert) { + s.SetManaged(v) + }) +} + +// UpdateManaged sets the "managed" field to the value that was provided on create. +func (u *GCPServiceAccountUpsertBulk) UpdateManaged() *GCPServiceAccountUpsertBulk { + return u.Update(func(s *GCPServiceAccountUpsert) { + s.UpdateManaged() + }) +} + +// SetManagedBy sets the "managed_by" field. +func (u *GCPServiceAccountUpsertBulk) SetManagedBy(v string) *GCPServiceAccountUpsertBulk { + return u.Update(func(s *GCPServiceAccountUpsert) { + s.SetManagedBy(v) + }) +} + +// UpdateManagedBy sets the "managed_by" field to the value that was provided on create. +func (u *GCPServiceAccountUpsertBulk) UpdateManagedBy() *GCPServiceAccountUpsertBulk { + return u.Update(func(s *GCPServiceAccountUpsert) { + s.UpdateManagedBy() + }) +} + +// Exec executes the query. +func (u *GCPServiceAccountUpsertBulk) Exec(ctx context.Context) error { + if u.create.err != nil { + return u.create.err + } + for i, b := range u.create.builders { + if len(b.conflict) != 0 { + return fmt.Errorf("ent: OnConflict was set for builder %d. Set it on the GCPServiceAccountCreateBulk instead", i) + } + } + if len(u.create.conflict) == 0 { + return errors.New("ent: missing options for GCPServiceAccountCreateBulk.OnConflict") + } + return u.create.Exec(ctx) +} + +// ExecX is like Exec, but panics if an error occurs. +func (u *GCPServiceAccountUpsertBulk) ExecX(ctx context.Context) { + if err := u.create.Exec(ctx); err != nil { + panic(err) + } +} diff --git a/pkg/ent/gcpserviceaccount_delete.go b/pkg/ent/gcpserviceaccount_delete.go new file mode 100644 index 000000000..5fbc7e0cc --- /dev/null +++ b/pkg/ent/gcpserviceaccount_delete.go @@ -0,0 +1,88 @@ +// Code generated by ent, DO NOT EDIT. + +package ent + +import ( + "context" + + "entgo.io/ent/dialect/sql" + "entgo.io/ent/dialect/sql/sqlgraph" + "entgo.io/ent/schema/field" + "github.com/GoogleCloudPlatform/scion/pkg/ent/gcpserviceaccount" + "github.com/GoogleCloudPlatform/scion/pkg/ent/predicate" +) + +// GCPServiceAccountDelete is the builder for deleting a GCPServiceAccount entity. +type GCPServiceAccountDelete struct { + config + hooks []Hook + mutation *GCPServiceAccountMutation +} + +// Where appends a list predicates to the GCPServiceAccountDelete builder. +func (_d *GCPServiceAccountDelete) Where(ps ...predicate.GCPServiceAccount) *GCPServiceAccountDelete { + _d.mutation.Where(ps...) + return _d +} + +// Exec executes the deletion query and returns how many vertices were deleted. +func (_d *GCPServiceAccountDelete) Exec(ctx context.Context) (int, error) { + return withHooks(ctx, _d.sqlExec, _d.mutation, _d.hooks) +} + +// ExecX is like Exec, but panics if an error occurs. +func (_d *GCPServiceAccountDelete) ExecX(ctx context.Context) int { + n, err := _d.Exec(ctx) + if err != nil { + panic(err) + } + return n +} + +func (_d *GCPServiceAccountDelete) sqlExec(ctx context.Context) (int, error) { + _spec := sqlgraph.NewDeleteSpec(gcpserviceaccount.Table, sqlgraph.NewFieldSpec(gcpserviceaccount.FieldID, field.TypeUUID)) + if ps := _d.mutation.predicates; len(ps) > 0 { + _spec.Predicate = func(selector *sql.Selector) { + for i := range ps { + ps[i](selector) + } + } + } + affected, err := sqlgraph.DeleteNodes(ctx, _d.driver, _spec) + if err != nil && sqlgraph.IsConstraintError(err) { + err = &ConstraintError{msg: err.Error(), wrap: err} + } + _d.mutation.done = true + return affected, err +} + +// GCPServiceAccountDeleteOne is the builder for deleting a single GCPServiceAccount entity. +type GCPServiceAccountDeleteOne struct { + _d *GCPServiceAccountDelete +} + +// Where appends a list predicates to the GCPServiceAccountDelete builder. +func (_d *GCPServiceAccountDeleteOne) Where(ps ...predicate.GCPServiceAccount) *GCPServiceAccountDeleteOne { + _d._d.mutation.Where(ps...) + return _d +} + +// Exec executes the deletion query. +func (_d *GCPServiceAccountDeleteOne) Exec(ctx context.Context) error { + n, err := _d._d.Exec(ctx) + switch { + case err != nil: + return err + case n == 0: + return &NotFoundError{gcpserviceaccount.Label} + default: + return nil + } +} + +// ExecX is like Exec, but panics if an error occurs. +func (_d *GCPServiceAccountDeleteOne) ExecX(ctx context.Context) { + if err := _d.Exec(ctx); err != nil { + panic(err) + } +} diff --git a/pkg/ent/gcpserviceaccount_query.go b/pkg/ent/gcpserviceaccount_query.go new file mode 100644 index 000000000..fe5657d10 --- /dev/null +++ b/pkg/ent/gcpserviceaccount_query.go @@ -0,0 +1,565 @@ +// Code generated by ent, DO NOT EDIT. + +package ent + +import ( + "context" + "fmt" + "math" + + "entgo.io/ent" + "entgo.io/ent/dialect" + "entgo.io/ent/dialect/sql" + "entgo.io/ent/dialect/sql/sqlgraph" + "entgo.io/ent/schema/field" + "github.com/GoogleCloudPlatform/scion/pkg/ent/gcpserviceaccount" + "github.com/GoogleCloudPlatform/scion/pkg/ent/predicate" + "github.com/google/uuid" +) + +// GCPServiceAccountQuery is the builder for querying GCPServiceAccount entities. +type GCPServiceAccountQuery struct { + config + ctx *QueryContext + order []gcpserviceaccount.OrderOption + inters []Interceptor + predicates []predicate.GCPServiceAccount + modifiers []func(*sql.Selector) + // intermediate query (i.e. traversal path). + sql *sql.Selector + path func(context.Context) (*sql.Selector, error) +} + +// Where adds a new predicate for the GCPServiceAccountQuery builder. +func (_q *GCPServiceAccountQuery) Where(ps ...predicate.GCPServiceAccount) *GCPServiceAccountQuery { + _q.predicates = append(_q.predicates, ps...) + return _q +} + +// Limit the number of records to be returned by this query. +func (_q *GCPServiceAccountQuery) Limit(limit int) *GCPServiceAccountQuery { + _q.ctx.Limit = &limit + return _q +} + +// Offset to start from. +func (_q *GCPServiceAccountQuery) Offset(offset int) *GCPServiceAccountQuery { + _q.ctx.Offset = &offset + return _q +} + +// Unique configures the query builder to filter duplicate records on query. +// By default, unique is set to true, and can be disabled using this method. +func (_q *GCPServiceAccountQuery) Unique(unique bool) *GCPServiceAccountQuery { + _q.ctx.Unique = &unique + return _q +} + +// Order specifies how the records should be ordered. +func (_q *GCPServiceAccountQuery) Order(o ...gcpserviceaccount.OrderOption) *GCPServiceAccountQuery { + _q.order = append(_q.order, o...) + return _q +} + +// First returns the first GCPServiceAccount entity from the query. +// Returns a *NotFoundError when no GCPServiceAccount was found. +func (_q *GCPServiceAccountQuery) First(ctx context.Context) (*GCPServiceAccount, error) { + nodes, err := _q.Limit(1).All(setContextOp(ctx, _q.ctx, ent.OpQueryFirst)) + if err != nil { + return nil, err + } + if len(nodes) == 0 { + return nil, &NotFoundError{gcpserviceaccount.Label} + } + return nodes[0], nil +} + +// FirstX is like First, but panics if an error occurs. +func (_q *GCPServiceAccountQuery) FirstX(ctx context.Context) *GCPServiceAccount { + node, err := _q.First(ctx) + if err != nil && !IsNotFound(err) { + panic(err) + } + return node +} + +// FirstID returns the first GCPServiceAccount ID from the query. +// Returns a *NotFoundError when no GCPServiceAccount ID was found. +func (_q *GCPServiceAccountQuery) FirstID(ctx context.Context) (id uuid.UUID, err error) { + var ids []uuid.UUID + if ids, err = _q.Limit(1).IDs(setContextOp(ctx, _q.ctx, ent.OpQueryFirstID)); err != nil { + return + } + if len(ids) == 0 { + err = &NotFoundError{gcpserviceaccount.Label} + return + } + return ids[0], nil +} + +// FirstIDX is like FirstID, but panics if an error occurs. +func (_q *GCPServiceAccountQuery) FirstIDX(ctx context.Context) uuid.UUID { + id, err := _q.FirstID(ctx) + if err != nil && !IsNotFound(err) { + panic(err) + } + return id +} + +// Only returns a single GCPServiceAccount entity found by the query, ensuring it only returns one. +// Returns a *NotSingularError when more than one GCPServiceAccount entity is found. +// Returns a *NotFoundError when no GCPServiceAccount entities are found. +func (_q *GCPServiceAccountQuery) Only(ctx context.Context) (*GCPServiceAccount, error) { + nodes, err := _q.Limit(2).All(setContextOp(ctx, _q.ctx, ent.OpQueryOnly)) + if err != nil { + return nil, err + } + switch len(nodes) { + case 1: + return nodes[0], nil + case 0: + return nil, &NotFoundError{gcpserviceaccount.Label} + default: + return nil, &NotSingularError{gcpserviceaccount.Label} + } +} + +// OnlyX is like Only, but panics if an error occurs. +func (_q *GCPServiceAccountQuery) OnlyX(ctx context.Context) *GCPServiceAccount { + node, err := _q.Only(ctx) + if err != nil { + panic(err) + } + return node +} + +// OnlyID is like Only, but returns the only GCPServiceAccount ID in the query. +// Returns a *NotSingularError when more than one GCPServiceAccount ID is found. +// Returns a *NotFoundError when no entities are found. +func (_q *GCPServiceAccountQuery) OnlyID(ctx context.Context) (id uuid.UUID, err error) { + var ids []uuid.UUID + if ids, err = _q.Limit(2).IDs(setContextOp(ctx, _q.ctx, ent.OpQueryOnlyID)); err != nil { + return + } + switch len(ids) { + case 1: + id = ids[0] + case 0: + err = &NotFoundError{gcpserviceaccount.Label} + default: + err = &NotSingularError{gcpserviceaccount.Label} + } + return +} + +// OnlyIDX is like OnlyID, but panics if an error occurs. +func (_q *GCPServiceAccountQuery) OnlyIDX(ctx context.Context) uuid.UUID { + id, err := _q.OnlyID(ctx) + if err != nil { + panic(err) + } + return id +} + +// All executes the query and returns a list of GCPServiceAccounts. +func (_q *GCPServiceAccountQuery) All(ctx context.Context) ([]*GCPServiceAccount, error) { + ctx = setContextOp(ctx, _q.ctx, ent.OpQueryAll) + if err := _q.prepareQuery(ctx); err != nil { + return nil, err + } + qr := querierAll[[]*GCPServiceAccount, *GCPServiceAccountQuery]() + return withInterceptors[[]*GCPServiceAccount](ctx, _q, qr, _q.inters) +} + +// AllX is like All, but panics if an error occurs. +func (_q *GCPServiceAccountQuery) AllX(ctx context.Context) []*GCPServiceAccount { + nodes, err := _q.All(ctx) + if err != nil { + panic(err) + } + return nodes +} + +// IDs executes the query and returns a list of GCPServiceAccount IDs. +func (_q *GCPServiceAccountQuery) IDs(ctx context.Context) (ids []uuid.UUID, err error) { + if _q.ctx.Unique == nil && _q.path != nil { + _q.Unique(true) + } + ctx = setContextOp(ctx, _q.ctx, ent.OpQueryIDs) + if err = _q.Select(gcpserviceaccount.FieldID).Scan(ctx, &ids); err != nil { + return nil, err + } + return ids, nil +} + +// IDsX is like IDs, but panics if an error occurs. +func (_q *GCPServiceAccountQuery) IDsX(ctx context.Context) []uuid.UUID { + ids, err := _q.IDs(ctx) + if err != nil { + panic(err) + } + return ids +} + +// Count returns the count of the given query. +func (_q *GCPServiceAccountQuery) Count(ctx context.Context) (int, error) { + ctx = setContextOp(ctx, _q.ctx, ent.OpQueryCount) + if err := _q.prepareQuery(ctx); err != nil { + return 0, err + } + return withInterceptors[int](ctx, _q, querierCount[*GCPServiceAccountQuery](), _q.inters) +} + +// CountX is like Count, but panics if an error occurs. +func (_q *GCPServiceAccountQuery) CountX(ctx context.Context) int { + count, err := _q.Count(ctx) + if err != nil { + panic(err) + } + return count +} + +// Exist returns true if the query has elements in the graph. +func (_q *GCPServiceAccountQuery) Exist(ctx context.Context) (bool, error) { + ctx = setContextOp(ctx, _q.ctx, ent.OpQueryExist) + switch _, err := _q.FirstID(ctx); { + case IsNotFound(err): + return false, nil + case err != nil: + return false, fmt.Errorf("ent: check existence: %w", err) + default: + return true, nil + } +} + +// ExistX is like Exist, but panics if an error occurs. +func (_q *GCPServiceAccountQuery) ExistX(ctx context.Context) bool { + exist, err := _q.Exist(ctx) + if err != nil { + panic(err) + } + return exist +} + +// Clone returns a duplicate of the GCPServiceAccountQuery builder, including all associated steps. It can be +// used to prepare common query builders and use them differently after the clone is made. +func (_q *GCPServiceAccountQuery) Clone() *GCPServiceAccountQuery { + if _q == nil { + return nil + } + return &GCPServiceAccountQuery{ + config: _q.config, + ctx: _q.ctx.Clone(), + order: append([]gcpserviceaccount.OrderOption{}, _q.order...), + inters: append([]Interceptor{}, _q.inters...), + predicates: append([]predicate.GCPServiceAccount{}, _q.predicates...), + // clone intermediate query. + sql: _q.sql.Clone(), + path: _q.path, + } +} + +// GroupBy is used to group vertices by one or more fields/columns. +// It is often used with aggregate functions, like: count, max, mean, min, sum. +// +// Example: +// +// var v []struct { +// Scope string `json:"scope,omitempty"` +// Count int `json:"count,omitempty"` +// } +// +// client.GCPServiceAccount.Query(). +// GroupBy(gcpserviceaccount.FieldScope). +// Aggregate(ent.Count()). +// Scan(ctx, &v) +func (_q *GCPServiceAccountQuery) GroupBy(field string, fields ...string) *GCPServiceAccountGroupBy { + _q.ctx.Fields = append([]string{field}, fields...) + grbuild := &GCPServiceAccountGroupBy{build: _q} + grbuild.flds = &_q.ctx.Fields + grbuild.label = gcpserviceaccount.Label + grbuild.scan = grbuild.Scan + return grbuild +} + +// Select allows the selection one or more fields/columns for the given query, +// instead of selecting all fields in the entity. +// +// Example: +// +// var v []struct { +// Scope string `json:"scope,omitempty"` +// } +// +// client.GCPServiceAccount.Query(). +// Select(gcpserviceaccount.FieldScope). +// Scan(ctx, &v) +func (_q *GCPServiceAccountQuery) Select(fields ...string) *GCPServiceAccountSelect { + _q.ctx.Fields = append(_q.ctx.Fields, fields...) + sbuild := &GCPServiceAccountSelect{GCPServiceAccountQuery: _q} + sbuild.label = gcpserviceaccount.Label + sbuild.flds, sbuild.scan = &_q.ctx.Fields, sbuild.Scan + return sbuild +} + +// Aggregate returns a GCPServiceAccountSelect configured with the given aggregations. +func (_q *GCPServiceAccountQuery) Aggregate(fns ...AggregateFunc) *GCPServiceAccountSelect { + return _q.Select().Aggregate(fns...) +} + +func (_q *GCPServiceAccountQuery) prepareQuery(ctx context.Context) error { + for _, inter := range _q.inters { + if inter == nil { + return fmt.Errorf("ent: uninitialized interceptor (forgotten import ent/runtime?)") + } + if trv, ok := inter.(Traverser); ok { + if err := trv.Traverse(ctx, _q); err != nil { + return err + } + } + } + for _, f := range _q.ctx.Fields { + if !gcpserviceaccount.ValidColumn(f) { + return &ValidationError{Name: f, err: fmt.Errorf("ent: invalid field %q for query", f)} + } + } + if _q.path != nil { + prev, err := _q.path(ctx) + if err != nil { + return err + } + _q.sql = prev + } + return nil +} + +func (_q *GCPServiceAccountQuery) sqlAll(ctx context.Context, hooks ...queryHook) ([]*GCPServiceAccount, error) { + var ( + nodes = []*GCPServiceAccount{} + _spec = _q.querySpec() + ) + _spec.ScanValues = func(columns []string) ([]any, error) { + return (*GCPServiceAccount).scanValues(nil, columns) + } + _spec.Assign = func(columns []string, values []any) error { + node := &GCPServiceAccount{config: _q.config} + nodes = append(nodes, node) + return node.assignValues(columns, values) + } + if len(_q.modifiers) > 0 { + _spec.Modifiers = _q.modifiers + } + for i := range hooks { + hooks[i](ctx, _spec) + } + if err := sqlgraph.QueryNodes(ctx, _q.driver, _spec); err != nil { + return nil, err + } + if len(nodes) == 0 { + return nodes, nil + } + return nodes, nil +} + +func (_q *GCPServiceAccountQuery) sqlCount(ctx context.Context) (int, error) { + _spec := _q.querySpec() + if len(_q.modifiers) > 0 { + _spec.Modifiers = _q.modifiers + } + _spec.Node.Columns = _q.ctx.Fields + if len(_q.ctx.Fields) > 0 { + _spec.Unique = _q.ctx.Unique != nil && *_q.ctx.Unique + } + return sqlgraph.CountNodes(ctx, _q.driver, _spec) +} + +func (_q *GCPServiceAccountQuery) querySpec() *sqlgraph.QuerySpec { + _spec := sqlgraph.NewQuerySpec(gcpserviceaccount.Table, gcpserviceaccount.Columns, sqlgraph.NewFieldSpec(gcpserviceaccount.FieldID, field.TypeUUID)) + _spec.From = _q.sql + if unique := _q.ctx.Unique; unique != nil { + _spec.Unique = *unique + } else if _q.path != nil { + _spec.Unique = true + } + if fields := _q.ctx.Fields; len(fields) > 0 { + _spec.Node.Columns = make([]string, 0, len(fields)) + _spec.Node.Columns = append(_spec.Node.Columns, gcpserviceaccount.FieldID) + for i := range fields { + if fields[i] != gcpserviceaccount.FieldID { + _spec.Node.Columns = append(_spec.Node.Columns, fields[i]) + } + } + } + if ps := _q.predicates; len(ps) > 0 { + _spec.Predicate = func(selector *sql.Selector) { + for i := range ps { + ps[i](selector) + } + } + } + if limit := _q.ctx.Limit; limit != nil { + _spec.Limit = *limit + } + if offset := _q.ctx.Offset; offset != nil { + _spec.Offset = *offset + } + if ps := _q.order; len(ps) > 0 { + _spec.Order = func(selector *sql.Selector) { + for i := range ps { + ps[i](selector) + } + } + } + return _spec +} + +func (_q *GCPServiceAccountQuery) sqlQuery(ctx context.Context) *sql.Selector { + builder := sql.Dialect(_q.driver.Dialect()) + t1 := builder.Table(gcpserviceaccount.Table) + columns := _q.ctx.Fields + if len(columns) == 0 { + columns = gcpserviceaccount.Columns + } + selector := builder.Select(t1.Columns(columns...)...).From(t1) + if _q.sql != nil { + selector = _q.sql + selector.Select(selector.Columns(columns...)...) + } + if _q.ctx.Unique != nil && *_q.ctx.Unique { + selector.Distinct() + } + for _, m := range _q.modifiers { + m(selector) + } + for _, p := range _q.predicates { + p(selector) + } + for _, p := range _q.order { + p(selector) + } + if offset := _q.ctx.Offset; offset != nil { + // limit is mandatory for offset clause. We start + // with default value, and override it below if needed. + selector.Offset(*offset).Limit(math.MaxInt32) + } + if limit := _q.ctx.Limit; limit != nil { + selector.Limit(*limit) + } + return selector +} + +// ForUpdate locks the selected rows against concurrent updates, and prevent them from being +// updated, deleted or "selected ... for update" by other sessions, until the transaction is +// either committed or rolled-back. +func (_q *GCPServiceAccountQuery) ForUpdate(opts ...sql.LockOption) *GCPServiceAccountQuery { + if _q.driver.Dialect() == dialect.Postgres { + _q.Unique(false) + } + _q.modifiers = append(_q.modifiers, func(s *sql.Selector) { + s.ForUpdate(opts...) + }) + return _q +} + +// ForShare behaves similarly to ForUpdate, except that it acquires a shared mode lock +// on any rows that are read. Other sessions can read the rows, but cannot modify them +// until your transaction commits. +func (_q *GCPServiceAccountQuery) ForShare(opts ...sql.LockOption) *GCPServiceAccountQuery { + if _q.driver.Dialect() == dialect.Postgres { + _q.Unique(false) + } + _q.modifiers = append(_q.modifiers, func(s *sql.Selector) { + s.ForShare(opts...) + }) + return _q +} + +// GCPServiceAccountGroupBy is the group-by builder for GCPServiceAccount entities. +type GCPServiceAccountGroupBy struct { + selector + build *GCPServiceAccountQuery +} + +// Aggregate adds the given aggregation functions to the group-by query. +func (_g *GCPServiceAccountGroupBy) Aggregate(fns ...AggregateFunc) *GCPServiceAccountGroupBy { + _g.fns = append(_g.fns, fns...) + return _g +} + +// Scan applies the selector query and scans the result into the given value. +func (_g *GCPServiceAccountGroupBy) Scan(ctx context.Context, v any) error { + ctx = setContextOp(ctx, _g.build.ctx, ent.OpQueryGroupBy) + if err := _g.build.prepareQuery(ctx); err != nil { + return err + } + return scanWithInterceptors[*GCPServiceAccountQuery, *GCPServiceAccountGroupBy](ctx, _g.build, _g, _g.build.inters, v) +} + +func (_g *GCPServiceAccountGroupBy) sqlScan(ctx context.Context, root *GCPServiceAccountQuery, v any) error { + selector := root.sqlQuery(ctx).Select() + aggregation := make([]string, 0, len(_g.fns)) + for _, fn := range _g.fns { + aggregation = append(aggregation, fn(selector)) + } + if len(selector.SelectedColumns()) == 0 { + columns := make([]string, 0, len(*_g.flds)+len(_g.fns)) + for _, f := range *_g.flds { + columns = append(columns, selector.C(f)) + } + columns = append(columns, aggregation...) + selector.Select(columns...) + } + selector.GroupBy(selector.Columns(*_g.flds...)...) + if err := selector.Err(); err != nil { + return err + } + rows := &sql.Rows{} + query, args := selector.Query() + if err := _g.build.driver.Query(ctx, query, args, rows); err != nil { + return err + } + defer rows.Close() + return sql.ScanSlice(rows, v) +} + +// GCPServiceAccountSelect is the builder for selecting fields of GCPServiceAccount entities. +type GCPServiceAccountSelect struct { + *GCPServiceAccountQuery + selector +} + +// Aggregate adds the given aggregation functions to the selector query. +func (_s *GCPServiceAccountSelect) Aggregate(fns ...AggregateFunc) *GCPServiceAccountSelect { + _s.fns = append(_s.fns, fns...) + return _s +} + +// Scan applies the selector query and scans the result into the given value. +func (_s *GCPServiceAccountSelect) Scan(ctx context.Context, v any) error { + ctx = setContextOp(ctx, _s.ctx, ent.OpQuerySelect) + if err := _s.prepareQuery(ctx); err != nil { + return err + } + return scanWithInterceptors[*GCPServiceAccountQuery, *GCPServiceAccountSelect](ctx, _s.GCPServiceAccountQuery, _s, _s.inters, v) +} + +func (_s *GCPServiceAccountSelect) sqlScan(ctx context.Context, root *GCPServiceAccountQuery, v any) error { + selector := root.sqlQuery(ctx) + aggregation := make([]string, 0, len(_s.fns)) + for _, fn := range _s.fns { + aggregation = append(aggregation, fn(selector)) + } + switch n := len(*_s.selector.flds); { + case n == 0 && len(aggregation) > 0: + selector.Select(aggregation...) + case n != 0 && len(aggregation) > 0: + selector.AppendSelect(aggregation...) + } + rows := &sql.Rows{} + query, args := selector.Query() + if err := _s.driver.Query(ctx, query, args, rows); err != nil { + return err + } + defer rows.Close() + return sql.ScanSlice(rows, v) +} diff --git a/pkg/ent/gcpserviceaccount_update.go b/pkg/ent/gcpserviceaccount_update.go new file mode 100644 index 000000000..fe9bcd234 --- /dev/null +++ b/pkg/ent/gcpserviceaccount_update.go @@ -0,0 +1,624 @@ +// Code generated by ent, DO NOT EDIT. + +package ent + +import ( + "context" + "errors" + "fmt" + "time" + + "entgo.io/ent/dialect/sql" + "entgo.io/ent/dialect/sql/sqlgraph" + "entgo.io/ent/schema/field" + "github.com/GoogleCloudPlatform/scion/pkg/ent/gcpserviceaccount" + "github.com/GoogleCloudPlatform/scion/pkg/ent/predicate" +) + +// GCPServiceAccountUpdate is the builder for updating GCPServiceAccount entities. +type GCPServiceAccountUpdate struct { + config + hooks []Hook + mutation *GCPServiceAccountMutation +} + +// Where appends a list predicates to the GCPServiceAccountUpdate builder. +func (_u *GCPServiceAccountUpdate) Where(ps ...predicate.GCPServiceAccount) *GCPServiceAccountUpdate { + _u.mutation.Where(ps...) + return _u +} + +// SetScope sets the "scope" field. +func (_u *GCPServiceAccountUpdate) SetScope(v string) *GCPServiceAccountUpdate { + _u.mutation.SetScope(v) + return _u +} + +// SetNillableScope sets the "scope" field if the given value is not nil. +func (_u *GCPServiceAccountUpdate) SetNillableScope(v *string) *GCPServiceAccountUpdate { + if v != nil { + _u.SetScope(*v) + } + return _u +} + +// SetScopeID sets the "scope_id" field. +func (_u *GCPServiceAccountUpdate) SetScopeID(v string) *GCPServiceAccountUpdate { + _u.mutation.SetScopeID(v) + return _u +} + +// SetNillableScopeID sets the "scope_id" field if the given value is not nil. +func (_u *GCPServiceAccountUpdate) SetNillableScopeID(v *string) *GCPServiceAccountUpdate { + if v != nil { + _u.SetScopeID(*v) + } + return _u +} + +// SetEmail sets the "email" field. +func (_u *GCPServiceAccountUpdate) SetEmail(v string) *GCPServiceAccountUpdate { + _u.mutation.SetEmail(v) + return _u +} + +// SetNillableEmail sets the "email" field if the given value is not nil. +func (_u *GCPServiceAccountUpdate) SetNillableEmail(v *string) *GCPServiceAccountUpdate { + if v != nil { + _u.SetEmail(*v) + } + return _u +} + +// SetProjectID sets the "project_id" field. +func (_u *GCPServiceAccountUpdate) SetProjectID(v string) *GCPServiceAccountUpdate { + _u.mutation.SetProjectID(v) + return _u +} + +// SetNillableProjectID sets the "project_id" field if the given value is not nil. +func (_u *GCPServiceAccountUpdate) SetNillableProjectID(v *string) *GCPServiceAccountUpdate { + if v != nil { + _u.SetProjectID(*v) + } + return _u +} + +// SetDisplayName sets the "display_name" field. +func (_u *GCPServiceAccountUpdate) SetDisplayName(v string) *GCPServiceAccountUpdate { + _u.mutation.SetDisplayName(v) + return _u +} + +// SetNillableDisplayName sets the "display_name" field if the given value is not nil. +func (_u *GCPServiceAccountUpdate) SetNillableDisplayName(v *string) *GCPServiceAccountUpdate { + if v != nil { + _u.SetDisplayName(*v) + } + return _u +} + +// SetDefaultScopes sets the "default_scopes" field. +func (_u *GCPServiceAccountUpdate) SetDefaultScopes(v string) *GCPServiceAccountUpdate { + _u.mutation.SetDefaultScopes(v) + return _u +} + +// SetNillableDefaultScopes sets the "default_scopes" field if the given value is not nil. +func (_u *GCPServiceAccountUpdate) SetNillableDefaultScopes(v *string) *GCPServiceAccountUpdate { + if v != nil { + _u.SetDefaultScopes(*v) + } + return _u +} + +// SetVerified sets the "verified" field. +func (_u *GCPServiceAccountUpdate) SetVerified(v bool) *GCPServiceAccountUpdate { + _u.mutation.SetVerified(v) + return _u +} + +// SetNillableVerified sets the "verified" field if the given value is not nil. +func (_u *GCPServiceAccountUpdate) SetNillableVerified(v *bool) *GCPServiceAccountUpdate { + if v != nil { + _u.SetVerified(*v) + } + return _u +} + +// SetVerifiedAt sets the "verified_at" field. +func (_u *GCPServiceAccountUpdate) SetVerifiedAt(v time.Time) *GCPServiceAccountUpdate { + _u.mutation.SetVerifiedAt(v) + return _u +} + +// SetNillableVerifiedAt sets the "verified_at" field if the given value is not nil. +func (_u *GCPServiceAccountUpdate) SetNillableVerifiedAt(v *time.Time) *GCPServiceAccountUpdate { + if v != nil { + _u.SetVerifiedAt(*v) + } + return _u +} + +// ClearVerifiedAt clears the value of the "verified_at" field. +func (_u *GCPServiceAccountUpdate) ClearVerifiedAt() *GCPServiceAccountUpdate { + _u.mutation.ClearVerifiedAt() + return _u +} + +// SetCreatedBy sets the "created_by" field. +func (_u *GCPServiceAccountUpdate) SetCreatedBy(v string) *GCPServiceAccountUpdate { + _u.mutation.SetCreatedBy(v) + return _u +} + +// SetNillableCreatedBy sets the "created_by" field if the given value is not nil. +func (_u *GCPServiceAccountUpdate) SetNillableCreatedBy(v *string) *GCPServiceAccountUpdate { + if v != nil { + _u.SetCreatedBy(*v) + } + return _u +} + +// SetManaged sets the "managed" field. +func (_u *GCPServiceAccountUpdate) SetManaged(v bool) *GCPServiceAccountUpdate { + _u.mutation.SetManaged(v) + return _u +} + +// SetNillableManaged sets the "managed" field if the given value is not nil. +func (_u *GCPServiceAccountUpdate) SetNillableManaged(v *bool) *GCPServiceAccountUpdate { + if v != nil { + _u.SetManaged(*v) + } + return _u +} + +// SetManagedBy sets the "managed_by" field. +func (_u *GCPServiceAccountUpdate) SetManagedBy(v string) *GCPServiceAccountUpdate { + _u.mutation.SetManagedBy(v) + return _u +} + +// SetNillableManagedBy sets the "managed_by" field if the given value is not nil. +func (_u *GCPServiceAccountUpdate) SetNillableManagedBy(v *string) *GCPServiceAccountUpdate { + if v != nil { + _u.SetManagedBy(*v) + } + return _u +} + +// Mutation returns the GCPServiceAccountMutation object of the builder. +func (_u *GCPServiceAccountUpdate) Mutation() *GCPServiceAccountMutation { + return _u.mutation +} + +// Save executes the query and returns the number of nodes affected by the update operation. +func (_u *GCPServiceAccountUpdate) Save(ctx context.Context) (int, error) { + return withHooks(ctx, _u.sqlSave, _u.mutation, _u.hooks) +} + +// SaveX is like Save, but panics if an error occurs. +func (_u *GCPServiceAccountUpdate) SaveX(ctx context.Context) int { + affected, err := _u.Save(ctx) + if err != nil { + panic(err) + } + return affected +} + +// Exec executes the query. +func (_u *GCPServiceAccountUpdate) Exec(ctx context.Context) error { + _, err := _u.Save(ctx) + return err +} + +// ExecX is like Exec, but panics if an error occurs. +func (_u *GCPServiceAccountUpdate) ExecX(ctx context.Context) { + if err := _u.Exec(ctx); err != nil { + panic(err) + } +} + +// check runs all checks and user-defined validators on the builder. +func (_u *GCPServiceAccountUpdate) check() error { + if v, ok := _u.mutation.Scope(); ok { + if err := gcpserviceaccount.ScopeValidator(v); err != nil { + return &ValidationError{Name: "scope", err: fmt.Errorf(`ent: validator failed for field "GCPServiceAccount.scope": %w`, err)} + } + } + if v, ok := _u.mutation.ScopeID(); ok { + if err := gcpserviceaccount.ScopeIDValidator(v); err != nil { + return &ValidationError{Name: "scope_id", err: fmt.Errorf(`ent: validator failed for field "GCPServiceAccount.scope_id": %w`, err)} + } + } + if v, ok := _u.mutation.Email(); ok { + if err := gcpserviceaccount.EmailValidator(v); err != nil { + return &ValidationError{Name: "email", err: fmt.Errorf(`ent: validator failed for field "GCPServiceAccount.email": %w`, err)} + } + } + if v, ok := _u.mutation.ProjectID(); ok { + if err := gcpserviceaccount.ProjectIDValidator(v); err != nil { + return &ValidationError{Name: "project_id", err: fmt.Errorf(`ent: validator failed for field "GCPServiceAccount.project_id": %w`, err)} + } + } + return nil +} + +func (_u *GCPServiceAccountUpdate) sqlSave(ctx context.Context) (_node int, err error) { + if err := _u.check(); err != nil { + return _node, err + } + _spec := sqlgraph.NewUpdateSpec(gcpserviceaccount.Table, gcpserviceaccount.Columns, sqlgraph.NewFieldSpec(gcpserviceaccount.FieldID, field.TypeUUID)) + if ps := _u.mutation.predicates; len(ps) > 0 { + _spec.Predicate = func(selector *sql.Selector) { + for i := range ps { + ps[i](selector) + } + } + } + if value, ok := _u.mutation.Scope(); ok { + _spec.SetField(gcpserviceaccount.FieldScope, field.TypeString, value) + } + if value, ok := _u.mutation.ScopeID(); ok { + _spec.SetField(gcpserviceaccount.FieldScopeID, field.TypeString, value) + } + if value, ok := _u.mutation.Email(); ok { + _spec.SetField(gcpserviceaccount.FieldEmail, field.TypeString, value) + } + if value, ok := _u.mutation.ProjectID(); ok { + _spec.SetField(gcpserviceaccount.FieldProjectID, field.TypeString, value) + } + if value, ok := _u.mutation.DisplayName(); ok { + _spec.SetField(gcpserviceaccount.FieldDisplayName, field.TypeString, value) + } + if value, ok := _u.mutation.DefaultScopes(); ok { + _spec.SetField(gcpserviceaccount.FieldDefaultScopes, field.TypeString, value) + } + if value, ok := _u.mutation.Verified(); ok { + _spec.SetField(gcpserviceaccount.FieldVerified, field.TypeBool, value) + } + if value, ok := _u.mutation.VerifiedAt(); ok { + _spec.SetField(gcpserviceaccount.FieldVerifiedAt, field.TypeTime, value) + } + if _u.mutation.VerifiedAtCleared() { + _spec.ClearField(gcpserviceaccount.FieldVerifiedAt, field.TypeTime) + } + if value, ok := _u.mutation.CreatedBy(); ok { + _spec.SetField(gcpserviceaccount.FieldCreatedBy, field.TypeString, value) + } + if value, ok := _u.mutation.Managed(); ok { + _spec.SetField(gcpserviceaccount.FieldManaged, field.TypeBool, value) + } + if value, ok := _u.mutation.ManagedBy(); ok { + _spec.SetField(gcpserviceaccount.FieldManagedBy, field.TypeString, value) + } + if _node, err = sqlgraph.UpdateNodes(ctx, _u.driver, _spec); err != nil { + if _, ok := err.(*sqlgraph.NotFoundError); ok { + err = &NotFoundError{gcpserviceaccount.Label} + } else if sqlgraph.IsConstraintError(err) { + err = &ConstraintError{msg: err.Error(), wrap: err} + } + return 0, err + } + _u.mutation.done = true + return _node, nil +} + +// GCPServiceAccountUpdateOne is the builder for updating a single GCPServiceAccount entity. +type GCPServiceAccountUpdateOne struct { + config + fields []string + hooks []Hook + mutation *GCPServiceAccountMutation +} + +// SetScope sets the "scope" field. +func (_u *GCPServiceAccountUpdateOne) SetScope(v string) *GCPServiceAccountUpdateOne { + _u.mutation.SetScope(v) + return _u +} + +// SetNillableScope sets the "scope" field if the given value is not nil. +func (_u *GCPServiceAccountUpdateOne) SetNillableScope(v *string) *GCPServiceAccountUpdateOne { + if v != nil { + _u.SetScope(*v) + } + return _u +} + +// SetScopeID sets the "scope_id" field. +func (_u *GCPServiceAccountUpdateOne) SetScopeID(v string) *GCPServiceAccountUpdateOne { + _u.mutation.SetScopeID(v) + return _u +} + +// SetNillableScopeID sets the "scope_id" field if the given value is not nil. +func (_u *GCPServiceAccountUpdateOne) SetNillableScopeID(v *string) *GCPServiceAccountUpdateOne { + if v != nil { + _u.SetScopeID(*v) + } + return _u +} + +// SetEmail sets the "email" field. +func (_u *GCPServiceAccountUpdateOne) SetEmail(v string) *GCPServiceAccountUpdateOne { + _u.mutation.SetEmail(v) + return _u +} + +// SetNillableEmail sets the "email" field if the given value is not nil. +func (_u *GCPServiceAccountUpdateOne) SetNillableEmail(v *string) *GCPServiceAccountUpdateOne { + if v != nil { + _u.SetEmail(*v) + } + return _u +} + +// SetProjectID sets the "project_id" field. +func (_u *GCPServiceAccountUpdateOne) SetProjectID(v string) *GCPServiceAccountUpdateOne { + _u.mutation.SetProjectID(v) + return _u +} + +// SetNillableProjectID sets the "project_id" field if the given value is not nil. +func (_u *GCPServiceAccountUpdateOne) SetNillableProjectID(v *string) *GCPServiceAccountUpdateOne { + if v != nil { + _u.SetProjectID(*v) + } + return _u +} + +// SetDisplayName sets the "display_name" field. +func (_u *GCPServiceAccountUpdateOne) SetDisplayName(v string) *GCPServiceAccountUpdateOne { + _u.mutation.SetDisplayName(v) + return _u +} + +// SetNillableDisplayName sets the "display_name" field if the given value is not nil. +func (_u *GCPServiceAccountUpdateOne) SetNillableDisplayName(v *string) *GCPServiceAccountUpdateOne { + if v != nil { + _u.SetDisplayName(*v) + } + return _u +} + +// SetDefaultScopes sets the "default_scopes" field. +func (_u *GCPServiceAccountUpdateOne) SetDefaultScopes(v string) *GCPServiceAccountUpdateOne { + _u.mutation.SetDefaultScopes(v) + return _u +} + +// SetNillableDefaultScopes sets the "default_scopes" field if the given value is not nil. +func (_u *GCPServiceAccountUpdateOne) SetNillableDefaultScopes(v *string) *GCPServiceAccountUpdateOne { + if v != nil { + _u.SetDefaultScopes(*v) + } + return _u +} + +// SetVerified sets the "verified" field. +func (_u *GCPServiceAccountUpdateOne) SetVerified(v bool) *GCPServiceAccountUpdateOne { + _u.mutation.SetVerified(v) + return _u +} + +// SetNillableVerified sets the "verified" field if the given value is not nil. +func (_u *GCPServiceAccountUpdateOne) SetNillableVerified(v *bool) *GCPServiceAccountUpdateOne { + if v != nil { + _u.SetVerified(*v) + } + return _u +} + +// SetVerifiedAt sets the "verified_at" field. +func (_u *GCPServiceAccountUpdateOne) SetVerifiedAt(v time.Time) *GCPServiceAccountUpdateOne { + _u.mutation.SetVerifiedAt(v) + return _u +} + +// SetNillableVerifiedAt sets the "verified_at" field if the given value is not nil. +func (_u *GCPServiceAccountUpdateOne) SetNillableVerifiedAt(v *time.Time) *GCPServiceAccountUpdateOne { + if v != nil { + _u.SetVerifiedAt(*v) + } + return _u +} + +// ClearVerifiedAt clears the value of the "verified_at" field. +func (_u *GCPServiceAccountUpdateOne) ClearVerifiedAt() *GCPServiceAccountUpdateOne { + _u.mutation.ClearVerifiedAt() + return _u +} + +// SetCreatedBy sets the "created_by" field. +func (_u *GCPServiceAccountUpdateOne) SetCreatedBy(v string) *GCPServiceAccountUpdateOne { + _u.mutation.SetCreatedBy(v) + return _u +} + +// SetNillableCreatedBy sets the "created_by" field if the given value is not nil. +func (_u *GCPServiceAccountUpdateOne) SetNillableCreatedBy(v *string) *GCPServiceAccountUpdateOne { + if v != nil { + _u.SetCreatedBy(*v) + } + return _u +} + +// SetManaged sets the "managed" field. +func (_u *GCPServiceAccountUpdateOne) SetManaged(v bool) *GCPServiceAccountUpdateOne { + _u.mutation.SetManaged(v) + return _u +} + +// SetNillableManaged sets the "managed" field if the given value is not nil. +func (_u *GCPServiceAccountUpdateOne) SetNillableManaged(v *bool) *GCPServiceAccountUpdateOne { + if v != nil { + _u.SetManaged(*v) + } + return _u +} + +// SetManagedBy sets the "managed_by" field. +func (_u *GCPServiceAccountUpdateOne) SetManagedBy(v string) *GCPServiceAccountUpdateOne { + _u.mutation.SetManagedBy(v) + return _u +} + +// SetNillableManagedBy sets the "managed_by" field if the given value is not nil. +func (_u *GCPServiceAccountUpdateOne) SetNillableManagedBy(v *string) *GCPServiceAccountUpdateOne { + if v != nil { + _u.SetManagedBy(*v) + } + return _u +} + +// Mutation returns the GCPServiceAccountMutation object of the builder. +func (_u *GCPServiceAccountUpdateOne) Mutation() *GCPServiceAccountMutation { + return _u.mutation +} + +// Where appends a list predicates to the GCPServiceAccountUpdate builder. +func (_u *GCPServiceAccountUpdateOne) Where(ps ...predicate.GCPServiceAccount) *GCPServiceAccountUpdateOne { + _u.mutation.Where(ps...) + return _u +} + +// Select allows selecting one or more fields (columns) of the returned entity. +// The default is selecting all fields defined in the entity schema. +func (_u *GCPServiceAccountUpdateOne) Select(field string, fields ...string) *GCPServiceAccountUpdateOne { + _u.fields = append([]string{field}, fields...) + return _u +} + +// Save executes the query and returns the updated GCPServiceAccount entity. +func (_u *GCPServiceAccountUpdateOne) Save(ctx context.Context) (*GCPServiceAccount, error) { + return withHooks(ctx, _u.sqlSave, _u.mutation, _u.hooks) +} + +// SaveX is like Save, but panics if an error occurs. +func (_u *GCPServiceAccountUpdateOne) SaveX(ctx context.Context) *GCPServiceAccount { + node, err := _u.Save(ctx) + if err != nil { + panic(err) + } + return node +} + +// Exec executes the query on the entity. +func (_u *GCPServiceAccountUpdateOne) Exec(ctx context.Context) error { + _, err := _u.Save(ctx) + return err +} + +// ExecX is like Exec, but panics if an error occurs. +func (_u *GCPServiceAccountUpdateOne) ExecX(ctx context.Context) { + if err := _u.Exec(ctx); err != nil { + panic(err) + } +} + +// check runs all checks and user-defined validators on the builder. +func (_u *GCPServiceAccountUpdateOne) check() error { + if v, ok := _u.mutation.Scope(); ok { + if err := gcpserviceaccount.ScopeValidator(v); err != nil { + return &ValidationError{Name: "scope", err: fmt.Errorf(`ent: validator failed for field "GCPServiceAccount.scope": %w`, err)} + } + } + if v, ok := _u.mutation.ScopeID(); ok { + if err := gcpserviceaccount.ScopeIDValidator(v); err != nil { + return &ValidationError{Name: "scope_id", err: fmt.Errorf(`ent: validator failed for field "GCPServiceAccount.scope_id": %w`, err)} + } + } + if v, ok := _u.mutation.Email(); ok { + if err := gcpserviceaccount.EmailValidator(v); err != nil { + return &ValidationError{Name: "email", err: fmt.Errorf(`ent: validator failed for field "GCPServiceAccount.email": %w`, err)} + } + } + if v, ok := _u.mutation.ProjectID(); ok { + if err := gcpserviceaccount.ProjectIDValidator(v); err != nil { + return &ValidationError{Name: "project_id", err: fmt.Errorf(`ent: validator failed for field "GCPServiceAccount.project_id": %w`, err)} + } + } + return nil +} + +func (_u *GCPServiceAccountUpdateOne) sqlSave(ctx context.Context) (_node *GCPServiceAccount, err error) { + if err := _u.check(); err != nil { + return _node, err + } + _spec := sqlgraph.NewUpdateSpec(gcpserviceaccount.Table, gcpserviceaccount.Columns, sqlgraph.NewFieldSpec(gcpserviceaccount.FieldID, field.TypeUUID)) + id, ok := _u.mutation.ID() + if !ok { + return nil, &ValidationError{Name: "id", err: errors.New(`ent: missing "GCPServiceAccount.id" for update`)} + } + _spec.Node.ID.Value = id + if fields := _u.fields; len(fields) > 0 { + _spec.Node.Columns = make([]string, 0, len(fields)) + _spec.Node.Columns = append(_spec.Node.Columns, gcpserviceaccount.FieldID) + for _, f := range fields { + if !gcpserviceaccount.ValidColumn(f) { + return nil, &ValidationError{Name: f, err: fmt.Errorf("ent: invalid field %q for query", f)} + } + if f != gcpserviceaccount.FieldID { + _spec.Node.Columns = append(_spec.Node.Columns, f) + } + } + } + if ps := _u.mutation.predicates; len(ps) > 0 { + _spec.Predicate = func(selector *sql.Selector) { + for i := range ps { + ps[i](selector) + } + } + } + if value, ok := _u.mutation.Scope(); ok { + _spec.SetField(gcpserviceaccount.FieldScope, field.TypeString, value) + } + if value, ok := _u.mutation.ScopeID(); ok { + _spec.SetField(gcpserviceaccount.FieldScopeID, field.TypeString, value) + } + if value, ok := _u.mutation.Email(); ok { + _spec.SetField(gcpserviceaccount.FieldEmail, field.TypeString, value) + } + if value, ok := _u.mutation.ProjectID(); ok { + _spec.SetField(gcpserviceaccount.FieldProjectID, field.TypeString, value) + } + if value, ok := _u.mutation.DisplayName(); ok { + _spec.SetField(gcpserviceaccount.FieldDisplayName, field.TypeString, value) + } + if value, ok := _u.mutation.DefaultScopes(); ok { + _spec.SetField(gcpserviceaccount.FieldDefaultScopes, field.TypeString, value) + } + if value, ok := _u.mutation.Verified(); ok { + _spec.SetField(gcpserviceaccount.FieldVerified, field.TypeBool, value) + } + if value, ok := _u.mutation.VerifiedAt(); ok { + _spec.SetField(gcpserviceaccount.FieldVerifiedAt, field.TypeTime, value) + } + if _u.mutation.VerifiedAtCleared() { + _spec.ClearField(gcpserviceaccount.FieldVerifiedAt, field.TypeTime) + } + if value, ok := _u.mutation.CreatedBy(); ok { + _spec.SetField(gcpserviceaccount.FieldCreatedBy, field.TypeString, value) + } + if value, ok := _u.mutation.Managed(); ok { + _spec.SetField(gcpserviceaccount.FieldManaged, field.TypeBool, value) + } + if value, ok := _u.mutation.ManagedBy(); ok { + _spec.SetField(gcpserviceaccount.FieldManagedBy, field.TypeString, value) + } + _node = &GCPServiceAccount{config: _u.config} + _spec.Assign = _node.assignValues + _spec.ScanValues = _node.scanValues + if err = sqlgraph.UpdateNode(ctx, _u.driver, _spec); err != nil { + if _, ok := err.(*sqlgraph.NotFoundError); ok { + err = &NotFoundError{gcpserviceaccount.Label} + } else if sqlgraph.IsConstraintError(err) { + err = &ConstraintError{msg: err.Error(), wrap: err} + } + return nil, err + } + _u.mutation.done = true + return _node, nil +} diff --git a/pkg/ent/generate.go b/pkg/ent/generate.go index 143829ddf..5cb93fa96 100644 --- a/pkg/ent/generate.go +++ b/pkg/ent/generate.go @@ -14,4 +14,4 @@ package ent -//go:generate go run -mod=mod entgo.io/ent/cmd/ent generate ./schema +//go:generate go run -mod=mod entgo.io/ent/cmd/ent generate --feature sql/upsert,sql/lock ./schema diff --git a/pkg/ent/githubinstallation.go b/pkg/ent/githubinstallation.go new file mode 100644 index 000000000..7e5769850 --- /dev/null +++ b/pkg/ent/githubinstallation.go @@ -0,0 +1,172 @@ +// Code generated by ent, DO NOT EDIT. + +package ent + +import ( + "fmt" + "strings" + "time" + + "entgo.io/ent" + "entgo.io/ent/dialect/sql" + "github.com/GoogleCloudPlatform/scion/pkg/ent/githubinstallation" +) + +// GithubInstallation is the model entity for the GithubInstallation schema. +type GithubInstallation struct { + config `json:"-"` + // ID of the ent. + ID int64 `json:"id,omitempty"` + // AccountLogin holds the value of the "account_login" field. + AccountLogin string `json:"account_login,omitempty"` + // AccountType holds the value of the "account_type" field. + AccountType string `json:"account_type,omitempty"` + // AppID holds the value of the "app_id" field. + AppID int64 `json:"app_id,omitempty"` + // Repositories holds the value of the "repositories" field. + Repositories string `json:"repositories,omitempty"` + // Status holds the value of the "status" field. + Status string `json:"status,omitempty"` + // Created holds the value of the "created" field. + Created time.Time `json:"created,omitempty"` + // Updated holds the value of the "updated" field. + Updated time.Time `json:"updated,omitempty"` + selectValues sql.SelectValues +} + +// scanValues returns the types for scanning values from sql.Rows. +func (*GithubInstallation) scanValues(columns []string) ([]any, error) { + values := make([]any, len(columns)) + for i := range columns { + switch columns[i] { + case githubinstallation.FieldID, githubinstallation.FieldAppID: + values[i] = new(sql.NullInt64) + case githubinstallation.FieldAccountLogin, githubinstallation.FieldAccountType, githubinstallation.FieldRepositories, githubinstallation.FieldStatus: + values[i] = new(sql.NullString) + case githubinstallation.FieldCreated, githubinstallation.FieldUpdated: + values[i] = new(sql.NullTime) + default: + values[i] = new(sql.UnknownType) + } + } + return values, nil +} + +// assignValues assigns the values that were returned from sql.Rows (after scanning) +// to the GithubInstallation fields. +func (_m *GithubInstallation) assignValues(columns []string, values []any) error { + if m, n := len(values), len(columns); m < n { + return fmt.Errorf("mismatch number of scan values: %d != %d", m, n) + } + for i := range columns { + switch columns[i] { + case githubinstallation.FieldID: + value, ok := values[i].(*sql.NullInt64) + if !ok { + return fmt.Errorf("unexpected type %T for field id", value) + } + _m.ID = int64(value.Int64) + case githubinstallation.FieldAccountLogin: + if value, ok := values[i].(*sql.NullString); !ok { + return fmt.Errorf("unexpected type %T for field account_login", values[i]) + } else if value.Valid { + _m.AccountLogin = value.String + } + case githubinstallation.FieldAccountType: + if value, ok := values[i].(*sql.NullString); !ok { + return fmt.Errorf("unexpected type %T for field account_type", values[i]) + } else if value.Valid { + _m.AccountType = value.String + } + case githubinstallation.FieldAppID: + if value, ok := values[i].(*sql.NullInt64); !ok { + return fmt.Errorf("unexpected type %T for field app_id", values[i]) + } else if value.Valid { + _m.AppID = value.Int64 + } + case githubinstallation.FieldRepositories: + if value, ok := values[i].(*sql.NullString); !ok { + return fmt.Errorf("unexpected type %T for field repositories", values[i]) + } else if value.Valid { + _m.Repositories = value.String + } + case githubinstallation.FieldStatus: + if value, ok := values[i].(*sql.NullString); !ok { + return fmt.Errorf("unexpected type %T for field status", values[i]) + } else if value.Valid { + _m.Status = value.String + } + case githubinstallation.FieldCreated: + if value, ok := values[i].(*sql.NullTime); !ok { + return fmt.Errorf("unexpected type %T for field created", values[i]) + } else if value.Valid { + _m.Created = value.Time + } + case githubinstallation.FieldUpdated: + if value, ok := values[i].(*sql.NullTime); !ok { + return fmt.Errorf("unexpected type %T for field updated", values[i]) + } else if value.Valid { + _m.Updated = value.Time + } + default: + _m.selectValues.Set(columns[i], values[i]) + } + } + return nil +} + +// Value returns the ent.Value that was dynamically selected and assigned to the GithubInstallation. +// This includes values selected through modifiers, order, etc. +func (_m *GithubInstallation) Value(name string) (ent.Value, error) { + return _m.selectValues.Get(name) +} + +// Update returns a builder for updating this GithubInstallation. +// Note that you need to call GithubInstallation.Unwrap() before calling this method if this GithubInstallation +// was returned from a transaction, and the transaction was committed or rolled back. +func (_m *GithubInstallation) Update() *GithubInstallationUpdateOne { + return NewGithubInstallationClient(_m.config).UpdateOne(_m) +} + +// Unwrap unwraps the GithubInstallation entity that was returned from a transaction after it was closed, +// so that all future queries will be executed through the driver which created the transaction. +func (_m *GithubInstallation) Unwrap() *GithubInstallation { + _tx, ok := _m.config.driver.(*txDriver) + if !ok { + panic("ent: GithubInstallation is not a transactional entity") + } + _m.config.driver = _tx.drv + return _m +} + +// String implements the fmt.Stringer. +func (_m *GithubInstallation) String() string { + var builder strings.Builder + builder.WriteString("GithubInstallation(") + builder.WriteString(fmt.Sprintf("id=%v, ", _m.ID)) + builder.WriteString("account_login=") + builder.WriteString(_m.AccountLogin) + builder.WriteString(", ") + builder.WriteString("account_type=") + builder.WriteString(_m.AccountType) + builder.WriteString(", ") + builder.WriteString("app_id=") + builder.WriteString(fmt.Sprintf("%v", _m.AppID)) + builder.WriteString(", ") + builder.WriteString("repositories=") + builder.WriteString(_m.Repositories) + builder.WriteString(", ") + builder.WriteString("status=") + builder.WriteString(_m.Status) + builder.WriteString(", ") + builder.WriteString("created=") + builder.WriteString(_m.Created.Format(time.ANSIC)) + builder.WriteString(", ") + builder.WriteString("updated=") + builder.WriteString(_m.Updated.Format(time.ANSIC)) + builder.WriteByte(')') + return builder.String() +} + +// GithubInstallations is a parsable slice of GithubInstallation. +type GithubInstallations []*GithubInstallation diff --git a/pkg/ent/githubinstallation/githubinstallation.go b/pkg/ent/githubinstallation/githubinstallation.go new file mode 100644 index 000000000..a8fafee95 --- /dev/null +++ b/pkg/ent/githubinstallation/githubinstallation.go @@ -0,0 +1,114 @@ +// Code generated by ent, DO NOT EDIT. + +package githubinstallation + +import ( + "time" + + "entgo.io/ent/dialect/sql" +) + +const ( + // Label holds the string label denoting the githubinstallation type in the database. + Label = "github_installation" + // FieldID holds the string denoting the id field in the database. + FieldID = "installation_id" + // FieldAccountLogin holds the string denoting the account_login field in the database. + FieldAccountLogin = "account_login" + // FieldAccountType holds the string denoting the account_type field in the database. + FieldAccountType = "account_type" + // FieldAppID holds the string denoting the app_id field in the database. + FieldAppID = "app_id" + // FieldRepositories holds the string denoting the repositories field in the database. + FieldRepositories = "repositories" + // FieldStatus holds the string denoting the status field in the database. + FieldStatus = "status" + // FieldCreated holds the string denoting the created field in the database. + FieldCreated = "created" + // FieldUpdated holds the string denoting the updated field in the database. + FieldUpdated = "updated" + // Table holds the table name of the githubinstallation in the database. + Table = "github_installations" +) + +// Columns holds all SQL columns for githubinstallation fields. +var Columns = []string{ + FieldID, + FieldAccountLogin, + FieldAccountType, + FieldAppID, + FieldRepositories, + FieldStatus, + FieldCreated, + FieldUpdated, +} + +// ValidColumn reports if the column name is valid (part of the table columns). +func ValidColumn(column string) bool { + for i := range Columns { + if column == Columns[i] { + return true + } + } + return false +} + +var ( + // AccountLoginValidator is a validator for the "account_login" field. It is called by the builders before save. + AccountLoginValidator func(string) error + // DefaultAccountType holds the default value on creation for the "account_type" field. + DefaultAccountType string + // DefaultRepositories holds the default value on creation for the "repositories" field. + DefaultRepositories string + // DefaultStatus holds the default value on creation for the "status" field. + DefaultStatus string + // DefaultCreated holds the default value on creation for the "created" field. + DefaultCreated func() time.Time + // DefaultUpdated holds the default value on creation for the "updated" field. + DefaultUpdated func() time.Time + // UpdateDefaultUpdated holds the default value on update for the "updated" field. + UpdateDefaultUpdated func() time.Time +) + +// OrderOption defines the ordering options for the GithubInstallation queries. +type OrderOption func(*sql.Selector) + +// ByID orders the results by the id field. +func ByID(opts ...sql.OrderTermOption) OrderOption { + return sql.OrderByField(FieldID, opts...).ToFunc() +} + +// ByAccountLogin orders the results by the account_login field. +func ByAccountLogin(opts ...sql.OrderTermOption) OrderOption { + return sql.OrderByField(FieldAccountLogin, opts...).ToFunc() +} + +// ByAccountType orders the results by the account_type field. +func ByAccountType(opts ...sql.OrderTermOption) OrderOption { + return sql.OrderByField(FieldAccountType, opts...).ToFunc() +} + +// ByAppID orders the results by the app_id field. +func ByAppID(opts ...sql.OrderTermOption) OrderOption { + return sql.OrderByField(FieldAppID, opts...).ToFunc() +} + +// ByRepositories orders the results by the repositories field. +func ByRepositories(opts ...sql.OrderTermOption) OrderOption { + return sql.OrderByField(FieldRepositories, opts...).ToFunc() +} + +// ByStatus orders the results by the status field. +func ByStatus(opts ...sql.OrderTermOption) OrderOption { + return sql.OrderByField(FieldStatus, opts...).ToFunc() +} + +// ByCreated orders the results by the created field. +func ByCreated(opts ...sql.OrderTermOption) OrderOption { + return sql.OrderByField(FieldCreated, opts...).ToFunc() +} + +// ByUpdated orders the results by the updated field. +func ByUpdated(opts ...sql.OrderTermOption) OrderOption { + return sql.OrderByField(FieldUpdated, opts...).ToFunc() +} diff --git a/pkg/ent/githubinstallation/where.go b/pkg/ent/githubinstallation/where.go new file mode 100644 index 000000000..1ec0a3304 --- /dev/null +++ b/pkg/ent/githubinstallation/where.go @@ -0,0 +1,485 @@ +// Code generated by ent, DO NOT EDIT. + +package githubinstallation + +import ( + "time" + + "entgo.io/ent/dialect/sql" + "github.com/GoogleCloudPlatform/scion/pkg/ent/predicate" +) + +// ID filters vertices based on their ID field. +func ID(id int64) predicate.GithubInstallation { + return predicate.GithubInstallation(sql.FieldEQ(FieldID, id)) +} + +// IDEQ applies the EQ predicate on the ID field. +func IDEQ(id int64) predicate.GithubInstallation { + return predicate.GithubInstallation(sql.FieldEQ(FieldID, id)) +} + +// IDNEQ applies the NEQ predicate on the ID field. +func IDNEQ(id int64) predicate.GithubInstallation { + return predicate.GithubInstallation(sql.FieldNEQ(FieldID, id)) +} + +// IDIn applies the In predicate on the ID field. +func IDIn(ids ...int64) predicate.GithubInstallation { + return predicate.GithubInstallation(sql.FieldIn(FieldID, ids...)) +} + +// IDNotIn applies the NotIn predicate on the ID field. +func IDNotIn(ids ...int64) predicate.GithubInstallation { + return predicate.GithubInstallation(sql.FieldNotIn(FieldID, ids...)) +} + +// IDGT applies the GT predicate on the ID field. +func IDGT(id int64) predicate.GithubInstallation { + return predicate.GithubInstallation(sql.FieldGT(FieldID, id)) +} + +// IDGTE applies the GTE predicate on the ID field. +func IDGTE(id int64) predicate.GithubInstallation { + return predicate.GithubInstallation(sql.FieldGTE(FieldID, id)) +} + +// IDLT applies the LT predicate on the ID field. +func IDLT(id int64) predicate.GithubInstallation { + return predicate.GithubInstallation(sql.FieldLT(FieldID, id)) +} + +// IDLTE applies the LTE predicate on the ID field. +func IDLTE(id int64) predicate.GithubInstallation { + return predicate.GithubInstallation(sql.FieldLTE(FieldID, id)) +} + +// AccountLogin applies equality check predicate on the "account_login" field. It's identical to AccountLoginEQ. +func AccountLogin(v string) predicate.GithubInstallation { + return predicate.GithubInstallation(sql.FieldEQ(FieldAccountLogin, v)) +} + +// AccountType applies equality check predicate on the "account_type" field. It's identical to AccountTypeEQ. +func AccountType(v string) predicate.GithubInstallation { + return predicate.GithubInstallation(sql.FieldEQ(FieldAccountType, v)) +} + +// AppID applies equality check predicate on the "app_id" field. It's identical to AppIDEQ. +func AppID(v int64) predicate.GithubInstallation { + return predicate.GithubInstallation(sql.FieldEQ(FieldAppID, v)) +} + +// Repositories applies equality check predicate on the "repositories" field. It's identical to RepositoriesEQ. +func Repositories(v string) predicate.GithubInstallation { + return predicate.GithubInstallation(sql.FieldEQ(FieldRepositories, v)) +} + +// Status applies equality check predicate on the "status" field. It's identical to StatusEQ. +func Status(v string) predicate.GithubInstallation { + return predicate.GithubInstallation(sql.FieldEQ(FieldStatus, v)) +} + +// Created applies equality check predicate on the "created" field. It's identical to CreatedEQ. +func Created(v time.Time) predicate.GithubInstallation { + return predicate.GithubInstallation(sql.FieldEQ(FieldCreated, v)) +} + +// Updated applies equality check predicate on the "updated" field. It's identical to UpdatedEQ. +func Updated(v time.Time) predicate.GithubInstallation { + return predicate.GithubInstallation(sql.FieldEQ(FieldUpdated, v)) +} + +// AccountLoginEQ applies the EQ predicate on the "account_login" field. +func AccountLoginEQ(v string) predicate.GithubInstallation { + return predicate.GithubInstallation(sql.FieldEQ(FieldAccountLogin, v)) +} + +// AccountLoginNEQ applies the NEQ predicate on the "account_login" field. +func AccountLoginNEQ(v string) predicate.GithubInstallation { + return predicate.GithubInstallation(sql.FieldNEQ(FieldAccountLogin, v)) +} + +// AccountLoginIn applies the In predicate on the "account_login" field. +func AccountLoginIn(vs ...string) predicate.GithubInstallation { + return predicate.GithubInstallation(sql.FieldIn(FieldAccountLogin, vs...)) +} + +// AccountLoginNotIn applies the NotIn predicate on the "account_login" field. +func AccountLoginNotIn(vs ...string) predicate.GithubInstallation { + return predicate.GithubInstallation(sql.FieldNotIn(FieldAccountLogin, vs...)) +} + +// AccountLoginGT applies the GT predicate on the "account_login" field. +func AccountLoginGT(v string) predicate.GithubInstallation { + return predicate.GithubInstallation(sql.FieldGT(FieldAccountLogin, v)) +} + +// AccountLoginGTE applies the GTE predicate on the "account_login" field. +func AccountLoginGTE(v string) predicate.GithubInstallation { + return predicate.GithubInstallation(sql.FieldGTE(FieldAccountLogin, v)) +} + +// AccountLoginLT applies the LT predicate on the "account_login" field. +func AccountLoginLT(v string) predicate.GithubInstallation { + return predicate.GithubInstallation(sql.FieldLT(FieldAccountLogin, v)) +} + +// AccountLoginLTE applies the LTE predicate on the "account_login" field. +func AccountLoginLTE(v string) predicate.GithubInstallation { + return predicate.GithubInstallation(sql.FieldLTE(FieldAccountLogin, v)) +} + +// AccountLoginContains applies the Contains predicate on the "account_login" field. +func AccountLoginContains(v string) predicate.GithubInstallation { + return predicate.GithubInstallation(sql.FieldContains(FieldAccountLogin, v)) +} + +// AccountLoginHasPrefix applies the HasPrefix predicate on the "account_login" field. +func AccountLoginHasPrefix(v string) predicate.GithubInstallation { + return predicate.GithubInstallation(sql.FieldHasPrefix(FieldAccountLogin, v)) +} + +// AccountLoginHasSuffix applies the HasSuffix predicate on the "account_login" field. +func AccountLoginHasSuffix(v string) predicate.GithubInstallation { + return predicate.GithubInstallation(sql.FieldHasSuffix(FieldAccountLogin, v)) +} + +// AccountLoginEqualFold applies the EqualFold predicate on the "account_login" field. +func AccountLoginEqualFold(v string) predicate.GithubInstallation { + return predicate.GithubInstallation(sql.FieldEqualFold(FieldAccountLogin, v)) +} + +// AccountLoginContainsFold applies the ContainsFold predicate on the "account_login" field. +func AccountLoginContainsFold(v string) predicate.GithubInstallation { + return predicate.GithubInstallation(sql.FieldContainsFold(FieldAccountLogin, v)) +} + +// AccountTypeEQ applies the EQ predicate on the "account_type" field. +func AccountTypeEQ(v string) predicate.GithubInstallation { + return predicate.GithubInstallation(sql.FieldEQ(FieldAccountType, v)) +} + +// AccountTypeNEQ applies the NEQ predicate on the "account_type" field. +func AccountTypeNEQ(v string) predicate.GithubInstallation { + return predicate.GithubInstallation(sql.FieldNEQ(FieldAccountType, v)) +} + +// AccountTypeIn applies the In predicate on the "account_type" field. +func AccountTypeIn(vs ...string) predicate.GithubInstallation { + return predicate.GithubInstallation(sql.FieldIn(FieldAccountType, vs...)) +} + +// AccountTypeNotIn applies the NotIn predicate on the "account_type" field. +func AccountTypeNotIn(vs ...string) predicate.GithubInstallation { + return predicate.GithubInstallation(sql.FieldNotIn(FieldAccountType, vs...)) +} + +// AccountTypeGT applies the GT predicate on the "account_type" field. +func AccountTypeGT(v string) predicate.GithubInstallation { + return predicate.GithubInstallation(sql.FieldGT(FieldAccountType, v)) +} + +// AccountTypeGTE applies the GTE predicate on the "account_type" field. +func AccountTypeGTE(v string) predicate.GithubInstallation { + return predicate.GithubInstallation(sql.FieldGTE(FieldAccountType, v)) +} + +// AccountTypeLT applies the LT predicate on the "account_type" field. +func AccountTypeLT(v string) predicate.GithubInstallation { + return predicate.GithubInstallation(sql.FieldLT(FieldAccountType, v)) +} + +// AccountTypeLTE applies the LTE predicate on the "account_type" field. +func AccountTypeLTE(v string) predicate.GithubInstallation { + return predicate.GithubInstallation(sql.FieldLTE(FieldAccountType, v)) +} + +// AccountTypeContains applies the Contains predicate on the "account_type" field. +func AccountTypeContains(v string) predicate.GithubInstallation { + return predicate.GithubInstallation(sql.FieldContains(FieldAccountType, v)) +} + +// AccountTypeHasPrefix applies the HasPrefix predicate on the "account_type" field. +func AccountTypeHasPrefix(v string) predicate.GithubInstallation { + return predicate.GithubInstallation(sql.FieldHasPrefix(FieldAccountType, v)) +} + +// AccountTypeHasSuffix applies the HasSuffix predicate on the "account_type" field. +func AccountTypeHasSuffix(v string) predicate.GithubInstallation { + return predicate.GithubInstallation(sql.FieldHasSuffix(FieldAccountType, v)) +} + +// AccountTypeEqualFold applies the EqualFold predicate on the "account_type" field. +func AccountTypeEqualFold(v string) predicate.GithubInstallation { + return predicate.GithubInstallation(sql.FieldEqualFold(FieldAccountType, v)) +} + +// AccountTypeContainsFold applies the ContainsFold predicate on the "account_type" field. +func AccountTypeContainsFold(v string) predicate.GithubInstallation { + return predicate.GithubInstallation(sql.FieldContainsFold(FieldAccountType, v)) +} + +// AppIDEQ applies the EQ predicate on the "app_id" field. +func AppIDEQ(v int64) predicate.GithubInstallation { + return predicate.GithubInstallation(sql.FieldEQ(FieldAppID, v)) +} + +// AppIDNEQ applies the NEQ predicate on the "app_id" field. +func AppIDNEQ(v int64) predicate.GithubInstallation { + return predicate.GithubInstallation(sql.FieldNEQ(FieldAppID, v)) +} + +// AppIDIn applies the In predicate on the "app_id" field. +func AppIDIn(vs ...int64) predicate.GithubInstallation { + return predicate.GithubInstallation(sql.FieldIn(FieldAppID, vs...)) +} + +// AppIDNotIn applies the NotIn predicate on the "app_id" field. +func AppIDNotIn(vs ...int64) predicate.GithubInstallation { + return predicate.GithubInstallation(sql.FieldNotIn(FieldAppID, vs...)) +} + +// AppIDGT applies the GT predicate on the "app_id" field. +func AppIDGT(v int64) predicate.GithubInstallation { + return predicate.GithubInstallation(sql.FieldGT(FieldAppID, v)) +} + +// AppIDGTE applies the GTE predicate on the "app_id" field. +func AppIDGTE(v int64) predicate.GithubInstallation { + return predicate.GithubInstallation(sql.FieldGTE(FieldAppID, v)) +} + +// AppIDLT applies the LT predicate on the "app_id" field. +func AppIDLT(v int64) predicate.GithubInstallation { + return predicate.GithubInstallation(sql.FieldLT(FieldAppID, v)) +} + +// AppIDLTE applies the LTE predicate on the "app_id" field. +func AppIDLTE(v int64) predicate.GithubInstallation { + return predicate.GithubInstallation(sql.FieldLTE(FieldAppID, v)) +} + +// RepositoriesEQ applies the EQ predicate on the "repositories" field. +func RepositoriesEQ(v string) predicate.GithubInstallation { + return predicate.GithubInstallation(sql.FieldEQ(FieldRepositories, v)) +} + +// RepositoriesNEQ applies the NEQ predicate on the "repositories" field. +func RepositoriesNEQ(v string) predicate.GithubInstallation { + return predicate.GithubInstallation(sql.FieldNEQ(FieldRepositories, v)) +} + +// RepositoriesIn applies the In predicate on the "repositories" field. +func RepositoriesIn(vs ...string) predicate.GithubInstallation { + return predicate.GithubInstallation(sql.FieldIn(FieldRepositories, vs...)) +} + +// RepositoriesNotIn applies the NotIn predicate on the "repositories" field. +func RepositoriesNotIn(vs ...string) predicate.GithubInstallation { + return predicate.GithubInstallation(sql.FieldNotIn(FieldRepositories, vs...)) +} + +// RepositoriesGT applies the GT predicate on the "repositories" field. +func RepositoriesGT(v string) predicate.GithubInstallation { + return predicate.GithubInstallation(sql.FieldGT(FieldRepositories, v)) +} + +// RepositoriesGTE applies the GTE predicate on the "repositories" field. +func RepositoriesGTE(v string) predicate.GithubInstallation { + return predicate.GithubInstallation(sql.FieldGTE(FieldRepositories, v)) +} + +// RepositoriesLT applies the LT predicate on the "repositories" field. +func RepositoriesLT(v string) predicate.GithubInstallation { + return predicate.GithubInstallation(sql.FieldLT(FieldRepositories, v)) +} + +// RepositoriesLTE applies the LTE predicate on the "repositories" field. +func RepositoriesLTE(v string) predicate.GithubInstallation { + return predicate.GithubInstallation(sql.FieldLTE(FieldRepositories, v)) +} + +// RepositoriesContains applies the Contains predicate on the "repositories" field. +func RepositoriesContains(v string) predicate.GithubInstallation { + return predicate.GithubInstallation(sql.FieldContains(FieldRepositories, v)) +} + +// RepositoriesHasPrefix applies the HasPrefix predicate on the "repositories" field. +func RepositoriesHasPrefix(v string) predicate.GithubInstallation { + return predicate.GithubInstallation(sql.FieldHasPrefix(FieldRepositories, v)) +} + +// RepositoriesHasSuffix applies the HasSuffix predicate on the "repositories" field. +func RepositoriesHasSuffix(v string) predicate.GithubInstallation { + return predicate.GithubInstallation(sql.FieldHasSuffix(FieldRepositories, v)) +} + +// RepositoriesEqualFold applies the EqualFold predicate on the "repositories" field. +func RepositoriesEqualFold(v string) predicate.GithubInstallation { + return predicate.GithubInstallation(sql.FieldEqualFold(FieldRepositories, v)) +} + +// RepositoriesContainsFold applies the ContainsFold predicate on the "repositories" field. +func RepositoriesContainsFold(v string) predicate.GithubInstallation { + return predicate.GithubInstallation(sql.FieldContainsFold(FieldRepositories, v)) +} + +// StatusEQ applies the EQ predicate on the "status" field. +func StatusEQ(v string) predicate.GithubInstallation { + return predicate.GithubInstallation(sql.FieldEQ(FieldStatus, v)) +} + +// StatusNEQ applies the NEQ predicate on the "status" field. +func StatusNEQ(v string) predicate.GithubInstallation { + return predicate.GithubInstallation(sql.FieldNEQ(FieldStatus, v)) +} + +// StatusIn applies the In predicate on the "status" field. +func StatusIn(vs ...string) predicate.GithubInstallation { + return predicate.GithubInstallation(sql.FieldIn(FieldStatus, vs...)) +} + +// StatusNotIn applies the NotIn predicate on the "status" field. +func StatusNotIn(vs ...string) predicate.GithubInstallation { + return predicate.GithubInstallation(sql.FieldNotIn(FieldStatus, vs...)) +} + +// StatusGT applies the GT predicate on the "status" field. +func StatusGT(v string) predicate.GithubInstallation { + return predicate.GithubInstallation(sql.FieldGT(FieldStatus, v)) +} + +// StatusGTE applies the GTE predicate on the "status" field. +func StatusGTE(v string) predicate.GithubInstallation { + return predicate.GithubInstallation(sql.FieldGTE(FieldStatus, v)) +} + +// StatusLT applies the LT predicate on the "status" field. +func StatusLT(v string) predicate.GithubInstallation { + return predicate.GithubInstallation(sql.FieldLT(FieldStatus, v)) +} + +// StatusLTE applies the LTE predicate on the "status" field. +func StatusLTE(v string) predicate.GithubInstallation { + return predicate.GithubInstallation(sql.FieldLTE(FieldStatus, v)) +} + +// StatusContains applies the Contains predicate on the "status" field. +func StatusContains(v string) predicate.GithubInstallation { + return predicate.GithubInstallation(sql.FieldContains(FieldStatus, v)) +} + +// StatusHasPrefix applies the HasPrefix predicate on the "status" field. +func StatusHasPrefix(v string) predicate.GithubInstallation { + return predicate.GithubInstallation(sql.FieldHasPrefix(FieldStatus, v)) +} + +// StatusHasSuffix applies the HasSuffix predicate on the "status" field. +func StatusHasSuffix(v string) predicate.GithubInstallation { + return predicate.GithubInstallation(sql.FieldHasSuffix(FieldStatus, v)) +} + +// StatusEqualFold applies the EqualFold predicate on the "status" field. +func StatusEqualFold(v string) predicate.GithubInstallation { + return predicate.GithubInstallation(sql.FieldEqualFold(FieldStatus, v)) +} + +// StatusContainsFold applies the ContainsFold predicate on the "status" field. +func StatusContainsFold(v string) predicate.GithubInstallation { + return predicate.GithubInstallation(sql.FieldContainsFold(FieldStatus, v)) +} + +// CreatedEQ applies the EQ predicate on the "created" field. +func CreatedEQ(v time.Time) predicate.GithubInstallation { + return predicate.GithubInstallation(sql.FieldEQ(FieldCreated, v)) +} + +// CreatedNEQ applies the NEQ predicate on the "created" field. +func CreatedNEQ(v time.Time) predicate.GithubInstallation { + return predicate.GithubInstallation(sql.FieldNEQ(FieldCreated, v)) +} + +// CreatedIn applies the In predicate on the "created" field. +func CreatedIn(vs ...time.Time) predicate.GithubInstallation { + return predicate.GithubInstallation(sql.FieldIn(FieldCreated, vs...)) +} + +// CreatedNotIn applies the NotIn predicate on the "created" field. +func CreatedNotIn(vs ...time.Time) predicate.GithubInstallation { + return predicate.GithubInstallation(sql.FieldNotIn(FieldCreated, vs...)) +} + +// CreatedGT applies the GT predicate on the "created" field. +func CreatedGT(v time.Time) predicate.GithubInstallation { + return predicate.GithubInstallation(sql.FieldGT(FieldCreated, v)) +} + +// CreatedGTE applies the GTE predicate on the "created" field. +func CreatedGTE(v time.Time) predicate.GithubInstallation { + return predicate.GithubInstallation(sql.FieldGTE(FieldCreated, v)) +} + +// CreatedLT applies the LT predicate on the "created" field. +func CreatedLT(v time.Time) predicate.GithubInstallation { + return predicate.GithubInstallation(sql.FieldLT(FieldCreated, v)) +} + +// CreatedLTE applies the LTE predicate on the "created" field. +func CreatedLTE(v time.Time) predicate.GithubInstallation { + return predicate.GithubInstallation(sql.FieldLTE(FieldCreated, v)) +} + +// UpdatedEQ applies the EQ predicate on the "updated" field. +func UpdatedEQ(v time.Time) predicate.GithubInstallation { + return predicate.GithubInstallation(sql.FieldEQ(FieldUpdated, v)) +} + +// UpdatedNEQ applies the NEQ predicate on the "updated" field. +func UpdatedNEQ(v time.Time) predicate.GithubInstallation { + return predicate.GithubInstallation(sql.FieldNEQ(FieldUpdated, v)) +} + +// UpdatedIn applies the In predicate on the "updated" field. +func UpdatedIn(vs ...time.Time) predicate.GithubInstallation { + return predicate.GithubInstallation(sql.FieldIn(FieldUpdated, vs...)) +} + +// UpdatedNotIn applies the NotIn predicate on the "updated" field. +func UpdatedNotIn(vs ...time.Time) predicate.GithubInstallation { + return predicate.GithubInstallation(sql.FieldNotIn(FieldUpdated, vs...)) +} + +// UpdatedGT applies the GT predicate on the "updated" field. +func UpdatedGT(v time.Time) predicate.GithubInstallation { + return predicate.GithubInstallation(sql.FieldGT(FieldUpdated, v)) +} + +// UpdatedGTE applies the GTE predicate on the "updated" field. +func UpdatedGTE(v time.Time) predicate.GithubInstallation { + return predicate.GithubInstallation(sql.FieldGTE(FieldUpdated, v)) +} + +// UpdatedLT applies the LT predicate on the "updated" field. +func UpdatedLT(v time.Time) predicate.GithubInstallation { + return predicate.GithubInstallation(sql.FieldLT(FieldUpdated, v)) +} + +// UpdatedLTE applies the LTE predicate on the "updated" field. +func UpdatedLTE(v time.Time) predicate.GithubInstallation { + return predicate.GithubInstallation(sql.FieldLTE(FieldUpdated, v)) +} + +// And groups predicates with the AND operator between them. +func And(predicates ...predicate.GithubInstallation) predicate.GithubInstallation { + return predicate.GithubInstallation(sql.AndPredicates(predicates...)) +} + +// Or groups predicates with the OR operator between them. +func Or(predicates ...predicate.GithubInstallation) predicate.GithubInstallation { + return predicate.GithubInstallation(sql.OrPredicates(predicates...)) +} + +// Not applies the not operator on the given predicate. +func Not(p predicate.GithubInstallation) predicate.GithubInstallation { + return predicate.GithubInstallation(sql.NotPredicates(p)) +} diff --git a/pkg/ent/githubinstallation_create.go b/pkg/ent/githubinstallation_create.go new file mode 100644 index 000000000..b457bcc20 --- /dev/null +++ b/pkg/ent/githubinstallation_create.go @@ -0,0 +1,860 @@ +// Code generated by ent, DO NOT EDIT. + +package ent + +import ( + "context" + "errors" + "fmt" + "time" + + "entgo.io/ent/dialect/sql" + "entgo.io/ent/dialect/sql/sqlgraph" + "entgo.io/ent/schema/field" + "github.com/GoogleCloudPlatform/scion/pkg/ent/githubinstallation" +) + +// GithubInstallationCreate is the builder for creating a GithubInstallation entity. +type GithubInstallationCreate struct { + config + mutation *GithubInstallationMutation + hooks []Hook + conflict []sql.ConflictOption +} + +// SetAccountLogin sets the "account_login" field. +func (_c *GithubInstallationCreate) SetAccountLogin(v string) *GithubInstallationCreate { + _c.mutation.SetAccountLogin(v) + return _c +} + +// SetAccountType sets the "account_type" field. +func (_c *GithubInstallationCreate) SetAccountType(v string) *GithubInstallationCreate { + _c.mutation.SetAccountType(v) + return _c +} + +// SetNillableAccountType sets the "account_type" field if the given value is not nil. +func (_c *GithubInstallationCreate) SetNillableAccountType(v *string) *GithubInstallationCreate { + if v != nil { + _c.SetAccountType(*v) + } + return _c +} + +// SetAppID sets the "app_id" field. +func (_c *GithubInstallationCreate) SetAppID(v int64) *GithubInstallationCreate { + _c.mutation.SetAppID(v) + return _c +} + +// SetRepositories sets the "repositories" field. +func (_c *GithubInstallationCreate) SetRepositories(v string) *GithubInstallationCreate { + _c.mutation.SetRepositories(v) + return _c +} + +// SetNillableRepositories sets the "repositories" field if the given value is not nil. +func (_c *GithubInstallationCreate) SetNillableRepositories(v *string) *GithubInstallationCreate { + if v != nil { + _c.SetRepositories(*v) + } + return _c +} + +// SetStatus sets the "status" field. +func (_c *GithubInstallationCreate) SetStatus(v string) *GithubInstallationCreate { + _c.mutation.SetStatus(v) + return _c +} + +// SetNillableStatus sets the "status" field if the given value is not nil. +func (_c *GithubInstallationCreate) SetNillableStatus(v *string) *GithubInstallationCreate { + if v != nil { + _c.SetStatus(*v) + } + return _c +} + +// SetCreated sets the "created" field. +func (_c *GithubInstallationCreate) SetCreated(v time.Time) *GithubInstallationCreate { + _c.mutation.SetCreated(v) + return _c +} + +// SetNillableCreated sets the "created" field if the given value is not nil. +func (_c *GithubInstallationCreate) SetNillableCreated(v *time.Time) *GithubInstallationCreate { + if v != nil { + _c.SetCreated(*v) + } + return _c +} + +// SetUpdated sets the "updated" field. +func (_c *GithubInstallationCreate) SetUpdated(v time.Time) *GithubInstallationCreate { + _c.mutation.SetUpdated(v) + return _c +} + +// SetNillableUpdated sets the "updated" field if the given value is not nil. +func (_c *GithubInstallationCreate) SetNillableUpdated(v *time.Time) *GithubInstallationCreate { + if v != nil { + _c.SetUpdated(*v) + } + return _c +} + +// SetID sets the "id" field. +func (_c *GithubInstallationCreate) SetID(v int64) *GithubInstallationCreate { + _c.mutation.SetID(v) + return _c +} + +// Mutation returns the GithubInstallationMutation object of the builder. +func (_c *GithubInstallationCreate) Mutation() *GithubInstallationMutation { + return _c.mutation +} + +// Save creates the GithubInstallation in the database. +func (_c *GithubInstallationCreate) Save(ctx context.Context) (*GithubInstallation, error) { + _c.defaults() + return withHooks(ctx, _c.sqlSave, _c.mutation, _c.hooks) +} + +// SaveX calls Save and panics if Save returns an error. +func (_c *GithubInstallationCreate) SaveX(ctx context.Context) *GithubInstallation { + v, err := _c.Save(ctx) + if err != nil { + panic(err) + } + return v +} + +// Exec executes the query. +func (_c *GithubInstallationCreate) Exec(ctx context.Context) error { + _, err := _c.Save(ctx) + return err +} + +// ExecX is like Exec, but panics if an error occurs. +func (_c *GithubInstallationCreate) ExecX(ctx context.Context) { + if err := _c.Exec(ctx); err != nil { + panic(err) + } +} + +// defaults sets the default values of the builder before save. +func (_c *GithubInstallationCreate) defaults() { + if _, ok := _c.mutation.AccountType(); !ok { + v := githubinstallation.DefaultAccountType + _c.mutation.SetAccountType(v) + } + if _, ok := _c.mutation.Repositories(); !ok { + v := githubinstallation.DefaultRepositories + _c.mutation.SetRepositories(v) + } + if _, ok := _c.mutation.Status(); !ok { + v := githubinstallation.DefaultStatus + _c.mutation.SetStatus(v) + } + if _, ok := _c.mutation.Created(); !ok { + v := githubinstallation.DefaultCreated() + _c.mutation.SetCreated(v) + } + if _, ok := _c.mutation.Updated(); !ok { + v := githubinstallation.DefaultUpdated() + _c.mutation.SetUpdated(v) + } +} + +// check runs all checks and user-defined validators on the builder. +func (_c *GithubInstallationCreate) check() error { + if _, ok := _c.mutation.AccountLogin(); !ok { + return &ValidationError{Name: "account_login", err: errors.New(`ent: missing required field "GithubInstallation.account_login"`)} + } + if v, ok := _c.mutation.AccountLogin(); ok { + if err := githubinstallation.AccountLoginValidator(v); err != nil { + return &ValidationError{Name: "account_login", err: fmt.Errorf(`ent: validator failed for field "GithubInstallation.account_login": %w`, err)} + } + } + if _, ok := _c.mutation.AccountType(); !ok { + return &ValidationError{Name: "account_type", err: errors.New(`ent: missing required field "GithubInstallation.account_type"`)} + } + if _, ok := _c.mutation.AppID(); !ok { + return &ValidationError{Name: "app_id", err: errors.New(`ent: missing required field "GithubInstallation.app_id"`)} + } + if _, ok := _c.mutation.Repositories(); !ok { + return &ValidationError{Name: "repositories", err: errors.New(`ent: missing required field "GithubInstallation.repositories"`)} + } + if _, ok := _c.mutation.Status(); !ok { + return &ValidationError{Name: "status", err: errors.New(`ent: missing required field "GithubInstallation.status"`)} + } + if _, ok := _c.mutation.Created(); !ok { + return &ValidationError{Name: "created", err: errors.New(`ent: missing required field "GithubInstallation.created"`)} + } + if _, ok := _c.mutation.Updated(); !ok { + return &ValidationError{Name: "updated", err: errors.New(`ent: missing required field "GithubInstallation.updated"`)} + } + return nil +} + +func (_c *GithubInstallationCreate) sqlSave(ctx context.Context) (*GithubInstallation, error) { + if err := _c.check(); err != nil { + return nil, err + } + _node, _spec := _c.createSpec() + if err := sqlgraph.CreateNode(ctx, _c.driver, _spec); err != nil { + if sqlgraph.IsConstraintError(err) { + err = &ConstraintError{msg: err.Error(), wrap: err} + } + return nil, err + } + if _spec.ID.Value != _node.ID { + id := _spec.ID.Value.(int64) + _node.ID = int64(id) + } + _c.mutation.id = &_node.ID + _c.mutation.done = true + return _node, nil +} + +func (_c *GithubInstallationCreate) createSpec() (*GithubInstallation, *sqlgraph.CreateSpec) { + var ( + _node = &GithubInstallation{config: _c.config} + _spec = sqlgraph.NewCreateSpec(githubinstallation.Table, sqlgraph.NewFieldSpec(githubinstallation.FieldID, field.TypeInt64)) + ) + _spec.OnConflict = _c.conflict + if id, ok := _c.mutation.ID(); ok { + _node.ID = id + _spec.ID.Value = id + } + if value, ok := _c.mutation.AccountLogin(); ok { + _spec.SetField(githubinstallation.FieldAccountLogin, field.TypeString, value) + _node.AccountLogin = value + } + if value, ok := _c.mutation.AccountType(); ok { + _spec.SetField(githubinstallation.FieldAccountType, field.TypeString, value) + _node.AccountType = value + } + if value, ok := _c.mutation.AppID(); ok { + _spec.SetField(githubinstallation.FieldAppID, field.TypeInt64, value) + _node.AppID = value + } + if value, ok := _c.mutation.Repositories(); ok { + _spec.SetField(githubinstallation.FieldRepositories, field.TypeString, value) + _node.Repositories = value + } + if value, ok := _c.mutation.Status(); ok { + _spec.SetField(githubinstallation.FieldStatus, field.TypeString, value) + _node.Status = value + } + if value, ok := _c.mutation.Created(); ok { + _spec.SetField(githubinstallation.FieldCreated, field.TypeTime, value) + _node.Created = value + } + if value, ok := _c.mutation.Updated(); ok { + _spec.SetField(githubinstallation.FieldUpdated, field.TypeTime, value) + _node.Updated = value + } + return _node, _spec +} + +// OnConflict allows configuring the `ON CONFLICT` / `ON DUPLICATE KEY` clause +// of the `INSERT` statement. For example: +// +// client.GithubInstallation.Create(). +// SetAccountLogin(v). +// OnConflict( +// // Update the row with the new values +// // the was proposed for insertion. +// sql.ResolveWithNewValues(), +// ). +// // Override some of the fields with custom +// // update values. +// Update(func(u *ent.GithubInstallationUpsert) { +// SetAccountLogin(v+v). +// }). +// Exec(ctx) +func (_c *GithubInstallationCreate) OnConflict(opts ...sql.ConflictOption) *GithubInstallationUpsertOne { + _c.conflict = opts + return &GithubInstallationUpsertOne{ + create: _c, + } +} + +// OnConflictColumns calls `OnConflict` and configures the columns +// as conflict target. Using this option is equivalent to using: +// +// client.GithubInstallation.Create(). +// OnConflict(sql.ConflictColumns(columns...)). +// Exec(ctx) +func (_c *GithubInstallationCreate) OnConflictColumns(columns ...string) *GithubInstallationUpsertOne { + _c.conflict = append(_c.conflict, sql.ConflictColumns(columns...)) + return &GithubInstallationUpsertOne{ + create: _c, + } +} + +type ( + // GithubInstallationUpsertOne is the builder for "upsert"-ing + // one GithubInstallation node. + GithubInstallationUpsertOne struct { + create *GithubInstallationCreate + } + + // GithubInstallationUpsert is the "OnConflict" setter. + GithubInstallationUpsert struct { + *sql.UpdateSet + } +) + +// SetAccountLogin sets the "account_login" field. +func (u *GithubInstallationUpsert) SetAccountLogin(v string) *GithubInstallationUpsert { + u.Set(githubinstallation.FieldAccountLogin, v) + return u +} + +// UpdateAccountLogin sets the "account_login" field to the value that was provided on create. +func (u *GithubInstallationUpsert) UpdateAccountLogin() *GithubInstallationUpsert { + u.SetExcluded(githubinstallation.FieldAccountLogin) + return u +} + +// SetAccountType sets the "account_type" field. +func (u *GithubInstallationUpsert) SetAccountType(v string) *GithubInstallationUpsert { + u.Set(githubinstallation.FieldAccountType, v) + return u +} + +// UpdateAccountType sets the "account_type" field to the value that was provided on create. +func (u *GithubInstallationUpsert) UpdateAccountType() *GithubInstallationUpsert { + u.SetExcluded(githubinstallation.FieldAccountType) + return u +} + +// SetAppID sets the "app_id" field. +func (u *GithubInstallationUpsert) SetAppID(v int64) *GithubInstallationUpsert { + u.Set(githubinstallation.FieldAppID, v) + return u +} + +// UpdateAppID sets the "app_id" field to the value that was provided on create. +func (u *GithubInstallationUpsert) UpdateAppID() *GithubInstallationUpsert { + u.SetExcluded(githubinstallation.FieldAppID) + return u +} + +// AddAppID adds v to the "app_id" field. +func (u *GithubInstallationUpsert) AddAppID(v int64) *GithubInstallationUpsert { + u.Add(githubinstallation.FieldAppID, v) + return u +} + +// SetRepositories sets the "repositories" field. +func (u *GithubInstallationUpsert) SetRepositories(v string) *GithubInstallationUpsert { + u.Set(githubinstallation.FieldRepositories, v) + return u +} + +// UpdateRepositories sets the "repositories" field to the value that was provided on create. +func (u *GithubInstallationUpsert) UpdateRepositories() *GithubInstallationUpsert { + u.SetExcluded(githubinstallation.FieldRepositories) + return u +} + +// SetStatus sets the "status" field. +func (u *GithubInstallationUpsert) SetStatus(v string) *GithubInstallationUpsert { + u.Set(githubinstallation.FieldStatus, v) + return u +} + +// UpdateStatus sets the "status" field to the value that was provided on create. +func (u *GithubInstallationUpsert) UpdateStatus() *GithubInstallationUpsert { + u.SetExcluded(githubinstallation.FieldStatus) + return u +} + +// SetUpdated sets the "updated" field. +func (u *GithubInstallationUpsert) SetUpdated(v time.Time) *GithubInstallationUpsert { + u.Set(githubinstallation.FieldUpdated, v) + return u +} + +// UpdateUpdated sets the "updated" field to the value that was provided on create. +func (u *GithubInstallationUpsert) UpdateUpdated() *GithubInstallationUpsert { + u.SetExcluded(githubinstallation.FieldUpdated) + return u +} + +// UpdateNewValues updates the mutable fields using the new values that were set on create except the ID field. +// Using this option is equivalent to using: +// +// client.GithubInstallation.Create(). +// OnConflict( +// sql.ResolveWithNewValues(), +// sql.ResolveWith(func(u *sql.UpdateSet) { +// u.SetIgnore(githubinstallation.FieldID) +// }), +// ). +// Exec(ctx) +func (u *GithubInstallationUpsertOne) UpdateNewValues() *GithubInstallationUpsertOne { + u.create.conflict = append(u.create.conflict, sql.ResolveWithNewValues()) + u.create.conflict = append(u.create.conflict, sql.ResolveWith(func(s *sql.UpdateSet) { + if _, exists := u.create.mutation.ID(); exists { + s.SetIgnore(githubinstallation.FieldID) + } + if _, exists := u.create.mutation.Created(); exists { + s.SetIgnore(githubinstallation.FieldCreated) + } + })) + return u +} + +// Ignore sets each column to itself in case of conflict. +// Using this option is equivalent to using: +// +// client.GithubInstallation.Create(). +// OnConflict(sql.ResolveWithIgnore()). +// Exec(ctx) +func (u *GithubInstallationUpsertOne) Ignore() *GithubInstallationUpsertOne { + u.create.conflict = append(u.create.conflict, sql.ResolveWithIgnore()) + return u +} + +// DoNothing configures the conflict_action to `DO NOTHING`. +// Supported only by SQLite and PostgreSQL. +func (u *GithubInstallationUpsertOne) DoNothing() *GithubInstallationUpsertOne { + u.create.conflict = append(u.create.conflict, sql.DoNothing()) + return u +} + +// Update allows overriding fields `UPDATE` values. See the GithubInstallationCreate.OnConflict +// documentation for more info. +func (u *GithubInstallationUpsertOne) Update(set func(*GithubInstallationUpsert)) *GithubInstallationUpsertOne { + u.create.conflict = append(u.create.conflict, sql.ResolveWith(func(update *sql.UpdateSet) { + set(&GithubInstallationUpsert{UpdateSet: update}) + })) + return u +} + +// SetAccountLogin sets the "account_login" field. +func (u *GithubInstallationUpsertOne) SetAccountLogin(v string) *GithubInstallationUpsertOne { + return u.Update(func(s *GithubInstallationUpsert) { + s.SetAccountLogin(v) + }) +} + +// UpdateAccountLogin sets the "account_login" field to the value that was provided on create. +func (u *GithubInstallationUpsertOne) UpdateAccountLogin() *GithubInstallationUpsertOne { + return u.Update(func(s *GithubInstallationUpsert) { + s.UpdateAccountLogin() + }) +} + +// SetAccountType sets the "account_type" field. +func (u *GithubInstallationUpsertOne) SetAccountType(v string) *GithubInstallationUpsertOne { + return u.Update(func(s *GithubInstallationUpsert) { + s.SetAccountType(v) + }) +} + +// UpdateAccountType sets the "account_type" field to the value that was provided on create. +func (u *GithubInstallationUpsertOne) UpdateAccountType() *GithubInstallationUpsertOne { + return u.Update(func(s *GithubInstallationUpsert) { + s.UpdateAccountType() + }) +} + +// SetAppID sets the "app_id" field. +func (u *GithubInstallationUpsertOne) SetAppID(v int64) *GithubInstallationUpsertOne { + return u.Update(func(s *GithubInstallationUpsert) { + s.SetAppID(v) + }) +} + +// AddAppID adds v to the "app_id" field. +func (u *GithubInstallationUpsertOne) AddAppID(v int64) *GithubInstallationUpsertOne { + return u.Update(func(s *GithubInstallationUpsert) { + s.AddAppID(v) + }) +} + +// UpdateAppID sets the "app_id" field to the value that was provided on create. +func (u *GithubInstallationUpsertOne) UpdateAppID() *GithubInstallationUpsertOne { + return u.Update(func(s *GithubInstallationUpsert) { + s.UpdateAppID() + }) +} + +// SetRepositories sets the "repositories" field. +func (u *GithubInstallationUpsertOne) SetRepositories(v string) *GithubInstallationUpsertOne { + return u.Update(func(s *GithubInstallationUpsert) { + s.SetRepositories(v) + }) +} + +// UpdateRepositories sets the "repositories" field to the value that was provided on create. +func (u *GithubInstallationUpsertOne) UpdateRepositories() *GithubInstallationUpsertOne { + return u.Update(func(s *GithubInstallationUpsert) { + s.UpdateRepositories() + }) +} + +// SetStatus sets the "status" field. +func (u *GithubInstallationUpsertOne) SetStatus(v string) *GithubInstallationUpsertOne { + return u.Update(func(s *GithubInstallationUpsert) { + s.SetStatus(v) + }) +} + +// UpdateStatus sets the "status" field to the value that was provided on create. +func (u *GithubInstallationUpsertOne) UpdateStatus() *GithubInstallationUpsertOne { + return u.Update(func(s *GithubInstallationUpsert) { + s.UpdateStatus() + }) +} + +// SetUpdated sets the "updated" field. +func (u *GithubInstallationUpsertOne) SetUpdated(v time.Time) *GithubInstallationUpsertOne { + return u.Update(func(s *GithubInstallationUpsert) { + s.SetUpdated(v) + }) +} + +// UpdateUpdated sets the "updated" field to the value that was provided on create. +func (u *GithubInstallationUpsertOne) UpdateUpdated() *GithubInstallationUpsertOne { + return u.Update(func(s *GithubInstallationUpsert) { + s.UpdateUpdated() + }) +} + +// Exec executes the query. +func (u *GithubInstallationUpsertOne) Exec(ctx context.Context) error { + if len(u.create.conflict) == 0 { + return errors.New("ent: missing options for GithubInstallationCreate.OnConflict") + } + return u.create.Exec(ctx) +} + +// ExecX is like Exec, but panics if an error occurs. +func (u *GithubInstallationUpsertOne) ExecX(ctx context.Context) { + if err := u.create.Exec(ctx); err != nil { + panic(err) + } +} + +// Exec executes the UPSERT query and returns the inserted/updated ID. +func (u *GithubInstallationUpsertOne) ID(ctx context.Context) (id int64, err error) { + node, err := u.create.Save(ctx) + if err != nil { + return id, err + } + return node.ID, nil +} + +// IDX is like ID, but panics if an error occurs. +func (u *GithubInstallationUpsertOne) IDX(ctx context.Context) int64 { + id, err := u.ID(ctx) + if err != nil { + panic(err) + } + return id +} + +// GithubInstallationCreateBulk is the builder for creating many GithubInstallation entities in bulk. +type GithubInstallationCreateBulk struct { + config + err error + builders []*GithubInstallationCreate + conflict []sql.ConflictOption +} + +// Save creates the GithubInstallation entities in the database. +func (_c *GithubInstallationCreateBulk) Save(ctx context.Context) ([]*GithubInstallation, error) { + if _c.err != nil { + return nil, _c.err + } + specs := make([]*sqlgraph.CreateSpec, len(_c.builders)) + nodes := make([]*GithubInstallation, len(_c.builders)) + mutators := make([]Mutator, len(_c.builders)) + for i := range _c.builders { + func(i int, root context.Context) { + builder := _c.builders[i] + builder.defaults() + var mut Mutator = MutateFunc(func(ctx context.Context, m Mutation) (Value, error) { + mutation, ok := m.(*GithubInstallationMutation) + if !ok { + return nil, fmt.Errorf("unexpected mutation type %T", m) + } + if err := builder.check(); err != nil { + return nil, err + } + builder.mutation = mutation + var err error + nodes[i], specs[i] = builder.createSpec() + if i < len(mutators)-1 { + _, err = mutators[i+1].Mutate(root, _c.builders[i+1].mutation) + } else { + spec := &sqlgraph.BatchCreateSpec{Nodes: specs} + spec.OnConflict = _c.conflict + // Invoke the actual operation on the latest mutation in the chain. + if err = sqlgraph.BatchCreate(ctx, _c.driver, spec); err != nil { + if sqlgraph.IsConstraintError(err) { + err = &ConstraintError{msg: err.Error(), wrap: err} + } + } + } + if err != nil { + return nil, err + } + mutation.id = &nodes[i].ID + if specs[i].ID.Value != nil && nodes[i].ID == 0 { + id := specs[i].ID.Value.(int64) + nodes[i].ID = int64(id) + } + mutation.done = true + return nodes[i], nil + }) + for i := len(builder.hooks) - 1; i >= 0; i-- { + mut = builder.hooks[i](mut) + } + mutators[i] = mut + }(i, ctx) + } + if len(mutators) > 0 { + if _, err := mutators[0].Mutate(ctx, _c.builders[0].mutation); err != nil { + return nil, err + } + } + return nodes, nil +} + +// SaveX is like Save, but panics if an error occurs. +func (_c *GithubInstallationCreateBulk) SaveX(ctx context.Context) []*GithubInstallation { + v, err := _c.Save(ctx) + if err != nil { + panic(err) + } + return v +} + +// Exec executes the query. +func (_c *GithubInstallationCreateBulk) Exec(ctx context.Context) error { + _, err := _c.Save(ctx) + return err +} + +// ExecX is like Exec, but panics if an error occurs. +func (_c *GithubInstallationCreateBulk) ExecX(ctx context.Context) { + if err := _c.Exec(ctx); err != nil { + panic(err) + } +} + +// OnConflict allows configuring the `ON CONFLICT` / `ON DUPLICATE KEY` clause +// of the `INSERT` statement. For example: +// +// client.GithubInstallation.CreateBulk(builders...). +// OnConflict( +// // Update the row with the new values +// // the was proposed for insertion. +// sql.ResolveWithNewValues(), +// ). +// // Override some of the fields with custom +// // update values. +// Update(func(u *ent.GithubInstallationUpsert) { +// SetAccountLogin(v+v). +// }). +// Exec(ctx) +func (_c *GithubInstallationCreateBulk) OnConflict(opts ...sql.ConflictOption) *GithubInstallationUpsertBulk { + _c.conflict = opts + return &GithubInstallationUpsertBulk{ + create: _c, + } +} + +// OnConflictColumns calls `OnConflict` and configures the columns +// as conflict target. Using this option is equivalent to using: +// +// client.GithubInstallation.Create(). +// OnConflict(sql.ConflictColumns(columns...)). +// Exec(ctx) +func (_c *GithubInstallationCreateBulk) OnConflictColumns(columns ...string) *GithubInstallationUpsertBulk { + _c.conflict = append(_c.conflict, sql.ConflictColumns(columns...)) + return &GithubInstallationUpsertBulk{ + create: _c, + } +} + +// GithubInstallationUpsertBulk is the builder for "upsert"-ing +// a bulk of GithubInstallation nodes. +type GithubInstallationUpsertBulk struct { + create *GithubInstallationCreateBulk +} + +// UpdateNewValues updates the mutable fields using the new values that +// were set on create. Using this option is equivalent to using: +// +// client.GithubInstallation.Create(). +// OnConflict( +// sql.ResolveWithNewValues(), +// sql.ResolveWith(func(u *sql.UpdateSet) { +// u.SetIgnore(githubinstallation.FieldID) +// }), +// ). +// Exec(ctx) +func (u *GithubInstallationUpsertBulk) UpdateNewValues() *GithubInstallationUpsertBulk { + u.create.conflict = append(u.create.conflict, sql.ResolveWithNewValues()) + u.create.conflict = append(u.create.conflict, sql.ResolveWith(func(s *sql.UpdateSet) { + for _, b := range u.create.builders { + if _, exists := b.mutation.ID(); exists { + s.SetIgnore(githubinstallation.FieldID) + } + if _, exists := b.mutation.Created(); exists { + s.SetIgnore(githubinstallation.FieldCreated) + } + } + })) + return u +} + +// Ignore sets each column to itself in case of conflict. +// Using this option is equivalent to using: +// +// client.GithubInstallation.Create(). +// OnConflict(sql.ResolveWithIgnore()). +// Exec(ctx) +func (u *GithubInstallationUpsertBulk) Ignore() *GithubInstallationUpsertBulk { + u.create.conflict = append(u.create.conflict, sql.ResolveWithIgnore()) + return u +} + +// DoNothing configures the conflict_action to `DO NOTHING`. +// Supported only by SQLite and PostgreSQL. +func (u *GithubInstallationUpsertBulk) DoNothing() *GithubInstallationUpsertBulk { + u.create.conflict = append(u.create.conflict, sql.DoNothing()) + return u +} + +// Update allows overriding fields `UPDATE` values. See the GithubInstallationCreateBulk.OnConflict +// documentation for more info. +func (u *GithubInstallationUpsertBulk) Update(set func(*GithubInstallationUpsert)) *GithubInstallationUpsertBulk { + u.create.conflict = append(u.create.conflict, sql.ResolveWith(func(update *sql.UpdateSet) { + set(&GithubInstallationUpsert{UpdateSet: update}) + })) + return u +} + +// SetAccountLogin sets the "account_login" field. +func (u *GithubInstallationUpsertBulk) SetAccountLogin(v string) *GithubInstallationUpsertBulk { + return u.Update(func(s *GithubInstallationUpsert) { + s.SetAccountLogin(v) + }) +} + +// UpdateAccountLogin sets the "account_login" field to the value that was provided on create. +func (u *GithubInstallationUpsertBulk) UpdateAccountLogin() *GithubInstallationUpsertBulk { + return u.Update(func(s *GithubInstallationUpsert) { + s.UpdateAccountLogin() + }) +} + +// SetAccountType sets the "account_type" field. +func (u *GithubInstallationUpsertBulk) SetAccountType(v string) *GithubInstallationUpsertBulk { + return u.Update(func(s *GithubInstallationUpsert) { + s.SetAccountType(v) + }) +} + +// UpdateAccountType sets the "account_type" field to the value that was provided on create. +func (u *GithubInstallationUpsertBulk) UpdateAccountType() *GithubInstallationUpsertBulk { + return u.Update(func(s *GithubInstallationUpsert) { + s.UpdateAccountType() + }) +} + +// SetAppID sets the "app_id" field. +func (u *GithubInstallationUpsertBulk) SetAppID(v int64) *GithubInstallationUpsertBulk { + return u.Update(func(s *GithubInstallationUpsert) { + s.SetAppID(v) + }) +} + +// AddAppID adds v to the "app_id" field. +func (u *GithubInstallationUpsertBulk) AddAppID(v int64) *GithubInstallationUpsertBulk { + return u.Update(func(s *GithubInstallationUpsert) { + s.AddAppID(v) + }) +} + +// UpdateAppID sets the "app_id" field to the value that was provided on create. +func (u *GithubInstallationUpsertBulk) UpdateAppID() *GithubInstallationUpsertBulk { + return u.Update(func(s *GithubInstallationUpsert) { + s.UpdateAppID() + }) +} + +// SetRepositories sets the "repositories" field. +func (u *GithubInstallationUpsertBulk) SetRepositories(v string) *GithubInstallationUpsertBulk { + return u.Update(func(s *GithubInstallationUpsert) { + s.SetRepositories(v) + }) +} + +// UpdateRepositories sets the "repositories" field to the value that was provided on create. +func (u *GithubInstallationUpsertBulk) UpdateRepositories() *GithubInstallationUpsertBulk { + return u.Update(func(s *GithubInstallationUpsert) { + s.UpdateRepositories() + }) +} + +// SetStatus sets the "status" field. +func (u *GithubInstallationUpsertBulk) SetStatus(v string) *GithubInstallationUpsertBulk { + return u.Update(func(s *GithubInstallationUpsert) { + s.SetStatus(v) + }) +} + +// UpdateStatus sets the "status" field to the value that was provided on create. +func (u *GithubInstallationUpsertBulk) UpdateStatus() *GithubInstallationUpsertBulk { + return u.Update(func(s *GithubInstallationUpsert) { + s.UpdateStatus() + }) +} + +// SetUpdated sets the "updated" field. +func (u *GithubInstallationUpsertBulk) SetUpdated(v time.Time) *GithubInstallationUpsertBulk { + return u.Update(func(s *GithubInstallationUpsert) { + s.SetUpdated(v) + }) +} + +// UpdateUpdated sets the "updated" field to the value that was provided on create. +func (u *GithubInstallationUpsertBulk) UpdateUpdated() *GithubInstallationUpsertBulk { + return u.Update(func(s *GithubInstallationUpsert) { + s.UpdateUpdated() + }) +} + +// Exec executes the query. +func (u *GithubInstallationUpsertBulk) Exec(ctx context.Context) error { + if u.create.err != nil { + return u.create.err + } + for i, b := range u.create.builders { + if len(b.conflict) != 0 { + return fmt.Errorf("ent: OnConflict was set for builder %d. Set it on the GithubInstallationCreateBulk instead", i) + } + } + if len(u.create.conflict) == 0 { + return errors.New("ent: missing options for GithubInstallationCreateBulk.OnConflict") + } + return u.create.Exec(ctx) +} + +// ExecX is like Exec, but panics if an error occurs. +func (u *GithubInstallationUpsertBulk) ExecX(ctx context.Context) { + if err := u.create.Exec(ctx); err != nil { + panic(err) + } +} diff --git a/pkg/ent/githubinstallation_delete.go b/pkg/ent/githubinstallation_delete.go new file mode 100644 index 000000000..31647abc9 --- /dev/null +++ b/pkg/ent/githubinstallation_delete.go @@ -0,0 +1,88 @@ +// Code generated by ent, DO NOT EDIT. + +package ent + +import ( + "context" + + "entgo.io/ent/dialect/sql" + "entgo.io/ent/dialect/sql/sqlgraph" + "entgo.io/ent/schema/field" + "github.com/GoogleCloudPlatform/scion/pkg/ent/githubinstallation" + "github.com/GoogleCloudPlatform/scion/pkg/ent/predicate" +) + +// GithubInstallationDelete is the builder for deleting a GithubInstallation entity. +type GithubInstallationDelete struct { + config + hooks []Hook + mutation *GithubInstallationMutation +} + +// Where appends a list predicates to the GithubInstallationDelete builder. +func (_d *GithubInstallationDelete) Where(ps ...predicate.GithubInstallation) *GithubInstallationDelete { + _d.mutation.Where(ps...) + return _d +} + +// Exec executes the deletion query and returns how many vertices were deleted. +func (_d *GithubInstallationDelete) Exec(ctx context.Context) (int, error) { + return withHooks(ctx, _d.sqlExec, _d.mutation, _d.hooks) +} + +// ExecX is like Exec, but panics if an error occurs. +func (_d *GithubInstallationDelete) ExecX(ctx context.Context) int { + n, err := _d.Exec(ctx) + if err != nil { + panic(err) + } + return n +} + +func (_d *GithubInstallationDelete) sqlExec(ctx context.Context) (int, error) { + _spec := sqlgraph.NewDeleteSpec(githubinstallation.Table, sqlgraph.NewFieldSpec(githubinstallation.FieldID, field.TypeInt64)) + if ps := _d.mutation.predicates; len(ps) > 0 { + _spec.Predicate = func(selector *sql.Selector) { + for i := range ps { + ps[i](selector) + } + } + } + affected, err := sqlgraph.DeleteNodes(ctx, _d.driver, _spec) + if err != nil && sqlgraph.IsConstraintError(err) { + err = &ConstraintError{msg: err.Error(), wrap: err} + } + _d.mutation.done = true + return affected, err +} + +// GithubInstallationDeleteOne is the builder for deleting a single GithubInstallation entity. +type GithubInstallationDeleteOne struct { + _d *GithubInstallationDelete +} + +// Where appends a list predicates to the GithubInstallationDelete builder. +func (_d *GithubInstallationDeleteOne) Where(ps ...predicate.GithubInstallation) *GithubInstallationDeleteOne { + _d._d.mutation.Where(ps...) + return _d +} + +// Exec executes the deletion query. +func (_d *GithubInstallationDeleteOne) Exec(ctx context.Context) error { + n, err := _d._d.Exec(ctx) + switch { + case err != nil: + return err + case n == 0: + return &NotFoundError{githubinstallation.Label} + default: + return nil + } +} + +// ExecX is like Exec, but panics if an error occurs. +func (_d *GithubInstallationDeleteOne) ExecX(ctx context.Context) { + if err := _d.Exec(ctx); err != nil { + panic(err) + } +} diff --git a/pkg/ent/githubinstallation_query.go b/pkg/ent/githubinstallation_query.go new file mode 100644 index 000000000..3ab727ef4 --- /dev/null +++ b/pkg/ent/githubinstallation_query.go @@ -0,0 +1,564 @@ +// Code generated by ent, DO NOT EDIT. + +package ent + +import ( + "context" + "fmt" + "math" + + "entgo.io/ent" + "entgo.io/ent/dialect" + "entgo.io/ent/dialect/sql" + "entgo.io/ent/dialect/sql/sqlgraph" + "entgo.io/ent/schema/field" + "github.com/GoogleCloudPlatform/scion/pkg/ent/githubinstallation" + "github.com/GoogleCloudPlatform/scion/pkg/ent/predicate" +) + +// GithubInstallationQuery is the builder for querying GithubInstallation entities. +type GithubInstallationQuery struct { + config + ctx *QueryContext + order []githubinstallation.OrderOption + inters []Interceptor + predicates []predicate.GithubInstallation + modifiers []func(*sql.Selector) + // intermediate query (i.e. traversal path). + sql *sql.Selector + path func(context.Context) (*sql.Selector, error) +} + +// Where adds a new predicate for the GithubInstallationQuery builder. +func (_q *GithubInstallationQuery) Where(ps ...predicate.GithubInstallation) *GithubInstallationQuery { + _q.predicates = append(_q.predicates, ps...) + return _q +} + +// Limit the number of records to be returned by this query. +func (_q *GithubInstallationQuery) Limit(limit int) *GithubInstallationQuery { + _q.ctx.Limit = &limit + return _q +} + +// Offset to start from. +func (_q *GithubInstallationQuery) Offset(offset int) *GithubInstallationQuery { + _q.ctx.Offset = &offset + return _q +} + +// Unique configures the query builder to filter duplicate records on query. +// By default, unique is set to true, and can be disabled using this method. +func (_q *GithubInstallationQuery) Unique(unique bool) *GithubInstallationQuery { + _q.ctx.Unique = &unique + return _q +} + +// Order specifies how the records should be ordered. +func (_q *GithubInstallationQuery) Order(o ...githubinstallation.OrderOption) *GithubInstallationQuery { + _q.order = append(_q.order, o...) + return _q +} + +// First returns the first GithubInstallation entity from the query. +// Returns a *NotFoundError when no GithubInstallation was found. +func (_q *GithubInstallationQuery) First(ctx context.Context) (*GithubInstallation, error) { + nodes, err := _q.Limit(1).All(setContextOp(ctx, _q.ctx, ent.OpQueryFirst)) + if err != nil { + return nil, err + } + if len(nodes) == 0 { + return nil, &NotFoundError{githubinstallation.Label} + } + return nodes[0], nil +} + +// FirstX is like First, but panics if an error occurs. +func (_q *GithubInstallationQuery) FirstX(ctx context.Context) *GithubInstallation { + node, err := _q.First(ctx) + if err != nil && !IsNotFound(err) { + panic(err) + } + return node +} + +// FirstID returns the first GithubInstallation ID from the query. +// Returns a *NotFoundError when no GithubInstallation ID was found. +func (_q *GithubInstallationQuery) FirstID(ctx context.Context) (id int64, err error) { + var ids []int64 + if ids, err = _q.Limit(1).IDs(setContextOp(ctx, _q.ctx, ent.OpQueryFirstID)); err != nil { + return + } + if len(ids) == 0 { + err = &NotFoundError{githubinstallation.Label} + return + } + return ids[0], nil +} + +// FirstIDX is like FirstID, but panics if an error occurs. +func (_q *GithubInstallationQuery) FirstIDX(ctx context.Context) int64 { + id, err := _q.FirstID(ctx) + if err != nil && !IsNotFound(err) { + panic(err) + } + return id +} + +// Only returns a single GithubInstallation entity found by the query, ensuring it only returns one. +// Returns a *NotSingularError when more than one GithubInstallation entity is found. +// Returns a *NotFoundError when no GithubInstallation entities are found. +func (_q *GithubInstallationQuery) Only(ctx context.Context) (*GithubInstallation, error) { + nodes, err := _q.Limit(2).All(setContextOp(ctx, _q.ctx, ent.OpQueryOnly)) + if err != nil { + return nil, err + } + switch len(nodes) { + case 1: + return nodes[0], nil + case 0: + return nil, &NotFoundError{githubinstallation.Label} + default: + return nil, &NotSingularError{githubinstallation.Label} + } +} + +// OnlyX is like Only, but panics if an error occurs. +func (_q *GithubInstallationQuery) OnlyX(ctx context.Context) *GithubInstallation { + node, err := _q.Only(ctx) + if err != nil { + panic(err) + } + return node +} + +// OnlyID is like Only, but returns the only GithubInstallation ID in the query. +// Returns a *NotSingularError when more than one GithubInstallation ID is found. +// Returns a *NotFoundError when no entities are found. +func (_q *GithubInstallationQuery) OnlyID(ctx context.Context) (id int64, err error) { + var ids []int64 + if ids, err = _q.Limit(2).IDs(setContextOp(ctx, _q.ctx, ent.OpQueryOnlyID)); err != nil { + return + } + switch len(ids) { + case 1: + id = ids[0] + case 0: + err = &NotFoundError{githubinstallation.Label} + default: + err = &NotSingularError{githubinstallation.Label} + } + return +} + +// OnlyIDX is like OnlyID, but panics if an error occurs. +func (_q *GithubInstallationQuery) OnlyIDX(ctx context.Context) int64 { + id, err := _q.OnlyID(ctx) + if err != nil { + panic(err) + } + return id +} + +// All executes the query and returns a list of GithubInstallations. +func (_q *GithubInstallationQuery) All(ctx context.Context) ([]*GithubInstallation, error) { + ctx = setContextOp(ctx, _q.ctx, ent.OpQueryAll) + if err := _q.prepareQuery(ctx); err != nil { + return nil, err + } + qr := querierAll[[]*GithubInstallation, *GithubInstallationQuery]() + return withInterceptors[[]*GithubInstallation](ctx, _q, qr, _q.inters) +} + +// AllX is like All, but panics if an error occurs. +func (_q *GithubInstallationQuery) AllX(ctx context.Context) []*GithubInstallation { + nodes, err := _q.All(ctx) + if err != nil { + panic(err) + } + return nodes +} + +// IDs executes the query and returns a list of GithubInstallation IDs. +func (_q *GithubInstallationQuery) IDs(ctx context.Context) (ids []int64, err error) { + if _q.ctx.Unique == nil && _q.path != nil { + _q.Unique(true) + } + ctx = setContextOp(ctx, _q.ctx, ent.OpQueryIDs) + if err = _q.Select(githubinstallation.FieldID).Scan(ctx, &ids); err != nil { + return nil, err + } + return ids, nil +} + +// IDsX is like IDs, but panics if an error occurs. +func (_q *GithubInstallationQuery) IDsX(ctx context.Context) []int64 { + ids, err := _q.IDs(ctx) + if err != nil { + panic(err) + } + return ids +} + +// Count returns the count of the given query. +func (_q *GithubInstallationQuery) Count(ctx context.Context) (int, error) { + ctx = setContextOp(ctx, _q.ctx, ent.OpQueryCount) + if err := _q.prepareQuery(ctx); err != nil { + return 0, err + } + return withInterceptors[int](ctx, _q, querierCount[*GithubInstallationQuery](), _q.inters) +} + +// CountX is like Count, but panics if an error occurs. +func (_q *GithubInstallationQuery) CountX(ctx context.Context) int { + count, err := _q.Count(ctx) + if err != nil { + panic(err) + } + return count +} + +// Exist returns true if the query has elements in the graph. +func (_q *GithubInstallationQuery) Exist(ctx context.Context) (bool, error) { + ctx = setContextOp(ctx, _q.ctx, ent.OpQueryExist) + switch _, err := _q.FirstID(ctx); { + case IsNotFound(err): + return false, nil + case err != nil: + return false, fmt.Errorf("ent: check existence: %w", err) + default: + return true, nil + } +} + +// ExistX is like Exist, but panics if an error occurs. +func (_q *GithubInstallationQuery) ExistX(ctx context.Context) bool { + exist, err := _q.Exist(ctx) + if err != nil { + panic(err) + } + return exist +} + +// Clone returns a duplicate of the GithubInstallationQuery builder, including all associated steps. It can be +// used to prepare common query builders and use them differently after the clone is made. +func (_q *GithubInstallationQuery) Clone() *GithubInstallationQuery { + if _q == nil { + return nil + } + return &GithubInstallationQuery{ + config: _q.config, + ctx: _q.ctx.Clone(), + order: append([]githubinstallation.OrderOption{}, _q.order...), + inters: append([]Interceptor{}, _q.inters...), + predicates: append([]predicate.GithubInstallation{}, _q.predicates...), + // clone intermediate query. + sql: _q.sql.Clone(), + path: _q.path, + } +} + +// GroupBy is used to group vertices by one or more fields/columns. +// It is often used with aggregate functions, like: count, max, mean, min, sum. +// +// Example: +// +// var v []struct { +// AccountLogin string `json:"account_login,omitempty"` +// Count int `json:"count,omitempty"` +// } +// +// client.GithubInstallation.Query(). +// GroupBy(githubinstallation.FieldAccountLogin). +// Aggregate(ent.Count()). +// Scan(ctx, &v) +func (_q *GithubInstallationQuery) GroupBy(field string, fields ...string) *GithubInstallationGroupBy { + _q.ctx.Fields = append([]string{field}, fields...) + grbuild := &GithubInstallationGroupBy{build: _q} + grbuild.flds = &_q.ctx.Fields + grbuild.label = githubinstallation.Label + grbuild.scan = grbuild.Scan + return grbuild +} + +// Select allows the selection one or more fields/columns for the given query, +// instead of selecting all fields in the entity. +// +// Example: +// +// var v []struct { +// AccountLogin string `json:"account_login,omitempty"` +// } +// +// client.GithubInstallation.Query(). +// Select(githubinstallation.FieldAccountLogin). +// Scan(ctx, &v) +func (_q *GithubInstallationQuery) Select(fields ...string) *GithubInstallationSelect { + _q.ctx.Fields = append(_q.ctx.Fields, fields...) + sbuild := &GithubInstallationSelect{GithubInstallationQuery: _q} + sbuild.label = githubinstallation.Label + sbuild.flds, sbuild.scan = &_q.ctx.Fields, sbuild.Scan + return sbuild +} + +// Aggregate returns a GithubInstallationSelect configured with the given aggregations. +func (_q *GithubInstallationQuery) Aggregate(fns ...AggregateFunc) *GithubInstallationSelect { + return _q.Select().Aggregate(fns...) +} + +func (_q *GithubInstallationQuery) prepareQuery(ctx context.Context) error { + for _, inter := range _q.inters { + if inter == nil { + return fmt.Errorf("ent: uninitialized interceptor (forgotten import ent/runtime?)") + } + if trv, ok := inter.(Traverser); ok { + if err := trv.Traverse(ctx, _q); err != nil { + return err + } + } + } + for _, f := range _q.ctx.Fields { + if !githubinstallation.ValidColumn(f) { + return &ValidationError{Name: f, err: fmt.Errorf("ent: invalid field %q for query", f)} + } + } + if _q.path != nil { + prev, err := _q.path(ctx) + if err != nil { + return err + } + _q.sql = prev + } + return nil +} + +func (_q *GithubInstallationQuery) sqlAll(ctx context.Context, hooks ...queryHook) ([]*GithubInstallation, error) { + var ( + nodes = []*GithubInstallation{} + _spec = _q.querySpec() + ) + _spec.ScanValues = func(columns []string) ([]any, error) { + return (*GithubInstallation).scanValues(nil, columns) + } + _spec.Assign = func(columns []string, values []any) error { + node := &GithubInstallation{config: _q.config} + nodes = append(nodes, node) + return node.assignValues(columns, values) + } + if len(_q.modifiers) > 0 { + _spec.Modifiers = _q.modifiers + } + for i := range hooks { + hooks[i](ctx, _spec) + } + if err := sqlgraph.QueryNodes(ctx, _q.driver, _spec); err != nil { + return nil, err + } + if len(nodes) == 0 { + return nodes, nil + } + return nodes, nil +} + +func (_q *GithubInstallationQuery) sqlCount(ctx context.Context) (int, error) { + _spec := _q.querySpec() + if len(_q.modifiers) > 0 { + _spec.Modifiers = _q.modifiers + } + _spec.Node.Columns = _q.ctx.Fields + if len(_q.ctx.Fields) > 0 { + _spec.Unique = _q.ctx.Unique != nil && *_q.ctx.Unique + } + return sqlgraph.CountNodes(ctx, _q.driver, _spec) +} + +func (_q *GithubInstallationQuery) querySpec() *sqlgraph.QuerySpec { + _spec := sqlgraph.NewQuerySpec(githubinstallation.Table, githubinstallation.Columns, sqlgraph.NewFieldSpec(githubinstallation.FieldID, field.TypeInt64)) + _spec.From = _q.sql + if unique := _q.ctx.Unique; unique != nil { + _spec.Unique = *unique + } else if _q.path != nil { + _spec.Unique = true + } + if fields := _q.ctx.Fields; len(fields) > 0 { + _spec.Node.Columns = make([]string, 0, len(fields)) + _spec.Node.Columns = append(_spec.Node.Columns, githubinstallation.FieldID) + for i := range fields { + if fields[i] != githubinstallation.FieldID { + _spec.Node.Columns = append(_spec.Node.Columns, fields[i]) + } + } + } + if ps := _q.predicates; len(ps) > 0 { + _spec.Predicate = func(selector *sql.Selector) { + for i := range ps { + ps[i](selector) + } + } + } + if limit := _q.ctx.Limit; limit != nil { + _spec.Limit = *limit + } + if offset := _q.ctx.Offset; offset != nil { + _spec.Offset = *offset + } + if ps := _q.order; len(ps) > 0 { + _spec.Order = func(selector *sql.Selector) { + for i := range ps { + ps[i](selector) + } + } + } + return _spec +} + +func (_q *GithubInstallationQuery) sqlQuery(ctx context.Context) *sql.Selector { + builder := sql.Dialect(_q.driver.Dialect()) + t1 := builder.Table(githubinstallation.Table) + columns := _q.ctx.Fields + if len(columns) == 0 { + columns = githubinstallation.Columns + } + selector := builder.Select(t1.Columns(columns...)...).From(t1) + if _q.sql != nil { + selector = _q.sql + selector.Select(selector.Columns(columns...)...) + } + if _q.ctx.Unique != nil && *_q.ctx.Unique { + selector.Distinct() + } + for _, m := range _q.modifiers { + m(selector) + } + for _, p := range _q.predicates { + p(selector) + } + for _, p := range _q.order { + p(selector) + } + if offset := _q.ctx.Offset; offset != nil { + // limit is mandatory for offset clause. We start + // with default value, and override it below if needed. + selector.Offset(*offset).Limit(math.MaxInt32) + } + if limit := _q.ctx.Limit; limit != nil { + selector.Limit(*limit) + } + return selector +} + +// ForUpdate locks the selected rows against concurrent updates, and prevent them from being +// updated, deleted or "selected ... for update" by other sessions, until the transaction is +// either committed or rolled-back. +func (_q *GithubInstallationQuery) ForUpdate(opts ...sql.LockOption) *GithubInstallationQuery { + if _q.driver.Dialect() == dialect.Postgres { + _q.Unique(false) + } + _q.modifiers = append(_q.modifiers, func(s *sql.Selector) { + s.ForUpdate(opts...) + }) + return _q +} + +// ForShare behaves similarly to ForUpdate, except that it acquires a shared mode lock +// on any rows that are read. Other sessions can read the rows, but cannot modify them +// until your transaction commits. +func (_q *GithubInstallationQuery) ForShare(opts ...sql.LockOption) *GithubInstallationQuery { + if _q.driver.Dialect() == dialect.Postgres { + _q.Unique(false) + } + _q.modifiers = append(_q.modifiers, func(s *sql.Selector) { + s.ForShare(opts...) + }) + return _q +} + +// GithubInstallationGroupBy is the group-by builder for GithubInstallation entities. +type GithubInstallationGroupBy struct { + selector + build *GithubInstallationQuery +} + +// Aggregate adds the given aggregation functions to the group-by query. +func (_g *GithubInstallationGroupBy) Aggregate(fns ...AggregateFunc) *GithubInstallationGroupBy { + _g.fns = append(_g.fns, fns...) + return _g +} + +// Scan applies the selector query and scans the result into the given value. +func (_g *GithubInstallationGroupBy) Scan(ctx context.Context, v any) error { + ctx = setContextOp(ctx, _g.build.ctx, ent.OpQueryGroupBy) + if err := _g.build.prepareQuery(ctx); err != nil { + return err + } + return scanWithInterceptors[*GithubInstallationQuery, *GithubInstallationGroupBy](ctx, _g.build, _g, _g.build.inters, v) +} + +func (_g *GithubInstallationGroupBy) sqlScan(ctx context.Context, root *GithubInstallationQuery, v any) error { + selector := root.sqlQuery(ctx).Select() + aggregation := make([]string, 0, len(_g.fns)) + for _, fn := range _g.fns { + aggregation = append(aggregation, fn(selector)) + } + if len(selector.SelectedColumns()) == 0 { + columns := make([]string, 0, len(*_g.flds)+len(_g.fns)) + for _, f := range *_g.flds { + columns = append(columns, selector.C(f)) + } + columns = append(columns, aggregation...) + selector.Select(columns...) + } + selector.GroupBy(selector.Columns(*_g.flds...)...) + if err := selector.Err(); err != nil { + return err + } + rows := &sql.Rows{} + query, args := selector.Query() + if err := _g.build.driver.Query(ctx, query, args, rows); err != nil { + return err + } + defer rows.Close() + return sql.ScanSlice(rows, v) +} + +// GithubInstallationSelect is the builder for selecting fields of GithubInstallation entities. +type GithubInstallationSelect struct { + *GithubInstallationQuery + selector +} + +// Aggregate adds the given aggregation functions to the selector query. +func (_s *GithubInstallationSelect) Aggregate(fns ...AggregateFunc) *GithubInstallationSelect { + _s.fns = append(_s.fns, fns...) + return _s +} + +// Scan applies the selector query and scans the result into the given value. +func (_s *GithubInstallationSelect) Scan(ctx context.Context, v any) error { + ctx = setContextOp(ctx, _s.ctx, ent.OpQuerySelect) + if err := _s.prepareQuery(ctx); err != nil { + return err + } + return scanWithInterceptors[*GithubInstallationQuery, *GithubInstallationSelect](ctx, _s.GithubInstallationQuery, _s, _s.inters, v) +} + +func (_s *GithubInstallationSelect) sqlScan(ctx context.Context, root *GithubInstallationQuery, v any) error { + selector := root.sqlQuery(ctx) + aggregation := make([]string, 0, len(_s.fns)) + for _, fn := range _s.fns { + aggregation = append(aggregation, fn(selector)) + } + switch n := len(*_s.selector.flds); { + case n == 0 && len(aggregation) > 0: + selector.Select(aggregation...) + case n != 0 && len(aggregation) > 0: + selector.AppendSelect(aggregation...) + } + rows := &sql.Rows{} + query, args := selector.Query() + if err := _s.driver.Query(ctx, query, args, rows); err != nil { + return err + } + defer rows.Close() + return sql.ScanSlice(rows, v) +} diff --git a/pkg/ent/githubinstallation_update.go b/pkg/ent/githubinstallation_update.go new file mode 100644 index 000000000..98e8d2941 --- /dev/null +++ b/pkg/ent/githubinstallation_update.go @@ -0,0 +1,428 @@ +// Code generated by ent, DO NOT EDIT. + +package ent + +import ( + "context" + "errors" + "fmt" + "time" + + "entgo.io/ent/dialect/sql" + "entgo.io/ent/dialect/sql/sqlgraph" + "entgo.io/ent/schema/field" + "github.com/GoogleCloudPlatform/scion/pkg/ent/githubinstallation" + "github.com/GoogleCloudPlatform/scion/pkg/ent/predicate" +) + +// GithubInstallationUpdate is the builder for updating GithubInstallation entities. +type GithubInstallationUpdate struct { + config + hooks []Hook + mutation *GithubInstallationMutation +} + +// Where appends a list predicates to the GithubInstallationUpdate builder. +func (_u *GithubInstallationUpdate) Where(ps ...predicate.GithubInstallation) *GithubInstallationUpdate { + _u.mutation.Where(ps...) + return _u +} + +// SetAccountLogin sets the "account_login" field. +func (_u *GithubInstallationUpdate) SetAccountLogin(v string) *GithubInstallationUpdate { + _u.mutation.SetAccountLogin(v) + return _u +} + +// SetNillableAccountLogin sets the "account_login" field if the given value is not nil. +func (_u *GithubInstallationUpdate) SetNillableAccountLogin(v *string) *GithubInstallationUpdate { + if v != nil { + _u.SetAccountLogin(*v) + } + return _u +} + +// SetAccountType sets the "account_type" field. +func (_u *GithubInstallationUpdate) SetAccountType(v string) *GithubInstallationUpdate { + _u.mutation.SetAccountType(v) + return _u +} + +// SetNillableAccountType sets the "account_type" field if the given value is not nil. +func (_u *GithubInstallationUpdate) SetNillableAccountType(v *string) *GithubInstallationUpdate { + if v != nil { + _u.SetAccountType(*v) + } + return _u +} + +// SetAppID sets the "app_id" field. +func (_u *GithubInstallationUpdate) SetAppID(v int64) *GithubInstallationUpdate { + _u.mutation.ResetAppID() + _u.mutation.SetAppID(v) + return _u +} + +// SetNillableAppID sets the "app_id" field if the given value is not nil. +func (_u *GithubInstallationUpdate) SetNillableAppID(v *int64) *GithubInstallationUpdate { + if v != nil { + _u.SetAppID(*v) + } + return _u +} + +// AddAppID adds value to the "app_id" field. +func (_u *GithubInstallationUpdate) AddAppID(v int64) *GithubInstallationUpdate { + _u.mutation.AddAppID(v) + return _u +} + +// SetRepositories sets the "repositories" field. +func (_u *GithubInstallationUpdate) SetRepositories(v string) *GithubInstallationUpdate { + _u.mutation.SetRepositories(v) + return _u +} + +// SetNillableRepositories sets the "repositories" field if the given value is not nil. +func (_u *GithubInstallationUpdate) SetNillableRepositories(v *string) *GithubInstallationUpdate { + if v != nil { + _u.SetRepositories(*v) + } + return _u +} + +// SetStatus sets the "status" field. +func (_u *GithubInstallationUpdate) SetStatus(v string) *GithubInstallationUpdate { + _u.mutation.SetStatus(v) + return _u +} + +// SetNillableStatus sets the "status" field if the given value is not nil. +func (_u *GithubInstallationUpdate) SetNillableStatus(v *string) *GithubInstallationUpdate { + if v != nil { + _u.SetStatus(*v) + } + return _u +} + +// SetUpdated sets the "updated" field. +func (_u *GithubInstallationUpdate) SetUpdated(v time.Time) *GithubInstallationUpdate { + _u.mutation.SetUpdated(v) + return _u +} + +// Mutation returns the GithubInstallationMutation object of the builder. +func (_u *GithubInstallationUpdate) Mutation() *GithubInstallationMutation { + return _u.mutation +} + +// Save executes the query and returns the number of nodes affected by the update operation. +func (_u *GithubInstallationUpdate) Save(ctx context.Context) (int, error) { + _u.defaults() + return withHooks(ctx, _u.sqlSave, _u.mutation, _u.hooks) +} + +// SaveX is like Save, but panics if an error occurs. +func (_u *GithubInstallationUpdate) SaveX(ctx context.Context) int { + affected, err := _u.Save(ctx) + if err != nil { + panic(err) + } + return affected +} + +// Exec executes the query. +func (_u *GithubInstallationUpdate) Exec(ctx context.Context) error { + _, err := _u.Save(ctx) + return err +} + +// ExecX is like Exec, but panics if an error occurs. +func (_u *GithubInstallationUpdate) ExecX(ctx context.Context) { + if err := _u.Exec(ctx); err != nil { + panic(err) + } +} + +// defaults sets the default values of the builder before save. +func (_u *GithubInstallationUpdate) defaults() { + if _, ok := _u.mutation.Updated(); !ok { + v := githubinstallation.UpdateDefaultUpdated() + _u.mutation.SetUpdated(v) + } +} + +// check runs all checks and user-defined validators on the builder. +func (_u *GithubInstallationUpdate) check() error { + if v, ok := _u.mutation.AccountLogin(); ok { + if err := githubinstallation.AccountLoginValidator(v); err != nil { + return &ValidationError{Name: "account_login", err: fmt.Errorf(`ent: validator failed for field "GithubInstallation.account_login": %w`, err)} + } + } + return nil +} + +func (_u *GithubInstallationUpdate) sqlSave(ctx context.Context) (_node int, err error) { + if err := _u.check(); err != nil { + return _node, err + } + _spec := sqlgraph.NewUpdateSpec(githubinstallation.Table, githubinstallation.Columns, sqlgraph.NewFieldSpec(githubinstallation.FieldID, field.TypeInt64)) + if ps := _u.mutation.predicates; len(ps) > 0 { + _spec.Predicate = func(selector *sql.Selector) { + for i := range ps { + ps[i](selector) + } + } + } + if value, ok := _u.mutation.AccountLogin(); ok { + _spec.SetField(githubinstallation.FieldAccountLogin, field.TypeString, value) + } + if value, ok := _u.mutation.AccountType(); ok { + _spec.SetField(githubinstallation.FieldAccountType, field.TypeString, value) + } + if value, ok := _u.mutation.AppID(); ok { + _spec.SetField(githubinstallation.FieldAppID, field.TypeInt64, value) + } + if value, ok := _u.mutation.AddedAppID(); ok { + _spec.AddField(githubinstallation.FieldAppID, field.TypeInt64, value) + } + if value, ok := _u.mutation.Repositories(); ok { + _spec.SetField(githubinstallation.FieldRepositories, field.TypeString, value) + } + if value, ok := _u.mutation.Status(); ok { + _spec.SetField(githubinstallation.FieldStatus, field.TypeString, value) + } + if value, ok := _u.mutation.Updated(); ok { + _spec.SetField(githubinstallation.FieldUpdated, field.TypeTime, value) + } + if _node, err = sqlgraph.UpdateNodes(ctx, _u.driver, _spec); err != nil { + if _, ok := err.(*sqlgraph.NotFoundError); ok { + err = &NotFoundError{githubinstallation.Label} + } else if sqlgraph.IsConstraintError(err) { + err = &ConstraintError{msg: err.Error(), wrap: err} + } + return 0, err + } + _u.mutation.done = true + return _node, nil +} + +// GithubInstallationUpdateOne is the builder for updating a single GithubInstallation entity. +type GithubInstallationUpdateOne struct { + config + fields []string + hooks []Hook + mutation *GithubInstallationMutation +} + +// SetAccountLogin sets the "account_login" field. +func (_u *GithubInstallationUpdateOne) SetAccountLogin(v string) *GithubInstallationUpdateOne { + _u.mutation.SetAccountLogin(v) + return _u +} + +// SetNillableAccountLogin sets the "account_login" field if the given value is not nil. +func (_u *GithubInstallationUpdateOne) SetNillableAccountLogin(v *string) *GithubInstallationUpdateOne { + if v != nil { + _u.SetAccountLogin(*v) + } + return _u +} + +// SetAccountType sets the "account_type" field. +func (_u *GithubInstallationUpdateOne) SetAccountType(v string) *GithubInstallationUpdateOne { + _u.mutation.SetAccountType(v) + return _u +} + +// SetNillableAccountType sets the "account_type" field if the given value is not nil. +func (_u *GithubInstallationUpdateOne) SetNillableAccountType(v *string) *GithubInstallationUpdateOne { + if v != nil { + _u.SetAccountType(*v) + } + return _u +} + +// SetAppID sets the "app_id" field. +func (_u *GithubInstallationUpdateOne) SetAppID(v int64) *GithubInstallationUpdateOne { + _u.mutation.ResetAppID() + _u.mutation.SetAppID(v) + return _u +} + +// SetNillableAppID sets the "app_id" field if the given value is not nil. +func (_u *GithubInstallationUpdateOne) SetNillableAppID(v *int64) *GithubInstallationUpdateOne { + if v != nil { + _u.SetAppID(*v) + } + return _u +} + +// AddAppID adds value to the "app_id" field. +func (_u *GithubInstallationUpdateOne) AddAppID(v int64) *GithubInstallationUpdateOne { + _u.mutation.AddAppID(v) + return _u +} + +// SetRepositories sets the "repositories" field. +func (_u *GithubInstallationUpdateOne) SetRepositories(v string) *GithubInstallationUpdateOne { + _u.mutation.SetRepositories(v) + return _u +} + +// SetNillableRepositories sets the "repositories" field if the given value is not nil. +func (_u *GithubInstallationUpdateOne) SetNillableRepositories(v *string) *GithubInstallationUpdateOne { + if v != nil { + _u.SetRepositories(*v) + } + return _u +} + +// SetStatus sets the "status" field. +func (_u *GithubInstallationUpdateOne) SetStatus(v string) *GithubInstallationUpdateOne { + _u.mutation.SetStatus(v) + return _u +} + +// SetNillableStatus sets the "status" field if the given value is not nil. +func (_u *GithubInstallationUpdateOne) SetNillableStatus(v *string) *GithubInstallationUpdateOne { + if v != nil { + _u.SetStatus(*v) + } + return _u +} + +// SetUpdated sets the "updated" field. +func (_u *GithubInstallationUpdateOne) SetUpdated(v time.Time) *GithubInstallationUpdateOne { + _u.mutation.SetUpdated(v) + return _u +} + +// Mutation returns the GithubInstallationMutation object of the builder. +func (_u *GithubInstallationUpdateOne) Mutation() *GithubInstallationMutation { + return _u.mutation +} + +// Where appends a list predicates to the GithubInstallationUpdate builder. +func (_u *GithubInstallationUpdateOne) Where(ps ...predicate.GithubInstallation) *GithubInstallationUpdateOne { + _u.mutation.Where(ps...) + return _u +} + +// Select allows selecting one or more fields (columns) of the returned entity. +// The default is selecting all fields defined in the entity schema. +func (_u *GithubInstallationUpdateOne) Select(field string, fields ...string) *GithubInstallationUpdateOne { + _u.fields = append([]string{field}, fields...) + return _u +} + +// Save executes the query and returns the updated GithubInstallation entity. +func (_u *GithubInstallationUpdateOne) Save(ctx context.Context) (*GithubInstallation, error) { + _u.defaults() + return withHooks(ctx, _u.sqlSave, _u.mutation, _u.hooks) +} + +// SaveX is like Save, but panics if an error occurs. +func (_u *GithubInstallationUpdateOne) SaveX(ctx context.Context) *GithubInstallation { + node, err := _u.Save(ctx) + if err != nil { + panic(err) + } + return node +} + +// Exec executes the query on the entity. +func (_u *GithubInstallationUpdateOne) Exec(ctx context.Context) error { + _, err := _u.Save(ctx) + return err +} + +// ExecX is like Exec, but panics if an error occurs. +func (_u *GithubInstallationUpdateOne) ExecX(ctx context.Context) { + if err := _u.Exec(ctx); err != nil { + panic(err) + } +} + +// defaults sets the default values of the builder before save. +func (_u *GithubInstallationUpdateOne) defaults() { + if _, ok := _u.mutation.Updated(); !ok { + v := githubinstallation.UpdateDefaultUpdated() + _u.mutation.SetUpdated(v) + } +} + +// check runs all checks and user-defined validators on the builder. +func (_u *GithubInstallationUpdateOne) check() error { + if v, ok := _u.mutation.AccountLogin(); ok { + if err := githubinstallation.AccountLoginValidator(v); err != nil { + return &ValidationError{Name: "account_login", err: fmt.Errorf(`ent: validator failed for field "GithubInstallation.account_login": %w`, err)} + } + } + return nil +} + +func (_u *GithubInstallationUpdateOne) sqlSave(ctx context.Context) (_node *GithubInstallation, err error) { + if err := _u.check(); err != nil { + return _node, err + } + _spec := sqlgraph.NewUpdateSpec(githubinstallation.Table, githubinstallation.Columns, sqlgraph.NewFieldSpec(githubinstallation.FieldID, field.TypeInt64)) + id, ok := _u.mutation.ID() + if !ok { + return nil, &ValidationError{Name: "id", err: errors.New(`ent: missing "GithubInstallation.id" for update`)} + } + _spec.Node.ID.Value = id + if fields := _u.fields; len(fields) > 0 { + _spec.Node.Columns = make([]string, 0, len(fields)) + _spec.Node.Columns = append(_spec.Node.Columns, githubinstallation.FieldID) + for _, f := range fields { + if !githubinstallation.ValidColumn(f) { + return nil, &ValidationError{Name: f, err: fmt.Errorf("ent: invalid field %q for query", f)} + } + if f != githubinstallation.FieldID { + _spec.Node.Columns = append(_spec.Node.Columns, f) + } + } + } + if ps := _u.mutation.predicates; len(ps) > 0 { + _spec.Predicate = func(selector *sql.Selector) { + for i := range ps { + ps[i](selector) + } + } + } + if value, ok := _u.mutation.AccountLogin(); ok { + _spec.SetField(githubinstallation.FieldAccountLogin, field.TypeString, value) + } + if value, ok := _u.mutation.AccountType(); ok { + _spec.SetField(githubinstallation.FieldAccountType, field.TypeString, value) + } + if value, ok := _u.mutation.AppID(); ok { + _spec.SetField(githubinstallation.FieldAppID, field.TypeInt64, value) + } + if value, ok := _u.mutation.AddedAppID(); ok { + _spec.AddField(githubinstallation.FieldAppID, field.TypeInt64, value) + } + if value, ok := _u.mutation.Repositories(); ok { + _spec.SetField(githubinstallation.FieldRepositories, field.TypeString, value) + } + if value, ok := _u.mutation.Status(); ok { + _spec.SetField(githubinstallation.FieldStatus, field.TypeString, value) + } + if value, ok := _u.mutation.Updated(); ok { + _spec.SetField(githubinstallation.FieldUpdated, field.TypeTime, value) + } + _node = &GithubInstallation{config: _u.config} + _spec.Assign = _node.assignValues + _spec.ScanValues = _node.scanValues + if err = sqlgraph.UpdateNode(ctx, _u.driver, _spec); err != nil { + if _, ok := err.(*sqlgraph.NotFoundError); ok { + err = &NotFoundError{githubinstallation.Label} + } else if sqlgraph.IsConstraintError(err) { + err = &ConstraintError{msg: err.Error(), wrap: err} + } + return nil, err + } + _u.mutation.done = true + return _node, nil +} diff --git a/pkg/ent/group_create.go b/pkg/ent/group_create.go index 48fad2925..43ee0d95d 100644 --- a/pkg/ent/group_create.go +++ b/pkg/ent/group_create.go @@ -8,6 +8,8 @@ import ( "fmt" "time" + "entgo.io/ent/dialect" + "entgo.io/ent/dialect/sql" "entgo.io/ent/dialect/sql/sqlgraph" "entgo.io/ent/schema/field" "github.com/GoogleCloudPlatform/scion/pkg/ent/group" @@ -22,6 +24,7 @@ type GroupCreate struct { config mutation *GroupMutation hooks []Hook + conflict []sql.ConflictOption } // SetName sets the "name" field. @@ -341,6 +344,7 @@ func (_c *GroupCreate) createSpec() (*Group, *sqlgraph.CreateSpec) { _node = &Group{config: _c.config} _spec = sqlgraph.NewCreateSpec(group.Table, sqlgraph.NewFieldSpec(group.FieldID, field.TypeUUID)) ) + _spec.OnConflict = _c.conflict if id, ok := _c.mutation.ID(); ok { _node.ID = id _spec.ID.Value = &id @@ -469,11 +473,488 @@ func (_c *GroupCreate) createSpec() (*Group, *sqlgraph.CreateSpec) { return _node, _spec } +// OnConflict allows configuring the `ON CONFLICT` / `ON DUPLICATE KEY` clause +// of the `INSERT` statement. For example: +// +// client.Group.Create(). +// SetName(v). +// OnConflict( +// // Update the row with the new values +// // the was proposed for insertion. +// sql.ResolveWithNewValues(), +// ). +// // Override some of the fields with custom +// // update values. +// Update(func(u *ent.GroupUpsert) { +// SetName(v+v). +// }). +// Exec(ctx) +func (_c *GroupCreate) OnConflict(opts ...sql.ConflictOption) *GroupUpsertOne { + _c.conflict = opts + return &GroupUpsertOne{ + create: _c, + } +} + +// OnConflictColumns calls `OnConflict` and configures the columns +// as conflict target. Using this option is equivalent to using: +// +// client.Group.Create(). +// OnConflict(sql.ConflictColumns(columns...)). +// Exec(ctx) +func (_c *GroupCreate) OnConflictColumns(columns ...string) *GroupUpsertOne { + _c.conflict = append(_c.conflict, sql.ConflictColumns(columns...)) + return &GroupUpsertOne{ + create: _c, + } +} + +type ( + // GroupUpsertOne is the builder for "upsert"-ing + // one Group node. + GroupUpsertOne struct { + create *GroupCreate + } + + // GroupUpsert is the "OnConflict" setter. + GroupUpsert struct { + *sql.UpdateSet + } +) + +// SetName sets the "name" field. +func (u *GroupUpsert) SetName(v string) *GroupUpsert { + u.Set(group.FieldName, v) + return u +} + +// UpdateName sets the "name" field to the value that was provided on create. +func (u *GroupUpsert) UpdateName() *GroupUpsert { + u.SetExcluded(group.FieldName) + return u +} + +// SetSlug sets the "slug" field. +func (u *GroupUpsert) SetSlug(v string) *GroupUpsert { + u.Set(group.FieldSlug, v) + return u +} + +// UpdateSlug sets the "slug" field to the value that was provided on create. +func (u *GroupUpsert) UpdateSlug() *GroupUpsert { + u.SetExcluded(group.FieldSlug) + return u +} + +// SetDescription sets the "description" field. +func (u *GroupUpsert) SetDescription(v string) *GroupUpsert { + u.Set(group.FieldDescription, v) + return u +} + +// UpdateDescription sets the "description" field to the value that was provided on create. +func (u *GroupUpsert) UpdateDescription() *GroupUpsert { + u.SetExcluded(group.FieldDescription) + return u +} + +// ClearDescription clears the value of the "description" field. +func (u *GroupUpsert) ClearDescription() *GroupUpsert { + u.SetNull(group.FieldDescription) + return u +} + +// SetGroupType sets the "group_type" field. +func (u *GroupUpsert) SetGroupType(v group.GroupType) *GroupUpsert { + u.Set(group.FieldGroupType, v) + return u +} + +// UpdateGroupType sets the "group_type" field to the value that was provided on create. +func (u *GroupUpsert) UpdateGroupType() *GroupUpsert { + u.SetExcluded(group.FieldGroupType) + return u +} + +// SetProjectID sets the "project_id" field. +func (u *GroupUpsert) SetProjectID(v uuid.UUID) *GroupUpsert { + u.Set(group.FieldProjectID, v) + return u +} + +// UpdateProjectID sets the "project_id" field to the value that was provided on create. +func (u *GroupUpsert) UpdateProjectID() *GroupUpsert { + u.SetExcluded(group.FieldProjectID) + return u +} + +// ClearProjectID clears the value of the "project_id" field. +func (u *GroupUpsert) ClearProjectID() *GroupUpsert { + u.SetNull(group.FieldProjectID) + return u +} + +// SetLabels sets the "labels" field. +func (u *GroupUpsert) SetLabels(v map[string]string) *GroupUpsert { + u.Set(group.FieldLabels, v) + return u +} + +// UpdateLabels sets the "labels" field to the value that was provided on create. +func (u *GroupUpsert) UpdateLabels() *GroupUpsert { + u.SetExcluded(group.FieldLabels) + return u +} + +// ClearLabels clears the value of the "labels" field. +func (u *GroupUpsert) ClearLabels() *GroupUpsert { + u.SetNull(group.FieldLabels) + return u +} + +// SetAnnotations sets the "annotations" field. +func (u *GroupUpsert) SetAnnotations(v map[string]string) *GroupUpsert { + u.Set(group.FieldAnnotations, v) + return u +} + +// UpdateAnnotations sets the "annotations" field to the value that was provided on create. +func (u *GroupUpsert) UpdateAnnotations() *GroupUpsert { + u.SetExcluded(group.FieldAnnotations) + return u +} + +// ClearAnnotations clears the value of the "annotations" field. +func (u *GroupUpsert) ClearAnnotations() *GroupUpsert { + u.SetNull(group.FieldAnnotations) + return u +} + +// SetUpdated sets the "updated" field. +func (u *GroupUpsert) SetUpdated(v time.Time) *GroupUpsert { + u.Set(group.FieldUpdated, v) + return u +} + +// UpdateUpdated sets the "updated" field to the value that was provided on create. +func (u *GroupUpsert) UpdateUpdated() *GroupUpsert { + u.SetExcluded(group.FieldUpdated) + return u +} + +// SetCreatedBy sets the "created_by" field. +func (u *GroupUpsert) SetCreatedBy(v string) *GroupUpsert { + u.Set(group.FieldCreatedBy, v) + return u +} + +// UpdateCreatedBy sets the "created_by" field to the value that was provided on create. +func (u *GroupUpsert) UpdateCreatedBy() *GroupUpsert { + u.SetExcluded(group.FieldCreatedBy) + return u +} + +// ClearCreatedBy clears the value of the "created_by" field. +func (u *GroupUpsert) ClearCreatedBy() *GroupUpsert { + u.SetNull(group.FieldCreatedBy) + return u +} + +// SetOwnerID sets the "owner_id" field. +func (u *GroupUpsert) SetOwnerID(v uuid.UUID) *GroupUpsert { + u.Set(group.FieldOwnerID, v) + return u +} + +// UpdateOwnerID sets the "owner_id" field to the value that was provided on create. +func (u *GroupUpsert) UpdateOwnerID() *GroupUpsert { + u.SetExcluded(group.FieldOwnerID) + return u +} + +// ClearOwnerID clears the value of the "owner_id" field. +func (u *GroupUpsert) ClearOwnerID() *GroupUpsert { + u.SetNull(group.FieldOwnerID) + return u +} + +// UpdateNewValues updates the mutable fields using the new values that were set on create except the ID field. +// Using this option is equivalent to using: +// +// client.Group.Create(). +// OnConflict( +// sql.ResolveWithNewValues(), +// sql.ResolveWith(func(u *sql.UpdateSet) { +// u.SetIgnore(group.FieldID) +// }), +// ). +// Exec(ctx) +func (u *GroupUpsertOne) UpdateNewValues() *GroupUpsertOne { + u.create.conflict = append(u.create.conflict, sql.ResolveWithNewValues()) + u.create.conflict = append(u.create.conflict, sql.ResolveWith(func(s *sql.UpdateSet) { + if _, exists := u.create.mutation.ID(); exists { + s.SetIgnore(group.FieldID) + } + if _, exists := u.create.mutation.Created(); exists { + s.SetIgnore(group.FieldCreated) + } + })) + return u +} + +// Ignore sets each column to itself in case of conflict. +// Using this option is equivalent to using: +// +// client.Group.Create(). +// OnConflict(sql.ResolveWithIgnore()). +// Exec(ctx) +func (u *GroupUpsertOne) Ignore() *GroupUpsertOne { + u.create.conflict = append(u.create.conflict, sql.ResolveWithIgnore()) + return u +} + +// DoNothing configures the conflict_action to `DO NOTHING`. +// Supported only by SQLite and PostgreSQL. +func (u *GroupUpsertOne) DoNothing() *GroupUpsertOne { + u.create.conflict = append(u.create.conflict, sql.DoNothing()) + return u +} + +// Update allows overriding fields `UPDATE` values. See the GroupCreate.OnConflict +// documentation for more info. +func (u *GroupUpsertOne) Update(set func(*GroupUpsert)) *GroupUpsertOne { + u.create.conflict = append(u.create.conflict, sql.ResolveWith(func(update *sql.UpdateSet) { + set(&GroupUpsert{UpdateSet: update}) + })) + return u +} + +// SetName sets the "name" field. +func (u *GroupUpsertOne) SetName(v string) *GroupUpsertOne { + return u.Update(func(s *GroupUpsert) { + s.SetName(v) + }) +} + +// UpdateName sets the "name" field to the value that was provided on create. +func (u *GroupUpsertOne) UpdateName() *GroupUpsertOne { + return u.Update(func(s *GroupUpsert) { + s.UpdateName() + }) +} + +// SetSlug sets the "slug" field. +func (u *GroupUpsertOne) SetSlug(v string) *GroupUpsertOne { + return u.Update(func(s *GroupUpsert) { + s.SetSlug(v) + }) +} + +// UpdateSlug sets the "slug" field to the value that was provided on create. +func (u *GroupUpsertOne) UpdateSlug() *GroupUpsertOne { + return u.Update(func(s *GroupUpsert) { + s.UpdateSlug() + }) +} + +// SetDescription sets the "description" field. +func (u *GroupUpsertOne) SetDescription(v string) *GroupUpsertOne { + return u.Update(func(s *GroupUpsert) { + s.SetDescription(v) + }) +} + +// UpdateDescription sets the "description" field to the value that was provided on create. +func (u *GroupUpsertOne) UpdateDescription() *GroupUpsertOne { + return u.Update(func(s *GroupUpsert) { + s.UpdateDescription() + }) +} + +// ClearDescription clears the value of the "description" field. +func (u *GroupUpsertOne) ClearDescription() *GroupUpsertOne { + return u.Update(func(s *GroupUpsert) { + s.ClearDescription() + }) +} + +// SetGroupType sets the "group_type" field. +func (u *GroupUpsertOne) SetGroupType(v group.GroupType) *GroupUpsertOne { + return u.Update(func(s *GroupUpsert) { + s.SetGroupType(v) + }) +} + +// UpdateGroupType sets the "group_type" field to the value that was provided on create. +func (u *GroupUpsertOne) UpdateGroupType() *GroupUpsertOne { + return u.Update(func(s *GroupUpsert) { + s.UpdateGroupType() + }) +} + +// SetProjectID sets the "project_id" field. +func (u *GroupUpsertOne) SetProjectID(v uuid.UUID) *GroupUpsertOne { + return u.Update(func(s *GroupUpsert) { + s.SetProjectID(v) + }) +} + +// UpdateProjectID sets the "project_id" field to the value that was provided on create. +func (u *GroupUpsertOne) UpdateProjectID() *GroupUpsertOne { + return u.Update(func(s *GroupUpsert) { + s.UpdateProjectID() + }) +} + +// ClearProjectID clears the value of the "project_id" field. +func (u *GroupUpsertOne) ClearProjectID() *GroupUpsertOne { + return u.Update(func(s *GroupUpsert) { + s.ClearProjectID() + }) +} + +// SetLabels sets the "labels" field. +func (u *GroupUpsertOne) SetLabels(v map[string]string) *GroupUpsertOne { + return u.Update(func(s *GroupUpsert) { + s.SetLabels(v) + }) +} + +// UpdateLabels sets the "labels" field to the value that was provided on create. +func (u *GroupUpsertOne) UpdateLabels() *GroupUpsertOne { + return u.Update(func(s *GroupUpsert) { + s.UpdateLabels() + }) +} + +// ClearLabels clears the value of the "labels" field. +func (u *GroupUpsertOne) ClearLabels() *GroupUpsertOne { + return u.Update(func(s *GroupUpsert) { + s.ClearLabels() + }) +} + +// SetAnnotations sets the "annotations" field. +func (u *GroupUpsertOne) SetAnnotations(v map[string]string) *GroupUpsertOne { + return u.Update(func(s *GroupUpsert) { + s.SetAnnotations(v) + }) +} + +// UpdateAnnotations sets the "annotations" field to the value that was provided on create. +func (u *GroupUpsertOne) UpdateAnnotations() *GroupUpsertOne { + return u.Update(func(s *GroupUpsert) { + s.UpdateAnnotations() + }) +} + +// ClearAnnotations clears the value of the "annotations" field. +func (u *GroupUpsertOne) ClearAnnotations() *GroupUpsertOne { + return u.Update(func(s *GroupUpsert) { + s.ClearAnnotations() + }) +} + +// SetUpdated sets the "updated" field. +func (u *GroupUpsertOne) SetUpdated(v time.Time) *GroupUpsertOne { + return u.Update(func(s *GroupUpsert) { + s.SetUpdated(v) + }) +} + +// UpdateUpdated sets the "updated" field to the value that was provided on create. +func (u *GroupUpsertOne) UpdateUpdated() *GroupUpsertOne { + return u.Update(func(s *GroupUpsert) { + s.UpdateUpdated() + }) +} + +// SetCreatedBy sets the "created_by" field. +func (u *GroupUpsertOne) SetCreatedBy(v string) *GroupUpsertOne { + return u.Update(func(s *GroupUpsert) { + s.SetCreatedBy(v) + }) +} + +// UpdateCreatedBy sets the "created_by" field to the value that was provided on create. +func (u *GroupUpsertOne) UpdateCreatedBy() *GroupUpsertOne { + return u.Update(func(s *GroupUpsert) { + s.UpdateCreatedBy() + }) +} + +// ClearCreatedBy clears the value of the "created_by" field. +func (u *GroupUpsertOne) ClearCreatedBy() *GroupUpsertOne { + return u.Update(func(s *GroupUpsert) { + s.ClearCreatedBy() + }) +} + +// SetOwnerID sets the "owner_id" field. +func (u *GroupUpsertOne) SetOwnerID(v uuid.UUID) *GroupUpsertOne { + return u.Update(func(s *GroupUpsert) { + s.SetOwnerID(v) + }) +} + +// UpdateOwnerID sets the "owner_id" field to the value that was provided on create. +func (u *GroupUpsertOne) UpdateOwnerID() *GroupUpsertOne { + return u.Update(func(s *GroupUpsert) { + s.UpdateOwnerID() + }) +} + +// ClearOwnerID clears the value of the "owner_id" field. +func (u *GroupUpsertOne) ClearOwnerID() *GroupUpsertOne { + return u.Update(func(s *GroupUpsert) { + s.ClearOwnerID() + }) +} + +// Exec executes the query. +func (u *GroupUpsertOne) Exec(ctx context.Context) error { + if len(u.create.conflict) == 0 { + return errors.New("ent: missing options for GroupCreate.OnConflict") + } + return u.create.Exec(ctx) +} + +// ExecX is like Exec, but panics if an error occurs. +func (u *GroupUpsertOne) ExecX(ctx context.Context) { + if err := u.create.Exec(ctx); err != nil { + panic(err) + } +} + +// Exec executes the UPSERT query and returns the inserted/updated ID. +func (u *GroupUpsertOne) ID(ctx context.Context) (id uuid.UUID, err error) { + if u.create.driver.Dialect() == dialect.MySQL { + // In case of "ON CONFLICT", there is no way to get back non-numeric ID + // fields from the database since MySQL does not support the RETURNING clause. + return id, errors.New("ent: GroupUpsertOne.ID is not supported by MySQL driver. Use GroupUpsertOne.Exec instead") + } + node, err := u.create.Save(ctx) + if err != nil { + return id, err + } + return node.ID, nil +} + +// IDX is like ID, but panics if an error occurs. +func (u *GroupUpsertOne) IDX(ctx context.Context) uuid.UUID { + id, err := u.ID(ctx) + if err != nil { + panic(err) + } + return id +} + // GroupCreateBulk is the builder for creating many Group entities in bulk. type GroupCreateBulk struct { config err error builders []*GroupCreate + conflict []sql.ConflictOption } // Save creates the Group entities in the database. @@ -503,6 +984,7 @@ func (_c *GroupCreateBulk) Save(ctx context.Context) ([]*Group, error) { _, err = mutators[i+1].Mutate(root, _c.builders[i+1].mutation) } else { spec := &sqlgraph.BatchCreateSpec{Nodes: specs} + spec.OnConflict = _c.conflict // Invoke the actual operation on the latest mutation in the chain. if err = sqlgraph.BatchCreate(ctx, _c.driver, spec); err != nil { if sqlgraph.IsConstraintError(err) { @@ -552,3 +1034,302 @@ func (_c *GroupCreateBulk) ExecX(ctx context.Context) { panic(err) } } + +// OnConflict allows configuring the `ON CONFLICT` / `ON DUPLICATE KEY` clause +// of the `INSERT` statement. For example: +// +// client.Group.CreateBulk(builders...). +// OnConflict( +// // Update the row with the new values +// // the was proposed for insertion. +// sql.ResolveWithNewValues(), +// ). +// // Override some of the fields with custom +// // update values. +// Update(func(u *ent.GroupUpsert) { +// SetName(v+v). +// }). +// Exec(ctx) +func (_c *GroupCreateBulk) OnConflict(opts ...sql.ConflictOption) *GroupUpsertBulk { + _c.conflict = opts + return &GroupUpsertBulk{ + create: _c, + } +} + +// OnConflictColumns calls `OnConflict` and configures the columns +// as conflict target. Using this option is equivalent to using: +// +// client.Group.Create(). +// OnConflict(sql.ConflictColumns(columns...)). +// Exec(ctx) +func (_c *GroupCreateBulk) OnConflictColumns(columns ...string) *GroupUpsertBulk { + _c.conflict = append(_c.conflict, sql.ConflictColumns(columns...)) + return &GroupUpsertBulk{ + create: _c, + } +} + +// GroupUpsertBulk is the builder for "upsert"-ing +// a bulk of Group nodes. +type GroupUpsertBulk struct { + create *GroupCreateBulk +} + +// UpdateNewValues updates the mutable fields using the new values that +// were set on create. Using this option is equivalent to using: +// +// client.Group.Create(). +// OnConflict( +// sql.ResolveWithNewValues(), +// sql.ResolveWith(func(u *sql.UpdateSet) { +// u.SetIgnore(group.FieldID) +// }), +// ). +// Exec(ctx) +func (u *GroupUpsertBulk) UpdateNewValues() *GroupUpsertBulk { + u.create.conflict = append(u.create.conflict, sql.ResolveWithNewValues()) + u.create.conflict = append(u.create.conflict, sql.ResolveWith(func(s *sql.UpdateSet) { + for _, b := range u.create.builders { + if _, exists := b.mutation.ID(); exists { + s.SetIgnore(group.FieldID) + } + if _, exists := b.mutation.Created(); exists { + s.SetIgnore(group.FieldCreated) + } + } + })) + return u +} + +// Ignore sets each column to itself in case of conflict. +// Using this option is equivalent to using: +// +// client.Group.Create(). +// OnConflict(sql.ResolveWithIgnore()). +// Exec(ctx) +func (u *GroupUpsertBulk) Ignore() *GroupUpsertBulk { + u.create.conflict = append(u.create.conflict, sql.ResolveWithIgnore()) + return u +} + +// DoNothing configures the conflict_action to `DO NOTHING`. +// Supported only by SQLite and PostgreSQL. +func (u *GroupUpsertBulk) DoNothing() *GroupUpsertBulk { + u.create.conflict = append(u.create.conflict, sql.DoNothing()) + return u +} + +// Update allows overriding fields `UPDATE` values. See the GroupCreateBulk.OnConflict +// documentation for more info. +func (u *GroupUpsertBulk) Update(set func(*GroupUpsert)) *GroupUpsertBulk { + u.create.conflict = append(u.create.conflict, sql.ResolveWith(func(update *sql.UpdateSet) { + set(&GroupUpsert{UpdateSet: update}) + })) + return u +} + +// SetName sets the "name" field. +func (u *GroupUpsertBulk) SetName(v string) *GroupUpsertBulk { + return u.Update(func(s *GroupUpsert) { + s.SetName(v) + }) +} + +// UpdateName sets the "name" field to the value that was provided on create. +func (u *GroupUpsertBulk) UpdateName() *GroupUpsertBulk { + return u.Update(func(s *GroupUpsert) { + s.UpdateName() + }) +} + +// SetSlug sets the "slug" field. +func (u *GroupUpsertBulk) SetSlug(v string) *GroupUpsertBulk { + return u.Update(func(s *GroupUpsert) { + s.SetSlug(v) + }) +} + +// UpdateSlug sets the "slug" field to the value that was provided on create. +func (u *GroupUpsertBulk) UpdateSlug() *GroupUpsertBulk { + return u.Update(func(s *GroupUpsert) { + s.UpdateSlug() + }) +} + +// SetDescription sets the "description" field. +func (u *GroupUpsertBulk) SetDescription(v string) *GroupUpsertBulk { + return u.Update(func(s *GroupUpsert) { + s.SetDescription(v) + }) +} + +// UpdateDescription sets the "description" field to the value that was provided on create. +func (u *GroupUpsertBulk) UpdateDescription() *GroupUpsertBulk { + return u.Update(func(s *GroupUpsert) { + s.UpdateDescription() + }) +} + +// ClearDescription clears the value of the "description" field. +func (u *GroupUpsertBulk) ClearDescription() *GroupUpsertBulk { + return u.Update(func(s *GroupUpsert) { + s.ClearDescription() + }) +} + +// SetGroupType sets the "group_type" field. +func (u *GroupUpsertBulk) SetGroupType(v group.GroupType) *GroupUpsertBulk { + return u.Update(func(s *GroupUpsert) { + s.SetGroupType(v) + }) +} + +// UpdateGroupType sets the "group_type" field to the value that was provided on create. +func (u *GroupUpsertBulk) UpdateGroupType() *GroupUpsertBulk { + return u.Update(func(s *GroupUpsert) { + s.UpdateGroupType() + }) +} + +// SetProjectID sets the "project_id" field. +func (u *GroupUpsertBulk) SetProjectID(v uuid.UUID) *GroupUpsertBulk { + return u.Update(func(s *GroupUpsert) { + s.SetProjectID(v) + }) +} + +// UpdateProjectID sets the "project_id" field to the value that was provided on create. +func (u *GroupUpsertBulk) UpdateProjectID() *GroupUpsertBulk { + return u.Update(func(s *GroupUpsert) { + s.UpdateProjectID() + }) +} + +// ClearProjectID clears the value of the "project_id" field. +func (u *GroupUpsertBulk) ClearProjectID() *GroupUpsertBulk { + return u.Update(func(s *GroupUpsert) { + s.ClearProjectID() + }) +} + +// SetLabels sets the "labels" field. +func (u *GroupUpsertBulk) SetLabels(v map[string]string) *GroupUpsertBulk { + return u.Update(func(s *GroupUpsert) { + s.SetLabels(v) + }) +} + +// UpdateLabels sets the "labels" field to the value that was provided on create. +func (u *GroupUpsertBulk) UpdateLabels() *GroupUpsertBulk { + return u.Update(func(s *GroupUpsert) { + s.UpdateLabels() + }) +} + +// ClearLabels clears the value of the "labels" field. +func (u *GroupUpsertBulk) ClearLabels() *GroupUpsertBulk { + return u.Update(func(s *GroupUpsert) { + s.ClearLabels() + }) +} + +// SetAnnotations sets the "annotations" field. +func (u *GroupUpsertBulk) SetAnnotations(v map[string]string) *GroupUpsertBulk { + return u.Update(func(s *GroupUpsert) { + s.SetAnnotations(v) + }) +} + +// UpdateAnnotations sets the "annotations" field to the value that was provided on create. +func (u *GroupUpsertBulk) UpdateAnnotations() *GroupUpsertBulk { + return u.Update(func(s *GroupUpsert) { + s.UpdateAnnotations() + }) +} + +// ClearAnnotations clears the value of the "annotations" field. +func (u *GroupUpsertBulk) ClearAnnotations() *GroupUpsertBulk { + return u.Update(func(s *GroupUpsert) { + s.ClearAnnotations() + }) +} + +// SetUpdated sets the "updated" field. +func (u *GroupUpsertBulk) SetUpdated(v time.Time) *GroupUpsertBulk { + return u.Update(func(s *GroupUpsert) { + s.SetUpdated(v) + }) +} + +// UpdateUpdated sets the "updated" field to the value that was provided on create. +func (u *GroupUpsertBulk) UpdateUpdated() *GroupUpsertBulk { + return u.Update(func(s *GroupUpsert) { + s.UpdateUpdated() + }) +} + +// SetCreatedBy sets the "created_by" field. +func (u *GroupUpsertBulk) SetCreatedBy(v string) *GroupUpsertBulk { + return u.Update(func(s *GroupUpsert) { + s.SetCreatedBy(v) + }) +} + +// UpdateCreatedBy sets the "created_by" field to the value that was provided on create. +func (u *GroupUpsertBulk) UpdateCreatedBy() *GroupUpsertBulk { + return u.Update(func(s *GroupUpsert) { + s.UpdateCreatedBy() + }) +} + +// ClearCreatedBy clears the value of the "created_by" field. +func (u *GroupUpsertBulk) ClearCreatedBy() *GroupUpsertBulk { + return u.Update(func(s *GroupUpsert) { + s.ClearCreatedBy() + }) +} + +// SetOwnerID sets the "owner_id" field. +func (u *GroupUpsertBulk) SetOwnerID(v uuid.UUID) *GroupUpsertBulk { + return u.Update(func(s *GroupUpsert) { + s.SetOwnerID(v) + }) +} + +// UpdateOwnerID sets the "owner_id" field to the value that was provided on create. +func (u *GroupUpsertBulk) UpdateOwnerID() *GroupUpsertBulk { + return u.Update(func(s *GroupUpsert) { + s.UpdateOwnerID() + }) +} + +// ClearOwnerID clears the value of the "owner_id" field. +func (u *GroupUpsertBulk) ClearOwnerID() *GroupUpsertBulk { + return u.Update(func(s *GroupUpsert) { + s.ClearOwnerID() + }) +} + +// Exec executes the query. +func (u *GroupUpsertBulk) Exec(ctx context.Context) error { + if u.create.err != nil { + return u.create.err + } + for i, b := range u.create.builders { + if len(b.conflict) != 0 { + return fmt.Errorf("ent: OnConflict was set for builder %d. Set it on the GroupCreateBulk instead", i) + } + } + if len(u.create.conflict) == 0 { + return errors.New("ent: missing options for GroupCreateBulk.OnConflict") + } + return u.create.Exec(ctx) +} + +// ExecX is like Exec, but panics if an error occurs. +func (u *GroupUpsertBulk) ExecX(ctx context.Context) { + if err := u.create.Exec(ctx); err != nil { + panic(err) + } +} diff --git a/pkg/ent/group_query.go b/pkg/ent/group_query.go index 543950b2b..0a529fa1e 100644 --- a/pkg/ent/group_query.go +++ b/pkg/ent/group_query.go @@ -9,6 +9,7 @@ import ( "math" "entgo.io/ent" + "entgo.io/ent/dialect" "entgo.io/ent/dialect/sql" "entgo.io/ent/dialect/sql/sqlgraph" "entgo.io/ent/schema/field" @@ -32,6 +33,7 @@ type GroupQuery struct { withChildGroups *GroupQuery withOwner *UserQuery withPolicyBindings *PolicyBindingQuery + modifiers []func(*sql.Selector) // intermediate query (i.e. traversal path). sql *sql.Selector path func(context.Context) (*sql.Selector, error) @@ -531,6 +533,9 @@ func (_q *GroupQuery) sqlAll(ctx context.Context, hooks ...queryHook) ([]*Group, node.Edges.loadedTypes = loadedTypes return node.assignValues(columns, values) } + if len(_q.modifiers) > 0 { + _spec.Modifiers = _q.modifiers + } for i := range hooks { hooks[i](ctx, _spec) } @@ -797,6 +802,9 @@ func (_q *GroupQuery) loadPolicyBindings(ctx context.Context, query *PolicyBindi func (_q *GroupQuery) sqlCount(ctx context.Context) (int, error) { _spec := _q.querySpec() + if len(_q.modifiers) > 0 { + _spec.Modifiers = _q.modifiers + } _spec.Node.Columns = _q.ctx.Fields if len(_q.ctx.Fields) > 0 { _spec.Unique = _q.ctx.Unique != nil && *_q.ctx.Unique @@ -862,6 +870,9 @@ func (_q *GroupQuery) sqlQuery(ctx context.Context) *sql.Selector { if _q.ctx.Unique != nil && *_q.ctx.Unique { selector.Distinct() } + for _, m := range _q.modifiers { + m(selector) + } for _, p := range _q.predicates { p(selector) } @@ -879,6 +890,32 @@ func (_q *GroupQuery) sqlQuery(ctx context.Context) *sql.Selector { return selector } +// ForUpdate locks the selected rows against concurrent updates, and prevent them from being +// updated, deleted or "selected ... for update" by other sessions, until the transaction is +// either committed or rolled-back. +func (_q *GroupQuery) ForUpdate(opts ...sql.LockOption) *GroupQuery { + if _q.driver.Dialect() == dialect.Postgres { + _q.Unique(false) + } + _q.modifiers = append(_q.modifiers, func(s *sql.Selector) { + s.ForUpdate(opts...) + }) + return _q +} + +// ForShare behaves similarly to ForUpdate, except that it acquires a shared mode lock +// on any rows that are read. Other sessions can read the rows, but cannot modify them +// until your transaction commits. +func (_q *GroupQuery) ForShare(opts ...sql.LockOption) *GroupQuery { + if _q.driver.Dialect() == dialect.Postgres { + _q.Unique(false) + } + _q.modifiers = append(_q.modifiers, func(s *sql.Selector) { + s.ForShare(opts...) + }) + return _q +} + // GroupGroupBy is the group-by builder for Group entities. type GroupGroupBy struct { selector diff --git a/pkg/ent/groupmembership_create.go b/pkg/ent/groupmembership_create.go index f3d24856d..60b8225b7 100644 --- a/pkg/ent/groupmembership_create.go +++ b/pkg/ent/groupmembership_create.go @@ -8,6 +8,8 @@ import ( "fmt" "time" + "entgo.io/ent/dialect" + "entgo.io/ent/dialect/sql" "entgo.io/ent/dialect/sql/sqlgraph" "entgo.io/ent/schema/field" "github.com/GoogleCloudPlatform/scion/pkg/ent/agent" @@ -22,6 +24,7 @@ type GroupMembershipCreate struct { config mutation *GroupMembershipMutation hooks []Hook + conflict []sql.ConflictOption } // SetRole sets the "role" field. @@ -228,6 +231,7 @@ func (_c *GroupMembershipCreate) createSpec() (*GroupMembership, *sqlgraph.Creat _node = &GroupMembership{config: _c.config} _spec = sqlgraph.NewCreateSpec(groupmembership.Table, sqlgraph.NewFieldSpec(groupmembership.FieldID, field.TypeUUID)) ) + _spec.OnConflict = _c.conflict if id, ok := _c.mutation.ID(); ok { _node.ID = id _spec.ID.Value = &id @@ -298,11 +302,319 @@ func (_c *GroupMembershipCreate) createSpec() (*GroupMembership, *sqlgraph.Creat return _node, _spec } +// OnConflict allows configuring the `ON CONFLICT` / `ON DUPLICATE KEY` clause +// of the `INSERT` statement. For example: +// +// client.GroupMembership.Create(). +// SetRole(v). +// OnConflict( +// // Update the row with the new values +// // the was proposed for insertion. +// sql.ResolveWithNewValues(), +// ). +// // Override some of the fields with custom +// // update values. +// Update(func(u *ent.GroupMembershipUpsert) { +// SetRole(v+v). +// }). +// Exec(ctx) +func (_c *GroupMembershipCreate) OnConflict(opts ...sql.ConflictOption) *GroupMembershipUpsertOne { + _c.conflict = opts + return &GroupMembershipUpsertOne{ + create: _c, + } +} + +// OnConflictColumns calls `OnConflict` and configures the columns +// as conflict target. Using this option is equivalent to using: +// +// client.GroupMembership.Create(). +// OnConflict(sql.ConflictColumns(columns...)). +// Exec(ctx) +func (_c *GroupMembershipCreate) OnConflictColumns(columns ...string) *GroupMembershipUpsertOne { + _c.conflict = append(_c.conflict, sql.ConflictColumns(columns...)) + return &GroupMembershipUpsertOne{ + create: _c, + } +} + +type ( + // GroupMembershipUpsertOne is the builder for "upsert"-ing + // one GroupMembership node. + GroupMembershipUpsertOne struct { + create *GroupMembershipCreate + } + + // GroupMembershipUpsert is the "OnConflict" setter. + GroupMembershipUpsert struct { + *sql.UpdateSet + } +) + +// SetRole sets the "role" field. +func (u *GroupMembershipUpsert) SetRole(v groupmembership.Role) *GroupMembershipUpsert { + u.Set(groupmembership.FieldRole, v) + return u +} + +// UpdateRole sets the "role" field to the value that was provided on create. +func (u *GroupMembershipUpsert) UpdateRole() *GroupMembershipUpsert { + u.SetExcluded(groupmembership.FieldRole) + return u +} + +// SetAddedBy sets the "added_by" field. +func (u *GroupMembershipUpsert) SetAddedBy(v string) *GroupMembershipUpsert { + u.Set(groupmembership.FieldAddedBy, v) + return u +} + +// UpdateAddedBy sets the "added_by" field to the value that was provided on create. +func (u *GroupMembershipUpsert) UpdateAddedBy() *GroupMembershipUpsert { + u.SetExcluded(groupmembership.FieldAddedBy) + return u +} + +// ClearAddedBy clears the value of the "added_by" field. +func (u *GroupMembershipUpsert) ClearAddedBy() *GroupMembershipUpsert { + u.SetNull(groupmembership.FieldAddedBy) + return u +} + +// SetGroupID sets the "group_id" field. +func (u *GroupMembershipUpsert) SetGroupID(v uuid.UUID) *GroupMembershipUpsert { + u.Set(groupmembership.FieldGroupID, v) + return u +} + +// UpdateGroupID sets the "group_id" field to the value that was provided on create. +func (u *GroupMembershipUpsert) UpdateGroupID() *GroupMembershipUpsert { + u.SetExcluded(groupmembership.FieldGroupID) + return u +} + +// SetUserID sets the "user_id" field. +func (u *GroupMembershipUpsert) SetUserID(v uuid.UUID) *GroupMembershipUpsert { + u.Set(groupmembership.FieldUserID, v) + return u +} + +// UpdateUserID sets the "user_id" field to the value that was provided on create. +func (u *GroupMembershipUpsert) UpdateUserID() *GroupMembershipUpsert { + u.SetExcluded(groupmembership.FieldUserID) + return u +} + +// ClearUserID clears the value of the "user_id" field. +func (u *GroupMembershipUpsert) ClearUserID() *GroupMembershipUpsert { + u.SetNull(groupmembership.FieldUserID) + return u +} + +// SetAgentID sets the "agent_id" field. +func (u *GroupMembershipUpsert) SetAgentID(v uuid.UUID) *GroupMembershipUpsert { + u.Set(groupmembership.FieldAgentID, v) + return u +} + +// UpdateAgentID sets the "agent_id" field to the value that was provided on create. +func (u *GroupMembershipUpsert) UpdateAgentID() *GroupMembershipUpsert { + u.SetExcluded(groupmembership.FieldAgentID) + return u +} + +// ClearAgentID clears the value of the "agent_id" field. +func (u *GroupMembershipUpsert) ClearAgentID() *GroupMembershipUpsert { + u.SetNull(groupmembership.FieldAgentID) + return u +} + +// UpdateNewValues updates the mutable fields using the new values that were set on create except the ID field. +// Using this option is equivalent to using: +// +// client.GroupMembership.Create(). +// OnConflict( +// sql.ResolveWithNewValues(), +// sql.ResolveWith(func(u *sql.UpdateSet) { +// u.SetIgnore(groupmembership.FieldID) +// }), +// ). +// Exec(ctx) +func (u *GroupMembershipUpsertOne) UpdateNewValues() *GroupMembershipUpsertOne { + u.create.conflict = append(u.create.conflict, sql.ResolveWithNewValues()) + u.create.conflict = append(u.create.conflict, sql.ResolveWith(func(s *sql.UpdateSet) { + if _, exists := u.create.mutation.ID(); exists { + s.SetIgnore(groupmembership.FieldID) + } + if _, exists := u.create.mutation.AddedAt(); exists { + s.SetIgnore(groupmembership.FieldAddedAt) + } + })) + return u +} + +// Ignore sets each column to itself in case of conflict. +// Using this option is equivalent to using: +// +// client.GroupMembership.Create(). +// OnConflict(sql.ResolveWithIgnore()). +// Exec(ctx) +func (u *GroupMembershipUpsertOne) Ignore() *GroupMembershipUpsertOne { + u.create.conflict = append(u.create.conflict, sql.ResolveWithIgnore()) + return u +} + +// DoNothing configures the conflict_action to `DO NOTHING`. +// Supported only by SQLite and PostgreSQL. +func (u *GroupMembershipUpsertOne) DoNothing() *GroupMembershipUpsertOne { + u.create.conflict = append(u.create.conflict, sql.DoNothing()) + return u +} + +// Update allows overriding fields `UPDATE` values. See the GroupMembershipCreate.OnConflict +// documentation for more info. +func (u *GroupMembershipUpsertOne) Update(set func(*GroupMembershipUpsert)) *GroupMembershipUpsertOne { + u.create.conflict = append(u.create.conflict, sql.ResolveWith(func(update *sql.UpdateSet) { + set(&GroupMembershipUpsert{UpdateSet: update}) + })) + return u +} + +// SetRole sets the "role" field. +func (u *GroupMembershipUpsertOne) SetRole(v groupmembership.Role) *GroupMembershipUpsertOne { + return u.Update(func(s *GroupMembershipUpsert) { + s.SetRole(v) + }) +} + +// UpdateRole sets the "role" field to the value that was provided on create. +func (u *GroupMembershipUpsertOne) UpdateRole() *GroupMembershipUpsertOne { + return u.Update(func(s *GroupMembershipUpsert) { + s.UpdateRole() + }) +} + +// SetAddedBy sets the "added_by" field. +func (u *GroupMembershipUpsertOne) SetAddedBy(v string) *GroupMembershipUpsertOne { + return u.Update(func(s *GroupMembershipUpsert) { + s.SetAddedBy(v) + }) +} + +// UpdateAddedBy sets the "added_by" field to the value that was provided on create. +func (u *GroupMembershipUpsertOne) UpdateAddedBy() *GroupMembershipUpsertOne { + return u.Update(func(s *GroupMembershipUpsert) { + s.UpdateAddedBy() + }) +} + +// ClearAddedBy clears the value of the "added_by" field. +func (u *GroupMembershipUpsertOne) ClearAddedBy() *GroupMembershipUpsertOne { + return u.Update(func(s *GroupMembershipUpsert) { + s.ClearAddedBy() + }) +} + +// SetGroupID sets the "group_id" field. +func (u *GroupMembershipUpsertOne) SetGroupID(v uuid.UUID) *GroupMembershipUpsertOne { + return u.Update(func(s *GroupMembershipUpsert) { + s.SetGroupID(v) + }) +} + +// UpdateGroupID sets the "group_id" field to the value that was provided on create. +func (u *GroupMembershipUpsertOne) UpdateGroupID() *GroupMembershipUpsertOne { + return u.Update(func(s *GroupMembershipUpsert) { + s.UpdateGroupID() + }) +} + +// SetUserID sets the "user_id" field. +func (u *GroupMembershipUpsertOne) SetUserID(v uuid.UUID) *GroupMembershipUpsertOne { + return u.Update(func(s *GroupMembershipUpsert) { + s.SetUserID(v) + }) +} + +// UpdateUserID sets the "user_id" field to the value that was provided on create. +func (u *GroupMembershipUpsertOne) UpdateUserID() *GroupMembershipUpsertOne { + return u.Update(func(s *GroupMembershipUpsert) { + s.UpdateUserID() + }) +} + +// ClearUserID clears the value of the "user_id" field. +func (u *GroupMembershipUpsertOne) ClearUserID() *GroupMembershipUpsertOne { + return u.Update(func(s *GroupMembershipUpsert) { + s.ClearUserID() + }) +} + +// SetAgentID sets the "agent_id" field. +func (u *GroupMembershipUpsertOne) SetAgentID(v uuid.UUID) *GroupMembershipUpsertOne { + return u.Update(func(s *GroupMembershipUpsert) { + s.SetAgentID(v) + }) +} + +// UpdateAgentID sets the "agent_id" field to the value that was provided on create. +func (u *GroupMembershipUpsertOne) UpdateAgentID() *GroupMembershipUpsertOne { + return u.Update(func(s *GroupMembershipUpsert) { + s.UpdateAgentID() + }) +} + +// ClearAgentID clears the value of the "agent_id" field. +func (u *GroupMembershipUpsertOne) ClearAgentID() *GroupMembershipUpsertOne { + return u.Update(func(s *GroupMembershipUpsert) { + s.ClearAgentID() + }) +} + +// Exec executes the query. +func (u *GroupMembershipUpsertOne) Exec(ctx context.Context) error { + if len(u.create.conflict) == 0 { + return errors.New("ent: missing options for GroupMembershipCreate.OnConflict") + } + return u.create.Exec(ctx) +} + +// ExecX is like Exec, but panics if an error occurs. +func (u *GroupMembershipUpsertOne) ExecX(ctx context.Context) { + if err := u.create.Exec(ctx); err != nil { + panic(err) + } +} + +// Exec executes the UPSERT query and returns the inserted/updated ID. +func (u *GroupMembershipUpsertOne) ID(ctx context.Context) (id uuid.UUID, err error) { + if u.create.driver.Dialect() == dialect.MySQL { + // In case of "ON CONFLICT", there is no way to get back non-numeric ID + // fields from the database since MySQL does not support the RETURNING clause. + return id, errors.New("ent: GroupMembershipUpsertOne.ID is not supported by MySQL driver. Use GroupMembershipUpsertOne.Exec instead") + } + node, err := u.create.Save(ctx) + if err != nil { + return id, err + } + return node.ID, nil +} + +// IDX is like ID, but panics if an error occurs. +func (u *GroupMembershipUpsertOne) IDX(ctx context.Context) uuid.UUID { + id, err := u.ID(ctx) + if err != nil { + panic(err) + } + return id +} + // GroupMembershipCreateBulk is the builder for creating many GroupMembership entities in bulk. type GroupMembershipCreateBulk struct { config err error builders []*GroupMembershipCreate + conflict []sql.ConflictOption } // Save creates the GroupMembership entities in the database. @@ -332,6 +644,7 @@ func (_c *GroupMembershipCreateBulk) Save(ctx context.Context) ([]*GroupMembersh _, err = mutators[i+1].Mutate(root, _c.builders[i+1].mutation) } else { spec := &sqlgraph.BatchCreateSpec{Nodes: specs} + spec.OnConflict = _c.conflict // Invoke the actual operation on the latest mutation in the chain. if err = sqlgraph.BatchCreate(ctx, _c.driver, spec); err != nil { if sqlgraph.IsConstraintError(err) { @@ -381,3 +694,211 @@ func (_c *GroupMembershipCreateBulk) ExecX(ctx context.Context) { panic(err) } } + +// OnConflict allows configuring the `ON CONFLICT` / `ON DUPLICATE KEY` clause +// of the `INSERT` statement. For example: +// +// client.GroupMembership.CreateBulk(builders...). +// OnConflict( +// // Update the row with the new values +// // the was proposed for insertion. +// sql.ResolveWithNewValues(), +// ). +// // Override some of the fields with custom +// // update values. +// Update(func(u *ent.GroupMembershipUpsert) { +// SetRole(v+v). +// }). +// Exec(ctx) +func (_c *GroupMembershipCreateBulk) OnConflict(opts ...sql.ConflictOption) *GroupMembershipUpsertBulk { + _c.conflict = opts + return &GroupMembershipUpsertBulk{ + create: _c, + } +} + +// OnConflictColumns calls `OnConflict` and configures the columns +// as conflict target. Using this option is equivalent to using: +// +// client.GroupMembership.Create(). +// OnConflict(sql.ConflictColumns(columns...)). +// Exec(ctx) +func (_c *GroupMembershipCreateBulk) OnConflictColumns(columns ...string) *GroupMembershipUpsertBulk { + _c.conflict = append(_c.conflict, sql.ConflictColumns(columns...)) + return &GroupMembershipUpsertBulk{ + create: _c, + } +} + +// GroupMembershipUpsertBulk is the builder for "upsert"-ing +// a bulk of GroupMembership nodes. +type GroupMembershipUpsertBulk struct { + create *GroupMembershipCreateBulk +} + +// UpdateNewValues updates the mutable fields using the new values that +// were set on create. Using this option is equivalent to using: +// +// client.GroupMembership.Create(). +// OnConflict( +// sql.ResolveWithNewValues(), +// sql.ResolveWith(func(u *sql.UpdateSet) { +// u.SetIgnore(groupmembership.FieldID) +// }), +// ). +// Exec(ctx) +func (u *GroupMembershipUpsertBulk) UpdateNewValues() *GroupMembershipUpsertBulk { + u.create.conflict = append(u.create.conflict, sql.ResolveWithNewValues()) + u.create.conflict = append(u.create.conflict, sql.ResolveWith(func(s *sql.UpdateSet) { + for _, b := range u.create.builders { + if _, exists := b.mutation.ID(); exists { + s.SetIgnore(groupmembership.FieldID) + } + if _, exists := b.mutation.AddedAt(); exists { + s.SetIgnore(groupmembership.FieldAddedAt) + } + } + })) + return u +} + +// Ignore sets each column to itself in case of conflict. +// Using this option is equivalent to using: +// +// client.GroupMembership.Create(). +// OnConflict(sql.ResolveWithIgnore()). +// Exec(ctx) +func (u *GroupMembershipUpsertBulk) Ignore() *GroupMembershipUpsertBulk { + u.create.conflict = append(u.create.conflict, sql.ResolveWithIgnore()) + return u +} + +// DoNothing configures the conflict_action to `DO NOTHING`. +// Supported only by SQLite and PostgreSQL. +func (u *GroupMembershipUpsertBulk) DoNothing() *GroupMembershipUpsertBulk { + u.create.conflict = append(u.create.conflict, sql.DoNothing()) + return u +} + +// Update allows overriding fields `UPDATE` values. See the GroupMembershipCreateBulk.OnConflict +// documentation for more info. +func (u *GroupMembershipUpsertBulk) Update(set func(*GroupMembershipUpsert)) *GroupMembershipUpsertBulk { + u.create.conflict = append(u.create.conflict, sql.ResolveWith(func(update *sql.UpdateSet) { + set(&GroupMembershipUpsert{UpdateSet: update}) + })) + return u +} + +// SetRole sets the "role" field. +func (u *GroupMembershipUpsertBulk) SetRole(v groupmembership.Role) *GroupMembershipUpsertBulk { + return u.Update(func(s *GroupMembershipUpsert) { + s.SetRole(v) + }) +} + +// UpdateRole sets the "role" field to the value that was provided on create. +func (u *GroupMembershipUpsertBulk) UpdateRole() *GroupMembershipUpsertBulk { + return u.Update(func(s *GroupMembershipUpsert) { + s.UpdateRole() + }) +} + +// SetAddedBy sets the "added_by" field. +func (u *GroupMembershipUpsertBulk) SetAddedBy(v string) *GroupMembershipUpsertBulk { + return u.Update(func(s *GroupMembershipUpsert) { + s.SetAddedBy(v) + }) +} + +// UpdateAddedBy sets the "added_by" field to the value that was provided on create. +func (u *GroupMembershipUpsertBulk) UpdateAddedBy() *GroupMembershipUpsertBulk { + return u.Update(func(s *GroupMembershipUpsert) { + s.UpdateAddedBy() + }) +} + +// ClearAddedBy clears the value of the "added_by" field. +func (u *GroupMembershipUpsertBulk) ClearAddedBy() *GroupMembershipUpsertBulk { + return u.Update(func(s *GroupMembershipUpsert) { + s.ClearAddedBy() + }) +} + +// SetGroupID sets the "group_id" field. +func (u *GroupMembershipUpsertBulk) SetGroupID(v uuid.UUID) *GroupMembershipUpsertBulk { + return u.Update(func(s *GroupMembershipUpsert) { + s.SetGroupID(v) + }) +} + +// UpdateGroupID sets the "group_id" field to the value that was provided on create. +func (u *GroupMembershipUpsertBulk) UpdateGroupID() *GroupMembershipUpsertBulk { + return u.Update(func(s *GroupMembershipUpsert) { + s.UpdateGroupID() + }) +} + +// SetUserID sets the "user_id" field. +func (u *GroupMembershipUpsertBulk) SetUserID(v uuid.UUID) *GroupMembershipUpsertBulk { + return u.Update(func(s *GroupMembershipUpsert) { + s.SetUserID(v) + }) +} + +// UpdateUserID sets the "user_id" field to the value that was provided on create. +func (u *GroupMembershipUpsertBulk) UpdateUserID() *GroupMembershipUpsertBulk { + return u.Update(func(s *GroupMembershipUpsert) { + s.UpdateUserID() + }) +} + +// ClearUserID clears the value of the "user_id" field. +func (u *GroupMembershipUpsertBulk) ClearUserID() *GroupMembershipUpsertBulk { + return u.Update(func(s *GroupMembershipUpsert) { + s.ClearUserID() + }) +} + +// SetAgentID sets the "agent_id" field. +func (u *GroupMembershipUpsertBulk) SetAgentID(v uuid.UUID) *GroupMembershipUpsertBulk { + return u.Update(func(s *GroupMembershipUpsert) { + s.SetAgentID(v) + }) +} + +// UpdateAgentID sets the "agent_id" field to the value that was provided on create. +func (u *GroupMembershipUpsertBulk) UpdateAgentID() *GroupMembershipUpsertBulk { + return u.Update(func(s *GroupMembershipUpsert) { + s.UpdateAgentID() + }) +} + +// ClearAgentID clears the value of the "agent_id" field. +func (u *GroupMembershipUpsertBulk) ClearAgentID() *GroupMembershipUpsertBulk { + return u.Update(func(s *GroupMembershipUpsert) { + s.ClearAgentID() + }) +} + +// Exec executes the query. +func (u *GroupMembershipUpsertBulk) Exec(ctx context.Context) error { + if u.create.err != nil { + return u.create.err + } + for i, b := range u.create.builders { + if len(b.conflict) != 0 { + return fmt.Errorf("ent: OnConflict was set for builder %d. Set it on the GroupMembershipCreateBulk instead", i) + } + } + if len(u.create.conflict) == 0 { + return errors.New("ent: missing options for GroupMembershipCreateBulk.OnConflict") + } + return u.create.Exec(ctx) +} + +// ExecX is like Exec, but panics if an error occurs. +func (u *GroupMembershipUpsertBulk) ExecX(ctx context.Context) { + if err := u.create.Exec(ctx); err != nil { + panic(err) + } +} diff --git a/pkg/ent/groupmembership_query.go b/pkg/ent/groupmembership_query.go index 5884f9f86..b913d78e2 100644 --- a/pkg/ent/groupmembership_query.go +++ b/pkg/ent/groupmembership_query.go @@ -8,6 +8,7 @@ import ( "math" "entgo.io/ent" + "entgo.io/ent/dialect" "entgo.io/ent/dialect/sql" "entgo.io/ent/dialect/sql/sqlgraph" "entgo.io/ent/schema/field" @@ -29,6 +30,7 @@ type GroupMembershipQuery struct { withGroup *GroupQuery withUser *UserQuery withAgent *AgentQuery + modifiers []func(*sql.Selector) // intermediate query (i.e. traversal path). sql *sql.Selector path func(context.Context) (*sql.Selector, error) @@ -458,6 +460,9 @@ func (_q *GroupMembershipQuery) sqlAll(ctx context.Context, hooks ...queryHook) node.Edges.loadedTypes = loadedTypes return node.assignValues(columns, values) } + if len(_q.modifiers) > 0 { + _spec.Modifiers = _q.modifiers + } for i := range hooks { hooks[i](ctx, _spec) } @@ -584,6 +589,9 @@ func (_q *GroupMembershipQuery) loadAgent(ctx context.Context, query *AgentQuery func (_q *GroupMembershipQuery) sqlCount(ctx context.Context) (int, error) { _spec := _q.querySpec() + if len(_q.modifiers) > 0 { + _spec.Modifiers = _q.modifiers + } _spec.Node.Columns = _q.ctx.Fields if len(_q.ctx.Fields) > 0 { _spec.Unique = _q.ctx.Unique != nil && *_q.ctx.Unique @@ -655,6 +663,9 @@ func (_q *GroupMembershipQuery) sqlQuery(ctx context.Context) *sql.Selector { if _q.ctx.Unique != nil && *_q.ctx.Unique { selector.Distinct() } + for _, m := range _q.modifiers { + m(selector) + } for _, p := range _q.predicates { p(selector) } @@ -672,6 +683,32 @@ func (_q *GroupMembershipQuery) sqlQuery(ctx context.Context) *sql.Selector { return selector } +// ForUpdate locks the selected rows against concurrent updates, and prevent them from being +// updated, deleted or "selected ... for update" by other sessions, until the transaction is +// either committed or rolled-back. +func (_q *GroupMembershipQuery) ForUpdate(opts ...sql.LockOption) *GroupMembershipQuery { + if _q.driver.Dialect() == dialect.Postgres { + _q.Unique(false) + } + _q.modifiers = append(_q.modifiers, func(s *sql.Selector) { + s.ForUpdate(opts...) + }) + return _q +} + +// ForShare behaves similarly to ForUpdate, except that it acquires a shared mode lock +// on any rows that are read. Other sessions can read the rows, but cannot modify them +// until your transaction commits. +func (_q *GroupMembershipQuery) ForShare(opts ...sql.LockOption) *GroupMembershipQuery { + if _q.driver.Dialect() == dialect.Postgres { + _q.Unique(false) + } + _q.modifiers = append(_q.modifiers, func(s *sql.Selector) { + s.ForShare(opts...) + }) + return _q +} + // GroupMembershipGroupBy is the group-by builder for GroupMembership entities. type GroupMembershipGroupBy struct { selector diff --git a/pkg/ent/harnessconfig.go b/pkg/ent/harnessconfig.go new file mode 100644 index 000000000..05ab07115 --- /dev/null +++ b/pkg/ent/harnessconfig.go @@ -0,0 +1,316 @@ +// Code generated by ent, DO NOT EDIT. + +package ent + +import ( + "fmt" + "strings" + "time" + + "entgo.io/ent" + "entgo.io/ent/dialect/sql" + "github.com/GoogleCloudPlatform/scion/pkg/ent/harnessconfig" + "github.com/google/uuid" +) + +// HarnessConfig is the model entity for the HarnessConfig schema. +type HarnessConfig struct { + config `json:"-"` + // ID of the ent. + ID uuid.UUID `json:"id,omitempty"` + // Name holds the value of the "name" field. + Name string `json:"name,omitempty"` + // Slug holds the value of the "slug" field. + Slug string `json:"slug,omitempty"` + // DisplayName holds the value of the "display_name" field. + DisplayName string `json:"display_name,omitempty"` + // Description holds the value of the "description" field. + Description string `json:"description,omitempty"` + // Harness holds the value of the "harness" field. + Harness string `json:"harness,omitempty"` + // Config holds the value of the "config" field. + Config string `json:"config,omitempty"` + // ContentHash holds the value of the "content_hash" field. + ContentHash string `json:"content_hash,omitempty"` + // Scope holds the value of the "scope" field. + Scope string `json:"scope,omitempty"` + // ScopeID holds the value of the "scope_id" field. + ScopeID string `json:"scope_id,omitempty"` + // StorageURI holds the value of the "storage_uri" field. + StorageURI string `json:"storage_uri,omitempty"` + // StorageBucket holds the value of the "storage_bucket" field. + StorageBucket string `json:"storage_bucket,omitempty"` + // StoragePath holds the value of the "storage_path" field. + StoragePath string `json:"storage_path,omitempty"` + // Files holds the value of the "files" field. + Files string `json:"files,omitempty"` + // Status holds the value of the "status" field. + Status harnessconfig.Status `json:"status,omitempty"` + // OwnerID holds the value of the "owner_id" field. + OwnerID string `json:"owner_id,omitempty"` + // CreatedBy holds the value of the "created_by" field. + CreatedBy string `json:"created_by,omitempty"` + // UpdatedBy holds the value of the "updated_by" field. + UpdatedBy string `json:"updated_by,omitempty"` + // Visibility holds the value of the "visibility" field. + Visibility string `json:"visibility,omitempty"` + // Created holds the value of the "created" field. + Created time.Time `json:"created,omitempty"` + // Updated holds the value of the "updated" field. + Updated time.Time `json:"updated,omitempty"` + selectValues sql.SelectValues +} + +// scanValues returns the types for scanning values from sql.Rows. +func (*HarnessConfig) scanValues(columns []string) ([]any, error) { + values := make([]any, len(columns)) + for i := range columns { + switch columns[i] { + case harnessconfig.FieldName, harnessconfig.FieldSlug, harnessconfig.FieldDisplayName, harnessconfig.FieldDescription, harnessconfig.FieldHarness, harnessconfig.FieldConfig, harnessconfig.FieldContentHash, harnessconfig.FieldScope, harnessconfig.FieldScopeID, harnessconfig.FieldStorageURI, harnessconfig.FieldStorageBucket, harnessconfig.FieldStoragePath, harnessconfig.FieldFiles, harnessconfig.FieldStatus, harnessconfig.FieldOwnerID, harnessconfig.FieldCreatedBy, harnessconfig.FieldUpdatedBy, harnessconfig.FieldVisibility: + values[i] = new(sql.NullString) + case harnessconfig.FieldCreated, harnessconfig.FieldUpdated: + values[i] = new(sql.NullTime) + case harnessconfig.FieldID: + values[i] = new(uuid.UUID) + default: + values[i] = new(sql.UnknownType) + } + } + return values, nil +} + +// assignValues assigns the values that were returned from sql.Rows (after scanning) +// to the HarnessConfig fields. +func (_m *HarnessConfig) assignValues(columns []string, values []any) error { + if m, n := len(values), len(columns); m < n { + return fmt.Errorf("mismatch number of scan values: %d != %d", m, n) + } + for i := range columns { + switch columns[i] { + case harnessconfig.FieldID: + if value, ok := values[i].(*uuid.UUID); !ok { + return fmt.Errorf("unexpected type %T for field id", values[i]) + } else if value != nil { + _m.ID = *value + } + case harnessconfig.FieldName: + if value, ok := values[i].(*sql.NullString); !ok { + return fmt.Errorf("unexpected type %T for field name", values[i]) + } else if value.Valid { + _m.Name = value.String + } + case harnessconfig.FieldSlug: + if value, ok := values[i].(*sql.NullString); !ok { + return fmt.Errorf("unexpected type %T for field slug", values[i]) + } else if value.Valid { + _m.Slug = value.String + } + case harnessconfig.FieldDisplayName: + if value, ok := values[i].(*sql.NullString); !ok { + return fmt.Errorf("unexpected type %T for field display_name", values[i]) + } else if value.Valid { + _m.DisplayName = value.String + } + case harnessconfig.FieldDescription: + if value, ok := values[i].(*sql.NullString); !ok { + return fmt.Errorf("unexpected type %T for field description", values[i]) + } else if value.Valid { + _m.Description = value.String + } + case harnessconfig.FieldHarness: + if value, ok := values[i].(*sql.NullString); !ok { + return fmt.Errorf("unexpected type %T for field harness", values[i]) + } else if value.Valid { + _m.Harness = value.String + } + case harnessconfig.FieldConfig: + if value, ok := values[i].(*sql.NullString); !ok { + return fmt.Errorf("unexpected type %T for field config", values[i]) + } else if value.Valid { + _m.Config = value.String + } + case harnessconfig.FieldContentHash: + if value, ok := values[i].(*sql.NullString); !ok { + return fmt.Errorf("unexpected type %T for field content_hash", values[i]) + } else if value.Valid { + _m.ContentHash = value.String + } + case harnessconfig.FieldScope: + if value, ok := values[i].(*sql.NullString); !ok { + return fmt.Errorf("unexpected type %T for field scope", values[i]) + } else if value.Valid { + _m.Scope = value.String + } + case harnessconfig.FieldScopeID: + if value, ok := values[i].(*sql.NullString); !ok { + return fmt.Errorf("unexpected type %T for field scope_id", values[i]) + } else if value.Valid { + _m.ScopeID = value.String + } + case harnessconfig.FieldStorageURI: + if value, ok := values[i].(*sql.NullString); !ok { + return fmt.Errorf("unexpected type %T for field storage_uri", values[i]) + } else if value.Valid { + _m.StorageURI = value.String + } + case harnessconfig.FieldStorageBucket: + if value, ok := values[i].(*sql.NullString); !ok { + return fmt.Errorf("unexpected type %T for field storage_bucket", values[i]) + } else if value.Valid { + _m.StorageBucket = value.String + } + case harnessconfig.FieldStoragePath: + if value, ok := values[i].(*sql.NullString); !ok { + return fmt.Errorf("unexpected type %T for field storage_path", values[i]) + } else if value.Valid { + _m.StoragePath = value.String + } + case harnessconfig.FieldFiles: + if value, ok := values[i].(*sql.NullString); !ok { + return fmt.Errorf("unexpected type %T for field files", values[i]) + } else if value.Valid { + _m.Files = value.String + } + case harnessconfig.FieldStatus: + if value, ok := values[i].(*sql.NullString); !ok { + return fmt.Errorf("unexpected type %T for field status", values[i]) + } else if value.Valid { + _m.Status = harnessconfig.Status(value.String) + } + case harnessconfig.FieldOwnerID: + if value, ok := values[i].(*sql.NullString); !ok { + return fmt.Errorf("unexpected type %T for field owner_id", values[i]) + } else if value.Valid { + _m.OwnerID = value.String + } + case harnessconfig.FieldCreatedBy: + if value, ok := values[i].(*sql.NullString); !ok { + return fmt.Errorf("unexpected type %T for field created_by", values[i]) + } else if value.Valid { + _m.CreatedBy = value.String + } + case harnessconfig.FieldUpdatedBy: + if value, ok := values[i].(*sql.NullString); !ok { + return fmt.Errorf("unexpected type %T for field updated_by", values[i]) + } else if value.Valid { + _m.UpdatedBy = value.String + } + case harnessconfig.FieldVisibility: + if value, ok := values[i].(*sql.NullString); !ok { + return fmt.Errorf("unexpected type %T for field visibility", values[i]) + } else if value.Valid { + _m.Visibility = value.String + } + case harnessconfig.FieldCreated: + if value, ok := values[i].(*sql.NullTime); !ok { + return fmt.Errorf("unexpected type %T for field created", values[i]) + } else if value.Valid { + _m.Created = value.Time + } + case harnessconfig.FieldUpdated: + if value, ok := values[i].(*sql.NullTime); !ok { + return fmt.Errorf("unexpected type %T for field updated", values[i]) + } else if value.Valid { + _m.Updated = value.Time + } + default: + _m.selectValues.Set(columns[i], values[i]) + } + } + return nil +} + +// Value returns the ent.Value that was dynamically selected and assigned to the HarnessConfig. +// This includes values selected through modifiers, order, etc. +func (_m *HarnessConfig) Value(name string) (ent.Value, error) { + return _m.selectValues.Get(name) +} + +// Update returns a builder for updating this HarnessConfig. +// Note that you need to call HarnessConfig.Unwrap() before calling this method if this HarnessConfig +// was returned from a transaction, and the transaction was committed or rolled back. +func (_m *HarnessConfig) Update() *HarnessConfigUpdateOne { + return NewHarnessConfigClient(_m.config).UpdateOne(_m) +} + +// Unwrap unwraps the HarnessConfig entity that was returned from a transaction after it was closed, +// so that all future queries will be executed through the driver which created the transaction. +func (_m *HarnessConfig) Unwrap() *HarnessConfig { + _tx, ok := _m.config.driver.(*txDriver) + if !ok { + panic("ent: HarnessConfig is not a transactional entity") + } + _m.config.driver = _tx.drv + return _m +} + +// String implements the fmt.Stringer. +func (_m *HarnessConfig) String() string { + var builder strings.Builder + builder.WriteString("HarnessConfig(") + builder.WriteString(fmt.Sprintf("id=%v, ", _m.ID)) + builder.WriteString("name=") + builder.WriteString(_m.Name) + builder.WriteString(", ") + builder.WriteString("slug=") + builder.WriteString(_m.Slug) + builder.WriteString(", ") + builder.WriteString("display_name=") + builder.WriteString(_m.DisplayName) + builder.WriteString(", ") + builder.WriteString("description=") + builder.WriteString(_m.Description) + builder.WriteString(", ") + builder.WriteString("harness=") + builder.WriteString(_m.Harness) + builder.WriteString(", ") + builder.WriteString("config=") + builder.WriteString(_m.Config) + builder.WriteString(", ") + builder.WriteString("content_hash=") + builder.WriteString(_m.ContentHash) + builder.WriteString(", ") + builder.WriteString("scope=") + builder.WriteString(_m.Scope) + builder.WriteString(", ") + builder.WriteString("scope_id=") + builder.WriteString(_m.ScopeID) + builder.WriteString(", ") + builder.WriteString("storage_uri=") + builder.WriteString(_m.StorageURI) + builder.WriteString(", ") + builder.WriteString("storage_bucket=") + builder.WriteString(_m.StorageBucket) + builder.WriteString(", ") + builder.WriteString("storage_path=") + builder.WriteString(_m.StoragePath) + builder.WriteString(", ") + builder.WriteString("files=") + builder.WriteString(_m.Files) + builder.WriteString(", ") + builder.WriteString("status=") + builder.WriteString(fmt.Sprintf("%v", _m.Status)) + builder.WriteString(", ") + builder.WriteString("owner_id=") + builder.WriteString(_m.OwnerID) + builder.WriteString(", ") + builder.WriteString("created_by=") + builder.WriteString(_m.CreatedBy) + builder.WriteString(", ") + builder.WriteString("updated_by=") + builder.WriteString(_m.UpdatedBy) + builder.WriteString(", ") + builder.WriteString("visibility=") + builder.WriteString(_m.Visibility) + builder.WriteString(", ") + builder.WriteString("created=") + builder.WriteString(_m.Created.Format(time.ANSIC)) + builder.WriteString(", ") + builder.WriteString("updated=") + builder.WriteString(_m.Updated.Format(time.ANSIC)) + builder.WriteByte(')') + return builder.String() +} + +// HarnessConfigs is a parsable slice of HarnessConfig. +type HarnessConfigs []*HarnessConfig diff --git a/pkg/ent/harnessconfig/harnessconfig.go b/pkg/ent/harnessconfig/harnessconfig.go new file mode 100644 index 000000000..988bd2e04 --- /dev/null +++ b/pkg/ent/harnessconfig/harnessconfig.go @@ -0,0 +1,251 @@ +// Code generated by ent, DO NOT EDIT. + +package harnessconfig + +import ( + "fmt" + "time" + + "entgo.io/ent/dialect/sql" + "github.com/google/uuid" +) + +const ( + // Label holds the string label denoting the harnessconfig type in the database. + Label = "harness_config" + // FieldID holds the string denoting the id field in the database. + FieldID = "id" + // FieldName holds the string denoting the name field in the database. + FieldName = "name" + // FieldSlug holds the string denoting the slug field in the database. + FieldSlug = "slug" + // FieldDisplayName holds the string denoting the display_name field in the database. + FieldDisplayName = "display_name" + // FieldDescription holds the string denoting the description field in the database. + FieldDescription = "description" + // FieldHarness holds the string denoting the harness field in the database. + FieldHarness = "harness" + // FieldConfig holds the string denoting the config field in the database. + FieldConfig = "config" + // FieldContentHash holds the string denoting the content_hash field in the database. + FieldContentHash = "content_hash" + // FieldScope holds the string denoting the scope field in the database. + FieldScope = "scope" + // FieldScopeID holds the string denoting the scope_id field in the database. + FieldScopeID = "scope_id" + // FieldStorageURI holds the string denoting the storage_uri field in the database. + FieldStorageURI = "storage_uri" + // FieldStorageBucket holds the string denoting the storage_bucket field in the database. + FieldStorageBucket = "storage_bucket" + // FieldStoragePath holds the string denoting the storage_path field in the database. + FieldStoragePath = "storage_path" + // FieldFiles holds the string denoting the files field in the database. + FieldFiles = "files" + // FieldStatus holds the string denoting the status field in the database. + FieldStatus = "status" + // FieldOwnerID holds the string denoting the owner_id field in the database. + FieldOwnerID = "owner_id" + // FieldCreatedBy holds the string denoting the created_by field in the database. + FieldCreatedBy = "created_by" + // FieldUpdatedBy holds the string denoting the updated_by field in the database. + FieldUpdatedBy = "updated_by" + // FieldVisibility holds the string denoting the visibility field in the database. + FieldVisibility = "visibility" + // FieldCreated holds the string denoting the created field in the database. + FieldCreated = "created" + // FieldUpdated holds the string denoting the updated field in the database. + FieldUpdated = "updated" + // Table holds the table name of the harnessconfig in the database. + Table = "harness_configs" +) + +// Columns holds all SQL columns for harnessconfig fields. +var Columns = []string{ + FieldID, + FieldName, + FieldSlug, + FieldDisplayName, + FieldDescription, + FieldHarness, + FieldConfig, + FieldContentHash, + FieldScope, + FieldScopeID, + FieldStorageURI, + FieldStorageBucket, + FieldStoragePath, + FieldFiles, + FieldStatus, + FieldOwnerID, + FieldCreatedBy, + FieldUpdatedBy, + FieldVisibility, + FieldCreated, + FieldUpdated, +} + +// ValidColumn reports if the column name is valid (part of the table columns). +func ValidColumn(column string) bool { + for i := range Columns { + if column == Columns[i] { + return true + } + } + return false +} + +var ( + // NameValidator is a validator for the "name" field. It is called by the builders before save. + NameValidator func(string) error + // SlugValidator is a validator for the "slug" field. It is called by the builders before save. + SlugValidator func(string) error + // HarnessValidator is a validator for the "harness" field. It is called by the builders before save. + HarnessValidator func(string) error + // DefaultScope holds the default value on creation for the "scope" field. + DefaultScope string + // DefaultVisibility holds the default value on creation for the "visibility" field. + DefaultVisibility string + // DefaultCreated holds the default value on creation for the "created" field. + DefaultCreated func() time.Time + // DefaultUpdated holds the default value on creation for the "updated" field. + DefaultUpdated func() time.Time + // UpdateDefaultUpdated holds the default value on update for the "updated" field. + UpdateDefaultUpdated func() time.Time + // DefaultID holds the default value on creation for the "id" field. + DefaultID func() uuid.UUID +) + +// Status defines the type for the "status" enum field. +type Status string + +// StatusActive is the default value of the Status enum. +const DefaultStatus = StatusActive + +// Status values. +const ( + StatusPending Status = "pending" + StatusActive Status = "active" + StatusArchived Status = "archived" +) + +func (s Status) String() string { + return string(s) +} + +// StatusValidator is a validator for the "status" field enum values. It is called by the builders before save. +func StatusValidator(s Status) error { + switch s { + case StatusPending, StatusActive, StatusArchived: + return nil + default: + return fmt.Errorf("harnessconfig: invalid enum value for status field: %q", s) + } +} + +// OrderOption defines the ordering options for the HarnessConfig queries. +type OrderOption func(*sql.Selector) + +// ByID orders the results by the id field. +func ByID(opts ...sql.OrderTermOption) OrderOption { + return sql.OrderByField(FieldID, opts...).ToFunc() +} + +// ByName orders the results by the name field. +func ByName(opts ...sql.OrderTermOption) OrderOption { + return sql.OrderByField(FieldName, opts...).ToFunc() +} + +// BySlug orders the results by the slug field. +func BySlug(opts ...sql.OrderTermOption) OrderOption { + return sql.OrderByField(FieldSlug, opts...).ToFunc() +} + +// ByDisplayName orders the results by the display_name field. +func ByDisplayName(opts ...sql.OrderTermOption) OrderOption { + return sql.OrderByField(FieldDisplayName, opts...).ToFunc() +} + +// ByDescription orders the results by the description field. +func ByDescription(opts ...sql.OrderTermOption) OrderOption { + return sql.OrderByField(FieldDescription, opts...).ToFunc() +} + +// ByHarness orders the results by the harness field. +func ByHarness(opts ...sql.OrderTermOption) OrderOption { + return sql.OrderByField(FieldHarness, opts...).ToFunc() +} + +// ByConfig orders the results by the config field. +func ByConfig(opts ...sql.OrderTermOption) OrderOption { + return sql.OrderByField(FieldConfig, opts...).ToFunc() +} + +// ByContentHash orders the results by the content_hash field. +func ByContentHash(opts ...sql.OrderTermOption) OrderOption { + return sql.OrderByField(FieldContentHash, opts...).ToFunc() +} + +// ByScope orders the results by the scope field. +func ByScope(opts ...sql.OrderTermOption) OrderOption { + return sql.OrderByField(FieldScope, opts...).ToFunc() +} + +// ByScopeID orders the results by the scope_id field. +func ByScopeID(opts ...sql.OrderTermOption) OrderOption { + return sql.OrderByField(FieldScopeID, opts...).ToFunc() +} + +// ByStorageURI orders the results by the storage_uri field. +func ByStorageURI(opts ...sql.OrderTermOption) OrderOption { + return sql.OrderByField(FieldStorageURI, opts...).ToFunc() +} + +// ByStorageBucket orders the results by the storage_bucket field. +func ByStorageBucket(opts ...sql.OrderTermOption) OrderOption { + return sql.OrderByField(FieldStorageBucket, opts...).ToFunc() +} + +// ByStoragePath orders the results by the storage_path field. +func ByStoragePath(opts ...sql.OrderTermOption) OrderOption { + return sql.OrderByField(FieldStoragePath, opts...).ToFunc() +} + +// ByFiles orders the results by the files field. +func ByFiles(opts ...sql.OrderTermOption) OrderOption { + return sql.OrderByField(FieldFiles, opts...).ToFunc() +} + +// ByStatus orders the results by the status field. +func ByStatus(opts ...sql.OrderTermOption) OrderOption { + return sql.OrderByField(FieldStatus, opts...).ToFunc() +} + +// ByOwnerID orders the results by the owner_id field. +func ByOwnerID(opts ...sql.OrderTermOption) OrderOption { + return sql.OrderByField(FieldOwnerID, opts...).ToFunc() +} + +// ByCreatedBy orders the results by the created_by field. +func ByCreatedBy(opts ...sql.OrderTermOption) OrderOption { + return sql.OrderByField(FieldCreatedBy, opts...).ToFunc() +} + +// ByUpdatedBy orders the results by the updated_by field. +func ByUpdatedBy(opts ...sql.OrderTermOption) OrderOption { + return sql.OrderByField(FieldUpdatedBy, opts...).ToFunc() +} + +// ByVisibility orders the results by the visibility field. +func ByVisibility(opts ...sql.OrderTermOption) OrderOption { + return sql.OrderByField(FieldVisibility, opts...).ToFunc() +} + +// ByCreated orders the results by the created field. +func ByCreated(opts ...sql.OrderTermOption) OrderOption { + return sql.OrderByField(FieldCreated, opts...).ToFunc() +} + +// ByUpdated orders the results by the updated field. +func ByUpdated(opts ...sql.OrderTermOption) OrderOption { + return sql.OrderByField(FieldUpdated, opts...).ToFunc() +} diff --git a/pkg/ent/harnessconfig/where.go b/pkg/ent/harnessconfig/where.go new file mode 100644 index 000000000..96312c474 --- /dev/null +++ b/pkg/ent/harnessconfig/where.go @@ -0,0 +1,1491 @@ +// Code generated by ent, DO NOT EDIT. + +package harnessconfig + +import ( + "time" + + "entgo.io/ent/dialect/sql" + "github.com/GoogleCloudPlatform/scion/pkg/ent/predicate" + "github.com/google/uuid" +) + +// ID filters vertices based on their ID field. +func ID(id uuid.UUID) predicate.HarnessConfig { + return predicate.HarnessConfig(sql.FieldEQ(FieldID, id)) +} + +// IDEQ applies the EQ predicate on the ID field. +func IDEQ(id uuid.UUID) predicate.HarnessConfig { + return predicate.HarnessConfig(sql.FieldEQ(FieldID, id)) +} + +// IDNEQ applies the NEQ predicate on the ID field. +func IDNEQ(id uuid.UUID) predicate.HarnessConfig { + return predicate.HarnessConfig(sql.FieldNEQ(FieldID, id)) +} + +// IDIn applies the In predicate on the ID field. +func IDIn(ids ...uuid.UUID) predicate.HarnessConfig { + return predicate.HarnessConfig(sql.FieldIn(FieldID, ids...)) +} + +// IDNotIn applies the NotIn predicate on the ID field. +func IDNotIn(ids ...uuid.UUID) predicate.HarnessConfig { + return predicate.HarnessConfig(sql.FieldNotIn(FieldID, ids...)) +} + +// IDGT applies the GT predicate on the ID field. +func IDGT(id uuid.UUID) predicate.HarnessConfig { + return predicate.HarnessConfig(sql.FieldGT(FieldID, id)) +} + +// IDGTE applies the GTE predicate on the ID field. +func IDGTE(id uuid.UUID) predicate.HarnessConfig { + return predicate.HarnessConfig(sql.FieldGTE(FieldID, id)) +} + +// IDLT applies the LT predicate on the ID field. +func IDLT(id uuid.UUID) predicate.HarnessConfig { + return predicate.HarnessConfig(sql.FieldLT(FieldID, id)) +} + +// IDLTE applies the LTE predicate on the ID field. +func IDLTE(id uuid.UUID) predicate.HarnessConfig { + return predicate.HarnessConfig(sql.FieldLTE(FieldID, id)) +} + +// Name applies equality check predicate on the "name" field. It's identical to NameEQ. +func Name(v string) predicate.HarnessConfig { + return predicate.HarnessConfig(sql.FieldEQ(FieldName, v)) +} + +// Slug applies equality check predicate on the "slug" field. It's identical to SlugEQ. +func Slug(v string) predicate.HarnessConfig { + return predicate.HarnessConfig(sql.FieldEQ(FieldSlug, v)) +} + +// DisplayName applies equality check predicate on the "display_name" field. It's identical to DisplayNameEQ. +func DisplayName(v string) predicate.HarnessConfig { + return predicate.HarnessConfig(sql.FieldEQ(FieldDisplayName, v)) +} + +// Description applies equality check predicate on the "description" field. It's identical to DescriptionEQ. +func Description(v string) predicate.HarnessConfig { + return predicate.HarnessConfig(sql.FieldEQ(FieldDescription, v)) +} + +// Harness applies equality check predicate on the "harness" field. It's identical to HarnessEQ. +func Harness(v string) predicate.HarnessConfig { + return predicate.HarnessConfig(sql.FieldEQ(FieldHarness, v)) +} + +// Config applies equality check predicate on the "config" field. It's identical to ConfigEQ. +func Config(v string) predicate.HarnessConfig { + return predicate.HarnessConfig(sql.FieldEQ(FieldConfig, v)) +} + +// ContentHash applies equality check predicate on the "content_hash" field. It's identical to ContentHashEQ. +func ContentHash(v string) predicate.HarnessConfig { + return predicate.HarnessConfig(sql.FieldEQ(FieldContentHash, v)) +} + +// Scope applies equality check predicate on the "scope" field. It's identical to ScopeEQ. +func Scope(v string) predicate.HarnessConfig { + return predicate.HarnessConfig(sql.FieldEQ(FieldScope, v)) +} + +// ScopeID applies equality check predicate on the "scope_id" field. It's identical to ScopeIDEQ. +func ScopeID(v string) predicate.HarnessConfig { + return predicate.HarnessConfig(sql.FieldEQ(FieldScopeID, v)) +} + +// StorageURI applies equality check predicate on the "storage_uri" field. It's identical to StorageURIEQ. +func StorageURI(v string) predicate.HarnessConfig { + return predicate.HarnessConfig(sql.FieldEQ(FieldStorageURI, v)) +} + +// StorageBucket applies equality check predicate on the "storage_bucket" field. It's identical to StorageBucketEQ. +func StorageBucket(v string) predicate.HarnessConfig { + return predicate.HarnessConfig(sql.FieldEQ(FieldStorageBucket, v)) +} + +// StoragePath applies equality check predicate on the "storage_path" field. It's identical to StoragePathEQ. +func StoragePath(v string) predicate.HarnessConfig { + return predicate.HarnessConfig(sql.FieldEQ(FieldStoragePath, v)) +} + +// Files applies equality check predicate on the "files" field. It's identical to FilesEQ. +func Files(v string) predicate.HarnessConfig { + return predicate.HarnessConfig(sql.FieldEQ(FieldFiles, v)) +} + +// OwnerID applies equality check predicate on the "owner_id" field. It's identical to OwnerIDEQ. +func OwnerID(v string) predicate.HarnessConfig { + return predicate.HarnessConfig(sql.FieldEQ(FieldOwnerID, v)) +} + +// CreatedBy applies equality check predicate on the "created_by" field. It's identical to CreatedByEQ. +func CreatedBy(v string) predicate.HarnessConfig { + return predicate.HarnessConfig(sql.FieldEQ(FieldCreatedBy, v)) +} + +// UpdatedBy applies equality check predicate on the "updated_by" field. It's identical to UpdatedByEQ. +func UpdatedBy(v string) predicate.HarnessConfig { + return predicate.HarnessConfig(sql.FieldEQ(FieldUpdatedBy, v)) +} + +// Visibility applies equality check predicate on the "visibility" field. It's identical to VisibilityEQ. +func Visibility(v string) predicate.HarnessConfig { + return predicate.HarnessConfig(sql.FieldEQ(FieldVisibility, v)) +} + +// Created applies equality check predicate on the "created" field. It's identical to CreatedEQ. +func Created(v time.Time) predicate.HarnessConfig { + return predicate.HarnessConfig(sql.FieldEQ(FieldCreated, v)) +} + +// Updated applies equality check predicate on the "updated" field. It's identical to UpdatedEQ. +func Updated(v time.Time) predicate.HarnessConfig { + return predicate.HarnessConfig(sql.FieldEQ(FieldUpdated, v)) +} + +// NameEQ applies the EQ predicate on the "name" field. +func NameEQ(v string) predicate.HarnessConfig { + return predicate.HarnessConfig(sql.FieldEQ(FieldName, v)) +} + +// NameNEQ applies the NEQ predicate on the "name" field. +func NameNEQ(v string) predicate.HarnessConfig { + return predicate.HarnessConfig(sql.FieldNEQ(FieldName, v)) +} + +// NameIn applies the In predicate on the "name" field. +func NameIn(vs ...string) predicate.HarnessConfig { + return predicate.HarnessConfig(sql.FieldIn(FieldName, vs...)) +} + +// NameNotIn applies the NotIn predicate on the "name" field. +func NameNotIn(vs ...string) predicate.HarnessConfig { + return predicate.HarnessConfig(sql.FieldNotIn(FieldName, vs...)) +} + +// NameGT applies the GT predicate on the "name" field. +func NameGT(v string) predicate.HarnessConfig { + return predicate.HarnessConfig(sql.FieldGT(FieldName, v)) +} + +// NameGTE applies the GTE predicate on the "name" field. +func NameGTE(v string) predicate.HarnessConfig { + return predicate.HarnessConfig(sql.FieldGTE(FieldName, v)) +} + +// NameLT applies the LT predicate on the "name" field. +func NameLT(v string) predicate.HarnessConfig { + return predicate.HarnessConfig(sql.FieldLT(FieldName, v)) +} + +// NameLTE applies the LTE predicate on the "name" field. +func NameLTE(v string) predicate.HarnessConfig { + return predicate.HarnessConfig(sql.FieldLTE(FieldName, v)) +} + +// NameContains applies the Contains predicate on the "name" field. +func NameContains(v string) predicate.HarnessConfig { + return predicate.HarnessConfig(sql.FieldContains(FieldName, v)) +} + +// NameHasPrefix applies the HasPrefix predicate on the "name" field. +func NameHasPrefix(v string) predicate.HarnessConfig { + return predicate.HarnessConfig(sql.FieldHasPrefix(FieldName, v)) +} + +// NameHasSuffix applies the HasSuffix predicate on the "name" field. +func NameHasSuffix(v string) predicate.HarnessConfig { + return predicate.HarnessConfig(sql.FieldHasSuffix(FieldName, v)) +} + +// NameEqualFold applies the EqualFold predicate on the "name" field. +func NameEqualFold(v string) predicate.HarnessConfig { + return predicate.HarnessConfig(sql.FieldEqualFold(FieldName, v)) +} + +// NameContainsFold applies the ContainsFold predicate on the "name" field. +func NameContainsFold(v string) predicate.HarnessConfig { + return predicate.HarnessConfig(sql.FieldContainsFold(FieldName, v)) +} + +// SlugEQ applies the EQ predicate on the "slug" field. +func SlugEQ(v string) predicate.HarnessConfig { + return predicate.HarnessConfig(sql.FieldEQ(FieldSlug, v)) +} + +// SlugNEQ applies the NEQ predicate on the "slug" field. +func SlugNEQ(v string) predicate.HarnessConfig { + return predicate.HarnessConfig(sql.FieldNEQ(FieldSlug, v)) +} + +// SlugIn applies the In predicate on the "slug" field. +func SlugIn(vs ...string) predicate.HarnessConfig { + return predicate.HarnessConfig(sql.FieldIn(FieldSlug, vs...)) +} + +// SlugNotIn applies the NotIn predicate on the "slug" field. +func SlugNotIn(vs ...string) predicate.HarnessConfig { + return predicate.HarnessConfig(sql.FieldNotIn(FieldSlug, vs...)) +} + +// SlugGT applies the GT predicate on the "slug" field. +func SlugGT(v string) predicate.HarnessConfig { + return predicate.HarnessConfig(sql.FieldGT(FieldSlug, v)) +} + +// SlugGTE applies the GTE predicate on the "slug" field. +func SlugGTE(v string) predicate.HarnessConfig { + return predicate.HarnessConfig(sql.FieldGTE(FieldSlug, v)) +} + +// SlugLT applies the LT predicate on the "slug" field. +func SlugLT(v string) predicate.HarnessConfig { + return predicate.HarnessConfig(sql.FieldLT(FieldSlug, v)) +} + +// SlugLTE applies the LTE predicate on the "slug" field. +func SlugLTE(v string) predicate.HarnessConfig { + return predicate.HarnessConfig(sql.FieldLTE(FieldSlug, v)) +} + +// SlugContains applies the Contains predicate on the "slug" field. +func SlugContains(v string) predicate.HarnessConfig { + return predicate.HarnessConfig(sql.FieldContains(FieldSlug, v)) +} + +// SlugHasPrefix applies the HasPrefix predicate on the "slug" field. +func SlugHasPrefix(v string) predicate.HarnessConfig { + return predicate.HarnessConfig(sql.FieldHasPrefix(FieldSlug, v)) +} + +// SlugHasSuffix applies the HasSuffix predicate on the "slug" field. +func SlugHasSuffix(v string) predicate.HarnessConfig { + return predicate.HarnessConfig(sql.FieldHasSuffix(FieldSlug, v)) +} + +// SlugEqualFold applies the EqualFold predicate on the "slug" field. +func SlugEqualFold(v string) predicate.HarnessConfig { + return predicate.HarnessConfig(sql.FieldEqualFold(FieldSlug, v)) +} + +// SlugContainsFold applies the ContainsFold predicate on the "slug" field. +func SlugContainsFold(v string) predicate.HarnessConfig { + return predicate.HarnessConfig(sql.FieldContainsFold(FieldSlug, v)) +} + +// DisplayNameEQ applies the EQ predicate on the "display_name" field. +func DisplayNameEQ(v string) predicate.HarnessConfig { + return predicate.HarnessConfig(sql.FieldEQ(FieldDisplayName, v)) +} + +// DisplayNameNEQ applies the NEQ predicate on the "display_name" field. +func DisplayNameNEQ(v string) predicate.HarnessConfig { + return predicate.HarnessConfig(sql.FieldNEQ(FieldDisplayName, v)) +} + +// DisplayNameIn applies the In predicate on the "display_name" field. +func DisplayNameIn(vs ...string) predicate.HarnessConfig { + return predicate.HarnessConfig(sql.FieldIn(FieldDisplayName, vs...)) +} + +// DisplayNameNotIn applies the NotIn predicate on the "display_name" field. +func DisplayNameNotIn(vs ...string) predicate.HarnessConfig { + return predicate.HarnessConfig(sql.FieldNotIn(FieldDisplayName, vs...)) +} + +// DisplayNameGT applies the GT predicate on the "display_name" field. +func DisplayNameGT(v string) predicate.HarnessConfig { + return predicate.HarnessConfig(sql.FieldGT(FieldDisplayName, v)) +} + +// DisplayNameGTE applies the GTE predicate on the "display_name" field. +func DisplayNameGTE(v string) predicate.HarnessConfig { + return predicate.HarnessConfig(sql.FieldGTE(FieldDisplayName, v)) +} + +// DisplayNameLT applies the LT predicate on the "display_name" field. +func DisplayNameLT(v string) predicate.HarnessConfig { + return predicate.HarnessConfig(sql.FieldLT(FieldDisplayName, v)) +} + +// DisplayNameLTE applies the LTE predicate on the "display_name" field. +func DisplayNameLTE(v string) predicate.HarnessConfig { + return predicate.HarnessConfig(sql.FieldLTE(FieldDisplayName, v)) +} + +// DisplayNameContains applies the Contains predicate on the "display_name" field. +func DisplayNameContains(v string) predicate.HarnessConfig { + return predicate.HarnessConfig(sql.FieldContains(FieldDisplayName, v)) +} + +// DisplayNameHasPrefix applies the HasPrefix predicate on the "display_name" field. +func DisplayNameHasPrefix(v string) predicate.HarnessConfig { + return predicate.HarnessConfig(sql.FieldHasPrefix(FieldDisplayName, v)) +} + +// DisplayNameHasSuffix applies the HasSuffix predicate on the "display_name" field. +func DisplayNameHasSuffix(v string) predicate.HarnessConfig { + return predicate.HarnessConfig(sql.FieldHasSuffix(FieldDisplayName, v)) +} + +// DisplayNameIsNil applies the IsNil predicate on the "display_name" field. +func DisplayNameIsNil() predicate.HarnessConfig { + return predicate.HarnessConfig(sql.FieldIsNull(FieldDisplayName)) +} + +// DisplayNameNotNil applies the NotNil predicate on the "display_name" field. +func DisplayNameNotNil() predicate.HarnessConfig { + return predicate.HarnessConfig(sql.FieldNotNull(FieldDisplayName)) +} + +// DisplayNameEqualFold applies the EqualFold predicate on the "display_name" field. +func DisplayNameEqualFold(v string) predicate.HarnessConfig { + return predicate.HarnessConfig(sql.FieldEqualFold(FieldDisplayName, v)) +} + +// DisplayNameContainsFold applies the ContainsFold predicate on the "display_name" field. +func DisplayNameContainsFold(v string) predicate.HarnessConfig { + return predicate.HarnessConfig(sql.FieldContainsFold(FieldDisplayName, v)) +} + +// DescriptionEQ applies the EQ predicate on the "description" field. +func DescriptionEQ(v string) predicate.HarnessConfig { + return predicate.HarnessConfig(sql.FieldEQ(FieldDescription, v)) +} + +// DescriptionNEQ applies the NEQ predicate on the "description" field. +func DescriptionNEQ(v string) predicate.HarnessConfig { + return predicate.HarnessConfig(sql.FieldNEQ(FieldDescription, v)) +} + +// DescriptionIn applies the In predicate on the "description" field. +func DescriptionIn(vs ...string) predicate.HarnessConfig { + return predicate.HarnessConfig(sql.FieldIn(FieldDescription, vs...)) +} + +// DescriptionNotIn applies the NotIn predicate on the "description" field. +func DescriptionNotIn(vs ...string) predicate.HarnessConfig { + return predicate.HarnessConfig(sql.FieldNotIn(FieldDescription, vs...)) +} + +// DescriptionGT applies the GT predicate on the "description" field. +func DescriptionGT(v string) predicate.HarnessConfig { + return predicate.HarnessConfig(sql.FieldGT(FieldDescription, v)) +} + +// DescriptionGTE applies the GTE predicate on the "description" field. +func DescriptionGTE(v string) predicate.HarnessConfig { + return predicate.HarnessConfig(sql.FieldGTE(FieldDescription, v)) +} + +// DescriptionLT applies the LT predicate on the "description" field. +func DescriptionLT(v string) predicate.HarnessConfig { + return predicate.HarnessConfig(sql.FieldLT(FieldDescription, v)) +} + +// DescriptionLTE applies the LTE predicate on the "description" field. +func DescriptionLTE(v string) predicate.HarnessConfig { + return predicate.HarnessConfig(sql.FieldLTE(FieldDescription, v)) +} + +// DescriptionContains applies the Contains predicate on the "description" field. +func DescriptionContains(v string) predicate.HarnessConfig { + return predicate.HarnessConfig(sql.FieldContains(FieldDescription, v)) +} + +// DescriptionHasPrefix applies the HasPrefix predicate on the "description" field. +func DescriptionHasPrefix(v string) predicate.HarnessConfig { + return predicate.HarnessConfig(sql.FieldHasPrefix(FieldDescription, v)) +} + +// DescriptionHasSuffix applies the HasSuffix predicate on the "description" field. +func DescriptionHasSuffix(v string) predicate.HarnessConfig { + return predicate.HarnessConfig(sql.FieldHasSuffix(FieldDescription, v)) +} + +// DescriptionIsNil applies the IsNil predicate on the "description" field. +func DescriptionIsNil() predicate.HarnessConfig { + return predicate.HarnessConfig(sql.FieldIsNull(FieldDescription)) +} + +// DescriptionNotNil applies the NotNil predicate on the "description" field. +func DescriptionNotNil() predicate.HarnessConfig { + return predicate.HarnessConfig(sql.FieldNotNull(FieldDescription)) +} + +// DescriptionEqualFold applies the EqualFold predicate on the "description" field. +func DescriptionEqualFold(v string) predicate.HarnessConfig { + return predicate.HarnessConfig(sql.FieldEqualFold(FieldDescription, v)) +} + +// DescriptionContainsFold applies the ContainsFold predicate on the "description" field. +func DescriptionContainsFold(v string) predicate.HarnessConfig { + return predicate.HarnessConfig(sql.FieldContainsFold(FieldDescription, v)) +} + +// HarnessEQ applies the EQ predicate on the "harness" field. +func HarnessEQ(v string) predicate.HarnessConfig { + return predicate.HarnessConfig(sql.FieldEQ(FieldHarness, v)) +} + +// HarnessNEQ applies the NEQ predicate on the "harness" field. +func HarnessNEQ(v string) predicate.HarnessConfig { + return predicate.HarnessConfig(sql.FieldNEQ(FieldHarness, v)) +} + +// HarnessIn applies the In predicate on the "harness" field. +func HarnessIn(vs ...string) predicate.HarnessConfig { + return predicate.HarnessConfig(sql.FieldIn(FieldHarness, vs...)) +} + +// HarnessNotIn applies the NotIn predicate on the "harness" field. +func HarnessNotIn(vs ...string) predicate.HarnessConfig { + return predicate.HarnessConfig(sql.FieldNotIn(FieldHarness, vs...)) +} + +// HarnessGT applies the GT predicate on the "harness" field. +func HarnessGT(v string) predicate.HarnessConfig { + return predicate.HarnessConfig(sql.FieldGT(FieldHarness, v)) +} + +// HarnessGTE applies the GTE predicate on the "harness" field. +func HarnessGTE(v string) predicate.HarnessConfig { + return predicate.HarnessConfig(sql.FieldGTE(FieldHarness, v)) +} + +// HarnessLT applies the LT predicate on the "harness" field. +func HarnessLT(v string) predicate.HarnessConfig { + return predicate.HarnessConfig(sql.FieldLT(FieldHarness, v)) +} + +// HarnessLTE applies the LTE predicate on the "harness" field. +func HarnessLTE(v string) predicate.HarnessConfig { + return predicate.HarnessConfig(sql.FieldLTE(FieldHarness, v)) +} + +// HarnessContains applies the Contains predicate on the "harness" field. +func HarnessContains(v string) predicate.HarnessConfig { + return predicate.HarnessConfig(sql.FieldContains(FieldHarness, v)) +} + +// HarnessHasPrefix applies the HasPrefix predicate on the "harness" field. +func HarnessHasPrefix(v string) predicate.HarnessConfig { + return predicate.HarnessConfig(sql.FieldHasPrefix(FieldHarness, v)) +} + +// HarnessHasSuffix applies the HasSuffix predicate on the "harness" field. +func HarnessHasSuffix(v string) predicate.HarnessConfig { + return predicate.HarnessConfig(sql.FieldHasSuffix(FieldHarness, v)) +} + +// HarnessEqualFold applies the EqualFold predicate on the "harness" field. +func HarnessEqualFold(v string) predicate.HarnessConfig { + return predicate.HarnessConfig(sql.FieldEqualFold(FieldHarness, v)) +} + +// HarnessContainsFold applies the ContainsFold predicate on the "harness" field. +func HarnessContainsFold(v string) predicate.HarnessConfig { + return predicate.HarnessConfig(sql.FieldContainsFold(FieldHarness, v)) +} + +// ConfigEQ applies the EQ predicate on the "config" field. +func ConfigEQ(v string) predicate.HarnessConfig { + return predicate.HarnessConfig(sql.FieldEQ(FieldConfig, v)) +} + +// ConfigNEQ applies the NEQ predicate on the "config" field. +func ConfigNEQ(v string) predicate.HarnessConfig { + return predicate.HarnessConfig(sql.FieldNEQ(FieldConfig, v)) +} + +// ConfigIn applies the In predicate on the "config" field. +func ConfigIn(vs ...string) predicate.HarnessConfig { + return predicate.HarnessConfig(sql.FieldIn(FieldConfig, vs...)) +} + +// ConfigNotIn applies the NotIn predicate on the "config" field. +func ConfigNotIn(vs ...string) predicate.HarnessConfig { + return predicate.HarnessConfig(sql.FieldNotIn(FieldConfig, vs...)) +} + +// ConfigGT applies the GT predicate on the "config" field. +func ConfigGT(v string) predicate.HarnessConfig { + return predicate.HarnessConfig(sql.FieldGT(FieldConfig, v)) +} + +// ConfigGTE applies the GTE predicate on the "config" field. +func ConfigGTE(v string) predicate.HarnessConfig { + return predicate.HarnessConfig(sql.FieldGTE(FieldConfig, v)) +} + +// ConfigLT applies the LT predicate on the "config" field. +func ConfigLT(v string) predicate.HarnessConfig { + return predicate.HarnessConfig(sql.FieldLT(FieldConfig, v)) +} + +// ConfigLTE applies the LTE predicate on the "config" field. +func ConfigLTE(v string) predicate.HarnessConfig { + return predicate.HarnessConfig(sql.FieldLTE(FieldConfig, v)) +} + +// ConfigContains applies the Contains predicate on the "config" field. +func ConfigContains(v string) predicate.HarnessConfig { + return predicate.HarnessConfig(sql.FieldContains(FieldConfig, v)) +} + +// ConfigHasPrefix applies the HasPrefix predicate on the "config" field. +func ConfigHasPrefix(v string) predicate.HarnessConfig { + return predicate.HarnessConfig(sql.FieldHasPrefix(FieldConfig, v)) +} + +// ConfigHasSuffix applies the HasSuffix predicate on the "config" field. +func ConfigHasSuffix(v string) predicate.HarnessConfig { + return predicate.HarnessConfig(sql.FieldHasSuffix(FieldConfig, v)) +} + +// ConfigIsNil applies the IsNil predicate on the "config" field. +func ConfigIsNil() predicate.HarnessConfig { + return predicate.HarnessConfig(sql.FieldIsNull(FieldConfig)) +} + +// ConfigNotNil applies the NotNil predicate on the "config" field. +func ConfigNotNil() predicate.HarnessConfig { + return predicate.HarnessConfig(sql.FieldNotNull(FieldConfig)) +} + +// ConfigEqualFold applies the EqualFold predicate on the "config" field. +func ConfigEqualFold(v string) predicate.HarnessConfig { + return predicate.HarnessConfig(sql.FieldEqualFold(FieldConfig, v)) +} + +// ConfigContainsFold applies the ContainsFold predicate on the "config" field. +func ConfigContainsFold(v string) predicate.HarnessConfig { + return predicate.HarnessConfig(sql.FieldContainsFold(FieldConfig, v)) +} + +// ContentHashEQ applies the EQ predicate on the "content_hash" field. +func ContentHashEQ(v string) predicate.HarnessConfig { + return predicate.HarnessConfig(sql.FieldEQ(FieldContentHash, v)) +} + +// ContentHashNEQ applies the NEQ predicate on the "content_hash" field. +func ContentHashNEQ(v string) predicate.HarnessConfig { + return predicate.HarnessConfig(sql.FieldNEQ(FieldContentHash, v)) +} + +// ContentHashIn applies the In predicate on the "content_hash" field. +func ContentHashIn(vs ...string) predicate.HarnessConfig { + return predicate.HarnessConfig(sql.FieldIn(FieldContentHash, vs...)) +} + +// ContentHashNotIn applies the NotIn predicate on the "content_hash" field. +func ContentHashNotIn(vs ...string) predicate.HarnessConfig { + return predicate.HarnessConfig(sql.FieldNotIn(FieldContentHash, vs...)) +} + +// ContentHashGT applies the GT predicate on the "content_hash" field. +func ContentHashGT(v string) predicate.HarnessConfig { + return predicate.HarnessConfig(sql.FieldGT(FieldContentHash, v)) +} + +// ContentHashGTE applies the GTE predicate on the "content_hash" field. +func ContentHashGTE(v string) predicate.HarnessConfig { + return predicate.HarnessConfig(sql.FieldGTE(FieldContentHash, v)) +} + +// ContentHashLT applies the LT predicate on the "content_hash" field. +func ContentHashLT(v string) predicate.HarnessConfig { + return predicate.HarnessConfig(sql.FieldLT(FieldContentHash, v)) +} + +// ContentHashLTE applies the LTE predicate on the "content_hash" field. +func ContentHashLTE(v string) predicate.HarnessConfig { + return predicate.HarnessConfig(sql.FieldLTE(FieldContentHash, v)) +} + +// ContentHashContains applies the Contains predicate on the "content_hash" field. +func ContentHashContains(v string) predicate.HarnessConfig { + return predicate.HarnessConfig(sql.FieldContains(FieldContentHash, v)) +} + +// ContentHashHasPrefix applies the HasPrefix predicate on the "content_hash" field. +func ContentHashHasPrefix(v string) predicate.HarnessConfig { + return predicate.HarnessConfig(sql.FieldHasPrefix(FieldContentHash, v)) +} + +// ContentHashHasSuffix applies the HasSuffix predicate on the "content_hash" field. +func ContentHashHasSuffix(v string) predicate.HarnessConfig { + return predicate.HarnessConfig(sql.FieldHasSuffix(FieldContentHash, v)) +} + +// ContentHashIsNil applies the IsNil predicate on the "content_hash" field. +func ContentHashIsNil() predicate.HarnessConfig { + return predicate.HarnessConfig(sql.FieldIsNull(FieldContentHash)) +} + +// ContentHashNotNil applies the NotNil predicate on the "content_hash" field. +func ContentHashNotNil() predicate.HarnessConfig { + return predicate.HarnessConfig(sql.FieldNotNull(FieldContentHash)) +} + +// ContentHashEqualFold applies the EqualFold predicate on the "content_hash" field. +func ContentHashEqualFold(v string) predicate.HarnessConfig { + return predicate.HarnessConfig(sql.FieldEqualFold(FieldContentHash, v)) +} + +// ContentHashContainsFold applies the ContainsFold predicate on the "content_hash" field. +func ContentHashContainsFold(v string) predicate.HarnessConfig { + return predicate.HarnessConfig(sql.FieldContainsFold(FieldContentHash, v)) +} + +// ScopeEQ applies the EQ predicate on the "scope" field. +func ScopeEQ(v string) predicate.HarnessConfig { + return predicate.HarnessConfig(sql.FieldEQ(FieldScope, v)) +} + +// ScopeNEQ applies the NEQ predicate on the "scope" field. +func ScopeNEQ(v string) predicate.HarnessConfig { + return predicate.HarnessConfig(sql.FieldNEQ(FieldScope, v)) +} + +// ScopeIn applies the In predicate on the "scope" field. +func ScopeIn(vs ...string) predicate.HarnessConfig { + return predicate.HarnessConfig(sql.FieldIn(FieldScope, vs...)) +} + +// ScopeNotIn applies the NotIn predicate on the "scope" field. +func ScopeNotIn(vs ...string) predicate.HarnessConfig { + return predicate.HarnessConfig(sql.FieldNotIn(FieldScope, vs...)) +} + +// ScopeGT applies the GT predicate on the "scope" field. +func ScopeGT(v string) predicate.HarnessConfig { + return predicate.HarnessConfig(sql.FieldGT(FieldScope, v)) +} + +// ScopeGTE applies the GTE predicate on the "scope" field. +func ScopeGTE(v string) predicate.HarnessConfig { + return predicate.HarnessConfig(sql.FieldGTE(FieldScope, v)) +} + +// ScopeLT applies the LT predicate on the "scope" field. +func ScopeLT(v string) predicate.HarnessConfig { + return predicate.HarnessConfig(sql.FieldLT(FieldScope, v)) +} + +// ScopeLTE applies the LTE predicate on the "scope" field. +func ScopeLTE(v string) predicate.HarnessConfig { + return predicate.HarnessConfig(sql.FieldLTE(FieldScope, v)) +} + +// ScopeContains applies the Contains predicate on the "scope" field. +func ScopeContains(v string) predicate.HarnessConfig { + return predicate.HarnessConfig(sql.FieldContains(FieldScope, v)) +} + +// ScopeHasPrefix applies the HasPrefix predicate on the "scope" field. +func ScopeHasPrefix(v string) predicate.HarnessConfig { + return predicate.HarnessConfig(sql.FieldHasPrefix(FieldScope, v)) +} + +// ScopeHasSuffix applies the HasSuffix predicate on the "scope" field. +func ScopeHasSuffix(v string) predicate.HarnessConfig { + return predicate.HarnessConfig(sql.FieldHasSuffix(FieldScope, v)) +} + +// ScopeEqualFold applies the EqualFold predicate on the "scope" field. +func ScopeEqualFold(v string) predicate.HarnessConfig { + return predicate.HarnessConfig(sql.FieldEqualFold(FieldScope, v)) +} + +// ScopeContainsFold applies the ContainsFold predicate on the "scope" field. +func ScopeContainsFold(v string) predicate.HarnessConfig { + return predicate.HarnessConfig(sql.FieldContainsFold(FieldScope, v)) +} + +// ScopeIDEQ applies the EQ predicate on the "scope_id" field. +func ScopeIDEQ(v string) predicate.HarnessConfig { + return predicate.HarnessConfig(sql.FieldEQ(FieldScopeID, v)) +} + +// ScopeIDNEQ applies the NEQ predicate on the "scope_id" field. +func ScopeIDNEQ(v string) predicate.HarnessConfig { + return predicate.HarnessConfig(sql.FieldNEQ(FieldScopeID, v)) +} + +// ScopeIDIn applies the In predicate on the "scope_id" field. +func ScopeIDIn(vs ...string) predicate.HarnessConfig { + return predicate.HarnessConfig(sql.FieldIn(FieldScopeID, vs...)) +} + +// ScopeIDNotIn applies the NotIn predicate on the "scope_id" field. +func ScopeIDNotIn(vs ...string) predicate.HarnessConfig { + return predicate.HarnessConfig(sql.FieldNotIn(FieldScopeID, vs...)) +} + +// ScopeIDGT applies the GT predicate on the "scope_id" field. +func ScopeIDGT(v string) predicate.HarnessConfig { + return predicate.HarnessConfig(sql.FieldGT(FieldScopeID, v)) +} + +// ScopeIDGTE applies the GTE predicate on the "scope_id" field. +func ScopeIDGTE(v string) predicate.HarnessConfig { + return predicate.HarnessConfig(sql.FieldGTE(FieldScopeID, v)) +} + +// ScopeIDLT applies the LT predicate on the "scope_id" field. +func ScopeIDLT(v string) predicate.HarnessConfig { + return predicate.HarnessConfig(sql.FieldLT(FieldScopeID, v)) +} + +// ScopeIDLTE applies the LTE predicate on the "scope_id" field. +func ScopeIDLTE(v string) predicate.HarnessConfig { + return predicate.HarnessConfig(sql.FieldLTE(FieldScopeID, v)) +} + +// ScopeIDContains applies the Contains predicate on the "scope_id" field. +func ScopeIDContains(v string) predicate.HarnessConfig { + return predicate.HarnessConfig(sql.FieldContains(FieldScopeID, v)) +} + +// ScopeIDHasPrefix applies the HasPrefix predicate on the "scope_id" field. +func ScopeIDHasPrefix(v string) predicate.HarnessConfig { + return predicate.HarnessConfig(sql.FieldHasPrefix(FieldScopeID, v)) +} + +// ScopeIDHasSuffix applies the HasSuffix predicate on the "scope_id" field. +func ScopeIDHasSuffix(v string) predicate.HarnessConfig { + return predicate.HarnessConfig(sql.FieldHasSuffix(FieldScopeID, v)) +} + +// ScopeIDIsNil applies the IsNil predicate on the "scope_id" field. +func ScopeIDIsNil() predicate.HarnessConfig { + return predicate.HarnessConfig(sql.FieldIsNull(FieldScopeID)) +} + +// ScopeIDNotNil applies the NotNil predicate on the "scope_id" field. +func ScopeIDNotNil() predicate.HarnessConfig { + return predicate.HarnessConfig(sql.FieldNotNull(FieldScopeID)) +} + +// ScopeIDEqualFold applies the EqualFold predicate on the "scope_id" field. +func ScopeIDEqualFold(v string) predicate.HarnessConfig { + return predicate.HarnessConfig(sql.FieldEqualFold(FieldScopeID, v)) +} + +// ScopeIDContainsFold applies the ContainsFold predicate on the "scope_id" field. +func ScopeIDContainsFold(v string) predicate.HarnessConfig { + return predicate.HarnessConfig(sql.FieldContainsFold(FieldScopeID, v)) +} + +// StorageURIEQ applies the EQ predicate on the "storage_uri" field. +func StorageURIEQ(v string) predicate.HarnessConfig { + return predicate.HarnessConfig(sql.FieldEQ(FieldStorageURI, v)) +} + +// StorageURINEQ applies the NEQ predicate on the "storage_uri" field. +func StorageURINEQ(v string) predicate.HarnessConfig { + return predicate.HarnessConfig(sql.FieldNEQ(FieldStorageURI, v)) +} + +// StorageURIIn applies the In predicate on the "storage_uri" field. +func StorageURIIn(vs ...string) predicate.HarnessConfig { + return predicate.HarnessConfig(sql.FieldIn(FieldStorageURI, vs...)) +} + +// StorageURINotIn applies the NotIn predicate on the "storage_uri" field. +func StorageURINotIn(vs ...string) predicate.HarnessConfig { + return predicate.HarnessConfig(sql.FieldNotIn(FieldStorageURI, vs...)) +} + +// StorageURIGT applies the GT predicate on the "storage_uri" field. +func StorageURIGT(v string) predicate.HarnessConfig { + return predicate.HarnessConfig(sql.FieldGT(FieldStorageURI, v)) +} + +// StorageURIGTE applies the GTE predicate on the "storage_uri" field. +func StorageURIGTE(v string) predicate.HarnessConfig { + return predicate.HarnessConfig(sql.FieldGTE(FieldStorageURI, v)) +} + +// StorageURILT applies the LT predicate on the "storage_uri" field. +func StorageURILT(v string) predicate.HarnessConfig { + return predicate.HarnessConfig(sql.FieldLT(FieldStorageURI, v)) +} + +// StorageURILTE applies the LTE predicate on the "storage_uri" field. +func StorageURILTE(v string) predicate.HarnessConfig { + return predicate.HarnessConfig(sql.FieldLTE(FieldStorageURI, v)) +} + +// StorageURIContains applies the Contains predicate on the "storage_uri" field. +func StorageURIContains(v string) predicate.HarnessConfig { + return predicate.HarnessConfig(sql.FieldContains(FieldStorageURI, v)) +} + +// StorageURIHasPrefix applies the HasPrefix predicate on the "storage_uri" field. +func StorageURIHasPrefix(v string) predicate.HarnessConfig { + return predicate.HarnessConfig(sql.FieldHasPrefix(FieldStorageURI, v)) +} + +// StorageURIHasSuffix applies the HasSuffix predicate on the "storage_uri" field. +func StorageURIHasSuffix(v string) predicate.HarnessConfig { + return predicate.HarnessConfig(sql.FieldHasSuffix(FieldStorageURI, v)) +} + +// StorageURIIsNil applies the IsNil predicate on the "storage_uri" field. +func StorageURIIsNil() predicate.HarnessConfig { + return predicate.HarnessConfig(sql.FieldIsNull(FieldStorageURI)) +} + +// StorageURINotNil applies the NotNil predicate on the "storage_uri" field. +func StorageURINotNil() predicate.HarnessConfig { + return predicate.HarnessConfig(sql.FieldNotNull(FieldStorageURI)) +} + +// StorageURIEqualFold applies the EqualFold predicate on the "storage_uri" field. +func StorageURIEqualFold(v string) predicate.HarnessConfig { + return predicate.HarnessConfig(sql.FieldEqualFold(FieldStorageURI, v)) +} + +// StorageURIContainsFold applies the ContainsFold predicate on the "storage_uri" field. +func StorageURIContainsFold(v string) predicate.HarnessConfig { + return predicate.HarnessConfig(sql.FieldContainsFold(FieldStorageURI, v)) +} + +// StorageBucketEQ applies the EQ predicate on the "storage_bucket" field. +func StorageBucketEQ(v string) predicate.HarnessConfig { + return predicate.HarnessConfig(sql.FieldEQ(FieldStorageBucket, v)) +} + +// StorageBucketNEQ applies the NEQ predicate on the "storage_bucket" field. +func StorageBucketNEQ(v string) predicate.HarnessConfig { + return predicate.HarnessConfig(sql.FieldNEQ(FieldStorageBucket, v)) +} + +// StorageBucketIn applies the In predicate on the "storage_bucket" field. +func StorageBucketIn(vs ...string) predicate.HarnessConfig { + return predicate.HarnessConfig(sql.FieldIn(FieldStorageBucket, vs...)) +} + +// StorageBucketNotIn applies the NotIn predicate on the "storage_bucket" field. +func StorageBucketNotIn(vs ...string) predicate.HarnessConfig { + return predicate.HarnessConfig(sql.FieldNotIn(FieldStorageBucket, vs...)) +} + +// StorageBucketGT applies the GT predicate on the "storage_bucket" field. +func StorageBucketGT(v string) predicate.HarnessConfig { + return predicate.HarnessConfig(sql.FieldGT(FieldStorageBucket, v)) +} + +// StorageBucketGTE applies the GTE predicate on the "storage_bucket" field. +func StorageBucketGTE(v string) predicate.HarnessConfig { + return predicate.HarnessConfig(sql.FieldGTE(FieldStorageBucket, v)) +} + +// StorageBucketLT applies the LT predicate on the "storage_bucket" field. +func StorageBucketLT(v string) predicate.HarnessConfig { + return predicate.HarnessConfig(sql.FieldLT(FieldStorageBucket, v)) +} + +// StorageBucketLTE applies the LTE predicate on the "storage_bucket" field. +func StorageBucketLTE(v string) predicate.HarnessConfig { + return predicate.HarnessConfig(sql.FieldLTE(FieldStorageBucket, v)) +} + +// StorageBucketContains applies the Contains predicate on the "storage_bucket" field. +func StorageBucketContains(v string) predicate.HarnessConfig { + return predicate.HarnessConfig(sql.FieldContains(FieldStorageBucket, v)) +} + +// StorageBucketHasPrefix applies the HasPrefix predicate on the "storage_bucket" field. +func StorageBucketHasPrefix(v string) predicate.HarnessConfig { + return predicate.HarnessConfig(sql.FieldHasPrefix(FieldStorageBucket, v)) +} + +// StorageBucketHasSuffix applies the HasSuffix predicate on the "storage_bucket" field. +func StorageBucketHasSuffix(v string) predicate.HarnessConfig { + return predicate.HarnessConfig(sql.FieldHasSuffix(FieldStorageBucket, v)) +} + +// StorageBucketIsNil applies the IsNil predicate on the "storage_bucket" field. +func StorageBucketIsNil() predicate.HarnessConfig { + return predicate.HarnessConfig(sql.FieldIsNull(FieldStorageBucket)) +} + +// StorageBucketNotNil applies the NotNil predicate on the "storage_bucket" field. +func StorageBucketNotNil() predicate.HarnessConfig { + return predicate.HarnessConfig(sql.FieldNotNull(FieldStorageBucket)) +} + +// StorageBucketEqualFold applies the EqualFold predicate on the "storage_bucket" field. +func StorageBucketEqualFold(v string) predicate.HarnessConfig { + return predicate.HarnessConfig(sql.FieldEqualFold(FieldStorageBucket, v)) +} + +// StorageBucketContainsFold applies the ContainsFold predicate on the "storage_bucket" field. +func StorageBucketContainsFold(v string) predicate.HarnessConfig { + return predicate.HarnessConfig(sql.FieldContainsFold(FieldStorageBucket, v)) +} + +// StoragePathEQ applies the EQ predicate on the "storage_path" field. +func StoragePathEQ(v string) predicate.HarnessConfig { + return predicate.HarnessConfig(sql.FieldEQ(FieldStoragePath, v)) +} + +// StoragePathNEQ applies the NEQ predicate on the "storage_path" field. +func StoragePathNEQ(v string) predicate.HarnessConfig { + return predicate.HarnessConfig(sql.FieldNEQ(FieldStoragePath, v)) +} + +// StoragePathIn applies the In predicate on the "storage_path" field. +func StoragePathIn(vs ...string) predicate.HarnessConfig { + return predicate.HarnessConfig(sql.FieldIn(FieldStoragePath, vs...)) +} + +// StoragePathNotIn applies the NotIn predicate on the "storage_path" field. +func StoragePathNotIn(vs ...string) predicate.HarnessConfig { + return predicate.HarnessConfig(sql.FieldNotIn(FieldStoragePath, vs...)) +} + +// StoragePathGT applies the GT predicate on the "storage_path" field. +func StoragePathGT(v string) predicate.HarnessConfig { + return predicate.HarnessConfig(sql.FieldGT(FieldStoragePath, v)) +} + +// StoragePathGTE applies the GTE predicate on the "storage_path" field. +func StoragePathGTE(v string) predicate.HarnessConfig { + return predicate.HarnessConfig(sql.FieldGTE(FieldStoragePath, v)) +} + +// StoragePathLT applies the LT predicate on the "storage_path" field. +func StoragePathLT(v string) predicate.HarnessConfig { + return predicate.HarnessConfig(sql.FieldLT(FieldStoragePath, v)) +} + +// StoragePathLTE applies the LTE predicate on the "storage_path" field. +func StoragePathLTE(v string) predicate.HarnessConfig { + return predicate.HarnessConfig(sql.FieldLTE(FieldStoragePath, v)) +} + +// StoragePathContains applies the Contains predicate on the "storage_path" field. +func StoragePathContains(v string) predicate.HarnessConfig { + return predicate.HarnessConfig(sql.FieldContains(FieldStoragePath, v)) +} + +// StoragePathHasPrefix applies the HasPrefix predicate on the "storage_path" field. +func StoragePathHasPrefix(v string) predicate.HarnessConfig { + return predicate.HarnessConfig(sql.FieldHasPrefix(FieldStoragePath, v)) +} + +// StoragePathHasSuffix applies the HasSuffix predicate on the "storage_path" field. +func StoragePathHasSuffix(v string) predicate.HarnessConfig { + return predicate.HarnessConfig(sql.FieldHasSuffix(FieldStoragePath, v)) +} + +// StoragePathIsNil applies the IsNil predicate on the "storage_path" field. +func StoragePathIsNil() predicate.HarnessConfig { + return predicate.HarnessConfig(sql.FieldIsNull(FieldStoragePath)) +} + +// StoragePathNotNil applies the NotNil predicate on the "storage_path" field. +func StoragePathNotNil() predicate.HarnessConfig { + return predicate.HarnessConfig(sql.FieldNotNull(FieldStoragePath)) +} + +// StoragePathEqualFold applies the EqualFold predicate on the "storage_path" field. +func StoragePathEqualFold(v string) predicate.HarnessConfig { + return predicate.HarnessConfig(sql.FieldEqualFold(FieldStoragePath, v)) +} + +// StoragePathContainsFold applies the ContainsFold predicate on the "storage_path" field. +func StoragePathContainsFold(v string) predicate.HarnessConfig { + return predicate.HarnessConfig(sql.FieldContainsFold(FieldStoragePath, v)) +} + +// FilesEQ applies the EQ predicate on the "files" field. +func FilesEQ(v string) predicate.HarnessConfig { + return predicate.HarnessConfig(sql.FieldEQ(FieldFiles, v)) +} + +// FilesNEQ applies the NEQ predicate on the "files" field. +func FilesNEQ(v string) predicate.HarnessConfig { + return predicate.HarnessConfig(sql.FieldNEQ(FieldFiles, v)) +} + +// FilesIn applies the In predicate on the "files" field. +func FilesIn(vs ...string) predicate.HarnessConfig { + return predicate.HarnessConfig(sql.FieldIn(FieldFiles, vs...)) +} + +// FilesNotIn applies the NotIn predicate on the "files" field. +func FilesNotIn(vs ...string) predicate.HarnessConfig { + return predicate.HarnessConfig(sql.FieldNotIn(FieldFiles, vs...)) +} + +// FilesGT applies the GT predicate on the "files" field. +func FilesGT(v string) predicate.HarnessConfig { + return predicate.HarnessConfig(sql.FieldGT(FieldFiles, v)) +} + +// FilesGTE applies the GTE predicate on the "files" field. +func FilesGTE(v string) predicate.HarnessConfig { + return predicate.HarnessConfig(sql.FieldGTE(FieldFiles, v)) +} + +// FilesLT applies the LT predicate on the "files" field. +func FilesLT(v string) predicate.HarnessConfig { + return predicate.HarnessConfig(sql.FieldLT(FieldFiles, v)) +} + +// FilesLTE applies the LTE predicate on the "files" field. +func FilesLTE(v string) predicate.HarnessConfig { + return predicate.HarnessConfig(sql.FieldLTE(FieldFiles, v)) +} + +// FilesContains applies the Contains predicate on the "files" field. +func FilesContains(v string) predicate.HarnessConfig { + return predicate.HarnessConfig(sql.FieldContains(FieldFiles, v)) +} + +// FilesHasPrefix applies the HasPrefix predicate on the "files" field. +func FilesHasPrefix(v string) predicate.HarnessConfig { + return predicate.HarnessConfig(sql.FieldHasPrefix(FieldFiles, v)) +} + +// FilesHasSuffix applies the HasSuffix predicate on the "files" field. +func FilesHasSuffix(v string) predicate.HarnessConfig { + return predicate.HarnessConfig(sql.FieldHasSuffix(FieldFiles, v)) +} + +// FilesIsNil applies the IsNil predicate on the "files" field. +func FilesIsNil() predicate.HarnessConfig { + return predicate.HarnessConfig(sql.FieldIsNull(FieldFiles)) +} + +// FilesNotNil applies the NotNil predicate on the "files" field. +func FilesNotNil() predicate.HarnessConfig { + return predicate.HarnessConfig(sql.FieldNotNull(FieldFiles)) +} + +// FilesEqualFold applies the EqualFold predicate on the "files" field. +func FilesEqualFold(v string) predicate.HarnessConfig { + return predicate.HarnessConfig(sql.FieldEqualFold(FieldFiles, v)) +} + +// FilesContainsFold applies the ContainsFold predicate on the "files" field. +func FilesContainsFold(v string) predicate.HarnessConfig { + return predicate.HarnessConfig(sql.FieldContainsFold(FieldFiles, v)) +} + +// StatusEQ applies the EQ predicate on the "status" field. +func StatusEQ(v Status) predicate.HarnessConfig { + return predicate.HarnessConfig(sql.FieldEQ(FieldStatus, v)) +} + +// StatusNEQ applies the NEQ predicate on the "status" field. +func StatusNEQ(v Status) predicate.HarnessConfig { + return predicate.HarnessConfig(sql.FieldNEQ(FieldStatus, v)) +} + +// StatusIn applies the In predicate on the "status" field. +func StatusIn(vs ...Status) predicate.HarnessConfig { + return predicate.HarnessConfig(sql.FieldIn(FieldStatus, vs...)) +} + +// StatusNotIn applies the NotIn predicate on the "status" field. +func StatusNotIn(vs ...Status) predicate.HarnessConfig { + return predicate.HarnessConfig(sql.FieldNotIn(FieldStatus, vs...)) +} + +// OwnerIDEQ applies the EQ predicate on the "owner_id" field. +func OwnerIDEQ(v string) predicate.HarnessConfig { + return predicate.HarnessConfig(sql.FieldEQ(FieldOwnerID, v)) +} + +// OwnerIDNEQ applies the NEQ predicate on the "owner_id" field. +func OwnerIDNEQ(v string) predicate.HarnessConfig { + return predicate.HarnessConfig(sql.FieldNEQ(FieldOwnerID, v)) +} + +// OwnerIDIn applies the In predicate on the "owner_id" field. +func OwnerIDIn(vs ...string) predicate.HarnessConfig { + return predicate.HarnessConfig(sql.FieldIn(FieldOwnerID, vs...)) +} + +// OwnerIDNotIn applies the NotIn predicate on the "owner_id" field. +func OwnerIDNotIn(vs ...string) predicate.HarnessConfig { + return predicate.HarnessConfig(sql.FieldNotIn(FieldOwnerID, vs...)) +} + +// OwnerIDGT applies the GT predicate on the "owner_id" field. +func OwnerIDGT(v string) predicate.HarnessConfig { + return predicate.HarnessConfig(sql.FieldGT(FieldOwnerID, v)) +} + +// OwnerIDGTE applies the GTE predicate on the "owner_id" field. +func OwnerIDGTE(v string) predicate.HarnessConfig { + return predicate.HarnessConfig(sql.FieldGTE(FieldOwnerID, v)) +} + +// OwnerIDLT applies the LT predicate on the "owner_id" field. +func OwnerIDLT(v string) predicate.HarnessConfig { + return predicate.HarnessConfig(sql.FieldLT(FieldOwnerID, v)) +} + +// OwnerIDLTE applies the LTE predicate on the "owner_id" field. +func OwnerIDLTE(v string) predicate.HarnessConfig { + return predicate.HarnessConfig(sql.FieldLTE(FieldOwnerID, v)) +} + +// OwnerIDContains applies the Contains predicate on the "owner_id" field. +func OwnerIDContains(v string) predicate.HarnessConfig { + return predicate.HarnessConfig(sql.FieldContains(FieldOwnerID, v)) +} + +// OwnerIDHasPrefix applies the HasPrefix predicate on the "owner_id" field. +func OwnerIDHasPrefix(v string) predicate.HarnessConfig { + return predicate.HarnessConfig(sql.FieldHasPrefix(FieldOwnerID, v)) +} + +// OwnerIDHasSuffix applies the HasSuffix predicate on the "owner_id" field. +func OwnerIDHasSuffix(v string) predicate.HarnessConfig { + return predicate.HarnessConfig(sql.FieldHasSuffix(FieldOwnerID, v)) +} + +// OwnerIDIsNil applies the IsNil predicate on the "owner_id" field. +func OwnerIDIsNil() predicate.HarnessConfig { + return predicate.HarnessConfig(sql.FieldIsNull(FieldOwnerID)) +} + +// OwnerIDNotNil applies the NotNil predicate on the "owner_id" field. +func OwnerIDNotNil() predicate.HarnessConfig { + return predicate.HarnessConfig(sql.FieldNotNull(FieldOwnerID)) +} + +// OwnerIDEqualFold applies the EqualFold predicate on the "owner_id" field. +func OwnerIDEqualFold(v string) predicate.HarnessConfig { + return predicate.HarnessConfig(sql.FieldEqualFold(FieldOwnerID, v)) +} + +// OwnerIDContainsFold applies the ContainsFold predicate on the "owner_id" field. +func OwnerIDContainsFold(v string) predicate.HarnessConfig { + return predicate.HarnessConfig(sql.FieldContainsFold(FieldOwnerID, v)) +} + +// CreatedByEQ applies the EQ predicate on the "created_by" field. +func CreatedByEQ(v string) predicate.HarnessConfig { + return predicate.HarnessConfig(sql.FieldEQ(FieldCreatedBy, v)) +} + +// CreatedByNEQ applies the NEQ predicate on the "created_by" field. +func CreatedByNEQ(v string) predicate.HarnessConfig { + return predicate.HarnessConfig(sql.FieldNEQ(FieldCreatedBy, v)) +} + +// CreatedByIn applies the In predicate on the "created_by" field. +func CreatedByIn(vs ...string) predicate.HarnessConfig { + return predicate.HarnessConfig(sql.FieldIn(FieldCreatedBy, vs...)) +} + +// CreatedByNotIn applies the NotIn predicate on the "created_by" field. +func CreatedByNotIn(vs ...string) predicate.HarnessConfig { + return predicate.HarnessConfig(sql.FieldNotIn(FieldCreatedBy, vs...)) +} + +// CreatedByGT applies the GT predicate on the "created_by" field. +func CreatedByGT(v string) predicate.HarnessConfig { + return predicate.HarnessConfig(sql.FieldGT(FieldCreatedBy, v)) +} + +// CreatedByGTE applies the GTE predicate on the "created_by" field. +func CreatedByGTE(v string) predicate.HarnessConfig { + return predicate.HarnessConfig(sql.FieldGTE(FieldCreatedBy, v)) +} + +// CreatedByLT applies the LT predicate on the "created_by" field. +func CreatedByLT(v string) predicate.HarnessConfig { + return predicate.HarnessConfig(sql.FieldLT(FieldCreatedBy, v)) +} + +// CreatedByLTE applies the LTE predicate on the "created_by" field. +func CreatedByLTE(v string) predicate.HarnessConfig { + return predicate.HarnessConfig(sql.FieldLTE(FieldCreatedBy, v)) +} + +// CreatedByContains applies the Contains predicate on the "created_by" field. +func CreatedByContains(v string) predicate.HarnessConfig { + return predicate.HarnessConfig(sql.FieldContains(FieldCreatedBy, v)) +} + +// CreatedByHasPrefix applies the HasPrefix predicate on the "created_by" field. +func CreatedByHasPrefix(v string) predicate.HarnessConfig { + return predicate.HarnessConfig(sql.FieldHasPrefix(FieldCreatedBy, v)) +} + +// CreatedByHasSuffix applies the HasSuffix predicate on the "created_by" field. +func CreatedByHasSuffix(v string) predicate.HarnessConfig { + return predicate.HarnessConfig(sql.FieldHasSuffix(FieldCreatedBy, v)) +} + +// CreatedByIsNil applies the IsNil predicate on the "created_by" field. +func CreatedByIsNil() predicate.HarnessConfig { + return predicate.HarnessConfig(sql.FieldIsNull(FieldCreatedBy)) +} + +// CreatedByNotNil applies the NotNil predicate on the "created_by" field. +func CreatedByNotNil() predicate.HarnessConfig { + return predicate.HarnessConfig(sql.FieldNotNull(FieldCreatedBy)) +} + +// CreatedByEqualFold applies the EqualFold predicate on the "created_by" field. +func CreatedByEqualFold(v string) predicate.HarnessConfig { + return predicate.HarnessConfig(sql.FieldEqualFold(FieldCreatedBy, v)) +} + +// CreatedByContainsFold applies the ContainsFold predicate on the "created_by" field. +func CreatedByContainsFold(v string) predicate.HarnessConfig { + return predicate.HarnessConfig(sql.FieldContainsFold(FieldCreatedBy, v)) +} + +// UpdatedByEQ applies the EQ predicate on the "updated_by" field. +func UpdatedByEQ(v string) predicate.HarnessConfig { + return predicate.HarnessConfig(sql.FieldEQ(FieldUpdatedBy, v)) +} + +// UpdatedByNEQ applies the NEQ predicate on the "updated_by" field. +func UpdatedByNEQ(v string) predicate.HarnessConfig { + return predicate.HarnessConfig(sql.FieldNEQ(FieldUpdatedBy, v)) +} + +// UpdatedByIn applies the In predicate on the "updated_by" field. +func UpdatedByIn(vs ...string) predicate.HarnessConfig { + return predicate.HarnessConfig(sql.FieldIn(FieldUpdatedBy, vs...)) +} + +// UpdatedByNotIn applies the NotIn predicate on the "updated_by" field. +func UpdatedByNotIn(vs ...string) predicate.HarnessConfig { + return predicate.HarnessConfig(sql.FieldNotIn(FieldUpdatedBy, vs...)) +} + +// UpdatedByGT applies the GT predicate on the "updated_by" field. +func UpdatedByGT(v string) predicate.HarnessConfig { + return predicate.HarnessConfig(sql.FieldGT(FieldUpdatedBy, v)) +} + +// UpdatedByGTE applies the GTE predicate on the "updated_by" field. +func UpdatedByGTE(v string) predicate.HarnessConfig { + return predicate.HarnessConfig(sql.FieldGTE(FieldUpdatedBy, v)) +} + +// UpdatedByLT applies the LT predicate on the "updated_by" field. +func UpdatedByLT(v string) predicate.HarnessConfig { + return predicate.HarnessConfig(sql.FieldLT(FieldUpdatedBy, v)) +} + +// UpdatedByLTE applies the LTE predicate on the "updated_by" field. +func UpdatedByLTE(v string) predicate.HarnessConfig { + return predicate.HarnessConfig(sql.FieldLTE(FieldUpdatedBy, v)) +} + +// UpdatedByContains applies the Contains predicate on the "updated_by" field. +func UpdatedByContains(v string) predicate.HarnessConfig { + return predicate.HarnessConfig(sql.FieldContains(FieldUpdatedBy, v)) +} + +// UpdatedByHasPrefix applies the HasPrefix predicate on the "updated_by" field. +func UpdatedByHasPrefix(v string) predicate.HarnessConfig { + return predicate.HarnessConfig(sql.FieldHasPrefix(FieldUpdatedBy, v)) +} + +// UpdatedByHasSuffix applies the HasSuffix predicate on the "updated_by" field. +func UpdatedByHasSuffix(v string) predicate.HarnessConfig { + return predicate.HarnessConfig(sql.FieldHasSuffix(FieldUpdatedBy, v)) +} + +// UpdatedByIsNil applies the IsNil predicate on the "updated_by" field. +func UpdatedByIsNil() predicate.HarnessConfig { + return predicate.HarnessConfig(sql.FieldIsNull(FieldUpdatedBy)) +} + +// UpdatedByNotNil applies the NotNil predicate on the "updated_by" field. +func UpdatedByNotNil() predicate.HarnessConfig { + return predicate.HarnessConfig(sql.FieldNotNull(FieldUpdatedBy)) +} + +// UpdatedByEqualFold applies the EqualFold predicate on the "updated_by" field. +func UpdatedByEqualFold(v string) predicate.HarnessConfig { + return predicate.HarnessConfig(sql.FieldEqualFold(FieldUpdatedBy, v)) +} + +// UpdatedByContainsFold applies the ContainsFold predicate on the "updated_by" field. +func UpdatedByContainsFold(v string) predicate.HarnessConfig { + return predicate.HarnessConfig(sql.FieldContainsFold(FieldUpdatedBy, v)) +} + +// VisibilityEQ applies the EQ predicate on the "visibility" field. +func VisibilityEQ(v string) predicate.HarnessConfig { + return predicate.HarnessConfig(sql.FieldEQ(FieldVisibility, v)) +} + +// VisibilityNEQ applies the NEQ predicate on the "visibility" field. +func VisibilityNEQ(v string) predicate.HarnessConfig { + return predicate.HarnessConfig(sql.FieldNEQ(FieldVisibility, v)) +} + +// VisibilityIn applies the In predicate on the "visibility" field. +func VisibilityIn(vs ...string) predicate.HarnessConfig { + return predicate.HarnessConfig(sql.FieldIn(FieldVisibility, vs...)) +} + +// VisibilityNotIn applies the NotIn predicate on the "visibility" field. +func VisibilityNotIn(vs ...string) predicate.HarnessConfig { + return predicate.HarnessConfig(sql.FieldNotIn(FieldVisibility, vs...)) +} + +// VisibilityGT applies the GT predicate on the "visibility" field. +func VisibilityGT(v string) predicate.HarnessConfig { + return predicate.HarnessConfig(sql.FieldGT(FieldVisibility, v)) +} + +// VisibilityGTE applies the GTE predicate on the "visibility" field. +func VisibilityGTE(v string) predicate.HarnessConfig { + return predicate.HarnessConfig(sql.FieldGTE(FieldVisibility, v)) +} + +// VisibilityLT applies the LT predicate on the "visibility" field. +func VisibilityLT(v string) predicate.HarnessConfig { + return predicate.HarnessConfig(sql.FieldLT(FieldVisibility, v)) +} + +// VisibilityLTE applies the LTE predicate on the "visibility" field. +func VisibilityLTE(v string) predicate.HarnessConfig { + return predicate.HarnessConfig(sql.FieldLTE(FieldVisibility, v)) +} + +// VisibilityContains applies the Contains predicate on the "visibility" field. +func VisibilityContains(v string) predicate.HarnessConfig { + return predicate.HarnessConfig(sql.FieldContains(FieldVisibility, v)) +} + +// VisibilityHasPrefix applies the HasPrefix predicate on the "visibility" field. +func VisibilityHasPrefix(v string) predicate.HarnessConfig { + return predicate.HarnessConfig(sql.FieldHasPrefix(FieldVisibility, v)) +} + +// VisibilityHasSuffix applies the HasSuffix predicate on the "visibility" field. +func VisibilityHasSuffix(v string) predicate.HarnessConfig { + return predicate.HarnessConfig(sql.FieldHasSuffix(FieldVisibility, v)) +} + +// VisibilityEqualFold applies the EqualFold predicate on the "visibility" field. +func VisibilityEqualFold(v string) predicate.HarnessConfig { + return predicate.HarnessConfig(sql.FieldEqualFold(FieldVisibility, v)) +} + +// VisibilityContainsFold applies the ContainsFold predicate on the "visibility" field. +func VisibilityContainsFold(v string) predicate.HarnessConfig { + return predicate.HarnessConfig(sql.FieldContainsFold(FieldVisibility, v)) +} + +// CreatedEQ applies the EQ predicate on the "created" field. +func CreatedEQ(v time.Time) predicate.HarnessConfig { + return predicate.HarnessConfig(sql.FieldEQ(FieldCreated, v)) +} + +// CreatedNEQ applies the NEQ predicate on the "created" field. +func CreatedNEQ(v time.Time) predicate.HarnessConfig { + return predicate.HarnessConfig(sql.FieldNEQ(FieldCreated, v)) +} + +// CreatedIn applies the In predicate on the "created" field. +func CreatedIn(vs ...time.Time) predicate.HarnessConfig { + return predicate.HarnessConfig(sql.FieldIn(FieldCreated, vs...)) +} + +// CreatedNotIn applies the NotIn predicate on the "created" field. +func CreatedNotIn(vs ...time.Time) predicate.HarnessConfig { + return predicate.HarnessConfig(sql.FieldNotIn(FieldCreated, vs...)) +} + +// CreatedGT applies the GT predicate on the "created" field. +func CreatedGT(v time.Time) predicate.HarnessConfig { + return predicate.HarnessConfig(sql.FieldGT(FieldCreated, v)) +} + +// CreatedGTE applies the GTE predicate on the "created" field. +func CreatedGTE(v time.Time) predicate.HarnessConfig { + return predicate.HarnessConfig(sql.FieldGTE(FieldCreated, v)) +} + +// CreatedLT applies the LT predicate on the "created" field. +func CreatedLT(v time.Time) predicate.HarnessConfig { + return predicate.HarnessConfig(sql.FieldLT(FieldCreated, v)) +} + +// CreatedLTE applies the LTE predicate on the "created" field. +func CreatedLTE(v time.Time) predicate.HarnessConfig { + return predicate.HarnessConfig(sql.FieldLTE(FieldCreated, v)) +} + +// UpdatedEQ applies the EQ predicate on the "updated" field. +func UpdatedEQ(v time.Time) predicate.HarnessConfig { + return predicate.HarnessConfig(sql.FieldEQ(FieldUpdated, v)) +} + +// UpdatedNEQ applies the NEQ predicate on the "updated" field. +func UpdatedNEQ(v time.Time) predicate.HarnessConfig { + return predicate.HarnessConfig(sql.FieldNEQ(FieldUpdated, v)) +} + +// UpdatedIn applies the In predicate on the "updated" field. +func UpdatedIn(vs ...time.Time) predicate.HarnessConfig { + return predicate.HarnessConfig(sql.FieldIn(FieldUpdated, vs...)) +} + +// UpdatedNotIn applies the NotIn predicate on the "updated" field. +func UpdatedNotIn(vs ...time.Time) predicate.HarnessConfig { + return predicate.HarnessConfig(sql.FieldNotIn(FieldUpdated, vs...)) +} + +// UpdatedGT applies the GT predicate on the "updated" field. +func UpdatedGT(v time.Time) predicate.HarnessConfig { + return predicate.HarnessConfig(sql.FieldGT(FieldUpdated, v)) +} + +// UpdatedGTE applies the GTE predicate on the "updated" field. +func UpdatedGTE(v time.Time) predicate.HarnessConfig { + return predicate.HarnessConfig(sql.FieldGTE(FieldUpdated, v)) +} + +// UpdatedLT applies the LT predicate on the "updated" field. +func UpdatedLT(v time.Time) predicate.HarnessConfig { + return predicate.HarnessConfig(sql.FieldLT(FieldUpdated, v)) +} + +// UpdatedLTE applies the LTE predicate on the "updated" field. +func UpdatedLTE(v time.Time) predicate.HarnessConfig { + return predicate.HarnessConfig(sql.FieldLTE(FieldUpdated, v)) +} + +// And groups predicates with the AND operator between them. +func And(predicates ...predicate.HarnessConfig) predicate.HarnessConfig { + return predicate.HarnessConfig(sql.AndPredicates(predicates...)) +} + +// Or groups predicates with the OR operator between them. +func Or(predicates ...predicate.HarnessConfig) predicate.HarnessConfig { + return predicate.HarnessConfig(sql.OrPredicates(predicates...)) +} + +// Not applies the not operator on the given predicate. +func Not(p predicate.HarnessConfig) predicate.HarnessConfig { + return predicate.HarnessConfig(sql.NotPredicates(p)) +} diff --git a/pkg/ent/harnessconfig_create.go b/pkg/ent/harnessconfig_create.go new file mode 100644 index 000000000..fb2a3c5fa --- /dev/null +++ b/pkg/ent/harnessconfig_create.go @@ -0,0 +1,1862 @@ +// Code generated by ent, DO NOT EDIT. + +package ent + +import ( + "context" + "errors" + "fmt" + "time" + + "entgo.io/ent/dialect" + "entgo.io/ent/dialect/sql" + "entgo.io/ent/dialect/sql/sqlgraph" + "entgo.io/ent/schema/field" + "github.com/GoogleCloudPlatform/scion/pkg/ent/harnessconfig" + "github.com/google/uuid" +) + +// HarnessConfigCreate is the builder for creating a HarnessConfig entity. +type HarnessConfigCreate struct { + config + mutation *HarnessConfigMutation + hooks []Hook + conflict []sql.ConflictOption +} + +// SetName sets the "name" field. +func (_c *HarnessConfigCreate) SetName(v string) *HarnessConfigCreate { + _c.mutation.SetName(v) + return _c +} + +// SetSlug sets the "slug" field. +func (_c *HarnessConfigCreate) SetSlug(v string) *HarnessConfigCreate { + _c.mutation.SetSlug(v) + return _c +} + +// SetDisplayName sets the "display_name" field. +func (_c *HarnessConfigCreate) SetDisplayName(v string) *HarnessConfigCreate { + _c.mutation.SetDisplayName(v) + return _c +} + +// SetNillableDisplayName sets the "display_name" field if the given value is not nil. +func (_c *HarnessConfigCreate) SetNillableDisplayName(v *string) *HarnessConfigCreate { + if v != nil { + _c.SetDisplayName(*v) + } + return _c +} + +// SetDescription sets the "description" field. +func (_c *HarnessConfigCreate) SetDescription(v string) *HarnessConfigCreate { + _c.mutation.SetDescription(v) + return _c +} + +// SetNillableDescription sets the "description" field if the given value is not nil. +func (_c *HarnessConfigCreate) SetNillableDescription(v *string) *HarnessConfigCreate { + if v != nil { + _c.SetDescription(*v) + } + return _c +} + +// SetHarness sets the "harness" field. +func (_c *HarnessConfigCreate) SetHarness(v string) *HarnessConfigCreate { + _c.mutation.SetHarness(v) + return _c +} + +// SetConfig sets the "config" field. +func (_c *HarnessConfigCreate) SetConfig(v string) *HarnessConfigCreate { + _c.mutation.SetConfig(v) + return _c +} + +// SetNillableConfig sets the "config" field if the given value is not nil. +func (_c *HarnessConfigCreate) SetNillableConfig(v *string) *HarnessConfigCreate { + if v != nil { + _c.SetConfig(*v) + } + return _c +} + +// SetContentHash sets the "content_hash" field. +func (_c *HarnessConfigCreate) SetContentHash(v string) *HarnessConfigCreate { + _c.mutation.SetContentHash(v) + return _c +} + +// SetNillableContentHash sets the "content_hash" field if the given value is not nil. +func (_c *HarnessConfigCreate) SetNillableContentHash(v *string) *HarnessConfigCreate { + if v != nil { + _c.SetContentHash(*v) + } + return _c +} + +// SetScope sets the "scope" field. +func (_c *HarnessConfigCreate) SetScope(v string) *HarnessConfigCreate { + _c.mutation.SetScope(v) + return _c +} + +// SetNillableScope sets the "scope" field if the given value is not nil. +func (_c *HarnessConfigCreate) SetNillableScope(v *string) *HarnessConfigCreate { + if v != nil { + _c.SetScope(*v) + } + return _c +} + +// SetScopeID sets the "scope_id" field. +func (_c *HarnessConfigCreate) SetScopeID(v string) *HarnessConfigCreate { + _c.mutation.SetScopeID(v) + return _c +} + +// SetNillableScopeID sets the "scope_id" field if the given value is not nil. +func (_c *HarnessConfigCreate) SetNillableScopeID(v *string) *HarnessConfigCreate { + if v != nil { + _c.SetScopeID(*v) + } + return _c +} + +// SetStorageURI sets the "storage_uri" field. +func (_c *HarnessConfigCreate) SetStorageURI(v string) *HarnessConfigCreate { + _c.mutation.SetStorageURI(v) + return _c +} + +// SetNillableStorageURI sets the "storage_uri" field if the given value is not nil. +func (_c *HarnessConfigCreate) SetNillableStorageURI(v *string) *HarnessConfigCreate { + if v != nil { + _c.SetStorageURI(*v) + } + return _c +} + +// SetStorageBucket sets the "storage_bucket" field. +func (_c *HarnessConfigCreate) SetStorageBucket(v string) *HarnessConfigCreate { + _c.mutation.SetStorageBucket(v) + return _c +} + +// SetNillableStorageBucket sets the "storage_bucket" field if the given value is not nil. +func (_c *HarnessConfigCreate) SetNillableStorageBucket(v *string) *HarnessConfigCreate { + if v != nil { + _c.SetStorageBucket(*v) + } + return _c +} + +// SetStoragePath sets the "storage_path" field. +func (_c *HarnessConfigCreate) SetStoragePath(v string) *HarnessConfigCreate { + _c.mutation.SetStoragePath(v) + return _c +} + +// SetNillableStoragePath sets the "storage_path" field if the given value is not nil. +func (_c *HarnessConfigCreate) SetNillableStoragePath(v *string) *HarnessConfigCreate { + if v != nil { + _c.SetStoragePath(*v) + } + return _c +} + +// SetFiles sets the "files" field. +func (_c *HarnessConfigCreate) SetFiles(v string) *HarnessConfigCreate { + _c.mutation.SetFiles(v) + return _c +} + +// SetNillableFiles sets the "files" field if the given value is not nil. +func (_c *HarnessConfigCreate) SetNillableFiles(v *string) *HarnessConfigCreate { + if v != nil { + _c.SetFiles(*v) + } + return _c +} + +// SetStatus sets the "status" field. +func (_c *HarnessConfigCreate) SetStatus(v harnessconfig.Status) *HarnessConfigCreate { + _c.mutation.SetStatus(v) + return _c +} + +// SetNillableStatus sets the "status" field if the given value is not nil. +func (_c *HarnessConfigCreate) SetNillableStatus(v *harnessconfig.Status) *HarnessConfigCreate { + if v != nil { + _c.SetStatus(*v) + } + return _c +} + +// SetOwnerID sets the "owner_id" field. +func (_c *HarnessConfigCreate) SetOwnerID(v string) *HarnessConfigCreate { + _c.mutation.SetOwnerID(v) + return _c +} + +// SetNillableOwnerID sets the "owner_id" field if the given value is not nil. +func (_c *HarnessConfigCreate) SetNillableOwnerID(v *string) *HarnessConfigCreate { + if v != nil { + _c.SetOwnerID(*v) + } + return _c +} + +// SetCreatedBy sets the "created_by" field. +func (_c *HarnessConfigCreate) SetCreatedBy(v string) *HarnessConfigCreate { + _c.mutation.SetCreatedBy(v) + return _c +} + +// SetNillableCreatedBy sets the "created_by" field if the given value is not nil. +func (_c *HarnessConfigCreate) SetNillableCreatedBy(v *string) *HarnessConfigCreate { + if v != nil { + _c.SetCreatedBy(*v) + } + return _c +} + +// SetUpdatedBy sets the "updated_by" field. +func (_c *HarnessConfigCreate) SetUpdatedBy(v string) *HarnessConfigCreate { + _c.mutation.SetUpdatedBy(v) + return _c +} + +// SetNillableUpdatedBy sets the "updated_by" field if the given value is not nil. +func (_c *HarnessConfigCreate) SetNillableUpdatedBy(v *string) *HarnessConfigCreate { + if v != nil { + _c.SetUpdatedBy(*v) + } + return _c +} + +// SetVisibility sets the "visibility" field. +func (_c *HarnessConfigCreate) SetVisibility(v string) *HarnessConfigCreate { + _c.mutation.SetVisibility(v) + return _c +} + +// SetNillableVisibility sets the "visibility" field if the given value is not nil. +func (_c *HarnessConfigCreate) SetNillableVisibility(v *string) *HarnessConfigCreate { + if v != nil { + _c.SetVisibility(*v) + } + return _c +} + +// SetCreated sets the "created" field. +func (_c *HarnessConfigCreate) SetCreated(v time.Time) *HarnessConfigCreate { + _c.mutation.SetCreated(v) + return _c +} + +// SetNillableCreated sets the "created" field if the given value is not nil. +func (_c *HarnessConfigCreate) SetNillableCreated(v *time.Time) *HarnessConfigCreate { + if v != nil { + _c.SetCreated(*v) + } + return _c +} + +// SetUpdated sets the "updated" field. +func (_c *HarnessConfigCreate) SetUpdated(v time.Time) *HarnessConfigCreate { + _c.mutation.SetUpdated(v) + return _c +} + +// SetNillableUpdated sets the "updated" field if the given value is not nil. +func (_c *HarnessConfigCreate) SetNillableUpdated(v *time.Time) *HarnessConfigCreate { + if v != nil { + _c.SetUpdated(*v) + } + return _c +} + +// SetID sets the "id" field. +func (_c *HarnessConfigCreate) SetID(v uuid.UUID) *HarnessConfigCreate { + _c.mutation.SetID(v) + return _c +} + +// SetNillableID sets the "id" field if the given value is not nil. +func (_c *HarnessConfigCreate) SetNillableID(v *uuid.UUID) *HarnessConfigCreate { + if v != nil { + _c.SetID(*v) + } + return _c +} + +// Mutation returns the HarnessConfigMutation object of the builder. +func (_c *HarnessConfigCreate) Mutation() *HarnessConfigMutation { + return _c.mutation +} + +// Save creates the HarnessConfig in the database. +func (_c *HarnessConfigCreate) Save(ctx context.Context) (*HarnessConfig, error) { + _c.defaults() + return withHooks(ctx, _c.sqlSave, _c.mutation, _c.hooks) +} + +// SaveX calls Save and panics if Save returns an error. +func (_c *HarnessConfigCreate) SaveX(ctx context.Context) *HarnessConfig { + v, err := _c.Save(ctx) + if err != nil { + panic(err) + } + return v +} + +// Exec executes the query. +func (_c *HarnessConfigCreate) Exec(ctx context.Context) error { + _, err := _c.Save(ctx) + return err +} + +// ExecX is like Exec, but panics if an error occurs. +func (_c *HarnessConfigCreate) ExecX(ctx context.Context) { + if err := _c.Exec(ctx); err != nil { + panic(err) + } +} + +// defaults sets the default values of the builder before save. +func (_c *HarnessConfigCreate) defaults() { + if _, ok := _c.mutation.Scope(); !ok { + v := harnessconfig.DefaultScope + _c.mutation.SetScope(v) + } + if _, ok := _c.mutation.Status(); !ok { + v := harnessconfig.DefaultStatus + _c.mutation.SetStatus(v) + } + if _, ok := _c.mutation.Visibility(); !ok { + v := harnessconfig.DefaultVisibility + _c.mutation.SetVisibility(v) + } + if _, ok := _c.mutation.Created(); !ok { + v := harnessconfig.DefaultCreated() + _c.mutation.SetCreated(v) + } + if _, ok := _c.mutation.Updated(); !ok { + v := harnessconfig.DefaultUpdated() + _c.mutation.SetUpdated(v) + } + if _, ok := _c.mutation.ID(); !ok { + v := harnessconfig.DefaultID() + _c.mutation.SetID(v) + } +} + +// check runs all checks and user-defined validators on the builder. +func (_c *HarnessConfigCreate) check() error { + if _, ok := _c.mutation.Name(); !ok { + return &ValidationError{Name: "name", err: errors.New(`ent: missing required field "HarnessConfig.name"`)} + } + if v, ok := _c.mutation.Name(); ok { + if err := harnessconfig.NameValidator(v); err != nil { + return &ValidationError{Name: "name", err: fmt.Errorf(`ent: validator failed for field "HarnessConfig.name": %w`, err)} + } + } + if _, ok := _c.mutation.Slug(); !ok { + return &ValidationError{Name: "slug", err: errors.New(`ent: missing required field "HarnessConfig.slug"`)} + } + if v, ok := _c.mutation.Slug(); ok { + if err := harnessconfig.SlugValidator(v); err != nil { + return &ValidationError{Name: "slug", err: fmt.Errorf(`ent: validator failed for field "HarnessConfig.slug": %w`, err)} + } + } + if _, ok := _c.mutation.Harness(); !ok { + return &ValidationError{Name: "harness", err: errors.New(`ent: missing required field "HarnessConfig.harness"`)} + } + if v, ok := _c.mutation.Harness(); ok { + if err := harnessconfig.HarnessValidator(v); err != nil { + return &ValidationError{Name: "harness", err: fmt.Errorf(`ent: validator failed for field "HarnessConfig.harness": %w`, err)} + } + } + if _, ok := _c.mutation.Scope(); !ok { + return &ValidationError{Name: "scope", err: errors.New(`ent: missing required field "HarnessConfig.scope"`)} + } + if _, ok := _c.mutation.Status(); !ok { + return &ValidationError{Name: "status", err: errors.New(`ent: missing required field "HarnessConfig.status"`)} + } + if v, ok := _c.mutation.Status(); ok { + if err := harnessconfig.StatusValidator(v); err != nil { + return &ValidationError{Name: "status", err: fmt.Errorf(`ent: validator failed for field "HarnessConfig.status": %w`, err)} + } + } + if _, ok := _c.mutation.Visibility(); !ok { + return &ValidationError{Name: "visibility", err: errors.New(`ent: missing required field "HarnessConfig.visibility"`)} + } + if _, ok := _c.mutation.Created(); !ok { + return &ValidationError{Name: "created", err: errors.New(`ent: missing required field "HarnessConfig.created"`)} + } + if _, ok := _c.mutation.Updated(); !ok { + return &ValidationError{Name: "updated", err: errors.New(`ent: missing required field "HarnessConfig.updated"`)} + } + return nil +} + +func (_c *HarnessConfigCreate) sqlSave(ctx context.Context) (*HarnessConfig, error) { + if err := _c.check(); err != nil { + return nil, err + } + _node, _spec := _c.createSpec() + if err := sqlgraph.CreateNode(ctx, _c.driver, _spec); err != nil { + if sqlgraph.IsConstraintError(err) { + err = &ConstraintError{msg: err.Error(), wrap: err} + } + return nil, err + } + if _spec.ID.Value != nil { + if id, ok := _spec.ID.Value.(*uuid.UUID); ok { + _node.ID = *id + } else if err := _node.ID.Scan(_spec.ID.Value); err != nil { + return nil, err + } + } + _c.mutation.id = &_node.ID + _c.mutation.done = true + return _node, nil +} + +func (_c *HarnessConfigCreate) createSpec() (*HarnessConfig, *sqlgraph.CreateSpec) { + var ( + _node = &HarnessConfig{config: _c.config} + _spec = sqlgraph.NewCreateSpec(harnessconfig.Table, sqlgraph.NewFieldSpec(harnessconfig.FieldID, field.TypeUUID)) + ) + _spec.OnConflict = _c.conflict + if id, ok := _c.mutation.ID(); ok { + _node.ID = id + _spec.ID.Value = &id + } + if value, ok := _c.mutation.Name(); ok { + _spec.SetField(harnessconfig.FieldName, field.TypeString, value) + _node.Name = value + } + if value, ok := _c.mutation.Slug(); ok { + _spec.SetField(harnessconfig.FieldSlug, field.TypeString, value) + _node.Slug = value + } + if value, ok := _c.mutation.DisplayName(); ok { + _spec.SetField(harnessconfig.FieldDisplayName, field.TypeString, value) + _node.DisplayName = value + } + if value, ok := _c.mutation.Description(); ok { + _spec.SetField(harnessconfig.FieldDescription, field.TypeString, value) + _node.Description = value + } + if value, ok := _c.mutation.Harness(); ok { + _spec.SetField(harnessconfig.FieldHarness, field.TypeString, value) + _node.Harness = value + } + if value, ok := _c.mutation.Config(); ok { + _spec.SetField(harnessconfig.FieldConfig, field.TypeString, value) + _node.Config = value + } + if value, ok := _c.mutation.ContentHash(); ok { + _spec.SetField(harnessconfig.FieldContentHash, field.TypeString, value) + _node.ContentHash = value + } + if value, ok := _c.mutation.Scope(); ok { + _spec.SetField(harnessconfig.FieldScope, field.TypeString, value) + _node.Scope = value + } + if value, ok := _c.mutation.ScopeID(); ok { + _spec.SetField(harnessconfig.FieldScopeID, field.TypeString, value) + _node.ScopeID = value + } + if value, ok := _c.mutation.StorageURI(); ok { + _spec.SetField(harnessconfig.FieldStorageURI, field.TypeString, value) + _node.StorageURI = value + } + if value, ok := _c.mutation.StorageBucket(); ok { + _spec.SetField(harnessconfig.FieldStorageBucket, field.TypeString, value) + _node.StorageBucket = value + } + if value, ok := _c.mutation.StoragePath(); ok { + _spec.SetField(harnessconfig.FieldStoragePath, field.TypeString, value) + _node.StoragePath = value + } + if value, ok := _c.mutation.Files(); ok { + _spec.SetField(harnessconfig.FieldFiles, field.TypeString, value) + _node.Files = value + } + if value, ok := _c.mutation.Status(); ok { + _spec.SetField(harnessconfig.FieldStatus, field.TypeEnum, value) + _node.Status = value + } + if value, ok := _c.mutation.OwnerID(); ok { + _spec.SetField(harnessconfig.FieldOwnerID, field.TypeString, value) + _node.OwnerID = value + } + if value, ok := _c.mutation.CreatedBy(); ok { + _spec.SetField(harnessconfig.FieldCreatedBy, field.TypeString, value) + _node.CreatedBy = value + } + if value, ok := _c.mutation.UpdatedBy(); ok { + _spec.SetField(harnessconfig.FieldUpdatedBy, field.TypeString, value) + _node.UpdatedBy = value + } + if value, ok := _c.mutation.Visibility(); ok { + _spec.SetField(harnessconfig.FieldVisibility, field.TypeString, value) + _node.Visibility = value + } + if value, ok := _c.mutation.Created(); ok { + _spec.SetField(harnessconfig.FieldCreated, field.TypeTime, value) + _node.Created = value + } + if value, ok := _c.mutation.Updated(); ok { + _spec.SetField(harnessconfig.FieldUpdated, field.TypeTime, value) + _node.Updated = value + } + return _node, _spec +} + +// OnConflict allows configuring the `ON CONFLICT` / `ON DUPLICATE KEY` clause +// of the `INSERT` statement. For example: +// +// client.HarnessConfig.Create(). +// SetName(v). +// OnConflict( +// // Update the row with the new values +// // the was proposed for insertion. +// sql.ResolveWithNewValues(), +// ). +// // Override some of the fields with custom +// // update values. +// Update(func(u *ent.HarnessConfigUpsert) { +// SetName(v+v). +// }). +// Exec(ctx) +func (_c *HarnessConfigCreate) OnConflict(opts ...sql.ConflictOption) *HarnessConfigUpsertOne { + _c.conflict = opts + return &HarnessConfigUpsertOne{ + create: _c, + } +} + +// OnConflictColumns calls `OnConflict` and configures the columns +// as conflict target. Using this option is equivalent to using: +// +// client.HarnessConfig.Create(). +// OnConflict(sql.ConflictColumns(columns...)). +// Exec(ctx) +func (_c *HarnessConfigCreate) OnConflictColumns(columns ...string) *HarnessConfigUpsertOne { + _c.conflict = append(_c.conflict, sql.ConflictColumns(columns...)) + return &HarnessConfigUpsertOne{ + create: _c, + } +} + +type ( + // HarnessConfigUpsertOne is the builder for "upsert"-ing + // one HarnessConfig node. + HarnessConfigUpsertOne struct { + create *HarnessConfigCreate + } + + // HarnessConfigUpsert is the "OnConflict" setter. + HarnessConfigUpsert struct { + *sql.UpdateSet + } +) + +// SetName sets the "name" field. +func (u *HarnessConfigUpsert) SetName(v string) *HarnessConfigUpsert { + u.Set(harnessconfig.FieldName, v) + return u +} + +// UpdateName sets the "name" field to the value that was provided on create. +func (u *HarnessConfigUpsert) UpdateName() *HarnessConfigUpsert { + u.SetExcluded(harnessconfig.FieldName) + return u +} + +// SetSlug sets the "slug" field. +func (u *HarnessConfigUpsert) SetSlug(v string) *HarnessConfigUpsert { + u.Set(harnessconfig.FieldSlug, v) + return u +} + +// UpdateSlug sets the "slug" field to the value that was provided on create. +func (u *HarnessConfigUpsert) UpdateSlug() *HarnessConfigUpsert { + u.SetExcluded(harnessconfig.FieldSlug) + return u +} + +// SetDisplayName sets the "display_name" field. +func (u *HarnessConfigUpsert) SetDisplayName(v string) *HarnessConfigUpsert { + u.Set(harnessconfig.FieldDisplayName, v) + return u +} + +// UpdateDisplayName sets the "display_name" field to the value that was provided on create. +func (u *HarnessConfigUpsert) UpdateDisplayName() *HarnessConfigUpsert { + u.SetExcluded(harnessconfig.FieldDisplayName) + return u +} + +// ClearDisplayName clears the value of the "display_name" field. +func (u *HarnessConfigUpsert) ClearDisplayName() *HarnessConfigUpsert { + u.SetNull(harnessconfig.FieldDisplayName) + return u +} + +// SetDescription sets the "description" field. +func (u *HarnessConfigUpsert) SetDescription(v string) *HarnessConfigUpsert { + u.Set(harnessconfig.FieldDescription, v) + return u +} + +// UpdateDescription sets the "description" field to the value that was provided on create. +func (u *HarnessConfigUpsert) UpdateDescription() *HarnessConfigUpsert { + u.SetExcluded(harnessconfig.FieldDescription) + return u +} + +// ClearDescription clears the value of the "description" field. +func (u *HarnessConfigUpsert) ClearDescription() *HarnessConfigUpsert { + u.SetNull(harnessconfig.FieldDescription) + return u +} + +// SetHarness sets the "harness" field. +func (u *HarnessConfigUpsert) SetHarness(v string) *HarnessConfigUpsert { + u.Set(harnessconfig.FieldHarness, v) + return u +} + +// UpdateHarness sets the "harness" field to the value that was provided on create. +func (u *HarnessConfigUpsert) UpdateHarness() *HarnessConfigUpsert { + u.SetExcluded(harnessconfig.FieldHarness) + return u +} + +// SetConfig sets the "config" field. +func (u *HarnessConfigUpsert) SetConfig(v string) *HarnessConfigUpsert { + u.Set(harnessconfig.FieldConfig, v) + return u +} + +// UpdateConfig sets the "config" field to the value that was provided on create. +func (u *HarnessConfigUpsert) UpdateConfig() *HarnessConfigUpsert { + u.SetExcluded(harnessconfig.FieldConfig) + return u +} + +// ClearConfig clears the value of the "config" field. +func (u *HarnessConfigUpsert) ClearConfig() *HarnessConfigUpsert { + u.SetNull(harnessconfig.FieldConfig) + return u +} + +// SetContentHash sets the "content_hash" field. +func (u *HarnessConfigUpsert) SetContentHash(v string) *HarnessConfigUpsert { + u.Set(harnessconfig.FieldContentHash, v) + return u +} + +// UpdateContentHash sets the "content_hash" field to the value that was provided on create. +func (u *HarnessConfigUpsert) UpdateContentHash() *HarnessConfigUpsert { + u.SetExcluded(harnessconfig.FieldContentHash) + return u +} + +// ClearContentHash clears the value of the "content_hash" field. +func (u *HarnessConfigUpsert) ClearContentHash() *HarnessConfigUpsert { + u.SetNull(harnessconfig.FieldContentHash) + return u +} + +// SetScope sets the "scope" field. +func (u *HarnessConfigUpsert) SetScope(v string) *HarnessConfigUpsert { + u.Set(harnessconfig.FieldScope, v) + return u +} + +// UpdateScope sets the "scope" field to the value that was provided on create. +func (u *HarnessConfigUpsert) UpdateScope() *HarnessConfigUpsert { + u.SetExcluded(harnessconfig.FieldScope) + return u +} + +// SetScopeID sets the "scope_id" field. +func (u *HarnessConfigUpsert) SetScopeID(v string) *HarnessConfigUpsert { + u.Set(harnessconfig.FieldScopeID, v) + return u +} + +// UpdateScopeID sets the "scope_id" field to the value that was provided on create. +func (u *HarnessConfigUpsert) UpdateScopeID() *HarnessConfigUpsert { + u.SetExcluded(harnessconfig.FieldScopeID) + return u +} + +// ClearScopeID clears the value of the "scope_id" field. +func (u *HarnessConfigUpsert) ClearScopeID() *HarnessConfigUpsert { + u.SetNull(harnessconfig.FieldScopeID) + return u +} + +// SetStorageURI sets the "storage_uri" field. +func (u *HarnessConfigUpsert) SetStorageURI(v string) *HarnessConfigUpsert { + u.Set(harnessconfig.FieldStorageURI, v) + return u +} + +// UpdateStorageURI sets the "storage_uri" field to the value that was provided on create. +func (u *HarnessConfigUpsert) UpdateStorageURI() *HarnessConfigUpsert { + u.SetExcluded(harnessconfig.FieldStorageURI) + return u +} + +// ClearStorageURI clears the value of the "storage_uri" field. +func (u *HarnessConfigUpsert) ClearStorageURI() *HarnessConfigUpsert { + u.SetNull(harnessconfig.FieldStorageURI) + return u +} + +// SetStorageBucket sets the "storage_bucket" field. +func (u *HarnessConfigUpsert) SetStorageBucket(v string) *HarnessConfigUpsert { + u.Set(harnessconfig.FieldStorageBucket, v) + return u +} + +// UpdateStorageBucket sets the "storage_bucket" field to the value that was provided on create. +func (u *HarnessConfigUpsert) UpdateStorageBucket() *HarnessConfigUpsert { + u.SetExcluded(harnessconfig.FieldStorageBucket) + return u +} + +// ClearStorageBucket clears the value of the "storage_bucket" field. +func (u *HarnessConfigUpsert) ClearStorageBucket() *HarnessConfigUpsert { + u.SetNull(harnessconfig.FieldStorageBucket) + return u +} + +// SetStoragePath sets the "storage_path" field. +func (u *HarnessConfigUpsert) SetStoragePath(v string) *HarnessConfigUpsert { + u.Set(harnessconfig.FieldStoragePath, v) + return u +} + +// UpdateStoragePath sets the "storage_path" field to the value that was provided on create. +func (u *HarnessConfigUpsert) UpdateStoragePath() *HarnessConfigUpsert { + u.SetExcluded(harnessconfig.FieldStoragePath) + return u +} + +// ClearStoragePath clears the value of the "storage_path" field. +func (u *HarnessConfigUpsert) ClearStoragePath() *HarnessConfigUpsert { + u.SetNull(harnessconfig.FieldStoragePath) + return u +} + +// SetFiles sets the "files" field. +func (u *HarnessConfigUpsert) SetFiles(v string) *HarnessConfigUpsert { + u.Set(harnessconfig.FieldFiles, v) + return u +} + +// UpdateFiles sets the "files" field to the value that was provided on create. +func (u *HarnessConfigUpsert) UpdateFiles() *HarnessConfigUpsert { + u.SetExcluded(harnessconfig.FieldFiles) + return u +} + +// ClearFiles clears the value of the "files" field. +func (u *HarnessConfigUpsert) ClearFiles() *HarnessConfigUpsert { + u.SetNull(harnessconfig.FieldFiles) + return u +} + +// SetStatus sets the "status" field. +func (u *HarnessConfigUpsert) SetStatus(v harnessconfig.Status) *HarnessConfigUpsert { + u.Set(harnessconfig.FieldStatus, v) + return u +} + +// UpdateStatus sets the "status" field to the value that was provided on create. +func (u *HarnessConfigUpsert) UpdateStatus() *HarnessConfigUpsert { + u.SetExcluded(harnessconfig.FieldStatus) + return u +} + +// SetOwnerID sets the "owner_id" field. +func (u *HarnessConfigUpsert) SetOwnerID(v string) *HarnessConfigUpsert { + u.Set(harnessconfig.FieldOwnerID, v) + return u +} + +// UpdateOwnerID sets the "owner_id" field to the value that was provided on create. +func (u *HarnessConfigUpsert) UpdateOwnerID() *HarnessConfigUpsert { + u.SetExcluded(harnessconfig.FieldOwnerID) + return u +} + +// ClearOwnerID clears the value of the "owner_id" field. +func (u *HarnessConfigUpsert) ClearOwnerID() *HarnessConfigUpsert { + u.SetNull(harnessconfig.FieldOwnerID) + return u +} + +// SetCreatedBy sets the "created_by" field. +func (u *HarnessConfigUpsert) SetCreatedBy(v string) *HarnessConfigUpsert { + u.Set(harnessconfig.FieldCreatedBy, v) + return u +} + +// UpdateCreatedBy sets the "created_by" field to the value that was provided on create. +func (u *HarnessConfigUpsert) UpdateCreatedBy() *HarnessConfigUpsert { + u.SetExcluded(harnessconfig.FieldCreatedBy) + return u +} + +// ClearCreatedBy clears the value of the "created_by" field. +func (u *HarnessConfigUpsert) ClearCreatedBy() *HarnessConfigUpsert { + u.SetNull(harnessconfig.FieldCreatedBy) + return u +} + +// SetUpdatedBy sets the "updated_by" field. +func (u *HarnessConfigUpsert) SetUpdatedBy(v string) *HarnessConfigUpsert { + u.Set(harnessconfig.FieldUpdatedBy, v) + return u +} + +// UpdateUpdatedBy sets the "updated_by" field to the value that was provided on create. +func (u *HarnessConfigUpsert) UpdateUpdatedBy() *HarnessConfigUpsert { + u.SetExcluded(harnessconfig.FieldUpdatedBy) + return u +} + +// ClearUpdatedBy clears the value of the "updated_by" field. +func (u *HarnessConfigUpsert) ClearUpdatedBy() *HarnessConfigUpsert { + u.SetNull(harnessconfig.FieldUpdatedBy) + return u +} + +// SetVisibility sets the "visibility" field. +func (u *HarnessConfigUpsert) SetVisibility(v string) *HarnessConfigUpsert { + u.Set(harnessconfig.FieldVisibility, v) + return u +} + +// UpdateVisibility sets the "visibility" field to the value that was provided on create. +func (u *HarnessConfigUpsert) UpdateVisibility() *HarnessConfigUpsert { + u.SetExcluded(harnessconfig.FieldVisibility) + return u +} + +// SetUpdated sets the "updated" field. +func (u *HarnessConfigUpsert) SetUpdated(v time.Time) *HarnessConfigUpsert { + u.Set(harnessconfig.FieldUpdated, v) + return u +} + +// UpdateUpdated sets the "updated" field to the value that was provided on create. +func (u *HarnessConfigUpsert) UpdateUpdated() *HarnessConfigUpsert { + u.SetExcluded(harnessconfig.FieldUpdated) + return u +} + +// UpdateNewValues updates the mutable fields using the new values that were set on create except the ID field. +// Using this option is equivalent to using: +// +// client.HarnessConfig.Create(). +// OnConflict( +// sql.ResolveWithNewValues(), +// sql.ResolveWith(func(u *sql.UpdateSet) { +// u.SetIgnore(harnessconfig.FieldID) +// }), +// ). +// Exec(ctx) +func (u *HarnessConfigUpsertOne) UpdateNewValues() *HarnessConfigUpsertOne { + u.create.conflict = append(u.create.conflict, sql.ResolveWithNewValues()) + u.create.conflict = append(u.create.conflict, sql.ResolveWith(func(s *sql.UpdateSet) { + if _, exists := u.create.mutation.ID(); exists { + s.SetIgnore(harnessconfig.FieldID) + } + if _, exists := u.create.mutation.Created(); exists { + s.SetIgnore(harnessconfig.FieldCreated) + } + })) + return u +} + +// Ignore sets each column to itself in case of conflict. +// Using this option is equivalent to using: +// +// client.HarnessConfig.Create(). +// OnConflict(sql.ResolveWithIgnore()). +// Exec(ctx) +func (u *HarnessConfigUpsertOne) Ignore() *HarnessConfigUpsertOne { + u.create.conflict = append(u.create.conflict, sql.ResolveWithIgnore()) + return u +} + +// DoNothing configures the conflict_action to `DO NOTHING`. +// Supported only by SQLite and PostgreSQL. +func (u *HarnessConfigUpsertOne) DoNothing() *HarnessConfigUpsertOne { + u.create.conflict = append(u.create.conflict, sql.DoNothing()) + return u +} + +// Update allows overriding fields `UPDATE` values. See the HarnessConfigCreate.OnConflict +// documentation for more info. +func (u *HarnessConfigUpsertOne) Update(set func(*HarnessConfigUpsert)) *HarnessConfigUpsertOne { + u.create.conflict = append(u.create.conflict, sql.ResolveWith(func(update *sql.UpdateSet) { + set(&HarnessConfigUpsert{UpdateSet: update}) + })) + return u +} + +// SetName sets the "name" field. +func (u *HarnessConfigUpsertOne) SetName(v string) *HarnessConfigUpsertOne { + return u.Update(func(s *HarnessConfigUpsert) { + s.SetName(v) + }) +} + +// UpdateName sets the "name" field to the value that was provided on create. +func (u *HarnessConfigUpsertOne) UpdateName() *HarnessConfigUpsertOne { + return u.Update(func(s *HarnessConfigUpsert) { + s.UpdateName() + }) +} + +// SetSlug sets the "slug" field. +func (u *HarnessConfigUpsertOne) SetSlug(v string) *HarnessConfigUpsertOne { + return u.Update(func(s *HarnessConfigUpsert) { + s.SetSlug(v) + }) +} + +// UpdateSlug sets the "slug" field to the value that was provided on create. +func (u *HarnessConfigUpsertOne) UpdateSlug() *HarnessConfigUpsertOne { + return u.Update(func(s *HarnessConfigUpsert) { + s.UpdateSlug() + }) +} + +// SetDisplayName sets the "display_name" field. +func (u *HarnessConfigUpsertOne) SetDisplayName(v string) *HarnessConfigUpsertOne { + return u.Update(func(s *HarnessConfigUpsert) { + s.SetDisplayName(v) + }) +} + +// UpdateDisplayName sets the "display_name" field to the value that was provided on create. +func (u *HarnessConfigUpsertOne) UpdateDisplayName() *HarnessConfigUpsertOne { + return u.Update(func(s *HarnessConfigUpsert) { + s.UpdateDisplayName() + }) +} + +// ClearDisplayName clears the value of the "display_name" field. +func (u *HarnessConfigUpsertOne) ClearDisplayName() *HarnessConfigUpsertOne { + return u.Update(func(s *HarnessConfigUpsert) { + s.ClearDisplayName() + }) +} + +// SetDescription sets the "description" field. +func (u *HarnessConfigUpsertOne) SetDescription(v string) *HarnessConfigUpsertOne { + return u.Update(func(s *HarnessConfigUpsert) { + s.SetDescription(v) + }) +} + +// UpdateDescription sets the "description" field to the value that was provided on create. +func (u *HarnessConfigUpsertOne) UpdateDescription() *HarnessConfigUpsertOne { + return u.Update(func(s *HarnessConfigUpsert) { + s.UpdateDescription() + }) +} + +// ClearDescription clears the value of the "description" field. +func (u *HarnessConfigUpsertOne) ClearDescription() *HarnessConfigUpsertOne { + return u.Update(func(s *HarnessConfigUpsert) { + s.ClearDescription() + }) +} + +// SetHarness sets the "harness" field. +func (u *HarnessConfigUpsertOne) SetHarness(v string) *HarnessConfigUpsertOne { + return u.Update(func(s *HarnessConfigUpsert) { + s.SetHarness(v) + }) +} + +// UpdateHarness sets the "harness" field to the value that was provided on create. +func (u *HarnessConfigUpsertOne) UpdateHarness() *HarnessConfigUpsertOne { + return u.Update(func(s *HarnessConfigUpsert) { + s.UpdateHarness() + }) +} + +// SetConfig sets the "config" field. +func (u *HarnessConfigUpsertOne) SetConfig(v string) *HarnessConfigUpsertOne { + return u.Update(func(s *HarnessConfigUpsert) { + s.SetConfig(v) + }) +} + +// UpdateConfig sets the "config" field to the value that was provided on create. +func (u *HarnessConfigUpsertOne) UpdateConfig() *HarnessConfigUpsertOne { + return u.Update(func(s *HarnessConfigUpsert) { + s.UpdateConfig() + }) +} + +// ClearConfig clears the value of the "config" field. +func (u *HarnessConfigUpsertOne) ClearConfig() *HarnessConfigUpsertOne { + return u.Update(func(s *HarnessConfigUpsert) { + s.ClearConfig() + }) +} + +// SetContentHash sets the "content_hash" field. +func (u *HarnessConfigUpsertOne) SetContentHash(v string) *HarnessConfigUpsertOne { + return u.Update(func(s *HarnessConfigUpsert) { + s.SetContentHash(v) + }) +} + +// UpdateContentHash sets the "content_hash" field to the value that was provided on create. +func (u *HarnessConfigUpsertOne) UpdateContentHash() *HarnessConfigUpsertOne { + return u.Update(func(s *HarnessConfigUpsert) { + s.UpdateContentHash() + }) +} + +// ClearContentHash clears the value of the "content_hash" field. +func (u *HarnessConfigUpsertOne) ClearContentHash() *HarnessConfigUpsertOne { + return u.Update(func(s *HarnessConfigUpsert) { + s.ClearContentHash() + }) +} + +// SetScope sets the "scope" field. +func (u *HarnessConfigUpsertOne) SetScope(v string) *HarnessConfigUpsertOne { + return u.Update(func(s *HarnessConfigUpsert) { + s.SetScope(v) + }) +} + +// UpdateScope sets the "scope" field to the value that was provided on create. +func (u *HarnessConfigUpsertOne) UpdateScope() *HarnessConfigUpsertOne { + return u.Update(func(s *HarnessConfigUpsert) { + s.UpdateScope() + }) +} + +// SetScopeID sets the "scope_id" field. +func (u *HarnessConfigUpsertOne) SetScopeID(v string) *HarnessConfigUpsertOne { + return u.Update(func(s *HarnessConfigUpsert) { + s.SetScopeID(v) + }) +} + +// UpdateScopeID sets the "scope_id" field to the value that was provided on create. +func (u *HarnessConfigUpsertOne) UpdateScopeID() *HarnessConfigUpsertOne { + return u.Update(func(s *HarnessConfigUpsert) { + s.UpdateScopeID() + }) +} + +// ClearScopeID clears the value of the "scope_id" field. +func (u *HarnessConfigUpsertOne) ClearScopeID() *HarnessConfigUpsertOne { + return u.Update(func(s *HarnessConfigUpsert) { + s.ClearScopeID() + }) +} + +// SetStorageURI sets the "storage_uri" field. +func (u *HarnessConfigUpsertOne) SetStorageURI(v string) *HarnessConfigUpsertOne { + return u.Update(func(s *HarnessConfigUpsert) { + s.SetStorageURI(v) + }) +} + +// UpdateStorageURI sets the "storage_uri" field to the value that was provided on create. +func (u *HarnessConfigUpsertOne) UpdateStorageURI() *HarnessConfigUpsertOne { + return u.Update(func(s *HarnessConfigUpsert) { + s.UpdateStorageURI() + }) +} + +// ClearStorageURI clears the value of the "storage_uri" field. +func (u *HarnessConfigUpsertOne) ClearStorageURI() *HarnessConfigUpsertOne { + return u.Update(func(s *HarnessConfigUpsert) { + s.ClearStorageURI() + }) +} + +// SetStorageBucket sets the "storage_bucket" field. +func (u *HarnessConfigUpsertOne) SetStorageBucket(v string) *HarnessConfigUpsertOne { + return u.Update(func(s *HarnessConfigUpsert) { + s.SetStorageBucket(v) + }) +} + +// UpdateStorageBucket sets the "storage_bucket" field to the value that was provided on create. +func (u *HarnessConfigUpsertOne) UpdateStorageBucket() *HarnessConfigUpsertOne { + return u.Update(func(s *HarnessConfigUpsert) { + s.UpdateStorageBucket() + }) +} + +// ClearStorageBucket clears the value of the "storage_bucket" field. +func (u *HarnessConfigUpsertOne) ClearStorageBucket() *HarnessConfigUpsertOne { + return u.Update(func(s *HarnessConfigUpsert) { + s.ClearStorageBucket() + }) +} + +// SetStoragePath sets the "storage_path" field. +func (u *HarnessConfigUpsertOne) SetStoragePath(v string) *HarnessConfigUpsertOne { + return u.Update(func(s *HarnessConfigUpsert) { + s.SetStoragePath(v) + }) +} + +// UpdateStoragePath sets the "storage_path" field to the value that was provided on create. +func (u *HarnessConfigUpsertOne) UpdateStoragePath() *HarnessConfigUpsertOne { + return u.Update(func(s *HarnessConfigUpsert) { + s.UpdateStoragePath() + }) +} + +// ClearStoragePath clears the value of the "storage_path" field. +func (u *HarnessConfigUpsertOne) ClearStoragePath() *HarnessConfigUpsertOne { + return u.Update(func(s *HarnessConfigUpsert) { + s.ClearStoragePath() + }) +} + +// SetFiles sets the "files" field. +func (u *HarnessConfigUpsertOne) SetFiles(v string) *HarnessConfigUpsertOne { + return u.Update(func(s *HarnessConfigUpsert) { + s.SetFiles(v) + }) +} + +// UpdateFiles sets the "files" field to the value that was provided on create. +func (u *HarnessConfigUpsertOne) UpdateFiles() *HarnessConfigUpsertOne { + return u.Update(func(s *HarnessConfigUpsert) { + s.UpdateFiles() + }) +} + +// ClearFiles clears the value of the "files" field. +func (u *HarnessConfigUpsertOne) ClearFiles() *HarnessConfigUpsertOne { + return u.Update(func(s *HarnessConfigUpsert) { + s.ClearFiles() + }) +} + +// SetStatus sets the "status" field. +func (u *HarnessConfigUpsertOne) SetStatus(v harnessconfig.Status) *HarnessConfigUpsertOne { + return u.Update(func(s *HarnessConfigUpsert) { + s.SetStatus(v) + }) +} + +// UpdateStatus sets the "status" field to the value that was provided on create. +func (u *HarnessConfigUpsertOne) UpdateStatus() *HarnessConfigUpsertOne { + return u.Update(func(s *HarnessConfigUpsert) { + s.UpdateStatus() + }) +} + +// SetOwnerID sets the "owner_id" field. +func (u *HarnessConfigUpsertOne) SetOwnerID(v string) *HarnessConfigUpsertOne { + return u.Update(func(s *HarnessConfigUpsert) { + s.SetOwnerID(v) + }) +} + +// UpdateOwnerID sets the "owner_id" field to the value that was provided on create. +func (u *HarnessConfigUpsertOne) UpdateOwnerID() *HarnessConfigUpsertOne { + return u.Update(func(s *HarnessConfigUpsert) { + s.UpdateOwnerID() + }) +} + +// ClearOwnerID clears the value of the "owner_id" field. +func (u *HarnessConfigUpsertOne) ClearOwnerID() *HarnessConfigUpsertOne { + return u.Update(func(s *HarnessConfigUpsert) { + s.ClearOwnerID() + }) +} + +// SetCreatedBy sets the "created_by" field. +func (u *HarnessConfigUpsertOne) SetCreatedBy(v string) *HarnessConfigUpsertOne { + return u.Update(func(s *HarnessConfigUpsert) { + s.SetCreatedBy(v) + }) +} + +// UpdateCreatedBy sets the "created_by" field to the value that was provided on create. +func (u *HarnessConfigUpsertOne) UpdateCreatedBy() *HarnessConfigUpsertOne { + return u.Update(func(s *HarnessConfigUpsert) { + s.UpdateCreatedBy() + }) +} + +// ClearCreatedBy clears the value of the "created_by" field. +func (u *HarnessConfigUpsertOne) ClearCreatedBy() *HarnessConfigUpsertOne { + return u.Update(func(s *HarnessConfigUpsert) { + s.ClearCreatedBy() + }) +} + +// SetUpdatedBy sets the "updated_by" field. +func (u *HarnessConfigUpsertOne) SetUpdatedBy(v string) *HarnessConfigUpsertOne { + return u.Update(func(s *HarnessConfigUpsert) { + s.SetUpdatedBy(v) + }) +} + +// UpdateUpdatedBy sets the "updated_by" field to the value that was provided on create. +func (u *HarnessConfigUpsertOne) UpdateUpdatedBy() *HarnessConfigUpsertOne { + return u.Update(func(s *HarnessConfigUpsert) { + s.UpdateUpdatedBy() + }) +} + +// ClearUpdatedBy clears the value of the "updated_by" field. +func (u *HarnessConfigUpsertOne) ClearUpdatedBy() *HarnessConfigUpsertOne { + return u.Update(func(s *HarnessConfigUpsert) { + s.ClearUpdatedBy() + }) +} + +// SetVisibility sets the "visibility" field. +func (u *HarnessConfigUpsertOne) SetVisibility(v string) *HarnessConfigUpsertOne { + return u.Update(func(s *HarnessConfigUpsert) { + s.SetVisibility(v) + }) +} + +// UpdateVisibility sets the "visibility" field to the value that was provided on create. +func (u *HarnessConfigUpsertOne) UpdateVisibility() *HarnessConfigUpsertOne { + return u.Update(func(s *HarnessConfigUpsert) { + s.UpdateVisibility() + }) +} + +// SetUpdated sets the "updated" field. +func (u *HarnessConfigUpsertOne) SetUpdated(v time.Time) *HarnessConfigUpsertOne { + return u.Update(func(s *HarnessConfigUpsert) { + s.SetUpdated(v) + }) +} + +// UpdateUpdated sets the "updated" field to the value that was provided on create. +func (u *HarnessConfigUpsertOne) UpdateUpdated() *HarnessConfigUpsertOne { + return u.Update(func(s *HarnessConfigUpsert) { + s.UpdateUpdated() + }) +} + +// Exec executes the query. +func (u *HarnessConfigUpsertOne) Exec(ctx context.Context) error { + if len(u.create.conflict) == 0 { + return errors.New("ent: missing options for HarnessConfigCreate.OnConflict") + } + return u.create.Exec(ctx) +} + +// ExecX is like Exec, but panics if an error occurs. +func (u *HarnessConfigUpsertOne) ExecX(ctx context.Context) { + if err := u.create.Exec(ctx); err != nil { + panic(err) + } +} + +// Exec executes the UPSERT query and returns the inserted/updated ID. +func (u *HarnessConfigUpsertOne) ID(ctx context.Context) (id uuid.UUID, err error) { + if u.create.driver.Dialect() == dialect.MySQL { + // In case of "ON CONFLICT", there is no way to get back non-numeric ID + // fields from the database since MySQL does not support the RETURNING clause. + return id, errors.New("ent: HarnessConfigUpsertOne.ID is not supported by MySQL driver. Use HarnessConfigUpsertOne.Exec instead") + } + node, err := u.create.Save(ctx) + if err != nil { + return id, err + } + return node.ID, nil +} + +// IDX is like ID, but panics if an error occurs. +func (u *HarnessConfigUpsertOne) IDX(ctx context.Context) uuid.UUID { + id, err := u.ID(ctx) + if err != nil { + panic(err) + } + return id +} + +// HarnessConfigCreateBulk is the builder for creating many HarnessConfig entities in bulk. +type HarnessConfigCreateBulk struct { + config + err error + builders []*HarnessConfigCreate + conflict []sql.ConflictOption +} + +// Save creates the HarnessConfig entities in the database. +func (_c *HarnessConfigCreateBulk) Save(ctx context.Context) ([]*HarnessConfig, error) { + if _c.err != nil { + return nil, _c.err + } + specs := make([]*sqlgraph.CreateSpec, len(_c.builders)) + nodes := make([]*HarnessConfig, len(_c.builders)) + mutators := make([]Mutator, len(_c.builders)) + for i := range _c.builders { + func(i int, root context.Context) { + builder := _c.builders[i] + builder.defaults() + var mut Mutator = MutateFunc(func(ctx context.Context, m Mutation) (Value, error) { + mutation, ok := m.(*HarnessConfigMutation) + if !ok { + return nil, fmt.Errorf("unexpected mutation type %T", m) + } + if err := builder.check(); err != nil { + return nil, err + } + builder.mutation = mutation + var err error + nodes[i], specs[i] = builder.createSpec() + if i < len(mutators)-1 { + _, err = mutators[i+1].Mutate(root, _c.builders[i+1].mutation) + } else { + spec := &sqlgraph.BatchCreateSpec{Nodes: specs} + spec.OnConflict = _c.conflict + // Invoke the actual operation on the latest mutation in the chain. + if err = sqlgraph.BatchCreate(ctx, _c.driver, spec); err != nil { + if sqlgraph.IsConstraintError(err) { + err = &ConstraintError{msg: err.Error(), wrap: err} + } + } + } + if err != nil { + return nil, err + } + mutation.id = &nodes[i].ID + mutation.done = true + return nodes[i], nil + }) + for i := len(builder.hooks) - 1; i >= 0; i-- { + mut = builder.hooks[i](mut) + } + mutators[i] = mut + }(i, ctx) + } + if len(mutators) > 0 { + if _, err := mutators[0].Mutate(ctx, _c.builders[0].mutation); err != nil { + return nil, err + } + } + return nodes, nil +} + +// SaveX is like Save, but panics if an error occurs. +func (_c *HarnessConfigCreateBulk) SaveX(ctx context.Context) []*HarnessConfig { + v, err := _c.Save(ctx) + if err != nil { + panic(err) + } + return v +} + +// Exec executes the query. +func (_c *HarnessConfigCreateBulk) Exec(ctx context.Context) error { + _, err := _c.Save(ctx) + return err +} + +// ExecX is like Exec, but panics if an error occurs. +func (_c *HarnessConfigCreateBulk) ExecX(ctx context.Context) { + if err := _c.Exec(ctx); err != nil { + panic(err) + } +} + +// OnConflict allows configuring the `ON CONFLICT` / `ON DUPLICATE KEY` clause +// of the `INSERT` statement. For example: +// +// client.HarnessConfig.CreateBulk(builders...). +// OnConflict( +// // Update the row with the new values +// // the was proposed for insertion. +// sql.ResolveWithNewValues(), +// ). +// // Override some of the fields with custom +// // update values. +// Update(func(u *ent.HarnessConfigUpsert) { +// SetName(v+v). +// }). +// Exec(ctx) +func (_c *HarnessConfigCreateBulk) OnConflict(opts ...sql.ConflictOption) *HarnessConfigUpsertBulk { + _c.conflict = opts + return &HarnessConfigUpsertBulk{ + create: _c, + } +} + +// OnConflictColumns calls `OnConflict` and configures the columns +// as conflict target. Using this option is equivalent to using: +// +// client.HarnessConfig.Create(). +// OnConflict(sql.ConflictColumns(columns...)). +// Exec(ctx) +func (_c *HarnessConfigCreateBulk) OnConflictColumns(columns ...string) *HarnessConfigUpsertBulk { + _c.conflict = append(_c.conflict, sql.ConflictColumns(columns...)) + return &HarnessConfigUpsertBulk{ + create: _c, + } +} + +// HarnessConfigUpsertBulk is the builder for "upsert"-ing +// a bulk of HarnessConfig nodes. +type HarnessConfigUpsertBulk struct { + create *HarnessConfigCreateBulk +} + +// UpdateNewValues updates the mutable fields using the new values that +// were set on create. Using this option is equivalent to using: +// +// client.HarnessConfig.Create(). +// OnConflict( +// sql.ResolveWithNewValues(), +// sql.ResolveWith(func(u *sql.UpdateSet) { +// u.SetIgnore(harnessconfig.FieldID) +// }), +// ). +// Exec(ctx) +func (u *HarnessConfigUpsertBulk) UpdateNewValues() *HarnessConfigUpsertBulk { + u.create.conflict = append(u.create.conflict, sql.ResolveWithNewValues()) + u.create.conflict = append(u.create.conflict, sql.ResolveWith(func(s *sql.UpdateSet) { + for _, b := range u.create.builders { + if _, exists := b.mutation.ID(); exists { + s.SetIgnore(harnessconfig.FieldID) + } + if _, exists := b.mutation.Created(); exists { + s.SetIgnore(harnessconfig.FieldCreated) + } + } + })) + return u +} + +// Ignore sets each column to itself in case of conflict. +// Using this option is equivalent to using: +// +// client.HarnessConfig.Create(). +// OnConflict(sql.ResolveWithIgnore()). +// Exec(ctx) +func (u *HarnessConfigUpsertBulk) Ignore() *HarnessConfigUpsertBulk { + u.create.conflict = append(u.create.conflict, sql.ResolveWithIgnore()) + return u +} + +// DoNothing configures the conflict_action to `DO NOTHING`. +// Supported only by SQLite and PostgreSQL. +func (u *HarnessConfigUpsertBulk) DoNothing() *HarnessConfigUpsertBulk { + u.create.conflict = append(u.create.conflict, sql.DoNothing()) + return u +} + +// Update allows overriding fields `UPDATE` values. See the HarnessConfigCreateBulk.OnConflict +// documentation for more info. +func (u *HarnessConfigUpsertBulk) Update(set func(*HarnessConfigUpsert)) *HarnessConfigUpsertBulk { + u.create.conflict = append(u.create.conflict, sql.ResolveWith(func(update *sql.UpdateSet) { + set(&HarnessConfigUpsert{UpdateSet: update}) + })) + return u +} + +// SetName sets the "name" field. +func (u *HarnessConfigUpsertBulk) SetName(v string) *HarnessConfigUpsertBulk { + return u.Update(func(s *HarnessConfigUpsert) { + s.SetName(v) + }) +} + +// UpdateName sets the "name" field to the value that was provided on create. +func (u *HarnessConfigUpsertBulk) UpdateName() *HarnessConfigUpsertBulk { + return u.Update(func(s *HarnessConfigUpsert) { + s.UpdateName() + }) +} + +// SetSlug sets the "slug" field. +func (u *HarnessConfigUpsertBulk) SetSlug(v string) *HarnessConfigUpsertBulk { + return u.Update(func(s *HarnessConfigUpsert) { + s.SetSlug(v) + }) +} + +// UpdateSlug sets the "slug" field to the value that was provided on create. +func (u *HarnessConfigUpsertBulk) UpdateSlug() *HarnessConfigUpsertBulk { + return u.Update(func(s *HarnessConfigUpsert) { + s.UpdateSlug() + }) +} + +// SetDisplayName sets the "display_name" field. +func (u *HarnessConfigUpsertBulk) SetDisplayName(v string) *HarnessConfigUpsertBulk { + return u.Update(func(s *HarnessConfigUpsert) { + s.SetDisplayName(v) + }) +} + +// UpdateDisplayName sets the "display_name" field to the value that was provided on create. +func (u *HarnessConfigUpsertBulk) UpdateDisplayName() *HarnessConfigUpsertBulk { + return u.Update(func(s *HarnessConfigUpsert) { + s.UpdateDisplayName() + }) +} + +// ClearDisplayName clears the value of the "display_name" field. +func (u *HarnessConfigUpsertBulk) ClearDisplayName() *HarnessConfigUpsertBulk { + return u.Update(func(s *HarnessConfigUpsert) { + s.ClearDisplayName() + }) +} + +// SetDescription sets the "description" field. +func (u *HarnessConfigUpsertBulk) SetDescription(v string) *HarnessConfigUpsertBulk { + return u.Update(func(s *HarnessConfigUpsert) { + s.SetDescription(v) + }) +} + +// UpdateDescription sets the "description" field to the value that was provided on create. +func (u *HarnessConfigUpsertBulk) UpdateDescription() *HarnessConfigUpsertBulk { + return u.Update(func(s *HarnessConfigUpsert) { + s.UpdateDescription() + }) +} + +// ClearDescription clears the value of the "description" field. +func (u *HarnessConfigUpsertBulk) ClearDescription() *HarnessConfigUpsertBulk { + return u.Update(func(s *HarnessConfigUpsert) { + s.ClearDescription() + }) +} + +// SetHarness sets the "harness" field. +func (u *HarnessConfigUpsertBulk) SetHarness(v string) *HarnessConfigUpsertBulk { + return u.Update(func(s *HarnessConfigUpsert) { + s.SetHarness(v) + }) +} + +// UpdateHarness sets the "harness" field to the value that was provided on create. +func (u *HarnessConfigUpsertBulk) UpdateHarness() *HarnessConfigUpsertBulk { + return u.Update(func(s *HarnessConfigUpsert) { + s.UpdateHarness() + }) +} + +// SetConfig sets the "config" field. +func (u *HarnessConfigUpsertBulk) SetConfig(v string) *HarnessConfigUpsertBulk { + return u.Update(func(s *HarnessConfigUpsert) { + s.SetConfig(v) + }) +} + +// UpdateConfig sets the "config" field to the value that was provided on create. +func (u *HarnessConfigUpsertBulk) UpdateConfig() *HarnessConfigUpsertBulk { + return u.Update(func(s *HarnessConfigUpsert) { + s.UpdateConfig() + }) +} + +// ClearConfig clears the value of the "config" field. +func (u *HarnessConfigUpsertBulk) ClearConfig() *HarnessConfigUpsertBulk { + return u.Update(func(s *HarnessConfigUpsert) { + s.ClearConfig() + }) +} + +// SetContentHash sets the "content_hash" field. +func (u *HarnessConfigUpsertBulk) SetContentHash(v string) *HarnessConfigUpsertBulk { + return u.Update(func(s *HarnessConfigUpsert) { + s.SetContentHash(v) + }) +} + +// UpdateContentHash sets the "content_hash" field to the value that was provided on create. +func (u *HarnessConfigUpsertBulk) UpdateContentHash() *HarnessConfigUpsertBulk { + return u.Update(func(s *HarnessConfigUpsert) { + s.UpdateContentHash() + }) +} + +// ClearContentHash clears the value of the "content_hash" field. +func (u *HarnessConfigUpsertBulk) ClearContentHash() *HarnessConfigUpsertBulk { + return u.Update(func(s *HarnessConfigUpsert) { + s.ClearContentHash() + }) +} + +// SetScope sets the "scope" field. +func (u *HarnessConfigUpsertBulk) SetScope(v string) *HarnessConfigUpsertBulk { + return u.Update(func(s *HarnessConfigUpsert) { + s.SetScope(v) + }) +} + +// UpdateScope sets the "scope" field to the value that was provided on create. +func (u *HarnessConfigUpsertBulk) UpdateScope() *HarnessConfigUpsertBulk { + return u.Update(func(s *HarnessConfigUpsert) { + s.UpdateScope() + }) +} + +// SetScopeID sets the "scope_id" field. +func (u *HarnessConfigUpsertBulk) SetScopeID(v string) *HarnessConfigUpsertBulk { + return u.Update(func(s *HarnessConfigUpsert) { + s.SetScopeID(v) + }) +} + +// UpdateScopeID sets the "scope_id" field to the value that was provided on create. +func (u *HarnessConfigUpsertBulk) UpdateScopeID() *HarnessConfigUpsertBulk { + return u.Update(func(s *HarnessConfigUpsert) { + s.UpdateScopeID() + }) +} + +// ClearScopeID clears the value of the "scope_id" field. +func (u *HarnessConfigUpsertBulk) ClearScopeID() *HarnessConfigUpsertBulk { + return u.Update(func(s *HarnessConfigUpsert) { + s.ClearScopeID() + }) +} + +// SetStorageURI sets the "storage_uri" field. +func (u *HarnessConfigUpsertBulk) SetStorageURI(v string) *HarnessConfigUpsertBulk { + return u.Update(func(s *HarnessConfigUpsert) { + s.SetStorageURI(v) + }) +} + +// UpdateStorageURI sets the "storage_uri" field to the value that was provided on create. +func (u *HarnessConfigUpsertBulk) UpdateStorageURI() *HarnessConfigUpsertBulk { + return u.Update(func(s *HarnessConfigUpsert) { + s.UpdateStorageURI() + }) +} + +// ClearStorageURI clears the value of the "storage_uri" field. +func (u *HarnessConfigUpsertBulk) ClearStorageURI() *HarnessConfigUpsertBulk { + return u.Update(func(s *HarnessConfigUpsert) { + s.ClearStorageURI() + }) +} + +// SetStorageBucket sets the "storage_bucket" field. +func (u *HarnessConfigUpsertBulk) SetStorageBucket(v string) *HarnessConfigUpsertBulk { + return u.Update(func(s *HarnessConfigUpsert) { + s.SetStorageBucket(v) + }) +} + +// UpdateStorageBucket sets the "storage_bucket" field to the value that was provided on create. +func (u *HarnessConfigUpsertBulk) UpdateStorageBucket() *HarnessConfigUpsertBulk { + return u.Update(func(s *HarnessConfigUpsert) { + s.UpdateStorageBucket() + }) +} + +// ClearStorageBucket clears the value of the "storage_bucket" field. +func (u *HarnessConfigUpsertBulk) ClearStorageBucket() *HarnessConfigUpsertBulk { + return u.Update(func(s *HarnessConfigUpsert) { + s.ClearStorageBucket() + }) +} + +// SetStoragePath sets the "storage_path" field. +func (u *HarnessConfigUpsertBulk) SetStoragePath(v string) *HarnessConfigUpsertBulk { + return u.Update(func(s *HarnessConfigUpsert) { + s.SetStoragePath(v) + }) +} + +// UpdateStoragePath sets the "storage_path" field to the value that was provided on create. +func (u *HarnessConfigUpsertBulk) UpdateStoragePath() *HarnessConfigUpsertBulk { + return u.Update(func(s *HarnessConfigUpsert) { + s.UpdateStoragePath() + }) +} + +// ClearStoragePath clears the value of the "storage_path" field. +func (u *HarnessConfigUpsertBulk) ClearStoragePath() *HarnessConfigUpsertBulk { + return u.Update(func(s *HarnessConfigUpsert) { + s.ClearStoragePath() + }) +} + +// SetFiles sets the "files" field. +func (u *HarnessConfigUpsertBulk) SetFiles(v string) *HarnessConfigUpsertBulk { + return u.Update(func(s *HarnessConfigUpsert) { + s.SetFiles(v) + }) +} + +// UpdateFiles sets the "files" field to the value that was provided on create. +func (u *HarnessConfigUpsertBulk) UpdateFiles() *HarnessConfigUpsertBulk { + return u.Update(func(s *HarnessConfigUpsert) { + s.UpdateFiles() + }) +} + +// ClearFiles clears the value of the "files" field. +func (u *HarnessConfigUpsertBulk) ClearFiles() *HarnessConfigUpsertBulk { + return u.Update(func(s *HarnessConfigUpsert) { + s.ClearFiles() + }) +} + +// SetStatus sets the "status" field. +func (u *HarnessConfigUpsertBulk) SetStatus(v harnessconfig.Status) *HarnessConfigUpsertBulk { + return u.Update(func(s *HarnessConfigUpsert) { + s.SetStatus(v) + }) +} + +// UpdateStatus sets the "status" field to the value that was provided on create. +func (u *HarnessConfigUpsertBulk) UpdateStatus() *HarnessConfigUpsertBulk { + return u.Update(func(s *HarnessConfigUpsert) { + s.UpdateStatus() + }) +} + +// SetOwnerID sets the "owner_id" field. +func (u *HarnessConfigUpsertBulk) SetOwnerID(v string) *HarnessConfigUpsertBulk { + return u.Update(func(s *HarnessConfigUpsert) { + s.SetOwnerID(v) + }) +} + +// UpdateOwnerID sets the "owner_id" field to the value that was provided on create. +func (u *HarnessConfigUpsertBulk) UpdateOwnerID() *HarnessConfigUpsertBulk { + return u.Update(func(s *HarnessConfigUpsert) { + s.UpdateOwnerID() + }) +} + +// ClearOwnerID clears the value of the "owner_id" field. +func (u *HarnessConfigUpsertBulk) ClearOwnerID() *HarnessConfigUpsertBulk { + return u.Update(func(s *HarnessConfigUpsert) { + s.ClearOwnerID() + }) +} + +// SetCreatedBy sets the "created_by" field. +func (u *HarnessConfigUpsertBulk) SetCreatedBy(v string) *HarnessConfigUpsertBulk { + return u.Update(func(s *HarnessConfigUpsert) { + s.SetCreatedBy(v) + }) +} + +// UpdateCreatedBy sets the "created_by" field to the value that was provided on create. +func (u *HarnessConfigUpsertBulk) UpdateCreatedBy() *HarnessConfigUpsertBulk { + return u.Update(func(s *HarnessConfigUpsert) { + s.UpdateCreatedBy() + }) +} + +// ClearCreatedBy clears the value of the "created_by" field. +func (u *HarnessConfigUpsertBulk) ClearCreatedBy() *HarnessConfigUpsertBulk { + return u.Update(func(s *HarnessConfigUpsert) { + s.ClearCreatedBy() + }) +} + +// SetUpdatedBy sets the "updated_by" field. +func (u *HarnessConfigUpsertBulk) SetUpdatedBy(v string) *HarnessConfigUpsertBulk { + return u.Update(func(s *HarnessConfigUpsert) { + s.SetUpdatedBy(v) + }) +} + +// UpdateUpdatedBy sets the "updated_by" field to the value that was provided on create. +func (u *HarnessConfigUpsertBulk) UpdateUpdatedBy() *HarnessConfigUpsertBulk { + return u.Update(func(s *HarnessConfigUpsert) { + s.UpdateUpdatedBy() + }) +} + +// ClearUpdatedBy clears the value of the "updated_by" field. +func (u *HarnessConfigUpsertBulk) ClearUpdatedBy() *HarnessConfigUpsertBulk { + return u.Update(func(s *HarnessConfigUpsert) { + s.ClearUpdatedBy() + }) +} + +// SetVisibility sets the "visibility" field. +func (u *HarnessConfigUpsertBulk) SetVisibility(v string) *HarnessConfigUpsertBulk { + return u.Update(func(s *HarnessConfigUpsert) { + s.SetVisibility(v) + }) +} + +// UpdateVisibility sets the "visibility" field to the value that was provided on create. +func (u *HarnessConfigUpsertBulk) UpdateVisibility() *HarnessConfigUpsertBulk { + return u.Update(func(s *HarnessConfigUpsert) { + s.UpdateVisibility() + }) +} + +// SetUpdated sets the "updated" field. +func (u *HarnessConfigUpsertBulk) SetUpdated(v time.Time) *HarnessConfigUpsertBulk { + return u.Update(func(s *HarnessConfigUpsert) { + s.SetUpdated(v) + }) +} + +// UpdateUpdated sets the "updated" field to the value that was provided on create. +func (u *HarnessConfigUpsertBulk) UpdateUpdated() *HarnessConfigUpsertBulk { + return u.Update(func(s *HarnessConfigUpsert) { + s.UpdateUpdated() + }) +} + +// Exec executes the query. +func (u *HarnessConfigUpsertBulk) Exec(ctx context.Context) error { + if u.create.err != nil { + return u.create.err + } + for i, b := range u.create.builders { + if len(b.conflict) != 0 { + return fmt.Errorf("ent: OnConflict was set for builder %d. Set it on the HarnessConfigCreateBulk instead", i) + } + } + if len(u.create.conflict) == 0 { + return errors.New("ent: missing options for HarnessConfigCreateBulk.OnConflict") + } + return u.create.Exec(ctx) +} + +// ExecX is like Exec, but panics if an error occurs. +func (u *HarnessConfigUpsertBulk) ExecX(ctx context.Context) { + if err := u.create.Exec(ctx); err != nil { + panic(err) + } +} diff --git a/pkg/ent/harnessconfig_delete.go b/pkg/ent/harnessconfig_delete.go new file mode 100644 index 000000000..1d63f7f2b --- /dev/null +++ b/pkg/ent/harnessconfig_delete.go @@ -0,0 +1,88 @@ +// Code generated by ent, DO NOT EDIT. + +package ent + +import ( + "context" + + "entgo.io/ent/dialect/sql" + "entgo.io/ent/dialect/sql/sqlgraph" + "entgo.io/ent/schema/field" + "github.com/GoogleCloudPlatform/scion/pkg/ent/harnessconfig" + "github.com/GoogleCloudPlatform/scion/pkg/ent/predicate" +) + +// HarnessConfigDelete is the builder for deleting a HarnessConfig entity. +type HarnessConfigDelete struct { + config + hooks []Hook + mutation *HarnessConfigMutation +} + +// Where appends a list predicates to the HarnessConfigDelete builder. +func (_d *HarnessConfigDelete) Where(ps ...predicate.HarnessConfig) *HarnessConfigDelete { + _d.mutation.Where(ps...) + return _d +} + +// Exec executes the deletion query and returns how many vertices were deleted. +func (_d *HarnessConfigDelete) Exec(ctx context.Context) (int, error) { + return withHooks(ctx, _d.sqlExec, _d.mutation, _d.hooks) +} + +// ExecX is like Exec, but panics if an error occurs. +func (_d *HarnessConfigDelete) ExecX(ctx context.Context) int { + n, err := _d.Exec(ctx) + if err != nil { + panic(err) + } + return n +} + +func (_d *HarnessConfigDelete) sqlExec(ctx context.Context) (int, error) { + _spec := sqlgraph.NewDeleteSpec(harnessconfig.Table, sqlgraph.NewFieldSpec(harnessconfig.FieldID, field.TypeUUID)) + if ps := _d.mutation.predicates; len(ps) > 0 { + _spec.Predicate = func(selector *sql.Selector) { + for i := range ps { + ps[i](selector) + } + } + } + affected, err := sqlgraph.DeleteNodes(ctx, _d.driver, _spec) + if err != nil && sqlgraph.IsConstraintError(err) { + err = &ConstraintError{msg: err.Error(), wrap: err} + } + _d.mutation.done = true + return affected, err +} + +// HarnessConfigDeleteOne is the builder for deleting a single HarnessConfig entity. +type HarnessConfigDeleteOne struct { + _d *HarnessConfigDelete +} + +// Where appends a list predicates to the HarnessConfigDelete builder. +func (_d *HarnessConfigDeleteOne) Where(ps ...predicate.HarnessConfig) *HarnessConfigDeleteOne { + _d._d.mutation.Where(ps...) + return _d +} + +// Exec executes the deletion query. +func (_d *HarnessConfigDeleteOne) Exec(ctx context.Context) error { + n, err := _d._d.Exec(ctx) + switch { + case err != nil: + return err + case n == 0: + return &NotFoundError{harnessconfig.Label} + default: + return nil + } +} + +// ExecX is like Exec, but panics if an error occurs. +func (_d *HarnessConfigDeleteOne) ExecX(ctx context.Context) { + if err := _d.Exec(ctx); err != nil { + panic(err) + } +} diff --git a/pkg/ent/harnessconfig_query.go b/pkg/ent/harnessconfig_query.go new file mode 100644 index 000000000..c297ad239 --- /dev/null +++ b/pkg/ent/harnessconfig_query.go @@ -0,0 +1,565 @@ +// Code generated by ent, DO NOT EDIT. + +package ent + +import ( + "context" + "fmt" + "math" + + "entgo.io/ent" + "entgo.io/ent/dialect" + "entgo.io/ent/dialect/sql" + "entgo.io/ent/dialect/sql/sqlgraph" + "entgo.io/ent/schema/field" + "github.com/GoogleCloudPlatform/scion/pkg/ent/harnessconfig" + "github.com/GoogleCloudPlatform/scion/pkg/ent/predicate" + "github.com/google/uuid" +) + +// HarnessConfigQuery is the builder for querying HarnessConfig entities. +type HarnessConfigQuery struct { + config + ctx *QueryContext + order []harnessconfig.OrderOption + inters []Interceptor + predicates []predicate.HarnessConfig + modifiers []func(*sql.Selector) + // intermediate query (i.e. traversal path). + sql *sql.Selector + path func(context.Context) (*sql.Selector, error) +} + +// Where adds a new predicate for the HarnessConfigQuery builder. +func (_q *HarnessConfigQuery) Where(ps ...predicate.HarnessConfig) *HarnessConfigQuery { + _q.predicates = append(_q.predicates, ps...) + return _q +} + +// Limit the number of records to be returned by this query. +func (_q *HarnessConfigQuery) Limit(limit int) *HarnessConfigQuery { + _q.ctx.Limit = &limit + return _q +} + +// Offset to start from. +func (_q *HarnessConfigQuery) Offset(offset int) *HarnessConfigQuery { + _q.ctx.Offset = &offset + return _q +} + +// Unique configures the query builder to filter duplicate records on query. +// By default, unique is set to true, and can be disabled using this method. +func (_q *HarnessConfigQuery) Unique(unique bool) *HarnessConfigQuery { + _q.ctx.Unique = &unique + return _q +} + +// Order specifies how the records should be ordered. +func (_q *HarnessConfigQuery) Order(o ...harnessconfig.OrderOption) *HarnessConfigQuery { + _q.order = append(_q.order, o...) + return _q +} + +// First returns the first HarnessConfig entity from the query. +// Returns a *NotFoundError when no HarnessConfig was found. +func (_q *HarnessConfigQuery) First(ctx context.Context) (*HarnessConfig, error) { + nodes, err := _q.Limit(1).All(setContextOp(ctx, _q.ctx, ent.OpQueryFirst)) + if err != nil { + return nil, err + } + if len(nodes) == 0 { + return nil, &NotFoundError{harnessconfig.Label} + } + return nodes[0], nil +} + +// FirstX is like First, but panics if an error occurs. +func (_q *HarnessConfigQuery) FirstX(ctx context.Context) *HarnessConfig { + node, err := _q.First(ctx) + if err != nil && !IsNotFound(err) { + panic(err) + } + return node +} + +// FirstID returns the first HarnessConfig ID from the query. +// Returns a *NotFoundError when no HarnessConfig ID was found. +func (_q *HarnessConfigQuery) FirstID(ctx context.Context) (id uuid.UUID, err error) { + var ids []uuid.UUID + if ids, err = _q.Limit(1).IDs(setContextOp(ctx, _q.ctx, ent.OpQueryFirstID)); err != nil { + return + } + if len(ids) == 0 { + err = &NotFoundError{harnessconfig.Label} + return + } + return ids[0], nil +} + +// FirstIDX is like FirstID, but panics if an error occurs. +func (_q *HarnessConfigQuery) FirstIDX(ctx context.Context) uuid.UUID { + id, err := _q.FirstID(ctx) + if err != nil && !IsNotFound(err) { + panic(err) + } + return id +} + +// Only returns a single HarnessConfig entity found by the query, ensuring it only returns one. +// Returns a *NotSingularError when more than one HarnessConfig entity is found. +// Returns a *NotFoundError when no HarnessConfig entities are found. +func (_q *HarnessConfigQuery) Only(ctx context.Context) (*HarnessConfig, error) { + nodes, err := _q.Limit(2).All(setContextOp(ctx, _q.ctx, ent.OpQueryOnly)) + if err != nil { + return nil, err + } + switch len(nodes) { + case 1: + return nodes[0], nil + case 0: + return nil, &NotFoundError{harnessconfig.Label} + default: + return nil, &NotSingularError{harnessconfig.Label} + } +} + +// OnlyX is like Only, but panics if an error occurs. +func (_q *HarnessConfigQuery) OnlyX(ctx context.Context) *HarnessConfig { + node, err := _q.Only(ctx) + if err != nil { + panic(err) + } + return node +} + +// OnlyID is like Only, but returns the only HarnessConfig ID in the query. +// Returns a *NotSingularError when more than one HarnessConfig ID is found. +// Returns a *NotFoundError when no entities are found. +func (_q *HarnessConfigQuery) OnlyID(ctx context.Context) (id uuid.UUID, err error) { + var ids []uuid.UUID + if ids, err = _q.Limit(2).IDs(setContextOp(ctx, _q.ctx, ent.OpQueryOnlyID)); err != nil { + return + } + switch len(ids) { + case 1: + id = ids[0] + case 0: + err = &NotFoundError{harnessconfig.Label} + default: + err = &NotSingularError{harnessconfig.Label} + } + return +} + +// OnlyIDX is like OnlyID, but panics if an error occurs. +func (_q *HarnessConfigQuery) OnlyIDX(ctx context.Context) uuid.UUID { + id, err := _q.OnlyID(ctx) + if err != nil { + panic(err) + } + return id +} + +// All executes the query and returns a list of HarnessConfigs. +func (_q *HarnessConfigQuery) All(ctx context.Context) ([]*HarnessConfig, error) { + ctx = setContextOp(ctx, _q.ctx, ent.OpQueryAll) + if err := _q.prepareQuery(ctx); err != nil { + return nil, err + } + qr := querierAll[[]*HarnessConfig, *HarnessConfigQuery]() + return withInterceptors[[]*HarnessConfig](ctx, _q, qr, _q.inters) +} + +// AllX is like All, but panics if an error occurs. +func (_q *HarnessConfigQuery) AllX(ctx context.Context) []*HarnessConfig { + nodes, err := _q.All(ctx) + if err != nil { + panic(err) + } + return nodes +} + +// IDs executes the query and returns a list of HarnessConfig IDs. +func (_q *HarnessConfigQuery) IDs(ctx context.Context) (ids []uuid.UUID, err error) { + if _q.ctx.Unique == nil && _q.path != nil { + _q.Unique(true) + } + ctx = setContextOp(ctx, _q.ctx, ent.OpQueryIDs) + if err = _q.Select(harnessconfig.FieldID).Scan(ctx, &ids); err != nil { + return nil, err + } + return ids, nil +} + +// IDsX is like IDs, but panics if an error occurs. +func (_q *HarnessConfigQuery) IDsX(ctx context.Context) []uuid.UUID { + ids, err := _q.IDs(ctx) + if err != nil { + panic(err) + } + return ids +} + +// Count returns the count of the given query. +func (_q *HarnessConfigQuery) Count(ctx context.Context) (int, error) { + ctx = setContextOp(ctx, _q.ctx, ent.OpQueryCount) + if err := _q.prepareQuery(ctx); err != nil { + return 0, err + } + return withInterceptors[int](ctx, _q, querierCount[*HarnessConfigQuery](), _q.inters) +} + +// CountX is like Count, but panics if an error occurs. +func (_q *HarnessConfigQuery) CountX(ctx context.Context) int { + count, err := _q.Count(ctx) + if err != nil { + panic(err) + } + return count +} + +// Exist returns true if the query has elements in the graph. +func (_q *HarnessConfigQuery) Exist(ctx context.Context) (bool, error) { + ctx = setContextOp(ctx, _q.ctx, ent.OpQueryExist) + switch _, err := _q.FirstID(ctx); { + case IsNotFound(err): + return false, nil + case err != nil: + return false, fmt.Errorf("ent: check existence: %w", err) + default: + return true, nil + } +} + +// ExistX is like Exist, but panics if an error occurs. +func (_q *HarnessConfigQuery) ExistX(ctx context.Context) bool { + exist, err := _q.Exist(ctx) + if err != nil { + panic(err) + } + return exist +} + +// Clone returns a duplicate of the HarnessConfigQuery builder, including all associated steps. It can be +// used to prepare common query builders and use them differently after the clone is made. +func (_q *HarnessConfigQuery) Clone() *HarnessConfigQuery { + if _q == nil { + return nil + } + return &HarnessConfigQuery{ + config: _q.config, + ctx: _q.ctx.Clone(), + order: append([]harnessconfig.OrderOption{}, _q.order...), + inters: append([]Interceptor{}, _q.inters...), + predicates: append([]predicate.HarnessConfig{}, _q.predicates...), + // clone intermediate query. + sql: _q.sql.Clone(), + path: _q.path, + } +} + +// GroupBy is used to group vertices by one or more fields/columns. +// It is often used with aggregate functions, like: count, max, mean, min, sum. +// +// Example: +// +// var v []struct { +// Name string `json:"name,omitempty"` +// Count int `json:"count,omitempty"` +// } +// +// client.HarnessConfig.Query(). +// GroupBy(harnessconfig.FieldName). +// Aggregate(ent.Count()). +// Scan(ctx, &v) +func (_q *HarnessConfigQuery) GroupBy(field string, fields ...string) *HarnessConfigGroupBy { + _q.ctx.Fields = append([]string{field}, fields...) + grbuild := &HarnessConfigGroupBy{build: _q} + grbuild.flds = &_q.ctx.Fields + grbuild.label = harnessconfig.Label + grbuild.scan = grbuild.Scan + return grbuild +} + +// Select allows the selection one or more fields/columns for the given query, +// instead of selecting all fields in the entity. +// +// Example: +// +// var v []struct { +// Name string `json:"name,omitempty"` +// } +// +// client.HarnessConfig.Query(). +// Select(harnessconfig.FieldName). +// Scan(ctx, &v) +func (_q *HarnessConfigQuery) Select(fields ...string) *HarnessConfigSelect { + _q.ctx.Fields = append(_q.ctx.Fields, fields...) + sbuild := &HarnessConfigSelect{HarnessConfigQuery: _q} + sbuild.label = harnessconfig.Label + sbuild.flds, sbuild.scan = &_q.ctx.Fields, sbuild.Scan + return sbuild +} + +// Aggregate returns a HarnessConfigSelect configured with the given aggregations. +func (_q *HarnessConfigQuery) Aggregate(fns ...AggregateFunc) *HarnessConfigSelect { + return _q.Select().Aggregate(fns...) +} + +func (_q *HarnessConfigQuery) prepareQuery(ctx context.Context) error { + for _, inter := range _q.inters { + if inter == nil { + return fmt.Errorf("ent: uninitialized interceptor (forgotten import ent/runtime?)") + } + if trv, ok := inter.(Traverser); ok { + if err := trv.Traverse(ctx, _q); err != nil { + return err + } + } + } + for _, f := range _q.ctx.Fields { + if !harnessconfig.ValidColumn(f) { + return &ValidationError{Name: f, err: fmt.Errorf("ent: invalid field %q for query", f)} + } + } + if _q.path != nil { + prev, err := _q.path(ctx) + if err != nil { + return err + } + _q.sql = prev + } + return nil +} + +func (_q *HarnessConfigQuery) sqlAll(ctx context.Context, hooks ...queryHook) ([]*HarnessConfig, error) { + var ( + nodes = []*HarnessConfig{} + _spec = _q.querySpec() + ) + _spec.ScanValues = func(columns []string) ([]any, error) { + return (*HarnessConfig).scanValues(nil, columns) + } + _spec.Assign = func(columns []string, values []any) error { + node := &HarnessConfig{config: _q.config} + nodes = append(nodes, node) + return node.assignValues(columns, values) + } + if len(_q.modifiers) > 0 { + _spec.Modifiers = _q.modifiers + } + for i := range hooks { + hooks[i](ctx, _spec) + } + if err := sqlgraph.QueryNodes(ctx, _q.driver, _spec); err != nil { + return nil, err + } + if len(nodes) == 0 { + return nodes, nil + } + return nodes, nil +} + +func (_q *HarnessConfigQuery) sqlCount(ctx context.Context) (int, error) { + _spec := _q.querySpec() + if len(_q.modifiers) > 0 { + _spec.Modifiers = _q.modifiers + } + _spec.Node.Columns = _q.ctx.Fields + if len(_q.ctx.Fields) > 0 { + _spec.Unique = _q.ctx.Unique != nil && *_q.ctx.Unique + } + return sqlgraph.CountNodes(ctx, _q.driver, _spec) +} + +func (_q *HarnessConfigQuery) querySpec() *sqlgraph.QuerySpec { + _spec := sqlgraph.NewQuerySpec(harnessconfig.Table, harnessconfig.Columns, sqlgraph.NewFieldSpec(harnessconfig.FieldID, field.TypeUUID)) + _spec.From = _q.sql + if unique := _q.ctx.Unique; unique != nil { + _spec.Unique = *unique + } else if _q.path != nil { + _spec.Unique = true + } + if fields := _q.ctx.Fields; len(fields) > 0 { + _spec.Node.Columns = make([]string, 0, len(fields)) + _spec.Node.Columns = append(_spec.Node.Columns, harnessconfig.FieldID) + for i := range fields { + if fields[i] != harnessconfig.FieldID { + _spec.Node.Columns = append(_spec.Node.Columns, fields[i]) + } + } + } + if ps := _q.predicates; len(ps) > 0 { + _spec.Predicate = func(selector *sql.Selector) { + for i := range ps { + ps[i](selector) + } + } + } + if limit := _q.ctx.Limit; limit != nil { + _spec.Limit = *limit + } + if offset := _q.ctx.Offset; offset != nil { + _spec.Offset = *offset + } + if ps := _q.order; len(ps) > 0 { + _spec.Order = func(selector *sql.Selector) { + for i := range ps { + ps[i](selector) + } + } + } + return _spec +} + +func (_q *HarnessConfigQuery) sqlQuery(ctx context.Context) *sql.Selector { + builder := sql.Dialect(_q.driver.Dialect()) + t1 := builder.Table(harnessconfig.Table) + columns := _q.ctx.Fields + if len(columns) == 0 { + columns = harnessconfig.Columns + } + selector := builder.Select(t1.Columns(columns...)...).From(t1) + if _q.sql != nil { + selector = _q.sql + selector.Select(selector.Columns(columns...)...) + } + if _q.ctx.Unique != nil && *_q.ctx.Unique { + selector.Distinct() + } + for _, m := range _q.modifiers { + m(selector) + } + for _, p := range _q.predicates { + p(selector) + } + for _, p := range _q.order { + p(selector) + } + if offset := _q.ctx.Offset; offset != nil { + // limit is mandatory for offset clause. We start + // with default value, and override it below if needed. + selector.Offset(*offset).Limit(math.MaxInt32) + } + if limit := _q.ctx.Limit; limit != nil { + selector.Limit(*limit) + } + return selector +} + +// ForUpdate locks the selected rows against concurrent updates, and prevent them from being +// updated, deleted or "selected ... for update" by other sessions, until the transaction is +// either committed or rolled-back. +func (_q *HarnessConfigQuery) ForUpdate(opts ...sql.LockOption) *HarnessConfigQuery { + if _q.driver.Dialect() == dialect.Postgres { + _q.Unique(false) + } + _q.modifiers = append(_q.modifiers, func(s *sql.Selector) { + s.ForUpdate(opts...) + }) + return _q +} + +// ForShare behaves similarly to ForUpdate, except that it acquires a shared mode lock +// on any rows that are read. Other sessions can read the rows, but cannot modify them +// until your transaction commits. +func (_q *HarnessConfigQuery) ForShare(opts ...sql.LockOption) *HarnessConfigQuery { + if _q.driver.Dialect() == dialect.Postgres { + _q.Unique(false) + } + _q.modifiers = append(_q.modifiers, func(s *sql.Selector) { + s.ForShare(opts...) + }) + return _q +} + +// HarnessConfigGroupBy is the group-by builder for HarnessConfig entities. +type HarnessConfigGroupBy struct { + selector + build *HarnessConfigQuery +} + +// Aggregate adds the given aggregation functions to the group-by query. +func (_g *HarnessConfigGroupBy) Aggregate(fns ...AggregateFunc) *HarnessConfigGroupBy { + _g.fns = append(_g.fns, fns...) + return _g +} + +// Scan applies the selector query and scans the result into the given value. +func (_g *HarnessConfigGroupBy) Scan(ctx context.Context, v any) error { + ctx = setContextOp(ctx, _g.build.ctx, ent.OpQueryGroupBy) + if err := _g.build.prepareQuery(ctx); err != nil { + return err + } + return scanWithInterceptors[*HarnessConfigQuery, *HarnessConfigGroupBy](ctx, _g.build, _g, _g.build.inters, v) +} + +func (_g *HarnessConfigGroupBy) sqlScan(ctx context.Context, root *HarnessConfigQuery, v any) error { + selector := root.sqlQuery(ctx).Select() + aggregation := make([]string, 0, len(_g.fns)) + for _, fn := range _g.fns { + aggregation = append(aggregation, fn(selector)) + } + if len(selector.SelectedColumns()) == 0 { + columns := make([]string, 0, len(*_g.flds)+len(_g.fns)) + for _, f := range *_g.flds { + columns = append(columns, selector.C(f)) + } + columns = append(columns, aggregation...) + selector.Select(columns...) + } + selector.GroupBy(selector.Columns(*_g.flds...)...) + if err := selector.Err(); err != nil { + return err + } + rows := &sql.Rows{} + query, args := selector.Query() + if err := _g.build.driver.Query(ctx, query, args, rows); err != nil { + return err + } + defer rows.Close() + return sql.ScanSlice(rows, v) +} + +// HarnessConfigSelect is the builder for selecting fields of HarnessConfig entities. +type HarnessConfigSelect struct { + *HarnessConfigQuery + selector +} + +// Aggregate adds the given aggregation functions to the selector query. +func (_s *HarnessConfigSelect) Aggregate(fns ...AggregateFunc) *HarnessConfigSelect { + _s.fns = append(_s.fns, fns...) + return _s +} + +// Scan applies the selector query and scans the result into the given value. +func (_s *HarnessConfigSelect) Scan(ctx context.Context, v any) error { + ctx = setContextOp(ctx, _s.ctx, ent.OpQuerySelect) + if err := _s.prepareQuery(ctx); err != nil { + return err + } + return scanWithInterceptors[*HarnessConfigQuery, *HarnessConfigSelect](ctx, _s.HarnessConfigQuery, _s, _s.inters, v) +} + +func (_s *HarnessConfigSelect) sqlScan(ctx context.Context, root *HarnessConfigQuery, v any) error { + selector := root.sqlQuery(ctx) + aggregation := make([]string, 0, len(_s.fns)) + for _, fn := range _s.fns { + aggregation = append(aggregation, fn(selector)) + } + switch n := len(*_s.selector.flds); { + case n == 0 && len(aggregation) > 0: + selector.Select(aggregation...) + case n != 0 && len(aggregation) > 0: + selector.AppendSelect(aggregation...) + } + rows := &sql.Rows{} + query, args := selector.Query() + if err := _s.driver.Query(ctx, query, args, rows); err != nil { + return err + } + defer rows.Close() + return sql.ScanSlice(rows, v) +} diff --git a/pkg/ent/harnessconfig_update.go b/pkg/ent/harnessconfig_update.go new file mode 100644 index 000000000..ac8379119 --- /dev/null +++ b/pkg/ent/harnessconfig_update.go @@ -0,0 +1,1096 @@ +// Code generated by ent, DO NOT EDIT. + +package ent + +import ( + "context" + "errors" + "fmt" + "time" + + "entgo.io/ent/dialect/sql" + "entgo.io/ent/dialect/sql/sqlgraph" + "entgo.io/ent/schema/field" + "github.com/GoogleCloudPlatform/scion/pkg/ent/harnessconfig" + "github.com/GoogleCloudPlatform/scion/pkg/ent/predicate" +) + +// HarnessConfigUpdate is the builder for updating HarnessConfig entities. +type HarnessConfigUpdate struct { + config + hooks []Hook + mutation *HarnessConfigMutation +} + +// Where appends a list predicates to the HarnessConfigUpdate builder. +func (_u *HarnessConfigUpdate) Where(ps ...predicate.HarnessConfig) *HarnessConfigUpdate { + _u.mutation.Where(ps...) + return _u +} + +// SetName sets the "name" field. +func (_u *HarnessConfigUpdate) SetName(v string) *HarnessConfigUpdate { + _u.mutation.SetName(v) + return _u +} + +// SetNillableName sets the "name" field if the given value is not nil. +func (_u *HarnessConfigUpdate) SetNillableName(v *string) *HarnessConfigUpdate { + if v != nil { + _u.SetName(*v) + } + return _u +} + +// SetSlug sets the "slug" field. +func (_u *HarnessConfigUpdate) SetSlug(v string) *HarnessConfigUpdate { + _u.mutation.SetSlug(v) + return _u +} + +// SetNillableSlug sets the "slug" field if the given value is not nil. +func (_u *HarnessConfigUpdate) SetNillableSlug(v *string) *HarnessConfigUpdate { + if v != nil { + _u.SetSlug(*v) + } + return _u +} + +// SetDisplayName sets the "display_name" field. +func (_u *HarnessConfigUpdate) SetDisplayName(v string) *HarnessConfigUpdate { + _u.mutation.SetDisplayName(v) + return _u +} + +// SetNillableDisplayName sets the "display_name" field if the given value is not nil. +func (_u *HarnessConfigUpdate) SetNillableDisplayName(v *string) *HarnessConfigUpdate { + if v != nil { + _u.SetDisplayName(*v) + } + return _u +} + +// ClearDisplayName clears the value of the "display_name" field. +func (_u *HarnessConfigUpdate) ClearDisplayName() *HarnessConfigUpdate { + _u.mutation.ClearDisplayName() + return _u +} + +// SetDescription sets the "description" field. +func (_u *HarnessConfigUpdate) SetDescription(v string) *HarnessConfigUpdate { + _u.mutation.SetDescription(v) + return _u +} + +// SetNillableDescription sets the "description" field if the given value is not nil. +func (_u *HarnessConfigUpdate) SetNillableDescription(v *string) *HarnessConfigUpdate { + if v != nil { + _u.SetDescription(*v) + } + return _u +} + +// ClearDescription clears the value of the "description" field. +func (_u *HarnessConfigUpdate) ClearDescription() *HarnessConfigUpdate { + _u.mutation.ClearDescription() + return _u +} + +// SetHarness sets the "harness" field. +func (_u *HarnessConfigUpdate) SetHarness(v string) *HarnessConfigUpdate { + _u.mutation.SetHarness(v) + return _u +} + +// SetNillableHarness sets the "harness" field if the given value is not nil. +func (_u *HarnessConfigUpdate) SetNillableHarness(v *string) *HarnessConfigUpdate { + if v != nil { + _u.SetHarness(*v) + } + return _u +} + +// SetConfig sets the "config" field. +func (_u *HarnessConfigUpdate) SetConfig(v string) *HarnessConfigUpdate { + _u.mutation.SetConfig(v) + return _u +} + +// SetNillableConfig sets the "config" field if the given value is not nil. +func (_u *HarnessConfigUpdate) SetNillableConfig(v *string) *HarnessConfigUpdate { + if v != nil { + _u.SetConfig(*v) + } + return _u +} + +// ClearConfig clears the value of the "config" field. +func (_u *HarnessConfigUpdate) ClearConfig() *HarnessConfigUpdate { + _u.mutation.ClearConfig() + return _u +} + +// SetContentHash sets the "content_hash" field. +func (_u *HarnessConfigUpdate) SetContentHash(v string) *HarnessConfigUpdate { + _u.mutation.SetContentHash(v) + return _u +} + +// SetNillableContentHash sets the "content_hash" field if the given value is not nil. +func (_u *HarnessConfigUpdate) SetNillableContentHash(v *string) *HarnessConfigUpdate { + if v != nil { + _u.SetContentHash(*v) + } + return _u +} + +// ClearContentHash clears the value of the "content_hash" field. +func (_u *HarnessConfigUpdate) ClearContentHash() *HarnessConfigUpdate { + _u.mutation.ClearContentHash() + return _u +} + +// SetScope sets the "scope" field. +func (_u *HarnessConfigUpdate) SetScope(v string) *HarnessConfigUpdate { + _u.mutation.SetScope(v) + return _u +} + +// SetNillableScope sets the "scope" field if the given value is not nil. +func (_u *HarnessConfigUpdate) SetNillableScope(v *string) *HarnessConfigUpdate { + if v != nil { + _u.SetScope(*v) + } + return _u +} + +// SetScopeID sets the "scope_id" field. +func (_u *HarnessConfigUpdate) SetScopeID(v string) *HarnessConfigUpdate { + _u.mutation.SetScopeID(v) + return _u +} + +// SetNillableScopeID sets the "scope_id" field if the given value is not nil. +func (_u *HarnessConfigUpdate) SetNillableScopeID(v *string) *HarnessConfigUpdate { + if v != nil { + _u.SetScopeID(*v) + } + return _u +} + +// ClearScopeID clears the value of the "scope_id" field. +func (_u *HarnessConfigUpdate) ClearScopeID() *HarnessConfigUpdate { + _u.mutation.ClearScopeID() + return _u +} + +// SetStorageURI sets the "storage_uri" field. +func (_u *HarnessConfigUpdate) SetStorageURI(v string) *HarnessConfigUpdate { + _u.mutation.SetStorageURI(v) + return _u +} + +// SetNillableStorageURI sets the "storage_uri" field if the given value is not nil. +func (_u *HarnessConfigUpdate) SetNillableStorageURI(v *string) *HarnessConfigUpdate { + if v != nil { + _u.SetStorageURI(*v) + } + return _u +} + +// ClearStorageURI clears the value of the "storage_uri" field. +func (_u *HarnessConfigUpdate) ClearStorageURI() *HarnessConfigUpdate { + _u.mutation.ClearStorageURI() + return _u +} + +// SetStorageBucket sets the "storage_bucket" field. +func (_u *HarnessConfigUpdate) SetStorageBucket(v string) *HarnessConfigUpdate { + _u.mutation.SetStorageBucket(v) + return _u +} + +// SetNillableStorageBucket sets the "storage_bucket" field if the given value is not nil. +func (_u *HarnessConfigUpdate) SetNillableStorageBucket(v *string) *HarnessConfigUpdate { + if v != nil { + _u.SetStorageBucket(*v) + } + return _u +} + +// ClearStorageBucket clears the value of the "storage_bucket" field. +func (_u *HarnessConfigUpdate) ClearStorageBucket() *HarnessConfigUpdate { + _u.mutation.ClearStorageBucket() + return _u +} + +// SetStoragePath sets the "storage_path" field. +func (_u *HarnessConfigUpdate) SetStoragePath(v string) *HarnessConfigUpdate { + _u.mutation.SetStoragePath(v) + return _u +} + +// SetNillableStoragePath sets the "storage_path" field if the given value is not nil. +func (_u *HarnessConfigUpdate) SetNillableStoragePath(v *string) *HarnessConfigUpdate { + if v != nil { + _u.SetStoragePath(*v) + } + return _u +} + +// ClearStoragePath clears the value of the "storage_path" field. +func (_u *HarnessConfigUpdate) ClearStoragePath() *HarnessConfigUpdate { + _u.mutation.ClearStoragePath() + return _u +} + +// SetFiles sets the "files" field. +func (_u *HarnessConfigUpdate) SetFiles(v string) *HarnessConfigUpdate { + _u.mutation.SetFiles(v) + return _u +} + +// SetNillableFiles sets the "files" field if the given value is not nil. +func (_u *HarnessConfigUpdate) SetNillableFiles(v *string) *HarnessConfigUpdate { + if v != nil { + _u.SetFiles(*v) + } + return _u +} + +// ClearFiles clears the value of the "files" field. +func (_u *HarnessConfigUpdate) ClearFiles() *HarnessConfigUpdate { + _u.mutation.ClearFiles() + return _u +} + +// SetStatus sets the "status" field. +func (_u *HarnessConfigUpdate) SetStatus(v harnessconfig.Status) *HarnessConfigUpdate { + _u.mutation.SetStatus(v) + return _u +} + +// SetNillableStatus sets the "status" field if the given value is not nil. +func (_u *HarnessConfigUpdate) SetNillableStatus(v *harnessconfig.Status) *HarnessConfigUpdate { + if v != nil { + _u.SetStatus(*v) + } + return _u +} + +// SetOwnerID sets the "owner_id" field. +func (_u *HarnessConfigUpdate) SetOwnerID(v string) *HarnessConfigUpdate { + _u.mutation.SetOwnerID(v) + return _u +} + +// SetNillableOwnerID sets the "owner_id" field if the given value is not nil. +func (_u *HarnessConfigUpdate) SetNillableOwnerID(v *string) *HarnessConfigUpdate { + if v != nil { + _u.SetOwnerID(*v) + } + return _u +} + +// ClearOwnerID clears the value of the "owner_id" field. +func (_u *HarnessConfigUpdate) ClearOwnerID() *HarnessConfigUpdate { + _u.mutation.ClearOwnerID() + return _u +} + +// SetCreatedBy sets the "created_by" field. +func (_u *HarnessConfigUpdate) SetCreatedBy(v string) *HarnessConfigUpdate { + _u.mutation.SetCreatedBy(v) + return _u +} + +// SetNillableCreatedBy sets the "created_by" field if the given value is not nil. +func (_u *HarnessConfigUpdate) SetNillableCreatedBy(v *string) *HarnessConfigUpdate { + if v != nil { + _u.SetCreatedBy(*v) + } + return _u +} + +// ClearCreatedBy clears the value of the "created_by" field. +func (_u *HarnessConfigUpdate) ClearCreatedBy() *HarnessConfigUpdate { + _u.mutation.ClearCreatedBy() + return _u +} + +// SetUpdatedBy sets the "updated_by" field. +func (_u *HarnessConfigUpdate) SetUpdatedBy(v string) *HarnessConfigUpdate { + _u.mutation.SetUpdatedBy(v) + return _u +} + +// SetNillableUpdatedBy sets the "updated_by" field if the given value is not nil. +func (_u *HarnessConfigUpdate) SetNillableUpdatedBy(v *string) *HarnessConfigUpdate { + if v != nil { + _u.SetUpdatedBy(*v) + } + return _u +} + +// ClearUpdatedBy clears the value of the "updated_by" field. +func (_u *HarnessConfigUpdate) ClearUpdatedBy() *HarnessConfigUpdate { + _u.mutation.ClearUpdatedBy() + return _u +} + +// SetVisibility sets the "visibility" field. +func (_u *HarnessConfigUpdate) SetVisibility(v string) *HarnessConfigUpdate { + _u.mutation.SetVisibility(v) + return _u +} + +// SetNillableVisibility sets the "visibility" field if the given value is not nil. +func (_u *HarnessConfigUpdate) SetNillableVisibility(v *string) *HarnessConfigUpdate { + if v != nil { + _u.SetVisibility(*v) + } + return _u +} + +// SetUpdated sets the "updated" field. +func (_u *HarnessConfigUpdate) SetUpdated(v time.Time) *HarnessConfigUpdate { + _u.mutation.SetUpdated(v) + return _u +} + +// Mutation returns the HarnessConfigMutation object of the builder. +func (_u *HarnessConfigUpdate) Mutation() *HarnessConfigMutation { + return _u.mutation +} + +// Save executes the query and returns the number of nodes affected by the update operation. +func (_u *HarnessConfigUpdate) Save(ctx context.Context) (int, error) { + _u.defaults() + return withHooks(ctx, _u.sqlSave, _u.mutation, _u.hooks) +} + +// SaveX is like Save, but panics if an error occurs. +func (_u *HarnessConfigUpdate) SaveX(ctx context.Context) int { + affected, err := _u.Save(ctx) + if err != nil { + panic(err) + } + return affected +} + +// Exec executes the query. +func (_u *HarnessConfigUpdate) Exec(ctx context.Context) error { + _, err := _u.Save(ctx) + return err +} + +// ExecX is like Exec, but panics if an error occurs. +func (_u *HarnessConfigUpdate) ExecX(ctx context.Context) { + if err := _u.Exec(ctx); err != nil { + panic(err) + } +} + +// defaults sets the default values of the builder before save. +func (_u *HarnessConfigUpdate) defaults() { + if _, ok := _u.mutation.Updated(); !ok { + v := harnessconfig.UpdateDefaultUpdated() + _u.mutation.SetUpdated(v) + } +} + +// check runs all checks and user-defined validators on the builder. +func (_u *HarnessConfigUpdate) check() error { + if v, ok := _u.mutation.Name(); ok { + if err := harnessconfig.NameValidator(v); err != nil { + return &ValidationError{Name: "name", err: fmt.Errorf(`ent: validator failed for field "HarnessConfig.name": %w`, err)} + } + } + if v, ok := _u.mutation.Slug(); ok { + if err := harnessconfig.SlugValidator(v); err != nil { + return &ValidationError{Name: "slug", err: fmt.Errorf(`ent: validator failed for field "HarnessConfig.slug": %w`, err)} + } + } + if v, ok := _u.mutation.Harness(); ok { + if err := harnessconfig.HarnessValidator(v); err != nil { + return &ValidationError{Name: "harness", err: fmt.Errorf(`ent: validator failed for field "HarnessConfig.harness": %w`, err)} + } + } + if v, ok := _u.mutation.Status(); ok { + if err := harnessconfig.StatusValidator(v); err != nil { + return &ValidationError{Name: "status", err: fmt.Errorf(`ent: validator failed for field "HarnessConfig.status": %w`, err)} + } + } + return nil +} + +func (_u *HarnessConfigUpdate) sqlSave(ctx context.Context) (_node int, err error) { + if err := _u.check(); err != nil { + return _node, err + } + _spec := sqlgraph.NewUpdateSpec(harnessconfig.Table, harnessconfig.Columns, sqlgraph.NewFieldSpec(harnessconfig.FieldID, field.TypeUUID)) + if ps := _u.mutation.predicates; len(ps) > 0 { + _spec.Predicate = func(selector *sql.Selector) { + for i := range ps { + ps[i](selector) + } + } + } + if value, ok := _u.mutation.Name(); ok { + _spec.SetField(harnessconfig.FieldName, field.TypeString, value) + } + if value, ok := _u.mutation.Slug(); ok { + _spec.SetField(harnessconfig.FieldSlug, field.TypeString, value) + } + if value, ok := _u.mutation.DisplayName(); ok { + _spec.SetField(harnessconfig.FieldDisplayName, field.TypeString, value) + } + if _u.mutation.DisplayNameCleared() { + _spec.ClearField(harnessconfig.FieldDisplayName, field.TypeString) + } + if value, ok := _u.mutation.Description(); ok { + _spec.SetField(harnessconfig.FieldDescription, field.TypeString, value) + } + if _u.mutation.DescriptionCleared() { + _spec.ClearField(harnessconfig.FieldDescription, field.TypeString) + } + if value, ok := _u.mutation.Harness(); ok { + _spec.SetField(harnessconfig.FieldHarness, field.TypeString, value) + } + if value, ok := _u.mutation.Config(); ok { + _spec.SetField(harnessconfig.FieldConfig, field.TypeString, value) + } + if _u.mutation.ConfigCleared() { + _spec.ClearField(harnessconfig.FieldConfig, field.TypeString) + } + if value, ok := _u.mutation.ContentHash(); ok { + _spec.SetField(harnessconfig.FieldContentHash, field.TypeString, value) + } + if _u.mutation.ContentHashCleared() { + _spec.ClearField(harnessconfig.FieldContentHash, field.TypeString) + } + if value, ok := _u.mutation.Scope(); ok { + _spec.SetField(harnessconfig.FieldScope, field.TypeString, value) + } + if value, ok := _u.mutation.ScopeID(); ok { + _spec.SetField(harnessconfig.FieldScopeID, field.TypeString, value) + } + if _u.mutation.ScopeIDCleared() { + _spec.ClearField(harnessconfig.FieldScopeID, field.TypeString) + } + if value, ok := _u.mutation.StorageURI(); ok { + _spec.SetField(harnessconfig.FieldStorageURI, field.TypeString, value) + } + if _u.mutation.StorageURICleared() { + _spec.ClearField(harnessconfig.FieldStorageURI, field.TypeString) + } + if value, ok := _u.mutation.StorageBucket(); ok { + _spec.SetField(harnessconfig.FieldStorageBucket, field.TypeString, value) + } + if _u.mutation.StorageBucketCleared() { + _spec.ClearField(harnessconfig.FieldStorageBucket, field.TypeString) + } + if value, ok := _u.mutation.StoragePath(); ok { + _spec.SetField(harnessconfig.FieldStoragePath, field.TypeString, value) + } + if _u.mutation.StoragePathCleared() { + _spec.ClearField(harnessconfig.FieldStoragePath, field.TypeString) + } + if value, ok := _u.mutation.Files(); ok { + _spec.SetField(harnessconfig.FieldFiles, field.TypeString, value) + } + if _u.mutation.FilesCleared() { + _spec.ClearField(harnessconfig.FieldFiles, field.TypeString) + } + if value, ok := _u.mutation.Status(); ok { + _spec.SetField(harnessconfig.FieldStatus, field.TypeEnum, value) + } + if value, ok := _u.mutation.OwnerID(); ok { + _spec.SetField(harnessconfig.FieldOwnerID, field.TypeString, value) + } + if _u.mutation.OwnerIDCleared() { + _spec.ClearField(harnessconfig.FieldOwnerID, field.TypeString) + } + if value, ok := _u.mutation.CreatedBy(); ok { + _spec.SetField(harnessconfig.FieldCreatedBy, field.TypeString, value) + } + if _u.mutation.CreatedByCleared() { + _spec.ClearField(harnessconfig.FieldCreatedBy, field.TypeString) + } + if value, ok := _u.mutation.UpdatedBy(); ok { + _spec.SetField(harnessconfig.FieldUpdatedBy, field.TypeString, value) + } + if _u.mutation.UpdatedByCleared() { + _spec.ClearField(harnessconfig.FieldUpdatedBy, field.TypeString) + } + if value, ok := _u.mutation.Visibility(); ok { + _spec.SetField(harnessconfig.FieldVisibility, field.TypeString, value) + } + if value, ok := _u.mutation.Updated(); ok { + _spec.SetField(harnessconfig.FieldUpdated, field.TypeTime, value) + } + if _node, err = sqlgraph.UpdateNodes(ctx, _u.driver, _spec); err != nil { + if _, ok := err.(*sqlgraph.NotFoundError); ok { + err = &NotFoundError{harnessconfig.Label} + } else if sqlgraph.IsConstraintError(err) { + err = &ConstraintError{msg: err.Error(), wrap: err} + } + return 0, err + } + _u.mutation.done = true + return _node, nil +} + +// HarnessConfigUpdateOne is the builder for updating a single HarnessConfig entity. +type HarnessConfigUpdateOne struct { + config + fields []string + hooks []Hook + mutation *HarnessConfigMutation +} + +// SetName sets the "name" field. +func (_u *HarnessConfigUpdateOne) SetName(v string) *HarnessConfigUpdateOne { + _u.mutation.SetName(v) + return _u +} + +// SetNillableName sets the "name" field if the given value is not nil. +func (_u *HarnessConfigUpdateOne) SetNillableName(v *string) *HarnessConfigUpdateOne { + if v != nil { + _u.SetName(*v) + } + return _u +} + +// SetSlug sets the "slug" field. +func (_u *HarnessConfigUpdateOne) SetSlug(v string) *HarnessConfigUpdateOne { + _u.mutation.SetSlug(v) + return _u +} + +// SetNillableSlug sets the "slug" field if the given value is not nil. +func (_u *HarnessConfigUpdateOne) SetNillableSlug(v *string) *HarnessConfigUpdateOne { + if v != nil { + _u.SetSlug(*v) + } + return _u +} + +// SetDisplayName sets the "display_name" field. +func (_u *HarnessConfigUpdateOne) SetDisplayName(v string) *HarnessConfigUpdateOne { + _u.mutation.SetDisplayName(v) + return _u +} + +// SetNillableDisplayName sets the "display_name" field if the given value is not nil. +func (_u *HarnessConfigUpdateOne) SetNillableDisplayName(v *string) *HarnessConfigUpdateOne { + if v != nil { + _u.SetDisplayName(*v) + } + return _u +} + +// ClearDisplayName clears the value of the "display_name" field. +func (_u *HarnessConfigUpdateOne) ClearDisplayName() *HarnessConfigUpdateOne { + _u.mutation.ClearDisplayName() + return _u +} + +// SetDescription sets the "description" field. +func (_u *HarnessConfigUpdateOne) SetDescription(v string) *HarnessConfigUpdateOne { + _u.mutation.SetDescription(v) + return _u +} + +// SetNillableDescription sets the "description" field if the given value is not nil. +func (_u *HarnessConfigUpdateOne) SetNillableDescription(v *string) *HarnessConfigUpdateOne { + if v != nil { + _u.SetDescription(*v) + } + return _u +} + +// ClearDescription clears the value of the "description" field. +func (_u *HarnessConfigUpdateOne) ClearDescription() *HarnessConfigUpdateOne { + _u.mutation.ClearDescription() + return _u +} + +// SetHarness sets the "harness" field. +func (_u *HarnessConfigUpdateOne) SetHarness(v string) *HarnessConfigUpdateOne { + _u.mutation.SetHarness(v) + return _u +} + +// SetNillableHarness sets the "harness" field if the given value is not nil. +func (_u *HarnessConfigUpdateOne) SetNillableHarness(v *string) *HarnessConfigUpdateOne { + if v != nil { + _u.SetHarness(*v) + } + return _u +} + +// SetConfig sets the "config" field. +func (_u *HarnessConfigUpdateOne) SetConfig(v string) *HarnessConfigUpdateOne { + _u.mutation.SetConfig(v) + return _u +} + +// SetNillableConfig sets the "config" field if the given value is not nil. +func (_u *HarnessConfigUpdateOne) SetNillableConfig(v *string) *HarnessConfigUpdateOne { + if v != nil { + _u.SetConfig(*v) + } + return _u +} + +// ClearConfig clears the value of the "config" field. +func (_u *HarnessConfigUpdateOne) ClearConfig() *HarnessConfigUpdateOne { + _u.mutation.ClearConfig() + return _u +} + +// SetContentHash sets the "content_hash" field. +func (_u *HarnessConfigUpdateOne) SetContentHash(v string) *HarnessConfigUpdateOne { + _u.mutation.SetContentHash(v) + return _u +} + +// SetNillableContentHash sets the "content_hash" field if the given value is not nil. +func (_u *HarnessConfigUpdateOne) SetNillableContentHash(v *string) *HarnessConfigUpdateOne { + if v != nil { + _u.SetContentHash(*v) + } + return _u +} + +// ClearContentHash clears the value of the "content_hash" field. +func (_u *HarnessConfigUpdateOne) ClearContentHash() *HarnessConfigUpdateOne { + _u.mutation.ClearContentHash() + return _u +} + +// SetScope sets the "scope" field. +func (_u *HarnessConfigUpdateOne) SetScope(v string) *HarnessConfigUpdateOne { + _u.mutation.SetScope(v) + return _u +} + +// SetNillableScope sets the "scope" field if the given value is not nil. +func (_u *HarnessConfigUpdateOne) SetNillableScope(v *string) *HarnessConfigUpdateOne { + if v != nil { + _u.SetScope(*v) + } + return _u +} + +// SetScopeID sets the "scope_id" field. +func (_u *HarnessConfigUpdateOne) SetScopeID(v string) *HarnessConfigUpdateOne { + _u.mutation.SetScopeID(v) + return _u +} + +// SetNillableScopeID sets the "scope_id" field if the given value is not nil. +func (_u *HarnessConfigUpdateOne) SetNillableScopeID(v *string) *HarnessConfigUpdateOne { + if v != nil { + _u.SetScopeID(*v) + } + return _u +} + +// ClearScopeID clears the value of the "scope_id" field. +func (_u *HarnessConfigUpdateOne) ClearScopeID() *HarnessConfigUpdateOne { + _u.mutation.ClearScopeID() + return _u +} + +// SetStorageURI sets the "storage_uri" field. +func (_u *HarnessConfigUpdateOne) SetStorageURI(v string) *HarnessConfigUpdateOne { + _u.mutation.SetStorageURI(v) + return _u +} + +// SetNillableStorageURI sets the "storage_uri" field if the given value is not nil. +func (_u *HarnessConfigUpdateOne) SetNillableStorageURI(v *string) *HarnessConfigUpdateOne { + if v != nil { + _u.SetStorageURI(*v) + } + return _u +} + +// ClearStorageURI clears the value of the "storage_uri" field. +func (_u *HarnessConfigUpdateOne) ClearStorageURI() *HarnessConfigUpdateOne { + _u.mutation.ClearStorageURI() + return _u +} + +// SetStorageBucket sets the "storage_bucket" field. +func (_u *HarnessConfigUpdateOne) SetStorageBucket(v string) *HarnessConfigUpdateOne { + _u.mutation.SetStorageBucket(v) + return _u +} + +// SetNillableStorageBucket sets the "storage_bucket" field if the given value is not nil. +func (_u *HarnessConfigUpdateOne) SetNillableStorageBucket(v *string) *HarnessConfigUpdateOne { + if v != nil { + _u.SetStorageBucket(*v) + } + return _u +} + +// ClearStorageBucket clears the value of the "storage_bucket" field. +func (_u *HarnessConfigUpdateOne) ClearStorageBucket() *HarnessConfigUpdateOne { + _u.mutation.ClearStorageBucket() + return _u +} + +// SetStoragePath sets the "storage_path" field. +func (_u *HarnessConfigUpdateOne) SetStoragePath(v string) *HarnessConfigUpdateOne { + _u.mutation.SetStoragePath(v) + return _u +} + +// SetNillableStoragePath sets the "storage_path" field if the given value is not nil. +func (_u *HarnessConfigUpdateOne) SetNillableStoragePath(v *string) *HarnessConfigUpdateOne { + if v != nil { + _u.SetStoragePath(*v) + } + return _u +} + +// ClearStoragePath clears the value of the "storage_path" field. +func (_u *HarnessConfigUpdateOne) ClearStoragePath() *HarnessConfigUpdateOne { + _u.mutation.ClearStoragePath() + return _u +} + +// SetFiles sets the "files" field. +func (_u *HarnessConfigUpdateOne) SetFiles(v string) *HarnessConfigUpdateOne { + _u.mutation.SetFiles(v) + return _u +} + +// SetNillableFiles sets the "files" field if the given value is not nil. +func (_u *HarnessConfigUpdateOne) SetNillableFiles(v *string) *HarnessConfigUpdateOne { + if v != nil { + _u.SetFiles(*v) + } + return _u +} + +// ClearFiles clears the value of the "files" field. +func (_u *HarnessConfigUpdateOne) ClearFiles() *HarnessConfigUpdateOne { + _u.mutation.ClearFiles() + return _u +} + +// SetStatus sets the "status" field. +func (_u *HarnessConfigUpdateOne) SetStatus(v harnessconfig.Status) *HarnessConfigUpdateOne { + _u.mutation.SetStatus(v) + return _u +} + +// SetNillableStatus sets the "status" field if the given value is not nil. +func (_u *HarnessConfigUpdateOne) SetNillableStatus(v *harnessconfig.Status) *HarnessConfigUpdateOne { + if v != nil { + _u.SetStatus(*v) + } + return _u +} + +// SetOwnerID sets the "owner_id" field. +func (_u *HarnessConfigUpdateOne) SetOwnerID(v string) *HarnessConfigUpdateOne { + _u.mutation.SetOwnerID(v) + return _u +} + +// SetNillableOwnerID sets the "owner_id" field if the given value is not nil. +func (_u *HarnessConfigUpdateOne) SetNillableOwnerID(v *string) *HarnessConfigUpdateOne { + if v != nil { + _u.SetOwnerID(*v) + } + return _u +} + +// ClearOwnerID clears the value of the "owner_id" field. +func (_u *HarnessConfigUpdateOne) ClearOwnerID() *HarnessConfigUpdateOne { + _u.mutation.ClearOwnerID() + return _u +} + +// SetCreatedBy sets the "created_by" field. +func (_u *HarnessConfigUpdateOne) SetCreatedBy(v string) *HarnessConfigUpdateOne { + _u.mutation.SetCreatedBy(v) + return _u +} + +// SetNillableCreatedBy sets the "created_by" field if the given value is not nil. +func (_u *HarnessConfigUpdateOne) SetNillableCreatedBy(v *string) *HarnessConfigUpdateOne { + if v != nil { + _u.SetCreatedBy(*v) + } + return _u +} + +// ClearCreatedBy clears the value of the "created_by" field. +func (_u *HarnessConfigUpdateOne) ClearCreatedBy() *HarnessConfigUpdateOne { + _u.mutation.ClearCreatedBy() + return _u +} + +// SetUpdatedBy sets the "updated_by" field. +func (_u *HarnessConfigUpdateOne) SetUpdatedBy(v string) *HarnessConfigUpdateOne { + _u.mutation.SetUpdatedBy(v) + return _u +} + +// SetNillableUpdatedBy sets the "updated_by" field if the given value is not nil. +func (_u *HarnessConfigUpdateOne) SetNillableUpdatedBy(v *string) *HarnessConfigUpdateOne { + if v != nil { + _u.SetUpdatedBy(*v) + } + return _u +} + +// ClearUpdatedBy clears the value of the "updated_by" field. +func (_u *HarnessConfigUpdateOne) ClearUpdatedBy() *HarnessConfigUpdateOne { + _u.mutation.ClearUpdatedBy() + return _u +} + +// SetVisibility sets the "visibility" field. +func (_u *HarnessConfigUpdateOne) SetVisibility(v string) *HarnessConfigUpdateOne { + _u.mutation.SetVisibility(v) + return _u +} + +// SetNillableVisibility sets the "visibility" field if the given value is not nil. +func (_u *HarnessConfigUpdateOne) SetNillableVisibility(v *string) *HarnessConfigUpdateOne { + if v != nil { + _u.SetVisibility(*v) + } + return _u +} + +// SetUpdated sets the "updated" field. +func (_u *HarnessConfigUpdateOne) SetUpdated(v time.Time) *HarnessConfigUpdateOne { + _u.mutation.SetUpdated(v) + return _u +} + +// Mutation returns the HarnessConfigMutation object of the builder. +func (_u *HarnessConfigUpdateOne) Mutation() *HarnessConfigMutation { + return _u.mutation +} + +// Where appends a list predicates to the HarnessConfigUpdate builder. +func (_u *HarnessConfigUpdateOne) Where(ps ...predicate.HarnessConfig) *HarnessConfigUpdateOne { + _u.mutation.Where(ps...) + return _u +} + +// Select allows selecting one or more fields (columns) of the returned entity. +// The default is selecting all fields defined in the entity schema. +func (_u *HarnessConfigUpdateOne) Select(field string, fields ...string) *HarnessConfigUpdateOne { + _u.fields = append([]string{field}, fields...) + return _u +} + +// Save executes the query and returns the updated HarnessConfig entity. +func (_u *HarnessConfigUpdateOne) Save(ctx context.Context) (*HarnessConfig, error) { + _u.defaults() + return withHooks(ctx, _u.sqlSave, _u.mutation, _u.hooks) +} + +// SaveX is like Save, but panics if an error occurs. +func (_u *HarnessConfigUpdateOne) SaveX(ctx context.Context) *HarnessConfig { + node, err := _u.Save(ctx) + if err != nil { + panic(err) + } + return node +} + +// Exec executes the query on the entity. +func (_u *HarnessConfigUpdateOne) Exec(ctx context.Context) error { + _, err := _u.Save(ctx) + return err +} + +// ExecX is like Exec, but panics if an error occurs. +func (_u *HarnessConfigUpdateOne) ExecX(ctx context.Context) { + if err := _u.Exec(ctx); err != nil { + panic(err) + } +} + +// defaults sets the default values of the builder before save. +func (_u *HarnessConfigUpdateOne) defaults() { + if _, ok := _u.mutation.Updated(); !ok { + v := harnessconfig.UpdateDefaultUpdated() + _u.mutation.SetUpdated(v) + } +} + +// check runs all checks and user-defined validators on the builder. +func (_u *HarnessConfigUpdateOne) check() error { + if v, ok := _u.mutation.Name(); ok { + if err := harnessconfig.NameValidator(v); err != nil { + return &ValidationError{Name: "name", err: fmt.Errorf(`ent: validator failed for field "HarnessConfig.name": %w`, err)} + } + } + if v, ok := _u.mutation.Slug(); ok { + if err := harnessconfig.SlugValidator(v); err != nil { + return &ValidationError{Name: "slug", err: fmt.Errorf(`ent: validator failed for field "HarnessConfig.slug": %w`, err)} + } + } + if v, ok := _u.mutation.Harness(); ok { + if err := harnessconfig.HarnessValidator(v); err != nil { + return &ValidationError{Name: "harness", err: fmt.Errorf(`ent: validator failed for field "HarnessConfig.harness": %w`, err)} + } + } + if v, ok := _u.mutation.Status(); ok { + if err := harnessconfig.StatusValidator(v); err != nil { + return &ValidationError{Name: "status", err: fmt.Errorf(`ent: validator failed for field "HarnessConfig.status": %w`, err)} + } + } + return nil +} + +func (_u *HarnessConfigUpdateOne) sqlSave(ctx context.Context) (_node *HarnessConfig, err error) { + if err := _u.check(); err != nil { + return _node, err + } + _spec := sqlgraph.NewUpdateSpec(harnessconfig.Table, harnessconfig.Columns, sqlgraph.NewFieldSpec(harnessconfig.FieldID, field.TypeUUID)) + id, ok := _u.mutation.ID() + if !ok { + return nil, &ValidationError{Name: "id", err: errors.New(`ent: missing "HarnessConfig.id" for update`)} + } + _spec.Node.ID.Value = id + if fields := _u.fields; len(fields) > 0 { + _spec.Node.Columns = make([]string, 0, len(fields)) + _spec.Node.Columns = append(_spec.Node.Columns, harnessconfig.FieldID) + for _, f := range fields { + if !harnessconfig.ValidColumn(f) { + return nil, &ValidationError{Name: f, err: fmt.Errorf("ent: invalid field %q for query", f)} + } + if f != harnessconfig.FieldID { + _spec.Node.Columns = append(_spec.Node.Columns, f) + } + } + } + if ps := _u.mutation.predicates; len(ps) > 0 { + _spec.Predicate = func(selector *sql.Selector) { + for i := range ps { + ps[i](selector) + } + } + } + if value, ok := _u.mutation.Name(); ok { + _spec.SetField(harnessconfig.FieldName, field.TypeString, value) + } + if value, ok := _u.mutation.Slug(); ok { + _spec.SetField(harnessconfig.FieldSlug, field.TypeString, value) + } + if value, ok := _u.mutation.DisplayName(); ok { + _spec.SetField(harnessconfig.FieldDisplayName, field.TypeString, value) + } + if _u.mutation.DisplayNameCleared() { + _spec.ClearField(harnessconfig.FieldDisplayName, field.TypeString) + } + if value, ok := _u.mutation.Description(); ok { + _spec.SetField(harnessconfig.FieldDescription, field.TypeString, value) + } + if _u.mutation.DescriptionCleared() { + _spec.ClearField(harnessconfig.FieldDescription, field.TypeString) + } + if value, ok := _u.mutation.Harness(); ok { + _spec.SetField(harnessconfig.FieldHarness, field.TypeString, value) + } + if value, ok := _u.mutation.Config(); ok { + _spec.SetField(harnessconfig.FieldConfig, field.TypeString, value) + } + if _u.mutation.ConfigCleared() { + _spec.ClearField(harnessconfig.FieldConfig, field.TypeString) + } + if value, ok := _u.mutation.ContentHash(); ok { + _spec.SetField(harnessconfig.FieldContentHash, field.TypeString, value) + } + if _u.mutation.ContentHashCleared() { + _spec.ClearField(harnessconfig.FieldContentHash, field.TypeString) + } + if value, ok := _u.mutation.Scope(); ok { + _spec.SetField(harnessconfig.FieldScope, field.TypeString, value) + } + if value, ok := _u.mutation.ScopeID(); ok { + _spec.SetField(harnessconfig.FieldScopeID, field.TypeString, value) + } + if _u.mutation.ScopeIDCleared() { + _spec.ClearField(harnessconfig.FieldScopeID, field.TypeString) + } + if value, ok := _u.mutation.StorageURI(); ok { + _spec.SetField(harnessconfig.FieldStorageURI, field.TypeString, value) + } + if _u.mutation.StorageURICleared() { + _spec.ClearField(harnessconfig.FieldStorageURI, field.TypeString) + } + if value, ok := _u.mutation.StorageBucket(); ok { + _spec.SetField(harnessconfig.FieldStorageBucket, field.TypeString, value) + } + if _u.mutation.StorageBucketCleared() { + _spec.ClearField(harnessconfig.FieldStorageBucket, field.TypeString) + } + if value, ok := _u.mutation.StoragePath(); ok { + _spec.SetField(harnessconfig.FieldStoragePath, field.TypeString, value) + } + if _u.mutation.StoragePathCleared() { + _spec.ClearField(harnessconfig.FieldStoragePath, field.TypeString) + } + if value, ok := _u.mutation.Files(); ok { + _spec.SetField(harnessconfig.FieldFiles, field.TypeString, value) + } + if _u.mutation.FilesCleared() { + _spec.ClearField(harnessconfig.FieldFiles, field.TypeString) + } + if value, ok := _u.mutation.Status(); ok { + _spec.SetField(harnessconfig.FieldStatus, field.TypeEnum, value) + } + if value, ok := _u.mutation.OwnerID(); ok { + _spec.SetField(harnessconfig.FieldOwnerID, field.TypeString, value) + } + if _u.mutation.OwnerIDCleared() { + _spec.ClearField(harnessconfig.FieldOwnerID, field.TypeString) + } + if value, ok := _u.mutation.CreatedBy(); ok { + _spec.SetField(harnessconfig.FieldCreatedBy, field.TypeString, value) + } + if _u.mutation.CreatedByCleared() { + _spec.ClearField(harnessconfig.FieldCreatedBy, field.TypeString) + } + if value, ok := _u.mutation.UpdatedBy(); ok { + _spec.SetField(harnessconfig.FieldUpdatedBy, field.TypeString, value) + } + if _u.mutation.UpdatedByCleared() { + _spec.ClearField(harnessconfig.FieldUpdatedBy, field.TypeString) + } + if value, ok := _u.mutation.Visibility(); ok { + _spec.SetField(harnessconfig.FieldVisibility, field.TypeString, value) + } + if value, ok := _u.mutation.Updated(); ok { + _spec.SetField(harnessconfig.FieldUpdated, field.TypeTime, value) + } + _node = &HarnessConfig{config: _u.config} + _spec.Assign = _node.assignValues + _spec.ScanValues = _node.scanValues + if err = sqlgraph.UpdateNode(ctx, _u.driver, _spec); err != nil { + if _, ok := err.(*sqlgraph.NotFoundError); ok { + err = &NotFoundError{harnessconfig.Label} + } else if sqlgraph.IsConstraintError(err) { + err = &ConstraintError{msg: err.Error(), wrap: err} + } + return nil, err + } + _u.mutation.done = true + return _node, nil +} diff --git a/pkg/ent/hook/hook.go b/pkg/ent/hook/hook.go index 889987add..840f24a60 100644 --- a/pkg/ent/hook/hook.go +++ b/pkg/ent/hook/hook.go @@ -33,6 +33,102 @@ func (f AgentFunc) Mutate(ctx context.Context, m ent.Mutation) (ent.Value, error return nil, fmt.Errorf("unexpected mutation type %T. expect *ent.AgentMutation", m) } +// The AllowListEntryFunc type is an adapter to allow the use of ordinary +// function as AllowListEntry mutator. +type AllowListEntryFunc func(context.Context, *ent.AllowListEntryMutation) (ent.Value, error) + +// Mutate calls f(ctx, m). +func (f AllowListEntryFunc) Mutate(ctx context.Context, m ent.Mutation) (ent.Value, error) { + if mv, ok := m.(*ent.AllowListEntryMutation); ok { + return f(ctx, mv) + } + return nil, fmt.Errorf("unexpected mutation type %T. expect *ent.AllowListEntryMutation", m) +} + +// The ApiKeyFunc type is an adapter to allow the use of ordinary +// function as ApiKey mutator. +type ApiKeyFunc func(context.Context, *ent.ApiKeyMutation) (ent.Value, error) + +// Mutate calls f(ctx, m). +func (f ApiKeyFunc) Mutate(ctx context.Context, m ent.Mutation) (ent.Value, error) { + if mv, ok := m.(*ent.ApiKeyMutation); ok { + return f(ctx, mv) + } + return nil, fmt.Errorf("unexpected mutation type %T. expect *ent.ApiKeyMutation", m) +} + +// The BrokerDispatchFunc type is an adapter to allow the use of ordinary +// function as BrokerDispatch mutator. +type BrokerDispatchFunc func(context.Context, *ent.BrokerDispatchMutation) (ent.Value, error) + +// Mutate calls f(ctx, m). +func (f BrokerDispatchFunc) Mutate(ctx context.Context, m ent.Mutation) (ent.Value, error) { + if mv, ok := m.(*ent.BrokerDispatchMutation); ok { + return f(ctx, mv) + } + return nil, fmt.Errorf("unexpected mutation type %T. expect *ent.BrokerDispatchMutation", m) +} + +// The BrokerJoinTokenFunc type is an adapter to allow the use of ordinary +// function as BrokerJoinToken mutator. +type BrokerJoinTokenFunc func(context.Context, *ent.BrokerJoinTokenMutation) (ent.Value, error) + +// Mutate calls f(ctx, m). +func (f BrokerJoinTokenFunc) Mutate(ctx context.Context, m ent.Mutation) (ent.Value, error) { + if mv, ok := m.(*ent.BrokerJoinTokenMutation); ok { + return f(ctx, mv) + } + return nil, fmt.Errorf("unexpected mutation type %T. expect *ent.BrokerJoinTokenMutation", m) +} + +// The BrokerSecretFunc type is an adapter to allow the use of ordinary +// function as BrokerSecret mutator. +type BrokerSecretFunc func(context.Context, *ent.BrokerSecretMutation) (ent.Value, error) + +// Mutate calls f(ctx, m). +func (f BrokerSecretFunc) Mutate(ctx context.Context, m ent.Mutation) (ent.Value, error) { + if mv, ok := m.(*ent.BrokerSecretMutation); ok { + return f(ctx, mv) + } + return nil, fmt.Errorf("unexpected mutation type %T. expect *ent.BrokerSecretMutation", m) +} + +// The EnvVarFunc type is an adapter to allow the use of ordinary +// function as EnvVar mutator. +type EnvVarFunc func(context.Context, *ent.EnvVarMutation) (ent.Value, error) + +// Mutate calls f(ctx, m). +func (f EnvVarFunc) Mutate(ctx context.Context, m ent.Mutation) (ent.Value, error) { + if mv, ok := m.(*ent.EnvVarMutation); ok { + return f(ctx, mv) + } + return nil, fmt.Errorf("unexpected mutation type %T. expect *ent.EnvVarMutation", m) +} + +// The GCPServiceAccountFunc type is an adapter to allow the use of ordinary +// function as GCPServiceAccount mutator. +type GCPServiceAccountFunc func(context.Context, *ent.GCPServiceAccountMutation) (ent.Value, error) + +// Mutate calls f(ctx, m). +func (f GCPServiceAccountFunc) Mutate(ctx context.Context, m ent.Mutation) (ent.Value, error) { + if mv, ok := m.(*ent.GCPServiceAccountMutation); ok { + return f(ctx, mv) + } + return nil, fmt.Errorf("unexpected mutation type %T. expect *ent.GCPServiceAccountMutation", m) +} + +// The GithubInstallationFunc type is an adapter to allow the use of ordinary +// function as GithubInstallation mutator. +type GithubInstallationFunc func(context.Context, *ent.GithubInstallationMutation) (ent.Value, error) + +// Mutate calls f(ctx, m). +func (f GithubInstallationFunc) Mutate(ctx context.Context, m ent.Mutation) (ent.Value, error) { + if mv, ok := m.(*ent.GithubInstallationMutation); ok { + return f(ctx, mv) + } + return nil, fmt.Errorf("unexpected mutation type %T. expect *ent.GithubInstallationMutation", m) +} + // The GroupFunc type is an adapter to allow the use of ordinary // function as Group mutator. type GroupFunc func(context.Context, *ent.GroupMutation) (ent.Value, error) @@ -57,6 +153,90 @@ func (f GroupMembershipFunc) Mutate(ctx context.Context, m ent.Mutation) (ent.Va return nil, fmt.Errorf("unexpected mutation type %T. expect *ent.GroupMembershipMutation", m) } +// The HarnessConfigFunc type is an adapter to allow the use of ordinary +// function as HarnessConfig mutator. +type HarnessConfigFunc func(context.Context, *ent.HarnessConfigMutation) (ent.Value, error) + +// Mutate calls f(ctx, m). +func (f HarnessConfigFunc) Mutate(ctx context.Context, m ent.Mutation) (ent.Value, error) { + if mv, ok := m.(*ent.HarnessConfigMutation); ok { + return f(ctx, mv) + } + return nil, fmt.Errorf("unexpected mutation type %T. expect *ent.HarnessConfigMutation", m) +} + +// The InviteCodeFunc type is an adapter to allow the use of ordinary +// function as InviteCode mutator. +type InviteCodeFunc func(context.Context, *ent.InviteCodeMutation) (ent.Value, error) + +// Mutate calls f(ctx, m). +func (f InviteCodeFunc) Mutate(ctx context.Context, m ent.Mutation) (ent.Value, error) { + if mv, ok := m.(*ent.InviteCodeMutation); ok { + return f(ctx, mv) + } + return nil, fmt.Errorf("unexpected mutation type %T. expect *ent.InviteCodeMutation", m) +} + +// The MaintenanceOperationFunc type is an adapter to allow the use of ordinary +// function as MaintenanceOperation mutator. +type MaintenanceOperationFunc func(context.Context, *ent.MaintenanceOperationMutation) (ent.Value, error) + +// Mutate calls f(ctx, m). +func (f MaintenanceOperationFunc) Mutate(ctx context.Context, m ent.Mutation) (ent.Value, error) { + if mv, ok := m.(*ent.MaintenanceOperationMutation); ok { + return f(ctx, mv) + } + return nil, fmt.Errorf("unexpected mutation type %T. expect *ent.MaintenanceOperationMutation", m) +} + +// The MaintenanceOperationRunFunc type is an adapter to allow the use of ordinary +// function as MaintenanceOperationRun mutator. +type MaintenanceOperationRunFunc func(context.Context, *ent.MaintenanceOperationRunMutation) (ent.Value, error) + +// Mutate calls f(ctx, m). +func (f MaintenanceOperationRunFunc) Mutate(ctx context.Context, m ent.Mutation) (ent.Value, error) { + if mv, ok := m.(*ent.MaintenanceOperationRunMutation); ok { + return f(ctx, mv) + } + return nil, fmt.Errorf("unexpected mutation type %T. expect *ent.MaintenanceOperationRunMutation", m) +} + +// The MessageFunc type is an adapter to allow the use of ordinary +// function as Message mutator. +type MessageFunc func(context.Context, *ent.MessageMutation) (ent.Value, error) + +// Mutate calls f(ctx, m). +func (f MessageFunc) Mutate(ctx context.Context, m ent.Mutation) (ent.Value, error) { + if mv, ok := m.(*ent.MessageMutation); ok { + return f(ctx, mv) + } + return nil, fmt.Errorf("unexpected mutation type %T. expect *ent.MessageMutation", m) +} + +// The NotificationFunc type is an adapter to allow the use of ordinary +// function as Notification mutator. +type NotificationFunc func(context.Context, *ent.NotificationMutation) (ent.Value, error) + +// Mutate calls f(ctx, m). +func (f NotificationFunc) Mutate(ctx context.Context, m ent.Mutation) (ent.Value, error) { + if mv, ok := m.(*ent.NotificationMutation); ok { + return f(ctx, mv) + } + return nil, fmt.Errorf("unexpected mutation type %T. expect *ent.NotificationMutation", m) +} + +// The NotificationSubscriptionFunc type is an adapter to allow the use of ordinary +// function as NotificationSubscription mutator. +type NotificationSubscriptionFunc func(context.Context, *ent.NotificationSubscriptionMutation) (ent.Value, error) + +// Mutate calls f(ctx, m). +func (f NotificationSubscriptionFunc) Mutate(ctx context.Context, m ent.Mutation) (ent.Value, error) { + if mv, ok := m.(*ent.NotificationSubscriptionMutation); ok { + return f(ctx, mv) + } + return nil, fmt.Errorf("unexpected mutation type %T. expect *ent.NotificationSubscriptionMutation", m) +} + // The PolicyBindingFunc type is an adapter to allow the use of ordinary // function as PolicyBinding mutator. type PolicyBindingFunc func(context.Context, *ent.PolicyBindingMutation) (ent.Value, error) @@ -81,6 +261,102 @@ func (f ProjectFunc) Mutate(ctx context.Context, m ent.Mutation) (ent.Value, err return nil, fmt.Errorf("unexpected mutation type %T. expect *ent.ProjectMutation", m) } +// The ProjectContributorFunc type is an adapter to allow the use of ordinary +// function as ProjectContributor mutator. +type ProjectContributorFunc func(context.Context, *ent.ProjectContributorMutation) (ent.Value, error) + +// Mutate calls f(ctx, m). +func (f ProjectContributorFunc) Mutate(ctx context.Context, m ent.Mutation) (ent.Value, error) { + if mv, ok := m.(*ent.ProjectContributorMutation); ok { + return f(ctx, mv) + } + return nil, fmt.Errorf("unexpected mutation type %T. expect *ent.ProjectContributorMutation", m) +} + +// The ProjectSyncStateFunc type is an adapter to allow the use of ordinary +// function as ProjectSyncState mutator. +type ProjectSyncStateFunc func(context.Context, *ent.ProjectSyncStateMutation) (ent.Value, error) + +// Mutate calls f(ctx, m). +func (f ProjectSyncStateFunc) Mutate(ctx context.Context, m ent.Mutation) (ent.Value, error) { + if mv, ok := m.(*ent.ProjectSyncStateMutation); ok { + return f(ctx, mv) + } + return nil, fmt.Errorf("unexpected mutation type %T. expect *ent.ProjectSyncStateMutation", m) +} + +// The RuntimeBrokerFunc type is an adapter to allow the use of ordinary +// function as RuntimeBroker mutator. +type RuntimeBrokerFunc func(context.Context, *ent.RuntimeBrokerMutation) (ent.Value, error) + +// Mutate calls f(ctx, m). +func (f RuntimeBrokerFunc) Mutate(ctx context.Context, m ent.Mutation) (ent.Value, error) { + if mv, ok := m.(*ent.RuntimeBrokerMutation); ok { + return f(ctx, mv) + } + return nil, fmt.Errorf("unexpected mutation type %T. expect *ent.RuntimeBrokerMutation", m) +} + +// The ScheduleFunc type is an adapter to allow the use of ordinary +// function as Schedule mutator. +type ScheduleFunc func(context.Context, *ent.ScheduleMutation) (ent.Value, error) + +// Mutate calls f(ctx, m). +func (f ScheduleFunc) Mutate(ctx context.Context, m ent.Mutation) (ent.Value, error) { + if mv, ok := m.(*ent.ScheduleMutation); ok { + return f(ctx, mv) + } + return nil, fmt.Errorf("unexpected mutation type %T. expect *ent.ScheduleMutation", m) +} + +// The ScheduledEventFunc type is an adapter to allow the use of ordinary +// function as ScheduledEvent mutator. +type ScheduledEventFunc func(context.Context, *ent.ScheduledEventMutation) (ent.Value, error) + +// Mutate calls f(ctx, m). +func (f ScheduledEventFunc) Mutate(ctx context.Context, m ent.Mutation) (ent.Value, error) { + if mv, ok := m.(*ent.ScheduledEventMutation); ok { + return f(ctx, mv) + } + return nil, fmt.Errorf("unexpected mutation type %T. expect *ent.ScheduledEventMutation", m) +} + +// The SecretFunc type is an adapter to allow the use of ordinary +// function as Secret mutator. +type SecretFunc func(context.Context, *ent.SecretMutation) (ent.Value, error) + +// Mutate calls f(ctx, m). +func (f SecretFunc) Mutate(ctx context.Context, m ent.Mutation) (ent.Value, error) { + if mv, ok := m.(*ent.SecretMutation); ok { + return f(ctx, mv) + } + return nil, fmt.Errorf("unexpected mutation type %T. expect *ent.SecretMutation", m) +} + +// The SubscriptionTemplateFunc type is an adapter to allow the use of ordinary +// function as SubscriptionTemplate mutator. +type SubscriptionTemplateFunc func(context.Context, *ent.SubscriptionTemplateMutation) (ent.Value, error) + +// Mutate calls f(ctx, m). +func (f SubscriptionTemplateFunc) Mutate(ctx context.Context, m ent.Mutation) (ent.Value, error) { + if mv, ok := m.(*ent.SubscriptionTemplateMutation); ok { + return f(ctx, mv) + } + return nil, fmt.Errorf("unexpected mutation type %T. expect *ent.SubscriptionTemplateMutation", m) +} + +// The TemplateFunc type is an adapter to allow the use of ordinary +// function as Template mutator. +type TemplateFunc func(context.Context, *ent.TemplateMutation) (ent.Value, error) + +// Mutate calls f(ctx, m). +func (f TemplateFunc) Mutate(ctx context.Context, m ent.Mutation) (ent.Value, error) { + if mv, ok := m.(*ent.TemplateMutation); ok { + return f(ctx, mv) + } + return nil, fmt.Errorf("unexpected mutation type %T. expect *ent.TemplateMutation", m) +} + // The UserFunc type is an adapter to allow the use of ordinary // function as User mutator. type UserFunc func(context.Context, *ent.UserMutation) (ent.Value, error) @@ -93,6 +369,18 @@ func (f UserFunc) Mutate(ctx context.Context, m ent.Mutation) (ent.Value, error) return nil, fmt.Errorf("unexpected mutation type %T. expect *ent.UserMutation", m) } +// The UserAccessTokenFunc type is an adapter to allow the use of ordinary +// function as UserAccessToken mutator. +type UserAccessTokenFunc func(context.Context, *ent.UserAccessTokenMutation) (ent.Value, error) + +// Mutate calls f(ctx, m). +func (f UserAccessTokenFunc) Mutate(ctx context.Context, m ent.Mutation) (ent.Value, error) { + if mv, ok := m.(*ent.UserAccessTokenMutation); ok { + return f(ctx, mv) + } + return nil, fmt.Errorf("unexpected mutation type %T. expect *ent.UserAccessTokenMutation", m) +} + // Condition is a hook condition function. type Condition func(context.Context, ent.Mutation) bool diff --git a/pkg/ent/invitecode.go b/pkg/ent/invitecode.go new file mode 100644 index 000000000..09c2479b5 --- /dev/null +++ b/pkg/ent/invitecode.go @@ -0,0 +1,198 @@ +// Code generated by ent, DO NOT EDIT. + +package ent + +import ( + "fmt" + "strings" + "time" + + "entgo.io/ent" + "entgo.io/ent/dialect/sql" + "github.com/GoogleCloudPlatform/scion/pkg/ent/invitecode" + "github.com/google/uuid" +) + +// InviteCode is the model entity for the InviteCode schema. +type InviteCode struct { + config `json:"-"` + // ID of the ent. + ID uuid.UUID `json:"id,omitempty"` + // CodeHash holds the value of the "code_hash" field. + CodeHash string `json:"-"` + // CodePrefix holds the value of the "code_prefix" field. + CodePrefix string `json:"code_prefix,omitempty"` + // MaxUses holds the value of the "max_uses" field. + MaxUses int `json:"max_uses,omitempty"` + // UseCount holds the value of the "use_count" field. + UseCount int `json:"use_count,omitempty"` + // ExpiresAt holds the value of the "expires_at" field. + ExpiresAt time.Time `json:"expires_at,omitempty"` + // Revoked holds the value of the "revoked" field. + Revoked bool `json:"revoked,omitempty"` + // CreatedBy holds the value of the "created_by" field. + CreatedBy string `json:"created_by,omitempty"` + // Note holds the value of the "note" field. + Note string `json:"note,omitempty"` + // Created holds the value of the "created" field. + Created time.Time `json:"created,omitempty"` + selectValues sql.SelectValues +} + +// scanValues returns the types for scanning values from sql.Rows. +func (*InviteCode) scanValues(columns []string) ([]any, error) { + values := make([]any, len(columns)) + for i := range columns { + switch columns[i] { + case invitecode.FieldRevoked: + values[i] = new(sql.NullBool) + case invitecode.FieldMaxUses, invitecode.FieldUseCount: + values[i] = new(sql.NullInt64) + case invitecode.FieldCodeHash, invitecode.FieldCodePrefix, invitecode.FieldCreatedBy, invitecode.FieldNote: + values[i] = new(sql.NullString) + case invitecode.FieldExpiresAt, invitecode.FieldCreated: + values[i] = new(sql.NullTime) + case invitecode.FieldID: + values[i] = new(uuid.UUID) + default: + values[i] = new(sql.UnknownType) + } + } + return values, nil +} + +// assignValues assigns the values that were returned from sql.Rows (after scanning) +// to the InviteCode fields. +func (_m *InviteCode) assignValues(columns []string, values []any) error { + if m, n := len(values), len(columns); m < n { + return fmt.Errorf("mismatch number of scan values: %d != %d", m, n) + } + for i := range columns { + switch columns[i] { + case invitecode.FieldID: + if value, ok := values[i].(*uuid.UUID); !ok { + return fmt.Errorf("unexpected type %T for field id", values[i]) + } else if value != nil { + _m.ID = *value + } + case invitecode.FieldCodeHash: + if value, ok := values[i].(*sql.NullString); !ok { + return fmt.Errorf("unexpected type %T for field code_hash", values[i]) + } else if value.Valid { + _m.CodeHash = value.String + } + case invitecode.FieldCodePrefix: + if value, ok := values[i].(*sql.NullString); !ok { + return fmt.Errorf("unexpected type %T for field code_prefix", values[i]) + } else if value.Valid { + _m.CodePrefix = value.String + } + case invitecode.FieldMaxUses: + if value, ok := values[i].(*sql.NullInt64); !ok { + return fmt.Errorf("unexpected type %T for field max_uses", values[i]) + } else if value.Valid { + _m.MaxUses = int(value.Int64) + } + case invitecode.FieldUseCount: + if value, ok := values[i].(*sql.NullInt64); !ok { + return fmt.Errorf("unexpected type %T for field use_count", values[i]) + } else if value.Valid { + _m.UseCount = int(value.Int64) + } + case invitecode.FieldExpiresAt: + if value, ok := values[i].(*sql.NullTime); !ok { + return fmt.Errorf("unexpected type %T for field expires_at", values[i]) + } else if value.Valid { + _m.ExpiresAt = value.Time + } + case invitecode.FieldRevoked: + if value, ok := values[i].(*sql.NullBool); !ok { + return fmt.Errorf("unexpected type %T for field revoked", values[i]) + } else if value.Valid { + _m.Revoked = value.Bool + } + case invitecode.FieldCreatedBy: + if value, ok := values[i].(*sql.NullString); !ok { + return fmt.Errorf("unexpected type %T for field created_by", values[i]) + } else if value.Valid { + _m.CreatedBy = value.String + } + case invitecode.FieldNote: + if value, ok := values[i].(*sql.NullString); !ok { + return fmt.Errorf("unexpected type %T for field note", values[i]) + } else if value.Valid { + _m.Note = value.String + } + case invitecode.FieldCreated: + if value, ok := values[i].(*sql.NullTime); !ok { + return fmt.Errorf("unexpected type %T for field created", values[i]) + } else if value.Valid { + _m.Created = value.Time + } + default: + _m.selectValues.Set(columns[i], values[i]) + } + } + return nil +} + +// Value returns the ent.Value that was dynamically selected and assigned to the InviteCode. +// This includes values selected through modifiers, order, etc. +func (_m *InviteCode) Value(name string) (ent.Value, error) { + return _m.selectValues.Get(name) +} + +// Update returns a builder for updating this InviteCode. +// Note that you need to call InviteCode.Unwrap() before calling this method if this InviteCode +// was returned from a transaction, and the transaction was committed or rolled back. +func (_m *InviteCode) Update() *InviteCodeUpdateOne { + return NewInviteCodeClient(_m.config).UpdateOne(_m) +} + +// Unwrap unwraps the InviteCode entity that was returned from a transaction after it was closed, +// so that all future queries will be executed through the driver which created the transaction. +func (_m *InviteCode) Unwrap() *InviteCode { + _tx, ok := _m.config.driver.(*txDriver) + if !ok { + panic("ent: InviteCode is not a transactional entity") + } + _m.config.driver = _tx.drv + return _m +} + +// String implements the fmt.Stringer. +func (_m *InviteCode) String() string { + var builder strings.Builder + builder.WriteString("InviteCode(") + builder.WriteString(fmt.Sprintf("id=%v, ", _m.ID)) + builder.WriteString("code_hash=") + builder.WriteString(", ") + builder.WriteString("code_prefix=") + builder.WriteString(_m.CodePrefix) + builder.WriteString(", ") + builder.WriteString("max_uses=") + builder.WriteString(fmt.Sprintf("%v", _m.MaxUses)) + builder.WriteString(", ") + builder.WriteString("use_count=") + builder.WriteString(fmt.Sprintf("%v", _m.UseCount)) + builder.WriteString(", ") + builder.WriteString("expires_at=") + builder.WriteString(_m.ExpiresAt.Format(time.ANSIC)) + builder.WriteString(", ") + builder.WriteString("revoked=") + builder.WriteString(fmt.Sprintf("%v", _m.Revoked)) + builder.WriteString(", ") + builder.WriteString("created_by=") + builder.WriteString(_m.CreatedBy) + builder.WriteString(", ") + builder.WriteString("note=") + builder.WriteString(_m.Note) + builder.WriteString(", ") + builder.WriteString("created=") + builder.WriteString(_m.Created.Format(time.ANSIC)) + builder.WriteByte(')') + return builder.String() +} + +// InviteCodes is a parsable slice of InviteCode. +type InviteCodes []*InviteCode diff --git a/pkg/ent/invitecode/invitecode.go b/pkg/ent/invitecode/invitecode.go new file mode 100644 index 000000000..b19d09528 --- /dev/null +++ b/pkg/ent/invitecode/invitecode.go @@ -0,0 +1,135 @@ +// Code generated by ent, DO NOT EDIT. + +package invitecode + +import ( + "time" + + "entgo.io/ent/dialect/sql" + "github.com/google/uuid" +) + +const ( + // Label holds the string label denoting the invitecode type in the database. + Label = "invite_code" + // FieldID holds the string denoting the id field in the database. + FieldID = "id" + // FieldCodeHash holds the string denoting the code_hash field in the database. + FieldCodeHash = "code_hash" + // FieldCodePrefix holds the string denoting the code_prefix field in the database. + FieldCodePrefix = "code_prefix" + // FieldMaxUses holds the string denoting the max_uses field in the database. + FieldMaxUses = "max_uses" + // FieldUseCount holds the string denoting the use_count field in the database. + FieldUseCount = "use_count" + // FieldExpiresAt holds the string denoting the expires_at field in the database. + FieldExpiresAt = "expires_at" + // FieldRevoked holds the string denoting the revoked field in the database. + FieldRevoked = "revoked" + // FieldCreatedBy holds the string denoting the created_by field in the database. + FieldCreatedBy = "created_by" + // FieldNote holds the string denoting the note field in the database. + FieldNote = "note" + // FieldCreated holds the string denoting the created field in the database. + FieldCreated = "created" + // Table holds the table name of the invitecode in the database. + Table = "invite_codes" +) + +// Columns holds all SQL columns for invitecode fields. +var Columns = []string{ + FieldID, + FieldCodeHash, + FieldCodePrefix, + FieldMaxUses, + FieldUseCount, + FieldExpiresAt, + FieldRevoked, + FieldCreatedBy, + FieldNote, + FieldCreated, +} + +// ValidColumn reports if the column name is valid (part of the table columns). +func ValidColumn(column string) bool { + for i := range Columns { + if column == Columns[i] { + return true + } + } + return false +} + +var ( + // CodeHashValidator is a validator for the "code_hash" field. It is called by the builders before save. + CodeHashValidator func(string) error + // CodePrefixValidator is a validator for the "code_prefix" field. It is called by the builders before save. + CodePrefixValidator func(string) error + // DefaultMaxUses holds the default value on creation for the "max_uses" field. + DefaultMaxUses int + // DefaultUseCount holds the default value on creation for the "use_count" field. + DefaultUseCount int + // DefaultRevoked holds the default value on creation for the "revoked" field. + DefaultRevoked bool + // CreatedByValidator is a validator for the "created_by" field. It is called by the builders before save. + CreatedByValidator func(string) error + // DefaultNote holds the default value on creation for the "note" field. + DefaultNote string + // DefaultCreated holds the default value on creation for the "created" field. + DefaultCreated func() time.Time + // DefaultID holds the default value on creation for the "id" field. + DefaultID func() uuid.UUID +) + +// OrderOption defines the ordering options for the InviteCode queries. +type OrderOption func(*sql.Selector) + +// ByID orders the results by the id field. +func ByID(opts ...sql.OrderTermOption) OrderOption { + return sql.OrderByField(FieldID, opts...).ToFunc() +} + +// ByCodeHash orders the results by the code_hash field. +func ByCodeHash(opts ...sql.OrderTermOption) OrderOption { + return sql.OrderByField(FieldCodeHash, opts...).ToFunc() +} + +// ByCodePrefix orders the results by the code_prefix field. +func ByCodePrefix(opts ...sql.OrderTermOption) OrderOption { + return sql.OrderByField(FieldCodePrefix, opts...).ToFunc() +} + +// ByMaxUses orders the results by the max_uses field. +func ByMaxUses(opts ...sql.OrderTermOption) OrderOption { + return sql.OrderByField(FieldMaxUses, opts...).ToFunc() +} + +// ByUseCount orders the results by the use_count field. +func ByUseCount(opts ...sql.OrderTermOption) OrderOption { + return sql.OrderByField(FieldUseCount, opts...).ToFunc() +} + +// ByExpiresAt orders the results by the expires_at field. +func ByExpiresAt(opts ...sql.OrderTermOption) OrderOption { + return sql.OrderByField(FieldExpiresAt, opts...).ToFunc() +} + +// ByRevoked orders the results by the revoked field. +func ByRevoked(opts ...sql.OrderTermOption) OrderOption { + return sql.OrderByField(FieldRevoked, opts...).ToFunc() +} + +// ByCreatedBy orders the results by the created_by field. +func ByCreatedBy(opts ...sql.OrderTermOption) OrderOption { + return sql.OrderByField(FieldCreatedBy, opts...).ToFunc() +} + +// ByNote orders the results by the note field. +func ByNote(opts ...sql.OrderTermOption) OrderOption { + return sql.OrderByField(FieldNote, opts...).ToFunc() +} + +// ByCreated orders the results by the created field. +func ByCreated(opts ...sql.OrderTermOption) OrderOption { + return sql.OrderByField(FieldCreated, opts...).ToFunc() +} diff --git a/pkg/ent/invitecode/where.go b/pkg/ent/invitecode/where.go new file mode 100644 index 000000000..416cd9c71 --- /dev/null +++ b/pkg/ent/invitecode/where.go @@ -0,0 +1,546 @@ +// Code generated by ent, DO NOT EDIT. + +package invitecode + +import ( + "time" + + "entgo.io/ent/dialect/sql" + "github.com/GoogleCloudPlatform/scion/pkg/ent/predicate" + "github.com/google/uuid" +) + +// ID filters vertices based on their ID field. +func ID(id uuid.UUID) predicate.InviteCode { + return predicate.InviteCode(sql.FieldEQ(FieldID, id)) +} + +// IDEQ applies the EQ predicate on the ID field. +func IDEQ(id uuid.UUID) predicate.InviteCode { + return predicate.InviteCode(sql.FieldEQ(FieldID, id)) +} + +// IDNEQ applies the NEQ predicate on the ID field. +func IDNEQ(id uuid.UUID) predicate.InviteCode { + return predicate.InviteCode(sql.FieldNEQ(FieldID, id)) +} + +// IDIn applies the In predicate on the ID field. +func IDIn(ids ...uuid.UUID) predicate.InviteCode { + return predicate.InviteCode(sql.FieldIn(FieldID, ids...)) +} + +// IDNotIn applies the NotIn predicate on the ID field. +func IDNotIn(ids ...uuid.UUID) predicate.InviteCode { + return predicate.InviteCode(sql.FieldNotIn(FieldID, ids...)) +} + +// IDGT applies the GT predicate on the ID field. +func IDGT(id uuid.UUID) predicate.InviteCode { + return predicate.InviteCode(sql.FieldGT(FieldID, id)) +} + +// IDGTE applies the GTE predicate on the ID field. +func IDGTE(id uuid.UUID) predicate.InviteCode { + return predicate.InviteCode(sql.FieldGTE(FieldID, id)) +} + +// IDLT applies the LT predicate on the ID field. +func IDLT(id uuid.UUID) predicate.InviteCode { + return predicate.InviteCode(sql.FieldLT(FieldID, id)) +} + +// IDLTE applies the LTE predicate on the ID field. +func IDLTE(id uuid.UUID) predicate.InviteCode { + return predicate.InviteCode(sql.FieldLTE(FieldID, id)) +} + +// CodeHash applies equality check predicate on the "code_hash" field. It's identical to CodeHashEQ. +func CodeHash(v string) predicate.InviteCode { + return predicate.InviteCode(sql.FieldEQ(FieldCodeHash, v)) +} + +// CodePrefix applies equality check predicate on the "code_prefix" field. It's identical to CodePrefixEQ. +func CodePrefix(v string) predicate.InviteCode { + return predicate.InviteCode(sql.FieldEQ(FieldCodePrefix, v)) +} + +// MaxUses applies equality check predicate on the "max_uses" field. It's identical to MaxUsesEQ. +func MaxUses(v int) predicate.InviteCode { + return predicate.InviteCode(sql.FieldEQ(FieldMaxUses, v)) +} + +// UseCount applies equality check predicate on the "use_count" field. It's identical to UseCountEQ. +func UseCount(v int) predicate.InviteCode { + return predicate.InviteCode(sql.FieldEQ(FieldUseCount, v)) +} + +// ExpiresAt applies equality check predicate on the "expires_at" field. It's identical to ExpiresAtEQ. +func ExpiresAt(v time.Time) predicate.InviteCode { + return predicate.InviteCode(sql.FieldEQ(FieldExpiresAt, v)) +} + +// Revoked applies equality check predicate on the "revoked" field. It's identical to RevokedEQ. +func Revoked(v bool) predicate.InviteCode { + return predicate.InviteCode(sql.FieldEQ(FieldRevoked, v)) +} + +// CreatedBy applies equality check predicate on the "created_by" field. It's identical to CreatedByEQ. +func CreatedBy(v string) predicate.InviteCode { + return predicate.InviteCode(sql.FieldEQ(FieldCreatedBy, v)) +} + +// Note applies equality check predicate on the "note" field. It's identical to NoteEQ. +func Note(v string) predicate.InviteCode { + return predicate.InviteCode(sql.FieldEQ(FieldNote, v)) +} + +// Created applies equality check predicate on the "created" field. It's identical to CreatedEQ. +func Created(v time.Time) predicate.InviteCode { + return predicate.InviteCode(sql.FieldEQ(FieldCreated, v)) +} + +// CodeHashEQ applies the EQ predicate on the "code_hash" field. +func CodeHashEQ(v string) predicate.InviteCode { + return predicate.InviteCode(sql.FieldEQ(FieldCodeHash, v)) +} + +// CodeHashNEQ applies the NEQ predicate on the "code_hash" field. +func CodeHashNEQ(v string) predicate.InviteCode { + return predicate.InviteCode(sql.FieldNEQ(FieldCodeHash, v)) +} + +// CodeHashIn applies the In predicate on the "code_hash" field. +func CodeHashIn(vs ...string) predicate.InviteCode { + return predicate.InviteCode(sql.FieldIn(FieldCodeHash, vs...)) +} + +// CodeHashNotIn applies the NotIn predicate on the "code_hash" field. +func CodeHashNotIn(vs ...string) predicate.InviteCode { + return predicate.InviteCode(sql.FieldNotIn(FieldCodeHash, vs...)) +} + +// CodeHashGT applies the GT predicate on the "code_hash" field. +func CodeHashGT(v string) predicate.InviteCode { + return predicate.InviteCode(sql.FieldGT(FieldCodeHash, v)) +} + +// CodeHashGTE applies the GTE predicate on the "code_hash" field. +func CodeHashGTE(v string) predicate.InviteCode { + return predicate.InviteCode(sql.FieldGTE(FieldCodeHash, v)) +} + +// CodeHashLT applies the LT predicate on the "code_hash" field. +func CodeHashLT(v string) predicate.InviteCode { + return predicate.InviteCode(sql.FieldLT(FieldCodeHash, v)) +} + +// CodeHashLTE applies the LTE predicate on the "code_hash" field. +func CodeHashLTE(v string) predicate.InviteCode { + return predicate.InviteCode(sql.FieldLTE(FieldCodeHash, v)) +} + +// CodeHashContains applies the Contains predicate on the "code_hash" field. +func CodeHashContains(v string) predicate.InviteCode { + return predicate.InviteCode(sql.FieldContains(FieldCodeHash, v)) +} + +// CodeHashHasPrefix applies the HasPrefix predicate on the "code_hash" field. +func CodeHashHasPrefix(v string) predicate.InviteCode { + return predicate.InviteCode(sql.FieldHasPrefix(FieldCodeHash, v)) +} + +// CodeHashHasSuffix applies the HasSuffix predicate on the "code_hash" field. +func CodeHashHasSuffix(v string) predicate.InviteCode { + return predicate.InviteCode(sql.FieldHasSuffix(FieldCodeHash, v)) +} + +// CodeHashEqualFold applies the EqualFold predicate on the "code_hash" field. +func CodeHashEqualFold(v string) predicate.InviteCode { + return predicate.InviteCode(sql.FieldEqualFold(FieldCodeHash, v)) +} + +// CodeHashContainsFold applies the ContainsFold predicate on the "code_hash" field. +func CodeHashContainsFold(v string) predicate.InviteCode { + return predicate.InviteCode(sql.FieldContainsFold(FieldCodeHash, v)) +} + +// CodePrefixEQ applies the EQ predicate on the "code_prefix" field. +func CodePrefixEQ(v string) predicate.InviteCode { + return predicate.InviteCode(sql.FieldEQ(FieldCodePrefix, v)) +} + +// CodePrefixNEQ applies the NEQ predicate on the "code_prefix" field. +func CodePrefixNEQ(v string) predicate.InviteCode { + return predicate.InviteCode(sql.FieldNEQ(FieldCodePrefix, v)) +} + +// CodePrefixIn applies the In predicate on the "code_prefix" field. +func CodePrefixIn(vs ...string) predicate.InviteCode { + return predicate.InviteCode(sql.FieldIn(FieldCodePrefix, vs...)) +} + +// CodePrefixNotIn applies the NotIn predicate on the "code_prefix" field. +func CodePrefixNotIn(vs ...string) predicate.InviteCode { + return predicate.InviteCode(sql.FieldNotIn(FieldCodePrefix, vs...)) +} + +// CodePrefixGT applies the GT predicate on the "code_prefix" field. +func CodePrefixGT(v string) predicate.InviteCode { + return predicate.InviteCode(sql.FieldGT(FieldCodePrefix, v)) +} + +// CodePrefixGTE applies the GTE predicate on the "code_prefix" field. +func CodePrefixGTE(v string) predicate.InviteCode { + return predicate.InviteCode(sql.FieldGTE(FieldCodePrefix, v)) +} + +// CodePrefixLT applies the LT predicate on the "code_prefix" field. +func CodePrefixLT(v string) predicate.InviteCode { + return predicate.InviteCode(sql.FieldLT(FieldCodePrefix, v)) +} + +// CodePrefixLTE applies the LTE predicate on the "code_prefix" field. +func CodePrefixLTE(v string) predicate.InviteCode { + return predicate.InviteCode(sql.FieldLTE(FieldCodePrefix, v)) +} + +// CodePrefixContains applies the Contains predicate on the "code_prefix" field. +func CodePrefixContains(v string) predicate.InviteCode { + return predicate.InviteCode(sql.FieldContains(FieldCodePrefix, v)) +} + +// CodePrefixHasPrefix applies the HasPrefix predicate on the "code_prefix" field. +func CodePrefixHasPrefix(v string) predicate.InviteCode { + return predicate.InviteCode(sql.FieldHasPrefix(FieldCodePrefix, v)) +} + +// CodePrefixHasSuffix applies the HasSuffix predicate on the "code_prefix" field. +func CodePrefixHasSuffix(v string) predicate.InviteCode { + return predicate.InviteCode(sql.FieldHasSuffix(FieldCodePrefix, v)) +} + +// CodePrefixEqualFold applies the EqualFold predicate on the "code_prefix" field. +func CodePrefixEqualFold(v string) predicate.InviteCode { + return predicate.InviteCode(sql.FieldEqualFold(FieldCodePrefix, v)) +} + +// CodePrefixContainsFold applies the ContainsFold predicate on the "code_prefix" field. +func CodePrefixContainsFold(v string) predicate.InviteCode { + return predicate.InviteCode(sql.FieldContainsFold(FieldCodePrefix, v)) +} + +// MaxUsesEQ applies the EQ predicate on the "max_uses" field. +func MaxUsesEQ(v int) predicate.InviteCode { + return predicate.InviteCode(sql.FieldEQ(FieldMaxUses, v)) +} + +// MaxUsesNEQ applies the NEQ predicate on the "max_uses" field. +func MaxUsesNEQ(v int) predicate.InviteCode { + return predicate.InviteCode(sql.FieldNEQ(FieldMaxUses, v)) +} + +// MaxUsesIn applies the In predicate on the "max_uses" field. +func MaxUsesIn(vs ...int) predicate.InviteCode { + return predicate.InviteCode(sql.FieldIn(FieldMaxUses, vs...)) +} + +// MaxUsesNotIn applies the NotIn predicate on the "max_uses" field. +func MaxUsesNotIn(vs ...int) predicate.InviteCode { + return predicate.InviteCode(sql.FieldNotIn(FieldMaxUses, vs...)) +} + +// MaxUsesGT applies the GT predicate on the "max_uses" field. +func MaxUsesGT(v int) predicate.InviteCode { + return predicate.InviteCode(sql.FieldGT(FieldMaxUses, v)) +} + +// MaxUsesGTE applies the GTE predicate on the "max_uses" field. +func MaxUsesGTE(v int) predicate.InviteCode { + return predicate.InviteCode(sql.FieldGTE(FieldMaxUses, v)) +} + +// MaxUsesLT applies the LT predicate on the "max_uses" field. +func MaxUsesLT(v int) predicate.InviteCode { + return predicate.InviteCode(sql.FieldLT(FieldMaxUses, v)) +} + +// MaxUsesLTE applies the LTE predicate on the "max_uses" field. +func MaxUsesLTE(v int) predicate.InviteCode { + return predicate.InviteCode(sql.FieldLTE(FieldMaxUses, v)) +} + +// UseCountEQ applies the EQ predicate on the "use_count" field. +func UseCountEQ(v int) predicate.InviteCode { + return predicate.InviteCode(sql.FieldEQ(FieldUseCount, v)) +} + +// UseCountNEQ applies the NEQ predicate on the "use_count" field. +func UseCountNEQ(v int) predicate.InviteCode { + return predicate.InviteCode(sql.FieldNEQ(FieldUseCount, v)) +} + +// UseCountIn applies the In predicate on the "use_count" field. +func UseCountIn(vs ...int) predicate.InviteCode { + return predicate.InviteCode(sql.FieldIn(FieldUseCount, vs...)) +} + +// UseCountNotIn applies the NotIn predicate on the "use_count" field. +func UseCountNotIn(vs ...int) predicate.InviteCode { + return predicate.InviteCode(sql.FieldNotIn(FieldUseCount, vs...)) +} + +// UseCountGT applies the GT predicate on the "use_count" field. +func UseCountGT(v int) predicate.InviteCode { + return predicate.InviteCode(sql.FieldGT(FieldUseCount, v)) +} + +// UseCountGTE applies the GTE predicate on the "use_count" field. +func UseCountGTE(v int) predicate.InviteCode { + return predicate.InviteCode(sql.FieldGTE(FieldUseCount, v)) +} + +// UseCountLT applies the LT predicate on the "use_count" field. +func UseCountLT(v int) predicate.InviteCode { + return predicate.InviteCode(sql.FieldLT(FieldUseCount, v)) +} + +// UseCountLTE applies the LTE predicate on the "use_count" field. +func UseCountLTE(v int) predicate.InviteCode { + return predicate.InviteCode(sql.FieldLTE(FieldUseCount, v)) +} + +// ExpiresAtEQ applies the EQ predicate on the "expires_at" field. +func ExpiresAtEQ(v time.Time) predicate.InviteCode { + return predicate.InviteCode(sql.FieldEQ(FieldExpiresAt, v)) +} + +// ExpiresAtNEQ applies the NEQ predicate on the "expires_at" field. +func ExpiresAtNEQ(v time.Time) predicate.InviteCode { + return predicate.InviteCode(sql.FieldNEQ(FieldExpiresAt, v)) +} + +// ExpiresAtIn applies the In predicate on the "expires_at" field. +func ExpiresAtIn(vs ...time.Time) predicate.InviteCode { + return predicate.InviteCode(sql.FieldIn(FieldExpiresAt, vs...)) +} + +// ExpiresAtNotIn applies the NotIn predicate on the "expires_at" field. +func ExpiresAtNotIn(vs ...time.Time) predicate.InviteCode { + return predicate.InviteCode(sql.FieldNotIn(FieldExpiresAt, vs...)) +} + +// ExpiresAtGT applies the GT predicate on the "expires_at" field. +func ExpiresAtGT(v time.Time) predicate.InviteCode { + return predicate.InviteCode(sql.FieldGT(FieldExpiresAt, v)) +} + +// ExpiresAtGTE applies the GTE predicate on the "expires_at" field. +func ExpiresAtGTE(v time.Time) predicate.InviteCode { + return predicate.InviteCode(sql.FieldGTE(FieldExpiresAt, v)) +} + +// ExpiresAtLT applies the LT predicate on the "expires_at" field. +func ExpiresAtLT(v time.Time) predicate.InviteCode { + return predicate.InviteCode(sql.FieldLT(FieldExpiresAt, v)) +} + +// ExpiresAtLTE applies the LTE predicate on the "expires_at" field. +func ExpiresAtLTE(v time.Time) predicate.InviteCode { + return predicate.InviteCode(sql.FieldLTE(FieldExpiresAt, v)) +} + +// RevokedEQ applies the EQ predicate on the "revoked" field. +func RevokedEQ(v bool) predicate.InviteCode { + return predicate.InviteCode(sql.FieldEQ(FieldRevoked, v)) +} + +// RevokedNEQ applies the NEQ predicate on the "revoked" field. +func RevokedNEQ(v bool) predicate.InviteCode { + return predicate.InviteCode(sql.FieldNEQ(FieldRevoked, v)) +} + +// CreatedByEQ applies the EQ predicate on the "created_by" field. +func CreatedByEQ(v string) predicate.InviteCode { + return predicate.InviteCode(sql.FieldEQ(FieldCreatedBy, v)) +} + +// CreatedByNEQ applies the NEQ predicate on the "created_by" field. +func CreatedByNEQ(v string) predicate.InviteCode { + return predicate.InviteCode(sql.FieldNEQ(FieldCreatedBy, v)) +} + +// CreatedByIn applies the In predicate on the "created_by" field. +func CreatedByIn(vs ...string) predicate.InviteCode { + return predicate.InviteCode(sql.FieldIn(FieldCreatedBy, vs...)) +} + +// CreatedByNotIn applies the NotIn predicate on the "created_by" field. +func CreatedByNotIn(vs ...string) predicate.InviteCode { + return predicate.InviteCode(sql.FieldNotIn(FieldCreatedBy, vs...)) +} + +// CreatedByGT applies the GT predicate on the "created_by" field. +func CreatedByGT(v string) predicate.InviteCode { + return predicate.InviteCode(sql.FieldGT(FieldCreatedBy, v)) +} + +// CreatedByGTE applies the GTE predicate on the "created_by" field. +func CreatedByGTE(v string) predicate.InviteCode { + return predicate.InviteCode(sql.FieldGTE(FieldCreatedBy, v)) +} + +// CreatedByLT applies the LT predicate on the "created_by" field. +func CreatedByLT(v string) predicate.InviteCode { + return predicate.InviteCode(sql.FieldLT(FieldCreatedBy, v)) +} + +// CreatedByLTE applies the LTE predicate on the "created_by" field. +func CreatedByLTE(v string) predicate.InviteCode { + return predicate.InviteCode(sql.FieldLTE(FieldCreatedBy, v)) +} + +// CreatedByContains applies the Contains predicate on the "created_by" field. +func CreatedByContains(v string) predicate.InviteCode { + return predicate.InviteCode(sql.FieldContains(FieldCreatedBy, v)) +} + +// CreatedByHasPrefix applies the HasPrefix predicate on the "created_by" field. +func CreatedByHasPrefix(v string) predicate.InviteCode { + return predicate.InviteCode(sql.FieldHasPrefix(FieldCreatedBy, v)) +} + +// CreatedByHasSuffix applies the HasSuffix predicate on the "created_by" field. +func CreatedByHasSuffix(v string) predicate.InviteCode { + return predicate.InviteCode(sql.FieldHasSuffix(FieldCreatedBy, v)) +} + +// CreatedByEqualFold applies the EqualFold predicate on the "created_by" field. +func CreatedByEqualFold(v string) predicate.InviteCode { + return predicate.InviteCode(sql.FieldEqualFold(FieldCreatedBy, v)) +} + +// CreatedByContainsFold applies the ContainsFold predicate on the "created_by" field. +func CreatedByContainsFold(v string) predicate.InviteCode { + return predicate.InviteCode(sql.FieldContainsFold(FieldCreatedBy, v)) +} + +// NoteEQ applies the EQ predicate on the "note" field. +func NoteEQ(v string) predicate.InviteCode { + return predicate.InviteCode(sql.FieldEQ(FieldNote, v)) +} + +// NoteNEQ applies the NEQ predicate on the "note" field. +func NoteNEQ(v string) predicate.InviteCode { + return predicate.InviteCode(sql.FieldNEQ(FieldNote, v)) +} + +// NoteIn applies the In predicate on the "note" field. +func NoteIn(vs ...string) predicate.InviteCode { + return predicate.InviteCode(sql.FieldIn(FieldNote, vs...)) +} + +// NoteNotIn applies the NotIn predicate on the "note" field. +func NoteNotIn(vs ...string) predicate.InviteCode { + return predicate.InviteCode(sql.FieldNotIn(FieldNote, vs...)) +} + +// NoteGT applies the GT predicate on the "note" field. +func NoteGT(v string) predicate.InviteCode { + return predicate.InviteCode(sql.FieldGT(FieldNote, v)) +} + +// NoteGTE applies the GTE predicate on the "note" field. +func NoteGTE(v string) predicate.InviteCode { + return predicate.InviteCode(sql.FieldGTE(FieldNote, v)) +} + +// NoteLT applies the LT predicate on the "note" field. +func NoteLT(v string) predicate.InviteCode { + return predicate.InviteCode(sql.FieldLT(FieldNote, v)) +} + +// NoteLTE applies the LTE predicate on the "note" field. +func NoteLTE(v string) predicate.InviteCode { + return predicate.InviteCode(sql.FieldLTE(FieldNote, v)) +} + +// NoteContains applies the Contains predicate on the "note" field. +func NoteContains(v string) predicate.InviteCode { + return predicate.InviteCode(sql.FieldContains(FieldNote, v)) +} + +// NoteHasPrefix applies the HasPrefix predicate on the "note" field. +func NoteHasPrefix(v string) predicate.InviteCode { + return predicate.InviteCode(sql.FieldHasPrefix(FieldNote, v)) +} + +// NoteHasSuffix applies the HasSuffix predicate on the "note" field. +func NoteHasSuffix(v string) predicate.InviteCode { + return predicate.InviteCode(sql.FieldHasSuffix(FieldNote, v)) +} + +// NoteEqualFold applies the EqualFold predicate on the "note" field. +func NoteEqualFold(v string) predicate.InviteCode { + return predicate.InviteCode(sql.FieldEqualFold(FieldNote, v)) +} + +// NoteContainsFold applies the ContainsFold predicate on the "note" field. +func NoteContainsFold(v string) predicate.InviteCode { + return predicate.InviteCode(sql.FieldContainsFold(FieldNote, v)) +} + +// CreatedEQ applies the EQ predicate on the "created" field. +func CreatedEQ(v time.Time) predicate.InviteCode { + return predicate.InviteCode(sql.FieldEQ(FieldCreated, v)) +} + +// CreatedNEQ applies the NEQ predicate on the "created" field. +func CreatedNEQ(v time.Time) predicate.InviteCode { + return predicate.InviteCode(sql.FieldNEQ(FieldCreated, v)) +} + +// CreatedIn applies the In predicate on the "created" field. +func CreatedIn(vs ...time.Time) predicate.InviteCode { + return predicate.InviteCode(sql.FieldIn(FieldCreated, vs...)) +} + +// CreatedNotIn applies the NotIn predicate on the "created" field. +func CreatedNotIn(vs ...time.Time) predicate.InviteCode { + return predicate.InviteCode(sql.FieldNotIn(FieldCreated, vs...)) +} + +// CreatedGT applies the GT predicate on the "created" field. +func CreatedGT(v time.Time) predicate.InviteCode { + return predicate.InviteCode(sql.FieldGT(FieldCreated, v)) +} + +// CreatedGTE applies the GTE predicate on the "created" field. +func CreatedGTE(v time.Time) predicate.InviteCode { + return predicate.InviteCode(sql.FieldGTE(FieldCreated, v)) +} + +// CreatedLT applies the LT predicate on the "created" field. +func CreatedLT(v time.Time) predicate.InviteCode { + return predicate.InviteCode(sql.FieldLT(FieldCreated, v)) +} + +// CreatedLTE applies the LTE predicate on the "created" field. +func CreatedLTE(v time.Time) predicate.InviteCode { + return predicate.InviteCode(sql.FieldLTE(FieldCreated, v)) +} + +// And groups predicates with the AND operator between them. +func And(predicates ...predicate.InviteCode) predicate.InviteCode { + return predicate.InviteCode(sql.AndPredicates(predicates...)) +} + +// Or groups predicates with the OR operator between them. +func Or(predicates ...predicate.InviteCode) predicate.InviteCode { + return predicate.InviteCode(sql.OrPredicates(predicates...)) +} + +// Not applies the not operator on the given predicate. +func Not(p predicate.InviteCode) predicate.InviteCode { + return predicate.InviteCode(sql.NotPredicates(p)) +} diff --git a/pkg/ent/invitecode_create.go b/pkg/ent/invitecode_create.go new file mode 100644 index 000000000..5668318fb --- /dev/null +++ b/pkg/ent/invitecode_create.go @@ -0,0 +1,1014 @@ +// Code generated by ent, DO NOT EDIT. + +package ent + +import ( + "context" + "errors" + "fmt" + "time" + + "entgo.io/ent/dialect" + "entgo.io/ent/dialect/sql" + "entgo.io/ent/dialect/sql/sqlgraph" + "entgo.io/ent/schema/field" + "github.com/GoogleCloudPlatform/scion/pkg/ent/invitecode" + "github.com/google/uuid" +) + +// InviteCodeCreate is the builder for creating a InviteCode entity. +type InviteCodeCreate struct { + config + mutation *InviteCodeMutation + hooks []Hook + conflict []sql.ConflictOption +} + +// SetCodeHash sets the "code_hash" field. +func (_c *InviteCodeCreate) SetCodeHash(v string) *InviteCodeCreate { + _c.mutation.SetCodeHash(v) + return _c +} + +// SetCodePrefix sets the "code_prefix" field. +func (_c *InviteCodeCreate) SetCodePrefix(v string) *InviteCodeCreate { + _c.mutation.SetCodePrefix(v) + return _c +} + +// SetMaxUses sets the "max_uses" field. +func (_c *InviteCodeCreate) SetMaxUses(v int) *InviteCodeCreate { + _c.mutation.SetMaxUses(v) + return _c +} + +// SetNillableMaxUses sets the "max_uses" field if the given value is not nil. +func (_c *InviteCodeCreate) SetNillableMaxUses(v *int) *InviteCodeCreate { + if v != nil { + _c.SetMaxUses(*v) + } + return _c +} + +// SetUseCount sets the "use_count" field. +func (_c *InviteCodeCreate) SetUseCount(v int) *InviteCodeCreate { + _c.mutation.SetUseCount(v) + return _c +} + +// SetNillableUseCount sets the "use_count" field if the given value is not nil. +func (_c *InviteCodeCreate) SetNillableUseCount(v *int) *InviteCodeCreate { + if v != nil { + _c.SetUseCount(*v) + } + return _c +} + +// SetExpiresAt sets the "expires_at" field. +func (_c *InviteCodeCreate) SetExpiresAt(v time.Time) *InviteCodeCreate { + _c.mutation.SetExpiresAt(v) + return _c +} + +// SetRevoked sets the "revoked" field. +func (_c *InviteCodeCreate) SetRevoked(v bool) *InviteCodeCreate { + _c.mutation.SetRevoked(v) + return _c +} + +// SetNillableRevoked sets the "revoked" field if the given value is not nil. +func (_c *InviteCodeCreate) SetNillableRevoked(v *bool) *InviteCodeCreate { + if v != nil { + _c.SetRevoked(*v) + } + return _c +} + +// SetCreatedBy sets the "created_by" field. +func (_c *InviteCodeCreate) SetCreatedBy(v string) *InviteCodeCreate { + _c.mutation.SetCreatedBy(v) + return _c +} + +// SetNote sets the "note" field. +func (_c *InviteCodeCreate) SetNote(v string) *InviteCodeCreate { + _c.mutation.SetNote(v) + return _c +} + +// SetNillableNote sets the "note" field if the given value is not nil. +func (_c *InviteCodeCreate) SetNillableNote(v *string) *InviteCodeCreate { + if v != nil { + _c.SetNote(*v) + } + return _c +} + +// SetCreated sets the "created" field. +func (_c *InviteCodeCreate) SetCreated(v time.Time) *InviteCodeCreate { + _c.mutation.SetCreated(v) + return _c +} + +// SetNillableCreated sets the "created" field if the given value is not nil. +func (_c *InviteCodeCreate) SetNillableCreated(v *time.Time) *InviteCodeCreate { + if v != nil { + _c.SetCreated(*v) + } + return _c +} + +// SetID sets the "id" field. +func (_c *InviteCodeCreate) SetID(v uuid.UUID) *InviteCodeCreate { + _c.mutation.SetID(v) + return _c +} + +// SetNillableID sets the "id" field if the given value is not nil. +func (_c *InviteCodeCreate) SetNillableID(v *uuid.UUID) *InviteCodeCreate { + if v != nil { + _c.SetID(*v) + } + return _c +} + +// Mutation returns the InviteCodeMutation object of the builder. +func (_c *InviteCodeCreate) Mutation() *InviteCodeMutation { + return _c.mutation +} + +// Save creates the InviteCode in the database. +func (_c *InviteCodeCreate) Save(ctx context.Context) (*InviteCode, error) { + _c.defaults() + return withHooks(ctx, _c.sqlSave, _c.mutation, _c.hooks) +} + +// SaveX calls Save and panics if Save returns an error. +func (_c *InviteCodeCreate) SaveX(ctx context.Context) *InviteCode { + v, err := _c.Save(ctx) + if err != nil { + panic(err) + } + return v +} + +// Exec executes the query. +func (_c *InviteCodeCreate) Exec(ctx context.Context) error { + _, err := _c.Save(ctx) + return err +} + +// ExecX is like Exec, but panics if an error occurs. +func (_c *InviteCodeCreate) ExecX(ctx context.Context) { + if err := _c.Exec(ctx); err != nil { + panic(err) + } +} + +// defaults sets the default values of the builder before save. +func (_c *InviteCodeCreate) defaults() { + if _, ok := _c.mutation.MaxUses(); !ok { + v := invitecode.DefaultMaxUses + _c.mutation.SetMaxUses(v) + } + if _, ok := _c.mutation.UseCount(); !ok { + v := invitecode.DefaultUseCount + _c.mutation.SetUseCount(v) + } + if _, ok := _c.mutation.Revoked(); !ok { + v := invitecode.DefaultRevoked + _c.mutation.SetRevoked(v) + } + if _, ok := _c.mutation.Note(); !ok { + v := invitecode.DefaultNote + _c.mutation.SetNote(v) + } + if _, ok := _c.mutation.Created(); !ok { + v := invitecode.DefaultCreated() + _c.mutation.SetCreated(v) + } + if _, ok := _c.mutation.ID(); !ok { + v := invitecode.DefaultID() + _c.mutation.SetID(v) + } +} + +// check runs all checks and user-defined validators on the builder. +func (_c *InviteCodeCreate) check() error { + if _, ok := _c.mutation.CodeHash(); !ok { + return &ValidationError{Name: "code_hash", err: errors.New(`ent: missing required field "InviteCode.code_hash"`)} + } + if v, ok := _c.mutation.CodeHash(); ok { + if err := invitecode.CodeHashValidator(v); err != nil { + return &ValidationError{Name: "code_hash", err: fmt.Errorf(`ent: validator failed for field "InviteCode.code_hash": %w`, err)} + } + } + if _, ok := _c.mutation.CodePrefix(); !ok { + return &ValidationError{Name: "code_prefix", err: errors.New(`ent: missing required field "InviteCode.code_prefix"`)} + } + if v, ok := _c.mutation.CodePrefix(); ok { + if err := invitecode.CodePrefixValidator(v); err != nil { + return &ValidationError{Name: "code_prefix", err: fmt.Errorf(`ent: validator failed for field "InviteCode.code_prefix": %w`, err)} + } + } + if _, ok := _c.mutation.MaxUses(); !ok { + return &ValidationError{Name: "max_uses", err: errors.New(`ent: missing required field "InviteCode.max_uses"`)} + } + if _, ok := _c.mutation.UseCount(); !ok { + return &ValidationError{Name: "use_count", err: errors.New(`ent: missing required field "InviteCode.use_count"`)} + } + if _, ok := _c.mutation.ExpiresAt(); !ok { + return &ValidationError{Name: "expires_at", err: errors.New(`ent: missing required field "InviteCode.expires_at"`)} + } + if _, ok := _c.mutation.Revoked(); !ok { + return &ValidationError{Name: "revoked", err: errors.New(`ent: missing required field "InviteCode.revoked"`)} + } + if _, ok := _c.mutation.CreatedBy(); !ok { + return &ValidationError{Name: "created_by", err: errors.New(`ent: missing required field "InviteCode.created_by"`)} + } + if v, ok := _c.mutation.CreatedBy(); ok { + if err := invitecode.CreatedByValidator(v); err != nil { + return &ValidationError{Name: "created_by", err: fmt.Errorf(`ent: validator failed for field "InviteCode.created_by": %w`, err)} + } + } + if _, ok := _c.mutation.Note(); !ok { + return &ValidationError{Name: "note", err: errors.New(`ent: missing required field "InviteCode.note"`)} + } + if _, ok := _c.mutation.Created(); !ok { + return &ValidationError{Name: "created", err: errors.New(`ent: missing required field "InviteCode.created"`)} + } + return nil +} + +func (_c *InviteCodeCreate) sqlSave(ctx context.Context) (*InviteCode, error) { + if err := _c.check(); err != nil { + return nil, err + } + _node, _spec := _c.createSpec() + if err := sqlgraph.CreateNode(ctx, _c.driver, _spec); err != nil { + if sqlgraph.IsConstraintError(err) { + err = &ConstraintError{msg: err.Error(), wrap: err} + } + return nil, err + } + if _spec.ID.Value != nil { + if id, ok := _spec.ID.Value.(*uuid.UUID); ok { + _node.ID = *id + } else if err := _node.ID.Scan(_spec.ID.Value); err != nil { + return nil, err + } + } + _c.mutation.id = &_node.ID + _c.mutation.done = true + return _node, nil +} + +func (_c *InviteCodeCreate) createSpec() (*InviteCode, *sqlgraph.CreateSpec) { + var ( + _node = &InviteCode{config: _c.config} + _spec = sqlgraph.NewCreateSpec(invitecode.Table, sqlgraph.NewFieldSpec(invitecode.FieldID, field.TypeUUID)) + ) + _spec.OnConflict = _c.conflict + if id, ok := _c.mutation.ID(); ok { + _node.ID = id + _spec.ID.Value = &id + } + if value, ok := _c.mutation.CodeHash(); ok { + _spec.SetField(invitecode.FieldCodeHash, field.TypeString, value) + _node.CodeHash = value + } + if value, ok := _c.mutation.CodePrefix(); ok { + _spec.SetField(invitecode.FieldCodePrefix, field.TypeString, value) + _node.CodePrefix = value + } + if value, ok := _c.mutation.MaxUses(); ok { + _spec.SetField(invitecode.FieldMaxUses, field.TypeInt, value) + _node.MaxUses = value + } + if value, ok := _c.mutation.UseCount(); ok { + _spec.SetField(invitecode.FieldUseCount, field.TypeInt, value) + _node.UseCount = value + } + if value, ok := _c.mutation.ExpiresAt(); ok { + _spec.SetField(invitecode.FieldExpiresAt, field.TypeTime, value) + _node.ExpiresAt = value + } + if value, ok := _c.mutation.Revoked(); ok { + _spec.SetField(invitecode.FieldRevoked, field.TypeBool, value) + _node.Revoked = value + } + if value, ok := _c.mutation.CreatedBy(); ok { + _spec.SetField(invitecode.FieldCreatedBy, field.TypeString, value) + _node.CreatedBy = value + } + if value, ok := _c.mutation.Note(); ok { + _spec.SetField(invitecode.FieldNote, field.TypeString, value) + _node.Note = value + } + if value, ok := _c.mutation.Created(); ok { + _spec.SetField(invitecode.FieldCreated, field.TypeTime, value) + _node.Created = value + } + return _node, _spec +} + +// OnConflict allows configuring the `ON CONFLICT` / `ON DUPLICATE KEY` clause +// of the `INSERT` statement. For example: +// +// client.InviteCode.Create(). +// SetCodeHash(v). +// OnConflict( +// // Update the row with the new values +// // the was proposed for insertion. +// sql.ResolveWithNewValues(), +// ). +// // Override some of the fields with custom +// // update values. +// Update(func(u *ent.InviteCodeUpsert) { +// SetCodeHash(v+v). +// }). +// Exec(ctx) +func (_c *InviteCodeCreate) OnConflict(opts ...sql.ConflictOption) *InviteCodeUpsertOne { + _c.conflict = opts + return &InviteCodeUpsertOne{ + create: _c, + } +} + +// OnConflictColumns calls `OnConflict` and configures the columns +// as conflict target. Using this option is equivalent to using: +// +// client.InviteCode.Create(). +// OnConflict(sql.ConflictColumns(columns...)). +// Exec(ctx) +func (_c *InviteCodeCreate) OnConflictColumns(columns ...string) *InviteCodeUpsertOne { + _c.conflict = append(_c.conflict, sql.ConflictColumns(columns...)) + return &InviteCodeUpsertOne{ + create: _c, + } +} + +type ( + // InviteCodeUpsertOne is the builder for "upsert"-ing + // one InviteCode node. + InviteCodeUpsertOne struct { + create *InviteCodeCreate + } + + // InviteCodeUpsert is the "OnConflict" setter. + InviteCodeUpsert struct { + *sql.UpdateSet + } +) + +// SetCodeHash sets the "code_hash" field. +func (u *InviteCodeUpsert) SetCodeHash(v string) *InviteCodeUpsert { + u.Set(invitecode.FieldCodeHash, v) + return u +} + +// UpdateCodeHash sets the "code_hash" field to the value that was provided on create. +func (u *InviteCodeUpsert) UpdateCodeHash() *InviteCodeUpsert { + u.SetExcluded(invitecode.FieldCodeHash) + return u +} + +// SetCodePrefix sets the "code_prefix" field. +func (u *InviteCodeUpsert) SetCodePrefix(v string) *InviteCodeUpsert { + u.Set(invitecode.FieldCodePrefix, v) + return u +} + +// UpdateCodePrefix sets the "code_prefix" field to the value that was provided on create. +func (u *InviteCodeUpsert) UpdateCodePrefix() *InviteCodeUpsert { + u.SetExcluded(invitecode.FieldCodePrefix) + return u +} + +// SetMaxUses sets the "max_uses" field. +func (u *InviteCodeUpsert) SetMaxUses(v int) *InviteCodeUpsert { + u.Set(invitecode.FieldMaxUses, v) + return u +} + +// UpdateMaxUses sets the "max_uses" field to the value that was provided on create. +func (u *InviteCodeUpsert) UpdateMaxUses() *InviteCodeUpsert { + u.SetExcluded(invitecode.FieldMaxUses) + return u +} + +// AddMaxUses adds v to the "max_uses" field. +func (u *InviteCodeUpsert) AddMaxUses(v int) *InviteCodeUpsert { + u.Add(invitecode.FieldMaxUses, v) + return u +} + +// SetUseCount sets the "use_count" field. +func (u *InviteCodeUpsert) SetUseCount(v int) *InviteCodeUpsert { + u.Set(invitecode.FieldUseCount, v) + return u +} + +// UpdateUseCount sets the "use_count" field to the value that was provided on create. +func (u *InviteCodeUpsert) UpdateUseCount() *InviteCodeUpsert { + u.SetExcluded(invitecode.FieldUseCount) + return u +} + +// AddUseCount adds v to the "use_count" field. +func (u *InviteCodeUpsert) AddUseCount(v int) *InviteCodeUpsert { + u.Add(invitecode.FieldUseCount, v) + return u +} + +// SetExpiresAt sets the "expires_at" field. +func (u *InviteCodeUpsert) SetExpiresAt(v time.Time) *InviteCodeUpsert { + u.Set(invitecode.FieldExpiresAt, v) + return u +} + +// UpdateExpiresAt sets the "expires_at" field to the value that was provided on create. +func (u *InviteCodeUpsert) UpdateExpiresAt() *InviteCodeUpsert { + u.SetExcluded(invitecode.FieldExpiresAt) + return u +} + +// SetRevoked sets the "revoked" field. +func (u *InviteCodeUpsert) SetRevoked(v bool) *InviteCodeUpsert { + u.Set(invitecode.FieldRevoked, v) + return u +} + +// UpdateRevoked sets the "revoked" field to the value that was provided on create. +func (u *InviteCodeUpsert) UpdateRevoked() *InviteCodeUpsert { + u.SetExcluded(invitecode.FieldRevoked) + return u +} + +// SetCreatedBy sets the "created_by" field. +func (u *InviteCodeUpsert) SetCreatedBy(v string) *InviteCodeUpsert { + u.Set(invitecode.FieldCreatedBy, v) + return u +} + +// UpdateCreatedBy sets the "created_by" field to the value that was provided on create. +func (u *InviteCodeUpsert) UpdateCreatedBy() *InviteCodeUpsert { + u.SetExcluded(invitecode.FieldCreatedBy) + return u +} + +// SetNote sets the "note" field. +func (u *InviteCodeUpsert) SetNote(v string) *InviteCodeUpsert { + u.Set(invitecode.FieldNote, v) + return u +} + +// UpdateNote sets the "note" field to the value that was provided on create. +func (u *InviteCodeUpsert) UpdateNote() *InviteCodeUpsert { + u.SetExcluded(invitecode.FieldNote) + return u +} + +// UpdateNewValues updates the mutable fields using the new values that were set on create except the ID field. +// Using this option is equivalent to using: +// +// client.InviteCode.Create(). +// OnConflict( +// sql.ResolveWithNewValues(), +// sql.ResolveWith(func(u *sql.UpdateSet) { +// u.SetIgnore(invitecode.FieldID) +// }), +// ). +// Exec(ctx) +func (u *InviteCodeUpsertOne) UpdateNewValues() *InviteCodeUpsertOne { + u.create.conflict = append(u.create.conflict, sql.ResolveWithNewValues()) + u.create.conflict = append(u.create.conflict, sql.ResolveWith(func(s *sql.UpdateSet) { + if _, exists := u.create.mutation.ID(); exists { + s.SetIgnore(invitecode.FieldID) + } + if _, exists := u.create.mutation.Created(); exists { + s.SetIgnore(invitecode.FieldCreated) + } + })) + return u +} + +// Ignore sets each column to itself in case of conflict. +// Using this option is equivalent to using: +// +// client.InviteCode.Create(). +// OnConflict(sql.ResolveWithIgnore()). +// Exec(ctx) +func (u *InviteCodeUpsertOne) Ignore() *InviteCodeUpsertOne { + u.create.conflict = append(u.create.conflict, sql.ResolveWithIgnore()) + return u +} + +// DoNothing configures the conflict_action to `DO NOTHING`. +// Supported only by SQLite and PostgreSQL. +func (u *InviteCodeUpsertOne) DoNothing() *InviteCodeUpsertOne { + u.create.conflict = append(u.create.conflict, sql.DoNothing()) + return u +} + +// Update allows overriding fields `UPDATE` values. See the InviteCodeCreate.OnConflict +// documentation for more info. +func (u *InviteCodeUpsertOne) Update(set func(*InviteCodeUpsert)) *InviteCodeUpsertOne { + u.create.conflict = append(u.create.conflict, sql.ResolveWith(func(update *sql.UpdateSet) { + set(&InviteCodeUpsert{UpdateSet: update}) + })) + return u +} + +// SetCodeHash sets the "code_hash" field. +func (u *InviteCodeUpsertOne) SetCodeHash(v string) *InviteCodeUpsertOne { + return u.Update(func(s *InviteCodeUpsert) { + s.SetCodeHash(v) + }) +} + +// UpdateCodeHash sets the "code_hash" field to the value that was provided on create. +func (u *InviteCodeUpsertOne) UpdateCodeHash() *InviteCodeUpsertOne { + return u.Update(func(s *InviteCodeUpsert) { + s.UpdateCodeHash() + }) +} + +// SetCodePrefix sets the "code_prefix" field. +func (u *InviteCodeUpsertOne) SetCodePrefix(v string) *InviteCodeUpsertOne { + return u.Update(func(s *InviteCodeUpsert) { + s.SetCodePrefix(v) + }) +} + +// UpdateCodePrefix sets the "code_prefix" field to the value that was provided on create. +func (u *InviteCodeUpsertOne) UpdateCodePrefix() *InviteCodeUpsertOne { + return u.Update(func(s *InviteCodeUpsert) { + s.UpdateCodePrefix() + }) +} + +// SetMaxUses sets the "max_uses" field. +func (u *InviteCodeUpsertOne) SetMaxUses(v int) *InviteCodeUpsertOne { + return u.Update(func(s *InviteCodeUpsert) { + s.SetMaxUses(v) + }) +} + +// AddMaxUses adds v to the "max_uses" field. +func (u *InviteCodeUpsertOne) AddMaxUses(v int) *InviteCodeUpsertOne { + return u.Update(func(s *InviteCodeUpsert) { + s.AddMaxUses(v) + }) +} + +// UpdateMaxUses sets the "max_uses" field to the value that was provided on create. +func (u *InviteCodeUpsertOne) UpdateMaxUses() *InviteCodeUpsertOne { + return u.Update(func(s *InviteCodeUpsert) { + s.UpdateMaxUses() + }) +} + +// SetUseCount sets the "use_count" field. +func (u *InviteCodeUpsertOne) SetUseCount(v int) *InviteCodeUpsertOne { + return u.Update(func(s *InviteCodeUpsert) { + s.SetUseCount(v) + }) +} + +// AddUseCount adds v to the "use_count" field. +func (u *InviteCodeUpsertOne) AddUseCount(v int) *InviteCodeUpsertOne { + return u.Update(func(s *InviteCodeUpsert) { + s.AddUseCount(v) + }) +} + +// UpdateUseCount sets the "use_count" field to the value that was provided on create. +func (u *InviteCodeUpsertOne) UpdateUseCount() *InviteCodeUpsertOne { + return u.Update(func(s *InviteCodeUpsert) { + s.UpdateUseCount() + }) +} + +// SetExpiresAt sets the "expires_at" field. +func (u *InviteCodeUpsertOne) SetExpiresAt(v time.Time) *InviteCodeUpsertOne { + return u.Update(func(s *InviteCodeUpsert) { + s.SetExpiresAt(v) + }) +} + +// UpdateExpiresAt sets the "expires_at" field to the value that was provided on create. +func (u *InviteCodeUpsertOne) UpdateExpiresAt() *InviteCodeUpsertOne { + return u.Update(func(s *InviteCodeUpsert) { + s.UpdateExpiresAt() + }) +} + +// SetRevoked sets the "revoked" field. +func (u *InviteCodeUpsertOne) SetRevoked(v bool) *InviteCodeUpsertOne { + return u.Update(func(s *InviteCodeUpsert) { + s.SetRevoked(v) + }) +} + +// UpdateRevoked sets the "revoked" field to the value that was provided on create. +func (u *InviteCodeUpsertOne) UpdateRevoked() *InviteCodeUpsertOne { + return u.Update(func(s *InviteCodeUpsert) { + s.UpdateRevoked() + }) +} + +// SetCreatedBy sets the "created_by" field. +func (u *InviteCodeUpsertOne) SetCreatedBy(v string) *InviteCodeUpsertOne { + return u.Update(func(s *InviteCodeUpsert) { + s.SetCreatedBy(v) + }) +} + +// UpdateCreatedBy sets the "created_by" field to the value that was provided on create. +func (u *InviteCodeUpsertOne) UpdateCreatedBy() *InviteCodeUpsertOne { + return u.Update(func(s *InviteCodeUpsert) { + s.UpdateCreatedBy() + }) +} + +// SetNote sets the "note" field. +func (u *InviteCodeUpsertOne) SetNote(v string) *InviteCodeUpsertOne { + return u.Update(func(s *InviteCodeUpsert) { + s.SetNote(v) + }) +} + +// UpdateNote sets the "note" field to the value that was provided on create. +func (u *InviteCodeUpsertOne) UpdateNote() *InviteCodeUpsertOne { + return u.Update(func(s *InviteCodeUpsert) { + s.UpdateNote() + }) +} + +// Exec executes the query. +func (u *InviteCodeUpsertOne) Exec(ctx context.Context) error { + if len(u.create.conflict) == 0 { + return errors.New("ent: missing options for InviteCodeCreate.OnConflict") + } + return u.create.Exec(ctx) +} + +// ExecX is like Exec, but panics if an error occurs. +func (u *InviteCodeUpsertOne) ExecX(ctx context.Context) { + if err := u.create.Exec(ctx); err != nil { + panic(err) + } +} + +// Exec executes the UPSERT query and returns the inserted/updated ID. +func (u *InviteCodeUpsertOne) ID(ctx context.Context) (id uuid.UUID, err error) { + if u.create.driver.Dialect() == dialect.MySQL { + // In case of "ON CONFLICT", there is no way to get back non-numeric ID + // fields from the database since MySQL does not support the RETURNING clause. + return id, errors.New("ent: InviteCodeUpsertOne.ID is not supported by MySQL driver. Use InviteCodeUpsertOne.Exec instead") + } + node, err := u.create.Save(ctx) + if err != nil { + return id, err + } + return node.ID, nil +} + +// IDX is like ID, but panics if an error occurs. +func (u *InviteCodeUpsertOne) IDX(ctx context.Context) uuid.UUID { + id, err := u.ID(ctx) + if err != nil { + panic(err) + } + return id +} + +// InviteCodeCreateBulk is the builder for creating many InviteCode entities in bulk. +type InviteCodeCreateBulk struct { + config + err error + builders []*InviteCodeCreate + conflict []sql.ConflictOption +} + +// Save creates the InviteCode entities in the database. +func (_c *InviteCodeCreateBulk) Save(ctx context.Context) ([]*InviteCode, error) { + if _c.err != nil { + return nil, _c.err + } + specs := make([]*sqlgraph.CreateSpec, len(_c.builders)) + nodes := make([]*InviteCode, len(_c.builders)) + mutators := make([]Mutator, len(_c.builders)) + for i := range _c.builders { + func(i int, root context.Context) { + builder := _c.builders[i] + builder.defaults() + var mut Mutator = MutateFunc(func(ctx context.Context, m Mutation) (Value, error) { + mutation, ok := m.(*InviteCodeMutation) + if !ok { + return nil, fmt.Errorf("unexpected mutation type %T", m) + } + if err := builder.check(); err != nil { + return nil, err + } + builder.mutation = mutation + var err error + nodes[i], specs[i] = builder.createSpec() + if i < len(mutators)-1 { + _, err = mutators[i+1].Mutate(root, _c.builders[i+1].mutation) + } else { + spec := &sqlgraph.BatchCreateSpec{Nodes: specs} + spec.OnConflict = _c.conflict + // Invoke the actual operation on the latest mutation in the chain. + if err = sqlgraph.BatchCreate(ctx, _c.driver, spec); err != nil { + if sqlgraph.IsConstraintError(err) { + err = &ConstraintError{msg: err.Error(), wrap: err} + } + } + } + if err != nil { + return nil, err + } + mutation.id = &nodes[i].ID + mutation.done = true + return nodes[i], nil + }) + for i := len(builder.hooks) - 1; i >= 0; i-- { + mut = builder.hooks[i](mut) + } + mutators[i] = mut + }(i, ctx) + } + if len(mutators) > 0 { + if _, err := mutators[0].Mutate(ctx, _c.builders[0].mutation); err != nil { + return nil, err + } + } + return nodes, nil +} + +// SaveX is like Save, but panics if an error occurs. +func (_c *InviteCodeCreateBulk) SaveX(ctx context.Context) []*InviteCode { + v, err := _c.Save(ctx) + if err != nil { + panic(err) + } + return v +} + +// Exec executes the query. +func (_c *InviteCodeCreateBulk) Exec(ctx context.Context) error { + _, err := _c.Save(ctx) + return err +} + +// ExecX is like Exec, but panics if an error occurs. +func (_c *InviteCodeCreateBulk) ExecX(ctx context.Context) { + if err := _c.Exec(ctx); err != nil { + panic(err) + } +} + +// OnConflict allows configuring the `ON CONFLICT` / `ON DUPLICATE KEY` clause +// of the `INSERT` statement. For example: +// +// client.InviteCode.CreateBulk(builders...). +// OnConflict( +// // Update the row with the new values +// // the was proposed for insertion. +// sql.ResolveWithNewValues(), +// ). +// // Override some of the fields with custom +// // update values. +// Update(func(u *ent.InviteCodeUpsert) { +// SetCodeHash(v+v). +// }). +// Exec(ctx) +func (_c *InviteCodeCreateBulk) OnConflict(opts ...sql.ConflictOption) *InviteCodeUpsertBulk { + _c.conflict = opts + return &InviteCodeUpsertBulk{ + create: _c, + } +} + +// OnConflictColumns calls `OnConflict` and configures the columns +// as conflict target. Using this option is equivalent to using: +// +// client.InviteCode.Create(). +// OnConflict(sql.ConflictColumns(columns...)). +// Exec(ctx) +func (_c *InviteCodeCreateBulk) OnConflictColumns(columns ...string) *InviteCodeUpsertBulk { + _c.conflict = append(_c.conflict, sql.ConflictColumns(columns...)) + return &InviteCodeUpsertBulk{ + create: _c, + } +} + +// InviteCodeUpsertBulk is the builder for "upsert"-ing +// a bulk of InviteCode nodes. +type InviteCodeUpsertBulk struct { + create *InviteCodeCreateBulk +} + +// UpdateNewValues updates the mutable fields using the new values that +// were set on create. Using this option is equivalent to using: +// +// client.InviteCode.Create(). +// OnConflict( +// sql.ResolveWithNewValues(), +// sql.ResolveWith(func(u *sql.UpdateSet) { +// u.SetIgnore(invitecode.FieldID) +// }), +// ). +// Exec(ctx) +func (u *InviteCodeUpsertBulk) UpdateNewValues() *InviteCodeUpsertBulk { + u.create.conflict = append(u.create.conflict, sql.ResolveWithNewValues()) + u.create.conflict = append(u.create.conflict, sql.ResolveWith(func(s *sql.UpdateSet) { + for _, b := range u.create.builders { + if _, exists := b.mutation.ID(); exists { + s.SetIgnore(invitecode.FieldID) + } + if _, exists := b.mutation.Created(); exists { + s.SetIgnore(invitecode.FieldCreated) + } + } + })) + return u +} + +// Ignore sets each column to itself in case of conflict. +// Using this option is equivalent to using: +// +// client.InviteCode.Create(). +// OnConflict(sql.ResolveWithIgnore()). +// Exec(ctx) +func (u *InviteCodeUpsertBulk) Ignore() *InviteCodeUpsertBulk { + u.create.conflict = append(u.create.conflict, sql.ResolveWithIgnore()) + return u +} + +// DoNothing configures the conflict_action to `DO NOTHING`. +// Supported only by SQLite and PostgreSQL. +func (u *InviteCodeUpsertBulk) DoNothing() *InviteCodeUpsertBulk { + u.create.conflict = append(u.create.conflict, sql.DoNothing()) + return u +} + +// Update allows overriding fields `UPDATE` values. See the InviteCodeCreateBulk.OnConflict +// documentation for more info. +func (u *InviteCodeUpsertBulk) Update(set func(*InviteCodeUpsert)) *InviteCodeUpsertBulk { + u.create.conflict = append(u.create.conflict, sql.ResolveWith(func(update *sql.UpdateSet) { + set(&InviteCodeUpsert{UpdateSet: update}) + })) + return u +} + +// SetCodeHash sets the "code_hash" field. +func (u *InviteCodeUpsertBulk) SetCodeHash(v string) *InviteCodeUpsertBulk { + return u.Update(func(s *InviteCodeUpsert) { + s.SetCodeHash(v) + }) +} + +// UpdateCodeHash sets the "code_hash" field to the value that was provided on create. +func (u *InviteCodeUpsertBulk) UpdateCodeHash() *InviteCodeUpsertBulk { + return u.Update(func(s *InviteCodeUpsert) { + s.UpdateCodeHash() + }) +} + +// SetCodePrefix sets the "code_prefix" field. +func (u *InviteCodeUpsertBulk) SetCodePrefix(v string) *InviteCodeUpsertBulk { + return u.Update(func(s *InviteCodeUpsert) { + s.SetCodePrefix(v) + }) +} + +// UpdateCodePrefix sets the "code_prefix" field to the value that was provided on create. +func (u *InviteCodeUpsertBulk) UpdateCodePrefix() *InviteCodeUpsertBulk { + return u.Update(func(s *InviteCodeUpsert) { + s.UpdateCodePrefix() + }) +} + +// SetMaxUses sets the "max_uses" field. +func (u *InviteCodeUpsertBulk) SetMaxUses(v int) *InviteCodeUpsertBulk { + return u.Update(func(s *InviteCodeUpsert) { + s.SetMaxUses(v) + }) +} + +// AddMaxUses adds v to the "max_uses" field. +func (u *InviteCodeUpsertBulk) AddMaxUses(v int) *InviteCodeUpsertBulk { + return u.Update(func(s *InviteCodeUpsert) { + s.AddMaxUses(v) + }) +} + +// UpdateMaxUses sets the "max_uses" field to the value that was provided on create. +func (u *InviteCodeUpsertBulk) UpdateMaxUses() *InviteCodeUpsertBulk { + return u.Update(func(s *InviteCodeUpsert) { + s.UpdateMaxUses() + }) +} + +// SetUseCount sets the "use_count" field. +func (u *InviteCodeUpsertBulk) SetUseCount(v int) *InviteCodeUpsertBulk { + return u.Update(func(s *InviteCodeUpsert) { + s.SetUseCount(v) + }) +} + +// AddUseCount adds v to the "use_count" field. +func (u *InviteCodeUpsertBulk) AddUseCount(v int) *InviteCodeUpsertBulk { + return u.Update(func(s *InviteCodeUpsert) { + s.AddUseCount(v) + }) +} + +// UpdateUseCount sets the "use_count" field to the value that was provided on create. +func (u *InviteCodeUpsertBulk) UpdateUseCount() *InviteCodeUpsertBulk { + return u.Update(func(s *InviteCodeUpsert) { + s.UpdateUseCount() + }) +} + +// SetExpiresAt sets the "expires_at" field. +func (u *InviteCodeUpsertBulk) SetExpiresAt(v time.Time) *InviteCodeUpsertBulk { + return u.Update(func(s *InviteCodeUpsert) { + s.SetExpiresAt(v) + }) +} + +// UpdateExpiresAt sets the "expires_at" field to the value that was provided on create. +func (u *InviteCodeUpsertBulk) UpdateExpiresAt() *InviteCodeUpsertBulk { + return u.Update(func(s *InviteCodeUpsert) { + s.UpdateExpiresAt() + }) +} + +// SetRevoked sets the "revoked" field. +func (u *InviteCodeUpsertBulk) SetRevoked(v bool) *InviteCodeUpsertBulk { + return u.Update(func(s *InviteCodeUpsert) { + s.SetRevoked(v) + }) +} + +// UpdateRevoked sets the "revoked" field to the value that was provided on create. +func (u *InviteCodeUpsertBulk) UpdateRevoked() *InviteCodeUpsertBulk { + return u.Update(func(s *InviteCodeUpsert) { + s.UpdateRevoked() + }) +} + +// SetCreatedBy sets the "created_by" field. +func (u *InviteCodeUpsertBulk) SetCreatedBy(v string) *InviteCodeUpsertBulk { + return u.Update(func(s *InviteCodeUpsert) { + s.SetCreatedBy(v) + }) +} + +// UpdateCreatedBy sets the "created_by" field to the value that was provided on create. +func (u *InviteCodeUpsertBulk) UpdateCreatedBy() *InviteCodeUpsertBulk { + return u.Update(func(s *InviteCodeUpsert) { + s.UpdateCreatedBy() + }) +} + +// SetNote sets the "note" field. +func (u *InviteCodeUpsertBulk) SetNote(v string) *InviteCodeUpsertBulk { + return u.Update(func(s *InviteCodeUpsert) { + s.SetNote(v) + }) +} + +// UpdateNote sets the "note" field to the value that was provided on create. +func (u *InviteCodeUpsertBulk) UpdateNote() *InviteCodeUpsertBulk { + return u.Update(func(s *InviteCodeUpsert) { + s.UpdateNote() + }) +} + +// Exec executes the query. +func (u *InviteCodeUpsertBulk) Exec(ctx context.Context) error { + if u.create.err != nil { + return u.create.err + } + for i, b := range u.create.builders { + if len(b.conflict) != 0 { + return fmt.Errorf("ent: OnConflict was set for builder %d. Set it on the InviteCodeCreateBulk instead", i) + } + } + if len(u.create.conflict) == 0 { + return errors.New("ent: missing options for InviteCodeCreateBulk.OnConflict") + } + return u.create.Exec(ctx) +} + +// ExecX is like Exec, but panics if an error occurs. +func (u *InviteCodeUpsertBulk) ExecX(ctx context.Context) { + if err := u.create.Exec(ctx); err != nil { + panic(err) + } +} diff --git a/pkg/ent/invitecode_delete.go b/pkg/ent/invitecode_delete.go new file mode 100644 index 000000000..c3fffaff9 --- /dev/null +++ b/pkg/ent/invitecode_delete.go @@ -0,0 +1,88 @@ +// Code generated by ent, DO NOT EDIT. + +package ent + +import ( + "context" + + "entgo.io/ent/dialect/sql" + "entgo.io/ent/dialect/sql/sqlgraph" + "entgo.io/ent/schema/field" + "github.com/GoogleCloudPlatform/scion/pkg/ent/invitecode" + "github.com/GoogleCloudPlatform/scion/pkg/ent/predicate" +) + +// InviteCodeDelete is the builder for deleting a InviteCode entity. +type InviteCodeDelete struct { + config + hooks []Hook + mutation *InviteCodeMutation +} + +// Where appends a list predicates to the InviteCodeDelete builder. +func (_d *InviteCodeDelete) Where(ps ...predicate.InviteCode) *InviteCodeDelete { + _d.mutation.Where(ps...) + return _d +} + +// Exec executes the deletion query and returns how many vertices were deleted. +func (_d *InviteCodeDelete) Exec(ctx context.Context) (int, error) { + return withHooks(ctx, _d.sqlExec, _d.mutation, _d.hooks) +} + +// ExecX is like Exec, but panics if an error occurs. +func (_d *InviteCodeDelete) ExecX(ctx context.Context) int { + n, err := _d.Exec(ctx) + if err != nil { + panic(err) + } + return n +} + +func (_d *InviteCodeDelete) sqlExec(ctx context.Context) (int, error) { + _spec := sqlgraph.NewDeleteSpec(invitecode.Table, sqlgraph.NewFieldSpec(invitecode.FieldID, field.TypeUUID)) + if ps := _d.mutation.predicates; len(ps) > 0 { + _spec.Predicate = func(selector *sql.Selector) { + for i := range ps { + ps[i](selector) + } + } + } + affected, err := sqlgraph.DeleteNodes(ctx, _d.driver, _spec) + if err != nil && sqlgraph.IsConstraintError(err) { + err = &ConstraintError{msg: err.Error(), wrap: err} + } + _d.mutation.done = true + return affected, err +} + +// InviteCodeDeleteOne is the builder for deleting a single InviteCode entity. +type InviteCodeDeleteOne struct { + _d *InviteCodeDelete +} + +// Where appends a list predicates to the InviteCodeDelete builder. +func (_d *InviteCodeDeleteOne) Where(ps ...predicate.InviteCode) *InviteCodeDeleteOne { + _d._d.mutation.Where(ps...) + return _d +} + +// Exec executes the deletion query. +func (_d *InviteCodeDeleteOne) Exec(ctx context.Context) error { + n, err := _d._d.Exec(ctx) + switch { + case err != nil: + return err + case n == 0: + return &NotFoundError{invitecode.Label} + default: + return nil + } +} + +// ExecX is like Exec, but panics if an error occurs. +func (_d *InviteCodeDeleteOne) ExecX(ctx context.Context) { + if err := _d.Exec(ctx); err != nil { + panic(err) + } +} diff --git a/pkg/ent/invitecode_query.go b/pkg/ent/invitecode_query.go new file mode 100644 index 000000000..231959563 --- /dev/null +++ b/pkg/ent/invitecode_query.go @@ -0,0 +1,565 @@ +// Code generated by ent, DO NOT EDIT. + +package ent + +import ( + "context" + "fmt" + "math" + + "entgo.io/ent" + "entgo.io/ent/dialect" + "entgo.io/ent/dialect/sql" + "entgo.io/ent/dialect/sql/sqlgraph" + "entgo.io/ent/schema/field" + "github.com/GoogleCloudPlatform/scion/pkg/ent/invitecode" + "github.com/GoogleCloudPlatform/scion/pkg/ent/predicate" + "github.com/google/uuid" +) + +// InviteCodeQuery is the builder for querying InviteCode entities. +type InviteCodeQuery struct { + config + ctx *QueryContext + order []invitecode.OrderOption + inters []Interceptor + predicates []predicate.InviteCode + modifiers []func(*sql.Selector) + // intermediate query (i.e. traversal path). + sql *sql.Selector + path func(context.Context) (*sql.Selector, error) +} + +// Where adds a new predicate for the InviteCodeQuery builder. +func (_q *InviteCodeQuery) Where(ps ...predicate.InviteCode) *InviteCodeQuery { + _q.predicates = append(_q.predicates, ps...) + return _q +} + +// Limit the number of records to be returned by this query. +func (_q *InviteCodeQuery) Limit(limit int) *InviteCodeQuery { + _q.ctx.Limit = &limit + return _q +} + +// Offset to start from. +func (_q *InviteCodeQuery) Offset(offset int) *InviteCodeQuery { + _q.ctx.Offset = &offset + return _q +} + +// Unique configures the query builder to filter duplicate records on query. +// By default, unique is set to true, and can be disabled using this method. +func (_q *InviteCodeQuery) Unique(unique bool) *InviteCodeQuery { + _q.ctx.Unique = &unique + return _q +} + +// Order specifies how the records should be ordered. +func (_q *InviteCodeQuery) Order(o ...invitecode.OrderOption) *InviteCodeQuery { + _q.order = append(_q.order, o...) + return _q +} + +// First returns the first InviteCode entity from the query. +// Returns a *NotFoundError when no InviteCode was found. +func (_q *InviteCodeQuery) First(ctx context.Context) (*InviteCode, error) { + nodes, err := _q.Limit(1).All(setContextOp(ctx, _q.ctx, ent.OpQueryFirst)) + if err != nil { + return nil, err + } + if len(nodes) == 0 { + return nil, &NotFoundError{invitecode.Label} + } + return nodes[0], nil +} + +// FirstX is like First, but panics if an error occurs. +func (_q *InviteCodeQuery) FirstX(ctx context.Context) *InviteCode { + node, err := _q.First(ctx) + if err != nil && !IsNotFound(err) { + panic(err) + } + return node +} + +// FirstID returns the first InviteCode ID from the query. +// Returns a *NotFoundError when no InviteCode ID was found. +func (_q *InviteCodeQuery) FirstID(ctx context.Context) (id uuid.UUID, err error) { + var ids []uuid.UUID + if ids, err = _q.Limit(1).IDs(setContextOp(ctx, _q.ctx, ent.OpQueryFirstID)); err != nil { + return + } + if len(ids) == 0 { + err = &NotFoundError{invitecode.Label} + return + } + return ids[0], nil +} + +// FirstIDX is like FirstID, but panics if an error occurs. +func (_q *InviteCodeQuery) FirstIDX(ctx context.Context) uuid.UUID { + id, err := _q.FirstID(ctx) + if err != nil && !IsNotFound(err) { + panic(err) + } + return id +} + +// Only returns a single InviteCode entity found by the query, ensuring it only returns one. +// Returns a *NotSingularError when more than one InviteCode entity is found. +// Returns a *NotFoundError when no InviteCode entities are found. +func (_q *InviteCodeQuery) Only(ctx context.Context) (*InviteCode, error) { + nodes, err := _q.Limit(2).All(setContextOp(ctx, _q.ctx, ent.OpQueryOnly)) + if err != nil { + return nil, err + } + switch len(nodes) { + case 1: + return nodes[0], nil + case 0: + return nil, &NotFoundError{invitecode.Label} + default: + return nil, &NotSingularError{invitecode.Label} + } +} + +// OnlyX is like Only, but panics if an error occurs. +func (_q *InviteCodeQuery) OnlyX(ctx context.Context) *InviteCode { + node, err := _q.Only(ctx) + if err != nil { + panic(err) + } + return node +} + +// OnlyID is like Only, but returns the only InviteCode ID in the query. +// Returns a *NotSingularError when more than one InviteCode ID is found. +// Returns a *NotFoundError when no entities are found. +func (_q *InviteCodeQuery) OnlyID(ctx context.Context) (id uuid.UUID, err error) { + var ids []uuid.UUID + if ids, err = _q.Limit(2).IDs(setContextOp(ctx, _q.ctx, ent.OpQueryOnlyID)); err != nil { + return + } + switch len(ids) { + case 1: + id = ids[0] + case 0: + err = &NotFoundError{invitecode.Label} + default: + err = &NotSingularError{invitecode.Label} + } + return +} + +// OnlyIDX is like OnlyID, but panics if an error occurs. +func (_q *InviteCodeQuery) OnlyIDX(ctx context.Context) uuid.UUID { + id, err := _q.OnlyID(ctx) + if err != nil { + panic(err) + } + return id +} + +// All executes the query and returns a list of InviteCodes. +func (_q *InviteCodeQuery) All(ctx context.Context) ([]*InviteCode, error) { + ctx = setContextOp(ctx, _q.ctx, ent.OpQueryAll) + if err := _q.prepareQuery(ctx); err != nil { + return nil, err + } + qr := querierAll[[]*InviteCode, *InviteCodeQuery]() + return withInterceptors[[]*InviteCode](ctx, _q, qr, _q.inters) +} + +// AllX is like All, but panics if an error occurs. +func (_q *InviteCodeQuery) AllX(ctx context.Context) []*InviteCode { + nodes, err := _q.All(ctx) + if err != nil { + panic(err) + } + return nodes +} + +// IDs executes the query and returns a list of InviteCode IDs. +func (_q *InviteCodeQuery) IDs(ctx context.Context) (ids []uuid.UUID, err error) { + if _q.ctx.Unique == nil && _q.path != nil { + _q.Unique(true) + } + ctx = setContextOp(ctx, _q.ctx, ent.OpQueryIDs) + if err = _q.Select(invitecode.FieldID).Scan(ctx, &ids); err != nil { + return nil, err + } + return ids, nil +} + +// IDsX is like IDs, but panics if an error occurs. +func (_q *InviteCodeQuery) IDsX(ctx context.Context) []uuid.UUID { + ids, err := _q.IDs(ctx) + if err != nil { + panic(err) + } + return ids +} + +// Count returns the count of the given query. +func (_q *InviteCodeQuery) Count(ctx context.Context) (int, error) { + ctx = setContextOp(ctx, _q.ctx, ent.OpQueryCount) + if err := _q.prepareQuery(ctx); err != nil { + return 0, err + } + return withInterceptors[int](ctx, _q, querierCount[*InviteCodeQuery](), _q.inters) +} + +// CountX is like Count, but panics if an error occurs. +func (_q *InviteCodeQuery) CountX(ctx context.Context) int { + count, err := _q.Count(ctx) + if err != nil { + panic(err) + } + return count +} + +// Exist returns true if the query has elements in the graph. +func (_q *InviteCodeQuery) Exist(ctx context.Context) (bool, error) { + ctx = setContextOp(ctx, _q.ctx, ent.OpQueryExist) + switch _, err := _q.FirstID(ctx); { + case IsNotFound(err): + return false, nil + case err != nil: + return false, fmt.Errorf("ent: check existence: %w", err) + default: + return true, nil + } +} + +// ExistX is like Exist, but panics if an error occurs. +func (_q *InviteCodeQuery) ExistX(ctx context.Context) bool { + exist, err := _q.Exist(ctx) + if err != nil { + panic(err) + } + return exist +} + +// Clone returns a duplicate of the InviteCodeQuery builder, including all associated steps. It can be +// used to prepare common query builders and use them differently after the clone is made. +func (_q *InviteCodeQuery) Clone() *InviteCodeQuery { + if _q == nil { + return nil + } + return &InviteCodeQuery{ + config: _q.config, + ctx: _q.ctx.Clone(), + order: append([]invitecode.OrderOption{}, _q.order...), + inters: append([]Interceptor{}, _q.inters...), + predicates: append([]predicate.InviteCode{}, _q.predicates...), + // clone intermediate query. + sql: _q.sql.Clone(), + path: _q.path, + } +} + +// GroupBy is used to group vertices by one or more fields/columns. +// It is often used with aggregate functions, like: count, max, mean, min, sum. +// +// Example: +// +// var v []struct { +// CodeHash string `json:"code_hash,omitempty"` +// Count int `json:"count,omitempty"` +// } +// +// client.InviteCode.Query(). +// GroupBy(invitecode.FieldCodeHash). +// Aggregate(ent.Count()). +// Scan(ctx, &v) +func (_q *InviteCodeQuery) GroupBy(field string, fields ...string) *InviteCodeGroupBy { + _q.ctx.Fields = append([]string{field}, fields...) + grbuild := &InviteCodeGroupBy{build: _q} + grbuild.flds = &_q.ctx.Fields + grbuild.label = invitecode.Label + grbuild.scan = grbuild.Scan + return grbuild +} + +// Select allows the selection one or more fields/columns for the given query, +// instead of selecting all fields in the entity. +// +// Example: +// +// var v []struct { +// CodeHash string `json:"code_hash,omitempty"` +// } +// +// client.InviteCode.Query(). +// Select(invitecode.FieldCodeHash). +// Scan(ctx, &v) +func (_q *InviteCodeQuery) Select(fields ...string) *InviteCodeSelect { + _q.ctx.Fields = append(_q.ctx.Fields, fields...) + sbuild := &InviteCodeSelect{InviteCodeQuery: _q} + sbuild.label = invitecode.Label + sbuild.flds, sbuild.scan = &_q.ctx.Fields, sbuild.Scan + return sbuild +} + +// Aggregate returns a InviteCodeSelect configured with the given aggregations. +func (_q *InviteCodeQuery) Aggregate(fns ...AggregateFunc) *InviteCodeSelect { + return _q.Select().Aggregate(fns...) +} + +func (_q *InviteCodeQuery) prepareQuery(ctx context.Context) error { + for _, inter := range _q.inters { + if inter == nil { + return fmt.Errorf("ent: uninitialized interceptor (forgotten import ent/runtime?)") + } + if trv, ok := inter.(Traverser); ok { + if err := trv.Traverse(ctx, _q); err != nil { + return err + } + } + } + for _, f := range _q.ctx.Fields { + if !invitecode.ValidColumn(f) { + return &ValidationError{Name: f, err: fmt.Errorf("ent: invalid field %q for query", f)} + } + } + if _q.path != nil { + prev, err := _q.path(ctx) + if err != nil { + return err + } + _q.sql = prev + } + return nil +} + +func (_q *InviteCodeQuery) sqlAll(ctx context.Context, hooks ...queryHook) ([]*InviteCode, error) { + var ( + nodes = []*InviteCode{} + _spec = _q.querySpec() + ) + _spec.ScanValues = func(columns []string) ([]any, error) { + return (*InviteCode).scanValues(nil, columns) + } + _spec.Assign = func(columns []string, values []any) error { + node := &InviteCode{config: _q.config} + nodes = append(nodes, node) + return node.assignValues(columns, values) + } + if len(_q.modifiers) > 0 { + _spec.Modifiers = _q.modifiers + } + for i := range hooks { + hooks[i](ctx, _spec) + } + if err := sqlgraph.QueryNodes(ctx, _q.driver, _spec); err != nil { + return nil, err + } + if len(nodes) == 0 { + return nodes, nil + } + return nodes, nil +} + +func (_q *InviteCodeQuery) sqlCount(ctx context.Context) (int, error) { + _spec := _q.querySpec() + if len(_q.modifiers) > 0 { + _spec.Modifiers = _q.modifiers + } + _spec.Node.Columns = _q.ctx.Fields + if len(_q.ctx.Fields) > 0 { + _spec.Unique = _q.ctx.Unique != nil && *_q.ctx.Unique + } + return sqlgraph.CountNodes(ctx, _q.driver, _spec) +} + +func (_q *InviteCodeQuery) querySpec() *sqlgraph.QuerySpec { + _spec := sqlgraph.NewQuerySpec(invitecode.Table, invitecode.Columns, sqlgraph.NewFieldSpec(invitecode.FieldID, field.TypeUUID)) + _spec.From = _q.sql + if unique := _q.ctx.Unique; unique != nil { + _spec.Unique = *unique + } else if _q.path != nil { + _spec.Unique = true + } + if fields := _q.ctx.Fields; len(fields) > 0 { + _spec.Node.Columns = make([]string, 0, len(fields)) + _spec.Node.Columns = append(_spec.Node.Columns, invitecode.FieldID) + for i := range fields { + if fields[i] != invitecode.FieldID { + _spec.Node.Columns = append(_spec.Node.Columns, fields[i]) + } + } + } + if ps := _q.predicates; len(ps) > 0 { + _spec.Predicate = func(selector *sql.Selector) { + for i := range ps { + ps[i](selector) + } + } + } + if limit := _q.ctx.Limit; limit != nil { + _spec.Limit = *limit + } + if offset := _q.ctx.Offset; offset != nil { + _spec.Offset = *offset + } + if ps := _q.order; len(ps) > 0 { + _spec.Order = func(selector *sql.Selector) { + for i := range ps { + ps[i](selector) + } + } + } + return _spec +} + +func (_q *InviteCodeQuery) sqlQuery(ctx context.Context) *sql.Selector { + builder := sql.Dialect(_q.driver.Dialect()) + t1 := builder.Table(invitecode.Table) + columns := _q.ctx.Fields + if len(columns) == 0 { + columns = invitecode.Columns + } + selector := builder.Select(t1.Columns(columns...)...).From(t1) + if _q.sql != nil { + selector = _q.sql + selector.Select(selector.Columns(columns...)...) + } + if _q.ctx.Unique != nil && *_q.ctx.Unique { + selector.Distinct() + } + for _, m := range _q.modifiers { + m(selector) + } + for _, p := range _q.predicates { + p(selector) + } + for _, p := range _q.order { + p(selector) + } + if offset := _q.ctx.Offset; offset != nil { + // limit is mandatory for offset clause. We start + // with default value, and override it below if needed. + selector.Offset(*offset).Limit(math.MaxInt32) + } + if limit := _q.ctx.Limit; limit != nil { + selector.Limit(*limit) + } + return selector +} + +// ForUpdate locks the selected rows against concurrent updates, and prevent them from being +// updated, deleted or "selected ... for update" by other sessions, until the transaction is +// either committed or rolled-back. +func (_q *InviteCodeQuery) ForUpdate(opts ...sql.LockOption) *InviteCodeQuery { + if _q.driver.Dialect() == dialect.Postgres { + _q.Unique(false) + } + _q.modifiers = append(_q.modifiers, func(s *sql.Selector) { + s.ForUpdate(opts...) + }) + return _q +} + +// ForShare behaves similarly to ForUpdate, except that it acquires a shared mode lock +// on any rows that are read. Other sessions can read the rows, but cannot modify them +// until your transaction commits. +func (_q *InviteCodeQuery) ForShare(opts ...sql.LockOption) *InviteCodeQuery { + if _q.driver.Dialect() == dialect.Postgres { + _q.Unique(false) + } + _q.modifiers = append(_q.modifiers, func(s *sql.Selector) { + s.ForShare(opts...) + }) + return _q +} + +// InviteCodeGroupBy is the group-by builder for InviteCode entities. +type InviteCodeGroupBy struct { + selector + build *InviteCodeQuery +} + +// Aggregate adds the given aggregation functions to the group-by query. +func (_g *InviteCodeGroupBy) Aggregate(fns ...AggregateFunc) *InviteCodeGroupBy { + _g.fns = append(_g.fns, fns...) + return _g +} + +// Scan applies the selector query and scans the result into the given value. +func (_g *InviteCodeGroupBy) Scan(ctx context.Context, v any) error { + ctx = setContextOp(ctx, _g.build.ctx, ent.OpQueryGroupBy) + if err := _g.build.prepareQuery(ctx); err != nil { + return err + } + return scanWithInterceptors[*InviteCodeQuery, *InviteCodeGroupBy](ctx, _g.build, _g, _g.build.inters, v) +} + +func (_g *InviteCodeGroupBy) sqlScan(ctx context.Context, root *InviteCodeQuery, v any) error { + selector := root.sqlQuery(ctx).Select() + aggregation := make([]string, 0, len(_g.fns)) + for _, fn := range _g.fns { + aggregation = append(aggregation, fn(selector)) + } + if len(selector.SelectedColumns()) == 0 { + columns := make([]string, 0, len(*_g.flds)+len(_g.fns)) + for _, f := range *_g.flds { + columns = append(columns, selector.C(f)) + } + columns = append(columns, aggregation...) + selector.Select(columns...) + } + selector.GroupBy(selector.Columns(*_g.flds...)...) + if err := selector.Err(); err != nil { + return err + } + rows := &sql.Rows{} + query, args := selector.Query() + if err := _g.build.driver.Query(ctx, query, args, rows); err != nil { + return err + } + defer rows.Close() + return sql.ScanSlice(rows, v) +} + +// InviteCodeSelect is the builder for selecting fields of InviteCode entities. +type InviteCodeSelect struct { + *InviteCodeQuery + selector +} + +// Aggregate adds the given aggregation functions to the selector query. +func (_s *InviteCodeSelect) Aggregate(fns ...AggregateFunc) *InviteCodeSelect { + _s.fns = append(_s.fns, fns...) + return _s +} + +// Scan applies the selector query and scans the result into the given value. +func (_s *InviteCodeSelect) Scan(ctx context.Context, v any) error { + ctx = setContextOp(ctx, _s.ctx, ent.OpQuerySelect) + if err := _s.prepareQuery(ctx); err != nil { + return err + } + return scanWithInterceptors[*InviteCodeQuery, *InviteCodeSelect](ctx, _s.InviteCodeQuery, _s, _s.inters, v) +} + +func (_s *InviteCodeSelect) sqlScan(ctx context.Context, root *InviteCodeQuery, v any) error { + selector := root.sqlQuery(ctx) + aggregation := make([]string, 0, len(_s.fns)) + for _, fn := range _s.fns { + aggregation = append(aggregation, fn(selector)) + } + switch n := len(*_s.selector.flds); { + case n == 0 && len(aggregation) > 0: + selector.Select(aggregation...) + case n != 0 && len(aggregation) > 0: + selector.AppendSelect(aggregation...) + } + rows := &sql.Rows{} + query, args := selector.Query() + if err := _s.driver.Query(ctx, query, args, rows); err != nil { + return err + } + defer rows.Close() + return sql.ScanSlice(rows, v) +} diff --git a/pkg/ent/invitecode_update.go b/pkg/ent/invitecode_update.go new file mode 100644 index 000000000..a4f1e70c7 --- /dev/null +++ b/pkg/ent/invitecode_update.go @@ -0,0 +1,534 @@ +// Code generated by ent, DO NOT EDIT. + +package ent + +import ( + "context" + "errors" + "fmt" + "time" + + "entgo.io/ent/dialect/sql" + "entgo.io/ent/dialect/sql/sqlgraph" + "entgo.io/ent/schema/field" + "github.com/GoogleCloudPlatform/scion/pkg/ent/invitecode" + "github.com/GoogleCloudPlatform/scion/pkg/ent/predicate" +) + +// InviteCodeUpdate is the builder for updating InviteCode entities. +type InviteCodeUpdate struct { + config + hooks []Hook + mutation *InviteCodeMutation +} + +// Where appends a list predicates to the InviteCodeUpdate builder. +func (_u *InviteCodeUpdate) Where(ps ...predicate.InviteCode) *InviteCodeUpdate { + _u.mutation.Where(ps...) + return _u +} + +// SetCodeHash sets the "code_hash" field. +func (_u *InviteCodeUpdate) SetCodeHash(v string) *InviteCodeUpdate { + _u.mutation.SetCodeHash(v) + return _u +} + +// SetNillableCodeHash sets the "code_hash" field if the given value is not nil. +func (_u *InviteCodeUpdate) SetNillableCodeHash(v *string) *InviteCodeUpdate { + if v != nil { + _u.SetCodeHash(*v) + } + return _u +} + +// SetCodePrefix sets the "code_prefix" field. +func (_u *InviteCodeUpdate) SetCodePrefix(v string) *InviteCodeUpdate { + _u.mutation.SetCodePrefix(v) + return _u +} + +// SetNillableCodePrefix sets the "code_prefix" field if the given value is not nil. +func (_u *InviteCodeUpdate) SetNillableCodePrefix(v *string) *InviteCodeUpdate { + if v != nil { + _u.SetCodePrefix(*v) + } + return _u +} + +// SetMaxUses sets the "max_uses" field. +func (_u *InviteCodeUpdate) SetMaxUses(v int) *InviteCodeUpdate { + _u.mutation.ResetMaxUses() + _u.mutation.SetMaxUses(v) + return _u +} + +// SetNillableMaxUses sets the "max_uses" field if the given value is not nil. +func (_u *InviteCodeUpdate) SetNillableMaxUses(v *int) *InviteCodeUpdate { + if v != nil { + _u.SetMaxUses(*v) + } + return _u +} + +// AddMaxUses adds value to the "max_uses" field. +func (_u *InviteCodeUpdate) AddMaxUses(v int) *InviteCodeUpdate { + _u.mutation.AddMaxUses(v) + return _u +} + +// SetUseCount sets the "use_count" field. +func (_u *InviteCodeUpdate) SetUseCount(v int) *InviteCodeUpdate { + _u.mutation.ResetUseCount() + _u.mutation.SetUseCount(v) + return _u +} + +// SetNillableUseCount sets the "use_count" field if the given value is not nil. +func (_u *InviteCodeUpdate) SetNillableUseCount(v *int) *InviteCodeUpdate { + if v != nil { + _u.SetUseCount(*v) + } + return _u +} + +// AddUseCount adds value to the "use_count" field. +func (_u *InviteCodeUpdate) AddUseCount(v int) *InviteCodeUpdate { + _u.mutation.AddUseCount(v) + return _u +} + +// SetExpiresAt sets the "expires_at" field. +func (_u *InviteCodeUpdate) SetExpiresAt(v time.Time) *InviteCodeUpdate { + _u.mutation.SetExpiresAt(v) + return _u +} + +// SetNillableExpiresAt sets the "expires_at" field if the given value is not nil. +func (_u *InviteCodeUpdate) SetNillableExpiresAt(v *time.Time) *InviteCodeUpdate { + if v != nil { + _u.SetExpiresAt(*v) + } + return _u +} + +// SetRevoked sets the "revoked" field. +func (_u *InviteCodeUpdate) SetRevoked(v bool) *InviteCodeUpdate { + _u.mutation.SetRevoked(v) + return _u +} + +// SetNillableRevoked sets the "revoked" field if the given value is not nil. +func (_u *InviteCodeUpdate) SetNillableRevoked(v *bool) *InviteCodeUpdate { + if v != nil { + _u.SetRevoked(*v) + } + return _u +} + +// SetCreatedBy sets the "created_by" field. +func (_u *InviteCodeUpdate) SetCreatedBy(v string) *InviteCodeUpdate { + _u.mutation.SetCreatedBy(v) + return _u +} + +// SetNillableCreatedBy sets the "created_by" field if the given value is not nil. +func (_u *InviteCodeUpdate) SetNillableCreatedBy(v *string) *InviteCodeUpdate { + if v != nil { + _u.SetCreatedBy(*v) + } + return _u +} + +// SetNote sets the "note" field. +func (_u *InviteCodeUpdate) SetNote(v string) *InviteCodeUpdate { + _u.mutation.SetNote(v) + return _u +} + +// SetNillableNote sets the "note" field if the given value is not nil. +func (_u *InviteCodeUpdate) SetNillableNote(v *string) *InviteCodeUpdate { + if v != nil { + _u.SetNote(*v) + } + return _u +} + +// Mutation returns the InviteCodeMutation object of the builder. +func (_u *InviteCodeUpdate) Mutation() *InviteCodeMutation { + return _u.mutation +} + +// Save executes the query and returns the number of nodes affected by the update operation. +func (_u *InviteCodeUpdate) Save(ctx context.Context) (int, error) { + return withHooks(ctx, _u.sqlSave, _u.mutation, _u.hooks) +} + +// SaveX is like Save, but panics if an error occurs. +func (_u *InviteCodeUpdate) SaveX(ctx context.Context) int { + affected, err := _u.Save(ctx) + if err != nil { + panic(err) + } + return affected +} + +// Exec executes the query. +func (_u *InviteCodeUpdate) Exec(ctx context.Context) error { + _, err := _u.Save(ctx) + return err +} + +// ExecX is like Exec, but panics if an error occurs. +func (_u *InviteCodeUpdate) ExecX(ctx context.Context) { + if err := _u.Exec(ctx); err != nil { + panic(err) + } +} + +// check runs all checks and user-defined validators on the builder. +func (_u *InviteCodeUpdate) check() error { + if v, ok := _u.mutation.CodeHash(); ok { + if err := invitecode.CodeHashValidator(v); err != nil { + return &ValidationError{Name: "code_hash", err: fmt.Errorf(`ent: validator failed for field "InviteCode.code_hash": %w`, err)} + } + } + if v, ok := _u.mutation.CodePrefix(); ok { + if err := invitecode.CodePrefixValidator(v); err != nil { + return &ValidationError{Name: "code_prefix", err: fmt.Errorf(`ent: validator failed for field "InviteCode.code_prefix": %w`, err)} + } + } + if v, ok := _u.mutation.CreatedBy(); ok { + if err := invitecode.CreatedByValidator(v); err != nil { + return &ValidationError{Name: "created_by", err: fmt.Errorf(`ent: validator failed for field "InviteCode.created_by": %w`, err)} + } + } + return nil +} + +func (_u *InviteCodeUpdate) sqlSave(ctx context.Context) (_node int, err error) { + if err := _u.check(); err != nil { + return _node, err + } + _spec := sqlgraph.NewUpdateSpec(invitecode.Table, invitecode.Columns, sqlgraph.NewFieldSpec(invitecode.FieldID, field.TypeUUID)) + if ps := _u.mutation.predicates; len(ps) > 0 { + _spec.Predicate = func(selector *sql.Selector) { + for i := range ps { + ps[i](selector) + } + } + } + if value, ok := _u.mutation.CodeHash(); ok { + _spec.SetField(invitecode.FieldCodeHash, field.TypeString, value) + } + if value, ok := _u.mutation.CodePrefix(); ok { + _spec.SetField(invitecode.FieldCodePrefix, field.TypeString, value) + } + if value, ok := _u.mutation.MaxUses(); ok { + _spec.SetField(invitecode.FieldMaxUses, field.TypeInt, value) + } + if value, ok := _u.mutation.AddedMaxUses(); ok { + _spec.AddField(invitecode.FieldMaxUses, field.TypeInt, value) + } + if value, ok := _u.mutation.UseCount(); ok { + _spec.SetField(invitecode.FieldUseCount, field.TypeInt, value) + } + if value, ok := _u.mutation.AddedUseCount(); ok { + _spec.AddField(invitecode.FieldUseCount, field.TypeInt, value) + } + if value, ok := _u.mutation.ExpiresAt(); ok { + _spec.SetField(invitecode.FieldExpiresAt, field.TypeTime, value) + } + if value, ok := _u.mutation.Revoked(); ok { + _spec.SetField(invitecode.FieldRevoked, field.TypeBool, value) + } + if value, ok := _u.mutation.CreatedBy(); ok { + _spec.SetField(invitecode.FieldCreatedBy, field.TypeString, value) + } + if value, ok := _u.mutation.Note(); ok { + _spec.SetField(invitecode.FieldNote, field.TypeString, value) + } + if _node, err = sqlgraph.UpdateNodes(ctx, _u.driver, _spec); err != nil { + if _, ok := err.(*sqlgraph.NotFoundError); ok { + err = &NotFoundError{invitecode.Label} + } else if sqlgraph.IsConstraintError(err) { + err = &ConstraintError{msg: err.Error(), wrap: err} + } + return 0, err + } + _u.mutation.done = true + return _node, nil +} + +// InviteCodeUpdateOne is the builder for updating a single InviteCode entity. +type InviteCodeUpdateOne struct { + config + fields []string + hooks []Hook + mutation *InviteCodeMutation +} + +// SetCodeHash sets the "code_hash" field. +func (_u *InviteCodeUpdateOne) SetCodeHash(v string) *InviteCodeUpdateOne { + _u.mutation.SetCodeHash(v) + return _u +} + +// SetNillableCodeHash sets the "code_hash" field if the given value is not nil. +func (_u *InviteCodeUpdateOne) SetNillableCodeHash(v *string) *InviteCodeUpdateOne { + if v != nil { + _u.SetCodeHash(*v) + } + return _u +} + +// SetCodePrefix sets the "code_prefix" field. +func (_u *InviteCodeUpdateOne) SetCodePrefix(v string) *InviteCodeUpdateOne { + _u.mutation.SetCodePrefix(v) + return _u +} + +// SetNillableCodePrefix sets the "code_prefix" field if the given value is not nil. +func (_u *InviteCodeUpdateOne) SetNillableCodePrefix(v *string) *InviteCodeUpdateOne { + if v != nil { + _u.SetCodePrefix(*v) + } + return _u +} + +// SetMaxUses sets the "max_uses" field. +func (_u *InviteCodeUpdateOne) SetMaxUses(v int) *InviteCodeUpdateOne { + _u.mutation.ResetMaxUses() + _u.mutation.SetMaxUses(v) + return _u +} + +// SetNillableMaxUses sets the "max_uses" field if the given value is not nil. +func (_u *InviteCodeUpdateOne) SetNillableMaxUses(v *int) *InviteCodeUpdateOne { + if v != nil { + _u.SetMaxUses(*v) + } + return _u +} + +// AddMaxUses adds value to the "max_uses" field. +func (_u *InviteCodeUpdateOne) AddMaxUses(v int) *InviteCodeUpdateOne { + _u.mutation.AddMaxUses(v) + return _u +} + +// SetUseCount sets the "use_count" field. +func (_u *InviteCodeUpdateOne) SetUseCount(v int) *InviteCodeUpdateOne { + _u.mutation.ResetUseCount() + _u.mutation.SetUseCount(v) + return _u +} + +// SetNillableUseCount sets the "use_count" field if the given value is not nil. +func (_u *InviteCodeUpdateOne) SetNillableUseCount(v *int) *InviteCodeUpdateOne { + if v != nil { + _u.SetUseCount(*v) + } + return _u +} + +// AddUseCount adds value to the "use_count" field. +func (_u *InviteCodeUpdateOne) AddUseCount(v int) *InviteCodeUpdateOne { + _u.mutation.AddUseCount(v) + return _u +} + +// SetExpiresAt sets the "expires_at" field. +func (_u *InviteCodeUpdateOne) SetExpiresAt(v time.Time) *InviteCodeUpdateOne { + _u.mutation.SetExpiresAt(v) + return _u +} + +// SetNillableExpiresAt sets the "expires_at" field if the given value is not nil. +func (_u *InviteCodeUpdateOne) SetNillableExpiresAt(v *time.Time) *InviteCodeUpdateOne { + if v != nil { + _u.SetExpiresAt(*v) + } + return _u +} + +// SetRevoked sets the "revoked" field. +func (_u *InviteCodeUpdateOne) SetRevoked(v bool) *InviteCodeUpdateOne { + _u.mutation.SetRevoked(v) + return _u +} + +// SetNillableRevoked sets the "revoked" field if the given value is not nil. +func (_u *InviteCodeUpdateOne) SetNillableRevoked(v *bool) *InviteCodeUpdateOne { + if v != nil { + _u.SetRevoked(*v) + } + return _u +} + +// SetCreatedBy sets the "created_by" field. +func (_u *InviteCodeUpdateOne) SetCreatedBy(v string) *InviteCodeUpdateOne { + _u.mutation.SetCreatedBy(v) + return _u +} + +// SetNillableCreatedBy sets the "created_by" field if the given value is not nil. +func (_u *InviteCodeUpdateOne) SetNillableCreatedBy(v *string) *InviteCodeUpdateOne { + if v != nil { + _u.SetCreatedBy(*v) + } + return _u +} + +// SetNote sets the "note" field. +func (_u *InviteCodeUpdateOne) SetNote(v string) *InviteCodeUpdateOne { + _u.mutation.SetNote(v) + return _u +} + +// SetNillableNote sets the "note" field if the given value is not nil. +func (_u *InviteCodeUpdateOne) SetNillableNote(v *string) *InviteCodeUpdateOne { + if v != nil { + _u.SetNote(*v) + } + return _u +} + +// Mutation returns the InviteCodeMutation object of the builder. +func (_u *InviteCodeUpdateOne) Mutation() *InviteCodeMutation { + return _u.mutation +} + +// Where appends a list predicates to the InviteCodeUpdate builder. +func (_u *InviteCodeUpdateOne) Where(ps ...predicate.InviteCode) *InviteCodeUpdateOne { + _u.mutation.Where(ps...) + return _u +} + +// Select allows selecting one or more fields (columns) of the returned entity. +// The default is selecting all fields defined in the entity schema. +func (_u *InviteCodeUpdateOne) Select(field string, fields ...string) *InviteCodeUpdateOne { + _u.fields = append([]string{field}, fields...) + return _u +} + +// Save executes the query and returns the updated InviteCode entity. +func (_u *InviteCodeUpdateOne) Save(ctx context.Context) (*InviteCode, error) { + return withHooks(ctx, _u.sqlSave, _u.mutation, _u.hooks) +} + +// SaveX is like Save, but panics if an error occurs. +func (_u *InviteCodeUpdateOne) SaveX(ctx context.Context) *InviteCode { + node, err := _u.Save(ctx) + if err != nil { + panic(err) + } + return node +} + +// Exec executes the query on the entity. +func (_u *InviteCodeUpdateOne) Exec(ctx context.Context) error { + _, err := _u.Save(ctx) + return err +} + +// ExecX is like Exec, but panics if an error occurs. +func (_u *InviteCodeUpdateOne) ExecX(ctx context.Context) { + if err := _u.Exec(ctx); err != nil { + panic(err) + } +} + +// check runs all checks and user-defined validators on the builder. +func (_u *InviteCodeUpdateOne) check() error { + if v, ok := _u.mutation.CodeHash(); ok { + if err := invitecode.CodeHashValidator(v); err != nil { + return &ValidationError{Name: "code_hash", err: fmt.Errorf(`ent: validator failed for field "InviteCode.code_hash": %w`, err)} + } + } + if v, ok := _u.mutation.CodePrefix(); ok { + if err := invitecode.CodePrefixValidator(v); err != nil { + return &ValidationError{Name: "code_prefix", err: fmt.Errorf(`ent: validator failed for field "InviteCode.code_prefix": %w`, err)} + } + } + if v, ok := _u.mutation.CreatedBy(); ok { + if err := invitecode.CreatedByValidator(v); err != nil { + return &ValidationError{Name: "created_by", err: fmt.Errorf(`ent: validator failed for field "InviteCode.created_by": %w`, err)} + } + } + return nil +} + +func (_u *InviteCodeUpdateOne) sqlSave(ctx context.Context) (_node *InviteCode, err error) { + if err := _u.check(); err != nil { + return _node, err + } + _spec := sqlgraph.NewUpdateSpec(invitecode.Table, invitecode.Columns, sqlgraph.NewFieldSpec(invitecode.FieldID, field.TypeUUID)) + id, ok := _u.mutation.ID() + if !ok { + return nil, &ValidationError{Name: "id", err: errors.New(`ent: missing "InviteCode.id" for update`)} + } + _spec.Node.ID.Value = id + if fields := _u.fields; len(fields) > 0 { + _spec.Node.Columns = make([]string, 0, len(fields)) + _spec.Node.Columns = append(_spec.Node.Columns, invitecode.FieldID) + for _, f := range fields { + if !invitecode.ValidColumn(f) { + return nil, &ValidationError{Name: f, err: fmt.Errorf("ent: invalid field %q for query", f)} + } + if f != invitecode.FieldID { + _spec.Node.Columns = append(_spec.Node.Columns, f) + } + } + } + if ps := _u.mutation.predicates; len(ps) > 0 { + _spec.Predicate = func(selector *sql.Selector) { + for i := range ps { + ps[i](selector) + } + } + } + if value, ok := _u.mutation.CodeHash(); ok { + _spec.SetField(invitecode.FieldCodeHash, field.TypeString, value) + } + if value, ok := _u.mutation.CodePrefix(); ok { + _spec.SetField(invitecode.FieldCodePrefix, field.TypeString, value) + } + if value, ok := _u.mutation.MaxUses(); ok { + _spec.SetField(invitecode.FieldMaxUses, field.TypeInt, value) + } + if value, ok := _u.mutation.AddedMaxUses(); ok { + _spec.AddField(invitecode.FieldMaxUses, field.TypeInt, value) + } + if value, ok := _u.mutation.UseCount(); ok { + _spec.SetField(invitecode.FieldUseCount, field.TypeInt, value) + } + if value, ok := _u.mutation.AddedUseCount(); ok { + _spec.AddField(invitecode.FieldUseCount, field.TypeInt, value) + } + if value, ok := _u.mutation.ExpiresAt(); ok { + _spec.SetField(invitecode.FieldExpiresAt, field.TypeTime, value) + } + if value, ok := _u.mutation.Revoked(); ok { + _spec.SetField(invitecode.FieldRevoked, field.TypeBool, value) + } + if value, ok := _u.mutation.CreatedBy(); ok { + _spec.SetField(invitecode.FieldCreatedBy, field.TypeString, value) + } + if value, ok := _u.mutation.Note(); ok { + _spec.SetField(invitecode.FieldNote, field.TypeString, value) + } + _node = &InviteCode{config: _u.config} + _spec.Assign = _node.assignValues + _spec.ScanValues = _node.scanValues + if err = sqlgraph.UpdateNode(ctx, _u.driver, _spec); err != nil { + if _, ok := err.(*sqlgraph.NotFoundError); ok { + err = &NotFoundError{invitecode.Label} + } else if sqlgraph.IsConstraintError(err) { + err = &ConstraintError{msg: err.Error(), wrap: err} + } + return nil, err + } + _u.mutation.done = true + return _node, nil +} diff --git a/pkg/ent/maintenanceoperation.go b/pkg/ent/maintenanceoperation.go new file mode 100644 index 000000000..2a8f6ab52 --- /dev/null +++ b/pkg/ent/maintenanceoperation.go @@ -0,0 +1,223 @@ +// Code generated by ent, DO NOT EDIT. + +package ent + +import ( + "fmt" + "strings" + "time" + + "entgo.io/ent" + "entgo.io/ent/dialect/sql" + "github.com/GoogleCloudPlatform/scion/pkg/ent/maintenanceoperation" + "github.com/google/uuid" +) + +// MaintenanceOperation is the model entity for the MaintenanceOperation schema. +type MaintenanceOperation struct { + config `json:"-"` + // ID of the ent. + ID uuid.UUID `json:"id,omitempty"` + // Key holds the value of the "key" field. + Key string `json:"key,omitempty"` + // Title holds the value of the "title" field. + Title string `json:"title,omitempty"` + // Description holds the value of the "description" field. + Description string `json:"description,omitempty"` + // Category holds the value of the "category" field. + Category string `json:"category,omitempty"` + // Status holds the value of the "status" field. + Status string `json:"status,omitempty"` + // StartedAt holds the value of the "started_at" field. + StartedAt *time.Time `json:"started_at,omitempty"` + // CompletedAt holds the value of the "completed_at" field. + CompletedAt *time.Time `json:"completed_at,omitempty"` + // StartedBy holds the value of the "started_by" field. + StartedBy string `json:"started_by,omitempty"` + // Result holds the value of the "result" field. + Result string `json:"result,omitempty"` + // Metadata holds the value of the "metadata" field. + Metadata string `json:"metadata,omitempty"` + // Created holds the value of the "created" field. + Created time.Time `json:"created,omitempty"` + selectValues sql.SelectValues +} + +// scanValues returns the types for scanning values from sql.Rows. +func (*MaintenanceOperation) scanValues(columns []string) ([]any, error) { + values := make([]any, len(columns)) + for i := range columns { + switch columns[i] { + case maintenanceoperation.FieldKey, maintenanceoperation.FieldTitle, maintenanceoperation.FieldDescription, maintenanceoperation.FieldCategory, maintenanceoperation.FieldStatus, maintenanceoperation.FieldStartedBy, maintenanceoperation.FieldResult, maintenanceoperation.FieldMetadata: + values[i] = new(sql.NullString) + case maintenanceoperation.FieldStartedAt, maintenanceoperation.FieldCompletedAt, maintenanceoperation.FieldCreated: + values[i] = new(sql.NullTime) + case maintenanceoperation.FieldID: + values[i] = new(uuid.UUID) + default: + values[i] = new(sql.UnknownType) + } + } + return values, nil +} + +// assignValues assigns the values that were returned from sql.Rows (after scanning) +// to the MaintenanceOperation fields. +func (_m *MaintenanceOperation) assignValues(columns []string, values []any) error { + if m, n := len(values), len(columns); m < n { + return fmt.Errorf("mismatch number of scan values: %d != %d", m, n) + } + for i := range columns { + switch columns[i] { + case maintenanceoperation.FieldID: + if value, ok := values[i].(*uuid.UUID); !ok { + return fmt.Errorf("unexpected type %T for field id", values[i]) + } else if value != nil { + _m.ID = *value + } + case maintenanceoperation.FieldKey: + if value, ok := values[i].(*sql.NullString); !ok { + return fmt.Errorf("unexpected type %T for field key", values[i]) + } else if value.Valid { + _m.Key = value.String + } + case maintenanceoperation.FieldTitle: + if value, ok := values[i].(*sql.NullString); !ok { + return fmt.Errorf("unexpected type %T for field title", values[i]) + } else if value.Valid { + _m.Title = value.String + } + case maintenanceoperation.FieldDescription: + if value, ok := values[i].(*sql.NullString); !ok { + return fmt.Errorf("unexpected type %T for field description", values[i]) + } else if value.Valid { + _m.Description = value.String + } + case maintenanceoperation.FieldCategory: + if value, ok := values[i].(*sql.NullString); !ok { + return fmt.Errorf("unexpected type %T for field category", values[i]) + } else if value.Valid { + _m.Category = value.String + } + case maintenanceoperation.FieldStatus: + if value, ok := values[i].(*sql.NullString); !ok { + return fmt.Errorf("unexpected type %T for field status", values[i]) + } else if value.Valid { + _m.Status = value.String + } + case maintenanceoperation.FieldStartedAt: + if value, ok := values[i].(*sql.NullTime); !ok { + return fmt.Errorf("unexpected type %T for field started_at", values[i]) + } else if value.Valid { + _m.StartedAt = new(time.Time) + *_m.StartedAt = value.Time + } + case maintenanceoperation.FieldCompletedAt: + if value, ok := values[i].(*sql.NullTime); !ok { + return fmt.Errorf("unexpected type %T for field completed_at", values[i]) + } else if value.Valid { + _m.CompletedAt = new(time.Time) + *_m.CompletedAt = value.Time + } + case maintenanceoperation.FieldStartedBy: + if value, ok := values[i].(*sql.NullString); !ok { + return fmt.Errorf("unexpected type %T for field started_by", values[i]) + } else if value.Valid { + _m.StartedBy = value.String + } + case maintenanceoperation.FieldResult: + if value, ok := values[i].(*sql.NullString); !ok { + return fmt.Errorf("unexpected type %T for field result", values[i]) + } else if value.Valid { + _m.Result = value.String + } + case maintenanceoperation.FieldMetadata: + if value, ok := values[i].(*sql.NullString); !ok { + return fmt.Errorf("unexpected type %T for field metadata", values[i]) + } else if value.Valid { + _m.Metadata = value.String + } + case maintenanceoperation.FieldCreated: + if value, ok := values[i].(*sql.NullTime); !ok { + return fmt.Errorf("unexpected type %T for field created", values[i]) + } else if value.Valid { + _m.Created = value.Time + } + default: + _m.selectValues.Set(columns[i], values[i]) + } + } + return nil +} + +// Value returns the ent.Value that was dynamically selected and assigned to the MaintenanceOperation. +// This includes values selected through modifiers, order, etc. +func (_m *MaintenanceOperation) Value(name string) (ent.Value, error) { + return _m.selectValues.Get(name) +} + +// Update returns a builder for updating this MaintenanceOperation. +// Note that you need to call MaintenanceOperation.Unwrap() before calling this method if this MaintenanceOperation +// was returned from a transaction, and the transaction was committed or rolled back. +func (_m *MaintenanceOperation) Update() *MaintenanceOperationUpdateOne { + return NewMaintenanceOperationClient(_m.config).UpdateOne(_m) +} + +// Unwrap unwraps the MaintenanceOperation entity that was returned from a transaction after it was closed, +// so that all future queries will be executed through the driver which created the transaction. +func (_m *MaintenanceOperation) Unwrap() *MaintenanceOperation { + _tx, ok := _m.config.driver.(*txDriver) + if !ok { + panic("ent: MaintenanceOperation is not a transactional entity") + } + _m.config.driver = _tx.drv + return _m +} + +// String implements the fmt.Stringer. +func (_m *MaintenanceOperation) String() string { + var builder strings.Builder + builder.WriteString("MaintenanceOperation(") + builder.WriteString(fmt.Sprintf("id=%v, ", _m.ID)) + builder.WriteString("key=") + builder.WriteString(_m.Key) + builder.WriteString(", ") + builder.WriteString("title=") + builder.WriteString(_m.Title) + builder.WriteString(", ") + builder.WriteString("description=") + builder.WriteString(_m.Description) + builder.WriteString(", ") + builder.WriteString("category=") + builder.WriteString(_m.Category) + builder.WriteString(", ") + builder.WriteString("status=") + builder.WriteString(_m.Status) + builder.WriteString(", ") + if v := _m.StartedAt; v != nil { + builder.WriteString("started_at=") + builder.WriteString(v.Format(time.ANSIC)) + } + builder.WriteString(", ") + if v := _m.CompletedAt; v != nil { + builder.WriteString("completed_at=") + builder.WriteString(v.Format(time.ANSIC)) + } + builder.WriteString(", ") + builder.WriteString("started_by=") + builder.WriteString(_m.StartedBy) + builder.WriteString(", ") + builder.WriteString("result=") + builder.WriteString(_m.Result) + builder.WriteString(", ") + builder.WriteString("metadata=") + builder.WriteString(_m.Metadata) + builder.WriteString(", ") + builder.WriteString("created=") + builder.WriteString(_m.Created.Format(time.ANSIC)) + builder.WriteByte(')') + return builder.String() +} + +// MaintenanceOperations is a parsable slice of MaintenanceOperation. +type MaintenanceOperations []*MaintenanceOperation diff --git a/pkg/ent/maintenanceoperation/maintenanceoperation.go b/pkg/ent/maintenanceoperation/maintenanceoperation.go new file mode 100644 index 000000000..29c36d452 --- /dev/null +++ b/pkg/ent/maintenanceoperation/maintenanceoperation.go @@ -0,0 +1,149 @@ +// Code generated by ent, DO NOT EDIT. + +package maintenanceoperation + +import ( + "time" + + "entgo.io/ent/dialect/sql" + "github.com/google/uuid" +) + +const ( + // Label holds the string label denoting the maintenanceoperation type in the database. + Label = "maintenance_operation" + // FieldID holds the string denoting the id field in the database. + FieldID = "id" + // FieldKey holds the string denoting the key field in the database. + FieldKey = "key" + // FieldTitle holds the string denoting the title field in the database. + FieldTitle = "title" + // FieldDescription holds the string denoting the description field in the database. + FieldDescription = "description" + // FieldCategory holds the string denoting the category field in the database. + FieldCategory = "category" + // FieldStatus holds the string denoting the status field in the database. + FieldStatus = "status" + // FieldStartedAt holds the string denoting the started_at field in the database. + FieldStartedAt = "started_at" + // FieldCompletedAt holds the string denoting the completed_at field in the database. + FieldCompletedAt = "completed_at" + // FieldStartedBy holds the string denoting the started_by field in the database. + FieldStartedBy = "started_by" + // FieldResult holds the string denoting the result field in the database. + FieldResult = "result" + // FieldMetadata holds the string denoting the metadata field in the database. + FieldMetadata = "metadata" + // FieldCreated holds the string denoting the created field in the database. + FieldCreated = "created" + // Table holds the table name of the maintenanceoperation in the database. + Table = "maintenance_operations" +) + +// Columns holds all SQL columns for maintenanceoperation fields. +var Columns = []string{ + FieldID, + FieldKey, + FieldTitle, + FieldDescription, + FieldCategory, + FieldStatus, + FieldStartedAt, + FieldCompletedAt, + FieldStartedBy, + FieldResult, + FieldMetadata, + FieldCreated, +} + +// ValidColumn reports if the column name is valid (part of the table columns). +func ValidColumn(column string) bool { + for i := range Columns { + if column == Columns[i] { + return true + } + } + return false +} + +var ( + // KeyValidator is a validator for the "key" field. It is called by the builders before save. + KeyValidator func(string) error + // TitleValidator is a validator for the "title" field. It is called by the builders before save. + TitleValidator func(string) error + // DefaultDescription holds the default value on creation for the "description" field. + DefaultDescription string + // CategoryValidator is a validator for the "category" field. It is called by the builders before save. + CategoryValidator func(string) error + // DefaultStatus holds the default value on creation for the "status" field. + DefaultStatus string + // DefaultMetadata holds the default value on creation for the "metadata" field. + DefaultMetadata string + // DefaultCreated holds the default value on creation for the "created" field. + DefaultCreated func() time.Time + // DefaultID holds the default value on creation for the "id" field. + DefaultID func() uuid.UUID +) + +// OrderOption defines the ordering options for the MaintenanceOperation queries. +type OrderOption func(*sql.Selector) + +// ByID orders the results by the id field. +func ByID(opts ...sql.OrderTermOption) OrderOption { + return sql.OrderByField(FieldID, opts...).ToFunc() +} + +// ByKey orders the results by the key field. +func ByKey(opts ...sql.OrderTermOption) OrderOption { + return sql.OrderByField(FieldKey, opts...).ToFunc() +} + +// ByTitle orders the results by the title field. +func ByTitle(opts ...sql.OrderTermOption) OrderOption { + return sql.OrderByField(FieldTitle, opts...).ToFunc() +} + +// ByDescription orders the results by the description field. +func ByDescription(opts ...sql.OrderTermOption) OrderOption { + return sql.OrderByField(FieldDescription, opts...).ToFunc() +} + +// ByCategory orders the results by the category field. +func ByCategory(opts ...sql.OrderTermOption) OrderOption { + return sql.OrderByField(FieldCategory, opts...).ToFunc() +} + +// ByStatus orders the results by the status field. +func ByStatus(opts ...sql.OrderTermOption) OrderOption { + return sql.OrderByField(FieldStatus, opts...).ToFunc() +} + +// ByStartedAt orders the results by the started_at field. +func ByStartedAt(opts ...sql.OrderTermOption) OrderOption { + return sql.OrderByField(FieldStartedAt, opts...).ToFunc() +} + +// ByCompletedAt orders the results by the completed_at field. +func ByCompletedAt(opts ...sql.OrderTermOption) OrderOption { + return sql.OrderByField(FieldCompletedAt, opts...).ToFunc() +} + +// ByStartedBy orders the results by the started_by field. +func ByStartedBy(opts ...sql.OrderTermOption) OrderOption { + return sql.OrderByField(FieldStartedBy, opts...).ToFunc() +} + +// ByResult orders the results by the result field. +func ByResult(opts ...sql.OrderTermOption) OrderOption { + return sql.OrderByField(FieldResult, opts...).ToFunc() +} + +// ByMetadata orders the results by the metadata field. +func ByMetadata(opts ...sql.OrderTermOption) OrderOption { + return sql.OrderByField(FieldMetadata, opts...).ToFunc() +} + +// ByCreated orders the results by the created field. +func ByCreated(opts ...sql.OrderTermOption) OrderOption { + return sql.OrderByField(FieldCreated, opts...).ToFunc() +} diff --git a/pkg/ent/maintenanceoperation/where.go b/pkg/ent/maintenanceoperation/where.go new file mode 100644 index 000000000..3fd4f77b3 --- /dev/null +++ b/pkg/ent/maintenanceoperation/where.go @@ -0,0 +1,806 @@ +// Code generated by ent, DO NOT EDIT. + +package maintenanceoperation + +import ( + "time" + + "entgo.io/ent/dialect/sql" + "github.com/GoogleCloudPlatform/scion/pkg/ent/predicate" + "github.com/google/uuid" +) + +// ID filters vertices based on their ID field. +func ID(id uuid.UUID) predicate.MaintenanceOperation { + return predicate.MaintenanceOperation(sql.FieldEQ(FieldID, id)) +} + +// IDEQ applies the EQ predicate on the ID field. +func IDEQ(id uuid.UUID) predicate.MaintenanceOperation { + return predicate.MaintenanceOperation(sql.FieldEQ(FieldID, id)) +} + +// IDNEQ applies the NEQ predicate on the ID field. +func IDNEQ(id uuid.UUID) predicate.MaintenanceOperation { + return predicate.MaintenanceOperation(sql.FieldNEQ(FieldID, id)) +} + +// IDIn applies the In predicate on the ID field. +func IDIn(ids ...uuid.UUID) predicate.MaintenanceOperation { + return predicate.MaintenanceOperation(sql.FieldIn(FieldID, ids...)) +} + +// IDNotIn applies the NotIn predicate on the ID field. +func IDNotIn(ids ...uuid.UUID) predicate.MaintenanceOperation { + return predicate.MaintenanceOperation(sql.FieldNotIn(FieldID, ids...)) +} + +// IDGT applies the GT predicate on the ID field. +func IDGT(id uuid.UUID) predicate.MaintenanceOperation { + return predicate.MaintenanceOperation(sql.FieldGT(FieldID, id)) +} + +// IDGTE applies the GTE predicate on the ID field. +func IDGTE(id uuid.UUID) predicate.MaintenanceOperation { + return predicate.MaintenanceOperation(sql.FieldGTE(FieldID, id)) +} + +// IDLT applies the LT predicate on the ID field. +func IDLT(id uuid.UUID) predicate.MaintenanceOperation { + return predicate.MaintenanceOperation(sql.FieldLT(FieldID, id)) +} + +// IDLTE applies the LTE predicate on the ID field. +func IDLTE(id uuid.UUID) predicate.MaintenanceOperation { + return predicate.MaintenanceOperation(sql.FieldLTE(FieldID, id)) +} + +// Key applies equality check predicate on the "key" field. It's identical to KeyEQ. +func Key(v string) predicate.MaintenanceOperation { + return predicate.MaintenanceOperation(sql.FieldEQ(FieldKey, v)) +} + +// Title applies equality check predicate on the "title" field. It's identical to TitleEQ. +func Title(v string) predicate.MaintenanceOperation { + return predicate.MaintenanceOperation(sql.FieldEQ(FieldTitle, v)) +} + +// Description applies equality check predicate on the "description" field. It's identical to DescriptionEQ. +func Description(v string) predicate.MaintenanceOperation { + return predicate.MaintenanceOperation(sql.FieldEQ(FieldDescription, v)) +} + +// Category applies equality check predicate on the "category" field. It's identical to CategoryEQ. +func Category(v string) predicate.MaintenanceOperation { + return predicate.MaintenanceOperation(sql.FieldEQ(FieldCategory, v)) +} + +// Status applies equality check predicate on the "status" field. It's identical to StatusEQ. +func Status(v string) predicate.MaintenanceOperation { + return predicate.MaintenanceOperation(sql.FieldEQ(FieldStatus, v)) +} + +// StartedAt applies equality check predicate on the "started_at" field. It's identical to StartedAtEQ. +func StartedAt(v time.Time) predicate.MaintenanceOperation { + return predicate.MaintenanceOperation(sql.FieldEQ(FieldStartedAt, v)) +} + +// CompletedAt applies equality check predicate on the "completed_at" field. It's identical to CompletedAtEQ. +func CompletedAt(v time.Time) predicate.MaintenanceOperation { + return predicate.MaintenanceOperation(sql.FieldEQ(FieldCompletedAt, v)) +} + +// StartedBy applies equality check predicate on the "started_by" field. It's identical to StartedByEQ. +func StartedBy(v string) predicate.MaintenanceOperation { + return predicate.MaintenanceOperation(sql.FieldEQ(FieldStartedBy, v)) +} + +// Result applies equality check predicate on the "result" field. It's identical to ResultEQ. +func Result(v string) predicate.MaintenanceOperation { + return predicate.MaintenanceOperation(sql.FieldEQ(FieldResult, v)) +} + +// Metadata applies equality check predicate on the "metadata" field. It's identical to MetadataEQ. +func Metadata(v string) predicate.MaintenanceOperation { + return predicate.MaintenanceOperation(sql.FieldEQ(FieldMetadata, v)) +} + +// Created applies equality check predicate on the "created" field. It's identical to CreatedEQ. +func Created(v time.Time) predicate.MaintenanceOperation { + return predicate.MaintenanceOperation(sql.FieldEQ(FieldCreated, v)) +} + +// KeyEQ applies the EQ predicate on the "key" field. +func KeyEQ(v string) predicate.MaintenanceOperation { + return predicate.MaintenanceOperation(sql.FieldEQ(FieldKey, v)) +} + +// KeyNEQ applies the NEQ predicate on the "key" field. +func KeyNEQ(v string) predicate.MaintenanceOperation { + return predicate.MaintenanceOperation(sql.FieldNEQ(FieldKey, v)) +} + +// KeyIn applies the In predicate on the "key" field. +func KeyIn(vs ...string) predicate.MaintenanceOperation { + return predicate.MaintenanceOperation(sql.FieldIn(FieldKey, vs...)) +} + +// KeyNotIn applies the NotIn predicate on the "key" field. +func KeyNotIn(vs ...string) predicate.MaintenanceOperation { + return predicate.MaintenanceOperation(sql.FieldNotIn(FieldKey, vs...)) +} + +// KeyGT applies the GT predicate on the "key" field. +func KeyGT(v string) predicate.MaintenanceOperation { + return predicate.MaintenanceOperation(sql.FieldGT(FieldKey, v)) +} + +// KeyGTE applies the GTE predicate on the "key" field. +func KeyGTE(v string) predicate.MaintenanceOperation { + return predicate.MaintenanceOperation(sql.FieldGTE(FieldKey, v)) +} + +// KeyLT applies the LT predicate on the "key" field. +func KeyLT(v string) predicate.MaintenanceOperation { + return predicate.MaintenanceOperation(sql.FieldLT(FieldKey, v)) +} + +// KeyLTE applies the LTE predicate on the "key" field. +func KeyLTE(v string) predicate.MaintenanceOperation { + return predicate.MaintenanceOperation(sql.FieldLTE(FieldKey, v)) +} + +// KeyContains applies the Contains predicate on the "key" field. +func KeyContains(v string) predicate.MaintenanceOperation { + return predicate.MaintenanceOperation(sql.FieldContains(FieldKey, v)) +} + +// KeyHasPrefix applies the HasPrefix predicate on the "key" field. +func KeyHasPrefix(v string) predicate.MaintenanceOperation { + return predicate.MaintenanceOperation(sql.FieldHasPrefix(FieldKey, v)) +} + +// KeyHasSuffix applies the HasSuffix predicate on the "key" field. +func KeyHasSuffix(v string) predicate.MaintenanceOperation { + return predicate.MaintenanceOperation(sql.FieldHasSuffix(FieldKey, v)) +} + +// KeyEqualFold applies the EqualFold predicate on the "key" field. +func KeyEqualFold(v string) predicate.MaintenanceOperation { + return predicate.MaintenanceOperation(sql.FieldEqualFold(FieldKey, v)) +} + +// KeyContainsFold applies the ContainsFold predicate on the "key" field. +func KeyContainsFold(v string) predicate.MaintenanceOperation { + return predicate.MaintenanceOperation(sql.FieldContainsFold(FieldKey, v)) +} + +// TitleEQ applies the EQ predicate on the "title" field. +func TitleEQ(v string) predicate.MaintenanceOperation { + return predicate.MaintenanceOperation(sql.FieldEQ(FieldTitle, v)) +} + +// TitleNEQ applies the NEQ predicate on the "title" field. +func TitleNEQ(v string) predicate.MaintenanceOperation { + return predicate.MaintenanceOperation(sql.FieldNEQ(FieldTitle, v)) +} + +// TitleIn applies the In predicate on the "title" field. +func TitleIn(vs ...string) predicate.MaintenanceOperation { + return predicate.MaintenanceOperation(sql.FieldIn(FieldTitle, vs...)) +} + +// TitleNotIn applies the NotIn predicate on the "title" field. +func TitleNotIn(vs ...string) predicate.MaintenanceOperation { + return predicate.MaintenanceOperation(sql.FieldNotIn(FieldTitle, vs...)) +} + +// TitleGT applies the GT predicate on the "title" field. +func TitleGT(v string) predicate.MaintenanceOperation { + return predicate.MaintenanceOperation(sql.FieldGT(FieldTitle, v)) +} + +// TitleGTE applies the GTE predicate on the "title" field. +func TitleGTE(v string) predicate.MaintenanceOperation { + return predicate.MaintenanceOperation(sql.FieldGTE(FieldTitle, v)) +} + +// TitleLT applies the LT predicate on the "title" field. +func TitleLT(v string) predicate.MaintenanceOperation { + return predicate.MaintenanceOperation(sql.FieldLT(FieldTitle, v)) +} + +// TitleLTE applies the LTE predicate on the "title" field. +func TitleLTE(v string) predicate.MaintenanceOperation { + return predicate.MaintenanceOperation(sql.FieldLTE(FieldTitle, v)) +} + +// TitleContains applies the Contains predicate on the "title" field. +func TitleContains(v string) predicate.MaintenanceOperation { + return predicate.MaintenanceOperation(sql.FieldContains(FieldTitle, v)) +} + +// TitleHasPrefix applies the HasPrefix predicate on the "title" field. +func TitleHasPrefix(v string) predicate.MaintenanceOperation { + return predicate.MaintenanceOperation(sql.FieldHasPrefix(FieldTitle, v)) +} + +// TitleHasSuffix applies the HasSuffix predicate on the "title" field. +func TitleHasSuffix(v string) predicate.MaintenanceOperation { + return predicate.MaintenanceOperation(sql.FieldHasSuffix(FieldTitle, v)) +} + +// TitleEqualFold applies the EqualFold predicate on the "title" field. +func TitleEqualFold(v string) predicate.MaintenanceOperation { + return predicate.MaintenanceOperation(sql.FieldEqualFold(FieldTitle, v)) +} + +// TitleContainsFold applies the ContainsFold predicate on the "title" field. +func TitleContainsFold(v string) predicate.MaintenanceOperation { + return predicate.MaintenanceOperation(sql.FieldContainsFold(FieldTitle, v)) +} + +// DescriptionEQ applies the EQ predicate on the "description" field. +func DescriptionEQ(v string) predicate.MaintenanceOperation { + return predicate.MaintenanceOperation(sql.FieldEQ(FieldDescription, v)) +} + +// DescriptionNEQ applies the NEQ predicate on the "description" field. +func DescriptionNEQ(v string) predicate.MaintenanceOperation { + return predicate.MaintenanceOperation(sql.FieldNEQ(FieldDescription, v)) +} + +// DescriptionIn applies the In predicate on the "description" field. +func DescriptionIn(vs ...string) predicate.MaintenanceOperation { + return predicate.MaintenanceOperation(sql.FieldIn(FieldDescription, vs...)) +} + +// DescriptionNotIn applies the NotIn predicate on the "description" field. +func DescriptionNotIn(vs ...string) predicate.MaintenanceOperation { + return predicate.MaintenanceOperation(sql.FieldNotIn(FieldDescription, vs...)) +} + +// DescriptionGT applies the GT predicate on the "description" field. +func DescriptionGT(v string) predicate.MaintenanceOperation { + return predicate.MaintenanceOperation(sql.FieldGT(FieldDescription, v)) +} + +// DescriptionGTE applies the GTE predicate on the "description" field. +func DescriptionGTE(v string) predicate.MaintenanceOperation { + return predicate.MaintenanceOperation(sql.FieldGTE(FieldDescription, v)) +} + +// DescriptionLT applies the LT predicate on the "description" field. +func DescriptionLT(v string) predicate.MaintenanceOperation { + return predicate.MaintenanceOperation(sql.FieldLT(FieldDescription, v)) +} + +// DescriptionLTE applies the LTE predicate on the "description" field. +func DescriptionLTE(v string) predicate.MaintenanceOperation { + return predicate.MaintenanceOperation(sql.FieldLTE(FieldDescription, v)) +} + +// DescriptionContains applies the Contains predicate on the "description" field. +func DescriptionContains(v string) predicate.MaintenanceOperation { + return predicate.MaintenanceOperation(sql.FieldContains(FieldDescription, v)) +} + +// DescriptionHasPrefix applies the HasPrefix predicate on the "description" field. +func DescriptionHasPrefix(v string) predicate.MaintenanceOperation { + return predicate.MaintenanceOperation(sql.FieldHasPrefix(FieldDescription, v)) +} + +// DescriptionHasSuffix applies the HasSuffix predicate on the "description" field. +func DescriptionHasSuffix(v string) predicate.MaintenanceOperation { + return predicate.MaintenanceOperation(sql.FieldHasSuffix(FieldDescription, v)) +} + +// DescriptionEqualFold applies the EqualFold predicate on the "description" field. +func DescriptionEqualFold(v string) predicate.MaintenanceOperation { + return predicate.MaintenanceOperation(sql.FieldEqualFold(FieldDescription, v)) +} + +// DescriptionContainsFold applies the ContainsFold predicate on the "description" field. +func DescriptionContainsFold(v string) predicate.MaintenanceOperation { + return predicate.MaintenanceOperation(sql.FieldContainsFold(FieldDescription, v)) +} + +// CategoryEQ applies the EQ predicate on the "category" field. +func CategoryEQ(v string) predicate.MaintenanceOperation { + return predicate.MaintenanceOperation(sql.FieldEQ(FieldCategory, v)) +} + +// CategoryNEQ applies the NEQ predicate on the "category" field. +func CategoryNEQ(v string) predicate.MaintenanceOperation { + return predicate.MaintenanceOperation(sql.FieldNEQ(FieldCategory, v)) +} + +// CategoryIn applies the In predicate on the "category" field. +func CategoryIn(vs ...string) predicate.MaintenanceOperation { + return predicate.MaintenanceOperation(sql.FieldIn(FieldCategory, vs...)) +} + +// CategoryNotIn applies the NotIn predicate on the "category" field. +func CategoryNotIn(vs ...string) predicate.MaintenanceOperation { + return predicate.MaintenanceOperation(sql.FieldNotIn(FieldCategory, vs...)) +} + +// CategoryGT applies the GT predicate on the "category" field. +func CategoryGT(v string) predicate.MaintenanceOperation { + return predicate.MaintenanceOperation(sql.FieldGT(FieldCategory, v)) +} + +// CategoryGTE applies the GTE predicate on the "category" field. +func CategoryGTE(v string) predicate.MaintenanceOperation { + return predicate.MaintenanceOperation(sql.FieldGTE(FieldCategory, v)) +} + +// CategoryLT applies the LT predicate on the "category" field. +func CategoryLT(v string) predicate.MaintenanceOperation { + return predicate.MaintenanceOperation(sql.FieldLT(FieldCategory, v)) +} + +// CategoryLTE applies the LTE predicate on the "category" field. +func CategoryLTE(v string) predicate.MaintenanceOperation { + return predicate.MaintenanceOperation(sql.FieldLTE(FieldCategory, v)) +} + +// CategoryContains applies the Contains predicate on the "category" field. +func CategoryContains(v string) predicate.MaintenanceOperation { + return predicate.MaintenanceOperation(sql.FieldContains(FieldCategory, v)) +} + +// CategoryHasPrefix applies the HasPrefix predicate on the "category" field. +func CategoryHasPrefix(v string) predicate.MaintenanceOperation { + return predicate.MaintenanceOperation(sql.FieldHasPrefix(FieldCategory, v)) +} + +// CategoryHasSuffix applies the HasSuffix predicate on the "category" field. +func CategoryHasSuffix(v string) predicate.MaintenanceOperation { + return predicate.MaintenanceOperation(sql.FieldHasSuffix(FieldCategory, v)) +} + +// CategoryEqualFold applies the EqualFold predicate on the "category" field. +func CategoryEqualFold(v string) predicate.MaintenanceOperation { + return predicate.MaintenanceOperation(sql.FieldEqualFold(FieldCategory, v)) +} + +// CategoryContainsFold applies the ContainsFold predicate on the "category" field. +func CategoryContainsFold(v string) predicate.MaintenanceOperation { + return predicate.MaintenanceOperation(sql.FieldContainsFold(FieldCategory, v)) +} + +// StatusEQ applies the EQ predicate on the "status" field. +func StatusEQ(v string) predicate.MaintenanceOperation { + return predicate.MaintenanceOperation(sql.FieldEQ(FieldStatus, v)) +} + +// StatusNEQ applies the NEQ predicate on the "status" field. +func StatusNEQ(v string) predicate.MaintenanceOperation { + return predicate.MaintenanceOperation(sql.FieldNEQ(FieldStatus, v)) +} + +// StatusIn applies the In predicate on the "status" field. +func StatusIn(vs ...string) predicate.MaintenanceOperation { + return predicate.MaintenanceOperation(sql.FieldIn(FieldStatus, vs...)) +} + +// StatusNotIn applies the NotIn predicate on the "status" field. +func StatusNotIn(vs ...string) predicate.MaintenanceOperation { + return predicate.MaintenanceOperation(sql.FieldNotIn(FieldStatus, vs...)) +} + +// StatusGT applies the GT predicate on the "status" field. +func StatusGT(v string) predicate.MaintenanceOperation { + return predicate.MaintenanceOperation(sql.FieldGT(FieldStatus, v)) +} + +// StatusGTE applies the GTE predicate on the "status" field. +func StatusGTE(v string) predicate.MaintenanceOperation { + return predicate.MaintenanceOperation(sql.FieldGTE(FieldStatus, v)) +} + +// StatusLT applies the LT predicate on the "status" field. +func StatusLT(v string) predicate.MaintenanceOperation { + return predicate.MaintenanceOperation(sql.FieldLT(FieldStatus, v)) +} + +// StatusLTE applies the LTE predicate on the "status" field. +func StatusLTE(v string) predicate.MaintenanceOperation { + return predicate.MaintenanceOperation(sql.FieldLTE(FieldStatus, v)) +} + +// StatusContains applies the Contains predicate on the "status" field. +func StatusContains(v string) predicate.MaintenanceOperation { + return predicate.MaintenanceOperation(sql.FieldContains(FieldStatus, v)) +} + +// StatusHasPrefix applies the HasPrefix predicate on the "status" field. +func StatusHasPrefix(v string) predicate.MaintenanceOperation { + return predicate.MaintenanceOperation(sql.FieldHasPrefix(FieldStatus, v)) +} + +// StatusHasSuffix applies the HasSuffix predicate on the "status" field. +func StatusHasSuffix(v string) predicate.MaintenanceOperation { + return predicate.MaintenanceOperation(sql.FieldHasSuffix(FieldStatus, v)) +} + +// StatusEqualFold applies the EqualFold predicate on the "status" field. +func StatusEqualFold(v string) predicate.MaintenanceOperation { + return predicate.MaintenanceOperation(sql.FieldEqualFold(FieldStatus, v)) +} + +// StatusContainsFold applies the ContainsFold predicate on the "status" field. +func StatusContainsFold(v string) predicate.MaintenanceOperation { + return predicate.MaintenanceOperation(sql.FieldContainsFold(FieldStatus, v)) +} + +// StartedAtEQ applies the EQ predicate on the "started_at" field. +func StartedAtEQ(v time.Time) predicate.MaintenanceOperation { + return predicate.MaintenanceOperation(sql.FieldEQ(FieldStartedAt, v)) +} + +// StartedAtNEQ applies the NEQ predicate on the "started_at" field. +func StartedAtNEQ(v time.Time) predicate.MaintenanceOperation { + return predicate.MaintenanceOperation(sql.FieldNEQ(FieldStartedAt, v)) +} + +// StartedAtIn applies the In predicate on the "started_at" field. +func StartedAtIn(vs ...time.Time) predicate.MaintenanceOperation { + return predicate.MaintenanceOperation(sql.FieldIn(FieldStartedAt, vs...)) +} + +// StartedAtNotIn applies the NotIn predicate on the "started_at" field. +func StartedAtNotIn(vs ...time.Time) predicate.MaintenanceOperation { + return predicate.MaintenanceOperation(sql.FieldNotIn(FieldStartedAt, vs...)) +} + +// StartedAtGT applies the GT predicate on the "started_at" field. +func StartedAtGT(v time.Time) predicate.MaintenanceOperation { + return predicate.MaintenanceOperation(sql.FieldGT(FieldStartedAt, v)) +} + +// StartedAtGTE applies the GTE predicate on the "started_at" field. +func StartedAtGTE(v time.Time) predicate.MaintenanceOperation { + return predicate.MaintenanceOperation(sql.FieldGTE(FieldStartedAt, v)) +} + +// StartedAtLT applies the LT predicate on the "started_at" field. +func StartedAtLT(v time.Time) predicate.MaintenanceOperation { + return predicate.MaintenanceOperation(sql.FieldLT(FieldStartedAt, v)) +} + +// StartedAtLTE applies the LTE predicate on the "started_at" field. +func StartedAtLTE(v time.Time) predicate.MaintenanceOperation { + return predicate.MaintenanceOperation(sql.FieldLTE(FieldStartedAt, v)) +} + +// StartedAtIsNil applies the IsNil predicate on the "started_at" field. +func StartedAtIsNil() predicate.MaintenanceOperation { + return predicate.MaintenanceOperation(sql.FieldIsNull(FieldStartedAt)) +} + +// StartedAtNotNil applies the NotNil predicate on the "started_at" field. +func StartedAtNotNil() predicate.MaintenanceOperation { + return predicate.MaintenanceOperation(sql.FieldNotNull(FieldStartedAt)) +} + +// CompletedAtEQ applies the EQ predicate on the "completed_at" field. +func CompletedAtEQ(v time.Time) predicate.MaintenanceOperation { + return predicate.MaintenanceOperation(sql.FieldEQ(FieldCompletedAt, v)) +} + +// CompletedAtNEQ applies the NEQ predicate on the "completed_at" field. +func CompletedAtNEQ(v time.Time) predicate.MaintenanceOperation { + return predicate.MaintenanceOperation(sql.FieldNEQ(FieldCompletedAt, v)) +} + +// CompletedAtIn applies the In predicate on the "completed_at" field. +func CompletedAtIn(vs ...time.Time) predicate.MaintenanceOperation { + return predicate.MaintenanceOperation(sql.FieldIn(FieldCompletedAt, vs...)) +} + +// CompletedAtNotIn applies the NotIn predicate on the "completed_at" field. +func CompletedAtNotIn(vs ...time.Time) predicate.MaintenanceOperation { + return predicate.MaintenanceOperation(sql.FieldNotIn(FieldCompletedAt, vs...)) +} + +// CompletedAtGT applies the GT predicate on the "completed_at" field. +func CompletedAtGT(v time.Time) predicate.MaintenanceOperation { + return predicate.MaintenanceOperation(sql.FieldGT(FieldCompletedAt, v)) +} + +// CompletedAtGTE applies the GTE predicate on the "completed_at" field. +func CompletedAtGTE(v time.Time) predicate.MaintenanceOperation { + return predicate.MaintenanceOperation(sql.FieldGTE(FieldCompletedAt, v)) +} + +// CompletedAtLT applies the LT predicate on the "completed_at" field. +func CompletedAtLT(v time.Time) predicate.MaintenanceOperation { + return predicate.MaintenanceOperation(sql.FieldLT(FieldCompletedAt, v)) +} + +// CompletedAtLTE applies the LTE predicate on the "completed_at" field. +func CompletedAtLTE(v time.Time) predicate.MaintenanceOperation { + return predicate.MaintenanceOperation(sql.FieldLTE(FieldCompletedAt, v)) +} + +// CompletedAtIsNil applies the IsNil predicate on the "completed_at" field. +func CompletedAtIsNil() predicate.MaintenanceOperation { + return predicate.MaintenanceOperation(sql.FieldIsNull(FieldCompletedAt)) +} + +// CompletedAtNotNil applies the NotNil predicate on the "completed_at" field. +func CompletedAtNotNil() predicate.MaintenanceOperation { + return predicate.MaintenanceOperation(sql.FieldNotNull(FieldCompletedAt)) +} + +// StartedByEQ applies the EQ predicate on the "started_by" field. +func StartedByEQ(v string) predicate.MaintenanceOperation { + return predicate.MaintenanceOperation(sql.FieldEQ(FieldStartedBy, v)) +} + +// StartedByNEQ applies the NEQ predicate on the "started_by" field. +func StartedByNEQ(v string) predicate.MaintenanceOperation { + return predicate.MaintenanceOperation(sql.FieldNEQ(FieldStartedBy, v)) +} + +// StartedByIn applies the In predicate on the "started_by" field. +func StartedByIn(vs ...string) predicate.MaintenanceOperation { + return predicate.MaintenanceOperation(sql.FieldIn(FieldStartedBy, vs...)) +} + +// StartedByNotIn applies the NotIn predicate on the "started_by" field. +func StartedByNotIn(vs ...string) predicate.MaintenanceOperation { + return predicate.MaintenanceOperation(sql.FieldNotIn(FieldStartedBy, vs...)) +} + +// StartedByGT applies the GT predicate on the "started_by" field. +func StartedByGT(v string) predicate.MaintenanceOperation { + return predicate.MaintenanceOperation(sql.FieldGT(FieldStartedBy, v)) +} + +// StartedByGTE applies the GTE predicate on the "started_by" field. +func StartedByGTE(v string) predicate.MaintenanceOperation { + return predicate.MaintenanceOperation(sql.FieldGTE(FieldStartedBy, v)) +} + +// StartedByLT applies the LT predicate on the "started_by" field. +func StartedByLT(v string) predicate.MaintenanceOperation { + return predicate.MaintenanceOperation(sql.FieldLT(FieldStartedBy, v)) +} + +// StartedByLTE applies the LTE predicate on the "started_by" field. +func StartedByLTE(v string) predicate.MaintenanceOperation { + return predicate.MaintenanceOperation(sql.FieldLTE(FieldStartedBy, v)) +} + +// StartedByContains applies the Contains predicate on the "started_by" field. +func StartedByContains(v string) predicate.MaintenanceOperation { + return predicate.MaintenanceOperation(sql.FieldContains(FieldStartedBy, v)) +} + +// StartedByHasPrefix applies the HasPrefix predicate on the "started_by" field. +func StartedByHasPrefix(v string) predicate.MaintenanceOperation { + return predicate.MaintenanceOperation(sql.FieldHasPrefix(FieldStartedBy, v)) +} + +// StartedByHasSuffix applies the HasSuffix predicate on the "started_by" field. +func StartedByHasSuffix(v string) predicate.MaintenanceOperation { + return predicate.MaintenanceOperation(sql.FieldHasSuffix(FieldStartedBy, v)) +} + +// StartedByIsNil applies the IsNil predicate on the "started_by" field. +func StartedByIsNil() predicate.MaintenanceOperation { + return predicate.MaintenanceOperation(sql.FieldIsNull(FieldStartedBy)) +} + +// StartedByNotNil applies the NotNil predicate on the "started_by" field. +func StartedByNotNil() predicate.MaintenanceOperation { + return predicate.MaintenanceOperation(sql.FieldNotNull(FieldStartedBy)) +} + +// StartedByEqualFold applies the EqualFold predicate on the "started_by" field. +func StartedByEqualFold(v string) predicate.MaintenanceOperation { + return predicate.MaintenanceOperation(sql.FieldEqualFold(FieldStartedBy, v)) +} + +// StartedByContainsFold applies the ContainsFold predicate on the "started_by" field. +func StartedByContainsFold(v string) predicate.MaintenanceOperation { + return predicate.MaintenanceOperation(sql.FieldContainsFold(FieldStartedBy, v)) +} + +// ResultEQ applies the EQ predicate on the "result" field. +func ResultEQ(v string) predicate.MaintenanceOperation { + return predicate.MaintenanceOperation(sql.FieldEQ(FieldResult, v)) +} + +// ResultNEQ applies the NEQ predicate on the "result" field. +func ResultNEQ(v string) predicate.MaintenanceOperation { + return predicate.MaintenanceOperation(sql.FieldNEQ(FieldResult, v)) +} + +// ResultIn applies the In predicate on the "result" field. +func ResultIn(vs ...string) predicate.MaintenanceOperation { + return predicate.MaintenanceOperation(sql.FieldIn(FieldResult, vs...)) +} + +// ResultNotIn applies the NotIn predicate on the "result" field. +func ResultNotIn(vs ...string) predicate.MaintenanceOperation { + return predicate.MaintenanceOperation(sql.FieldNotIn(FieldResult, vs...)) +} + +// ResultGT applies the GT predicate on the "result" field. +func ResultGT(v string) predicate.MaintenanceOperation { + return predicate.MaintenanceOperation(sql.FieldGT(FieldResult, v)) +} + +// ResultGTE applies the GTE predicate on the "result" field. +func ResultGTE(v string) predicate.MaintenanceOperation { + return predicate.MaintenanceOperation(sql.FieldGTE(FieldResult, v)) +} + +// ResultLT applies the LT predicate on the "result" field. +func ResultLT(v string) predicate.MaintenanceOperation { + return predicate.MaintenanceOperation(sql.FieldLT(FieldResult, v)) +} + +// ResultLTE applies the LTE predicate on the "result" field. +func ResultLTE(v string) predicate.MaintenanceOperation { + return predicate.MaintenanceOperation(sql.FieldLTE(FieldResult, v)) +} + +// ResultContains applies the Contains predicate on the "result" field. +func ResultContains(v string) predicate.MaintenanceOperation { + return predicate.MaintenanceOperation(sql.FieldContains(FieldResult, v)) +} + +// ResultHasPrefix applies the HasPrefix predicate on the "result" field. +func ResultHasPrefix(v string) predicate.MaintenanceOperation { + return predicate.MaintenanceOperation(sql.FieldHasPrefix(FieldResult, v)) +} + +// ResultHasSuffix applies the HasSuffix predicate on the "result" field. +func ResultHasSuffix(v string) predicate.MaintenanceOperation { + return predicate.MaintenanceOperation(sql.FieldHasSuffix(FieldResult, v)) +} + +// ResultIsNil applies the IsNil predicate on the "result" field. +func ResultIsNil() predicate.MaintenanceOperation { + return predicate.MaintenanceOperation(sql.FieldIsNull(FieldResult)) +} + +// ResultNotNil applies the NotNil predicate on the "result" field. +func ResultNotNil() predicate.MaintenanceOperation { + return predicate.MaintenanceOperation(sql.FieldNotNull(FieldResult)) +} + +// ResultEqualFold applies the EqualFold predicate on the "result" field. +func ResultEqualFold(v string) predicate.MaintenanceOperation { + return predicate.MaintenanceOperation(sql.FieldEqualFold(FieldResult, v)) +} + +// ResultContainsFold applies the ContainsFold predicate on the "result" field. +func ResultContainsFold(v string) predicate.MaintenanceOperation { + return predicate.MaintenanceOperation(sql.FieldContainsFold(FieldResult, v)) +} + +// MetadataEQ applies the EQ predicate on the "metadata" field. +func MetadataEQ(v string) predicate.MaintenanceOperation { + return predicate.MaintenanceOperation(sql.FieldEQ(FieldMetadata, v)) +} + +// MetadataNEQ applies the NEQ predicate on the "metadata" field. +func MetadataNEQ(v string) predicate.MaintenanceOperation { + return predicate.MaintenanceOperation(sql.FieldNEQ(FieldMetadata, v)) +} + +// MetadataIn applies the In predicate on the "metadata" field. +func MetadataIn(vs ...string) predicate.MaintenanceOperation { + return predicate.MaintenanceOperation(sql.FieldIn(FieldMetadata, vs...)) +} + +// MetadataNotIn applies the NotIn predicate on the "metadata" field. +func MetadataNotIn(vs ...string) predicate.MaintenanceOperation { + return predicate.MaintenanceOperation(sql.FieldNotIn(FieldMetadata, vs...)) +} + +// MetadataGT applies the GT predicate on the "metadata" field. +func MetadataGT(v string) predicate.MaintenanceOperation { + return predicate.MaintenanceOperation(sql.FieldGT(FieldMetadata, v)) +} + +// MetadataGTE applies the GTE predicate on the "metadata" field. +func MetadataGTE(v string) predicate.MaintenanceOperation { + return predicate.MaintenanceOperation(sql.FieldGTE(FieldMetadata, v)) +} + +// MetadataLT applies the LT predicate on the "metadata" field. +func MetadataLT(v string) predicate.MaintenanceOperation { + return predicate.MaintenanceOperation(sql.FieldLT(FieldMetadata, v)) +} + +// MetadataLTE applies the LTE predicate on the "metadata" field. +func MetadataLTE(v string) predicate.MaintenanceOperation { + return predicate.MaintenanceOperation(sql.FieldLTE(FieldMetadata, v)) +} + +// MetadataContains applies the Contains predicate on the "metadata" field. +func MetadataContains(v string) predicate.MaintenanceOperation { + return predicate.MaintenanceOperation(sql.FieldContains(FieldMetadata, v)) +} + +// MetadataHasPrefix applies the HasPrefix predicate on the "metadata" field. +func MetadataHasPrefix(v string) predicate.MaintenanceOperation { + return predicate.MaintenanceOperation(sql.FieldHasPrefix(FieldMetadata, v)) +} + +// MetadataHasSuffix applies the HasSuffix predicate on the "metadata" field. +func MetadataHasSuffix(v string) predicate.MaintenanceOperation { + return predicate.MaintenanceOperation(sql.FieldHasSuffix(FieldMetadata, v)) +} + +// MetadataEqualFold applies the EqualFold predicate on the "metadata" field. +func MetadataEqualFold(v string) predicate.MaintenanceOperation { + return predicate.MaintenanceOperation(sql.FieldEqualFold(FieldMetadata, v)) +} + +// MetadataContainsFold applies the ContainsFold predicate on the "metadata" field. +func MetadataContainsFold(v string) predicate.MaintenanceOperation { + return predicate.MaintenanceOperation(sql.FieldContainsFold(FieldMetadata, v)) +} + +// CreatedEQ applies the EQ predicate on the "created" field. +func CreatedEQ(v time.Time) predicate.MaintenanceOperation { + return predicate.MaintenanceOperation(sql.FieldEQ(FieldCreated, v)) +} + +// CreatedNEQ applies the NEQ predicate on the "created" field. +func CreatedNEQ(v time.Time) predicate.MaintenanceOperation { + return predicate.MaintenanceOperation(sql.FieldNEQ(FieldCreated, v)) +} + +// CreatedIn applies the In predicate on the "created" field. +func CreatedIn(vs ...time.Time) predicate.MaintenanceOperation { + return predicate.MaintenanceOperation(sql.FieldIn(FieldCreated, vs...)) +} + +// CreatedNotIn applies the NotIn predicate on the "created" field. +func CreatedNotIn(vs ...time.Time) predicate.MaintenanceOperation { + return predicate.MaintenanceOperation(sql.FieldNotIn(FieldCreated, vs...)) +} + +// CreatedGT applies the GT predicate on the "created" field. +func CreatedGT(v time.Time) predicate.MaintenanceOperation { + return predicate.MaintenanceOperation(sql.FieldGT(FieldCreated, v)) +} + +// CreatedGTE applies the GTE predicate on the "created" field. +func CreatedGTE(v time.Time) predicate.MaintenanceOperation { + return predicate.MaintenanceOperation(sql.FieldGTE(FieldCreated, v)) +} + +// CreatedLT applies the LT predicate on the "created" field. +func CreatedLT(v time.Time) predicate.MaintenanceOperation { + return predicate.MaintenanceOperation(sql.FieldLT(FieldCreated, v)) +} + +// CreatedLTE applies the LTE predicate on the "created" field. +func CreatedLTE(v time.Time) predicate.MaintenanceOperation { + return predicate.MaintenanceOperation(sql.FieldLTE(FieldCreated, v)) +} + +// And groups predicates with the AND operator between them. +func And(predicates ...predicate.MaintenanceOperation) predicate.MaintenanceOperation { + return predicate.MaintenanceOperation(sql.AndPredicates(predicates...)) +} + +// Or groups predicates with the OR operator between them. +func Or(predicates ...predicate.MaintenanceOperation) predicate.MaintenanceOperation { + return predicate.MaintenanceOperation(sql.OrPredicates(predicates...)) +} + +// Not applies the not operator on the given predicate. +func Not(p predicate.MaintenanceOperation) predicate.MaintenanceOperation { + return predicate.MaintenanceOperation(sql.NotPredicates(p)) +} diff --git a/pkg/ent/maintenanceoperation_create.go b/pkg/ent/maintenanceoperation_create.go new file mode 100644 index 000000000..eefc2cf6d --- /dev/null +++ b/pkg/ent/maintenanceoperation_create.go @@ -0,0 +1,1168 @@ +// Code generated by ent, DO NOT EDIT. + +package ent + +import ( + "context" + "errors" + "fmt" + "time" + + "entgo.io/ent/dialect" + "entgo.io/ent/dialect/sql" + "entgo.io/ent/dialect/sql/sqlgraph" + "entgo.io/ent/schema/field" + "github.com/GoogleCloudPlatform/scion/pkg/ent/maintenanceoperation" + "github.com/google/uuid" +) + +// MaintenanceOperationCreate is the builder for creating a MaintenanceOperation entity. +type MaintenanceOperationCreate struct { + config + mutation *MaintenanceOperationMutation + hooks []Hook + conflict []sql.ConflictOption +} + +// SetKey sets the "key" field. +func (_c *MaintenanceOperationCreate) SetKey(v string) *MaintenanceOperationCreate { + _c.mutation.SetKey(v) + return _c +} + +// SetTitle sets the "title" field. +func (_c *MaintenanceOperationCreate) SetTitle(v string) *MaintenanceOperationCreate { + _c.mutation.SetTitle(v) + return _c +} + +// SetDescription sets the "description" field. +func (_c *MaintenanceOperationCreate) SetDescription(v string) *MaintenanceOperationCreate { + _c.mutation.SetDescription(v) + return _c +} + +// SetNillableDescription sets the "description" field if the given value is not nil. +func (_c *MaintenanceOperationCreate) SetNillableDescription(v *string) *MaintenanceOperationCreate { + if v != nil { + _c.SetDescription(*v) + } + return _c +} + +// SetCategory sets the "category" field. +func (_c *MaintenanceOperationCreate) SetCategory(v string) *MaintenanceOperationCreate { + _c.mutation.SetCategory(v) + return _c +} + +// SetStatus sets the "status" field. +func (_c *MaintenanceOperationCreate) SetStatus(v string) *MaintenanceOperationCreate { + _c.mutation.SetStatus(v) + return _c +} + +// SetNillableStatus sets the "status" field if the given value is not nil. +func (_c *MaintenanceOperationCreate) SetNillableStatus(v *string) *MaintenanceOperationCreate { + if v != nil { + _c.SetStatus(*v) + } + return _c +} + +// SetStartedAt sets the "started_at" field. +func (_c *MaintenanceOperationCreate) SetStartedAt(v time.Time) *MaintenanceOperationCreate { + _c.mutation.SetStartedAt(v) + return _c +} + +// SetNillableStartedAt sets the "started_at" field if the given value is not nil. +func (_c *MaintenanceOperationCreate) SetNillableStartedAt(v *time.Time) *MaintenanceOperationCreate { + if v != nil { + _c.SetStartedAt(*v) + } + return _c +} + +// SetCompletedAt sets the "completed_at" field. +func (_c *MaintenanceOperationCreate) SetCompletedAt(v time.Time) *MaintenanceOperationCreate { + _c.mutation.SetCompletedAt(v) + return _c +} + +// SetNillableCompletedAt sets the "completed_at" field if the given value is not nil. +func (_c *MaintenanceOperationCreate) SetNillableCompletedAt(v *time.Time) *MaintenanceOperationCreate { + if v != nil { + _c.SetCompletedAt(*v) + } + return _c +} + +// SetStartedBy sets the "started_by" field. +func (_c *MaintenanceOperationCreate) SetStartedBy(v string) *MaintenanceOperationCreate { + _c.mutation.SetStartedBy(v) + return _c +} + +// SetNillableStartedBy sets the "started_by" field if the given value is not nil. +func (_c *MaintenanceOperationCreate) SetNillableStartedBy(v *string) *MaintenanceOperationCreate { + if v != nil { + _c.SetStartedBy(*v) + } + return _c +} + +// SetResult sets the "result" field. +func (_c *MaintenanceOperationCreate) SetResult(v string) *MaintenanceOperationCreate { + _c.mutation.SetResult(v) + return _c +} + +// SetNillableResult sets the "result" field if the given value is not nil. +func (_c *MaintenanceOperationCreate) SetNillableResult(v *string) *MaintenanceOperationCreate { + if v != nil { + _c.SetResult(*v) + } + return _c +} + +// SetMetadata sets the "metadata" field. +func (_c *MaintenanceOperationCreate) SetMetadata(v string) *MaintenanceOperationCreate { + _c.mutation.SetMetadata(v) + return _c +} + +// SetNillableMetadata sets the "metadata" field if the given value is not nil. +func (_c *MaintenanceOperationCreate) SetNillableMetadata(v *string) *MaintenanceOperationCreate { + if v != nil { + _c.SetMetadata(*v) + } + return _c +} + +// SetCreated sets the "created" field. +func (_c *MaintenanceOperationCreate) SetCreated(v time.Time) *MaintenanceOperationCreate { + _c.mutation.SetCreated(v) + return _c +} + +// SetNillableCreated sets the "created" field if the given value is not nil. +func (_c *MaintenanceOperationCreate) SetNillableCreated(v *time.Time) *MaintenanceOperationCreate { + if v != nil { + _c.SetCreated(*v) + } + return _c +} + +// SetID sets the "id" field. +func (_c *MaintenanceOperationCreate) SetID(v uuid.UUID) *MaintenanceOperationCreate { + _c.mutation.SetID(v) + return _c +} + +// SetNillableID sets the "id" field if the given value is not nil. +func (_c *MaintenanceOperationCreate) SetNillableID(v *uuid.UUID) *MaintenanceOperationCreate { + if v != nil { + _c.SetID(*v) + } + return _c +} + +// Mutation returns the MaintenanceOperationMutation object of the builder. +func (_c *MaintenanceOperationCreate) Mutation() *MaintenanceOperationMutation { + return _c.mutation +} + +// Save creates the MaintenanceOperation in the database. +func (_c *MaintenanceOperationCreate) Save(ctx context.Context) (*MaintenanceOperation, error) { + _c.defaults() + return withHooks(ctx, _c.sqlSave, _c.mutation, _c.hooks) +} + +// SaveX calls Save and panics if Save returns an error. +func (_c *MaintenanceOperationCreate) SaveX(ctx context.Context) *MaintenanceOperation { + v, err := _c.Save(ctx) + if err != nil { + panic(err) + } + return v +} + +// Exec executes the query. +func (_c *MaintenanceOperationCreate) Exec(ctx context.Context) error { + _, err := _c.Save(ctx) + return err +} + +// ExecX is like Exec, but panics if an error occurs. +func (_c *MaintenanceOperationCreate) ExecX(ctx context.Context) { + if err := _c.Exec(ctx); err != nil { + panic(err) + } +} + +// defaults sets the default values of the builder before save. +func (_c *MaintenanceOperationCreate) defaults() { + if _, ok := _c.mutation.Description(); !ok { + v := maintenanceoperation.DefaultDescription + _c.mutation.SetDescription(v) + } + if _, ok := _c.mutation.Status(); !ok { + v := maintenanceoperation.DefaultStatus + _c.mutation.SetStatus(v) + } + if _, ok := _c.mutation.Metadata(); !ok { + v := maintenanceoperation.DefaultMetadata + _c.mutation.SetMetadata(v) + } + if _, ok := _c.mutation.Created(); !ok { + v := maintenanceoperation.DefaultCreated() + _c.mutation.SetCreated(v) + } + if _, ok := _c.mutation.ID(); !ok { + v := maintenanceoperation.DefaultID() + _c.mutation.SetID(v) + } +} + +// check runs all checks and user-defined validators on the builder. +func (_c *MaintenanceOperationCreate) check() error { + if _, ok := _c.mutation.Key(); !ok { + return &ValidationError{Name: "key", err: errors.New(`ent: missing required field "MaintenanceOperation.key"`)} + } + if v, ok := _c.mutation.Key(); ok { + if err := maintenanceoperation.KeyValidator(v); err != nil { + return &ValidationError{Name: "key", err: fmt.Errorf(`ent: validator failed for field "MaintenanceOperation.key": %w`, err)} + } + } + if _, ok := _c.mutation.Title(); !ok { + return &ValidationError{Name: "title", err: errors.New(`ent: missing required field "MaintenanceOperation.title"`)} + } + if v, ok := _c.mutation.Title(); ok { + if err := maintenanceoperation.TitleValidator(v); err != nil { + return &ValidationError{Name: "title", err: fmt.Errorf(`ent: validator failed for field "MaintenanceOperation.title": %w`, err)} + } + } + if _, ok := _c.mutation.Description(); !ok { + return &ValidationError{Name: "description", err: errors.New(`ent: missing required field "MaintenanceOperation.description"`)} + } + if _, ok := _c.mutation.Category(); !ok { + return &ValidationError{Name: "category", err: errors.New(`ent: missing required field "MaintenanceOperation.category"`)} + } + if v, ok := _c.mutation.Category(); ok { + if err := maintenanceoperation.CategoryValidator(v); err != nil { + return &ValidationError{Name: "category", err: fmt.Errorf(`ent: validator failed for field "MaintenanceOperation.category": %w`, err)} + } + } + if _, ok := _c.mutation.Status(); !ok { + return &ValidationError{Name: "status", err: errors.New(`ent: missing required field "MaintenanceOperation.status"`)} + } + if _, ok := _c.mutation.Metadata(); !ok { + return &ValidationError{Name: "metadata", err: errors.New(`ent: missing required field "MaintenanceOperation.metadata"`)} + } + if _, ok := _c.mutation.Created(); !ok { + return &ValidationError{Name: "created", err: errors.New(`ent: missing required field "MaintenanceOperation.created"`)} + } + return nil +} + +func (_c *MaintenanceOperationCreate) sqlSave(ctx context.Context) (*MaintenanceOperation, error) { + if err := _c.check(); err != nil { + return nil, err + } + _node, _spec := _c.createSpec() + if err := sqlgraph.CreateNode(ctx, _c.driver, _spec); err != nil { + if sqlgraph.IsConstraintError(err) { + err = &ConstraintError{msg: err.Error(), wrap: err} + } + return nil, err + } + if _spec.ID.Value != nil { + if id, ok := _spec.ID.Value.(*uuid.UUID); ok { + _node.ID = *id + } else if err := _node.ID.Scan(_spec.ID.Value); err != nil { + return nil, err + } + } + _c.mutation.id = &_node.ID + _c.mutation.done = true + return _node, nil +} + +func (_c *MaintenanceOperationCreate) createSpec() (*MaintenanceOperation, *sqlgraph.CreateSpec) { + var ( + _node = &MaintenanceOperation{config: _c.config} + _spec = sqlgraph.NewCreateSpec(maintenanceoperation.Table, sqlgraph.NewFieldSpec(maintenanceoperation.FieldID, field.TypeUUID)) + ) + _spec.OnConflict = _c.conflict + if id, ok := _c.mutation.ID(); ok { + _node.ID = id + _spec.ID.Value = &id + } + if value, ok := _c.mutation.Key(); ok { + _spec.SetField(maintenanceoperation.FieldKey, field.TypeString, value) + _node.Key = value + } + if value, ok := _c.mutation.Title(); ok { + _spec.SetField(maintenanceoperation.FieldTitle, field.TypeString, value) + _node.Title = value + } + if value, ok := _c.mutation.Description(); ok { + _spec.SetField(maintenanceoperation.FieldDescription, field.TypeString, value) + _node.Description = value + } + if value, ok := _c.mutation.Category(); ok { + _spec.SetField(maintenanceoperation.FieldCategory, field.TypeString, value) + _node.Category = value + } + if value, ok := _c.mutation.Status(); ok { + _spec.SetField(maintenanceoperation.FieldStatus, field.TypeString, value) + _node.Status = value + } + if value, ok := _c.mutation.StartedAt(); ok { + _spec.SetField(maintenanceoperation.FieldStartedAt, field.TypeTime, value) + _node.StartedAt = &value + } + if value, ok := _c.mutation.CompletedAt(); ok { + _spec.SetField(maintenanceoperation.FieldCompletedAt, field.TypeTime, value) + _node.CompletedAt = &value + } + if value, ok := _c.mutation.StartedBy(); ok { + _spec.SetField(maintenanceoperation.FieldStartedBy, field.TypeString, value) + _node.StartedBy = value + } + if value, ok := _c.mutation.Result(); ok { + _spec.SetField(maintenanceoperation.FieldResult, field.TypeString, value) + _node.Result = value + } + if value, ok := _c.mutation.Metadata(); ok { + _spec.SetField(maintenanceoperation.FieldMetadata, field.TypeString, value) + _node.Metadata = value + } + if value, ok := _c.mutation.Created(); ok { + _spec.SetField(maintenanceoperation.FieldCreated, field.TypeTime, value) + _node.Created = value + } + return _node, _spec +} + +// OnConflict allows configuring the `ON CONFLICT` / `ON DUPLICATE KEY` clause +// of the `INSERT` statement. For example: +// +// client.MaintenanceOperation.Create(). +// SetKey(v). +// OnConflict( +// // Update the row with the new values +// // the was proposed for insertion. +// sql.ResolveWithNewValues(), +// ). +// // Override some of the fields with custom +// // update values. +// Update(func(u *ent.MaintenanceOperationUpsert) { +// SetKey(v+v). +// }). +// Exec(ctx) +func (_c *MaintenanceOperationCreate) OnConflict(opts ...sql.ConflictOption) *MaintenanceOperationUpsertOne { + _c.conflict = opts + return &MaintenanceOperationUpsertOne{ + create: _c, + } +} + +// OnConflictColumns calls `OnConflict` and configures the columns +// as conflict target. Using this option is equivalent to using: +// +// client.MaintenanceOperation.Create(). +// OnConflict(sql.ConflictColumns(columns...)). +// Exec(ctx) +func (_c *MaintenanceOperationCreate) OnConflictColumns(columns ...string) *MaintenanceOperationUpsertOne { + _c.conflict = append(_c.conflict, sql.ConflictColumns(columns...)) + return &MaintenanceOperationUpsertOne{ + create: _c, + } +} + +type ( + // MaintenanceOperationUpsertOne is the builder for "upsert"-ing + // one MaintenanceOperation node. + MaintenanceOperationUpsertOne struct { + create *MaintenanceOperationCreate + } + + // MaintenanceOperationUpsert is the "OnConflict" setter. + MaintenanceOperationUpsert struct { + *sql.UpdateSet + } +) + +// SetKey sets the "key" field. +func (u *MaintenanceOperationUpsert) SetKey(v string) *MaintenanceOperationUpsert { + u.Set(maintenanceoperation.FieldKey, v) + return u +} + +// UpdateKey sets the "key" field to the value that was provided on create. +func (u *MaintenanceOperationUpsert) UpdateKey() *MaintenanceOperationUpsert { + u.SetExcluded(maintenanceoperation.FieldKey) + return u +} + +// SetTitle sets the "title" field. +func (u *MaintenanceOperationUpsert) SetTitle(v string) *MaintenanceOperationUpsert { + u.Set(maintenanceoperation.FieldTitle, v) + return u +} + +// UpdateTitle sets the "title" field to the value that was provided on create. +func (u *MaintenanceOperationUpsert) UpdateTitle() *MaintenanceOperationUpsert { + u.SetExcluded(maintenanceoperation.FieldTitle) + return u +} + +// SetDescription sets the "description" field. +func (u *MaintenanceOperationUpsert) SetDescription(v string) *MaintenanceOperationUpsert { + u.Set(maintenanceoperation.FieldDescription, v) + return u +} + +// UpdateDescription sets the "description" field to the value that was provided on create. +func (u *MaintenanceOperationUpsert) UpdateDescription() *MaintenanceOperationUpsert { + u.SetExcluded(maintenanceoperation.FieldDescription) + return u +} + +// SetCategory sets the "category" field. +func (u *MaintenanceOperationUpsert) SetCategory(v string) *MaintenanceOperationUpsert { + u.Set(maintenanceoperation.FieldCategory, v) + return u +} + +// UpdateCategory sets the "category" field to the value that was provided on create. +func (u *MaintenanceOperationUpsert) UpdateCategory() *MaintenanceOperationUpsert { + u.SetExcluded(maintenanceoperation.FieldCategory) + return u +} + +// SetStatus sets the "status" field. +func (u *MaintenanceOperationUpsert) SetStatus(v string) *MaintenanceOperationUpsert { + u.Set(maintenanceoperation.FieldStatus, v) + return u +} + +// UpdateStatus sets the "status" field to the value that was provided on create. +func (u *MaintenanceOperationUpsert) UpdateStatus() *MaintenanceOperationUpsert { + u.SetExcluded(maintenanceoperation.FieldStatus) + return u +} + +// SetStartedAt sets the "started_at" field. +func (u *MaintenanceOperationUpsert) SetStartedAt(v time.Time) *MaintenanceOperationUpsert { + u.Set(maintenanceoperation.FieldStartedAt, v) + return u +} + +// UpdateStartedAt sets the "started_at" field to the value that was provided on create. +func (u *MaintenanceOperationUpsert) UpdateStartedAt() *MaintenanceOperationUpsert { + u.SetExcluded(maintenanceoperation.FieldStartedAt) + return u +} + +// ClearStartedAt clears the value of the "started_at" field. +func (u *MaintenanceOperationUpsert) ClearStartedAt() *MaintenanceOperationUpsert { + u.SetNull(maintenanceoperation.FieldStartedAt) + return u +} + +// SetCompletedAt sets the "completed_at" field. +func (u *MaintenanceOperationUpsert) SetCompletedAt(v time.Time) *MaintenanceOperationUpsert { + u.Set(maintenanceoperation.FieldCompletedAt, v) + return u +} + +// UpdateCompletedAt sets the "completed_at" field to the value that was provided on create. +func (u *MaintenanceOperationUpsert) UpdateCompletedAt() *MaintenanceOperationUpsert { + u.SetExcluded(maintenanceoperation.FieldCompletedAt) + return u +} + +// ClearCompletedAt clears the value of the "completed_at" field. +func (u *MaintenanceOperationUpsert) ClearCompletedAt() *MaintenanceOperationUpsert { + u.SetNull(maintenanceoperation.FieldCompletedAt) + return u +} + +// SetStartedBy sets the "started_by" field. +func (u *MaintenanceOperationUpsert) SetStartedBy(v string) *MaintenanceOperationUpsert { + u.Set(maintenanceoperation.FieldStartedBy, v) + return u +} + +// UpdateStartedBy sets the "started_by" field to the value that was provided on create. +func (u *MaintenanceOperationUpsert) UpdateStartedBy() *MaintenanceOperationUpsert { + u.SetExcluded(maintenanceoperation.FieldStartedBy) + return u +} + +// ClearStartedBy clears the value of the "started_by" field. +func (u *MaintenanceOperationUpsert) ClearStartedBy() *MaintenanceOperationUpsert { + u.SetNull(maintenanceoperation.FieldStartedBy) + return u +} + +// SetResult sets the "result" field. +func (u *MaintenanceOperationUpsert) SetResult(v string) *MaintenanceOperationUpsert { + u.Set(maintenanceoperation.FieldResult, v) + return u +} + +// UpdateResult sets the "result" field to the value that was provided on create. +func (u *MaintenanceOperationUpsert) UpdateResult() *MaintenanceOperationUpsert { + u.SetExcluded(maintenanceoperation.FieldResult) + return u +} + +// ClearResult clears the value of the "result" field. +func (u *MaintenanceOperationUpsert) ClearResult() *MaintenanceOperationUpsert { + u.SetNull(maintenanceoperation.FieldResult) + return u +} + +// SetMetadata sets the "metadata" field. +func (u *MaintenanceOperationUpsert) SetMetadata(v string) *MaintenanceOperationUpsert { + u.Set(maintenanceoperation.FieldMetadata, v) + return u +} + +// UpdateMetadata sets the "metadata" field to the value that was provided on create. +func (u *MaintenanceOperationUpsert) UpdateMetadata() *MaintenanceOperationUpsert { + u.SetExcluded(maintenanceoperation.FieldMetadata) + return u +} + +// UpdateNewValues updates the mutable fields using the new values that were set on create except the ID field. +// Using this option is equivalent to using: +// +// client.MaintenanceOperation.Create(). +// OnConflict( +// sql.ResolveWithNewValues(), +// sql.ResolveWith(func(u *sql.UpdateSet) { +// u.SetIgnore(maintenanceoperation.FieldID) +// }), +// ). +// Exec(ctx) +func (u *MaintenanceOperationUpsertOne) UpdateNewValues() *MaintenanceOperationUpsertOne { + u.create.conflict = append(u.create.conflict, sql.ResolveWithNewValues()) + u.create.conflict = append(u.create.conflict, sql.ResolveWith(func(s *sql.UpdateSet) { + if _, exists := u.create.mutation.ID(); exists { + s.SetIgnore(maintenanceoperation.FieldID) + } + if _, exists := u.create.mutation.Created(); exists { + s.SetIgnore(maintenanceoperation.FieldCreated) + } + })) + return u +} + +// Ignore sets each column to itself in case of conflict. +// Using this option is equivalent to using: +// +// client.MaintenanceOperation.Create(). +// OnConflict(sql.ResolveWithIgnore()). +// Exec(ctx) +func (u *MaintenanceOperationUpsertOne) Ignore() *MaintenanceOperationUpsertOne { + u.create.conflict = append(u.create.conflict, sql.ResolveWithIgnore()) + return u +} + +// DoNothing configures the conflict_action to `DO NOTHING`. +// Supported only by SQLite and PostgreSQL. +func (u *MaintenanceOperationUpsertOne) DoNothing() *MaintenanceOperationUpsertOne { + u.create.conflict = append(u.create.conflict, sql.DoNothing()) + return u +} + +// Update allows overriding fields `UPDATE` values. See the MaintenanceOperationCreate.OnConflict +// documentation for more info. +func (u *MaintenanceOperationUpsertOne) Update(set func(*MaintenanceOperationUpsert)) *MaintenanceOperationUpsertOne { + u.create.conflict = append(u.create.conflict, sql.ResolveWith(func(update *sql.UpdateSet) { + set(&MaintenanceOperationUpsert{UpdateSet: update}) + })) + return u +} + +// SetKey sets the "key" field. +func (u *MaintenanceOperationUpsertOne) SetKey(v string) *MaintenanceOperationUpsertOne { + return u.Update(func(s *MaintenanceOperationUpsert) { + s.SetKey(v) + }) +} + +// UpdateKey sets the "key" field to the value that was provided on create. +func (u *MaintenanceOperationUpsertOne) UpdateKey() *MaintenanceOperationUpsertOne { + return u.Update(func(s *MaintenanceOperationUpsert) { + s.UpdateKey() + }) +} + +// SetTitle sets the "title" field. +func (u *MaintenanceOperationUpsertOne) SetTitle(v string) *MaintenanceOperationUpsertOne { + return u.Update(func(s *MaintenanceOperationUpsert) { + s.SetTitle(v) + }) +} + +// UpdateTitle sets the "title" field to the value that was provided on create. +func (u *MaintenanceOperationUpsertOne) UpdateTitle() *MaintenanceOperationUpsertOne { + return u.Update(func(s *MaintenanceOperationUpsert) { + s.UpdateTitle() + }) +} + +// SetDescription sets the "description" field. +func (u *MaintenanceOperationUpsertOne) SetDescription(v string) *MaintenanceOperationUpsertOne { + return u.Update(func(s *MaintenanceOperationUpsert) { + s.SetDescription(v) + }) +} + +// UpdateDescription sets the "description" field to the value that was provided on create. +func (u *MaintenanceOperationUpsertOne) UpdateDescription() *MaintenanceOperationUpsertOne { + return u.Update(func(s *MaintenanceOperationUpsert) { + s.UpdateDescription() + }) +} + +// SetCategory sets the "category" field. +func (u *MaintenanceOperationUpsertOne) SetCategory(v string) *MaintenanceOperationUpsertOne { + return u.Update(func(s *MaintenanceOperationUpsert) { + s.SetCategory(v) + }) +} + +// UpdateCategory sets the "category" field to the value that was provided on create. +func (u *MaintenanceOperationUpsertOne) UpdateCategory() *MaintenanceOperationUpsertOne { + return u.Update(func(s *MaintenanceOperationUpsert) { + s.UpdateCategory() + }) +} + +// SetStatus sets the "status" field. +func (u *MaintenanceOperationUpsertOne) SetStatus(v string) *MaintenanceOperationUpsertOne { + return u.Update(func(s *MaintenanceOperationUpsert) { + s.SetStatus(v) + }) +} + +// UpdateStatus sets the "status" field to the value that was provided on create. +func (u *MaintenanceOperationUpsertOne) UpdateStatus() *MaintenanceOperationUpsertOne { + return u.Update(func(s *MaintenanceOperationUpsert) { + s.UpdateStatus() + }) +} + +// SetStartedAt sets the "started_at" field. +func (u *MaintenanceOperationUpsertOne) SetStartedAt(v time.Time) *MaintenanceOperationUpsertOne { + return u.Update(func(s *MaintenanceOperationUpsert) { + s.SetStartedAt(v) + }) +} + +// UpdateStartedAt sets the "started_at" field to the value that was provided on create. +func (u *MaintenanceOperationUpsertOne) UpdateStartedAt() *MaintenanceOperationUpsertOne { + return u.Update(func(s *MaintenanceOperationUpsert) { + s.UpdateStartedAt() + }) +} + +// ClearStartedAt clears the value of the "started_at" field. +func (u *MaintenanceOperationUpsertOne) ClearStartedAt() *MaintenanceOperationUpsertOne { + return u.Update(func(s *MaintenanceOperationUpsert) { + s.ClearStartedAt() + }) +} + +// SetCompletedAt sets the "completed_at" field. +func (u *MaintenanceOperationUpsertOne) SetCompletedAt(v time.Time) *MaintenanceOperationUpsertOne { + return u.Update(func(s *MaintenanceOperationUpsert) { + s.SetCompletedAt(v) + }) +} + +// UpdateCompletedAt sets the "completed_at" field to the value that was provided on create. +func (u *MaintenanceOperationUpsertOne) UpdateCompletedAt() *MaintenanceOperationUpsertOne { + return u.Update(func(s *MaintenanceOperationUpsert) { + s.UpdateCompletedAt() + }) +} + +// ClearCompletedAt clears the value of the "completed_at" field. +func (u *MaintenanceOperationUpsertOne) ClearCompletedAt() *MaintenanceOperationUpsertOne { + return u.Update(func(s *MaintenanceOperationUpsert) { + s.ClearCompletedAt() + }) +} + +// SetStartedBy sets the "started_by" field. +func (u *MaintenanceOperationUpsertOne) SetStartedBy(v string) *MaintenanceOperationUpsertOne { + return u.Update(func(s *MaintenanceOperationUpsert) { + s.SetStartedBy(v) + }) +} + +// UpdateStartedBy sets the "started_by" field to the value that was provided on create. +func (u *MaintenanceOperationUpsertOne) UpdateStartedBy() *MaintenanceOperationUpsertOne { + return u.Update(func(s *MaintenanceOperationUpsert) { + s.UpdateStartedBy() + }) +} + +// ClearStartedBy clears the value of the "started_by" field. +func (u *MaintenanceOperationUpsertOne) ClearStartedBy() *MaintenanceOperationUpsertOne { + return u.Update(func(s *MaintenanceOperationUpsert) { + s.ClearStartedBy() + }) +} + +// SetResult sets the "result" field. +func (u *MaintenanceOperationUpsertOne) SetResult(v string) *MaintenanceOperationUpsertOne { + return u.Update(func(s *MaintenanceOperationUpsert) { + s.SetResult(v) + }) +} + +// UpdateResult sets the "result" field to the value that was provided on create. +func (u *MaintenanceOperationUpsertOne) UpdateResult() *MaintenanceOperationUpsertOne { + return u.Update(func(s *MaintenanceOperationUpsert) { + s.UpdateResult() + }) +} + +// ClearResult clears the value of the "result" field. +func (u *MaintenanceOperationUpsertOne) ClearResult() *MaintenanceOperationUpsertOne { + return u.Update(func(s *MaintenanceOperationUpsert) { + s.ClearResult() + }) +} + +// SetMetadata sets the "metadata" field. +func (u *MaintenanceOperationUpsertOne) SetMetadata(v string) *MaintenanceOperationUpsertOne { + return u.Update(func(s *MaintenanceOperationUpsert) { + s.SetMetadata(v) + }) +} + +// UpdateMetadata sets the "metadata" field to the value that was provided on create. +func (u *MaintenanceOperationUpsertOne) UpdateMetadata() *MaintenanceOperationUpsertOne { + return u.Update(func(s *MaintenanceOperationUpsert) { + s.UpdateMetadata() + }) +} + +// Exec executes the query. +func (u *MaintenanceOperationUpsertOne) Exec(ctx context.Context) error { + if len(u.create.conflict) == 0 { + return errors.New("ent: missing options for MaintenanceOperationCreate.OnConflict") + } + return u.create.Exec(ctx) +} + +// ExecX is like Exec, but panics if an error occurs. +func (u *MaintenanceOperationUpsertOne) ExecX(ctx context.Context) { + if err := u.create.Exec(ctx); err != nil { + panic(err) + } +} + +// Exec executes the UPSERT query and returns the inserted/updated ID. +func (u *MaintenanceOperationUpsertOne) ID(ctx context.Context) (id uuid.UUID, err error) { + if u.create.driver.Dialect() == dialect.MySQL { + // In case of "ON CONFLICT", there is no way to get back non-numeric ID + // fields from the database since MySQL does not support the RETURNING clause. + return id, errors.New("ent: MaintenanceOperationUpsertOne.ID is not supported by MySQL driver. Use MaintenanceOperationUpsertOne.Exec instead") + } + node, err := u.create.Save(ctx) + if err != nil { + return id, err + } + return node.ID, nil +} + +// IDX is like ID, but panics if an error occurs. +func (u *MaintenanceOperationUpsertOne) IDX(ctx context.Context) uuid.UUID { + id, err := u.ID(ctx) + if err != nil { + panic(err) + } + return id +} + +// MaintenanceOperationCreateBulk is the builder for creating many MaintenanceOperation entities in bulk. +type MaintenanceOperationCreateBulk struct { + config + err error + builders []*MaintenanceOperationCreate + conflict []sql.ConflictOption +} + +// Save creates the MaintenanceOperation entities in the database. +func (_c *MaintenanceOperationCreateBulk) Save(ctx context.Context) ([]*MaintenanceOperation, error) { + if _c.err != nil { + return nil, _c.err + } + specs := make([]*sqlgraph.CreateSpec, len(_c.builders)) + nodes := make([]*MaintenanceOperation, len(_c.builders)) + mutators := make([]Mutator, len(_c.builders)) + for i := range _c.builders { + func(i int, root context.Context) { + builder := _c.builders[i] + builder.defaults() + var mut Mutator = MutateFunc(func(ctx context.Context, m Mutation) (Value, error) { + mutation, ok := m.(*MaintenanceOperationMutation) + if !ok { + return nil, fmt.Errorf("unexpected mutation type %T", m) + } + if err := builder.check(); err != nil { + return nil, err + } + builder.mutation = mutation + var err error + nodes[i], specs[i] = builder.createSpec() + if i < len(mutators)-1 { + _, err = mutators[i+1].Mutate(root, _c.builders[i+1].mutation) + } else { + spec := &sqlgraph.BatchCreateSpec{Nodes: specs} + spec.OnConflict = _c.conflict + // Invoke the actual operation on the latest mutation in the chain. + if err = sqlgraph.BatchCreate(ctx, _c.driver, spec); err != nil { + if sqlgraph.IsConstraintError(err) { + err = &ConstraintError{msg: err.Error(), wrap: err} + } + } + } + if err != nil { + return nil, err + } + mutation.id = &nodes[i].ID + mutation.done = true + return nodes[i], nil + }) + for i := len(builder.hooks) - 1; i >= 0; i-- { + mut = builder.hooks[i](mut) + } + mutators[i] = mut + }(i, ctx) + } + if len(mutators) > 0 { + if _, err := mutators[0].Mutate(ctx, _c.builders[0].mutation); err != nil { + return nil, err + } + } + return nodes, nil +} + +// SaveX is like Save, but panics if an error occurs. +func (_c *MaintenanceOperationCreateBulk) SaveX(ctx context.Context) []*MaintenanceOperation { + v, err := _c.Save(ctx) + if err != nil { + panic(err) + } + return v +} + +// Exec executes the query. +func (_c *MaintenanceOperationCreateBulk) Exec(ctx context.Context) error { + _, err := _c.Save(ctx) + return err +} + +// ExecX is like Exec, but panics if an error occurs. +func (_c *MaintenanceOperationCreateBulk) ExecX(ctx context.Context) { + if err := _c.Exec(ctx); err != nil { + panic(err) + } +} + +// OnConflict allows configuring the `ON CONFLICT` / `ON DUPLICATE KEY` clause +// of the `INSERT` statement. For example: +// +// client.MaintenanceOperation.CreateBulk(builders...). +// OnConflict( +// // Update the row with the new values +// // the was proposed for insertion. +// sql.ResolveWithNewValues(), +// ). +// // Override some of the fields with custom +// // update values. +// Update(func(u *ent.MaintenanceOperationUpsert) { +// SetKey(v+v). +// }). +// Exec(ctx) +func (_c *MaintenanceOperationCreateBulk) OnConflict(opts ...sql.ConflictOption) *MaintenanceOperationUpsertBulk { + _c.conflict = opts + return &MaintenanceOperationUpsertBulk{ + create: _c, + } +} + +// OnConflictColumns calls `OnConflict` and configures the columns +// as conflict target. Using this option is equivalent to using: +// +// client.MaintenanceOperation.Create(). +// OnConflict(sql.ConflictColumns(columns...)). +// Exec(ctx) +func (_c *MaintenanceOperationCreateBulk) OnConflictColumns(columns ...string) *MaintenanceOperationUpsertBulk { + _c.conflict = append(_c.conflict, sql.ConflictColumns(columns...)) + return &MaintenanceOperationUpsertBulk{ + create: _c, + } +} + +// MaintenanceOperationUpsertBulk is the builder for "upsert"-ing +// a bulk of MaintenanceOperation nodes. +type MaintenanceOperationUpsertBulk struct { + create *MaintenanceOperationCreateBulk +} + +// UpdateNewValues updates the mutable fields using the new values that +// were set on create. Using this option is equivalent to using: +// +// client.MaintenanceOperation.Create(). +// OnConflict( +// sql.ResolveWithNewValues(), +// sql.ResolveWith(func(u *sql.UpdateSet) { +// u.SetIgnore(maintenanceoperation.FieldID) +// }), +// ). +// Exec(ctx) +func (u *MaintenanceOperationUpsertBulk) UpdateNewValues() *MaintenanceOperationUpsertBulk { + u.create.conflict = append(u.create.conflict, sql.ResolveWithNewValues()) + u.create.conflict = append(u.create.conflict, sql.ResolveWith(func(s *sql.UpdateSet) { + for _, b := range u.create.builders { + if _, exists := b.mutation.ID(); exists { + s.SetIgnore(maintenanceoperation.FieldID) + } + if _, exists := b.mutation.Created(); exists { + s.SetIgnore(maintenanceoperation.FieldCreated) + } + } + })) + return u +} + +// Ignore sets each column to itself in case of conflict. +// Using this option is equivalent to using: +// +// client.MaintenanceOperation.Create(). +// OnConflict(sql.ResolveWithIgnore()). +// Exec(ctx) +func (u *MaintenanceOperationUpsertBulk) Ignore() *MaintenanceOperationUpsertBulk { + u.create.conflict = append(u.create.conflict, sql.ResolveWithIgnore()) + return u +} + +// DoNothing configures the conflict_action to `DO NOTHING`. +// Supported only by SQLite and PostgreSQL. +func (u *MaintenanceOperationUpsertBulk) DoNothing() *MaintenanceOperationUpsertBulk { + u.create.conflict = append(u.create.conflict, sql.DoNothing()) + return u +} + +// Update allows overriding fields `UPDATE` values. See the MaintenanceOperationCreateBulk.OnConflict +// documentation for more info. +func (u *MaintenanceOperationUpsertBulk) Update(set func(*MaintenanceOperationUpsert)) *MaintenanceOperationUpsertBulk { + u.create.conflict = append(u.create.conflict, sql.ResolveWith(func(update *sql.UpdateSet) { + set(&MaintenanceOperationUpsert{UpdateSet: update}) + })) + return u +} + +// SetKey sets the "key" field. +func (u *MaintenanceOperationUpsertBulk) SetKey(v string) *MaintenanceOperationUpsertBulk { + return u.Update(func(s *MaintenanceOperationUpsert) { + s.SetKey(v) + }) +} + +// UpdateKey sets the "key" field to the value that was provided on create. +func (u *MaintenanceOperationUpsertBulk) UpdateKey() *MaintenanceOperationUpsertBulk { + return u.Update(func(s *MaintenanceOperationUpsert) { + s.UpdateKey() + }) +} + +// SetTitle sets the "title" field. +func (u *MaintenanceOperationUpsertBulk) SetTitle(v string) *MaintenanceOperationUpsertBulk { + return u.Update(func(s *MaintenanceOperationUpsert) { + s.SetTitle(v) + }) +} + +// UpdateTitle sets the "title" field to the value that was provided on create. +func (u *MaintenanceOperationUpsertBulk) UpdateTitle() *MaintenanceOperationUpsertBulk { + return u.Update(func(s *MaintenanceOperationUpsert) { + s.UpdateTitle() + }) +} + +// SetDescription sets the "description" field. +func (u *MaintenanceOperationUpsertBulk) SetDescription(v string) *MaintenanceOperationUpsertBulk { + return u.Update(func(s *MaintenanceOperationUpsert) { + s.SetDescription(v) + }) +} + +// UpdateDescription sets the "description" field to the value that was provided on create. +func (u *MaintenanceOperationUpsertBulk) UpdateDescription() *MaintenanceOperationUpsertBulk { + return u.Update(func(s *MaintenanceOperationUpsert) { + s.UpdateDescription() + }) +} + +// SetCategory sets the "category" field. +func (u *MaintenanceOperationUpsertBulk) SetCategory(v string) *MaintenanceOperationUpsertBulk { + return u.Update(func(s *MaintenanceOperationUpsert) { + s.SetCategory(v) + }) +} + +// UpdateCategory sets the "category" field to the value that was provided on create. +func (u *MaintenanceOperationUpsertBulk) UpdateCategory() *MaintenanceOperationUpsertBulk { + return u.Update(func(s *MaintenanceOperationUpsert) { + s.UpdateCategory() + }) +} + +// SetStatus sets the "status" field. +func (u *MaintenanceOperationUpsertBulk) SetStatus(v string) *MaintenanceOperationUpsertBulk { + return u.Update(func(s *MaintenanceOperationUpsert) { + s.SetStatus(v) + }) +} + +// UpdateStatus sets the "status" field to the value that was provided on create. +func (u *MaintenanceOperationUpsertBulk) UpdateStatus() *MaintenanceOperationUpsertBulk { + return u.Update(func(s *MaintenanceOperationUpsert) { + s.UpdateStatus() + }) +} + +// SetStartedAt sets the "started_at" field. +func (u *MaintenanceOperationUpsertBulk) SetStartedAt(v time.Time) *MaintenanceOperationUpsertBulk { + return u.Update(func(s *MaintenanceOperationUpsert) { + s.SetStartedAt(v) + }) +} + +// UpdateStartedAt sets the "started_at" field to the value that was provided on create. +func (u *MaintenanceOperationUpsertBulk) UpdateStartedAt() *MaintenanceOperationUpsertBulk { + return u.Update(func(s *MaintenanceOperationUpsert) { + s.UpdateStartedAt() + }) +} + +// ClearStartedAt clears the value of the "started_at" field. +func (u *MaintenanceOperationUpsertBulk) ClearStartedAt() *MaintenanceOperationUpsertBulk { + return u.Update(func(s *MaintenanceOperationUpsert) { + s.ClearStartedAt() + }) +} + +// SetCompletedAt sets the "completed_at" field. +func (u *MaintenanceOperationUpsertBulk) SetCompletedAt(v time.Time) *MaintenanceOperationUpsertBulk { + return u.Update(func(s *MaintenanceOperationUpsert) { + s.SetCompletedAt(v) + }) +} + +// UpdateCompletedAt sets the "completed_at" field to the value that was provided on create. +func (u *MaintenanceOperationUpsertBulk) UpdateCompletedAt() *MaintenanceOperationUpsertBulk { + return u.Update(func(s *MaintenanceOperationUpsert) { + s.UpdateCompletedAt() + }) +} + +// ClearCompletedAt clears the value of the "completed_at" field. +func (u *MaintenanceOperationUpsertBulk) ClearCompletedAt() *MaintenanceOperationUpsertBulk { + return u.Update(func(s *MaintenanceOperationUpsert) { + s.ClearCompletedAt() + }) +} + +// SetStartedBy sets the "started_by" field. +func (u *MaintenanceOperationUpsertBulk) SetStartedBy(v string) *MaintenanceOperationUpsertBulk { + return u.Update(func(s *MaintenanceOperationUpsert) { + s.SetStartedBy(v) + }) +} + +// UpdateStartedBy sets the "started_by" field to the value that was provided on create. +func (u *MaintenanceOperationUpsertBulk) UpdateStartedBy() *MaintenanceOperationUpsertBulk { + return u.Update(func(s *MaintenanceOperationUpsert) { + s.UpdateStartedBy() + }) +} + +// ClearStartedBy clears the value of the "started_by" field. +func (u *MaintenanceOperationUpsertBulk) ClearStartedBy() *MaintenanceOperationUpsertBulk { + return u.Update(func(s *MaintenanceOperationUpsert) { + s.ClearStartedBy() + }) +} + +// SetResult sets the "result" field. +func (u *MaintenanceOperationUpsertBulk) SetResult(v string) *MaintenanceOperationUpsertBulk { + return u.Update(func(s *MaintenanceOperationUpsert) { + s.SetResult(v) + }) +} + +// UpdateResult sets the "result" field to the value that was provided on create. +func (u *MaintenanceOperationUpsertBulk) UpdateResult() *MaintenanceOperationUpsertBulk { + return u.Update(func(s *MaintenanceOperationUpsert) { + s.UpdateResult() + }) +} + +// ClearResult clears the value of the "result" field. +func (u *MaintenanceOperationUpsertBulk) ClearResult() *MaintenanceOperationUpsertBulk { + return u.Update(func(s *MaintenanceOperationUpsert) { + s.ClearResult() + }) +} + +// SetMetadata sets the "metadata" field. +func (u *MaintenanceOperationUpsertBulk) SetMetadata(v string) *MaintenanceOperationUpsertBulk { + return u.Update(func(s *MaintenanceOperationUpsert) { + s.SetMetadata(v) + }) +} + +// UpdateMetadata sets the "metadata" field to the value that was provided on create. +func (u *MaintenanceOperationUpsertBulk) UpdateMetadata() *MaintenanceOperationUpsertBulk { + return u.Update(func(s *MaintenanceOperationUpsert) { + s.UpdateMetadata() + }) +} + +// Exec executes the query. +func (u *MaintenanceOperationUpsertBulk) Exec(ctx context.Context) error { + if u.create.err != nil { + return u.create.err + } + for i, b := range u.create.builders { + if len(b.conflict) != 0 { + return fmt.Errorf("ent: OnConflict was set for builder %d. Set it on the MaintenanceOperationCreateBulk instead", i) + } + } + if len(u.create.conflict) == 0 { + return errors.New("ent: missing options for MaintenanceOperationCreateBulk.OnConflict") + } + return u.create.Exec(ctx) +} + +// ExecX is like Exec, but panics if an error occurs. +func (u *MaintenanceOperationUpsertBulk) ExecX(ctx context.Context) { + if err := u.create.Exec(ctx); err != nil { + panic(err) + } +} diff --git a/pkg/ent/maintenanceoperation_delete.go b/pkg/ent/maintenanceoperation_delete.go new file mode 100644 index 000000000..1e533174a --- /dev/null +++ b/pkg/ent/maintenanceoperation_delete.go @@ -0,0 +1,88 @@ +// Code generated by ent, DO NOT EDIT. + +package ent + +import ( + "context" + + "entgo.io/ent/dialect/sql" + "entgo.io/ent/dialect/sql/sqlgraph" + "entgo.io/ent/schema/field" + "github.com/GoogleCloudPlatform/scion/pkg/ent/maintenanceoperation" + "github.com/GoogleCloudPlatform/scion/pkg/ent/predicate" +) + +// MaintenanceOperationDelete is the builder for deleting a MaintenanceOperation entity. +type MaintenanceOperationDelete struct { + config + hooks []Hook + mutation *MaintenanceOperationMutation +} + +// Where appends a list predicates to the MaintenanceOperationDelete builder. +func (_d *MaintenanceOperationDelete) Where(ps ...predicate.MaintenanceOperation) *MaintenanceOperationDelete { + _d.mutation.Where(ps...) + return _d +} + +// Exec executes the deletion query and returns how many vertices were deleted. +func (_d *MaintenanceOperationDelete) Exec(ctx context.Context) (int, error) { + return withHooks(ctx, _d.sqlExec, _d.mutation, _d.hooks) +} + +// ExecX is like Exec, but panics if an error occurs. +func (_d *MaintenanceOperationDelete) ExecX(ctx context.Context) int { + n, err := _d.Exec(ctx) + if err != nil { + panic(err) + } + return n +} + +func (_d *MaintenanceOperationDelete) sqlExec(ctx context.Context) (int, error) { + _spec := sqlgraph.NewDeleteSpec(maintenanceoperation.Table, sqlgraph.NewFieldSpec(maintenanceoperation.FieldID, field.TypeUUID)) + if ps := _d.mutation.predicates; len(ps) > 0 { + _spec.Predicate = func(selector *sql.Selector) { + for i := range ps { + ps[i](selector) + } + } + } + affected, err := sqlgraph.DeleteNodes(ctx, _d.driver, _spec) + if err != nil && sqlgraph.IsConstraintError(err) { + err = &ConstraintError{msg: err.Error(), wrap: err} + } + _d.mutation.done = true + return affected, err +} + +// MaintenanceOperationDeleteOne is the builder for deleting a single MaintenanceOperation entity. +type MaintenanceOperationDeleteOne struct { + _d *MaintenanceOperationDelete +} + +// Where appends a list predicates to the MaintenanceOperationDelete builder. +func (_d *MaintenanceOperationDeleteOne) Where(ps ...predicate.MaintenanceOperation) *MaintenanceOperationDeleteOne { + _d._d.mutation.Where(ps...) + return _d +} + +// Exec executes the deletion query. +func (_d *MaintenanceOperationDeleteOne) Exec(ctx context.Context) error { + n, err := _d._d.Exec(ctx) + switch { + case err != nil: + return err + case n == 0: + return &NotFoundError{maintenanceoperation.Label} + default: + return nil + } +} + +// ExecX is like Exec, but panics if an error occurs. +func (_d *MaintenanceOperationDeleteOne) ExecX(ctx context.Context) { + if err := _d.Exec(ctx); err != nil { + panic(err) + } +} diff --git a/pkg/ent/maintenanceoperation_query.go b/pkg/ent/maintenanceoperation_query.go new file mode 100644 index 000000000..8e75f5966 --- /dev/null +++ b/pkg/ent/maintenanceoperation_query.go @@ -0,0 +1,565 @@ +// Code generated by ent, DO NOT EDIT. + +package ent + +import ( + "context" + "fmt" + "math" + + "entgo.io/ent" + "entgo.io/ent/dialect" + "entgo.io/ent/dialect/sql" + "entgo.io/ent/dialect/sql/sqlgraph" + "entgo.io/ent/schema/field" + "github.com/GoogleCloudPlatform/scion/pkg/ent/maintenanceoperation" + "github.com/GoogleCloudPlatform/scion/pkg/ent/predicate" + "github.com/google/uuid" +) + +// MaintenanceOperationQuery is the builder for querying MaintenanceOperation entities. +type MaintenanceOperationQuery struct { + config + ctx *QueryContext + order []maintenanceoperation.OrderOption + inters []Interceptor + predicates []predicate.MaintenanceOperation + modifiers []func(*sql.Selector) + // intermediate query (i.e. traversal path). + sql *sql.Selector + path func(context.Context) (*sql.Selector, error) +} + +// Where adds a new predicate for the MaintenanceOperationQuery builder. +func (_q *MaintenanceOperationQuery) Where(ps ...predicate.MaintenanceOperation) *MaintenanceOperationQuery { + _q.predicates = append(_q.predicates, ps...) + return _q +} + +// Limit the number of records to be returned by this query. +func (_q *MaintenanceOperationQuery) Limit(limit int) *MaintenanceOperationQuery { + _q.ctx.Limit = &limit + return _q +} + +// Offset to start from. +func (_q *MaintenanceOperationQuery) Offset(offset int) *MaintenanceOperationQuery { + _q.ctx.Offset = &offset + return _q +} + +// Unique configures the query builder to filter duplicate records on query. +// By default, unique is set to true, and can be disabled using this method. +func (_q *MaintenanceOperationQuery) Unique(unique bool) *MaintenanceOperationQuery { + _q.ctx.Unique = &unique + return _q +} + +// Order specifies how the records should be ordered. +func (_q *MaintenanceOperationQuery) Order(o ...maintenanceoperation.OrderOption) *MaintenanceOperationQuery { + _q.order = append(_q.order, o...) + return _q +} + +// First returns the first MaintenanceOperation entity from the query. +// Returns a *NotFoundError when no MaintenanceOperation was found. +func (_q *MaintenanceOperationQuery) First(ctx context.Context) (*MaintenanceOperation, error) { + nodes, err := _q.Limit(1).All(setContextOp(ctx, _q.ctx, ent.OpQueryFirst)) + if err != nil { + return nil, err + } + if len(nodes) == 0 { + return nil, &NotFoundError{maintenanceoperation.Label} + } + return nodes[0], nil +} + +// FirstX is like First, but panics if an error occurs. +func (_q *MaintenanceOperationQuery) FirstX(ctx context.Context) *MaintenanceOperation { + node, err := _q.First(ctx) + if err != nil && !IsNotFound(err) { + panic(err) + } + return node +} + +// FirstID returns the first MaintenanceOperation ID from the query. +// Returns a *NotFoundError when no MaintenanceOperation ID was found. +func (_q *MaintenanceOperationQuery) FirstID(ctx context.Context) (id uuid.UUID, err error) { + var ids []uuid.UUID + if ids, err = _q.Limit(1).IDs(setContextOp(ctx, _q.ctx, ent.OpQueryFirstID)); err != nil { + return + } + if len(ids) == 0 { + err = &NotFoundError{maintenanceoperation.Label} + return + } + return ids[0], nil +} + +// FirstIDX is like FirstID, but panics if an error occurs. +func (_q *MaintenanceOperationQuery) FirstIDX(ctx context.Context) uuid.UUID { + id, err := _q.FirstID(ctx) + if err != nil && !IsNotFound(err) { + panic(err) + } + return id +} + +// Only returns a single MaintenanceOperation entity found by the query, ensuring it only returns one. +// Returns a *NotSingularError when more than one MaintenanceOperation entity is found. +// Returns a *NotFoundError when no MaintenanceOperation entities are found. +func (_q *MaintenanceOperationQuery) Only(ctx context.Context) (*MaintenanceOperation, error) { + nodes, err := _q.Limit(2).All(setContextOp(ctx, _q.ctx, ent.OpQueryOnly)) + if err != nil { + return nil, err + } + switch len(nodes) { + case 1: + return nodes[0], nil + case 0: + return nil, &NotFoundError{maintenanceoperation.Label} + default: + return nil, &NotSingularError{maintenanceoperation.Label} + } +} + +// OnlyX is like Only, but panics if an error occurs. +func (_q *MaintenanceOperationQuery) OnlyX(ctx context.Context) *MaintenanceOperation { + node, err := _q.Only(ctx) + if err != nil { + panic(err) + } + return node +} + +// OnlyID is like Only, but returns the only MaintenanceOperation ID in the query. +// Returns a *NotSingularError when more than one MaintenanceOperation ID is found. +// Returns a *NotFoundError when no entities are found. +func (_q *MaintenanceOperationQuery) OnlyID(ctx context.Context) (id uuid.UUID, err error) { + var ids []uuid.UUID + if ids, err = _q.Limit(2).IDs(setContextOp(ctx, _q.ctx, ent.OpQueryOnlyID)); err != nil { + return + } + switch len(ids) { + case 1: + id = ids[0] + case 0: + err = &NotFoundError{maintenanceoperation.Label} + default: + err = &NotSingularError{maintenanceoperation.Label} + } + return +} + +// OnlyIDX is like OnlyID, but panics if an error occurs. +func (_q *MaintenanceOperationQuery) OnlyIDX(ctx context.Context) uuid.UUID { + id, err := _q.OnlyID(ctx) + if err != nil { + panic(err) + } + return id +} + +// All executes the query and returns a list of MaintenanceOperations. +func (_q *MaintenanceOperationQuery) All(ctx context.Context) ([]*MaintenanceOperation, error) { + ctx = setContextOp(ctx, _q.ctx, ent.OpQueryAll) + if err := _q.prepareQuery(ctx); err != nil { + return nil, err + } + qr := querierAll[[]*MaintenanceOperation, *MaintenanceOperationQuery]() + return withInterceptors[[]*MaintenanceOperation](ctx, _q, qr, _q.inters) +} + +// AllX is like All, but panics if an error occurs. +func (_q *MaintenanceOperationQuery) AllX(ctx context.Context) []*MaintenanceOperation { + nodes, err := _q.All(ctx) + if err != nil { + panic(err) + } + return nodes +} + +// IDs executes the query and returns a list of MaintenanceOperation IDs. +func (_q *MaintenanceOperationQuery) IDs(ctx context.Context) (ids []uuid.UUID, err error) { + if _q.ctx.Unique == nil && _q.path != nil { + _q.Unique(true) + } + ctx = setContextOp(ctx, _q.ctx, ent.OpQueryIDs) + if err = _q.Select(maintenanceoperation.FieldID).Scan(ctx, &ids); err != nil { + return nil, err + } + return ids, nil +} + +// IDsX is like IDs, but panics if an error occurs. +func (_q *MaintenanceOperationQuery) IDsX(ctx context.Context) []uuid.UUID { + ids, err := _q.IDs(ctx) + if err != nil { + panic(err) + } + return ids +} + +// Count returns the count of the given query. +func (_q *MaintenanceOperationQuery) Count(ctx context.Context) (int, error) { + ctx = setContextOp(ctx, _q.ctx, ent.OpQueryCount) + if err := _q.prepareQuery(ctx); err != nil { + return 0, err + } + return withInterceptors[int](ctx, _q, querierCount[*MaintenanceOperationQuery](), _q.inters) +} + +// CountX is like Count, but panics if an error occurs. +func (_q *MaintenanceOperationQuery) CountX(ctx context.Context) int { + count, err := _q.Count(ctx) + if err != nil { + panic(err) + } + return count +} + +// Exist returns true if the query has elements in the graph. +func (_q *MaintenanceOperationQuery) Exist(ctx context.Context) (bool, error) { + ctx = setContextOp(ctx, _q.ctx, ent.OpQueryExist) + switch _, err := _q.FirstID(ctx); { + case IsNotFound(err): + return false, nil + case err != nil: + return false, fmt.Errorf("ent: check existence: %w", err) + default: + return true, nil + } +} + +// ExistX is like Exist, but panics if an error occurs. +func (_q *MaintenanceOperationQuery) ExistX(ctx context.Context) bool { + exist, err := _q.Exist(ctx) + if err != nil { + panic(err) + } + return exist +} + +// Clone returns a duplicate of the MaintenanceOperationQuery builder, including all associated steps. It can be +// used to prepare common query builders and use them differently after the clone is made. +func (_q *MaintenanceOperationQuery) Clone() *MaintenanceOperationQuery { + if _q == nil { + return nil + } + return &MaintenanceOperationQuery{ + config: _q.config, + ctx: _q.ctx.Clone(), + order: append([]maintenanceoperation.OrderOption{}, _q.order...), + inters: append([]Interceptor{}, _q.inters...), + predicates: append([]predicate.MaintenanceOperation{}, _q.predicates...), + // clone intermediate query. + sql: _q.sql.Clone(), + path: _q.path, + } +} + +// GroupBy is used to group vertices by one or more fields/columns. +// It is often used with aggregate functions, like: count, max, mean, min, sum. +// +// Example: +// +// var v []struct { +// Key string `json:"key,omitempty"` +// Count int `json:"count,omitempty"` +// } +// +// client.MaintenanceOperation.Query(). +// GroupBy(maintenanceoperation.FieldKey). +// Aggregate(ent.Count()). +// Scan(ctx, &v) +func (_q *MaintenanceOperationQuery) GroupBy(field string, fields ...string) *MaintenanceOperationGroupBy { + _q.ctx.Fields = append([]string{field}, fields...) + grbuild := &MaintenanceOperationGroupBy{build: _q} + grbuild.flds = &_q.ctx.Fields + grbuild.label = maintenanceoperation.Label + grbuild.scan = grbuild.Scan + return grbuild +} + +// Select allows the selection one or more fields/columns for the given query, +// instead of selecting all fields in the entity. +// +// Example: +// +// var v []struct { +// Key string `json:"key,omitempty"` +// } +// +// client.MaintenanceOperation.Query(). +// Select(maintenanceoperation.FieldKey). +// Scan(ctx, &v) +func (_q *MaintenanceOperationQuery) Select(fields ...string) *MaintenanceOperationSelect { + _q.ctx.Fields = append(_q.ctx.Fields, fields...) + sbuild := &MaintenanceOperationSelect{MaintenanceOperationQuery: _q} + sbuild.label = maintenanceoperation.Label + sbuild.flds, sbuild.scan = &_q.ctx.Fields, sbuild.Scan + return sbuild +} + +// Aggregate returns a MaintenanceOperationSelect configured with the given aggregations. +func (_q *MaintenanceOperationQuery) Aggregate(fns ...AggregateFunc) *MaintenanceOperationSelect { + return _q.Select().Aggregate(fns...) +} + +func (_q *MaintenanceOperationQuery) prepareQuery(ctx context.Context) error { + for _, inter := range _q.inters { + if inter == nil { + return fmt.Errorf("ent: uninitialized interceptor (forgotten import ent/runtime?)") + } + if trv, ok := inter.(Traverser); ok { + if err := trv.Traverse(ctx, _q); err != nil { + return err + } + } + } + for _, f := range _q.ctx.Fields { + if !maintenanceoperation.ValidColumn(f) { + return &ValidationError{Name: f, err: fmt.Errorf("ent: invalid field %q for query", f)} + } + } + if _q.path != nil { + prev, err := _q.path(ctx) + if err != nil { + return err + } + _q.sql = prev + } + return nil +} + +func (_q *MaintenanceOperationQuery) sqlAll(ctx context.Context, hooks ...queryHook) ([]*MaintenanceOperation, error) { + var ( + nodes = []*MaintenanceOperation{} + _spec = _q.querySpec() + ) + _spec.ScanValues = func(columns []string) ([]any, error) { + return (*MaintenanceOperation).scanValues(nil, columns) + } + _spec.Assign = func(columns []string, values []any) error { + node := &MaintenanceOperation{config: _q.config} + nodes = append(nodes, node) + return node.assignValues(columns, values) + } + if len(_q.modifiers) > 0 { + _spec.Modifiers = _q.modifiers + } + for i := range hooks { + hooks[i](ctx, _spec) + } + if err := sqlgraph.QueryNodes(ctx, _q.driver, _spec); err != nil { + return nil, err + } + if len(nodes) == 0 { + return nodes, nil + } + return nodes, nil +} + +func (_q *MaintenanceOperationQuery) sqlCount(ctx context.Context) (int, error) { + _spec := _q.querySpec() + if len(_q.modifiers) > 0 { + _spec.Modifiers = _q.modifiers + } + _spec.Node.Columns = _q.ctx.Fields + if len(_q.ctx.Fields) > 0 { + _spec.Unique = _q.ctx.Unique != nil && *_q.ctx.Unique + } + return sqlgraph.CountNodes(ctx, _q.driver, _spec) +} + +func (_q *MaintenanceOperationQuery) querySpec() *sqlgraph.QuerySpec { + _spec := sqlgraph.NewQuerySpec(maintenanceoperation.Table, maintenanceoperation.Columns, sqlgraph.NewFieldSpec(maintenanceoperation.FieldID, field.TypeUUID)) + _spec.From = _q.sql + if unique := _q.ctx.Unique; unique != nil { + _spec.Unique = *unique + } else if _q.path != nil { + _spec.Unique = true + } + if fields := _q.ctx.Fields; len(fields) > 0 { + _spec.Node.Columns = make([]string, 0, len(fields)) + _spec.Node.Columns = append(_spec.Node.Columns, maintenanceoperation.FieldID) + for i := range fields { + if fields[i] != maintenanceoperation.FieldID { + _spec.Node.Columns = append(_spec.Node.Columns, fields[i]) + } + } + } + if ps := _q.predicates; len(ps) > 0 { + _spec.Predicate = func(selector *sql.Selector) { + for i := range ps { + ps[i](selector) + } + } + } + if limit := _q.ctx.Limit; limit != nil { + _spec.Limit = *limit + } + if offset := _q.ctx.Offset; offset != nil { + _spec.Offset = *offset + } + if ps := _q.order; len(ps) > 0 { + _spec.Order = func(selector *sql.Selector) { + for i := range ps { + ps[i](selector) + } + } + } + return _spec +} + +func (_q *MaintenanceOperationQuery) sqlQuery(ctx context.Context) *sql.Selector { + builder := sql.Dialect(_q.driver.Dialect()) + t1 := builder.Table(maintenanceoperation.Table) + columns := _q.ctx.Fields + if len(columns) == 0 { + columns = maintenanceoperation.Columns + } + selector := builder.Select(t1.Columns(columns...)...).From(t1) + if _q.sql != nil { + selector = _q.sql + selector.Select(selector.Columns(columns...)...) + } + if _q.ctx.Unique != nil && *_q.ctx.Unique { + selector.Distinct() + } + for _, m := range _q.modifiers { + m(selector) + } + for _, p := range _q.predicates { + p(selector) + } + for _, p := range _q.order { + p(selector) + } + if offset := _q.ctx.Offset; offset != nil { + // limit is mandatory for offset clause. We start + // with default value, and override it below if needed. + selector.Offset(*offset).Limit(math.MaxInt32) + } + if limit := _q.ctx.Limit; limit != nil { + selector.Limit(*limit) + } + return selector +} + +// ForUpdate locks the selected rows against concurrent updates, and prevent them from being +// updated, deleted or "selected ... for update" by other sessions, until the transaction is +// either committed or rolled-back. +func (_q *MaintenanceOperationQuery) ForUpdate(opts ...sql.LockOption) *MaintenanceOperationQuery { + if _q.driver.Dialect() == dialect.Postgres { + _q.Unique(false) + } + _q.modifiers = append(_q.modifiers, func(s *sql.Selector) { + s.ForUpdate(opts...) + }) + return _q +} + +// ForShare behaves similarly to ForUpdate, except that it acquires a shared mode lock +// on any rows that are read. Other sessions can read the rows, but cannot modify them +// until your transaction commits. +func (_q *MaintenanceOperationQuery) ForShare(opts ...sql.LockOption) *MaintenanceOperationQuery { + if _q.driver.Dialect() == dialect.Postgres { + _q.Unique(false) + } + _q.modifiers = append(_q.modifiers, func(s *sql.Selector) { + s.ForShare(opts...) + }) + return _q +} + +// MaintenanceOperationGroupBy is the group-by builder for MaintenanceOperation entities. +type MaintenanceOperationGroupBy struct { + selector + build *MaintenanceOperationQuery +} + +// Aggregate adds the given aggregation functions to the group-by query. +func (_g *MaintenanceOperationGroupBy) Aggregate(fns ...AggregateFunc) *MaintenanceOperationGroupBy { + _g.fns = append(_g.fns, fns...) + return _g +} + +// Scan applies the selector query and scans the result into the given value. +func (_g *MaintenanceOperationGroupBy) Scan(ctx context.Context, v any) error { + ctx = setContextOp(ctx, _g.build.ctx, ent.OpQueryGroupBy) + if err := _g.build.prepareQuery(ctx); err != nil { + return err + } + return scanWithInterceptors[*MaintenanceOperationQuery, *MaintenanceOperationGroupBy](ctx, _g.build, _g, _g.build.inters, v) +} + +func (_g *MaintenanceOperationGroupBy) sqlScan(ctx context.Context, root *MaintenanceOperationQuery, v any) error { + selector := root.sqlQuery(ctx).Select() + aggregation := make([]string, 0, len(_g.fns)) + for _, fn := range _g.fns { + aggregation = append(aggregation, fn(selector)) + } + if len(selector.SelectedColumns()) == 0 { + columns := make([]string, 0, len(*_g.flds)+len(_g.fns)) + for _, f := range *_g.flds { + columns = append(columns, selector.C(f)) + } + columns = append(columns, aggregation...) + selector.Select(columns...) + } + selector.GroupBy(selector.Columns(*_g.flds...)...) + if err := selector.Err(); err != nil { + return err + } + rows := &sql.Rows{} + query, args := selector.Query() + if err := _g.build.driver.Query(ctx, query, args, rows); err != nil { + return err + } + defer rows.Close() + return sql.ScanSlice(rows, v) +} + +// MaintenanceOperationSelect is the builder for selecting fields of MaintenanceOperation entities. +type MaintenanceOperationSelect struct { + *MaintenanceOperationQuery + selector +} + +// Aggregate adds the given aggregation functions to the selector query. +func (_s *MaintenanceOperationSelect) Aggregate(fns ...AggregateFunc) *MaintenanceOperationSelect { + _s.fns = append(_s.fns, fns...) + return _s +} + +// Scan applies the selector query and scans the result into the given value. +func (_s *MaintenanceOperationSelect) Scan(ctx context.Context, v any) error { + ctx = setContextOp(ctx, _s.ctx, ent.OpQuerySelect) + if err := _s.prepareQuery(ctx); err != nil { + return err + } + return scanWithInterceptors[*MaintenanceOperationQuery, *MaintenanceOperationSelect](ctx, _s.MaintenanceOperationQuery, _s, _s.inters, v) +} + +func (_s *MaintenanceOperationSelect) sqlScan(ctx context.Context, root *MaintenanceOperationQuery, v any) error { + selector := root.sqlQuery(ctx) + aggregation := make([]string, 0, len(_s.fns)) + for _, fn := range _s.fns { + aggregation = append(aggregation, fn(selector)) + } + switch n := len(*_s.selector.flds); { + case n == 0 && len(aggregation) > 0: + selector.Select(aggregation...) + case n != 0 && len(aggregation) > 0: + selector.AppendSelect(aggregation...) + } + rows := &sql.Rows{} + query, args := selector.Query() + if err := _s.driver.Query(ctx, query, args, rows); err != nil { + return err + } + defer rows.Close() + return sql.ScanSlice(rows, v) +} diff --git a/pkg/ent/maintenanceoperation_update.go b/pkg/ent/maintenanceoperation_update.go new file mode 100644 index 000000000..703ea8250 --- /dev/null +++ b/pkg/ent/maintenanceoperation_update.go @@ -0,0 +1,634 @@ +// Code generated by ent, DO NOT EDIT. + +package ent + +import ( + "context" + "errors" + "fmt" + "time" + + "entgo.io/ent/dialect/sql" + "entgo.io/ent/dialect/sql/sqlgraph" + "entgo.io/ent/schema/field" + "github.com/GoogleCloudPlatform/scion/pkg/ent/maintenanceoperation" + "github.com/GoogleCloudPlatform/scion/pkg/ent/predicate" +) + +// MaintenanceOperationUpdate is the builder for updating MaintenanceOperation entities. +type MaintenanceOperationUpdate struct { + config + hooks []Hook + mutation *MaintenanceOperationMutation +} + +// Where appends a list predicates to the MaintenanceOperationUpdate builder. +func (_u *MaintenanceOperationUpdate) Where(ps ...predicate.MaintenanceOperation) *MaintenanceOperationUpdate { + _u.mutation.Where(ps...) + return _u +} + +// SetKey sets the "key" field. +func (_u *MaintenanceOperationUpdate) SetKey(v string) *MaintenanceOperationUpdate { + _u.mutation.SetKey(v) + return _u +} + +// SetNillableKey sets the "key" field if the given value is not nil. +func (_u *MaintenanceOperationUpdate) SetNillableKey(v *string) *MaintenanceOperationUpdate { + if v != nil { + _u.SetKey(*v) + } + return _u +} + +// SetTitle sets the "title" field. +func (_u *MaintenanceOperationUpdate) SetTitle(v string) *MaintenanceOperationUpdate { + _u.mutation.SetTitle(v) + return _u +} + +// SetNillableTitle sets the "title" field if the given value is not nil. +func (_u *MaintenanceOperationUpdate) SetNillableTitle(v *string) *MaintenanceOperationUpdate { + if v != nil { + _u.SetTitle(*v) + } + return _u +} + +// SetDescription sets the "description" field. +func (_u *MaintenanceOperationUpdate) SetDescription(v string) *MaintenanceOperationUpdate { + _u.mutation.SetDescription(v) + return _u +} + +// SetNillableDescription sets the "description" field if the given value is not nil. +func (_u *MaintenanceOperationUpdate) SetNillableDescription(v *string) *MaintenanceOperationUpdate { + if v != nil { + _u.SetDescription(*v) + } + return _u +} + +// SetCategory sets the "category" field. +func (_u *MaintenanceOperationUpdate) SetCategory(v string) *MaintenanceOperationUpdate { + _u.mutation.SetCategory(v) + return _u +} + +// SetNillableCategory sets the "category" field if the given value is not nil. +func (_u *MaintenanceOperationUpdate) SetNillableCategory(v *string) *MaintenanceOperationUpdate { + if v != nil { + _u.SetCategory(*v) + } + return _u +} + +// SetStatus sets the "status" field. +func (_u *MaintenanceOperationUpdate) SetStatus(v string) *MaintenanceOperationUpdate { + _u.mutation.SetStatus(v) + return _u +} + +// SetNillableStatus sets the "status" field if the given value is not nil. +func (_u *MaintenanceOperationUpdate) SetNillableStatus(v *string) *MaintenanceOperationUpdate { + if v != nil { + _u.SetStatus(*v) + } + return _u +} + +// SetStartedAt sets the "started_at" field. +func (_u *MaintenanceOperationUpdate) SetStartedAt(v time.Time) *MaintenanceOperationUpdate { + _u.mutation.SetStartedAt(v) + return _u +} + +// SetNillableStartedAt sets the "started_at" field if the given value is not nil. +func (_u *MaintenanceOperationUpdate) SetNillableStartedAt(v *time.Time) *MaintenanceOperationUpdate { + if v != nil { + _u.SetStartedAt(*v) + } + return _u +} + +// ClearStartedAt clears the value of the "started_at" field. +func (_u *MaintenanceOperationUpdate) ClearStartedAt() *MaintenanceOperationUpdate { + _u.mutation.ClearStartedAt() + return _u +} + +// SetCompletedAt sets the "completed_at" field. +func (_u *MaintenanceOperationUpdate) SetCompletedAt(v time.Time) *MaintenanceOperationUpdate { + _u.mutation.SetCompletedAt(v) + return _u +} + +// SetNillableCompletedAt sets the "completed_at" field if the given value is not nil. +func (_u *MaintenanceOperationUpdate) SetNillableCompletedAt(v *time.Time) *MaintenanceOperationUpdate { + if v != nil { + _u.SetCompletedAt(*v) + } + return _u +} + +// ClearCompletedAt clears the value of the "completed_at" field. +func (_u *MaintenanceOperationUpdate) ClearCompletedAt() *MaintenanceOperationUpdate { + _u.mutation.ClearCompletedAt() + return _u +} + +// SetStartedBy sets the "started_by" field. +func (_u *MaintenanceOperationUpdate) SetStartedBy(v string) *MaintenanceOperationUpdate { + _u.mutation.SetStartedBy(v) + return _u +} + +// SetNillableStartedBy sets the "started_by" field if the given value is not nil. +func (_u *MaintenanceOperationUpdate) SetNillableStartedBy(v *string) *MaintenanceOperationUpdate { + if v != nil { + _u.SetStartedBy(*v) + } + return _u +} + +// ClearStartedBy clears the value of the "started_by" field. +func (_u *MaintenanceOperationUpdate) ClearStartedBy() *MaintenanceOperationUpdate { + _u.mutation.ClearStartedBy() + return _u +} + +// SetResult sets the "result" field. +func (_u *MaintenanceOperationUpdate) SetResult(v string) *MaintenanceOperationUpdate { + _u.mutation.SetResult(v) + return _u +} + +// SetNillableResult sets the "result" field if the given value is not nil. +func (_u *MaintenanceOperationUpdate) SetNillableResult(v *string) *MaintenanceOperationUpdate { + if v != nil { + _u.SetResult(*v) + } + return _u +} + +// ClearResult clears the value of the "result" field. +func (_u *MaintenanceOperationUpdate) ClearResult() *MaintenanceOperationUpdate { + _u.mutation.ClearResult() + return _u +} + +// SetMetadata sets the "metadata" field. +func (_u *MaintenanceOperationUpdate) SetMetadata(v string) *MaintenanceOperationUpdate { + _u.mutation.SetMetadata(v) + return _u +} + +// SetNillableMetadata sets the "metadata" field if the given value is not nil. +func (_u *MaintenanceOperationUpdate) SetNillableMetadata(v *string) *MaintenanceOperationUpdate { + if v != nil { + _u.SetMetadata(*v) + } + return _u +} + +// Mutation returns the MaintenanceOperationMutation object of the builder. +func (_u *MaintenanceOperationUpdate) Mutation() *MaintenanceOperationMutation { + return _u.mutation +} + +// Save executes the query and returns the number of nodes affected by the update operation. +func (_u *MaintenanceOperationUpdate) Save(ctx context.Context) (int, error) { + return withHooks(ctx, _u.sqlSave, _u.mutation, _u.hooks) +} + +// SaveX is like Save, but panics if an error occurs. +func (_u *MaintenanceOperationUpdate) SaveX(ctx context.Context) int { + affected, err := _u.Save(ctx) + if err != nil { + panic(err) + } + return affected +} + +// Exec executes the query. +func (_u *MaintenanceOperationUpdate) Exec(ctx context.Context) error { + _, err := _u.Save(ctx) + return err +} + +// ExecX is like Exec, but panics if an error occurs. +func (_u *MaintenanceOperationUpdate) ExecX(ctx context.Context) { + if err := _u.Exec(ctx); err != nil { + panic(err) + } +} + +// check runs all checks and user-defined validators on the builder. +func (_u *MaintenanceOperationUpdate) check() error { + if v, ok := _u.mutation.Key(); ok { + if err := maintenanceoperation.KeyValidator(v); err != nil { + return &ValidationError{Name: "key", err: fmt.Errorf(`ent: validator failed for field "MaintenanceOperation.key": %w`, err)} + } + } + if v, ok := _u.mutation.Title(); ok { + if err := maintenanceoperation.TitleValidator(v); err != nil { + return &ValidationError{Name: "title", err: fmt.Errorf(`ent: validator failed for field "MaintenanceOperation.title": %w`, err)} + } + } + if v, ok := _u.mutation.Category(); ok { + if err := maintenanceoperation.CategoryValidator(v); err != nil { + return &ValidationError{Name: "category", err: fmt.Errorf(`ent: validator failed for field "MaintenanceOperation.category": %w`, err)} + } + } + return nil +} + +func (_u *MaintenanceOperationUpdate) sqlSave(ctx context.Context) (_node int, err error) { + if err := _u.check(); err != nil { + return _node, err + } + _spec := sqlgraph.NewUpdateSpec(maintenanceoperation.Table, maintenanceoperation.Columns, sqlgraph.NewFieldSpec(maintenanceoperation.FieldID, field.TypeUUID)) + if ps := _u.mutation.predicates; len(ps) > 0 { + _spec.Predicate = func(selector *sql.Selector) { + for i := range ps { + ps[i](selector) + } + } + } + if value, ok := _u.mutation.Key(); ok { + _spec.SetField(maintenanceoperation.FieldKey, field.TypeString, value) + } + if value, ok := _u.mutation.Title(); ok { + _spec.SetField(maintenanceoperation.FieldTitle, field.TypeString, value) + } + if value, ok := _u.mutation.Description(); ok { + _spec.SetField(maintenanceoperation.FieldDescription, field.TypeString, value) + } + if value, ok := _u.mutation.Category(); ok { + _spec.SetField(maintenanceoperation.FieldCategory, field.TypeString, value) + } + if value, ok := _u.mutation.Status(); ok { + _spec.SetField(maintenanceoperation.FieldStatus, field.TypeString, value) + } + if value, ok := _u.mutation.StartedAt(); ok { + _spec.SetField(maintenanceoperation.FieldStartedAt, field.TypeTime, value) + } + if _u.mutation.StartedAtCleared() { + _spec.ClearField(maintenanceoperation.FieldStartedAt, field.TypeTime) + } + if value, ok := _u.mutation.CompletedAt(); ok { + _spec.SetField(maintenanceoperation.FieldCompletedAt, field.TypeTime, value) + } + if _u.mutation.CompletedAtCleared() { + _spec.ClearField(maintenanceoperation.FieldCompletedAt, field.TypeTime) + } + if value, ok := _u.mutation.StartedBy(); ok { + _spec.SetField(maintenanceoperation.FieldStartedBy, field.TypeString, value) + } + if _u.mutation.StartedByCleared() { + _spec.ClearField(maintenanceoperation.FieldStartedBy, field.TypeString) + } + if value, ok := _u.mutation.Result(); ok { + _spec.SetField(maintenanceoperation.FieldResult, field.TypeString, value) + } + if _u.mutation.ResultCleared() { + _spec.ClearField(maintenanceoperation.FieldResult, field.TypeString) + } + if value, ok := _u.mutation.Metadata(); ok { + _spec.SetField(maintenanceoperation.FieldMetadata, field.TypeString, value) + } + if _node, err = sqlgraph.UpdateNodes(ctx, _u.driver, _spec); err != nil { + if _, ok := err.(*sqlgraph.NotFoundError); ok { + err = &NotFoundError{maintenanceoperation.Label} + } else if sqlgraph.IsConstraintError(err) { + err = &ConstraintError{msg: err.Error(), wrap: err} + } + return 0, err + } + _u.mutation.done = true + return _node, nil +} + +// MaintenanceOperationUpdateOne is the builder for updating a single MaintenanceOperation entity. +type MaintenanceOperationUpdateOne struct { + config + fields []string + hooks []Hook + mutation *MaintenanceOperationMutation +} + +// SetKey sets the "key" field. +func (_u *MaintenanceOperationUpdateOne) SetKey(v string) *MaintenanceOperationUpdateOne { + _u.mutation.SetKey(v) + return _u +} + +// SetNillableKey sets the "key" field if the given value is not nil. +func (_u *MaintenanceOperationUpdateOne) SetNillableKey(v *string) *MaintenanceOperationUpdateOne { + if v != nil { + _u.SetKey(*v) + } + return _u +} + +// SetTitle sets the "title" field. +func (_u *MaintenanceOperationUpdateOne) SetTitle(v string) *MaintenanceOperationUpdateOne { + _u.mutation.SetTitle(v) + return _u +} + +// SetNillableTitle sets the "title" field if the given value is not nil. +func (_u *MaintenanceOperationUpdateOne) SetNillableTitle(v *string) *MaintenanceOperationUpdateOne { + if v != nil { + _u.SetTitle(*v) + } + return _u +} + +// SetDescription sets the "description" field. +func (_u *MaintenanceOperationUpdateOne) SetDescription(v string) *MaintenanceOperationUpdateOne { + _u.mutation.SetDescription(v) + return _u +} + +// SetNillableDescription sets the "description" field if the given value is not nil. +func (_u *MaintenanceOperationUpdateOne) SetNillableDescription(v *string) *MaintenanceOperationUpdateOne { + if v != nil { + _u.SetDescription(*v) + } + return _u +} + +// SetCategory sets the "category" field. +func (_u *MaintenanceOperationUpdateOne) SetCategory(v string) *MaintenanceOperationUpdateOne { + _u.mutation.SetCategory(v) + return _u +} + +// SetNillableCategory sets the "category" field if the given value is not nil. +func (_u *MaintenanceOperationUpdateOne) SetNillableCategory(v *string) *MaintenanceOperationUpdateOne { + if v != nil { + _u.SetCategory(*v) + } + return _u +} + +// SetStatus sets the "status" field. +func (_u *MaintenanceOperationUpdateOne) SetStatus(v string) *MaintenanceOperationUpdateOne { + _u.mutation.SetStatus(v) + return _u +} + +// SetNillableStatus sets the "status" field if the given value is not nil. +func (_u *MaintenanceOperationUpdateOne) SetNillableStatus(v *string) *MaintenanceOperationUpdateOne { + if v != nil { + _u.SetStatus(*v) + } + return _u +} + +// SetStartedAt sets the "started_at" field. +func (_u *MaintenanceOperationUpdateOne) SetStartedAt(v time.Time) *MaintenanceOperationUpdateOne { + _u.mutation.SetStartedAt(v) + return _u +} + +// SetNillableStartedAt sets the "started_at" field if the given value is not nil. +func (_u *MaintenanceOperationUpdateOne) SetNillableStartedAt(v *time.Time) *MaintenanceOperationUpdateOne { + if v != nil { + _u.SetStartedAt(*v) + } + return _u +} + +// ClearStartedAt clears the value of the "started_at" field. +func (_u *MaintenanceOperationUpdateOne) ClearStartedAt() *MaintenanceOperationUpdateOne { + _u.mutation.ClearStartedAt() + return _u +} + +// SetCompletedAt sets the "completed_at" field. +func (_u *MaintenanceOperationUpdateOne) SetCompletedAt(v time.Time) *MaintenanceOperationUpdateOne { + _u.mutation.SetCompletedAt(v) + return _u +} + +// SetNillableCompletedAt sets the "completed_at" field if the given value is not nil. +func (_u *MaintenanceOperationUpdateOne) SetNillableCompletedAt(v *time.Time) *MaintenanceOperationUpdateOne { + if v != nil { + _u.SetCompletedAt(*v) + } + return _u +} + +// ClearCompletedAt clears the value of the "completed_at" field. +func (_u *MaintenanceOperationUpdateOne) ClearCompletedAt() *MaintenanceOperationUpdateOne { + _u.mutation.ClearCompletedAt() + return _u +} + +// SetStartedBy sets the "started_by" field. +func (_u *MaintenanceOperationUpdateOne) SetStartedBy(v string) *MaintenanceOperationUpdateOne { + _u.mutation.SetStartedBy(v) + return _u +} + +// SetNillableStartedBy sets the "started_by" field if the given value is not nil. +func (_u *MaintenanceOperationUpdateOne) SetNillableStartedBy(v *string) *MaintenanceOperationUpdateOne { + if v != nil { + _u.SetStartedBy(*v) + } + return _u +} + +// ClearStartedBy clears the value of the "started_by" field. +func (_u *MaintenanceOperationUpdateOne) ClearStartedBy() *MaintenanceOperationUpdateOne { + _u.mutation.ClearStartedBy() + return _u +} + +// SetResult sets the "result" field. +func (_u *MaintenanceOperationUpdateOne) SetResult(v string) *MaintenanceOperationUpdateOne { + _u.mutation.SetResult(v) + return _u +} + +// SetNillableResult sets the "result" field if the given value is not nil. +func (_u *MaintenanceOperationUpdateOne) SetNillableResult(v *string) *MaintenanceOperationUpdateOne { + if v != nil { + _u.SetResult(*v) + } + return _u +} + +// ClearResult clears the value of the "result" field. +func (_u *MaintenanceOperationUpdateOne) ClearResult() *MaintenanceOperationUpdateOne { + _u.mutation.ClearResult() + return _u +} + +// SetMetadata sets the "metadata" field. +func (_u *MaintenanceOperationUpdateOne) SetMetadata(v string) *MaintenanceOperationUpdateOne { + _u.mutation.SetMetadata(v) + return _u +} + +// SetNillableMetadata sets the "metadata" field if the given value is not nil. +func (_u *MaintenanceOperationUpdateOne) SetNillableMetadata(v *string) *MaintenanceOperationUpdateOne { + if v != nil { + _u.SetMetadata(*v) + } + return _u +} + +// Mutation returns the MaintenanceOperationMutation object of the builder. +func (_u *MaintenanceOperationUpdateOne) Mutation() *MaintenanceOperationMutation { + return _u.mutation +} + +// Where appends a list predicates to the MaintenanceOperationUpdate builder. +func (_u *MaintenanceOperationUpdateOne) Where(ps ...predicate.MaintenanceOperation) *MaintenanceOperationUpdateOne { + _u.mutation.Where(ps...) + return _u +} + +// Select allows selecting one or more fields (columns) of the returned entity. +// The default is selecting all fields defined in the entity schema. +func (_u *MaintenanceOperationUpdateOne) Select(field string, fields ...string) *MaintenanceOperationUpdateOne { + _u.fields = append([]string{field}, fields...) + return _u +} + +// Save executes the query and returns the updated MaintenanceOperation entity. +func (_u *MaintenanceOperationUpdateOne) Save(ctx context.Context) (*MaintenanceOperation, error) { + return withHooks(ctx, _u.sqlSave, _u.mutation, _u.hooks) +} + +// SaveX is like Save, but panics if an error occurs. +func (_u *MaintenanceOperationUpdateOne) SaveX(ctx context.Context) *MaintenanceOperation { + node, err := _u.Save(ctx) + if err != nil { + panic(err) + } + return node +} + +// Exec executes the query on the entity. +func (_u *MaintenanceOperationUpdateOne) Exec(ctx context.Context) error { + _, err := _u.Save(ctx) + return err +} + +// ExecX is like Exec, but panics if an error occurs. +func (_u *MaintenanceOperationUpdateOne) ExecX(ctx context.Context) { + if err := _u.Exec(ctx); err != nil { + panic(err) + } +} + +// check runs all checks and user-defined validators on the builder. +func (_u *MaintenanceOperationUpdateOne) check() error { + if v, ok := _u.mutation.Key(); ok { + if err := maintenanceoperation.KeyValidator(v); err != nil { + return &ValidationError{Name: "key", err: fmt.Errorf(`ent: validator failed for field "MaintenanceOperation.key": %w`, err)} + } + } + if v, ok := _u.mutation.Title(); ok { + if err := maintenanceoperation.TitleValidator(v); err != nil { + return &ValidationError{Name: "title", err: fmt.Errorf(`ent: validator failed for field "MaintenanceOperation.title": %w`, err)} + } + } + if v, ok := _u.mutation.Category(); ok { + if err := maintenanceoperation.CategoryValidator(v); err != nil { + return &ValidationError{Name: "category", err: fmt.Errorf(`ent: validator failed for field "MaintenanceOperation.category": %w`, err)} + } + } + return nil +} + +func (_u *MaintenanceOperationUpdateOne) sqlSave(ctx context.Context) (_node *MaintenanceOperation, err error) { + if err := _u.check(); err != nil { + return _node, err + } + _spec := sqlgraph.NewUpdateSpec(maintenanceoperation.Table, maintenanceoperation.Columns, sqlgraph.NewFieldSpec(maintenanceoperation.FieldID, field.TypeUUID)) + id, ok := _u.mutation.ID() + if !ok { + return nil, &ValidationError{Name: "id", err: errors.New(`ent: missing "MaintenanceOperation.id" for update`)} + } + _spec.Node.ID.Value = id + if fields := _u.fields; len(fields) > 0 { + _spec.Node.Columns = make([]string, 0, len(fields)) + _spec.Node.Columns = append(_spec.Node.Columns, maintenanceoperation.FieldID) + for _, f := range fields { + if !maintenanceoperation.ValidColumn(f) { + return nil, &ValidationError{Name: f, err: fmt.Errorf("ent: invalid field %q for query", f)} + } + if f != maintenanceoperation.FieldID { + _spec.Node.Columns = append(_spec.Node.Columns, f) + } + } + } + if ps := _u.mutation.predicates; len(ps) > 0 { + _spec.Predicate = func(selector *sql.Selector) { + for i := range ps { + ps[i](selector) + } + } + } + if value, ok := _u.mutation.Key(); ok { + _spec.SetField(maintenanceoperation.FieldKey, field.TypeString, value) + } + if value, ok := _u.mutation.Title(); ok { + _spec.SetField(maintenanceoperation.FieldTitle, field.TypeString, value) + } + if value, ok := _u.mutation.Description(); ok { + _spec.SetField(maintenanceoperation.FieldDescription, field.TypeString, value) + } + if value, ok := _u.mutation.Category(); ok { + _spec.SetField(maintenanceoperation.FieldCategory, field.TypeString, value) + } + if value, ok := _u.mutation.Status(); ok { + _spec.SetField(maintenanceoperation.FieldStatus, field.TypeString, value) + } + if value, ok := _u.mutation.StartedAt(); ok { + _spec.SetField(maintenanceoperation.FieldStartedAt, field.TypeTime, value) + } + if _u.mutation.StartedAtCleared() { + _spec.ClearField(maintenanceoperation.FieldStartedAt, field.TypeTime) + } + if value, ok := _u.mutation.CompletedAt(); ok { + _spec.SetField(maintenanceoperation.FieldCompletedAt, field.TypeTime, value) + } + if _u.mutation.CompletedAtCleared() { + _spec.ClearField(maintenanceoperation.FieldCompletedAt, field.TypeTime) + } + if value, ok := _u.mutation.StartedBy(); ok { + _spec.SetField(maintenanceoperation.FieldStartedBy, field.TypeString, value) + } + if _u.mutation.StartedByCleared() { + _spec.ClearField(maintenanceoperation.FieldStartedBy, field.TypeString) + } + if value, ok := _u.mutation.Result(); ok { + _spec.SetField(maintenanceoperation.FieldResult, field.TypeString, value) + } + if _u.mutation.ResultCleared() { + _spec.ClearField(maintenanceoperation.FieldResult, field.TypeString) + } + if value, ok := _u.mutation.Metadata(); ok { + _spec.SetField(maintenanceoperation.FieldMetadata, field.TypeString, value) + } + _node = &MaintenanceOperation{config: _u.config} + _spec.Assign = _node.assignValues + _spec.ScanValues = _node.scanValues + if err = sqlgraph.UpdateNode(ctx, _u.driver, _spec); err != nil { + if _, ok := err.(*sqlgraph.NotFoundError); ok { + err = &NotFoundError{maintenanceoperation.Label} + } else if sqlgraph.IsConstraintError(err) { + err = &ConstraintError{msg: err.Error(), wrap: err} + } + return nil, err + } + _u.mutation.done = true + return _node, nil +} diff --git a/pkg/ent/maintenanceoperationrun.go b/pkg/ent/maintenanceoperationrun.go new file mode 100644 index 000000000..1c5240d7e --- /dev/null +++ b/pkg/ent/maintenanceoperationrun.go @@ -0,0 +1,176 @@ +// Code generated by ent, DO NOT EDIT. + +package ent + +import ( + "fmt" + "strings" + "time" + + "entgo.io/ent" + "entgo.io/ent/dialect/sql" + "github.com/GoogleCloudPlatform/scion/pkg/ent/maintenanceoperationrun" + "github.com/google/uuid" +) + +// MaintenanceOperationRun is the model entity for the MaintenanceOperationRun schema. +type MaintenanceOperationRun struct { + config `json:"-"` + // ID of the ent. + ID uuid.UUID `json:"id,omitempty"` + // OperationKey holds the value of the "operation_key" field. + OperationKey string `json:"operation_key,omitempty"` + // Status holds the value of the "status" field. + Status string `json:"status,omitempty"` + // StartedAt holds the value of the "started_at" field. + StartedAt time.Time `json:"started_at,omitempty"` + // CompletedAt holds the value of the "completed_at" field. + CompletedAt *time.Time `json:"completed_at,omitempty"` + // StartedBy holds the value of the "started_by" field. + StartedBy string `json:"started_by,omitempty"` + // Result holds the value of the "result" field. + Result string `json:"result,omitempty"` + // Log holds the value of the "log" field. + Log string `json:"log,omitempty"` + selectValues sql.SelectValues +} + +// scanValues returns the types for scanning values from sql.Rows. +func (*MaintenanceOperationRun) scanValues(columns []string) ([]any, error) { + values := make([]any, len(columns)) + for i := range columns { + switch columns[i] { + case maintenanceoperationrun.FieldOperationKey, maintenanceoperationrun.FieldStatus, maintenanceoperationrun.FieldStartedBy, maintenanceoperationrun.FieldResult, maintenanceoperationrun.FieldLog: + values[i] = new(sql.NullString) + case maintenanceoperationrun.FieldStartedAt, maintenanceoperationrun.FieldCompletedAt: + values[i] = new(sql.NullTime) + case maintenanceoperationrun.FieldID: + values[i] = new(uuid.UUID) + default: + values[i] = new(sql.UnknownType) + } + } + return values, nil +} + +// assignValues assigns the values that were returned from sql.Rows (after scanning) +// to the MaintenanceOperationRun fields. +func (_m *MaintenanceOperationRun) assignValues(columns []string, values []any) error { + if m, n := len(values), len(columns); m < n { + return fmt.Errorf("mismatch number of scan values: %d != %d", m, n) + } + for i := range columns { + switch columns[i] { + case maintenanceoperationrun.FieldID: + if value, ok := values[i].(*uuid.UUID); !ok { + return fmt.Errorf("unexpected type %T for field id", values[i]) + } else if value != nil { + _m.ID = *value + } + case maintenanceoperationrun.FieldOperationKey: + if value, ok := values[i].(*sql.NullString); !ok { + return fmt.Errorf("unexpected type %T for field operation_key", values[i]) + } else if value.Valid { + _m.OperationKey = value.String + } + case maintenanceoperationrun.FieldStatus: + if value, ok := values[i].(*sql.NullString); !ok { + return fmt.Errorf("unexpected type %T for field status", values[i]) + } else if value.Valid { + _m.Status = value.String + } + case maintenanceoperationrun.FieldStartedAt: + if value, ok := values[i].(*sql.NullTime); !ok { + return fmt.Errorf("unexpected type %T for field started_at", values[i]) + } else if value.Valid { + _m.StartedAt = value.Time + } + case maintenanceoperationrun.FieldCompletedAt: + if value, ok := values[i].(*sql.NullTime); !ok { + return fmt.Errorf("unexpected type %T for field completed_at", values[i]) + } else if value.Valid { + _m.CompletedAt = new(time.Time) + *_m.CompletedAt = value.Time + } + case maintenanceoperationrun.FieldStartedBy: + if value, ok := values[i].(*sql.NullString); !ok { + return fmt.Errorf("unexpected type %T for field started_by", values[i]) + } else if value.Valid { + _m.StartedBy = value.String + } + case maintenanceoperationrun.FieldResult: + if value, ok := values[i].(*sql.NullString); !ok { + return fmt.Errorf("unexpected type %T for field result", values[i]) + } else if value.Valid { + _m.Result = value.String + } + case maintenanceoperationrun.FieldLog: + if value, ok := values[i].(*sql.NullString); !ok { + return fmt.Errorf("unexpected type %T for field log", values[i]) + } else if value.Valid { + _m.Log = value.String + } + default: + _m.selectValues.Set(columns[i], values[i]) + } + } + return nil +} + +// Value returns the ent.Value that was dynamically selected and assigned to the MaintenanceOperationRun. +// This includes values selected through modifiers, order, etc. +func (_m *MaintenanceOperationRun) Value(name string) (ent.Value, error) { + return _m.selectValues.Get(name) +} + +// Update returns a builder for updating this MaintenanceOperationRun. +// Note that you need to call MaintenanceOperationRun.Unwrap() before calling this method if this MaintenanceOperationRun +// was returned from a transaction, and the transaction was committed or rolled back. +func (_m *MaintenanceOperationRun) Update() *MaintenanceOperationRunUpdateOne { + return NewMaintenanceOperationRunClient(_m.config).UpdateOne(_m) +} + +// Unwrap unwraps the MaintenanceOperationRun entity that was returned from a transaction after it was closed, +// so that all future queries will be executed through the driver which created the transaction. +func (_m *MaintenanceOperationRun) Unwrap() *MaintenanceOperationRun { + _tx, ok := _m.config.driver.(*txDriver) + if !ok { + panic("ent: MaintenanceOperationRun is not a transactional entity") + } + _m.config.driver = _tx.drv + return _m +} + +// String implements the fmt.Stringer. +func (_m *MaintenanceOperationRun) String() string { + var builder strings.Builder + builder.WriteString("MaintenanceOperationRun(") + builder.WriteString(fmt.Sprintf("id=%v, ", _m.ID)) + builder.WriteString("operation_key=") + builder.WriteString(_m.OperationKey) + builder.WriteString(", ") + builder.WriteString("status=") + builder.WriteString(_m.Status) + builder.WriteString(", ") + builder.WriteString("started_at=") + builder.WriteString(_m.StartedAt.Format(time.ANSIC)) + builder.WriteString(", ") + if v := _m.CompletedAt; v != nil { + builder.WriteString("completed_at=") + builder.WriteString(v.Format(time.ANSIC)) + } + builder.WriteString(", ") + builder.WriteString("started_by=") + builder.WriteString(_m.StartedBy) + builder.WriteString(", ") + builder.WriteString("result=") + builder.WriteString(_m.Result) + builder.WriteString(", ") + builder.WriteString("log=") + builder.WriteString(_m.Log) + builder.WriteByte(')') + return builder.String() +} + +// MaintenanceOperationRuns is a parsable slice of MaintenanceOperationRun. +type MaintenanceOperationRuns []*MaintenanceOperationRun diff --git a/pkg/ent/maintenanceoperationrun/maintenanceoperationrun.go b/pkg/ent/maintenanceoperationrun/maintenanceoperationrun.go new file mode 100644 index 000000000..bc3721459 --- /dev/null +++ b/pkg/ent/maintenanceoperationrun/maintenanceoperationrun.go @@ -0,0 +1,111 @@ +// Code generated by ent, DO NOT EDIT. + +package maintenanceoperationrun + +import ( + "time" + + "entgo.io/ent/dialect/sql" + "github.com/google/uuid" +) + +const ( + // Label holds the string label denoting the maintenanceoperationrun type in the database. + Label = "maintenance_operation_run" + // FieldID holds the string denoting the id field in the database. + FieldID = "id" + // FieldOperationKey holds the string denoting the operation_key field in the database. + FieldOperationKey = "operation_key" + // FieldStatus holds the string denoting the status field in the database. + FieldStatus = "status" + // FieldStartedAt holds the string denoting the started_at field in the database. + FieldStartedAt = "started_at" + // FieldCompletedAt holds the string denoting the completed_at field in the database. + FieldCompletedAt = "completed_at" + // FieldStartedBy holds the string denoting the started_by field in the database. + FieldStartedBy = "started_by" + // FieldResult holds the string denoting the result field in the database. + FieldResult = "result" + // FieldLog holds the string denoting the log field in the database. + FieldLog = "log" + // Table holds the table name of the maintenanceoperationrun in the database. + Table = "maintenance_operation_runs" +) + +// Columns holds all SQL columns for maintenanceoperationrun fields. +var Columns = []string{ + FieldID, + FieldOperationKey, + FieldStatus, + FieldStartedAt, + FieldCompletedAt, + FieldStartedBy, + FieldResult, + FieldLog, +} + +// ValidColumn reports if the column name is valid (part of the table columns). +func ValidColumn(column string) bool { + for i := range Columns { + if column == Columns[i] { + return true + } + } + return false +} + +var ( + // OperationKeyValidator is a validator for the "operation_key" field. It is called by the builders before save. + OperationKeyValidator func(string) error + // DefaultStatus holds the default value on creation for the "status" field. + DefaultStatus string + // DefaultStartedAt holds the default value on creation for the "started_at" field. + DefaultStartedAt func() time.Time + // DefaultLog holds the default value on creation for the "log" field. + DefaultLog string + // DefaultID holds the default value on creation for the "id" field. + DefaultID func() uuid.UUID +) + +// OrderOption defines the ordering options for the MaintenanceOperationRun queries. +type OrderOption func(*sql.Selector) + +// ByID orders the results by the id field. +func ByID(opts ...sql.OrderTermOption) OrderOption { + return sql.OrderByField(FieldID, opts...).ToFunc() +} + +// ByOperationKey orders the results by the operation_key field. +func ByOperationKey(opts ...sql.OrderTermOption) OrderOption { + return sql.OrderByField(FieldOperationKey, opts...).ToFunc() +} + +// ByStatus orders the results by the status field. +func ByStatus(opts ...sql.OrderTermOption) OrderOption { + return sql.OrderByField(FieldStatus, opts...).ToFunc() +} + +// ByStartedAt orders the results by the started_at field. +func ByStartedAt(opts ...sql.OrderTermOption) OrderOption { + return sql.OrderByField(FieldStartedAt, opts...).ToFunc() +} + +// ByCompletedAt orders the results by the completed_at field. +func ByCompletedAt(opts ...sql.OrderTermOption) OrderOption { + return sql.OrderByField(FieldCompletedAt, opts...).ToFunc() +} + +// ByStartedBy orders the results by the started_by field. +func ByStartedBy(opts ...sql.OrderTermOption) OrderOption { + return sql.OrderByField(FieldStartedBy, opts...).ToFunc() +} + +// ByResult orders the results by the result field. +func ByResult(opts ...sql.OrderTermOption) OrderOption { + return sql.OrderByField(FieldResult, opts...).ToFunc() +} + +// ByLog orders the results by the log field. +func ByLog(opts ...sql.OrderTermOption) OrderOption { + return sql.OrderByField(FieldLog, opts...).ToFunc() +} diff --git a/pkg/ent/maintenanceoperationrun/where.go b/pkg/ent/maintenanceoperationrun/where.go new file mode 100644 index 000000000..dcb586cb2 --- /dev/null +++ b/pkg/ent/maintenanceoperationrun/where.go @@ -0,0 +1,541 @@ +// Code generated by ent, DO NOT EDIT. + +package maintenanceoperationrun + +import ( + "time" + + "entgo.io/ent/dialect/sql" + "github.com/GoogleCloudPlatform/scion/pkg/ent/predicate" + "github.com/google/uuid" +) + +// ID filters vertices based on their ID field. +func ID(id uuid.UUID) predicate.MaintenanceOperationRun { + return predicate.MaintenanceOperationRun(sql.FieldEQ(FieldID, id)) +} + +// IDEQ applies the EQ predicate on the ID field. +func IDEQ(id uuid.UUID) predicate.MaintenanceOperationRun { + return predicate.MaintenanceOperationRun(sql.FieldEQ(FieldID, id)) +} + +// IDNEQ applies the NEQ predicate on the ID field. +func IDNEQ(id uuid.UUID) predicate.MaintenanceOperationRun { + return predicate.MaintenanceOperationRun(sql.FieldNEQ(FieldID, id)) +} + +// IDIn applies the In predicate on the ID field. +func IDIn(ids ...uuid.UUID) predicate.MaintenanceOperationRun { + return predicate.MaintenanceOperationRun(sql.FieldIn(FieldID, ids...)) +} + +// IDNotIn applies the NotIn predicate on the ID field. +func IDNotIn(ids ...uuid.UUID) predicate.MaintenanceOperationRun { + return predicate.MaintenanceOperationRun(sql.FieldNotIn(FieldID, ids...)) +} + +// IDGT applies the GT predicate on the ID field. +func IDGT(id uuid.UUID) predicate.MaintenanceOperationRun { + return predicate.MaintenanceOperationRun(sql.FieldGT(FieldID, id)) +} + +// IDGTE applies the GTE predicate on the ID field. +func IDGTE(id uuid.UUID) predicate.MaintenanceOperationRun { + return predicate.MaintenanceOperationRun(sql.FieldGTE(FieldID, id)) +} + +// IDLT applies the LT predicate on the ID field. +func IDLT(id uuid.UUID) predicate.MaintenanceOperationRun { + return predicate.MaintenanceOperationRun(sql.FieldLT(FieldID, id)) +} + +// IDLTE applies the LTE predicate on the ID field. +func IDLTE(id uuid.UUID) predicate.MaintenanceOperationRun { + return predicate.MaintenanceOperationRun(sql.FieldLTE(FieldID, id)) +} + +// OperationKey applies equality check predicate on the "operation_key" field. It's identical to OperationKeyEQ. +func OperationKey(v string) predicate.MaintenanceOperationRun { + return predicate.MaintenanceOperationRun(sql.FieldEQ(FieldOperationKey, v)) +} + +// Status applies equality check predicate on the "status" field. It's identical to StatusEQ. +func Status(v string) predicate.MaintenanceOperationRun { + return predicate.MaintenanceOperationRun(sql.FieldEQ(FieldStatus, v)) +} + +// StartedAt applies equality check predicate on the "started_at" field. It's identical to StartedAtEQ. +func StartedAt(v time.Time) predicate.MaintenanceOperationRun { + return predicate.MaintenanceOperationRun(sql.FieldEQ(FieldStartedAt, v)) +} + +// CompletedAt applies equality check predicate on the "completed_at" field. It's identical to CompletedAtEQ. +func CompletedAt(v time.Time) predicate.MaintenanceOperationRun { + return predicate.MaintenanceOperationRun(sql.FieldEQ(FieldCompletedAt, v)) +} + +// StartedBy applies equality check predicate on the "started_by" field. It's identical to StartedByEQ. +func StartedBy(v string) predicate.MaintenanceOperationRun { + return predicate.MaintenanceOperationRun(sql.FieldEQ(FieldStartedBy, v)) +} + +// Result applies equality check predicate on the "result" field. It's identical to ResultEQ. +func Result(v string) predicate.MaintenanceOperationRun { + return predicate.MaintenanceOperationRun(sql.FieldEQ(FieldResult, v)) +} + +// Log applies equality check predicate on the "log" field. It's identical to LogEQ. +func Log(v string) predicate.MaintenanceOperationRun { + return predicate.MaintenanceOperationRun(sql.FieldEQ(FieldLog, v)) +} + +// OperationKeyEQ applies the EQ predicate on the "operation_key" field. +func OperationKeyEQ(v string) predicate.MaintenanceOperationRun { + return predicate.MaintenanceOperationRun(sql.FieldEQ(FieldOperationKey, v)) +} + +// OperationKeyNEQ applies the NEQ predicate on the "operation_key" field. +func OperationKeyNEQ(v string) predicate.MaintenanceOperationRun { + return predicate.MaintenanceOperationRun(sql.FieldNEQ(FieldOperationKey, v)) +} + +// OperationKeyIn applies the In predicate on the "operation_key" field. +func OperationKeyIn(vs ...string) predicate.MaintenanceOperationRun { + return predicate.MaintenanceOperationRun(sql.FieldIn(FieldOperationKey, vs...)) +} + +// OperationKeyNotIn applies the NotIn predicate on the "operation_key" field. +func OperationKeyNotIn(vs ...string) predicate.MaintenanceOperationRun { + return predicate.MaintenanceOperationRun(sql.FieldNotIn(FieldOperationKey, vs...)) +} + +// OperationKeyGT applies the GT predicate on the "operation_key" field. +func OperationKeyGT(v string) predicate.MaintenanceOperationRun { + return predicate.MaintenanceOperationRun(sql.FieldGT(FieldOperationKey, v)) +} + +// OperationKeyGTE applies the GTE predicate on the "operation_key" field. +func OperationKeyGTE(v string) predicate.MaintenanceOperationRun { + return predicate.MaintenanceOperationRun(sql.FieldGTE(FieldOperationKey, v)) +} + +// OperationKeyLT applies the LT predicate on the "operation_key" field. +func OperationKeyLT(v string) predicate.MaintenanceOperationRun { + return predicate.MaintenanceOperationRun(sql.FieldLT(FieldOperationKey, v)) +} + +// OperationKeyLTE applies the LTE predicate on the "operation_key" field. +func OperationKeyLTE(v string) predicate.MaintenanceOperationRun { + return predicate.MaintenanceOperationRun(sql.FieldLTE(FieldOperationKey, v)) +} + +// OperationKeyContains applies the Contains predicate on the "operation_key" field. +func OperationKeyContains(v string) predicate.MaintenanceOperationRun { + return predicate.MaintenanceOperationRun(sql.FieldContains(FieldOperationKey, v)) +} + +// OperationKeyHasPrefix applies the HasPrefix predicate on the "operation_key" field. +func OperationKeyHasPrefix(v string) predicate.MaintenanceOperationRun { + return predicate.MaintenanceOperationRun(sql.FieldHasPrefix(FieldOperationKey, v)) +} + +// OperationKeyHasSuffix applies the HasSuffix predicate on the "operation_key" field. +func OperationKeyHasSuffix(v string) predicate.MaintenanceOperationRun { + return predicate.MaintenanceOperationRun(sql.FieldHasSuffix(FieldOperationKey, v)) +} + +// OperationKeyEqualFold applies the EqualFold predicate on the "operation_key" field. +func OperationKeyEqualFold(v string) predicate.MaintenanceOperationRun { + return predicate.MaintenanceOperationRun(sql.FieldEqualFold(FieldOperationKey, v)) +} + +// OperationKeyContainsFold applies the ContainsFold predicate on the "operation_key" field. +func OperationKeyContainsFold(v string) predicate.MaintenanceOperationRun { + return predicate.MaintenanceOperationRun(sql.FieldContainsFold(FieldOperationKey, v)) +} + +// StatusEQ applies the EQ predicate on the "status" field. +func StatusEQ(v string) predicate.MaintenanceOperationRun { + return predicate.MaintenanceOperationRun(sql.FieldEQ(FieldStatus, v)) +} + +// StatusNEQ applies the NEQ predicate on the "status" field. +func StatusNEQ(v string) predicate.MaintenanceOperationRun { + return predicate.MaintenanceOperationRun(sql.FieldNEQ(FieldStatus, v)) +} + +// StatusIn applies the In predicate on the "status" field. +func StatusIn(vs ...string) predicate.MaintenanceOperationRun { + return predicate.MaintenanceOperationRun(sql.FieldIn(FieldStatus, vs...)) +} + +// StatusNotIn applies the NotIn predicate on the "status" field. +func StatusNotIn(vs ...string) predicate.MaintenanceOperationRun { + return predicate.MaintenanceOperationRun(sql.FieldNotIn(FieldStatus, vs...)) +} + +// StatusGT applies the GT predicate on the "status" field. +func StatusGT(v string) predicate.MaintenanceOperationRun { + return predicate.MaintenanceOperationRun(sql.FieldGT(FieldStatus, v)) +} + +// StatusGTE applies the GTE predicate on the "status" field. +func StatusGTE(v string) predicate.MaintenanceOperationRun { + return predicate.MaintenanceOperationRun(sql.FieldGTE(FieldStatus, v)) +} + +// StatusLT applies the LT predicate on the "status" field. +func StatusLT(v string) predicate.MaintenanceOperationRun { + return predicate.MaintenanceOperationRun(sql.FieldLT(FieldStatus, v)) +} + +// StatusLTE applies the LTE predicate on the "status" field. +func StatusLTE(v string) predicate.MaintenanceOperationRun { + return predicate.MaintenanceOperationRun(sql.FieldLTE(FieldStatus, v)) +} + +// StatusContains applies the Contains predicate on the "status" field. +func StatusContains(v string) predicate.MaintenanceOperationRun { + return predicate.MaintenanceOperationRun(sql.FieldContains(FieldStatus, v)) +} + +// StatusHasPrefix applies the HasPrefix predicate on the "status" field. +func StatusHasPrefix(v string) predicate.MaintenanceOperationRun { + return predicate.MaintenanceOperationRun(sql.FieldHasPrefix(FieldStatus, v)) +} + +// StatusHasSuffix applies the HasSuffix predicate on the "status" field. +func StatusHasSuffix(v string) predicate.MaintenanceOperationRun { + return predicate.MaintenanceOperationRun(sql.FieldHasSuffix(FieldStatus, v)) +} + +// StatusEqualFold applies the EqualFold predicate on the "status" field. +func StatusEqualFold(v string) predicate.MaintenanceOperationRun { + return predicate.MaintenanceOperationRun(sql.FieldEqualFold(FieldStatus, v)) +} + +// StatusContainsFold applies the ContainsFold predicate on the "status" field. +func StatusContainsFold(v string) predicate.MaintenanceOperationRun { + return predicate.MaintenanceOperationRun(sql.FieldContainsFold(FieldStatus, v)) +} + +// StartedAtEQ applies the EQ predicate on the "started_at" field. +func StartedAtEQ(v time.Time) predicate.MaintenanceOperationRun { + return predicate.MaintenanceOperationRun(sql.FieldEQ(FieldStartedAt, v)) +} + +// StartedAtNEQ applies the NEQ predicate on the "started_at" field. +func StartedAtNEQ(v time.Time) predicate.MaintenanceOperationRun { + return predicate.MaintenanceOperationRun(sql.FieldNEQ(FieldStartedAt, v)) +} + +// StartedAtIn applies the In predicate on the "started_at" field. +func StartedAtIn(vs ...time.Time) predicate.MaintenanceOperationRun { + return predicate.MaintenanceOperationRun(sql.FieldIn(FieldStartedAt, vs...)) +} + +// StartedAtNotIn applies the NotIn predicate on the "started_at" field. +func StartedAtNotIn(vs ...time.Time) predicate.MaintenanceOperationRun { + return predicate.MaintenanceOperationRun(sql.FieldNotIn(FieldStartedAt, vs...)) +} + +// StartedAtGT applies the GT predicate on the "started_at" field. +func StartedAtGT(v time.Time) predicate.MaintenanceOperationRun { + return predicate.MaintenanceOperationRun(sql.FieldGT(FieldStartedAt, v)) +} + +// StartedAtGTE applies the GTE predicate on the "started_at" field. +func StartedAtGTE(v time.Time) predicate.MaintenanceOperationRun { + return predicate.MaintenanceOperationRun(sql.FieldGTE(FieldStartedAt, v)) +} + +// StartedAtLT applies the LT predicate on the "started_at" field. +func StartedAtLT(v time.Time) predicate.MaintenanceOperationRun { + return predicate.MaintenanceOperationRun(sql.FieldLT(FieldStartedAt, v)) +} + +// StartedAtLTE applies the LTE predicate on the "started_at" field. +func StartedAtLTE(v time.Time) predicate.MaintenanceOperationRun { + return predicate.MaintenanceOperationRun(sql.FieldLTE(FieldStartedAt, v)) +} + +// CompletedAtEQ applies the EQ predicate on the "completed_at" field. +func CompletedAtEQ(v time.Time) predicate.MaintenanceOperationRun { + return predicate.MaintenanceOperationRun(sql.FieldEQ(FieldCompletedAt, v)) +} + +// CompletedAtNEQ applies the NEQ predicate on the "completed_at" field. +func CompletedAtNEQ(v time.Time) predicate.MaintenanceOperationRun { + return predicate.MaintenanceOperationRun(sql.FieldNEQ(FieldCompletedAt, v)) +} + +// CompletedAtIn applies the In predicate on the "completed_at" field. +func CompletedAtIn(vs ...time.Time) predicate.MaintenanceOperationRun { + return predicate.MaintenanceOperationRun(sql.FieldIn(FieldCompletedAt, vs...)) +} + +// CompletedAtNotIn applies the NotIn predicate on the "completed_at" field. +func CompletedAtNotIn(vs ...time.Time) predicate.MaintenanceOperationRun { + return predicate.MaintenanceOperationRun(sql.FieldNotIn(FieldCompletedAt, vs...)) +} + +// CompletedAtGT applies the GT predicate on the "completed_at" field. +func CompletedAtGT(v time.Time) predicate.MaintenanceOperationRun { + return predicate.MaintenanceOperationRun(sql.FieldGT(FieldCompletedAt, v)) +} + +// CompletedAtGTE applies the GTE predicate on the "completed_at" field. +func CompletedAtGTE(v time.Time) predicate.MaintenanceOperationRun { + return predicate.MaintenanceOperationRun(sql.FieldGTE(FieldCompletedAt, v)) +} + +// CompletedAtLT applies the LT predicate on the "completed_at" field. +func CompletedAtLT(v time.Time) predicate.MaintenanceOperationRun { + return predicate.MaintenanceOperationRun(sql.FieldLT(FieldCompletedAt, v)) +} + +// CompletedAtLTE applies the LTE predicate on the "completed_at" field. +func CompletedAtLTE(v time.Time) predicate.MaintenanceOperationRun { + return predicate.MaintenanceOperationRun(sql.FieldLTE(FieldCompletedAt, v)) +} + +// CompletedAtIsNil applies the IsNil predicate on the "completed_at" field. +func CompletedAtIsNil() predicate.MaintenanceOperationRun { + return predicate.MaintenanceOperationRun(sql.FieldIsNull(FieldCompletedAt)) +} + +// CompletedAtNotNil applies the NotNil predicate on the "completed_at" field. +func CompletedAtNotNil() predicate.MaintenanceOperationRun { + return predicate.MaintenanceOperationRun(sql.FieldNotNull(FieldCompletedAt)) +} + +// StartedByEQ applies the EQ predicate on the "started_by" field. +func StartedByEQ(v string) predicate.MaintenanceOperationRun { + return predicate.MaintenanceOperationRun(sql.FieldEQ(FieldStartedBy, v)) +} + +// StartedByNEQ applies the NEQ predicate on the "started_by" field. +func StartedByNEQ(v string) predicate.MaintenanceOperationRun { + return predicate.MaintenanceOperationRun(sql.FieldNEQ(FieldStartedBy, v)) +} + +// StartedByIn applies the In predicate on the "started_by" field. +func StartedByIn(vs ...string) predicate.MaintenanceOperationRun { + return predicate.MaintenanceOperationRun(sql.FieldIn(FieldStartedBy, vs...)) +} + +// StartedByNotIn applies the NotIn predicate on the "started_by" field. +func StartedByNotIn(vs ...string) predicate.MaintenanceOperationRun { + return predicate.MaintenanceOperationRun(sql.FieldNotIn(FieldStartedBy, vs...)) +} + +// StartedByGT applies the GT predicate on the "started_by" field. +func StartedByGT(v string) predicate.MaintenanceOperationRun { + return predicate.MaintenanceOperationRun(sql.FieldGT(FieldStartedBy, v)) +} + +// StartedByGTE applies the GTE predicate on the "started_by" field. +func StartedByGTE(v string) predicate.MaintenanceOperationRun { + return predicate.MaintenanceOperationRun(sql.FieldGTE(FieldStartedBy, v)) +} + +// StartedByLT applies the LT predicate on the "started_by" field. +func StartedByLT(v string) predicate.MaintenanceOperationRun { + return predicate.MaintenanceOperationRun(sql.FieldLT(FieldStartedBy, v)) +} + +// StartedByLTE applies the LTE predicate on the "started_by" field. +func StartedByLTE(v string) predicate.MaintenanceOperationRun { + return predicate.MaintenanceOperationRun(sql.FieldLTE(FieldStartedBy, v)) +} + +// StartedByContains applies the Contains predicate on the "started_by" field. +func StartedByContains(v string) predicate.MaintenanceOperationRun { + return predicate.MaintenanceOperationRun(sql.FieldContains(FieldStartedBy, v)) +} + +// StartedByHasPrefix applies the HasPrefix predicate on the "started_by" field. +func StartedByHasPrefix(v string) predicate.MaintenanceOperationRun { + return predicate.MaintenanceOperationRun(sql.FieldHasPrefix(FieldStartedBy, v)) +} + +// StartedByHasSuffix applies the HasSuffix predicate on the "started_by" field. +func StartedByHasSuffix(v string) predicate.MaintenanceOperationRun { + return predicate.MaintenanceOperationRun(sql.FieldHasSuffix(FieldStartedBy, v)) +} + +// StartedByIsNil applies the IsNil predicate on the "started_by" field. +func StartedByIsNil() predicate.MaintenanceOperationRun { + return predicate.MaintenanceOperationRun(sql.FieldIsNull(FieldStartedBy)) +} + +// StartedByNotNil applies the NotNil predicate on the "started_by" field. +func StartedByNotNil() predicate.MaintenanceOperationRun { + return predicate.MaintenanceOperationRun(sql.FieldNotNull(FieldStartedBy)) +} + +// StartedByEqualFold applies the EqualFold predicate on the "started_by" field. +func StartedByEqualFold(v string) predicate.MaintenanceOperationRun { + return predicate.MaintenanceOperationRun(sql.FieldEqualFold(FieldStartedBy, v)) +} + +// StartedByContainsFold applies the ContainsFold predicate on the "started_by" field. +func StartedByContainsFold(v string) predicate.MaintenanceOperationRun { + return predicate.MaintenanceOperationRun(sql.FieldContainsFold(FieldStartedBy, v)) +} + +// ResultEQ applies the EQ predicate on the "result" field. +func ResultEQ(v string) predicate.MaintenanceOperationRun { + return predicate.MaintenanceOperationRun(sql.FieldEQ(FieldResult, v)) +} + +// ResultNEQ applies the NEQ predicate on the "result" field. +func ResultNEQ(v string) predicate.MaintenanceOperationRun { + return predicate.MaintenanceOperationRun(sql.FieldNEQ(FieldResult, v)) +} + +// ResultIn applies the In predicate on the "result" field. +func ResultIn(vs ...string) predicate.MaintenanceOperationRun { + return predicate.MaintenanceOperationRun(sql.FieldIn(FieldResult, vs...)) +} + +// ResultNotIn applies the NotIn predicate on the "result" field. +func ResultNotIn(vs ...string) predicate.MaintenanceOperationRun { + return predicate.MaintenanceOperationRun(sql.FieldNotIn(FieldResult, vs...)) +} + +// ResultGT applies the GT predicate on the "result" field. +func ResultGT(v string) predicate.MaintenanceOperationRun { + return predicate.MaintenanceOperationRun(sql.FieldGT(FieldResult, v)) +} + +// ResultGTE applies the GTE predicate on the "result" field. +func ResultGTE(v string) predicate.MaintenanceOperationRun { + return predicate.MaintenanceOperationRun(sql.FieldGTE(FieldResult, v)) +} + +// ResultLT applies the LT predicate on the "result" field. +func ResultLT(v string) predicate.MaintenanceOperationRun { + return predicate.MaintenanceOperationRun(sql.FieldLT(FieldResult, v)) +} + +// ResultLTE applies the LTE predicate on the "result" field. +func ResultLTE(v string) predicate.MaintenanceOperationRun { + return predicate.MaintenanceOperationRun(sql.FieldLTE(FieldResult, v)) +} + +// ResultContains applies the Contains predicate on the "result" field. +func ResultContains(v string) predicate.MaintenanceOperationRun { + return predicate.MaintenanceOperationRun(sql.FieldContains(FieldResult, v)) +} + +// ResultHasPrefix applies the HasPrefix predicate on the "result" field. +func ResultHasPrefix(v string) predicate.MaintenanceOperationRun { + return predicate.MaintenanceOperationRun(sql.FieldHasPrefix(FieldResult, v)) +} + +// ResultHasSuffix applies the HasSuffix predicate on the "result" field. +func ResultHasSuffix(v string) predicate.MaintenanceOperationRun { + return predicate.MaintenanceOperationRun(sql.FieldHasSuffix(FieldResult, v)) +} + +// ResultIsNil applies the IsNil predicate on the "result" field. +func ResultIsNil() predicate.MaintenanceOperationRun { + return predicate.MaintenanceOperationRun(sql.FieldIsNull(FieldResult)) +} + +// ResultNotNil applies the NotNil predicate on the "result" field. +func ResultNotNil() predicate.MaintenanceOperationRun { + return predicate.MaintenanceOperationRun(sql.FieldNotNull(FieldResult)) +} + +// ResultEqualFold applies the EqualFold predicate on the "result" field. +func ResultEqualFold(v string) predicate.MaintenanceOperationRun { + return predicate.MaintenanceOperationRun(sql.FieldEqualFold(FieldResult, v)) +} + +// ResultContainsFold applies the ContainsFold predicate on the "result" field. +func ResultContainsFold(v string) predicate.MaintenanceOperationRun { + return predicate.MaintenanceOperationRun(sql.FieldContainsFold(FieldResult, v)) +} + +// LogEQ applies the EQ predicate on the "log" field. +func LogEQ(v string) predicate.MaintenanceOperationRun { + return predicate.MaintenanceOperationRun(sql.FieldEQ(FieldLog, v)) +} + +// LogNEQ applies the NEQ predicate on the "log" field. +func LogNEQ(v string) predicate.MaintenanceOperationRun { + return predicate.MaintenanceOperationRun(sql.FieldNEQ(FieldLog, v)) +} + +// LogIn applies the In predicate on the "log" field. +func LogIn(vs ...string) predicate.MaintenanceOperationRun { + return predicate.MaintenanceOperationRun(sql.FieldIn(FieldLog, vs...)) +} + +// LogNotIn applies the NotIn predicate on the "log" field. +func LogNotIn(vs ...string) predicate.MaintenanceOperationRun { + return predicate.MaintenanceOperationRun(sql.FieldNotIn(FieldLog, vs...)) +} + +// LogGT applies the GT predicate on the "log" field. +func LogGT(v string) predicate.MaintenanceOperationRun { + return predicate.MaintenanceOperationRun(sql.FieldGT(FieldLog, v)) +} + +// LogGTE applies the GTE predicate on the "log" field. +func LogGTE(v string) predicate.MaintenanceOperationRun { + return predicate.MaintenanceOperationRun(sql.FieldGTE(FieldLog, v)) +} + +// LogLT applies the LT predicate on the "log" field. +func LogLT(v string) predicate.MaintenanceOperationRun { + return predicate.MaintenanceOperationRun(sql.FieldLT(FieldLog, v)) +} + +// LogLTE applies the LTE predicate on the "log" field. +func LogLTE(v string) predicate.MaintenanceOperationRun { + return predicate.MaintenanceOperationRun(sql.FieldLTE(FieldLog, v)) +} + +// LogContains applies the Contains predicate on the "log" field. +func LogContains(v string) predicate.MaintenanceOperationRun { + return predicate.MaintenanceOperationRun(sql.FieldContains(FieldLog, v)) +} + +// LogHasPrefix applies the HasPrefix predicate on the "log" field. +func LogHasPrefix(v string) predicate.MaintenanceOperationRun { + return predicate.MaintenanceOperationRun(sql.FieldHasPrefix(FieldLog, v)) +} + +// LogHasSuffix applies the HasSuffix predicate on the "log" field. +func LogHasSuffix(v string) predicate.MaintenanceOperationRun { + return predicate.MaintenanceOperationRun(sql.FieldHasSuffix(FieldLog, v)) +} + +// LogEqualFold applies the EqualFold predicate on the "log" field. +func LogEqualFold(v string) predicate.MaintenanceOperationRun { + return predicate.MaintenanceOperationRun(sql.FieldEqualFold(FieldLog, v)) +} + +// LogContainsFold applies the ContainsFold predicate on the "log" field. +func LogContainsFold(v string) predicate.MaintenanceOperationRun { + return predicate.MaintenanceOperationRun(sql.FieldContainsFold(FieldLog, v)) +} + +// And groups predicates with the AND operator between them. +func And(predicates ...predicate.MaintenanceOperationRun) predicate.MaintenanceOperationRun { + return predicate.MaintenanceOperationRun(sql.AndPredicates(predicates...)) +} + +// Or groups predicates with the OR operator between them. +func Or(predicates ...predicate.MaintenanceOperationRun) predicate.MaintenanceOperationRun { + return predicate.MaintenanceOperationRun(sql.OrPredicates(predicates...)) +} + +// Not applies the not operator on the given predicate. +func Not(p predicate.MaintenanceOperationRun) predicate.MaintenanceOperationRun { + return predicate.MaintenanceOperationRun(sql.NotPredicates(p)) +} diff --git a/pkg/ent/maintenanceoperationrun_create.go b/pkg/ent/maintenanceoperationrun_create.go new file mode 100644 index 000000000..5743296e3 --- /dev/null +++ b/pkg/ent/maintenanceoperationrun_create.go @@ -0,0 +1,909 @@ +// Code generated by ent, DO NOT EDIT. + +package ent + +import ( + "context" + "errors" + "fmt" + "time" + + "entgo.io/ent/dialect" + "entgo.io/ent/dialect/sql" + "entgo.io/ent/dialect/sql/sqlgraph" + "entgo.io/ent/schema/field" + "github.com/GoogleCloudPlatform/scion/pkg/ent/maintenanceoperationrun" + "github.com/google/uuid" +) + +// MaintenanceOperationRunCreate is the builder for creating a MaintenanceOperationRun entity. +type MaintenanceOperationRunCreate struct { + config + mutation *MaintenanceOperationRunMutation + hooks []Hook + conflict []sql.ConflictOption +} + +// SetOperationKey sets the "operation_key" field. +func (_c *MaintenanceOperationRunCreate) SetOperationKey(v string) *MaintenanceOperationRunCreate { + _c.mutation.SetOperationKey(v) + return _c +} + +// SetStatus sets the "status" field. +func (_c *MaintenanceOperationRunCreate) SetStatus(v string) *MaintenanceOperationRunCreate { + _c.mutation.SetStatus(v) + return _c +} + +// SetNillableStatus sets the "status" field if the given value is not nil. +func (_c *MaintenanceOperationRunCreate) SetNillableStatus(v *string) *MaintenanceOperationRunCreate { + if v != nil { + _c.SetStatus(*v) + } + return _c +} + +// SetStartedAt sets the "started_at" field. +func (_c *MaintenanceOperationRunCreate) SetStartedAt(v time.Time) *MaintenanceOperationRunCreate { + _c.mutation.SetStartedAt(v) + return _c +} + +// SetNillableStartedAt sets the "started_at" field if the given value is not nil. +func (_c *MaintenanceOperationRunCreate) SetNillableStartedAt(v *time.Time) *MaintenanceOperationRunCreate { + if v != nil { + _c.SetStartedAt(*v) + } + return _c +} + +// SetCompletedAt sets the "completed_at" field. +func (_c *MaintenanceOperationRunCreate) SetCompletedAt(v time.Time) *MaintenanceOperationRunCreate { + _c.mutation.SetCompletedAt(v) + return _c +} + +// SetNillableCompletedAt sets the "completed_at" field if the given value is not nil. +func (_c *MaintenanceOperationRunCreate) SetNillableCompletedAt(v *time.Time) *MaintenanceOperationRunCreate { + if v != nil { + _c.SetCompletedAt(*v) + } + return _c +} + +// SetStartedBy sets the "started_by" field. +func (_c *MaintenanceOperationRunCreate) SetStartedBy(v string) *MaintenanceOperationRunCreate { + _c.mutation.SetStartedBy(v) + return _c +} + +// SetNillableStartedBy sets the "started_by" field if the given value is not nil. +func (_c *MaintenanceOperationRunCreate) SetNillableStartedBy(v *string) *MaintenanceOperationRunCreate { + if v != nil { + _c.SetStartedBy(*v) + } + return _c +} + +// SetResult sets the "result" field. +func (_c *MaintenanceOperationRunCreate) SetResult(v string) *MaintenanceOperationRunCreate { + _c.mutation.SetResult(v) + return _c +} + +// SetNillableResult sets the "result" field if the given value is not nil. +func (_c *MaintenanceOperationRunCreate) SetNillableResult(v *string) *MaintenanceOperationRunCreate { + if v != nil { + _c.SetResult(*v) + } + return _c +} + +// SetLog sets the "log" field. +func (_c *MaintenanceOperationRunCreate) SetLog(v string) *MaintenanceOperationRunCreate { + _c.mutation.SetLog(v) + return _c +} + +// SetNillableLog sets the "log" field if the given value is not nil. +func (_c *MaintenanceOperationRunCreate) SetNillableLog(v *string) *MaintenanceOperationRunCreate { + if v != nil { + _c.SetLog(*v) + } + return _c +} + +// SetID sets the "id" field. +func (_c *MaintenanceOperationRunCreate) SetID(v uuid.UUID) *MaintenanceOperationRunCreate { + _c.mutation.SetID(v) + return _c +} + +// SetNillableID sets the "id" field if the given value is not nil. +func (_c *MaintenanceOperationRunCreate) SetNillableID(v *uuid.UUID) *MaintenanceOperationRunCreate { + if v != nil { + _c.SetID(*v) + } + return _c +} + +// Mutation returns the MaintenanceOperationRunMutation object of the builder. +func (_c *MaintenanceOperationRunCreate) Mutation() *MaintenanceOperationRunMutation { + return _c.mutation +} + +// Save creates the MaintenanceOperationRun in the database. +func (_c *MaintenanceOperationRunCreate) Save(ctx context.Context) (*MaintenanceOperationRun, error) { + _c.defaults() + return withHooks(ctx, _c.sqlSave, _c.mutation, _c.hooks) +} + +// SaveX calls Save and panics if Save returns an error. +func (_c *MaintenanceOperationRunCreate) SaveX(ctx context.Context) *MaintenanceOperationRun { + v, err := _c.Save(ctx) + if err != nil { + panic(err) + } + return v +} + +// Exec executes the query. +func (_c *MaintenanceOperationRunCreate) Exec(ctx context.Context) error { + _, err := _c.Save(ctx) + return err +} + +// ExecX is like Exec, but panics if an error occurs. +func (_c *MaintenanceOperationRunCreate) ExecX(ctx context.Context) { + if err := _c.Exec(ctx); err != nil { + panic(err) + } +} + +// defaults sets the default values of the builder before save. +func (_c *MaintenanceOperationRunCreate) defaults() { + if _, ok := _c.mutation.Status(); !ok { + v := maintenanceoperationrun.DefaultStatus + _c.mutation.SetStatus(v) + } + if _, ok := _c.mutation.StartedAt(); !ok { + v := maintenanceoperationrun.DefaultStartedAt() + _c.mutation.SetStartedAt(v) + } + if _, ok := _c.mutation.Log(); !ok { + v := maintenanceoperationrun.DefaultLog + _c.mutation.SetLog(v) + } + if _, ok := _c.mutation.ID(); !ok { + v := maintenanceoperationrun.DefaultID() + _c.mutation.SetID(v) + } +} + +// check runs all checks and user-defined validators on the builder. +func (_c *MaintenanceOperationRunCreate) check() error { + if _, ok := _c.mutation.OperationKey(); !ok { + return &ValidationError{Name: "operation_key", err: errors.New(`ent: missing required field "MaintenanceOperationRun.operation_key"`)} + } + if v, ok := _c.mutation.OperationKey(); ok { + if err := maintenanceoperationrun.OperationKeyValidator(v); err != nil { + return &ValidationError{Name: "operation_key", err: fmt.Errorf(`ent: validator failed for field "MaintenanceOperationRun.operation_key": %w`, err)} + } + } + if _, ok := _c.mutation.Status(); !ok { + return &ValidationError{Name: "status", err: errors.New(`ent: missing required field "MaintenanceOperationRun.status"`)} + } + if _, ok := _c.mutation.StartedAt(); !ok { + return &ValidationError{Name: "started_at", err: errors.New(`ent: missing required field "MaintenanceOperationRun.started_at"`)} + } + if _, ok := _c.mutation.Log(); !ok { + return &ValidationError{Name: "log", err: errors.New(`ent: missing required field "MaintenanceOperationRun.log"`)} + } + return nil +} + +func (_c *MaintenanceOperationRunCreate) sqlSave(ctx context.Context) (*MaintenanceOperationRun, error) { + if err := _c.check(); err != nil { + return nil, err + } + _node, _spec := _c.createSpec() + if err := sqlgraph.CreateNode(ctx, _c.driver, _spec); err != nil { + if sqlgraph.IsConstraintError(err) { + err = &ConstraintError{msg: err.Error(), wrap: err} + } + return nil, err + } + if _spec.ID.Value != nil { + if id, ok := _spec.ID.Value.(*uuid.UUID); ok { + _node.ID = *id + } else if err := _node.ID.Scan(_spec.ID.Value); err != nil { + return nil, err + } + } + _c.mutation.id = &_node.ID + _c.mutation.done = true + return _node, nil +} + +func (_c *MaintenanceOperationRunCreate) createSpec() (*MaintenanceOperationRun, *sqlgraph.CreateSpec) { + var ( + _node = &MaintenanceOperationRun{config: _c.config} + _spec = sqlgraph.NewCreateSpec(maintenanceoperationrun.Table, sqlgraph.NewFieldSpec(maintenanceoperationrun.FieldID, field.TypeUUID)) + ) + _spec.OnConflict = _c.conflict + if id, ok := _c.mutation.ID(); ok { + _node.ID = id + _spec.ID.Value = &id + } + if value, ok := _c.mutation.OperationKey(); ok { + _spec.SetField(maintenanceoperationrun.FieldOperationKey, field.TypeString, value) + _node.OperationKey = value + } + if value, ok := _c.mutation.Status(); ok { + _spec.SetField(maintenanceoperationrun.FieldStatus, field.TypeString, value) + _node.Status = value + } + if value, ok := _c.mutation.StartedAt(); ok { + _spec.SetField(maintenanceoperationrun.FieldStartedAt, field.TypeTime, value) + _node.StartedAt = value + } + if value, ok := _c.mutation.CompletedAt(); ok { + _spec.SetField(maintenanceoperationrun.FieldCompletedAt, field.TypeTime, value) + _node.CompletedAt = &value + } + if value, ok := _c.mutation.StartedBy(); ok { + _spec.SetField(maintenanceoperationrun.FieldStartedBy, field.TypeString, value) + _node.StartedBy = value + } + if value, ok := _c.mutation.Result(); ok { + _spec.SetField(maintenanceoperationrun.FieldResult, field.TypeString, value) + _node.Result = value + } + if value, ok := _c.mutation.Log(); ok { + _spec.SetField(maintenanceoperationrun.FieldLog, field.TypeString, value) + _node.Log = value + } + return _node, _spec +} + +// OnConflict allows configuring the `ON CONFLICT` / `ON DUPLICATE KEY` clause +// of the `INSERT` statement. For example: +// +// client.MaintenanceOperationRun.Create(). +// SetOperationKey(v). +// OnConflict( +// // Update the row with the new values +// // the was proposed for insertion. +// sql.ResolveWithNewValues(), +// ). +// // Override some of the fields with custom +// // update values. +// Update(func(u *ent.MaintenanceOperationRunUpsert) { +// SetOperationKey(v+v). +// }). +// Exec(ctx) +func (_c *MaintenanceOperationRunCreate) OnConflict(opts ...sql.ConflictOption) *MaintenanceOperationRunUpsertOne { + _c.conflict = opts + return &MaintenanceOperationRunUpsertOne{ + create: _c, + } +} + +// OnConflictColumns calls `OnConflict` and configures the columns +// as conflict target. Using this option is equivalent to using: +// +// client.MaintenanceOperationRun.Create(). +// OnConflict(sql.ConflictColumns(columns...)). +// Exec(ctx) +func (_c *MaintenanceOperationRunCreate) OnConflictColumns(columns ...string) *MaintenanceOperationRunUpsertOne { + _c.conflict = append(_c.conflict, sql.ConflictColumns(columns...)) + return &MaintenanceOperationRunUpsertOne{ + create: _c, + } +} + +type ( + // MaintenanceOperationRunUpsertOne is the builder for "upsert"-ing + // one MaintenanceOperationRun node. + MaintenanceOperationRunUpsertOne struct { + create *MaintenanceOperationRunCreate + } + + // MaintenanceOperationRunUpsert is the "OnConflict" setter. + MaintenanceOperationRunUpsert struct { + *sql.UpdateSet + } +) + +// SetOperationKey sets the "operation_key" field. +func (u *MaintenanceOperationRunUpsert) SetOperationKey(v string) *MaintenanceOperationRunUpsert { + u.Set(maintenanceoperationrun.FieldOperationKey, v) + return u +} + +// UpdateOperationKey sets the "operation_key" field to the value that was provided on create. +func (u *MaintenanceOperationRunUpsert) UpdateOperationKey() *MaintenanceOperationRunUpsert { + u.SetExcluded(maintenanceoperationrun.FieldOperationKey) + return u +} + +// SetStatus sets the "status" field. +func (u *MaintenanceOperationRunUpsert) SetStatus(v string) *MaintenanceOperationRunUpsert { + u.Set(maintenanceoperationrun.FieldStatus, v) + return u +} + +// UpdateStatus sets the "status" field to the value that was provided on create. +func (u *MaintenanceOperationRunUpsert) UpdateStatus() *MaintenanceOperationRunUpsert { + u.SetExcluded(maintenanceoperationrun.FieldStatus) + return u +} + +// SetCompletedAt sets the "completed_at" field. +func (u *MaintenanceOperationRunUpsert) SetCompletedAt(v time.Time) *MaintenanceOperationRunUpsert { + u.Set(maintenanceoperationrun.FieldCompletedAt, v) + return u +} + +// UpdateCompletedAt sets the "completed_at" field to the value that was provided on create. +func (u *MaintenanceOperationRunUpsert) UpdateCompletedAt() *MaintenanceOperationRunUpsert { + u.SetExcluded(maintenanceoperationrun.FieldCompletedAt) + return u +} + +// ClearCompletedAt clears the value of the "completed_at" field. +func (u *MaintenanceOperationRunUpsert) ClearCompletedAt() *MaintenanceOperationRunUpsert { + u.SetNull(maintenanceoperationrun.FieldCompletedAt) + return u +} + +// SetStartedBy sets the "started_by" field. +func (u *MaintenanceOperationRunUpsert) SetStartedBy(v string) *MaintenanceOperationRunUpsert { + u.Set(maintenanceoperationrun.FieldStartedBy, v) + return u +} + +// UpdateStartedBy sets the "started_by" field to the value that was provided on create. +func (u *MaintenanceOperationRunUpsert) UpdateStartedBy() *MaintenanceOperationRunUpsert { + u.SetExcluded(maintenanceoperationrun.FieldStartedBy) + return u +} + +// ClearStartedBy clears the value of the "started_by" field. +func (u *MaintenanceOperationRunUpsert) ClearStartedBy() *MaintenanceOperationRunUpsert { + u.SetNull(maintenanceoperationrun.FieldStartedBy) + return u +} + +// SetResult sets the "result" field. +func (u *MaintenanceOperationRunUpsert) SetResult(v string) *MaintenanceOperationRunUpsert { + u.Set(maintenanceoperationrun.FieldResult, v) + return u +} + +// UpdateResult sets the "result" field to the value that was provided on create. +func (u *MaintenanceOperationRunUpsert) UpdateResult() *MaintenanceOperationRunUpsert { + u.SetExcluded(maintenanceoperationrun.FieldResult) + return u +} + +// ClearResult clears the value of the "result" field. +func (u *MaintenanceOperationRunUpsert) ClearResult() *MaintenanceOperationRunUpsert { + u.SetNull(maintenanceoperationrun.FieldResult) + return u +} + +// SetLog sets the "log" field. +func (u *MaintenanceOperationRunUpsert) SetLog(v string) *MaintenanceOperationRunUpsert { + u.Set(maintenanceoperationrun.FieldLog, v) + return u +} + +// UpdateLog sets the "log" field to the value that was provided on create. +func (u *MaintenanceOperationRunUpsert) UpdateLog() *MaintenanceOperationRunUpsert { + u.SetExcluded(maintenanceoperationrun.FieldLog) + return u +} + +// UpdateNewValues updates the mutable fields using the new values that were set on create except the ID field. +// Using this option is equivalent to using: +// +// client.MaintenanceOperationRun.Create(). +// OnConflict( +// sql.ResolveWithNewValues(), +// sql.ResolveWith(func(u *sql.UpdateSet) { +// u.SetIgnore(maintenanceoperationrun.FieldID) +// }), +// ). +// Exec(ctx) +func (u *MaintenanceOperationRunUpsertOne) UpdateNewValues() *MaintenanceOperationRunUpsertOne { + u.create.conflict = append(u.create.conflict, sql.ResolveWithNewValues()) + u.create.conflict = append(u.create.conflict, sql.ResolveWith(func(s *sql.UpdateSet) { + if _, exists := u.create.mutation.ID(); exists { + s.SetIgnore(maintenanceoperationrun.FieldID) + } + if _, exists := u.create.mutation.StartedAt(); exists { + s.SetIgnore(maintenanceoperationrun.FieldStartedAt) + } + })) + return u +} + +// Ignore sets each column to itself in case of conflict. +// Using this option is equivalent to using: +// +// client.MaintenanceOperationRun.Create(). +// OnConflict(sql.ResolveWithIgnore()). +// Exec(ctx) +func (u *MaintenanceOperationRunUpsertOne) Ignore() *MaintenanceOperationRunUpsertOne { + u.create.conflict = append(u.create.conflict, sql.ResolveWithIgnore()) + return u +} + +// DoNothing configures the conflict_action to `DO NOTHING`. +// Supported only by SQLite and PostgreSQL. +func (u *MaintenanceOperationRunUpsertOne) DoNothing() *MaintenanceOperationRunUpsertOne { + u.create.conflict = append(u.create.conflict, sql.DoNothing()) + return u +} + +// Update allows overriding fields `UPDATE` values. See the MaintenanceOperationRunCreate.OnConflict +// documentation for more info. +func (u *MaintenanceOperationRunUpsertOne) Update(set func(*MaintenanceOperationRunUpsert)) *MaintenanceOperationRunUpsertOne { + u.create.conflict = append(u.create.conflict, sql.ResolveWith(func(update *sql.UpdateSet) { + set(&MaintenanceOperationRunUpsert{UpdateSet: update}) + })) + return u +} + +// SetOperationKey sets the "operation_key" field. +func (u *MaintenanceOperationRunUpsertOne) SetOperationKey(v string) *MaintenanceOperationRunUpsertOne { + return u.Update(func(s *MaintenanceOperationRunUpsert) { + s.SetOperationKey(v) + }) +} + +// UpdateOperationKey sets the "operation_key" field to the value that was provided on create. +func (u *MaintenanceOperationRunUpsertOne) UpdateOperationKey() *MaintenanceOperationRunUpsertOne { + return u.Update(func(s *MaintenanceOperationRunUpsert) { + s.UpdateOperationKey() + }) +} + +// SetStatus sets the "status" field. +func (u *MaintenanceOperationRunUpsertOne) SetStatus(v string) *MaintenanceOperationRunUpsertOne { + return u.Update(func(s *MaintenanceOperationRunUpsert) { + s.SetStatus(v) + }) +} + +// UpdateStatus sets the "status" field to the value that was provided on create. +func (u *MaintenanceOperationRunUpsertOne) UpdateStatus() *MaintenanceOperationRunUpsertOne { + return u.Update(func(s *MaintenanceOperationRunUpsert) { + s.UpdateStatus() + }) +} + +// SetCompletedAt sets the "completed_at" field. +func (u *MaintenanceOperationRunUpsertOne) SetCompletedAt(v time.Time) *MaintenanceOperationRunUpsertOne { + return u.Update(func(s *MaintenanceOperationRunUpsert) { + s.SetCompletedAt(v) + }) +} + +// UpdateCompletedAt sets the "completed_at" field to the value that was provided on create. +func (u *MaintenanceOperationRunUpsertOne) UpdateCompletedAt() *MaintenanceOperationRunUpsertOne { + return u.Update(func(s *MaintenanceOperationRunUpsert) { + s.UpdateCompletedAt() + }) +} + +// ClearCompletedAt clears the value of the "completed_at" field. +func (u *MaintenanceOperationRunUpsertOne) ClearCompletedAt() *MaintenanceOperationRunUpsertOne { + return u.Update(func(s *MaintenanceOperationRunUpsert) { + s.ClearCompletedAt() + }) +} + +// SetStartedBy sets the "started_by" field. +func (u *MaintenanceOperationRunUpsertOne) SetStartedBy(v string) *MaintenanceOperationRunUpsertOne { + return u.Update(func(s *MaintenanceOperationRunUpsert) { + s.SetStartedBy(v) + }) +} + +// UpdateStartedBy sets the "started_by" field to the value that was provided on create. +func (u *MaintenanceOperationRunUpsertOne) UpdateStartedBy() *MaintenanceOperationRunUpsertOne { + return u.Update(func(s *MaintenanceOperationRunUpsert) { + s.UpdateStartedBy() + }) +} + +// ClearStartedBy clears the value of the "started_by" field. +func (u *MaintenanceOperationRunUpsertOne) ClearStartedBy() *MaintenanceOperationRunUpsertOne { + return u.Update(func(s *MaintenanceOperationRunUpsert) { + s.ClearStartedBy() + }) +} + +// SetResult sets the "result" field. +func (u *MaintenanceOperationRunUpsertOne) SetResult(v string) *MaintenanceOperationRunUpsertOne { + return u.Update(func(s *MaintenanceOperationRunUpsert) { + s.SetResult(v) + }) +} + +// UpdateResult sets the "result" field to the value that was provided on create. +func (u *MaintenanceOperationRunUpsertOne) UpdateResult() *MaintenanceOperationRunUpsertOne { + return u.Update(func(s *MaintenanceOperationRunUpsert) { + s.UpdateResult() + }) +} + +// ClearResult clears the value of the "result" field. +func (u *MaintenanceOperationRunUpsertOne) ClearResult() *MaintenanceOperationRunUpsertOne { + return u.Update(func(s *MaintenanceOperationRunUpsert) { + s.ClearResult() + }) +} + +// SetLog sets the "log" field. +func (u *MaintenanceOperationRunUpsertOne) SetLog(v string) *MaintenanceOperationRunUpsertOne { + return u.Update(func(s *MaintenanceOperationRunUpsert) { + s.SetLog(v) + }) +} + +// UpdateLog sets the "log" field to the value that was provided on create. +func (u *MaintenanceOperationRunUpsertOne) UpdateLog() *MaintenanceOperationRunUpsertOne { + return u.Update(func(s *MaintenanceOperationRunUpsert) { + s.UpdateLog() + }) +} + +// Exec executes the query. +func (u *MaintenanceOperationRunUpsertOne) Exec(ctx context.Context) error { + if len(u.create.conflict) == 0 { + return errors.New("ent: missing options for MaintenanceOperationRunCreate.OnConflict") + } + return u.create.Exec(ctx) +} + +// ExecX is like Exec, but panics if an error occurs. +func (u *MaintenanceOperationRunUpsertOne) ExecX(ctx context.Context) { + if err := u.create.Exec(ctx); err != nil { + panic(err) + } +} + +// Exec executes the UPSERT query and returns the inserted/updated ID. +func (u *MaintenanceOperationRunUpsertOne) ID(ctx context.Context) (id uuid.UUID, err error) { + if u.create.driver.Dialect() == dialect.MySQL { + // In case of "ON CONFLICT", there is no way to get back non-numeric ID + // fields from the database since MySQL does not support the RETURNING clause. + return id, errors.New("ent: MaintenanceOperationRunUpsertOne.ID is not supported by MySQL driver. Use MaintenanceOperationRunUpsertOne.Exec instead") + } + node, err := u.create.Save(ctx) + if err != nil { + return id, err + } + return node.ID, nil +} + +// IDX is like ID, but panics if an error occurs. +func (u *MaintenanceOperationRunUpsertOne) IDX(ctx context.Context) uuid.UUID { + id, err := u.ID(ctx) + if err != nil { + panic(err) + } + return id +} + +// MaintenanceOperationRunCreateBulk is the builder for creating many MaintenanceOperationRun entities in bulk. +type MaintenanceOperationRunCreateBulk struct { + config + err error + builders []*MaintenanceOperationRunCreate + conflict []sql.ConflictOption +} + +// Save creates the MaintenanceOperationRun entities in the database. +func (_c *MaintenanceOperationRunCreateBulk) Save(ctx context.Context) ([]*MaintenanceOperationRun, error) { + if _c.err != nil { + return nil, _c.err + } + specs := make([]*sqlgraph.CreateSpec, len(_c.builders)) + nodes := make([]*MaintenanceOperationRun, len(_c.builders)) + mutators := make([]Mutator, len(_c.builders)) + for i := range _c.builders { + func(i int, root context.Context) { + builder := _c.builders[i] + builder.defaults() + var mut Mutator = MutateFunc(func(ctx context.Context, m Mutation) (Value, error) { + mutation, ok := m.(*MaintenanceOperationRunMutation) + if !ok { + return nil, fmt.Errorf("unexpected mutation type %T", m) + } + if err := builder.check(); err != nil { + return nil, err + } + builder.mutation = mutation + var err error + nodes[i], specs[i] = builder.createSpec() + if i < len(mutators)-1 { + _, err = mutators[i+1].Mutate(root, _c.builders[i+1].mutation) + } else { + spec := &sqlgraph.BatchCreateSpec{Nodes: specs} + spec.OnConflict = _c.conflict + // Invoke the actual operation on the latest mutation in the chain. + if err = sqlgraph.BatchCreate(ctx, _c.driver, spec); err != nil { + if sqlgraph.IsConstraintError(err) { + err = &ConstraintError{msg: err.Error(), wrap: err} + } + } + } + if err != nil { + return nil, err + } + mutation.id = &nodes[i].ID + mutation.done = true + return nodes[i], nil + }) + for i := len(builder.hooks) - 1; i >= 0; i-- { + mut = builder.hooks[i](mut) + } + mutators[i] = mut + }(i, ctx) + } + if len(mutators) > 0 { + if _, err := mutators[0].Mutate(ctx, _c.builders[0].mutation); err != nil { + return nil, err + } + } + return nodes, nil +} + +// SaveX is like Save, but panics if an error occurs. +func (_c *MaintenanceOperationRunCreateBulk) SaveX(ctx context.Context) []*MaintenanceOperationRun { + v, err := _c.Save(ctx) + if err != nil { + panic(err) + } + return v +} + +// Exec executes the query. +func (_c *MaintenanceOperationRunCreateBulk) Exec(ctx context.Context) error { + _, err := _c.Save(ctx) + return err +} + +// ExecX is like Exec, but panics if an error occurs. +func (_c *MaintenanceOperationRunCreateBulk) ExecX(ctx context.Context) { + if err := _c.Exec(ctx); err != nil { + panic(err) + } +} + +// OnConflict allows configuring the `ON CONFLICT` / `ON DUPLICATE KEY` clause +// of the `INSERT` statement. For example: +// +// client.MaintenanceOperationRun.CreateBulk(builders...). +// OnConflict( +// // Update the row with the new values +// // the was proposed for insertion. +// sql.ResolveWithNewValues(), +// ). +// // Override some of the fields with custom +// // update values. +// Update(func(u *ent.MaintenanceOperationRunUpsert) { +// SetOperationKey(v+v). +// }). +// Exec(ctx) +func (_c *MaintenanceOperationRunCreateBulk) OnConflict(opts ...sql.ConflictOption) *MaintenanceOperationRunUpsertBulk { + _c.conflict = opts + return &MaintenanceOperationRunUpsertBulk{ + create: _c, + } +} + +// OnConflictColumns calls `OnConflict` and configures the columns +// as conflict target. Using this option is equivalent to using: +// +// client.MaintenanceOperationRun.Create(). +// OnConflict(sql.ConflictColumns(columns...)). +// Exec(ctx) +func (_c *MaintenanceOperationRunCreateBulk) OnConflictColumns(columns ...string) *MaintenanceOperationRunUpsertBulk { + _c.conflict = append(_c.conflict, sql.ConflictColumns(columns...)) + return &MaintenanceOperationRunUpsertBulk{ + create: _c, + } +} + +// MaintenanceOperationRunUpsertBulk is the builder for "upsert"-ing +// a bulk of MaintenanceOperationRun nodes. +type MaintenanceOperationRunUpsertBulk struct { + create *MaintenanceOperationRunCreateBulk +} + +// UpdateNewValues updates the mutable fields using the new values that +// were set on create. Using this option is equivalent to using: +// +// client.MaintenanceOperationRun.Create(). +// OnConflict( +// sql.ResolveWithNewValues(), +// sql.ResolveWith(func(u *sql.UpdateSet) { +// u.SetIgnore(maintenanceoperationrun.FieldID) +// }), +// ). +// Exec(ctx) +func (u *MaintenanceOperationRunUpsertBulk) UpdateNewValues() *MaintenanceOperationRunUpsertBulk { + u.create.conflict = append(u.create.conflict, sql.ResolveWithNewValues()) + u.create.conflict = append(u.create.conflict, sql.ResolveWith(func(s *sql.UpdateSet) { + for _, b := range u.create.builders { + if _, exists := b.mutation.ID(); exists { + s.SetIgnore(maintenanceoperationrun.FieldID) + } + if _, exists := b.mutation.StartedAt(); exists { + s.SetIgnore(maintenanceoperationrun.FieldStartedAt) + } + } + })) + return u +} + +// Ignore sets each column to itself in case of conflict. +// Using this option is equivalent to using: +// +// client.MaintenanceOperationRun.Create(). +// OnConflict(sql.ResolveWithIgnore()). +// Exec(ctx) +func (u *MaintenanceOperationRunUpsertBulk) Ignore() *MaintenanceOperationRunUpsertBulk { + u.create.conflict = append(u.create.conflict, sql.ResolveWithIgnore()) + return u +} + +// DoNothing configures the conflict_action to `DO NOTHING`. +// Supported only by SQLite and PostgreSQL. +func (u *MaintenanceOperationRunUpsertBulk) DoNothing() *MaintenanceOperationRunUpsertBulk { + u.create.conflict = append(u.create.conflict, sql.DoNothing()) + return u +} + +// Update allows overriding fields `UPDATE` values. See the MaintenanceOperationRunCreateBulk.OnConflict +// documentation for more info. +func (u *MaintenanceOperationRunUpsertBulk) Update(set func(*MaintenanceOperationRunUpsert)) *MaintenanceOperationRunUpsertBulk { + u.create.conflict = append(u.create.conflict, sql.ResolveWith(func(update *sql.UpdateSet) { + set(&MaintenanceOperationRunUpsert{UpdateSet: update}) + })) + return u +} + +// SetOperationKey sets the "operation_key" field. +func (u *MaintenanceOperationRunUpsertBulk) SetOperationKey(v string) *MaintenanceOperationRunUpsertBulk { + return u.Update(func(s *MaintenanceOperationRunUpsert) { + s.SetOperationKey(v) + }) +} + +// UpdateOperationKey sets the "operation_key" field to the value that was provided on create. +func (u *MaintenanceOperationRunUpsertBulk) UpdateOperationKey() *MaintenanceOperationRunUpsertBulk { + return u.Update(func(s *MaintenanceOperationRunUpsert) { + s.UpdateOperationKey() + }) +} + +// SetStatus sets the "status" field. +func (u *MaintenanceOperationRunUpsertBulk) SetStatus(v string) *MaintenanceOperationRunUpsertBulk { + return u.Update(func(s *MaintenanceOperationRunUpsert) { + s.SetStatus(v) + }) +} + +// UpdateStatus sets the "status" field to the value that was provided on create. +func (u *MaintenanceOperationRunUpsertBulk) UpdateStatus() *MaintenanceOperationRunUpsertBulk { + return u.Update(func(s *MaintenanceOperationRunUpsert) { + s.UpdateStatus() + }) +} + +// SetCompletedAt sets the "completed_at" field. +func (u *MaintenanceOperationRunUpsertBulk) SetCompletedAt(v time.Time) *MaintenanceOperationRunUpsertBulk { + return u.Update(func(s *MaintenanceOperationRunUpsert) { + s.SetCompletedAt(v) + }) +} + +// UpdateCompletedAt sets the "completed_at" field to the value that was provided on create. +func (u *MaintenanceOperationRunUpsertBulk) UpdateCompletedAt() *MaintenanceOperationRunUpsertBulk { + return u.Update(func(s *MaintenanceOperationRunUpsert) { + s.UpdateCompletedAt() + }) +} + +// ClearCompletedAt clears the value of the "completed_at" field. +func (u *MaintenanceOperationRunUpsertBulk) ClearCompletedAt() *MaintenanceOperationRunUpsertBulk { + return u.Update(func(s *MaintenanceOperationRunUpsert) { + s.ClearCompletedAt() + }) +} + +// SetStartedBy sets the "started_by" field. +func (u *MaintenanceOperationRunUpsertBulk) SetStartedBy(v string) *MaintenanceOperationRunUpsertBulk { + return u.Update(func(s *MaintenanceOperationRunUpsert) { + s.SetStartedBy(v) + }) +} + +// UpdateStartedBy sets the "started_by" field to the value that was provided on create. +func (u *MaintenanceOperationRunUpsertBulk) UpdateStartedBy() *MaintenanceOperationRunUpsertBulk { + return u.Update(func(s *MaintenanceOperationRunUpsert) { + s.UpdateStartedBy() + }) +} + +// ClearStartedBy clears the value of the "started_by" field. +func (u *MaintenanceOperationRunUpsertBulk) ClearStartedBy() *MaintenanceOperationRunUpsertBulk { + return u.Update(func(s *MaintenanceOperationRunUpsert) { + s.ClearStartedBy() + }) +} + +// SetResult sets the "result" field. +func (u *MaintenanceOperationRunUpsertBulk) SetResult(v string) *MaintenanceOperationRunUpsertBulk { + return u.Update(func(s *MaintenanceOperationRunUpsert) { + s.SetResult(v) + }) +} + +// UpdateResult sets the "result" field to the value that was provided on create. +func (u *MaintenanceOperationRunUpsertBulk) UpdateResult() *MaintenanceOperationRunUpsertBulk { + return u.Update(func(s *MaintenanceOperationRunUpsert) { + s.UpdateResult() + }) +} + +// ClearResult clears the value of the "result" field. +func (u *MaintenanceOperationRunUpsertBulk) ClearResult() *MaintenanceOperationRunUpsertBulk { + return u.Update(func(s *MaintenanceOperationRunUpsert) { + s.ClearResult() + }) +} + +// SetLog sets the "log" field. +func (u *MaintenanceOperationRunUpsertBulk) SetLog(v string) *MaintenanceOperationRunUpsertBulk { + return u.Update(func(s *MaintenanceOperationRunUpsert) { + s.SetLog(v) + }) +} + +// UpdateLog sets the "log" field to the value that was provided on create. +func (u *MaintenanceOperationRunUpsertBulk) UpdateLog() *MaintenanceOperationRunUpsertBulk { + return u.Update(func(s *MaintenanceOperationRunUpsert) { + s.UpdateLog() + }) +} + +// Exec executes the query. +func (u *MaintenanceOperationRunUpsertBulk) Exec(ctx context.Context) error { + if u.create.err != nil { + return u.create.err + } + for i, b := range u.create.builders { + if len(b.conflict) != 0 { + return fmt.Errorf("ent: OnConflict was set for builder %d. Set it on the MaintenanceOperationRunCreateBulk instead", i) + } + } + if len(u.create.conflict) == 0 { + return errors.New("ent: missing options for MaintenanceOperationRunCreateBulk.OnConflict") + } + return u.create.Exec(ctx) +} + +// ExecX is like Exec, but panics if an error occurs. +func (u *MaintenanceOperationRunUpsertBulk) ExecX(ctx context.Context) { + if err := u.create.Exec(ctx); err != nil { + panic(err) + } +} diff --git a/pkg/ent/maintenanceoperationrun_delete.go b/pkg/ent/maintenanceoperationrun_delete.go new file mode 100644 index 000000000..52e40a36f --- /dev/null +++ b/pkg/ent/maintenanceoperationrun_delete.go @@ -0,0 +1,88 @@ +// Code generated by ent, DO NOT EDIT. + +package ent + +import ( + "context" + + "entgo.io/ent/dialect/sql" + "entgo.io/ent/dialect/sql/sqlgraph" + "entgo.io/ent/schema/field" + "github.com/GoogleCloudPlatform/scion/pkg/ent/maintenanceoperationrun" + "github.com/GoogleCloudPlatform/scion/pkg/ent/predicate" +) + +// MaintenanceOperationRunDelete is the builder for deleting a MaintenanceOperationRun entity. +type MaintenanceOperationRunDelete struct { + config + hooks []Hook + mutation *MaintenanceOperationRunMutation +} + +// Where appends a list predicates to the MaintenanceOperationRunDelete builder. +func (_d *MaintenanceOperationRunDelete) Where(ps ...predicate.MaintenanceOperationRun) *MaintenanceOperationRunDelete { + _d.mutation.Where(ps...) + return _d +} + +// Exec executes the deletion query and returns how many vertices were deleted. +func (_d *MaintenanceOperationRunDelete) Exec(ctx context.Context) (int, error) { + return withHooks(ctx, _d.sqlExec, _d.mutation, _d.hooks) +} + +// ExecX is like Exec, but panics if an error occurs. +func (_d *MaintenanceOperationRunDelete) ExecX(ctx context.Context) int { + n, err := _d.Exec(ctx) + if err != nil { + panic(err) + } + return n +} + +func (_d *MaintenanceOperationRunDelete) sqlExec(ctx context.Context) (int, error) { + _spec := sqlgraph.NewDeleteSpec(maintenanceoperationrun.Table, sqlgraph.NewFieldSpec(maintenanceoperationrun.FieldID, field.TypeUUID)) + if ps := _d.mutation.predicates; len(ps) > 0 { + _spec.Predicate = func(selector *sql.Selector) { + for i := range ps { + ps[i](selector) + } + } + } + affected, err := sqlgraph.DeleteNodes(ctx, _d.driver, _spec) + if err != nil && sqlgraph.IsConstraintError(err) { + err = &ConstraintError{msg: err.Error(), wrap: err} + } + _d.mutation.done = true + return affected, err +} + +// MaintenanceOperationRunDeleteOne is the builder for deleting a single MaintenanceOperationRun entity. +type MaintenanceOperationRunDeleteOne struct { + _d *MaintenanceOperationRunDelete +} + +// Where appends a list predicates to the MaintenanceOperationRunDelete builder. +func (_d *MaintenanceOperationRunDeleteOne) Where(ps ...predicate.MaintenanceOperationRun) *MaintenanceOperationRunDeleteOne { + _d._d.mutation.Where(ps...) + return _d +} + +// Exec executes the deletion query. +func (_d *MaintenanceOperationRunDeleteOne) Exec(ctx context.Context) error { + n, err := _d._d.Exec(ctx) + switch { + case err != nil: + return err + case n == 0: + return &NotFoundError{maintenanceoperationrun.Label} + default: + return nil + } +} + +// ExecX is like Exec, but panics if an error occurs. +func (_d *MaintenanceOperationRunDeleteOne) ExecX(ctx context.Context) { + if err := _d.Exec(ctx); err != nil { + panic(err) + } +} diff --git a/pkg/ent/maintenanceoperationrun_query.go b/pkg/ent/maintenanceoperationrun_query.go new file mode 100644 index 000000000..1b3f9450e --- /dev/null +++ b/pkg/ent/maintenanceoperationrun_query.go @@ -0,0 +1,565 @@ +// Code generated by ent, DO NOT EDIT. + +package ent + +import ( + "context" + "fmt" + "math" + + "entgo.io/ent" + "entgo.io/ent/dialect" + "entgo.io/ent/dialect/sql" + "entgo.io/ent/dialect/sql/sqlgraph" + "entgo.io/ent/schema/field" + "github.com/GoogleCloudPlatform/scion/pkg/ent/maintenanceoperationrun" + "github.com/GoogleCloudPlatform/scion/pkg/ent/predicate" + "github.com/google/uuid" +) + +// MaintenanceOperationRunQuery is the builder for querying MaintenanceOperationRun entities. +type MaintenanceOperationRunQuery struct { + config + ctx *QueryContext + order []maintenanceoperationrun.OrderOption + inters []Interceptor + predicates []predicate.MaintenanceOperationRun + modifiers []func(*sql.Selector) + // intermediate query (i.e. traversal path). + sql *sql.Selector + path func(context.Context) (*sql.Selector, error) +} + +// Where adds a new predicate for the MaintenanceOperationRunQuery builder. +func (_q *MaintenanceOperationRunQuery) Where(ps ...predicate.MaintenanceOperationRun) *MaintenanceOperationRunQuery { + _q.predicates = append(_q.predicates, ps...) + return _q +} + +// Limit the number of records to be returned by this query. +func (_q *MaintenanceOperationRunQuery) Limit(limit int) *MaintenanceOperationRunQuery { + _q.ctx.Limit = &limit + return _q +} + +// Offset to start from. +func (_q *MaintenanceOperationRunQuery) Offset(offset int) *MaintenanceOperationRunQuery { + _q.ctx.Offset = &offset + return _q +} + +// Unique configures the query builder to filter duplicate records on query. +// By default, unique is set to true, and can be disabled using this method. +func (_q *MaintenanceOperationRunQuery) Unique(unique bool) *MaintenanceOperationRunQuery { + _q.ctx.Unique = &unique + return _q +} + +// Order specifies how the records should be ordered. +func (_q *MaintenanceOperationRunQuery) Order(o ...maintenanceoperationrun.OrderOption) *MaintenanceOperationRunQuery { + _q.order = append(_q.order, o...) + return _q +} + +// First returns the first MaintenanceOperationRun entity from the query. +// Returns a *NotFoundError when no MaintenanceOperationRun was found. +func (_q *MaintenanceOperationRunQuery) First(ctx context.Context) (*MaintenanceOperationRun, error) { + nodes, err := _q.Limit(1).All(setContextOp(ctx, _q.ctx, ent.OpQueryFirst)) + if err != nil { + return nil, err + } + if len(nodes) == 0 { + return nil, &NotFoundError{maintenanceoperationrun.Label} + } + return nodes[0], nil +} + +// FirstX is like First, but panics if an error occurs. +func (_q *MaintenanceOperationRunQuery) FirstX(ctx context.Context) *MaintenanceOperationRun { + node, err := _q.First(ctx) + if err != nil && !IsNotFound(err) { + panic(err) + } + return node +} + +// FirstID returns the first MaintenanceOperationRun ID from the query. +// Returns a *NotFoundError when no MaintenanceOperationRun ID was found. +func (_q *MaintenanceOperationRunQuery) FirstID(ctx context.Context) (id uuid.UUID, err error) { + var ids []uuid.UUID + if ids, err = _q.Limit(1).IDs(setContextOp(ctx, _q.ctx, ent.OpQueryFirstID)); err != nil { + return + } + if len(ids) == 0 { + err = &NotFoundError{maintenanceoperationrun.Label} + return + } + return ids[0], nil +} + +// FirstIDX is like FirstID, but panics if an error occurs. +func (_q *MaintenanceOperationRunQuery) FirstIDX(ctx context.Context) uuid.UUID { + id, err := _q.FirstID(ctx) + if err != nil && !IsNotFound(err) { + panic(err) + } + return id +} + +// Only returns a single MaintenanceOperationRun entity found by the query, ensuring it only returns one. +// Returns a *NotSingularError when more than one MaintenanceOperationRun entity is found. +// Returns a *NotFoundError when no MaintenanceOperationRun entities are found. +func (_q *MaintenanceOperationRunQuery) Only(ctx context.Context) (*MaintenanceOperationRun, error) { + nodes, err := _q.Limit(2).All(setContextOp(ctx, _q.ctx, ent.OpQueryOnly)) + if err != nil { + return nil, err + } + switch len(nodes) { + case 1: + return nodes[0], nil + case 0: + return nil, &NotFoundError{maintenanceoperationrun.Label} + default: + return nil, &NotSingularError{maintenanceoperationrun.Label} + } +} + +// OnlyX is like Only, but panics if an error occurs. +func (_q *MaintenanceOperationRunQuery) OnlyX(ctx context.Context) *MaintenanceOperationRun { + node, err := _q.Only(ctx) + if err != nil { + panic(err) + } + return node +} + +// OnlyID is like Only, but returns the only MaintenanceOperationRun ID in the query. +// Returns a *NotSingularError when more than one MaintenanceOperationRun ID is found. +// Returns a *NotFoundError when no entities are found. +func (_q *MaintenanceOperationRunQuery) OnlyID(ctx context.Context) (id uuid.UUID, err error) { + var ids []uuid.UUID + if ids, err = _q.Limit(2).IDs(setContextOp(ctx, _q.ctx, ent.OpQueryOnlyID)); err != nil { + return + } + switch len(ids) { + case 1: + id = ids[0] + case 0: + err = &NotFoundError{maintenanceoperationrun.Label} + default: + err = &NotSingularError{maintenanceoperationrun.Label} + } + return +} + +// OnlyIDX is like OnlyID, but panics if an error occurs. +func (_q *MaintenanceOperationRunQuery) OnlyIDX(ctx context.Context) uuid.UUID { + id, err := _q.OnlyID(ctx) + if err != nil { + panic(err) + } + return id +} + +// All executes the query and returns a list of MaintenanceOperationRuns. +func (_q *MaintenanceOperationRunQuery) All(ctx context.Context) ([]*MaintenanceOperationRun, error) { + ctx = setContextOp(ctx, _q.ctx, ent.OpQueryAll) + if err := _q.prepareQuery(ctx); err != nil { + return nil, err + } + qr := querierAll[[]*MaintenanceOperationRun, *MaintenanceOperationRunQuery]() + return withInterceptors[[]*MaintenanceOperationRun](ctx, _q, qr, _q.inters) +} + +// AllX is like All, but panics if an error occurs. +func (_q *MaintenanceOperationRunQuery) AllX(ctx context.Context) []*MaintenanceOperationRun { + nodes, err := _q.All(ctx) + if err != nil { + panic(err) + } + return nodes +} + +// IDs executes the query and returns a list of MaintenanceOperationRun IDs. +func (_q *MaintenanceOperationRunQuery) IDs(ctx context.Context) (ids []uuid.UUID, err error) { + if _q.ctx.Unique == nil && _q.path != nil { + _q.Unique(true) + } + ctx = setContextOp(ctx, _q.ctx, ent.OpQueryIDs) + if err = _q.Select(maintenanceoperationrun.FieldID).Scan(ctx, &ids); err != nil { + return nil, err + } + return ids, nil +} + +// IDsX is like IDs, but panics if an error occurs. +func (_q *MaintenanceOperationRunQuery) IDsX(ctx context.Context) []uuid.UUID { + ids, err := _q.IDs(ctx) + if err != nil { + panic(err) + } + return ids +} + +// Count returns the count of the given query. +func (_q *MaintenanceOperationRunQuery) Count(ctx context.Context) (int, error) { + ctx = setContextOp(ctx, _q.ctx, ent.OpQueryCount) + if err := _q.prepareQuery(ctx); err != nil { + return 0, err + } + return withInterceptors[int](ctx, _q, querierCount[*MaintenanceOperationRunQuery](), _q.inters) +} + +// CountX is like Count, but panics if an error occurs. +func (_q *MaintenanceOperationRunQuery) CountX(ctx context.Context) int { + count, err := _q.Count(ctx) + if err != nil { + panic(err) + } + return count +} + +// Exist returns true if the query has elements in the graph. +func (_q *MaintenanceOperationRunQuery) Exist(ctx context.Context) (bool, error) { + ctx = setContextOp(ctx, _q.ctx, ent.OpQueryExist) + switch _, err := _q.FirstID(ctx); { + case IsNotFound(err): + return false, nil + case err != nil: + return false, fmt.Errorf("ent: check existence: %w", err) + default: + return true, nil + } +} + +// ExistX is like Exist, but panics if an error occurs. +func (_q *MaintenanceOperationRunQuery) ExistX(ctx context.Context) bool { + exist, err := _q.Exist(ctx) + if err != nil { + panic(err) + } + return exist +} + +// Clone returns a duplicate of the MaintenanceOperationRunQuery builder, including all associated steps. It can be +// used to prepare common query builders and use them differently after the clone is made. +func (_q *MaintenanceOperationRunQuery) Clone() *MaintenanceOperationRunQuery { + if _q == nil { + return nil + } + return &MaintenanceOperationRunQuery{ + config: _q.config, + ctx: _q.ctx.Clone(), + order: append([]maintenanceoperationrun.OrderOption{}, _q.order...), + inters: append([]Interceptor{}, _q.inters...), + predicates: append([]predicate.MaintenanceOperationRun{}, _q.predicates...), + // clone intermediate query. + sql: _q.sql.Clone(), + path: _q.path, + } +} + +// GroupBy is used to group vertices by one or more fields/columns. +// It is often used with aggregate functions, like: count, max, mean, min, sum. +// +// Example: +// +// var v []struct { +// OperationKey string `json:"operation_key,omitempty"` +// Count int `json:"count,omitempty"` +// } +// +// client.MaintenanceOperationRun.Query(). +// GroupBy(maintenanceoperationrun.FieldOperationKey). +// Aggregate(ent.Count()). +// Scan(ctx, &v) +func (_q *MaintenanceOperationRunQuery) GroupBy(field string, fields ...string) *MaintenanceOperationRunGroupBy { + _q.ctx.Fields = append([]string{field}, fields...) + grbuild := &MaintenanceOperationRunGroupBy{build: _q} + grbuild.flds = &_q.ctx.Fields + grbuild.label = maintenanceoperationrun.Label + grbuild.scan = grbuild.Scan + return grbuild +} + +// Select allows the selection one or more fields/columns for the given query, +// instead of selecting all fields in the entity. +// +// Example: +// +// var v []struct { +// OperationKey string `json:"operation_key,omitempty"` +// } +// +// client.MaintenanceOperationRun.Query(). +// Select(maintenanceoperationrun.FieldOperationKey). +// Scan(ctx, &v) +func (_q *MaintenanceOperationRunQuery) Select(fields ...string) *MaintenanceOperationRunSelect { + _q.ctx.Fields = append(_q.ctx.Fields, fields...) + sbuild := &MaintenanceOperationRunSelect{MaintenanceOperationRunQuery: _q} + sbuild.label = maintenanceoperationrun.Label + sbuild.flds, sbuild.scan = &_q.ctx.Fields, sbuild.Scan + return sbuild +} + +// Aggregate returns a MaintenanceOperationRunSelect configured with the given aggregations. +func (_q *MaintenanceOperationRunQuery) Aggregate(fns ...AggregateFunc) *MaintenanceOperationRunSelect { + return _q.Select().Aggregate(fns...) +} + +func (_q *MaintenanceOperationRunQuery) prepareQuery(ctx context.Context) error { + for _, inter := range _q.inters { + if inter == nil { + return fmt.Errorf("ent: uninitialized interceptor (forgotten import ent/runtime?)") + } + if trv, ok := inter.(Traverser); ok { + if err := trv.Traverse(ctx, _q); err != nil { + return err + } + } + } + for _, f := range _q.ctx.Fields { + if !maintenanceoperationrun.ValidColumn(f) { + return &ValidationError{Name: f, err: fmt.Errorf("ent: invalid field %q for query", f)} + } + } + if _q.path != nil { + prev, err := _q.path(ctx) + if err != nil { + return err + } + _q.sql = prev + } + return nil +} + +func (_q *MaintenanceOperationRunQuery) sqlAll(ctx context.Context, hooks ...queryHook) ([]*MaintenanceOperationRun, error) { + var ( + nodes = []*MaintenanceOperationRun{} + _spec = _q.querySpec() + ) + _spec.ScanValues = func(columns []string) ([]any, error) { + return (*MaintenanceOperationRun).scanValues(nil, columns) + } + _spec.Assign = func(columns []string, values []any) error { + node := &MaintenanceOperationRun{config: _q.config} + nodes = append(nodes, node) + return node.assignValues(columns, values) + } + if len(_q.modifiers) > 0 { + _spec.Modifiers = _q.modifiers + } + for i := range hooks { + hooks[i](ctx, _spec) + } + if err := sqlgraph.QueryNodes(ctx, _q.driver, _spec); err != nil { + return nil, err + } + if len(nodes) == 0 { + return nodes, nil + } + return nodes, nil +} + +func (_q *MaintenanceOperationRunQuery) sqlCount(ctx context.Context) (int, error) { + _spec := _q.querySpec() + if len(_q.modifiers) > 0 { + _spec.Modifiers = _q.modifiers + } + _spec.Node.Columns = _q.ctx.Fields + if len(_q.ctx.Fields) > 0 { + _spec.Unique = _q.ctx.Unique != nil && *_q.ctx.Unique + } + return sqlgraph.CountNodes(ctx, _q.driver, _spec) +} + +func (_q *MaintenanceOperationRunQuery) querySpec() *sqlgraph.QuerySpec { + _spec := sqlgraph.NewQuerySpec(maintenanceoperationrun.Table, maintenanceoperationrun.Columns, sqlgraph.NewFieldSpec(maintenanceoperationrun.FieldID, field.TypeUUID)) + _spec.From = _q.sql + if unique := _q.ctx.Unique; unique != nil { + _spec.Unique = *unique + } else if _q.path != nil { + _spec.Unique = true + } + if fields := _q.ctx.Fields; len(fields) > 0 { + _spec.Node.Columns = make([]string, 0, len(fields)) + _spec.Node.Columns = append(_spec.Node.Columns, maintenanceoperationrun.FieldID) + for i := range fields { + if fields[i] != maintenanceoperationrun.FieldID { + _spec.Node.Columns = append(_spec.Node.Columns, fields[i]) + } + } + } + if ps := _q.predicates; len(ps) > 0 { + _spec.Predicate = func(selector *sql.Selector) { + for i := range ps { + ps[i](selector) + } + } + } + if limit := _q.ctx.Limit; limit != nil { + _spec.Limit = *limit + } + if offset := _q.ctx.Offset; offset != nil { + _spec.Offset = *offset + } + if ps := _q.order; len(ps) > 0 { + _spec.Order = func(selector *sql.Selector) { + for i := range ps { + ps[i](selector) + } + } + } + return _spec +} + +func (_q *MaintenanceOperationRunQuery) sqlQuery(ctx context.Context) *sql.Selector { + builder := sql.Dialect(_q.driver.Dialect()) + t1 := builder.Table(maintenanceoperationrun.Table) + columns := _q.ctx.Fields + if len(columns) == 0 { + columns = maintenanceoperationrun.Columns + } + selector := builder.Select(t1.Columns(columns...)...).From(t1) + if _q.sql != nil { + selector = _q.sql + selector.Select(selector.Columns(columns...)...) + } + if _q.ctx.Unique != nil && *_q.ctx.Unique { + selector.Distinct() + } + for _, m := range _q.modifiers { + m(selector) + } + for _, p := range _q.predicates { + p(selector) + } + for _, p := range _q.order { + p(selector) + } + if offset := _q.ctx.Offset; offset != nil { + // limit is mandatory for offset clause. We start + // with default value, and override it below if needed. + selector.Offset(*offset).Limit(math.MaxInt32) + } + if limit := _q.ctx.Limit; limit != nil { + selector.Limit(*limit) + } + return selector +} + +// ForUpdate locks the selected rows against concurrent updates, and prevent them from being +// updated, deleted or "selected ... for update" by other sessions, until the transaction is +// either committed or rolled-back. +func (_q *MaintenanceOperationRunQuery) ForUpdate(opts ...sql.LockOption) *MaintenanceOperationRunQuery { + if _q.driver.Dialect() == dialect.Postgres { + _q.Unique(false) + } + _q.modifiers = append(_q.modifiers, func(s *sql.Selector) { + s.ForUpdate(opts...) + }) + return _q +} + +// ForShare behaves similarly to ForUpdate, except that it acquires a shared mode lock +// on any rows that are read. Other sessions can read the rows, but cannot modify them +// until your transaction commits. +func (_q *MaintenanceOperationRunQuery) ForShare(opts ...sql.LockOption) *MaintenanceOperationRunQuery { + if _q.driver.Dialect() == dialect.Postgres { + _q.Unique(false) + } + _q.modifiers = append(_q.modifiers, func(s *sql.Selector) { + s.ForShare(opts...) + }) + return _q +} + +// MaintenanceOperationRunGroupBy is the group-by builder for MaintenanceOperationRun entities. +type MaintenanceOperationRunGroupBy struct { + selector + build *MaintenanceOperationRunQuery +} + +// Aggregate adds the given aggregation functions to the group-by query. +func (_g *MaintenanceOperationRunGroupBy) Aggregate(fns ...AggregateFunc) *MaintenanceOperationRunGroupBy { + _g.fns = append(_g.fns, fns...) + return _g +} + +// Scan applies the selector query and scans the result into the given value. +func (_g *MaintenanceOperationRunGroupBy) Scan(ctx context.Context, v any) error { + ctx = setContextOp(ctx, _g.build.ctx, ent.OpQueryGroupBy) + if err := _g.build.prepareQuery(ctx); err != nil { + return err + } + return scanWithInterceptors[*MaintenanceOperationRunQuery, *MaintenanceOperationRunGroupBy](ctx, _g.build, _g, _g.build.inters, v) +} + +func (_g *MaintenanceOperationRunGroupBy) sqlScan(ctx context.Context, root *MaintenanceOperationRunQuery, v any) error { + selector := root.sqlQuery(ctx).Select() + aggregation := make([]string, 0, len(_g.fns)) + for _, fn := range _g.fns { + aggregation = append(aggregation, fn(selector)) + } + if len(selector.SelectedColumns()) == 0 { + columns := make([]string, 0, len(*_g.flds)+len(_g.fns)) + for _, f := range *_g.flds { + columns = append(columns, selector.C(f)) + } + columns = append(columns, aggregation...) + selector.Select(columns...) + } + selector.GroupBy(selector.Columns(*_g.flds...)...) + if err := selector.Err(); err != nil { + return err + } + rows := &sql.Rows{} + query, args := selector.Query() + if err := _g.build.driver.Query(ctx, query, args, rows); err != nil { + return err + } + defer rows.Close() + return sql.ScanSlice(rows, v) +} + +// MaintenanceOperationRunSelect is the builder for selecting fields of MaintenanceOperationRun entities. +type MaintenanceOperationRunSelect struct { + *MaintenanceOperationRunQuery + selector +} + +// Aggregate adds the given aggregation functions to the selector query. +func (_s *MaintenanceOperationRunSelect) Aggregate(fns ...AggregateFunc) *MaintenanceOperationRunSelect { + _s.fns = append(_s.fns, fns...) + return _s +} + +// Scan applies the selector query and scans the result into the given value. +func (_s *MaintenanceOperationRunSelect) Scan(ctx context.Context, v any) error { + ctx = setContextOp(ctx, _s.ctx, ent.OpQuerySelect) + if err := _s.prepareQuery(ctx); err != nil { + return err + } + return scanWithInterceptors[*MaintenanceOperationRunQuery, *MaintenanceOperationRunSelect](ctx, _s.MaintenanceOperationRunQuery, _s, _s.inters, v) +} + +func (_s *MaintenanceOperationRunSelect) sqlScan(ctx context.Context, root *MaintenanceOperationRunQuery, v any) error { + selector := root.sqlQuery(ctx) + aggregation := make([]string, 0, len(_s.fns)) + for _, fn := range _s.fns { + aggregation = append(aggregation, fn(selector)) + } + switch n := len(*_s.selector.flds); { + case n == 0 && len(aggregation) > 0: + selector.Select(aggregation...) + case n != 0 && len(aggregation) > 0: + selector.AppendSelect(aggregation...) + } + rows := &sql.Rows{} + query, args := selector.Query() + if err := _s.driver.Query(ctx, query, args, rows); err != nil { + return err + } + defer rows.Close() + return sql.ScanSlice(rows, v) +} diff --git a/pkg/ent/maintenanceoperationrun_update.go b/pkg/ent/maintenanceoperationrun_update.go new file mode 100644 index 000000000..aaf8065f7 --- /dev/null +++ b/pkg/ent/maintenanceoperationrun_update.go @@ -0,0 +1,460 @@ +// Code generated by ent, DO NOT EDIT. + +package ent + +import ( + "context" + "errors" + "fmt" + "time" + + "entgo.io/ent/dialect/sql" + "entgo.io/ent/dialect/sql/sqlgraph" + "entgo.io/ent/schema/field" + "github.com/GoogleCloudPlatform/scion/pkg/ent/maintenanceoperationrun" + "github.com/GoogleCloudPlatform/scion/pkg/ent/predicate" +) + +// MaintenanceOperationRunUpdate is the builder for updating MaintenanceOperationRun entities. +type MaintenanceOperationRunUpdate struct { + config + hooks []Hook + mutation *MaintenanceOperationRunMutation +} + +// Where appends a list predicates to the MaintenanceOperationRunUpdate builder. +func (_u *MaintenanceOperationRunUpdate) Where(ps ...predicate.MaintenanceOperationRun) *MaintenanceOperationRunUpdate { + _u.mutation.Where(ps...) + return _u +} + +// SetOperationKey sets the "operation_key" field. +func (_u *MaintenanceOperationRunUpdate) SetOperationKey(v string) *MaintenanceOperationRunUpdate { + _u.mutation.SetOperationKey(v) + return _u +} + +// SetNillableOperationKey sets the "operation_key" field if the given value is not nil. +func (_u *MaintenanceOperationRunUpdate) SetNillableOperationKey(v *string) *MaintenanceOperationRunUpdate { + if v != nil { + _u.SetOperationKey(*v) + } + return _u +} + +// SetStatus sets the "status" field. +func (_u *MaintenanceOperationRunUpdate) SetStatus(v string) *MaintenanceOperationRunUpdate { + _u.mutation.SetStatus(v) + return _u +} + +// SetNillableStatus sets the "status" field if the given value is not nil. +func (_u *MaintenanceOperationRunUpdate) SetNillableStatus(v *string) *MaintenanceOperationRunUpdate { + if v != nil { + _u.SetStatus(*v) + } + return _u +} + +// SetCompletedAt sets the "completed_at" field. +func (_u *MaintenanceOperationRunUpdate) SetCompletedAt(v time.Time) *MaintenanceOperationRunUpdate { + _u.mutation.SetCompletedAt(v) + return _u +} + +// SetNillableCompletedAt sets the "completed_at" field if the given value is not nil. +func (_u *MaintenanceOperationRunUpdate) SetNillableCompletedAt(v *time.Time) *MaintenanceOperationRunUpdate { + if v != nil { + _u.SetCompletedAt(*v) + } + return _u +} + +// ClearCompletedAt clears the value of the "completed_at" field. +func (_u *MaintenanceOperationRunUpdate) ClearCompletedAt() *MaintenanceOperationRunUpdate { + _u.mutation.ClearCompletedAt() + return _u +} + +// SetStartedBy sets the "started_by" field. +func (_u *MaintenanceOperationRunUpdate) SetStartedBy(v string) *MaintenanceOperationRunUpdate { + _u.mutation.SetStartedBy(v) + return _u +} + +// SetNillableStartedBy sets the "started_by" field if the given value is not nil. +func (_u *MaintenanceOperationRunUpdate) SetNillableStartedBy(v *string) *MaintenanceOperationRunUpdate { + if v != nil { + _u.SetStartedBy(*v) + } + return _u +} + +// ClearStartedBy clears the value of the "started_by" field. +func (_u *MaintenanceOperationRunUpdate) ClearStartedBy() *MaintenanceOperationRunUpdate { + _u.mutation.ClearStartedBy() + return _u +} + +// SetResult sets the "result" field. +func (_u *MaintenanceOperationRunUpdate) SetResult(v string) *MaintenanceOperationRunUpdate { + _u.mutation.SetResult(v) + return _u +} + +// SetNillableResult sets the "result" field if the given value is not nil. +func (_u *MaintenanceOperationRunUpdate) SetNillableResult(v *string) *MaintenanceOperationRunUpdate { + if v != nil { + _u.SetResult(*v) + } + return _u +} + +// ClearResult clears the value of the "result" field. +func (_u *MaintenanceOperationRunUpdate) ClearResult() *MaintenanceOperationRunUpdate { + _u.mutation.ClearResult() + return _u +} + +// SetLog sets the "log" field. +func (_u *MaintenanceOperationRunUpdate) SetLog(v string) *MaintenanceOperationRunUpdate { + _u.mutation.SetLog(v) + return _u +} + +// SetNillableLog sets the "log" field if the given value is not nil. +func (_u *MaintenanceOperationRunUpdate) SetNillableLog(v *string) *MaintenanceOperationRunUpdate { + if v != nil { + _u.SetLog(*v) + } + return _u +} + +// Mutation returns the MaintenanceOperationRunMutation object of the builder. +func (_u *MaintenanceOperationRunUpdate) Mutation() *MaintenanceOperationRunMutation { + return _u.mutation +} + +// Save executes the query and returns the number of nodes affected by the update operation. +func (_u *MaintenanceOperationRunUpdate) Save(ctx context.Context) (int, error) { + return withHooks(ctx, _u.sqlSave, _u.mutation, _u.hooks) +} + +// SaveX is like Save, but panics if an error occurs. +func (_u *MaintenanceOperationRunUpdate) SaveX(ctx context.Context) int { + affected, err := _u.Save(ctx) + if err != nil { + panic(err) + } + return affected +} + +// Exec executes the query. +func (_u *MaintenanceOperationRunUpdate) Exec(ctx context.Context) error { + _, err := _u.Save(ctx) + return err +} + +// ExecX is like Exec, but panics if an error occurs. +func (_u *MaintenanceOperationRunUpdate) ExecX(ctx context.Context) { + if err := _u.Exec(ctx); err != nil { + panic(err) + } +} + +// check runs all checks and user-defined validators on the builder. +func (_u *MaintenanceOperationRunUpdate) check() error { + if v, ok := _u.mutation.OperationKey(); ok { + if err := maintenanceoperationrun.OperationKeyValidator(v); err != nil { + return &ValidationError{Name: "operation_key", err: fmt.Errorf(`ent: validator failed for field "MaintenanceOperationRun.operation_key": %w`, err)} + } + } + return nil +} + +func (_u *MaintenanceOperationRunUpdate) sqlSave(ctx context.Context) (_node int, err error) { + if err := _u.check(); err != nil { + return _node, err + } + _spec := sqlgraph.NewUpdateSpec(maintenanceoperationrun.Table, maintenanceoperationrun.Columns, sqlgraph.NewFieldSpec(maintenanceoperationrun.FieldID, field.TypeUUID)) + if ps := _u.mutation.predicates; len(ps) > 0 { + _spec.Predicate = func(selector *sql.Selector) { + for i := range ps { + ps[i](selector) + } + } + } + if value, ok := _u.mutation.OperationKey(); ok { + _spec.SetField(maintenanceoperationrun.FieldOperationKey, field.TypeString, value) + } + if value, ok := _u.mutation.Status(); ok { + _spec.SetField(maintenanceoperationrun.FieldStatus, field.TypeString, value) + } + if value, ok := _u.mutation.CompletedAt(); ok { + _spec.SetField(maintenanceoperationrun.FieldCompletedAt, field.TypeTime, value) + } + if _u.mutation.CompletedAtCleared() { + _spec.ClearField(maintenanceoperationrun.FieldCompletedAt, field.TypeTime) + } + if value, ok := _u.mutation.StartedBy(); ok { + _spec.SetField(maintenanceoperationrun.FieldStartedBy, field.TypeString, value) + } + if _u.mutation.StartedByCleared() { + _spec.ClearField(maintenanceoperationrun.FieldStartedBy, field.TypeString) + } + if value, ok := _u.mutation.Result(); ok { + _spec.SetField(maintenanceoperationrun.FieldResult, field.TypeString, value) + } + if _u.mutation.ResultCleared() { + _spec.ClearField(maintenanceoperationrun.FieldResult, field.TypeString) + } + if value, ok := _u.mutation.Log(); ok { + _spec.SetField(maintenanceoperationrun.FieldLog, field.TypeString, value) + } + if _node, err = sqlgraph.UpdateNodes(ctx, _u.driver, _spec); err != nil { + if _, ok := err.(*sqlgraph.NotFoundError); ok { + err = &NotFoundError{maintenanceoperationrun.Label} + } else if sqlgraph.IsConstraintError(err) { + err = &ConstraintError{msg: err.Error(), wrap: err} + } + return 0, err + } + _u.mutation.done = true + return _node, nil +} + +// MaintenanceOperationRunUpdateOne is the builder for updating a single MaintenanceOperationRun entity. +type MaintenanceOperationRunUpdateOne struct { + config + fields []string + hooks []Hook + mutation *MaintenanceOperationRunMutation +} + +// SetOperationKey sets the "operation_key" field. +func (_u *MaintenanceOperationRunUpdateOne) SetOperationKey(v string) *MaintenanceOperationRunUpdateOne { + _u.mutation.SetOperationKey(v) + return _u +} + +// SetNillableOperationKey sets the "operation_key" field if the given value is not nil. +func (_u *MaintenanceOperationRunUpdateOne) SetNillableOperationKey(v *string) *MaintenanceOperationRunUpdateOne { + if v != nil { + _u.SetOperationKey(*v) + } + return _u +} + +// SetStatus sets the "status" field. +func (_u *MaintenanceOperationRunUpdateOne) SetStatus(v string) *MaintenanceOperationRunUpdateOne { + _u.mutation.SetStatus(v) + return _u +} + +// SetNillableStatus sets the "status" field if the given value is not nil. +func (_u *MaintenanceOperationRunUpdateOne) SetNillableStatus(v *string) *MaintenanceOperationRunUpdateOne { + if v != nil { + _u.SetStatus(*v) + } + return _u +} + +// SetCompletedAt sets the "completed_at" field. +func (_u *MaintenanceOperationRunUpdateOne) SetCompletedAt(v time.Time) *MaintenanceOperationRunUpdateOne { + _u.mutation.SetCompletedAt(v) + return _u +} + +// SetNillableCompletedAt sets the "completed_at" field if the given value is not nil. +func (_u *MaintenanceOperationRunUpdateOne) SetNillableCompletedAt(v *time.Time) *MaintenanceOperationRunUpdateOne { + if v != nil { + _u.SetCompletedAt(*v) + } + return _u +} + +// ClearCompletedAt clears the value of the "completed_at" field. +func (_u *MaintenanceOperationRunUpdateOne) ClearCompletedAt() *MaintenanceOperationRunUpdateOne { + _u.mutation.ClearCompletedAt() + return _u +} + +// SetStartedBy sets the "started_by" field. +func (_u *MaintenanceOperationRunUpdateOne) SetStartedBy(v string) *MaintenanceOperationRunUpdateOne { + _u.mutation.SetStartedBy(v) + return _u +} + +// SetNillableStartedBy sets the "started_by" field if the given value is not nil. +func (_u *MaintenanceOperationRunUpdateOne) SetNillableStartedBy(v *string) *MaintenanceOperationRunUpdateOne { + if v != nil { + _u.SetStartedBy(*v) + } + return _u +} + +// ClearStartedBy clears the value of the "started_by" field. +func (_u *MaintenanceOperationRunUpdateOne) ClearStartedBy() *MaintenanceOperationRunUpdateOne { + _u.mutation.ClearStartedBy() + return _u +} + +// SetResult sets the "result" field. +func (_u *MaintenanceOperationRunUpdateOne) SetResult(v string) *MaintenanceOperationRunUpdateOne { + _u.mutation.SetResult(v) + return _u +} + +// SetNillableResult sets the "result" field if the given value is not nil. +func (_u *MaintenanceOperationRunUpdateOne) SetNillableResult(v *string) *MaintenanceOperationRunUpdateOne { + if v != nil { + _u.SetResult(*v) + } + return _u +} + +// ClearResult clears the value of the "result" field. +func (_u *MaintenanceOperationRunUpdateOne) ClearResult() *MaintenanceOperationRunUpdateOne { + _u.mutation.ClearResult() + return _u +} + +// SetLog sets the "log" field. +func (_u *MaintenanceOperationRunUpdateOne) SetLog(v string) *MaintenanceOperationRunUpdateOne { + _u.mutation.SetLog(v) + return _u +} + +// SetNillableLog sets the "log" field if the given value is not nil. +func (_u *MaintenanceOperationRunUpdateOne) SetNillableLog(v *string) *MaintenanceOperationRunUpdateOne { + if v != nil { + _u.SetLog(*v) + } + return _u +} + +// Mutation returns the MaintenanceOperationRunMutation object of the builder. +func (_u *MaintenanceOperationRunUpdateOne) Mutation() *MaintenanceOperationRunMutation { + return _u.mutation +} + +// Where appends a list predicates to the MaintenanceOperationRunUpdate builder. +func (_u *MaintenanceOperationRunUpdateOne) Where(ps ...predicate.MaintenanceOperationRun) *MaintenanceOperationRunUpdateOne { + _u.mutation.Where(ps...) + return _u +} + +// Select allows selecting one or more fields (columns) of the returned entity. +// The default is selecting all fields defined in the entity schema. +func (_u *MaintenanceOperationRunUpdateOne) Select(field string, fields ...string) *MaintenanceOperationRunUpdateOne { + _u.fields = append([]string{field}, fields...) + return _u +} + +// Save executes the query and returns the updated MaintenanceOperationRun entity. +func (_u *MaintenanceOperationRunUpdateOne) Save(ctx context.Context) (*MaintenanceOperationRun, error) { + return withHooks(ctx, _u.sqlSave, _u.mutation, _u.hooks) +} + +// SaveX is like Save, but panics if an error occurs. +func (_u *MaintenanceOperationRunUpdateOne) SaveX(ctx context.Context) *MaintenanceOperationRun { + node, err := _u.Save(ctx) + if err != nil { + panic(err) + } + return node +} + +// Exec executes the query on the entity. +func (_u *MaintenanceOperationRunUpdateOne) Exec(ctx context.Context) error { + _, err := _u.Save(ctx) + return err +} + +// ExecX is like Exec, but panics if an error occurs. +func (_u *MaintenanceOperationRunUpdateOne) ExecX(ctx context.Context) { + if err := _u.Exec(ctx); err != nil { + panic(err) + } +} + +// check runs all checks and user-defined validators on the builder. +func (_u *MaintenanceOperationRunUpdateOne) check() error { + if v, ok := _u.mutation.OperationKey(); ok { + if err := maintenanceoperationrun.OperationKeyValidator(v); err != nil { + return &ValidationError{Name: "operation_key", err: fmt.Errorf(`ent: validator failed for field "MaintenanceOperationRun.operation_key": %w`, err)} + } + } + return nil +} + +func (_u *MaintenanceOperationRunUpdateOne) sqlSave(ctx context.Context) (_node *MaintenanceOperationRun, err error) { + if err := _u.check(); err != nil { + return _node, err + } + _spec := sqlgraph.NewUpdateSpec(maintenanceoperationrun.Table, maintenanceoperationrun.Columns, sqlgraph.NewFieldSpec(maintenanceoperationrun.FieldID, field.TypeUUID)) + id, ok := _u.mutation.ID() + if !ok { + return nil, &ValidationError{Name: "id", err: errors.New(`ent: missing "MaintenanceOperationRun.id" for update`)} + } + _spec.Node.ID.Value = id + if fields := _u.fields; len(fields) > 0 { + _spec.Node.Columns = make([]string, 0, len(fields)) + _spec.Node.Columns = append(_spec.Node.Columns, maintenanceoperationrun.FieldID) + for _, f := range fields { + if !maintenanceoperationrun.ValidColumn(f) { + return nil, &ValidationError{Name: f, err: fmt.Errorf("ent: invalid field %q for query", f)} + } + if f != maintenanceoperationrun.FieldID { + _spec.Node.Columns = append(_spec.Node.Columns, f) + } + } + } + if ps := _u.mutation.predicates; len(ps) > 0 { + _spec.Predicate = func(selector *sql.Selector) { + for i := range ps { + ps[i](selector) + } + } + } + if value, ok := _u.mutation.OperationKey(); ok { + _spec.SetField(maintenanceoperationrun.FieldOperationKey, field.TypeString, value) + } + if value, ok := _u.mutation.Status(); ok { + _spec.SetField(maintenanceoperationrun.FieldStatus, field.TypeString, value) + } + if value, ok := _u.mutation.CompletedAt(); ok { + _spec.SetField(maintenanceoperationrun.FieldCompletedAt, field.TypeTime, value) + } + if _u.mutation.CompletedAtCleared() { + _spec.ClearField(maintenanceoperationrun.FieldCompletedAt, field.TypeTime) + } + if value, ok := _u.mutation.StartedBy(); ok { + _spec.SetField(maintenanceoperationrun.FieldStartedBy, field.TypeString, value) + } + if _u.mutation.StartedByCleared() { + _spec.ClearField(maintenanceoperationrun.FieldStartedBy, field.TypeString) + } + if value, ok := _u.mutation.Result(); ok { + _spec.SetField(maintenanceoperationrun.FieldResult, field.TypeString, value) + } + if _u.mutation.ResultCleared() { + _spec.ClearField(maintenanceoperationrun.FieldResult, field.TypeString) + } + if value, ok := _u.mutation.Log(); ok { + _spec.SetField(maintenanceoperationrun.FieldLog, field.TypeString, value) + } + _node = &MaintenanceOperationRun{config: _u.config} + _spec.Assign = _node.assignValues + _spec.ScanValues = _node.scanValues + if err = sqlgraph.UpdateNode(ctx, _u.driver, _spec); err != nil { + if _, ok := err.(*sqlgraph.NotFoundError); ok { + err = &NotFoundError{maintenanceoperationrun.Label} + } else if sqlgraph.IsConstraintError(err) { + err = &ConstraintError{msg: err.Error(), wrap: err} + } + return nil, err + } + _u.mutation.done = true + return _node, nil +} diff --git a/pkg/ent/message.go b/pkg/ent/message.go new file mode 100644 index 000000000..31cf52e9b --- /dev/null +++ b/pkg/ent/message.go @@ -0,0 +1,266 @@ +// Code generated by ent, DO NOT EDIT. + +package ent + +import ( + "fmt" + "strings" + "time" + + "entgo.io/ent" + "entgo.io/ent/dialect/sql" + "github.com/GoogleCloudPlatform/scion/pkg/ent/message" + "github.com/google/uuid" +) + +// Message is the model entity for the Message schema. +type Message struct { + config `json:"-"` + // ID of the ent. + ID uuid.UUID `json:"id,omitempty"` + // ProjectID holds the value of the "project_id" field. + ProjectID uuid.UUID `json:"project_id,omitempty"` + // Sender holds the value of the "sender" field. + Sender string `json:"sender,omitempty"` + // SenderID holds the value of the "sender_id" field. + SenderID string `json:"sender_id,omitempty"` + // Recipient holds the value of the "recipient" field. + Recipient string `json:"recipient,omitempty"` + // RecipientID holds the value of the "recipient_id" field. + RecipientID string `json:"recipient_id,omitempty"` + // Msg holds the value of the "msg" field. + Msg string `json:"msg,omitempty"` + // Type holds the value of the "type" field. + Type string `json:"type,omitempty"` + // Urgent holds the value of the "urgent" field. + Urgent bool `json:"urgent,omitempty"` + // Broadcasted holds the value of the "broadcasted" field. + Broadcasted bool `json:"broadcasted,omitempty"` + // Read holds the value of the "read" field. + Read bool `json:"read,omitempty"` + // AgentID holds the value of the "agent_id" field. + AgentID string `json:"agent_id,omitempty"` + // GroupID holds the value of the "group_id" field. + GroupID string `json:"group_id,omitempty"` + // DispatchState holds the value of the "dispatch_state" field. + DispatchState string `json:"dispatch_state,omitempty"` + // DispatchedAt holds the value of the "dispatched_at" field. + DispatchedAt *time.Time `json:"dispatched_at,omitempty"` + // Created holds the value of the "created" field. + Created time.Time `json:"created,omitempty"` + selectValues sql.SelectValues +} + +// scanValues returns the types for scanning values from sql.Rows. +func (*Message) scanValues(columns []string) ([]any, error) { + values := make([]any, len(columns)) + for i := range columns { + switch columns[i] { + case message.FieldUrgent, message.FieldBroadcasted, message.FieldRead: + values[i] = new(sql.NullBool) + case message.FieldSender, message.FieldSenderID, message.FieldRecipient, message.FieldRecipientID, message.FieldMsg, message.FieldType, message.FieldAgentID, message.FieldGroupID, message.FieldDispatchState: + values[i] = new(sql.NullString) + case message.FieldDispatchedAt, message.FieldCreated: + values[i] = new(sql.NullTime) + case message.FieldID, message.FieldProjectID: + values[i] = new(uuid.UUID) + default: + values[i] = new(sql.UnknownType) + } + } + return values, nil +} + +// assignValues assigns the values that were returned from sql.Rows (after scanning) +// to the Message fields. +func (_m *Message) assignValues(columns []string, values []any) error { + if m, n := len(values), len(columns); m < n { + return fmt.Errorf("mismatch number of scan values: %d != %d", m, n) + } + for i := range columns { + switch columns[i] { + case message.FieldID: + if value, ok := values[i].(*uuid.UUID); !ok { + return fmt.Errorf("unexpected type %T for field id", values[i]) + } else if value != nil { + _m.ID = *value + } + case message.FieldProjectID: + if value, ok := values[i].(*uuid.UUID); !ok { + return fmt.Errorf("unexpected type %T for field project_id", values[i]) + } else if value != nil { + _m.ProjectID = *value + } + case message.FieldSender: + if value, ok := values[i].(*sql.NullString); !ok { + return fmt.Errorf("unexpected type %T for field sender", values[i]) + } else if value.Valid { + _m.Sender = value.String + } + case message.FieldSenderID: + if value, ok := values[i].(*sql.NullString); !ok { + return fmt.Errorf("unexpected type %T for field sender_id", values[i]) + } else if value.Valid { + _m.SenderID = value.String + } + case message.FieldRecipient: + if value, ok := values[i].(*sql.NullString); !ok { + return fmt.Errorf("unexpected type %T for field recipient", values[i]) + } else if value.Valid { + _m.Recipient = value.String + } + case message.FieldRecipientID: + if value, ok := values[i].(*sql.NullString); !ok { + return fmt.Errorf("unexpected type %T for field recipient_id", values[i]) + } else if value.Valid { + _m.RecipientID = value.String + } + case message.FieldMsg: + if value, ok := values[i].(*sql.NullString); !ok { + return fmt.Errorf("unexpected type %T for field msg", values[i]) + } else if value.Valid { + _m.Msg = value.String + } + case message.FieldType: + if value, ok := values[i].(*sql.NullString); !ok { + return fmt.Errorf("unexpected type %T for field type", values[i]) + } else if value.Valid { + _m.Type = value.String + } + case message.FieldUrgent: + if value, ok := values[i].(*sql.NullBool); !ok { + return fmt.Errorf("unexpected type %T for field urgent", values[i]) + } else if value.Valid { + _m.Urgent = value.Bool + } + case message.FieldBroadcasted: + if value, ok := values[i].(*sql.NullBool); !ok { + return fmt.Errorf("unexpected type %T for field broadcasted", values[i]) + } else if value.Valid { + _m.Broadcasted = value.Bool + } + case message.FieldRead: + if value, ok := values[i].(*sql.NullBool); !ok { + return fmt.Errorf("unexpected type %T for field read", values[i]) + } else if value.Valid { + _m.Read = value.Bool + } + case message.FieldAgentID: + if value, ok := values[i].(*sql.NullString); !ok { + return fmt.Errorf("unexpected type %T for field agent_id", values[i]) + } else if value.Valid { + _m.AgentID = value.String + } + case message.FieldGroupID: + if value, ok := values[i].(*sql.NullString); !ok { + return fmt.Errorf("unexpected type %T for field group_id", values[i]) + } else if value.Valid { + _m.GroupID = value.String + } + case message.FieldDispatchState: + if value, ok := values[i].(*sql.NullString); !ok { + return fmt.Errorf("unexpected type %T for field dispatch_state", values[i]) + } else if value.Valid { + _m.DispatchState = value.String + } + case message.FieldDispatchedAt: + if value, ok := values[i].(*sql.NullTime); !ok { + return fmt.Errorf("unexpected type %T for field dispatched_at", values[i]) + } else if value.Valid { + _m.DispatchedAt = new(time.Time) + *_m.DispatchedAt = value.Time + } + case message.FieldCreated: + if value, ok := values[i].(*sql.NullTime); !ok { + return fmt.Errorf("unexpected type %T for field created", values[i]) + } else if value.Valid { + _m.Created = value.Time + } + default: + _m.selectValues.Set(columns[i], values[i]) + } + } + return nil +} + +// Value returns the ent.Value that was dynamically selected and assigned to the Message. +// This includes values selected through modifiers, order, etc. +func (_m *Message) Value(name string) (ent.Value, error) { + return _m.selectValues.Get(name) +} + +// Update returns a builder for updating this Message. +// Note that you need to call Message.Unwrap() before calling this method if this Message +// was returned from a transaction, and the transaction was committed or rolled back. +func (_m *Message) Update() *MessageUpdateOne { + return NewMessageClient(_m.config).UpdateOne(_m) +} + +// Unwrap unwraps the Message entity that was returned from a transaction after it was closed, +// so that all future queries will be executed through the driver which created the transaction. +func (_m *Message) Unwrap() *Message { + _tx, ok := _m.config.driver.(*txDriver) + if !ok { + panic("ent: Message is not a transactional entity") + } + _m.config.driver = _tx.drv + return _m +} + +// String implements the fmt.Stringer. +func (_m *Message) String() string { + var builder strings.Builder + builder.WriteString("Message(") + builder.WriteString(fmt.Sprintf("id=%v, ", _m.ID)) + builder.WriteString("project_id=") + builder.WriteString(fmt.Sprintf("%v", _m.ProjectID)) + builder.WriteString(", ") + builder.WriteString("sender=") + builder.WriteString(_m.Sender) + builder.WriteString(", ") + builder.WriteString("sender_id=") + builder.WriteString(_m.SenderID) + builder.WriteString(", ") + builder.WriteString("recipient=") + builder.WriteString(_m.Recipient) + builder.WriteString(", ") + builder.WriteString("recipient_id=") + builder.WriteString(_m.RecipientID) + builder.WriteString(", ") + builder.WriteString("msg=") + builder.WriteString(_m.Msg) + builder.WriteString(", ") + builder.WriteString("type=") + builder.WriteString(_m.Type) + builder.WriteString(", ") + builder.WriteString("urgent=") + builder.WriteString(fmt.Sprintf("%v", _m.Urgent)) + builder.WriteString(", ") + builder.WriteString("broadcasted=") + builder.WriteString(fmt.Sprintf("%v", _m.Broadcasted)) + builder.WriteString(", ") + builder.WriteString("read=") + builder.WriteString(fmt.Sprintf("%v", _m.Read)) + builder.WriteString(", ") + builder.WriteString("agent_id=") + builder.WriteString(_m.AgentID) + builder.WriteString(", ") + builder.WriteString("group_id=") + builder.WriteString(_m.GroupID) + builder.WriteString(", ") + builder.WriteString("dispatch_state=") + builder.WriteString(_m.DispatchState) + builder.WriteString(", ") + if v := _m.DispatchedAt; v != nil { + builder.WriteString("dispatched_at=") + builder.WriteString(v.Format(time.ANSIC)) + } + builder.WriteString(", ") + builder.WriteString("created=") + builder.WriteString(_m.Created.Format(time.ANSIC)) + builder.WriteByte(')') + return builder.String() +} + +// Messages is a parsable slice of Message. +type Messages []*Message diff --git a/pkg/ent/message/message.go b/pkg/ent/message/message.go new file mode 100644 index 000000000..c93f6e82c --- /dev/null +++ b/pkg/ent/message/message.go @@ -0,0 +1,185 @@ +// Code generated by ent, DO NOT EDIT. + +package message + +import ( + "time" + + "entgo.io/ent/dialect/sql" + "github.com/google/uuid" +) + +const ( + // Label holds the string label denoting the message type in the database. + Label = "message" + // FieldID holds the string denoting the id field in the database. + FieldID = "id" + // FieldProjectID holds the string denoting the project_id field in the database. + FieldProjectID = "project_id" + // FieldSender holds the string denoting the sender field in the database. + FieldSender = "sender" + // FieldSenderID holds the string denoting the sender_id field in the database. + FieldSenderID = "sender_id" + // FieldRecipient holds the string denoting the recipient field in the database. + FieldRecipient = "recipient" + // FieldRecipientID holds the string denoting the recipient_id field in the database. + FieldRecipientID = "recipient_id" + // FieldMsg holds the string denoting the msg field in the database. + FieldMsg = "msg" + // FieldType holds the string denoting the type field in the database. + FieldType = "type" + // FieldUrgent holds the string denoting the urgent field in the database. + FieldUrgent = "urgent" + // FieldBroadcasted holds the string denoting the broadcasted field in the database. + FieldBroadcasted = "broadcasted" + // FieldRead holds the string denoting the read field in the database. + FieldRead = "read" + // FieldAgentID holds the string denoting the agent_id field in the database. + FieldAgentID = "agent_id" + // FieldGroupID holds the string denoting the group_id field in the database. + FieldGroupID = "group_id" + // FieldDispatchState holds the string denoting the dispatch_state field in the database. + FieldDispatchState = "dispatch_state" + // FieldDispatchedAt holds the string denoting the dispatched_at field in the database. + FieldDispatchedAt = "dispatched_at" + // FieldCreated holds the string denoting the created field in the database. + FieldCreated = "created" + // Table holds the table name of the message in the database. + Table = "messages" +) + +// Columns holds all SQL columns for message fields. +var Columns = []string{ + FieldID, + FieldProjectID, + FieldSender, + FieldSenderID, + FieldRecipient, + FieldRecipientID, + FieldMsg, + FieldType, + FieldUrgent, + FieldBroadcasted, + FieldRead, + FieldAgentID, + FieldGroupID, + FieldDispatchState, + FieldDispatchedAt, + FieldCreated, +} + +// ValidColumn reports if the column name is valid (part of the table columns). +func ValidColumn(column string) bool { + for i := range Columns { + if column == Columns[i] { + return true + } + } + return false +} + +var ( + // SenderValidator is a validator for the "sender" field. It is called by the builders before save. + SenderValidator func(string) error + // RecipientValidator is a validator for the "recipient" field. It is called by the builders before save. + RecipientValidator func(string) error + // MsgValidator is a validator for the "msg" field. It is called by the builders before save. + MsgValidator func(string) error + // DefaultType holds the default value on creation for the "type" field. + DefaultType string + // DefaultUrgent holds the default value on creation for the "urgent" field. + DefaultUrgent bool + // DefaultBroadcasted holds the default value on creation for the "broadcasted" field. + DefaultBroadcasted bool + // DefaultRead holds the default value on creation for the "read" field. + DefaultRead bool + // DefaultDispatchState holds the default value on creation for the "dispatch_state" field. + DefaultDispatchState string + // DefaultCreated holds the default value on creation for the "created" field. + DefaultCreated func() time.Time + // DefaultID holds the default value on creation for the "id" field. + DefaultID func() uuid.UUID +) + +// OrderOption defines the ordering options for the Message queries. +type OrderOption func(*sql.Selector) + +// ByID orders the results by the id field. +func ByID(opts ...sql.OrderTermOption) OrderOption { + return sql.OrderByField(FieldID, opts...).ToFunc() +} + +// ByProjectID orders the results by the project_id field. +func ByProjectID(opts ...sql.OrderTermOption) OrderOption { + return sql.OrderByField(FieldProjectID, opts...).ToFunc() +} + +// BySender orders the results by the sender field. +func BySender(opts ...sql.OrderTermOption) OrderOption { + return sql.OrderByField(FieldSender, opts...).ToFunc() +} + +// BySenderID orders the results by the sender_id field. +func BySenderID(opts ...sql.OrderTermOption) OrderOption { + return sql.OrderByField(FieldSenderID, opts...).ToFunc() +} + +// ByRecipient orders the results by the recipient field. +func ByRecipient(opts ...sql.OrderTermOption) OrderOption { + return sql.OrderByField(FieldRecipient, opts...).ToFunc() +} + +// ByRecipientID orders the results by the recipient_id field. +func ByRecipientID(opts ...sql.OrderTermOption) OrderOption { + return sql.OrderByField(FieldRecipientID, opts...).ToFunc() +} + +// ByMsg orders the results by the msg field. +func ByMsg(opts ...sql.OrderTermOption) OrderOption { + return sql.OrderByField(FieldMsg, opts...).ToFunc() +} + +// ByType orders the results by the type field. +func ByType(opts ...sql.OrderTermOption) OrderOption { + return sql.OrderByField(FieldType, opts...).ToFunc() +} + +// ByUrgent orders the results by the urgent field. +func ByUrgent(opts ...sql.OrderTermOption) OrderOption { + return sql.OrderByField(FieldUrgent, opts...).ToFunc() +} + +// ByBroadcasted orders the results by the broadcasted field. +func ByBroadcasted(opts ...sql.OrderTermOption) OrderOption { + return sql.OrderByField(FieldBroadcasted, opts...).ToFunc() +} + +// ByRead orders the results by the read field. +func ByRead(opts ...sql.OrderTermOption) OrderOption { + return sql.OrderByField(FieldRead, opts...).ToFunc() +} + +// ByAgentID orders the results by the agent_id field. +func ByAgentID(opts ...sql.OrderTermOption) OrderOption { + return sql.OrderByField(FieldAgentID, opts...).ToFunc() +} + +// ByGroupID orders the results by the group_id field. +func ByGroupID(opts ...sql.OrderTermOption) OrderOption { + return sql.OrderByField(FieldGroupID, opts...).ToFunc() +} + +// ByDispatchState orders the results by the dispatch_state field. +func ByDispatchState(opts ...sql.OrderTermOption) OrderOption { + return sql.OrderByField(FieldDispatchState, opts...).ToFunc() +} + +// ByDispatchedAt orders the results by the dispatched_at field. +func ByDispatchedAt(opts ...sql.OrderTermOption) OrderOption { + return sql.OrderByField(FieldDispatchedAt, opts...).ToFunc() +} + +// ByCreated orders the results by the created field. +func ByCreated(opts ...sql.OrderTermOption) OrderOption { + return sql.OrderByField(FieldCreated, opts...).ToFunc() +} diff --git a/pkg/ent/message/where.go b/pkg/ent/message/where.go new file mode 100644 index 000000000..d38547160 --- /dev/null +++ b/pkg/ent/message/where.go @@ -0,0 +1,931 @@ +// Code generated by ent, DO NOT EDIT. + +package message + +import ( + "time" + + "entgo.io/ent/dialect/sql" + "github.com/GoogleCloudPlatform/scion/pkg/ent/predicate" + "github.com/google/uuid" +) + +// ID filters vertices based on their ID field. +func ID(id uuid.UUID) predicate.Message { + return predicate.Message(sql.FieldEQ(FieldID, id)) +} + +// IDEQ applies the EQ predicate on the ID field. +func IDEQ(id uuid.UUID) predicate.Message { + return predicate.Message(sql.FieldEQ(FieldID, id)) +} + +// IDNEQ applies the NEQ predicate on the ID field. +func IDNEQ(id uuid.UUID) predicate.Message { + return predicate.Message(sql.FieldNEQ(FieldID, id)) +} + +// IDIn applies the In predicate on the ID field. +func IDIn(ids ...uuid.UUID) predicate.Message { + return predicate.Message(sql.FieldIn(FieldID, ids...)) +} + +// IDNotIn applies the NotIn predicate on the ID field. +func IDNotIn(ids ...uuid.UUID) predicate.Message { + return predicate.Message(sql.FieldNotIn(FieldID, ids...)) +} + +// IDGT applies the GT predicate on the ID field. +func IDGT(id uuid.UUID) predicate.Message { + return predicate.Message(sql.FieldGT(FieldID, id)) +} + +// IDGTE applies the GTE predicate on the ID field. +func IDGTE(id uuid.UUID) predicate.Message { + return predicate.Message(sql.FieldGTE(FieldID, id)) +} + +// IDLT applies the LT predicate on the ID field. +func IDLT(id uuid.UUID) predicate.Message { + return predicate.Message(sql.FieldLT(FieldID, id)) +} + +// IDLTE applies the LTE predicate on the ID field. +func IDLTE(id uuid.UUID) predicate.Message { + return predicate.Message(sql.FieldLTE(FieldID, id)) +} + +// ProjectID applies equality check predicate on the "project_id" field. It's identical to ProjectIDEQ. +func ProjectID(v uuid.UUID) predicate.Message { + return predicate.Message(sql.FieldEQ(FieldProjectID, v)) +} + +// Sender applies equality check predicate on the "sender" field. It's identical to SenderEQ. +func Sender(v string) predicate.Message { + return predicate.Message(sql.FieldEQ(FieldSender, v)) +} + +// SenderID applies equality check predicate on the "sender_id" field. It's identical to SenderIDEQ. +func SenderID(v string) predicate.Message { + return predicate.Message(sql.FieldEQ(FieldSenderID, v)) +} + +// Recipient applies equality check predicate on the "recipient" field. It's identical to RecipientEQ. +func Recipient(v string) predicate.Message { + return predicate.Message(sql.FieldEQ(FieldRecipient, v)) +} + +// RecipientID applies equality check predicate on the "recipient_id" field. It's identical to RecipientIDEQ. +func RecipientID(v string) predicate.Message { + return predicate.Message(sql.FieldEQ(FieldRecipientID, v)) +} + +// Msg applies equality check predicate on the "msg" field. It's identical to MsgEQ. +func Msg(v string) predicate.Message { + return predicate.Message(sql.FieldEQ(FieldMsg, v)) +} + +// Type applies equality check predicate on the "type" field. It's identical to TypeEQ. +func Type(v string) predicate.Message { + return predicate.Message(sql.FieldEQ(FieldType, v)) +} + +// Urgent applies equality check predicate on the "urgent" field. It's identical to UrgentEQ. +func Urgent(v bool) predicate.Message { + return predicate.Message(sql.FieldEQ(FieldUrgent, v)) +} + +// Broadcasted applies equality check predicate on the "broadcasted" field. It's identical to BroadcastedEQ. +func Broadcasted(v bool) predicate.Message { + return predicate.Message(sql.FieldEQ(FieldBroadcasted, v)) +} + +// Read applies equality check predicate on the "read" field. It's identical to ReadEQ. +func Read(v bool) predicate.Message { + return predicate.Message(sql.FieldEQ(FieldRead, v)) +} + +// AgentID applies equality check predicate on the "agent_id" field. It's identical to AgentIDEQ. +func AgentID(v string) predicate.Message { + return predicate.Message(sql.FieldEQ(FieldAgentID, v)) +} + +// GroupID applies equality check predicate on the "group_id" field. It's identical to GroupIDEQ. +func GroupID(v string) predicate.Message { + return predicate.Message(sql.FieldEQ(FieldGroupID, v)) +} + +// DispatchState applies equality check predicate on the "dispatch_state" field. It's identical to DispatchStateEQ. +func DispatchState(v string) predicate.Message { + return predicate.Message(sql.FieldEQ(FieldDispatchState, v)) +} + +// DispatchedAt applies equality check predicate on the "dispatched_at" field. It's identical to DispatchedAtEQ. +func DispatchedAt(v time.Time) predicate.Message { + return predicate.Message(sql.FieldEQ(FieldDispatchedAt, v)) +} + +// Created applies equality check predicate on the "created" field. It's identical to CreatedEQ. +func Created(v time.Time) predicate.Message { + return predicate.Message(sql.FieldEQ(FieldCreated, v)) +} + +// ProjectIDEQ applies the EQ predicate on the "project_id" field. +func ProjectIDEQ(v uuid.UUID) predicate.Message { + return predicate.Message(sql.FieldEQ(FieldProjectID, v)) +} + +// ProjectIDNEQ applies the NEQ predicate on the "project_id" field. +func ProjectIDNEQ(v uuid.UUID) predicate.Message { + return predicate.Message(sql.FieldNEQ(FieldProjectID, v)) +} + +// ProjectIDIn applies the In predicate on the "project_id" field. +func ProjectIDIn(vs ...uuid.UUID) predicate.Message { + return predicate.Message(sql.FieldIn(FieldProjectID, vs...)) +} + +// ProjectIDNotIn applies the NotIn predicate on the "project_id" field. +func ProjectIDNotIn(vs ...uuid.UUID) predicate.Message { + return predicate.Message(sql.FieldNotIn(FieldProjectID, vs...)) +} + +// ProjectIDGT applies the GT predicate on the "project_id" field. +func ProjectIDGT(v uuid.UUID) predicate.Message { + return predicate.Message(sql.FieldGT(FieldProjectID, v)) +} + +// ProjectIDGTE applies the GTE predicate on the "project_id" field. +func ProjectIDGTE(v uuid.UUID) predicate.Message { + return predicate.Message(sql.FieldGTE(FieldProjectID, v)) +} + +// ProjectIDLT applies the LT predicate on the "project_id" field. +func ProjectIDLT(v uuid.UUID) predicate.Message { + return predicate.Message(sql.FieldLT(FieldProjectID, v)) +} + +// ProjectIDLTE applies the LTE predicate on the "project_id" field. +func ProjectIDLTE(v uuid.UUID) predicate.Message { + return predicate.Message(sql.FieldLTE(FieldProjectID, v)) +} + +// SenderEQ applies the EQ predicate on the "sender" field. +func SenderEQ(v string) predicate.Message { + return predicate.Message(sql.FieldEQ(FieldSender, v)) +} + +// SenderNEQ applies the NEQ predicate on the "sender" field. +func SenderNEQ(v string) predicate.Message { + return predicate.Message(sql.FieldNEQ(FieldSender, v)) +} + +// SenderIn applies the In predicate on the "sender" field. +func SenderIn(vs ...string) predicate.Message { + return predicate.Message(sql.FieldIn(FieldSender, vs...)) +} + +// SenderNotIn applies the NotIn predicate on the "sender" field. +func SenderNotIn(vs ...string) predicate.Message { + return predicate.Message(sql.FieldNotIn(FieldSender, vs...)) +} + +// SenderGT applies the GT predicate on the "sender" field. +func SenderGT(v string) predicate.Message { + return predicate.Message(sql.FieldGT(FieldSender, v)) +} + +// SenderGTE applies the GTE predicate on the "sender" field. +func SenderGTE(v string) predicate.Message { + return predicate.Message(sql.FieldGTE(FieldSender, v)) +} + +// SenderLT applies the LT predicate on the "sender" field. +func SenderLT(v string) predicate.Message { + return predicate.Message(sql.FieldLT(FieldSender, v)) +} + +// SenderLTE applies the LTE predicate on the "sender" field. +func SenderLTE(v string) predicate.Message { + return predicate.Message(sql.FieldLTE(FieldSender, v)) +} + +// SenderContains applies the Contains predicate on the "sender" field. +func SenderContains(v string) predicate.Message { + return predicate.Message(sql.FieldContains(FieldSender, v)) +} + +// SenderHasPrefix applies the HasPrefix predicate on the "sender" field. +func SenderHasPrefix(v string) predicate.Message { + return predicate.Message(sql.FieldHasPrefix(FieldSender, v)) +} + +// SenderHasSuffix applies the HasSuffix predicate on the "sender" field. +func SenderHasSuffix(v string) predicate.Message { + return predicate.Message(sql.FieldHasSuffix(FieldSender, v)) +} + +// SenderEqualFold applies the EqualFold predicate on the "sender" field. +func SenderEqualFold(v string) predicate.Message { + return predicate.Message(sql.FieldEqualFold(FieldSender, v)) +} + +// SenderContainsFold applies the ContainsFold predicate on the "sender" field. +func SenderContainsFold(v string) predicate.Message { + return predicate.Message(sql.FieldContainsFold(FieldSender, v)) +} + +// SenderIDEQ applies the EQ predicate on the "sender_id" field. +func SenderIDEQ(v string) predicate.Message { + return predicate.Message(sql.FieldEQ(FieldSenderID, v)) +} + +// SenderIDNEQ applies the NEQ predicate on the "sender_id" field. +func SenderIDNEQ(v string) predicate.Message { + return predicate.Message(sql.FieldNEQ(FieldSenderID, v)) +} + +// SenderIDIn applies the In predicate on the "sender_id" field. +func SenderIDIn(vs ...string) predicate.Message { + return predicate.Message(sql.FieldIn(FieldSenderID, vs...)) +} + +// SenderIDNotIn applies the NotIn predicate on the "sender_id" field. +func SenderIDNotIn(vs ...string) predicate.Message { + return predicate.Message(sql.FieldNotIn(FieldSenderID, vs...)) +} + +// SenderIDGT applies the GT predicate on the "sender_id" field. +func SenderIDGT(v string) predicate.Message { + return predicate.Message(sql.FieldGT(FieldSenderID, v)) +} + +// SenderIDGTE applies the GTE predicate on the "sender_id" field. +func SenderIDGTE(v string) predicate.Message { + return predicate.Message(sql.FieldGTE(FieldSenderID, v)) +} + +// SenderIDLT applies the LT predicate on the "sender_id" field. +func SenderIDLT(v string) predicate.Message { + return predicate.Message(sql.FieldLT(FieldSenderID, v)) +} + +// SenderIDLTE applies the LTE predicate on the "sender_id" field. +func SenderIDLTE(v string) predicate.Message { + return predicate.Message(sql.FieldLTE(FieldSenderID, v)) +} + +// SenderIDContains applies the Contains predicate on the "sender_id" field. +func SenderIDContains(v string) predicate.Message { + return predicate.Message(sql.FieldContains(FieldSenderID, v)) +} + +// SenderIDHasPrefix applies the HasPrefix predicate on the "sender_id" field. +func SenderIDHasPrefix(v string) predicate.Message { + return predicate.Message(sql.FieldHasPrefix(FieldSenderID, v)) +} + +// SenderIDHasSuffix applies the HasSuffix predicate on the "sender_id" field. +func SenderIDHasSuffix(v string) predicate.Message { + return predicate.Message(sql.FieldHasSuffix(FieldSenderID, v)) +} + +// SenderIDIsNil applies the IsNil predicate on the "sender_id" field. +func SenderIDIsNil() predicate.Message { + return predicate.Message(sql.FieldIsNull(FieldSenderID)) +} + +// SenderIDNotNil applies the NotNil predicate on the "sender_id" field. +func SenderIDNotNil() predicate.Message { + return predicate.Message(sql.FieldNotNull(FieldSenderID)) +} + +// SenderIDEqualFold applies the EqualFold predicate on the "sender_id" field. +func SenderIDEqualFold(v string) predicate.Message { + return predicate.Message(sql.FieldEqualFold(FieldSenderID, v)) +} + +// SenderIDContainsFold applies the ContainsFold predicate on the "sender_id" field. +func SenderIDContainsFold(v string) predicate.Message { + return predicate.Message(sql.FieldContainsFold(FieldSenderID, v)) +} + +// RecipientEQ applies the EQ predicate on the "recipient" field. +func RecipientEQ(v string) predicate.Message { + return predicate.Message(sql.FieldEQ(FieldRecipient, v)) +} + +// RecipientNEQ applies the NEQ predicate on the "recipient" field. +func RecipientNEQ(v string) predicate.Message { + return predicate.Message(sql.FieldNEQ(FieldRecipient, v)) +} + +// RecipientIn applies the In predicate on the "recipient" field. +func RecipientIn(vs ...string) predicate.Message { + return predicate.Message(sql.FieldIn(FieldRecipient, vs...)) +} + +// RecipientNotIn applies the NotIn predicate on the "recipient" field. +func RecipientNotIn(vs ...string) predicate.Message { + return predicate.Message(sql.FieldNotIn(FieldRecipient, vs...)) +} + +// RecipientGT applies the GT predicate on the "recipient" field. +func RecipientGT(v string) predicate.Message { + return predicate.Message(sql.FieldGT(FieldRecipient, v)) +} + +// RecipientGTE applies the GTE predicate on the "recipient" field. +func RecipientGTE(v string) predicate.Message { + return predicate.Message(sql.FieldGTE(FieldRecipient, v)) +} + +// RecipientLT applies the LT predicate on the "recipient" field. +func RecipientLT(v string) predicate.Message { + return predicate.Message(sql.FieldLT(FieldRecipient, v)) +} + +// RecipientLTE applies the LTE predicate on the "recipient" field. +func RecipientLTE(v string) predicate.Message { + return predicate.Message(sql.FieldLTE(FieldRecipient, v)) +} + +// RecipientContains applies the Contains predicate on the "recipient" field. +func RecipientContains(v string) predicate.Message { + return predicate.Message(sql.FieldContains(FieldRecipient, v)) +} + +// RecipientHasPrefix applies the HasPrefix predicate on the "recipient" field. +func RecipientHasPrefix(v string) predicate.Message { + return predicate.Message(sql.FieldHasPrefix(FieldRecipient, v)) +} + +// RecipientHasSuffix applies the HasSuffix predicate on the "recipient" field. +func RecipientHasSuffix(v string) predicate.Message { + return predicate.Message(sql.FieldHasSuffix(FieldRecipient, v)) +} + +// RecipientEqualFold applies the EqualFold predicate on the "recipient" field. +func RecipientEqualFold(v string) predicate.Message { + return predicate.Message(sql.FieldEqualFold(FieldRecipient, v)) +} + +// RecipientContainsFold applies the ContainsFold predicate on the "recipient" field. +func RecipientContainsFold(v string) predicate.Message { + return predicate.Message(sql.FieldContainsFold(FieldRecipient, v)) +} + +// RecipientIDEQ applies the EQ predicate on the "recipient_id" field. +func RecipientIDEQ(v string) predicate.Message { + return predicate.Message(sql.FieldEQ(FieldRecipientID, v)) +} + +// RecipientIDNEQ applies the NEQ predicate on the "recipient_id" field. +func RecipientIDNEQ(v string) predicate.Message { + return predicate.Message(sql.FieldNEQ(FieldRecipientID, v)) +} + +// RecipientIDIn applies the In predicate on the "recipient_id" field. +func RecipientIDIn(vs ...string) predicate.Message { + return predicate.Message(sql.FieldIn(FieldRecipientID, vs...)) +} + +// RecipientIDNotIn applies the NotIn predicate on the "recipient_id" field. +func RecipientIDNotIn(vs ...string) predicate.Message { + return predicate.Message(sql.FieldNotIn(FieldRecipientID, vs...)) +} + +// RecipientIDGT applies the GT predicate on the "recipient_id" field. +func RecipientIDGT(v string) predicate.Message { + return predicate.Message(sql.FieldGT(FieldRecipientID, v)) +} + +// RecipientIDGTE applies the GTE predicate on the "recipient_id" field. +func RecipientIDGTE(v string) predicate.Message { + return predicate.Message(sql.FieldGTE(FieldRecipientID, v)) +} + +// RecipientIDLT applies the LT predicate on the "recipient_id" field. +func RecipientIDLT(v string) predicate.Message { + return predicate.Message(sql.FieldLT(FieldRecipientID, v)) +} + +// RecipientIDLTE applies the LTE predicate on the "recipient_id" field. +func RecipientIDLTE(v string) predicate.Message { + return predicate.Message(sql.FieldLTE(FieldRecipientID, v)) +} + +// RecipientIDContains applies the Contains predicate on the "recipient_id" field. +func RecipientIDContains(v string) predicate.Message { + return predicate.Message(sql.FieldContains(FieldRecipientID, v)) +} + +// RecipientIDHasPrefix applies the HasPrefix predicate on the "recipient_id" field. +func RecipientIDHasPrefix(v string) predicate.Message { + return predicate.Message(sql.FieldHasPrefix(FieldRecipientID, v)) +} + +// RecipientIDHasSuffix applies the HasSuffix predicate on the "recipient_id" field. +func RecipientIDHasSuffix(v string) predicate.Message { + return predicate.Message(sql.FieldHasSuffix(FieldRecipientID, v)) +} + +// RecipientIDIsNil applies the IsNil predicate on the "recipient_id" field. +func RecipientIDIsNil() predicate.Message { + return predicate.Message(sql.FieldIsNull(FieldRecipientID)) +} + +// RecipientIDNotNil applies the NotNil predicate on the "recipient_id" field. +func RecipientIDNotNil() predicate.Message { + return predicate.Message(sql.FieldNotNull(FieldRecipientID)) +} + +// RecipientIDEqualFold applies the EqualFold predicate on the "recipient_id" field. +func RecipientIDEqualFold(v string) predicate.Message { + return predicate.Message(sql.FieldEqualFold(FieldRecipientID, v)) +} + +// RecipientIDContainsFold applies the ContainsFold predicate on the "recipient_id" field. +func RecipientIDContainsFold(v string) predicate.Message { + return predicate.Message(sql.FieldContainsFold(FieldRecipientID, v)) +} + +// MsgEQ applies the EQ predicate on the "msg" field. +func MsgEQ(v string) predicate.Message { + return predicate.Message(sql.FieldEQ(FieldMsg, v)) +} + +// MsgNEQ applies the NEQ predicate on the "msg" field. +func MsgNEQ(v string) predicate.Message { + return predicate.Message(sql.FieldNEQ(FieldMsg, v)) +} + +// MsgIn applies the In predicate on the "msg" field. +func MsgIn(vs ...string) predicate.Message { + return predicate.Message(sql.FieldIn(FieldMsg, vs...)) +} + +// MsgNotIn applies the NotIn predicate on the "msg" field. +func MsgNotIn(vs ...string) predicate.Message { + return predicate.Message(sql.FieldNotIn(FieldMsg, vs...)) +} + +// MsgGT applies the GT predicate on the "msg" field. +func MsgGT(v string) predicate.Message { + return predicate.Message(sql.FieldGT(FieldMsg, v)) +} + +// MsgGTE applies the GTE predicate on the "msg" field. +func MsgGTE(v string) predicate.Message { + return predicate.Message(sql.FieldGTE(FieldMsg, v)) +} + +// MsgLT applies the LT predicate on the "msg" field. +func MsgLT(v string) predicate.Message { + return predicate.Message(sql.FieldLT(FieldMsg, v)) +} + +// MsgLTE applies the LTE predicate on the "msg" field. +func MsgLTE(v string) predicate.Message { + return predicate.Message(sql.FieldLTE(FieldMsg, v)) +} + +// MsgContains applies the Contains predicate on the "msg" field. +func MsgContains(v string) predicate.Message { + return predicate.Message(sql.FieldContains(FieldMsg, v)) +} + +// MsgHasPrefix applies the HasPrefix predicate on the "msg" field. +func MsgHasPrefix(v string) predicate.Message { + return predicate.Message(sql.FieldHasPrefix(FieldMsg, v)) +} + +// MsgHasSuffix applies the HasSuffix predicate on the "msg" field. +func MsgHasSuffix(v string) predicate.Message { + return predicate.Message(sql.FieldHasSuffix(FieldMsg, v)) +} + +// MsgEqualFold applies the EqualFold predicate on the "msg" field. +func MsgEqualFold(v string) predicate.Message { + return predicate.Message(sql.FieldEqualFold(FieldMsg, v)) +} + +// MsgContainsFold applies the ContainsFold predicate on the "msg" field. +func MsgContainsFold(v string) predicate.Message { + return predicate.Message(sql.FieldContainsFold(FieldMsg, v)) +} + +// TypeEQ applies the EQ predicate on the "type" field. +func TypeEQ(v string) predicate.Message { + return predicate.Message(sql.FieldEQ(FieldType, v)) +} + +// TypeNEQ applies the NEQ predicate on the "type" field. +func TypeNEQ(v string) predicate.Message { + return predicate.Message(sql.FieldNEQ(FieldType, v)) +} + +// TypeIn applies the In predicate on the "type" field. +func TypeIn(vs ...string) predicate.Message { + return predicate.Message(sql.FieldIn(FieldType, vs...)) +} + +// TypeNotIn applies the NotIn predicate on the "type" field. +func TypeNotIn(vs ...string) predicate.Message { + return predicate.Message(sql.FieldNotIn(FieldType, vs...)) +} + +// TypeGT applies the GT predicate on the "type" field. +func TypeGT(v string) predicate.Message { + return predicate.Message(sql.FieldGT(FieldType, v)) +} + +// TypeGTE applies the GTE predicate on the "type" field. +func TypeGTE(v string) predicate.Message { + return predicate.Message(sql.FieldGTE(FieldType, v)) +} + +// TypeLT applies the LT predicate on the "type" field. +func TypeLT(v string) predicate.Message { + return predicate.Message(sql.FieldLT(FieldType, v)) +} + +// TypeLTE applies the LTE predicate on the "type" field. +func TypeLTE(v string) predicate.Message { + return predicate.Message(sql.FieldLTE(FieldType, v)) +} + +// TypeContains applies the Contains predicate on the "type" field. +func TypeContains(v string) predicate.Message { + return predicate.Message(sql.FieldContains(FieldType, v)) +} + +// TypeHasPrefix applies the HasPrefix predicate on the "type" field. +func TypeHasPrefix(v string) predicate.Message { + return predicate.Message(sql.FieldHasPrefix(FieldType, v)) +} + +// TypeHasSuffix applies the HasSuffix predicate on the "type" field. +func TypeHasSuffix(v string) predicate.Message { + return predicate.Message(sql.FieldHasSuffix(FieldType, v)) +} + +// TypeEqualFold applies the EqualFold predicate on the "type" field. +func TypeEqualFold(v string) predicate.Message { + return predicate.Message(sql.FieldEqualFold(FieldType, v)) +} + +// TypeContainsFold applies the ContainsFold predicate on the "type" field. +func TypeContainsFold(v string) predicate.Message { + return predicate.Message(sql.FieldContainsFold(FieldType, v)) +} + +// UrgentEQ applies the EQ predicate on the "urgent" field. +func UrgentEQ(v bool) predicate.Message { + return predicate.Message(sql.FieldEQ(FieldUrgent, v)) +} + +// UrgentNEQ applies the NEQ predicate on the "urgent" field. +func UrgentNEQ(v bool) predicate.Message { + return predicate.Message(sql.FieldNEQ(FieldUrgent, v)) +} + +// BroadcastedEQ applies the EQ predicate on the "broadcasted" field. +func BroadcastedEQ(v bool) predicate.Message { + return predicate.Message(sql.FieldEQ(FieldBroadcasted, v)) +} + +// BroadcastedNEQ applies the NEQ predicate on the "broadcasted" field. +func BroadcastedNEQ(v bool) predicate.Message { + return predicate.Message(sql.FieldNEQ(FieldBroadcasted, v)) +} + +// ReadEQ applies the EQ predicate on the "read" field. +func ReadEQ(v bool) predicate.Message { + return predicate.Message(sql.FieldEQ(FieldRead, v)) +} + +// ReadNEQ applies the NEQ predicate on the "read" field. +func ReadNEQ(v bool) predicate.Message { + return predicate.Message(sql.FieldNEQ(FieldRead, v)) +} + +// AgentIDEQ applies the EQ predicate on the "agent_id" field. +func AgentIDEQ(v string) predicate.Message { + return predicate.Message(sql.FieldEQ(FieldAgentID, v)) +} + +// AgentIDNEQ applies the NEQ predicate on the "agent_id" field. +func AgentIDNEQ(v string) predicate.Message { + return predicate.Message(sql.FieldNEQ(FieldAgentID, v)) +} + +// AgentIDIn applies the In predicate on the "agent_id" field. +func AgentIDIn(vs ...string) predicate.Message { + return predicate.Message(sql.FieldIn(FieldAgentID, vs...)) +} + +// AgentIDNotIn applies the NotIn predicate on the "agent_id" field. +func AgentIDNotIn(vs ...string) predicate.Message { + return predicate.Message(sql.FieldNotIn(FieldAgentID, vs...)) +} + +// AgentIDGT applies the GT predicate on the "agent_id" field. +func AgentIDGT(v string) predicate.Message { + return predicate.Message(sql.FieldGT(FieldAgentID, v)) +} + +// AgentIDGTE applies the GTE predicate on the "agent_id" field. +func AgentIDGTE(v string) predicate.Message { + return predicate.Message(sql.FieldGTE(FieldAgentID, v)) +} + +// AgentIDLT applies the LT predicate on the "agent_id" field. +func AgentIDLT(v string) predicate.Message { + return predicate.Message(sql.FieldLT(FieldAgentID, v)) +} + +// AgentIDLTE applies the LTE predicate on the "agent_id" field. +func AgentIDLTE(v string) predicate.Message { + return predicate.Message(sql.FieldLTE(FieldAgentID, v)) +} + +// AgentIDContains applies the Contains predicate on the "agent_id" field. +func AgentIDContains(v string) predicate.Message { + return predicate.Message(sql.FieldContains(FieldAgentID, v)) +} + +// AgentIDHasPrefix applies the HasPrefix predicate on the "agent_id" field. +func AgentIDHasPrefix(v string) predicate.Message { + return predicate.Message(sql.FieldHasPrefix(FieldAgentID, v)) +} + +// AgentIDHasSuffix applies the HasSuffix predicate on the "agent_id" field. +func AgentIDHasSuffix(v string) predicate.Message { + return predicate.Message(sql.FieldHasSuffix(FieldAgentID, v)) +} + +// AgentIDIsNil applies the IsNil predicate on the "agent_id" field. +func AgentIDIsNil() predicate.Message { + return predicate.Message(sql.FieldIsNull(FieldAgentID)) +} + +// AgentIDNotNil applies the NotNil predicate on the "agent_id" field. +func AgentIDNotNil() predicate.Message { + return predicate.Message(sql.FieldNotNull(FieldAgentID)) +} + +// AgentIDEqualFold applies the EqualFold predicate on the "agent_id" field. +func AgentIDEqualFold(v string) predicate.Message { + return predicate.Message(sql.FieldEqualFold(FieldAgentID, v)) +} + +// AgentIDContainsFold applies the ContainsFold predicate on the "agent_id" field. +func AgentIDContainsFold(v string) predicate.Message { + return predicate.Message(sql.FieldContainsFold(FieldAgentID, v)) +} + +// GroupIDEQ applies the EQ predicate on the "group_id" field. +func GroupIDEQ(v string) predicate.Message { + return predicate.Message(sql.FieldEQ(FieldGroupID, v)) +} + +// GroupIDNEQ applies the NEQ predicate on the "group_id" field. +func GroupIDNEQ(v string) predicate.Message { + return predicate.Message(sql.FieldNEQ(FieldGroupID, v)) +} + +// GroupIDIn applies the In predicate on the "group_id" field. +func GroupIDIn(vs ...string) predicate.Message { + return predicate.Message(sql.FieldIn(FieldGroupID, vs...)) +} + +// GroupIDNotIn applies the NotIn predicate on the "group_id" field. +func GroupIDNotIn(vs ...string) predicate.Message { + return predicate.Message(sql.FieldNotIn(FieldGroupID, vs...)) +} + +// GroupIDGT applies the GT predicate on the "group_id" field. +func GroupIDGT(v string) predicate.Message { + return predicate.Message(sql.FieldGT(FieldGroupID, v)) +} + +// GroupIDGTE applies the GTE predicate on the "group_id" field. +func GroupIDGTE(v string) predicate.Message { + return predicate.Message(sql.FieldGTE(FieldGroupID, v)) +} + +// GroupIDLT applies the LT predicate on the "group_id" field. +func GroupIDLT(v string) predicate.Message { + return predicate.Message(sql.FieldLT(FieldGroupID, v)) +} + +// GroupIDLTE applies the LTE predicate on the "group_id" field. +func GroupIDLTE(v string) predicate.Message { + return predicate.Message(sql.FieldLTE(FieldGroupID, v)) +} + +// GroupIDContains applies the Contains predicate on the "group_id" field. +func GroupIDContains(v string) predicate.Message { + return predicate.Message(sql.FieldContains(FieldGroupID, v)) +} + +// GroupIDHasPrefix applies the HasPrefix predicate on the "group_id" field. +func GroupIDHasPrefix(v string) predicate.Message { + return predicate.Message(sql.FieldHasPrefix(FieldGroupID, v)) +} + +// GroupIDHasSuffix applies the HasSuffix predicate on the "group_id" field. +func GroupIDHasSuffix(v string) predicate.Message { + return predicate.Message(sql.FieldHasSuffix(FieldGroupID, v)) +} + +// GroupIDIsNil applies the IsNil predicate on the "group_id" field. +func GroupIDIsNil() predicate.Message { + return predicate.Message(sql.FieldIsNull(FieldGroupID)) +} + +// GroupIDNotNil applies the NotNil predicate on the "group_id" field. +func GroupIDNotNil() predicate.Message { + return predicate.Message(sql.FieldNotNull(FieldGroupID)) +} + +// GroupIDEqualFold applies the EqualFold predicate on the "group_id" field. +func GroupIDEqualFold(v string) predicate.Message { + return predicate.Message(sql.FieldEqualFold(FieldGroupID, v)) +} + +// GroupIDContainsFold applies the ContainsFold predicate on the "group_id" field. +func GroupIDContainsFold(v string) predicate.Message { + return predicate.Message(sql.FieldContainsFold(FieldGroupID, v)) +} + +// DispatchStateEQ applies the EQ predicate on the "dispatch_state" field. +func DispatchStateEQ(v string) predicate.Message { + return predicate.Message(sql.FieldEQ(FieldDispatchState, v)) +} + +// DispatchStateNEQ applies the NEQ predicate on the "dispatch_state" field. +func DispatchStateNEQ(v string) predicate.Message { + return predicate.Message(sql.FieldNEQ(FieldDispatchState, v)) +} + +// DispatchStateIn applies the In predicate on the "dispatch_state" field. +func DispatchStateIn(vs ...string) predicate.Message { + return predicate.Message(sql.FieldIn(FieldDispatchState, vs...)) +} + +// DispatchStateNotIn applies the NotIn predicate on the "dispatch_state" field. +func DispatchStateNotIn(vs ...string) predicate.Message { + return predicate.Message(sql.FieldNotIn(FieldDispatchState, vs...)) +} + +// DispatchStateGT applies the GT predicate on the "dispatch_state" field. +func DispatchStateGT(v string) predicate.Message { + return predicate.Message(sql.FieldGT(FieldDispatchState, v)) +} + +// DispatchStateGTE applies the GTE predicate on the "dispatch_state" field. +func DispatchStateGTE(v string) predicate.Message { + return predicate.Message(sql.FieldGTE(FieldDispatchState, v)) +} + +// DispatchStateLT applies the LT predicate on the "dispatch_state" field. +func DispatchStateLT(v string) predicate.Message { + return predicate.Message(sql.FieldLT(FieldDispatchState, v)) +} + +// DispatchStateLTE applies the LTE predicate on the "dispatch_state" field. +func DispatchStateLTE(v string) predicate.Message { + return predicate.Message(sql.FieldLTE(FieldDispatchState, v)) +} + +// DispatchStateContains applies the Contains predicate on the "dispatch_state" field. +func DispatchStateContains(v string) predicate.Message { + return predicate.Message(sql.FieldContains(FieldDispatchState, v)) +} + +// DispatchStateHasPrefix applies the HasPrefix predicate on the "dispatch_state" field. +func DispatchStateHasPrefix(v string) predicate.Message { + return predicate.Message(sql.FieldHasPrefix(FieldDispatchState, v)) +} + +// DispatchStateHasSuffix applies the HasSuffix predicate on the "dispatch_state" field. +func DispatchStateHasSuffix(v string) predicate.Message { + return predicate.Message(sql.FieldHasSuffix(FieldDispatchState, v)) +} + +// DispatchStateEqualFold applies the EqualFold predicate on the "dispatch_state" field. +func DispatchStateEqualFold(v string) predicate.Message { + return predicate.Message(sql.FieldEqualFold(FieldDispatchState, v)) +} + +// DispatchStateContainsFold applies the ContainsFold predicate on the "dispatch_state" field. +func DispatchStateContainsFold(v string) predicate.Message { + return predicate.Message(sql.FieldContainsFold(FieldDispatchState, v)) +} + +// DispatchedAtEQ applies the EQ predicate on the "dispatched_at" field. +func DispatchedAtEQ(v time.Time) predicate.Message { + return predicate.Message(sql.FieldEQ(FieldDispatchedAt, v)) +} + +// DispatchedAtNEQ applies the NEQ predicate on the "dispatched_at" field. +func DispatchedAtNEQ(v time.Time) predicate.Message { + return predicate.Message(sql.FieldNEQ(FieldDispatchedAt, v)) +} + +// DispatchedAtIn applies the In predicate on the "dispatched_at" field. +func DispatchedAtIn(vs ...time.Time) predicate.Message { + return predicate.Message(sql.FieldIn(FieldDispatchedAt, vs...)) +} + +// DispatchedAtNotIn applies the NotIn predicate on the "dispatched_at" field. +func DispatchedAtNotIn(vs ...time.Time) predicate.Message { + return predicate.Message(sql.FieldNotIn(FieldDispatchedAt, vs...)) +} + +// DispatchedAtGT applies the GT predicate on the "dispatched_at" field. +func DispatchedAtGT(v time.Time) predicate.Message { + return predicate.Message(sql.FieldGT(FieldDispatchedAt, v)) +} + +// DispatchedAtGTE applies the GTE predicate on the "dispatched_at" field. +func DispatchedAtGTE(v time.Time) predicate.Message { + return predicate.Message(sql.FieldGTE(FieldDispatchedAt, v)) +} + +// DispatchedAtLT applies the LT predicate on the "dispatched_at" field. +func DispatchedAtLT(v time.Time) predicate.Message { + return predicate.Message(sql.FieldLT(FieldDispatchedAt, v)) +} + +// DispatchedAtLTE applies the LTE predicate on the "dispatched_at" field. +func DispatchedAtLTE(v time.Time) predicate.Message { + return predicate.Message(sql.FieldLTE(FieldDispatchedAt, v)) +} + +// DispatchedAtIsNil applies the IsNil predicate on the "dispatched_at" field. +func DispatchedAtIsNil() predicate.Message { + return predicate.Message(sql.FieldIsNull(FieldDispatchedAt)) +} + +// DispatchedAtNotNil applies the NotNil predicate on the "dispatched_at" field. +func DispatchedAtNotNil() predicate.Message { + return predicate.Message(sql.FieldNotNull(FieldDispatchedAt)) +} + +// CreatedEQ applies the EQ predicate on the "created" field. +func CreatedEQ(v time.Time) predicate.Message { + return predicate.Message(sql.FieldEQ(FieldCreated, v)) +} + +// CreatedNEQ applies the NEQ predicate on the "created" field. +func CreatedNEQ(v time.Time) predicate.Message { + return predicate.Message(sql.FieldNEQ(FieldCreated, v)) +} + +// CreatedIn applies the In predicate on the "created" field. +func CreatedIn(vs ...time.Time) predicate.Message { + return predicate.Message(sql.FieldIn(FieldCreated, vs...)) +} + +// CreatedNotIn applies the NotIn predicate on the "created" field. +func CreatedNotIn(vs ...time.Time) predicate.Message { + return predicate.Message(sql.FieldNotIn(FieldCreated, vs...)) +} + +// CreatedGT applies the GT predicate on the "created" field. +func CreatedGT(v time.Time) predicate.Message { + return predicate.Message(sql.FieldGT(FieldCreated, v)) +} + +// CreatedGTE applies the GTE predicate on the "created" field. +func CreatedGTE(v time.Time) predicate.Message { + return predicate.Message(sql.FieldGTE(FieldCreated, v)) +} + +// CreatedLT applies the LT predicate on the "created" field. +func CreatedLT(v time.Time) predicate.Message { + return predicate.Message(sql.FieldLT(FieldCreated, v)) +} + +// CreatedLTE applies the LTE predicate on the "created" field. +func CreatedLTE(v time.Time) predicate.Message { + return predicate.Message(sql.FieldLTE(FieldCreated, v)) +} + +// And groups predicates with the AND operator between them. +func And(predicates ...predicate.Message) predicate.Message { + return predicate.Message(sql.AndPredicates(predicates...)) +} + +// Or groups predicates with the OR operator between them. +func Or(predicates ...predicate.Message) predicate.Message { + return predicate.Message(sql.OrPredicates(predicates...)) +} + +// Not applies the not operator on the given predicate. +func Not(p predicate.Message) predicate.Message { + return predicate.Message(sql.NotPredicates(p)) +} diff --git a/pkg/ent/message_create.go b/pkg/ent/message_create.go new file mode 100644 index 000000000..f29d7aa46 --- /dev/null +++ b/pkg/ent/message_create.go @@ -0,0 +1,1429 @@ +// Code generated by ent, DO NOT EDIT. + +package ent + +import ( + "context" + "errors" + "fmt" + "time" + + "entgo.io/ent/dialect" + "entgo.io/ent/dialect/sql" + "entgo.io/ent/dialect/sql/sqlgraph" + "entgo.io/ent/schema/field" + "github.com/GoogleCloudPlatform/scion/pkg/ent/message" + "github.com/google/uuid" +) + +// MessageCreate is the builder for creating a Message entity. +type MessageCreate struct { + config + mutation *MessageMutation + hooks []Hook + conflict []sql.ConflictOption +} + +// SetProjectID sets the "project_id" field. +func (_c *MessageCreate) SetProjectID(v uuid.UUID) *MessageCreate { + _c.mutation.SetProjectID(v) + return _c +} + +// SetSender sets the "sender" field. +func (_c *MessageCreate) SetSender(v string) *MessageCreate { + _c.mutation.SetSender(v) + return _c +} + +// SetSenderID sets the "sender_id" field. +func (_c *MessageCreate) SetSenderID(v string) *MessageCreate { + _c.mutation.SetSenderID(v) + return _c +} + +// SetNillableSenderID sets the "sender_id" field if the given value is not nil. +func (_c *MessageCreate) SetNillableSenderID(v *string) *MessageCreate { + if v != nil { + _c.SetSenderID(*v) + } + return _c +} + +// SetRecipient sets the "recipient" field. +func (_c *MessageCreate) SetRecipient(v string) *MessageCreate { + _c.mutation.SetRecipient(v) + return _c +} + +// SetRecipientID sets the "recipient_id" field. +func (_c *MessageCreate) SetRecipientID(v string) *MessageCreate { + _c.mutation.SetRecipientID(v) + return _c +} + +// SetNillableRecipientID sets the "recipient_id" field if the given value is not nil. +func (_c *MessageCreate) SetNillableRecipientID(v *string) *MessageCreate { + if v != nil { + _c.SetRecipientID(*v) + } + return _c +} + +// SetMsg sets the "msg" field. +func (_c *MessageCreate) SetMsg(v string) *MessageCreate { + _c.mutation.SetMsg(v) + return _c +} + +// SetType sets the "type" field. +func (_c *MessageCreate) SetType(v string) *MessageCreate { + _c.mutation.SetType(v) + return _c +} + +// SetNillableType sets the "type" field if the given value is not nil. +func (_c *MessageCreate) SetNillableType(v *string) *MessageCreate { + if v != nil { + _c.SetType(*v) + } + return _c +} + +// SetUrgent sets the "urgent" field. +func (_c *MessageCreate) SetUrgent(v bool) *MessageCreate { + _c.mutation.SetUrgent(v) + return _c +} + +// SetNillableUrgent sets the "urgent" field if the given value is not nil. +func (_c *MessageCreate) SetNillableUrgent(v *bool) *MessageCreate { + if v != nil { + _c.SetUrgent(*v) + } + return _c +} + +// SetBroadcasted sets the "broadcasted" field. +func (_c *MessageCreate) SetBroadcasted(v bool) *MessageCreate { + _c.mutation.SetBroadcasted(v) + return _c +} + +// SetNillableBroadcasted sets the "broadcasted" field if the given value is not nil. +func (_c *MessageCreate) SetNillableBroadcasted(v *bool) *MessageCreate { + if v != nil { + _c.SetBroadcasted(*v) + } + return _c +} + +// SetRead sets the "read" field. +func (_c *MessageCreate) SetRead(v bool) *MessageCreate { + _c.mutation.SetRead(v) + return _c +} + +// SetNillableRead sets the "read" field if the given value is not nil. +func (_c *MessageCreate) SetNillableRead(v *bool) *MessageCreate { + if v != nil { + _c.SetRead(*v) + } + return _c +} + +// SetAgentID sets the "agent_id" field. +func (_c *MessageCreate) SetAgentID(v string) *MessageCreate { + _c.mutation.SetAgentID(v) + return _c +} + +// SetNillableAgentID sets the "agent_id" field if the given value is not nil. +func (_c *MessageCreate) SetNillableAgentID(v *string) *MessageCreate { + if v != nil { + _c.SetAgentID(*v) + } + return _c +} + +// SetGroupID sets the "group_id" field. +func (_c *MessageCreate) SetGroupID(v string) *MessageCreate { + _c.mutation.SetGroupID(v) + return _c +} + +// SetNillableGroupID sets the "group_id" field if the given value is not nil. +func (_c *MessageCreate) SetNillableGroupID(v *string) *MessageCreate { + if v != nil { + _c.SetGroupID(*v) + } + return _c +} + +// SetDispatchState sets the "dispatch_state" field. +func (_c *MessageCreate) SetDispatchState(v string) *MessageCreate { + _c.mutation.SetDispatchState(v) + return _c +} + +// SetNillableDispatchState sets the "dispatch_state" field if the given value is not nil. +func (_c *MessageCreate) SetNillableDispatchState(v *string) *MessageCreate { + if v != nil { + _c.SetDispatchState(*v) + } + return _c +} + +// SetDispatchedAt sets the "dispatched_at" field. +func (_c *MessageCreate) SetDispatchedAt(v time.Time) *MessageCreate { + _c.mutation.SetDispatchedAt(v) + return _c +} + +// SetNillableDispatchedAt sets the "dispatched_at" field if the given value is not nil. +func (_c *MessageCreate) SetNillableDispatchedAt(v *time.Time) *MessageCreate { + if v != nil { + _c.SetDispatchedAt(*v) + } + return _c +} + +// SetCreated sets the "created" field. +func (_c *MessageCreate) SetCreated(v time.Time) *MessageCreate { + _c.mutation.SetCreated(v) + return _c +} + +// SetNillableCreated sets the "created" field if the given value is not nil. +func (_c *MessageCreate) SetNillableCreated(v *time.Time) *MessageCreate { + if v != nil { + _c.SetCreated(*v) + } + return _c +} + +// SetID sets the "id" field. +func (_c *MessageCreate) SetID(v uuid.UUID) *MessageCreate { + _c.mutation.SetID(v) + return _c +} + +// SetNillableID sets the "id" field if the given value is not nil. +func (_c *MessageCreate) SetNillableID(v *uuid.UUID) *MessageCreate { + if v != nil { + _c.SetID(*v) + } + return _c +} + +// Mutation returns the MessageMutation object of the builder. +func (_c *MessageCreate) Mutation() *MessageMutation { + return _c.mutation +} + +// Save creates the Message in the database. +func (_c *MessageCreate) Save(ctx context.Context) (*Message, error) { + _c.defaults() + return withHooks(ctx, _c.sqlSave, _c.mutation, _c.hooks) +} + +// SaveX calls Save and panics if Save returns an error. +func (_c *MessageCreate) SaveX(ctx context.Context) *Message { + v, err := _c.Save(ctx) + if err != nil { + panic(err) + } + return v +} + +// Exec executes the query. +func (_c *MessageCreate) Exec(ctx context.Context) error { + _, err := _c.Save(ctx) + return err +} + +// ExecX is like Exec, but panics if an error occurs. +func (_c *MessageCreate) ExecX(ctx context.Context) { + if err := _c.Exec(ctx); err != nil { + panic(err) + } +} + +// defaults sets the default values of the builder before save. +func (_c *MessageCreate) defaults() { + if _, ok := _c.mutation.GetType(); !ok { + v := message.DefaultType + _c.mutation.SetType(v) + } + if _, ok := _c.mutation.Urgent(); !ok { + v := message.DefaultUrgent + _c.mutation.SetUrgent(v) + } + if _, ok := _c.mutation.Broadcasted(); !ok { + v := message.DefaultBroadcasted + _c.mutation.SetBroadcasted(v) + } + if _, ok := _c.mutation.Read(); !ok { + v := message.DefaultRead + _c.mutation.SetRead(v) + } + if _, ok := _c.mutation.DispatchState(); !ok { + v := message.DefaultDispatchState + _c.mutation.SetDispatchState(v) + } + if _, ok := _c.mutation.Created(); !ok { + v := message.DefaultCreated() + _c.mutation.SetCreated(v) + } + if _, ok := _c.mutation.ID(); !ok { + v := message.DefaultID() + _c.mutation.SetID(v) + } +} + +// check runs all checks and user-defined validators on the builder. +func (_c *MessageCreate) check() error { + if _, ok := _c.mutation.ProjectID(); !ok { + return &ValidationError{Name: "project_id", err: errors.New(`ent: missing required field "Message.project_id"`)} + } + if _, ok := _c.mutation.Sender(); !ok { + return &ValidationError{Name: "sender", err: errors.New(`ent: missing required field "Message.sender"`)} + } + if v, ok := _c.mutation.Sender(); ok { + if err := message.SenderValidator(v); err != nil { + return &ValidationError{Name: "sender", err: fmt.Errorf(`ent: validator failed for field "Message.sender": %w`, err)} + } + } + if _, ok := _c.mutation.Recipient(); !ok { + return &ValidationError{Name: "recipient", err: errors.New(`ent: missing required field "Message.recipient"`)} + } + if v, ok := _c.mutation.Recipient(); ok { + if err := message.RecipientValidator(v); err != nil { + return &ValidationError{Name: "recipient", err: fmt.Errorf(`ent: validator failed for field "Message.recipient": %w`, err)} + } + } + if _, ok := _c.mutation.Msg(); !ok { + return &ValidationError{Name: "msg", err: errors.New(`ent: missing required field "Message.msg"`)} + } + if v, ok := _c.mutation.Msg(); ok { + if err := message.MsgValidator(v); err != nil { + return &ValidationError{Name: "msg", err: fmt.Errorf(`ent: validator failed for field "Message.msg": %w`, err)} + } + } + if _, ok := _c.mutation.GetType(); !ok { + return &ValidationError{Name: "type", err: errors.New(`ent: missing required field "Message.type"`)} + } + if _, ok := _c.mutation.Urgent(); !ok { + return &ValidationError{Name: "urgent", err: errors.New(`ent: missing required field "Message.urgent"`)} + } + if _, ok := _c.mutation.Broadcasted(); !ok { + return &ValidationError{Name: "broadcasted", err: errors.New(`ent: missing required field "Message.broadcasted"`)} + } + if _, ok := _c.mutation.Read(); !ok { + return &ValidationError{Name: "read", err: errors.New(`ent: missing required field "Message.read"`)} + } + if _, ok := _c.mutation.DispatchState(); !ok { + return &ValidationError{Name: "dispatch_state", err: errors.New(`ent: missing required field "Message.dispatch_state"`)} + } + if _, ok := _c.mutation.Created(); !ok { + return &ValidationError{Name: "created", err: errors.New(`ent: missing required field "Message.created"`)} + } + return nil +} + +func (_c *MessageCreate) sqlSave(ctx context.Context) (*Message, error) { + if err := _c.check(); err != nil { + return nil, err + } + _node, _spec := _c.createSpec() + if err := sqlgraph.CreateNode(ctx, _c.driver, _spec); err != nil { + if sqlgraph.IsConstraintError(err) { + err = &ConstraintError{msg: err.Error(), wrap: err} + } + return nil, err + } + if _spec.ID.Value != nil { + if id, ok := _spec.ID.Value.(*uuid.UUID); ok { + _node.ID = *id + } else if err := _node.ID.Scan(_spec.ID.Value); err != nil { + return nil, err + } + } + _c.mutation.id = &_node.ID + _c.mutation.done = true + return _node, nil +} + +func (_c *MessageCreate) createSpec() (*Message, *sqlgraph.CreateSpec) { + var ( + _node = &Message{config: _c.config} + _spec = sqlgraph.NewCreateSpec(message.Table, sqlgraph.NewFieldSpec(message.FieldID, field.TypeUUID)) + ) + _spec.OnConflict = _c.conflict + if id, ok := _c.mutation.ID(); ok { + _node.ID = id + _spec.ID.Value = &id + } + if value, ok := _c.mutation.ProjectID(); ok { + _spec.SetField(message.FieldProjectID, field.TypeUUID, value) + _node.ProjectID = value + } + if value, ok := _c.mutation.Sender(); ok { + _spec.SetField(message.FieldSender, field.TypeString, value) + _node.Sender = value + } + if value, ok := _c.mutation.SenderID(); ok { + _spec.SetField(message.FieldSenderID, field.TypeString, value) + _node.SenderID = value + } + if value, ok := _c.mutation.Recipient(); ok { + _spec.SetField(message.FieldRecipient, field.TypeString, value) + _node.Recipient = value + } + if value, ok := _c.mutation.RecipientID(); ok { + _spec.SetField(message.FieldRecipientID, field.TypeString, value) + _node.RecipientID = value + } + if value, ok := _c.mutation.Msg(); ok { + _spec.SetField(message.FieldMsg, field.TypeString, value) + _node.Msg = value + } + if value, ok := _c.mutation.GetType(); ok { + _spec.SetField(message.FieldType, field.TypeString, value) + _node.Type = value + } + if value, ok := _c.mutation.Urgent(); ok { + _spec.SetField(message.FieldUrgent, field.TypeBool, value) + _node.Urgent = value + } + if value, ok := _c.mutation.Broadcasted(); ok { + _spec.SetField(message.FieldBroadcasted, field.TypeBool, value) + _node.Broadcasted = value + } + if value, ok := _c.mutation.Read(); ok { + _spec.SetField(message.FieldRead, field.TypeBool, value) + _node.Read = value + } + if value, ok := _c.mutation.AgentID(); ok { + _spec.SetField(message.FieldAgentID, field.TypeString, value) + _node.AgentID = value + } + if value, ok := _c.mutation.GroupID(); ok { + _spec.SetField(message.FieldGroupID, field.TypeString, value) + _node.GroupID = value + } + if value, ok := _c.mutation.DispatchState(); ok { + _spec.SetField(message.FieldDispatchState, field.TypeString, value) + _node.DispatchState = value + } + if value, ok := _c.mutation.DispatchedAt(); ok { + _spec.SetField(message.FieldDispatchedAt, field.TypeTime, value) + _node.DispatchedAt = &value + } + if value, ok := _c.mutation.Created(); ok { + _spec.SetField(message.FieldCreated, field.TypeTime, value) + _node.Created = value + } + return _node, _spec +} + +// OnConflict allows configuring the `ON CONFLICT` / `ON DUPLICATE KEY` clause +// of the `INSERT` statement. For example: +// +// client.Message.Create(). +// SetProjectID(v). +// OnConflict( +// // Update the row with the new values +// // the was proposed for insertion. +// sql.ResolveWithNewValues(), +// ). +// // Override some of the fields with custom +// // update values. +// Update(func(u *ent.MessageUpsert) { +// SetProjectID(v+v). +// }). +// Exec(ctx) +func (_c *MessageCreate) OnConflict(opts ...sql.ConflictOption) *MessageUpsertOne { + _c.conflict = opts + return &MessageUpsertOne{ + create: _c, + } +} + +// OnConflictColumns calls `OnConflict` and configures the columns +// as conflict target. Using this option is equivalent to using: +// +// client.Message.Create(). +// OnConflict(sql.ConflictColumns(columns...)). +// Exec(ctx) +func (_c *MessageCreate) OnConflictColumns(columns ...string) *MessageUpsertOne { + _c.conflict = append(_c.conflict, sql.ConflictColumns(columns...)) + return &MessageUpsertOne{ + create: _c, + } +} + +type ( + // MessageUpsertOne is the builder for "upsert"-ing + // one Message node. + MessageUpsertOne struct { + create *MessageCreate + } + + // MessageUpsert is the "OnConflict" setter. + MessageUpsert struct { + *sql.UpdateSet + } +) + +// SetProjectID sets the "project_id" field. +func (u *MessageUpsert) SetProjectID(v uuid.UUID) *MessageUpsert { + u.Set(message.FieldProjectID, v) + return u +} + +// UpdateProjectID sets the "project_id" field to the value that was provided on create. +func (u *MessageUpsert) UpdateProjectID() *MessageUpsert { + u.SetExcluded(message.FieldProjectID) + return u +} + +// SetSender sets the "sender" field. +func (u *MessageUpsert) SetSender(v string) *MessageUpsert { + u.Set(message.FieldSender, v) + return u +} + +// UpdateSender sets the "sender" field to the value that was provided on create. +func (u *MessageUpsert) UpdateSender() *MessageUpsert { + u.SetExcluded(message.FieldSender) + return u +} + +// SetSenderID sets the "sender_id" field. +func (u *MessageUpsert) SetSenderID(v string) *MessageUpsert { + u.Set(message.FieldSenderID, v) + return u +} + +// UpdateSenderID sets the "sender_id" field to the value that was provided on create. +func (u *MessageUpsert) UpdateSenderID() *MessageUpsert { + u.SetExcluded(message.FieldSenderID) + return u +} + +// ClearSenderID clears the value of the "sender_id" field. +func (u *MessageUpsert) ClearSenderID() *MessageUpsert { + u.SetNull(message.FieldSenderID) + return u +} + +// SetRecipient sets the "recipient" field. +func (u *MessageUpsert) SetRecipient(v string) *MessageUpsert { + u.Set(message.FieldRecipient, v) + return u +} + +// UpdateRecipient sets the "recipient" field to the value that was provided on create. +func (u *MessageUpsert) UpdateRecipient() *MessageUpsert { + u.SetExcluded(message.FieldRecipient) + return u +} + +// SetRecipientID sets the "recipient_id" field. +func (u *MessageUpsert) SetRecipientID(v string) *MessageUpsert { + u.Set(message.FieldRecipientID, v) + return u +} + +// UpdateRecipientID sets the "recipient_id" field to the value that was provided on create. +func (u *MessageUpsert) UpdateRecipientID() *MessageUpsert { + u.SetExcluded(message.FieldRecipientID) + return u +} + +// ClearRecipientID clears the value of the "recipient_id" field. +func (u *MessageUpsert) ClearRecipientID() *MessageUpsert { + u.SetNull(message.FieldRecipientID) + return u +} + +// SetMsg sets the "msg" field. +func (u *MessageUpsert) SetMsg(v string) *MessageUpsert { + u.Set(message.FieldMsg, v) + return u +} + +// UpdateMsg sets the "msg" field to the value that was provided on create. +func (u *MessageUpsert) UpdateMsg() *MessageUpsert { + u.SetExcluded(message.FieldMsg) + return u +} + +// SetType sets the "type" field. +func (u *MessageUpsert) SetType(v string) *MessageUpsert { + u.Set(message.FieldType, v) + return u +} + +// UpdateType sets the "type" field to the value that was provided on create. +func (u *MessageUpsert) UpdateType() *MessageUpsert { + u.SetExcluded(message.FieldType) + return u +} + +// SetUrgent sets the "urgent" field. +func (u *MessageUpsert) SetUrgent(v bool) *MessageUpsert { + u.Set(message.FieldUrgent, v) + return u +} + +// UpdateUrgent sets the "urgent" field to the value that was provided on create. +func (u *MessageUpsert) UpdateUrgent() *MessageUpsert { + u.SetExcluded(message.FieldUrgent) + return u +} + +// SetBroadcasted sets the "broadcasted" field. +func (u *MessageUpsert) SetBroadcasted(v bool) *MessageUpsert { + u.Set(message.FieldBroadcasted, v) + return u +} + +// UpdateBroadcasted sets the "broadcasted" field to the value that was provided on create. +func (u *MessageUpsert) UpdateBroadcasted() *MessageUpsert { + u.SetExcluded(message.FieldBroadcasted) + return u +} + +// SetRead sets the "read" field. +func (u *MessageUpsert) SetRead(v bool) *MessageUpsert { + u.Set(message.FieldRead, v) + return u +} + +// UpdateRead sets the "read" field to the value that was provided on create. +func (u *MessageUpsert) UpdateRead() *MessageUpsert { + u.SetExcluded(message.FieldRead) + return u +} + +// SetAgentID sets the "agent_id" field. +func (u *MessageUpsert) SetAgentID(v string) *MessageUpsert { + u.Set(message.FieldAgentID, v) + return u +} + +// UpdateAgentID sets the "agent_id" field to the value that was provided on create. +func (u *MessageUpsert) UpdateAgentID() *MessageUpsert { + u.SetExcluded(message.FieldAgentID) + return u +} + +// ClearAgentID clears the value of the "agent_id" field. +func (u *MessageUpsert) ClearAgentID() *MessageUpsert { + u.SetNull(message.FieldAgentID) + return u +} + +// SetGroupID sets the "group_id" field. +func (u *MessageUpsert) SetGroupID(v string) *MessageUpsert { + u.Set(message.FieldGroupID, v) + return u +} + +// UpdateGroupID sets the "group_id" field to the value that was provided on create. +func (u *MessageUpsert) UpdateGroupID() *MessageUpsert { + u.SetExcluded(message.FieldGroupID) + return u +} + +// ClearGroupID clears the value of the "group_id" field. +func (u *MessageUpsert) ClearGroupID() *MessageUpsert { + u.SetNull(message.FieldGroupID) + return u +} + +// SetDispatchState sets the "dispatch_state" field. +func (u *MessageUpsert) SetDispatchState(v string) *MessageUpsert { + u.Set(message.FieldDispatchState, v) + return u +} + +// UpdateDispatchState sets the "dispatch_state" field to the value that was provided on create. +func (u *MessageUpsert) UpdateDispatchState() *MessageUpsert { + u.SetExcluded(message.FieldDispatchState) + return u +} + +// SetDispatchedAt sets the "dispatched_at" field. +func (u *MessageUpsert) SetDispatchedAt(v time.Time) *MessageUpsert { + u.Set(message.FieldDispatchedAt, v) + return u +} + +// UpdateDispatchedAt sets the "dispatched_at" field to the value that was provided on create. +func (u *MessageUpsert) UpdateDispatchedAt() *MessageUpsert { + u.SetExcluded(message.FieldDispatchedAt) + return u +} + +// ClearDispatchedAt clears the value of the "dispatched_at" field. +func (u *MessageUpsert) ClearDispatchedAt() *MessageUpsert { + u.SetNull(message.FieldDispatchedAt) + return u +} + +// UpdateNewValues updates the mutable fields using the new values that were set on create except the ID field. +// Using this option is equivalent to using: +// +// client.Message.Create(). +// OnConflict( +// sql.ResolveWithNewValues(), +// sql.ResolveWith(func(u *sql.UpdateSet) { +// u.SetIgnore(message.FieldID) +// }), +// ). +// Exec(ctx) +func (u *MessageUpsertOne) UpdateNewValues() *MessageUpsertOne { + u.create.conflict = append(u.create.conflict, sql.ResolveWithNewValues()) + u.create.conflict = append(u.create.conflict, sql.ResolveWith(func(s *sql.UpdateSet) { + if _, exists := u.create.mutation.ID(); exists { + s.SetIgnore(message.FieldID) + } + if _, exists := u.create.mutation.Created(); exists { + s.SetIgnore(message.FieldCreated) + } + })) + return u +} + +// Ignore sets each column to itself in case of conflict. +// Using this option is equivalent to using: +// +// client.Message.Create(). +// OnConflict(sql.ResolveWithIgnore()). +// Exec(ctx) +func (u *MessageUpsertOne) Ignore() *MessageUpsertOne { + u.create.conflict = append(u.create.conflict, sql.ResolveWithIgnore()) + return u +} + +// DoNothing configures the conflict_action to `DO NOTHING`. +// Supported only by SQLite and PostgreSQL. +func (u *MessageUpsertOne) DoNothing() *MessageUpsertOne { + u.create.conflict = append(u.create.conflict, sql.DoNothing()) + return u +} + +// Update allows overriding fields `UPDATE` values. See the MessageCreate.OnConflict +// documentation for more info. +func (u *MessageUpsertOne) Update(set func(*MessageUpsert)) *MessageUpsertOne { + u.create.conflict = append(u.create.conflict, sql.ResolveWith(func(update *sql.UpdateSet) { + set(&MessageUpsert{UpdateSet: update}) + })) + return u +} + +// SetProjectID sets the "project_id" field. +func (u *MessageUpsertOne) SetProjectID(v uuid.UUID) *MessageUpsertOne { + return u.Update(func(s *MessageUpsert) { + s.SetProjectID(v) + }) +} + +// UpdateProjectID sets the "project_id" field to the value that was provided on create. +func (u *MessageUpsertOne) UpdateProjectID() *MessageUpsertOne { + return u.Update(func(s *MessageUpsert) { + s.UpdateProjectID() + }) +} + +// SetSender sets the "sender" field. +func (u *MessageUpsertOne) SetSender(v string) *MessageUpsertOne { + return u.Update(func(s *MessageUpsert) { + s.SetSender(v) + }) +} + +// UpdateSender sets the "sender" field to the value that was provided on create. +func (u *MessageUpsertOne) UpdateSender() *MessageUpsertOne { + return u.Update(func(s *MessageUpsert) { + s.UpdateSender() + }) +} + +// SetSenderID sets the "sender_id" field. +func (u *MessageUpsertOne) SetSenderID(v string) *MessageUpsertOne { + return u.Update(func(s *MessageUpsert) { + s.SetSenderID(v) + }) +} + +// UpdateSenderID sets the "sender_id" field to the value that was provided on create. +func (u *MessageUpsertOne) UpdateSenderID() *MessageUpsertOne { + return u.Update(func(s *MessageUpsert) { + s.UpdateSenderID() + }) +} + +// ClearSenderID clears the value of the "sender_id" field. +func (u *MessageUpsertOne) ClearSenderID() *MessageUpsertOne { + return u.Update(func(s *MessageUpsert) { + s.ClearSenderID() + }) +} + +// SetRecipient sets the "recipient" field. +func (u *MessageUpsertOne) SetRecipient(v string) *MessageUpsertOne { + return u.Update(func(s *MessageUpsert) { + s.SetRecipient(v) + }) +} + +// UpdateRecipient sets the "recipient" field to the value that was provided on create. +func (u *MessageUpsertOne) UpdateRecipient() *MessageUpsertOne { + return u.Update(func(s *MessageUpsert) { + s.UpdateRecipient() + }) +} + +// SetRecipientID sets the "recipient_id" field. +func (u *MessageUpsertOne) SetRecipientID(v string) *MessageUpsertOne { + return u.Update(func(s *MessageUpsert) { + s.SetRecipientID(v) + }) +} + +// UpdateRecipientID sets the "recipient_id" field to the value that was provided on create. +func (u *MessageUpsertOne) UpdateRecipientID() *MessageUpsertOne { + return u.Update(func(s *MessageUpsert) { + s.UpdateRecipientID() + }) +} + +// ClearRecipientID clears the value of the "recipient_id" field. +func (u *MessageUpsertOne) ClearRecipientID() *MessageUpsertOne { + return u.Update(func(s *MessageUpsert) { + s.ClearRecipientID() + }) +} + +// SetMsg sets the "msg" field. +func (u *MessageUpsertOne) SetMsg(v string) *MessageUpsertOne { + return u.Update(func(s *MessageUpsert) { + s.SetMsg(v) + }) +} + +// UpdateMsg sets the "msg" field to the value that was provided on create. +func (u *MessageUpsertOne) UpdateMsg() *MessageUpsertOne { + return u.Update(func(s *MessageUpsert) { + s.UpdateMsg() + }) +} + +// SetType sets the "type" field. +func (u *MessageUpsertOne) SetType(v string) *MessageUpsertOne { + return u.Update(func(s *MessageUpsert) { + s.SetType(v) + }) +} + +// UpdateType sets the "type" field to the value that was provided on create. +func (u *MessageUpsertOne) UpdateType() *MessageUpsertOne { + return u.Update(func(s *MessageUpsert) { + s.UpdateType() + }) +} + +// SetUrgent sets the "urgent" field. +func (u *MessageUpsertOne) SetUrgent(v bool) *MessageUpsertOne { + return u.Update(func(s *MessageUpsert) { + s.SetUrgent(v) + }) +} + +// UpdateUrgent sets the "urgent" field to the value that was provided on create. +func (u *MessageUpsertOne) UpdateUrgent() *MessageUpsertOne { + return u.Update(func(s *MessageUpsert) { + s.UpdateUrgent() + }) +} + +// SetBroadcasted sets the "broadcasted" field. +func (u *MessageUpsertOne) SetBroadcasted(v bool) *MessageUpsertOne { + return u.Update(func(s *MessageUpsert) { + s.SetBroadcasted(v) + }) +} + +// UpdateBroadcasted sets the "broadcasted" field to the value that was provided on create. +func (u *MessageUpsertOne) UpdateBroadcasted() *MessageUpsertOne { + return u.Update(func(s *MessageUpsert) { + s.UpdateBroadcasted() + }) +} + +// SetRead sets the "read" field. +func (u *MessageUpsertOne) SetRead(v bool) *MessageUpsertOne { + return u.Update(func(s *MessageUpsert) { + s.SetRead(v) + }) +} + +// UpdateRead sets the "read" field to the value that was provided on create. +func (u *MessageUpsertOne) UpdateRead() *MessageUpsertOne { + return u.Update(func(s *MessageUpsert) { + s.UpdateRead() + }) +} + +// SetAgentID sets the "agent_id" field. +func (u *MessageUpsertOne) SetAgentID(v string) *MessageUpsertOne { + return u.Update(func(s *MessageUpsert) { + s.SetAgentID(v) + }) +} + +// UpdateAgentID sets the "agent_id" field to the value that was provided on create. +func (u *MessageUpsertOne) UpdateAgentID() *MessageUpsertOne { + return u.Update(func(s *MessageUpsert) { + s.UpdateAgentID() + }) +} + +// ClearAgentID clears the value of the "agent_id" field. +func (u *MessageUpsertOne) ClearAgentID() *MessageUpsertOne { + return u.Update(func(s *MessageUpsert) { + s.ClearAgentID() + }) +} + +// SetGroupID sets the "group_id" field. +func (u *MessageUpsertOne) SetGroupID(v string) *MessageUpsertOne { + return u.Update(func(s *MessageUpsert) { + s.SetGroupID(v) + }) +} + +// UpdateGroupID sets the "group_id" field to the value that was provided on create. +func (u *MessageUpsertOne) UpdateGroupID() *MessageUpsertOne { + return u.Update(func(s *MessageUpsert) { + s.UpdateGroupID() + }) +} + +// ClearGroupID clears the value of the "group_id" field. +func (u *MessageUpsertOne) ClearGroupID() *MessageUpsertOne { + return u.Update(func(s *MessageUpsert) { + s.ClearGroupID() + }) +} + +// SetDispatchState sets the "dispatch_state" field. +func (u *MessageUpsertOne) SetDispatchState(v string) *MessageUpsertOne { + return u.Update(func(s *MessageUpsert) { + s.SetDispatchState(v) + }) +} + +// UpdateDispatchState sets the "dispatch_state" field to the value that was provided on create. +func (u *MessageUpsertOne) UpdateDispatchState() *MessageUpsertOne { + return u.Update(func(s *MessageUpsert) { + s.UpdateDispatchState() + }) +} + +// SetDispatchedAt sets the "dispatched_at" field. +func (u *MessageUpsertOne) SetDispatchedAt(v time.Time) *MessageUpsertOne { + return u.Update(func(s *MessageUpsert) { + s.SetDispatchedAt(v) + }) +} + +// UpdateDispatchedAt sets the "dispatched_at" field to the value that was provided on create. +func (u *MessageUpsertOne) UpdateDispatchedAt() *MessageUpsertOne { + return u.Update(func(s *MessageUpsert) { + s.UpdateDispatchedAt() + }) +} + +// ClearDispatchedAt clears the value of the "dispatched_at" field. +func (u *MessageUpsertOne) ClearDispatchedAt() *MessageUpsertOne { + return u.Update(func(s *MessageUpsert) { + s.ClearDispatchedAt() + }) +} + +// Exec executes the query. +func (u *MessageUpsertOne) Exec(ctx context.Context) error { + if len(u.create.conflict) == 0 { + return errors.New("ent: missing options for MessageCreate.OnConflict") + } + return u.create.Exec(ctx) +} + +// ExecX is like Exec, but panics if an error occurs. +func (u *MessageUpsertOne) ExecX(ctx context.Context) { + if err := u.create.Exec(ctx); err != nil { + panic(err) + } +} + +// Exec executes the UPSERT query and returns the inserted/updated ID. +func (u *MessageUpsertOne) ID(ctx context.Context) (id uuid.UUID, err error) { + if u.create.driver.Dialect() == dialect.MySQL { + // In case of "ON CONFLICT", there is no way to get back non-numeric ID + // fields from the database since MySQL does not support the RETURNING clause. + return id, errors.New("ent: MessageUpsertOne.ID is not supported by MySQL driver. Use MessageUpsertOne.Exec instead") + } + node, err := u.create.Save(ctx) + if err != nil { + return id, err + } + return node.ID, nil +} + +// IDX is like ID, but panics if an error occurs. +func (u *MessageUpsertOne) IDX(ctx context.Context) uuid.UUID { + id, err := u.ID(ctx) + if err != nil { + panic(err) + } + return id +} + +// MessageCreateBulk is the builder for creating many Message entities in bulk. +type MessageCreateBulk struct { + config + err error + builders []*MessageCreate + conflict []sql.ConflictOption +} + +// Save creates the Message entities in the database. +func (_c *MessageCreateBulk) Save(ctx context.Context) ([]*Message, error) { + if _c.err != nil { + return nil, _c.err + } + specs := make([]*sqlgraph.CreateSpec, len(_c.builders)) + nodes := make([]*Message, len(_c.builders)) + mutators := make([]Mutator, len(_c.builders)) + for i := range _c.builders { + func(i int, root context.Context) { + builder := _c.builders[i] + builder.defaults() + var mut Mutator = MutateFunc(func(ctx context.Context, m Mutation) (Value, error) { + mutation, ok := m.(*MessageMutation) + if !ok { + return nil, fmt.Errorf("unexpected mutation type %T", m) + } + if err := builder.check(); err != nil { + return nil, err + } + builder.mutation = mutation + var err error + nodes[i], specs[i] = builder.createSpec() + if i < len(mutators)-1 { + _, err = mutators[i+1].Mutate(root, _c.builders[i+1].mutation) + } else { + spec := &sqlgraph.BatchCreateSpec{Nodes: specs} + spec.OnConflict = _c.conflict + // Invoke the actual operation on the latest mutation in the chain. + if err = sqlgraph.BatchCreate(ctx, _c.driver, spec); err != nil { + if sqlgraph.IsConstraintError(err) { + err = &ConstraintError{msg: err.Error(), wrap: err} + } + } + } + if err != nil { + return nil, err + } + mutation.id = &nodes[i].ID + mutation.done = true + return nodes[i], nil + }) + for i := len(builder.hooks) - 1; i >= 0; i-- { + mut = builder.hooks[i](mut) + } + mutators[i] = mut + }(i, ctx) + } + if len(mutators) > 0 { + if _, err := mutators[0].Mutate(ctx, _c.builders[0].mutation); err != nil { + return nil, err + } + } + return nodes, nil +} + +// SaveX is like Save, but panics if an error occurs. +func (_c *MessageCreateBulk) SaveX(ctx context.Context) []*Message { + v, err := _c.Save(ctx) + if err != nil { + panic(err) + } + return v +} + +// Exec executes the query. +func (_c *MessageCreateBulk) Exec(ctx context.Context) error { + _, err := _c.Save(ctx) + return err +} + +// ExecX is like Exec, but panics if an error occurs. +func (_c *MessageCreateBulk) ExecX(ctx context.Context) { + if err := _c.Exec(ctx); err != nil { + panic(err) + } +} + +// OnConflict allows configuring the `ON CONFLICT` / `ON DUPLICATE KEY` clause +// of the `INSERT` statement. For example: +// +// client.Message.CreateBulk(builders...). +// OnConflict( +// // Update the row with the new values +// // the was proposed for insertion. +// sql.ResolveWithNewValues(), +// ). +// // Override some of the fields with custom +// // update values. +// Update(func(u *ent.MessageUpsert) { +// SetProjectID(v+v). +// }). +// Exec(ctx) +func (_c *MessageCreateBulk) OnConflict(opts ...sql.ConflictOption) *MessageUpsertBulk { + _c.conflict = opts + return &MessageUpsertBulk{ + create: _c, + } +} + +// OnConflictColumns calls `OnConflict` and configures the columns +// as conflict target. Using this option is equivalent to using: +// +// client.Message.Create(). +// OnConflict(sql.ConflictColumns(columns...)). +// Exec(ctx) +func (_c *MessageCreateBulk) OnConflictColumns(columns ...string) *MessageUpsertBulk { + _c.conflict = append(_c.conflict, sql.ConflictColumns(columns...)) + return &MessageUpsertBulk{ + create: _c, + } +} + +// MessageUpsertBulk is the builder for "upsert"-ing +// a bulk of Message nodes. +type MessageUpsertBulk struct { + create *MessageCreateBulk +} + +// UpdateNewValues updates the mutable fields using the new values that +// were set on create. Using this option is equivalent to using: +// +// client.Message.Create(). +// OnConflict( +// sql.ResolveWithNewValues(), +// sql.ResolveWith(func(u *sql.UpdateSet) { +// u.SetIgnore(message.FieldID) +// }), +// ). +// Exec(ctx) +func (u *MessageUpsertBulk) UpdateNewValues() *MessageUpsertBulk { + u.create.conflict = append(u.create.conflict, sql.ResolveWithNewValues()) + u.create.conflict = append(u.create.conflict, sql.ResolveWith(func(s *sql.UpdateSet) { + for _, b := range u.create.builders { + if _, exists := b.mutation.ID(); exists { + s.SetIgnore(message.FieldID) + } + if _, exists := b.mutation.Created(); exists { + s.SetIgnore(message.FieldCreated) + } + } + })) + return u +} + +// Ignore sets each column to itself in case of conflict. +// Using this option is equivalent to using: +// +// client.Message.Create(). +// OnConflict(sql.ResolveWithIgnore()). +// Exec(ctx) +func (u *MessageUpsertBulk) Ignore() *MessageUpsertBulk { + u.create.conflict = append(u.create.conflict, sql.ResolveWithIgnore()) + return u +} + +// DoNothing configures the conflict_action to `DO NOTHING`. +// Supported only by SQLite and PostgreSQL. +func (u *MessageUpsertBulk) DoNothing() *MessageUpsertBulk { + u.create.conflict = append(u.create.conflict, sql.DoNothing()) + return u +} + +// Update allows overriding fields `UPDATE` values. See the MessageCreateBulk.OnConflict +// documentation for more info. +func (u *MessageUpsertBulk) Update(set func(*MessageUpsert)) *MessageUpsertBulk { + u.create.conflict = append(u.create.conflict, sql.ResolveWith(func(update *sql.UpdateSet) { + set(&MessageUpsert{UpdateSet: update}) + })) + return u +} + +// SetProjectID sets the "project_id" field. +func (u *MessageUpsertBulk) SetProjectID(v uuid.UUID) *MessageUpsertBulk { + return u.Update(func(s *MessageUpsert) { + s.SetProjectID(v) + }) +} + +// UpdateProjectID sets the "project_id" field to the value that was provided on create. +func (u *MessageUpsertBulk) UpdateProjectID() *MessageUpsertBulk { + return u.Update(func(s *MessageUpsert) { + s.UpdateProjectID() + }) +} + +// SetSender sets the "sender" field. +func (u *MessageUpsertBulk) SetSender(v string) *MessageUpsertBulk { + return u.Update(func(s *MessageUpsert) { + s.SetSender(v) + }) +} + +// UpdateSender sets the "sender" field to the value that was provided on create. +func (u *MessageUpsertBulk) UpdateSender() *MessageUpsertBulk { + return u.Update(func(s *MessageUpsert) { + s.UpdateSender() + }) +} + +// SetSenderID sets the "sender_id" field. +func (u *MessageUpsertBulk) SetSenderID(v string) *MessageUpsertBulk { + return u.Update(func(s *MessageUpsert) { + s.SetSenderID(v) + }) +} + +// UpdateSenderID sets the "sender_id" field to the value that was provided on create. +func (u *MessageUpsertBulk) UpdateSenderID() *MessageUpsertBulk { + return u.Update(func(s *MessageUpsert) { + s.UpdateSenderID() + }) +} + +// ClearSenderID clears the value of the "sender_id" field. +func (u *MessageUpsertBulk) ClearSenderID() *MessageUpsertBulk { + return u.Update(func(s *MessageUpsert) { + s.ClearSenderID() + }) +} + +// SetRecipient sets the "recipient" field. +func (u *MessageUpsertBulk) SetRecipient(v string) *MessageUpsertBulk { + return u.Update(func(s *MessageUpsert) { + s.SetRecipient(v) + }) +} + +// UpdateRecipient sets the "recipient" field to the value that was provided on create. +func (u *MessageUpsertBulk) UpdateRecipient() *MessageUpsertBulk { + return u.Update(func(s *MessageUpsert) { + s.UpdateRecipient() + }) +} + +// SetRecipientID sets the "recipient_id" field. +func (u *MessageUpsertBulk) SetRecipientID(v string) *MessageUpsertBulk { + return u.Update(func(s *MessageUpsert) { + s.SetRecipientID(v) + }) +} + +// UpdateRecipientID sets the "recipient_id" field to the value that was provided on create. +func (u *MessageUpsertBulk) UpdateRecipientID() *MessageUpsertBulk { + return u.Update(func(s *MessageUpsert) { + s.UpdateRecipientID() + }) +} + +// ClearRecipientID clears the value of the "recipient_id" field. +func (u *MessageUpsertBulk) ClearRecipientID() *MessageUpsertBulk { + return u.Update(func(s *MessageUpsert) { + s.ClearRecipientID() + }) +} + +// SetMsg sets the "msg" field. +func (u *MessageUpsertBulk) SetMsg(v string) *MessageUpsertBulk { + return u.Update(func(s *MessageUpsert) { + s.SetMsg(v) + }) +} + +// UpdateMsg sets the "msg" field to the value that was provided on create. +func (u *MessageUpsertBulk) UpdateMsg() *MessageUpsertBulk { + return u.Update(func(s *MessageUpsert) { + s.UpdateMsg() + }) +} + +// SetType sets the "type" field. +func (u *MessageUpsertBulk) SetType(v string) *MessageUpsertBulk { + return u.Update(func(s *MessageUpsert) { + s.SetType(v) + }) +} + +// UpdateType sets the "type" field to the value that was provided on create. +func (u *MessageUpsertBulk) UpdateType() *MessageUpsertBulk { + return u.Update(func(s *MessageUpsert) { + s.UpdateType() + }) +} + +// SetUrgent sets the "urgent" field. +func (u *MessageUpsertBulk) SetUrgent(v bool) *MessageUpsertBulk { + return u.Update(func(s *MessageUpsert) { + s.SetUrgent(v) + }) +} + +// UpdateUrgent sets the "urgent" field to the value that was provided on create. +func (u *MessageUpsertBulk) UpdateUrgent() *MessageUpsertBulk { + return u.Update(func(s *MessageUpsert) { + s.UpdateUrgent() + }) +} + +// SetBroadcasted sets the "broadcasted" field. +func (u *MessageUpsertBulk) SetBroadcasted(v bool) *MessageUpsertBulk { + return u.Update(func(s *MessageUpsert) { + s.SetBroadcasted(v) + }) +} + +// UpdateBroadcasted sets the "broadcasted" field to the value that was provided on create. +func (u *MessageUpsertBulk) UpdateBroadcasted() *MessageUpsertBulk { + return u.Update(func(s *MessageUpsert) { + s.UpdateBroadcasted() + }) +} + +// SetRead sets the "read" field. +func (u *MessageUpsertBulk) SetRead(v bool) *MessageUpsertBulk { + return u.Update(func(s *MessageUpsert) { + s.SetRead(v) + }) +} + +// UpdateRead sets the "read" field to the value that was provided on create. +func (u *MessageUpsertBulk) UpdateRead() *MessageUpsertBulk { + return u.Update(func(s *MessageUpsert) { + s.UpdateRead() + }) +} + +// SetAgentID sets the "agent_id" field. +func (u *MessageUpsertBulk) SetAgentID(v string) *MessageUpsertBulk { + return u.Update(func(s *MessageUpsert) { + s.SetAgentID(v) + }) +} + +// UpdateAgentID sets the "agent_id" field to the value that was provided on create. +func (u *MessageUpsertBulk) UpdateAgentID() *MessageUpsertBulk { + return u.Update(func(s *MessageUpsert) { + s.UpdateAgentID() + }) +} + +// ClearAgentID clears the value of the "agent_id" field. +func (u *MessageUpsertBulk) ClearAgentID() *MessageUpsertBulk { + return u.Update(func(s *MessageUpsert) { + s.ClearAgentID() + }) +} + +// SetGroupID sets the "group_id" field. +func (u *MessageUpsertBulk) SetGroupID(v string) *MessageUpsertBulk { + return u.Update(func(s *MessageUpsert) { + s.SetGroupID(v) + }) +} + +// UpdateGroupID sets the "group_id" field to the value that was provided on create. +func (u *MessageUpsertBulk) UpdateGroupID() *MessageUpsertBulk { + return u.Update(func(s *MessageUpsert) { + s.UpdateGroupID() + }) +} + +// ClearGroupID clears the value of the "group_id" field. +func (u *MessageUpsertBulk) ClearGroupID() *MessageUpsertBulk { + return u.Update(func(s *MessageUpsert) { + s.ClearGroupID() + }) +} + +// SetDispatchState sets the "dispatch_state" field. +func (u *MessageUpsertBulk) SetDispatchState(v string) *MessageUpsertBulk { + return u.Update(func(s *MessageUpsert) { + s.SetDispatchState(v) + }) +} + +// UpdateDispatchState sets the "dispatch_state" field to the value that was provided on create. +func (u *MessageUpsertBulk) UpdateDispatchState() *MessageUpsertBulk { + return u.Update(func(s *MessageUpsert) { + s.UpdateDispatchState() + }) +} + +// SetDispatchedAt sets the "dispatched_at" field. +func (u *MessageUpsertBulk) SetDispatchedAt(v time.Time) *MessageUpsertBulk { + return u.Update(func(s *MessageUpsert) { + s.SetDispatchedAt(v) + }) +} + +// UpdateDispatchedAt sets the "dispatched_at" field to the value that was provided on create. +func (u *MessageUpsertBulk) UpdateDispatchedAt() *MessageUpsertBulk { + return u.Update(func(s *MessageUpsert) { + s.UpdateDispatchedAt() + }) +} + +// ClearDispatchedAt clears the value of the "dispatched_at" field. +func (u *MessageUpsertBulk) ClearDispatchedAt() *MessageUpsertBulk { + return u.Update(func(s *MessageUpsert) { + s.ClearDispatchedAt() + }) +} + +// Exec executes the query. +func (u *MessageUpsertBulk) Exec(ctx context.Context) error { + if u.create.err != nil { + return u.create.err + } + for i, b := range u.create.builders { + if len(b.conflict) != 0 { + return fmt.Errorf("ent: OnConflict was set for builder %d. Set it on the MessageCreateBulk instead", i) + } + } + if len(u.create.conflict) == 0 { + return errors.New("ent: missing options for MessageCreateBulk.OnConflict") + } + return u.create.Exec(ctx) +} + +// ExecX is like Exec, but panics if an error occurs. +func (u *MessageUpsertBulk) ExecX(ctx context.Context) { + if err := u.create.Exec(ctx); err != nil { + panic(err) + } +} diff --git a/pkg/ent/message_delete.go b/pkg/ent/message_delete.go new file mode 100644 index 000000000..18db7ee5d --- /dev/null +++ b/pkg/ent/message_delete.go @@ -0,0 +1,88 @@ +// Code generated by ent, DO NOT EDIT. + +package ent + +import ( + "context" + + "entgo.io/ent/dialect/sql" + "entgo.io/ent/dialect/sql/sqlgraph" + "entgo.io/ent/schema/field" + "github.com/GoogleCloudPlatform/scion/pkg/ent/message" + "github.com/GoogleCloudPlatform/scion/pkg/ent/predicate" +) + +// MessageDelete is the builder for deleting a Message entity. +type MessageDelete struct { + config + hooks []Hook + mutation *MessageMutation +} + +// Where appends a list predicates to the MessageDelete builder. +func (_d *MessageDelete) Where(ps ...predicate.Message) *MessageDelete { + _d.mutation.Where(ps...) + return _d +} + +// Exec executes the deletion query and returns how many vertices were deleted. +func (_d *MessageDelete) Exec(ctx context.Context) (int, error) { + return withHooks(ctx, _d.sqlExec, _d.mutation, _d.hooks) +} + +// ExecX is like Exec, but panics if an error occurs. +func (_d *MessageDelete) ExecX(ctx context.Context) int { + n, err := _d.Exec(ctx) + if err != nil { + panic(err) + } + return n +} + +func (_d *MessageDelete) sqlExec(ctx context.Context) (int, error) { + _spec := sqlgraph.NewDeleteSpec(message.Table, sqlgraph.NewFieldSpec(message.FieldID, field.TypeUUID)) + if ps := _d.mutation.predicates; len(ps) > 0 { + _spec.Predicate = func(selector *sql.Selector) { + for i := range ps { + ps[i](selector) + } + } + } + affected, err := sqlgraph.DeleteNodes(ctx, _d.driver, _spec) + if err != nil && sqlgraph.IsConstraintError(err) { + err = &ConstraintError{msg: err.Error(), wrap: err} + } + _d.mutation.done = true + return affected, err +} + +// MessageDeleteOne is the builder for deleting a single Message entity. +type MessageDeleteOne struct { + _d *MessageDelete +} + +// Where appends a list predicates to the MessageDelete builder. +func (_d *MessageDeleteOne) Where(ps ...predicate.Message) *MessageDeleteOne { + _d._d.mutation.Where(ps...) + return _d +} + +// Exec executes the deletion query. +func (_d *MessageDeleteOne) Exec(ctx context.Context) error { + n, err := _d._d.Exec(ctx) + switch { + case err != nil: + return err + case n == 0: + return &NotFoundError{message.Label} + default: + return nil + } +} + +// ExecX is like Exec, but panics if an error occurs. +func (_d *MessageDeleteOne) ExecX(ctx context.Context) { + if err := _d.Exec(ctx); err != nil { + panic(err) + } +} diff --git a/pkg/ent/message_query.go b/pkg/ent/message_query.go new file mode 100644 index 000000000..3f0667250 --- /dev/null +++ b/pkg/ent/message_query.go @@ -0,0 +1,565 @@ +// Code generated by ent, DO NOT EDIT. + +package ent + +import ( + "context" + "fmt" + "math" + + "entgo.io/ent" + "entgo.io/ent/dialect" + "entgo.io/ent/dialect/sql" + "entgo.io/ent/dialect/sql/sqlgraph" + "entgo.io/ent/schema/field" + "github.com/GoogleCloudPlatform/scion/pkg/ent/message" + "github.com/GoogleCloudPlatform/scion/pkg/ent/predicate" + "github.com/google/uuid" +) + +// MessageQuery is the builder for querying Message entities. +type MessageQuery struct { + config + ctx *QueryContext + order []message.OrderOption + inters []Interceptor + predicates []predicate.Message + modifiers []func(*sql.Selector) + // intermediate query (i.e. traversal path). + sql *sql.Selector + path func(context.Context) (*sql.Selector, error) +} + +// Where adds a new predicate for the MessageQuery builder. +func (_q *MessageQuery) Where(ps ...predicate.Message) *MessageQuery { + _q.predicates = append(_q.predicates, ps...) + return _q +} + +// Limit the number of records to be returned by this query. +func (_q *MessageQuery) Limit(limit int) *MessageQuery { + _q.ctx.Limit = &limit + return _q +} + +// Offset to start from. +func (_q *MessageQuery) Offset(offset int) *MessageQuery { + _q.ctx.Offset = &offset + return _q +} + +// Unique configures the query builder to filter duplicate records on query. +// By default, unique is set to true, and can be disabled using this method. +func (_q *MessageQuery) Unique(unique bool) *MessageQuery { + _q.ctx.Unique = &unique + return _q +} + +// Order specifies how the records should be ordered. +func (_q *MessageQuery) Order(o ...message.OrderOption) *MessageQuery { + _q.order = append(_q.order, o...) + return _q +} + +// First returns the first Message entity from the query. +// Returns a *NotFoundError when no Message was found. +func (_q *MessageQuery) First(ctx context.Context) (*Message, error) { + nodes, err := _q.Limit(1).All(setContextOp(ctx, _q.ctx, ent.OpQueryFirst)) + if err != nil { + return nil, err + } + if len(nodes) == 0 { + return nil, &NotFoundError{message.Label} + } + return nodes[0], nil +} + +// FirstX is like First, but panics if an error occurs. +func (_q *MessageQuery) FirstX(ctx context.Context) *Message { + node, err := _q.First(ctx) + if err != nil && !IsNotFound(err) { + panic(err) + } + return node +} + +// FirstID returns the first Message ID from the query. +// Returns a *NotFoundError when no Message ID was found. +func (_q *MessageQuery) FirstID(ctx context.Context) (id uuid.UUID, err error) { + var ids []uuid.UUID + if ids, err = _q.Limit(1).IDs(setContextOp(ctx, _q.ctx, ent.OpQueryFirstID)); err != nil { + return + } + if len(ids) == 0 { + err = &NotFoundError{message.Label} + return + } + return ids[0], nil +} + +// FirstIDX is like FirstID, but panics if an error occurs. +func (_q *MessageQuery) FirstIDX(ctx context.Context) uuid.UUID { + id, err := _q.FirstID(ctx) + if err != nil && !IsNotFound(err) { + panic(err) + } + return id +} + +// Only returns a single Message entity found by the query, ensuring it only returns one. +// Returns a *NotSingularError when more than one Message entity is found. +// Returns a *NotFoundError when no Message entities are found. +func (_q *MessageQuery) Only(ctx context.Context) (*Message, error) { + nodes, err := _q.Limit(2).All(setContextOp(ctx, _q.ctx, ent.OpQueryOnly)) + if err != nil { + return nil, err + } + switch len(nodes) { + case 1: + return nodes[0], nil + case 0: + return nil, &NotFoundError{message.Label} + default: + return nil, &NotSingularError{message.Label} + } +} + +// OnlyX is like Only, but panics if an error occurs. +func (_q *MessageQuery) OnlyX(ctx context.Context) *Message { + node, err := _q.Only(ctx) + if err != nil { + panic(err) + } + return node +} + +// OnlyID is like Only, but returns the only Message ID in the query. +// Returns a *NotSingularError when more than one Message ID is found. +// Returns a *NotFoundError when no entities are found. +func (_q *MessageQuery) OnlyID(ctx context.Context) (id uuid.UUID, err error) { + var ids []uuid.UUID + if ids, err = _q.Limit(2).IDs(setContextOp(ctx, _q.ctx, ent.OpQueryOnlyID)); err != nil { + return + } + switch len(ids) { + case 1: + id = ids[0] + case 0: + err = &NotFoundError{message.Label} + default: + err = &NotSingularError{message.Label} + } + return +} + +// OnlyIDX is like OnlyID, but panics if an error occurs. +func (_q *MessageQuery) OnlyIDX(ctx context.Context) uuid.UUID { + id, err := _q.OnlyID(ctx) + if err != nil { + panic(err) + } + return id +} + +// All executes the query and returns a list of Messages. +func (_q *MessageQuery) All(ctx context.Context) ([]*Message, error) { + ctx = setContextOp(ctx, _q.ctx, ent.OpQueryAll) + if err := _q.prepareQuery(ctx); err != nil { + return nil, err + } + qr := querierAll[[]*Message, *MessageQuery]() + return withInterceptors[[]*Message](ctx, _q, qr, _q.inters) +} + +// AllX is like All, but panics if an error occurs. +func (_q *MessageQuery) AllX(ctx context.Context) []*Message { + nodes, err := _q.All(ctx) + if err != nil { + panic(err) + } + return nodes +} + +// IDs executes the query and returns a list of Message IDs. +func (_q *MessageQuery) IDs(ctx context.Context) (ids []uuid.UUID, err error) { + if _q.ctx.Unique == nil && _q.path != nil { + _q.Unique(true) + } + ctx = setContextOp(ctx, _q.ctx, ent.OpQueryIDs) + if err = _q.Select(message.FieldID).Scan(ctx, &ids); err != nil { + return nil, err + } + return ids, nil +} + +// IDsX is like IDs, but panics if an error occurs. +func (_q *MessageQuery) IDsX(ctx context.Context) []uuid.UUID { + ids, err := _q.IDs(ctx) + if err != nil { + panic(err) + } + return ids +} + +// Count returns the count of the given query. +func (_q *MessageQuery) Count(ctx context.Context) (int, error) { + ctx = setContextOp(ctx, _q.ctx, ent.OpQueryCount) + if err := _q.prepareQuery(ctx); err != nil { + return 0, err + } + return withInterceptors[int](ctx, _q, querierCount[*MessageQuery](), _q.inters) +} + +// CountX is like Count, but panics if an error occurs. +func (_q *MessageQuery) CountX(ctx context.Context) int { + count, err := _q.Count(ctx) + if err != nil { + panic(err) + } + return count +} + +// Exist returns true if the query has elements in the graph. +func (_q *MessageQuery) Exist(ctx context.Context) (bool, error) { + ctx = setContextOp(ctx, _q.ctx, ent.OpQueryExist) + switch _, err := _q.FirstID(ctx); { + case IsNotFound(err): + return false, nil + case err != nil: + return false, fmt.Errorf("ent: check existence: %w", err) + default: + return true, nil + } +} + +// ExistX is like Exist, but panics if an error occurs. +func (_q *MessageQuery) ExistX(ctx context.Context) bool { + exist, err := _q.Exist(ctx) + if err != nil { + panic(err) + } + return exist +} + +// Clone returns a duplicate of the MessageQuery builder, including all associated steps. It can be +// used to prepare common query builders and use them differently after the clone is made. +func (_q *MessageQuery) Clone() *MessageQuery { + if _q == nil { + return nil + } + return &MessageQuery{ + config: _q.config, + ctx: _q.ctx.Clone(), + order: append([]message.OrderOption{}, _q.order...), + inters: append([]Interceptor{}, _q.inters...), + predicates: append([]predicate.Message{}, _q.predicates...), + // clone intermediate query. + sql: _q.sql.Clone(), + path: _q.path, + } +} + +// GroupBy is used to group vertices by one or more fields/columns. +// It is often used with aggregate functions, like: count, max, mean, min, sum. +// +// Example: +// +// var v []struct { +// ProjectID uuid.UUID `json:"project_id,omitempty"` +// Count int `json:"count,omitempty"` +// } +// +// client.Message.Query(). +// GroupBy(message.FieldProjectID). +// Aggregate(ent.Count()). +// Scan(ctx, &v) +func (_q *MessageQuery) GroupBy(field string, fields ...string) *MessageGroupBy { + _q.ctx.Fields = append([]string{field}, fields...) + grbuild := &MessageGroupBy{build: _q} + grbuild.flds = &_q.ctx.Fields + grbuild.label = message.Label + grbuild.scan = grbuild.Scan + return grbuild +} + +// Select allows the selection one or more fields/columns for the given query, +// instead of selecting all fields in the entity. +// +// Example: +// +// var v []struct { +// ProjectID uuid.UUID `json:"project_id,omitempty"` +// } +// +// client.Message.Query(). +// Select(message.FieldProjectID). +// Scan(ctx, &v) +func (_q *MessageQuery) Select(fields ...string) *MessageSelect { + _q.ctx.Fields = append(_q.ctx.Fields, fields...) + sbuild := &MessageSelect{MessageQuery: _q} + sbuild.label = message.Label + sbuild.flds, sbuild.scan = &_q.ctx.Fields, sbuild.Scan + return sbuild +} + +// Aggregate returns a MessageSelect configured with the given aggregations. +func (_q *MessageQuery) Aggregate(fns ...AggregateFunc) *MessageSelect { + return _q.Select().Aggregate(fns...) +} + +func (_q *MessageQuery) prepareQuery(ctx context.Context) error { + for _, inter := range _q.inters { + if inter == nil { + return fmt.Errorf("ent: uninitialized interceptor (forgotten import ent/runtime?)") + } + if trv, ok := inter.(Traverser); ok { + if err := trv.Traverse(ctx, _q); err != nil { + return err + } + } + } + for _, f := range _q.ctx.Fields { + if !message.ValidColumn(f) { + return &ValidationError{Name: f, err: fmt.Errorf("ent: invalid field %q for query", f)} + } + } + if _q.path != nil { + prev, err := _q.path(ctx) + if err != nil { + return err + } + _q.sql = prev + } + return nil +} + +func (_q *MessageQuery) sqlAll(ctx context.Context, hooks ...queryHook) ([]*Message, error) { + var ( + nodes = []*Message{} + _spec = _q.querySpec() + ) + _spec.ScanValues = func(columns []string) ([]any, error) { + return (*Message).scanValues(nil, columns) + } + _spec.Assign = func(columns []string, values []any) error { + node := &Message{config: _q.config} + nodes = append(nodes, node) + return node.assignValues(columns, values) + } + if len(_q.modifiers) > 0 { + _spec.Modifiers = _q.modifiers + } + for i := range hooks { + hooks[i](ctx, _spec) + } + if err := sqlgraph.QueryNodes(ctx, _q.driver, _spec); err != nil { + return nil, err + } + if len(nodes) == 0 { + return nodes, nil + } + return nodes, nil +} + +func (_q *MessageQuery) sqlCount(ctx context.Context) (int, error) { + _spec := _q.querySpec() + if len(_q.modifiers) > 0 { + _spec.Modifiers = _q.modifiers + } + _spec.Node.Columns = _q.ctx.Fields + if len(_q.ctx.Fields) > 0 { + _spec.Unique = _q.ctx.Unique != nil && *_q.ctx.Unique + } + return sqlgraph.CountNodes(ctx, _q.driver, _spec) +} + +func (_q *MessageQuery) querySpec() *sqlgraph.QuerySpec { + _spec := sqlgraph.NewQuerySpec(message.Table, message.Columns, sqlgraph.NewFieldSpec(message.FieldID, field.TypeUUID)) + _spec.From = _q.sql + if unique := _q.ctx.Unique; unique != nil { + _spec.Unique = *unique + } else if _q.path != nil { + _spec.Unique = true + } + if fields := _q.ctx.Fields; len(fields) > 0 { + _spec.Node.Columns = make([]string, 0, len(fields)) + _spec.Node.Columns = append(_spec.Node.Columns, message.FieldID) + for i := range fields { + if fields[i] != message.FieldID { + _spec.Node.Columns = append(_spec.Node.Columns, fields[i]) + } + } + } + if ps := _q.predicates; len(ps) > 0 { + _spec.Predicate = func(selector *sql.Selector) { + for i := range ps { + ps[i](selector) + } + } + } + if limit := _q.ctx.Limit; limit != nil { + _spec.Limit = *limit + } + if offset := _q.ctx.Offset; offset != nil { + _spec.Offset = *offset + } + if ps := _q.order; len(ps) > 0 { + _spec.Order = func(selector *sql.Selector) { + for i := range ps { + ps[i](selector) + } + } + } + return _spec +} + +func (_q *MessageQuery) sqlQuery(ctx context.Context) *sql.Selector { + builder := sql.Dialect(_q.driver.Dialect()) + t1 := builder.Table(message.Table) + columns := _q.ctx.Fields + if len(columns) == 0 { + columns = message.Columns + } + selector := builder.Select(t1.Columns(columns...)...).From(t1) + if _q.sql != nil { + selector = _q.sql + selector.Select(selector.Columns(columns...)...) + } + if _q.ctx.Unique != nil && *_q.ctx.Unique { + selector.Distinct() + } + for _, m := range _q.modifiers { + m(selector) + } + for _, p := range _q.predicates { + p(selector) + } + for _, p := range _q.order { + p(selector) + } + if offset := _q.ctx.Offset; offset != nil { + // limit is mandatory for offset clause. We start + // with default value, and override it below if needed. + selector.Offset(*offset).Limit(math.MaxInt32) + } + if limit := _q.ctx.Limit; limit != nil { + selector.Limit(*limit) + } + return selector +} + +// ForUpdate locks the selected rows against concurrent updates, and prevent them from being +// updated, deleted or "selected ... for update" by other sessions, until the transaction is +// either committed or rolled-back. +func (_q *MessageQuery) ForUpdate(opts ...sql.LockOption) *MessageQuery { + if _q.driver.Dialect() == dialect.Postgres { + _q.Unique(false) + } + _q.modifiers = append(_q.modifiers, func(s *sql.Selector) { + s.ForUpdate(opts...) + }) + return _q +} + +// ForShare behaves similarly to ForUpdate, except that it acquires a shared mode lock +// on any rows that are read. Other sessions can read the rows, but cannot modify them +// until your transaction commits. +func (_q *MessageQuery) ForShare(opts ...sql.LockOption) *MessageQuery { + if _q.driver.Dialect() == dialect.Postgres { + _q.Unique(false) + } + _q.modifiers = append(_q.modifiers, func(s *sql.Selector) { + s.ForShare(opts...) + }) + return _q +} + +// MessageGroupBy is the group-by builder for Message entities. +type MessageGroupBy struct { + selector + build *MessageQuery +} + +// Aggregate adds the given aggregation functions to the group-by query. +func (_g *MessageGroupBy) Aggregate(fns ...AggregateFunc) *MessageGroupBy { + _g.fns = append(_g.fns, fns...) + return _g +} + +// Scan applies the selector query and scans the result into the given value. +func (_g *MessageGroupBy) Scan(ctx context.Context, v any) error { + ctx = setContextOp(ctx, _g.build.ctx, ent.OpQueryGroupBy) + if err := _g.build.prepareQuery(ctx); err != nil { + return err + } + return scanWithInterceptors[*MessageQuery, *MessageGroupBy](ctx, _g.build, _g, _g.build.inters, v) +} + +func (_g *MessageGroupBy) sqlScan(ctx context.Context, root *MessageQuery, v any) error { + selector := root.sqlQuery(ctx).Select() + aggregation := make([]string, 0, len(_g.fns)) + for _, fn := range _g.fns { + aggregation = append(aggregation, fn(selector)) + } + if len(selector.SelectedColumns()) == 0 { + columns := make([]string, 0, len(*_g.flds)+len(_g.fns)) + for _, f := range *_g.flds { + columns = append(columns, selector.C(f)) + } + columns = append(columns, aggregation...) + selector.Select(columns...) + } + selector.GroupBy(selector.Columns(*_g.flds...)...) + if err := selector.Err(); err != nil { + return err + } + rows := &sql.Rows{} + query, args := selector.Query() + if err := _g.build.driver.Query(ctx, query, args, rows); err != nil { + return err + } + defer rows.Close() + return sql.ScanSlice(rows, v) +} + +// MessageSelect is the builder for selecting fields of Message entities. +type MessageSelect struct { + *MessageQuery + selector +} + +// Aggregate adds the given aggregation functions to the selector query. +func (_s *MessageSelect) Aggregate(fns ...AggregateFunc) *MessageSelect { + _s.fns = append(_s.fns, fns...) + return _s +} + +// Scan applies the selector query and scans the result into the given value. +func (_s *MessageSelect) Scan(ctx context.Context, v any) error { + ctx = setContextOp(ctx, _s.ctx, ent.OpQuerySelect) + if err := _s.prepareQuery(ctx); err != nil { + return err + } + return scanWithInterceptors[*MessageQuery, *MessageSelect](ctx, _s.MessageQuery, _s, _s.inters, v) +} + +func (_s *MessageSelect) sqlScan(ctx context.Context, root *MessageQuery, v any) error { + selector := root.sqlQuery(ctx) + aggregation := make([]string, 0, len(_s.fns)) + for _, fn := range _s.fns { + aggregation = append(aggregation, fn(selector)) + } + switch n := len(*_s.selector.flds); { + case n == 0 && len(aggregation) > 0: + selector.Select(aggregation...) + case n != 0 && len(aggregation) > 0: + selector.AppendSelect(aggregation...) + } + rows := &sql.Rows{} + query, args := selector.Query() + if err := _s.driver.Query(ctx, query, args, rows); err != nil { + return err + } + defer rows.Close() + return sql.ScanSlice(rows, v) +} diff --git a/pkg/ent/message_update.go b/pkg/ent/message_update.go new file mode 100644 index 000000000..5e925590f --- /dev/null +++ b/pkg/ent/message_update.go @@ -0,0 +1,789 @@ +// Code generated by ent, DO NOT EDIT. + +package ent + +import ( + "context" + "errors" + "fmt" + "time" + + "entgo.io/ent/dialect/sql" + "entgo.io/ent/dialect/sql/sqlgraph" + "entgo.io/ent/schema/field" + "github.com/GoogleCloudPlatform/scion/pkg/ent/message" + "github.com/GoogleCloudPlatform/scion/pkg/ent/predicate" + "github.com/google/uuid" +) + +// MessageUpdate is the builder for updating Message entities. +type MessageUpdate struct { + config + hooks []Hook + mutation *MessageMutation +} + +// Where appends a list predicates to the MessageUpdate builder. +func (_u *MessageUpdate) Where(ps ...predicate.Message) *MessageUpdate { + _u.mutation.Where(ps...) + return _u +} + +// SetProjectID sets the "project_id" field. +func (_u *MessageUpdate) SetProjectID(v uuid.UUID) *MessageUpdate { + _u.mutation.SetProjectID(v) + return _u +} + +// SetNillableProjectID sets the "project_id" field if the given value is not nil. +func (_u *MessageUpdate) SetNillableProjectID(v *uuid.UUID) *MessageUpdate { + if v != nil { + _u.SetProjectID(*v) + } + return _u +} + +// SetSender sets the "sender" field. +func (_u *MessageUpdate) SetSender(v string) *MessageUpdate { + _u.mutation.SetSender(v) + return _u +} + +// SetNillableSender sets the "sender" field if the given value is not nil. +func (_u *MessageUpdate) SetNillableSender(v *string) *MessageUpdate { + if v != nil { + _u.SetSender(*v) + } + return _u +} + +// SetSenderID sets the "sender_id" field. +func (_u *MessageUpdate) SetSenderID(v string) *MessageUpdate { + _u.mutation.SetSenderID(v) + return _u +} + +// SetNillableSenderID sets the "sender_id" field if the given value is not nil. +func (_u *MessageUpdate) SetNillableSenderID(v *string) *MessageUpdate { + if v != nil { + _u.SetSenderID(*v) + } + return _u +} + +// ClearSenderID clears the value of the "sender_id" field. +func (_u *MessageUpdate) ClearSenderID() *MessageUpdate { + _u.mutation.ClearSenderID() + return _u +} + +// SetRecipient sets the "recipient" field. +func (_u *MessageUpdate) SetRecipient(v string) *MessageUpdate { + _u.mutation.SetRecipient(v) + return _u +} + +// SetNillableRecipient sets the "recipient" field if the given value is not nil. +func (_u *MessageUpdate) SetNillableRecipient(v *string) *MessageUpdate { + if v != nil { + _u.SetRecipient(*v) + } + return _u +} + +// SetRecipientID sets the "recipient_id" field. +func (_u *MessageUpdate) SetRecipientID(v string) *MessageUpdate { + _u.mutation.SetRecipientID(v) + return _u +} + +// SetNillableRecipientID sets the "recipient_id" field if the given value is not nil. +func (_u *MessageUpdate) SetNillableRecipientID(v *string) *MessageUpdate { + if v != nil { + _u.SetRecipientID(*v) + } + return _u +} + +// ClearRecipientID clears the value of the "recipient_id" field. +func (_u *MessageUpdate) ClearRecipientID() *MessageUpdate { + _u.mutation.ClearRecipientID() + return _u +} + +// SetMsg sets the "msg" field. +func (_u *MessageUpdate) SetMsg(v string) *MessageUpdate { + _u.mutation.SetMsg(v) + return _u +} + +// SetNillableMsg sets the "msg" field if the given value is not nil. +func (_u *MessageUpdate) SetNillableMsg(v *string) *MessageUpdate { + if v != nil { + _u.SetMsg(*v) + } + return _u +} + +// SetType sets the "type" field. +func (_u *MessageUpdate) SetType(v string) *MessageUpdate { + _u.mutation.SetType(v) + return _u +} + +// SetNillableType sets the "type" field if the given value is not nil. +func (_u *MessageUpdate) SetNillableType(v *string) *MessageUpdate { + if v != nil { + _u.SetType(*v) + } + return _u +} + +// SetUrgent sets the "urgent" field. +func (_u *MessageUpdate) SetUrgent(v bool) *MessageUpdate { + _u.mutation.SetUrgent(v) + return _u +} + +// SetNillableUrgent sets the "urgent" field if the given value is not nil. +func (_u *MessageUpdate) SetNillableUrgent(v *bool) *MessageUpdate { + if v != nil { + _u.SetUrgent(*v) + } + return _u +} + +// SetBroadcasted sets the "broadcasted" field. +func (_u *MessageUpdate) SetBroadcasted(v bool) *MessageUpdate { + _u.mutation.SetBroadcasted(v) + return _u +} + +// SetNillableBroadcasted sets the "broadcasted" field if the given value is not nil. +func (_u *MessageUpdate) SetNillableBroadcasted(v *bool) *MessageUpdate { + if v != nil { + _u.SetBroadcasted(*v) + } + return _u +} + +// SetRead sets the "read" field. +func (_u *MessageUpdate) SetRead(v bool) *MessageUpdate { + _u.mutation.SetRead(v) + return _u +} + +// SetNillableRead sets the "read" field if the given value is not nil. +func (_u *MessageUpdate) SetNillableRead(v *bool) *MessageUpdate { + if v != nil { + _u.SetRead(*v) + } + return _u +} + +// SetAgentID sets the "agent_id" field. +func (_u *MessageUpdate) SetAgentID(v string) *MessageUpdate { + _u.mutation.SetAgentID(v) + return _u +} + +// SetNillableAgentID sets the "agent_id" field if the given value is not nil. +func (_u *MessageUpdate) SetNillableAgentID(v *string) *MessageUpdate { + if v != nil { + _u.SetAgentID(*v) + } + return _u +} + +// ClearAgentID clears the value of the "agent_id" field. +func (_u *MessageUpdate) ClearAgentID() *MessageUpdate { + _u.mutation.ClearAgentID() + return _u +} + +// SetGroupID sets the "group_id" field. +func (_u *MessageUpdate) SetGroupID(v string) *MessageUpdate { + _u.mutation.SetGroupID(v) + return _u +} + +// SetNillableGroupID sets the "group_id" field if the given value is not nil. +func (_u *MessageUpdate) SetNillableGroupID(v *string) *MessageUpdate { + if v != nil { + _u.SetGroupID(*v) + } + return _u +} + +// ClearGroupID clears the value of the "group_id" field. +func (_u *MessageUpdate) ClearGroupID() *MessageUpdate { + _u.mutation.ClearGroupID() + return _u +} + +// SetDispatchState sets the "dispatch_state" field. +func (_u *MessageUpdate) SetDispatchState(v string) *MessageUpdate { + _u.mutation.SetDispatchState(v) + return _u +} + +// SetNillableDispatchState sets the "dispatch_state" field if the given value is not nil. +func (_u *MessageUpdate) SetNillableDispatchState(v *string) *MessageUpdate { + if v != nil { + _u.SetDispatchState(*v) + } + return _u +} + +// SetDispatchedAt sets the "dispatched_at" field. +func (_u *MessageUpdate) SetDispatchedAt(v time.Time) *MessageUpdate { + _u.mutation.SetDispatchedAt(v) + return _u +} + +// SetNillableDispatchedAt sets the "dispatched_at" field if the given value is not nil. +func (_u *MessageUpdate) SetNillableDispatchedAt(v *time.Time) *MessageUpdate { + if v != nil { + _u.SetDispatchedAt(*v) + } + return _u +} + +// ClearDispatchedAt clears the value of the "dispatched_at" field. +func (_u *MessageUpdate) ClearDispatchedAt() *MessageUpdate { + _u.mutation.ClearDispatchedAt() + return _u +} + +// Mutation returns the MessageMutation object of the builder. +func (_u *MessageUpdate) Mutation() *MessageMutation { + return _u.mutation +} + +// Save executes the query and returns the number of nodes affected by the update operation. +func (_u *MessageUpdate) Save(ctx context.Context) (int, error) { + return withHooks(ctx, _u.sqlSave, _u.mutation, _u.hooks) +} + +// SaveX is like Save, but panics if an error occurs. +func (_u *MessageUpdate) SaveX(ctx context.Context) int { + affected, err := _u.Save(ctx) + if err != nil { + panic(err) + } + return affected +} + +// Exec executes the query. +func (_u *MessageUpdate) Exec(ctx context.Context) error { + _, err := _u.Save(ctx) + return err +} + +// ExecX is like Exec, but panics if an error occurs. +func (_u *MessageUpdate) ExecX(ctx context.Context) { + if err := _u.Exec(ctx); err != nil { + panic(err) + } +} + +// check runs all checks and user-defined validators on the builder. +func (_u *MessageUpdate) check() error { + if v, ok := _u.mutation.Sender(); ok { + if err := message.SenderValidator(v); err != nil { + return &ValidationError{Name: "sender", err: fmt.Errorf(`ent: validator failed for field "Message.sender": %w`, err)} + } + } + if v, ok := _u.mutation.Recipient(); ok { + if err := message.RecipientValidator(v); err != nil { + return &ValidationError{Name: "recipient", err: fmt.Errorf(`ent: validator failed for field "Message.recipient": %w`, err)} + } + } + if v, ok := _u.mutation.Msg(); ok { + if err := message.MsgValidator(v); err != nil { + return &ValidationError{Name: "msg", err: fmt.Errorf(`ent: validator failed for field "Message.msg": %w`, err)} + } + } + return nil +} + +func (_u *MessageUpdate) sqlSave(ctx context.Context) (_node int, err error) { + if err := _u.check(); err != nil { + return _node, err + } + _spec := sqlgraph.NewUpdateSpec(message.Table, message.Columns, sqlgraph.NewFieldSpec(message.FieldID, field.TypeUUID)) + if ps := _u.mutation.predicates; len(ps) > 0 { + _spec.Predicate = func(selector *sql.Selector) { + for i := range ps { + ps[i](selector) + } + } + } + if value, ok := _u.mutation.ProjectID(); ok { + _spec.SetField(message.FieldProjectID, field.TypeUUID, value) + } + if value, ok := _u.mutation.Sender(); ok { + _spec.SetField(message.FieldSender, field.TypeString, value) + } + if value, ok := _u.mutation.SenderID(); ok { + _spec.SetField(message.FieldSenderID, field.TypeString, value) + } + if _u.mutation.SenderIDCleared() { + _spec.ClearField(message.FieldSenderID, field.TypeString) + } + if value, ok := _u.mutation.Recipient(); ok { + _spec.SetField(message.FieldRecipient, field.TypeString, value) + } + if value, ok := _u.mutation.RecipientID(); ok { + _spec.SetField(message.FieldRecipientID, field.TypeString, value) + } + if _u.mutation.RecipientIDCleared() { + _spec.ClearField(message.FieldRecipientID, field.TypeString) + } + if value, ok := _u.mutation.Msg(); ok { + _spec.SetField(message.FieldMsg, field.TypeString, value) + } + if value, ok := _u.mutation.GetType(); ok { + _spec.SetField(message.FieldType, field.TypeString, value) + } + if value, ok := _u.mutation.Urgent(); ok { + _spec.SetField(message.FieldUrgent, field.TypeBool, value) + } + if value, ok := _u.mutation.Broadcasted(); ok { + _spec.SetField(message.FieldBroadcasted, field.TypeBool, value) + } + if value, ok := _u.mutation.Read(); ok { + _spec.SetField(message.FieldRead, field.TypeBool, value) + } + if value, ok := _u.mutation.AgentID(); ok { + _spec.SetField(message.FieldAgentID, field.TypeString, value) + } + if _u.mutation.AgentIDCleared() { + _spec.ClearField(message.FieldAgentID, field.TypeString) + } + if value, ok := _u.mutation.GroupID(); ok { + _spec.SetField(message.FieldGroupID, field.TypeString, value) + } + if _u.mutation.GroupIDCleared() { + _spec.ClearField(message.FieldGroupID, field.TypeString) + } + if value, ok := _u.mutation.DispatchState(); ok { + _spec.SetField(message.FieldDispatchState, field.TypeString, value) + } + if value, ok := _u.mutation.DispatchedAt(); ok { + _spec.SetField(message.FieldDispatchedAt, field.TypeTime, value) + } + if _u.mutation.DispatchedAtCleared() { + _spec.ClearField(message.FieldDispatchedAt, field.TypeTime) + } + if _node, err = sqlgraph.UpdateNodes(ctx, _u.driver, _spec); err != nil { + if _, ok := err.(*sqlgraph.NotFoundError); ok { + err = &NotFoundError{message.Label} + } else if sqlgraph.IsConstraintError(err) { + err = &ConstraintError{msg: err.Error(), wrap: err} + } + return 0, err + } + _u.mutation.done = true + return _node, nil +} + +// MessageUpdateOne is the builder for updating a single Message entity. +type MessageUpdateOne struct { + config + fields []string + hooks []Hook + mutation *MessageMutation +} + +// SetProjectID sets the "project_id" field. +func (_u *MessageUpdateOne) SetProjectID(v uuid.UUID) *MessageUpdateOne { + _u.mutation.SetProjectID(v) + return _u +} + +// SetNillableProjectID sets the "project_id" field if the given value is not nil. +func (_u *MessageUpdateOne) SetNillableProjectID(v *uuid.UUID) *MessageUpdateOne { + if v != nil { + _u.SetProjectID(*v) + } + return _u +} + +// SetSender sets the "sender" field. +func (_u *MessageUpdateOne) SetSender(v string) *MessageUpdateOne { + _u.mutation.SetSender(v) + return _u +} + +// SetNillableSender sets the "sender" field if the given value is not nil. +func (_u *MessageUpdateOne) SetNillableSender(v *string) *MessageUpdateOne { + if v != nil { + _u.SetSender(*v) + } + return _u +} + +// SetSenderID sets the "sender_id" field. +func (_u *MessageUpdateOne) SetSenderID(v string) *MessageUpdateOne { + _u.mutation.SetSenderID(v) + return _u +} + +// SetNillableSenderID sets the "sender_id" field if the given value is not nil. +func (_u *MessageUpdateOne) SetNillableSenderID(v *string) *MessageUpdateOne { + if v != nil { + _u.SetSenderID(*v) + } + return _u +} + +// ClearSenderID clears the value of the "sender_id" field. +func (_u *MessageUpdateOne) ClearSenderID() *MessageUpdateOne { + _u.mutation.ClearSenderID() + return _u +} + +// SetRecipient sets the "recipient" field. +func (_u *MessageUpdateOne) SetRecipient(v string) *MessageUpdateOne { + _u.mutation.SetRecipient(v) + return _u +} + +// SetNillableRecipient sets the "recipient" field if the given value is not nil. +func (_u *MessageUpdateOne) SetNillableRecipient(v *string) *MessageUpdateOne { + if v != nil { + _u.SetRecipient(*v) + } + return _u +} + +// SetRecipientID sets the "recipient_id" field. +func (_u *MessageUpdateOne) SetRecipientID(v string) *MessageUpdateOne { + _u.mutation.SetRecipientID(v) + return _u +} + +// SetNillableRecipientID sets the "recipient_id" field if the given value is not nil. +func (_u *MessageUpdateOne) SetNillableRecipientID(v *string) *MessageUpdateOne { + if v != nil { + _u.SetRecipientID(*v) + } + return _u +} + +// ClearRecipientID clears the value of the "recipient_id" field. +func (_u *MessageUpdateOne) ClearRecipientID() *MessageUpdateOne { + _u.mutation.ClearRecipientID() + return _u +} + +// SetMsg sets the "msg" field. +func (_u *MessageUpdateOne) SetMsg(v string) *MessageUpdateOne { + _u.mutation.SetMsg(v) + return _u +} + +// SetNillableMsg sets the "msg" field if the given value is not nil. +func (_u *MessageUpdateOne) SetNillableMsg(v *string) *MessageUpdateOne { + if v != nil { + _u.SetMsg(*v) + } + return _u +} + +// SetType sets the "type" field. +func (_u *MessageUpdateOne) SetType(v string) *MessageUpdateOne { + _u.mutation.SetType(v) + return _u +} + +// SetNillableType sets the "type" field if the given value is not nil. +func (_u *MessageUpdateOne) SetNillableType(v *string) *MessageUpdateOne { + if v != nil { + _u.SetType(*v) + } + return _u +} + +// SetUrgent sets the "urgent" field. +func (_u *MessageUpdateOne) SetUrgent(v bool) *MessageUpdateOne { + _u.mutation.SetUrgent(v) + return _u +} + +// SetNillableUrgent sets the "urgent" field if the given value is not nil. +func (_u *MessageUpdateOne) SetNillableUrgent(v *bool) *MessageUpdateOne { + if v != nil { + _u.SetUrgent(*v) + } + return _u +} + +// SetBroadcasted sets the "broadcasted" field. +func (_u *MessageUpdateOne) SetBroadcasted(v bool) *MessageUpdateOne { + _u.mutation.SetBroadcasted(v) + return _u +} + +// SetNillableBroadcasted sets the "broadcasted" field if the given value is not nil. +func (_u *MessageUpdateOne) SetNillableBroadcasted(v *bool) *MessageUpdateOne { + if v != nil { + _u.SetBroadcasted(*v) + } + return _u +} + +// SetRead sets the "read" field. +func (_u *MessageUpdateOne) SetRead(v bool) *MessageUpdateOne { + _u.mutation.SetRead(v) + return _u +} + +// SetNillableRead sets the "read" field if the given value is not nil. +func (_u *MessageUpdateOne) SetNillableRead(v *bool) *MessageUpdateOne { + if v != nil { + _u.SetRead(*v) + } + return _u +} + +// SetAgentID sets the "agent_id" field. +func (_u *MessageUpdateOne) SetAgentID(v string) *MessageUpdateOne { + _u.mutation.SetAgentID(v) + return _u +} + +// SetNillableAgentID sets the "agent_id" field if the given value is not nil. +func (_u *MessageUpdateOne) SetNillableAgentID(v *string) *MessageUpdateOne { + if v != nil { + _u.SetAgentID(*v) + } + return _u +} + +// ClearAgentID clears the value of the "agent_id" field. +func (_u *MessageUpdateOne) ClearAgentID() *MessageUpdateOne { + _u.mutation.ClearAgentID() + return _u +} + +// SetGroupID sets the "group_id" field. +func (_u *MessageUpdateOne) SetGroupID(v string) *MessageUpdateOne { + _u.mutation.SetGroupID(v) + return _u +} + +// SetNillableGroupID sets the "group_id" field if the given value is not nil. +func (_u *MessageUpdateOne) SetNillableGroupID(v *string) *MessageUpdateOne { + if v != nil { + _u.SetGroupID(*v) + } + return _u +} + +// ClearGroupID clears the value of the "group_id" field. +func (_u *MessageUpdateOne) ClearGroupID() *MessageUpdateOne { + _u.mutation.ClearGroupID() + return _u +} + +// SetDispatchState sets the "dispatch_state" field. +func (_u *MessageUpdateOne) SetDispatchState(v string) *MessageUpdateOne { + _u.mutation.SetDispatchState(v) + return _u +} + +// SetNillableDispatchState sets the "dispatch_state" field if the given value is not nil. +func (_u *MessageUpdateOne) SetNillableDispatchState(v *string) *MessageUpdateOne { + if v != nil { + _u.SetDispatchState(*v) + } + return _u +} + +// SetDispatchedAt sets the "dispatched_at" field. +func (_u *MessageUpdateOne) SetDispatchedAt(v time.Time) *MessageUpdateOne { + _u.mutation.SetDispatchedAt(v) + return _u +} + +// SetNillableDispatchedAt sets the "dispatched_at" field if the given value is not nil. +func (_u *MessageUpdateOne) SetNillableDispatchedAt(v *time.Time) *MessageUpdateOne { + if v != nil { + _u.SetDispatchedAt(*v) + } + return _u +} + +// ClearDispatchedAt clears the value of the "dispatched_at" field. +func (_u *MessageUpdateOne) ClearDispatchedAt() *MessageUpdateOne { + _u.mutation.ClearDispatchedAt() + return _u +} + +// Mutation returns the MessageMutation object of the builder. +func (_u *MessageUpdateOne) Mutation() *MessageMutation { + return _u.mutation +} + +// Where appends a list predicates to the MessageUpdate builder. +func (_u *MessageUpdateOne) Where(ps ...predicate.Message) *MessageUpdateOne { + _u.mutation.Where(ps...) + return _u +} + +// Select allows selecting one or more fields (columns) of the returned entity. +// The default is selecting all fields defined in the entity schema. +func (_u *MessageUpdateOne) Select(field string, fields ...string) *MessageUpdateOne { + _u.fields = append([]string{field}, fields...) + return _u +} + +// Save executes the query and returns the updated Message entity. +func (_u *MessageUpdateOne) Save(ctx context.Context) (*Message, error) { + return withHooks(ctx, _u.sqlSave, _u.mutation, _u.hooks) +} + +// SaveX is like Save, but panics if an error occurs. +func (_u *MessageUpdateOne) SaveX(ctx context.Context) *Message { + node, err := _u.Save(ctx) + if err != nil { + panic(err) + } + return node +} + +// Exec executes the query on the entity. +func (_u *MessageUpdateOne) Exec(ctx context.Context) error { + _, err := _u.Save(ctx) + return err +} + +// ExecX is like Exec, but panics if an error occurs. +func (_u *MessageUpdateOne) ExecX(ctx context.Context) { + if err := _u.Exec(ctx); err != nil { + panic(err) + } +} + +// check runs all checks and user-defined validators on the builder. +func (_u *MessageUpdateOne) check() error { + if v, ok := _u.mutation.Sender(); ok { + if err := message.SenderValidator(v); err != nil { + return &ValidationError{Name: "sender", err: fmt.Errorf(`ent: validator failed for field "Message.sender": %w`, err)} + } + } + if v, ok := _u.mutation.Recipient(); ok { + if err := message.RecipientValidator(v); err != nil { + return &ValidationError{Name: "recipient", err: fmt.Errorf(`ent: validator failed for field "Message.recipient": %w`, err)} + } + } + if v, ok := _u.mutation.Msg(); ok { + if err := message.MsgValidator(v); err != nil { + return &ValidationError{Name: "msg", err: fmt.Errorf(`ent: validator failed for field "Message.msg": %w`, err)} + } + } + return nil +} + +func (_u *MessageUpdateOne) sqlSave(ctx context.Context) (_node *Message, err error) { + if err := _u.check(); err != nil { + return _node, err + } + _spec := sqlgraph.NewUpdateSpec(message.Table, message.Columns, sqlgraph.NewFieldSpec(message.FieldID, field.TypeUUID)) + id, ok := _u.mutation.ID() + if !ok { + return nil, &ValidationError{Name: "id", err: errors.New(`ent: missing "Message.id" for update`)} + } + _spec.Node.ID.Value = id + if fields := _u.fields; len(fields) > 0 { + _spec.Node.Columns = make([]string, 0, len(fields)) + _spec.Node.Columns = append(_spec.Node.Columns, message.FieldID) + for _, f := range fields { + if !message.ValidColumn(f) { + return nil, &ValidationError{Name: f, err: fmt.Errorf("ent: invalid field %q for query", f)} + } + if f != message.FieldID { + _spec.Node.Columns = append(_spec.Node.Columns, f) + } + } + } + if ps := _u.mutation.predicates; len(ps) > 0 { + _spec.Predicate = func(selector *sql.Selector) { + for i := range ps { + ps[i](selector) + } + } + } + if value, ok := _u.mutation.ProjectID(); ok { + _spec.SetField(message.FieldProjectID, field.TypeUUID, value) + } + if value, ok := _u.mutation.Sender(); ok { + _spec.SetField(message.FieldSender, field.TypeString, value) + } + if value, ok := _u.mutation.SenderID(); ok { + _spec.SetField(message.FieldSenderID, field.TypeString, value) + } + if _u.mutation.SenderIDCleared() { + _spec.ClearField(message.FieldSenderID, field.TypeString) + } + if value, ok := _u.mutation.Recipient(); ok { + _spec.SetField(message.FieldRecipient, field.TypeString, value) + } + if value, ok := _u.mutation.RecipientID(); ok { + _spec.SetField(message.FieldRecipientID, field.TypeString, value) + } + if _u.mutation.RecipientIDCleared() { + _spec.ClearField(message.FieldRecipientID, field.TypeString) + } + if value, ok := _u.mutation.Msg(); ok { + _spec.SetField(message.FieldMsg, field.TypeString, value) + } + if value, ok := _u.mutation.GetType(); ok { + _spec.SetField(message.FieldType, field.TypeString, value) + } + if value, ok := _u.mutation.Urgent(); ok { + _spec.SetField(message.FieldUrgent, field.TypeBool, value) + } + if value, ok := _u.mutation.Broadcasted(); ok { + _spec.SetField(message.FieldBroadcasted, field.TypeBool, value) + } + if value, ok := _u.mutation.Read(); ok { + _spec.SetField(message.FieldRead, field.TypeBool, value) + } + if value, ok := _u.mutation.AgentID(); ok { + _spec.SetField(message.FieldAgentID, field.TypeString, value) + } + if _u.mutation.AgentIDCleared() { + _spec.ClearField(message.FieldAgentID, field.TypeString) + } + if value, ok := _u.mutation.GroupID(); ok { + _spec.SetField(message.FieldGroupID, field.TypeString, value) + } + if _u.mutation.GroupIDCleared() { + _spec.ClearField(message.FieldGroupID, field.TypeString) + } + if value, ok := _u.mutation.DispatchState(); ok { + _spec.SetField(message.FieldDispatchState, field.TypeString, value) + } + if value, ok := _u.mutation.DispatchedAt(); ok { + _spec.SetField(message.FieldDispatchedAt, field.TypeTime, value) + } + if _u.mutation.DispatchedAtCleared() { + _spec.ClearField(message.FieldDispatchedAt, field.TypeTime) + } + _node = &Message{config: _u.config} + _spec.Assign = _node.assignValues + _spec.ScanValues = _node.scanValues + if err = sqlgraph.UpdateNode(ctx, _u.driver, _spec); err != nil { + if _, ok := err.(*sqlgraph.NotFoundError); ok { + err = &NotFoundError{message.Label} + } else if sqlgraph.IsConstraintError(err) { + err = &ConstraintError{msg: err.Error(), wrap: err} + } + return nil, err + } + _u.mutation.done = true + return _node, nil +} diff --git a/pkg/ent/migrate/schema.go b/pkg/ent/migrate/schema.go index 5b465bcc1..1d531285d 100644 --- a/pkg/ent/migrate/schema.go +++ b/pkg/ent/migrate/schema.go @@ -41,13 +41,38 @@ var ( {Name: "name", Type: field.TypeString}, {Name: "template", Type: field.TypeString, Nullable: true}, {Name: "status", Type: field.TypeEnum, Enums: []string{"created", "provisioning", "cloning", "starting", "running", "suspended", "stopping", "stopped", "error"}, Default: "created"}, + {Name: "created_by", Type: field.TypeUUID, Nullable: true}, + {Name: "owner_id", Type: field.TypeUUID, Nullable: true}, {Name: "delegation_enabled", Type: field.TypeBool, Default: false}, {Name: "visibility", Type: field.TypeString, Default: "private"}, + {Name: "labels", Type: field.TypeJSON, Nullable: true}, + {Name: "annotations", Type: field.TypeJSON, Nullable: true}, + {Name: "phase", Type: field.TypeString, Nullable: true}, + {Name: "activity", Type: field.TypeString, Nullable: true}, + {Name: "tool_name", Type: field.TypeString, Nullable: true}, + {Name: "connection_state", Type: field.TypeString, Nullable: true}, + {Name: "container_status", Type: field.TypeString, Nullable: true}, + {Name: "runtime_state", Type: field.TypeString, Nullable: true}, + {Name: "stalled_from_activity", Type: field.TypeString, Nullable: true}, + {Name: "current_turns", Type: field.TypeInt, Default: 0}, + {Name: "current_model_calls", Type: field.TypeInt, Default: 0}, + {Name: "image", Type: field.TypeString, Nullable: true}, + {Name: "detached", Type: field.TypeBool, Default: false}, + {Name: "runtime", Type: field.TypeString, Nullable: true}, + {Name: "runtime_broker_id", Type: field.TypeString, Nullable: true}, + {Name: "web_pty_enabled", Type: field.TypeBool, Default: false}, + {Name: "task_summary", Type: field.TypeString, Nullable: true}, + {Name: "message", Type: field.TypeString, Nullable: true}, + {Name: "applied_config", Type: field.TypeString, Nullable: true, Size: 2147483647}, + {Name: "ancestry", Type: field.TypeJSON, Nullable: true}, {Name: "created", Type: field.TypeTime}, {Name: "updated", Type: field.TypeTime}, + {Name: "last_seen", Type: field.TypeTime, Nullable: true}, + {Name: "last_activity_event", Type: field.TypeTime, Nullable: true}, + {Name: "started_at", Type: field.TypeTime, Nullable: true}, + {Name: "deleted_at", Type: field.TypeTime, Nullable: true}, + {Name: "state_version", Type: field.TypeInt64, Default: 1}, {Name: "project_id", Type: field.TypeUUID}, - {Name: "created_by", Type: field.TypeUUID, Nullable: true}, - {Name: "owner_id", Type: field.TypeUUID, Nullable: true}, } // AgentsTable holds the schema information for the "agents" table. AgentsTable = &schema.Table{ @@ -57,31 +82,219 @@ var ( ForeignKeys: []*schema.ForeignKey{ { Symbol: "agents_projects_agents", - Columns: []*schema.Column{AgentsColumns[9]}, + Columns: []*schema.Column{AgentsColumns[36]}, RefColumns: []*schema.Column{ProjectsColumns[0]}, OnDelete: schema.NoAction, }, + }, + Indexes: []*schema.Index{ { - Symbol: "agents_users_created_agents", - Columns: []*schema.Column{AgentsColumns[10]}, - RefColumns: []*schema.Column{UsersColumns[0]}, - OnDelete: schema.SetNull, + Name: "agent_slug_project_id", + Unique: true, + Columns: []*schema.Column{AgentsColumns[1], AgentsColumns[36]}, + }, + }, + } + // AllowListColumns holds the columns for the "allow_list" table. + AllowListColumns = []*schema.Column{ + {Name: "id", Type: field.TypeUUID}, + {Name: "email", Type: field.TypeString, Unique: true}, + {Name: "note", Type: field.TypeString, Default: ""}, + {Name: "added_by", Type: field.TypeString}, + {Name: "invite_id", Type: field.TypeString, Nullable: true}, + {Name: "created", Type: field.TypeTime}, + } + // AllowListTable holds the schema information for the "allow_list" table. + AllowListTable = &schema.Table{ + Name: "allow_list", + Columns: AllowListColumns, + PrimaryKey: []*schema.Column{AllowListColumns[0]}, + Indexes: []*schema.Index{ + { + Name: "allowlistentry_created_id", + Unique: false, + Columns: []*schema.Column{AllowListColumns[5], AllowListColumns[0]}, }, + }, + } + // APIKeysColumns holds the columns for the "api_keys" table. + APIKeysColumns = []*schema.Column{ + {Name: "id", Type: field.TypeUUID}, + {Name: "user_id", Type: field.TypeUUID}, + {Name: "name", Type: field.TypeString, Nullable: true}, + {Name: "prefix", Type: field.TypeString, Nullable: true}, + {Name: "key_hash", Type: field.TypeString, Unique: true}, + {Name: "scopes", Type: field.TypeString, Nullable: true}, + {Name: "revoked", Type: field.TypeBool, Default: false}, + {Name: "expires_at", Type: field.TypeTime, Nullable: true}, + {Name: "last_used", Type: field.TypeTime, Nullable: true}, + {Name: "created", Type: field.TypeTime}, + } + // APIKeysTable holds the schema information for the "api_keys" table. + APIKeysTable = &schema.Table{ + Name: "api_keys", + Columns: APIKeysColumns, + PrimaryKey: []*schema.Column{APIKeysColumns[0]}, + Indexes: []*schema.Index{ { - Symbol: "agents_users_owned_agents", - Columns: []*schema.Column{AgentsColumns[11]}, - RefColumns: []*schema.Column{UsersColumns[0]}, - OnDelete: schema.SetNull, + Name: "apikey_user_id", + Unique: false, + Columns: []*schema.Column{APIKeysColumns[1]}, }, }, + } + // BrokerDispatchColumns holds the columns for the "broker_dispatch" table. + BrokerDispatchColumns = []*schema.Column{ + {Name: "id", Type: field.TypeUUID}, + {Name: "broker_id", Type: field.TypeUUID}, + {Name: "agent_id", Type: field.TypeUUID, Nullable: true}, + {Name: "agent_slug", Type: field.TypeString, Nullable: true}, + {Name: "project_id", Type: field.TypeUUID, Nullable: true}, + {Name: "op", Type: field.TypeString}, + {Name: "args", Type: field.TypeString, Nullable: true}, + {Name: "state", Type: field.TypeString, Default: "pending"}, + {Name: "result", Type: field.TypeString, Nullable: true}, + {Name: "claimed_by", Type: field.TypeString, Nullable: true}, + {Name: "attempts", Type: field.TypeInt, Default: 0}, + {Name: "error", Type: field.TypeString, Nullable: true}, + {Name: "created_at", Type: field.TypeTime}, + {Name: "updated_at", Type: field.TypeTime}, + {Name: "deadline_at", Type: field.TypeTime, Nullable: true}, + } + // BrokerDispatchTable holds the schema information for the "broker_dispatch" table. + BrokerDispatchTable = &schema.Table{ + Name: "broker_dispatch", + Columns: BrokerDispatchColumns, + PrimaryKey: []*schema.Column{BrokerDispatchColumns[0]}, Indexes: []*schema.Index{ { - Name: "agent_slug_project_id", + Name: "brokerdispatch_broker_id_state", + Unique: false, + Columns: []*schema.Column{BrokerDispatchColumns[1], BrokerDispatchColumns[7]}, + }, + }, + } + // BrokerJoinTokensColumns holds the columns for the "broker_join_tokens" table. + BrokerJoinTokensColumns = []*schema.Column{ + {Name: "broker_id", Type: field.TypeUUID}, + {Name: "token_hash", Type: field.TypeString, Unique: true}, + {Name: "expires_at", Type: field.TypeTime}, + {Name: "created_by", Type: field.TypeString}, + {Name: "created", Type: field.TypeTime}, + } + // BrokerJoinTokensTable holds the schema information for the "broker_join_tokens" table. + BrokerJoinTokensTable = &schema.Table{ + Name: "broker_join_tokens", + Columns: BrokerJoinTokensColumns, + PrimaryKey: []*schema.Column{BrokerJoinTokensColumns[0]}, + Indexes: []*schema.Index{ + { + Name: "brokerjointoken_expires_at", + Unique: false, + Columns: []*schema.Column{BrokerJoinTokensColumns[2]}, + }, + }, + } + // BrokerSecretsColumns holds the columns for the "broker_secrets" table. + BrokerSecretsColumns = []*schema.Column{ + {Name: "broker_id", Type: field.TypeUUID}, + {Name: "secret_key", Type: field.TypeBytes}, + {Name: "algorithm", Type: field.TypeString, Default: "hmac-sha256"}, + {Name: "rotated_at", Type: field.TypeTime, Nullable: true}, + {Name: "expires_at", Type: field.TypeTime, Nullable: true}, + {Name: "status", Type: field.TypeString, Default: "active"}, + {Name: "created", Type: field.TypeTime}, + } + // BrokerSecretsTable holds the schema information for the "broker_secrets" table. + BrokerSecretsTable = &schema.Table{ + Name: "broker_secrets", + Columns: BrokerSecretsColumns, + PrimaryKey: []*schema.Column{BrokerSecretsColumns[0]}, + } + // EnvVarsColumns holds the columns for the "env_vars" table. + EnvVarsColumns = []*schema.Column{ + {Name: "id", Type: field.TypeUUID}, + {Name: "key", Type: field.TypeString}, + {Name: "value", Type: field.TypeString}, + {Name: "scope", Type: field.TypeString}, + {Name: "scope_id", Type: field.TypeString}, + {Name: "description", Type: field.TypeString, Nullable: true}, + {Name: "sensitive", Type: field.TypeBool, Default: false}, + {Name: "injection_mode", Type: field.TypeEnum, Enums: []string{"always", "as_needed"}, Default: "as_needed"}, + {Name: "secret", Type: field.TypeBool, Default: false}, + {Name: "created_by", Type: field.TypeString, Nullable: true}, + {Name: "created", Type: field.TypeTime}, + {Name: "updated", Type: field.TypeTime}, + } + // EnvVarsTable holds the schema information for the "env_vars" table. + EnvVarsTable = &schema.Table{ + Name: "env_vars", + Columns: EnvVarsColumns, + PrimaryKey: []*schema.Column{EnvVarsColumns[0]}, + Indexes: []*schema.Index{ + { + Name: "envvar_key_scope_scope_id", + Unique: true, + Columns: []*schema.Column{EnvVarsColumns[1], EnvVarsColumns[3], EnvVarsColumns[4]}, + }, + { + Name: "envvar_scope_scope_id", + Unique: false, + Columns: []*schema.Column{EnvVarsColumns[3], EnvVarsColumns[4]}, + }, + }, + } + // GcpServiceAccountsColumns holds the columns for the "gcp_service_accounts" table. + GcpServiceAccountsColumns = []*schema.Column{ + {Name: "id", Type: field.TypeUUID}, + {Name: "scope", Type: field.TypeString}, + {Name: "scope_id", Type: field.TypeString}, + {Name: "email", Type: field.TypeString}, + {Name: "project_id", Type: field.TypeString}, + {Name: "display_name", Type: field.TypeString, Default: ""}, + {Name: "default_scopes", Type: field.TypeString, Default: ""}, + {Name: "verified", Type: field.TypeBool, Default: false}, + {Name: "verified_at", Type: field.TypeTime, Nullable: true}, + {Name: "created_by", Type: field.TypeString, Default: ""}, + {Name: "managed", Type: field.TypeBool, Default: false}, + {Name: "managed_by", Type: field.TypeString, Default: ""}, + {Name: "created", Type: field.TypeTime}, + } + // GcpServiceAccountsTable holds the schema information for the "gcp_service_accounts" table. + GcpServiceAccountsTable = &schema.Table{ + Name: "gcp_service_accounts", + Columns: GcpServiceAccountsColumns, + PrimaryKey: []*schema.Column{GcpServiceAccountsColumns[0]}, + Indexes: []*schema.Index{ + { + Name: "gcpserviceaccount_email_scope_scope_id", Unique: true, - Columns: []*schema.Column{AgentsColumns[1], AgentsColumns[9]}, + Columns: []*schema.Column{GcpServiceAccountsColumns[3], GcpServiceAccountsColumns[1], GcpServiceAccountsColumns[2]}, + }, + { + Name: "gcpserviceaccount_scope_scope_id", + Unique: false, + Columns: []*schema.Column{GcpServiceAccountsColumns[1], GcpServiceAccountsColumns[2]}, }, }, } + // GithubInstallationsColumns holds the columns for the "github_installations" table. + GithubInstallationsColumns = []*schema.Column{ + {Name: "installation_id", Type: field.TypeInt64, Increment: true}, + {Name: "account_login", Type: field.TypeString}, + {Name: "account_type", Type: field.TypeString, Default: "Organization"}, + {Name: "app_id", Type: field.TypeInt64}, + {Name: "repositories", Type: field.TypeString, Default: "[]"}, + {Name: "status", Type: field.TypeString, Default: "active"}, + {Name: "created", Type: field.TypeTime}, + {Name: "updated", Type: field.TypeTime}, + } + // GithubInstallationsTable holds the schema information for the "github_installations" table. + GithubInstallationsTable = &schema.Table{ + Name: "github_installations", + Columns: GithubInstallationsColumns, + PrimaryKey: []*schema.Column{GithubInstallationsColumns[0]}, + } // GroupsColumns holds the columns for the "groups" table. GroupsColumns = []*schema.Column{ {Name: "id", Type: field.TypeUUID}, @@ -159,6 +372,233 @@ var ( }, }, } + // HarnessConfigsColumns holds the columns for the "harness_configs" table. + HarnessConfigsColumns = []*schema.Column{ + {Name: "id", Type: field.TypeUUID}, + {Name: "name", Type: field.TypeString}, + {Name: "slug", Type: field.TypeString}, + {Name: "display_name", Type: field.TypeString, Nullable: true}, + {Name: "description", Type: field.TypeString, Nullable: true}, + {Name: "harness", Type: field.TypeString}, + {Name: "config", Type: field.TypeString, Nullable: true}, + {Name: "content_hash", Type: field.TypeString, Nullable: true}, + {Name: "scope", Type: field.TypeString, Default: "global"}, + {Name: "scope_id", Type: field.TypeString, Nullable: true}, + {Name: "storage_uri", Type: field.TypeString, Nullable: true}, + {Name: "storage_bucket", Type: field.TypeString, Nullable: true}, + {Name: "storage_path", Type: field.TypeString, Nullable: true}, + {Name: "files", Type: field.TypeString, Nullable: true}, + {Name: "status", Type: field.TypeEnum, Enums: []string{"pending", "active", "archived"}, Default: "active"}, + {Name: "owner_id", Type: field.TypeString, Nullable: true}, + {Name: "created_by", Type: field.TypeString, Nullable: true}, + {Name: "updated_by", Type: field.TypeString, Nullable: true}, + {Name: "visibility", Type: field.TypeString, Default: "private"}, + {Name: "created", Type: field.TypeTime}, + {Name: "updated", Type: field.TypeTime}, + } + // HarnessConfigsTable holds the schema information for the "harness_configs" table. + HarnessConfigsTable = &schema.Table{ + Name: "harness_configs", + Columns: HarnessConfigsColumns, + PrimaryKey: []*schema.Column{HarnessConfigsColumns[0]}, + Indexes: []*schema.Index{ + { + Name: "harnessconfig_slug_scope_scope_id", + Unique: true, + Columns: []*schema.Column{HarnessConfigsColumns[2], HarnessConfigsColumns[8], HarnessConfigsColumns[9]}, + }, + { + Name: "harnessconfig_harness", + Unique: false, + Columns: []*schema.Column{HarnessConfigsColumns[5]}, + }, + { + Name: "harnessconfig_status", + Unique: false, + Columns: []*schema.Column{HarnessConfigsColumns[14]}, + }, + { + Name: "harnessconfig_content_hash", + Unique: false, + Columns: []*schema.Column{HarnessConfigsColumns[7]}, + }, + }, + } + // InviteCodesColumns holds the columns for the "invite_codes" table. + InviteCodesColumns = []*schema.Column{ + {Name: "id", Type: field.TypeUUID}, + {Name: "code_hash", Type: field.TypeString, Unique: true}, + {Name: "code_prefix", Type: field.TypeString}, + {Name: "max_uses", Type: field.TypeInt, Default: 1}, + {Name: "use_count", Type: field.TypeInt, Default: 0}, + {Name: "expires_at", Type: field.TypeTime}, + {Name: "revoked", Type: field.TypeBool, Default: false}, + {Name: "created_by", Type: field.TypeString}, + {Name: "note", Type: field.TypeString, Default: ""}, + {Name: "created", Type: field.TypeTime}, + } + // InviteCodesTable holds the schema information for the "invite_codes" table. + InviteCodesTable = &schema.Table{ + Name: "invite_codes", + Columns: InviteCodesColumns, + PrimaryKey: []*schema.Column{InviteCodesColumns[0]}, + Indexes: []*schema.Index{ + { + Name: "invitecode_expires_at", + Unique: false, + Columns: []*schema.Column{InviteCodesColumns[5]}, + }, + }, + } + // MaintenanceOperationsColumns holds the columns for the "maintenance_operations" table. + MaintenanceOperationsColumns = []*schema.Column{ + {Name: "id", Type: field.TypeUUID}, + {Name: "key", Type: field.TypeString, Unique: true}, + {Name: "title", Type: field.TypeString}, + {Name: "description", Type: field.TypeString, Default: ""}, + {Name: "category", Type: field.TypeString}, + {Name: "status", Type: field.TypeString, Default: "pending"}, + {Name: "started_at", Type: field.TypeTime, Nullable: true}, + {Name: "completed_at", Type: field.TypeTime, Nullable: true}, + {Name: "started_by", Type: field.TypeString, Nullable: true}, + {Name: "result", Type: field.TypeString, Nullable: true}, + {Name: "metadata", Type: field.TypeString, Default: "{}"}, + {Name: "created", Type: field.TypeTime}, + } + // MaintenanceOperationsTable holds the schema information for the "maintenance_operations" table. + MaintenanceOperationsTable = &schema.Table{ + Name: "maintenance_operations", + Columns: MaintenanceOperationsColumns, + PrimaryKey: []*schema.Column{MaintenanceOperationsColumns[0]}, + } + // MaintenanceOperationRunsColumns holds the columns for the "maintenance_operation_runs" table. + MaintenanceOperationRunsColumns = []*schema.Column{ + {Name: "id", Type: field.TypeUUID}, + {Name: "operation_key", Type: field.TypeString}, + {Name: "status", Type: field.TypeString, Default: "running"}, + {Name: "started_at", Type: field.TypeTime}, + {Name: "completed_at", Type: field.TypeTime, Nullable: true}, + {Name: "started_by", Type: field.TypeString, Nullable: true}, + {Name: "result", Type: field.TypeString, Nullable: true}, + {Name: "log", Type: field.TypeString, Default: ""}, + } + // MaintenanceOperationRunsTable holds the schema information for the "maintenance_operation_runs" table. + MaintenanceOperationRunsTable = &schema.Table{ + Name: "maintenance_operation_runs", + Columns: MaintenanceOperationRunsColumns, + PrimaryKey: []*schema.Column{MaintenanceOperationRunsColumns[0]}, + Indexes: []*schema.Index{ + { + Name: "maintenanceoperationrun_operation_key", + Unique: false, + Columns: []*schema.Column{MaintenanceOperationRunsColumns[1]}, + }, + }, + } + // MessagesColumns holds the columns for the "messages" table. + MessagesColumns = []*schema.Column{ + {Name: "id", Type: field.TypeUUID}, + {Name: "project_id", Type: field.TypeUUID}, + {Name: "sender", Type: field.TypeString}, + {Name: "sender_id", Type: field.TypeString, Nullable: true}, + {Name: "recipient", Type: field.TypeString}, + {Name: "recipient_id", Type: field.TypeString, Nullable: true}, + {Name: "msg", Type: field.TypeString}, + {Name: "type", Type: field.TypeString, Default: "instruction"}, + {Name: "urgent", Type: field.TypeBool, Default: false}, + {Name: "broadcasted", Type: field.TypeBool, Default: false}, + {Name: "read", Type: field.TypeBool, Default: false}, + {Name: "agent_id", Type: field.TypeString, Nullable: true}, + {Name: "group_id", Type: field.TypeString, Nullable: true}, + {Name: "dispatch_state", Type: field.TypeString, Default: "pending"}, + {Name: "dispatched_at", Type: field.TypeTime, Nullable: true}, + {Name: "created", Type: field.TypeTime}, + } + // MessagesTable holds the schema information for the "messages" table. + MessagesTable = &schema.Table{ + Name: "messages", + Columns: MessagesColumns, + PrimaryKey: []*schema.Column{MessagesColumns[0]}, + Indexes: []*schema.Index{ + { + Name: "message_project_id", + Unique: false, + Columns: []*schema.Column{MessagesColumns[1]}, + }, + { + Name: "message_recipient_recipient_id", + Unique: false, + Columns: []*schema.Column{MessagesColumns[4], MessagesColumns[5]}, + }, + { + Name: "message_created", + Unique: false, + Columns: []*schema.Column{MessagesColumns[15]}, + }, + }, + } + // NotificationsColumns holds the columns for the "notifications" table. + NotificationsColumns = []*schema.Column{ + {Name: "id", Type: field.TypeUUID}, + {Name: "subscription_id", Type: field.TypeUUID}, + {Name: "agent_id", Type: field.TypeUUID}, + {Name: "project_id", Type: field.TypeUUID}, + {Name: "subscriber_type", Type: field.TypeString}, + {Name: "subscriber_id", Type: field.TypeString}, + {Name: "status", Type: field.TypeString}, + {Name: "message", Type: field.TypeString}, + {Name: "dispatched", Type: field.TypeBool, Default: false}, + {Name: "acknowledged", Type: field.TypeBool, Default: false}, + {Name: "created", Type: field.TypeTime}, + } + // NotificationsTable holds the schema information for the "notifications" table. + NotificationsTable = &schema.Table{ + Name: "notifications", + Columns: NotificationsColumns, + PrimaryKey: []*schema.Column{NotificationsColumns[0]}, + Indexes: []*schema.Index{ + { + Name: "notification_subscription_id", + Unique: false, + Columns: []*schema.Column{NotificationsColumns[1]}, + }, + { + Name: "notification_project_id_subscriber_type_subscriber_id", + Unique: false, + Columns: []*schema.Column{NotificationsColumns[3], NotificationsColumns[4], NotificationsColumns[5]}, + }, + }, + } + // NotificationSubscriptionsColumns holds the columns for the "notification_subscriptions" table. + NotificationSubscriptionsColumns = []*schema.Column{ + {Name: "id", Type: field.TypeUUID}, + {Name: "scope", Type: field.TypeString, Default: "agent"}, + {Name: "agent_id", Type: field.TypeUUID, Nullable: true}, + {Name: "subscriber_type", Type: field.TypeString, Default: "agent"}, + {Name: "subscriber_id", Type: field.TypeString}, + {Name: "project_id", Type: field.TypeUUID}, + {Name: "trigger_activities", Type: field.TypeString}, + {Name: "created_by", Type: field.TypeString}, + {Name: "created", Type: field.TypeTime}, + } + // NotificationSubscriptionsTable holds the schema information for the "notification_subscriptions" table. + NotificationSubscriptionsTable = &schema.Table{ + Name: "notification_subscriptions", + Columns: NotificationSubscriptionsColumns, + PrimaryKey: []*schema.Column{NotificationSubscriptionsColumns[0]}, + Indexes: []*schema.Index{ + { + Name: "notificationsubscription_scope_agent_id_subscriber_type_subscriber_id_project_id", + Unique: false, + Columns: []*schema.Column{NotificationSubscriptionsColumns[1], NotificationSubscriptionsColumns[2], NotificationSubscriptionsColumns[3], NotificationSubscriptionsColumns[4], NotificationSubscriptionsColumns[5]}, + }, + { + Name: "notificationsubscription_project_id", + Unique: false, + Columns: []*schema.Column{NotificationSubscriptionsColumns[5]}, + }, + }, + } // PolicyBindingsColumns holds the columns for the "policy_bindings" table. PolicyBindingsColumns = []*schema.Column{ {Name: "id", Type: field.TypeUUID}, @@ -208,13 +648,19 @@ var ( {Name: "name", Type: field.TypeString}, {Name: "slug", Type: field.TypeString, Unique: true}, {Name: "git_remote", Type: field.TypeString, Nullable: true}, + {Name: "default_runtime_broker_id", Type: field.TypeString, Nullable: true}, {Name: "labels", Type: field.TypeJSON, Nullable: true}, {Name: "annotations", Type: field.TypeJSON, Nullable: true}, + {Name: "shared_dirs", Type: field.TypeString, Nullable: true}, {Name: "created", Type: field.TypeTime}, {Name: "updated", Type: field.TypeTime}, {Name: "created_by", Type: field.TypeString, Nullable: true}, {Name: "owner_id", Type: field.TypeString, Nullable: true}, {Name: "visibility", Type: field.TypeString, Default: "private"}, + {Name: "github_installation_id", Type: field.TypeInt64, Nullable: true}, + {Name: "github_permissions", Type: field.TypeString, Nullable: true}, + {Name: "github_app_status", Type: field.TypeString, Nullable: true}, + {Name: "git_identity", Type: field.TypeString, Nullable: true}, } // ProjectsTable holds the schema information for the "projects" table. ProjectsTable = &schema.Table{ @@ -222,6 +668,295 @@ var ( Columns: ProjectsColumns, PrimaryKey: []*schema.Column{ProjectsColumns[0]}, } + // ProjectContributorsColumns holds the columns for the "project_contributors" table. + ProjectContributorsColumns = []*schema.Column{ + {Name: "id", Type: field.TypeUUID}, + {Name: "project_id", Type: field.TypeUUID}, + {Name: "broker_id", Type: field.TypeUUID}, + {Name: "broker_name", Type: field.TypeString}, + {Name: "mode", Type: field.TypeString, Default: "connected"}, + {Name: "status", Type: field.TypeString, Default: "offline"}, + {Name: "profiles", Type: field.TypeString, Nullable: true}, + {Name: "last_seen", Type: field.TypeTime, Nullable: true}, + {Name: "local_path", Type: field.TypeString, Nullable: true}, + {Name: "linked_by", Type: field.TypeString, Nullable: true}, + {Name: "linked_at", Type: field.TypeTime, Nullable: true}, + } + // ProjectContributorsTable holds the schema information for the "project_contributors" table. + ProjectContributorsTable = &schema.Table{ + Name: "project_contributors", + Columns: ProjectContributorsColumns, + PrimaryKey: []*schema.Column{ProjectContributorsColumns[0]}, + Indexes: []*schema.Index{ + { + Name: "projectcontributor_project_id_broker_id", + Unique: true, + Columns: []*schema.Column{ProjectContributorsColumns[1], ProjectContributorsColumns[2]}, + }, + { + Name: "projectcontributor_broker_id", + Unique: false, + Columns: []*schema.Column{ProjectContributorsColumns[2]}, + }, + }, + } + // ProjectSyncStateColumns holds the columns for the "project_sync_state" table. + ProjectSyncStateColumns = []*schema.Column{ + {Name: "id", Type: field.TypeUUID}, + {Name: "project_id", Type: field.TypeUUID}, + {Name: "broker_id", Type: field.TypeString, Default: ""}, + {Name: "last_sync_time", Type: field.TypeTime, Nullable: true}, + {Name: "last_commit_sha", Type: field.TypeString, Nullable: true}, + {Name: "file_count", Type: field.TypeInt, Default: 0}, + {Name: "total_bytes", Type: field.TypeInt64, Default: 0}, + } + // ProjectSyncStateTable holds the schema information for the "project_sync_state" table. + ProjectSyncStateTable = &schema.Table{ + Name: "project_sync_state", + Columns: ProjectSyncStateColumns, + PrimaryKey: []*schema.Column{ProjectSyncStateColumns[0]}, + Indexes: []*schema.Index{ + { + Name: "projectsyncstate_project_id_broker_id", + Unique: true, + Columns: []*schema.Column{ProjectSyncStateColumns[1], ProjectSyncStateColumns[2]}, + }, + }, + } + // RuntimeBrokersColumns holds the columns for the "runtime_brokers" table. + RuntimeBrokersColumns = []*schema.Column{ + {Name: "id", Type: field.TypeUUID}, + {Name: "name", Type: field.TypeString}, + {Name: "slug", Type: field.TypeString}, + {Name: "type", Type: field.TypeString, Nullable: true}, + {Name: "mode", Type: field.TypeString, Default: "connected"}, + {Name: "version", Type: field.TypeString, Nullable: true}, + {Name: "lock_version", Type: field.TypeInt64, Default: 0}, + {Name: "status", Type: field.TypeString, Default: "offline"}, + {Name: "connection_state", Type: field.TypeString, Default: "disconnected"}, + {Name: "last_heartbeat", Type: field.TypeTime, Nullable: true}, + {Name: "capabilities", Type: field.TypeString, Nullable: true}, + {Name: "supported_harnesses", Type: field.TypeString, Nullable: true}, + {Name: "resources", Type: field.TypeString, Nullable: true}, + {Name: "runtimes", Type: field.TypeString, Nullable: true}, + {Name: "labels", Type: field.TypeString, Nullable: true}, + {Name: "annotations", Type: field.TypeString, Nullable: true}, + {Name: "endpoint", Type: field.TypeString, Nullable: true}, + {Name: "created_by", Type: field.TypeString, Nullable: true}, + {Name: "auto_provide", Type: field.TypeBool, Default: false}, + {Name: "connected_hub_id", Type: field.TypeString, Nullable: true}, + {Name: "connected_session_id", Type: field.TypeString, Nullable: true}, + {Name: "connected_at", Type: field.TypeTime, Nullable: true}, + {Name: "created", Type: field.TypeTime}, + {Name: "updated", Type: field.TypeTime}, + } + // RuntimeBrokersTable holds the schema information for the "runtime_brokers" table. + RuntimeBrokersTable = &schema.Table{ + Name: "runtime_brokers", + Columns: RuntimeBrokersColumns, + PrimaryKey: []*schema.Column{RuntimeBrokersColumns[0]}, + Indexes: []*schema.Index{ + { + Name: "runtimebroker_slug", + Unique: false, + Columns: []*schema.Column{RuntimeBrokersColumns[2]}, + }, + { + Name: "runtimebroker_status", + Unique: false, + Columns: []*schema.Column{RuntimeBrokersColumns[7]}, + }, + }, + } + // SchedulesColumns holds the columns for the "schedules" table. + SchedulesColumns = []*schema.Column{ + {Name: "id", Type: field.TypeUUID}, + {Name: "project_id", Type: field.TypeUUID}, + {Name: "name", Type: field.TypeString}, + {Name: "cron_expr", Type: field.TypeString}, + {Name: "event_type", Type: field.TypeString}, + {Name: "payload", Type: field.TypeString, Default: "{}"}, + {Name: "status", Type: field.TypeString, Default: "active"}, + {Name: "next_run_at", Type: field.TypeTime, Nullable: true}, + {Name: "last_run_at", Type: field.TypeTime, Nullable: true}, + {Name: "last_run_status", Type: field.TypeString, Nullable: true}, + {Name: "last_run_error", Type: field.TypeString, Nullable: true}, + {Name: "run_count", Type: field.TypeInt, Default: 0}, + {Name: "error_count", Type: field.TypeInt, Default: 0}, + {Name: "created_by", Type: field.TypeString, Nullable: true}, + {Name: "created", Type: field.TypeTime}, + {Name: "updated", Type: field.TypeTime}, + } + // SchedulesTable holds the schema information for the "schedules" table. + SchedulesTable = &schema.Table{ + Name: "schedules", + Columns: SchedulesColumns, + PrimaryKey: []*schema.Column{SchedulesColumns[0]}, + Indexes: []*schema.Index{ + { + Name: "schedule_project_id_name", + Unique: true, + Columns: []*schema.Column{SchedulesColumns[1], SchedulesColumns[2]}, + }, + { + Name: "schedule_next_run_at", + Unique: false, + Columns: []*schema.Column{SchedulesColumns[7]}, + }, + }, + } + // ScheduledEventsColumns holds the columns for the "scheduled_events" table. + ScheduledEventsColumns = []*schema.Column{ + {Name: "id", Type: field.TypeUUID}, + {Name: "project_id", Type: field.TypeUUID}, + {Name: "event_type", Type: field.TypeString}, + {Name: "fire_at", Type: field.TypeTime}, + {Name: "payload", Type: field.TypeString}, + {Name: "status", Type: field.TypeString, Default: "pending"}, + {Name: "created_by", Type: field.TypeString, Nullable: true}, + {Name: "fired_at", Type: field.TypeTime, Nullable: true}, + {Name: "error", Type: field.TypeString, Nullable: true}, + {Name: "schedule_id", Type: field.TypeString, Nullable: true}, + {Name: "created", Type: field.TypeTime}, + } + // ScheduledEventsTable holds the schema information for the "scheduled_events" table. + ScheduledEventsTable = &schema.Table{ + Name: "scheduled_events", + Columns: ScheduledEventsColumns, + PrimaryKey: []*schema.Column{ScheduledEventsColumns[0]}, + Indexes: []*schema.Index{ + { + Name: "scheduledevent_fire_at", + Unique: false, + Columns: []*schema.Column{ScheduledEventsColumns[3]}, + }, + { + Name: "scheduledevent_project_id", + Unique: false, + Columns: []*schema.Column{ScheduledEventsColumns[1]}, + }, + { + Name: "scheduledevent_status", + Unique: false, + Columns: []*schema.Column{ScheduledEventsColumns[5]}, + }, + }, + } + // SecretsColumns holds the columns for the "secrets" table. + SecretsColumns = []*schema.Column{ + {Name: "id", Type: field.TypeUUID}, + {Name: "key", Type: field.TypeString}, + {Name: "encrypted_value", Type: field.TypeString}, + {Name: "secret_ref", Type: field.TypeString, Nullable: true}, + {Name: "secret_type", Type: field.TypeEnum, Enums: []string{"environment", "variable", "file", "internal"}, Default: "environment"}, + {Name: "target", Type: field.TypeString, Nullable: true}, + {Name: "scope", Type: field.TypeString}, + {Name: "scope_id", Type: field.TypeString}, + {Name: "description", Type: field.TypeString, Nullable: true}, + {Name: "injection_mode", Type: field.TypeEnum, Enums: []string{"always", "as_needed"}, Default: "as_needed"}, + {Name: "allow_progeny", Type: field.TypeBool, Default: false}, + {Name: "version", Type: field.TypeInt, Default: 1}, + {Name: "created_by", Type: field.TypeString, Nullable: true}, + {Name: "updated_by", Type: field.TypeString, Nullable: true}, + {Name: "created", Type: field.TypeTime}, + {Name: "updated", Type: field.TypeTime}, + } + // SecretsTable holds the schema information for the "secrets" table. + SecretsTable = &schema.Table{ + Name: "secrets", + Columns: SecretsColumns, + PrimaryKey: []*schema.Column{SecretsColumns[0]}, + Indexes: []*schema.Index{ + { + Name: "secret_key_scope_scope_id", + Unique: true, + Columns: []*schema.Column{SecretsColumns[1], SecretsColumns[6], SecretsColumns[7]}, + }, + { + Name: "secret_scope_scope_id", + Unique: false, + Columns: []*schema.Column{SecretsColumns[6], SecretsColumns[7]}, + }, + }, + } + // SubscriptionTemplatesColumns holds the columns for the "subscription_templates" table. + SubscriptionTemplatesColumns = []*schema.Column{ + {Name: "id", Type: field.TypeUUID}, + {Name: "name", Type: field.TypeString}, + {Name: "scope", Type: field.TypeString, Default: "project"}, + {Name: "trigger_activities", Type: field.TypeString}, + {Name: "project_id", Type: field.TypeUUID, Nullable: true}, + {Name: "created_by", Type: field.TypeString}, + } + // SubscriptionTemplatesTable holds the schema information for the "subscription_templates" table. + SubscriptionTemplatesTable = &schema.Table{ + Name: "subscription_templates", + Columns: SubscriptionTemplatesColumns, + PrimaryKey: []*schema.Column{SubscriptionTemplatesColumns[0]}, + Indexes: []*schema.Index{ + { + Name: "subscriptiontemplate_project_id_name", + Unique: true, + Columns: []*schema.Column{SubscriptionTemplatesColumns[4], SubscriptionTemplatesColumns[1]}, + }, + }, + } + // TemplatesColumns holds the columns for the "templates" table. + TemplatesColumns = []*schema.Column{ + {Name: "id", Type: field.TypeUUID}, + {Name: "name", Type: field.TypeString}, + {Name: "slug", Type: field.TypeString}, + {Name: "display_name", Type: field.TypeString, Nullable: true}, + {Name: "description", Type: field.TypeString, Nullable: true}, + {Name: "harness", Type: field.TypeString}, + {Name: "default_harness_config", Type: field.TypeString, Nullable: true}, + {Name: "image", Type: field.TypeString, Nullable: true}, + {Name: "config", Type: field.TypeString, Nullable: true}, + {Name: "content_hash", Type: field.TypeString, Nullable: true}, + {Name: "scope", Type: field.TypeString, Default: "global"}, + {Name: "scope_id", Type: field.TypeString, Nullable: true}, + {Name: "project_id", Type: field.TypeString, Nullable: true}, + {Name: "storage_uri", Type: field.TypeString, Nullable: true}, + {Name: "storage_bucket", Type: field.TypeString, Nullable: true}, + {Name: "storage_path", Type: field.TypeString, Nullable: true}, + {Name: "files", Type: field.TypeString, Nullable: true}, + {Name: "base_template", Type: field.TypeString, Nullable: true}, + {Name: "status", Type: field.TypeEnum, Enums: []string{"pending", "active", "archived"}, Default: "active"}, + {Name: "owner_id", Type: field.TypeString, Nullable: true}, + {Name: "created_by", Type: field.TypeString, Nullable: true}, + {Name: "updated_by", Type: field.TypeString, Nullable: true}, + {Name: "visibility", Type: field.TypeString, Default: "private"}, + {Name: "created", Type: field.TypeTime}, + {Name: "updated", Type: field.TypeTime}, + } + // TemplatesTable holds the schema information for the "templates" table. + TemplatesTable = &schema.Table{ + Name: "templates", + Columns: TemplatesColumns, + PrimaryKey: []*schema.Column{TemplatesColumns[0]}, + Indexes: []*schema.Index{ + { + Name: "template_slug_scope_scope_id", + Unique: true, + Columns: []*schema.Column{TemplatesColumns[2], TemplatesColumns[10], TemplatesColumns[11]}, + }, + { + Name: "template_harness", + Unique: false, + Columns: []*schema.Column{TemplatesColumns[5]}, + }, + { + Name: "template_status", + Unique: false, + Columns: []*schema.Column{TemplatesColumns[18]}, + }, + { + Name: "template_content_hash", + Unique: false, + Columns: []*schema.Column{TemplatesColumns[9]}, + }, + }, + } // UsersColumns holds the columns for the "users" table. UsersColumns = []*schema.Column{ {Name: "id", Type: field.TypeUUID}, @@ -233,12 +968,52 @@ var ( {Name: "preferences", Type: field.TypeJSON, Nullable: true}, {Name: "created", Type: field.TypeTime}, {Name: "last_login", Type: field.TypeTime, Nullable: true}, + {Name: "last_seen", Type: field.TypeTime, Nullable: true}, } // UsersTable holds the schema information for the "users" table. UsersTable = &schema.Table{ Name: "users", Columns: UsersColumns, PrimaryKey: []*schema.Column{UsersColumns[0]}, + Indexes: []*schema.Index{ + { + Name: "user_last_seen", + Unique: false, + Columns: []*schema.Column{UsersColumns[9]}, + }, + }, + } + // UserAccessTokensColumns holds the columns for the "user_access_tokens" table. + UserAccessTokensColumns = []*schema.Column{ + {Name: "id", Type: field.TypeUUID}, + {Name: "user_id", Type: field.TypeUUID}, + {Name: "name", Type: field.TypeString}, + {Name: "prefix", Type: field.TypeString}, + {Name: "key_hash", Type: field.TypeString, Unique: true}, + {Name: "project_id", Type: field.TypeUUID}, + {Name: "scopes", Type: field.TypeString}, + {Name: "revoked", Type: field.TypeBool, Default: false}, + {Name: "expires_at", Type: field.TypeTime, Nullable: true}, + {Name: "last_used", Type: field.TypeTime, Nullable: true}, + {Name: "created", Type: field.TypeTime}, + } + // UserAccessTokensTable holds the schema information for the "user_access_tokens" table. + UserAccessTokensTable = &schema.Table{ + Name: "user_access_tokens", + Columns: UserAccessTokensColumns, + PrimaryKey: []*schema.Column{UserAccessTokensColumns[0]}, + Indexes: []*schema.Index{ + { + Name: "useraccesstoken_user_id", + Unique: false, + Columns: []*schema.Column{UserAccessTokensColumns[1]}, + }, + { + Name: "useraccesstoken_project_id", + Unique: false, + Columns: []*schema.Column{UserAccessTokensColumns[5]}, + }, + }, } // GroupChildGroupsColumns holds the columns for the "group_child_groups" table. GroupChildGroupsColumns = []*schema.Column{ @@ -269,23 +1044,90 @@ var ( Tables = []*schema.Table{ AccessPoliciesTable, AgentsTable, + AllowListTable, + APIKeysTable, + BrokerDispatchTable, + BrokerJoinTokensTable, + BrokerSecretsTable, + EnvVarsTable, + GcpServiceAccountsTable, + GithubInstallationsTable, GroupsTable, GroupMembershipsTable, + HarnessConfigsTable, + InviteCodesTable, + MaintenanceOperationsTable, + MaintenanceOperationRunsTable, + MessagesTable, + NotificationsTable, + NotificationSubscriptionsTable, PolicyBindingsTable, ProjectsTable, + ProjectContributorsTable, + ProjectSyncStateTable, + RuntimeBrokersTable, + SchedulesTable, + ScheduledEventsTable, + SecretsTable, + SubscriptionTemplatesTable, + TemplatesTable, UsersTable, + UserAccessTokensTable, GroupChildGroupsTable, } ) func init() { AgentsTable.ForeignKeys[0].RefTable = ProjectsTable - AgentsTable.ForeignKeys[1].RefTable = UsersTable - AgentsTable.ForeignKeys[2].RefTable = UsersTable + AllowListTable.Annotation = &entsql.Annotation{ + Table: "allow_list", + } + APIKeysTable.Annotation = &entsql.Annotation{ + Table: "api_keys", + } + BrokerDispatchTable.Annotation = &entsql.Annotation{ + Table: "broker_dispatch", + } + BrokerJoinTokensTable.Annotation = &entsql.Annotation{ + Table: "broker_join_tokens", + } + BrokerSecretsTable.Annotation = &entsql.Annotation{ + Table: "broker_secrets", + } + EnvVarsTable.Annotation = &entsql.Annotation{ + Table: "env_vars", + } + GcpServiceAccountsTable.Annotation = &entsql.Annotation{ + Table: "gcp_service_accounts", + } + GithubInstallationsTable.Annotation = &entsql.Annotation{ + Table: "github_installations", + } GroupsTable.ForeignKeys[0].RefTable = UsersTable GroupMembershipsTable.ForeignKeys[0].RefTable = GroupsTable GroupMembershipsTable.ForeignKeys[1].RefTable = UsersTable GroupMembershipsTable.ForeignKeys[2].RefTable = AgentsTable + HarnessConfigsTable.Annotation = &entsql.Annotation{ + Table: "harness_configs", + } + InviteCodesTable.Annotation = &entsql.Annotation{ + Table: "invite_codes", + } + MaintenanceOperationsTable.Annotation = &entsql.Annotation{ + Table: "maintenance_operations", + } + MaintenanceOperationRunsTable.Annotation = &entsql.Annotation{ + Table: "maintenance_operation_runs", + } + MessagesTable.Annotation = &entsql.Annotation{ + Table: "messages", + } + NotificationsTable.Annotation = &entsql.Annotation{ + Table: "notifications", + } + NotificationSubscriptionsTable.Annotation = &entsql.Annotation{ + Table: "notification_subscriptions", + } PolicyBindingsTable.ForeignKeys[0].RefTable = AccessPoliciesTable PolicyBindingsTable.ForeignKeys[1].RefTable = UsersTable PolicyBindingsTable.ForeignKeys[2].RefTable = GroupsTable @@ -293,6 +1135,33 @@ func init() { ProjectsTable.Annotation = &entsql.Annotation{ Table: "projects", } + ProjectContributorsTable.Annotation = &entsql.Annotation{ + Table: "project_contributors", + } + ProjectSyncStateTable.Annotation = &entsql.Annotation{ + Table: "project_sync_state", + } + RuntimeBrokersTable.Annotation = &entsql.Annotation{ + Table: "runtime_brokers", + } + SchedulesTable.Annotation = &entsql.Annotation{ + Table: "schedules", + } + ScheduledEventsTable.Annotation = &entsql.Annotation{ + Table: "scheduled_events", + } + SecretsTable.Annotation = &entsql.Annotation{ + Table: "secrets", + } + SubscriptionTemplatesTable.Annotation = &entsql.Annotation{ + Table: "subscription_templates", + } + TemplatesTable.Annotation = &entsql.Annotation{ + Table: "templates", + } + UserAccessTokensTable.Annotation = &entsql.Annotation{ + Table: "user_access_tokens", + } GroupChildGroupsTable.ForeignKeys[0].RefTable = GroupsTable GroupChildGroupsTable.ForeignKeys[1].RefTable = GroupsTable } diff --git a/pkg/ent/mutation.go b/pkg/ent/mutation.go index 023803a8f..85a330d04 100644 --- a/pkg/ent/mutation.go +++ b/pkg/ent/mutation.go @@ -13,13 +13,37 @@ import ( "entgo.io/ent/dialect/sql" "github.com/GoogleCloudPlatform/scion/pkg/ent/accesspolicy" "github.com/GoogleCloudPlatform/scion/pkg/ent/agent" + "github.com/GoogleCloudPlatform/scion/pkg/ent/allowlistentry" + "github.com/GoogleCloudPlatform/scion/pkg/ent/apikey" + "github.com/GoogleCloudPlatform/scion/pkg/ent/brokerdispatch" + "github.com/GoogleCloudPlatform/scion/pkg/ent/brokerjointoken" + "github.com/GoogleCloudPlatform/scion/pkg/ent/brokersecret" + "github.com/GoogleCloudPlatform/scion/pkg/ent/envvar" + "github.com/GoogleCloudPlatform/scion/pkg/ent/gcpserviceaccount" + "github.com/GoogleCloudPlatform/scion/pkg/ent/githubinstallation" "github.com/GoogleCloudPlatform/scion/pkg/ent/group" "github.com/GoogleCloudPlatform/scion/pkg/ent/groupmembership" + "github.com/GoogleCloudPlatform/scion/pkg/ent/harnessconfig" + "github.com/GoogleCloudPlatform/scion/pkg/ent/invitecode" + "github.com/GoogleCloudPlatform/scion/pkg/ent/maintenanceoperation" + "github.com/GoogleCloudPlatform/scion/pkg/ent/maintenanceoperationrun" + "github.com/GoogleCloudPlatform/scion/pkg/ent/message" + "github.com/GoogleCloudPlatform/scion/pkg/ent/notification" + "github.com/GoogleCloudPlatform/scion/pkg/ent/notificationsubscription" "github.com/GoogleCloudPlatform/scion/pkg/ent/policybinding" "github.com/GoogleCloudPlatform/scion/pkg/ent/predicate" "github.com/GoogleCloudPlatform/scion/pkg/ent/project" + "github.com/GoogleCloudPlatform/scion/pkg/ent/projectcontributor" + "github.com/GoogleCloudPlatform/scion/pkg/ent/projectsyncstate" + "github.com/GoogleCloudPlatform/scion/pkg/ent/runtimebroker" + "github.com/GoogleCloudPlatform/scion/pkg/ent/schedule" + "github.com/GoogleCloudPlatform/scion/pkg/ent/scheduledevent" "github.com/GoogleCloudPlatform/scion/pkg/ent/schema" + "github.com/GoogleCloudPlatform/scion/pkg/ent/secret" + "github.com/GoogleCloudPlatform/scion/pkg/ent/subscriptiontemplate" + "github.com/GoogleCloudPlatform/scion/pkg/ent/template" "github.com/GoogleCloudPlatform/scion/pkg/ent/user" + "github.com/GoogleCloudPlatform/scion/pkg/ent/useraccesstoken" "github.com/google/uuid" ) @@ -32,13 +56,37 @@ const ( OpUpdateOne = ent.OpUpdateOne // Node types. - TypeAccessPolicy = "AccessPolicy" - TypeAgent = "Agent" - TypeGroup = "Group" - TypeGroupMembership = "GroupMembership" - TypePolicyBinding = "PolicyBinding" - TypeProject = "Project" - TypeUser = "User" + TypeAccessPolicy = "AccessPolicy" + TypeAgent = "Agent" + TypeAllowListEntry = "AllowListEntry" + TypeApiKey = "ApiKey" + TypeBrokerDispatch = "BrokerDispatch" + TypeBrokerJoinToken = "BrokerJoinToken" + TypeBrokerSecret = "BrokerSecret" + TypeEnvVar = "EnvVar" + TypeGCPServiceAccount = "GCPServiceAccount" + TypeGithubInstallation = "GithubInstallation" + TypeGroup = "Group" + TypeGroupMembership = "GroupMembership" + TypeHarnessConfig = "HarnessConfig" + TypeInviteCode = "InviteCode" + TypeMaintenanceOperation = "MaintenanceOperation" + TypeMaintenanceOperationRun = "MaintenanceOperationRun" + TypeMessage = "Message" + TypeNotification = "Notification" + TypeNotificationSubscription = "NotificationSubscription" + TypePolicyBinding = "PolicyBinding" + TypeProject = "Project" + TypeProjectContributor = "ProjectContributor" + TypeProjectSyncState = "ProjectSyncState" + TypeRuntimeBroker = "RuntimeBroker" + TypeSchedule = "Schedule" + TypeScheduledEvent = "ScheduledEvent" + TypeSecret = "Secret" + TypeSubscriptionTemplate = "SubscriptionTemplate" + TypeTemplate = "Template" + TypeUser = "User" + TypeUserAccessToken = "UserAccessToken" ) // AccessPolicyMutation represents an operation that mutates the AccessPolicy nodes in the graph. @@ -1440,17 +1488,44 @@ type AgentMutation struct { name *string template *string status *agent.Status + created_by *uuid.UUID + owner_id *uuid.UUID delegation_enabled *bool visibility *string + labels *map[string]string + annotations *map[string]string + phase *string + activity *string + tool_name *string + connection_state *string + container_status *string + runtime_state *string + stalled_from_activity *string + current_turns *int + addcurrent_turns *int + current_model_calls *int + addcurrent_model_calls *int + image *string + detached *bool + runtime *string + runtime_broker_id *string + web_pty_enabled *bool + task_summary *string + message *string + applied_config *string + ancestry *[]string + appendancestry []string created *time.Time updated *time.Time + last_seen *time.Time + last_activity_event *time.Time + started_at *time.Time + deleted_at *time.Time + state_version *int64 + addstate_version *int64 clearedFields map[string]struct{} project *uuid.UUID clearedproject bool - creator *uuid.UUID - clearedcreator bool - owner *uuid.UUID - clearedowner bool memberships map[uuid.UUID]struct{} removedmemberships map[uuid.UUID]struct{} clearedmemberships bool @@ -1761,12 +1836,12 @@ func (m *AgentMutation) ResetStatus() { // SetCreatedBy sets the "created_by" field. func (m *AgentMutation) SetCreatedBy(u uuid.UUID) { - m.creator = &u + m.created_by = &u } // CreatedBy returns the value of the "created_by" field in the mutation. func (m *AgentMutation) CreatedBy() (r uuid.UUID, exists bool) { - v := m.creator + v := m.created_by if v == nil { return } @@ -1792,7 +1867,7 @@ func (m *AgentMutation) OldCreatedBy(ctx context.Context) (v *uuid.UUID, err err // ClearCreatedBy clears the value of the "created_by" field. func (m *AgentMutation) ClearCreatedBy() { - m.creator = nil + m.created_by = nil m.clearedFields[agent.FieldCreatedBy] = struct{}{} } @@ -1804,18 +1879,18 @@ func (m *AgentMutation) CreatedByCleared() bool { // ResetCreatedBy resets all changes to the "created_by" field. func (m *AgentMutation) ResetCreatedBy() { - m.creator = nil + m.created_by = nil delete(m.clearedFields, agent.FieldCreatedBy) } // SetOwnerID sets the "owner_id" field. func (m *AgentMutation) SetOwnerID(u uuid.UUID) { - m.owner = &u + m.owner_id = &u } // OwnerID returns the value of the "owner_id" field in the mutation. func (m *AgentMutation) OwnerID() (r uuid.UUID, exists bool) { - v := m.owner + v := m.owner_id if v == nil { return } @@ -1841,7 +1916,7 @@ func (m *AgentMutation) OldOwnerID(ctx context.Context) (v *uuid.UUID, err error // ClearOwnerID clears the value of the "owner_id" field. func (m *AgentMutation) ClearOwnerID() { - m.owner = nil + m.owner_id = nil m.clearedFields[agent.FieldOwnerID] = struct{}{} } @@ -1853,7 +1928,7 @@ func (m *AgentMutation) OwnerIDCleared() bool { // ResetOwnerID resets all changes to the "owner_id" field. func (m *AgentMutation) ResetOwnerID() { - m.owner = nil + m.owner_id = nil delete(m.clearedFields, agent.FieldOwnerID) } @@ -1929,289 +2004,25525 @@ func (m *AgentMutation) ResetVisibility() { m.visibility = nil } -// SetCreated sets the "created" field. -func (m *AgentMutation) SetCreated(t time.Time) { - m.created = &t +// SetLabels sets the "labels" field. +func (m *AgentMutation) SetLabels(value map[string]string) { + m.labels = &value } -// Created returns the value of the "created" field in the mutation. -func (m *AgentMutation) Created() (r time.Time, exists bool) { - v := m.created +// Labels returns the value of the "labels" field in the mutation. +func (m *AgentMutation) Labels() (r map[string]string, exists bool) { + v := m.labels if v == nil { return } return *v, true } -// OldCreated returns the old "created" field's value of the Agent entity. +// OldLabels returns the old "labels" field's value of the Agent entity. // If the Agent object wasn't provided to the builder, the object is fetched from the database. // An error is returned if the mutation operation is not UpdateOne, or the database query fails. -func (m *AgentMutation) OldCreated(ctx context.Context) (v time.Time, err error) { +func (m *AgentMutation) OldLabels(ctx context.Context) (v map[string]string, err error) { if !m.op.Is(OpUpdateOne) { - return v, errors.New("OldCreated is only allowed on UpdateOne operations") + return v, errors.New("OldLabels is only allowed on UpdateOne operations") } if m.id == nil || m.oldValue == nil { - return v, errors.New("OldCreated requires an ID field in the mutation") + return v, errors.New("OldLabels requires an ID field in the mutation") } oldValue, err := m.oldValue(ctx) if err != nil { - return v, fmt.Errorf("querying old value for OldCreated: %w", err) + return v, fmt.Errorf("querying old value for OldLabels: %w", err) } - return oldValue.Created, nil + return oldValue.Labels, nil } -// ResetCreated resets all changes to the "created" field. -func (m *AgentMutation) ResetCreated() { - m.created = nil +// ClearLabels clears the value of the "labels" field. +func (m *AgentMutation) ClearLabels() { + m.labels = nil + m.clearedFields[agent.FieldLabels] = struct{}{} } -// SetUpdated sets the "updated" field. -func (m *AgentMutation) SetUpdated(t time.Time) { - m.updated = &t +// LabelsCleared returns if the "labels" field was cleared in this mutation. +func (m *AgentMutation) LabelsCleared() bool { + _, ok := m.clearedFields[agent.FieldLabels] + return ok } -// Updated returns the value of the "updated" field in the mutation. -func (m *AgentMutation) Updated() (r time.Time, exists bool) { - v := m.updated +// ResetLabels resets all changes to the "labels" field. +func (m *AgentMutation) ResetLabels() { + m.labels = nil + delete(m.clearedFields, agent.FieldLabels) +} + +// SetAnnotations sets the "annotations" field. +func (m *AgentMutation) SetAnnotations(value map[string]string) { + m.annotations = &value +} + +// Annotations returns the value of the "annotations" field in the mutation. +func (m *AgentMutation) Annotations() (r map[string]string, exists bool) { + v := m.annotations if v == nil { return } return *v, true } -// OldUpdated returns the old "updated" field's value of the Agent entity. +// OldAnnotations returns the old "annotations" field's value of the Agent entity. // If the Agent object wasn't provided to the builder, the object is fetched from the database. // An error is returned if the mutation operation is not UpdateOne, or the database query fails. -func (m *AgentMutation) OldUpdated(ctx context.Context) (v time.Time, err error) { +func (m *AgentMutation) OldAnnotations(ctx context.Context) (v map[string]string, err error) { if !m.op.Is(OpUpdateOne) { - return v, errors.New("OldUpdated is only allowed on UpdateOne operations") + return v, errors.New("OldAnnotations is only allowed on UpdateOne operations") } if m.id == nil || m.oldValue == nil { - return v, errors.New("OldUpdated requires an ID field in the mutation") + return v, errors.New("OldAnnotations requires an ID field in the mutation") } oldValue, err := m.oldValue(ctx) if err != nil { - return v, fmt.Errorf("querying old value for OldUpdated: %w", err) + return v, fmt.Errorf("querying old value for OldAnnotations: %w", err) } - return oldValue.Updated, nil + return oldValue.Annotations, nil } -// ResetUpdated resets all changes to the "updated" field. -func (m *AgentMutation) ResetUpdated() { - m.updated = nil +// ClearAnnotations clears the value of the "annotations" field. +func (m *AgentMutation) ClearAnnotations() { + m.annotations = nil + m.clearedFields[agent.FieldAnnotations] = struct{}{} } -// ClearProject clears the "project" edge to the Project entity. -func (m *AgentMutation) ClearProject() { - m.clearedproject = true - m.clearedFields[agent.FieldProjectID] = struct{}{} +// AnnotationsCleared returns if the "annotations" field was cleared in this mutation. +func (m *AgentMutation) AnnotationsCleared() bool { + _, ok := m.clearedFields[agent.FieldAnnotations] + return ok } -// ProjectCleared reports if the "project" edge to the Project entity was cleared. -func (m *AgentMutation) ProjectCleared() bool { - return m.clearedproject +// ResetAnnotations resets all changes to the "annotations" field. +func (m *AgentMutation) ResetAnnotations() { + m.annotations = nil + delete(m.clearedFields, agent.FieldAnnotations) } -// ProjectIDs returns the "project" edge IDs in the mutation. -// Note that IDs always returns len(IDs) <= 1 for unique edges, and you should use -// ProjectID instead. It exists only for internal usage by the builders. -func (m *AgentMutation) ProjectIDs() (ids []uuid.UUID) { - if id := m.project; id != nil { - ids = append(ids, *id) +// SetPhase sets the "phase" field. +func (m *AgentMutation) SetPhase(s string) { + m.phase = &s +} + +// Phase returns the value of the "phase" field in the mutation. +func (m *AgentMutation) Phase() (r string, exists bool) { + v := m.phase + if v == nil { + return } - return + return *v, true } -// ResetProject resets all changes to the "project" edge. -func (m *AgentMutation) ResetProject() { - m.project = nil - m.clearedproject = false +// OldPhase returns the old "phase" field's value of the Agent entity. +// If the Agent object wasn't provided to the builder, the object is fetched from the database. +// An error is returned if the mutation operation is not UpdateOne, or the database query fails. +func (m *AgentMutation) OldPhase(ctx context.Context) (v string, err error) { + if !m.op.Is(OpUpdateOne) { + return v, errors.New("OldPhase is only allowed on UpdateOne operations") + } + if m.id == nil || m.oldValue == nil { + return v, errors.New("OldPhase requires an ID field in the mutation") + } + oldValue, err := m.oldValue(ctx) + if err != nil { + return v, fmt.Errorf("querying old value for OldPhase: %w", err) + } + return oldValue.Phase, nil } -// SetCreatorID sets the "creator" edge to the User entity by id. -func (m *AgentMutation) SetCreatorID(id uuid.UUID) { - m.creator = &id +// ClearPhase clears the value of the "phase" field. +func (m *AgentMutation) ClearPhase() { + m.phase = nil + m.clearedFields[agent.FieldPhase] = struct{}{} } -// ClearCreator clears the "creator" edge to the User entity. -func (m *AgentMutation) ClearCreator() { - m.clearedcreator = true - m.clearedFields[agent.FieldCreatedBy] = struct{}{} +// PhaseCleared returns if the "phase" field was cleared in this mutation. +func (m *AgentMutation) PhaseCleared() bool { + _, ok := m.clearedFields[agent.FieldPhase] + return ok +} + +// ResetPhase resets all changes to the "phase" field. +func (m *AgentMutation) ResetPhase() { + m.phase = nil + delete(m.clearedFields, agent.FieldPhase) } -// CreatorCleared reports if the "creator" edge to the User entity was cleared. -func (m *AgentMutation) CreatorCleared() bool { - return m.CreatedByCleared() || m.clearedcreator +// SetActivity sets the "activity" field. +func (m *AgentMutation) SetActivity(s string) { + m.activity = &s } -// CreatorID returns the "creator" edge ID in the mutation. -func (m *AgentMutation) CreatorID() (id uuid.UUID, exists bool) { - if m.creator != nil { - return *m.creator, true +// Activity returns the value of the "activity" field in the mutation. +func (m *AgentMutation) Activity() (r string, exists bool) { + v := m.activity + if v == nil { + return } - return + return *v, true } -// CreatorIDs returns the "creator" edge IDs in the mutation. -// Note that IDs always returns len(IDs) <= 1 for unique edges, and you should use -// CreatorID instead. It exists only for internal usage by the builders. -func (m *AgentMutation) CreatorIDs() (ids []uuid.UUID) { - if id := m.creator; id != nil { - ids = append(ids, *id) +// OldActivity returns the old "activity" field's value of the Agent entity. +// If the Agent object wasn't provided to the builder, the object is fetched from the database. +// An error is returned if the mutation operation is not UpdateOne, or the database query fails. +func (m *AgentMutation) OldActivity(ctx context.Context) (v string, err error) { + if !m.op.Is(OpUpdateOne) { + return v, errors.New("OldActivity is only allowed on UpdateOne operations") } - return + if m.id == nil || m.oldValue == nil { + return v, errors.New("OldActivity requires an ID field in the mutation") + } + oldValue, err := m.oldValue(ctx) + if err != nil { + return v, fmt.Errorf("querying old value for OldActivity: %w", err) + } + return oldValue.Activity, nil } -// ResetCreator resets all changes to the "creator" edge. -func (m *AgentMutation) ResetCreator() { - m.creator = nil - m.clearedcreator = false +// ClearActivity clears the value of the "activity" field. +func (m *AgentMutation) ClearActivity() { + m.activity = nil + m.clearedFields[agent.FieldActivity] = struct{}{} } -// ClearOwner clears the "owner" edge to the User entity. -func (m *AgentMutation) ClearOwner() { - m.clearedowner = true - m.clearedFields[agent.FieldOwnerID] = struct{}{} +// ActivityCleared returns if the "activity" field was cleared in this mutation. +func (m *AgentMutation) ActivityCleared() bool { + _, ok := m.clearedFields[agent.FieldActivity] + return ok } -// OwnerCleared reports if the "owner" edge to the User entity was cleared. -func (m *AgentMutation) OwnerCleared() bool { - return m.OwnerIDCleared() || m.clearedowner +// ResetActivity resets all changes to the "activity" field. +func (m *AgentMutation) ResetActivity() { + m.activity = nil + delete(m.clearedFields, agent.FieldActivity) } -// OwnerIDs returns the "owner" edge IDs in the mutation. -// Note that IDs always returns len(IDs) <= 1 for unique edges, and you should use -// OwnerID instead. It exists only for internal usage by the builders. -func (m *AgentMutation) OwnerIDs() (ids []uuid.UUID) { - if id := m.owner; id != nil { - ids = append(ids, *id) - } - return +// SetToolName sets the "tool_name" field. +func (m *AgentMutation) SetToolName(s string) { + m.tool_name = &s } -// ResetOwner resets all changes to the "owner" edge. -func (m *AgentMutation) ResetOwner() { - m.owner = nil - m.clearedowner = false +// ToolName returns the value of the "tool_name" field in the mutation. +func (m *AgentMutation) ToolName() (r string, exists bool) { + v := m.tool_name + if v == nil { + return + } + return *v, true } -// AddMembershipIDs adds the "memberships" edge to the GroupMembership entity by ids. -func (m *AgentMutation) AddMembershipIDs(ids ...uuid.UUID) { - if m.memberships == nil { - m.memberships = make(map[uuid.UUID]struct{}) +// OldToolName returns the old "tool_name" field's value of the Agent entity. +// If the Agent object wasn't provided to the builder, the object is fetched from the database. +// An error is returned if the mutation operation is not UpdateOne, or the database query fails. +func (m *AgentMutation) OldToolName(ctx context.Context) (v string, err error) { + if !m.op.Is(OpUpdateOne) { + return v, errors.New("OldToolName is only allowed on UpdateOne operations") } - for i := range ids { - m.memberships[ids[i]] = struct{}{} + if m.id == nil || m.oldValue == nil { + return v, errors.New("OldToolName requires an ID field in the mutation") + } + oldValue, err := m.oldValue(ctx) + if err != nil { + return v, fmt.Errorf("querying old value for OldToolName: %w", err) } + return oldValue.ToolName, nil } -// ClearMemberships clears the "memberships" edge to the GroupMembership entity. -func (m *AgentMutation) ClearMemberships() { - m.clearedmemberships = true +// ClearToolName clears the value of the "tool_name" field. +func (m *AgentMutation) ClearToolName() { + m.tool_name = nil + m.clearedFields[agent.FieldToolName] = struct{}{} } -// MembershipsCleared reports if the "memberships" edge to the GroupMembership entity was cleared. -func (m *AgentMutation) MembershipsCleared() bool { - return m.clearedmemberships +// ToolNameCleared returns if the "tool_name" field was cleared in this mutation. +func (m *AgentMutation) ToolNameCleared() bool { + _, ok := m.clearedFields[agent.FieldToolName] + return ok } -// RemoveMembershipIDs removes the "memberships" edge to the GroupMembership entity by IDs. -func (m *AgentMutation) RemoveMembershipIDs(ids ...uuid.UUID) { - if m.removedmemberships == nil { - m.removedmemberships = make(map[uuid.UUID]struct{}) - } - for i := range ids { - delete(m.memberships, ids[i]) - m.removedmemberships[ids[i]] = struct{}{} - } +// ResetToolName resets all changes to the "tool_name" field. +func (m *AgentMutation) ResetToolName() { + m.tool_name = nil + delete(m.clearedFields, agent.FieldToolName) } -// RemovedMemberships returns the removed IDs of the "memberships" edge to the GroupMembership entity. -func (m *AgentMutation) RemovedMembershipsIDs() (ids []uuid.UUID) { - for id := range m.removedmemberships { - ids = append(ids, id) +// SetConnectionState sets the "connection_state" field. +func (m *AgentMutation) SetConnectionState(s string) { + m.connection_state = &s +} + +// ConnectionState returns the value of the "connection_state" field in the mutation. +func (m *AgentMutation) ConnectionState() (r string, exists bool) { + v := m.connection_state + if v == nil { + return } - return + return *v, true } -// MembershipsIDs returns the "memberships" edge IDs in the mutation. -func (m *AgentMutation) MembershipsIDs() (ids []uuid.UUID) { - for id := range m.memberships { - ids = append(ids, id) +// OldConnectionState returns the old "connection_state" field's value of the Agent entity. +// If the Agent object wasn't provided to the builder, the object is fetched from the database. +// An error is returned if the mutation operation is not UpdateOne, or the database query fails. +func (m *AgentMutation) OldConnectionState(ctx context.Context) (v string, err error) { + if !m.op.Is(OpUpdateOne) { + return v, errors.New("OldConnectionState is only allowed on UpdateOne operations") } - return + if m.id == nil || m.oldValue == nil { + return v, errors.New("OldConnectionState requires an ID field in the mutation") + } + oldValue, err := m.oldValue(ctx) + if err != nil { + return v, fmt.Errorf("querying old value for OldConnectionState: %w", err) + } + return oldValue.ConnectionState, nil } -// ResetMemberships resets all changes to the "memberships" edge. -func (m *AgentMutation) ResetMemberships() { - m.memberships = nil - m.clearedmemberships = false - m.removedmemberships = nil +// ClearConnectionState clears the value of the "connection_state" field. +func (m *AgentMutation) ClearConnectionState() { + m.connection_state = nil + m.clearedFields[agent.FieldConnectionState] = struct{}{} } -// AddPolicyBindingIDs adds the "policy_bindings" edge to the PolicyBinding entity by ids. -func (m *AgentMutation) AddPolicyBindingIDs(ids ...uuid.UUID) { - if m.policy_bindings == nil { - m.policy_bindings = make(map[uuid.UUID]struct{}) - } - for i := range ids { - m.policy_bindings[ids[i]] = struct{}{} - } +// ConnectionStateCleared returns if the "connection_state" field was cleared in this mutation. +func (m *AgentMutation) ConnectionStateCleared() bool { + _, ok := m.clearedFields[agent.FieldConnectionState] + return ok } -// ClearPolicyBindings clears the "policy_bindings" edge to the PolicyBinding entity. -func (m *AgentMutation) ClearPolicyBindings() { - m.clearedpolicy_bindings = true +// ResetConnectionState resets all changes to the "connection_state" field. +func (m *AgentMutation) ResetConnectionState() { + m.connection_state = nil + delete(m.clearedFields, agent.FieldConnectionState) } -// PolicyBindingsCleared reports if the "policy_bindings" edge to the PolicyBinding entity was cleared. -func (m *AgentMutation) PolicyBindingsCleared() bool { - return m.clearedpolicy_bindings +// SetContainerStatus sets the "container_status" field. +func (m *AgentMutation) SetContainerStatus(s string) { + m.container_status = &s } -// RemovePolicyBindingIDs removes the "policy_bindings" edge to the PolicyBinding entity by IDs. -func (m *AgentMutation) RemovePolicyBindingIDs(ids ...uuid.UUID) { - if m.removedpolicy_bindings == nil { - m.removedpolicy_bindings = make(map[uuid.UUID]struct{}) - } - for i := range ids { - delete(m.policy_bindings, ids[i]) - m.removedpolicy_bindings[ids[i]] = struct{}{} +// ContainerStatus returns the value of the "container_status" field in the mutation. +func (m *AgentMutation) ContainerStatus() (r string, exists bool) { + v := m.container_status + if v == nil { + return } + return *v, true } -// RemovedPolicyBindings returns the removed IDs of the "policy_bindings" edge to the PolicyBinding entity. -func (m *AgentMutation) RemovedPolicyBindingsIDs() (ids []uuid.UUID) { - for id := range m.removedpolicy_bindings { - ids = append(ids, id) +// OldContainerStatus returns the old "container_status" field's value of the Agent entity. +// If the Agent object wasn't provided to the builder, the object is fetched from the database. +// An error is returned if the mutation operation is not UpdateOne, or the database query fails. +func (m *AgentMutation) OldContainerStatus(ctx context.Context) (v string, err error) { + if !m.op.Is(OpUpdateOne) { + return v, errors.New("OldContainerStatus is only allowed on UpdateOne operations") } - return + if m.id == nil || m.oldValue == nil { + return v, errors.New("OldContainerStatus requires an ID field in the mutation") + } + oldValue, err := m.oldValue(ctx) + if err != nil { + return v, fmt.Errorf("querying old value for OldContainerStatus: %w", err) + } + return oldValue.ContainerStatus, nil } -// PolicyBindingsIDs returns the "policy_bindings" edge IDs in the mutation. -func (m *AgentMutation) PolicyBindingsIDs() (ids []uuid.UUID) { - for id := range m.policy_bindings { - ids = append(ids, id) - } - return +// ClearContainerStatus clears the value of the "container_status" field. +func (m *AgentMutation) ClearContainerStatus() { + m.container_status = nil + m.clearedFields[agent.FieldContainerStatus] = struct{}{} } -// ResetPolicyBindings resets all changes to the "policy_bindings" edge. -func (m *AgentMutation) ResetPolicyBindings() { - m.policy_bindings = nil - m.clearedpolicy_bindings = false - m.removedpolicy_bindings = nil +// ContainerStatusCleared returns if the "container_status" field was cleared in this mutation. +func (m *AgentMutation) ContainerStatusCleared() bool { + _, ok := m.clearedFields[agent.FieldContainerStatus] + return ok } -// Where appends a list predicates to the AgentMutation builder. +// ResetContainerStatus resets all changes to the "container_status" field. +func (m *AgentMutation) ResetContainerStatus() { + m.container_status = nil + delete(m.clearedFields, agent.FieldContainerStatus) +} + +// SetRuntimeState sets the "runtime_state" field. +func (m *AgentMutation) SetRuntimeState(s string) { + m.runtime_state = &s +} + +// RuntimeState returns the value of the "runtime_state" field in the mutation. +func (m *AgentMutation) RuntimeState() (r string, exists bool) { + v := m.runtime_state + if v == nil { + return + } + return *v, true +} + +// OldRuntimeState returns the old "runtime_state" field's value of the Agent entity. +// If the Agent object wasn't provided to the builder, the object is fetched from the database. +// An error is returned if the mutation operation is not UpdateOne, or the database query fails. +func (m *AgentMutation) OldRuntimeState(ctx context.Context) (v string, err error) { + if !m.op.Is(OpUpdateOne) { + return v, errors.New("OldRuntimeState is only allowed on UpdateOne operations") + } + if m.id == nil || m.oldValue == nil { + return v, errors.New("OldRuntimeState requires an ID field in the mutation") + } + oldValue, err := m.oldValue(ctx) + if err != nil { + return v, fmt.Errorf("querying old value for OldRuntimeState: %w", err) + } + return oldValue.RuntimeState, nil +} + +// ClearRuntimeState clears the value of the "runtime_state" field. +func (m *AgentMutation) ClearRuntimeState() { + m.runtime_state = nil + m.clearedFields[agent.FieldRuntimeState] = struct{}{} +} + +// RuntimeStateCleared returns if the "runtime_state" field was cleared in this mutation. +func (m *AgentMutation) RuntimeStateCleared() bool { + _, ok := m.clearedFields[agent.FieldRuntimeState] + return ok +} + +// ResetRuntimeState resets all changes to the "runtime_state" field. +func (m *AgentMutation) ResetRuntimeState() { + m.runtime_state = nil + delete(m.clearedFields, agent.FieldRuntimeState) +} + +// SetStalledFromActivity sets the "stalled_from_activity" field. +func (m *AgentMutation) SetStalledFromActivity(s string) { + m.stalled_from_activity = &s +} + +// StalledFromActivity returns the value of the "stalled_from_activity" field in the mutation. +func (m *AgentMutation) StalledFromActivity() (r string, exists bool) { + v := m.stalled_from_activity + if v == nil { + return + } + return *v, true +} + +// OldStalledFromActivity returns the old "stalled_from_activity" field's value of the Agent entity. +// If the Agent object wasn't provided to the builder, the object is fetched from the database. +// An error is returned if the mutation operation is not UpdateOne, or the database query fails. +func (m *AgentMutation) OldStalledFromActivity(ctx context.Context) (v string, err error) { + if !m.op.Is(OpUpdateOne) { + return v, errors.New("OldStalledFromActivity is only allowed on UpdateOne operations") + } + if m.id == nil || m.oldValue == nil { + return v, errors.New("OldStalledFromActivity requires an ID field in the mutation") + } + oldValue, err := m.oldValue(ctx) + if err != nil { + return v, fmt.Errorf("querying old value for OldStalledFromActivity: %w", err) + } + return oldValue.StalledFromActivity, nil +} + +// ClearStalledFromActivity clears the value of the "stalled_from_activity" field. +func (m *AgentMutation) ClearStalledFromActivity() { + m.stalled_from_activity = nil + m.clearedFields[agent.FieldStalledFromActivity] = struct{}{} +} + +// StalledFromActivityCleared returns if the "stalled_from_activity" field was cleared in this mutation. +func (m *AgentMutation) StalledFromActivityCleared() bool { + _, ok := m.clearedFields[agent.FieldStalledFromActivity] + return ok +} + +// ResetStalledFromActivity resets all changes to the "stalled_from_activity" field. +func (m *AgentMutation) ResetStalledFromActivity() { + m.stalled_from_activity = nil + delete(m.clearedFields, agent.FieldStalledFromActivity) +} + +// SetCurrentTurns sets the "current_turns" field. +func (m *AgentMutation) SetCurrentTurns(i int) { + m.current_turns = &i + m.addcurrent_turns = nil +} + +// CurrentTurns returns the value of the "current_turns" field in the mutation. +func (m *AgentMutation) CurrentTurns() (r int, exists bool) { + v := m.current_turns + if v == nil { + return + } + return *v, true +} + +// OldCurrentTurns returns the old "current_turns" field's value of the Agent entity. +// If the Agent object wasn't provided to the builder, the object is fetched from the database. +// An error is returned if the mutation operation is not UpdateOne, or the database query fails. +func (m *AgentMutation) OldCurrentTurns(ctx context.Context) (v int, err error) { + if !m.op.Is(OpUpdateOne) { + return v, errors.New("OldCurrentTurns is only allowed on UpdateOne operations") + } + if m.id == nil || m.oldValue == nil { + return v, errors.New("OldCurrentTurns requires an ID field in the mutation") + } + oldValue, err := m.oldValue(ctx) + if err != nil { + return v, fmt.Errorf("querying old value for OldCurrentTurns: %w", err) + } + return oldValue.CurrentTurns, nil +} + +// AddCurrentTurns adds i to the "current_turns" field. +func (m *AgentMutation) AddCurrentTurns(i int) { + if m.addcurrent_turns != nil { + *m.addcurrent_turns += i + } else { + m.addcurrent_turns = &i + } +} + +// AddedCurrentTurns returns the value that was added to the "current_turns" field in this mutation. +func (m *AgentMutation) AddedCurrentTurns() (r int, exists bool) { + v := m.addcurrent_turns + if v == nil { + return + } + return *v, true +} + +// ResetCurrentTurns resets all changes to the "current_turns" field. +func (m *AgentMutation) ResetCurrentTurns() { + m.current_turns = nil + m.addcurrent_turns = nil +} + +// SetCurrentModelCalls sets the "current_model_calls" field. +func (m *AgentMutation) SetCurrentModelCalls(i int) { + m.current_model_calls = &i + m.addcurrent_model_calls = nil +} + +// CurrentModelCalls returns the value of the "current_model_calls" field in the mutation. +func (m *AgentMutation) CurrentModelCalls() (r int, exists bool) { + v := m.current_model_calls + if v == nil { + return + } + return *v, true +} + +// OldCurrentModelCalls returns the old "current_model_calls" field's value of the Agent entity. +// If the Agent object wasn't provided to the builder, the object is fetched from the database. +// An error is returned if the mutation operation is not UpdateOne, or the database query fails. +func (m *AgentMutation) OldCurrentModelCalls(ctx context.Context) (v int, err error) { + if !m.op.Is(OpUpdateOne) { + return v, errors.New("OldCurrentModelCalls is only allowed on UpdateOne operations") + } + if m.id == nil || m.oldValue == nil { + return v, errors.New("OldCurrentModelCalls requires an ID field in the mutation") + } + oldValue, err := m.oldValue(ctx) + if err != nil { + return v, fmt.Errorf("querying old value for OldCurrentModelCalls: %w", err) + } + return oldValue.CurrentModelCalls, nil +} + +// AddCurrentModelCalls adds i to the "current_model_calls" field. +func (m *AgentMutation) AddCurrentModelCalls(i int) { + if m.addcurrent_model_calls != nil { + *m.addcurrent_model_calls += i + } else { + m.addcurrent_model_calls = &i + } +} + +// AddedCurrentModelCalls returns the value that was added to the "current_model_calls" field in this mutation. +func (m *AgentMutation) AddedCurrentModelCalls() (r int, exists bool) { + v := m.addcurrent_model_calls + if v == nil { + return + } + return *v, true +} + +// ResetCurrentModelCalls resets all changes to the "current_model_calls" field. +func (m *AgentMutation) ResetCurrentModelCalls() { + m.current_model_calls = nil + m.addcurrent_model_calls = nil +} + +// SetImage sets the "image" field. +func (m *AgentMutation) SetImage(s string) { + m.image = &s +} + +// Image returns the value of the "image" field in the mutation. +func (m *AgentMutation) Image() (r string, exists bool) { + v := m.image + if v == nil { + return + } + return *v, true +} + +// OldImage returns the old "image" field's value of the Agent entity. +// If the Agent object wasn't provided to the builder, the object is fetched from the database. +// An error is returned if the mutation operation is not UpdateOne, or the database query fails. +func (m *AgentMutation) OldImage(ctx context.Context) (v string, err error) { + if !m.op.Is(OpUpdateOne) { + return v, errors.New("OldImage is only allowed on UpdateOne operations") + } + if m.id == nil || m.oldValue == nil { + return v, errors.New("OldImage requires an ID field in the mutation") + } + oldValue, err := m.oldValue(ctx) + if err != nil { + return v, fmt.Errorf("querying old value for OldImage: %w", err) + } + return oldValue.Image, nil +} + +// ClearImage clears the value of the "image" field. +func (m *AgentMutation) ClearImage() { + m.image = nil + m.clearedFields[agent.FieldImage] = struct{}{} +} + +// ImageCleared returns if the "image" field was cleared in this mutation. +func (m *AgentMutation) ImageCleared() bool { + _, ok := m.clearedFields[agent.FieldImage] + return ok +} + +// ResetImage resets all changes to the "image" field. +func (m *AgentMutation) ResetImage() { + m.image = nil + delete(m.clearedFields, agent.FieldImage) +} + +// SetDetached sets the "detached" field. +func (m *AgentMutation) SetDetached(b bool) { + m.detached = &b +} + +// Detached returns the value of the "detached" field in the mutation. +func (m *AgentMutation) Detached() (r bool, exists bool) { + v := m.detached + if v == nil { + return + } + return *v, true +} + +// OldDetached returns the old "detached" field's value of the Agent entity. +// If the Agent object wasn't provided to the builder, the object is fetched from the database. +// An error is returned if the mutation operation is not UpdateOne, or the database query fails. +func (m *AgentMutation) OldDetached(ctx context.Context) (v bool, err error) { + if !m.op.Is(OpUpdateOne) { + return v, errors.New("OldDetached is only allowed on UpdateOne operations") + } + if m.id == nil || m.oldValue == nil { + return v, errors.New("OldDetached requires an ID field in the mutation") + } + oldValue, err := m.oldValue(ctx) + if err != nil { + return v, fmt.Errorf("querying old value for OldDetached: %w", err) + } + return oldValue.Detached, nil +} + +// ResetDetached resets all changes to the "detached" field. +func (m *AgentMutation) ResetDetached() { + m.detached = nil +} + +// SetRuntime sets the "runtime" field. +func (m *AgentMutation) SetRuntime(s string) { + m.runtime = &s +} + +// Runtime returns the value of the "runtime" field in the mutation. +func (m *AgentMutation) Runtime() (r string, exists bool) { + v := m.runtime + if v == nil { + return + } + return *v, true +} + +// OldRuntime returns the old "runtime" field's value of the Agent entity. +// If the Agent object wasn't provided to the builder, the object is fetched from the database. +// An error is returned if the mutation operation is not UpdateOne, or the database query fails. +func (m *AgentMutation) OldRuntime(ctx context.Context) (v string, err error) { + if !m.op.Is(OpUpdateOne) { + return v, errors.New("OldRuntime is only allowed on UpdateOne operations") + } + if m.id == nil || m.oldValue == nil { + return v, errors.New("OldRuntime requires an ID field in the mutation") + } + oldValue, err := m.oldValue(ctx) + if err != nil { + return v, fmt.Errorf("querying old value for OldRuntime: %w", err) + } + return oldValue.Runtime, nil +} + +// ClearRuntime clears the value of the "runtime" field. +func (m *AgentMutation) ClearRuntime() { + m.runtime = nil + m.clearedFields[agent.FieldRuntime] = struct{}{} +} + +// RuntimeCleared returns if the "runtime" field was cleared in this mutation. +func (m *AgentMutation) RuntimeCleared() bool { + _, ok := m.clearedFields[agent.FieldRuntime] + return ok +} + +// ResetRuntime resets all changes to the "runtime" field. +func (m *AgentMutation) ResetRuntime() { + m.runtime = nil + delete(m.clearedFields, agent.FieldRuntime) +} + +// SetRuntimeBrokerID sets the "runtime_broker_id" field. +func (m *AgentMutation) SetRuntimeBrokerID(s string) { + m.runtime_broker_id = &s +} + +// RuntimeBrokerID returns the value of the "runtime_broker_id" field in the mutation. +func (m *AgentMutation) RuntimeBrokerID() (r string, exists bool) { + v := m.runtime_broker_id + if v == nil { + return + } + return *v, true +} + +// OldRuntimeBrokerID returns the old "runtime_broker_id" field's value of the Agent entity. +// If the Agent object wasn't provided to the builder, the object is fetched from the database. +// An error is returned if the mutation operation is not UpdateOne, or the database query fails. +func (m *AgentMutation) OldRuntimeBrokerID(ctx context.Context) (v string, err error) { + if !m.op.Is(OpUpdateOne) { + return v, errors.New("OldRuntimeBrokerID is only allowed on UpdateOne operations") + } + if m.id == nil || m.oldValue == nil { + return v, errors.New("OldRuntimeBrokerID requires an ID field in the mutation") + } + oldValue, err := m.oldValue(ctx) + if err != nil { + return v, fmt.Errorf("querying old value for OldRuntimeBrokerID: %w", err) + } + return oldValue.RuntimeBrokerID, nil +} + +// ClearRuntimeBrokerID clears the value of the "runtime_broker_id" field. +func (m *AgentMutation) ClearRuntimeBrokerID() { + m.runtime_broker_id = nil + m.clearedFields[agent.FieldRuntimeBrokerID] = struct{}{} +} + +// RuntimeBrokerIDCleared returns if the "runtime_broker_id" field was cleared in this mutation. +func (m *AgentMutation) RuntimeBrokerIDCleared() bool { + _, ok := m.clearedFields[agent.FieldRuntimeBrokerID] + return ok +} + +// ResetRuntimeBrokerID resets all changes to the "runtime_broker_id" field. +func (m *AgentMutation) ResetRuntimeBrokerID() { + m.runtime_broker_id = nil + delete(m.clearedFields, agent.FieldRuntimeBrokerID) +} + +// SetWebPtyEnabled sets the "web_pty_enabled" field. +func (m *AgentMutation) SetWebPtyEnabled(b bool) { + m.web_pty_enabled = &b +} + +// WebPtyEnabled returns the value of the "web_pty_enabled" field in the mutation. +func (m *AgentMutation) WebPtyEnabled() (r bool, exists bool) { + v := m.web_pty_enabled + if v == nil { + return + } + return *v, true +} + +// OldWebPtyEnabled returns the old "web_pty_enabled" field's value of the Agent entity. +// If the Agent object wasn't provided to the builder, the object is fetched from the database. +// An error is returned if the mutation operation is not UpdateOne, or the database query fails. +func (m *AgentMutation) OldWebPtyEnabled(ctx context.Context) (v bool, err error) { + if !m.op.Is(OpUpdateOne) { + return v, errors.New("OldWebPtyEnabled is only allowed on UpdateOne operations") + } + if m.id == nil || m.oldValue == nil { + return v, errors.New("OldWebPtyEnabled requires an ID field in the mutation") + } + oldValue, err := m.oldValue(ctx) + if err != nil { + return v, fmt.Errorf("querying old value for OldWebPtyEnabled: %w", err) + } + return oldValue.WebPtyEnabled, nil +} + +// ResetWebPtyEnabled resets all changes to the "web_pty_enabled" field. +func (m *AgentMutation) ResetWebPtyEnabled() { + m.web_pty_enabled = nil +} + +// SetTaskSummary sets the "task_summary" field. +func (m *AgentMutation) SetTaskSummary(s string) { + m.task_summary = &s +} + +// TaskSummary returns the value of the "task_summary" field in the mutation. +func (m *AgentMutation) TaskSummary() (r string, exists bool) { + v := m.task_summary + if v == nil { + return + } + return *v, true +} + +// OldTaskSummary returns the old "task_summary" field's value of the Agent entity. +// If the Agent object wasn't provided to the builder, the object is fetched from the database. +// An error is returned if the mutation operation is not UpdateOne, or the database query fails. +func (m *AgentMutation) OldTaskSummary(ctx context.Context) (v string, err error) { + if !m.op.Is(OpUpdateOne) { + return v, errors.New("OldTaskSummary is only allowed on UpdateOne operations") + } + if m.id == nil || m.oldValue == nil { + return v, errors.New("OldTaskSummary requires an ID field in the mutation") + } + oldValue, err := m.oldValue(ctx) + if err != nil { + return v, fmt.Errorf("querying old value for OldTaskSummary: %w", err) + } + return oldValue.TaskSummary, nil +} + +// ClearTaskSummary clears the value of the "task_summary" field. +func (m *AgentMutation) ClearTaskSummary() { + m.task_summary = nil + m.clearedFields[agent.FieldTaskSummary] = struct{}{} +} + +// TaskSummaryCleared returns if the "task_summary" field was cleared in this mutation. +func (m *AgentMutation) TaskSummaryCleared() bool { + _, ok := m.clearedFields[agent.FieldTaskSummary] + return ok +} + +// ResetTaskSummary resets all changes to the "task_summary" field. +func (m *AgentMutation) ResetTaskSummary() { + m.task_summary = nil + delete(m.clearedFields, agent.FieldTaskSummary) +} + +// SetMessage sets the "message" field. +func (m *AgentMutation) SetMessage(s string) { + m.message = &s +} + +// Message returns the value of the "message" field in the mutation. +func (m *AgentMutation) Message() (r string, exists bool) { + v := m.message + if v == nil { + return + } + return *v, true +} + +// OldMessage returns the old "message" field's value of the Agent entity. +// If the Agent object wasn't provided to the builder, the object is fetched from the database. +// An error is returned if the mutation operation is not UpdateOne, or the database query fails. +func (m *AgentMutation) OldMessage(ctx context.Context) (v string, err error) { + if !m.op.Is(OpUpdateOne) { + return v, errors.New("OldMessage is only allowed on UpdateOne operations") + } + if m.id == nil || m.oldValue == nil { + return v, errors.New("OldMessage requires an ID field in the mutation") + } + oldValue, err := m.oldValue(ctx) + if err != nil { + return v, fmt.Errorf("querying old value for OldMessage: %w", err) + } + return oldValue.Message, nil +} + +// ClearMessage clears the value of the "message" field. +func (m *AgentMutation) ClearMessage() { + m.message = nil + m.clearedFields[agent.FieldMessage] = struct{}{} +} + +// MessageCleared returns if the "message" field was cleared in this mutation. +func (m *AgentMutation) MessageCleared() bool { + _, ok := m.clearedFields[agent.FieldMessage] + return ok +} + +// ResetMessage resets all changes to the "message" field. +func (m *AgentMutation) ResetMessage() { + m.message = nil + delete(m.clearedFields, agent.FieldMessage) +} + +// SetAppliedConfig sets the "applied_config" field. +func (m *AgentMutation) SetAppliedConfig(s string) { + m.applied_config = &s +} + +// AppliedConfig returns the value of the "applied_config" field in the mutation. +func (m *AgentMutation) AppliedConfig() (r string, exists bool) { + v := m.applied_config + if v == nil { + return + } + return *v, true +} + +// OldAppliedConfig returns the old "applied_config" field's value of the Agent entity. +// If the Agent object wasn't provided to the builder, the object is fetched from the database. +// An error is returned if the mutation operation is not UpdateOne, or the database query fails. +func (m *AgentMutation) OldAppliedConfig(ctx context.Context) (v string, err error) { + if !m.op.Is(OpUpdateOne) { + return v, errors.New("OldAppliedConfig is only allowed on UpdateOne operations") + } + if m.id == nil || m.oldValue == nil { + return v, errors.New("OldAppliedConfig requires an ID field in the mutation") + } + oldValue, err := m.oldValue(ctx) + if err != nil { + return v, fmt.Errorf("querying old value for OldAppliedConfig: %w", err) + } + return oldValue.AppliedConfig, nil +} + +// ClearAppliedConfig clears the value of the "applied_config" field. +func (m *AgentMutation) ClearAppliedConfig() { + m.applied_config = nil + m.clearedFields[agent.FieldAppliedConfig] = struct{}{} +} + +// AppliedConfigCleared returns if the "applied_config" field was cleared in this mutation. +func (m *AgentMutation) AppliedConfigCleared() bool { + _, ok := m.clearedFields[agent.FieldAppliedConfig] + return ok +} + +// ResetAppliedConfig resets all changes to the "applied_config" field. +func (m *AgentMutation) ResetAppliedConfig() { + m.applied_config = nil + delete(m.clearedFields, agent.FieldAppliedConfig) +} + +// SetAncestry sets the "ancestry" field. +func (m *AgentMutation) SetAncestry(s []string) { + m.ancestry = &s + m.appendancestry = nil +} + +// Ancestry returns the value of the "ancestry" field in the mutation. +func (m *AgentMutation) Ancestry() (r []string, exists bool) { + v := m.ancestry + if v == nil { + return + } + return *v, true +} + +// OldAncestry returns the old "ancestry" field's value of the Agent entity. +// If the Agent object wasn't provided to the builder, the object is fetched from the database. +// An error is returned if the mutation operation is not UpdateOne, or the database query fails. +func (m *AgentMutation) OldAncestry(ctx context.Context) (v []string, err error) { + if !m.op.Is(OpUpdateOne) { + return v, errors.New("OldAncestry is only allowed on UpdateOne operations") + } + if m.id == nil || m.oldValue == nil { + return v, errors.New("OldAncestry requires an ID field in the mutation") + } + oldValue, err := m.oldValue(ctx) + if err != nil { + return v, fmt.Errorf("querying old value for OldAncestry: %w", err) + } + return oldValue.Ancestry, nil +} + +// AppendAncestry adds s to the "ancestry" field. +func (m *AgentMutation) AppendAncestry(s []string) { + m.appendancestry = append(m.appendancestry, s...) +} + +// AppendedAncestry returns the list of values that were appended to the "ancestry" field in this mutation. +func (m *AgentMutation) AppendedAncestry() ([]string, bool) { + if len(m.appendancestry) == 0 { + return nil, false + } + return m.appendancestry, true +} + +// ClearAncestry clears the value of the "ancestry" field. +func (m *AgentMutation) ClearAncestry() { + m.ancestry = nil + m.appendancestry = nil + m.clearedFields[agent.FieldAncestry] = struct{}{} +} + +// AncestryCleared returns if the "ancestry" field was cleared in this mutation. +func (m *AgentMutation) AncestryCleared() bool { + _, ok := m.clearedFields[agent.FieldAncestry] + return ok +} + +// ResetAncestry resets all changes to the "ancestry" field. +func (m *AgentMutation) ResetAncestry() { + m.ancestry = nil + m.appendancestry = nil + delete(m.clearedFields, agent.FieldAncestry) +} + +// SetCreated sets the "created" field. +func (m *AgentMutation) SetCreated(t time.Time) { + m.created = &t +} + +// Created returns the value of the "created" field in the mutation. +func (m *AgentMutation) Created() (r time.Time, exists bool) { + v := m.created + if v == nil { + return + } + return *v, true +} + +// OldCreated returns the old "created" field's value of the Agent entity. +// If the Agent object wasn't provided to the builder, the object is fetched from the database. +// An error is returned if the mutation operation is not UpdateOne, or the database query fails. +func (m *AgentMutation) OldCreated(ctx context.Context) (v time.Time, err error) { + if !m.op.Is(OpUpdateOne) { + return v, errors.New("OldCreated is only allowed on UpdateOne operations") + } + if m.id == nil || m.oldValue == nil { + return v, errors.New("OldCreated requires an ID field in the mutation") + } + oldValue, err := m.oldValue(ctx) + if err != nil { + return v, fmt.Errorf("querying old value for OldCreated: %w", err) + } + return oldValue.Created, nil +} + +// ResetCreated resets all changes to the "created" field. +func (m *AgentMutation) ResetCreated() { + m.created = nil +} + +// SetUpdated sets the "updated" field. +func (m *AgentMutation) SetUpdated(t time.Time) { + m.updated = &t +} + +// Updated returns the value of the "updated" field in the mutation. +func (m *AgentMutation) Updated() (r time.Time, exists bool) { + v := m.updated + if v == nil { + return + } + return *v, true +} + +// OldUpdated returns the old "updated" field's value of the Agent entity. +// If the Agent object wasn't provided to the builder, the object is fetched from the database. +// An error is returned if the mutation operation is not UpdateOne, or the database query fails. +func (m *AgentMutation) OldUpdated(ctx context.Context) (v time.Time, err error) { + if !m.op.Is(OpUpdateOne) { + return v, errors.New("OldUpdated is only allowed on UpdateOne operations") + } + if m.id == nil || m.oldValue == nil { + return v, errors.New("OldUpdated requires an ID field in the mutation") + } + oldValue, err := m.oldValue(ctx) + if err != nil { + return v, fmt.Errorf("querying old value for OldUpdated: %w", err) + } + return oldValue.Updated, nil +} + +// ResetUpdated resets all changes to the "updated" field. +func (m *AgentMutation) ResetUpdated() { + m.updated = nil +} + +// SetLastSeen sets the "last_seen" field. +func (m *AgentMutation) SetLastSeen(t time.Time) { + m.last_seen = &t +} + +// LastSeen returns the value of the "last_seen" field in the mutation. +func (m *AgentMutation) LastSeen() (r time.Time, exists bool) { + v := m.last_seen + if v == nil { + return + } + return *v, true +} + +// OldLastSeen returns the old "last_seen" field's value of the Agent entity. +// If the Agent object wasn't provided to the builder, the object is fetched from the database. +// An error is returned if the mutation operation is not UpdateOne, or the database query fails. +func (m *AgentMutation) OldLastSeen(ctx context.Context) (v *time.Time, err error) { + if !m.op.Is(OpUpdateOne) { + return v, errors.New("OldLastSeen is only allowed on UpdateOne operations") + } + if m.id == nil || m.oldValue == nil { + return v, errors.New("OldLastSeen requires an ID field in the mutation") + } + oldValue, err := m.oldValue(ctx) + if err != nil { + return v, fmt.Errorf("querying old value for OldLastSeen: %w", err) + } + return oldValue.LastSeen, nil +} + +// ClearLastSeen clears the value of the "last_seen" field. +func (m *AgentMutation) ClearLastSeen() { + m.last_seen = nil + m.clearedFields[agent.FieldLastSeen] = struct{}{} +} + +// LastSeenCleared returns if the "last_seen" field was cleared in this mutation. +func (m *AgentMutation) LastSeenCleared() bool { + _, ok := m.clearedFields[agent.FieldLastSeen] + return ok +} + +// ResetLastSeen resets all changes to the "last_seen" field. +func (m *AgentMutation) ResetLastSeen() { + m.last_seen = nil + delete(m.clearedFields, agent.FieldLastSeen) +} + +// SetLastActivityEvent sets the "last_activity_event" field. +func (m *AgentMutation) SetLastActivityEvent(t time.Time) { + m.last_activity_event = &t +} + +// LastActivityEvent returns the value of the "last_activity_event" field in the mutation. +func (m *AgentMutation) LastActivityEvent() (r time.Time, exists bool) { + v := m.last_activity_event + if v == nil { + return + } + return *v, true +} + +// OldLastActivityEvent returns the old "last_activity_event" field's value of the Agent entity. +// If the Agent object wasn't provided to the builder, the object is fetched from the database. +// An error is returned if the mutation operation is not UpdateOne, or the database query fails. +func (m *AgentMutation) OldLastActivityEvent(ctx context.Context) (v *time.Time, err error) { + if !m.op.Is(OpUpdateOne) { + return v, errors.New("OldLastActivityEvent is only allowed on UpdateOne operations") + } + if m.id == nil || m.oldValue == nil { + return v, errors.New("OldLastActivityEvent requires an ID field in the mutation") + } + oldValue, err := m.oldValue(ctx) + if err != nil { + return v, fmt.Errorf("querying old value for OldLastActivityEvent: %w", err) + } + return oldValue.LastActivityEvent, nil +} + +// ClearLastActivityEvent clears the value of the "last_activity_event" field. +func (m *AgentMutation) ClearLastActivityEvent() { + m.last_activity_event = nil + m.clearedFields[agent.FieldLastActivityEvent] = struct{}{} +} + +// LastActivityEventCleared returns if the "last_activity_event" field was cleared in this mutation. +func (m *AgentMutation) LastActivityEventCleared() bool { + _, ok := m.clearedFields[agent.FieldLastActivityEvent] + return ok +} + +// ResetLastActivityEvent resets all changes to the "last_activity_event" field. +func (m *AgentMutation) ResetLastActivityEvent() { + m.last_activity_event = nil + delete(m.clearedFields, agent.FieldLastActivityEvent) +} + +// SetStartedAt sets the "started_at" field. +func (m *AgentMutation) SetStartedAt(t time.Time) { + m.started_at = &t +} + +// StartedAt returns the value of the "started_at" field in the mutation. +func (m *AgentMutation) StartedAt() (r time.Time, exists bool) { + v := m.started_at + if v == nil { + return + } + return *v, true +} + +// OldStartedAt returns the old "started_at" field's value of the Agent entity. +// If the Agent object wasn't provided to the builder, the object is fetched from the database. +// An error is returned if the mutation operation is not UpdateOne, or the database query fails. +func (m *AgentMutation) OldStartedAt(ctx context.Context) (v *time.Time, err error) { + if !m.op.Is(OpUpdateOne) { + return v, errors.New("OldStartedAt is only allowed on UpdateOne operations") + } + if m.id == nil || m.oldValue == nil { + return v, errors.New("OldStartedAt requires an ID field in the mutation") + } + oldValue, err := m.oldValue(ctx) + if err != nil { + return v, fmt.Errorf("querying old value for OldStartedAt: %w", err) + } + return oldValue.StartedAt, nil +} + +// ClearStartedAt clears the value of the "started_at" field. +func (m *AgentMutation) ClearStartedAt() { + m.started_at = nil + m.clearedFields[agent.FieldStartedAt] = struct{}{} +} + +// StartedAtCleared returns if the "started_at" field was cleared in this mutation. +func (m *AgentMutation) StartedAtCleared() bool { + _, ok := m.clearedFields[agent.FieldStartedAt] + return ok +} + +// ResetStartedAt resets all changes to the "started_at" field. +func (m *AgentMutation) ResetStartedAt() { + m.started_at = nil + delete(m.clearedFields, agent.FieldStartedAt) +} + +// SetDeletedAt sets the "deleted_at" field. +func (m *AgentMutation) SetDeletedAt(t time.Time) { + m.deleted_at = &t +} + +// DeletedAt returns the value of the "deleted_at" field in the mutation. +func (m *AgentMutation) DeletedAt() (r time.Time, exists bool) { + v := m.deleted_at + if v == nil { + return + } + return *v, true +} + +// OldDeletedAt returns the old "deleted_at" field's value of the Agent entity. +// If the Agent object wasn't provided to the builder, the object is fetched from the database. +// An error is returned if the mutation operation is not UpdateOne, or the database query fails. +func (m *AgentMutation) OldDeletedAt(ctx context.Context) (v *time.Time, err error) { + if !m.op.Is(OpUpdateOne) { + return v, errors.New("OldDeletedAt is only allowed on UpdateOne operations") + } + if m.id == nil || m.oldValue == nil { + return v, errors.New("OldDeletedAt requires an ID field in the mutation") + } + oldValue, err := m.oldValue(ctx) + if err != nil { + return v, fmt.Errorf("querying old value for OldDeletedAt: %w", err) + } + return oldValue.DeletedAt, nil +} + +// ClearDeletedAt clears the value of the "deleted_at" field. +func (m *AgentMutation) ClearDeletedAt() { + m.deleted_at = nil + m.clearedFields[agent.FieldDeletedAt] = struct{}{} +} + +// DeletedAtCleared returns if the "deleted_at" field was cleared in this mutation. +func (m *AgentMutation) DeletedAtCleared() bool { + _, ok := m.clearedFields[agent.FieldDeletedAt] + return ok +} + +// ResetDeletedAt resets all changes to the "deleted_at" field. +func (m *AgentMutation) ResetDeletedAt() { + m.deleted_at = nil + delete(m.clearedFields, agent.FieldDeletedAt) +} + +// SetStateVersion sets the "state_version" field. +func (m *AgentMutation) SetStateVersion(i int64) { + m.state_version = &i + m.addstate_version = nil +} + +// StateVersion returns the value of the "state_version" field in the mutation. +func (m *AgentMutation) StateVersion() (r int64, exists bool) { + v := m.state_version + if v == nil { + return + } + return *v, true +} + +// OldStateVersion returns the old "state_version" field's value of the Agent entity. +// If the Agent object wasn't provided to the builder, the object is fetched from the database. +// An error is returned if the mutation operation is not UpdateOne, or the database query fails. +func (m *AgentMutation) OldStateVersion(ctx context.Context) (v int64, err error) { + if !m.op.Is(OpUpdateOne) { + return v, errors.New("OldStateVersion is only allowed on UpdateOne operations") + } + if m.id == nil || m.oldValue == nil { + return v, errors.New("OldStateVersion requires an ID field in the mutation") + } + oldValue, err := m.oldValue(ctx) + if err != nil { + return v, fmt.Errorf("querying old value for OldStateVersion: %w", err) + } + return oldValue.StateVersion, nil +} + +// AddStateVersion adds i to the "state_version" field. +func (m *AgentMutation) AddStateVersion(i int64) { + if m.addstate_version != nil { + *m.addstate_version += i + } else { + m.addstate_version = &i + } +} + +// AddedStateVersion returns the value that was added to the "state_version" field in this mutation. +func (m *AgentMutation) AddedStateVersion() (r int64, exists bool) { + v := m.addstate_version + if v == nil { + return + } + return *v, true +} + +// ResetStateVersion resets all changes to the "state_version" field. +func (m *AgentMutation) ResetStateVersion() { + m.state_version = nil + m.addstate_version = nil +} + +// ClearProject clears the "project" edge to the Project entity. +func (m *AgentMutation) ClearProject() { + m.clearedproject = true + m.clearedFields[agent.FieldProjectID] = struct{}{} +} + +// ProjectCleared reports if the "project" edge to the Project entity was cleared. +func (m *AgentMutation) ProjectCleared() bool { + return m.clearedproject +} + +// ProjectIDs returns the "project" edge IDs in the mutation. +// Note that IDs always returns len(IDs) <= 1 for unique edges, and you should use +// ProjectID instead. It exists only for internal usage by the builders. +func (m *AgentMutation) ProjectIDs() (ids []uuid.UUID) { + if id := m.project; id != nil { + ids = append(ids, *id) + } + return +} + +// ResetProject resets all changes to the "project" edge. +func (m *AgentMutation) ResetProject() { + m.project = nil + m.clearedproject = false +} + +// AddMembershipIDs adds the "memberships" edge to the GroupMembership entity by ids. +func (m *AgentMutation) AddMembershipIDs(ids ...uuid.UUID) { + if m.memberships == nil { + m.memberships = make(map[uuid.UUID]struct{}) + } + for i := range ids { + m.memberships[ids[i]] = struct{}{} + } +} + +// ClearMemberships clears the "memberships" edge to the GroupMembership entity. +func (m *AgentMutation) ClearMemberships() { + m.clearedmemberships = true +} + +// MembershipsCleared reports if the "memberships" edge to the GroupMembership entity was cleared. +func (m *AgentMutation) MembershipsCleared() bool { + return m.clearedmemberships +} + +// RemoveMembershipIDs removes the "memberships" edge to the GroupMembership entity by IDs. +func (m *AgentMutation) RemoveMembershipIDs(ids ...uuid.UUID) { + if m.removedmemberships == nil { + m.removedmemberships = make(map[uuid.UUID]struct{}) + } + for i := range ids { + delete(m.memberships, ids[i]) + m.removedmemberships[ids[i]] = struct{}{} + } +} + +// RemovedMemberships returns the removed IDs of the "memberships" edge to the GroupMembership entity. +func (m *AgentMutation) RemovedMembershipsIDs() (ids []uuid.UUID) { + for id := range m.removedmemberships { + ids = append(ids, id) + } + return +} + +// MembershipsIDs returns the "memberships" edge IDs in the mutation. +func (m *AgentMutation) MembershipsIDs() (ids []uuid.UUID) { + for id := range m.memberships { + ids = append(ids, id) + } + return +} + +// ResetMemberships resets all changes to the "memberships" edge. +func (m *AgentMutation) ResetMemberships() { + m.memberships = nil + m.clearedmemberships = false + m.removedmemberships = nil +} + +// AddPolicyBindingIDs adds the "policy_bindings" edge to the PolicyBinding entity by ids. +func (m *AgentMutation) AddPolicyBindingIDs(ids ...uuid.UUID) { + if m.policy_bindings == nil { + m.policy_bindings = make(map[uuid.UUID]struct{}) + } + for i := range ids { + m.policy_bindings[ids[i]] = struct{}{} + } +} + +// ClearPolicyBindings clears the "policy_bindings" edge to the PolicyBinding entity. +func (m *AgentMutation) ClearPolicyBindings() { + m.clearedpolicy_bindings = true +} + +// PolicyBindingsCleared reports if the "policy_bindings" edge to the PolicyBinding entity was cleared. +func (m *AgentMutation) PolicyBindingsCleared() bool { + return m.clearedpolicy_bindings +} + +// RemovePolicyBindingIDs removes the "policy_bindings" edge to the PolicyBinding entity by IDs. +func (m *AgentMutation) RemovePolicyBindingIDs(ids ...uuid.UUID) { + if m.removedpolicy_bindings == nil { + m.removedpolicy_bindings = make(map[uuid.UUID]struct{}) + } + for i := range ids { + delete(m.policy_bindings, ids[i]) + m.removedpolicy_bindings[ids[i]] = struct{}{} + } +} + +// RemovedPolicyBindings returns the removed IDs of the "policy_bindings" edge to the PolicyBinding entity. +func (m *AgentMutation) RemovedPolicyBindingsIDs() (ids []uuid.UUID) { + for id := range m.removedpolicy_bindings { + ids = append(ids, id) + } + return +} + +// PolicyBindingsIDs returns the "policy_bindings" edge IDs in the mutation. +func (m *AgentMutation) PolicyBindingsIDs() (ids []uuid.UUID) { + for id := range m.policy_bindings { + ids = append(ids, id) + } + return +} + +// ResetPolicyBindings resets all changes to the "policy_bindings" edge. +func (m *AgentMutation) ResetPolicyBindings() { + m.policy_bindings = nil + m.clearedpolicy_bindings = false + m.removedpolicy_bindings = nil +} + +// Where appends a list predicates to the AgentMutation builder. func (m *AgentMutation) Where(ps ...predicate.Agent) { m.predicates = append(m.predicates, ps...) } -// WhereP appends storage-level predicates to the AgentMutation builder. Using this method, +// WhereP appends storage-level predicates to the AgentMutation builder. Using this method, +// users can use type-assertion to append predicates that do not depend on any generated package. +func (m *AgentMutation) WhereP(ps ...func(*sql.Selector)) { + p := make([]predicate.Agent, len(ps)) + for i := range ps { + p[i] = ps[i] + } + m.Where(p...) +} + +// Op returns the operation name. +func (m *AgentMutation) Op() Op { + return m.op +} + +// SetOp allows setting the mutation operation. +func (m *AgentMutation) SetOp(op Op) { + m.op = op +} + +// Type returns the node type of this mutation (Agent). +func (m *AgentMutation) Type() string { + return m.typ +} + +// Fields returns all fields that were changed during this mutation. Note that in +// order to get all numeric fields that were incremented/decremented, call +// AddedFields(). +func (m *AgentMutation) Fields() []string { + fields := make([]string, 0, 36) + if m.slug != nil { + fields = append(fields, agent.FieldSlug) + } + if m.name != nil { + fields = append(fields, agent.FieldName) + } + if m.template != nil { + fields = append(fields, agent.FieldTemplate) + } + if m.project != nil { + fields = append(fields, agent.FieldProjectID) + } + if m.status != nil { + fields = append(fields, agent.FieldStatus) + } + if m.created_by != nil { + fields = append(fields, agent.FieldCreatedBy) + } + if m.owner_id != nil { + fields = append(fields, agent.FieldOwnerID) + } + if m.delegation_enabled != nil { + fields = append(fields, agent.FieldDelegationEnabled) + } + if m.visibility != nil { + fields = append(fields, agent.FieldVisibility) + } + if m.labels != nil { + fields = append(fields, agent.FieldLabels) + } + if m.annotations != nil { + fields = append(fields, agent.FieldAnnotations) + } + if m.phase != nil { + fields = append(fields, agent.FieldPhase) + } + if m.activity != nil { + fields = append(fields, agent.FieldActivity) + } + if m.tool_name != nil { + fields = append(fields, agent.FieldToolName) + } + if m.connection_state != nil { + fields = append(fields, agent.FieldConnectionState) + } + if m.container_status != nil { + fields = append(fields, agent.FieldContainerStatus) + } + if m.runtime_state != nil { + fields = append(fields, agent.FieldRuntimeState) + } + if m.stalled_from_activity != nil { + fields = append(fields, agent.FieldStalledFromActivity) + } + if m.current_turns != nil { + fields = append(fields, agent.FieldCurrentTurns) + } + if m.current_model_calls != nil { + fields = append(fields, agent.FieldCurrentModelCalls) + } + if m.image != nil { + fields = append(fields, agent.FieldImage) + } + if m.detached != nil { + fields = append(fields, agent.FieldDetached) + } + if m.runtime != nil { + fields = append(fields, agent.FieldRuntime) + } + if m.runtime_broker_id != nil { + fields = append(fields, agent.FieldRuntimeBrokerID) + } + if m.web_pty_enabled != nil { + fields = append(fields, agent.FieldWebPtyEnabled) + } + if m.task_summary != nil { + fields = append(fields, agent.FieldTaskSummary) + } + if m.message != nil { + fields = append(fields, agent.FieldMessage) + } + if m.applied_config != nil { + fields = append(fields, agent.FieldAppliedConfig) + } + if m.ancestry != nil { + fields = append(fields, agent.FieldAncestry) + } + if m.created != nil { + fields = append(fields, agent.FieldCreated) + } + if m.updated != nil { + fields = append(fields, agent.FieldUpdated) + } + if m.last_seen != nil { + fields = append(fields, agent.FieldLastSeen) + } + if m.last_activity_event != nil { + fields = append(fields, agent.FieldLastActivityEvent) + } + if m.started_at != nil { + fields = append(fields, agent.FieldStartedAt) + } + if m.deleted_at != nil { + fields = append(fields, agent.FieldDeletedAt) + } + if m.state_version != nil { + fields = append(fields, agent.FieldStateVersion) + } + return fields +} + +// Field returns the value of a field with the given name. The second boolean +// return value indicates that this field was not set, or was not defined in the +// schema. +func (m *AgentMutation) Field(name string) (ent.Value, bool) { + switch name { + case agent.FieldSlug: + return m.Slug() + case agent.FieldName: + return m.Name() + case agent.FieldTemplate: + return m.Template() + case agent.FieldProjectID: + return m.ProjectID() + case agent.FieldStatus: + return m.Status() + case agent.FieldCreatedBy: + return m.CreatedBy() + case agent.FieldOwnerID: + return m.OwnerID() + case agent.FieldDelegationEnabled: + return m.DelegationEnabled() + case agent.FieldVisibility: + return m.Visibility() + case agent.FieldLabels: + return m.Labels() + case agent.FieldAnnotations: + return m.Annotations() + case agent.FieldPhase: + return m.Phase() + case agent.FieldActivity: + return m.Activity() + case agent.FieldToolName: + return m.ToolName() + case agent.FieldConnectionState: + return m.ConnectionState() + case agent.FieldContainerStatus: + return m.ContainerStatus() + case agent.FieldRuntimeState: + return m.RuntimeState() + case agent.FieldStalledFromActivity: + return m.StalledFromActivity() + case agent.FieldCurrentTurns: + return m.CurrentTurns() + case agent.FieldCurrentModelCalls: + return m.CurrentModelCalls() + case agent.FieldImage: + return m.Image() + case agent.FieldDetached: + return m.Detached() + case agent.FieldRuntime: + return m.Runtime() + case agent.FieldRuntimeBrokerID: + return m.RuntimeBrokerID() + case agent.FieldWebPtyEnabled: + return m.WebPtyEnabled() + case agent.FieldTaskSummary: + return m.TaskSummary() + case agent.FieldMessage: + return m.Message() + case agent.FieldAppliedConfig: + return m.AppliedConfig() + case agent.FieldAncestry: + return m.Ancestry() + case agent.FieldCreated: + return m.Created() + case agent.FieldUpdated: + return m.Updated() + case agent.FieldLastSeen: + return m.LastSeen() + case agent.FieldLastActivityEvent: + return m.LastActivityEvent() + case agent.FieldStartedAt: + return m.StartedAt() + case agent.FieldDeletedAt: + return m.DeletedAt() + case agent.FieldStateVersion: + return m.StateVersion() + } + return nil, false +} + +// OldField returns the old value of the field from the database. An error is +// returned if the mutation operation is not UpdateOne, or the query to the +// database failed. +func (m *AgentMutation) OldField(ctx context.Context, name string) (ent.Value, error) { + switch name { + case agent.FieldSlug: + return m.OldSlug(ctx) + case agent.FieldName: + return m.OldName(ctx) + case agent.FieldTemplate: + return m.OldTemplate(ctx) + case agent.FieldProjectID: + return m.OldProjectID(ctx) + case agent.FieldStatus: + return m.OldStatus(ctx) + case agent.FieldCreatedBy: + return m.OldCreatedBy(ctx) + case agent.FieldOwnerID: + return m.OldOwnerID(ctx) + case agent.FieldDelegationEnabled: + return m.OldDelegationEnabled(ctx) + case agent.FieldVisibility: + return m.OldVisibility(ctx) + case agent.FieldLabels: + return m.OldLabels(ctx) + case agent.FieldAnnotations: + return m.OldAnnotations(ctx) + case agent.FieldPhase: + return m.OldPhase(ctx) + case agent.FieldActivity: + return m.OldActivity(ctx) + case agent.FieldToolName: + return m.OldToolName(ctx) + case agent.FieldConnectionState: + return m.OldConnectionState(ctx) + case agent.FieldContainerStatus: + return m.OldContainerStatus(ctx) + case agent.FieldRuntimeState: + return m.OldRuntimeState(ctx) + case agent.FieldStalledFromActivity: + return m.OldStalledFromActivity(ctx) + case agent.FieldCurrentTurns: + return m.OldCurrentTurns(ctx) + case agent.FieldCurrentModelCalls: + return m.OldCurrentModelCalls(ctx) + case agent.FieldImage: + return m.OldImage(ctx) + case agent.FieldDetached: + return m.OldDetached(ctx) + case agent.FieldRuntime: + return m.OldRuntime(ctx) + case agent.FieldRuntimeBrokerID: + return m.OldRuntimeBrokerID(ctx) + case agent.FieldWebPtyEnabled: + return m.OldWebPtyEnabled(ctx) + case agent.FieldTaskSummary: + return m.OldTaskSummary(ctx) + case agent.FieldMessage: + return m.OldMessage(ctx) + case agent.FieldAppliedConfig: + return m.OldAppliedConfig(ctx) + case agent.FieldAncestry: + return m.OldAncestry(ctx) + case agent.FieldCreated: + return m.OldCreated(ctx) + case agent.FieldUpdated: + return m.OldUpdated(ctx) + case agent.FieldLastSeen: + return m.OldLastSeen(ctx) + case agent.FieldLastActivityEvent: + return m.OldLastActivityEvent(ctx) + case agent.FieldStartedAt: + return m.OldStartedAt(ctx) + case agent.FieldDeletedAt: + return m.OldDeletedAt(ctx) + case agent.FieldStateVersion: + return m.OldStateVersion(ctx) + } + return nil, fmt.Errorf("unknown Agent field %s", name) +} + +// SetField sets the value of a field with the given name. It returns an error if +// the field is not defined in the schema, or if the type mismatched the field +// type. +func (m *AgentMutation) SetField(name string, value ent.Value) error { + switch name { + case agent.FieldSlug: + v, ok := value.(string) + if !ok { + return fmt.Errorf("unexpected type %T for field %s", value, name) + } + m.SetSlug(v) + return nil + case agent.FieldName: + v, ok := value.(string) + if !ok { + return fmt.Errorf("unexpected type %T for field %s", value, name) + } + m.SetName(v) + return nil + case agent.FieldTemplate: + v, ok := value.(string) + if !ok { + return fmt.Errorf("unexpected type %T for field %s", value, name) + } + m.SetTemplate(v) + return nil + case agent.FieldProjectID: + v, ok := value.(uuid.UUID) + if !ok { + return fmt.Errorf("unexpected type %T for field %s", value, name) + } + m.SetProjectID(v) + return nil + case agent.FieldStatus: + v, ok := value.(agent.Status) + if !ok { + return fmt.Errorf("unexpected type %T for field %s", value, name) + } + m.SetStatus(v) + return nil + case agent.FieldCreatedBy: + v, ok := value.(uuid.UUID) + if !ok { + return fmt.Errorf("unexpected type %T for field %s", value, name) + } + m.SetCreatedBy(v) + return nil + case agent.FieldOwnerID: + v, ok := value.(uuid.UUID) + if !ok { + return fmt.Errorf("unexpected type %T for field %s", value, name) + } + m.SetOwnerID(v) + return nil + case agent.FieldDelegationEnabled: + v, ok := value.(bool) + if !ok { + return fmt.Errorf("unexpected type %T for field %s", value, name) + } + m.SetDelegationEnabled(v) + return nil + case agent.FieldVisibility: + v, ok := value.(string) + if !ok { + return fmt.Errorf("unexpected type %T for field %s", value, name) + } + m.SetVisibility(v) + return nil + case agent.FieldLabels: + v, ok := value.(map[string]string) + if !ok { + return fmt.Errorf("unexpected type %T for field %s", value, name) + } + m.SetLabels(v) + return nil + case agent.FieldAnnotations: + v, ok := value.(map[string]string) + if !ok { + return fmt.Errorf("unexpected type %T for field %s", value, name) + } + m.SetAnnotations(v) + return nil + case agent.FieldPhase: + v, ok := value.(string) + if !ok { + return fmt.Errorf("unexpected type %T for field %s", value, name) + } + m.SetPhase(v) + return nil + case agent.FieldActivity: + v, ok := value.(string) + if !ok { + return fmt.Errorf("unexpected type %T for field %s", value, name) + } + m.SetActivity(v) + return nil + case agent.FieldToolName: + v, ok := value.(string) + if !ok { + return fmt.Errorf("unexpected type %T for field %s", value, name) + } + m.SetToolName(v) + return nil + case agent.FieldConnectionState: + v, ok := value.(string) + if !ok { + return fmt.Errorf("unexpected type %T for field %s", value, name) + } + m.SetConnectionState(v) + return nil + case agent.FieldContainerStatus: + v, ok := value.(string) + if !ok { + return fmt.Errorf("unexpected type %T for field %s", value, name) + } + m.SetContainerStatus(v) + return nil + case agent.FieldRuntimeState: + v, ok := value.(string) + if !ok { + return fmt.Errorf("unexpected type %T for field %s", value, name) + } + m.SetRuntimeState(v) + return nil + case agent.FieldStalledFromActivity: + v, ok := value.(string) + if !ok { + return fmt.Errorf("unexpected type %T for field %s", value, name) + } + m.SetStalledFromActivity(v) + return nil + case agent.FieldCurrentTurns: + v, ok := value.(int) + if !ok { + return fmt.Errorf("unexpected type %T for field %s", value, name) + } + m.SetCurrentTurns(v) + return nil + case agent.FieldCurrentModelCalls: + v, ok := value.(int) + if !ok { + return fmt.Errorf("unexpected type %T for field %s", value, name) + } + m.SetCurrentModelCalls(v) + return nil + case agent.FieldImage: + v, ok := value.(string) + if !ok { + return fmt.Errorf("unexpected type %T for field %s", value, name) + } + m.SetImage(v) + return nil + case agent.FieldDetached: + v, ok := value.(bool) + if !ok { + return fmt.Errorf("unexpected type %T for field %s", value, name) + } + m.SetDetached(v) + return nil + case agent.FieldRuntime: + v, ok := value.(string) + if !ok { + return fmt.Errorf("unexpected type %T for field %s", value, name) + } + m.SetRuntime(v) + return nil + case agent.FieldRuntimeBrokerID: + v, ok := value.(string) + if !ok { + return fmt.Errorf("unexpected type %T for field %s", value, name) + } + m.SetRuntimeBrokerID(v) + return nil + case agent.FieldWebPtyEnabled: + v, ok := value.(bool) + if !ok { + return fmt.Errorf("unexpected type %T for field %s", value, name) + } + m.SetWebPtyEnabled(v) + return nil + case agent.FieldTaskSummary: + v, ok := value.(string) + if !ok { + return fmt.Errorf("unexpected type %T for field %s", value, name) + } + m.SetTaskSummary(v) + return nil + case agent.FieldMessage: + v, ok := value.(string) + if !ok { + return fmt.Errorf("unexpected type %T for field %s", value, name) + } + m.SetMessage(v) + return nil + case agent.FieldAppliedConfig: + v, ok := value.(string) + if !ok { + return fmt.Errorf("unexpected type %T for field %s", value, name) + } + m.SetAppliedConfig(v) + return nil + case agent.FieldAncestry: + v, ok := value.([]string) + if !ok { + return fmt.Errorf("unexpected type %T for field %s", value, name) + } + m.SetAncestry(v) + return nil + case agent.FieldCreated: + v, ok := value.(time.Time) + if !ok { + return fmt.Errorf("unexpected type %T for field %s", value, name) + } + m.SetCreated(v) + return nil + case agent.FieldUpdated: + v, ok := value.(time.Time) + if !ok { + return fmt.Errorf("unexpected type %T for field %s", value, name) + } + m.SetUpdated(v) + return nil + case agent.FieldLastSeen: + v, ok := value.(time.Time) + if !ok { + return fmt.Errorf("unexpected type %T for field %s", value, name) + } + m.SetLastSeen(v) + return nil + case agent.FieldLastActivityEvent: + v, ok := value.(time.Time) + if !ok { + return fmt.Errorf("unexpected type %T for field %s", value, name) + } + m.SetLastActivityEvent(v) + return nil + case agent.FieldStartedAt: + v, ok := value.(time.Time) + if !ok { + return fmt.Errorf("unexpected type %T for field %s", value, name) + } + m.SetStartedAt(v) + return nil + case agent.FieldDeletedAt: + v, ok := value.(time.Time) + if !ok { + return fmt.Errorf("unexpected type %T for field %s", value, name) + } + m.SetDeletedAt(v) + return nil + case agent.FieldStateVersion: + v, ok := value.(int64) + if !ok { + return fmt.Errorf("unexpected type %T for field %s", value, name) + } + m.SetStateVersion(v) + return nil + } + return fmt.Errorf("unknown Agent field %s", name) +} + +// AddedFields returns all numeric fields that were incremented/decremented during +// this mutation. +func (m *AgentMutation) AddedFields() []string { + var fields []string + if m.addcurrent_turns != nil { + fields = append(fields, agent.FieldCurrentTurns) + } + if m.addcurrent_model_calls != nil { + fields = append(fields, agent.FieldCurrentModelCalls) + } + if m.addstate_version != nil { + fields = append(fields, agent.FieldStateVersion) + } + return fields +} + +// AddedField returns the numeric value that was incremented/decremented on a field +// with the given name. The second boolean return value indicates that this field +// was not set, or was not defined in the schema. +func (m *AgentMutation) AddedField(name string) (ent.Value, bool) { + switch name { + case agent.FieldCurrentTurns: + return m.AddedCurrentTurns() + case agent.FieldCurrentModelCalls: + return m.AddedCurrentModelCalls() + case agent.FieldStateVersion: + return m.AddedStateVersion() + } + return nil, false +} + +// AddField adds the value to the field with the given name. It returns an error if +// the field is not defined in the schema, or if the type mismatched the field +// type. +func (m *AgentMutation) AddField(name string, value ent.Value) error { + switch name { + case agent.FieldCurrentTurns: + v, ok := value.(int) + if !ok { + return fmt.Errorf("unexpected type %T for field %s", value, name) + } + m.AddCurrentTurns(v) + return nil + case agent.FieldCurrentModelCalls: + v, ok := value.(int) + if !ok { + return fmt.Errorf("unexpected type %T for field %s", value, name) + } + m.AddCurrentModelCalls(v) + return nil + case agent.FieldStateVersion: + v, ok := value.(int64) + if !ok { + return fmt.Errorf("unexpected type %T for field %s", value, name) + } + m.AddStateVersion(v) + return nil + } + return fmt.Errorf("unknown Agent numeric field %s", name) +} + +// ClearedFields returns all nullable fields that were cleared during this +// mutation. +func (m *AgentMutation) ClearedFields() []string { + var fields []string + if m.FieldCleared(agent.FieldTemplate) { + fields = append(fields, agent.FieldTemplate) + } + if m.FieldCleared(agent.FieldCreatedBy) { + fields = append(fields, agent.FieldCreatedBy) + } + if m.FieldCleared(agent.FieldOwnerID) { + fields = append(fields, agent.FieldOwnerID) + } + if m.FieldCleared(agent.FieldLabels) { + fields = append(fields, agent.FieldLabels) + } + if m.FieldCleared(agent.FieldAnnotations) { + fields = append(fields, agent.FieldAnnotations) + } + if m.FieldCleared(agent.FieldPhase) { + fields = append(fields, agent.FieldPhase) + } + if m.FieldCleared(agent.FieldActivity) { + fields = append(fields, agent.FieldActivity) + } + if m.FieldCleared(agent.FieldToolName) { + fields = append(fields, agent.FieldToolName) + } + if m.FieldCleared(agent.FieldConnectionState) { + fields = append(fields, agent.FieldConnectionState) + } + if m.FieldCleared(agent.FieldContainerStatus) { + fields = append(fields, agent.FieldContainerStatus) + } + if m.FieldCleared(agent.FieldRuntimeState) { + fields = append(fields, agent.FieldRuntimeState) + } + if m.FieldCleared(agent.FieldStalledFromActivity) { + fields = append(fields, agent.FieldStalledFromActivity) + } + if m.FieldCleared(agent.FieldImage) { + fields = append(fields, agent.FieldImage) + } + if m.FieldCleared(agent.FieldRuntime) { + fields = append(fields, agent.FieldRuntime) + } + if m.FieldCleared(agent.FieldRuntimeBrokerID) { + fields = append(fields, agent.FieldRuntimeBrokerID) + } + if m.FieldCleared(agent.FieldTaskSummary) { + fields = append(fields, agent.FieldTaskSummary) + } + if m.FieldCleared(agent.FieldMessage) { + fields = append(fields, agent.FieldMessage) + } + if m.FieldCleared(agent.FieldAppliedConfig) { + fields = append(fields, agent.FieldAppliedConfig) + } + if m.FieldCleared(agent.FieldAncestry) { + fields = append(fields, agent.FieldAncestry) + } + if m.FieldCleared(agent.FieldLastSeen) { + fields = append(fields, agent.FieldLastSeen) + } + if m.FieldCleared(agent.FieldLastActivityEvent) { + fields = append(fields, agent.FieldLastActivityEvent) + } + if m.FieldCleared(agent.FieldStartedAt) { + fields = append(fields, agent.FieldStartedAt) + } + if m.FieldCleared(agent.FieldDeletedAt) { + fields = append(fields, agent.FieldDeletedAt) + } + return fields +} + +// FieldCleared returns a boolean indicating if a field with the given name was +// cleared in this mutation. +func (m *AgentMutation) FieldCleared(name string) bool { + _, ok := m.clearedFields[name] + return ok +} + +// ClearField clears the value of the field with the given name. It returns an +// error if the field is not defined in the schema. +func (m *AgentMutation) ClearField(name string) error { + switch name { + case agent.FieldTemplate: + m.ClearTemplate() + return nil + case agent.FieldCreatedBy: + m.ClearCreatedBy() + return nil + case agent.FieldOwnerID: + m.ClearOwnerID() + return nil + case agent.FieldLabels: + m.ClearLabels() + return nil + case agent.FieldAnnotations: + m.ClearAnnotations() + return nil + case agent.FieldPhase: + m.ClearPhase() + return nil + case agent.FieldActivity: + m.ClearActivity() + return nil + case agent.FieldToolName: + m.ClearToolName() + return nil + case agent.FieldConnectionState: + m.ClearConnectionState() + return nil + case agent.FieldContainerStatus: + m.ClearContainerStatus() + return nil + case agent.FieldRuntimeState: + m.ClearRuntimeState() + return nil + case agent.FieldStalledFromActivity: + m.ClearStalledFromActivity() + return nil + case agent.FieldImage: + m.ClearImage() + return nil + case agent.FieldRuntime: + m.ClearRuntime() + return nil + case agent.FieldRuntimeBrokerID: + m.ClearRuntimeBrokerID() + return nil + case agent.FieldTaskSummary: + m.ClearTaskSummary() + return nil + case agent.FieldMessage: + m.ClearMessage() + return nil + case agent.FieldAppliedConfig: + m.ClearAppliedConfig() + return nil + case agent.FieldAncestry: + m.ClearAncestry() + return nil + case agent.FieldLastSeen: + m.ClearLastSeen() + return nil + case agent.FieldLastActivityEvent: + m.ClearLastActivityEvent() + return nil + case agent.FieldStartedAt: + m.ClearStartedAt() + return nil + case agent.FieldDeletedAt: + m.ClearDeletedAt() + return nil + } + return fmt.Errorf("unknown Agent nullable field %s", name) +} + +// ResetField resets all changes in the mutation for the field with the given name. +// It returns an error if the field is not defined in the schema. +func (m *AgentMutation) ResetField(name string) error { + switch name { + case agent.FieldSlug: + m.ResetSlug() + return nil + case agent.FieldName: + m.ResetName() + return nil + case agent.FieldTemplate: + m.ResetTemplate() + return nil + case agent.FieldProjectID: + m.ResetProjectID() + return nil + case agent.FieldStatus: + m.ResetStatus() + return nil + case agent.FieldCreatedBy: + m.ResetCreatedBy() + return nil + case agent.FieldOwnerID: + m.ResetOwnerID() + return nil + case agent.FieldDelegationEnabled: + m.ResetDelegationEnabled() + return nil + case agent.FieldVisibility: + m.ResetVisibility() + return nil + case agent.FieldLabels: + m.ResetLabels() + return nil + case agent.FieldAnnotations: + m.ResetAnnotations() + return nil + case agent.FieldPhase: + m.ResetPhase() + return nil + case agent.FieldActivity: + m.ResetActivity() + return nil + case agent.FieldToolName: + m.ResetToolName() + return nil + case agent.FieldConnectionState: + m.ResetConnectionState() + return nil + case agent.FieldContainerStatus: + m.ResetContainerStatus() + return nil + case agent.FieldRuntimeState: + m.ResetRuntimeState() + return nil + case agent.FieldStalledFromActivity: + m.ResetStalledFromActivity() + return nil + case agent.FieldCurrentTurns: + m.ResetCurrentTurns() + return nil + case agent.FieldCurrentModelCalls: + m.ResetCurrentModelCalls() + return nil + case agent.FieldImage: + m.ResetImage() + return nil + case agent.FieldDetached: + m.ResetDetached() + return nil + case agent.FieldRuntime: + m.ResetRuntime() + return nil + case agent.FieldRuntimeBrokerID: + m.ResetRuntimeBrokerID() + return nil + case agent.FieldWebPtyEnabled: + m.ResetWebPtyEnabled() + return nil + case agent.FieldTaskSummary: + m.ResetTaskSummary() + return nil + case agent.FieldMessage: + m.ResetMessage() + return nil + case agent.FieldAppliedConfig: + m.ResetAppliedConfig() + return nil + case agent.FieldAncestry: + m.ResetAncestry() + return nil + case agent.FieldCreated: + m.ResetCreated() + return nil + case agent.FieldUpdated: + m.ResetUpdated() + return nil + case agent.FieldLastSeen: + m.ResetLastSeen() + return nil + case agent.FieldLastActivityEvent: + m.ResetLastActivityEvent() + return nil + case agent.FieldStartedAt: + m.ResetStartedAt() + return nil + case agent.FieldDeletedAt: + m.ResetDeletedAt() + return nil + case agent.FieldStateVersion: + m.ResetStateVersion() + return nil + } + return fmt.Errorf("unknown Agent field %s", name) +} + +// AddedEdges returns all edge names that were set/added in this mutation. +func (m *AgentMutation) AddedEdges() []string { + edges := make([]string, 0, 3) + if m.project != nil { + edges = append(edges, agent.EdgeProject) + } + if m.memberships != nil { + edges = append(edges, agent.EdgeMemberships) + } + if m.policy_bindings != nil { + edges = append(edges, agent.EdgePolicyBindings) + } + return edges +} + +// AddedIDs returns all IDs (to other nodes) that were added for the given edge +// name in this mutation. +func (m *AgentMutation) AddedIDs(name string) []ent.Value { + switch name { + case agent.EdgeProject: + if id := m.project; id != nil { + return []ent.Value{*id} + } + case agent.EdgeMemberships: + ids := make([]ent.Value, 0, len(m.memberships)) + for id := range m.memberships { + ids = append(ids, id) + } + return ids + case agent.EdgePolicyBindings: + ids := make([]ent.Value, 0, len(m.policy_bindings)) + for id := range m.policy_bindings { + ids = append(ids, id) + } + return ids + } + return nil +} + +// RemovedEdges returns all edge names that were removed in this mutation. +func (m *AgentMutation) RemovedEdges() []string { + edges := make([]string, 0, 3) + if m.removedmemberships != nil { + edges = append(edges, agent.EdgeMemberships) + } + if m.removedpolicy_bindings != nil { + edges = append(edges, agent.EdgePolicyBindings) + } + return edges +} + +// RemovedIDs returns all IDs (to other nodes) that were removed for the edge with +// the given name in this mutation. +func (m *AgentMutation) RemovedIDs(name string) []ent.Value { + switch name { + case agent.EdgeMemberships: + ids := make([]ent.Value, 0, len(m.removedmemberships)) + for id := range m.removedmemberships { + ids = append(ids, id) + } + return ids + case agent.EdgePolicyBindings: + ids := make([]ent.Value, 0, len(m.removedpolicy_bindings)) + for id := range m.removedpolicy_bindings { + ids = append(ids, id) + } + return ids + } + return nil +} + +// ClearedEdges returns all edge names that were cleared in this mutation. +func (m *AgentMutation) ClearedEdges() []string { + edges := make([]string, 0, 3) + if m.clearedproject { + edges = append(edges, agent.EdgeProject) + } + if m.clearedmemberships { + edges = append(edges, agent.EdgeMemberships) + } + if m.clearedpolicy_bindings { + edges = append(edges, agent.EdgePolicyBindings) + } + return edges +} + +// EdgeCleared returns a boolean which indicates if the edge with the given name +// was cleared in this mutation. +func (m *AgentMutation) EdgeCleared(name string) bool { + switch name { + case agent.EdgeProject: + return m.clearedproject + case agent.EdgeMemberships: + return m.clearedmemberships + case agent.EdgePolicyBindings: + return m.clearedpolicy_bindings + } + return false +} + +// ClearEdge clears the value of the edge with the given name. It returns an error +// if that edge is not defined in the schema. +func (m *AgentMutation) ClearEdge(name string) error { + switch name { + case agent.EdgeProject: + m.ClearProject() + return nil + } + return fmt.Errorf("unknown Agent unique edge %s", name) +} + +// ResetEdge resets all changes to the edge with the given name in this mutation. +// It returns an error if the edge is not defined in the schema. +func (m *AgentMutation) ResetEdge(name string) error { + switch name { + case agent.EdgeProject: + m.ResetProject() + return nil + case agent.EdgeMemberships: + m.ResetMemberships() + return nil + case agent.EdgePolicyBindings: + m.ResetPolicyBindings() + return nil + } + return fmt.Errorf("unknown Agent edge %s", name) +} + +// AllowListEntryMutation represents an operation that mutates the AllowListEntry nodes in the graph. +type AllowListEntryMutation struct { + config + op Op + typ string + id *uuid.UUID + email *string + note *string + added_by *string + invite_id *string + created *time.Time + clearedFields map[string]struct{} + done bool + oldValue func(context.Context) (*AllowListEntry, error) + predicates []predicate.AllowListEntry +} + +var _ ent.Mutation = (*AllowListEntryMutation)(nil) + +// allowlistentryOption allows management of the mutation configuration using functional options. +type allowlistentryOption func(*AllowListEntryMutation) + +// newAllowListEntryMutation creates new mutation for the AllowListEntry entity. +func newAllowListEntryMutation(c config, op Op, opts ...allowlistentryOption) *AllowListEntryMutation { + m := &AllowListEntryMutation{ + config: c, + op: op, + typ: TypeAllowListEntry, + clearedFields: make(map[string]struct{}), + } + for _, opt := range opts { + opt(m) + } + return m +} + +// withAllowListEntryID sets the ID field of the mutation. +func withAllowListEntryID(id uuid.UUID) allowlistentryOption { + return func(m *AllowListEntryMutation) { + var ( + err error + once sync.Once + value *AllowListEntry + ) + m.oldValue = func(ctx context.Context) (*AllowListEntry, error) { + once.Do(func() { + if m.done { + err = errors.New("querying old values post mutation is not allowed") + } else { + value, err = m.Client().AllowListEntry.Get(ctx, id) + } + }) + return value, err + } + m.id = &id + } +} + +// withAllowListEntry sets the old AllowListEntry of the mutation. +func withAllowListEntry(node *AllowListEntry) allowlistentryOption { + return func(m *AllowListEntryMutation) { + m.oldValue = func(context.Context) (*AllowListEntry, error) { + return node, nil + } + m.id = &node.ID + } +} + +// Client returns a new `ent.Client` from the mutation. If the mutation was +// executed in a transaction (ent.Tx), a transactional client is returned. +func (m AllowListEntryMutation) Client() *Client { + client := &Client{config: m.config} + client.init() + return client +} + +// Tx returns an `ent.Tx` for mutations that were executed in transactions; +// it returns an error otherwise. +func (m AllowListEntryMutation) Tx() (*Tx, error) { + if _, ok := m.driver.(*txDriver); !ok { + return nil, errors.New("ent: mutation is not running in a transaction") + } + tx := &Tx{config: m.config} + tx.init() + return tx, nil +} + +// SetID sets the value of the id field. Note that this +// operation is only accepted on creation of AllowListEntry entities. +func (m *AllowListEntryMutation) SetID(id uuid.UUID) { + m.id = &id +} + +// ID returns the ID value in the mutation. Note that the ID is only available +// if it was provided to the builder or after it was returned from the database. +func (m *AllowListEntryMutation) ID() (id uuid.UUID, exists bool) { + if m.id == nil { + return + } + return *m.id, true +} + +// IDs queries the database and returns the entity ids that match the mutation's predicate. +// That means, if the mutation is applied within a transaction with an isolation level such +// as sql.LevelSerializable, the returned ids match the ids of the rows that will be updated +// or updated by the mutation. +func (m *AllowListEntryMutation) IDs(ctx context.Context) ([]uuid.UUID, error) { + switch { + case m.op.Is(OpUpdateOne | OpDeleteOne): + id, exists := m.ID() + if exists { + return []uuid.UUID{id}, nil + } + fallthrough + case m.op.Is(OpUpdate | OpDelete): + return m.Client().AllowListEntry.Query().Where(m.predicates...).IDs(ctx) + default: + return nil, fmt.Errorf("IDs is not allowed on %s operations", m.op) + } +} + +// SetEmail sets the "email" field. +func (m *AllowListEntryMutation) SetEmail(s string) { + m.email = &s +} + +// Email returns the value of the "email" field in the mutation. +func (m *AllowListEntryMutation) Email() (r string, exists bool) { + v := m.email + if v == nil { + return + } + return *v, true +} + +// OldEmail returns the old "email" field's value of the AllowListEntry entity. +// If the AllowListEntry object wasn't provided to the builder, the object is fetched from the database. +// An error is returned if the mutation operation is not UpdateOne, or the database query fails. +func (m *AllowListEntryMutation) OldEmail(ctx context.Context) (v string, err error) { + if !m.op.Is(OpUpdateOne) { + return v, errors.New("OldEmail is only allowed on UpdateOne operations") + } + if m.id == nil || m.oldValue == nil { + return v, errors.New("OldEmail requires an ID field in the mutation") + } + oldValue, err := m.oldValue(ctx) + if err != nil { + return v, fmt.Errorf("querying old value for OldEmail: %w", err) + } + return oldValue.Email, nil +} + +// ResetEmail resets all changes to the "email" field. +func (m *AllowListEntryMutation) ResetEmail() { + m.email = nil +} + +// SetNote sets the "note" field. +func (m *AllowListEntryMutation) SetNote(s string) { + m.note = &s +} + +// Note returns the value of the "note" field in the mutation. +func (m *AllowListEntryMutation) Note() (r string, exists bool) { + v := m.note + if v == nil { + return + } + return *v, true +} + +// OldNote returns the old "note" field's value of the AllowListEntry entity. +// If the AllowListEntry object wasn't provided to the builder, the object is fetched from the database. +// An error is returned if the mutation operation is not UpdateOne, or the database query fails. +func (m *AllowListEntryMutation) OldNote(ctx context.Context) (v string, err error) { + if !m.op.Is(OpUpdateOne) { + return v, errors.New("OldNote is only allowed on UpdateOne operations") + } + if m.id == nil || m.oldValue == nil { + return v, errors.New("OldNote requires an ID field in the mutation") + } + oldValue, err := m.oldValue(ctx) + if err != nil { + return v, fmt.Errorf("querying old value for OldNote: %w", err) + } + return oldValue.Note, nil +} + +// ResetNote resets all changes to the "note" field. +func (m *AllowListEntryMutation) ResetNote() { + m.note = nil +} + +// SetAddedBy sets the "added_by" field. +func (m *AllowListEntryMutation) SetAddedBy(s string) { + m.added_by = &s +} + +// AddedBy returns the value of the "added_by" field in the mutation. +func (m *AllowListEntryMutation) AddedBy() (r string, exists bool) { + v := m.added_by + if v == nil { + return + } + return *v, true +} + +// OldAddedBy returns the old "added_by" field's value of the AllowListEntry entity. +// If the AllowListEntry object wasn't provided to the builder, the object is fetched from the database. +// An error is returned if the mutation operation is not UpdateOne, or the database query fails. +func (m *AllowListEntryMutation) OldAddedBy(ctx context.Context) (v string, err error) { + if !m.op.Is(OpUpdateOne) { + return v, errors.New("OldAddedBy is only allowed on UpdateOne operations") + } + if m.id == nil || m.oldValue == nil { + return v, errors.New("OldAddedBy requires an ID field in the mutation") + } + oldValue, err := m.oldValue(ctx) + if err != nil { + return v, fmt.Errorf("querying old value for OldAddedBy: %w", err) + } + return oldValue.AddedBy, nil +} + +// ResetAddedBy resets all changes to the "added_by" field. +func (m *AllowListEntryMutation) ResetAddedBy() { + m.added_by = nil +} + +// SetInviteID sets the "invite_id" field. +func (m *AllowListEntryMutation) SetInviteID(s string) { + m.invite_id = &s +} + +// InviteID returns the value of the "invite_id" field in the mutation. +func (m *AllowListEntryMutation) InviteID() (r string, exists bool) { + v := m.invite_id + if v == nil { + return + } + return *v, true +} + +// OldInviteID returns the old "invite_id" field's value of the AllowListEntry entity. +// If the AllowListEntry object wasn't provided to the builder, the object is fetched from the database. +// An error is returned if the mutation operation is not UpdateOne, or the database query fails. +func (m *AllowListEntryMutation) OldInviteID(ctx context.Context) (v string, err error) { + if !m.op.Is(OpUpdateOne) { + return v, errors.New("OldInviteID is only allowed on UpdateOne operations") + } + if m.id == nil || m.oldValue == nil { + return v, errors.New("OldInviteID requires an ID field in the mutation") + } + oldValue, err := m.oldValue(ctx) + if err != nil { + return v, fmt.Errorf("querying old value for OldInviteID: %w", err) + } + return oldValue.InviteID, nil +} + +// ClearInviteID clears the value of the "invite_id" field. +func (m *AllowListEntryMutation) ClearInviteID() { + m.invite_id = nil + m.clearedFields[allowlistentry.FieldInviteID] = struct{}{} +} + +// InviteIDCleared returns if the "invite_id" field was cleared in this mutation. +func (m *AllowListEntryMutation) InviteIDCleared() bool { + _, ok := m.clearedFields[allowlistentry.FieldInviteID] + return ok +} + +// ResetInviteID resets all changes to the "invite_id" field. +func (m *AllowListEntryMutation) ResetInviteID() { + m.invite_id = nil + delete(m.clearedFields, allowlistentry.FieldInviteID) +} + +// SetCreated sets the "created" field. +func (m *AllowListEntryMutation) SetCreated(t time.Time) { + m.created = &t +} + +// Created returns the value of the "created" field in the mutation. +func (m *AllowListEntryMutation) Created() (r time.Time, exists bool) { + v := m.created + if v == nil { + return + } + return *v, true +} + +// OldCreated returns the old "created" field's value of the AllowListEntry entity. +// If the AllowListEntry object wasn't provided to the builder, the object is fetched from the database. +// An error is returned if the mutation operation is not UpdateOne, or the database query fails. +func (m *AllowListEntryMutation) OldCreated(ctx context.Context) (v time.Time, err error) { + if !m.op.Is(OpUpdateOne) { + return v, errors.New("OldCreated is only allowed on UpdateOne operations") + } + if m.id == nil || m.oldValue == nil { + return v, errors.New("OldCreated requires an ID field in the mutation") + } + oldValue, err := m.oldValue(ctx) + if err != nil { + return v, fmt.Errorf("querying old value for OldCreated: %w", err) + } + return oldValue.Created, nil +} + +// ResetCreated resets all changes to the "created" field. +func (m *AllowListEntryMutation) ResetCreated() { + m.created = nil +} + +// Where appends a list predicates to the AllowListEntryMutation builder. +func (m *AllowListEntryMutation) Where(ps ...predicate.AllowListEntry) { + m.predicates = append(m.predicates, ps...) +} + +// WhereP appends storage-level predicates to the AllowListEntryMutation builder. Using this method, +// users can use type-assertion to append predicates that do not depend on any generated package. +func (m *AllowListEntryMutation) WhereP(ps ...func(*sql.Selector)) { + p := make([]predicate.AllowListEntry, len(ps)) + for i := range ps { + p[i] = ps[i] + } + m.Where(p...) +} + +// Op returns the operation name. +func (m *AllowListEntryMutation) Op() Op { + return m.op +} + +// SetOp allows setting the mutation operation. +func (m *AllowListEntryMutation) SetOp(op Op) { + m.op = op +} + +// Type returns the node type of this mutation (AllowListEntry). +func (m *AllowListEntryMutation) Type() string { + return m.typ +} + +// Fields returns all fields that were changed during this mutation. Note that in +// order to get all numeric fields that were incremented/decremented, call +// AddedFields(). +func (m *AllowListEntryMutation) Fields() []string { + fields := make([]string, 0, 5) + if m.email != nil { + fields = append(fields, allowlistentry.FieldEmail) + } + if m.note != nil { + fields = append(fields, allowlistentry.FieldNote) + } + if m.added_by != nil { + fields = append(fields, allowlistentry.FieldAddedBy) + } + if m.invite_id != nil { + fields = append(fields, allowlistentry.FieldInviteID) + } + if m.created != nil { + fields = append(fields, allowlistentry.FieldCreated) + } + return fields +} + +// Field returns the value of a field with the given name. The second boolean +// return value indicates that this field was not set, or was not defined in the +// schema. +func (m *AllowListEntryMutation) Field(name string) (ent.Value, bool) { + switch name { + case allowlistentry.FieldEmail: + return m.Email() + case allowlistentry.FieldNote: + return m.Note() + case allowlistentry.FieldAddedBy: + return m.AddedBy() + case allowlistentry.FieldInviteID: + return m.InviteID() + case allowlistentry.FieldCreated: + return m.Created() + } + return nil, false +} + +// OldField returns the old value of the field from the database. An error is +// returned if the mutation operation is not UpdateOne, or the query to the +// database failed. +func (m *AllowListEntryMutation) OldField(ctx context.Context, name string) (ent.Value, error) { + switch name { + case allowlistentry.FieldEmail: + return m.OldEmail(ctx) + case allowlistentry.FieldNote: + return m.OldNote(ctx) + case allowlistentry.FieldAddedBy: + return m.OldAddedBy(ctx) + case allowlistentry.FieldInviteID: + return m.OldInviteID(ctx) + case allowlistentry.FieldCreated: + return m.OldCreated(ctx) + } + return nil, fmt.Errorf("unknown AllowListEntry field %s", name) +} + +// SetField sets the value of a field with the given name. It returns an error if +// the field is not defined in the schema, or if the type mismatched the field +// type. +func (m *AllowListEntryMutation) SetField(name string, value ent.Value) error { + switch name { + case allowlistentry.FieldEmail: + v, ok := value.(string) + if !ok { + return fmt.Errorf("unexpected type %T for field %s", value, name) + } + m.SetEmail(v) + return nil + case allowlistentry.FieldNote: + v, ok := value.(string) + if !ok { + return fmt.Errorf("unexpected type %T for field %s", value, name) + } + m.SetNote(v) + return nil + case allowlistentry.FieldAddedBy: + v, ok := value.(string) + if !ok { + return fmt.Errorf("unexpected type %T for field %s", value, name) + } + m.SetAddedBy(v) + return nil + case allowlistentry.FieldInviteID: + v, ok := value.(string) + if !ok { + return fmt.Errorf("unexpected type %T for field %s", value, name) + } + m.SetInviteID(v) + return nil + case allowlistentry.FieldCreated: + v, ok := value.(time.Time) + if !ok { + return fmt.Errorf("unexpected type %T for field %s", value, name) + } + m.SetCreated(v) + return nil + } + return fmt.Errorf("unknown AllowListEntry field %s", name) +} + +// AddedFields returns all numeric fields that were incremented/decremented during +// this mutation. +func (m *AllowListEntryMutation) AddedFields() []string { + return nil +} + +// AddedField returns the numeric value that was incremented/decremented on a field +// with the given name. The second boolean return value indicates that this field +// was not set, or was not defined in the schema. +func (m *AllowListEntryMutation) AddedField(name string) (ent.Value, bool) { + return nil, false +} + +// AddField adds the value to the field with the given name. It returns an error if +// the field is not defined in the schema, or if the type mismatched the field +// type. +func (m *AllowListEntryMutation) AddField(name string, value ent.Value) error { + switch name { + } + return fmt.Errorf("unknown AllowListEntry numeric field %s", name) +} + +// ClearedFields returns all nullable fields that were cleared during this +// mutation. +func (m *AllowListEntryMutation) ClearedFields() []string { + var fields []string + if m.FieldCleared(allowlistentry.FieldInviteID) { + fields = append(fields, allowlistentry.FieldInviteID) + } + return fields +} + +// FieldCleared returns a boolean indicating if a field with the given name was +// cleared in this mutation. +func (m *AllowListEntryMutation) FieldCleared(name string) bool { + _, ok := m.clearedFields[name] + return ok +} + +// ClearField clears the value of the field with the given name. It returns an +// error if the field is not defined in the schema. +func (m *AllowListEntryMutation) ClearField(name string) error { + switch name { + case allowlistentry.FieldInviteID: + m.ClearInviteID() + return nil + } + return fmt.Errorf("unknown AllowListEntry nullable field %s", name) +} + +// ResetField resets all changes in the mutation for the field with the given name. +// It returns an error if the field is not defined in the schema. +func (m *AllowListEntryMutation) ResetField(name string) error { + switch name { + case allowlistentry.FieldEmail: + m.ResetEmail() + return nil + case allowlistentry.FieldNote: + m.ResetNote() + return nil + case allowlistentry.FieldAddedBy: + m.ResetAddedBy() + return nil + case allowlistentry.FieldInviteID: + m.ResetInviteID() + return nil + case allowlistentry.FieldCreated: + m.ResetCreated() + return nil + } + return fmt.Errorf("unknown AllowListEntry field %s", name) +} + +// AddedEdges returns all edge names that were set/added in this mutation. +func (m *AllowListEntryMutation) AddedEdges() []string { + edges := make([]string, 0, 0) + return edges +} + +// AddedIDs returns all IDs (to other nodes) that were added for the given edge +// name in this mutation. +func (m *AllowListEntryMutation) AddedIDs(name string) []ent.Value { + return nil +} + +// RemovedEdges returns all edge names that were removed in this mutation. +func (m *AllowListEntryMutation) RemovedEdges() []string { + edges := make([]string, 0, 0) + return edges +} + +// RemovedIDs returns all IDs (to other nodes) that were removed for the edge with +// the given name in this mutation. +func (m *AllowListEntryMutation) RemovedIDs(name string) []ent.Value { + return nil +} + +// ClearedEdges returns all edge names that were cleared in this mutation. +func (m *AllowListEntryMutation) ClearedEdges() []string { + edges := make([]string, 0, 0) + return edges +} + +// EdgeCleared returns a boolean which indicates if the edge with the given name +// was cleared in this mutation. +func (m *AllowListEntryMutation) EdgeCleared(name string) bool { + return false +} + +// ClearEdge clears the value of the edge with the given name. It returns an error +// if that edge is not defined in the schema. +func (m *AllowListEntryMutation) ClearEdge(name string) error { + return fmt.Errorf("unknown AllowListEntry unique edge %s", name) +} + +// ResetEdge resets all changes to the edge with the given name in this mutation. +// It returns an error if the edge is not defined in the schema. +func (m *AllowListEntryMutation) ResetEdge(name string) error { + return fmt.Errorf("unknown AllowListEntry edge %s", name) +} + +// ApiKeyMutation represents an operation that mutates the ApiKey nodes in the graph. +type ApiKeyMutation struct { + config + op Op + typ string + id *uuid.UUID + user_id *uuid.UUID + name *string + prefix *string + key_hash *string + scopes *string + revoked *bool + expires_at *time.Time + last_used *time.Time + created *time.Time + clearedFields map[string]struct{} + done bool + oldValue func(context.Context) (*ApiKey, error) + predicates []predicate.ApiKey +} + +var _ ent.Mutation = (*ApiKeyMutation)(nil) + +// apikeyOption allows management of the mutation configuration using functional options. +type apikeyOption func(*ApiKeyMutation) + +// newApiKeyMutation creates new mutation for the ApiKey entity. +func newApiKeyMutation(c config, op Op, opts ...apikeyOption) *ApiKeyMutation { + m := &ApiKeyMutation{ + config: c, + op: op, + typ: TypeApiKey, + clearedFields: make(map[string]struct{}), + } + for _, opt := range opts { + opt(m) + } + return m +} + +// withApiKeyID sets the ID field of the mutation. +func withApiKeyID(id uuid.UUID) apikeyOption { + return func(m *ApiKeyMutation) { + var ( + err error + once sync.Once + value *ApiKey + ) + m.oldValue = func(ctx context.Context) (*ApiKey, error) { + once.Do(func() { + if m.done { + err = errors.New("querying old values post mutation is not allowed") + } else { + value, err = m.Client().ApiKey.Get(ctx, id) + } + }) + return value, err + } + m.id = &id + } +} + +// withApiKey sets the old ApiKey of the mutation. +func withApiKey(node *ApiKey) apikeyOption { + return func(m *ApiKeyMutation) { + m.oldValue = func(context.Context) (*ApiKey, error) { + return node, nil + } + m.id = &node.ID + } +} + +// Client returns a new `ent.Client` from the mutation. If the mutation was +// executed in a transaction (ent.Tx), a transactional client is returned. +func (m ApiKeyMutation) Client() *Client { + client := &Client{config: m.config} + client.init() + return client +} + +// Tx returns an `ent.Tx` for mutations that were executed in transactions; +// it returns an error otherwise. +func (m ApiKeyMutation) Tx() (*Tx, error) { + if _, ok := m.driver.(*txDriver); !ok { + return nil, errors.New("ent: mutation is not running in a transaction") + } + tx := &Tx{config: m.config} + tx.init() + return tx, nil +} + +// SetID sets the value of the id field. Note that this +// operation is only accepted on creation of ApiKey entities. +func (m *ApiKeyMutation) SetID(id uuid.UUID) { + m.id = &id +} + +// ID returns the ID value in the mutation. Note that the ID is only available +// if it was provided to the builder or after it was returned from the database. +func (m *ApiKeyMutation) ID() (id uuid.UUID, exists bool) { + if m.id == nil { + return + } + return *m.id, true +} + +// IDs queries the database and returns the entity ids that match the mutation's predicate. +// That means, if the mutation is applied within a transaction with an isolation level such +// as sql.LevelSerializable, the returned ids match the ids of the rows that will be updated +// or updated by the mutation. +func (m *ApiKeyMutation) IDs(ctx context.Context) ([]uuid.UUID, error) { + switch { + case m.op.Is(OpUpdateOne | OpDeleteOne): + id, exists := m.ID() + if exists { + return []uuid.UUID{id}, nil + } + fallthrough + case m.op.Is(OpUpdate | OpDelete): + return m.Client().ApiKey.Query().Where(m.predicates...).IDs(ctx) + default: + return nil, fmt.Errorf("IDs is not allowed on %s operations", m.op) + } +} + +// SetUserID sets the "user_id" field. +func (m *ApiKeyMutation) SetUserID(u uuid.UUID) { + m.user_id = &u +} + +// UserID returns the value of the "user_id" field in the mutation. +func (m *ApiKeyMutation) UserID() (r uuid.UUID, exists bool) { + v := m.user_id + if v == nil { + return + } + return *v, true +} + +// OldUserID returns the old "user_id" field's value of the ApiKey entity. +// If the ApiKey object wasn't provided to the builder, the object is fetched from the database. +// An error is returned if the mutation operation is not UpdateOne, or the database query fails. +func (m *ApiKeyMutation) OldUserID(ctx context.Context) (v uuid.UUID, err error) { + if !m.op.Is(OpUpdateOne) { + return v, errors.New("OldUserID is only allowed on UpdateOne operations") + } + if m.id == nil || m.oldValue == nil { + return v, errors.New("OldUserID requires an ID field in the mutation") + } + oldValue, err := m.oldValue(ctx) + if err != nil { + return v, fmt.Errorf("querying old value for OldUserID: %w", err) + } + return oldValue.UserID, nil +} + +// ResetUserID resets all changes to the "user_id" field. +func (m *ApiKeyMutation) ResetUserID() { + m.user_id = nil +} + +// SetName sets the "name" field. +func (m *ApiKeyMutation) SetName(s string) { + m.name = &s +} + +// Name returns the value of the "name" field in the mutation. +func (m *ApiKeyMutation) Name() (r string, exists bool) { + v := m.name + if v == nil { + return + } + return *v, true +} + +// OldName returns the old "name" field's value of the ApiKey entity. +// If the ApiKey object wasn't provided to the builder, the object is fetched from the database. +// An error is returned if the mutation operation is not UpdateOne, or the database query fails. +func (m *ApiKeyMutation) OldName(ctx context.Context) (v string, err error) { + if !m.op.Is(OpUpdateOne) { + return v, errors.New("OldName is only allowed on UpdateOne operations") + } + if m.id == nil || m.oldValue == nil { + return v, errors.New("OldName requires an ID field in the mutation") + } + oldValue, err := m.oldValue(ctx) + if err != nil { + return v, fmt.Errorf("querying old value for OldName: %w", err) + } + return oldValue.Name, nil +} + +// ClearName clears the value of the "name" field. +func (m *ApiKeyMutation) ClearName() { + m.name = nil + m.clearedFields[apikey.FieldName] = struct{}{} +} + +// NameCleared returns if the "name" field was cleared in this mutation. +func (m *ApiKeyMutation) NameCleared() bool { + _, ok := m.clearedFields[apikey.FieldName] + return ok +} + +// ResetName resets all changes to the "name" field. +func (m *ApiKeyMutation) ResetName() { + m.name = nil + delete(m.clearedFields, apikey.FieldName) +} + +// SetPrefix sets the "prefix" field. +func (m *ApiKeyMutation) SetPrefix(s string) { + m.prefix = &s +} + +// Prefix returns the value of the "prefix" field in the mutation. +func (m *ApiKeyMutation) Prefix() (r string, exists bool) { + v := m.prefix + if v == nil { + return + } + return *v, true +} + +// OldPrefix returns the old "prefix" field's value of the ApiKey entity. +// If the ApiKey object wasn't provided to the builder, the object is fetched from the database. +// An error is returned if the mutation operation is not UpdateOne, or the database query fails. +func (m *ApiKeyMutation) OldPrefix(ctx context.Context) (v string, err error) { + if !m.op.Is(OpUpdateOne) { + return v, errors.New("OldPrefix is only allowed on UpdateOne operations") + } + if m.id == nil || m.oldValue == nil { + return v, errors.New("OldPrefix requires an ID field in the mutation") + } + oldValue, err := m.oldValue(ctx) + if err != nil { + return v, fmt.Errorf("querying old value for OldPrefix: %w", err) + } + return oldValue.Prefix, nil +} + +// ClearPrefix clears the value of the "prefix" field. +func (m *ApiKeyMutation) ClearPrefix() { + m.prefix = nil + m.clearedFields[apikey.FieldPrefix] = struct{}{} +} + +// PrefixCleared returns if the "prefix" field was cleared in this mutation. +func (m *ApiKeyMutation) PrefixCleared() bool { + _, ok := m.clearedFields[apikey.FieldPrefix] + return ok +} + +// ResetPrefix resets all changes to the "prefix" field. +func (m *ApiKeyMutation) ResetPrefix() { + m.prefix = nil + delete(m.clearedFields, apikey.FieldPrefix) +} + +// SetKeyHash sets the "key_hash" field. +func (m *ApiKeyMutation) SetKeyHash(s string) { + m.key_hash = &s +} + +// KeyHash returns the value of the "key_hash" field in the mutation. +func (m *ApiKeyMutation) KeyHash() (r string, exists bool) { + v := m.key_hash + if v == nil { + return + } + return *v, true +} + +// OldKeyHash returns the old "key_hash" field's value of the ApiKey entity. +// If the ApiKey object wasn't provided to the builder, the object is fetched from the database. +// An error is returned if the mutation operation is not UpdateOne, or the database query fails. +func (m *ApiKeyMutation) OldKeyHash(ctx context.Context) (v string, err error) { + if !m.op.Is(OpUpdateOne) { + return v, errors.New("OldKeyHash is only allowed on UpdateOne operations") + } + if m.id == nil || m.oldValue == nil { + return v, errors.New("OldKeyHash requires an ID field in the mutation") + } + oldValue, err := m.oldValue(ctx) + if err != nil { + return v, fmt.Errorf("querying old value for OldKeyHash: %w", err) + } + return oldValue.KeyHash, nil +} + +// ResetKeyHash resets all changes to the "key_hash" field. +func (m *ApiKeyMutation) ResetKeyHash() { + m.key_hash = nil +} + +// SetScopes sets the "scopes" field. +func (m *ApiKeyMutation) SetScopes(s string) { + m.scopes = &s +} + +// Scopes returns the value of the "scopes" field in the mutation. +func (m *ApiKeyMutation) Scopes() (r string, exists bool) { + v := m.scopes + if v == nil { + return + } + return *v, true +} + +// OldScopes returns the old "scopes" field's value of the ApiKey entity. +// If the ApiKey object wasn't provided to the builder, the object is fetched from the database. +// An error is returned if the mutation operation is not UpdateOne, or the database query fails. +func (m *ApiKeyMutation) OldScopes(ctx context.Context) (v string, err error) { + if !m.op.Is(OpUpdateOne) { + return v, errors.New("OldScopes is only allowed on UpdateOne operations") + } + if m.id == nil || m.oldValue == nil { + return v, errors.New("OldScopes requires an ID field in the mutation") + } + oldValue, err := m.oldValue(ctx) + if err != nil { + return v, fmt.Errorf("querying old value for OldScopes: %w", err) + } + return oldValue.Scopes, nil +} + +// ClearScopes clears the value of the "scopes" field. +func (m *ApiKeyMutation) ClearScopes() { + m.scopes = nil + m.clearedFields[apikey.FieldScopes] = struct{}{} +} + +// ScopesCleared returns if the "scopes" field was cleared in this mutation. +func (m *ApiKeyMutation) ScopesCleared() bool { + _, ok := m.clearedFields[apikey.FieldScopes] + return ok +} + +// ResetScopes resets all changes to the "scopes" field. +func (m *ApiKeyMutation) ResetScopes() { + m.scopes = nil + delete(m.clearedFields, apikey.FieldScopes) +} + +// SetRevoked sets the "revoked" field. +func (m *ApiKeyMutation) SetRevoked(b bool) { + m.revoked = &b +} + +// Revoked returns the value of the "revoked" field in the mutation. +func (m *ApiKeyMutation) Revoked() (r bool, exists bool) { + v := m.revoked + if v == nil { + return + } + return *v, true +} + +// OldRevoked returns the old "revoked" field's value of the ApiKey entity. +// If the ApiKey object wasn't provided to the builder, the object is fetched from the database. +// An error is returned if the mutation operation is not UpdateOne, or the database query fails. +func (m *ApiKeyMutation) OldRevoked(ctx context.Context) (v bool, err error) { + if !m.op.Is(OpUpdateOne) { + return v, errors.New("OldRevoked is only allowed on UpdateOne operations") + } + if m.id == nil || m.oldValue == nil { + return v, errors.New("OldRevoked requires an ID field in the mutation") + } + oldValue, err := m.oldValue(ctx) + if err != nil { + return v, fmt.Errorf("querying old value for OldRevoked: %w", err) + } + return oldValue.Revoked, nil +} + +// ResetRevoked resets all changes to the "revoked" field. +func (m *ApiKeyMutation) ResetRevoked() { + m.revoked = nil +} + +// SetExpiresAt sets the "expires_at" field. +func (m *ApiKeyMutation) SetExpiresAt(t time.Time) { + m.expires_at = &t +} + +// ExpiresAt returns the value of the "expires_at" field in the mutation. +func (m *ApiKeyMutation) ExpiresAt() (r time.Time, exists bool) { + v := m.expires_at + if v == nil { + return + } + return *v, true +} + +// OldExpiresAt returns the old "expires_at" field's value of the ApiKey entity. +// If the ApiKey object wasn't provided to the builder, the object is fetched from the database. +// An error is returned if the mutation operation is not UpdateOne, or the database query fails. +func (m *ApiKeyMutation) OldExpiresAt(ctx context.Context) (v *time.Time, err error) { + if !m.op.Is(OpUpdateOne) { + return v, errors.New("OldExpiresAt is only allowed on UpdateOne operations") + } + if m.id == nil || m.oldValue == nil { + return v, errors.New("OldExpiresAt requires an ID field in the mutation") + } + oldValue, err := m.oldValue(ctx) + if err != nil { + return v, fmt.Errorf("querying old value for OldExpiresAt: %w", err) + } + return oldValue.ExpiresAt, nil +} + +// ClearExpiresAt clears the value of the "expires_at" field. +func (m *ApiKeyMutation) ClearExpiresAt() { + m.expires_at = nil + m.clearedFields[apikey.FieldExpiresAt] = struct{}{} +} + +// ExpiresAtCleared returns if the "expires_at" field was cleared in this mutation. +func (m *ApiKeyMutation) ExpiresAtCleared() bool { + _, ok := m.clearedFields[apikey.FieldExpiresAt] + return ok +} + +// ResetExpiresAt resets all changes to the "expires_at" field. +func (m *ApiKeyMutation) ResetExpiresAt() { + m.expires_at = nil + delete(m.clearedFields, apikey.FieldExpiresAt) +} + +// SetLastUsed sets the "last_used" field. +func (m *ApiKeyMutation) SetLastUsed(t time.Time) { + m.last_used = &t +} + +// LastUsed returns the value of the "last_used" field in the mutation. +func (m *ApiKeyMutation) LastUsed() (r time.Time, exists bool) { + v := m.last_used + if v == nil { + return + } + return *v, true +} + +// OldLastUsed returns the old "last_used" field's value of the ApiKey entity. +// If the ApiKey object wasn't provided to the builder, the object is fetched from the database. +// An error is returned if the mutation operation is not UpdateOne, or the database query fails. +func (m *ApiKeyMutation) OldLastUsed(ctx context.Context) (v *time.Time, err error) { + if !m.op.Is(OpUpdateOne) { + return v, errors.New("OldLastUsed is only allowed on UpdateOne operations") + } + if m.id == nil || m.oldValue == nil { + return v, errors.New("OldLastUsed requires an ID field in the mutation") + } + oldValue, err := m.oldValue(ctx) + if err != nil { + return v, fmt.Errorf("querying old value for OldLastUsed: %w", err) + } + return oldValue.LastUsed, nil +} + +// ClearLastUsed clears the value of the "last_used" field. +func (m *ApiKeyMutation) ClearLastUsed() { + m.last_used = nil + m.clearedFields[apikey.FieldLastUsed] = struct{}{} +} + +// LastUsedCleared returns if the "last_used" field was cleared in this mutation. +func (m *ApiKeyMutation) LastUsedCleared() bool { + _, ok := m.clearedFields[apikey.FieldLastUsed] + return ok +} + +// ResetLastUsed resets all changes to the "last_used" field. +func (m *ApiKeyMutation) ResetLastUsed() { + m.last_used = nil + delete(m.clearedFields, apikey.FieldLastUsed) +} + +// SetCreated sets the "created" field. +func (m *ApiKeyMutation) SetCreated(t time.Time) { + m.created = &t +} + +// Created returns the value of the "created" field in the mutation. +func (m *ApiKeyMutation) Created() (r time.Time, exists bool) { + v := m.created + if v == nil { + return + } + return *v, true +} + +// OldCreated returns the old "created" field's value of the ApiKey entity. +// If the ApiKey object wasn't provided to the builder, the object is fetched from the database. +// An error is returned if the mutation operation is not UpdateOne, or the database query fails. +func (m *ApiKeyMutation) OldCreated(ctx context.Context) (v time.Time, err error) { + if !m.op.Is(OpUpdateOne) { + return v, errors.New("OldCreated is only allowed on UpdateOne operations") + } + if m.id == nil || m.oldValue == nil { + return v, errors.New("OldCreated requires an ID field in the mutation") + } + oldValue, err := m.oldValue(ctx) + if err != nil { + return v, fmt.Errorf("querying old value for OldCreated: %w", err) + } + return oldValue.Created, nil +} + +// ResetCreated resets all changes to the "created" field. +func (m *ApiKeyMutation) ResetCreated() { + m.created = nil +} + +// Where appends a list predicates to the ApiKeyMutation builder. +func (m *ApiKeyMutation) Where(ps ...predicate.ApiKey) { + m.predicates = append(m.predicates, ps...) +} + +// WhereP appends storage-level predicates to the ApiKeyMutation builder. Using this method, +// users can use type-assertion to append predicates that do not depend on any generated package. +func (m *ApiKeyMutation) WhereP(ps ...func(*sql.Selector)) { + p := make([]predicate.ApiKey, len(ps)) + for i := range ps { + p[i] = ps[i] + } + m.Where(p...) +} + +// Op returns the operation name. +func (m *ApiKeyMutation) Op() Op { + return m.op +} + +// SetOp allows setting the mutation operation. +func (m *ApiKeyMutation) SetOp(op Op) { + m.op = op +} + +// Type returns the node type of this mutation (ApiKey). +func (m *ApiKeyMutation) Type() string { + return m.typ +} + +// Fields returns all fields that were changed during this mutation. Note that in +// order to get all numeric fields that were incremented/decremented, call +// AddedFields(). +func (m *ApiKeyMutation) Fields() []string { + fields := make([]string, 0, 9) + if m.user_id != nil { + fields = append(fields, apikey.FieldUserID) + } + if m.name != nil { + fields = append(fields, apikey.FieldName) + } + if m.prefix != nil { + fields = append(fields, apikey.FieldPrefix) + } + if m.key_hash != nil { + fields = append(fields, apikey.FieldKeyHash) + } + if m.scopes != nil { + fields = append(fields, apikey.FieldScopes) + } + if m.revoked != nil { + fields = append(fields, apikey.FieldRevoked) + } + if m.expires_at != nil { + fields = append(fields, apikey.FieldExpiresAt) + } + if m.last_used != nil { + fields = append(fields, apikey.FieldLastUsed) + } + if m.created != nil { + fields = append(fields, apikey.FieldCreated) + } + return fields +} + +// Field returns the value of a field with the given name. The second boolean +// return value indicates that this field was not set, or was not defined in the +// schema. +func (m *ApiKeyMutation) Field(name string) (ent.Value, bool) { + switch name { + case apikey.FieldUserID: + return m.UserID() + case apikey.FieldName: + return m.Name() + case apikey.FieldPrefix: + return m.Prefix() + case apikey.FieldKeyHash: + return m.KeyHash() + case apikey.FieldScopes: + return m.Scopes() + case apikey.FieldRevoked: + return m.Revoked() + case apikey.FieldExpiresAt: + return m.ExpiresAt() + case apikey.FieldLastUsed: + return m.LastUsed() + case apikey.FieldCreated: + return m.Created() + } + return nil, false +} + +// OldField returns the old value of the field from the database. An error is +// returned if the mutation operation is not UpdateOne, or the query to the +// database failed. +func (m *ApiKeyMutation) OldField(ctx context.Context, name string) (ent.Value, error) { + switch name { + case apikey.FieldUserID: + return m.OldUserID(ctx) + case apikey.FieldName: + return m.OldName(ctx) + case apikey.FieldPrefix: + return m.OldPrefix(ctx) + case apikey.FieldKeyHash: + return m.OldKeyHash(ctx) + case apikey.FieldScopes: + return m.OldScopes(ctx) + case apikey.FieldRevoked: + return m.OldRevoked(ctx) + case apikey.FieldExpiresAt: + return m.OldExpiresAt(ctx) + case apikey.FieldLastUsed: + return m.OldLastUsed(ctx) + case apikey.FieldCreated: + return m.OldCreated(ctx) + } + return nil, fmt.Errorf("unknown ApiKey field %s", name) +} + +// SetField sets the value of a field with the given name. It returns an error if +// the field is not defined in the schema, or if the type mismatched the field +// type. +func (m *ApiKeyMutation) SetField(name string, value ent.Value) error { + switch name { + case apikey.FieldUserID: + v, ok := value.(uuid.UUID) + if !ok { + return fmt.Errorf("unexpected type %T for field %s", value, name) + } + m.SetUserID(v) + return nil + case apikey.FieldName: + v, ok := value.(string) + if !ok { + return fmt.Errorf("unexpected type %T for field %s", value, name) + } + m.SetName(v) + return nil + case apikey.FieldPrefix: + v, ok := value.(string) + if !ok { + return fmt.Errorf("unexpected type %T for field %s", value, name) + } + m.SetPrefix(v) + return nil + case apikey.FieldKeyHash: + v, ok := value.(string) + if !ok { + return fmt.Errorf("unexpected type %T for field %s", value, name) + } + m.SetKeyHash(v) + return nil + case apikey.FieldScopes: + v, ok := value.(string) + if !ok { + return fmt.Errorf("unexpected type %T for field %s", value, name) + } + m.SetScopes(v) + return nil + case apikey.FieldRevoked: + v, ok := value.(bool) + if !ok { + return fmt.Errorf("unexpected type %T for field %s", value, name) + } + m.SetRevoked(v) + return nil + case apikey.FieldExpiresAt: + v, ok := value.(time.Time) + if !ok { + return fmt.Errorf("unexpected type %T for field %s", value, name) + } + m.SetExpiresAt(v) + return nil + case apikey.FieldLastUsed: + v, ok := value.(time.Time) + if !ok { + return fmt.Errorf("unexpected type %T for field %s", value, name) + } + m.SetLastUsed(v) + return nil + case apikey.FieldCreated: + v, ok := value.(time.Time) + if !ok { + return fmt.Errorf("unexpected type %T for field %s", value, name) + } + m.SetCreated(v) + return nil + } + return fmt.Errorf("unknown ApiKey field %s", name) +} + +// AddedFields returns all numeric fields that were incremented/decremented during +// this mutation. +func (m *ApiKeyMutation) AddedFields() []string { + return nil +} + +// AddedField returns the numeric value that was incremented/decremented on a field +// with the given name. The second boolean return value indicates that this field +// was not set, or was not defined in the schema. +func (m *ApiKeyMutation) AddedField(name string) (ent.Value, bool) { + return nil, false +} + +// AddField adds the value to the field with the given name. It returns an error if +// the field is not defined in the schema, or if the type mismatched the field +// type. +func (m *ApiKeyMutation) AddField(name string, value ent.Value) error { + switch name { + } + return fmt.Errorf("unknown ApiKey numeric field %s", name) +} + +// ClearedFields returns all nullable fields that were cleared during this +// mutation. +func (m *ApiKeyMutation) ClearedFields() []string { + var fields []string + if m.FieldCleared(apikey.FieldName) { + fields = append(fields, apikey.FieldName) + } + if m.FieldCleared(apikey.FieldPrefix) { + fields = append(fields, apikey.FieldPrefix) + } + if m.FieldCleared(apikey.FieldScopes) { + fields = append(fields, apikey.FieldScopes) + } + if m.FieldCleared(apikey.FieldExpiresAt) { + fields = append(fields, apikey.FieldExpiresAt) + } + if m.FieldCleared(apikey.FieldLastUsed) { + fields = append(fields, apikey.FieldLastUsed) + } + return fields +} + +// FieldCleared returns a boolean indicating if a field with the given name was +// cleared in this mutation. +func (m *ApiKeyMutation) FieldCleared(name string) bool { + _, ok := m.clearedFields[name] + return ok +} + +// ClearField clears the value of the field with the given name. It returns an +// error if the field is not defined in the schema. +func (m *ApiKeyMutation) ClearField(name string) error { + switch name { + case apikey.FieldName: + m.ClearName() + return nil + case apikey.FieldPrefix: + m.ClearPrefix() + return nil + case apikey.FieldScopes: + m.ClearScopes() + return nil + case apikey.FieldExpiresAt: + m.ClearExpiresAt() + return nil + case apikey.FieldLastUsed: + m.ClearLastUsed() + return nil + } + return fmt.Errorf("unknown ApiKey nullable field %s", name) +} + +// ResetField resets all changes in the mutation for the field with the given name. +// It returns an error if the field is not defined in the schema. +func (m *ApiKeyMutation) ResetField(name string) error { + switch name { + case apikey.FieldUserID: + m.ResetUserID() + return nil + case apikey.FieldName: + m.ResetName() + return nil + case apikey.FieldPrefix: + m.ResetPrefix() + return nil + case apikey.FieldKeyHash: + m.ResetKeyHash() + return nil + case apikey.FieldScopes: + m.ResetScopes() + return nil + case apikey.FieldRevoked: + m.ResetRevoked() + return nil + case apikey.FieldExpiresAt: + m.ResetExpiresAt() + return nil + case apikey.FieldLastUsed: + m.ResetLastUsed() + return nil + case apikey.FieldCreated: + m.ResetCreated() + return nil + } + return fmt.Errorf("unknown ApiKey field %s", name) +} + +// AddedEdges returns all edge names that were set/added in this mutation. +func (m *ApiKeyMutation) AddedEdges() []string { + edges := make([]string, 0, 0) + return edges +} + +// AddedIDs returns all IDs (to other nodes) that were added for the given edge +// name in this mutation. +func (m *ApiKeyMutation) AddedIDs(name string) []ent.Value { + return nil +} + +// RemovedEdges returns all edge names that were removed in this mutation. +func (m *ApiKeyMutation) RemovedEdges() []string { + edges := make([]string, 0, 0) + return edges +} + +// RemovedIDs returns all IDs (to other nodes) that were removed for the edge with +// the given name in this mutation. +func (m *ApiKeyMutation) RemovedIDs(name string) []ent.Value { + return nil +} + +// ClearedEdges returns all edge names that were cleared in this mutation. +func (m *ApiKeyMutation) ClearedEdges() []string { + edges := make([]string, 0, 0) + return edges +} + +// EdgeCleared returns a boolean which indicates if the edge with the given name +// was cleared in this mutation. +func (m *ApiKeyMutation) EdgeCleared(name string) bool { + return false +} + +// ClearEdge clears the value of the edge with the given name. It returns an error +// if that edge is not defined in the schema. +func (m *ApiKeyMutation) ClearEdge(name string) error { + return fmt.Errorf("unknown ApiKey unique edge %s", name) +} + +// ResetEdge resets all changes to the edge with the given name in this mutation. +// It returns an error if the edge is not defined in the schema. +func (m *ApiKeyMutation) ResetEdge(name string) error { + return fmt.Errorf("unknown ApiKey edge %s", name) +} + +// BrokerDispatchMutation represents an operation that mutates the BrokerDispatch nodes in the graph. +type BrokerDispatchMutation struct { + config + op Op + typ string + id *uuid.UUID + broker_id *uuid.UUID + agent_id *uuid.UUID + agent_slug *string + project_id *uuid.UUID + _op *string + args *string + state *string + result *string + claimed_by *string + attempts *int + addattempts *int + error *string + created_at *time.Time + updated_at *time.Time + deadline_at *time.Time + clearedFields map[string]struct{} + done bool + oldValue func(context.Context) (*BrokerDispatch, error) + predicates []predicate.BrokerDispatch +} + +var _ ent.Mutation = (*BrokerDispatchMutation)(nil) + +// brokerdispatchOption allows management of the mutation configuration using functional options. +type brokerdispatchOption func(*BrokerDispatchMutation) + +// newBrokerDispatchMutation creates new mutation for the BrokerDispatch entity. +func newBrokerDispatchMutation(c config, op Op, opts ...brokerdispatchOption) *BrokerDispatchMutation { + m := &BrokerDispatchMutation{ + config: c, + op: op, + typ: TypeBrokerDispatch, + clearedFields: make(map[string]struct{}), + } + for _, opt := range opts { + opt(m) + } + return m +} + +// withBrokerDispatchID sets the ID field of the mutation. +func withBrokerDispatchID(id uuid.UUID) brokerdispatchOption { + return func(m *BrokerDispatchMutation) { + var ( + err error + once sync.Once + value *BrokerDispatch + ) + m.oldValue = func(ctx context.Context) (*BrokerDispatch, error) { + once.Do(func() { + if m.done { + err = errors.New("querying old values post mutation is not allowed") + } else { + value, err = m.Client().BrokerDispatch.Get(ctx, id) + } + }) + return value, err + } + m.id = &id + } +} + +// withBrokerDispatch sets the old BrokerDispatch of the mutation. +func withBrokerDispatch(node *BrokerDispatch) brokerdispatchOption { + return func(m *BrokerDispatchMutation) { + m.oldValue = func(context.Context) (*BrokerDispatch, error) { + return node, nil + } + m.id = &node.ID + } +} + +// Client returns a new `ent.Client` from the mutation. If the mutation was +// executed in a transaction (ent.Tx), a transactional client is returned. +func (m BrokerDispatchMutation) Client() *Client { + client := &Client{config: m.config} + client.init() + return client +} + +// Tx returns an `ent.Tx` for mutations that were executed in transactions; +// it returns an error otherwise. +func (m BrokerDispatchMutation) Tx() (*Tx, error) { + if _, ok := m.driver.(*txDriver); !ok { + return nil, errors.New("ent: mutation is not running in a transaction") + } + tx := &Tx{config: m.config} + tx.init() + return tx, nil +} + +// SetID sets the value of the id field. Note that this +// operation is only accepted on creation of BrokerDispatch entities. +func (m *BrokerDispatchMutation) SetID(id uuid.UUID) { + m.id = &id +} + +// ID returns the ID value in the mutation. Note that the ID is only available +// if it was provided to the builder or after it was returned from the database. +func (m *BrokerDispatchMutation) ID() (id uuid.UUID, exists bool) { + if m.id == nil { + return + } + return *m.id, true +} + +// IDs queries the database and returns the entity ids that match the mutation's predicate. +// That means, if the mutation is applied within a transaction with an isolation level such +// as sql.LevelSerializable, the returned ids match the ids of the rows that will be updated +// or updated by the mutation. +func (m *BrokerDispatchMutation) IDs(ctx context.Context) ([]uuid.UUID, error) { + switch { + case m.op.Is(OpUpdateOne | OpDeleteOne): + id, exists := m.ID() + if exists { + return []uuid.UUID{id}, nil + } + fallthrough + case m.op.Is(OpUpdate | OpDelete): + return m.Client().BrokerDispatch.Query().Where(m.predicates...).IDs(ctx) + default: + return nil, fmt.Errorf("IDs is not allowed on %s operations", m.op) + } +} + +// SetBrokerID sets the "broker_id" field. +func (m *BrokerDispatchMutation) SetBrokerID(u uuid.UUID) { + m.broker_id = &u +} + +// BrokerID returns the value of the "broker_id" field in the mutation. +func (m *BrokerDispatchMutation) BrokerID() (r uuid.UUID, exists bool) { + v := m.broker_id + if v == nil { + return + } + return *v, true +} + +// OldBrokerID returns the old "broker_id" field's value of the BrokerDispatch entity. +// If the BrokerDispatch object wasn't provided to the builder, the object is fetched from the database. +// An error is returned if the mutation operation is not UpdateOne, or the database query fails. +func (m *BrokerDispatchMutation) OldBrokerID(ctx context.Context) (v uuid.UUID, err error) { + if !m.op.Is(OpUpdateOne) { + return v, errors.New("OldBrokerID is only allowed on UpdateOne operations") + } + if m.id == nil || m.oldValue == nil { + return v, errors.New("OldBrokerID requires an ID field in the mutation") + } + oldValue, err := m.oldValue(ctx) + if err != nil { + return v, fmt.Errorf("querying old value for OldBrokerID: %w", err) + } + return oldValue.BrokerID, nil +} + +// ResetBrokerID resets all changes to the "broker_id" field. +func (m *BrokerDispatchMutation) ResetBrokerID() { + m.broker_id = nil +} + +// SetAgentID sets the "agent_id" field. +func (m *BrokerDispatchMutation) SetAgentID(u uuid.UUID) { + m.agent_id = &u +} + +// AgentID returns the value of the "agent_id" field in the mutation. +func (m *BrokerDispatchMutation) AgentID() (r uuid.UUID, exists bool) { + v := m.agent_id + if v == nil { + return + } + return *v, true +} + +// OldAgentID returns the old "agent_id" field's value of the BrokerDispatch entity. +// If the BrokerDispatch object wasn't provided to the builder, the object is fetched from the database. +// An error is returned if the mutation operation is not UpdateOne, or the database query fails. +func (m *BrokerDispatchMutation) OldAgentID(ctx context.Context) (v *uuid.UUID, err error) { + if !m.op.Is(OpUpdateOne) { + return v, errors.New("OldAgentID is only allowed on UpdateOne operations") + } + if m.id == nil || m.oldValue == nil { + return v, errors.New("OldAgentID requires an ID field in the mutation") + } + oldValue, err := m.oldValue(ctx) + if err != nil { + return v, fmt.Errorf("querying old value for OldAgentID: %w", err) + } + return oldValue.AgentID, nil +} + +// ClearAgentID clears the value of the "agent_id" field. +func (m *BrokerDispatchMutation) ClearAgentID() { + m.agent_id = nil + m.clearedFields[brokerdispatch.FieldAgentID] = struct{}{} +} + +// AgentIDCleared returns if the "agent_id" field was cleared in this mutation. +func (m *BrokerDispatchMutation) AgentIDCleared() bool { + _, ok := m.clearedFields[brokerdispatch.FieldAgentID] + return ok +} + +// ResetAgentID resets all changes to the "agent_id" field. +func (m *BrokerDispatchMutation) ResetAgentID() { + m.agent_id = nil + delete(m.clearedFields, brokerdispatch.FieldAgentID) +} + +// SetAgentSlug sets the "agent_slug" field. +func (m *BrokerDispatchMutation) SetAgentSlug(s string) { + m.agent_slug = &s +} + +// AgentSlug returns the value of the "agent_slug" field in the mutation. +func (m *BrokerDispatchMutation) AgentSlug() (r string, exists bool) { + v := m.agent_slug + if v == nil { + return + } + return *v, true +} + +// OldAgentSlug returns the old "agent_slug" field's value of the BrokerDispatch entity. +// If the BrokerDispatch object wasn't provided to the builder, the object is fetched from the database. +// An error is returned if the mutation operation is not UpdateOne, or the database query fails. +func (m *BrokerDispatchMutation) OldAgentSlug(ctx context.Context) (v string, err error) { + if !m.op.Is(OpUpdateOne) { + return v, errors.New("OldAgentSlug is only allowed on UpdateOne operations") + } + if m.id == nil || m.oldValue == nil { + return v, errors.New("OldAgentSlug requires an ID field in the mutation") + } + oldValue, err := m.oldValue(ctx) + if err != nil { + return v, fmt.Errorf("querying old value for OldAgentSlug: %w", err) + } + return oldValue.AgentSlug, nil +} + +// ClearAgentSlug clears the value of the "agent_slug" field. +func (m *BrokerDispatchMutation) ClearAgentSlug() { + m.agent_slug = nil + m.clearedFields[brokerdispatch.FieldAgentSlug] = struct{}{} +} + +// AgentSlugCleared returns if the "agent_slug" field was cleared in this mutation. +func (m *BrokerDispatchMutation) AgentSlugCleared() bool { + _, ok := m.clearedFields[brokerdispatch.FieldAgentSlug] + return ok +} + +// ResetAgentSlug resets all changes to the "agent_slug" field. +func (m *BrokerDispatchMutation) ResetAgentSlug() { + m.agent_slug = nil + delete(m.clearedFields, brokerdispatch.FieldAgentSlug) +} + +// SetProjectID sets the "project_id" field. +func (m *BrokerDispatchMutation) SetProjectID(u uuid.UUID) { + m.project_id = &u +} + +// ProjectID returns the value of the "project_id" field in the mutation. +func (m *BrokerDispatchMutation) ProjectID() (r uuid.UUID, exists bool) { + v := m.project_id + if v == nil { + return + } + return *v, true +} + +// OldProjectID returns the old "project_id" field's value of the BrokerDispatch entity. +// If the BrokerDispatch object wasn't provided to the builder, the object is fetched from the database. +// An error is returned if the mutation operation is not UpdateOne, or the database query fails. +func (m *BrokerDispatchMutation) OldProjectID(ctx context.Context) (v *uuid.UUID, err error) { + if !m.op.Is(OpUpdateOne) { + return v, errors.New("OldProjectID is only allowed on UpdateOne operations") + } + if m.id == nil || m.oldValue == nil { + return v, errors.New("OldProjectID requires an ID field in the mutation") + } + oldValue, err := m.oldValue(ctx) + if err != nil { + return v, fmt.Errorf("querying old value for OldProjectID: %w", err) + } + return oldValue.ProjectID, nil +} + +// ClearProjectID clears the value of the "project_id" field. +func (m *BrokerDispatchMutation) ClearProjectID() { + m.project_id = nil + m.clearedFields[brokerdispatch.FieldProjectID] = struct{}{} +} + +// ProjectIDCleared returns if the "project_id" field was cleared in this mutation. +func (m *BrokerDispatchMutation) ProjectIDCleared() bool { + _, ok := m.clearedFields[brokerdispatch.FieldProjectID] + return ok +} + +// ResetProjectID resets all changes to the "project_id" field. +func (m *BrokerDispatchMutation) ResetProjectID() { + m.project_id = nil + delete(m.clearedFields, brokerdispatch.FieldProjectID) +} + +// SetOpField sets the "op" field. +func (m *BrokerDispatchMutation) SetOpField(s string) { + m._op = &s +} + +// GetOp returns the value of the "op" field in the mutation. +func (m *BrokerDispatchMutation) GetOp() (r string, exists bool) { + v := m._op + if v == nil { + return + } + return *v, true +} + +// OldOp returns the old "op" field's value of the BrokerDispatch entity. +// If the BrokerDispatch object wasn't provided to the builder, the object is fetched from the database. +// An error is returned if the mutation operation is not UpdateOne, or the database query fails. +func (m *BrokerDispatchMutation) OldOp(ctx context.Context) (v string, err error) { + if !m.op.Is(OpUpdateOne) { + return v, errors.New("OldOp is only allowed on UpdateOne operations") + } + if m.id == nil || m.oldValue == nil { + return v, errors.New("OldOp requires an ID field in the mutation") + } + oldValue, err := m.oldValue(ctx) + if err != nil { + return v, fmt.Errorf("querying old value for OldOp: %w", err) + } + return oldValue.Op, nil +} + +// ResetOp resets all changes to the "op" field. +func (m *BrokerDispatchMutation) ResetOp() { + m._op = nil +} + +// SetArgs sets the "args" field. +func (m *BrokerDispatchMutation) SetArgs(s string) { + m.args = &s +} + +// Args returns the value of the "args" field in the mutation. +func (m *BrokerDispatchMutation) Args() (r string, exists bool) { + v := m.args + if v == nil { + return + } + return *v, true +} + +// OldArgs returns the old "args" field's value of the BrokerDispatch entity. +// If the BrokerDispatch object wasn't provided to the builder, the object is fetched from the database. +// An error is returned if the mutation operation is not UpdateOne, or the database query fails. +func (m *BrokerDispatchMutation) OldArgs(ctx context.Context) (v string, err error) { + if !m.op.Is(OpUpdateOne) { + return v, errors.New("OldArgs is only allowed on UpdateOne operations") + } + if m.id == nil || m.oldValue == nil { + return v, errors.New("OldArgs requires an ID field in the mutation") + } + oldValue, err := m.oldValue(ctx) + if err != nil { + return v, fmt.Errorf("querying old value for OldArgs: %w", err) + } + return oldValue.Args, nil +} + +// ClearArgs clears the value of the "args" field. +func (m *BrokerDispatchMutation) ClearArgs() { + m.args = nil + m.clearedFields[brokerdispatch.FieldArgs] = struct{}{} +} + +// ArgsCleared returns if the "args" field was cleared in this mutation. +func (m *BrokerDispatchMutation) ArgsCleared() bool { + _, ok := m.clearedFields[brokerdispatch.FieldArgs] + return ok +} + +// ResetArgs resets all changes to the "args" field. +func (m *BrokerDispatchMutation) ResetArgs() { + m.args = nil + delete(m.clearedFields, brokerdispatch.FieldArgs) +} + +// SetState sets the "state" field. +func (m *BrokerDispatchMutation) SetState(s string) { + m.state = &s +} + +// State returns the value of the "state" field in the mutation. +func (m *BrokerDispatchMutation) State() (r string, exists bool) { + v := m.state + if v == nil { + return + } + return *v, true +} + +// OldState returns the old "state" field's value of the BrokerDispatch entity. +// If the BrokerDispatch object wasn't provided to the builder, the object is fetched from the database. +// An error is returned if the mutation operation is not UpdateOne, or the database query fails. +func (m *BrokerDispatchMutation) OldState(ctx context.Context) (v string, err error) { + if !m.op.Is(OpUpdateOne) { + return v, errors.New("OldState is only allowed on UpdateOne operations") + } + if m.id == nil || m.oldValue == nil { + return v, errors.New("OldState requires an ID field in the mutation") + } + oldValue, err := m.oldValue(ctx) + if err != nil { + return v, fmt.Errorf("querying old value for OldState: %w", err) + } + return oldValue.State, nil +} + +// ResetState resets all changes to the "state" field. +func (m *BrokerDispatchMutation) ResetState() { + m.state = nil +} + +// SetResult sets the "result" field. +func (m *BrokerDispatchMutation) SetResult(s string) { + m.result = &s +} + +// Result returns the value of the "result" field in the mutation. +func (m *BrokerDispatchMutation) Result() (r string, exists bool) { + v := m.result + if v == nil { + return + } + return *v, true +} + +// OldResult returns the old "result" field's value of the BrokerDispatch entity. +// If the BrokerDispatch object wasn't provided to the builder, the object is fetched from the database. +// An error is returned if the mutation operation is not UpdateOne, or the database query fails. +func (m *BrokerDispatchMutation) OldResult(ctx context.Context) (v string, err error) { + if !m.op.Is(OpUpdateOne) { + return v, errors.New("OldResult is only allowed on UpdateOne operations") + } + if m.id == nil || m.oldValue == nil { + return v, errors.New("OldResult requires an ID field in the mutation") + } + oldValue, err := m.oldValue(ctx) + if err != nil { + return v, fmt.Errorf("querying old value for OldResult: %w", err) + } + return oldValue.Result, nil +} + +// ClearResult clears the value of the "result" field. +func (m *BrokerDispatchMutation) ClearResult() { + m.result = nil + m.clearedFields[brokerdispatch.FieldResult] = struct{}{} +} + +// ResultCleared returns if the "result" field was cleared in this mutation. +func (m *BrokerDispatchMutation) ResultCleared() bool { + _, ok := m.clearedFields[brokerdispatch.FieldResult] + return ok +} + +// ResetResult resets all changes to the "result" field. +func (m *BrokerDispatchMutation) ResetResult() { + m.result = nil + delete(m.clearedFields, brokerdispatch.FieldResult) +} + +// SetClaimedBy sets the "claimed_by" field. +func (m *BrokerDispatchMutation) SetClaimedBy(s string) { + m.claimed_by = &s +} + +// ClaimedBy returns the value of the "claimed_by" field in the mutation. +func (m *BrokerDispatchMutation) ClaimedBy() (r string, exists bool) { + v := m.claimed_by + if v == nil { + return + } + return *v, true +} + +// OldClaimedBy returns the old "claimed_by" field's value of the BrokerDispatch entity. +// If the BrokerDispatch object wasn't provided to the builder, the object is fetched from the database. +// An error is returned if the mutation operation is not UpdateOne, or the database query fails. +func (m *BrokerDispatchMutation) OldClaimedBy(ctx context.Context) (v string, err error) { + if !m.op.Is(OpUpdateOne) { + return v, errors.New("OldClaimedBy is only allowed on UpdateOne operations") + } + if m.id == nil || m.oldValue == nil { + return v, errors.New("OldClaimedBy requires an ID field in the mutation") + } + oldValue, err := m.oldValue(ctx) + if err != nil { + return v, fmt.Errorf("querying old value for OldClaimedBy: %w", err) + } + return oldValue.ClaimedBy, nil +} + +// ClearClaimedBy clears the value of the "claimed_by" field. +func (m *BrokerDispatchMutation) ClearClaimedBy() { + m.claimed_by = nil + m.clearedFields[brokerdispatch.FieldClaimedBy] = struct{}{} +} + +// ClaimedByCleared returns if the "claimed_by" field was cleared in this mutation. +func (m *BrokerDispatchMutation) ClaimedByCleared() bool { + _, ok := m.clearedFields[brokerdispatch.FieldClaimedBy] + return ok +} + +// ResetClaimedBy resets all changes to the "claimed_by" field. +func (m *BrokerDispatchMutation) ResetClaimedBy() { + m.claimed_by = nil + delete(m.clearedFields, brokerdispatch.FieldClaimedBy) +} + +// SetAttempts sets the "attempts" field. +func (m *BrokerDispatchMutation) SetAttempts(i int) { + m.attempts = &i + m.addattempts = nil +} + +// Attempts returns the value of the "attempts" field in the mutation. +func (m *BrokerDispatchMutation) Attempts() (r int, exists bool) { + v := m.attempts + if v == nil { + return + } + return *v, true +} + +// OldAttempts returns the old "attempts" field's value of the BrokerDispatch entity. +// If the BrokerDispatch object wasn't provided to the builder, the object is fetched from the database. +// An error is returned if the mutation operation is not UpdateOne, or the database query fails. +func (m *BrokerDispatchMutation) OldAttempts(ctx context.Context) (v int, err error) { + if !m.op.Is(OpUpdateOne) { + return v, errors.New("OldAttempts is only allowed on UpdateOne operations") + } + if m.id == nil || m.oldValue == nil { + return v, errors.New("OldAttempts requires an ID field in the mutation") + } + oldValue, err := m.oldValue(ctx) + if err != nil { + return v, fmt.Errorf("querying old value for OldAttempts: %w", err) + } + return oldValue.Attempts, nil +} + +// AddAttempts adds i to the "attempts" field. +func (m *BrokerDispatchMutation) AddAttempts(i int) { + if m.addattempts != nil { + *m.addattempts += i + } else { + m.addattempts = &i + } +} + +// AddedAttempts returns the value that was added to the "attempts" field in this mutation. +func (m *BrokerDispatchMutation) AddedAttempts() (r int, exists bool) { + v := m.addattempts + if v == nil { + return + } + return *v, true +} + +// ResetAttempts resets all changes to the "attempts" field. +func (m *BrokerDispatchMutation) ResetAttempts() { + m.attempts = nil + m.addattempts = nil +} + +// SetError sets the "error" field. +func (m *BrokerDispatchMutation) SetError(s string) { + m.error = &s +} + +// Error returns the value of the "error" field in the mutation. +func (m *BrokerDispatchMutation) Error() (r string, exists bool) { + v := m.error + if v == nil { + return + } + return *v, true +} + +// OldError returns the old "error" field's value of the BrokerDispatch entity. +// If the BrokerDispatch object wasn't provided to the builder, the object is fetched from the database. +// An error is returned if the mutation operation is not UpdateOne, or the database query fails. +func (m *BrokerDispatchMutation) OldError(ctx context.Context) (v string, err error) { + if !m.op.Is(OpUpdateOne) { + return v, errors.New("OldError is only allowed on UpdateOne operations") + } + if m.id == nil || m.oldValue == nil { + return v, errors.New("OldError requires an ID field in the mutation") + } + oldValue, err := m.oldValue(ctx) + if err != nil { + return v, fmt.Errorf("querying old value for OldError: %w", err) + } + return oldValue.Error, nil +} + +// ClearError clears the value of the "error" field. +func (m *BrokerDispatchMutation) ClearError() { + m.error = nil + m.clearedFields[brokerdispatch.FieldError] = struct{}{} +} + +// ErrorCleared returns if the "error" field was cleared in this mutation. +func (m *BrokerDispatchMutation) ErrorCleared() bool { + _, ok := m.clearedFields[brokerdispatch.FieldError] + return ok +} + +// ResetError resets all changes to the "error" field. +func (m *BrokerDispatchMutation) ResetError() { + m.error = nil + delete(m.clearedFields, brokerdispatch.FieldError) +} + +// SetCreatedAt sets the "created_at" field. +func (m *BrokerDispatchMutation) SetCreatedAt(t time.Time) { + m.created_at = &t +} + +// CreatedAt returns the value of the "created_at" field in the mutation. +func (m *BrokerDispatchMutation) CreatedAt() (r time.Time, exists bool) { + v := m.created_at + if v == nil { + return + } + return *v, true +} + +// OldCreatedAt returns the old "created_at" field's value of the BrokerDispatch entity. +// If the BrokerDispatch object wasn't provided to the builder, the object is fetched from the database. +// An error is returned if the mutation operation is not UpdateOne, or the database query fails. +func (m *BrokerDispatchMutation) OldCreatedAt(ctx context.Context) (v time.Time, err error) { + if !m.op.Is(OpUpdateOne) { + return v, errors.New("OldCreatedAt is only allowed on UpdateOne operations") + } + if m.id == nil || m.oldValue == nil { + return v, errors.New("OldCreatedAt requires an ID field in the mutation") + } + oldValue, err := m.oldValue(ctx) + if err != nil { + return v, fmt.Errorf("querying old value for OldCreatedAt: %w", err) + } + return oldValue.CreatedAt, nil +} + +// ResetCreatedAt resets all changes to the "created_at" field. +func (m *BrokerDispatchMutation) ResetCreatedAt() { + m.created_at = nil +} + +// SetUpdatedAt sets the "updated_at" field. +func (m *BrokerDispatchMutation) SetUpdatedAt(t time.Time) { + m.updated_at = &t +} + +// UpdatedAt returns the value of the "updated_at" field in the mutation. +func (m *BrokerDispatchMutation) UpdatedAt() (r time.Time, exists bool) { + v := m.updated_at + if v == nil { + return + } + return *v, true +} + +// OldUpdatedAt returns the old "updated_at" field's value of the BrokerDispatch entity. +// If the BrokerDispatch object wasn't provided to the builder, the object is fetched from the database. +// An error is returned if the mutation operation is not UpdateOne, or the database query fails. +func (m *BrokerDispatchMutation) OldUpdatedAt(ctx context.Context) (v time.Time, err error) { + if !m.op.Is(OpUpdateOne) { + return v, errors.New("OldUpdatedAt is only allowed on UpdateOne operations") + } + if m.id == nil || m.oldValue == nil { + return v, errors.New("OldUpdatedAt requires an ID field in the mutation") + } + oldValue, err := m.oldValue(ctx) + if err != nil { + return v, fmt.Errorf("querying old value for OldUpdatedAt: %w", err) + } + return oldValue.UpdatedAt, nil +} + +// ResetUpdatedAt resets all changes to the "updated_at" field. +func (m *BrokerDispatchMutation) ResetUpdatedAt() { + m.updated_at = nil +} + +// SetDeadlineAt sets the "deadline_at" field. +func (m *BrokerDispatchMutation) SetDeadlineAt(t time.Time) { + m.deadline_at = &t +} + +// DeadlineAt returns the value of the "deadline_at" field in the mutation. +func (m *BrokerDispatchMutation) DeadlineAt() (r time.Time, exists bool) { + v := m.deadline_at + if v == nil { + return + } + return *v, true +} + +// OldDeadlineAt returns the old "deadline_at" field's value of the BrokerDispatch entity. +// If the BrokerDispatch object wasn't provided to the builder, the object is fetched from the database. +// An error is returned if the mutation operation is not UpdateOne, or the database query fails. +func (m *BrokerDispatchMutation) OldDeadlineAt(ctx context.Context) (v *time.Time, err error) { + if !m.op.Is(OpUpdateOne) { + return v, errors.New("OldDeadlineAt is only allowed on UpdateOne operations") + } + if m.id == nil || m.oldValue == nil { + return v, errors.New("OldDeadlineAt requires an ID field in the mutation") + } + oldValue, err := m.oldValue(ctx) + if err != nil { + return v, fmt.Errorf("querying old value for OldDeadlineAt: %w", err) + } + return oldValue.DeadlineAt, nil +} + +// ClearDeadlineAt clears the value of the "deadline_at" field. +func (m *BrokerDispatchMutation) ClearDeadlineAt() { + m.deadline_at = nil + m.clearedFields[brokerdispatch.FieldDeadlineAt] = struct{}{} +} + +// DeadlineAtCleared returns if the "deadline_at" field was cleared in this mutation. +func (m *BrokerDispatchMutation) DeadlineAtCleared() bool { + _, ok := m.clearedFields[brokerdispatch.FieldDeadlineAt] + return ok +} + +// ResetDeadlineAt resets all changes to the "deadline_at" field. +func (m *BrokerDispatchMutation) ResetDeadlineAt() { + m.deadline_at = nil + delete(m.clearedFields, brokerdispatch.FieldDeadlineAt) +} + +// Where appends a list predicates to the BrokerDispatchMutation builder. +func (m *BrokerDispatchMutation) Where(ps ...predicate.BrokerDispatch) { + m.predicates = append(m.predicates, ps...) +} + +// WhereP appends storage-level predicates to the BrokerDispatchMutation builder. Using this method, +// users can use type-assertion to append predicates that do not depend on any generated package. +func (m *BrokerDispatchMutation) WhereP(ps ...func(*sql.Selector)) { + p := make([]predicate.BrokerDispatch, len(ps)) + for i := range ps { + p[i] = ps[i] + } + m.Where(p...) +} + +// Op returns the operation name. +func (m *BrokerDispatchMutation) Op() Op { + return m.op +} + +// SetOp allows setting the mutation operation. +func (m *BrokerDispatchMutation) SetOp(op Op) { + m.op = op +} + +// Type returns the node type of this mutation (BrokerDispatch). +func (m *BrokerDispatchMutation) Type() string { + return m.typ +} + +// Fields returns all fields that were changed during this mutation. Note that in +// order to get all numeric fields that were incremented/decremented, call +// AddedFields(). +func (m *BrokerDispatchMutation) Fields() []string { + fields := make([]string, 0, 14) + if m.broker_id != nil { + fields = append(fields, brokerdispatch.FieldBrokerID) + } + if m.agent_id != nil { + fields = append(fields, brokerdispatch.FieldAgentID) + } + if m.agent_slug != nil { + fields = append(fields, brokerdispatch.FieldAgentSlug) + } + if m.project_id != nil { + fields = append(fields, brokerdispatch.FieldProjectID) + } + if m._op != nil { + fields = append(fields, brokerdispatch.FieldOp) + } + if m.args != nil { + fields = append(fields, brokerdispatch.FieldArgs) + } + if m.state != nil { + fields = append(fields, brokerdispatch.FieldState) + } + if m.result != nil { + fields = append(fields, brokerdispatch.FieldResult) + } + if m.claimed_by != nil { + fields = append(fields, brokerdispatch.FieldClaimedBy) + } + if m.attempts != nil { + fields = append(fields, brokerdispatch.FieldAttempts) + } + if m.error != nil { + fields = append(fields, brokerdispatch.FieldError) + } + if m.created_at != nil { + fields = append(fields, brokerdispatch.FieldCreatedAt) + } + if m.updated_at != nil { + fields = append(fields, brokerdispatch.FieldUpdatedAt) + } + if m.deadline_at != nil { + fields = append(fields, brokerdispatch.FieldDeadlineAt) + } + return fields +} + +// Field returns the value of a field with the given name. The second boolean +// return value indicates that this field was not set, or was not defined in the +// schema. +func (m *BrokerDispatchMutation) Field(name string) (ent.Value, bool) { + switch name { + case brokerdispatch.FieldBrokerID: + return m.BrokerID() + case brokerdispatch.FieldAgentID: + return m.AgentID() + case brokerdispatch.FieldAgentSlug: + return m.AgentSlug() + case brokerdispatch.FieldProjectID: + return m.ProjectID() + case brokerdispatch.FieldOp: + return m.GetOp() + case brokerdispatch.FieldArgs: + return m.Args() + case brokerdispatch.FieldState: + return m.State() + case brokerdispatch.FieldResult: + return m.Result() + case brokerdispatch.FieldClaimedBy: + return m.ClaimedBy() + case brokerdispatch.FieldAttempts: + return m.Attempts() + case brokerdispatch.FieldError: + return m.Error() + case brokerdispatch.FieldCreatedAt: + return m.CreatedAt() + case brokerdispatch.FieldUpdatedAt: + return m.UpdatedAt() + case brokerdispatch.FieldDeadlineAt: + return m.DeadlineAt() + } + return nil, false +} + +// OldField returns the old value of the field from the database. An error is +// returned if the mutation operation is not UpdateOne, or the query to the +// database failed. +func (m *BrokerDispatchMutation) OldField(ctx context.Context, name string) (ent.Value, error) { + switch name { + case brokerdispatch.FieldBrokerID: + return m.OldBrokerID(ctx) + case brokerdispatch.FieldAgentID: + return m.OldAgentID(ctx) + case brokerdispatch.FieldAgentSlug: + return m.OldAgentSlug(ctx) + case brokerdispatch.FieldProjectID: + return m.OldProjectID(ctx) + case brokerdispatch.FieldOp: + return m.OldOp(ctx) + case brokerdispatch.FieldArgs: + return m.OldArgs(ctx) + case brokerdispatch.FieldState: + return m.OldState(ctx) + case brokerdispatch.FieldResult: + return m.OldResult(ctx) + case brokerdispatch.FieldClaimedBy: + return m.OldClaimedBy(ctx) + case brokerdispatch.FieldAttempts: + return m.OldAttempts(ctx) + case brokerdispatch.FieldError: + return m.OldError(ctx) + case brokerdispatch.FieldCreatedAt: + return m.OldCreatedAt(ctx) + case brokerdispatch.FieldUpdatedAt: + return m.OldUpdatedAt(ctx) + case brokerdispatch.FieldDeadlineAt: + return m.OldDeadlineAt(ctx) + } + return nil, fmt.Errorf("unknown BrokerDispatch field %s", name) +} + +// SetField sets the value of a field with the given name. It returns an error if +// the field is not defined in the schema, or if the type mismatched the field +// type. +func (m *BrokerDispatchMutation) SetField(name string, value ent.Value) error { + switch name { + case brokerdispatch.FieldBrokerID: + v, ok := value.(uuid.UUID) + if !ok { + return fmt.Errorf("unexpected type %T for field %s", value, name) + } + m.SetBrokerID(v) + return nil + case brokerdispatch.FieldAgentID: + v, ok := value.(uuid.UUID) + if !ok { + return fmt.Errorf("unexpected type %T for field %s", value, name) + } + m.SetAgentID(v) + return nil + case brokerdispatch.FieldAgentSlug: + v, ok := value.(string) + if !ok { + return fmt.Errorf("unexpected type %T for field %s", value, name) + } + m.SetAgentSlug(v) + return nil + case brokerdispatch.FieldProjectID: + v, ok := value.(uuid.UUID) + if !ok { + return fmt.Errorf("unexpected type %T for field %s", value, name) + } + m.SetProjectID(v) + return nil + case brokerdispatch.FieldOp: + v, ok := value.(string) + if !ok { + return fmt.Errorf("unexpected type %T for field %s", value, name) + } + m.SetOpField(v) + return nil + case brokerdispatch.FieldArgs: + v, ok := value.(string) + if !ok { + return fmt.Errorf("unexpected type %T for field %s", value, name) + } + m.SetArgs(v) + return nil + case brokerdispatch.FieldState: + v, ok := value.(string) + if !ok { + return fmt.Errorf("unexpected type %T for field %s", value, name) + } + m.SetState(v) + return nil + case brokerdispatch.FieldResult: + v, ok := value.(string) + if !ok { + return fmt.Errorf("unexpected type %T for field %s", value, name) + } + m.SetResult(v) + return nil + case brokerdispatch.FieldClaimedBy: + v, ok := value.(string) + if !ok { + return fmt.Errorf("unexpected type %T for field %s", value, name) + } + m.SetClaimedBy(v) + return nil + case brokerdispatch.FieldAttempts: + v, ok := value.(int) + if !ok { + return fmt.Errorf("unexpected type %T for field %s", value, name) + } + m.SetAttempts(v) + return nil + case brokerdispatch.FieldError: + v, ok := value.(string) + if !ok { + return fmt.Errorf("unexpected type %T for field %s", value, name) + } + m.SetError(v) + return nil + case brokerdispatch.FieldCreatedAt: + v, ok := value.(time.Time) + if !ok { + return fmt.Errorf("unexpected type %T for field %s", value, name) + } + m.SetCreatedAt(v) + return nil + case brokerdispatch.FieldUpdatedAt: + v, ok := value.(time.Time) + if !ok { + return fmt.Errorf("unexpected type %T for field %s", value, name) + } + m.SetUpdatedAt(v) + return nil + case brokerdispatch.FieldDeadlineAt: + v, ok := value.(time.Time) + if !ok { + return fmt.Errorf("unexpected type %T for field %s", value, name) + } + m.SetDeadlineAt(v) + return nil + } + return fmt.Errorf("unknown BrokerDispatch field %s", name) +} + +// AddedFields returns all numeric fields that were incremented/decremented during +// this mutation. +func (m *BrokerDispatchMutation) AddedFields() []string { + var fields []string + if m.addattempts != nil { + fields = append(fields, brokerdispatch.FieldAttempts) + } + return fields +} + +// AddedField returns the numeric value that was incremented/decremented on a field +// with the given name. The second boolean return value indicates that this field +// was not set, or was not defined in the schema. +func (m *BrokerDispatchMutation) AddedField(name string) (ent.Value, bool) { + switch name { + case brokerdispatch.FieldAttempts: + return m.AddedAttempts() + } + return nil, false +} + +// AddField adds the value to the field with the given name. It returns an error if +// the field is not defined in the schema, or if the type mismatched the field +// type. +func (m *BrokerDispatchMutation) AddField(name string, value ent.Value) error { + switch name { + case brokerdispatch.FieldAttempts: + v, ok := value.(int) + if !ok { + return fmt.Errorf("unexpected type %T for field %s", value, name) + } + m.AddAttempts(v) + return nil + } + return fmt.Errorf("unknown BrokerDispatch numeric field %s", name) +} + +// ClearedFields returns all nullable fields that were cleared during this +// mutation. +func (m *BrokerDispatchMutation) ClearedFields() []string { + var fields []string + if m.FieldCleared(brokerdispatch.FieldAgentID) { + fields = append(fields, brokerdispatch.FieldAgentID) + } + if m.FieldCleared(brokerdispatch.FieldAgentSlug) { + fields = append(fields, brokerdispatch.FieldAgentSlug) + } + if m.FieldCleared(brokerdispatch.FieldProjectID) { + fields = append(fields, brokerdispatch.FieldProjectID) + } + if m.FieldCleared(brokerdispatch.FieldArgs) { + fields = append(fields, brokerdispatch.FieldArgs) + } + if m.FieldCleared(brokerdispatch.FieldResult) { + fields = append(fields, brokerdispatch.FieldResult) + } + if m.FieldCleared(brokerdispatch.FieldClaimedBy) { + fields = append(fields, brokerdispatch.FieldClaimedBy) + } + if m.FieldCleared(brokerdispatch.FieldError) { + fields = append(fields, brokerdispatch.FieldError) + } + if m.FieldCleared(brokerdispatch.FieldDeadlineAt) { + fields = append(fields, brokerdispatch.FieldDeadlineAt) + } + return fields +} + +// FieldCleared returns a boolean indicating if a field with the given name was +// cleared in this mutation. +func (m *BrokerDispatchMutation) FieldCleared(name string) bool { + _, ok := m.clearedFields[name] + return ok +} + +// ClearField clears the value of the field with the given name. It returns an +// error if the field is not defined in the schema. +func (m *BrokerDispatchMutation) ClearField(name string) error { + switch name { + case brokerdispatch.FieldAgentID: + m.ClearAgentID() + return nil + case brokerdispatch.FieldAgentSlug: + m.ClearAgentSlug() + return nil + case brokerdispatch.FieldProjectID: + m.ClearProjectID() + return nil + case brokerdispatch.FieldArgs: + m.ClearArgs() + return nil + case brokerdispatch.FieldResult: + m.ClearResult() + return nil + case brokerdispatch.FieldClaimedBy: + m.ClearClaimedBy() + return nil + case brokerdispatch.FieldError: + m.ClearError() + return nil + case brokerdispatch.FieldDeadlineAt: + m.ClearDeadlineAt() + return nil + } + return fmt.Errorf("unknown BrokerDispatch nullable field %s", name) +} + +// ResetField resets all changes in the mutation for the field with the given name. +// It returns an error if the field is not defined in the schema. +func (m *BrokerDispatchMutation) ResetField(name string) error { + switch name { + case brokerdispatch.FieldBrokerID: + m.ResetBrokerID() + return nil + case brokerdispatch.FieldAgentID: + m.ResetAgentID() + return nil + case brokerdispatch.FieldAgentSlug: + m.ResetAgentSlug() + return nil + case brokerdispatch.FieldProjectID: + m.ResetProjectID() + return nil + case brokerdispatch.FieldOp: + m.ResetOp() + return nil + case brokerdispatch.FieldArgs: + m.ResetArgs() + return nil + case brokerdispatch.FieldState: + m.ResetState() + return nil + case brokerdispatch.FieldResult: + m.ResetResult() + return nil + case brokerdispatch.FieldClaimedBy: + m.ResetClaimedBy() + return nil + case brokerdispatch.FieldAttempts: + m.ResetAttempts() + return nil + case brokerdispatch.FieldError: + m.ResetError() + return nil + case brokerdispatch.FieldCreatedAt: + m.ResetCreatedAt() + return nil + case brokerdispatch.FieldUpdatedAt: + m.ResetUpdatedAt() + return nil + case brokerdispatch.FieldDeadlineAt: + m.ResetDeadlineAt() + return nil + } + return fmt.Errorf("unknown BrokerDispatch field %s", name) +} + +// AddedEdges returns all edge names that were set/added in this mutation. +func (m *BrokerDispatchMutation) AddedEdges() []string { + edges := make([]string, 0, 0) + return edges +} + +// AddedIDs returns all IDs (to other nodes) that were added for the given edge +// name in this mutation. +func (m *BrokerDispatchMutation) AddedIDs(name string) []ent.Value { + return nil +} + +// RemovedEdges returns all edge names that were removed in this mutation. +func (m *BrokerDispatchMutation) RemovedEdges() []string { + edges := make([]string, 0, 0) + return edges +} + +// RemovedIDs returns all IDs (to other nodes) that were removed for the edge with +// the given name in this mutation. +func (m *BrokerDispatchMutation) RemovedIDs(name string) []ent.Value { + return nil +} + +// ClearedEdges returns all edge names that were cleared in this mutation. +func (m *BrokerDispatchMutation) ClearedEdges() []string { + edges := make([]string, 0, 0) + return edges +} + +// EdgeCleared returns a boolean which indicates if the edge with the given name +// was cleared in this mutation. +func (m *BrokerDispatchMutation) EdgeCleared(name string) bool { + return false +} + +// ClearEdge clears the value of the edge with the given name. It returns an error +// if that edge is not defined in the schema. +func (m *BrokerDispatchMutation) ClearEdge(name string) error { + return fmt.Errorf("unknown BrokerDispatch unique edge %s", name) +} + +// ResetEdge resets all changes to the edge with the given name in this mutation. +// It returns an error if the edge is not defined in the schema. +func (m *BrokerDispatchMutation) ResetEdge(name string) error { + return fmt.Errorf("unknown BrokerDispatch edge %s", name) +} + +// BrokerJoinTokenMutation represents an operation that mutates the BrokerJoinToken nodes in the graph. +type BrokerJoinTokenMutation struct { + config + op Op + typ string + id *uuid.UUID + token_hash *string + expires_at *time.Time + created_by *string + created *time.Time + clearedFields map[string]struct{} + done bool + oldValue func(context.Context) (*BrokerJoinToken, error) + predicates []predicate.BrokerJoinToken +} + +var _ ent.Mutation = (*BrokerJoinTokenMutation)(nil) + +// brokerjointokenOption allows management of the mutation configuration using functional options. +type brokerjointokenOption func(*BrokerJoinTokenMutation) + +// newBrokerJoinTokenMutation creates new mutation for the BrokerJoinToken entity. +func newBrokerJoinTokenMutation(c config, op Op, opts ...brokerjointokenOption) *BrokerJoinTokenMutation { + m := &BrokerJoinTokenMutation{ + config: c, + op: op, + typ: TypeBrokerJoinToken, + clearedFields: make(map[string]struct{}), + } + for _, opt := range opts { + opt(m) + } + return m +} + +// withBrokerJoinTokenID sets the ID field of the mutation. +func withBrokerJoinTokenID(id uuid.UUID) brokerjointokenOption { + return func(m *BrokerJoinTokenMutation) { + var ( + err error + once sync.Once + value *BrokerJoinToken + ) + m.oldValue = func(ctx context.Context) (*BrokerJoinToken, error) { + once.Do(func() { + if m.done { + err = errors.New("querying old values post mutation is not allowed") + } else { + value, err = m.Client().BrokerJoinToken.Get(ctx, id) + } + }) + return value, err + } + m.id = &id + } +} + +// withBrokerJoinToken sets the old BrokerJoinToken of the mutation. +func withBrokerJoinToken(node *BrokerJoinToken) brokerjointokenOption { + return func(m *BrokerJoinTokenMutation) { + m.oldValue = func(context.Context) (*BrokerJoinToken, error) { + return node, nil + } + m.id = &node.ID + } +} + +// Client returns a new `ent.Client` from the mutation. If the mutation was +// executed in a transaction (ent.Tx), a transactional client is returned. +func (m BrokerJoinTokenMutation) Client() *Client { + client := &Client{config: m.config} + client.init() + return client +} + +// Tx returns an `ent.Tx` for mutations that were executed in transactions; +// it returns an error otherwise. +func (m BrokerJoinTokenMutation) Tx() (*Tx, error) { + if _, ok := m.driver.(*txDriver); !ok { + return nil, errors.New("ent: mutation is not running in a transaction") + } + tx := &Tx{config: m.config} + tx.init() + return tx, nil +} + +// SetID sets the value of the id field. Note that this +// operation is only accepted on creation of BrokerJoinToken entities. +func (m *BrokerJoinTokenMutation) SetID(id uuid.UUID) { + m.id = &id +} + +// ID returns the ID value in the mutation. Note that the ID is only available +// if it was provided to the builder or after it was returned from the database. +func (m *BrokerJoinTokenMutation) ID() (id uuid.UUID, exists bool) { + if m.id == nil { + return + } + return *m.id, true +} + +// IDs queries the database and returns the entity ids that match the mutation's predicate. +// That means, if the mutation is applied within a transaction with an isolation level such +// as sql.LevelSerializable, the returned ids match the ids of the rows that will be updated +// or updated by the mutation. +func (m *BrokerJoinTokenMutation) IDs(ctx context.Context) ([]uuid.UUID, error) { + switch { + case m.op.Is(OpUpdateOne | OpDeleteOne): + id, exists := m.ID() + if exists { + return []uuid.UUID{id}, nil + } + fallthrough + case m.op.Is(OpUpdate | OpDelete): + return m.Client().BrokerJoinToken.Query().Where(m.predicates...).IDs(ctx) + default: + return nil, fmt.Errorf("IDs is not allowed on %s operations", m.op) + } +} + +// SetTokenHash sets the "token_hash" field. +func (m *BrokerJoinTokenMutation) SetTokenHash(s string) { + m.token_hash = &s +} + +// TokenHash returns the value of the "token_hash" field in the mutation. +func (m *BrokerJoinTokenMutation) TokenHash() (r string, exists bool) { + v := m.token_hash + if v == nil { + return + } + return *v, true +} + +// OldTokenHash returns the old "token_hash" field's value of the BrokerJoinToken entity. +// If the BrokerJoinToken object wasn't provided to the builder, the object is fetched from the database. +// An error is returned if the mutation operation is not UpdateOne, or the database query fails. +func (m *BrokerJoinTokenMutation) OldTokenHash(ctx context.Context) (v string, err error) { + if !m.op.Is(OpUpdateOne) { + return v, errors.New("OldTokenHash is only allowed on UpdateOne operations") + } + if m.id == nil || m.oldValue == nil { + return v, errors.New("OldTokenHash requires an ID field in the mutation") + } + oldValue, err := m.oldValue(ctx) + if err != nil { + return v, fmt.Errorf("querying old value for OldTokenHash: %w", err) + } + return oldValue.TokenHash, nil +} + +// ResetTokenHash resets all changes to the "token_hash" field. +func (m *BrokerJoinTokenMutation) ResetTokenHash() { + m.token_hash = nil +} + +// SetExpiresAt sets the "expires_at" field. +func (m *BrokerJoinTokenMutation) SetExpiresAt(t time.Time) { + m.expires_at = &t +} + +// ExpiresAt returns the value of the "expires_at" field in the mutation. +func (m *BrokerJoinTokenMutation) ExpiresAt() (r time.Time, exists bool) { + v := m.expires_at + if v == nil { + return + } + return *v, true +} + +// OldExpiresAt returns the old "expires_at" field's value of the BrokerJoinToken entity. +// If the BrokerJoinToken object wasn't provided to the builder, the object is fetched from the database. +// An error is returned if the mutation operation is not UpdateOne, or the database query fails. +func (m *BrokerJoinTokenMutation) OldExpiresAt(ctx context.Context) (v time.Time, err error) { + if !m.op.Is(OpUpdateOne) { + return v, errors.New("OldExpiresAt is only allowed on UpdateOne operations") + } + if m.id == nil || m.oldValue == nil { + return v, errors.New("OldExpiresAt requires an ID field in the mutation") + } + oldValue, err := m.oldValue(ctx) + if err != nil { + return v, fmt.Errorf("querying old value for OldExpiresAt: %w", err) + } + return oldValue.ExpiresAt, nil +} + +// ResetExpiresAt resets all changes to the "expires_at" field. +func (m *BrokerJoinTokenMutation) ResetExpiresAt() { + m.expires_at = nil +} + +// SetCreatedBy sets the "created_by" field. +func (m *BrokerJoinTokenMutation) SetCreatedBy(s string) { + m.created_by = &s +} + +// CreatedBy returns the value of the "created_by" field in the mutation. +func (m *BrokerJoinTokenMutation) CreatedBy() (r string, exists bool) { + v := m.created_by + if v == nil { + return + } + return *v, true +} + +// OldCreatedBy returns the old "created_by" field's value of the BrokerJoinToken entity. +// If the BrokerJoinToken object wasn't provided to the builder, the object is fetched from the database. +// An error is returned if the mutation operation is not UpdateOne, or the database query fails. +func (m *BrokerJoinTokenMutation) OldCreatedBy(ctx context.Context) (v string, err error) { + if !m.op.Is(OpUpdateOne) { + return v, errors.New("OldCreatedBy is only allowed on UpdateOne operations") + } + if m.id == nil || m.oldValue == nil { + return v, errors.New("OldCreatedBy requires an ID field in the mutation") + } + oldValue, err := m.oldValue(ctx) + if err != nil { + return v, fmt.Errorf("querying old value for OldCreatedBy: %w", err) + } + return oldValue.CreatedBy, nil +} + +// ResetCreatedBy resets all changes to the "created_by" field. +func (m *BrokerJoinTokenMutation) ResetCreatedBy() { + m.created_by = nil +} + +// SetCreated sets the "created" field. +func (m *BrokerJoinTokenMutation) SetCreated(t time.Time) { + m.created = &t +} + +// Created returns the value of the "created" field in the mutation. +func (m *BrokerJoinTokenMutation) Created() (r time.Time, exists bool) { + v := m.created + if v == nil { + return + } + return *v, true +} + +// OldCreated returns the old "created" field's value of the BrokerJoinToken entity. +// If the BrokerJoinToken object wasn't provided to the builder, the object is fetched from the database. +// An error is returned if the mutation operation is not UpdateOne, or the database query fails. +func (m *BrokerJoinTokenMutation) OldCreated(ctx context.Context) (v time.Time, err error) { + if !m.op.Is(OpUpdateOne) { + return v, errors.New("OldCreated is only allowed on UpdateOne operations") + } + if m.id == nil || m.oldValue == nil { + return v, errors.New("OldCreated requires an ID field in the mutation") + } + oldValue, err := m.oldValue(ctx) + if err != nil { + return v, fmt.Errorf("querying old value for OldCreated: %w", err) + } + return oldValue.Created, nil +} + +// ResetCreated resets all changes to the "created" field. +func (m *BrokerJoinTokenMutation) ResetCreated() { + m.created = nil +} + +// Where appends a list predicates to the BrokerJoinTokenMutation builder. +func (m *BrokerJoinTokenMutation) Where(ps ...predicate.BrokerJoinToken) { + m.predicates = append(m.predicates, ps...) +} + +// WhereP appends storage-level predicates to the BrokerJoinTokenMutation builder. Using this method, +// users can use type-assertion to append predicates that do not depend on any generated package. +func (m *BrokerJoinTokenMutation) WhereP(ps ...func(*sql.Selector)) { + p := make([]predicate.BrokerJoinToken, len(ps)) + for i := range ps { + p[i] = ps[i] + } + m.Where(p...) +} + +// Op returns the operation name. +func (m *BrokerJoinTokenMutation) Op() Op { + return m.op +} + +// SetOp allows setting the mutation operation. +func (m *BrokerJoinTokenMutation) SetOp(op Op) { + m.op = op +} + +// Type returns the node type of this mutation (BrokerJoinToken). +func (m *BrokerJoinTokenMutation) Type() string { + return m.typ +} + +// Fields returns all fields that were changed during this mutation. Note that in +// order to get all numeric fields that were incremented/decremented, call +// AddedFields(). +func (m *BrokerJoinTokenMutation) Fields() []string { + fields := make([]string, 0, 4) + if m.token_hash != nil { + fields = append(fields, brokerjointoken.FieldTokenHash) + } + if m.expires_at != nil { + fields = append(fields, brokerjointoken.FieldExpiresAt) + } + if m.created_by != nil { + fields = append(fields, brokerjointoken.FieldCreatedBy) + } + if m.created != nil { + fields = append(fields, brokerjointoken.FieldCreated) + } + return fields +} + +// Field returns the value of a field with the given name. The second boolean +// return value indicates that this field was not set, or was not defined in the +// schema. +func (m *BrokerJoinTokenMutation) Field(name string) (ent.Value, bool) { + switch name { + case brokerjointoken.FieldTokenHash: + return m.TokenHash() + case brokerjointoken.FieldExpiresAt: + return m.ExpiresAt() + case brokerjointoken.FieldCreatedBy: + return m.CreatedBy() + case brokerjointoken.FieldCreated: + return m.Created() + } + return nil, false +} + +// OldField returns the old value of the field from the database. An error is +// returned if the mutation operation is not UpdateOne, or the query to the +// database failed. +func (m *BrokerJoinTokenMutation) OldField(ctx context.Context, name string) (ent.Value, error) { + switch name { + case brokerjointoken.FieldTokenHash: + return m.OldTokenHash(ctx) + case brokerjointoken.FieldExpiresAt: + return m.OldExpiresAt(ctx) + case brokerjointoken.FieldCreatedBy: + return m.OldCreatedBy(ctx) + case brokerjointoken.FieldCreated: + return m.OldCreated(ctx) + } + return nil, fmt.Errorf("unknown BrokerJoinToken field %s", name) +} + +// SetField sets the value of a field with the given name. It returns an error if +// the field is not defined in the schema, or if the type mismatched the field +// type. +func (m *BrokerJoinTokenMutation) SetField(name string, value ent.Value) error { + switch name { + case brokerjointoken.FieldTokenHash: + v, ok := value.(string) + if !ok { + return fmt.Errorf("unexpected type %T for field %s", value, name) + } + m.SetTokenHash(v) + return nil + case brokerjointoken.FieldExpiresAt: + v, ok := value.(time.Time) + if !ok { + return fmt.Errorf("unexpected type %T for field %s", value, name) + } + m.SetExpiresAt(v) + return nil + case brokerjointoken.FieldCreatedBy: + v, ok := value.(string) + if !ok { + return fmt.Errorf("unexpected type %T for field %s", value, name) + } + m.SetCreatedBy(v) + return nil + case brokerjointoken.FieldCreated: + v, ok := value.(time.Time) + if !ok { + return fmt.Errorf("unexpected type %T for field %s", value, name) + } + m.SetCreated(v) + return nil + } + return fmt.Errorf("unknown BrokerJoinToken field %s", name) +} + +// AddedFields returns all numeric fields that were incremented/decremented during +// this mutation. +func (m *BrokerJoinTokenMutation) AddedFields() []string { + return nil +} + +// AddedField returns the numeric value that was incremented/decremented on a field +// with the given name. The second boolean return value indicates that this field +// was not set, or was not defined in the schema. +func (m *BrokerJoinTokenMutation) AddedField(name string) (ent.Value, bool) { + return nil, false +} + +// AddField adds the value to the field with the given name. It returns an error if +// the field is not defined in the schema, or if the type mismatched the field +// type. +func (m *BrokerJoinTokenMutation) AddField(name string, value ent.Value) error { + switch name { + } + return fmt.Errorf("unknown BrokerJoinToken numeric field %s", name) +} + +// ClearedFields returns all nullable fields that were cleared during this +// mutation. +func (m *BrokerJoinTokenMutation) ClearedFields() []string { + return nil +} + +// FieldCleared returns a boolean indicating if a field with the given name was +// cleared in this mutation. +func (m *BrokerJoinTokenMutation) FieldCleared(name string) bool { + _, ok := m.clearedFields[name] + return ok +} + +// ClearField clears the value of the field with the given name. It returns an +// error if the field is not defined in the schema. +func (m *BrokerJoinTokenMutation) ClearField(name string) error { + return fmt.Errorf("unknown BrokerJoinToken nullable field %s", name) +} + +// ResetField resets all changes in the mutation for the field with the given name. +// It returns an error if the field is not defined in the schema. +func (m *BrokerJoinTokenMutation) ResetField(name string) error { + switch name { + case brokerjointoken.FieldTokenHash: + m.ResetTokenHash() + return nil + case brokerjointoken.FieldExpiresAt: + m.ResetExpiresAt() + return nil + case brokerjointoken.FieldCreatedBy: + m.ResetCreatedBy() + return nil + case brokerjointoken.FieldCreated: + m.ResetCreated() + return nil + } + return fmt.Errorf("unknown BrokerJoinToken field %s", name) +} + +// AddedEdges returns all edge names that were set/added in this mutation. +func (m *BrokerJoinTokenMutation) AddedEdges() []string { + edges := make([]string, 0, 0) + return edges +} + +// AddedIDs returns all IDs (to other nodes) that were added for the given edge +// name in this mutation. +func (m *BrokerJoinTokenMutation) AddedIDs(name string) []ent.Value { + return nil +} + +// RemovedEdges returns all edge names that were removed in this mutation. +func (m *BrokerJoinTokenMutation) RemovedEdges() []string { + edges := make([]string, 0, 0) + return edges +} + +// RemovedIDs returns all IDs (to other nodes) that were removed for the edge with +// the given name in this mutation. +func (m *BrokerJoinTokenMutation) RemovedIDs(name string) []ent.Value { + return nil +} + +// ClearedEdges returns all edge names that were cleared in this mutation. +func (m *BrokerJoinTokenMutation) ClearedEdges() []string { + edges := make([]string, 0, 0) + return edges +} + +// EdgeCleared returns a boolean which indicates if the edge with the given name +// was cleared in this mutation. +func (m *BrokerJoinTokenMutation) EdgeCleared(name string) bool { + return false +} + +// ClearEdge clears the value of the edge with the given name. It returns an error +// if that edge is not defined in the schema. +func (m *BrokerJoinTokenMutation) ClearEdge(name string) error { + return fmt.Errorf("unknown BrokerJoinToken unique edge %s", name) +} + +// ResetEdge resets all changes to the edge with the given name in this mutation. +// It returns an error if the edge is not defined in the schema. +func (m *BrokerJoinTokenMutation) ResetEdge(name string) error { + return fmt.Errorf("unknown BrokerJoinToken edge %s", name) +} + +// BrokerSecretMutation represents an operation that mutates the BrokerSecret nodes in the graph. +type BrokerSecretMutation struct { + config + op Op + typ string + id *uuid.UUID + secret_key *[]byte + algorithm *string + rotated_at *time.Time + expires_at *time.Time + status *string + created *time.Time + clearedFields map[string]struct{} + done bool + oldValue func(context.Context) (*BrokerSecret, error) + predicates []predicate.BrokerSecret +} + +var _ ent.Mutation = (*BrokerSecretMutation)(nil) + +// brokersecretOption allows management of the mutation configuration using functional options. +type brokersecretOption func(*BrokerSecretMutation) + +// newBrokerSecretMutation creates new mutation for the BrokerSecret entity. +func newBrokerSecretMutation(c config, op Op, opts ...brokersecretOption) *BrokerSecretMutation { + m := &BrokerSecretMutation{ + config: c, + op: op, + typ: TypeBrokerSecret, + clearedFields: make(map[string]struct{}), + } + for _, opt := range opts { + opt(m) + } + return m +} + +// withBrokerSecretID sets the ID field of the mutation. +func withBrokerSecretID(id uuid.UUID) brokersecretOption { + return func(m *BrokerSecretMutation) { + var ( + err error + once sync.Once + value *BrokerSecret + ) + m.oldValue = func(ctx context.Context) (*BrokerSecret, error) { + once.Do(func() { + if m.done { + err = errors.New("querying old values post mutation is not allowed") + } else { + value, err = m.Client().BrokerSecret.Get(ctx, id) + } + }) + return value, err + } + m.id = &id + } +} + +// withBrokerSecret sets the old BrokerSecret of the mutation. +func withBrokerSecret(node *BrokerSecret) brokersecretOption { + return func(m *BrokerSecretMutation) { + m.oldValue = func(context.Context) (*BrokerSecret, error) { + return node, nil + } + m.id = &node.ID + } +} + +// Client returns a new `ent.Client` from the mutation. If the mutation was +// executed in a transaction (ent.Tx), a transactional client is returned. +func (m BrokerSecretMutation) Client() *Client { + client := &Client{config: m.config} + client.init() + return client +} + +// Tx returns an `ent.Tx` for mutations that were executed in transactions; +// it returns an error otherwise. +func (m BrokerSecretMutation) Tx() (*Tx, error) { + if _, ok := m.driver.(*txDriver); !ok { + return nil, errors.New("ent: mutation is not running in a transaction") + } + tx := &Tx{config: m.config} + tx.init() + return tx, nil +} + +// SetID sets the value of the id field. Note that this +// operation is only accepted on creation of BrokerSecret entities. +func (m *BrokerSecretMutation) SetID(id uuid.UUID) { + m.id = &id +} + +// ID returns the ID value in the mutation. Note that the ID is only available +// if it was provided to the builder or after it was returned from the database. +func (m *BrokerSecretMutation) ID() (id uuid.UUID, exists bool) { + if m.id == nil { + return + } + return *m.id, true +} + +// IDs queries the database and returns the entity ids that match the mutation's predicate. +// That means, if the mutation is applied within a transaction with an isolation level such +// as sql.LevelSerializable, the returned ids match the ids of the rows that will be updated +// or updated by the mutation. +func (m *BrokerSecretMutation) IDs(ctx context.Context) ([]uuid.UUID, error) { + switch { + case m.op.Is(OpUpdateOne | OpDeleteOne): + id, exists := m.ID() + if exists { + return []uuid.UUID{id}, nil + } + fallthrough + case m.op.Is(OpUpdate | OpDelete): + return m.Client().BrokerSecret.Query().Where(m.predicates...).IDs(ctx) + default: + return nil, fmt.Errorf("IDs is not allowed on %s operations", m.op) + } +} + +// SetSecretKey sets the "secret_key" field. +func (m *BrokerSecretMutation) SetSecretKey(b []byte) { + m.secret_key = &b +} + +// SecretKey returns the value of the "secret_key" field in the mutation. +func (m *BrokerSecretMutation) SecretKey() (r []byte, exists bool) { + v := m.secret_key + if v == nil { + return + } + return *v, true +} + +// OldSecretKey returns the old "secret_key" field's value of the BrokerSecret entity. +// If the BrokerSecret object wasn't provided to the builder, the object is fetched from the database. +// An error is returned if the mutation operation is not UpdateOne, or the database query fails. +func (m *BrokerSecretMutation) OldSecretKey(ctx context.Context) (v []byte, err error) { + if !m.op.Is(OpUpdateOne) { + return v, errors.New("OldSecretKey is only allowed on UpdateOne operations") + } + if m.id == nil || m.oldValue == nil { + return v, errors.New("OldSecretKey requires an ID field in the mutation") + } + oldValue, err := m.oldValue(ctx) + if err != nil { + return v, fmt.Errorf("querying old value for OldSecretKey: %w", err) + } + return oldValue.SecretKey, nil +} + +// ResetSecretKey resets all changes to the "secret_key" field. +func (m *BrokerSecretMutation) ResetSecretKey() { + m.secret_key = nil +} + +// SetAlgorithm sets the "algorithm" field. +func (m *BrokerSecretMutation) SetAlgorithm(s string) { + m.algorithm = &s +} + +// Algorithm returns the value of the "algorithm" field in the mutation. +func (m *BrokerSecretMutation) Algorithm() (r string, exists bool) { + v := m.algorithm + if v == nil { + return + } + return *v, true +} + +// OldAlgorithm returns the old "algorithm" field's value of the BrokerSecret entity. +// If the BrokerSecret object wasn't provided to the builder, the object is fetched from the database. +// An error is returned if the mutation operation is not UpdateOne, or the database query fails. +func (m *BrokerSecretMutation) OldAlgorithm(ctx context.Context) (v string, err error) { + if !m.op.Is(OpUpdateOne) { + return v, errors.New("OldAlgorithm is only allowed on UpdateOne operations") + } + if m.id == nil || m.oldValue == nil { + return v, errors.New("OldAlgorithm requires an ID field in the mutation") + } + oldValue, err := m.oldValue(ctx) + if err != nil { + return v, fmt.Errorf("querying old value for OldAlgorithm: %w", err) + } + return oldValue.Algorithm, nil +} + +// ResetAlgorithm resets all changes to the "algorithm" field. +func (m *BrokerSecretMutation) ResetAlgorithm() { + m.algorithm = nil +} + +// SetRotatedAt sets the "rotated_at" field. +func (m *BrokerSecretMutation) SetRotatedAt(t time.Time) { + m.rotated_at = &t +} + +// RotatedAt returns the value of the "rotated_at" field in the mutation. +func (m *BrokerSecretMutation) RotatedAt() (r time.Time, exists bool) { + v := m.rotated_at + if v == nil { + return + } + return *v, true +} + +// OldRotatedAt returns the old "rotated_at" field's value of the BrokerSecret entity. +// If the BrokerSecret object wasn't provided to the builder, the object is fetched from the database. +// An error is returned if the mutation operation is not UpdateOne, or the database query fails. +func (m *BrokerSecretMutation) OldRotatedAt(ctx context.Context) (v *time.Time, err error) { + if !m.op.Is(OpUpdateOne) { + return v, errors.New("OldRotatedAt is only allowed on UpdateOne operations") + } + if m.id == nil || m.oldValue == nil { + return v, errors.New("OldRotatedAt requires an ID field in the mutation") + } + oldValue, err := m.oldValue(ctx) + if err != nil { + return v, fmt.Errorf("querying old value for OldRotatedAt: %w", err) + } + return oldValue.RotatedAt, nil +} + +// ClearRotatedAt clears the value of the "rotated_at" field. +func (m *BrokerSecretMutation) ClearRotatedAt() { + m.rotated_at = nil + m.clearedFields[brokersecret.FieldRotatedAt] = struct{}{} +} + +// RotatedAtCleared returns if the "rotated_at" field was cleared in this mutation. +func (m *BrokerSecretMutation) RotatedAtCleared() bool { + _, ok := m.clearedFields[brokersecret.FieldRotatedAt] + return ok +} + +// ResetRotatedAt resets all changes to the "rotated_at" field. +func (m *BrokerSecretMutation) ResetRotatedAt() { + m.rotated_at = nil + delete(m.clearedFields, brokersecret.FieldRotatedAt) +} + +// SetExpiresAt sets the "expires_at" field. +func (m *BrokerSecretMutation) SetExpiresAt(t time.Time) { + m.expires_at = &t +} + +// ExpiresAt returns the value of the "expires_at" field in the mutation. +func (m *BrokerSecretMutation) ExpiresAt() (r time.Time, exists bool) { + v := m.expires_at + if v == nil { + return + } + return *v, true +} + +// OldExpiresAt returns the old "expires_at" field's value of the BrokerSecret entity. +// If the BrokerSecret object wasn't provided to the builder, the object is fetched from the database. +// An error is returned if the mutation operation is not UpdateOne, or the database query fails. +func (m *BrokerSecretMutation) OldExpiresAt(ctx context.Context) (v *time.Time, err error) { + if !m.op.Is(OpUpdateOne) { + return v, errors.New("OldExpiresAt is only allowed on UpdateOne operations") + } + if m.id == nil || m.oldValue == nil { + return v, errors.New("OldExpiresAt requires an ID field in the mutation") + } + oldValue, err := m.oldValue(ctx) + if err != nil { + return v, fmt.Errorf("querying old value for OldExpiresAt: %w", err) + } + return oldValue.ExpiresAt, nil +} + +// ClearExpiresAt clears the value of the "expires_at" field. +func (m *BrokerSecretMutation) ClearExpiresAt() { + m.expires_at = nil + m.clearedFields[brokersecret.FieldExpiresAt] = struct{}{} +} + +// ExpiresAtCleared returns if the "expires_at" field was cleared in this mutation. +func (m *BrokerSecretMutation) ExpiresAtCleared() bool { + _, ok := m.clearedFields[brokersecret.FieldExpiresAt] + return ok +} + +// ResetExpiresAt resets all changes to the "expires_at" field. +func (m *BrokerSecretMutation) ResetExpiresAt() { + m.expires_at = nil + delete(m.clearedFields, brokersecret.FieldExpiresAt) +} + +// SetStatus sets the "status" field. +func (m *BrokerSecretMutation) SetStatus(s string) { + m.status = &s +} + +// Status returns the value of the "status" field in the mutation. +func (m *BrokerSecretMutation) Status() (r string, exists bool) { + v := m.status + if v == nil { + return + } + return *v, true +} + +// OldStatus returns the old "status" field's value of the BrokerSecret entity. +// If the BrokerSecret object wasn't provided to the builder, the object is fetched from the database. +// An error is returned if the mutation operation is not UpdateOne, or the database query fails. +func (m *BrokerSecretMutation) OldStatus(ctx context.Context) (v string, err error) { + if !m.op.Is(OpUpdateOne) { + return v, errors.New("OldStatus is only allowed on UpdateOne operations") + } + if m.id == nil || m.oldValue == nil { + return v, errors.New("OldStatus requires an ID field in the mutation") + } + oldValue, err := m.oldValue(ctx) + if err != nil { + return v, fmt.Errorf("querying old value for OldStatus: %w", err) + } + return oldValue.Status, nil +} + +// ResetStatus resets all changes to the "status" field. +func (m *BrokerSecretMutation) ResetStatus() { + m.status = nil +} + +// SetCreated sets the "created" field. +func (m *BrokerSecretMutation) SetCreated(t time.Time) { + m.created = &t +} + +// Created returns the value of the "created" field in the mutation. +func (m *BrokerSecretMutation) Created() (r time.Time, exists bool) { + v := m.created + if v == nil { + return + } + return *v, true +} + +// OldCreated returns the old "created" field's value of the BrokerSecret entity. +// If the BrokerSecret object wasn't provided to the builder, the object is fetched from the database. +// An error is returned if the mutation operation is not UpdateOne, or the database query fails. +func (m *BrokerSecretMutation) OldCreated(ctx context.Context) (v time.Time, err error) { + if !m.op.Is(OpUpdateOne) { + return v, errors.New("OldCreated is only allowed on UpdateOne operations") + } + if m.id == nil || m.oldValue == nil { + return v, errors.New("OldCreated requires an ID field in the mutation") + } + oldValue, err := m.oldValue(ctx) + if err != nil { + return v, fmt.Errorf("querying old value for OldCreated: %w", err) + } + return oldValue.Created, nil +} + +// ResetCreated resets all changes to the "created" field. +func (m *BrokerSecretMutation) ResetCreated() { + m.created = nil +} + +// Where appends a list predicates to the BrokerSecretMutation builder. +func (m *BrokerSecretMutation) Where(ps ...predicate.BrokerSecret) { + m.predicates = append(m.predicates, ps...) +} + +// WhereP appends storage-level predicates to the BrokerSecretMutation builder. Using this method, +// users can use type-assertion to append predicates that do not depend on any generated package. +func (m *BrokerSecretMutation) WhereP(ps ...func(*sql.Selector)) { + p := make([]predicate.BrokerSecret, len(ps)) + for i := range ps { + p[i] = ps[i] + } + m.Where(p...) +} + +// Op returns the operation name. +func (m *BrokerSecretMutation) Op() Op { + return m.op +} + +// SetOp allows setting the mutation operation. +func (m *BrokerSecretMutation) SetOp(op Op) { + m.op = op +} + +// Type returns the node type of this mutation (BrokerSecret). +func (m *BrokerSecretMutation) Type() string { + return m.typ +} + +// Fields returns all fields that were changed during this mutation. Note that in +// order to get all numeric fields that were incremented/decremented, call +// AddedFields(). +func (m *BrokerSecretMutation) Fields() []string { + fields := make([]string, 0, 6) + if m.secret_key != nil { + fields = append(fields, brokersecret.FieldSecretKey) + } + if m.algorithm != nil { + fields = append(fields, brokersecret.FieldAlgorithm) + } + if m.rotated_at != nil { + fields = append(fields, brokersecret.FieldRotatedAt) + } + if m.expires_at != nil { + fields = append(fields, brokersecret.FieldExpiresAt) + } + if m.status != nil { + fields = append(fields, brokersecret.FieldStatus) + } + if m.created != nil { + fields = append(fields, brokersecret.FieldCreated) + } + return fields +} + +// Field returns the value of a field with the given name. The second boolean +// return value indicates that this field was not set, or was not defined in the +// schema. +func (m *BrokerSecretMutation) Field(name string) (ent.Value, bool) { + switch name { + case brokersecret.FieldSecretKey: + return m.SecretKey() + case brokersecret.FieldAlgorithm: + return m.Algorithm() + case brokersecret.FieldRotatedAt: + return m.RotatedAt() + case brokersecret.FieldExpiresAt: + return m.ExpiresAt() + case brokersecret.FieldStatus: + return m.Status() + case brokersecret.FieldCreated: + return m.Created() + } + return nil, false +} + +// OldField returns the old value of the field from the database. An error is +// returned if the mutation operation is not UpdateOne, or the query to the +// database failed. +func (m *BrokerSecretMutation) OldField(ctx context.Context, name string) (ent.Value, error) { + switch name { + case brokersecret.FieldSecretKey: + return m.OldSecretKey(ctx) + case brokersecret.FieldAlgorithm: + return m.OldAlgorithm(ctx) + case brokersecret.FieldRotatedAt: + return m.OldRotatedAt(ctx) + case brokersecret.FieldExpiresAt: + return m.OldExpiresAt(ctx) + case brokersecret.FieldStatus: + return m.OldStatus(ctx) + case brokersecret.FieldCreated: + return m.OldCreated(ctx) + } + return nil, fmt.Errorf("unknown BrokerSecret field %s", name) +} + +// SetField sets the value of a field with the given name. It returns an error if +// the field is not defined in the schema, or if the type mismatched the field +// type. +func (m *BrokerSecretMutation) SetField(name string, value ent.Value) error { + switch name { + case brokersecret.FieldSecretKey: + v, ok := value.([]byte) + if !ok { + return fmt.Errorf("unexpected type %T for field %s", value, name) + } + m.SetSecretKey(v) + return nil + case brokersecret.FieldAlgorithm: + v, ok := value.(string) + if !ok { + return fmt.Errorf("unexpected type %T for field %s", value, name) + } + m.SetAlgorithm(v) + return nil + case brokersecret.FieldRotatedAt: + v, ok := value.(time.Time) + if !ok { + return fmt.Errorf("unexpected type %T for field %s", value, name) + } + m.SetRotatedAt(v) + return nil + case brokersecret.FieldExpiresAt: + v, ok := value.(time.Time) + if !ok { + return fmt.Errorf("unexpected type %T for field %s", value, name) + } + m.SetExpiresAt(v) + return nil + case brokersecret.FieldStatus: + v, ok := value.(string) + if !ok { + return fmt.Errorf("unexpected type %T for field %s", value, name) + } + m.SetStatus(v) + return nil + case brokersecret.FieldCreated: + v, ok := value.(time.Time) + if !ok { + return fmt.Errorf("unexpected type %T for field %s", value, name) + } + m.SetCreated(v) + return nil + } + return fmt.Errorf("unknown BrokerSecret field %s", name) +} + +// AddedFields returns all numeric fields that were incremented/decremented during +// this mutation. +func (m *BrokerSecretMutation) AddedFields() []string { + return nil +} + +// AddedField returns the numeric value that was incremented/decremented on a field +// with the given name. The second boolean return value indicates that this field +// was not set, or was not defined in the schema. +func (m *BrokerSecretMutation) AddedField(name string) (ent.Value, bool) { + return nil, false +} + +// AddField adds the value to the field with the given name. It returns an error if +// the field is not defined in the schema, or if the type mismatched the field +// type. +func (m *BrokerSecretMutation) AddField(name string, value ent.Value) error { + switch name { + } + return fmt.Errorf("unknown BrokerSecret numeric field %s", name) +} + +// ClearedFields returns all nullable fields that were cleared during this +// mutation. +func (m *BrokerSecretMutation) ClearedFields() []string { + var fields []string + if m.FieldCleared(brokersecret.FieldRotatedAt) { + fields = append(fields, brokersecret.FieldRotatedAt) + } + if m.FieldCleared(brokersecret.FieldExpiresAt) { + fields = append(fields, brokersecret.FieldExpiresAt) + } + return fields +} + +// FieldCleared returns a boolean indicating if a field with the given name was +// cleared in this mutation. +func (m *BrokerSecretMutation) FieldCleared(name string) bool { + _, ok := m.clearedFields[name] + return ok +} + +// ClearField clears the value of the field with the given name. It returns an +// error if the field is not defined in the schema. +func (m *BrokerSecretMutation) ClearField(name string) error { + switch name { + case brokersecret.FieldRotatedAt: + m.ClearRotatedAt() + return nil + case brokersecret.FieldExpiresAt: + m.ClearExpiresAt() + return nil + } + return fmt.Errorf("unknown BrokerSecret nullable field %s", name) +} + +// ResetField resets all changes in the mutation for the field with the given name. +// It returns an error if the field is not defined in the schema. +func (m *BrokerSecretMutation) ResetField(name string) error { + switch name { + case brokersecret.FieldSecretKey: + m.ResetSecretKey() + return nil + case brokersecret.FieldAlgorithm: + m.ResetAlgorithm() + return nil + case brokersecret.FieldRotatedAt: + m.ResetRotatedAt() + return nil + case brokersecret.FieldExpiresAt: + m.ResetExpiresAt() + return nil + case brokersecret.FieldStatus: + m.ResetStatus() + return nil + case brokersecret.FieldCreated: + m.ResetCreated() + return nil + } + return fmt.Errorf("unknown BrokerSecret field %s", name) +} + +// AddedEdges returns all edge names that were set/added in this mutation. +func (m *BrokerSecretMutation) AddedEdges() []string { + edges := make([]string, 0, 0) + return edges +} + +// AddedIDs returns all IDs (to other nodes) that were added for the given edge +// name in this mutation. +func (m *BrokerSecretMutation) AddedIDs(name string) []ent.Value { + return nil +} + +// RemovedEdges returns all edge names that were removed in this mutation. +func (m *BrokerSecretMutation) RemovedEdges() []string { + edges := make([]string, 0, 0) + return edges +} + +// RemovedIDs returns all IDs (to other nodes) that were removed for the edge with +// the given name in this mutation. +func (m *BrokerSecretMutation) RemovedIDs(name string) []ent.Value { + return nil +} + +// ClearedEdges returns all edge names that were cleared in this mutation. +func (m *BrokerSecretMutation) ClearedEdges() []string { + edges := make([]string, 0, 0) + return edges +} + +// EdgeCleared returns a boolean which indicates if the edge with the given name +// was cleared in this mutation. +func (m *BrokerSecretMutation) EdgeCleared(name string) bool { + return false +} + +// ClearEdge clears the value of the edge with the given name. It returns an error +// if that edge is not defined in the schema. +func (m *BrokerSecretMutation) ClearEdge(name string) error { + return fmt.Errorf("unknown BrokerSecret unique edge %s", name) +} + +// ResetEdge resets all changes to the edge with the given name in this mutation. +// It returns an error if the edge is not defined in the schema. +func (m *BrokerSecretMutation) ResetEdge(name string) error { + return fmt.Errorf("unknown BrokerSecret edge %s", name) +} + +// EnvVarMutation represents an operation that mutates the EnvVar nodes in the graph. +type EnvVarMutation struct { + config + op Op + typ string + id *uuid.UUID + key *string + value *string + scope *string + scope_id *string + description *string + sensitive *bool + injection_mode *envvar.InjectionMode + secret *bool + created_by *string + created *time.Time + updated *time.Time + clearedFields map[string]struct{} + done bool + oldValue func(context.Context) (*EnvVar, error) + predicates []predicate.EnvVar +} + +var _ ent.Mutation = (*EnvVarMutation)(nil) + +// envvarOption allows management of the mutation configuration using functional options. +type envvarOption func(*EnvVarMutation) + +// newEnvVarMutation creates new mutation for the EnvVar entity. +func newEnvVarMutation(c config, op Op, opts ...envvarOption) *EnvVarMutation { + m := &EnvVarMutation{ + config: c, + op: op, + typ: TypeEnvVar, + clearedFields: make(map[string]struct{}), + } + for _, opt := range opts { + opt(m) + } + return m +} + +// withEnvVarID sets the ID field of the mutation. +func withEnvVarID(id uuid.UUID) envvarOption { + return func(m *EnvVarMutation) { + var ( + err error + once sync.Once + value *EnvVar + ) + m.oldValue = func(ctx context.Context) (*EnvVar, error) { + once.Do(func() { + if m.done { + err = errors.New("querying old values post mutation is not allowed") + } else { + value, err = m.Client().EnvVar.Get(ctx, id) + } + }) + return value, err + } + m.id = &id + } +} + +// withEnvVar sets the old EnvVar of the mutation. +func withEnvVar(node *EnvVar) envvarOption { + return func(m *EnvVarMutation) { + m.oldValue = func(context.Context) (*EnvVar, error) { + return node, nil + } + m.id = &node.ID + } +} + +// Client returns a new `ent.Client` from the mutation. If the mutation was +// executed in a transaction (ent.Tx), a transactional client is returned. +func (m EnvVarMutation) Client() *Client { + client := &Client{config: m.config} + client.init() + return client +} + +// Tx returns an `ent.Tx` for mutations that were executed in transactions; +// it returns an error otherwise. +func (m EnvVarMutation) Tx() (*Tx, error) { + if _, ok := m.driver.(*txDriver); !ok { + return nil, errors.New("ent: mutation is not running in a transaction") + } + tx := &Tx{config: m.config} + tx.init() + return tx, nil +} + +// SetID sets the value of the id field. Note that this +// operation is only accepted on creation of EnvVar entities. +func (m *EnvVarMutation) SetID(id uuid.UUID) { + m.id = &id +} + +// ID returns the ID value in the mutation. Note that the ID is only available +// if it was provided to the builder or after it was returned from the database. +func (m *EnvVarMutation) ID() (id uuid.UUID, exists bool) { + if m.id == nil { + return + } + return *m.id, true +} + +// IDs queries the database and returns the entity ids that match the mutation's predicate. +// That means, if the mutation is applied within a transaction with an isolation level such +// as sql.LevelSerializable, the returned ids match the ids of the rows that will be updated +// or updated by the mutation. +func (m *EnvVarMutation) IDs(ctx context.Context) ([]uuid.UUID, error) { + switch { + case m.op.Is(OpUpdateOne | OpDeleteOne): + id, exists := m.ID() + if exists { + return []uuid.UUID{id}, nil + } + fallthrough + case m.op.Is(OpUpdate | OpDelete): + return m.Client().EnvVar.Query().Where(m.predicates...).IDs(ctx) + default: + return nil, fmt.Errorf("IDs is not allowed on %s operations", m.op) + } +} + +// SetKey sets the "key" field. +func (m *EnvVarMutation) SetKey(s string) { + m.key = &s +} + +// Key returns the value of the "key" field in the mutation. +func (m *EnvVarMutation) Key() (r string, exists bool) { + v := m.key + if v == nil { + return + } + return *v, true +} + +// OldKey returns the old "key" field's value of the EnvVar entity. +// If the EnvVar object wasn't provided to the builder, the object is fetched from the database. +// An error is returned if the mutation operation is not UpdateOne, or the database query fails. +func (m *EnvVarMutation) OldKey(ctx context.Context) (v string, err error) { + if !m.op.Is(OpUpdateOne) { + return v, errors.New("OldKey is only allowed on UpdateOne operations") + } + if m.id == nil || m.oldValue == nil { + return v, errors.New("OldKey requires an ID field in the mutation") + } + oldValue, err := m.oldValue(ctx) + if err != nil { + return v, fmt.Errorf("querying old value for OldKey: %w", err) + } + return oldValue.Key, nil +} + +// ResetKey resets all changes to the "key" field. +func (m *EnvVarMutation) ResetKey() { + m.key = nil +} + +// SetValue sets the "value" field. +func (m *EnvVarMutation) SetValue(s string) { + m.value = &s +} + +// Value returns the value of the "value" field in the mutation. +func (m *EnvVarMutation) Value() (r string, exists bool) { + v := m.value + if v == nil { + return + } + return *v, true +} + +// OldValue returns the old "value" field's value of the EnvVar entity. +// If the EnvVar object wasn't provided to the builder, the object is fetched from the database. +// An error is returned if the mutation operation is not UpdateOne, or the database query fails. +func (m *EnvVarMutation) OldValue(ctx context.Context) (v string, err error) { + if !m.op.Is(OpUpdateOne) { + return v, errors.New("OldValue is only allowed on UpdateOne operations") + } + if m.id == nil || m.oldValue == nil { + return v, errors.New("OldValue requires an ID field in the mutation") + } + oldValue, err := m.oldValue(ctx) + if err != nil { + return v, fmt.Errorf("querying old value for OldValue: %w", err) + } + return oldValue.Value, nil +} + +// ResetValue resets all changes to the "value" field. +func (m *EnvVarMutation) ResetValue() { + m.value = nil +} + +// SetScope sets the "scope" field. +func (m *EnvVarMutation) SetScope(s string) { + m.scope = &s +} + +// Scope returns the value of the "scope" field in the mutation. +func (m *EnvVarMutation) Scope() (r string, exists bool) { + v := m.scope + if v == nil { + return + } + return *v, true +} + +// OldScope returns the old "scope" field's value of the EnvVar entity. +// If the EnvVar object wasn't provided to the builder, the object is fetched from the database. +// An error is returned if the mutation operation is not UpdateOne, or the database query fails. +func (m *EnvVarMutation) OldScope(ctx context.Context) (v string, err error) { + if !m.op.Is(OpUpdateOne) { + return v, errors.New("OldScope is only allowed on UpdateOne operations") + } + if m.id == nil || m.oldValue == nil { + return v, errors.New("OldScope requires an ID field in the mutation") + } + oldValue, err := m.oldValue(ctx) + if err != nil { + return v, fmt.Errorf("querying old value for OldScope: %w", err) + } + return oldValue.Scope, nil +} + +// ResetScope resets all changes to the "scope" field. +func (m *EnvVarMutation) ResetScope() { + m.scope = nil +} + +// SetScopeID sets the "scope_id" field. +func (m *EnvVarMutation) SetScopeID(s string) { + m.scope_id = &s +} + +// ScopeID returns the value of the "scope_id" field in the mutation. +func (m *EnvVarMutation) ScopeID() (r string, exists bool) { + v := m.scope_id + if v == nil { + return + } + return *v, true +} + +// OldScopeID returns the old "scope_id" field's value of the EnvVar entity. +// If the EnvVar object wasn't provided to the builder, the object is fetched from the database. +// An error is returned if the mutation operation is not UpdateOne, or the database query fails. +func (m *EnvVarMutation) OldScopeID(ctx context.Context) (v string, err error) { + if !m.op.Is(OpUpdateOne) { + return v, errors.New("OldScopeID is only allowed on UpdateOne operations") + } + if m.id == nil || m.oldValue == nil { + return v, errors.New("OldScopeID requires an ID field in the mutation") + } + oldValue, err := m.oldValue(ctx) + if err != nil { + return v, fmt.Errorf("querying old value for OldScopeID: %w", err) + } + return oldValue.ScopeID, nil +} + +// ResetScopeID resets all changes to the "scope_id" field. +func (m *EnvVarMutation) ResetScopeID() { + m.scope_id = nil +} + +// SetDescription sets the "description" field. +func (m *EnvVarMutation) SetDescription(s string) { + m.description = &s +} + +// Description returns the value of the "description" field in the mutation. +func (m *EnvVarMutation) Description() (r string, exists bool) { + v := m.description + if v == nil { + return + } + return *v, true +} + +// OldDescription returns the old "description" field's value of the EnvVar entity. +// If the EnvVar object wasn't provided to the builder, the object is fetched from the database. +// An error is returned if the mutation operation is not UpdateOne, or the database query fails. +func (m *EnvVarMutation) OldDescription(ctx context.Context) (v string, err error) { + if !m.op.Is(OpUpdateOne) { + return v, errors.New("OldDescription is only allowed on UpdateOne operations") + } + if m.id == nil || m.oldValue == nil { + return v, errors.New("OldDescription requires an ID field in the mutation") + } + oldValue, err := m.oldValue(ctx) + if err != nil { + return v, fmt.Errorf("querying old value for OldDescription: %w", err) + } + return oldValue.Description, nil +} + +// ClearDescription clears the value of the "description" field. +func (m *EnvVarMutation) ClearDescription() { + m.description = nil + m.clearedFields[envvar.FieldDescription] = struct{}{} +} + +// DescriptionCleared returns if the "description" field was cleared in this mutation. +func (m *EnvVarMutation) DescriptionCleared() bool { + _, ok := m.clearedFields[envvar.FieldDescription] + return ok +} + +// ResetDescription resets all changes to the "description" field. +func (m *EnvVarMutation) ResetDescription() { + m.description = nil + delete(m.clearedFields, envvar.FieldDescription) +} + +// SetSensitive sets the "sensitive" field. +func (m *EnvVarMutation) SetSensitive(b bool) { + m.sensitive = &b +} + +// Sensitive returns the value of the "sensitive" field in the mutation. +func (m *EnvVarMutation) Sensitive() (r bool, exists bool) { + v := m.sensitive + if v == nil { + return + } + return *v, true +} + +// OldSensitive returns the old "sensitive" field's value of the EnvVar entity. +// If the EnvVar object wasn't provided to the builder, the object is fetched from the database. +// An error is returned if the mutation operation is not UpdateOne, or the database query fails. +func (m *EnvVarMutation) OldSensitive(ctx context.Context) (v bool, err error) { + if !m.op.Is(OpUpdateOne) { + return v, errors.New("OldSensitive is only allowed on UpdateOne operations") + } + if m.id == nil || m.oldValue == nil { + return v, errors.New("OldSensitive requires an ID field in the mutation") + } + oldValue, err := m.oldValue(ctx) + if err != nil { + return v, fmt.Errorf("querying old value for OldSensitive: %w", err) + } + return oldValue.Sensitive, nil +} + +// ResetSensitive resets all changes to the "sensitive" field. +func (m *EnvVarMutation) ResetSensitive() { + m.sensitive = nil +} + +// SetInjectionMode sets the "injection_mode" field. +func (m *EnvVarMutation) SetInjectionMode(em envvar.InjectionMode) { + m.injection_mode = &em +} + +// InjectionMode returns the value of the "injection_mode" field in the mutation. +func (m *EnvVarMutation) InjectionMode() (r envvar.InjectionMode, exists bool) { + v := m.injection_mode + if v == nil { + return + } + return *v, true +} + +// OldInjectionMode returns the old "injection_mode" field's value of the EnvVar entity. +// If the EnvVar object wasn't provided to the builder, the object is fetched from the database. +// An error is returned if the mutation operation is not UpdateOne, or the database query fails. +func (m *EnvVarMutation) OldInjectionMode(ctx context.Context) (v envvar.InjectionMode, err error) { + if !m.op.Is(OpUpdateOne) { + return v, errors.New("OldInjectionMode is only allowed on UpdateOne operations") + } + if m.id == nil || m.oldValue == nil { + return v, errors.New("OldInjectionMode requires an ID field in the mutation") + } + oldValue, err := m.oldValue(ctx) + if err != nil { + return v, fmt.Errorf("querying old value for OldInjectionMode: %w", err) + } + return oldValue.InjectionMode, nil +} + +// ResetInjectionMode resets all changes to the "injection_mode" field. +func (m *EnvVarMutation) ResetInjectionMode() { + m.injection_mode = nil +} + +// SetSecret sets the "secret" field. +func (m *EnvVarMutation) SetSecret(b bool) { + m.secret = &b +} + +// Secret returns the value of the "secret" field in the mutation. +func (m *EnvVarMutation) Secret() (r bool, exists bool) { + v := m.secret + if v == nil { + return + } + return *v, true +} + +// OldSecret returns the old "secret" field's value of the EnvVar entity. +// If the EnvVar object wasn't provided to the builder, the object is fetched from the database. +// An error is returned if the mutation operation is not UpdateOne, or the database query fails. +func (m *EnvVarMutation) OldSecret(ctx context.Context) (v bool, err error) { + if !m.op.Is(OpUpdateOne) { + return v, errors.New("OldSecret is only allowed on UpdateOne operations") + } + if m.id == nil || m.oldValue == nil { + return v, errors.New("OldSecret requires an ID field in the mutation") + } + oldValue, err := m.oldValue(ctx) + if err != nil { + return v, fmt.Errorf("querying old value for OldSecret: %w", err) + } + return oldValue.Secret, nil +} + +// ResetSecret resets all changes to the "secret" field. +func (m *EnvVarMutation) ResetSecret() { + m.secret = nil +} + +// SetCreatedBy sets the "created_by" field. +func (m *EnvVarMutation) SetCreatedBy(s string) { + m.created_by = &s +} + +// CreatedBy returns the value of the "created_by" field in the mutation. +func (m *EnvVarMutation) CreatedBy() (r string, exists bool) { + v := m.created_by + if v == nil { + return + } + return *v, true +} + +// OldCreatedBy returns the old "created_by" field's value of the EnvVar entity. +// If the EnvVar object wasn't provided to the builder, the object is fetched from the database. +// An error is returned if the mutation operation is not UpdateOne, or the database query fails. +func (m *EnvVarMutation) OldCreatedBy(ctx context.Context) (v string, err error) { + if !m.op.Is(OpUpdateOne) { + return v, errors.New("OldCreatedBy is only allowed on UpdateOne operations") + } + if m.id == nil || m.oldValue == nil { + return v, errors.New("OldCreatedBy requires an ID field in the mutation") + } + oldValue, err := m.oldValue(ctx) + if err != nil { + return v, fmt.Errorf("querying old value for OldCreatedBy: %w", err) + } + return oldValue.CreatedBy, nil +} + +// ClearCreatedBy clears the value of the "created_by" field. +func (m *EnvVarMutation) ClearCreatedBy() { + m.created_by = nil + m.clearedFields[envvar.FieldCreatedBy] = struct{}{} +} + +// CreatedByCleared returns if the "created_by" field was cleared in this mutation. +func (m *EnvVarMutation) CreatedByCleared() bool { + _, ok := m.clearedFields[envvar.FieldCreatedBy] + return ok +} + +// ResetCreatedBy resets all changes to the "created_by" field. +func (m *EnvVarMutation) ResetCreatedBy() { + m.created_by = nil + delete(m.clearedFields, envvar.FieldCreatedBy) +} + +// SetCreated sets the "created" field. +func (m *EnvVarMutation) SetCreated(t time.Time) { + m.created = &t +} + +// Created returns the value of the "created" field in the mutation. +func (m *EnvVarMutation) Created() (r time.Time, exists bool) { + v := m.created + if v == nil { + return + } + return *v, true +} + +// OldCreated returns the old "created" field's value of the EnvVar entity. +// If the EnvVar object wasn't provided to the builder, the object is fetched from the database. +// An error is returned if the mutation operation is not UpdateOne, or the database query fails. +func (m *EnvVarMutation) OldCreated(ctx context.Context) (v time.Time, err error) { + if !m.op.Is(OpUpdateOne) { + return v, errors.New("OldCreated is only allowed on UpdateOne operations") + } + if m.id == nil || m.oldValue == nil { + return v, errors.New("OldCreated requires an ID field in the mutation") + } + oldValue, err := m.oldValue(ctx) + if err != nil { + return v, fmt.Errorf("querying old value for OldCreated: %w", err) + } + return oldValue.Created, nil +} + +// ResetCreated resets all changes to the "created" field. +func (m *EnvVarMutation) ResetCreated() { + m.created = nil +} + +// SetUpdated sets the "updated" field. +func (m *EnvVarMutation) SetUpdated(t time.Time) { + m.updated = &t +} + +// Updated returns the value of the "updated" field in the mutation. +func (m *EnvVarMutation) Updated() (r time.Time, exists bool) { + v := m.updated + if v == nil { + return + } + return *v, true +} + +// OldUpdated returns the old "updated" field's value of the EnvVar entity. +// If the EnvVar object wasn't provided to the builder, the object is fetched from the database. +// An error is returned if the mutation operation is not UpdateOne, or the database query fails. +func (m *EnvVarMutation) OldUpdated(ctx context.Context) (v time.Time, err error) { + if !m.op.Is(OpUpdateOne) { + return v, errors.New("OldUpdated is only allowed on UpdateOne operations") + } + if m.id == nil || m.oldValue == nil { + return v, errors.New("OldUpdated requires an ID field in the mutation") + } + oldValue, err := m.oldValue(ctx) + if err != nil { + return v, fmt.Errorf("querying old value for OldUpdated: %w", err) + } + return oldValue.Updated, nil +} + +// ResetUpdated resets all changes to the "updated" field. +func (m *EnvVarMutation) ResetUpdated() { + m.updated = nil +} + +// Where appends a list predicates to the EnvVarMutation builder. +func (m *EnvVarMutation) Where(ps ...predicate.EnvVar) { + m.predicates = append(m.predicates, ps...) +} + +// WhereP appends storage-level predicates to the EnvVarMutation builder. Using this method, +// users can use type-assertion to append predicates that do not depend on any generated package. +func (m *EnvVarMutation) WhereP(ps ...func(*sql.Selector)) { + p := make([]predicate.EnvVar, len(ps)) + for i := range ps { + p[i] = ps[i] + } + m.Where(p...) +} + +// Op returns the operation name. +func (m *EnvVarMutation) Op() Op { + return m.op +} + +// SetOp allows setting the mutation operation. +func (m *EnvVarMutation) SetOp(op Op) { + m.op = op +} + +// Type returns the node type of this mutation (EnvVar). +func (m *EnvVarMutation) Type() string { + return m.typ +} + +// Fields returns all fields that were changed during this mutation. Note that in +// order to get all numeric fields that were incremented/decremented, call +// AddedFields(). +func (m *EnvVarMutation) Fields() []string { + fields := make([]string, 0, 11) + if m.key != nil { + fields = append(fields, envvar.FieldKey) + } + if m.value != nil { + fields = append(fields, envvar.FieldValue) + } + if m.scope != nil { + fields = append(fields, envvar.FieldScope) + } + if m.scope_id != nil { + fields = append(fields, envvar.FieldScopeID) + } + if m.description != nil { + fields = append(fields, envvar.FieldDescription) + } + if m.sensitive != nil { + fields = append(fields, envvar.FieldSensitive) + } + if m.injection_mode != nil { + fields = append(fields, envvar.FieldInjectionMode) + } + if m.secret != nil { + fields = append(fields, envvar.FieldSecret) + } + if m.created_by != nil { + fields = append(fields, envvar.FieldCreatedBy) + } + if m.created != nil { + fields = append(fields, envvar.FieldCreated) + } + if m.updated != nil { + fields = append(fields, envvar.FieldUpdated) + } + return fields +} + +// Field returns the value of a field with the given name. The second boolean +// return value indicates that this field was not set, or was not defined in the +// schema. +func (m *EnvVarMutation) Field(name string) (ent.Value, bool) { + switch name { + case envvar.FieldKey: + return m.Key() + case envvar.FieldValue: + return m.Value() + case envvar.FieldScope: + return m.Scope() + case envvar.FieldScopeID: + return m.ScopeID() + case envvar.FieldDescription: + return m.Description() + case envvar.FieldSensitive: + return m.Sensitive() + case envvar.FieldInjectionMode: + return m.InjectionMode() + case envvar.FieldSecret: + return m.Secret() + case envvar.FieldCreatedBy: + return m.CreatedBy() + case envvar.FieldCreated: + return m.Created() + case envvar.FieldUpdated: + return m.Updated() + } + return nil, false +} + +// OldField returns the old value of the field from the database. An error is +// returned if the mutation operation is not UpdateOne, or the query to the +// database failed. +func (m *EnvVarMutation) OldField(ctx context.Context, name string) (ent.Value, error) { + switch name { + case envvar.FieldKey: + return m.OldKey(ctx) + case envvar.FieldValue: + return m.OldValue(ctx) + case envvar.FieldScope: + return m.OldScope(ctx) + case envvar.FieldScopeID: + return m.OldScopeID(ctx) + case envvar.FieldDescription: + return m.OldDescription(ctx) + case envvar.FieldSensitive: + return m.OldSensitive(ctx) + case envvar.FieldInjectionMode: + return m.OldInjectionMode(ctx) + case envvar.FieldSecret: + return m.OldSecret(ctx) + case envvar.FieldCreatedBy: + return m.OldCreatedBy(ctx) + case envvar.FieldCreated: + return m.OldCreated(ctx) + case envvar.FieldUpdated: + return m.OldUpdated(ctx) + } + return nil, fmt.Errorf("unknown EnvVar field %s", name) +} + +// SetField sets the value of a field with the given name. It returns an error if +// the field is not defined in the schema, or if the type mismatched the field +// type. +func (m *EnvVarMutation) SetField(name string, value ent.Value) error { + switch name { + case envvar.FieldKey: + v, ok := value.(string) + if !ok { + return fmt.Errorf("unexpected type %T for field %s", value, name) + } + m.SetKey(v) + return nil + case envvar.FieldValue: + v, ok := value.(string) + if !ok { + return fmt.Errorf("unexpected type %T for field %s", value, name) + } + m.SetValue(v) + return nil + case envvar.FieldScope: + v, ok := value.(string) + if !ok { + return fmt.Errorf("unexpected type %T for field %s", value, name) + } + m.SetScope(v) + return nil + case envvar.FieldScopeID: + v, ok := value.(string) + if !ok { + return fmt.Errorf("unexpected type %T for field %s", value, name) + } + m.SetScopeID(v) + return nil + case envvar.FieldDescription: + v, ok := value.(string) + if !ok { + return fmt.Errorf("unexpected type %T for field %s", value, name) + } + m.SetDescription(v) + return nil + case envvar.FieldSensitive: + v, ok := value.(bool) + if !ok { + return fmt.Errorf("unexpected type %T for field %s", value, name) + } + m.SetSensitive(v) + return nil + case envvar.FieldInjectionMode: + v, ok := value.(envvar.InjectionMode) + if !ok { + return fmt.Errorf("unexpected type %T for field %s", value, name) + } + m.SetInjectionMode(v) + return nil + case envvar.FieldSecret: + v, ok := value.(bool) + if !ok { + return fmt.Errorf("unexpected type %T for field %s", value, name) + } + m.SetSecret(v) + return nil + case envvar.FieldCreatedBy: + v, ok := value.(string) + if !ok { + return fmt.Errorf("unexpected type %T for field %s", value, name) + } + m.SetCreatedBy(v) + return nil + case envvar.FieldCreated: + v, ok := value.(time.Time) + if !ok { + return fmt.Errorf("unexpected type %T for field %s", value, name) + } + m.SetCreated(v) + return nil + case envvar.FieldUpdated: + v, ok := value.(time.Time) + if !ok { + return fmt.Errorf("unexpected type %T for field %s", value, name) + } + m.SetUpdated(v) + return nil + } + return fmt.Errorf("unknown EnvVar field %s", name) +} + +// AddedFields returns all numeric fields that were incremented/decremented during +// this mutation. +func (m *EnvVarMutation) AddedFields() []string { + return nil +} + +// AddedField returns the numeric value that was incremented/decremented on a field +// with the given name. The second boolean return value indicates that this field +// was not set, or was not defined in the schema. +func (m *EnvVarMutation) AddedField(name string) (ent.Value, bool) { + return nil, false +} + +// AddField adds the value to the field with the given name. It returns an error if +// the field is not defined in the schema, or if the type mismatched the field +// type. +func (m *EnvVarMutation) AddField(name string, value ent.Value) error { + switch name { + } + return fmt.Errorf("unknown EnvVar numeric field %s", name) +} + +// ClearedFields returns all nullable fields that were cleared during this +// mutation. +func (m *EnvVarMutation) ClearedFields() []string { + var fields []string + if m.FieldCleared(envvar.FieldDescription) { + fields = append(fields, envvar.FieldDescription) + } + if m.FieldCleared(envvar.FieldCreatedBy) { + fields = append(fields, envvar.FieldCreatedBy) + } + return fields +} + +// FieldCleared returns a boolean indicating if a field with the given name was +// cleared in this mutation. +func (m *EnvVarMutation) FieldCleared(name string) bool { + _, ok := m.clearedFields[name] + return ok +} + +// ClearField clears the value of the field with the given name. It returns an +// error if the field is not defined in the schema. +func (m *EnvVarMutation) ClearField(name string) error { + switch name { + case envvar.FieldDescription: + m.ClearDescription() + return nil + case envvar.FieldCreatedBy: + m.ClearCreatedBy() + return nil + } + return fmt.Errorf("unknown EnvVar nullable field %s", name) +} + +// ResetField resets all changes in the mutation for the field with the given name. +// It returns an error if the field is not defined in the schema. +func (m *EnvVarMutation) ResetField(name string) error { + switch name { + case envvar.FieldKey: + m.ResetKey() + return nil + case envvar.FieldValue: + m.ResetValue() + return nil + case envvar.FieldScope: + m.ResetScope() + return nil + case envvar.FieldScopeID: + m.ResetScopeID() + return nil + case envvar.FieldDescription: + m.ResetDescription() + return nil + case envvar.FieldSensitive: + m.ResetSensitive() + return nil + case envvar.FieldInjectionMode: + m.ResetInjectionMode() + return nil + case envvar.FieldSecret: + m.ResetSecret() + return nil + case envvar.FieldCreatedBy: + m.ResetCreatedBy() + return nil + case envvar.FieldCreated: + m.ResetCreated() + return nil + case envvar.FieldUpdated: + m.ResetUpdated() + return nil + } + return fmt.Errorf("unknown EnvVar field %s", name) +} + +// AddedEdges returns all edge names that were set/added in this mutation. +func (m *EnvVarMutation) AddedEdges() []string { + edges := make([]string, 0, 0) + return edges +} + +// AddedIDs returns all IDs (to other nodes) that were added for the given edge +// name in this mutation. +func (m *EnvVarMutation) AddedIDs(name string) []ent.Value { + return nil +} + +// RemovedEdges returns all edge names that were removed in this mutation. +func (m *EnvVarMutation) RemovedEdges() []string { + edges := make([]string, 0, 0) + return edges +} + +// RemovedIDs returns all IDs (to other nodes) that were removed for the edge with +// the given name in this mutation. +func (m *EnvVarMutation) RemovedIDs(name string) []ent.Value { + return nil +} + +// ClearedEdges returns all edge names that were cleared in this mutation. +func (m *EnvVarMutation) ClearedEdges() []string { + edges := make([]string, 0, 0) + return edges +} + +// EdgeCleared returns a boolean which indicates if the edge with the given name +// was cleared in this mutation. +func (m *EnvVarMutation) EdgeCleared(name string) bool { + return false +} + +// ClearEdge clears the value of the edge with the given name. It returns an error +// if that edge is not defined in the schema. +func (m *EnvVarMutation) ClearEdge(name string) error { + return fmt.Errorf("unknown EnvVar unique edge %s", name) +} + +// ResetEdge resets all changes to the edge with the given name in this mutation. +// It returns an error if the edge is not defined in the schema. +func (m *EnvVarMutation) ResetEdge(name string) error { + return fmt.Errorf("unknown EnvVar edge %s", name) +} + +// GCPServiceAccountMutation represents an operation that mutates the GCPServiceAccount nodes in the graph. +type GCPServiceAccountMutation struct { + config + op Op + typ string + id *uuid.UUID + scope *string + scope_id *string + email *string + project_id *string + display_name *string + default_scopes *string + verified *bool + verified_at *time.Time + created_by *string + managed *bool + managed_by *string + created *time.Time + clearedFields map[string]struct{} + done bool + oldValue func(context.Context) (*GCPServiceAccount, error) + predicates []predicate.GCPServiceAccount +} + +var _ ent.Mutation = (*GCPServiceAccountMutation)(nil) + +// gcpserviceaccountOption allows management of the mutation configuration using functional options. +type gcpserviceaccountOption func(*GCPServiceAccountMutation) + +// newGCPServiceAccountMutation creates new mutation for the GCPServiceAccount entity. +func newGCPServiceAccountMutation(c config, op Op, opts ...gcpserviceaccountOption) *GCPServiceAccountMutation { + m := &GCPServiceAccountMutation{ + config: c, + op: op, + typ: TypeGCPServiceAccount, + clearedFields: make(map[string]struct{}), + } + for _, opt := range opts { + opt(m) + } + return m +} + +// withGCPServiceAccountID sets the ID field of the mutation. +func withGCPServiceAccountID(id uuid.UUID) gcpserviceaccountOption { + return func(m *GCPServiceAccountMutation) { + var ( + err error + once sync.Once + value *GCPServiceAccount + ) + m.oldValue = func(ctx context.Context) (*GCPServiceAccount, error) { + once.Do(func() { + if m.done { + err = errors.New("querying old values post mutation is not allowed") + } else { + value, err = m.Client().GCPServiceAccount.Get(ctx, id) + } + }) + return value, err + } + m.id = &id + } +} + +// withGCPServiceAccount sets the old GCPServiceAccount of the mutation. +func withGCPServiceAccount(node *GCPServiceAccount) gcpserviceaccountOption { + return func(m *GCPServiceAccountMutation) { + m.oldValue = func(context.Context) (*GCPServiceAccount, error) { + return node, nil + } + m.id = &node.ID + } +} + +// Client returns a new `ent.Client` from the mutation. If the mutation was +// executed in a transaction (ent.Tx), a transactional client is returned. +func (m GCPServiceAccountMutation) Client() *Client { + client := &Client{config: m.config} + client.init() + return client +} + +// Tx returns an `ent.Tx` for mutations that were executed in transactions; +// it returns an error otherwise. +func (m GCPServiceAccountMutation) Tx() (*Tx, error) { + if _, ok := m.driver.(*txDriver); !ok { + return nil, errors.New("ent: mutation is not running in a transaction") + } + tx := &Tx{config: m.config} + tx.init() + return tx, nil +} + +// SetID sets the value of the id field. Note that this +// operation is only accepted on creation of GCPServiceAccount entities. +func (m *GCPServiceAccountMutation) SetID(id uuid.UUID) { + m.id = &id +} + +// ID returns the ID value in the mutation. Note that the ID is only available +// if it was provided to the builder or after it was returned from the database. +func (m *GCPServiceAccountMutation) ID() (id uuid.UUID, exists bool) { + if m.id == nil { + return + } + return *m.id, true +} + +// IDs queries the database and returns the entity ids that match the mutation's predicate. +// That means, if the mutation is applied within a transaction with an isolation level such +// as sql.LevelSerializable, the returned ids match the ids of the rows that will be updated +// or updated by the mutation. +func (m *GCPServiceAccountMutation) IDs(ctx context.Context) ([]uuid.UUID, error) { + switch { + case m.op.Is(OpUpdateOne | OpDeleteOne): + id, exists := m.ID() + if exists { + return []uuid.UUID{id}, nil + } + fallthrough + case m.op.Is(OpUpdate | OpDelete): + return m.Client().GCPServiceAccount.Query().Where(m.predicates...).IDs(ctx) + default: + return nil, fmt.Errorf("IDs is not allowed on %s operations", m.op) + } +} + +// SetScope sets the "scope" field. +func (m *GCPServiceAccountMutation) SetScope(s string) { + m.scope = &s +} + +// Scope returns the value of the "scope" field in the mutation. +func (m *GCPServiceAccountMutation) Scope() (r string, exists bool) { + v := m.scope + if v == nil { + return + } + return *v, true +} + +// OldScope returns the old "scope" field's value of the GCPServiceAccount entity. +// If the GCPServiceAccount object wasn't provided to the builder, the object is fetched from the database. +// An error is returned if the mutation operation is not UpdateOne, or the database query fails. +func (m *GCPServiceAccountMutation) OldScope(ctx context.Context) (v string, err error) { + if !m.op.Is(OpUpdateOne) { + return v, errors.New("OldScope is only allowed on UpdateOne operations") + } + if m.id == nil || m.oldValue == nil { + return v, errors.New("OldScope requires an ID field in the mutation") + } + oldValue, err := m.oldValue(ctx) + if err != nil { + return v, fmt.Errorf("querying old value for OldScope: %w", err) + } + return oldValue.Scope, nil +} + +// ResetScope resets all changes to the "scope" field. +func (m *GCPServiceAccountMutation) ResetScope() { + m.scope = nil +} + +// SetScopeID sets the "scope_id" field. +func (m *GCPServiceAccountMutation) SetScopeID(s string) { + m.scope_id = &s +} + +// ScopeID returns the value of the "scope_id" field in the mutation. +func (m *GCPServiceAccountMutation) ScopeID() (r string, exists bool) { + v := m.scope_id + if v == nil { + return + } + return *v, true +} + +// OldScopeID returns the old "scope_id" field's value of the GCPServiceAccount entity. +// If the GCPServiceAccount object wasn't provided to the builder, the object is fetched from the database. +// An error is returned if the mutation operation is not UpdateOne, or the database query fails. +func (m *GCPServiceAccountMutation) OldScopeID(ctx context.Context) (v string, err error) { + if !m.op.Is(OpUpdateOne) { + return v, errors.New("OldScopeID is only allowed on UpdateOne operations") + } + if m.id == nil || m.oldValue == nil { + return v, errors.New("OldScopeID requires an ID field in the mutation") + } + oldValue, err := m.oldValue(ctx) + if err != nil { + return v, fmt.Errorf("querying old value for OldScopeID: %w", err) + } + return oldValue.ScopeID, nil +} + +// ResetScopeID resets all changes to the "scope_id" field. +func (m *GCPServiceAccountMutation) ResetScopeID() { + m.scope_id = nil +} + +// SetEmail sets the "email" field. +func (m *GCPServiceAccountMutation) SetEmail(s string) { + m.email = &s +} + +// Email returns the value of the "email" field in the mutation. +func (m *GCPServiceAccountMutation) Email() (r string, exists bool) { + v := m.email + if v == nil { + return + } + return *v, true +} + +// OldEmail returns the old "email" field's value of the GCPServiceAccount entity. +// If the GCPServiceAccount object wasn't provided to the builder, the object is fetched from the database. +// An error is returned if the mutation operation is not UpdateOne, or the database query fails. +func (m *GCPServiceAccountMutation) OldEmail(ctx context.Context) (v string, err error) { + if !m.op.Is(OpUpdateOne) { + return v, errors.New("OldEmail is only allowed on UpdateOne operations") + } + if m.id == nil || m.oldValue == nil { + return v, errors.New("OldEmail requires an ID field in the mutation") + } + oldValue, err := m.oldValue(ctx) + if err != nil { + return v, fmt.Errorf("querying old value for OldEmail: %w", err) + } + return oldValue.Email, nil +} + +// ResetEmail resets all changes to the "email" field. +func (m *GCPServiceAccountMutation) ResetEmail() { + m.email = nil +} + +// SetProjectID sets the "project_id" field. +func (m *GCPServiceAccountMutation) SetProjectID(s string) { + m.project_id = &s +} + +// ProjectID returns the value of the "project_id" field in the mutation. +func (m *GCPServiceAccountMutation) ProjectID() (r string, exists bool) { + v := m.project_id + if v == nil { + return + } + return *v, true +} + +// OldProjectID returns the old "project_id" field's value of the GCPServiceAccount entity. +// If the GCPServiceAccount object wasn't provided to the builder, the object is fetched from the database. +// An error is returned if the mutation operation is not UpdateOne, or the database query fails. +func (m *GCPServiceAccountMutation) OldProjectID(ctx context.Context) (v string, err error) { + if !m.op.Is(OpUpdateOne) { + return v, errors.New("OldProjectID is only allowed on UpdateOne operations") + } + if m.id == nil || m.oldValue == nil { + return v, errors.New("OldProjectID requires an ID field in the mutation") + } + oldValue, err := m.oldValue(ctx) + if err != nil { + return v, fmt.Errorf("querying old value for OldProjectID: %w", err) + } + return oldValue.ProjectID, nil +} + +// ResetProjectID resets all changes to the "project_id" field. +func (m *GCPServiceAccountMutation) ResetProjectID() { + m.project_id = nil +} + +// SetDisplayName sets the "display_name" field. +func (m *GCPServiceAccountMutation) SetDisplayName(s string) { + m.display_name = &s +} + +// DisplayName returns the value of the "display_name" field in the mutation. +func (m *GCPServiceAccountMutation) DisplayName() (r string, exists bool) { + v := m.display_name + if v == nil { + return + } + return *v, true +} + +// OldDisplayName returns the old "display_name" field's value of the GCPServiceAccount entity. +// If the GCPServiceAccount object wasn't provided to the builder, the object is fetched from the database. +// An error is returned if the mutation operation is not UpdateOne, or the database query fails. +func (m *GCPServiceAccountMutation) OldDisplayName(ctx context.Context) (v string, err error) { + if !m.op.Is(OpUpdateOne) { + return v, errors.New("OldDisplayName is only allowed on UpdateOne operations") + } + if m.id == nil || m.oldValue == nil { + return v, errors.New("OldDisplayName requires an ID field in the mutation") + } + oldValue, err := m.oldValue(ctx) + if err != nil { + return v, fmt.Errorf("querying old value for OldDisplayName: %w", err) + } + return oldValue.DisplayName, nil +} + +// ResetDisplayName resets all changes to the "display_name" field. +func (m *GCPServiceAccountMutation) ResetDisplayName() { + m.display_name = nil +} + +// SetDefaultScopes sets the "default_scopes" field. +func (m *GCPServiceAccountMutation) SetDefaultScopes(s string) { + m.default_scopes = &s +} + +// DefaultScopes returns the value of the "default_scopes" field in the mutation. +func (m *GCPServiceAccountMutation) DefaultScopes() (r string, exists bool) { + v := m.default_scopes + if v == nil { + return + } + return *v, true +} + +// OldDefaultScopes returns the old "default_scopes" field's value of the GCPServiceAccount entity. +// If the GCPServiceAccount object wasn't provided to the builder, the object is fetched from the database. +// An error is returned if the mutation operation is not UpdateOne, or the database query fails. +func (m *GCPServiceAccountMutation) OldDefaultScopes(ctx context.Context) (v string, err error) { + if !m.op.Is(OpUpdateOne) { + return v, errors.New("OldDefaultScopes is only allowed on UpdateOne operations") + } + if m.id == nil || m.oldValue == nil { + return v, errors.New("OldDefaultScopes requires an ID field in the mutation") + } + oldValue, err := m.oldValue(ctx) + if err != nil { + return v, fmt.Errorf("querying old value for OldDefaultScopes: %w", err) + } + return oldValue.DefaultScopes, nil +} + +// ResetDefaultScopes resets all changes to the "default_scopes" field. +func (m *GCPServiceAccountMutation) ResetDefaultScopes() { + m.default_scopes = nil +} + +// SetVerified sets the "verified" field. +func (m *GCPServiceAccountMutation) SetVerified(b bool) { + m.verified = &b +} + +// Verified returns the value of the "verified" field in the mutation. +func (m *GCPServiceAccountMutation) Verified() (r bool, exists bool) { + v := m.verified + if v == nil { + return + } + return *v, true +} + +// OldVerified returns the old "verified" field's value of the GCPServiceAccount entity. +// If the GCPServiceAccount object wasn't provided to the builder, the object is fetched from the database. +// An error is returned if the mutation operation is not UpdateOne, or the database query fails. +func (m *GCPServiceAccountMutation) OldVerified(ctx context.Context) (v bool, err error) { + if !m.op.Is(OpUpdateOne) { + return v, errors.New("OldVerified is only allowed on UpdateOne operations") + } + if m.id == nil || m.oldValue == nil { + return v, errors.New("OldVerified requires an ID field in the mutation") + } + oldValue, err := m.oldValue(ctx) + if err != nil { + return v, fmt.Errorf("querying old value for OldVerified: %w", err) + } + return oldValue.Verified, nil +} + +// ResetVerified resets all changes to the "verified" field. +func (m *GCPServiceAccountMutation) ResetVerified() { + m.verified = nil +} + +// SetVerifiedAt sets the "verified_at" field. +func (m *GCPServiceAccountMutation) SetVerifiedAt(t time.Time) { + m.verified_at = &t +} + +// VerifiedAt returns the value of the "verified_at" field in the mutation. +func (m *GCPServiceAccountMutation) VerifiedAt() (r time.Time, exists bool) { + v := m.verified_at + if v == nil { + return + } + return *v, true +} + +// OldVerifiedAt returns the old "verified_at" field's value of the GCPServiceAccount entity. +// If the GCPServiceAccount object wasn't provided to the builder, the object is fetched from the database. +// An error is returned if the mutation operation is not UpdateOne, or the database query fails. +func (m *GCPServiceAccountMutation) OldVerifiedAt(ctx context.Context) (v *time.Time, err error) { + if !m.op.Is(OpUpdateOne) { + return v, errors.New("OldVerifiedAt is only allowed on UpdateOne operations") + } + if m.id == nil || m.oldValue == nil { + return v, errors.New("OldVerifiedAt requires an ID field in the mutation") + } + oldValue, err := m.oldValue(ctx) + if err != nil { + return v, fmt.Errorf("querying old value for OldVerifiedAt: %w", err) + } + return oldValue.VerifiedAt, nil +} + +// ClearVerifiedAt clears the value of the "verified_at" field. +func (m *GCPServiceAccountMutation) ClearVerifiedAt() { + m.verified_at = nil + m.clearedFields[gcpserviceaccount.FieldVerifiedAt] = struct{}{} +} + +// VerifiedAtCleared returns if the "verified_at" field was cleared in this mutation. +func (m *GCPServiceAccountMutation) VerifiedAtCleared() bool { + _, ok := m.clearedFields[gcpserviceaccount.FieldVerifiedAt] + return ok +} + +// ResetVerifiedAt resets all changes to the "verified_at" field. +func (m *GCPServiceAccountMutation) ResetVerifiedAt() { + m.verified_at = nil + delete(m.clearedFields, gcpserviceaccount.FieldVerifiedAt) +} + +// SetCreatedBy sets the "created_by" field. +func (m *GCPServiceAccountMutation) SetCreatedBy(s string) { + m.created_by = &s +} + +// CreatedBy returns the value of the "created_by" field in the mutation. +func (m *GCPServiceAccountMutation) CreatedBy() (r string, exists bool) { + v := m.created_by + if v == nil { + return + } + return *v, true +} + +// OldCreatedBy returns the old "created_by" field's value of the GCPServiceAccount entity. +// If the GCPServiceAccount object wasn't provided to the builder, the object is fetched from the database. +// An error is returned if the mutation operation is not UpdateOne, or the database query fails. +func (m *GCPServiceAccountMutation) OldCreatedBy(ctx context.Context) (v string, err error) { + if !m.op.Is(OpUpdateOne) { + return v, errors.New("OldCreatedBy is only allowed on UpdateOne operations") + } + if m.id == nil || m.oldValue == nil { + return v, errors.New("OldCreatedBy requires an ID field in the mutation") + } + oldValue, err := m.oldValue(ctx) + if err != nil { + return v, fmt.Errorf("querying old value for OldCreatedBy: %w", err) + } + return oldValue.CreatedBy, nil +} + +// ResetCreatedBy resets all changes to the "created_by" field. +func (m *GCPServiceAccountMutation) ResetCreatedBy() { + m.created_by = nil +} + +// SetManaged sets the "managed" field. +func (m *GCPServiceAccountMutation) SetManaged(b bool) { + m.managed = &b +} + +// Managed returns the value of the "managed" field in the mutation. +func (m *GCPServiceAccountMutation) Managed() (r bool, exists bool) { + v := m.managed + if v == nil { + return + } + return *v, true +} + +// OldManaged returns the old "managed" field's value of the GCPServiceAccount entity. +// If the GCPServiceAccount object wasn't provided to the builder, the object is fetched from the database. +// An error is returned if the mutation operation is not UpdateOne, or the database query fails. +func (m *GCPServiceAccountMutation) OldManaged(ctx context.Context) (v bool, err error) { + if !m.op.Is(OpUpdateOne) { + return v, errors.New("OldManaged is only allowed on UpdateOne operations") + } + if m.id == nil || m.oldValue == nil { + return v, errors.New("OldManaged requires an ID field in the mutation") + } + oldValue, err := m.oldValue(ctx) + if err != nil { + return v, fmt.Errorf("querying old value for OldManaged: %w", err) + } + return oldValue.Managed, nil +} + +// ResetManaged resets all changes to the "managed" field. +func (m *GCPServiceAccountMutation) ResetManaged() { + m.managed = nil +} + +// SetManagedBy sets the "managed_by" field. +func (m *GCPServiceAccountMutation) SetManagedBy(s string) { + m.managed_by = &s +} + +// ManagedBy returns the value of the "managed_by" field in the mutation. +func (m *GCPServiceAccountMutation) ManagedBy() (r string, exists bool) { + v := m.managed_by + if v == nil { + return + } + return *v, true +} + +// OldManagedBy returns the old "managed_by" field's value of the GCPServiceAccount entity. +// If the GCPServiceAccount object wasn't provided to the builder, the object is fetched from the database. +// An error is returned if the mutation operation is not UpdateOne, or the database query fails. +func (m *GCPServiceAccountMutation) OldManagedBy(ctx context.Context) (v string, err error) { + if !m.op.Is(OpUpdateOne) { + return v, errors.New("OldManagedBy is only allowed on UpdateOne operations") + } + if m.id == nil || m.oldValue == nil { + return v, errors.New("OldManagedBy requires an ID field in the mutation") + } + oldValue, err := m.oldValue(ctx) + if err != nil { + return v, fmt.Errorf("querying old value for OldManagedBy: %w", err) + } + return oldValue.ManagedBy, nil +} + +// ResetManagedBy resets all changes to the "managed_by" field. +func (m *GCPServiceAccountMutation) ResetManagedBy() { + m.managed_by = nil +} + +// SetCreated sets the "created" field. +func (m *GCPServiceAccountMutation) SetCreated(t time.Time) { + m.created = &t +} + +// Created returns the value of the "created" field in the mutation. +func (m *GCPServiceAccountMutation) Created() (r time.Time, exists bool) { + v := m.created + if v == nil { + return + } + return *v, true +} + +// OldCreated returns the old "created" field's value of the GCPServiceAccount entity. +// If the GCPServiceAccount object wasn't provided to the builder, the object is fetched from the database. +// An error is returned if the mutation operation is not UpdateOne, or the database query fails. +func (m *GCPServiceAccountMutation) OldCreated(ctx context.Context) (v time.Time, err error) { + if !m.op.Is(OpUpdateOne) { + return v, errors.New("OldCreated is only allowed on UpdateOne operations") + } + if m.id == nil || m.oldValue == nil { + return v, errors.New("OldCreated requires an ID field in the mutation") + } + oldValue, err := m.oldValue(ctx) + if err != nil { + return v, fmt.Errorf("querying old value for OldCreated: %w", err) + } + return oldValue.Created, nil +} + +// ResetCreated resets all changes to the "created" field. +func (m *GCPServiceAccountMutation) ResetCreated() { + m.created = nil +} + +// Where appends a list predicates to the GCPServiceAccountMutation builder. +func (m *GCPServiceAccountMutation) Where(ps ...predicate.GCPServiceAccount) { + m.predicates = append(m.predicates, ps...) +} + +// WhereP appends storage-level predicates to the GCPServiceAccountMutation builder. Using this method, +// users can use type-assertion to append predicates that do not depend on any generated package. +func (m *GCPServiceAccountMutation) WhereP(ps ...func(*sql.Selector)) { + p := make([]predicate.GCPServiceAccount, len(ps)) + for i := range ps { + p[i] = ps[i] + } + m.Where(p...) +} + +// Op returns the operation name. +func (m *GCPServiceAccountMutation) Op() Op { + return m.op +} + +// SetOp allows setting the mutation operation. +func (m *GCPServiceAccountMutation) SetOp(op Op) { + m.op = op +} + +// Type returns the node type of this mutation (GCPServiceAccount). +func (m *GCPServiceAccountMutation) Type() string { + return m.typ +} + +// Fields returns all fields that were changed during this mutation. Note that in +// order to get all numeric fields that were incremented/decremented, call +// AddedFields(). +func (m *GCPServiceAccountMutation) Fields() []string { + fields := make([]string, 0, 12) + if m.scope != nil { + fields = append(fields, gcpserviceaccount.FieldScope) + } + if m.scope_id != nil { + fields = append(fields, gcpserviceaccount.FieldScopeID) + } + if m.email != nil { + fields = append(fields, gcpserviceaccount.FieldEmail) + } + if m.project_id != nil { + fields = append(fields, gcpserviceaccount.FieldProjectID) + } + if m.display_name != nil { + fields = append(fields, gcpserviceaccount.FieldDisplayName) + } + if m.default_scopes != nil { + fields = append(fields, gcpserviceaccount.FieldDefaultScopes) + } + if m.verified != nil { + fields = append(fields, gcpserviceaccount.FieldVerified) + } + if m.verified_at != nil { + fields = append(fields, gcpserviceaccount.FieldVerifiedAt) + } + if m.created_by != nil { + fields = append(fields, gcpserviceaccount.FieldCreatedBy) + } + if m.managed != nil { + fields = append(fields, gcpserviceaccount.FieldManaged) + } + if m.managed_by != nil { + fields = append(fields, gcpserviceaccount.FieldManagedBy) + } + if m.created != nil { + fields = append(fields, gcpserviceaccount.FieldCreated) + } + return fields +} + +// Field returns the value of a field with the given name. The second boolean +// return value indicates that this field was not set, or was not defined in the +// schema. +func (m *GCPServiceAccountMutation) Field(name string) (ent.Value, bool) { + switch name { + case gcpserviceaccount.FieldScope: + return m.Scope() + case gcpserviceaccount.FieldScopeID: + return m.ScopeID() + case gcpserviceaccount.FieldEmail: + return m.Email() + case gcpserviceaccount.FieldProjectID: + return m.ProjectID() + case gcpserviceaccount.FieldDisplayName: + return m.DisplayName() + case gcpserviceaccount.FieldDefaultScopes: + return m.DefaultScopes() + case gcpserviceaccount.FieldVerified: + return m.Verified() + case gcpserviceaccount.FieldVerifiedAt: + return m.VerifiedAt() + case gcpserviceaccount.FieldCreatedBy: + return m.CreatedBy() + case gcpserviceaccount.FieldManaged: + return m.Managed() + case gcpserviceaccount.FieldManagedBy: + return m.ManagedBy() + case gcpserviceaccount.FieldCreated: + return m.Created() + } + return nil, false +} + +// OldField returns the old value of the field from the database. An error is +// returned if the mutation operation is not UpdateOne, or the query to the +// database failed. +func (m *GCPServiceAccountMutation) OldField(ctx context.Context, name string) (ent.Value, error) { + switch name { + case gcpserviceaccount.FieldScope: + return m.OldScope(ctx) + case gcpserviceaccount.FieldScopeID: + return m.OldScopeID(ctx) + case gcpserviceaccount.FieldEmail: + return m.OldEmail(ctx) + case gcpserviceaccount.FieldProjectID: + return m.OldProjectID(ctx) + case gcpserviceaccount.FieldDisplayName: + return m.OldDisplayName(ctx) + case gcpserviceaccount.FieldDefaultScopes: + return m.OldDefaultScopes(ctx) + case gcpserviceaccount.FieldVerified: + return m.OldVerified(ctx) + case gcpserviceaccount.FieldVerifiedAt: + return m.OldVerifiedAt(ctx) + case gcpserviceaccount.FieldCreatedBy: + return m.OldCreatedBy(ctx) + case gcpserviceaccount.FieldManaged: + return m.OldManaged(ctx) + case gcpserviceaccount.FieldManagedBy: + return m.OldManagedBy(ctx) + case gcpserviceaccount.FieldCreated: + return m.OldCreated(ctx) + } + return nil, fmt.Errorf("unknown GCPServiceAccount field %s", name) +} + +// SetField sets the value of a field with the given name. It returns an error if +// the field is not defined in the schema, or if the type mismatched the field +// type. +func (m *GCPServiceAccountMutation) SetField(name string, value ent.Value) error { + switch name { + case gcpserviceaccount.FieldScope: + v, ok := value.(string) + if !ok { + return fmt.Errorf("unexpected type %T for field %s", value, name) + } + m.SetScope(v) + return nil + case gcpserviceaccount.FieldScopeID: + v, ok := value.(string) + if !ok { + return fmt.Errorf("unexpected type %T for field %s", value, name) + } + m.SetScopeID(v) + return nil + case gcpserviceaccount.FieldEmail: + v, ok := value.(string) + if !ok { + return fmt.Errorf("unexpected type %T for field %s", value, name) + } + m.SetEmail(v) + return nil + case gcpserviceaccount.FieldProjectID: + v, ok := value.(string) + if !ok { + return fmt.Errorf("unexpected type %T for field %s", value, name) + } + m.SetProjectID(v) + return nil + case gcpserviceaccount.FieldDisplayName: + v, ok := value.(string) + if !ok { + return fmt.Errorf("unexpected type %T for field %s", value, name) + } + m.SetDisplayName(v) + return nil + case gcpserviceaccount.FieldDefaultScopes: + v, ok := value.(string) + if !ok { + return fmt.Errorf("unexpected type %T for field %s", value, name) + } + m.SetDefaultScopes(v) + return nil + case gcpserviceaccount.FieldVerified: + v, ok := value.(bool) + if !ok { + return fmt.Errorf("unexpected type %T for field %s", value, name) + } + m.SetVerified(v) + return nil + case gcpserviceaccount.FieldVerifiedAt: + v, ok := value.(time.Time) + if !ok { + return fmt.Errorf("unexpected type %T for field %s", value, name) + } + m.SetVerifiedAt(v) + return nil + case gcpserviceaccount.FieldCreatedBy: + v, ok := value.(string) + if !ok { + return fmt.Errorf("unexpected type %T for field %s", value, name) + } + m.SetCreatedBy(v) + return nil + case gcpserviceaccount.FieldManaged: + v, ok := value.(bool) + if !ok { + return fmt.Errorf("unexpected type %T for field %s", value, name) + } + m.SetManaged(v) + return nil + case gcpserviceaccount.FieldManagedBy: + v, ok := value.(string) + if !ok { + return fmt.Errorf("unexpected type %T for field %s", value, name) + } + m.SetManagedBy(v) + return nil + case gcpserviceaccount.FieldCreated: + v, ok := value.(time.Time) + if !ok { + return fmt.Errorf("unexpected type %T for field %s", value, name) + } + m.SetCreated(v) + return nil + } + return fmt.Errorf("unknown GCPServiceAccount field %s", name) +} + +// AddedFields returns all numeric fields that were incremented/decremented during +// this mutation. +func (m *GCPServiceAccountMutation) AddedFields() []string { + return nil +} + +// AddedField returns the numeric value that was incremented/decremented on a field +// with the given name. The second boolean return value indicates that this field +// was not set, or was not defined in the schema. +func (m *GCPServiceAccountMutation) AddedField(name string) (ent.Value, bool) { + return nil, false +} + +// AddField adds the value to the field with the given name. It returns an error if +// the field is not defined in the schema, or if the type mismatched the field +// type. +func (m *GCPServiceAccountMutation) AddField(name string, value ent.Value) error { + switch name { + } + return fmt.Errorf("unknown GCPServiceAccount numeric field %s", name) +} + +// ClearedFields returns all nullable fields that were cleared during this +// mutation. +func (m *GCPServiceAccountMutation) ClearedFields() []string { + var fields []string + if m.FieldCleared(gcpserviceaccount.FieldVerifiedAt) { + fields = append(fields, gcpserviceaccount.FieldVerifiedAt) + } + return fields +} + +// FieldCleared returns a boolean indicating if a field with the given name was +// cleared in this mutation. +func (m *GCPServiceAccountMutation) FieldCleared(name string) bool { + _, ok := m.clearedFields[name] + return ok +} + +// ClearField clears the value of the field with the given name. It returns an +// error if the field is not defined in the schema. +func (m *GCPServiceAccountMutation) ClearField(name string) error { + switch name { + case gcpserviceaccount.FieldVerifiedAt: + m.ClearVerifiedAt() + return nil + } + return fmt.Errorf("unknown GCPServiceAccount nullable field %s", name) +} + +// ResetField resets all changes in the mutation for the field with the given name. +// It returns an error if the field is not defined in the schema. +func (m *GCPServiceAccountMutation) ResetField(name string) error { + switch name { + case gcpserviceaccount.FieldScope: + m.ResetScope() + return nil + case gcpserviceaccount.FieldScopeID: + m.ResetScopeID() + return nil + case gcpserviceaccount.FieldEmail: + m.ResetEmail() + return nil + case gcpserviceaccount.FieldProjectID: + m.ResetProjectID() + return nil + case gcpserviceaccount.FieldDisplayName: + m.ResetDisplayName() + return nil + case gcpserviceaccount.FieldDefaultScopes: + m.ResetDefaultScopes() + return nil + case gcpserviceaccount.FieldVerified: + m.ResetVerified() + return nil + case gcpserviceaccount.FieldVerifiedAt: + m.ResetVerifiedAt() + return nil + case gcpserviceaccount.FieldCreatedBy: + m.ResetCreatedBy() + return nil + case gcpserviceaccount.FieldManaged: + m.ResetManaged() + return nil + case gcpserviceaccount.FieldManagedBy: + m.ResetManagedBy() + return nil + case gcpserviceaccount.FieldCreated: + m.ResetCreated() + return nil + } + return fmt.Errorf("unknown GCPServiceAccount field %s", name) +} + +// AddedEdges returns all edge names that were set/added in this mutation. +func (m *GCPServiceAccountMutation) AddedEdges() []string { + edges := make([]string, 0, 0) + return edges +} + +// AddedIDs returns all IDs (to other nodes) that were added for the given edge +// name in this mutation. +func (m *GCPServiceAccountMutation) AddedIDs(name string) []ent.Value { + return nil +} + +// RemovedEdges returns all edge names that were removed in this mutation. +func (m *GCPServiceAccountMutation) RemovedEdges() []string { + edges := make([]string, 0, 0) + return edges +} + +// RemovedIDs returns all IDs (to other nodes) that were removed for the edge with +// the given name in this mutation. +func (m *GCPServiceAccountMutation) RemovedIDs(name string) []ent.Value { + return nil +} + +// ClearedEdges returns all edge names that were cleared in this mutation. +func (m *GCPServiceAccountMutation) ClearedEdges() []string { + edges := make([]string, 0, 0) + return edges +} + +// EdgeCleared returns a boolean which indicates if the edge with the given name +// was cleared in this mutation. +func (m *GCPServiceAccountMutation) EdgeCleared(name string) bool { + return false +} + +// ClearEdge clears the value of the edge with the given name. It returns an error +// if that edge is not defined in the schema. +func (m *GCPServiceAccountMutation) ClearEdge(name string) error { + return fmt.Errorf("unknown GCPServiceAccount unique edge %s", name) +} + +// ResetEdge resets all changes to the edge with the given name in this mutation. +// It returns an error if the edge is not defined in the schema. +func (m *GCPServiceAccountMutation) ResetEdge(name string) error { + return fmt.Errorf("unknown GCPServiceAccount edge %s", name) +} + +// GithubInstallationMutation represents an operation that mutates the GithubInstallation nodes in the graph. +type GithubInstallationMutation struct { + config + op Op + typ string + id *int64 + account_login *string + account_type *string + app_id *int64 + addapp_id *int64 + repositories *string + status *string + created *time.Time + updated *time.Time + clearedFields map[string]struct{} + done bool + oldValue func(context.Context) (*GithubInstallation, error) + predicates []predicate.GithubInstallation +} + +var _ ent.Mutation = (*GithubInstallationMutation)(nil) + +// githubinstallationOption allows management of the mutation configuration using functional options. +type githubinstallationOption func(*GithubInstallationMutation) + +// newGithubInstallationMutation creates new mutation for the GithubInstallation entity. +func newGithubInstallationMutation(c config, op Op, opts ...githubinstallationOption) *GithubInstallationMutation { + m := &GithubInstallationMutation{ + config: c, + op: op, + typ: TypeGithubInstallation, + clearedFields: make(map[string]struct{}), + } + for _, opt := range opts { + opt(m) + } + return m +} + +// withGithubInstallationID sets the ID field of the mutation. +func withGithubInstallationID(id int64) githubinstallationOption { + return func(m *GithubInstallationMutation) { + var ( + err error + once sync.Once + value *GithubInstallation + ) + m.oldValue = func(ctx context.Context) (*GithubInstallation, error) { + once.Do(func() { + if m.done { + err = errors.New("querying old values post mutation is not allowed") + } else { + value, err = m.Client().GithubInstallation.Get(ctx, id) + } + }) + return value, err + } + m.id = &id + } +} + +// withGithubInstallation sets the old GithubInstallation of the mutation. +func withGithubInstallation(node *GithubInstallation) githubinstallationOption { + return func(m *GithubInstallationMutation) { + m.oldValue = func(context.Context) (*GithubInstallation, error) { + return node, nil + } + m.id = &node.ID + } +} + +// Client returns a new `ent.Client` from the mutation. If the mutation was +// executed in a transaction (ent.Tx), a transactional client is returned. +func (m GithubInstallationMutation) Client() *Client { + client := &Client{config: m.config} + client.init() + return client +} + +// Tx returns an `ent.Tx` for mutations that were executed in transactions; +// it returns an error otherwise. +func (m GithubInstallationMutation) Tx() (*Tx, error) { + if _, ok := m.driver.(*txDriver); !ok { + return nil, errors.New("ent: mutation is not running in a transaction") + } + tx := &Tx{config: m.config} + tx.init() + return tx, nil +} + +// SetID sets the value of the id field. Note that this +// operation is only accepted on creation of GithubInstallation entities. +func (m *GithubInstallationMutation) SetID(id int64) { + m.id = &id +} + +// ID returns the ID value in the mutation. Note that the ID is only available +// if it was provided to the builder or after it was returned from the database. +func (m *GithubInstallationMutation) ID() (id int64, exists bool) { + if m.id == nil { + return + } + return *m.id, true +} + +// IDs queries the database and returns the entity ids that match the mutation's predicate. +// That means, if the mutation is applied within a transaction with an isolation level such +// as sql.LevelSerializable, the returned ids match the ids of the rows that will be updated +// or updated by the mutation. +func (m *GithubInstallationMutation) IDs(ctx context.Context) ([]int64, error) { + switch { + case m.op.Is(OpUpdateOne | OpDeleteOne): + id, exists := m.ID() + if exists { + return []int64{id}, nil + } + fallthrough + case m.op.Is(OpUpdate | OpDelete): + return m.Client().GithubInstallation.Query().Where(m.predicates...).IDs(ctx) + default: + return nil, fmt.Errorf("IDs is not allowed on %s operations", m.op) + } +} + +// SetAccountLogin sets the "account_login" field. +func (m *GithubInstallationMutation) SetAccountLogin(s string) { + m.account_login = &s +} + +// AccountLogin returns the value of the "account_login" field in the mutation. +func (m *GithubInstallationMutation) AccountLogin() (r string, exists bool) { + v := m.account_login + if v == nil { + return + } + return *v, true +} + +// OldAccountLogin returns the old "account_login" field's value of the GithubInstallation entity. +// If the GithubInstallation object wasn't provided to the builder, the object is fetched from the database. +// An error is returned if the mutation operation is not UpdateOne, or the database query fails. +func (m *GithubInstallationMutation) OldAccountLogin(ctx context.Context) (v string, err error) { + if !m.op.Is(OpUpdateOne) { + return v, errors.New("OldAccountLogin is only allowed on UpdateOne operations") + } + if m.id == nil || m.oldValue == nil { + return v, errors.New("OldAccountLogin requires an ID field in the mutation") + } + oldValue, err := m.oldValue(ctx) + if err != nil { + return v, fmt.Errorf("querying old value for OldAccountLogin: %w", err) + } + return oldValue.AccountLogin, nil +} + +// ResetAccountLogin resets all changes to the "account_login" field. +func (m *GithubInstallationMutation) ResetAccountLogin() { + m.account_login = nil +} + +// SetAccountType sets the "account_type" field. +func (m *GithubInstallationMutation) SetAccountType(s string) { + m.account_type = &s +} + +// AccountType returns the value of the "account_type" field in the mutation. +func (m *GithubInstallationMutation) AccountType() (r string, exists bool) { + v := m.account_type + if v == nil { + return + } + return *v, true +} + +// OldAccountType returns the old "account_type" field's value of the GithubInstallation entity. +// If the GithubInstallation object wasn't provided to the builder, the object is fetched from the database. +// An error is returned if the mutation operation is not UpdateOne, or the database query fails. +func (m *GithubInstallationMutation) OldAccountType(ctx context.Context) (v string, err error) { + if !m.op.Is(OpUpdateOne) { + return v, errors.New("OldAccountType is only allowed on UpdateOne operations") + } + if m.id == nil || m.oldValue == nil { + return v, errors.New("OldAccountType requires an ID field in the mutation") + } + oldValue, err := m.oldValue(ctx) + if err != nil { + return v, fmt.Errorf("querying old value for OldAccountType: %w", err) + } + return oldValue.AccountType, nil +} + +// ResetAccountType resets all changes to the "account_type" field. +func (m *GithubInstallationMutation) ResetAccountType() { + m.account_type = nil +} + +// SetAppID sets the "app_id" field. +func (m *GithubInstallationMutation) SetAppID(i int64) { + m.app_id = &i + m.addapp_id = nil +} + +// AppID returns the value of the "app_id" field in the mutation. +func (m *GithubInstallationMutation) AppID() (r int64, exists bool) { + v := m.app_id + if v == nil { + return + } + return *v, true +} + +// OldAppID returns the old "app_id" field's value of the GithubInstallation entity. +// If the GithubInstallation object wasn't provided to the builder, the object is fetched from the database. +// An error is returned if the mutation operation is not UpdateOne, or the database query fails. +func (m *GithubInstallationMutation) OldAppID(ctx context.Context) (v int64, err error) { + if !m.op.Is(OpUpdateOne) { + return v, errors.New("OldAppID is only allowed on UpdateOne operations") + } + if m.id == nil || m.oldValue == nil { + return v, errors.New("OldAppID requires an ID field in the mutation") + } + oldValue, err := m.oldValue(ctx) + if err != nil { + return v, fmt.Errorf("querying old value for OldAppID: %w", err) + } + return oldValue.AppID, nil +} + +// AddAppID adds i to the "app_id" field. +func (m *GithubInstallationMutation) AddAppID(i int64) { + if m.addapp_id != nil { + *m.addapp_id += i + } else { + m.addapp_id = &i + } +} + +// AddedAppID returns the value that was added to the "app_id" field in this mutation. +func (m *GithubInstallationMutation) AddedAppID() (r int64, exists bool) { + v := m.addapp_id + if v == nil { + return + } + return *v, true +} + +// ResetAppID resets all changes to the "app_id" field. +func (m *GithubInstallationMutation) ResetAppID() { + m.app_id = nil + m.addapp_id = nil +} + +// SetRepositories sets the "repositories" field. +func (m *GithubInstallationMutation) SetRepositories(s string) { + m.repositories = &s +} + +// Repositories returns the value of the "repositories" field in the mutation. +func (m *GithubInstallationMutation) Repositories() (r string, exists bool) { + v := m.repositories + if v == nil { + return + } + return *v, true +} + +// OldRepositories returns the old "repositories" field's value of the GithubInstallation entity. +// If the GithubInstallation object wasn't provided to the builder, the object is fetched from the database. +// An error is returned if the mutation operation is not UpdateOne, or the database query fails. +func (m *GithubInstallationMutation) OldRepositories(ctx context.Context) (v string, err error) { + if !m.op.Is(OpUpdateOne) { + return v, errors.New("OldRepositories is only allowed on UpdateOne operations") + } + if m.id == nil || m.oldValue == nil { + return v, errors.New("OldRepositories requires an ID field in the mutation") + } + oldValue, err := m.oldValue(ctx) + if err != nil { + return v, fmt.Errorf("querying old value for OldRepositories: %w", err) + } + return oldValue.Repositories, nil +} + +// ResetRepositories resets all changes to the "repositories" field. +func (m *GithubInstallationMutation) ResetRepositories() { + m.repositories = nil +} + +// SetStatus sets the "status" field. +func (m *GithubInstallationMutation) SetStatus(s string) { + m.status = &s +} + +// Status returns the value of the "status" field in the mutation. +func (m *GithubInstallationMutation) Status() (r string, exists bool) { + v := m.status + if v == nil { + return + } + return *v, true +} + +// OldStatus returns the old "status" field's value of the GithubInstallation entity. +// If the GithubInstallation object wasn't provided to the builder, the object is fetched from the database. +// An error is returned if the mutation operation is not UpdateOne, or the database query fails. +func (m *GithubInstallationMutation) OldStatus(ctx context.Context) (v string, err error) { + if !m.op.Is(OpUpdateOne) { + return v, errors.New("OldStatus is only allowed on UpdateOne operations") + } + if m.id == nil || m.oldValue == nil { + return v, errors.New("OldStatus requires an ID field in the mutation") + } + oldValue, err := m.oldValue(ctx) + if err != nil { + return v, fmt.Errorf("querying old value for OldStatus: %w", err) + } + return oldValue.Status, nil +} + +// ResetStatus resets all changes to the "status" field. +func (m *GithubInstallationMutation) ResetStatus() { + m.status = nil +} + +// SetCreated sets the "created" field. +func (m *GithubInstallationMutation) SetCreated(t time.Time) { + m.created = &t +} + +// Created returns the value of the "created" field in the mutation. +func (m *GithubInstallationMutation) Created() (r time.Time, exists bool) { + v := m.created + if v == nil { + return + } + return *v, true +} + +// OldCreated returns the old "created" field's value of the GithubInstallation entity. +// If the GithubInstallation object wasn't provided to the builder, the object is fetched from the database. +// An error is returned if the mutation operation is not UpdateOne, or the database query fails. +func (m *GithubInstallationMutation) OldCreated(ctx context.Context) (v time.Time, err error) { + if !m.op.Is(OpUpdateOne) { + return v, errors.New("OldCreated is only allowed on UpdateOne operations") + } + if m.id == nil || m.oldValue == nil { + return v, errors.New("OldCreated requires an ID field in the mutation") + } + oldValue, err := m.oldValue(ctx) + if err != nil { + return v, fmt.Errorf("querying old value for OldCreated: %w", err) + } + return oldValue.Created, nil +} + +// ResetCreated resets all changes to the "created" field. +func (m *GithubInstallationMutation) ResetCreated() { + m.created = nil +} + +// SetUpdated sets the "updated" field. +func (m *GithubInstallationMutation) SetUpdated(t time.Time) { + m.updated = &t +} + +// Updated returns the value of the "updated" field in the mutation. +func (m *GithubInstallationMutation) Updated() (r time.Time, exists bool) { + v := m.updated + if v == nil { + return + } + return *v, true +} + +// OldUpdated returns the old "updated" field's value of the GithubInstallation entity. +// If the GithubInstallation object wasn't provided to the builder, the object is fetched from the database. +// An error is returned if the mutation operation is not UpdateOne, or the database query fails. +func (m *GithubInstallationMutation) OldUpdated(ctx context.Context) (v time.Time, err error) { + if !m.op.Is(OpUpdateOne) { + return v, errors.New("OldUpdated is only allowed on UpdateOne operations") + } + if m.id == nil || m.oldValue == nil { + return v, errors.New("OldUpdated requires an ID field in the mutation") + } + oldValue, err := m.oldValue(ctx) + if err != nil { + return v, fmt.Errorf("querying old value for OldUpdated: %w", err) + } + return oldValue.Updated, nil +} + +// ResetUpdated resets all changes to the "updated" field. +func (m *GithubInstallationMutation) ResetUpdated() { + m.updated = nil +} + +// Where appends a list predicates to the GithubInstallationMutation builder. +func (m *GithubInstallationMutation) Where(ps ...predicate.GithubInstallation) { + m.predicates = append(m.predicates, ps...) +} + +// WhereP appends storage-level predicates to the GithubInstallationMutation builder. Using this method, +// users can use type-assertion to append predicates that do not depend on any generated package. +func (m *GithubInstallationMutation) WhereP(ps ...func(*sql.Selector)) { + p := make([]predicate.GithubInstallation, len(ps)) + for i := range ps { + p[i] = ps[i] + } + m.Where(p...) +} + +// Op returns the operation name. +func (m *GithubInstallationMutation) Op() Op { + return m.op +} + +// SetOp allows setting the mutation operation. +func (m *GithubInstallationMutation) SetOp(op Op) { + m.op = op +} + +// Type returns the node type of this mutation (GithubInstallation). +func (m *GithubInstallationMutation) Type() string { + return m.typ +} + +// Fields returns all fields that were changed during this mutation. Note that in +// order to get all numeric fields that were incremented/decremented, call +// AddedFields(). +func (m *GithubInstallationMutation) Fields() []string { + fields := make([]string, 0, 7) + if m.account_login != nil { + fields = append(fields, githubinstallation.FieldAccountLogin) + } + if m.account_type != nil { + fields = append(fields, githubinstallation.FieldAccountType) + } + if m.app_id != nil { + fields = append(fields, githubinstallation.FieldAppID) + } + if m.repositories != nil { + fields = append(fields, githubinstallation.FieldRepositories) + } + if m.status != nil { + fields = append(fields, githubinstallation.FieldStatus) + } + if m.created != nil { + fields = append(fields, githubinstallation.FieldCreated) + } + if m.updated != nil { + fields = append(fields, githubinstallation.FieldUpdated) + } + return fields +} + +// Field returns the value of a field with the given name. The second boolean +// return value indicates that this field was not set, or was not defined in the +// schema. +func (m *GithubInstallationMutation) Field(name string) (ent.Value, bool) { + switch name { + case githubinstallation.FieldAccountLogin: + return m.AccountLogin() + case githubinstallation.FieldAccountType: + return m.AccountType() + case githubinstallation.FieldAppID: + return m.AppID() + case githubinstallation.FieldRepositories: + return m.Repositories() + case githubinstallation.FieldStatus: + return m.Status() + case githubinstallation.FieldCreated: + return m.Created() + case githubinstallation.FieldUpdated: + return m.Updated() + } + return nil, false +} + +// OldField returns the old value of the field from the database. An error is +// returned if the mutation operation is not UpdateOne, or the query to the +// database failed. +func (m *GithubInstallationMutation) OldField(ctx context.Context, name string) (ent.Value, error) { + switch name { + case githubinstallation.FieldAccountLogin: + return m.OldAccountLogin(ctx) + case githubinstallation.FieldAccountType: + return m.OldAccountType(ctx) + case githubinstallation.FieldAppID: + return m.OldAppID(ctx) + case githubinstallation.FieldRepositories: + return m.OldRepositories(ctx) + case githubinstallation.FieldStatus: + return m.OldStatus(ctx) + case githubinstallation.FieldCreated: + return m.OldCreated(ctx) + case githubinstallation.FieldUpdated: + return m.OldUpdated(ctx) + } + return nil, fmt.Errorf("unknown GithubInstallation field %s", name) +} + +// SetField sets the value of a field with the given name. It returns an error if +// the field is not defined in the schema, or if the type mismatched the field +// type. +func (m *GithubInstallationMutation) SetField(name string, value ent.Value) error { + switch name { + case githubinstallation.FieldAccountLogin: + v, ok := value.(string) + if !ok { + return fmt.Errorf("unexpected type %T for field %s", value, name) + } + m.SetAccountLogin(v) + return nil + case githubinstallation.FieldAccountType: + v, ok := value.(string) + if !ok { + return fmt.Errorf("unexpected type %T for field %s", value, name) + } + m.SetAccountType(v) + return nil + case githubinstallation.FieldAppID: + v, ok := value.(int64) + if !ok { + return fmt.Errorf("unexpected type %T for field %s", value, name) + } + m.SetAppID(v) + return nil + case githubinstallation.FieldRepositories: + v, ok := value.(string) + if !ok { + return fmt.Errorf("unexpected type %T for field %s", value, name) + } + m.SetRepositories(v) + return nil + case githubinstallation.FieldStatus: + v, ok := value.(string) + if !ok { + return fmt.Errorf("unexpected type %T for field %s", value, name) + } + m.SetStatus(v) + return nil + case githubinstallation.FieldCreated: + v, ok := value.(time.Time) + if !ok { + return fmt.Errorf("unexpected type %T for field %s", value, name) + } + m.SetCreated(v) + return nil + case githubinstallation.FieldUpdated: + v, ok := value.(time.Time) + if !ok { + return fmt.Errorf("unexpected type %T for field %s", value, name) + } + m.SetUpdated(v) + return nil + } + return fmt.Errorf("unknown GithubInstallation field %s", name) +} + +// AddedFields returns all numeric fields that were incremented/decremented during +// this mutation. +func (m *GithubInstallationMutation) AddedFields() []string { + var fields []string + if m.addapp_id != nil { + fields = append(fields, githubinstallation.FieldAppID) + } + return fields +} + +// AddedField returns the numeric value that was incremented/decremented on a field +// with the given name. The second boolean return value indicates that this field +// was not set, or was not defined in the schema. +func (m *GithubInstallationMutation) AddedField(name string) (ent.Value, bool) { + switch name { + case githubinstallation.FieldAppID: + return m.AddedAppID() + } + return nil, false +} + +// AddField adds the value to the field with the given name. It returns an error if +// the field is not defined in the schema, or if the type mismatched the field +// type. +func (m *GithubInstallationMutation) AddField(name string, value ent.Value) error { + switch name { + case githubinstallation.FieldAppID: + v, ok := value.(int64) + if !ok { + return fmt.Errorf("unexpected type %T for field %s", value, name) + } + m.AddAppID(v) + return nil + } + return fmt.Errorf("unknown GithubInstallation numeric field %s", name) +} + +// ClearedFields returns all nullable fields that were cleared during this +// mutation. +func (m *GithubInstallationMutation) ClearedFields() []string { + return nil +} + +// FieldCleared returns a boolean indicating if a field with the given name was +// cleared in this mutation. +func (m *GithubInstallationMutation) FieldCleared(name string) bool { + _, ok := m.clearedFields[name] + return ok +} + +// ClearField clears the value of the field with the given name. It returns an +// error if the field is not defined in the schema. +func (m *GithubInstallationMutation) ClearField(name string) error { + return fmt.Errorf("unknown GithubInstallation nullable field %s", name) +} + +// ResetField resets all changes in the mutation for the field with the given name. +// It returns an error if the field is not defined in the schema. +func (m *GithubInstallationMutation) ResetField(name string) error { + switch name { + case githubinstallation.FieldAccountLogin: + m.ResetAccountLogin() + return nil + case githubinstallation.FieldAccountType: + m.ResetAccountType() + return nil + case githubinstallation.FieldAppID: + m.ResetAppID() + return nil + case githubinstallation.FieldRepositories: + m.ResetRepositories() + return nil + case githubinstallation.FieldStatus: + m.ResetStatus() + return nil + case githubinstallation.FieldCreated: + m.ResetCreated() + return nil + case githubinstallation.FieldUpdated: + m.ResetUpdated() + return nil + } + return fmt.Errorf("unknown GithubInstallation field %s", name) +} + +// AddedEdges returns all edge names that were set/added in this mutation. +func (m *GithubInstallationMutation) AddedEdges() []string { + edges := make([]string, 0, 0) + return edges +} + +// AddedIDs returns all IDs (to other nodes) that were added for the given edge +// name in this mutation. +func (m *GithubInstallationMutation) AddedIDs(name string) []ent.Value { + return nil +} + +// RemovedEdges returns all edge names that were removed in this mutation. +func (m *GithubInstallationMutation) RemovedEdges() []string { + edges := make([]string, 0, 0) + return edges +} + +// RemovedIDs returns all IDs (to other nodes) that were removed for the edge with +// the given name in this mutation. +func (m *GithubInstallationMutation) RemovedIDs(name string) []ent.Value { + return nil +} + +// ClearedEdges returns all edge names that were cleared in this mutation. +func (m *GithubInstallationMutation) ClearedEdges() []string { + edges := make([]string, 0, 0) + return edges +} + +// EdgeCleared returns a boolean which indicates if the edge with the given name +// was cleared in this mutation. +func (m *GithubInstallationMutation) EdgeCleared(name string) bool { + return false +} + +// ClearEdge clears the value of the edge with the given name. It returns an error +// if that edge is not defined in the schema. +func (m *GithubInstallationMutation) ClearEdge(name string) error { + return fmt.Errorf("unknown GithubInstallation unique edge %s", name) +} + +// ResetEdge resets all changes to the edge with the given name in this mutation. +// It returns an error if the edge is not defined in the schema. +func (m *GithubInstallationMutation) ResetEdge(name string) error { + return fmt.Errorf("unknown GithubInstallation edge %s", name) +} + +// GroupMutation represents an operation that mutates the Group nodes in the graph. +type GroupMutation struct { + config + op Op + typ string + id *uuid.UUID + name *string + slug *string + description *string + group_type *group.GroupType + project_id *uuid.UUID + labels *map[string]string + annotations *map[string]string + created *time.Time + updated *time.Time + created_by *string + clearedFields map[string]struct{} + memberships map[uuid.UUID]struct{} + removedmemberships map[uuid.UUID]struct{} + clearedmemberships bool + parent_groups map[uuid.UUID]struct{} + removedparent_groups map[uuid.UUID]struct{} + clearedparent_groups bool + child_groups map[uuid.UUID]struct{} + removedchild_groups map[uuid.UUID]struct{} + clearedchild_groups bool + owner *uuid.UUID + clearedowner bool + policy_bindings map[uuid.UUID]struct{} + removedpolicy_bindings map[uuid.UUID]struct{} + clearedpolicy_bindings bool + done bool + oldValue func(context.Context) (*Group, error) + predicates []predicate.Group +} + +var _ ent.Mutation = (*GroupMutation)(nil) + +// groupOption allows management of the mutation configuration using functional options. +type groupOption func(*GroupMutation) + +// newGroupMutation creates new mutation for the Group entity. +func newGroupMutation(c config, op Op, opts ...groupOption) *GroupMutation { + m := &GroupMutation{ + config: c, + op: op, + typ: TypeGroup, + clearedFields: make(map[string]struct{}), + } + for _, opt := range opts { + opt(m) + } + return m +} + +// withGroupID sets the ID field of the mutation. +func withGroupID(id uuid.UUID) groupOption { + return func(m *GroupMutation) { + var ( + err error + once sync.Once + value *Group + ) + m.oldValue = func(ctx context.Context) (*Group, error) { + once.Do(func() { + if m.done { + err = errors.New("querying old values post mutation is not allowed") + } else { + value, err = m.Client().Group.Get(ctx, id) + } + }) + return value, err + } + m.id = &id + } +} + +// withGroup sets the old Group of the mutation. +func withGroup(node *Group) groupOption { + return func(m *GroupMutation) { + m.oldValue = func(context.Context) (*Group, error) { + return node, nil + } + m.id = &node.ID + } +} + +// Client returns a new `ent.Client` from the mutation. If the mutation was +// executed in a transaction (ent.Tx), a transactional client is returned. +func (m GroupMutation) Client() *Client { + client := &Client{config: m.config} + client.init() + return client +} + +// Tx returns an `ent.Tx` for mutations that were executed in transactions; +// it returns an error otherwise. +func (m GroupMutation) Tx() (*Tx, error) { + if _, ok := m.driver.(*txDriver); !ok { + return nil, errors.New("ent: mutation is not running in a transaction") + } + tx := &Tx{config: m.config} + tx.init() + return tx, nil +} + +// SetID sets the value of the id field. Note that this +// operation is only accepted on creation of Group entities. +func (m *GroupMutation) SetID(id uuid.UUID) { + m.id = &id +} + +// ID returns the ID value in the mutation. Note that the ID is only available +// if it was provided to the builder or after it was returned from the database. +func (m *GroupMutation) ID() (id uuid.UUID, exists bool) { + if m.id == nil { + return + } + return *m.id, true +} + +// IDs queries the database and returns the entity ids that match the mutation's predicate. +// That means, if the mutation is applied within a transaction with an isolation level such +// as sql.LevelSerializable, the returned ids match the ids of the rows that will be updated +// or updated by the mutation. +func (m *GroupMutation) IDs(ctx context.Context) ([]uuid.UUID, error) { + switch { + case m.op.Is(OpUpdateOne | OpDeleteOne): + id, exists := m.ID() + if exists { + return []uuid.UUID{id}, nil + } + fallthrough + case m.op.Is(OpUpdate | OpDelete): + return m.Client().Group.Query().Where(m.predicates...).IDs(ctx) + default: + return nil, fmt.Errorf("IDs is not allowed on %s operations", m.op) + } +} + +// SetName sets the "name" field. +func (m *GroupMutation) SetName(s string) { + m.name = &s +} + +// Name returns the value of the "name" field in the mutation. +func (m *GroupMutation) Name() (r string, exists bool) { + v := m.name + if v == nil { + return + } + return *v, true +} + +// OldName returns the old "name" field's value of the Group entity. +// If the Group object wasn't provided to the builder, the object is fetched from the database. +// An error is returned if the mutation operation is not UpdateOne, or the database query fails. +func (m *GroupMutation) OldName(ctx context.Context) (v string, err error) { + if !m.op.Is(OpUpdateOne) { + return v, errors.New("OldName is only allowed on UpdateOne operations") + } + if m.id == nil || m.oldValue == nil { + return v, errors.New("OldName requires an ID field in the mutation") + } + oldValue, err := m.oldValue(ctx) + if err != nil { + return v, fmt.Errorf("querying old value for OldName: %w", err) + } + return oldValue.Name, nil +} + +// ResetName resets all changes to the "name" field. +func (m *GroupMutation) ResetName() { + m.name = nil +} + +// SetSlug sets the "slug" field. +func (m *GroupMutation) SetSlug(s string) { + m.slug = &s +} + +// Slug returns the value of the "slug" field in the mutation. +func (m *GroupMutation) Slug() (r string, exists bool) { + v := m.slug + if v == nil { + return + } + return *v, true +} + +// OldSlug returns the old "slug" field's value of the Group entity. +// If the Group object wasn't provided to the builder, the object is fetched from the database. +// An error is returned if the mutation operation is not UpdateOne, or the database query fails. +func (m *GroupMutation) OldSlug(ctx context.Context) (v string, err error) { + if !m.op.Is(OpUpdateOne) { + return v, errors.New("OldSlug is only allowed on UpdateOne operations") + } + if m.id == nil || m.oldValue == nil { + return v, errors.New("OldSlug requires an ID field in the mutation") + } + oldValue, err := m.oldValue(ctx) + if err != nil { + return v, fmt.Errorf("querying old value for OldSlug: %w", err) + } + return oldValue.Slug, nil +} + +// ResetSlug resets all changes to the "slug" field. +func (m *GroupMutation) ResetSlug() { + m.slug = nil +} + +// SetDescription sets the "description" field. +func (m *GroupMutation) SetDescription(s string) { + m.description = &s +} + +// Description returns the value of the "description" field in the mutation. +func (m *GroupMutation) Description() (r string, exists bool) { + v := m.description + if v == nil { + return + } + return *v, true +} + +// OldDescription returns the old "description" field's value of the Group entity. +// If the Group object wasn't provided to the builder, the object is fetched from the database. +// An error is returned if the mutation operation is not UpdateOne, or the database query fails. +func (m *GroupMutation) OldDescription(ctx context.Context) (v string, err error) { + if !m.op.Is(OpUpdateOne) { + return v, errors.New("OldDescription is only allowed on UpdateOne operations") + } + if m.id == nil || m.oldValue == nil { + return v, errors.New("OldDescription requires an ID field in the mutation") + } + oldValue, err := m.oldValue(ctx) + if err != nil { + return v, fmt.Errorf("querying old value for OldDescription: %w", err) + } + return oldValue.Description, nil +} + +// ClearDescription clears the value of the "description" field. +func (m *GroupMutation) ClearDescription() { + m.description = nil + m.clearedFields[group.FieldDescription] = struct{}{} +} + +// DescriptionCleared returns if the "description" field was cleared in this mutation. +func (m *GroupMutation) DescriptionCleared() bool { + _, ok := m.clearedFields[group.FieldDescription] + return ok +} + +// ResetDescription resets all changes to the "description" field. +func (m *GroupMutation) ResetDescription() { + m.description = nil + delete(m.clearedFields, group.FieldDescription) +} + +// SetGroupType sets the "group_type" field. +func (m *GroupMutation) SetGroupType(gt group.GroupType) { + m.group_type = > +} + +// GroupType returns the value of the "group_type" field in the mutation. +func (m *GroupMutation) GroupType() (r group.GroupType, exists bool) { + v := m.group_type + if v == nil { + return + } + return *v, true +} + +// OldGroupType returns the old "group_type" field's value of the Group entity. +// If the Group object wasn't provided to the builder, the object is fetched from the database. +// An error is returned if the mutation operation is not UpdateOne, or the database query fails. +func (m *GroupMutation) OldGroupType(ctx context.Context) (v group.GroupType, err error) { + if !m.op.Is(OpUpdateOne) { + return v, errors.New("OldGroupType is only allowed on UpdateOne operations") + } + if m.id == nil || m.oldValue == nil { + return v, errors.New("OldGroupType requires an ID field in the mutation") + } + oldValue, err := m.oldValue(ctx) + if err != nil { + return v, fmt.Errorf("querying old value for OldGroupType: %w", err) + } + return oldValue.GroupType, nil +} + +// ResetGroupType resets all changes to the "group_type" field. +func (m *GroupMutation) ResetGroupType() { + m.group_type = nil +} + +// SetProjectID sets the "project_id" field. +func (m *GroupMutation) SetProjectID(u uuid.UUID) { + m.project_id = &u +} + +// ProjectID returns the value of the "project_id" field in the mutation. +func (m *GroupMutation) ProjectID() (r uuid.UUID, exists bool) { + v := m.project_id + if v == nil { + return + } + return *v, true +} + +// OldProjectID returns the old "project_id" field's value of the Group entity. +// If the Group object wasn't provided to the builder, the object is fetched from the database. +// An error is returned if the mutation operation is not UpdateOne, or the database query fails. +func (m *GroupMutation) OldProjectID(ctx context.Context) (v *uuid.UUID, err error) { + if !m.op.Is(OpUpdateOne) { + return v, errors.New("OldProjectID is only allowed on UpdateOne operations") + } + if m.id == nil || m.oldValue == nil { + return v, errors.New("OldProjectID requires an ID field in the mutation") + } + oldValue, err := m.oldValue(ctx) + if err != nil { + return v, fmt.Errorf("querying old value for OldProjectID: %w", err) + } + return oldValue.ProjectID, nil +} + +// ClearProjectID clears the value of the "project_id" field. +func (m *GroupMutation) ClearProjectID() { + m.project_id = nil + m.clearedFields[group.FieldProjectID] = struct{}{} +} + +// ProjectIDCleared returns if the "project_id" field was cleared in this mutation. +func (m *GroupMutation) ProjectIDCleared() bool { + _, ok := m.clearedFields[group.FieldProjectID] + return ok +} + +// ResetProjectID resets all changes to the "project_id" field. +func (m *GroupMutation) ResetProjectID() { + m.project_id = nil + delete(m.clearedFields, group.FieldProjectID) +} + +// SetLabels sets the "labels" field. +func (m *GroupMutation) SetLabels(value map[string]string) { + m.labels = &value +} + +// Labels returns the value of the "labels" field in the mutation. +func (m *GroupMutation) Labels() (r map[string]string, exists bool) { + v := m.labels + if v == nil { + return + } + return *v, true +} + +// OldLabels returns the old "labels" field's value of the Group entity. +// If the Group object wasn't provided to the builder, the object is fetched from the database. +// An error is returned if the mutation operation is not UpdateOne, or the database query fails. +func (m *GroupMutation) OldLabels(ctx context.Context) (v map[string]string, err error) { + if !m.op.Is(OpUpdateOne) { + return v, errors.New("OldLabels is only allowed on UpdateOne operations") + } + if m.id == nil || m.oldValue == nil { + return v, errors.New("OldLabels requires an ID field in the mutation") + } + oldValue, err := m.oldValue(ctx) + if err != nil { + return v, fmt.Errorf("querying old value for OldLabels: %w", err) + } + return oldValue.Labels, nil +} + +// ClearLabels clears the value of the "labels" field. +func (m *GroupMutation) ClearLabels() { + m.labels = nil + m.clearedFields[group.FieldLabels] = struct{}{} +} + +// LabelsCleared returns if the "labels" field was cleared in this mutation. +func (m *GroupMutation) LabelsCleared() bool { + _, ok := m.clearedFields[group.FieldLabels] + return ok +} + +// ResetLabels resets all changes to the "labels" field. +func (m *GroupMutation) ResetLabels() { + m.labels = nil + delete(m.clearedFields, group.FieldLabels) +} + +// SetAnnotations sets the "annotations" field. +func (m *GroupMutation) SetAnnotations(value map[string]string) { + m.annotations = &value +} + +// Annotations returns the value of the "annotations" field in the mutation. +func (m *GroupMutation) Annotations() (r map[string]string, exists bool) { + v := m.annotations + if v == nil { + return + } + return *v, true +} + +// OldAnnotations returns the old "annotations" field's value of the Group entity. +// If the Group object wasn't provided to the builder, the object is fetched from the database. +// An error is returned if the mutation operation is not UpdateOne, or the database query fails. +func (m *GroupMutation) OldAnnotations(ctx context.Context) (v map[string]string, err error) { + if !m.op.Is(OpUpdateOne) { + return v, errors.New("OldAnnotations is only allowed on UpdateOne operations") + } + if m.id == nil || m.oldValue == nil { + return v, errors.New("OldAnnotations requires an ID field in the mutation") + } + oldValue, err := m.oldValue(ctx) + if err != nil { + return v, fmt.Errorf("querying old value for OldAnnotations: %w", err) + } + return oldValue.Annotations, nil +} + +// ClearAnnotations clears the value of the "annotations" field. +func (m *GroupMutation) ClearAnnotations() { + m.annotations = nil + m.clearedFields[group.FieldAnnotations] = struct{}{} +} + +// AnnotationsCleared returns if the "annotations" field was cleared in this mutation. +func (m *GroupMutation) AnnotationsCleared() bool { + _, ok := m.clearedFields[group.FieldAnnotations] + return ok +} + +// ResetAnnotations resets all changes to the "annotations" field. +func (m *GroupMutation) ResetAnnotations() { + m.annotations = nil + delete(m.clearedFields, group.FieldAnnotations) +} + +// SetCreated sets the "created" field. +func (m *GroupMutation) SetCreated(t time.Time) { + m.created = &t +} + +// Created returns the value of the "created" field in the mutation. +func (m *GroupMutation) Created() (r time.Time, exists bool) { + v := m.created + if v == nil { + return + } + return *v, true +} + +// OldCreated returns the old "created" field's value of the Group entity. +// If the Group object wasn't provided to the builder, the object is fetched from the database. +// An error is returned if the mutation operation is not UpdateOne, or the database query fails. +func (m *GroupMutation) OldCreated(ctx context.Context) (v time.Time, err error) { + if !m.op.Is(OpUpdateOne) { + return v, errors.New("OldCreated is only allowed on UpdateOne operations") + } + if m.id == nil || m.oldValue == nil { + return v, errors.New("OldCreated requires an ID field in the mutation") + } + oldValue, err := m.oldValue(ctx) + if err != nil { + return v, fmt.Errorf("querying old value for OldCreated: %w", err) + } + return oldValue.Created, nil +} + +// ResetCreated resets all changes to the "created" field. +func (m *GroupMutation) ResetCreated() { + m.created = nil +} + +// SetUpdated sets the "updated" field. +func (m *GroupMutation) SetUpdated(t time.Time) { + m.updated = &t +} + +// Updated returns the value of the "updated" field in the mutation. +func (m *GroupMutation) Updated() (r time.Time, exists bool) { + v := m.updated + if v == nil { + return + } + return *v, true +} + +// OldUpdated returns the old "updated" field's value of the Group entity. +// If the Group object wasn't provided to the builder, the object is fetched from the database. +// An error is returned if the mutation operation is not UpdateOne, or the database query fails. +func (m *GroupMutation) OldUpdated(ctx context.Context) (v time.Time, err error) { + if !m.op.Is(OpUpdateOne) { + return v, errors.New("OldUpdated is only allowed on UpdateOne operations") + } + if m.id == nil || m.oldValue == nil { + return v, errors.New("OldUpdated requires an ID field in the mutation") + } + oldValue, err := m.oldValue(ctx) + if err != nil { + return v, fmt.Errorf("querying old value for OldUpdated: %w", err) + } + return oldValue.Updated, nil +} + +// ResetUpdated resets all changes to the "updated" field. +func (m *GroupMutation) ResetUpdated() { + m.updated = nil +} + +// SetCreatedBy sets the "created_by" field. +func (m *GroupMutation) SetCreatedBy(s string) { + m.created_by = &s +} + +// CreatedBy returns the value of the "created_by" field in the mutation. +func (m *GroupMutation) CreatedBy() (r string, exists bool) { + v := m.created_by + if v == nil { + return + } + return *v, true +} + +// OldCreatedBy returns the old "created_by" field's value of the Group entity. +// If the Group object wasn't provided to the builder, the object is fetched from the database. +// An error is returned if the mutation operation is not UpdateOne, or the database query fails. +func (m *GroupMutation) OldCreatedBy(ctx context.Context) (v string, err error) { + if !m.op.Is(OpUpdateOne) { + return v, errors.New("OldCreatedBy is only allowed on UpdateOne operations") + } + if m.id == nil || m.oldValue == nil { + return v, errors.New("OldCreatedBy requires an ID field in the mutation") + } + oldValue, err := m.oldValue(ctx) + if err != nil { + return v, fmt.Errorf("querying old value for OldCreatedBy: %w", err) + } + return oldValue.CreatedBy, nil +} + +// ClearCreatedBy clears the value of the "created_by" field. +func (m *GroupMutation) ClearCreatedBy() { + m.created_by = nil + m.clearedFields[group.FieldCreatedBy] = struct{}{} +} + +// CreatedByCleared returns if the "created_by" field was cleared in this mutation. +func (m *GroupMutation) CreatedByCleared() bool { + _, ok := m.clearedFields[group.FieldCreatedBy] + return ok +} + +// ResetCreatedBy resets all changes to the "created_by" field. +func (m *GroupMutation) ResetCreatedBy() { + m.created_by = nil + delete(m.clearedFields, group.FieldCreatedBy) +} + +// SetOwnerID sets the "owner_id" field. +func (m *GroupMutation) SetOwnerID(u uuid.UUID) { + m.owner = &u +} + +// OwnerID returns the value of the "owner_id" field in the mutation. +func (m *GroupMutation) OwnerID() (r uuid.UUID, exists bool) { + v := m.owner + if v == nil { + return + } + return *v, true +} + +// OldOwnerID returns the old "owner_id" field's value of the Group entity. +// If the Group object wasn't provided to the builder, the object is fetched from the database. +// An error is returned if the mutation operation is not UpdateOne, or the database query fails. +func (m *GroupMutation) OldOwnerID(ctx context.Context) (v *uuid.UUID, err error) { + if !m.op.Is(OpUpdateOne) { + return v, errors.New("OldOwnerID is only allowed on UpdateOne operations") + } + if m.id == nil || m.oldValue == nil { + return v, errors.New("OldOwnerID requires an ID field in the mutation") + } + oldValue, err := m.oldValue(ctx) + if err != nil { + return v, fmt.Errorf("querying old value for OldOwnerID: %w", err) + } + return oldValue.OwnerID, nil +} + +// ClearOwnerID clears the value of the "owner_id" field. +func (m *GroupMutation) ClearOwnerID() { + m.owner = nil + m.clearedFields[group.FieldOwnerID] = struct{}{} +} + +// OwnerIDCleared returns if the "owner_id" field was cleared in this mutation. +func (m *GroupMutation) OwnerIDCleared() bool { + _, ok := m.clearedFields[group.FieldOwnerID] + return ok +} + +// ResetOwnerID resets all changes to the "owner_id" field. +func (m *GroupMutation) ResetOwnerID() { + m.owner = nil + delete(m.clearedFields, group.FieldOwnerID) +} + +// AddMembershipIDs adds the "memberships" edge to the GroupMembership entity by ids. +func (m *GroupMutation) AddMembershipIDs(ids ...uuid.UUID) { + if m.memberships == nil { + m.memberships = make(map[uuid.UUID]struct{}) + } + for i := range ids { + m.memberships[ids[i]] = struct{}{} + } +} + +// ClearMemberships clears the "memberships" edge to the GroupMembership entity. +func (m *GroupMutation) ClearMemberships() { + m.clearedmemberships = true +} + +// MembershipsCleared reports if the "memberships" edge to the GroupMembership entity was cleared. +func (m *GroupMutation) MembershipsCleared() bool { + return m.clearedmemberships +} + +// RemoveMembershipIDs removes the "memberships" edge to the GroupMembership entity by IDs. +func (m *GroupMutation) RemoveMembershipIDs(ids ...uuid.UUID) { + if m.removedmemberships == nil { + m.removedmemberships = make(map[uuid.UUID]struct{}) + } + for i := range ids { + delete(m.memberships, ids[i]) + m.removedmemberships[ids[i]] = struct{}{} + } +} + +// RemovedMemberships returns the removed IDs of the "memberships" edge to the GroupMembership entity. +func (m *GroupMutation) RemovedMembershipsIDs() (ids []uuid.UUID) { + for id := range m.removedmemberships { + ids = append(ids, id) + } + return +} + +// MembershipsIDs returns the "memberships" edge IDs in the mutation. +func (m *GroupMutation) MembershipsIDs() (ids []uuid.UUID) { + for id := range m.memberships { + ids = append(ids, id) + } + return +} + +// ResetMemberships resets all changes to the "memberships" edge. +func (m *GroupMutation) ResetMemberships() { + m.memberships = nil + m.clearedmemberships = false + m.removedmemberships = nil +} + +// AddParentGroupIDs adds the "parent_groups" edge to the Group entity by ids. +func (m *GroupMutation) AddParentGroupIDs(ids ...uuid.UUID) { + if m.parent_groups == nil { + m.parent_groups = make(map[uuid.UUID]struct{}) + } + for i := range ids { + m.parent_groups[ids[i]] = struct{}{} + } +} + +// ClearParentGroups clears the "parent_groups" edge to the Group entity. +func (m *GroupMutation) ClearParentGroups() { + m.clearedparent_groups = true +} + +// ParentGroupsCleared reports if the "parent_groups" edge to the Group entity was cleared. +func (m *GroupMutation) ParentGroupsCleared() bool { + return m.clearedparent_groups +} + +// RemoveParentGroupIDs removes the "parent_groups" edge to the Group entity by IDs. +func (m *GroupMutation) RemoveParentGroupIDs(ids ...uuid.UUID) { + if m.removedparent_groups == nil { + m.removedparent_groups = make(map[uuid.UUID]struct{}) + } + for i := range ids { + delete(m.parent_groups, ids[i]) + m.removedparent_groups[ids[i]] = struct{}{} + } +} + +// RemovedParentGroups returns the removed IDs of the "parent_groups" edge to the Group entity. +func (m *GroupMutation) RemovedParentGroupsIDs() (ids []uuid.UUID) { + for id := range m.removedparent_groups { + ids = append(ids, id) + } + return +} + +// ParentGroupsIDs returns the "parent_groups" edge IDs in the mutation. +func (m *GroupMutation) ParentGroupsIDs() (ids []uuid.UUID) { + for id := range m.parent_groups { + ids = append(ids, id) + } + return +} + +// ResetParentGroups resets all changes to the "parent_groups" edge. +func (m *GroupMutation) ResetParentGroups() { + m.parent_groups = nil + m.clearedparent_groups = false + m.removedparent_groups = nil +} + +// AddChildGroupIDs adds the "child_groups" edge to the Group entity by ids. +func (m *GroupMutation) AddChildGroupIDs(ids ...uuid.UUID) { + if m.child_groups == nil { + m.child_groups = make(map[uuid.UUID]struct{}) + } + for i := range ids { + m.child_groups[ids[i]] = struct{}{} + } +} + +// ClearChildGroups clears the "child_groups" edge to the Group entity. +func (m *GroupMutation) ClearChildGroups() { + m.clearedchild_groups = true +} + +// ChildGroupsCleared reports if the "child_groups" edge to the Group entity was cleared. +func (m *GroupMutation) ChildGroupsCleared() bool { + return m.clearedchild_groups +} + +// RemoveChildGroupIDs removes the "child_groups" edge to the Group entity by IDs. +func (m *GroupMutation) RemoveChildGroupIDs(ids ...uuid.UUID) { + if m.removedchild_groups == nil { + m.removedchild_groups = make(map[uuid.UUID]struct{}) + } + for i := range ids { + delete(m.child_groups, ids[i]) + m.removedchild_groups[ids[i]] = struct{}{} + } +} + +// RemovedChildGroups returns the removed IDs of the "child_groups" edge to the Group entity. +func (m *GroupMutation) RemovedChildGroupsIDs() (ids []uuid.UUID) { + for id := range m.removedchild_groups { + ids = append(ids, id) + } + return +} + +// ChildGroupsIDs returns the "child_groups" edge IDs in the mutation. +func (m *GroupMutation) ChildGroupsIDs() (ids []uuid.UUID) { + for id := range m.child_groups { + ids = append(ids, id) + } + return +} + +// ResetChildGroups resets all changes to the "child_groups" edge. +func (m *GroupMutation) ResetChildGroups() { + m.child_groups = nil + m.clearedchild_groups = false + m.removedchild_groups = nil +} + +// ClearOwner clears the "owner" edge to the User entity. +func (m *GroupMutation) ClearOwner() { + m.clearedowner = true + m.clearedFields[group.FieldOwnerID] = struct{}{} +} + +// OwnerCleared reports if the "owner" edge to the User entity was cleared. +func (m *GroupMutation) OwnerCleared() bool { + return m.OwnerIDCleared() || m.clearedowner +} + +// OwnerIDs returns the "owner" edge IDs in the mutation. +// Note that IDs always returns len(IDs) <= 1 for unique edges, and you should use +// OwnerID instead. It exists only for internal usage by the builders. +func (m *GroupMutation) OwnerIDs() (ids []uuid.UUID) { + if id := m.owner; id != nil { + ids = append(ids, *id) + } + return +} + +// ResetOwner resets all changes to the "owner" edge. +func (m *GroupMutation) ResetOwner() { + m.owner = nil + m.clearedowner = false +} + +// AddPolicyBindingIDs adds the "policy_bindings" edge to the PolicyBinding entity by ids. +func (m *GroupMutation) AddPolicyBindingIDs(ids ...uuid.UUID) { + if m.policy_bindings == nil { + m.policy_bindings = make(map[uuid.UUID]struct{}) + } + for i := range ids { + m.policy_bindings[ids[i]] = struct{}{} + } +} + +// ClearPolicyBindings clears the "policy_bindings" edge to the PolicyBinding entity. +func (m *GroupMutation) ClearPolicyBindings() { + m.clearedpolicy_bindings = true +} + +// PolicyBindingsCleared reports if the "policy_bindings" edge to the PolicyBinding entity was cleared. +func (m *GroupMutation) PolicyBindingsCleared() bool { + return m.clearedpolicy_bindings +} + +// RemovePolicyBindingIDs removes the "policy_bindings" edge to the PolicyBinding entity by IDs. +func (m *GroupMutation) RemovePolicyBindingIDs(ids ...uuid.UUID) { + if m.removedpolicy_bindings == nil { + m.removedpolicy_bindings = make(map[uuid.UUID]struct{}) + } + for i := range ids { + delete(m.policy_bindings, ids[i]) + m.removedpolicy_bindings[ids[i]] = struct{}{} + } +} + +// RemovedPolicyBindings returns the removed IDs of the "policy_bindings" edge to the PolicyBinding entity. +func (m *GroupMutation) RemovedPolicyBindingsIDs() (ids []uuid.UUID) { + for id := range m.removedpolicy_bindings { + ids = append(ids, id) + } + return +} + +// PolicyBindingsIDs returns the "policy_bindings" edge IDs in the mutation. +func (m *GroupMutation) PolicyBindingsIDs() (ids []uuid.UUID) { + for id := range m.policy_bindings { + ids = append(ids, id) + } + return +} + +// ResetPolicyBindings resets all changes to the "policy_bindings" edge. +func (m *GroupMutation) ResetPolicyBindings() { + m.policy_bindings = nil + m.clearedpolicy_bindings = false + m.removedpolicy_bindings = nil +} + +// Where appends a list predicates to the GroupMutation builder. +func (m *GroupMutation) Where(ps ...predicate.Group) { + m.predicates = append(m.predicates, ps...) +} + +// WhereP appends storage-level predicates to the GroupMutation builder. Using this method, +// users can use type-assertion to append predicates that do not depend on any generated package. +func (m *GroupMutation) WhereP(ps ...func(*sql.Selector)) { + p := make([]predicate.Group, len(ps)) + for i := range ps { + p[i] = ps[i] + } + m.Where(p...) +} + +// Op returns the operation name. +func (m *GroupMutation) Op() Op { + return m.op +} + +// SetOp allows setting the mutation operation. +func (m *GroupMutation) SetOp(op Op) { + m.op = op +} + +// Type returns the node type of this mutation (Group). +func (m *GroupMutation) Type() string { + return m.typ +} + +// Fields returns all fields that were changed during this mutation. Note that in +// order to get all numeric fields that were incremented/decremented, call +// AddedFields(). +func (m *GroupMutation) Fields() []string { + fields := make([]string, 0, 11) + if m.name != nil { + fields = append(fields, group.FieldName) + } + if m.slug != nil { + fields = append(fields, group.FieldSlug) + } + if m.description != nil { + fields = append(fields, group.FieldDescription) + } + if m.group_type != nil { + fields = append(fields, group.FieldGroupType) + } + if m.project_id != nil { + fields = append(fields, group.FieldProjectID) + } + if m.labels != nil { + fields = append(fields, group.FieldLabels) + } + if m.annotations != nil { + fields = append(fields, group.FieldAnnotations) + } + if m.created != nil { + fields = append(fields, group.FieldCreated) + } + if m.updated != nil { + fields = append(fields, group.FieldUpdated) + } + if m.created_by != nil { + fields = append(fields, group.FieldCreatedBy) + } + if m.owner != nil { + fields = append(fields, group.FieldOwnerID) + } + return fields +} + +// Field returns the value of a field with the given name. The second boolean +// return value indicates that this field was not set, or was not defined in the +// schema. +func (m *GroupMutation) Field(name string) (ent.Value, bool) { + switch name { + case group.FieldName: + return m.Name() + case group.FieldSlug: + return m.Slug() + case group.FieldDescription: + return m.Description() + case group.FieldGroupType: + return m.GroupType() + case group.FieldProjectID: + return m.ProjectID() + case group.FieldLabels: + return m.Labels() + case group.FieldAnnotations: + return m.Annotations() + case group.FieldCreated: + return m.Created() + case group.FieldUpdated: + return m.Updated() + case group.FieldCreatedBy: + return m.CreatedBy() + case group.FieldOwnerID: + return m.OwnerID() + } + return nil, false +} + +// OldField returns the old value of the field from the database. An error is +// returned if the mutation operation is not UpdateOne, or the query to the +// database failed. +func (m *GroupMutation) OldField(ctx context.Context, name string) (ent.Value, error) { + switch name { + case group.FieldName: + return m.OldName(ctx) + case group.FieldSlug: + return m.OldSlug(ctx) + case group.FieldDescription: + return m.OldDescription(ctx) + case group.FieldGroupType: + return m.OldGroupType(ctx) + case group.FieldProjectID: + return m.OldProjectID(ctx) + case group.FieldLabels: + return m.OldLabels(ctx) + case group.FieldAnnotations: + return m.OldAnnotations(ctx) + case group.FieldCreated: + return m.OldCreated(ctx) + case group.FieldUpdated: + return m.OldUpdated(ctx) + case group.FieldCreatedBy: + return m.OldCreatedBy(ctx) + case group.FieldOwnerID: + return m.OldOwnerID(ctx) + } + return nil, fmt.Errorf("unknown Group field %s", name) +} + +// SetField sets the value of a field with the given name. It returns an error if +// the field is not defined in the schema, or if the type mismatched the field +// type. +func (m *GroupMutation) SetField(name string, value ent.Value) error { + switch name { + case group.FieldName: + v, ok := value.(string) + if !ok { + return fmt.Errorf("unexpected type %T for field %s", value, name) + } + m.SetName(v) + return nil + case group.FieldSlug: + v, ok := value.(string) + if !ok { + return fmt.Errorf("unexpected type %T for field %s", value, name) + } + m.SetSlug(v) + return nil + case group.FieldDescription: + v, ok := value.(string) + if !ok { + return fmt.Errorf("unexpected type %T for field %s", value, name) + } + m.SetDescription(v) + return nil + case group.FieldGroupType: + v, ok := value.(group.GroupType) + if !ok { + return fmt.Errorf("unexpected type %T for field %s", value, name) + } + m.SetGroupType(v) + return nil + case group.FieldProjectID: + v, ok := value.(uuid.UUID) + if !ok { + return fmt.Errorf("unexpected type %T for field %s", value, name) + } + m.SetProjectID(v) + return nil + case group.FieldLabels: + v, ok := value.(map[string]string) + if !ok { + return fmt.Errorf("unexpected type %T for field %s", value, name) + } + m.SetLabels(v) + return nil + case group.FieldAnnotations: + v, ok := value.(map[string]string) + if !ok { + return fmt.Errorf("unexpected type %T for field %s", value, name) + } + m.SetAnnotations(v) + return nil + case group.FieldCreated: + v, ok := value.(time.Time) + if !ok { + return fmt.Errorf("unexpected type %T for field %s", value, name) + } + m.SetCreated(v) + return nil + case group.FieldUpdated: + v, ok := value.(time.Time) + if !ok { + return fmt.Errorf("unexpected type %T for field %s", value, name) + } + m.SetUpdated(v) + return nil + case group.FieldCreatedBy: + v, ok := value.(string) + if !ok { + return fmt.Errorf("unexpected type %T for field %s", value, name) + } + m.SetCreatedBy(v) + return nil + case group.FieldOwnerID: + v, ok := value.(uuid.UUID) + if !ok { + return fmt.Errorf("unexpected type %T for field %s", value, name) + } + m.SetOwnerID(v) + return nil + } + return fmt.Errorf("unknown Group field %s", name) +} + +// AddedFields returns all numeric fields that were incremented/decremented during +// this mutation. +func (m *GroupMutation) AddedFields() []string { + return nil +} + +// AddedField returns the numeric value that was incremented/decremented on a field +// with the given name. The second boolean return value indicates that this field +// was not set, or was not defined in the schema. +func (m *GroupMutation) AddedField(name string) (ent.Value, bool) { + return nil, false +} + +// AddField adds the value to the field with the given name. It returns an error if +// the field is not defined in the schema, or if the type mismatched the field +// type. +func (m *GroupMutation) AddField(name string, value ent.Value) error { + switch name { + } + return fmt.Errorf("unknown Group numeric field %s", name) +} + +// ClearedFields returns all nullable fields that were cleared during this +// mutation. +func (m *GroupMutation) ClearedFields() []string { + var fields []string + if m.FieldCleared(group.FieldDescription) { + fields = append(fields, group.FieldDescription) + } + if m.FieldCleared(group.FieldProjectID) { + fields = append(fields, group.FieldProjectID) + } + if m.FieldCleared(group.FieldLabels) { + fields = append(fields, group.FieldLabels) + } + if m.FieldCleared(group.FieldAnnotations) { + fields = append(fields, group.FieldAnnotations) + } + if m.FieldCleared(group.FieldCreatedBy) { + fields = append(fields, group.FieldCreatedBy) + } + if m.FieldCleared(group.FieldOwnerID) { + fields = append(fields, group.FieldOwnerID) + } + return fields +} + +// FieldCleared returns a boolean indicating if a field with the given name was +// cleared in this mutation. +func (m *GroupMutation) FieldCleared(name string) bool { + _, ok := m.clearedFields[name] + return ok +} + +// ClearField clears the value of the field with the given name. It returns an +// error if the field is not defined in the schema. +func (m *GroupMutation) ClearField(name string) error { + switch name { + case group.FieldDescription: + m.ClearDescription() + return nil + case group.FieldProjectID: + m.ClearProjectID() + return nil + case group.FieldLabels: + m.ClearLabels() + return nil + case group.FieldAnnotations: + m.ClearAnnotations() + return nil + case group.FieldCreatedBy: + m.ClearCreatedBy() + return nil + case group.FieldOwnerID: + m.ClearOwnerID() + return nil + } + return fmt.Errorf("unknown Group nullable field %s", name) +} + +// ResetField resets all changes in the mutation for the field with the given name. +// It returns an error if the field is not defined in the schema. +func (m *GroupMutation) ResetField(name string) error { + switch name { + case group.FieldName: + m.ResetName() + return nil + case group.FieldSlug: + m.ResetSlug() + return nil + case group.FieldDescription: + m.ResetDescription() + return nil + case group.FieldGroupType: + m.ResetGroupType() + return nil + case group.FieldProjectID: + m.ResetProjectID() + return nil + case group.FieldLabels: + m.ResetLabels() + return nil + case group.FieldAnnotations: + m.ResetAnnotations() + return nil + case group.FieldCreated: + m.ResetCreated() + return nil + case group.FieldUpdated: + m.ResetUpdated() + return nil + case group.FieldCreatedBy: + m.ResetCreatedBy() + return nil + case group.FieldOwnerID: + m.ResetOwnerID() + return nil + } + return fmt.Errorf("unknown Group field %s", name) +} + +// AddedEdges returns all edge names that were set/added in this mutation. +func (m *GroupMutation) AddedEdges() []string { + edges := make([]string, 0, 5) + if m.memberships != nil { + edges = append(edges, group.EdgeMemberships) + } + if m.parent_groups != nil { + edges = append(edges, group.EdgeParentGroups) + } + if m.child_groups != nil { + edges = append(edges, group.EdgeChildGroups) + } + if m.owner != nil { + edges = append(edges, group.EdgeOwner) + } + if m.policy_bindings != nil { + edges = append(edges, group.EdgePolicyBindings) + } + return edges +} + +// AddedIDs returns all IDs (to other nodes) that were added for the given edge +// name in this mutation. +func (m *GroupMutation) AddedIDs(name string) []ent.Value { + switch name { + case group.EdgeMemberships: + ids := make([]ent.Value, 0, len(m.memberships)) + for id := range m.memberships { + ids = append(ids, id) + } + return ids + case group.EdgeParentGroups: + ids := make([]ent.Value, 0, len(m.parent_groups)) + for id := range m.parent_groups { + ids = append(ids, id) + } + return ids + case group.EdgeChildGroups: + ids := make([]ent.Value, 0, len(m.child_groups)) + for id := range m.child_groups { + ids = append(ids, id) + } + return ids + case group.EdgeOwner: + if id := m.owner; id != nil { + return []ent.Value{*id} + } + case group.EdgePolicyBindings: + ids := make([]ent.Value, 0, len(m.policy_bindings)) + for id := range m.policy_bindings { + ids = append(ids, id) + } + return ids + } + return nil +} + +// RemovedEdges returns all edge names that were removed in this mutation. +func (m *GroupMutation) RemovedEdges() []string { + edges := make([]string, 0, 5) + if m.removedmemberships != nil { + edges = append(edges, group.EdgeMemberships) + } + if m.removedparent_groups != nil { + edges = append(edges, group.EdgeParentGroups) + } + if m.removedchild_groups != nil { + edges = append(edges, group.EdgeChildGroups) + } + if m.removedpolicy_bindings != nil { + edges = append(edges, group.EdgePolicyBindings) + } + return edges +} + +// RemovedIDs returns all IDs (to other nodes) that were removed for the edge with +// the given name in this mutation. +func (m *GroupMutation) RemovedIDs(name string) []ent.Value { + switch name { + case group.EdgeMemberships: + ids := make([]ent.Value, 0, len(m.removedmemberships)) + for id := range m.removedmemberships { + ids = append(ids, id) + } + return ids + case group.EdgeParentGroups: + ids := make([]ent.Value, 0, len(m.removedparent_groups)) + for id := range m.removedparent_groups { + ids = append(ids, id) + } + return ids + case group.EdgeChildGroups: + ids := make([]ent.Value, 0, len(m.removedchild_groups)) + for id := range m.removedchild_groups { + ids = append(ids, id) + } + return ids + case group.EdgePolicyBindings: + ids := make([]ent.Value, 0, len(m.removedpolicy_bindings)) + for id := range m.removedpolicy_bindings { + ids = append(ids, id) + } + return ids + } + return nil +} + +// ClearedEdges returns all edge names that were cleared in this mutation. +func (m *GroupMutation) ClearedEdges() []string { + edges := make([]string, 0, 5) + if m.clearedmemberships { + edges = append(edges, group.EdgeMemberships) + } + if m.clearedparent_groups { + edges = append(edges, group.EdgeParentGroups) + } + if m.clearedchild_groups { + edges = append(edges, group.EdgeChildGroups) + } + if m.clearedowner { + edges = append(edges, group.EdgeOwner) + } + if m.clearedpolicy_bindings { + edges = append(edges, group.EdgePolicyBindings) + } + return edges +} + +// EdgeCleared returns a boolean which indicates if the edge with the given name +// was cleared in this mutation. +func (m *GroupMutation) EdgeCleared(name string) bool { + switch name { + case group.EdgeMemberships: + return m.clearedmemberships + case group.EdgeParentGroups: + return m.clearedparent_groups + case group.EdgeChildGroups: + return m.clearedchild_groups + case group.EdgeOwner: + return m.clearedowner + case group.EdgePolicyBindings: + return m.clearedpolicy_bindings + } + return false +} + +// ClearEdge clears the value of the edge with the given name. It returns an error +// if that edge is not defined in the schema. +func (m *GroupMutation) ClearEdge(name string) error { + switch name { + case group.EdgeOwner: + m.ClearOwner() + return nil + } + return fmt.Errorf("unknown Group unique edge %s", name) +} + +// ResetEdge resets all changes to the edge with the given name in this mutation. +// It returns an error if the edge is not defined in the schema. +func (m *GroupMutation) ResetEdge(name string) error { + switch name { + case group.EdgeMemberships: + m.ResetMemberships() + return nil + case group.EdgeParentGroups: + m.ResetParentGroups() + return nil + case group.EdgeChildGroups: + m.ResetChildGroups() + return nil + case group.EdgeOwner: + m.ResetOwner() + return nil + case group.EdgePolicyBindings: + m.ResetPolicyBindings() + return nil + } + return fmt.Errorf("unknown Group edge %s", name) +} + +// GroupMembershipMutation represents an operation that mutates the GroupMembership nodes in the graph. +type GroupMembershipMutation struct { + config + op Op + typ string + id *uuid.UUID + role *groupmembership.Role + added_by *string + added_at *time.Time + clearedFields map[string]struct{} + group *uuid.UUID + clearedgroup bool + user *uuid.UUID + cleareduser bool + agent *uuid.UUID + clearedagent bool + done bool + oldValue func(context.Context) (*GroupMembership, error) + predicates []predicate.GroupMembership +} + +var _ ent.Mutation = (*GroupMembershipMutation)(nil) + +// groupmembershipOption allows management of the mutation configuration using functional options. +type groupmembershipOption func(*GroupMembershipMutation) + +// newGroupMembershipMutation creates new mutation for the GroupMembership entity. +func newGroupMembershipMutation(c config, op Op, opts ...groupmembershipOption) *GroupMembershipMutation { + m := &GroupMembershipMutation{ + config: c, + op: op, + typ: TypeGroupMembership, + clearedFields: make(map[string]struct{}), + } + for _, opt := range opts { + opt(m) + } + return m +} + +// withGroupMembershipID sets the ID field of the mutation. +func withGroupMembershipID(id uuid.UUID) groupmembershipOption { + return func(m *GroupMembershipMutation) { + var ( + err error + once sync.Once + value *GroupMembership + ) + m.oldValue = func(ctx context.Context) (*GroupMembership, error) { + once.Do(func() { + if m.done { + err = errors.New("querying old values post mutation is not allowed") + } else { + value, err = m.Client().GroupMembership.Get(ctx, id) + } + }) + return value, err + } + m.id = &id + } +} + +// withGroupMembership sets the old GroupMembership of the mutation. +func withGroupMembership(node *GroupMembership) groupmembershipOption { + return func(m *GroupMembershipMutation) { + m.oldValue = func(context.Context) (*GroupMembership, error) { + return node, nil + } + m.id = &node.ID + } +} + +// Client returns a new `ent.Client` from the mutation. If the mutation was +// executed in a transaction (ent.Tx), a transactional client is returned. +func (m GroupMembershipMutation) Client() *Client { + client := &Client{config: m.config} + client.init() + return client +} + +// Tx returns an `ent.Tx` for mutations that were executed in transactions; +// it returns an error otherwise. +func (m GroupMembershipMutation) Tx() (*Tx, error) { + if _, ok := m.driver.(*txDriver); !ok { + return nil, errors.New("ent: mutation is not running in a transaction") + } + tx := &Tx{config: m.config} + tx.init() + return tx, nil +} + +// SetID sets the value of the id field. Note that this +// operation is only accepted on creation of GroupMembership entities. +func (m *GroupMembershipMutation) SetID(id uuid.UUID) { + m.id = &id +} + +// ID returns the ID value in the mutation. Note that the ID is only available +// if it was provided to the builder or after it was returned from the database. +func (m *GroupMembershipMutation) ID() (id uuid.UUID, exists bool) { + if m.id == nil { + return + } + return *m.id, true +} + +// IDs queries the database and returns the entity ids that match the mutation's predicate. +// That means, if the mutation is applied within a transaction with an isolation level such +// as sql.LevelSerializable, the returned ids match the ids of the rows that will be updated +// or updated by the mutation. +func (m *GroupMembershipMutation) IDs(ctx context.Context) ([]uuid.UUID, error) { + switch { + case m.op.Is(OpUpdateOne | OpDeleteOne): + id, exists := m.ID() + if exists { + return []uuid.UUID{id}, nil + } + fallthrough + case m.op.Is(OpUpdate | OpDelete): + return m.Client().GroupMembership.Query().Where(m.predicates...).IDs(ctx) + default: + return nil, fmt.Errorf("IDs is not allowed on %s operations", m.op) + } +} + +// SetRole sets the "role" field. +func (m *GroupMembershipMutation) SetRole(gr groupmembership.Role) { + m.role = &gr +} + +// Role returns the value of the "role" field in the mutation. +func (m *GroupMembershipMutation) Role() (r groupmembership.Role, exists bool) { + v := m.role + if v == nil { + return + } + return *v, true +} + +// OldRole returns the old "role" field's value of the GroupMembership entity. +// If the GroupMembership object wasn't provided to the builder, the object is fetched from the database. +// An error is returned if the mutation operation is not UpdateOne, or the database query fails. +func (m *GroupMembershipMutation) OldRole(ctx context.Context) (v groupmembership.Role, err error) { + if !m.op.Is(OpUpdateOne) { + return v, errors.New("OldRole is only allowed on UpdateOne operations") + } + if m.id == nil || m.oldValue == nil { + return v, errors.New("OldRole requires an ID field in the mutation") + } + oldValue, err := m.oldValue(ctx) + if err != nil { + return v, fmt.Errorf("querying old value for OldRole: %w", err) + } + return oldValue.Role, nil +} + +// ResetRole resets all changes to the "role" field. +func (m *GroupMembershipMutation) ResetRole() { + m.role = nil +} + +// SetAddedBy sets the "added_by" field. +func (m *GroupMembershipMutation) SetAddedBy(s string) { + m.added_by = &s +} + +// AddedBy returns the value of the "added_by" field in the mutation. +func (m *GroupMembershipMutation) AddedBy() (r string, exists bool) { + v := m.added_by + if v == nil { + return + } + return *v, true +} + +// OldAddedBy returns the old "added_by" field's value of the GroupMembership entity. +// If the GroupMembership object wasn't provided to the builder, the object is fetched from the database. +// An error is returned if the mutation operation is not UpdateOne, or the database query fails. +func (m *GroupMembershipMutation) OldAddedBy(ctx context.Context) (v string, err error) { + if !m.op.Is(OpUpdateOne) { + return v, errors.New("OldAddedBy is only allowed on UpdateOne operations") + } + if m.id == nil || m.oldValue == nil { + return v, errors.New("OldAddedBy requires an ID field in the mutation") + } + oldValue, err := m.oldValue(ctx) + if err != nil { + return v, fmt.Errorf("querying old value for OldAddedBy: %w", err) + } + return oldValue.AddedBy, nil +} + +// ClearAddedBy clears the value of the "added_by" field. +func (m *GroupMembershipMutation) ClearAddedBy() { + m.added_by = nil + m.clearedFields[groupmembership.FieldAddedBy] = struct{}{} +} + +// AddedByCleared returns if the "added_by" field was cleared in this mutation. +func (m *GroupMembershipMutation) AddedByCleared() bool { + _, ok := m.clearedFields[groupmembership.FieldAddedBy] + return ok +} + +// ResetAddedBy resets all changes to the "added_by" field. +func (m *GroupMembershipMutation) ResetAddedBy() { + m.added_by = nil + delete(m.clearedFields, groupmembership.FieldAddedBy) +} + +// SetAddedAt sets the "added_at" field. +func (m *GroupMembershipMutation) SetAddedAt(t time.Time) { + m.added_at = &t +} + +// AddedAt returns the value of the "added_at" field in the mutation. +func (m *GroupMembershipMutation) AddedAt() (r time.Time, exists bool) { + v := m.added_at + if v == nil { + return + } + return *v, true +} + +// OldAddedAt returns the old "added_at" field's value of the GroupMembership entity. +// If the GroupMembership object wasn't provided to the builder, the object is fetched from the database. +// An error is returned if the mutation operation is not UpdateOne, or the database query fails. +func (m *GroupMembershipMutation) OldAddedAt(ctx context.Context) (v time.Time, err error) { + if !m.op.Is(OpUpdateOne) { + return v, errors.New("OldAddedAt is only allowed on UpdateOne operations") + } + if m.id == nil || m.oldValue == nil { + return v, errors.New("OldAddedAt requires an ID field in the mutation") + } + oldValue, err := m.oldValue(ctx) + if err != nil { + return v, fmt.Errorf("querying old value for OldAddedAt: %w", err) + } + return oldValue.AddedAt, nil +} + +// ResetAddedAt resets all changes to the "added_at" field. +func (m *GroupMembershipMutation) ResetAddedAt() { + m.added_at = nil +} + +// SetGroupID sets the "group_id" field. +func (m *GroupMembershipMutation) SetGroupID(u uuid.UUID) { + m.group = &u +} + +// GroupID returns the value of the "group_id" field in the mutation. +func (m *GroupMembershipMutation) GroupID() (r uuid.UUID, exists bool) { + v := m.group + if v == nil { + return + } + return *v, true +} + +// OldGroupID returns the old "group_id" field's value of the GroupMembership entity. +// If the GroupMembership object wasn't provided to the builder, the object is fetched from the database. +// An error is returned if the mutation operation is not UpdateOne, or the database query fails. +func (m *GroupMembershipMutation) OldGroupID(ctx context.Context) (v uuid.UUID, err error) { + if !m.op.Is(OpUpdateOne) { + return v, errors.New("OldGroupID is only allowed on UpdateOne operations") + } + if m.id == nil || m.oldValue == nil { + return v, errors.New("OldGroupID requires an ID field in the mutation") + } + oldValue, err := m.oldValue(ctx) + if err != nil { + return v, fmt.Errorf("querying old value for OldGroupID: %w", err) + } + return oldValue.GroupID, nil +} + +// ResetGroupID resets all changes to the "group_id" field. +func (m *GroupMembershipMutation) ResetGroupID() { + m.group = nil +} + +// SetUserID sets the "user_id" field. +func (m *GroupMembershipMutation) SetUserID(u uuid.UUID) { + m.user = &u +} + +// UserID returns the value of the "user_id" field in the mutation. +func (m *GroupMembershipMutation) UserID() (r uuid.UUID, exists bool) { + v := m.user + if v == nil { + return + } + return *v, true +} + +// OldUserID returns the old "user_id" field's value of the GroupMembership entity. +// If the GroupMembership object wasn't provided to the builder, the object is fetched from the database. +// An error is returned if the mutation operation is not UpdateOne, or the database query fails. +func (m *GroupMembershipMutation) OldUserID(ctx context.Context) (v *uuid.UUID, err error) { + if !m.op.Is(OpUpdateOne) { + return v, errors.New("OldUserID is only allowed on UpdateOne operations") + } + if m.id == nil || m.oldValue == nil { + return v, errors.New("OldUserID requires an ID field in the mutation") + } + oldValue, err := m.oldValue(ctx) + if err != nil { + return v, fmt.Errorf("querying old value for OldUserID: %w", err) + } + return oldValue.UserID, nil +} + +// ClearUserID clears the value of the "user_id" field. +func (m *GroupMembershipMutation) ClearUserID() { + m.user = nil + m.clearedFields[groupmembership.FieldUserID] = struct{}{} +} + +// UserIDCleared returns if the "user_id" field was cleared in this mutation. +func (m *GroupMembershipMutation) UserIDCleared() bool { + _, ok := m.clearedFields[groupmembership.FieldUserID] + return ok +} + +// ResetUserID resets all changes to the "user_id" field. +func (m *GroupMembershipMutation) ResetUserID() { + m.user = nil + delete(m.clearedFields, groupmembership.FieldUserID) +} + +// SetAgentID sets the "agent_id" field. +func (m *GroupMembershipMutation) SetAgentID(u uuid.UUID) { + m.agent = &u +} + +// AgentID returns the value of the "agent_id" field in the mutation. +func (m *GroupMembershipMutation) AgentID() (r uuid.UUID, exists bool) { + v := m.agent + if v == nil { + return + } + return *v, true +} + +// OldAgentID returns the old "agent_id" field's value of the GroupMembership entity. +// If the GroupMembership object wasn't provided to the builder, the object is fetched from the database. +// An error is returned if the mutation operation is not UpdateOne, or the database query fails. +func (m *GroupMembershipMutation) OldAgentID(ctx context.Context) (v *uuid.UUID, err error) { + if !m.op.Is(OpUpdateOne) { + return v, errors.New("OldAgentID is only allowed on UpdateOne operations") + } + if m.id == nil || m.oldValue == nil { + return v, errors.New("OldAgentID requires an ID field in the mutation") + } + oldValue, err := m.oldValue(ctx) + if err != nil { + return v, fmt.Errorf("querying old value for OldAgentID: %w", err) + } + return oldValue.AgentID, nil +} + +// ClearAgentID clears the value of the "agent_id" field. +func (m *GroupMembershipMutation) ClearAgentID() { + m.agent = nil + m.clearedFields[groupmembership.FieldAgentID] = struct{}{} +} + +// AgentIDCleared returns if the "agent_id" field was cleared in this mutation. +func (m *GroupMembershipMutation) AgentIDCleared() bool { + _, ok := m.clearedFields[groupmembership.FieldAgentID] + return ok +} + +// ResetAgentID resets all changes to the "agent_id" field. +func (m *GroupMembershipMutation) ResetAgentID() { + m.agent = nil + delete(m.clearedFields, groupmembership.FieldAgentID) +} + +// ClearGroup clears the "group" edge to the Group entity. +func (m *GroupMembershipMutation) ClearGroup() { + m.clearedgroup = true + m.clearedFields[groupmembership.FieldGroupID] = struct{}{} +} + +// GroupCleared reports if the "group" edge to the Group entity was cleared. +func (m *GroupMembershipMutation) GroupCleared() bool { + return m.clearedgroup +} + +// GroupIDs returns the "group" edge IDs in the mutation. +// Note that IDs always returns len(IDs) <= 1 for unique edges, and you should use +// GroupID instead. It exists only for internal usage by the builders. +func (m *GroupMembershipMutation) GroupIDs() (ids []uuid.UUID) { + if id := m.group; id != nil { + ids = append(ids, *id) + } + return +} + +// ResetGroup resets all changes to the "group" edge. +func (m *GroupMembershipMutation) ResetGroup() { + m.group = nil + m.clearedgroup = false +} + +// ClearUser clears the "user" edge to the User entity. +func (m *GroupMembershipMutation) ClearUser() { + m.cleareduser = true + m.clearedFields[groupmembership.FieldUserID] = struct{}{} +} + +// UserCleared reports if the "user" edge to the User entity was cleared. +func (m *GroupMembershipMutation) UserCleared() bool { + return m.UserIDCleared() || m.cleareduser +} + +// UserIDs returns the "user" edge IDs in the mutation. +// Note that IDs always returns len(IDs) <= 1 for unique edges, and you should use +// UserID instead. It exists only for internal usage by the builders. +func (m *GroupMembershipMutation) UserIDs() (ids []uuid.UUID) { + if id := m.user; id != nil { + ids = append(ids, *id) + } + return +} + +// ResetUser resets all changes to the "user" edge. +func (m *GroupMembershipMutation) ResetUser() { + m.user = nil + m.cleareduser = false +} + +// ClearAgent clears the "agent" edge to the Agent entity. +func (m *GroupMembershipMutation) ClearAgent() { + m.clearedagent = true + m.clearedFields[groupmembership.FieldAgentID] = struct{}{} +} + +// AgentCleared reports if the "agent" edge to the Agent entity was cleared. +func (m *GroupMembershipMutation) AgentCleared() bool { + return m.AgentIDCleared() || m.clearedagent +} + +// AgentIDs returns the "agent" edge IDs in the mutation. +// Note that IDs always returns len(IDs) <= 1 for unique edges, and you should use +// AgentID instead. It exists only for internal usage by the builders. +func (m *GroupMembershipMutation) AgentIDs() (ids []uuid.UUID) { + if id := m.agent; id != nil { + ids = append(ids, *id) + } + return +} + +// ResetAgent resets all changes to the "agent" edge. +func (m *GroupMembershipMutation) ResetAgent() { + m.agent = nil + m.clearedagent = false +} + +// Where appends a list predicates to the GroupMembershipMutation builder. +func (m *GroupMembershipMutation) Where(ps ...predicate.GroupMembership) { + m.predicates = append(m.predicates, ps...) +} + +// WhereP appends storage-level predicates to the GroupMembershipMutation builder. Using this method, +// users can use type-assertion to append predicates that do not depend on any generated package. +func (m *GroupMembershipMutation) WhereP(ps ...func(*sql.Selector)) { + p := make([]predicate.GroupMembership, len(ps)) + for i := range ps { + p[i] = ps[i] + } + m.Where(p...) +} + +// Op returns the operation name. +func (m *GroupMembershipMutation) Op() Op { + return m.op +} + +// SetOp allows setting the mutation operation. +func (m *GroupMembershipMutation) SetOp(op Op) { + m.op = op +} + +// Type returns the node type of this mutation (GroupMembership). +func (m *GroupMembershipMutation) Type() string { + return m.typ +} + +// Fields returns all fields that were changed during this mutation. Note that in +// order to get all numeric fields that were incremented/decremented, call +// AddedFields(). +func (m *GroupMembershipMutation) Fields() []string { + fields := make([]string, 0, 6) + if m.role != nil { + fields = append(fields, groupmembership.FieldRole) + } + if m.added_by != nil { + fields = append(fields, groupmembership.FieldAddedBy) + } + if m.added_at != nil { + fields = append(fields, groupmembership.FieldAddedAt) + } + if m.group != nil { + fields = append(fields, groupmembership.FieldGroupID) + } + if m.user != nil { + fields = append(fields, groupmembership.FieldUserID) + } + if m.agent != nil { + fields = append(fields, groupmembership.FieldAgentID) + } + return fields +} + +// Field returns the value of a field with the given name. The second boolean +// return value indicates that this field was not set, or was not defined in the +// schema. +func (m *GroupMembershipMutation) Field(name string) (ent.Value, bool) { + switch name { + case groupmembership.FieldRole: + return m.Role() + case groupmembership.FieldAddedBy: + return m.AddedBy() + case groupmembership.FieldAddedAt: + return m.AddedAt() + case groupmembership.FieldGroupID: + return m.GroupID() + case groupmembership.FieldUserID: + return m.UserID() + case groupmembership.FieldAgentID: + return m.AgentID() + } + return nil, false +} + +// OldField returns the old value of the field from the database. An error is +// returned if the mutation operation is not UpdateOne, or the query to the +// database failed. +func (m *GroupMembershipMutation) OldField(ctx context.Context, name string) (ent.Value, error) { + switch name { + case groupmembership.FieldRole: + return m.OldRole(ctx) + case groupmembership.FieldAddedBy: + return m.OldAddedBy(ctx) + case groupmembership.FieldAddedAt: + return m.OldAddedAt(ctx) + case groupmembership.FieldGroupID: + return m.OldGroupID(ctx) + case groupmembership.FieldUserID: + return m.OldUserID(ctx) + case groupmembership.FieldAgentID: + return m.OldAgentID(ctx) + } + return nil, fmt.Errorf("unknown GroupMembership field %s", name) +} + +// SetField sets the value of a field with the given name. It returns an error if +// the field is not defined in the schema, or if the type mismatched the field +// type. +func (m *GroupMembershipMutation) SetField(name string, value ent.Value) error { + switch name { + case groupmembership.FieldRole: + v, ok := value.(groupmembership.Role) + if !ok { + return fmt.Errorf("unexpected type %T for field %s", value, name) + } + m.SetRole(v) + return nil + case groupmembership.FieldAddedBy: + v, ok := value.(string) + if !ok { + return fmt.Errorf("unexpected type %T for field %s", value, name) + } + m.SetAddedBy(v) + return nil + case groupmembership.FieldAddedAt: + v, ok := value.(time.Time) + if !ok { + return fmt.Errorf("unexpected type %T for field %s", value, name) + } + m.SetAddedAt(v) + return nil + case groupmembership.FieldGroupID: + v, ok := value.(uuid.UUID) + if !ok { + return fmt.Errorf("unexpected type %T for field %s", value, name) + } + m.SetGroupID(v) + return nil + case groupmembership.FieldUserID: + v, ok := value.(uuid.UUID) + if !ok { + return fmt.Errorf("unexpected type %T for field %s", value, name) + } + m.SetUserID(v) + return nil + case groupmembership.FieldAgentID: + v, ok := value.(uuid.UUID) + if !ok { + return fmt.Errorf("unexpected type %T for field %s", value, name) + } + m.SetAgentID(v) + return nil + } + return fmt.Errorf("unknown GroupMembership field %s", name) +} + +// AddedFields returns all numeric fields that were incremented/decremented during +// this mutation. +func (m *GroupMembershipMutation) AddedFields() []string { + return nil +} + +// AddedField returns the numeric value that was incremented/decremented on a field +// with the given name. The second boolean return value indicates that this field +// was not set, or was not defined in the schema. +func (m *GroupMembershipMutation) AddedField(name string) (ent.Value, bool) { + return nil, false +} + +// AddField adds the value to the field with the given name. It returns an error if +// the field is not defined in the schema, or if the type mismatched the field +// type. +func (m *GroupMembershipMutation) AddField(name string, value ent.Value) error { + switch name { + } + return fmt.Errorf("unknown GroupMembership numeric field %s", name) +} + +// ClearedFields returns all nullable fields that were cleared during this +// mutation. +func (m *GroupMembershipMutation) ClearedFields() []string { + var fields []string + if m.FieldCleared(groupmembership.FieldAddedBy) { + fields = append(fields, groupmembership.FieldAddedBy) + } + if m.FieldCleared(groupmembership.FieldUserID) { + fields = append(fields, groupmembership.FieldUserID) + } + if m.FieldCleared(groupmembership.FieldAgentID) { + fields = append(fields, groupmembership.FieldAgentID) + } + return fields +} + +// FieldCleared returns a boolean indicating if a field with the given name was +// cleared in this mutation. +func (m *GroupMembershipMutation) FieldCleared(name string) bool { + _, ok := m.clearedFields[name] + return ok +} + +// ClearField clears the value of the field with the given name. It returns an +// error if the field is not defined in the schema. +func (m *GroupMembershipMutation) ClearField(name string) error { + switch name { + case groupmembership.FieldAddedBy: + m.ClearAddedBy() + return nil + case groupmembership.FieldUserID: + m.ClearUserID() + return nil + case groupmembership.FieldAgentID: + m.ClearAgentID() + return nil + } + return fmt.Errorf("unknown GroupMembership nullable field %s", name) +} + +// ResetField resets all changes in the mutation for the field with the given name. +// It returns an error if the field is not defined in the schema. +func (m *GroupMembershipMutation) ResetField(name string) error { + switch name { + case groupmembership.FieldRole: + m.ResetRole() + return nil + case groupmembership.FieldAddedBy: + m.ResetAddedBy() + return nil + case groupmembership.FieldAddedAt: + m.ResetAddedAt() + return nil + case groupmembership.FieldGroupID: + m.ResetGroupID() + return nil + case groupmembership.FieldUserID: + m.ResetUserID() + return nil + case groupmembership.FieldAgentID: + m.ResetAgentID() + return nil + } + return fmt.Errorf("unknown GroupMembership field %s", name) +} + +// AddedEdges returns all edge names that were set/added in this mutation. +func (m *GroupMembershipMutation) AddedEdges() []string { + edges := make([]string, 0, 3) + if m.group != nil { + edges = append(edges, groupmembership.EdgeGroup) + } + if m.user != nil { + edges = append(edges, groupmembership.EdgeUser) + } + if m.agent != nil { + edges = append(edges, groupmembership.EdgeAgent) + } + return edges +} + +// AddedIDs returns all IDs (to other nodes) that were added for the given edge +// name in this mutation. +func (m *GroupMembershipMutation) AddedIDs(name string) []ent.Value { + switch name { + case groupmembership.EdgeGroup: + if id := m.group; id != nil { + return []ent.Value{*id} + } + case groupmembership.EdgeUser: + if id := m.user; id != nil { + return []ent.Value{*id} + } + case groupmembership.EdgeAgent: + if id := m.agent; id != nil { + return []ent.Value{*id} + } + } + return nil +} + +// RemovedEdges returns all edge names that were removed in this mutation. +func (m *GroupMembershipMutation) RemovedEdges() []string { + edges := make([]string, 0, 3) + return edges +} + +// RemovedIDs returns all IDs (to other nodes) that were removed for the edge with +// the given name in this mutation. +func (m *GroupMembershipMutation) RemovedIDs(name string) []ent.Value { + return nil +} + +// ClearedEdges returns all edge names that were cleared in this mutation. +func (m *GroupMembershipMutation) ClearedEdges() []string { + edges := make([]string, 0, 3) + if m.clearedgroup { + edges = append(edges, groupmembership.EdgeGroup) + } + if m.cleareduser { + edges = append(edges, groupmembership.EdgeUser) + } + if m.clearedagent { + edges = append(edges, groupmembership.EdgeAgent) + } + return edges +} + +// EdgeCleared returns a boolean which indicates if the edge with the given name +// was cleared in this mutation. +func (m *GroupMembershipMutation) EdgeCleared(name string) bool { + switch name { + case groupmembership.EdgeGroup: + return m.clearedgroup + case groupmembership.EdgeUser: + return m.cleareduser + case groupmembership.EdgeAgent: + return m.clearedagent + } + return false +} + +// ClearEdge clears the value of the edge with the given name. It returns an error +// if that edge is not defined in the schema. +func (m *GroupMembershipMutation) ClearEdge(name string) error { + switch name { + case groupmembership.EdgeGroup: + m.ClearGroup() + return nil + case groupmembership.EdgeUser: + m.ClearUser() + return nil + case groupmembership.EdgeAgent: + m.ClearAgent() + return nil + } + return fmt.Errorf("unknown GroupMembership unique edge %s", name) +} + +// ResetEdge resets all changes to the edge with the given name in this mutation. +// It returns an error if the edge is not defined in the schema. +func (m *GroupMembershipMutation) ResetEdge(name string) error { + switch name { + case groupmembership.EdgeGroup: + m.ResetGroup() + return nil + case groupmembership.EdgeUser: + m.ResetUser() + return nil + case groupmembership.EdgeAgent: + m.ResetAgent() + return nil + } + return fmt.Errorf("unknown GroupMembership edge %s", name) +} + +// HarnessConfigMutation represents an operation that mutates the HarnessConfig nodes in the graph. +type HarnessConfigMutation struct { + config + op Op + typ string + id *uuid.UUID + name *string + slug *string + display_name *string + description *string + harness *string + _config *string + content_hash *string + scope *string + scope_id *string + storage_uri *string + storage_bucket *string + storage_path *string + files *string + status *harnessconfig.Status + owner_id *string + created_by *string + updated_by *string + visibility *string + created *time.Time + updated *time.Time + clearedFields map[string]struct{} + done bool + oldValue func(context.Context) (*HarnessConfig, error) + predicates []predicate.HarnessConfig +} + +var _ ent.Mutation = (*HarnessConfigMutation)(nil) + +// harnessconfigOption allows management of the mutation configuration using functional options. +type harnessconfigOption func(*HarnessConfigMutation) + +// newHarnessConfigMutation creates new mutation for the HarnessConfig entity. +func newHarnessConfigMutation(c config, op Op, opts ...harnessconfigOption) *HarnessConfigMutation { + m := &HarnessConfigMutation{ + config: c, + op: op, + typ: TypeHarnessConfig, + clearedFields: make(map[string]struct{}), + } + for _, opt := range opts { + opt(m) + } + return m +} + +// withHarnessConfigID sets the ID field of the mutation. +func withHarnessConfigID(id uuid.UUID) harnessconfigOption { + return func(m *HarnessConfigMutation) { + var ( + err error + once sync.Once + value *HarnessConfig + ) + m.oldValue = func(ctx context.Context) (*HarnessConfig, error) { + once.Do(func() { + if m.done { + err = errors.New("querying old values post mutation is not allowed") + } else { + value, err = m.Client().HarnessConfig.Get(ctx, id) + } + }) + return value, err + } + m.id = &id + } +} + +// withHarnessConfig sets the old HarnessConfig of the mutation. +func withHarnessConfig(node *HarnessConfig) harnessconfigOption { + return func(m *HarnessConfigMutation) { + m.oldValue = func(context.Context) (*HarnessConfig, error) { + return node, nil + } + m.id = &node.ID + } +} + +// Client returns a new `ent.Client` from the mutation. If the mutation was +// executed in a transaction (ent.Tx), a transactional client is returned. +func (m HarnessConfigMutation) Client() *Client { + client := &Client{config: m.config} + client.init() + return client +} + +// Tx returns an `ent.Tx` for mutations that were executed in transactions; +// it returns an error otherwise. +func (m HarnessConfigMutation) Tx() (*Tx, error) { + if _, ok := m.driver.(*txDriver); !ok { + return nil, errors.New("ent: mutation is not running in a transaction") + } + tx := &Tx{config: m.config} + tx.init() + return tx, nil +} + +// SetID sets the value of the id field. Note that this +// operation is only accepted on creation of HarnessConfig entities. +func (m *HarnessConfigMutation) SetID(id uuid.UUID) { + m.id = &id +} + +// ID returns the ID value in the mutation. Note that the ID is only available +// if it was provided to the builder or after it was returned from the database. +func (m *HarnessConfigMutation) ID() (id uuid.UUID, exists bool) { + if m.id == nil { + return + } + return *m.id, true +} + +// IDs queries the database and returns the entity ids that match the mutation's predicate. +// That means, if the mutation is applied within a transaction with an isolation level such +// as sql.LevelSerializable, the returned ids match the ids of the rows that will be updated +// or updated by the mutation. +func (m *HarnessConfigMutation) IDs(ctx context.Context) ([]uuid.UUID, error) { + switch { + case m.op.Is(OpUpdateOne | OpDeleteOne): + id, exists := m.ID() + if exists { + return []uuid.UUID{id}, nil + } + fallthrough + case m.op.Is(OpUpdate | OpDelete): + return m.Client().HarnessConfig.Query().Where(m.predicates...).IDs(ctx) + default: + return nil, fmt.Errorf("IDs is not allowed on %s operations", m.op) + } +} + +// SetName sets the "name" field. +func (m *HarnessConfigMutation) SetName(s string) { + m.name = &s +} + +// Name returns the value of the "name" field in the mutation. +func (m *HarnessConfigMutation) Name() (r string, exists bool) { + v := m.name + if v == nil { + return + } + return *v, true +} + +// OldName returns the old "name" field's value of the HarnessConfig entity. +// If the HarnessConfig object wasn't provided to the builder, the object is fetched from the database. +// An error is returned if the mutation operation is not UpdateOne, or the database query fails. +func (m *HarnessConfigMutation) OldName(ctx context.Context) (v string, err error) { + if !m.op.Is(OpUpdateOne) { + return v, errors.New("OldName is only allowed on UpdateOne operations") + } + if m.id == nil || m.oldValue == nil { + return v, errors.New("OldName requires an ID field in the mutation") + } + oldValue, err := m.oldValue(ctx) + if err != nil { + return v, fmt.Errorf("querying old value for OldName: %w", err) + } + return oldValue.Name, nil +} + +// ResetName resets all changes to the "name" field. +func (m *HarnessConfigMutation) ResetName() { + m.name = nil +} + +// SetSlug sets the "slug" field. +func (m *HarnessConfigMutation) SetSlug(s string) { + m.slug = &s +} + +// Slug returns the value of the "slug" field in the mutation. +func (m *HarnessConfigMutation) Slug() (r string, exists bool) { + v := m.slug + if v == nil { + return + } + return *v, true +} + +// OldSlug returns the old "slug" field's value of the HarnessConfig entity. +// If the HarnessConfig object wasn't provided to the builder, the object is fetched from the database. +// An error is returned if the mutation operation is not UpdateOne, or the database query fails. +func (m *HarnessConfigMutation) OldSlug(ctx context.Context) (v string, err error) { + if !m.op.Is(OpUpdateOne) { + return v, errors.New("OldSlug is only allowed on UpdateOne operations") + } + if m.id == nil || m.oldValue == nil { + return v, errors.New("OldSlug requires an ID field in the mutation") + } + oldValue, err := m.oldValue(ctx) + if err != nil { + return v, fmt.Errorf("querying old value for OldSlug: %w", err) + } + return oldValue.Slug, nil +} + +// ResetSlug resets all changes to the "slug" field. +func (m *HarnessConfigMutation) ResetSlug() { + m.slug = nil +} + +// SetDisplayName sets the "display_name" field. +func (m *HarnessConfigMutation) SetDisplayName(s string) { + m.display_name = &s +} + +// DisplayName returns the value of the "display_name" field in the mutation. +func (m *HarnessConfigMutation) DisplayName() (r string, exists bool) { + v := m.display_name + if v == nil { + return + } + return *v, true +} + +// OldDisplayName returns the old "display_name" field's value of the HarnessConfig entity. +// If the HarnessConfig object wasn't provided to the builder, the object is fetched from the database. +// An error is returned if the mutation operation is not UpdateOne, or the database query fails. +func (m *HarnessConfigMutation) OldDisplayName(ctx context.Context) (v string, err error) { + if !m.op.Is(OpUpdateOne) { + return v, errors.New("OldDisplayName is only allowed on UpdateOne operations") + } + if m.id == nil || m.oldValue == nil { + return v, errors.New("OldDisplayName requires an ID field in the mutation") + } + oldValue, err := m.oldValue(ctx) + if err != nil { + return v, fmt.Errorf("querying old value for OldDisplayName: %w", err) + } + return oldValue.DisplayName, nil +} + +// ClearDisplayName clears the value of the "display_name" field. +func (m *HarnessConfigMutation) ClearDisplayName() { + m.display_name = nil + m.clearedFields[harnessconfig.FieldDisplayName] = struct{}{} +} + +// DisplayNameCleared returns if the "display_name" field was cleared in this mutation. +func (m *HarnessConfigMutation) DisplayNameCleared() bool { + _, ok := m.clearedFields[harnessconfig.FieldDisplayName] + return ok +} + +// ResetDisplayName resets all changes to the "display_name" field. +func (m *HarnessConfigMutation) ResetDisplayName() { + m.display_name = nil + delete(m.clearedFields, harnessconfig.FieldDisplayName) +} + +// SetDescription sets the "description" field. +func (m *HarnessConfigMutation) SetDescription(s string) { + m.description = &s +} + +// Description returns the value of the "description" field in the mutation. +func (m *HarnessConfigMutation) Description() (r string, exists bool) { + v := m.description + if v == nil { + return + } + return *v, true +} + +// OldDescription returns the old "description" field's value of the HarnessConfig entity. +// If the HarnessConfig object wasn't provided to the builder, the object is fetched from the database. +// An error is returned if the mutation operation is not UpdateOne, or the database query fails. +func (m *HarnessConfigMutation) OldDescription(ctx context.Context) (v string, err error) { + if !m.op.Is(OpUpdateOne) { + return v, errors.New("OldDescription is only allowed on UpdateOne operations") + } + if m.id == nil || m.oldValue == nil { + return v, errors.New("OldDescription requires an ID field in the mutation") + } + oldValue, err := m.oldValue(ctx) + if err != nil { + return v, fmt.Errorf("querying old value for OldDescription: %w", err) + } + return oldValue.Description, nil +} + +// ClearDescription clears the value of the "description" field. +func (m *HarnessConfigMutation) ClearDescription() { + m.description = nil + m.clearedFields[harnessconfig.FieldDescription] = struct{}{} +} + +// DescriptionCleared returns if the "description" field was cleared in this mutation. +func (m *HarnessConfigMutation) DescriptionCleared() bool { + _, ok := m.clearedFields[harnessconfig.FieldDescription] + return ok +} + +// ResetDescription resets all changes to the "description" field. +func (m *HarnessConfigMutation) ResetDescription() { + m.description = nil + delete(m.clearedFields, harnessconfig.FieldDescription) +} + +// SetHarness sets the "harness" field. +func (m *HarnessConfigMutation) SetHarness(s string) { + m.harness = &s +} + +// Harness returns the value of the "harness" field in the mutation. +func (m *HarnessConfigMutation) Harness() (r string, exists bool) { + v := m.harness + if v == nil { + return + } + return *v, true +} + +// OldHarness returns the old "harness" field's value of the HarnessConfig entity. +// If the HarnessConfig object wasn't provided to the builder, the object is fetched from the database. +// An error is returned if the mutation operation is not UpdateOne, or the database query fails. +func (m *HarnessConfigMutation) OldHarness(ctx context.Context) (v string, err error) { + if !m.op.Is(OpUpdateOne) { + return v, errors.New("OldHarness is only allowed on UpdateOne operations") + } + if m.id == nil || m.oldValue == nil { + return v, errors.New("OldHarness requires an ID field in the mutation") + } + oldValue, err := m.oldValue(ctx) + if err != nil { + return v, fmt.Errorf("querying old value for OldHarness: %w", err) + } + return oldValue.Harness, nil +} + +// ResetHarness resets all changes to the "harness" field. +func (m *HarnessConfigMutation) ResetHarness() { + m.harness = nil +} + +// SetConfig sets the "config" field. +func (m *HarnessConfigMutation) SetConfig(s string) { + m._config = &s +} + +// Config returns the value of the "config" field in the mutation. +func (m *HarnessConfigMutation) Config() (r string, exists bool) { + v := m._config + if v == nil { + return + } + return *v, true +} + +// OldConfig returns the old "config" field's value of the HarnessConfig entity. +// If the HarnessConfig object wasn't provided to the builder, the object is fetched from the database. +// An error is returned if the mutation operation is not UpdateOne, or the database query fails. +func (m *HarnessConfigMutation) OldConfig(ctx context.Context) (v string, err error) { + if !m.op.Is(OpUpdateOne) { + return v, errors.New("OldConfig is only allowed on UpdateOne operations") + } + if m.id == nil || m.oldValue == nil { + return v, errors.New("OldConfig requires an ID field in the mutation") + } + oldValue, err := m.oldValue(ctx) + if err != nil { + return v, fmt.Errorf("querying old value for OldConfig: %w", err) + } + return oldValue.Config, nil +} + +// ClearConfig clears the value of the "config" field. +func (m *HarnessConfigMutation) ClearConfig() { + m._config = nil + m.clearedFields[harnessconfig.FieldConfig] = struct{}{} +} + +// ConfigCleared returns if the "config" field was cleared in this mutation. +func (m *HarnessConfigMutation) ConfigCleared() bool { + _, ok := m.clearedFields[harnessconfig.FieldConfig] + return ok +} + +// ResetConfig resets all changes to the "config" field. +func (m *HarnessConfigMutation) ResetConfig() { + m._config = nil + delete(m.clearedFields, harnessconfig.FieldConfig) +} + +// SetContentHash sets the "content_hash" field. +func (m *HarnessConfigMutation) SetContentHash(s string) { + m.content_hash = &s +} + +// ContentHash returns the value of the "content_hash" field in the mutation. +func (m *HarnessConfigMutation) ContentHash() (r string, exists bool) { + v := m.content_hash + if v == nil { + return + } + return *v, true +} + +// OldContentHash returns the old "content_hash" field's value of the HarnessConfig entity. +// If the HarnessConfig object wasn't provided to the builder, the object is fetched from the database. +// An error is returned if the mutation operation is not UpdateOne, or the database query fails. +func (m *HarnessConfigMutation) OldContentHash(ctx context.Context) (v string, err error) { + if !m.op.Is(OpUpdateOne) { + return v, errors.New("OldContentHash is only allowed on UpdateOne operations") + } + if m.id == nil || m.oldValue == nil { + return v, errors.New("OldContentHash requires an ID field in the mutation") + } + oldValue, err := m.oldValue(ctx) + if err != nil { + return v, fmt.Errorf("querying old value for OldContentHash: %w", err) + } + return oldValue.ContentHash, nil +} + +// ClearContentHash clears the value of the "content_hash" field. +func (m *HarnessConfigMutation) ClearContentHash() { + m.content_hash = nil + m.clearedFields[harnessconfig.FieldContentHash] = struct{}{} +} + +// ContentHashCleared returns if the "content_hash" field was cleared in this mutation. +func (m *HarnessConfigMutation) ContentHashCleared() bool { + _, ok := m.clearedFields[harnessconfig.FieldContentHash] + return ok +} + +// ResetContentHash resets all changes to the "content_hash" field. +func (m *HarnessConfigMutation) ResetContentHash() { + m.content_hash = nil + delete(m.clearedFields, harnessconfig.FieldContentHash) +} + +// SetScope sets the "scope" field. +func (m *HarnessConfigMutation) SetScope(s string) { + m.scope = &s +} + +// Scope returns the value of the "scope" field in the mutation. +func (m *HarnessConfigMutation) Scope() (r string, exists bool) { + v := m.scope + if v == nil { + return + } + return *v, true +} + +// OldScope returns the old "scope" field's value of the HarnessConfig entity. +// If the HarnessConfig object wasn't provided to the builder, the object is fetched from the database. +// An error is returned if the mutation operation is not UpdateOne, or the database query fails. +func (m *HarnessConfigMutation) OldScope(ctx context.Context) (v string, err error) { + if !m.op.Is(OpUpdateOne) { + return v, errors.New("OldScope is only allowed on UpdateOne operations") + } + if m.id == nil || m.oldValue == nil { + return v, errors.New("OldScope requires an ID field in the mutation") + } + oldValue, err := m.oldValue(ctx) + if err != nil { + return v, fmt.Errorf("querying old value for OldScope: %w", err) + } + return oldValue.Scope, nil +} + +// ResetScope resets all changes to the "scope" field. +func (m *HarnessConfigMutation) ResetScope() { + m.scope = nil +} + +// SetScopeID sets the "scope_id" field. +func (m *HarnessConfigMutation) SetScopeID(s string) { + m.scope_id = &s +} + +// ScopeID returns the value of the "scope_id" field in the mutation. +func (m *HarnessConfigMutation) ScopeID() (r string, exists bool) { + v := m.scope_id + if v == nil { + return + } + return *v, true +} + +// OldScopeID returns the old "scope_id" field's value of the HarnessConfig entity. +// If the HarnessConfig object wasn't provided to the builder, the object is fetched from the database. +// An error is returned if the mutation operation is not UpdateOne, or the database query fails. +func (m *HarnessConfigMutation) OldScopeID(ctx context.Context) (v string, err error) { + if !m.op.Is(OpUpdateOne) { + return v, errors.New("OldScopeID is only allowed on UpdateOne operations") + } + if m.id == nil || m.oldValue == nil { + return v, errors.New("OldScopeID requires an ID field in the mutation") + } + oldValue, err := m.oldValue(ctx) + if err != nil { + return v, fmt.Errorf("querying old value for OldScopeID: %w", err) + } + return oldValue.ScopeID, nil +} + +// ClearScopeID clears the value of the "scope_id" field. +func (m *HarnessConfigMutation) ClearScopeID() { + m.scope_id = nil + m.clearedFields[harnessconfig.FieldScopeID] = struct{}{} +} + +// ScopeIDCleared returns if the "scope_id" field was cleared in this mutation. +func (m *HarnessConfigMutation) ScopeIDCleared() bool { + _, ok := m.clearedFields[harnessconfig.FieldScopeID] + return ok +} + +// ResetScopeID resets all changes to the "scope_id" field. +func (m *HarnessConfigMutation) ResetScopeID() { + m.scope_id = nil + delete(m.clearedFields, harnessconfig.FieldScopeID) +} + +// SetStorageURI sets the "storage_uri" field. +func (m *HarnessConfigMutation) SetStorageURI(s string) { + m.storage_uri = &s +} + +// StorageURI returns the value of the "storage_uri" field in the mutation. +func (m *HarnessConfigMutation) StorageURI() (r string, exists bool) { + v := m.storage_uri + if v == nil { + return + } + return *v, true +} + +// OldStorageURI returns the old "storage_uri" field's value of the HarnessConfig entity. +// If the HarnessConfig object wasn't provided to the builder, the object is fetched from the database. +// An error is returned if the mutation operation is not UpdateOne, or the database query fails. +func (m *HarnessConfigMutation) OldStorageURI(ctx context.Context) (v string, err error) { + if !m.op.Is(OpUpdateOne) { + return v, errors.New("OldStorageURI is only allowed on UpdateOne operations") + } + if m.id == nil || m.oldValue == nil { + return v, errors.New("OldStorageURI requires an ID field in the mutation") + } + oldValue, err := m.oldValue(ctx) + if err != nil { + return v, fmt.Errorf("querying old value for OldStorageURI: %w", err) + } + return oldValue.StorageURI, nil +} + +// ClearStorageURI clears the value of the "storage_uri" field. +func (m *HarnessConfigMutation) ClearStorageURI() { + m.storage_uri = nil + m.clearedFields[harnessconfig.FieldStorageURI] = struct{}{} +} + +// StorageURICleared returns if the "storage_uri" field was cleared in this mutation. +func (m *HarnessConfigMutation) StorageURICleared() bool { + _, ok := m.clearedFields[harnessconfig.FieldStorageURI] + return ok +} + +// ResetStorageURI resets all changes to the "storage_uri" field. +func (m *HarnessConfigMutation) ResetStorageURI() { + m.storage_uri = nil + delete(m.clearedFields, harnessconfig.FieldStorageURI) +} + +// SetStorageBucket sets the "storage_bucket" field. +func (m *HarnessConfigMutation) SetStorageBucket(s string) { + m.storage_bucket = &s +} + +// StorageBucket returns the value of the "storage_bucket" field in the mutation. +func (m *HarnessConfigMutation) StorageBucket() (r string, exists bool) { + v := m.storage_bucket + if v == nil { + return + } + return *v, true +} + +// OldStorageBucket returns the old "storage_bucket" field's value of the HarnessConfig entity. +// If the HarnessConfig object wasn't provided to the builder, the object is fetched from the database. +// An error is returned if the mutation operation is not UpdateOne, or the database query fails. +func (m *HarnessConfigMutation) OldStorageBucket(ctx context.Context) (v string, err error) { + if !m.op.Is(OpUpdateOne) { + return v, errors.New("OldStorageBucket is only allowed on UpdateOne operations") + } + if m.id == nil || m.oldValue == nil { + return v, errors.New("OldStorageBucket requires an ID field in the mutation") + } + oldValue, err := m.oldValue(ctx) + if err != nil { + return v, fmt.Errorf("querying old value for OldStorageBucket: %w", err) + } + return oldValue.StorageBucket, nil +} + +// ClearStorageBucket clears the value of the "storage_bucket" field. +func (m *HarnessConfigMutation) ClearStorageBucket() { + m.storage_bucket = nil + m.clearedFields[harnessconfig.FieldStorageBucket] = struct{}{} +} + +// StorageBucketCleared returns if the "storage_bucket" field was cleared in this mutation. +func (m *HarnessConfigMutation) StorageBucketCleared() bool { + _, ok := m.clearedFields[harnessconfig.FieldStorageBucket] + return ok +} + +// ResetStorageBucket resets all changes to the "storage_bucket" field. +func (m *HarnessConfigMutation) ResetStorageBucket() { + m.storage_bucket = nil + delete(m.clearedFields, harnessconfig.FieldStorageBucket) +} + +// SetStoragePath sets the "storage_path" field. +func (m *HarnessConfigMutation) SetStoragePath(s string) { + m.storage_path = &s +} + +// StoragePath returns the value of the "storage_path" field in the mutation. +func (m *HarnessConfigMutation) StoragePath() (r string, exists bool) { + v := m.storage_path + if v == nil { + return + } + return *v, true +} + +// OldStoragePath returns the old "storage_path" field's value of the HarnessConfig entity. +// If the HarnessConfig object wasn't provided to the builder, the object is fetched from the database. +// An error is returned if the mutation operation is not UpdateOne, or the database query fails. +func (m *HarnessConfigMutation) OldStoragePath(ctx context.Context) (v string, err error) { + if !m.op.Is(OpUpdateOne) { + return v, errors.New("OldStoragePath is only allowed on UpdateOne operations") + } + if m.id == nil || m.oldValue == nil { + return v, errors.New("OldStoragePath requires an ID field in the mutation") + } + oldValue, err := m.oldValue(ctx) + if err != nil { + return v, fmt.Errorf("querying old value for OldStoragePath: %w", err) + } + return oldValue.StoragePath, nil +} + +// ClearStoragePath clears the value of the "storage_path" field. +func (m *HarnessConfigMutation) ClearStoragePath() { + m.storage_path = nil + m.clearedFields[harnessconfig.FieldStoragePath] = struct{}{} +} + +// StoragePathCleared returns if the "storage_path" field was cleared in this mutation. +func (m *HarnessConfigMutation) StoragePathCleared() bool { + _, ok := m.clearedFields[harnessconfig.FieldStoragePath] + return ok +} + +// ResetStoragePath resets all changes to the "storage_path" field. +func (m *HarnessConfigMutation) ResetStoragePath() { + m.storage_path = nil + delete(m.clearedFields, harnessconfig.FieldStoragePath) +} + +// SetFiles sets the "files" field. +func (m *HarnessConfigMutation) SetFiles(s string) { + m.files = &s +} + +// Files returns the value of the "files" field in the mutation. +func (m *HarnessConfigMutation) Files() (r string, exists bool) { + v := m.files + if v == nil { + return + } + return *v, true +} + +// OldFiles returns the old "files" field's value of the HarnessConfig entity. +// If the HarnessConfig object wasn't provided to the builder, the object is fetched from the database. +// An error is returned if the mutation operation is not UpdateOne, or the database query fails. +func (m *HarnessConfigMutation) OldFiles(ctx context.Context) (v string, err error) { + if !m.op.Is(OpUpdateOne) { + return v, errors.New("OldFiles is only allowed on UpdateOne operations") + } + if m.id == nil || m.oldValue == nil { + return v, errors.New("OldFiles requires an ID field in the mutation") + } + oldValue, err := m.oldValue(ctx) + if err != nil { + return v, fmt.Errorf("querying old value for OldFiles: %w", err) + } + return oldValue.Files, nil +} + +// ClearFiles clears the value of the "files" field. +func (m *HarnessConfigMutation) ClearFiles() { + m.files = nil + m.clearedFields[harnessconfig.FieldFiles] = struct{}{} +} + +// FilesCleared returns if the "files" field was cleared in this mutation. +func (m *HarnessConfigMutation) FilesCleared() bool { + _, ok := m.clearedFields[harnessconfig.FieldFiles] + return ok +} + +// ResetFiles resets all changes to the "files" field. +func (m *HarnessConfigMutation) ResetFiles() { + m.files = nil + delete(m.clearedFields, harnessconfig.FieldFiles) +} + +// SetStatus sets the "status" field. +func (m *HarnessConfigMutation) SetStatus(h harnessconfig.Status) { + m.status = &h +} + +// Status returns the value of the "status" field in the mutation. +func (m *HarnessConfigMutation) Status() (r harnessconfig.Status, exists bool) { + v := m.status + if v == nil { + return + } + return *v, true +} + +// OldStatus returns the old "status" field's value of the HarnessConfig entity. +// If the HarnessConfig object wasn't provided to the builder, the object is fetched from the database. +// An error is returned if the mutation operation is not UpdateOne, or the database query fails. +func (m *HarnessConfigMutation) OldStatus(ctx context.Context) (v harnessconfig.Status, err error) { + if !m.op.Is(OpUpdateOne) { + return v, errors.New("OldStatus is only allowed on UpdateOne operations") + } + if m.id == nil || m.oldValue == nil { + return v, errors.New("OldStatus requires an ID field in the mutation") + } + oldValue, err := m.oldValue(ctx) + if err != nil { + return v, fmt.Errorf("querying old value for OldStatus: %w", err) + } + return oldValue.Status, nil +} + +// ResetStatus resets all changes to the "status" field. +func (m *HarnessConfigMutation) ResetStatus() { + m.status = nil +} + +// SetOwnerID sets the "owner_id" field. +func (m *HarnessConfigMutation) SetOwnerID(s string) { + m.owner_id = &s +} + +// OwnerID returns the value of the "owner_id" field in the mutation. +func (m *HarnessConfigMutation) OwnerID() (r string, exists bool) { + v := m.owner_id + if v == nil { + return + } + return *v, true +} + +// OldOwnerID returns the old "owner_id" field's value of the HarnessConfig entity. +// If the HarnessConfig object wasn't provided to the builder, the object is fetched from the database. +// An error is returned if the mutation operation is not UpdateOne, or the database query fails. +func (m *HarnessConfigMutation) OldOwnerID(ctx context.Context) (v string, err error) { + if !m.op.Is(OpUpdateOne) { + return v, errors.New("OldOwnerID is only allowed on UpdateOne operations") + } + if m.id == nil || m.oldValue == nil { + return v, errors.New("OldOwnerID requires an ID field in the mutation") + } + oldValue, err := m.oldValue(ctx) + if err != nil { + return v, fmt.Errorf("querying old value for OldOwnerID: %w", err) + } + return oldValue.OwnerID, nil +} + +// ClearOwnerID clears the value of the "owner_id" field. +func (m *HarnessConfigMutation) ClearOwnerID() { + m.owner_id = nil + m.clearedFields[harnessconfig.FieldOwnerID] = struct{}{} +} + +// OwnerIDCleared returns if the "owner_id" field was cleared in this mutation. +func (m *HarnessConfigMutation) OwnerIDCleared() bool { + _, ok := m.clearedFields[harnessconfig.FieldOwnerID] + return ok +} + +// ResetOwnerID resets all changes to the "owner_id" field. +func (m *HarnessConfigMutation) ResetOwnerID() { + m.owner_id = nil + delete(m.clearedFields, harnessconfig.FieldOwnerID) +} + +// SetCreatedBy sets the "created_by" field. +func (m *HarnessConfigMutation) SetCreatedBy(s string) { + m.created_by = &s +} + +// CreatedBy returns the value of the "created_by" field in the mutation. +func (m *HarnessConfigMutation) CreatedBy() (r string, exists bool) { + v := m.created_by + if v == nil { + return + } + return *v, true +} + +// OldCreatedBy returns the old "created_by" field's value of the HarnessConfig entity. +// If the HarnessConfig object wasn't provided to the builder, the object is fetched from the database. +// An error is returned if the mutation operation is not UpdateOne, or the database query fails. +func (m *HarnessConfigMutation) OldCreatedBy(ctx context.Context) (v string, err error) { + if !m.op.Is(OpUpdateOne) { + return v, errors.New("OldCreatedBy is only allowed on UpdateOne operations") + } + if m.id == nil || m.oldValue == nil { + return v, errors.New("OldCreatedBy requires an ID field in the mutation") + } + oldValue, err := m.oldValue(ctx) + if err != nil { + return v, fmt.Errorf("querying old value for OldCreatedBy: %w", err) + } + return oldValue.CreatedBy, nil +} + +// ClearCreatedBy clears the value of the "created_by" field. +func (m *HarnessConfigMutation) ClearCreatedBy() { + m.created_by = nil + m.clearedFields[harnessconfig.FieldCreatedBy] = struct{}{} +} + +// CreatedByCleared returns if the "created_by" field was cleared in this mutation. +func (m *HarnessConfigMutation) CreatedByCleared() bool { + _, ok := m.clearedFields[harnessconfig.FieldCreatedBy] + return ok +} + +// ResetCreatedBy resets all changes to the "created_by" field. +func (m *HarnessConfigMutation) ResetCreatedBy() { + m.created_by = nil + delete(m.clearedFields, harnessconfig.FieldCreatedBy) +} + +// SetUpdatedBy sets the "updated_by" field. +func (m *HarnessConfigMutation) SetUpdatedBy(s string) { + m.updated_by = &s +} + +// UpdatedBy returns the value of the "updated_by" field in the mutation. +func (m *HarnessConfigMutation) UpdatedBy() (r string, exists bool) { + v := m.updated_by + if v == nil { + return + } + return *v, true +} + +// OldUpdatedBy returns the old "updated_by" field's value of the HarnessConfig entity. +// If the HarnessConfig object wasn't provided to the builder, the object is fetched from the database. +// An error is returned if the mutation operation is not UpdateOne, or the database query fails. +func (m *HarnessConfigMutation) OldUpdatedBy(ctx context.Context) (v string, err error) { + if !m.op.Is(OpUpdateOne) { + return v, errors.New("OldUpdatedBy is only allowed on UpdateOne operations") + } + if m.id == nil || m.oldValue == nil { + return v, errors.New("OldUpdatedBy requires an ID field in the mutation") + } + oldValue, err := m.oldValue(ctx) + if err != nil { + return v, fmt.Errorf("querying old value for OldUpdatedBy: %w", err) + } + return oldValue.UpdatedBy, nil +} + +// ClearUpdatedBy clears the value of the "updated_by" field. +func (m *HarnessConfigMutation) ClearUpdatedBy() { + m.updated_by = nil + m.clearedFields[harnessconfig.FieldUpdatedBy] = struct{}{} +} + +// UpdatedByCleared returns if the "updated_by" field was cleared in this mutation. +func (m *HarnessConfigMutation) UpdatedByCleared() bool { + _, ok := m.clearedFields[harnessconfig.FieldUpdatedBy] + return ok +} + +// ResetUpdatedBy resets all changes to the "updated_by" field. +func (m *HarnessConfigMutation) ResetUpdatedBy() { + m.updated_by = nil + delete(m.clearedFields, harnessconfig.FieldUpdatedBy) +} + +// SetVisibility sets the "visibility" field. +func (m *HarnessConfigMutation) SetVisibility(s string) { + m.visibility = &s +} + +// Visibility returns the value of the "visibility" field in the mutation. +func (m *HarnessConfigMutation) Visibility() (r string, exists bool) { + v := m.visibility + if v == nil { + return + } + return *v, true +} + +// OldVisibility returns the old "visibility" field's value of the HarnessConfig entity. +// If the HarnessConfig object wasn't provided to the builder, the object is fetched from the database. +// An error is returned if the mutation operation is not UpdateOne, or the database query fails. +func (m *HarnessConfigMutation) OldVisibility(ctx context.Context) (v string, err error) { + if !m.op.Is(OpUpdateOne) { + return v, errors.New("OldVisibility is only allowed on UpdateOne operations") + } + if m.id == nil || m.oldValue == nil { + return v, errors.New("OldVisibility requires an ID field in the mutation") + } + oldValue, err := m.oldValue(ctx) + if err != nil { + return v, fmt.Errorf("querying old value for OldVisibility: %w", err) + } + return oldValue.Visibility, nil +} + +// ResetVisibility resets all changes to the "visibility" field. +func (m *HarnessConfigMutation) ResetVisibility() { + m.visibility = nil +} + +// SetCreated sets the "created" field. +func (m *HarnessConfigMutation) SetCreated(t time.Time) { + m.created = &t +} + +// Created returns the value of the "created" field in the mutation. +func (m *HarnessConfigMutation) Created() (r time.Time, exists bool) { + v := m.created + if v == nil { + return + } + return *v, true +} + +// OldCreated returns the old "created" field's value of the HarnessConfig entity. +// If the HarnessConfig object wasn't provided to the builder, the object is fetched from the database. +// An error is returned if the mutation operation is not UpdateOne, or the database query fails. +func (m *HarnessConfigMutation) OldCreated(ctx context.Context) (v time.Time, err error) { + if !m.op.Is(OpUpdateOne) { + return v, errors.New("OldCreated is only allowed on UpdateOne operations") + } + if m.id == nil || m.oldValue == nil { + return v, errors.New("OldCreated requires an ID field in the mutation") + } + oldValue, err := m.oldValue(ctx) + if err != nil { + return v, fmt.Errorf("querying old value for OldCreated: %w", err) + } + return oldValue.Created, nil +} + +// ResetCreated resets all changes to the "created" field. +func (m *HarnessConfigMutation) ResetCreated() { + m.created = nil +} + +// SetUpdated sets the "updated" field. +func (m *HarnessConfigMutation) SetUpdated(t time.Time) { + m.updated = &t +} + +// Updated returns the value of the "updated" field in the mutation. +func (m *HarnessConfigMutation) Updated() (r time.Time, exists bool) { + v := m.updated + if v == nil { + return + } + return *v, true +} + +// OldUpdated returns the old "updated" field's value of the HarnessConfig entity. +// If the HarnessConfig object wasn't provided to the builder, the object is fetched from the database. +// An error is returned if the mutation operation is not UpdateOne, or the database query fails. +func (m *HarnessConfigMutation) OldUpdated(ctx context.Context) (v time.Time, err error) { + if !m.op.Is(OpUpdateOne) { + return v, errors.New("OldUpdated is only allowed on UpdateOne operations") + } + if m.id == nil || m.oldValue == nil { + return v, errors.New("OldUpdated requires an ID field in the mutation") + } + oldValue, err := m.oldValue(ctx) + if err != nil { + return v, fmt.Errorf("querying old value for OldUpdated: %w", err) + } + return oldValue.Updated, nil +} + +// ResetUpdated resets all changes to the "updated" field. +func (m *HarnessConfigMutation) ResetUpdated() { + m.updated = nil +} + +// Where appends a list predicates to the HarnessConfigMutation builder. +func (m *HarnessConfigMutation) Where(ps ...predicate.HarnessConfig) { + m.predicates = append(m.predicates, ps...) +} + +// WhereP appends storage-level predicates to the HarnessConfigMutation builder. Using this method, +// users can use type-assertion to append predicates that do not depend on any generated package. +func (m *HarnessConfigMutation) WhereP(ps ...func(*sql.Selector)) { + p := make([]predicate.HarnessConfig, len(ps)) + for i := range ps { + p[i] = ps[i] + } + m.Where(p...) +} + +// Op returns the operation name. +func (m *HarnessConfigMutation) Op() Op { + return m.op +} + +// SetOp allows setting the mutation operation. +func (m *HarnessConfigMutation) SetOp(op Op) { + m.op = op +} + +// Type returns the node type of this mutation (HarnessConfig). +func (m *HarnessConfigMutation) Type() string { + return m.typ +} + +// Fields returns all fields that were changed during this mutation. Note that in +// order to get all numeric fields that were incremented/decremented, call +// AddedFields(). +func (m *HarnessConfigMutation) Fields() []string { + fields := make([]string, 0, 20) + if m.name != nil { + fields = append(fields, harnessconfig.FieldName) + } + if m.slug != nil { + fields = append(fields, harnessconfig.FieldSlug) + } + if m.display_name != nil { + fields = append(fields, harnessconfig.FieldDisplayName) + } + if m.description != nil { + fields = append(fields, harnessconfig.FieldDescription) + } + if m.harness != nil { + fields = append(fields, harnessconfig.FieldHarness) + } + if m._config != nil { + fields = append(fields, harnessconfig.FieldConfig) + } + if m.content_hash != nil { + fields = append(fields, harnessconfig.FieldContentHash) + } + if m.scope != nil { + fields = append(fields, harnessconfig.FieldScope) + } + if m.scope_id != nil { + fields = append(fields, harnessconfig.FieldScopeID) + } + if m.storage_uri != nil { + fields = append(fields, harnessconfig.FieldStorageURI) + } + if m.storage_bucket != nil { + fields = append(fields, harnessconfig.FieldStorageBucket) + } + if m.storage_path != nil { + fields = append(fields, harnessconfig.FieldStoragePath) + } + if m.files != nil { + fields = append(fields, harnessconfig.FieldFiles) + } + if m.status != nil { + fields = append(fields, harnessconfig.FieldStatus) + } + if m.owner_id != nil { + fields = append(fields, harnessconfig.FieldOwnerID) + } + if m.created_by != nil { + fields = append(fields, harnessconfig.FieldCreatedBy) + } + if m.updated_by != nil { + fields = append(fields, harnessconfig.FieldUpdatedBy) + } + if m.visibility != nil { + fields = append(fields, harnessconfig.FieldVisibility) + } + if m.created != nil { + fields = append(fields, harnessconfig.FieldCreated) + } + if m.updated != nil { + fields = append(fields, harnessconfig.FieldUpdated) + } + return fields +} + +// Field returns the value of a field with the given name. The second boolean +// return value indicates that this field was not set, or was not defined in the +// schema. +func (m *HarnessConfigMutation) Field(name string) (ent.Value, bool) { + switch name { + case harnessconfig.FieldName: + return m.Name() + case harnessconfig.FieldSlug: + return m.Slug() + case harnessconfig.FieldDisplayName: + return m.DisplayName() + case harnessconfig.FieldDescription: + return m.Description() + case harnessconfig.FieldHarness: + return m.Harness() + case harnessconfig.FieldConfig: + return m.Config() + case harnessconfig.FieldContentHash: + return m.ContentHash() + case harnessconfig.FieldScope: + return m.Scope() + case harnessconfig.FieldScopeID: + return m.ScopeID() + case harnessconfig.FieldStorageURI: + return m.StorageURI() + case harnessconfig.FieldStorageBucket: + return m.StorageBucket() + case harnessconfig.FieldStoragePath: + return m.StoragePath() + case harnessconfig.FieldFiles: + return m.Files() + case harnessconfig.FieldStatus: + return m.Status() + case harnessconfig.FieldOwnerID: + return m.OwnerID() + case harnessconfig.FieldCreatedBy: + return m.CreatedBy() + case harnessconfig.FieldUpdatedBy: + return m.UpdatedBy() + case harnessconfig.FieldVisibility: + return m.Visibility() + case harnessconfig.FieldCreated: + return m.Created() + case harnessconfig.FieldUpdated: + return m.Updated() + } + return nil, false +} + +// OldField returns the old value of the field from the database. An error is +// returned if the mutation operation is not UpdateOne, or the query to the +// database failed. +func (m *HarnessConfigMutation) OldField(ctx context.Context, name string) (ent.Value, error) { + switch name { + case harnessconfig.FieldName: + return m.OldName(ctx) + case harnessconfig.FieldSlug: + return m.OldSlug(ctx) + case harnessconfig.FieldDisplayName: + return m.OldDisplayName(ctx) + case harnessconfig.FieldDescription: + return m.OldDescription(ctx) + case harnessconfig.FieldHarness: + return m.OldHarness(ctx) + case harnessconfig.FieldConfig: + return m.OldConfig(ctx) + case harnessconfig.FieldContentHash: + return m.OldContentHash(ctx) + case harnessconfig.FieldScope: + return m.OldScope(ctx) + case harnessconfig.FieldScopeID: + return m.OldScopeID(ctx) + case harnessconfig.FieldStorageURI: + return m.OldStorageURI(ctx) + case harnessconfig.FieldStorageBucket: + return m.OldStorageBucket(ctx) + case harnessconfig.FieldStoragePath: + return m.OldStoragePath(ctx) + case harnessconfig.FieldFiles: + return m.OldFiles(ctx) + case harnessconfig.FieldStatus: + return m.OldStatus(ctx) + case harnessconfig.FieldOwnerID: + return m.OldOwnerID(ctx) + case harnessconfig.FieldCreatedBy: + return m.OldCreatedBy(ctx) + case harnessconfig.FieldUpdatedBy: + return m.OldUpdatedBy(ctx) + case harnessconfig.FieldVisibility: + return m.OldVisibility(ctx) + case harnessconfig.FieldCreated: + return m.OldCreated(ctx) + case harnessconfig.FieldUpdated: + return m.OldUpdated(ctx) + } + return nil, fmt.Errorf("unknown HarnessConfig field %s", name) +} + +// SetField sets the value of a field with the given name. It returns an error if +// the field is not defined in the schema, or if the type mismatched the field +// type. +func (m *HarnessConfigMutation) SetField(name string, value ent.Value) error { + switch name { + case harnessconfig.FieldName: + v, ok := value.(string) + if !ok { + return fmt.Errorf("unexpected type %T for field %s", value, name) + } + m.SetName(v) + return nil + case harnessconfig.FieldSlug: + v, ok := value.(string) + if !ok { + return fmt.Errorf("unexpected type %T for field %s", value, name) + } + m.SetSlug(v) + return nil + case harnessconfig.FieldDisplayName: + v, ok := value.(string) + if !ok { + return fmt.Errorf("unexpected type %T for field %s", value, name) + } + m.SetDisplayName(v) + return nil + case harnessconfig.FieldDescription: + v, ok := value.(string) + if !ok { + return fmt.Errorf("unexpected type %T for field %s", value, name) + } + m.SetDescription(v) + return nil + case harnessconfig.FieldHarness: + v, ok := value.(string) + if !ok { + return fmt.Errorf("unexpected type %T for field %s", value, name) + } + m.SetHarness(v) + return nil + case harnessconfig.FieldConfig: + v, ok := value.(string) + if !ok { + return fmt.Errorf("unexpected type %T for field %s", value, name) + } + m.SetConfig(v) + return nil + case harnessconfig.FieldContentHash: + v, ok := value.(string) + if !ok { + return fmt.Errorf("unexpected type %T for field %s", value, name) + } + m.SetContentHash(v) + return nil + case harnessconfig.FieldScope: + v, ok := value.(string) + if !ok { + return fmt.Errorf("unexpected type %T for field %s", value, name) + } + m.SetScope(v) + return nil + case harnessconfig.FieldScopeID: + v, ok := value.(string) + if !ok { + return fmt.Errorf("unexpected type %T for field %s", value, name) + } + m.SetScopeID(v) + return nil + case harnessconfig.FieldStorageURI: + v, ok := value.(string) + if !ok { + return fmt.Errorf("unexpected type %T for field %s", value, name) + } + m.SetStorageURI(v) + return nil + case harnessconfig.FieldStorageBucket: + v, ok := value.(string) + if !ok { + return fmt.Errorf("unexpected type %T for field %s", value, name) + } + m.SetStorageBucket(v) + return nil + case harnessconfig.FieldStoragePath: + v, ok := value.(string) + if !ok { + return fmt.Errorf("unexpected type %T for field %s", value, name) + } + m.SetStoragePath(v) + return nil + case harnessconfig.FieldFiles: + v, ok := value.(string) + if !ok { + return fmt.Errorf("unexpected type %T for field %s", value, name) + } + m.SetFiles(v) + return nil + case harnessconfig.FieldStatus: + v, ok := value.(harnessconfig.Status) + if !ok { + return fmt.Errorf("unexpected type %T for field %s", value, name) + } + m.SetStatus(v) + return nil + case harnessconfig.FieldOwnerID: + v, ok := value.(string) + if !ok { + return fmt.Errorf("unexpected type %T for field %s", value, name) + } + m.SetOwnerID(v) + return nil + case harnessconfig.FieldCreatedBy: + v, ok := value.(string) + if !ok { + return fmt.Errorf("unexpected type %T for field %s", value, name) + } + m.SetCreatedBy(v) + return nil + case harnessconfig.FieldUpdatedBy: + v, ok := value.(string) + if !ok { + return fmt.Errorf("unexpected type %T for field %s", value, name) + } + m.SetUpdatedBy(v) + return nil + case harnessconfig.FieldVisibility: + v, ok := value.(string) + if !ok { + return fmt.Errorf("unexpected type %T for field %s", value, name) + } + m.SetVisibility(v) + return nil + case harnessconfig.FieldCreated: + v, ok := value.(time.Time) + if !ok { + return fmt.Errorf("unexpected type %T for field %s", value, name) + } + m.SetCreated(v) + return nil + case harnessconfig.FieldUpdated: + v, ok := value.(time.Time) + if !ok { + return fmt.Errorf("unexpected type %T for field %s", value, name) + } + m.SetUpdated(v) + return nil + } + return fmt.Errorf("unknown HarnessConfig field %s", name) +} + +// AddedFields returns all numeric fields that were incremented/decremented during +// this mutation. +func (m *HarnessConfigMutation) AddedFields() []string { + return nil +} + +// AddedField returns the numeric value that was incremented/decremented on a field +// with the given name. The second boolean return value indicates that this field +// was not set, or was not defined in the schema. +func (m *HarnessConfigMutation) AddedField(name string) (ent.Value, bool) { + return nil, false +} + +// AddField adds the value to the field with the given name. It returns an error if +// the field is not defined in the schema, or if the type mismatched the field +// type. +func (m *HarnessConfigMutation) AddField(name string, value ent.Value) error { + switch name { + } + return fmt.Errorf("unknown HarnessConfig numeric field %s", name) +} + +// ClearedFields returns all nullable fields that were cleared during this +// mutation. +func (m *HarnessConfigMutation) ClearedFields() []string { + var fields []string + if m.FieldCleared(harnessconfig.FieldDisplayName) { + fields = append(fields, harnessconfig.FieldDisplayName) + } + if m.FieldCleared(harnessconfig.FieldDescription) { + fields = append(fields, harnessconfig.FieldDescription) + } + if m.FieldCleared(harnessconfig.FieldConfig) { + fields = append(fields, harnessconfig.FieldConfig) + } + if m.FieldCleared(harnessconfig.FieldContentHash) { + fields = append(fields, harnessconfig.FieldContentHash) + } + if m.FieldCleared(harnessconfig.FieldScopeID) { + fields = append(fields, harnessconfig.FieldScopeID) + } + if m.FieldCleared(harnessconfig.FieldStorageURI) { + fields = append(fields, harnessconfig.FieldStorageURI) + } + if m.FieldCleared(harnessconfig.FieldStorageBucket) { + fields = append(fields, harnessconfig.FieldStorageBucket) + } + if m.FieldCleared(harnessconfig.FieldStoragePath) { + fields = append(fields, harnessconfig.FieldStoragePath) + } + if m.FieldCleared(harnessconfig.FieldFiles) { + fields = append(fields, harnessconfig.FieldFiles) + } + if m.FieldCleared(harnessconfig.FieldOwnerID) { + fields = append(fields, harnessconfig.FieldOwnerID) + } + if m.FieldCleared(harnessconfig.FieldCreatedBy) { + fields = append(fields, harnessconfig.FieldCreatedBy) + } + if m.FieldCleared(harnessconfig.FieldUpdatedBy) { + fields = append(fields, harnessconfig.FieldUpdatedBy) + } + return fields +} + +// FieldCleared returns a boolean indicating if a field with the given name was +// cleared in this mutation. +func (m *HarnessConfigMutation) FieldCleared(name string) bool { + _, ok := m.clearedFields[name] + return ok +} + +// ClearField clears the value of the field with the given name. It returns an +// error if the field is not defined in the schema. +func (m *HarnessConfigMutation) ClearField(name string) error { + switch name { + case harnessconfig.FieldDisplayName: + m.ClearDisplayName() + return nil + case harnessconfig.FieldDescription: + m.ClearDescription() + return nil + case harnessconfig.FieldConfig: + m.ClearConfig() + return nil + case harnessconfig.FieldContentHash: + m.ClearContentHash() + return nil + case harnessconfig.FieldScopeID: + m.ClearScopeID() + return nil + case harnessconfig.FieldStorageURI: + m.ClearStorageURI() + return nil + case harnessconfig.FieldStorageBucket: + m.ClearStorageBucket() + return nil + case harnessconfig.FieldStoragePath: + m.ClearStoragePath() + return nil + case harnessconfig.FieldFiles: + m.ClearFiles() + return nil + case harnessconfig.FieldOwnerID: + m.ClearOwnerID() + return nil + case harnessconfig.FieldCreatedBy: + m.ClearCreatedBy() + return nil + case harnessconfig.FieldUpdatedBy: + m.ClearUpdatedBy() + return nil + } + return fmt.Errorf("unknown HarnessConfig nullable field %s", name) +} + +// ResetField resets all changes in the mutation for the field with the given name. +// It returns an error if the field is not defined in the schema. +func (m *HarnessConfigMutation) ResetField(name string) error { + switch name { + case harnessconfig.FieldName: + m.ResetName() + return nil + case harnessconfig.FieldSlug: + m.ResetSlug() + return nil + case harnessconfig.FieldDisplayName: + m.ResetDisplayName() + return nil + case harnessconfig.FieldDescription: + m.ResetDescription() + return nil + case harnessconfig.FieldHarness: + m.ResetHarness() + return nil + case harnessconfig.FieldConfig: + m.ResetConfig() + return nil + case harnessconfig.FieldContentHash: + m.ResetContentHash() + return nil + case harnessconfig.FieldScope: + m.ResetScope() + return nil + case harnessconfig.FieldScopeID: + m.ResetScopeID() + return nil + case harnessconfig.FieldStorageURI: + m.ResetStorageURI() + return nil + case harnessconfig.FieldStorageBucket: + m.ResetStorageBucket() + return nil + case harnessconfig.FieldStoragePath: + m.ResetStoragePath() + return nil + case harnessconfig.FieldFiles: + m.ResetFiles() + return nil + case harnessconfig.FieldStatus: + m.ResetStatus() + return nil + case harnessconfig.FieldOwnerID: + m.ResetOwnerID() + return nil + case harnessconfig.FieldCreatedBy: + m.ResetCreatedBy() + return nil + case harnessconfig.FieldUpdatedBy: + m.ResetUpdatedBy() + return nil + case harnessconfig.FieldVisibility: + m.ResetVisibility() + return nil + case harnessconfig.FieldCreated: + m.ResetCreated() + return nil + case harnessconfig.FieldUpdated: + m.ResetUpdated() + return nil + } + return fmt.Errorf("unknown HarnessConfig field %s", name) +} + +// AddedEdges returns all edge names that were set/added in this mutation. +func (m *HarnessConfigMutation) AddedEdges() []string { + edges := make([]string, 0, 0) + return edges +} + +// AddedIDs returns all IDs (to other nodes) that were added for the given edge +// name in this mutation. +func (m *HarnessConfigMutation) AddedIDs(name string) []ent.Value { + return nil +} + +// RemovedEdges returns all edge names that were removed in this mutation. +func (m *HarnessConfigMutation) RemovedEdges() []string { + edges := make([]string, 0, 0) + return edges +} + +// RemovedIDs returns all IDs (to other nodes) that were removed for the edge with +// the given name in this mutation. +func (m *HarnessConfigMutation) RemovedIDs(name string) []ent.Value { + return nil +} + +// ClearedEdges returns all edge names that were cleared in this mutation. +func (m *HarnessConfigMutation) ClearedEdges() []string { + edges := make([]string, 0, 0) + return edges +} + +// EdgeCleared returns a boolean which indicates if the edge with the given name +// was cleared in this mutation. +func (m *HarnessConfigMutation) EdgeCleared(name string) bool { + return false +} + +// ClearEdge clears the value of the edge with the given name. It returns an error +// if that edge is not defined in the schema. +func (m *HarnessConfigMutation) ClearEdge(name string) error { + return fmt.Errorf("unknown HarnessConfig unique edge %s", name) +} + +// ResetEdge resets all changes to the edge with the given name in this mutation. +// It returns an error if the edge is not defined in the schema. +func (m *HarnessConfigMutation) ResetEdge(name string) error { + return fmt.Errorf("unknown HarnessConfig edge %s", name) +} + +// InviteCodeMutation represents an operation that mutates the InviteCode nodes in the graph. +type InviteCodeMutation struct { + config + op Op + typ string + id *uuid.UUID + code_hash *string + code_prefix *string + max_uses *int + addmax_uses *int + use_count *int + adduse_count *int + expires_at *time.Time + revoked *bool + created_by *string + note *string + created *time.Time + clearedFields map[string]struct{} + done bool + oldValue func(context.Context) (*InviteCode, error) + predicates []predicate.InviteCode +} + +var _ ent.Mutation = (*InviteCodeMutation)(nil) + +// invitecodeOption allows management of the mutation configuration using functional options. +type invitecodeOption func(*InviteCodeMutation) + +// newInviteCodeMutation creates new mutation for the InviteCode entity. +func newInviteCodeMutation(c config, op Op, opts ...invitecodeOption) *InviteCodeMutation { + m := &InviteCodeMutation{ + config: c, + op: op, + typ: TypeInviteCode, + clearedFields: make(map[string]struct{}), + } + for _, opt := range opts { + opt(m) + } + return m +} + +// withInviteCodeID sets the ID field of the mutation. +func withInviteCodeID(id uuid.UUID) invitecodeOption { + return func(m *InviteCodeMutation) { + var ( + err error + once sync.Once + value *InviteCode + ) + m.oldValue = func(ctx context.Context) (*InviteCode, error) { + once.Do(func() { + if m.done { + err = errors.New("querying old values post mutation is not allowed") + } else { + value, err = m.Client().InviteCode.Get(ctx, id) + } + }) + return value, err + } + m.id = &id + } +} + +// withInviteCode sets the old InviteCode of the mutation. +func withInviteCode(node *InviteCode) invitecodeOption { + return func(m *InviteCodeMutation) { + m.oldValue = func(context.Context) (*InviteCode, error) { + return node, nil + } + m.id = &node.ID + } +} + +// Client returns a new `ent.Client` from the mutation. If the mutation was +// executed in a transaction (ent.Tx), a transactional client is returned. +func (m InviteCodeMutation) Client() *Client { + client := &Client{config: m.config} + client.init() + return client +} + +// Tx returns an `ent.Tx` for mutations that were executed in transactions; +// it returns an error otherwise. +func (m InviteCodeMutation) Tx() (*Tx, error) { + if _, ok := m.driver.(*txDriver); !ok { + return nil, errors.New("ent: mutation is not running in a transaction") + } + tx := &Tx{config: m.config} + tx.init() + return tx, nil +} + +// SetID sets the value of the id field. Note that this +// operation is only accepted on creation of InviteCode entities. +func (m *InviteCodeMutation) SetID(id uuid.UUID) { + m.id = &id +} + +// ID returns the ID value in the mutation. Note that the ID is only available +// if it was provided to the builder or after it was returned from the database. +func (m *InviteCodeMutation) ID() (id uuid.UUID, exists bool) { + if m.id == nil { + return + } + return *m.id, true +} + +// IDs queries the database and returns the entity ids that match the mutation's predicate. +// That means, if the mutation is applied within a transaction with an isolation level such +// as sql.LevelSerializable, the returned ids match the ids of the rows that will be updated +// or updated by the mutation. +func (m *InviteCodeMutation) IDs(ctx context.Context) ([]uuid.UUID, error) { + switch { + case m.op.Is(OpUpdateOne | OpDeleteOne): + id, exists := m.ID() + if exists { + return []uuid.UUID{id}, nil + } + fallthrough + case m.op.Is(OpUpdate | OpDelete): + return m.Client().InviteCode.Query().Where(m.predicates...).IDs(ctx) + default: + return nil, fmt.Errorf("IDs is not allowed on %s operations", m.op) + } +} + +// SetCodeHash sets the "code_hash" field. +func (m *InviteCodeMutation) SetCodeHash(s string) { + m.code_hash = &s +} + +// CodeHash returns the value of the "code_hash" field in the mutation. +func (m *InviteCodeMutation) CodeHash() (r string, exists bool) { + v := m.code_hash + if v == nil { + return + } + return *v, true +} + +// OldCodeHash returns the old "code_hash" field's value of the InviteCode entity. +// If the InviteCode object wasn't provided to the builder, the object is fetched from the database. +// An error is returned if the mutation operation is not UpdateOne, or the database query fails. +func (m *InviteCodeMutation) OldCodeHash(ctx context.Context) (v string, err error) { + if !m.op.Is(OpUpdateOne) { + return v, errors.New("OldCodeHash is only allowed on UpdateOne operations") + } + if m.id == nil || m.oldValue == nil { + return v, errors.New("OldCodeHash requires an ID field in the mutation") + } + oldValue, err := m.oldValue(ctx) + if err != nil { + return v, fmt.Errorf("querying old value for OldCodeHash: %w", err) + } + return oldValue.CodeHash, nil +} + +// ResetCodeHash resets all changes to the "code_hash" field. +func (m *InviteCodeMutation) ResetCodeHash() { + m.code_hash = nil +} + +// SetCodePrefix sets the "code_prefix" field. +func (m *InviteCodeMutation) SetCodePrefix(s string) { + m.code_prefix = &s +} + +// CodePrefix returns the value of the "code_prefix" field in the mutation. +func (m *InviteCodeMutation) CodePrefix() (r string, exists bool) { + v := m.code_prefix + if v == nil { + return + } + return *v, true +} + +// OldCodePrefix returns the old "code_prefix" field's value of the InviteCode entity. +// If the InviteCode object wasn't provided to the builder, the object is fetched from the database. +// An error is returned if the mutation operation is not UpdateOne, or the database query fails. +func (m *InviteCodeMutation) OldCodePrefix(ctx context.Context) (v string, err error) { + if !m.op.Is(OpUpdateOne) { + return v, errors.New("OldCodePrefix is only allowed on UpdateOne operations") + } + if m.id == nil || m.oldValue == nil { + return v, errors.New("OldCodePrefix requires an ID field in the mutation") + } + oldValue, err := m.oldValue(ctx) + if err != nil { + return v, fmt.Errorf("querying old value for OldCodePrefix: %w", err) + } + return oldValue.CodePrefix, nil +} + +// ResetCodePrefix resets all changes to the "code_prefix" field. +func (m *InviteCodeMutation) ResetCodePrefix() { + m.code_prefix = nil +} + +// SetMaxUses sets the "max_uses" field. +func (m *InviteCodeMutation) SetMaxUses(i int) { + m.max_uses = &i + m.addmax_uses = nil +} + +// MaxUses returns the value of the "max_uses" field in the mutation. +func (m *InviteCodeMutation) MaxUses() (r int, exists bool) { + v := m.max_uses + if v == nil { + return + } + return *v, true +} + +// OldMaxUses returns the old "max_uses" field's value of the InviteCode entity. +// If the InviteCode object wasn't provided to the builder, the object is fetched from the database. +// An error is returned if the mutation operation is not UpdateOne, or the database query fails. +func (m *InviteCodeMutation) OldMaxUses(ctx context.Context) (v int, err error) { + if !m.op.Is(OpUpdateOne) { + return v, errors.New("OldMaxUses is only allowed on UpdateOne operations") + } + if m.id == nil || m.oldValue == nil { + return v, errors.New("OldMaxUses requires an ID field in the mutation") + } + oldValue, err := m.oldValue(ctx) + if err != nil { + return v, fmt.Errorf("querying old value for OldMaxUses: %w", err) + } + return oldValue.MaxUses, nil +} + +// AddMaxUses adds i to the "max_uses" field. +func (m *InviteCodeMutation) AddMaxUses(i int) { + if m.addmax_uses != nil { + *m.addmax_uses += i + } else { + m.addmax_uses = &i + } +} + +// AddedMaxUses returns the value that was added to the "max_uses" field in this mutation. +func (m *InviteCodeMutation) AddedMaxUses() (r int, exists bool) { + v := m.addmax_uses + if v == nil { + return + } + return *v, true +} + +// ResetMaxUses resets all changes to the "max_uses" field. +func (m *InviteCodeMutation) ResetMaxUses() { + m.max_uses = nil + m.addmax_uses = nil +} + +// SetUseCount sets the "use_count" field. +func (m *InviteCodeMutation) SetUseCount(i int) { + m.use_count = &i + m.adduse_count = nil +} + +// UseCount returns the value of the "use_count" field in the mutation. +func (m *InviteCodeMutation) UseCount() (r int, exists bool) { + v := m.use_count + if v == nil { + return + } + return *v, true +} + +// OldUseCount returns the old "use_count" field's value of the InviteCode entity. +// If the InviteCode object wasn't provided to the builder, the object is fetched from the database. +// An error is returned if the mutation operation is not UpdateOne, or the database query fails. +func (m *InviteCodeMutation) OldUseCount(ctx context.Context) (v int, err error) { + if !m.op.Is(OpUpdateOne) { + return v, errors.New("OldUseCount is only allowed on UpdateOne operations") + } + if m.id == nil || m.oldValue == nil { + return v, errors.New("OldUseCount requires an ID field in the mutation") + } + oldValue, err := m.oldValue(ctx) + if err != nil { + return v, fmt.Errorf("querying old value for OldUseCount: %w", err) + } + return oldValue.UseCount, nil +} + +// AddUseCount adds i to the "use_count" field. +func (m *InviteCodeMutation) AddUseCount(i int) { + if m.adduse_count != nil { + *m.adduse_count += i + } else { + m.adduse_count = &i + } +} + +// AddedUseCount returns the value that was added to the "use_count" field in this mutation. +func (m *InviteCodeMutation) AddedUseCount() (r int, exists bool) { + v := m.adduse_count + if v == nil { + return + } + return *v, true +} + +// ResetUseCount resets all changes to the "use_count" field. +func (m *InviteCodeMutation) ResetUseCount() { + m.use_count = nil + m.adduse_count = nil +} + +// SetExpiresAt sets the "expires_at" field. +func (m *InviteCodeMutation) SetExpiresAt(t time.Time) { + m.expires_at = &t +} + +// ExpiresAt returns the value of the "expires_at" field in the mutation. +func (m *InviteCodeMutation) ExpiresAt() (r time.Time, exists bool) { + v := m.expires_at + if v == nil { + return + } + return *v, true +} + +// OldExpiresAt returns the old "expires_at" field's value of the InviteCode entity. +// If the InviteCode object wasn't provided to the builder, the object is fetched from the database. +// An error is returned if the mutation operation is not UpdateOne, or the database query fails. +func (m *InviteCodeMutation) OldExpiresAt(ctx context.Context) (v time.Time, err error) { + if !m.op.Is(OpUpdateOne) { + return v, errors.New("OldExpiresAt is only allowed on UpdateOne operations") + } + if m.id == nil || m.oldValue == nil { + return v, errors.New("OldExpiresAt requires an ID field in the mutation") + } + oldValue, err := m.oldValue(ctx) + if err != nil { + return v, fmt.Errorf("querying old value for OldExpiresAt: %w", err) + } + return oldValue.ExpiresAt, nil +} + +// ResetExpiresAt resets all changes to the "expires_at" field. +func (m *InviteCodeMutation) ResetExpiresAt() { + m.expires_at = nil +} + +// SetRevoked sets the "revoked" field. +func (m *InviteCodeMutation) SetRevoked(b bool) { + m.revoked = &b +} + +// Revoked returns the value of the "revoked" field in the mutation. +func (m *InviteCodeMutation) Revoked() (r bool, exists bool) { + v := m.revoked + if v == nil { + return + } + return *v, true +} + +// OldRevoked returns the old "revoked" field's value of the InviteCode entity. +// If the InviteCode object wasn't provided to the builder, the object is fetched from the database. +// An error is returned if the mutation operation is not UpdateOne, or the database query fails. +func (m *InviteCodeMutation) OldRevoked(ctx context.Context) (v bool, err error) { + if !m.op.Is(OpUpdateOne) { + return v, errors.New("OldRevoked is only allowed on UpdateOne operations") + } + if m.id == nil || m.oldValue == nil { + return v, errors.New("OldRevoked requires an ID field in the mutation") + } + oldValue, err := m.oldValue(ctx) + if err != nil { + return v, fmt.Errorf("querying old value for OldRevoked: %w", err) + } + return oldValue.Revoked, nil +} + +// ResetRevoked resets all changes to the "revoked" field. +func (m *InviteCodeMutation) ResetRevoked() { + m.revoked = nil +} + +// SetCreatedBy sets the "created_by" field. +func (m *InviteCodeMutation) SetCreatedBy(s string) { + m.created_by = &s +} + +// CreatedBy returns the value of the "created_by" field in the mutation. +func (m *InviteCodeMutation) CreatedBy() (r string, exists bool) { + v := m.created_by + if v == nil { + return + } + return *v, true +} + +// OldCreatedBy returns the old "created_by" field's value of the InviteCode entity. +// If the InviteCode object wasn't provided to the builder, the object is fetched from the database. +// An error is returned if the mutation operation is not UpdateOne, or the database query fails. +func (m *InviteCodeMutation) OldCreatedBy(ctx context.Context) (v string, err error) { + if !m.op.Is(OpUpdateOne) { + return v, errors.New("OldCreatedBy is only allowed on UpdateOne operations") + } + if m.id == nil || m.oldValue == nil { + return v, errors.New("OldCreatedBy requires an ID field in the mutation") + } + oldValue, err := m.oldValue(ctx) + if err != nil { + return v, fmt.Errorf("querying old value for OldCreatedBy: %w", err) + } + return oldValue.CreatedBy, nil +} + +// ResetCreatedBy resets all changes to the "created_by" field. +func (m *InviteCodeMutation) ResetCreatedBy() { + m.created_by = nil +} + +// SetNote sets the "note" field. +func (m *InviteCodeMutation) SetNote(s string) { + m.note = &s +} + +// Note returns the value of the "note" field in the mutation. +func (m *InviteCodeMutation) Note() (r string, exists bool) { + v := m.note + if v == nil { + return + } + return *v, true +} + +// OldNote returns the old "note" field's value of the InviteCode entity. +// If the InviteCode object wasn't provided to the builder, the object is fetched from the database. +// An error is returned if the mutation operation is not UpdateOne, or the database query fails. +func (m *InviteCodeMutation) OldNote(ctx context.Context) (v string, err error) { + if !m.op.Is(OpUpdateOne) { + return v, errors.New("OldNote is only allowed on UpdateOne operations") + } + if m.id == nil || m.oldValue == nil { + return v, errors.New("OldNote requires an ID field in the mutation") + } + oldValue, err := m.oldValue(ctx) + if err != nil { + return v, fmt.Errorf("querying old value for OldNote: %w", err) + } + return oldValue.Note, nil +} + +// ResetNote resets all changes to the "note" field. +func (m *InviteCodeMutation) ResetNote() { + m.note = nil +} + +// SetCreated sets the "created" field. +func (m *InviteCodeMutation) SetCreated(t time.Time) { + m.created = &t +} + +// Created returns the value of the "created" field in the mutation. +func (m *InviteCodeMutation) Created() (r time.Time, exists bool) { + v := m.created + if v == nil { + return + } + return *v, true +} + +// OldCreated returns the old "created" field's value of the InviteCode entity. +// If the InviteCode object wasn't provided to the builder, the object is fetched from the database. +// An error is returned if the mutation operation is not UpdateOne, or the database query fails. +func (m *InviteCodeMutation) OldCreated(ctx context.Context) (v time.Time, err error) { + if !m.op.Is(OpUpdateOne) { + return v, errors.New("OldCreated is only allowed on UpdateOne operations") + } + if m.id == nil || m.oldValue == nil { + return v, errors.New("OldCreated requires an ID field in the mutation") + } + oldValue, err := m.oldValue(ctx) + if err != nil { + return v, fmt.Errorf("querying old value for OldCreated: %w", err) + } + return oldValue.Created, nil +} + +// ResetCreated resets all changes to the "created" field. +func (m *InviteCodeMutation) ResetCreated() { + m.created = nil +} + +// Where appends a list predicates to the InviteCodeMutation builder. +func (m *InviteCodeMutation) Where(ps ...predicate.InviteCode) { + m.predicates = append(m.predicates, ps...) +} + +// WhereP appends storage-level predicates to the InviteCodeMutation builder. Using this method, +// users can use type-assertion to append predicates that do not depend on any generated package. +func (m *InviteCodeMutation) WhereP(ps ...func(*sql.Selector)) { + p := make([]predicate.InviteCode, len(ps)) + for i := range ps { + p[i] = ps[i] + } + m.Where(p...) +} + +// Op returns the operation name. +func (m *InviteCodeMutation) Op() Op { + return m.op +} + +// SetOp allows setting the mutation operation. +func (m *InviteCodeMutation) SetOp(op Op) { + m.op = op +} + +// Type returns the node type of this mutation (InviteCode). +func (m *InviteCodeMutation) Type() string { + return m.typ +} + +// Fields returns all fields that were changed during this mutation. Note that in +// order to get all numeric fields that were incremented/decremented, call +// AddedFields(). +func (m *InviteCodeMutation) Fields() []string { + fields := make([]string, 0, 9) + if m.code_hash != nil { + fields = append(fields, invitecode.FieldCodeHash) + } + if m.code_prefix != nil { + fields = append(fields, invitecode.FieldCodePrefix) + } + if m.max_uses != nil { + fields = append(fields, invitecode.FieldMaxUses) + } + if m.use_count != nil { + fields = append(fields, invitecode.FieldUseCount) + } + if m.expires_at != nil { + fields = append(fields, invitecode.FieldExpiresAt) + } + if m.revoked != nil { + fields = append(fields, invitecode.FieldRevoked) + } + if m.created_by != nil { + fields = append(fields, invitecode.FieldCreatedBy) + } + if m.note != nil { + fields = append(fields, invitecode.FieldNote) + } + if m.created != nil { + fields = append(fields, invitecode.FieldCreated) + } + return fields +} + +// Field returns the value of a field with the given name. The second boolean +// return value indicates that this field was not set, or was not defined in the +// schema. +func (m *InviteCodeMutation) Field(name string) (ent.Value, bool) { + switch name { + case invitecode.FieldCodeHash: + return m.CodeHash() + case invitecode.FieldCodePrefix: + return m.CodePrefix() + case invitecode.FieldMaxUses: + return m.MaxUses() + case invitecode.FieldUseCount: + return m.UseCount() + case invitecode.FieldExpiresAt: + return m.ExpiresAt() + case invitecode.FieldRevoked: + return m.Revoked() + case invitecode.FieldCreatedBy: + return m.CreatedBy() + case invitecode.FieldNote: + return m.Note() + case invitecode.FieldCreated: + return m.Created() + } + return nil, false +} + +// OldField returns the old value of the field from the database. An error is +// returned if the mutation operation is not UpdateOne, or the query to the +// database failed. +func (m *InviteCodeMutation) OldField(ctx context.Context, name string) (ent.Value, error) { + switch name { + case invitecode.FieldCodeHash: + return m.OldCodeHash(ctx) + case invitecode.FieldCodePrefix: + return m.OldCodePrefix(ctx) + case invitecode.FieldMaxUses: + return m.OldMaxUses(ctx) + case invitecode.FieldUseCount: + return m.OldUseCount(ctx) + case invitecode.FieldExpiresAt: + return m.OldExpiresAt(ctx) + case invitecode.FieldRevoked: + return m.OldRevoked(ctx) + case invitecode.FieldCreatedBy: + return m.OldCreatedBy(ctx) + case invitecode.FieldNote: + return m.OldNote(ctx) + case invitecode.FieldCreated: + return m.OldCreated(ctx) + } + return nil, fmt.Errorf("unknown InviteCode field %s", name) +} + +// SetField sets the value of a field with the given name. It returns an error if +// the field is not defined in the schema, or if the type mismatched the field +// type. +func (m *InviteCodeMutation) SetField(name string, value ent.Value) error { + switch name { + case invitecode.FieldCodeHash: + v, ok := value.(string) + if !ok { + return fmt.Errorf("unexpected type %T for field %s", value, name) + } + m.SetCodeHash(v) + return nil + case invitecode.FieldCodePrefix: + v, ok := value.(string) + if !ok { + return fmt.Errorf("unexpected type %T for field %s", value, name) + } + m.SetCodePrefix(v) + return nil + case invitecode.FieldMaxUses: + v, ok := value.(int) + if !ok { + return fmt.Errorf("unexpected type %T for field %s", value, name) + } + m.SetMaxUses(v) + return nil + case invitecode.FieldUseCount: + v, ok := value.(int) + if !ok { + return fmt.Errorf("unexpected type %T for field %s", value, name) + } + m.SetUseCount(v) + return nil + case invitecode.FieldExpiresAt: + v, ok := value.(time.Time) + if !ok { + return fmt.Errorf("unexpected type %T for field %s", value, name) + } + m.SetExpiresAt(v) + return nil + case invitecode.FieldRevoked: + v, ok := value.(bool) + if !ok { + return fmt.Errorf("unexpected type %T for field %s", value, name) + } + m.SetRevoked(v) + return nil + case invitecode.FieldCreatedBy: + v, ok := value.(string) + if !ok { + return fmt.Errorf("unexpected type %T for field %s", value, name) + } + m.SetCreatedBy(v) + return nil + case invitecode.FieldNote: + v, ok := value.(string) + if !ok { + return fmt.Errorf("unexpected type %T for field %s", value, name) + } + m.SetNote(v) + return nil + case invitecode.FieldCreated: + v, ok := value.(time.Time) + if !ok { + return fmt.Errorf("unexpected type %T for field %s", value, name) + } + m.SetCreated(v) + return nil + } + return fmt.Errorf("unknown InviteCode field %s", name) +} + +// AddedFields returns all numeric fields that were incremented/decremented during +// this mutation. +func (m *InviteCodeMutation) AddedFields() []string { + var fields []string + if m.addmax_uses != nil { + fields = append(fields, invitecode.FieldMaxUses) + } + if m.adduse_count != nil { + fields = append(fields, invitecode.FieldUseCount) + } + return fields +} + +// AddedField returns the numeric value that was incremented/decremented on a field +// with the given name. The second boolean return value indicates that this field +// was not set, or was not defined in the schema. +func (m *InviteCodeMutation) AddedField(name string) (ent.Value, bool) { + switch name { + case invitecode.FieldMaxUses: + return m.AddedMaxUses() + case invitecode.FieldUseCount: + return m.AddedUseCount() + } + return nil, false +} + +// AddField adds the value to the field with the given name. It returns an error if +// the field is not defined in the schema, or if the type mismatched the field +// type. +func (m *InviteCodeMutation) AddField(name string, value ent.Value) error { + switch name { + case invitecode.FieldMaxUses: + v, ok := value.(int) + if !ok { + return fmt.Errorf("unexpected type %T for field %s", value, name) + } + m.AddMaxUses(v) + return nil + case invitecode.FieldUseCount: + v, ok := value.(int) + if !ok { + return fmt.Errorf("unexpected type %T for field %s", value, name) + } + m.AddUseCount(v) + return nil + } + return fmt.Errorf("unknown InviteCode numeric field %s", name) +} + +// ClearedFields returns all nullable fields that were cleared during this +// mutation. +func (m *InviteCodeMutation) ClearedFields() []string { + return nil +} + +// FieldCleared returns a boolean indicating if a field with the given name was +// cleared in this mutation. +func (m *InviteCodeMutation) FieldCleared(name string) bool { + _, ok := m.clearedFields[name] + return ok +} + +// ClearField clears the value of the field with the given name. It returns an +// error if the field is not defined in the schema. +func (m *InviteCodeMutation) ClearField(name string) error { + return fmt.Errorf("unknown InviteCode nullable field %s", name) +} + +// ResetField resets all changes in the mutation for the field with the given name. +// It returns an error if the field is not defined in the schema. +func (m *InviteCodeMutation) ResetField(name string) error { + switch name { + case invitecode.FieldCodeHash: + m.ResetCodeHash() + return nil + case invitecode.FieldCodePrefix: + m.ResetCodePrefix() + return nil + case invitecode.FieldMaxUses: + m.ResetMaxUses() + return nil + case invitecode.FieldUseCount: + m.ResetUseCount() + return nil + case invitecode.FieldExpiresAt: + m.ResetExpiresAt() + return nil + case invitecode.FieldRevoked: + m.ResetRevoked() + return nil + case invitecode.FieldCreatedBy: + m.ResetCreatedBy() + return nil + case invitecode.FieldNote: + m.ResetNote() + return nil + case invitecode.FieldCreated: + m.ResetCreated() + return nil + } + return fmt.Errorf("unknown InviteCode field %s", name) +} + +// AddedEdges returns all edge names that were set/added in this mutation. +func (m *InviteCodeMutation) AddedEdges() []string { + edges := make([]string, 0, 0) + return edges +} + +// AddedIDs returns all IDs (to other nodes) that were added for the given edge +// name in this mutation. +func (m *InviteCodeMutation) AddedIDs(name string) []ent.Value { + return nil +} + +// RemovedEdges returns all edge names that were removed in this mutation. +func (m *InviteCodeMutation) RemovedEdges() []string { + edges := make([]string, 0, 0) + return edges +} + +// RemovedIDs returns all IDs (to other nodes) that were removed for the edge with +// the given name in this mutation. +func (m *InviteCodeMutation) RemovedIDs(name string) []ent.Value { + return nil +} + +// ClearedEdges returns all edge names that were cleared in this mutation. +func (m *InviteCodeMutation) ClearedEdges() []string { + edges := make([]string, 0, 0) + return edges +} + +// EdgeCleared returns a boolean which indicates if the edge with the given name +// was cleared in this mutation. +func (m *InviteCodeMutation) EdgeCleared(name string) bool { + return false +} + +// ClearEdge clears the value of the edge with the given name. It returns an error +// if that edge is not defined in the schema. +func (m *InviteCodeMutation) ClearEdge(name string) error { + return fmt.Errorf("unknown InviteCode unique edge %s", name) +} + +// ResetEdge resets all changes to the edge with the given name in this mutation. +// It returns an error if the edge is not defined in the schema. +func (m *InviteCodeMutation) ResetEdge(name string) error { + return fmt.Errorf("unknown InviteCode edge %s", name) +} + +// MaintenanceOperationMutation represents an operation that mutates the MaintenanceOperation nodes in the graph. +type MaintenanceOperationMutation struct { + config + op Op + typ string + id *uuid.UUID + key *string + title *string + description *string + category *string + status *string + started_at *time.Time + completed_at *time.Time + started_by *string + result *string + metadata *string + created *time.Time + clearedFields map[string]struct{} + done bool + oldValue func(context.Context) (*MaintenanceOperation, error) + predicates []predicate.MaintenanceOperation +} + +var _ ent.Mutation = (*MaintenanceOperationMutation)(nil) + +// maintenanceoperationOption allows management of the mutation configuration using functional options. +type maintenanceoperationOption func(*MaintenanceOperationMutation) + +// newMaintenanceOperationMutation creates new mutation for the MaintenanceOperation entity. +func newMaintenanceOperationMutation(c config, op Op, opts ...maintenanceoperationOption) *MaintenanceOperationMutation { + m := &MaintenanceOperationMutation{ + config: c, + op: op, + typ: TypeMaintenanceOperation, + clearedFields: make(map[string]struct{}), + } + for _, opt := range opts { + opt(m) + } + return m +} + +// withMaintenanceOperationID sets the ID field of the mutation. +func withMaintenanceOperationID(id uuid.UUID) maintenanceoperationOption { + return func(m *MaintenanceOperationMutation) { + var ( + err error + once sync.Once + value *MaintenanceOperation + ) + m.oldValue = func(ctx context.Context) (*MaintenanceOperation, error) { + once.Do(func() { + if m.done { + err = errors.New("querying old values post mutation is not allowed") + } else { + value, err = m.Client().MaintenanceOperation.Get(ctx, id) + } + }) + return value, err + } + m.id = &id + } +} + +// withMaintenanceOperation sets the old MaintenanceOperation of the mutation. +func withMaintenanceOperation(node *MaintenanceOperation) maintenanceoperationOption { + return func(m *MaintenanceOperationMutation) { + m.oldValue = func(context.Context) (*MaintenanceOperation, error) { + return node, nil + } + m.id = &node.ID + } +} + +// Client returns a new `ent.Client` from the mutation. If the mutation was +// executed in a transaction (ent.Tx), a transactional client is returned. +func (m MaintenanceOperationMutation) Client() *Client { + client := &Client{config: m.config} + client.init() + return client +} + +// Tx returns an `ent.Tx` for mutations that were executed in transactions; +// it returns an error otherwise. +func (m MaintenanceOperationMutation) Tx() (*Tx, error) { + if _, ok := m.driver.(*txDriver); !ok { + return nil, errors.New("ent: mutation is not running in a transaction") + } + tx := &Tx{config: m.config} + tx.init() + return tx, nil +} + +// SetID sets the value of the id field. Note that this +// operation is only accepted on creation of MaintenanceOperation entities. +func (m *MaintenanceOperationMutation) SetID(id uuid.UUID) { + m.id = &id +} + +// ID returns the ID value in the mutation. Note that the ID is only available +// if it was provided to the builder or after it was returned from the database. +func (m *MaintenanceOperationMutation) ID() (id uuid.UUID, exists bool) { + if m.id == nil { + return + } + return *m.id, true +} + +// IDs queries the database and returns the entity ids that match the mutation's predicate. +// That means, if the mutation is applied within a transaction with an isolation level such +// as sql.LevelSerializable, the returned ids match the ids of the rows that will be updated +// or updated by the mutation. +func (m *MaintenanceOperationMutation) IDs(ctx context.Context) ([]uuid.UUID, error) { + switch { + case m.op.Is(OpUpdateOne | OpDeleteOne): + id, exists := m.ID() + if exists { + return []uuid.UUID{id}, nil + } + fallthrough + case m.op.Is(OpUpdate | OpDelete): + return m.Client().MaintenanceOperation.Query().Where(m.predicates...).IDs(ctx) + default: + return nil, fmt.Errorf("IDs is not allowed on %s operations", m.op) + } +} + +// SetKey sets the "key" field. +func (m *MaintenanceOperationMutation) SetKey(s string) { + m.key = &s +} + +// Key returns the value of the "key" field in the mutation. +func (m *MaintenanceOperationMutation) Key() (r string, exists bool) { + v := m.key + if v == nil { + return + } + return *v, true +} + +// OldKey returns the old "key" field's value of the MaintenanceOperation entity. +// If the MaintenanceOperation object wasn't provided to the builder, the object is fetched from the database. +// An error is returned if the mutation operation is not UpdateOne, or the database query fails. +func (m *MaintenanceOperationMutation) OldKey(ctx context.Context) (v string, err error) { + if !m.op.Is(OpUpdateOne) { + return v, errors.New("OldKey is only allowed on UpdateOne operations") + } + if m.id == nil || m.oldValue == nil { + return v, errors.New("OldKey requires an ID field in the mutation") + } + oldValue, err := m.oldValue(ctx) + if err != nil { + return v, fmt.Errorf("querying old value for OldKey: %w", err) + } + return oldValue.Key, nil +} + +// ResetKey resets all changes to the "key" field. +func (m *MaintenanceOperationMutation) ResetKey() { + m.key = nil +} + +// SetTitle sets the "title" field. +func (m *MaintenanceOperationMutation) SetTitle(s string) { + m.title = &s +} + +// Title returns the value of the "title" field in the mutation. +func (m *MaintenanceOperationMutation) Title() (r string, exists bool) { + v := m.title + if v == nil { + return + } + return *v, true +} + +// OldTitle returns the old "title" field's value of the MaintenanceOperation entity. +// If the MaintenanceOperation object wasn't provided to the builder, the object is fetched from the database. +// An error is returned if the mutation operation is not UpdateOne, or the database query fails. +func (m *MaintenanceOperationMutation) OldTitle(ctx context.Context) (v string, err error) { + if !m.op.Is(OpUpdateOne) { + return v, errors.New("OldTitle is only allowed on UpdateOne operations") + } + if m.id == nil || m.oldValue == nil { + return v, errors.New("OldTitle requires an ID field in the mutation") + } + oldValue, err := m.oldValue(ctx) + if err != nil { + return v, fmt.Errorf("querying old value for OldTitle: %w", err) + } + return oldValue.Title, nil +} + +// ResetTitle resets all changes to the "title" field. +func (m *MaintenanceOperationMutation) ResetTitle() { + m.title = nil +} + +// SetDescription sets the "description" field. +func (m *MaintenanceOperationMutation) SetDescription(s string) { + m.description = &s +} + +// Description returns the value of the "description" field in the mutation. +func (m *MaintenanceOperationMutation) Description() (r string, exists bool) { + v := m.description + if v == nil { + return + } + return *v, true +} + +// OldDescription returns the old "description" field's value of the MaintenanceOperation entity. +// If the MaintenanceOperation object wasn't provided to the builder, the object is fetched from the database. +// An error is returned if the mutation operation is not UpdateOne, or the database query fails. +func (m *MaintenanceOperationMutation) OldDescription(ctx context.Context) (v string, err error) { + if !m.op.Is(OpUpdateOne) { + return v, errors.New("OldDescription is only allowed on UpdateOne operations") + } + if m.id == nil || m.oldValue == nil { + return v, errors.New("OldDescription requires an ID field in the mutation") + } + oldValue, err := m.oldValue(ctx) + if err != nil { + return v, fmt.Errorf("querying old value for OldDescription: %w", err) + } + return oldValue.Description, nil +} + +// ResetDescription resets all changes to the "description" field. +func (m *MaintenanceOperationMutation) ResetDescription() { + m.description = nil +} + +// SetCategory sets the "category" field. +func (m *MaintenanceOperationMutation) SetCategory(s string) { + m.category = &s +} + +// Category returns the value of the "category" field in the mutation. +func (m *MaintenanceOperationMutation) Category() (r string, exists bool) { + v := m.category + if v == nil { + return + } + return *v, true +} + +// OldCategory returns the old "category" field's value of the MaintenanceOperation entity. +// If the MaintenanceOperation object wasn't provided to the builder, the object is fetched from the database. +// An error is returned if the mutation operation is not UpdateOne, or the database query fails. +func (m *MaintenanceOperationMutation) OldCategory(ctx context.Context) (v string, err error) { + if !m.op.Is(OpUpdateOne) { + return v, errors.New("OldCategory is only allowed on UpdateOne operations") + } + if m.id == nil || m.oldValue == nil { + return v, errors.New("OldCategory requires an ID field in the mutation") + } + oldValue, err := m.oldValue(ctx) + if err != nil { + return v, fmt.Errorf("querying old value for OldCategory: %w", err) + } + return oldValue.Category, nil +} + +// ResetCategory resets all changes to the "category" field. +func (m *MaintenanceOperationMutation) ResetCategory() { + m.category = nil +} + +// SetStatus sets the "status" field. +func (m *MaintenanceOperationMutation) SetStatus(s string) { + m.status = &s +} + +// Status returns the value of the "status" field in the mutation. +func (m *MaintenanceOperationMutation) Status() (r string, exists bool) { + v := m.status + if v == nil { + return + } + return *v, true +} + +// OldStatus returns the old "status" field's value of the MaintenanceOperation entity. +// If the MaintenanceOperation object wasn't provided to the builder, the object is fetched from the database. +// An error is returned if the mutation operation is not UpdateOne, or the database query fails. +func (m *MaintenanceOperationMutation) OldStatus(ctx context.Context) (v string, err error) { + if !m.op.Is(OpUpdateOne) { + return v, errors.New("OldStatus is only allowed on UpdateOne operations") + } + if m.id == nil || m.oldValue == nil { + return v, errors.New("OldStatus requires an ID field in the mutation") + } + oldValue, err := m.oldValue(ctx) + if err != nil { + return v, fmt.Errorf("querying old value for OldStatus: %w", err) + } + return oldValue.Status, nil +} + +// ResetStatus resets all changes to the "status" field. +func (m *MaintenanceOperationMutation) ResetStatus() { + m.status = nil +} + +// SetStartedAt sets the "started_at" field. +func (m *MaintenanceOperationMutation) SetStartedAt(t time.Time) { + m.started_at = &t +} + +// StartedAt returns the value of the "started_at" field in the mutation. +func (m *MaintenanceOperationMutation) StartedAt() (r time.Time, exists bool) { + v := m.started_at + if v == nil { + return + } + return *v, true +} + +// OldStartedAt returns the old "started_at" field's value of the MaintenanceOperation entity. +// If the MaintenanceOperation object wasn't provided to the builder, the object is fetched from the database. +// An error is returned if the mutation operation is not UpdateOne, or the database query fails. +func (m *MaintenanceOperationMutation) OldStartedAt(ctx context.Context) (v *time.Time, err error) { + if !m.op.Is(OpUpdateOne) { + return v, errors.New("OldStartedAt is only allowed on UpdateOne operations") + } + if m.id == nil || m.oldValue == nil { + return v, errors.New("OldStartedAt requires an ID field in the mutation") + } + oldValue, err := m.oldValue(ctx) + if err != nil { + return v, fmt.Errorf("querying old value for OldStartedAt: %w", err) + } + return oldValue.StartedAt, nil +} + +// ClearStartedAt clears the value of the "started_at" field. +func (m *MaintenanceOperationMutation) ClearStartedAt() { + m.started_at = nil + m.clearedFields[maintenanceoperation.FieldStartedAt] = struct{}{} +} + +// StartedAtCleared returns if the "started_at" field was cleared in this mutation. +func (m *MaintenanceOperationMutation) StartedAtCleared() bool { + _, ok := m.clearedFields[maintenanceoperation.FieldStartedAt] + return ok +} + +// ResetStartedAt resets all changes to the "started_at" field. +func (m *MaintenanceOperationMutation) ResetStartedAt() { + m.started_at = nil + delete(m.clearedFields, maintenanceoperation.FieldStartedAt) +} + +// SetCompletedAt sets the "completed_at" field. +func (m *MaintenanceOperationMutation) SetCompletedAt(t time.Time) { + m.completed_at = &t +} + +// CompletedAt returns the value of the "completed_at" field in the mutation. +func (m *MaintenanceOperationMutation) CompletedAt() (r time.Time, exists bool) { + v := m.completed_at + if v == nil { + return + } + return *v, true +} + +// OldCompletedAt returns the old "completed_at" field's value of the MaintenanceOperation entity. +// If the MaintenanceOperation object wasn't provided to the builder, the object is fetched from the database. +// An error is returned if the mutation operation is not UpdateOne, or the database query fails. +func (m *MaintenanceOperationMutation) OldCompletedAt(ctx context.Context) (v *time.Time, err error) { + if !m.op.Is(OpUpdateOne) { + return v, errors.New("OldCompletedAt is only allowed on UpdateOne operations") + } + if m.id == nil || m.oldValue == nil { + return v, errors.New("OldCompletedAt requires an ID field in the mutation") + } + oldValue, err := m.oldValue(ctx) + if err != nil { + return v, fmt.Errorf("querying old value for OldCompletedAt: %w", err) + } + return oldValue.CompletedAt, nil +} + +// ClearCompletedAt clears the value of the "completed_at" field. +func (m *MaintenanceOperationMutation) ClearCompletedAt() { + m.completed_at = nil + m.clearedFields[maintenanceoperation.FieldCompletedAt] = struct{}{} +} + +// CompletedAtCleared returns if the "completed_at" field was cleared in this mutation. +func (m *MaintenanceOperationMutation) CompletedAtCleared() bool { + _, ok := m.clearedFields[maintenanceoperation.FieldCompletedAt] + return ok +} + +// ResetCompletedAt resets all changes to the "completed_at" field. +func (m *MaintenanceOperationMutation) ResetCompletedAt() { + m.completed_at = nil + delete(m.clearedFields, maintenanceoperation.FieldCompletedAt) +} + +// SetStartedBy sets the "started_by" field. +func (m *MaintenanceOperationMutation) SetStartedBy(s string) { + m.started_by = &s +} + +// StartedBy returns the value of the "started_by" field in the mutation. +func (m *MaintenanceOperationMutation) StartedBy() (r string, exists bool) { + v := m.started_by + if v == nil { + return + } + return *v, true +} + +// OldStartedBy returns the old "started_by" field's value of the MaintenanceOperation entity. +// If the MaintenanceOperation object wasn't provided to the builder, the object is fetched from the database. +// An error is returned if the mutation operation is not UpdateOne, or the database query fails. +func (m *MaintenanceOperationMutation) OldStartedBy(ctx context.Context) (v string, err error) { + if !m.op.Is(OpUpdateOne) { + return v, errors.New("OldStartedBy is only allowed on UpdateOne operations") + } + if m.id == nil || m.oldValue == nil { + return v, errors.New("OldStartedBy requires an ID field in the mutation") + } + oldValue, err := m.oldValue(ctx) + if err != nil { + return v, fmt.Errorf("querying old value for OldStartedBy: %w", err) + } + return oldValue.StartedBy, nil +} + +// ClearStartedBy clears the value of the "started_by" field. +func (m *MaintenanceOperationMutation) ClearStartedBy() { + m.started_by = nil + m.clearedFields[maintenanceoperation.FieldStartedBy] = struct{}{} +} + +// StartedByCleared returns if the "started_by" field was cleared in this mutation. +func (m *MaintenanceOperationMutation) StartedByCleared() bool { + _, ok := m.clearedFields[maintenanceoperation.FieldStartedBy] + return ok +} + +// ResetStartedBy resets all changes to the "started_by" field. +func (m *MaintenanceOperationMutation) ResetStartedBy() { + m.started_by = nil + delete(m.clearedFields, maintenanceoperation.FieldStartedBy) +} + +// SetResult sets the "result" field. +func (m *MaintenanceOperationMutation) SetResult(s string) { + m.result = &s +} + +// Result returns the value of the "result" field in the mutation. +func (m *MaintenanceOperationMutation) Result() (r string, exists bool) { + v := m.result + if v == nil { + return + } + return *v, true +} + +// OldResult returns the old "result" field's value of the MaintenanceOperation entity. +// If the MaintenanceOperation object wasn't provided to the builder, the object is fetched from the database. +// An error is returned if the mutation operation is not UpdateOne, or the database query fails. +func (m *MaintenanceOperationMutation) OldResult(ctx context.Context) (v string, err error) { + if !m.op.Is(OpUpdateOne) { + return v, errors.New("OldResult is only allowed on UpdateOne operations") + } + if m.id == nil || m.oldValue == nil { + return v, errors.New("OldResult requires an ID field in the mutation") + } + oldValue, err := m.oldValue(ctx) + if err != nil { + return v, fmt.Errorf("querying old value for OldResult: %w", err) + } + return oldValue.Result, nil +} + +// ClearResult clears the value of the "result" field. +func (m *MaintenanceOperationMutation) ClearResult() { + m.result = nil + m.clearedFields[maintenanceoperation.FieldResult] = struct{}{} +} + +// ResultCleared returns if the "result" field was cleared in this mutation. +func (m *MaintenanceOperationMutation) ResultCleared() bool { + _, ok := m.clearedFields[maintenanceoperation.FieldResult] + return ok +} + +// ResetResult resets all changes to the "result" field. +func (m *MaintenanceOperationMutation) ResetResult() { + m.result = nil + delete(m.clearedFields, maintenanceoperation.FieldResult) +} + +// SetMetadata sets the "metadata" field. +func (m *MaintenanceOperationMutation) SetMetadata(s string) { + m.metadata = &s +} + +// Metadata returns the value of the "metadata" field in the mutation. +func (m *MaintenanceOperationMutation) Metadata() (r string, exists bool) { + v := m.metadata + if v == nil { + return + } + return *v, true +} + +// OldMetadata returns the old "metadata" field's value of the MaintenanceOperation entity. +// If the MaintenanceOperation object wasn't provided to the builder, the object is fetched from the database. +// An error is returned if the mutation operation is not UpdateOne, or the database query fails. +func (m *MaintenanceOperationMutation) OldMetadata(ctx context.Context) (v string, err error) { + if !m.op.Is(OpUpdateOne) { + return v, errors.New("OldMetadata is only allowed on UpdateOne operations") + } + if m.id == nil || m.oldValue == nil { + return v, errors.New("OldMetadata requires an ID field in the mutation") + } + oldValue, err := m.oldValue(ctx) + if err != nil { + return v, fmt.Errorf("querying old value for OldMetadata: %w", err) + } + return oldValue.Metadata, nil +} + +// ResetMetadata resets all changes to the "metadata" field. +func (m *MaintenanceOperationMutation) ResetMetadata() { + m.metadata = nil +} + +// SetCreated sets the "created" field. +func (m *MaintenanceOperationMutation) SetCreated(t time.Time) { + m.created = &t +} + +// Created returns the value of the "created" field in the mutation. +func (m *MaintenanceOperationMutation) Created() (r time.Time, exists bool) { + v := m.created + if v == nil { + return + } + return *v, true +} + +// OldCreated returns the old "created" field's value of the MaintenanceOperation entity. +// If the MaintenanceOperation object wasn't provided to the builder, the object is fetched from the database. +// An error is returned if the mutation operation is not UpdateOne, or the database query fails. +func (m *MaintenanceOperationMutation) OldCreated(ctx context.Context) (v time.Time, err error) { + if !m.op.Is(OpUpdateOne) { + return v, errors.New("OldCreated is only allowed on UpdateOne operations") + } + if m.id == nil || m.oldValue == nil { + return v, errors.New("OldCreated requires an ID field in the mutation") + } + oldValue, err := m.oldValue(ctx) + if err != nil { + return v, fmt.Errorf("querying old value for OldCreated: %w", err) + } + return oldValue.Created, nil +} + +// ResetCreated resets all changes to the "created" field. +func (m *MaintenanceOperationMutation) ResetCreated() { + m.created = nil +} + +// Where appends a list predicates to the MaintenanceOperationMutation builder. +func (m *MaintenanceOperationMutation) Where(ps ...predicate.MaintenanceOperation) { + m.predicates = append(m.predicates, ps...) +} + +// WhereP appends storage-level predicates to the MaintenanceOperationMutation builder. Using this method, +// users can use type-assertion to append predicates that do not depend on any generated package. +func (m *MaintenanceOperationMutation) WhereP(ps ...func(*sql.Selector)) { + p := make([]predicate.MaintenanceOperation, len(ps)) + for i := range ps { + p[i] = ps[i] + } + m.Where(p...) +} + +// Op returns the operation name. +func (m *MaintenanceOperationMutation) Op() Op { + return m.op +} + +// SetOp allows setting the mutation operation. +func (m *MaintenanceOperationMutation) SetOp(op Op) { + m.op = op +} + +// Type returns the node type of this mutation (MaintenanceOperation). +func (m *MaintenanceOperationMutation) Type() string { + return m.typ +} + +// Fields returns all fields that were changed during this mutation. Note that in +// order to get all numeric fields that were incremented/decremented, call +// AddedFields(). +func (m *MaintenanceOperationMutation) Fields() []string { + fields := make([]string, 0, 11) + if m.key != nil { + fields = append(fields, maintenanceoperation.FieldKey) + } + if m.title != nil { + fields = append(fields, maintenanceoperation.FieldTitle) + } + if m.description != nil { + fields = append(fields, maintenanceoperation.FieldDescription) + } + if m.category != nil { + fields = append(fields, maintenanceoperation.FieldCategory) + } + if m.status != nil { + fields = append(fields, maintenanceoperation.FieldStatus) + } + if m.started_at != nil { + fields = append(fields, maintenanceoperation.FieldStartedAt) + } + if m.completed_at != nil { + fields = append(fields, maintenanceoperation.FieldCompletedAt) + } + if m.started_by != nil { + fields = append(fields, maintenanceoperation.FieldStartedBy) + } + if m.result != nil { + fields = append(fields, maintenanceoperation.FieldResult) + } + if m.metadata != nil { + fields = append(fields, maintenanceoperation.FieldMetadata) + } + if m.created != nil { + fields = append(fields, maintenanceoperation.FieldCreated) + } + return fields +} + +// Field returns the value of a field with the given name. The second boolean +// return value indicates that this field was not set, or was not defined in the +// schema. +func (m *MaintenanceOperationMutation) Field(name string) (ent.Value, bool) { + switch name { + case maintenanceoperation.FieldKey: + return m.Key() + case maintenanceoperation.FieldTitle: + return m.Title() + case maintenanceoperation.FieldDescription: + return m.Description() + case maintenanceoperation.FieldCategory: + return m.Category() + case maintenanceoperation.FieldStatus: + return m.Status() + case maintenanceoperation.FieldStartedAt: + return m.StartedAt() + case maintenanceoperation.FieldCompletedAt: + return m.CompletedAt() + case maintenanceoperation.FieldStartedBy: + return m.StartedBy() + case maintenanceoperation.FieldResult: + return m.Result() + case maintenanceoperation.FieldMetadata: + return m.Metadata() + case maintenanceoperation.FieldCreated: + return m.Created() + } + return nil, false +} + +// OldField returns the old value of the field from the database. An error is +// returned if the mutation operation is not UpdateOne, or the query to the +// database failed. +func (m *MaintenanceOperationMutation) OldField(ctx context.Context, name string) (ent.Value, error) { + switch name { + case maintenanceoperation.FieldKey: + return m.OldKey(ctx) + case maintenanceoperation.FieldTitle: + return m.OldTitle(ctx) + case maintenanceoperation.FieldDescription: + return m.OldDescription(ctx) + case maintenanceoperation.FieldCategory: + return m.OldCategory(ctx) + case maintenanceoperation.FieldStatus: + return m.OldStatus(ctx) + case maintenanceoperation.FieldStartedAt: + return m.OldStartedAt(ctx) + case maintenanceoperation.FieldCompletedAt: + return m.OldCompletedAt(ctx) + case maintenanceoperation.FieldStartedBy: + return m.OldStartedBy(ctx) + case maintenanceoperation.FieldResult: + return m.OldResult(ctx) + case maintenanceoperation.FieldMetadata: + return m.OldMetadata(ctx) + case maintenanceoperation.FieldCreated: + return m.OldCreated(ctx) + } + return nil, fmt.Errorf("unknown MaintenanceOperation field %s", name) +} + +// SetField sets the value of a field with the given name. It returns an error if +// the field is not defined in the schema, or if the type mismatched the field +// type. +func (m *MaintenanceOperationMutation) SetField(name string, value ent.Value) error { + switch name { + case maintenanceoperation.FieldKey: + v, ok := value.(string) + if !ok { + return fmt.Errorf("unexpected type %T for field %s", value, name) + } + m.SetKey(v) + return nil + case maintenanceoperation.FieldTitle: + v, ok := value.(string) + if !ok { + return fmt.Errorf("unexpected type %T for field %s", value, name) + } + m.SetTitle(v) + return nil + case maintenanceoperation.FieldDescription: + v, ok := value.(string) + if !ok { + return fmt.Errorf("unexpected type %T for field %s", value, name) + } + m.SetDescription(v) + return nil + case maintenanceoperation.FieldCategory: + v, ok := value.(string) + if !ok { + return fmt.Errorf("unexpected type %T for field %s", value, name) + } + m.SetCategory(v) + return nil + case maintenanceoperation.FieldStatus: + v, ok := value.(string) + if !ok { + return fmt.Errorf("unexpected type %T for field %s", value, name) + } + m.SetStatus(v) + return nil + case maintenanceoperation.FieldStartedAt: + v, ok := value.(time.Time) + if !ok { + return fmt.Errorf("unexpected type %T for field %s", value, name) + } + m.SetStartedAt(v) + return nil + case maintenanceoperation.FieldCompletedAt: + v, ok := value.(time.Time) + if !ok { + return fmt.Errorf("unexpected type %T for field %s", value, name) + } + m.SetCompletedAt(v) + return nil + case maintenanceoperation.FieldStartedBy: + v, ok := value.(string) + if !ok { + return fmt.Errorf("unexpected type %T for field %s", value, name) + } + m.SetStartedBy(v) + return nil + case maintenanceoperation.FieldResult: + v, ok := value.(string) + if !ok { + return fmt.Errorf("unexpected type %T for field %s", value, name) + } + m.SetResult(v) + return nil + case maintenanceoperation.FieldMetadata: + v, ok := value.(string) + if !ok { + return fmt.Errorf("unexpected type %T for field %s", value, name) + } + m.SetMetadata(v) + return nil + case maintenanceoperation.FieldCreated: + v, ok := value.(time.Time) + if !ok { + return fmt.Errorf("unexpected type %T for field %s", value, name) + } + m.SetCreated(v) + return nil + } + return fmt.Errorf("unknown MaintenanceOperation field %s", name) +} + +// AddedFields returns all numeric fields that were incremented/decremented during +// this mutation. +func (m *MaintenanceOperationMutation) AddedFields() []string { + return nil +} + +// AddedField returns the numeric value that was incremented/decremented on a field +// with the given name. The second boolean return value indicates that this field +// was not set, or was not defined in the schema. +func (m *MaintenanceOperationMutation) AddedField(name string) (ent.Value, bool) { + return nil, false +} + +// AddField adds the value to the field with the given name. It returns an error if +// the field is not defined in the schema, or if the type mismatched the field +// type. +func (m *MaintenanceOperationMutation) AddField(name string, value ent.Value) error { + switch name { + } + return fmt.Errorf("unknown MaintenanceOperation numeric field %s", name) +} + +// ClearedFields returns all nullable fields that were cleared during this +// mutation. +func (m *MaintenanceOperationMutation) ClearedFields() []string { + var fields []string + if m.FieldCleared(maintenanceoperation.FieldStartedAt) { + fields = append(fields, maintenanceoperation.FieldStartedAt) + } + if m.FieldCleared(maintenanceoperation.FieldCompletedAt) { + fields = append(fields, maintenanceoperation.FieldCompletedAt) + } + if m.FieldCleared(maintenanceoperation.FieldStartedBy) { + fields = append(fields, maintenanceoperation.FieldStartedBy) + } + if m.FieldCleared(maintenanceoperation.FieldResult) { + fields = append(fields, maintenanceoperation.FieldResult) + } + return fields +} + +// FieldCleared returns a boolean indicating if a field with the given name was +// cleared in this mutation. +func (m *MaintenanceOperationMutation) FieldCleared(name string) bool { + _, ok := m.clearedFields[name] + return ok +} + +// ClearField clears the value of the field with the given name. It returns an +// error if the field is not defined in the schema. +func (m *MaintenanceOperationMutation) ClearField(name string) error { + switch name { + case maintenanceoperation.FieldStartedAt: + m.ClearStartedAt() + return nil + case maintenanceoperation.FieldCompletedAt: + m.ClearCompletedAt() + return nil + case maintenanceoperation.FieldStartedBy: + m.ClearStartedBy() + return nil + case maintenanceoperation.FieldResult: + m.ClearResult() + return nil + } + return fmt.Errorf("unknown MaintenanceOperation nullable field %s", name) +} + +// ResetField resets all changes in the mutation for the field with the given name. +// It returns an error if the field is not defined in the schema. +func (m *MaintenanceOperationMutation) ResetField(name string) error { + switch name { + case maintenanceoperation.FieldKey: + m.ResetKey() + return nil + case maintenanceoperation.FieldTitle: + m.ResetTitle() + return nil + case maintenanceoperation.FieldDescription: + m.ResetDescription() + return nil + case maintenanceoperation.FieldCategory: + m.ResetCategory() + return nil + case maintenanceoperation.FieldStatus: + m.ResetStatus() + return nil + case maintenanceoperation.FieldStartedAt: + m.ResetStartedAt() + return nil + case maintenanceoperation.FieldCompletedAt: + m.ResetCompletedAt() + return nil + case maintenanceoperation.FieldStartedBy: + m.ResetStartedBy() + return nil + case maintenanceoperation.FieldResult: + m.ResetResult() + return nil + case maintenanceoperation.FieldMetadata: + m.ResetMetadata() + return nil + case maintenanceoperation.FieldCreated: + m.ResetCreated() + return nil + } + return fmt.Errorf("unknown MaintenanceOperation field %s", name) +} + +// AddedEdges returns all edge names that were set/added in this mutation. +func (m *MaintenanceOperationMutation) AddedEdges() []string { + edges := make([]string, 0, 0) + return edges +} + +// AddedIDs returns all IDs (to other nodes) that were added for the given edge +// name in this mutation. +func (m *MaintenanceOperationMutation) AddedIDs(name string) []ent.Value { + return nil +} + +// RemovedEdges returns all edge names that were removed in this mutation. +func (m *MaintenanceOperationMutation) RemovedEdges() []string { + edges := make([]string, 0, 0) + return edges +} + +// RemovedIDs returns all IDs (to other nodes) that were removed for the edge with +// the given name in this mutation. +func (m *MaintenanceOperationMutation) RemovedIDs(name string) []ent.Value { + return nil +} + +// ClearedEdges returns all edge names that were cleared in this mutation. +func (m *MaintenanceOperationMutation) ClearedEdges() []string { + edges := make([]string, 0, 0) + return edges +} + +// EdgeCleared returns a boolean which indicates if the edge with the given name +// was cleared in this mutation. +func (m *MaintenanceOperationMutation) EdgeCleared(name string) bool { + return false +} + +// ClearEdge clears the value of the edge with the given name. It returns an error +// if that edge is not defined in the schema. +func (m *MaintenanceOperationMutation) ClearEdge(name string) error { + return fmt.Errorf("unknown MaintenanceOperation unique edge %s", name) +} + +// ResetEdge resets all changes to the edge with the given name in this mutation. +// It returns an error if the edge is not defined in the schema. +func (m *MaintenanceOperationMutation) ResetEdge(name string) error { + return fmt.Errorf("unknown MaintenanceOperation edge %s", name) +} + +// MaintenanceOperationRunMutation represents an operation that mutates the MaintenanceOperationRun nodes in the graph. +type MaintenanceOperationRunMutation struct { + config + op Op + typ string + id *uuid.UUID + operation_key *string + status *string + started_at *time.Time + completed_at *time.Time + started_by *string + result *string + log *string + clearedFields map[string]struct{} + done bool + oldValue func(context.Context) (*MaintenanceOperationRun, error) + predicates []predicate.MaintenanceOperationRun +} + +var _ ent.Mutation = (*MaintenanceOperationRunMutation)(nil) + +// maintenanceoperationrunOption allows management of the mutation configuration using functional options. +type maintenanceoperationrunOption func(*MaintenanceOperationRunMutation) + +// newMaintenanceOperationRunMutation creates new mutation for the MaintenanceOperationRun entity. +func newMaintenanceOperationRunMutation(c config, op Op, opts ...maintenanceoperationrunOption) *MaintenanceOperationRunMutation { + m := &MaintenanceOperationRunMutation{ + config: c, + op: op, + typ: TypeMaintenanceOperationRun, + clearedFields: make(map[string]struct{}), + } + for _, opt := range opts { + opt(m) + } + return m +} + +// withMaintenanceOperationRunID sets the ID field of the mutation. +func withMaintenanceOperationRunID(id uuid.UUID) maintenanceoperationrunOption { + return func(m *MaintenanceOperationRunMutation) { + var ( + err error + once sync.Once + value *MaintenanceOperationRun + ) + m.oldValue = func(ctx context.Context) (*MaintenanceOperationRun, error) { + once.Do(func() { + if m.done { + err = errors.New("querying old values post mutation is not allowed") + } else { + value, err = m.Client().MaintenanceOperationRun.Get(ctx, id) + } + }) + return value, err + } + m.id = &id + } +} + +// withMaintenanceOperationRun sets the old MaintenanceOperationRun of the mutation. +func withMaintenanceOperationRun(node *MaintenanceOperationRun) maintenanceoperationrunOption { + return func(m *MaintenanceOperationRunMutation) { + m.oldValue = func(context.Context) (*MaintenanceOperationRun, error) { + return node, nil + } + m.id = &node.ID + } +} + +// Client returns a new `ent.Client` from the mutation. If the mutation was +// executed in a transaction (ent.Tx), a transactional client is returned. +func (m MaintenanceOperationRunMutation) Client() *Client { + client := &Client{config: m.config} + client.init() + return client +} + +// Tx returns an `ent.Tx` for mutations that were executed in transactions; +// it returns an error otherwise. +func (m MaintenanceOperationRunMutation) Tx() (*Tx, error) { + if _, ok := m.driver.(*txDriver); !ok { + return nil, errors.New("ent: mutation is not running in a transaction") + } + tx := &Tx{config: m.config} + tx.init() + return tx, nil +} + +// SetID sets the value of the id field. Note that this +// operation is only accepted on creation of MaintenanceOperationRun entities. +func (m *MaintenanceOperationRunMutation) SetID(id uuid.UUID) { + m.id = &id +} + +// ID returns the ID value in the mutation. Note that the ID is only available +// if it was provided to the builder or after it was returned from the database. +func (m *MaintenanceOperationRunMutation) ID() (id uuid.UUID, exists bool) { + if m.id == nil { + return + } + return *m.id, true +} + +// IDs queries the database and returns the entity ids that match the mutation's predicate. +// That means, if the mutation is applied within a transaction with an isolation level such +// as sql.LevelSerializable, the returned ids match the ids of the rows that will be updated +// or updated by the mutation. +func (m *MaintenanceOperationRunMutation) IDs(ctx context.Context) ([]uuid.UUID, error) { + switch { + case m.op.Is(OpUpdateOne | OpDeleteOne): + id, exists := m.ID() + if exists { + return []uuid.UUID{id}, nil + } + fallthrough + case m.op.Is(OpUpdate | OpDelete): + return m.Client().MaintenanceOperationRun.Query().Where(m.predicates...).IDs(ctx) + default: + return nil, fmt.Errorf("IDs is not allowed on %s operations", m.op) + } +} + +// SetOperationKey sets the "operation_key" field. +func (m *MaintenanceOperationRunMutation) SetOperationKey(s string) { + m.operation_key = &s +} + +// OperationKey returns the value of the "operation_key" field in the mutation. +func (m *MaintenanceOperationRunMutation) OperationKey() (r string, exists bool) { + v := m.operation_key + if v == nil { + return + } + return *v, true +} + +// OldOperationKey returns the old "operation_key" field's value of the MaintenanceOperationRun entity. +// If the MaintenanceOperationRun object wasn't provided to the builder, the object is fetched from the database. +// An error is returned if the mutation operation is not UpdateOne, or the database query fails. +func (m *MaintenanceOperationRunMutation) OldOperationKey(ctx context.Context) (v string, err error) { + if !m.op.Is(OpUpdateOne) { + return v, errors.New("OldOperationKey is only allowed on UpdateOne operations") + } + if m.id == nil || m.oldValue == nil { + return v, errors.New("OldOperationKey requires an ID field in the mutation") + } + oldValue, err := m.oldValue(ctx) + if err != nil { + return v, fmt.Errorf("querying old value for OldOperationKey: %w", err) + } + return oldValue.OperationKey, nil +} + +// ResetOperationKey resets all changes to the "operation_key" field. +func (m *MaintenanceOperationRunMutation) ResetOperationKey() { + m.operation_key = nil +} + +// SetStatus sets the "status" field. +func (m *MaintenanceOperationRunMutation) SetStatus(s string) { + m.status = &s +} + +// Status returns the value of the "status" field in the mutation. +func (m *MaintenanceOperationRunMutation) Status() (r string, exists bool) { + v := m.status + if v == nil { + return + } + return *v, true +} + +// OldStatus returns the old "status" field's value of the MaintenanceOperationRun entity. +// If the MaintenanceOperationRun object wasn't provided to the builder, the object is fetched from the database. +// An error is returned if the mutation operation is not UpdateOne, or the database query fails. +func (m *MaintenanceOperationRunMutation) OldStatus(ctx context.Context) (v string, err error) { + if !m.op.Is(OpUpdateOne) { + return v, errors.New("OldStatus is only allowed on UpdateOne operations") + } + if m.id == nil || m.oldValue == nil { + return v, errors.New("OldStatus requires an ID field in the mutation") + } + oldValue, err := m.oldValue(ctx) + if err != nil { + return v, fmt.Errorf("querying old value for OldStatus: %w", err) + } + return oldValue.Status, nil +} + +// ResetStatus resets all changes to the "status" field. +func (m *MaintenanceOperationRunMutation) ResetStatus() { + m.status = nil +} + +// SetStartedAt sets the "started_at" field. +func (m *MaintenanceOperationRunMutation) SetStartedAt(t time.Time) { + m.started_at = &t +} + +// StartedAt returns the value of the "started_at" field in the mutation. +func (m *MaintenanceOperationRunMutation) StartedAt() (r time.Time, exists bool) { + v := m.started_at + if v == nil { + return + } + return *v, true +} + +// OldStartedAt returns the old "started_at" field's value of the MaintenanceOperationRun entity. +// If the MaintenanceOperationRun object wasn't provided to the builder, the object is fetched from the database. +// An error is returned if the mutation operation is not UpdateOne, or the database query fails. +func (m *MaintenanceOperationRunMutation) OldStartedAt(ctx context.Context) (v time.Time, err error) { + if !m.op.Is(OpUpdateOne) { + return v, errors.New("OldStartedAt is only allowed on UpdateOne operations") + } + if m.id == nil || m.oldValue == nil { + return v, errors.New("OldStartedAt requires an ID field in the mutation") + } + oldValue, err := m.oldValue(ctx) + if err != nil { + return v, fmt.Errorf("querying old value for OldStartedAt: %w", err) + } + return oldValue.StartedAt, nil +} + +// ResetStartedAt resets all changes to the "started_at" field. +func (m *MaintenanceOperationRunMutation) ResetStartedAt() { + m.started_at = nil +} + +// SetCompletedAt sets the "completed_at" field. +func (m *MaintenanceOperationRunMutation) SetCompletedAt(t time.Time) { + m.completed_at = &t +} + +// CompletedAt returns the value of the "completed_at" field in the mutation. +func (m *MaintenanceOperationRunMutation) CompletedAt() (r time.Time, exists bool) { + v := m.completed_at + if v == nil { + return + } + return *v, true +} + +// OldCompletedAt returns the old "completed_at" field's value of the MaintenanceOperationRun entity. +// If the MaintenanceOperationRun object wasn't provided to the builder, the object is fetched from the database. +// An error is returned if the mutation operation is not UpdateOne, or the database query fails. +func (m *MaintenanceOperationRunMutation) OldCompletedAt(ctx context.Context) (v *time.Time, err error) { + if !m.op.Is(OpUpdateOne) { + return v, errors.New("OldCompletedAt is only allowed on UpdateOne operations") + } + if m.id == nil || m.oldValue == nil { + return v, errors.New("OldCompletedAt requires an ID field in the mutation") + } + oldValue, err := m.oldValue(ctx) + if err != nil { + return v, fmt.Errorf("querying old value for OldCompletedAt: %w", err) + } + return oldValue.CompletedAt, nil +} + +// ClearCompletedAt clears the value of the "completed_at" field. +func (m *MaintenanceOperationRunMutation) ClearCompletedAt() { + m.completed_at = nil + m.clearedFields[maintenanceoperationrun.FieldCompletedAt] = struct{}{} +} + +// CompletedAtCleared returns if the "completed_at" field was cleared in this mutation. +func (m *MaintenanceOperationRunMutation) CompletedAtCleared() bool { + _, ok := m.clearedFields[maintenanceoperationrun.FieldCompletedAt] + return ok +} + +// ResetCompletedAt resets all changes to the "completed_at" field. +func (m *MaintenanceOperationRunMutation) ResetCompletedAt() { + m.completed_at = nil + delete(m.clearedFields, maintenanceoperationrun.FieldCompletedAt) +} + +// SetStartedBy sets the "started_by" field. +func (m *MaintenanceOperationRunMutation) SetStartedBy(s string) { + m.started_by = &s +} + +// StartedBy returns the value of the "started_by" field in the mutation. +func (m *MaintenanceOperationRunMutation) StartedBy() (r string, exists bool) { + v := m.started_by + if v == nil { + return + } + return *v, true +} + +// OldStartedBy returns the old "started_by" field's value of the MaintenanceOperationRun entity. +// If the MaintenanceOperationRun object wasn't provided to the builder, the object is fetched from the database. +// An error is returned if the mutation operation is not UpdateOne, or the database query fails. +func (m *MaintenanceOperationRunMutation) OldStartedBy(ctx context.Context) (v string, err error) { + if !m.op.Is(OpUpdateOne) { + return v, errors.New("OldStartedBy is only allowed on UpdateOne operations") + } + if m.id == nil || m.oldValue == nil { + return v, errors.New("OldStartedBy requires an ID field in the mutation") + } + oldValue, err := m.oldValue(ctx) + if err != nil { + return v, fmt.Errorf("querying old value for OldStartedBy: %w", err) + } + return oldValue.StartedBy, nil +} + +// ClearStartedBy clears the value of the "started_by" field. +func (m *MaintenanceOperationRunMutation) ClearStartedBy() { + m.started_by = nil + m.clearedFields[maintenanceoperationrun.FieldStartedBy] = struct{}{} +} + +// StartedByCleared returns if the "started_by" field was cleared in this mutation. +func (m *MaintenanceOperationRunMutation) StartedByCleared() bool { + _, ok := m.clearedFields[maintenanceoperationrun.FieldStartedBy] + return ok +} + +// ResetStartedBy resets all changes to the "started_by" field. +func (m *MaintenanceOperationRunMutation) ResetStartedBy() { + m.started_by = nil + delete(m.clearedFields, maintenanceoperationrun.FieldStartedBy) +} + +// SetResult sets the "result" field. +func (m *MaintenanceOperationRunMutation) SetResult(s string) { + m.result = &s +} + +// Result returns the value of the "result" field in the mutation. +func (m *MaintenanceOperationRunMutation) Result() (r string, exists bool) { + v := m.result + if v == nil { + return + } + return *v, true +} + +// OldResult returns the old "result" field's value of the MaintenanceOperationRun entity. +// If the MaintenanceOperationRun object wasn't provided to the builder, the object is fetched from the database. +// An error is returned if the mutation operation is not UpdateOne, or the database query fails. +func (m *MaintenanceOperationRunMutation) OldResult(ctx context.Context) (v string, err error) { + if !m.op.Is(OpUpdateOne) { + return v, errors.New("OldResult is only allowed on UpdateOne operations") + } + if m.id == nil || m.oldValue == nil { + return v, errors.New("OldResult requires an ID field in the mutation") + } + oldValue, err := m.oldValue(ctx) + if err != nil { + return v, fmt.Errorf("querying old value for OldResult: %w", err) + } + return oldValue.Result, nil +} + +// ClearResult clears the value of the "result" field. +func (m *MaintenanceOperationRunMutation) ClearResult() { + m.result = nil + m.clearedFields[maintenanceoperationrun.FieldResult] = struct{}{} +} + +// ResultCleared returns if the "result" field was cleared in this mutation. +func (m *MaintenanceOperationRunMutation) ResultCleared() bool { + _, ok := m.clearedFields[maintenanceoperationrun.FieldResult] + return ok +} + +// ResetResult resets all changes to the "result" field. +func (m *MaintenanceOperationRunMutation) ResetResult() { + m.result = nil + delete(m.clearedFields, maintenanceoperationrun.FieldResult) +} + +// SetLog sets the "log" field. +func (m *MaintenanceOperationRunMutation) SetLog(s string) { + m.log = &s +} + +// Log returns the value of the "log" field in the mutation. +func (m *MaintenanceOperationRunMutation) Log() (r string, exists bool) { + v := m.log + if v == nil { + return + } + return *v, true +} + +// OldLog returns the old "log" field's value of the MaintenanceOperationRun entity. +// If the MaintenanceOperationRun object wasn't provided to the builder, the object is fetched from the database. +// An error is returned if the mutation operation is not UpdateOne, or the database query fails. +func (m *MaintenanceOperationRunMutation) OldLog(ctx context.Context) (v string, err error) { + if !m.op.Is(OpUpdateOne) { + return v, errors.New("OldLog is only allowed on UpdateOne operations") + } + if m.id == nil || m.oldValue == nil { + return v, errors.New("OldLog requires an ID field in the mutation") + } + oldValue, err := m.oldValue(ctx) + if err != nil { + return v, fmt.Errorf("querying old value for OldLog: %w", err) + } + return oldValue.Log, nil +} + +// ResetLog resets all changes to the "log" field. +func (m *MaintenanceOperationRunMutation) ResetLog() { + m.log = nil +} + +// Where appends a list predicates to the MaintenanceOperationRunMutation builder. +func (m *MaintenanceOperationRunMutation) Where(ps ...predicate.MaintenanceOperationRun) { + m.predicates = append(m.predicates, ps...) +} + +// WhereP appends storage-level predicates to the MaintenanceOperationRunMutation builder. Using this method, +// users can use type-assertion to append predicates that do not depend on any generated package. +func (m *MaintenanceOperationRunMutation) WhereP(ps ...func(*sql.Selector)) { + p := make([]predicate.MaintenanceOperationRun, len(ps)) + for i := range ps { + p[i] = ps[i] + } + m.Where(p...) +} + +// Op returns the operation name. +func (m *MaintenanceOperationRunMutation) Op() Op { + return m.op +} + +// SetOp allows setting the mutation operation. +func (m *MaintenanceOperationRunMutation) SetOp(op Op) { + m.op = op +} + +// Type returns the node type of this mutation (MaintenanceOperationRun). +func (m *MaintenanceOperationRunMutation) Type() string { + return m.typ +} + +// Fields returns all fields that were changed during this mutation. Note that in +// order to get all numeric fields that were incremented/decremented, call +// AddedFields(). +func (m *MaintenanceOperationRunMutation) Fields() []string { + fields := make([]string, 0, 7) + if m.operation_key != nil { + fields = append(fields, maintenanceoperationrun.FieldOperationKey) + } + if m.status != nil { + fields = append(fields, maintenanceoperationrun.FieldStatus) + } + if m.started_at != nil { + fields = append(fields, maintenanceoperationrun.FieldStartedAt) + } + if m.completed_at != nil { + fields = append(fields, maintenanceoperationrun.FieldCompletedAt) + } + if m.started_by != nil { + fields = append(fields, maintenanceoperationrun.FieldStartedBy) + } + if m.result != nil { + fields = append(fields, maintenanceoperationrun.FieldResult) + } + if m.log != nil { + fields = append(fields, maintenanceoperationrun.FieldLog) + } + return fields +} + +// Field returns the value of a field with the given name. The second boolean +// return value indicates that this field was not set, or was not defined in the +// schema. +func (m *MaintenanceOperationRunMutation) Field(name string) (ent.Value, bool) { + switch name { + case maintenanceoperationrun.FieldOperationKey: + return m.OperationKey() + case maintenanceoperationrun.FieldStatus: + return m.Status() + case maintenanceoperationrun.FieldStartedAt: + return m.StartedAt() + case maintenanceoperationrun.FieldCompletedAt: + return m.CompletedAt() + case maintenanceoperationrun.FieldStartedBy: + return m.StartedBy() + case maintenanceoperationrun.FieldResult: + return m.Result() + case maintenanceoperationrun.FieldLog: + return m.Log() + } + return nil, false +} + +// OldField returns the old value of the field from the database. An error is +// returned if the mutation operation is not UpdateOne, or the query to the +// database failed. +func (m *MaintenanceOperationRunMutation) OldField(ctx context.Context, name string) (ent.Value, error) { + switch name { + case maintenanceoperationrun.FieldOperationKey: + return m.OldOperationKey(ctx) + case maintenanceoperationrun.FieldStatus: + return m.OldStatus(ctx) + case maintenanceoperationrun.FieldStartedAt: + return m.OldStartedAt(ctx) + case maintenanceoperationrun.FieldCompletedAt: + return m.OldCompletedAt(ctx) + case maintenanceoperationrun.FieldStartedBy: + return m.OldStartedBy(ctx) + case maintenanceoperationrun.FieldResult: + return m.OldResult(ctx) + case maintenanceoperationrun.FieldLog: + return m.OldLog(ctx) + } + return nil, fmt.Errorf("unknown MaintenanceOperationRun field %s", name) +} + +// SetField sets the value of a field with the given name. It returns an error if +// the field is not defined in the schema, or if the type mismatched the field +// type. +func (m *MaintenanceOperationRunMutation) SetField(name string, value ent.Value) error { + switch name { + case maintenanceoperationrun.FieldOperationKey: + v, ok := value.(string) + if !ok { + return fmt.Errorf("unexpected type %T for field %s", value, name) + } + m.SetOperationKey(v) + return nil + case maintenanceoperationrun.FieldStatus: + v, ok := value.(string) + if !ok { + return fmt.Errorf("unexpected type %T for field %s", value, name) + } + m.SetStatus(v) + return nil + case maintenanceoperationrun.FieldStartedAt: + v, ok := value.(time.Time) + if !ok { + return fmt.Errorf("unexpected type %T for field %s", value, name) + } + m.SetStartedAt(v) + return nil + case maintenanceoperationrun.FieldCompletedAt: + v, ok := value.(time.Time) + if !ok { + return fmt.Errorf("unexpected type %T for field %s", value, name) + } + m.SetCompletedAt(v) + return nil + case maintenanceoperationrun.FieldStartedBy: + v, ok := value.(string) + if !ok { + return fmt.Errorf("unexpected type %T for field %s", value, name) + } + m.SetStartedBy(v) + return nil + case maintenanceoperationrun.FieldResult: + v, ok := value.(string) + if !ok { + return fmt.Errorf("unexpected type %T for field %s", value, name) + } + m.SetResult(v) + return nil + case maintenanceoperationrun.FieldLog: + v, ok := value.(string) + if !ok { + return fmt.Errorf("unexpected type %T for field %s", value, name) + } + m.SetLog(v) + return nil + } + return fmt.Errorf("unknown MaintenanceOperationRun field %s", name) +} + +// AddedFields returns all numeric fields that were incremented/decremented during +// this mutation. +func (m *MaintenanceOperationRunMutation) AddedFields() []string { + return nil +} + +// AddedField returns the numeric value that was incremented/decremented on a field +// with the given name. The second boolean return value indicates that this field +// was not set, or was not defined in the schema. +func (m *MaintenanceOperationRunMutation) AddedField(name string) (ent.Value, bool) { + return nil, false +} + +// AddField adds the value to the field with the given name. It returns an error if +// the field is not defined in the schema, or if the type mismatched the field +// type. +func (m *MaintenanceOperationRunMutation) AddField(name string, value ent.Value) error { + switch name { + } + return fmt.Errorf("unknown MaintenanceOperationRun numeric field %s", name) +} + +// ClearedFields returns all nullable fields that were cleared during this +// mutation. +func (m *MaintenanceOperationRunMutation) ClearedFields() []string { + var fields []string + if m.FieldCleared(maintenanceoperationrun.FieldCompletedAt) { + fields = append(fields, maintenanceoperationrun.FieldCompletedAt) + } + if m.FieldCleared(maintenanceoperationrun.FieldStartedBy) { + fields = append(fields, maintenanceoperationrun.FieldStartedBy) + } + if m.FieldCleared(maintenanceoperationrun.FieldResult) { + fields = append(fields, maintenanceoperationrun.FieldResult) + } + return fields +} + +// FieldCleared returns a boolean indicating if a field with the given name was +// cleared in this mutation. +func (m *MaintenanceOperationRunMutation) FieldCleared(name string) bool { + _, ok := m.clearedFields[name] + return ok +} + +// ClearField clears the value of the field with the given name. It returns an +// error if the field is not defined in the schema. +func (m *MaintenanceOperationRunMutation) ClearField(name string) error { + switch name { + case maintenanceoperationrun.FieldCompletedAt: + m.ClearCompletedAt() + return nil + case maintenanceoperationrun.FieldStartedBy: + m.ClearStartedBy() + return nil + case maintenanceoperationrun.FieldResult: + m.ClearResult() + return nil + } + return fmt.Errorf("unknown MaintenanceOperationRun nullable field %s", name) +} + +// ResetField resets all changes in the mutation for the field with the given name. +// It returns an error if the field is not defined in the schema. +func (m *MaintenanceOperationRunMutation) ResetField(name string) error { + switch name { + case maintenanceoperationrun.FieldOperationKey: + m.ResetOperationKey() + return nil + case maintenanceoperationrun.FieldStatus: + m.ResetStatus() + return nil + case maintenanceoperationrun.FieldStartedAt: + m.ResetStartedAt() + return nil + case maintenanceoperationrun.FieldCompletedAt: + m.ResetCompletedAt() + return nil + case maintenanceoperationrun.FieldStartedBy: + m.ResetStartedBy() + return nil + case maintenanceoperationrun.FieldResult: + m.ResetResult() + return nil + case maintenanceoperationrun.FieldLog: + m.ResetLog() + return nil + } + return fmt.Errorf("unknown MaintenanceOperationRun field %s", name) +} + +// AddedEdges returns all edge names that were set/added in this mutation. +func (m *MaintenanceOperationRunMutation) AddedEdges() []string { + edges := make([]string, 0, 0) + return edges +} + +// AddedIDs returns all IDs (to other nodes) that were added for the given edge +// name in this mutation. +func (m *MaintenanceOperationRunMutation) AddedIDs(name string) []ent.Value { + return nil +} + +// RemovedEdges returns all edge names that were removed in this mutation. +func (m *MaintenanceOperationRunMutation) RemovedEdges() []string { + edges := make([]string, 0, 0) + return edges +} + +// RemovedIDs returns all IDs (to other nodes) that were removed for the edge with +// the given name in this mutation. +func (m *MaintenanceOperationRunMutation) RemovedIDs(name string) []ent.Value { + return nil +} + +// ClearedEdges returns all edge names that were cleared in this mutation. +func (m *MaintenanceOperationRunMutation) ClearedEdges() []string { + edges := make([]string, 0, 0) + return edges +} + +// EdgeCleared returns a boolean which indicates if the edge with the given name +// was cleared in this mutation. +func (m *MaintenanceOperationRunMutation) EdgeCleared(name string) bool { + return false +} + +// ClearEdge clears the value of the edge with the given name. It returns an error +// if that edge is not defined in the schema. +func (m *MaintenanceOperationRunMutation) ClearEdge(name string) error { + return fmt.Errorf("unknown MaintenanceOperationRun unique edge %s", name) +} + +// ResetEdge resets all changes to the edge with the given name in this mutation. +// It returns an error if the edge is not defined in the schema. +func (m *MaintenanceOperationRunMutation) ResetEdge(name string) error { + return fmt.Errorf("unknown MaintenanceOperationRun edge %s", name) +} + +// MessageMutation represents an operation that mutates the Message nodes in the graph. +type MessageMutation struct { + config + op Op + typ string + id *uuid.UUID + project_id *uuid.UUID + sender *string + sender_id *string + recipient *string + recipient_id *string + msg *string + _type *string + urgent *bool + broadcasted *bool + read *bool + agent_id *string + group_id *string + dispatch_state *string + dispatched_at *time.Time + created *time.Time + clearedFields map[string]struct{} + done bool + oldValue func(context.Context) (*Message, error) + predicates []predicate.Message +} + +var _ ent.Mutation = (*MessageMutation)(nil) + +// messageOption allows management of the mutation configuration using functional options. +type messageOption func(*MessageMutation) + +// newMessageMutation creates new mutation for the Message entity. +func newMessageMutation(c config, op Op, opts ...messageOption) *MessageMutation { + m := &MessageMutation{ + config: c, + op: op, + typ: TypeMessage, + clearedFields: make(map[string]struct{}), + } + for _, opt := range opts { + opt(m) + } + return m +} + +// withMessageID sets the ID field of the mutation. +func withMessageID(id uuid.UUID) messageOption { + return func(m *MessageMutation) { + var ( + err error + once sync.Once + value *Message + ) + m.oldValue = func(ctx context.Context) (*Message, error) { + once.Do(func() { + if m.done { + err = errors.New("querying old values post mutation is not allowed") + } else { + value, err = m.Client().Message.Get(ctx, id) + } + }) + return value, err + } + m.id = &id + } +} + +// withMessage sets the old Message of the mutation. +func withMessage(node *Message) messageOption { + return func(m *MessageMutation) { + m.oldValue = func(context.Context) (*Message, error) { + return node, nil + } + m.id = &node.ID + } +} + +// Client returns a new `ent.Client` from the mutation. If the mutation was +// executed in a transaction (ent.Tx), a transactional client is returned. +func (m MessageMutation) Client() *Client { + client := &Client{config: m.config} + client.init() + return client +} + +// Tx returns an `ent.Tx` for mutations that were executed in transactions; +// it returns an error otherwise. +func (m MessageMutation) Tx() (*Tx, error) { + if _, ok := m.driver.(*txDriver); !ok { + return nil, errors.New("ent: mutation is not running in a transaction") + } + tx := &Tx{config: m.config} + tx.init() + return tx, nil +} + +// SetID sets the value of the id field. Note that this +// operation is only accepted on creation of Message entities. +func (m *MessageMutation) SetID(id uuid.UUID) { + m.id = &id +} + +// ID returns the ID value in the mutation. Note that the ID is only available +// if it was provided to the builder or after it was returned from the database. +func (m *MessageMutation) ID() (id uuid.UUID, exists bool) { + if m.id == nil { + return + } + return *m.id, true +} + +// IDs queries the database and returns the entity ids that match the mutation's predicate. +// That means, if the mutation is applied within a transaction with an isolation level such +// as sql.LevelSerializable, the returned ids match the ids of the rows that will be updated +// or updated by the mutation. +func (m *MessageMutation) IDs(ctx context.Context) ([]uuid.UUID, error) { + switch { + case m.op.Is(OpUpdateOne | OpDeleteOne): + id, exists := m.ID() + if exists { + return []uuid.UUID{id}, nil + } + fallthrough + case m.op.Is(OpUpdate | OpDelete): + return m.Client().Message.Query().Where(m.predicates...).IDs(ctx) + default: + return nil, fmt.Errorf("IDs is not allowed on %s operations", m.op) + } +} + +// SetProjectID sets the "project_id" field. +func (m *MessageMutation) SetProjectID(u uuid.UUID) { + m.project_id = &u +} + +// ProjectID returns the value of the "project_id" field in the mutation. +func (m *MessageMutation) ProjectID() (r uuid.UUID, exists bool) { + v := m.project_id + if v == nil { + return + } + return *v, true +} + +// OldProjectID returns the old "project_id" field's value of the Message entity. +// If the Message object wasn't provided to the builder, the object is fetched from the database. +// An error is returned if the mutation operation is not UpdateOne, or the database query fails. +func (m *MessageMutation) OldProjectID(ctx context.Context) (v uuid.UUID, err error) { + if !m.op.Is(OpUpdateOne) { + return v, errors.New("OldProjectID is only allowed on UpdateOne operations") + } + if m.id == nil || m.oldValue == nil { + return v, errors.New("OldProjectID requires an ID field in the mutation") + } + oldValue, err := m.oldValue(ctx) + if err != nil { + return v, fmt.Errorf("querying old value for OldProjectID: %w", err) + } + return oldValue.ProjectID, nil +} + +// ResetProjectID resets all changes to the "project_id" field. +func (m *MessageMutation) ResetProjectID() { + m.project_id = nil +} + +// SetSender sets the "sender" field. +func (m *MessageMutation) SetSender(s string) { + m.sender = &s +} + +// Sender returns the value of the "sender" field in the mutation. +func (m *MessageMutation) Sender() (r string, exists bool) { + v := m.sender + if v == nil { + return + } + return *v, true +} + +// OldSender returns the old "sender" field's value of the Message entity. +// If the Message object wasn't provided to the builder, the object is fetched from the database. +// An error is returned if the mutation operation is not UpdateOne, or the database query fails. +func (m *MessageMutation) OldSender(ctx context.Context) (v string, err error) { + if !m.op.Is(OpUpdateOne) { + return v, errors.New("OldSender is only allowed on UpdateOne operations") + } + if m.id == nil || m.oldValue == nil { + return v, errors.New("OldSender requires an ID field in the mutation") + } + oldValue, err := m.oldValue(ctx) + if err != nil { + return v, fmt.Errorf("querying old value for OldSender: %w", err) + } + return oldValue.Sender, nil +} + +// ResetSender resets all changes to the "sender" field. +func (m *MessageMutation) ResetSender() { + m.sender = nil +} + +// SetSenderID sets the "sender_id" field. +func (m *MessageMutation) SetSenderID(s string) { + m.sender_id = &s +} + +// SenderID returns the value of the "sender_id" field in the mutation. +func (m *MessageMutation) SenderID() (r string, exists bool) { + v := m.sender_id + if v == nil { + return + } + return *v, true +} + +// OldSenderID returns the old "sender_id" field's value of the Message entity. +// If the Message object wasn't provided to the builder, the object is fetched from the database. +// An error is returned if the mutation operation is not UpdateOne, or the database query fails. +func (m *MessageMutation) OldSenderID(ctx context.Context) (v string, err error) { + if !m.op.Is(OpUpdateOne) { + return v, errors.New("OldSenderID is only allowed on UpdateOne operations") + } + if m.id == nil || m.oldValue == nil { + return v, errors.New("OldSenderID requires an ID field in the mutation") + } + oldValue, err := m.oldValue(ctx) + if err != nil { + return v, fmt.Errorf("querying old value for OldSenderID: %w", err) + } + return oldValue.SenderID, nil +} + +// ClearSenderID clears the value of the "sender_id" field. +func (m *MessageMutation) ClearSenderID() { + m.sender_id = nil + m.clearedFields[message.FieldSenderID] = struct{}{} +} + +// SenderIDCleared returns if the "sender_id" field was cleared in this mutation. +func (m *MessageMutation) SenderIDCleared() bool { + _, ok := m.clearedFields[message.FieldSenderID] + return ok +} + +// ResetSenderID resets all changes to the "sender_id" field. +func (m *MessageMutation) ResetSenderID() { + m.sender_id = nil + delete(m.clearedFields, message.FieldSenderID) +} + +// SetRecipient sets the "recipient" field. +func (m *MessageMutation) SetRecipient(s string) { + m.recipient = &s +} + +// Recipient returns the value of the "recipient" field in the mutation. +func (m *MessageMutation) Recipient() (r string, exists bool) { + v := m.recipient + if v == nil { + return + } + return *v, true +} + +// OldRecipient returns the old "recipient" field's value of the Message entity. +// If the Message object wasn't provided to the builder, the object is fetched from the database. +// An error is returned if the mutation operation is not UpdateOne, or the database query fails. +func (m *MessageMutation) OldRecipient(ctx context.Context) (v string, err error) { + if !m.op.Is(OpUpdateOne) { + return v, errors.New("OldRecipient is only allowed on UpdateOne operations") + } + if m.id == nil || m.oldValue == nil { + return v, errors.New("OldRecipient requires an ID field in the mutation") + } + oldValue, err := m.oldValue(ctx) + if err != nil { + return v, fmt.Errorf("querying old value for OldRecipient: %w", err) + } + return oldValue.Recipient, nil +} + +// ResetRecipient resets all changes to the "recipient" field. +func (m *MessageMutation) ResetRecipient() { + m.recipient = nil +} + +// SetRecipientID sets the "recipient_id" field. +func (m *MessageMutation) SetRecipientID(s string) { + m.recipient_id = &s +} + +// RecipientID returns the value of the "recipient_id" field in the mutation. +func (m *MessageMutation) RecipientID() (r string, exists bool) { + v := m.recipient_id + if v == nil { + return + } + return *v, true +} + +// OldRecipientID returns the old "recipient_id" field's value of the Message entity. +// If the Message object wasn't provided to the builder, the object is fetched from the database. +// An error is returned if the mutation operation is not UpdateOne, or the database query fails. +func (m *MessageMutation) OldRecipientID(ctx context.Context) (v string, err error) { + if !m.op.Is(OpUpdateOne) { + return v, errors.New("OldRecipientID is only allowed on UpdateOne operations") + } + if m.id == nil || m.oldValue == nil { + return v, errors.New("OldRecipientID requires an ID field in the mutation") + } + oldValue, err := m.oldValue(ctx) + if err != nil { + return v, fmt.Errorf("querying old value for OldRecipientID: %w", err) + } + return oldValue.RecipientID, nil +} + +// ClearRecipientID clears the value of the "recipient_id" field. +func (m *MessageMutation) ClearRecipientID() { + m.recipient_id = nil + m.clearedFields[message.FieldRecipientID] = struct{}{} +} + +// RecipientIDCleared returns if the "recipient_id" field was cleared in this mutation. +func (m *MessageMutation) RecipientIDCleared() bool { + _, ok := m.clearedFields[message.FieldRecipientID] + return ok +} + +// ResetRecipientID resets all changes to the "recipient_id" field. +func (m *MessageMutation) ResetRecipientID() { + m.recipient_id = nil + delete(m.clearedFields, message.FieldRecipientID) +} + +// SetMsg sets the "msg" field. +func (m *MessageMutation) SetMsg(s string) { + m.msg = &s +} + +// Msg returns the value of the "msg" field in the mutation. +func (m *MessageMutation) Msg() (r string, exists bool) { + v := m.msg + if v == nil { + return + } + return *v, true +} + +// OldMsg returns the old "msg" field's value of the Message entity. +// If the Message object wasn't provided to the builder, the object is fetched from the database. +// An error is returned if the mutation operation is not UpdateOne, or the database query fails. +func (m *MessageMutation) OldMsg(ctx context.Context) (v string, err error) { + if !m.op.Is(OpUpdateOne) { + return v, errors.New("OldMsg is only allowed on UpdateOne operations") + } + if m.id == nil || m.oldValue == nil { + return v, errors.New("OldMsg requires an ID field in the mutation") + } + oldValue, err := m.oldValue(ctx) + if err != nil { + return v, fmt.Errorf("querying old value for OldMsg: %w", err) + } + return oldValue.Msg, nil +} + +// ResetMsg resets all changes to the "msg" field. +func (m *MessageMutation) ResetMsg() { + m.msg = nil +} + +// SetType sets the "type" field. +func (m *MessageMutation) SetType(s string) { + m._type = &s +} + +// GetType returns the value of the "type" field in the mutation. +func (m *MessageMutation) GetType() (r string, exists bool) { + v := m._type + if v == nil { + return + } + return *v, true +} + +// OldType returns the old "type" field's value of the Message entity. +// If the Message object wasn't provided to the builder, the object is fetched from the database. +// An error is returned if the mutation operation is not UpdateOne, or the database query fails. +func (m *MessageMutation) OldType(ctx context.Context) (v string, err error) { + if !m.op.Is(OpUpdateOne) { + return v, errors.New("OldType is only allowed on UpdateOne operations") + } + if m.id == nil || m.oldValue == nil { + return v, errors.New("OldType requires an ID field in the mutation") + } + oldValue, err := m.oldValue(ctx) + if err != nil { + return v, fmt.Errorf("querying old value for OldType: %w", err) + } + return oldValue.Type, nil +} + +// ResetType resets all changes to the "type" field. +func (m *MessageMutation) ResetType() { + m._type = nil +} + +// SetUrgent sets the "urgent" field. +func (m *MessageMutation) SetUrgent(b bool) { + m.urgent = &b +} + +// Urgent returns the value of the "urgent" field in the mutation. +func (m *MessageMutation) Urgent() (r bool, exists bool) { + v := m.urgent + if v == nil { + return + } + return *v, true +} + +// OldUrgent returns the old "urgent" field's value of the Message entity. +// If the Message object wasn't provided to the builder, the object is fetched from the database. +// An error is returned if the mutation operation is not UpdateOne, or the database query fails. +func (m *MessageMutation) OldUrgent(ctx context.Context) (v bool, err error) { + if !m.op.Is(OpUpdateOne) { + return v, errors.New("OldUrgent is only allowed on UpdateOne operations") + } + if m.id == nil || m.oldValue == nil { + return v, errors.New("OldUrgent requires an ID field in the mutation") + } + oldValue, err := m.oldValue(ctx) + if err != nil { + return v, fmt.Errorf("querying old value for OldUrgent: %w", err) + } + return oldValue.Urgent, nil +} + +// ResetUrgent resets all changes to the "urgent" field. +func (m *MessageMutation) ResetUrgent() { + m.urgent = nil +} + +// SetBroadcasted sets the "broadcasted" field. +func (m *MessageMutation) SetBroadcasted(b bool) { + m.broadcasted = &b +} + +// Broadcasted returns the value of the "broadcasted" field in the mutation. +func (m *MessageMutation) Broadcasted() (r bool, exists bool) { + v := m.broadcasted + if v == nil { + return + } + return *v, true +} + +// OldBroadcasted returns the old "broadcasted" field's value of the Message entity. +// If the Message object wasn't provided to the builder, the object is fetched from the database. +// An error is returned if the mutation operation is not UpdateOne, or the database query fails. +func (m *MessageMutation) OldBroadcasted(ctx context.Context) (v bool, err error) { + if !m.op.Is(OpUpdateOne) { + return v, errors.New("OldBroadcasted is only allowed on UpdateOne operations") + } + if m.id == nil || m.oldValue == nil { + return v, errors.New("OldBroadcasted requires an ID field in the mutation") + } + oldValue, err := m.oldValue(ctx) + if err != nil { + return v, fmt.Errorf("querying old value for OldBroadcasted: %w", err) + } + return oldValue.Broadcasted, nil +} + +// ResetBroadcasted resets all changes to the "broadcasted" field. +func (m *MessageMutation) ResetBroadcasted() { + m.broadcasted = nil +} + +// SetRead sets the "read" field. +func (m *MessageMutation) SetRead(b bool) { + m.read = &b +} + +// Read returns the value of the "read" field in the mutation. +func (m *MessageMutation) Read() (r bool, exists bool) { + v := m.read + if v == nil { + return + } + return *v, true +} + +// OldRead returns the old "read" field's value of the Message entity. +// If the Message object wasn't provided to the builder, the object is fetched from the database. +// An error is returned if the mutation operation is not UpdateOne, or the database query fails. +func (m *MessageMutation) OldRead(ctx context.Context) (v bool, err error) { + if !m.op.Is(OpUpdateOne) { + return v, errors.New("OldRead is only allowed on UpdateOne operations") + } + if m.id == nil || m.oldValue == nil { + return v, errors.New("OldRead requires an ID field in the mutation") + } + oldValue, err := m.oldValue(ctx) + if err != nil { + return v, fmt.Errorf("querying old value for OldRead: %w", err) + } + return oldValue.Read, nil +} + +// ResetRead resets all changes to the "read" field. +func (m *MessageMutation) ResetRead() { + m.read = nil +} + +// SetAgentID sets the "agent_id" field. +func (m *MessageMutation) SetAgentID(s string) { + m.agent_id = &s +} + +// AgentID returns the value of the "agent_id" field in the mutation. +func (m *MessageMutation) AgentID() (r string, exists bool) { + v := m.agent_id + if v == nil { + return + } + return *v, true +} + +// OldAgentID returns the old "agent_id" field's value of the Message entity. +// If the Message object wasn't provided to the builder, the object is fetched from the database. +// An error is returned if the mutation operation is not UpdateOne, or the database query fails. +func (m *MessageMutation) OldAgentID(ctx context.Context) (v string, err error) { + if !m.op.Is(OpUpdateOne) { + return v, errors.New("OldAgentID is only allowed on UpdateOne operations") + } + if m.id == nil || m.oldValue == nil { + return v, errors.New("OldAgentID requires an ID field in the mutation") + } + oldValue, err := m.oldValue(ctx) + if err != nil { + return v, fmt.Errorf("querying old value for OldAgentID: %w", err) + } + return oldValue.AgentID, nil +} + +// ClearAgentID clears the value of the "agent_id" field. +func (m *MessageMutation) ClearAgentID() { + m.agent_id = nil + m.clearedFields[message.FieldAgentID] = struct{}{} +} + +// AgentIDCleared returns if the "agent_id" field was cleared in this mutation. +func (m *MessageMutation) AgentIDCleared() bool { + _, ok := m.clearedFields[message.FieldAgentID] + return ok +} + +// ResetAgentID resets all changes to the "agent_id" field. +func (m *MessageMutation) ResetAgentID() { + m.agent_id = nil + delete(m.clearedFields, message.FieldAgentID) +} + +// SetGroupID sets the "group_id" field. +func (m *MessageMutation) SetGroupID(s string) { + m.group_id = &s +} + +// GroupID returns the value of the "group_id" field in the mutation. +func (m *MessageMutation) GroupID() (r string, exists bool) { + v := m.group_id + if v == nil { + return + } + return *v, true +} + +// OldGroupID returns the old "group_id" field's value of the Message entity. +// If the Message object wasn't provided to the builder, the object is fetched from the database. +// An error is returned if the mutation operation is not UpdateOne, or the database query fails. +func (m *MessageMutation) OldGroupID(ctx context.Context) (v string, err error) { + if !m.op.Is(OpUpdateOne) { + return v, errors.New("OldGroupID is only allowed on UpdateOne operations") + } + if m.id == nil || m.oldValue == nil { + return v, errors.New("OldGroupID requires an ID field in the mutation") + } + oldValue, err := m.oldValue(ctx) + if err != nil { + return v, fmt.Errorf("querying old value for OldGroupID: %w", err) + } + return oldValue.GroupID, nil +} + +// ClearGroupID clears the value of the "group_id" field. +func (m *MessageMutation) ClearGroupID() { + m.group_id = nil + m.clearedFields[message.FieldGroupID] = struct{}{} +} + +// GroupIDCleared returns if the "group_id" field was cleared in this mutation. +func (m *MessageMutation) GroupIDCleared() bool { + _, ok := m.clearedFields[message.FieldGroupID] + return ok +} + +// ResetGroupID resets all changes to the "group_id" field. +func (m *MessageMutation) ResetGroupID() { + m.group_id = nil + delete(m.clearedFields, message.FieldGroupID) +} + +// SetDispatchState sets the "dispatch_state" field. +func (m *MessageMutation) SetDispatchState(s string) { + m.dispatch_state = &s +} + +// DispatchState returns the value of the "dispatch_state" field in the mutation. +func (m *MessageMutation) DispatchState() (r string, exists bool) { + v := m.dispatch_state + if v == nil { + return + } + return *v, true +} + +// OldDispatchState returns the old "dispatch_state" field's value of the Message entity. +// If the Message object wasn't provided to the builder, the object is fetched from the database. +// An error is returned if the mutation operation is not UpdateOne, or the database query fails. +func (m *MessageMutation) OldDispatchState(ctx context.Context) (v string, err error) { + if !m.op.Is(OpUpdateOne) { + return v, errors.New("OldDispatchState is only allowed on UpdateOne operations") + } + if m.id == nil || m.oldValue == nil { + return v, errors.New("OldDispatchState requires an ID field in the mutation") + } + oldValue, err := m.oldValue(ctx) + if err != nil { + return v, fmt.Errorf("querying old value for OldDispatchState: %w", err) + } + return oldValue.DispatchState, nil +} + +// ResetDispatchState resets all changes to the "dispatch_state" field. +func (m *MessageMutation) ResetDispatchState() { + m.dispatch_state = nil +} + +// SetDispatchedAt sets the "dispatched_at" field. +func (m *MessageMutation) SetDispatchedAt(t time.Time) { + m.dispatched_at = &t +} + +// DispatchedAt returns the value of the "dispatched_at" field in the mutation. +func (m *MessageMutation) DispatchedAt() (r time.Time, exists bool) { + v := m.dispatched_at + if v == nil { + return + } + return *v, true +} + +// OldDispatchedAt returns the old "dispatched_at" field's value of the Message entity. +// If the Message object wasn't provided to the builder, the object is fetched from the database. +// An error is returned if the mutation operation is not UpdateOne, or the database query fails. +func (m *MessageMutation) OldDispatchedAt(ctx context.Context) (v *time.Time, err error) { + if !m.op.Is(OpUpdateOne) { + return v, errors.New("OldDispatchedAt is only allowed on UpdateOne operations") + } + if m.id == nil || m.oldValue == nil { + return v, errors.New("OldDispatchedAt requires an ID field in the mutation") + } + oldValue, err := m.oldValue(ctx) + if err != nil { + return v, fmt.Errorf("querying old value for OldDispatchedAt: %w", err) + } + return oldValue.DispatchedAt, nil +} + +// ClearDispatchedAt clears the value of the "dispatched_at" field. +func (m *MessageMutation) ClearDispatchedAt() { + m.dispatched_at = nil + m.clearedFields[message.FieldDispatchedAt] = struct{}{} +} + +// DispatchedAtCleared returns if the "dispatched_at" field was cleared in this mutation. +func (m *MessageMutation) DispatchedAtCleared() bool { + _, ok := m.clearedFields[message.FieldDispatchedAt] + return ok +} + +// ResetDispatchedAt resets all changes to the "dispatched_at" field. +func (m *MessageMutation) ResetDispatchedAt() { + m.dispatched_at = nil + delete(m.clearedFields, message.FieldDispatchedAt) +} + +// SetCreated sets the "created" field. +func (m *MessageMutation) SetCreated(t time.Time) { + m.created = &t +} + +// Created returns the value of the "created" field in the mutation. +func (m *MessageMutation) Created() (r time.Time, exists bool) { + v := m.created + if v == nil { + return + } + return *v, true +} + +// OldCreated returns the old "created" field's value of the Message entity. +// If the Message object wasn't provided to the builder, the object is fetched from the database. +// An error is returned if the mutation operation is not UpdateOne, or the database query fails. +func (m *MessageMutation) OldCreated(ctx context.Context) (v time.Time, err error) { + if !m.op.Is(OpUpdateOne) { + return v, errors.New("OldCreated is only allowed on UpdateOne operations") + } + if m.id == nil || m.oldValue == nil { + return v, errors.New("OldCreated requires an ID field in the mutation") + } + oldValue, err := m.oldValue(ctx) + if err != nil { + return v, fmt.Errorf("querying old value for OldCreated: %w", err) + } + return oldValue.Created, nil +} + +// ResetCreated resets all changes to the "created" field. +func (m *MessageMutation) ResetCreated() { + m.created = nil +} + +// Where appends a list predicates to the MessageMutation builder. +func (m *MessageMutation) Where(ps ...predicate.Message) { + m.predicates = append(m.predicates, ps...) +} + +// WhereP appends storage-level predicates to the MessageMutation builder. Using this method, +// users can use type-assertion to append predicates that do not depend on any generated package. +func (m *MessageMutation) WhereP(ps ...func(*sql.Selector)) { + p := make([]predicate.Message, len(ps)) + for i := range ps { + p[i] = ps[i] + } + m.Where(p...) +} + +// Op returns the operation name. +func (m *MessageMutation) Op() Op { + return m.op +} + +// SetOp allows setting the mutation operation. +func (m *MessageMutation) SetOp(op Op) { + m.op = op +} + +// Type returns the node type of this mutation (Message). +func (m *MessageMutation) Type() string { + return m.typ +} + +// Fields returns all fields that were changed during this mutation. Note that in +// order to get all numeric fields that were incremented/decremented, call +// AddedFields(). +func (m *MessageMutation) Fields() []string { + fields := make([]string, 0, 15) + if m.project_id != nil { + fields = append(fields, message.FieldProjectID) + } + if m.sender != nil { + fields = append(fields, message.FieldSender) + } + if m.sender_id != nil { + fields = append(fields, message.FieldSenderID) + } + if m.recipient != nil { + fields = append(fields, message.FieldRecipient) + } + if m.recipient_id != nil { + fields = append(fields, message.FieldRecipientID) + } + if m.msg != nil { + fields = append(fields, message.FieldMsg) + } + if m._type != nil { + fields = append(fields, message.FieldType) + } + if m.urgent != nil { + fields = append(fields, message.FieldUrgent) + } + if m.broadcasted != nil { + fields = append(fields, message.FieldBroadcasted) + } + if m.read != nil { + fields = append(fields, message.FieldRead) + } + if m.agent_id != nil { + fields = append(fields, message.FieldAgentID) + } + if m.group_id != nil { + fields = append(fields, message.FieldGroupID) + } + if m.dispatch_state != nil { + fields = append(fields, message.FieldDispatchState) + } + if m.dispatched_at != nil { + fields = append(fields, message.FieldDispatchedAt) + } + if m.created != nil { + fields = append(fields, message.FieldCreated) + } + return fields +} + +// Field returns the value of a field with the given name. The second boolean +// return value indicates that this field was not set, or was not defined in the +// schema. +func (m *MessageMutation) Field(name string) (ent.Value, bool) { + switch name { + case message.FieldProjectID: + return m.ProjectID() + case message.FieldSender: + return m.Sender() + case message.FieldSenderID: + return m.SenderID() + case message.FieldRecipient: + return m.Recipient() + case message.FieldRecipientID: + return m.RecipientID() + case message.FieldMsg: + return m.Msg() + case message.FieldType: + return m.GetType() + case message.FieldUrgent: + return m.Urgent() + case message.FieldBroadcasted: + return m.Broadcasted() + case message.FieldRead: + return m.Read() + case message.FieldAgentID: + return m.AgentID() + case message.FieldGroupID: + return m.GroupID() + case message.FieldDispatchState: + return m.DispatchState() + case message.FieldDispatchedAt: + return m.DispatchedAt() + case message.FieldCreated: + return m.Created() + } + return nil, false +} + +// OldField returns the old value of the field from the database. An error is +// returned if the mutation operation is not UpdateOne, or the query to the +// database failed. +func (m *MessageMutation) OldField(ctx context.Context, name string) (ent.Value, error) { + switch name { + case message.FieldProjectID: + return m.OldProjectID(ctx) + case message.FieldSender: + return m.OldSender(ctx) + case message.FieldSenderID: + return m.OldSenderID(ctx) + case message.FieldRecipient: + return m.OldRecipient(ctx) + case message.FieldRecipientID: + return m.OldRecipientID(ctx) + case message.FieldMsg: + return m.OldMsg(ctx) + case message.FieldType: + return m.OldType(ctx) + case message.FieldUrgent: + return m.OldUrgent(ctx) + case message.FieldBroadcasted: + return m.OldBroadcasted(ctx) + case message.FieldRead: + return m.OldRead(ctx) + case message.FieldAgentID: + return m.OldAgentID(ctx) + case message.FieldGroupID: + return m.OldGroupID(ctx) + case message.FieldDispatchState: + return m.OldDispatchState(ctx) + case message.FieldDispatchedAt: + return m.OldDispatchedAt(ctx) + case message.FieldCreated: + return m.OldCreated(ctx) + } + return nil, fmt.Errorf("unknown Message field %s", name) +} + +// SetField sets the value of a field with the given name. It returns an error if +// the field is not defined in the schema, or if the type mismatched the field +// type. +func (m *MessageMutation) SetField(name string, value ent.Value) error { + switch name { + case message.FieldProjectID: + v, ok := value.(uuid.UUID) + if !ok { + return fmt.Errorf("unexpected type %T for field %s", value, name) + } + m.SetProjectID(v) + return nil + case message.FieldSender: + v, ok := value.(string) + if !ok { + return fmt.Errorf("unexpected type %T for field %s", value, name) + } + m.SetSender(v) + return nil + case message.FieldSenderID: + v, ok := value.(string) + if !ok { + return fmt.Errorf("unexpected type %T for field %s", value, name) + } + m.SetSenderID(v) + return nil + case message.FieldRecipient: + v, ok := value.(string) + if !ok { + return fmt.Errorf("unexpected type %T for field %s", value, name) + } + m.SetRecipient(v) + return nil + case message.FieldRecipientID: + v, ok := value.(string) + if !ok { + return fmt.Errorf("unexpected type %T for field %s", value, name) + } + m.SetRecipientID(v) + return nil + case message.FieldMsg: + v, ok := value.(string) + if !ok { + return fmt.Errorf("unexpected type %T for field %s", value, name) + } + m.SetMsg(v) + return nil + case message.FieldType: + v, ok := value.(string) + if !ok { + return fmt.Errorf("unexpected type %T for field %s", value, name) + } + m.SetType(v) + return nil + case message.FieldUrgent: + v, ok := value.(bool) + if !ok { + return fmt.Errorf("unexpected type %T for field %s", value, name) + } + m.SetUrgent(v) + return nil + case message.FieldBroadcasted: + v, ok := value.(bool) + if !ok { + return fmt.Errorf("unexpected type %T for field %s", value, name) + } + m.SetBroadcasted(v) + return nil + case message.FieldRead: + v, ok := value.(bool) + if !ok { + return fmt.Errorf("unexpected type %T for field %s", value, name) + } + m.SetRead(v) + return nil + case message.FieldAgentID: + v, ok := value.(string) + if !ok { + return fmt.Errorf("unexpected type %T for field %s", value, name) + } + m.SetAgentID(v) + return nil + case message.FieldGroupID: + v, ok := value.(string) + if !ok { + return fmt.Errorf("unexpected type %T for field %s", value, name) + } + m.SetGroupID(v) + return nil + case message.FieldDispatchState: + v, ok := value.(string) + if !ok { + return fmt.Errorf("unexpected type %T for field %s", value, name) + } + m.SetDispatchState(v) + return nil + case message.FieldDispatchedAt: + v, ok := value.(time.Time) + if !ok { + return fmt.Errorf("unexpected type %T for field %s", value, name) + } + m.SetDispatchedAt(v) + return nil + case message.FieldCreated: + v, ok := value.(time.Time) + if !ok { + return fmt.Errorf("unexpected type %T for field %s", value, name) + } + m.SetCreated(v) + return nil + } + return fmt.Errorf("unknown Message field %s", name) +} + +// AddedFields returns all numeric fields that were incremented/decremented during +// this mutation. +func (m *MessageMutation) AddedFields() []string { + return nil +} + +// AddedField returns the numeric value that was incremented/decremented on a field +// with the given name. The second boolean return value indicates that this field +// was not set, or was not defined in the schema. +func (m *MessageMutation) AddedField(name string) (ent.Value, bool) { + return nil, false +} + +// AddField adds the value to the field with the given name. It returns an error if +// the field is not defined in the schema, or if the type mismatched the field +// type. +func (m *MessageMutation) AddField(name string, value ent.Value) error { + switch name { + } + return fmt.Errorf("unknown Message numeric field %s", name) +} + +// ClearedFields returns all nullable fields that were cleared during this +// mutation. +func (m *MessageMutation) ClearedFields() []string { + var fields []string + if m.FieldCleared(message.FieldSenderID) { + fields = append(fields, message.FieldSenderID) + } + if m.FieldCleared(message.FieldRecipientID) { + fields = append(fields, message.FieldRecipientID) + } + if m.FieldCleared(message.FieldAgentID) { + fields = append(fields, message.FieldAgentID) + } + if m.FieldCleared(message.FieldGroupID) { + fields = append(fields, message.FieldGroupID) + } + if m.FieldCleared(message.FieldDispatchedAt) { + fields = append(fields, message.FieldDispatchedAt) + } + return fields +} + +// FieldCleared returns a boolean indicating if a field with the given name was +// cleared in this mutation. +func (m *MessageMutation) FieldCleared(name string) bool { + _, ok := m.clearedFields[name] + return ok +} + +// ClearField clears the value of the field with the given name. It returns an +// error if the field is not defined in the schema. +func (m *MessageMutation) ClearField(name string) error { + switch name { + case message.FieldSenderID: + m.ClearSenderID() + return nil + case message.FieldRecipientID: + m.ClearRecipientID() + return nil + case message.FieldAgentID: + m.ClearAgentID() + return nil + case message.FieldGroupID: + m.ClearGroupID() + return nil + case message.FieldDispatchedAt: + m.ClearDispatchedAt() + return nil + } + return fmt.Errorf("unknown Message nullable field %s", name) +} + +// ResetField resets all changes in the mutation for the field with the given name. +// It returns an error if the field is not defined in the schema. +func (m *MessageMutation) ResetField(name string) error { + switch name { + case message.FieldProjectID: + m.ResetProjectID() + return nil + case message.FieldSender: + m.ResetSender() + return nil + case message.FieldSenderID: + m.ResetSenderID() + return nil + case message.FieldRecipient: + m.ResetRecipient() + return nil + case message.FieldRecipientID: + m.ResetRecipientID() + return nil + case message.FieldMsg: + m.ResetMsg() + return nil + case message.FieldType: + m.ResetType() + return nil + case message.FieldUrgent: + m.ResetUrgent() + return nil + case message.FieldBroadcasted: + m.ResetBroadcasted() + return nil + case message.FieldRead: + m.ResetRead() + return nil + case message.FieldAgentID: + m.ResetAgentID() + return nil + case message.FieldGroupID: + m.ResetGroupID() + return nil + case message.FieldDispatchState: + m.ResetDispatchState() + return nil + case message.FieldDispatchedAt: + m.ResetDispatchedAt() + return nil + case message.FieldCreated: + m.ResetCreated() + return nil + } + return fmt.Errorf("unknown Message field %s", name) +} + +// AddedEdges returns all edge names that were set/added in this mutation. +func (m *MessageMutation) AddedEdges() []string { + edges := make([]string, 0, 0) + return edges +} + +// AddedIDs returns all IDs (to other nodes) that were added for the given edge +// name in this mutation. +func (m *MessageMutation) AddedIDs(name string) []ent.Value { + return nil +} + +// RemovedEdges returns all edge names that were removed in this mutation. +func (m *MessageMutation) RemovedEdges() []string { + edges := make([]string, 0, 0) + return edges +} + +// RemovedIDs returns all IDs (to other nodes) that were removed for the edge with +// the given name in this mutation. +func (m *MessageMutation) RemovedIDs(name string) []ent.Value { + return nil +} + +// ClearedEdges returns all edge names that were cleared in this mutation. +func (m *MessageMutation) ClearedEdges() []string { + edges := make([]string, 0, 0) + return edges +} + +// EdgeCleared returns a boolean which indicates if the edge with the given name +// was cleared in this mutation. +func (m *MessageMutation) EdgeCleared(name string) bool { + return false +} + +// ClearEdge clears the value of the edge with the given name. It returns an error +// if that edge is not defined in the schema. +func (m *MessageMutation) ClearEdge(name string) error { + return fmt.Errorf("unknown Message unique edge %s", name) +} + +// ResetEdge resets all changes to the edge with the given name in this mutation. +// It returns an error if the edge is not defined in the schema. +func (m *MessageMutation) ResetEdge(name string) error { + return fmt.Errorf("unknown Message edge %s", name) +} + +// NotificationMutation represents an operation that mutates the Notification nodes in the graph. +type NotificationMutation struct { + config + op Op + typ string + id *uuid.UUID + subscription_id *uuid.UUID + agent_id *uuid.UUID + project_id *uuid.UUID + subscriber_type *string + subscriber_id *string + status *string + message *string + dispatched *bool + acknowledged *bool + created *time.Time + clearedFields map[string]struct{} + done bool + oldValue func(context.Context) (*Notification, error) + predicates []predicate.Notification +} + +var _ ent.Mutation = (*NotificationMutation)(nil) + +// notificationOption allows management of the mutation configuration using functional options. +type notificationOption func(*NotificationMutation) + +// newNotificationMutation creates new mutation for the Notification entity. +func newNotificationMutation(c config, op Op, opts ...notificationOption) *NotificationMutation { + m := &NotificationMutation{ + config: c, + op: op, + typ: TypeNotification, + clearedFields: make(map[string]struct{}), + } + for _, opt := range opts { + opt(m) + } + return m +} + +// withNotificationID sets the ID field of the mutation. +func withNotificationID(id uuid.UUID) notificationOption { + return func(m *NotificationMutation) { + var ( + err error + once sync.Once + value *Notification + ) + m.oldValue = func(ctx context.Context) (*Notification, error) { + once.Do(func() { + if m.done { + err = errors.New("querying old values post mutation is not allowed") + } else { + value, err = m.Client().Notification.Get(ctx, id) + } + }) + return value, err + } + m.id = &id + } +} + +// withNotification sets the old Notification of the mutation. +func withNotification(node *Notification) notificationOption { + return func(m *NotificationMutation) { + m.oldValue = func(context.Context) (*Notification, error) { + return node, nil + } + m.id = &node.ID + } +} + +// Client returns a new `ent.Client` from the mutation. If the mutation was +// executed in a transaction (ent.Tx), a transactional client is returned. +func (m NotificationMutation) Client() *Client { + client := &Client{config: m.config} + client.init() + return client +} + +// Tx returns an `ent.Tx` for mutations that were executed in transactions; +// it returns an error otherwise. +func (m NotificationMutation) Tx() (*Tx, error) { + if _, ok := m.driver.(*txDriver); !ok { + return nil, errors.New("ent: mutation is not running in a transaction") + } + tx := &Tx{config: m.config} + tx.init() + return tx, nil +} + +// SetID sets the value of the id field. Note that this +// operation is only accepted on creation of Notification entities. +func (m *NotificationMutation) SetID(id uuid.UUID) { + m.id = &id +} + +// ID returns the ID value in the mutation. Note that the ID is only available +// if it was provided to the builder or after it was returned from the database. +func (m *NotificationMutation) ID() (id uuid.UUID, exists bool) { + if m.id == nil { + return + } + return *m.id, true +} + +// IDs queries the database and returns the entity ids that match the mutation's predicate. +// That means, if the mutation is applied within a transaction with an isolation level such +// as sql.LevelSerializable, the returned ids match the ids of the rows that will be updated +// or updated by the mutation. +func (m *NotificationMutation) IDs(ctx context.Context) ([]uuid.UUID, error) { + switch { + case m.op.Is(OpUpdateOne | OpDeleteOne): + id, exists := m.ID() + if exists { + return []uuid.UUID{id}, nil + } + fallthrough + case m.op.Is(OpUpdate | OpDelete): + return m.Client().Notification.Query().Where(m.predicates...).IDs(ctx) + default: + return nil, fmt.Errorf("IDs is not allowed on %s operations", m.op) + } +} + +// SetSubscriptionID sets the "subscription_id" field. +func (m *NotificationMutation) SetSubscriptionID(u uuid.UUID) { + m.subscription_id = &u +} + +// SubscriptionID returns the value of the "subscription_id" field in the mutation. +func (m *NotificationMutation) SubscriptionID() (r uuid.UUID, exists bool) { + v := m.subscription_id + if v == nil { + return + } + return *v, true +} + +// OldSubscriptionID returns the old "subscription_id" field's value of the Notification entity. +// If the Notification object wasn't provided to the builder, the object is fetched from the database. +// An error is returned if the mutation operation is not UpdateOne, or the database query fails. +func (m *NotificationMutation) OldSubscriptionID(ctx context.Context) (v uuid.UUID, err error) { + if !m.op.Is(OpUpdateOne) { + return v, errors.New("OldSubscriptionID is only allowed on UpdateOne operations") + } + if m.id == nil || m.oldValue == nil { + return v, errors.New("OldSubscriptionID requires an ID field in the mutation") + } + oldValue, err := m.oldValue(ctx) + if err != nil { + return v, fmt.Errorf("querying old value for OldSubscriptionID: %w", err) + } + return oldValue.SubscriptionID, nil +} + +// ResetSubscriptionID resets all changes to the "subscription_id" field. +func (m *NotificationMutation) ResetSubscriptionID() { + m.subscription_id = nil +} + +// SetAgentID sets the "agent_id" field. +func (m *NotificationMutation) SetAgentID(u uuid.UUID) { + m.agent_id = &u +} + +// AgentID returns the value of the "agent_id" field in the mutation. +func (m *NotificationMutation) AgentID() (r uuid.UUID, exists bool) { + v := m.agent_id + if v == nil { + return + } + return *v, true +} + +// OldAgentID returns the old "agent_id" field's value of the Notification entity. +// If the Notification object wasn't provided to the builder, the object is fetched from the database. +// An error is returned if the mutation operation is not UpdateOne, or the database query fails. +func (m *NotificationMutation) OldAgentID(ctx context.Context) (v uuid.UUID, err error) { + if !m.op.Is(OpUpdateOne) { + return v, errors.New("OldAgentID is only allowed on UpdateOne operations") + } + if m.id == nil || m.oldValue == nil { + return v, errors.New("OldAgentID requires an ID field in the mutation") + } + oldValue, err := m.oldValue(ctx) + if err != nil { + return v, fmt.Errorf("querying old value for OldAgentID: %w", err) + } + return oldValue.AgentID, nil +} + +// ResetAgentID resets all changes to the "agent_id" field. +func (m *NotificationMutation) ResetAgentID() { + m.agent_id = nil +} + +// SetProjectID sets the "project_id" field. +func (m *NotificationMutation) SetProjectID(u uuid.UUID) { + m.project_id = &u +} + +// ProjectID returns the value of the "project_id" field in the mutation. +func (m *NotificationMutation) ProjectID() (r uuid.UUID, exists bool) { + v := m.project_id + if v == nil { + return + } + return *v, true +} + +// OldProjectID returns the old "project_id" field's value of the Notification entity. +// If the Notification object wasn't provided to the builder, the object is fetched from the database. +// An error is returned if the mutation operation is not UpdateOne, or the database query fails. +func (m *NotificationMutation) OldProjectID(ctx context.Context) (v uuid.UUID, err error) { + if !m.op.Is(OpUpdateOne) { + return v, errors.New("OldProjectID is only allowed on UpdateOne operations") + } + if m.id == nil || m.oldValue == nil { + return v, errors.New("OldProjectID requires an ID field in the mutation") + } + oldValue, err := m.oldValue(ctx) + if err != nil { + return v, fmt.Errorf("querying old value for OldProjectID: %w", err) + } + return oldValue.ProjectID, nil +} + +// ResetProjectID resets all changes to the "project_id" field. +func (m *NotificationMutation) ResetProjectID() { + m.project_id = nil +} + +// SetSubscriberType sets the "subscriber_type" field. +func (m *NotificationMutation) SetSubscriberType(s string) { + m.subscriber_type = &s +} + +// SubscriberType returns the value of the "subscriber_type" field in the mutation. +func (m *NotificationMutation) SubscriberType() (r string, exists bool) { + v := m.subscriber_type + if v == nil { + return + } + return *v, true +} + +// OldSubscriberType returns the old "subscriber_type" field's value of the Notification entity. +// If the Notification object wasn't provided to the builder, the object is fetched from the database. +// An error is returned if the mutation operation is not UpdateOne, or the database query fails. +func (m *NotificationMutation) OldSubscriberType(ctx context.Context) (v string, err error) { + if !m.op.Is(OpUpdateOne) { + return v, errors.New("OldSubscriberType is only allowed on UpdateOne operations") + } + if m.id == nil || m.oldValue == nil { + return v, errors.New("OldSubscriberType requires an ID field in the mutation") + } + oldValue, err := m.oldValue(ctx) + if err != nil { + return v, fmt.Errorf("querying old value for OldSubscriberType: %w", err) + } + return oldValue.SubscriberType, nil +} + +// ResetSubscriberType resets all changes to the "subscriber_type" field. +func (m *NotificationMutation) ResetSubscriberType() { + m.subscriber_type = nil +} + +// SetSubscriberID sets the "subscriber_id" field. +func (m *NotificationMutation) SetSubscriberID(s string) { + m.subscriber_id = &s +} + +// SubscriberID returns the value of the "subscriber_id" field in the mutation. +func (m *NotificationMutation) SubscriberID() (r string, exists bool) { + v := m.subscriber_id + if v == nil { + return + } + return *v, true +} + +// OldSubscriberID returns the old "subscriber_id" field's value of the Notification entity. +// If the Notification object wasn't provided to the builder, the object is fetched from the database. +// An error is returned if the mutation operation is not UpdateOne, or the database query fails. +func (m *NotificationMutation) OldSubscriberID(ctx context.Context) (v string, err error) { + if !m.op.Is(OpUpdateOne) { + return v, errors.New("OldSubscriberID is only allowed on UpdateOne operations") + } + if m.id == nil || m.oldValue == nil { + return v, errors.New("OldSubscriberID requires an ID field in the mutation") + } + oldValue, err := m.oldValue(ctx) + if err != nil { + return v, fmt.Errorf("querying old value for OldSubscriberID: %w", err) + } + return oldValue.SubscriberID, nil +} + +// ResetSubscriberID resets all changes to the "subscriber_id" field. +func (m *NotificationMutation) ResetSubscriberID() { + m.subscriber_id = nil +} + +// SetStatus sets the "status" field. +func (m *NotificationMutation) SetStatus(s string) { + m.status = &s +} + +// Status returns the value of the "status" field in the mutation. +func (m *NotificationMutation) Status() (r string, exists bool) { + v := m.status + if v == nil { + return + } + return *v, true +} + +// OldStatus returns the old "status" field's value of the Notification entity. +// If the Notification object wasn't provided to the builder, the object is fetched from the database. +// An error is returned if the mutation operation is not UpdateOne, or the database query fails. +func (m *NotificationMutation) OldStatus(ctx context.Context) (v string, err error) { + if !m.op.Is(OpUpdateOne) { + return v, errors.New("OldStatus is only allowed on UpdateOne operations") + } + if m.id == nil || m.oldValue == nil { + return v, errors.New("OldStatus requires an ID field in the mutation") + } + oldValue, err := m.oldValue(ctx) + if err != nil { + return v, fmt.Errorf("querying old value for OldStatus: %w", err) + } + return oldValue.Status, nil +} + +// ResetStatus resets all changes to the "status" field. +func (m *NotificationMutation) ResetStatus() { + m.status = nil +} + +// SetMessage sets the "message" field. +func (m *NotificationMutation) SetMessage(s string) { + m.message = &s +} + +// Message returns the value of the "message" field in the mutation. +func (m *NotificationMutation) Message() (r string, exists bool) { + v := m.message + if v == nil { + return + } + return *v, true +} + +// OldMessage returns the old "message" field's value of the Notification entity. +// If the Notification object wasn't provided to the builder, the object is fetched from the database. +// An error is returned if the mutation operation is not UpdateOne, or the database query fails. +func (m *NotificationMutation) OldMessage(ctx context.Context) (v string, err error) { + if !m.op.Is(OpUpdateOne) { + return v, errors.New("OldMessage is only allowed on UpdateOne operations") + } + if m.id == nil || m.oldValue == nil { + return v, errors.New("OldMessage requires an ID field in the mutation") + } + oldValue, err := m.oldValue(ctx) + if err != nil { + return v, fmt.Errorf("querying old value for OldMessage: %w", err) + } + return oldValue.Message, nil +} + +// ResetMessage resets all changes to the "message" field. +func (m *NotificationMutation) ResetMessage() { + m.message = nil +} + +// SetDispatched sets the "dispatched" field. +func (m *NotificationMutation) SetDispatched(b bool) { + m.dispatched = &b +} + +// Dispatched returns the value of the "dispatched" field in the mutation. +func (m *NotificationMutation) Dispatched() (r bool, exists bool) { + v := m.dispatched + if v == nil { + return + } + return *v, true +} + +// OldDispatched returns the old "dispatched" field's value of the Notification entity. +// If the Notification object wasn't provided to the builder, the object is fetched from the database. +// An error is returned if the mutation operation is not UpdateOne, or the database query fails. +func (m *NotificationMutation) OldDispatched(ctx context.Context) (v bool, err error) { + if !m.op.Is(OpUpdateOne) { + return v, errors.New("OldDispatched is only allowed on UpdateOne operations") + } + if m.id == nil || m.oldValue == nil { + return v, errors.New("OldDispatched requires an ID field in the mutation") + } + oldValue, err := m.oldValue(ctx) + if err != nil { + return v, fmt.Errorf("querying old value for OldDispatched: %w", err) + } + return oldValue.Dispatched, nil +} + +// ResetDispatched resets all changes to the "dispatched" field. +func (m *NotificationMutation) ResetDispatched() { + m.dispatched = nil +} + +// SetAcknowledged sets the "acknowledged" field. +func (m *NotificationMutation) SetAcknowledged(b bool) { + m.acknowledged = &b +} + +// Acknowledged returns the value of the "acknowledged" field in the mutation. +func (m *NotificationMutation) Acknowledged() (r bool, exists bool) { + v := m.acknowledged + if v == nil { + return + } + return *v, true +} + +// OldAcknowledged returns the old "acknowledged" field's value of the Notification entity. +// If the Notification object wasn't provided to the builder, the object is fetched from the database. +// An error is returned if the mutation operation is not UpdateOne, or the database query fails. +func (m *NotificationMutation) OldAcknowledged(ctx context.Context) (v bool, err error) { + if !m.op.Is(OpUpdateOne) { + return v, errors.New("OldAcknowledged is only allowed on UpdateOne operations") + } + if m.id == nil || m.oldValue == nil { + return v, errors.New("OldAcknowledged requires an ID field in the mutation") + } + oldValue, err := m.oldValue(ctx) + if err != nil { + return v, fmt.Errorf("querying old value for OldAcknowledged: %w", err) + } + return oldValue.Acknowledged, nil +} + +// ResetAcknowledged resets all changes to the "acknowledged" field. +func (m *NotificationMutation) ResetAcknowledged() { + m.acknowledged = nil +} + +// SetCreated sets the "created" field. +func (m *NotificationMutation) SetCreated(t time.Time) { + m.created = &t +} + +// Created returns the value of the "created" field in the mutation. +func (m *NotificationMutation) Created() (r time.Time, exists bool) { + v := m.created + if v == nil { + return + } + return *v, true +} + +// OldCreated returns the old "created" field's value of the Notification entity. +// If the Notification object wasn't provided to the builder, the object is fetched from the database. +// An error is returned if the mutation operation is not UpdateOne, or the database query fails. +func (m *NotificationMutation) OldCreated(ctx context.Context) (v time.Time, err error) { + if !m.op.Is(OpUpdateOne) { + return v, errors.New("OldCreated is only allowed on UpdateOne operations") + } + if m.id == nil || m.oldValue == nil { + return v, errors.New("OldCreated requires an ID field in the mutation") + } + oldValue, err := m.oldValue(ctx) + if err != nil { + return v, fmt.Errorf("querying old value for OldCreated: %w", err) + } + return oldValue.Created, nil +} + +// ResetCreated resets all changes to the "created" field. +func (m *NotificationMutation) ResetCreated() { + m.created = nil +} + +// Where appends a list predicates to the NotificationMutation builder. +func (m *NotificationMutation) Where(ps ...predicate.Notification) { + m.predicates = append(m.predicates, ps...) +} + +// WhereP appends storage-level predicates to the NotificationMutation builder. Using this method, +// users can use type-assertion to append predicates that do not depend on any generated package. +func (m *NotificationMutation) WhereP(ps ...func(*sql.Selector)) { + p := make([]predicate.Notification, len(ps)) + for i := range ps { + p[i] = ps[i] + } + m.Where(p...) +} + +// Op returns the operation name. +func (m *NotificationMutation) Op() Op { + return m.op +} + +// SetOp allows setting the mutation operation. +func (m *NotificationMutation) SetOp(op Op) { + m.op = op +} + +// Type returns the node type of this mutation (Notification). +func (m *NotificationMutation) Type() string { + return m.typ +} + +// Fields returns all fields that were changed during this mutation. Note that in +// order to get all numeric fields that were incremented/decremented, call +// AddedFields(). +func (m *NotificationMutation) Fields() []string { + fields := make([]string, 0, 10) + if m.subscription_id != nil { + fields = append(fields, notification.FieldSubscriptionID) + } + if m.agent_id != nil { + fields = append(fields, notification.FieldAgentID) + } + if m.project_id != nil { + fields = append(fields, notification.FieldProjectID) + } + if m.subscriber_type != nil { + fields = append(fields, notification.FieldSubscriberType) + } + if m.subscriber_id != nil { + fields = append(fields, notification.FieldSubscriberID) + } + if m.status != nil { + fields = append(fields, notification.FieldStatus) + } + if m.message != nil { + fields = append(fields, notification.FieldMessage) + } + if m.dispatched != nil { + fields = append(fields, notification.FieldDispatched) + } + if m.acknowledged != nil { + fields = append(fields, notification.FieldAcknowledged) + } + if m.created != nil { + fields = append(fields, notification.FieldCreated) + } + return fields +} + +// Field returns the value of a field with the given name. The second boolean +// return value indicates that this field was not set, or was not defined in the +// schema. +func (m *NotificationMutation) Field(name string) (ent.Value, bool) { + switch name { + case notification.FieldSubscriptionID: + return m.SubscriptionID() + case notification.FieldAgentID: + return m.AgentID() + case notification.FieldProjectID: + return m.ProjectID() + case notification.FieldSubscriberType: + return m.SubscriberType() + case notification.FieldSubscriberID: + return m.SubscriberID() + case notification.FieldStatus: + return m.Status() + case notification.FieldMessage: + return m.Message() + case notification.FieldDispatched: + return m.Dispatched() + case notification.FieldAcknowledged: + return m.Acknowledged() + case notification.FieldCreated: + return m.Created() + } + return nil, false +} + +// OldField returns the old value of the field from the database. An error is +// returned if the mutation operation is not UpdateOne, or the query to the +// database failed. +func (m *NotificationMutation) OldField(ctx context.Context, name string) (ent.Value, error) { + switch name { + case notification.FieldSubscriptionID: + return m.OldSubscriptionID(ctx) + case notification.FieldAgentID: + return m.OldAgentID(ctx) + case notification.FieldProjectID: + return m.OldProjectID(ctx) + case notification.FieldSubscriberType: + return m.OldSubscriberType(ctx) + case notification.FieldSubscriberID: + return m.OldSubscriberID(ctx) + case notification.FieldStatus: + return m.OldStatus(ctx) + case notification.FieldMessage: + return m.OldMessage(ctx) + case notification.FieldDispatched: + return m.OldDispatched(ctx) + case notification.FieldAcknowledged: + return m.OldAcknowledged(ctx) + case notification.FieldCreated: + return m.OldCreated(ctx) + } + return nil, fmt.Errorf("unknown Notification field %s", name) +} + +// SetField sets the value of a field with the given name. It returns an error if +// the field is not defined in the schema, or if the type mismatched the field +// type. +func (m *NotificationMutation) SetField(name string, value ent.Value) error { + switch name { + case notification.FieldSubscriptionID: + v, ok := value.(uuid.UUID) + if !ok { + return fmt.Errorf("unexpected type %T for field %s", value, name) + } + m.SetSubscriptionID(v) + return nil + case notification.FieldAgentID: + v, ok := value.(uuid.UUID) + if !ok { + return fmt.Errorf("unexpected type %T for field %s", value, name) + } + m.SetAgentID(v) + return nil + case notification.FieldProjectID: + v, ok := value.(uuid.UUID) + if !ok { + return fmt.Errorf("unexpected type %T for field %s", value, name) + } + m.SetProjectID(v) + return nil + case notification.FieldSubscriberType: + v, ok := value.(string) + if !ok { + return fmt.Errorf("unexpected type %T for field %s", value, name) + } + m.SetSubscriberType(v) + return nil + case notification.FieldSubscriberID: + v, ok := value.(string) + if !ok { + return fmt.Errorf("unexpected type %T for field %s", value, name) + } + m.SetSubscriberID(v) + return nil + case notification.FieldStatus: + v, ok := value.(string) + if !ok { + return fmt.Errorf("unexpected type %T for field %s", value, name) + } + m.SetStatus(v) + return nil + case notification.FieldMessage: + v, ok := value.(string) + if !ok { + return fmt.Errorf("unexpected type %T for field %s", value, name) + } + m.SetMessage(v) + return nil + case notification.FieldDispatched: + v, ok := value.(bool) + if !ok { + return fmt.Errorf("unexpected type %T for field %s", value, name) + } + m.SetDispatched(v) + return nil + case notification.FieldAcknowledged: + v, ok := value.(bool) + if !ok { + return fmt.Errorf("unexpected type %T for field %s", value, name) + } + m.SetAcknowledged(v) + return nil + case notification.FieldCreated: + v, ok := value.(time.Time) + if !ok { + return fmt.Errorf("unexpected type %T for field %s", value, name) + } + m.SetCreated(v) + return nil + } + return fmt.Errorf("unknown Notification field %s", name) +} + +// AddedFields returns all numeric fields that were incremented/decremented during +// this mutation. +func (m *NotificationMutation) AddedFields() []string { + return nil +} + +// AddedField returns the numeric value that was incremented/decremented on a field +// with the given name. The second boolean return value indicates that this field +// was not set, or was not defined in the schema. +func (m *NotificationMutation) AddedField(name string) (ent.Value, bool) { + return nil, false +} + +// AddField adds the value to the field with the given name. It returns an error if +// the field is not defined in the schema, or if the type mismatched the field +// type. +func (m *NotificationMutation) AddField(name string, value ent.Value) error { + switch name { + } + return fmt.Errorf("unknown Notification numeric field %s", name) +} + +// ClearedFields returns all nullable fields that were cleared during this +// mutation. +func (m *NotificationMutation) ClearedFields() []string { + return nil +} + +// FieldCleared returns a boolean indicating if a field with the given name was +// cleared in this mutation. +func (m *NotificationMutation) FieldCleared(name string) bool { + _, ok := m.clearedFields[name] + return ok +} + +// ClearField clears the value of the field with the given name. It returns an +// error if the field is not defined in the schema. +func (m *NotificationMutation) ClearField(name string) error { + return fmt.Errorf("unknown Notification nullable field %s", name) +} + +// ResetField resets all changes in the mutation for the field with the given name. +// It returns an error if the field is not defined in the schema. +func (m *NotificationMutation) ResetField(name string) error { + switch name { + case notification.FieldSubscriptionID: + m.ResetSubscriptionID() + return nil + case notification.FieldAgentID: + m.ResetAgentID() + return nil + case notification.FieldProjectID: + m.ResetProjectID() + return nil + case notification.FieldSubscriberType: + m.ResetSubscriberType() + return nil + case notification.FieldSubscriberID: + m.ResetSubscriberID() + return nil + case notification.FieldStatus: + m.ResetStatus() + return nil + case notification.FieldMessage: + m.ResetMessage() + return nil + case notification.FieldDispatched: + m.ResetDispatched() + return nil + case notification.FieldAcknowledged: + m.ResetAcknowledged() + return nil + case notification.FieldCreated: + m.ResetCreated() + return nil + } + return fmt.Errorf("unknown Notification field %s", name) +} + +// AddedEdges returns all edge names that were set/added in this mutation. +func (m *NotificationMutation) AddedEdges() []string { + edges := make([]string, 0, 0) + return edges +} + +// AddedIDs returns all IDs (to other nodes) that were added for the given edge +// name in this mutation. +func (m *NotificationMutation) AddedIDs(name string) []ent.Value { + return nil +} + +// RemovedEdges returns all edge names that were removed in this mutation. +func (m *NotificationMutation) RemovedEdges() []string { + edges := make([]string, 0, 0) + return edges +} + +// RemovedIDs returns all IDs (to other nodes) that were removed for the edge with +// the given name in this mutation. +func (m *NotificationMutation) RemovedIDs(name string) []ent.Value { + return nil +} + +// ClearedEdges returns all edge names that were cleared in this mutation. +func (m *NotificationMutation) ClearedEdges() []string { + edges := make([]string, 0, 0) + return edges +} + +// EdgeCleared returns a boolean which indicates if the edge with the given name +// was cleared in this mutation. +func (m *NotificationMutation) EdgeCleared(name string) bool { + return false +} + +// ClearEdge clears the value of the edge with the given name. It returns an error +// if that edge is not defined in the schema. +func (m *NotificationMutation) ClearEdge(name string) error { + return fmt.Errorf("unknown Notification unique edge %s", name) +} + +// ResetEdge resets all changes to the edge with the given name in this mutation. +// It returns an error if the edge is not defined in the schema. +func (m *NotificationMutation) ResetEdge(name string) error { + return fmt.Errorf("unknown Notification edge %s", name) +} + +// NotificationSubscriptionMutation represents an operation that mutates the NotificationSubscription nodes in the graph. +type NotificationSubscriptionMutation struct { + config + op Op + typ string + id *uuid.UUID + scope *string + agent_id *uuid.UUID + subscriber_type *string + subscriber_id *string + project_id *uuid.UUID + trigger_activities *string + created_by *string + created *time.Time + clearedFields map[string]struct{} + done bool + oldValue func(context.Context) (*NotificationSubscription, error) + predicates []predicate.NotificationSubscription +} + +var _ ent.Mutation = (*NotificationSubscriptionMutation)(nil) + +// notificationsubscriptionOption allows management of the mutation configuration using functional options. +type notificationsubscriptionOption func(*NotificationSubscriptionMutation) + +// newNotificationSubscriptionMutation creates new mutation for the NotificationSubscription entity. +func newNotificationSubscriptionMutation(c config, op Op, opts ...notificationsubscriptionOption) *NotificationSubscriptionMutation { + m := &NotificationSubscriptionMutation{ + config: c, + op: op, + typ: TypeNotificationSubscription, + clearedFields: make(map[string]struct{}), + } + for _, opt := range opts { + opt(m) + } + return m +} + +// withNotificationSubscriptionID sets the ID field of the mutation. +func withNotificationSubscriptionID(id uuid.UUID) notificationsubscriptionOption { + return func(m *NotificationSubscriptionMutation) { + var ( + err error + once sync.Once + value *NotificationSubscription + ) + m.oldValue = func(ctx context.Context) (*NotificationSubscription, error) { + once.Do(func() { + if m.done { + err = errors.New("querying old values post mutation is not allowed") + } else { + value, err = m.Client().NotificationSubscription.Get(ctx, id) + } + }) + return value, err + } + m.id = &id + } +} + +// withNotificationSubscription sets the old NotificationSubscription of the mutation. +func withNotificationSubscription(node *NotificationSubscription) notificationsubscriptionOption { + return func(m *NotificationSubscriptionMutation) { + m.oldValue = func(context.Context) (*NotificationSubscription, error) { + return node, nil + } + m.id = &node.ID + } +} + +// Client returns a new `ent.Client` from the mutation. If the mutation was +// executed in a transaction (ent.Tx), a transactional client is returned. +func (m NotificationSubscriptionMutation) Client() *Client { + client := &Client{config: m.config} + client.init() + return client +} + +// Tx returns an `ent.Tx` for mutations that were executed in transactions; +// it returns an error otherwise. +func (m NotificationSubscriptionMutation) Tx() (*Tx, error) { + if _, ok := m.driver.(*txDriver); !ok { + return nil, errors.New("ent: mutation is not running in a transaction") + } + tx := &Tx{config: m.config} + tx.init() + return tx, nil +} + +// SetID sets the value of the id field. Note that this +// operation is only accepted on creation of NotificationSubscription entities. +func (m *NotificationSubscriptionMutation) SetID(id uuid.UUID) { + m.id = &id +} + +// ID returns the ID value in the mutation. Note that the ID is only available +// if it was provided to the builder or after it was returned from the database. +func (m *NotificationSubscriptionMutation) ID() (id uuid.UUID, exists bool) { + if m.id == nil { + return + } + return *m.id, true +} + +// IDs queries the database and returns the entity ids that match the mutation's predicate. +// That means, if the mutation is applied within a transaction with an isolation level such +// as sql.LevelSerializable, the returned ids match the ids of the rows that will be updated +// or updated by the mutation. +func (m *NotificationSubscriptionMutation) IDs(ctx context.Context) ([]uuid.UUID, error) { + switch { + case m.op.Is(OpUpdateOne | OpDeleteOne): + id, exists := m.ID() + if exists { + return []uuid.UUID{id}, nil + } + fallthrough + case m.op.Is(OpUpdate | OpDelete): + return m.Client().NotificationSubscription.Query().Where(m.predicates...).IDs(ctx) + default: + return nil, fmt.Errorf("IDs is not allowed on %s operations", m.op) + } +} + +// SetScope sets the "scope" field. +func (m *NotificationSubscriptionMutation) SetScope(s string) { + m.scope = &s +} + +// Scope returns the value of the "scope" field in the mutation. +func (m *NotificationSubscriptionMutation) Scope() (r string, exists bool) { + v := m.scope + if v == nil { + return + } + return *v, true +} + +// OldScope returns the old "scope" field's value of the NotificationSubscription entity. +// If the NotificationSubscription object wasn't provided to the builder, the object is fetched from the database. +// An error is returned if the mutation operation is not UpdateOne, or the database query fails. +func (m *NotificationSubscriptionMutation) OldScope(ctx context.Context) (v string, err error) { + if !m.op.Is(OpUpdateOne) { + return v, errors.New("OldScope is only allowed on UpdateOne operations") + } + if m.id == nil || m.oldValue == nil { + return v, errors.New("OldScope requires an ID field in the mutation") + } + oldValue, err := m.oldValue(ctx) + if err != nil { + return v, fmt.Errorf("querying old value for OldScope: %w", err) + } + return oldValue.Scope, nil +} + +// ResetScope resets all changes to the "scope" field. +func (m *NotificationSubscriptionMutation) ResetScope() { + m.scope = nil +} + +// SetAgentID sets the "agent_id" field. +func (m *NotificationSubscriptionMutation) SetAgentID(u uuid.UUID) { + m.agent_id = &u +} + +// AgentID returns the value of the "agent_id" field in the mutation. +func (m *NotificationSubscriptionMutation) AgentID() (r uuid.UUID, exists bool) { + v := m.agent_id + if v == nil { + return + } + return *v, true +} + +// OldAgentID returns the old "agent_id" field's value of the NotificationSubscription entity. +// If the NotificationSubscription object wasn't provided to the builder, the object is fetched from the database. +// An error is returned if the mutation operation is not UpdateOne, or the database query fails. +func (m *NotificationSubscriptionMutation) OldAgentID(ctx context.Context) (v *uuid.UUID, err error) { + if !m.op.Is(OpUpdateOne) { + return v, errors.New("OldAgentID is only allowed on UpdateOne operations") + } + if m.id == nil || m.oldValue == nil { + return v, errors.New("OldAgentID requires an ID field in the mutation") + } + oldValue, err := m.oldValue(ctx) + if err != nil { + return v, fmt.Errorf("querying old value for OldAgentID: %w", err) + } + return oldValue.AgentID, nil +} + +// ClearAgentID clears the value of the "agent_id" field. +func (m *NotificationSubscriptionMutation) ClearAgentID() { + m.agent_id = nil + m.clearedFields[notificationsubscription.FieldAgentID] = struct{}{} +} + +// AgentIDCleared returns if the "agent_id" field was cleared in this mutation. +func (m *NotificationSubscriptionMutation) AgentIDCleared() bool { + _, ok := m.clearedFields[notificationsubscription.FieldAgentID] + return ok +} + +// ResetAgentID resets all changes to the "agent_id" field. +func (m *NotificationSubscriptionMutation) ResetAgentID() { + m.agent_id = nil + delete(m.clearedFields, notificationsubscription.FieldAgentID) +} + +// SetSubscriberType sets the "subscriber_type" field. +func (m *NotificationSubscriptionMutation) SetSubscriberType(s string) { + m.subscriber_type = &s +} + +// SubscriberType returns the value of the "subscriber_type" field in the mutation. +func (m *NotificationSubscriptionMutation) SubscriberType() (r string, exists bool) { + v := m.subscriber_type + if v == nil { + return + } + return *v, true +} + +// OldSubscriberType returns the old "subscriber_type" field's value of the NotificationSubscription entity. +// If the NotificationSubscription object wasn't provided to the builder, the object is fetched from the database. +// An error is returned if the mutation operation is not UpdateOne, or the database query fails. +func (m *NotificationSubscriptionMutation) OldSubscriberType(ctx context.Context) (v string, err error) { + if !m.op.Is(OpUpdateOne) { + return v, errors.New("OldSubscriberType is only allowed on UpdateOne operations") + } + if m.id == nil || m.oldValue == nil { + return v, errors.New("OldSubscriberType requires an ID field in the mutation") + } + oldValue, err := m.oldValue(ctx) + if err != nil { + return v, fmt.Errorf("querying old value for OldSubscriberType: %w", err) + } + return oldValue.SubscriberType, nil +} + +// ResetSubscriberType resets all changes to the "subscriber_type" field. +func (m *NotificationSubscriptionMutation) ResetSubscriberType() { + m.subscriber_type = nil +} + +// SetSubscriberID sets the "subscriber_id" field. +func (m *NotificationSubscriptionMutation) SetSubscriberID(s string) { + m.subscriber_id = &s +} + +// SubscriberID returns the value of the "subscriber_id" field in the mutation. +func (m *NotificationSubscriptionMutation) SubscriberID() (r string, exists bool) { + v := m.subscriber_id + if v == nil { + return + } + return *v, true +} + +// OldSubscriberID returns the old "subscriber_id" field's value of the NotificationSubscription entity. +// If the NotificationSubscription object wasn't provided to the builder, the object is fetched from the database. +// An error is returned if the mutation operation is not UpdateOne, or the database query fails. +func (m *NotificationSubscriptionMutation) OldSubscriberID(ctx context.Context) (v string, err error) { + if !m.op.Is(OpUpdateOne) { + return v, errors.New("OldSubscriberID is only allowed on UpdateOne operations") + } + if m.id == nil || m.oldValue == nil { + return v, errors.New("OldSubscriberID requires an ID field in the mutation") + } + oldValue, err := m.oldValue(ctx) + if err != nil { + return v, fmt.Errorf("querying old value for OldSubscriberID: %w", err) + } + return oldValue.SubscriberID, nil +} + +// ResetSubscriberID resets all changes to the "subscriber_id" field. +func (m *NotificationSubscriptionMutation) ResetSubscriberID() { + m.subscriber_id = nil +} + +// SetProjectID sets the "project_id" field. +func (m *NotificationSubscriptionMutation) SetProjectID(u uuid.UUID) { + m.project_id = &u +} + +// ProjectID returns the value of the "project_id" field in the mutation. +func (m *NotificationSubscriptionMutation) ProjectID() (r uuid.UUID, exists bool) { + v := m.project_id + if v == nil { + return + } + return *v, true +} + +// OldProjectID returns the old "project_id" field's value of the NotificationSubscription entity. +// If the NotificationSubscription object wasn't provided to the builder, the object is fetched from the database. +// An error is returned if the mutation operation is not UpdateOne, or the database query fails. +func (m *NotificationSubscriptionMutation) OldProjectID(ctx context.Context) (v uuid.UUID, err error) { + if !m.op.Is(OpUpdateOne) { + return v, errors.New("OldProjectID is only allowed on UpdateOne operations") + } + if m.id == nil || m.oldValue == nil { + return v, errors.New("OldProjectID requires an ID field in the mutation") + } + oldValue, err := m.oldValue(ctx) + if err != nil { + return v, fmt.Errorf("querying old value for OldProjectID: %w", err) + } + return oldValue.ProjectID, nil +} + +// ResetProjectID resets all changes to the "project_id" field. +func (m *NotificationSubscriptionMutation) ResetProjectID() { + m.project_id = nil +} + +// SetTriggerActivities sets the "trigger_activities" field. +func (m *NotificationSubscriptionMutation) SetTriggerActivities(s string) { + m.trigger_activities = &s +} + +// TriggerActivities returns the value of the "trigger_activities" field in the mutation. +func (m *NotificationSubscriptionMutation) TriggerActivities() (r string, exists bool) { + v := m.trigger_activities + if v == nil { + return + } + return *v, true +} + +// OldTriggerActivities returns the old "trigger_activities" field's value of the NotificationSubscription entity. +// If the NotificationSubscription object wasn't provided to the builder, the object is fetched from the database. +// An error is returned if the mutation operation is not UpdateOne, or the database query fails. +func (m *NotificationSubscriptionMutation) OldTriggerActivities(ctx context.Context) (v string, err error) { + if !m.op.Is(OpUpdateOne) { + return v, errors.New("OldTriggerActivities is only allowed on UpdateOne operations") + } + if m.id == nil || m.oldValue == nil { + return v, errors.New("OldTriggerActivities requires an ID field in the mutation") + } + oldValue, err := m.oldValue(ctx) + if err != nil { + return v, fmt.Errorf("querying old value for OldTriggerActivities: %w", err) + } + return oldValue.TriggerActivities, nil +} + +// ResetTriggerActivities resets all changes to the "trigger_activities" field. +func (m *NotificationSubscriptionMutation) ResetTriggerActivities() { + m.trigger_activities = nil +} + +// SetCreatedBy sets the "created_by" field. +func (m *NotificationSubscriptionMutation) SetCreatedBy(s string) { + m.created_by = &s +} + +// CreatedBy returns the value of the "created_by" field in the mutation. +func (m *NotificationSubscriptionMutation) CreatedBy() (r string, exists bool) { + v := m.created_by + if v == nil { + return + } + return *v, true +} + +// OldCreatedBy returns the old "created_by" field's value of the NotificationSubscription entity. +// If the NotificationSubscription object wasn't provided to the builder, the object is fetched from the database. +// An error is returned if the mutation operation is not UpdateOne, or the database query fails. +func (m *NotificationSubscriptionMutation) OldCreatedBy(ctx context.Context) (v string, err error) { + if !m.op.Is(OpUpdateOne) { + return v, errors.New("OldCreatedBy is only allowed on UpdateOne operations") + } + if m.id == nil || m.oldValue == nil { + return v, errors.New("OldCreatedBy requires an ID field in the mutation") + } + oldValue, err := m.oldValue(ctx) + if err != nil { + return v, fmt.Errorf("querying old value for OldCreatedBy: %w", err) + } + return oldValue.CreatedBy, nil +} + +// ResetCreatedBy resets all changes to the "created_by" field. +func (m *NotificationSubscriptionMutation) ResetCreatedBy() { + m.created_by = nil +} + +// SetCreated sets the "created" field. +func (m *NotificationSubscriptionMutation) SetCreated(t time.Time) { + m.created = &t +} + +// Created returns the value of the "created" field in the mutation. +func (m *NotificationSubscriptionMutation) Created() (r time.Time, exists bool) { + v := m.created + if v == nil { + return + } + return *v, true +} + +// OldCreated returns the old "created" field's value of the NotificationSubscription entity. +// If the NotificationSubscription object wasn't provided to the builder, the object is fetched from the database. +// An error is returned if the mutation operation is not UpdateOne, or the database query fails. +func (m *NotificationSubscriptionMutation) OldCreated(ctx context.Context) (v time.Time, err error) { + if !m.op.Is(OpUpdateOne) { + return v, errors.New("OldCreated is only allowed on UpdateOne operations") + } + if m.id == nil || m.oldValue == nil { + return v, errors.New("OldCreated requires an ID field in the mutation") + } + oldValue, err := m.oldValue(ctx) + if err != nil { + return v, fmt.Errorf("querying old value for OldCreated: %w", err) + } + return oldValue.Created, nil +} + +// ResetCreated resets all changes to the "created" field. +func (m *NotificationSubscriptionMutation) ResetCreated() { + m.created = nil +} + +// Where appends a list predicates to the NotificationSubscriptionMutation builder. +func (m *NotificationSubscriptionMutation) Where(ps ...predicate.NotificationSubscription) { + m.predicates = append(m.predicates, ps...) +} + +// WhereP appends storage-level predicates to the NotificationSubscriptionMutation builder. Using this method, +// users can use type-assertion to append predicates that do not depend on any generated package. +func (m *NotificationSubscriptionMutation) WhereP(ps ...func(*sql.Selector)) { + p := make([]predicate.NotificationSubscription, len(ps)) + for i := range ps { + p[i] = ps[i] + } + m.Where(p...) +} + +// Op returns the operation name. +func (m *NotificationSubscriptionMutation) Op() Op { + return m.op +} + +// SetOp allows setting the mutation operation. +func (m *NotificationSubscriptionMutation) SetOp(op Op) { + m.op = op +} + +// Type returns the node type of this mutation (NotificationSubscription). +func (m *NotificationSubscriptionMutation) Type() string { + return m.typ +} + +// Fields returns all fields that were changed during this mutation. Note that in +// order to get all numeric fields that were incremented/decremented, call +// AddedFields(). +func (m *NotificationSubscriptionMutation) Fields() []string { + fields := make([]string, 0, 8) + if m.scope != nil { + fields = append(fields, notificationsubscription.FieldScope) + } + if m.agent_id != nil { + fields = append(fields, notificationsubscription.FieldAgentID) + } + if m.subscriber_type != nil { + fields = append(fields, notificationsubscription.FieldSubscriberType) + } + if m.subscriber_id != nil { + fields = append(fields, notificationsubscription.FieldSubscriberID) + } + if m.project_id != nil { + fields = append(fields, notificationsubscription.FieldProjectID) + } + if m.trigger_activities != nil { + fields = append(fields, notificationsubscription.FieldTriggerActivities) + } + if m.created_by != nil { + fields = append(fields, notificationsubscription.FieldCreatedBy) + } + if m.created != nil { + fields = append(fields, notificationsubscription.FieldCreated) + } + return fields +} + +// Field returns the value of a field with the given name. The second boolean +// return value indicates that this field was not set, or was not defined in the +// schema. +func (m *NotificationSubscriptionMutation) Field(name string) (ent.Value, bool) { + switch name { + case notificationsubscription.FieldScope: + return m.Scope() + case notificationsubscription.FieldAgentID: + return m.AgentID() + case notificationsubscription.FieldSubscriberType: + return m.SubscriberType() + case notificationsubscription.FieldSubscriberID: + return m.SubscriberID() + case notificationsubscription.FieldProjectID: + return m.ProjectID() + case notificationsubscription.FieldTriggerActivities: + return m.TriggerActivities() + case notificationsubscription.FieldCreatedBy: + return m.CreatedBy() + case notificationsubscription.FieldCreated: + return m.Created() + } + return nil, false +} + +// OldField returns the old value of the field from the database. An error is +// returned if the mutation operation is not UpdateOne, or the query to the +// database failed. +func (m *NotificationSubscriptionMutation) OldField(ctx context.Context, name string) (ent.Value, error) { + switch name { + case notificationsubscription.FieldScope: + return m.OldScope(ctx) + case notificationsubscription.FieldAgentID: + return m.OldAgentID(ctx) + case notificationsubscription.FieldSubscriberType: + return m.OldSubscriberType(ctx) + case notificationsubscription.FieldSubscriberID: + return m.OldSubscriberID(ctx) + case notificationsubscription.FieldProjectID: + return m.OldProjectID(ctx) + case notificationsubscription.FieldTriggerActivities: + return m.OldTriggerActivities(ctx) + case notificationsubscription.FieldCreatedBy: + return m.OldCreatedBy(ctx) + case notificationsubscription.FieldCreated: + return m.OldCreated(ctx) + } + return nil, fmt.Errorf("unknown NotificationSubscription field %s", name) +} + +// SetField sets the value of a field with the given name. It returns an error if +// the field is not defined in the schema, or if the type mismatched the field +// type. +func (m *NotificationSubscriptionMutation) SetField(name string, value ent.Value) error { + switch name { + case notificationsubscription.FieldScope: + v, ok := value.(string) + if !ok { + return fmt.Errorf("unexpected type %T for field %s", value, name) + } + m.SetScope(v) + return nil + case notificationsubscription.FieldAgentID: + v, ok := value.(uuid.UUID) + if !ok { + return fmt.Errorf("unexpected type %T for field %s", value, name) + } + m.SetAgentID(v) + return nil + case notificationsubscription.FieldSubscriberType: + v, ok := value.(string) + if !ok { + return fmt.Errorf("unexpected type %T for field %s", value, name) + } + m.SetSubscriberType(v) + return nil + case notificationsubscription.FieldSubscriberID: + v, ok := value.(string) + if !ok { + return fmt.Errorf("unexpected type %T for field %s", value, name) + } + m.SetSubscriberID(v) + return nil + case notificationsubscription.FieldProjectID: + v, ok := value.(uuid.UUID) + if !ok { + return fmt.Errorf("unexpected type %T for field %s", value, name) + } + m.SetProjectID(v) + return nil + case notificationsubscription.FieldTriggerActivities: + v, ok := value.(string) + if !ok { + return fmt.Errorf("unexpected type %T for field %s", value, name) + } + m.SetTriggerActivities(v) + return nil + case notificationsubscription.FieldCreatedBy: + v, ok := value.(string) + if !ok { + return fmt.Errorf("unexpected type %T for field %s", value, name) + } + m.SetCreatedBy(v) + return nil + case notificationsubscription.FieldCreated: + v, ok := value.(time.Time) + if !ok { + return fmt.Errorf("unexpected type %T for field %s", value, name) + } + m.SetCreated(v) + return nil + } + return fmt.Errorf("unknown NotificationSubscription field %s", name) +} + +// AddedFields returns all numeric fields that were incremented/decremented during +// this mutation. +func (m *NotificationSubscriptionMutation) AddedFields() []string { + return nil +} + +// AddedField returns the numeric value that was incremented/decremented on a field +// with the given name. The second boolean return value indicates that this field +// was not set, or was not defined in the schema. +func (m *NotificationSubscriptionMutation) AddedField(name string) (ent.Value, bool) { + return nil, false +} + +// AddField adds the value to the field with the given name. It returns an error if +// the field is not defined in the schema, or if the type mismatched the field +// type. +func (m *NotificationSubscriptionMutation) AddField(name string, value ent.Value) error { + switch name { + } + return fmt.Errorf("unknown NotificationSubscription numeric field %s", name) +} + +// ClearedFields returns all nullable fields that were cleared during this +// mutation. +func (m *NotificationSubscriptionMutation) ClearedFields() []string { + var fields []string + if m.FieldCleared(notificationsubscription.FieldAgentID) { + fields = append(fields, notificationsubscription.FieldAgentID) + } + return fields +} + +// FieldCleared returns a boolean indicating if a field with the given name was +// cleared in this mutation. +func (m *NotificationSubscriptionMutation) FieldCleared(name string) bool { + _, ok := m.clearedFields[name] + return ok +} + +// ClearField clears the value of the field with the given name. It returns an +// error if the field is not defined in the schema. +func (m *NotificationSubscriptionMutation) ClearField(name string) error { + switch name { + case notificationsubscription.FieldAgentID: + m.ClearAgentID() + return nil + } + return fmt.Errorf("unknown NotificationSubscription nullable field %s", name) +} + +// ResetField resets all changes in the mutation for the field with the given name. +// It returns an error if the field is not defined in the schema. +func (m *NotificationSubscriptionMutation) ResetField(name string) error { + switch name { + case notificationsubscription.FieldScope: + m.ResetScope() + return nil + case notificationsubscription.FieldAgentID: + m.ResetAgentID() + return nil + case notificationsubscription.FieldSubscriberType: + m.ResetSubscriberType() + return nil + case notificationsubscription.FieldSubscriberID: + m.ResetSubscriberID() + return nil + case notificationsubscription.FieldProjectID: + m.ResetProjectID() + return nil + case notificationsubscription.FieldTriggerActivities: + m.ResetTriggerActivities() + return nil + case notificationsubscription.FieldCreatedBy: + m.ResetCreatedBy() + return nil + case notificationsubscription.FieldCreated: + m.ResetCreated() + return nil + } + return fmt.Errorf("unknown NotificationSubscription field %s", name) +} + +// AddedEdges returns all edge names that were set/added in this mutation. +func (m *NotificationSubscriptionMutation) AddedEdges() []string { + edges := make([]string, 0, 0) + return edges +} + +// AddedIDs returns all IDs (to other nodes) that were added for the given edge +// name in this mutation. +func (m *NotificationSubscriptionMutation) AddedIDs(name string) []ent.Value { + return nil +} + +// RemovedEdges returns all edge names that were removed in this mutation. +func (m *NotificationSubscriptionMutation) RemovedEdges() []string { + edges := make([]string, 0, 0) + return edges +} + +// RemovedIDs returns all IDs (to other nodes) that were removed for the edge with +// the given name in this mutation. +func (m *NotificationSubscriptionMutation) RemovedIDs(name string) []ent.Value { + return nil +} + +// ClearedEdges returns all edge names that were cleared in this mutation. +func (m *NotificationSubscriptionMutation) ClearedEdges() []string { + edges := make([]string, 0, 0) + return edges +} + +// EdgeCleared returns a boolean which indicates if the edge with the given name +// was cleared in this mutation. +func (m *NotificationSubscriptionMutation) EdgeCleared(name string) bool { + return false +} + +// ClearEdge clears the value of the edge with the given name. It returns an error +// if that edge is not defined in the schema. +func (m *NotificationSubscriptionMutation) ClearEdge(name string) error { + return fmt.Errorf("unknown NotificationSubscription unique edge %s", name) +} + +// ResetEdge resets all changes to the edge with the given name in this mutation. +// It returns an error if the edge is not defined in the schema. +func (m *NotificationSubscriptionMutation) ResetEdge(name string) error { + return fmt.Errorf("unknown NotificationSubscription edge %s", name) +} + +// PolicyBindingMutation represents an operation that mutates the PolicyBinding nodes in the graph. +type PolicyBindingMutation struct { + config + op Op + typ string + id *uuid.UUID + principal_type *policybinding.PrincipalType + created *time.Time + created_by *string + clearedFields map[string]struct{} + policy *uuid.UUID + clearedpolicy bool + user *uuid.UUID + cleareduser bool + group *uuid.UUID + clearedgroup bool + agent *uuid.UUID + clearedagent bool + done bool + oldValue func(context.Context) (*PolicyBinding, error) + predicates []predicate.PolicyBinding +} + +var _ ent.Mutation = (*PolicyBindingMutation)(nil) + +// policybindingOption allows management of the mutation configuration using functional options. +type policybindingOption func(*PolicyBindingMutation) + +// newPolicyBindingMutation creates new mutation for the PolicyBinding entity. +func newPolicyBindingMutation(c config, op Op, opts ...policybindingOption) *PolicyBindingMutation { + m := &PolicyBindingMutation{ + config: c, + op: op, + typ: TypePolicyBinding, + clearedFields: make(map[string]struct{}), + } + for _, opt := range opts { + opt(m) + } + return m +} + +// withPolicyBindingID sets the ID field of the mutation. +func withPolicyBindingID(id uuid.UUID) policybindingOption { + return func(m *PolicyBindingMutation) { + var ( + err error + once sync.Once + value *PolicyBinding + ) + m.oldValue = func(ctx context.Context) (*PolicyBinding, error) { + once.Do(func() { + if m.done { + err = errors.New("querying old values post mutation is not allowed") + } else { + value, err = m.Client().PolicyBinding.Get(ctx, id) + } + }) + return value, err + } + m.id = &id + } +} + +// withPolicyBinding sets the old PolicyBinding of the mutation. +func withPolicyBinding(node *PolicyBinding) policybindingOption { + return func(m *PolicyBindingMutation) { + m.oldValue = func(context.Context) (*PolicyBinding, error) { + return node, nil + } + m.id = &node.ID + } +} + +// Client returns a new `ent.Client` from the mutation. If the mutation was +// executed in a transaction (ent.Tx), a transactional client is returned. +func (m PolicyBindingMutation) Client() *Client { + client := &Client{config: m.config} + client.init() + return client +} + +// Tx returns an `ent.Tx` for mutations that were executed in transactions; +// it returns an error otherwise. +func (m PolicyBindingMutation) Tx() (*Tx, error) { + if _, ok := m.driver.(*txDriver); !ok { + return nil, errors.New("ent: mutation is not running in a transaction") + } + tx := &Tx{config: m.config} + tx.init() + return tx, nil +} + +// SetID sets the value of the id field. Note that this +// operation is only accepted on creation of PolicyBinding entities. +func (m *PolicyBindingMutation) SetID(id uuid.UUID) { + m.id = &id +} + +// ID returns the ID value in the mutation. Note that the ID is only available +// if it was provided to the builder or after it was returned from the database. +func (m *PolicyBindingMutation) ID() (id uuid.UUID, exists bool) { + if m.id == nil { + return + } + return *m.id, true +} + +// IDs queries the database and returns the entity ids that match the mutation's predicate. +// That means, if the mutation is applied within a transaction with an isolation level such +// as sql.LevelSerializable, the returned ids match the ids of the rows that will be updated +// or updated by the mutation. +func (m *PolicyBindingMutation) IDs(ctx context.Context) ([]uuid.UUID, error) { + switch { + case m.op.Is(OpUpdateOne | OpDeleteOne): + id, exists := m.ID() + if exists { + return []uuid.UUID{id}, nil + } + fallthrough + case m.op.Is(OpUpdate | OpDelete): + return m.Client().PolicyBinding.Query().Where(m.predicates...).IDs(ctx) + default: + return nil, fmt.Errorf("IDs is not allowed on %s operations", m.op) + } +} + +// SetPrincipalType sets the "principal_type" field. +func (m *PolicyBindingMutation) SetPrincipalType(pt policybinding.PrincipalType) { + m.principal_type = &pt +} + +// PrincipalType returns the value of the "principal_type" field in the mutation. +func (m *PolicyBindingMutation) PrincipalType() (r policybinding.PrincipalType, exists bool) { + v := m.principal_type + if v == nil { + return + } + return *v, true +} + +// OldPrincipalType returns the old "principal_type" field's value of the PolicyBinding entity. +// If the PolicyBinding object wasn't provided to the builder, the object is fetched from the database. +// An error is returned if the mutation operation is not UpdateOne, or the database query fails. +func (m *PolicyBindingMutation) OldPrincipalType(ctx context.Context) (v policybinding.PrincipalType, err error) { + if !m.op.Is(OpUpdateOne) { + return v, errors.New("OldPrincipalType is only allowed on UpdateOne operations") + } + if m.id == nil || m.oldValue == nil { + return v, errors.New("OldPrincipalType requires an ID field in the mutation") + } + oldValue, err := m.oldValue(ctx) + if err != nil { + return v, fmt.Errorf("querying old value for OldPrincipalType: %w", err) + } + return oldValue.PrincipalType, nil +} + +// ResetPrincipalType resets all changes to the "principal_type" field. +func (m *PolicyBindingMutation) ResetPrincipalType() { + m.principal_type = nil +} + +// SetCreated sets the "created" field. +func (m *PolicyBindingMutation) SetCreated(t time.Time) { + m.created = &t +} + +// Created returns the value of the "created" field in the mutation. +func (m *PolicyBindingMutation) Created() (r time.Time, exists bool) { + v := m.created + if v == nil { + return + } + return *v, true +} + +// OldCreated returns the old "created" field's value of the PolicyBinding entity. +// If the PolicyBinding object wasn't provided to the builder, the object is fetched from the database. +// An error is returned if the mutation operation is not UpdateOne, or the database query fails. +func (m *PolicyBindingMutation) OldCreated(ctx context.Context) (v time.Time, err error) { + if !m.op.Is(OpUpdateOne) { + return v, errors.New("OldCreated is only allowed on UpdateOne operations") + } + if m.id == nil || m.oldValue == nil { + return v, errors.New("OldCreated requires an ID field in the mutation") + } + oldValue, err := m.oldValue(ctx) + if err != nil { + return v, fmt.Errorf("querying old value for OldCreated: %w", err) + } + return oldValue.Created, nil +} + +// ResetCreated resets all changes to the "created" field. +func (m *PolicyBindingMutation) ResetCreated() { + m.created = nil +} + +// SetCreatedBy sets the "created_by" field. +func (m *PolicyBindingMutation) SetCreatedBy(s string) { + m.created_by = &s +} + +// CreatedBy returns the value of the "created_by" field in the mutation. +func (m *PolicyBindingMutation) CreatedBy() (r string, exists bool) { + v := m.created_by + if v == nil { + return + } + return *v, true +} + +// OldCreatedBy returns the old "created_by" field's value of the PolicyBinding entity. +// If the PolicyBinding object wasn't provided to the builder, the object is fetched from the database. +// An error is returned if the mutation operation is not UpdateOne, or the database query fails. +func (m *PolicyBindingMutation) OldCreatedBy(ctx context.Context) (v string, err error) { + if !m.op.Is(OpUpdateOne) { + return v, errors.New("OldCreatedBy is only allowed on UpdateOne operations") + } + if m.id == nil || m.oldValue == nil { + return v, errors.New("OldCreatedBy requires an ID field in the mutation") + } + oldValue, err := m.oldValue(ctx) + if err != nil { + return v, fmt.Errorf("querying old value for OldCreatedBy: %w", err) + } + return oldValue.CreatedBy, nil +} + +// ClearCreatedBy clears the value of the "created_by" field. +func (m *PolicyBindingMutation) ClearCreatedBy() { + m.created_by = nil + m.clearedFields[policybinding.FieldCreatedBy] = struct{}{} +} + +// CreatedByCleared returns if the "created_by" field was cleared in this mutation. +func (m *PolicyBindingMutation) CreatedByCleared() bool { + _, ok := m.clearedFields[policybinding.FieldCreatedBy] + return ok +} + +// ResetCreatedBy resets all changes to the "created_by" field. +func (m *PolicyBindingMutation) ResetCreatedBy() { + m.created_by = nil + delete(m.clearedFields, policybinding.FieldCreatedBy) +} + +// SetPolicyID sets the "policy_id" field. +func (m *PolicyBindingMutation) SetPolicyID(u uuid.UUID) { + m.policy = &u +} + +// PolicyID returns the value of the "policy_id" field in the mutation. +func (m *PolicyBindingMutation) PolicyID() (r uuid.UUID, exists bool) { + v := m.policy + if v == nil { + return + } + return *v, true +} + +// OldPolicyID returns the old "policy_id" field's value of the PolicyBinding entity. +// If the PolicyBinding object wasn't provided to the builder, the object is fetched from the database. +// An error is returned if the mutation operation is not UpdateOne, or the database query fails. +func (m *PolicyBindingMutation) OldPolicyID(ctx context.Context) (v *uuid.UUID, err error) { + if !m.op.Is(OpUpdateOne) { + return v, errors.New("OldPolicyID is only allowed on UpdateOne operations") + } + if m.id == nil || m.oldValue == nil { + return v, errors.New("OldPolicyID requires an ID field in the mutation") + } + oldValue, err := m.oldValue(ctx) + if err != nil { + return v, fmt.Errorf("querying old value for OldPolicyID: %w", err) + } + return oldValue.PolicyID, nil +} + +// ClearPolicyID clears the value of the "policy_id" field. +func (m *PolicyBindingMutation) ClearPolicyID() { + m.policy = nil + m.clearedFields[policybinding.FieldPolicyID] = struct{}{} +} + +// PolicyIDCleared returns if the "policy_id" field was cleared in this mutation. +func (m *PolicyBindingMutation) PolicyIDCleared() bool { + _, ok := m.clearedFields[policybinding.FieldPolicyID] + return ok +} + +// ResetPolicyID resets all changes to the "policy_id" field. +func (m *PolicyBindingMutation) ResetPolicyID() { + m.policy = nil + delete(m.clearedFields, policybinding.FieldPolicyID) +} + +// SetUserID sets the "user_id" field. +func (m *PolicyBindingMutation) SetUserID(u uuid.UUID) { + m.user = &u +} + +// UserID returns the value of the "user_id" field in the mutation. +func (m *PolicyBindingMutation) UserID() (r uuid.UUID, exists bool) { + v := m.user + if v == nil { + return + } + return *v, true +} + +// OldUserID returns the old "user_id" field's value of the PolicyBinding entity. +// If the PolicyBinding object wasn't provided to the builder, the object is fetched from the database. +// An error is returned if the mutation operation is not UpdateOne, or the database query fails. +func (m *PolicyBindingMutation) OldUserID(ctx context.Context) (v *uuid.UUID, err error) { + if !m.op.Is(OpUpdateOne) { + return v, errors.New("OldUserID is only allowed on UpdateOne operations") + } + if m.id == nil || m.oldValue == nil { + return v, errors.New("OldUserID requires an ID field in the mutation") + } + oldValue, err := m.oldValue(ctx) + if err != nil { + return v, fmt.Errorf("querying old value for OldUserID: %w", err) + } + return oldValue.UserID, nil +} + +// ClearUserID clears the value of the "user_id" field. +func (m *PolicyBindingMutation) ClearUserID() { + m.user = nil + m.clearedFields[policybinding.FieldUserID] = struct{}{} +} + +// UserIDCleared returns if the "user_id" field was cleared in this mutation. +func (m *PolicyBindingMutation) UserIDCleared() bool { + _, ok := m.clearedFields[policybinding.FieldUserID] + return ok +} + +// ResetUserID resets all changes to the "user_id" field. +func (m *PolicyBindingMutation) ResetUserID() { + m.user = nil + delete(m.clearedFields, policybinding.FieldUserID) +} + +// SetGroupID sets the "group_id" field. +func (m *PolicyBindingMutation) SetGroupID(u uuid.UUID) { + m.group = &u +} + +// GroupID returns the value of the "group_id" field in the mutation. +func (m *PolicyBindingMutation) GroupID() (r uuid.UUID, exists bool) { + v := m.group + if v == nil { + return + } + return *v, true +} + +// OldGroupID returns the old "group_id" field's value of the PolicyBinding entity. +// If the PolicyBinding object wasn't provided to the builder, the object is fetched from the database. +// An error is returned if the mutation operation is not UpdateOne, or the database query fails. +func (m *PolicyBindingMutation) OldGroupID(ctx context.Context) (v *uuid.UUID, err error) { + if !m.op.Is(OpUpdateOne) { + return v, errors.New("OldGroupID is only allowed on UpdateOne operations") + } + if m.id == nil || m.oldValue == nil { + return v, errors.New("OldGroupID requires an ID field in the mutation") + } + oldValue, err := m.oldValue(ctx) + if err != nil { + return v, fmt.Errorf("querying old value for OldGroupID: %w", err) + } + return oldValue.GroupID, nil +} + +// ClearGroupID clears the value of the "group_id" field. +func (m *PolicyBindingMutation) ClearGroupID() { + m.group = nil + m.clearedFields[policybinding.FieldGroupID] = struct{}{} +} + +// GroupIDCleared returns if the "group_id" field was cleared in this mutation. +func (m *PolicyBindingMutation) GroupIDCleared() bool { + _, ok := m.clearedFields[policybinding.FieldGroupID] + return ok +} + +// ResetGroupID resets all changes to the "group_id" field. +func (m *PolicyBindingMutation) ResetGroupID() { + m.group = nil + delete(m.clearedFields, policybinding.FieldGroupID) +} + +// SetAgentID sets the "agent_id" field. +func (m *PolicyBindingMutation) SetAgentID(u uuid.UUID) { + m.agent = &u +} + +// AgentID returns the value of the "agent_id" field in the mutation. +func (m *PolicyBindingMutation) AgentID() (r uuid.UUID, exists bool) { + v := m.agent + if v == nil { + return + } + return *v, true +} + +// OldAgentID returns the old "agent_id" field's value of the PolicyBinding entity. +// If the PolicyBinding object wasn't provided to the builder, the object is fetched from the database. +// An error is returned if the mutation operation is not UpdateOne, or the database query fails. +func (m *PolicyBindingMutation) OldAgentID(ctx context.Context) (v *uuid.UUID, err error) { + if !m.op.Is(OpUpdateOne) { + return v, errors.New("OldAgentID is only allowed on UpdateOne operations") + } + if m.id == nil || m.oldValue == nil { + return v, errors.New("OldAgentID requires an ID field in the mutation") + } + oldValue, err := m.oldValue(ctx) + if err != nil { + return v, fmt.Errorf("querying old value for OldAgentID: %w", err) + } + return oldValue.AgentID, nil +} + +// ClearAgentID clears the value of the "agent_id" field. +func (m *PolicyBindingMutation) ClearAgentID() { + m.agent = nil + m.clearedFields[policybinding.FieldAgentID] = struct{}{} +} + +// AgentIDCleared returns if the "agent_id" field was cleared in this mutation. +func (m *PolicyBindingMutation) AgentIDCleared() bool { + _, ok := m.clearedFields[policybinding.FieldAgentID] + return ok +} + +// ResetAgentID resets all changes to the "agent_id" field. +func (m *PolicyBindingMutation) ResetAgentID() { + m.agent = nil + delete(m.clearedFields, policybinding.FieldAgentID) +} + +// ClearPolicy clears the "policy" edge to the AccessPolicy entity. +func (m *PolicyBindingMutation) ClearPolicy() { + m.clearedpolicy = true + m.clearedFields[policybinding.FieldPolicyID] = struct{}{} +} + +// PolicyCleared reports if the "policy" edge to the AccessPolicy entity was cleared. +func (m *PolicyBindingMutation) PolicyCleared() bool { + return m.PolicyIDCleared() || m.clearedpolicy +} + +// PolicyIDs returns the "policy" edge IDs in the mutation. +// Note that IDs always returns len(IDs) <= 1 for unique edges, and you should use +// PolicyID instead. It exists only for internal usage by the builders. +func (m *PolicyBindingMutation) PolicyIDs() (ids []uuid.UUID) { + if id := m.policy; id != nil { + ids = append(ids, *id) + } + return +} + +// ResetPolicy resets all changes to the "policy" edge. +func (m *PolicyBindingMutation) ResetPolicy() { + m.policy = nil + m.clearedpolicy = false +} + +// ClearUser clears the "user" edge to the User entity. +func (m *PolicyBindingMutation) ClearUser() { + m.cleareduser = true + m.clearedFields[policybinding.FieldUserID] = struct{}{} +} + +// UserCleared reports if the "user" edge to the User entity was cleared. +func (m *PolicyBindingMutation) UserCleared() bool { + return m.UserIDCleared() || m.cleareduser +} + +// UserIDs returns the "user" edge IDs in the mutation. +// Note that IDs always returns len(IDs) <= 1 for unique edges, and you should use +// UserID instead. It exists only for internal usage by the builders. +func (m *PolicyBindingMutation) UserIDs() (ids []uuid.UUID) { + if id := m.user; id != nil { + ids = append(ids, *id) + } + return +} + +// ResetUser resets all changes to the "user" edge. +func (m *PolicyBindingMutation) ResetUser() { + m.user = nil + m.cleareduser = false +} + +// ClearGroup clears the "group" edge to the Group entity. +func (m *PolicyBindingMutation) ClearGroup() { + m.clearedgroup = true + m.clearedFields[policybinding.FieldGroupID] = struct{}{} +} + +// GroupCleared reports if the "group" edge to the Group entity was cleared. +func (m *PolicyBindingMutation) GroupCleared() bool { + return m.GroupIDCleared() || m.clearedgroup +} + +// GroupIDs returns the "group" edge IDs in the mutation. +// Note that IDs always returns len(IDs) <= 1 for unique edges, and you should use +// GroupID instead. It exists only for internal usage by the builders. +func (m *PolicyBindingMutation) GroupIDs() (ids []uuid.UUID) { + if id := m.group; id != nil { + ids = append(ids, *id) + } + return +} + +// ResetGroup resets all changes to the "group" edge. +func (m *PolicyBindingMutation) ResetGroup() { + m.group = nil + m.clearedgroup = false +} + +// ClearAgent clears the "agent" edge to the Agent entity. +func (m *PolicyBindingMutation) ClearAgent() { + m.clearedagent = true + m.clearedFields[policybinding.FieldAgentID] = struct{}{} +} + +// AgentCleared reports if the "agent" edge to the Agent entity was cleared. +func (m *PolicyBindingMutation) AgentCleared() bool { + return m.AgentIDCleared() || m.clearedagent +} + +// AgentIDs returns the "agent" edge IDs in the mutation. +// Note that IDs always returns len(IDs) <= 1 for unique edges, and you should use +// AgentID instead. It exists only for internal usage by the builders. +func (m *PolicyBindingMutation) AgentIDs() (ids []uuid.UUID) { + if id := m.agent; id != nil { + ids = append(ids, *id) + } + return +} + +// ResetAgent resets all changes to the "agent" edge. +func (m *PolicyBindingMutation) ResetAgent() { + m.agent = nil + m.clearedagent = false +} + +// Where appends a list predicates to the PolicyBindingMutation builder. +func (m *PolicyBindingMutation) Where(ps ...predicate.PolicyBinding) { + m.predicates = append(m.predicates, ps...) +} + +// WhereP appends storage-level predicates to the PolicyBindingMutation builder. Using this method, +// users can use type-assertion to append predicates that do not depend on any generated package. +func (m *PolicyBindingMutation) WhereP(ps ...func(*sql.Selector)) { + p := make([]predicate.PolicyBinding, len(ps)) + for i := range ps { + p[i] = ps[i] + } + m.Where(p...) +} + +// Op returns the operation name. +func (m *PolicyBindingMutation) Op() Op { + return m.op +} + +// SetOp allows setting the mutation operation. +func (m *PolicyBindingMutation) SetOp(op Op) { + m.op = op +} + +// Type returns the node type of this mutation (PolicyBinding). +func (m *PolicyBindingMutation) Type() string { + return m.typ +} + +// Fields returns all fields that were changed during this mutation. Note that in +// order to get all numeric fields that were incremented/decremented, call +// AddedFields(). +func (m *PolicyBindingMutation) Fields() []string { + fields := make([]string, 0, 7) + if m.principal_type != nil { + fields = append(fields, policybinding.FieldPrincipalType) + } + if m.created != nil { + fields = append(fields, policybinding.FieldCreated) + } + if m.created_by != nil { + fields = append(fields, policybinding.FieldCreatedBy) + } + if m.policy != nil { + fields = append(fields, policybinding.FieldPolicyID) + } + if m.user != nil { + fields = append(fields, policybinding.FieldUserID) + } + if m.group != nil { + fields = append(fields, policybinding.FieldGroupID) + } + if m.agent != nil { + fields = append(fields, policybinding.FieldAgentID) + } + return fields +} + +// Field returns the value of a field with the given name. The second boolean +// return value indicates that this field was not set, or was not defined in the +// schema. +func (m *PolicyBindingMutation) Field(name string) (ent.Value, bool) { + switch name { + case policybinding.FieldPrincipalType: + return m.PrincipalType() + case policybinding.FieldCreated: + return m.Created() + case policybinding.FieldCreatedBy: + return m.CreatedBy() + case policybinding.FieldPolicyID: + return m.PolicyID() + case policybinding.FieldUserID: + return m.UserID() + case policybinding.FieldGroupID: + return m.GroupID() + case policybinding.FieldAgentID: + return m.AgentID() + } + return nil, false +} + +// OldField returns the old value of the field from the database. An error is +// returned if the mutation operation is not UpdateOne, or the query to the +// database failed. +func (m *PolicyBindingMutation) OldField(ctx context.Context, name string) (ent.Value, error) { + switch name { + case policybinding.FieldPrincipalType: + return m.OldPrincipalType(ctx) + case policybinding.FieldCreated: + return m.OldCreated(ctx) + case policybinding.FieldCreatedBy: + return m.OldCreatedBy(ctx) + case policybinding.FieldPolicyID: + return m.OldPolicyID(ctx) + case policybinding.FieldUserID: + return m.OldUserID(ctx) + case policybinding.FieldGroupID: + return m.OldGroupID(ctx) + case policybinding.FieldAgentID: + return m.OldAgentID(ctx) + } + return nil, fmt.Errorf("unknown PolicyBinding field %s", name) +} + +// SetField sets the value of a field with the given name. It returns an error if +// the field is not defined in the schema, or if the type mismatched the field +// type. +func (m *PolicyBindingMutation) SetField(name string, value ent.Value) error { + switch name { + case policybinding.FieldPrincipalType: + v, ok := value.(policybinding.PrincipalType) + if !ok { + return fmt.Errorf("unexpected type %T for field %s", value, name) + } + m.SetPrincipalType(v) + return nil + case policybinding.FieldCreated: + v, ok := value.(time.Time) + if !ok { + return fmt.Errorf("unexpected type %T for field %s", value, name) + } + m.SetCreated(v) + return nil + case policybinding.FieldCreatedBy: + v, ok := value.(string) + if !ok { + return fmt.Errorf("unexpected type %T for field %s", value, name) + } + m.SetCreatedBy(v) + return nil + case policybinding.FieldPolicyID: + v, ok := value.(uuid.UUID) + if !ok { + return fmt.Errorf("unexpected type %T for field %s", value, name) + } + m.SetPolicyID(v) + return nil + case policybinding.FieldUserID: + v, ok := value.(uuid.UUID) + if !ok { + return fmt.Errorf("unexpected type %T for field %s", value, name) + } + m.SetUserID(v) + return nil + case policybinding.FieldGroupID: + v, ok := value.(uuid.UUID) + if !ok { + return fmt.Errorf("unexpected type %T for field %s", value, name) + } + m.SetGroupID(v) + return nil + case policybinding.FieldAgentID: + v, ok := value.(uuid.UUID) + if !ok { + return fmt.Errorf("unexpected type %T for field %s", value, name) + } + m.SetAgentID(v) + return nil + } + return fmt.Errorf("unknown PolicyBinding field %s", name) +} + +// AddedFields returns all numeric fields that were incremented/decremented during +// this mutation. +func (m *PolicyBindingMutation) AddedFields() []string { + return nil +} + +// AddedField returns the numeric value that was incremented/decremented on a field +// with the given name. The second boolean return value indicates that this field +// was not set, or was not defined in the schema. +func (m *PolicyBindingMutation) AddedField(name string) (ent.Value, bool) { + return nil, false +} + +// AddField adds the value to the field with the given name. It returns an error if +// the field is not defined in the schema, or if the type mismatched the field +// type. +func (m *PolicyBindingMutation) AddField(name string, value ent.Value) error { + switch name { + } + return fmt.Errorf("unknown PolicyBinding numeric field %s", name) +} + +// ClearedFields returns all nullable fields that were cleared during this +// mutation. +func (m *PolicyBindingMutation) ClearedFields() []string { + var fields []string + if m.FieldCleared(policybinding.FieldCreatedBy) { + fields = append(fields, policybinding.FieldCreatedBy) + } + if m.FieldCleared(policybinding.FieldPolicyID) { + fields = append(fields, policybinding.FieldPolicyID) + } + if m.FieldCleared(policybinding.FieldUserID) { + fields = append(fields, policybinding.FieldUserID) + } + if m.FieldCleared(policybinding.FieldGroupID) { + fields = append(fields, policybinding.FieldGroupID) + } + if m.FieldCleared(policybinding.FieldAgentID) { + fields = append(fields, policybinding.FieldAgentID) + } + return fields +} + +// FieldCleared returns a boolean indicating if a field with the given name was +// cleared in this mutation. +func (m *PolicyBindingMutation) FieldCleared(name string) bool { + _, ok := m.clearedFields[name] + return ok +} + +// ClearField clears the value of the field with the given name. It returns an +// error if the field is not defined in the schema. +func (m *PolicyBindingMutation) ClearField(name string) error { + switch name { + case policybinding.FieldCreatedBy: + m.ClearCreatedBy() + return nil + case policybinding.FieldPolicyID: + m.ClearPolicyID() + return nil + case policybinding.FieldUserID: + m.ClearUserID() + return nil + case policybinding.FieldGroupID: + m.ClearGroupID() + return nil + case policybinding.FieldAgentID: + m.ClearAgentID() + return nil + } + return fmt.Errorf("unknown PolicyBinding nullable field %s", name) +} + +// ResetField resets all changes in the mutation for the field with the given name. +// It returns an error if the field is not defined in the schema. +func (m *PolicyBindingMutation) ResetField(name string) error { + switch name { + case policybinding.FieldPrincipalType: + m.ResetPrincipalType() + return nil + case policybinding.FieldCreated: + m.ResetCreated() + return nil + case policybinding.FieldCreatedBy: + m.ResetCreatedBy() + return nil + case policybinding.FieldPolicyID: + m.ResetPolicyID() + return nil + case policybinding.FieldUserID: + m.ResetUserID() + return nil + case policybinding.FieldGroupID: + m.ResetGroupID() + return nil + case policybinding.FieldAgentID: + m.ResetAgentID() + return nil + } + return fmt.Errorf("unknown PolicyBinding field %s", name) +} + +// AddedEdges returns all edge names that were set/added in this mutation. +func (m *PolicyBindingMutation) AddedEdges() []string { + edges := make([]string, 0, 4) + if m.policy != nil { + edges = append(edges, policybinding.EdgePolicy) + } + if m.user != nil { + edges = append(edges, policybinding.EdgeUser) + } + if m.group != nil { + edges = append(edges, policybinding.EdgeGroup) + } + if m.agent != nil { + edges = append(edges, policybinding.EdgeAgent) + } + return edges +} + +// AddedIDs returns all IDs (to other nodes) that were added for the given edge +// name in this mutation. +func (m *PolicyBindingMutation) AddedIDs(name string) []ent.Value { + switch name { + case policybinding.EdgePolicy: + if id := m.policy; id != nil { + return []ent.Value{*id} + } + case policybinding.EdgeUser: + if id := m.user; id != nil { + return []ent.Value{*id} + } + case policybinding.EdgeGroup: + if id := m.group; id != nil { + return []ent.Value{*id} + } + case policybinding.EdgeAgent: + if id := m.agent; id != nil { + return []ent.Value{*id} + } + } + return nil +} + +// RemovedEdges returns all edge names that were removed in this mutation. +func (m *PolicyBindingMutation) RemovedEdges() []string { + edges := make([]string, 0, 4) + return edges +} + +// RemovedIDs returns all IDs (to other nodes) that were removed for the edge with +// the given name in this mutation. +func (m *PolicyBindingMutation) RemovedIDs(name string) []ent.Value { + return nil +} + +// ClearedEdges returns all edge names that were cleared in this mutation. +func (m *PolicyBindingMutation) ClearedEdges() []string { + edges := make([]string, 0, 4) + if m.clearedpolicy { + edges = append(edges, policybinding.EdgePolicy) + } + if m.cleareduser { + edges = append(edges, policybinding.EdgeUser) + } + if m.clearedgroup { + edges = append(edges, policybinding.EdgeGroup) + } + if m.clearedagent { + edges = append(edges, policybinding.EdgeAgent) + } + return edges +} + +// EdgeCleared returns a boolean which indicates if the edge with the given name +// was cleared in this mutation. +func (m *PolicyBindingMutation) EdgeCleared(name string) bool { + switch name { + case policybinding.EdgePolicy: + return m.clearedpolicy + case policybinding.EdgeUser: + return m.cleareduser + case policybinding.EdgeGroup: + return m.clearedgroup + case policybinding.EdgeAgent: + return m.clearedagent + } + return false +} + +// ClearEdge clears the value of the edge with the given name. It returns an error +// if that edge is not defined in the schema. +func (m *PolicyBindingMutation) ClearEdge(name string) error { + switch name { + case policybinding.EdgePolicy: + m.ClearPolicy() + return nil + case policybinding.EdgeUser: + m.ClearUser() + return nil + case policybinding.EdgeGroup: + m.ClearGroup() + return nil + case policybinding.EdgeAgent: + m.ClearAgent() + return nil + } + return fmt.Errorf("unknown PolicyBinding unique edge %s", name) +} + +// ResetEdge resets all changes to the edge with the given name in this mutation. +// It returns an error if the edge is not defined in the schema. +func (m *PolicyBindingMutation) ResetEdge(name string) error { + switch name { + case policybinding.EdgePolicy: + m.ResetPolicy() + return nil + case policybinding.EdgeUser: + m.ResetUser() + return nil + case policybinding.EdgeGroup: + m.ResetGroup() + return nil + case policybinding.EdgeAgent: + m.ResetAgent() + return nil + } + return fmt.Errorf("unknown PolicyBinding edge %s", name) +} + +// ProjectMutation represents an operation that mutates the Project nodes in the graph. +type ProjectMutation struct { + config + op Op + typ string + id *uuid.UUID + name *string + slug *string + git_remote *string + default_runtime_broker_id *string + labels *map[string]string + annotations *map[string]string + shared_dirs *string + created *time.Time + updated *time.Time + created_by *string + owner_id *string + visibility *string + github_installation_id *int64 + addgithub_installation_id *int64 + github_permissions *string + github_app_status *string + git_identity *string + clearedFields map[string]struct{} + agents map[uuid.UUID]struct{} + removedagents map[uuid.UUID]struct{} + clearedagents bool + done bool + oldValue func(context.Context) (*Project, error) + predicates []predicate.Project +} + +var _ ent.Mutation = (*ProjectMutation)(nil) + +// projectOption allows management of the mutation configuration using functional options. +type projectOption func(*ProjectMutation) + +// newProjectMutation creates new mutation for the Project entity. +func newProjectMutation(c config, op Op, opts ...projectOption) *ProjectMutation { + m := &ProjectMutation{ + config: c, + op: op, + typ: TypeProject, + clearedFields: make(map[string]struct{}), + } + for _, opt := range opts { + opt(m) + } + return m +} + +// withProjectID sets the ID field of the mutation. +func withProjectID(id uuid.UUID) projectOption { + return func(m *ProjectMutation) { + var ( + err error + once sync.Once + value *Project + ) + m.oldValue = func(ctx context.Context) (*Project, error) { + once.Do(func() { + if m.done { + err = errors.New("querying old values post mutation is not allowed") + } else { + value, err = m.Client().Project.Get(ctx, id) + } + }) + return value, err + } + m.id = &id + } +} + +// withProject sets the old Project of the mutation. +func withProject(node *Project) projectOption { + return func(m *ProjectMutation) { + m.oldValue = func(context.Context) (*Project, error) { + return node, nil + } + m.id = &node.ID + } +} + +// Client returns a new `ent.Client` from the mutation. If the mutation was +// executed in a transaction (ent.Tx), a transactional client is returned. +func (m ProjectMutation) Client() *Client { + client := &Client{config: m.config} + client.init() + return client +} + +// Tx returns an `ent.Tx` for mutations that were executed in transactions; +// it returns an error otherwise. +func (m ProjectMutation) Tx() (*Tx, error) { + if _, ok := m.driver.(*txDriver); !ok { + return nil, errors.New("ent: mutation is not running in a transaction") + } + tx := &Tx{config: m.config} + tx.init() + return tx, nil +} + +// SetID sets the value of the id field. Note that this +// operation is only accepted on creation of Project entities. +func (m *ProjectMutation) SetID(id uuid.UUID) { + m.id = &id +} + +// ID returns the ID value in the mutation. Note that the ID is only available +// if it was provided to the builder or after it was returned from the database. +func (m *ProjectMutation) ID() (id uuid.UUID, exists bool) { + if m.id == nil { + return + } + return *m.id, true +} + +// IDs queries the database and returns the entity ids that match the mutation's predicate. +// That means, if the mutation is applied within a transaction with an isolation level such +// as sql.LevelSerializable, the returned ids match the ids of the rows that will be updated +// or updated by the mutation. +func (m *ProjectMutation) IDs(ctx context.Context) ([]uuid.UUID, error) { + switch { + case m.op.Is(OpUpdateOne | OpDeleteOne): + id, exists := m.ID() + if exists { + return []uuid.UUID{id}, nil + } + fallthrough + case m.op.Is(OpUpdate | OpDelete): + return m.Client().Project.Query().Where(m.predicates...).IDs(ctx) + default: + return nil, fmt.Errorf("IDs is not allowed on %s operations", m.op) + } +} + +// SetName sets the "name" field. +func (m *ProjectMutation) SetName(s string) { + m.name = &s +} + +// Name returns the value of the "name" field in the mutation. +func (m *ProjectMutation) Name() (r string, exists bool) { + v := m.name + if v == nil { + return + } + return *v, true +} + +// OldName returns the old "name" field's value of the Project entity. +// If the Project object wasn't provided to the builder, the object is fetched from the database. +// An error is returned if the mutation operation is not UpdateOne, or the database query fails. +func (m *ProjectMutation) OldName(ctx context.Context) (v string, err error) { + if !m.op.Is(OpUpdateOne) { + return v, errors.New("OldName is only allowed on UpdateOne operations") + } + if m.id == nil || m.oldValue == nil { + return v, errors.New("OldName requires an ID field in the mutation") + } + oldValue, err := m.oldValue(ctx) + if err != nil { + return v, fmt.Errorf("querying old value for OldName: %w", err) + } + return oldValue.Name, nil +} + +// ResetName resets all changes to the "name" field. +func (m *ProjectMutation) ResetName() { + m.name = nil +} + +// SetSlug sets the "slug" field. +func (m *ProjectMutation) SetSlug(s string) { + m.slug = &s +} + +// Slug returns the value of the "slug" field in the mutation. +func (m *ProjectMutation) Slug() (r string, exists bool) { + v := m.slug + if v == nil { + return + } + return *v, true +} + +// OldSlug returns the old "slug" field's value of the Project entity. +// If the Project object wasn't provided to the builder, the object is fetched from the database. +// An error is returned if the mutation operation is not UpdateOne, or the database query fails. +func (m *ProjectMutation) OldSlug(ctx context.Context) (v string, err error) { + if !m.op.Is(OpUpdateOne) { + return v, errors.New("OldSlug is only allowed on UpdateOne operations") + } + if m.id == nil || m.oldValue == nil { + return v, errors.New("OldSlug requires an ID field in the mutation") + } + oldValue, err := m.oldValue(ctx) + if err != nil { + return v, fmt.Errorf("querying old value for OldSlug: %w", err) + } + return oldValue.Slug, nil +} + +// ResetSlug resets all changes to the "slug" field. +func (m *ProjectMutation) ResetSlug() { + m.slug = nil +} + +// SetGitRemote sets the "git_remote" field. +func (m *ProjectMutation) SetGitRemote(s string) { + m.git_remote = &s +} + +// GitRemote returns the value of the "git_remote" field in the mutation. +func (m *ProjectMutation) GitRemote() (r string, exists bool) { + v := m.git_remote + if v == nil { + return + } + return *v, true +} + +// OldGitRemote returns the old "git_remote" field's value of the Project entity. +// If the Project object wasn't provided to the builder, the object is fetched from the database. +// An error is returned if the mutation operation is not UpdateOne, or the database query fails. +func (m *ProjectMutation) OldGitRemote(ctx context.Context) (v *string, err error) { + if !m.op.Is(OpUpdateOne) { + return v, errors.New("OldGitRemote is only allowed on UpdateOne operations") + } + if m.id == nil || m.oldValue == nil { + return v, errors.New("OldGitRemote requires an ID field in the mutation") + } + oldValue, err := m.oldValue(ctx) + if err != nil { + return v, fmt.Errorf("querying old value for OldGitRemote: %w", err) + } + return oldValue.GitRemote, nil +} + +// ClearGitRemote clears the value of the "git_remote" field. +func (m *ProjectMutation) ClearGitRemote() { + m.git_remote = nil + m.clearedFields[project.FieldGitRemote] = struct{}{} +} + +// GitRemoteCleared returns if the "git_remote" field was cleared in this mutation. +func (m *ProjectMutation) GitRemoteCleared() bool { + _, ok := m.clearedFields[project.FieldGitRemote] + return ok +} + +// ResetGitRemote resets all changes to the "git_remote" field. +func (m *ProjectMutation) ResetGitRemote() { + m.git_remote = nil + delete(m.clearedFields, project.FieldGitRemote) +} + +// SetDefaultRuntimeBrokerID sets the "default_runtime_broker_id" field. +func (m *ProjectMutation) SetDefaultRuntimeBrokerID(s string) { + m.default_runtime_broker_id = &s +} + +// DefaultRuntimeBrokerID returns the value of the "default_runtime_broker_id" field in the mutation. +func (m *ProjectMutation) DefaultRuntimeBrokerID() (r string, exists bool) { + v := m.default_runtime_broker_id + if v == nil { + return + } + return *v, true +} + +// OldDefaultRuntimeBrokerID returns the old "default_runtime_broker_id" field's value of the Project entity. +// If the Project object wasn't provided to the builder, the object is fetched from the database. +// An error is returned if the mutation operation is not UpdateOne, or the database query fails. +func (m *ProjectMutation) OldDefaultRuntimeBrokerID(ctx context.Context) (v *string, err error) { + if !m.op.Is(OpUpdateOne) { + return v, errors.New("OldDefaultRuntimeBrokerID is only allowed on UpdateOne operations") + } + if m.id == nil || m.oldValue == nil { + return v, errors.New("OldDefaultRuntimeBrokerID requires an ID field in the mutation") + } + oldValue, err := m.oldValue(ctx) + if err != nil { + return v, fmt.Errorf("querying old value for OldDefaultRuntimeBrokerID: %w", err) + } + return oldValue.DefaultRuntimeBrokerID, nil +} + +// ClearDefaultRuntimeBrokerID clears the value of the "default_runtime_broker_id" field. +func (m *ProjectMutation) ClearDefaultRuntimeBrokerID() { + m.default_runtime_broker_id = nil + m.clearedFields[project.FieldDefaultRuntimeBrokerID] = struct{}{} +} + +// DefaultRuntimeBrokerIDCleared returns if the "default_runtime_broker_id" field was cleared in this mutation. +func (m *ProjectMutation) DefaultRuntimeBrokerIDCleared() bool { + _, ok := m.clearedFields[project.FieldDefaultRuntimeBrokerID] + return ok +} + +// ResetDefaultRuntimeBrokerID resets all changes to the "default_runtime_broker_id" field. +func (m *ProjectMutation) ResetDefaultRuntimeBrokerID() { + m.default_runtime_broker_id = nil + delete(m.clearedFields, project.FieldDefaultRuntimeBrokerID) +} + +// SetLabels sets the "labels" field. +func (m *ProjectMutation) SetLabels(value map[string]string) { + m.labels = &value +} + +// Labels returns the value of the "labels" field in the mutation. +func (m *ProjectMutation) Labels() (r map[string]string, exists bool) { + v := m.labels + if v == nil { + return + } + return *v, true +} + +// OldLabels returns the old "labels" field's value of the Project entity. +// If the Project object wasn't provided to the builder, the object is fetched from the database. +// An error is returned if the mutation operation is not UpdateOne, or the database query fails. +func (m *ProjectMutation) OldLabels(ctx context.Context) (v map[string]string, err error) { + if !m.op.Is(OpUpdateOne) { + return v, errors.New("OldLabels is only allowed on UpdateOne operations") + } + if m.id == nil || m.oldValue == nil { + return v, errors.New("OldLabels requires an ID field in the mutation") + } + oldValue, err := m.oldValue(ctx) + if err != nil { + return v, fmt.Errorf("querying old value for OldLabels: %w", err) + } + return oldValue.Labels, nil +} + +// ClearLabels clears the value of the "labels" field. +func (m *ProjectMutation) ClearLabels() { + m.labels = nil + m.clearedFields[project.FieldLabels] = struct{}{} +} + +// LabelsCleared returns if the "labels" field was cleared in this mutation. +func (m *ProjectMutation) LabelsCleared() bool { + _, ok := m.clearedFields[project.FieldLabels] + return ok +} + +// ResetLabels resets all changes to the "labels" field. +func (m *ProjectMutation) ResetLabels() { + m.labels = nil + delete(m.clearedFields, project.FieldLabels) +} + +// SetAnnotations sets the "annotations" field. +func (m *ProjectMutation) SetAnnotations(value map[string]string) { + m.annotations = &value +} + +// Annotations returns the value of the "annotations" field in the mutation. +func (m *ProjectMutation) Annotations() (r map[string]string, exists bool) { + v := m.annotations + if v == nil { + return + } + return *v, true +} + +// OldAnnotations returns the old "annotations" field's value of the Project entity. +// If the Project object wasn't provided to the builder, the object is fetched from the database. +// An error is returned if the mutation operation is not UpdateOne, or the database query fails. +func (m *ProjectMutation) OldAnnotations(ctx context.Context) (v map[string]string, err error) { + if !m.op.Is(OpUpdateOne) { + return v, errors.New("OldAnnotations is only allowed on UpdateOne operations") + } + if m.id == nil || m.oldValue == nil { + return v, errors.New("OldAnnotations requires an ID field in the mutation") + } + oldValue, err := m.oldValue(ctx) + if err != nil { + return v, fmt.Errorf("querying old value for OldAnnotations: %w", err) + } + return oldValue.Annotations, nil +} + +// ClearAnnotations clears the value of the "annotations" field. +func (m *ProjectMutation) ClearAnnotations() { + m.annotations = nil + m.clearedFields[project.FieldAnnotations] = struct{}{} +} + +// AnnotationsCleared returns if the "annotations" field was cleared in this mutation. +func (m *ProjectMutation) AnnotationsCleared() bool { + _, ok := m.clearedFields[project.FieldAnnotations] + return ok +} + +// ResetAnnotations resets all changes to the "annotations" field. +func (m *ProjectMutation) ResetAnnotations() { + m.annotations = nil + delete(m.clearedFields, project.FieldAnnotations) +} + +// SetSharedDirs sets the "shared_dirs" field. +func (m *ProjectMutation) SetSharedDirs(s string) { + m.shared_dirs = &s +} + +// SharedDirs returns the value of the "shared_dirs" field in the mutation. +func (m *ProjectMutation) SharedDirs() (r string, exists bool) { + v := m.shared_dirs + if v == nil { + return + } + return *v, true +} + +// OldSharedDirs returns the old "shared_dirs" field's value of the Project entity. +// If the Project object wasn't provided to the builder, the object is fetched from the database. +// An error is returned if the mutation operation is not UpdateOne, or the database query fails. +func (m *ProjectMutation) OldSharedDirs(ctx context.Context) (v string, err error) { + if !m.op.Is(OpUpdateOne) { + return v, errors.New("OldSharedDirs is only allowed on UpdateOne operations") + } + if m.id == nil || m.oldValue == nil { + return v, errors.New("OldSharedDirs requires an ID field in the mutation") + } + oldValue, err := m.oldValue(ctx) + if err != nil { + return v, fmt.Errorf("querying old value for OldSharedDirs: %w", err) + } + return oldValue.SharedDirs, nil +} + +// ClearSharedDirs clears the value of the "shared_dirs" field. +func (m *ProjectMutation) ClearSharedDirs() { + m.shared_dirs = nil + m.clearedFields[project.FieldSharedDirs] = struct{}{} +} + +// SharedDirsCleared returns if the "shared_dirs" field was cleared in this mutation. +func (m *ProjectMutation) SharedDirsCleared() bool { + _, ok := m.clearedFields[project.FieldSharedDirs] + return ok +} + +// ResetSharedDirs resets all changes to the "shared_dirs" field. +func (m *ProjectMutation) ResetSharedDirs() { + m.shared_dirs = nil + delete(m.clearedFields, project.FieldSharedDirs) +} + +// SetCreated sets the "created" field. +func (m *ProjectMutation) SetCreated(t time.Time) { + m.created = &t +} + +// Created returns the value of the "created" field in the mutation. +func (m *ProjectMutation) Created() (r time.Time, exists bool) { + v := m.created + if v == nil { + return + } + return *v, true +} + +// OldCreated returns the old "created" field's value of the Project entity. +// If the Project object wasn't provided to the builder, the object is fetched from the database. +// An error is returned if the mutation operation is not UpdateOne, or the database query fails. +func (m *ProjectMutation) OldCreated(ctx context.Context) (v time.Time, err error) { + if !m.op.Is(OpUpdateOne) { + return v, errors.New("OldCreated is only allowed on UpdateOne operations") + } + if m.id == nil || m.oldValue == nil { + return v, errors.New("OldCreated requires an ID field in the mutation") + } + oldValue, err := m.oldValue(ctx) + if err != nil { + return v, fmt.Errorf("querying old value for OldCreated: %w", err) + } + return oldValue.Created, nil +} + +// ResetCreated resets all changes to the "created" field. +func (m *ProjectMutation) ResetCreated() { + m.created = nil +} + +// SetUpdated sets the "updated" field. +func (m *ProjectMutation) SetUpdated(t time.Time) { + m.updated = &t +} + +// Updated returns the value of the "updated" field in the mutation. +func (m *ProjectMutation) Updated() (r time.Time, exists bool) { + v := m.updated + if v == nil { + return + } + return *v, true +} + +// OldUpdated returns the old "updated" field's value of the Project entity. +// If the Project object wasn't provided to the builder, the object is fetched from the database. +// An error is returned if the mutation operation is not UpdateOne, or the database query fails. +func (m *ProjectMutation) OldUpdated(ctx context.Context) (v time.Time, err error) { + if !m.op.Is(OpUpdateOne) { + return v, errors.New("OldUpdated is only allowed on UpdateOne operations") + } + if m.id == nil || m.oldValue == nil { + return v, errors.New("OldUpdated requires an ID field in the mutation") + } + oldValue, err := m.oldValue(ctx) + if err != nil { + return v, fmt.Errorf("querying old value for OldUpdated: %w", err) + } + return oldValue.Updated, nil +} + +// ResetUpdated resets all changes to the "updated" field. +func (m *ProjectMutation) ResetUpdated() { + m.updated = nil +} + +// SetCreatedBy sets the "created_by" field. +func (m *ProjectMutation) SetCreatedBy(s string) { + m.created_by = &s +} + +// CreatedBy returns the value of the "created_by" field in the mutation. +func (m *ProjectMutation) CreatedBy() (r string, exists bool) { + v := m.created_by + if v == nil { + return + } + return *v, true +} + +// OldCreatedBy returns the old "created_by" field's value of the Project entity. +// If the Project object wasn't provided to the builder, the object is fetched from the database. +// An error is returned if the mutation operation is not UpdateOne, or the database query fails. +func (m *ProjectMutation) OldCreatedBy(ctx context.Context) (v string, err error) { + if !m.op.Is(OpUpdateOne) { + return v, errors.New("OldCreatedBy is only allowed on UpdateOne operations") + } + if m.id == nil || m.oldValue == nil { + return v, errors.New("OldCreatedBy requires an ID field in the mutation") + } + oldValue, err := m.oldValue(ctx) + if err != nil { + return v, fmt.Errorf("querying old value for OldCreatedBy: %w", err) + } + return oldValue.CreatedBy, nil +} + +// ClearCreatedBy clears the value of the "created_by" field. +func (m *ProjectMutation) ClearCreatedBy() { + m.created_by = nil + m.clearedFields[project.FieldCreatedBy] = struct{}{} +} + +// CreatedByCleared returns if the "created_by" field was cleared in this mutation. +func (m *ProjectMutation) CreatedByCleared() bool { + _, ok := m.clearedFields[project.FieldCreatedBy] + return ok +} + +// ResetCreatedBy resets all changes to the "created_by" field. +func (m *ProjectMutation) ResetCreatedBy() { + m.created_by = nil + delete(m.clearedFields, project.FieldCreatedBy) +} + +// SetOwnerID sets the "owner_id" field. +func (m *ProjectMutation) SetOwnerID(s string) { + m.owner_id = &s +} + +// OwnerID returns the value of the "owner_id" field in the mutation. +func (m *ProjectMutation) OwnerID() (r string, exists bool) { + v := m.owner_id + if v == nil { + return + } + return *v, true +} + +// OldOwnerID returns the old "owner_id" field's value of the Project entity. +// If the Project object wasn't provided to the builder, the object is fetched from the database. +// An error is returned if the mutation operation is not UpdateOne, or the database query fails. +func (m *ProjectMutation) OldOwnerID(ctx context.Context) (v string, err error) { + if !m.op.Is(OpUpdateOne) { + return v, errors.New("OldOwnerID is only allowed on UpdateOne operations") + } + if m.id == nil || m.oldValue == nil { + return v, errors.New("OldOwnerID requires an ID field in the mutation") + } + oldValue, err := m.oldValue(ctx) + if err != nil { + return v, fmt.Errorf("querying old value for OldOwnerID: %w", err) + } + return oldValue.OwnerID, nil +} + +// ClearOwnerID clears the value of the "owner_id" field. +func (m *ProjectMutation) ClearOwnerID() { + m.owner_id = nil + m.clearedFields[project.FieldOwnerID] = struct{}{} +} + +// OwnerIDCleared returns if the "owner_id" field was cleared in this mutation. +func (m *ProjectMutation) OwnerIDCleared() bool { + _, ok := m.clearedFields[project.FieldOwnerID] + return ok +} + +// ResetOwnerID resets all changes to the "owner_id" field. +func (m *ProjectMutation) ResetOwnerID() { + m.owner_id = nil + delete(m.clearedFields, project.FieldOwnerID) +} + +// SetVisibility sets the "visibility" field. +func (m *ProjectMutation) SetVisibility(s string) { + m.visibility = &s +} + +// Visibility returns the value of the "visibility" field in the mutation. +func (m *ProjectMutation) Visibility() (r string, exists bool) { + v := m.visibility + if v == nil { + return + } + return *v, true +} + +// OldVisibility returns the old "visibility" field's value of the Project entity. +// If the Project object wasn't provided to the builder, the object is fetched from the database. +// An error is returned if the mutation operation is not UpdateOne, or the database query fails. +func (m *ProjectMutation) OldVisibility(ctx context.Context) (v string, err error) { + if !m.op.Is(OpUpdateOne) { + return v, errors.New("OldVisibility is only allowed on UpdateOne operations") + } + if m.id == nil || m.oldValue == nil { + return v, errors.New("OldVisibility requires an ID field in the mutation") + } + oldValue, err := m.oldValue(ctx) + if err != nil { + return v, fmt.Errorf("querying old value for OldVisibility: %w", err) + } + return oldValue.Visibility, nil +} + +// ResetVisibility resets all changes to the "visibility" field. +func (m *ProjectMutation) ResetVisibility() { + m.visibility = nil +} + +// SetGithubInstallationID sets the "github_installation_id" field. +func (m *ProjectMutation) SetGithubInstallationID(i int64) { + m.github_installation_id = &i + m.addgithub_installation_id = nil +} + +// GithubInstallationID returns the value of the "github_installation_id" field in the mutation. +func (m *ProjectMutation) GithubInstallationID() (r int64, exists bool) { + v := m.github_installation_id + if v == nil { + return + } + return *v, true +} + +// OldGithubInstallationID returns the old "github_installation_id" field's value of the Project entity. +// If the Project object wasn't provided to the builder, the object is fetched from the database. +// An error is returned if the mutation operation is not UpdateOne, or the database query fails. +func (m *ProjectMutation) OldGithubInstallationID(ctx context.Context) (v *int64, err error) { + if !m.op.Is(OpUpdateOne) { + return v, errors.New("OldGithubInstallationID is only allowed on UpdateOne operations") + } + if m.id == nil || m.oldValue == nil { + return v, errors.New("OldGithubInstallationID requires an ID field in the mutation") + } + oldValue, err := m.oldValue(ctx) + if err != nil { + return v, fmt.Errorf("querying old value for OldGithubInstallationID: %w", err) + } + return oldValue.GithubInstallationID, nil +} + +// AddGithubInstallationID adds i to the "github_installation_id" field. +func (m *ProjectMutation) AddGithubInstallationID(i int64) { + if m.addgithub_installation_id != nil { + *m.addgithub_installation_id += i + } else { + m.addgithub_installation_id = &i + } +} + +// AddedGithubInstallationID returns the value that was added to the "github_installation_id" field in this mutation. +func (m *ProjectMutation) AddedGithubInstallationID() (r int64, exists bool) { + v := m.addgithub_installation_id + if v == nil { + return + } + return *v, true +} + +// ClearGithubInstallationID clears the value of the "github_installation_id" field. +func (m *ProjectMutation) ClearGithubInstallationID() { + m.github_installation_id = nil + m.addgithub_installation_id = nil + m.clearedFields[project.FieldGithubInstallationID] = struct{}{} +} + +// GithubInstallationIDCleared returns if the "github_installation_id" field was cleared in this mutation. +func (m *ProjectMutation) GithubInstallationIDCleared() bool { + _, ok := m.clearedFields[project.FieldGithubInstallationID] + return ok +} + +// ResetGithubInstallationID resets all changes to the "github_installation_id" field. +func (m *ProjectMutation) ResetGithubInstallationID() { + m.github_installation_id = nil + m.addgithub_installation_id = nil + delete(m.clearedFields, project.FieldGithubInstallationID) +} + +// SetGithubPermissions sets the "github_permissions" field. +func (m *ProjectMutation) SetGithubPermissions(s string) { + m.github_permissions = &s +} + +// GithubPermissions returns the value of the "github_permissions" field in the mutation. +func (m *ProjectMutation) GithubPermissions() (r string, exists bool) { + v := m.github_permissions + if v == nil { + return + } + return *v, true +} + +// OldGithubPermissions returns the old "github_permissions" field's value of the Project entity. +// If the Project object wasn't provided to the builder, the object is fetched from the database. +// An error is returned if the mutation operation is not UpdateOne, or the database query fails. +func (m *ProjectMutation) OldGithubPermissions(ctx context.Context) (v string, err error) { + if !m.op.Is(OpUpdateOne) { + return v, errors.New("OldGithubPermissions is only allowed on UpdateOne operations") + } + if m.id == nil || m.oldValue == nil { + return v, errors.New("OldGithubPermissions requires an ID field in the mutation") + } + oldValue, err := m.oldValue(ctx) + if err != nil { + return v, fmt.Errorf("querying old value for OldGithubPermissions: %w", err) + } + return oldValue.GithubPermissions, nil +} + +// ClearGithubPermissions clears the value of the "github_permissions" field. +func (m *ProjectMutation) ClearGithubPermissions() { + m.github_permissions = nil + m.clearedFields[project.FieldGithubPermissions] = struct{}{} +} + +// GithubPermissionsCleared returns if the "github_permissions" field was cleared in this mutation. +func (m *ProjectMutation) GithubPermissionsCleared() bool { + _, ok := m.clearedFields[project.FieldGithubPermissions] + return ok +} + +// ResetGithubPermissions resets all changes to the "github_permissions" field. +func (m *ProjectMutation) ResetGithubPermissions() { + m.github_permissions = nil + delete(m.clearedFields, project.FieldGithubPermissions) +} + +// SetGithubAppStatus sets the "github_app_status" field. +func (m *ProjectMutation) SetGithubAppStatus(s string) { + m.github_app_status = &s +} + +// GithubAppStatus returns the value of the "github_app_status" field in the mutation. +func (m *ProjectMutation) GithubAppStatus() (r string, exists bool) { + v := m.github_app_status + if v == nil { + return + } + return *v, true +} + +// OldGithubAppStatus returns the old "github_app_status" field's value of the Project entity. +// If the Project object wasn't provided to the builder, the object is fetched from the database. +// An error is returned if the mutation operation is not UpdateOne, or the database query fails. +func (m *ProjectMutation) OldGithubAppStatus(ctx context.Context) (v string, err error) { + if !m.op.Is(OpUpdateOne) { + return v, errors.New("OldGithubAppStatus is only allowed on UpdateOne operations") + } + if m.id == nil || m.oldValue == nil { + return v, errors.New("OldGithubAppStatus requires an ID field in the mutation") + } + oldValue, err := m.oldValue(ctx) + if err != nil { + return v, fmt.Errorf("querying old value for OldGithubAppStatus: %w", err) + } + return oldValue.GithubAppStatus, nil +} + +// ClearGithubAppStatus clears the value of the "github_app_status" field. +func (m *ProjectMutation) ClearGithubAppStatus() { + m.github_app_status = nil + m.clearedFields[project.FieldGithubAppStatus] = struct{}{} +} + +// GithubAppStatusCleared returns if the "github_app_status" field was cleared in this mutation. +func (m *ProjectMutation) GithubAppStatusCleared() bool { + _, ok := m.clearedFields[project.FieldGithubAppStatus] + return ok +} + +// ResetGithubAppStatus resets all changes to the "github_app_status" field. +func (m *ProjectMutation) ResetGithubAppStatus() { + m.github_app_status = nil + delete(m.clearedFields, project.FieldGithubAppStatus) +} + +// SetGitIdentity sets the "git_identity" field. +func (m *ProjectMutation) SetGitIdentity(s string) { + m.git_identity = &s +} + +// GitIdentity returns the value of the "git_identity" field in the mutation. +func (m *ProjectMutation) GitIdentity() (r string, exists bool) { + v := m.git_identity + if v == nil { + return + } + return *v, true +} + +// OldGitIdentity returns the old "git_identity" field's value of the Project entity. +// If the Project object wasn't provided to the builder, the object is fetched from the database. +// An error is returned if the mutation operation is not UpdateOne, or the database query fails. +func (m *ProjectMutation) OldGitIdentity(ctx context.Context) (v string, err error) { + if !m.op.Is(OpUpdateOne) { + return v, errors.New("OldGitIdentity is only allowed on UpdateOne operations") + } + if m.id == nil || m.oldValue == nil { + return v, errors.New("OldGitIdentity requires an ID field in the mutation") + } + oldValue, err := m.oldValue(ctx) + if err != nil { + return v, fmt.Errorf("querying old value for OldGitIdentity: %w", err) + } + return oldValue.GitIdentity, nil +} + +// ClearGitIdentity clears the value of the "git_identity" field. +func (m *ProjectMutation) ClearGitIdentity() { + m.git_identity = nil + m.clearedFields[project.FieldGitIdentity] = struct{}{} +} + +// GitIdentityCleared returns if the "git_identity" field was cleared in this mutation. +func (m *ProjectMutation) GitIdentityCleared() bool { + _, ok := m.clearedFields[project.FieldGitIdentity] + return ok +} + +// ResetGitIdentity resets all changes to the "git_identity" field. +func (m *ProjectMutation) ResetGitIdentity() { + m.git_identity = nil + delete(m.clearedFields, project.FieldGitIdentity) +} + +// AddAgentIDs adds the "agents" edge to the Agent entity by ids. +func (m *ProjectMutation) AddAgentIDs(ids ...uuid.UUID) { + if m.agents == nil { + m.agents = make(map[uuid.UUID]struct{}) + } + for i := range ids { + m.agents[ids[i]] = struct{}{} + } +} + +// ClearAgents clears the "agents" edge to the Agent entity. +func (m *ProjectMutation) ClearAgents() { + m.clearedagents = true +} + +// AgentsCleared reports if the "agents" edge to the Agent entity was cleared. +func (m *ProjectMutation) AgentsCleared() bool { + return m.clearedagents +} + +// RemoveAgentIDs removes the "agents" edge to the Agent entity by IDs. +func (m *ProjectMutation) RemoveAgentIDs(ids ...uuid.UUID) { + if m.removedagents == nil { + m.removedagents = make(map[uuid.UUID]struct{}) + } + for i := range ids { + delete(m.agents, ids[i]) + m.removedagents[ids[i]] = struct{}{} + } +} + +// RemovedAgents returns the removed IDs of the "agents" edge to the Agent entity. +func (m *ProjectMutation) RemovedAgentsIDs() (ids []uuid.UUID) { + for id := range m.removedagents { + ids = append(ids, id) + } + return +} + +// AgentsIDs returns the "agents" edge IDs in the mutation. +func (m *ProjectMutation) AgentsIDs() (ids []uuid.UUID) { + for id := range m.agents { + ids = append(ids, id) + } + return +} + +// ResetAgents resets all changes to the "agents" edge. +func (m *ProjectMutation) ResetAgents() { + m.agents = nil + m.clearedagents = false + m.removedagents = nil +} + +// Where appends a list predicates to the ProjectMutation builder. +func (m *ProjectMutation) Where(ps ...predicate.Project) { + m.predicates = append(m.predicates, ps...) +} + +// WhereP appends storage-level predicates to the ProjectMutation builder. Using this method, +// users can use type-assertion to append predicates that do not depend on any generated package. +func (m *ProjectMutation) WhereP(ps ...func(*sql.Selector)) { + p := make([]predicate.Project, len(ps)) + for i := range ps { + p[i] = ps[i] + } + m.Where(p...) +} + +// Op returns the operation name. +func (m *ProjectMutation) Op() Op { + return m.op +} + +// SetOp allows setting the mutation operation. +func (m *ProjectMutation) SetOp(op Op) { + m.op = op +} + +// Type returns the node type of this mutation (Project). +func (m *ProjectMutation) Type() string { + return m.typ +} + +// Fields returns all fields that were changed during this mutation. Note that in +// order to get all numeric fields that were incremented/decremented, call +// AddedFields(). +func (m *ProjectMutation) Fields() []string { + fields := make([]string, 0, 16) + if m.name != nil { + fields = append(fields, project.FieldName) + } + if m.slug != nil { + fields = append(fields, project.FieldSlug) + } + if m.git_remote != nil { + fields = append(fields, project.FieldGitRemote) + } + if m.default_runtime_broker_id != nil { + fields = append(fields, project.FieldDefaultRuntimeBrokerID) + } + if m.labels != nil { + fields = append(fields, project.FieldLabels) + } + if m.annotations != nil { + fields = append(fields, project.FieldAnnotations) + } + if m.shared_dirs != nil { + fields = append(fields, project.FieldSharedDirs) + } + if m.created != nil { + fields = append(fields, project.FieldCreated) + } + if m.updated != nil { + fields = append(fields, project.FieldUpdated) + } + if m.created_by != nil { + fields = append(fields, project.FieldCreatedBy) + } + if m.owner_id != nil { + fields = append(fields, project.FieldOwnerID) + } + if m.visibility != nil { + fields = append(fields, project.FieldVisibility) + } + if m.github_installation_id != nil { + fields = append(fields, project.FieldGithubInstallationID) + } + if m.github_permissions != nil { + fields = append(fields, project.FieldGithubPermissions) + } + if m.github_app_status != nil { + fields = append(fields, project.FieldGithubAppStatus) + } + if m.git_identity != nil { + fields = append(fields, project.FieldGitIdentity) + } + return fields +} + +// Field returns the value of a field with the given name. The second boolean +// return value indicates that this field was not set, or was not defined in the +// schema. +func (m *ProjectMutation) Field(name string) (ent.Value, bool) { + switch name { + case project.FieldName: + return m.Name() + case project.FieldSlug: + return m.Slug() + case project.FieldGitRemote: + return m.GitRemote() + case project.FieldDefaultRuntimeBrokerID: + return m.DefaultRuntimeBrokerID() + case project.FieldLabels: + return m.Labels() + case project.FieldAnnotations: + return m.Annotations() + case project.FieldSharedDirs: + return m.SharedDirs() + case project.FieldCreated: + return m.Created() + case project.FieldUpdated: + return m.Updated() + case project.FieldCreatedBy: + return m.CreatedBy() + case project.FieldOwnerID: + return m.OwnerID() + case project.FieldVisibility: + return m.Visibility() + case project.FieldGithubInstallationID: + return m.GithubInstallationID() + case project.FieldGithubPermissions: + return m.GithubPermissions() + case project.FieldGithubAppStatus: + return m.GithubAppStatus() + case project.FieldGitIdentity: + return m.GitIdentity() + } + return nil, false +} + +// OldField returns the old value of the field from the database. An error is +// returned if the mutation operation is not UpdateOne, or the query to the +// database failed. +func (m *ProjectMutation) OldField(ctx context.Context, name string) (ent.Value, error) { + switch name { + case project.FieldName: + return m.OldName(ctx) + case project.FieldSlug: + return m.OldSlug(ctx) + case project.FieldGitRemote: + return m.OldGitRemote(ctx) + case project.FieldDefaultRuntimeBrokerID: + return m.OldDefaultRuntimeBrokerID(ctx) + case project.FieldLabels: + return m.OldLabels(ctx) + case project.FieldAnnotations: + return m.OldAnnotations(ctx) + case project.FieldSharedDirs: + return m.OldSharedDirs(ctx) + case project.FieldCreated: + return m.OldCreated(ctx) + case project.FieldUpdated: + return m.OldUpdated(ctx) + case project.FieldCreatedBy: + return m.OldCreatedBy(ctx) + case project.FieldOwnerID: + return m.OldOwnerID(ctx) + case project.FieldVisibility: + return m.OldVisibility(ctx) + case project.FieldGithubInstallationID: + return m.OldGithubInstallationID(ctx) + case project.FieldGithubPermissions: + return m.OldGithubPermissions(ctx) + case project.FieldGithubAppStatus: + return m.OldGithubAppStatus(ctx) + case project.FieldGitIdentity: + return m.OldGitIdentity(ctx) + } + return nil, fmt.Errorf("unknown Project field %s", name) +} + +// SetField sets the value of a field with the given name. It returns an error if +// the field is not defined in the schema, or if the type mismatched the field +// type. +func (m *ProjectMutation) SetField(name string, value ent.Value) error { + switch name { + case project.FieldName: + v, ok := value.(string) + if !ok { + return fmt.Errorf("unexpected type %T for field %s", value, name) + } + m.SetName(v) + return nil + case project.FieldSlug: + v, ok := value.(string) + if !ok { + return fmt.Errorf("unexpected type %T for field %s", value, name) + } + m.SetSlug(v) + return nil + case project.FieldGitRemote: + v, ok := value.(string) + if !ok { + return fmt.Errorf("unexpected type %T for field %s", value, name) + } + m.SetGitRemote(v) + return nil + case project.FieldDefaultRuntimeBrokerID: + v, ok := value.(string) + if !ok { + return fmt.Errorf("unexpected type %T for field %s", value, name) + } + m.SetDefaultRuntimeBrokerID(v) + return nil + case project.FieldLabels: + v, ok := value.(map[string]string) + if !ok { + return fmt.Errorf("unexpected type %T for field %s", value, name) + } + m.SetLabels(v) + return nil + case project.FieldAnnotations: + v, ok := value.(map[string]string) + if !ok { + return fmt.Errorf("unexpected type %T for field %s", value, name) + } + m.SetAnnotations(v) + return nil + case project.FieldSharedDirs: + v, ok := value.(string) + if !ok { + return fmt.Errorf("unexpected type %T for field %s", value, name) + } + m.SetSharedDirs(v) + return nil + case project.FieldCreated: + v, ok := value.(time.Time) + if !ok { + return fmt.Errorf("unexpected type %T for field %s", value, name) + } + m.SetCreated(v) + return nil + case project.FieldUpdated: + v, ok := value.(time.Time) + if !ok { + return fmt.Errorf("unexpected type %T for field %s", value, name) + } + m.SetUpdated(v) + return nil + case project.FieldCreatedBy: + v, ok := value.(string) + if !ok { + return fmt.Errorf("unexpected type %T for field %s", value, name) + } + m.SetCreatedBy(v) + return nil + case project.FieldOwnerID: + v, ok := value.(string) + if !ok { + return fmt.Errorf("unexpected type %T for field %s", value, name) + } + m.SetOwnerID(v) + return nil + case project.FieldVisibility: + v, ok := value.(string) + if !ok { + return fmt.Errorf("unexpected type %T for field %s", value, name) + } + m.SetVisibility(v) + return nil + case project.FieldGithubInstallationID: + v, ok := value.(int64) + if !ok { + return fmt.Errorf("unexpected type %T for field %s", value, name) + } + m.SetGithubInstallationID(v) + return nil + case project.FieldGithubPermissions: + v, ok := value.(string) + if !ok { + return fmt.Errorf("unexpected type %T for field %s", value, name) + } + m.SetGithubPermissions(v) + return nil + case project.FieldGithubAppStatus: + v, ok := value.(string) + if !ok { + return fmt.Errorf("unexpected type %T for field %s", value, name) + } + m.SetGithubAppStatus(v) + return nil + case project.FieldGitIdentity: + v, ok := value.(string) + if !ok { + return fmt.Errorf("unexpected type %T for field %s", value, name) + } + m.SetGitIdentity(v) + return nil + } + return fmt.Errorf("unknown Project field %s", name) +} + +// AddedFields returns all numeric fields that were incremented/decremented during +// this mutation. +func (m *ProjectMutation) AddedFields() []string { + var fields []string + if m.addgithub_installation_id != nil { + fields = append(fields, project.FieldGithubInstallationID) + } + return fields +} + +// AddedField returns the numeric value that was incremented/decremented on a field +// with the given name. The second boolean return value indicates that this field +// was not set, or was not defined in the schema. +func (m *ProjectMutation) AddedField(name string) (ent.Value, bool) { + switch name { + case project.FieldGithubInstallationID: + return m.AddedGithubInstallationID() + } + return nil, false +} + +// AddField adds the value to the field with the given name. It returns an error if +// the field is not defined in the schema, or if the type mismatched the field +// type. +func (m *ProjectMutation) AddField(name string, value ent.Value) error { + switch name { + case project.FieldGithubInstallationID: + v, ok := value.(int64) + if !ok { + return fmt.Errorf("unexpected type %T for field %s", value, name) + } + m.AddGithubInstallationID(v) + return nil + } + return fmt.Errorf("unknown Project numeric field %s", name) +} + +// ClearedFields returns all nullable fields that were cleared during this +// mutation. +func (m *ProjectMutation) ClearedFields() []string { + var fields []string + if m.FieldCleared(project.FieldGitRemote) { + fields = append(fields, project.FieldGitRemote) + } + if m.FieldCleared(project.FieldDefaultRuntimeBrokerID) { + fields = append(fields, project.FieldDefaultRuntimeBrokerID) + } + if m.FieldCleared(project.FieldLabels) { + fields = append(fields, project.FieldLabels) + } + if m.FieldCleared(project.FieldAnnotations) { + fields = append(fields, project.FieldAnnotations) + } + if m.FieldCleared(project.FieldSharedDirs) { + fields = append(fields, project.FieldSharedDirs) + } + if m.FieldCleared(project.FieldCreatedBy) { + fields = append(fields, project.FieldCreatedBy) + } + if m.FieldCleared(project.FieldOwnerID) { + fields = append(fields, project.FieldOwnerID) + } + if m.FieldCleared(project.FieldGithubInstallationID) { + fields = append(fields, project.FieldGithubInstallationID) + } + if m.FieldCleared(project.FieldGithubPermissions) { + fields = append(fields, project.FieldGithubPermissions) + } + if m.FieldCleared(project.FieldGithubAppStatus) { + fields = append(fields, project.FieldGithubAppStatus) + } + if m.FieldCleared(project.FieldGitIdentity) { + fields = append(fields, project.FieldGitIdentity) + } + return fields +} + +// FieldCleared returns a boolean indicating if a field with the given name was +// cleared in this mutation. +func (m *ProjectMutation) FieldCleared(name string) bool { + _, ok := m.clearedFields[name] + return ok +} + +// ClearField clears the value of the field with the given name. It returns an +// error if the field is not defined in the schema. +func (m *ProjectMutation) ClearField(name string) error { + switch name { + case project.FieldGitRemote: + m.ClearGitRemote() + return nil + case project.FieldDefaultRuntimeBrokerID: + m.ClearDefaultRuntimeBrokerID() + return nil + case project.FieldLabels: + m.ClearLabels() + return nil + case project.FieldAnnotations: + m.ClearAnnotations() + return nil + case project.FieldSharedDirs: + m.ClearSharedDirs() + return nil + case project.FieldCreatedBy: + m.ClearCreatedBy() + return nil + case project.FieldOwnerID: + m.ClearOwnerID() + return nil + case project.FieldGithubInstallationID: + m.ClearGithubInstallationID() + return nil + case project.FieldGithubPermissions: + m.ClearGithubPermissions() + return nil + case project.FieldGithubAppStatus: + m.ClearGithubAppStatus() + return nil + case project.FieldGitIdentity: + m.ClearGitIdentity() + return nil + } + return fmt.Errorf("unknown Project nullable field %s", name) +} + +// ResetField resets all changes in the mutation for the field with the given name. +// It returns an error if the field is not defined in the schema. +func (m *ProjectMutation) ResetField(name string) error { + switch name { + case project.FieldName: + m.ResetName() + return nil + case project.FieldSlug: + m.ResetSlug() + return nil + case project.FieldGitRemote: + m.ResetGitRemote() + return nil + case project.FieldDefaultRuntimeBrokerID: + m.ResetDefaultRuntimeBrokerID() + return nil + case project.FieldLabels: + m.ResetLabels() + return nil + case project.FieldAnnotations: + m.ResetAnnotations() + return nil + case project.FieldSharedDirs: + m.ResetSharedDirs() + return nil + case project.FieldCreated: + m.ResetCreated() + return nil + case project.FieldUpdated: + m.ResetUpdated() + return nil + case project.FieldCreatedBy: + m.ResetCreatedBy() + return nil + case project.FieldOwnerID: + m.ResetOwnerID() + return nil + case project.FieldVisibility: + m.ResetVisibility() + return nil + case project.FieldGithubInstallationID: + m.ResetGithubInstallationID() + return nil + case project.FieldGithubPermissions: + m.ResetGithubPermissions() + return nil + case project.FieldGithubAppStatus: + m.ResetGithubAppStatus() + return nil + case project.FieldGitIdentity: + m.ResetGitIdentity() + return nil + } + return fmt.Errorf("unknown Project field %s", name) +} + +// AddedEdges returns all edge names that were set/added in this mutation. +func (m *ProjectMutation) AddedEdges() []string { + edges := make([]string, 0, 1) + if m.agents != nil { + edges = append(edges, project.EdgeAgents) + } + return edges +} + +// AddedIDs returns all IDs (to other nodes) that were added for the given edge +// name in this mutation. +func (m *ProjectMutation) AddedIDs(name string) []ent.Value { + switch name { + case project.EdgeAgents: + ids := make([]ent.Value, 0, len(m.agents)) + for id := range m.agents { + ids = append(ids, id) + } + return ids + } + return nil +} + +// RemovedEdges returns all edge names that were removed in this mutation. +func (m *ProjectMutation) RemovedEdges() []string { + edges := make([]string, 0, 1) + if m.removedagents != nil { + edges = append(edges, project.EdgeAgents) + } + return edges +} + +// RemovedIDs returns all IDs (to other nodes) that were removed for the edge with +// the given name in this mutation. +func (m *ProjectMutation) RemovedIDs(name string) []ent.Value { + switch name { + case project.EdgeAgents: + ids := make([]ent.Value, 0, len(m.removedagents)) + for id := range m.removedagents { + ids = append(ids, id) + } + return ids + } + return nil +} + +// ClearedEdges returns all edge names that were cleared in this mutation. +func (m *ProjectMutation) ClearedEdges() []string { + edges := make([]string, 0, 1) + if m.clearedagents { + edges = append(edges, project.EdgeAgents) + } + return edges +} + +// EdgeCleared returns a boolean which indicates if the edge with the given name +// was cleared in this mutation. +func (m *ProjectMutation) EdgeCleared(name string) bool { + switch name { + case project.EdgeAgents: + return m.clearedagents + } + return false +} + +// ClearEdge clears the value of the edge with the given name. It returns an error +// if that edge is not defined in the schema. +func (m *ProjectMutation) ClearEdge(name string) error { + switch name { + } + return fmt.Errorf("unknown Project unique edge %s", name) +} + +// ResetEdge resets all changes to the edge with the given name in this mutation. +// It returns an error if the edge is not defined in the schema. +func (m *ProjectMutation) ResetEdge(name string) error { + switch name { + case project.EdgeAgents: + m.ResetAgents() + return nil + } + return fmt.Errorf("unknown Project edge %s", name) +} + +// ProjectContributorMutation represents an operation that mutates the ProjectContributor nodes in the graph. +type ProjectContributorMutation struct { + config + op Op + typ string + id *uuid.UUID + project_id *uuid.UUID + broker_id *uuid.UUID + broker_name *string + mode *string + status *string + profiles *string + last_seen *time.Time + local_path *string + linked_by *string + linked_at *time.Time + clearedFields map[string]struct{} + done bool + oldValue func(context.Context) (*ProjectContributor, error) + predicates []predicate.ProjectContributor +} + +var _ ent.Mutation = (*ProjectContributorMutation)(nil) + +// projectcontributorOption allows management of the mutation configuration using functional options. +type projectcontributorOption func(*ProjectContributorMutation) + +// newProjectContributorMutation creates new mutation for the ProjectContributor entity. +func newProjectContributorMutation(c config, op Op, opts ...projectcontributorOption) *ProjectContributorMutation { + m := &ProjectContributorMutation{ + config: c, + op: op, + typ: TypeProjectContributor, + clearedFields: make(map[string]struct{}), + } + for _, opt := range opts { + opt(m) + } + return m +} + +// withProjectContributorID sets the ID field of the mutation. +func withProjectContributorID(id uuid.UUID) projectcontributorOption { + return func(m *ProjectContributorMutation) { + var ( + err error + once sync.Once + value *ProjectContributor + ) + m.oldValue = func(ctx context.Context) (*ProjectContributor, error) { + once.Do(func() { + if m.done { + err = errors.New("querying old values post mutation is not allowed") + } else { + value, err = m.Client().ProjectContributor.Get(ctx, id) + } + }) + return value, err + } + m.id = &id + } +} + +// withProjectContributor sets the old ProjectContributor of the mutation. +func withProjectContributor(node *ProjectContributor) projectcontributorOption { + return func(m *ProjectContributorMutation) { + m.oldValue = func(context.Context) (*ProjectContributor, error) { + return node, nil + } + m.id = &node.ID + } +} + +// Client returns a new `ent.Client` from the mutation. If the mutation was +// executed in a transaction (ent.Tx), a transactional client is returned. +func (m ProjectContributorMutation) Client() *Client { + client := &Client{config: m.config} + client.init() + return client +} + +// Tx returns an `ent.Tx` for mutations that were executed in transactions; +// it returns an error otherwise. +func (m ProjectContributorMutation) Tx() (*Tx, error) { + if _, ok := m.driver.(*txDriver); !ok { + return nil, errors.New("ent: mutation is not running in a transaction") + } + tx := &Tx{config: m.config} + tx.init() + return tx, nil +} + +// SetID sets the value of the id field. Note that this +// operation is only accepted on creation of ProjectContributor entities. +func (m *ProjectContributorMutation) SetID(id uuid.UUID) { + m.id = &id +} + +// ID returns the ID value in the mutation. Note that the ID is only available +// if it was provided to the builder or after it was returned from the database. +func (m *ProjectContributorMutation) ID() (id uuid.UUID, exists bool) { + if m.id == nil { + return + } + return *m.id, true +} + +// IDs queries the database and returns the entity ids that match the mutation's predicate. +// That means, if the mutation is applied within a transaction with an isolation level such +// as sql.LevelSerializable, the returned ids match the ids of the rows that will be updated +// or updated by the mutation. +func (m *ProjectContributorMutation) IDs(ctx context.Context) ([]uuid.UUID, error) { + switch { + case m.op.Is(OpUpdateOne | OpDeleteOne): + id, exists := m.ID() + if exists { + return []uuid.UUID{id}, nil + } + fallthrough + case m.op.Is(OpUpdate | OpDelete): + return m.Client().ProjectContributor.Query().Where(m.predicates...).IDs(ctx) + default: + return nil, fmt.Errorf("IDs is not allowed on %s operations", m.op) + } +} + +// SetProjectID sets the "project_id" field. +func (m *ProjectContributorMutation) SetProjectID(u uuid.UUID) { + m.project_id = &u +} + +// ProjectID returns the value of the "project_id" field in the mutation. +func (m *ProjectContributorMutation) ProjectID() (r uuid.UUID, exists bool) { + v := m.project_id + if v == nil { + return + } + return *v, true +} + +// OldProjectID returns the old "project_id" field's value of the ProjectContributor entity. +// If the ProjectContributor object wasn't provided to the builder, the object is fetched from the database. +// An error is returned if the mutation operation is not UpdateOne, or the database query fails. +func (m *ProjectContributorMutation) OldProjectID(ctx context.Context) (v uuid.UUID, err error) { + if !m.op.Is(OpUpdateOne) { + return v, errors.New("OldProjectID is only allowed on UpdateOne operations") + } + if m.id == nil || m.oldValue == nil { + return v, errors.New("OldProjectID requires an ID field in the mutation") + } + oldValue, err := m.oldValue(ctx) + if err != nil { + return v, fmt.Errorf("querying old value for OldProjectID: %w", err) + } + return oldValue.ProjectID, nil +} + +// ResetProjectID resets all changes to the "project_id" field. +func (m *ProjectContributorMutation) ResetProjectID() { + m.project_id = nil +} + +// SetBrokerID sets the "broker_id" field. +func (m *ProjectContributorMutation) SetBrokerID(u uuid.UUID) { + m.broker_id = &u +} + +// BrokerID returns the value of the "broker_id" field in the mutation. +func (m *ProjectContributorMutation) BrokerID() (r uuid.UUID, exists bool) { + v := m.broker_id + if v == nil { + return + } + return *v, true +} + +// OldBrokerID returns the old "broker_id" field's value of the ProjectContributor entity. +// If the ProjectContributor object wasn't provided to the builder, the object is fetched from the database. +// An error is returned if the mutation operation is not UpdateOne, or the database query fails. +func (m *ProjectContributorMutation) OldBrokerID(ctx context.Context) (v uuid.UUID, err error) { + if !m.op.Is(OpUpdateOne) { + return v, errors.New("OldBrokerID is only allowed on UpdateOne operations") + } + if m.id == nil || m.oldValue == nil { + return v, errors.New("OldBrokerID requires an ID field in the mutation") + } + oldValue, err := m.oldValue(ctx) + if err != nil { + return v, fmt.Errorf("querying old value for OldBrokerID: %w", err) + } + return oldValue.BrokerID, nil +} + +// ResetBrokerID resets all changes to the "broker_id" field. +func (m *ProjectContributorMutation) ResetBrokerID() { + m.broker_id = nil +} + +// SetBrokerName sets the "broker_name" field. +func (m *ProjectContributorMutation) SetBrokerName(s string) { + m.broker_name = &s +} + +// BrokerName returns the value of the "broker_name" field in the mutation. +func (m *ProjectContributorMutation) BrokerName() (r string, exists bool) { + v := m.broker_name + if v == nil { + return + } + return *v, true +} + +// OldBrokerName returns the old "broker_name" field's value of the ProjectContributor entity. +// If the ProjectContributor object wasn't provided to the builder, the object is fetched from the database. +// An error is returned if the mutation operation is not UpdateOne, or the database query fails. +func (m *ProjectContributorMutation) OldBrokerName(ctx context.Context) (v string, err error) { + if !m.op.Is(OpUpdateOne) { + return v, errors.New("OldBrokerName is only allowed on UpdateOne operations") + } + if m.id == nil || m.oldValue == nil { + return v, errors.New("OldBrokerName requires an ID field in the mutation") + } + oldValue, err := m.oldValue(ctx) + if err != nil { + return v, fmt.Errorf("querying old value for OldBrokerName: %w", err) + } + return oldValue.BrokerName, nil +} + +// ResetBrokerName resets all changes to the "broker_name" field. +func (m *ProjectContributorMutation) ResetBrokerName() { + m.broker_name = nil +} + +// SetMode sets the "mode" field. +func (m *ProjectContributorMutation) SetMode(s string) { + m.mode = &s +} + +// Mode returns the value of the "mode" field in the mutation. +func (m *ProjectContributorMutation) Mode() (r string, exists bool) { + v := m.mode + if v == nil { + return + } + return *v, true +} + +// OldMode returns the old "mode" field's value of the ProjectContributor entity. +// If the ProjectContributor object wasn't provided to the builder, the object is fetched from the database. +// An error is returned if the mutation operation is not UpdateOne, or the database query fails. +func (m *ProjectContributorMutation) OldMode(ctx context.Context) (v string, err error) { + if !m.op.Is(OpUpdateOne) { + return v, errors.New("OldMode is only allowed on UpdateOne operations") + } + if m.id == nil || m.oldValue == nil { + return v, errors.New("OldMode requires an ID field in the mutation") + } + oldValue, err := m.oldValue(ctx) + if err != nil { + return v, fmt.Errorf("querying old value for OldMode: %w", err) + } + return oldValue.Mode, nil +} + +// ResetMode resets all changes to the "mode" field. +func (m *ProjectContributorMutation) ResetMode() { + m.mode = nil +} + +// SetStatus sets the "status" field. +func (m *ProjectContributorMutation) SetStatus(s string) { + m.status = &s +} + +// Status returns the value of the "status" field in the mutation. +func (m *ProjectContributorMutation) Status() (r string, exists bool) { + v := m.status + if v == nil { + return + } + return *v, true +} + +// OldStatus returns the old "status" field's value of the ProjectContributor entity. +// If the ProjectContributor object wasn't provided to the builder, the object is fetched from the database. +// An error is returned if the mutation operation is not UpdateOne, or the database query fails. +func (m *ProjectContributorMutation) OldStatus(ctx context.Context) (v string, err error) { + if !m.op.Is(OpUpdateOne) { + return v, errors.New("OldStatus is only allowed on UpdateOne operations") + } + if m.id == nil || m.oldValue == nil { + return v, errors.New("OldStatus requires an ID field in the mutation") + } + oldValue, err := m.oldValue(ctx) + if err != nil { + return v, fmt.Errorf("querying old value for OldStatus: %w", err) + } + return oldValue.Status, nil +} + +// ResetStatus resets all changes to the "status" field. +func (m *ProjectContributorMutation) ResetStatus() { + m.status = nil +} + +// SetProfiles sets the "profiles" field. +func (m *ProjectContributorMutation) SetProfiles(s string) { + m.profiles = &s +} + +// Profiles returns the value of the "profiles" field in the mutation. +func (m *ProjectContributorMutation) Profiles() (r string, exists bool) { + v := m.profiles + if v == nil { + return + } + return *v, true +} + +// OldProfiles returns the old "profiles" field's value of the ProjectContributor entity. +// If the ProjectContributor object wasn't provided to the builder, the object is fetched from the database. +// An error is returned if the mutation operation is not UpdateOne, or the database query fails. +func (m *ProjectContributorMutation) OldProfiles(ctx context.Context) (v string, err error) { + if !m.op.Is(OpUpdateOne) { + return v, errors.New("OldProfiles is only allowed on UpdateOne operations") + } + if m.id == nil || m.oldValue == nil { + return v, errors.New("OldProfiles requires an ID field in the mutation") + } + oldValue, err := m.oldValue(ctx) + if err != nil { + return v, fmt.Errorf("querying old value for OldProfiles: %w", err) + } + return oldValue.Profiles, nil +} + +// ClearProfiles clears the value of the "profiles" field. +func (m *ProjectContributorMutation) ClearProfiles() { + m.profiles = nil + m.clearedFields[projectcontributor.FieldProfiles] = struct{}{} +} + +// ProfilesCleared returns if the "profiles" field was cleared in this mutation. +func (m *ProjectContributorMutation) ProfilesCleared() bool { + _, ok := m.clearedFields[projectcontributor.FieldProfiles] + return ok +} + +// ResetProfiles resets all changes to the "profiles" field. +func (m *ProjectContributorMutation) ResetProfiles() { + m.profiles = nil + delete(m.clearedFields, projectcontributor.FieldProfiles) +} + +// SetLastSeen sets the "last_seen" field. +func (m *ProjectContributorMutation) SetLastSeen(t time.Time) { + m.last_seen = &t +} + +// LastSeen returns the value of the "last_seen" field in the mutation. +func (m *ProjectContributorMutation) LastSeen() (r time.Time, exists bool) { + v := m.last_seen + if v == nil { + return + } + return *v, true +} + +// OldLastSeen returns the old "last_seen" field's value of the ProjectContributor entity. +// If the ProjectContributor object wasn't provided to the builder, the object is fetched from the database. +// An error is returned if the mutation operation is not UpdateOne, or the database query fails. +func (m *ProjectContributorMutation) OldLastSeen(ctx context.Context) (v *time.Time, err error) { + if !m.op.Is(OpUpdateOne) { + return v, errors.New("OldLastSeen is only allowed on UpdateOne operations") + } + if m.id == nil || m.oldValue == nil { + return v, errors.New("OldLastSeen requires an ID field in the mutation") + } + oldValue, err := m.oldValue(ctx) + if err != nil { + return v, fmt.Errorf("querying old value for OldLastSeen: %w", err) + } + return oldValue.LastSeen, nil +} + +// ClearLastSeen clears the value of the "last_seen" field. +func (m *ProjectContributorMutation) ClearLastSeen() { + m.last_seen = nil + m.clearedFields[projectcontributor.FieldLastSeen] = struct{}{} +} + +// LastSeenCleared returns if the "last_seen" field was cleared in this mutation. +func (m *ProjectContributorMutation) LastSeenCleared() bool { + _, ok := m.clearedFields[projectcontributor.FieldLastSeen] + return ok +} + +// ResetLastSeen resets all changes to the "last_seen" field. +func (m *ProjectContributorMutation) ResetLastSeen() { + m.last_seen = nil + delete(m.clearedFields, projectcontributor.FieldLastSeen) +} + +// SetLocalPath sets the "local_path" field. +func (m *ProjectContributorMutation) SetLocalPath(s string) { + m.local_path = &s +} + +// LocalPath returns the value of the "local_path" field in the mutation. +func (m *ProjectContributorMutation) LocalPath() (r string, exists bool) { + v := m.local_path + if v == nil { + return + } + return *v, true +} + +// OldLocalPath returns the old "local_path" field's value of the ProjectContributor entity. +// If the ProjectContributor object wasn't provided to the builder, the object is fetched from the database. +// An error is returned if the mutation operation is not UpdateOne, or the database query fails. +func (m *ProjectContributorMutation) OldLocalPath(ctx context.Context) (v string, err error) { + if !m.op.Is(OpUpdateOne) { + return v, errors.New("OldLocalPath is only allowed on UpdateOne operations") + } + if m.id == nil || m.oldValue == nil { + return v, errors.New("OldLocalPath requires an ID field in the mutation") + } + oldValue, err := m.oldValue(ctx) + if err != nil { + return v, fmt.Errorf("querying old value for OldLocalPath: %w", err) + } + return oldValue.LocalPath, nil +} + +// ClearLocalPath clears the value of the "local_path" field. +func (m *ProjectContributorMutation) ClearLocalPath() { + m.local_path = nil + m.clearedFields[projectcontributor.FieldLocalPath] = struct{}{} +} + +// LocalPathCleared returns if the "local_path" field was cleared in this mutation. +func (m *ProjectContributorMutation) LocalPathCleared() bool { + _, ok := m.clearedFields[projectcontributor.FieldLocalPath] + return ok +} + +// ResetLocalPath resets all changes to the "local_path" field. +func (m *ProjectContributorMutation) ResetLocalPath() { + m.local_path = nil + delete(m.clearedFields, projectcontributor.FieldLocalPath) +} + +// SetLinkedBy sets the "linked_by" field. +func (m *ProjectContributorMutation) SetLinkedBy(s string) { + m.linked_by = &s +} + +// LinkedBy returns the value of the "linked_by" field in the mutation. +func (m *ProjectContributorMutation) LinkedBy() (r string, exists bool) { + v := m.linked_by + if v == nil { + return + } + return *v, true +} + +// OldLinkedBy returns the old "linked_by" field's value of the ProjectContributor entity. +// If the ProjectContributor object wasn't provided to the builder, the object is fetched from the database. +// An error is returned if the mutation operation is not UpdateOne, or the database query fails. +func (m *ProjectContributorMutation) OldLinkedBy(ctx context.Context) (v string, err error) { + if !m.op.Is(OpUpdateOne) { + return v, errors.New("OldLinkedBy is only allowed on UpdateOne operations") + } + if m.id == nil || m.oldValue == nil { + return v, errors.New("OldLinkedBy requires an ID field in the mutation") + } + oldValue, err := m.oldValue(ctx) + if err != nil { + return v, fmt.Errorf("querying old value for OldLinkedBy: %w", err) + } + return oldValue.LinkedBy, nil +} + +// ClearLinkedBy clears the value of the "linked_by" field. +func (m *ProjectContributorMutation) ClearLinkedBy() { + m.linked_by = nil + m.clearedFields[projectcontributor.FieldLinkedBy] = struct{}{} +} + +// LinkedByCleared returns if the "linked_by" field was cleared in this mutation. +func (m *ProjectContributorMutation) LinkedByCleared() bool { + _, ok := m.clearedFields[projectcontributor.FieldLinkedBy] + return ok +} + +// ResetLinkedBy resets all changes to the "linked_by" field. +func (m *ProjectContributorMutation) ResetLinkedBy() { + m.linked_by = nil + delete(m.clearedFields, projectcontributor.FieldLinkedBy) +} + +// SetLinkedAt sets the "linked_at" field. +func (m *ProjectContributorMutation) SetLinkedAt(t time.Time) { + m.linked_at = &t +} + +// LinkedAt returns the value of the "linked_at" field in the mutation. +func (m *ProjectContributorMutation) LinkedAt() (r time.Time, exists bool) { + v := m.linked_at + if v == nil { + return + } + return *v, true +} + +// OldLinkedAt returns the old "linked_at" field's value of the ProjectContributor entity. +// If the ProjectContributor object wasn't provided to the builder, the object is fetched from the database. +// An error is returned if the mutation operation is not UpdateOne, or the database query fails. +func (m *ProjectContributorMutation) OldLinkedAt(ctx context.Context) (v *time.Time, err error) { + if !m.op.Is(OpUpdateOne) { + return v, errors.New("OldLinkedAt is only allowed on UpdateOne operations") + } + if m.id == nil || m.oldValue == nil { + return v, errors.New("OldLinkedAt requires an ID field in the mutation") + } + oldValue, err := m.oldValue(ctx) + if err != nil { + return v, fmt.Errorf("querying old value for OldLinkedAt: %w", err) + } + return oldValue.LinkedAt, nil +} + +// ClearLinkedAt clears the value of the "linked_at" field. +func (m *ProjectContributorMutation) ClearLinkedAt() { + m.linked_at = nil + m.clearedFields[projectcontributor.FieldLinkedAt] = struct{}{} +} + +// LinkedAtCleared returns if the "linked_at" field was cleared in this mutation. +func (m *ProjectContributorMutation) LinkedAtCleared() bool { + _, ok := m.clearedFields[projectcontributor.FieldLinkedAt] + return ok +} + +// ResetLinkedAt resets all changes to the "linked_at" field. +func (m *ProjectContributorMutation) ResetLinkedAt() { + m.linked_at = nil + delete(m.clearedFields, projectcontributor.FieldLinkedAt) +} + +// Where appends a list predicates to the ProjectContributorMutation builder. +func (m *ProjectContributorMutation) Where(ps ...predicate.ProjectContributor) { + m.predicates = append(m.predicates, ps...) +} + +// WhereP appends storage-level predicates to the ProjectContributorMutation builder. Using this method, +// users can use type-assertion to append predicates that do not depend on any generated package. +func (m *ProjectContributorMutation) WhereP(ps ...func(*sql.Selector)) { + p := make([]predicate.ProjectContributor, len(ps)) + for i := range ps { + p[i] = ps[i] + } + m.Where(p...) +} + +// Op returns the operation name. +func (m *ProjectContributorMutation) Op() Op { + return m.op +} + +// SetOp allows setting the mutation operation. +func (m *ProjectContributorMutation) SetOp(op Op) { + m.op = op +} + +// Type returns the node type of this mutation (ProjectContributor). +func (m *ProjectContributorMutation) Type() string { + return m.typ +} + +// Fields returns all fields that were changed during this mutation. Note that in +// order to get all numeric fields that were incremented/decremented, call +// AddedFields(). +func (m *ProjectContributorMutation) Fields() []string { + fields := make([]string, 0, 10) + if m.project_id != nil { + fields = append(fields, projectcontributor.FieldProjectID) + } + if m.broker_id != nil { + fields = append(fields, projectcontributor.FieldBrokerID) + } + if m.broker_name != nil { + fields = append(fields, projectcontributor.FieldBrokerName) + } + if m.mode != nil { + fields = append(fields, projectcontributor.FieldMode) + } + if m.status != nil { + fields = append(fields, projectcontributor.FieldStatus) + } + if m.profiles != nil { + fields = append(fields, projectcontributor.FieldProfiles) + } + if m.last_seen != nil { + fields = append(fields, projectcontributor.FieldLastSeen) + } + if m.local_path != nil { + fields = append(fields, projectcontributor.FieldLocalPath) + } + if m.linked_by != nil { + fields = append(fields, projectcontributor.FieldLinkedBy) + } + if m.linked_at != nil { + fields = append(fields, projectcontributor.FieldLinkedAt) + } + return fields +} + +// Field returns the value of a field with the given name. The second boolean +// return value indicates that this field was not set, or was not defined in the +// schema. +func (m *ProjectContributorMutation) Field(name string) (ent.Value, bool) { + switch name { + case projectcontributor.FieldProjectID: + return m.ProjectID() + case projectcontributor.FieldBrokerID: + return m.BrokerID() + case projectcontributor.FieldBrokerName: + return m.BrokerName() + case projectcontributor.FieldMode: + return m.Mode() + case projectcontributor.FieldStatus: + return m.Status() + case projectcontributor.FieldProfiles: + return m.Profiles() + case projectcontributor.FieldLastSeen: + return m.LastSeen() + case projectcontributor.FieldLocalPath: + return m.LocalPath() + case projectcontributor.FieldLinkedBy: + return m.LinkedBy() + case projectcontributor.FieldLinkedAt: + return m.LinkedAt() + } + return nil, false +} + +// OldField returns the old value of the field from the database. An error is +// returned if the mutation operation is not UpdateOne, or the query to the +// database failed. +func (m *ProjectContributorMutation) OldField(ctx context.Context, name string) (ent.Value, error) { + switch name { + case projectcontributor.FieldProjectID: + return m.OldProjectID(ctx) + case projectcontributor.FieldBrokerID: + return m.OldBrokerID(ctx) + case projectcontributor.FieldBrokerName: + return m.OldBrokerName(ctx) + case projectcontributor.FieldMode: + return m.OldMode(ctx) + case projectcontributor.FieldStatus: + return m.OldStatus(ctx) + case projectcontributor.FieldProfiles: + return m.OldProfiles(ctx) + case projectcontributor.FieldLastSeen: + return m.OldLastSeen(ctx) + case projectcontributor.FieldLocalPath: + return m.OldLocalPath(ctx) + case projectcontributor.FieldLinkedBy: + return m.OldLinkedBy(ctx) + case projectcontributor.FieldLinkedAt: + return m.OldLinkedAt(ctx) + } + return nil, fmt.Errorf("unknown ProjectContributor field %s", name) +} + +// SetField sets the value of a field with the given name. It returns an error if +// the field is not defined in the schema, or if the type mismatched the field +// type. +func (m *ProjectContributorMutation) SetField(name string, value ent.Value) error { + switch name { + case projectcontributor.FieldProjectID: + v, ok := value.(uuid.UUID) + if !ok { + return fmt.Errorf("unexpected type %T for field %s", value, name) + } + m.SetProjectID(v) + return nil + case projectcontributor.FieldBrokerID: + v, ok := value.(uuid.UUID) + if !ok { + return fmt.Errorf("unexpected type %T for field %s", value, name) + } + m.SetBrokerID(v) + return nil + case projectcontributor.FieldBrokerName: + v, ok := value.(string) + if !ok { + return fmt.Errorf("unexpected type %T for field %s", value, name) + } + m.SetBrokerName(v) + return nil + case projectcontributor.FieldMode: + v, ok := value.(string) + if !ok { + return fmt.Errorf("unexpected type %T for field %s", value, name) + } + m.SetMode(v) + return nil + case projectcontributor.FieldStatus: + v, ok := value.(string) + if !ok { + return fmt.Errorf("unexpected type %T for field %s", value, name) + } + m.SetStatus(v) + return nil + case projectcontributor.FieldProfiles: + v, ok := value.(string) + if !ok { + return fmt.Errorf("unexpected type %T for field %s", value, name) + } + m.SetProfiles(v) + return nil + case projectcontributor.FieldLastSeen: + v, ok := value.(time.Time) + if !ok { + return fmt.Errorf("unexpected type %T for field %s", value, name) + } + m.SetLastSeen(v) + return nil + case projectcontributor.FieldLocalPath: + v, ok := value.(string) + if !ok { + return fmt.Errorf("unexpected type %T for field %s", value, name) + } + m.SetLocalPath(v) + return nil + case projectcontributor.FieldLinkedBy: + v, ok := value.(string) + if !ok { + return fmt.Errorf("unexpected type %T for field %s", value, name) + } + m.SetLinkedBy(v) + return nil + case projectcontributor.FieldLinkedAt: + v, ok := value.(time.Time) + if !ok { + return fmt.Errorf("unexpected type %T for field %s", value, name) + } + m.SetLinkedAt(v) + return nil + } + return fmt.Errorf("unknown ProjectContributor field %s", name) +} + +// AddedFields returns all numeric fields that were incremented/decremented during +// this mutation. +func (m *ProjectContributorMutation) AddedFields() []string { + return nil +} + +// AddedField returns the numeric value that was incremented/decremented on a field +// with the given name. The second boolean return value indicates that this field +// was not set, or was not defined in the schema. +func (m *ProjectContributorMutation) AddedField(name string) (ent.Value, bool) { + return nil, false +} + +// AddField adds the value to the field with the given name. It returns an error if +// the field is not defined in the schema, or if the type mismatched the field +// type. +func (m *ProjectContributorMutation) AddField(name string, value ent.Value) error { + switch name { + } + return fmt.Errorf("unknown ProjectContributor numeric field %s", name) +} + +// ClearedFields returns all nullable fields that were cleared during this +// mutation. +func (m *ProjectContributorMutation) ClearedFields() []string { + var fields []string + if m.FieldCleared(projectcontributor.FieldProfiles) { + fields = append(fields, projectcontributor.FieldProfiles) + } + if m.FieldCleared(projectcontributor.FieldLastSeen) { + fields = append(fields, projectcontributor.FieldLastSeen) + } + if m.FieldCleared(projectcontributor.FieldLocalPath) { + fields = append(fields, projectcontributor.FieldLocalPath) + } + if m.FieldCleared(projectcontributor.FieldLinkedBy) { + fields = append(fields, projectcontributor.FieldLinkedBy) + } + if m.FieldCleared(projectcontributor.FieldLinkedAt) { + fields = append(fields, projectcontributor.FieldLinkedAt) + } + return fields +} + +// FieldCleared returns a boolean indicating if a field with the given name was +// cleared in this mutation. +func (m *ProjectContributorMutation) FieldCleared(name string) bool { + _, ok := m.clearedFields[name] + return ok +} + +// ClearField clears the value of the field with the given name. It returns an +// error if the field is not defined in the schema. +func (m *ProjectContributorMutation) ClearField(name string) error { + switch name { + case projectcontributor.FieldProfiles: + m.ClearProfiles() + return nil + case projectcontributor.FieldLastSeen: + m.ClearLastSeen() + return nil + case projectcontributor.FieldLocalPath: + m.ClearLocalPath() + return nil + case projectcontributor.FieldLinkedBy: + m.ClearLinkedBy() + return nil + case projectcontributor.FieldLinkedAt: + m.ClearLinkedAt() + return nil + } + return fmt.Errorf("unknown ProjectContributor nullable field %s", name) +} + +// ResetField resets all changes in the mutation for the field with the given name. +// It returns an error if the field is not defined in the schema. +func (m *ProjectContributorMutation) ResetField(name string) error { + switch name { + case projectcontributor.FieldProjectID: + m.ResetProjectID() + return nil + case projectcontributor.FieldBrokerID: + m.ResetBrokerID() + return nil + case projectcontributor.FieldBrokerName: + m.ResetBrokerName() + return nil + case projectcontributor.FieldMode: + m.ResetMode() + return nil + case projectcontributor.FieldStatus: + m.ResetStatus() + return nil + case projectcontributor.FieldProfiles: + m.ResetProfiles() + return nil + case projectcontributor.FieldLastSeen: + m.ResetLastSeen() + return nil + case projectcontributor.FieldLocalPath: + m.ResetLocalPath() + return nil + case projectcontributor.FieldLinkedBy: + m.ResetLinkedBy() + return nil + case projectcontributor.FieldLinkedAt: + m.ResetLinkedAt() + return nil + } + return fmt.Errorf("unknown ProjectContributor field %s", name) +} + +// AddedEdges returns all edge names that were set/added in this mutation. +func (m *ProjectContributorMutation) AddedEdges() []string { + edges := make([]string, 0, 0) + return edges +} + +// AddedIDs returns all IDs (to other nodes) that were added for the given edge +// name in this mutation. +func (m *ProjectContributorMutation) AddedIDs(name string) []ent.Value { + return nil +} + +// RemovedEdges returns all edge names that were removed in this mutation. +func (m *ProjectContributorMutation) RemovedEdges() []string { + edges := make([]string, 0, 0) + return edges +} + +// RemovedIDs returns all IDs (to other nodes) that were removed for the edge with +// the given name in this mutation. +func (m *ProjectContributorMutation) RemovedIDs(name string) []ent.Value { + return nil +} + +// ClearedEdges returns all edge names that were cleared in this mutation. +func (m *ProjectContributorMutation) ClearedEdges() []string { + edges := make([]string, 0, 0) + return edges +} + +// EdgeCleared returns a boolean which indicates if the edge with the given name +// was cleared in this mutation. +func (m *ProjectContributorMutation) EdgeCleared(name string) bool { + return false +} + +// ClearEdge clears the value of the edge with the given name. It returns an error +// if that edge is not defined in the schema. +func (m *ProjectContributorMutation) ClearEdge(name string) error { + return fmt.Errorf("unknown ProjectContributor unique edge %s", name) +} + +// ResetEdge resets all changes to the edge with the given name in this mutation. +// It returns an error if the edge is not defined in the schema. +func (m *ProjectContributorMutation) ResetEdge(name string) error { + return fmt.Errorf("unknown ProjectContributor edge %s", name) +} + +// ProjectSyncStateMutation represents an operation that mutates the ProjectSyncState nodes in the graph. +type ProjectSyncStateMutation struct { + config + op Op + typ string + id *uuid.UUID + project_id *uuid.UUID + broker_id *string + last_sync_time *time.Time + last_commit_sha *string + file_count *int + addfile_count *int + total_bytes *int64 + addtotal_bytes *int64 + clearedFields map[string]struct{} + done bool + oldValue func(context.Context) (*ProjectSyncState, error) + predicates []predicate.ProjectSyncState +} + +var _ ent.Mutation = (*ProjectSyncStateMutation)(nil) + +// projectsyncstateOption allows management of the mutation configuration using functional options. +type projectsyncstateOption func(*ProjectSyncStateMutation) + +// newProjectSyncStateMutation creates new mutation for the ProjectSyncState entity. +func newProjectSyncStateMutation(c config, op Op, opts ...projectsyncstateOption) *ProjectSyncStateMutation { + m := &ProjectSyncStateMutation{ + config: c, + op: op, + typ: TypeProjectSyncState, + clearedFields: make(map[string]struct{}), + } + for _, opt := range opts { + opt(m) + } + return m +} + +// withProjectSyncStateID sets the ID field of the mutation. +func withProjectSyncStateID(id uuid.UUID) projectsyncstateOption { + return func(m *ProjectSyncStateMutation) { + var ( + err error + once sync.Once + value *ProjectSyncState + ) + m.oldValue = func(ctx context.Context) (*ProjectSyncState, error) { + once.Do(func() { + if m.done { + err = errors.New("querying old values post mutation is not allowed") + } else { + value, err = m.Client().ProjectSyncState.Get(ctx, id) + } + }) + return value, err + } + m.id = &id + } +} + +// withProjectSyncState sets the old ProjectSyncState of the mutation. +func withProjectSyncState(node *ProjectSyncState) projectsyncstateOption { + return func(m *ProjectSyncStateMutation) { + m.oldValue = func(context.Context) (*ProjectSyncState, error) { + return node, nil + } + m.id = &node.ID + } +} + +// Client returns a new `ent.Client` from the mutation. If the mutation was +// executed in a transaction (ent.Tx), a transactional client is returned. +func (m ProjectSyncStateMutation) Client() *Client { + client := &Client{config: m.config} + client.init() + return client +} + +// Tx returns an `ent.Tx` for mutations that were executed in transactions; +// it returns an error otherwise. +func (m ProjectSyncStateMutation) Tx() (*Tx, error) { + if _, ok := m.driver.(*txDriver); !ok { + return nil, errors.New("ent: mutation is not running in a transaction") + } + tx := &Tx{config: m.config} + tx.init() + return tx, nil +} + +// SetID sets the value of the id field. Note that this +// operation is only accepted on creation of ProjectSyncState entities. +func (m *ProjectSyncStateMutation) SetID(id uuid.UUID) { + m.id = &id +} + +// ID returns the ID value in the mutation. Note that the ID is only available +// if it was provided to the builder or after it was returned from the database. +func (m *ProjectSyncStateMutation) ID() (id uuid.UUID, exists bool) { + if m.id == nil { + return + } + return *m.id, true +} + +// IDs queries the database and returns the entity ids that match the mutation's predicate. +// That means, if the mutation is applied within a transaction with an isolation level such +// as sql.LevelSerializable, the returned ids match the ids of the rows that will be updated +// or updated by the mutation. +func (m *ProjectSyncStateMutation) IDs(ctx context.Context) ([]uuid.UUID, error) { + switch { + case m.op.Is(OpUpdateOne | OpDeleteOne): + id, exists := m.ID() + if exists { + return []uuid.UUID{id}, nil + } + fallthrough + case m.op.Is(OpUpdate | OpDelete): + return m.Client().ProjectSyncState.Query().Where(m.predicates...).IDs(ctx) + default: + return nil, fmt.Errorf("IDs is not allowed on %s operations", m.op) + } +} + +// SetProjectID sets the "project_id" field. +func (m *ProjectSyncStateMutation) SetProjectID(u uuid.UUID) { + m.project_id = &u +} + +// ProjectID returns the value of the "project_id" field in the mutation. +func (m *ProjectSyncStateMutation) ProjectID() (r uuid.UUID, exists bool) { + v := m.project_id + if v == nil { + return + } + return *v, true +} + +// OldProjectID returns the old "project_id" field's value of the ProjectSyncState entity. +// If the ProjectSyncState object wasn't provided to the builder, the object is fetched from the database. +// An error is returned if the mutation operation is not UpdateOne, or the database query fails. +func (m *ProjectSyncStateMutation) OldProjectID(ctx context.Context) (v uuid.UUID, err error) { + if !m.op.Is(OpUpdateOne) { + return v, errors.New("OldProjectID is only allowed on UpdateOne operations") + } + if m.id == nil || m.oldValue == nil { + return v, errors.New("OldProjectID requires an ID field in the mutation") + } + oldValue, err := m.oldValue(ctx) + if err != nil { + return v, fmt.Errorf("querying old value for OldProjectID: %w", err) + } + return oldValue.ProjectID, nil +} + +// ResetProjectID resets all changes to the "project_id" field. +func (m *ProjectSyncStateMutation) ResetProjectID() { + m.project_id = nil +} + +// SetBrokerID sets the "broker_id" field. +func (m *ProjectSyncStateMutation) SetBrokerID(s string) { + m.broker_id = &s +} + +// BrokerID returns the value of the "broker_id" field in the mutation. +func (m *ProjectSyncStateMutation) BrokerID() (r string, exists bool) { + v := m.broker_id + if v == nil { + return + } + return *v, true +} + +// OldBrokerID returns the old "broker_id" field's value of the ProjectSyncState entity. +// If the ProjectSyncState object wasn't provided to the builder, the object is fetched from the database. +// An error is returned if the mutation operation is not UpdateOne, or the database query fails. +func (m *ProjectSyncStateMutation) OldBrokerID(ctx context.Context) (v string, err error) { + if !m.op.Is(OpUpdateOne) { + return v, errors.New("OldBrokerID is only allowed on UpdateOne operations") + } + if m.id == nil || m.oldValue == nil { + return v, errors.New("OldBrokerID requires an ID field in the mutation") + } + oldValue, err := m.oldValue(ctx) + if err != nil { + return v, fmt.Errorf("querying old value for OldBrokerID: %w", err) + } + return oldValue.BrokerID, nil +} + +// ResetBrokerID resets all changes to the "broker_id" field. +func (m *ProjectSyncStateMutation) ResetBrokerID() { + m.broker_id = nil +} + +// SetLastSyncTime sets the "last_sync_time" field. +func (m *ProjectSyncStateMutation) SetLastSyncTime(t time.Time) { + m.last_sync_time = &t +} + +// LastSyncTime returns the value of the "last_sync_time" field in the mutation. +func (m *ProjectSyncStateMutation) LastSyncTime() (r time.Time, exists bool) { + v := m.last_sync_time + if v == nil { + return + } + return *v, true +} + +// OldLastSyncTime returns the old "last_sync_time" field's value of the ProjectSyncState entity. +// If the ProjectSyncState object wasn't provided to the builder, the object is fetched from the database. +// An error is returned if the mutation operation is not UpdateOne, or the database query fails. +func (m *ProjectSyncStateMutation) OldLastSyncTime(ctx context.Context) (v *time.Time, err error) { + if !m.op.Is(OpUpdateOne) { + return v, errors.New("OldLastSyncTime is only allowed on UpdateOne operations") + } + if m.id == nil || m.oldValue == nil { + return v, errors.New("OldLastSyncTime requires an ID field in the mutation") + } + oldValue, err := m.oldValue(ctx) + if err != nil { + return v, fmt.Errorf("querying old value for OldLastSyncTime: %w", err) + } + return oldValue.LastSyncTime, nil +} + +// ClearLastSyncTime clears the value of the "last_sync_time" field. +func (m *ProjectSyncStateMutation) ClearLastSyncTime() { + m.last_sync_time = nil + m.clearedFields[projectsyncstate.FieldLastSyncTime] = struct{}{} +} + +// LastSyncTimeCleared returns if the "last_sync_time" field was cleared in this mutation. +func (m *ProjectSyncStateMutation) LastSyncTimeCleared() bool { + _, ok := m.clearedFields[projectsyncstate.FieldLastSyncTime] + return ok +} + +// ResetLastSyncTime resets all changes to the "last_sync_time" field. +func (m *ProjectSyncStateMutation) ResetLastSyncTime() { + m.last_sync_time = nil + delete(m.clearedFields, projectsyncstate.FieldLastSyncTime) +} + +// SetLastCommitSha sets the "last_commit_sha" field. +func (m *ProjectSyncStateMutation) SetLastCommitSha(s string) { + m.last_commit_sha = &s +} + +// LastCommitSha returns the value of the "last_commit_sha" field in the mutation. +func (m *ProjectSyncStateMutation) LastCommitSha() (r string, exists bool) { + v := m.last_commit_sha + if v == nil { + return + } + return *v, true +} + +// OldLastCommitSha returns the old "last_commit_sha" field's value of the ProjectSyncState entity. +// If the ProjectSyncState object wasn't provided to the builder, the object is fetched from the database. +// An error is returned if the mutation operation is not UpdateOne, or the database query fails. +func (m *ProjectSyncStateMutation) OldLastCommitSha(ctx context.Context) (v string, err error) { + if !m.op.Is(OpUpdateOne) { + return v, errors.New("OldLastCommitSha is only allowed on UpdateOne operations") + } + if m.id == nil || m.oldValue == nil { + return v, errors.New("OldLastCommitSha requires an ID field in the mutation") + } + oldValue, err := m.oldValue(ctx) + if err != nil { + return v, fmt.Errorf("querying old value for OldLastCommitSha: %w", err) + } + return oldValue.LastCommitSha, nil +} + +// ClearLastCommitSha clears the value of the "last_commit_sha" field. +func (m *ProjectSyncStateMutation) ClearLastCommitSha() { + m.last_commit_sha = nil + m.clearedFields[projectsyncstate.FieldLastCommitSha] = struct{}{} +} + +// LastCommitShaCleared returns if the "last_commit_sha" field was cleared in this mutation. +func (m *ProjectSyncStateMutation) LastCommitShaCleared() bool { + _, ok := m.clearedFields[projectsyncstate.FieldLastCommitSha] + return ok +} + +// ResetLastCommitSha resets all changes to the "last_commit_sha" field. +func (m *ProjectSyncStateMutation) ResetLastCommitSha() { + m.last_commit_sha = nil + delete(m.clearedFields, projectsyncstate.FieldLastCommitSha) +} + +// SetFileCount sets the "file_count" field. +func (m *ProjectSyncStateMutation) SetFileCount(i int) { + m.file_count = &i + m.addfile_count = nil +} + +// FileCount returns the value of the "file_count" field in the mutation. +func (m *ProjectSyncStateMutation) FileCount() (r int, exists bool) { + v := m.file_count + if v == nil { + return + } + return *v, true +} + +// OldFileCount returns the old "file_count" field's value of the ProjectSyncState entity. +// If the ProjectSyncState object wasn't provided to the builder, the object is fetched from the database. +// An error is returned if the mutation operation is not UpdateOne, or the database query fails. +func (m *ProjectSyncStateMutation) OldFileCount(ctx context.Context) (v int, err error) { + if !m.op.Is(OpUpdateOne) { + return v, errors.New("OldFileCount is only allowed on UpdateOne operations") + } + if m.id == nil || m.oldValue == nil { + return v, errors.New("OldFileCount requires an ID field in the mutation") + } + oldValue, err := m.oldValue(ctx) + if err != nil { + return v, fmt.Errorf("querying old value for OldFileCount: %w", err) + } + return oldValue.FileCount, nil +} + +// AddFileCount adds i to the "file_count" field. +func (m *ProjectSyncStateMutation) AddFileCount(i int) { + if m.addfile_count != nil { + *m.addfile_count += i + } else { + m.addfile_count = &i + } +} + +// AddedFileCount returns the value that was added to the "file_count" field in this mutation. +func (m *ProjectSyncStateMutation) AddedFileCount() (r int, exists bool) { + v := m.addfile_count + if v == nil { + return + } + return *v, true +} + +// ResetFileCount resets all changes to the "file_count" field. +func (m *ProjectSyncStateMutation) ResetFileCount() { + m.file_count = nil + m.addfile_count = nil +} + +// SetTotalBytes sets the "total_bytes" field. +func (m *ProjectSyncStateMutation) SetTotalBytes(i int64) { + m.total_bytes = &i + m.addtotal_bytes = nil +} + +// TotalBytes returns the value of the "total_bytes" field in the mutation. +func (m *ProjectSyncStateMutation) TotalBytes() (r int64, exists bool) { + v := m.total_bytes + if v == nil { + return + } + return *v, true +} + +// OldTotalBytes returns the old "total_bytes" field's value of the ProjectSyncState entity. +// If the ProjectSyncState object wasn't provided to the builder, the object is fetched from the database. +// An error is returned if the mutation operation is not UpdateOne, or the database query fails. +func (m *ProjectSyncStateMutation) OldTotalBytes(ctx context.Context) (v int64, err error) { + if !m.op.Is(OpUpdateOne) { + return v, errors.New("OldTotalBytes is only allowed on UpdateOne operations") + } + if m.id == nil || m.oldValue == nil { + return v, errors.New("OldTotalBytes requires an ID field in the mutation") + } + oldValue, err := m.oldValue(ctx) + if err != nil { + return v, fmt.Errorf("querying old value for OldTotalBytes: %w", err) + } + return oldValue.TotalBytes, nil +} + +// AddTotalBytes adds i to the "total_bytes" field. +func (m *ProjectSyncStateMutation) AddTotalBytes(i int64) { + if m.addtotal_bytes != nil { + *m.addtotal_bytes += i + } else { + m.addtotal_bytes = &i + } +} + +// AddedTotalBytes returns the value that was added to the "total_bytes" field in this mutation. +func (m *ProjectSyncStateMutation) AddedTotalBytes() (r int64, exists bool) { + v := m.addtotal_bytes + if v == nil { + return + } + return *v, true +} + +// ResetTotalBytes resets all changes to the "total_bytes" field. +func (m *ProjectSyncStateMutation) ResetTotalBytes() { + m.total_bytes = nil + m.addtotal_bytes = nil +} + +// Where appends a list predicates to the ProjectSyncStateMutation builder. +func (m *ProjectSyncStateMutation) Where(ps ...predicate.ProjectSyncState) { + m.predicates = append(m.predicates, ps...) +} + +// WhereP appends storage-level predicates to the ProjectSyncStateMutation builder. Using this method, +// users can use type-assertion to append predicates that do not depend on any generated package. +func (m *ProjectSyncStateMutation) WhereP(ps ...func(*sql.Selector)) { + p := make([]predicate.ProjectSyncState, len(ps)) + for i := range ps { + p[i] = ps[i] + } + m.Where(p...) +} + +// Op returns the operation name. +func (m *ProjectSyncStateMutation) Op() Op { + return m.op +} + +// SetOp allows setting the mutation operation. +func (m *ProjectSyncStateMutation) SetOp(op Op) { + m.op = op +} + +// Type returns the node type of this mutation (ProjectSyncState). +func (m *ProjectSyncStateMutation) Type() string { + return m.typ +} + +// Fields returns all fields that were changed during this mutation. Note that in +// order to get all numeric fields that were incremented/decremented, call +// AddedFields(). +func (m *ProjectSyncStateMutation) Fields() []string { + fields := make([]string, 0, 6) + if m.project_id != nil { + fields = append(fields, projectsyncstate.FieldProjectID) + } + if m.broker_id != nil { + fields = append(fields, projectsyncstate.FieldBrokerID) + } + if m.last_sync_time != nil { + fields = append(fields, projectsyncstate.FieldLastSyncTime) + } + if m.last_commit_sha != nil { + fields = append(fields, projectsyncstate.FieldLastCommitSha) + } + if m.file_count != nil { + fields = append(fields, projectsyncstate.FieldFileCount) + } + if m.total_bytes != nil { + fields = append(fields, projectsyncstate.FieldTotalBytes) + } + return fields +} + +// Field returns the value of a field with the given name. The second boolean +// return value indicates that this field was not set, or was not defined in the +// schema. +func (m *ProjectSyncStateMutation) Field(name string) (ent.Value, bool) { + switch name { + case projectsyncstate.FieldProjectID: + return m.ProjectID() + case projectsyncstate.FieldBrokerID: + return m.BrokerID() + case projectsyncstate.FieldLastSyncTime: + return m.LastSyncTime() + case projectsyncstate.FieldLastCommitSha: + return m.LastCommitSha() + case projectsyncstate.FieldFileCount: + return m.FileCount() + case projectsyncstate.FieldTotalBytes: + return m.TotalBytes() + } + return nil, false +} + +// OldField returns the old value of the field from the database. An error is +// returned if the mutation operation is not UpdateOne, or the query to the +// database failed. +func (m *ProjectSyncStateMutation) OldField(ctx context.Context, name string) (ent.Value, error) { + switch name { + case projectsyncstate.FieldProjectID: + return m.OldProjectID(ctx) + case projectsyncstate.FieldBrokerID: + return m.OldBrokerID(ctx) + case projectsyncstate.FieldLastSyncTime: + return m.OldLastSyncTime(ctx) + case projectsyncstate.FieldLastCommitSha: + return m.OldLastCommitSha(ctx) + case projectsyncstate.FieldFileCount: + return m.OldFileCount(ctx) + case projectsyncstate.FieldTotalBytes: + return m.OldTotalBytes(ctx) + } + return nil, fmt.Errorf("unknown ProjectSyncState field %s", name) +} + +// SetField sets the value of a field with the given name. It returns an error if +// the field is not defined in the schema, or if the type mismatched the field +// type. +func (m *ProjectSyncStateMutation) SetField(name string, value ent.Value) error { + switch name { + case projectsyncstate.FieldProjectID: + v, ok := value.(uuid.UUID) + if !ok { + return fmt.Errorf("unexpected type %T for field %s", value, name) + } + m.SetProjectID(v) + return nil + case projectsyncstate.FieldBrokerID: + v, ok := value.(string) + if !ok { + return fmt.Errorf("unexpected type %T for field %s", value, name) + } + m.SetBrokerID(v) + return nil + case projectsyncstate.FieldLastSyncTime: + v, ok := value.(time.Time) + if !ok { + return fmt.Errorf("unexpected type %T for field %s", value, name) + } + m.SetLastSyncTime(v) + return nil + case projectsyncstate.FieldLastCommitSha: + v, ok := value.(string) + if !ok { + return fmt.Errorf("unexpected type %T for field %s", value, name) + } + m.SetLastCommitSha(v) + return nil + case projectsyncstate.FieldFileCount: + v, ok := value.(int) + if !ok { + return fmt.Errorf("unexpected type %T for field %s", value, name) + } + m.SetFileCount(v) + return nil + case projectsyncstate.FieldTotalBytes: + v, ok := value.(int64) + if !ok { + return fmt.Errorf("unexpected type %T for field %s", value, name) + } + m.SetTotalBytes(v) + return nil + } + return fmt.Errorf("unknown ProjectSyncState field %s", name) +} + +// AddedFields returns all numeric fields that were incremented/decremented during +// this mutation. +func (m *ProjectSyncStateMutation) AddedFields() []string { + var fields []string + if m.addfile_count != nil { + fields = append(fields, projectsyncstate.FieldFileCount) + } + if m.addtotal_bytes != nil { + fields = append(fields, projectsyncstate.FieldTotalBytes) + } + return fields +} + +// AddedField returns the numeric value that was incremented/decremented on a field +// with the given name. The second boolean return value indicates that this field +// was not set, or was not defined in the schema. +func (m *ProjectSyncStateMutation) AddedField(name string) (ent.Value, bool) { + switch name { + case projectsyncstate.FieldFileCount: + return m.AddedFileCount() + case projectsyncstate.FieldTotalBytes: + return m.AddedTotalBytes() + } + return nil, false +} + +// AddField adds the value to the field with the given name. It returns an error if +// the field is not defined in the schema, or if the type mismatched the field +// type. +func (m *ProjectSyncStateMutation) AddField(name string, value ent.Value) error { + switch name { + case projectsyncstate.FieldFileCount: + v, ok := value.(int) + if !ok { + return fmt.Errorf("unexpected type %T for field %s", value, name) + } + m.AddFileCount(v) + return nil + case projectsyncstate.FieldTotalBytes: + v, ok := value.(int64) + if !ok { + return fmt.Errorf("unexpected type %T for field %s", value, name) + } + m.AddTotalBytes(v) + return nil + } + return fmt.Errorf("unknown ProjectSyncState numeric field %s", name) +} + +// ClearedFields returns all nullable fields that were cleared during this +// mutation. +func (m *ProjectSyncStateMutation) ClearedFields() []string { + var fields []string + if m.FieldCleared(projectsyncstate.FieldLastSyncTime) { + fields = append(fields, projectsyncstate.FieldLastSyncTime) + } + if m.FieldCleared(projectsyncstate.FieldLastCommitSha) { + fields = append(fields, projectsyncstate.FieldLastCommitSha) + } + return fields +} + +// FieldCleared returns a boolean indicating if a field with the given name was +// cleared in this mutation. +func (m *ProjectSyncStateMutation) FieldCleared(name string) bool { + _, ok := m.clearedFields[name] + return ok +} + +// ClearField clears the value of the field with the given name. It returns an +// error if the field is not defined in the schema. +func (m *ProjectSyncStateMutation) ClearField(name string) error { + switch name { + case projectsyncstate.FieldLastSyncTime: + m.ClearLastSyncTime() + return nil + case projectsyncstate.FieldLastCommitSha: + m.ClearLastCommitSha() + return nil + } + return fmt.Errorf("unknown ProjectSyncState nullable field %s", name) +} + +// ResetField resets all changes in the mutation for the field with the given name. +// It returns an error if the field is not defined in the schema. +func (m *ProjectSyncStateMutation) ResetField(name string) error { + switch name { + case projectsyncstate.FieldProjectID: + m.ResetProjectID() + return nil + case projectsyncstate.FieldBrokerID: + m.ResetBrokerID() + return nil + case projectsyncstate.FieldLastSyncTime: + m.ResetLastSyncTime() + return nil + case projectsyncstate.FieldLastCommitSha: + m.ResetLastCommitSha() + return nil + case projectsyncstate.FieldFileCount: + m.ResetFileCount() + return nil + case projectsyncstate.FieldTotalBytes: + m.ResetTotalBytes() + return nil + } + return fmt.Errorf("unknown ProjectSyncState field %s", name) +} + +// AddedEdges returns all edge names that were set/added in this mutation. +func (m *ProjectSyncStateMutation) AddedEdges() []string { + edges := make([]string, 0, 0) + return edges +} + +// AddedIDs returns all IDs (to other nodes) that were added for the given edge +// name in this mutation. +func (m *ProjectSyncStateMutation) AddedIDs(name string) []ent.Value { + return nil +} + +// RemovedEdges returns all edge names that were removed in this mutation. +func (m *ProjectSyncStateMutation) RemovedEdges() []string { + edges := make([]string, 0, 0) + return edges +} + +// RemovedIDs returns all IDs (to other nodes) that were removed for the edge with +// the given name in this mutation. +func (m *ProjectSyncStateMutation) RemovedIDs(name string) []ent.Value { + return nil +} + +// ClearedEdges returns all edge names that were cleared in this mutation. +func (m *ProjectSyncStateMutation) ClearedEdges() []string { + edges := make([]string, 0, 0) + return edges +} + +// EdgeCleared returns a boolean which indicates if the edge with the given name +// was cleared in this mutation. +func (m *ProjectSyncStateMutation) EdgeCleared(name string) bool { + return false +} + +// ClearEdge clears the value of the edge with the given name. It returns an error +// if that edge is not defined in the schema. +func (m *ProjectSyncStateMutation) ClearEdge(name string) error { + return fmt.Errorf("unknown ProjectSyncState unique edge %s", name) +} + +// ResetEdge resets all changes to the edge with the given name in this mutation. +// It returns an error if the edge is not defined in the schema. +func (m *ProjectSyncStateMutation) ResetEdge(name string) error { + return fmt.Errorf("unknown ProjectSyncState edge %s", name) +} + +// RuntimeBrokerMutation represents an operation that mutates the RuntimeBroker nodes in the graph. +type RuntimeBrokerMutation struct { + config + op Op + typ string + id *uuid.UUID + name *string + slug *string + _type *string + mode *string + version *string + lock_version *int64 + addlock_version *int64 + status *string + connection_state *string + last_heartbeat *time.Time + capabilities *string + supported_harnesses *string + resources *string + runtimes *string + labels *string + annotations *string + endpoint *string + created_by *string + auto_provide *bool + connected_hub_id *string + connected_session_id *string + connected_at *time.Time + created *time.Time + updated *time.Time + clearedFields map[string]struct{} + done bool + oldValue func(context.Context) (*RuntimeBroker, error) + predicates []predicate.RuntimeBroker +} + +var _ ent.Mutation = (*RuntimeBrokerMutation)(nil) + +// runtimebrokerOption allows management of the mutation configuration using functional options. +type runtimebrokerOption func(*RuntimeBrokerMutation) + +// newRuntimeBrokerMutation creates new mutation for the RuntimeBroker entity. +func newRuntimeBrokerMutation(c config, op Op, opts ...runtimebrokerOption) *RuntimeBrokerMutation { + m := &RuntimeBrokerMutation{ + config: c, + op: op, + typ: TypeRuntimeBroker, + clearedFields: make(map[string]struct{}), + } + for _, opt := range opts { + opt(m) + } + return m +} + +// withRuntimeBrokerID sets the ID field of the mutation. +func withRuntimeBrokerID(id uuid.UUID) runtimebrokerOption { + return func(m *RuntimeBrokerMutation) { + var ( + err error + once sync.Once + value *RuntimeBroker + ) + m.oldValue = func(ctx context.Context) (*RuntimeBroker, error) { + once.Do(func() { + if m.done { + err = errors.New("querying old values post mutation is not allowed") + } else { + value, err = m.Client().RuntimeBroker.Get(ctx, id) + } + }) + return value, err + } + m.id = &id + } +} + +// withRuntimeBroker sets the old RuntimeBroker of the mutation. +func withRuntimeBroker(node *RuntimeBroker) runtimebrokerOption { + return func(m *RuntimeBrokerMutation) { + m.oldValue = func(context.Context) (*RuntimeBroker, error) { + return node, nil + } + m.id = &node.ID + } +} + +// Client returns a new `ent.Client` from the mutation. If the mutation was +// executed in a transaction (ent.Tx), a transactional client is returned. +func (m RuntimeBrokerMutation) Client() *Client { + client := &Client{config: m.config} + client.init() + return client +} + +// Tx returns an `ent.Tx` for mutations that were executed in transactions; +// it returns an error otherwise. +func (m RuntimeBrokerMutation) Tx() (*Tx, error) { + if _, ok := m.driver.(*txDriver); !ok { + return nil, errors.New("ent: mutation is not running in a transaction") + } + tx := &Tx{config: m.config} + tx.init() + return tx, nil +} + +// SetID sets the value of the id field. Note that this +// operation is only accepted on creation of RuntimeBroker entities. +func (m *RuntimeBrokerMutation) SetID(id uuid.UUID) { + m.id = &id +} + +// ID returns the ID value in the mutation. Note that the ID is only available +// if it was provided to the builder or after it was returned from the database. +func (m *RuntimeBrokerMutation) ID() (id uuid.UUID, exists bool) { + if m.id == nil { + return + } + return *m.id, true +} + +// IDs queries the database and returns the entity ids that match the mutation's predicate. +// That means, if the mutation is applied within a transaction with an isolation level such +// as sql.LevelSerializable, the returned ids match the ids of the rows that will be updated +// or updated by the mutation. +func (m *RuntimeBrokerMutation) IDs(ctx context.Context) ([]uuid.UUID, error) { + switch { + case m.op.Is(OpUpdateOne | OpDeleteOne): + id, exists := m.ID() + if exists { + return []uuid.UUID{id}, nil + } + fallthrough + case m.op.Is(OpUpdate | OpDelete): + return m.Client().RuntimeBroker.Query().Where(m.predicates...).IDs(ctx) + default: + return nil, fmt.Errorf("IDs is not allowed on %s operations", m.op) + } +} + +// SetName sets the "name" field. +func (m *RuntimeBrokerMutation) SetName(s string) { + m.name = &s +} + +// Name returns the value of the "name" field in the mutation. +func (m *RuntimeBrokerMutation) Name() (r string, exists bool) { + v := m.name + if v == nil { + return + } + return *v, true +} + +// OldName returns the old "name" field's value of the RuntimeBroker entity. +// If the RuntimeBroker object wasn't provided to the builder, the object is fetched from the database. +// An error is returned if the mutation operation is not UpdateOne, or the database query fails. +func (m *RuntimeBrokerMutation) OldName(ctx context.Context) (v string, err error) { + if !m.op.Is(OpUpdateOne) { + return v, errors.New("OldName is only allowed on UpdateOne operations") + } + if m.id == nil || m.oldValue == nil { + return v, errors.New("OldName requires an ID field in the mutation") + } + oldValue, err := m.oldValue(ctx) + if err != nil { + return v, fmt.Errorf("querying old value for OldName: %w", err) + } + return oldValue.Name, nil +} + +// ResetName resets all changes to the "name" field. +func (m *RuntimeBrokerMutation) ResetName() { + m.name = nil +} + +// SetSlug sets the "slug" field. +func (m *RuntimeBrokerMutation) SetSlug(s string) { + m.slug = &s +} + +// Slug returns the value of the "slug" field in the mutation. +func (m *RuntimeBrokerMutation) Slug() (r string, exists bool) { + v := m.slug + if v == nil { + return + } + return *v, true +} + +// OldSlug returns the old "slug" field's value of the RuntimeBroker entity. +// If the RuntimeBroker object wasn't provided to the builder, the object is fetched from the database. +// An error is returned if the mutation operation is not UpdateOne, or the database query fails. +func (m *RuntimeBrokerMutation) OldSlug(ctx context.Context) (v string, err error) { + if !m.op.Is(OpUpdateOne) { + return v, errors.New("OldSlug is only allowed on UpdateOne operations") + } + if m.id == nil || m.oldValue == nil { + return v, errors.New("OldSlug requires an ID field in the mutation") + } + oldValue, err := m.oldValue(ctx) + if err != nil { + return v, fmt.Errorf("querying old value for OldSlug: %w", err) + } + return oldValue.Slug, nil +} + +// ResetSlug resets all changes to the "slug" field. +func (m *RuntimeBrokerMutation) ResetSlug() { + m.slug = nil +} + +// SetType sets the "type" field. +func (m *RuntimeBrokerMutation) SetType(s string) { + m._type = &s +} + +// GetType returns the value of the "type" field in the mutation. +func (m *RuntimeBrokerMutation) GetType() (r string, exists bool) { + v := m._type + if v == nil { + return + } + return *v, true +} + +// OldType returns the old "type" field's value of the RuntimeBroker entity. +// If the RuntimeBroker object wasn't provided to the builder, the object is fetched from the database. +// An error is returned if the mutation operation is not UpdateOne, or the database query fails. +func (m *RuntimeBrokerMutation) OldType(ctx context.Context) (v string, err error) { + if !m.op.Is(OpUpdateOne) { + return v, errors.New("OldType is only allowed on UpdateOne operations") + } + if m.id == nil || m.oldValue == nil { + return v, errors.New("OldType requires an ID field in the mutation") + } + oldValue, err := m.oldValue(ctx) + if err != nil { + return v, fmt.Errorf("querying old value for OldType: %w", err) + } + return oldValue.Type, nil +} + +// ClearType clears the value of the "type" field. +func (m *RuntimeBrokerMutation) ClearType() { + m._type = nil + m.clearedFields[runtimebroker.FieldType] = struct{}{} +} + +// TypeCleared returns if the "type" field was cleared in this mutation. +func (m *RuntimeBrokerMutation) TypeCleared() bool { + _, ok := m.clearedFields[runtimebroker.FieldType] + return ok +} + +// ResetType resets all changes to the "type" field. +func (m *RuntimeBrokerMutation) ResetType() { + m._type = nil + delete(m.clearedFields, runtimebroker.FieldType) +} + +// SetMode sets the "mode" field. +func (m *RuntimeBrokerMutation) SetMode(s string) { + m.mode = &s +} + +// Mode returns the value of the "mode" field in the mutation. +func (m *RuntimeBrokerMutation) Mode() (r string, exists bool) { + v := m.mode + if v == nil { + return + } + return *v, true +} + +// OldMode returns the old "mode" field's value of the RuntimeBroker entity. +// If the RuntimeBroker object wasn't provided to the builder, the object is fetched from the database. +// An error is returned if the mutation operation is not UpdateOne, or the database query fails. +func (m *RuntimeBrokerMutation) OldMode(ctx context.Context) (v string, err error) { + if !m.op.Is(OpUpdateOne) { + return v, errors.New("OldMode is only allowed on UpdateOne operations") + } + if m.id == nil || m.oldValue == nil { + return v, errors.New("OldMode requires an ID field in the mutation") + } + oldValue, err := m.oldValue(ctx) + if err != nil { + return v, fmt.Errorf("querying old value for OldMode: %w", err) + } + return oldValue.Mode, nil +} + +// ResetMode resets all changes to the "mode" field. +func (m *RuntimeBrokerMutation) ResetMode() { + m.mode = nil +} + +// SetVersion sets the "version" field. +func (m *RuntimeBrokerMutation) SetVersion(s string) { + m.version = &s +} + +// Version returns the value of the "version" field in the mutation. +func (m *RuntimeBrokerMutation) Version() (r string, exists bool) { + v := m.version + if v == nil { + return + } + return *v, true +} + +// OldVersion returns the old "version" field's value of the RuntimeBroker entity. +// If the RuntimeBroker object wasn't provided to the builder, the object is fetched from the database. +// An error is returned if the mutation operation is not UpdateOne, or the database query fails. +func (m *RuntimeBrokerMutation) OldVersion(ctx context.Context) (v string, err error) { + if !m.op.Is(OpUpdateOne) { + return v, errors.New("OldVersion is only allowed on UpdateOne operations") + } + if m.id == nil || m.oldValue == nil { + return v, errors.New("OldVersion requires an ID field in the mutation") + } + oldValue, err := m.oldValue(ctx) + if err != nil { + return v, fmt.Errorf("querying old value for OldVersion: %w", err) + } + return oldValue.Version, nil +} + +// ClearVersion clears the value of the "version" field. +func (m *RuntimeBrokerMutation) ClearVersion() { + m.version = nil + m.clearedFields[runtimebroker.FieldVersion] = struct{}{} +} + +// VersionCleared returns if the "version" field was cleared in this mutation. +func (m *RuntimeBrokerMutation) VersionCleared() bool { + _, ok := m.clearedFields[runtimebroker.FieldVersion] + return ok +} + +// ResetVersion resets all changes to the "version" field. +func (m *RuntimeBrokerMutation) ResetVersion() { + m.version = nil + delete(m.clearedFields, runtimebroker.FieldVersion) +} + +// SetLockVersion sets the "lock_version" field. +func (m *RuntimeBrokerMutation) SetLockVersion(i int64) { + m.lock_version = &i + m.addlock_version = nil +} + +// LockVersion returns the value of the "lock_version" field in the mutation. +func (m *RuntimeBrokerMutation) LockVersion() (r int64, exists bool) { + v := m.lock_version + if v == nil { + return + } + return *v, true +} + +// OldLockVersion returns the old "lock_version" field's value of the RuntimeBroker entity. +// If the RuntimeBroker object wasn't provided to the builder, the object is fetched from the database. +// An error is returned if the mutation operation is not UpdateOne, or the database query fails. +func (m *RuntimeBrokerMutation) OldLockVersion(ctx context.Context) (v int64, err error) { + if !m.op.Is(OpUpdateOne) { + return v, errors.New("OldLockVersion is only allowed on UpdateOne operations") + } + if m.id == nil || m.oldValue == nil { + return v, errors.New("OldLockVersion requires an ID field in the mutation") + } + oldValue, err := m.oldValue(ctx) + if err != nil { + return v, fmt.Errorf("querying old value for OldLockVersion: %w", err) + } + return oldValue.LockVersion, nil +} + +// AddLockVersion adds i to the "lock_version" field. +func (m *RuntimeBrokerMutation) AddLockVersion(i int64) { + if m.addlock_version != nil { + *m.addlock_version += i + } else { + m.addlock_version = &i + } +} + +// AddedLockVersion returns the value that was added to the "lock_version" field in this mutation. +func (m *RuntimeBrokerMutation) AddedLockVersion() (r int64, exists bool) { + v := m.addlock_version + if v == nil { + return + } + return *v, true +} + +// ResetLockVersion resets all changes to the "lock_version" field. +func (m *RuntimeBrokerMutation) ResetLockVersion() { + m.lock_version = nil + m.addlock_version = nil +} + +// SetStatus sets the "status" field. +func (m *RuntimeBrokerMutation) SetStatus(s string) { + m.status = &s +} + +// Status returns the value of the "status" field in the mutation. +func (m *RuntimeBrokerMutation) Status() (r string, exists bool) { + v := m.status + if v == nil { + return + } + return *v, true +} + +// OldStatus returns the old "status" field's value of the RuntimeBroker entity. +// If the RuntimeBroker object wasn't provided to the builder, the object is fetched from the database. +// An error is returned if the mutation operation is not UpdateOne, or the database query fails. +func (m *RuntimeBrokerMutation) OldStatus(ctx context.Context) (v string, err error) { + if !m.op.Is(OpUpdateOne) { + return v, errors.New("OldStatus is only allowed on UpdateOne operations") + } + if m.id == nil || m.oldValue == nil { + return v, errors.New("OldStatus requires an ID field in the mutation") + } + oldValue, err := m.oldValue(ctx) + if err != nil { + return v, fmt.Errorf("querying old value for OldStatus: %w", err) + } + return oldValue.Status, nil +} + +// ResetStatus resets all changes to the "status" field. +func (m *RuntimeBrokerMutation) ResetStatus() { + m.status = nil +} + +// SetConnectionState sets the "connection_state" field. +func (m *RuntimeBrokerMutation) SetConnectionState(s string) { + m.connection_state = &s +} + +// ConnectionState returns the value of the "connection_state" field in the mutation. +func (m *RuntimeBrokerMutation) ConnectionState() (r string, exists bool) { + v := m.connection_state + if v == nil { + return + } + return *v, true +} + +// OldConnectionState returns the old "connection_state" field's value of the RuntimeBroker entity. +// If the RuntimeBroker object wasn't provided to the builder, the object is fetched from the database. +// An error is returned if the mutation operation is not UpdateOne, or the database query fails. +func (m *RuntimeBrokerMutation) OldConnectionState(ctx context.Context) (v string, err error) { + if !m.op.Is(OpUpdateOne) { + return v, errors.New("OldConnectionState is only allowed on UpdateOne operations") + } + if m.id == nil || m.oldValue == nil { + return v, errors.New("OldConnectionState requires an ID field in the mutation") + } + oldValue, err := m.oldValue(ctx) + if err != nil { + return v, fmt.Errorf("querying old value for OldConnectionState: %w", err) + } + return oldValue.ConnectionState, nil +} + +// ResetConnectionState resets all changes to the "connection_state" field. +func (m *RuntimeBrokerMutation) ResetConnectionState() { + m.connection_state = nil +} + +// SetLastHeartbeat sets the "last_heartbeat" field. +func (m *RuntimeBrokerMutation) SetLastHeartbeat(t time.Time) { + m.last_heartbeat = &t +} + +// LastHeartbeat returns the value of the "last_heartbeat" field in the mutation. +func (m *RuntimeBrokerMutation) LastHeartbeat() (r time.Time, exists bool) { + v := m.last_heartbeat + if v == nil { + return + } + return *v, true +} + +// OldLastHeartbeat returns the old "last_heartbeat" field's value of the RuntimeBroker entity. +// If the RuntimeBroker object wasn't provided to the builder, the object is fetched from the database. +// An error is returned if the mutation operation is not UpdateOne, or the database query fails. +func (m *RuntimeBrokerMutation) OldLastHeartbeat(ctx context.Context) (v *time.Time, err error) { + if !m.op.Is(OpUpdateOne) { + return v, errors.New("OldLastHeartbeat is only allowed on UpdateOne operations") + } + if m.id == nil || m.oldValue == nil { + return v, errors.New("OldLastHeartbeat requires an ID field in the mutation") + } + oldValue, err := m.oldValue(ctx) + if err != nil { + return v, fmt.Errorf("querying old value for OldLastHeartbeat: %w", err) + } + return oldValue.LastHeartbeat, nil +} + +// ClearLastHeartbeat clears the value of the "last_heartbeat" field. +func (m *RuntimeBrokerMutation) ClearLastHeartbeat() { + m.last_heartbeat = nil + m.clearedFields[runtimebroker.FieldLastHeartbeat] = struct{}{} +} + +// LastHeartbeatCleared returns if the "last_heartbeat" field was cleared in this mutation. +func (m *RuntimeBrokerMutation) LastHeartbeatCleared() bool { + _, ok := m.clearedFields[runtimebroker.FieldLastHeartbeat] + return ok +} + +// ResetLastHeartbeat resets all changes to the "last_heartbeat" field. +func (m *RuntimeBrokerMutation) ResetLastHeartbeat() { + m.last_heartbeat = nil + delete(m.clearedFields, runtimebroker.FieldLastHeartbeat) +} + +// SetCapabilities sets the "capabilities" field. +func (m *RuntimeBrokerMutation) SetCapabilities(s string) { + m.capabilities = &s +} + +// Capabilities returns the value of the "capabilities" field in the mutation. +func (m *RuntimeBrokerMutation) Capabilities() (r string, exists bool) { + v := m.capabilities + if v == nil { + return + } + return *v, true +} + +// OldCapabilities returns the old "capabilities" field's value of the RuntimeBroker entity. +// If the RuntimeBroker object wasn't provided to the builder, the object is fetched from the database. +// An error is returned if the mutation operation is not UpdateOne, or the database query fails. +func (m *RuntimeBrokerMutation) OldCapabilities(ctx context.Context) (v string, err error) { + if !m.op.Is(OpUpdateOne) { + return v, errors.New("OldCapabilities is only allowed on UpdateOne operations") + } + if m.id == nil || m.oldValue == nil { + return v, errors.New("OldCapabilities requires an ID field in the mutation") + } + oldValue, err := m.oldValue(ctx) + if err != nil { + return v, fmt.Errorf("querying old value for OldCapabilities: %w", err) + } + return oldValue.Capabilities, nil +} + +// ClearCapabilities clears the value of the "capabilities" field. +func (m *RuntimeBrokerMutation) ClearCapabilities() { + m.capabilities = nil + m.clearedFields[runtimebroker.FieldCapabilities] = struct{}{} +} + +// CapabilitiesCleared returns if the "capabilities" field was cleared in this mutation. +func (m *RuntimeBrokerMutation) CapabilitiesCleared() bool { + _, ok := m.clearedFields[runtimebroker.FieldCapabilities] + return ok +} + +// ResetCapabilities resets all changes to the "capabilities" field. +func (m *RuntimeBrokerMutation) ResetCapabilities() { + m.capabilities = nil + delete(m.clearedFields, runtimebroker.FieldCapabilities) +} + +// SetSupportedHarnesses sets the "supported_harnesses" field. +func (m *RuntimeBrokerMutation) SetSupportedHarnesses(s string) { + m.supported_harnesses = &s +} + +// SupportedHarnesses returns the value of the "supported_harnesses" field in the mutation. +func (m *RuntimeBrokerMutation) SupportedHarnesses() (r string, exists bool) { + v := m.supported_harnesses + if v == nil { + return + } + return *v, true +} + +// OldSupportedHarnesses returns the old "supported_harnesses" field's value of the RuntimeBroker entity. +// If the RuntimeBroker object wasn't provided to the builder, the object is fetched from the database. +// An error is returned if the mutation operation is not UpdateOne, or the database query fails. +func (m *RuntimeBrokerMutation) OldSupportedHarnesses(ctx context.Context) (v string, err error) { + if !m.op.Is(OpUpdateOne) { + return v, errors.New("OldSupportedHarnesses is only allowed on UpdateOne operations") + } + if m.id == nil || m.oldValue == nil { + return v, errors.New("OldSupportedHarnesses requires an ID field in the mutation") + } + oldValue, err := m.oldValue(ctx) + if err != nil { + return v, fmt.Errorf("querying old value for OldSupportedHarnesses: %w", err) + } + return oldValue.SupportedHarnesses, nil +} + +// ClearSupportedHarnesses clears the value of the "supported_harnesses" field. +func (m *RuntimeBrokerMutation) ClearSupportedHarnesses() { + m.supported_harnesses = nil + m.clearedFields[runtimebroker.FieldSupportedHarnesses] = struct{}{} +} + +// SupportedHarnessesCleared returns if the "supported_harnesses" field was cleared in this mutation. +func (m *RuntimeBrokerMutation) SupportedHarnessesCleared() bool { + _, ok := m.clearedFields[runtimebroker.FieldSupportedHarnesses] + return ok +} + +// ResetSupportedHarnesses resets all changes to the "supported_harnesses" field. +func (m *RuntimeBrokerMutation) ResetSupportedHarnesses() { + m.supported_harnesses = nil + delete(m.clearedFields, runtimebroker.FieldSupportedHarnesses) +} + +// SetResources sets the "resources" field. +func (m *RuntimeBrokerMutation) SetResources(s string) { + m.resources = &s +} + +// Resources returns the value of the "resources" field in the mutation. +func (m *RuntimeBrokerMutation) Resources() (r string, exists bool) { + v := m.resources + if v == nil { + return + } + return *v, true +} + +// OldResources returns the old "resources" field's value of the RuntimeBroker entity. +// If the RuntimeBroker object wasn't provided to the builder, the object is fetched from the database. +// An error is returned if the mutation operation is not UpdateOne, or the database query fails. +func (m *RuntimeBrokerMutation) OldResources(ctx context.Context) (v string, err error) { + if !m.op.Is(OpUpdateOne) { + return v, errors.New("OldResources is only allowed on UpdateOne operations") + } + if m.id == nil || m.oldValue == nil { + return v, errors.New("OldResources requires an ID field in the mutation") + } + oldValue, err := m.oldValue(ctx) + if err != nil { + return v, fmt.Errorf("querying old value for OldResources: %w", err) + } + return oldValue.Resources, nil +} + +// ClearResources clears the value of the "resources" field. +func (m *RuntimeBrokerMutation) ClearResources() { + m.resources = nil + m.clearedFields[runtimebroker.FieldResources] = struct{}{} +} + +// ResourcesCleared returns if the "resources" field was cleared in this mutation. +func (m *RuntimeBrokerMutation) ResourcesCleared() bool { + _, ok := m.clearedFields[runtimebroker.FieldResources] + return ok +} + +// ResetResources resets all changes to the "resources" field. +func (m *RuntimeBrokerMutation) ResetResources() { + m.resources = nil + delete(m.clearedFields, runtimebroker.FieldResources) +} + +// SetRuntimes sets the "runtimes" field. +func (m *RuntimeBrokerMutation) SetRuntimes(s string) { + m.runtimes = &s +} + +// Runtimes returns the value of the "runtimes" field in the mutation. +func (m *RuntimeBrokerMutation) Runtimes() (r string, exists bool) { + v := m.runtimes + if v == nil { + return + } + return *v, true +} + +// OldRuntimes returns the old "runtimes" field's value of the RuntimeBroker entity. +// If the RuntimeBroker object wasn't provided to the builder, the object is fetched from the database. +// An error is returned if the mutation operation is not UpdateOne, or the database query fails. +func (m *RuntimeBrokerMutation) OldRuntimes(ctx context.Context) (v string, err error) { + if !m.op.Is(OpUpdateOne) { + return v, errors.New("OldRuntimes is only allowed on UpdateOne operations") + } + if m.id == nil || m.oldValue == nil { + return v, errors.New("OldRuntimes requires an ID field in the mutation") + } + oldValue, err := m.oldValue(ctx) + if err != nil { + return v, fmt.Errorf("querying old value for OldRuntimes: %w", err) + } + return oldValue.Runtimes, nil +} + +// ClearRuntimes clears the value of the "runtimes" field. +func (m *RuntimeBrokerMutation) ClearRuntimes() { + m.runtimes = nil + m.clearedFields[runtimebroker.FieldRuntimes] = struct{}{} +} + +// RuntimesCleared returns if the "runtimes" field was cleared in this mutation. +func (m *RuntimeBrokerMutation) RuntimesCleared() bool { + _, ok := m.clearedFields[runtimebroker.FieldRuntimes] + return ok +} + +// ResetRuntimes resets all changes to the "runtimes" field. +func (m *RuntimeBrokerMutation) ResetRuntimes() { + m.runtimes = nil + delete(m.clearedFields, runtimebroker.FieldRuntimes) +} + +// SetLabels sets the "labels" field. +func (m *RuntimeBrokerMutation) SetLabels(s string) { + m.labels = &s +} + +// Labels returns the value of the "labels" field in the mutation. +func (m *RuntimeBrokerMutation) Labels() (r string, exists bool) { + v := m.labels + if v == nil { + return + } + return *v, true +} + +// OldLabels returns the old "labels" field's value of the RuntimeBroker entity. +// If the RuntimeBroker object wasn't provided to the builder, the object is fetched from the database. +// An error is returned if the mutation operation is not UpdateOne, or the database query fails. +func (m *RuntimeBrokerMutation) OldLabels(ctx context.Context) (v string, err error) { + if !m.op.Is(OpUpdateOne) { + return v, errors.New("OldLabels is only allowed on UpdateOne operations") + } + if m.id == nil || m.oldValue == nil { + return v, errors.New("OldLabels requires an ID field in the mutation") + } + oldValue, err := m.oldValue(ctx) + if err != nil { + return v, fmt.Errorf("querying old value for OldLabels: %w", err) + } + return oldValue.Labels, nil +} + +// ClearLabels clears the value of the "labels" field. +func (m *RuntimeBrokerMutation) ClearLabels() { + m.labels = nil + m.clearedFields[runtimebroker.FieldLabels] = struct{}{} +} + +// LabelsCleared returns if the "labels" field was cleared in this mutation. +func (m *RuntimeBrokerMutation) LabelsCleared() bool { + _, ok := m.clearedFields[runtimebroker.FieldLabels] + return ok +} + +// ResetLabels resets all changes to the "labels" field. +func (m *RuntimeBrokerMutation) ResetLabels() { + m.labels = nil + delete(m.clearedFields, runtimebroker.FieldLabels) +} + +// SetAnnotations sets the "annotations" field. +func (m *RuntimeBrokerMutation) SetAnnotations(s string) { + m.annotations = &s +} + +// Annotations returns the value of the "annotations" field in the mutation. +func (m *RuntimeBrokerMutation) Annotations() (r string, exists bool) { + v := m.annotations + if v == nil { + return + } + return *v, true +} + +// OldAnnotations returns the old "annotations" field's value of the RuntimeBroker entity. +// If the RuntimeBroker object wasn't provided to the builder, the object is fetched from the database. +// An error is returned if the mutation operation is not UpdateOne, or the database query fails. +func (m *RuntimeBrokerMutation) OldAnnotations(ctx context.Context) (v string, err error) { + if !m.op.Is(OpUpdateOne) { + return v, errors.New("OldAnnotations is only allowed on UpdateOne operations") + } + if m.id == nil || m.oldValue == nil { + return v, errors.New("OldAnnotations requires an ID field in the mutation") + } + oldValue, err := m.oldValue(ctx) + if err != nil { + return v, fmt.Errorf("querying old value for OldAnnotations: %w", err) + } + return oldValue.Annotations, nil +} + +// ClearAnnotations clears the value of the "annotations" field. +func (m *RuntimeBrokerMutation) ClearAnnotations() { + m.annotations = nil + m.clearedFields[runtimebroker.FieldAnnotations] = struct{}{} +} + +// AnnotationsCleared returns if the "annotations" field was cleared in this mutation. +func (m *RuntimeBrokerMutation) AnnotationsCleared() bool { + _, ok := m.clearedFields[runtimebroker.FieldAnnotations] + return ok +} + +// ResetAnnotations resets all changes to the "annotations" field. +func (m *RuntimeBrokerMutation) ResetAnnotations() { + m.annotations = nil + delete(m.clearedFields, runtimebroker.FieldAnnotations) +} + +// SetEndpoint sets the "endpoint" field. +func (m *RuntimeBrokerMutation) SetEndpoint(s string) { + m.endpoint = &s +} + +// Endpoint returns the value of the "endpoint" field in the mutation. +func (m *RuntimeBrokerMutation) Endpoint() (r string, exists bool) { + v := m.endpoint + if v == nil { + return + } + return *v, true +} + +// OldEndpoint returns the old "endpoint" field's value of the RuntimeBroker entity. +// If the RuntimeBroker object wasn't provided to the builder, the object is fetched from the database. +// An error is returned if the mutation operation is not UpdateOne, or the database query fails. +func (m *RuntimeBrokerMutation) OldEndpoint(ctx context.Context) (v string, err error) { + if !m.op.Is(OpUpdateOne) { + return v, errors.New("OldEndpoint is only allowed on UpdateOne operations") + } + if m.id == nil || m.oldValue == nil { + return v, errors.New("OldEndpoint requires an ID field in the mutation") + } + oldValue, err := m.oldValue(ctx) + if err != nil { + return v, fmt.Errorf("querying old value for OldEndpoint: %w", err) + } + return oldValue.Endpoint, nil +} + +// ClearEndpoint clears the value of the "endpoint" field. +func (m *RuntimeBrokerMutation) ClearEndpoint() { + m.endpoint = nil + m.clearedFields[runtimebroker.FieldEndpoint] = struct{}{} +} + +// EndpointCleared returns if the "endpoint" field was cleared in this mutation. +func (m *RuntimeBrokerMutation) EndpointCleared() bool { + _, ok := m.clearedFields[runtimebroker.FieldEndpoint] + return ok +} + +// ResetEndpoint resets all changes to the "endpoint" field. +func (m *RuntimeBrokerMutation) ResetEndpoint() { + m.endpoint = nil + delete(m.clearedFields, runtimebroker.FieldEndpoint) +} + +// SetCreatedBy sets the "created_by" field. +func (m *RuntimeBrokerMutation) SetCreatedBy(s string) { + m.created_by = &s +} + +// CreatedBy returns the value of the "created_by" field in the mutation. +func (m *RuntimeBrokerMutation) CreatedBy() (r string, exists bool) { + v := m.created_by + if v == nil { + return + } + return *v, true +} + +// OldCreatedBy returns the old "created_by" field's value of the RuntimeBroker entity. +// If the RuntimeBroker object wasn't provided to the builder, the object is fetched from the database. +// An error is returned if the mutation operation is not UpdateOne, or the database query fails. +func (m *RuntimeBrokerMutation) OldCreatedBy(ctx context.Context) (v string, err error) { + if !m.op.Is(OpUpdateOne) { + return v, errors.New("OldCreatedBy is only allowed on UpdateOne operations") + } + if m.id == nil || m.oldValue == nil { + return v, errors.New("OldCreatedBy requires an ID field in the mutation") + } + oldValue, err := m.oldValue(ctx) + if err != nil { + return v, fmt.Errorf("querying old value for OldCreatedBy: %w", err) + } + return oldValue.CreatedBy, nil +} + +// ClearCreatedBy clears the value of the "created_by" field. +func (m *RuntimeBrokerMutation) ClearCreatedBy() { + m.created_by = nil + m.clearedFields[runtimebroker.FieldCreatedBy] = struct{}{} +} + +// CreatedByCleared returns if the "created_by" field was cleared in this mutation. +func (m *RuntimeBrokerMutation) CreatedByCleared() bool { + _, ok := m.clearedFields[runtimebroker.FieldCreatedBy] + return ok +} + +// ResetCreatedBy resets all changes to the "created_by" field. +func (m *RuntimeBrokerMutation) ResetCreatedBy() { + m.created_by = nil + delete(m.clearedFields, runtimebroker.FieldCreatedBy) +} + +// SetAutoProvide sets the "auto_provide" field. +func (m *RuntimeBrokerMutation) SetAutoProvide(b bool) { + m.auto_provide = &b +} + +// AutoProvide returns the value of the "auto_provide" field in the mutation. +func (m *RuntimeBrokerMutation) AutoProvide() (r bool, exists bool) { + v := m.auto_provide + if v == nil { + return + } + return *v, true +} + +// OldAutoProvide returns the old "auto_provide" field's value of the RuntimeBroker entity. +// If the RuntimeBroker object wasn't provided to the builder, the object is fetched from the database. +// An error is returned if the mutation operation is not UpdateOne, or the database query fails. +func (m *RuntimeBrokerMutation) OldAutoProvide(ctx context.Context) (v bool, err error) { + if !m.op.Is(OpUpdateOne) { + return v, errors.New("OldAutoProvide is only allowed on UpdateOne operations") + } + if m.id == nil || m.oldValue == nil { + return v, errors.New("OldAutoProvide requires an ID field in the mutation") + } + oldValue, err := m.oldValue(ctx) + if err != nil { + return v, fmt.Errorf("querying old value for OldAutoProvide: %w", err) + } + return oldValue.AutoProvide, nil +} + +// ResetAutoProvide resets all changes to the "auto_provide" field. +func (m *RuntimeBrokerMutation) ResetAutoProvide() { + m.auto_provide = nil +} + +// SetConnectedHubID sets the "connected_hub_id" field. +func (m *RuntimeBrokerMutation) SetConnectedHubID(s string) { + m.connected_hub_id = &s +} + +// ConnectedHubID returns the value of the "connected_hub_id" field in the mutation. +func (m *RuntimeBrokerMutation) ConnectedHubID() (r string, exists bool) { + v := m.connected_hub_id + if v == nil { + return + } + return *v, true +} + +// OldConnectedHubID returns the old "connected_hub_id" field's value of the RuntimeBroker entity. +// If the RuntimeBroker object wasn't provided to the builder, the object is fetched from the database. +// An error is returned if the mutation operation is not UpdateOne, or the database query fails. +func (m *RuntimeBrokerMutation) OldConnectedHubID(ctx context.Context) (v *string, err error) { + if !m.op.Is(OpUpdateOne) { + return v, errors.New("OldConnectedHubID is only allowed on UpdateOne operations") + } + if m.id == nil || m.oldValue == nil { + return v, errors.New("OldConnectedHubID requires an ID field in the mutation") + } + oldValue, err := m.oldValue(ctx) + if err != nil { + return v, fmt.Errorf("querying old value for OldConnectedHubID: %w", err) + } + return oldValue.ConnectedHubID, nil +} + +// ClearConnectedHubID clears the value of the "connected_hub_id" field. +func (m *RuntimeBrokerMutation) ClearConnectedHubID() { + m.connected_hub_id = nil + m.clearedFields[runtimebroker.FieldConnectedHubID] = struct{}{} +} + +// ConnectedHubIDCleared returns if the "connected_hub_id" field was cleared in this mutation. +func (m *RuntimeBrokerMutation) ConnectedHubIDCleared() bool { + _, ok := m.clearedFields[runtimebroker.FieldConnectedHubID] + return ok +} + +// ResetConnectedHubID resets all changes to the "connected_hub_id" field. +func (m *RuntimeBrokerMutation) ResetConnectedHubID() { + m.connected_hub_id = nil + delete(m.clearedFields, runtimebroker.FieldConnectedHubID) +} + +// SetConnectedSessionID sets the "connected_session_id" field. +func (m *RuntimeBrokerMutation) SetConnectedSessionID(s string) { + m.connected_session_id = &s +} + +// ConnectedSessionID returns the value of the "connected_session_id" field in the mutation. +func (m *RuntimeBrokerMutation) ConnectedSessionID() (r string, exists bool) { + v := m.connected_session_id + if v == nil { + return + } + return *v, true +} + +// OldConnectedSessionID returns the old "connected_session_id" field's value of the RuntimeBroker entity. +// If the RuntimeBroker object wasn't provided to the builder, the object is fetched from the database. +// An error is returned if the mutation operation is not UpdateOne, or the database query fails. +func (m *RuntimeBrokerMutation) OldConnectedSessionID(ctx context.Context) (v *string, err error) { + if !m.op.Is(OpUpdateOne) { + return v, errors.New("OldConnectedSessionID is only allowed on UpdateOne operations") + } + if m.id == nil || m.oldValue == nil { + return v, errors.New("OldConnectedSessionID requires an ID field in the mutation") + } + oldValue, err := m.oldValue(ctx) + if err != nil { + return v, fmt.Errorf("querying old value for OldConnectedSessionID: %w", err) + } + return oldValue.ConnectedSessionID, nil +} + +// ClearConnectedSessionID clears the value of the "connected_session_id" field. +func (m *RuntimeBrokerMutation) ClearConnectedSessionID() { + m.connected_session_id = nil + m.clearedFields[runtimebroker.FieldConnectedSessionID] = struct{}{} +} + +// ConnectedSessionIDCleared returns if the "connected_session_id" field was cleared in this mutation. +func (m *RuntimeBrokerMutation) ConnectedSessionIDCleared() bool { + _, ok := m.clearedFields[runtimebroker.FieldConnectedSessionID] + return ok +} + +// ResetConnectedSessionID resets all changes to the "connected_session_id" field. +func (m *RuntimeBrokerMutation) ResetConnectedSessionID() { + m.connected_session_id = nil + delete(m.clearedFields, runtimebroker.FieldConnectedSessionID) +} + +// SetConnectedAt sets the "connected_at" field. +func (m *RuntimeBrokerMutation) SetConnectedAt(t time.Time) { + m.connected_at = &t +} + +// ConnectedAt returns the value of the "connected_at" field in the mutation. +func (m *RuntimeBrokerMutation) ConnectedAt() (r time.Time, exists bool) { + v := m.connected_at + if v == nil { + return + } + return *v, true +} + +// OldConnectedAt returns the old "connected_at" field's value of the RuntimeBroker entity. +// If the RuntimeBroker object wasn't provided to the builder, the object is fetched from the database. +// An error is returned if the mutation operation is not UpdateOne, or the database query fails. +func (m *RuntimeBrokerMutation) OldConnectedAt(ctx context.Context) (v *time.Time, err error) { + if !m.op.Is(OpUpdateOne) { + return v, errors.New("OldConnectedAt is only allowed on UpdateOne operations") + } + if m.id == nil || m.oldValue == nil { + return v, errors.New("OldConnectedAt requires an ID field in the mutation") + } + oldValue, err := m.oldValue(ctx) + if err != nil { + return v, fmt.Errorf("querying old value for OldConnectedAt: %w", err) + } + return oldValue.ConnectedAt, nil +} + +// ClearConnectedAt clears the value of the "connected_at" field. +func (m *RuntimeBrokerMutation) ClearConnectedAt() { + m.connected_at = nil + m.clearedFields[runtimebroker.FieldConnectedAt] = struct{}{} +} + +// ConnectedAtCleared returns if the "connected_at" field was cleared in this mutation. +func (m *RuntimeBrokerMutation) ConnectedAtCleared() bool { + _, ok := m.clearedFields[runtimebroker.FieldConnectedAt] + return ok +} + +// ResetConnectedAt resets all changes to the "connected_at" field. +func (m *RuntimeBrokerMutation) ResetConnectedAt() { + m.connected_at = nil + delete(m.clearedFields, runtimebroker.FieldConnectedAt) +} + +// SetCreated sets the "created" field. +func (m *RuntimeBrokerMutation) SetCreated(t time.Time) { + m.created = &t +} + +// Created returns the value of the "created" field in the mutation. +func (m *RuntimeBrokerMutation) Created() (r time.Time, exists bool) { + v := m.created + if v == nil { + return + } + return *v, true +} + +// OldCreated returns the old "created" field's value of the RuntimeBroker entity. +// If the RuntimeBroker object wasn't provided to the builder, the object is fetched from the database. +// An error is returned if the mutation operation is not UpdateOne, or the database query fails. +func (m *RuntimeBrokerMutation) OldCreated(ctx context.Context) (v time.Time, err error) { + if !m.op.Is(OpUpdateOne) { + return v, errors.New("OldCreated is only allowed on UpdateOne operations") + } + if m.id == nil || m.oldValue == nil { + return v, errors.New("OldCreated requires an ID field in the mutation") + } + oldValue, err := m.oldValue(ctx) + if err != nil { + return v, fmt.Errorf("querying old value for OldCreated: %w", err) + } + return oldValue.Created, nil +} + +// ResetCreated resets all changes to the "created" field. +func (m *RuntimeBrokerMutation) ResetCreated() { + m.created = nil +} + +// SetUpdated sets the "updated" field. +func (m *RuntimeBrokerMutation) SetUpdated(t time.Time) { + m.updated = &t +} + +// Updated returns the value of the "updated" field in the mutation. +func (m *RuntimeBrokerMutation) Updated() (r time.Time, exists bool) { + v := m.updated + if v == nil { + return + } + return *v, true +} + +// OldUpdated returns the old "updated" field's value of the RuntimeBroker entity. +// If the RuntimeBroker object wasn't provided to the builder, the object is fetched from the database. +// An error is returned if the mutation operation is not UpdateOne, or the database query fails. +func (m *RuntimeBrokerMutation) OldUpdated(ctx context.Context) (v time.Time, err error) { + if !m.op.Is(OpUpdateOne) { + return v, errors.New("OldUpdated is only allowed on UpdateOne operations") + } + if m.id == nil || m.oldValue == nil { + return v, errors.New("OldUpdated requires an ID field in the mutation") + } + oldValue, err := m.oldValue(ctx) + if err != nil { + return v, fmt.Errorf("querying old value for OldUpdated: %w", err) + } + return oldValue.Updated, nil +} + +// ResetUpdated resets all changes to the "updated" field. +func (m *RuntimeBrokerMutation) ResetUpdated() { + m.updated = nil +} + +// Where appends a list predicates to the RuntimeBrokerMutation builder. +func (m *RuntimeBrokerMutation) Where(ps ...predicate.RuntimeBroker) { + m.predicates = append(m.predicates, ps...) +} + +// WhereP appends storage-level predicates to the RuntimeBrokerMutation builder. Using this method, +// users can use type-assertion to append predicates that do not depend on any generated package. +func (m *RuntimeBrokerMutation) WhereP(ps ...func(*sql.Selector)) { + p := make([]predicate.RuntimeBroker, len(ps)) + for i := range ps { + p[i] = ps[i] + } + m.Where(p...) +} + +// Op returns the operation name. +func (m *RuntimeBrokerMutation) Op() Op { + return m.op +} + +// SetOp allows setting the mutation operation. +func (m *RuntimeBrokerMutation) SetOp(op Op) { + m.op = op +} + +// Type returns the node type of this mutation (RuntimeBroker). +func (m *RuntimeBrokerMutation) Type() string { + return m.typ +} + +// Fields returns all fields that were changed during this mutation. Note that in +// order to get all numeric fields that were incremented/decremented, call +// AddedFields(). +func (m *RuntimeBrokerMutation) Fields() []string { + fields := make([]string, 0, 23) + if m.name != nil { + fields = append(fields, runtimebroker.FieldName) + } + if m.slug != nil { + fields = append(fields, runtimebroker.FieldSlug) + } + if m._type != nil { + fields = append(fields, runtimebroker.FieldType) + } + if m.mode != nil { + fields = append(fields, runtimebroker.FieldMode) + } + if m.version != nil { + fields = append(fields, runtimebroker.FieldVersion) + } + if m.lock_version != nil { + fields = append(fields, runtimebroker.FieldLockVersion) + } + if m.status != nil { + fields = append(fields, runtimebroker.FieldStatus) + } + if m.connection_state != nil { + fields = append(fields, runtimebroker.FieldConnectionState) + } + if m.last_heartbeat != nil { + fields = append(fields, runtimebroker.FieldLastHeartbeat) + } + if m.capabilities != nil { + fields = append(fields, runtimebroker.FieldCapabilities) + } + if m.supported_harnesses != nil { + fields = append(fields, runtimebroker.FieldSupportedHarnesses) + } + if m.resources != nil { + fields = append(fields, runtimebroker.FieldResources) + } + if m.runtimes != nil { + fields = append(fields, runtimebroker.FieldRuntimes) + } + if m.labels != nil { + fields = append(fields, runtimebroker.FieldLabels) + } + if m.annotations != nil { + fields = append(fields, runtimebroker.FieldAnnotations) + } + if m.endpoint != nil { + fields = append(fields, runtimebroker.FieldEndpoint) + } + if m.created_by != nil { + fields = append(fields, runtimebroker.FieldCreatedBy) + } + if m.auto_provide != nil { + fields = append(fields, runtimebroker.FieldAutoProvide) + } + if m.connected_hub_id != nil { + fields = append(fields, runtimebroker.FieldConnectedHubID) + } + if m.connected_session_id != nil { + fields = append(fields, runtimebroker.FieldConnectedSessionID) + } + if m.connected_at != nil { + fields = append(fields, runtimebroker.FieldConnectedAt) + } + if m.created != nil { + fields = append(fields, runtimebroker.FieldCreated) + } + if m.updated != nil { + fields = append(fields, runtimebroker.FieldUpdated) + } + return fields +} + +// Field returns the value of a field with the given name. The second boolean +// return value indicates that this field was not set, or was not defined in the +// schema. +func (m *RuntimeBrokerMutation) Field(name string) (ent.Value, bool) { + switch name { + case runtimebroker.FieldName: + return m.Name() + case runtimebroker.FieldSlug: + return m.Slug() + case runtimebroker.FieldType: + return m.GetType() + case runtimebroker.FieldMode: + return m.Mode() + case runtimebroker.FieldVersion: + return m.Version() + case runtimebroker.FieldLockVersion: + return m.LockVersion() + case runtimebroker.FieldStatus: + return m.Status() + case runtimebroker.FieldConnectionState: + return m.ConnectionState() + case runtimebroker.FieldLastHeartbeat: + return m.LastHeartbeat() + case runtimebroker.FieldCapabilities: + return m.Capabilities() + case runtimebroker.FieldSupportedHarnesses: + return m.SupportedHarnesses() + case runtimebroker.FieldResources: + return m.Resources() + case runtimebroker.FieldRuntimes: + return m.Runtimes() + case runtimebroker.FieldLabels: + return m.Labels() + case runtimebroker.FieldAnnotations: + return m.Annotations() + case runtimebroker.FieldEndpoint: + return m.Endpoint() + case runtimebroker.FieldCreatedBy: + return m.CreatedBy() + case runtimebroker.FieldAutoProvide: + return m.AutoProvide() + case runtimebroker.FieldConnectedHubID: + return m.ConnectedHubID() + case runtimebroker.FieldConnectedSessionID: + return m.ConnectedSessionID() + case runtimebroker.FieldConnectedAt: + return m.ConnectedAt() + case runtimebroker.FieldCreated: + return m.Created() + case runtimebroker.FieldUpdated: + return m.Updated() + } + return nil, false +} + +// OldField returns the old value of the field from the database. An error is +// returned if the mutation operation is not UpdateOne, or the query to the +// database failed. +func (m *RuntimeBrokerMutation) OldField(ctx context.Context, name string) (ent.Value, error) { + switch name { + case runtimebroker.FieldName: + return m.OldName(ctx) + case runtimebroker.FieldSlug: + return m.OldSlug(ctx) + case runtimebroker.FieldType: + return m.OldType(ctx) + case runtimebroker.FieldMode: + return m.OldMode(ctx) + case runtimebroker.FieldVersion: + return m.OldVersion(ctx) + case runtimebroker.FieldLockVersion: + return m.OldLockVersion(ctx) + case runtimebroker.FieldStatus: + return m.OldStatus(ctx) + case runtimebroker.FieldConnectionState: + return m.OldConnectionState(ctx) + case runtimebroker.FieldLastHeartbeat: + return m.OldLastHeartbeat(ctx) + case runtimebroker.FieldCapabilities: + return m.OldCapabilities(ctx) + case runtimebroker.FieldSupportedHarnesses: + return m.OldSupportedHarnesses(ctx) + case runtimebroker.FieldResources: + return m.OldResources(ctx) + case runtimebroker.FieldRuntimes: + return m.OldRuntimes(ctx) + case runtimebroker.FieldLabels: + return m.OldLabels(ctx) + case runtimebroker.FieldAnnotations: + return m.OldAnnotations(ctx) + case runtimebroker.FieldEndpoint: + return m.OldEndpoint(ctx) + case runtimebroker.FieldCreatedBy: + return m.OldCreatedBy(ctx) + case runtimebroker.FieldAutoProvide: + return m.OldAutoProvide(ctx) + case runtimebroker.FieldConnectedHubID: + return m.OldConnectedHubID(ctx) + case runtimebroker.FieldConnectedSessionID: + return m.OldConnectedSessionID(ctx) + case runtimebroker.FieldConnectedAt: + return m.OldConnectedAt(ctx) + case runtimebroker.FieldCreated: + return m.OldCreated(ctx) + case runtimebroker.FieldUpdated: + return m.OldUpdated(ctx) + } + return nil, fmt.Errorf("unknown RuntimeBroker field %s", name) +} + +// SetField sets the value of a field with the given name. It returns an error if +// the field is not defined in the schema, or if the type mismatched the field +// type. +func (m *RuntimeBrokerMutation) SetField(name string, value ent.Value) error { + switch name { + case runtimebroker.FieldName: + v, ok := value.(string) + if !ok { + return fmt.Errorf("unexpected type %T for field %s", value, name) + } + m.SetName(v) + return nil + case runtimebroker.FieldSlug: + v, ok := value.(string) + if !ok { + return fmt.Errorf("unexpected type %T for field %s", value, name) + } + m.SetSlug(v) + return nil + case runtimebroker.FieldType: + v, ok := value.(string) + if !ok { + return fmt.Errorf("unexpected type %T for field %s", value, name) + } + m.SetType(v) + return nil + case runtimebroker.FieldMode: + v, ok := value.(string) + if !ok { + return fmt.Errorf("unexpected type %T for field %s", value, name) + } + m.SetMode(v) + return nil + case runtimebroker.FieldVersion: + v, ok := value.(string) + if !ok { + return fmt.Errorf("unexpected type %T for field %s", value, name) + } + m.SetVersion(v) + return nil + case runtimebroker.FieldLockVersion: + v, ok := value.(int64) + if !ok { + return fmt.Errorf("unexpected type %T for field %s", value, name) + } + m.SetLockVersion(v) + return nil + case runtimebroker.FieldStatus: + v, ok := value.(string) + if !ok { + return fmt.Errorf("unexpected type %T for field %s", value, name) + } + m.SetStatus(v) + return nil + case runtimebroker.FieldConnectionState: + v, ok := value.(string) + if !ok { + return fmt.Errorf("unexpected type %T for field %s", value, name) + } + m.SetConnectionState(v) + return nil + case runtimebroker.FieldLastHeartbeat: + v, ok := value.(time.Time) + if !ok { + return fmt.Errorf("unexpected type %T for field %s", value, name) + } + m.SetLastHeartbeat(v) + return nil + case runtimebroker.FieldCapabilities: + v, ok := value.(string) + if !ok { + return fmt.Errorf("unexpected type %T for field %s", value, name) + } + m.SetCapabilities(v) + return nil + case runtimebroker.FieldSupportedHarnesses: + v, ok := value.(string) + if !ok { + return fmt.Errorf("unexpected type %T for field %s", value, name) + } + m.SetSupportedHarnesses(v) + return nil + case runtimebroker.FieldResources: + v, ok := value.(string) + if !ok { + return fmt.Errorf("unexpected type %T for field %s", value, name) + } + m.SetResources(v) + return nil + case runtimebroker.FieldRuntimes: + v, ok := value.(string) + if !ok { + return fmt.Errorf("unexpected type %T for field %s", value, name) + } + m.SetRuntimes(v) + return nil + case runtimebroker.FieldLabels: + v, ok := value.(string) + if !ok { + return fmt.Errorf("unexpected type %T for field %s", value, name) + } + m.SetLabels(v) + return nil + case runtimebroker.FieldAnnotations: + v, ok := value.(string) + if !ok { + return fmt.Errorf("unexpected type %T for field %s", value, name) + } + m.SetAnnotations(v) + return nil + case runtimebroker.FieldEndpoint: + v, ok := value.(string) + if !ok { + return fmt.Errorf("unexpected type %T for field %s", value, name) + } + m.SetEndpoint(v) + return nil + case runtimebroker.FieldCreatedBy: + v, ok := value.(string) + if !ok { + return fmt.Errorf("unexpected type %T for field %s", value, name) + } + m.SetCreatedBy(v) + return nil + case runtimebroker.FieldAutoProvide: + v, ok := value.(bool) + if !ok { + return fmt.Errorf("unexpected type %T for field %s", value, name) + } + m.SetAutoProvide(v) + return nil + case runtimebroker.FieldConnectedHubID: + v, ok := value.(string) + if !ok { + return fmt.Errorf("unexpected type %T for field %s", value, name) + } + m.SetConnectedHubID(v) + return nil + case runtimebroker.FieldConnectedSessionID: + v, ok := value.(string) + if !ok { + return fmt.Errorf("unexpected type %T for field %s", value, name) + } + m.SetConnectedSessionID(v) + return nil + case runtimebroker.FieldConnectedAt: + v, ok := value.(time.Time) + if !ok { + return fmt.Errorf("unexpected type %T for field %s", value, name) + } + m.SetConnectedAt(v) + return nil + case runtimebroker.FieldCreated: + v, ok := value.(time.Time) + if !ok { + return fmt.Errorf("unexpected type %T for field %s", value, name) + } + m.SetCreated(v) + return nil + case runtimebroker.FieldUpdated: + v, ok := value.(time.Time) + if !ok { + return fmt.Errorf("unexpected type %T for field %s", value, name) + } + m.SetUpdated(v) + return nil + } + return fmt.Errorf("unknown RuntimeBroker field %s", name) +} + +// AddedFields returns all numeric fields that were incremented/decremented during +// this mutation. +func (m *RuntimeBrokerMutation) AddedFields() []string { + var fields []string + if m.addlock_version != nil { + fields = append(fields, runtimebroker.FieldLockVersion) + } + return fields +} + +// AddedField returns the numeric value that was incremented/decremented on a field +// with the given name. The second boolean return value indicates that this field +// was not set, or was not defined in the schema. +func (m *RuntimeBrokerMutation) AddedField(name string) (ent.Value, bool) { + switch name { + case runtimebroker.FieldLockVersion: + return m.AddedLockVersion() + } + return nil, false +} + +// AddField adds the value to the field with the given name. It returns an error if +// the field is not defined in the schema, or if the type mismatched the field +// type. +func (m *RuntimeBrokerMutation) AddField(name string, value ent.Value) error { + switch name { + case runtimebroker.FieldLockVersion: + v, ok := value.(int64) + if !ok { + return fmt.Errorf("unexpected type %T for field %s", value, name) + } + m.AddLockVersion(v) + return nil + } + return fmt.Errorf("unknown RuntimeBroker numeric field %s", name) +} + +// ClearedFields returns all nullable fields that were cleared during this +// mutation. +func (m *RuntimeBrokerMutation) ClearedFields() []string { + var fields []string + if m.FieldCleared(runtimebroker.FieldType) { + fields = append(fields, runtimebroker.FieldType) + } + if m.FieldCleared(runtimebroker.FieldVersion) { + fields = append(fields, runtimebroker.FieldVersion) + } + if m.FieldCleared(runtimebroker.FieldLastHeartbeat) { + fields = append(fields, runtimebroker.FieldLastHeartbeat) + } + if m.FieldCleared(runtimebroker.FieldCapabilities) { + fields = append(fields, runtimebroker.FieldCapabilities) + } + if m.FieldCleared(runtimebroker.FieldSupportedHarnesses) { + fields = append(fields, runtimebroker.FieldSupportedHarnesses) + } + if m.FieldCleared(runtimebroker.FieldResources) { + fields = append(fields, runtimebroker.FieldResources) + } + if m.FieldCleared(runtimebroker.FieldRuntimes) { + fields = append(fields, runtimebroker.FieldRuntimes) + } + if m.FieldCleared(runtimebroker.FieldLabels) { + fields = append(fields, runtimebroker.FieldLabels) + } + if m.FieldCleared(runtimebroker.FieldAnnotations) { + fields = append(fields, runtimebroker.FieldAnnotations) + } + if m.FieldCleared(runtimebroker.FieldEndpoint) { + fields = append(fields, runtimebroker.FieldEndpoint) + } + if m.FieldCleared(runtimebroker.FieldCreatedBy) { + fields = append(fields, runtimebroker.FieldCreatedBy) + } + if m.FieldCleared(runtimebroker.FieldConnectedHubID) { + fields = append(fields, runtimebroker.FieldConnectedHubID) + } + if m.FieldCleared(runtimebroker.FieldConnectedSessionID) { + fields = append(fields, runtimebroker.FieldConnectedSessionID) + } + if m.FieldCleared(runtimebroker.FieldConnectedAt) { + fields = append(fields, runtimebroker.FieldConnectedAt) + } + return fields +} + +// FieldCleared returns a boolean indicating if a field with the given name was +// cleared in this mutation. +func (m *RuntimeBrokerMutation) FieldCleared(name string) bool { + _, ok := m.clearedFields[name] + return ok +} + +// ClearField clears the value of the field with the given name. It returns an +// error if the field is not defined in the schema. +func (m *RuntimeBrokerMutation) ClearField(name string) error { + switch name { + case runtimebroker.FieldType: + m.ClearType() + return nil + case runtimebroker.FieldVersion: + m.ClearVersion() + return nil + case runtimebroker.FieldLastHeartbeat: + m.ClearLastHeartbeat() + return nil + case runtimebroker.FieldCapabilities: + m.ClearCapabilities() + return nil + case runtimebroker.FieldSupportedHarnesses: + m.ClearSupportedHarnesses() + return nil + case runtimebroker.FieldResources: + m.ClearResources() + return nil + case runtimebroker.FieldRuntimes: + m.ClearRuntimes() + return nil + case runtimebroker.FieldLabels: + m.ClearLabels() + return nil + case runtimebroker.FieldAnnotations: + m.ClearAnnotations() + return nil + case runtimebroker.FieldEndpoint: + m.ClearEndpoint() + return nil + case runtimebroker.FieldCreatedBy: + m.ClearCreatedBy() + return nil + case runtimebroker.FieldConnectedHubID: + m.ClearConnectedHubID() + return nil + case runtimebroker.FieldConnectedSessionID: + m.ClearConnectedSessionID() + return nil + case runtimebroker.FieldConnectedAt: + m.ClearConnectedAt() + return nil + } + return fmt.Errorf("unknown RuntimeBroker nullable field %s", name) +} + +// ResetField resets all changes in the mutation for the field with the given name. +// It returns an error if the field is not defined in the schema. +func (m *RuntimeBrokerMutation) ResetField(name string) error { + switch name { + case runtimebroker.FieldName: + m.ResetName() + return nil + case runtimebroker.FieldSlug: + m.ResetSlug() + return nil + case runtimebroker.FieldType: + m.ResetType() + return nil + case runtimebroker.FieldMode: + m.ResetMode() + return nil + case runtimebroker.FieldVersion: + m.ResetVersion() + return nil + case runtimebroker.FieldLockVersion: + m.ResetLockVersion() + return nil + case runtimebroker.FieldStatus: + m.ResetStatus() + return nil + case runtimebroker.FieldConnectionState: + m.ResetConnectionState() + return nil + case runtimebroker.FieldLastHeartbeat: + m.ResetLastHeartbeat() + return nil + case runtimebroker.FieldCapabilities: + m.ResetCapabilities() + return nil + case runtimebroker.FieldSupportedHarnesses: + m.ResetSupportedHarnesses() + return nil + case runtimebroker.FieldResources: + m.ResetResources() + return nil + case runtimebroker.FieldRuntimes: + m.ResetRuntimes() + return nil + case runtimebroker.FieldLabels: + m.ResetLabels() + return nil + case runtimebroker.FieldAnnotations: + m.ResetAnnotations() + return nil + case runtimebroker.FieldEndpoint: + m.ResetEndpoint() + return nil + case runtimebroker.FieldCreatedBy: + m.ResetCreatedBy() + return nil + case runtimebroker.FieldAutoProvide: + m.ResetAutoProvide() + return nil + case runtimebroker.FieldConnectedHubID: + m.ResetConnectedHubID() + return nil + case runtimebroker.FieldConnectedSessionID: + m.ResetConnectedSessionID() + return nil + case runtimebroker.FieldConnectedAt: + m.ResetConnectedAt() + return nil + case runtimebroker.FieldCreated: + m.ResetCreated() + return nil + case runtimebroker.FieldUpdated: + m.ResetUpdated() + return nil + } + return fmt.Errorf("unknown RuntimeBroker field %s", name) +} + +// AddedEdges returns all edge names that were set/added in this mutation. +func (m *RuntimeBrokerMutation) AddedEdges() []string { + edges := make([]string, 0, 0) + return edges +} + +// AddedIDs returns all IDs (to other nodes) that were added for the given edge +// name in this mutation. +func (m *RuntimeBrokerMutation) AddedIDs(name string) []ent.Value { + return nil +} + +// RemovedEdges returns all edge names that were removed in this mutation. +func (m *RuntimeBrokerMutation) RemovedEdges() []string { + edges := make([]string, 0, 0) + return edges +} + +// RemovedIDs returns all IDs (to other nodes) that were removed for the edge with +// the given name in this mutation. +func (m *RuntimeBrokerMutation) RemovedIDs(name string) []ent.Value { + return nil +} + +// ClearedEdges returns all edge names that were cleared in this mutation. +func (m *RuntimeBrokerMutation) ClearedEdges() []string { + edges := make([]string, 0, 0) + return edges +} + +// EdgeCleared returns a boolean which indicates if the edge with the given name +// was cleared in this mutation. +func (m *RuntimeBrokerMutation) EdgeCleared(name string) bool { + return false +} + +// ClearEdge clears the value of the edge with the given name. It returns an error +// if that edge is not defined in the schema. +func (m *RuntimeBrokerMutation) ClearEdge(name string) error { + return fmt.Errorf("unknown RuntimeBroker unique edge %s", name) +} + +// ResetEdge resets all changes to the edge with the given name in this mutation. +// It returns an error if the edge is not defined in the schema. +func (m *RuntimeBrokerMutation) ResetEdge(name string) error { + return fmt.Errorf("unknown RuntimeBroker edge %s", name) +} + +// ScheduleMutation represents an operation that mutates the Schedule nodes in the graph. +type ScheduleMutation struct { + config + op Op + typ string + id *uuid.UUID + project_id *uuid.UUID + name *string + cron_expr *string + event_type *string + payload *string + status *string + next_run_at *time.Time + last_run_at *time.Time + last_run_status *string + last_run_error *string + run_count *int + addrun_count *int + error_count *int + adderror_count *int + created_by *string + created *time.Time + updated *time.Time + clearedFields map[string]struct{} + done bool + oldValue func(context.Context) (*Schedule, error) + predicates []predicate.Schedule +} + +var _ ent.Mutation = (*ScheduleMutation)(nil) + +// scheduleOption allows management of the mutation configuration using functional options. +type scheduleOption func(*ScheduleMutation) + +// newScheduleMutation creates new mutation for the Schedule entity. +func newScheduleMutation(c config, op Op, opts ...scheduleOption) *ScheduleMutation { + m := &ScheduleMutation{ + config: c, + op: op, + typ: TypeSchedule, + clearedFields: make(map[string]struct{}), + } + for _, opt := range opts { + opt(m) + } + return m +} + +// withScheduleID sets the ID field of the mutation. +func withScheduleID(id uuid.UUID) scheduleOption { + return func(m *ScheduleMutation) { + var ( + err error + once sync.Once + value *Schedule + ) + m.oldValue = func(ctx context.Context) (*Schedule, error) { + once.Do(func() { + if m.done { + err = errors.New("querying old values post mutation is not allowed") + } else { + value, err = m.Client().Schedule.Get(ctx, id) + } + }) + return value, err + } + m.id = &id + } +} + +// withSchedule sets the old Schedule of the mutation. +func withSchedule(node *Schedule) scheduleOption { + return func(m *ScheduleMutation) { + m.oldValue = func(context.Context) (*Schedule, error) { + return node, nil + } + m.id = &node.ID + } +} + +// Client returns a new `ent.Client` from the mutation. If the mutation was +// executed in a transaction (ent.Tx), a transactional client is returned. +func (m ScheduleMutation) Client() *Client { + client := &Client{config: m.config} + client.init() + return client +} + +// Tx returns an `ent.Tx` for mutations that were executed in transactions; +// it returns an error otherwise. +func (m ScheduleMutation) Tx() (*Tx, error) { + if _, ok := m.driver.(*txDriver); !ok { + return nil, errors.New("ent: mutation is not running in a transaction") + } + tx := &Tx{config: m.config} + tx.init() + return tx, nil +} + +// SetID sets the value of the id field. Note that this +// operation is only accepted on creation of Schedule entities. +func (m *ScheduleMutation) SetID(id uuid.UUID) { + m.id = &id +} + +// ID returns the ID value in the mutation. Note that the ID is only available +// if it was provided to the builder or after it was returned from the database. +func (m *ScheduleMutation) ID() (id uuid.UUID, exists bool) { + if m.id == nil { + return + } + return *m.id, true +} + +// IDs queries the database and returns the entity ids that match the mutation's predicate. +// That means, if the mutation is applied within a transaction with an isolation level such +// as sql.LevelSerializable, the returned ids match the ids of the rows that will be updated +// or updated by the mutation. +func (m *ScheduleMutation) IDs(ctx context.Context) ([]uuid.UUID, error) { + switch { + case m.op.Is(OpUpdateOne | OpDeleteOne): + id, exists := m.ID() + if exists { + return []uuid.UUID{id}, nil + } + fallthrough + case m.op.Is(OpUpdate | OpDelete): + return m.Client().Schedule.Query().Where(m.predicates...).IDs(ctx) + default: + return nil, fmt.Errorf("IDs is not allowed on %s operations", m.op) + } +} + +// SetProjectID sets the "project_id" field. +func (m *ScheduleMutation) SetProjectID(u uuid.UUID) { + m.project_id = &u +} + +// ProjectID returns the value of the "project_id" field in the mutation. +func (m *ScheduleMutation) ProjectID() (r uuid.UUID, exists bool) { + v := m.project_id + if v == nil { + return + } + return *v, true +} + +// OldProjectID returns the old "project_id" field's value of the Schedule entity. +// If the Schedule object wasn't provided to the builder, the object is fetched from the database. +// An error is returned if the mutation operation is not UpdateOne, or the database query fails. +func (m *ScheduleMutation) OldProjectID(ctx context.Context) (v uuid.UUID, err error) { + if !m.op.Is(OpUpdateOne) { + return v, errors.New("OldProjectID is only allowed on UpdateOne operations") + } + if m.id == nil || m.oldValue == nil { + return v, errors.New("OldProjectID requires an ID field in the mutation") + } + oldValue, err := m.oldValue(ctx) + if err != nil { + return v, fmt.Errorf("querying old value for OldProjectID: %w", err) + } + return oldValue.ProjectID, nil +} + +// ResetProjectID resets all changes to the "project_id" field. +func (m *ScheduleMutation) ResetProjectID() { + m.project_id = nil +} + +// SetName sets the "name" field. +func (m *ScheduleMutation) SetName(s string) { + m.name = &s +} + +// Name returns the value of the "name" field in the mutation. +func (m *ScheduleMutation) Name() (r string, exists bool) { + v := m.name + if v == nil { + return + } + return *v, true +} + +// OldName returns the old "name" field's value of the Schedule entity. +// If the Schedule object wasn't provided to the builder, the object is fetched from the database. +// An error is returned if the mutation operation is not UpdateOne, or the database query fails. +func (m *ScheduleMutation) OldName(ctx context.Context) (v string, err error) { + if !m.op.Is(OpUpdateOne) { + return v, errors.New("OldName is only allowed on UpdateOne operations") + } + if m.id == nil || m.oldValue == nil { + return v, errors.New("OldName requires an ID field in the mutation") + } + oldValue, err := m.oldValue(ctx) + if err != nil { + return v, fmt.Errorf("querying old value for OldName: %w", err) + } + return oldValue.Name, nil +} + +// ResetName resets all changes to the "name" field. +func (m *ScheduleMutation) ResetName() { + m.name = nil +} + +// SetCronExpr sets the "cron_expr" field. +func (m *ScheduleMutation) SetCronExpr(s string) { + m.cron_expr = &s +} + +// CronExpr returns the value of the "cron_expr" field in the mutation. +func (m *ScheduleMutation) CronExpr() (r string, exists bool) { + v := m.cron_expr + if v == nil { + return + } + return *v, true +} + +// OldCronExpr returns the old "cron_expr" field's value of the Schedule entity. +// If the Schedule object wasn't provided to the builder, the object is fetched from the database. +// An error is returned if the mutation operation is not UpdateOne, or the database query fails. +func (m *ScheduleMutation) OldCronExpr(ctx context.Context) (v string, err error) { + if !m.op.Is(OpUpdateOne) { + return v, errors.New("OldCronExpr is only allowed on UpdateOne operations") + } + if m.id == nil || m.oldValue == nil { + return v, errors.New("OldCronExpr requires an ID field in the mutation") + } + oldValue, err := m.oldValue(ctx) + if err != nil { + return v, fmt.Errorf("querying old value for OldCronExpr: %w", err) + } + return oldValue.CronExpr, nil +} + +// ResetCronExpr resets all changes to the "cron_expr" field. +func (m *ScheduleMutation) ResetCronExpr() { + m.cron_expr = nil +} + +// SetEventType sets the "event_type" field. +func (m *ScheduleMutation) SetEventType(s string) { + m.event_type = &s +} + +// EventType returns the value of the "event_type" field in the mutation. +func (m *ScheduleMutation) EventType() (r string, exists bool) { + v := m.event_type + if v == nil { + return + } + return *v, true +} + +// OldEventType returns the old "event_type" field's value of the Schedule entity. +// If the Schedule object wasn't provided to the builder, the object is fetched from the database. +// An error is returned if the mutation operation is not UpdateOne, or the database query fails. +func (m *ScheduleMutation) OldEventType(ctx context.Context) (v string, err error) { + if !m.op.Is(OpUpdateOne) { + return v, errors.New("OldEventType is only allowed on UpdateOne operations") + } + if m.id == nil || m.oldValue == nil { + return v, errors.New("OldEventType requires an ID field in the mutation") + } + oldValue, err := m.oldValue(ctx) + if err != nil { + return v, fmt.Errorf("querying old value for OldEventType: %w", err) + } + return oldValue.EventType, nil +} + +// ResetEventType resets all changes to the "event_type" field. +func (m *ScheduleMutation) ResetEventType() { + m.event_type = nil +} + +// SetPayload sets the "payload" field. +func (m *ScheduleMutation) SetPayload(s string) { + m.payload = &s +} + +// Payload returns the value of the "payload" field in the mutation. +func (m *ScheduleMutation) Payload() (r string, exists bool) { + v := m.payload + if v == nil { + return + } + return *v, true +} + +// OldPayload returns the old "payload" field's value of the Schedule entity. +// If the Schedule object wasn't provided to the builder, the object is fetched from the database. +// An error is returned if the mutation operation is not UpdateOne, or the database query fails. +func (m *ScheduleMutation) OldPayload(ctx context.Context) (v string, err error) { + if !m.op.Is(OpUpdateOne) { + return v, errors.New("OldPayload is only allowed on UpdateOne operations") + } + if m.id == nil || m.oldValue == nil { + return v, errors.New("OldPayload requires an ID field in the mutation") + } + oldValue, err := m.oldValue(ctx) + if err != nil { + return v, fmt.Errorf("querying old value for OldPayload: %w", err) + } + return oldValue.Payload, nil +} + +// ResetPayload resets all changes to the "payload" field. +func (m *ScheduleMutation) ResetPayload() { + m.payload = nil +} + +// SetStatus sets the "status" field. +func (m *ScheduleMutation) SetStatus(s string) { + m.status = &s +} + +// Status returns the value of the "status" field in the mutation. +func (m *ScheduleMutation) Status() (r string, exists bool) { + v := m.status + if v == nil { + return + } + return *v, true +} + +// OldStatus returns the old "status" field's value of the Schedule entity. +// If the Schedule object wasn't provided to the builder, the object is fetched from the database. +// An error is returned if the mutation operation is not UpdateOne, or the database query fails. +func (m *ScheduleMutation) OldStatus(ctx context.Context) (v string, err error) { + if !m.op.Is(OpUpdateOne) { + return v, errors.New("OldStatus is only allowed on UpdateOne operations") + } + if m.id == nil || m.oldValue == nil { + return v, errors.New("OldStatus requires an ID field in the mutation") + } + oldValue, err := m.oldValue(ctx) + if err != nil { + return v, fmt.Errorf("querying old value for OldStatus: %w", err) + } + return oldValue.Status, nil +} + +// ResetStatus resets all changes to the "status" field. +func (m *ScheduleMutation) ResetStatus() { + m.status = nil +} + +// SetNextRunAt sets the "next_run_at" field. +func (m *ScheduleMutation) SetNextRunAt(t time.Time) { + m.next_run_at = &t +} + +// NextRunAt returns the value of the "next_run_at" field in the mutation. +func (m *ScheduleMutation) NextRunAt() (r time.Time, exists bool) { + v := m.next_run_at + if v == nil { + return + } + return *v, true +} + +// OldNextRunAt returns the old "next_run_at" field's value of the Schedule entity. +// If the Schedule object wasn't provided to the builder, the object is fetched from the database. +// An error is returned if the mutation operation is not UpdateOne, or the database query fails. +func (m *ScheduleMutation) OldNextRunAt(ctx context.Context) (v *time.Time, err error) { + if !m.op.Is(OpUpdateOne) { + return v, errors.New("OldNextRunAt is only allowed on UpdateOne operations") + } + if m.id == nil || m.oldValue == nil { + return v, errors.New("OldNextRunAt requires an ID field in the mutation") + } + oldValue, err := m.oldValue(ctx) + if err != nil { + return v, fmt.Errorf("querying old value for OldNextRunAt: %w", err) + } + return oldValue.NextRunAt, nil +} + +// ClearNextRunAt clears the value of the "next_run_at" field. +func (m *ScheduleMutation) ClearNextRunAt() { + m.next_run_at = nil + m.clearedFields[schedule.FieldNextRunAt] = struct{}{} +} + +// NextRunAtCleared returns if the "next_run_at" field was cleared in this mutation. +func (m *ScheduleMutation) NextRunAtCleared() bool { + _, ok := m.clearedFields[schedule.FieldNextRunAt] + return ok +} + +// ResetNextRunAt resets all changes to the "next_run_at" field. +func (m *ScheduleMutation) ResetNextRunAt() { + m.next_run_at = nil + delete(m.clearedFields, schedule.FieldNextRunAt) +} + +// SetLastRunAt sets the "last_run_at" field. +func (m *ScheduleMutation) SetLastRunAt(t time.Time) { + m.last_run_at = &t +} + +// LastRunAt returns the value of the "last_run_at" field in the mutation. +func (m *ScheduleMutation) LastRunAt() (r time.Time, exists bool) { + v := m.last_run_at + if v == nil { + return + } + return *v, true +} + +// OldLastRunAt returns the old "last_run_at" field's value of the Schedule entity. +// If the Schedule object wasn't provided to the builder, the object is fetched from the database. +// An error is returned if the mutation operation is not UpdateOne, or the database query fails. +func (m *ScheduleMutation) OldLastRunAt(ctx context.Context) (v *time.Time, err error) { + if !m.op.Is(OpUpdateOne) { + return v, errors.New("OldLastRunAt is only allowed on UpdateOne operations") + } + if m.id == nil || m.oldValue == nil { + return v, errors.New("OldLastRunAt requires an ID field in the mutation") + } + oldValue, err := m.oldValue(ctx) + if err != nil { + return v, fmt.Errorf("querying old value for OldLastRunAt: %w", err) + } + return oldValue.LastRunAt, nil +} + +// ClearLastRunAt clears the value of the "last_run_at" field. +func (m *ScheduleMutation) ClearLastRunAt() { + m.last_run_at = nil + m.clearedFields[schedule.FieldLastRunAt] = struct{}{} +} + +// LastRunAtCleared returns if the "last_run_at" field was cleared in this mutation. +func (m *ScheduleMutation) LastRunAtCleared() bool { + _, ok := m.clearedFields[schedule.FieldLastRunAt] + return ok +} + +// ResetLastRunAt resets all changes to the "last_run_at" field. +func (m *ScheduleMutation) ResetLastRunAt() { + m.last_run_at = nil + delete(m.clearedFields, schedule.FieldLastRunAt) +} + +// SetLastRunStatus sets the "last_run_status" field. +func (m *ScheduleMutation) SetLastRunStatus(s string) { + m.last_run_status = &s +} + +// LastRunStatus returns the value of the "last_run_status" field in the mutation. +func (m *ScheduleMutation) LastRunStatus() (r string, exists bool) { + v := m.last_run_status + if v == nil { + return + } + return *v, true +} + +// OldLastRunStatus returns the old "last_run_status" field's value of the Schedule entity. +// If the Schedule object wasn't provided to the builder, the object is fetched from the database. +// An error is returned if the mutation operation is not UpdateOne, or the database query fails. +func (m *ScheduleMutation) OldLastRunStatus(ctx context.Context) (v string, err error) { + if !m.op.Is(OpUpdateOne) { + return v, errors.New("OldLastRunStatus is only allowed on UpdateOne operations") + } + if m.id == nil || m.oldValue == nil { + return v, errors.New("OldLastRunStatus requires an ID field in the mutation") + } + oldValue, err := m.oldValue(ctx) + if err != nil { + return v, fmt.Errorf("querying old value for OldLastRunStatus: %w", err) + } + return oldValue.LastRunStatus, nil +} + +// ClearLastRunStatus clears the value of the "last_run_status" field. +func (m *ScheduleMutation) ClearLastRunStatus() { + m.last_run_status = nil + m.clearedFields[schedule.FieldLastRunStatus] = struct{}{} +} + +// LastRunStatusCleared returns if the "last_run_status" field was cleared in this mutation. +func (m *ScheduleMutation) LastRunStatusCleared() bool { + _, ok := m.clearedFields[schedule.FieldLastRunStatus] + return ok +} + +// ResetLastRunStatus resets all changes to the "last_run_status" field. +func (m *ScheduleMutation) ResetLastRunStatus() { + m.last_run_status = nil + delete(m.clearedFields, schedule.FieldLastRunStatus) +} + +// SetLastRunError sets the "last_run_error" field. +func (m *ScheduleMutation) SetLastRunError(s string) { + m.last_run_error = &s +} + +// LastRunError returns the value of the "last_run_error" field in the mutation. +func (m *ScheduleMutation) LastRunError() (r string, exists bool) { + v := m.last_run_error + if v == nil { + return + } + return *v, true +} + +// OldLastRunError returns the old "last_run_error" field's value of the Schedule entity. +// If the Schedule object wasn't provided to the builder, the object is fetched from the database. +// An error is returned if the mutation operation is not UpdateOne, or the database query fails. +func (m *ScheduleMutation) OldLastRunError(ctx context.Context) (v string, err error) { + if !m.op.Is(OpUpdateOne) { + return v, errors.New("OldLastRunError is only allowed on UpdateOne operations") + } + if m.id == nil || m.oldValue == nil { + return v, errors.New("OldLastRunError requires an ID field in the mutation") + } + oldValue, err := m.oldValue(ctx) + if err != nil { + return v, fmt.Errorf("querying old value for OldLastRunError: %w", err) + } + return oldValue.LastRunError, nil +} + +// ClearLastRunError clears the value of the "last_run_error" field. +func (m *ScheduleMutation) ClearLastRunError() { + m.last_run_error = nil + m.clearedFields[schedule.FieldLastRunError] = struct{}{} +} + +// LastRunErrorCleared returns if the "last_run_error" field was cleared in this mutation. +func (m *ScheduleMutation) LastRunErrorCleared() bool { + _, ok := m.clearedFields[schedule.FieldLastRunError] + return ok +} + +// ResetLastRunError resets all changes to the "last_run_error" field. +func (m *ScheduleMutation) ResetLastRunError() { + m.last_run_error = nil + delete(m.clearedFields, schedule.FieldLastRunError) +} + +// SetRunCount sets the "run_count" field. +func (m *ScheduleMutation) SetRunCount(i int) { + m.run_count = &i + m.addrun_count = nil +} + +// RunCount returns the value of the "run_count" field in the mutation. +func (m *ScheduleMutation) RunCount() (r int, exists bool) { + v := m.run_count + if v == nil { + return + } + return *v, true +} + +// OldRunCount returns the old "run_count" field's value of the Schedule entity. +// If the Schedule object wasn't provided to the builder, the object is fetched from the database. +// An error is returned if the mutation operation is not UpdateOne, or the database query fails. +func (m *ScheduleMutation) OldRunCount(ctx context.Context) (v int, err error) { + if !m.op.Is(OpUpdateOne) { + return v, errors.New("OldRunCount is only allowed on UpdateOne operations") + } + if m.id == nil || m.oldValue == nil { + return v, errors.New("OldRunCount requires an ID field in the mutation") + } + oldValue, err := m.oldValue(ctx) + if err != nil { + return v, fmt.Errorf("querying old value for OldRunCount: %w", err) + } + return oldValue.RunCount, nil +} + +// AddRunCount adds i to the "run_count" field. +func (m *ScheduleMutation) AddRunCount(i int) { + if m.addrun_count != nil { + *m.addrun_count += i + } else { + m.addrun_count = &i + } +} + +// AddedRunCount returns the value that was added to the "run_count" field in this mutation. +func (m *ScheduleMutation) AddedRunCount() (r int, exists bool) { + v := m.addrun_count + if v == nil { + return + } + return *v, true +} + +// ResetRunCount resets all changes to the "run_count" field. +func (m *ScheduleMutation) ResetRunCount() { + m.run_count = nil + m.addrun_count = nil +} + +// SetErrorCount sets the "error_count" field. +func (m *ScheduleMutation) SetErrorCount(i int) { + m.error_count = &i + m.adderror_count = nil +} + +// ErrorCount returns the value of the "error_count" field in the mutation. +func (m *ScheduleMutation) ErrorCount() (r int, exists bool) { + v := m.error_count + if v == nil { + return + } + return *v, true +} + +// OldErrorCount returns the old "error_count" field's value of the Schedule entity. +// If the Schedule object wasn't provided to the builder, the object is fetched from the database. +// An error is returned if the mutation operation is not UpdateOne, or the database query fails. +func (m *ScheduleMutation) OldErrorCount(ctx context.Context) (v int, err error) { + if !m.op.Is(OpUpdateOne) { + return v, errors.New("OldErrorCount is only allowed on UpdateOne operations") + } + if m.id == nil || m.oldValue == nil { + return v, errors.New("OldErrorCount requires an ID field in the mutation") + } + oldValue, err := m.oldValue(ctx) + if err != nil { + return v, fmt.Errorf("querying old value for OldErrorCount: %w", err) + } + return oldValue.ErrorCount, nil +} + +// AddErrorCount adds i to the "error_count" field. +func (m *ScheduleMutation) AddErrorCount(i int) { + if m.adderror_count != nil { + *m.adderror_count += i + } else { + m.adderror_count = &i + } +} + +// AddedErrorCount returns the value that was added to the "error_count" field in this mutation. +func (m *ScheduleMutation) AddedErrorCount() (r int, exists bool) { + v := m.adderror_count + if v == nil { + return + } + return *v, true +} + +// ResetErrorCount resets all changes to the "error_count" field. +func (m *ScheduleMutation) ResetErrorCount() { + m.error_count = nil + m.adderror_count = nil +} + +// SetCreatedBy sets the "created_by" field. +func (m *ScheduleMutation) SetCreatedBy(s string) { + m.created_by = &s +} + +// CreatedBy returns the value of the "created_by" field in the mutation. +func (m *ScheduleMutation) CreatedBy() (r string, exists bool) { + v := m.created_by + if v == nil { + return + } + return *v, true +} + +// OldCreatedBy returns the old "created_by" field's value of the Schedule entity. +// If the Schedule object wasn't provided to the builder, the object is fetched from the database. +// An error is returned if the mutation operation is not UpdateOne, or the database query fails. +func (m *ScheduleMutation) OldCreatedBy(ctx context.Context) (v string, err error) { + if !m.op.Is(OpUpdateOne) { + return v, errors.New("OldCreatedBy is only allowed on UpdateOne operations") + } + if m.id == nil || m.oldValue == nil { + return v, errors.New("OldCreatedBy requires an ID field in the mutation") + } + oldValue, err := m.oldValue(ctx) + if err != nil { + return v, fmt.Errorf("querying old value for OldCreatedBy: %w", err) + } + return oldValue.CreatedBy, nil +} + +// ClearCreatedBy clears the value of the "created_by" field. +func (m *ScheduleMutation) ClearCreatedBy() { + m.created_by = nil + m.clearedFields[schedule.FieldCreatedBy] = struct{}{} +} + +// CreatedByCleared returns if the "created_by" field was cleared in this mutation. +func (m *ScheduleMutation) CreatedByCleared() bool { + _, ok := m.clearedFields[schedule.FieldCreatedBy] + return ok +} + +// ResetCreatedBy resets all changes to the "created_by" field. +func (m *ScheduleMutation) ResetCreatedBy() { + m.created_by = nil + delete(m.clearedFields, schedule.FieldCreatedBy) +} + +// SetCreated sets the "created" field. +func (m *ScheduleMutation) SetCreated(t time.Time) { + m.created = &t +} + +// Created returns the value of the "created" field in the mutation. +func (m *ScheduleMutation) Created() (r time.Time, exists bool) { + v := m.created + if v == nil { + return + } + return *v, true +} + +// OldCreated returns the old "created" field's value of the Schedule entity. +// If the Schedule object wasn't provided to the builder, the object is fetched from the database. +// An error is returned if the mutation operation is not UpdateOne, or the database query fails. +func (m *ScheduleMutation) OldCreated(ctx context.Context) (v time.Time, err error) { + if !m.op.Is(OpUpdateOne) { + return v, errors.New("OldCreated is only allowed on UpdateOne operations") + } + if m.id == nil || m.oldValue == nil { + return v, errors.New("OldCreated requires an ID field in the mutation") + } + oldValue, err := m.oldValue(ctx) + if err != nil { + return v, fmt.Errorf("querying old value for OldCreated: %w", err) + } + return oldValue.Created, nil +} + +// ResetCreated resets all changes to the "created" field. +func (m *ScheduleMutation) ResetCreated() { + m.created = nil +} + +// SetUpdated sets the "updated" field. +func (m *ScheduleMutation) SetUpdated(t time.Time) { + m.updated = &t +} + +// Updated returns the value of the "updated" field in the mutation. +func (m *ScheduleMutation) Updated() (r time.Time, exists bool) { + v := m.updated + if v == nil { + return + } + return *v, true +} + +// OldUpdated returns the old "updated" field's value of the Schedule entity. +// If the Schedule object wasn't provided to the builder, the object is fetched from the database. +// An error is returned if the mutation operation is not UpdateOne, or the database query fails. +func (m *ScheduleMutation) OldUpdated(ctx context.Context) (v time.Time, err error) { + if !m.op.Is(OpUpdateOne) { + return v, errors.New("OldUpdated is only allowed on UpdateOne operations") + } + if m.id == nil || m.oldValue == nil { + return v, errors.New("OldUpdated requires an ID field in the mutation") + } + oldValue, err := m.oldValue(ctx) + if err != nil { + return v, fmt.Errorf("querying old value for OldUpdated: %w", err) + } + return oldValue.Updated, nil +} + +// ResetUpdated resets all changes to the "updated" field. +func (m *ScheduleMutation) ResetUpdated() { + m.updated = nil +} + +// Where appends a list predicates to the ScheduleMutation builder. +func (m *ScheduleMutation) Where(ps ...predicate.Schedule) { + m.predicates = append(m.predicates, ps...) +} + +// WhereP appends storage-level predicates to the ScheduleMutation builder. Using this method, +// users can use type-assertion to append predicates that do not depend on any generated package. +func (m *ScheduleMutation) WhereP(ps ...func(*sql.Selector)) { + p := make([]predicate.Schedule, len(ps)) + for i := range ps { + p[i] = ps[i] + } + m.Where(p...) +} + +// Op returns the operation name. +func (m *ScheduleMutation) Op() Op { + return m.op +} + +// SetOp allows setting the mutation operation. +func (m *ScheduleMutation) SetOp(op Op) { + m.op = op +} + +// Type returns the node type of this mutation (Schedule). +func (m *ScheduleMutation) Type() string { + return m.typ +} + +// Fields returns all fields that were changed during this mutation. Note that in +// order to get all numeric fields that were incremented/decremented, call +// AddedFields(). +func (m *ScheduleMutation) Fields() []string { + fields := make([]string, 0, 15) + if m.project_id != nil { + fields = append(fields, schedule.FieldProjectID) + } + if m.name != nil { + fields = append(fields, schedule.FieldName) + } + if m.cron_expr != nil { + fields = append(fields, schedule.FieldCronExpr) + } + if m.event_type != nil { + fields = append(fields, schedule.FieldEventType) + } + if m.payload != nil { + fields = append(fields, schedule.FieldPayload) + } + if m.status != nil { + fields = append(fields, schedule.FieldStatus) + } + if m.next_run_at != nil { + fields = append(fields, schedule.FieldNextRunAt) + } + if m.last_run_at != nil { + fields = append(fields, schedule.FieldLastRunAt) + } + if m.last_run_status != nil { + fields = append(fields, schedule.FieldLastRunStatus) + } + if m.last_run_error != nil { + fields = append(fields, schedule.FieldLastRunError) + } + if m.run_count != nil { + fields = append(fields, schedule.FieldRunCount) + } + if m.error_count != nil { + fields = append(fields, schedule.FieldErrorCount) + } + if m.created_by != nil { + fields = append(fields, schedule.FieldCreatedBy) + } + if m.created != nil { + fields = append(fields, schedule.FieldCreated) + } + if m.updated != nil { + fields = append(fields, schedule.FieldUpdated) + } + return fields +} + +// Field returns the value of a field with the given name. The second boolean +// return value indicates that this field was not set, or was not defined in the +// schema. +func (m *ScheduleMutation) Field(name string) (ent.Value, bool) { + switch name { + case schedule.FieldProjectID: + return m.ProjectID() + case schedule.FieldName: + return m.Name() + case schedule.FieldCronExpr: + return m.CronExpr() + case schedule.FieldEventType: + return m.EventType() + case schedule.FieldPayload: + return m.Payload() + case schedule.FieldStatus: + return m.Status() + case schedule.FieldNextRunAt: + return m.NextRunAt() + case schedule.FieldLastRunAt: + return m.LastRunAt() + case schedule.FieldLastRunStatus: + return m.LastRunStatus() + case schedule.FieldLastRunError: + return m.LastRunError() + case schedule.FieldRunCount: + return m.RunCount() + case schedule.FieldErrorCount: + return m.ErrorCount() + case schedule.FieldCreatedBy: + return m.CreatedBy() + case schedule.FieldCreated: + return m.Created() + case schedule.FieldUpdated: + return m.Updated() + } + return nil, false +} + +// OldField returns the old value of the field from the database. An error is +// returned if the mutation operation is not UpdateOne, or the query to the +// database failed. +func (m *ScheduleMutation) OldField(ctx context.Context, name string) (ent.Value, error) { + switch name { + case schedule.FieldProjectID: + return m.OldProjectID(ctx) + case schedule.FieldName: + return m.OldName(ctx) + case schedule.FieldCronExpr: + return m.OldCronExpr(ctx) + case schedule.FieldEventType: + return m.OldEventType(ctx) + case schedule.FieldPayload: + return m.OldPayload(ctx) + case schedule.FieldStatus: + return m.OldStatus(ctx) + case schedule.FieldNextRunAt: + return m.OldNextRunAt(ctx) + case schedule.FieldLastRunAt: + return m.OldLastRunAt(ctx) + case schedule.FieldLastRunStatus: + return m.OldLastRunStatus(ctx) + case schedule.FieldLastRunError: + return m.OldLastRunError(ctx) + case schedule.FieldRunCount: + return m.OldRunCount(ctx) + case schedule.FieldErrorCount: + return m.OldErrorCount(ctx) + case schedule.FieldCreatedBy: + return m.OldCreatedBy(ctx) + case schedule.FieldCreated: + return m.OldCreated(ctx) + case schedule.FieldUpdated: + return m.OldUpdated(ctx) + } + return nil, fmt.Errorf("unknown Schedule field %s", name) +} + +// SetField sets the value of a field with the given name. It returns an error if +// the field is not defined in the schema, or if the type mismatched the field +// type. +func (m *ScheduleMutation) SetField(name string, value ent.Value) error { + switch name { + case schedule.FieldProjectID: + v, ok := value.(uuid.UUID) + if !ok { + return fmt.Errorf("unexpected type %T for field %s", value, name) + } + m.SetProjectID(v) + return nil + case schedule.FieldName: + v, ok := value.(string) + if !ok { + return fmt.Errorf("unexpected type %T for field %s", value, name) + } + m.SetName(v) + return nil + case schedule.FieldCronExpr: + v, ok := value.(string) + if !ok { + return fmt.Errorf("unexpected type %T for field %s", value, name) + } + m.SetCronExpr(v) + return nil + case schedule.FieldEventType: + v, ok := value.(string) + if !ok { + return fmt.Errorf("unexpected type %T for field %s", value, name) + } + m.SetEventType(v) + return nil + case schedule.FieldPayload: + v, ok := value.(string) + if !ok { + return fmt.Errorf("unexpected type %T for field %s", value, name) + } + m.SetPayload(v) + return nil + case schedule.FieldStatus: + v, ok := value.(string) + if !ok { + return fmt.Errorf("unexpected type %T for field %s", value, name) + } + m.SetStatus(v) + return nil + case schedule.FieldNextRunAt: + v, ok := value.(time.Time) + if !ok { + return fmt.Errorf("unexpected type %T for field %s", value, name) + } + m.SetNextRunAt(v) + return nil + case schedule.FieldLastRunAt: + v, ok := value.(time.Time) + if !ok { + return fmt.Errorf("unexpected type %T for field %s", value, name) + } + m.SetLastRunAt(v) + return nil + case schedule.FieldLastRunStatus: + v, ok := value.(string) + if !ok { + return fmt.Errorf("unexpected type %T for field %s", value, name) + } + m.SetLastRunStatus(v) + return nil + case schedule.FieldLastRunError: + v, ok := value.(string) + if !ok { + return fmt.Errorf("unexpected type %T for field %s", value, name) + } + m.SetLastRunError(v) + return nil + case schedule.FieldRunCount: + v, ok := value.(int) + if !ok { + return fmt.Errorf("unexpected type %T for field %s", value, name) + } + m.SetRunCount(v) + return nil + case schedule.FieldErrorCount: + v, ok := value.(int) + if !ok { + return fmt.Errorf("unexpected type %T for field %s", value, name) + } + m.SetErrorCount(v) + return nil + case schedule.FieldCreatedBy: + v, ok := value.(string) + if !ok { + return fmt.Errorf("unexpected type %T for field %s", value, name) + } + m.SetCreatedBy(v) + return nil + case schedule.FieldCreated: + v, ok := value.(time.Time) + if !ok { + return fmt.Errorf("unexpected type %T for field %s", value, name) + } + m.SetCreated(v) + return nil + case schedule.FieldUpdated: + v, ok := value.(time.Time) + if !ok { + return fmt.Errorf("unexpected type %T for field %s", value, name) + } + m.SetUpdated(v) + return nil + } + return fmt.Errorf("unknown Schedule field %s", name) +} + +// AddedFields returns all numeric fields that were incremented/decremented during +// this mutation. +func (m *ScheduleMutation) AddedFields() []string { + var fields []string + if m.addrun_count != nil { + fields = append(fields, schedule.FieldRunCount) + } + if m.adderror_count != nil { + fields = append(fields, schedule.FieldErrorCount) + } + return fields +} + +// AddedField returns the numeric value that was incremented/decremented on a field +// with the given name. The second boolean return value indicates that this field +// was not set, or was not defined in the schema. +func (m *ScheduleMutation) AddedField(name string) (ent.Value, bool) { + switch name { + case schedule.FieldRunCount: + return m.AddedRunCount() + case schedule.FieldErrorCount: + return m.AddedErrorCount() + } + return nil, false +} + +// AddField adds the value to the field with the given name. It returns an error if +// the field is not defined in the schema, or if the type mismatched the field +// type. +func (m *ScheduleMutation) AddField(name string, value ent.Value) error { + switch name { + case schedule.FieldRunCount: + v, ok := value.(int) + if !ok { + return fmt.Errorf("unexpected type %T for field %s", value, name) + } + m.AddRunCount(v) + return nil + case schedule.FieldErrorCount: + v, ok := value.(int) + if !ok { + return fmt.Errorf("unexpected type %T for field %s", value, name) + } + m.AddErrorCount(v) + return nil + } + return fmt.Errorf("unknown Schedule numeric field %s", name) +} + +// ClearedFields returns all nullable fields that were cleared during this +// mutation. +func (m *ScheduleMutation) ClearedFields() []string { + var fields []string + if m.FieldCleared(schedule.FieldNextRunAt) { + fields = append(fields, schedule.FieldNextRunAt) + } + if m.FieldCleared(schedule.FieldLastRunAt) { + fields = append(fields, schedule.FieldLastRunAt) + } + if m.FieldCleared(schedule.FieldLastRunStatus) { + fields = append(fields, schedule.FieldLastRunStatus) + } + if m.FieldCleared(schedule.FieldLastRunError) { + fields = append(fields, schedule.FieldLastRunError) + } + if m.FieldCleared(schedule.FieldCreatedBy) { + fields = append(fields, schedule.FieldCreatedBy) + } + return fields +} + +// FieldCleared returns a boolean indicating if a field with the given name was +// cleared in this mutation. +func (m *ScheduleMutation) FieldCleared(name string) bool { + _, ok := m.clearedFields[name] + return ok +} + +// ClearField clears the value of the field with the given name. It returns an +// error if the field is not defined in the schema. +func (m *ScheduleMutation) ClearField(name string) error { + switch name { + case schedule.FieldNextRunAt: + m.ClearNextRunAt() + return nil + case schedule.FieldLastRunAt: + m.ClearLastRunAt() + return nil + case schedule.FieldLastRunStatus: + m.ClearLastRunStatus() + return nil + case schedule.FieldLastRunError: + m.ClearLastRunError() + return nil + case schedule.FieldCreatedBy: + m.ClearCreatedBy() + return nil + } + return fmt.Errorf("unknown Schedule nullable field %s", name) +} + +// ResetField resets all changes in the mutation for the field with the given name. +// It returns an error if the field is not defined in the schema. +func (m *ScheduleMutation) ResetField(name string) error { + switch name { + case schedule.FieldProjectID: + m.ResetProjectID() + return nil + case schedule.FieldName: + m.ResetName() + return nil + case schedule.FieldCronExpr: + m.ResetCronExpr() + return nil + case schedule.FieldEventType: + m.ResetEventType() + return nil + case schedule.FieldPayload: + m.ResetPayload() + return nil + case schedule.FieldStatus: + m.ResetStatus() + return nil + case schedule.FieldNextRunAt: + m.ResetNextRunAt() + return nil + case schedule.FieldLastRunAt: + m.ResetLastRunAt() + return nil + case schedule.FieldLastRunStatus: + m.ResetLastRunStatus() + return nil + case schedule.FieldLastRunError: + m.ResetLastRunError() + return nil + case schedule.FieldRunCount: + m.ResetRunCount() + return nil + case schedule.FieldErrorCount: + m.ResetErrorCount() + return nil + case schedule.FieldCreatedBy: + m.ResetCreatedBy() + return nil + case schedule.FieldCreated: + m.ResetCreated() + return nil + case schedule.FieldUpdated: + m.ResetUpdated() + return nil + } + return fmt.Errorf("unknown Schedule field %s", name) +} + +// AddedEdges returns all edge names that were set/added in this mutation. +func (m *ScheduleMutation) AddedEdges() []string { + edges := make([]string, 0, 0) + return edges +} + +// AddedIDs returns all IDs (to other nodes) that were added for the given edge +// name in this mutation. +func (m *ScheduleMutation) AddedIDs(name string) []ent.Value { + return nil +} + +// RemovedEdges returns all edge names that were removed in this mutation. +func (m *ScheduleMutation) RemovedEdges() []string { + edges := make([]string, 0, 0) + return edges +} + +// RemovedIDs returns all IDs (to other nodes) that were removed for the edge with +// the given name in this mutation. +func (m *ScheduleMutation) RemovedIDs(name string) []ent.Value { + return nil +} + +// ClearedEdges returns all edge names that were cleared in this mutation. +func (m *ScheduleMutation) ClearedEdges() []string { + edges := make([]string, 0, 0) + return edges +} + +// EdgeCleared returns a boolean which indicates if the edge with the given name +// was cleared in this mutation. +func (m *ScheduleMutation) EdgeCleared(name string) bool { + return false +} + +// ClearEdge clears the value of the edge with the given name. It returns an error +// if that edge is not defined in the schema. +func (m *ScheduleMutation) ClearEdge(name string) error { + return fmt.Errorf("unknown Schedule unique edge %s", name) +} + +// ResetEdge resets all changes to the edge with the given name in this mutation. +// It returns an error if the edge is not defined in the schema. +func (m *ScheduleMutation) ResetEdge(name string) error { + return fmt.Errorf("unknown Schedule edge %s", name) +} + +// ScheduledEventMutation represents an operation that mutates the ScheduledEvent nodes in the graph. +type ScheduledEventMutation struct { + config + op Op + typ string + id *uuid.UUID + project_id *uuid.UUID + event_type *string + fire_at *time.Time + payload *string + status *string + created_by *string + fired_at *time.Time + error *string + schedule_id *string + created *time.Time + clearedFields map[string]struct{} + done bool + oldValue func(context.Context) (*ScheduledEvent, error) + predicates []predicate.ScheduledEvent +} + +var _ ent.Mutation = (*ScheduledEventMutation)(nil) + +// scheduledeventOption allows management of the mutation configuration using functional options. +type scheduledeventOption func(*ScheduledEventMutation) + +// newScheduledEventMutation creates new mutation for the ScheduledEvent entity. +func newScheduledEventMutation(c config, op Op, opts ...scheduledeventOption) *ScheduledEventMutation { + m := &ScheduledEventMutation{ + config: c, + op: op, + typ: TypeScheduledEvent, + clearedFields: make(map[string]struct{}), + } + for _, opt := range opts { + opt(m) + } + return m +} + +// withScheduledEventID sets the ID field of the mutation. +func withScheduledEventID(id uuid.UUID) scheduledeventOption { + return func(m *ScheduledEventMutation) { + var ( + err error + once sync.Once + value *ScheduledEvent + ) + m.oldValue = func(ctx context.Context) (*ScheduledEvent, error) { + once.Do(func() { + if m.done { + err = errors.New("querying old values post mutation is not allowed") + } else { + value, err = m.Client().ScheduledEvent.Get(ctx, id) + } + }) + return value, err + } + m.id = &id + } +} + +// withScheduledEvent sets the old ScheduledEvent of the mutation. +func withScheduledEvent(node *ScheduledEvent) scheduledeventOption { + return func(m *ScheduledEventMutation) { + m.oldValue = func(context.Context) (*ScheduledEvent, error) { + return node, nil + } + m.id = &node.ID + } +} + +// Client returns a new `ent.Client` from the mutation. If the mutation was +// executed in a transaction (ent.Tx), a transactional client is returned. +func (m ScheduledEventMutation) Client() *Client { + client := &Client{config: m.config} + client.init() + return client +} + +// Tx returns an `ent.Tx` for mutations that were executed in transactions; +// it returns an error otherwise. +func (m ScheduledEventMutation) Tx() (*Tx, error) { + if _, ok := m.driver.(*txDriver); !ok { + return nil, errors.New("ent: mutation is not running in a transaction") + } + tx := &Tx{config: m.config} + tx.init() + return tx, nil +} + +// SetID sets the value of the id field. Note that this +// operation is only accepted on creation of ScheduledEvent entities. +func (m *ScheduledEventMutation) SetID(id uuid.UUID) { + m.id = &id +} + +// ID returns the ID value in the mutation. Note that the ID is only available +// if it was provided to the builder or after it was returned from the database. +func (m *ScheduledEventMutation) ID() (id uuid.UUID, exists bool) { + if m.id == nil { + return + } + return *m.id, true +} + +// IDs queries the database and returns the entity ids that match the mutation's predicate. +// That means, if the mutation is applied within a transaction with an isolation level such +// as sql.LevelSerializable, the returned ids match the ids of the rows that will be updated +// or updated by the mutation. +func (m *ScheduledEventMutation) IDs(ctx context.Context) ([]uuid.UUID, error) { + switch { + case m.op.Is(OpUpdateOne | OpDeleteOne): + id, exists := m.ID() + if exists { + return []uuid.UUID{id}, nil + } + fallthrough + case m.op.Is(OpUpdate | OpDelete): + return m.Client().ScheduledEvent.Query().Where(m.predicates...).IDs(ctx) + default: + return nil, fmt.Errorf("IDs is not allowed on %s operations", m.op) + } +} + +// SetProjectID sets the "project_id" field. +func (m *ScheduledEventMutation) SetProjectID(u uuid.UUID) { + m.project_id = &u +} + +// ProjectID returns the value of the "project_id" field in the mutation. +func (m *ScheduledEventMutation) ProjectID() (r uuid.UUID, exists bool) { + v := m.project_id + if v == nil { + return + } + return *v, true +} + +// OldProjectID returns the old "project_id" field's value of the ScheduledEvent entity. +// If the ScheduledEvent object wasn't provided to the builder, the object is fetched from the database. +// An error is returned if the mutation operation is not UpdateOne, or the database query fails. +func (m *ScheduledEventMutation) OldProjectID(ctx context.Context) (v uuid.UUID, err error) { + if !m.op.Is(OpUpdateOne) { + return v, errors.New("OldProjectID is only allowed on UpdateOne operations") + } + if m.id == nil || m.oldValue == nil { + return v, errors.New("OldProjectID requires an ID field in the mutation") + } + oldValue, err := m.oldValue(ctx) + if err != nil { + return v, fmt.Errorf("querying old value for OldProjectID: %w", err) + } + return oldValue.ProjectID, nil +} + +// ResetProjectID resets all changes to the "project_id" field. +func (m *ScheduledEventMutation) ResetProjectID() { + m.project_id = nil +} + +// SetEventType sets the "event_type" field. +func (m *ScheduledEventMutation) SetEventType(s string) { + m.event_type = &s +} + +// EventType returns the value of the "event_type" field in the mutation. +func (m *ScheduledEventMutation) EventType() (r string, exists bool) { + v := m.event_type + if v == nil { + return + } + return *v, true +} + +// OldEventType returns the old "event_type" field's value of the ScheduledEvent entity. +// If the ScheduledEvent object wasn't provided to the builder, the object is fetched from the database. +// An error is returned if the mutation operation is not UpdateOne, or the database query fails. +func (m *ScheduledEventMutation) OldEventType(ctx context.Context) (v string, err error) { + if !m.op.Is(OpUpdateOne) { + return v, errors.New("OldEventType is only allowed on UpdateOne operations") + } + if m.id == nil || m.oldValue == nil { + return v, errors.New("OldEventType requires an ID field in the mutation") + } + oldValue, err := m.oldValue(ctx) + if err != nil { + return v, fmt.Errorf("querying old value for OldEventType: %w", err) + } + return oldValue.EventType, nil +} + +// ResetEventType resets all changes to the "event_type" field. +func (m *ScheduledEventMutation) ResetEventType() { + m.event_type = nil +} + +// SetFireAt sets the "fire_at" field. +func (m *ScheduledEventMutation) SetFireAt(t time.Time) { + m.fire_at = &t +} + +// FireAt returns the value of the "fire_at" field in the mutation. +func (m *ScheduledEventMutation) FireAt() (r time.Time, exists bool) { + v := m.fire_at + if v == nil { + return + } + return *v, true +} + +// OldFireAt returns the old "fire_at" field's value of the ScheduledEvent entity. +// If the ScheduledEvent object wasn't provided to the builder, the object is fetched from the database. +// An error is returned if the mutation operation is not UpdateOne, or the database query fails. +func (m *ScheduledEventMutation) OldFireAt(ctx context.Context) (v time.Time, err error) { + if !m.op.Is(OpUpdateOne) { + return v, errors.New("OldFireAt is only allowed on UpdateOne operations") + } + if m.id == nil || m.oldValue == nil { + return v, errors.New("OldFireAt requires an ID field in the mutation") + } + oldValue, err := m.oldValue(ctx) + if err != nil { + return v, fmt.Errorf("querying old value for OldFireAt: %w", err) + } + return oldValue.FireAt, nil +} + +// ResetFireAt resets all changes to the "fire_at" field. +func (m *ScheduledEventMutation) ResetFireAt() { + m.fire_at = nil +} + +// SetPayload sets the "payload" field. +func (m *ScheduledEventMutation) SetPayload(s string) { + m.payload = &s +} + +// Payload returns the value of the "payload" field in the mutation. +func (m *ScheduledEventMutation) Payload() (r string, exists bool) { + v := m.payload + if v == nil { + return + } + return *v, true +} + +// OldPayload returns the old "payload" field's value of the ScheduledEvent entity. +// If the ScheduledEvent object wasn't provided to the builder, the object is fetched from the database. +// An error is returned if the mutation operation is not UpdateOne, or the database query fails. +func (m *ScheduledEventMutation) OldPayload(ctx context.Context) (v string, err error) { + if !m.op.Is(OpUpdateOne) { + return v, errors.New("OldPayload is only allowed on UpdateOne operations") + } + if m.id == nil || m.oldValue == nil { + return v, errors.New("OldPayload requires an ID field in the mutation") + } + oldValue, err := m.oldValue(ctx) + if err != nil { + return v, fmt.Errorf("querying old value for OldPayload: %w", err) + } + return oldValue.Payload, nil +} + +// ResetPayload resets all changes to the "payload" field. +func (m *ScheduledEventMutation) ResetPayload() { + m.payload = nil +} + +// SetStatus sets the "status" field. +func (m *ScheduledEventMutation) SetStatus(s string) { + m.status = &s +} + +// Status returns the value of the "status" field in the mutation. +func (m *ScheduledEventMutation) Status() (r string, exists bool) { + v := m.status + if v == nil { + return + } + return *v, true +} + +// OldStatus returns the old "status" field's value of the ScheduledEvent entity. +// If the ScheduledEvent object wasn't provided to the builder, the object is fetched from the database. +// An error is returned if the mutation operation is not UpdateOne, or the database query fails. +func (m *ScheduledEventMutation) OldStatus(ctx context.Context) (v string, err error) { + if !m.op.Is(OpUpdateOne) { + return v, errors.New("OldStatus is only allowed on UpdateOne operations") + } + if m.id == nil || m.oldValue == nil { + return v, errors.New("OldStatus requires an ID field in the mutation") + } + oldValue, err := m.oldValue(ctx) + if err != nil { + return v, fmt.Errorf("querying old value for OldStatus: %w", err) + } + return oldValue.Status, nil +} + +// ResetStatus resets all changes to the "status" field. +func (m *ScheduledEventMutation) ResetStatus() { + m.status = nil +} + +// SetCreatedBy sets the "created_by" field. +func (m *ScheduledEventMutation) SetCreatedBy(s string) { + m.created_by = &s +} + +// CreatedBy returns the value of the "created_by" field in the mutation. +func (m *ScheduledEventMutation) CreatedBy() (r string, exists bool) { + v := m.created_by + if v == nil { + return + } + return *v, true +} + +// OldCreatedBy returns the old "created_by" field's value of the ScheduledEvent entity. +// If the ScheduledEvent object wasn't provided to the builder, the object is fetched from the database. +// An error is returned if the mutation operation is not UpdateOne, or the database query fails. +func (m *ScheduledEventMutation) OldCreatedBy(ctx context.Context) (v string, err error) { + if !m.op.Is(OpUpdateOne) { + return v, errors.New("OldCreatedBy is only allowed on UpdateOne operations") + } + if m.id == nil || m.oldValue == nil { + return v, errors.New("OldCreatedBy requires an ID field in the mutation") + } + oldValue, err := m.oldValue(ctx) + if err != nil { + return v, fmt.Errorf("querying old value for OldCreatedBy: %w", err) + } + return oldValue.CreatedBy, nil +} + +// ClearCreatedBy clears the value of the "created_by" field. +func (m *ScheduledEventMutation) ClearCreatedBy() { + m.created_by = nil + m.clearedFields[scheduledevent.FieldCreatedBy] = struct{}{} +} + +// CreatedByCleared returns if the "created_by" field was cleared in this mutation. +func (m *ScheduledEventMutation) CreatedByCleared() bool { + _, ok := m.clearedFields[scheduledevent.FieldCreatedBy] + return ok +} + +// ResetCreatedBy resets all changes to the "created_by" field. +func (m *ScheduledEventMutation) ResetCreatedBy() { + m.created_by = nil + delete(m.clearedFields, scheduledevent.FieldCreatedBy) +} + +// SetFiredAt sets the "fired_at" field. +func (m *ScheduledEventMutation) SetFiredAt(t time.Time) { + m.fired_at = &t +} + +// FiredAt returns the value of the "fired_at" field in the mutation. +func (m *ScheduledEventMutation) FiredAt() (r time.Time, exists bool) { + v := m.fired_at + if v == nil { + return + } + return *v, true +} + +// OldFiredAt returns the old "fired_at" field's value of the ScheduledEvent entity. +// If the ScheduledEvent object wasn't provided to the builder, the object is fetched from the database. +// An error is returned if the mutation operation is not UpdateOne, or the database query fails. +func (m *ScheduledEventMutation) OldFiredAt(ctx context.Context) (v *time.Time, err error) { + if !m.op.Is(OpUpdateOne) { + return v, errors.New("OldFiredAt is only allowed on UpdateOne operations") + } + if m.id == nil || m.oldValue == nil { + return v, errors.New("OldFiredAt requires an ID field in the mutation") + } + oldValue, err := m.oldValue(ctx) + if err != nil { + return v, fmt.Errorf("querying old value for OldFiredAt: %w", err) + } + return oldValue.FiredAt, nil +} + +// ClearFiredAt clears the value of the "fired_at" field. +func (m *ScheduledEventMutation) ClearFiredAt() { + m.fired_at = nil + m.clearedFields[scheduledevent.FieldFiredAt] = struct{}{} +} + +// FiredAtCleared returns if the "fired_at" field was cleared in this mutation. +func (m *ScheduledEventMutation) FiredAtCleared() bool { + _, ok := m.clearedFields[scheduledevent.FieldFiredAt] + return ok +} + +// ResetFiredAt resets all changes to the "fired_at" field. +func (m *ScheduledEventMutation) ResetFiredAt() { + m.fired_at = nil + delete(m.clearedFields, scheduledevent.FieldFiredAt) +} + +// SetError sets the "error" field. +func (m *ScheduledEventMutation) SetError(s string) { + m.error = &s +} + +// Error returns the value of the "error" field in the mutation. +func (m *ScheduledEventMutation) Error() (r string, exists bool) { + v := m.error + if v == nil { + return + } + return *v, true +} + +// OldError returns the old "error" field's value of the ScheduledEvent entity. +// If the ScheduledEvent object wasn't provided to the builder, the object is fetched from the database. +// An error is returned if the mutation operation is not UpdateOne, or the database query fails. +func (m *ScheduledEventMutation) OldError(ctx context.Context) (v string, err error) { + if !m.op.Is(OpUpdateOne) { + return v, errors.New("OldError is only allowed on UpdateOne operations") + } + if m.id == nil || m.oldValue == nil { + return v, errors.New("OldError requires an ID field in the mutation") + } + oldValue, err := m.oldValue(ctx) + if err != nil { + return v, fmt.Errorf("querying old value for OldError: %w", err) + } + return oldValue.Error, nil +} + +// ClearError clears the value of the "error" field. +func (m *ScheduledEventMutation) ClearError() { + m.error = nil + m.clearedFields[scheduledevent.FieldError] = struct{}{} +} + +// ErrorCleared returns if the "error" field was cleared in this mutation. +func (m *ScheduledEventMutation) ErrorCleared() bool { + _, ok := m.clearedFields[scheduledevent.FieldError] + return ok +} + +// ResetError resets all changes to the "error" field. +func (m *ScheduledEventMutation) ResetError() { + m.error = nil + delete(m.clearedFields, scheduledevent.FieldError) +} + +// SetScheduleID sets the "schedule_id" field. +func (m *ScheduledEventMutation) SetScheduleID(s string) { + m.schedule_id = &s +} + +// ScheduleID returns the value of the "schedule_id" field in the mutation. +func (m *ScheduledEventMutation) ScheduleID() (r string, exists bool) { + v := m.schedule_id + if v == nil { + return + } + return *v, true +} + +// OldScheduleID returns the old "schedule_id" field's value of the ScheduledEvent entity. +// If the ScheduledEvent object wasn't provided to the builder, the object is fetched from the database. +// An error is returned if the mutation operation is not UpdateOne, or the database query fails. +func (m *ScheduledEventMutation) OldScheduleID(ctx context.Context) (v string, err error) { + if !m.op.Is(OpUpdateOne) { + return v, errors.New("OldScheduleID is only allowed on UpdateOne operations") + } + if m.id == nil || m.oldValue == nil { + return v, errors.New("OldScheduleID requires an ID field in the mutation") + } + oldValue, err := m.oldValue(ctx) + if err != nil { + return v, fmt.Errorf("querying old value for OldScheduleID: %w", err) + } + return oldValue.ScheduleID, nil +} + +// ClearScheduleID clears the value of the "schedule_id" field. +func (m *ScheduledEventMutation) ClearScheduleID() { + m.schedule_id = nil + m.clearedFields[scheduledevent.FieldScheduleID] = struct{}{} +} + +// ScheduleIDCleared returns if the "schedule_id" field was cleared in this mutation. +func (m *ScheduledEventMutation) ScheduleIDCleared() bool { + _, ok := m.clearedFields[scheduledevent.FieldScheduleID] + return ok +} + +// ResetScheduleID resets all changes to the "schedule_id" field. +func (m *ScheduledEventMutation) ResetScheduleID() { + m.schedule_id = nil + delete(m.clearedFields, scheduledevent.FieldScheduleID) +} + +// SetCreated sets the "created" field. +func (m *ScheduledEventMutation) SetCreated(t time.Time) { + m.created = &t +} + +// Created returns the value of the "created" field in the mutation. +func (m *ScheduledEventMutation) Created() (r time.Time, exists bool) { + v := m.created + if v == nil { + return + } + return *v, true +} + +// OldCreated returns the old "created" field's value of the ScheduledEvent entity. +// If the ScheduledEvent object wasn't provided to the builder, the object is fetched from the database. +// An error is returned if the mutation operation is not UpdateOne, or the database query fails. +func (m *ScheduledEventMutation) OldCreated(ctx context.Context) (v time.Time, err error) { + if !m.op.Is(OpUpdateOne) { + return v, errors.New("OldCreated is only allowed on UpdateOne operations") + } + if m.id == nil || m.oldValue == nil { + return v, errors.New("OldCreated requires an ID field in the mutation") + } + oldValue, err := m.oldValue(ctx) + if err != nil { + return v, fmt.Errorf("querying old value for OldCreated: %w", err) + } + return oldValue.Created, nil +} + +// ResetCreated resets all changes to the "created" field. +func (m *ScheduledEventMutation) ResetCreated() { + m.created = nil +} + +// Where appends a list predicates to the ScheduledEventMutation builder. +func (m *ScheduledEventMutation) Where(ps ...predicate.ScheduledEvent) { + m.predicates = append(m.predicates, ps...) +} + +// WhereP appends storage-level predicates to the ScheduledEventMutation builder. Using this method, // users can use type-assertion to append predicates that do not depend on any generated package. -func (m *AgentMutation) WhereP(ps ...func(*sql.Selector)) { - p := make([]predicate.Agent, len(ps)) +func (m *ScheduledEventMutation) WhereP(ps ...func(*sql.Selector)) { + p := make([]predicate.ScheduledEvent, len(ps)) for i := range ps { p[i] = ps[i] } @@ -2219,57 +27530,54 @@ func (m *AgentMutation) WhereP(ps ...func(*sql.Selector)) { } // Op returns the operation name. -func (m *AgentMutation) Op() Op { +func (m *ScheduledEventMutation) Op() Op { return m.op } // SetOp allows setting the mutation operation. -func (m *AgentMutation) SetOp(op Op) { +func (m *ScheduledEventMutation) SetOp(op Op) { m.op = op } -// Type returns the node type of this mutation (Agent). -func (m *AgentMutation) Type() string { +// Type returns the node type of this mutation (ScheduledEvent). +func (m *ScheduledEventMutation) Type() string { return m.typ } // Fields returns all fields that were changed during this mutation. Note that in // order to get all numeric fields that were incremented/decremented, call // AddedFields(). -func (m *AgentMutation) Fields() []string { - fields := make([]string, 0, 11) - if m.slug != nil { - fields = append(fields, agent.FieldSlug) +func (m *ScheduledEventMutation) Fields() []string { + fields := make([]string, 0, 10) + if m.project_id != nil { + fields = append(fields, scheduledevent.FieldProjectID) } - if m.name != nil { - fields = append(fields, agent.FieldName) + if m.event_type != nil { + fields = append(fields, scheduledevent.FieldEventType) } - if m.template != nil { - fields = append(fields, agent.FieldTemplate) + if m.fire_at != nil { + fields = append(fields, scheduledevent.FieldFireAt) } - if m.project != nil { - fields = append(fields, agent.FieldProjectID) + if m.payload != nil { + fields = append(fields, scheduledevent.FieldPayload) } if m.status != nil { - fields = append(fields, agent.FieldStatus) + fields = append(fields, scheduledevent.FieldStatus) } - if m.creator != nil { - fields = append(fields, agent.FieldCreatedBy) + if m.created_by != nil { + fields = append(fields, scheduledevent.FieldCreatedBy) } - if m.owner != nil { - fields = append(fields, agent.FieldOwnerID) + if m.fired_at != nil { + fields = append(fields, scheduledevent.FieldFiredAt) } - if m.delegation_enabled != nil { - fields = append(fields, agent.FieldDelegationEnabled) + if m.error != nil { + fields = append(fields, scheduledevent.FieldError) } - if m.visibility != nil { - fields = append(fields, agent.FieldVisibility) + if m.schedule_id != nil { + fields = append(fields, scheduledevent.FieldScheduleID) } if m.created != nil { - fields = append(fields, agent.FieldCreated) - } - if m.updated != nil { - fields = append(fields, agent.FieldUpdated) + fields = append(fields, scheduledevent.FieldCreated) } return fields } @@ -2277,30 +27585,28 @@ func (m *AgentMutation) Fields() []string { // Field returns the value of a field with the given name. The second boolean // return value indicates that this field was not set, or was not defined in the // schema. -func (m *AgentMutation) Field(name string) (ent.Value, bool) { +func (m *ScheduledEventMutation) Field(name string) (ent.Value, bool) { switch name { - case agent.FieldSlug: - return m.Slug() - case agent.FieldName: - return m.Name() - case agent.FieldTemplate: - return m.Template() - case agent.FieldProjectID: + case scheduledevent.FieldProjectID: return m.ProjectID() - case agent.FieldStatus: + case scheduledevent.FieldEventType: + return m.EventType() + case scheduledevent.FieldFireAt: + return m.FireAt() + case scheduledevent.FieldPayload: + return m.Payload() + case scheduledevent.FieldStatus: return m.Status() - case agent.FieldCreatedBy: + case scheduledevent.FieldCreatedBy: return m.CreatedBy() - case agent.FieldOwnerID: - return m.OwnerID() - case agent.FieldDelegationEnabled: - return m.DelegationEnabled() - case agent.FieldVisibility: - return m.Visibility() - case agent.FieldCreated: + case scheduledevent.FieldFiredAt: + return m.FiredAt() + case scheduledevent.FieldError: + return m.Error() + case scheduledevent.FieldScheduleID: + return m.ScheduleID() + case scheduledevent.FieldCreated: return m.Created() - case agent.FieldUpdated: - return m.Updated() } return nil, false } @@ -2308,606 +27614,682 @@ func (m *AgentMutation) Field(name string) (ent.Value, bool) { // OldField returns the old value of the field from the database. An error is // returned if the mutation operation is not UpdateOne, or the query to the // database failed. -func (m *AgentMutation) OldField(ctx context.Context, name string) (ent.Value, error) { +func (m *ScheduledEventMutation) OldField(ctx context.Context, name string) (ent.Value, error) { switch name { - case agent.FieldSlug: - return m.OldSlug(ctx) - case agent.FieldName: - return m.OldName(ctx) - case agent.FieldTemplate: - return m.OldTemplate(ctx) - case agent.FieldProjectID: + case scheduledevent.FieldProjectID: return m.OldProjectID(ctx) - case agent.FieldStatus: + case scheduledevent.FieldEventType: + return m.OldEventType(ctx) + case scheduledevent.FieldFireAt: + return m.OldFireAt(ctx) + case scheduledevent.FieldPayload: + return m.OldPayload(ctx) + case scheduledevent.FieldStatus: return m.OldStatus(ctx) - case agent.FieldCreatedBy: + case scheduledevent.FieldCreatedBy: return m.OldCreatedBy(ctx) - case agent.FieldOwnerID: - return m.OldOwnerID(ctx) - case agent.FieldDelegationEnabled: - return m.OldDelegationEnabled(ctx) - case agent.FieldVisibility: - return m.OldVisibility(ctx) - case agent.FieldCreated: + case scheduledevent.FieldFiredAt: + return m.OldFiredAt(ctx) + case scheduledevent.FieldError: + return m.OldError(ctx) + case scheduledevent.FieldScheduleID: + return m.OldScheduleID(ctx) + case scheduledevent.FieldCreated: return m.OldCreated(ctx) - case agent.FieldUpdated: - return m.OldUpdated(ctx) } - return nil, fmt.Errorf("unknown Agent field %s", name) + return nil, fmt.Errorf("unknown ScheduledEvent field %s", name) } // SetField sets the value of a field with the given name. It returns an error if // the field is not defined in the schema, or if the type mismatched the field // type. -func (m *AgentMutation) SetField(name string, value ent.Value) error { +func (m *ScheduledEventMutation) SetField(name string, value ent.Value) error { switch name { - case agent.FieldSlug: - v, ok := value.(string) + case scheduledevent.FieldProjectID: + v, ok := value.(uuid.UUID) if !ok { return fmt.Errorf("unexpected type %T for field %s", value, name) } - m.SetSlug(v) + m.SetProjectID(v) return nil - case agent.FieldName: + case scheduledevent.FieldEventType: v, ok := value.(string) if !ok { return fmt.Errorf("unexpected type %T for field %s", value, name) } - m.SetName(v) + m.SetEventType(v) return nil - case agent.FieldTemplate: - v, ok := value.(string) + case scheduledevent.FieldFireAt: + v, ok := value.(time.Time) if !ok { return fmt.Errorf("unexpected type %T for field %s", value, name) } - m.SetTemplate(v) + m.SetFireAt(v) return nil - case agent.FieldProjectID: - v, ok := value.(uuid.UUID) + case scheduledevent.FieldPayload: + v, ok := value.(string) if !ok { return fmt.Errorf("unexpected type %T for field %s", value, name) } - m.SetProjectID(v) + m.SetPayload(v) return nil - case agent.FieldStatus: - v, ok := value.(agent.Status) + case scheduledevent.FieldStatus: + v, ok := value.(string) if !ok { return fmt.Errorf("unexpected type %T for field %s", value, name) } m.SetStatus(v) return nil - case agent.FieldCreatedBy: - v, ok := value.(uuid.UUID) + case scheduledevent.FieldCreatedBy: + v, ok := value.(string) if !ok { return fmt.Errorf("unexpected type %T for field %s", value, name) } m.SetCreatedBy(v) return nil - case agent.FieldOwnerID: - v, ok := value.(uuid.UUID) + case scheduledevent.FieldFiredAt: + v, ok := value.(time.Time) if !ok { return fmt.Errorf("unexpected type %T for field %s", value, name) } - m.SetOwnerID(v) + m.SetFiredAt(v) return nil - case agent.FieldDelegationEnabled: - v, ok := value.(bool) + case scheduledevent.FieldError: + v, ok := value.(string) if !ok { return fmt.Errorf("unexpected type %T for field %s", value, name) } - m.SetDelegationEnabled(v) + m.SetError(v) return nil - case agent.FieldVisibility: + case scheduledevent.FieldScheduleID: v, ok := value.(string) if !ok { return fmt.Errorf("unexpected type %T for field %s", value, name) } - m.SetVisibility(v) + m.SetScheduleID(v) return nil - case agent.FieldCreated: + case scheduledevent.FieldCreated: v, ok := value.(time.Time) if !ok { return fmt.Errorf("unexpected type %T for field %s", value, name) } m.SetCreated(v) return nil - case agent.FieldUpdated: - v, ok := value.(time.Time) - if !ok { - return fmt.Errorf("unexpected type %T for field %s", value, name) - } - m.SetUpdated(v) - return nil } - return fmt.Errorf("unknown Agent field %s", name) + return fmt.Errorf("unknown ScheduledEvent field %s", name) } // AddedFields returns all numeric fields that were incremented/decremented during // this mutation. -func (m *AgentMutation) AddedFields() []string { +func (m *ScheduledEventMutation) AddedFields() []string { return nil } // AddedField returns the numeric value that was incremented/decremented on a field // with the given name. The second boolean return value indicates that this field // was not set, or was not defined in the schema. -func (m *AgentMutation) AddedField(name string) (ent.Value, bool) { +func (m *ScheduledEventMutation) AddedField(name string) (ent.Value, bool) { return nil, false } // AddField adds the value to the field with the given name. It returns an error if // the field is not defined in the schema, or if the type mismatched the field // type. -func (m *AgentMutation) AddField(name string, value ent.Value) error { +func (m *ScheduledEventMutation) AddField(name string, value ent.Value) error { switch name { } - return fmt.Errorf("unknown Agent numeric field %s", name) + return fmt.Errorf("unknown ScheduledEvent numeric field %s", name) } // ClearedFields returns all nullable fields that were cleared during this // mutation. -func (m *AgentMutation) ClearedFields() []string { +func (m *ScheduledEventMutation) ClearedFields() []string { var fields []string - if m.FieldCleared(agent.FieldTemplate) { - fields = append(fields, agent.FieldTemplate) + if m.FieldCleared(scheduledevent.FieldCreatedBy) { + fields = append(fields, scheduledevent.FieldCreatedBy) } - if m.FieldCleared(agent.FieldCreatedBy) { - fields = append(fields, agent.FieldCreatedBy) + if m.FieldCleared(scheduledevent.FieldFiredAt) { + fields = append(fields, scheduledevent.FieldFiredAt) } - if m.FieldCleared(agent.FieldOwnerID) { - fields = append(fields, agent.FieldOwnerID) + if m.FieldCleared(scheduledevent.FieldError) { + fields = append(fields, scheduledevent.FieldError) + } + if m.FieldCleared(scheduledevent.FieldScheduleID) { + fields = append(fields, scheduledevent.FieldScheduleID) } return fields } // FieldCleared returns a boolean indicating if a field with the given name was // cleared in this mutation. -func (m *AgentMutation) FieldCleared(name string) bool { +func (m *ScheduledEventMutation) FieldCleared(name string) bool { _, ok := m.clearedFields[name] return ok } // ClearField clears the value of the field with the given name. It returns an // error if the field is not defined in the schema. -func (m *AgentMutation) ClearField(name string) error { +func (m *ScheduledEventMutation) ClearField(name string) error { switch name { - case agent.FieldTemplate: - m.ClearTemplate() - return nil - case agent.FieldCreatedBy: + case scheduledevent.FieldCreatedBy: m.ClearCreatedBy() return nil - case agent.FieldOwnerID: - m.ClearOwnerID() + case scheduledevent.FieldFiredAt: + m.ClearFiredAt() + return nil + case scheduledevent.FieldError: + m.ClearError() + return nil + case scheduledevent.FieldScheduleID: + m.ClearScheduleID() return nil } - return fmt.Errorf("unknown Agent nullable field %s", name) + return fmt.Errorf("unknown ScheduledEvent nullable field %s", name) } // ResetField resets all changes in the mutation for the field with the given name. // It returns an error if the field is not defined in the schema. -func (m *AgentMutation) ResetField(name string) error { +func (m *ScheduledEventMutation) ResetField(name string) error { switch name { - case agent.FieldSlug: - m.ResetSlug() + case scheduledevent.FieldProjectID: + m.ResetProjectID() return nil - case agent.FieldName: - m.ResetName() + case scheduledevent.FieldEventType: + m.ResetEventType() return nil - case agent.FieldTemplate: - m.ResetTemplate() + case scheduledevent.FieldFireAt: + m.ResetFireAt() return nil - case agent.FieldProjectID: - m.ResetProjectID() + case scheduledevent.FieldPayload: + m.ResetPayload() return nil - case agent.FieldStatus: + case scheduledevent.FieldStatus: m.ResetStatus() return nil - case agent.FieldCreatedBy: + case scheduledevent.FieldCreatedBy: m.ResetCreatedBy() return nil - case agent.FieldOwnerID: - m.ResetOwnerID() + case scheduledevent.FieldFiredAt: + m.ResetFiredAt() return nil - case agent.FieldDelegationEnabled: - m.ResetDelegationEnabled() + case scheduledevent.FieldError: + m.ResetError() return nil - case agent.FieldVisibility: - m.ResetVisibility() + case scheduledevent.FieldScheduleID: + m.ResetScheduleID() return nil - case agent.FieldCreated: + case scheduledevent.FieldCreated: m.ResetCreated() return nil - case agent.FieldUpdated: - m.ResetUpdated() - return nil - } - return fmt.Errorf("unknown Agent field %s", name) -} - -// AddedEdges returns all edge names that were set/added in this mutation. -func (m *AgentMutation) AddedEdges() []string { - edges := make([]string, 0, 5) - if m.project != nil { - edges = append(edges, agent.EdgeProject) - } - if m.creator != nil { - edges = append(edges, agent.EdgeCreator) - } - if m.owner != nil { - edges = append(edges, agent.EdgeOwner) - } - if m.memberships != nil { - edges = append(edges, agent.EdgeMemberships) - } - if m.policy_bindings != nil { - edges = append(edges, agent.EdgePolicyBindings) } - return edges + return fmt.Errorf("unknown ScheduledEvent field %s", name) } - -// AddedIDs returns all IDs (to other nodes) that were added for the given edge -// name in this mutation. -func (m *AgentMutation) AddedIDs(name string) []ent.Value { - switch name { - case agent.EdgeProject: - if id := m.project; id != nil { - return []ent.Value{*id} - } - case agent.EdgeCreator: - if id := m.creator; id != nil { - return []ent.Value{*id} - } - case agent.EdgeOwner: - if id := m.owner; id != nil { - return []ent.Value{*id} - } - case agent.EdgeMemberships: - ids := make([]ent.Value, 0, len(m.memberships)) - for id := range m.memberships { - ids = append(ids, id) - } - return ids - case agent.EdgePolicyBindings: - ids := make([]ent.Value, 0, len(m.policy_bindings)) - for id := range m.policy_bindings { - ids = append(ids, id) - } - return ids - } + +// AddedEdges returns all edge names that were set/added in this mutation. +func (m *ScheduledEventMutation) AddedEdges() []string { + edges := make([]string, 0, 0) + return edges +} + +// AddedIDs returns all IDs (to other nodes) that were added for the given edge +// name in this mutation. +func (m *ScheduledEventMutation) AddedIDs(name string) []ent.Value { return nil } // RemovedEdges returns all edge names that were removed in this mutation. -func (m *AgentMutation) RemovedEdges() []string { - edges := make([]string, 0, 5) - if m.removedmemberships != nil { - edges = append(edges, agent.EdgeMemberships) - } - if m.removedpolicy_bindings != nil { - edges = append(edges, agent.EdgePolicyBindings) - } +func (m *ScheduledEventMutation) RemovedEdges() []string { + edges := make([]string, 0, 0) return edges } // RemovedIDs returns all IDs (to other nodes) that were removed for the edge with // the given name in this mutation. -func (m *AgentMutation) RemovedIDs(name string) []ent.Value { - switch name { - case agent.EdgeMemberships: - ids := make([]ent.Value, 0, len(m.removedmemberships)) - for id := range m.removedmemberships { - ids = append(ids, id) - } - return ids - case agent.EdgePolicyBindings: - ids := make([]ent.Value, 0, len(m.removedpolicy_bindings)) - for id := range m.removedpolicy_bindings { - ids = append(ids, id) - } - return ids - } +func (m *ScheduledEventMutation) RemovedIDs(name string) []ent.Value { return nil } // ClearedEdges returns all edge names that were cleared in this mutation. -func (m *AgentMutation) ClearedEdges() []string { - edges := make([]string, 0, 5) - if m.clearedproject { - edges = append(edges, agent.EdgeProject) - } - if m.clearedcreator { - edges = append(edges, agent.EdgeCreator) - } - if m.clearedowner { - edges = append(edges, agent.EdgeOwner) - } - if m.clearedmemberships { - edges = append(edges, agent.EdgeMemberships) - } - if m.clearedpolicy_bindings { - edges = append(edges, agent.EdgePolicyBindings) - } +func (m *ScheduledEventMutation) ClearedEdges() []string { + edges := make([]string, 0, 0) return edges } // EdgeCleared returns a boolean which indicates if the edge with the given name // was cleared in this mutation. -func (m *AgentMutation) EdgeCleared(name string) bool { - switch name { - case agent.EdgeProject: - return m.clearedproject - case agent.EdgeCreator: - return m.clearedcreator - case agent.EdgeOwner: - return m.clearedowner - case agent.EdgeMemberships: - return m.clearedmemberships - case agent.EdgePolicyBindings: - return m.clearedpolicy_bindings - } +func (m *ScheduledEventMutation) EdgeCleared(name string) bool { return false } // ClearEdge clears the value of the edge with the given name. It returns an error // if that edge is not defined in the schema. -func (m *AgentMutation) ClearEdge(name string) error { - switch name { - case agent.EdgeProject: - m.ClearProject() - return nil - case agent.EdgeCreator: - m.ClearCreator() - return nil - case agent.EdgeOwner: - m.ClearOwner() - return nil - } - return fmt.Errorf("unknown Agent unique edge %s", name) +func (m *ScheduledEventMutation) ClearEdge(name string) error { + return fmt.Errorf("unknown ScheduledEvent unique edge %s", name) } // ResetEdge resets all changes to the edge with the given name in this mutation. // It returns an error if the edge is not defined in the schema. -func (m *AgentMutation) ResetEdge(name string) error { - switch name { - case agent.EdgeProject: - m.ResetProject() - return nil - case agent.EdgeCreator: - m.ResetCreator() - return nil - case agent.EdgeOwner: - m.ResetOwner() - return nil - case agent.EdgeMemberships: - m.ResetMemberships() - return nil - case agent.EdgePolicyBindings: - m.ResetPolicyBindings() - return nil - } - return fmt.Errorf("unknown Agent edge %s", name) +func (m *ScheduledEventMutation) ResetEdge(name string) error { + return fmt.Errorf("unknown ScheduledEvent edge %s", name) } -// GroupMutation represents an operation that mutates the Group nodes in the graph. -type GroupMutation struct { +// SecretMutation represents an operation that mutates the Secret nodes in the graph. +type SecretMutation struct { config - op Op - typ string - id *uuid.UUID - name *string - slug *string - description *string - group_type *group.GroupType - project_id *uuid.UUID - labels *map[string]string - annotations *map[string]string - created *time.Time - updated *time.Time - created_by *string - clearedFields map[string]struct{} - memberships map[uuid.UUID]struct{} - removedmemberships map[uuid.UUID]struct{} - clearedmemberships bool - parent_groups map[uuid.UUID]struct{} - removedparent_groups map[uuid.UUID]struct{} - clearedparent_groups bool - child_groups map[uuid.UUID]struct{} - removedchild_groups map[uuid.UUID]struct{} - clearedchild_groups bool - owner *uuid.UUID - clearedowner bool - policy_bindings map[uuid.UUID]struct{} - removedpolicy_bindings map[uuid.UUID]struct{} - clearedpolicy_bindings bool - done bool - oldValue func(context.Context) (*Group, error) - predicates []predicate.Group + op Op + typ string + id *uuid.UUID + key *string + encrypted_value *string + secret_ref *string + secret_type *secret.SecretType + target *string + scope *string + scope_id *string + description *string + injection_mode *secret.InjectionMode + allow_progeny *bool + version *int + addversion *int + created_by *string + updated_by *string + created *time.Time + updated *time.Time + clearedFields map[string]struct{} + done bool + oldValue func(context.Context) (*Secret, error) + predicates []predicate.Secret } -var _ ent.Mutation = (*GroupMutation)(nil) +var _ ent.Mutation = (*SecretMutation)(nil) -// groupOption allows management of the mutation configuration using functional options. -type groupOption func(*GroupMutation) +// secretOption allows management of the mutation configuration using functional options. +type secretOption func(*SecretMutation) -// newGroupMutation creates new mutation for the Group entity. -func newGroupMutation(c config, op Op, opts ...groupOption) *GroupMutation { - m := &GroupMutation{ +// newSecretMutation creates new mutation for the Secret entity. +func newSecretMutation(c config, op Op, opts ...secretOption) *SecretMutation { + m := &SecretMutation{ config: c, op: op, - typ: TypeGroup, + typ: TypeSecret, clearedFields: make(map[string]struct{}), } for _, opt := range opts { opt(m) } - return m + return m +} + +// withSecretID sets the ID field of the mutation. +func withSecretID(id uuid.UUID) secretOption { + return func(m *SecretMutation) { + var ( + err error + once sync.Once + value *Secret + ) + m.oldValue = func(ctx context.Context) (*Secret, error) { + once.Do(func() { + if m.done { + err = errors.New("querying old values post mutation is not allowed") + } else { + value, err = m.Client().Secret.Get(ctx, id) + } + }) + return value, err + } + m.id = &id + } +} + +// withSecret sets the old Secret of the mutation. +func withSecret(node *Secret) secretOption { + return func(m *SecretMutation) { + m.oldValue = func(context.Context) (*Secret, error) { + return node, nil + } + m.id = &node.ID + } +} + +// Client returns a new `ent.Client` from the mutation. If the mutation was +// executed in a transaction (ent.Tx), a transactional client is returned. +func (m SecretMutation) Client() *Client { + client := &Client{config: m.config} + client.init() + return client +} + +// Tx returns an `ent.Tx` for mutations that were executed in transactions; +// it returns an error otherwise. +func (m SecretMutation) Tx() (*Tx, error) { + if _, ok := m.driver.(*txDriver); !ok { + return nil, errors.New("ent: mutation is not running in a transaction") + } + tx := &Tx{config: m.config} + tx.init() + return tx, nil +} + +// SetID sets the value of the id field. Note that this +// operation is only accepted on creation of Secret entities. +func (m *SecretMutation) SetID(id uuid.UUID) { + m.id = &id +} + +// ID returns the ID value in the mutation. Note that the ID is only available +// if it was provided to the builder or after it was returned from the database. +func (m *SecretMutation) ID() (id uuid.UUID, exists bool) { + if m.id == nil { + return + } + return *m.id, true +} + +// IDs queries the database and returns the entity ids that match the mutation's predicate. +// That means, if the mutation is applied within a transaction with an isolation level such +// as sql.LevelSerializable, the returned ids match the ids of the rows that will be updated +// or updated by the mutation. +func (m *SecretMutation) IDs(ctx context.Context) ([]uuid.UUID, error) { + switch { + case m.op.Is(OpUpdateOne | OpDeleteOne): + id, exists := m.ID() + if exists { + return []uuid.UUID{id}, nil + } + fallthrough + case m.op.Is(OpUpdate | OpDelete): + return m.Client().Secret.Query().Where(m.predicates...).IDs(ctx) + default: + return nil, fmt.Errorf("IDs is not allowed on %s operations", m.op) + } +} + +// SetKey sets the "key" field. +func (m *SecretMutation) SetKey(s string) { + m.key = &s +} + +// Key returns the value of the "key" field in the mutation. +func (m *SecretMutation) Key() (r string, exists bool) { + v := m.key + if v == nil { + return + } + return *v, true +} + +// OldKey returns the old "key" field's value of the Secret entity. +// If the Secret object wasn't provided to the builder, the object is fetched from the database. +// An error is returned if the mutation operation is not UpdateOne, or the database query fails. +func (m *SecretMutation) OldKey(ctx context.Context) (v string, err error) { + if !m.op.Is(OpUpdateOne) { + return v, errors.New("OldKey is only allowed on UpdateOne operations") + } + if m.id == nil || m.oldValue == nil { + return v, errors.New("OldKey requires an ID field in the mutation") + } + oldValue, err := m.oldValue(ctx) + if err != nil { + return v, fmt.Errorf("querying old value for OldKey: %w", err) + } + return oldValue.Key, nil +} + +// ResetKey resets all changes to the "key" field. +func (m *SecretMutation) ResetKey() { + m.key = nil +} + +// SetEncryptedValue sets the "encrypted_value" field. +func (m *SecretMutation) SetEncryptedValue(s string) { + m.encrypted_value = &s +} + +// EncryptedValue returns the value of the "encrypted_value" field in the mutation. +func (m *SecretMutation) EncryptedValue() (r string, exists bool) { + v := m.encrypted_value + if v == nil { + return + } + return *v, true +} + +// OldEncryptedValue returns the old "encrypted_value" field's value of the Secret entity. +// If the Secret object wasn't provided to the builder, the object is fetched from the database. +// An error is returned if the mutation operation is not UpdateOne, or the database query fails. +func (m *SecretMutation) OldEncryptedValue(ctx context.Context) (v string, err error) { + if !m.op.Is(OpUpdateOne) { + return v, errors.New("OldEncryptedValue is only allowed on UpdateOne operations") + } + if m.id == nil || m.oldValue == nil { + return v, errors.New("OldEncryptedValue requires an ID field in the mutation") + } + oldValue, err := m.oldValue(ctx) + if err != nil { + return v, fmt.Errorf("querying old value for OldEncryptedValue: %w", err) + } + return oldValue.EncryptedValue, nil +} + +// ResetEncryptedValue resets all changes to the "encrypted_value" field. +func (m *SecretMutation) ResetEncryptedValue() { + m.encrypted_value = nil +} + +// SetSecretRef sets the "secret_ref" field. +func (m *SecretMutation) SetSecretRef(s string) { + m.secret_ref = &s +} + +// SecretRef returns the value of the "secret_ref" field in the mutation. +func (m *SecretMutation) SecretRef() (r string, exists bool) { + v := m.secret_ref + if v == nil { + return + } + return *v, true +} + +// OldSecretRef returns the old "secret_ref" field's value of the Secret entity. +// If the Secret object wasn't provided to the builder, the object is fetched from the database. +// An error is returned if the mutation operation is not UpdateOne, or the database query fails. +func (m *SecretMutation) OldSecretRef(ctx context.Context) (v string, err error) { + if !m.op.Is(OpUpdateOne) { + return v, errors.New("OldSecretRef is only allowed on UpdateOne operations") + } + if m.id == nil || m.oldValue == nil { + return v, errors.New("OldSecretRef requires an ID field in the mutation") + } + oldValue, err := m.oldValue(ctx) + if err != nil { + return v, fmt.Errorf("querying old value for OldSecretRef: %w", err) + } + return oldValue.SecretRef, nil } -// withGroupID sets the ID field of the mutation. -func withGroupID(id uuid.UUID) groupOption { - return func(m *GroupMutation) { - var ( - err error - once sync.Once - value *Group - ) - m.oldValue = func(ctx context.Context) (*Group, error) { - once.Do(func() { - if m.done { - err = errors.New("querying old values post mutation is not allowed") - } else { - value, err = m.Client().Group.Get(ctx, id) - } - }) - return value, err - } - m.id = &id - } +// ClearSecretRef clears the value of the "secret_ref" field. +func (m *SecretMutation) ClearSecretRef() { + m.secret_ref = nil + m.clearedFields[secret.FieldSecretRef] = struct{}{} } -// withGroup sets the old Group of the mutation. -func withGroup(node *Group) groupOption { - return func(m *GroupMutation) { - m.oldValue = func(context.Context) (*Group, error) { - return node, nil - } - m.id = &node.ID - } +// SecretRefCleared returns if the "secret_ref" field was cleared in this mutation. +func (m *SecretMutation) SecretRefCleared() bool { + _, ok := m.clearedFields[secret.FieldSecretRef] + return ok } -// Client returns a new `ent.Client` from the mutation. If the mutation was -// executed in a transaction (ent.Tx), a transactional client is returned. -func (m GroupMutation) Client() *Client { - client := &Client{config: m.config} - client.init() - return client +// ResetSecretRef resets all changes to the "secret_ref" field. +func (m *SecretMutation) ResetSecretRef() { + m.secret_ref = nil + delete(m.clearedFields, secret.FieldSecretRef) } -// Tx returns an `ent.Tx` for mutations that were executed in transactions; -// it returns an error otherwise. -func (m GroupMutation) Tx() (*Tx, error) { - if _, ok := m.driver.(*txDriver); !ok { - return nil, errors.New("ent: mutation is not running in a transaction") +// SetSecretType sets the "secret_type" field. +func (m *SecretMutation) SetSecretType(st secret.SecretType) { + m.secret_type = &st +} + +// SecretType returns the value of the "secret_type" field in the mutation. +func (m *SecretMutation) SecretType() (r secret.SecretType, exists bool) { + v := m.secret_type + if v == nil { + return } - tx := &Tx{config: m.config} - tx.init() - return tx, nil + return *v, true } -// SetID sets the value of the id field. Note that this -// operation is only accepted on creation of Group entities. -func (m *GroupMutation) SetID(id uuid.UUID) { - m.id = &id +// OldSecretType returns the old "secret_type" field's value of the Secret entity. +// If the Secret object wasn't provided to the builder, the object is fetched from the database. +// An error is returned if the mutation operation is not UpdateOne, or the database query fails. +func (m *SecretMutation) OldSecretType(ctx context.Context) (v secret.SecretType, err error) { + if !m.op.Is(OpUpdateOne) { + return v, errors.New("OldSecretType is only allowed on UpdateOne operations") + } + if m.id == nil || m.oldValue == nil { + return v, errors.New("OldSecretType requires an ID field in the mutation") + } + oldValue, err := m.oldValue(ctx) + if err != nil { + return v, fmt.Errorf("querying old value for OldSecretType: %w", err) + } + return oldValue.SecretType, nil } -// ID returns the ID value in the mutation. Note that the ID is only available -// if it was provided to the builder or after it was returned from the database. -func (m *GroupMutation) ID() (id uuid.UUID, exists bool) { - if m.id == nil { +// ResetSecretType resets all changes to the "secret_type" field. +func (m *SecretMutation) ResetSecretType() { + m.secret_type = nil +} + +// SetTarget sets the "target" field. +func (m *SecretMutation) SetTarget(s string) { + m.target = &s +} + +// Target returns the value of the "target" field in the mutation. +func (m *SecretMutation) Target() (r string, exists bool) { + v := m.target + if v == nil { return } - return *m.id, true + return *v, true } -// IDs queries the database and returns the entity ids that match the mutation's predicate. -// That means, if the mutation is applied within a transaction with an isolation level such -// as sql.LevelSerializable, the returned ids match the ids of the rows that will be updated -// or updated by the mutation. -func (m *GroupMutation) IDs(ctx context.Context) ([]uuid.UUID, error) { - switch { - case m.op.Is(OpUpdateOne | OpDeleteOne): - id, exists := m.ID() - if exists { - return []uuid.UUID{id}, nil - } - fallthrough - case m.op.Is(OpUpdate | OpDelete): - return m.Client().Group.Query().Where(m.predicates...).IDs(ctx) - default: - return nil, fmt.Errorf("IDs is not allowed on %s operations", m.op) +// OldTarget returns the old "target" field's value of the Secret entity. +// If the Secret object wasn't provided to the builder, the object is fetched from the database. +// An error is returned if the mutation operation is not UpdateOne, or the database query fails. +func (m *SecretMutation) OldTarget(ctx context.Context) (v string, err error) { + if !m.op.Is(OpUpdateOne) { + return v, errors.New("OldTarget is only allowed on UpdateOne operations") + } + if m.id == nil || m.oldValue == nil { + return v, errors.New("OldTarget requires an ID field in the mutation") + } + oldValue, err := m.oldValue(ctx) + if err != nil { + return v, fmt.Errorf("querying old value for OldTarget: %w", err) } + return oldValue.Target, nil } -// SetName sets the "name" field. -func (m *GroupMutation) SetName(s string) { - m.name = &s +// ClearTarget clears the value of the "target" field. +func (m *SecretMutation) ClearTarget() { + m.target = nil + m.clearedFields[secret.FieldTarget] = struct{}{} } -// Name returns the value of the "name" field in the mutation. -func (m *GroupMutation) Name() (r string, exists bool) { - v := m.name +// TargetCleared returns if the "target" field was cleared in this mutation. +func (m *SecretMutation) TargetCleared() bool { + _, ok := m.clearedFields[secret.FieldTarget] + return ok +} + +// ResetTarget resets all changes to the "target" field. +func (m *SecretMutation) ResetTarget() { + m.target = nil + delete(m.clearedFields, secret.FieldTarget) +} + +// SetScope sets the "scope" field. +func (m *SecretMutation) SetScope(s string) { + m.scope = &s +} + +// Scope returns the value of the "scope" field in the mutation. +func (m *SecretMutation) Scope() (r string, exists bool) { + v := m.scope if v == nil { return } return *v, true } -// OldName returns the old "name" field's value of the Group entity. -// If the Group object wasn't provided to the builder, the object is fetched from the database. +// OldScope returns the old "scope" field's value of the Secret entity. +// If the Secret object wasn't provided to the builder, the object is fetched from the database. // An error is returned if the mutation operation is not UpdateOne, or the database query fails. -func (m *GroupMutation) OldName(ctx context.Context) (v string, err error) { +func (m *SecretMutation) OldScope(ctx context.Context) (v string, err error) { if !m.op.Is(OpUpdateOne) { - return v, errors.New("OldName is only allowed on UpdateOne operations") + return v, errors.New("OldScope is only allowed on UpdateOne operations") } if m.id == nil || m.oldValue == nil { - return v, errors.New("OldName requires an ID field in the mutation") + return v, errors.New("OldScope requires an ID field in the mutation") } oldValue, err := m.oldValue(ctx) if err != nil { - return v, fmt.Errorf("querying old value for OldName: %w", err) + return v, fmt.Errorf("querying old value for OldScope: %w", err) } - return oldValue.Name, nil + return oldValue.Scope, nil } -// ResetName resets all changes to the "name" field. -func (m *GroupMutation) ResetName() { - m.name = nil +// ResetScope resets all changes to the "scope" field. +func (m *SecretMutation) ResetScope() { + m.scope = nil } -// SetSlug sets the "slug" field. -func (m *GroupMutation) SetSlug(s string) { - m.slug = &s +// SetScopeID sets the "scope_id" field. +func (m *SecretMutation) SetScopeID(s string) { + m.scope_id = &s } -// Slug returns the value of the "slug" field in the mutation. -func (m *GroupMutation) Slug() (r string, exists bool) { - v := m.slug +// ScopeID returns the value of the "scope_id" field in the mutation. +func (m *SecretMutation) ScopeID() (r string, exists bool) { + v := m.scope_id if v == nil { return } return *v, true } -// OldSlug returns the old "slug" field's value of the Group entity. -// If the Group object wasn't provided to the builder, the object is fetched from the database. +// OldScopeID returns the old "scope_id" field's value of the Secret entity. +// If the Secret object wasn't provided to the builder, the object is fetched from the database. // An error is returned if the mutation operation is not UpdateOne, or the database query fails. -func (m *GroupMutation) OldSlug(ctx context.Context) (v string, err error) { +func (m *SecretMutation) OldScopeID(ctx context.Context) (v string, err error) { if !m.op.Is(OpUpdateOne) { - return v, errors.New("OldSlug is only allowed on UpdateOne operations") + return v, errors.New("OldScopeID is only allowed on UpdateOne operations") } if m.id == nil || m.oldValue == nil { - return v, errors.New("OldSlug requires an ID field in the mutation") + return v, errors.New("OldScopeID requires an ID field in the mutation") } oldValue, err := m.oldValue(ctx) if err != nil { - return v, fmt.Errorf("querying old value for OldSlug: %w", err) + return v, fmt.Errorf("querying old value for OldScopeID: %w", err) } - return oldValue.Slug, nil + return oldValue.ScopeID, nil } -// ResetSlug resets all changes to the "slug" field. -func (m *GroupMutation) ResetSlug() { - m.slug = nil +// ResetScopeID resets all changes to the "scope_id" field. +func (m *SecretMutation) ResetScopeID() { + m.scope_id = nil } // SetDescription sets the "description" field. -func (m *GroupMutation) SetDescription(s string) { +func (m *SecretMutation) SetDescription(s string) { m.description = &s } // Description returns the value of the "description" field in the mutation. -func (m *GroupMutation) Description() (r string, exists bool) { +func (m *SecretMutation) Description() (r string, exists bool) { v := m.description if v == nil { return @@ -2915,10 +28297,10 @@ func (m *GroupMutation) Description() (r string, exists bool) { return *v, true } -// OldDescription returns the old "description" field's value of the Group entity. -// If the Group object wasn't provided to the builder, the object is fetched from the database. +// OldDescription returns the old "description" field's value of the Secret entity. +// If the Secret object wasn't provided to the builder, the object is fetched from the database. // An error is returned if the mutation operation is not UpdateOne, or the database query fails. -func (m *GroupMutation) OldDescription(ctx context.Context) (v string, err error) { +func (m *SecretMutation) OldDescription(ctx context.Context) (v string, err error) { if !m.op.Is(OpUpdateOne) { return v, errors.New("OldDescription is only allowed on UpdateOne operations") } @@ -2933,628 +28315,1109 @@ func (m *GroupMutation) OldDescription(ctx context.Context) (v string, err error } // ClearDescription clears the value of the "description" field. -func (m *GroupMutation) ClearDescription() { +func (m *SecretMutation) ClearDescription() { m.description = nil - m.clearedFields[group.FieldDescription] = struct{}{} + m.clearedFields[secret.FieldDescription] = struct{}{} } // DescriptionCleared returns if the "description" field was cleared in this mutation. -func (m *GroupMutation) DescriptionCleared() bool { - _, ok := m.clearedFields[group.FieldDescription] +func (m *SecretMutation) DescriptionCleared() bool { + _, ok := m.clearedFields[secret.FieldDescription] return ok } // ResetDescription resets all changes to the "description" field. -func (m *GroupMutation) ResetDescription() { +func (m *SecretMutation) ResetDescription() { m.description = nil - delete(m.clearedFields, group.FieldDescription) + delete(m.clearedFields, secret.FieldDescription) } -// SetGroupType sets the "group_type" field. -func (m *GroupMutation) SetGroupType(gt group.GroupType) { - m.group_type = > +// SetInjectionMode sets the "injection_mode" field. +func (m *SecretMutation) SetInjectionMode(sm secret.InjectionMode) { + m.injection_mode = &sm } -// GroupType returns the value of the "group_type" field in the mutation. -func (m *GroupMutation) GroupType() (r group.GroupType, exists bool) { - v := m.group_type +// InjectionMode returns the value of the "injection_mode" field in the mutation. +func (m *SecretMutation) InjectionMode() (r secret.InjectionMode, exists bool) { + v := m.injection_mode if v == nil { return } return *v, true } -// OldGroupType returns the old "group_type" field's value of the Group entity. -// If the Group object wasn't provided to the builder, the object is fetched from the database. +// OldInjectionMode returns the old "injection_mode" field's value of the Secret entity. +// If the Secret object wasn't provided to the builder, the object is fetched from the database. // An error is returned if the mutation operation is not UpdateOne, or the database query fails. -func (m *GroupMutation) OldGroupType(ctx context.Context) (v group.GroupType, err error) { +func (m *SecretMutation) OldInjectionMode(ctx context.Context) (v secret.InjectionMode, err error) { if !m.op.Is(OpUpdateOne) { - return v, errors.New("OldGroupType is only allowed on UpdateOne operations") + return v, errors.New("OldInjectionMode is only allowed on UpdateOne operations") } if m.id == nil || m.oldValue == nil { - return v, errors.New("OldGroupType requires an ID field in the mutation") + return v, errors.New("OldInjectionMode requires an ID field in the mutation") } oldValue, err := m.oldValue(ctx) if err != nil { - return v, fmt.Errorf("querying old value for OldGroupType: %w", err) + return v, fmt.Errorf("querying old value for OldInjectionMode: %w", err) } - return oldValue.GroupType, nil + return oldValue.InjectionMode, nil } -// ResetGroupType resets all changes to the "group_type" field. -func (m *GroupMutation) ResetGroupType() { - m.group_type = nil +// ResetInjectionMode resets all changes to the "injection_mode" field. +func (m *SecretMutation) ResetInjectionMode() { + m.injection_mode = nil +} + +// SetAllowProgeny sets the "allow_progeny" field. +func (m *SecretMutation) SetAllowProgeny(b bool) { + m.allow_progeny = &b +} + +// AllowProgeny returns the value of the "allow_progeny" field in the mutation. +func (m *SecretMutation) AllowProgeny() (r bool, exists bool) { + v := m.allow_progeny + if v == nil { + return + } + return *v, true +} + +// OldAllowProgeny returns the old "allow_progeny" field's value of the Secret entity. +// If the Secret object wasn't provided to the builder, the object is fetched from the database. +// An error is returned if the mutation operation is not UpdateOne, or the database query fails. +func (m *SecretMutation) OldAllowProgeny(ctx context.Context) (v bool, err error) { + if !m.op.Is(OpUpdateOne) { + return v, errors.New("OldAllowProgeny is only allowed on UpdateOne operations") + } + if m.id == nil || m.oldValue == nil { + return v, errors.New("OldAllowProgeny requires an ID field in the mutation") + } + oldValue, err := m.oldValue(ctx) + if err != nil { + return v, fmt.Errorf("querying old value for OldAllowProgeny: %w", err) + } + return oldValue.AllowProgeny, nil +} + +// ResetAllowProgeny resets all changes to the "allow_progeny" field. +func (m *SecretMutation) ResetAllowProgeny() { + m.allow_progeny = nil +} + +// SetVersion sets the "version" field. +func (m *SecretMutation) SetVersion(i int) { + m.version = &i + m.addversion = nil +} + +// Version returns the value of the "version" field in the mutation. +func (m *SecretMutation) Version() (r int, exists bool) { + v := m.version + if v == nil { + return + } + return *v, true +} + +// OldVersion returns the old "version" field's value of the Secret entity. +// If the Secret object wasn't provided to the builder, the object is fetched from the database. +// An error is returned if the mutation operation is not UpdateOne, or the database query fails. +func (m *SecretMutation) OldVersion(ctx context.Context) (v int, err error) { + if !m.op.Is(OpUpdateOne) { + return v, errors.New("OldVersion is only allowed on UpdateOne operations") + } + if m.id == nil || m.oldValue == nil { + return v, errors.New("OldVersion requires an ID field in the mutation") + } + oldValue, err := m.oldValue(ctx) + if err != nil { + return v, fmt.Errorf("querying old value for OldVersion: %w", err) + } + return oldValue.Version, nil +} + +// AddVersion adds i to the "version" field. +func (m *SecretMutation) AddVersion(i int) { + if m.addversion != nil { + *m.addversion += i + } else { + m.addversion = &i + } +} + +// AddedVersion returns the value that was added to the "version" field in this mutation. +func (m *SecretMutation) AddedVersion() (r int, exists bool) { + v := m.addversion + if v == nil { + return + } + return *v, true +} + +// ResetVersion resets all changes to the "version" field. +func (m *SecretMutation) ResetVersion() { + m.version = nil + m.addversion = nil +} + +// SetCreatedBy sets the "created_by" field. +func (m *SecretMutation) SetCreatedBy(s string) { + m.created_by = &s +} + +// CreatedBy returns the value of the "created_by" field in the mutation. +func (m *SecretMutation) CreatedBy() (r string, exists bool) { + v := m.created_by + if v == nil { + return + } + return *v, true +} + +// OldCreatedBy returns the old "created_by" field's value of the Secret entity. +// If the Secret object wasn't provided to the builder, the object is fetched from the database. +// An error is returned if the mutation operation is not UpdateOne, or the database query fails. +func (m *SecretMutation) OldCreatedBy(ctx context.Context) (v string, err error) { + if !m.op.Is(OpUpdateOne) { + return v, errors.New("OldCreatedBy is only allowed on UpdateOne operations") + } + if m.id == nil || m.oldValue == nil { + return v, errors.New("OldCreatedBy requires an ID field in the mutation") + } + oldValue, err := m.oldValue(ctx) + if err != nil { + return v, fmt.Errorf("querying old value for OldCreatedBy: %w", err) + } + return oldValue.CreatedBy, nil +} + +// ClearCreatedBy clears the value of the "created_by" field. +func (m *SecretMutation) ClearCreatedBy() { + m.created_by = nil + m.clearedFields[secret.FieldCreatedBy] = struct{}{} +} + +// CreatedByCleared returns if the "created_by" field was cleared in this mutation. +func (m *SecretMutation) CreatedByCleared() bool { + _, ok := m.clearedFields[secret.FieldCreatedBy] + return ok } -// SetProjectID sets the "project_id" field. -func (m *GroupMutation) SetProjectID(u uuid.UUID) { - m.project_id = &u +// ResetCreatedBy resets all changes to the "created_by" field. +func (m *SecretMutation) ResetCreatedBy() { + m.created_by = nil + delete(m.clearedFields, secret.FieldCreatedBy) } -// ProjectID returns the value of the "project_id" field in the mutation. -func (m *GroupMutation) ProjectID() (r uuid.UUID, exists bool) { - v := m.project_id +// SetUpdatedBy sets the "updated_by" field. +func (m *SecretMutation) SetUpdatedBy(s string) { + m.updated_by = &s +} + +// UpdatedBy returns the value of the "updated_by" field in the mutation. +func (m *SecretMutation) UpdatedBy() (r string, exists bool) { + v := m.updated_by if v == nil { return } return *v, true } -// OldProjectID returns the old "project_id" field's value of the Group entity. -// If the Group object wasn't provided to the builder, the object is fetched from the database. +// OldUpdatedBy returns the old "updated_by" field's value of the Secret entity. +// If the Secret object wasn't provided to the builder, the object is fetched from the database. // An error is returned if the mutation operation is not UpdateOne, or the database query fails. -func (m *GroupMutation) OldProjectID(ctx context.Context) (v *uuid.UUID, err error) { +func (m *SecretMutation) OldUpdatedBy(ctx context.Context) (v string, err error) { if !m.op.Is(OpUpdateOne) { - return v, errors.New("OldProjectID is only allowed on UpdateOne operations") + return v, errors.New("OldUpdatedBy is only allowed on UpdateOne operations") } if m.id == nil || m.oldValue == nil { - return v, errors.New("OldProjectID requires an ID field in the mutation") + return v, errors.New("OldUpdatedBy requires an ID field in the mutation") } oldValue, err := m.oldValue(ctx) if err != nil { - return v, fmt.Errorf("querying old value for OldProjectID: %w", err) + return v, fmt.Errorf("querying old value for OldUpdatedBy: %w", err) } - return oldValue.ProjectID, nil + return oldValue.UpdatedBy, nil } -// ClearProjectID clears the value of the "project_id" field. -func (m *GroupMutation) ClearProjectID() { - m.project_id = nil - m.clearedFields[group.FieldProjectID] = struct{}{} +// ClearUpdatedBy clears the value of the "updated_by" field. +func (m *SecretMutation) ClearUpdatedBy() { + m.updated_by = nil + m.clearedFields[secret.FieldUpdatedBy] = struct{}{} } -// ProjectIDCleared returns if the "project_id" field was cleared in this mutation. -func (m *GroupMutation) ProjectIDCleared() bool { - _, ok := m.clearedFields[group.FieldProjectID] +// UpdatedByCleared returns if the "updated_by" field was cleared in this mutation. +func (m *SecretMutation) UpdatedByCleared() bool { + _, ok := m.clearedFields[secret.FieldUpdatedBy] return ok } -// ResetProjectID resets all changes to the "project_id" field. -func (m *GroupMutation) ResetProjectID() { - m.project_id = nil - delete(m.clearedFields, group.FieldProjectID) +// ResetUpdatedBy resets all changes to the "updated_by" field. +func (m *SecretMutation) ResetUpdatedBy() { + m.updated_by = nil + delete(m.clearedFields, secret.FieldUpdatedBy) } -// SetLabels sets the "labels" field. -func (m *GroupMutation) SetLabels(value map[string]string) { - m.labels = &value +// SetCreated sets the "created" field. +func (m *SecretMutation) SetCreated(t time.Time) { + m.created = &t } -// Labels returns the value of the "labels" field in the mutation. -func (m *GroupMutation) Labels() (r map[string]string, exists bool) { - v := m.labels +// Created returns the value of the "created" field in the mutation. +func (m *SecretMutation) Created() (r time.Time, exists bool) { + v := m.created if v == nil { return } return *v, true } -// OldLabels returns the old "labels" field's value of the Group entity. -// If the Group object wasn't provided to the builder, the object is fetched from the database. +// OldCreated returns the old "created" field's value of the Secret entity. +// If the Secret object wasn't provided to the builder, the object is fetched from the database. // An error is returned if the mutation operation is not UpdateOne, or the database query fails. -func (m *GroupMutation) OldLabels(ctx context.Context) (v map[string]string, err error) { +func (m *SecretMutation) OldCreated(ctx context.Context) (v time.Time, err error) { if !m.op.Is(OpUpdateOne) { - return v, errors.New("OldLabels is only allowed on UpdateOne operations") + return v, errors.New("OldCreated is only allowed on UpdateOne operations") } if m.id == nil || m.oldValue == nil { - return v, errors.New("OldLabels requires an ID field in the mutation") + return v, errors.New("OldCreated requires an ID field in the mutation") } oldValue, err := m.oldValue(ctx) if err != nil { - return v, fmt.Errorf("querying old value for OldLabels: %w", err) + return v, fmt.Errorf("querying old value for OldCreated: %w", err) } - return oldValue.Labels, nil -} - -// ClearLabels clears the value of the "labels" field. -func (m *GroupMutation) ClearLabels() { - m.labels = nil - m.clearedFields[group.FieldLabels] = struct{}{} -} - -// LabelsCleared returns if the "labels" field was cleared in this mutation. -func (m *GroupMutation) LabelsCleared() bool { - _, ok := m.clearedFields[group.FieldLabels] - return ok + return oldValue.Created, nil } -// ResetLabels resets all changes to the "labels" field. -func (m *GroupMutation) ResetLabels() { - m.labels = nil - delete(m.clearedFields, group.FieldLabels) +// ResetCreated resets all changes to the "created" field. +func (m *SecretMutation) ResetCreated() { + m.created = nil } -// SetAnnotations sets the "annotations" field. -func (m *GroupMutation) SetAnnotations(value map[string]string) { - m.annotations = &value +// SetUpdated sets the "updated" field. +func (m *SecretMutation) SetUpdated(t time.Time) { + m.updated = &t } -// Annotations returns the value of the "annotations" field in the mutation. -func (m *GroupMutation) Annotations() (r map[string]string, exists bool) { - v := m.annotations +// Updated returns the value of the "updated" field in the mutation. +func (m *SecretMutation) Updated() (r time.Time, exists bool) { + v := m.updated if v == nil { return } return *v, true } -// OldAnnotations returns the old "annotations" field's value of the Group entity. -// If the Group object wasn't provided to the builder, the object is fetched from the database. +// OldUpdated returns the old "updated" field's value of the Secret entity. +// If the Secret object wasn't provided to the builder, the object is fetched from the database. // An error is returned if the mutation operation is not UpdateOne, or the database query fails. -func (m *GroupMutation) OldAnnotations(ctx context.Context) (v map[string]string, err error) { +func (m *SecretMutation) OldUpdated(ctx context.Context) (v time.Time, err error) { if !m.op.Is(OpUpdateOne) { - return v, errors.New("OldAnnotations is only allowed on UpdateOne operations") + return v, errors.New("OldUpdated is only allowed on UpdateOne operations") } if m.id == nil || m.oldValue == nil { - return v, errors.New("OldAnnotations requires an ID field in the mutation") + return v, errors.New("OldUpdated requires an ID field in the mutation") } oldValue, err := m.oldValue(ctx) if err != nil { - return v, fmt.Errorf("querying old value for OldAnnotations: %w", err) + return v, fmt.Errorf("querying old value for OldUpdated: %w", err) } - return oldValue.Annotations, nil + return oldValue.Updated, nil } -// ClearAnnotations clears the value of the "annotations" field. -func (m *GroupMutation) ClearAnnotations() { - m.annotations = nil - m.clearedFields[group.FieldAnnotations] = struct{}{} +// ResetUpdated resets all changes to the "updated" field. +func (m *SecretMutation) ResetUpdated() { + m.updated = nil } -// AnnotationsCleared returns if the "annotations" field was cleared in this mutation. -func (m *GroupMutation) AnnotationsCleared() bool { - _, ok := m.clearedFields[group.FieldAnnotations] - return ok +// Where appends a list predicates to the SecretMutation builder. +func (m *SecretMutation) Where(ps ...predicate.Secret) { + m.predicates = append(m.predicates, ps...) } -// ResetAnnotations resets all changes to the "annotations" field. -func (m *GroupMutation) ResetAnnotations() { - m.annotations = nil - delete(m.clearedFields, group.FieldAnnotations) +// WhereP appends storage-level predicates to the SecretMutation builder. Using this method, +// users can use type-assertion to append predicates that do not depend on any generated package. +func (m *SecretMutation) WhereP(ps ...func(*sql.Selector)) { + p := make([]predicate.Secret, len(ps)) + for i := range ps { + p[i] = ps[i] + } + m.Where(p...) } -// SetCreated sets the "created" field. -func (m *GroupMutation) SetCreated(t time.Time) { - m.created = &t +// Op returns the operation name. +func (m *SecretMutation) Op() Op { + return m.op } -// Created returns the value of the "created" field in the mutation. -func (m *GroupMutation) Created() (r time.Time, exists bool) { - v := m.created - if v == nil { - return +// SetOp allows setting the mutation operation. +func (m *SecretMutation) SetOp(op Op) { + m.op = op +} + +// Type returns the node type of this mutation (Secret). +func (m *SecretMutation) Type() string { + return m.typ +} + +// Fields returns all fields that were changed during this mutation. Note that in +// order to get all numeric fields that were incremented/decremented, call +// AddedFields(). +func (m *SecretMutation) Fields() []string { + fields := make([]string, 0, 15) + if m.key != nil { + fields = append(fields, secret.FieldKey) + } + if m.encrypted_value != nil { + fields = append(fields, secret.FieldEncryptedValue) + } + if m.secret_ref != nil { + fields = append(fields, secret.FieldSecretRef) + } + if m.secret_type != nil { + fields = append(fields, secret.FieldSecretType) + } + if m.target != nil { + fields = append(fields, secret.FieldTarget) + } + if m.scope != nil { + fields = append(fields, secret.FieldScope) + } + if m.scope_id != nil { + fields = append(fields, secret.FieldScopeID) + } + if m.description != nil { + fields = append(fields, secret.FieldDescription) + } + if m.injection_mode != nil { + fields = append(fields, secret.FieldInjectionMode) + } + if m.allow_progeny != nil { + fields = append(fields, secret.FieldAllowProgeny) + } + if m.version != nil { + fields = append(fields, secret.FieldVersion) + } + if m.created_by != nil { + fields = append(fields, secret.FieldCreatedBy) + } + if m.updated_by != nil { + fields = append(fields, secret.FieldUpdatedBy) + } + if m.created != nil { + fields = append(fields, secret.FieldCreated) + } + if m.updated != nil { + fields = append(fields, secret.FieldUpdated) + } + return fields +} + +// Field returns the value of a field with the given name. The second boolean +// return value indicates that this field was not set, or was not defined in the +// schema. +func (m *SecretMutation) Field(name string) (ent.Value, bool) { + switch name { + case secret.FieldKey: + return m.Key() + case secret.FieldEncryptedValue: + return m.EncryptedValue() + case secret.FieldSecretRef: + return m.SecretRef() + case secret.FieldSecretType: + return m.SecretType() + case secret.FieldTarget: + return m.Target() + case secret.FieldScope: + return m.Scope() + case secret.FieldScopeID: + return m.ScopeID() + case secret.FieldDescription: + return m.Description() + case secret.FieldInjectionMode: + return m.InjectionMode() + case secret.FieldAllowProgeny: + return m.AllowProgeny() + case secret.FieldVersion: + return m.Version() + case secret.FieldCreatedBy: + return m.CreatedBy() + case secret.FieldUpdatedBy: + return m.UpdatedBy() + case secret.FieldCreated: + return m.Created() + case secret.FieldUpdated: + return m.Updated() + } + return nil, false +} + +// OldField returns the old value of the field from the database. An error is +// returned if the mutation operation is not UpdateOne, or the query to the +// database failed. +func (m *SecretMutation) OldField(ctx context.Context, name string) (ent.Value, error) { + switch name { + case secret.FieldKey: + return m.OldKey(ctx) + case secret.FieldEncryptedValue: + return m.OldEncryptedValue(ctx) + case secret.FieldSecretRef: + return m.OldSecretRef(ctx) + case secret.FieldSecretType: + return m.OldSecretType(ctx) + case secret.FieldTarget: + return m.OldTarget(ctx) + case secret.FieldScope: + return m.OldScope(ctx) + case secret.FieldScopeID: + return m.OldScopeID(ctx) + case secret.FieldDescription: + return m.OldDescription(ctx) + case secret.FieldInjectionMode: + return m.OldInjectionMode(ctx) + case secret.FieldAllowProgeny: + return m.OldAllowProgeny(ctx) + case secret.FieldVersion: + return m.OldVersion(ctx) + case secret.FieldCreatedBy: + return m.OldCreatedBy(ctx) + case secret.FieldUpdatedBy: + return m.OldUpdatedBy(ctx) + case secret.FieldCreated: + return m.OldCreated(ctx) + case secret.FieldUpdated: + return m.OldUpdated(ctx) + } + return nil, fmt.Errorf("unknown Secret field %s", name) +} + +// SetField sets the value of a field with the given name. It returns an error if +// the field is not defined in the schema, or if the type mismatched the field +// type. +func (m *SecretMutation) SetField(name string, value ent.Value) error { + switch name { + case secret.FieldKey: + v, ok := value.(string) + if !ok { + return fmt.Errorf("unexpected type %T for field %s", value, name) + } + m.SetKey(v) + return nil + case secret.FieldEncryptedValue: + v, ok := value.(string) + if !ok { + return fmt.Errorf("unexpected type %T for field %s", value, name) + } + m.SetEncryptedValue(v) + return nil + case secret.FieldSecretRef: + v, ok := value.(string) + if !ok { + return fmt.Errorf("unexpected type %T for field %s", value, name) + } + m.SetSecretRef(v) + return nil + case secret.FieldSecretType: + v, ok := value.(secret.SecretType) + if !ok { + return fmt.Errorf("unexpected type %T for field %s", value, name) + } + m.SetSecretType(v) + return nil + case secret.FieldTarget: + v, ok := value.(string) + if !ok { + return fmt.Errorf("unexpected type %T for field %s", value, name) + } + m.SetTarget(v) + return nil + case secret.FieldScope: + v, ok := value.(string) + if !ok { + return fmt.Errorf("unexpected type %T for field %s", value, name) + } + m.SetScope(v) + return nil + case secret.FieldScopeID: + v, ok := value.(string) + if !ok { + return fmt.Errorf("unexpected type %T for field %s", value, name) + } + m.SetScopeID(v) + return nil + case secret.FieldDescription: + v, ok := value.(string) + if !ok { + return fmt.Errorf("unexpected type %T for field %s", value, name) + } + m.SetDescription(v) + return nil + case secret.FieldInjectionMode: + v, ok := value.(secret.InjectionMode) + if !ok { + return fmt.Errorf("unexpected type %T for field %s", value, name) + } + m.SetInjectionMode(v) + return nil + case secret.FieldAllowProgeny: + v, ok := value.(bool) + if !ok { + return fmt.Errorf("unexpected type %T for field %s", value, name) + } + m.SetAllowProgeny(v) + return nil + case secret.FieldVersion: + v, ok := value.(int) + if !ok { + return fmt.Errorf("unexpected type %T for field %s", value, name) + } + m.SetVersion(v) + return nil + case secret.FieldCreatedBy: + v, ok := value.(string) + if !ok { + return fmt.Errorf("unexpected type %T for field %s", value, name) + } + m.SetCreatedBy(v) + return nil + case secret.FieldUpdatedBy: + v, ok := value.(string) + if !ok { + return fmt.Errorf("unexpected type %T for field %s", value, name) + } + m.SetUpdatedBy(v) + return nil + case secret.FieldCreated: + v, ok := value.(time.Time) + if !ok { + return fmt.Errorf("unexpected type %T for field %s", value, name) + } + m.SetCreated(v) + return nil + case secret.FieldUpdated: + v, ok := value.(time.Time) + if !ok { + return fmt.Errorf("unexpected type %T for field %s", value, name) + } + m.SetUpdated(v) + return nil } - return *v, true + return fmt.Errorf("unknown Secret field %s", name) } -// OldCreated returns the old "created" field's value of the Group entity. -// If the Group object wasn't provided to the builder, the object is fetched from the database. -// An error is returned if the mutation operation is not UpdateOne, or the database query fails. -func (m *GroupMutation) OldCreated(ctx context.Context) (v time.Time, err error) { - if !m.op.Is(OpUpdateOne) { - return v, errors.New("OldCreated is only allowed on UpdateOne operations") - } - if m.id == nil || m.oldValue == nil { - return v, errors.New("OldCreated requires an ID field in the mutation") - } - oldValue, err := m.oldValue(ctx) - if err != nil { - return v, fmt.Errorf("querying old value for OldCreated: %w", err) +// AddedFields returns all numeric fields that were incremented/decremented during +// this mutation. +func (m *SecretMutation) AddedFields() []string { + var fields []string + if m.addversion != nil { + fields = append(fields, secret.FieldVersion) } - return oldValue.Created, nil -} - -// ResetCreated resets all changes to the "created" field. -func (m *GroupMutation) ResetCreated() { - m.created = nil + return fields } -// SetUpdated sets the "updated" field. -func (m *GroupMutation) SetUpdated(t time.Time) { - m.updated = &t +// AddedField returns the numeric value that was incremented/decremented on a field +// with the given name. The second boolean return value indicates that this field +// was not set, or was not defined in the schema. +func (m *SecretMutation) AddedField(name string) (ent.Value, bool) { + switch name { + case secret.FieldVersion: + return m.AddedVersion() + } + return nil, false } -// Updated returns the value of the "updated" field in the mutation. -func (m *GroupMutation) Updated() (r time.Time, exists bool) { - v := m.updated - if v == nil { - return +// AddField adds the value to the field with the given name. It returns an error if +// the field is not defined in the schema, or if the type mismatched the field +// type. +func (m *SecretMutation) AddField(name string, value ent.Value) error { + switch name { + case secret.FieldVersion: + v, ok := value.(int) + if !ok { + return fmt.Errorf("unexpected type %T for field %s", value, name) + } + m.AddVersion(v) + return nil } - return *v, true + return fmt.Errorf("unknown Secret numeric field %s", name) } -// OldUpdated returns the old "updated" field's value of the Group entity. -// If the Group object wasn't provided to the builder, the object is fetched from the database. -// An error is returned if the mutation operation is not UpdateOne, or the database query fails. -func (m *GroupMutation) OldUpdated(ctx context.Context) (v time.Time, err error) { - if !m.op.Is(OpUpdateOne) { - return v, errors.New("OldUpdated is only allowed on UpdateOne operations") +// ClearedFields returns all nullable fields that were cleared during this +// mutation. +func (m *SecretMutation) ClearedFields() []string { + var fields []string + if m.FieldCleared(secret.FieldSecretRef) { + fields = append(fields, secret.FieldSecretRef) } - if m.id == nil || m.oldValue == nil { - return v, errors.New("OldUpdated requires an ID field in the mutation") + if m.FieldCleared(secret.FieldTarget) { + fields = append(fields, secret.FieldTarget) } - oldValue, err := m.oldValue(ctx) - if err != nil { - return v, fmt.Errorf("querying old value for OldUpdated: %w", err) + if m.FieldCleared(secret.FieldDescription) { + fields = append(fields, secret.FieldDescription) } - return oldValue.Updated, nil -} - -// ResetUpdated resets all changes to the "updated" field. -func (m *GroupMutation) ResetUpdated() { - m.updated = nil + if m.FieldCleared(secret.FieldCreatedBy) { + fields = append(fields, secret.FieldCreatedBy) + } + if m.FieldCleared(secret.FieldUpdatedBy) { + fields = append(fields, secret.FieldUpdatedBy) + } + return fields } -// SetCreatedBy sets the "created_by" field. -func (m *GroupMutation) SetCreatedBy(s string) { - m.created_by = &s +// FieldCleared returns a boolean indicating if a field with the given name was +// cleared in this mutation. +func (m *SecretMutation) FieldCleared(name string) bool { + _, ok := m.clearedFields[name] + return ok } -// CreatedBy returns the value of the "created_by" field in the mutation. -func (m *GroupMutation) CreatedBy() (r string, exists bool) { - v := m.created_by - if v == nil { - return +// ClearField clears the value of the field with the given name. It returns an +// error if the field is not defined in the schema. +func (m *SecretMutation) ClearField(name string) error { + switch name { + case secret.FieldSecretRef: + m.ClearSecretRef() + return nil + case secret.FieldTarget: + m.ClearTarget() + return nil + case secret.FieldDescription: + m.ClearDescription() + return nil + case secret.FieldCreatedBy: + m.ClearCreatedBy() + return nil + case secret.FieldUpdatedBy: + m.ClearUpdatedBy() + return nil } - return *v, true + return fmt.Errorf("unknown Secret nullable field %s", name) } -// OldCreatedBy returns the old "created_by" field's value of the Group entity. -// If the Group object wasn't provided to the builder, the object is fetched from the database. -// An error is returned if the mutation operation is not UpdateOne, or the database query fails. -func (m *GroupMutation) OldCreatedBy(ctx context.Context) (v string, err error) { - if !m.op.Is(OpUpdateOne) { - return v, errors.New("OldCreatedBy is only allowed on UpdateOne operations") - } - if m.id == nil || m.oldValue == nil { - return v, errors.New("OldCreatedBy requires an ID field in the mutation") - } - oldValue, err := m.oldValue(ctx) - if err != nil { - return v, fmt.Errorf("querying old value for OldCreatedBy: %w", err) +// ResetField resets all changes in the mutation for the field with the given name. +// It returns an error if the field is not defined in the schema. +func (m *SecretMutation) ResetField(name string) error { + switch name { + case secret.FieldKey: + m.ResetKey() + return nil + case secret.FieldEncryptedValue: + m.ResetEncryptedValue() + return nil + case secret.FieldSecretRef: + m.ResetSecretRef() + return nil + case secret.FieldSecretType: + m.ResetSecretType() + return nil + case secret.FieldTarget: + m.ResetTarget() + return nil + case secret.FieldScope: + m.ResetScope() + return nil + case secret.FieldScopeID: + m.ResetScopeID() + return nil + case secret.FieldDescription: + m.ResetDescription() + return nil + case secret.FieldInjectionMode: + m.ResetInjectionMode() + return nil + case secret.FieldAllowProgeny: + m.ResetAllowProgeny() + return nil + case secret.FieldVersion: + m.ResetVersion() + return nil + case secret.FieldCreatedBy: + m.ResetCreatedBy() + return nil + case secret.FieldUpdatedBy: + m.ResetUpdatedBy() + return nil + case secret.FieldCreated: + m.ResetCreated() + return nil + case secret.FieldUpdated: + m.ResetUpdated() + return nil } - return oldValue.CreatedBy, nil -} - -// ClearCreatedBy clears the value of the "created_by" field. -func (m *GroupMutation) ClearCreatedBy() { - m.created_by = nil - m.clearedFields[group.FieldCreatedBy] = struct{}{} + return fmt.Errorf("unknown Secret field %s", name) } -// CreatedByCleared returns if the "created_by" field was cleared in this mutation. -func (m *GroupMutation) CreatedByCleared() bool { - _, ok := m.clearedFields[group.FieldCreatedBy] - return ok +// AddedEdges returns all edge names that were set/added in this mutation. +func (m *SecretMutation) AddedEdges() []string { + edges := make([]string, 0, 0) + return edges } -// ResetCreatedBy resets all changes to the "created_by" field. -func (m *GroupMutation) ResetCreatedBy() { - m.created_by = nil - delete(m.clearedFields, group.FieldCreatedBy) +// AddedIDs returns all IDs (to other nodes) that were added for the given edge +// name in this mutation. +func (m *SecretMutation) AddedIDs(name string) []ent.Value { + return nil } -// SetOwnerID sets the "owner_id" field. -func (m *GroupMutation) SetOwnerID(u uuid.UUID) { - m.owner = &u +// RemovedEdges returns all edge names that were removed in this mutation. +func (m *SecretMutation) RemovedEdges() []string { + edges := make([]string, 0, 0) + return edges } -// OwnerID returns the value of the "owner_id" field in the mutation. -func (m *GroupMutation) OwnerID() (r uuid.UUID, exists bool) { - v := m.owner - if v == nil { - return - } - return *v, true +// RemovedIDs returns all IDs (to other nodes) that were removed for the edge with +// the given name in this mutation. +func (m *SecretMutation) RemovedIDs(name string) []ent.Value { + return nil } -// OldOwnerID returns the old "owner_id" field's value of the Group entity. -// If the Group object wasn't provided to the builder, the object is fetched from the database. -// An error is returned if the mutation operation is not UpdateOne, or the database query fails. -func (m *GroupMutation) OldOwnerID(ctx context.Context) (v *uuid.UUID, err error) { - if !m.op.Is(OpUpdateOne) { - return v, errors.New("OldOwnerID is only allowed on UpdateOne operations") - } - if m.id == nil || m.oldValue == nil { - return v, errors.New("OldOwnerID requires an ID field in the mutation") - } - oldValue, err := m.oldValue(ctx) - if err != nil { - return v, fmt.Errorf("querying old value for OldOwnerID: %w", err) - } - return oldValue.OwnerID, nil +// ClearedEdges returns all edge names that were cleared in this mutation. +func (m *SecretMutation) ClearedEdges() []string { + edges := make([]string, 0, 0) + return edges } -// ClearOwnerID clears the value of the "owner_id" field. -func (m *GroupMutation) ClearOwnerID() { - m.owner = nil - m.clearedFields[group.FieldOwnerID] = struct{}{} +// EdgeCleared returns a boolean which indicates if the edge with the given name +// was cleared in this mutation. +func (m *SecretMutation) EdgeCleared(name string) bool { + return false } -// OwnerIDCleared returns if the "owner_id" field was cleared in this mutation. -func (m *GroupMutation) OwnerIDCleared() bool { - _, ok := m.clearedFields[group.FieldOwnerID] - return ok +// ClearEdge clears the value of the edge with the given name. It returns an error +// if that edge is not defined in the schema. +func (m *SecretMutation) ClearEdge(name string) error { + return fmt.Errorf("unknown Secret unique edge %s", name) } -// ResetOwnerID resets all changes to the "owner_id" field. -func (m *GroupMutation) ResetOwnerID() { - m.owner = nil - delete(m.clearedFields, group.FieldOwnerID) +// ResetEdge resets all changes to the edge with the given name in this mutation. +// It returns an error if the edge is not defined in the schema. +func (m *SecretMutation) ResetEdge(name string) error { + return fmt.Errorf("unknown Secret edge %s", name) } -// AddMembershipIDs adds the "memberships" edge to the GroupMembership entity by ids. -func (m *GroupMutation) AddMembershipIDs(ids ...uuid.UUID) { - if m.memberships == nil { - m.memberships = make(map[uuid.UUID]struct{}) +// SubscriptionTemplateMutation represents an operation that mutates the SubscriptionTemplate nodes in the graph. +type SubscriptionTemplateMutation struct { + config + op Op + typ string + id *uuid.UUID + name *string + scope *string + trigger_activities *string + project_id *uuid.UUID + created_by *string + clearedFields map[string]struct{} + done bool + oldValue func(context.Context) (*SubscriptionTemplate, error) + predicates []predicate.SubscriptionTemplate +} + +var _ ent.Mutation = (*SubscriptionTemplateMutation)(nil) + +// subscriptiontemplateOption allows management of the mutation configuration using functional options. +type subscriptiontemplateOption func(*SubscriptionTemplateMutation) + +// newSubscriptionTemplateMutation creates new mutation for the SubscriptionTemplate entity. +func newSubscriptionTemplateMutation(c config, op Op, opts ...subscriptiontemplateOption) *SubscriptionTemplateMutation { + m := &SubscriptionTemplateMutation{ + config: c, + op: op, + typ: TypeSubscriptionTemplate, + clearedFields: make(map[string]struct{}), } - for i := range ids { - m.memberships[ids[i]] = struct{}{} + for _, opt := range opts { + opt(m) } + return m } -// ClearMemberships clears the "memberships" edge to the GroupMembership entity. -func (m *GroupMutation) ClearMemberships() { - m.clearedmemberships = true -} - -// MembershipsCleared reports if the "memberships" edge to the GroupMembership entity was cleared. -func (m *GroupMutation) MembershipsCleared() bool { - return m.clearedmemberships +// withSubscriptionTemplateID sets the ID field of the mutation. +func withSubscriptionTemplateID(id uuid.UUID) subscriptiontemplateOption { + return func(m *SubscriptionTemplateMutation) { + var ( + err error + once sync.Once + value *SubscriptionTemplate + ) + m.oldValue = func(ctx context.Context) (*SubscriptionTemplate, error) { + once.Do(func() { + if m.done { + err = errors.New("querying old values post mutation is not allowed") + } else { + value, err = m.Client().SubscriptionTemplate.Get(ctx, id) + } + }) + return value, err + } + m.id = &id + } } -// RemoveMembershipIDs removes the "memberships" edge to the GroupMembership entity by IDs. -func (m *GroupMutation) RemoveMembershipIDs(ids ...uuid.UUID) { - if m.removedmemberships == nil { - m.removedmemberships = make(map[uuid.UUID]struct{}) - } - for i := range ids { - delete(m.memberships, ids[i]) - m.removedmemberships[ids[i]] = struct{}{} +// withSubscriptionTemplate sets the old SubscriptionTemplate of the mutation. +func withSubscriptionTemplate(node *SubscriptionTemplate) subscriptiontemplateOption { + return func(m *SubscriptionTemplateMutation) { + m.oldValue = func(context.Context) (*SubscriptionTemplate, error) { + return node, nil + } + m.id = &node.ID } } -// RemovedMemberships returns the removed IDs of the "memberships" edge to the GroupMembership entity. -func (m *GroupMutation) RemovedMembershipsIDs() (ids []uuid.UUID) { - for id := range m.removedmemberships { - ids = append(ids, id) - } - return +// Client returns a new `ent.Client` from the mutation. If the mutation was +// executed in a transaction (ent.Tx), a transactional client is returned. +func (m SubscriptionTemplateMutation) Client() *Client { + client := &Client{config: m.config} + client.init() + return client } -// MembershipsIDs returns the "memberships" edge IDs in the mutation. -func (m *GroupMutation) MembershipsIDs() (ids []uuid.UUID) { - for id := range m.memberships { - ids = append(ids, id) +// Tx returns an `ent.Tx` for mutations that were executed in transactions; +// it returns an error otherwise. +func (m SubscriptionTemplateMutation) Tx() (*Tx, error) { + if _, ok := m.driver.(*txDriver); !ok { + return nil, errors.New("ent: mutation is not running in a transaction") } - return + tx := &Tx{config: m.config} + tx.init() + return tx, nil } -// ResetMemberships resets all changes to the "memberships" edge. -func (m *GroupMutation) ResetMemberships() { - m.memberships = nil - m.clearedmemberships = false - m.removedmemberships = nil +// SetID sets the value of the id field. Note that this +// operation is only accepted on creation of SubscriptionTemplate entities. +func (m *SubscriptionTemplateMutation) SetID(id uuid.UUID) { + m.id = &id } -// AddParentGroupIDs adds the "parent_groups" edge to the Group entity by ids. -func (m *GroupMutation) AddParentGroupIDs(ids ...uuid.UUID) { - if m.parent_groups == nil { - m.parent_groups = make(map[uuid.UUID]struct{}) +// ID returns the ID value in the mutation. Note that the ID is only available +// if it was provided to the builder or after it was returned from the database. +func (m *SubscriptionTemplateMutation) ID() (id uuid.UUID, exists bool) { + if m.id == nil { + return } - for i := range ids { - m.parent_groups[ids[i]] = struct{}{} + return *m.id, true +} + +// IDs queries the database and returns the entity ids that match the mutation's predicate. +// That means, if the mutation is applied within a transaction with an isolation level such +// as sql.LevelSerializable, the returned ids match the ids of the rows that will be updated +// or updated by the mutation. +func (m *SubscriptionTemplateMutation) IDs(ctx context.Context) ([]uuid.UUID, error) { + switch { + case m.op.Is(OpUpdateOne | OpDeleteOne): + id, exists := m.ID() + if exists { + return []uuid.UUID{id}, nil + } + fallthrough + case m.op.Is(OpUpdate | OpDelete): + return m.Client().SubscriptionTemplate.Query().Where(m.predicates...).IDs(ctx) + default: + return nil, fmt.Errorf("IDs is not allowed on %s operations", m.op) } } -// ClearParentGroups clears the "parent_groups" edge to the Group entity. -func (m *GroupMutation) ClearParentGroups() { - m.clearedparent_groups = true +// SetName sets the "name" field. +func (m *SubscriptionTemplateMutation) SetName(s string) { + m.name = &s } -// ParentGroupsCleared reports if the "parent_groups" edge to the Group entity was cleared. -func (m *GroupMutation) ParentGroupsCleared() bool { - return m.clearedparent_groups +// Name returns the value of the "name" field in the mutation. +func (m *SubscriptionTemplateMutation) Name() (r string, exists bool) { + v := m.name + if v == nil { + return + } + return *v, true } -// RemoveParentGroupIDs removes the "parent_groups" edge to the Group entity by IDs. -func (m *GroupMutation) RemoveParentGroupIDs(ids ...uuid.UUID) { - if m.removedparent_groups == nil { - m.removedparent_groups = make(map[uuid.UUID]struct{}) +// OldName returns the old "name" field's value of the SubscriptionTemplate entity. +// If the SubscriptionTemplate object wasn't provided to the builder, the object is fetched from the database. +// An error is returned if the mutation operation is not UpdateOne, or the database query fails. +func (m *SubscriptionTemplateMutation) OldName(ctx context.Context) (v string, err error) { + if !m.op.Is(OpUpdateOne) { + return v, errors.New("OldName is only allowed on UpdateOne operations") } - for i := range ids { - delete(m.parent_groups, ids[i]) - m.removedparent_groups[ids[i]] = struct{}{} + if m.id == nil || m.oldValue == nil { + return v, errors.New("OldName requires an ID field in the mutation") + } + oldValue, err := m.oldValue(ctx) + if err != nil { + return v, fmt.Errorf("querying old value for OldName: %w", err) } + return oldValue.Name, nil } -// RemovedParentGroups returns the removed IDs of the "parent_groups" edge to the Group entity. -func (m *GroupMutation) RemovedParentGroupsIDs() (ids []uuid.UUID) { - for id := range m.removedparent_groups { - ids = append(ids, id) - } - return +// ResetName resets all changes to the "name" field. +func (m *SubscriptionTemplateMutation) ResetName() { + m.name = nil } -// ParentGroupsIDs returns the "parent_groups" edge IDs in the mutation. -func (m *GroupMutation) ParentGroupsIDs() (ids []uuid.UUID) { - for id := range m.parent_groups { - ids = append(ids, id) - } - return +// SetScope sets the "scope" field. +func (m *SubscriptionTemplateMutation) SetScope(s string) { + m.scope = &s } -// ResetParentGroups resets all changes to the "parent_groups" edge. -func (m *GroupMutation) ResetParentGroups() { - m.parent_groups = nil - m.clearedparent_groups = false - m.removedparent_groups = nil +// Scope returns the value of the "scope" field in the mutation. +func (m *SubscriptionTemplateMutation) Scope() (r string, exists bool) { + v := m.scope + if v == nil { + return + } + return *v, true } -// AddChildGroupIDs adds the "child_groups" edge to the Group entity by ids. -func (m *GroupMutation) AddChildGroupIDs(ids ...uuid.UUID) { - if m.child_groups == nil { - m.child_groups = make(map[uuid.UUID]struct{}) +// OldScope returns the old "scope" field's value of the SubscriptionTemplate entity. +// If the SubscriptionTemplate object wasn't provided to the builder, the object is fetched from the database. +// An error is returned if the mutation operation is not UpdateOne, or the database query fails. +func (m *SubscriptionTemplateMutation) OldScope(ctx context.Context) (v string, err error) { + if !m.op.Is(OpUpdateOne) { + return v, errors.New("OldScope is only allowed on UpdateOne operations") } - for i := range ids { - m.child_groups[ids[i]] = struct{}{} + if m.id == nil || m.oldValue == nil { + return v, errors.New("OldScope requires an ID field in the mutation") + } + oldValue, err := m.oldValue(ctx) + if err != nil { + return v, fmt.Errorf("querying old value for OldScope: %w", err) } + return oldValue.Scope, nil } -// ClearChildGroups clears the "child_groups" edge to the Group entity. -func (m *GroupMutation) ClearChildGroups() { - m.clearedchild_groups = true +// ResetScope resets all changes to the "scope" field. +func (m *SubscriptionTemplateMutation) ResetScope() { + m.scope = nil } -// ChildGroupsCleared reports if the "child_groups" edge to the Group entity was cleared. -func (m *GroupMutation) ChildGroupsCleared() bool { - return m.clearedchild_groups +// SetTriggerActivities sets the "trigger_activities" field. +func (m *SubscriptionTemplateMutation) SetTriggerActivities(s string) { + m.trigger_activities = &s } -// RemoveChildGroupIDs removes the "child_groups" edge to the Group entity by IDs. -func (m *GroupMutation) RemoveChildGroupIDs(ids ...uuid.UUID) { - if m.removedchild_groups == nil { - m.removedchild_groups = make(map[uuid.UUID]struct{}) - } - for i := range ids { - delete(m.child_groups, ids[i]) - m.removedchild_groups[ids[i]] = struct{}{} +// TriggerActivities returns the value of the "trigger_activities" field in the mutation. +func (m *SubscriptionTemplateMutation) TriggerActivities() (r string, exists bool) { + v := m.trigger_activities + if v == nil { + return } + return *v, true } -// RemovedChildGroups returns the removed IDs of the "child_groups" edge to the Group entity. -func (m *GroupMutation) RemovedChildGroupsIDs() (ids []uuid.UUID) { - for id := range m.removedchild_groups { - ids = append(ids, id) +// OldTriggerActivities returns the old "trigger_activities" field's value of the SubscriptionTemplate entity. +// If the SubscriptionTemplate object wasn't provided to the builder, the object is fetched from the database. +// An error is returned if the mutation operation is not UpdateOne, or the database query fails. +func (m *SubscriptionTemplateMutation) OldTriggerActivities(ctx context.Context) (v string, err error) { + if !m.op.Is(OpUpdateOne) { + return v, errors.New("OldTriggerActivities is only allowed on UpdateOne operations") } - return -} - -// ChildGroupsIDs returns the "child_groups" edge IDs in the mutation. -func (m *GroupMutation) ChildGroupsIDs() (ids []uuid.UUID) { - for id := range m.child_groups { - ids = append(ids, id) + if m.id == nil || m.oldValue == nil { + return v, errors.New("OldTriggerActivities requires an ID field in the mutation") } - return -} - -// ResetChildGroups resets all changes to the "child_groups" edge. -func (m *GroupMutation) ResetChildGroups() { - m.child_groups = nil - m.clearedchild_groups = false - m.removedchild_groups = nil + oldValue, err := m.oldValue(ctx) + if err != nil { + return v, fmt.Errorf("querying old value for OldTriggerActivities: %w", err) + } + return oldValue.TriggerActivities, nil } -// ClearOwner clears the "owner" edge to the User entity. -func (m *GroupMutation) ClearOwner() { - m.clearedowner = true - m.clearedFields[group.FieldOwnerID] = struct{}{} +// ResetTriggerActivities resets all changes to the "trigger_activities" field. +func (m *SubscriptionTemplateMutation) ResetTriggerActivities() { + m.trigger_activities = nil } -// OwnerCleared reports if the "owner" edge to the User entity was cleared. -func (m *GroupMutation) OwnerCleared() bool { - return m.OwnerIDCleared() || m.clearedowner +// SetProjectID sets the "project_id" field. +func (m *SubscriptionTemplateMutation) SetProjectID(u uuid.UUID) { + m.project_id = &u } -// OwnerIDs returns the "owner" edge IDs in the mutation. -// Note that IDs always returns len(IDs) <= 1 for unique edges, and you should use -// OwnerID instead. It exists only for internal usage by the builders. -func (m *GroupMutation) OwnerIDs() (ids []uuid.UUID) { - if id := m.owner; id != nil { - ids = append(ids, *id) +// ProjectID returns the value of the "project_id" field in the mutation. +func (m *SubscriptionTemplateMutation) ProjectID() (r uuid.UUID, exists bool) { + v := m.project_id + if v == nil { + return } - return -} - -// ResetOwner resets all changes to the "owner" edge. -func (m *GroupMutation) ResetOwner() { - m.owner = nil - m.clearedowner = false + return *v, true } -// AddPolicyBindingIDs adds the "policy_bindings" edge to the PolicyBinding entity by ids. -func (m *GroupMutation) AddPolicyBindingIDs(ids ...uuid.UUID) { - if m.policy_bindings == nil { - m.policy_bindings = make(map[uuid.UUID]struct{}) +// OldProjectID returns the old "project_id" field's value of the SubscriptionTemplate entity. +// If the SubscriptionTemplate object wasn't provided to the builder, the object is fetched from the database. +// An error is returned if the mutation operation is not UpdateOne, or the database query fails. +func (m *SubscriptionTemplateMutation) OldProjectID(ctx context.Context) (v *uuid.UUID, err error) { + if !m.op.Is(OpUpdateOne) { + return v, errors.New("OldProjectID is only allowed on UpdateOne operations") } - for i := range ids { - m.policy_bindings[ids[i]] = struct{}{} + if m.id == nil || m.oldValue == nil { + return v, errors.New("OldProjectID requires an ID field in the mutation") + } + oldValue, err := m.oldValue(ctx) + if err != nil { + return v, fmt.Errorf("querying old value for OldProjectID: %w", err) } + return oldValue.ProjectID, nil } -// ClearPolicyBindings clears the "policy_bindings" edge to the PolicyBinding entity. -func (m *GroupMutation) ClearPolicyBindings() { - m.clearedpolicy_bindings = true +// ClearProjectID clears the value of the "project_id" field. +func (m *SubscriptionTemplateMutation) ClearProjectID() { + m.project_id = nil + m.clearedFields[subscriptiontemplate.FieldProjectID] = struct{}{} } -// PolicyBindingsCleared reports if the "policy_bindings" edge to the PolicyBinding entity was cleared. -func (m *GroupMutation) PolicyBindingsCleared() bool { - return m.clearedpolicy_bindings +// ProjectIDCleared returns if the "project_id" field was cleared in this mutation. +func (m *SubscriptionTemplateMutation) ProjectIDCleared() bool { + _, ok := m.clearedFields[subscriptiontemplate.FieldProjectID] + return ok } -// RemovePolicyBindingIDs removes the "policy_bindings" edge to the PolicyBinding entity by IDs. -func (m *GroupMutation) RemovePolicyBindingIDs(ids ...uuid.UUID) { - if m.removedpolicy_bindings == nil { - m.removedpolicy_bindings = make(map[uuid.UUID]struct{}) - } - for i := range ids { - delete(m.policy_bindings, ids[i]) - m.removedpolicy_bindings[ids[i]] = struct{}{} - } +// ResetProjectID resets all changes to the "project_id" field. +func (m *SubscriptionTemplateMutation) ResetProjectID() { + m.project_id = nil + delete(m.clearedFields, subscriptiontemplate.FieldProjectID) } -// RemovedPolicyBindings returns the removed IDs of the "policy_bindings" edge to the PolicyBinding entity. -func (m *GroupMutation) RemovedPolicyBindingsIDs() (ids []uuid.UUID) { - for id := range m.removedpolicy_bindings { - ids = append(ids, id) - } - return +// SetCreatedBy sets the "created_by" field. +func (m *SubscriptionTemplateMutation) SetCreatedBy(s string) { + m.created_by = &s } -// PolicyBindingsIDs returns the "policy_bindings" edge IDs in the mutation. -func (m *GroupMutation) PolicyBindingsIDs() (ids []uuid.UUID) { - for id := range m.policy_bindings { - ids = append(ids, id) +// CreatedBy returns the value of the "created_by" field in the mutation. +func (m *SubscriptionTemplateMutation) CreatedBy() (r string, exists bool) { + v := m.created_by + if v == nil { + return } - return + return *v, true } -// ResetPolicyBindings resets all changes to the "policy_bindings" edge. -func (m *GroupMutation) ResetPolicyBindings() { - m.policy_bindings = nil - m.clearedpolicy_bindings = false - m.removedpolicy_bindings = nil +// OldCreatedBy returns the old "created_by" field's value of the SubscriptionTemplate entity. +// If the SubscriptionTemplate object wasn't provided to the builder, the object is fetched from the database. +// An error is returned if the mutation operation is not UpdateOne, or the database query fails. +func (m *SubscriptionTemplateMutation) OldCreatedBy(ctx context.Context) (v string, err error) { + if !m.op.Is(OpUpdateOne) { + return v, errors.New("OldCreatedBy is only allowed on UpdateOne operations") + } + if m.id == nil || m.oldValue == nil { + return v, errors.New("OldCreatedBy requires an ID field in the mutation") + } + oldValue, err := m.oldValue(ctx) + if err != nil { + return v, fmt.Errorf("querying old value for OldCreatedBy: %w", err) + } + return oldValue.CreatedBy, nil } -// Where appends a list predicates to the GroupMutation builder. -func (m *GroupMutation) Where(ps ...predicate.Group) { +// ResetCreatedBy resets all changes to the "created_by" field. +func (m *SubscriptionTemplateMutation) ResetCreatedBy() { + m.created_by = nil +} + +// Where appends a list predicates to the SubscriptionTemplateMutation builder. +func (m *SubscriptionTemplateMutation) Where(ps ...predicate.SubscriptionTemplate) { m.predicates = append(m.predicates, ps...) } -// WhereP appends storage-level predicates to the GroupMutation builder. Using this method, +// WhereP appends storage-level predicates to the SubscriptionTemplateMutation builder. Using this method, // users can use type-assertion to append predicates that do not depend on any generated package. -func (m *GroupMutation) WhereP(ps ...func(*sql.Selector)) { - p := make([]predicate.Group, len(ps)) +func (m *SubscriptionTemplateMutation) WhereP(ps ...func(*sql.Selector)) { + p := make([]predicate.SubscriptionTemplate, len(ps)) for i := range ps { p[i] = ps[i] } @@ -3562,57 +29425,39 @@ func (m *GroupMutation) WhereP(ps ...func(*sql.Selector)) { } // Op returns the operation name. -func (m *GroupMutation) Op() Op { +func (m *SubscriptionTemplateMutation) Op() Op { return m.op } // SetOp allows setting the mutation operation. -func (m *GroupMutation) SetOp(op Op) { +func (m *SubscriptionTemplateMutation) SetOp(op Op) { m.op = op } -// Type returns the node type of this mutation (Group). -func (m *GroupMutation) Type() string { +// Type returns the node type of this mutation (SubscriptionTemplate). +func (m *SubscriptionTemplateMutation) Type() string { return m.typ } // Fields returns all fields that were changed during this mutation. Note that in // order to get all numeric fields that were incremented/decremented, call // AddedFields(). -func (m *GroupMutation) Fields() []string { - fields := make([]string, 0, 11) +func (m *SubscriptionTemplateMutation) Fields() []string { + fields := make([]string, 0, 5) if m.name != nil { - fields = append(fields, group.FieldName) - } - if m.slug != nil { - fields = append(fields, group.FieldSlug) + fields = append(fields, subscriptiontemplate.FieldName) } - if m.description != nil { - fields = append(fields, group.FieldDescription) + if m.scope != nil { + fields = append(fields, subscriptiontemplate.FieldScope) } - if m.group_type != nil { - fields = append(fields, group.FieldGroupType) + if m.trigger_activities != nil { + fields = append(fields, subscriptiontemplate.FieldTriggerActivities) } if m.project_id != nil { - fields = append(fields, group.FieldProjectID) - } - if m.labels != nil { - fields = append(fields, group.FieldLabels) - } - if m.annotations != nil { - fields = append(fields, group.FieldAnnotations) - } - if m.created != nil { - fields = append(fields, group.FieldCreated) - } - if m.updated != nil { - fields = append(fields, group.FieldUpdated) + fields = append(fields, subscriptiontemplate.FieldProjectID) } if m.created_by != nil { - fields = append(fields, group.FieldCreatedBy) - } - if m.owner != nil { - fields = append(fields, group.FieldOwnerID) + fields = append(fields, subscriptiontemplate.FieldCreatedBy) } return fields } @@ -3620,30 +29465,18 @@ func (m *GroupMutation) Fields() []string { // Field returns the value of a field with the given name. The second boolean // return value indicates that this field was not set, or was not defined in the // schema. -func (m *GroupMutation) Field(name string) (ent.Value, bool) { +func (m *SubscriptionTemplateMutation) Field(name string) (ent.Value, bool) { switch name { - case group.FieldName: + case subscriptiontemplate.FieldName: return m.Name() - case group.FieldSlug: - return m.Slug() - case group.FieldDescription: - return m.Description() - case group.FieldGroupType: - return m.GroupType() - case group.FieldProjectID: + case subscriptiontemplate.FieldScope: + return m.Scope() + case subscriptiontemplate.FieldTriggerActivities: + return m.TriggerActivities() + case subscriptiontemplate.FieldProjectID: return m.ProjectID() - case group.FieldLabels: - return m.Labels() - case group.FieldAnnotations: - return m.Annotations() - case group.FieldCreated: - return m.Created() - case group.FieldUpdated: - return m.Updated() - case group.FieldCreatedBy: + case subscriptiontemplate.FieldCreatedBy: return m.CreatedBy() - case group.FieldOwnerID: - return m.OwnerID() } return nil, false } @@ -3651,453 +29484,234 @@ func (m *GroupMutation) Field(name string) (ent.Value, bool) { // OldField returns the old value of the field from the database. An error is // returned if the mutation operation is not UpdateOne, or the query to the // database failed. -func (m *GroupMutation) OldField(ctx context.Context, name string) (ent.Value, error) { +func (m *SubscriptionTemplateMutation) OldField(ctx context.Context, name string) (ent.Value, error) { switch name { - case group.FieldName: + case subscriptiontemplate.FieldName: return m.OldName(ctx) - case group.FieldSlug: - return m.OldSlug(ctx) - case group.FieldDescription: - return m.OldDescription(ctx) - case group.FieldGroupType: - return m.OldGroupType(ctx) - case group.FieldProjectID: + case subscriptiontemplate.FieldScope: + return m.OldScope(ctx) + case subscriptiontemplate.FieldTriggerActivities: + return m.OldTriggerActivities(ctx) + case subscriptiontemplate.FieldProjectID: return m.OldProjectID(ctx) - case group.FieldLabels: - return m.OldLabels(ctx) - case group.FieldAnnotations: - return m.OldAnnotations(ctx) - case group.FieldCreated: - return m.OldCreated(ctx) - case group.FieldUpdated: - return m.OldUpdated(ctx) - case group.FieldCreatedBy: + case subscriptiontemplate.FieldCreatedBy: return m.OldCreatedBy(ctx) - case group.FieldOwnerID: - return m.OldOwnerID(ctx) } - return nil, fmt.Errorf("unknown Group field %s", name) + return nil, fmt.Errorf("unknown SubscriptionTemplate field %s", name) } // SetField sets the value of a field with the given name. It returns an error if // the field is not defined in the schema, or if the type mismatched the field // type. -func (m *GroupMutation) SetField(name string, value ent.Value) error { +func (m *SubscriptionTemplateMutation) SetField(name string, value ent.Value) error { switch name { - case group.FieldName: + case subscriptiontemplate.FieldName: v, ok := value.(string) if !ok { return fmt.Errorf("unexpected type %T for field %s", value, name) } m.SetName(v) return nil - case group.FieldSlug: + case subscriptiontemplate.FieldScope: v, ok := value.(string) if !ok { return fmt.Errorf("unexpected type %T for field %s", value, name) } - m.SetSlug(v) + m.SetScope(v) return nil - case group.FieldDescription: + case subscriptiontemplate.FieldTriggerActivities: v, ok := value.(string) if !ok { return fmt.Errorf("unexpected type %T for field %s", value, name) } - m.SetDescription(v) - return nil - case group.FieldGroupType: - v, ok := value.(group.GroupType) - if !ok { - return fmt.Errorf("unexpected type %T for field %s", value, name) - } - m.SetGroupType(v) + m.SetTriggerActivities(v) return nil - case group.FieldProjectID: + case subscriptiontemplate.FieldProjectID: v, ok := value.(uuid.UUID) if !ok { return fmt.Errorf("unexpected type %T for field %s", value, name) } m.SetProjectID(v) return nil - case group.FieldLabels: - v, ok := value.(map[string]string) - if !ok { - return fmt.Errorf("unexpected type %T for field %s", value, name) - } - m.SetLabels(v) - return nil - case group.FieldAnnotations: - v, ok := value.(map[string]string) - if !ok { - return fmt.Errorf("unexpected type %T for field %s", value, name) - } - m.SetAnnotations(v) - return nil - case group.FieldCreated: - v, ok := value.(time.Time) - if !ok { - return fmt.Errorf("unexpected type %T for field %s", value, name) - } - m.SetCreated(v) - return nil - case group.FieldUpdated: - v, ok := value.(time.Time) - if !ok { - return fmt.Errorf("unexpected type %T for field %s", value, name) - } - m.SetUpdated(v) - return nil - case group.FieldCreatedBy: + case subscriptiontemplate.FieldCreatedBy: v, ok := value.(string) if !ok { return fmt.Errorf("unexpected type %T for field %s", value, name) } m.SetCreatedBy(v) return nil - case group.FieldOwnerID: - v, ok := value.(uuid.UUID) - if !ok { - return fmt.Errorf("unexpected type %T for field %s", value, name) - } - m.SetOwnerID(v) - return nil } - return fmt.Errorf("unknown Group field %s", name) + return fmt.Errorf("unknown SubscriptionTemplate field %s", name) } // AddedFields returns all numeric fields that were incremented/decremented during // this mutation. -func (m *GroupMutation) AddedFields() []string { +func (m *SubscriptionTemplateMutation) AddedFields() []string { return nil } // AddedField returns the numeric value that was incremented/decremented on a field // with the given name. The second boolean return value indicates that this field // was not set, or was not defined in the schema. -func (m *GroupMutation) AddedField(name string) (ent.Value, bool) { +func (m *SubscriptionTemplateMutation) AddedField(name string) (ent.Value, bool) { return nil, false } // AddField adds the value to the field with the given name. It returns an error if // the field is not defined in the schema, or if the type mismatched the field // type. -func (m *GroupMutation) AddField(name string, value ent.Value) error { +func (m *SubscriptionTemplateMutation) AddField(name string, value ent.Value) error { switch name { } - return fmt.Errorf("unknown Group numeric field %s", name) + return fmt.Errorf("unknown SubscriptionTemplate numeric field %s", name) } // ClearedFields returns all nullable fields that were cleared during this // mutation. -func (m *GroupMutation) ClearedFields() []string { +func (m *SubscriptionTemplateMutation) ClearedFields() []string { var fields []string - if m.FieldCleared(group.FieldDescription) { - fields = append(fields, group.FieldDescription) - } - if m.FieldCleared(group.FieldProjectID) { - fields = append(fields, group.FieldProjectID) - } - if m.FieldCleared(group.FieldLabels) { - fields = append(fields, group.FieldLabels) - } - if m.FieldCleared(group.FieldAnnotations) { - fields = append(fields, group.FieldAnnotations) - } - if m.FieldCleared(group.FieldCreatedBy) { - fields = append(fields, group.FieldCreatedBy) - } - if m.FieldCleared(group.FieldOwnerID) { - fields = append(fields, group.FieldOwnerID) + if m.FieldCleared(subscriptiontemplate.FieldProjectID) { + fields = append(fields, subscriptiontemplate.FieldProjectID) } return fields } // FieldCleared returns a boolean indicating if a field with the given name was // cleared in this mutation. -func (m *GroupMutation) FieldCleared(name string) bool { +func (m *SubscriptionTemplateMutation) FieldCleared(name string) bool { _, ok := m.clearedFields[name] return ok } // ClearField clears the value of the field with the given name. It returns an // error if the field is not defined in the schema. -func (m *GroupMutation) ClearField(name string) error { +func (m *SubscriptionTemplateMutation) ClearField(name string) error { switch name { - case group.FieldDescription: - m.ClearDescription() - return nil - case group.FieldProjectID: + case subscriptiontemplate.FieldProjectID: m.ClearProjectID() return nil - case group.FieldLabels: - m.ClearLabels() - return nil - case group.FieldAnnotations: - m.ClearAnnotations() - return nil - case group.FieldCreatedBy: - m.ClearCreatedBy() - return nil - case group.FieldOwnerID: - m.ClearOwnerID() - return nil } - return fmt.Errorf("unknown Group nullable field %s", name) + return fmt.Errorf("unknown SubscriptionTemplate nullable field %s", name) } // ResetField resets all changes in the mutation for the field with the given name. // It returns an error if the field is not defined in the schema. -func (m *GroupMutation) ResetField(name string) error { +func (m *SubscriptionTemplateMutation) ResetField(name string) error { switch name { - case group.FieldName: + case subscriptiontemplate.FieldName: m.ResetName() return nil - case group.FieldSlug: - m.ResetSlug() - return nil - case group.FieldDescription: - m.ResetDescription() + case subscriptiontemplate.FieldScope: + m.ResetScope() return nil - case group.FieldGroupType: - m.ResetGroupType() + case subscriptiontemplate.FieldTriggerActivities: + m.ResetTriggerActivities() return nil - case group.FieldProjectID: + case subscriptiontemplate.FieldProjectID: m.ResetProjectID() return nil - case group.FieldLabels: - m.ResetLabels() - return nil - case group.FieldAnnotations: - m.ResetAnnotations() - return nil - case group.FieldCreated: - m.ResetCreated() - return nil - case group.FieldUpdated: - m.ResetUpdated() - return nil - case group.FieldCreatedBy: + case subscriptiontemplate.FieldCreatedBy: m.ResetCreatedBy() return nil - case group.FieldOwnerID: - m.ResetOwnerID() - return nil - } - return fmt.Errorf("unknown Group field %s", name) -} - -// AddedEdges returns all edge names that were set/added in this mutation. -func (m *GroupMutation) AddedEdges() []string { - edges := make([]string, 0, 5) - if m.memberships != nil { - edges = append(edges, group.EdgeMemberships) - } - if m.parent_groups != nil { - edges = append(edges, group.EdgeParentGroups) - } - if m.child_groups != nil { - edges = append(edges, group.EdgeChildGroups) } - if m.owner != nil { - edges = append(edges, group.EdgeOwner) - } - if m.policy_bindings != nil { - edges = append(edges, group.EdgePolicyBindings) - } - return edges + return fmt.Errorf("unknown SubscriptionTemplate field %s", name) } -// AddedIDs returns all IDs (to other nodes) that were added for the given edge -// name in this mutation. -func (m *GroupMutation) AddedIDs(name string) []ent.Value { - switch name { - case group.EdgeMemberships: - ids := make([]ent.Value, 0, len(m.memberships)) - for id := range m.memberships { - ids = append(ids, id) - } - return ids - case group.EdgeParentGroups: - ids := make([]ent.Value, 0, len(m.parent_groups)) - for id := range m.parent_groups { - ids = append(ids, id) - } - return ids - case group.EdgeChildGroups: - ids := make([]ent.Value, 0, len(m.child_groups)) - for id := range m.child_groups { - ids = append(ids, id) - } - return ids - case group.EdgeOwner: - if id := m.owner; id != nil { - return []ent.Value{*id} - } - case group.EdgePolicyBindings: - ids := make([]ent.Value, 0, len(m.policy_bindings)) - for id := range m.policy_bindings { - ids = append(ids, id) - } - return ids - } +// AddedEdges returns all edge names that were set/added in this mutation. +func (m *SubscriptionTemplateMutation) AddedEdges() []string { + edges := make([]string, 0, 0) + return edges +} + +// AddedIDs returns all IDs (to other nodes) that were added for the given edge +// name in this mutation. +func (m *SubscriptionTemplateMutation) AddedIDs(name string) []ent.Value { return nil } // RemovedEdges returns all edge names that were removed in this mutation. -func (m *GroupMutation) RemovedEdges() []string { - edges := make([]string, 0, 5) - if m.removedmemberships != nil { - edges = append(edges, group.EdgeMemberships) - } - if m.removedparent_groups != nil { - edges = append(edges, group.EdgeParentGroups) - } - if m.removedchild_groups != nil { - edges = append(edges, group.EdgeChildGroups) - } - if m.removedpolicy_bindings != nil { - edges = append(edges, group.EdgePolicyBindings) - } +func (m *SubscriptionTemplateMutation) RemovedEdges() []string { + edges := make([]string, 0, 0) return edges } // RemovedIDs returns all IDs (to other nodes) that were removed for the edge with // the given name in this mutation. -func (m *GroupMutation) RemovedIDs(name string) []ent.Value { - switch name { - case group.EdgeMemberships: - ids := make([]ent.Value, 0, len(m.removedmemberships)) - for id := range m.removedmemberships { - ids = append(ids, id) - } - return ids - case group.EdgeParentGroups: - ids := make([]ent.Value, 0, len(m.removedparent_groups)) - for id := range m.removedparent_groups { - ids = append(ids, id) - } - return ids - case group.EdgeChildGroups: - ids := make([]ent.Value, 0, len(m.removedchild_groups)) - for id := range m.removedchild_groups { - ids = append(ids, id) - } - return ids - case group.EdgePolicyBindings: - ids := make([]ent.Value, 0, len(m.removedpolicy_bindings)) - for id := range m.removedpolicy_bindings { - ids = append(ids, id) - } - return ids - } +func (m *SubscriptionTemplateMutation) RemovedIDs(name string) []ent.Value { return nil } // ClearedEdges returns all edge names that were cleared in this mutation. -func (m *GroupMutation) ClearedEdges() []string { - edges := make([]string, 0, 5) - if m.clearedmemberships { - edges = append(edges, group.EdgeMemberships) - } - if m.clearedparent_groups { - edges = append(edges, group.EdgeParentGroups) - } - if m.clearedchild_groups { - edges = append(edges, group.EdgeChildGroups) - } - if m.clearedowner { - edges = append(edges, group.EdgeOwner) - } - if m.clearedpolicy_bindings { - edges = append(edges, group.EdgePolicyBindings) - } +func (m *SubscriptionTemplateMutation) ClearedEdges() []string { + edges := make([]string, 0, 0) return edges } // EdgeCleared returns a boolean which indicates if the edge with the given name // was cleared in this mutation. -func (m *GroupMutation) EdgeCleared(name string) bool { - switch name { - case group.EdgeMemberships: - return m.clearedmemberships - case group.EdgeParentGroups: - return m.clearedparent_groups - case group.EdgeChildGroups: - return m.clearedchild_groups - case group.EdgeOwner: - return m.clearedowner - case group.EdgePolicyBindings: - return m.clearedpolicy_bindings - } +func (m *SubscriptionTemplateMutation) EdgeCleared(name string) bool { return false } // ClearEdge clears the value of the edge with the given name. It returns an error // if that edge is not defined in the schema. -func (m *GroupMutation) ClearEdge(name string) error { - switch name { - case group.EdgeOwner: - m.ClearOwner() - return nil - } - return fmt.Errorf("unknown Group unique edge %s", name) +func (m *SubscriptionTemplateMutation) ClearEdge(name string) error { + return fmt.Errorf("unknown SubscriptionTemplate unique edge %s", name) } // ResetEdge resets all changes to the edge with the given name in this mutation. // It returns an error if the edge is not defined in the schema. -func (m *GroupMutation) ResetEdge(name string) error { - switch name { - case group.EdgeMemberships: - m.ResetMemberships() - return nil - case group.EdgeParentGroups: - m.ResetParentGroups() - return nil - case group.EdgeChildGroups: - m.ResetChildGroups() - return nil - case group.EdgeOwner: - m.ResetOwner() - return nil - case group.EdgePolicyBindings: - m.ResetPolicyBindings() - return nil - } - return fmt.Errorf("unknown Group edge %s", name) +func (m *SubscriptionTemplateMutation) ResetEdge(name string) error { + return fmt.Errorf("unknown SubscriptionTemplate edge %s", name) } -// GroupMembershipMutation represents an operation that mutates the GroupMembership nodes in the graph. -type GroupMembershipMutation struct { +// TemplateMutation represents an operation that mutates the Template nodes in the graph. +type TemplateMutation struct { config - op Op - typ string - id *uuid.UUID - role *groupmembership.Role - added_by *string - added_at *time.Time - clearedFields map[string]struct{} - group *uuid.UUID - clearedgroup bool - user *uuid.UUID - cleareduser bool - agent *uuid.UUID - clearedagent bool - done bool - oldValue func(context.Context) (*GroupMembership, error) - predicates []predicate.GroupMembership + op Op + typ string + id *uuid.UUID + name *string + slug *string + display_name *string + description *string + harness *string + default_harness_config *string + image *string + _config *string + content_hash *string + scope *string + scope_id *string + project_id *string + storage_uri *string + storage_bucket *string + storage_path *string + files *string + base_template *string + status *template.Status + owner_id *string + created_by *string + updated_by *string + visibility *string + created *time.Time + updated *time.Time + clearedFields map[string]struct{} + done bool + oldValue func(context.Context) (*Template, error) + predicates []predicate.Template } -var _ ent.Mutation = (*GroupMembershipMutation)(nil) +var _ ent.Mutation = (*TemplateMutation)(nil) -// groupmembershipOption allows management of the mutation configuration using functional options. -type groupmembershipOption func(*GroupMembershipMutation) +// templateOption allows management of the mutation configuration using functional options. +type templateOption func(*TemplateMutation) -// newGroupMembershipMutation creates new mutation for the GroupMembership entity. -func newGroupMembershipMutation(c config, op Op, opts ...groupmembershipOption) *GroupMembershipMutation { - m := &GroupMembershipMutation{ +// newTemplateMutation creates new mutation for the Template entity. +func newTemplateMutation(c config, op Op, opts ...templateOption) *TemplateMutation { + m := &TemplateMutation{ config: c, op: op, - typ: TypeGroupMembership, + typ: TypeTemplate, clearedFields: make(map[string]struct{}), } for _, opt := range opts { @@ -4106,20 +29720,20 @@ func newGroupMembershipMutation(c config, op Op, opts ...groupmembershipOption) return m } -// withGroupMembershipID sets the ID field of the mutation. -func withGroupMembershipID(id uuid.UUID) groupmembershipOption { - return func(m *GroupMembershipMutation) { +// withTemplateID sets the ID field of the mutation. +func withTemplateID(id uuid.UUID) templateOption { + return func(m *TemplateMutation) { var ( err error once sync.Once - value *GroupMembership + value *Template ) - m.oldValue = func(ctx context.Context) (*GroupMembership, error) { + m.oldValue = func(ctx context.Context) (*Template, error) { once.Do(func() { if m.done { err = errors.New("querying old values post mutation is not allowed") } else { - value, err = m.Client().GroupMembership.Get(ctx, id) + value, err = m.Client().Template.Get(ctx, id) } }) return value, err @@ -4128,10 +29742,10 @@ func withGroupMembershipID(id uuid.UUID) groupmembershipOption { } } -// withGroupMembership sets the old GroupMembership of the mutation. -func withGroupMembership(node *GroupMembership) groupmembershipOption { - return func(m *GroupMembershipMutation) { - m.oldValue = func(context.Context) (*GroupMembership, error) { +// withTemplate sets the old Template of the mutation. +func withTemplate(node *Template) templateOption { + return func(m *TemplateMutation) { + m.oldValue = func(context.Context) (*Template, error) { return node, nil } m.id = &node.ID @@ -4140,7 +29754,7 @@ func withGroupMembership(node *GroupMembership) groupmembershipOption { // Client returns a new `ent.Client` from the mutation. If the mutation was // executed in a transaction (ent.Tx), a transactional client is returned. -func (m GroupMembershipMutation) Client() *Client { +func (m TemplateMutation) Client() *Client { client := &Client{config: m.config} client.init() return client @@ -4148,1293 +29762,1130 @@ func (m GroupMembershipMutation) Client() *Client { // Tx returns an `ent.Tx` for mutations that were executed in transactions; // it returns an error otherwise. -func (m GroupMembershipMutation) Tx() (*Tx, error) { +func (m TemplateMutation) Tx() (*Tx, error) { if _, ok := m.driver.(*txDriver); !ok { return nil, errors.New("ent: mutation is not running in a transaction") } - tx := &Tx{config: m.config} - tx.init() - return tx, nil -} - -// SetID sets the value of the id field. Note that this -// operation is only accepted on creation of GroupMembership entities. -func (m *GroupMembershipMutation) SetID(id uuid.UUID) { - m.id = &id -} - -// ID returns the ID value in the mutation. Note that the ID is only available -// if it was provided to the builder or after it was returned from the database. -func (m *GroupMembershipMutation) ID() (id uuid.UUID, exists bool) { - if m.id == nil { - return - } - return *m.id, true -} - -// IDs queries the database and returns the entity ids that match the mutation's predicate. -// That means, if the mutation is applied within a transaction with an isolation level such -// as sql.LevelSerializable, the returned ids match the ids of the rows that will be updated -// or updated by the mutation. -func (m *GroupMembershipMutation) IDs(ctx context.Context) ([]uuid.UUID, error) { - switch { - case m.op.Is(OpUpdateOne | OpDeleteOne): - id, exists := m.ID() - if exists { - return []uuid.UUID{id}, nil - } - fallthrough - case m.op.Is(OpUpdate | OpDelete): - return m.Client().GroupMembership.Query().Where(m.predicates...).IDs(ctx) - default: - return nil, fmt.Errorf("IDs is not allowed on %s operations", m.op) - } -} - -// SetRole sets the "role" field. -func (m *GroupMembershipMutation) SetRole(gr groupmembership.Role) { - m.role = &gr -} - -// Role returns the value of the "role" field in the mutation. -func (m *GroupMembershipMutation) Role() (r groupmembership.Role, exists bool) { - v := m.role - if v == nil { - return - } - return *v, true -} - -// OldRole returns the old "role" field's value of the GroupMembership entity. -// If the GroupMembership object wasn't provided to the builder, the object is fetched from the database. -// An error is returned if the mutation operation is not UpdateOne, or the database query fails. -func (m *GroupMembershipMutation) OldRole(ctx context.Context) (v groupmembership.Role, err error) { - if !m.op.Is(OpUpdateOne) { - return v, errors.New("OldRole is only allowed on UpdateOne operations") - } - if m.id == nil || m.oldValue == nil { - return v, errors.New("OldRole requires an ID field in the mutation") - } - oldValue, err := m.oldValue(ctx) - if err != nil { - return v, fmt.Errorf("querying old value for OldRole: %w", err) - } - return oldValue.Role, nil -} - -// ResetRole resets all changes to the "role" field. -func (m *GroupMembershipMutation) ResetRole() { - m.role = nil -} - -// SetAddedBy sets the "added_by" field. -func (m *GroupMembershipMutation) SetAddedBy(s string) { - m.added_by = &s -} - -// AddedBy returns the value of the "added_by" field in the mutation. -func (m *GroupMembershipMutation) AddedBy() (r string, exists bool) { - v := m.added_by - if v == nil { - return - } - return *v, true -} - -// OldAddedBy returns the old "added_by" field's value of the GroupMembership entity. -// If the GroupMembership object wasn't provided to the builder, the object is fetched from the database. -// An error is returned if the mutation operation is not UpdateOne, or the database query fails. -func (m *GroupMembershipMutation) OldAddedBy(ctx context.Context) (v string, err error) { - if !m.op.Is(OpUpdateOne) { - return v, errors.New("OldAddedBy is only allowed on UpdateOne operations") - } - if m.id == nil || m.oldValue == nil { - return v, errors.New("OldAddedBy requires an ID field in the mutation") - } - oldValue, err := m.oldValue(ctx) - if err != nil { - return v, fmt.Errorf("querying old value for OldAddedBy: %w", err) - } - return oldValue.AddedBy, nil + tx := &Tx{config: m.config} + tx.init() + return tx, nil } -// ClearAddedBy clears the value of the "added_by" field. -func (m *GroupMembershipMutation) ClearAddedBy() { - m.added_by = nil - m.clearedFields[groupmembership.FieldAddedBy] = struct{}{} +// SetID sets the value of the id field. Note that this +// operation is only accepted on creation of Template entities. +func (m *TemplateMutation) SetID(id uuid.UUID) { + m.id = &id } -// AddedByCleared returns if the "added_by" field was cleared in this mutation. -func (m *GroupMembershipMutation) AddedByCleared() bool { - _, ok := m.clearedFields[groupmembership.FieldAddedBy] - return ok +// ID returns the ID value in the mutation. Note that the ID is only available +// if it was provided to the builder or after it was returned from the database. +func (m *TemplateMutation) ID() (id uuid.UUID, exists bool) { + if m.id == nil { + return + } + return *m.id, true } -// ResetAddedBy resets all changes to the "added_by" field. -func (m *GroupMembershipMutation) ResetAddedBy() { - m.added_by = nil - delete(m.clearedFields, groupmembership.FieldAddedBy) +// IDs queries the database and returns the entity ids that match the mutation's predicate. +// That means, if the mutation is applied within a transaction with an isolation level such +// as sql.LevelSerializable, the returned ids match the ids of the rows that will be updated +// or updated by the mutation. +func (m *TemplateMutation) IDs(ctx context.Context) ([]uuid.UUID, error) { + switch { + case m.op.Is(OpUpdateOne | OpDeleteOne): + id, exists := m.ID() + if exists { + return []uuid.UUID{id}, nil + } + fallthrough + case m.op.Is(OpUpdate | OpDelete): + return m.Client().Template.Query().Where(m.predicates...).IDs(ctx) + default: + return nil, fmt.Errorf("IDs is not allowed on %s operations", m.op) + } } -// SetAddedAt sets the "added_at" field. -func (m *GroupMembershipMutation) SetAddedAt(t time.Time) { - m.added_at = &t +// SetName sets the "name" field. +func (m *TemplateMutation) SetName(s string) { + m.name = &s } -// AddedAt returns the value of the "added_at" field in the mutation. -func (m *GroupMembershipMutation) AddedAt() (r time.Time, exists bool) { - v := m.added_at +// Name returns the value of the "name" field in the mutation. +func (m *TemplateMutation) Name() (r string, exists bool) { + v := m.name if v == nil { return } return *v, true } -// OldAddedAt returns the old "added_at" field's value of the GroupMembership entity. -// If the GroupMembership object wasn't provided to the builder, the object is fetched from the database. +// OldName returns the old "name" field's value of the Template entity. +// If the Template object wasn't provided to the builder, the object is fetched from the database. // An error is returned if the mutation operation is not UpdateOne, or the database query fails. -func (m *GroupMembershipMutation) OldAddedAt(ctx context.Context) (v time.Time, err error) { +func (m *TemplateMutation) OldName(ctx context.Context) (v string, err error) { if !m.op.Is(OpUpdateOne) { - return v, errors.New("OldAddedAt is only allowed on UpdateOne operations") + return v, errors.New("OldName is only allowed on UpdateOne operations") } if m.id == nil || m.oldValue == nil { - return v, errors.New("OldAddedAt requires an ID field in the mutation") + return v, errors.New("OldName requires an ID field in the mutation") } oldValue, err := m.oldValue(ctx) if err != nil { - return v, fmt.Errorf("querying old value for OldAddedAt: %w", err) + return v, fmt.Errorf("querying old value for OldName: %w", err) } - return oldValue.AddedAt, nil + return oldValue.Name, nil } -// ResetAddedAt resets all changes to the "added_at" field. -func (m *GroupMembershipMutation) ResetAddedAt() { - m.added_at = nil +// ResetName resets all changes to the "name" field. +func (m *TemplateMutation) ResetName() { + m.name = nil } -// SetGroupID sets the "group_id" field. -func (m *GroupMembershipMutation) SetGroupID(u uuid.UUID) { - m.group = &u +// SetSlug sets the "slug" field. +func (m *TemplateMutation) SetSlug(s string) { + m.slug = &s } -// GroupID returns the value of the "group_id" field in the mutation. -func (m *GroupMembershipMutation) GroupID() (r uuid.UUID, exists bool) { - v := m.group +// Slug returns the value of the "slug" field in the mutation. +func (m *TemplateMutation) Slug() (r string, exists bool) { + v := m.slug if v == nil { return } return *v, true } -// OldGroupID returns the old "group_id" field's value of the GroupMembership entity. -// If the GroupMembership object wasn't provided to the builder, the object is fetched from the database. +// OldSlug returns the old "slug" field's value of the Template entity. +// If the Template object wasn't provided to the builder, the object is fetched from the database. // An error is returned if the mutation operation is not UpdateOne, or the database query fails. -func (m *GroupMembershipMutation) OldGroupID(ctx context.Context) (v uuid.UUID, err error) { +func (m *TemplateMutation) OldSlug(ctx context.Context) (v string, err error) { if !m.op.Is(OpUpdateOne) { - return v, errors.New("OldGroupID is only allowed on UpdateOne operations") + return v, errors.New("OldSlug is only allowed on UpdateOne operations") } if m.id == nil || m.oldValue == nil { - return v, errors.New("OldGroupID requires an ID field in the mutation") + return v, errors.New("OldSlug requires an ID field in the mutation") } oldValue, err := m.oldValue(ctx) if err != nil { - return v, fmt.Errorf("querying old value for OldGroupID: %w", err) + return v, fmt.Errorf("querying old value for OldSlug: %w", err) } - return oldValue.GroupID, nil + return oldValue.Slug, nil } -// ResetGroupID resets all changes to the "group_id" field. -func (m *GroupMembershipMutation) ResetGroupID() { - m.group = nil +// ResetSlug resets all changes to the "slug" field. +func (m *TemplateMutation) ResetSlug() { + m.slug = nil } -// SetUserID sets the "user_id" field. -func (m *GroupMembershipMutation) SetUserID(u uuid.UUID) { - m.user = &u +// SetDisplayName sets the "display_name" field. +func (m *TemplateMutation) SetDisplayName(s string) { + m.display_name = &s } -// UserID returns the value of the "user_id" field in the mutation. -func (m *GroupMembershipMutation) UserID() (r uuid.UUID, exists bool) { - v := m.user +// DisplayName returns the value of the "display_name" field in the mutation. +func (m *TemplateMutation) DisplayName() (r string, exists bool) { + v := m.display_name if v == nil { return } return *v, true } -// OldUserID returns the old "user_id" field's value of the GroupMembership entity. -// If the GroupMembership object wasn't provided to the builder, the object is fetched from the database. +// OldDisplayName returns the old "display_name" field's value of the Template entity. +// If the Template object wasn't provided to the builder, the object is fetched from the database. // An error is returned if the mutation operation is not UpdateOne, or the database query fails. -func (m *GroupMembershipMutation) OldUserID(ctx context.Context) (v *uuid.UUID, err error) { +func (m *TemplateMutation) OldDisplayName(ctx context.Context) (v string, err error) { if !m.op.Is(OpUpdateOne) { - return v, errors.New("OldUserID is only allowed on UpdateOne operations") + return v, errors.New("OldDisplayName is only allowed on UpdateOne operations") } if m.id == nil || m.oldValue == nil { - return v, errors.New("OldUserID requires an ID field in the mutation") + return v, errors.New("OldDisplayName requires an ID field in the mutation") } oldValue, err := m.oldValue(ctx) if err != nil { - return v, fmt.Errorf("querying old value for OldUserID: %w", err) + return v, fmt.Errorf("querying old value for OldDisplayName: %w", err) } - return oldValue.UserID, nil + return oldValue.DisplayName, nil } -// ClearUserID clears the value of the "user_id" field. -func (m *GroupMembershipMutation) ClearUserID() { - m.user = nil - m.clearedFields[groupmembership.FieldUserID] = struct{}{} +// ClearDisplayName clears the value of the "display_name" field. +func (m *TemplateMutation) ClearDisplayName() { + m.display_name = nil + m.clearedFields[template.FieldDisplayName] = struct{}{} } -// UserIDCleared returns if the "user_id" field was cleared in this mutation. -func (m *GroupMembershipMutation) UserIDCleared() bool { - _, ok := m.clearedFields[groupmembership.FieldUserID] +// DisplayNameCleared returns if the "display_name" field was cleared in this mutation. +func (m *TemplateMutation) DisplayNameCleared() bool { + _, ok := m.clearedFields[template.FieldDisplayName] return ok } -// ResetUserID resets all changes to the "user_id" field. -func (m *GroupMembershipMutation) ResetUserID() { - m.user = nil - delete(m.clearedFields, groupmembership.FieldUserID) +// ResetDisplayName resets all changes to the "display_name" field. +func (m *TemplateMutation) ResetDisplayName() { + m.display_name = nil + delete(m.clearedFields, template.FieldDisplayName) } -// SetAgentID sets the "agent_id" field. -func (m *GroupMembershipMutation) SetAgentID(u uuid.UUID) { - m.agent = &u +// SetDescription sets the "description" field. +func (m *TemplateMutation) SetDescription(s string) { + m.description = &s } -// AgentID returns the value of the "agent_id" field in the mutation. -func (m *GroupMembershipMutation) AgentID() (r uuid.UUID, exists bool) { - v := m.agent +// Description returns the value of the "description" field in the mutation. +func (m *TemplateMutation) Description() (r string, exists bool) { + v := m.description if v == nil { return } return *v, true } -// OldAgentID returns the old "agent_id" field's value of the GroupMembership entity. -// If the GroupMembership object wasn't provided to the builder, the object is fetched from the database. +// OldDescription returns the old "description" field's value of the Template entity. +// If the Template object wasn't provided to the builder, the object is fetched from the database. // An error is returned if the mutation operation is not UpdateOne, or the database query fails. -func (m *GroupMembershipMutation) OldAgentID(ctx context.Context) (v *uuid.UUID, err error) { +func (m *TemplateMutation) OldDescription(ctx context.Context) (v string, err error) { if !m.op.Is(OpUpdateOne) { - return v, errors.New("OldAgentID is only allowed on UpdateOne operations") + return v, errors.New("OldDescription is only allowed on UpdateOne operations") } if m.id == nil || m.oldValue == nil { - return v, errors.New("OldAgentID requires an ID field in the mutation") + return v, errors.New("OldDescription requires an ID field in the mutation") } oldValue, err := m.oldValue(ctx) if err != nil { - return v, fmt.Errorf("querying old value for OldAgentID: %w", err) + return v, fmt.Errorf("querying old value for OldDescription: %w", err) } - return oldValue.AgentID, nil + return oldValue.Description, nil } -// ClearAgentID clears the value of the "agent_id" field. -func (m *GroupMembershipMutation) ClearAgentID() { - m.agent = nil - m.clearedFields[groupmembership.FieldAgentID] = struct{}{} +// ClearDescription clears the value of the "description" field. +func (m *TemplateMutation) ClearDescription() { + m.description = nil + m.clearedFields[template.FieldDescription] = struct{}{} } -// AgentIDCleared returns if the "agent_id" field was cleared in this mutation. -func (m *GroupMembershipMutation) AgentIDCleared() bool { - _, ok := m.clearedFields[groupmembership.FieldAgentID] +// DescriptionCleared returns if the "description" field was cleared in this mutation. +func (m *TemplateMutation) DescriptionCleared() bool { + _, ok := m.clearedFields[template.FieldDescription] return ok } -// ResetAgentID resets all changes to the "agent_id" field. -func (m *GroupMembershipMutation) ResetAgentID() { - m.agent = nil - delete(m.clearedFields, groupmembership.FieldAgentID) -} - -// ClearGroup clears the "group" edge to the Group entity. -func (m *GroupMembershipMutation) ClearGroup() { - m.clearedgroup = true - m.clearedFields[groupmembership.FieldGroupID] = struct{}{} +// ResetDescription resets all changes to the "description" field. +func (m *TemplateMutation) ResetDescription() { + m.description = nil + delete(m.clearedFields, template.FieldDescription) } -// GroupCleared reports if the "group" edge to the Group entity was cleared. -func (m *GroupMembershipMutation) GroupCleared() bool { - return m.clearedgroup +// SetHarness sets the "harness" field. +func (m *TemplateMutation) SetHarness(s string) { + m.harness = &s } -// GroupIDs returns the "group" edge IDs in the mutation. -// Note that IDs always returns len(IDs) <= 1 for unique edges, and you should use -// GroupID instead. It exists only for internal usage by the builders. -func (m *GroupMembershipMutation) GroupIDs() (ids []uuid.UUID) { - if id := m.group; id != nil { - ids = append(ids, *id) +// Harness returns the value of the "harness" field in the mutation. +func (m *TemplateMutation) Harness() (r string, exists bool) { + v := m.harness + if v == nil { + return } - return -} - -// ResetGroup resets all changes to the "group" edge. -func (m *GroupMembershipMutation) ResetGroup() { - m.group = nil - m.clearedgroup = false -} - -// ClearUser clears the "user" edge to the User entity. -func (m *GroupMembershipMutation) ClearUser() { - m.cleareduser = true - m.clearedFields[groupmembership.FieldUserID] = struct{}{} -} - -// UserCleared reports if the "user" edge to the User entity was cleared. -func (m *GroupMembershipMutation) UserCleared() bool { - return m.UserIDCleared() || m.cleareduser + return *v, true } -// UserIDs returns the "user" edge IDs in the mutation. -// Note that IDs always returns len(IDs) <= 1 for unique edges, and you should use -// UserID instead. It exists only for internal usage by the builders. -func (m *GroupMembershipMutation) UserIDs() (ids []uuid.UUID) { - if id := m.user; id != nil { - ids = append(ids, *id) +// OldHarness returns the old "harness" field's value of the Template entity. +// If the Template object wasn't provided to the builder, the object is fetched from the database. +// An error is returned if the mutation operation is not UpdateOne, or the database query fails. +func (m *TemplateMutation) OldHarness(ctx context.Context) (v string, err error) { + if !m.op.Is(OpUpdateOne) { + return v, errors.New("OldHarness is only allowed on UpdateOne operations") } - return -} - -// ResetUser resets all changes to the "user" edge. -func (m *GroupMembershipMutation) ResetUser() { - m.user = nil - m.cleareduser = false + if m.id == nil || m.oldValue == nil { + return v, errors.New("OldHarness requires an ID field in the mutation") + } + oldValue, err := m.oldValue(ctx) + if err != nil { + return v, fmt.Errorf("querying old value for OldHarness: %w", err) + } + return oldValue.Harness, nil } -// ClearAgent clears the "agent" edge to the Agent entity. -func (m *GroupMembershipMutation) ClearAgent() { - m.clearedagent = true - m.clearedFields[groupmembership.FieldAgentID] = struct{}{} +// ResetHarness resets all changes to the "harness" field. +func (m *TemplateMutation) ResetHarness() { + m.harness = nil } -// AgentCleared reports if the "agent" edge to the Agent entity was cleared. -func (m *GroupMembershipMutation) AgentCleared() bool { - return m.AgentIDCleared() || m.clearedagent +// SetDefaultHarnessConfig sets the "default_harness_config" field. +func (m *TemplateMutation) SetDefaultHarnessConfig(s string) { + m.default_harness_config = &s } -// AgentIDs returns the "agent" edge IDs in the mutation. -// Note that IDs always returns len(IDs) <= 1 for unique edges, and you should use -// AgentID instead. It exists only for internal usage by the builders. -func (m *GroupMembershipMutation) AgentIDs() (ids []uuid.UUID) { - if id := m.agent; id != nil { - ids = append(ids, *id) +// DefaultHarnessConfig returns the value of the "default_harness_config" field in the mutation. +func (m *TemplateMutation) DefaultHarnessConfig() (r string, exists bool) { + v := m.default_harness_config + if v == nil { + return } - return -} - -// ResetAgent resets all changes to the "agent" edge. -func (m *GroupMembershipMutation) ResetAgent() { - m.agent = nil - m.clearedagent = false -} - -// Where appends a list predicates to the GroupMembershipMutation builder. -func (m *GroupMembershipMutation) Where(ps ...predicate.GroupMembership) { - m.predicates = append(m.predicates, ps...) + return *v, true } -// WhereP appends storage-level predicates to the GroupMembershipMutation builder. Using this method, -// users can use type-assertion to append predicates that do not depend on any generated package. -func (m *GroupMembershipMutation) WhereP(ps ...func(*sql.Selector)) { - p := make([]predicate.GroupMembership, len(ps)) - for i := range ps { - p[i] = ps[i] +// OldDefaultHarnessConfig returns the old "default_harness_config" field's value of the Template entity. +// If the Template object wasn't provided to the builder, the object is fetched from the database. +// An error is returned if the mutation operation is not UpdateOne, or the database query fails. +func (m *TemplateMutation) OldDefaultHarnessConfig(ctx context.Context) (v string, err error) { + if !m.op.Is(OpUpdateOne) { + return v, errors.New("OldDefaultHarnessConfig is only allowed on UpdateOne operations") + } + if m.id == nil || m.oldValue == nil { + return v, errors.New("OldDefaultHarnessConfig requires an ID field in the mutation") } - m.Where(p...) + oldValue, err := m.oldValue(ctx) + if err != nil { + return v, fmt.Errorf("querying old value for OldDefaultHarnessConfig: %w", err) + } + return oldValue.DefaultHarnessConfig, nil } -// Op returns the operation name. -func (m *GroupMembershipMutation) Op() Op { - return m.op +// ClearDefaultHarnessConfig clears the value of the "default_harness_config" field. +func (m *TemplateMutation) ClearDefaultHarnessConfig() { + m.default_harness_config = nil + m.clearedFields[template.FieldDefaultHarnessConfig] = struct{}{} } -// SetOp allows setting the mutation operation. -func (m *GroupMembershipMutation) SetOp(op Op) { - m.op = op +// DefaultHarnessConfigCleared returns if the "default_harness_config" field was cleared in this mutation. +func (m *TemplateMutation) DefaultHarnessConfigCleared() bool { + _, ok := m.clearedFields[template.FieldDefaultHarnessConfig] + return ok } -// Type returns the node type of this mutation (GroupMembership). -func (m *GroupMembershipMutation) Type() string { - return m.typ +// ResetDefaultHarnessConfig resets all changes to the "default_harness_config" field. +func (m *TemplateMutation) ResetDefaultHarnessConfig() { + m.default_harness_config = nil + delete(m.clearedFields, template.FieldDefaultHarnessConfig) } -// Fields returns all fields that were changed during this mutation. Note that in -// order to get all numeric fields that were incremented/decremented, call -// AddedFields(). -func (m *GroupMembershipMutation) Fields() []string { - fields := make([]string, 0, 6) - if m.role != nil { - fields = append(fields, groupmembership.FieldRole) - } - if m.added_by != nil { - fields = append(fields, groupmembership.FieldAddedBy) - } - if m.added_at != nil { - fields = append(fields, groupmembership.FieldAddedAt) - } - if m.group != nil { - fields = append(fields, groupmembership.FieldGroupID) - } - if m.user != nil { - fields = append(fields, groupmembership.FieldUserID) - } - if m.agent != nil { - fields = append(fields, groupmembership.FieldAgentID) - } - return fields +// SetImage sets the "image" field. +func (m *TemplateMutation) SetImage(s string) { + m.image = &s } -// Field returns the value of a field with the given name. The second boolean -// return value indicates that this field was not set, or was not defined in the -// schema. -func (m *GroupMembershipMutation) Field(name string) (ent.Value, bool) { - switch name { - case groupmembership.FieldRole: - return m.Role() - case groupmembership.FieldAddedBy: - return m.AddedBy() - case groupmembership.FieldAddedAt: - return m.AddedAt() - case groupmembership.FieldGroupID: - return m.GroupID() - case groupmembership.FieldUserID: - return m.UserID() - case groupmembership.FieldAgentID: - return m.AgentID() +// Image returns the value of the "image" field in the mutation. +func (m *TemplateMutation) Image() (r string, exists bool) { + v := m.image + if v == nil { + return } - return nil, false + return *v, true } -// OldField returns the old value of the field from the database. An error is -// returned if the mutation operation is not UpdateOne, or the query to the -// database failed. -func (m *GroupMembershipMutation) OldField(ctx context.Context, name string) (ent.Value, error) { - switch name { - case groupmembership.FieldRole: - return m.OldRole(ctx) - case groupmembership.FieldAddedBy: - return m.OldAddedBy(ctx) - case groupmembership.FieldAddedAt: - return m.OldAddedAt(ctx) - case groupmembership.FieldGroupID: - return m.OldGroupID(ctx) - case groupmembership.FieldUserID: - return m.OldUserID(ctx) - case groupmembership.FieldAgentID: - return m.OldAgentID(ctx) +// OldImage returns the old "image" field's value of the Template entity. +// If the Template object wasn't provided to the builder, the object is fetched from the database. +// An error is returned if the mutation operation is not UpdateOne, or the database query fails. +func (m *TemplateMutation) OldImage(ctx context.Context) (v string, err error) { + if !m.op.Is(OpUpdateOne) { + return v, errors.New("OldImage is only allowed on UpdateOne operations") } - return nil, fmt.Errorf("unknown GroupMembership field %s", name) + if m.id == nil || m.oldValue == nil { + return v, errors.New("OldImage requires an ID field in the mutation") + } + oldValue, err := m.oldValue(ctx) + if err != nil { + return v, fmt.Errorf("querying old value for OldImage: %w", err) + } + return oldValue.Image, nil } -// SetField sets the value of a field with the given name. It returns an error if -// the field is not defined in the schema, or if the type mismatched the field -// type. -func (m *GroupMembershipMutation) SetField(name string, value ent.Value) error { - switch name { - case groupmembership.FieldRole: - v, ok := value.(groupmembership.Role) - if !ok { - return fmt.Errorf("unexpected type %T for field %s", value, name) - } - m.SetRole(v) - return nil - case groupmembership.FieldAddedBy: - v, ok := value.(string) - if !ok { - return fmt.Errorf("unexpected type %T for field %s", value, name) - } - m.SetAddedBy(v) - return nil - case groupmembership.FieldAddedAt: - v, ok := value.(time.Time) - if !ok { - return fmt.Errorf("unexpected type %T for field %s", value, name) - } - m.SetAddedAt(v) - return nil - case groupmembership.FieldGroupID: - v, ok := value.(uuid.UUID) - if !ok { - return fmt.Errorf("unexpected type %T for field %s", value, name) - } - m.SetGroupID(v) - return nil - case groupmembership.FieldUserID: - v, ok := value.(uuid.UUID) - if !ok { - return fmt.Errorf("unexpected type %T for field %s", value, name) - } - m.SetUserID(v) - return nil - case groupmembership.FieldAgentID: - v, ok := value.(uuid.UUID) - if !ok { - return fmt.Errorf("unexpected type %T for field %s", value, name) - } - m.SetAgentID(v) - return nil - } - return fmt.Errorf("unknown GroupMembership field %s", name) +// ClearImage clears the value of the "image" field. +func (m *TemplateMutation) ClearImage() { + m.image = nil + m.clearedFields[template.FieldImage] = struct{}{} } -// AddedFields returns all numeric fields that were incremented/decremented during -// this mutation. -func (m *GroupMembershipMutation) AddedFields() []string { - return nil +// ImageCleared returns if the "image" field was cleared in this mutation. +func (m *TemplateMutation) ImageCleared() bool { + _, ok := m.clearedFields[template.FieldImage] + return ok } -// AddedField returns the numeric value that was incremented/decremented on a field -// with the given name. The second boolean return value indicates that this field -// was not set, or was not defined in the schema. -func (m *GroupMembershipMutation) AddedField(name string) (ent.Value, bool) { - return nil, false +// ResetImage resets all changes to the "image" field. +func (m *TemplateMutation) ResetImage() { + m.image = nil + delete(m.clearedFields, template.FieldImage) } -// AddField adds the value to the field with the given name. It returns an error if -// the field is not defined in the schema, or if the type mismatched the field -// type. -func (m *GroupMembershipMutation) AddField(name string, value ent.Value) error { - switch name { +// SetConfig sets the "config" field. +func (m *TemplateMutation) SetConfig(s string) { + m._config = &s +} + +// Config returns the value of the "config" field in the mutation. +func (m *TemplateMutation) Config() (r string, exists bool) { + v := m._config + if v == nil { + return } - return fmt.Errorf("unknown GroupMembership numeric field %s", name) + return *v, true } -// ClearedFields returns all nullable fields that were cleared during this -// mutation. -func (m *GroupMembershipMutation) ClearedFields() []string { - var fields []string - if m.FieldCleared(groupmembership.FieldAddedBy) { - fields = append(fields, groupmembership.FieldAddedBy) +// OldConfig returns the old "config" field's value of the Template entity. +// If the Template object wasn't provided to the builder, the object is fetched from the database. +// An error is returned if the mutation operation is not UpdateOne, or the database query fails. +func (m *TemplateMutation) OldConfig(ctx context.Context) (v string, err error) { + if !m.op.Is(OpUpdateOne) { + return v, errors.New("OldConfig is only allowed on UpdateOne operations") } - if m.FieldCleared(groupmembership.FieldUserID) { - fields = append(fields, groupmembership.FieldUserID) + if m.id == nil || m.oldValue == nil { + return v, errors.New("OldConfig requires an ID field in the mutation") } - if m.FieldCleared(groupmembership.FieldAgentID) { - fields = append(fields, groupmembership.FieldAgentID) + oldValue, err := m.oldValue(ctx) + if err != nil { + return v, fmt.Errorf("querying old value for OldConfig: %w", err) } - return fields + return oldValue.Config, nil } -// FieldCleared returns a boolean indicating if a field with the given name was -// cleared in this mutation. -func (m *GroupMembershipMutation) FieldCleared(name string) bool { - _, ok := m.clearedFields[name] +// ClearConfig clears the value of the "config" field. +func (m *TemplateMutation) ClearConfig() { + m._config = nil + m.clearedFields[template.FieldConfig] = struct{}{} +} + +// ConfigCleared returns if the "config" field was cleared in this mutation. +func (m *TemplateMutation) ConfigCleared() bool { + _, ok := m.clearedFields[template.FieldConfig] return ok } -// ClearField clears the value of the field with the given name. It returns an -// error if the field is not defined in the schema. -func (m *GroupMembershipMutation) ClearField(name string) error { - switch name { - case groupmembership.FieldAddedBy: - m.ClearAddedBy() - return nil - case groupmembership.FieldUserID: - m.ClearUserID() - return nil - case groupmembership.FieldAgentID: - m.ClearAgentID() - return nil - } - return fmt.Errorf("unknown GroupMembership nullable field %s", name) +// ResetConfig resets all changes to the "config" field. +func (m *TemplateMutation) ResetConfig() { + m._config = nil + delete(m.clearedFields, template.FieldConfig) } -// ResetField resets all changes in the mutation for the field with the given name. -// It returns an error if the field is not defined in the schema. -func (m *GroupMembershipMutation) ResetField(name string) error { - switch name { - case groupmembership.FieldRole: - m.ResetRole() - return nil - case groupmembership.FieldAddedBy: - m.ResetAddedBy() - return nil - case groupmembership.FieldAddedAt: - m.ResetAddedAt() - return nil - case groupmembership.FieldGroupID: - m.ResetGroupID() - return nil - case groupmembership.FieldUserID: - m.ResetUserID() - return nil - case groupmembership.FieldAgentID: - m.ResetAgentID() - return nil +// SetContentHash sets the "content_hash" field. +func (m *TemplateMutation) SetContentHash(s string) { + m.content_hash = &s +} + +// ContentHash returns the value of the "content_hash" field in the mutation. +func (m *TemplateMutation) ContentHash() (r string, exists bool) { + v := m.content_hash + if v == nil { + return } - return fmt.Errorf("unknown GroupMembership field %s", name) + return *v, true } -// AddedEdges returns all edge names that were set/added in this mutation. -func (m *GroupMembershipMutation) AddedEdges() []string { - edges := make([]string, 0, 3) - if m.group != nil { - edges = append(edges, groupmembership.EdgeGroup) +// OldContentHash returns the old "content_hash" field's value of the Template entity. +// If the Template object wasn't provided to the builder, the object is fetched from the database. +// An error is returned if the mutation operation is not UpdateOne, or the database query fails. +func (m *TemplateMutation) OldContentHash(ctx context.Context) (v string, err error) { + if !m.op.Is(OpUpdateOne) { + return v, errors.New("OldContentHash is only allowed on UpdateOne operations") } - if m.user != nil { - edges = append(edges, groupmembership.EdgeUser) + if m.id == nil || m.oldValue == nil { + return v, errors.New("OldContentHash requires an ID field in the mutation") } - if m.agent != nil { - edges = append(edges, groupmembership.EdgeAgent) + oldValue, err := m.oldValue(ctx) + if err != nil { + return v, fmt.Errorf("querying old value for OldContentHash: %w", err) } - return edges + return oldValue.ContentHash, nil } -// AddedIDs returns all IDs (to other nodes) that were added for the given edge -// name in this mutation. -func (m *GroupMembershipMutation) AddedIDs(name string) []ent.Value { - switch name { - case groupmembership.EdgeGroup: - if id := m.group; id != nil { - return []ent.Value{*id} - } - case groupmembership.EdgeUser: - if id := m.user; id != nil { - return []ent.Value{*id} - } - case groupmembership.EdgeAgent: - if id := m.agent; id != nil { - return []ent.Value{*id} - } - } - return nil +// ClearContentHash clears the value of the "content_hash" field. +func (m *TemplateMutation) ClearContentHash() { + m.content_hash = nil + m.clearedFields[template.FieldContentHash] = struct{}{} +} + +// ContentHashCleared returns if the "content_hash" field was cleared in this mutation. +func (m *TemplateMutation) ContentHashCleared() bool { + _, ok := m.clearedFields[template.FieldContentHash] + return ok } -// RemovedEdges returns all edge names that were removed in this mutation. -func (m *GroupMembershipMutation) RemovedEdges() []string { - edges := make([]string, 0, 3) - return edges +// ResetContentHash resets all changes to the "content_hash" field. +func (m *TemplateMutation) ResetContentHash() { + m.content_hash = nil + delete(m.clearedFields, template.FieldContentHash) } -// RemovedIDs returns all IDs (to other nodes) that were removed for the edge with -// the given name in this mutation. -func (m *GroupMembershipMutation) RemovedIDs(name string) []ent.Value { - return nil +// SetScope sets the "scope" field. +func (m *TemplateMutation) SetScope(s string) { + m.scope = &s } -// ClearedEdges returns all edge names that were cleared in this mutation. -func (m *GroupMembershipMutation) ClearedEdges() []string { - edges := make([]string, 0, 3) - if m.clearedgroup { - edges = append(edges, groupmembership.EdgeGroup) +// Scope returns the value of the "scope" field in the mutation. +func (m *TemplateMutation) Scope() (r string, exists bool) { + v := m.scope + if v == nil { + return } - if m.cleareduser { - edges = append(edges, groupmembership.EdgeUser) + return *v, true +} + +// OldScope returns the old "scope" field's value of the Template entity. +// If the Template object wasn't provided to the builder, the object is fetched from the database. +// An error is returned if the mutation operation is not UpdateOne, or the database query fails. +func (m *TemplateMutation) OldScope(ctx context.Context) (v string, err error) { + if !m.op.Is(OpUpdateOne) { + return v, errors.New("OldScope is only allowed on UpdateOne operations") } - if m.clearedagent { - edges = append(edges, groupmembership.EdgeAgent) + if m.id == nil || m.oldValue == nil { + return v, errors.New("OldScope requires an ID field in the mutation") } - return edges + oldValue, err := m.oldValue(ctx) + if err != nil { + return v, fmt.Errorf("querying old value for OldScope: %w", err) + } + return oldValue.Scope, nil } -// EdgeCleared returns a boolean which indicates if the edge with the given name -// was cleared in this mutation. -func (m *GroupMembershipMutation) EdgeCleared(name string) bool { - switch name { - case groupmembership.EdgeGroup: - return m.clearedgroup - case groupmembership.EdgeUser: - return m.cleareduser - case groupmembership.EdgeAgent: - return m.clearedagent - } - return false +// ResetScope resets all changes to the "scope" field. +func (m *TemplateMutation) ResetScope() { + m.scope = nil } -// ClearEdge clears the value of the edge with the given name. It returns an error -// if that edge is not defined in the schema. -func (m *GroupMembershipMutation) ClearEdge(name string) error { - switch name { - case groupmembership.EdgeGroup: - m.ClearGroup() - return nil - case groupmembership.EdgeUser: - m.ClearUser() - return nil - case groupmembership.EdgeAgent: - m.ClearAgent() - return nil +// SetScopeID sets the "scope_id" field. +func (m *TemplateMutation) SetScopeID(s string) { + m.scope_id = &s +} + +// ScopeID returns the value of the "scope_id" field in the mutation. +func (m *TemplateMutation) ScopeID() (r string, exists bool) { + v := m.scope_id + if v == nil { + return } - return fmt.Errorf("unknown GroupMembership unique edge %s", name) + return *v, true } -// ResetEdge resets all changes to the edge with the given name in this mutation. -// It returns an error if the edge is not defined in the schema. -func (m *GroupMembershipMutation) ResetEdge(name string) error { - switch name { - case groupmembership.EdgeGroup: - m.ResetGroup() - return nil - case groupmembership.EdgeUser: - m.ResetUser() - return nil - case groupmembership.EdgeAgent: - m.ResetAgent() - return nil +// OldScopeID returns the old "scope_id" field's value of the Template entity. +// If the Template object wasn't provided to the builder, the object is fetched from the database. +// An error is returned if the mutation operation is not UpdateOne, or the database query fails. +func (m *TemplateMutation) OldScopeID(ctx context.Context) (v string, err error) { + if !m.op.Is(OpUpdateOne) { + return v, errors.New("OldScopeID is only allowed on UpdateOne operations") } - return fmt.Errorf("unknown GroupMembership edge %s", name) + if m.id == nil || m.oldValue == nil { + return v, errors.New("OldScopeID requires an ID field in the mutation") + } + oldValue, err := m.oldValue(ctx) + if err != nil { + return v, fmt.Errorf("querying old value for OldScopeID: %w", err) + } + return oldValue.ScopeID, nil } -// PolicyBindingMutation represents an operation that mutates the PolicyBinding nodes in the graph. -type PolicyBindingMutation struct { - config - op Op - typ string - id *uuid.UUID - principal_type *policybinding.PrincipalType - created *time.Time - created_by *string - clearedFields map[string]struct{} - policy *uuid.UUID - clearedpolicy bool - user *uuid.UUID - cleareduser bool - group *uuid.UUID - clearedgroup bool - agent *uuid.UUID - clearedagent bool - done bool - oldValue func(context.Context) (*PolicyBinding, error) - predicates []predicate.PolicyBinding +// ClearScopeID clears the value of the "scope_id" field. +func (m *TemplateMutation) ClearScopeID() { + m.scope_id = nil + m.clearedFields[template.FieldScopeID] = struct{}{} } -var _ ent.Mutation = (*PolicyBindingMutation)(nil) +// ScopeIDCleared returns if the "scope_id" field was cleared in this mutation. +func (m *TemplateMutation) ScopeIDCleared() bool { + _, ok := m.clearedFields[template.FieldScopeID] + return ok +} -// policybindingOption allows management of the mutation configuration using functional options. -type policybindingOption func(*PolicyBindingMutation) +// ResetScopeID resets all changes to the "scope_id" field. +func (m *TemplateMutation) ResetScopeID() { + m.scope_id = nil + delete(m.clearedFields, template.FieldScopeID) +} -// newPolicyBindingMutation creates new mutation for the PolicyBinding entity. -func newPolicyBindingMutation(c config, op Op, opts ...policybindingOption) *PolicyBindingMutation { - m := &PolicyBindingMutation{ - config: c, - op: op, - typ: TypePolicyBinding, - clearedFields: make(map[string]struct{}), - } - for _, opt := range opts { - opt(m) - } - return m +// SetProjectID sets the "project_id" field. +func (m *TemplateMutation) SetProjectID(s string) { + m.project_id = &s } -// withPolicyBindingID sets the ID field of the mutation. -func withPolicyBindingID(id uuid.UUID) policybindingOption { - return func(m *PolicyBindingMutation) { - var ( - err error - once sync.Once - value *PolicyBinding - ) - m.oldValue = func(ctx context.Context) (*PolicyBinding, error) { - once.Do(func() { - if m.done { - err = errors.New("querying old values post mutation is not allowed") - } else { - value, err = m.Client().PolicyBinding.Get(ctx, id) - } - }) - return value, err - } - m.id = &id +// ProjectID returns the value of the "project_id" field in the mutation. +func (m *TemplateMutation) ProjectID() (r string, exists bool) { + v := m.project_id + if v == nil { + return } + return *v, true } -// withPolicyBinding sets the old PolicyBinding of the mutation. -func withPolicyBinding(node *PolicyBinding) policybindingOption { - return func(m *PolicyBindingMutation) { - m.oldValue = func(context.Context) (*PolicyBinding, error) { - return node, nil - } - m.id = &node.ID +// OldProjectID returns the old "project_id" field's value of the Template entity. +// If the Template object wasn't provided to the builder, the object is fetched from the database. +// An error is returned if the mutation operation is not UpdateOne, or the database query fails. +func (m *TemplateMutation) OldProjectID(ctx context.Context) (v string, err error) { + if !m.op.Is(OpUpdateOne) { + return v, errors.New("OldProjectID is only allowed on UpdateOne operations") } + if m.id == nil || m.oldValue == nil { + return v, errors.New("OldProjectID requires an ID field in the mutation") + } + oldValue, err := m.oldValue(ctx) + if err != nil { + return v, fmt.Errorf("querying old value for OldProjectID: %w", err) + } + return oldValue.ProjectID, nil } -// Client returns a new `ent.Client` from the mutation. If the mutation was -// executed in a transaction (ent.Tx), a transactional client is returned. -func (m PolicyBindingMutation) Client() *Client { - client := &Client{config: m.config} - client.init() - return client +// ClearProjectID clears the value of the "project_id" field. +func (m *TemplateMutation) ClearProjectID() { + m.project_id = nil + m.clearedFields[template.FieldProjectID] = struct{}{} } -// Tx returns an `ent.Tx` for mutations that were executed in transactions; -// it returns an error otherwise. -func (m PolicyBindingMutation) Tx() (*Tx, error) { - if _, ok := m.driver.(*txDriver); !ok { - return nil, errors.New("ent: mutation is not running in a transaction") - } - tx := &Tx{config: m.config} - tx.init() - return tx, nil +// ProjectIDCleared returns if the "project_id" field was cleared in this mutation. +func (m *TemplateMutation) ProjectIDCleared() bool { + _, ok := m.clearedFields[template.FieldProjectID] + return ok } -// SetID sets the value of the id field. Note that this -// operation is only accepted on creation of PolicyBinding entities. -func (m *PolicyBindingMutation) SetID(id uuid.UUID) { - m.id = &id +// ResetProjectID resets all changes to the "project_id" field. +func (m *TemplateMutation) ResetProjectID() { + m.project_id = nil + delete(m.clearedFields, template.FieldProjectID) } -// ID returns the ID value in the mutation. Note that the ID is only available -// if it was provided to the builder or after it was returned from the database. -func (m *PolicyBindingMutation) ID() (id uuid.UUID, exists bool) { - if m.id == nil { +// SetStorageURI sets the "storage_uri" field. +func (m *TemplateMutation) SetStorageURI(s string) { + m.storage_uri = &s +} + +// StorageURI returns the value of the "storage_uri" field in the mutation. +func (m *TemplateMutation) StorageURI() (r string, exists bool) { + v := m.storage_uri + if v == nil { return } - return *m.id, true + return *v, true } -// IDs queries the database and returns the entity ids that match the mutation's predicate. -// That means, if the mutation is applied within a transaction with an isolation level such -// as sql.LevelSerializable, the returned ids match the ids of the rows that will be updated -// or updated by the mutation. -func (m *PolicyBindingMutation) IDs(ctx context.Context) ([]uuid.UUID, error) { - switch { - case m.op.Is(OpUpdateOne | OpDeleteOne): - id, exists := m.ID() - if exists { - return []uuid.UUID{id}, nil - } - fallthrough - case m.op.Is(OpUpdate | OpDelete): - return m.Client().PolicyBinding.Query().Where(m.predicates...).IDs(ctx) - default: - return nil, fmt.Errorf("IDs is not allowed on %s operations", m.op) +// OldStorageURI returns the old "storage_uri" field's value of the Template entity. +// If the Template object wasn't provided to the builder, the object is fetched from the database. +// An error is returned if the mutation operation is not UpdateOne, or the database query fails. +func (m *TemplateMutation) OldStorageURI(ctx context.Context) (v string, err error) { + if !m.op.Is(OpUpdateOne) { + return v, errors.New("OldStorageURI is only allowed on UpdateOne operations") + } + if m.id == nil || m.oldValue == nil { + return v, errors.New("OldStorageURI requires an ID field in the mutation") + } + oldValue, err := m.oldValue(ctx) + if err != nil { + return v, fmt.Errorf("querying old value for OldStorageURI: %w", err) } + return oldValue.StorageURI, nil } -// SetPrincipalType sets the "principal_type" field. -func (m *PolicyBindingMutation) SetPrincipalType(pt policybinding.PrincipalType) { - m.principal_type = &pt +// ClearStorageURI clears the value of the "storage_uri" field. +func (m *TemplateMutation) ClearStorageURI() { + m.storage_uri = nil + m.clearedFields[template.FieldStorageURI] = struct{}{} } -// PrincipalType returns the value of the "principal_type" field in the mutation. -func (m *PolicyBindingMutation) PrincipalType() (r policybinding.PrincipalType, exists bool) { - v := m.principal_type +// StorageURICleared returns if the "storage_uri" field was cleared in this mutation. +func (m *TemplateMutation) StorageURICleared() bool { + _, ok := m.clearedFields[template.FieldStorageURI] + return ok +} + +// ResetStorageURI resets all changes to the "storage_uri" field. +func (m *TemplateMutation) ResetStorageURI() { + m.storage_uri = nil + delete(m.clearedFields, template.FieldStorageURI) +} + +// SetStorageBucket sets the "storage_bucket" field. +func (m *TemplateMutation) SetStorageBucket(s string) { + m.storage_bucket = &s +} + +// StorageBucket returns the value of the "storage_bucket" field in the mutation. +func (m *TemplateMutation) StorageBucket() (r string, exists bool) { + v := m.storage_bucket if v == nil { return } return *v, true } -// OldPrincipalType returns the old "principal_type" field's value of the PolicyBinding entity. -// If the PolicyBinding object wasn't provided to the builder, the object is fetched from the database. +// OldStorageBucket returns the old "storage_bucket" field's value of the Template entity. +// If the Template object wasn't provided to the builder, the object is fetched from the database. // An error is returned if the mutation operation is not UpdateOne, or the database query fails. -func (m *PolicyBindingMutation) OldPrincipalType(ctx context.Context) (v policybinding.PrincipalType, err error) { +func (m *TemplateMutation) OldStorageBucket(ctx context.Context) (v string, err error) { if !m.op.Is(OpUpdateOne) { - return v, errors.New("OldPrincipalType is only allowed on UpdateOne operations") + return v, errors.New("OldStorageBucket is only allowed on UpdateOne operations") } if m.id == nil || m.oldValue == nil { - return v, errors.New("OldPrincipalType requires an ID field in the mutation") + return v, errors.New("OldStorageBucket requires an ID field in the mutation") } oldValue, err := m.oldValue(ctx) if err != nil { - return v, fmt.Errorf("querying old value for OldPrincipalType: %w", err) + return v, fmt.Errorf("querying old value for OldStorageBucket: %w", err) } - return oldValue.PrincipalType, nil + return oldValue.StorageBucket, nil +} + +// ClearStorageBucket clears the value of the "storage_bucket" field. +func (m *TemplateMutation) ClearStorageBucket() { + m.storage_bucket = nil + m.clearedFields[template.FieldStorageBucket] = struct{}{} +} + +// StorageBucketCleared returns if the "storage_bucket" field was cleared in this mutation. +func (m *TemplateMutation) StorageBucketCleared() bool { + _, ok := m.clearedFields[template.FieldStorageBucket] + return ok } -// ResetPrincipalType resets all changes to the "principal_type" field. -func (m *PolicyBindingMutation) ResetPrincipalType() { - m.principal_type = nil +// ResetStorageBucket resets all changes to the "storage_bucket" field. +func (m *TemplateMutation) ResetStorageBucket() { + m.storage_bucket = nil + delete(m.clearedFields, template.FieldStorageBucket) } -// SetCreated sets the "created" field. -func (m *PolicyBindingMutation) SetCreated(t time.Time) { - m.created = &t +// SetStoragePath sets the "storage_path" field. +func (m *TemplateMutation) SetStoragePath(s string) { + m.storage_path = &s } -// Created returns the value of the "created" field in the mutation. -func (m *PolicyBindingMutation) Created() (r time.Time, exists bool) { - v := m.created +// StoragePath returns the value of the "storage_path" field in the mutation. +func (m *TemplateMutation) StoragePath() (r string, exists bool) { + v := m.storage_path if v == nil { return } return *v, true } -// OldCreated returns the old "created" field's value of the PolicyBinding entity. -// If the PolicyBinding object wasn't provided to the builder, the object is fetched from the database. +// OldStoragePath returns the old "storage_path" field's value of the Template entity. +// If the Template object wasn't provided to the builder, the object is fetched from the database. // An error is returned if the mutation operation is not UpdateOne, or the database query fails. -func (m *PolicyBindingMutation) OldCreated(ctx context.Context) (v time.Time, err error) { +func (m *TemplateMutation) OldStoragePath(ctx context.Context) (v string, err error) { if !m.op.Is(OpUpdateOne) { - return v, errors.New("OldCreated is only allowed on UpdateOne operations") + return v, errors.New("OldStoragePath is only allowed on UpdateOne operations") } if m.id == nil || m.oldValue == nil { - return v, errors.New("OldCreated requires an ID field in the mutation") + return v, errors.New("OldStoragePath requires an ID field in the mutation") } oldValue, err := m.oldValue(ctx) if err != nil { - return v, fmt.Errorf("querying old value for OldCreated: %w", err) + return v, fmt.Errorf("querying old value for OldStoragePath: %w", err) } - return oldValue.Created, nil + return oldValue.StoragePath, nil } -// ResetCreated resets all changes to the "created" field. -func (m *PolicyBindingMutation) ResetCreated() { - m.created = nil +// ClearStoragePath clears the value of the "storage_path" field. +func (m *TemplateMutation) ClearStoragePath() { + m.storage_path = nil + m.clearedFields[template.FieldStoragePath] = struct{}{} } -// SetCreatedBy sets the "created_by" field. -func (m *PolicyBindingMutation) SetCreatedBy(s string) { - m.created_by = &s +// StoragePathCleared returns if the "storage_path" field was cleared in this mutation. +func (m *TemplateMutation) StoragePathCleared() bool { + _, ok := m.clearedFields[template.FieldStoragePath] + return ok } -// CreatedBy returns the value of the "created_by" field in the mutation. -func (m *PolicyBindingMutation) CreatedBy() (r string, exists bool) { - v := m.created_by +// ResetStoragePath resets all changes to the "storage_path" field. +func (m *TemplateMutation) ResetStoragePath() { + m.storage_path = nil + delete(m.clearedFields, template.FieldStoragePath) +} + +// SetFiles sets the "files" field. +func (m *TemplateMutation) SetFiles(s string) { + m.files = &s +} + +// Files returns the value of the "files" field in the mutation. +func (m *TemplateMutation) Files() (r string, exists bool) { + v := m.files if v == nil { return } return *v, true } -// OldCreatedBy returns the old "created_by" field's value of the PolicyBinding entity. -// If the PolicyBinding object wasn't provided to the builder, the object is fetched from the database. +// OldFiles returns the old "files" field's value of the Template entity. +// If the Template object wasn't provided to the builder, the object is fetched from the database. // An error is returned if the mutation operation is not UpdateOne, or the database query fails. -func (m *PolicyBindingMutation) OldCreatedBy(ctx context.Context) (v string, err error) { +func (m *TemplateMutation) OldFiles(ctx context.Context) (v string, err error) { if !m.op.Is(OpUpdateOne) { - return v, errors.New("OldCreatedBy is only allowed on UpdateOne operations") + return v, errors.New("OldFiles is only allowed on UpdateOne operations") } if m.id == nil || m.oldValue == nil { - return v, errors.New("OldCreatedBy requires an ID field in the mutation") + return v, errors.New("OldFiles requires an ID field in the mutation") } oldValue, err := m.oldValue(ctx) if err != nil { - return v, fmt.Errorf("querying old value for OldCreatedBy: %w", err) + return v, fmt.Errorf("querying old value for OldFiles: %w", err) } - return oldValue.CreatedBy, nil + return oldValue.Files, nil } -// ClearCreatedBy clears the value of the "created_by" field. -func (m *PolicyBindingMutation) ClearCreatedBy() { - m.created_by = nil - m.clearedFields[policybinding.FieldCreatedBy] = struct{}{} +// ClearFiles clears the value of the "files" field. +func (m *TemplateMutation) ClearFiles() { + m.files = nil + m.clearedFields[template.FieldFiles] = struct{}{} } -// CreatedByCleared returns if the "created_by" field was cleared in this mutation. -func (m *PolicyBindingMutation) CreatedByCleared() bool { - _, ok := m.clearedFields[policybinding.FieldCreatedBy] +// FilesCleared returns if the "files" field was cleared in this mutation. +func (m *TemplateMutation) FilesCleared() bool { + _, ok := m.clearedFields[template.FieldFiles] return ok } -// ResetCreatedBy resets all changes to the "created_by" field. -func (m *PolicyBindingMutation) ResetCreatedBy() { - m.created_by = nil - delete(m.clearedFields, policybinding.FieldCreatedBy) +// ResetFiles resets all changes to the "files" field. +func (m *TemplateMutation) ResetFiles() { + m.files = nil + delete(m.clearedFields, template.FieldFiles) } -// SetPolicyID sets the "policy_id" field. -func (m *PolicyBindingMutation) SetPolicyID(u uuid.UUID) { - m.policy = &u +// SetBaseTemplate sets the "base_template" field. +func (m *TemplateMutation) SetBaseTemplate(s string) { + m.base_template = &s } -// PolicyID returns the value of the "policy_id" field in the mutation. -func (m *PolicyBindingMutation) PolicyID() (r uuid.UUID, exists bool) { - v := m.policy +// BaseTemplate returns the value of the "base_template" field in the mutation. +func (m *TemplateMutation) BaseTemplate() (r string, exists bool) { + v := m.base_template if v == nil { return } return *v, true } -// OldPolicyID returns the old "policy_id" field's value of the PolicyBinding entity. -// If the PolicyBinding object wasn't provided to the builder, the object is fetched from the database. +// OldBaseTemplate returns the old "base_template" field's value of the Template entity. +// If the Template object wasn't provided to the builder, the object is fetched from the database. // An error is returned if the mutation operation is not UpdateOne, or the database query fails. -func (m *PolicyBindingMutation) OldPolicyID(ctx context.Context) (v *uuid.UUID, err error) { +func (m *TemplateMutation) OldBaseTemplate(ctx context.Context) (v string, err error) { if !m.op.Is(OpUpdateOne) { - return v, errors.New("OldPolicyID is only allowed on UpdateOne operations") + return v, errors.New("OldBaseTemplate is only allowed on UpdateOne operations") } if m.id == nil || m.oldValue == nil { - return v, errors.New("OldPolicyID requires an ID field in the mutation") + return v, errors.New("OldBaseTemplate requires an ID field in the mutation") } oldValue, err := m.oldValue(ctx) if err != nil { - return v, fmt.Errorf("querying old value for OldPolicyID: %w", err) + return v, fmt.Errorf("querying old value for OldBaseTemplate: %w", err) } - return oldValue.PolicyID, nil + return oldValue.BaseTemplate, nil } -// ClearPolicyID clears the value of the "policy_id" field. -func (m *PolicyBindingMutation) ClearPolicyID() { - m.policy = nil - m.clearedFields[policybinding.FieldPolicyID] = struct{}{} +// ClearBaseTemplate clears the value of the "base_template" field. +func (m *TemplateMutation) ClearBaseTemplate() { + m.base_template = nil + m.clearedFields[template.FieldBaseTemplate] = struct{}{} } -// PolicyIDCleared returns if the "policy_id" field was cleared in this mutation. -func (m *PolicyBindingMutation) PolicyIDCleared() bool { - _, ok := m.clearedFields[policybinding.FieldPolicyID] +// BaseTemplateCleared returns if the "base_template" field was cleared in this mutation. +func (m *TemplateMutation) BaseTemplateCleared() bool { + _, ok := m.clearedFields[template.FieldBaseTemplate] return ok } -// ResetPolicyID resets all changes to the "policy_id" field. -func (m *PolicyBindingMutation) ResetPolicyID() { - m.policy = nil - delete(m.clearedFields, policybinding.FieldPolicyID) +// ResetBaseTemplate resets all changes to the "base_template" field. +func (m *TemplateMutation) ResetBaseTemplate() { + m.base_template = nil + delete(m.clearedFields, template.FieldBaseTemplate) } -// SetUserID sets the "user_id" field. -func (m *PolicyBindingMutation) SetUserID(u uuid.UUID) { - m.user = &u +// SetStatus sets the "status" field. +func (m *TemplateMutation) SetStatus(t template.Status) { + m.status = &t } -// UserID returns the value of the "user_id" field in the mutation. -func (m *PolicyBindingMutation) UserID() (r uuid.UUID, exists bool) { - v := m.user +// Status returns the value of the "status" field in the mutation. +func (m *TemplateMutation) Status() (r template.Status, exists bool) { + v := m.status if v == nil { return } return *v, true } -// OldUserID returns the old "user_id" field's value of the PolicyBinding entity. -// If the PolicyBinding object wasn't provided to the builder, the object is fetched from the database. +// OldStatus returns the old "status" field's value of the Template entity. +// If the Template object wasn't provided to the builder, the object is fetched from the database. // An error is returned if the mutation operation is not UpdateOne, or the database query fails. -func (m *PolicyBindingMutation) OldUserID(ctx context.Context) (v *uuid.UUID, err error) { +func (m *TemplateMutation) OldStatus(ctx context.Context) (v template.Status, err error) { if !m.op.Is(OpUpdateOne) { - return v, errors.New("OldUserID is only allowed on UpdateOne operations") + return v, errors.New("OldStatus is only allowed on UpdateOne operations") } if m.id == nil || m.oldValue == nil { - return v, errors.New("OldUserID requires an ID field in the mutation") + return v, errors.New("OldStatus requires an ID field in the mutation") } oldValue, err := m.oldValue(ctx) if err != nil { - return v, fmt.Errorf("querying old value for OldUserID: %w", err) + return v, fmt.Errorf("querying old value for OldStatus: %w", err) } - return oldValue.UserID, nil -} - -// ClearUserID clears the value of the "user_id" field. -func (m *PolicyBindingMutation) ClearUserID() { - m.user = nil - m.clearedFields[policybinding.FieldUserID] = struct{}{} -} - -// UserIDCleared returns if the "user_id" field was cleared in this mutation. -func (m *PolicyBindingMutation) UserIDCleared() bool { - _, ok := m.clearedFields[policybinding.FieldUserID] - return ok + return oldValue.Status, nil } -// ResetUserID resets all changes to the "user_id" field. -func (m *PolicyBindingMutation) ResetUserID() { - m.user = nil - delete(m.clearedFields, policybinding.FieldUserID) +// ResetStatus resets all changes to the "status" field. +func (m *TemplateMutation) ResetStatus() { + m.status = nil } -// SetGroupID sets the "group_id" field. -func (m *PolicyBindingMutation) SetGroupID(u uuid.UUID) { - m.group = &u +// SetOwnerID sets the "owner_id" field. +func (m *TemplateMutation) SetOwnerID(s string) { + m.owner_id = &s } -// GroupID returns the value of the "group_id" field in the mutation. -func (m *PolicyBindingMutation) GroupID() (r uuid.UUID, exists bool) { - v := m.group +// OwnerID returns the value of the "owner_id" field in the mutation. +func (m *TemplateMutation) OwnerID() (r string, exists bool) { + v := m.owner_id if v == nil { return } return *v, true } -// OldGroupID returns the old "group_id" field's value of the PolicyBinding entity. -// If the PolicyBinding object wasn't provided to the builder, the object is fetched from the database. +// OldOwnerID returns the old "owner_id" field's value of the Template entity. +// If the Template object wasn't provided to the builder, the object is fetched from the database. // An error is returned if the mutation operation is not UpdateOne, or the database query fails. -func (m *PolicyBindingMutation) OldGroupID(ctx context.Context) (v *uuid.UUID, err error) { +func (m *TemplateMutation) OldOwnerID(ctx context.Context) (v string, err error) { if !m.op.Is(OpUpdateOne) { - return v, errors.New("OldGroupID is only allowed on UpdateOne operations") + return v, errors.New("OldOwnerID is only allowed on UpdateOne operations") } if m.id == nil || m.oldValue == nil { - return v, errors.New("OldGroupID requires an ID field in the mutation") + return v, errors.New("OldOwnerID requires an ID field in the mutation") } oldValue, err := m.oldValue(ctx) if err != nil { - return v, fmt.Errorf("querying old value for OldGroupID: %w", err) + return v, fmt.Errorf("querying old value for OldOwnerID: %w", err) } - return oldValue.GroupID, nil + return oldValue.OwnerID, nil } -// ClearGroupID clears the value of the "group_id" field. -func (m *PolicyBindingMutation) ClearGroupID() { - m.group = nil - m.clearedFields[policybinding.FieldGroupID] = struct{}{} +// ClearOwnerID clears the value of the "owner_id" field. +func (m *TemplateMutation) ClearOwnerID() { + m.owner_id = nil + m.clearedFields[template.FieldOwnerID] = struct{}{} } -// GroupIDCleared returns if the "group_id" field was cleared in this mutation. -func (m *PolicyBindingMutation) GroupIDCleared() bool { - _, ok := m.clearedFields[policybinding.FieldGroupID] +// OwnerIDCleared returns if the "owner_id" field was cleared in this mutation. +func (m *TemplateMutation) OwnerIDCleared() bool { + _, ok := m.clearedFields[template.FieldOwnerID] return ok } -// ResetGroupID resets all changes to the "group_id" field. -func (m *PolicyBindingMutation) ResetGroupID() { - m.group = nil - delete(m.clearedFields, policybinding.FieldGroupID) +// ResetOwnerID resets all changes to the "owner_id" field. +func (m *TemplateMutation) ResetOwnerID() { + m.owner_id = nil + delete(m.clearedFields, template.FieldOwnerID) } -// SetAgentID sets the "agent_id" field. -func (m *PolicyBindingMutation) SetAgentID(u uuid.UUID) { - m.agent = &u +// SetCreatedBy sets the "created_by" field. +func (m *TemplateMutation) SetCreatedBy(s string) { + m.created_by = &s } -// AgentID returns the value of the "agent_id" field in the mutation. -func (m *PolicyBindingMutation) AgentID() (r uuid.UUID, exists bool) { - v := m.agent +// CreatedBy returns the value of the "created_by" field in the mutation. +func (m *TemplateMutation) CreatedBy() (r string, exists bool) { + v := m.created_by if v == nil { return } return *v, true } -// OldAgentID returns the old "agent_id" field's value of the PolicyBinding entity. -// If the PolicyBinding object wasn't provided to the builder, the object is fetched from the database. +// OldCreatedBy returns the old "created_by" field's value of the Template entity. +// If the Template object wasn't provided to the builder, the object is fetched from the database. // An error is returned if the mutation operation is not UpdateOne, or the database query fails. -func (m *PolicyBindingMutation) OldAgentID(ctx context.Context) (v *uuid.UUID, err error) { +func (m *TemplateMutation) OldCreatedBy(ctx context.Context) (v string, err error) { if !m.op.Is(OpUpdateOne) { - return v, errors.New("OldAgentID is only allowed on UpdateOne operations") + return v, errors.New("OldCreatedBy is only allowed on UpdateOne operations") } if m.id == nil || m.oldValue == nil { - return v, errors.New("OldAgentID requires an ID field in the mutation") + return v, errors.New("OldCreatedBy requires an ID field in the mutation") } oldValue, err := m.oldValue(ctx) if err != nil { - return v, fmt.Errorf("querying old value for OldAgentID: %w", err) + return v, fmt.Errorf("querying old value for OldCreatedBy: %w", err) } - return oldValue.AgentID, nil + return oldValue.CreatedBy, nil } -// ClearAgentID clears the value of the "agent_id" field. -func (m *PolicyBindingMutation) ClearAgentID() { - m.agent = nil - m.clearedFields[policybinding.FieldAgentID] = struct{}{} +// ClearCreatedBy clears the value of the "created_by" field. +func (m *TemplateMutation) ClearCreatedBy() { + m.created_by = nil + m.clearedFields[template.FieldCreatedBy] = struct{}{} } -// AgentIDCleared returns if the "agent_id" field was cleared in this mutation. -func (m *PolicyBindingMutation) AgentIDCleared() bool { - _, ok := m.clearedFields[policybinding.FieldAgentID] +// CreatedByCleared returns if the "created_by" field was cleared in this mutation. +func (m *TemplateMutation) CreatedByCleared() bool { + _, ok := m.clearedFields[template.FieldCreatedBy] return ok } -// ResetAgentID resets all changes to the "agent_id" field. -func (m *PolicyBindingMutation) ResetAgentID() { - m.agent = nil - delete(m.clearedFields, policybinding.FieldAgentID) +// ResetCreatedBy resets all changes to the "created_by" field. +func (m *TemplateMutation) ResetCreatedBy() { + m.created_by = nil + delete(m.clearedFields, template.FieldCreatedBy) } -// ClearPolicy clears the "policy" edge to the AccessPolicy entity. -func (m *PolicyBindingMutation) ClearPolicy() { - m.clearedpolicy = true - m.clearedFields[policybinding.FieldPolicyID] = struct{}{} +// SetUpdatedBy sets the "updated_by" field. +func (m *TemplateMutation) SetUpdatedBy(s string) { + m.updated_by = &s } -// PolicyCleared reports if the "policy" edge to the AccessPolicy entity was cleared. -func (m *PolicyBindingMutation) PolicyCleared() bool { - return m.PolicyIDCleared() || m.clearedpolicy +// UpdatedBy returns the value of the "updated_by" field in the mutation. +func (m *TemplateMutation) UpdatedBy() (r string, exists bool) { + v := m.updated_by + if v == nil { + return + } + return *v, true } -// PolicyIDs returns the "policy" edge IDs in the mutation. -// Note that IDs always returns len(IDs) <= 1 for unique edges, and you should use -// PolicyID instead. It exists only for internal usage by the builders. -func (m *PolicyBindingMutation) PolicyIDs() (ids []uuid.UUID) { - if id := m.policy; id != nil { - ids = append(ids, *id) +// OldUpdatedBy returns the old "updated_by" field's value of the Template entity. +// If the Template object wasn't provided to the builder, the object is fetched from the database. +// An error is returned if the mutation operation is not UpdateOne, or the database query fails. +func (m *TemplateMutation) OldUpdatedBy(ctx context.Context) (v string, err error) { + if !m.op.Is(OpUpdateOne) { + return v, errors.New("OldUpdatedBy is only allowed on UpdateOne operations") } - return + if m.id == nil || m.oldValue == nil { + return v, errors.New("OldUpdatedBy requires an ID field in the mutation") + } + oldValue, err := m.oldValue(ctx) + if err != nil { + return v, fmt.Errorf("querying old value for OldUpdatedBy: %w", err) + } + return oldValue.UpdatedBy, nil } -// ResetPolicy resets all changes to the "policy" edge. -func (m *PolicyBindingMutation) ResetPolicy() { - m.policy = nil - m.clearedpolicy = false +// ClearUpdatedBy clears the value of the "updated_by" field. +func (m *TemplateMutation) ClearUpdatedBy() { + m.updated_by = nil + m.clearedFields[template.FieldUpdatedBy] = struct{}{} } -// ClearUser clears the "user" edge to the User entity. -func (m *PolicyBindingMutation) ClearUser() { - m.cleareduser = true - m.clearedFields[policybinding.FieldUserID] = struct{}{} +// UpdatedByCleared returns if the "updated_by" field was cleared in this mutation. +func (m *TemplateMutation) UpdatedByCleared() bool { + _, ok := m.clearedFields[template.FieldUpdatedBy] + return ok } -// UserCleared reports if the "user" edge to the User entity was cleared. -func (m *PolicyBindingMutation) UserCleared() bool { - return m.UserIDCleared() || m.cleareduser +// ResetUpdatedBy resets all changes to the "updated_by" field. +func (m *TemplateMutation) ResetUpdatedBy() { + m.updated_by = nil + delete(m.clearedFields, template.FieldUpdatedBy) } -// UserIDs returns the "user" edge IDs in the mutation. -// Note that IDs always returns len(IDs) <= 1 for unique edges, and you should use -// UserID instead. It exists only for internal usage by the builders. -func (m *PolicyBindingMutation) UserIDs() (ids []uuid.UUID) { - if id := m.user; id != nil { - ids = append(ids, *id) +// SetVisibility sets the "visibility" field. +func (m *TemplateMutation) SetVisibility(s string) { + m.visibility = &s +} + +// Visibility returns the value of the "visibility" field in the mutation. +func (m *TemplateMutation) Visibility() (r string, exists bool) { + v := m.visibility + if v == nil { + return } - return + return *v, true } -// ResetUser resets all changes to the "user" edge. -func (m *PolicyBindingMutation) ResetUser() { - m.user = nil - m.cleareduser = false +// OldVisibility returns the old "visibility" field's value of the Template entity. +// If the Template object wasn't provided to the builder, the object is fetched from the database. +// An error is returned if the mutation operation is not UpdateOne, or the database query fails. +func (m *TemplateMutation) OldVisibility(ctx context.Context) (v string, err error) { + if !m.op.Is(OpUpdateOne) { + return v, errors.New("OldVisibility is only allowed on UpdateOne operations") + } + if m.id == nil || m.oldValue == nil { + return v, errors.New("OldVisibility requires an ID field in the mutation") + } + oldValue, err := m.oldValue(ctx) + if err != nil { + return v, fmt.Errorf("querying old value for OldVisibility: %w", err) + } + return oldValue.Visibility, nil +} + +// ResetVisibility resets all changes to the "visibility" field. +func (m *TemplateMutation) ResetVisibility() { + m.visibility = nil } -// ClearGroup clears the "group" edge to the Group entity. -func (m *PolicyBindingMutation) ClearGroup() { - m.clearedgroup = true - m.clearedFields[policybinding.FieldGroupID] = struct{}{} +// SetCreated sets the "created" field. +func (m *TemplateMutation) SetCreated(t time.Time) { + m.created = &t } -// GroupCleared reports if the "group" edge to the Group entity was cleared. -func (m *PolicyBindingMutation) GroupCleared() bool { - return m.GroupIDCleared() || m.clearedgroup +// Created returns the value of the "created" field in the mutation. +func (m *TemplateMutation) Created() (r time.Time, exists bool) { + v := m.created + if v == nil { + return + } + return *v, true } -// GroupIDs returns the "group" edge IDs in the mutation. -// Note that IDs always returns len(IDs) <= 1 for unique edges, and you should use -// GroupID instead. It exists only for internal usage by the builders. -func (m *PolicyBindingMutation) GroupIDs() (ids []uuid.UUID) { - if id := m.group; id != nil { - ids = append(ids, *id) +// OldCreated returns the old "created" field's value of the Template entity. +// If the Template object wasn't provided to the builder, the object is fetched from the database. +// An error is returned if the mutation operation is not UpdateOne, or the database query fails. +func (m *TemplateMutation) OldCreated(ctx context.Context) (v time.Time, err error) { + if !m.op.Is(OpUpdateOne) { + return v, errors.New("OldCreated is only allowed on UpdateOne operations") } - return + if m.id == nil || m.oldValue == nil { + return v, errors.New("OldCreated requires an ID field in the mutation") + } + oldValue, err := m.oldValue(ctx) + if err != nil { + return v, fmt.Errorf("querying old value for OldCreated: %w", err) + } + return oldValue.Created, nil } -// ResetGroup resets all changes to the "group" edge. -func (m *PolicyBindingMutation) ResetGroup() { - m.group = nil - m.clearedgroup = false +// ResetCreated resets all changes to the "created" field. +func (m *TemplateMutation) ResetCreated() { + m.created = nil } -// ClearAgent clears the "agent" edge to the Agent entity. -func (m *PolicyBindingMutation) ClearAgent() { - m.clearedagent = true - m.clearedFields[policybinding.FieldAgentID] = struct{}{} +// SetUpdated sets the "updated" field. +func (m *TemplateMutation) SetUpdated(t time.Time) { + m.updated = &t } -// AgentCleared reports if the "agent" edge to the Agent entity was cleared. -func (m *PolicyBindingMutation) AgentCleared() bool { - return m.AgentIDCleared() || m.clearedagent +// Updated returns the value of the "updated" field in the mutation. +func (m *TemplateMutation) Updated() (r time.Time, exists bool) { + v := m.updated + if v == nil { + return + } + return *v, true } -// AgentIDs returns the "agent" edge IDs in the mutation. -// Note that IDs always returns len(IDs) <= 1 for unique edges, and you should use -// AgentID instead. It exists only for internal usage by the builders. -func (m *PolicyBindingMutation) AgentIDs() (ids []uuid.UUID) { - if id := m.agent; id != nil { - ids = append(ids, *id) +// OldUpdated returns the old "updated" field's value of the Template entity. +// If the Template object wasn't provided to the builder, the object is fetched from the database. +// An error is returned if the mutation operation is not UpdateOne, or the database query fails. +func (m *TemplateMutation) OldUpdated(ctx context.Context) (v time.Time, err error) { + if !m.op.Is(OpUpdateOne) { + return v, errors.New("OldUpdated is only allowed on UpdateOne operations") } - return + if m.id == nil || m.oldValue == nil { + return v, errors.New("OldUpdated requires an ID field in the mutation") + } + oldValue, err := m.oldValue(ctx) + if err != nil { + return v, fmt.Errorf("querying old value for OldUpdated: %w", err) + } + return oldValue.Updated, nil } -// ResetAgent resets all changes to the "agent" edge. -func (m *PolicyBindingMutation) ResetAgent() { - m.agent = nil - m.clearedagent = false +// ResetUpdated resets all changes to the "updated" field. +func (m *TemplateMutation) ResetUpdated() { + m.updated = nil } -// Where appends a list predicates to the PolicyBindingMutation builder. -func (m *PolicyBindingMutation) Where(ps ...predicate.PolicyBinding) { +// Where appends a list predicates to the TemplateMutation builder. +func (m *TemplateMutation) Where(ps ...predicate.Template) { m.predicates = append(m.predicates, ps...) } -// WhereP appends storage-level predicates to the PolicyBindingMutation builder. Using this method, +// WhereP appends storage-level predicates to the TemplateMutation builder. Using this method, // users can use type-assertion to append predicates that do not depend on any generated package. -func (m *PolicyBindingMutation) WhereP(ps ...func(*sql.Selector)) { - p := make([]predicate.PolicyBinding, len(ps)) +func (m *TemplateMutation) WhereP(ps ...func(*sql.Selector)) { + p := make([]predicate.Template, len(ps)) for i := range ps { p[i] = ps[i] } @@ -5442,45 +30893,96 @@ func (m *PolicyBindingMutation) WhereP(ps ...func(*sql.Selector)) { } // Op returns the operation name. -func (m *PolicyBindingMutation) Op() Op { +func (m *TemplateMutation) Op() Op { return m.op } // SetOp allows setting the mutation operation. -func (m *PolicyBindingMutation) SetOp(op Op) { +func (m *TemplateMutation) SetOp(op Op) { m.op = op } -// Type returns the node type of this mutation (PolicyBinding). -func (m *PolicyBindingMutation) Type() string { +// Type returns the node type of this mutation (Template). +func (m *TemplateMutation) Type() string { return m.typ } // Fields returns all fields that were changed during this mutation. Note that in // order to get all numeric fields that were incremented/decremented, call // AddedFields(). -func (m *PolicyBindingMutation) Fields() []string { - fields := make([]string, 0, 7) - if m.principal_type != nil { - fields = append(fields, policybinding.FieldPrincipalType) +func (m *TemplateMutation) Fields() []string { + fields := make([]string, 0, 24) + if m.name != nil { + fields = append(fields, template.FieldName) } - if m.created != nil { - fields = append(fields, policybinding.FieldCreated) + if m.slug != nil { + fields = append(fields, template.FieldSlug) + } + if m.display_name != nil { + fields = append(fields, template.FieldDisplayName) + } + if m.description != nil { + fields = append(fields, template.FieldDescription) + } + if m.harness != nil { + fields = append(fields, template.FieldHarness) + } + if m.default_harness_config != nil { + fields = append(fields, template.FieldDefaultHarnessConfig) + } + if m.image != nil { + fields = append(fields, template.FieldImage) + } + if m._config != nil { + fields = append(fields, template.FieldConfig) + } + if m.content_hash != nil { + fields = append(fields, template.FieldContentHash) + } + if m.scope != nil { + fields = append(fields, template.FieldScope) + } + if m.scope_id != nil { + fields = append(fields, template.FieldScopeID) + } + if m.project_id != nil { + fields = append(fields, template.FieldProjectID) + } + if m.storage_uri != nil { + fields = append(fields, template.FieldStorageURI) + } + if m.storage_bucket != nil { + fields = append(fields, template.FieldStorageBucket) + } + if m.storage_path != nil { + fields = append(fields, template.FieldStoragePath) + } + if m.files != nil { + fields = append(fields, template.FieldFiles) + } + if m.base_template != nil { + fields = append(fields, template.FieldBaseTemplate) + } + if m.status != nil { + fields = append(fields, template.FieldStatus) + } + if m.owner_id != nil { + fields = append(fields, template.FieldOwnerID) } if m.created_by != nil { - fields = append(fields, policybinding.FieldCreatedBy) + fields = append(fields, template.FieldCreatedBy) } - if m.policy != nil { - fields = append(fields, policybinding.FieldPolicyID) + if m.updated_by != nil { + fields = append(fields, template.FieldUpdatedBy) } - if m.user != nil { - fields = append(fields, policybinding.FieldUserID) + if m.visibility != nil { + fields = append(fields, template.FieldVisibility) } - if m.group != nil { - fields = append(fields, policybinding.FieldGroupID) + if m.created != nil { + fields = append(fields, template.FieldCreated) } - if m.agent != nil { - fields = append(fields, policybinding.FieldAgentID) + if m.updated != nil { + fields = append(fields, template.FieldUpdated) } return fields } @@ -5488,22 +30990,56 @@ func (m *PolicyBindingMutation) Fields() []string { // Field returns the value of a field with the given name. The second boolean // return value indicates that this field was not set, or was not defined in the // schema. -func (m *PolicyBindingMutation) Field(name string) (ent.Value, bool) { +func (m *TemplateMutation) Field(name string) (ent.Value, bool) { switch name { - case policybinding.FieldPrincipalType: - return m.PrincipalType() - case policybinding.FieldCreated: - return m.Created() - case policybinding.FieldCreatedBy: + case template.FieldName: + return m.Name() + case template.FieldSlug: + return m.Slug() + case template.FieldDisplayName: + return m.DisplayName() + case template.FieldDescription: + return m.Description() + case template.FieldHarness: + return m.Harness() + case template.FieldDefaultHarnessConfig: + return m.DefaultHarnessConfig() + case template.FieldImage: + return m.Image() + case template.FieldConfig: + return m.Config() + case template.FieldContentHash: + return m.ContentHash() + case template.FieldScope: + return m.Scope() + case template.FieldScopeID: + return m.ScopeID() + case template.FieldProjectID: + return m.ProjectID() + case template.FieldStorageURI: + return m.StorageURI() + case template.FieldStorageBucket: + return m.StorageBucket() + case template.FieldStoragePath: + return m.StoragePath() + case template.FieldFiles: + return m.Files() + case template.FieldBaseTemplate: + return m.BaseTemplate() + case template.FieldStatus: + return m.Status() + case template.FieldOwnerID: + return m.OwnerID() + case template.FieldCreatedBy: return m.CreatedBy() - case policybinding.FieldPolicyID: - return m.PolicyID() - case policybinding.FieldUserID: - return m.UserID() - case policybinding.FieldGroupID: - return m.GroupID() - case policybinding.FieldAgentID: - return m.AgentID() + case template.FieldUpdatedBy: + return m.UpdatedBy() + case template.FieldVisibility: + return m.Visibility() + case template.FieldCreated: + return m.Created() + case template.FieldUpdated: + return m.Updated() } return nil, false } @@ -5511,351 +31047,546 @@ func (m *PolicyBindingMutation) Field(name string) (ent.Value, bool) { // OldField returns the old value of the field from the database. An error is // returned if the mutation operation is not UpdateOne, or the query to the // database failed. -func (m *PolicyBindingMutation) OldField(ctx context.Context, name string) (ent.Value, error) { +func (m *TemplateMutation) OldField(ctx context.Context, name string) (ent.Value, error) { switch name { - case policybinding.FieldPrincipalType: - return m.OldPrincipalType(ctx) - case policybinding.FieldCreated: - return m.OldCreated(ctx) - case policybinding.FieldCreatedBy: + case template.FieldName: + return m.OldName(ctx) + case template.FieldSlug: + return m.OldSlug(ctx) + case template.FieldDisplayName: + return m.OldDisplayName(ctx) + case template.FieldDescription: + return m.OldDescription(ctx) + case template.FieldHarness: + return m.OldHarness(ctx) + case template.FieldDefaultHarnessConfig: + return m.OldDefaultHarnessConfig(ctx) + case template.FieldImage: + return m.OldImage(ctx) + case template.FieldConfig: + return m.OldConfig(ctx) + case template.FieldContentHash: + return m.OldContentHash(ctx) + case template.FieldScope: + return m.OldScope(ctx) + case template.FieldScopeID: + return m.OldScopeID(ctx) + case template.FieldProjectID: + return m.OldProjectID(ctx) + case template.FieldStorageURI: + return m.OldStorageURI(ctx) + case template.FieldStorageBucket: + return m.OldStorageBucket(ctx) + case template.FieldStoragePath: + return m.OldStoragePath(ctx) + case template.FieldFiles: + return m.OldFiles(ctx) + case template.FieldBaseTemplate: + return m.OldBaseTemplate(ctx) + case template.FieldStatus: + return m.OldStatus(ctx) + case template.FieldOwnerID: + return m.OldOwnerID(ctx) + case template.FieldCreatedBy: return m.OldCreatedBy(ctx) - case policybinding.FieldPolicyID: - return m.OldPolicyID(ctx) - case policybinding.FieldUserID: - return m.OldUserID(ctx) - case policybinding.FieldGroupID: - return m.OldGroupID(ctx) - case policybinding.FieldAgentID: - return m.OldAgentID(ctx) + case template.FieldUpdatedBy: + return m.OldUpdatedBy(ctx) + case template.FieldVisibility: + return m.OldVisibility(ctx) + case template.FieldCreated: + return m.OldCreated(ctx) + case template.FieldUpdated: + return m.OldUpdated(ctx) } - return nil, fmt.Errorf("unknown PolicyBinding field %s", name) + return nil, fmt.Errorf("unknown Template field %s", name) } // SetField sets the value of a field with the given name. It returns an error if // the field is not defined in the schema, or if the type mismatched the field // type. -func (m *PolicyBindingMutation) SetField(name string, value ent.Value) error { +func (m *TemplateMutation) SetField(name string, value ent.Value) error { switch name { - case policybinding.FieldPrincipalType: - v, ok := value.(policybinding.PrincipalType) + case template.FieldName: + v, ok := value.(string) + if !ok { + return fmt.Errorf("unexpected type %T for field %s", value, name) + } + m.SetName(v) + return nil + case template.FieldSlug: + v, ok := value.(string) + if !ok { + return fmt.Errorf("unexpected type %T for field %s", value, name) + } + m.SetSlug(v) + return nil + case template.FieldDisplayName: + v, ok := value.(string) + if !ok { + return fmt.Errorf("unexpected type %T for field %s", value, name) + } + m.SetDisplayName(v) + return nil + case template.FieldDescription: + v, ok := value.(string) + if !ok { + return fmt.Errorf("unexpected type %T for field %s", value, name) + } + m.SetDescription(v) + return nil + case template.FieldHarness: + v, ok := value.(string) + if !ok { + return fmt.Errorf("unexpected type %T for field %s", value, name) + } + m.SetHarness(v) + return nil + case template.FieldDefaultHarnessConfig: + v, ok := value.(string) + if !ok { + return fmt.Errorf("unexpected type %T for field %s", value, name) + } + m.SetDefaultHarnessConfig(v) + return nil + case template.FieldImage: + v, ok := value.(string) + if !ok { + return fmt.Errorf("unexpected type %T for field %s", value, name) + } + m.SetImage(v) + return nil + case template.FieldConfig: + v, ok := value.(string) + if !ok { + return fmt.Errorf("unexpected type %T for field %s", value, name) + } + m.SetConfig(v) + return nil + case template.FieldContentHash: + v, ok := value.(string) + if !ok { + return fmt.Errorf("unexpected type %T for field %s", value, name) + } + m.SetContentHash(v) + return nil + case template.FieldScope: + v, ok := value.(string) + if !ok { + return fmt.Errorf("unexpected type %T for field %s", value, name) + } + m.SetScope(v) + return nil + case template.FieldScopeID: + v, ok := value.(string) + if !ok { + return fmt.Errorf("unexpected type %T for field %s", value, name) + } + m.SetScopeID(v) + return nil + case template.FieldProjectID: + v, ok := value.(string) + if !ok { + return fmt.Errorf("unexpected type %T for field %s", value, name) + } + m.SetProjectID(v) + return nil + case template.FieldStorageURI: + v, ok := value.(string) + if !ok { + return fmt.Errorf("unexpected type %T for field %s", value, name) + } + m.SetStorageURI(v) + return nil + case template.FieldStorageBucket: + v, ok := value.(string) if !ok { return fmt.Errorf("unexpected type %T for field %s", value, name) } - m.SetPrincipalType(v) + m.SetStorageBucket(v) return nil - case policybinding.FieldCreated: - v, ok := value.(time.Time) + case template.FieldStoragePath: + v, ok := value.(string) if !ok { return fmt.Errorf("unexpected type %T for field %s", value, name) } - m.SetCreated(v) + m.SetStoragePath(v) return nil - case policybinding.FieldCreatedBy: + case template.FieldFiles: + v, ok := value.(string) + if !ok { + return fmt.Errorf("unexpected type %T for field %s", value, name) + } + m.SetFiles(v) + return nil + case template.FieldBaseTemplate: + v, ok := value.(string) + if !ok { + return fmt.Errorf("unexpected type %T for field %s", value, name) + } + m.SetBaseTemplate(v) + return nil + case template.FieldStatus: + v, ok := value.(template.Status) + if !ok { + return fmt.Errorf("unexpected type %T for field %s", value, name) + } + m.SetStatus(v) + return nil + case template.FieldOwnerID: + v, ok := value.(string) + if !ok { + return fmt.Errorf("unexpected type %T for field %s", value, name) + } + m.SetOwnerID(v) + return nil + case template.FieldCreatedBy: v, ok := value.(string) if !ok { return fmt.Errorf("unexpected type %T for field %s", value, name) } m.SetCreatedBy(v) return nil - case policybinding.FieldPolicyID: - v, ok := value.(uuid.UUID) + case template.FieldUpdatedBy: + v, ok := value.(string) if !ok { return fmt.Errorf("unexpected type %T for field %s", value, name) } - m.SetPolicyID(v) + m.SetUpdatedBy(v) return nil - case policybinding.FieldUserID: - v, ok := value.(uuid.UUID) + case template.FieldVisibility: + v, ok := value.(string) if !ok { return fmt.Errorf("unexpected type %T for field %s", value, name) } - m.SetUserID(v) + m.SetVisibility(v) return nil - case policybinding.FieldGroupID: - v, ok := value.(uuid.UUID) + case template.FieldCreated: + v, ok := value.(time.Time) if !ok { return fmt.Errorf("unexpected type %T for field %s", value, name) } - m.SetGroupID(v) + m.SetCreated(v) return nil - case policybinding.FieldAgentID: - v, ok := value.(uuid.UUID) + case template.FieldUpdated: + v, ok := value.(time.Time) if !ok { return fmt.Errorf("unexpected type %T for field %s", value, name) } - m.SetAgentID(v) + m.SetUpdated(v) return nil } - return fmt.Errorf("unknown PolicyBinding field %s", name) + return fmt.Errorf("unknown Template field %s", name) } // AddedFields returns all numeric fields that were incremented/decremented during // this mutation. -func (m *PolicyBindingMutation) AddedFields() []string { +func (m *TemplateMutation) AddedFields() []string { return nil } // AddedField returns the numeric value that was incremented/decremented on a field // with the given name. The second boolean return value indicates that this field // was not set, or was not defined in the schema. -func (m *PolicyBindingMutation) AddedField(name string) (ent.Value, bool) { +func (m *TemplateMutation) AddedField(name string) (ent.Value, bool) { return nil, false } // AddField adds the value to the field with the given name. It returns an error if // the field is not defined in the schema, or if the type mismatched the field // type. -func (m *PolicyBindingMutation) AddField(name string, value ent.Value) error { +func (m *TemplateMutation) AddField(name string, value ent.Value) error { switch name { } - return fmt.Errorf("unknown PolicyBinding numeric field %s", name) + return fmt.Errorf("unknown Template numeric field %s", name) } // ClearedFields returns all nullable fields that were cleared during this // mutation. -func (m *PolicyBindingMutation) ClearedFields() []string { +func (m *TemplateMutation) ClearedFields() []string { var fields []string - if m.FieldCleared(policybinding.FieldCreatedBy) { - fields = append(fields, policybinding.FieldCreatedBy) + if m.FieldCleared(template.FieldDisplayName) { + fields = append(fields, template.FieldDisplayName) } - if m.FieldCleared(policybinding.FieldPolicyID) { - fields = append(fields, policybinding.FieldPolicyID) + if m.FieldCleared(template.FieldDescription) { + fields = append(fields, template.FieldDescription) } - if m.FieldCleared(policybinding.FieldUserID) { - fields = append(fields, policybinding.FieldUserID) + if m.FieldCleared(template.FieldDefaultHarnessConfig) { + fields = append(fields, template.FieldDefaultHarnessConfig) } - if m.FieldCleared(policybinding.FieldGroupID) { - fields = append(fields, policybinding.FieldGroupID) + if m.FieldCleared(template.FieldImage) { + fields = append(fields, template.FieldImage) } - if m.FieldCleared(policybinding.FieldAgentID) { - fields = append(fields, policybinding.FieldAgentID) + if m.FieldCleared(template.FieldConfig) { + fields = append(fields, template.FieldConfig) + } + if m.FieldCleared(template.FieldContentHash) { + fields = append(fields, template.FieldContentHash) + } + if m.FieldCleared(template.FieldScopeID) { + fields = append(fields, template.FieldScopeID) + } + if m.FieldCleared(template.FieldProjectID) { + fields = append(fields, template.FieldProjectID) + } + if m.FieldCleared(template.FieldStorageURI) { + fields = append(fields, template.FieldStorageURI) + } + if m.FieldCleared(template.FieldStorageBucket) { + fields = append(fields, template.FieldStorageBucket) + } + if m.FieldCleared(template.FieldStoragePath) { + fields = append(fields, template.FieldStoragePath) + } + if m.FieldCleared(template.FieldFiles) { + fields = append(fields, template.FieldFiles) + } + if m.FieldCleared(template.FieldBaseTemplate) { + fields = append(fields, template.FieldBaseTemplate) + } + if m.FieldCleared(template.FieldOwnerID) { + fields = append(fields, template.FieldOwnerID) + } + if m.FieldCleared(template.FieldCreatedBy) { + fields = append(fields, template.FieldCreatedBy) + } + if m.FieldCleared(template.FieldUpdatedBy) { + fields = append(fields, template.FieldUpdatedBy) } return fields } // FieldCleared returns a boolean indicating if a field with the given name was // cleared in this mutation. -func (m *PolicyBindingMutation) FieldCleared(name string) bool { +func (m *TemplateMutation) FieldCleared(name string) bool { _, ok := m.clearedFields[name] return ok } // ClearField clears the value of the field with the given name. It returns an // error if the field is not defined in the schema. -func (m *PolicyBindingMutation) ClearField(name string) error { +func (m *TemplateMutation) ClearField(name string) error { switch name { - case policybinding.FieldCreatedBy: - m.ClearCreatedBy() + case template.FieldDisplayName: + m.ClearDisplayName() return nil - case policybinding.FieldPolicyID: - m.ClearPolicyID() + case template.FieldDescription: + m.ClearDescription() return nil - case policybinding.FieldUserID: - m.ClearUserID() + case template.FieldDefaultHarnessConfig: + m.ClearDefaultHarnessConfig() return nil - case policybinding.FieldGroupID: - m.ClearGroupID() + case template.FieldImage: + m.ClearImage() return nil - case policybinding.FieldAgentID: - m.ClearAgentID() + case template.FieldConfig: + m.ClearConfig() + return nil + case template.FieldContentHash: + m.ClearContentHash() + return nil + case template.FieldScopeID: + m.ClearScopeID() + return nil + case template.FieldProjectID: + m.ClearProjectID() + return nil + case template.FieldStorageURI: + m.ClearStorageURI() + return nil + case template.FieldStorageBucket: + m.ClearStorageBucket() + return nil + case template.FieldStoragePath: + m.ClearStoragePath() + return nil + case template.FieldFiles: + m.ClearFiles() + return nil + case template.FieldBaseTemplate: + m.ClearBaseTemplate() + return nil + case template.FieldOwnerID: + m.ClearOwnerID() + return nil + case template.FieldCreatedBy: + m.ClearCreatedBy() + return nil + case template.FieldUpdatedBy: + m.ClearUpdatedBy() return nil } - return fmt.Errorf("unknown PolicyBinding nullable field %s", name) + return fmt.Errorf("unknown Template nullable field %s", name) } // ResetField resets all changes in the mutation for the field with the given name. // It returns an error if the field is not defined in the schema. -func (m *PolicyBindingMutation) ResetField(name string) error { +func (m *TemplateMutation) ResetField(name string) error { switch name { - case policybinding.FieldPrincipalType: - m.ResetPrincipalType() + case template.FieldName: + m.ResetName() return nil - case policybinding.FieldCreated: - m.ResetCreated() + case template.FieldSlug: + m.ResetSlug() return nil - case policybinding.FieldCreatedBy: + case template.FieldDisplayName: + m.ResetDisplayName() + return nil + case template.FieldDescription: + m.ResetDescription() + return nil + case template.FieldHarness: + m.ResetHarness() + return nil + case template.FieldDefaultHarnessConfig: + m.ResetDefaultHarnessConfig() + return nil + case template.FieldImage: + m.ResetImage() + return nil + case template.FieldConfig: + m.ResetConfig() + return nil + case template.FieldContentHash: + m.ResetContentHash() + return nil + case template.FieldScope: + m.ResetScope() + return nil + case template.FieldScopeID: + m.ResetScopeID() + return nil + case template.FieldProjectID: + m.ResetProjectID() + return nil + case template.FieldStorageURI: + m.ResetStorageURI() + return nil + case template.FieldStorageBucket: + m.ResetStorageBucket() + return nil + case template.FieldStoragePath: + m.ResetStoragePath() + return nil + case template.FieldFiles: + m.ResetFiles() + return nil + case template.FieldBaseTemplate: + m.ResetBaseTemplate() + return nil + case template.FieldStatus: + m.ResetStatus() + return nil + case template.FieldOwnerID: + m.ResetOwnerID() + return nil + case template.FieldCreatedBy: m.ResetCreatedBy() return nil - case policybinding.FieldPolicyID: - m.ResetPolicyID() + case template.FieldUpdatedBy: + m.ResetUpdatedBy() return nil - case policybinding.FieldUserID: - m.ResetUserID() + case template.FieldVisibility: + m.ResetVisibility() return nil - case policybinding.FieldGroupID: - m.ResetGroupID() + case template.FieldCreated: + m.ResetCreated() return nil - case policybinding.FieldAgentID: - m.ResetAgentID() + case template.FieldUpdated: + m.ResetUpdated() return nil } - return fmt.Errorf("unknown PolicyBinding field %s", name) + return fmt.Errorf("unknown Template field %s", name) } // AddedEdges returns all edge names that were set/added in this mutation. -func (m *PolicyBindingMutation) AddedEdges() []string { - edges := make([]string, 0, 4) - if m.policy != nil { - edges = append(edges, policybinding.EdgePolicy) - } - if m.user != nil { - edges = append(edges, policybinding.EdgeUser) - } - if m.group != nil { - edges = append(edges, policybinding.EdgeGroup) - } - if m.agent != nil { - edges = append(edges, policybinding.EdgeAgent) - } +func (m *TemplateMutation) AddedEdges() []string { + edges := make([]string, 0, 0) return edges } // AddedIDs returns all IDs (to other nodes) that were added for the given edge // name in this mutation. -func (m *PolicyBindingMutation) AddedIDs(name string) []ent.Value { - switch name { - case policybinding.EdgePolicy: - if id := m.policy; id != nil { - return []ent.Value{*id} - } - case policybinding.EdgeUser: - if id := m.user; id != nil { - return []ent.Value{*id} - } - case policybinding.EdgeGroup: - if id := m.group; id != nil { - return []ent.Value{*id} - } - case policybinding.EdgeAgent: - if id := m.agent; id != nil { - return []ent.Value{*id} - } - } +func (m *TemplateMutation) AddedIDs(name string) []ent.Value { return nil } // RemovedEdges returns all edge names that were removed in this mutation. -func (m *PolicyBindingMutation) RemovedEdges() []string { - edges := make([]string, 0, 4) +func (m *TemplateMutation) RemovedEdges() []string { + edges := make([]string, 0, 0) return edges } // RemovedIDs returns all IDs (to other nodes) that were removed for the edge with // the given name in this mutation. -func (m *PolicyBindingMutation) RemovedIDs(name string) []ent.Value { +func (m *TemplateMutation) RemovedIDs(name string) []ent.Value { return nil } // ClearedEdges returns all edge names that were cleared in this mutation. -func (m *PolicyBindingMutation) ClearedEdges() []string { - edges := make([]string, 0, 4) - if m.clearedpolicy { - edges = append(edges, policybinding.EdgePolicy) - } - if m.cleareduser { - edges = append(edges, policybinding.EdgeUser) - } - if m.clearedgroup { - edges = append(edges, policybinding.EdgeGroup) - } - if m.clearedagent { - edges = append(edges, policybinding.EdgeAgent) - } +func (m *TemplateMutation) ClearedEdges() []string { + edges := make([]string, 0, 0) return edges } // EdgeCleared returns a boolean which indicates if the edge with the given name // was cleared in this mutation. -func (m *PolicyBindingMutation) EdgeCleared(name string) bool { - switch name { - case policybinding.EdgePolicy: - return m.clearedpolicy - case policybinding.EdgeUser: - return m.cleareduser - case policybinding.EdgeGroup: - return m.clearedgroup - case policybinding.EdgeAgent: - return m.clearedagent - } +func (m *TemplateMutation) EdgeCleared(name string) bool { return false } // ClearEdge clears the value of the edge with the given name. It returns an error // if that edge is not defined in the schema. -func (m *PolicyBindingMutation) ClearEdge(name string) error { - switch name { - case policybinding.EdgePolicy: - m.ClearPolicy() - return nil - case policybinding.EdgeUser: - m.ClearUser() - return nil - case policybinding.EdgeGroup: - m.ClearGroup() - return nil - case policybinding.EdgeAgent: - m.ClearAgent() - return nil - } - return fmt.Errorf("unknown PolicyBinding unique edge %s", name) +func (m *TemplateMutation) ClearEdge(name string) error { + return fmt.Errorf("unknown Template unique edge %s", name) } // ResetEdge resets all changes to the edge with the given name in this mutation. -// It returns an error if the edge is not defined in the schema. -func (m *PolicyBindingMutation) ResetEdge(name string) error { - switch name { - case policybinding.EdgePolicy: - m.ResetPolicy() - return nil - case policybinding.EdgeUser: - m.ResetUser() - return nil - case policybinding.EdgeGroup: - m.ResetGroup() - return nil - case policybinding.EdgeAgent: - m.ResetAgent() - return nil - } - return fmt.Errorf("unknown PolicyBinding edge %s", name) -} - -// ProjectMutation represents an operation that mutates the Project nodes in the graph. -type ProjectMutation struct { - config - op Op - typ string - id *uuid.UUID - name *string - slug *string - git_remote *string - labels *map[string]string - annotations *map[string]string - created *time.Time - updated *time.Time - created_by *string - owner_id *string - visibility *string - clearedFields map[string]struct{} - agents map[uuid.UUID]struct{} - removedagents map[uuid.UUID]struct{} - clearedagents bool - done bool - oldValue func(context.Context) (*Project, error) - predicates []predicate.Project +// It returns an error if the edge is not defined in the schema. +func (m *TemplateMutation) ResetEdge(name string) error { + return fmt.Errorf("unknown Template edge %s", name) } -var _ ent.Mutation = (*ProjectMutation)(nil) +// UserMutation represents an operation that mutates the User nodes in the graph. +type UserMutation struct { + config + op Op + typ string + id *uuid.UUID + email *string + display_name *string + avatar_url *string + role *user.Role + status *user.Status + preferences **schema.UserPreferences + created *time.Time + last_login *time.Time + last_seen *time.Time + clearedFields map[string]struct{} + owned_groups map[uuid.UUID]struct{} + removedowned_groups map[uuid.UUID]struct{} + clearedowned_groups bool + memberships map[uuid.UUID]struct{} + removedmemberships map[uuid.UUID]struct{} + clearedmemberships bool + policy_bindings map[uuid.UUID]struct{} + removedpolicy_bindings map[uuid.UUID]struct{} + clearedpolicy_bindings bool + done bool + oldValue func(context.Context) (*User, error) + predicates []predicate.User +} -// projectOption allows management of the mutation configuration using functional options. -type projectOption func(*ProjectMutation) +var _ ent.Mutation = (*UserMutation)(nil) -// newProjectMutation creates new mutation for the Project entity. -func newProjectMutation(c config, op Op, opts ...projectOption) *ProjectMutation { - m := &ProjectMutation{ +// userOption allows management of the mutation configuration using functional options. +type userOption func(*UserMutation) + +// newUserMutation creates new mutation for the User entity. +func newUserMutation(c config, op Op, opts ...userOption) *UserMutation { + m := &UserMutation{ config: c, op: op, - typ: TypeProject, + typ: TypeUser, clearedFields: make(map[string]struct{}), } for _, opt := range opts { @@ -5864,20 +31595,20 @@ func newProjectMutation(c config, op Op, opts ...projectOption) *ProjectMutation return m } -// withProjectID sets the ID field of the mutation. -func withProjectID(id uuid.UUID) projectOption { - return func(m *ProjectMutation) { +// withUserID sets the ID field of the mutation. +func withUserID(id uuid.UUID) userOption { + return func(m *UserMutation) { var ( err error once sync.Once - value *Project + value *User ) - m.oldValue = func(ctx context.Context) (*Project, error) { + m.oldValue = func(ctx context.Context) (*User, error) { once.Do(func() { if m.done { err = errors.New("querying old values post mutation is not allowed") } else { - value, err = m.Client().Project.Get(ctx, id) + value, err = m.Client().User.Get(ctx, id) } }) return value, err @@ -5886,10 +31617,10 @@ func withProjectID(id uuid.UUID) projectOption { } } -// withProject sets the old Project of the mutation. -func withProject(node *Project) projectOption { - return func(m *ProjectMutation) { - m.oldValue = func(context.Context) (*Project, error) { +// withUser sets the old User of the mutation. +func withUser(node *User) userOption { + return func(m *UserMutation) { + m.oldValue = func(context.Context) (*User, error) { return node, nil } m.id = &node.ID @@ -5898,7 +31629,7 @@ func withProject(node *Project) projectOption { // Client returns a new `ent.Client` from the mutation. If the mutation was // executed in a transaction (ent.Tx), a transactional client is returned. -func (m ProjectMutation) Client() *Client { +func (m UserMutation) Client() *Client { client := &Client{config: m.config} client.init() return client @@ -5906,7 +31637,7 @@ func (m ProjectMutation) Client() *Client { // Tx returns an `ent.Tx` for mutations that were executed in transactions; // it returns an error otherwise. -func (m ProjectMutation) Tx() (*Tx, error) { +func (m UserMutation) Tx() (*Tx, error) { if _, ok := m.driver.(*txDriver); !ok { return nil, errors.New("ent: mutation is not running in a transaction") } @@ -5916,14 +31647,14 @@ func (m ProjectMutation) Tx() (*Tx, error) { } // SetID sets the value of the id field. Note that this -// operation is only accepted on creation of Project entities. -func (m *ProjectMutation) SetID(id uuid.UUID) { +// operation is only accepted on creation of User entities. +func (m *UserMutation) SetID(id uuid.UUID) { m.id = &id } // ID returns the ID value in the mutation. Note that the ID is only available // if it was provided to the builder or after it was returned from the database. -func (m *ProjectMutation) ID() (id uuid.UUID, exists bool) { +func (m *UserMutation) ID() (id uuid.UUID, exists bool) { if m.id == nil { return } @@ -5934,7 +31665,7 @@ func (m *ProjectMutation) ID() (id uuid.UUID, exists bool) { // That means, if the mutation is applied within a transaction with an isolation level such // as sql.LevelSerializable, the returned ids match the ids of the rows that will be updated // or updated by the mutation. -func (m *ProjectMutation) IDs(ctx context.Context) ([]uuid.UUID, error) { +func (m *UserMutation) IDs(ctx context.Context) ([]uuid.UUID, error) { switch { case m.op.Is(OpUpdateOne | OpDeleteOne): id, exists := m.ID() @@ -5943,238 +31674,261 @@ func (m *ProjectMutation) IDs(ctx context.Context) ([]uuid.UUID, error) { } fallthrough case m.op.Is(OpUpdate | OpDelete): - return m.Client().Project.Query().Where(m.predicates...).IDs(ctx) + return m.Client().User.Query().Where(m.predicates...).IDs(ctx) default: return nil, fmt.Errorf("IDs is not allowed on %s operations", m.op) } } -// SetName sets the "name" field. -func (m *ProjectMutation) SetName(s string) { - m.name = &s +// SetEmail sets the "email" field. +func (m *UserMutation) SetEmail(s string) { + m.email = &s } -// Name returns the value of the "name" field in the mutation. -func (m *ProjectMutation) Name() (r string, exists bool) { - v := m.name +// Email returns the value of the "email" field in the mutation. +func (m *UserMutation) Email() (r string, exists bool) { + v := m.email if v == nil { return } return *v, true } -// OldName returns the old "name" field's value of the Project entity. -// If the Project object wasn't provided to the builder, the object is fetched from the database. +// OldEmail returns the old "email" field's value of the User entity. +// If the User object wasn't provided to the builder, the object is fetched from the database. // An error is returned if the mutation operation is not UpdateOne, or the database query fails. -func (m *ProjectMutation) OldName(ctx context.Context) (v string, err error) { +func (m *UserMutation) OldEmail(ctx context.Context) (v string, err error) { if !m.op.Is(OpUpdateOne) { - return v, errors.New("OldName is only allowed on UpdateOne operations") + return v, errors.New("OldEmail is only allowed on UpdateOne operations") } if m.id == nil || m.oldValue == nil { - return v, errors.New("OldName requires an ID field in the mutation") + return v, errors.New("OldEmail requires an ID field in the mutation") } oldValue, err := m.oldValue(ctx) if err != nil { - return v, fmt.Errorf("querying old value for OldName: %w", err) + return v, fmt.Errorf("querying old value for OldEmail: %w", err) } - return oldValue.Name, nil + return oldValue.Email, nil } -// ResetName resets all changes to the "name" field. -func (m *ProjectMutation) ResetName() { - m.name = nil +// ResetEmail resets all changes to the "email" field. +func (m *UserMutation) ResetEmail() { + m.email = nil } -// SetSlug sets the "slug" field. -func (m *ProjectMutation) SetSlug(s string) { - m.slug = &s +// SetDisplayName sets the "display_name" field. +func (m *UserMutation) SetDisplayName(s string) { + m.display_name = &s } -// Slug returns the value of the "slug" field in the mutation. -func (m *ProjectMutation) Slug() (r string, exists bool) { - v := m.slug +// DisplayName returns the value of the "display_name" field in the mutation. +func (m *UserMutation) DisplayName() (r string, exists bool) { + v := m.display_name if v == nil { return } return *v, true } -// OldSlug returns the old "slug" field's value of the Project entity. -// If the Project object wasn't provided to the builder, the object is fetched from the database. +// OldDisplayName returns the old "display_name" field's value of the User entity. +// If the User object wasn't provided to the builder, the object is fetched from the database. // An error is returned if the mutation operation is not UpdateOne, or the database query fails. -func (m *ProjectMutation) OldSlug(ctx context.Context) (v string, err error) { +func (m *UserMutation) OldDisplayName(ctx context.Context) (v string, err error) { if !m.op.Is(OpUpdateOne) { - return v, errors.New("OldSlug is only allowed on UpdateOne operations") + return v, errors.New("OldDisplayName is only allowed on UpdateOne operations") } if m.id == nil || m.oldValue == nil { - return v, errors.New("OldSlug requires an ID field in the mutation") + return v, errors.New("OldDisplayName requires an ID field in the mutation") } oldValue, err := m.oldValue(ctx) if err != nil { - return v, fmt.Errorf("querying old value for OldSlug: %w", err) + return v, fmt.Errorf("querying old value for OldDisplayName: %w", err) } - return oldValue.Slug, nil + return oldValue.DisplayName, nil } -// ResetSlug resets all changes to the "slug" field. -func (m *ProjectMutation) ResetSlug() { - m.slug = nil +// ResetDisplayName resets all changes to the "display_name" field. +func (m *UserMutation) ResetDisplayName() { + m.display_name = nil } -// SetGitRemote sets the "git_remote" field. -func (m *ProjectMutation) SetGitRemote(s string) { - m.git_remote = &s +// SetAvatarURL sets the "avatar_url" field. +func (m *UserMutation) SetAvatarURL(s string) { + m.avatar_url = &s } -// GitRemote returns the value of the "git_remote" field in the mutation. -func (m *ProjectMutation) GitRemote() (r string, exists bool) { - v := m.git_remote +// AvatarURL returns the value of the "avatar_url" field in the mutation. +func (m *UserMutation) AvatarURL() (r string, exists bool) { + v := m.avatar_url if v == nil { return } return *v, true } -// OldGitRemote returns the old "git_remote" field's value of the Project entity. -// If the Project object wasn't provided to the builder, the object is fetched from the database. +// OldAvatarURL returns the old "avatar_url" field's value of the User entity. +// If the User object wasn't provided to the builder, the object is fetched from the database. // An error is returned if the mutation operation is not UpdateOne, or the database query fails. -func (m *ProjectMutation) OldGitRemote(ctx context.Context) (v *string, err error) { +func (m *UserMutation) OldAvatarURL(ctx context.Context) (v string, err error) { if !m.op.Is(OpUpdateOne) { - return v, errors.New("OldGitRemote is only allowed on UpdateOne operations") + return v, errors.New("OldAvatarURL is only allowed on UpdateOne operations") } if m.id == nil || m.oldValue == nil { - return v, errors.New("OldGitRemote requires an ID field in the mutation") + return v, errors.New("OldAvatarURL requires an ID field in the mutation") } oldValue, err := m.oldValue(ctx) if err != nil { - return v, fmt.Errorf("querying old value for OldGitRemote: %w", err) + return v, fmt.Errorf("querying old value for OldAvatarURL: %w", err) } - return oldValue.GitRemote, nil + return oldValue.AvatarURL, nil } -// ClearGitRemote clears the value of the "git_remote" field. -func (m *ProjectMutation) ClearGitRemote() { - m.git_remote = nil - m.clearedFields[project.FieldGitRemote] = struct{}{} +// ClearAvatarURL clears the value of the "avatar_url" field. +func (m *UserMutation) ClearAvatarURL() { + m.avatar_url = nil + m.clearedFields[user.FieldAvatarURL] = struct{}{} } -// GitRemoteCleared returns if the "git_remote" field was cleared in this mutation. -func (m *ProjectMutation) GitRemoteCleared() bool { - _, ok := m.clearedFields[project.FieldGitRemote] +// AvatarURLCleared returns if the "avatar_url" field was cleared in this mutation. +func (m *UserMutation) AvatarURLCleared() bool { + _, ok := m.clearedFields[user.FieldAvatarURL] return ok } -// ResetGitRemote resets all changes to the "git_remote" field. -func (m *ProjectMutation) ResetGitRemote() { - m.git_remote = nil - delete(m.clearedFields, project.FieldGitRemote) +// ResetAvatarURL resets all changes to the "avatar_url" field. +func (m *UserMutation) ResetAvatarURL() { + m.avatar_url = nil + delete(m.clearedFields, user.FieldAvatarURL) } -// SetLabels sets the "labels" field. -func (m *ProjectMutation) SetLabels(value map[string]string) { - m.labels = &value +// SetRole sets the "role" field. +func (m *UserMutation) SetRole(u user.Role) { + m.role = &u } -// Labels returns the value of the "labels" field in the mutation. -func (m *ProjectMutation) Labels() (r map[string]string, exists bool) { - v := m.labels +// Role returns the value of the "role" field in the mutation. +func (m *UserMutation) Role() (r user.Role, exists bool) { + v := m.role if v == nil { return } return *v, true } -// OldLabels returns the old "labels" field's value of the Project entity. -// If the Project object wasn't provided to the builder, the object is fetched from the database. +// OldRole returns the old "role" field's value of the User entity. +// If the User object wasn't provided to the builder, the object is fetched from the database. // An error is returned if the mutation operation is not UpdateOne, or the database query fails. -func (m *ProjectMutation) OldLabels(ctx context.Context) (v map[string]string, err error) { +func (m *UserMutation) OldRole(ctx context.Context) (v user.Role, err error) { if !m.op.Is(OpUpdateOne) { - return v, errors.New("OldLabels is only allowed on UpdateOne operations") + return v, errors.New("OldRole is only allowed on UpdateOne operations") } if m.id == nil || m.oldValue == nil { - return v, errors.New("OldLabels requires an ID field in the mutation") + return v, errors.New("OldRole requires an ID field in the mutation") } oldValue, err := m.oldValue(ctx) if err != nil { - return v, fmt.Errorf("querying old value for OldLabels: %w", err) + return v, fmt.Errorf("querying old value for OldRole: %w", err) } - return oldValue.Labels, nil + return oldValue.Role, nil } -// ClearLabels clears the value of the "labels" field. -func (m *ProjectMutation) ClearLabels() { - m.labels = nil - m.clearedFields[project.FieldLabels] = struct{}{} +// ResetRole resets all changes to the "role" field. +func (m *UserMutation) ResetRole() { + m.role = nil } -// LabelsCleared returns if the "labels" field was cleared in this mutation. -func (m *ProjectMutation) LabelsCleared() bool { - _, ok := m.clearedFields[project.FieldLabels] - return ok +// SetStatus sets the "status" field. +func (m *UserMutation) SetStatus(u user.Status) { + m.status = &u +} + +// Status returns the value of the "status" field in the mutation. +func (m *UserMutation) Status() (r user.Status, exists bool) { + v := m.status + if v == nil { + return + } + return *v, true +} + +// OldStatus returns the old "status" field's value of the User entity. +// If the User object wasn't provided to the builder, the object is fetched from the database. +// An error is returned if the mutation operation is not UpdateOne, or the database query fails. +func (m *UserMutation) OldStatus(ctx context.Context) (v user.Status, err error) { + if !m.op.Is(OpUpdateOne) { + return v, errors.New("OldStatus is only allowed on UpdateOne operations") + } + if m.id == nil || m.oldValue == nil { + return v, errors.New("OldStatus requires an ID field in the mutation") + } + oldValue, err := m.oldValue(ctx) + if err != nil { + return v, fmt.Errorf("querying old value for OldStatus: %w", err) + } + return oldValue.Status, nil } -// ResetLabels resets all changes to the "labels" field. -func (m *ProjectMutation) ResetLabels() { - m.labels = nil - delete(m.clearedFields, project.FieldLabels) +// ResetStatus resets all changes to the "status" field. +func (m *UserMutation) ResetStatus() { + m.status = nil } -// SetAnnotations sets the "annotations" field. -func (m *ProjectMutation) SetAnnotations(value map[string]string) { - m.annotations = &value +// SetPreferences sets the "preferences" field. +func (m *UserMutation) SetPreferences(sp *schema.UserPreferences) { + m.preferences = &sp } -// Annotations returns the value of the "annotations" field in the mutation. -func (m *ProjectMutation) Annotations() (r map[string]string, exists bool) { - v := m.annotations +// Preferences returns the value of the "preferences" field in the mutation. +func (m *UserMutation) Preferences() (r *schema.UserPreferences, exists bool) { + v := m.preferences if v == nil { return } return *v, true } -// OldAnnotations returns the old "annotations" field's value of the Project entity. -// If the Project object wasn't provided to the builder, the object is fetched from the database. +// OldPreferences returns the old "preferences" field's value of the User entity. +// If the User object wasn't provided to the builder, the object is fetched from the database. // An error is returned if the mutation operation is not UpdateOne, or the database query fails. -func (m *ProjectMutation) OldAnnotations(ctx context.Context) (v map[string]string, err error) { +func (m *UserMutation) OldPreferences(ctx context.Context) (v *schema.UserPreferences, err error) { if !m.op.Is(OpUpdateOne) { - return v, errors.New("OldAnnotations is only allowed on UpdateOne operations") + return v, errors.New("OldPreferences is only allowed on UpdateOne operations") } if m.id == nil || m.oldValue == nil { - return v, errors.New("OldAnnotations requires an ID field in the mutation") + return v, errors.New("OldPreferences requires an ID field in the mutation") } oldValue, err := m.oldValue(ctx) if err != nil { - return v, fmt.Errorf("querying old value for OldAnnotations: %w", err) + return v, fmt.Errorf("querying old value for OldPreferences: %w", err) } - return oldValue.Annotations, nil + return oldValue.Preferences, nil } -// ClearAnnotations clears the value of the "annotations" field. -func (m *ProjectMutation) ClearAnnotations() { - m.annotations = nil - m.clearedFields[project.FieldAnnotations] = struct{}{} +// ClearPreferences clears the value of the "preferences" field. +func (m *UserMutation) ClearPreferences() { + m.preferences = nil + m.clearedFields[user.FieldPreferences] = struct{}{} } -// AnnotationsCleared returns if the "annotations" field was cleared in this mutation. -func (m *ProjectMutation) AnnotationsCleared() bool { - _, ok := m.clearedFields[project.FieldAnnotations] +// PreferencesCleared returns if the "preferences" field was cleared in this mutation. +func (m *UserMutation) PreferencesCleared() bool { + _, ok := m.clearedFields[user.FieldPreferences] return ok } -// ResetAnnotations resets all changes to the "annotations" field. -func (m *ProjectMutation) ResetAnnotations() { - m.annotations = nil - delete(m.clearedFields, project.FieldAnnotations) +// ResetPreferences resets all changes to the "preferences" field. +func (m *UserMutation) ResetPreferences() { + m.preferences = nil + delete(m.clearedFields, user.FieldPreferences) } // SetCreated sets the "created" field. -func (m *ProjectMutation) SetCreated(t time.Time) { +func (m *UserMutation) SetCreated(t time.Time) { m.created = &t } // Created returns the value of the "created" field in the mutation. -func (m *ProjectMutation) Created() (r time.Time, exists bool) { +func (m *UserMutation) Created() (r time.Time, exists bool) { v := m.created if v == nil { return @@ -6182,10 +31936,10 @@ func (m *ProjectMutation) Created() (r time.Time, exists bool) { return *v, true } -// OldCreated returns the old "created" field's value of the Project entity. -// If the Project object wasn't provided to the builder, the object is fetched from the database. +// OldCreated returns the old "created" field's value of the User entity. +// If the User object wasn't provided to the builder, the object is fetched from the database. // An error is returned if the mutation operation is not UpdateOne, or the database query fails. -func (m *ProjectMutation) OldCreated(ctx context.Context) (v time.Time, err error) { +func (m *UserMutation) OldCreated(ctx context.Context) (v time.Time, err error) { if !m.op.Is(OpUpdateOne) { return v, errors.New("OldCreated is only allowed on UpdateOne operations") } @@ -6200,243 +31954,279 @@ func (m *ProjectMutation) OldCreated(ctx context.Context) (v time.Time, err erro } // ResetCreated resets all changes to the "created" field. -func (m *ProjectMutation) ResetCreated() { +func (m *UserMutation) ResetCreated() { m.created = nil } -// SetUpdated sets the "updated" field. -func (m *ProjectMutation) SetUpdated(t time.Time) { - m.updated = &t +// SetLastLogin sets the "last_login" field. +func (m *UserMutation) SetLastLogin(t time.Time) { + m.last_login = &t } -// Updated returns the value of the "updated" field in the mutation. -func (m *ProjectMutation) Updated() (r time.Time, exists bool) { - v := m.updated +// LastLogin returns the value of the "last_login" field in the mutation. +func (m *UserMutation) LastLogin() (r time.Time, exists bool) { + v := m.last_login if v == nil { return } return *v, true } -// OldUpdated returns the old "updated" field's value of the Project entity. -// If the Project object wasn't provided to the builder, the object is fetched from the database. +// OldLastLogin returns the old "last_login" field's value of the User entity. +// If the User object wasn't provided to the builder, the object is fetched from the database. // An error is returned if the mutation operation is not UpdateOne, or the database query fails. -func (m *ProjectMutation) OldUpdated(ctx context.Context) (v time.Time, err error) { +func (m *UserMutation) OldLastLogin(ctx context.Context) (v *time.Time, err error) { if !m.op.Is(OpUpdateOne) { - return v, errors.New("OldUpdated is only allowed on UpdateOne operations") + return v, errors.New("OldLastLogin is only allowed on UpdateOne operations") } if m.id == nil || m.oldValue == nil { - return v, errors.New("OldUpdated requires an ID field in the mutation") + return v, errors.New("OldLastLogin requires an ID field in the mutation") } oldValue, err := m.oldValue(ctx) if err != nil { - return v, fmt.Errorf("querying old value for OldUpdated: %w", err) + return v, fmt.Errorf("querying old value for OldLastLogin: %w", err) } - return oldValue.Updated, nil + return oldValue.LastLogin, nil } -// ResetUpdated resets all changes to the "updated" field. -func (m *ProjectMutation) ResetUpdated() { - m.updated = nil +// ClearLastLogin clears the value of the "last_login" field. +func (m *UserMutation) ClearLastLogin() { + m.last_login = nil + m.clearedFields[user.FieldLastLogin] = struct{}{} } -// SetCreatedBy sets the "created_by" field. -func (m *ProjectMutation) SetCreatedBy(s string) { - m.created_by = &s +// LastLoginCleared returns if the "last_login" field was cleared in this mutation. +func (m *UserMutation) LastLoginCleared() bool { + _, ok := m.clearedFields[user.FieldLastLogin] + return ok } -// CreatedBy returns the value of the "created_by" field in the mutation. -func (m *ProjectMutation) CreatedBy() (r string, exists bool) { - v := m.created_by +// ResetLastLogin resets all changes to the "last_login" field. +func (m *UserMutation) ResetLastLogin() { + m.last_login = nil + delete(m.clearedFields, user.FieldLastLogin) +} + +// SetLastSeen sets the "last_seen" field. +func (m *UserMutation) SetLastSeen(t time.Time) { + m.last_seen = &t +} + +// LastSeen returns the value of the "last_seen" field in the mutation. +func (m *UserMutation) LastSeen() (r time.Time, exists bool) { + v := m.last_seen if v == nil { return } return *v, true } -// OldCreatedBy returns the old "created_by" field's value of the Project entity. -// If the Project object wasn't provided to the builder, the object is fetched from the database. +// OldLastSeen returns the old "last_seen" field's value of the User entity. +// If the User object wasn't provided to the builder, the object is fetched from the database. // An error is returned if the mutation operation is not UpdateOne, or the database query fails. -func (m *ProjectMutation) OldCreatedBy(ctx context.Context) (v string, err error) { +func (m *UserMutation) OldLastSeen(ctx context.Context) (v *time.Time, err error) { if !m.op.Is(OpUpdateOne) { - return v, errors.New("OldCreatedBy is only allowed on UpdateOne operations") + return v, errors.New("OldLastSeen is only allowed on UpdateOne operations") } if m.id == nil || m.oldValue == nil { - return v, errors.New("OldCreatedBy requires an ID field in the mutation") + return v, errors.New("OldLastSeen requires an ID field in the mutation") } oldValue, err := m.oldValue(ctx) if err != nil { - return v, fmt.Errorf("querying old value for OldCreatedBy: %w", err) + return v, fmt.Errorf("querying old value for OldLastSeen: %w", err) } - return oldValue.CreatedBy, nil + return oldValue.LastSeen, nil } -// ClearCreatedBy clears the value of the "created_by" field. -func (m *ProjectMutation) ClearCreatedBy() { - m.created_by = nil - m.clearedFields[project.FieldCreatedBy] = struct{}{} +// ClearLastSeen clears the value of the "last_seen" field. +func (m *UserMutation) ClearLastSeen() { + m.last_seen = nil + m.clearedFields[user.FieldLastSeen] = struct{}{} } -// CreatedByCleared returns if the "created_by" field was cleared in this mutation. -func (m *ProjectMutation) CreatedByCleared() bool { - _, ok := m.clearedFields[project.FieldCreatedBy] +// LastSeenCleared returns if the "last_seen" field was cleared in this mutation. +func (m *UserMutation) LastSeenCleared() bool { + _, ok := m.clearedFields[user.FieldLastSeen] return ok } -// ResetCreatedBy resets all changes to the "created_by" field. -func (m *ProjectMutation) ResetCreatedBy() { - m.created_by = nil - delete(m.clearedFields, project.FieldCreatedBy) +// ResetLastSeen resets all changes to the "last_seen" field. +func (m *UserMutation) ResetLastSeen() { + m.last_seen = nil + delete(m.clearedFields, user.FieldLastSeen) } -// SetOwnerID sets the "owner_id" field. -func (m *ProjectMutation) SetOwnerID(s string) { - m.owner_id = &s +// AddOwnedGroupIDs adds the "owned_groups" edge to the Group entity by ids. +func (m *UserMutation) AddOwnedGroupIDs(ids ...uuid.UUID) { + if m.owned_groups == nil { + m.owned_groups = make(map[uuid.UUID]struct{}) + } + for i := range ids { + m.owned_groups[ids[i]] = struct{}{} + } } -// OwnerID returns the value of the "owner_id" field in the mutation. -func (m *ProjectMutation) OwnerID() (r string, exists bool) { - v := m.owner_id - if v == nil { - return - } - return *v, true +// ClearOwnedGroups clears the "owned_groups" edge to the Group entity. +func (m *UserMutation) ClearOwnedGroups() { + m.clearedowned_groups = true } -// OldOwnerID returns the old "owner_id" field's value of the Project entity. -// If the Project object wasn't provided to the builder, the object is fetched from the database. -// An error is returned if the mutation operation is not UpdateOne, or the database query fails. -func (m *ProjectMutation) OldOwnerID(ctx context.Context) (v string, err error) { - if !m.op.Is(OpUpdateOne) { - return v, errors.New("OldOwnerID is only allowed on UpdateOne operations") +// OwnedGroupsCleared reports if the "owned_groups" edge to the Group entity was cleared. +func (m *UserMutation) OwnedGroupsCleared() bool { + return m.clearedowned_groups +} + +// RemoveOwnedGroupIDs removes the "owned_groups" edge to the Group entity by IDs. +func (m *UserMutation) RemoveOwnedGroupIDs(ids ...uuid.UUID) { + if m.removedowned_groups == nil { + m.removedowned_groups = make(map[uuid.UUID]struct{}) } - if m.id == nil || m.oldValue == nil { - return v, errors.New("OldOwnerID requires an ID field in the mutation") + for i := range ids { + delete(m.owned_groups, ids[i]) + m.removedowned_groups[ids[i]] = struct{}{} } - oldValue, err := m.oldValue(ctx) - if err != nil { - return v, fmt.Errorf("querying old value for OldOwnerID: %w", err) +} + +// RemovedOwnedGroups returns the removed IDs of the "owned_groups" edge to the Group entity. +func (m *UserMutation) RemovedOwnedGroupsIDs() (ids []uuid.UUID) { + for id := range m.removedowned_groups { + ids = append(ids, id) } - return oldValue.OwnerID, nil + return } -// ClearOwnerID clears the value of the "owner_id" field. -func (m *ProjectMutation) ClearOwnerID() { - m.owner_id = nil - m.clearedFields[project.FieldOwnerID] = struct{}{} +// OwnedGroupsIDs returns the "owned_groups" edge IDs in the mutation. +func (m *UserMutation) OwnedGroupsIDs() (ids []uuid.UUID) { + for id := range m.owned_groups { + ids = append(ids, id) + } + return } -// OwnerIDCleared returns if the "owner_id" field was cleared in this mutation. -func (m *ProjectMutation) OwnerIDCleared() bool { - _, ok := m.clearedFields[project.FieldOwnerID] - return ok +// ResetOwnedGroups resets all changes to the "owned_groups" edge. +func (m *UserMutation) ResetOwnedGroups() { + m.owned_groups = nil + m.clearedowned_groups = false + m.removedowned_groups = nil } -// ResetOwnerID resets all changes to the "owner_id" field. -func (m *ProjectMutation) ResetOwnerID() { - m.owner_id = nil - delete(m.clearedFields, project.FieldOwnerID) +// AddMembershipIDs adds the "memberships" edge to the GroupMembership entity by ids. +func (m *UserMutation) AddMembershipIDs(ids ...uuid.UUID) { + if m.memberships == nil { + m.memberships = make(map[uuid.UUID]struct{}) + } + for i := range ids { + m.memberships[ids[i]] = struct{}{} + } } -// SetVisibility sets the "visibility" field. -func (m *ProjectMutation) SetVisibility(s string) { - m.visibility = &s +// ClearMemberships clears the "memberships" edge to the GroupMembership entity. +func (m *UserMutation) ClearMemberships() { + m.clearedmemberships = true } -// Visibility returns the value of the "visibility" field in the mutation. -func (m *ProjectMutation) Visibility() (r string, exists bool) { - v := m.visibility - if v == nil { - return - } - return *v, true +// MembershipsCleared reports if the "memberships" edge to the GroupMembership entity was cleared. +func (m *UserMutation) MembershipsCleared() bool { + return m.clearedmemberships } -// OldVisibility returns the old "visibility" field's value of the Project entity. -// If the Project object wasn't provided to the builder, the object is fetched from the database. -// An error is returned if the mutation operation is not UpdateOne, or the database query fails. -func (m *ProjectMutation) OldVisibility(ctx context.Context) (v string, err error) { - if !m.op.Is(OpUpdateOne) { - return v, errors.New("OldVisibility is only allowed on UpdateOne operations") +// RemoveMembershipIDs removes the "memberships" edge to the GroupMembership entity by IDs. +func (m *UserMutation) RemoveMembershipIDs(ids ...uuid.UUID) { + if m.removedmemberships == nil { + m.removedmemberships = make(map[uuid.UUID]struct{}) } - if m.id == nil || m.oldValue == nil { - return v, errors.New("OldVisibility requires an ID field in the mutation") + for i := range ids { + delete(m.memberships, ids[i]) + m.removedmemberships[ids[i]] = struct{}{} + } +} + +// RemovedMemberships returns the removed IDs of the "memberships" edge to the GroupMembership entity. +func (m *UserMutation) RemovedMembershipsIDs() (ids []uuid.UUID) { + for id := range m.removedmemberships { + ids = append(ids, id) } - oldValue, err := m.oldValue(ctx) - if err != nil { - return v, fmt.Errorf("querying old value for OldVisibility: %w", err) + return +} + +// MembershipsIDs returns the "memberships" edge IDs in the mutation. +func (m *UserMutation) MembershipsIDs() (ids []uuid.UUID) { + for id := range m.memberships { + ids = append(ids, id) } - return oldValue.Visibility, nil + return } -// ResetVisibility resets all changes to the "visibility" field. -func (m *ProjectMutation) ResetVisibility() { - m.visibility = nil +// ResetMemberships resets all changes to the "memberships" edge. +func (m *UserMutation) ResetMemberships() { + m.memberships = nil + m.clearedmemberships = false + m.removedmemberships = nil } -// AddAgentIDs adds the "agents" edge to the Agent entity by ids. -func (m *ProjectMutation) AddAgentIDs(ids ...uuid.UUID) { - if m.agents == nil { - m.agents = make(map[uuid.UUID]struct{}) +// AddPolicyBindingIDs adds the "policy_bindings" edge to the PolicyBinding entity by ids. +func (m *UserMutation) AddPolicyBindingIDs(ids ...uuid.UUID) { + if m.policy_bindings == nil { + m.policy_bindings = make(map[uuid.UUID]struct{}) } for i := range ids { - m.agents[ids[i]] = struct{}{} + m.policy_bindings[ids[i]] = struct{}{} } } -// ClearAgents clears the "agents" edge to the Agent entity. -func (m *ProjectMutation) ClearAgents() { - m.clearedagents = true +// ClearPolicyBindings clears the "policy_bindings" edge to the PolicyBinding entity. +func (m *UserMutation) ClearPolicyBindings() { + m.clearedpolicy_bindings = true } -// AgentsCleared reports if the "agents" edge to the Agent entity was cleared. -func (m *ProjectMutation) AgentsCleared() bool { - return m.clearedagents +// PolicyBindingsCleared reports if the "policy_bindings" edge to the PolicyBinding entity was cleared. +func (m *UserMutation) PolicyBindingsCleared() bool { + return m.clearedpolicy_bindings } -// RemoveAgentIDs removes the "agents" edge to the Agent entity by IDs. -func (m *ProjectMutation) RemoveAgentIDs(ids ...uuid.UUID) { - if m.removedagents == nil { - m.removedagents = make(map[uuid.UUID]struct{}) +// RemovePolicyBindingIDs removes the "policy_bindings" edge to the PolicyBinding entity by IDs. +func (m *UserMutation) RemovePolicyBindingIDs(ids ...uuid.UUID) { + if m.removedpolicy_bindings == nil { + m.removedpolicy_bindings = make(map[uuid.UUID]struct{}) } for i := range ids { - delete(m.agents, ids[i]) - m.removedagents[ids[i]] = struct{}{} + delete(m.policy_bindings, ids[i]) + m.removedpolicy_bindings[ids[i]] = struct{}{} } } -// RemovedAgents returns the removed IDs of the "agents" edge to the Agent entity. -func (m *ProjectMutation) RemovedAgentsIDs() (ids []uuid.UUID) { - for id := range m.removedagents { +// RemovedPolicyBindings returns the removed IDs of the "policy_bindings" edge to the PolicyBinding entity. +func (m *UserMutation) RemovedPolicyBindingsIDs() (ids []uuid.UUID) { + for id := range m.removedpolicy_bindings { ids = append(ids, id) } return } -// AgentsIDs returns the "agents" edge IDs in the mutation. -func (m *ProjectMutation) AgentsIDs() (ids []uuid.UUID) { - for id := range m.agents { +// PolicyBindingsIDs returns the "policy_bindings" edge IDs in the mutation. +func (m *UserMutation) PolicyBindingsIDs() (ids []uuid.UUID) { + for id := range m.policy_bindings { ids = append(ids, id) } return } -// ResetAgents resets all changes to the "agents" edge. -func (m *ProjectMutation) ResetAgents() { - m.agents = nil - m.clearedagents = false - m.removedagents = nil +// ResetPolicyBindings resets all changes to the "policy_bindings" edge. +func (m *UserMutation) ResetPolicyBindings() { + m.policy_bindings = nil + m.clearedpolicy_bindings = false + m.removedpolicy_bindings = nil } -// Where appends a list predicates to the ProjectMutation builder. -func (m *ProjectMutation) Where(ps ...predicate.Project) { +// Where appends a list predicates to the UserMutation builder. +func (m *UserMutation) Where(ps ...predicate.User) { m.predicates = append(m.predicates, ps...) } -// WhereP appends storage-level predicates to the ProjectMutation builder. Using this method, +// WhereP appends storage-level predicates to the UserMutation builder. Using this method, // users can use type-assertion to append predicates that do not depend on any generated package. -func (m *ProjectMutation) WhereP(ps ...func(*sql.Selector)) { - p := make([]predicate.Project, len(ps)) +func (m *UserMutation) WhereP(ps ...func(*sql.Selector)) { + p := make([]predicate.User, len(ps)) for i := range ps { p[i] = ps[i] } @@ -6444,54 +32234,51 @@ func (m *ProjectMutation) WhereP(ps ...func(*sql.Selector)) { } // Op returns the operation name. -func (m *ProjectMutation) Op() Op { +func (m *UserMutation) Op() Op { return m.op } // SetOp allows setting the mutation operation. -func (m *ProjectMutation) SetOp(op Op) { +func (m *UserMutation) SetOp(op Op) { m.op = op } -// Type returns the node type of this mutation (Project). -func (m *ProjectMutation) Type() string { +// Type returns the node type of this mutation (User). +func (m *UserMutation) Type() string { return m.typ } // Fields returns all fields that were changed during this mutation. Note that in // order to get all numeric fields that were incremented/decremented, call // AddedFields(). -func (m *ProjectMutation) Fields() []string { - fields := make([]string, 0, 10) - if m.name != nil { - fields = append(fields, project.FieldName) - } - if m.slug != nil { - fields = append(fields, project.FieldSlug) +func (m *UserMutation) Fields() []string { + fields := make([]string, 0, 9) + if m.email != nil { + fields = append(fields, user.FieldEmail) } - if m.git_remote != nil { - fields = append(fields, project.FieldGitRemote) + if m.display_name != nil { + fields = append(fields, user.FieldDisplayName) } - if m.labels != nil { - fields = append(fields, project.FieldLabels) + if m.avatar_url != nil { + fields = append(fields, user.FieldAvatarURL) } - if m.annotations != nil { - fields = append(fields, project.FieldAnnotations) + if m.role != nil { + fields = append(fields, user.FieldRole) } - if m.created != nil { - fields = append(fields, project.FieldCreated) + if m.status != nil { + fields = append(fields, user.FieldStatus) } - if m.updated != nil { - fields = append(fields, project.FieldUpdated) + if m.preferences != nil { + fields = append(fields, user.FieldPreferences) } - if m.created_by != nil { - fields = append(fields, project.FieldCreatedBy) + if m.created != nil { + fields = append(fields, user.FieldCreated) } - if m.owner_id != nil { - fields = append(fields, project.FieldOwnerID) + if m.last_login != nil { + fields = append(fields, user.FieldLastLogin) } - if m.visibility != nil { - fields = append(fields, project.FieldVisibility) + if m.last_seen != nil { + fields = append(fields, user.FieldLastSeen) } return fields } @@ -6499,28 +32286,26 @@ func (m *ProjectMutation) Fields() []string { // Field returns the value of a field with the given name. The second boolean // return value indicates that this field was not set, or was not defined in the // schema. -func (m *ProjectMutation) Field(name string) (ent.Value, bool) { +func (m *UserMutation) Field(name string) (ent.Value, bool) { switch name { - case project.FieldName: - return m.Name() - case project.FieldSlug: - return m.Slug() - case project.FieldGitRemote: - return m.GitRemote() - case project.FieldLabels: - return m.Labels() - case project.FieldAnnotations: - return m.Annotations() - case project.FieldCreated: + case user.FieldEmail: + return m.Email() + case user.FieldDisplayName: + return m.DisplayName() + case user.FieldAvatarURL: + return m.AvatarURL() + case user.FieldRole: + return m.Role() + case user.FieldStatus: + return m.Status() + case user.FieldPreferences: + return m.Preferences() + case user.FieldCreated: return m.Created() - case project.FieldUpdated: - return m.Updated() - case project.FieldCreatedBy: - return m.CreatedBy() - case project.FieldOwnerID: - return m.OwnerID() - case project.FieldVisibility: - return m.Visibility() + case user.FieldLastLogin: + return m.LastLogin() + case user.FieldLastSeen: + return m.LastSeen() } return nil, false } @@ -6528,239 +32313,239 @@ func (m *ProjectMutation) Field(name string) (ent.Value, bool) { // OldField returns the old value of the field from the database. An error is // returned if the mutation operation is not UpdateOne, or the query to the // database failed. -func (m *ProjectMutation) OldField(ctx context.Context, name string) (ent.Value, error) { +func (m *UserMutation) OldField(ctx context.Context, name string) (ent.Value, error) { switch name { - case project.FieldName: - return m.OldName(ctx) - case project.FieldSlug: - return m.OldSlug(ctx) - case project.FieldGitRemote: - return m.OldGitRemote(ctx) - case project.FieldLabels: - return m.OldLabels(ctx) - case project.FieldAnnotations: - return m.OldAnnotations(ctx) - case project.FieldCreated: + case user.FieldEmail: + return m.OldEmail(ctx) + case user.FieldDisplayName: + return m.OldDisplayName(ctx) + case user.FieldAvatarURL: + return m.OldAvatarURL(ctx) + case user.FieldRole: + return m.OldRole(ctx) + case user.FieldStatus: + return m.OldStatus(ctx) + case user.FieldPreferences: + return m.OldPreferences(ctx) + case user.FieldCreated: return m.OldCreated(ctx) - case project.FieldUpdated: - return m.OldUpdated(ctx) - case project.FieldCreatedBy: - return m.OldCreatedBy(ctx) - case project.FieldOwnerID: - return m.OldOwnerID(ctx) - case project.FieldVisibility: - return m.OldVisibility(ctx) + case user.FieldLastLogin: + return m.OldLastLogin(ctx) + case user.FieldLastSeen: + return m.OldLastSeen(ctx) } - return nil, fmt.Errorf("unknown Project field %s", name) + return nil, fmt.Errorf("unknown User field %s", name) } // SetField sets the value of a field with the given name. It returns an error if // the field is not defined in the schema, or if the type mismatched the field // type. -func (m *ProjectMutation) SetField(name string, value ent.Value) error { +func (m *UserMutation) SetField(name string, value ent.Value) error { switch name { - case project.FieldName: + case user.FieldEmail: v, ok := value.(string) if !ok { return fmt.Errorf("unexpected type %T for field %s", value, name) } - m.SetName(v) + m.SetEmail(v) return nil - case project.FieldSlug: + case user.FieldDisplayName: v, ok := value.(string) if !ok { return fmt.Errorf("unexpected type %T for field %s", value, name) } - m.SetSlug(v) + m.SetDisplayName(v) return nil - case project.FieldGitRemote: + case user.FieldAvatarURL: v, ok := value.(string) if !ok { return fmt.Errorf("unexpected type %T for field %s", value, name) } - m.SetGitRemote(v) + m.SetAvatarURL(v) return nil - case project.FieldLabels: - v, ok := value.(map[string]string) + case user.FieldRole: + v, ok := value.(user.Role) if !ok { return fmt.Errorf("unexpected type %T for field %s", value, name) } - m.SetLabels(v) + m.SetRole(v) return nil - case project.FieldAnnotations: - v, ok := value.(map[string]string) + case user.FieldStatus: + v, ok := value.(user.Status) if !ok { return fmt.Errorf("unexpected type %T for field %s", value, name) } - m.SetAnnotations(v) + m.SetStatus(v) return nil - case project.FieldCreated: - v, ok := value.(time.Time) + case user.FieldPreferences: + v, ok := value.(*schema.UserPreferences) if !ok { return fmt.Errorf("unexpected type %T for field %s", value, name) } - m.SetCreated(v) + m.SetPreferences(v) return nil - case project.FieldUpdated: + case user.FieldCreated: v, ok := value.(time.Time) if !ok { return fmt.Errorf("unexpected type %T for field %s", value, name) } - m.SetUpdated(v) - return nil - case project.FieldCreatedBy: - v, ok := value.(string) - if !ok { - return fmt.Errorf("unexpected type %T for field %s", value, name) - } - m.SetCreatedBy(v) - return nil - case project.FieldOwnerID: - v, ok := value.(string) + m.SetCreated(v) + return nil + case user.FieldLastLogin: + v, ok := value.(time.Time) if !ok { return fmt.Errorf("unexpected type %T for field %s", value, name) } - m.SetOwnerID(v) + m.SetLastLogin(v) return nil - case project.FieldVisibility: - v, ok := value.(string) + case user.FieldLastSeen: + v, ok := value.(time.Time) if !ok { return fmt.Errorf("unexpected type %T for field %s", value, name) } - m.SetVisibility(v) + m.SetLastSeen(v) return nil } - return fmt.Errorf("unknown Project field %s", name) + return fmt.Errorf("unknown User field %s", name) } // AddedFields returns all numeric fields that were incremented/decremented during // this mutation. -func (m *ProjectMutation) AddedFields() []string { +func (m *UserMutation) AddedFields() []string { return nil } // AddedField returns the numeric value that was incremented/decremented on a field // with the given name. The second boolean return value indicates that this field // was not set, or was not defined in the schema. -func (m *ProjectMutation) AddedField(name string) (ent.Value, bool) { +func (m *UserMutation) AddedField(name string) (ent.Value, bool) { return nil, false } // AddField adds the value to the field with the given name. It returns an error if // the field is not defined in the schema, or if the type mismatched the field // type. -func (m *ProjectMutation) AddField(name string, value ent.Value) error { +func (m *UserMutation) AddField(name string, value ent.Value) error { switch name { } - return fmt.Errorf("unknown Project numeric field %s", name) + return fmt.Errorf("unknown User numeric field %s", name) } // ClearedFields returns all nullable fields that were cleared during this // mutation. -func (m *ProjectMutation) ClearedFields() []string { +func (m *UserMutation) ClearedFields() []string { var fields []string - if m.FieldCleared(project.FieldGitRemote) { - fields = append(fields, project.FieldGitRemote) - } - if m.FieldCleared(project.FieldLabels) { - fields = append(fields, project.FieldLabels) + if m.FieldCleared(user.FieldAvatarURL) { + fields = append(fields, user.FieldAvatarURL) } - if m.FieldCleared(project.FieldAnnotations) { - fields = append(fields, project.FieldAnnotations) + if m.FieldCleared(user.FieldPreferences) { + fields = append(fields, user.FieldPreferences) } - if m.FieldCleared(project.FieldCreatedBy) { - fields = append(fields, project.FieldCreatedBy) + if m.FieldCleared(user.FieldLastLogin) { + fields = append(fields, user.FieldLastLogin) } - if m.FieldCleared(project.FieldOwnerID) { - fields = append(fields, project.FieldOwnerID) + if m.FieldCleared(user.FieldLastSeen) { + fields = append(fields, user.FieldLastSeen) } return fields } // FieldCleared returns a boolean indicating if a field with the given name was // cleared in this mutation. -func (m *ProjectMutation) FieldCleared(name string) bool { +func (m *UserMutation) FieldCleared(name string) bool { _, ok := m.clearedFields[name] return ok } // ClearField clears the value of the field with the given name. It returns an // error if the field is not defined in the schema. -func (m *ProjectMutation) ClearField(name string) error { +func (m *UserMutation) ClearField(name string) error { switch name { - case project.FieldGitRemote: - m.ClearGitRemote() - return nil - case project.FieldLabels: - m.ClearLabels() + case user.FieldAvatarURL: + m.ClearAvatarURL() return nil - case project.FieldAnnotations: - m.ClearAnnotations() + case user.FieldPreferences: + m.ClearPreferences() return nil - case project.FieldCreatedBy: - m.ClearCreatedBy() + case user.FieldLastLogin: + m.ClearLastLogin() return nil - case project.FieldOwnerID: - m.ClearOwnerID() + case user.FieldLastSeen: + m.ClearLastSeen() return nil } - return fmt.Errorf("unknown Project nullable field %s", name) + return fmt.Errorf("unknown User nullable field %s", name) } // ResetField resets all changes in the mutation for the field with the given name. // It returns an error if the field is not defined in the schema. -func (m *ProjectMutation) ResetField(name string) error { +func (m *UserMutation) ResetField(name string) error { switch name { - case project.FieldName: - m.ResetName() - return nil - case project.FieldSlug: - m.ResetSlug() + case user.FieldEmail: + m.ResetEmail() return nil - case project.FieldGitRemote: - m.ResetGitRemote() + case user.FieldDisplayName: + m.ResetDisplayName() return nil - case project.FieldLabels: - m.ResetLabels() + case user.FieldAvatarURL: + m.ResetAvatarURL() return nil - case project.FieldAnnotations: - m.ResetAnnotations() + case user.FieldRole: + m.ResetRole() return nil - case project.FieldCreated: - m.ResetCreated() + case user.FieldStatus: + m.ResetStatus() return nil - case project.FieldUpdated: - m.ResetUpdated() + case user.FieldPreferences: + m.ResetPreferences() return nil - case project.FieldCreatedBy: - m.ResetCreatedBy() + case user.FieldCreated: + m.ResetCreated() return nil - case project.FieldOwnerID: - m.ResetOwnerID() + case user.FieldLastLogin: + m.ResetLastLogin() return nil - case project.FieldVisibility: - m.ResetVisibility() + case user.FieldLastSeen: + m.ResetLastSeen() return nil } - return fmt.Errorf("unknown Project field %s", name) + return fmt.Errorf("unknown User field %s", name) } // AddedEdges returns all edge names that were set/added in this mutation. -func (m *ProjectMutation) AddedEdges() []string { - edges := make([]string, 0, 1) - if m.agents != nil { - edges = append(edges, project.EdgeAgents) +func (m *UserMutation) AddedEdges() []string { + edges := make([]string, 0, 3) + if m.owned_groups != nil { + edges = append(edges, user.EdgeOwnedGroups) + } + if m.memberships != nil { + edges = append(edges, user.EdgeMemberships) + } + if m.policy_bindings != nil { + edges = append(edges, user.EdgePolicyBindings) } return edges } // AddedIDs returns all IDs (to other nodes) that were added for the given edge // name in this mutation. -func (m *ProjectMutation) AddedIDs(name string) []ent.Value { +func (m *UserMutation) AddedIDs(name string) []ent.Value { switch name { - case project.EdgeAgents: - ids := make([]ent.Value, 0, len(m.agents)) - for id := range m.agents { + case user.EdgeOwnedGroups: + ids := make([]ent.Value, 0, len(m.owned_groups)) + for id := range m.owned_groups { + ids = append(ids, id) + } + return ids + case user.EdgeMemberships: + ids := make([]ent.Value, 0, len(m.memberships)) + for id := range m.memberships { + ids = append(ids, id) + } + return ids + case user.EdgePolicyBindings: + ids := make([]ent.Value, 0, len(m.policy_bindings)) + for id := range m.policy_bindings { ids = append(ids, id) } return ids @@ -6769,21 +32554,39 @@ func (m *ProjectMutation) AddedIDs(name string) []ent.Value { } // RemovedEdges returns all edge names that were removed in this mutation. -func (m *ProjectMutation) RemovedEdges() []string { - edges := make([]string, 0, 1) - if m.removedagents != nil { - edges = append(edges, project.EdgeAgents) +func (m *UserMutation) RemovedEdges() []string { + edges := make([]string, 0, 3) + if m.removedowned_groups != nil { + edges = append(edges, user.EdgeOwnedGroups) + } + if m.removedmemberships != nil { + edges = append(edges, user.EdgeMemberships) + } + if m.removedpolicy_bindings != nil { + edges = append(edges, user.EdgePolicyBindings) } return edges } // RemovedIDs returns all IDs (to other nodes) that were removed for the edge with // the given name in this mutation. -func (m *ProjectMutation) RemovedIDs(name string) []ent.Value { +func (m *UserMutation) RemovedIDs(name string) []ent.Value { switch name { - case project.EdgeAgents: - ids := make([]ent.Value, 0, len(m.removedagents)) - for id := range m.removedagents { + case user.EdgeOwnedGroups: + ids := make([]ent.Value, 0, len(m.removedowned_groups)) + for id := range m.removedowned_groups { + ids = append(ids, id) + } + return ids + case user.EdgeMemberships: + ids := make([]ent.Value, 0, len(m.removedmemberships)) + for id := range m.removedmemberships { + ids = append(ids, id) + } + return ids + case user.EdgePolicyBindings: + ids := make([]ent.Value, 0, len(m.removedpolicy_bindings)) + for id := range m.removedpolicy_bindings { ids = append(ids, id) } return ids @@ -6792,89 +32595,92 @@ func (m *ProjectMutation) RemovedIDs(name string) []ent.Value { } // ClearedEdges returns all edge names that were cleared in this mutation. -func (m *ProjectMutation) ClearedEdges() []string { - edges := make([]string, 0, 1) - if m.clearedagents { - edges = append(edges, project.EdgeAgents) +func (m *UserMutation) ClearedEdges() []string { + edges := make([]string, 0, 3) + if m.clearedowned_groups { + edges = append(edges, user.EdgeOwnedGroups) + } + if m.clearedmemberships { + edges = append(edges, user.EdgeMemberships) + } + if m.clearedpolicy_bindings { + edges = append(edges, user.EdgePolicyBindings) } return edges } // EdgeCleared returns a boolean which indicates if the edge with the given name // was cleared in this mutation. -func (m *ProjectMutation) EdgeCleared(name string) bool { +func (m *UserMutation) EdgeCleared(name string) bool { switch name { - case project.EdgeAgents: - return m.clearedagents + case user.EdgeOwnedGroups: + return m.clearedowned_groups + case user.EdgeMemberships: + return m.clearedmemberships + case user.EdgePolicyBindings: + return m.clearedpolicy_bindings } return false } // ClearEdge clears the value of the edge with the given name. It returns an error // if that edge is not defined in the schema. -func (m *ProjectMutation) ClearEdge(name string) error { +func (m *UserMutation) ClearEdge(name string) error { switch name { } - return fmt.Errorf("unknown Project unique edge %s", name) + return fmt.Errorf("unknown User unique edge %s", name) } // ResetEdge resets all changes to the edge with the given name in this mutation. // It returns an error if the edge is not defined in the schema. -func (m *ProjectMutation) ResetEdge(name string) error { +func (m *UserMutation) ResetEdge(name string) error { switch name { - case project.EdgeAgents: - m.ResetAgents() + case user.EdgeOwnedGroups: + m.ResetOwnedGroups() + return nil + case user.EdgeMemberships: + m.ResetMemberships() + return nil + case user.EdgePolicyBindings: + m.ResetPolicyBindings() return nil } - return fmt.Errorf("unknown Project edge %s", name) + return fmt.Errorf("unknown User edge %s", name) } -// UserMutation represents an operation that mutates the User nodes in the graph. -type UserMutation struct { +// UserAccessTokenMutation represents an operation that mutates the UserAccessToken nodes in the graph. +type UserAccessTokenMutation struct { config - op Op - typ string - id *uuid.UUID - email *string - display_name *string - avatar_url *string - role *user.Role - status *user.Status - preferences **schema.UserPreferences - created *time.Time - last_login *time.Time - clearedFields map[string]struct{} - created_agents map[uuid.UUID]struct{} - removedcreated_agents map[uuid.UUID]struct{} - clearedcreated_agents bool - owned_agents map[uuid.UUID]struct{} - removedowned_agents map[uuid.UUID]struct{} - clearedowned_agents bool - owned_groups map[uuid.UUID]struct{} - removedowned_groups map[uuid.UUID]struct{} - clearedowned_groups bool - memberships map[uuid.UUID]struct{} - removedmemberships map[uuid.UUID]struct{} - clearedmemberships bool - policy_bindings map[uuid.UUID]struct{} - removedpolicy_bindings map[uuid.UUID]struct{} - clearedpolicy_bindings bool - done bool - oldValue func(context.Context) (*User, error) - predicates []predicate.User + op Op + typ string + id *uuid.UUID + user_id *uuid.UUID + name *string + prefix *string + key_hash *string + project_id *uuid.UUID + scopes *string + revoked *bool + expires_at *time.Time + last_used *time.Time + created *time.Time + clearedFields map[string]struct{} + done bool + oldValue func(context.Context) (*UserAccessToken, error) + predicates []predicate.UserAccessToken } -var _ ent.Mutation = (*UserMutation)(nil) +var _ ent.Mutation = (*UserAccessTokenMutation)(nil) -// userOption allows management of the mutation configuration using functional options. -type userOption func(*UserMutation) +// useraccesstokenOption allows management of the mutation configuration using functional options. +type useraccesstokenOption func(*UserAccessTokenMutation) -// newUserMutation creates new mutation for the User entity. -func newUserMutation(c config, op Op, opts ...userOption) *UserMutation { - m := &UserMutation{ +// newUserAccessTokenMutation creates new mutation for the UserAccessToken entity. +func newUserAccessTokenMutation(c config, op Op, opts ...useraccesstokenOption) *UserAccessTokenMutation { + m := &UserAccessTokenMutation{ config: c, op: op, - typ: TypeUser, + typ: TypeUserAccessToken, clearedFields: make(map[string]struct{}), } for _, opt := range opts { @@ -6883,20 +32689,20 @@ func newUserMutation(c config, op Op, opts ...userOption) *UserMutation { return m } -// withUserID sets the ID field of the mutation. -func withUserID(id uuid.UUID) userOption { - return func(m *UserMutation) { +// withUserAccessTokenID sets the ID field of the mutation. +func withUserAccessTokenID(id uuid.UUID) useraccesstokenOption { + return func(m *UserAccessTokenMutation) { var ( err error once sync.Once - value *User + value *UserAccessToken ) - m.oldValue = func(ctx context.Context) (*User, error) { + m.oldValue = func(ctx context.Context) (*UserAccessToken, error) { once.Do(func() { if m.done { err = errors.New("querying old values post mutation is not allowed") } else { - value, err = m.Client().User.Get(ctx, id) + value, err = m.Client().UserAccessToken.Get(ctx, id) } }) return value, err @@ -6905,10 +32711,10 @@ func withUserID(id uuid.UUID) userOption { } } -// withUser sets the old User of the mutation. -func withUser(node *User) userOption { - return func(m *UserMutation) { - m.oldValue = func(context.Context) (*User, error) { +// withUserAccessToken sets the old UserAccessToken of the mutation. +func withUserAccessToken(node *UserAccessToken) useraccesstokenOption { + return func(m *UserAccessTokenMutation) { + m.oldValue = func(context.Context) (*UserAccessToken, error) { return node, nil } m.id = &node.ID @@ -6917,7 +32723,7 @@ func withUser(node *User) userOption { // Client returns a new `ent.Client` from the mutation. If the mutation was // executed in a transaction (ent.Tx), a transactional client is returned. -func (m UserMutation) Client() *Client { +func (m UserAccessTokenMutation) Client() *Client { client := &Client{config: m.config} client.init() return client @@ -6925,7 +32731,7 @@ func (m UserMutation) Client() *Client { // Tx returns an `ent.Tx` for mutations that were executed in transactions; // it returns an error otherwise. -func (m UserMutation) Tx() (*Tx, error) { +func (m UserAccessTokenMutation) Tx() (*Tx, error) { if _, ok := m.driver.(*txDriver); !ok { return nil, errors.New("ent: mutation is not running in a transaction") } @@ -6935,14 +32741,14 @@ func (m UserMutation) Tx() (*Tx, error) { } // SetID sets the value of the id field. Note that this -// operation is only accepted on creation of User entities. -func (m *UserMutation) SetID(id uuid.UUID) { +// operation is only accepted on creation of UserAccessToken entities. +func (m *UserAccessTokenMutation) SetID(id uuid.UUID) { m.id = &id } // ID returns the ID value in the mutation. Note that the ID is only available // if it was provided to the builder or after it was returned from the database. -func (m *UserMutation) ID() (id uuid.UUID, exists bool) { +func (m *UserAccessTokenMutation) ID() (id uuid.UUID, exists bool) { if m.id == nil { return } @@ -6953,7 +32759,7 @@ func (m *UserMutation) ID() (id uuid.UUID, exists bool) { // That means, if the mutation is applied within a transaction with an isolation level such // as sql.LevelSerializable, the returned ids match the ids of the rows that will be updated // or updated by the mutation. -func (m *UserMutation) IDs(ctx context.Context) ([]uuid.UUID, error) { +func (m *UserAccessTokenMutation) IDs(ctx context.Context) ([]uuid.UUID, error) { switch { case m.op.Is(OpUpdateOne | OpDeleteOne): id, exists := m.ID() @@ -6962,618 +32768,407 @@ func (m *UserMutation) IDs(ctx context.Context) ([]uuid.UUID, error) { } fallthrough case m.op.Is(OpUpdate | OpDelete): - return m.Client().User.Query().Where(m.predicates...).IDs(ctx) + return m.Client().UserAccessToken.Query().Where(m.predicates...).IDs(ctx) default: return nil, fmt.Errorf("IDs is not allowed on %s operations", m.op) } } -// SetEmail sets the "email" field. -func (m *UserMutation) SetEmail(s string) { - m.email = &s -} - -// Email returns the value of the "email" field in the mutation. -func (m *UserMutation) Email() (r string, exists bool) { - v := m.email - if v == nil { - return - } - return *v, true -} - -// OldEmail returns the old "email" field's value of the User entity. -// If the User object wasn't provided to the builder, the object is fetched from the database. -// An error is returned if the mutation operation is not UpdateOne, or the database query fails. -func (m *UserMutation) OldEmail(ctx context.Context) (v string, err error) { - if !m.op.Is(OpUpdateOne) { - return v, errors.New("OldEmail is only allowed on UpdateOne operations") - } - if m.id == nil || m.oldValue == nil { - return v, errors.New("OldEmail requires an ID field in the mutation") - } - oldValue, err := m.oldValue(ctx) - if err != nil { - return v, fmt.Errorf("querying old value for OldEmail: %w", err) - } - return oldValue.Email, nil -} - -// ResetEmail resets all changes to the "email" field. -func (m *UserMutation) ResetEmail() { - m.email = nil -} - -// SetDisplayName sets the "display_name" field. -func (m *UserMutation) SetDisplayName(s string) { - m.display_name = &s -} - -// DisplayName returns the value of the "display_name" field in the mutation. -func (m *UserMutation) DisplayName() (r string, exists bool) { - v := m.display_name - if v == nil { - return - } - return *v, true -} - -// OldDisplayName returns the old "display_name" field's value of the User entity. -// If the User object wasn't provided to the builder, the object is fetched from the database. -// An error is returned if the mutation operation is not UpdateOne, or the database query fails. -func (m *UserMutation) OldDisplayName(ctx context.Context) (v string, err error) { - if !m.op.Is(OpUpdateOne) { - return v, errors.New("OldDisplayName is only allowed on UpdateOne operations") - } - if m.id == nil || m.oldValue == nil { - return v, errors.New("OldDisplayName requires an ID field in the mutation") - } - oldValue, err := m.oldValue(ctx) - if err != nil { - return v, fmt.Errorf("querying old value for OldDisplayName: %w", err) - } - return oldValue.DisplayName, nil -} - -// ResetDisplayName resets all changes to the "display_name" field. -func (m *UserMutation) ResetDisplayName() { - m.display_name = nil -} - -// SetAvatarURL sets the "avatar_url" field. -func (m *UserMutation) SetAvatarURL(s string) { - m.avatar_url = &s -} - -// AvatarURL returns the value of the "avatar_url" field in the mutation. -func (m *UserMutation) AvatarURL() (r string, exists bool) { - v := m.avatar_url - if v == nil { - return - } - return *v, true -} - -// OldAvatarURL returns the old "avatar_url" field's value of the User entity. -// If the User object wasn't provided to the builder, the object is fetched from the database. -// An error is returned if the mutation operation is not UpdateOne, or the database query fails. -func (m *UserMutation) OldAvatarURL(ctx context.Context) (v string, err error) { - if !m.op.Is(OpUpdateOne) { - return v, errors.New("OldAvatarURL is only allowed on UpdateOne operations") - } - if m.id == nil || m.oldValue == nil { - return v, errors.New("OldAvatarURL requires an ID field in the mutation") - } - oldValue, err := m.oldValue(ctx) - if err != nil { - return v, fmt.Errorf("querying old value for OldAvatarURL: %w", err) - } - return oldValue.AvatarURL, nil -} - -// ClearAvatarURL clears the value of the "avatar_url" field. -func (m *UserMutation) ClearAvatarURL() { - m.avatar_url = nil - m.clearedFields[user.FieldAvatarURL] = struct{}{} -} - -// AvatarURLCleared returns if the "avatar_url" field was cleared in this mutation. -func (m *UserMutation) AvatarURLCleared() bool { - _, ok := m.clearedFields[user.FieldAvatarURL] - return ok -} - -// ResetAvatarURL resets all changes to the "avatar_url" field. -func (m *UserMutation) ResetAvatarURL() { - m.avatar_url = nil - delete(m.clearedFields, user.FieldAvatarURL) -} - -// SetRole sets the "role" field. -func (m *UserMutation) SetRole(u user.Role) { - m.role = &u -} - -// Role returns the value of the "role" field in the mutation. -func (m *UserMutation) Role() (r user.Role, exists bool) { - v := m.role - if v == nil { - return - } - return *v, true -} - -// OldRole returns the old "role" field's value of the User entity. -// If the User object wasn't provided to the builder, the object is fetched from the database. -// An error is returned if the mutation operation is not UpdateOne, or the database query fails. -func (m *UserMutation) OldRole(ctx context.Context) (v user.Role, err error) { - if !m.op.Is(OpUpdateOne) { - return v, errors.New("OldRole is only allowed on UpdateOne operations") - } - if m.id == nil || m.oldValue == nil { - return v, errors.New("OldRole requires an ID field in the mutation") - } - oldValue, err := m.oldValue(ctx) - if err != nil { - return v, fmt.Errorf("querying old value for OldRole: %w", err) - } - return oldValue.Role, nil -} - -// ResetRole resets all changes to the "role" field. -func (m *UserMutation) ResetRole() { - m.role = nil -} - -// SetStatus sets the "status" field. -func (m *UserMutation) SetStatus(u user.Status) { - m.status = &u +// SetUserID sets the "user_id" field. +func (m *UserAccessTokenMutation) SetUserID(u uuid.UUID) { + m.user_id = &u } -// Status returns the value of the "status" field in the mutation. -func (m *UserMutation) Status() (r user.Status, exists bool) { - v := m.status +// UserID returns the value of the "user_id" field in the mutation. +func (m *UserAccessTokenMutation) UserID() (r uuid.UUID, exists bool) { + v := m.user_id if v == nil { return } return *v, true } -// OldStatus returns the old "status" field's value of the User entity. -// If the User object wasn't provided to the builder, the object is fetched from the database. +// OldUserID returns the old "user_id" field's value of the UserAccessToken entity. +// If the UserAccessToken object wasn't provided to the builder, the object is fetched from the database. // An error is returned if the mutation operation is not UpdateOne, or the database query fails. -func (m *UserMutation) OldStatus(ctx context.Context) (v user.Status, err error) { +func (m *UserAccessTokenMutation) OldUserID(ctx context.Context) (v uuid.UUID, err error) { if !m.op.Is(OpUpdateOne) { - return v, errors.New("OldStatus is only allowed on UpdateOne operations") + return v, errors.New("OldUserID is only allowed on UpdateOne operations") } if m.id == nil || m.oldValue == nil { - return v, errors.New("OldStatus requires an ID field in the mutation") + return v, errors.New("OldUserID requires an ID field in the mutation") } oldValue, err := m.oldValue(ctx) if err != nil { - return v, fmt.Errorf("querying old value for OldStatus: %w", err) + return v, fmt.Errorf("querying old value for OldUserID: %w", err) } - return oldValue.Status, nil + return oldValue.UserID, nil } -// ResetStatus resets all changes to the "status" field. -func (m *UserMutation) ResetStatus() { - m.status = nil +// ResetUserID resets all changes to the "user_id" field. +func (m *UserAccessTokenMutation) ResetUserID() { + m.user_id = nil } -// SetPreferences sets the "preferences" field. -func (m *UserMutation) SetPreferences(sp *schema.UserPreferences) { - m.preferences = &sp +// SetName sets the "name" field. +func (m *UserAccessTokenMutation) SetName(s string) { + m.name = &s } -// Preferences returns the value of the "preferences" field in the mutation. -func (m *UserMutation) Preferences() (r *schema.UserPreferences, exists bool) { - v := m.preferences +// Name returns the value of the "name" field in the mutation. +func (m *UserAccessTokenMutation) Name() (r string, exists bool) { + v := m.name if v == nil { return } return *v, true } -// OldPreferences returns the old "preferences" field's value of the User entity. -// If the User object wasn't provided to the builder, the object is fetched from the database. +// OldName returns the old "name" field's value of the UserAccessToken entity. +// If the UserAccessToken object wasn't provided to the builder, the object is fetched from the database. // An error is returned if the mutation operation is not UpdateOne, or the database query fails. -func (m *UserMutation) OldPreferences(ctx context.Context) (v *schema.UserPreferences, err error) { +func (m *UserAccessTokenMutation) OldName(ctx context.Context) (v string, err error) { if !m.op.Is(OpUpdateOne) { - return v, errors.New("OldPreferences is only allowed on UpdateOne operations") + return v, errors.New("OldName is only allowed on UpdateOne operations") } if m.id == nil || m.oldValue == nil { - return v, errors.New("OldPreferences requires an ID field in the mutation") + return v, errors.New("OldName requires an ID field in the mutation") } oldValue, err := m.oldValue(ctx) if err != nil { - return v, fmt.Errorf("querying old value for OldPreferences: %w", err) + return v, fmt.Errorf("querying old value for OldName: %w", err) } - return oldValue.Preferences, nil -} - -// ClearPreferences clears the value of the "preferences" field. -func (m *UserMutation) ClearPreferences() { - m.preferences = nil - m.clearedFields[user.FieldPreferences] = struct{}{} -} - -// PreferencesCleared returns if the "preferences" field was cleared in this mutation. -func (m *UserMutation) PreferencesCleared() bool { - _, ok := m.clearedFields[user.FieldPreferences] - return ok + return oldValue.Name, nil } -// ResetPreferences resets all changes to the "preferences" field. -func (m *UserMutation) ResetPreferences() { - m.preferences = nil - delete(m.clearedFields, user.FieldPreferences) +// ResetName resets all changes to the "name" field. +func (m *UserAccessTokenMutation) ResetName() { + m.name = nil } -// SetCreated sets the "created" field. -func (m *UserMutation) SetCreated(t time.Time) { - m.created = &t +// SetPrefix sets the "prefix" field. +func (m *UserAccessTokenMutation) SetPrefix(s string) { + m.prefix = &s } -// Created returns the value of the "created" field in the mutation. -func (m *UserMutation) Created() (r time.Time, exists bool) { - v := m.created +// Prefix returns the value of the "prefix" field in the mutation. +func (m *UserAccessTokenMutation) Prefix() (r string, exists bool) { + v := m.prefix if v == nil { return } return *v, true } -// OldCreated returns the old "created" field's value of the User entity. -// If the User object wasn't provided to the builder, the object is fetched from the database. +// OldPrefix returns the old "prefix" field's value of the UserAccessToken entity. +// If the UserAccessToken object wasn't provided to the builder, the object is fetched from the database. // An error is returned if the mutation operation is not UpdateOne, or the database query fails. -func (m *UserMutation) OldCreated(ctx context.Context) (v time.Time, err error) { +func (m *UserAccessTokenMutation) OldPrefix(ctx context.Context) (v string, err error) { if !m.op.Is(OpUpdateOne) { - return v, errors.New("OldCreated is only allowed on UpdateOne operations") + return v, errors.New("OldPrefix is only allowed on UpdateOne operations") } if m.id == nil || m.oldValue == nil { - return v, errors.New("OldCreated requires an ID field in the mutation") + return v, errors.New("OldPrefix requires an ID field in the mutation") } oldValue, err := m.oldValue(ctx) if err != nil { - return v, fmt.Errorf("querying old value for OldCreated: %w", err) + return v, fmt.Errorf("querying old value for OldPrefix: %w", err) } - return oldValue.Created, nil + return oldValue.Prefix, nil } -// ResetCreated resets all changes to the "created" field. -func (m *UserMutation) ResetCreated() { - m.created = nil +// ResetPrefix resets all changes to the "prefix" field. +func (m *UserAccessTokenMutation) ResetPrefix() { + m.prefix = nil } -// SetLastLogin sets the "last_login" field. -func (m *UserMutation) SetLastLogin(t time.Time) { - m.last_login = &t +// SetKeyHash sets the "key_hash" field. +func (m *UserAccessTokenMutation) SetKeyHash(s string) { + m.key_hash = &s } -// LastLogin returns the value of the "last_login" field in the mutation. -func (m *UserMutation) LastLogin() (r time.Time, exists bool) { - v := m.last_login +// KeyHash returns the value of the "key_hash" field in the mutation. +func (m *UserAccessTokenMutation) KeyHash() (r string, exists bool) { + v := m.key_hash if v == nil { return } return *v, true } -// OldLastLogin returns the old "last_login" field's value of the User entity. -// If the User object wasn't provided to the builder, the object is fetched from the database. +// OldKeyHash returns the old "key_hash" field's value of the UserAccessToken entity. +// If the UserAccessToken object wasn't provided to the builder, the object is fetched from the database. // An error is returned if the mutation operation is not UpdateOne, or the database query fails. -func (m *UserMutation) OldLastLogin(ctx context.Context) (v *time.Time, err error) { +func (m *UserAccessTokenMutation) OldKeyHash(ctx context.Context) (v string, err error) { if !m.op.Is(OpUpdateOne) { - return v, errors.New("OldLastLogin is only allowed on UpdateOne operations") + return v, errors.New("OldKeyHash is only allowed on UpdateOne operations") } if m.id == nil || m.oldValue == nil { - return v, errors.New("OldLastLogin requires an ID field in the mutation") + return v, errors.New("OldKeyHash requires an ID field in the mutation") } oldValue, err := m.oldValue(ctx) if err != nil { - return v, fmt.Errorf("querying old value for OldLastLogin: %w", err) + return v, fmt.Errorf("querying old value for OldKeyHash: %w", err) } - return oldValue.LastLogin, nil + return oldValue.KeyHash, nil } -// ClearLastLogin clears the value of the "last_login" field. -func (m *UserMutation) ClearLastLogin() { - m.last_login = nil - m.clearedFields[user.FieldLastLogin] = struct{}{} +// ResetKeyHash resets all changes to the "key_hash" field. +func (m *UserAccessTokenMutation) ResetKeyHash() { + m.key_hash = nil } -// LastLoginCleared returns if the "last_login" field was cleared in this mutation. -func (m *UserMutation) LastLoginCleared() bool { - _, ok := m.clearedFields[user.FieldLastLogin] - return ok +// SetProjectID sets the "project_id" field. +func (m *UserAccessTokenMutation) SetProjectID(u uuid.UUID) { + m.project_id = &u } -// ResetLastLogin resets all changes to the "last_login" field. -func (m *UserMutation) ResetLastLogin() { - m.last_login = nil - delete(m.clearedFields, user.FieldLastLogin) +// ProjectID returns the value of the "project_id" field in the mutation. +func (m *UserAccessTokenMutation) ProjectID() (r uuid.UUID, exists bool) { + v := m.project_id + if v == nil { + return + } + return *v, true } -// AddCreatedAgentIDs adds the "created_agents" edge to the Agent entity by ids. -func (m *UserMutation) AddCreatedAgentIDs(ids ...uuid.UUID) { - if m.created_agents == nil { - m.created_agents = make(map[uuid.UUID]struct{}) +// OldProjectID returns the old "project_id" field's value of the UserAccessToken entity. +// If the UserAccessToken object wasn't provided to the builder, the object is fetched from the database. +// An error is returned if the mutation operation is not UpdateOne, or the database query fails. +func (m *UserAccessTokenMutation) OldProjectID(ctx context.Context) (v uuid.UUID, err error) { + if !m.op.Is(OpUpdateOne) { + return v, errors.New("OldProjectID is only allowed on UpdateOne operations") } - for i := range ids { - m.created_agents[ids[i]] = struct{}{} + if m.id == nil || m.oldValue == nil { + return v, errors.New("OldProjectID requires an ID field in the mutation") } + oldValue, err := m.oldValue(ctx) + if err != nil { + return v, fmt.Errorf("querying old value for OldProjectID: %w", err) + } + return oldValue.ProjectID, nil } -// ClearCreatedAgents clears the "created_agents" edge to the Agent entity. -func (m *UserMutation) ClearCreatedAgents() { - m.clearedcreated_agents = true -} - -// CreatedAgentsCleared reports if the "created_agents" edge to the Agent entity was cleared. -func (m *UserMutation) CreatedAgentsCleared() bool { - return m.clearedcreated_agents +// ResetProjectID resets all changes to the "project_id" field. +func (m *UserAccessTokenMutation) ResetProjectID() { + m.project_id = nil } -// RemoveCreatedAgentIDs removes the "created_agents" edge to the Agent entity by IDs. -func (m *UserMutation) RemoveCreatedAgentIDs(ids ...uuid.UUID) { - if m.removedcreated_agents == nil { - m.removedcreated_agents = make(map[uuid.UUID]struct{}) - } - for i := range ids { - delete(m.created_agents, ids[i]) - m.removedcreated_agents[ids[i]] = struct{}{} - } +// SetScopes sets the "scopes" field. +func (m *UserAccessTokenMutation) SetScopes(s string) { + m.scopes = &s } -// RemovedCreatedAgents returns the removed IDs of the "created_agents" edge to the Agent entity. -func (m *UserMutation) RemovedCreatedAgentsIDs() (ids []uuid.UUID) { - for id := range m.removedcreated_agents { - ids = append(ids, id) +// Scopes returns the value of the "scopes" field in the mutation. +func (m *UserAccessTokenMutation) Scopes() (r string, exists bool) { + v := m.scopes + if v == nil { + return } - return + return *v, true } -// CreatedAgentsIDs returns the "created_agents" edge IDs in the mutation. -func (m *UserMutation) CreatedAgentsIDs() (ids []uuid.UUID) { - for id := range m.created_agents { - ids = append(ids, id) +// OldScopes returns the old "scopes" field's value of the UserAccessToken entity. +// If the UserAccessToken object wasn't provided to the builder, the object is fetched from the database. +// An error is returned if the mutation operation is not UpdateOne, or the database query fails. +func (m *UserAccessTokenMutation) OldScopes(ctx context.Context) (v string, err error) { + if !m.op.Is(OpUpdateOne) { + return v, errors.New("OldScopes is only allowed on UpdateOne operations") } - return -} - -// ResetCreatedAgents resets all changes to the "created_agents" edge. -func (m *UserMutation) ResetCreatedAgents() { - m.created_agents = nil - m.clearedcreated_agents = false - m.removedcreated_agents = nil -} - -// AddOwnedAgentIDs adds the "owned_agents" edge to the Agent entity by ids. -func (m *UserMutation) AddOwnedAgentIDs(ids ...uuid.UUID) { - if m.owned_agents == nil { - m.owned_agents = make(map[uuid.UUID]struct{}) + if m.id == nil || m.oldValue == nil { + return v, errors.New("OldScopes requires an ID field in the mutation") } - for i := range ids { - m.owned_agents[ids[i]] = struct{}{} + oldValue, err := m.oldValue(ctx) + if err != nil { + return v, fmt.Errorf("querying old value for OldScopes: %w", err) } + return oldValue.Scopes, nil } -// ClearOwnedAgents clears the "owned_agents" edge to the Agent entity. -func (m *UserMutation) ClearOwnedAgents() { - m.clearedowned_agents = true +// ResetScopes resets all changes to the "scopes" field. +func (m *UserAccessTokenMutation) ResetScopes() { + m.scopes = nil } -// OwnedAgentsCleared reports if the "owned_agents" edge to the Agent entity was cleared. -func (m *UserMutation) OwnedAgentsCleared() bool { - return m.clearedowned_agents -} - -// RemoveOwnedAgentIDs removes the "owned_agents" edge to the Agent entity by IDs. -func (m *UserMutation) RemoveOwnedAgentIDs(ids ...uuid.UUID) { - if m.removedowned_agents == nil { - m.removedowned_agents = make(map[uuid.UUID]struct{}) - } - for i := range ids { - delete(m.owned_agents, ids[i]) - m.removedowned_agents[ids[i]] = struct{}{} - } +// SetRevoked sets the "revoked" field. +func (m *UserAccessTokenMutation) SetRevoked(b bool) { + m.revoked = &b } -// RemovedOwnedAgents returns the removed IDs of the "owned_agents" edge to the Agent entity. -func (m *UserMutation) RemovedOwnedAgentsIDs() (ids []uuid.UUID) { - for id := range m.removedowned_agents { - ids = append(ids, id) +// Revoked returns the value of the "revoked" field in the mutation. +func (m *UserAccessTokenMutation) Revoked() (r bool, exists bool) { + v := m.revoked + if v == nil { + return } - return + return *v, true } -// OwnedAgentsIDs returns the "owned_agents" edge IDs in the mutation. -func (m *UserMutation) OwnedAgentsIDs() (ids []uuid.UUID) { - for id := range m.owned_agents { - ids = append(ids, id) +// OldRevoked returns the old "revoked" field's value of the UserAccessToken entity. +// If the UserAccessToken object wasn't provided to the builder, the object is fetched from the database. +// An error is returned if the mutation operation is not UpdateOne, or the database query fails. +func (m *UserAccessTokenMutation) OldRevoked(ctx context.Context) (v bool, err error) { + if !m.op.Is(OpUpdateOne) { + return v, errors.New("OldRevoked is only allowed on UpdateOne operations") } - return -} - -// ResetOwnedAgents resets all changes to the "owned_agents" edge. -func (m *UserMutation) ResetOwnedAgents() { - m.owned_agents = nil - m.clearedowned_agents = false - m.removedowned_agents = nil -} - -// AddOwnedGroupIDs adds the "owned_groups" edge to the Group entity by ids. -func (m *UserMutation) AddOwnedGroupIDs(ids ...uuid.UUID) { - if m.owned_groups == nil { - m.owned_groups = make(map[uuid.UUID]struct{}) + if m.id == nil || m.oldValue == nil { + return v, errors.New("OldRevoked requires an ID field in the mutation") } - for i := range ids { - m.owned_groups[ids[i]] = struct{}{} + oldValue, err := m.oldValue(ctx) + if err != nil { + return v, fmt.Errorf("querying old value for OldRevoked: %w", err) } + return oldValue.Revoked, nil } -// ClearOwnedGroups clears the "owned_groups" edge to the Group entity. -func (m *UserMutation) ClearOwnedGroups() { - m.clearedowned_groups = true +// ResetRevoked resets all changes to the "revoked" field. +func (m *UserAccessTokenMutation) ResetRevoked() { + m.revoked = nil } -// OwnedGroupsCleared reports if the "owned_groups" edge to the Group entity was cleared. -func (m *UserMutation) OwnedGroupsCleared() bool { - return m.clearedowned_groups +// SetExpiresAt sets the "expires_at" field. +func (m *UserAccessTokenMutation) SetExpiresAt(t time.Time) { + m.expires_at = &t } -// RemoveOwnedGroupIDs removes the "owned_groups" edge to the Group entity by IDs. -func (m *UserMutation) RemoveOwnedGroupIDs(ids ...uuid.UUID) { - if m.removedowned_groups == nil { - m.removedowned_groups = make(map[uuid.UUID]struct{}) - } - for i := range ids { - delete(m.owned_groups, ids[i]) - m.removedowned_groups[ids[i]] = struct{}{} +// ExpiresAt returns the value of the "expires_at" field in the mutation. +func (m *UserAccessTokenMutation) ExpiresAt() (r time.Time, exists bool) { + v := m.expires_at + if v == nil { + return } + return *v, true } -// RemovedOwnedGroups returns the removed IDs of the "owned_groups" edge to the Group entity. -func (m *UserMutation) RemovedOwnedGroupsIDs() (ids []uuid.UUID) { - for id := range m.removedowned_groups { - ids = append(ids, id) +// OldExpiresAt returns the old "expires_at" field's value of the UserAccessToken entity. +// If the UserAccessToken object wasn't provided to the builder, the object is fetched from the database. +// An error is returned if the mutation operation is not UpdateOne, or the database query fails. +func (m *UserAccessTokenMutation) OldExpiresAt(ctx context.Context) (v *time.Time, err error) { + if !m.op.Is(OpUpdateOne) { + return v, errors.New("OldExpiresAt is only allowed on UpdateOne operations") } - return -} - -// OwnedGroupsIDs returns the "owned_groups" edge IDs in the mutation. -func (m *UserMutation) OwnedGroupsIDs() (ids []uuid.UUID) { - for id := range m.owned_groups { - ids = append(ids, id) + if m.id == nil || m.oldValue == nil { + return v, errors.New("OldExpiresAt requires an ID field in the mutation") } - return + oldValue, err := m.oldValue(ctx) + if err != nil { + return v, fmt.Errorf("querying old value for OldExpiresAt: %w", err) + } + return oldValue.ExpiresAt, nil } -// ResetOwnedGroups resets all changes to the "owned_groups" edge. -func (m *UserMutation) ResetOwnedGroups() { - m.owned_groups = nil - m.clearedowned_groups = false - m.removedowned_groups = nil +// ClearExpiresAt clears the value of the "expires_at" field. +func (m *UserAccessTokenMutation) ClearExpiresAt() { + m.expires_at = nil + m.clearedFields[useraccesstoken.FieldExpiresAt] = struct{}{} } -// AddMembershipIDs adds the "memberships" edge to the GroupMembership entity by ids. -func (m *UserMutation) AddMembershipIDs(ids ...uuid.UUID) { - if m.memberships == nil { - m.memberships = make(map[uuid.UUID]struct{}) - } - for i := range ids { - m.memberships[ids[i]] = struct{}{} - } +// ExpiresAtCleared returns if the "expires_at" field was cleared in this mutation. +func (m *UserAccessTokenMutation) ExpiresAtCleared() bool { + _, ok := m.clearedFields[useraccesstoken.FieldExpiresAt] + return ok } -// ClearMemberships clears the "memberships" edge to the GroupMembership entity. -func (m *UserMutation) ClearMemberships() { - m.clearedmemberships = true +// ResetExpiresAt resets all changes to the "expires_at" field. +func (m *UserAccessTokenMutation) ResetExpiresAt() { + m.expires_at = nil + delete(m.clearedFields, useraccesstoken.FieldExpiresAt) } -// MembershipsCleared reports if the "memberships" edge to the GroupMembership entity was cleared. -func (m *UserMutation) MembershipsCleared() bool { - return m.clearedmemberships +// SetLastUsed sets the "last_used" field. +func (m *UserAccessTokenMutation) SetLastUsed(t time.Time) { + m.last_used = &t } -// RemoveMembershipIDs removes the "memberships" edge to the GroupMembership entity by IDs. -func (m *UserMutation) RemoveMembershipIDs(ids ...uuid.UUID) { - if m.removedmemberships == nil { - m.removedmemberships = make(map[uuid.UUID]struct{}) - } - for i := range ids { - delete(m.memberships, ids[i]) - m.removedmemberships[ids[i]] = struct{}{} +// LastUsed returns the value of the "last_used" field in the mutation. +func (m *UserAccessTokenMutation) LastUsed() (r time.Time, exists bool) { + v := m.last_used + if v == nil { + return } + return *v, true } -// RemovedMemberships returns the removed IDs of the "memberships" edge to the GroupMembership entity. -func (m *UserMutation) RemovedMembershipsIDs() (ids []uuid.UUID) { - for id := range m.removedmemberships { - ids = append(ids, id) +// OldLastUsed returns the old "last_used" field's value of the UserAccessToken entity. +// If the UserAccessToken object wasn't provided to the builder, the object is fetched from the database. +// An error is returned if the mutation operation is not UpdateOne, or the database query fails. +func (m *UserAccessTokenMutation) OldLastUsed(ctx context.Context) (v *time.Time, err error) { + if !m.op.Is(OpUpdateOne) { + return v, errors.New("OldLastUsed is only allowed on UpdateOne operations") } - return -} - -// MembershipsIDs returns the "memberships" edge IDs in the mutation. -func (m *UserMutation) MembershipsIDs() (ids []uuid.UUID) { - for id := range m.memberships { - ids = append(ids, id) + if m.id == nil || m.oldValue == nil { + return v, errors.New("OldLastUsed requires an ID field in the mutation") } - return + oldValue, err := m.oldValue(ctx) + if err != nil { + return v, fmt.Errorf("querying old value for OldLastUsed: %w", err) + } + return oldValue.LastUsed, nil } -// ResetMemberships resets all changes to the "memberships" edge. -func (m *UserMutation) ResetMemberships() { - m.memberships = nil - m.clearedmemberships = false - m.removedmemberships = nil +// ClearLastUsed clears the value of the "last_used" field. +func (m *UserAccessTokenMutation) ClearLastUsed() { + m.last_used = nil + m.clearedFields[useraccesstoken.FieldLastUsed] = struct{}{} } -// AddPolicyBindingIDs adds the "policy_bindings" edge to the PolicyBinding entity by ids. -func (m *UserMutation) AddPolicyBindingIDs(ids ...uuid.UUID) { - if m.policy_bindings == nil { - m.policy_bindings = make(map[uuid.UUID]struct{}) - } - for i := range ids { - m.policy_bindings[ids[i]] = struct{}{} - } +// LastUsedCleared returns if the "last_used" field was cleared in this mutation. +func (m *UserAccessTokenMutation) LastUsedCleared() bool { + _, ok := m.clearedFields[useraccesstoken.FieldLastUsed] + return ok } -// ClearPolicyBindings clears the "policy_bindings" edge to the PolicyBinding entity. -func (m *UserMutation) ClearPolicyBindings() { - m.clearedpolicy_bindings = true +// ResetLastUsed resets all changes to the "last_used" field. +func (m *UserAccessTokenMutation) ResetLastUsed() { + m.last_used = nil + delete(m.clearedFields, useraccesstoken.FieldLastUsed) } -// PolicyBindingsCleared reports if the "policy_bindings" edge to the PolicyBinding entity was cleared. -func (m *UserMutation) PolicyBindingsCleared() bool { - return m.clearedpolicy_bindings +// SetCreated sets the "created" field. +func (m *UserAccessTokenMutation) SetCreated(t time.Time) { + m.created = &t } -// RemovePolicyBindingIDs removes the "policy_bindings" edge to the PolicyBinding entity by IDs. -func (m *UserMutation) RemovePolicyBindingIDs(ids ...uuid.UUID) { - if m.removedpolicy_bindings == nil { - m.removedpolicy_bindings = make(map[uuid.UUID]struct{}) - } - for i := range ids { - delete(m.policy_bindings, ids[i]) - m.removedpolicy_bindings[ids[i]] = struct{}{} +// Created returns the value of the "created" field in the mutation. +func (m *UserAccessTokenMutation) Created() (r time.Time, exists bool) { + v := m.created + if v == nil { + return } + return *v, true } -// RemovedPolicyBindings returns the removed IDs of the "policy_bindings" edge to the PolicyBinding entity. -func (m *UserMutation) RemovedPolicyBindingsIDs() (ids []uuid.UUID) { - for id := range m.removedpolicy_bindings { - ids = append(ids, id) +// OldCreated returns the old "created" field's value of the UserAccessToken entity. +// If the UserAccessToken object wasn't provided to the builder, the object is fetched from the database. +// An error is returned if the mutation operation is not UpdateOne, or the database query fails. +func (m *UserAccessTokenMutation) OldCreated(ctx context.Context) (v time.Time, err error) { + if !m.op.Is(OpUpdateOne) { + return v, errors.New("OldCreated is only allowed on UpdateOne operations") } - return -} - -// PolicyBindingsIDs returns the "policy_bindings" edge IDs in the mutation. -func (m *UserMutation) PolicyBindingsIDs() (ids []uuid.UUID) { - for id := range m.policy_bindings { - ids = append(ids, id) + if m.id == nil || m.oldValue == nil { + return v, errors.New("OldCreated requires an ID field in the mutation") } - return + oldValue, err := m.oldValue(ctx) + if err != nil { + return v, fmt.Errorf("querying old value for OldCreated: %w", err) + } + return oldValue.Created, nil } -// ResetPolicyBindings resets all changes to the "policy_bindings" edge. -func (m *UserMutation) ResetPolicyBindings() { - m.policy_bindings = nil - m.clearedpolicy_bindings = false - m.removedpolicy_bindings = nil +// ResetCreated resets all changes to the "created" field. +func (m *UserAccessTokenMutation) ResetCreated() { + m.created = nil } -// Where appends a list predicates to the UserMutation builder. -func (m *UserMutation) Where(ps ...predicate.User) { +// Where appends a list predicates to the UserAccessTokenMutation builder. +func (m *UserAccessTokenMutation) Where(ps ...predicate.UserAccessToken) { m.predicates = append(m.predicates, ps...) } -// WhereP appends storage-level predicates to the UserMutation builder. Using this method, +// WhereP appends storage-level predicates to the UserAccessTokenMutation builder. Using this method, // users can use type-assertion to append predicates that do not depend on any generated package. -func (m *UserMutation) WhereP(ps ...func(*sql.Selector)) { - p := make([]predicate.User, len(ps)) +func (m *UserAccessTokenMutation) WhereP(ps ...func(*sql.Selector)) { + p := make([]predicate.UserAccessToken, len(ps)) for i := range ps { p[i] = ps[i] } @@ -7581,48 +33176,54 @@ func (m *UserMutation) WhereP(ps ...func(*sql.Selector)) { } // Op returns the operation name. -func (m *UserMutation) Op() Op { +func (m *UserAccessTokenMutation) Op() Op { return m.op } // SetOp allows setting the mutation operation. -func (m *UserMutation) SetOp(op Op) { +func (m *UserAccessTokenMutation) SetOp(op Op) { m.op = op } -// Type returns the node type of this mutation (User). -func (m *UserMutation) Type() string { +// Type returns the node type of this mutation (UserAccessToken). +func (m *UserAccessTokenMutation) Type() string { return m.typ } // Fields returns all fields that were changed during this mutation. Note that in // order to get all numeric fields that were incremented/decremented, call // AddedFields(). -func (m *UserMutation) Fields() []string { - fields := make([]string, 0, 8) - if m.email != nil { - fields = append(fields, user.FieldEmail) +func (m *UserAccessTokenMutation) Fields() []string { + fields := make([]string, 0, 10) + if m.user_id != nil { + fields = append(fields, useraccesstoken.FieldUserID) } - if m.display_name != nil { - fields = append(fields, user.FieldDisplayName) + if m.name != nil { + fields = append(fields, useraccesstoken.FieldName) + } + if m.prefix != nil { + fields = append(fields, useraccesstoken.FieldPrefix) + } + if m.key_hash != nil { + fields = append(fields, useraccesstoken.FieldKeyHash) + } + if m.project_id != nil { + fields = append(fields, useraccesstoken.FieldProjectID) } - if m.avatar_url != nil { - fields = append(fields, user.FieldAvatarURL) + if m.scopes != nil { + fields = append(fields, useraccesstoken.FieldScopes) } - if m.role != nil { - fields = append(fields, user.FieldRole) + if m.revoked != nil { + fields = append(fields, useraccesstoken.FieldRevoked) } - if m.status != nil { - fields = append(fields, user.FieldStatus) + if m.expires_at != nil { + fields = append(fields, useraccesstoken.FieldExpiresAt) } - if m.preferences != nil { - fields = append(fields, user.FieldPreferences) + if m.last_used != nil { + fields = append(fields, useraccesstoken.FieldLastUsed) } if m.created != nil { - fields = append(fields, user.FieldCreated) - } - if m.last_login != nil { - fields = append(fields, user.FieldLastLogin) + fields = append(fields, useraccesstoken.FieldCreated) } return fields } @@ -7630,24 +33231,28 @@ func (m *UserMutation) Fields() []string { // Field returns the value of a field with the given name. The second boolean // return value indicates that this field was not set, or was not defined in the // schema. -func (m *UserMutation) Field(name string) (ent.Value, bool) { +func (m *UserAccessTokenMutation) Field(name string) (ent.Value, bool) { switch name { - case user.FieldEmail: - return m.Email() - case user.FieldDisplayName: - return m.DisplayName() - case user.FieldAvatarURL: - return m.AvatarURL() - case user.FieldRole: - return m.Role() - case user.FieldStatus: - return m.Status() - case user.FieldPreferences: - return m.Preferences() - case user.FieldCreated: + case useraccesstoken.FieldUserID: + return m.UserID() + case useraccesstoken.FieldName: + return m.Name() + case useraccesstoken.FieldPrefix: + return m.Prefix() + case useraccesstoken.FieldKeyHash: + return m.KeyHash() + case useraccesstoken.FieldProjectID: + return m.ProjectID() + case useraccesstoken.FieldScopes: + return m.Scopes() + case useraccesstoken.FieldRevoked: + return m.Revoked() + case useraccesstoken.FieldExpiresAt: + return m.ExpiresAt() + case useraccesstoken.FieldLastUsed: + return m.LastUsed() + case useraccesstoken.FieldCreated: return m.Created() - case user.FieldLastLogin: - return m.LastLogin() } return nil, false } @@ -7655,371 +33260,249 @@ func (m *UserMutation) Field(name string) (ent.Value, bool) { // OldField returns the old value of the field from the database. An error is // returned if the mutation operation is not UpdateOne, or the query to the // database failed. -func (m *UserMutation) OldField(ctx context.Context, name string) (ent.Value, error) { +func (m *UserAccessTokenMutation) OldField(ctx context.Context, name string) (ent.Value, error) { switch name { - case user.FieldEmail: - return m.OldEmail(ctx) - case user.FieldDisplayName: - return m.OldDisplayName(ctx) - case user.FieldAvatarURL: - return m.OldAvatarURL(ctx) - case user.FieldRole: - return m.OldRole(ctx) - case user.FieldStatus: - return m.OldStatus(ctx) - case user.FieldPreferences: - return m.OldPreferences(ctx) - case user.FieldCreated: + case useraccesstoken.FieldUserID: + return m.OldUserID(ctx) + case useraccesstoken.FieldName: + return m.OldName(ctx) + case useraccesstoken.FieldPrefix: + return m.OldPrefix(ctx) + case useraccesstoken.FieldKeyHash: + return m.OldKeyHash(ctx) + case useraccesstoken.FieldProjectID: + return m.OldProjectID(ctx) + case useraccesstoken.FieldScopes: + return m.OldScopes(ctx) + case useraccesstoken.FieldRevoked: + return m.OldRevoked(ctx) + case useraccesstoken.FieldExpiresAt: + return m.OldExpiresAt(ctx) + case useraccesstoken.FieldLastUsed: + return m.OldLastUsed(ctx) + case useraccesstoken.FieldCreated: return m.OldCreated(ctx) - case user.FieldLastLogin: - return m.OldLastLogin(ctx) } - return nil, fmt.Errorf("unknown User field %s", name) + return nil, fmt.Errorf("unknown UserAccessToken field %s", name) } // SetField sets the value of a field with the given name. It returns an error if // the field is not defined in the schema, or if the type mismatched the field // type. -func (m *UserMutation) SetField(name string, value ent.Value) error { +func (m *UserAccessTokenMutation) SetField(name string, value ent.Value) error { switch name { - case user.FieldEmail: + case useraccesstoken.FieldUserID: + v, ok := value.(uuid.UUID) + if !ok { + return fmt.Errorf("unexpected type %T for field %s", value, name) + } + m.SetUserID(v) + return nil + case useraccesstoken.FieldName: v, ok := value.(string) if !ok { return fmt.Errorf("unexpected type %T for field %s", value, name) } - m.SetEmail(v) + m.SetName(v) return nil - case user.FieldDisplayName: + case useraccesstoken.FieldPrefix: v, ok := value.(string) if !ok { return fmt.Errorf("unexpected type %T for field %s", value, name) } - m.SetDisplayName(v) + m.SetPrefix(v) return nil - case user.FieldAvatarURL: + case useraccesstoken.FieldKeyHash: v, ok := value.(string) if !ok { return fmt.Errorf("unexpected type %T for field %s", value, name) } - m.SetAvatarURL(v) + m.SetKeyHash(v) return nil - case user.FieldRole: - v, ok := value.(user.Role) + case useraccesstoken.FieldProjectID: + v, ok := value.(uuid.UUID) if !ok { return fmt.Errorf("unexpected type %T for field %s", value, name) } - m.SetRole(v) + m.SetProjectID(v) return nil - case user.FieldStatus: - v, ok := value.(user.Status) + case useraccesstoken.FieldScopes: + v, ok := value.(string) if !ok { return fmt.Errorf("unexpected type %T for field %s", value, name) } - m.SetStatus(v) + m.SetScopes(v) return nil - case user.FieldPreferences: - v, ok := value.(*schema.UserPreferences) + case useraccesstoken.FieldRevoked: + v, ok := value.(bool) if !ok { return fmt.Errorf("unexpected type %T for field %s", value, name) } - m.SetPreferences(v) + m.SetRevoked(v) return nil - case user.FieldCreated: + case useraccesstoken.FieldExpiresAt: v, ok := value.(time.Time) if !ok { return fmt.Errorf("unexpected type %T for field %s", value, name) } - m.SetCreated(v) + m.SetExpiresAt(v) return nil - case user.FieldLastLogin: + case useraccesstoken.FieldLastUsed: v, ok := value.(time.Time) if !ok { return fmt.Errorf("unexpected type %T for field %s", value, name) } - m.SetLastLogin(v) + m.SetLastUsed(v) + return nil + case useraccesstoken.FieldCreated: + v, ok := value.(time.Time) + if !ok { + return fmt.Errorf("unexpected type %T for field %s", value, name) + } + m.SetCreated(v) return nil } - return fmt.Errorf("unknown User field %s", name) + return fmt.Errorf("unknown UserAccessToken field %s", name) } // AddedFields returns all numeric fields that were incremented/decremented during // this mutation. -func (m *UserMutation) AddedFields() []string { +func (m *UserAccessTokenMutation) AddedFields() []string { return nil } // AddedField returns the numeric value that was incremented/decremented on a field // with the given name. The second boolean return value indicates that this field // was not set, or was not defined in the schema. -func (m *UserMutation) AddedField(name string) (ent.Value, bool) { +func (m *UserAccessTokenMutation) AddedField(name string) (ent.Value, bool) { return nil, false } // AddField adds the value to the field with the given name. It returns an error if // the field is not defined in the schema, or if the type mismatched the field // type. -func (m *UserMutation) AddField(name string, value ent.Value) error { +func (m *UserAccessTokenMutation) AddField(name string, value ent.Value) error { switch name { } - return fmt.Errorf("unknown User numeric field %s", name) + return fmt.Errorf("unknown UserAccessToken numeric field %s", name) } // ClearedFields returns all nullable fields that were cleared during this // mutation. -func (m *UserMutation) ClearedFields() []string { +func (m *UserAccessTokenMutation) ClearedFields() []string { var fields []string - if m.FieldCleared(user.FieldAvatarURL) { - fields = append(fields, user.FieldAvatarURL) + if m.FieldCleared(useraccesstoken.FieldExpiresAt) { + fields = append(fields, useraccesstoken.FieldExpiresAt) } - if m.FieldCleared(user.FieldPreferences) { - fields = append(fields, user.FieldPreferences) - } - if m.FieldCleared(user.FieldLastLogin) { - fields = append(fields, user.FieldLastLogin) + if m.FieldCleared(useraccesstoken.FieldLastUsed) { + fields = append(fields, useraccesstoken.FieldLastUsed) } return fields } // FieldCleared returns a boolean indicating if a field with the given name was // cleared in this mutation. -func (m *UserMutation) FieldCleared(name string) bool { +func (m *UserAccessTokenMutation) FieldCleared(name string) bool { _, ok := m.clearedFields[name] return ok } // ClearField clears the value of the field with the given name. It returns an // error if the field is not defined in the schema. -func (m *UserMutation) ClearField(name string) error { +func (m *UserAccessTokenMutation) ClearField(name string) error { switch name { - case user.FieldAvatarURL: - m.ClearAvatarURL() - return nil - case user.FieldPreferences: - m.ClearPreferences() + case useraccesstoken.FieldExpiresAt: + m.ClearExpiresAt() return nil - case user.FieldLastLogin: - m.ClearLastLogin() + case useraccesstoken.FieldLastUsed: + m.ClearLastUsed() return nil } - return fmt.Errorf("unknown User nullable field %s", name) + return fmt.Errorf("unknown UserAccessToken nullable field %s", name) } // ResetField resets all changes in the mutation for the field with the given name. // It returns an error if the field is not defined in the schema. -func (m *UserMutation) ResetField(name string) error { +func (m *UserAccessTokenMutation) ResetField(name string) error { switch name { - case user.FieldEmail: - m.ResetEmail() + case useraccesstoken.FieldUserID: + m.ResetUserID() return nil - case user.FieldDisplayName: - m.ResetDisplayName() + case useraccesstoken.FieldName: + m.ResetName() return nil - case user.FieldAvatarURL: - m.ResetAvatarURL() + case useraccesstoken.FieldPrefix: + m.ResetPrefix() return nil - case user.FieldRole: - m.ResetRole() + case useraccesstoken.FieldKeyHash: + m.ResetKeyHash() return nil - case user.FieldStatus: - m.ResetStatus() + case useraccesstoken.FieldProjectID: + m.ResetProjectID() return nil - case user.FieldPreferences: - m.ResetPreferences() + case useraccesstoken.FieldScopes: + m.ResetScopes() return nil - case user.FieldCreated: - m.ResetCreated() + case useraccesstoken.FieldRevoked: + m.ResetRevoked() return nil - case user.FieldLastLogin: - m.ResetLastLogin() + case useraccesstoken.FieldExpiresAt: + m.ResetExpiresAt() + return nil + case useraccesstoken.FieldLastUsed: + m.ResetLastUsed() + return nil + case useraccesstoken.FieldCreated: + m.ResetCreated() return nil } - return fmt.Errorf("unknown User field %s", name) + return fmt.Errorf("unknown UserAccessToken field %s", name) } // AddedEdges returns all edge names that were set/added in this mutation. -func (m *UserMutation) AddedEdges() []string { - edges := make([]string, 0, 5) - if m.created_agents != nil { - edges = append(edges, user.EdgeCreatedAgents) - } - if m.owned_agents != nil { - edges = append(edges, user.EdgeOwnedAgents) - } - if m.owned_groups != nil { - edges = append(edges, user.EdgeOwnedGroups) - } - if m.memberships != nil { - edges = append(edges, user.EdgeMemberships) - } - if m.policy_bindings != nil { - edges = append(edges, user.EdgePolicyBindings) - } +func (m *UserAccessTokenMutation) AddedEdges() []string { + edges := make([]string, 0, 0) return edges } // AddedIDs returns all IDs (to other nodes) that were added for the given edge // name in this mutation. -func (m *UserMutation) AddedIDs(name string) []ent.Value { - switch name { - case user.EdgeCreatedAgents: - ids := make([]ent.Value, 0, len(m.created_agents)) - for id := range m.created_agents { - ids = append(ids, id) - } - return ids - case user.EdgeOwnedAgents: - ids := make([]ent.Value, 0, len(m.owned_agents)) - for id := range m.owned_agents { - ids = append(ids, id) - } - return ids - case user.EdgeOwnedGroups: - ids := make([]ent.Value, 0, len(m.owned_groups)) - for id := range m.owned_groups { - ids = append(ids, id) - } - return ids - case user.EdgeMemberships: - ids := make([]ent.Value, 0, len(m.memberships)) - for id := range m.memberships { - ids = append(ids, id) - } - return ids - case user.EdgePolicyBindings: - ids := make([]ent.Value, 0, len(m.policy_bindings)) - for id := range m.policy_bindings { - ids = append(ids, id) - } - return ids - } +func (m *UserAccessTokenMutation) AddedIDs(name string) []ent.Value { return nil } // RemovedEdges returns all edge names that were removed in this mutation. -func (m *UserMutation) RemovedEdges() []string { - edges := make([]string, 0, 5) - if m.removedcreated_agents != nil { - edges = append(edges, user.EdgeCreatedAgents) - } - if m.removedowned_agents != nil { - edges = append(edges, user.EdgeOwnedAgents) - } - if m.removedowned_groups != nil { - edges = append(edges, user.EdgeOwnedGroups) - } - if m.removedmemberships != nil { - edges = append(edges, user.EdgeMemberships) - } - if m.removedpolicy_bindings != nil { - edges = append(edges, user.EdgePolicyBindings) - } +func (m *UserAccessTokenMutation) RemovedEdges() []string { + edges := make([]string, 0, 0) return edges } // RemovedIDs returns all IDs (to other nodes) that were removed for the edge with // the given name in this mutation. -func (m *UserMutation) RemovedIDs(name string) []ent.Value { - switch name { - case user.EdgeCreatedAgents: - ids := make([]ent.Value, 0, len(m.removedcreated_agents)) - for id := range m.removedcreated_agents { - ids = append(ids, id) - } - return ids - case user.EdgeOwnedAgents: - ids := make([]ent.Value, 0, len(m.removedowned_agents)) - for id := range m.removedowned_agents { - ids = append(ids, id) - } - return ids - case user.EdgeOwnedGroups: - ids := make([]ent.Value, 0, len(m.removedowned_groups)) - for id := range m.removedowned_groups { - ids = append(ids, id) - } - return ids - case user.EdgeMemberships: - ids := make([]ent.Value, 0, len(m.removedmemberships)) - for id := range m.removedmemberships { - ids = append(ids, id) - } - return ids - case user.EdgePolicyBindings: - ids := make([]ent.Value, 0, len(m.removedpolicy_bindings)) - for id := range m.removedpolicy_bindings { - ids = append(ids, id) - } - return ids - } +func (m *UserAccessTokenMutation) RemovedIDs(name string) []ent.Value { return nil } // ClearedEdges returns all edge names that were cleared in this mutation. -func (m *UserMutation) ClearedEdges() []string { - edges := make([]string, 0, 5) - if m.clearedcreated_agents { - edges = append(edges, user.EdgeCreatedAgents) - } - if m.clearedowned_agents { - edges = append(edges, user.EdgeOwnedAgents) - } - if m.clearedowned_groups { - edges = append(edges, user.EdgeOwnedGroups) - } - if m.clearedmemberships { - edges = append(edges, user.EdgeMemberships) - } - if m.clearedpolicy_bindings { - edges = append(edges, user.EdgePolicyBindings) - } +func (m *UserAccessTokenMutation) ClearedEdges() []string { + edges := make([]string, 0, 0) return edges } // EdgeCleared returns a boolean which indicates if the edge with the given name // was cleared in this mutation. -func (m *UserMutation) EdgeCleared(name string) bool { - switch name { - case user.EdgeCreatedAgents: - return m.clearedcreated_agents - case user.EdgeOwnedAgents: - return m.clearedowned_agents - case user.EdgeOwnedGroups: - return m.clearedowned_groups - case user.EdgeMemberships: - return m.clearedmemberships - case user.EdgePolicyBindings: - return m.clearedpolicy_bindings - } +func (m *UserAccessTokenMutation) EdgeCleared(name string) bool { return false } // ClearEdge clears the value of the edge with the given name. It returns an error // if that edge is not defined in the schema. -func (m *UserMutation) ClearEdge(name string) error { - switch name { - } - return fmt.Errorf("unknown User unique edge %s", name) +func (m *UserAccessTokenMutation) ClearEdge(name string) error { + return fmt.Errorf("unknown UserAccessToken unique edge %s", name) } // ResetEdge resets all changes to the edge with the given name in this mutation. // It returns an error if the edge is not defined in the schema. -func (m *UserMutation) ResetEdge(name string) error { - switch name { - case user.EdgeCreatedAgents: - m.ResetCreatedAgents() - return nil - case user.EdgeOwnedAgents: - m.ResetOwnedAgents() - return nil - case user.EdgeOwnedGroups: - m.ResetOwnedGroups() - return nil - case user.EdgeMemberships: - m.ResetMemberships() - return nil - case user.EdgePolicyBindings: - m.ResetPolicyBindings() - return nil - } - return fmt.Errorf("unknown User edge %s", name) +func (m *UserAccessTokenMutation) ResetEdge(name string) error { + return fmt.Errorf("unknown UserAccessToken edge %s", name) } diff --git a/pkg/ent/notification.go b/pkg/ent/notification.go new file mode 100644 index 000000000..08e322a45 --- /dev/null +++ b/pkg/ent/notification.go @@ -0,0 +1,208 @@ +// Code generated by ent, DO NOT EDIT. + +package ent + +import ( + "fmt" + "strings" + "time" + + "entgo.io/ent" + "entgo.io/ent/dialect/sql" + "github.com/GoogleCloudPlatform/scion/pkg/ent/notification" + "github.com/google/uuid" +) + +// Notification is the model entity for the Notification schema. +type Notification struct { + config `json:"-"` + // ID of the ent. + ID uuid.UUID `json:"id,omitempty"` + // SubscriptionID holds the value of the "subscription_id" field. + SubscriptionID uuid.UUID `json:"subscription_id,omitempty"` + // AgentID holds the value of the "agent_id" field. + AgentID uuid.UUID `json:"agent_id,omitempty"` + // ProjectID holds the value of the "project_id" field. + ProjectID uuid.UUID `json:"project_id,omitempty"` + // SubscriberType holds the value of the "subscriber_type" field. + SubscriberType string `json:"subscriber_type,omitempty"` + // SubscriberID holds the value of the "subscriber_id" field. + SubscriberID string `json:"subscriber_id,omitempty"` + // Status holds the value of the "status" field. + Status string `json:"status,omitempty"` + // Message holds the value of the "message" field. + Message string `json:"message,omitempty"` + // Dispatched holds the value of the "dispatched" field. + Dispatched bool `json:"dispatched,omitempty"` + // Acknowledged holds the value of the "acknowledged" field. + Acknowledged bool `json:"acknowledged,omitempty"` + // Created holds the value of the "created" field. + Created time.Time `json:"created,omitempty"` + selectValues sql.SelectValues +} + +// scanValues returns the types for scanning values from sql.Rows. +func (*Notification) scanValues(columns []string) ([]any, error) { + values := make([]any, len(columns)) + for i := range columns { + switch columns[i] { + case notification.FieldDispatched, notification.FieldAcknowledged: + values[i] = new(sql.NullBool) + case notification.FieldSubscriberType, notification.FieldSubscriberID, notification.FieldStatus, notification.FieldMessage: + values[i] = new(sql.NullString) + case notification.FieldCreated: + values[i] = new(sql.NullTime) + case notification.FieldID, notification.FieldSubscriptionID, notification.FieldAgentID, notification.FieldProjectID: + values[i] = new(uuid.UUID) + default: + values[i] = new(sql.UnknownType) + } + } + return values, nil +} + +// assignValues assigns the values that were returned from sql.Rows (after scanning) +// to the Notification fields. +func (_m *Notification) assignValues(columns []string, values []any) error { + if m, n := len(values), len(columns); m < n { + return fmt.Errorf("mismatch number of scan values: %d != %d", m, n) + } + for i := range columns { + switch columns[i] { + case notification.FieldID: + if value, ok := values[i].(*uuid.UUID); !ok { + return fmt.Errorf("unexpected type %T for field id", values[i]) + } else if value != nil { + _m.ID = *value + } + case notification.FieldSubscriptionID: + if value, ok := values[i].(*uuid.UUID); !ok { + return fmt.Errorf("unexpected type %T for field subscription_id", values[i]) + } else if value != nil { + _m.SubscriptionID = *value + } + case notification.FieldAgentID: + if value, ok := values[i].(*uuid.UUID); !ok { + return fmt.Errorf("unexpected type %T for field agent_id", values[i]) + } else if value != nil { + _m.AgentID = *value + } + case notification.FieldProjectID: + if value, ok := values[i].(*uuid.UUID); !ok { + return fmt.Errorf("unexpected type %T for field project_id", values[i]) + } else if value != nil { + _m.ProjectID = *value + } + case notification.FieldSubscriberType: + if value, ok := values[i].(*sql.NullString); !ok { + return fmt.Errorf("unexpected type %T for field subscriber_type", values[i]) + } else if value.Valid { + _m.SubscriberType = value.String + } + case notification.FieldSubscriberID: + if value, ok := values[i].(*sql.NullString); !ok { + return fmt.Errorf("unexpected type %T for field subscriber_id", values[i]) + } else if value.Valid { + _m.SubscriberID = value.String + } + case notification.FieldStatus: + if value, ok := values[i].(*sql.NullString); !ok { + return fmt.Errorf("unexpected type %T for field status", values[i]) + } else if value.Valid { + _m.Status = value.String + } + case notification.FieldMessage: + if value, ok := values[i].(*sql.NullString); !ok { + return fmt.Errorf("unexpected type %T for field message", values[i]) + } else if value.Valid { + _m.Message = value.String + } + case notification.FieldDispatched: + if value, ok := values[i].(*sql.NullBool); !ok { + return fmt.Errorf("unexpected type %T for field dispatched", values[i]) + } else if value.Valid { + _m.Dispatched = value.Bool + } + case notification.FieldAcknowledged: + if value, ok := values[i].(*sql.NullBool); !ok { + return fmt.Errorf("unexpected type %T for field acknowledged", values[i]) + } else if value.Valid { + _m.Acknowledged = value.Bool + } + case notification.FieldCreated: + if value, ok := values[i].(*sql.NullTime); !ok { + return fmt.Errorf("unexpected type %T for field created", values[i]) + } else if value.Valid { + _m.Created = value.Time + } + default: + _m.selectValues.Set(columns[i], values[i]) + } + } + return nil +} + +// Value returns the ent.Value that was dynamically selected and assigned to the Notification. +// This includes values selected through modifiers, order, etc. +func (_m *Notification) Value(name string) (ent.Value, error) { + return _m.selectValues.Get(name) +} + +// Update returns a builder for updating this Notification. +// Note that you need to call Notification.Unwrap() before calling this method if this Notification +// was returned from a transaction, and the transaction was committed or rolled back. +func (_m *Notification) Update() *NotificationUpdateOne { + return NewNotificationClient(_m.config).UpdateOne(_m) +} + +// Unwrap unwraps the Notification entity that was returned from a transaction after it was closed, +// so that all future queries will be executed through the driver which created the transaction. +func (_m *Notification) Unwrap() *Notification { + _tx, ok := _m.config.driver.(*txDriver) + if !ok { + panic("ent: Notification is not a transactional entity") + } + _m.config.driver = _tx.drv + return _m +} + +// String implements the fmt.Stringer. +func (_m *Notification) String() string { + var builder strings.Builder + builder.WriteString("Notification(") + builder.WriteString(fmt.Sprintf("id=%v, ", _m.ID)) + builder.WriteString("subscription_id=") + builder.WriteString(fmt.Sprintf("%v", _m.SubscriptionID)) + builder.WriteString(", ") + builder.WriteString("agent_id=") + builder.WriteString(fmt.Sprintf("%v", _m.AgentID)) + builder.WriteString(", ") + builder.WriteString("project_id=") + builder.WriteString(fmt.Sprintf("%v", _m.ProjectID)) + builder.WriteString(", ") + builder.WriteString("subscriber_type=") + builder.WriteString(_m.SubscriberType) + builder.WriteString(", ") + builder.WriteString("subscriber_id=") + builder.WriteString(_m.SubscriberID) + builder.WriteString(", ") + builder.WriteString("status=") + builder.WriteString(_m.Status) + builder.WriteString(", ") + builder.WriteString("message=") + builder.WriteString(_m.Message) + builder.WriteString(", ") + builder.WriteString("dispatched=") + builder.WriteString(fmt.Sprintf("%v", _m.Dispatched)) + builder.WriteString(", ") + builder.WriteString("acknowledged=") + builder.WriteString(fmt.Sprintf("%v", _m.Acknowledged)) + builder.WriteString(", ") + builder.WriteString("created=") + builder.WriteString(_m.Created.Format(time.ANSIC)) + builder.WriteByte(')') + return builder.String() +} + +// Notifications is a parsable slice of Notification. +type Notifications []*Notification diff --git a/pkg/ent/notification/notification.go b/pkg/ent/notification/notification.go new file mode 100644 index 000000000..ccb96cc81 --- /dev/null +++ b/pkg/ent/notification/notification.go @@ -0,0 +1,141 @@ +// Code generated by ent, DO NOT EDIT. + +package notification + +import ( + "time" + + "entgo.io/ent/dialect/sql" + "github.com/google/uuid" +) + +const ( + // Label holds the string label denoting the notification type in the database. + Label = "notification" + // FieldID holds the string denoting the id field in the database. + FieldID = "id" + // FieldSubscriptionID holds the string denoting the subscription_id field in the database. + FieldSubscriptionID = "subscription_id" + // FieldAgentID holds the string denoting the agent_id field in the database. + FieldAgentID = "agent_id" + // FieldProjectID holds the string denoting the project_id field in the database. + FieldProjectID = "project_id" + // FieldSubscriberType holds the string denoting the subscriber_type field in the database. + FieldSubscriberType = "subscriber_type" + // FieldSubscriberID holds the string denoting the subscriber_id field in the database. + FieldSubscriberID = "subscriber_id" + // FieldStatus holds the string denoting the status field in the database. + FieldStatus = "status" + // FieldMessage holds the string denoting the message field in the database. + FieldMessage = "message" + // FieldDispatched holds the string denoting the dispatched field in the database. + FieldDispatched = "dispatched" + // FieldAcknowledged holds the string denoting the acknowledged field in the database. + FieldAcknowledged = "acknowledged" + // FieldCreated holds the string denoting the created field in the database. + FieldCreated = "created" + // Table holds the table name of the notification in the database. + Table = "notifications" +) + +// Columns holds all SQL columns for notification fields. +var Columns = []string{ + FieldID, + FieldSubscriptionID, + FieldAgentID, + FieldProjectID, + FieldSubscriberType, + FieldSubscriberID, + FieldStatus, + FieldMessage, + FieldDispatched, + FieldAcknowledged, + FieldCreated, +} + +// ValidColumn reports if the column name is valid (part of the table columns). +func ValidColumn(column string) bool { + for i := range Columns { + if column == Columns[i] { + return true + } + } + return false +} + +var ( + // SubscriberTypeValidator is a validator for the "subscriber_type" field. It is called by the builders before save. + SubscriberTypeValidator func(string) error + // SubscriberIDValidator is a validator for the "subscriber_id" field. It is called by the builders before save. + SubscriberIDValidator func(string) error + // StatusValidator is a validator for the "status" field. It is called by the builders before save. + StatusValidator func(string) error + // MessageValidator is a validator for the "message" field. It is called by the builders before save. + MessageValidator func(string) error + // DefaultDispatched holds the default value on creation for the "dispatched" field. + DefaultDispatched bool + // DefaultAcknowledged holds the default value on creation for the "acknowledged" field. + DefaultAcknowledged bool + // DefaultCreated holds the default value on creation for the "created" field. + DefaultCreated func() time.Time + // DefaultID holds the default value on creation for the "id" field. + DefaultID func() uuid.UUID +) + +// OrderOption defines the ordering options for the Notification queries. +type OrderOption func(*sql.Selector) + +// ByID orders the results by the id field. +func ByID(opts ...sql.OrderTermOption) OrderOption { + return sql.OrderByField(FieldID, opts...).ToFunc() +} + +// BySubscriptionID orders the results by the subscription_id field. +func BySubscriptionID(opts ...sql.OrderTermOption) OrderOption { + return sql.OrderByField(FieldSubscriptionID, opts...).ToFunc() +} + +// ByAgentID orders the results by the agent_id field. +func ByAgentID(opts ...sql.OrderTermOption) OrderOption { + return sql.OrderByField(FieldAgentID, opts...).ToFunc() +} + +// ByProjectID orders the results by the project_id field. +func ByProjectID(opts ...sql.OrderTermOption) OrderOption { + return sql.OrderByField(FieldProjectID, opts...).ToFunc() +} + +// BySubscriberType orders the results by the subscriber_type field. +func BySubscriberType(opts ...sql.OrderTermOption) OrderOption { + return sql.OrderByField(FieldSubscriberType, opts...).ToFunc() +} + +// BySubscriberID orders the results by the subscriber_id field. +func BySubscriberID(opts ...sql.OrderTermOption) OrderOption { + return sql.OrderByField(FieldSubscriberID, opts...).ToFunc() +} + +// ByStatus orders the results by the status field. +func ByStatus(opts ...sql.OrderTermOption) OrderOption { + return sql.OrderByField(FieldStatus, opts...).ToFunc() +} + +// ByMessage orders the results by the message field. +func ByMessage(opts ...sql.OrderTermOption) OrderOption { + return sql.OrderByField(FieldMessage, opts...).ToFunc() +} + +// ByDispatched orders the results by the dispatched field. +func ByDispatched(opts ...sql.OrderTermOption) OrderOption { + return sql.OrderByField(FieldDispatched, opts...).ToFunc() +} + +// ByAcknowledged orders the results by the acknowledged field. +func ByAcknowledged(opts ...sql.OrderTermOption) OrderOption { + return sql.OrderByField(FieldAcknowledged, opts...).ToFunc() +} + +// ByCreated orders the results by the created field. +func ByCreated(opts ...sql.OrderTermOption) OrderOption { + return sql.OrderByField(FieldCreated, opts...).ToFunc() +} diff --git a/pkg/ent/notification/where.go b/pkg/ent/notification/where.go new file mode 100644 index 000000000..1dd76dd11 --- /dev/null +++ b/pkg/ent/notification/where.go @@ -0,0 +1,561 @@ +// Code generated by ent, DO NOT EDIT. + +package notification + +import ( + "time" + + "entgo.io/ent/dialect/sql" + "github.com/GoogleCloudPlatform/scion/pkg/ent/predicate" + "github.com/google/uuid" +) + +// ID filters vertices based on their ID field. +func ID(id uuid.UUID) predicate.Notification { + return predicate.Notification(sql.FieldEQ(FieldID, id)) +} + +// IDEQ applies the EQ predicate on the ID field. +func IDEQ(id uuid.UUID) predicate.Notification { + return predicate.Notification(sql.FieldEQ(FieldID, id)) +} + +// IDNEQ applies the NEQ predicate on the ID field. +func IDNEQ(id uuid.UUID) predicate.Notification { + return predicate.Notification(sql.FieldNEQ(FieldID, id)) +} + +// IDIn applies the In predicate on the ID field. +func IDIn(ids ...uuid.UUID) predicate.Notification { + return predicate.Notification(sql.FieldIn(FieldID, ids...)) +} + +// IDNotIn applies the NotIn predicate on the ID field. +func IDNotIn(ids ...uuid.UUID) predicate.Notification { + return predicate.Notification(sql.FieldNotIn(FieldID, ids...)) +} + +// IDGT applies the GT predicate on the ID field. +func IDGT(id uuid.UUID) predicate.Notification { + return predicate.Notification(sql.FieldGT(FieldID, id)) +} + +// IDGTE applies the GTE predicate on the ID field. +func IDGTE(id uuid.UUID) predicate.Notification { + return predicate.Notification(sql.FieldGTE(FieldID, id)) +} + +// IDLT applies the LT predicate on the ID field. +func IDLT(id uuid.UUID) predicate.Notification { + return predicate.Notification(sql.FieldLT(FieldID, id)) +} + +// IDLTE applies the LTE predicate on the ID field. +func IDLTE(id uuid.UUID) predicate.Notification { + return predicate.Notification(sql.FieldLTE(FieldID, id)) +} + +// SubscriptionID applies equality check predicate on the "subscription_id" field. It's identical to SubscriptionIDEQ. +func SubscriptionID(v uuid.UUID) predicate.Notification { + return predicate.Notification(sql.FieldEQ(FieldSubscriptionID, v)) +} + +// AgentID applies equality check predicate on the "agent_id" field. It's identical to AgentIDEQ. +func AgentID(v uuid.UUID) predicate.Notification { + return predicate.Notification(sql.FieldEQ(FieldAgentID, v)) +} + +// ProjectID applies equality check predicate on the "project_id" field. It's identical to ProjectIDEQ. +func ProjectID(v uuid.UUID) predicate.Notification { + return predicate.Notification(sql.FieldEQ(FieldProjectID, v)) +} + +// SubscriberType applies equality check predicate on the "subscriber_type" field. It's identical to SubscriberTypeEQ. +func SubscriberType(v string) predicate.Notification { + return predicate.Notification(sql.FieldEQ(FieldSubscriberType, v)) +} + +// SubscriberID applies equality check predicate on the "subscriber_id" field. It's identical to SubscriberIDEQ. +func SubscriberID(v string) predicate.Notification { + return predicate.Notification(sql.FieldEQ(FieldSubscriberID, v)) +} + +// Status applies equality check predicate on the "status" field. It's identical to StatusEQ. +func Status(v string) predicate.Notification { + return predicate.Notification(sql.FieldEQ(FieldStatus, v)) +} + +// Message applies equality check predicate on the "message" field. It's identical to MessageEQ. +func Message(v string) predicate.Notification { + return predicate.Notification(sql.FieldEQ(FieldMessage, v)) +} + +// Dispatched applies equality check predicate on the "dispatched" field. It's identical to DispatchedEQ. +func Dispatched(v bool) predicate.Notification { + return predicate.Notification(sql.FieldEQ(FieldDispatched, v)) +} + +// Acknowledged applies equality check predicate on the "acknowledged" field. It's identical to AcknowledgedEQ. +func Acknowledged(v bool) predicate.Notification { + return predicate.Notification(sql.FieldEQ(FieldAcknowledged, v)) +} + +// Created applies equality check predicate on the "created" field. It's identical to CreatedEQ. +func Created(v time.Time) predicate.Notification { + return predicate.Notification(sql.FieldEQ(FieldCreated, v)) +} + +// SubscriptionIDEQ applies the EQ predicate on the "subscription_id" field. +func SubscriptionIDEQ(v uuid.UUID) predicate.Notification { + return predicate.Notification(sql.FieldEQ(FieldSubscriptionID, v)) +} + +// SubscriptionIDNEQ applies the NEQ predicate on the "subscription_id" field. +func SubscriptionIDNEQ(v uuid.UUID) predicate.Notification { + return predicate.Notification(sql.FieldNEQ(FieldSubscriptionID, v)) +} + +// SubscriptionIDIn applies the In predicate on the "subscription_id" field. +func SubscriptionIDIn(vs ...uuid.UUID) predicate.Notification { + return predicate.Notification(sql.FieldIn(FieldSubscriptionID, vs...)) +} + +// SubscriptionIDNotIn applies the NotIn predicate on the "subscription_id" field. +func SubscriptionIDNotIn(vs ...uuid.UUID) predicate.Notification { + return predicate.Notification(sql.FieldNotIn(FieldSubscriptionID, vs...)) +} + +// SubscriptionIDGT applies the GT predicate on the "subscription_id" field. +func SubscriptionIDGT(v uuid.UUID) predicate.Notification { + return predicate.Notification(sql.FieldGT(FieldSubscriptionID, v)) +} + +// SubscriptionIDGTE applies the GTE predicate on the "subscription_id" field. +func SubscriptionIDGTE(v uuid.UUID) predicate.Notification { + return predicate.Notification(sql.FieldGTE(FieldSubscriptionID, v)) +} + +// SubscriptionIDLT applies the LT predicate on the "subscription_id" field. +func SubscriptionIDLT(v uuid.UUID) predicate.Notification { + return predicate.Notification(sql.FieldLT(FieldSubscriptionID, v)) +} + +// SubscriptionIDLTE applies the LTE predicate on the "subscription_id" field. +func SubscriptionIDLTE(v uuid.UUID) predicate.Notification { + return predicate.Notification(sql.FieldLTE(FieldSubscriptionID, v)) +} + +// AgentIDEQ applies the EQ predicate on the "agent_id" field. +func AgentIDEQ(v uuid.UUID) predicate.Notification { + return predicate.Notification(sql.FieldEQ(FieldAgentID, v)) +} + +// AgentIDNEQ applies the NEQ predicate on the "agent_id" field. +func AgentIDNEQ(v uuid.UUID) predicate.Notification { + return predicate.Notification(sql.FieldNEQ(FieldAgentID, v)) +} + +// AgentIDIn applies the In predicate on the "agent_id" field. +func AgentIDIn(vs ...uuid.UUID) predicate.Notification { + return predicate.Notification(sql.FieldIn(FieldAgentID, vs...)) +} + +// AgentIDNotIn applies the NotIn predicate on the "agent_id" field. +func AgentIDNotIn(vs ...uuid.UUID) predicate.Notification { + return predicate.Notification(sql.FieldNotIn(FieldAgentID, vs...)) +} + +// AgentIDGT applies the GT predicate on the "agent_id" field. +func AgentIDGT(v uuid.UUID) predicate.Notification { + return predicate.Notification(sql.FieldGT(FieldAgentID, v)) +} + +// AgentIDGTE applies the GTE predicate on the "agent_id" field. +func AgentIDGTE(v uuid.UUID) predicate.Notification { + return predicate.Notification(sql.FieldGTE(FieldAgentID, v)) +} + +// AgentIDLT applies the LT predicate on the "agent_id" field. +func AgentIDLT(v uuid.UUID) predicate.Notification { + return predicate.Notification(sql.FieldLT(FieldAgentID, v)) +} + +// AgentIDLTE applies the LTE predicate on the "agent_id" field. +func AgentIDLTE(v uuid.UUID) predicate.Notification { + return predicate.Notification(sql.FieldLTE(FieldAgentID, v)) +} + +// ProjectIDEQ applies the EQ predicate on the "project_id" field. +func ProjectIDEQ(v uuid.UUID) predicate.Notification { + return predicate.Notification(sql.FieldEQ(FieldProjectID, v)) +} + +// ProjectIDNEQ applies the NEQ predicate on the "project_id" field. +func ProjectIDNEQ(v uuid.UUID) predicate.Notification { + return predicate.Notification(sql.FieldNEQ(FieldProjectID, v)) +} + +// ProjectIDIn applies the In predicate on the "project_id" field. +func ProjectIDIn(vs ...uuid.UUID) predicate.Notification { + return predicate.Notification(sql.FieldIn(FieldProjectID, vs...)) +} + +// ProjectIDNotIn applies the NotIn predicate on the "project_id" field. +func ProjectIDNotIn(vs ...uuid.UUID) predicate.Notification { + return predicate.Notification(sql.FieldNotIn(FieldProjectID, vs...)) +} + +// ProjectIDGT applies the GT predicate on the "project_id" field. +func ProjectIDGT(v uuid.UUID) predicate.Notification { + return predicate.Notification(sql.FieldGT(FieldProjectID, v)) +} + +// ProjectIDGTE applies the GTE predicate on the "project_id" field. +func ProjectIDGTE(v uuid.UUID) predicate.Notification { + return predicate.Notification(sql.FieldGTE(FieldProjectID, v)) +} + +// ProjectIDLT applies the LT predicate on the "project_id" field. +func ProjectIDLT(v uuid.UUID) predicate.Notification { + return predicate.Notification(sql.FieldLT(FieldProjectID, v)) +} + +// ProjectIDLTE applies the LTE predicate on the "project_id" field. +func ProjectIDLTE(v uuid.UUID) predicate.Notification { + return predicate.Notification(sql.FieldLTE(FieldProjectID, v)) +} + +// SubscriberTypeEQ applies the EQ predicate on the "subscriber_type" field. +func SubscriberTypeEQ(v string) predicate.Notification { + return predicate.Notification(sql.FieldEQ(FieldSubscriberType, v)) +} + +// SubscriberTypeNEQ applies the NEQ predicate on the "subscriber_type" field. +func SubscriberTypeNEQ(v string) predicate.Notification { + return predicate.Notification(sql.FieldNEQ(FieldSubscriberType, v)) +} + +// SubscriberTypeIn applies the In predicate on the "subscriber_type" field. +func SubscriberTypeIn(vs ...string) predicate.Notification { + return predicate.Notification(sql.FieldIn(FieldSubscriberType, vs...)) +} + +// SubscriberTypeNotIn applies the NotIn predicate on the "subscriber_type" field. +func SubscriberTypeNotIn(vs ...string) predicate.Notification { + return predicate.Notification(sql.FieldNotIn(FieldSubscriberType, vs...)) +} + +// SubscriberTypeGT applies the GT predicate on the "subscriber_type" field. +func SubscriberTypeGT(v string) predicate.Notification { + return predicate.Notification(sql.FieldGT(FieldSubscriberType, v)) +} + +// SubscriberTypeGTE applies the GTE predicate on the "subscriber_type" field. +func SubscriberTypeGTE(v string) predicate.Notification { + return predicate.Notification(sql.FieldGTE(FieldSubscriberType, v)) +} + +// SubscriberTypeLT applies the LT predicate on the "subscriber_type" field. +func SubscriberTypeLT(v string) predicate.Notification { + return predicate.Notification(sql.FieldLT(FieldSubscriberType, v)) +} + +// SubscriberTypeLTE applies the LTE predicate on the "subscriber_type" field. +func SubscriberTypeLTE(v string) predicate.Notification { + return predicate.Notification(sql.FieldLTE(FieldSubscriberType, v)) +} + +// SubscriberTypeContains applies the Contains predicate on the "subscriber_type" field. +func SubscriberTypeContains(v string) predicate.Notification { + return predicate.Notification(sql.FieldContains(FieldSubscriberType, v)) +} + +// SubscriberTypeHasPrefix applies the HasPrefix predicate on the "subscriber_type" field. +func SubscriberTypeHasPrefix(v string) predicate.Notification { + return predicate.Notification(sql.FieldHasPrefix(FieldSubscriberType, v)) +} + +// SubscriberTypeHasSuffix applies the HasSuffix predicate on the "subscriber_type" field. +func SubscriberTypeHasSuffix(v string) predicate.Notification { + return predicate.Notification(sql.FieldHasSuffix(FieldSubscriberType, v)) +} + +// SubscriberTypeEqualFold applies the EqualFold predicate on the "subscriber_type" field. +func SubscriberTypeEqualFold(v string) predicate.Notification { + return predicate.Notification(sql.FieldEqualFold(FieldSubscriberType, v)) +} + +// SubscriberTypeContainsFold applies the ContainsFold predicate on the "subscriber_type" field. +func SubscriberTypeContainsFold(v string) predicate.Notification { + return predicate.Notification(sql.FieldContainsFold(FieldSubscriberType, v)) +} + +// SubscriberIDEQ applies the EQ predicate on the "subscriber_id" field. +func SubscriberIDEQ(v string) predicate.Notification { + return predicate.Notification(sql.FieldEQ(FieldSubscriberID, v)) +} + +// SubscriberIDNEQ applies the NEQ predicate on the "subscriber_id" field. +func SubscriberIDNEQ(v string) predicate.Notification { + return predicate.Notification(sql.FieldNEQ(FieldSubscriberID, v)) +} + +// SubscriberIDIn applies the In predicate on the "subscriber_id" field. +func SubscriberIDIn(vs ...string) predicate.Notification { + return predicate.Notification(sql.FieldIn(FieldSubscriberID, vs...)) +} + +// SubscriberIDNotIn applies the NotIn predicate on the "subscriber_id" field. +func SubscriberIDNotIn(vs ...string) predicate.Notification { + return predicate.Notification(sql.FieldNotIn(FieldSubscriberID, vs...)) +} + +// SubscriberIDGT applies the GT predicate on the "subscriber_id" field. +func SubscriberIDGT(v string) predicate.Notification { + return predicate.Notification(sql.FieldGT(FieldSubscriberID, v)) +} + +// SubscriberIDGTE applies the GTE predicate on the "subscriber_id" field. +func SubscriberIDGTE(v string) predicate.Notification { + return predicate.Notification(sql.FieldGTE(FieldSubscriberID, v)) +} + +// SubscriberIDLT applies the LT predicate on the "subscriber_id" field. +func SubscriberIDLT(v string) predicate.Notification { + return predicate.Notification(sql.FieldLT(FieldSubscriberID, v)) +} + +// SubscriberIDLTE applies the LTE predicate on the "subscriber_id" field. +func SubscriberIDLTE(v string) predicate.Notification { + return predicate.Notification(sql.FieldLTE(FieldSubscriberID, v)) +} + +// SubscriberIDContains applies the Contains predicate on the "subscriber_id" field. +func SubscriberIDContains(v string) predicate.Notification { + return predicate.Notification(sql.FieldContains(FieldSubscriberID, v)) +} + +// SubscriberIDHasPrefix applies the HasPrefix predicate on the "subscriber_id" field. +func SubscriberIDHasPrefix(v string) predicate.Notification { + return predicate.Notification(sql.FieldHasPrefix(FieldSubscriberID, v)) +} + +// SubscriberIDHasSuffix applies the HasSuffix predicate on the "subscriber_id" field. +func SubscriberIDHasSuffix(v string) predicate.Notification { + return predicate.Notification(sql.FieldHasSuffix(FieldSubscriberID, v)) +} + +// SubscriberIDEqualFold applies the EqualFold predicate on the "subscriber_id" field. +func SubscriberIDEqualFold(v string) predicate.Notification { + return predicate.Notification(sql.FieldEqualFold(FieldSubscriberID, v)) +} + +// SubscriberIDContainsFold applies the ContainsFold predicate on the "subscriber_id" field. +func SubscriberIDContainsFold(v string) predicate.Notification { + return predicate.Notification(sql.FieldContainsFold(FieldSubscriberID, v)) +} + +// StatusEQ applies the EQ predicate on the "status" field. +func StatusEQ(v string) predicate.Notification { + return predicate.Notification(sql.FieldEQ(FieldStatus, v)) +} + +// StatusNEQ applies the NEQ predicate on the "status" field. +func StatusNEQ(v string) predicate.Notification { + return predicate.Notification(sql.FieldNEQ(FieldStatus, v)) +} + +// StatusIn applies the In predicate on the "status" field. +func StatusIn(vs ...string) predicate.Notification { + return predicate.Notification(sql.FieldIn(FieldStatus, vs...)) +} + +// StatusNotIn applies the NotIn predicate on the "status" field. +func StatusNotIn(vs ...string) predicate.Notification { + return predicate.Notification(sql.FieldNotIn(FieldStatus, vs...)) +} + +// StatusGT applies the GT predicate on the "status" field. +func StatusGT(v string) predicate.Notification { + return predicate.Notification(sql.FieldGT(FieldStatus, v)) +} + +// StatusGTE applies the GTE predicate on the "status" field. +func StatusGTE(v string) predicate.Notification { + return predicate.Notification(sql.FieldGTE(FieldStatus, v)) +} + +// StatusLT applies the LT predicate on the "status" field. +func StatusLT(v string) predicate.Notification { + return predicate.Notification(sql.FieldLT(FieldStatus, v)) +} + +// StatusLTE applies the LTE predicate on the "status" field. +func StatusLTE(v string) predicate.Notification { + return predicate.Notification(sql.FieldLTE(FieldStatus, v)) +} + +// StatusContains applies the Contains predicate on the "status" field. +func StatusContains(v string) predicate.Notification { + return predicate.Notification(sql.FieldContains(FieldStatus, v)) +} + +// StatusHasPrefix applies the HasPrefix predicate on the "status" field. +func StatusHasPrefix(v string) predicate.Notification { + return predicate.Notification(sql.FieldHasPrefix(FieldStatus, v)) +} + +// StatusHasSuffix applies the HasSuffix predicate on the "status" field. +func StatusHasSuffix(v string) predicate.Notification { + return predicate.Notification(sql.FieldHasSuffix(FieldStatus, v)) +} + +// StatusEqualFold applies the EqualFold predicate on the "status" field. +func StatusEqualFold(v string) predicate.Notification { + return predicate.Notification(sql.FieldEqualFold(FieldStatus, v)) +} + +// StatusContainsFold applies the ContainsFold predicate on the "status" field. +func StatusContainsFold(v string) predicate.Notification { + return predicate.Notification(sql.FieldContainsFold(FieldStatus, v)) +} + +// MessageEQ applies the EQ predicate on the "message" field. +func MessageEQ(v string) predicate.Notification { + return predicate.Notification(sql.FieldEQ(FieldMessage, v)) +} + +// MessageNEQ applies the NEQ predicate on the "message" field. +func MessageNEQ(v string) predicate.Notification { + return predicate.Notification(sql.FieldNEQ(FieldMessage, v)) +} + +// MessageIn applies the In predicate on the "message" field. +func MessageIn(vs ...string) predicate.Notification { + return predicate.Notification(sql.FieldIn(FieldMessage, vs...)) +} + +// MessageNotIn applies the NotIn predicate on the "message" field. +func MessageNotIn(vs ...string) predicate.Notification { + return predicate.Notification(sql.FieldNotIn(FieldMessage, vs...)) +} + +// MessageGT applies the GT predicate on the "message" field. +func MessageGT(v string) predicate.Notification { + return predicate.Notification(sql.FieldGT(FieldMessage, v)) +} + +// MessageGTE applies the GTE predicate on the "message" field. +func MessageGTE(v string) predicate.Notification { + return predicate.Notification(sql.FieldGTE(FieldMessage, v)) +} + +// MessageLT applies the LT predicate on the "message" field. +func MessageLT(v string) predicate.Notification { + return predicate.Notification(sql.FieldLT(FieldMessage, v)) +} + +// MessageLTE applies the LTE predicate on the "message" field. +func MessageLTE(v string) predicate.Notification { + return predicate.Notification(sql.FieldLTE(FieldMessage, v)) +} + +// MessageContains applies the Contains predicate on the "message" field. +func MessageContains(v string) predicate.Notification { + return predicate.Notification(sql.FieldContains(FieldMessage, v)) +} + +// MessageHasPrefix applies the HasPrefix predicate on the "message" field. +func MessageHasPrefix(v string) predicate.Notification { + return predicate.Notification(sql.FieldHasPrefix(FieldMessage, v)) +} + +// MessageHasSuffix applies the HasSuffix predicate on the "message" field. +func MessageHasSuffix(v string) predicate.Notification { + return predicate.Notification(sql.FieldHasSuffix(FieldMessage, v)) +} + +// MessageEqualFold applies the EqualFold predicate on the "message" field. +func MessageEqualFold(v string) predicate.Notification { + return predicate.Notification(sql.FieldEqualFold(FieldMessage, v)) +} + +// MessageContainsFold applies the ContainsFold predicate on the "message" field. +func MessageContainsFold(v string) predicate.Notification { + return predicate.Notification(sql.FieldContainsFold(FieldMessage, v)) +} + +// DispatchedEQ applies the EQ predicate on the "dispatched" field. +func DispatchedEQ(v bool) predicate.Notification { + return predicate.Notification(sql.FieldEQ(FieldDispatched, v)) +} + +// DispatchedNEQ applies the NEQ predicate on the "dispatched" field. +func DispatchedNEQ(v bool) predicate.Notification { + return predicate.Notification(sql.FieldNEQ(FieldDispatched, v)) +} + +// AcknowledgedEQ applies the EQ predicate on the "acknowledged" field. +func AcknowledgedEQ(v bool) predicate.Notification { + return predicate.Notification(sql.FieldEQ(FieldAcknowledged, v)) +} + +// AcknowledgedNEQ applies the NEQ predicate on the "acknowledged" field. +func AcknowledgedNEQ(v bool) predicate.Notification { + return predicate.Notification(sql.FieldNEQ(FieldAcknowledged, v)) +} + +// CreatedEQ applies the EQ predicate on the "created" field. +func CreatedEQ(v time.Time) predicate.Notification { + return predicate.Notification(sql.FieldEQ(FieldCreated, v)) +} + +// CreatedNEQ applies the NEQ predicate on the "created" field. +func CreatedNEQ(v time.Time) predicate.Notification { + return predicate.Notification(sql.FieldNEQ(FieldCreated, v)) +} + +// CreatedIn applies the In predicate on the "created" field. +func CreatedIn(vs ...time.Time) predicate.Notification { + return predicate.Notification(sql.FieldIn(FieldCreated, vs...)) +} + +// CreatedNotIn applies the NotIn predicate on the "created" field. +func CreatedNotIn(vs ...time.Time) predicate.Notification { + return predicate.Notification(sql.FieldNotIn(FieldCreated, vs...)) +} + +// CreatedGT applies the GT predicate on the "created" field. +func CreatedGT(v time.Time) predicate.Notification { + return predicate.Notification(sql.FieldGT(FieldCreated, v)) +} + +// CreatedGTE applies the GTE predicate on the "created" field. +func CreatedGTE(v time.Time) predicate.Notification { + return predicate.Notification(sql.FieldGTE(FieldCreated, v)) +} + +// CreatedLT applies the LT predicate on the "created" field. +func CreatedLT(v time.Time) predicate.Notification { + return predicate.Notification(sql.FieldLT(FieldCreated, v)) +} + +// CreatedLTE applies the LTE predicate on the "created" field. +func CreatedLTE(v time.Time) predicate.Notification { + return predicate.Notification(sql.FieldLTE(FieldCreated, v)) +} + +// And groups predicates with the AND operator between them. +func And(predicates ...predicate.Notification) predicate.Notification { + return predicate.Notification(sql.AndPredicates(predicates...)) +} + +// Or groups predicates with the OR operator between them. +func Or(predicates ...predicate.Notification) predicate.Notification { + return predicate.Notification(sql.OrPredicates(predicates...)) +} + +// Not applies the not operator on the given predicate. +func Not(p predicate.Notification) predicate.Notification { + return predicate.Notification(sql.NotPredicates(p)) +} diff --git a/pkg/ent/notification_create.go b/pkg/ent/notification_create.go new file mode 100644 index 000000000..94bb3994b --- /dev/null +++ b/pkg/ent/notification_create.go @@ -0,0 +1,1008 @@ +// Code generated by ent, DO NOT EDIT. + +package ent + +import ( + "context" + "errors" + "fmt" + "time" + + "entgo.io/ent/dialect" + "entgo.io/ent/dialect/sql" + "entgo.io/ent/dialect/sql/sqlgraph" + "entgo.io/ent/schema/field" + "github.com/GoogleCloudPlatform/scion/pkg/ent/notification" + "github.com/google/uuid" +) + +// NotificationCreate is the builder for creating a Notification entity. +type NotificationCreate struct { + config + mutation *NotificationMutation + hooks []Hook + conflict []sql.ConflictOption +} + +// SetSubscriptionID sets the "subscription_id" field. +func (_c *NotificationCreate) SetSubscriptionID(v uuid.UUID) *NotificationCreate { + _c.mutation.SetSubscriptionID(v) + return _c +} + +// SetAgentID sets the "agent_id" field. +func (_c *NotificationCreate) SetAgentID(v uuid.UUID) *NotificationCreate { + _c.mutation.SetAgentID(v) + return _c +} + +// SetProjectID sets the "project_id" field. +func (_c *NotificationCreate) SetProjectID(v uuid.UUID) *NotificationCreate { + _c.mutation.SetProjectID(v) + return _c +} + +// SetSubscriberType sets the "subscriber_type" field. +func (_c *NotificationCreate) SetSubscriberType(v string) *NotificationCreate { + _c.mutation.SetSubscriberType(v) + return _c +} + +// SetSubscriberID sets the "subscriber_id" field. +func (_c *NotificationCreate) SetSubscriberID(v string) *NotificationCreate { + _c.mutation.SetSubscriberID(v) + return _c +} + +// SetStatus sets the "status" field. +func (_c *NotificationCreate) SetStatus(v string) *NotificationCreate { + _c.mutation.SetStatus(v) + return _c +} + +// SetMessage sets the "message" field. +func (_c *NotificationCreate) SetMessage(v string) *NotificationCreate { + _c.mutation.SetMessage(v) + return _c +} + +// SetDispatched sets the "dispatched" field. +func (_c *NotificationCreate) SetDispatched(v bool) *NotificationCreate { + _c.mutation.SetDispatched(v) + return _c +} + +// SetNillableDispatched sets the "dispatched" field if the given value is not nil. +func (_c *NotificationCreate) SetNillableDispatched(v *bool) *NotificationCreate { + if v != nil { + _c.SetDispatched(*v) + } + return _c +} + +// SetAcknowledged sets the "acknowledged" field. +func (_c *NotificationCreate) SetAcknowledged(v bool) *NotificationCreate { + _c.mutation.SetAcknowledged(v) + return _c +} + +// SetNillableAcknowledged sets the "acknowledged" field if the given value is not nil. +func (_c *NotificationCreate) SetNillableAcknowledged(v *bool) *NotificationCreate { + if v != nil { + _c.SetAcknowledged(*v) + } + return _c +} + +// SetCreated sets the "created" field. +func (_c *NotificationCreate) SetCreated(v time.Time) *NotificationCreate { + _c.mutation.SetCreated(v) + return _c +} + +// SetNillableCreated sets the "created" field if the given value is not nil. +func (_c *NotificationCreate) SetNillableCreated(v *time.Time) *NotificationCreate { + if v != nil { + _c.SetCreated(*v) + } + return _c +} + +// SetID sets the "id" field. +func (_c *NotificationCreate) SetID(v uuid.UUID) *NotificationCreate { + _c.mutation.SetID(v) + return _c +} + +// SetNillableID sets the "id" field if the given value is not nil. +func (_c *NotificationCreate) SetNillableID(v *uuid.UUID) *NotificationCreate { + if v != nil { + _c.SetID(*v) + } + return _c +} + +// Mutation returns the NotificationMutation object of the builder. +func (_c *NotificationCreate) Mutation() *NotificationMutation { + return _c.mutation +} + +// Save creates the Notification in the database. +func (_c *NotificationCreate) Save(ctx context.Context) (*Notification, error) { + _c.defaults() + return withHooks(ctx, _c.sqlSave, _c.mutation, _c.hooks) +} + +// SaveX calls Save and panics if Save returns an error. +func (_c *NotificationCreate) SaveX(ctx context.Context) *Notification { + v, err := _c.Save(ctx) + if err != nil { + panic(err) + } + return v +} + +// Exec executes the query. +func (_c *NotificationCreate) Exec(ctx context.Context) error { + _, err := _c.Save(ctx) + return err +} + +// ExecX is like Exec, but panics if an error occurs. +func (_c *NotificationCreate) ExecX(ctx context.Context) { + if err := _c.Exec(ctx); err != nil { + panic(err) + } +} + +// defaults sets the default values of the builder before save. +func (_c *NotificationCreate) defaults() { + if _, ok := _c.mutation.Dispatched(); !ok { + v := notification.DefaultDispatched + _c.mutation.SetDispatched(v) + } + if _, ok := _c.mutation.Acknowledged(); !ok { + v := notification.DefaultAcknowledged + _c.mutation.SetAcknowledged(v) + } + if _, ok := _c.mutation.Created(); !ok { + v := notification.DefaultCreated() + _c.mutation.SetCreated(v) + } + if _, ok := _c.mutation.ID(); !ok { + v := notification.DefaultID() + _c.mutation.SetID(v) + } +} + +// check runs all checks and user-defined validators on the builder. +func (_c *NotificationCreate) check() error { + if _, ok := _c.mutation.SubscriptionID(); !ok { + return &ValidationError{Name: "subscription_id", err: errors.New(`ent: missing required field "Notification.subscription_id"`)} + } + if _, ok := _c.mutation.AgentID(); !ok { + return &ValidationError{Name: "agent_id", err: errors.New(`ent: missing required field "Notification.agent_id"`)} + } + if _, ok := _c.mutation.ProjectID(); !ok { + return &ValidationError{Name: "project_id", err: errors.New(`ent: missing required field "Notification.project_id"`)} + } + if _, ok := _c.mutation.SubscriberType(); !ok { + return &ValidationError{Name: "subscriber_type", err: errors.New(`ent: missing required field "Notification.subscriber_type"`)} + } + if v, ok := _c.mutation.SubscriberType(); ok { + if err := notification.SubscriberTypeValidator(v); err != nil { + return &ValidationError{Name: "subscriber_type", err: fmt.Errorf(`ent: validator failed for field "Notification.subscriber_type": %w`, err)} + } + } + if _, ok := _c.mutation.SubscriberID(); !ok { + return &ValidationError{Name: "subscriber_id", err: errors.New(`ent: missing required field "Notification.subscriber_id"`)} + } + if v, ok := _c.mutation.SubscriberID(); ok { + if err := notification.SubscriberIDValidator(v); err != nil { + return &ValidationError{Name: "subscriber_id", err: fmt.Errorf(`ent: validator failed for field "Notification.subscriber_id": %w`, err)} + } + } + if _, ok := _c.mutation.Status(); !ok { + return &ValidationError{Name: "status", err: errors.New(`ent: missing required field "Notification.status"`)} + } + if v, ok := _c.mutation.Status(); ok { + if err := notification.StatusValidator(v); err != nil { + return &ValidationError{Name: "status", err: fmt.Errorf(`ent: validator failed for field "Notification.status": %w`, err)} + } + } + if _, ok := _c.mutation.Message(); !ok { + return &ValidationError{Name: "message", err: errors.New(`ent: missing required field "Notification.message"`)} + } + if v, ok := _c.mutation.Message(); ok { + if err := notification.MessageValidator(v); err != nil { + return &ValidationError{Name: "message", err: fmt.Errorf(`ent: validator failed for field "Notification.message": %w`, err)} + } + } + if _, ok := _c.mutation.Dispatched(); !ok { + return &ValidationError{Name: "dispatched", err: errors.New(`ent: missing required field "Notification.dispatched"`)} + } + if _, ok := _c.mutation.Acknowledged(); !ok { + return &ValidationError{Name: "acknowledged", err: errors.New(`ent: missing required field "Notification.acknowledged"`)} + } + if _, ok := _c.mutation.Created(); !ok { + return &ValidationError{Name: "created", err: errors.New(`ent: missing required field "Notification.created"`)} + } + return nil +} + +func (_c *NotificationCreate) sqlSave(ctx context.Context) (*Notification, error) { + if err := _c.check(); err != nil { + return nil, err + } + _node, _spec := _c.createSpec() + if err := sqlgraph.CreateNode(ctx, _c.driver, _spec); err != nil { + if sqlgraph.IsConstraintError(err) { + err = &ConstraintError{msg: err.Error(), wrap: err} + } + return nil, err + } + if _spec.ID.Value != nil { + if id, ok := _spec.ID.Value.(*uuid.UUID); ok { + _node.ID = *id + } else if err := _node.ID.Scan(_spec.ID.Value); err != nil { + return nil, err + } + } + _c.mutation.id = &_node.ID + _c.mutation.done = true + return _node, nil +} + +func (_c *NotificationCreate) createSpec() (*Notification, *sqlgraph.CreateSpec) { + var ( + _node = &Notification{config: _c.config} + _spec = sqlgraph.NewCreateSpec(notification.Table, sqlgraph.NewFieldSpec(notification.FieldID, field.TypeUUID)) + ) + _spec.OnConflict = _c.conflict + if id, ok := _c.mutation.ID(); ok { + _node.ID = id + _spec.ID.Value = &id + } + if value, ok := _c.mutation.SubscriptionID(); ok { + _spec.SetField(notification.FieldSubscriptionID, field.TypeUUID, value) + _node.SubscriptionID = value + } + if value, ok := _c.mutation.AgentID(); ok { + _spec.SetField(notification.FieldAgentID, field.TypeUUID, value) + _node.AgentID = value + } + if value, ok := _c.mutation.ProjectID(); ok { + _spec.SetField(notification.FieldProjectID, field.TypeUUID, value) + _node.ProjectID = value + } + if value, ok := _c.mutation.SubscriberType(); ok { + _spec.SetField(notification.FieldSubscriberType, field.TypeString, value) + _node.SubscriberType = value + } + if value, ok := _c.mutation.SubscriberID(); ok { + _spec.SetField(notification.FieldSubscriberID, field.TypeString, value) + _node.SubscriberID = value + } + if value, ok := _c.mutation.Status(); ok { + _spec.SetField(notification.FieldStatus, field.TypeString, value) + _node.Status = value + } + if value, ok := _c.mutation.Message(); ok { + _spec.SetField(notification.FieldMessage, field.TypeString, value) + _node.Message = value + } + if value, ok := _c.mutation.Dispatched(); ok { + _spec.SetField(notification.FieldDispatched, field.TypeBool, value) + _node.Dispatched = value + } + if value, ok := _c.mutation.Acknowledged(); ok { + _spec.SetField(notification.FieldAcknowledged, field.TypeBool, value) + _node.Acknowledged = value + } + if value, ok := _c.mutation.Created(); ok { + _spec.SetField(notification.FieldCreated, field.TypeTime, value) + _node.Created = value + } + return _node, _spec +} + +// OnConflict allows configuring the `ON CONFLICT` / `ON DUPLICATE KEY` clause +// of the `INSERT` statement. For example: +// +// client.Notification.Create(). +// SetSubscriptionID(v). +// OnConflict( +// // Update the row with the new values +// // the was proposed for insertion. +// sql.ResolveWithNewValues(), +// ). +// // Override some of the fields with custom +// // update values. +// Update(func(u *ent.NotificationUpsert) { +// SetSubscriptionID(v+v). +// }). +// Exec(ctx) +func (_c *NotificationCreate) OnConflict(opts ...sql.ConflictOption) *NotificationUpsertOne { + _c.conflict = opts + return &NotificationUpsertOne{ + create: _c, + } +} + +// OnConflictColumns calls `OnConflict` and configures the columns +// as conflict target. Using this option is equivalent to using: +// +// client.Notification.Create(). +// OnConflict(sql.ConflictColumns(columns...)). +// Exec(ctx) +func (_c *NotificationCreate) OnConflictColumns(columns ...string) *NotificationUpsertOne { + _c.conflict = append(_c.conflict, sql.ConflictColumns(columns...)) + return &NotificationUpsertOne{ + create: _c, + } +} + +type ( + // NotificationUpsertOne is the builder for "upsert"-ing + // one Notification node. + NotificationUpsertOne struct { + create *NotificationCreate + } + + // NotificationUpsert is the "OnConflict" setter. + NotificationUpsert struct { + *sql.UpdateSet + } +) + +// SetSubscriptionID sets the "subscription_id" field. +func (u *NotificationUpsert) SetSubscriptionID(v uuid.UUID) *NotificationUpsert { + u.Set(notification.FieldSubscriptionID, v) + return u +} + +// UpdateSubscriptionID sets the "subscription_id" field to the value that was provided on create. +func (u *NotificationUpsert) UpdateSubscriptionID() *NotificationUpsert { + u.SetExcluded(notification.FieldSubscriptionID) + return u +} + +// SetAgentID sets the "agent_id" field. +func (u *NotificationUpsert) SetAgentID(v uuid.UUID) *NotificationUpsert { + u.Set(notification.FieldAgentID, v) + return u +} + +// UpdateAgentID sets the "agent_id" field to the value that was provided on create. +func (u *NotificationUpsert) UpdateAgentID() *NotificationUpsert { + u.SetExcluded(notification.FieldAgentID) + return u +} + +// SetProjectID sets the "project_id" field. +func (u *NotificationUpsert) SetProjectID(v uuid.UUID) *NotificationUpsert { + u.Set(notification.FieldProjectID, v) + return u +} + +// UpdateProjectID sets the "project_id" field to the value that was provided on create. +func (u *NotificationUpsert) UpdateProjectID() *NotificationUpsert { + u.SetExcluded(notification.FieldProjectID) + return u +} + +// SetSubscriberType sets the "subscriber_type" field. +func (u *NotificationUpsert) SetSubscriberType(v string) *NotificationUpsert { + u.Set(notification.FieldSubscriberType, v) + return u +} + +// UpdateSubscriberType sets the "subscriber_type" field to the value that was provided on create. +func (u *NotificationUpsert) UpdateSubscriberType() *NotificationUpsert { + u.SetExcluded(notification.FieldSubscriberType) + return u +} + +// SetSubscriberID sets the "subscriber_id" field. +func (u *NotificationUpsert) SetSubscriberID(v string) *NotificationUpsert { + u.Set(notification.FieldSubscriberID, v) + return u +} + +// UpdateSubscriberID sets the "subscriber_id" field to the value that was provided on create. +func (u *NotificationUpsert) UpdateSubscriberID() *NotificationUpsert { + u.SetExcluded(notification.FieldSubscriberID) + return u +} + +// SetStatus sets the "status" field. +func (u *NotificationUpsert) SetStatus(v string) *NotificationUpsert { + u.Set(notification.FieldStatus, v) + return u +} + +// UpdateStatus sets the "status" field to the value that was provided on create. +func (u *NotificationUpsert) UpdateStatus() *NotificationUpsert { + u.SetExcluded(notification.FieldStatus) + return u +} + +// SetMessage sets the "message" field. +func (u *NotificationUpsert) SetMessage(v string) *NotificationUpsert { + u.Set(notification.FieldMessage, v) + return u +} + +// UpdateMessage sets the "message" field to the value that was provided on create. +func (u *NotificationUpsert) UpdateMessage() *NotificationUpsert { + u.SetExcluded(notification.FieldMessage) + return u +} + +// SetDispatched sets the "dispatched" field. +func (u *NotificationUpsert) SetDispatched(v bool) *NotificationUpsert { + u.Set(notification.FieldDispatched, v) + return u +} + +// UpdateDispatched sets the "dispatched" field to the value that was provided on create. +func (u *NotificationUpsert) UpdateDispatched() *NotificationUpsert { + u.SetExcluded(notification.FieldDispatched) + return u +} + +// SetAcknowledged sets the "acknowledged" field. +func (u *NotificationUpsert) SetAcknowledged(v bool) *NotificationUpsert { + u.Set(notification.FieldAcknowledged, v) + return u +} + +// UpdateAcknowledged sets the "acknowledged" field to the value that was provided on create. +func (u *NotificationUpsert) UpdateAcknowledged() *NotificationUpsert { + u.SetExcluded(notification.FieldAcknowledged) + return u +} + +// UpdateNewValues updates the mutable fields using the new values that were set on create except the ID field. +// Using this option is equivalent to using: +// +// client.Notification.Create(). +// OnConflict( +// sql.ResolveWithNewValues(), +// sql.ResolveWith(func(u *sql.UpdateSet) { +// u.SetIgnore(notification.FieldID) +// }), +// ). +// Exec(ctx) +func (u *NotificationUpsertOne) UpdateNewValues() *NotificationUpsertOne { + u.create.conflict = append(u.create.conflict, sql.ResolveWithNewValues()) + u.create.conflict = append(u.create.conflict, sql.ResolveWith(func(s *sql.UpdateSet) { + if _, exists := u.create.mutation.ID(); exists { + s.SetIgnore(notification.FieldID) + } + if _, exists := u.create.mutation.Created(); exists { + s.SetIgnore(notification.FieldCreated) + } + })) + return u +} + +// Ignore sets each column to itself in case of conflict. +// Using this option is equivalent to using: +// +// client.Notification.Create(). +// OnConflict(sql.ResolveWithIgnore()). +// Exec(ctx) +func (u *NotificationUpsertOne) Ignore() *NotificationUpsertOne { + u.create.conflict = append(u.create.conflict, sql.ResolveWithIgnore()) + return u +} + +// DoNothing configures the conflict_action to `DO NOTHING`. +// Supported only by SQLite and PostgreSQL. +func (u *NotificationUpsertOne) DoNothing() *NotificationUpsertOne { + u.create.conflict = append(u.create.conflict, sql.DoNothing()) + return u +} + +// Update allows overriding fields `UPDATE` values. See the NotificationCreate.OnConflict +// documentation for more info. +func (u *NotificationUpsertOne) Update(set func(*NotificationUpsert)) *NotificationUpsertOne { + u.create.conflict = append(u.create.conflict, sql.ResolveWith(func(update *sql.UpdateSet) { + set(&NotificationUpsert{UpdateSet: update}) + })) + return u +} + +// SetSubscriptionID sets the "subscription_id" field. +func (u *NotificationUpsertOne) SetSubscriptionID(v uuid.UUID) *NotificationUpsertOne { + return u.Update(func(s *NotificationUpsert) { + s.SetSubscriptionID(v) + }) +} + +// UpdateSubscriptionID sets the "subscription_id" field to the value that was provided on create. +func (u *NotificationUpsertOne) UpdateSubscriptionID() *NotificationUpsertOne { + return u.Update(func(s *NotificationUpsert) { + s.UpdateSubscriptionID() + }) +} + +// SetAgentID sets the "agent_id" field. +func (u *NotificationUpsertOne) SetAgentID(v uuid.UUID) *NotificationUpsertOne { + return u.Update(func(s *NotificationUpsert) { + s.SetAgentID(v) + }) +} + +// UpdateAgentID sets the "agent_id" field to the value that was provided on create. +func (u *NotificationUpsertOne) UpdateAgentID() *NotificationUpsertOne { + return u.Update(func(s *NotificationUpsert) { + s.UpdateAgentID() + }) +} + +// SetProjectID sets the "project_id" field. +func (u *NotificationUpsertOne) SetProjectID(v uuid.UUID) *NotificationUpsertOne { + return u.Update(func(s *NotificationUpsert) { + s.SetProjectID(v) + }) +} + +// UpdateProjectID sets the "project_id" field to the value that was provided on create. +func (u *NotificationUpsertOne) UpdateProjectID() *NotificationUpsertOne { + return u.Update(func(s *NotificationUpsert) { + s.UpdateProjectID() + }) +} + +// SetSubscriberType sets the "subscriber_type" field. +func (u *NotificationUpsertOne) SetSubscriberType(v string) *NotificationUpsertOne { + return u.Update(func(s *NotificationUpsert) { + s.SetSubscriberType(v) + }) +} + +// UpdateSubscriberType sets the "subscriber_type" field to the value that was provided on create. +func (u *NotificationUpsertOne) UpdateSubscriberType() *NotificationUpsertOne { + return u.Update(func(s *NotificationUpsert) { + s.UpdateSubscriberType() + }) +} + +// SetSubscriberID sets the "subscriber_id" field. +func (u *NotificationUpsertOne) SetSubscriberID(v string) *NotificationUpsertOne { + return u.Update(func(s *NotificationUpsert) { + s.SetSubscriberID(v) + }) +} + +// UpdateSubscriberID sets the "subscriber_id" field to the value that was provided on create. +func (u *NotificationUpsertOne) UpdateSubscriberID() *NotificationUpsertOne { + return u.Update(func(s *NotificationUpsert) { + s.UpdateSubscriberID() + }) +} + +// SetStatus sets the "status" field. +func (u *NotificationUpsertOne) SetStatus(v string) *NotificationUpsertOne { + return u.Update(func(s *NotificationUpsert) { + s.SetStatus(v) + }) +} + +// UpdateStatus sets the "status" field to the value that was provided on create. +func (u *NotificationUpsertOne) UpdateStatus() *NotificationUpsertOne { + return u.Update(func(s *NotificationUpsert) { + s.UpdateStatus() + }) +} + +// SetMessage sets the "message" field. +func (u *NotificationUpsertOne) SetMessage(v string) *NotificationUpsertOne { + return u.Update(func(s *NotificationUpsert) { + s.SetMessage(v) + }) +} + +// UpdateMessage sets the "message" field to the value that was provided on create. +func (u *NotificationUpsertOne) UpdateMessage() *NotificationUpsertOne { + return u.Update(func(s *NotificationUpsert) { + s.UpdateMessage() + }) +} + +// SetDispatched sets the "dispatched" field. +func (u *NotificationUpsertOne) SetDispatched(v bool) *NotificationUpsertOne { + return u.Update(func(s *NotificationUpsert) { + s.SetDispatched(v) + }) +} + +// UpdateDispatched sets the "dispatched" field to the value that was provided on create. +func (u *NotificationUpsertOne) UpdateDispatched() *NotificationUpsertOne { + return u.Update(func(s *NotificationUpsert) { + s.UpdateDispatched() + }) +} + +// SetAcknowledged sets the "acknowledged" field. +func (u *NotificationUpsertOne) SetAcknowledged(v bool) *NotificationUpsertOne { + return u.Update(func(s *NotificationUpsert) { + s.SetAcknowledged(v) + }) +} + +// UpdateAcknowledged sets the "acknowledged" field to the value that was provided on create. +func (u *NotificationUpsertOne) UpdateAcknowledged() *NotificationUpsertOne { + return u.Update(func(s *NotificationUpsert) { + s.UpdateAcknowledged() + }) +} + +// Exec executes the query. +func (u *NotificationUpsertOne) Exec(ctx context.Context) error { + if len(u.create.conflict) == 0 { + return errors.New("ent: missing options for NotificationCreate.OnConflict") + } + return u.create.Exec(ctx) +} + +// ExecX is like Exec, but panics if an error occurs. +func (u *NotificationUpsertOne) ExecX(ctx context.Context) { + if err := u.create.Exec(ctx); err != nil { + panic(err) + } +} + +// Exec executes the UPSERT query and returns the inserted/updated ID. +func (u *NotificationUpsertOne) ID(ctx context.Context) (id uuid.UUID, err error) { + if u.create.driver.Dialect() == dialect.MySQL { + // In case of "ON CONFLICT", there is no way to get back non-numeric ID + // fields from the database since MySQL does not support the RETURNING clause. + return id, errors.New("ent: NotificationUpsertOne.ID is not supported by MySQL driver. Use NotificationUpsertOne.Exec instead") + } + node, err := u.create.Save(ctx) + if err != nil { + return id, err + } + return node.ID, nil +} + +// IDX is like ID, but panics if an error occurs. +func (u *NotificationUpsertOne) IDX(ctx context.Context) uuid.UUID { + id, err := u.ID(ctx) + if err != nil { + panic(err) + } + return id +} + +// NotificationCreateBulk is the builder for creating many Notification entities in bulk. +type NotificationCreateBulk struct { + config + err error + builders []*NotificationCreate + conflict []sql.ConflictOption +} + +// Save creates the Notification entities in the database. +func (_c *NotificationCreateBulk) Save(ctx context.Context) ([]*Notification, error) { + if _c.err != nil { + return nil, _c.err + } + specs := make([]*sqlgraph.CreateSpec, len(_c.builders)) + nodes := make([]*Notification, len(_c.builders)) + mutators := make([]Mutator, len(_c.builders)) + for i := range _c.builders { + func(i int, root context.Context) { + builder := _c.builders[i] + builder.defaults() + var mut Mutator = MutateFunc(func(ctx context.Context, m Mutation) (Value, error) { + mutation, ok := m.(*NotificationMutation) + if !ok { + return nil, fmt.Errorf("unexpected mutation type %T", m) + } + if err := builder.check(); err != nil { + return nil, err + } + builder.mutation = mutation + var err error + nodes[i], specs[i] = builder.createSpec() + if i < len(mutators)-1 { + _, err = mutators[i+1].Mutate(root, _c.builders[i+1].mutation) + } else { + spec := &sqlgraph.BatchCreateSpec{Nodes: specs} + spec.OnConflict = _c.conflict + // Invoke the actual operation on the latest mutation in the chain. + if err = sqlgraph.BatchCreate(ctx, _c.driver, spec); err != nil { + if sqlgraph.IsConstraintError(err) { + err = &ConstraintError{msg: err.Error(), wrap: err} + } + } + } + if err != nil { + return nil, err + } + mutation.id = &nodes[i].ID + mutation.done = true + return nodes[i], nil + }) + for i := len(builder.hooks) - 1; i >= 0; i-- { + mut = builder.hooks[i](mut) + } + mutators[i] = mut + }(i, ctx) + } + if len(mutators) > 0 { + if _, err := mutators[0].Mutate(ctx, _c.builders[0].mutation); err != nil { + return nil, err + } + } + return nodes, nil +} + +// SaveX is like Save, but panics if an error occurs. +func (_c *NotificationCreateBulk) SaveX(ctx context.Context) []*Notification { + v, err := _c.Save(ctx) + if err != nil { + panic(err) + } + return v +} + +// Exec executes the query. +func (_c *NotificationCreateBulk) Exec(ctx context.Context) error { + _, err := _c.Save(ctx) + return err +} + +// ExecX is like Exec, but panics if an error occurs. +func (_c *NotificationCreateBulk) ExecX(ctx context.Context) { + if err := _c.Exec(ctx); err != nil { + panic(err) + } +} + +// OnConflict allows configuring the `ON CONFLICT` / `ON DUPLICATE KEY` clause +// of the `INSERT` statement. For example: +// +// client.Notification.CreateBulk(builders...). +// OnConflict( +// // Update the row with the new values +// // the was proposed for insertion. +// sql.ResolveWithNewValues(), +// ). +// // Override some of the fields with custom +// // update values. +// Update(func(u *ent.NotificationUpsert) { +// SetSubscriptionID(v+v). +// }). +// Exec(ctx) +func (_c *NotificationCreateBulk) OnConflict(opts ...sql.ConflictOption) *NotificationUpsertBulk { + _c.conflict = opts + return &NotificationUpsertBulk{ + create: _c, + } +} + +// OnConflictColumns calls `OnConflict` and configures the columns +// as conflict target. Using this option is equivalent to using: +// +// client.Notification.Create(). +// OnConflict(sql.ConflictColumns(columns...)). +// Exec(ctx) +func (_c *NotificationCreateBulk) OnConflictColumns(columns ...string) *NotificationUpsertBulk { + _c.conflict = append(_c.conflict, sql.ConflictColumns(columns...)) + return &NotificationUpsertBulk{ + create: _c, + } +} + +// NotificationUpsertBulk is the builder for "upsert"-ing +// a bulk of Notification nodes. +type NotificationUpsertBulk struct { + create *NotificationCreateBulk +} + +// UpdateNewValues updates the mutable fields using the new values that +// were set on create. Using this option is equivalent to using: +// +// client.Notification.Create(). +// OnConflict( +// sql.ResolveWithNewValues(), +// sql.ResolveWith(func(u *sql.UpdateSet) { +// u.SetIgnore(notification.FieldID) +// }), +// ). +// Exec(ctx) +func (u *NotificationUpsertBulk) UpdateNewValues() *NotificationUpsertBulk { + u.create.conflict = append(u.create.conflict, sql.ResolveWithNewValues()) + u.create.conflict = append(u.create.conflict, sql.ResolveWith(func(s *sql.UpdateSet) { + for _, b := range u.create.builders { + if _, exists := b.mutation.ID(); exists { + s.SetIgnore(notification.FieldID) + } + if _, exists := b.mutation.Created(); exists { + s.SetIgnore(notification.FieldCreated) + } + } + })) + return u +} + +// Ignore sets each column to itself in case of conflict. +// Using this option is equivalent to using: +// +// client.Notification.Create(). +// OnConflict(sql.ResolveWithIgnore()). +// Exec(ctx) +func (u *NotificationUpsertBulk) Ignore() *NotificationUpsertBulk { + u.create.conflict = append(u.create.conflict, sql.ResolveWithIgnore()) + return u +} + +// DoNothing configures the conflict_action to `DO NOTHING`. +// Supported only by SQLite and PostgreSQL. +func (u *NotificationUpsertBulk) DoNothing() *NotificationUpsertBulk { + u.create.conflict = append(u.create.conflict, sql.DoNothing()) + return u +} + +// Update allows overriding fields `UPDATE` values. See the NotificationCreateBulk.OnConflict +// documentation for more info. +func (u *NotificationUpsertBulk) Update(set func(*NotificationUpsert)) *NotificationUpsertBulk { + u.create.conflict = append(u.create.conflict, sql.ResolveWith(func(update *sql.UpdateSet) { + set(&NotificationUpsert{UpdateSet: update}) + })) + return u +} + +// SetSubscriptionID sets the "subscription_id" field. +func (u *NotificationUpsertBulk) SetSubscriptionID(v uuid.UUID) *NotificationUpsertBulk { + return u.Update(func(s *NotificationUpsert) { + s.SetSubscriptionID(v) + }) +} + +// UpdateSubscriptionID sets the "subscription_id" field to the value that was provided on create. +func (u *NotificationUpsertBulk) UpdateSubscriptionID() *NotificationUpsertBulk { + return u.Update(func(s *NotificationUpsert) { + s.UpdateSubscriptionID() + }) +} + +// SetAgentID sets the "agent_id" field. +func (u *NotificationUpsertBulk) SetAgentID(v uuid.UUID) *NotificationUpsertBulk { + return u.Update(func(s *NotificationUpsert) { + s.SetAgentID(v) + }) +} + +// UpdateAgentID sets the "agent_id" field to the value that was provided on create. +func (u *NotificationUpsertBulk) UpdateAgentID() *NotificationUpsertBulk { + return u.Update(func(s *NotificationUpsert) { + s.UpdateAgentID() + }) +} + +// SetProjectID sets the "project_id" field. +func (u *NotificationUpsertBulk) SetProjectID(v uuid.UUID) *NotificationUpsertBulk { + return u.Update(func(s *NotificationUpsert) { + s.SetProjectID(v) + }) +} + +// UpdateProjectID sets the "project_id" field to the value that was provided on create. +func (u *NotificationUpsertBulk) UpdateProjectID() *NotificationUpsertBulk { + return u.Update(func(s *NotificationUpsert) { + s.UpdateProjectID() + }) +} + +// SetSubscriberType sets the "subscriber_type" field. +func (u *NotificationUpsertBulk) SetSubscriberType(v string) *NotificationUpsertBulk { + return u.Update(func(s *NotificationUpsert) { + s.SetSubscriberType(v) + }) +} + +// UpdateSubscriberType sets the "subscriber_type" field to the value that was provided on create. +func (u *NotificationUpsertBulk) UpdateSubscriberType() *NotificationUpsertBulk { + return u.Update(func(s *NotificationUpsert) { + s.UpdateSubscriberType() + }) +} + +// SetSubscriberID sets the "subscriber_id" field. +func (u *NotificationUpsertBulk) SetSubscriberID(v string) *NotificationUpsertBulk { + return u.Update(func(s *NotificationUpsert) { + s.SetSubscriberID(v) + }) +} + +// UpdateSubscriberID sets the "subscriber_id" field to the value that was provided on create. +func (u *NotificationUpsertBulk) UpdateSubscriberID() *NotificationUpsertBulk { + return u.Update(func(s *NotificationUpsert) { + s.UpdateSubscriberID() + }) +} + +// SetStatus sets the "status" field. +func (u *NotificationUpsertBulk) SetStatus(v string) *NotificationUpsertBulk { + return u.Update(func(s *NotificationUpsert) { + s.SetStatus(v) + }) +} + +// UpdateStatus sets the "status" field to the value that was provided on create. +func (u *NotificationUpsertBulk) UpdateStatus() *NotificationUpsertBulk { + return u.Update(func(s *NotificationUpsert) { + s.UpdateStatus() + }) +} + +// SetMessage sets the "message" field. +func (u *NotificationUpsertBulk) SetMessage(v string) *NotificationUpsertBulk { + return u.Update(func(s *NotificationUpsert) { + s.SetMessage(v) + }) +} + +// UpdateMessage sets the "message" field to the value that was provided on create. +func (u *NotificationUpsertBulk) UpdateMessage() *NotificationUpsertBulk { + return u.Update(func(s *NotificationUpsert) { + s.UpdateMessage() + }) +} + +// SetDispatched sets the "dispatched" field. +func (u *NotificationUpsertBulk) SetDispatched(v bool) *NotificationUpsertBulk { + return u.Update(func(s *NotificationUpsert) { + s.SetDispatched(v) + }) +} + +// UpdateDispatched sets the "dispatched" field to the value that was provided on create. +func (u *NotificationUpsertBulk) UpdateDispatched() *NotificationUpsertBulk { + return u.Update(func(s *NotificationUpsert) { + s.UpdateDispatched() + }) +} + +// SetAcknowledged sets the "acknowledged" field. +func (u *NotificationUpsertBulk) SetAcknowledged(v bool) *NotificationUpsertBulk { + return u.Update(func(s *NotificationUpsert) { + s.SetAcknowledged(v) + }) +} + +// UpdateAcknowledged sets the "acknowledged" field to the value that was provided on create. +func (u *NotificationUpsertBulk) UpdateAcknowledged() *NotificationUpsertBulk { + return u.Update(func(s *NotificationUpsert) { + s.UpdateAcknowledged() + }) +} + +// Exec executes the query. +func (u *NotificationUpsertBulk) Exec(ctx context.Context) error { + if u.create.err != nil { + return u.create.err + } + for i, b := range u.create.builders { + if len(b.conflict) != 0 { + return fmt.Errorf("ent: OnConflict was set for builder %d. Set it on the NotificationCreateBulk instead", i) + } + } + if len(u.create.conflict) == 0 { + return errors.New("ent: missing options for NotificationCreateBulk.OnConflict") + } + return u.create.Exec(ctx) +} + +// ExecX is like Exec, but panics if an error occurs. +func (u *NotificationUpsertBulk) ExecX(ctx context.Context) { + if err := u.create.Exec(ctx); err != nil { + panic(err) + } +} diff --git a/pkg/ent/notification_delete.go b/pkg/ent/notification_delete.go new file mode 100644 index 000000000..0426bcd8f --- /dev/null +++ b/pkg/ent/notification_delete.go @@ -0,0 +1,88 @@ +// Code generated by ent, DO NOT EDIT. + +package ent + +import ( + "context" + + "entgo.io/ent/dialect/sql" + "entgo.io/ent/dialect/sql/sqlgraph" + "entgo.io/ent/schema/field" + "github.com/GoogleCloudPlatform/scion/pkg/ent/notification" + "github.com/GoogleCloudPlatform/scion/pkg/ent/predicate" +) + +// NotificationDelete is the builder for deleting a Notification entity. +type NotificationDelete struct { + config + hooks []Hook + mutation *NotificationMutation +} + +// Where appends a list predicates to the NotificationDelete builder. +func (_d *NotificationDelete) Where(ps ...predicate.Notification) *NotificationDelete { + _d.mutation.Where(ps...) + return _d +} + +// Exec executes the deletion query and returns how many vertices were deleted. +func (_d *NotificationDelete) Exec(ctx context.Context) (int, error) { + return withHooks(ctx, _d.sqlExec, _d.mutation, _d.hooks) +} + +// ExecX is like Exec, but panics if an error occurs. +func (_d *NotificationDelete) ExecX(ctx context.Context) int { + n, err := _d.Exec(ctx) + if err != nil { + panic(err) + } + return n +} + +func (_d *NotificationDelete) sqlExec(ctx context.Context) (int, error) { + _spec := sqlgraph.NewDeleteSpec(notification.Table, sqlgraph.NewFieldSpec(notification.FieldID, field.TypeUUID)) + if ps := _d.mutation.predicates; len(ps) > 0 { + _spec.Predicate = func(selector *sql.Selector) { + for i := range ps { + ps[i](selector) + } + } + } + affected, err := sqlgraph.DeleteNodes(ctx, _d.driver, _spec) + if err != nil && sqlgraph.IsConstraintError(err) { + err = &ConstraintError{msg: err.Error(), wrap: err} + } + _d.mutation.done = true + return affected, err +} + +// NotificationDeleteOne is the builder for deleting a single Notification entity. +type NotificationDeleteOne struct { + _d *NotificationDelete +} + +// Where appends a list predicates to the NotificationDelete builder. +func (_d *NotificationDeleteOne) Where(ps ...predicate.Notification) *NotificationDeleteOne { + _d._d.mutation.Where(ps...) + return _d +} + +// Exec executes the deletion query. +func (_d *NotificationDeleteOne) Exec(ctx context.Context) error { + n, err := _d._d.Exec(ctx) + switch { + case err != nil: + return err + case n == 0: + return &NotFoundError{notification.Label} + default: + return nil + } +} + +// ExecX is like Exec, but panics if an error occurs. +func (_d *NotificationDeleteOne) ExecX(ctx context.Context) { + if err := _d.Exec(ctx); err != nil { + panic(err) + } +} diff --git a/pkg/ent/notification_query.go b/pkg/ent/notification_query.go new file mode 100644 index 000000000..03a858392 --- /dev/null +++ b/pkg/ent/notification_query.go @@ -0,0 +1,565 @@ +// Code generated by ent, DO NOT EDIT. + +package ent + +import ( + "context" + "fmt" + "math" + + "entgo.io/ent" + "entgo.io/ent/dialect" + "entgo.io/ent/dialect/sql" + "entgo.io/ent/dialect/sql/sqlgraph" + "entgo.io/ent/schema/field" + "github.com/GoogleCloudPlatform/scion/pkg/ent/notification" + "github.com/GoogleCloudPlatform/scion/pkg/ent/predicate" + "github.com/google/uuid" +) + +// NotificationQuery is the builder for querying Notification entities. +type NotificationQuery struct { + config + ctx *QueryContext + order []notification.OrderOption + inters []Interceptor + predicates []predicate.Notification + modifiers []func(*sql.Selector) + // intermediate query (i.e. traversal path). + sql *sql.Selector + path func(context.Context) (*sql.Selector, error) +} + +// Where adds a new predicate for the NotificationQuery builder. +func (_q *NotificationQuery) Where(ps ...predicate.Notification) *NotificationQuery { + _q.predicates = append(_q.predicates, ps...) + return _q +} + +// Limit the number of records to be returned by this query. +func (_q *NotificationQuery) Limit(limit int) *NotificationQuery { + _q.ctx.Limit = &limit + return _q +} + +// Offset to start from. +func (_q *NotificationQuery) Offset(offset int) *NotificationQuery { + _q.ctx.Offset = &offset + return _q +} + +// Unique configures the query builder to filter duplicate records on query. +// By default, unique is set to true, and can be disabled using this method. +func (_q *NotificationQuery) Unique(unique bool) *NotificationQuery { + _q.ctx.Unique = &unique + return _q +} + +// Order specifies how the records should be ordered. +func (_q *NotificationQuery) Order(o ...notification.OrderOption) *NotificationQuery { + _q.order = append(_q.order, o...) + return _q +} + +// First returns the first Notification entity from the query. +// Returns a *NotFoundError when no Notification was found. +func (_q *NotificationQuery) First(ctx context.Context) (*Notification, error) { + nodes, err := _q.Limit(1).All(setContextOp(ctx, _q.ctx, ent.OpQueryFirst)) + if err != nil { + return nil, err + } + if len(nodes) == 0 { + return nil, &NotFoundError{notification.Label} + } + return nodes[0], nil +} + +// FirstX is like First, but panics if an error occurs. +func (_q *NotificationQuery) FirstX(ctx context.Context) *Notification { + node, err := _q.First(ctx) + if err != nil && !IsNotFound(err) { + panic(err) + } + return node +} + +// FirstID returns the first Notification ID from the query. +// Returns a *NotFoundError when no Notification ID was found. +func (_q *NotificationQuery) FirstID(ctx context.Context) (id uuid.UUID, err error) { + var ids []uuid.UUID + if ids, err = _q.Limit(1).IDs(setContextOp(ctx, _q.ctx, ent.OpQueryFirstID)); err != nil { + return + } + if len(ids) == 0 { + err = &NotFoundError{notification.Label} + return + } + return ids[0], nil +} + +// FirstIDX is like FirstID, but panics if an error occurs. +func (_q *NotificationQuery) FirstIDX(ctx context.Context) uuid.UUID { + id, err := _q.FirstID(ctx) + if err != nil && !IsNotFound(err) { + panic(err) + } + return id +} + +// Only returns a single Notification entity found by the query, ensuring it only returns one. +// Returns a *NotSingularError when more than one Notification entity is found. +// Returns a *NotFoundError when no Notification entities are found. +func (_q *NotificationQuery) Only(ctx context.Context) (*Notification, error) { + nodes, err := _q.Limit(2).All(setContextOp(ctx, _q.ctx, ent.OpQueryOnly)) + if err != nil { + return nil, err + } + switch len(nodes) { + case 1: + return nodes[0], nil + case 0: + return nil, &NotFoundError{notification.Label} + default: + return nil, &NotSingularError{notification.Label} + } +} + +// OnlyX is like Only, but panics if an error occurs. +func (_q *NotificationQuery) OnlyX(ctx context.Context) *Notification { + node, err := _q.Only(ctx) + if err != nil { + panic(err) + } + return node +} + +// OnlyID is like Only, but returns the only Notification ID in the query. +// Returns a *NotSingularError when more than one Notification ID is found. +// Returns a *NotFoundError when no entities are found. +func (_q *NotificationQuery) OnlyID(ctx context.Context) (id uuid.UUID, err error) { + var ids []uuid.UUID + if ids, err = _q.Limit(2).IDs(setContextOp(ctx, _q.ctx, ent.OpQueryOnlyID)); err != nil { + return + } + switch len(ids) { + case 1: + id = ids[0] + case 0: + err = &NotFoundError{notification.Label} + default: + err = &NotSingularError{notification.Label} + } + return +} + +// OnlyIDX is like OnlyID, but panics if an error occurs. +func (_q *NotificationQuery) OnlyIDX(ctx context.Context) uuid.UUID { + id, err := _q.OnlyID(ctx) + if err != nil { + panic(err) + } + return id +} + +// All executes the query and returns a list of Notifications. +func (_q *NotificationQuery) All(ctx context.Context) ([]*Notification, error) { + ctx = setContextOp(ctx, _q.ctx, ent.OpQueryAll) + if err := _q.prepareQuery(ctx); err != nil { + return nil, err + } + qr := querierAll[[]*Notification, *NotificationQuery]() + return withInterceptors[[]*Notification](ctx, _q, qr, _q.inters) +} + +// AllX is like All, but panics if an error occurs. +func (_q *NotificationQuery) AllX(ctx context.Context) []*Notification { + nodes, err := _q.All(ctx) + if err != nil { + panic(err) + } + return nodes +} + +// IDs executes the query and returns a list of Notification IDs. +func (_q *NotificationQuery) IDs(ctx context.Context) (ids []uuid.UUID, err error) { + if _q.ctx.Unique == nil && _q.path != nil { + _q.Unique(true) + } + ctx = setContextOp(ctx, _q.ctx, ent.OpQueryIDs) + if err = _q.Select(notification.FieldID).Scan(ctx, &ids); err != nil { + return nil, err + } + return ids, nil +} + +// IDsX is like IDs, but panics if an error occurs. +func (_q *NotificationQuery) IDsX(ctx context.Context) []uuid.UUID { + ids, err := _q.IDs(ctx) + if err != nil { + panic(err) + } + return ids +} + +// Count returns the count of the given query. +func (_q *NotificationQuery) Count(ctx context.Context) (int, error) { + ctx = setContextOp(ctx, _q.ctx, ent.OpQueryCount) + if err := _q.prepareQuery(ctx); err != nil { + return 0, err + } + return withInterceptors[int](ctx, _q, querierCount[*NotificationQuery](), _q.inters) +} + +// CountX is like Count, but panics if an error occurs. +func (_q *NotificationQuery) CountX(ctx context.Context) int { + count, err := _q.Count(ctx) + if err != nil { + panic(err) + } + return count +} + +// Exist returns true if the query has elements in the graph. +func (_q *NotificationQuery) Exist(ctx context.Context) (bool, error) { + ctx = setContextOp(ctx, _q.ctx, ent.OpQueryExist) + switch _, err := _q.FirstID(ctx); { + case IsNotFound(err): + return false, nil + case err != nil: + return false, fmt.Errorf("ent: check existence: %w", err) + default: + return true, nil + } +} + +// ExistX is like Exist, but panics if an error occurs. +func (_q *NotificationQuery) ExistX(ctx context.Context) bool { + exist, err := _q.Exist(ctx) + if err != nil { + panic(err) + } + return exist +} + +// Clone returns a duplicate of the NotificationQuery builder, including all associated steps. It can be +// used to prepare common query builders and use them differently after the clone is made. +func (_q *NotificationQuery) Clone() *NotificationQuery { + if _q == nil { + return nil + } + return &NotificationQuery{ + config: _q.config, + ctx: _q.ctx.Clone(), + order: append([]notification.OrderOption{}, _q.order...), + inters: append([]Interceptor{}, _q.inters...), + predicates: append([]predicate.Notification{}, _q.predicates...), + // clone intermediate query. + sql: _q.sql.Clone(), + path: _q.path, + } +} + +// GroupBy is used to group vertices by one or more fields/columns. +// It is often used with aggregate functions, like: count, max, mean, min, sum. +// +// Example: +// +// var v []struct { +// SubscriptionID uuid.UUID `json:"subscription_id,omitempty"` +// Count int `json:"count,omitempty"` +// } +// +// client.Notification.Query(). +// GroupBy(notification.FieldSubscriptionID). +// Aggregate(ent.Count()). +// Scan(ctx, &v) +func (_q *NotificationQuery) GroupBy(field string, fields ...string) *NotificationGroupBy { + _q.ctx.Fields = append([]string{field}, fields...) + grbuild := &NotificationGroupBy{build: _q} + grbuild.flds = &_q.ctx.Fields + grbuild.label = notification.Label + grbuild.scan = grbuild.Scan + return grbuild +} + +// Select allows the selection one or more fields/columns for the given query, +// instead of selecting all fields in the entity. +// +// Example: +// +// var v []struct { +// SubscriptionID uuid.UUID `json:"subscription_id,omitempty"` +// } +// +// client.Notification.Query(). +// Select(notification.FieldSubscriptionID). +// Scan(ctx, &v) +func (_q *NotificationQuery) Select(fields ...string) *NotificationSelect { + _q.ctx.Fields = append(_q.ctx.Fields, fields...) + sbuild := &NotificationSelect{NotificationQuery: _q} + sbuild.label = notification.Label + sbuild.flds, sbuild.scan = &_q.ctx.Fields, sbuild.Scan + return sbuild +} + +// Aggregate returns a NotificationSelect configured with the given aggregations. +func (_q *NotificationQuery) Aggregate(fns ...AggregateFunc) *NotificationSelect { + return _q.Select().Aggregate(fns...) +} + +func (_q *NotificationQuery) prepareQuery(ctx context.Context) error { + for _, inter := range _q.inters { + if inter == nil { + return fmt.Errorf("ent: uninitialized interceptor (forgotten import ent/runtime?)") + } + if trv, ok := inter.(Traverser); ok { + if err := trv.Traverse(ctx, _q); err != nil { + return err + } + } + } + for _, f := range _q.ctx.Fields { + if !notification.ValidColumn(f) { + return &ValidationError{Name: f, err: fmt.Errorf("ent: invalid field %q for query", f)} + } + } + if _q.path != nil { + prev, err := _q.path(ctx) + if err != nil { + return err + } + _q.sql = prev + } + return nil +} + +func (_q *NotificationQuery) sqlAll(ctx context.Context, hooks ...queryHook) ([]*Notification, error) { + var ( + nodes = []*Notification{} + _spec = _q.querySpec() + ) + _spec.ScanValues = func(columns []string) ([]any, error) { + return (*Notification).scanValues(nil, columns) + } + _spec.Assign = func(columns []string, values []any) error { + node := &Notification{config: _q.config} + nodes = append(nodes, node) + return node.assignValues(columns, values) + } + if len(_q.modifiers) > 0 { + _spec.Modifiers = _q.modifiers + } + for i := range hooks { + hooks[i](ctx, _spec) + } + if err := sqlgraph.QueryNodes(ctx, _q.driver, _spec); err != nil { + return nil, err + } + if len(nodes) == 0 { + return nodes, nil + } + return nodes, nil +} + +func (_q *NotificationQuery) sqlCount(ctx context.Context) (int, error) { + _spec := _q.querySpec() + if len(_q.modifiers) > 0 { + _spec.Modifiers = _q.modifiers + } + _spec.Node.Columns = _q.ctx.Fields + if len(_q.ctx.Fields) > 0 { + _spec.Unique = _q.ctx.Unique != nil && *_q.ctx.Unique + } + return sqlgraph.CountNodes(ctx, _q.driver, _spec) +} + +func (_q *NotificationQuery) querySpec() *sqlgraph.QuerySpec { + _spec := sqlgraph.NewQuerySpec(notification.Table, notification.Columns, sqlgraph.NewFieldSpec(notification.FieldID, field.TypeUUID)) + _spec.From = _q.sql + if unique := _q.ctx.Unique; unique != nil { + _spec.Unique = *unique + } else if _q.path != nil { + _spec.Unique = true + } + if fields := _q.ctx.Fields; len(fields) > 0 { + _spec.Node.Columns = make([]string, 0, len(fields)) + _spec.Node.Columns = append(_spec.Node.Columns, notification.FieldID) + for i := range fields { + if fields[i] != notification.FieldID { + _spec.Node.Columns = append(_spec.Node.Columns, fields[i]) + } + } + } + if ps := _q.predicates; len(ps) > 0 { + _spec.Predicate = func(selector *sql.Selector) { + for i := range ps { + ps[i](selector) + } + } + } + if limit := _q.ctx.Limit; limit != nil { + _spec.Limit = *limit + } + if offset := _q.ctx.Offset; offset != nil { + _spec.Offset = *offset + } + if ps := _q.order; len(ps) > 0 { + _spec.Order = func(selector *sql.Selector) { + for i := range ps { + ps[i](selector) + } + } + } + return _spec +} + +func (_q *NotificationQuery) sqlQuery(ctx context.Context) *sql.Selector { + builder := sql.Dialect(_q.driver.Dialect()) + t1 := builder.Table(notification.Table) + columns := _q.ctx.Fields + if len(columns) == 0 { + columns = notification.Columns + } + selector := builder.Select(t1.Columns(columns...)...).From(t1) + if _q.sql != nil { + selector = _q.sql + selector.Select(selector.Columns(columns...)...) + } + if _q.ctx.Unique != nil && *_q.ctx.Unique { + selector.Distinct() + } + for _, m := range _q.modifiers { + m(selector) + } + for _, p := range _q.predicates { + p(selector) + } + for _, p := range _q.order { + p(selector) + } + if offset := _q.ctx.Offset; offset != nil { + // limit is mandatory for offset clause. We start + // with default value, and override it below if needed. + selector.Offset(*offset).Limit(math.MaxInt32) + } + if limit := _q.ctx.Limit; limit != nil { + selector.Limit(*limit) + } + return selector +} + +// ForUpdate locks the selected rows against concurrent updates, and prevent them from being +// updated, deleted or "selected ... for update" by other sessions, until the transaction is +// either committed or rolled-back. +func (_q *NotificationQuery) ForUpdate(opts ...sql.LockOption) *NotificationQuery { + if _q.driver.Dialect() == dialect.Postgres { + _q.Unique(false) + } + _q.modifiers = append(_q.modifiers, func(s *sql.Selector) { + s.ForUpdate(opts...) + }) + return _q +} + +// ForShare behaves similarly to ForUpdate, except that it acquires a shared mode lock +// on any rows that are read. Other sessions can read the rows, but cannot modify them +// until your transaction commits. +func (_q *NotificationQuery) ForShare(opts ...sql.LockOption) *NotificationQuery { + if _q.driver.Dialect() == dialect.Postgres { + _q.Unique(false) + } + _q.modifiers = append(_q.modifiers, func(s *sql.Selector) { + s.ForShare(opts...) + }) + return _q +} + +// NotificationGroupBy is the group-by builder for Notification entities. +type NotificationGroupBy struct { + selector + build *NotificationQuery +} + +// Aggregate adds the given aggregation functions to the group-by query. +func (_g *NotificationGroupBy) Aggregate(fns ...AggregateFunc) *NotificationGroupBy { + _g.fns = append(_g.fns, fns...) + return _g +} + +// Scan applies the selector query and scans the result into the given value. +func (_g *NotificationGroupBy) Scan(ctx context.Context, v any) error { + ctx = setContextOp(ctx, _g.build.ctx, ent.OpQueryGroupBy) + if err := _g.build.prepareQuery(ctx); err != nil { + return err + } + return scanWithInterceptors[*NotificationQuery, *NotificationGroupBy](ctx, _g.build, _g, _g.build.inters, v) +} + +func (_g *NotificationGroupBy) sqlScan(ctx context.Context, root *NotificationQuery, v any) error { + selector := root.sqlQuery(ctx).Select() + aggregation := make([]string, 0, len(_g.fns)) + for _, fn := range _g.fns { + aggregation = append(aggregation, fn(selector)) + } + if len(selector.SelectedColumns()) == 0 { + columns := make([]string, 0, len(*_g.flds)+len(_g.fns)) + for _, f := range *_g.flds { + columns = append(columns, selector.C(f)) + } + columns = append(columns, aggregation...) + selector.Select(columns...) + } + selector.GroupBy(selector.Columns(*_g.flds...)...) + if err := selector.Err(); err != nil { + return err + } + rows := &sql.Rows{} + query, args := selector.Query() + if err := _g.build.driver.Query(ctx, query, args, rows); err != nil { + return err + } + defer rows.Close() + return sql.ScanSlice(rows, v) +} + +// NotificationSelect is the builder for selecting fields of Notification entities. +type NotificationSelect struct { + *NotificationQuery + selector +} + +// Aggregate adds the given aggregation functions to the selector query. +func (_s *NotificationSelect) Aggregate(fns ...AggregateFunc) *NotificationSelect { + _s.fns = append(_s.fns, fns...) + return _s +} + +// Scan applies the selector query and scans the result into the given value. +func (_s *NotificationSelect) Scan(ctx context.Context, v any) error { + ctx = setContextOp(ctx, _s.ctx, ent.OpQuerySelect) + if err := _s.prepareQuery(ctx); err != nil { + return err + } + return scanWithInterceptors[*NotificationQuery, *NotificationSelect](ctx, _s.NotificationQuery, _s, _s.inters, v) +} + +func (_s *NotificationSelect) sqlScan(ctx context.Context, root *NotificationQuery, v any) error { + selector := root.sqlQuery(ctx) + aggregation := make([]string, 0, len(_s.fns)) + for _, fn := range _s.fns { + aggregation = append(aggregation, fn(selector)) + } + switch n := len(*_s.selector.flds); { + case n == 0 && len(aggregation) > 0: + selector.Select(aggregation...) + case n != 0 && len(aggregation) > 0: + selector.AppendSelect(aggregation...) + } + rows := &sql.Rows{} + query, args := selector.Query() + if err := _s.driver.Query(ctx, query, args, rows); err != nil { + return err + } + defer rows.Close() + return sql.ScanSlice(rows, v) +} diff --git a/pkg/ent/notification_update.go b/pkg/ent/notification_update.go new file mode 100644 index 000000000..65166cd0f --- /dev/null +++ b/pkg/ent/notification_update.go @@ -0,0 +1,538 @@ +// Code generated by ent, DO NOT EDIT. + +package ent + +import ( + "context" + "errors" + "fmt" + + "entgo.io/ent/dialect/sql" + "entgo.io/ent/dialect/sql/sqlgraph" + "entgo.io/ent/schema/field" + "github.com/GoogleCloudPlatform/scion/pkg/ent/notification" + "github.com/GoogleCloudPlatform/scion/pkg/ent/predicate" + "github.com/google/uuid" +) + +// NotificationUpdate is the builder for updating Notification entities. +type NotificationUpdate struct { + config + hooks []Hook + mutation *NotificationMutation +} + +// Where appends a list predicates to the NotificationUpdate builder. +func (_u *NotificationUpdate) Where(ps ...predicate.Notification) *NotificationUpdate { + _u.mutation.Where(ps...) + return _u +} + +// SetSubscriptionID sets the "subscription_id" field. +func (_u *NotificationUpdate) SetSubscriptionID(v uuid.UUID) *NotificationUpdate { + _u.mutation.SetSubscriptionID(v) + return _u +} + +// SetNillableSubscriptionID sets the "subscription_id" field if the given value is not nil. +func (_u *NotificationUpdate) SetNillableSubscriptionID(v *uuid.UUID) *NotificationUpdate { + if v != nil { + _u.SetSubscriptionID(*v) + } + return _u +} + +// SetAgentID sets the "agent_id" field. +func (_u *NotificationUpdate) SetAgentID(v uuid.UUID) *NotificationUpdate { + _u.mutation.SetAgentID(v) + return _u +} + +// SetNillableAgentID sets the "agent_id" field if the given value is not nil. +func (_u *NotificationUpdate) SetNillableAgentID(v *uuid.UUID) *NotificationUpdate { + if v != nil { + _u.SetAgentID(*v) + } + return _u +} + +// SetProjectID sets the "project_id" field. +func (_u *NotificationUpdate) SetProjectID(v uuid.UUID) *NotificationUpdate { + _u.mutation.SetProjectID(v) + return _u +} + +// SetNillableProjectID sets the "project_id" field if the given value is not nil. +func (_u *NotificationUpdate) SetNillableProjectID(v *uuid.UUID) *NotificationUpdate { + if v != nil { + _u.SetProjectID(*v) + } + return _u +} + +// SetSubscriberType sets the "subscriber_type" field. +func (_u *NotificationUpdate) SetSubscriberType(v string) *NotificationUpdate { + _u.mutation.SetSubscriberType(v) + return _u +} + +// SetNillableSubscriberType sets the "subscriber_type" field if the given value is not nil. +func (_u *NotificationUpdate) SetNillableSubscriberType(v *string) *NotificationUpdate { + if v != nil { + _u.SetSubscriberType(*v) + } + return _u +} + +// SetSubscriberID sets the "subscriber_id" field. +func (_u *NotificationUpdate) SetSubscriberID(v string) *NotificationUpdate { + _u.mutation.SetSubscriberID(v) + return _u +} + +// SetNillableSubscriberID sets the "subscriber_id" field if the given value is not nil. +func (_u *NotificationUpdate) SetNillableSubscriberID(v *string) *NotificationUpdate { + if v != nil { + _u.SetSubscriberID(*v) + } + return _u +} + +// SetStatus sets the "status" field. +func (_u *NotificationUpdate) SetStatus(v string) *NotificationUpdate { + _u.mutation.SetStatus(v) + return _u +} + +// SetNillableStatus sets the "status" field if the given value is not nil. +func (_u *NotificationUpdate) SetNillableStatus(v *string) *NotificationUpdate { + if v != nil { + _u.SetStatus(*v) + } + return _u +} + +// SetMessage sets the "message" field. +func (_u *NotificationUpdate) SetMessage(v string) *NotificationUpdate { + _u.mutation.SetMessage(v) + return _u +} + +// SetNillableMessage sets the "message" field if the given value is not nil. +func (_u *NotificationUpdate) SetNillableMessage(v *string) *NotificationUpdate { + if v != nil { + _u.SetMessage(*v) + } + return _u +} + +// SetDispatched sets the "dispatched" field. +func (_u *NotificationUpdate) SetDispatched(v bool) *NotificationUpdate { + _u.mutation.SetDispatched(v) + return _u +} + +// SetNillableDispatched sets the "dispatched" field if the given value is not nil. +func (_u *NotificationUpdate) SetNillableDispatched(v *bool) *NotificationUpdate { + if v != nil { + _u.SetDispatched(*v) + } + return _u +} + +// SetAcknowledged sets the "acknowledged" field. +func (_u *NotificationUpdate) SetAcknowledged(v bool) *NotificationUpdate { + _u.mutation.SetAcknowledged(v) + return _u +} + +// SetNillableAcknowledged sets the "acknowledged" field if the given value is not nil. +func (_u *NotificationUpdate) SetNillableAcknowledged(v *bool) *NotificationUpdate { + if v != nil { + _u.SetAcknowledged(*v) + } + return _u +} + +// Mutation returns the NotificationMutation object of the builder. +func (_u *NotificationUpdate) Mutation() *NotificationMutation { + return _u.mutation +} + +// Save executes the query and returns the number of nodes affected by the update operation. +func (_u *NotificationUpdate) Save(ctx context.Context) (int, error) { + return withHooks(ctx, _u.sqlSave, _u.mutation, _u.hooks) +} + +// SaveX is like Save, but panics if an error occurs. +func (_u *NotificationUpdate) SaveX(ctx context.Context) int { + affected, err := _u.Save(ctx) + if err != nil { + panic(err) + } + return affected +} + +// Exec executes the query. +func (_u *NotificationUpdate) Exec(ctx context.Context) error { + _, err := _u.Save(ctx) + return err +} + +// ExecX is like Exec, but panics if an error occurs. +func (_u *NotificationUpdate) ExecX(ctx context.Context) { + if err := _u.Exec(ctx); err != nil { + panic(err) + } +} + +// check runs all checks and user-defined validators on the builder. +func (_u *NotificationUpdate) check() error { + if v, ok := _u.mutation.SubscriberType(); ok { + if err := notification.SubscriberTypeValidator(v); err != nil { + return &ValidationError{Name: "subscriber_type", err: fmt.Errorf(`ent: validator failed for field "Notification.subscriber_type": %w`, err)} + } + } + if v, ok := _u.mutation.SubscriberID(); ok { + if err := notification.SubscriberIDValidator(v); err != nil { + return &ValidationError{Name: "subscriber_id", err: fmt.Errorf(`ent: validator failed for field "Notification.subscriber_id": %w`, err)} + } + } + if v, ok := _u.mutation.Status(); ok { + if err := notification.StatusValidator(v); err != nil { + return &ValidationError{Name: "status", err: fmt.Errorf(`ent: validator failed for field "Notification.status": %w`, err)} + } + } + if v, ok := _u.mutation.Message(); ok { + if err := notification.MessageValidator(v); err != nil { + return &ValidationError{Name: "message", err: fmt.Errorf(`ent: validator failed for field "Notification.message": %w`, err)} + } + } + return nil +} + +func (_u *NotificationUpdate) sqlSave(ctx context.Context) (_node int, err error) { + if err := _u.check(); err != nil { + return _node, err + } + _spec := sqlgraph.NewUpdateSpec(notification.Table, notification.Columns, sqlgraph.NewFieldSpec(notification.FieldID, field.TypeUUID)) + if ps := _u.mutation.predicates; len(ps) > 0 { + _spec.Predicate = func(selector *sql.Selector) { + for i := range ps { + ps[i](selector) + } + } + } + if value, ok := _u.mutation.SubscriptionID(); ok { + _spec.SetField(notification.FieldSubscriptionID, field.TypeUUID, value) + } + if value, ok := _u.mutation.AgentID(); ok { + _spec.SetField(notification.FieldAgentID, field.TypeUUID, value) + } + if value, ok := _u.mutation.ProjectID(); ok { + _spec.SetField(notification.FieldProjectID, field.TypeUUID, value) + } + if value, ok := _u.mutation.SubscriberType(); ok { + _spec.SetField(notification.FieldSubscriberType, field.TypeString, value) + } + if value, ok := _u.mutation.SubscriberID(); ok { + _spec.SetField(notification.FieldSubscriberID, field.TypeString, value) + } + if value, ok := _u.mutation.Status(); ok { + _spec.SetField(notification.FieldStatus, field.TypeString, value) + } + if value, ok := _u.mutation.Message(); ok { + _spec.SetField(notification.FieldMessage, field.TypeString, value) + } + if value, ok := _u.mutation.Dispatched(); ok { + _spec.SetField(notification.FieldDispatched, field.TypeBool, value) + } + if value, ok := _u.mutation.Acknowledged(); ok { + _spec.SetField(notification.FieldAcknowledged, field.TypeBool, value) + } + if _node, err = sqlgraph.UpdateNodes(ctx, _u.driver, _spec); err != nil { + if _, ok := err.(*sqlgraph.NotFoundError); ok { + err = &NotFoundError{notification.Label} + } else if sqlgraph.IsConstraintError(err) { + err = &ConstraintError{msg: err.Error(), wrap: err} + } + return 0, err + } + _u.mutation.done = true + return _node, nil +} + +// NotificationUpdateOne is the builder for updating a single Notification entity. +type NotificationUpdateOne struct { + config + fields []string + hooks []Hook + mutation *NotificationMutation +} + +// SetSubscriptionID sets the "subscription_id" field. +func (_u *NotificationUpdateOne) SetSubscriptionID(v uuid.UUID) *NotificationUpdateOne { + _u.mutation.SetSubscriptionID(v) + return _u +} + +// SetNillableSubscriptionID sets the "subscription_id" field if the given value is not nil. +func (_u *NotificationUpdateOne) SetNillableSubscriptionID(v *uuid.UUID) *NotificationUpdateOne { + if v != nil { + _u.SetSubscriptionID(*v) + } + return _u +} + +// SetAgentID sets the "agent_id" field. +func (_u *NotificationUpdateOne) SetAgentID(v uuid.UUID) *NotificationUpdateOne { + _u.mutation.SetAgentID(v) + return _u +} + +// SetNillableAgentID sets the "agent_id" field if the given value is not nil. +func (_u *NotificationUpdateOne) SetNillableAgentID(v *uuid.UUID) *NotificationUpdateOne { + if v != nil { + _u.SetAgentID(*v) + } + return _u +} + +// SetProjectID sets the "project_id" field. +func (_u *NotificationUpdateOne) SetProjectID(v uuid.UUID) *NotificationUpdateOne { + _u.mutation.SetProjectID(v) + return _u +} + +// SetNillableProjectID sets the "project_id" field if the given value is not nil. +func (_u *NotificationUpdateOne) SetNillableProjectID(v *uuid.UUID) *NotificationUpdateOne { + if v != nil { + _u.SetProjectID(*v) + } + return _u +} + +// SetSubscriberType sets the "subscriber_type" field. +func (_u *NotificationUpdateOne) SetSubscriberType(v string) *NotificationUpdateOne { + _u.mutation.SetSubscriberType(v) + return _u +} + +// SetNillableSubscriberType sets the "subscriber_type" field if the given value is not nil. +func (_u *NotificationUpdateOne) SetNillableSubscriberType(v *string) *NotificationUpdateOne { + if v != nil { + _u.SetSubscriberType(*v) + } + return _u +} + +// SetSubscriberID sets the "subscriber_id" field. +func (_u *NotificationUpdateOne) SetSubscriberID(v string) *NotificationUpdateOne { + _u.mutation.SetSubscriberID(v) + return _u +} + +// SetNillableSubscriberID sets the "subscriber_id" field if the given value is not nil. +func (_u *NotificationUpdateOne) SetNillableSubscriberID(v *string) *NotificationUpdateOne { + if v != nil { + _u.SetSubscriberID(*v) + } + return _u +} + +// SetStatus sets the "status" field. +func (_u *NotificationUpdateOne) SetStatus(v string) *NotificationUpdateOne { + _u.mutation.SetStatus(v) + return _u +} + +// SetNillableStatus sets the "status" field if the given value is not nil. +func (_u *NotificationUpdateOne) SetNillableStatus(v *string) *NotificationUpdateOne { + if v != nil { + _u.SetStatus(*v) + } + return _u +} + +// SetMessage sets the "message" field. +func (_u *NotificationUpdateOne) SetMessage(v string) *NotificationUpdateOne { + _u.mutation.SetMessage(v) + return _u +} + +// SetNillableMessage sets the "message" field if the given value is not nil. +func (_u *NotificationUpdateOne) SetNillableMessage(v *string) *NotificationUpdateOne { + if v != nil { + _u.SetMessage(*v) + } + return _u +} + +// SetDispatched sets the "dispatched" field. +func (_u *NotificationUpdateOne) SetDispatched(v bool) *NotificationUpdateOne { + _u.mutation.SetDispatched(v) + return _u +} + +// SetNillableDispatched sets the "dispatched" field if the given value is not nil. +func (_u *NotificationUpdateOne) SetNillableDispatched(v *bool) *NotificationUpdateOne { + if v != nil { + _u.SetDispatched(*v) + } + return _u +} + +// SetAcknowledged sets the "acknowledged" field. +func (_u *NotificationUpdateOne) SetAcknowledged(v bool) *NotificationUpdateOne { + _u.mutation.SetAcknowledged(v) + return _u +} + +// SetNillableAcknowledged sets the "acknowledged" field if the given value is not nil. +func (_u *NotificationUpdateOne) SetNillableAcknowledged(v *bool) *NotificationUpdateOne { + if v != nil { + _u.SetAcknowledged(*v) + } + return _u +} + +// Mutation returns the NotificationMutation object of the builder. +func (_u *NotificationUpdateOne) Mutation() *NotificationMutation { + return _u.mutation +} + +// Where appends a list predicates to the NotificationUpdate builder. +func (_u *NotificationUpdateOne) Where(ps ...predicate.Notification) *NotificationUpdateOne { + _u.mutation.Where(ps...) + return _u +} + +// Select allows selecting one or more fields (columns) of the returned entity. +// The default is selecting all fields defined in the entity schema. +func (_u *NotificationUpdateOne) Select(field string, fields ...string) *NotificationUpdateOne { + _u.fields = append([]string{field}, fields...) + return _u +} + +// Save executes the query and returns the updated Notification entity. +func (_u *NotificationUpdateOne) Save(ctx context.Context) (*Notification, error) { + return withHooks(ctx, _u.sqlSave, _u.mutation, _u.hooks) +} + +// SaveX is like Save, but panics if an error occurs. +func (_u *NotificationUpdateOne) SaveX(ctx context.Context) *Notification { + node, err := _u.Save(ctx) + if err != nil { + panic(err) + } + return node +} + +// Exec executes the query on the entity. +func (_u *NotificationUpdateOne) Exec(ctx context.Context) error { + _, err := _u.Save(ctx) + return err +} + +// ExecX is like Exec, but panics if an error occurs. +func (_u *NotificationUpdateOne) ExecX(ctx context.Context) { + if err := _u.Exec(ctx); err != nil { + panic(err) + } +} + +// check runs all checks and user-defined validators on the builder. +func (_u *NotificationUpdateOne) check() error { + if v, ok := _u.mutation.SubscriberType(); ok { + if err := notification.SubscriberTypeValidator(v); err != nil { + return &ValidationError{Name: "subscriber_type", err: fmt.Errorf(`ent: validator failed for field "Notification.subscriber_type": %w`, err)} + } + } + if v, ok := _u.mutation.SubscriberID(); ok { + if err := notification.SubscriberIDValidator(v); err != nil { + return &ValidationError{Name: "subscriber_id", err: fmt.Errorf(`ent: validator failed for field "Notification.subscriber_id": %w`, err)} + } + } + if v, ok := _u.mutation.Status(); ok { + if err := notification.StatusValidator(v); err != nil { + return &ValidationError{Name: "status", err: fmt.Errorf(`ent: validator failed for field "Notification.status": %w`, err)} + } + } + if v, ok := _u.mutation.Message(); ok { + if err := notification.MessageValidator(v); err != nil { + return &ValidationError{Name: "message", err: fmt.Errorf(`ent: validator failed for field "Notification.message": %w`, err)} + } + } + return nil +} + +func (_u *NotificationUpdateOne) sqlSave(ctx context.Context) (_node *Notification, err error) { + if err := _u.check(); err != nil { + return _node, err + } + _spec := sqlgraph.NewUpdateSpec(notification.Table, notification.Columns, sqlgraph.NewFieldSpec(notification.FieldID, field.TypeUUID)) + id, ok := _u.mutation.ID() + if !ok { + return nil, &ValidationError{Name: "id", err: errors.New(`ent: missing "Notification.id" for update`)} + } + _spec.Node.ID.Value = id + if fields := _u.fields; len(fields) > 0 { + _spec.Node.Columns = make([]string, 0, len(fields)) + _spec.Node.Columns = append(_spec.Node.Columns, notification.FieldID) + for _, f := range fields { + if !notification.ValidColumn(f) { + return nil, &ValidationError{Name: f, err: fmt.Errorf("ent: invalid field %q for query", f)} + } + if f != notification.FieldID { + _spec.Node.Columns = append(_spec.Node.Columns, f) + } + } + } + if ps := _u.mutation.predicates; len(ps) > 0 { + _spec.Predicate = func(selector *sql.Selector) { + for i := range ps { + ps[i](selector) + } + } + } + if value, ok := _u.mutation.SubscriptionID(); ok { + _spec.SetField(notification.FieldSubscriptionID, field.TypeUUID, value) + } + if value, ok := _u.mutation.AgentID(); ok { + _spec.SetField(notification.FieldAgentID, field.TypeUUID, value) + } + if value, ok := _u.mutation.ProjectID(); ok { + _spec.SetField(notification.FieldProjectID, field.TypeUUID, value) + } + if value, ok := _u.mutation.SubscriberType(); ok { + _spec.SetField(notification.FieldSubscriberType, field.TypeString, value) + } + if value, ok := _u.mutation.SubscriberID(); ok { + _spec.SetField(notification.FieldSubscriberID, field.TypeString, value) + } + if value, ok := _u.mutation.Status(); ok { + _spec.SetField(notification.FieldStatus, field.TypeString, value) + } + if value, ok := _u.mutation.Message(); ok { + _spec.SetField(notification.FieldMessage, field.TypeString, value) + } + if value, ok := _u.mutation.Dispatched(); ok { + _spec.SetField(notification.FieldDispatched, field.TypeBool, value) + } + if value, ok := _u.mutation.Acknowledged(); ok { + _spec.SetField(notification.FieldAcknowledged, field.TypeBool, value) + } + _node = &Notification{config: _u.config} + _spec.Assign = _node.assignValues + _spec.ScanValues = _node.scanValues + if err = sqlgraph.UpdateNode(ctx, _u.driver, _spec); err != nil { + if _, ok := err.(*sqlgraph.NotFoundError); ok { + err = &NotFoundError{notification.Label} + } else if sqlgraph.IsConstraintError(err) { + err = &ConstraintError{msg: err.Error(), wrap: err} + } + return nil, err + } + _u.mutation.done = true + return _node, nil +} diff --git a/pkg/ent/notificationsubscription.go b/pkg/ent/notificationsubscription.go new file mode 100644 index 000000000..fe6952d31 --- /dev/null +++ b/pkg/ent/notificationsubscription.go @@ -0,0 +1,189 @@ +// Code generated by ent, DO NOT EDIT. + +package ent + +import ( + "fmt" + "strings" + "time" + + "entgo.io/ent" + "entgo.io/ent/dialect/sql" + "github.com/GoogleCloudPlatform/scion/pkg/ent/notificationsubscription" + "github.com/google/uuid" +) + +// NotificationSubscription is the model entity for the NotificationSubscription schema. +type NotificationSubscription struct { + config `json:"-"` + // ID of the ent. + ID uuid.UUID `json:"id,omitempty"` + // Scope holds the value of the "scope" field. + Scope string `json:"scope,omitempty"` + // AgentID holds the value of the "agent_id" field. + AgentID *uuid.UUID `json:"agent_id,omitempty"` + // SubscriberType holds the value of the "subscriber_type" field. + SubscriberType string `json:"subscriber_type,omitempty"` + // SubscriberID holds the value of the "subscriber_id" field. + SubscriberID string `json:"subscriber_id,omitempty"` + // ProjectID holds the value of the "project_id" field. + ProjectID uuid.UUID `json:"project_id,omitempty"` + // TriggerActivities holds the value of the "trigger_activities" field. + TriggerActivities string `json:"trigger_activities,omitempty"` + // CreatedBy holds the value of the "created_by" field. + CreatedBy string `json:"created_by,omitempty"` + // Created holds the value of the "created" field. + Created time.Time `json:"created,omitempty"` + selectValues sql.SelectValues +} + +// scanValues returns the types for scanning values from sql.Rows. +func (*NotificationSubscription) scanValues(columns []string) ([]any, error) { + values := make([]any, len(columns)) + for i := range columns { + switch columns[i] { + case notificationsubscription.FieldAgentID: + values[i] = &sql.NullScanner{S: new(uuid.UUID)} + case notificationsubscription.FieldScope, notificationsubscription.FieldSubscriberType, notificationsubscription.FieldSubscriberID, notificationsubscription.FieldTriggerActivities, notificationsubscription.FieldCreatedBy: + values[i] = new(sql.NullString) + case notificationsubscription.FieldCreated: + values[i] = new(sql.NullTime) + case notificationsubscription.FieldID, notificationsubscription.FieldProjectID: + values[i] = new(uuid.UUID) + default: + values[i] = new(sql.UnknownType) + } + } + return values, nil +} + +// assignValues assigns the values that were returned from sql.Rows (after scanning) +// to the NotificationSubscription fields. +func (_m *NotificationSubscription) assignValues(columns []string, values []any) error { + if m, n := len(values), len(columns); m < n { + return fmt.Errorf("mismatch number of scan values: %d != %d", m, n) + } + for i := range columns { + switch columns[i] { + case notificationsubscription.FieldID: + if value, ok := values[i].(*uuid.UUID); !ok { + return fmt.Errorf("unexpected type %T for field id", values[i]) + } else if value != nil { + _m.ID = *value + } + case notificationsubscription.FieldScope: + if value, ok := values[i].(*sql.NullString); !ok { + return fmt.Errorf("unexpected type %T for field scope", values[i]) + } else if value.Valid { + _m.Scope = value.String + } + case notificationsubscription.FieldAgentID: + if value, ok := values[i].(*sql.NullScanner); !ok { + return fmt.Errorf("unexpected type %T for field agent_id", values[i]) + } else if value.Valid { + _m.AgentID = new(uuid.UUID) + *_m.AgentID = *value.S.(*uuid.UUID) + } + case notificationsubscription.FieldSubscriberType: + if value, ok := values[i].(*sql.NullString); !ok { + return fmt.Errorf("unexpected type %T for field subscriber_type", values[i]) + } else if value.Valid { + _m.SubscriberType = value.String + } + case notificationsubscription.FieldSubscriberID: + if value, ok := values[i].(*sql.NullString); !ok { + return fmt.Errorf("unexpected type %T for field subscriber_id", values[i]) + } else if value.Valid { + _m.SubscriberID = value.String + } + case notificationsubscription.FieldProjectID: + if value, ok := values[i].(*uuid.UUID); !ok { + return fmt.Errorf("unexpected type %T for field project_id", values[i]) + } else if value != nil { + _m.ProjectID = *value + } + case notificationsubscription.FieldTriggerActivities: + if value, ok := values[i].(*sql.NullString); !ok { + return fmt.Errorf("unexpected type %T for field trigger_activities", values[i]) + } else if value.Valid { + _m.TriggerActivities = value.String + } + case notificationsubscription.FieldCreatedBy: + if value, ok := values[i].(*sql.NullString); !ok { + return fmt.Errorf("unexpected type %T for field created_by", values[i]) + } else if value.Valid { + _m.CreatedBy = value.String + } + case notificationsubscription.FieldCreated: + if value, ok := values[i].(*sql.NullTime); !ok { + return fmt.Errorf("unexpected type %T for field created", values[i]) + } else if value.Valid { + _m.Created = value.Time + } + default: + _m.selectValues.Set(columns[i], values[i]) + } + } + return nil +} + +// Value returns the ent.Value that was dynamically selected and assigned to the NotificationSubscription. +// This includes values selected through modifiers, order, etc. +func (_m *NotificationSubscription) Value(name string) (ent.Value, error) { + return _m.selectValues.Get(name) +} + +// Update returns a builder for updating this NotificationSubscription. +// Note that you need to call NotificationSubscription.Unwrap() before calling this method if this NotificationSubscription +// was returned from a transaction, and the transaction was committed or rolled back. +func (_m *NotificationSubscription) Update() *NotificationSubscriptionUpdateOne { + return NewNotificationSubscriptionClient(_m.config).UpdateOne(_m) +} + +// Unwrap unwraps the NotificationSubscription entity that was returned from a transaction after it was closed, +// so that all future queries will be executed through the driver which created the transaction. +func (_m *NotificationSubscription) Unwrap() *NotificationSubscription { + _tx, ok := _m.config.driver.(*txDriver) + if !ok { + panic("ent: NotificationSubscription is not a transactional entity") + } + _m.config.driver = _tx.drv + return _m +} + +// String implements the fmt.Stringer. +func (_m *NotificationSubscription) String() string { + var builder strings.Builder + builder.WriteString("NotificationSubscription(") + builder.WriteString(fmt.Sprintf("id=%v, ", _m.ID)) + builder.WriteString("scope=") + builder.WriteString(_m.Scope) + builder.WriteString(", ") + if v := _m.AgentID; v != nil { + builder.WriteString("agent_id=") + builder.WriteString(fmt.Sprintf("%v", *v)) + } + builder.WriteString(", ") + builder.WriteString("subscriber_type=") + builder.WriteString(_m.SubscriberType) + builder.WriteString(", ") + builder.WriteString("subscriber_id=") + builder.WriteString(_m.SubscriberID) + builder.WriteString(", ") + builder.WriteString("project_id=") + builder.WriteString(fmt.Sprintf("%v", _m.ProjectID)) + builder.WriteString(", ") + builder.WriteString("trigger_activities=") + builder.WriteString(_m.TriggerActivities) + builder.WriteString(", ") + builder.WriteString("created_by=") + builder.WriteString(_m.CreatedBy) + builder.WriteString(", ") + builder.WriteString("created=") + builder.WriteString(_m.Created.Format(time.ANSIC)) + builder.WriteByte(')') + return builder.String() +} + +// NotificationSubscriptions is a parsable slice of NotificationSubscription. +type NotificationSubscriptions []*NotificationSubscription diff --git a/pkg/ent/notificationsubscription/notificationsubscription.go b/pkg/ent/notificationsubscription/notificationsubscription.go new file mode 100644 index 000000000..2bc971e81 --- /dev/null +++ b/pkg/ent/notificationsubscription/notificationsubscription.go @@ -0,0 +1,123 @@ +// Code generated by ent, DO NOT EDIT. + +package notificationsubscription + +import ( + "time" + + "entgo.io/ent/dialect/sql" + "github.com/google/uuid" +) + +const ( + // Label holds the string label denoting the notificationsubscription type in the database. + Label = "notification_subscription" + // FieldID holds the string denoting the id field in the database. + FieldID = "id" + // FieldScope holds the string denoting the scope field in the database. + FieldScope = "scope" + // FieldAgentID holds the string denoting the agent_id field in the database. + FieldAgentID = "agent_id" + // FieldSubscriberType holds the string denoting the subscriber_type field in the database. + FieldSubscriberType = "subscriber_type" + // FieldSubscriberID holds the string denoting the subscriber_id field in the database. + FieldSubscriberID = "subscriber_id" + // FieldProjectID holds the string denoting the project_id field in the database. + FieldProjectID = "project_id" + // FieldTriggerActivities holds the string denoting the trigger_activities field in the database. + FieldTriggerActivities = "trigger_activities" + // FieldCreatedBy holds the string denoting the created_by field in the database. + FieldCreatedBy = "created_by" + // FieldCreated holds the string denoting the created field in the database. + FieldCreated = "created" + // Table holds the table name of the notificationsubscription in the database. + Table = "notification_subscriptions" +) + +// Columns holds all SQL columns for notificationsubscription fields. +var Columns = []string{ + FieldID, + FieldScope, + FieldAgentID, + FieldSubscriberType, + FieldSubscriberID, + FieldProjectID, + FieldTriggerActivities, + FieldCreatedBy, + FieldCreated, +} + +// ValidColumn reports if the column name is valid (part of the table columns). +func ValidColumn(column string) bool { + for i := range Columns { + if column == Columns[i] { + return true + } + } + return false +} + +var ( + // DefaultScope holds the default value on creation for the "scope" field. + DefaultScope string + // DefaultSubscriberType holds the default value on creation for the "subscriber_type" field. + DefaultSubscriberType string + // SubscriberIDValidator is a validator for the "subscriber_id" field. It is called by the builders before save. + SubscriberIDValidator func(string) error + // TriggerActivitiesValidator is a validator for the "trigger_activities" field. It is called by the builders before save. + TriggerActivitiesValidator func(string) error + // CreatedByValidator is a validator for the "created_by" field. It is called by the builders before save. + CreatedByValidator func(string) error + // DefaultCreated holds the default value on creation for the "created" field. + DefaultCreated func() time.Time + // DefaultID holds the default value on creation for the "id" field. + DefaultID func() uuid.UUID +) + +// OrderOption defines the ordering options for the NotificationSubscription queries. +type OrderOption func(*sql.Selector) + +// ByID orders the results by the id field. +func ByID(opts ...sql.OrderTermOption) OrderOption { + return sql.OrderByField(FieldID, opts...).ToFunc() +} + +// ByScope orders the results by the scope field. +func ByScope(opts ...sql.OrderTermOption) OrderOption { + return sql.OrderByField(FieldScope, opts...).ToFunc() +} + +// ByAgentID orders the results by the agent_id field. +func ByAgentID(opts ...sql.OrderTermOption) OrderOption { + return sql.OrderByField(FieldAgentID, opts...).ToFunc() +} + +// BySubscriberType orders the results by the subscriber_type field. +func BySubscriberType(opts ...sql.OrderTermOption) OrderOption { + return sql.OrderByField(FieldSubscriberType, opts...).ToFunc() +} + +// BySubscriberID orders the results by the subscriber_id field. +func BySubscriberID(opts ...sql.OrderTermOption) OrderOption { + return sql.OrderByField(FieldSubscriberID, opts...).ToFunc() +} + +// ByProjectID orders the results by the project_id field. +func ByProjectID(opts ...sql.OrderTermOption) OrderOption { + return sql.OrderByField(FieldProjectID, opts...).ToFunc() +} + +// ByTriggerActivities orders the results by the trigger_activities field. +func ByTriggerActivities(opts ...sql.OrderTermOption) OrderOption { + return sql.OrderByField(FieldTriggerActivities, opts...).ToFunc() +} + +// ByCreatedBy orders the results by the created_by field. +func ByCreatedBy(opts ...sql.OrderTermOption) OrderOption { + return sql.OrderByField(FieldCreatedBy, opts...).ToFunc() +} + +// ByCreated orders the results by the created field. +func ByCreated(opts ...sql.OrderTermOption) OrderOption { + return sql.OrderByField(FieldCreated, opts...).ToFunc() +} diff --git a/pkg/ent/notificationsubscription/where.go b/pkg/ent/notificationsubscription/where.go new file mode 100644 index 000000000..a783f41ae --- /dev/null +++ b/pkg/ent/notificationsubscription/where.go @@ -0,0 +1,566 @@ +// Code generated by ent, DO NOT EDIT. + +package notificationsubscription + +import ( + "time" + + "entgo.io/ent/dialect/sql" + "github.com/GoogleCloudPlatform/scion/pkg/ent/predicate" + "github.com/google/uuid" +) + +// ID filters vertices based on their ID field. +func ID(id uuid.UUID) predicate.NotificationSubscription { + return predicate.NotificationSubscription(sql.FieldEQ(FieldID, id)) +} + +// IDEQ applies the EQ predicate on the ID field. +func IDEQ(id uuid.UUID) predicate.NotificationSubscription { + return predicate.NotificationSubscription(sql.FieldEQ(FieldID, id)) +} + +// IDNEQ applies the NEQ predicate on the ID field. +func IDNEQ(id uuid.UUID) predicate.NotificationSubscription { + return predicate.NotificationSubscription(sql.FieldNEQ(FieldID, id)) +} + +// IDIn applies the In predicate on the ID field. +func IDIn(ids ...uuid.UUID) predicate.NotificationSubscription { + return predicate.NotificationSubscription(sql.FieldIn(FieldID, ids...)) +} + +// IDNotIn applies the NotIn predicate on the ID field. +func IDNotIn(ids ...uuid.UUID) predicate.NotificationSubscription { + return predicate.NotificationSubscription(sql.FieldNotIn(FieldID, ids...)) +} + +// IDGT applies the GT predicate on the ID field. +func IDGT(id uuid.UUID) predicate.NotificationSubscription { + return predicate.NotificationSubscription(sql.FieldGT(FieldID, id)) +} + +// IDGTE applies the GTE predicate on the ID field. +func IDGTE(id uuid.UUID) predicate.NotificationSubscription { + return predicate.NotificationSubscription(sql.FieldGTE(FieldID, id)) +} + +// IDLT applies the LT predicate on the ID field. +func IDLT(id uuid.UUID) predicate.NotificationSubscription { + return predicate.NotificationSubscription(sql.FieldLT(FieldID, id)) +} + +// IDLTE applies the LTE predicate on the ID field. +func IDLTE(id uuid.UUID) predicate.NotificationSubscription { + return predicate.NotificationSubscription(sql.FieldLTE(FieldID, id)) +} + +// Scope applies equality check predicate on the "scope" field. It's identical to ScopeEQ. +func Scope(v string) predicate.NotificationSubscription { + return predicate.NotificationSubscription(sql.FieldEQ(FieldScope, v)) +} + +// AgentID applies equality check predicate on the "agent_id" field. It's identical to AgentIDEQ. +func AgentID(v uuid.UUID) predicate.NotificationSubscription { + return predicate.NotificationSubscription(sql.FieldEQ(FieldAgentID, v)) +} + +// SubscriberType applies equality check predicate on the "subscriber_type" field. It's identical to SubscriberTypeEQ. +func SubscriberType(v string) predicate.NotificationSubscription { + return predicate.NotificationSubscription(sql.FieldEQ(FieldSubscriberType, v)) +} + +// SubscriberID applies equality check predicate on the "subscriber_id" field. It's identical to SubscriberIDEQ. +func SubscriberID(v string) predicate.NotificationSubscription { + return predicate.NotificationSubscription(sql.FieldEQ(FieldSubscriberID, v)) +} + +// ProjectID applies equality check predicate on the "project_id" field. It's identical to ProjectIDEQ. +func ProjectID(v uuid.UUID) predicate.NotificationSubscription { + return predicate.NotificationSubscription(sql.FieldEQ(FieldProjectID, v)) +} + +// TriggerActivities applies equality check predicate on the "trigger_activities" field. It's identical to TriggerActivitiesEQ. +func TriggerActivities(v string) predicate.NotificationSubscription { + return predicate.NotificationSubscription(sql.FieldEQ(FieldTriggerActivities, v)) +} + +// CreatedBy applies equality check predicate on the "created_by" field. It's identical to CreatedByEQ. +func CreatedBy(v string) predicate.NotificationSubscription { + return predicate.NotificationSubscription(sql.FieldEQ(FieldCreatedBy, v)) +} + +// Created applies equality check predicate on the "created" field. It's identical to CreatedEQ. +func Created(v time.Time) predicate.NotificationSubscription { + return predicate.NotificationSubscription(sql.FieldEQ(FieldCreated, v)) +} + +// ScopeEQ applies the EQ predicate on the "scope" field. +func ScopeEQ(v string) predicate.NotificationSubscription { + return predicate.NotificationSubscription(sql.FieldEQ(FieldScope, v)) +} + +// ScopeNEQ applies the NEQ predicate on the "scope" field. +func ScopeNEQ(v string) predicate.NotificationSubscription { + return predicate.NotificationSubscription(sql.FieldNEQ(FieldScope, v)) +} + +// ScopeIn applies the In predicate on the "scope" field. +func ScopeIn(vs ...string) predicate.NotificationSubscription { + return predicate.NotificationSubscription(sql.FieldIn(FieldScope, vs...)) +} + +// ScopeNotIn applies the NotIn predicate on the "scope" field. +func ScopeNotIn(vs ...string) predicate.NotificationSubscription { + return predicate.NotificationSubscription(sql.FieldNotIn(FieldScope, vs...)) +} + +// ScopeGT applies the GT predicate on the "scope" field. +func ScopeGT(v string) predicate.NotificationSubscription { + return predicate.NotificationSubscription(sql.FieldGT(FieldScope, v)) +} + +// ScopeGTE applies the GTE predicate on the "scope" field. +func ScopeGTE(v string) predicate.NotificationSubscription { + return predicate.NotificationSubscription(sql.FieldGTE(FieldScope, v)) +} + +// ScopeLT applies the LT predicate on the "scope" field. +func ScopeLT(v string) predicate.NotificationSubscription { + return predicate.NotificationSubscription(sql.FieldLT(FieldScope, v)) +} + +// ScopeLTE applies the LTE predicate on the "scope" field. +func ScopeLTE(v string) predicate.NotificationSubscription { + return predicate.NotificationSubscription(sql.FieldLTE(FieldScope, v)) +} + +// ScopeContains applies the Contains predicate on the "scope" field. +func ScopeContains(v string) predicate.NotificationSubscription { + return predicate.NotificationSubscription(sql.FieldContains(FieldScope, v)) +} + +// ScopeHasPrefix applies the HasPrefix predicate on the "scope" field. +func ScopeHasPrefix(v string) predicate.NotificationSubscription { + return predicate.NotificationSubscription(sql.FieldHasPrefix(FieldScope, v)) +} + +// ScopeHasSuffix applies the HasSuffix predicate on the "scope" field. +func ScopeHasSuffix(v string) predicate.NotificationSubscription { + return predicate.NotificationSubscription(sql.FieldHasSuffix(FieldScope, v)) +} + +// ScopeEqualFold applies the EqualFold predicate on the "scope" field. +func ScopeEqualFold(v string) predicate.NotificationSubscription { + return predicate.NotificationSubscription(sql.FieldEqualFold(FieldScope, v)) +} + +// ScopeContainsFold applies the ContainsFold predicate on the "scope" field. +func ScopeContainsFold(v string) predicate.NotificationSubscription { + return predicate.NotificationSubscription(sql.FieldContainsFold(FieldScope, v)) +} + +// AgentIDEQ applies the EQ predicate on the "agent_id" field. +func AgentIDEQ(v uuid.UUID) predicate.NotificationSubscription { + return predicate.NotificationSubscription(sql.FieldEQ(FieldAgentID, v)) +} + +// AgentIDNEQ applies the NEQ predicate on the "agent_id" field. +func AgentIDNEQ(v uuid.UUID) predicate.NotificationSubscription { + return predicate.NotificationSubscription(sql.FieldNEQ(FieldAgentID, v)) +} + +// AgentIDIn applies the In predicate on the "agent_id" field. +func AgentIDIn(vs ...uuid.UUID) predicate.NotificationSubscription { + return predicate.NotificationSubscription(sql.FieldIn(FieldAgentID, vs...)) +} + +// AgentIDNotIn applies the NotIn predicate on the "agent_id" field. +func AgentIDNotIn(vs ...uuid.UUID) predicate.NotificationSubscription { + return predicate.NotificationSubscription(sql.FieldNotIn(FieldAgentID, vs...)) +} + +// AgentIDGT applies the GT predicate on the "agent_id" field. +func AgentIDGT(v uuid.UUID) predicate.NotificationSubscription { + return predicate.NotificationSubscription(sql.FieldGT(FieldAgentID, v)) +} + +// AgentIDGTE applies the GTE predicate on the "agent_id" field. +func AgentIDGTE(v uuid.UUID) predicate.NotificationSubscription { + return predicate.NotificationSubscription(sql.FieldGTE(FieldAgentID, v)) +} + +// AgentIDLT applies the LT predicate on the "agent_id" field. +func AgentIDLT(v uuid.UUID) predicate.NotificationSubscription { + return predicate.NotificationSubscription(sql.FieldLT(FieldAgentID, v)) +} + +// AgentIDLTE applies the LTE predicate on the "agent_id" field. +func AgentIDLTE(v uuid.UUID) predicate.NotificationSubscription { + return predicate.NotificationSubscription(sql.FieldLTE(FieldAgentID, v)) +} + +// AgentIDIsNil applies the IsNil predicate on the "agent_id" field. +func AgentIDIsNil() predicate.NotificationSubscription { + return predicate.NotificationSubscription(sql.FieldIsNull(FieldAgentID)) +} + +// AgentIDNotNil applies the NotNil predicate on the "agent_id" field. +func AgentIDNotNil() predicate.NotificationSubscription { + return predicate.NotificationSubscription(sql.FieldNotNull(FieldAgentID)) +} + +// SubscriberTypeEQ applies the EQ predicate on the "subscriber_type" field. +func SubscriberTypeEQ(v string) predicate.NotificationSubscription { + return predicate.NotificationSubscription(sql.FieldEQ(FieldSubscriberType, v)) +} + +// SubscriberTypeNEQ applies the NEQ predicate on the "subscriber_type" field. +func SubscriberTypeNEQ(v string) predicate.NotificationSubscription { + return predicate.NotificationSubscription(sql.FieldNEQ(FieldSubscriberType, v)) +} + +// SubscriberTypeIn applies the In predicate on the "subscriber_type" field. +func SubscriberTypeIn(vs ...string) predicate.NotificationSubscription { + return predicate.NotificationSubscription(sql.FieldIn(FieldSubscriberType, vs...)) +} + +// SubscriberTypeNotIn applies the NotIn predicate on the "subscriber_type" field. +func SubscriberTypeNotIn(vs ...string) predicate.NotificationSubscription { + return predicate.NotificationSubscription(sql.FieldNotIn(FieldSubscriberType, vs...)) +} + +// SubscriberTypeGT applies the GT predicate on the "subscriber_type" field. +func SubscriberTypeGT(v string) predicate.NotificationSubscription { + return predicate.NotificationSubscription(sql.FieldGT(FieldSubscriberType, v)) +} + +// SubscriberTypeGTE applies the GTE predicate on the "subscriber_type" field. +func SubscriberTypeGTE(v string) predicate.NotificationSubscription { + return predicate.NotificationSubscription(sql.FieldGTE(FieldSubscriberType, v)) +} + +// SubscriberTypeLT applies the LT predicate on the "subscriber_type" field. +func SubscriberTypeLT(v string) predicate.NotificationSubscription { + return predicate.NotificationSubscription(sql.FieldLT(FieldSubscriberType, v)) +} + +// SubscriberTypeLTE applies the LTE predicate on the "subscriber_type" field. +func SubscriberTypeLTE(v string) predicate.NotificationSubscription { + return predicate.NotificationSubscription(sql.FieldLTE(FieldSubscriberType, v)) +} + +// SubscriberTypeContains applies the Contains predicate on the "subscriber_type" field. +func SubscriberTypeContains(v string) predicate.NotificationSubscription { + return predicate.NotificationSubscription(sql.FieldContains(FieldSubscriberType, v)) +} + +// SubscriberTypeHasPrefix applies the HasPrefix predicate on the "subscriber_type" field. +func SubscriberTypeHasPrefix(v string) predicate.NotificationSubscription { + return predicate.NotificationSubscription(sql.FieldHasPrefix(FieldSubscriberType, v)) +} + +// SubscriberTypeHasSuffix applies the HasSuffix predicate on the "subscriber_type" field. +func SubscriberTypeHasSuffix(v string) predicate.NotificationSubscription { + return predicate.NotificationSubscription(sql.FieldHasSuffix(FieldSubscriberType, v)) +} + +// SubscriberTypeEqualFold applies the EqualFold predicate on the "subscriber_type" field. +func SubscriberTypeEqualFold(v string) predicate.NotificationSubscription { + return predicate.NotificationSubscription(sql.FieldEqualFold(FieldSubscriberType, v)) +} + +// SubscriberTypeContainsFold applies the ContainsFold predicate on the "subscriber_type" field. +func SubscriberTypeContainsFold(v string) predicate.NotificationSubscription { + return predicate.NotificationSubscription(sql.FieldContainsFold(FieldSubscriberType, v)) +} + +// SubscriberIDEQ applies the EQ predicate on the "subscriber_id" field. +func SubscriberIDEQ(v string) predicate.NotificationSubscription { + return predicate.NotificationSubscription(sql.FieldEQ(FieldSubscriberID, v)) +} + +// SubscriberIDNEQ applies the NEQ predicate on the "subscriber_id" field. +func SubscriberIDNEQ(v string) predicate.NotificationSubscription { + return predicate.NotificationSubscription(sql.FieldNEQ(FieldSubscriberID, v)) +} + +// SubscriberIDIn applies the In predicate on the "subscriber_id" field. +func SubscriberIDIn(vs ...string) predicate.NotificationSubscription { + return predicate.NotificationSubscription(sql.FieldIn(FieldSubscriberID, vs...)) +} + +// SubscriberIDNotIn applies the NotIn predicate on the "subscriber_id" field. +func SubscriberIDNotIn(vs ...string) predicate.NotificationSubscription { + return predicate.NotificationSubscription(sql.FieldNotIn(FieldSubscriberID, vs...)) +} + +// SubscriberIDGT applies the GT predicate on the "subscriber_id" field. +func SubscriberIDGT(v string) predicate.NotificationSubscription { + return predicate.NotificationSubscription(sql.FieldGT(FieldSubscriberID, v)) +} + +// SubscriberIDGTE applies the GTE predicate on the "subscriber_id" field. +func SubscriberIDGTE(v string) predicate.NotificationSubscription { + return predicate.NotificationSubscription(sql.FieldGTE(FieldSubscriberID, v)) +} + +// SubscriberIDLT applies the LT predicate on the "subscriber_id" field. +func SubscriberIDLT(v string) predicate.NotificationSubscription { + return predicate.NotificationSubscription(sql.FieldLT(FieldSubscriberID, v)) +} + +// SubscriberIDLTE applies the LTE predicate on the "subscriber_id" field. +func SubscriberIDLTE(v string) predicate.NotificationSubscription { + return predicate.NotificationSubscription(sql.FieldLTE(FieldSubscriberID, v)) +} + +// SubscriberIDContains applies the Contains predicate on the "subscriber_id" field. +func SubscriberIDContains(v string) predicate.NotificationSubscription { + return predicate.NotificationSubscription(sql.FieldContains(FieldSubscriberID, v)) +} + +// SubscriberIDHasPrefix applies the HasPrefix predicate on the "subscriber_id" field. +func SubscriberIDHasPrefix(v string) predicate.NotificationSubscription { + return predicate.NotificationSubscription(sql.FieldHasPrefix(FieldSubscriberID, v)) +} + +// SubscriberIDHasSuffix applies the HasSuffix predicate on the "subscriber_id" field. +func SubscriberIDHasSuffix(v string) predicate.NotificationSubscription { + return predicate.NotificationSubscription(sql.FieldHasSuffix(FieldSubscriberID, v)) +} + +// SubscriberIDEqualFold applies the EqualFold predicate on the "subscriber_id" field. +func SubscriberIDEqualFold(v string) predicate.NotificationSubscription { + return predicate.NotificationSubscription(sql.FieldEqualFold(FieldSubscriberID, v)) +} + +// SubscriberIDContainsFold applies the ContainsFold predicate on the "subscriber_id" field. +func SubscriberIDContainsFold(v string) predicate.NotificationSubscription { + return predicate.NotificationSubscription(sql.FieldContainsFold(FieldSubscriberID, v)) +} + +// ProjectIDEQ applies the EQ predicate on the "project_id" field. +func ProjectIDEQ(v uuid.UUID) predicate.NotificationSubscription { + return predicate.NotificationSubscription(sql.FieldEQ(FieldProjectID, v)) +} + +// ProjectIDNEQ applies the NEQ predicate on the "project_id" field. +func ProjectIDNEQ(v uuid.UUID) predicate.NotificationSubscription { + return predicate.NotificationSubscription(sql.FieldNEQ(FieldProjectID, v)) +} + +// ProjectIDIn applies the In predicate on the "project_id" field. +func ProjectIDIn(vs ...uuid.UUID) predicate.NotificationSubscription { + return predicate.NotificationSubscription(sql.FieldIn(FieldProjectID, vs...)) +} + +// ProjectIDNotIn applies the NotIn predicate on the "project_id" field. +func ProjectIDNotIn(vs ...uuid.UUID) predicate.NotificationSubscription { + return predicate.NotificationSubscription(sql.FieldNotIn(FieldProjectID, vs...)) +} + +// ProjectIDGT applies the GT predicate on the "project_id" field. +func ProjectIDGT(v uuid.UUID) predicate.NotificationSubscription { + return predicate.NotificationSubscription(sql.FieldGT(FieldProjectID, v)) +} + +// ProjectIDGTE applies the GTE predicate on the "project_id" field. +func ProjectIDGTE(v uuid.UUID) predicate.NotificationSubscription { + return predicate.NotificationSubscription(sql.FieldGTE(FieldProjectID, v)) +} + +// ProjectIDLT applies the LT predicate on the "project_id" field. +func ProjectIDLT(v uuid.UUID) predicate.NotificationSubscription { + return predicate.NotificationSubscription(sql.FieldLT(FieldProjectID, v)) +} + +// ProjectIDLTE applies the LTE predicate on the "project_id" field. +func ProjectIDLTE(v uuid.UUID) predicate.NotificationSubscription { + return predicate.NotificationSubscription(sql.FieldLTE(FieldProjectID, v)) +} + +// TriggerActivitiesEQ applies the EQ predicate on the "trigger_activities" field. +func TriggerActivitiesEQ(v string) predicate.NotificationSubscription { + return predicate.NotificationSubscription(sql.FieldEQ(FieldTriggerActivities, v)) +} + +// TriggerActivitiesNEQ applies the NEQ predicate on the "trigger_activities" field. +func TriggerActivitiesNEQ(v string) predicate.NotificationSubscription { + return predicate.NotificationSubscription(sql.FieldNEQ(FieldTriggerActivities, v)) +} + +// TriggerActivitiesIn applies the In predicate on the "trigger_activities" field. +func TriggerActivitiesIn(vs ...string) predicate.NotificationSubscription { + return predicate.NotificationSubscription(sql.FieldIn(FieldTriggerActivities, vs...)) +} + +// TriggerActivitiesNotIn applies the NotIn predicate on the "trigger_activities" field. +func TriggerActivitiesNotIn(vs ...string) predicate.NotificationSubscription { + return predicate.NotificationSubscription(sql.FieldNotIn(FieldTriggerActivities, vs...)) +} + +// TriggerActivitiesGT applies the GT predicate on the "trigger_activities" field. +func TriggerActivitiesGT(v string) predicate.NotificationSubscription { + return predicate.NotificationSubscription(sql.FieldGT(FieldTriggerActivities, v)) +} + +// TriggerActivitiesGTE applies the GTE predicate on the "trigger_activities" field. +func TriggerActivitiesGTE(v string) predicate.NotificationSubscription { + return predicate.NotificationSubscription(sql.FieldGTE(FieldTriggerActivities, v)) +} + +// TriggerActivitiesLT applies the LT predicate on the "trigger_activities" field. +func TriggerActivitiesLT(v string) predicate.NotificationSubscription { + return predicate.NotificationSubscription(sql.FieldLT(FieldTriggerActivities, v)) +} + +// TriggerActivitiesLTE applies the LTE predicate on the "trigger_activities" field. +func TriggerActivitiesLTE(v string) predicate.NotificationSubscription { + return predicate.NotificationSubscription(sql.FieldLTE(FieldTriggerActivities, v)) +} + +// TriggerActivitiesContains applies the Contains predicate on the "trigger_activities" field. +func TriggerActivitiesContains(v string) predicate.NotificationSubscription { + return predicate.NotificationSubscription(sql.FieldContains(FieldTriggerActivities, v)) +} + +// TriggerActivitiesHasPrefix applies the HasPrefix predicate on the "trigger_activities" field. +func TriggerActivitiesHasPrefix(v string) predicate.NotificationSubscription { + return predicate.NotificationSubscription(sql.FieldHasPrefix(FieldTriggerActivities, v)) +} + +// TriggerActivitiesHasSuffix applies the HasSuffix predicate on the "trigger_activities" field. +func TriggerActivitiesHasSuffix(v string) predicate.NotificationSubscription { + return predicate.NotificationSubscription(sql.FieldHasSuffix(FieldTriggerActivities, v)) +} + +// TriggerActivitiesEqualFold applies the EqualFold predicate on the "trigger_activities" field. +func TriggerActivitiesEqualFold(v string) predicate.NotificationSubscription { + return predicate.NotificationSubscription(sql.FieldEqualFold(FieldTriggerActivities, v)) +} + +// TriggerActivitiesContainsFold applies the ContainsFold predicate on the "trigger_activities" field. +func TriggerActivitiesContainsFold(v string) predicate.NotificationSubscription { + return predicate.NotificationSubscription(sql.FieldContainsFold(FieldTriggerActivities, v)) +} + +// CreatedByEQ applies the EQ predicate on the "created_by" field. +func CreatedByEQ(v string) predicate.NotificationSubscription { + return predicate.NotificationSubscription(sql.FieldEQ(FieldCreatedBy, v)) +} + +// CreatedByNEQ applies the NEQ predicate on the "created_by" field. +func CreatedByNEQ(v string) predicate.NotificationSubscription { + return predicate.NotificationSubscription(sql.FieldNEQ(FieldCreatedBy, v)) +} + +// CreatedByIn applies the In predicate on the "created_by" field. +func CreatedByIn(vs ...string) predicate.NotificationSubscription { + return predicate.NotificationSubscription(sql.FieldIn(FieldCreatedBy, vs...)) +} + +// CreatedByNotIn applies the NotIn predicate on the "created_by" field. +func CreatedByNotIn(vs ...string) predicate.NotificationSubscription { + return predicate.NotificationSubscription(sql.FieldNotIn(FieldCreatedBy, vs...)) +} + +// CreatedByGT applies the GT predicate on the "created_by" field. +func CreatedByGT(v string) predicate.NotificationSubscription { + return predicate.NotificationSubscription(sql.FieldGT(FieldCreatedBy, v)) +} + +// CreatedByGTE applies the GTE predicate on the "created_by" field. +func CreatedByGTE(v string) predicate.NotificationSubscription { + return predicate.NotificationSubscription(sql.FieldGTE(FieldCreatedBy, v)) +} + +// CreatedByLT applies the LT predicate on the "created_by" field. +func CreatedByLT(v string) predicate.NotificationSubscription { + return predicate.NotificationSubscription(sql.FieldLT(FieldCreatedBy, v)) +} + +// CreatedByLTE applies the LTE predicate on the "created_by" field. +func CreatedByLTE(v string) predicate.NotificationSubscription { + return predicate.NotificationSubscription(sql.FieldLTE(FieldCreatedBy, v)) +} + +// CreatedByContains applies the Contains predicate on the "created_by" field. +func CreatedByContains(v string) predicate.NotificationSubscription { + return predicate.NotificationSubscription(sql.FieldContains(FieldCreatedBy, v)) +} + +// CreatedByHasPrefix applies the HasPrefix predicate on the "created_by" field. +func CreatedByHasPrefix(v string) predicate.NotificationSubscription { + return predicate.NotificationSubscription(sql.FieldHasPrefix(FieldCreatedBy, v)) +} + +// CreatedByHasSuffix applies the HasSuffix predicate on the "created_by" field. +func CreatedByHasSuffix(v string) predicate.NotificationSubscription { + return predicate.NotificationSubscription(sql.FieldHasSuffix(FieldCreatedBy, v)) +} + +// CreatedByEqualFold applies the EqualFold predicate on the "created_by" field. +func CreatedByEqualFold(v string) predicate.NotificationSubscription { + return predicate.NotificationSubscription(sql.FieldEqualFold(FieldCreatedBy, v)) +} + +// CreatedByContainsFold applies the ContainsFold predicate on the "created_by" field. +func CreatedByContainsFold(v string) predicate.NotificationSubscription { + return predicate.NotificationSubscription(sql.FieldContainsFold(FieldCreatedBy, v)) +} + +// CreatedEQ applies the EQ predicate on the "created" field. +func CreatedEQ(v time.Time) predicate.NotificationSubscription { + return predicate.NotificationSubscription(sql.FieldEQ(FieldCreated, v)) +} + +// CreatedNEQ applies the NEQ predicate on the "created" field. +func CreatedNEQ(v time.Time) predicate.NotificationSubscription { + return predicate.NotificationSubscription(sql.FieldNEQ(FieldCreated, v)) +} + +// CreatedIn applies the In predicate on the "created" field. +func CreatedIn(vs ...time.Time) predicate.NotificationSubscription { + return predicate.NotificationSubscription(sql.FieldIn(FieldCreated, vs...)) +} + +// CreatedNotIn applies the NotIn predicate on the "created" field. +func CreatedNotIn(vs ...time.Time) predicate.NotificationSubscription { + return predicate.NotificationSubscription(sql.FieldNotIn(FieldCreated, vs...)) +} + +// CreatedGT applies the GT predicate on the "created" field. +func CreatedGT(v time.Time) predicate.NotificationSubscription { + return predicate.NotificationSubscription(sql.FieldGT(FieldCreated, v)) +} + +// CreatedGTE applies the GTE predicate on the "created" field. +func CreatedGTE(v time.Time) predicate.NotificationSubscription { + return predicate.NotificationSubscription(sql.FieldGTE(FieldCreated, v)) +} + +// CreatedLT applies the LT predicate on the "created" field. +func CreatedLT(v time.Time) predicate.NotificationSubscription { + return predicate.NotificationSubscription(sql.FieldLT(FieldCreated, v)) +} + +// CreatedLTE applies the LTE predicate on the "created" field. +func CreatedLTE(v time.Time) predicate.NotificationSubscription { + return predicate.NotificationSubscription(sql.FieldLTE(FieldCreated, v)) +} + +// And groups predicates with the AND operator between them. +func And(predicates ...predicate.NotificationSubscription) predicate.NotificationSubscription { + return predicate.NotificationSubscription(sql.AndPredicates(predicates...)) +} + +// Or groups predicates with the OR operator between them. +func Or(predicates ...predicate.NotificationSubscription) predicate.NotificationSubscription { + return predicate.NotificationSubscription(sql.OrPredicates(predicates...)) +} + +// Not applies the not operator on the given predicate. +func Not(p predicate.NotificationSubscription) predicate.NotificationSubscription { + return predicate.NotificationSubscription(sql.NotPredicates(p)) +} diff --git a/pkg/ent/notificationsubscription_create.go b/pkg/ent/notificationsubscription_create.go new file mode 100644 index 000000000..b7b0dcbec --- /dev/null +++ b/pkg/ent/notificationsubscription_create.go @@ -0,0 +1,922 @@ +// Code generated by ent, DO NOT EDIT. + +package ent + +import ( + "context" + "errors" + "fmt" + "time" + + "entgo.io/ent/dialect" + "entgo.io/ent/dialect/sql" + "entgo.io/ent/dialect/sql/sqlgraph" + "entgo.io/ent/schema/field" + "github.com/GoogleCloudPlatform/scion/pkg/ent/notificationsubscription" + "github.com/google/uuid" +) + +// NotificationSubscriptionCreate is the builder for creating a NotificationSubscription entity. +type NotificationSubscriptionCreate struct { + config + mutation *NotificationSubscriptionMutation + hooks []Hook + conflict []sql.ConflictOption +} + +// SetScope sets the "scope" field. +func (_c *NotificationSubscriptionCreate) SetScope(v string) *NotificationSubscriptionCreate { + _c.mutation.SetScope(v) + return _c +} + +// SetNillableScope sets the "scope" field if the given value is not nil. +func (_c *NotificationSubscriptionCreate) SetNillableScope(v *string) *NotificationSubscriptionCreate { + if v != nil { + _c.SetScope(*v) + } + return _c +} + +// SetAgentID sets the "agent_id" field. +func (_c *NotificationSubscriptionCreate) SetAgentID(v uuid.UUID) *NotificationSubscriptionCreate { + _c.mutation.SetAgentID(v) + return _c +} + +// SetNillableAgentID sets the "agent_id" field if the given value is not nil. +func (_c *NotificationSubscriptionCreate) SetNillableAgentID(v *uuid.UUID) *NotificationSubscriptionCreate { + if v != nil { + _c.SetAgentID(*v) + } + return _c +} + +// SetSubscriberType sets the "subscriber_type" field. +func (_c *NotificationSubscriptionCreate) SetSubscriberType(v string) *NotificationSubscriptionCreate { + _c.mutation.SetSubscriberType(v) + return _c +} + +// SetNillableSubscriberType sets the "subscriber_type" field if the given value is not nil. +func (_c *NotificationSubscriptionCreate) SetNillableSubscriberType(v *string) *NotificationSubscriptionCreate { + if v != nil { + _c.SetSubscriberType(*v) + } + return _c +} + +// SetSubscriberID sets the "subscriber_id" field. +func (_c *NotificationSubscriptionCreate) SetSubscriberID(v string) *NotificationSubscriptionCreate { + _c.mutation.SetSubscriberID(v) + return _c +} + +// SetProjectID sets the "project_id" field. +func (_c *NotificationSubscriptionCreate) SetProjectID(v uuid.UUID) *NotificationSubscriptionCreate { + _c.mutation.SetProjectID(v) + return _c +} + +// SetTriggerActivities sets the "trigger_activities" field. +func (_c *NotificationSubscriptionCreate) SetTriggerActivities(v string) *NotificationSubscriptionCreate { + _c.mutation.SetTriggerActivities(v) + return _c +} + +// SetCreatedBy sets the "created_by" field. +func (_c *NotificationSubscriptionCreate) SetCreatedBy(v string) *NotificationSubscriptionCreate { + _c.mutation.SetCreatedBy(v) + return _c +} + +// SetCreated sets the "created" field. +func (_c *NotificationSubscriptionCreate) SetCreated(v time.Time) *NotificationSubscriptionCreate { + _c.mutation.SetCreated(v) + return _c +} + +// SetNillableCreated sets the "created" field if the given value is not nil. +func (_c *NotificationSubscriptionCreate) SetNillableCreated(v *time.Time) *NotificationSubscriptionCreate { + if v != nil { + _c.SetCreated(*v) + } + return _c +} + +// SetID sets the "id" field. +func (_c *NotificationSubscriptionCreate) SetID(v uuid.UUID) *NotificationSubscriptionCreate { + _c.mutation.SetID(v) + return _c +} + +// SetNillableID sets the "id" field if the given value is not nil. +func (_c *NotificationSubscriptionCreate) SetNillableID(v *uuid.UUID) *NotificationSubscriptionCreate { + if v != nil { + _c.SetID(*v) + } + return _c +} + +// Mutation returns the NotificationSubscriptionMutation object of the builder. +func (_c *NotificationSubscriptionCreate) Mutation() *NotificationSubscriptionMutation { + return _c.mutation +} + +// Save creates the NotificationSubscription in the database. +func (_c *NotificationSubscriptionCreate) Save(ctx context.Context) (*NotificationSubscription, error) { + _c.defaults() + return withHooks(ctx, _c.sqlSave, _c.mutation, _c.hooks) +} + +// SaveX calls Save and panics if Save returns an error. +func (_c *NotificationSubscriptionCreate) SaveX(ctx context.Context) *NotificationSubscription { + v, err := _c.Save(ctx) + if err != nil { + panic(err) + } + return v +} + +// Exec executes the query. +func (_c *NotificationSubscriptionCreate) Exec(ctx context.Context) error { + _, err := _c.Save(ctx) + return err +} + +// ExecX is like Exec, but panics if an error occurs. +func (_c *NotificationSubscriptionCreate) ExecX(ctx context.Context) { + if err := _c.Exec(ctx); err != nil { + panic(err) + } +} + +// defaults sets the default values of the builder before save. +func (_c *NotificationSubscriptionCreate) defaults() { + if _, ok := _c.mutation.Scope(); !ok { + v := notificationsubscription.DefaultScope + _c.mutation.SetScope(v) + } + if _, ok := _c.mutation.SubscriberType(); !ok { + v := notificationsubscription.DefaultSubscriberType + _c.mutation.SetSubscriberType(v) + } + if _, ok := _c.mutation.Created(); !ok { + v := notificationsubscription.DefaultCreated() + _c.mutation.SetCreated(v) + } + if _, ok := _c.mutation.ID(); !ok { + v := notificationsubscription.DefaultID() + _c.mutation.SetID(v) + } +} + +// check runs all checks and user-defined validators on the builder. +func (_c *NotificationSubscriptionCreate) check() error { + if _, ok := _c.mutation.Scope(); !ok { + return &ValidationError{Name: "scope", err: errors.New(`ent: missing required field "NotificationSubscription.scope"`)} + } + if _, ok := _c.mutation.SubscriberType(); !ok { + return &ValidationError{Name: "subscriber_type", err: errors.New(`ent: missing required field "NotificationSubscription.subscriber_type"`)} + } + if _, ok := _c.mutation.SubscriberID(); !ok { + return &ValidationError{Name: "subscriber_id", err: errors.New(`ent: missing required field "NotificationSubscription.subscriber_id"`)} + } + if v, ok := _c.mutation.SubscriberID(); ok { + if err := notificationsubscription.SubscriberIDValidator(v); err != nil { + return &ValidationError{Name: "subscriber_id", err: fmt.Errorf(`ent: validator failed for field "NotificationSubscription.subscriber_id": %w`, err)} + } + } + if _, ok := _c.mutation.ProjectID(); !ok { + return &ValidationError{Name: "project_id", err: errors.New(`ent: missing required field "NotificationSubscription.project_id"`)} + } + if _, ok := _c.mutation.TriggerActivities(); !ok { + return &ValidationError{Name: "trigger_activities", err: errors.New(`ent: missing required field "NotificationSubscription.trigger_activities"`)} + } + if v, ok := _c.mutation.TriggerActivities(); ok { + if err := notificationsubscription.TriggerActivitiesValidator(v); err != nil { + return &ValidationError{Name: "trigger_activities", err: fmt.Errorf(`ent: validator failed for field "NotificationSubscription.trigger_activities": %w`, err)} + } + } + if _, ok := _c.mutation.CreatedBy(); !ok { + return &ValidationError{Name: "created_by", err: errors.New(`ent: missing required field "NotificationSubscription.created_by"`)} + } + if v, ok := _c.mutation.CreatedBy(); ok { + if err := notificationsubscription.CreatedByValidator(v); err != nil { + return &ValidationError{Name: "created_by", err: fmt.Errorf(`ent: validator failed for field "NotificationSubscription.created_by": %w`, err)} + } + } + if _, ok := _c.mutation.Created(); !ok { + return &ValidationError{Name: "created", err: errors.New(`ent: missing required field "NotificationSubscription.created"`)} + } + return nil +} + +func (_c *NotificationSubscriptionCreate) sqlSave(ctx context.Context) (*NotificationSubscription, error) { + if err := _c.check(); err != nil { + return nil, err + } + _node, _spec := _c.createSpec() + if err := sqlgraph.CreateNode(ctx, _c.driver, _spec); err != nil { + if sqlgraph.IsConstraintError(err) { + err = &ConstraintError{msg: err.Error(), wrap: err} + } + return nil, err + } + if _spec.ID.Value != nil { + if id, ok := _spec.ID.Value.(*uuid.UUID); ok { + _node.ID = *id + } else if err := _node.ID.Scan(_spec.ID.Value); err != nil { + return nil, err + } + } + _c.mutation.id = &_node.ID + _c.mutation.done = true + return _node, nil +} + +func (_c *NotificationSubscriptionCreate) createSpec() (*NotificationSubscription, *sqlgraph.CreateSpec) { + var ( + _node = &NotificationSubscription{config: _c.config} + _spec = sqlgraph.NewCreateSpec(notificationsubscription.Table, sqlgraph.NewFieldSpec(notificationsubscription.FieldID, field.TypeUUID)) + ) + _spec.OnConflict = _c.conflict + if id, ok := _c.mutation.ID(); ok { + _node.ID = id + _spec.ID.Value = &id + } + if value, ok := _c.mutation.Scope(); ok { + _spec.SetField(notificationsubscription.FieldScope, field.TypeString, value) + _node.Scope = value + } + if value, ok := _c.mutation.AgentID(); ok { + _spec.SetField(notificationsubscription.FieldAgentID, field.TypeUUID, value) + _node.AgentID = &value + } + if value, ok := _c.mutation.SubscriberType(); ok { + _spec.SetField(notificationsubscription.FieldSubscriberType, field.TypeString, value) + _node.SubscriberType = value + } + if value, ok := _c.mutation.SubscriberID(); ok { + _spec.SetField(notificationsubscription.FieldSubscriberID, field.TypeString, value) + _node.SubscriberID = value + } + if value, ok := _c.mutation.ProjectID(); ok { + _spec.SetField(notificationsubscription.FieldProjectID, field.TypeUUID, value) + _node.ProjectID = value + } + if value, ok := _c.mutation.TriggerActivities(); ok { + _spec.SetField(notificationsubscription.FieldTriggerActivities, field.TypeString, value) + _node.TriggerActivities = value + } + if value, ok := _c.mutation.CreatedBy(); ok { + _spec.SetField(notificationsubscription.FieldCreatedBy, field.TypeString, value) + _node.CreatedBy = value + } + if value, ok := _c.mutation.Created(); ok { + _spec.SetField(notificationsubscription.FieldCreated, field.TypeTime, value) + _node.Created = value + } + return _node, _spec +} + +// OnConflict allows configuring the `ON CONFLICT` / `ON DUPLICATE KEY` clause +// of the `INSERT` statement. For example: +// +// client.NotificationSubscription.Create(). +// SetScope(v). +// OnConflict( +// // Update the row with the new values +// // the was proposed for insertion. +// sql.ResolveWithNewValues(), +// ). +// // Override some of the fields with custom +// // update values. +// Update(func(u *ent.NotificationSubscriptionUpsert) { +// SetScope(v+v). +// }). +// Exec(ctx) +func (_c *NotificationSubscriptionCreate) OnConflict(opts ...sql.ConflictOption) *NotificationSubscriptionUpsertOne { + _c.conflict = opts + return &NotificationSubscriptionUpsertOne{ + create: _c, + } +} + +// OnConflictColumns calls `OnConflict` and configures the columns +// as conflict target. Using this option is equivalent to using: +// +// client.NotificationSubscription.Create(). +// OnConflict(sql.ConflictColumns(columns...)). +// Exec(ctx) +func (_c *NotificationSubscriptionCreate) OnConflictColumns(columns ...string) *NotificationSubscriptionUpsertOne { + _c.conflict = append(_c.conflict, sql.ConflictColumns(columns...)) + return &NotificationSubscriptionUpsertOne{ + create: _c, + } +} + +type ( + // NotificationSubscriptionUpsertOne is the builder for "upsert"-ing + // one NotificationSubscription node. + NotificationSubscriptionUpsertOne struct { + create *NotificationSubscriptionCreate + } + + // NotificationSubscriptionUpsert is the "OnConflict" setter. + NotificationSubscriptionUpsert struct { + *sql.UpdateSet + } +) + +// SetScope sets the "scope" field. +func (u *NotificationSubscriptionUpsert) SetScope(v string) *NotificationSubscriptionUpsert { + u.Set(notificationsubscription.FieldScope, v) + return u +} + +// UpdateScope sets the "scope" field to the value that was provided on create. +func (u *NotificationSubscriptionUpsert) UpdateScope() *NotificationSubscriptionUpsert { + u.SetExcluded(notificationsubscription.FieldScope) + return u +} + +// SetAgentID sets the "agent_id" field. +func (u *NotificationSubscriptionUpsert) SetAgentID(v uuid.UUID) *NotificationSubscriptionUpsert { + u.Set(notificationsubscription.FieldAgentID, v) + return u +} + +// UpdateAgentID sets the "agent_id" field to the value that was provided on create. +func (u *NotificationSubscriptionUpsert) UpdateAgentID() *NotificationSubscriptionUpsert { + u.SetExcluded(notificationsubscription.FieldAgentID) + return u +} + +// ClearAgentID clears the value of the "agent_id" field. +func (u *NotificationSubscriptionUpsert) ClearAgentID() *NotificationSubscriptionUpsert { + u.SetNull(notificationsubscription.FieldAgentID) + return u +} + +// SetSubscriberType sets the "subscriber_type" field. +func (u *NotificationSubscriptionUpsert) SetSubscriberType(v string) *NotificationSubscriptionUpsert { + u.Set(notificationsubscription.FieldSubscriberType, v) + return u +} + +// UpdateSubscriberType sets the "subscriber_type" field to the value that was provided on create. +func (u *NotificationSubscriptionUpsert) UpdateSubscriberType() *NotificationSubscriptionUpsert { + u.SetExcluded(notificationsubscription.FieldSubscriberType) + return u +} + +// SetSubscriberID sets the "subscriber_id" field. +func (u *NotificationSubscriptionUpsert) SetSubscriberID(v string) *NotificationSubscriptionUpsert { + u.Set(notificationsubscription.FieldSubscriberID, v) + return u +} + +// UpdateSubscriberID sets the "subscriber_id" field to the value that was provided on create. +func (u *NotificationSubscriptionUpsert) UpdateSubscriberID() *NotificationSubscriptionUpsert { + u.SetExcluded(notificationsubscription.FieldSubscriberID) + return u +} + +// SetProjectID sets the "project_id" field. +func (u *NotificationSubscriptionUpsert) SetProjectID(v uuid.UUID) *NotificationSubscriptionUpsert { + u.Set(notificationsubscription.FieldProjectID, v) + return u +} + +// UpdateProjectID sets the "project_id" field to the value that was provided on create. +func (u *NotificationSubscriptionUpsert) UpdateProjectID() *NotificationSubscriptionUpsert { + u.SetExcluded(notificationsubscription.FieldProjectID) + return u +} + +// SetTriggerActivities sets the "trigger_activities" field. +func (u *NotificationSubscriptionUpsert) SetTriggerActivities(v string) *NotificationSubscriptionUpsert { + u.Set(notificationsubscription.FieldTriggerActivities, v) + return u +} + +// UpdateTriggerActivities sets the "trigger_activities" field to the value that was provided on create. +func (u *NotificationSubscriptionUpsert) UpdateTriggerActivities() *NotificationSubscriptionUpsert { + u.SetExcluded(notificationsubscription.FieldTriggerActivities) + return u +} + +// SetCreatedBy sets the "created_by" field. +func (u *NotificationSubscriptionUpsert) SetCreatedBy(v string) *NotificationSubscriptionUpsert { + u.Set(notificationsubscription.FieldCreatedBy, v) + return u +} + +// UpdateCreatedBy sets the "created_by" field to the value that was provided on create. +func (u *NotificationSubscriptionUpsert) UpdateCreatedBy() *NotificationSubscriptionUpsert { + u.SetExcluded(notificationsubscription.FieldCreatedBy) + return u +} + +// UpdateNewValues updates the mutable fields using the new values that were set on create except the ID field. +// Using this option is equivalent to using: +// +// client.NotificationSubscription.Create(). +// OnConflict( +// sql.ResolveWithNewValues(), +// sql.ResolveWith(func(u *sql.UpdateSet) { +// u.SetIgnore(notificationsubscription.FieldID) +// }), +// ). +// Exec(ctx) +func (u *NotificationSubscriptionUpsertOne) UpdateNewValues() *NotificationSubscriptionUpsertOne { + u.create.conflict = append(u.create.conflict, sql.ResolveWithNewValues()) + u.create.conflict = append(u.create.conflict, sql.ResolveWith(func(s *sql.UpdateSet) { + if _, exists := u.create.mutation.ID(); exists { + s.SetIgnore(notificationsubscription.FieldID) + } + if _, exists := u.create.mutation.Created(); exists { + s.SetIgnore(notificationsubscription.FieldCreated) + } + })) + return u +} + +// Ignore sets each column to itself in case of conflict. +// Using this option is equivalent to using: +// +// client.NotificationSubscription.Create(). +// OnConflict(sql.ResolveWithIgnore()). +// Exec(ctx) +func (u *NotificationSubscriptionUpsertOne) Ignore() *NotificationSubscriptionUpsertOne { + u.create.conflict = append(u.create.conflict, sql.ResolveWithIgnore()) + return u +} + +// DoNothing configures the conflict_action to `DO NOTHING`. +// Supported only by SQLite and PostgreSQL. +func (u *NotificationSubscriptionUpsertOne) DoNothing() *NotificationSubscriptionUpsertOne { + u.create.conflict = append(u.create.conflict, sql.DoNothing()) + return u +} + +// Update allows overriding fields `UPDATE` values. See the NotificationSubscriptionCreate.OnConflict +// documentation for more info. +func (u *NotificationSubscriptionUpsertOne) Update(set func(*NotificationSubscriptionUpsert)) *NotificationSubscriptionUpsertOne { + u.create.conflict = append(u.create.conflict, sql.ResolveWith(func(update *sql.UpdateSet) { + set(&NotificationSubscriptionUpsert{UpdateSet: update}) + })) + return u +} + +// SetScope sets the "scope" field. +func (u *NotificationSubscriptionUpsertOne) SetScope(v string) *NotificationSubscriptionUpsertOne { + return u.Update(func(s *NotificationSubscriptionUpsert) { + s.SetScope(v) + }) +} + +// UpdateScope sets the "scope" field to the value that was provided on create. +func (u *NotificationSubscriptionUpsertOne) UpdateScope() *NotificationSubscriptionUpsertOne { + return u.Update(func(s *NotificationSubscriptionUpsert) { + s.UpdateScope() + }) +} + +// SetAgentID sets the "agent_id" field. +func (u *NotificationSubscriptionUpsertOne) SetAgentID(v uuid.UUID) *NotificationSubscriptionUpsertOne { + return u.Update(func(s *NotificationSubscriptionUpsert) { + s.SetAgentID(v) + }) +} + +// UpdateAgentID sets the "agent_id" field to the value that was provided on create. +func (u *NotificationSubscriptionUpsertOne) UpdateAgentID() *NotificationSubscriptionUpsertOne { + return u.Update(func(s *NotificationSubscriptionUpsert) { + s.UpdateAgentID() + }) +} + +// ClearAgentID clears the value of the "agent_id" field. +func (u *NotificationSubscriptionUpsertOne) ClearAgentID() *NotificationSubscriptionUpsertOne { + return u.Update(func(s *NotificationSubscriptionUpsert) { + s.ClearAgentID() + }) +} + +// SetSubscriberType sets the "subscriber_type" field. +func (u *NotificationSubscriptionUpsertOne) SetSubscriberType(v string) *NotificationSubscriptionUpsertOne { + return u.Update(func(s *NotificationSubscriptionUpsert) { + s.SetSubscriberType(v) + }) +} + +// UpdateSubscriberType sets the "subscriber_type" field to the value that was provided on create. +func (u *NotificationSubscriptionUpsertOne) UpdateSubscriberType() *NotificationSubscriptionUpsertOne { + return u.Update(func(s *NotificationSubscriptionUpsert) { + s.UpdateSubscriberType() + }) +} + +// SetSubscriberID sets the "subscriber_id" field. +func (u *NotificationSubscriptionUpsertOne) SetSubscriberID(v string) *NotificationSubscriptionUpsertOne { + return u.Update(func(s *NotificationSubscriptionUpsert) { + s.SetSubscriberID(v) + }) +} + +// UpdateSubscriberID sets the "subscriber_id" field to the value that was provided on create. +func (u *NotificationSubscriptionUpsertOne) UpdateSubscriberID() *NotificationSubscriptionUpsertOne { + return u.Update(func(s *NotificationSubscriptionUpsert) { + s.UpdateSubscriberID() + }) +} + +// SetProjectID sets the "project_id" field. +func (u *NotificationSubscriptionUpsertOne) SetProjectID(v uuid.UUID) *NotificationSubscriptionUpsertOne { + return u.Update(func(s *NotificationSubscriptionUpsert) { + s.SetProjectID(v) + }) +} + +// UpdateProjectID sets the "project_id" field to the value that was provided on create. +func (u *NotificationSubscriptionUpsertOne) UpdateProjectID() *NotificationSubscriptionUpsertOne { + return u.Update(func(s *NotificationSubscriptionUpsert) { + s.UpdateProjectID() + }) +} + +// SetTriggerActivities sets the "trigger_activities" field. +func (u *NotificationSubscriptionUpsertOne) SetTriggerActivities(v string) *NotificationSubscriptionUpsertOne { + return u.Update(func(s *NotificationSubscriptionUpsert) { + s.SetTriggerActivities(v) + }) +} + +// UpdateTriggerActivities sets the "trigger_activities" field to the value that was provided on create. +func (u *NotificationSubscriptionUpsertOne) UpdateTriggerActivities() *NotificationSubscriptionUpsertOne { + return u.Update(func(s *NotificationSubscriptionUpsert) { + s.UpdateTriggerActivities() + }) +} + +// SetCreatedBy sets the "created_by" field. +func (u *NotificationSubscriptionUpsertOne) SetCreatedBy(v string) *NotificationSubscriptionUpsertOne { + return u.Update(func(s *NotificationSubscriptionUpsert) { + s.SetCreatedBy(v) + }) +} + +// UpdateCreatedBy sets the "created_by" field to the value that was provided on create. +func (u *NotificationSubscriptionUpsertOne) UpdateCreatedBy() *NotificationSubscriptionUpsertOne { + return u.Update(func(s *NotificationSubscriptionUpsert) { + s.UpdateCreatedBy() + }) +} + +// Exec executes the query. +func (u *NotificationSubscriptionUpsertOne) Exec(ctx context.Context) error { + if len(u.create.conflict) == 0 { + return errors.New("ent: missing options for NotificationSubscriptionCreate.OnConflict") + } + return u.create.Exec(ctx) +} + +// ExecX is like Exec, but panics if an error occurs. +func (u *NotificationSubscriptionUpsertOne) ExecX(ctx context.Context) { + if err := u.create.Exec(ctx); err != nil { + panic(err) + } +} + +// Exec executes the UPSERT query and returns the inserted/updated ID. +func (u *NotificationSubscriptionUpsertOne) ID(ctx context.Context) (id uuid.UUID, err error) { + if u.create.driver.Dialect() == dialect.MySQL { + // In case of "ON CONFLICT", there is no way to get back non-numeric ID + // fields from the database since MySQL does not support the RETURNING clause. + return id, errors.New("ent: NotificationSubscriptionUpsertOne.ID is not supported by MySQL driver. Use NotificationSubscriptionUpsertOne.Exec instead") + } + node, err := u.create.Save(ctx) + if err != nil { + return id, err + } + return node.ID, nil +} + +// IDX is like ID, but panics if an error occurs. +func (u *NotificationSubscriptionUpsertOne) IDX(ctx context.Context) uuid.UUID { + id, err := u.ID(ctx) + if err != nil { + panic(err) + } + return id +} + +// NotificationSubscriptionCreateBulk is the builder for creating many NotificationSubscription entities in bulk. +type NotificationSubscriptionCreateBulk struct { + config + err error + builders []*NotificationSubscriptionCreate + conflict []sql.ConflictOption +} + +// Save creates the NotificationSubscription entities in the database. +func (_c *NotificationSubscriptionCreateBulk) Save(ctx context.Context) ([]*NotificationSubscription, error) { + if _c.err != nil { + return nil, _c.err + } + specs := make([]*sqlgraph.CreateSpec, len(_c.builders)) + nodes := make([]*NotificationSubscription, len(_c.builders)) + mutators := make([]Mutator, len(_c.builders)) + for i := range _c.builders { + func(i int, root context.Context) { + builder := _c.builders[i] + builder.defaults() + var mut Mutator = MutateFunc(func(ctx context.Context, m Mutation) (Value, error) { + mutation, ok := m.(*NotificationSubscriptionMutation) + if !ok { + return nil, fmt.Errorf("unexpected mutation type %T", m) + } + if err := builder.check(); err != nil { + return nil, err + } + builder.mutation = mutation + var err error + nodes[i], specs[i] = builder.createSpec() + if i < len(mutators)-1 { + _, err = mutators[i+1].Mutate(root, _c.builders[i+1].mutation) + } else { + spec := &sqlgraph.BatchCreateSpec{Nodes: specs} + spec.OnConflict = _c.conflict + // Invoke the actual operation on the latest mutation in the chain. + if err = sqlgraph.BatchCreate(ctx, _c.driver, spec); err != nil { + if sqlgraph.IsConstraintError(err) { + err = &ConstraintError{msg: err.Error(), wrap: err} + } + } + } + if err != nil { + return nil, err + } + mutation.id = &nodes[i].ID + mutation.done = true + return nodes[i], nil + }) + for i := len(builder.hooks) - 1; i >= 0; i-- { + mut = builder.hooks[i](mut) + } + mutators[i] = mut + }(i, ctx) + } + if len(mutators) > 0 { + if _, err := mutators[0].Mutate(ctx, _c.builders[0].mutation); err != nil { + return nil, err + } + } + return nodes, nil +} + +// SaveX is like Save, but panics if an error occurs. +func (_c *NotificationSubscriptionCreateBulk) SaveX(ctx context.Context) []*NotificationSubscription { + v, err := _c.Save(ctx) + if err != nil { + panic(err) + } + return v +} + +// Exec executes the query. +func (_c *NotificationSubscriptionCreateBulk) Exec(ctx context.Context) error { + _, err := _c.Save(ctx) + return err +} + +// ExecX is like Exec, but panics if an error occurs. +func (_c *NotificationSubscriptionCreateBulk) ExecX(ctx context.Context) { + if err := _c.Exec(ctx); err != nil { + panic(err) + } +} + +// OnConflict allows configuring the `ON CONFLICT` / `ON DUPLICATE KEY` clause +// of the `INSERT` statement. For example: +// +// client.NotificationSubscription.CreateBulk(builders...). +// OnConflict( +// // Update the row with the new values +// // the was proposed for insertion. +// sql.ResolveWithNewValues(), +// ). +// // Override some of the fields with custom +// // update values. +// Update(func(u *ent.NotificationSubscriptionUpsert) { +// SetScope(v+v). +// }). +// Exec(ctx) +func (_c *NotificationSubscriptionCreateBulk) OnConflict(opts ...sql.ConflictOption) *NotificationSubscriptionUpsertBulk { + _c.conflict = opts + return &NotificationSubscriptionUpsertBulk{ + create: _c, + } +} + +// OnConflictColumns calls `OnConflict` and configures the columns +// as conflict target. Using this option is equivalent to using: +// +// client.NotificationSubscription.Create(). +// OnConflict(sql.ConflictColumns(columns...)). +// Exec(ctx) +func (_c *NotificationSubscriptionCreateBulk) OnConflictColumns(columns ...string) *NotificationSubscriptionUpsertBulk { + _c.conflict = append(_c.conflict, sql.ConflictColumns(columns...)) + return &NotificationSubscriptionUpsertBulk{ + create: _c, + } +} + +// NotificationSubscriptionUpsertBulk is the builder for "upsert"-ing +// a bulk of NotificationSubscription nodes. +type NotificationSubscriptionUpsertBulk struct { + create *NotificationSubscriptionCreateBulk +} + +// UpdateNewValues updates the mutable fields using the new values that +// were set on create. Using this option is equivalent to using: +// +// client.NotificationSubscription.Create(). +// OnConflict( +// sql.ResolveWithNewValues(), +// sql.ResolveWith(func(u *sql.UpdateSet) { +// u.SetIgnore(notificationsubscription.FieldID) +// }), +// ). +// Exec(ctx) +func (u *NotificationSubscriptionUpsertBulk) UpdateNewValues() *NotificationSubscriptionUpsertBulk { + u.create.conflict = append(u.create.conflict, sql.ResolveWithNewValues()) + u.create.conflict = append(u.create.conflict, sql.ResolveWith(func(s *sql.UpdateSet) { + for _, b := range u.create.builders { + if _, exists := b.mutation.ID(); exists { + s.SetIgnore(notificationsubscription.FieldID) + } + if _, exists := b.mutation.Created(); exists { + s.SetIgnore(notificationsubscription.FieldCreated) + } + } + })) + return u +} + +// Ignore sets each column to itself in case of conflict. +// Using this option is equivalent to using: +// +// client.NotificationSubscription.Create(). +// OnConflict(sql.ResolveWithIgnore()). +// Exec(ctx) +func (u *NotificationSubscriptionUpsertBulk) Ignore() *NotificationSubscriptionUpsertBulk { + u.create.conflict = append(u.create.conflict, sql.ResolveWithIgnore()) + return u +} + +// DoNothing configures the conflict_action to `DO NOTHING`. +// Supported only by SQLite and PostgreSQL. +func (u *NotificationSubscriptionUpsertBulk) DoNothing() *NotificationSubscriptionUpsertBulk { + u.create.conflict = append(u.create.conflict, sql.DoNothing()) + return u +} + +// Update allows overriding fields `UPDATE` values. See the NotificationSubscriptionCreateBulk.OnConflict +// documentation for more info. +func (u *NotificationSubscriptionUpsertBulk) Update(set func(*NotificationSubscriptionUpsert)) *NotificationSubscriptionUpsertBulk { + u.create.conflict = append(u.create.conflict, sql.ResolveWith(func(update *sql.UpdateSet) { + set(&NotificationSubscriptionUpsert{UpdateSet: update}) + })) + return u +} + +// SetScope sets the "scope" field. +func (u *NotificationSubscriptionUpsertBulk) SetScope(v string) *NotificationSubscriptionUpsertBulk { + return u.Update(func(s *NotificationSubscriptionUpsert) { + s.SetScope(v) + }) +} + +// UpdateScope sets the "scope" field to the value that was provided on create. +func (u *NotificationSubscriptionUpsertBulk) UpdateScope() *NotificationSubscriptionUpsertBulk { + return u.Update(func(s *NotificationSubscriptionUpsert) { + s.UpdateScope() + }) +} + +// SetAgentID sets the "agent_id" field. +func (u *NotificationSubscriptionUpsertBulk) SetAgentID(v uuid.UUID) *NotificationSubscriptionUpsertBulk { + return u.Update(func(s *NotificationSubscriptionUpsert) { + s.SetAgentID(v) + }) +} + +// UpdateAgentID sets the "agent_id" field to the value that was provided on create. +func (u *NotificationSubscriptionUpsertBulk) UpdateAgentID() *NotificationSubscriptionUpsertBulk { + return u.Update(func(s *NotificationSubscriptionUpsert) { + s.UpdateAgentID() + }) +} + +// ClearAgentID clears the value of the "agent_id" field. +func (u *NotificationSubscriptionUpsertBulk) ClearAgentID() *NotificationSubscriptionUpsertBulk { + return u.Update(func(s *NotificationSubscriptionUpsert) { + s.ClearAgentID() + }) +} + +// SetSubscriberType sets the "subscriber_type" field. +func (u *NotificationSubscriptionUpsertBulk) SetSubscriberType(v string) *NotificationSubscriptionUpsertBulk { + return u.Update(func(s *NotificationSubscriptionUpsert) { + s.SetSubscriberType(v) + }) +} + +// UpdateSubscriberType sets the "subscriber_type" field to the value that was provided on create. +func (u *NotificationSubscriptionUpsertBulk) UpdateSubscriberType() *NotificationSubscriptionUpsertBulk { + return u.Update(func(s *NotificationSubscriptionUpsert) { + s.UpdateSubscriberType() + }) +} + +// SetSubscriberID sets the "subscriber_id" field. +func (u *NotificationSubscriptionUpsertBulk) SetSubscriberID(v string) *NotificationSubscriptionUpsertBulk { + return u.Update(func(s *NotificationSubscriptionUpsert) { + s.SetSubscriberID(v) + }) +} + +// UpdateSubscriberID sets the "subscriber_id" field to the value that was provided on create. +func (u *NotificationSubscriptionUpsertBulk) UpdateSubscriberID() *NotificationSubscriptionUpsertBulk { + return u.Update(func(s *NotificationSubscriptionUpsert) { + s.UpdateSubscriberID() + }) +} + +// SetProjectID sets the "project_id" field. +func (u *NotificationSubscriptionUpsertBulk) SetProjectID(v uuid.UUID) *NotificationSubscriptionUpsertBulk { + return u.Update(func(s *NotificationSubscriptionUpsert) { + s.SetProjectID(v) + }) +} + +// UpdateProjectID sets the "project_id" field to the value that was provided on create. +func (u *NotificationSubscriptionUpsertBulk) UpdateProjectID() *NotificationSubscriptionUpsertBulk { + return u.Update(func(s *NotificationSubscriptionUpsert) { + s.UpdateProjectID() + }) +} + +// SetTriggerActivities sets the "trigger_activities" field. +func (u *NotificationSubscriptionUpsertBulk) SetTriggerActivities(v string) *NotificationSubscriptionUpsertBulk { + return u.Update(func(s *NotificationSubscriptionUpsert) { + s.SetTriggerActivities(v) + }) +} + +// UpdateTriggerActivities sets the "trigger_activities" field to the value that was provided on create. +func (u *NotificationSubscriptionUpsertBulk) UpdateTriggerActivities() *NotificationSubscriptionUpsertBulk { + return u.Update(func(s *NotificationSubscriptionUpsert) { + s.UpdateTriggerActivities() + }) +} + +// SetCreatedBy sets the "created_by" field. +func (u *NotificationSubscriptionUpsertBulk) SetCreatedBy(v string) *NotificationSubscriptionUpsertBulk { + return u.Update(func(s *NotificationSubscriptionUpsert) { + s.SetCreatedBy(v) + }) +} + +// UpdateCreatedBy sets the "created_by" field to the value that was provided on create. +func (u *NotificationSubscriptionUpsertBulk) UpdateCreatedBy() *NotificationSubscriptionUpsertBulk { + return u.Update(func(s *NotificationSubscriptionUpsert) { + s.UpdateCreatedBy() + }) +} + +// Exec executes the query. +func (u *NotificationSubscriptionUpsertBulk) Exec(ctx context.Context) error { + if u.create.err != nil { + return u.create.err + } + for i, b := range u.create.builders { + if len(b.conflict) != 0 { + return fmt.Errorf("ent: OnConflict was set for builder %d. Set it on the NotificationSubscriptionCreateBulk instead", i) + } + } + if len(u.create.conflict) == 0 { + return errors.New("ent: missing options for NotificationSubscriptionCreateBulk.OnConflict") + } + return u.create.Exec(ctx) +} + +// ExecX is like Exec, but panics if an error occurs. +func (u *NotificationSubscriptionUpsertBulk) ExecX(ctx context.Context) { + if err := u.create.Exec(ctx); err != nil { + panic(err) + } +} diff --git a/pkg/ent/notificationsubscription_delete.go b/pkg/ent/notificationsubscription_delete.go new file mode 100644 index 000000000..98118d835 --- /dev/null +++ b/pkg/ent/notificationsubscription_delete.go @@ -0,0 +1,88 @@ +// Code generated by ent, DO NOT EDIT. + +package ent + +import ( + "context" + + "entgo.io/ent/dialect/sql" + "entgo.io/ent/dialect/sql/sqlgraph" + "entgo.io/ent/schema/field" + "github.com/GoogleCloudPlatform/scion/pkg/ent/notificationsubscription" + "github.com/GoogleCloudPlatform/scion/pkg/ent/predicate" +) + +// NotificationSubscriptionDelete is the builder for deleting a NotificationSubscription entity. +type NotificationSubscriptionDelete struct { + config + hooks []Hook + mutation *NotificationSubscriptionMutation +} + +// Where appends a list predicates to the NotificationSubscriptionDelete builder. +func (_d *NotificationSubscriptionDelete) Where(ps ...predicate.NotificationSubscription) *NotificationSubscriptionDelete { + _d.mutation.Where(ps...) + return _d +} + +// Exec executes the deletion query and returns how many vertices were deleted. +func (_d *NotificationSubscriptionDelete) Exec(ctx context.Context) (int, error) { + return withHooks(ctx, _d.sqlExec, _d.mutation, _d.hooks) +} + +// ExecX is like Exec, but panics if an error occurs. +func (_d *NotificationSubscriptionDelete) ExecX(ctx context.Context) int { + n, err := _d.Exec(ctx) + if err != nil { + panic(err) + } + return n +} + +func (_d *NotificationSubscriptionDelete) sqlExec(ctx context.Context) (int, error) { + _spec := sqlgraph.NewDeleteSpec(notificationsubscription.Table, sqlgraph.NewFieldSpec(notificationsubscription.FieldID, field.TypeUUID)) + if ps := _d.mutation.predicates; len(ps) > 0 { + _spec.Predicate = func(selector *sql.Selector) { + for i := range ps { + ps[i](selector) + } + } + } + affected, err := sqlgraph.DeleteNodes(ctx, _d.driver, _spec) + if err != nil && sqlgraph.IsConstraintError(err) { + err = &ConstraintError{msg: err.Error(), wrap: err} + } + _d.mutation.done = true + return affected, err +} + +// NotificationSubscriptionDeleteOne is the builder for deleting a single NotificationSubscription entity. +type NotificationSubscriptionDeleteOne struct { + _d *NotificationSubscriptionDelete +} + +// Where appends a list predicates to the NotificationSubscriptionDelete builder. +func (_d *NotificationSubscriptionDeleteOne) Where(ps ...predicate.NotificationSubscription) *NotificationSubscriptionDeleteOne { + _d._d.mutation.Where(ps...) + return _d +} + +// Exec executes the deletion query. +func (_d *NotificationSubscriptionDeleteOne) Exec(ctx context.Context) error { + n, err := _d._d.Exec(ctx) + switch { + case err != nil: + return err + case n == 0: + return &NotFoundError{notificationsubscription.Label} + default: + return nil + } +} + +// ExecX is like Exec, but panics if an error occurs. +func (_d *NotificationSubscriptionDeleteOne) ExecX(ctx context.Context) { + if err := _d.Exec(ctx); err != nil { + panic(err) + } +} diff --git a/pkg/ent/notificationsubscription_query.go b/pkg/ent/notificationsubscription_query.go new file mode 100644 index 000000000..0122d0b9d --- /dev/null +++ b/pkg/ent/notificationsubscription_query.go @@ -0,0 +1,565 @@ +// Code generated by ent, DO NOT EDIT. + +package ent + +import ( + "context" + "fmt" + "math" + + "entgo.io/ent" + "entgo.io/ent/dialect" + "entgo.io/ent/dialect/sql" + "entgo.io/ent/dialect/sql/sqlgraph" + "entgo.io/ent/schema/field" + "github.com/GoogleCloudPlatform/scion/pkg/ent/notificationsubscription" + "github.com/GoogleCloudPlatform/scion/pkg/ent/predicate" + "github.com/google/uuid" +) + +// NotificationSubscriptionQuery is the builder for querying NotificationSubscription entities. +type NotificationSubscriptionQuery struct { + config + ctx *QueryContext + order []notificationsubscription.OrderOption + inters []Interceptor + predicates []predicate.NotificationSubscription + modifiers []func(*sql.Selector) + // intermediate query (i.e. traversal path). + sql *sql.Selector + path func(context.Context) (*sql.Selector, error) +} + +// Where adds a new predicate for the NotificationSubscriptionQuery builder. +func (_q *NotificationSubscriptionQuery) Where(ps ...predicate.NotificationSubscription) *NotificationSubscriptionQuery { + _q.predicates = append(_q.predicates, ps...) + return _q +} + +// Limit the number of records to be returned by this query. +func (_q *NotificationSubscriptionQuery) Limit(limit int) *NotificationSubscriptionQuery { + _q.ctx.Limit = &limit + return _q +} + +// Offset to start from. +func (_q *NotificationSubscriptionQuery) Offset(offset int) *NotificationSubscriptionQuery { + _q.ctx.Offset = &offset + return _q +} + +// Unique configures the query builder to filter duplicate records on query. +// By default, unique is set to true, and can be disabled using this method. +func (_q *NotificationSubscriptionQuery) Unique(unique bool) *NotificationSubscriptionQuery { + _q.ctx.Unique = &unique + return _q +} + +// Order specifies how the records should be ordered. +func (_q *NotificationSubscriptionQuery) Order(o ...notificationsubscription.OrderOption) *NotificationSubscriptionQuery { + _q.order = append(_q.order, o...) + return _q +} + +// First returns the first NotificationSubscription entity from the query. +// Returns a *NotFoundError when no NotificationSubscription was found. +func (_q *NotificationSubscriptionQuery) First(ctx context.Context) (*NotificationSubscription, error) { + nodes, err := _q.Limit(1).All(setContextOp(ctx, _q.ctx, ent.OpQueryFirst)) + if err != nil { + return nil, err + } + if len(nodes) == 0 { + return nil, &NotFoundError{notificationsubscription.Label} + } + return nodes[0], nil +} + +// FirstX is like First, but panics if an error occurs. +func (_q *NotificationSubscriptionQuery) FirstX(ctx context.Context) *NotificationSubscription { + node, err := _q.First(ctx) + if err != nil && !IsNotFound(err) { + panic(err) + } + return node +} + +// FirstID returns the first NotificationSubscription ID from the query. +// Returns a *NotFoundError when no NotificationSubscription ID was found. +func (_q *NotificationSubscriptionQuery) FirstID(ctx context.Context) (id uuid.UUID, err error) { + var ids []uuid.UUID + if ids, err = _q.Limit(1).IDs(setContextOp(ctx, _q.ctx, ent.OpQueryFirstID)); err != nil { + return + } + if len(ids) == 0 { + err = &NotFoundError{notificationsubscription.Label} + return + } + return ids[0], nil +} + +// FirstIDX is like FirstID, but panics if an error occurs. +func (_q *NotificationSubscriptionQuery) FirstIDX(ctx context.Context) uuid.UUID { + id, err := _q.FirstID(ctx) + if err != nil && !IsNotFound(err) { + panic(err) + } + return id +} + +// Only returns a single NotificationSubscription entity found by the query, ensuring it only returns one. +// Returns a *NotSingularError when more than one NotificationSubscription entity is found. +// Returns a *NotFoundError when no NotificationSubscription entities are found. +func (_q *NotificationSubscriptionQuery) Only(ctx context.Context) (*NotificationSubscription, error) { + nodes, err := _q.Limit(2).All(setContextOp(ctx, _q.ctx, ent.OpQueryOnly)) + if err != nil { + return nil, err + } + switch len(nodes) { + case 1: + return nodes[0], nil + case 0: + return nil, &NotFoundError{notificationsubscription.Label} + default: + return nil, &NotSingularError{notificationsubscription.Label} + } +} + +// OnlyX is like Only, but panics if an error occurs. +func (_q *NotificationSubscriptionQuery) OnlyX(ctx context.Context) *NotificationSubscription { + node, err := _q.Only(ctx) + if err != nil { + panic(err) + } + return node +} + +// OnlyID is like Only, but returns the only NotificationSubscription ID in the query. +// Returns a *NotSingularError when more than one NotificationSubscription ID is found. +// Returns a *NotFoundError when no entities are found. +func (_q *NotificationSubscriptionQuery) OnlyID(ctx context.Context) (id uuid.UUID, err error) { + var ids []uuid.UUID + if ids, err = _q.Limit(2).IDs(setContextOp(ctx, _q.ctx, ent.OpQueryOnlyID)); err != nil { + return + } + switch len(ids) { + case 1: + id = ids[0] + case 0: + err = &NotFoundError{notificationsubscription.Label} + default: + err = &NotSingularError{notificationsubscription.Label} + } + return +} + +// OnlyIDX is like OnlyID, but panics if an error occurs. +func (_q *NotificationSubscriptionQuery) OnlyIDX(ctx context.Context) uuid.UUID { + id, err := _q.OnlyID(ctx) + if err != nil { + panic(err) + } + return id +} + +// All executes the query and returns a list of NotificationSubscriptions. +func (_q *NotificationSubscriptionQuery) All(ctx context.Context) ([]*NotificationSubscription, error) { + ctx = setContextOp(ctx, _q.ctx, ent.OpQueryAll) + if err := _q.prepareQuery(ctx); err != nil { + return nil, err + } + qr := querierAll[[]*NotificationSubscription, *NotificationSubscriptionQuery]() + return withInterceptors[[]*NotificationSubscription](ctx, _q, qr, _q.inters) +} + +// AllX is like All, but panics if an error occurs. +func (_q *NotificationSubscriptionQuery) AllX(ctx context.Context) []*NotificationSubscription { + nodes, err := _q.All(ctx) + if err != nil { + panic(err) + } + return nodes +} + +// IDs executes the query and returns a list of NotificationSubscription IDs. +func (_q *NotificationSubscriptionQuery) IDs(ctx context.Context) (ids []uuid.UUID, err error) { + if _q.ctx.Unique == nil && _q.path != nil { + _q.Unique(true) + } + ctx = setContextOp(ctx, _q.ctx, ent.OpQueryIDs) + if err = _q.Select(notificationsubscription.FieldID).Scan(ctx, &ids); err != nil { + return nil, err + } + return ids, nil +} + +// IDsX is like IDs, but panics if an error occurs. +func (_q *NotificationSubscriptionQuery) IDsX(ctx context.Context) []uuid.UUID { + ids, err := _q.IDs(ctx) + if err != nil { + panic(err) + } + return ids +} + +// Count returns the count of the given query. +func (_q *NotificationSubscriptionQuery) Count(ctx context.Context) (int, error) { + ctx = setContextOp(ctx, _q.ctx, ent.OpQueryCount) + if err := _q.prepareQuery(ctx); err != nil { + return 0, err + } + return withInterceptors[int](ctx, _q, querierCount[*NotificationSubscriptionQuery](), _q.inters) +} + +// CountX is like Count, but panics if an error occurs. +func (_q *NotificationSubscriptionQuery) CountX(ctx context.Context) int { + count, err := _q.Count(ctx) + if err != nil { + panic(err) + } + return count +} + +// Exist returns true if the query has elements in the graph. +func (_q *NotificationSubscriptionQuery) Exist(ctx context.Context) (bool, error) { + ctx = setContextOp(ctx, _q.ctx, ent.OpQueryExist) + switch _, err := _q.FirstID(ctx); { + case IsNotFound(err): + return false, nil + case err != nil: + return false, fmt.Errorf("ent: check existence: %w", err) + default: + return true, nil + } +} + +// ExistX is like Exist, but panics if an error occurs. +func (_q *NotificationSubscriptionQuery) ExistX(ctx context.Context) bool { + exist, err := _q.Exist(ctx) + if err != nil { + panic(err) + } + return exist +} + +// Clone returns a duplicate of the NotificationSubscriptionQuery builder, including all associated steps. It can be +// used to prepare common query builders and use them differently after the clone is made. +func (_q *NotificationSubscriptionQuery) Clone() *NotificationSubscriptionQuery { + if _q == nil { + return nil + } + return &NotificationSubscriptionQuery{ + config: _q.config, + ctx: _q.ctx.Clone(), + order: append([]notificationsubscription.OrderOption{}, _q.order...), + inters: append([]Interceptor{}, _q.inters...), + predicates: append([]predicate.NotificationSubscription{}, _q.predicates...), + // clone intermediate query. + sql: _q.sql.Clone(), + path: _q.path, + } +} + +// GroupBy is used to group vertices by one or more fields/columns. +// It is often used with aggregate functions, like: count, max, mean, min, sum. +// +// Example: +// +// var v []struct { +// Scope string `json:"scope,omitempty"` +// Count int `json:"count,omitempty"` +// } +// +// client.NotificationSubscription.Query(). +// GroupBy(notificationsubscription.FieldScope). +// Aggregate(ent.Count()). +// Scan(ctx, &v) +func (_q *NotificationSubscriptionQuery) GroupBy(field string, fields ...string) *NotificationSubscriptionGroupBy { + _q.ctx.Fields = append([]string{field}, fields...) + grbuild := &NotificationSubscriptionGroupBy{build: _q} + grbuild.flds = &_q.ctx.Fields + grbuild.label = notificationsubscription.Label + grbuild.scan = grbuild.Scan + return grbuild +} + +// Select allows the selection one or more fields/columns for the given query, +// instead of selecting all fields in the entity. +// +// Example: +// +// var v []struct { +// Scope string `json:"scope,omitempty"` +// } +// +// client.NotificationSubscription.Query(). +// Select(notificationsubscription.FieldScope). +// Scan(ctx, &v) +func (_q *NotificationSubscriptionQuery) Select(fields ...string) *NotificationSubscriptionSelect { + _q.ctx.Fields = append(_q.ctx.Fields, fields...) + sbuild := &NotificationSubscriptionSelect{NotificationSubscriptionQuery: _q} + sbuild.label = notificationsubscription.Label + sbuild.flds, sbuild.scan = &_q.ctx.Fields, sbuild.Scan + return sbuild +} + +// Aggregate returns a NotificationSubscriptionSelect configured with the given aggregations. +func (_q *NotificationSubscriptionQuery) Aggregate(fns ...AggregateFunc) *NotificationSubscriptionSelect { + return _q.Select().Aggregate(fns...) +} + +func (_q *NotificationSubscriptionQuery) prepareQuery(ctx context.Context) error { + for _, inter := range _q.inters { + if inter == nil { + return fmt.Errorf("ent: uninitialized interceptor (forgotten import ent/runtime?)") + } + if trv, ok := inter.(Traverser); ok { + if err := trv.Traverse(ctx, _q); err != nil { + return err + } + } + } + for _, f := range _q.ctx.Fields { + if !notificationsubscription.ValidColumn(f) { + return &ValidationError{Name: f, err: fmt.Errorf("ent: invalid field %q for query", f)} + } + } + if _q.path != nil { + prev, err := _q.path(ctx) + if err != nil { + return err + } + _q.sql = prev + } + return nil +} + +func (_q *NotificationSubscriptionQuery) sqlAll(ctx context.Context, hooks ...queryHook) ([]*NotificationSubscription, error) { + var ( + nodes = []*NotificationSubscription{} + _spec = _q.querySpec() + ) + _spec.ScanValues = func(columns []string) ([]any, error) { + return (*NotificationSubscription).scanValues(nil, columns) + } + _spec.Assign = func(columns []string, values []any) error { + node := &NotificationSubscription{config: _q.config} + nodes = append(nodes, node) + return node.assignValues(columns, values) + } + if len(_q.modifiers) > 0 { + _spec.Modifiers = _q.modifiers + } + for i := range hooks { + hooks[i](ctx, _spec) + } + if err := sqlgraph.QueryNodes(ctx, _q.driver, _spec); err != nil { + return nil, err + } + if len(nodes) == 0 { + return nodes, nil + } + return nodes, nil +} + +func (_q *NotificationSubscriptionQuery) sqlCount(ctx context.Context) (int, error) { + _spec := _q.querySpec() + if len(_q.modifiers) > 0 { + _spec.Modifiers = _q.modifiers + } + _spec.Node.Columns = _q.ctx.Fields + if len(_q.ctx.Fields) > 0 { + _spec.Unique = _q.ctx.Unique != nil && *_q.ctx.Unique + } + return sqlgraph.CountNodes(ctx, _q.driver, _spec) +} + +func (_q *NotificationSubscriptionQuery) querySpec() *sqlgraph.QuerySpec { + _spec := sqlgraph.NewQuerySpec(notificationsubscription.Table, notificationsubscription.Columns, sqlgraph.NewFieldSpec(notificationsubscription.FieldID, field.TypeUUID)) + _spec.From = _q.sql + if unique := _q.ctx.Unique; unique != nil { + _spec.Unique = *unique + } else if _q.path != nil { + _spec.Unique = true + } + if fields := _q.ctx.Fields; len(fields) > 0 { + _spec.Node.Columns = make([]string, 0, len(fields)) + _spec.Node.Columns = append(_spec.Node.Columns, notificationsubscription.FieldID) + for i := range fields { + if fields[i] != notificationsubscription.FieldID { + _spec.Node.Columns = append(_spec.Node.Columns, fields[i]) + } + } + } + if ps := _q.predicates; len(ps) > 0 { + _spec.Predicate = func(selector *sql.Selector) { + for i := range ps { + ps[i](selector) + } + } + } + if limit := _q.ctx.Limit; limit != nil { + _spec.Limit = *limit + } + if offset := _q.ctx.Offset; offset != nil { + _spec.Offset = *offset + } + if ps := _q.order; len(ps) > 0 { + _spec.Order = func(selector *sql.Selector) { + for i := range ps { + ps[i](selector) + } + } + } + return _spec +} + +func (_q *NotificationSubscriptionQuery) sqlQuery(ctx context.Context) *sql.Selector { + builder := sql.Dialect(_q.driver.Dialect()) + t1 := builder.Table(notificationsubscription.Table) + columns := _q.ctx.Fields + if len(columns) == 0 { + columns = notificationsubscription.Columns + } + selector := builder.Select(t1.Columns(columns...)...).From(t1) + if _q.sql != nil { + selector = _q.sql + selector.Select(selector.Columns(columns...)...) + } + if _q.ctx.Unique != nil && *_q.ctx.Unique { + selector.Distinct() + } + for _, m := range _q.modifiers { + m(selector) + } + for _, p := range _q.predicates { + p(selector) + } + for _, p := range _q.order { + p(selector) + } + if offset := _q.ctx.Offset; offset != nil { + // limit is mandatory for offset clause. We start + // with default value, and override it below if needed. + selector.Offset(*offset).Limit(math.MaxInt32) + } + if limit := _q.ctx.Limit; limit != nil { + selector.Limit(*limit) + } + return selector +} + +// ForUpdate locks the selected rows against concurrent updates, and prevent them from being +// updated, deleted or "selected ... for update" by other sessions, until the transaction is +// either committed or rolled-back. +func (_q *NotificationSubscriptionQuery) ForUpdate(opts ...sql.LockOption) *NotificationSubscriptionQuery { + if _q.driver.Dialect() == dialect.Postgres { + _q.Unique(false) + } + _q.modifiers = append(_q.modifiers, func(s *sql.Selector) { + s.ForUpdate(opts...) + }) + return _q +} + +// ForShare behaves similarly to ForUpdate, except that it acquires a shared mode lock +// on any rows that are read. Other sessions can read the rows, but cannot modify them +// until your transaction commits. +func (_q *NotificationSubscriptionQuery) ForShare(opts ...sql.LockOption) *NotificationSubscriptionQuery { + if _q.driver.Dialect() == dialect.Postgres { + _q.Unique(false) + } + _q.modifiers = append(_q.modifiers, func(s *sql.Selector) { + s.ForShare(opts...) + }) + return _q +} + +// NotificationSubscriptionGroupBy is the group-by builder for NotificationSubscription entities. +type NotificationSubscriptionGroupBy struct { + selector + build *NotificationSubscriptionQuery +} + +// Aggregate adds the given aggregation functions to the group-by query. +func (_g *NotificationSubscriptionGroupBy) Aggregate(fns ...AggregateFunc) *NotificationSubscriptionGroupBy { + _g.fns = append(_g.fns, fns...) + return _g +} + +// Scan applies the selector query and scans the result into the given value. +func (_g *NotificationSubscriptionGroupBy) Scan(ctx context.Context, v any) error { + ctx = setContextOp(ctx, _g.build.ctx, ent.OpQueryGroupBy) + if err := _g.build.prepareQuery(ctx); err != nil { + return err + } + return scanWithInterceptors[*NotificationSubscriptionQuery, *NotificationSubscriptionGroupBy](ctx, _g.build, _g, _g.build.inters, v) +} + +func (_g *NotificationSubscriptionGroupBy) sqlScan(ctx context.Context, root *NotificationSubscriptionQuery, v any) error { + selector := root.sqlQuery(ctx).Select() + aggregation := make([]string, 0, len(_g.fns)) + for _, fn := range _g.fns { + aggregation = append(aggregation, fn(selector)) + } + if len(selector.SelectedColumns()) == 0 { + columns := make([]string, 0, len(*_g.flds)+len(_g.fns)) + for _, f := range *_g.flds { + columns = append(columns, selector.C(f)) + } + columns = append(columns, aggregation...) + selector.Select(columns...) + } + selector.GroupBy(selector.Columns(*_g.flds...)...) + if err := selector.Err(); err != nil { + return err + } + rows := &sql.Rows{} + query, args := selector.Query() + if err := _g.build.driver.Query(ctx, query, args, rows); err != nil { + return err + } + defer rows.Close() + return sql.ScanSlice(rows, v) +} + +// NotificationSubscriptionSelect is the builder for selecting fields of NotificationSubscription entities. +type NotificationSubscriptionSelect struct { + *NotificationSubscriptionQuery + selector +} + +// Aggregate adds the given aggregation functions to the selector query. +func (_s *NotificationSubscriptionSelect) Aggregate(fns ...AggregateFunc) *NotificationSubscriptionSelect { + _s.fns = append(_s.fns, fns...) + return _s +} + +// Scan applies the selector query and scans the result into the given value. +func (_s *NotificationSubscriptionSelect) Scan(ctx context.Context, v any) error { + ctx = setContextOp(ctx, _s.ctx, ent.OpQuerySelect) + if err := _s.prepareQuery(ctx); err != nil { + return err + } + return scanWithInterceptors[*NotificationSubscriptionQuery, *NotificationSubscriptionSelect](ctx, _s.NotificationSubscriptionQuery, _s, _s.inters, v) +} + +func (_s *NotificationSubscriptionSelect) sqlScan(ctx context.Context, root *NotificationSubscriptionQuery, v any) error { + selector := root.sqlQuery(ctx) + aggregation := make([]string, 0, len(_s.fns)) + for _, fn := range _s.fns { + aggregation = append(aggregation, fn(selector)) + } + switch n := len(*_s.selector.flds); { + case n == 0 && len(aggregation) > 0: + selector.Select(aggregation...) + case n != 0 && len(aggregation) > 0: + selector.AppendSelect(aggregation...) + } + rows := &sql.Rows{} + query, args := selector.Query() + if err := _s.driver.Query(ctx, query, args, rows); err != nil { + return err + } + defer rows.Close() + return sql.ScanSlice(rows, v) +} diff --git a/pkg/ent/notificationsubscription_update.go b/pkg/ent/notificationsubscription_update.go new file mode 100644 index 000000000..94495f7c1 --- /dev/null +++ b/pkg/ent/notificationsubscription_update.go @@ -0,0 +1,478 @@ +// Code generated by ent, DO NOT EDIT. + +package ent + +import ( + "context" + "errors" + "fmt" + + "entgo.io/ent/dialect/sql" + "entgo.io/ent/dialect/sql/sqlgraph" + "entgo.io/ent/schema/field" + "github.com/GoogleCloudPlatform/scion/pkg/ent/notificationsubscription" + "github.com/GoogleCloudPlatform/scion/pkg/ent/predicate" + "github.com/google/uuid" +) + +// NotificationSubscriptionUpdate is the builder for updating NotificationSubscription entities. +type NotificationSubscriptionUpdate struct { + config + hooks []Hook + mutation *NotificationSubscriptionMutation +} + +// Where appends a list predicates to the NotificationSubscriptionUpdate builder. +func (_u *NotificationSubscriptionUpdate) Where(ps ...predicate.NotificationSubscription) *NotificationSubscriptionUpdate { + _u.mutation.Where(ps...) + return _u +} + +// SetScope sets the "scope" field. +func (_u *NotificationSubscriptionUpdate) SetScope(v string) *NotificationSubscriptionUpdate { + _u.mutation.SetScope(v) + return _u +} + +// SetNillableScope sets the "scope" field if the given value is not nil. +func (_u *NotificationSubscriptionUpdate) SetNillableScope(v *string) *NotificationSubscriptionUpdate { + if v != nil { + _u.SetScope(*v) + } + return _u +} + +// SetAgentID sets the "agent_id" field. +func (_u *NotificationSubscriptionUpdate) SetAgentID(v uuid.UUID) *NotificationSubscriptionUpdate { + _u.mutation.SetAgentID(v) + return _u +} + +// SetNillableAgentID sets the "agent_id" field if the given value is not nil. +func (_u *NotificationSubscriptionUpdate) SetNillableAgentID(v *uuid.UUID) *NotificationSubscriptionUpdate { + if v != nil { + _u.SetAgentID(*v) + } + return _u +} + +// ClearAgentID clears the value of the "agent_id" field. +func (_u *NotificationSubscriptionUpdate) ClearAgentID() *NotificationSubscriptionUpdate { + _u.mutation.ClearAgentID() + return _u +} + +// SetSubscriberType sets the "subscriber_type" field. +func (_u *NotificationSubscriptionUpdate) SetSubscriberType(v string) *NotificationSubscriptionUpdate { + _u.mutation.SetSubscriberType(v) + return _u +} + +// SetNillableSubscriberType sets the "subscriber_type" field if the given value is not nil. +func (_u *NotificationSubscriptionUpdate) SetNillableSubscriberType(v *string) *NotificationSubscriptionUpdate { + if v != nil { + _u.SetSubscriberType(*v) + } + return _u +} + +// SetSubscriberID sets the "subscriber_id" field. +func (_u *NotificationSubscriptionUpdate) SetSubscriberID(v string) *NotificationSubscriptionUpdate { + _u.mutation.SetSubscriberID(v) + return _u +} + +// SetNillableSubscriberID sets the "subscriber_id" field if the given value is not nil. +func (_u *NotificationSubscriptionUpdate) SetNillableSubscriberID(v *string) *NotificationSubscriptionUpdate { + if v != nil { + _u.SetSubscriberID(*v) + } + return _u +} + +// SetProjectID sets the "project_id" field. +func (_u *NotificationSubscriptionUpdate) SetProjectID(v uuid.UUID) *NotificationSubscriptionUpdate { + _u.mutation.SetProjectID(v) + return _u +} + +// SetNillableProjectID sets the "project_id" field if the given value is not nil. +func (_u *NotificationSubscriptionUpdate) SetNillableProjectID(v *uuid.UUID) *NotificationSubscriptionUpdate { + if v != nil { + _u.SetProjectID(*v) + } + return _u +} + +// SetTriggerActivities sets the "trigger_activities" field. +func (_u *NotificationSubscriptionUpdate) SetTriggerActivities(v string) *NotificationSubscriptionUpdate { + _u.mutation.SetTriggerActivities(v) + return _u +} + +// SetNillableTriggerActivities sets the "trigger_activities" field if the given value is not nil. +func (_u *NotificationSubscriptionUpdate) SetNillableTriggerActivities(v *string) *NotificationSubscriptionUpdate { + if v != nil { + _u.SetTriggerActivities(*v) + } + return _u +} + +// SetCreatedBy sets the "created_by" field. +func (_u *NotificationSubscriptionUpdate) SetCreatedBy(v string) *NotificationSubscriptionUpdate { + _u.mutation.SetCreatedBy(v) + return _u +} + +// SetNillableCreatedBy sets the "created_by" field if the given value is not nil. +func (_u *NotificationSubscriptionUpdate) SetNillableCreatedBy(v *string) *NotificationSubscriptionUpdate { + if v != nil { + _u.SetCreatedBy(*v) + } + return _u +} + +// Mutation returns the NotificationSubscriptionMutation object of the builder. +func (_u *NotificationSubscriptionUpdate) Mutation() *NotificationSubscriptionMutation { + return _u.mutation +} + +// Save executes the query and returns the number of nodes affected by the update operation. +func (_u *NotificationSubscriptionUpdate) Save(ctx context.Context) (int, error) { + return withHooks(ctx, _u.sqlSave, _u.mutation, _u.hooks) +} + +// SaveX is like Save, but panics if an error occurs. +func (_u *NotificationSubscriptionUpdate) SaveX(ctx context.Context) int { + affected, err := _u.Save(ctx) + if err != nil { + panic(err) + } + return affected +} + +// Exec executes the query. +func (_u *NotificationSubscriptionUpdate) Exec(ctx context.Context) error { + _, err := _u.Save(ctx) + return err +} + +// ExecX is like Exec, but panics if an error occurs. +func (_u *NotificationSubscriptionUpdate) ExecX(ctx context.Context) { + if err := _u.Exec(ctx); err != nil { + panic(err) + } +} + +// check runs all checks and user-defined validators on the builder. +func (_u *NotificationSubscriptionUpdate) check() error { + if v, ok := _u.mutation.SubscriberID(); ok { + if err := notificationsubscription.SubscriberIDValidator(v); err != nil { + return &ValidationError{Name: "subscriber_id", err: fmt.Errorf(`ent: validator failed for field "NotificationSubscription.subscriber_id": %w`, err)} + } + } + if v, ok := _u.mutation.TriggerActivities(); ok { + if err := notificationsubscription.TriggerActivitiesValidator(v); err != nil { + return &ValidationError{Name: "trigger_activities", err: fmt.Errorf(`ent: validator failed for field "NotificationSubscription.trigger_activities": %w`, err)} + } + } + if v, ok := _u.mutation.CreatedBy(); ok { + if err := notificationsubscription.CreatedByValidator(v); err != nil { + return &ValidationError{Name: "created_by", err: fmt.Errorf(`ent: validator failed for field "NotificationSubscription.created_by": %w`, err)} + } + } + return nil +} + +func (_u *NotificationSubscriptionUpdate) sqlSave(ctx context.Context) (_node int, err error) { + if err := _u.check(); err != nil { + return _node, err + } + _spec := sqlgraph.NewUpdateSpec(notificationsubscription.Table, notificationsubscription.Columns, sqlgraph.NewFieldSpec(notificationsubscription.FieldID, field.TypeUUID)) + if ps := _u.mutation.predicates; len(ps) > 0 { + _spec.Predicate = func(selector *sql.Selector) { + for i := range ps { + ps[i](selector) + } + } + } + if value, ok := _u.mutation.Scope(); ok { + _spec.SetField(notificationsubscription.FieldScope, field.TypeString, value) + } + if value, ok := _u.mutation.AgentID(); ok { + _spec.SetField(notificationsubscription.FieldAgentID, field.TypeUUID, value) + } + if _u.mutation.AgentIDCleared() { + _spec.ClearField(notificationsubscription.FieldAgentID, field.TypeUUID) + } + if value, ok := _u.mutation.SubscriberType(); ok { + _spec.SetField(notificationsubscription.FieldSubscriberType, field.TypeString, value) + } + if value, ok := _u.mutation.SubscriberID(); ok { + _spec.SetField(notificationsubscription.FieldSubscriberID, field.TypeString, value) + } + if value, ok := _u.mutation.ProjectID(); ok { + _spec.SetField(notificationsubscription.FieldProjectID, field.TypeUUID, value) + } + if value, ok := _u.mutation.TriggerActivities(); ok { + _spec.SetField(notificationsubscription.FieldTriggerActivities, field.TypeString, value) + } + if value, ok := _u.mutation.CreatedBy(); ok { + _spec.SetField(notificationsubscription.FieldCreatedBy, field.TypeString, value) + } + if _node, err = sqlgraph.UpdateNodes(ctx, _u.driver, _spec); err != nil { + if _, ok := err.(*sqlgraph.NotFoundError); ok { + err = &NotFoundError{notificationsubscription.Label} + } else if sqlgraph.IsConstraintError(err) { + err = &ConstraintError{msg: err.Error(), wrap: err} + } + return 0, err + } + _u.mutation.done = true + return _node, nil +} + +// NotificationSubscriptionUpdateOne is the builder for updating a single NotificationSubscription entity. +type NotificationSubscriptionUpdateOne struct { + config + fields []string + hooks []Hook + mutation *NotificationSubscriptionMutation +} + +// SetScope sets the "scope" field. +func (_u *NotificationSubscriptionUpdateOne) SetScope(v string) *NotificationSubscriptionUpdateOne { + _u.mutation.SetScope(v) + return _u +} + +// SetNillableScope sets the "scope" field if the given value is not nil. +func (_u *NotificationSubscriptionUpdateOne) SetNillableScope(v *string) *NotificationSubscriptionUpdateOne { + if v != nil { + _u.SetScope(*v) + } + return _u +} + +// SetAgentID sets the "agent_id" field. +func (_u *NotificationSubscriptionUpdateOne) SetAgentID(v uuid.UUID) *NotificationSubscriptionUpdateOne { + _u.mutation.SetAgentID(v) + return _u +} + +// SetNillableAgentID sets the "agent_id" field if the given value is not nil. +func (_u *NotificationSubscriptionUpdateOne) SetNillableAgentID(v *uuid.UUID) *NotificationSubscriptionUpdateOne { + if v != nil { + _u.SetAgentID(*v) + } + return _u +} + +// ClearAgentID clears the value of the "agent_id" field. +func (_u *NotificationSubscriptionUpdateOne) ClearAgentID() *NotificationSubscriptionUpdateOne { + _u.mutation.ClearAgentID() + return _u +} + +// SetSubscriberType sets the "subscriber_type" field. +func (_u *NotificationSubscriptionUpdateOne) SetSubscriberType(v string) *NotificationSubscriptionUpdateOne { + _u.mutation.SetSubscriberType(v) + return _u +} + +// SetNillableSubscriberType sets the "subscriber_type" field if the given value is not nil. +func (_u *NotificationSubscriptionUpdateOne) SetNillableSubscriberType(v *string) *NotificationSubscriptionUpdateOne { + if v != nil { + _u.SetSubscriberType(*v) + } + return _u +} + +// SetSubscriberID sets the "subscriber_id" field. +func (_u *NotificationSubscriptionUpdateOne) SetSubscriberID(v string) *NotificationSubscriptionUpdateOne { + _u.mutation.SetSubscriberID(v) + return _u +} + +// SetNillableSubscriberID sets the "subscriber_id" field if the given value is not nil. +func (_u *NotificationSubscriptionUpdateOne) SetNillableSubscriberID(v *string) *NotificationSubscriptionUpdateOne { + if v != nil { + _u.SetSubscriberID(*v) + } + return _u +} + +// SetProjectID sets the "project_id" field. +func (_u *NotificationSubscriptionUpdateOne) SetProjectID(v uuid.UUID) *NotificationSubscriptionUpdateOne { + _u.mutation.SetProjectID(v) + return _u +} + +// SetNillableProjectID sets the "project_id" field if the given value is not nil. +func (_u *NotificationSubscriptionUpdateOne) SetNillableProjectID(v *uuid.UUID) *NotificationSubscriptionUpdateOne { + if v != nil { + _u.SetProjectID(*v) + } + return _u +} + +// SetTriggerActivities sets the "trigger_activities" field. +func (_u *NotificationSubscriptionUpdateOne) SetTriggerActivities(v string) *NotificationSubscriptionUpdateOne { + _u.mutation.SetTriggerActivities(v) + return _u +} + +// SetNillableTriggerActivities sets the "trigger_activities" field if the given value is not nil. +func (_u *NotificationSubscriptionUpdateOne) SetNillableTriggerActivities(v *string) *NotificationSubscriptionUpdateOne { + if v != nil { + _u.SetTriggerActivities(*v) + } + return _u +} + +// SetCreatedBy sets the "created_by" field. +func (_u *NotificationSubscriptionUpdateOne) SetCreatedBy(v string) *NotificationSubscriptionUpdateOne { + _u.mutation.SetCreatedBy(v) + return _u +} + +// SetNillableCreatedBy sets the "created_by" field if the given value is not nil. +func (_u *NotificationSubscriptionUpdateOne) SetNillableCreatedBy(v *string) *NotificationSubscriptionUpdateOne { + if v != nil { + _u.SetCreatedBy(*v) + } + return _u +} + +// Mutation returns the NotificationSubscriptionMutation object of the builder. +func (_u *NotificationSubscriptionUpdateOne) Mutation() *NotificationSubscriptionMutation { + return _u.mutation +} + +// Where appends a list predicates to the NotificationSubscriptionUpdate builder. +func (_u *NotificationSubscriptionUpdateOne) Where(ps ...predicate.NotificationSubscription) *NotificationSubscriptionUpdateOne { + _u.mutation.Where(ps...) + return _u +} + +// Select allows selecting one or more fields (columns) of the returned entity. +// The default is selecting all fields defined in the entity schema. +func (_u *NotificationSubscriptionUpdateOne) Select(field string, fields ...string) *NotificationSubscriptionUpdateOne { + _u.fields = append([]string{field}, fields...) + return _u +} + +// Save executes the query and returns the updated NotificationSubscription entity. +func (_u *NotificationSubscriptionUpdateOne) Save(ctx context.Context) (*NotificationSubscription, error) { + return withHooks(ctx, _u.sqlSave, _u.mutation, _u.hooks) +} + +// SaveX is like Save, but panics if an error occurs. +func (_u *NotificationSubscriptionUpdateOne) SaveX(ctx context.Context) *NotificationSubscription { + node, err := _u.Save(ctx) + if err != nil { + panic(err) + } + return node +} + +// Exec executes the query on the entity. +func (_u *NotificationSubscriptionUpdateOne) Exec(ctx context.Context) error { + _, err := _u.Save(ctx) + return err +} + +// ExecX is like Exec, but panics if an error occurs. +func (_u *NotificationSubscriptionUpdateOne) ExecX(ctx context.Context) { + if err := _u.Exec(ctx); err != nil { + panic(err) + } +} + +// check runs all checks and user-defined validators on the builder. +func (_u *NotificationSubscriptionUpdateOne) check() error { + if v, ok := _u.mutation.SubscriberID(); ok { + if err := notificationsubscription.SubscriberIDValidator(v); err != nil { + return &ValidationError{Name: "subscriber_id", err: fmt.Errorf(`ent: validator failed for field "NotificationSubscription.subscriber_id": %w`, err)} + } + } + if v, ok := _u.mutation.TriggerActivities(); ok { + if err := notificationsubscription.TriggerActivitiesValidator(v); err != nil { + return &ValidationError{Name: "trigger_activities", err: fmt.Errorf(`ent: validator failed for field "NotificationSubscription.trigger_activities": %w`, err)} + } + } + if v, ok := _u.mutation.CreatedBy(); ok { + if err := notificationsubscription.CreatedByValidator(v); err != nil { + return &ValidationError{Name: "created_by", err: fmt.Errorf(`ent: validator failed for field "NotificationSubscription.created_by": %w`, err)} + } + } + return nil +} + +func (_u *NotificationSubscriptionUpdateOne) sqlSave(ctx context.Context) (_node *NotificationSubscription, err error) { + if err := _u.check(); err != nil { + return _node, err + } + _spec := sqlgraph.NewUpdateSpec(notificationsubscription.Table, notificationsubscription.Columns, sqlgraph.NewFieldSpec(notificationsubscription.FieldID, field.TypeUUID)) + id, ok := _u.mutation.ID() + if !ok { + return nil, &ValidationError{Name: "id", err: errors.New(`ent: missing "NotificationSubscription.id" for update`)} + } + _spec.Node.ID.Value = id + if fields := _u.fields; len(fields) > 0 { + _spec.Node.Columns = make([]string, 0, len(fields)) + _spec.Node.Columns = append(_spec.Node.Columns, notificationsubscription.FieldID) + for _, f := range fields { + if !notificationsubscription.ValidColumn(f) { + return nil, &ValidationError{Name: f, err: fmt.Errorf("ent: invalid field %q for query", f)} + } + if f != notificationsubscription.FieldID { + _spec.Node.Columns = append(_spec.Node.Columns, f) + } + } + } + if ps := _u.mutation.predicates; len(ps) > 0 { + _spec.Predicate = func(selector *sql.Selector) { + for i := range ps { + ps[i](selector) + } + } + } + if value, ok := _u.mutation.Scope(); ok { + _spec.SetField(notificationsubscription.FieldScope, field.TypeString, value) + } + if value, ok := _u.mutation.AgentID(); ok { + _spec.SetField(notificationsubscription.FieldAgentID, field.TypeUUID, value) + } + if _u.mutation.AgentIDCleared() { + _spec.ClearField(notificationsubscription.FieldAgentID, field.TypeUUID) + } + if value, ok := _u.mutation.SubscriberType(); ok { + _spec.SetField(notificationsubscription.FieldSubscriberType, field.TypeString, value) + } + if value, ok := _u.mutation.SubscriberID(); ok { + _spec.SetField(notificationsubscription.FieldSubscriberID, field.TypeString, value) + } + if value, ok := _u.mutation.ProjectID(); ok { + _spec.SetField(notificationsubscription.FieldProjectID, field.TypeUUID, value) + } + if value, ok := _u.mutation.TriggerActivities(); ok { + _spec.SetField(notificationsubscription.FieldTriggerActivities, field.TypeString, value) + } + if value, ok := _u.mutation.CreatedBy(); ok { + _spec.SetField(notificationsubscription.FieldCreatedBy, field.TypeString, value) + } + _node = &NotificationSubscription{config: _u.config} + _spec.Assign = _node.assignValues + _spec.ScanValues = _node.scanValues + if err = sqlgraph.UpdateNode(ctx, _u.driver, _spec); err != nil { + if _, ok := err.(*sqlgraph.NotFoundError); ok { + err = &NotFoundError{notificationsubscription.Label} + } else if sqlgraph.IsConstraintError(err) { + err = &ConstraintError{msg: err.Error(), wrap: err} + } + return nil, err + } + _u.mutation.done = true + return _node, nil +} diff --git a/pkg/ent/policybinding_create.go b/pkg/ent/policybinding_create.go index 41f8cd3a9..d57a7ad41 100644 --- a/pkg/ent/policybinding_create.go +++ b/pkg/ent/policybinding_create.go @@ -8,6 +8,8 @@ import ( "fmt" "time" + "entgo.io/ent/dialect" + "entgo.io/ent/dialect/sql" "entgo.io/ent/dialect/sql/sqlgraph" "entgo.io/ent/schema/field" "github.com/GoogleCloudPlatform/scion/pkg/ent/accesspolicy" @@ -23,6 +25,7 @@ type PolicyBindingCreate struct { config mutation *PolicyBindingMutation hooks []Hook + conflict []sql.ConflictOption } // SetPrincipalType sets the "principal_type" field. @@ -238,6 +241,7 @@ func (_c *PolicyBindingCreate) createSpec() (*PolicyBinding, *sqlgraph.CreateSpe _node = &PolicyBinding{config: _c.config} _spec = sqlgraph.NewCreateSpec(policybinding.Table, sqlgraph.NewFieldSpec(policybinding.FieldID, field.TypeUUID)) ) + _spec.OnConflict = _c.conflict if id, ok := _c.mutation.ID(); ok { _node.ID = id _spec.ID.Value = &id @@ -325,11 +329,371 @@ func (_c *PolicyBindingCreate) createSpec() (*PolicyBinding, *sqlgraph.CreateSpe return _node, _spec } +// OnConflict allows configuring the `ON CONFLICT` / `ON DUPLICATE KEY` clause +// of the `INSERT` statement. For example: +// +// client.PolicyBinding.Create(). +// SetPrincipalType(v). +// OnConflict( +// // Update the row with the new values +// // the was proposed for insertion. +// sql.ResolveWithNewValues(), +// ). +// // Override some of the fields with custom +// // update values. +// Update(func(u *ent.PolicyBindingUpsert) { +// SetPrincipalType(v+v). +// }). +// Exec(ctx) +func (_c *PolicyBindingCreate) OnConflict(opts ...sql.ConflictOption) *PolicyBindingUpsertOne { + _c.conflict = opts + return &PolicyBindingUpsertOne{ + create: _c, + } +} + +// OnConflictColumns calls `OnConflict` and configures the columns +// as conflict target. Using this option is equivalent to using: +// +// client.PolicyBinding.Create(). +// OnConflict(sql.ConflictColumns(columns...)). +// Exec(ctx) +func (_c *PolicyBindingCreate) OnConflictColumns(columns ...string) *PolicyBindingUpsertOne { + _c.conflict = append(_c.conflict, sql.ConflictColumns(columns...)) + return &PolicyBindingUpsertOne{ + create: _c, + } +} + +type ( + // PolicyBindingUpsertOne is the builder for "upsert"-ing + // one PolicyBinding node. + PolicyBindingUpsertOne struct { + create *PolicyBindingCreate + } + + // PolicyBindingUpsert is the "OnConflict" setter. + PolicyBindingUpsert struct { + *sql.UpdateSet + } +) + +// SetPrincipalType sets the "principal_type" field. +func (u *PolicyBindingUpsert) SetPrincipalType(v policybinding.PrincipalType) *PolicyBindingUpsert { + u.Set(policybinding.FieldPrincipalType, v) + return u +} + +// UpdatePrincipalType sets the "principal_type" field to the value that was provided on create. +func (u *PolicyBindingUpsert) UpdatePrincipalType() *PolicyBindingUpsert { + u.SetExcluded(policybinding.FieldPrincipalType) + return u +} + +// SetCreatedBy sets the "created_by" field. +func (u *PolicyBindingUpsert) SetCreatedBy(v string) *PolicyBindingUpsert { + u.Set(policybinding.FieldCreatedBy, v) + return u +} + +// UpdateCreatedBy sets the "created_by" field to the value that was provided on create. +func (u *PolicyBindingUpsert) UpdateCreatedBy() *PolicyBindingUpsert { + u.SetExcluded(policybinding.FieldCreatedBy) + return u +} + +// ClearCreatedBy clears the value of the "created_by" field. +func (u *PolicyBindingUpsert) ClearCreatedBy() *PolicyBindingUpsert { + u.SetNull(policybinding.FieldCreatedBy) + return u +} + +// SetPolicyID sets the "policy_id" field. +func (u *PolicyBindingUpsert) SetPolicyID(v uuid.UUID) *PolicyBindingUpsert { + u.Set(policybinding.FieldPolicyID, v) + return u +} + +// UpdatePolicyID sets the "policy_id" field to the value that was provided on create. +func (u *PolicyBindingUpsert) UpdatePolicyID() *PolicyBindingUpsert { + u.SetExcluded(policybinding.FieldPolicyID) + return u +} + +// ClearPolicyID clears the value of the "policy_id" field. +func (u *PolicyBindingUpsert) ClearPolicyID() *PolicyBindingUpsert { + u.SetNull(policybinding.FieldPolicyID) + return u +} + +// SetUserID sets the "user_id" field. +func (u *PolicyBindingUpsert) SetUserID(v uuid.UUID) *PolicyBindingUpsert { + u.Set(policybinding.FieldUserID, v) + return u +} + +// UpdateUserID sets the "user_id" field to the value that was provided on create. +func (u *PolicyBindingUpsert) UpdateUserID() *PolicyBindingUpsert { + u.SetExcluded(policybinding.FieldUserID) + return u +} + +// ClearUserID clears the value of the "user_id" field. +func (u *PolicyBindingUpsert) ClearUserID() *PolicyBindingUpsert { + u.SetNull(policybinding.FieldUserID) + return u +} + +// SetGroupID sets the "group_id" field. +func (u *PolicyBindingUpsert) SetGroupID(v uuid.UUID) *PolicyBindingUpsert { + u.Set(policybinding.FieldGroupID, v) + return u +} + +// UpdateGroupID sets the "group_id" field to the value that was provided on create. +func (u *PolicyBindingUpsert) UpdateGroupID() *PolicyBindingUpsert { + u.SetExcluded(policybinding.FieldGroupID) + return u +} + +// ClearGroupID clears the value of the "group_id" field. +func (u *PolicyBindingUpsert) ClearGroupID() *PolicyBindingUpsert { + u.SetNull(policybinding.FieldGroupID) + return u +} + +// SetAgentID sets the "agent_id" field. +func (u *PolicyBindingUpsert) SetAgentID(v uuid.UUID) *PolicyBindingUpsert { + u.Set(policybinding.FieldAgentID, v) + return u +} + +// UpdateAgentID sets the "agent_id" field to the value that was provided on create. +func (u *PolicyBindingUpsert) UpdateAgentID() *PolicyBindingUpsert { + u.SetExcluded(policybinding.FieldAgentID) + return u +} + +// ClearAgentID clears the value of the "agent_id" field. +func (u *PolicyBindingUpsert) ClearAgentID() *PolicyBindingUpsert { + u.SetNull(policybinding.FieldAgentID) + return u +} + +// UpdateNewValues updates the mutable fields using the new values that were set on create except the ID field. +// Using this option is equivalent to using: +// +// client.PolicyBinding.Create(). +// OnConflict( +// sql.ResolveWithNewValues(), +// sql.ResolveWith(func(u *sql.UpdateSet) { +// u.SetIgnore(policybinding.FieldID) +// }), +// ). +// Exec(ctx) +func (u *PolicyBindingUpsertOne) UpdateNewValues() *PolicyBindingUpsertOne { + u.create.conflict = append(u.create.conflict, sql.ResolveWithNewValues()) + u.create.conflict = append(u.create.conflict, sql.ResolveWith(func(s *sql.UpdateSet) { + if _, exists := u.create.mutation.ID(); exists { + s.SetIgnore(policybinding.FieldID) + } + if _, exists := u.create.mutation.Created(); exists { + s.SetIgnore(policybinding.FieldCreated) + } + })) + return u +} + +// Ignore sets each column to itself in case of conflict. +// Using this option is equivalent to using: +// +// client.PolicyBinding.Create(). +// OnConflict(sql.ResolveWithIgnore()). +// Exec(ctx) +func (u *PolicyBindingUpsertOne) Ignore() *PolicyBindingUpsertOne { + u.create.conflict = append(u.create.conflict, sql.ResolveWithIgnore()) + return u +} + +// DoNothing configures the conflict_action to `DO NOTHING`. +// Supported only by SQLite and PostgreSQL. +func (u *PolicyBindingUpsertOne) DoNothing() *PolicyBindingUpsertOne { + u.create.conflict = append(u.create.conflict, sql.DoNothing()) + return u +} + +// Update allows overriding fields `UPDATE` values. See the PolicyBindingCreate.OnConflict +// documentation for more info. +func (u *PolicyBindingUpsertOne) Update(set func(*PolicyBindingUpsert)) *PolicyBindingUpsertOne { + u.create.conflict = append(u.create.conflict, sql.ResolveWith(func(update *sql.UpdateSet) { + set(&PolicyBindingUpsert{UpdateSet: update}) + })) + return u +} + +// SetPrincipalType sets the "principal_type" field. +func (u *PolicyBindingUpsertOne) SetPrincipalType(v policybinding.PrincipalType) *PolicyBindingUpsertOne { + return u.Update(func(s *PolicyBindingUpsert) { + s.SetPrincipalType(v) + }) +} + +// UpdatePrincipalType sets the "principal_type" field to the value that was provided on create. +func (u *PolicyBindingUpsertOne) UpdatePrincipalType() *PolicyBindingUpsertOne { + return u.Update(func(s *PolicyBindingUpsert) { + s.UpdatePrincipalType() + }) +} + +// SetCreatedBy sets the "created_by" field. +func (u *PolicyBindingUpsertOne) SetCreatedBy(v string) *PolicyBindingUpsertOne { + return u.Update(func(s *PolicyBindingUpsert) { + s.SetCreatedBy(v) + }) +} + +// UpdateCreatedBy sets the "created_by" field to the value that was provided on create. +func (u *PolicyBindingUpsertOne) UpdateCreatedBy() *PolicyBindingUpsertOne { + return u.Update(func(s *PolicyBindingUpsert) { + s.UpdateCreatedBy() + }) +} + +// ClearCreatedBy clears the value of the "created_by" field. +func (u *PolicyBindingUpsertOne) ClearCreatedBy() *PolicyBindingUpsertOne { + return u.Update(func(s *PolicyBindingUpsert) { + s.ClearCreatedBy() + }) +} + +// SetPolicyID sets the "policy_id" field. +func (u *PolicyBindingUpsertOne) SetPolicyID(v uuid.UUID) *PolicyBindingUpsertOne { + return u.Update(func(s *PolicyBindingUpsert) { + s.SetPolicyID(v) + }) +} + +// UpdatePolicyID sets the "policy_id" field to the value that was provided on create. +func (u *PolicyBindingUpsertOne) UpdatePolicyID() *PolicyBindingUpsertOne { + return u.Update(func(s *PolicyBindingUpsert) { + s.UpdatePolicyID() + }) +} + +// ClearPolicyID clears the value of the "policy_id" field. +func (u *PolicyBindingUpsertOne) ClearPolicyID() *PolicyBindingUpsertOne { + return u.Update(func(s *PolicyBindingUpsert) { + s.ClearPolicyID() + }) +} + +// SetUserID sets the "user_id" field. +func (u *PolicyBindingUpsertOne) SetUserID(v uuid.UUID) *PolicyBindingUpsertOne { + return u.Update(func(s *PolicyBindingUpsert) { + s.SetUserID(v) + }) +} + +// UpdateUserID sets the "user_id" field to the value that was provided on create. +func (u *PolicyBindingUpsertOne) UpdateUserID() *PolicyBindingUpsertOne { + return u.Update(func(s *PolicyBindingUpsert) { + s.UpdateUserID() + }) +} + +// ClearUserID clears the value of the "user_id" field. +func (u *PolicyBindingUpsertOne) ClearUserID() *PolicyBindingUpsertOne { + return u.Update(func(s *PolicyBindingUpsert) { + s.ClearUserID() + }) +} + +// SetGroupID sets the "group_id" field. +func (u *PolicyBindingUpsertOne) SetGroupID(v uuid.UUID) *PolicyBindingUpsertOne { + return u.Update(func(s *PolicyBindingUpsert) { + s.SetGroupID(v) + }) +} + +// UpdateGroupID sets the "group_id" field to the value that was provided on create. +func (u *PolicyBindingUpsertOne) UpdateGroupID() *PolicyBindingUpsertOne { + return u.Update(func(s *PolicyBindingUpsert) { + s.UpdateGroupID() + }) +} + +// ClearGroupID clears the value of the "group_id" field. +func (u *PolicyBindingUpsertOne) ClearGroupID() *PolicyBindingUpsertOne { + return u.Update(func(s *PolicyBindingUpsert) { + s.ClearGroupID() + }) +} + +// SetAgentID sets the "agent_id" field. +func (u *PolicyBindingUpsertOne) SetAgentID(v uuid.UUID) *PolicyBindingUpsertOne { + return u.Update(func(s *PolicyBindingUpsert) { + s.SetAgentID(v) + }) +} + +// UpdateAgentID sets the "agent_id" field to the value that was provided on create. +func (u *PolicyBindingUpsertOne) UpdateAgentID() *PolicyBindingUpsertOne { + return u.Update(func(s *PolicyBindingUpsert) { + s.UpdateAgentID() + }) +} + +// ClearAgentID clears the value of the "agent_id" field. +func (u *PolicyBindingUpsertOne) ClearAgentID() *PolicyBindingUpsertOne { + return u.Update(func(s *PolicyBindingUpsert) { + s.ClearAgentID() + }) +} + +// Exec executes the query. +func (u *PolicyBindingUpsertOne) Exec(ctx context.Context) error { + if len(u.create.conflict) == 0 { + return errors.New("ent: missing options for PolicyBindingCreate.OnConflict") + } + return u.create.Exec(ctx) +} + +// ExecX is like Exec, but panics if an error occurs. +func (u *PolicyBindingUpsertOne) ExecX(ctx context.Context) { + if err := u.create.Exec(ctx); err != nil { + panic(err) + } +} + +// Exec executes the UPSERT query and returns the inserted/updated ID. +func (u *PolicyBindingUpsertOne) ID(ctx context.Context) (id uuid.UUID, err error) { + if u.create.driver.Dialect() == dialect.MySQL { + // In case of "ON CONFLICT", there is no way to get back non-numeric ID + // fields from the database since MySQL does not support the RETURNING clause. + return id, errors.New("ent: PolicyBindingUpsertOne.ID is not supported by MySQL driver. Use PolicyBindingUpsertOne.Exec instead") + } + node, err := u.create.Save(ctx) + if err != nil { + return id, err + } + return node.ID, nil +} + +// IDX is like ID, but panics if an error occurs. +func (u *PolicyBindingUpsertOne) IDX(ctx context.Context) uuid.UUID { + id, err := u.ID(ctx) + if err != nil { + panic(err) + } + return id +} + // PolicyBindingCreateBulk is the builder for creating many PolicyBinding entities in bulk. type PolicyBindingCreateBulk struct { config err error builders []*PolicyBindingCreate + conflict []sql.ConflictOption } // Save creates the PolicyBinding entities in the database. @@ -359,6 +723,7 @@ func (_c *PolicyBindingCreateBulk) Save(ctx context.Context) ([]*PolicyBinding, _, err = mutators[i+1].Mutate(root, _c.builders[i+1].mutation) } else { spec := &sqlgraph.BatchCreateSpec{Nodes: specs} + spec.OnConflict = _c.conflict // Invoke the actual operation on the latest mutation in the chain. if err = sqlgraph.BatchCreate(ctx, _c.driver, spec); err != nil { if sqlgraph.IsConstraintError(err) { @@ -408,3 +773,239 @@ func (_c *PolicyBindingCreateBulk) ExecX(ctx context.Context) { panic(err) } } + +// OnConflict allows configuring the `ON CONFLICT` / `ON DUPLICATE KEY` clause +// of the `INSERT` statement. For example: +// +// client.PolicyBinding.CreateBulk(builders...). +// OnConflict( +// // Update the row with the new values +// // the was proposed for insertion. +// sql.ResolveWithNewValues(), +// ). +// // Override some of the fields with custom +// // update values. +// Update(func(u *ent.PolicyBindingUpsert) { +// SetPrincipalType(v+v). +// }). +// Exec(ctx) +func (_c *PolicyBindingCreateBulk) OnConflict(opts ...sql.ConflictOption) *PolicyBindingUpsertBulk { + _c.conflict = opts + return &PolicyBindingUpsertBulk{ + create: _c, + } +} + +// OnConflictColumns calls `OnConflict` and configures the columns +// as conflict target. Using this option is equivalent to using: +// +// client.PolicyBinding.Create(). +// OnConflict(sql.ConflictColumns(columns...)). +// Exec(ctx) +func (_c *PolicyBindingCreateBulk) OnConflictColumns(columns ...string) *PolicyBindingUpsertBulk { + _c.conflict = append(_c.conflict, sql.ConflictColumns(columns...)) + return &PolicyBindingUpsertBulk{ + create: _c, + } +} + +// PolicyBindingUpsertBulk is the builder for "upsert"-ing +// a bulk of PolicyBinding nodes. +type PolicyBindingUpsertBulk struct { + create *PolicyBindingCreateBulk +} + +// UpdateNewValues updates the mutable fields using the new values that +// were set on create. Using this option is equivalent to using: +// +// client.PolicyBinding.Create(). +// OnConflict( +// sql.ResolveWithNewValues(), +// sql.ResolveWith(func(u *sql.UpdateSet) { +// u.SetIgnore(policybinding.FieldID) +// }), +// ). +// Exec(ctx) +func (u *PolicyBindingUpsertBulk) UpdateNewValues() *PolicyBindingUpsertBulk { + u.create.conflict = append(u.create.conflict, sql.ResolveWithNewValues()) + u.create.conflict = append(u.create.conflict, sql.ResolveWith(func(s *sql.UpdateSet) { + for _, b := range u.create.builders { + if _, exists := b.mutation.ID(); exists { + s.SetIgnore(policybinding.FieldID) + } + if _, exists := b.mutation.Created(); exists { + s.SetIgnore(policybinding.FieldCreated) + } + } + })) + return u +} + +// Ignore sets each column to itself in case of conflict. +// Using this option is equivalent to using: +// +// client.PolicyBinding.Create(). +// OnConflict(sql.ResolveWithIgnore()). +// Exec(ctx) +func (u *PolicyBindingUpsertBulk) Ignore() *PolicyBindingUpsertBulk { + u.create.conflict = append(u.create.conflict, sql.ResolveWithIgnore()) + return u +} + +// DoNothing configures the conflict_action to `DO NOTHING`. +// Supported only by SQLite and PostgreSQL. +func (u *PolicyBindingUpsertBulk) DoNothing() *PolicyBindingUpsertBulk { + u.create.conflict = append(u.create.conflict, sql.DoNothing()) + return u +} + +// Update allows overriding fields `UPDATE` values. See the PolicyBindingCreateBulk.OnConflict +// documentation for more info. +func (u *PolicyBindingUpsertBulk) Update(set func(*PolicyBindingUpsert)) *PolicyBindingUpsertBulk { + u.create.conflict = append(u.create.conflict, sql.ResolveWith(func(update *sql.UpdateSet) { + set(&PolicyBindingUpsert{UpdateSet: update}) + })) + return u +} + +// SetPrincipalType sets the "principal_type" field. +func (u *PolicyBindingUpsertBulk) SetPrincipalType(v policybinding.PrincipalType) *PolicyBindingUpsertBulk { + return u.Update(func(s *PolicyBindingUpsert) { + s.SetPrincipalType(v) + }) +} + +// UpdatePrincipalType sets the "principal_type" field to the value that was provided on create. +func (u *PolicyBindingUpsertBulk) UpdatePrincipalType() *PolicyBindingUpsertBulk { + return u.Update(func(s *PolicyBindingUpsert) { + s.UpdatePrincipalType() + }) +} + +// SetCreatedBy sets the "created_by" field. +func (u *PolicyBindingUpsertBulk) SetCreatedBy(v string) *PolicyBindingUpsertBulk { + return u.Update(func(s *PolicyBindingUpsert) { + s.SetCreatedBy(v) + }) +} + +// UpdateCreatedBy sets the "created_by" field to the value that was provided on create. +func (u *PolicyBindingUpsertBulk) UpdateCreatedBy() *PolicyBindingUpsertBulk { + return u.Update(func(s *PolicyBindingUpsert) { + s.UpdateCreatedBy() + }) +} + +// ClearCreatedBy clears the value of the "created_by" field. +func (u *PolicyBindingUpsertBulk) ClearCreatedBy() *PolicyBindingUpsertBulk { + return u.Update(func(s *PolicyBindingUpsert) { + s.ClearCreatedBy() + }) +} + +// SetPolicyID sets the "policy_id" field. +func (u *PolicyBindingUpsertBulk) SetPolicyID(v uuid.UUID) *PolicyBindingUpsertBulk { + return u.Update(func(s *PolicyBindingUpsert) { + s.SetPolicyID(v) + }) +} + +// UpdatePolicyID sets the "policy_id" field to the value that was provided on create. +func (u *PolicyBindingUpsertBulk) UpdatePolicyID() *PolicyBindingUpsertBulk { + return u.Update(func(s *PolicyBindingUpsert) { + s.UpdatePolicyID() + }) +} + +// ClearPolicyID clears the value of the "policy_id" field. +func (u *PolicyBindingUpsertBulk) ClearPolicyID() *PolicyBindingUpsertBulk { + return u.Update(func(s *PolicyBindingUpsert) { + s.ClearPolicyID() + }) +} + +// SetUserID sets the "user_id" field. +func (u *PolicyBindingUpsertBulk) SetUserID(v uuid.UUID) *PolicyBindingUpsertBulk { + return u.Update(func(s *PolicyBindingUpsert) { + s.SetUserID(v) + }) +} + +// UpdateUserID sets the "user_id" field to the value that was provided on create. +func (u *PolicyBindingUpsertBulk) UpdateUserID() *PolicyBindingUpsertBulk { + return u.Update(func(s *PolicyBindingUpsert) { + s.UpdateUserID() + }) +} + +// ClearUserID clears the value of the "user_id" field. +func (u *PolicyBindingUpsertBulk) ClearUserID() *PolicyBindingUpsertBulk { + return u.Update(func(s *PolicyBindingUpsert) { + s.ClearUserID() + }) +} + +// SetGroupID sets the "group_id" field. +func (u *PolicyBindingUpsertBulk) SetGroupID(v uuid.UUID) *PolicyBindingUpsertBulk { + return u.Update(func(s *PolicyBindingUpsert) { + s.SetGroupID(v) + }) +} + +// UpdateGroupID sets the "group_id" field to the value that was provided on create. +func (u *PolicyBindingUpsertBulk) UpdateGroupID() *PolicyBindingUpsertBulk { + return u.Update(func(s *PolicyBindingUpsert) { + s.UpdateGroupID() + }) +} + +// ClearGroupID clears the value of the "group_id" field. +func (u *PolicyBindingUpsertBulk) ClearGroupID() *PolicyBindingUpsertBulk { + return u.Update(func(s *PolicyBindingUpsert) { + s.ClearGroupID() + }) +} + +// SetAgentID sets the "agent_id" field. +func (u *PolicyBindingUpsertBulk) SetAgentID(v uuid.UUID) *PolicyBindingUpsertBulk { + return u.Update(func(s *PolicyBindingUpsert) { + s.SetAgentID(v) + }) +} + +// UpdateAgentID sets the "agent_id" field to the value that was provided on create. +func (u *PolicyBindingUpsertBulk) UpdateAgentID() *PolicyBindingUpsertBulk { + return u.Update(func(s *PolicyBindingUpsert) { + s.UpdateAgentID() + }) +} + +// ClearAgentID clears the value of the "agent_id" field. +func (u *PolicyBindingUpsertBulk) ClearAgentID() *PolicyBindingUpsertBulk { + return u.Update(func(s *PolicyBindingUpsert) { + s.ClearAgentID() + }) +} + +// Exec executes the query. +func (u *PolicyBindingUpsertBulk) Exec(ctx context.Context) error { + if u.create.err != nil { + return u.create.err + } + for i, b := range u.create.builders { + if len(b.conflict) != 0 { + return fmt.Errorf("ent: OnConflict was set for builder %d. Set it on the PolicyBindingCreateBulk instead", i) + } + } + if len(u.create.conflict) == 0 { + return errors.New("ent: missing options for PolicyBindingCreateBulk.OnConflict") + } + return u.create.Exec(ctx) +} + +// ExecX is like Exec, but panics if an error occurs. +func (u *PolicyBindingUpsertBulk) ExecX(ctx context.Context) { + if err := u.create.Exec(ctx); err != nil { + panic(err) + } +} diff --git a/pkg/ent/policybinding_query.go b/pkg/ent/policybinding_query.go index 2580b7618..6a2498f2c 100644 --- a/pkg/ent/policybinding_query.go +++ b/pkg/ent/policybinding_query.go @@ -8,6 +8,7 @@ import ( "math" "entgo.io/ent" + "entgo.io/ent/dialect" "entgo.io/ent/dialect/sql" "entgo.io/ent/dialect/sql/sqlgraph" "entgo.io/ent/schema/field" @@ -31,6 +32,7 @@ type PolicyBindingQuery struct { withUser *UserQuery withGroup *GroupQuery withAgent *AgentQuery + modifiers []func(*sql.Selector) // intermediate query (i.e. traversal path). sql *sql.Selector path func(context.Context) (*sql.Selector, error) @@ -495,6 +497,9 @@ func (_q *PolicyBindingQuery) sqlAll(ctx context.Context, hooks ...queryHook) ([ node.Edges.loadedTypes = loadedTypes return node.assignValues(columns, values) } + if len(_q.modifiers) > 0 { + _spec.Modifiers = _q.modifiers + } for i := range hooks { hooks[i](ctx, _spec) } @@ -662,6 +667,9 @@ func (_q *PolicyBindingQuery) loadAgent(ctx context.Context, query *AgentQuery, func (_q *PolicyBindingQuery) sqlCount(ctx context.Context) (int, error) { _spec := _q.querySpec() + if len(_q.modifiers) > 0 { + _spec.Modifiers = _q.modifiers + } _spec.Node.Columns = _q.ctx.Fields if len(_q.ctx.Fields) > 0 { _spec.Unique = _q.ctx.Unique != nil && *_q.ctx.Unique @@ -736,6 +744,9 @@ func (_q *PolicyBindingQuery) sqlQuery(ctx context.Context) *sql.Selector { if _q.ctx.Unique != nil && *_q.ctx.Unique { selector.Distinct() } + for _, m := range _q.modifiers { + m(selector) + } for _, p := range _q.predicates { p(selector) } @@ -753,6 +764,32 @@ func (_q *PolicyBindingQuery) sqlQuery(ctx context.Context) *sql.Selector { return selector } +// ForUpdate locks the selected rows against concurrent updates, and prevent them from being +// updated, deleted or "selected ... for update" by other sessions, until the transaction is +// either committed or rolled-back. +func (_q *PolicyBindingQuery) ForUpdate(opts ...sql.LockOption) *PolicyBindingQuery { + if _q.driver.Dialect() == dialect.Postgres { + _q.Unique(false) + } + _q.modifiers = append(_q.modifiers, func(s *sql.Selector) { + s.ForUpdate(opts...) + }) + return _q +} + +// ForShare behaves similarly to ForUpdate, except that it acquires a shared mode lock +// on any rows that are read. Other sessions can read the rows, but cannot modify them +// until your transaction commits. +func (_q *PolicyBindingQuery) ForShare(opts ...sql.LockOption) *PolicyBindingQuery { + if _q.driver.Dialect() == dialect.Postgres { + _q.Unique(false) + } + _q.modifiers = append(_q.modifiers, func(s *sql.Selector) { + s.ForShare(opts...) + }) + return _q +} + // PolicyBindingGroupBy is the group-by builder for PolicyBinding entities. type PolicyBindingGroupBy struct { selector diff --git a/pkg/ent/predicate/predicate.go b/pkg/ent/predicate/predicate.go index 58e745e1e..bb8ac70de 100644 --- a/pkg/ent/predicate/predicate.go +++ b/pkg/ent/predicate/predicate.go @@ -12,17 +12,89 @@ type AccessPolicy func(*sql.Selector) // Agent is the predicate function for agent builders. type Agent func(*sql.Selector) +// AllowListEntry is the predicate function for allowlistentry builders. +type AllowListEntry func(*sql.Selector) + +// ApiKey is the predicate function for apikey builders. +type ApiKey func(*sql.Selector) + +// BrokerDispatch is the predicate function for brokerdispatch builders. +type BrokerDispatch func(*sql.Selector) + +// BrokerJoinToken is the predicate function for brokerjointoken builders. +type BrokerJoinToken func(*sql.Selector) + +// BrokerSecret is the predicate function for brokersecret builders. +type BrokerSecret func(*sql.Selector) + +// EnvVar is the predicate function for envvar builders. +type EnvVar func(*sql.Selector) + +// GCPServiceAccount is the predicate function for gcpserviceaccount builders. +type GCPServiceAccount func(*sql.Selector) + +// GithubInstallation is the predicate function for githubinstallation builders. +type GithubInstallation func(*sql.Selector) + // Group is the predicate function for group builders. type Group func(*sql.Selector) // GroupMembership is the predicate function for groupmembership builders. type GroupMembership func(*sql.Selector) +// HarnessConfig is the predicate function for harnessconfig builders. +type HarnessConfig func(*sql.Selector) + +// InviteCode is the predicate function for invitecode builders. +type InviteCode func(*sql.Selector) + +// MaintenanceOperation is the predicate function for maintenanceoperation builders. +type MaintenanceOperation func(*sql.Selector) + +// MaintenanceOperationRun is the predicate function for maintenanceoperationrun builders. +type MaintenanceOperationRun func(*sql.Selector) + +// Message is the predicate function for message builders. +type Message func(*sql.Selector) + +// Notification is the predicate function for notification builders. +type Notification func(*sql.Selector) + +// NotificationSubscription is the predicate function for notificationsubscription builders. +type NotificationSubscription func(*sql.Selector) + // PolicyBinding is the predicate function for policybinding builders. type PolicyBinding func(*sql.Selector) // Project is the predicate function for project builders. type Project func(*sql.Selector) +// ProjectContributor is the predicate function for projectcontributor builders. +type ProjectContributor func(*sql.Selector) + +// ProjectSyncState is the predicate function for projectsyncstate builders. +type ProjectSyncState func(*sql.Selector) + +// RuntimeBroker is the predicate function for runtimebroker builders. +type RuntimeBroker func(*sql.Selector) + +// Schedule is the predicate function for schedule builders. +type Schedule func(*sql.Selector) + +// ScheduledEvent is the predicate function for scheduledevent builders. +type ScheduledEvent func(*sql.Selector) + +// Secret is the predicate function for secret builders. +type Secret func(*sql.Selector) + +// SubscriptionTemplate is the predicate function for subscriptiontemplate builders. +type SubscriptionTemplate func(*sql.Selector) + +// Template is the predicate function for template builders. +type Template func(*sql.Selector) + // User is the predicate function for user builders. type User func(*sql.Selector) + +// UserAccessToken is the predicate function for useraccesstoken builders. +type UserAccessToken func(*sql.Selector) diff --git a/pkg/ent/project.go b/pkg/ent/project.go index 32b7418f6..9110915bc 100644 --- a/pkg/ent/project.go +++ b/pkg/ent/project.go @@ -25,10 +25,14 @@ type Project struct { Slug string `json:"slug,omitempty"` // GitRemote holds the value of the "git_remote" field. GitRemote *string `json:"git_remote,omitempty"` + // DefaultRuntimeBrokerID holds the value of the "default_runtime_broker_id" field. + DefaultRuntimeBrokerID *string `json:"default_runtime_broker_id,omitempty"` // Labels holds the value of the "labels" field. Labels map[string]string `json:"labels,omitempty"` // Annotations holds the value of the "annotations" field. Annotations map[string]string `json:"annotations,omitempty"` + // SharedDirs holds the value of the "shared_dirs" field. + SharedDirs string `json:"shared_dirs,omitempty"` // Created holds the value of the "created" field. Created time.Time `json:"created,omitempty"` // Updated holds the value of the "updated" field. @@ -39,6 +43,14 @@ type Project struct { OwnerID string `json:"owner_id,omitempty"` // Visibility holds the value of the "visibility" field. Visibility string `json:"visibility,omitempty"` + // GithubInstallationID holds the value of the "github_installation_id" field. + GithubInstallationID *int64 `json:"github_installation_id,omitempty"` + // GithubPermissions holds the value of the "github_permissions" field. + GithubPermissions string `json:"github_permissions,omitempty"` + // GithubAppStatus holds the value of the "github_app_status" field. + GithubAppStatus string `json:"github_app_status,omitempty"` + // GitIdentity holds the value of the "git_identity" field. + GitIdentity string `json:"git_identity,omitempty"` // Edges holds the relations/edges for other nodes in the graph. // The values are being populated by the ProjectQuery when eager-loading is set. Edges ProjectEdges `json:"edges"` @@ -70,7 +82,9 @@ func (*Project) scanValues(columns []string) ([]any, error) { switch columns[i] { case project.FieldLabels, project.FieldAnnotations: values[i] = new([]byte) - case project.FieldName, project.FieldSlug, project.FieldGitRemote, project.FieldCreatedBy, project.FieldOwnerID, project.FieldVisibility: + case project.FieldGithubInstallationID: + values[i] = new(sql.NullInt64) + case project.FieldName, project.FieldSlug, project.FieldGitRemote, project.FieldDefaultRuntimeBrokerID, project.FieldSharedDirs, project.FieldCreatedBy, project.FieldOwnerID, project.FieldVisibility, project.FieldGithubPermissions, project.FieldGithubAppStatus, project.FieldGitIdentity: values[i] = new(sql.NullString) case project.FieldCreated, project.FieldUpdated: values[i] = new(sql.NullTime) @@ -116,6 +130,13 @@ func (_m *Project) assignValues(columns []string, values []any) error { _m.GitRemote = new(string) *_m.GitRemote = value.String } + case project.FieldDefaultRuntimeBrokerID: + if value, ok := values[i].(*sql.NullString); !ok { + return fmt.Errorf("unexpected type %T for field default_runtime_broker_id", values[i]) + } else if value.Valid { + _m.DefaultRuntimeBrokerID = new(string) + *_m.DefaultRuntimeBrokerID = value.String + } case project.FieldLabels: if value, ok := values[i].(*[]byte); !ok { return fmt.Errorf("unexpected type %T for field labels", values[i]) @@ -132,6 +153,12 @@ func (_m *Project) assignValues(columns []string, values []any) error { return fmt.Errorf("unmarshal field annotations: %w", err) } } + case project.FieldSharedDirs: + if value, ok := values[i].(*sql.NullString); !ok { + return fmt.Errorf("unexpected type %T for field shared_dirs", values[i]) + } else if value.Valid { + _m.SharedDirs = value.String + } case project.FieldCreated: if value, ok := values[i].(*sql.NullTime); !ok { return fmt.Errorf("unexpected type %T for field created", values[i]) @@ -162,6 +189,31 @@ func (_m *Project) assignValues(columns []string, values []any) error { } else if value.Valid { _m.Visibility = value.String } + case project.FieldGithubInstallationID: + if value, ok := values[i].(*sql.NullInt64); !ok { + return fmt.Errorf("unexpected type %T for field github_installation_id", values[i]) + } else if value.Valid { + _m.GithubInstallationID = new(int64) + *_m.GithubInstallationID = value.Int64 + } + case project.FieldGithubPermissions: + if value, ok := values[i].(*sql.NullString); !ok { + return fmt.Errorf("unexpected type %T for field github_permissions", values[i]) + } else if value.Valid { + _m.GithubPermissions = value.String + } + case project.FieldGithubAppStatus: + if value, ok := values[i].(*sql.NullString); !ok { + return fmt.Errorf("unexpected type %T for field github_app_status", values[i]) + } else if value.Valid { + _m.GithubAppStatus = value.String + } + case project.FieldGitIdentity: + if value, ok := values[i].(*sql.NullString); !ok { + return fmt.Errorf("unexpected type %T for field git_identity", values[i]) + } else if value.Valid { + _m.GitIdentity = value.String + } default: _m.selectValues.Set(columns[i], values[i]) } @@ -214,12 +266,20 @@ func (_m *Project) String() string { builder.WriteString(*v) } builder.WriteString(", ") + if v := _m.DefaultRuntimeBrokerID; v != nil { + builder.WriteString("default_runtime_broker_id=") + builder.WriteString(*v) + } + builder.WriteString(", ") builder.WriteString("labels=") builder.WriteString(fmt.Sprintf("%v", _m.Labels)) builder.WriteString(", ") builder.WriteString("annotations=") builder.WriteString(fmt.Sprintf("%v", _m.Annotations)) builder.WriteString(", ") + builder.WriteString("shared_dirs=") + builder.WriteString(_m.SharedDirs) + builder.WriteString(", ") builder.WriteString("created=") builder.WriteString(_m.Created.Format(time.ANSIC)) builder.WriteString(", ") @@ -234,6 +294,20 @@ func (_m *Project) String() string { builder.WriteString(", ") builder.WriteString("visibility=") builder.WriteString(_m.Visibility) + builder.WriteString(", ") + if v := _m.GithubInstallationID; v != nil { + builder.WriteString("github_installation_id=") + builder.WriteString(fmt.Sprintf("%v", *v)) + } + builder.WriteString(", ") + builder.WriteString("github_permissions=") + builder.WriteString(_m.GithubPermissions) + builder.WriteString(", ") + builder.WriteString("github_app_status=") + builder.WriteString(_m.GithubAppStatus) + builder.WriteString(", ") + builder.WriteString("git_identity=") + builder.WriteString(_m.GitIdentity) builder.WriteByte(')') return builder.String() } diff --git a/pkg/ent/project/project.go b/pkg/ent/project/project.go index 8911eb1aa..a8988dcc5 100644 --- a/pkg/ent/project/project.go +++ b/pkg/ent/project/project.go @@ -21,10 +21,14 @@ const ( FieldSlug = "slug" // FieldGitRemote holds the string denoting the git_remote field in the database. FieldGitRemote = "git_remote" + // FieldDefaultRuntimeBrokerID holds the string denoting the default_runtime_broker_id field in the database. + FieldDefaultRuntimeBrokerID = "default_runtime_broker_id" // FieldLabels holds the string denoting the labels field in the database. FieldLabels = "labels" // FieldAnnotations holds the string denoting the annotations field in the database. FieldAnnotations = "annotations" + // FieldSharedDirs holds the string denoting the shared_dirs field in the database. + FieldSharedDirs = "shared_dirs" // FieldCreated holds the string denoting the created field in the database. FieldCreated = "created" // FieldUpdated holds the string denoting the updated field in the database. @@ -35,6 +39,14 @@ const ( FieldOwnerID = "owner_id" // FieldVisibility holds the string denoting the visibility field in the database. FieldVisibility = "visibility" + // FieldGithubInstallationID holds the string denoting the github_installation_id field in the database. + FieldGithubInstallationID = "github_installation_id" + // FieldGithubPermissions holds the string denoting the github_permissions field in the database. + FieldGithubPermissions = "github_permissions" + // FieldGithubAppStatus holds the string denoting the github_app_status field in the database. + FieldGithubAppStatus = "github_app_status" + // FieldGitIdentity holds the string denoting the git_identity field in the database. + FieldGitIdentity = "git_identity" // EdgeAgents holds the string denoting the agents edge name in mutations. EdgeAgents = "agents" // Table holds the table name of the project in the database. @@ -54,13 +66,19 @@ var Columns = []string{ FieldName, FieldSlug, FieldGitRemote, + FieldDefaultRuntimeBrokerID, FieldLabels, FieldAnnotations, + FieldSharedDirs, FieldCreated, FieldUpdated, FieldCreatedBy, FieldOwnerID, FieldVisibility, + FieldGithubInstallationID, + FieldGithubPermissions, + FieldGithubAppStatus, + FieldGitIdentity, } // ValidColumn reports if the column name is valid (part of the table columns). @@ -113,6 +131,16 @@ func ByGitRemote(opts ...sql.OrderTermOption) OrderOption { return sql.OrderByField(FieldGitRemote, opts...).ToFunc() } +// ByDefaultRuntimeBrokerID orders the results by the default_runtime_broker_id field. +func ByDefaultRuntimeBrokerID(opts ...sql.OrderTermOption) OrderOption { + return sql.OrderByField(FieldDefaultRuntimeBrokerID, opts...).ToFunc() +} + +// BySharedDirs orders the results by the shared_dirs field. +func BySharedDirs(opts ...sql.OrderTermOption) OrderOption { + return sql.OrderByField(FieldSharedDirs, opts...).ToFunc() +} + // ByCreated orders the results by the created field. func ByCreated(opts ...sql.OrderTermOption) OrderOption { return sql.OrderByField(FieldCreated, opts...).ToFunc() @@ -138,6 +166,26 @@ func ByVisibility(opts ...sql.OrderTermOption) OrderOption { return sql.OrderByField(FieldVisibility, opts...).ToFunc() } +// ByGithubInstallationID orders the results by the github_installation_id field. +func ByGithubInstallationID(opts ...sql.OrderTermOption) OrderOption { + return sql.OrderByField(FieldGithubInstallationID, opts...).ToFunc() +} + +// ByGithubPermissions orders the results by the github_permissions field. +func ByGithubPermissions(opts ...sql.OrderTermOption) OrderOption { + return sql.OrderByField(FieldGithubPermissions, opts...).ToFunc() +} + +// ByGithubAppStatus orders the results by the github_app_status field. +func ByGithubAppStatus(opts ...sql.OrderTermOption) OrderOption { + return sql.OrderByField(FieldGithubAppStatus, opts...).ToFunc() +} + +// ByGitIdentity orders the results by the git_identity field. +func ByGitIdentity(opts ...sql.OrderTermOption) OrderOption { + return sql.OrderByField(FieldGitIdentity, opts...).ToFunc() +} + // ByAgentsCount orders the results by agents count. func ByAgentsCount(opts ...sql.OrderTermOption) OrderOption { return func(s *sql.Selector) { diff --git a/pkg/ent/project/where.go b/pkg/ent/project/where.go index 4e0643014..d7e0f6b94 100644 --- a/pkg/ent/project/where.go +++ b/pkg/ent/project/where.go @@ -71,6 +71,16 @@ func GitRemote(v string) predicate.Project { return predicate.Project(sql.FieldEQ(FieldGitRemote, v)) } +// DefaultRuntimeBrokerID applies equality check predicate on the "default_runtime_broker_id" field. It's identical to DefaultRuntimeBrokerIDEQ. +func DefaultRuntimeBrokerID(v string) predicate.Project { + return predicate.Project(sql.FieldEQ(FieldDefaultRuntimeBrokerID, v)) +} + +// SharedDirs applies equality check predicate on the "shared_dirs" field. It's identical to SharedDirsEQ. +func SharedDirs(v string) predicate.Project { + return predicate.Project(sql.FieldEQ(FieldSharedDirs, v)) +} + // Created applies equality check predicate on the "created" field. It's identical to CreatedEQ. func Created(v time.Time) predicate.Project { return predicate.Project(sql.FieldEQ(FieldCreated, v)) @@ -96,6 +106,26 @@ func Visibility(v string) predicate.Project { return predicate.Project(sql.FieldEQ(FieldVisibility, v)) } +// GithubInstallationID applies equality check predicate on the "github_installation_id" field. It's identical to GithubInstallationIDEQ. +func GithubInstallationID(v int64) predicate.Project { + return predicate.Project(sql.FieldEQ(FieldGithubInstallationID, v)) +} + +// GithubPermissions applies equality check predicate on the "github_permissions" field. It's identical to GithubPermissionsEQ. +func GithubPermissions(v string) predicate.Project { + return predicate.Project(sql.FieldEQ(FieldGithubPermissions, v)) +} + +// GithubAppStatus applies equality check predicate on the "github_app_status" field. It's identical to GithubAppStatusEQ. +func GithubAppStatus(v string) predicate.Project { + return predicate.Project(sql.FieldEQ(FieldGithubAppStatus, v)) +} + +// GitIdentity applies equality check predicate on the "git_identity" field. It's identical to GitIdentityEQ. +func GitIdentity(v string) predicate.Project { + return predicate.Project(sql.FieldEQ(FieldGitIdentity, v)) +} + // NameEQ applies the EQ predicate on the "name" field. func NameEQ(v string) predicate.Project { return predicate.Project(sql.FieldEQ(FieldName, v)) @@ -301,6 +331,81 @@ func GitRemoteContainsFold(v string) predicate.Project { return predicate.Project(sql.FieldContainsFold(FieldGitRemote, v)) } +// DefaultRuntimeBrokerIDEQ applies the EQ predicate on the "default_runtime_broker_id" field. +func DefaultRuntimeBrokerIDEQ(v string) predicate.Project { + return predicate.Project(sql.FieldEQ(FieldDefaultRuntimeBrokerID, v)) +} + +// DefaultRuntimeBrokerIDNEQ applies the NEQ predicate on the "default_runtime_broker_id" field. +func DefaultRuntimeBrokerIDNEQ(v string) predicate.Project { + return predicate.Project(sql.FieldNEQ(FieldDefaultRuntimeBrokerID, v)) +} + +// DefaultRuntimeBrokerIDIn applies the In predicate on the "default_runtime_broker_id" field. +func DefaultRuntimeBrokerIDIn(vs ...string) predicate.Project { + return predicate.Project(sql.FieldIn(FieldDefaultRuntimeBrokerID, vs...)) +} + +// DefaultRuntimeBrokerIDNotIn applies the NotIn predicate on the "default_runtime_broker_id" field. +func DefaultRuntimeBrokerIDNotIn(vs ...string) predicate.Project { + return predicate.Project(sql.FieldNotIn(FieldDefaultRuntimeBrokerID, vs...)) +} + +// DefaultRuntimeBrokerIDGT applies the GT predicate on the "default_runtime_broker_id" field. +func DefaultRuntimeBrokerIDGT(v string) predicate.Project { + return predicate.Project(sql.FieldGT(FieldDefaultRuntimeBrokerID, v)) +} + +// DefaultRuntimeBrokerIDGTE applies the GTE predicate on the "default_runtime_broker_id" field. +func DefaultRuntimeBrokerIDGTE(v string) predicate.Project { + return predicate.Project(sql.FieldGTE(FieldDefaultRuntimeBrokerID, v)) +} + +// DefaultRuntimeBrokerIDLT applies the LT predicate on the "default_runtime_broker_id" field. +func DefaultRuntimeBrokerIDLT(v string) predicate.Project { + return predicate.Project(sql.FieldLT(FieldDefaultRuntimeBrokerID, v)) +} + +// DefaultRuntimeBrokerIDLTE applies the LTE predicate on the "default_runtime_broker_id" field. +func DefaultRuntimeBrokerIDLTE(v string) predicate.Project { + return predicate.Project(sql.FieldLTE(FieldDefaultRuntimeBrokerID, v)) +} + +// DefaultRuntimeBrokerIDContains applies the Contains predicate on the "default_runtime_broker_id" field. +func DefaultRuntimeBrokerIDContains(v string) predicate.Project { + return predicate.Project(sql.FieldContains(FieldDefaultRuntimeBrokerID, v)) +} + +// DefaultRuntimeBrokerIDHasPrefix applies the HasPrefix predicate on the "default_runtime_broker_id" field. +func DefaultRuntimeBrokerIDHasPrefix(v string) predicate.Project { + return predicate.Project(sql.FieldHasPrefix(FieldDefaultRuntimeBrokerID, v)) +} + +// DefaultRuntimeBrokerIDHasSuffix applies the HasSuffix predicate on the "default_runtime_broker_id" field. +func DefaultRuntimeBrokerIDHasSuffix(v string) predicate.Project { + return predicate.Project(sql.FieldHasSuffix(FieldDefaultRuntimeBrokerID, v)) +} + +// DefaultRuntimeBrokerIDIsNil applies the IsNil predicate on the "default_runtime_broker_id" field. +func DefaultRuntimeBrokerIDIsNil() predicate.Project { + return predicate.Project(sql.FieldIsNull(FieldDefaultRuntimeBrokerID)) +} + +// DefaultRuntimeBrokerIDNotNil applies the NotNil predicate on the "default_runtime_broker_id" field. +func DefaultRuntimeBrokerIDNotNil() predicate.Project { + return predicate.Project(sql.FieldNotNull(FieldDefaultRuntimeBrokerID)) +} + +// DefaultRuntimeBrokerIDEqualFold applies the EqualFold predicate on the "default_runtime_broker_id" field. +func DefaultRuntimeBrokerIDEqualFold(v string) predicate.Project { + return predicate.Project(sql.FieldEqualFold(FieldDefaultRuntimeBrokerID, v)) +} + +// DefaultRuntimeBrokerIDContainsFold applies the ContainsFold predicate on the "default_runtime_broker_id" field. +func DefaultRuntimeBrokerIDContainsFold(v string) predicate.Project { + return predicate.Project(sql.FieldContainsFold(FieldDefaultRuntimeBrokerID, v)) +} + // LabelsIsNil applies the IsNil predicate on the "labels" field. func LabelsIsNil() predicate.Project { return predicate.Project(sql.FieldIsNull(FieldLabels)) @@ -321,6 +426,81 @@ func AnnotationsNotNil() predicate.Project { return predicate.Project(sql.FieldNotNull(FieldAnnotations)) } +// SharedDirsEQ applies the EQ predicate on the "shared_dirs" field. +func SharedDirsEQ(v string) predicate.Project { + return predicate.Project(sql.FieldEQ(FieldSharedDirs, v)) +} + +// SharedDirsNEQ applies the NEQ predicate on the "shared_dirs" field. +func SharedDirsNEQ(v string) predicate.Project { + return predicate.Project(sql.FieldNEQ(FieldSharedDirs, v)) +} + +// SharedDirsIn applies the In predicate on the "shared_dirs" field. +func SharedDirsIn(vs ...string) predicate.Project { + return predicate.Project(sql.FieldIn(FieldSharedDirs, vs...)) +} + +// SharedDirsNotIn applies the NotIn predicate on the "shared_dirs" field. +func SharedDirsNotIn(vs ...string) predicate.Project { + return predicate.Project(sql.FieldNotIn(FieldSharedDirs, vs...)) +} + +// SharedDirsGT applies the GT predicate on the "shared_dirs" field. +func SharedDirsGT(v string) predicate.Project { + return predicate.Project(sql.FieldGT(FieldSharedDirs, v)) +} + +// SharedDirsGTE applies the GTE predicate on the "shared_dirs" field. +func SharedDirsGTE(v string) predicate.Project { + return predicate.Project(sql.FieldGTE(FieldSharedDirs, v)) +} + +// SharedDirsLT applies the LT predicate on the "shared_dirs" field. +func SharedDirsLT(v string) predicate.Project { + return predicate.Project(sql.FieldLT(FieldSharedDirs, v)) +} + +// SharedDirsLTE applies the LTE predicate on the "shared_dirs" field. +func SharedDirsLTE(v string) predicate.Project { + return predicate.Project(sql.FieldLTE(FieldSharedDirs, v)) +} + +// SharedDirsContains applies the Contains predicate on the "shared_dirs" field. +func SharedDirsContains(v string) predicate.Project { + return predicate.Project(sql.FieldContains(FieldSharedDirs, v)) +} + +// SharedDirsHasPrefix applies the HasPrefix predicate on the "shared_dirs" field. +func SharedDirsHasPrefix(v string) predicate.Project { + return predicate.Project(sql.FieldHasPrefix(FieldSharedDirs, v)) +} + +// SharedDirsHasSuffix applies the HasSuffix predicate on the "shared_dirs" field. +func SharedDirsHasSuffix(v string) predicate.Project { + return predicate.Project(sql.FieldHasSuffix(FieldSharedDirs, v)) +} + +// SharedDirsIsNil applies the IsNil predicate on the "shared_dirs" field. +func SharedDirsIsNil() predicate.Project { + return predicate.Project(sql.FieldIsNull(FieldSharedDirs)) +} + +// SharedDirsNotNil applies the NotNil predicate on the "shared_dirs" field. +func SharedDirsNotNil() predicate.Project { + return predicate.Project(sql.FieldNotNull(FieldSharedDirs)) +} + +// SharedDirsEqualFold applies the EqualFold predicate on the "shared_dirs" field. +func SharedDirsEqualFold(v string) predicate.Project { + return predicate.Project(sql.FieldEqualFold(FieldSharedDirs, v)) +} + +// SharedDirsContainsFold applies the ContainsFold predicate on the "shared_dirs" field. +func SharedDirsContainsFold(v string) predicate.Project { + return predicate.Project(sql.FieldContainsFold(FieldSharedDirs, v)) +} + // CreatedEQ applies the EQ predicate on the "created" field. func CreatedEQ(v time.Time) predicate.Project { return predicate.Project(sql.FieldEQ(FieldCreated, v)) @@ -616,6 +796,281 @@ func VisibilityContainsFold(v string) predicate.Project { return predicate.Project(sql.FieldContainsFold(FieldVisibility, v)) } +// GithubInstallationIDEQ applies the EQ predicate on the "github_installation_id" field. +func GithubInstallationIDEQ(v int64) predicate.Project { + return predicate.Project(sql.FieldEQ(FieldGithubInstallationID, v)) +} + +// GithubInstallationIDNEQ applies the NEQ predicate on the "github_installation_id" field. +func GithubInstallationIDNEQ(v int64) predicate.Project { + return predicate.Project(sql.FieldNEQ(FieldGithubInstallationID, v)) +} + +// GithubInstallationIDIn applies the In predicate on the "github_installation_id" field. +func GithubInstallationIDIn(vs ...int64) predicate.Project { + return predicate.Project(sql.FieldIn(FieldGithubInstallationID, vs...)) +} + +// GithubInstallationIDNotIn applies the NotIn predicate on the "github_installation_id" field. +func GithubInstallationIDNotIn(vs ...int64) predicate.Project { + return predicate.Project(sql.FieldNotIn(FieldGithubInstallationID, vs...)) +} + +// GithubInstallationIDGT applies the GT predicate on the "github_installation_id" field. +func GithubInstallationIDGT(v int64) predicate.Project { + return predicate.Project(sql.FieldGT(FieldGithubInstallationID, v)) +} + +// GithubInstallationIDGTE applies the GTE predicate on the "github_installation_id" field. +func GithubInstallationIDGTE(v int64) predicate.Project { + return predicate.Project(sql.FieldGTE(FieldGithubInstallationID, v)) +} + +// GithubInstallationIDLT applies the LT predicate on the "github_installation_id" field. +func GithubInstallationIDLT(v int64) predicate.Project { + return predicate.Project(sql.FieldLT(FieldGithubInstallationID, v)) +} + +// GithubInstallationIDLTE applies the LTE predicate on the "github_installation_id" field. +func GithubInstallationIDLTE(v int64) predicate.Project { + return predicate.Project(sql.FieldLTE(FieldGithubInstallationID, v)) +} + +// GithubInstallationIDIsNil applies the IsNil predicate on the "github_installation_id" field. +func GithubInstallationIDIsNil() predicate.Project { + return predicate.Project(sql.FieldIsNull(FieldGithubInstallationID)) +} + +// GithubInstallationIDNotNil applies the NotNil predicate on the "github_installation_id" field. +func GithubInstallationIDNotNil() predicate.Project { + return predicate.Project(sql.FieldNotNull(FieldGithubInstallationID)) +} + +// GithubPermissionsEQ applies the EQ predicate on the "github_permissions" field. +func GithubPermissionsEQ(v string) predicate.Project { + return predicate.Project(sql.FieldEQ(FieldGithubPermissions, v)) +} + +// GithubPermissionsNEQ applies the NEQ predicate on the "github_permissions" field. +func GithubPermissionsNEQ(v string) predicate.Project { + return predicate.Project(sql.FieldNEQ(FieldGithubPermissions, v)) +} + +// GithubPermissionsIn applies the In predicate on the "github_permissions" field. +func GithubPermissionsIn(vs ...string) predicate.Project { + return predicate.Project(sql.FieldIn(FieldGithubPermissions, vs...)) +} + +// GithubPermissionsNotIn applies the NotIn predicate on the "github_permissions" field. +func GithubPermissionsNotIn(vs ...string) predicate.Project { + return predicate.Project(sql.FieldNotIn(FieldGithubPermissions, vs...)) +} + +// GithubPermissionsGT applies the GT predicate on the "github_permissions" field. +func GithubPermissionsGT(v string) predicate.Project { + return predicate.Project(sql.FieldGT(FieldGithubPermissions, v)) +} + +// GithubPermissionsGTE applies the GTE predicate on the "github_permissions" field. +func GithubPermissionsGTE(v string) predicate.Project { + return predicate.Project(sql.FieldGTE(FieldGithubPermissions, v)) +} + +// GithubPermissionsLT applies the LT predicate on the "github_permissions" field. +func GithubPermissionsLT(v string) predicate.Project { + return predicate.Project(sql.FieldLT(FieldGithubPermissions, v)) +} + +// GithubPermissionsLTE applies the LTE predicate on the "github_permissions" field. +func GithubPermissionsLTE(v string) predicate.Project { + return predicate.Project(sql.FieldLTE(FieldGithubPermissions, v)) +} + +// GithubPermissionsContains applies the Contains predicate on the "github_permissions" field. +func GithubPermissionsContains(v string) predicate.Project { + return predicate.Project(sql.FieldContains(FieldGithubPermissions, v)) +} + +// GithubPermissionsHasPrefix applies the HasPrefix predicate on the "github_permissions" field. +func GithubPermissionsHasPrefix(v string) predicate.Project { + return predicate.Project(sql.FieldHasPrefix(FieldGithubPermissions, v)) +} + +// GithubPermissionsHasSuffix applies the HasSuffix predicate on the "github_permissions" field. +func GithubPermissionsHasSuffix(v string) predicate.Project { + return predicate.Project(sql.FieldHasSuffix(FieldGithubPermissions, v)) +} + +// GithubPermissionsIsNil applies the IsNil predicate on the "github_permissions" field. +func GithubPermissionsIsNil() predicate.Project { + return predicate.Project(sql.FieldIsNull(FieldGithubPermissions)) +} + +// GithubPermissionsNotNil applies the NotNil predicate on the "github_permissions" field. +func GithubPermissionsNotNil() predicate.Project { + return predicate.Project(sql.FieldNotNull(FieldGithubPermissions)) +} + +// GithubPermissionsEqualFold applies the EqualFold predicate on the "github_permissions" field. +func GithubPermissionsEqualFold(v string) predicate.Project { + return predicate.Project(sql.FieldEqualFold(FieldGithubPermissions, v)) +} + +// GithubPermissionsContainsFold applies the ContainsFold predicate on the "github_permissions" field. +func GithubPermissionsContainsFold(v string) predicate.Project { + return predicate.Project(sql.FieldContainsFold(FieldGithubPermissions, v)) +} + +// GithubAppStatusEQ applies the EQ predicate on the "github_app_status" field. +func GithubAppStatusEQ(v string) predicate.Project { + return predicate.Project(sql.FieldEQ(FieldGithubAppStatus, v)) +} + +// GithubAppStatusNEQ applies the NEQ predicate on the "github_app_status" field. +func GithubAppStatusNEQ(v string) predicate.Project { + return predicate.Project(sql.FieldNEQ(FieldGithubAppStatus, v)) +} + +// GithubAppStatusIn applies the In predicate on the "github_app_status" field. +func GithubAppStatusIn(vs ...string) predicate.Project { + return predicate.Project(sql.FieldIn(FieldGithubAppStatus, vs...)) +} + +// GithubAppStatusNotIn applies the NotIn predicate on the "github_app_status" field. +func GithubAppStatusNotIn(vs ...string) predicate.Project { + return predicate.Project(sql.FieldNotIn(FieldGithubAppStatus, vs...)) +} + +// GithubAppStatusGT applies the GT predicate on the "github_app_status" field. +func GithubAppStatusGT(v string) predicate.Project { + return predicate.Project(sql.FieldGT(FieldGithubAppStatus, v)) +} + +// GithubAppStatusGTE applies the GTE predicate on the "github_app_status" field. +func GithubAppStatusGTE(v string) predicate.Project { + return predicate.Project(sql.FieldGTE(FieldGithubAppStatus, v)) +} + +// GithubAppStatusLT applies the LT predicate on the "github_app_status" field. +func GithubAppStatusLT(v string) predicate.Project { + return predicate.Project(sql.FieldLT(FieldGithubAppStatus, v)) +} + +// GithubAppStatusLTE applies the LTE predicate on the "github_app_status" field. +func GithubAppStatusLTE(v string) predicate.Project { + return predicate.Project(sql.FieldLTE(FieldGithubAppStatus, v)) +} + +// GithubAppStatusContains applies the Contains predicate on the "github_app_status" field. +func GithubAppStatusContains(v string) predicate.Project { + return predicate.Project(sql.FieldContains(FieldGithubAppStatus, v)) +} + +// GithubAppStatusHasPrefix applies the HasPrefix predicate on the "github_app_status" field. +func GithubAppStatusHasPrefix(v string) predicate.Project { + return predicate.Project(sql.FieldHasPrefix(FieldGithubAppStatus, v)) +} + +// GithubAppStatusHasSuffix applies the HasSuffix predicate on the "github_app_status" field. +func GithubAppStatusHasSuffix(v string) predicate.Project { + return predicate.Project(sql.FieldHasSuffix(FieldGithubAppStatus, v)) +} + +// GithubAppStatusIsNil applies the IsNil predicate on the "github_app_status" field. +func GithubAppStatusIsNil() predicate.Project { + return predicate.Project(sql.FieldIsNull(FieldGithubAppStatus)) +} + +// GithubAppStatusNotNil applies the NotNil predicate on the "github_app_status" field. +func GithubAppStatusNotNil() predicate.Project { + return predicate.Project(sql.FieldNotNull(FieldGithubAppStatus)) +} + +// GithubAppStatusEqualFold applies the EqualFold predicate on the "github_app_status" field. +func GithubAppStatusEqualFold(v string) predicate.Project { + return predicate.Project(sql.FieldEqualFold(FieldGithubAppStatus, v)) +} + +// GithubAppStatusContainsFold applies the ContainsFold predicate on the "github_app_status" field. +func GithubAppStatusContainsFold(v string) predicate.Project { + return predicate.Project(sql.FieldContainsFold(FieldGithubAppStatus, v)) +} + +// GitIdentityEQ applies the EQ predicate on the "git_identity" field. +func GitIdentityEQ(v string) predicate.Project { + return predicate.Project(sql.FieldEQ(FieldGitIdentity, v)) +} + +// GitIdentityNEQ applies the NEQ predicate on the "git_identity" field. +func GitIdentityNEQ(v string) predicate.Project { + return predicate.Project(sql.FieldNEQ(FieldGitIdentity, v)) +} + +// GitIdentityIn applies the In predicate on the "git_identity" field. +func GitIdentityIn(vs ...string) predicate.Project { + return predicate.Project(sql.FieldIn(FieldGitIdentity, vs...)) +} + +// GitIdentityNotIn applies the NotIn predicate on the "git_identity" field. +func GitIdentityNotIn(vs ...string) predicate.Project { + return predicate.Project(sql.FieldNotIn(FieldGitIdentity, vs...)) +} + +// GitIdentityGT applies the GT predicate on the "git_identity" field. +func GitIdentityGT(v string) predicate.Project { + return predicate.Project(sql.FieldGT(FieldGitIdentity, v)) +} + +// GitIdentityGTE applies the GTE predicate on the "git_identity" field. +func GitIdentityGTE(v string) predicate.Project { + return predicate.Project(sql.FieldGTE(FieldGitIdentity, v)) +} + +// GitIdentityLT applies the LT predicate on the "git_identity" field. +func GitIdentityLT(v string) predicate.Project { + return predicate.Project(sql.FieldLT(FieldGitIdentity, v)) +} + +// GitIdentityLTE applies the LTE predicate on the "git_identity" field. +func GitIdentityLTE(v string) predicate.Project { + return predicate.Project(sql.FieldLTE(FieldGitIdentity, v)) +} + +// GitIdentityContains applies the Contains predicate on the "git_identity" field. +func GitIdentityContains(v string) predicate.Project { + return predicate.Project(sql.FieldContains(FieldGitIdentity, v)) +} + +// GitIdentityHasPrefix applies the HasPrefix predicate on the "git_identity" field. +func GitIdentityHasPrefix(v string) predicate.Project { + return predicate.Project(sql.FieldHasPrefix(FieldGitIdentity, v)) +} + +// GitIdentityHasSuffix applies the HasSuffix predicate on the "git_identity" field. +func GitIdentityHasSuffix(v string) predicate.Project { + return predicate.Project(sql.FieldHasSuffix(FieldGitIdentity, v)) +} + +// GitIdentityIsNil applies the IsNil predicate on the "git_identity" field. +func GitIdentityIsNil() predicate.Project { + return predicate.Project(sql.FieldIsNull(FieldGitIdentity)) +} + +// GitIdentityNotNil applies the NotNil predicate on the "git_identity" field. +func GitIdentityNotNil() predicate.Project { + return predicate.Project(sql.FieldNotNull(FieldGitIdentity)) +} + +// GitIdentityEqualFold applies the EqualFold predicate on the "git_identity" field. +func GitIdentityEqualFold(v string) predicate.Project { + return predicate.Project(sql.FieldEqualFold(FieldGitIdentity, v)) +} + +// GitIdentityContainsFold applies the ContainsFold predicate on the "git_identity" field. +func GitIdentityContainsFold(v string) predicate.Project { + return predicate.Project(sql.FieldContainsFold(FieldGitIdentity, v)) +} + // HasAgents applies the HasEdge predicate on the "agents" edge. func HasAgents() predicate.Project { return predicate.Project(func(s *sql.Selector) { diff --git a/pkg/ent/project_create.go b/pkg/ent/project_create.go index 315948661..376e38471 100644 --- a/pkg/ent/project_create.go +++ b/pkg/ent/project_create.go @@ -8,6 +8,8 @@ import ( "fmt" "time" + "entgo.io/ent/dialect" + "entgo.io/ent/dialect/sql" "entgo.io/ent/dialect/sql/sqlgraph" "entgo.io/ent/schema/field" "github.com/GoogleCloudPlatform/scion/pkg/ent/agent" @@ -20,6 +22,7 @@ type ProjectCreate struct { config mutation *ProjectMutation hooks []Hook + conflict []sql.ConflictOption } // SetName sets the "name" field. @@ -48,6 +51,20 @@ func (_c *ProjectCreate) SetNillableGitRemote(v *string) *ProjectCreate { return _c } +// SetDefaultRuntimeBrokerID sets the "default_runtime_broker_id" field. +func (_c *ProjectCreate) SetDefaultRuntimeBrokerID(v string) *ProjectCreate { + _c.mutation.SetDefaultRuntimeBrokerID(v) + return _c +} + +// SetNillableDefaultRuntimeBrokerID sets the "default_runtime_broker_id" field if the given value is not nil. +func (_c *ProjectCreate) SetNillableDefaultRuntimeBrokerID(v *string) *ProjectCreate { + if v != nil { + _c.SetDefaultRuntimeBrokerID(*v) + } + return _c +} + // SetLabels sets the "labels" field. func (_c *ProjectCreate) SetLabels(v map[string]string) *ProjectCreate { _c.mutation.SetLabels(v) @@ -60,6 +77,20 @@ func (_c *ProjectCreate) SetAnnotations(v map[string]string) *ProjectCreate { return _c } +// SetSharedDirs sets the "shared_dirs" field. +func (_c *ProjectCreate) SetSharedDirs(v string) *ProjectCreate { + _c.mutation.SetSharedDirs(v) + return _c +} + +// SetNillableSharedDirs sets the "shared_dirs" field if the given value is not nil. +func (_c *ProjectCreate) SetNillableSharedDirs(v *string) *ProjectCreate { + if v != nil { + _c.SetSharedDirs(*v) + } + return _c +} + // SetCreated sets the "created" field. func (_c *ProjectCreate) SetCreated(v time.Time) *ProjectCreate { _c.mutation.SetCreated(v) @@ -130,6 +161,62 @@ func (_c *ProjectCreate) SetNillableVisibility(v *string) *ProjectCreate { return _c } +// SetGithubInstallationID sets the "github_installation_id" field. +func (_c *ProjectCreate) SetGithubInstallationID(v int64) *ProjectCreate { + _c.mutation.SetGithubInstallationID(v) + return _c +} + +// SetNillableGithubInstallationID sets the "github_installation_id" field if the given value is not nil. +func (_c *ProjectCreate) SetNillableGithubInstallationID(v *int64) *ProjectCreate { + if v != nil { + _c.SetGithubInstallationID(*v) + } + return _c +} + +// SetGithubPermissions sets the "github_permissions" field. +func (_c *ProjectCreate) SetGithubPermissions(v string) *ProjectCreate { + _c.mutation.SetGithubPermissions(v) + return _c +} + +// SetNillableGithubPermissions sets the "github_permissions" field if the given value is not nil. +func (_c *ProjectCreate) SetNillableGithubPermissions(v *string) *ProjectCreate { + if v != nil { + _c.SetGithubPermissions(*v) + } + return _c +} + +// SetGithubAppStatus sets the "github_app_status" field. +func (_c *ProjectCreate) SetGithubAppStatus(v string) *ProjectCreate { + _c.mutation.SetGithubAppStatus(v) + return _c +} + +// SetNillableGithubAppStatus sets the "github_app_status" field if the given value is not nil. +func (_c *ProjectCreate) SetNillableGithubAppStatus(v *string) *ProjectCreate { + if v != nil { + _c.SetGithubAppStatus(*v) + } + return _c +} + +// SetGitIdentity sets the "git_identity" field. +func (_c *ProjectCreate) SetGitIdentity(v string) *ProjectCreate { + _c.mutation.SetGitIdentity(v) + return _c +} + +// SetNillableGitIdentity sets the "git_identity" field if the given value is not nil. +func (_c *ProjectCreate) SetNillableGitIdentity(v *string) *ProjectCreate { + if v != nil { + _c.SetGitIdentity(*v) + } + return _c +} + // SetID sets the "id" field. func (_c *ProjectCreate) SetID(v uuid.UUID) *ProjectCreate { _c.mutation.SetID(v) @@ -270,6 +357,7 @@ func (_c *ProjectCreate) createSpec() (*Project, *sqlgraph.CreateSpec) { _node = &Project{config: _c.config} _spec = sqlgraph.NewCreateSpec(project.Table, sqlgraph.NewFieldSpec(project.FieldID, field.TypeUUID)) ) + _spec.OnConflict = _c.conflict if id, ok := _c.mutation.ID(); ok { _node.ID = id _spec.ID.Value = &id @@ -286,6 +374,10 @@ func (_c *ProjectCreate) createSpec() (*Project, *sqlgraph.CreateSpec) { _spec.SetField(project.FieldGitRemote, field.TypeString, value) _node.GitRemote = &value } + if value, ok := _c.mutation.DefaultRuntimeBrokerID(); ok { + _spec.SetField(project.FieldDefaultRuntimeBrokerID, field.TypeString, value) + _node.DefaultRuntimeBrokerID = &value + } if value, ok := _c.mutation.Labels(); ok { _spec.SetField(project.FieldLabels, field.TypeJSON, value) _node.Labels = value @@ -294,6 +386,10 @@ func (_c *ProjectCreate) createSpec() (*Project, *sqlgraph.CreateSpec) { _spec.SetField(project.FieldAnnotations, field.TypeJSON, value) _node.Annotations = value } + if value, ok := _c.mutation.SharedDirs(); ok { + _spec.SetField(project.FieldSharedDirs, field.TypeString, value) + _node.SharedDirs = value + } if value, ok := _c.mutation.Created(); ok { _spec.SetField(project.FieldCreated, field.TypeTime, value) _node.Created = value @@ -314,6 +410,22 @@ func (_c *ProjectCreate) createSpec() (*Project, *sqlgraph.CreateSpec) { _spec.SetField(project.FieldVisibility, field.TypeString, value) _node.Visibility = value } + if value, ok := _c.mutation.GithubInstallationID(); ok { + _spec.SetField(project.FieldGithubInstallationID, field.TypeInt64, value) + _node.GithubInstallationID = &value + } + if value, ok := _c.mutation.GithubPermissions(); ok { + _spec.SetField(project.FieldGithubPermissions, field.TypeString, value) + _node.GithubPermissions = value + } + if value, ok := _c.mutation.GithubAppStatus(); ok { + _spec.SetField(project.FieldGithubAppStatus, field.TypeString, value) + _node.GithubAppStatus = value + } + if value, ok := _c.mutation.GitIdentity(); ok { + _spec.SetField(project.FieldGitIdentity, field.TypeString, value) + _node.GitIdentity = value + } if nodes := _c.mutation.AgentsIDs(); len(nodes) > 0 { edge := &sqlgraph.EdgeSpec{ Rel: sqlgraph.O2M, @@ -333,11 +445,696 @@ func (_c *ProjectCreate) createSpec() (*Project, *sqlgraph.CreateSpec) { return _node, _spec } +// OnConflict allows configuring the `ON CONFLICT` / `ON DUPLICATE KEY` clause +// of the `INSERT` statement. For example: +// +// client.Project.Create(). +// SetName(v). +// OnConflict( +// // Update the row with the new values +// // the was proposed for insertion. +// sql.ResolveWithNewValues(), +// ). +// // Override some of the fields with custom +// // update values. +// Update(func(u *ent.ProjectUpsert) { +// SetName(v+v). +// }). +// Exec(ctx) +func (_c *ProjectCreate) OnConflict(opts ...sql.ConflictOption) *ProjectUpsertOne { + _c.conflict = opts + return &ProjectUpsertOne{ + create: _c, + } +} + +// OnConflictColumns calls `OnConflict` and configures the columns +// as conflict target. Using this option is equivalent to using: +// +// client.Project.Create(). +// OnConflict(sql.ConflictColumns(columns...)). +// Exec(ctx) +func (_c *ProjectCreate) OnConflictColumns(columns ...string) *ProjectUpsertOne { + _c.conflict = append(_c.conflict, sql.ConflictColumns(columns...)) + return &ProjectUpsertOne{ + create: _c, + } +} + +type ( + // ProjectUpsertOne is the builder for "upsert"-ing + // one Project node. + ProjectUpsertOne struct { + create *ProjectCreate + } + + // ProjectUpsert is the "OnConflict" setter. + ProjectUpsert struct { + *sql.UpdateSet + } +) + +// SetName sets the "name" field. +func (u *ProjectUpsert) SetName(v string) *ProjectUpsert { + u.Set(project.FieldName, v) + return u +} + +// UpdateName sets the "name" field to the value that was provided on create. +func (u *ProjectUpsert) UpdateName() *ProjectUpsert { + u.SetExcluded(project.FieldName) + return u +} + +// SetSlug sets the "slug" field. +func (u *ProjectUpsert) SetSlug(v string) *ProjectUpsert { + u.Set(project.FieldSlug, v) + return u +} + +// UpdateSlug sets the "slug" field to the value that was provided on create. +func (u *ProjectUpsert) UpdateSlug() *ProjectUpsert { + u.SetExcluded(project.FieldSlug) + return u +} + +// SetGitRemote sets the "git_remote" field. +func (u *ProjectUpsert) SetGitRemote(v string) *ProjectUpsert { + u.Set(project.FieldGitRemote, v) + return u +} + +// UpdateGitRemote sets the "git_remote" field to the value that was provided on create. +func (u *ProjectUpsert) UpdateGitRemote() *ProjectUpsert { + u.SetExcluded(project.FieldGitRemote) + return u +} + +// ClearGitRemote clears the value of the "git_remote" field. +func (u *ProjectUpsert) ClearGitRemote() *ProjectUpsert { + u.SetNull(project.FieldGitRemote) + return u +} + +// SetDefaultRuntimeBrokerID sets the "default_runtime_broker_id" field. +func (u *ProjectUpsert) SetDefaultRuntimeBrokerID(v string) *ProjectUpsert { + u.Set(project.FieldDefaultRuntimeBrokerID, v) + return u +} + +// UpdateDefaultRuntimeBrokerID sets the "default_runtime_broker_id" field to the value that was provided on create. +func (u *ProjectUpsert) UpdateDefaultRuntimeBrokerID() *ProjectUpsert { + u.SetExcluded(project.FieldDefaultRuntimeBrokerID) + return u +} + +// ClearDefaultRuntimeBrokerID clears the value of the "default_runtime_broker_id" field. +func (u *ProjectUpsert) ClearDefaultRuntimeBrokerID() *ProjectUpsert { + u.SetNull(project.FieldDefaultRuntimeBrokerID) + return u +} + +// SetLabels sets the "labels" field. +func (u *ProjectUpsert) SetLabels(v map[string]string) *ProjectUpsert { + u.Set(project.FieldLabels, v) + return u +} + +// UpdateLabels sets the "labels" field to the value that was provided on create. +func (u *ProjectUpsert) UpdateLabels() *ProjectUpsert { + u.SetExcluded(project.FieldLabels) + return u +} + +// ClearLabels clears the value of the "labels" field. +func (u *ProjectUpsert) ClearLabels() *ProjectUpsert { + u.SetNull(project.FieldLabels) + return u +} + +// SetAnnotations sets the "annotations" field. +func (u *ProjectUpsert) SetAnnotations(v map[string]string) *ProjectUpsert { + u.Set(project.FieldAnnotations, v) + return u +} + +// UpdateAnnotations sets the "annotations" field to the value that was provided on create. +func (u *ProjectUpsert) UpdateAnnotations() *ProjectUpsert { + u.SetExcluded(project.FieldAnnotations) + return u +} + +// ClearAnnotations clears the value of the "annotations" field. +func (u *ProjectUpsert) ClearAnnotations() *ProjectUpsert { + u.SetNull(project.FieldAnnotations) + return u +} + +// SetSharedDirs sets the "shared_dirs" field. +func (u *ProjectUpsert) SetSharedDirs(v string) *ProjectUpsert { + u.Set(project.FieldSharedDirs, v) + return u +} + +// UpdateSharedDirs sets the "shared_dirs" field to the value that was provided on create. +func (u *ProjectUpsert) UpdateSharedDirs() *ProjectUpsert { + u.SetExcluded(project.FieldSharedDirs) + return u +} + +// ClearSharedDirs clears the value of the "shared_dirs" field. +func (u *ProjectUpsert) ClearSharedDirs() *ProjectUpsert { + u.SetNull(project.FieldSharedDirs) + return u +} + +// SetUpdated sets the "updated" field. +func (u *ProjectUpsert) SetUpdated(v time.Time) *ProjectUpsert { + u.Set(project.FieldUpdated, v) + return u +} + +// UpdateUpdated sets the "updated" field to the value that was provided on create. +func (u *ProjectUpsert) UpdateUpdated() *ProjectUpsert { + u.SetExcluded(project.FieldUpdated) + return u +} + +// SetCreatedBy sets the "created_by" field. +func (u *ProjectUpsert) SetCreatedBy(v string) *ProjectUpsert { + u.Set(project.FieldCreatedBy, v) + return u +} + +// UpdateCreatedBy sets the "created_by" field to the value that was provided on create. +func (u *ProjectUpsert) UpdateCreatedBy() *ProjectUpsert { + u.SetExcluded(project.FieldCreatedBy) + return u +} + +// ClearCreatedBy clears the value of the "created_by" field. +func (u *ProjectUpsert) ClearCreatedBy() *ProjectUpsert { + u.SetNull(project.FieldCreatedBy) + return u +} + +// SetOwnerID sets the "owner_id" field. +func (u *ProjectUpsert) SetOwnerID(v string) *ProjectUpsert { + u.Set(project.FieldOwnerID, v) + return u +} + +// UpdateOwnerID sets the "owner_id" field to the value that was provided on create. +func (u *ProjectUpsert) UpdateOwnerID() *ProjectUpsert { + u.SetExcluded(project.FieldOwnerID) + return u +} + +// ClearOwnerID clears the value of the "owner_id" field. +func (u *ProjectUpsert) ClearOwnerID() *ProjectUpsert { + u.SetNull(project.FieldOwnerID) + return u +} + +// SetVisibility sets the "visibility" field. +func (u *ProjectUpsert) SetVisibility(v string) *ProjectUpsert { + u.Set(project.FieldVisibility, v) + return u +} + +// UpdateVisibility sets the "visibility" field to the value that was provided on create. +func (u *ProjectUpsert) UpdateVisibility() *ProjectUpsert { + u.SetExcluded(project.FieldVisibility) + return u +} + +// SetGithubInstallationID sets the "github_installation_id" field. +func (u *ProjectUpsert) SetGithubInstallationID(v int64) *ProjectUpsert { + u.Set(project.FieldGithubInstallationID, v) + return u +} + +// UpdateGithubInstallationID sets the "github_installation_id" field to the value that was provided on create. +func (u *ProjectUpsert) UpdateGithubInstallationID() *ProjectUpsert { + u.SetExcluded(project.FieldGithubInstallationID) + return u +} + +// AddGithubInstallationID adds v to the "github_installation_id" field. +func (u *ProjectUpsert) AddGithubInstallationID(v int64) *ProjectUpsert { + u.Add(project.FieldGithubInstallationID, v) + return u +} + +// ClearGithubInstallationID clears the value of the "github_installation_id" field. +func (u *ProjectUpsert) ClearGithubInstallationID() *ProjectUpsert { + u.SetNull(project.FieldGithubInstallationID) + return u +} + +// SetGithubPermissions sets the "github_permissions" field. +func (u *ProjectUpsert) SetGithubPermissions(v string) *ProjectUpsert { + u.Set(project.FieldGithubPermissions, v) + return u +} + +// UpdateGithubPermissions sets the "github_permissions" field to the value that was provided on create. +func (u *ProjectUpsert) UpdateGithubPermissions() *ProjectUpsert { + u.SetExcluded(project.FieldGithubPermissions) + return u +} + +// ClearGithubPermissions clears the value of the "github_permissions" field. +func (u *ProjectUpsert) ClearGithubPermissions() *ProjectUpsert { + u.SetNull(project.FieldGithubPermissions) + return u +} + +// SetGithubAppStatus sets the "github_app_status" field. +func (u *ProjectUpsert) SetGithubAppStatus(v string) *ProjectUpsert { + u.Set(project.FieldGithubAppStatus, v) + return u +} + +// UpdateGithubAppStatus sets the "github_app_status" field to the value that was provided on create. +func (u *ProjectUpsert) UpdateGithubAppStatus() *ProjectUpsert { + u.SetExcluded(project.FieldGithubAppStatus) + return u +} + +// ClearGithubAppStatus clears the value of the "github_app_status" field. +func (u *ProjectUpsert) ClearGithubAppStatus() *ProjectUpsert { + u.SetNull(project.FieldGithubAppStatus) + return u +} + +// SetGitIdentity sets the "git_identity" field. +func (u *ProjectUpsert) SetGitIdentity(v string) *ProjectUpsert { + u.Set(project.FieldGitIdentity, v) + return u +} + +// UpdateGitIdentity sets the "git_identity" field to the value that was provided on create. +func (u *ProjectUpsert) UpdateGitIdentity() *ProjectUpsert { + u.SetExcluded(project.FieldGitIdentity) + return u +} + +// ClearGitIdentity clears the value of the "git_identity" field. +func (u *ProjectUpsert) ClearGitIdentity() *ProjectUpsert { + u.SetNull(project.FieldGitIdentity) + return u +} + +// UpdateNewValues updates the mutable fields using the new values that were set on create except the ID field. +// Using this option is equivalent to using: +// +// client.Project.Create(). +// OnConflict( +// sql.ResolveWithNewValues(), +// sql.ResolveWith(func(u *sql.UpdateSet) { +// u.SetIgnore(project.FieldID) +// }), +// ). +// Exec(ctx) +func (u *ProjectUpsertOne) UpdateNewValues() *ProjectUpsertOne { + u.create.conflict = append(u.create.conflict, sql.ResolveWithNewValues()) + u.create.conflict = append(u.create.conflict, sql.ResolveWith(func(s *sql.UpdateSet) { + if _, exists := u.create.mutation.ID(); exists { + s.SetIgnore(project.FieldID) + } + if _, exists := u.create.mutation.Created(); exists { + s.SetIgnore(project.FieldCreated) + } + })) + return u +} + +// Ignore sets each column to itself in case of conflict. +// Using this option is equivalent to using: +// +// client.Project.Create(). +// OnConflict(sql.ResolveWithIgnore()). +// Exec(ctx) +func (u *ProjectUpsertOne) Ignore() *ProjectUpsertOne { + u.create.conflict = append(u.create.conflict, sql.ResolveWithIgnore()) + return u +} + +// DoNothing configures the conflict_action to `DO NOTHING`. +// Supported only by SQLite and PostgreSQL. +func (u *ProjectUpsertOne) DoNothing() *ProjectUpsertOne { + u.create.conflict = append(u.create.conflict, sql.DoNothing()) + return u +} + +// Update allows overriding fields `UPDATE` values. See the ProjectCreate.OnConflict +// documentation for more info. +func (u *ProjectUpsertOne) Update(set func(*ProjectUpsert)) *ProjectUpsertOne { + u.create.conflict = append(u.create.conflict, sql.ResolveWith(func(update *sql.UpdateSet) { + set(&ProjectUpsert{UpdateSet: update}) + })) + return u +} + +// SetName sets the "name" field. +func (u *ProjectUpsertOne) SetName(v string) *ProjectUpsertOne { + return u.Update(func(s *ProjectUpsert) { + s.SetName(v) + }) +} + +// UpdateName sets the "name" field to the value that was provided on create. +func (u *ProjectUpsertOne) UpdateName() *ProjectUpsertOne { + return u.Update(func(s *ProjectUpsert) { + s.UpdateName() + }) +} + +// SetSlug sets the "slug" field. +func (u *ProjectUpsertOne) SetSlug(v string) *ProjectUpsertOne { + return u.Update(func(s *ProjectUpsert) { + s.SetSlug(v) + }) +} + +// UpdateSlug sets the "slug" field to the value that was provided on create. +func (u *ProjectUpsertOne) UpdateSlug() *ProjectUpsertOne { + return u.Update(func(s *ProjectUpsert) { + s.UpdateSlug() + }) +} + +// SetGitRemote sets the "git_remote" field. +func (u *ProjectUpsertOne) SetGitRemote(v string) *ProjectUpsertOne { + return u.Update(func(s *ProjectUpsert) { + s.SetGitRemote(v) + }) +} + +// UpdateGitRemote sets the "git_remote" field to the value that was provided on create. +func (u *ProjectUpsertOne) UpdateGitRemote() *ProjectUpsertOne { + return u.Update(func(s *ProjectUpsert) { + s.UpdateGitRemote() + }) +} + +// ClearGitRemote clears the value of the "git_remote" field. +func (u *ProjectUpsertOne) ClearGitRemote() *ProjectUpsertOne { + return u.Update(func(s *ProjectUpsert) { + s.ClearGitRemote() + }) +} + +// SetDefaultRuntimeBrokerID sets the "default_runtime_broker_id" field. +func (u *ProjectUpsertOne) SetDefaultRuntimeBrokerID(v string) *ProjectUpsertOne { + return u.Update(func(s *ProjectUpsert) { + s.SetDefaultRuntimeBrokerID(v) + }) +} + +// UpdateDefaultRuntimeBrokerID sets the "default_runtime_broker_id" field to the value that was provided on create. +func (u *ProjectUpsertOne) UpdateDefaultRuntimeBrokerID() *ProjectUpsertOne { + return u.Update(func(s *ProjectUpsert) { + s.UpdateDefaultRuntimeBrokerID() + }) +} + +// ClearDefaultRuntimeBrokerID clears the value of the "default_runtime_broker_id" field. +func (u *ProjectUpsertOne) ClearDefaultRuntimeBrokerID() *ProjectUpsertOne { + return u.Update(func(s *ProjectUpsert) { + s.ClearDefaultRuntimeBrokerID() + }) +} + +// SetLabels sets the "labels" field. +func (u *ProjectUpsertOne) SetLabels(v map[string]string) *ProjectUpsertOne { + return u.Update(func(s *ProjectUpsert) { + s.SetLabels(v) + }) +} + +// UpdateLabels sets the "labels" field to the value that was provided on create. +func (u *ProjectUpsertOne) UpdateLabels() *ProjectUpsertOne { + return u.Update(func(s *ProjectUpsert) { + s.UpdateLabels() + }) +} + +// ClearLabels clears the value of the "labels" field. +func (u *ProjectUpsertOne) ClearLabels() *ProjectUpsertOne { + return u.Update(func(s *ProjectUpsert) { + s.ClearLabels() + }) +} + +// SetAnnotations sets the "annotations" field. +func (u *ProjectUpsertOne) SetAnnotations(v map[string]string) *ProjectUpsertOne { + return u.Update(func(s *ProjectUpsert) { + s.SetAnnotations(v) + }) +} + +// UpdateAnnotations sets the "annotations" field to the value that was provided on create. +func (u *ProjectUpsertOne) UpdateAnnotations() *ProjectUpsertOne { + return u.Update(func(s *ProjectUpsert) { + s.UpdateAnnotations() + }) +} + +// ClearAnnotations clears the value of the "annotations" field. +func (u *ProjectUpsertOne) ClearAnnotations() *ProjectUpsertOne { + return u.Update(func(s *ProjectUpsert) { + s.ClearAnnotations() + }) +} + +// SetSharedDirs sets the "shared_dirs" field. +func (u *ProjectUpsertOne) SetSharedDirs(v string) *ProjectUpsertOne { + return u.Update(func(s *ProjectUpsert) { + s.SetSharedDirs(v) + }) +} + +// UpdateSharedDirs sets the "shared_dirs" field to the value that was provided on create. +func (u *ProjectUpsertOne) UpdateSharedDirs() *ProjectUpsertOne { + return u.Update(func(s *ProjectUpsert) { + s.UpdateSharedDirs() + }) +} + +// ClearSharedDirs clears the value of the "shared_dirs" field. +func (u *ProjectUpsertOne) ClearSharedDirs() *ProjectUpsertOne { + return u.Update(func(s *ProjectUpsert) { + s.ClearSharedDirs() + }) +} + +// SetUpdated sets the "updated" field. +func (u *ProjectUpsertOne) SetUpdated(v time.Time) *ProjectUpsertOne { + return u.Update(func(s *ProjectUpsert) { + s.SetUpdated(v) + }) +} + +// UpdateUpdated sets the "updated" field to the value that was provided on create. +func (u *ProjectUpsertOne) UpdateUpdated() *ProjectUpsertOne { + return u.Update(func(s *ProjectUpsert) { + s.UpdateUpdated() + }) +} + +// SetCreatedBy sets the "created_by" field. +func (u *ProjectUpsertOne) SetCreatedBy(v string) *ProjectUpsertOne { + return u.Update(func(s *ProjectUpsert) { + s.SetCreatedBy(v) + }) +} + +// UpdateCreatedBy sets the "created_by" field to the value that was provided on create. +func (u *ProjectUpsertOne) UpdateCreatedBy() *ProjectUpsertOne { + return u.Update(func(s *ProjectUpsert) { + s.UpdateCreatedBy() + }) +} + +// ClearCreatedBy clears the value of the "created_by" field. +func (u *ProjectUpsertOne) ClearCreatedBy() *ProjectUpsertOne { + return u.Update(func(s *ProjectUpsert) { + s.ClearCreatedBy() + }) +} + +// SetOwnerID sets the "owner_id" field. +func (u *ProjectUpsertOne) SetOwnerID(v string) *ProjectUpsertOne { + return u.Update(func(s *ProjectUpsert) { + s.SetOwnerID(v) + }) +} + +// UpdateOwnerID sets the "owner_id" field to the value that was provided on create. +func (u *ProjectUpsertOne) UpdateOwnerID() *ProjectUpsertOne { + return u.Update(func(s *ProjectUpsert) { + s.UpdateOwnerID() + }) +} + +// ClearOwnerID clears the value of the "owner_id" field. +func (u *ProjectUpsertOne) ClearOwnerID() *ProjectUpsertOne { + return u.Update(func(s *ProjectUpsert) { + s.ClearOwnerID() + }) +} + +// SetVisibility sets the "visibility" field. +func (u *ProjectUpsertOne) SetVisibility(v string) *ProjectUpsertOne { + return u.Update(func(s *ProjectUpsert) { + s.SetVisibility(v) + }) +} + +// UpdateVisibility sets the "visibility" field to the value that was provided on create. +func (u *ProjectUpsertOne) UpdateVisibility() *ProjectUpsertOne { + return u.Update(func(s *ProjectUpsert) { + s.UpdateVisibility() + }) +} + +// SetGithubInstallationID sets the "github_installation_id" field. +func (u *ProjectUpsertOne) SetGithubInstallationID(v int64) *ProjectUpsertOne { + return u.Update(func(s *ProjectUpsert) { + s.SetGithubInstallationID(v) + }) +} + +// AddGithubInstallationID adds v to the "github_installation_id" field. +func (u *ProjectUpsertOne) AddGithubInstallationID(v int64) *ProjectUpsertOne { + return u.Update(func(s *ProjectUpsert) { + s.AddGithubInstallationID(v) + }) +} + +// UpdateGithubInstallationID sets the "github_installation_id" field to the value that was provided on create. +func (u *ProjectUpsertOne) UpdateGithubInstallationID() *ProjectUpsertOne { + return u.Update(func(s *ProjectUpsert) { + s.UpdateGithubInstallationID() + }) +} + +// ClearGithubInstallationID clears the value of the "github_installation_id" field. +func (u *ProjectUpsertOne) ClearGithubInstallationID() *ProjectUpsertOne { + return u.Update(func(s *ProjectUpsert) { + s.ClearGithubInstallationID() + }) +} + +// SetGithubPermissions sets the "github_permissions" field. +func (u *ProjectUpsertOne) SetGithubPermissions(v string) *ProjectUpsertOne { + return u.Update(func(s *ProjectUpsert) { + s.SetGithubPermissions(v) + }) +} + +// UpdateGithubPermissions sets the "github_permissions" field to the value that was provided on create. +func (u *ProjectUpsertOne) UpdateGithubPermissions() *ProjectUpsertOne { + return u.Update(func(s *ProjectUpsert) { + s.UpdateGithubPermissions() + }) +} + +// ClearGithubPermissions clears the value of the "github_permissions" field. +func (u *ProjectUpsertOne) ClearGithubPermissions() *ProjectUpsertOne { + return u.Update(func(s *ProjectUpsert) { + s.ClearGithubPermissions() + }) +} + +// SetGithubAppStatus sets the "github_app_status" field. +func (u *ProjectUpsertOne) SetGithubAppStatus(v string) *ProjectUpsertOne { + return u.Update(func(s *ProjectUpsert) { + s.SetGithubAppStatus(v) + }) +} + +// UpdateGithubAppStatus sets the "github_app_status" field to the value that was provided on create. +func (u *ProjectUpsertOne) UpdateGithubAppStatus() *ProjectUpsertOne { + return u.Update(func(s *ProjectUpsert) { + s.UpdateGithubAppStatus() + }) +} + +// ClearGithubAppStatus clears the value of the "github_app_status" field. +func (u *ProjectUpsertOne) ClearGithubAppStatus() *ProjectUpsertOne { + return u.Update(func(s *ProjectUpsert) { + s.ClearGithubAppStatus() + }) +} + +// SetGitIdentity sets the "git_identity" field. +func (u *ProjectUpsertOne) SetGitIdentity(v string) *ProjectUpsertOne { + return u.Update(func(s *ProjectUpsert) { + s.SetGitIdentity(v) + }) +} + +// UpdateGitIdentity sets the "git_identity" field to the value that was provided on create. +func (u *ProjectUpsertOne) UpdateGitIdentity() *ProjectUpsertOne { + return u.Update(func(s *ProjectUpsert) { + s.UpdateGitIdentity() + }) +} + +// ClearGitIdentity clears the value of the "git_identity" field. +func (u *ProjectUpsertOne) ClearGitIdentity() *ProjectUpsertOne { + return u.Update(func(s *ProjectUpsert) { + s.ClearGitIdentity() + }) +} + +// Exec executes the query. +func (u *ProjectUpsertOne) Exec(ctx context.Context) error { + if len(u.create.conflict) == 0 { + return errors.New("ent: missing options for ProjectCreate.OnConflict") + } + return u.create.Exec(ctx) +} + +// ExecX is like Exec, but panics if an error occurs. +func (u *ProjectUpsertOne) ExecX(ctx context.Context) { + if err := u.create.Exec(ctx); err != nil { + panic(err) + } +} + +// Exec executes the UPSERT query and returns the inserted/updated ID. +func (u *ProjectUpsertOne) ID(ctx context.Context) (id uuid.UUID, err error) { + if u.create.driver.Dialect() == dialect.MySQL { + // In case of "ON CONFLICT", there is no way to get back non-numeric ID + // fields from the database since MySQL does not support the RETURNING clause. + return id, errors.New("ent: ProjectUpsertOne.ID is not supported by MySQL driver. Use ProjectUpsertOne.Exec instead") + } + node, err := u.create.Save(ctx) + if err != nil { + return id, err + } + return node.ID, nil +} + +// IDX is like ID, but panics if an error occurs. +func (u *ProjectUpsertOne) IDX(ctx context.Context) uuid.UUID { + id, err := u.ID(ctx) + if err != nil { + panic(err) + } + return id +} + // ProjectCreateBulk is the builder for creating many Project entities in bulk. type ProjectCreateBulk struct { config err error builders []*ProjectCreate + conflict []sql.ConflictOption } // Save creates the Project entities in the database. @@ -367,6 +1164,7 @@ func (_c *ProjectCreateBulk) Save(ctx context.Context) ([]*Project, error) { _, err = mutators[i+1].Mutate(root, _c.builders[i+1].mutation) } else { spec := &sqlgraph.BatchCreateSpec{Nodes: specs} + spec.OnConflict = _c.conflict // Invoke the actual operation on the latest mutation in the chain. if err = sqlgraph.BatchCreate(ctx, _c.driver, spec); err != nil { if sqlgraph.IsConstraintError(err) { @@ -416,3 +1214,414 @@ func (_c *ProjectCreateBulk) ExecX(ctx context.Context) { panic(err) } } + +// OnConflict allows configuring the `ON CONFLICT` / `ON DUPLICATE KEY` clause +// of the `INSERT` statement. For example: +// +// client.Project.CreateBulk(builders...). +// OnConflict( +// // Update the row with the new values +// // the was proposed for insertion. +// sql.ResolveWithNewValues(), +// ). +// // Override some of the fields with custom +// // update values. +// Update(func(u *ent.ProjectUpsert) { +// SetName(v+v). +// }). +// Exec(ctx) +func (_c *ProjectCreateBulk) OnConflict(opts ...sql.ConflictOption) *ProjectUpsertBulk { + _c.conflict = opts + return &ProjectUpsertBulk{ + create: _c, + } +} + +// OnConflictColumns calls `OnConflict` and configures the columns +// as conflict target. Using this option is equivalent to using: +// +// client.Project.Create(). +// OnConflict(sql.ConflictColumns(columns...)). +// Exec(ctx) +func (_c *ProjectCreateBulk) OnConflictColumns(columns ...string) *ProjectUpsertBulk { + _c.conflict = append(_c.conflict, sql.ConflictColumns(columns...)) + return &ProjectUpsertBulk{ + create: _c, + } +} + +// ProjectUpsertBulk is the builder for "upsert"-ing +// a bulk of Project nodes. +type ProjectUpsertBulk struct { + create *ProjectCreateBulk +} + +// UpdateNewValues updates the mutable fields using the new values that +// were set on create. Using this option is equivalent to using: +// +// client.Project.Create(). +// OnConflict( +// sql.ResolveWithNewValues(), +// sql.ResolveWith(func(u *sql.UpdateSet) { +// u.SetIgnore(project.FieldID) +// }), +// ). +// Exec(ctx) +func (u *ProjectUpsertBulk) UpdateNewValues() *ProjectUpsertBulk { + u.create.conflict = append(u.create.conflict, sql.ResolveWithNewValues()) + u.create.conflict = append(u.create.conflict, sql.ResolveWith(func(s *sql.UpdateSet) { + for _, b := range u.create.builders { + if _, exists := b.mutation.ID(); exists { + s.SetIgnore(project.FieldID) + } + if _, exists := b.mutation.Created(); exists { + s.SetIgnore(project.FieldCreated) + } + } + })) + return u +} + +// Ignore sets each column to itself in case of conflict. +// Using this option is equivalent to using: +// +// client.Project.Create(). +// OnConflict(sql.ResolveWithIgnore()). +// Exec(ctx) +func (u *ProjectUpsertBulk) Ignore() *ProjectUpsertBulk { + u.create.conflict = append(u.create.conflict, sql.ResolveWithIgnore()) + return u +} + +// DoNothing configures the conflict_action to `DO NOTHING`. +// Supported only by SQLite and PostgreSQL. +func (u *ProjectUpsertBulk) DoNothing() *ProjectUpsertBulk { + u.create.conflict = append(u.create.conflict, sql.DoNothing()) + return u +} + +// Update allows overriding fields `UPDATE` values. See the ProjectCreateBulk.OnConflict +// documentation for more info. +func (u *ProjectUpsertBulk) Update(set func(*ProjectUpsert)) *ProjectUpsertBulk { + u.create.conflict = append(u.create.conflict, sql.ResolveWith(func(update *sql.UpdateSet) { + set(&ProjectUpsert{UpdateSet: update}) + })) + return u +} + +// SetName sets the "name" field. +func (u *ProjectUpsertBulk) SetName(v string) *ProjectUpsertBulk { + return u.Update(func(s *ProjectUpsert) { + s.SetName(v) + }) +} + +// UpdateName sets the "name" field to the value that was provided on create. +func (u *ProjectUpsertBulk) UpdateName() *ProjectUpsertBulk { + return u.Update(func(s *ProjectUpsert) { + s.UpdateName() + }) +} + +// SetSlug sets the "slug" field. +func (u *ProjectUpsertBulk) SetSlug(v string) *ProjectUpsertBulk { + return u.Update(func(s *ProjectUpsert) { + s.SetSlug(v) + }) +} + +// UpdateSlug sets the "slug" field to the value that was provided on create. +func (u *ProjectUpsertBulk) UpdateSlug() *ProjectUpsertBulk { + return u.Update(func(s *ProjectUpsert) { + s.UpdateSlug() + }) +} + +// SetGitRemote sets the "git_remote" field. +func (u *ProjectUpsertBulk) SetGitRemote(v string) *ProjectUpsertBulk { + return u.Update(func(s *ProjectUpsert) { + s.SetGitRemote(v) + }) +} + +// UpdateGitRemote sets the "git_remote" field to the value that was provided on create. +func (u *ProjectUpsertBulk) UpdateGitRemote() *ProjectUpsertBulk { + return u.Update(func(s *ProjectUpsert) { + s.UpdateGitRemote() + }) +} + +// ClearGitRemote clears the value of the "git_remote" field. +func (u *ProjectUpsertBulk) ClearGitRemote() *ProjectUpsertBulk { + return u.Update(func(s *ProjectUpsert) { + s.ClearGitRemote() + }) +} + +// SetDefaultRuntimeBrokerID sets the "default_runtime_broker_id" field. +func (u *ProjectUpsertBulk) SetDefaultRuntimeBrokerID(v string) *ProjectUpsertBulk { + return u.Update(func(s *ProjectUpsert) { + s.SetDefaultRuntimeBrokerID(v) + }) +} + +// UpdateDefaultRuntimeBrokerID sets the "default_runtime_broker_id" field to the value that was provided on create. +func (u *ProjectUpsertBulk) UpdateDefaultRuntimeBrokerID() *ProjectUpsertBulk { + return u.Update(func(s *ProjectUpsert) { + s.UpdateDefaultRuntimeBrokerID() + }) +} + +// ClearDefaultRuntimeBrokerID clears the value of the "default_runtime_broker_id" field. +func (u *ProjectUpsertBulk) ClearDefaultRuntimeBrokerID() *ProjectUpsertBulk { + return u.Update(func(s *ProjectUpsert) { + s.ClearDefaultRuntimeBrokerID() + }) +} + +// SetLabels sets the "labels" field. +func (u *ProjectUpsertBulk) SetLabels(v map[string]string) *ProjectUpsertBulk { + return u.Update(func(s *ProjectUpsert) { + s.SetLabels(v) + }) +} + +// UpdateLabels sets the "labels" field to the value that was provided on create. +func (u *ProjectUpsertBulk) UpdateLabels() *ProjectUpsertBulk { + return u.Update(func(s *ProjectUpsert) { + s.UpdateLabels() + }) +} + +// ClearLabels clears the value of the "labels" field. +func (u *ProjectUpsertBulk) ClearLabels() *ProjectUpsertBulk { + return u.Update(func(s *ProjectUpsert) { + s.ClearLabels() + }) +} + +// SetAnnotations sets the "annotations" field. +func (u *ProjectUpsertBulk) SetAnnotations(v map[string]string) *ProjectUpsertBulk { + return u.Update(func(s *ProjectUpsert) { + s.SetAnnotations(v) + }) +} + +// UpdateAnnotations sets the "annotations" field to the value that was provided on create. +func (u *ProjectUpsertBulk) UpdateAnnotations() *ProjectUpsertBulk { + return u.Update(func(s *ProjectUpsert) { + s.UpdateAnnotations() + }) +} + +// ClearAnnotations clears the value of the "annotations" field. +func (u *ProjectUpsertBulk) ClearAnnotations() *ProjectUpsertBulk { + return u.Update(func(s *ProjectUpsert) { + s.ClearAnnotations() + }) +} + +// SetSharedDirs sets the "shared_dirs" field. +func (u *ProjectUpsertBulk) SetSharedDirs(v string) *ProjectUpsertBulk { + return u.Update(func(s *ProjectUpsert) { + s.SetSharedDirs(v) + }) +} + +// UpdateSharedDirs sets the "shared_dirs" field to the value that was provided on create. +func (u *ProjectUpsertBulk) UpdateSharedDirs() *ProjectUpsertBulk { + return u.Update(func(s *ProjectUpsert) { + s.UpdateSharedDirs() + }) +} + +// ClearSharedDirs clears the value of the "shared_dirs" field. +func (u *ProjectUpsertBulk) ClearSharedDirs() *ProjectUpsertBulk { + return u.Update(func(s *ProjectUpsert) { + s.ClearSharedDirs() + }) +} + +// SetUpdated sets the "updated" field. +func (u *ProjectUpsertBulk) SetUpdated(v time.Time) *ProjectUpsertBulk { + return u.Update(func(s *ProjectUpsert) { + s.SetUpdated(v) + }) +} + +// UpdateUpdated sets the "updated" field to the value that was provided on create. +func (u *ProjectUpsertBulk) UpdateUpdated() *ProjectUpsertBulk { + return u.Update(func(s *ProjectUpsert) { + s.UpdateUpdated() + }) +} + +// SetCreatedBy sets the "created_by" field. +func (u *ProjectUpsertBulk) SetCreatedBy(v string) *ProjectUpsertBulk { + return u.Update(func(s *ProjectUpsert) { + s.SetCreatedBy(v) + }) +} + +// UpdateCreatedBy sets the "created_by" field to the value that was provided on create. +func (u *ProjectUpsertBulk) UpdateCreatedBy() *ProjectUpsertBulk { + return u.Update(func(s *ProjectUpsert) { + s.UpdateCreatedBy() + }) +} + +// ClearCreatedBy clears the value of the "created_by" field. +func (u *ProjectUpsertBulk) ClearCreatedBy() *ProjectUpsertBulk { + return u.Update(func(s *ProjectUpsert) { + s.ClearCreatedBy() + }) +} + +// SetOwnerID sets the "owner_id" field. +func (u *ProjectUpsertBulk) SetOwnerID(v string) *ProjectUpsertBulk { + return u.Update(func(s *ProjectUpsert) { + s.SetOwnerID(v) + }) +} + +// UpdateOwnerID sets the "owner_id" field to the value that was provided on create. +func (u *ProjectUpsertBulk) UpdateOwnerID() *ProjectUpsertBulk { + return u.Update(func(s *ProjectUpsert) { + s.UpdateOwnerID() + }) +} + +// ClearOwnerID clears the value of the "owner_id" field. +func (u *ProjectUpsertBulk) ClearOwnerID() *ProjectUpsertBulk { + return u.Update(func(s *ProjectUpsert) { + s.ClearOwnerID() + }) +} + +// SetVisibility sets the "visibility" field. +func (u *ProjectUpsertBulk) SetVisibility(v string) *ProjectUpsertBulk { + return u.Update(func(s *ProjectUpsert) { + s.SetVisibility(v) + }) +} + +// UpdateVisibility sets the "visibility" field to the value that was provided on create. +func (u *ProjectUpsertBulk) UpdateVisibility() *ProjectUpsertBulk { + return u.Update(func(s *ProjectUpsert) { + s.UpdateVisibility() + }) +} + +// SetGithubInstallationID sets the "github_installation_id" field. +func (u *ProjectUpsertBulk) SetGithubInstallationID(v int64) *ProjectUpsertBulk { + return u.Update(func(s *ProjectUpsert) { + s.SetGithubInstallationID(v) + }) +} + +// AddGithubInstallationID adds v to the "github_installation_id" field. +func (u *ProjectUpsertBulk) AddGithubInstallationID(v int64) *ProjectUpsertBulk { + return u.Update(func(s *ProjectUpsert) { + s.AddGithubInstallationID(v) + }) +} + +// UpdateGithubInstallationID sets the "github_installation_id" field to the value that was provided on create. +func (u *ProjectUpsertBulk) UpdateGithubInstallationID() *ProjectUpsertBulk { + return u.Update(func(s *ProjectUpsert) { + s.UpdateGithubInstallationID() + }) +} + +// ClearGithubInstallationID clears the value of the "github_installation_id" field. +func (u *ProjectUpsertBulk) ClearGithubInstallationID() *ProjectUpsertBulk { + return u.Update(func(s *ProjectUpsert) { + s.ClearGithubInstallationID() + }) +} + +// SetGithubPermissions sets the "github_permissions" field. +func (u *ProjectUpsertBulk) SetGithubPermissions(v string) *ProjectUpsertBulk { + return u.Update(func(s *ProjectUpsert) { + s.SetGithubPermissions(v) + }) +} + +// UpdateGithubPermissions sets the "github_permissions" field to the value that was provided on create. +func (u *ProjectUpsertBulk) UpdateGithubPermissions() *ProjectUpsertBulk { + return u.Update(func(s *ProjectUpsert) { + s.UpdateGithubPermissions() + }) +} + +// ClearGithubPermissions clears the value of the "github_permissions" field. +func (u *ProjectUpsertBulk) ClearGithubPermissions() *ProjectUpsertBulk { + return u.Update(func(s *ProjectUpsert) { + s.ClearGithubPermissions() + }) +} + +// SetGithubAppStatus sets the "github_app_status" field. +func (u *ProjectUpsertBulk) SetGithubAppStatus(v string) *ProjectUpsertBulk { + return u.Update(func(s *ProjectUpsert) { + s.SetGithubAppStatus(v) + }) +} + +// UpdateGithubAppStatus sets the "github_app_status" field to the value that was provided on create. +func (u *ProjectUpsertBulk) UpdateGithubAppStatus() *ProjectUpsertBulk { + return u.Update(func(s *ProjectUpsert) { + s.UpdateGithubAppStatus() + }) +} + +// ClearGithubAppStatus clears the value of the "github_app_status" field. +func (u *ProjectUpsertBulk) ClearGithubAppStatus() *ProjectUpsertBulk { + return u.Update(func(s *ProjectUpsert) { + s.ClearGithubAppStatus() + }) +} + +// SetGitIdentity sets the "git_identity" field. +func (u *ProjectUpsertBulk) SetGitIdentity(v string) *ProjectUpsertBulk { + return u.Update(func(s *ProjectUpsert) { + s.SetGitIdentity(v) + }) +} + +// UpdateGitIdentity sets the "git_identity" field to the value that was provided on create. +func (u *ProjectUpsertBulk) UpdateGitIdentity() *ProjectUpsertBulk { + return u.Update(func(s *ProjectUpsert) { + s.UpdateGitIdentity() + }) +} + +// ClearGitIdentity clears the value of the "git_identity" field. +func (u *ProjectUpsertBulk) ClearGitIdentity() *ProjectUpsertBulk { + return u.Update(func(s *ProjectUpsert) { + s.ClearGitIdentity() + }) +} + +// Exec executes the query. +func (u *ProjectUpsertBulk) Exec(ctx context.Context) error { + if u.create.err != nil { + return u.create.err + } + for i, b := range u.create.builders { + if len(b.conflict) != 0 { + return fmt.Errorf("ent: OnConflict was set for builder %d. Set it on the ProjectCreateBulk instead", i) + } + } + if len(u.create.conflict) == 0 { + return errors.New("ent: missing options for ProjectCreateBulk.OnConflict") + } + return u.create.Exec(ctx) +} + +// ExecX is like Exec, but panics if an error occurs. +func (u *ProjectUpsertBulk) ExecX(ctx context.Context) { + if err := u.create.Exec(ctx); err != nil { + panic(err) + } +} diff --git a/pkg/ent/project_query.go b/pkg/ent/project_query.go index 15f69e7be..d339a90eb 100644 --- a/pkg/ent/project_query.go +++ b/pkg/ent/project_query.go @@ -9,6 +9,7 @@ import ( "math" "entgo.io/ent" + "entgo.io/ent/dialect" "entgo.io/ent/dialect/sql" "entgo.io/ent/dialect/sql/sqlgraph" "entgo.io/ent/schema/field" @@ -26,6 +27,7 @@ type ProjectQuery struct { inters []Interceptor predicates []predicate.Project withAgents *AgentQuery + modifiers []func(*sql.Selector) // intermediate query (i.e. traversal path). sql *sql.Selector path func(context.Context) (*sql.Selector, error) @@ -385,6 +387,9 @@ func (_q *ProjectQuery) sqlAll(ctx context.Context, hooks ...queryHook) ([]*Proj node.Edges.loadedTypes = loadedTypes return node.assignValues(columns, values) } + if len(_q.modifiers) > 0 { + _spec.Modifiers = _q.modifiers + } for i := range hooks { hooks[i](ctx, _spec) } @@ -437,6 +442,9 @@ func (_q *ProjectQuery) loadAgents(ctx context.Context, query *AgentQuery, nodes func (_q *ProjectQuery) sqlCount(ctx context.Context) (int, error) { _spec := _q.querySpec() + if len(_q.modifiers) > 0 { + _spec.Modifiers = _q.modifiers + } _spec.Node.Columns = _q.ctx.Fields if len(_q.ctx.Fields) > 0 { _spec.Unique = _q.ctx.Unique != nil && *_q.ctx.Unique @@ -499,6 +507,9 @@ func (_q *ProjectQuery) sqlQuery(ctx context.Context) *sql.Selector { if _q.ctx.Unique != nil && *_q.ctx.Unique { selector.Distinct() } + for _, m := range _q.modifiers { + m(selector) + } for _, p := range _q.predicates { p(selector) } @@ -516,6 +527,32 @@ func (_q *ProjectQuery) sqlQuery(ctx context.Context) *sql.Selector { return selector } +// ForUpdate locks the selected rows against concurrent updates, and prevent them from being +// updated, deleted or "selected ... for update" by other sessions, until the transaction is +// either committed or rolled-back. +func (_q *ProjectQuery) ForUpdate(opts ...sql.LockOption) *ProjectQuery { + if _q.driver.Dialect() == dialect.Postgres { + _q.Unique(false) + } + _q.modifiers = append(_q.modifiers, func(s *sql.Selector) { + s.ForUpdate(opts...) + }) + return _q +} + +// ForShare behaves similarly to ForUpdate, except that it acquires a shared mode lock +// on any rows that are read. Other sessions can read the rows, but cannot modify them +// until your transaction commits. +func (_q *ProjectQuery) ForShare(opts ...sql.LockOption) *ProjectQuery { + if _q.driver.Dialect() == dialect.Postgres { + _q.Unique(false) + } + _q.modifiers = append(_q.modifiers, func(s *sql.Selector) { + s.ForShare(opts...) + }) + return _q +} + // ProjectGroupBy is the group-by builder for Project entities. type ProjectGroupBy struct { selector diff --git a/pkg/ent/project_update.go b/pkg/ent/project_update.go index 23bc242da..81ada5256 100644 --- a/pkg/ent/project_update.go +++ b/pkg/ent/project_update.go @@ -78,6 +78,26 @@ func (_u *ProjectUpdate) ClearGitRemote() *ProjectUpdate { return _u } +// SetDefaultRuntimeBrokerID sets the "default_runtime_broker_id" field. +func (_u *ProjectUpdate) SetDefaultRuntimeBrokerID(v string) *ProjectUpdate { + _u.mutation.SetDefaultRuntimeBrokerID(v) + return _u +} + +// SetNillableDefaultRuntimeBrokerID sets the "default_runtime_broker_id" field if the given value is not nil. +func (_u *ProjectUpdate) SetNillableDefaultRuntimeBrokerID(v *string) *ProjectUpdate { + if v != nil { + _u.SetDefaultRuntimeBrokerID(*v) + } + return _u +} + +// ClearDefaultRuntimeBrokerID clears the value of the "default_runtime_broker_id" field. +func (_u *ProjectUpdate) ClearDefaultRuntimeBrokerID() *ProjectUpdate { + _u.mutation.ClearDefaultRuntimeBrokerID() + return _u +} + // SetLabels sets the "labels" field. func (_u *ProjectUpdate) SetLabels(v map[string]string) *ProjectUpdate { _u.mutation.SetLabels(v) @@ -102,6 +122,26 @@ func (_u *ProjectUpdate) ClearAnnotations() *ProjectUpdate { return _u } +// SetSharedDirs sets the "shared_dirs" field. +func (_u *ProjectUpdate) SetSharedDirs(v string) *ProjectUpdate { + _u.mutation.SetSharedDirs(v) + return _u +} + +// SetNillableSharedDirs sets the "shared_dirs" field if the given value is not nil. +func (_u *ProjectUpdate) SetNillableSharedDirs(v *string) *ProjectUpdate { + if v != nil { + _u.SetSharedDirs(*v) + } + return _u +} + +// ClearSharedDirs clears the value of the "shared_dirs" field. +func (_u *ProjectUpdate) ClearSharedDirs() *ProjectUpdate { + _u.mutation.ClearSharedDirs() + return _u +} + // SetUpdated sets the "updated" field. func (_u *ProjectUpdate) SetUpdated(v time.Time) *ProjectUpdate { _u.mutation.SetUpdated(v) @@ -162,6 +202,93 @@ func (_u *ProjectUpdate) SetNillableVisibility(v *string) *ProjectUpdate { return _u } +// SetGithubInstallationID sets the "github_installation_id" field. +func (_u *ProjectUpdate) SetGithubInstallationID(v int64) *ProjectUpdate { + _u.mutation.ResetGithubInstallationID() + _u.mutation.SetGithubInstallationID(v) + return _u +} + +// SetNillableGithubInstallationID sets the "github_installation_id" field if the given value is not nil. +func (_u *ProjectUpdate) SetNillableGithubInstallationID(v *int64) *ProjectUpdate { + if v != nil { + _u.SetGithubInstallationID(*v) + } + return _u +} + +// AddGithubInstallationID adds value to the "github_installation_id" field. +func (_u *ProjectUpdate) AddGithubInstallationID(v int64) *ProjectUpdate { + _u.mutation.AddGithubInstallationID(v) + return _u +} + +// ClearGithubInstallationID clears the value of the "github_installation_id" field. +func (_u *ProjectUpdate) ClearGithubInstallationID() *ProjectUpdate { + _u.mutation.ClearGithubInstallationID() + return _u +} + +// SetGithubPermissions sets the "github_permissions" field. +func (_u *ProjectUpdate) SetGithubPermissions(v string) *ProjectUpdate { + _u.mutation.SetGithubPermissions(v) + return _u +} + +// SetNillableGithubPermissions sets the "github_permissions" field if the given value is not nil. +func (_u *ProjectUpdate) SetNillableGithubPermissions(v *string) *ProjectUpdate { + if v != nil { + _u.SetGithubPermissions(*v) + } + return _u +} + +// ClearGithubPermissions clears the value of the "github_permissions" field. +func (_u *ProjectUpdate) ClearGithubPermissions() *ProjectUpdate { + _u.mutation.ClearGithubPermissions() + return _u +} + +// SetGithubAppStatus sets the "github_app_status" field. +func (_u *ProjectUpdate) SetGithubAppStatus(v string) *ProjectUpdate { + _u.mutation.SetGithubAppStatus(v) + return _u +} + +// SetNillableGithubAppStatus sets the "github_app_status" field if the given value is not nil. +func (_u *ProjectUpdate) SetNillableGithubAppStatus(v *string) *ProjectUpdate { + if v != nil { + _u.SetGithubAppStatus(*v) + } + return _u +} + +// ClearGithubAppStatus clears the value of the "github_app_status" field. +func (_u *ProjectUpdate) ClearGithubAppStatus() *ProjectUpdate { + _u.mutation.ClearGithubAppStatus() + return _u +} + +// SetGitIdentity sets the "git_identity" field. +func (_u *ProjectUpdate) SetGitIdentity(v string) *ProjectUpdate { + _u.mutation.SetGitIdentity(v) + return _u +} + +// SetNillableGitIdentity sets the "git_identity" field if the given value is not nil. +func (_u *ProjectUpdate) SetNillableGitIdentity(v *string) *ProjectUpdate { + if v != nil { + _u.SetGitIdentity(*v) + } + return _u +} + +// ClearGitIdentity clears the value of the "git_identity" field. +func (_u *ProjectUpdate) ClearGitIdentity() *ProjectUpdate { + _u.mutation.ClearGitIdentity() + return _u +} + // AddAgentIDs adds the "agents" edge to the Agent entity by IDs. func (_u *ProjectUpdate) AddAgentIDs(ids ...uuid.UUID) *ProjectUpdate { _u.mutation.AddAgentIDs(ids...) @@ -278,6 +405,12 @@ func (_u *ProjectUpdate) sqlSave(ctx context.Context) (_node int, err error) { if _u.mutation.GitRemoteCleared() { _spec.ClearField(project.FieldGitRemote, field.TypeString) } + if value, ok := _u.mutation.DefaultRuntimeBrokerID(); ok { + _spec.SetField(project.FieldDefaultRuntimeBrokerID, field.TypeString, value) + } + if _u.mutation.DefaultRuntimeBrokerIDCleared() { + _spec.ClearField(project.FieldDefaultRuntimeBrokerID, field.TypeString) + } if value, ok := _u.mutation.Labels(); ok { _spec.SetField(project.FieldLabels, field.TypeJSON, value) } @@ -290,6 +423,12 @@ func (_u *ProjectUpdate) sqlSave(ctx context.Context) (_node int, err error) { if _u.mutation.AnnotationsCleared() { _spec.ClearField(project.FieldAnnotations, field.TypeJSON) } + if value, ok := _u.mutation.SharedDirs(); ok { + _spec.SetField(project.FieldSharedDirs, field.TypeString, value) + } + if _u.mutation.SharedDirsCleared() { + _spec.ClearField(project.FieldSharedDirs, field.TypeString) + } if value, ok := _u.mutation.Updated(); ok { _spec.SetField(project.FieldUpdated, field.TypeTime, value) } @@ -308,6 +447,33 @@ func (_u *ProjectUpdate) sqlSave(ctx context.Context) (_node int, err error) { if value, ok := _u.mutation.Visibility(); ok { _spec.SetField(project.FieldVisibility, field.TypeString, value) } + if value, ok := _u.mutation.GithubInstallationID(); ok { + _spec.SetField(project.FieldGithubInstallationID, field.TypeInt64, value) + } + if value, ok := _u.mutation.AddedGithubInstallationID(); ok { + _spec.AddField(project.FieldGithubInstallationID, field.TypeInt64, value) + } + if _u.mutation.GithubInstallationIDCleared() { + _spec.ClearField(project.FieldGithubInstallationID, field.TypeInt64) + } + if value, ok := _u.mutation.GithubPermissions(); ok { + _spec.SetField(project.FieldGithubPermissions, field.TypeString, value) + } + if _u.mutation.GithubPermissionsCleared() { + _spec.ClearField(project.FieldGithubPermissions, field.TypeString) + } + if value, ok := _u.mutation.GithubAppStatus(); ok { + _spec.SetField(project.FieldGithubAppStatus, field.TypeString, value) + } + if _u.mutation.GithubAppStatusCleared() { + _spec.ClearField(project.FieldGithubAppStatus, field.TypeString) + } + if value, ok := _u.mutation.GitIdentity(); ok { + _spec.SetField(project.FieldGitIdentity, field.TypeString, value) + } + if _u.mutation.GitIdentityCleared() { + _spec.ClearField(project.FieldGitIdentity, field.TypeString) + } if _u.mutation.AgentsCleared() { edge := &sqlgraph.EdgeSpec{ Rel: sqlgraph.O2M, @@ -421,6 +587,26 @@ func (_u *ProjectUpdateOne) ClearGitRemote() *ProjectUpdateOne { return _u } +// SetDefaultRuntimeBrokerID sets the "default_runtime_broker_id" field. +func (_u *ProjectUpdateOne) SetDefaultRuntimeBrokerID(v string) *ProjectUpdateOne { + _u.mutation.SetDefaultRuntimeBrokerID(v) + return _u +} + +// SetNillableDefaultRuntimeBrokerID sets the "default_runtime_broker_id" field if the given value is not nil. +func (_u *ProjectUpdateOne) SetNillableDefaultRuntimeBrokerID(v *string) *ProjectUpdateOne { + if v != nil { + _u.SetDefaultRuntimeBrokerID(*v) + } + return _u +} + +// ClearDefaultRuntimeBrokerID clears the value of the "default_runtime_broker_id" field. +func (_u *ProjectUpdateOne) ClearDefaultRuntimeBrokerID() *ProjectUpdateOne { + _u.mutation.ClearDefaultRuntimeBrokerID() + return _u +} + // SetLabels sets the "labels" field. func (_u *ProjectUpdateOne) SetLabels(v map[string]string) *ProjectUpdateOne { _u.mutation.SetLabels(v) @@ -445,6 +631,26 @@ func (_u *ProjectUpdateOne) ClearAnnotations() *ProjectUpdateOne { return _u } +// SetSharedDirs sets the "shared_dirs" field. +func (_u *ProjectUpdateOne) SetSharedDirs(v string) *ProjectUpdateOne { + _u.mutation.SetSharedDirs(v) + return _u +} + +// SetNillableSharedDirs sets the "shared_dirs" field if the given value is not nil. +func (_u *ProjectUpdateOne) SetNillableSharedDirs(v *string) *ProjectUpdateOne { + if v != nil { + _u.SetSharedDirs(*v) + } + return _u +} + +// ClearSharedDirs clears the value of the "shared_dirs" field. +func (_u *ProjectUpdateOne) ClearSharedDirs() *ProjectUpdateOne { + _u.mutation.ClearSharedDirs() + return _u +} + // SetUpdated sets the "updated" field. func (_u *ProjectUpdateOne) SetUpdated(v time.Time) *ProjectUpdateOne { _u.mutation.SetUpdated(v) @@ -505,6 +711,93 @@ func (_u *ProjectUpdateOne) SetNillableVisibility(v *string) *ProjectUpdateOne { return _u } +// SetGithubInstallationID sets the "github_installation_id" field. +func (_u *ProjectUpdateOne) SetGithubInstallationID(v int64) *ProjectUpdateOne { + _u.mutation.ResetGithubInstallationID() + _u.mutation.SetGithubInstallationID(v) + return _u +} + +// SetNillableGithubInstallationID sets the "github_installation_id" field if the given value is not nil. +func (_u *ProjectUpdateOne) SetNillableGithubInstallationID(v *int64) *ProjectUpdateOne { + if v != nil { + _u.SetGithubInstallationID(*v) + } + return _u +} + +// AddGithubInstallationID adds value to the "github_installation_id" field. +func (_u *ProjectUpdateOne) AddGithubInstallationID(v int64) *ProjectUpdateOne { + _u.mutation.AddGithubInstallationID(v) + return _u +} + +// ClearGithubInstallationID clears the value of the "github_installation_id" field. +func (_u *ProjectUpdateOne) ClearGithubInstallationID() *ProjectUpdateOne { + _u.mutation.ClearGithubInstallationID() + return _u +} + +// SetGithubPermissions sets the "github_permissions" field. +func (_u *ProjectUpdateOne) SetGithubPermissions(v string) *ProjectUpdateOne { + _u.mutation.SetGithubPermissions(v) + return _u +} + +// SetNillableGithubPermissions sets the "github_permissions" field if the given value is not nil. +func (_u *ProjectUpdateOne) SetNillableGithubPermissions(v *string) *ProjectUpdateOne { + if v != nil { + _u.SetGithubPermissions(*v) + } + return _u +} + +// ClearGithubPermissions clears the value of the "github_permissions" field. +func (_u *ProjectUpdateOne) ClearGithubPermissions() *ProjectUpdateOne { + _u.mutation.ClearGithubPermissions() + return _u +} + +// SetGithubAppStatus sets the "github_app_status" field. +func (_u *ProjectUpdateOne) SetGithubAppStatus(v string) *ProjectUpdateOne { + _u.mutation.SetGithubAppStatus(v) + return _u +} + +// SetNillableGithubAppStatus sets the "github_app_status" field if the given value is not nil. +func (_u *ProjectUpdateOne) SetNillableGithubAppStatus(v *string) *ProjectUpdateOne { + if v != nil { + _u.SetGithubAppStatus(*v) + } + return _u +} + +// ClearGithubAppStatus clears the value of the "github_app_status" field. +func (_u *ProjectUpdateOne) ClearGithubAppStatus() *ProjectUpdateOne { + _u.mutation.ClearGithubAppStatus() + return _u +} + +// SetGitIdentity sets the "git_identity" field. +func (_u *ProjectUpdateOne) SetGitIdentity(v string) *ProjectUpdateOne { + _u.mutation.SetGitIdentity(v) + return _u +} + +// SetNillableGitIdentity sets the "git_identity" field if the given value is not nil. +func (_u *ProjectUpdateOne) SetNillableGitIdentity(v *string) *ProjectUpdateOne { + if v != nil { + _u.SetGitIdentity(*v) + } + return _u +} + +// ClearGitIdentity clears the value of the "git_identity" field. +func (_u *ProjectUpdateOne) ClearGitIdentity() *ProjectUpdateOne { + _u.mutation.ClearGitIdentity() + return _u +} + // AddAgentIDs adds the "agents" edge to the Agent entity by IDs. func (_u *ProjectUpdateOne) AddAgentIDs(ids ...uuid.UUID) *ProjectUpdateOne { _u.mutation.AddAgentIDs(ids...) @@ -651,6 +944,12 @@ func (_u *ProjectUpdateOne) sqlSave(ctx context.Context) (_node *Project, err er if _u.mutation.GitRemoteCleared() { _spec.ClearField(project.FieldGitRemote, field.TypeString) } + if value, ok := _u.mutation.DefaultRuntimeBrokerID(); ok { + _spec.SetField(project.FieldDefaultRuntimeBrokerID, field.TypeString, value) + } + if _u.mutation.DefaultRuntimeBrokerIDCleared() { + _spec.ClearField(project.FieldDefaultRuntimeBrokerID, field.TypeString) + } if value, ok := _u.mutation.Labels(); ok { _spec.SetField(project.FieldLabels, field.TypeJSON, value) } @@ -663,6 +962,12 @@ func (_u *ProjectUpdateOne) sqlSave(ctx context.Context) (_node *Project, err er if _u.mutation.AnnotationsCleared() { _spec.ClearField(project.FieldAnnotations, field.TypeJSON) } + if value, ok := _u.mutation.SharedDirs(); ok { + _spec.SetField(project.FieldSharedDirs, field.TypeString, value) + } + if _u.mutation.SharedDirsCleared() { + _spec.ClearField(project.FieldSharedDirs, field.TypeString) + } if value, ok := _u.mutation.Updated(); ok { _spec.SetField(project.FieldUpdated, field.TypeTime, value) } @@ -681,6 +986,33 @@ func (_u *ProjectUpdateOne) sqlSave(ctx context.Context) (_node *Project, err er if value, ok := _u.mutation.Visibility(); ok { _spec.SetField(project.FieldVisibility, field.TypeString, value) } + if value, ok := _u.mutation.GithubInstallationID(); ok { + _spec.SetField(project.FieldGithubInstallationID, field.TypeInt64, value) + } + if value, ok := _u.mutation.AddedGithubInstallationID(); ok { + _spec.AddField(project.FieldGithubInstallationID, field.TypeInt64, value) + } + if _u.mutation.GithubInstallationIDCleared() { + _spec.ClearField(project.FieldGithubInstallationID, field.TypeInt64) + } + if value, ok := _u.mutation.GithubPermissions(); ok { + _spec.SetField(project.FieldGithubPermissions, field.TypeString, value) + } + if _u.mutation.GithubPermissionsCleared() { + _spec.ClearField(project.FieldGithubPermissions, field.TypeString) + } + if value, ok := _u.mutation.GithubAppStatus(); ok { + _spec.SetField(project.FieldGithubAppStatus, field.TypeString, value) + } + if _u.mutation.GithubAppStatusCleared() { + _spec.ClearField(project.FieldGithubAppStatus, field.TypeString) + } + if value, ok := _u.mutation.GitIdentity(); ok { + _spec.SetField(project.FieldGitIdentity, field.TypeString, value) + } + if _u.mutation.GitIdentityCleared() { + _spec.ClearField(project.FieldGitIdentity, field.TypeString) + } if _u.mutation.AgentsCleared() { edge := &sqlgraph.EdgeSpec{ Rel: sqlgraph.O2M, diff --git a/pkg/ent/projectcontributor.go b/pkg/ent/projectcontributor.go new file mode 100644 index 000000000..927f1b119 --- /dev/null +++ b/pkg/ent/projectcontributor.go @@ -0,0 +1,212 @@ +// Code generated by ent, DO NOT EDIT. + +package ent + +import ( + "fmt" + "strings" + "time" + + "entgo.io/ent" + "entgo.io/ent/dialect/sql" + "github.com/GoogleCloudPlatform/scion/pkg/ent/projectcontributor" + "github.com/google/uuid" +) + +// ProjectContributor is the model entity for the ProjectContributor schema. +type ProjectContributor struct { + config `json:"-"` + // ID of the ent. + ID uuid.UUID `json:"id,omitempty"` + // ProjectID holds the value of the "project_id" field. + ProjectID uuid.UUID `json:"project_id,omitempty"` + // BrokerID holds the value of the "broker_id" field. + BrokerID uuid.UUID `json:"broker_id,omitempty"` + // BrokerName holds the value of the "broker_name" field. + BrokerName string `json:"broker_name,omitempty"` + // Mode holds the value of the "mode" field. + Mode string `json:"mode,omitempty"` + // Status holds the value of the "status" field. + Status string `json:"status,omitempty"` + // Profiles holds the value of the "profiles" field. + Profiles string `json:"profiles,omitempty"` + // LastSeen holds the value of the "last_seen" field. + LastSeen *time.Time `json:"last_seen,omitempty"` + // LocalPath holds the value of the "local_path" field. + LocalPath string `json:"local_path,omitempty"` + // LinkedBy holds the value of the "linked_by" field. + LinkedBy string `json:"linked_by,omitempty"` + // LinkedAt holds the value of the "linked_at" field. + LinkedAt *time.Time `json:"linked_at,omitempty"` + selectValues sql.SelectValues +} + +// scanValues returns the types for scanning values from sql.Rows. +func (*ProjectContributor) scanValues(columns []string) ([]any, error) { + values := make([]any, len(columns)) + for i := range columns { + switch columns[i] { + case projectcontributor.FieldBrokerName, projectcontributor.FieldMode, projectcontributor.FieldStatus, projectcontributor.FieldProfiles, projectcontributor.FieldLocalPath, projectcontributor.FieldLinkedBy: + values[i] = new(sql.NullString) + case projectcontributor.FieldLastSeen, projectcontributor.FieldLinkedAt: + values[i] = new(sql.NullTime) + case projectcontributor.FieldID, projectcontributor.FieldProjectID, projectcontributor.FieldBrokerID: + values[i] = new(uuid.UUID) + default: + values[i] = new(sql.UnknownType) + } + } + return values, nil +} + +// assignValues assigns the values that were returned from sql.Rows (after scanning) +// to the ProjectContributor fields. +func (_m *ProjectContributor) assignValues(columns []string, values []any) error { + if m, n := len(values), len(columns); m < n { + return fmt.Errorf("mismatch number of scan values: %d != %d", m, n) + } + for i := range columns { + switch columns[i] { + case projectcontributor.FieldID: + if value, ok := values[i].(*uuid.UUID); !ok { + return fmt.Errorf("unexpected type %T for field id", values[i]) + } else if value != nil { + _m.ID = *value + } + case projectcontributor.FieldProjectID: + if value, ok := values[i].(*uuid.UUID); !ok { + return fmt.Errorf("unexpected type %T for field project_id", values[i]) + } else if value != nil { + _m.ProjectID = *value + } + case projectcontributor.FieldBrokerID: + if value, ok := values[i].(*uuid.UUID); !ok { + return fmt.Errorf("unexpected type %T for field broker_id", values[i]) + } else if value != nil { + _m.BrokerID = *value + } + case projectcontributor.FieldBrokerName: + if value, ok := values[i].(*sql.NullString); !ok { + return fmt.Errorf("unexpected type %T for field broker_name", values[i]) + } else if value.Valid { + _m.BrokerName = value.String + } + case projectcontributor.FieldMode: + if value, ok := values[i].(*sql.NullString); !ok { + return fmt.Errorf("unexpected type %T for field mode", values[i]) + } else if value.Valid { + _m.Mode = value.String + } + case projectcontributor.FieldStatus: + if value, ok := values[i].(*sql.NullString); !ok { + return fmt.Errorf("unexpected type %T for field status", values[i]) + } else if value.Valid { + _m.Status = value.String + } + case projectcontributor.FieldProfiles: + if value, ok := values[i].(*sql.NullString); !ok { + return fmt.Errorf("unexpected type %T for field profiles", values[i]) + } else if value.Valid { + _m.Profiles = value.String + } + case projectcontributor.FieldLastSeen: + if value, ok := values[i].(*sql.NullTime); !ok { + return fmt.Errorf("unexpected type %T for field last_seen", values[i]) + } else if value.Valid { + _m.LastSeen = new(time.Time) + *_m.LastSeen = value.Time + } + case projectcontributor.FieldLocalPath: + if value, ok := values[i].(*sql.NullString); !ok { + return fmt.Errorf("unexpected type %T for field local_path", values[i]) + } else if value.Valid { + _m.LocalPath = value.String + } + case projectcontributor.FieldLinkedBy: + if value, ok := values[i].(*sql.NullString); !ok { + return fmt.Errorf("unexpected type %T for field linked_by", values[i]) + } else if value.Valid { + _m.LinkedBy = value.String + } + case projectcontributor.FieldLinkedAt: + if value, ok := values[i].(*sql.NullTime); !ok { + return fmt.Errorf("unexpected type %T for field linked_at", values[i]) + } else if value.Valid { + _m.LinkedAt = new(time.Time) + *_m.LinkedAt = value.Time + } + default: + _m.selectValues.Set(columns[i], values[i]) + } + } + return nil +} + +// Value returns the ent.Value that was dynamically selected and assigned to the ProjectContributor. +// This includes values selected through modifiers, order, etc. +func (_m *ProjectContributor) Value(name string) (ent.Value, error) { + return _m.selectValues.Get(name) +} + +// Update returns a builder for updating this ProjectContributor. +// Note that you need to call ProjectContributor.Unwrap() before calling this method if this ProjectContributor +// was returned from a transaction, and the transaction was committed or rolled back. +func (_m *ProjectContributor) Update() *ProjectContributorUpdateOne { + return NewProjectContributorClient(_m.config).UpdateOne(_m) +} + +// Unwrap unwraps the ProjectContributor entity that was returned from a transaction after it was closed, +// so that all future queries will be executed through the driver which created the transaction. +func (_m *ProjectContributor) Unwrap() *ProjectContributor { + _tx, ok := _m.config.driver.(*txDriver) + if !ok { + panic("ent: ProjectContributor is not a transactional entity") + } + _m.config.driver = _tx.drv + return _m +} + +// String implements the fmt.Stringer. +func (_m *ProjectContributor) String() string { + var builder strings.Builder + builder.WriteString("ProjectContributor(") + builder.WriteString(fmt.Sprintf("id=%v, ", _m.ID)) + builder.WriteString("project_id=") + builder.WriteString(fmt.Sprintf("%v", _m.ProjectID)) + builder.WriteString(", ") + builder.WriteString("broker_id=") + builder.WriteString(fmt.Sprintf("%v", _m.BrokerID)) + builder.WriteString(", ") + builder.WriteString("broker_name=") + builder.WriteString(_m.BrokerName) + builder.WriteString(", ") + builder.WriteString("mode=") + builder.WriteString(_m.Mode) + builder.WriteString(", ") + builder.WriteString("status=") + builder.WriteString(_m.Status) + builder.WriteString(", ") + builder.WriteString("profiles=") + builder.WriteString(_m.Profiles) + builder.WriteString(", ") + if v := _m.LastSeen; v != nil { + builder.WriteString("last_seen=") + builder.WriteString(v.Format(time.ANSIC)) + } + builder.WriteString(", ") + builder.WriteString("local_path=") + builder.WriteString(_m.LocalPath) + builder.WriteString(", ") + builder.WriteString("linked_by=") + builder.WriteString(_m.LinkedBy) + builder.WriteString(", ") + if v := _m.LinkedAt; v != nil { + builder.WriteString("linked_at=") + builder.WriteString(v.Format(time.ANSIC)) + } + builder.WriteByte(')') + return builder.String() +} + +// ProjectContributors is a parsable slice of ProjectContributor. +type ProjectContributors []*ProjectContributor diff --git a/pkg/ent/projectcontributor/projectcontributor.go b/pkg/ent/projectcontributor/projectcontributor.go new file mode 100644 index 000000000..3e21bfd64 --- /dev/null +++ b/pkg/ent/projectcontributor/projectcontributor.go @@ -0,0 +1,131 @@ +// Code generated by ent, DO NOT EDIT. + +package projectcontributor + +import ( + "entgo.io/ent/dialect/sql" + "github.com/google/uuid" +) + +const ( + // Label holds the string label denoting the projectcontributor type in the database. + Label = "project_contributor" + // FieldID holds the string denoting the id field in the database. + FieldID = "id" + // FieldProjectID holds the string denoting the project_id field in the database. + FieldProjectID = "project_id" + // FieldBrokerID holds the string denoting the broker_id field in the database. + FieldBrokerID = "broker_id" + // FieldBrokerName holds the string denoting the broker_name field in the database. + FieldBrokerName = "broker_name" + // FieldMode holds the string denoting the mode field in the database. + FieldMode = "mode" + // FieldStatus holds the string denoting the status field in the database. + FieldStatus = "status" + // FieldProfiles holds the string denoting the profiles field in the database. + FieldProfiles = "profiles" + // FieldLastSeen holds the string denoting the last_seen field in the database. + FieldLastSeen = "last_seen" + // FieldLocalPath holds the string denoting the local_path field in the database. + FieldLocalPath = "local_path" + // FieldLinkedBy holds the string denoting the linked_by field in the database. + FieldLinkedBy = "linked_by" + // FieldLinkedAt holds the string denoting the linked_at field in the database. + FieldLinkedAt = "linked_at" + // Table holds the table name of the projectcontributor in the database. + Table = "project_contributors" +) + +// Columns holds all SQL columns for projectcontributor fields. +var Columns = []string{ + FieldID, + FieldProjectID, + FieldBrokerID, + FieldBrokerName, + FieldMode, + FieldStatus, + FieldProfiles, + FieldLastSeen, + FieldLocalPath, + FieldLinkedBy, + FieldLinkedAt, +} + +// ValidColumn reports if the column name is valid (part of the table columns). +func ValidColumn(column string) bool { + for i := range Columns { + if column == Columns[i] { + return true + } + } + return false +} + +var ( + // BrokerNameValidator is a validator for the "broker_name" field. It is called by the builders before save. + BrokerNameValidator func(string) error + // DefaultMode holds the default value on creation for the "mode" field. + DefaultMode string + // DefaultStatus holds the default value on creation for the "status" field. + DefaultStatus string + // DefaultID holds the default value on creation for the "id" field. + DefaultID func() uuid.UUID +) + +// OrderOption defines the ordering options for the ProjectContributor queries. +type OrderOption func(*sql.Selector) + +// ByID orders the results by the id field. +func ByID(opts ...sql.OrderTermOption) OrderOption { + return sql.OrderByField(FieldID, opts...).ToFunc() +} + +// ByProjectID orders the results by the project_id field. +func ByProjectID(opts ...sql.OrderTermOption) OrderOption { + return sql.OrderByField(FieldProjectID, opts...).ToFunc() +} + +// ByBrokerID orders the results by the broker_id field. +func ByBrokerID(opts ...sql.OrderTermOption) OrderOption { + return sql.OrderByField(FieldBrokerID, opts...).ToFunc() +} + +// ByBrokerName orders the results by the broker_name field. +func ByBrokerName(opts ...sql.OrderTermOption) OrderOption { + return sql.OrderByField(FieldBrokerName, opts...).ToFunc() +} + +// ByMode orders the results by the mode field. +func ByMode(opts ...sql.OrderTermOption) OrderOption { + return sql.OrderByField(FieldMode, opts...).ToFunc() +} + +// ByStatus orders the results by the status field. +func ByStatus(opts ...sql.OrderTermOption) OrderOption { + return sql.OrderByField(FieldStatus, opts...).ToFunc() +} + +// ByProfiles orders the results by the profiles field. +func ByProfiles(opts ...sql.OrderTermOption) OrderOption { + return sql.OrderByField(FieldProfiles, opts...).ToFunc() +} + +// ByLastSeen orders the results by the last_seen field. +func ByLastSeen(opts ...sql.OrderTermOption) OrderOption { + return sql.OrderByField(FieldLastSeen, opts...).ToFunc() +} + +// ByLocalPath orders the results by the local_path field. +func ByLocalPath(opts ...sql.OrderTermOption) OrderOption { + return sql.OrderByField(FieldLocalPath, opts...).ToFunc() +} + +// ByLinkedBy orders the results by the linked_by field. +func ByLinkedBy(opts ...sql.OrderTermOption) OrderOption { + return sql.OrderByField(FieldLinkedBy, opts...).ToFunc() +} + +// ByLinkedAt orders the results by the linked_at field. +func ByLinkedAt(opts ...sql.OrderTermOption) OrderOption { + return sql.OrderByField(FieldLinkedAt, opts...).ToFunc() +} diff --git a/pkg/ent/projectcontributor/where.go b/pkg/ent/projectcontributor/where.go new file mode 100644 index 000000000..d34c16ab5 --- /dev/null +++ b/pkg/ent/projectcontributor/where.go @@ -0,0 +1,721 @@ +// Code generated by ent, DO NOT EDIT. + +package projectcontributor + +import ( + "time" + + "entgo.io/ent/dialect/sql" + "github.com/GoogleCloudPlatform/scion/pkg/ent/predicate" + "github.com/google/uuid" +) + +// ID filters vertices based on their ID field. +func ID(id uuid.UUID) predicate.ProjectContributor { + return predicate.ProjectContributor(sql.FieldEQ(FieldID, id)) +} + +// IDEQ applies the EQ predicate on the ID field. +func IDEQ(id uuid.UUID) predicate.ProjectContributor { + return predicate.ProjectContributor(sql.FieldEQ(FieldID, id)) +} + +// IDNEQ applies the NEQ predicate on the ID field. +func IDNEQ(id uuid.UUID) predicate.ProjectContributor { + return predicate.ProjectContributor(sql.FieldNEQ(FieldID, id)) +} + +// IDIn applies the In predicate on the ID field. +func IDIn(ids ...uuid.UUID) predicate.ProjectContributor { + return predicate.ProjectContributor(sql.FieldIn(FieldID, ids...)) +} + +// IDNotIn applies the NotIn predicate on the ID field. +func IDNotIn(ids ...uuid.UUID) predicate.ProjectContributor { + return predicate.ProjectContributor(sql.FieldNotIn(FieldID, ids...)) +} + +// IDGT applies the GT predicate on the ID field. +func IDGT(id uuid.UUID) predicate.ProjectContributor { + return predicate.ProjectContributor(sql.FieldGT(FieldID, id)) +} + +// IDGTE applies the GTE predicate on the ID field. +func IDGTE(id uuid.UUID) predicate.ProjectContributor { + return predicate.ProjectContributor(sql.FieldGTE(FieldID, id)) +} + +// IDLT applies the LT predicate on the ID field. +func IDLT(id uuid.UUID) predicate.ProjectContributor { + return predicate.ProjectContributor(sql.FieldLT(FieldID, id)) +} + +// IDLTE applies the LTE predicate on the ID field. +func IDLTE(id uuid.UUID) predicate.ProjectContributor { + return predicate.ProjectContributor(sql.FieldLTE(FieldID, id)) +} + +// ProjectID applies equality check predicate on the "project_id" field. It's identical to ProjectIDEQ. +func ProjectID(v uuid.UUID) predicate.ProjectContributor { + return predicate.ProjectContributor(sql.FieldEQ(FieldProjectID, v)) +} + +// BrokerID applies equality check predicate on the "broker_id" field. It's identical to BrokerIDEQ. +func BrokerID(v uuid.UUID) predicate.ProjectContributor { + return predicate.ProjectContributor(sql.FieldEQ(FieldBrokerID, v)) +} + +// BrokerName applies equality check predicate on the "broker_name" field. It's identical to BrokerNameEQ. +func BrokerName(v string) predicate.ProjectContributor { + return predicate.ProjectContributor(sql.FieldEQ(FieldBrokerName, v)) +} + +// Mode applies equality check predicate on the "mode" field. It's identical to ModeEQ. +func Mode(v string) predicate.ProjectContributor { + return predicate.ProjectContributor(sql.FieldEQ(FieldMode, v)) +} + +// Status applies equality check predicate on the "status" field. It's identical to StatusEQ. +func Status(v string) predicate.ProjectContributor { + return predicate.ProjectContributor(sql.FieldEQ(FieldStatus, v)) +} + +// Profiles applies equality check predicate on the "profiles" field. It's identical to ProfilesEQ. +func Profiles(v string) predicate.ProjectContributor { + return predicate.ProjectContributor(sql.FieldEQ(FieldProfiles, v)) +} + +// LastSeen applies equality check predicate on the "last_seen" field. It's identical to LastSeenEQ. +func LastSeen(v time.Time) predicate.ProjectContributor { + return predicate.ProjectContributor(sql.FieldEQ(FieldLastSeen, v)) +} + +// LocalPath applies equality check predicate on the "local_path" field. It's identical to LocalPathEQ. +func LocalPath(v string) predicate.ProjectContributor { + return predicate.ProjectContributor(sql.FieldEQ(FieldLocalPath, v)) +} + +// LinkedBy applies equality check predicate on the "linked_by" field. It's identical to LinkedByEQ. +func LinkedBy(v string) predicate.ProjectContributor { + return predicate.ProjectContributor(sql.FieldEQ(FieldLinkedBy, v)) +} + +// LinkedAt applies equality check predicate on the "linked_at" field. It's identical to LinkedAtEQ. +func LinkedAt(v time.Time) predicate.ProjectContributor { + return predicate.ProjectContributor(sql.FieldEQ(FieldLinkedAt, v)) +} + +// ProjectIDEQ applies the EQ predicate on the "project_id" field. +func ProjectIDEQ(v uuid.UUID) predicate.ProjectContributor { + return predicate.ProjectContributor(sql.FieldEQ(FieldProjectID, v)) +} + +// ProjectIDNEQ applies the NEQ predicate on the "project_id" field. +func ProjectIDNEQ(v uuid.UUID) predicate.ProjectContributor { + return predicate.ProjectContributor(sql.FieldNEQ(FieldProjectID, v)) +} + +// ProjectIDIn applies the In predicate on the "project_id" field. +func ProjectIDIn(vs ...uuid.UUID) predicate.ProjectContributor { + return predicate.ProjectContributor(sql.FieldIn(FieldProjectID, vs...)) +} + +// ProjectIDNotIn applies the NotIn predicate on the "project_id" field. +func ProjectIDNotIn(vs ...uuid.UUID) predicate.ProjectContributor { + return predicate.ProjectContributor(sql.FieldNotIn(FieldProjectID, vs...)) +} + +// ProjectIDGT applies the GT predicate on the "project_id" field. +func ProjectIDGT(v uuid.UUID) predicate.ProjectContributor { + return predicate.ProjectContributor(sql.FieldGT(FieldProjectID, v)) +} + +// ProjectIDGTE applies the GTE predicate on the "project_id" field. +func ProjectIDGTE(v uuid.UUID) predicate.ProjectContributor { + return predicate.ProjectContributor(sql.FieldGTE(FieldProjectID, v)) +} + +// ProjectIDLT applies the LT predicate on the "project_id" field. +func ProjectIDLT(v uuid.UUID) predicate.ProjectContributor { + return predicate.ProjectContributor(sql.FieldLT(FieldProjectID, v)) +} + +// ProjectIDLTE applies the LTE predicate on the "project_id" field. +func ProjectIDLTE(v uuid.UUID) predicate.ProjectContributor { + return predicate.ProjectContributor(sql.FieldLTE(FieldProjectID, v)) +} + +// BrokerIDEQ applies the EQ predicate on the "broker_id" field. +func BrokerIDEQ(v uuid.UUID) predicate.ProjectContributor { + return predicate.ProjectContributor(sql.FieldEQ(FieldBrokerID, v)) +} + +// BrokerIDNEQ applies the NEQ predicate on the "broker_id" field. +func BrokerIDNEQ(v uuid.UUID) predicate.ProjectContributor { + return predicate.ProjectContributor(sql.FieldNEQ(FieldBrokerID, v)) +} + +// BrokerIDIn applies the In predicate on the "broker_id" field. +func BrokerIDIn(vs ...uuid.UUID) predicate.ProjectContributor { + return predicate.ProjectContributor(sql.FieldIn(FieldBrokerID, vs...)) +} + +// BrokerIDNotIn applies the NotIn predicate on the "broker_id" field. +func BrokerIDNotIn(vs ...uuid.UUID) predicate.ProjectContributor { + return predicate.ProjectContributor(sql.FieldNotIn(FieldBrokerID, vs...)) +} + +// BrokerIDGT applies the GT predicate on the "broker_id" field. +func BrokerIDGT(v uuid.UUID) predicate.ProjectContributor { + return predicate.ProjectContributor(sql.FieldGT(FieldBrokerID, v)) +} + +// BrokerIDGTE applies the GTE predicate on the "broker_id" field. +func BrokerIDGTE(v uuid.UUID) predicate.ProjectContributor { + return predicate.ProjectContributor(sql.FieldGTE(FieldBrokerID, v)) +} + +// BrokerIDLT applies the LT predicate on the "broker_id" field. +func BrokerIDLT(v uuid.UUID) predicate.ProjectContributor { + return predicate.ProjectContributor(sql.FieldLT(FieldBrokerID, v)) +} + +// BrokerIDLTE applies the LTE predicate on the "broker_id" field. +func BrokerIDLTE(v uuid.UUID) predicate.ProjectContributor { + return predicate.ProjectContributor(sql.FieldLTE(FieldBrokerID, v)) +} + +// BrokerNameEQ applies the EQ predicate on the "broker_name" field. +func BrokerNameEQ(v string) predicate.ProjectContributor { + return predicate.ProjectContributor(sql.FieldEQ(FieldBrokerName, v)) +} + +// BrokerNameNEQ applies the NEQ predicate on the "broker_name" field. +func BrokerNameNEQ(v string) predicate.ProjectContributor { + return predicate.ProjectContributor(sql.FieldNEQ(FieldBrokerName, v)) +} + +// BrokerNameIn applies the In predicate on the "broker_name" field. +func BrokerNameIn(vs ...string) predicate.ProjectContributor { + return predicate.ProjectContributor(sql.FieldIn(FieldBrokerName, vs...)) +} + +// BrokerNameNotIn applies the NotIn predicate on the "broker_name" field. +func BrokerNameNotIn(vs ...string) predicate.ProjectContributor { + return predicate.ProjectContributor(sql.FieldNotIn(FieldBrokerName, vs...)) +} + +// BrokerNameGT applies the GT predicate on the "broker_name" field. +func BrokerNameGT(v string) predicate.ProjectContributor { + return predicate.ProjectContributor(sql.FieldGT(FieldBrokerName, v)) +} + +// BrokerNameGTE applies the GTE predicate on the "broker_name" field. +func BrokerNameGTE(v string) predicate.ProjectContributor { + return predicate.ProjectContributor(sql.FieldGTE(FieldBrokerName, v)) +} + +// BrokerNameLT applies the LT predicate on the "broker_name" field. +func BrokerNameLT(v string) predicate.ProjectContributor { + return predicate.ProjectContributor(sql.FieldLT(FieldBrokerName, v)) +} + +// BrokerNameLTE applies the LTE predicate on the "broker_name" field. +func BrokerNameLTE(v string) predicate.ProjectContributor { + return predicate.ProjectContributor(sql.FieldLTE(FieldBrokerName, v)) +} + +// BrokerNameContains applies the Contains predicate on the "broker_name" field. +func BrokerNameContains(v string) predicate.ProjectContributor { + return predicate.ProjectContributor(sql.FieldContains(FieldBrokerName, v)) +} + +// BrokerNameHasPrefix applies the HasPrefix predicate on the "broker_name" field. +func BrokerNameHasPrefix(v string) predicate.ProjectContributor { + return predicate.ProjectContributor(sql.FieldHasPrefix(FieldBrokerName, v)) +} + +// BrokerNameHasSuffix applies the HasSuffix predicate on the "broker_name" field. +func BrokerNameHasSuffix(v string) predicate.ProjectContributor { + return predicate.ProjectContributor(sql.FieldHasSuffix(FieldBrokerName, v)) +} + +// BrokerNameEqualFold applies the EqualFold predicate on the "broker_name" field. +func BrokerNameEqualFold(v string) predicate.ProjectContributor { + return predicate.ProjectContributor(sql.FieldEqualFold(FieldBrokerName, v)) +} + +// BrokerNameContainsFold applies the ContainsFold predicate on the "broker_name" field. +func BrokerNameContainsFold(v string) predicate.ProjectContributor { + return predicate.ProjectContributor(sql.FieldContainsFold(FieldBrokerName, v)) +} + +// ModeEQ applies the EQ predicate on the "mode" field. +func ModeEQ(v string) predicate.ProjectContributor { + return predicate.ProjectContributor(sql.FieldEQ(FieldMode, v)) +} + +// ModeNEQ applies the NEQ predicate on the "mode" field. +func ModeNEQ(v string) predicate.ProjectContributor { + return predicate.ProjectContributor(sql.FieldNEQ(FieldMode, v)) +} + +// ModeIn applies the In predicate on the "mode" field. +func ModeIn(vs ...string) predicate.ProjectContributor { + return predicate.ProjectContributor(sql.FieldIn(FieldMode, vs...)) +} + +// ModeNotIn applies the NotIn predicate on the "mode" field. +func ModeNotIn(vs ...string) predicate.ProjectContributor { + return predicate.ProjectContributor(sql.FieldNotIn(FieldMode, vs...)) +} + +// ModeGT applies the GT predicate on the "mode" field. +func ModeGT(v string) predicate.ProjectContributor { + return predicate.ProjectContributor(sql.FieldGT(FieldMode, v)) +} + +// ModeGTE applies the GTE predicate on the "mode" field. +func ModeGTE(v string) predicate.ProjectContributor { + return predicate.ProjectContributor(sql.FieldGTE(FieldMode, v)) +} + +// ModeLT applies the LT predicate on the "mode" field. +func ModeLT(v string) predicate.ProjectContributor { + return predicate.ProjectContributor(sql.FieldLT(FieldMode, v)) +} + +// ModeLTE applies the LTE predicate on the "mode" field. +func ModeLTE(v string) predicate.ProjectContributor { + return predicate.ProjectContributor(sql.FieldLTE(FieldMode, v)) +} + +// ModeContains applies the Contains predicate on the "mode" field. +func ModeContains(v string) predicate.ProjectContributor { + return predicate.ProjectContributor(sql.FieldContains(FieldMode, v)) +} + +// ModeHasPrefix applies the HasPrefix predicate on the "mode" field. +func ModeHasPrefix(v string) predicate.ProjectContributor { + return predicate.ProjectContributor(sql.FieldHasPrefix(FieldMode, v)) +} + +// ModeHasSuffix applies the HasSuffix predicate on the "mode" field. +func ModeHasSuffix(v string) predicate.ProjectContributor { + return predicate.ProjectContributor(sql.FieldHasSuffix(FieldMode, v)) +} + +// ModeEqualFold applies the EqualFold predicate on the "mode" field. +func ModeEqualFold(v string) predicate.ProjectContributor { + return predicate.ProjectContributor(sql.FieldEqualFold(FieldMode, v)) +} + +// ModeContainsFold applies the ContainsFold predicate on the "mode" field. +func ModeContainsFold(v string) predicate.ProjectContributor { + return predicate.ProjectContributor(sql.FieldContainsFold(FieldMode, v)) +} + +// StatusEQ applies the EQ predicate on the "status" field. +func StatusEQ(v string) predicate.ProjectContributor { + return predicate.ProjectContributor(sql.FieldEQ(FieldStatus, v)) +} + +// StatusNEQ applies the NEQ predicate on the "status" field. +func StatusNEQ(v string) predicate.ProjectContributor { + return predicate.ProjectContributor(sql.FieldNEQ(FieldStatus, v)) +} + +// StatusIn applies the In predicate on the "status" field. +func StatusIn(vs ...string) predicate.ProjectContributor { + return predicate.ProjectContributor(sql.FieldIn(FieldStatus, vs...)) +} + +// StatusNotIn applies the NotIn predicate on the "status" field. +func StatusNotIn(vs ...string) predicate.ProjectContributor { + return predicate.ProjectContributor(sql.FieldNotIn(FieldStatus, vs...)) +} + +// StatusGT applies the GT predicate on the "status" field. +func StatusGT(v string) predicate.ProjectContributor { + return predicate.ProjectContributor(sql.FieldGT(FieldStatus, v)) +} + +// StatusGTE applies the GTE predicate on the "status" field. +func StatusGTE(v string) predicate.ProjectContributor { + return predicate.ProjectContributor(sql.FieldGTE(FieldStatus, v)) +} + +// StatusLT applies the LT predicate on the "status" field. +func StatusLT(v string) predicate.ProjectContributor { + return predicate.ProjectContributor(sql.FieldLT(FieldStatus, v)) +} + +// StatusLTE applies the LTE predicate on the "status" field. +func StatusLTE(v string) predicate.ProjectContributor { + return predicate.ProjectContributor(sql.FieldLTE(FieldStatus, v)) +} + +// StatusContains applies the Contains predicate on the "status" field. +func StatusContains(v string) predicate.ProjectContributor { + return predicate.ProjectContributor(sql.FieldContains(FieldStatus, v)) +} + +// StatusHasPrefix applies the HasPrefix predicate on the "status" field. +func StatusHasPrefix(v string) predicate.ProjectContributor { + return predicate.ProjectContributor(sql.FieldHasPrefix(FieldStatus, v)) +} + +// StatusHasSuffix applies the HasSuffix predicate on the "status" field. +func StatusHasSuffix(v string) predicate.ProjectContributor { + return predicate.ProjectContributor(sql.FieldHasSuffix(FieldStatus, v)) +} + +// StatusEqualFold applies the EqualFold predicate on the "status" field. +func StatusEqualFold(v string) predicate.ProjectContributor { + return predicate.ProjectContributor(sql.FieldEqualFold(FieldStatus, v)) +} + +// StatusContainsFold applies the ContainsFold predicate on the "status" field. +func StatusContainsFold(v string) predicate.ProjectContributor { + return predicate.ProjectContributor(sql.FieldContainsFold(FieldStatus, v)) +} + +// ProfilesEQ applies the EQ predicate on the "profiles" field. +func ProfilesEQ(v string) predicate.ProjectContributor { + return predicate.ProjectContributor(sql.FieldEQ(FieldProfiles, v)) +} + +// ProfilesNEQ applies the NEQ predicate on the "profiles" field. +func ProfilesNEQ(v string) predicate.ProjectContributor { + return predicate.ProjectContributor(sql.FieldNEQ(FieldProfiles, v)) +} + +// ProfilesIn applies the In predicate on the "profiles" field. +func ProfilesIn(vs ...string) predicate.ProjectContributor { + return predicate.ProjectContributor(sql.FieldIn(FieldProfiles, vs...)) +} + +// ProfilesNotIn applies the NotIn predicate on the "profiles" field. +func ProfilesNotIn(vs ...string) predicate.ProjectContributor { + return predicate.ProjectContributor(sql.FieldNotIn(FieldProfiles, vs...)) +} + +// ProfilesGT applies the GT predicate on the "profiles" field. +func ProfilesGT(v string) predicate.ProjectContributor { + return predicate.ProjectContributor(sql.FieldGT(FieldProfiles, v)) +} + +// ProfilesGTE applies the GTE predicate on the "profiles" field. +func ProfilesGTE(v string) predicate.ProjectContributor { + return predicate.ProjectContributor(sql.FieldGTE(FieldProfiles, v)) +} + +// ProfilesLT applies the LT predicate on the "profiles" field. +func ProfilesLT(v string) predicate.ProjectContributor { + return predicate.ProjectContributor(sql.FieldLT(FieldProfiles, v)) +} + +// ProfilesLTE applies the LTE predicate on the "profiles" field. +func ProfilesLTE(v string) predicate.ProjectContributor { + return predicate.ProjectContributor(sql.FieldLTE(FieldProfiles, v)) +} + +// ProfilesContains applies the Contains predicate on the "profiles" field. +func ProfilesContains(v string) predicate.ProjectContributor { + return predicate.ProjectContributor(sql.FieldContains(FieldProfiles, v)) +} + +// ProfilesHasPrefix applies the HasPrefix predicate on the "profiles" field. +func ProfilesHasPrefix(v string) predicate.ProjectContributor { + return predicate.ProjectContributor(sql.FieldHasPrefix(FieldProfiles, v)) +} + +// ProfilesHasSuffix applies the HasSuffix predicate on the "profiles" field. +func ProfilesHasSuffix(v string) predicate.ProjectContributor { + return predicate.ProjectContributor(sql.FieldHasSuffix(FieldProfiles, v)) +} + +// ProfilesIsNil applies the IsNil predicate on the "profiles" field. +func ProfilesIsNil() predicate.ProjectContributor { + return predicate.ProjectContributor(sql.FieldIsNull(FieldProfiles)) +} + +// ProfilesNotNil applies the NotNil predicate on the "profiles" field. +func ProfilesNotNil() predicate.ProjectContributor { + return predicate.ProjectContributor(sql.FieldNotNull(FieldProfiles)) +} + +// ProfilesEqualFold applies the EqualFold predicate on the "profiles" field. +func ProfilesEqualFold(v string) predicate.ProjectContributor { + return predicate.ProjectContributor(sql.FieldEqualFold(FieldProfiles, v)) +} + +// ProfilesContainsFold applies the ContainsFold predicate on the "profiles" field. +func ProfilesContainsFold(v string) predicate.ProjectContributor { + return predicate.ProjectContributor(sql.FieldContainsFold(FieldProfiles, v)) +} + +// LastSeenEQ applies the EQ predicate on the "last_seen" field. +func LastSeenEQ(v time.Time) predicate.ProjectContributor { + return predicate.ProjectContributor(sql.FieldEQ(FieldLastSeen, v)) +} + +// LastSeenNEQ applies the NEQ predicate on the "last_seen" field. +func LastSeenNEQ(v time.Time) predicate.ProjectContributor { + return predicate.ProjectContributor(sql.FieldNEQ(FieldLastSeen, v)) +} + +// LastSeenIn applies the In predicate on the "last_seen" field. +func LastSeenIn(vs ...time.Time) predicate.ProjectContributor { + return predicate.ProjectContributor(sql.FieldIn(FieldLastSeen, vs...)) +} + +// LastSeenNotIn applies the NotIn predicate on the "last_seen" field. +func LastSeenNotIn(vs ...time.Time) predicate.ProjectContributor { + return predicate.ProjectContributor(sql.FieldNotIn(FieldLastSeen, vs...)) +} + +// LastSeenGT applies the GT predicate on the "last_seen" field. +func LastSeenGT(v time.Time) predicate.ProjectContributor { + return predicate.ProjectContributor(sql.FieldGT(FieldLastSeen, v)) +} + +// LastSeenGTE applies the GTE predicate on the "last_seen" field. +func LastSeenGTE(v time.Time) predicate.ProjectContributor { + return predicate.ProjectContributor(sql.FieldGTE(FieldLastSeen, v)) +} + +// LastSeenLT applies the LT predicate on the "last_seen" field. +func LastSeenLT(v time.Time) predicate.ProjectContributor { + return predicate.ProjectContributor(sql.FieldLT(FieldLastSeen, v)) +} + +// LastSeenLTE applies the LTE predicate on the "last_seen" field. +func LastSeenLTE(v time.Time) predicate.ProjectContributor { + return predicate.ProjectContributor(sql.FieldLTE(FieldLastSeen, v)) +} + +// LastSeenIsNil applies the IsNil predicate on the "last_seen" field. +func LastSeenIsNil() predicate.ProjectContributor { + return predicate.ProjectContributor(sql.FieldIsNull(FieldLastSeen)) +} + +// LastSeenNotNil applies the NotNil predicate on the "last_seen" field. +func LastSeenNotNil() predicate.ProjectContributor { + return predicate.ProjectContributor(sql.FieldNotNull(FieldLastSeen)) +} + +// LocalPathEQ applies the EQ predicate on the "local_path" field. +func LocalPathEQ(v string) predicate.ProjectContributor { + return predicate.ProjectContributor(sql.FieldEQ(FieldLocalPath, v)) +} + +// LocalPathNEQ applies the NEQ predicate on the "local_path" field. +func LocalPathNEQ(v string) predicate.ProjectContributor { + return predicate.ProjectContributor(sql.FieldNEQ(FieldLocalPath, v)) +} + +// LocalPathIn applies the In predicate on the "local_path" field. +func LocalPathIn(vs ...string) predicate.ProjectContributor { + return predicate.ProjectContributor(sql.FieldIn(FieldLocalPath, vs...)) +} + +// LocalPathNotIn applies the NotIn predicate on the "local_path" field. +func LocalPathNotIn(vs ...string) predicate.ProjectContributor { + return predicate.ProjectContributor(sql.FieldNotIn(FieldLocalPath, vs...)) +} + +// LocalPathGT applies the GT predicate on the "local_path" field. +func LocalPathGT(v string) predicate.ProjectContributor { + return predicate.ProjectContributor(sql.FieldGT(FieldLocalPath, v)) +} + +// LocalPathGTE applies the GTE predicate on the "local_path" field. +func LocalPathGTE(v string) predicate.ProjectContributor { + return predicate.ProjectContributor(sql.FieldGTE(FieldLocalPath, v)) +} + +// LocalPathLT applies the LT predicate on the "local_path" field. +func LocalPathLT(v string) predicate.ProjectContributor { + return predicate.ProjectContributor(sql.FieldLT(FieldLocalPath, v)) +} + +// LocalPathLTE applies the LTE predicate on the "local_path" field. +func LocalPathLTE(v string) predicate.ProjectContributor { + return predicate.ProjectContributor(sql.FieldLTE(FieldLocalPath, v)) +} + +// LocalPathContains applies the Contains predicate on the "local_path" field. +func LocalPathContains(v string) predicate.ProjectContributor { + return predicate.ProjectContributor(sql.FieldContains(FieldLocalPath, v)) +} + +// LocalPathHasPrefix applies the HasPrefix predicate on the "local_path" field. +func LocalPathHasPrefix(v string) predicate.ProjectContributor { + return predicate.ProjectContributor(sql.FieldHasPrefix(FieldLocalPath, v)) +} + +// LocalPathHasSuffix applies the HasSuffix predicate on the "local_path" field. +func LocalPathHasSuffix(v string) predicate.ProjectContributor { + return predicate.ProjectContributor(sql.FieldHasSuffix(FieldLocalPath, v)) +} + +// LocalPathIsNil applies the IsNil predicate on the "local_path" field. +func LocalPathIsNil() predicate.ProjectContributor { + return predicate.ProjectContributor(sql.FieldIsNull(FieldLocalPath)) +} + +// LocalPathNotNil applies the NotNil predicate on the "local_path" field. +func LocalPathNotNil() predicate.ProjectContributor { + return predicate.ProjectContributor(sql.FieldNotNull(FieldLocalPath)) +} + +// LocalPathEqualFold applies the EqualFold predicate on the "local_path" field. +func LocalPathEqualFold(v string) predicate.ProjectContributor { + return predicate.ProjectContributor(sql.FieldEqualFold(FieldLocalPath, v)) +} + +// LocalPathContainsFold applies the ContainsFold predicate on the "local_path" field. +func LocalPathContainsFold(v string) predicate.ProjectContributor { + return predicate.ProjectContributor(sql.FieldContainsFold(FieldLocalPath, v)) +} + +// LinkedByEQ applies the EQ predicate on the "linked_by" field. +func LinkedByEQ(v string) predicate.ProjectContributor { + return predicate.ProjectContributor(sql.FieldEQ(FieldLinkedBy, v)) +} + +// LinkedByNEQ applies the NEQ predicate on the "linked_by" field. +func LinkedByNEQ(v string) predicate.ProjectContributor { + return predicate.ProjectContributor(sql.FieldNEQ(FieldLinkedBy, v)) +} + +// LinkedByIn applies the In predicate on the "linked_by" field. +func LinkedByIn(vs ...string) predicate.ProjectContributor { + return predicate.ProjectContributor(sql.FieldIn(FieldLinkedBy, vs...)) +} + +// LinkedByNotIn applies the NotIn predicate on the "linked_by" field. +func LinkedByNotIn(vs ...string) predicate.ProjectContributor { + return predicate.ProjectContributor(sql.FieldNotIn(FieldLinkedBy, vs...)) +} + +// LinkedByGT applies the GT predicate on the "linked_by" field. +func LinkedByGT(v string) predicate.ProjectContributor { + return predicate.ProjectContributor(sql.FieldGT(FieldLinkedBy, v)) +} + +// LinkedByGTE applies the GTE predicate on the "linked_by" field. +func LinkedByGTE(v string) predicate.ProjectContributor { + return predicate.ProjectContributor(sql.FieldGTE(FieldLinkedBy, v)) +} + +// LinkedByLT applies the LT predicate on the "linked_by" field. +func LinkedByLT(v string) predicate.ProjectContributor { + return predicate.ProjectContributor(sql.FieldLT(FieldLinkedBy, v)) +} + +// LinkedByLTE applies the LTE predicate on the "linked_by" field. +func LinkedByLTE(v string) predicate.ProjectContributor { + return predicate.ProjectContributor(sql.FieldLTE(FieldLinkedBy, v)) +} + +// LinkedByContains applies the Contains predicate on the "linked_by" field. +func LinkedByContains(v string) predicate.ProjectContributor { + return predicate.ProjectContributor(sql.FieldContains(FieldLinkedBy, v)) +} + +// LinkedByHasPrefix applies the HasPrefix predicate on the "linked_by" field. +func LinkedByHasPrefix(v string) predicate.ProjectContributor { + return predicate.ProjectContributor(sql.FieldHasPrefix(FieldLinkedBy, v)) +} + +// LinkedByHasSuffix applies the HasSuffix predicate on the "linked_by" field. +func LinkedByHasSuffix(v string) predicate.ProjectContributor { + return predicate.ProjectContributor(sql.FieldHasSuffix(FieldLinkedBy, v)) +} + +// LinkedByIsNil applies the IsNil predicate on the "linked_by" field. +func LinkedByIsNil() predicate.ProjectContributor { + return predicate.ProjectContributor(sql.FieldIsNull(FieldLinkedBy)) +} + +// LinkedByNotNil applies the NotNil predicate on the "linked_by" field. +func LinkedByNotNil() predicate.ProjectContributor { + return predicate.ProjectContributor(sql.FieldNotNull(FieldLinkedBy)) +} + +// LinkedByEqualFold applies the EqualFold predicate on the "linked_by" field. +func LinkedByEqualFold(v string) predicate.ProjectContributor { + return predicate.ProjectContributor(sql.FieldEqualFold(FieldLinkedBy, v)) +} + +// LinkedByContainsFold applies the ContainsFold predicate on the "linked_by" field. +func LinkedByContainsFold(v string) predicate.ProjectContributor { + return predicate.ProjectContributor(sql.FieldContainsFold(FieldLinkedBy, v)) +} + +// LinkedAtEQ applies the EQ predicate on the "linked_at" field. +func LinkedAtEQ(v time.Time) predicate.ProjectContributor { + return predicate.ProjectContributor(sql.FieldEQ(FieldLinkedAt, v)) +} + +// LinkedAtNEQ applies the NEQ predicate on the "linked_at" field. +func LinkedAtNEQ(v time.Time) predicate.ProjectContributor { + return predicate.ProjectContributor(sql.FieldNEQ(FieldLinkedAt, v)) +} + +// LinkedAtIn applies the In predicate on the "linked_at" field. +func LinkedAtIn(vs ...time.Time) predicate.ProjectContributor { + return predicate.ProjectContributor(sql.FieldIn(FieldLinkedAt, vs...)) +} + +// LinkedAtNotIn applies the NotIn predicate on the "linked_at" field. +func LinkedAtNotIn(vs ...time.Time) predicate.ProjectContributor { + return predicate.ProjectContributor(sql.FieldNotIn(FieldLinkedAt, vs...)) +} + +// LinkedAtGT applies the GT predicate on the "linked_at" field. +func LinkedAtGT(v time.Time) predicate.ProjectContributor { + return predicate.ProjectContributor(sql.FieldGT(FieldLinkedAt, v)) +} + +// LinkedAtGTE applies the GTE predicate on the "linked_at" field. +func LinkedAtGTE(v time.Time) predicate.ProjectContributor { + return predicate.ProjectContributor(sql.FieldGTE(FieldLinkedAt, v)) +} + +// LinkedAtLT applies the LT predicate on the "linked_at" field. +func LinkedAtLT(v time.Time) predicate.ProjectContributor { + return predicate.ProjectContributor(sql.FieldLT(FieldLinkedAt, v)) +} + +// LinkedAtLTE applies the LTE predicate on the "linked_at" field. +func LinkedAtLTE(v time.Time) predicate.ProjectContributor { + return predicate.ProjectContributor(sql.FieldLTE(FieldLinkedAt, v)) +} + +// LinkedAtIsNil applies the IsNil predicate on the "linked_at" field. +func LinkedAtIsNil() predicate.ProjectContributor { + return predicate.ProjectContributor(sql.FieldIsNull(FieldLinkedAt)) +} + +// LinkedAtNotNil applies the NotNil predicate on the "linked_at" field. +func LinkedAtNotNil() predicate.ProjectContributor { + return predicate.ProjectContributor(sql.FieldNotNull(FieldLinkedAt)) +} + +// And groups predicates with the AND operator between them. +func And(predicates ...predicate.ProjectContributor) predicate.ProjectContributor { + return predicate.ProjectContributor(sql.AndPredicates(predicates...)) +} + +// Or groups predicates with the OR operator between them. +func Or(predicates ...predicate.ProjectContributor) predicate.ProjectContributor { + return predicate.ProjectContributor(sql.OrPredicates(predicates...)) +} + +// Not applies the not operator on the given predicate. +func Not(p predicate.ProjectContributor) predicate.ProjectContributor { + return predicate.ProjectContributor(sql.NotPredicates(p)) +} diff --git a/pkg/ent/projectcontributor_create.go b/pkg/ent/projectcontributor_create.go new file mode 100644 index 000000000..d2b1620d4 --- /dev/null +++ b/pkg/ent/projectcontributor_create.go @@ -0,0 +1,1140 @@ +// Code generated by ent, DO NOT EDIT. + +package ent + +import ( + "context" + "errors" + "fmt" + "time" + + "entgo.io/ent/dialect" + "entgo.io/ent/dialect/sql" + "entgo.io/ent/dialect/sql/sqlgraph" + "entgo.io/ent/schema/field" + "github.com/GoogleCloudPlatform/scion/pkg/ent/projectcontributor" + "github.com/google/uuid" +) + +// ProjectContributorCreate is the builder for creating a ProjectContributor entity. +type ProjectContributorCreate struct { + config + mutation *ProjectContributorMutation + hooks []Hook + conflict []sql.ConflictOption +} + +// SetProjectID sets the "project_id" field. +func (_c *ProjectContributorCreate) SetProjectID(v uuid.UUID) *ProjectContributorCreate { + _c.mutation.SetProjectID(v) + return _c +} + +// SetBrokerID sets the "broker_id" field. +func (_c *ProjectContributorCreate) SetBrokerID(v uuid.UUID) *ProjectContributorCreate { + _c.mutation.SetBrokerID(v) + return _c +} + +// SetBrokerName sets the "broker_name" field. +func (_c *ProjectContributorCreate) SetBrokerName(v string) *ProjectContributorCreate { + _c.mutation.SetBrokerName(v) + return _c +} + +// SetMode sets the "mode" field. +func (_c *ProjectContributorCreate) SetMode(v string) *ProjectContributorCreate { + _c.mutation.SetMode(v) + return _c +} + +// SetNillableMode sets the "mode" field if the given value is not nil. +func (_c *ProjectContributorCreate) SetNillableMode(v *string) *ProjectContributorCreate { + if v != nil { + _c.SetMode(*v) + } + return _c +} + +// SetStatus sets the "status" field. +func (_c *ProjectContributorCreate) SetStatus(v string) *ProjectContributorCreate { + _c.mutation.SetStatus(v) + return _c +} + +// SetNillableStatus sets the "status" field if the given value is not nil. +func (_c *ProjectContributorCreate) SetNillableStatus(v *string) *ProjectContributorCreate { + if v != nil { + _c.SetStatus(*v) + } + return _c +} + +// SetProfiles sets the "profiles" field. +func (_c *ProjectContributorCreate) SetProfiles(v string) *ProjectContributorCreate { + _c.mutation.SetProfiles(v) + return _c +} + +// SetNillableProfiles sets the "profiles" field if the given value is not nil. +func (_c *ProjectContributorCreate) SetNillableProfiles(v *string) *ProjectContributorCreate { + if v != nil { + _c.SetProfiles(*v) + } + return _c +} + +// SetLastSeen sets the "last_seen" field. +func (_c *ProjectContributorCreate) SetLastSeen(v time.Time) *ProjectContributorCreate { + _c.mutation.SetLastSeen(v) + return _c +} + +// SetNillableLastSeen sets the "last_seen" field if the given value is not nil. +func (_c *ProjectContributorCreate) SetNillableLastSeen(v *time.Time) *ProjectContributorCreate { + if v != nil { + _c.SetLastSeen(*v) + } + return _c +} + +// SetLocalPath sets the "local_path" field. +func (_c *ProjectContributorCreate) SetLocalPath(v string) *ProjectContributorCreate { + _c.mutation.SetLocalPath(v) + return _c +} + +// SetNillableLocalPath sets the "local_path" field if the given value is not nil. +func (_c *ProjectContributorCreate) SetNillableLocalPath(v *string) *ProjectContributorCreate { + if v != nil { + _c.SetLocalPath(*v) + } + return _c +} + +// SetLinkedBy sets the "linked_by" field. +func (_c *ProjectContributorCreate) SetLinkedBy(v string) *ProjectContributorCreate { + _c.mutation.SetLinkedBy(v) + return _c +} + +// SetNillableLinkedBy sets the "linked_by" field if the given value is not nil. +func (_c *ProjectContributorCreate) SetNillableLinkedBy(v *string) *ProjectContributorCreate { + if v != nil { + _c.SetLinkedBy(*v) + } + return _c +} + +// SetLinkedAt sets the "linked_at" field. +func (_c *ProjectContributorCreate) SetLinkedAt(v time.Time) *ProjectContributorCreate { + _c.mutation.SetLinkedAt(v) + return _c +} + +// SetNillableLinkedAt sets the "linked_at" field if the given value is not nil. +func (_c *ProjectContributorCreate) SetNillableLinkedAt(v *time.Time) *ProjectContributorCreate { + if v != nil { + _c.SetLinkedAt(*v) + } + return _c +} + +// SetID sets the "id" field. +func (_c *ProjectContributorCreate) SetID(v uuid.UUID) *ProjectContributorCreate { + _c.mutation.SetID(v) + return _c +} + +// SetNillableID sets the "id" field if the given value is not nil. +func (_c *ProjectContributorCreate) SetNillableID(v *uuid.UUID) *ProjectContributorCreate { + if v != nil { + _c.SetID(*v) + } + return _c +} + +// Mutation returns the ProjectContributorMutation object of the builder. +func (_c *ProjectContributorCreate) Mutation() *ProjectContributorMutation { + return _c.mutation +} + +// Save creates the ProjectContributor in the database. +func (_c *ProjectContributorCreate) Save(ctx context.Context) (*ProjectContributor, error) { + _c.defaults() + return withHooks(ctx, _c.sqlSave, _c.mutation, _c.hooks) +} + +// SaveX calls Save and panics if Save returns an error. +func (_c *ProjectContributorCreate) SaveX(ctx context.Context) *ProjectContributor { + v, err := _c.Save(ctx) + if err != nil { + panic(err) + } + return v +} + +// Exec executes the query. +func (_c *ProjectContributorCreate) Exec(ctx context.Context) error { + _, err := _c.Save(ctx) + return err +} + +// ExecX is like Exec, but panics if an error occurs. +func (_c *ProjectContributorCreate) ExecX(ctx context.Context) { + if err := _c.Exec(ctx); err != nil { + panic(err) + } +} + +// defaults sets the default values of the builder before save. +func (_c *ProjectContributorCreate) defaults() { + if _, ok := _c.mutation.Mode(); !ok { + v := projectcontributor.DefaultMode + _c.mutation.SetMode(v) + } + if _, ok := _c.mutation.Status(); !ok { + v := projectcontributor.DefaultStatus + _c.mutation.SetStatus(v) + } + if _, ok := _c.mutation.ID(); !ok { + v := projectcontributor.DefaultID() + _c.mutation.SetID(v) + } +} + +// check runs all checks and user-defined validators on the builder. +func (_c *ProjectContributorCreate) check() error { + if _, ok := _c.mutation.ProjectID(); !ok { + return &ValidationError{Name: "project_id", err: errors.New(`ent: missing required field "ProjectContributor.project_id"`)} + } + if _, ok := _c.mutation.BrokerID(); !ok { + return &ValidationError{Name: "broker_id", err: errors.New(`ent: missing required field "ProjectContributor.broker_id"`)} + } + if _, ok := _c.mutation.BrokerName(); !ok { + return &ValidationError{Name: "broker_name", err: errors.New(`ent: missing required field "ProjectContributor.broker_name"`)} + } + if v, ok := _c.mutation.BrokerName(); ok { + if err := projectcontributor.BrokerNameValidator(v); err != nil { + return &ValidationError{Name: "broker_name", err: fmt.Errorf(`ent: validator failed for field "ProjectContributor.broker_name": %w`, err)} + } + } + if _, ok := _c.mutation.Mode(); !ok { + return &ValidationError{Name: "mode", err: errors.New(`ent: missing required field "ProjectContributor.mode"`)} + } + if _, ok := _c.mutation.Status(); !ok { + return &ValidationError{Name: "status", err: errors.New(`ent: missing required field "ProjectContributor.status"`)} + } + return nil +} + +func (_c *ProjectContributorCreate) sqlSave(ctx context.Context) (*ProjectContributor, error) { + if err := _c.check(); err != nil { + return nil, err + } + _node, _spec := _c.createSpec() + if err := sqlgraph.CreateNode(ctx, _c.driver, _spec); err != nil { + if sqlgraph.IsConstraintError(err) { + err = &ConstraintError{msg: err.Error(), wrap: err} + } + return nil, err + } + if _spec.ID.Value != nil { + if id, ok := _spec.ID.Value.(*uuid.UUID); ok { + _node.ID = *id + } else if err := _node.ID.Scan(_spec.ID.Value); err != nil { + return nil, err + } + } + _c.mutation.id = &_node.ID + _c.mutation.done = true + return _node, nil +} + +func (_c *ProjectContributorCreate) createSpec() (*ProjectContributor, *sqlgraph.CreateSpec) { + var ( + _node = &ProjectContributor{config: _c.config} + _spec = sqlgraph.NewCreateSpec(projectcontributor.Table, sqlgraph.NewFieldSpec(projectcontributor.FieldID, field.TypeUUID)) + ) + _spec.OnConflict = _c.conflict + if id, ok := _c.mutation.ID(); ok { + _node.ID = id + _spec.ID.Value = &id + } + if value, ok := _c.mutation.ProjectID(); ok { + _spec.SetField(projectcontributor.FieldProjectID, field.TypeUUID, value) + _node.ProjectID = value + } + if value, ok := _c.mutation.BrokerID(); ok { + _spec.SetField(projectcontributor.FieldBrokerID, field.TypeUUID, value) + _node.BrokerID = value + } + if value, ok := _c.mutation.BrokerName(); ok { + _spec.SetField(projectcontributor.FieldBrokerName, field.TypeString, value) + _node.BrokerName = value + } + if value, ok := _c.mutation.Mode(); ok { + _spec.SetField(projectcontributor.FieldMode, field.TypeString, value) + _node.Mode = value + } + if value, ok := _c.mutation.Status(); ok { + _spec.SetField(projectcontributor.FieldStatus, field.TypeString, value) + _node.Status = value + } + if value, ok := _c.mutation.Profiles(); ok { + _spec.SetField(projectcontributor.FieldProfiles, field.TypeString, value) + _node.Profiles = value + } + if value, ok := _c.mutation.LastSeen(); ok { + _spec.SetField(projectcontributor.FieldLastSeen, field.TypeTime, value) + _node.LastSeen = &value + } + if value, ok := _c.mutation.LocalPath(); ok { + _spec.SetField(projectcontributor.FieldLocalPath, field.TypeString, value) + _node.LocalPath = value + } + if value, ok := _c.mutation.LinkedBy(); ok { + _spec.SetField(projectcontributor.FieldLinkedBy, field.TypeString, value) + _node.LinkedBy = value + } + if value, ok := _c.mutation.LinkedAt(); ok { + _spec.SetField(projectcontributor.FieldLinkedAt, field.TypeTime, value) + _node.LinkedAt = &value + } + return _node, _spec +} + +// OnConflict allows configuring the `ON CONFLICT` / `ON DUPLICATE KEY` clause +// of the `INSERT` statement. For example: +// +// client.ProjectContributor.Create(). +// SetProjectID(v). +// OnConflict( +// // Update the row with the new values +// // the was proposed for insertion. +// sql.ResolveWithNewValues(), +// ). +// // Override some of the fields with custom +// // update values. +// Update(func(u *ent.ProjectContributorUpsert) { +// SetProjectID(v+v). +// }). +// Exec(ctx) +func (_c *ProjectContributorCreate) OnConflict(opts ...sql.ConflictOption) *ProjectContributorUpsertOne { + _c.conflict = opts + return &ProjectContributorUpsertOne{ + create: _c, + } +} + +// OnConflictColumns calls `OnConflict` and configures the columns +// as conflict target. Using this option is equivalent to using: +// +// client.ProjectContributor.Create(). +// OnConflict(sql.ConflictColumns(columns...)). +// Exec(ctx) +func (_c *ProjectContributorCreate) OnConflictColumns(columns ...string) *ProjectContributorUpsertOne { + _c.conflict = append(_c.conflict, sql.ConflictColumns(columns...)) + return &ProjectContributorUpsertOne{ + create: _c, + } +} + +type ( + // ProjectContributorUpsertOne is the builder for "upsert"-ing + // one ProjectContributor node. + ProjectContributorUpsertOne struct { + create *ProjectContributorCreate + } + + // ProjectContributorUpsert is the "OnConflict" setter. + ProjectContributorUpsert struct { + *sql.UpdateSet + } +) + +// SetProjectID sets the "project_id" field. +func (u *ProjectContributorUpsert) SetProjectID(v uuid.UUID) *ProjectContributorUpsert { + u.Set(projectcontributor.FieldProjectID, v) + return u +} + +// UpdateProjectID sets the "project_id" field to the value that was provided on create. +func (u *ProjectContributorUpsert) UpdateProjectID() *ProjectContributorUpsert { + u.SetExcluded(projectcontributor.FieldProjectID) + return u +} + +// SetBrokerID sets the "broker_id" field. +func (u *ProjectContributorUpsert) SetBrokerID(v uuid.UUID) *ProjectContributorUpsert { + u.Set(projectcontributor.FieldBrokerID, v) + return u +} + +// UpdateBrokerID sets the "broker_id" field to the value that was provided on create. +func (u *ProjectContributorUpsert) UpdateBrokerID() *ProjectContributorUpsert { + u.SetExcluded(projectcontributor.FieldBrokerID) + return u +} + +// SetBrokerName sets the "broker_name" field. +func (u *ProjectContributorUpsert) SetBrokerName(v string) *ProjectContributorUpsert { + u.Set(projectcontributor.FieldBrokerName, v) + return u +} + +// UpdateBrokerName sets the "broker_name" field to the value that was provided on create. +func (u *ProjectContributorUpsert) UpdateBrokerName() *ProjectContributorUpsert { + u.SetExcluded(projectcontributor.FieldBrokerName) + return u +} + +// SetMode sets the "mode" field. +func (u *ProjectContributorUpsert) SetMode(v string) *ProjectContributorUpsert { + u.Set(projectcontributor.FieldMode, v) + return u +} + +// UpdateMode sets the "mode" field to the value that was provided on create. +func (u *ProjectContributorUpsert) UpdateMode() *ProjectContributorUpsert { + u.SetExcluded(projectcontributor.FieldMode) + return u +} + +// SetStatus sets the "status" field. +func (u *ProjectContributorUpsert) SetStatus(v string) *ProjectContributorUpsert { + u.Set(projectcontributor.FieldStatus, v) + return u +} + +// UpdateStatus sets the "status" field to the value that was provided on create. +func (u *ProjectContributorUpsert) UpdateStatus() *ProjectContributorUpsert { + u.SetExcluded(projectcontributor.FieldStatus) + return u +} + +// SetProfiles sets the "profiles" field. +func (u *ProjectContributorUpsert) SetProfiles(v string) *ProjectContributorUpsert { + u.Set(projectcontributor.FieldProfiles, v) + return u +} + +// UpdateProfiles sets the "profiles" field to the value that was provided on create. +func (u *ProjectContributorUpsert) UpdateProfiles() *ProjectContributorUpsert { + u.SetExcluded(projectcontributor.FieldProfiles) + return u +} + +// ClearProfiles clears the value of the "profiles" field. +func (u *ProjectContributorUpsert) ClearProfiles() *ProjectContributorUpsert { + u.SetNull(projectcontributor.FieldProfiles) + return u +} + +// SetLastSeen sets the "last_seen" field. +func (u *ProjectContributorUpsert) SetLastSeen(v time.Time) *ProjectContributorUpsert { + u.Set(projectcontributor.FieldLastSeen, v) + return u +} + +// UpdateLastSeen sets the "last_seen" field to the value that was provided on create. +func (u *ProjectContributorUpsert) UpdateLastSeen() *ProjectContributorUpsert { + u.SetExcluded(projectcontributor.FieldLastSeen) + return u +} + +// ClearLastSeen clears the value of the "last_seen" field. +func (u *ProjectContributorUpsert) ClearLastSeen() *ProjectContributorUpsert { + u.SetNull(projectcontributor.FieldLastSeen) + return u +} + +// SetLocalPath sets the "local_path" field. +func (u *ProjectContributorUpsert) SetLocalPath(v string) *ProjectContributorUpsert { + u.Set(projectcontributor.FieldLocalPath, v) + return u +} + +// UpdateLocalPath sets the "local_path" field to the value that was provided on create. +func (u *ProjectContributorUpsert) UpdateLocalPath() *ProjectContributorUpsert { + u.SetExcluded(projectcontributor.FieldLocalPath) + return u +} + +// ClearLocalPath clears the value of the "local_path" field. +func (u *ProjectContributorUpsert) ClearLocalPath() *ProjectContributorUpsert { + u.SetNull(projectcontributor.FieldLocalPath) + return u +} + +// SetLinkedBy sets the "linked_by" field. +func (u *ProjectContributorUpsert) SetLinkedBy(v string) *ProjectContributorUpsert { + u.Set(projectcontributor.FieldLinkedBy, v) + return u +} + +// UpdateLinkedBy sets the "linked_by" field to the value that was provided on create. +func (u *ProjectContributorUpsert) UpdateLinkedBy() *ProjectContributorUpsert { + u.SetExcluded(projectcontributor.FieldLinkedBy) + return u +} + +// ClearLinkedBy clears the value of the "linked_by" field. +func (u *ProjectContributorUpsert) ClearLinkedBy() *ProjectContributorUpsert { + u.SetNull(projectcontributor.FieldLinkedBy) + return u +} + +// SetLinkedAt sets the "linked_at" field. +func (u *ProjectContributorUpsert) SetLinkedAt(v time.Time) *ProjectContributorUpsert { + u.Set(projectcontributor.FieldLinkedAt, v) + return u +} + +// UpdateLinkedAt sets the "linked_at" field to the value that was provided on create. +func (u *ProjectContributorUpsert) UpdateLinkedAt() *ProjectContributorUpsert { + u.SetExcluded(projectcontributor.FieldLinkedAt) + return u +} + +// ClearLinkedAt clears the value of the "linked_at" field. +func (u *ProjectContributorUpsert) ClearLinkedAt() *ProjectContributorUpsert { + u.SetNull(projectcontributor.FieldLinkedAt) + return u +} + +// UpdateNewValues updates the mutable fields using the new values that were set on create except the ID field. +// Using this option is equivalent to using: +// +// client.ProjectContributor.Create(). +// OnConflict( +// sql.ResolveWithNewValues(), +// sql.ResolveWith(func(u *sql.UpdateSet) { +// u.SetIgnore(projectcontributor.FieldID) +// }), +// ). +// Exec(ctx) +func (u *ProjectContributorUpsertOne) UpdateNewValues() *ProjectContributorUpsertOne { + u.create.conflict = append(u.create.conflict, sql.ResolveWithNewValues()) + u.create.conflict = append(u.create.conflict, sql.ResolveWith(func(s *sql.UpdateSet) { + if _, exists := u.create.mutation.ID(); exists { + s.SetIgnore(projectcontributor.FieldID) + } + })) + return u +} + +// Ignore sets each column to itself in case of conflict. +// Using this option is equivalent to using: +// +// client.ProjectContributor.Create(). +// OnConflict(sql.ResolveWithIgnore()). +// Exec(ctx) +func (u *ProjectContributorUpsertOne) Ignore() *ProjectContributorUpsertOne { + u.create.conflict = append(u.create.conflict, sql.ResolveWithIgnore()) + return u +} + +// DoNothing configures the conflict_action to `DO NOTHING`. +// Supported only by SQLite and PostgreSQL. +func (u *ProjectContributorUpsertOne) DoNothing() *ProjectContributorUpsertOne { + u.create.conflict = append(u.create.conflict, sql.DoNothing()) + return u +} + +// Update allows overriding fields `UPDATE` values. See the ProjectContributorCreate.OnConflict +// documentation for more info. +func (u *ProjectContributorUpsertOne) Update(set func(*ProjectContributorUpsert)) *ProjectContributorUpsertOne { + u.create.conflict = append(u.create.conflict, sql.ResolveWith(func(update *sql.UpdateSet) { + set(&ProjectContributorUpsert{UpdateSet: update}) + })) + return u +} + +// SetProjectID sets the "project_id" field. +func (u *ProjectContributorUpsertOne) SetProjectID(v uuid.UUID) *ProjectContributorUpsertOne { + return u.Update(func(s *ProjectContributorUpsert) { + s.SetProjectID(v) + }) +} + +// UpdateProjectID sets the "project_id" field to the value that was provided on create. +func (u *ProjectContributorUpsertOne) UpdateProjectID() *ProjectContributorUpsertOne { + return u.Update(func(s *ProjectContributorUpsert) { + s.UpdateProjectID() + }) +} + +// SetBrokerID sets the "broker_id" field. +func (u *ProjectContributorUpsertOne) SetBrokerID(v uuid.UUID) *ProjectContributorUpsertOne { + return u.Update(func(s *ProjectContributorUpsert) { + s.SetBrokerID(v) + }) +} + +// UpdateBrokerID sets the "broker_id" field to the value that was provided on create. +func (u *ProjectContributorUpsertOne) UpdateBrokerID() *ProjectContributorUpsertOne { + return u.Update(func(s *ProjectContributorUpsert) { + s.UpdateBrokerID() + }) +} + +// SetBrokerName sets the "broker_name" field. +func (u *ProjectContributorUpsertOne) SetBrokerName(v string) *ProjectContributorUpsertOne { + return u.Update(func(s *ProjectContributorUpsert) { + s.SetBrokerName(v) + }) +} + +// UpdateBrokerName sets the "broker_name" field to the value that was provided on create. +func (u *ProjectContributorUpsertOne) UpdateBrokerName() *ProjectContributorUpsertOne { + return u.Update(func(s *ProjectContributorUpsert) { + s.UpdateBrokerName() + }) +} + +// SetMode sets the "mode" field. +func (u *ProjectContributorUpsertOne) SetMode(v string) *ProjectContributorUpsertOne { + return u.Update(func(s *ProjectContributorUpsert) { + s.SetMode(v) + }) +} + +// UpdateMode sets the "mode" field to the value that was provided on create. +func (u *ProjectContributorUpsertOne) UpdateMode() *ProjectContributorUpsertOne { + return u.Update(func(s *ProjectContributorUpsert) { + s.UpdateMode() + }) +} + +// SetStatus sets the "status" field. +func (u *ProjectContributorUpsertOne) SetStatus(v string) *ProjectContributorUpsertOne { + return u.Update(func(s *ProjectContributorUpsert) { + s.SetStatus(v) + }) +} + +// UpdateStatus sets the "status" field to the value that was provided on create. +func (u *ProjectContributorUpsertOne) UpdateStatus() *ProjectContributorUpsertOne { + return u.Update(func(s *ProjectContributorUpsert) { + s.UpdateStatus() + }) +} + +// SetProfiles sets the "profiles" field. +func (u *ProjectContributorUpsertOne) SetProfiles(v string) *ProjectContributorUpsertOne { + return u.Update(func(s *ProjectContributorUpsert) { + s.SetProfiles(v) + }) +} + +// UpdateProfiles sets the "profiles" field to the value that was provided on create. +func (u *ProjectContributorUpsertOne) UpdateProfiles() *ProjectContributorUpsertOne { + return u.Update(func(s *ProjectContributorUpsert) { + s.UpdateProfiles() + }) +} + +// ClearProfiles clears the value of the "profiles" field. +func (u *ProjectContributorUpsertOne) ClearProfiles() *ProjectContributorUpsertOne { + return u.Update(func(s *ProjectContributorUpsert) { + s.ClearProfiles() + }) +} + +// SetLastSeen sets the "last_seen" field. +func (u *ProjectContributorUpsertOne) SetLastSeen(v time.Time) *ProjectContributorUpsertOne { + return u.Update(func(s *ProjectContributorUpsert) { + s.SetLastSeen(v) + }) +} + +// UpdateLastSeen sets the "last_seen" field to the value that was provided on create. +func (u *ProjectContributorUpsertOne) UpdateLastSeen() *ProjectContributorUpsertOne { + return u.Update(func(s *ProjectContributorUpsert) { + s.UpdateLastSeen() + }) +} + +// ClearLastSeen clears the value of the "last_seen" field. +func (u *ProjectContributorUpsertOne) ClearLastSeen() *ProjectContributorUpsertOne { + return u.Update(func(s *ProjectContributorUpsert) { + s.ClearLastSeen() + }) +} + +// SetLocalPath sets the "local_path" field. +func (u *ProjectContributorUpsertOne) SetLocalPath(v string) *ProjectContributorUpsertOne { + return u.Update(func(s *ProjectContributorUpsert) { + s.SetLocalPath(v) + }) +} + +// UpdateLocalPath sets the "local_path" field to the value that was provided on create. +func (u *ProjectContributorUpsertOne) UpdateLocalPath() *ProjectContributorUpsertOne { + return u.Update(func(s *ProjectContributorUpsert) { + s.UpdateLocalPath() + }) +} + +// ClearLocalPath clears the value of the "local_path" field. +func (u *ProjectContributorUpsertOne) ClearLocalPath() *ProjectContributorUpsertOne { + return u.Update(func(s *ProjectContributorUpsert) { + s.ClearLocalPath() + }) +} + +// SetLinkedBy sets the "linked_by" field. +func (u *ProjectContributorUpsertOne) SetLinkedBy(v string) *ProjectContributorUpsertOne { + return u.Update(func(s *ProjectContributorUpsert) { + s.SetLinkedBy(v) + }) +} + +// UpdateLinkedBy sets the "linked_by" field to the value that was provided on create. +func (u *ProjectContributorUpsertOne) UpdateLinkedBy() *ProjectContributorUpsertOne { + return u.Update(func(s *ProjectContributorUpsert) { + s.UpdateLinkedBy() + }) +} + +// ClearLinkedBy clears the value of the "linked_by" field. +func (u *ProjectContributorUpsertOne) ClearLinkedBy() *ProjectContributorUpsertOne { + return u.Update(func(s *ProjectContributorUpsert) { + s.ClearLinkedBy() + }) +} + +// SetLinkedAt sets the "linked_at" field. +func (u *ProjectContributorUpsertOne) SetLinkedAt(v time.Time) *ProjectContributorUpsertOne { + return u.Update(func(s *ProjectContributorUpsert) { + s.SetLinkedAt(v) + }) +} + +// UpdateLinkedAt sets the "linked_at" field to the value that was provided on create. +func (u *ProjectContributorUpsertOne) UpdateLinkedAt() *ProjectContributorUpsertOne { + return u.Update(func(s *ProjectContributorUpsert) { + s.UpdateLinkedAt() + }) +} + +// ClearLinkedAt clears the value of the "linked_at" field. +func (u *ProjectContributorUpsertOne) ClearLinkedAt() *ProjectContributorUpsertOne { + return u.Update(func(s *ProjectContributorUpsert) { + s.ClearLinkedAt() + }) +} + +// Exec executes the query. +func (u *ProjectContributorUpsertOne) Exec(ctx context.Context) error { + if len(u.create.conflict) == 0 { + return errors.New("ent: missing options for ProjectContributorCreate.OnConflict") + } + return u.create.Exec(ctx) +} + +// ExecX is like Exec, but panics if an error occurs. +func (u *ProjectContributorUpsertOne) ExecX(ctx context.Context) { + if err := u.create.Exec(ctx); err != nil { + panic(err) + } +} + +// Exec executes the UPSERT query and returns the inserted/updated ID. +func (u *ProjectContributorUpsertOne) ID(ctx context.Context) (id uuid.UUID, err error) { + if u.create.driver.Dialect() == dialect.MySQL { + // In case of "ON CONFLICT", there is no way to get back non-numeric ID + // fields from the database since MySQL does not support the RETURNING clause. + return id, errors.New("ent: ProjectContributorUpsertOne.ID is not supported by MySQL driver. Use ProjectContributorUpsertOne.Exec instead") + } + node, err := u.create.Save(ctx) + if err != nil { + return id, err + } + return node.ID, nil +} + +// IDX is like ID, but panics if an error occurs. +func (u *ProjectContributorUpsertOne) IDX(ctx context.Context) uuid.UUID { + id, err := u.ID(ctx) + if err != nil { + panic(err) + } + return id +} + +// ProjectContributorCreateBulk is the builder for creating many ProjectContributor entities in bulk. +type ProjectContributorCreateBulk struct { + config + err error + builders []*ProjectContributorCreate + conflict []sql.ConflictOption +} + +// Save creates the ProjectContributor entities in the database. +func (_c *ProjectContributorCreateBulk) Save(ctx context.Context) ([]*ProjectContributor, error) { + if _c.err != nil { + return nil, _c.err + } + specs := make([]*sqlgraph.CreateSpec, len(_c.builders)) + nodes := make([]*ProjectContributor, len(_c.builders)) + mutators := make([]Mutator, len(_c.builders)) + for i := range _c.builders { + func(i int, root context.Context) { + builder := _c.builders[i] + builder.defaults() + var mut Mutator = MutateFunc(func(ctx context.Context, m Mutation) (Value, error) { + mutation, ok := m.(*ProjectContributorMutation) + if !ok { + return nil, fmt.Errorf("unexpected mutation type %T", m) + } + if err := builder.check(); err != nil { + return nil, err + } + builder.mutation = mutation + var err error + nodes[i], specs[i] = builder.createSpec() + if i < len(mutators)-1 { + _, err = mutators[i+1].Mutate(root, _c.builders[i+1].mutation) + } else { + spec := &sqlgraph.BatchCreateSpec{Nodes: specs} + spec.OnConflict = _c.conflict + // Invoke the actual operation on the latest mutation in the chain. + if err = sqlgraph.BatchCreate(ctx, _c.driver, spec); err != nil { + if sqlgraph.IsConstraintError(err) { + err = &ConstraintError{msg: err.Error(), wrap: err} + } + } + } + if err != nil { + return nil, err + } + mutation.id = &nodes[i].ID + mutation.done = true + return nodes[i], nil + }) + for i := len(builder.hooks) - 1; i >= 0; i-- { + mut = builder.hooks[i](mut) + } + mutators[i] = mut + }(i, ctx) + } + if len(mutators) > 0 { + if _, err := mutators[0].Mutate(ctx, _c.builders[0].mutation); err != nil { + return nil, err + } + } + return nodes, nil +} + +// SaveX is like Save, but panics if an error occurs. +func (_c *ProjectContributorCreateBulk) SaveX(ctx context.Context) []*ProjectContributor { + v, err := _c.Save(ctx) + if err != nil { + panic(err) + } + return v +} + +// Exec executes the query. +func (_c *ProjectContributorCreateBulk) Exec(ctx context.Context) error { + _, err := _c.Save(ctx) + return err +} + +// ExecX is like Exec, but panics if an error occurs. +func (_c *ProjectContributorCreateBulk) ExecX(ctx context.Context) { + if err := _c.Exec(ctx); err != nil { + panic(err) + } +} + +// OnConflict allows configuring the `ON CONFLICT` / `ON DUPLICATE KEY` clause +// of the `INSERT` statement. For example: +// +// client.ProjectContributor.CreateBulk(builders...). +// OnConflict( +// // Update the row with the new values +// // the was proposed for insertion. +// sql.ResolveWithNewValues(), +// ). +// // Override some of the fields with custom +// // update values. +// Update(func(u *ent.ProjectContributorUpsert) { +// SetProjectID(v+v). +// }). +// Exec(ctx) +func (_c *ProjectContributorCreateBulk) OnConflict(opts ...sql.ConflictOption) *ProjectContributorUpsertBulk { + _c.conflict = opts + return &ProjectContributorUpsertBulk{ + create: _c, + } +} + +// OnConflictColumns calls `OnConflict` and configures the columns +// as conflict target. Using this option is equivalent to using: +// +// client.ProjectContributor.Create(). +// OnConflict(sql.ConflictColumns(columns...)). +// Exec(ctx) +func (_c *ProjectContributorCreateBulk) OnConflictColumns(columns ...string) *ProjectContributorUpsertBulk { + _c.conflict = append(_c.conflict, sql.ConflictColumns(columns...)) + return &ProjectContributorUpsertBulk{ + create: _c, + } +} + +// ProjectContributorUpsertBulk is the builder for "upsert"-ing +// a bulk of ProjectContributor nodes. +type ProjectContributorUpsertBulk struct { + create *ProjectContributorCreateBulk +} + +// UpdateNewValues updates the mutable fields using the new values that +// were set on create. Using this option is equivalent to using: +// +// client.ProjectContributor.Create(). +// OnConflict( +// sql.ResolveWithNewValues(), +// sql.ResolveWith(func(u *sql.UpdateSet) { +// u.SetIgnore(projectcontributor.FieldID) +// }), +// ). +// Exec(ctx) +func (u *ProjectContributorUpsertBulk) UpdateNewValues() *ProjectContributorUpsertBulk { + u.create.conflict = append(u.create.conflict, sql.ResolveWithNewValues()) + u.create.conflict = append(u.create.conflict, sql.ResolveWith(func(s *sql.UpdateSet) { + for _, b := range u.create.builders { + if _, exists := b.mutation.ID(); exists { + s.SetIgnore(projectcontributor.FieldID) + } + } + })) + return u +} + +// Ignore sets each column to itself in case of conflict. +// Using this option is equivalent to using: +// +// client.ProjectContributor.Create(). +// OnConflict(sql.ResolveWithIgnore()). +// Exec(ctx) +func (u *ProjectContributorUpsertBulk) Ignore() *ProjectContributorUpsertBulk { + u.create.conflict = append(u.create.conflict, sql.ResolveWithIgnore()) + return u +} + +// DoNothing configures the conflict_action to `DO NOTHING`. +// Supported only by SQLite and PostgreSQL. +func (u *ProjectContributorUpsertBulk) DoNothing() *ProjectContributorUpsertBulk { + u.create.conflict = append(u.create.conflict, sql.DoNothing()) + return u +} + +// Update allows overriding fields `UPDATE` values. See the ProjectContributorCreateBulk.OnConflict +// documentation for more info. +func (u *ProjectContributorUpsertBulk) Update(set func(*ProjectContributorUpsert)) *ProjectContributorUpsertBulk { + u.create.conflict = append(u.create.conflict, sql.ResolveWith(func(update *sql.UpdateSet) { + set(&ProjectContributorUpsert{UpdateSet: update}) + })) + return u +} + +// SetProjectID sets the "project_id" field. +func (u *ProjectContributorUpsertBulk) SetProjectID(v uuid.UUID) *ProjectContributorUpsertBulk { + return u.Update(func(s *ProjectContributorUpsert) { + s.SetProjectID(v) + }) +} + +// UpdateProjectID sets the "project_id" field to the value that was provided on create. +func (u *ProjectContributorUpsertBulk) UpdateProjectID() *ProjectContributorUpsertBulk { + return u.Update(func(s *ProjectContributorUpsert) { + s.UpdateProjectID() + }) +} + +// SetBrokerID sets the "broker_id" field. +func (u *ProjectContributorUpsertBulk) SetBrokerID(v uuid.UUID) *ProjectContributorUpsertBulk { + return u.Update(func(s *ProjectContributorUpsert) { + s.SetBrokerID(v) + }) +} + +// UpdateBrokerID sets the "broker_id" field to the value that was provided on create. +func (u *ProjectContributorUpsertBulk) UpdateBrokerID() *ProjectContributorUpsertBulk { + return u.Update(func(s *ProjectContributorUpsert) { + s.UpdateBrokerID() + }) +} + +// SetBrokerName sets the "broker_name" field. +func (u *ProjectContributorUpsertBulk) SetBrokerName(v string) *ProjectContributorUpsertBulk { + return u.Update(func(s *ProjectContributorUpsert) { + s.SetBrokerName(v) + }) +} + +// UpdateBrokerName sets the "broker_name" field to the value that was provided on create. +func (u *ProjectContributorUpsertBulk) UpdateBrokerName() *ProjectContributorUpsertBulk { + return u.Update(func(s *ProjectContributorUpsert) { + s.UpdateBrokerName() + }) +} + +// SetMode sets the "mode" field. +func (u *ProjectContributorUpsertBulk) SetMode(v string) *ProjectContributorUpsertBulk { + return u.Update(func(s *ProjectContributorUpsert) { + s.SetMode(v) + }) +} + +// UpdateMode sets the "mode" field to the value that was provided on create. +func (u *ProjectContributorUpsertBulk) UpdateMode() *ProjectContributorUpsertBulk { + return u.Update(func(s *ProjectContributorUpsert) { + s.UpdateMode() + }) +} + +// SetStatus sets the "status" field. +func (u *ProjectContributorUpsertBulk) SetStatus(v string) *ProjectContributorUpsertBulk { + return u.Update(func(s *ProjectContributorUpsert) { + s.SetStatus(v) + }) +} + +// UpdateStatus sets the "status" field to the value that was provided on create. +func (u *ProjectContributorUpsertBulk) UpdateStatus() *ProjectContributorUpsertBulk { + return u.Update(func(s *ProjectContributorUpsert) { + s.UpdateStatus() + }) +} + +// SetProfiles sets the "profiles" field. +func (u *ProjectContributorUpsertBulk) SetProfiles(v string) *ProjectContributorUpsertBulk { + return u.Update(func(s *ProjectContributorUpsert) { + s.SetProfiles(v) + }) +} + +// UpdateProfiles sets the "profiles" field to the value that was provided on create. +func (u *ProjectContributorUpsertBulk) UpdateProfiles() *ProjectContributorUpsertBulk { + return u.Update(func(s *ProjectContributorUpsert) { + s.UpdateProfiles() + }) +} + +// ClearProfiles clears the value of the "profiles" field. +func (u *ProjectContributorUpsertBulk) ClearProfiles() *ProjectContributorUpsertBulk { + return u.Update(func(s *ProjectContributorUpsert) { + s.ClearProfiles() + }) +} + +// SetLastSeen sets the "last_seen" field. +func (u *ProjectContributorUpsertBulk) SetLastSeen(v time.Time) *ProjectContributorUpsertBulk { + return u.Update(func(s *ProjectContributorUpsert) { + s.SetLastSeen(v) + }) +} + +// UpdateLastSeen sets the "last_seen" field to the value that was provided on create. +func (u *ProjectContributorUpsertBulk) UpdateLastSeen() *ProjectContributorUpsertBulk { + return u.Update(func(s *ProjectContributorUpsert) { + s.UpdateLastSeen() + }) +} + +// ClearLastSeen clears the value of the "last_seen" field. +func (u *ProjectContributorUpsertBulk) ClearLastSeen() *ProjectContributorUpsertBulk { + return u.Update(func(s *ProjectContributorUpsert) { + s.ClearLastSeen() + }) +} + +// SetLocalPath sets the "local_path" field. +func (u *ProjectContributorUpsertBulk) SetLocalPath(v string) *ProjectContributorUpsertBulk { + return u.Update(func(s *ProjectContributorUpsert) { + s.SetLocalPath(v) + }) +} + +// UpdateLocalPath sets the "local_path" field to the value that was provided on create. +func (u *ProjectContributorUpsertBulk) UpdateLocalPath() *ProjectContributorUpsertBulk { + return u.Update(func(s *ProjectContributorUpsert) { + s.UpdateLocalPath() + }) +} + +// ClearLocalPath clears the value of the "local_path" field. +func (u *ProjectContributorUpsertBulk) ClearLocalPath() *ProjectContributorUpsertBulk { + return u.Update(func(s *ProjectContributorUpsert) { + s.ClearLocalPath() + }) +} + +// SetLinkedBy sets the "linked_by" field. +func (u *ProjectContributorUpsertBulk) SetLinkedBy(v string) *ProjectContributorUpsertBulk { + return u.Update(func(s *ProjectContributorUpsert) { + s.SetLinkedBy(v) + }) +} + +// UpdateLinkedBy sets the "linked_by" field to the value that was provided on create. +func (u *ProjectContributorUpsertBulk) UpdateLinkedBy() *ProjectContributorUpsertBulk { + return u.Update(func(s *ProjectContributorUpsert) { + s.UpdateLinkedBy() + }) +} + +// ClearLinkedBy clears the value of the "linked_by" field. +func (u *ProjectContributorUpsertBulk) ClearLinkedBy() *ProjectContributorUpsertBulk { + return u.Update(func(s *ProjectContributorUpsert) { + s.ClearLinkedBy() + }) +} + +// SetLinkedAt sets the "linked_at" field. +func (u *ProjectContributorUpsertBulk) SetLinkedAt(v time.Time) *ProjectContributorUpsertBulk { + return u.Update(func(s *ProjectContributorUpsert) { + s.SetLinkedAt(v) + }) +} + +// UpdateLinkedAt sets the "linked_at" field to the value that was provided on create. +func (u *ProjectContributorUpsertBulk) UpdateLinkedAt() *ProjectContributorUpsertBulk { + return u.Update(func(s *ProjectContributorUpsert) { + s.UpdateLinkedAt() + }) +} + +// ClearLinkedAt clears the value of the "linked_at" field. +func (u *ProjectContributorUpsertBulk) ClearLinkedAt() *ProjectContributorUpsertBulk { + return u.Update(func(s *ProjectContributorUpsert) { + s.ClearLinkedAt() + }) +} + +// Exec executes the query. +func (u *ProjectContributorUpsertBulk) Exec(ctx context.Context) error { + if u.create.err != nil { + return u.create.err + } + for i, b := range u.create.builders { + if len(b.conflict) != 0 { + return fmt.Errorf("ent: OnConflict was set for builder %d. Set it on the ProjectContributorCreateBulk instead", i) + } + } + if len(u.create.conflict) == 0 { + return errors.New("ent: missing options for ProjectContributorCreateBulk.OnConflict") + } + return u.create.Exec(ctx) +} + +// ExecX is like Exec, but panics if an error occurs. +func (u *ProjectContributorUpsertBulk) ExecX(ctx context.Context) { + if err := u.create.Exec(ctx); err != nil { + panic(err) + } +} diff --git a/pkg/ent/projectcontributor_delete.go b/pkg/ent/projectcontributor_delete.go new file mode 100644 index 000000000..974b17485 --- /dev/null +++ b/pkg/ent/projectcontributor_delete.go @@ -0,0 +1,88 @@ +// Code generated by ent, DO NOT EDIT. + +package ent + +import ( + "context" + + "entgo.io/ent/dialect/sql" + "entgo.io/ent/dialect/sql/sqlgraph" + "entgo.io/ent/schema/field" + "github.com/GoogleCloudPlatform/scion/pkg/ent/predicate" + "github.com/GoogleCloudPlatform/scion/pkg/ent/projectcontributor" +) + +// ProjectContributorDelete is the builder for deleting a ProjectContributor entity. +type ProjectContributorDelete struct { + config + hooks []Hook + mutation *ProjectContributorMutation +} + +// Where appends a list predicates to the ProjectContributorDelete builder. +func (_d *ProjectContributorDelete) Where(ps ...predicate.ProjectContributor) *ProjectContributorDelete { + _d.mutation.Where(ps...) + return _d +} + +// Exec executes the deletion query and returns how many vertices were deleted. +func (_d *ProjectContributorDelete) Exec(ctx context.Context) (int, error) { + return withHooks(ctx, _d.sqlExec, _d.mutation, _d.hooks) +} + +// ExecX is like Exec, but panics if an error occurs. +func (_d *ProjectContributorDelete) ExecX(ctx context.Context) int { + n, err := _d.Exec(ctx) + if err != nil { + panic(err) + } + return n +} + +func (_d *ProjectContributorDelete) sqlExec(ctx context.Context) (int, error) { + _spec := sqlgraph.NewDeleteSpec(projectcontributor.Table, sqlgraph.NewFieldSpec(projectcontributor.FieldID, field.TypeUUID)) + if ps := _d.mutation.predicates; len(ps) > 0 { + _spec.Predicate = func(selector *sql.Selector) { + for i := range ps { + ps[i](selector) + } + } + } + affected, err := sqlgraph.DeleteNodes(ctx, _d.driver, _spec) + if err != nil && sqlgraph.IsConstraintError(err) { + err = &ConstraintError{msg: err.Error(), wrap: err} + } + _d.mutation.done = true + return affected, err +} + +// ProjectContributorDeleteOne is the builder for deleting a single ProjectContributor entity. +type ProjectContributorDeleteOne struct { + _d *ProjectContributorDelete +} + +// Where appends a list predicates to the ProjectContributorDelete builder. +func (_d *ProjectContributorDeleteOne) Where(ps ...predicate.ProjectContributor) *ProjectContributorDeleteOne { + _d._d.mutation.Where(ps...) + return _d +} + +// Exec executes the deletion query. +func (_d *ProjectContributorDeleteOne) Exec(ctx context.Context) error { + n, err := _d._d.Exec(ctx) + switch { + case err != nil: + return err + case n == 0: + return &NotFoundError{projectcontributor.Label} + default: + return nil + } +} + +// ExecX is like Exec, but panics if an error occurs. +func (_d *ProjectContributorDeleteOne) ExecX(ctx context.Context) { + if err := _d.Exec(ctx); err != nil { + panic(err) + } +} diff --git a/pkg/ent/projectcontributor_query.go b/pkg/ent/projectcontributor_query.go new file mode 100644 index 000000000..9398e2ec2 --- /dev/null +++ b/pkg/ent/projectcontributor_query.go @@ -0,0 +1,565 @@ +// Code generated by ent, DO NOT EDIT. + +package ent + +import ( + "context" + "fmt" + "math" + + "entgo.io/ent" + "entgo.io/ent/dialect" + "entgo.io/ent/dialect/sql" + "entgo.io/ent/dialect/sql/sqlgraph" + "entgo.io/ent/schema/field" + "github.com/GoogleCloudPlatform/scion/pkg/ent/predicate" + "github.com/GoogleCloudPlatform/scion/pkg/ent/projectcontributor" + "github.com/google/uuid" +) + +// ProjectContributorQuery is the builder for querying ProjectContributor entities. +type ProjectContributorQuery struct { + config + ctx *QueryContext + order []projectcontributor.OrderOption + inters []Interceptor + predicates []predicate.ProjectContributor + modifiers []func(*sql.Selector) + // intermediate query (i.e. traversal path). + sql *sql.Selector + path func(context.Context) (*sql.Selector, error) +} + +// Where adds a new predicate for the ProjectContributorQuery builder. +func (_q *ProjectContributorQuery) Where(ps ...predicate.ProjectContributor) *ProjectContributorQuery { + _q.predicates = append(_q.predicates, ps...) + return _q +} + +// Limit the number of records to be returned by this query. +func (_q *ProjectContributorQuery) Limit(limit int) *ProjectContributorQuery { + _q.ctx.Limit = &limit + return _q +} + +// Offset to start from. +func (_q *ProjectContributorQuery) Offset(offset int) *ProjectContributorQuery { + _q.ctx.Offset = &offset + return _q +} + +// Unique configures the query builder to filter duplicate records on query. +// By default, unique is set to true, and can be disabled using this method. +func (_q *ProjectContributorQuery) Unique(unique bool) *ProjectContributorQuery { + _q.ctx.Unique = &unique + return _q +} + +// Order specifies how the records should be ordered. +func (_q *ProjectContributorQuery) Order(o ...projectcontributor.OrderOption) *ProjectContributorQuery { + _q.order = append(_q.order, o...) + return _q +} + +// First returns the first ProjectContributor entity from the query. +// Returns a *NotFoundError when no ProjectContributor was found. +func (_q *ProjectContributorQuery) First(ctx context.Context) (*ProjectContributor, error) { + nodes, err := _q.Limit(1).All(setContextOp(ctx, _q.ctx, ent.OpQueryFirst)) + if err != nil { + return nil, err + } + if len(nodes) == 0 { + return nil, &NotFoundError{projectcontributor.Label} + } + return nodes[0], nil +} + +// FirstX is like First, but panics if an error occurs. +func (_q *ProjectContributorQuery) FirstX(ctx context.Context) *ProjectContributor { + node, err := _q.First(ctx) + if err != nil && !IsNotFound(err) { + panic(err) + } + return node +} + +// FirstID returns the first ProjectContributor ID from the query. +// Returns a *NotFoundError when no ProjectContributor ID was found. +func (_q *ProjectContributorQuery) FirstID(ctx context.Context) (id uuid.UUID, err error) { + var ids []uuid.UUID + if ids, err = _q.Limit(1).IDs(setContextOp(ctx, _q.ctx, ent.OpQueryFirstID)); err != nil { + return + } + if len(ids) == 0 { + err = &NotFoundError{projectcontributor.Label} + return + } + return ids[0], nil +} + +// FirstIDX is like FirstID, but panics if an error occurs. +func (_q *ProjectContributorQuery) FirstIDX(ctx context.Context) uuid.UUID { + id, err := _q.FirstID(ctx) + if err != nil && !IsNotFound(err) { + panic(err) + } + return id +} + +// Only returns a single ProjectContributor entity found by the query, ensuring it only returns one. +// Returns a *NotSingularError when more than one ProjectContributor entity is found. +// Returns a *NotFoundError when no ProjectContributor entities are found. +func (_q *ProjectContributorQuery) Only(ctx context.Context) (*ProjectContributor, error) { + nodes, err := _q.Limit(2).All(setContextOp(ctx, _q.ctx, ent.OpQueryOnly)) + if err != nil { + return nil, err + } + switch len(nodes) { + case 1: + return nodes[0], nil + case 0: + return nil, &NotFoundError{projectcontributor.Label} + default: + return nil, &NotSingularError{projectcontributor.Label} + } +} + +// OnlyX is like Only, but panics if an error occurs. +func (_q *ProjectContributorQuery) OnlyX(ctx context.Context) *ProjectContributor { + node, err := _q.Only(ctx) + if err != nil { + panic(err) + } + return node +} + +// OnlyID is like Only, but returns the only ProjectContributor ID in the query. +// Returns a *NotSingularError when more than one ProjectContributor ID is found. +// Returns a *NotFoundError when no entities are found. +func (_q *ProjectContributorQuery) OnlyID(ctx context.Context) (id uuid.UUID, err error) { + var ids []uuid.UUID + if ids, err = _q.Limit(2).IDs(setContextOp(ctx, _q.ctx, ent.OpQueryOnlyID)); err != nil { + return + } + switch len(ids) { + case 1: + id = ids[0] + case 0: + err = &NotFoundError{projectcontributor.Label} + default: + err = &NotSingularError{projectcontributor.Label} + } + return +} + +// OnlyIDX is like OnlyID, but panics if an error occurs. +func (_q *ProjectContributorQuery) OnlyIDX(ctx context.Context) uuid.UUID { + id, err := _q.OnlyID(ctx) + if err != nil { + panic(err) + } + return id +} + +// All executes the query and returns a list of ProjectContributors. +func (_q *ProjectContributorQuery) All(ctx context.Context) ([]*ProjectContributor, error) { + ctx = setContextOp(ctx, _q.ctx, ent.OpQueryAll) + if err := _q.prepareQuery(ctx); err != nil { + return nil, err + } + qr := querierAll[[]*ProjectContributor, *ProjectContributorQuery]() + return withInterceptors[[]*ProjectContributor](ctx, _q, qr, _q.inters) +} + +// AllX is like All, but panics if an error occurs. +func (_q *ProjectContributorQuery) AllX(ctx context.Context) []*ProjectContributor { + nodes, err := _q.All(ctx) + if err != nil { + panic(err) + } + return nodes +} + +// IDs executes the query and returns a list of ProjectContributor IDs. +func (_q *ProjectContributorQuery) IDs(ctx context.Context) (ids []uuid.UUID, err error) { + if _q.ctx.Unique == nil && _q.path != nil { + _q.Unique(true) + } + ctx = setContextOp(ctx, _q.ctx, ent.OpQueryIDs) + if err = _q.Select(projectcontributor.FieldID).Scan(ctx, &ids); err != nil { + return nil, err + } + return ids, nil +} + +// IDsX is like IDs, but panics if an error occurs. +func (_q *ProjectContributorQuery) IDsX(ctx context.Context) []uuid.UUID { + ids, err := _q.IDs(ctx) + if err != nil { + panic(err) + } + return ids +} + +// Count returns the count of the given query. +func (_q *ProjectContributorQuery) Count(ctx context.Context) (int, error) { + ctx = setContextOp(ctx, _q.ctx, ent.OpQueryCount) + if err := _q.prepareQuery(ctx); err != nil { + return 0, err + } + return withInterceptors[int](ctx, _q, querierCount[*ProjectContributorQuery](), _q.inters) +} + +// CountX is like Count, but panics if an error occurs. +func (_q *ProjectContributorQuery) CountX(ctx context.Context) int { + count, err := _q.Count(ctx) + if err != nil { + panic(err) + } + return count +} + +// Exist returns true if the query has elements in the graph. +func (_q *ProjectContributorQuery) Exist(ctx context.Context) (bool, error) { + ctx = setContextOp(ctx, _q.ctx, ent.OpQueryExist) + switch _, err := _q.FirstID(ctx); { + case IsNotFound(err): + return false, nil + case err != nil: + return false, fmt.Errorf("ent: check existence: %w", err) + default: + return true, nil + } +} + +// ExistX is like Exist, but panics if an error occurs. +func (_q *ProjectContributorQuery) ExistX(ctx context.Context) bool { + exist, err := _q.Exist(ctx) + if err != nil { + panic(err) + } + return exist +} + +// Clone returns a duplicate of the ProjectContributorQuery builder, including all associated steps. It can be +// used to prepare common query builders and use them differently after the clone is made. +func (_q *ProjectContributorQuery) Clone() *ProjectContributorQuery { + if _q == nil { + return nil + } + return &ProjectContributorQuery{ + config: _q.config, + ctx: _q.ctx.Clone(), + order: append([]projectcontributor.OrderOption{}, _q.order...), + inters: append([]Interceptor{}, _q.inters...), + predicates: append([]predicate.ProjectContributor{}, _q.predicates...), + // clone intermediate query. + sql: _q.sql.Clone(), + path: _q.path, + } +} + +// GroupBy is used to group vertices by one or more fields/columns. +// It is often used with aggregate functions, like: count, max, mean, min, sum. +// +// Example: +// +// var v []struct { +// ProjectID uuid.UUID `json:"project_id,omitempty"` +// Count int `json:"count,omitempty"` +// } +// +// client.ProjectContributor.Query(). +// GroupBy(projectcontributor.FieldProjectID). +// Aggregate(ent.Count()). +// Scan(ctx, &v) +func (_q *ProjectContributorQuery) GroupBy(field string, fields ...string) *ProjectContributorGroupBy { + _q.ctx.Fields = append([]string{field}, fields...) + grbuild := &ProjectContributorGroupBy{build: _q} + grbuild.flds = &_q.ctx.Fields + grbuild.label = projectcontributor.Label + grbuild.scan = grbuild.Scan + return grbuild +} + +// Select allows the selection one or more fields/columns for the given query, +// instead of selecting all fields in the entity. +// +// Example: +// +// var v []struct { +// ProjectID uuid.UUID `json:"project_id,omitempty"` +// } +// +// client.ProjectContributor.Query(). +// Select(projectcontributor.FieldProjectID). +// Scan(ctx, &v) +func (_q *ProjectContributorQuery) Select(fields ...string) *ProjectContributorSelect { + _q.ctx.Fields = append(_q.ctx.Fields, fields...) + sbuild := &ProjectContributorSelect{ProjectContributorQuery: _q} + sbuild.label = projectcontributor.Label + sbuild.flds, sbuild.scan = &_q.ctx.Fields, sbuild.Scan + return sbuild +} + +// Aggregate returns a ProjectContributorSelect configured with the given aggregations. +func (_q *ProjectContributorQuery) Aggregate(fns ...AggregateFunc) *ProjectContributorSelect { + return _q.Select().Aggregate(fns...) +} + +func (_q *ProjectContributorQuery) prepareQuery(ctx context.Context) error { + for _, inter := range _q.inters { + if inter == nil { + return fmt.Errorf("ent: uninitialized interceptor (forgotten import ent/runtime?)") + } + if trv, ok := inter.(Traverser); ok { + if err := trv.Traverse(ctx, _q); err != nil { + return err + } + } + } + for _, f := range _q.ctx.Fields { + if !projectcontributor.ValidColumn(f) { + return &ValidationError{Name: f, err: fmt.Errorf("ent: invalid field %q for query", f)} + } + } + if _q.path != nil { + prev, err := _q.path(ctx) + if err != nil { + return err + } + _q.sql = prev + } + return nil +} + +func (_q *ProjectContributorQuery) sqlAll(ctx context.Context, hooks ...queryHook) ([]*ProjectContributor, error) { + var ( + nodes = []*ProjectContributor{} + _spec = _q.querySpec() + ) + _spec.ScanValues = func(columns []string) ([]any, error) { + return (*ProjectContributor).scanValues(nil, columns) + } + _spec.Assign = func(columns []string, values []any) error { + node := &ProjectContributor{config: _q.config} + nodes = append(nodes, node) + return node.assignValues(columns, values) + } + if len(_q.modifiers) > 0 { + _spec.Modifiers = _q.modifiers + } + for i := range hooks { + hooks[i](ctx, _spec) + } + if err := sqlgraph.QueryNodes(ctx, _q.driver, _spec); err != nil { + return nil, err + } + if len(nodes) == 0 { + return nodes, nil + } + return nodes, nil +} + +func (_q *ProjectContributorQuery) sqlCount(ctx context.Context) (int, error) { + _spec := _q.querySpec() + if len(_q.modifiers) > 0 { + _spec.Modifiers = _q.modifiers + } + _spec.Node.Columns = _q.ctx.Fields + if len(_q.ctx.Fields) > 0 { + _spec.Unique = _q.ctx.Unique != nil && *_q.ctx.Unique + } + return sqlgraph.CountNodes(ctx, _q.driver, _spec) +} + +func (_q *ProjectContributorQuery) querySpec() *sqlgraph.QuerySpec { + _spec := sqlgraph.NewQuerySpec(projectcontributor.Table, projectcontributor.Columns, sqlgraph.NewFieldSpec(projectcontributor.FieldID, field.TypeUUID)) + _spec.From = _q.sql + if unique := _q.ctx.Unique; unique != nil { + _spec.Unique = *unique + } else if _q.path != nil { + _spec.Unique = true + } + if fields := _q.ctx.Fields; len(fields) > 0 { + _spec.Node.Columns = make([]string, 0, len(fields)) + _spec.Node.Columns = append(_spec.Node.Columns, projectcontributor.FieldID) + for i := range fields { + if fields[i] != projectcontributor.FieldID { + _spec.Node.Columns = append(_spec.Node.Columns, fields[i]) + } + } + } + if ps := _q.predicates; len(ps) > 0 { + _spec.Predicate = func(selector *sql.Selector) { + for i := range ps { + ps[i](selector) + } + } + } + if limit := _q.ctx.Limit; limit != nil { + _spec.Limit = *limit + } + if offset := _q.ctx.Offset; offset != nil { + _spec.Offset = *offset + } + if ps := _q.order; len(ps) > 0 { + _spec.Order = func(selector *sql.Selector) { + for i := range ps { + ps[i](selector) + } + } + } + return _spec +} + +func (_q *ProjectContributorQuery) sqlQuery(ctx context.Context) *sql.Selector { + builder := sql.Dialect(_q.driver.Dialect()) + t1 := builder.Table(projectcontributor.Table) + columns := _q.ctx.Fields + if len(columns) == 0 { + columns = projectcontributor.Columns + } + selector := builder.Select(t1.Columns(columns...)...).From(t1) + if _q.sql != nil { + selector = _q.sql + selector.Select(selector.Columns(columns...)...) + } + if _q.ctx.Unique != nil && *_q.ctx.Unique { + selector.Distinct() + } + for _, m := range _q.modifiers { + m(selector) + } + for _, p := range _q.predicates { + p(selector) + } + for _, p := range _q.order { + p(selector) + } + if offset := _q.ctx.Offset; offset != nil { + // limit is mandatory for offset clause. We start + // with default value, and override it below if needed. + selector.Offset(*offset).Limit(math.MaxInt32) + } + if limit := _q.ctx.Limit; limit != nil { + selector.Limit(*limit) + } + return selector +} + +// ForUpdate locks the selected rows against concurrent updates, and prevent them from being +// updated, deleted or "selected ... for update" by other sessions, until the transaction is +// either committed or rolled-back. +func (_q *ProjectContributorQuery) ForUpdate(opts ...sql.LockOption) *ProjectContributorQuery { + if _q.driver.Dialect() == dialect.Postgres { + _q.Unique(false) + } + _q.modifiers = append(_q.modifiers, func(s *sql.Selector) { + s.ForUpdate(opts...) + }) + return _q +} + +// ForShare behaves similarly to ForUpdate, except that it acquires a shared mode lock +// on any rows that are read. Other sessions can read the rows, but cannot modify them +// until your transaction commits. +func (_q *ProjectContributorQuery) ForShare(opts ...sql.LockOption) *ProjectContributorQuery { + if _q.driver.Dialect() == dialect.Postgres { + _q.Unique(false) + } + _q.modifiers = append(_q.modifiers, func(s *sql.Selector) { + s.ForShare(opts...) + }) + return _q +} + +// ProjectContributorGroupBy is the group-by builder for ProjectContributor entities. +type ProjectContributorGroupBy struct { + selector + build *ProjectContributorQuery +} + +// Aggregate adds the given aggregation functions to the group-by query. +func (_g *ProjectContributorGroupBy) Aggregate(fns ...AggregateFunc) *ProjectContributorGroupBy { + _g.fns = append(_g.fns, fns...) + return _g +} + +// Scan applies the selector query and scans the result into the given value. +func (_g *ProjectContributorGroupBy) Scan(ctx context.Context, v any) error { + ctx = setContextOp(ctx, _g.build.ctx, ent.OpQueryGroupBy) + if err := _g.build.prepareQuery(ctx); err != nil { + return err + } + return scanWithInterceptors[*ProjectContributorQuery, *ProjectContributorGroupBy](ctx, _g.build, _g, _g.build.inters, v) +} + +func (_g *ProjectContributorGroupBy) sqlScan(ctx context.Context, root *ProjectContributorQuery, v any) error { + selector := root.sqlQuery(ctx).Select() + aggregation := make([]string, 0, len(_g.fns)) + for _, fn := range _g.fns { + aggregation = append(aggregation, fn(selector)) + } + if len(selector.SelectedColumns()) == 0 { + columns := make([]string, 0, len(*_g.flds)+len(_g.fns)) + for _, f := range *_g.flds { + columns = append(columns, selector.C(f)) + } + columns = append(columns, aggregation...) + selector.Select(columns...) + } + selector.GroupBy(selector.Columns(*_g.flds...)...) + if err := selector.Err(); err != nil { + return err + } + rows := &sql.Rows{} + query, args := selector.Query() + if err := _g.build.driver.Query(ctx, query, args, rows); err != nil { + return err + } + defer rows.Close() + return sql.ScanSlice(rows, v) +} + +// ProjectContributorSelect is the builder for selecting fields of ProjectContributor entities. +type ProjectContributorSelect struct { + *ProjectContributorQuery + selector +} + +// Aggregate adds the given aggregation functions to the selector query. +func (_s *ProjectContributorSelect) Aggregate(fns ...AggregateFunc) *ProjectContributorSelect { + _s.fns = append(_s.fns, fns...) + return _s +} + +// Scan applies the selector query and scans the result into the given value. +func (_s *ProjectContributorSelect) Scan(ctx context.Context, v any) error { + ctx = setContextOp(ctx, _s.ctx, ent.OpQuerySelect) + if err := _s.prepareQuery(ctx); err != nil { + return err + } + return scanWithInterceptors[*ProjectContributorQuery, *ProjectContributorSelect](ctx, _s.ProjectContributorQuery, _s, _s.inters, v) +} + +func (_s *ProjectContributorSelect) sqlScan(ctx context.Context, root *ProjectContributorQuery, v any) error { + selector := root.sqlQuery(ctx) + aggregation := make([]string, 0, len(_s.fns)) + for _, fn := range _s.fns { + aggregation = append(aggregation, fn(selector)) + } + switch n := len(*_s.selector.flds); { + case n == 0 && len(aggregation) > 0: + selector.Select(aggregation...) + case n != 0 && len(aggregation) > 0: + selector.AppendSelect(aggregation...) + } + rows := &sql.Rows{} + query, args := selector.Query() + if err := _s.driver.Query(ctx, query, args, rows); err != nil { + return err + } + defer rows.Close() + return sql.ScanSlice(rows, v) +} diff --git a/pkg/ent/projectcontributor_update.go b/pkg/ent/projectcontributor_update.go new file mode 100644 index 000000000..644dccc62 --- /dev/null +++ b/pkg/ent/projectcontributor_update.go @@ -0,0 +1,633 @@ +// Code generated by ent, DO NOT EDIT. + +package ent + +import ( + "context" + "errors" + "fmt" + "time" + + "entgo.io/ent/dialect/sql" + "entgo.io/ent/dialect/sql/sqlgraph" + "entgo.io/ent/schema/field" + "github.com/GoogleCloudPlatform/scion/pkg/ent/predicate" + "github.com/GoogleCloudPlatform/scion/pkg/ent/projectcontributor" + "github.com/google/uuid" +) + +// ProjectContributorUpdate is the builder for updating ProjectContributor entities. +type ProjectContributorUpdate struct { + config + hooks []Hook + mutation *ProjectContributorMutation +} + +// Where appends a list predicates to the ProjectContributorUpdate builder. +func (_u *ProjectContributorUpdate) Where(ps ...predicate.ProjectContributor) *ProjectContributorUpdate { + _u.mutation.Where(ps...) + return _u +} + +// SetProjectID sets the "project_id" field. +func (_u *ProjectContributorUpdate) SetProjectID(v uuid.UUID) *ProjectContributorUpdate { + _u.mutation.SetProjectID(v) + return _u +} + +// SetNillableProjectID sets the "project_id" field if the given value is not nil. +func (_u *ProjectContributorUpdate) SetNillableProjectID(v *uuid.UUID) *ProjectContributorUpdate { + if v != nil { + _u.SetProjectID(*v) + } + return _u +} + +// SetBrokerID sets the "broker_id" field. +func (_u *ProjectContributorUpdate) SetBrokerID(v uuid.UUID) *ProjectContributorUpdate { + _u.mutation.SetBrokerID(v) + return _u +} + +// SetNillableBrokerID sets the "broker_id" field if the given value is not nil. +func (_u *ProjectContributorUpdate) SetNillableBrokerID(v *uuid.UUID) *ProjectContributorUpdate { + if v != nil { + _u.SetBrokerID(*v) + } + return _u +} + +// SetBrokerName sets the "broker_name" field. +func (_u *ProjectContributorUpdate) SetBrokerName(v string) *ProjectContributorUpdate { + _u.mutation.SetBrokerName(v) + return _u +} + +// SetNillableBrokerName sets the "broker_name" field if the given value is not nil. +func (_u *ProjectContributorUpdate) SetNillableBrokerName(v *string) *ProjectContributorUpdate { + if v != nil { + _u.SetBrokerName(*v) + } + return _u +} + +// SetMode sets the "mode" field. +func (_u *ProjectContributorUpdate) SetMode(v string) *ProjectContributorUpdate { + _u.mutation.SetMode(v) + return _u +} + +// SetNillableMode sets the "mode" field if the given value is not nil. +func (_u *ProjectContributorUpdate) SetNillableMode(v *string) *ProjectContributorUpdate { + if v != nil { + _u.SetMode(*v) + } + return _u +} + +// SetStatus sets the "status" field. +func (_u *ProjectContributorUpdate) SetStatus(v string) *ProjectContributorUpdate { + _u.mutation.SetStatus(v) + return _u +} + +// SetNillableStatus sets the "status" field if the given value is not nil. +func (_u *ProjectContributorUpdate) SetNillableStatus(v *string) *ProjectContributorUpdate { + if v != nil { + _u.SetStatus(*v) + } + return _u +} + +// SetProfiles sets the "profiles" field. +func (_u *ProjectContributorUpdate) SetProfiles(v string) *ProjectContributorUpdate { + _u.mutation.SetProfiles(v) + return _u +} + +// SetNillableProfiles sets the "profiles" field if the given value is not nil. +func (_u *ProjectContributorUpdate) SetNillableProfiles(v *string) *ProjectContributorUpdate { + if v != nil { + _u.SetProfiles(*v) + } + return _u +} + +// ClearProfiles clears the value of the "profiles" field. +func (_u *ProjectContributorUpdate) ClearProfiles() *ProjectContributorUpdate { + _u.mutation.ClearProfiles() + return _u +} + +// SetLastSeen sets the "last_seen" field. +func (_u *ProjectContributorUpdate) SetLastSeen(v time.Time) *ProjectContributorUpdate { + _u.mutation.SetLastSeen(v) + return _u +} + +// SetNillableLastSeen sets the "last_seen" field if the given value is not nil. +func (_u *ProjectContributorUpdate) SetNillableLastSeen(v *time.Time) *ProjectContributorUpdate { + if v != nil { + _u.SetLastSeen(*v) + } + return _u +} + +// ClearLastSeen clears the value of the "last_seen" field. +func (_u *ProjectContributorUpdate) ClearLastSeen() *ProjectContributorUpdate { + _u.mutation.ClearLastSeen() + return _u +} + +// SetLocalPath sets the "local_path" field. +func (_u *ProjectContributorUpdate) SetLocalPath(v string) *ProjectContributorUpdate { + _u.mutation.SetLocalPath(v) + return _u +} + +// SetNillableLocalPath sets the "local_path" field if the given value is not nil. +func (_u *ProjectContributorUpdate) SetNillableLocalPath(v *string) *ProjectContributorUpdate { + if v != nil { + _u.SetLocalPath(*v) + } + return _u +} + +// ClearLocalPath clears the value of the "local_path" field. +func (_u *ProjectContributorUpdate) ClearLocalPath() *ProjectContributorUpdate { + _u.mutation.ClearLocalPath() + return _u +} + +// SetLinkedBy sets the "linked_by" field. +func (_u *ProjectContributorUpdate) SetLinkedBy(v string) *ProjectContributorUpdate { + _u.mutation.SetLinkedBy(v) + return _u +} + +// SetNillableLinkedBy sets the "linked_by" field if the given value is not nil. +func (_u *ProjectContributorUpdate) SetNillableLinkedBy(v *string) *ProjectContributorUpdate { + if v != nil { + _u.SetLinkedBy(*v) + } + return _u +} + +// ClearLinkedBy clears the value of the "linked_by" field. +func (_u *ProjectContributorUpdate) ClearLinkedBy() *ProjectContributorUpdate { + _u.mutation.ClearLinkedBy() + return _u +} + +// SetLinkedAt sets the "linked_at" field. +func (_u *ProjectContributorUpdate) SetLinkedAt(v time.Time) *ProjectContributorUpdate { + _u.mutation.SetLinkedAt(v) + return _u +} + +// SetNillableLinkedAt sets the "linked_at" field if the given value is not nil. +func (_u *ProjectContributorUpdate) SetNillableLinkedAt(v *time.Time) *ProjectContributorUpdate { + if v != nil { + _u.SetLinkedAt(*v) + } + return _u +} + +// ClearLinkedAt clears the value of the "linked_at" field. +func (_u *ProjectContributorUpdate) ClearLinkedAt() *ProjectContributorUpdate { + _u.mutation.ClearLinkedAt() + return _u +} + +// Mutation returns the ProjectContributorMutation object of the builder. +func (_u *ProjectContributorUpdate) Mutation() *ProjectContributorMutation { + return _u.mutation +} + +// Save executes the query and returns the number of nodes affected by the update operation. +func (_u *ProjectContributorUpdate) Save(ctx context.Context) (int, error) { + return withHooks(ctx, _u.sqlSave, _u.mutation, _u.hooks) +} + +// SaveX is like Save, but panics if an error occurs. +func (_u *ProjectContributorUpdate) SaveX(ctx context.Context) int { + affected, err := _u.Save(ctx) + if err != nil { + panic(err) + } + return affected +} + +// Exec executes the query. +func (_u *ProjectContributorUpdate) Exec(ctx context.Context) error { + _, err := _u.Save(ctx) + return err +} + +// ExecX is like Exec, but panics if an error occurs. +func (_u *ProjectContributorUpdate) ExecX(ctx context.Context) { + if err := _u.Exec(ctx); err != nil { + panic(err) + } +} + +// check runs all checks and user-defined validators on the builder. +func (_u *ProjectContributorUpdate) check() error { + if v, ok := _u.mutation.BrokerName(); ok { + if err := projectcontributor.BrokerNameValidator(v); err != nil { + return &ValidationError{Name: "broker_name", err: fmt.Errorf(`ent: validator failed for field "ProjectContributor.broker_name": %w`, err)} + } + } + return nil +} + +func (_u *ProjectContributorUpdate) sqlSave(ctx context.Context) (_node int, err error) { + if err := _u.check(); err != nil { + return _node, err + } + _spec := sqlgraph.NewUpdateSpec(projectcontributor.Table, projectcontributor.Columns, sqlgraph.NewFieldSpec(projectcontributor.FieldID, field.TypeUUID)) + if ps := _u.mutation.predicates; len(ps) > 0 { + _spec.Predicate = func(selector *sql.Selector) { + for i := range ps { + ps[i](selector) + } + } + } + if value, ok := _u.mutation.ProjectID(); ok { + _spec.SetField(projectcontributor.FieldProjectID, field.TypeUUID, value) + } + if value, ok := _u.mutation.BrokerID(); ok { + _spec.SetField(projectcontributor.FieldBrokerID, field.TypeUUID, value) + } + if value, ok := _u.mutation.BrokerName(); ok { + _spec.SetField(projectcontributor.FieldBrokerName, field.TypeString, value) + } + if value, ok := _u.mutation.Mode(); ok { + _spec.SetField(projectcontributor.FieldMode, field.TypeString, value) + } + if value, ok := _u.mutation.Status(); ok { + _spec.SetField(projectcontributor.FieldStatus, field.TypeString, value) + } + if value, ok := _u.mutation.Profiles(); ok { + _spec.SetField(projectcontributor.FieldProfiles, field.TypeString, value) + } + if _u.mutation.ProfilesCleared() { + _spec.ClearField(projectcontributor.FieldProfiles, field.TypeString) + } + if value, ok := _u.mutation.LastSeen(); ok { + _spec.SetField(projectcontributor.FieldLastSeen, field.TypeTime, value) + } + if _u.mutation.LastSeenCleared() { + _spec.ClearField(projectcontributor.FieldLastSeen, field.TypeTime) + } + if value, ok := _u.mutation.LocalPath(); ok { + _spec.SetField(projectcontributor.FieldLocalPath, field.TypeString, value) + } + if _u.mutation.LocalPathCleared() { + _spec.ClearField(projectcontributor.FieldLocalPath, field.TypeString) + } + if value, ok := _u.mutation.LinkedBy(); ok { + _spec.SetField(projectcontributor.FieldLinkedBy, field.TypeString, value) + } + if _u.mutation.LinkedByCleared() { + _spec.ClearField(projectcontributor.FieldLinkedBy, field.TypeString) + } + if value, ok := _u.mutation.LinkedAt(); ok { + _spec.SetField(projectcontributor.FieldLinkedAt, field.TypeTime, value) + } + if _u.mutation.LinkedAtCleared() { + _spec.ClearField(projectcontributor.FieldLinkedAt, field.TypeTime) + } + if _node, err = sqlgraph.UpdateNodes(ctx, _u.driver, _spec); err != nil { + if _, ok := err.(*sqlgraph.NotFoundError); ok { + err = &NotFoundError{projectcontributor.Label} + } else if sqlgraph.IsConstraintError(err) { + err = &ConstraintError{msg: err.Error(), wrap: err} + } + return 0, err + } + _u.mutation.done = true + return _node, nil +} + +// ProjectContributorUpdateOne is the builder for updating a single ProjectContributor entity. +type ProjectContributorUpdateOne struct { + config + fields []string + hooks []Hook + mutation *ProjectContributorMutation +} + +// SetProjectID sets the "project_id" field. +func (_u *ProjectContributorUpdateOne) SetProjectID(v uuid.UUID) *ProjectContributorUpdateOne { + _u.mutation.SetProjectID(v) + return _u +} + +// SetNillableProjectID sets the "project_id" field if the given value is not nil. +func (_u *ProjectContributorUpdateOne) SetNillableProjectID(v *uuid.UUID) *ProjectContributorUpdateOne { + if v != nil { + _u.SetProjectID(*v) + } + return _u +} + +// SetBrokerID sets the "broker_id" field. +func (_u *ProjectContributorUpdateOne) SetBrokerID(v uuid.UUID) *ProjectContributorUpdateOne { + _u.mutation.SetBrokerID(v) + return _u +} + +// SetNillableBrokerID sets the "broker_id" field if the given value is not nil. +func (_u *ProjectContributorUpdateOne) SetNillableBrokerID(v *uuid.UUID) *ProjectContributorUpdateOne { + if v != nil { + _u.SetBrokerID(*v) + } + return _u +} + +// SetBrokerName sets the "broker_name" field. +func (_u *ProjectContributorUpdateOne) SetBrokerName(v string) *ProjectContributorUpdateOne { + _u.mutation.SetBrokerName(v) + return _u +} + +// SetNillableBrokerName sets the "broker_name" field if the given value is not nil. +func (_u *ProjectContributorUpdateOne) SetNillableBrokerName(v *string) *ProjectContributorUpdateOne { + if v != nil { + _u.SetBrokerName(*v) + } + return _u +} + +// SetMode sets the "mode" field. +func (_u *ProjectContributorUpdateOne) SetMode(v string) *ProjectContributorUpdateOne { + _u.mutation.SetMode(v) + return _u +} + +// SetNillableMode sets the "mode" field if the given value is not nil. +func (_u *ProjectContributorUpdateOne) SetNillableMode(v *string) *ProjectContributorUpdateOne { + if v != nil { + _u.SetMode(*v) + } + return _u +} + +// SetStatus sets the "status" field. +func (_u *ProjectContributorUpdateOne) SetStatus(v string) *ProjectContributorUpdateOne { + _u.mutation.SetStatus(v) + return _u +} + +// SetNillableStatus sets the "status" field if the given value is not nil. +func (_u *ProjectContributorUpdateOne) SetNillableStatus(v *string) *ProjectContributorUpdateOne { + if v != nil { + _u.SetStatus(*v) + } + return _u +} + +// SetProfiles sets the "profiles" field. +func (_u *ProjectContributorUpdateOne) SetProfiles(v string) *ProjectContributorUpdateOne { + _u.mutation.SetProfiles(v) + return _u +} + +// SetNillableProfiles sets the "profiles" field if the given value is not nil. +func (_u *ProjectContributorUpdateOne) SetNillableProfiles(v *string) *ProjectContributorUpdateOne { + if v != nil { + _u.SetProfiles(*v) + } + return _u +} + +// ClearProfiles clears the value of the "profiles" field. +func (_u *ProjectContributorUpdateOne) ClearProfiles() *ProjectContributorUpdateOne { + _u.mutation.ClearProfiles() + return _u +} + +// SetLastSeen sets the "last_seen" field. +func (_u *ProjectContributorUpdateOne) SetLastSeen(v time.Time) *ProjectContributorUpdateOne { + _u.mutation.SetLastSeen(v) + return _u +} + +// SetNillableLastSeen sets the "last_seen" field if the given value is not nil. +func (_u *ProjectContributorUpdateOne) SetNillableLastSeen(v *time.Time) *ProjectContributorUpdateOne { + if v != nil { + _u.SetLastSeen(*v) + } + return _u +} + +// ClearLastSeen clears the value of the "last_seen" field. +func (_u *ProjectContributorUpdateOne) ClearLastSeen() *ProjectContributorUpdateOne { + _u.mutation.ClearLastSeen() + return _u +} + +// SetLocalPath sets the "local_path" field. +func (_u *ProjectContributorUpdateOne) SetLocalPath(v string) *ProjectContributorUpdateOne { + _u.mutation.SetLocalPath(v) + return _u +} + +// SetNillableLocalPath sets the "local_path" field if the given value is not nil. +func (_u *ProjectContributorUpdateOne) SetNillableLocalPath(v *string) *ProjectContributorUpdateOne { + if v != nil { + _u.SetLocalPath(*v) + } + return _u +} + +// ClearLocalPath clears the value of the "local_path" field. +func (_u *ProjectContributorUpdateOne) ClearLocalPath() *ProjectContributorUpdateOne { + _u.mutation.ClearLocalPath() + return _u +} + +// SetLinkedBy sets the "linked_by" field. +func (_u *ProjectContributorUpdateOne) SetLinkedBy(v string) *ProjectContributorUpdateOne { + _u.mutation.SetLinkedBy(v) + return _u +} + +// SetNillableLinkedBy sets the "linked_by" field if the given value is not nil. +func (_u *ProjectContributorUpdateOne) SetNillableLinkedBy(v *string) *ProjectContributorUpdateOne { + if v != nil { + _u.SetLinkedBy(*v) + } + return _u +} + +// ClearLinkedBy clears the value of the "linked_by" field. +func (_u *ProjectContributorUpdateOne) ClearLinkedBy() *ProjectContributorUpdateOne { + _u.mutation.ClearLinkedBy() + return _u +} + +// SetLinkedAt sets the "linked_at" field. +func (_u *ProjectContributorUpdateOne) SetLinkedAt(v time.Time) *ProjectContributorUpdateOne { + _u.mutation.SetLinkedAt(v) + return _u +} + +// SetNillableLinkedAt sets the "linked_at" field if the given value is not nil. +func (_u *ProjectContributorUpdateOne) SetNillableLinkedAt(v *time.Time) *ProjectContributorUpdateOne { + if v != nil { + _u.SetLinkedAt(*v) + } + return _u +} + +// ClearLinkedAt clears the value of the "linked_at" field. +func (_u *ProjectContributorUpdateOne) ClearLinkedAt() *ProjectContributorUpdateOne { + _u.mutation.ClearLinkedAt() + return _u +} + +// Mutation returns the ProjectContributorMutation object of the builder. +func (_u *ProjectContributorUpdateOne) Mutation() *ProjectContributorMutation { + return _u.mutation +} + +// Where appends a list predicates to the ProjectContributorUpdate builder. +func (_u *ProjectContributorUpdateOne) Where(ps ...predicate.ProjectContributor) *ProjectContributorUpdateOne { + _u.mutation.Where(ps...) + return _u +} + +// Select allows selecting one or more fields (columns) of the returned entity. +// The default is selecting all fields defined in the entity schema. +func (_u *ProjectContributorUpdateOne) Select(field string, fields ...string) *ProjectContributorUpdateOne { + _u.fields = append([]string{field}, fields...) + return _u +} + +// Save executes the query and returns the updated ProjectContributor entity. +func (_u *ProjectContributorUpdateOne) Save(ctx context.Context) (*ProjectContributor, error) { + return withHooks(ctx, _u.sqlSave, _u.mutation, _u.hooks) +} + +// SaveX is like Save, but panics if an error occurs. +func (_u *ProjectContributorUpdateOne) SaveX(ctx context.Context) *ProjectContributor { + node, err := _u.Save(ctx) + if err != nil { + panic(err) + } + return node +} + +// Exec executes the query on the entity. +func (_u *ProjectContributorUpdateOne) Exec(ctx context.Context) error { + _, err := _u.Save(ctx) + return err +} + +// ExecX is like Exec, but panics if an error occurs. +func (_u *ProjectContributorUpdateOne) ExecX(ctx context.Context) { + if err := _u.Exec(ctx); err != nil { + panic(err) + } +} + +// check runs all checks and user-defined validators on the builder. +func (_u *ProjectContributorUpdateOne) check() error { + if v, ok := _u.mutation.BrokerName(); ok { + if err := projectcontributor.BrokerNameValidator(v); err != nil { + return &ValidationError{Name: "broker_name", err: fmt.Errorf(`ent: validator failed for field "ProjectContributor.broker_name": %w`, err)} + } + } + return nil +} + +func (_u *ProjectContributorUpdateOne) sqlSave(ctx context.Context) (_node *ProjectContributor, err error) { + if err := _u.check(); err != nil { + return _node, err + } + _spec := sqlgraph.NewUpdateSpec(projectcontributor.Table, projectcontributor.Columns, sqlgraph.NewFieldSpec(projectcontributor.FieldID, field.TypeUUID)) + id, ok := _u.mutation.ID() + if !ok { + return nil, &ValidationError{Name: "id", err: errors.New(`ent: missing "ProjectContributor.id" for update`)} + } + _spec.Node.ID.Value = id + if fields := _u.fields; len(fields) > 0 { + _spec.Node.Columns = make([]string, 0, len(fields)) + _spec.Node.Columns = append(_spec.Node.Columns, projectcontributor.FieldID) + for _, f := range fields { + if !projectcontributor.ValidColumn(f) { + return nil, &ValidationError{Name: f, err: fmt.Errorf("ent: invalid field %q for query", f)} + } + if f != projectcontributor.FieldID { + _spec.Node.Columns = append(_spec.Node.Columns, f) + } + } + } + if ps := _u.mutation.predicates; len(ps) > 0 { + _spec.Predicate = func(selector *sql.Selector) { + for i := range ps { + ps[i](selector) + } + } + } + if value, ok := _u.mutation.ProjectID(); ok { + _spec.SetField(projectcontributor.FieldProjectID, field.TypeUUID, value) + } + if value, ok := _u.mutation.BrokerID(); ok { + _spec.SetField(projectcontributor.FieldBrokerID, field.TypeUUID, value) + } + if value, ok := _u.mutation.BrokerName(); ok { + _spec.SetField(projectcontributor.FieldBrokerName, field.TypeString, value) + } + if value, ok := _u.mutation.Mode(); ok { + _spec.SetField(projectcontributor.FieldMode, field.TypeString, value) + } + if value, ok := _u.mutation.Status(); ok { + _spec.SetField(projectcontributor.FieldStatus, field.TypeString, value) + } + if value, ok := _u.mutation.Profiles(); ok { + _spec.SetField(projectcontributor.FieldProfiles, field.TypeString, value) + } + if _u.mutation.ProfilesCleared() { + _spec.ClearField(projectcontributor.FieldProfiles, field.TypeString) + } + if value, ok := _u.mutation.LastSeen(); ok { + _spec.SetField(projectcontributor.FieldLastSeen, field.TypeTime, value) + } + if _u.mutation.LastSeenCleared() { + _spec.ClearField(projectcontributor.FieldLastSeen, field.TypeTime) + } + if value, ok := _u.mutation.LocalPath(); ok { + _spec.SetField(projectcontributor.FieldLocalPath, field.TypeString, value) + } + if _u.mutation.LocalPathCleared() { + _spec.ClearField(projectcontributor.FieldLocalPath, field.TypeString) + } + if value, ok := _u.mutation.LinkedBy(); ok { + _spec.SetField(projectcontributor.FieldLinkedBy, field.TypeString, value) + } + if _u.mutation.LinkedByCleared() { + _spec.ClearField(projectcontributor.FieldLinkedBy, field.TypeString) + } + if value, ok := _u.mutation.LinkedAt(); ok { + _spec.SetField(projectcontributor.FieldLinkedAt, field.TypeTime, value) + } + if _u.mutation.LinkedAtCleared() { + _spec.ClearField(projectcontributor.FieldLinkedAt, field.TypeTime) + } + _node = &ProjectContributor{config: _u.config} + _spec.Assign = _node.assignValues + _spec.ScanValues = _node.scanValues + if err = sqlgraph.UpdateNode(ctx, _u.driver, _spec); err != nil { + if _, ok := err.(*sqlgraph.NotFoundError); ok { + err = &NotFoundError{projectcontributor.Label} + } else if sqlgraph.IsConstraintError(err) { + err = &ConstraintError{msg: err.Error(), wrap: err} + } + return nil, err + } + _u.mutation.done = true + return _node, nil +} diff --git a/pkg/ent/projectsyncstate.go b/pkg/ent/projectsyncstate.go new file mode 100644 index 000000000..5f21d5d5d --- /dev/null +++ b/pkg/ent/projectsyncstate.go @@ -0,0 +1,167 @@ +// Code generated by ent, DO NOT EDIT. + +package ent + +import ( + "fmt" + "strings" + "time" + + "entgo.io/ent" + "entgo.io/ent/dialect/sql" + "github.com/GoogleCloudPlatform/scion/pkg/ent/projectsyncstate" + "github.com/google/uuid" +) + +// ProjectSyncState is the model entity for the ProjectSyncState schema. +type ProjectSyncState struct { + config `json:"-"` + // ID of the ent. + ID uuid.UUID `json:"id,omitempty"` + // ProjectID holds the value of the "project_id" field. + ProjectID uuid.UUID `json:"project_id,omitempty"` + // BrokerID holds the value of the "broker_id" field. + BrokerID string `json:"broker_id,omitempty"` + // LastSyncTime holds the value of the "last_sync_time" field. + LastSyncTime *time.Time `json:"last_sync_time,omitempty"` + // LastCommitSha holds the value of the "last_commit_sha" field. + LastCommitSha string `json:"last_commit_sha,omitempty"` + // FileCount holds the value of the "file_count" field. + FileCount int `json:"file_count,omitempty"` + // TotalBytes holds the value of the "total_bytes" field. + TotalBytes int64 `json:"total_bytes,omitempty"` + selectValues sql.SelectValues +} + +// scanValues returns the types for scanning values from sql.Rows. +func (*ProjectSyncState) scanValues(columns []string) ([]any, error) { + values := make([]any, len(columns)) + for i := range columns { + switch columns[i] { + case projectsyncstate.FieldFileCount, projectsyncstate.FieldTotalBytes: + values[i] = new(sql.NullInt64) + case projectsyncstate.FieldBrokerID, projectsyncstate.FieldLastCommitSha: + values[i] = new(sql.NullString) + case projectsyncstate.FieldLastSyncTime: + values[i] = new(sql.NullTime) + case projectsyncstate.FieldID, projectsyncstate.FieldProjectID: + values[i] = new(uuid.UUID) + default: + values[i] = new(sql.UnknownType) + } + } + return values, nil +} + +// assignValues assigns the values that were returned from sql.Rows (after scanning) +// to the ProjectSyncState fields. +func (_m *ProjectSyncState) assignValues(columns []string, values []any) error { + if m, n := len(values), len(columns); m < n { + return fmt.Errorf("mismatch number of scan values: %d != %d", m, n) + } + for i := range columns { + switch columns[i] { + case projectsyncstate.FieldID: + if value, ok := values[i].(*uuid.UUID); !ok { + return fmt.Errorf("unexpected type %T for field id", values[i]) + } else if value != nil { + _m.ID = *value + } + case projectsyncstate.FieldProjectID: + if value, ok := values[i].(*uuid.UUID); !ok { + return fmt.Errorf("unexpected type %T for field project_id", values[i]) + } else if value != nil { + _m.ProjectID = *value + } + case projectsyncstate.FieldBrokerID: + if value, ok := values[i].(*sql.NullString); !ok { + return fmt.Errorf("unexpected type %T for field broker_id", values[i]) + } else if value.Valid { + _m.BrokerID = value.String + } + case projectsyncstate.FieldLastSyncTime: + if value, ok := values[i].(*sql.NullTime); !ok { + return fmt.Errorf("unexpected type %T for field last_sync_time", values[i]) + } else if value.Valid { + _m.LastSyncTime = new(time.Time) + *_m.LastSyncTime = value.Time + } + case projectsyncstate.FieldLastCommitSha: + if value, ok := values[i].(*sql.NullString); !ok { + return fmt.Errorf("unexpected type %T for field last_commit_sha", values[i]) + } else if value.Valid { + _m.LastCommitSha = value.String + } + case projectsyncstate.FieldFileCount: + if value, ok := values[i].(*sql.NullInt64); !ok { + return fmt.Errorf("unexpected type %T for field file_count", values[i]) + } else if value.Valid { + _m.FileCount = int(value.Int64) + } + case projectsyncstate.FieldTotalBytes: + if value, ok := values[i].(*sql.NullInt64); !ok { + return fmt.Errorf("unexpected type %T for field total_bytes", values[i]) + } else if value.Valid { + _m.TotalBytes = value.Int64 + } + default: + _m.selectValues.Set(columns[i], values[i]) + } + } + return nil +} + +// Value returns the ent.Value that was dynamically selected and assigned to the ProjectSyncState. +// This includes values selected through modifiers, order, etc. +func (_m *ProjectSyncState) Value(name string) (ent.Value, error) { + return _m.selectValues.Get(name) +} + +// Update returns a builder for updating this ProjectSyncState. +// Note that you need to call ProjectSyncState.Unwrap() before calling this method if this ProjectSyncState +// was returned from a transaction, and the transaction was committed or rolled back. +func (_m *ProjectSyncState) Update() *ProjectSyncStateUpdateOne { + return NewProjectSyncStateClient(_m.config).UpdateOne(_m) +} + +// Unwrap unwraps the ProjectSyncState entity that was returned from a transaction after it was closed, +// so that all future queries will be executed through the driver which created the transaction. +func (_m *ProjectSyncState) Unwrap() *ProjectSyncState { + _tx, ok := _m.config.driver.(*txDriver) + if !ok { + panic("ent: ProjectSyncState is not a transactional entity") + } + _m.config.driver = _tx.drv + return _m +} + +// String implements the fmt.Stringer. +func (_m *ProjectSyncState) String() string { + var builder strings.Builder + builder.WriteString("ProjectSyncState(") + builder.WriteString(fmt.Sprintf("id=%v, ", _m.ID)) + builder.WriteString("project_id=") + builder.WriteString(fmt.Sprintf("%v", _m.ProjectID)) + builder.WriteString(", ") + builder.WriteString("broker_id=") + builder.WriteString(_m.BrokerID) + builder.WriteString(", ") + if v := _m.LastSyncTime; v != nil { + builder.WriteString("last_sync_time=") + builder.WriteString(v.Format(time.ANSIC)) + } + builder.WriteString(", ") + builder.WriteString("last_commit_sha=") + builder.WriteString(_m.LastCommitSha) + builder.WriteString(", ") + builder.WriteString("file_count=") + builder.WriteString(fmt.Sprintf("%v", _m.FileCount)) + builder.WriteString(", ") + builder.WriteString("total_bytes=") + builder.WriteString(fmt.Sprintf("%v", _m.TotalBytes)) + builder.WriteByte(')') + return builder.String() +} + +// ProjectSyncStates is a parsable slice of ProjectSyncState. +type ProjectSyncStates []*ProjectSyncState diff --git a/pkg/ent/projectsyncstate/projectsyncstate.go b/pkg/ent/projectsyncstate/projectsyncstate.go new file mode 100644 index 000000000..b0154db21 --- /dev/null +++ b/pkg/ent/projectsyncstate/projectsyncstate.go @@ -0,0 +1,99 @@ +// Code generated by ent, DO NOT EDIT. + +package projectsyncstate + +import ( + "entgo.io/ent/dialect/sql" + "github.com/google/uuid" +) + +const ( + // Label holds the string label denoting the projectsyncstate type in the database. + Label = "project_sync_state" + // FieldID holds the string denoting the id field in the database. + FieldID = "id" + // FieldProjectID holds the string denoting the project_id field in the database. + FieldProjectID = "project_id" + // FieldBrokerID holds the string denoting the broker_id field in the database. + FieldBrokerID = "broker_id" + // FieldLastSyncTime holds the string denoting the last_sync_time field in the database. + FieldLastSyncTime = "last_sync_time" + // FieldLastCommitSha holds the string denoting the last_commit_sha field in the database. + FieldLastCommitSha = "last_commit_sha" + // FieldFileCount holds the string denoting the file_count field in the database. + FieldFileCount = "file_count" + // FieldTotalBytes holds the string denoting the total_bytes field in the database. + FieldTotalBytes = "total_bytes" + // Table holds the table name of the projectsyncstate in the database. + Table = "project_sync_state" +) + +// Columns holds all SQL columns for projectsyncstate fields. +var Columns = []string{ + FieldID, + FieldProjectID, + FieldBrokerID, + FieldLastSyncTime, + FieldLastCommitSha, + FieldFileCount, + FieldTotalBytes, +} + +// ValidColumn reports if the column name is valid (part of the table columns). +func ValidColumn(column string) bool { + for i := range Columns { + if column == Columns[i] { + return true + } + } + return false +} + +var ( + // DefaultBrokerID holds the default value on creation for the "broker_id" field. + DefaultBrokerID string + // DefaultFileCount holds the default value on creation for the "file_count" field. + DefaultFileCount int + // DefaultTotalBytes holds the default value on creation for the "total_bytes" field. + DefaultTotalBytes int64 + // DefaultID holds the default value on creation for the "id" field. + DefaultID func() uuid.UUID +) + +// OrderOption defines the ordering options for the ProjectSyncState queries. +type OrderOption func(*sql.Selector) + +// ByID orders the results by the id field. +func ByID(opts ...sql.OrderTermOption) OrderOption { + return sql.OrderByField(FieldID, opts...).ToFunc() +} + +// ByProjectID orders the results by the project_id field. +func ByProjectID(opts ...sql.OrderTermOption) OrderOption { + return sql.OrderByField(FieldProjectID, opts...).ToFunc() +} + +// ByBrokerID orders the results by the broker_id field. +func ByBrokerID(opts ...sql.OrderTermOption) OrderOption { + return sql.OrderByField(FieldBrokerID, opts...).ToFunc() +} + +// ByLastSyncTime orders the results by the last_sync_time field. +func ByLastSyncTime(opts ...sql.OrderTermOption) OrderOption { + return sql.OrderByField(FieldLastSyncTime, opts...).ToFunc() +} + +// ByLastCommitSha orders the results by the last_commit_sha field. +func ByLastCommitSha(opts ...sql.OrderTermOption) OrderOption { + return sql.OrderByField(FieldLastCommitSha, opts...).ToFunc() +} + +// ByFileCount orders the results by the file_count field. +func ByFileCount(opts ...sql.OrderTermOption) OrderOption { + return sql.OrderByField(FieldFileCount, opts...).ToFunc() +} + +// ByTotalBytes orders the results by the total_bytes field. +func ByTotalBytes(opts ...sql.OrderTermOption) OrderOption { + return sql.OrderByField(FieldTotalBytes, opts...).ToFunc() +} diff --git a/pkg/ent/projectsyncstate/where.go b/pkg/ent/projectsyncstate/where.go new file mode 100644 index 000000000..6d80806c4 --- /dev/null +++ b/pkg/ent/projectsyncstate/where.go @@ -0,0 +1,411 @@ +// Code generated by ent, DO NOT EDIT. + +package projectsyncstate + +import ( + "time" + + "entgo.io/ent/dialect/sql" + "github.com/GoogleCloudPlatform/scion/pkg/ent/predicate" + "github.com/google/uuid" +) + +// ID filters vertices based on their ID field. +func ID(id uuid.UUID) predicate.ProjectSyncState { + return predicate.ProjectSyncState(sql.FieldEQ(FieldID, id)) +} + +// IDEQ applies the EQ predicate on the ID field. +func IDEQ(id uuid.UUID) predicate.ProjectSyncState { + return predicate.ProjectSyncState(sql.FieldEQ(FieldID, id)) +} + +// IDNEQ applies the NEQ predicate on the ID field. +func IDNEQ(id uuid.UUID) predicate.ProjectSyncState { + return predicate.ProjectSyncState(sql.FieldNEQ(FieldID, id)) +} + +// IDIn applies the In predicate on the ID field. +func IDIn(ids ...uuid.UUID) predicate.ProjectSyncState { + return predicate.ProjectSyncState(sql.FieldIn(FieldID, ids...)) +} + +// IDNotIn applies the NotIn predicate on the ID field. +func IDNotIn(ids ...uuid.UUID) predicate.ProjectSyncState { + return predicate.ProjectSyncState(sql.FieldNotIn(FieldID, ids...)) +} + +// IDGT applies the GT predicate on the ID field. +func IDGT(id uuid.UUID) predicate.ProjectSyncState { + return predicate.ProjectSyncState(sql.FieldGT(FieldID, id)) +} + +// IDGTE applies the GTE predicate on the ID field. +func IDGTE(id uuid.UUID) predicate.ProjectSyncState { + return predicate.ProjectSyncState(sql.FieldGTE(FieldID, id)) +} + +// IDLT applies the LT predicate on the ID field. +func IDLT(id uuid.UUID) predicate.ProjectSyncState { + return predicate.ProjectSyncState(sql.FieldLT(FieldID, id)) +} + +// IDLTE applies the LTE predicate on the ID field. +func IDLTE(id uuid.UUID) predicate.ProjectSyncState { + return predicate.ProjectSyncState(sql.FieldLTE(FieldID, id)) +} + +// ProjectID applies equality check predicate on the "project_id" field. It's identical to ProjectIDEQ. +func ProjectID(v uuid.UUID) predicate.ProjectSyncState { + return predicate.ProjectSyncState(sql.FieldEQ(FieldProjectID, v)) +} + +// BrokerID applies equality check predicate on the "broker_id" field. It's identical to BrokerIDEQ. +func BrokerID(v string) predicate.ProjectSyncState { + return predicate.ProjectSyncState(sql.FieldEQ(FieldBrokerID, v)) +} + +// LastSyncTime applies equality check predicate on the "last_sync_time" field. It's identical to LastSyncTimeEQ. +func LastSyncTime(v time.Time) predicate.ProjectSyncState { + return predicate.ProjectSyncState(sql.FieldEQ(FieldLastSyncTime, v)) +} + +// LastCommitSha applies equality check predicate on the "last_commit_sha" field. It's identical to LastCommitShaEQ. +func LastCommitSha(v string) predicate.ProjectSyncState { + return predicate.ProjectSyncState(sql.FieldEQ(FieldLastCommitSha, v)) +} + +// FileCount applies equality check predicate on the "file_count" field. It's identical to FileCountEQ. +func FileCount(v int) predicate.ProjectSyncState { + return predicate.ProjectSyncState(sql.FieldEQ(FieldFileCount, v)) +} + +// TotalBytes applies equality check predicate on the "total_bytes" field. It's identical to TotalBytesEQ. +func TotalBytes(v int64) predicate.ProjectSyncState { + return predicate.ProjectSyncState(sql.FieldEQ(FieldTotalBytes, v)) +} + +// ProjectIDEQ applies the EQ predicate on the "project_id" field. +func ProjectIDEQ(v uuid.UUID) predicate.ProjectSyncState { + return predicate.ProjectSyncState(sql.FieldEQ(FieldProjectID, v)) +} + +// ProjectIDNEQ applies the NEQ predicate on the "project_id" field. +func ProjectIDNEQ(v uuid.UUID) predicate.ProjectSyncState { + return predicate.ProjectSyncState(sql.FieldNEQ(FieldProjectID, v)) +} + +// ProjectIDIn applies the In predicate on the "project_id" field. +func ProjectIDIn(vs ...uuid.UUID) predicate.ProjectSyncState { + return predicate.ProjectSyncState(sql.FieldIn(FieldProjectID, vs...)) +} + +// ProjectIDNotIn applies the NotIn predicate on the "project_id" field. +func ProjectIDNotIn(vs ...uuid.UUID) predicate.ProjectSyncState { + return predicate.ProjectSyncState(sql.FieldNotIn(FieldProjectID, vs...)) +} + +// ProjectIDGT applies the GT predicate on the "project_id" field. +func ProjectIDGT(v uuid.UUID) predicate.ProjectSyncState { + return predicate.ProjectSyncState(sql.FieldGT(FieldProjectID, v)) +} + +// ProjectIDGTE applies the GTE predicate on the "project_id" field. +func ProjectIDGTE(v uuid.UUID) predicate.ProjectSyncState { + return predicate.ProjectSyncState(sql.FieldGTE(FieldProjectID, v)) +} + +// ProjectIDLT applies the LT predicate on the "project_id" field. +func ProjectIDLT(v uuid.UUID) predicate.ProjectSyncState { + return predicate.ProjectSyncState(sql.FieldLT(FieldProjectID, v)) +} + +// ProjectIDLTE applies the LTE predicate on the "project_id" field. +func ProjectIDLTE(v uuid.UUID) predicate.ProjectSyncState { + return predicate.ProjectSyncState(sql.FieldLTE(FieldProjectID, v)) +} + +// BrokerIDEQ applies the EQ predicate on the "broker_id" field. +func BrokerIDEQ(v string) predicate.ProjectSyncState { + return predicate.ProjectSyncState(sql.FieldEQ(FieldBrokerID, v)) +} + +// BrokerIDNEQ applies the NEQ predicate on the "broker_id" field. +func BrokerIDNEQ(v string) predicate.ProjectSyncState { + return predicate.ProjectSyncState(sql.FieldNEQ(FieldBrokerID, v)) +} + +// BrokerIDIn applies the In predicate on the "broker_id" field. +func BrokerIDIn(vs ...string) predicate.ProjectSyncState { + return predicate.ProjectSyncState(sql.FieldIn(FieldBrokerID, vs...)) +} + +// BrokerIDNotIn applies the NotIn predicate on the "broker_id" field. +func BrokerIDNotIn(vs ...string) predicate.ProjectSyncState { + return predicate.ProjectSyncState(sql.FieldNotIn(FieldBrokerID, vs...)) +} + +// BrokerIDGT applies the GT predicate on the "broker_id" field. +func BrokerIDGT(v string) predicate.ProjectSyncState { + return predicate.ProjectSyncState(sql.FieldGT(FieldBrokerID, v)) +} + +// BrokerIDGTE applies the GTE predicate on the "broker_id" field. +func BrokerIDGTE(v string) predicate.ProjectSyncState { + return predicate.ProjectSyncState(sql.FieldGTE(FieldBrokerID, v)) +} + +// BrokerIDLT applies the LT predicate on the "broker_id" field. +func BrokerIDLT(v string) predicate.ProjectSyncState { + return predicate.ProjectSyncState(sql.FieldLT(FieldBrokerID, v)) +} + +// BrokerIDLTE applies the LTE predicate on the "broker_id" field. +func BrokerIDLTE(v string) predicate.ProjectSyncState { + return predicate.ProjectSyncState(sql.FieldLTE(FieldBrokerID, v)) +} + +// BrokerIDContains applies the Contains predicate on the "broker_id" field. +func BrokerIDContains(v string) predicate.ProjectSyncState { + return predicate.ProjectSyncState(sql.FieldContains(FieldBrokerID, v)) +} + +// BrokerIDHasPrefix applies the HasPrefix predicate on the "broker_id" field. +func BrokerIDHasPrefix(v string) predicate.ProjectSyncState { + return predicate.ProjectSyncState(sql.FieldHasPrefix(FieldBrokerID, v)) +} + +// BrokerIDHasSuffix applies the HasSuffix predicate on the "broker_id" field. +func BrokerIDHasSuffix(v string) predicate.ProjectSyncState { + return predicate.ProjectSyncState(sql.FieldHasSuffix(FieldBrokerID, v)) +} + +// BrokerIDEqualFold applies the EqualFold predicate on the "broker_id" field. +func BrokerIDEqualFold(v string) predicate.ProjectSyncState { + return predicate.ProjectSyncState(sql.FieldEqualFold(FieldBrokerID, v)) +} + +// BrokerIDContainsFold applies the ContainsFold predicate on the "broker_id" field. +func BrokerIDContainsFold(v string) predicate.ProjectSyncState { + return predicate.ProjectSyncState(sql.FieldContainsFold(FieldBrokerID, v)) +} + +// LastSyncTimeEQ applies the EQ predicate on the "last_sync_time" field. +func LastSyncTimeEQ(v time.Time) predicate.ProjectSyncState { + return predicate.ProjectSyncState(sql.FieldEQ(FieldLastSyncTime, v)) +} + +// LastSyncTimeNEQ applies the NEQ predicate on the "last_sync_time" field. +func LastSyncTimeNEQ(v time.Time) predicate.ProjectSyncState { + return predicate.ProjectSyncState(sql.FieldNEQ(FieldLastSyncTime, v)) +} + +// LastSyncTimeIn applies the In predicate on the "last_sync_time" field. +func LastSyncTimeIn(vs ...time.Time) predicate.ProjectSyncState { + return predicate.ProjectSyncState(sql.FieldIn(FieldLastSyncTime, vs...)) +} + +// LastSyncTimeNotIn applies the NotIn predicate on the "last_sync_time" field. +func LastSyncTimeNotIn(vs ...time.Time) predicate.ProjectSyncState { + return predicate.ProjectSyncState(sql.FieldNotIn(FieldLastSyncTime, vs...)) +} + +// LastSyncTimeGT applies the GT predicate on the "last_sync_time" field. +func LastSyncTimeGT(v time.Time) predicate.ProjectSyncState { + return predicate.ProjectSyncState(sql.FieldGT(FieldLastSyncTime, v)) +} + +// LastSyncTimeGTE applies the GTE predicate on the "last_sync_time" field. +func LastSyncTimeGTE(v time.Time) predicate.ProjectSyncState { + return predicate.ProjectSyncState(sql.FieldGTE(FieldLastSyncTime, v)) +} + +// LastSyncTimeLT applies the LT predicate on the "last_sync_time" field. +func LastSyncTimeLT(v time.Time) predicate.ProjectSyncState { + return predicate.ProjectSyncState(sql.FieldLT(FieldLastSyncTime, v)) +} + +// LastSyncTimeLTE applies the LTE predicate on the "last_sync_time" field. +func LastSyncTimeLTE(v time.Time) predicate.ProjectSyncState { + return predicate.ProjectSyncState(sql.FieldLTE(FieldLastSyncTime, v)) +} + +// LastSyncTimeIsNil applies the IsNil predicate on the "last_sync_time" field. +func LastSyncTimeIsNil() predicate.ProjectSyncState { + return predicate.ProjectSyncState(sql.FieldIsNull(FieldLastSyncTime)) +} + +// LastSyncTimeNotNil applies the NotNil predicate on the "last_sync_time" field. +func LastSyncTimeNotNil() predicate.ProjectSyncState { + return predicate.ProjectSyncState(sql.FieldNotNull(FieldLastSyncTime)) +} + +// LastCommitShaEQ applies the EQ predicate on the "last_commit_sha" field. +func LastCommitShaEQ(v string) predicate.ProjectSyncState { + return predicate.ProjectSyncState(sql.FieldEQ(FieldLastCommitSha, v)) +} + +// LastCommitShaNEQ applies the NEQ predicate on the "last_commit_sha" field. +func LastCommitShaNEQ(v string) predicate.ProjectSyncState { + return predicate.ProjectSyncState(sql.FieldNEQ(FieldLastCommitSha, v)) +} + +// LastCommitShaIn applies the In predicate on the "last_commit_sha" field. +func LastCommitShaIn(vs ...string) predicate.ProjectSyncState { + return predicate.ProjectSyncState(sql.FieldIn(FieldLastCommitSha, vs...)) +} + +// LastCommitShaNotIn applies the NotIn predicate on the "last_commit_sha" field. +func LastCommitShaNotIn(vs ...string) predicate.ProjectSyncState { + return predicate.ProjectSyncState(sql.FieldNotIn(FieldLastCommitSha, vs...)) +} + +// LastCommitShaGT applies the GT predicate on the "last_commit_sha" field. +func LastCommitShaGT(v string) predicate.ProjectSyncState { + return predicate.ProjectSyncState(sql.FieldGT(FieldLastCommitSha, v)) +} + +// LastCommitShaGTE applies the GTE predicate on the "last_commit_sha" field. +func LastCommitShaGTE(v string) predicate.ProjectSyncState { + return predicate.ProjectSyncState(sql.FieldGTE(FieldLastCommitSha, v)) +} + +// LastCommitShaLT applies the LT predicate on the "last_commit_sha" field. +func LastCommitShaLT(v string) predicate.ProjectSyncState { + return predicate.ProjectSyncState(sql.FieldLT(FieldLastCommitSha, v)) +} + +// LastCommitShaLTE applies the LTE predicate on the "last_commit_sha" field. +func LastCommitShaLTE(v string) predicate.ProjectSyncState { + return predicate.ProjectSyncState(sql.FieldLTE(FieldLastCommitSha, v)) +} + +// LastCommitShaContains applies the Contains predicate on the "last_commit_sha" field. +func LastCommitShaContains(v string) predicate.ProjectSyncState { + return predicate.ProjectSyncState(sql.FieldContains(FieldLastCommitSha, v)) +} + +// LastCommitShaHasPrefix applies the HasPrefix predicate on the "last_commit_sha" field. +func LastCommitShaHasPrefix(v string) predicate.ProjectSyncState { + return predicate.ProjectSyncState(sql.FieldHasPrefix(FieldLastCommitSha, v)) +} + +// LastCommitShaHasSuffix applies the HasSuffix predicate on the "last_commit_sha" field. +func LastCommitShaHasSuffix(v string) predicate.ProjectSyncState { + return predicate.ProjectSyncState(sql.FieldHasSuffix(FieldLastCommitSha, v)) +} + +// LastCommitShaIsNil applies the IsNil predicate on the "last_commit_sha" field. +func LastCommitShaIsNil() predicate.ProjectSyncState { + return predicate.ProjectSyncState(sql.FieldIsNull(FieldLastCommitSha)) +} + +// LastCommitShaNotNil applies the NotNil predicate on the "last_commit_sha" field. +func LastCommitShaNotNil() predicate.ProjectSyncState { + return predicate.ProjectSyncState(sql.FieldNotNull(FieldLastCommitSha)) +} + +// LastCommitShaEqualFold applies the EqualFold predicate on the "last_commit_sha" field. +func LastCommitShaEqualFold(v string) predicate.ProjectSyncState { + return predicate.ProjectSyncState(sql.FieldEqualFold(FieldLastCommitSha, v)) +} + +// LastCommitShaContainsFold applies the ContainsFold predicate on the "last_commit_sha" field. +func LastCommitShaContainsFold(v string) predicate.ProjectSyncState { + return predicate.ProjectSyncState(sql.FieldContainsFold(FieldLastCommitSha, v)) +} + +// FileCountEQ applies the EQ predicate on the "file_count" field. +func FileCountEQ(v int) predicate.ProjectSyncState { + return predicate.ProjectSyncState(sql.FieldEQ(FieldFileCount, v)) +} + +// FileCountNEQ applies the NEQ predicate on the "file_count" field. +func FileCountNEQ(v int) predicate.ProjectSyncState { + return predicate.ProjectSyncState(sql.FieldNEQ(FieldFileCount, v)) +} + +// FileCountIn applies the In predicate on the "file_count" field. +func FileCountIn(vs ...int) predicate.ProjectSyncState { + return predicate.ProjectSyncState(sql.FieldIn(FieldFileCount, vs...)) +} + +// FileCountNotIn applies the NotIn predicate on the "file_count" field. +func FileCountNotIn(vs ...int) predicate.ProjectSyncState { + return predicate.ProjectSyncState(sql.FieldNotIn(FieldFileCount, vs...)) +} + +// FileCountGT applies the GT predicate on the "file_count" field. +func FileCountGT(v int) predicate.ProjectSyncState { + return predicate.ProjectSyncState(sql.FieldGT(FieldFileCount, v)) +} + +// FileCountGTE applies the GTE predicate on the "file_count" field. +func FileCountGTE(v int) predicate.ProjectSyncState { + return predicate.ProjectSyncState(sql.FieldGTE(FieldFileCount, v)) +} + +// FileCountLT applies the LT predicate on the "file_count" field. +func FileCountLT(v int) predicate.ProjectSyncState { + return predicate.ProjectSyncState(sql.FieldLT(FieldFileCount, v)) +} + +// FileCountLTE applies the LTE predicate on the "file_count" field. +func FileCountLTE(v int) predicate.ProjectSyncState { + return predicate.ProjectSyncState(sql.FieldLTE(FieldFileCount, v)) +} + +// TotalBytesEQ applies the EQ predicate on the "total_bytes" field. +func TotalBytesEQ(v int64) predicate.ProjectSyncState { + return predicate.ProjectSyncState(sql.FieldEQ(FieldTotalBytes, v)) +} + +// TotalBytesNEQ applies the NEQ predicate on the "total_bytes" field. +func TotalBytesNEQ(v int64) predicate.ProjectSyncState { + return predicate.ProjectSyncState(sql.FieldNEQ(FieldTotalBytes, v)) +} + +// TotalBytesIn applies the In predicate on the "total_bytes" field. +func TotalBytesIn(vs ...int64) predicate.ProjectSyncState { + return predicate.ProjectSyncState(sql.FieldIn(FieldTotalBytes, vs...)) +} + +// TotalBytesNotIn applies the NotIn predicate on the "total_bytes" field. +func TotalBytesNotIn(vs ...int64) predicate.ProjectSyncState { + return predicate.ProjectSyncState(sql.FieldNotIn(FieldTotalBytes, vs...)) +} + +// TotalBytesGT applies the GT predicate on the "total_bytes" field. +func TotalBytesGT(v int64) predicate.ProjectSyncState { + return predicate.ProjectSyncState(sql.FieldGT(FieldTotalBytes, v)) +} + +// TotalBytesGTE applies the GTE predicate on the "total_bytes" field. +func TotalBytesGTE(v int64) predicate.ProjectSyncState { + return predicate.ProjectSyncState(sql.FieldGTE(FieldTotalBytes, v)) +} + +// TotalBytesLT applies the LT predicate on the "total_bytes" field. +func TotalBytesLT(v int64) predicate.ProjectSyncState { + return predicate.ProjectSyncState(sql.FieldLT(FieldTotalBytes, v)) +} + +// TotalBytesLTE applies the LTE predicate on the "total_bytes" field. +func TotalBytesLTE(v int64) predicate.ProjectSyncState { + return predicate.ProjectSyncState(sql.FieldLTE(FieldTotalBytes, v)) +} + +// And groups predicates with the AND operator between them. +func And(predicates ...predicate.ProjectSyncState) predicate.ProjectSyncState { + return predicate.ProjectSyncState(sql.AndPredicates(predicates...)) +} + +// Or groups predicates with the OR operator between them. +func Or(predicates ...predicate.ProjectSyncState) predicate.ProjectSyncState { + return predicate.ProjectSyncState(sql.OrPredicates(predicates...)) +} + +// Not applies the not operator on the given predicate. +func Not(p predicate.ProjectSyncState) predicate.ProjectSyncState { + return predicate.ProjectSyncState(sql.NotPredicates(p)) +} diff --git a/pkg/ent/projectsyncstate_create.go b/pkg/ent/projectsyncstate_create.go new file mode 100644 index 000000000..13e37464f --- /dev/null +++ b/pkg/ent/projectsyncstate_create.go @@ -0,0 +1,900 @@ +// Code generated by ent, DO NOT EDIT. + +package ent + +import ( + "context" + "errors" + "fmt" + "time" + + "entgo.io/ent/dialect" + "entgo.io/ent/dialect/sql" + "entgo.io/ent/dialect/sql/sqlgraph" + "entgo.io/ent/schema/field" + "github.com/GoogleCloudPlatform/scion/pkg/ent/projectsyncstate" + "github.com/google/uuid" +) + +// ProjectSyncStateCreate is the builder for creating a ProjectSyncState entity. +type ProjectSyncStateCreate struct { + config + mutation *ProjectSyncStateMutation + hooks []Hook + conflict []sql.ConflictOption +} + +// SetProjectID sets the "project_id" field. +func (_c *ProjectSyncStateCreate) SetProjectID(v uuid.UUID) *ProjectSyncStateCreate { + _c.mutation.SetProjectID(v) + return _c +} + +// SetBrokerID sets the "broker_id" field. +func (_c *ProjectSyncStateCreate) SetBrokerID(v string) *ProjectSyncStateCreate { + _c.mutation.SetBrokerID(v) + return _c +} + +// SetNillableBrokerID sets the "broker_id" field if the given value is not nil. +func (_c *ProjectSyncStateCreate) SetNillableBrokerID(v *string) *ProjectSyncStateCreate { + if v != nil { + _c.SetBrokerID(*v) + } + return _c +} + +// SetLastSyncTime sets the "last_sync_time" field. +func (_c *ProjectSyncStateCreate) SetLastSyncTime(v time.Time) *ProjectSyncStateCreate { + _c.mutation.SetLastSyncTime(v) + return _c +} + +// SetNillableLastSyncTime sets the "last_sync_time" field if the given value is not nil. +func (_c *ProjectSyncStateCreate) SetNillableLastSyncTime(v *time.Time) *ProjectSyncStateCreate { + if v != nil { + _c.SetLastSyncTime(*v) + } + return _c +} + +// SetLastCommitSha sets the "last_commit_sha" field. +func (_c *ProjectSyncStateCreate) SetLastCommitSha(v string) *ProjectSyncStateCreate { + _c.mutation.SetLastCommitSha(v) + return _c +} + +// SetNillableLastCommitSha sets the "last_commit_sha" field if the given value is not nil. +func (_c *ProjectSyncStateCreate) SetNillableLastCommitSha(v *string) *ProjectSyncStateCreate { + if v != nil { + _c.SetLastCommitSha(*v) + } + return _c +} + +// SetFileCount sets the "file_count" field. +func (_c *ProjectSyncStateCreate) SetFileCount(v int) *ProjectSyncStateCreate { + _c.mutation.SetFileCount(v) + return _c +} + +// SetNillableFileCount sets the "file_count" field if the given value is not nil. +func (_c *ProjectSyncStateCreate) SetNillableFileCount(v *int) *ProjectSyncStateCreate { + if v != nil { + _c.SetFileCount(*v) + } + return _c +} + +// SetTotalBytes sets the "total_bytes" field. +func (_c *ProjectSyncStateCreate) SetTotalBytes(v int64) *ProjectSyncStateCreate { + _c.mutation.SetTotalBytes(v) + return _c +} + +// SetNillableTotalBytes sets the "total_bytes" field if the given value is not nil. +func (_c *ProjectSyncStateCreate) SetNillableTotalBytes(v *int64) *ProjectSyncStateCreate { + if v != nil { + _c.SetTotalBytes(*v) + } + return _c +} + +// SetID sets the "id" field. +func (_c *ProjectSyncStateCreate) SetID(v uuid.UUID) *ProjectSyncStateCreate { + _c.mutation.SetID(v) + return _c +} + +// SetNillableID sets the "id" field if the given value is not nil. +func (_c *ProjectSyncStateCreate) SetNillableID(v *uuid.UUID) *ProjectSyncStateCreate { + if v != nil { + _c.SetID(*v) + } + return _c +} + +// Mutation returns the ProjectSyncStateMutation object of the builder. +func (_c *ProjectSyncStateCreate) Mutation() *ProjectSyncStateMutation { + return _c.mutation +} + +// Save creates the ProjectSyncState in the database. +func (_c *ProjectSyncStateCreate) Save(ctx context.Context) (*ProjectSyncState, error) { + _c.defaults() + return withHooks(ctx, _c.sqlSave, _c.mutation, _c.hooks) +} + +// SaveX calls Save and panics if Save returns an error. +func (_c *ProjectSyncStateCreate) SaveX(ctx context.Context) *ProjectSyncState { + v, err := _c.Save(ctx) + if err != nil { + panic(err) + } + return v +} + +// Exec executes the query. +func (_c *ProjectSyncStateCreate) Exec(ctx context.Context) error { + _, err := _c.Save(ctx) + return err +} + +// ExecX is like Exec, but panics if an error occurs. +func (_c *ProjectSyncStateCreate) ExecX(ctx context.Context) { + if err := _c.Exec(ctx); err != nil { + panic(err) + } +} + +// defaults sets the default values of the builder before save. +func (_c *ProjectSyncStateCreate) defaults() { + if _, ok := _c.mutation.BrokerID(); !ok { + v := projectsyncstate.DefaultBrokerID + _c.mutation.SetBrokerID(v) + } + if _, ok := _c.mutation.FileCount(); !ok { + v := projectsyncstate.DefaultFileCount + _c.mutation.SetFileCount(v) + } + if _, ok := _c.mutation.TotalBytes(); !ok { + v := projectsyncstate.DefaultTotalBytes + _c.mutation.SetTotalBytes(v) + } + if _, ok := _c.mutation.ID(); !ok { + v := projectsyncstate.DefaultID() + _c.mutation.SetID(v) + } +} + +// check runs all checks and user-defined validators on the builder. +func (_c *ProjectSyncStateCreate) check() error { + if _, ok := _c.mutation.ProjectID(); !ok { + return &ValidationError{Name: "project_id", err: errors.New(`ent: missing required field "ProjectSyncState.project_id"`)} + } + if _, ok := _c.mutation.BrokerID(); !ok { + return &ValidationError{Name: "broker_id", err: errors.New(`ent: missing required field "ProjectSyncState.broker_id"`)} + } + if _, ok := _c.mutation.FileCount(); !ok { + return &ValidationError{Name: "file_count", err: errors.New(`ent: missing required field "ProjectSyncState.file_count"`)} + } + if _, ok := _c.mutation.TotalBytes(); !ok { + return &ValidationError{Name: "total_bytes", err: errors.New(`ent: missing required field "ProjectSyncState.total_bytes"`)} + } + return nil +} + +func (_c *ProjectSyncStateCreate) sqlSave(ctx context.Context) (*ProjectSyncState, error) { + if err := _c.check(); err != nil { + return nil, err + } + _node, _spec := _c.createSpec() + if err := sqlgraph.CreateNode(ctx, _c.driver, _spec); err != nil { + if sqlgraph.IsConstraintError(err) { + err = &ConstraintError{msg: err.Error(), wrap: err} + } + return nil, err + } + if _spec.ID.Value != nil { + if id, ok := _spec.ID.Value.(*uuid.UUID); ok { + _node.ID = *id + } else if err := _node.ID.Scan(_spec.ID.Value); err != nil { + return nil, err + } + } + _c.mutation.id = &_node.ID + _c.mutation.done = true + return _node, nil +} + +func (_c *ProjectSyncStateCreate) createSpec() (*ProjectSyncState, *sqlgraph.CreateSpec) { + var ( + _node = &ProjectSyncState{config: _c.config} + _spec = sqlgraph.NewCreateSpec(projectsyncstate.Table, sqlgraph.NewFieldSpec(projectsyncstate.FieldID, field.TypeUUID)) + ) + _spec.OnConflict = _c.conflict + if id, ok := _c.mutation.ID(); ok { + _node.ID = id + _spec.ID.Value = &id + } + if value, ok := _c.mutation.ProjectID(); ok { + _spec.SetField(projectsyncstate.FieldProjectID, field.TypeUUID, value) + _node.ProjectID = value + } + if value, ok := _c.mutation.BrokerID(); ok { + _spec.SetField(projectsyncstate.FieldBrokerID, field.TypeString, value) + _node.BrokerID = value + } + if value, ok := _c.mutation.LastSyncTime(); ok { + _spec.SetField(projectsyncstate.FieldLastSyncTime, field.TypeTime, value) + _node.LastSyncTime = &value + } + if value, ok := _c.mutation.LastCommitSha(); ok { + _spec.SetField(projectsyncstate.FieldLastCommitSha, field.TypeString, value) + _node.LastCommitSha = value + } + if value, ok := _c.mutation.FileCount(); ok { + _spec.SetField(projectsyncstate.FieldFileCount, field.TypeInt, value) + _node.FileCount = value + } + if value, ok := _c.mutation.TotalBytes(); ok { + _spec.SetField(projectsyncstate.FieldTotalBytes, field.TypeInt64, value) + _node.TotalBytes = value + } + return _node, _spec +} + +// OnConflict allows configuring the `ON CONFLICT` / `ON DUPLICATE KEY` clause +// of the `INSERT` statement. For example: +// +// client.ProjectSyncState.Create(). +// SetProjectID(v). +// OnConflict( +// // Update the row with the new values +// // the was proposed for insertion. +// sql.ResolveWithNewValues(), +// ). +// // Override some of the fields with custom +// // update values. +// Update(func(u *ent.ProjectSyncStateUpsert) { +// SetProjectID(v+v). +// }). +// Exec(ctx) +func (_c *ProjectSyncStateCreate) OnConflict(opts ...sql.ConflictOption) *ProjectSyncStateUpsertOne { + _c.conflict = opts + return &ProjectSyncStateUpsertOne{ + create: _c, + } +} + +// OnConflictColumns calls `OnConflict` and configures the columns +// as conflict target. Using this option is equivalent to using: +// +// client.ProjectSyncState.Create(). +// OnConflict(sql.ConflictColumns(columns...)). +// Exec(ctx) +func (_c *ProjectSyncStateCreate) OnConflictColumns(columns ...string) *ProjectSyncStateUpsertOne { + _c.conflict = append(_c.conflict, sql.ConflictColumns(columns...)) + return &ProjectSyncStateUpsertOne{ + create: _c, + } +} + +type ( + // ProjectSyncStateUpsertOne is the builder for "upsert"-ing + // one ProjectSyncState node. + ProjectSyncStateUpsertOne struct { + create *ProjectSyncStateCreate + } + + // ProjectSyncStateUpsert is the "OnConflict" setter. + ProjectSyncStateUpsert struct { + *sql.UpdateSet + } +) + +// SetProjectID sets the "project_id" field. +func (u *ProjectSyncStateUpsert) SetProjectID(v uuid.UUID) *ProjectSyncStateUpsert { + u.Set(projectsyncstate.FieldProjectID, v) + return u +} + +// UpdateProjectID sets the "project_id" field to the value that was provided on create. +func (u *ProjectSyncStateUpsert) UpdateProjectID() *ProjectSyncStateUpsert { + u.SetExcluded(projectsyncstate.FieldProjectID) + return u +} + +// SetBrokerID sets the "broker_id" field. +func (u *ProjectSyncStateUpsert) SetBrokerID(v string) *ProjectSyncStateUpsert { + u.Set(projectsyncstate.FieldBrokerID, v) + return u +} + +// UpdateBrokerID sets the "broker_id" field to the value that was provided on create. +func (u *ProjectSyncStateUpsert) UpdateBrokerID() *ProjectSyncStateUpsert { + u.SetExcluded(projectsyncstate.FieldBrokerID) + return u +} + +// SetLastSyncTime sets the "last_sync_time" field. +func (u *ProjectSyncStateUpsert) SetLastSyncTime(v time.Time) *ProjectSyncStateUpsert { + u.Set(projectsyncstate.FieldLastSyncTime, v) + return u +} + +// UpdateLastSyncTime sets the "last_sync_time" field to the value that was provided on create. +func (u *ProjectSyncStateUpsert) UpdateLastSyncTime() *ProjectSyncStateUpsert { + u.SetExcluded(projectsyncstate.FieldLastSyncTime) + return u +} + +// ClearLastSyncTime clears the value of the "last_sync_time" field. +func (u *ProjectSyncStateUpsert) ClearLastSyncTime() *ProjectSyncStateUpsert { + u.SetNull(projectsyncstate.FieldLastSyncTime) + return u +} + +// SetLastCommitSha sets the "last_commit_sha" field. +func (u *ProjectSyncStateUpsert) SetLastCommitSha(v string) *ProjectSyncStateUpsert { + u.Set(projectsyncstate.FieldLastCommitSha, v) + return u +} + +// UpdateLastCommitSha sets the "last_commit_sha" field to the value that was provided on create. +func (u *ProjectSyncStateUpsert) UpdateLastCommitSha() *ProjectSyncStateUpsert { + u.SetExcluded(projectsyncstate.FieldLastCommitSha) + return u +} + +// ClearLastCommitSha clears the value of the "last_commit_sha" field. +func (u *ProjectSyncStateUpsert) ClearLastCommitSha() *ProjectSyncStateUpsert { + u.SetNull(projectsyncstate.FieldLastCommitSha) + return u +} + +// SetFileCount sets the "file_count" field. +func (u *ProjectSyncStateUpsert) SetFileCount(v int) *ProjectSyncStateUpsert { + u.Set(projectsyncstate.FieldFileCount, v) + return u +} + +// UpdateFileCount sets the "file_count" field to the value that was provided on create. +func (u *ProjectSyncStateUpsert) UpdateFileCount() *ProjectSyncStateUpsert { + u.SetExcluded(projectsyncstate.FieldFileCount) + return u +} + +// AddFileCount adds v to the "file_count" field. +func (u *ProjectSyncStateUpsert) AddFileCount(v int) *ProjectSyncStateUpsert { + u.Add(projectsyncstate.FieldFileCount, v) + return u +} + +// SetTotalBytes sets the "total_bytes" field. +func (u *ProjectSyncStateUpsert) SetTotalBytes(v int64) *ProjectSyncStateUpsert { + u.Set(projectsyncstate.FieldTotalBytes, v) + return u +} + +// UpdateTotalBytes sets the "total_bytes" field to the value that was provided on create. +func (u *ProjectSyncStateUpsert) UpdateTotalBytes() *ProjectSyncStateUpsert { + u.SetExcluded(projectsyncstate.FieldTotalBytes) + return u +} + +// AddTotalBytes adds v to the "total_bytes" field. +func (u *ProjectSyncStateUpsert) AddTotalBytes(v int64) *ProjectSyncStateUpsert { + u.Add(projectsyncstate.FieldTotalBytes, v) + return u +} + +// UpdateNewValues updates the mutable fields using the new values that were set on create except the ID field. +// Using this option is equivalent to using: +// +// client.ProjectSyncState.Create(). +// OnConflict( +// sql.ResolveWithNewValues(), +// sql.ResolveWith(func(u *sql.UpdateSet) { +// u.SetIgnore(projectsyncstate.FieldID) +// }), +// ). +// Exec(ctx) +func (u *ProjectSyncStateUpsertOne) UpdateNewValues() *ProjectSyncStateUpsertOne { + u.create.conflict = append(u.create.conflict, sql.ResolveWithNewValues()) + u.create.conflict = append(u.create.conflict, sql.ResolveWith(func(s *sql.UpdateSet) { + if _, exists := u.create.mutation.ID(); exists { + s.SetIgnore(projectsyncstate.FieldID) + } + })) + return u +} + +// Ignore sets each column to itself in case of conflict. +// Using this option is equivalent to using: +// +// client.ProjectSyncState.Create(). +// OnConflict(sql.ResolveWithIgnore()). +// Exec(ctx) +func (u *ProjectSyncStateUpsertOne) Ignore() *ProjectSyncStateUpsertOne { + u.create.conflict = append(u.create.conflict, sql.ResolveWithIgnore()) + return u +} + +// DoNothing configures the conflict_action to `DO NOTHING`. +// Supported only by SQLite and PostgreSQL. +func (u *ProjectSyncStateUpsertOne) DoNothing() *ProjectSyncStateUpsertOne { + u.create.conflict = append(u.create.conflict, sql.DoNothing()) + return u +} + +// Update allows overriding fields `UPDATE` values. See the ProjectSyncStateCreate.OnConflict +// documentation for more info. +func (u *ProjectSyncStateUpsertOne) Update(set func(*ProjectSyncStateUpsert)) *ProjectSyncStateUpsertOne { + u.create.conflict = append(u.create.conflict, sql.ResolveWith(func(update *sql.UpdateSet) { + set(&ProjectSyncStateUpsert{UpdateSet: update}) + })) + return u +} + +// SetProjectID sets the "project_id" field. +func (u *ProjectSyncStateUpsertOne) SetProjectID(v uuid.UUID) *ProjectSyncStateUpsertOne { + return u.Update(func(s *ProjectSyncStateUpsert) { + s.SetProjectID(v) + }) +} + +// UpdateProjectID sets the "project_id" field to the value that was provided on create. +func (u *ProjectSyncStateUpsertOne) UpdateProjectID() *ProjectSyncStateUpsertOne { + return u.Update(func(s *ProjectSyncStateUpsert) { + s.UpdateProjectID() + }) +} + +// SetBrokerID sets the "broker_id" field. +func (u *ProjectSyncStateUpsertOne) SetBrokerID(v string) *ProjectSyncStateUpsertOne { + return u.Update(func(s *ProjectSyncStateUpsert) { + s.SetBrokerID(v) + }) +} + +// UpdateBrokerID sets the "broker_id" field to the value that was provided on create. +func (u *ProjectSyncStateUpsertOne) UpdateBrokerID() *ProjectSyncStateUpsertOne { + return u.Update(func(s *ProjectSyncStateUpsert) { + s.UpdateBrokerID() + }) +} + +// SetLastSyncTime sets the "last_sync_time" field. +func (u *ProjectSyncStateUpsertOne) SetLastSyncTime(v time.Time) *ProjectSyncStateUpsertOne { + return u.Update(func(s *ProjectSyncStateUpsert) { + s.SetLastSyncTime(v) + }) +} + +// UpdateLastSyncTime sets the "last_sync_time" field to the value that was provided on create. +func (u *ProjectSyncStateUpsertOne) UpdateLastSyncTime() *ProjectSyncStateUpsertOne { + return u.Update(func(s *ProjectSyncStateUpsert) { + s.UpdateLastSyncTime() + }) +} + +// ClearLastSyncTime clears the value of the "last_sync_time" field. +func (u *ProjectSyncStateUpsertOne) ClearLastSyncTime() *ProjectSyncStateUpsertOne { + return u.Update(func(s *ProjectSyncStateUpsert) { + s.ClearLastSyncTime() + }) +} + +// SetLastCommitSha sets the "last_commit_sha" field. +func (u *ProjectSyncStateUpsertOne) SetLastCommitSha(v string) *ProjectSyncStateUpsertOne { + return u.Update(func(s *ProjectSyncStateUpsert) { + s.SetLastCommitSha(v) + }) +} + +// UpdateLastCommitSha sets the "last_commit_sha" field to the value that was provided on create. +func (u *ProjectSyncStateUpsertOne) UpdateLastCommitSha() *ProjectSyncStateUpsertOne { + return u.Update(func(s *ProjectSyncStateUpsert) { + s.UpdateLastCommitSha() + }) +} + +// ClearLastCommitSha clears the value of the "last_commit_sha" field. +func (u *ProjectSyncStateUpsertOne) ClearLastCommitSha() *ProjectSyncStateUpsertOne { + return u.Update(func(s *ProjectSyncStateUpsert) { + s.ClearLastCommitSha() + }) +} + +// SetFileCount sets the "file_count" field. +func (u *ProjectSyncStateUpsertOne) SetFileCount(v int) *ProjectSyncStateUpsertOne { + return u.Update(func(s *ProjectSyncStateUpsert) { + s.SetFileCount(v) + }) +} + +// AddFileCount adds v to the "file_count" field. +func (u *ProjectSyncStateUpsertOne) AddFileCount(v int) *ProjectSyncStateUpsertOne { + return u.Update(func(s *ProjectSyncStateUpsert) { + s.AddFileCount(v) + }) +} + +// UpdateFileCount sets the "file_count" field to the value that was provided on create. +func (u *ProjectSyncStateUpsertOne) UpdateFileCount() *ProjectSyncStateUpsertOne { + return u.Update(func(s *ProjectSyncStateUpsert) { + s.UpdateFileCount() + }) +} + +// SetTotalBytes sets the "total_bytes" field. +func (u *ProjectSyncStateUpsertOne) SetTotalBytes(v int64) *ProjectSyncStateUpsertOne { + return u.Update(func(s *ProjectSyncStateUpsert) { + s.SetTotalBytes(v) + }) +} + +// AddTotalBytes adds v to the "total_bytes" field. +func (u *ProjectSyncStateUpsertOne) AddTotalBytes(v int64) *ProjectSyncStateUpsertOne { + return u.Update(func(s *ProjectSyncStateUpsert) { + s.AddTotalBytes(v) + }) +} + +// UpdateTotalBytes sets the "total_bytes" field to the value that was provided on create. +func (u *ProjectSyncStateUpsertOne) UpdateTotalBytes() *ProjectSyncStateUpsertOne { + return u.Update(func(s *ProjectSyncStateUpsert) { + s.UpdateTotalBytes() + }) +} + +// Exec executes the query. +func (u *ProjectSyncStateUpsertOne) Exec(ctx context.Context) error { + if len(u.create.conflict) == 0 { + return errors.New("ent: missing options for ProjectSyncStateCreate.OnConflict") + } + return u.create.Exec(ctx) +} + +// ExecX is like Exec, but panics if an error occurs. +func (u *ProjectSyncStateUpsertOne) ExecX(ctx context.Context) { + if err := u.create.Exec(ctx); err != nil { + panic(err) + } +} + +// Exec executes the UPSERT query and returns the inserted/updated ID. +func (u *ProjectSyncStateUpsertOne) ID(ctx context.Context) (id uuid.UUID, err error) { + if u.create.driver.Dialect() == dialect.MySQL { + // In case of "ON CONFLICT", there is no way to get back non-numeric ID + // fields from the database since MySQL does not support the RETURNING clause. + return id, errors.New("ent: ProjectSyncStateUpsertOne.ID is not supported by MySQL driver. Use ProjectSyncStateUpsertOne.Exec instead") + } + node, err := u.create.Save(ctx) + if err != nil { + return id, err + } + return node.ID, nil +} + +// IDX is like ID, but panics if an error occurs. +func (u *ProjectSyncStateUpsertOne) IDX(ctx context.Context) uuid.UUID { + id, err := u.ID(ctx) + if err != nil { + panic(err) + } + return id +} + +// ProjectSyncStateCreateBulk is the builder for creating many ProjectSyncState entities in bulk. +type ProjectSyncStateCreateBulk struct { + config + err error + builders []*ProjectSyncStateCreate + conflict []sql.ConflictOption +} + +// Save creates the ProjectSyncState entities in the database. +func (_c *ProjectSyncStateCreateBulk) Save(ctx context.Context) ([]*ProjectSyncState, error) { + if _c.err != nil { + return nil, _c.err + } + specs := make([]*sqlgraph.CreateSpec, len(_c.builders)) + nodes := make([]*ProjectSyncState, len(_c.builders)) + mutators := make([]Mutator, len(_c.builders)) + for i := range _c.builders { + func(i int, root context.Context) { + builder := _c.builders[i] + builder.defaults() + var mut Mutator = MutateFunc(func(ctx context.Context, m Mutation) (Value, error) { + mutation, ok := m.(*ProjectSyncStateMutation) + if !ok { + return nil, fmt.Errorf("unexpected mutation type %T", m) + } + if err := builder.check(); err != nil { + return nil, err + } + builder.mutation = mutation + var err error + nodes[i], specs[i] = builder.createSpec() + if i < len(mutators)-1 { + _, err = mutators[i+1].Mutate(root, _c.builders[i+1].mutation) + } else { + spec := &sqlgraph.BatchCreateSpec{Nodes: specs} + spec.OnConflict = _c.conflict + // Invoke the actual operation on the latest mutation in the chain. + if err = sqlgraph.BatchCreate(ctx, _c.driver, spec); err != nil { + if sqlgraph.IsConstraintError(err) { + err = &ConstraintError{msg: err.Error(), wrap: err} + } + } + } + if err != nil { + return nil, err + } + mutation.id = &nodes[i].ID + mutation.done = true + return nodes[i], nil + }) + for i := len(builder.hooks) - 1; i >= 0; i-- { + mut = builder.hooks[i](mut) + } + mutators[i] = mut + }(i, ctx) + } + if len(mutators) > 0 { + if _, err := mutators[0].Mutate(ctx, _c.builders[0].mutation); err != nil { + return nil, err + } + } + return nodes, nil +} + +// SaveX is like Save, but panics if an error occurs. +func (_c *ProjectSyncStateCreateBulk) SaveX(ctx context.Context) []*ProjectSyncState { + v, err := _c.Save(ctx) + if err != nil { + panic(err) + } + return v +} + +// Exec executes the query. +func (_c *ProjectSyncStateCreateBulk) Exec(ctx context.Context) error { + _, err := _c.Save(ctx) + return err +} + +// ExecX is like Exec, but panics if an error occurs. +func (_c *ProjectSyncStateCreateBulk) ExecX(ctx context.Context) { + if err := _c.Exec(ctx); err != nil { + panic(err) + } +} + +// OnConflict allows configuring the `ON CONFLICT` / `ON DUPLICATE KEY` clause +// of the `INSERT` statement. For example: +// +// client.ProjectSyncState.CreateBulk(builders...). +// OnConflict( +// // Update the row with the new values +// // the was proposed for insertion. +// sql.ResolveWithNewValues(), +// ). +// // Override some of the fields with custom +// // update values. +// Update(func(u *ent.ProjectSyncStateUpsert) { +// SetProjectID(v+v). +// }). +// Exec(ctx) +func (_c *ProjectSyncStateCreateBulk) OnConflict(opts ...sql.ConflictOption) *ProjectSyncStateUpsertBulk { + _c.conflict = opts + return &ProjectSyncStateUpsertBulk{ + create: _c, + } +} + +// OnConflictColumns calls `OnConflict` and configures the columns +// as conflict target. Using this option is equivalent to using: +// +// client.ProjectSyncState.Create(). +// OnConflict(sql.ConflictColumns(columns...)). +// Exec(ctx) +func (_c *ProjectSyncStateCreateBulk) OnConflictColumns(columns ...string) *ProjectSyncStateUpsertBulk { + _c.conflict = append(_c.conflict, sql.ConflictColumns(columns...)) + return &ProjectSyncStateUpsertBulk{ + create: _c, + } +} + +// ProjectSyncStateUpsertBulk is the builder for "upsert"-ing +// a bulk of ProjectSyncState nodes. +type ProjectSyncStateUpsertBulk struct { + create *ProjectSyncStateCreateBulk +} + +// UpdateNewValues updates the mutable fields using the new values that +// were set on create. Using this option is equivalent to using: +// +// client.ProjectSyncState.Create(). +// OnConflict( +// sql.ResolveWithNewValues(), +// sql.ResolveWith(func(u *sql.UpdateSet) { +// u.SetIgnore(projectsyncstate.FieldID) +// }), +// ). +// Exec(ctx) +func (u *ProjectSyncStateUpsertBulk) UpdateNewValues() *ProjectSyncStateUpsertBulk { + u.create.conflict = append(u.create.conflict, sql.ResolveWithNewValues()) + u.create.conflict = append(u.create.conflict, sql.ResolveWith(func(s *sql.UpdateSet) { + for _, b := range u.create.builders { + if _, exists := b.mutation.ID(); exists { + s.SetIgnore(projectsyncstate.FieldID) + } + } + })) + return u +} + +// Ignore sets each column to itself in case of conflict. +// Using this option is equivalent to using: +// +// client.ProjectSyncState.Create(). +// OnConflict(sql.ResolveWithIgnore()). +// Exec(ctx) +func (u *ProjectSyncStateUpsertBulk) Ignore() *ProjectSyncStateUpsertBulk { + u.create.conflict = append(u.create.conflict, sql.ResolveWithIgnore()) + return u +} + +// DoNothing configures the conflict_action to `DO NOTHING`. +// Supported only by SQLite and PostgreSQL. +func (u *ProjectSyncStateUpsertBulk) DoNothing() *ProjectSyncStateUpsertBulk { + u.create.conflict = append(u.create.conflict, sql.DoNothing()) + return u +} + +// Update allows overriding fields `UPDATE` values. See the ProjectSyncStateCreateBulk.OnConflict +// documentation for more info. +func (u *ProjectSyncStateUpsertBulk) Update(set func(*ProjectSyncStateUpsert)) *ProjectSyncStateUpsertBulk { + u.create.conflict = append(u.create.conflict, sql.ResolveWith(func(update *sql.UpdateSet) { + set(&ProjectSyncStateUpsert{UpdateSet: update}) + })) + return u +} + +// SetProjectID sets the "project_id" field. +func (u *ProjectSyncStateUpsertBulk) SetProjectID(v uuid.UUID) *ProjectSyncStateUpsertBulk { + return u.Update(func(s *ProjectSyncStateUpsert) { + s.SetProjectID(v) + }) +} + +// UpdateProjectID sets the "project_id" field to the value that was provided on create. +func (u *ProjectSyncStateUpsertBulk) UpdateProjectID() *ProjectSyncStateUpsertBulk { + return u.Update(func(s *ProjectSyncStateUpsert) { + s.UpdateProjectID() + }) +} + +// SetBrokerID sets the "broker_id" field. +func (u *ProjectSyncStateUpsertBulk) SetBrokerID(v string) *ProjectSyncStateUpsertBulk { + return u.Update(func(s *ProjectSyncStateUpsert) { + s.SetBrokerID(v) + }) +} + +// UpdateBrokerID sets the "broker_id" field to the value that was provided on create. +func (u *ProjectSyncStateUpsertBulk) UpdateBrokerID() *ProjectSyncStateUpsertBulk { + return u.Update(func(s *ProjectSyncStateUpsert) { + s.UpdateBrokerID() + }) +} + +// SetLastSyncTime sets the "last_sync_time" field. +func (u *ProjectSyncStateUpsertBulk) SetLastSyncTime(v time.Time) *ProjectSyncStateUpsertBulk { + return u.Update(func(s *ProjectSyncStateUpsert) { + s.SetLastSyncTime(v) + }) +} + +// UpdateLastSyncTime sets the "last_sync_time" field to the value that was provided on create. +func (u *ProjectSyncStateUpsertBulk) UpdateLastSyncTime() *ProjectSyncStateUpsertBulk { + return u.Update(func(s *ProjectSyncStateUpsert) { + s.UpdateLastSyncTime() + }) +} + +// ClearLastSyncTime clears the value of the "last_sync_time" field. +func (u *ProjectSyncStateUpsertBulk) ClearLastSyncTime() *ProjectSyncStateUpsertBulk { + return u.Update(func(s *ProjectSyncStateUpsert) { + s.ClearLastSyncTime() + }) +} + +// SetLastCommitSha sets the "last_commit_sha" field. +func (u *ProjectSyncStateUpsertBulk) SetLastCommitSha(v string) *ProjectSyncStateUpsertBulk { + return u.Update(func(s *ProjectSyncStateUpsert) { + s.SetLastCommitSha(v) + }) +} + +// UpdateLastCommitSha sets the "last_commit_sha" field to the value that was provided on create. +func (u *ProjectSyncStateUpsertBulk) UpdateLastCommitSha() *ProjectSyncStateUpsertBulk { + return u.Update(func(s *ProjectSyncStateUpsert) { + s.UpdateLastCommitSha() + }) +} + +// ClearLastCommitSha clears the value of the "last_commit_sha" field. +func (u *ProjectSyncStateUpsertBulk) ClearLastCommitSha() *ProjectSyncStateUpsertBulk { + return u.Update(func(s *ProjectSyncStateUpsert) { + s.ClearLastCommitSha() + }) +} + +// SetFileCount sets the "file_count" field. +func (u *ProjectSyncStateUpsertBulk) SetFileCount(v int) *ProjectSyncStateUpsertBulk { + return u.Update(func(s *ProjectSyncStateUpsert) { + s.SetFileCount(v) + }) +} + +// AddFileCount adds v to the "file_count" field. +func (u *ProjectSyncStateUpsertBulk) AddFileCount(v int) *ProjectSyncStateUpsertBulk { + return u.Update(func(s *ProjectSyncStateUpsert) { + s.AddFileCount(v) + }) +} + +// UpdateFileCount sets the "file_count" field to the value that was provided on create. +func (u *ProjectSyncStateUpsertBulk) UpdateFileCount() *ProjectSyncStateUpsertBulk { + return u.Update(func(s *ProjectSyncStateUpsert) { + s.UpdateFileCount() + }) +} + +// SetTotalBytes sets the "total_bytes" field. +func (u *ProjectSyncStateUpsertBulk) SetTotalBytes(v int64) *ProjectSyncStateUpsertBulk { + return u.Update(func(s *ProjectSyncStateUpsert) { + s.SetTotalBytes(v) + }) +} + +// AddTotalBytes adds v to the "total_bytes" field. +func (u *ProjectSyncStateUpsertBulk) AddTotalBytes(v int64) *ProjectSyncStateUpsertBulk { + return u.Update(func(s *ProjectSyncStateUpsert) { + s.AddTotalBytes(v) + }) +} + +// UpdateTotalBytes sets the "total_bytes" field to the value that was provided on create. +func (u *ProjectSyncStateUpsertBulk) UpdateTotalBytes() *ProjectSyncStateUpsertBulk { + return u.Update(func(s *ProjectSyncStateUpsert) { + s.UpdateTotalBytes() + }) +} + +// Exec executes the query. +func (u *ProjectSyncStateUpsertBulk) Exec(ctx context.Context) error { + if u.create.err != nil { + return u.create.err + } + for i, b := range u.create.builders { + if len(b.conflict) != 0 { + return fmt.Errorf("ent: OnConflict was set for builder %d. Set it on the ProjectSyncStateCreateBulk instead", i) + } + } + if len(u.create.conflict) == 0 { + return errors.New("ent: missing options for ProjectSyncStateCreateBulk.OnConflict") + } + return u.create.Exec(ctx) +} + +// ExecX is like Exec, but panics if an error occurs. +func (u *ProjectSyncStateUpsertBulk) ExecX(ctx context.Context) { + if err := u.create.Exec(ctx); err != nil { + panic(err) + } +} diff --git a/pkg/ent/projectsyncstate_delete.go b/pkg/ent/projectsyncstate_delete.go new file mode 100644 index 000000000..06d24dc03 --- /dev/null +++ b/pkg/ent/projectsyncstate_delete.go @@ -0,0 +1,88 @@ +// Code generated by ent, DO NOT EDIT. + +package ent + +import ( + "context" + + "entgo.io/ent/dialect/sql" + "entgo.io/ent/dialect/sql/sqlgraph" + "entgo.io/ent/schema/field" + "github.com/GoogleCloudPlatform/scion/pkg/ent/predicate" + "github.com/GoogleCloudPlatform/scion/pkg/ent/projectsyncstate" +) + +// ProjectSyncStateDelete is the builder for deleting a ProjectSyncState entity. +type ProjectSyncStateDelete struct { + config + hooks []Hook + mutation *ProjectSyncStateMutation +} + +// Where appends a list predicates to the ProjectSyncStateDelete builder. +func (_d *ProjectSyncStateDelete) Where(ps ...predicate.ProjectSyncState) *ProjectSyncStateDelete { + _d.mutation.Where(ps...) + return _d +} + +// Exec executes the deletion query and returns how many vertices were deleted. +func (_d *ProjectSyncStateDelete) Exec(ctx context.Context) (int, error) { + return withHooks(ctx, _d.sqlExec, _d.mutation, _d.hooks) +} + +// ExecX is like Exec, but panics if an error occurs. +func (_d *ProjectSyncStateDelete) ExecX(ctx context.Context) int { + n, err := _d.Exec(ctx) + if err != nil { + panic(err) + } + return n +} + +func (_d *ProjectSyncStateDelete) sqlExec(ctx context.Context) (int, error) { + _spec := sqlgraph.NewDeleteSpec(projectsyncstate.Table, sqlgraph.NewFieldSpec(projectsyncstate.FieldID, field.TypeUUID)) + if ps := _d.mutation.predicates; len(ps) > 0 { + _spec.Predicate = func(selector *sql.Selector) { + for i := range ps { + ps[i](selector) + } + } + } + affected, err := sqlgraph.DeleteNodes(ctx, _d.driver, _spec) + if err != nil && sqlgraph.IsConstraintError(err) { + err = &ConstraintError{msg: err.Error(), wrap: err} + } + _d.mutation.done = true + return affected, err +} + +// ProjectSyncStateDeleteOne is the builder for deleting a single ProjectSyncState entity. +type ProjectSyncStateDeleteOne struct { + _d *ProjectSyncStateDelete +} + +// Where appends a list predicates to the ProjectSyncStateDelete builder. +func (_d *ProjectSyncStateDeleteOne) Where(ps ...predicate.ProjectSyncState) *ProjectSyncStateDeleteOne { + _d._d.mutation.Where(ps...) + return _d +} + +// Exec executes the deletion query. +func (_d *ProjectSyncStateDeleteOne) Exec(ctx context.Context) error { + n, err := _d._d.Exec(ctx) + switch { + case err != nil: + return err + case n == 0: + return &NotFoundError{projectsyncstate.Label} + default: + return nil + } +} + +// ExecX is like Exec, but panics if an error occurs. +func (_d *ProjectSyncStateDeleteOne) ExecX(ctx context.Context) { + if err := _d.Exec(ctx); err != nil { + panic(err) + } +} diff --git a/pkg/ent/projectsyncstate_query.go b/pkg/ent/projectsyncstate_query.go new file mode 100644 index 000000000..d04ccdfb2 --- /dev/null +++ b/pkg/ent/projectsyncstate_query.go @@ -0,0 +1,565 @@ +// Code generated by ent, DO NOT EDIT. + +package ent + +import ( + "context" + "fmt" + "math" + + "entgo.io/ent" + "entgo.io/ent/dialect" + "entgo.io/ent/dialect/sql" + "entgo.io/ent/dialect/sql/sqlgraph" + "entgo.io/ent/schema/field" + "github.com/GoogleCloudPlatform/scion/pkg/ent/predicate" + "github.com/GoogleCloudPlatform/scion/pkg/ent/projectsyncstate" + "github.com/google/uuid" +) + +// ProjectSyncStateQuery is the builder for querying ProjectSyncState entities. +type ProjectSyncStateQuery struct { + config + ctx *QueryContext + order []projectsyncstate.OrderOption + inters []Interceptor + predicates []predicate.ProjectSyncState + modifiers []func(*sql.Selector) + // intermediate query (i.e. traversal path). + sql *sql.Selector + path func(context.Context) (*sql.Selector, error) +} + +// Where adds a new predicate for the ProjectSyncStateQuery builder. +func (_q *ProjectSyncStateQuery) Where(ps ...predicate.ProjectSyncState) *ProjectSyncStateQuery { + _q.predicates = append(_q.predicates, ps...) + return _q +} + +// Limit the number of records to be returned by this query. +func (_q *ProjectSyncStateQuery) Limit(limit int) *ProjectSyncStateQuery { + _q.ctx.Limit = &limit + return _q +} + +// Offset to start from. +func (_q *ProjectSyncStateQuery) Offset(offset int) *ProjectSyncStateQuery { + _q.ctx.Offset = &offset + return _q +} + +// Unique configures the query builder to filter duplicate records on query. +// By default, unique is set to true, and can be disabled using this method. +func (_q *ProjectSyncStateQuery) Unique(unique bool) *ProjectSyncStateQuery { + _q.ctx.Unique = &unique + return _q +} + +// Order specifies how the records should be ordered. +func (_q *ProjectSyncStateQuery) Order(o ...projectsyncstate.OrderOption) *ProjectSyncStateQuery { + _q.order = append(_q.order, o...) + return _q +} + +// First returns the first ProjectSyncState entity from the query. +// Returns a *NotFoundError when no ProjectSyncState was found. +func (_q *ProjectSyncStateQuery) First(ctx context.Context) (*ProjectSyncState, error) { + nodes, err := _q.Limit(1).All(setContextOp(ctx, _q.ctx, ent.OpQueryFirst)) + if err != nil { + return nil, err + } + if len(nodes) == 0 { + return nil, &NotFoundError{projectsyncstate.Label} + } + return nodes[0], nil +} + +// FirstX is like First, but panics if an error occurs. +func (_q *ProjectSyncStateQuery) FirstX(ctx context.Context) *ProjectSyncState { + node, err := _q.First(ctx) + if err != nil && !IsNotFound(err) { + panic(err) + } + return node +} + +// FirstID returns the first ProjectSyncState ID from the query. +// Returns a *NotFoundError when no ProjectSyncState ID was found. +func (_q *ProjectSyncStateQuery) FirstID(ctx context.Context) (id uuid.UUID, err error) { + var ids []uuid.UUID + if ids, err = _q.Limit(1).IDs(setContextOp(ctx, _q.ctx, ent.OpQueryFirstID)); err != nil { + return + } + if len(ids) == 0 { + err = &NotFoundError{projectsyncstate.Label} + return + } + return ids[0], nil +} + +// FirstIDX is like FirstID, but panics if an error occurs. +func (_q *ProjectSyncStateQuery) FirstIDX(ctx context.Context) uuid.UUID { + id, err := _q.FirstID(ctx) + if err != nil && !IsNotFound(err) { + panic(err) + } + return id +} + +// Only returns a single ProjectSyncState entity found by the query, ensuring it only returns one. +// Returns a *NotSingularError when more than one ProjectSyncState entity is found. +// Returns a *NotFoundError when no ProjectSyncState entities are found. +func (_q *ProjectSyncStateQuery) Only(ctx context.Context) (*ProjectSyncState, error) { + nodes, err := _q.Limit(2).All(setContextOp(ctx, _q.ctx, ent.OpQueryOnly)) + if err != nil { + return nil, err + } + switch len(nodes) { + case 1: + return nodes[0], nil + case 0: + return nil, &NotFoundError{projectsyncstate.Label} + default: + return nil, &NotSingularError{projectsyncstate.Label} + } +} + +// OnlyX is like Only, but panics if an error occurs. +func (_q *ProjectSyncStateQuery) OnlyX(ctx context.Context) *ProjectSyncState { + node, err := _q.Only(ctx) + if err != nil { + panic(err) + } + return node +} + +// OnlyID is like Only, but returns the only ProjectSyncState ID in the query. +// Returns a *NotSingularError when more than one ProjectSyncState ID is found. +// Returns a *NotFoundError when no entities are found. +func (_q *ProjectSyncStateQuery) OnlyID(ctx context.Context) (id uuid.UUID, err error) { + var ids []uuid.UUID + if ids, err = _q.Limit(2).IDs(setContextOp(ctx, _q.ctx, ent.OpQueryOnlyID)); err != nil { + return + } + switch len(ids) { + case 1: + id = ids[0] + case 0: + err = &NotFoundError{projectsyncstate.Label} + default: + err = &NotSingularError{projectsyncstate.Label} + } + return +} + +// OnlyIDX is like OnlyID, but panics if an error occurs. +func (_q *ProjectSyncStateQuery) OnlyIDX(ctx context.Context) uuid.UUID { + id, err := _q.OnlyID(ctx) + if err != nil { + panic(err) + } + return id +} + +// All executes the query and returns a list of ProjectSyncStates. +func (_q *ProjectSyncStateQuery) All(ctx context.Context) ([]*ProjectSyncState, error) { + ctx = setContextOp(ctx, _q.ctx, ent.OpQueryAll) + if err := _q.prepareQuery(ctx); err != nil { + return nil, err + } + qr := querierAll[[]*ProjectSyncState, *ProjectSyncStateQuery]() + return withInterceptors[[]*ProjectSyncState](ctx, _q, qr, _q.inters) +} + +// AllX is like All, but panics if an error occurs. +func (_q *ProjectSyncStateQuery) AllX(ctx context.Context) []*ProjectSyncState { + nodes, err := _q.All(ctx) + if err != nil { + panic(err) + } + return nodes +} + +// IDs executes the query and returns a list of ProjectSyncState IDs. +func (_q *ProjectSyncStateQuery) IDs(ctx context.Context) (ids []uuid.UUID, err error) { + if _q.ctx.Unique == nil && _q.path != nil { + _q.Unique(true) + } + ctx = setContextOp(ctx, _q.ctx, ent.OpQueryIDs) + if err = _q.Select(projectsyncstate.FieldID).Scan(ctx, &ids); err != nil { + return nil, err + } + return ids, nil +} + +// IDsX is like IDs, but panics if an error occurs. +func (_q *ProjectSyncStateQuery) IDsX(ctx context.Context) []uuid.UUID { + ids, err := _q.IDs(ctx) + if err != nil { + panic(err) + } + return ids +} + +// Count returns the count of the given query. +func (_q *ProjectSyncStateQuery) Count(ctx context.Context) (int, error) { + ctx = setContextOp(ctx, _q.ctx, ent.OpQueryCount) + if err := _q.prepareQuery(ctx); err != nil { + return 0, err + } + return withInterceptors[int](ctx, _q, querierCount[*ProjectSyncStateQuery](), _q.inters) +} + +// CountX is like Count, but panics if an error occurs. +func (_q *ProjectSyncStateQuery) CountX(ctx context.Context) int { + count, err := _q.Count(ctx) + if err != nil { + panic(err) + } + return count +} + +// Exist returns true if the query has elements in the graph. +func (_q *ProjectSyncStateQuery) Exist(ctx context.Context) (bool, error) { + ctx = setContextOp(ctx, _q.ctx, ent.OpQueryExist) + switch _, err := _q.FirstID(ctx); { + case IsNotFound(err): + return false, nil + case err != nil: + return false, fmt.Errorf("ent: check existence: %w", err) + default: + return true, nil + } +} + +// ExistX is like Exist, but panics if an error occurs. +func (_q *ProjectSyncStateQuery) ExistX(ctx context.Context) bool { + exist, err := _q.Exist(ctx) + if err != nil { + panic(err) + } + return exist +} + +// Clone returns a duplicate of the ProjectSyncStateQuery builder, including all associated steps. It can be +// used to prepare common query builders and use them differently after the clone is made. +func (_q *ProjectSyncStateQuery) Clone() *ProjectSyncStateQuery { + if _q == nil { + return nil + } + return &ProjectSyncStateQuery{ + config: _q.config, + ctx: _q.ctx.Clone(), + order: append([]projectsyncstate.OrderOption{}, _q.order...), + inters: append([]Interceptor{}, _q.inters...), + predicates: append([]predicate.ProjectSyncState{}, _q.predicates...), + // clone intermediate query. + sql: _q.sql.Clone(), + path: _q.path, + } +} + +// GroupBy is used to group vertices by one or more fields/columns. +// It is often used with aggregate functions, like: count, max, mean, min, sum. +// +// Example: +// +// var v []struct { +// ProjectID uuid.UUID `json:"project_id,omitempty"` +// Count int `json:"count,omitempty"` +// } +// +// client.ProjectSyncState.Query(). +// GroupBy(projectsyncstate.FieldProjectID). +// Aggregate(ent.Count()). +// Scan(ctx, &v) +func (_q *ProjectSyncStateQuery) GroupBy(field string, fields ...string) *ProjectSyncStateGroupBy { + _q.ctx.Fields = append([]string{field}, fields...) + grbuild := &ProjectSyncStateGroupBy{build: _q} + grbuild.flds = &_q.ctx.Fields + grbuild.label = projectsyncstate.Label + grbuild.scan = grbuild.Scan + return grbuild +} + +// Select allows the selection one or more fields/columns for the given query, +// instead of selecting all fields in the entity. +// +// Example: +// +// var v []struct { +// ProjectID uuid.UUID `json:"project_id,omitempty"` +// } +// +// client.ProjectSyncState.Query(). +// Select(projectsyncstate.FieldProjectID). +// Scan(ctx, &v) +func (_q *ProjectSyncStateQuery) Select(fields ...string) *ProjectSyncStateSelect { + _q.ctx.Fields = append(_q.ctx.Fields, fields...) + sbuild := &ProjectSyncStateSelect{ProjectSyncStateQuery: _q} + sbuild.label = projectsyncstate.Label + sbuild.flds, sbuild.scan = &_q.ctx.Fields, sbuild.Scan + return sbuild +} + +// Aggregate returns a ProjectSyncStateSelect configured with the given aggregations. +func (_q *ProjectSyncStateQuery) Aggregate(fns ...AggregateFunc) *ProjectSyncStateSelect { + return _q.Select().Aggregate(fns...) +} + +func (_q *ProjectSyncStateQuery) prepareQuery(ctx context.Context) error { + for _, inter := range _q.inters { + if inter == nil { + return fmt.Errorf("ent: uninitialized interceptor (forgotten import ent/runtime?)") + } + if trv, ok := inter.(Traverser); ok { + if err := trv.Traverse(ctx, _q); err != nil { + return err + } + } + } + for _, f := range _q.ctx.Fields { + if !projectsyncstate.ValidColumn(f) { + return &ValidationError{Name: f, err: fmt.Errorf("ent: invalid field %q for query", f)} + } + } + if _q.path != nil { + prev, err := _q.path(ctx) + if err != nil { + return err + } + _q.sql = prev + } + return nil +} + +func (_q *ProjectSyncStateQuery) sqlAll(ctx context.Context, hooks ...queryHook) ([]*ProjectSyncState, error) { + var ( + nodes = []*ProjectSyncState{} + _spec = _q.querySpec() + ) + _spec.ScanValues = func(columns []string) ([]any, error) { + return (*ProjectSyncState).scanValues(nil, columns) + } + _spec.Assign = func(columns []string, values []any) error { + node := &ProjectSyncState{config: _q.config} + nodes = append(nodes, node) + return node.assignValues(columns, values) + } + if len(_q.modifiers) > 0 { + _spec.Modifiers = _q.modifiers + } + for i := range hooks { + hooks[i](ctx, _spec) + } + if err := sqlgraph.QueryNodes(ctx, _q.driver, _spec); err != nil { + return nil, err + } + if len(nodes) == 0 { + return nodes, nil + } + return nodes, nil +} + +func (_q *ProjectSyncStateQuery) sqlCount(ctx context.Context) (int, error) { + _spec := _q.querySpec() + if len(_q.modifiers) > 0 { + _spec.Modifiers = _q.modifiers + } + _spec.Node.Columns = _q.ctx.Fields + if len(_q.ctx.Fields) > 0 { + _spec.Unique = _q.ctx.Unique != nil && *_q.ctx.Unique + } + return sqlgraph.CountNodes(ctx, _q.driver, _spec) +} + +func (_q *ProjectSyncStateQuery) querySpec() *sqlgraph.QuerySpec { + _spec := sqlgraph.NewQuerySpec(projectsyncstate.Table, projectsyncstate.Columns, sqlgraph.NewFieldSpec(projectsyncstate.FieldID, field.TypeUUID)) + _spec.From = _q.sql + if unique := _q.ctx.Unique; unique != nil { + _spec.Unique = *unique + } else if _q.path != nil { + _spec.Unique = true + } + if fields := _q.ctx.Fields; len(fields) > 0 { + _spec.Node.Columns = make([]string, 0, len(fields)) + _spec.Node.Columns = append(_spec.Node.Columns, projectsyncstate.FieldID) + for i := range fields { + if fields[i] != projectsyncstate.FieldID { + _spec.Node.Columns = append(_spec.Node.Columns, fields[i]) + } + } + } + if ps := _q.predicates; len(ps) > 0 { + _spec.Predicate = func(selector *sql.Selector) { + for i := range ps { + ps[i](selector) + } + } + } + if limit := _q.ctx.Limit; limit != nil { + _spec.Limit = *limit + } + if offset := _q.ctx.Offset; offset != nil { + _spec.Offset = *offset + } + if ps := _q.order; len(ps) > 0 { + _spec.Order = func(selector *sql.Selector) { + for i := range ps { + ps[i](selector) + } + } + } + return _spec +} + +func (_q *ProjectSyncStateQuery) sqlQuery(ctx context.Context) *sql.Selector { + builder := sql.Dialect(_q.driver.Dialect()) + t1 := builder.Table(projectsyncstate.Table) + columns := _q.ctx.Fields + if len(columns) == 0 { + columns = projectsyncstate.Columns + } + selector := builder.Select(t1.Columns(columns...)...).From(t1) + if _q.sql != nil { + selector = _q.sql + selector.Select(selector.Columns(columns...)...) + } + if _q.ctx.Unique != nil && *_q.ctx.Unique { + selector.Distinct() + } + for _, m := range _q.modifiers { + m(selector) + } + for _, p := range _q.predicates { + p(selector) + } + for _, p := range _q.order { + p(selector) + } + if offset := _q.ctx.Offset; offset != nil { + // limit is mandatory for offset clause. We start + // with default value, and override it below if needed. + selector.Offset(*offset).Limit(math.MaxInt32) + } + if limit := _q.ctx.Limit; limit != nil { + selector.Limit(*limit) + } + return selector +} + +// ForUpdate locks the selected rows against concurrent updates, and prevent them from being +// updated, deleted or "selected ... for update" by other sessions, until the transaction is +// either committed or rolled-back. +func (_q *ProjectSyncStateQuery) ForUpdate(opts ...sql.LockOption) *ProjectSyncStateQuery { + if _q.driver.Dialect() == dialect.Postgres { + _q.Unique(false) + } + _q.modifiers = append(_q.modifiers, func(s *sql.Selector) { + s.ForUpdate(opts...) + }) + return _q +} + +// ForShare behaves similarly to ForUpdate, except that it acquires a shared mode lock +// on any rows that are read. Other sessions can read the rows, but cannot modify them +// until your transaction commits. +func (_q *ProjectSyncStateQuery) ForShare(opts ...sql.LockOption) *ProjectSyncStateQuery { + if _q.driver.Dialect() == dialect.Postgres { + _q.Unique(false) + } + _q.modifiers = append(_q.modifiers, func(s *sql.Selector) { + s.ForShare(opts...) + }) + return _q +} + +// ProjectSyncStateGroupBy is the group-by builder for ProjectSyncState entities. +type ProjectSyncStateGroupBy struct { + selector + build *ProjectSyncStateQuery +} + +// Aggregate adds the given aggregation functions to the group-by query. +func (_g *ProjectSyncStateGroupBy) Aggregate(fns ...AggregateFunc) *ProjectSyncStateGroupBy { + _g.fns = append(_g.fns, fns...) + return _g +} + +// Scan applies the selector query and scans the result into the given value. +func (_g *ProjectSyncStateGroupBy) Scan(ctx context.Context, v any) error { + ctx = setContextOp(ctx, _g.build.ctx, ent.OpQueryGroupBy) + if err := _g.build.prepareQuery(ctx); err != nil { + return err + } + return scanWithInterceptors[*ProjectSyncStateQuery, *ProjectSyncStateGroupBy](ctx, _g.build, _g, _g.build.inters, v) +} + +func (_g *ProjectSyncStateGroupBy) sqlScan(ctx context.Context, root *ProjectSyncStateQuery, v any) error { + selector := root.sqlQuery(ctx).Select() + aggregation := make([]string, 0, len(_g.fns)) + for _, fn := range _g.fns { + aggregation = append(aggregation, fn(selector)) + } + if len(selector.SelectedColumns()) == 0 { + columns := make([]string, 0, len(*_g.flds)+len(_g.fns)) + for _, f := range *_g.flds { + columns = append(columns, selector.C(f)) + } + columns = append(columns, aggregation...) + selector.Select(columns...) + } + selector.GroupBy(selector.Columns(*_g.flds...)...) + if err := selector.Err(); err != nil { + return err + } + rows := &sql.Rows{} + query, args := selector.Query() + if err := _g.build.driver.Query(ctx, query, args, rows); err != nil { + return err + } + defer rows.Close() + return sql.ScanSlice(rows, v) +} + +// ProjectSyncStateSelect is the builder for selecting fields of ProjectSyncState entities. +type ProjectSyncStateSelect struct { + *ProjectSyncStateQuery + selector +} + +// Aggregate adds the given aggregation functions to the selector query. +func (_s *ProjectSyncStateSelect) Aggregate(fns ...AggregateFunc) *ProjectSyncStateSelect { + _s.fns = append(_s.fns, fns...) + return _s +} + +// Scan applies the selector query and scans the result into the given value. +func (_s *ProjectSyncStateSelect) Scan(ctx context.Context, v any) error { + ctx = setContextOp(ctx, _s.ctx, ent.OpQuerySelect) + if err := _s.prepareQuery(ctx); err != nil { + return err + } + return scanWithInterceptors[*ProjectSyncStateQuery, *ProjectSyncStateSelect](ctx, _s.ProjectSyncStateQuery, _s, _s.inters, v) +} + +func (_s *ProjectSyncStateSelect) sqlScan(ctx context.Context, root *ProjectSyncStateQuery, v any) error { + selector := root.sqlQuery(ctx) + aggregation := make([]string, 0, len(_s.fns)) + for _, fn := range _s.fns { + aggregation = append(aggregation, fn(selector)) + } + switch n := len(*_s.selector.flds); { + case n == 0 && len(aggregation) > 0: + selector.Select(aggregation...) + case n != 0 && len(aggregation) > 0: + selector.AppendSelect(aggregation...) + } + rows := &sql.Rows{} + query, args := selector.Query() + if err := _s.driver.Query(ctx, query, args, rows); err != nil { + return err + } + defer rows.Close() + return sql.ScanSlice(rows, v) +} diff --git a/pkg/ent/projectsyncstate_update.go b/pkg/ent/projectsyncstate_update.go new file mode 100644 index 000000000..4339cd8b0 --- /dev/null +++ b/pkg/ent/projectsyncstate_update.go @@ -0,0 +1,457 @@ +// Code generated by ent, DO NOT EDIT. + +package ent + +import ( + "context" + "errors" + "fmt" + "time" + + "entgo.io/ent/dialect/sql" + "entgo.io/ent/dialect/sql/sqlgraph" + "entgo.io/ent/schema/field" + "github.com/GoogleCloudPlatform/scion/pkg/ent/predicate" + "github.com/GoogleCloudPlatform/scion/pkg/ent/projectsyncstate" + "github.com/google/uuid" +) + +// ProjectSyncStateUpdate is the builder for updating ProjectSyncState entities. +type ProjectSyncStateUpdate struct { + config + hooks []Hook + mutation *ProjectSyncStateMutation +} + +// Where appends a list predicates to the ProjectSyncStateUpdate builder. +func (_u *ProjectSyncStateUpdate) Where(ps ...predicate.ProjectSyncState) *ProjectSyncStateUpdate { + _u.mutation.Where(ps...) + return _u +} + +// SetProjectID sets the "project_id" field. +func (_u *ProjectSyncStateUpdate) SetProjectID(v uuid.UUID) *ProjectSyncStateUpdate { + _u.mutation.SetProjectID(v) + return _u +} + +// SetNillableProjectID sets the "project_id" field if the given value is not nil. +func (_u *ProjectSyncStateUpdate) SetNillableProjectID(v *uuid.UUID) *ProjectSyncStateUpdate { + if v != nil { + _u.SetProjectID(*v) + } + return _u +} + +// SetBrokerID sets the "broker_id" field. +func (_u *ProjectSyncStateUpdate) SetBrokerID(v string) *ProjectSyncStateUpdate { + _u.mutation.SetBrokerID(v) + return _u +} + +// SetNillableBrokerID sets the "broker_id" field if the given value is not nil. +func (_u *ProjectSyncStateUpdate) SetNillableBrokerID(v *string) *ProjectSyncStateUpdate { + if v != nil { + _u.SetBrokerID(*v) + } + return _u +} + +// SetLastSyncTime sets the "last_sync_time" field. +func (_u *ProjectSyncStateUpdate) SetLastSyncTime(v time.Time) *ProjectSyncStateUpdate { + _u.mutation.SetLastSyncTime(v) + return _u +} + +// SetNillableLastSyncTime sets the "last_sync_time" field if the given value is not nil. +func (_u *ProjectSyncStateUpdate) SetNillableLastSyncTime(v *time.Time) *ProjectSyncStateUpdate { + if v != nil { + _u.SetLastSyncTime(*v) + } + return _u +} + +// ClearLastSyncTime clears the value of the "last_sync_time" field. +func (_u *ProjectSyncStateUpdate) ClearLastSyncTime() *ProjectSyncStateUpdate { + _u.mutation.ClearLastSyncTime() + return _u +} + +// SetLastCommitSha sets the "last_commit_sha" field. +func (_u *ProjectSyncStateUpdate) SetLastCommitSha(v string) *ProjectSyncStateUpdate { + _u.mutation.SetLastCommitSha(v) + return _u +} + +// SetNillableLastCommitSha sets the "last_commit_sha" field if the given value is not nil. +func (_u *ProjectSyncStateUpdate) SetNillableLastCommitSha(v *string) *ProjectSyncStateUpdate { + if v != nil { + _u.SetLastCommitSha(*v) + } + return _u +} + +// ClearLastCommitSha clears the value of the "last_commit_sha" field. +func (_u *ProjectSyncStateUpdate) ClearLastCommitSha() *ProjectSyncStateUpdate { + _u.mutation.ClearLastCommitSha() + return _u +} + +// SetFileCount sets the "file_count" field. +func (_u *ProjectSyncStateUpdate) SetFileCount(v int) *ProjectSyncStateUpdate { + _u.mutation.ResetFileCount() + _u.mutation.SetFileCount(v) + return _u +} + +// SetNillableFileCount sets the "file_count" field if the given value is not nil. +func (_u *ProjectSyncStateUpdate) SetNillableFileCount(v *int) *ProjectSyncStateUpdate { + if v != nil { + _u.SetFileCount(*v) + } + return _u +} + +// AddFileCount adds value to the "file_count" field. +func (_u *ProjectSyncStateUpdate) AddFileCount(v int) *ProjectSyncStateUpdate { + _u.mutation.AddFileCount(v) + return _u +} + +// SetTotalBytes sets the "total_bytes" field. +func (_u *ProjectSyncStateUpdate) SetTotalBytes(v int64) *ProjectSyncStateUpdate { + _u.mutation.ResetTotalBytes() + _u.mutation.SetTotalBytes(v) + return _u +} + +// SetNillableTotalBytes sets the "total_bytes" field if the given value is not nil. +func (_u *ProjectSyncStateUpdate) SetNillableTotalBytes(v *int64) *ProjectSyncStateUpdate { + if v != nil { + _u.SetTotalBytes(*v) + } + return _u +} + +// AddTotalBytes adds value to the "total_bytes" field. +func (_u *ProjectSyncStateUpdate) AddTotalBytes(v int64) *ProjectSyncStateUpdate { + _u.mutation.AddTotalBytes(v) + return _u +} + +// Mutation returns the ProjectSyncStateMutation object of the builder. +func (_u *ProjectSyncStateUpdate) Mutation() *ProjectSyncStateMutation { + return _u.mutation +} + +// Save executes the query and returns the number of nodes affected by the update operation. +func (_u *ProjectSyncStateUpdate) Save(ctx context.Context) (int, error) { + return withHooks(ctx, _u.sqlSave, _u.mutation, _u.hooks) +} + +// SaveX is like Save, but panics if an error occurs. +func (_u *ProjectSyncStateUpdate) SaveX(ctx context.Context) int { + affected, err := _u.Save(ctx) + if err != nil { + panic(err) + } + return affected +} + +// Exec executes the query. +func (_u *ProjectSyncStateUpdate) Exec(ctx context.Context) error { + _, err := _u.Save(ctx) + return err +} + +// ExecX is like Exec, but panics if an error occurs. +func (_u *ProjectSyncStateUpdate) ExecX(ctx context.Context) { + if err := _u.Exec(ctx); err != nil { + panic(err) + } +} + +func (_u *ProjectSyncStateUpdate) sqlSave(ctx context.Context) (_node int, err error) { + _spec := sqlgraph.NewUpdateSpec(projectsyncstate.Table, projectsyncstate.Columns, sqlgraph.NewFieldSpec(projectsyncstate.FieldID, field.TypeUUID)) + if ps := _u.mutation.predicates; len(ps) > 0 { + _spec.Predicate = func(selector *sql.Selector) { + for i := range ps { + ps[i](selector) + } + } + } + if value, ok := _u.mutation.ProjectID(); ok { + _spec.SetField(projectsyncstate.FieldProjectID, field.TypeUUID, value) + } + if value, ok := _u.mutation.BrokerID(); ok { + _spec.SetField(projectsyncstate.FieldBrokerID, field.TypeString, value) + } + if value, ok := _u.mutation.LastSyncTime(); ok { + _spec.SetField(projectsyncstate.FieldLastSyncTime, field.TypeTime, value) + } + if _u.mutation.LastSyncTimeCleared() { + _spec.ClearField(projectsyncstate.FieldLastSyncTime, field.TypeTime) + } + if value, ok := _u.mutation.LastCommitSha(); ok { + _spec.SetField(projectsyncstate.FieldLastCommitSha, field.TypeString, value) + } + if _u.mutation.LastCommitShaCleared() { + _spec.ClearField(projectsyncstate.FieldLastCommitSha, field.TypeString) + } + if value, ok := _u.mutation.FileCount(); ok { + _spec.SetField(projectsyncstate.FieldFileCount, field.TypeInt, value) + } + if value, ok := _u.mutation.AddedFileCount(); ok { + _spec.AddField(projectsyncstate.FieldFileCount, field.TypeInt, value) + } + if value, ok := _u.mutation.TotalBytes(); ok { + _spec.SetField(projectsyncstate.FieldTotalBytes, field.TypeInt64, value) + } + if value, ok := _u.mutation.AddedTotalBytes(); ok { + _spec.AddField(projectsyncstate.FieldTotalBytes, field.TypeInt64, value) + } + if _node, err = sqlgraph.UpdateNodes(ctx, _u.driver, _spec); err != nil { + if _, ok := err.(*sqlgraph.NotFoundError); ok { + err = &NotFoundError{projectsyncstate.Label} + } else if sqlgraph.IsConstraintError(err) { + err = &ConstraintError{msg: err.Error(), wrap: err} + } + return 0, err + } + _u.mutation.done = true + return _node, nil +} + +// ProjectSyncStateUpdateOne is the builder for updating a single ProjectSyncState entity. +type ProjectSyncStateUpdateOne struct { + config + fields []string + hooks []Hook + mutation *ProjectSyncStateMutation +} + +// SetProjectID sets the "project_id" field. +func (_u *ProjectSyncStateUpdateOne) SetProjectID(v uuid.UUID) *ProjectSyncStateUpdateOne { + _u.mutation.SetProjectID(v) + return _u +} + +// SetNillableProjectID sets the "project_id" field if the given value is not nil. +func (_u *ProjectSyncStateUpdateOne) SetNillableProjectID(v *uuid.UUID) *ProjectSyncStateUpdateOne { + if v != nil { + _u.SetProjectID(*v) + } + return _u +} + +// SetBrokerID sets the "broker_id" field. +func (_u *ProjectSyncStateUpdateOne) SetBrokerID(v string) *ProjectSyncStateUpdateOne { + _u.mutation.SetBrokerID(v) + return _u +} + +// SetNillableBrokerID sets the "broker_id" field if the given value is not nil. +func (_u *ProjectSyncStateUpdateOne) SetNillableBrokerID(v *string) *ProjectSyncStateUpdateOne { + if v != nil { + _u.SetBrokerID(*v) + } + return _u +} + +// SetLastSyncTime sets the "last_sync_time" field. +func (_u *ProjectSyncStateUpdateOne) SetLastSyncTime(v time.Time) *ProjectSyncStateUpdateOne { + _u.mutation.SetLastSyncTime(v) + return _u +} + +// SetNillableLastSyncTime sets the "last_sync_time" field if the given value is not nil. +func (_u *ProjectSyncStateUpdateOne) SetNillableLastSyncTime(v *time.Time) *ProjectSyncStateUpdateOne { + if v != nil { + _u.SetLastSyncTime(*v) + } + return _u +} + +// ClearLastSyncTime clears the value of the "last_sync_time" field. +func (_u *ProjectSyncStateUpdateOne) ClearLastSyncTime() *ProjectSyncStateUpdateOne { + _u.mutation.ClearLastSyncTime() + return _u +} + +// SetLastCommitSha sets the "last_commit_sha" field. +func (_u *ProjectSyncStateUpdateOne) SetLastCommitSha(v string) *ProjectSyncStateUpdateOne { + _u.mutation.SetLastCommitSha(v) + return _u +} + +// SetNillableLastCommitSha sets the "last_commit_sha" field if the given value is not nil. +func (_u *ProjectSyncStateUpdateOne) SetNillableLastCommitSha(v *string) *ProjectSyncStateUpdateOne { + if v != nil { + _u.SetLastCommitSha(*v) + } + return _u +} + +// ClearLastCommitSha clears the value of the "last_commit_sha" field. +func (_u *ProjectSyncStateUpdateOne) ClearLastCommitSha() *ProjectSyncStateUpdateOne { + _u.mutation.ClearLastCommitSha() + return _u +} + +// SetFileCount sets the "file_count" field. +func (_u *ProjectSyncStateUpdateOne) SetFileCount(v int) *ProjectSyncStateUpdateOne { + _u.mutation.ResetFileCount() + _u.mutation.SetFileCount(v) + return _u +} + +// SetNillableFileCount sets the "file_count" field if the given value is not nil. +func (_u *ProjectSyncStateUpdateOne) SetNillableFileCount(v *int) *ProjectSyncStateUpdateOne { + if v != nil { + _u.SetFileCount(*v) + } + return _u +} + +// AddFileCount adds value to the "file_count" field. +func (_u *ProjectSyncStateUpdateOne) AddFileCount(v int) *ProjectSyncStateUpdateOne { + _u.mutation.AddFileCount(v) + return _u +} + +// SetTotalBytes sets the "total_bytes" field. +func (_u *ProjectSyncStateUpdateOne) SetTotalBytes(v int64) *ProjectSyncStateUpdateOne { + _u.mutation.ResetTotalBytes() + _u.mutation.SetTotalBytes(v) + return _u +} + +// SetNillableTotalBytes sets the "total_bytes" field if the given value is not nil. +func (_u *ProjectSyncStateUpdateOne) SetNillableTotalBytes(v *int64) *ProjectSyncStateUpdateOne { + if v != nil { + _u.SetTotalBytes(*v) + } + return _u +} + +// AddTotalBytes adds value to the "total_bytes" field. +func (_u *ProjectSyncStateUpdateOne) AddTotalBytes(v int64) *ProjectSyncStateUpdateOne { + _u.mutation.AddTotalBytes(v) + return _u +} + +// Mutation returns the ProjectSyncStateMutation object of the builder. +func (_u *ProjectSyncStateUpdateOne) Mutation() *ProjectSyncStateMutation { + return _u.mutation +} + +// Where appends a list predicates to the ProjectSyncStateUpdate builder. +func (_u *ProjectSyncStateUpdateOne) Where(ps ...predicate.ProjectSyncState) *ProjectSyncStateUpdateOne { + _u.mutation.Where(ps...) + return _u +} + +// Select allows selecting one or more fields (columns) of the returned entity. +// The default is selecting all fields defined in the entity schema. +func (_u *ProjectSyncStateUpdateOne) Select(field string, fields ...string) *ProjectSyncStateUpdateOne { + _u.fields = append([]string{field}, fields...) + return _u +} + +// Save executes the query and returns the updated ProjectSyncState entity. +func (_u *ProjectSyncStateUpdateOne) Save(ctx context.Context) (*ProjectSyncState, error) { + return withHooks(ctx, _u.sqlSave, _u.mutation, _u.hooks) +} + +// SaveX is like Save, but panics if an error occurs. +func (_u *ProjectSyncStateUpdateOne) SaveX(ctx context.Context) *ProjectSyncState { + node, err := _u.Save(ctx) + if err != nil { + panic(err) + } + return node +} + +// Exec executes the query on the entity. +func (_u *ProjectSyncStateUpdateOne) Exec(ctx context.Context) error { + _, err := _u.Save(ctx) + return err +} + +// ExecX is like Exec, but panics if an error occurs. +func (_u *ProjectSyncStateUpdateOne) ExecX(ctx context.Context) { + if err := _u.Exec(ctx); err != nil { + panic(err) + } +} + +func (_u *ProjectSyncStateUpdateOne) sqlSave(ctx context.Context) (_node *ProjectSyncState, err error) { + _spec := sqlgraph.NewUpdateSpec(projectsyncstate.Table, projectsyncstate.Columns, sqlgraph.NewFieldSpec(projectsyncstate.FieldID, field.TypeUUID)) + id, ok := _u.mutation.ID() + if !ok { + return nil, &ValidationError{Name: "id", err: errors.New(`ent: missing "ProjectSyncState.id" for update`)} + } + _spec.Node.ID.Value = id + if fields := _u.fields; len(fields) > 0 { + _spec.Node.Columns = make([]string, 0, len(fields)) + _spec.Node.Columns = append(_spec.Node.Columns, projectsyncstate.FieldID) + for _, f := range fields { + if !projectsyncstate.ValidColumn(f) { + return nil, &ValidationError{Name: f, err: fmt.Errorf("ent: invalid field %q for query", f)} + } + if f != projectsyncstate.FieldID { + _spec.Node.Columns = append(_spec.Node.Columns, f) + } + } + } + if ps := _u.mutation.predicates; len(ps) > 0 { + _spec.Predicate = func(selector *sql.Selector) { + for i := range ps { + ps[i](selector) + } + } + } + if value, ok := _u.mutation.ProjectID(); ok { + _spec.SetField(projectsyncstate.FieldProjectID, field.TypeUUID, value) + } + if value, ok := _u.mutation.BrokerID(); ok { + _spec.SetField(projectsyncstate.FieldBrokerID, field.TypeString, value) + } + if value, ok := _u.mutation.LastSyncTime(); ok { + _spec.SetField(projectsyncstate.FieldLastSyncTime, field.TypeTime, value) + } + if _u.mutation.LastSyncTimeCleared() { + _spec.ClearField(projectsyncstate.FieldLastSyncTime, field.TypeTime) + } + if value, ok := _u.mutation.LastCommitSha(); ok { + _spec.SetField(projectsyncstate.FieldLastCommitSha, field.TypeString, value) + } + if _u.mutation.LastCommitShaCleared() { + _spec.ClearField(projectsyncstate.FieldLastCommitSha, field.TypeString) + } + if value, ok := _u.mutation.FileCount(); ok { + _spec.SetField(projectsyncstate.FieldFileCount, field.TypeInt, value) + } + if value, ok := _u.mutation.AddedFileCount(); ok { + _spec.AddField(projectsyncstate.FieldFileCount, field.TypeInt, value) + } + if value, ok := _u.mutation.TotalBytes(); ok { + _spec.SetField(projectsyncstate.FieldTotalBytes, field.TypeInt64, value) + } + if value, ok := _u.mutation.AddedTotalBytes(); ok { + _spec.AddField(projectsyncstate.FieldTotalBytes, field.TypeInt64, value) + } + _node = &ProjectSyncState{config: _u.config} + _spec.Assign = _node.assignValues + _spec.ScanValues = _node.scanValues + if err = sqlgraph.UpdateNode(ctx, _u.driver, _spec); err != nil { + if _, ok := err.(*sqlgraph.NotFoundError); ok { + err = &NotFoundError{projectsyncstate.Label} + } else if sqlgraph.IsConstraintError(err) { + err = &ConstraintError{msg: err.Error(), wrap: err} + } + return nil, err + } + _u.mutation.done = true + return _node, nil +} diff --git a/pkg/ent/runtime.go b/pkg/ent/runtime.go index 09fe221e7..5f4320d11 100644 --- a/pkg/ent/runtime.go +++ b/pkg/ent/runtime.go @@ -7,12 +7,36 @@ import ( "github.com/GoogleCloudPlatform/scion/pkg/ent/accesspolicy" "github.com/GoogleCloudPlatform/scion/pkg/ent/agent" + "github.com/GoogleCloudPlatform/scion/pkg/ent/allowlistentry" + "github.com/GoogleCloudPlatform/scion/pkg/ent/apikey" + "github.com/GoogleCloudPlatform/scion/pkg/ent/brokerdispatch" + "github.com/GoogleCloudPlatform/scion/pkg/ent/brokerjointoken" + "github.com/GoogleCloudPlatform/scion/pkg/ent/brokersecret" + "github.com/GoogleCloudPlatform/scion/pkg/ent/envvar" + "github.com/GoogleCloudPlatform/scion/pkg/ent/gcpserviceaccount" + "github.com/GoogleCloudPlatform/scion/pkg/ent/githubinstallation" "github.com/GoogleCloudPlatform/scion/pkg/ent/group" "github.com/GoogleCloudPlatform/scion/pkg/ent/groupmembership" + "github.com/GoogleCloudPlatform/scion/pkg/ent/harnessconfig" + "github.com/GoogleCloudPlatform/scion/pkg/ent/invitecode" + "github.com/GoogleCloudPlatform/scion/pkg/ent/maintenanceoperation" + "github.com/GoogleCloudPlatform/scion/pkg/ent/maintenanceoperationrun" + "github.com/GoogleCloudPlatform/scion/pkg/ent/message" + "github.com/GoogleCloudPlatform/scion/pkg/ent/notification" + "github.com/GoogleCloudPlatform/scion/pkg/ent/notificationsubscription" "github.com/GoogleCloudPlatform/scion/pkg/ent/policybinding" "github.com/GoogleCloudPlatform/scion/pkg/ent/project" + "github.com/GoogleCloudPlatform/scion/pkg/ent/projectcontributor" + "github.com/GoogleCloudPlatform/scion/pkg/ent/projectsyncstate" + "github.com/GoogleCloudPlatform/scion/pkg/ent/runtimebroker" + "github.com/GoogleCloudPlatform/scion/pkg/ent/schedule" + "github.com/GoogleCloudPlatform/scion/pkg/ent/scheduledevent" "github.com/GoogleCloudPlatform/scion/pkg/ent/schema" + "github.com/GoogleCloudPlatform/scion/pkg/ent/secret" + "github.com/GoogleCloudPlatform/scion/pkg/ent/subscriptiontemplate" + "github.com/GoogleCloudPlatform/scion/pkg/ent/template" "github.com/GoogleCloudPlatform/scion/pkg/ent/user" + "github.com/GoogleCloudPlatform/scion/pkg/ent/useraccesstoken" "github.com/google/uuid" ) @@ -66,20 +90,250 @@ func init() { agentDescVisibility := agentFields[9].Descriptor() // agent.DefaultVisibility holds the default value on creation for the visibility field. agent.DefaultVisibility = agentDescVisibility.Default.(string) + // agentDescCurrentTurns is the schema descriptor for current_turns field. + agentDescCurrentTurns := agentFields[19].Descriptor() + // agent.DefaultCurrentTurns holds the default value on creation for the current_turns field. + agent.DefaultCurrentTurns = agentDescCurrentTurns.Default.(int) + // agentDescCurrentModelCalls is the schema descriptor for current_model_calls field. + agentDescCurrentModelCalls := agentFields[20].Descriptor() + // agent.DefaultCurrentModelCalls holds the default value on creation for the current_model_calls field. + agent.DefaultCurrentModelCalls = agentDescCurrentModelCalls.Default.(int) + // agentDescDetached is the schema descriptor for detached field. + agentDescDetached := agentFields[22].Descriptor() + // agent.DefaultDetached holds the default value on creation for the detached field. + agent.DefaultDetached = agentDescDetached.Default.(bool) + // agentDescWebPtyEnabled is the schema descriptor for web_pty_enabled field. + agentDescWebPtyEnabled := agentFields[25].Descriptor() + // agent.DefaultWebPtyEnabled holds the default value on creation for the web_pty_enabled field. + agent.DefaultWebPtyEnabled = agentDescWebPtyEnabled.Default.(bool) // agentDescCreated is the schema descriptor for created field. - agentDescCreated := agentFields[10].Descriptor() + agentDescCreated := agentFields[30].Descriptor() // agent.DefaultCreated holds the default value on creation for the created field. agent.DefaultCreated = agentDescCreated.Default.(func() time.Time) // agentDescUpdated is the schema descriptor for updated field. - agentDescUpdated := agentFields[11].Descriptor() + agentDescUpdated := agentFields[31].Descriptor() // agent.DefaultUpdated holds the default value on creation for the updated field. agent.DefaultUpdated = agentDescUpdated.Default.(func() time.Time) // agent.UpdateDefaultUpdated holds the default value on update for the updated field. agent.UpdateDefaultUpdated = agentDescUpdated.UpdateDefault.(func() time.Time) + // agentDescStateVersion is the schema descriptor for state_version field. + agentDescStateVersion := agentFields[36].Descriptor() + // agent.DefaultStateVersion holds the default value on creation for the state_version field. + agent.DefaultStateVersion = agentDescStateVersion.Default.(int64) // agentDescID is the schema descriptor for id field. agentDescID := agentFields[0].Descriptor() // agent.DefaultID holds the default value on creation for the id field. agent.DefaultID = agentDescID.Default.(func() uuid.UUID) + allowlistentryFields := schema.AllowListEntry{}.Fields() + _ = allowlistentryFields + // allowlistentryDescEmail is the schema descriptor for email field. + allowlistentryDescEmail := allowlistentryFields[1].Descriptor() + // allowlistentry.EmailValidator is a validator for the "email" field. It is called by the builders before save. + allowlistentry.EmailValidator = allowlistentryDescEmail.Validators[0].(func(string) error) + // allowlistentryDescNote is the schema descriptor for note field. + allowlistentryDescNote := allowlistentryFields[2].Descriptor() + // allowlistentry.DefaultNote holds the default value on creation for the note field. + allowlistentry.DefaultNote = allowlistentryDescNote.Default.(string) + // allowlistentryDescAddedBy is the schema descriptor for added_by field. + allowlistentryDescAddedBy := allowlistentryFields[3].Descriptor() + // allowlistentry.AddedByValidator is a validator for the "added_by" field. It is called by the builders before save. + allowlistentry.AddedByValidator = allowlistentryDescAddedBy.Validators[0].(func(string) error) + // allowlistentryDescCreated is the schema descriptor for created field. + allowlistentryDescCreated := allowlistentryFields[5].Descriptor() + // allowlistentry.DefaultCreated holds the default value on creation for the created field. + allowlistentry.DefaultCreated = allowlistentryDescCreated.Default.(func() time.Time) + // allowlistentryDescID is the schema descriptor for id field. + allowlistentryDescID := allowlistentryFields[0].Descriptor() + // allowlistentry.DefaultID holds the default value on creation for the id field. + allowlistentry.DefaultID = allowlistentryDescID.Default.(func() uuid.UUID) + apikeyFields := schema.ApiKey{}.Fields() + _ = apikeyFields + // apikeyDescKeyHash is the schema descriptor for key_hash field. + apikeyDescKeyHash := apikeyFields[4].Descriptor() + // apikey.KeyHashValidator is a validator for the "key_hash" field. It is called by the builders before save. + apikey.KeyHashValidator = apikeyDescKeyHash.Validators[0].(func(string) error) + // apikeyDescRevoked is the schema descriptor for revoked field. + apikeyDescRevoked := apikeyFields[6].Descriptor() + // apikey.DefaultRevoked holds the default value on creation for the revoked field. + apikey.DefaultRevoked = apikeyDescRevoked.Default.(bool) + // apikeyDescCreated is the schema descriptor for created field. + apikeyDescCreated := apikeyFields[9].Descriptor() + // apikey.DefaultCreated holds the default value on creation for the created field. + apikey.DefaultCreated = apikeyDescCreated.Default.(func() time.Time) + // apikeyDescID is the schema descriptor for id field. + apikeyDescID := apikeyFields[0].Descriptor() + // apikey.DefaultID holds the default value on creation for the id field. + apikey.DefaultID = apikeyDescID.Default.(func() uuid.UUID) + brokerdispatchFields := schema.BrokerDispatch{}.Fields() + _ = brokerdispatchFields + // brokerdispatchDescOp is the schema descriptor for op field. + brokerdispatchDescOp := brokerdispatchFields[5].Descriptor() + // brokerdispatch.OpValidator is a validator for the "op" field. It is called by the builders before save. + brokerdispatch.OpValidator = brokerdispatchDescOp.Validators[0].(func(string) error) + // brokerdispatchDescState is the schema descriptor for state field. + brokerdispatchDescState := brokerdispatchFields[7].Descriptor() + // brokerdispatch.DefaultState holds the default value on creation for the state field. + brokerdispatch.DefaultState = brokerdispatchDescState.Default.(string) + // brokerdispatchDescAttempts is the schema descriptor for attempts field. + brokerdispatchDescAttempts := brokerdispatchFields[10].Descriptor() + // brokerdispatch.DefaultAttempts holds the default value on creation for the attempts field. + brokerdispatch.DefaultAttempts = brokerdispatchDescAttempts.Default.(int) + // brokerdispatchDescCreatedAt is the schema descriptor for created_at field. + brokerdispatchDescCreatedAt := brokerdispatchFields[12].Descriptor() + // brokerdispatch.DefaultCreatedAt holds the default value on creation for the created_at field. + brokerdispatch.DefaultCreatedAt = brokerdispatchDescCreatedAt.Default.(func() time.Time) + // brokerdispatchDescUpdatedAt is the schema descriptor for updated_at field. + brokerdispatchDescUpdatedAt := brokerdispatchFields[13].Descriptor() + // brokerdispatch.DefaultUpdatedAt holds the default value on creation for the updated_at field. + brokerdispatch.DefaultUpdatedAt = brokerdispatchDescUpdatedAt.Default.(func() time.Time) + // brokerdispatch.UpdateDefaultUpdatedAt holds the default value on update for the updated_at field. + brokerdispatch.UpdateDefaultUpdatedAt = brokerdispatchDescUpdatedAt.UpdateDefault.(func() time.Time) + // brokerdispatchDescID is the schema descriptor for id field. + brokerdispatchDescID := brokerdispatchFields[0].Descriptor() + // brokerdispatch.DefaultID holds the default value on creation for the id field. + brokerdispatch.DefaultID = brokerdispatchDescID.Default.(func() uuid.UUID) + brokerjointokenFields := schema.BrokerJoinToken{}.Fields() + _ = brokerjointokenFields + // brokerjointokenDescTokenHash is the schema descriptor for token_hash field. + brokerjointokenDescTokenHash := brokerjointokenFields[1].Descriptor() + // brokerjointoken.TokenHashValidator is a validator for the "token_hash" field. It is called by the builders before save. + brokerjointoken.TokenHashValidator = brokerjointokenDescTokenHash.Validators[0].(func(string) error) + // brokerjointokenDescCreatedBy is the schema descriptor for created_by field. + brokerjointokenDescCreatedBy := brokerjointokenFields[3].Descriptor() + // brokerjointoken.CreatedByValidator is a validator for the "created_by" field. It is called by the builders before save. + brokerjointoken.CreatedByValidator = brokerjointokenDescCreatedBy.Validators[0].(func(string) error) + // brokerjointokenDescCreated is the schema descriptor for created field. + brokerjointokenDescCreated := brokerjointokenFields[4].Descriptor() + // brokerjointoken.DefaultCreated holds the default value on creation for the created field. + brokerjointoken.DefaultCreated = brokerjointokenDescCreated.Default.(func() time.Time) + brokersecretFields := schema.BrokerSecret{}.Fields() + _ = brokersecretFields + // brokersecretDescSecretKey is the schema descriptor for secret_key field. + brokersecretDescSecretKey := brokersecretFields[1].Descriptor() + // brokersecret.SecretKeyValidator is a validator for the "secret_key" field. It is called by the builders before save. + brokersecret.SecretKeyValidator = brokersecretDescSecretKey.Validators[0].(func([]byte) error) + // brokersecretDescAlgorithm is the schema descriptor for algorithm field. + brokersecretDescAlgorithm := brokersecretFields[2].Descriptor() + // brokersecret.DefaultAlgorithm holds the default value on creation for the algorithm field. + brokersecret.DefaultAlgorithm = brokersecretDescAlgorithm.Default.(string) + // brokersecretDescStatus is the schema descriptor for status field. + brokersecretDescStatus := brokersecretFields[5].Descriptor() + // brokersecret.DefaultStatus holds the default value on creation for the status field. + brokersecret.DefaultStatus = brokersecretDescStatus.Default.(string) + // brokersecretDescCreated is the schema descriptor for created field. + brokersecretDescCreated := brokersecretFields[6].Descriptor() + // brokersecret.DefaultCreated holds the default value on creation for the created field. + brokersecret.DefaultCreated = brokersecretDescCreated.Default.(func() time.Time) + envvarFields := schema.EnvVar{}.Fields() + _ = envvarFields + // envvarDescKey is the schema descriptor for key field. + envvarDescKey := envvarFields[1].Descriptor() + // envvar.KeyValidator is a validator for the "key" field. It is called by the builders before save. + envvar.KeyValidator = envvarDescKey.Validators[0].(func(string) error) + // envvarDescScope is the schema descriptor for scope field. + envvarDescScope := envvarFields[3].Descriptor() + // envvar.ScopeValidator is a validator for the "scope" field. It is called by the builders before save. + envvar.ScopeValidator = envvarDescScope.Validators[0].(func(string) error) + // envvarDescSensitive is the schema descriptor for sensitive field. + envvarDescSensitive := envvarFields[6].Descriptor() + // envvar.DefaultSensitive holds the default value on creation for the sensitive field. + envvar.DefaultSensitive = envvarDescSensitive.Default.(bool) + // envvarDescSecret is the schema descriptor for secret field. + envvarDescSecret := envvarFields[8].Descriptor() + // envvar.DefaultSecret holds the default value on creation for the secret field. + envvar.DefaultSecret = envvarDescSecret.Default.(bool) + // envvarDescCreated is the schema descriptor for created field. + envvarDescCreated := envvarFields[10].Descriptor() + // envvar.DefaultCreated holds the default value on creation for the created field. + envvar.DefaultCreated = envvarDescCreated.Default.(func() time.Time) + // envvarDescUpdated is the schema descriptor for updated field. + envvarDescUpdated := envvarFields[11].Descriptor() + // envvar.DefaultUpdated holds the default value on creation for the updated field. + envvar.DefaultUpdated = envvarDescUpdated.Default.(func() time.Time) + // envvar.UpdateDefaultUpdated holds the default value on update for the updated field. + envvar.UpdateDefaultUpdated = envvarDescUpdated.UpdateDefault.(func() time.Time) + // envvarDescID is the schema descriptor for id field. + envvarDescID := envvarFields[0].Descriptor() + // envvar.DefaultID holds the default value on creation for the id field. + envvar.DefaultID = envvarDescID.Default.(func() uuid.UUID) + gcpserviceaccountFields := schema.GCPServiceAccount{}.Fields() + _ = gcpserviceaccountFields + // gcpserviceaccountDescScope is the schema descriptor for scope field. + gcpserviceaccountDescScope := gcpserviceaccountFields[1].Descriptor() + // gcpserviceaccount.ScopeValidator is a validator for the "scope" field. It is called by the builders before save. + gcpserviceaccount.ScopeValidator = gcpserviceaccountDescScope.Validators[0].(func(string) error) + // gcpserviceaccountDescScopeID is the schema descriptor for scope_id field. + gcpserviceaccountDescScopeID := gcpserviceaccountFields[2].Descriptor() + // gcpserviceaccount.ScopeIDValidator is a validator for the "scope_id" field. It is called by the builders before save. + gcpserviceaccount.ScopeIDValidator = gcpserviceaccountDescScopeID.Validators[0].(func(string) error) + // gcpserviceaccountDescEmail is the schema descriptor for email field. + gcpserviceaccountDescEmail := gcpserviceaccountFields[3].Descriptor() + // gcpserviceaccount.EmailValidator is a validator for the "email" field. It is called by the builders before save. + gcpserviceaccount.EmailValidator = gcpserviceaccountDescEmail.Validators[0].(func(string) error) + // gcpserviceaccountDescProjectID is the schema descriptor for project_id field. + gcpserviceaccountDescProjectID := gcpserviceaccountFields[4].Descriptor() + // gcpserviceaccount.ProjectIDValidator is a validator for the "project_id" field. It is called by the builders before save. + gcpserviceaccount.ProjectIDValidator = gcpserviceaccountDescProjectID.Validators[0].(func(string) error) + // gcpserviceaccountDescDisplayName is the schema descriptor for display_name field. + gcpserviceaccountDescDisplayName := gcpserviceaccountFields[5].Descriptor() + // gcpserviceaccount.DefaultDisplayName holds the default value on creation for the display_name field. + gcpserviceaccount.DefaultDisplayName = gcpserviceaccountDescDisplayName.Default.(string) + // gcpserviceaccountDescDefaultScopes is the schema descriptor for default_scopes field. + gcpserviceaccountDescDefaultScopes := gcpserviceaccountFields[6].Descriptor() + // gcpserviceaccount.DefaultDefaultScopes holds the default value on creation for the default_scopes field. + gcpserviceaccount.DefaultDefaultScopes = gcpserviceaccountDescDefaultScopes.Default.(string) + // gcpserviceaccountDescVerified is the schema descriptor for verified field. + gcpserviceaccountDescVerified := gcpserviceaccountFields[7].Descriptor() + // gcpserviceaccount.DefaultVerified holds the default value on creation for the verified field. + gcpserviceaccount.DefaultVerified = gcpserviceaccountDescVerified.Default.(bool) + // gcpserviceaccountDescCreatedBy is the schema descriptor for created_by field. + gcpserviceaccountDescCreatedBy := gcpserviceaccountFields[9].Descriptor() + // gcpserviceaccount.DefaultCreatedBy holds the default value on creation for the created_by field. + gcpserviceaccount.DefaultCreatedBy = gcpserviceaccountDescCreatedBy.Default.(string) + // gcpserviceaccountDescManaged is the schema descriptor for managed field. + gcpserviceaccountDescManaged := gcpserviceaccountFields[10].Descriptor() + // gcpserviceaccount.DefaultManaged holds the default value on creation for the managed field. + gcpserviceaccount.DefaultManaged = gcpserviceaccountDescManaged.Default.(bool) + // gcpserviceaccountDescManagedBy is the schema descriptor for managed_by field. + gcpserviceaccountDescManagedBy := gcpserviceaccountFields[11].Descriptor() + // gcpserviceaccount.DefaultManagedBy holds the default value on creation for the managed_by field. + gcpserviceaccount.DefaultManagedBy = gcpserviceaccountDescManagedBy.Default.(string) + // gcpserviceaccountDescCreated is the schema descriptor for created field. + gcpserviceaccountDescCreated := gcpserviceaccountFields[12].Descriptor() + // gcpserviceaccount.DefaultCreated holds the default value on creation for the created field. + gcpserviceaccount.DefaultCreated = gcpserviceaccountDescCreated.Default.(func() time.Time) + // gcpserviceaccountDescID is the schema descriptor for id field. + gcpserviceaccountDescID := gcpserviceaccountFields[0].Descriptor() + // gcpserviceaccount.DefaultID holds the default value on creation for the id field. + gcpserviceaccount.DefaultID = gcpserviceaccountDescID.Default.(func() uuid.UUID) + githubinstallationFields := schema.GithubInstallation{}.Fields() + _ = githubinstallationFields + // githubinstallationDescAccountLogin is the schema descriptor for account_login field. + githubinstallationDescAccountLogin := githubinstallationFields[1].Descriptor() + // githubinstallation.AccountLoginValidator is a validator for the "account_login" field. It is called by the builders before save. + githubinstallation.AccountLoginValidator = githubinstallationDescAccountLogin.Validators[0].(func(string) error) + // githubinstallationDescAccountType is the schema descriptor for account_type field. + githubinstallationDescAccountType := githubinstallationFields[2].Descriptor() + // githubinstallation.DefaultAccountType holds the default value on creation for the account_type field. + githubinstallation.DefaultAccountType = githubinstallationDescAccountType.Default.(string) + // githubinstallationDescRepositories is the schema descriptor for repositories field. + githubinstallationDescRepositories := githubinstallationFields[4].Descriptor() + // githubinstallation.DefaultRepositories holds the default value on creation for the repositories field. + githubinstallation.DefaultRepositories = githubinstallationDescRepositories.Default.(string) + // githubinstallationDescStatus is the schema descriptor for status field. + githubinstallationDescStatus := githubinstallationFields[5].Descriptor() + // githubinstallation.DefaultStatus holds the default value on creation for the status field. + githubinstallation.DefaultStatus = githubinstallationDescStatus.Default.(string) + // githubinstallationDescCreated is the schema descriptor for created field. + githubinstallationDescCreated := githubinstallationFields[6].Descriptor() + // githubinstallation.DefaultCreated holds the default value on creation for the created field. + githubinstallation.DefaultCreated = githubinstallationDescCreated.Default.(func() time.Time) + // githubinstallationDescUpdated is the schema descriptor for updated field. + githubinstallationDescUpdated := githubinstallationFields[7].Descriptor() + // githubinstallation.DefaultUpdated holds the default value on creation for the updated field. + githubinstallation.DefaultUpdated = githubinstallationDescUpdated.Default.(func() time.Time) + // githubinstallation.UpdateDefaultUpdated holds the default value on update for the updated field. + githubinstallation.UpdateDefaultUpdated = githubinstallationDescUpdated.UpdateDefault.(func() time.Time) groupFields := schema.Group{}.Fields() _ = groupFields // groupDescName is the schema descriptor for name field. @@ -114,6 +368,242 @@ func init() { groupmembershipDescID := groupmembershipFields[0].Descriptor() // groupmembership.DefaultID holds the default value on creation for the id field. groupmembership.DefaultID = groupmembershipDescID.Default.(func() uuid.UUID) + harnessconfigFields := schema.HarnessConfig{}.Fields() + _ = harnessconfigFields + // harnessconfigDescName is the schema descriptor for name field. + harnessconfigDescName := harnessconfigFields[1].Descriptor() + // harnessconfig.NameValidator is a validator for the "name" field. It is called by the builders before save. + harnessconfig.NameValidator = harnessconfigDescName.Validators[0].(func(string) error) + // harnessconfigDescSlug is the schema descriptor for slug field. + harnessconfigDescSlug := harnessconfigFields[2].Descriptor() + // harnessconfig.SlugValidator is a validator for the "slug" field. It is called by the builders before save. + harnessconfig.SlugValidator = harnessconfigDescSlug.Validators[0].(func(string) error) + // harnessconfigDescHarness is the schema descriptor for harness field. + harnessconfigDescHarness := harnessconfigFields[5].Descriptor() + // harnessconfig.HarnessValidator is a validator for the "harness" field. It is called by the builders before save. + harnessconfig.HarnessValidator = harnessconfigDescHarness.Validators[0].(func(string) error) + // harnessconfigDescScope is the schema descriptor for scope field. + harnessconfigDescScope := harnessconfigFields[8].Descriptor() + // harnessconfig.DefaultScope holds the default value on creation for the scope field. + harnessconfig.DefaultScope = harnessconfigDescScope.Default.(string) + // harnessconfigDescVisibility is the schema descriptor for visibility field. + harnessconfigDescVisibility := harnessconfigFields[18].Descriptor() + // harnessconfig.DefaultVisibility holds the default value on creation for the visibility field. + harnessconfig.DefaultVisibility = harnessconfigDescVisibility.Default.(string) + // harnessconfigDescCreated is the schema descriptor for created field. + harnessconfigDescCreated := harnessconfigFields[19].Descriptor() + // harnessconfig.DefaultCreated holds the default value on creation for the created field. + harnessconfig.DefaultCreated = harnessconfigDescCreated.Default.(func() time.Time) + // harnessconfigDescUpdated is the schema descriptor for updated field. + harnessconfigDescUpdated := harnessconfigFields[20].Descriptor() + // harnessconfig.DefaultUpdated holds the default value on creation for the updated field. + harnessconfig.DefaultUpdated = harnessconfigDescUpdated.Default.(func() time.Time) + // harnessconfig.UpdateDefaultUpdated holds the default value on update for the updated field. + harnessconfig.UpdateDefaultUpdated = harnessconfigDescUpdated.UpdateDefault.(func() time.Time) + // harnessconfigDescID is the schema descriptor for id field. + harnessconfigDescID := harnessconfigFields[0].Descriptor() + // harnessconfig.DefaultID holds the default value on creation for the id field. + harnessconfig.DefaultID = harnessconfigDescID.Default.(func() uuid.UUID) + invitecodeFields := schema.InviteCode{}.Fields() + _ = invitecodeFields + // invitecodeDescCodeHash is the schema descriptor for code_hash field. + invitecodeDescCodeHash := invitecodeFields[1].Descriptor() + // invitecode.CodeHashValidator is a validator for the "code_hash" field. It is called by the builders before save. + invitecode.CodeHashValidator = invitecodeDescCodeHash.Validators[0].(func(string) error) + // invitecodeDescCodePrefix is the schema descriptor for code_prefix field. + invitecodeDescCodePrefix := invitecodeFields[2].Descriptor() + // invitecode.CodePrefixValidator is a validator for the "code_prefix" field. It is called by the builders before save. + invitecode.CodePrefixValidator = invitecodeDescCodePrefix.Validators[0].(func(string) error) + // invitecodeDescMaxUses is the schema descriptor for max_uses field. + invitecodeDescMaxUses := invitecodeFields[3].Descriptor() + // invitecode.DefaultMaxUses holds the default value on creation for the max_uses field. + invitecode.DefaultMaxUses = invitecodeDescMaxUses.Default.(int) + // invitecodeDescUseCount is the schema descriptor for use_count field. + invitecodeDescUseCount := invitecodeFields[4].Descriptor() + // invitecode.DefaultUseCount holds the default value on creation for the use_count field. + invitecode.DefaultUseCount = invitecodeDescUseCount.Default.(int) + // invitecodeDescRevoked is the schema descriptor for revoked field. + invitecodeDescRevoked := invitecodeFields[6].Descriptor() + // invitecode.DefaultRevoked holds the default value on creation for the revoked field. + invitecode.DefaultRevoked = invitecodeDescRevoked.Default.(bool) + // invitecodeDescCreatedBy is the schema descriptor for created_by field. + invitecodeDescCreatedBy := invitecodeFields[7].Descriptor() + // invitecode.CreatedByValidator is a validator for the "created_by" field. It is called by the builders before save. + invitecode.CreatedByValidator = invitecodeDescCreatedBy.Validators[0].(func(string) error) + // invitecodeDescNote is the schema descriptor for note field. + invitecodeDescNote := invitecodeFields[8].Descriptor() + // invitecode.DefaultNote holds the default value on creation for the note field. + invitecode.DefaultNote = invitecodeDescNote.Default.(string) + // invitecodeDescCreated is the schema descriptor for created field. + invitecodeDescCreated := invitecodeFields[9].Descriptor() + // invitecode.DefaultCreated holds the default value on creation for the created field. + invitecode.DefaultCreated = invitecodeDescCreated.Default.(func() time.Time) + // invitecodeDescID is the schema descriptor for id field. + invitecodeDescID := invitecodeFields[0].Descriptor() + // invitecode.DefaultID holds the default value on creation for the id field. + invitecode.DefaultID = invitecodeDescID.Default.(func() uuid.UUID) + maintenanceoperationFields := schema.MaintenanceOperation{}.Fields() + _ = maintenanceoperationFields + // maintenanceoperationDescKey is the schema descriptor for key field. + maintenanceoperationDescKey := maintenanceoperationFields[1].Descriptor() + // maintenanceoperation.KeyValidator is a validator for the "key" field. It is called by the builders before save. + maintenanceoperation.KeyValidator = maintenanceoperationDescKey.Validators[0].(func(string) error) + // maintenanceoperationDescTitle is the schema descriptor for title field. + maintenanceoperationDescTitle := maintenanceoperationFields[2].Descriptor() + // maintenanceoperation.TitleValidator is a validator for the "title" field. It is called by the builders before save. + maintenanceoperation.TitleValidator = maintenanceoperationDescTitle.Validators[0].(func(string) error) + // maintenanceoperationDescDescription is the schema descriptor for description field. + maintenanceoperationDescDescription := maintenanceoperationFields[3].Descriptor() + // maintenanceoperation.DefaultDescription holds the default value on creation for the description field. + maintenanceoperation.DefaultDescription = maintenanceoperationDescDescription.Default.(string) + // maintenanceoperationDescCategory is the schema descriptor for category field. + maintenanceoperationDescCategory := maintenanceoperationFields[4].Descriptor() + // maintenanceoperation.CategoryValidator is a validator for the "category" field. It is called by the builders before save. + maintenanceoperation.CategoryValidator = maintenanceoperationDescCategory.Validators[0].(func(string) error) + // maintenanceoperationDescStatus is the schema descriptor for status field. + maintenanceoperationDescStatus := maintenanceoperationFields[5].Descriptor() + // maintenanceoperation.DefaultStatus holds the default value on creation for the status field. + maintenanceoperation.DefaultStatus = maintenanceoperationDescStatus.Default.(string) + // maintenanceoperationDescMetadata is the schema descriptor for metadata field. + maintenanceoperationDescMetadata := maintenanceoperationFields[10].Descriptor() + // maintenanceoperation.DefaultMetadata holds the default value on creation for the metadata field. + maintenanceoperation.DefaultMetadata = maintenanceoperationDescMetadata.Default.(string) + // maintenanceoperationDescCreated is the schema descriptor for created field. + maintenanceoperationDescCreated := maintenanceoperationFields[11].Descriptor() + // maintenanceoperation.DefaultCreated holds the default value on creation for the created field. + maintenanceoperation.DefaultCreated = maintenanceoperationDescCreated.Default.(func() time.Time) + // maintenanceoperationDescID is the schema descriptor for id field. + maintenanceoperationDescID := maintenanceoperationFields[0].Descriptor() + // maintenanceoperation.DefaultID holds the default value on creation for the id field. + maintenanceoperation.DefaultID = maintenanceoperationDescID.Default.(func() uuid.UUID) + maintenanceoperationrunFields := schema.MaintenanceOperationRun{}.Fields() + _ = maintenanceoperationrunFields + // maintenanceoperationrunDescOperationKey is the schema descriptor for operation_key field. + maintenanceoperationrunDescOperationKey := maintenanceoperationrunFields[1].Descriptor() + // maintenanceoperationrun.OperationKeyValidator is a validator for the "operation_key" field. It is called by the builders before save. + maintenanceoperationrun.OperationKeyValidator = maintenanceoperationrunDescOperationKey.Validators[0].(func(string) error) + // maintenanceoperationrunDescStatus is the schema descriptor for status field. + maintenanceoperationrunDescStatus := maintenanceoperationrunFields[2].Descriptor() + // maintenanceoperationrun.DefaultStatus holds the default value on creation for the status field. + maintenanceoperationrun.DefaultStatus = maintenanceoperationrunDescStatus.Default.(string) + // maintenanceoperationrunDescStartedAt is the schema descriptor for started_at field. + maintenanceoperationrunDescStartedAt := maintenanceoperationrunFields[3].Descriptor() + // maintenanceoperationrun.DefaultStartedAt holds the default value on creation for the started_at field. + maintenanceoperationrun.DefaultStartedAt = maintenanceoperationrunDescStartedAt.Default.(func() time.Time) + // maintenanceoperationrunDescLog is the schema descriptor for log field. + maintenanceoperationrunDescLog := maintenanceoperationrunFields[7].Descriptor() + // maintenanceoperationrun.DefaultLog holds the default value on creation for the log field. + maintenanceoperationrun.DefaultLog = maintenanceoperationrunDescLog.Default.(string) + // maintenanceoperationrunDescID is the schema descriptor for id field. + maintenanceoperationrunDescID := maintenanceoperationrunFields[0].Descriptor() + // maintenanceoperationrun.DefaultID holds the default value on creation for the id field. + maintenanceoperationrun.DefaultID = maintenanceoperationrunDescID.Default.(func() uuid.UUID) + messageFields := schema.Message{}.Fields() + _ = messageFields + // messageDescSender is the schema descriptor for sender field. + messageDescSender := messageFields[2].Descriptor() + // message.SenderValidator is a validator for the "sender" field. It is called by the builders before save. + message.SenderValidator = messageDescSender.Validators[0].(func(string) error) + // messageDescRecipient is the schema descriptor for recipient field. + messageDescRecipient := messageFields[4].Descriptor() + // message.RecipientValidator is a validator for the "recipient" field. It is called by the builders before save. + message.RecipientValidator = messageDescRecipient.Validators[0].(func(string) error) + // messageDescMsg is the schema descriptor for msg field. + messageDescMsg := messageFields[6].Descriptor() + // message.MsgValidator is a validator for the "msg" field. It is called by the builders before save. + message.MsgValidator = messageDescMsg.Validators[0].(func(string) error) + // messageDescType is the schema descriptor for type field. + messageDescType := messageFields[7].Descriptor() + // message.DefaultType holds the default value on creation for the type field. + message.DefaultType = messageDescType.Default.(string) + // messageDescUrgent is the schema descriptor for urgent field. + messageDescUrgent := messageFields[8].Descriptor() + // message.DefaultUrgent holds the default value on creation for the urgent field. + message.DefaultUrgent = messageDescUrgent.Default.(bool) + // messageDescBroadcasted is the schema descriptor for broadcasted field. + messageDescBroadcasted := messageFields[9].Descriptor() + // message.DefaultBroadcasted holds the default value on creation for the broadcasted field. + message.DefaultBroadcasted = messageDescBroadcasted.Default.(bool) + // messageDescRead is the schema descriptor for read field. + messageDescRead := messageFields[10].Descriptor() + // message.DefaultRead holds the default value on creation for the read field. + message.DefaultRead = messageDescRead.Default.(bool) + // messageDescDispatchState is the schema descriptor for dispatch_state field. + messageDescDispatchState := messageFields[13].Descriptor() + // message.DefaultDispatchState holds the default value on creation for the dispatch_state field. + message.DefaultDispatchState = messageDescDispatchState.Default.(string) + // messageDescCreated is the schema descriptor for created field. + messageDescCreated := messageFields[15].Descriptor() + // message.DefaultCreated holds the default value on creation for the created field. + message.DefaultCreated = messageDescCreated.Default.(func() time.Time) + // messageDescID is the schema descriptor for id field. + messageDescID := messageFields[0].Descriptor() + // message.DefaultID holds the default value on creation for the id field. + message.DefaultID = messageDescID.Default.(func() uuid.UUID) + notificationFields := schema.Notification{}.Fields() + _ = notificationFields + // notificationDescSubscriberType is the schema descriptor for subscriber_type field. + notificationDescSubscriberType := notificationFields[4].Descriptor() + // notification.SubscriberTypeValidator is a validator for the "subscriber_type" field. It is called by the builders before save. + notification.SubscriberTypeValidator = notificationDescSubscriberType.Validators[0].(func(string) error) + // notificationDescSubscriberID is the schema descriptor for subscriber_id field. + notificationDescSubscriberID := notificationFields[5].Descriptor() + // notification.SubscriberIDValidator is a validator for the "subscriber_id" field. It is called by the builders before save. + notification.SubscriberIDValidator = notificationDescSubscriberID.Validators[0].(func(string) error) + // notificationDescStatus is the schema descriptor for status field. + notificationDescStatus := notificationFields[6].Descriptor() + // notification.StatusValidator is a validator for the "status" field. It is called by the builders before save. + notification.StatusValidator = notificationDescStatus.Validators[0].(func(string) error) + // notificationDescMessage is the schema descriptor for message field. + notificationDescMessage := notificationFields[7].Descriptor() + // notification.MessageValidator is a validator for the "message" field. It is called by the builders before save. + notification.MessageValidator = notificationDescMessage.Validators[0].(func(string) error) + // notificationDescDispatched is the schema descriptor for dispatched field. + notificationDescDispatched := notificationFields[8].Descriptor() + // notification.DefaultDispatched holds the default value on creation for the dispatched field. + notification.DefaultDispatched = notificationDescDispatched.Default.(bool) + // notificationDescAcknowledged is the schema descriptor for acknowledged field. + notificationDescAcknowledged := notificationFields[9].Descriptor() + // notification.DefaultAcknowledged holds the default value on creation for the acknowledged field. + notification.DefaultAcknowledged = notificationDescAcknowledged.Default.(bool) + // notificationDescCreated is the schema descriptor for created field. + notificationDescCreated := notificationFields[10].Descriptor() + // notification.DefaultCreated holds the default value on creation for the created field. + notification.DefaultCreated = notificationDescCreated.Default.(func() time.Time) + // notificationDescID is the schema descriptor for id field. + notificationDescID := notificationFields[0].Descriptor() + // notification.DefaultID holds the default value on creation for the id field. + notification.DefaultID = notificationDescID.Default.(func() uuid.UUID) + notificationsubscriptionFields := schema.NotificationSubscription{}.Fields() + _ = notificationsubscriptionFields + // notificationsubscriptionDescScope is the schema descriptor for scope field. + notificationsubscriptionDescScope := notificationsubscriptionFields[1].Descriptor() + // notificationsubscription.DefaultScope holds the default value on creation for the scope field. + notificationsubscription.DefaultScope = notificationsubscriptionDescScope.Default.(string) + // notificationsubscriptionDescSubscriberType is the schema descriptor for subscriber_type field. + notificationsubscriptionDescSubscriberType := notificationsubscriptionFields[3].Descriptor() + // notificationsubscription.DefaultSubscriberType holds the default value on creation for the subscriber_type field. + notificationsubscription.DefaultSubscriberType = notificationsubscriptionDescSubscriberType.Default.(string) + // notificationsubscriptionDescSubscriberID is the schema descriptor for subscriber_id field. + notificationsubscriptionDescSubscriberID := notificationsubscriptionFields[4].Descriptor() + // notificationsubscription.SubscriberIDValidator is a validator for the "subscriber_id" field. It is called by the builders before save. + notificationsubscription.SubscriberIDValidator = notificationsubscriptionDescSubscriberID.Validators[0].(func(string) error) + // notificationsubscriptionDescTriggerActivities is the schema descriptor for trigger_activities field. + notificationsubscriptionDescTriggerActivities := notificationsubscriptionFields[6].Descriptor() + // notificationsubscription.TriggerActivitiesValidator is a validator for the "trigger_activities" field. It is called by the builders before save. + notificationsubscription.TriggerActivitiesValidator = notificationsubscriptionDescTriggerActivities.Validators[0].(func(string) error) + // notificationsubscriptionDescCreatedBy is the schema descriptor for created_by field. + notificationsubscriptionDescCreatedBy := notificationsubscriptionFields[7].Descriptor() + // notificationsubscription.CreatedByValidator is a validator for the "created_by" field. It is called by the builders before save. + notificationsubscription.CreatedByValidator = notificationsubscriptionDescCreatedBy.Validators[0].(func(string) error) + // notificationsubscriptionDescCreated is the schema descriptor for created field. + notificationsubscriptionDescCreated := notificationsubscriptionFields[8].Descriptor() + // notificationsubscription.DefaultCreated holds the default value on creation for the created field. + notificationsubscription.DefaultCreated = notificationsubscriptionDescCreated.Default.(func() time.Time) + // notificationsubscriptionDescID is the schema descriptor for id field. + notificationsubscriptionDescID := notificationsubscriptionFields[0].Descriptor() + // notificationsubscription.DefaultID holds the default value on creation for the id field. + notificationsubscription.DefaultID = notificationsubscriptionDescID.Default.(func() uuid.UUID) policybindingFields := schema.PolicyBinding{}.Fields() _ = policybindingFields // policybindingDescCreated is the schema descriptor for created field. @@ -135,33 +625,261 @@ func init() { // project.SlugValidator is a validator for the "slug" field. It is called by the builders before save. project.SlugValidator = projectDescSlug.Validators[0].(func(string) error) // projectDescCreated is the schema descriptor for created field. - projectDescCreated := projectFields[6].Descriptor() + projectDescCreated := projectFields[8].Descriptor() // project.DefaultCreated holds the default value on creation for the created field. project.DefaultCreated = projectDescCreated.Default.(func() time.Time) // projectDescUpdated is the schema descriptor for updated field. - projectDescUpdated := projectFields[7].Descriptor() + projectDescUpdated := projectFields[9].Descriptor() // project.DefaultUpdated holds the default value on creation for the updated field. project.DefaultUpdated = projectDescUpdated.Default.(func() time.Time) // project.UpdateDefaultUpdated holds the default value on update for the updated field. project.UpdateDefaultUpdated = projectDescUpdated.UpdateDefault.(func() time.Time) // projectDescVisibility is the schema descriptor for visibility field. - projectDescVisibility := projectFields[10].Descriptor() + projectDescVisibility := projectFields[12].Descriptor() // project.DefaultVisibility holds the default value on creation for the visibility field. project.DefaultVisibility = projectDescVisibility.Default.(string) // projectDescID is the schema descriptor for id field. projectDescID := projectFields[0].Descriptor() // project.DefaultID holds the default value on creation for the id field. project.DefaultID = projectDescID.Default.(func() uuid.UUID) + projectcontributorFields := schema.ProjectContributor{}.Fields() + _ = projectcontributorFields + // projectcontributorDescBrokerName is the schema descriptor for broker_name field. + projectcontributorDescBrokerName := projectcontributorFields[3].Descriptor() + // projectcontributor.BrokerNameValidator is a validator for the "broker_name" field. It is called by the builders before save. + projectcontributor.BrokerNameValidator = projectcontributorDescBrokerName.Validators[0].(func(string) error) + // projectcontributorDescMode is the schema descriptor for mode field. + projectcontributorDescMode := projectcontributorFields[4].Descriptor() + // projectcontributor.DefaultMode holds the default value on creation for the mode field. + projectcontributor.DefaultMode = projectcontributorDescMode.Default.(string) + // projectcontributorDescStatus is the schema descriptor for status field. + projectcontributorDescStatus := projectcontributorFields[5].Descriptor() + // projectcontributor.DefaultStatus holds the default value on creation for the status field. + projectcontributor.DefaultStatus = projectcontributorDescStatus.Default.(string) + // projectcontributorDescID is the schema descriptor for id field. + projectcontributorDescID := projectcontributorFields[0].Descriptor() + // projectcontributor.DefaultID holds the default value on creation for the id field. + projectcontributor.DefaultID = projectcontributorDescID.Default.(func() uuid.UUID) + projectsyncstateFields := schema.ProjectSyncState{}.Fields() + _ = projectsyncstateFields + // projectsyncstateDescBrokerID is the schema descriptor for broker_id field. + projectsyncstateDescBrokerID := projectsyncstateFields[2].Descriptor() + // projectsyncstate.DefaultBrokerID holds the default value on creation for the broker_id field. + projectsyncstate.DefaultBrokerID = projectsyncstateDescBrokerID.Default.(string) + // projectsyncstateDescFileCount is the schema descriptor for file_count field. + projectsyncstateDescFileCount := projectsyncstateFields[5].Descriptor() + // projectsyncstate.DefaultFileCount holds the default value on creation for the file_count field. + projectsyncstate.DefaultFileCount = projectsyncstateDescFileCount.Default.(int) + // projectsyncstateDescTotalBytes is the schema descriptor for total_bytes field. + projectsyncstateDescTotalBytes := projectsyncstateFields[6].Descriptor() + // projectsyncstate.DefaultTotalBytes holds the default value on creation for the total_bytes field. + projectsyncstate.DefaultTotalBytes = projectsyncstateDescTotalBytes.Default.(int64) + // projectsyncstateDescID is the schema descriptor for id field. + projectsyncstateDescID := projectsyncstateFields[0].Descriptor() + // projectsyncstate.DefaultID holds the default value on creation for the id field. + projectsyncstate.DefaultID = projectsyncstateDescID.Default.(func() uuid.UUID) + runtimebrokerFields := schema.RuntimeBroker{}.Fields() + _ = runtimebrokerFields + // runtimebrokerDescName is the schema descriptor for name field. + runtimebrokerDescName := runtimebrokerFields[1].Descriptor() + // runtimebroker.NameValidator is a validator for the "name" field. It is called by the builders before save. + runtimebroker.NameValidator = runtimebrokerDescName.Validators[0].(func(string) error) + // runtimebrokerDescSlug is the schema descriptor for slug field. + runtimebrokerDescSlug := runtimebrokerFields[2].Descriptor() + // runtimebroker.SlugValidator is a validator for the "slug" field. It is called by the builders before save. + runtimebroker.SlugValidator = runtimebrokerDescSlug.Validators[0].(func(string) error) + // runtimebrokerDescMode is the schema descriptor for mode field. + runtimebrokerDescMode := runtimebrokerFields[4].Descriptor() + // runtimebroker.DefaultMode holds the default value on creation for the mode field. + runtimebroker.DefaultMode = runtimebrokerDescMode.Default.(string) + // runtimebrokerDescLockVersion is the schema descriptor for lock_version field. + runtimebrokerDescLockVersion := runtimebrokerFields[6].Descriptor() + // runtimebroker.DefaultLockVersion holds the default value on creation for the lock_version field. + runtimebroker.DefaultLockVersion = runtimebrokerDescLockVersion.Default.(int64) + // runtimebrokerDescStatus is the schema descriptor for status field. + runtimebrokerDescStatus := runtimebrokerFields[7].Descriptor() + // runtimebroker.DefaultStatus holds the default value on creation for the status field. + runtimebroker.DefaultStatus = runtimebrokerDescStatus.Default.(string) + // runtimebrokerDescConnectionState is the schema descriptor for connection_state field. + runtimebrokerDescConnectionState := runtimebrokerFields[8].Descriptor() + // runtimebroker.DefaultConnectionState holds the default value on creation for the connection_state field. + runtimebroker.DefaultConnectionState = runtimebrokerDescConnectionState.Default.(string) + // runtimebrokerDescAutoProvide is the schema descriptor for auto_provide field. + runtimebrokerDescAutoProvide := runtimebrokerFields[18].Descriptor() + // runtimebroker.DefaultAutoProvide holds the default value on creation for the auto_provide field. + runtimebroker.DefaultAutoProvide = runtimebrokerDescAutoProvide.Default.(bool) + // runtimebrokerDescCreated is the schema descriptor for created field. + runtimebrokerDescCreated := runtimebrokerFields[22].Descriptor() + // runtimebroker.DefaultCreated holds the default value on creation for the created field. + runtimebroker.DefaultCreated = runtimebrokerDescCreated.Default.(func() time.Time) + // runtimebrokerDescUpdated is the schema descriptor for updated field. + runtimebrokerDescUpdated := runtimebrokerFields[23].Descriptor() + // runtimebroker.DefaultUpdated holds the default value on creation for the updated field. + runtimebroker.DefaultUpdated = runtimebrokerDescUpdated.Default.(func() time.Time) + // runtimebroker.UpdateDefaultUpdated holds the default value on update for the updated field. + runtimebroker.UpdateDefaultUpdated = runtimebrokerDescUpdated.UpdateDefault.(func() time.Time) + // runtimebrokerDescID is the schema descriptor for id field. + runtimebrokerDescID := runtimebrokerFields[0].Descriptor() + // runtimebroker.DefaultID holds the default value on creation for the id field. + runtimebroker.DefaultID = runtimebrokerDescID.Default.(func() uuid.UUID) + scheduleFields := schema.Schedule{}.Fields() + _ = scheduleFields + // scheduleDescName is the schema descriptor for name field. + scheduleDescName := scheduleFields[2].Descriptor() + // schedule.NameValidator is a validator for the "name" field. It is called by the builders before save. + schedule.NameValidator = scheduleDescName.Validators[0].(func(string) error) + // scheduleDescCronExpr is the schema descriptor for cron_expr field. + scheduleDescCronExpr := scheduleFields[3].Descriptor() + // schedule.CronExprValidator is a validator for the "cron_expr" field. It is called by the builders before save. + schedule.CronExprValidator = scheduleDescCronExpr.Validators[0].(func(string) error) + // scheduleDescEventType is the schema descriptor for event_type field. + scheduleDescEventType := scheduleFields[4].Descriptor() + // schedule.EventTypeValidator is a validator for the "event_type" field. It is called by the builders before save. + schedule.EventTypeValidator = scheduleDescEventType.Validators[0].(func(string) error) + // scheduleDescPayload is the schema descriptor for payload field. + scheduleDescPayload := scheduleFields[5].Descriptor() + // schedule.DefaultPayload holds the default value on creation for the payload field. + schedule.DefaultPayload = scheduleDescPayload.Default.(string) + // scheduleDescStatus is the schema descriptor for status field. + scheduleDescStatus := scheduleFields[6].Descriptor() + // schedule.DefaultStatus holds the default value on creation for the status field. + schedule.DefaultStatus = scheduleDescStatus.Default.(string) + // scheduleDescRunCount is the schema descriptor for run_count field. + scheduleDescRunCount := scheduleFields[11].Descriptor() + // schedule.DefaultRunCount holds the default value on creation for the run_count field. + schedule.DefaultRunCount = scheduleDescRunCount.Default.(int) + // scheduleDescErrorCount is the schema descriptor for error_count field. + scheduleDescErrorCount := scheduleFields[12].Descriptor() + // schedule.DefaultErrorCount holds the default value on creation for the error_count field. + schedule.DefaultErrorCount = scheduleDescErrorCount.Default.(int) + // scheduleDescCreated is the schema descriptor for created field. + scheduleDescCreated := scheduleFields[14].Descriptor() + // schedule.DefaultCreated holds the default value on creation for the created field. + schedule.DefaultCreated = scheduleDescCreated.Default.(func() time.Time) + // scheduleDescUpdated is the schema descriptor for updated field. + scheduleDescUpdated := scheduleFields[15].Descriptor() + // schedule.DefaultUpdated holds the default value on creation for the updated field. + schedule.DefaultUpdated = scheduleDescUpdated.Default.(func() time.Time) + // schedule.UpdateDefaultUpdated holds the default value on update for the updated field. + schedule.UpdateDefaultUpdated = scheduleDescUpdated.UpdateDefault.(func() time.Time) + // scheduleDescID is the schema descriptor for id field. + scheduleDescID := scheduleFields[0].Descriptor() + // schedule.DefaultID holds the default value on creation for the id field. + schedule.DefaultID = scheduleDescID.Default.(func() uuid.UUID) + scheduledeventFields := schema.ScheduledEvent{}.Fields() + _ = scheduledeventFields + // scheduledeventDescEventType is the schema descriptor for event_type field. + scheduledeventDescEventType := scheduledeventFields[2].Descriptor() + // scheduledevent.EventTypeValidator is a validator for the "event_type" field. It is called by the builders before save. + scheduledevent.EventTypeValidator = scheduledeventDescEventType.Validators[0].(func(string) error) + // scheduledeventDescPayload is the schema descriptor for payload field. + scheduledeventDescPayload := scheduledeventFields[4].Descriptor() + // scheduledevent.PayloadValidator is a validator for the "payload" field. It is called by the builders before save. + scheduledevent.PayloadValidator = scheduledeventDescPayload.Validators[0].(func(string) error) + // scheduledeventDescStatus is the schema descriptor for status field. + scheduledeventDescStatus := scheduledeventFields[5].Descriptor() + // scheduledevent.DefaultStatus holds the default value on creation for the status field. + scheduledevent.DefaultStatus = scheduledeventDescStatus.Default.(string) + // scheduledeventDescCreated is the schema descriptor for created field. + scheduledeventDescCreated := scheduledeventFields[10].Descriptor() + // scheduledevent.DefaultCreated holds the default value on creation for the created field. + scheduledevent.DefaultCreated = scheduledeventDescCreated.Default.(func() time.Time) + // scheduledeventDescID is the schema descriptor for id field. + scheduledeventDescID := scheduledeventFields[0].Descriptor() + // scheduledevent.DefaultID holds the default value on creation for the id field. + scheduledevent.DefaultID = scheduledeventDescID.Default.(func() uuid.UUID) + secretFields := schema.Secret{}.Fields() + _ = secretFields + // secretDescKey is the schema descriptor for key field. + secretDescKey := secretFields[1].Descriptor() + // secret.KeyValidator is a validator for the "key" field. It is called by the builders before save. + secret.KeyValidator = secretDescKey.Validators[0].(func(string) error) + // secretDescScope is the schema descriptor for scope field. + secretDescScope := secretFields[6].Descriptor() + // secret.ScopeValidator is a validator for the "scope" field. It is called by the builders before save. + secret.ScopeValidator = secretDescScope.Validators[0].(func(string) error) + // secretDescAllowProgeny is the schema descriptor for allow_progeny field. + secretDescAllowProgeny := secretFields[10].Descriptor() + // secret.DefaultAllowProgeny holds the default value on creation for the allow_progeny field. + secret.DefaultAllowProgeny = secretDescAllowProgeny.Default.(bool) + // secretDescVersion is the schema descriptor for version field. + secretDescVersion := secretFields[11].Descriptor() + // secret.DefaultVersion holds the default value on creation for the version field. + secret.DefaultVersion = secretDescVersion.Default.(int) + // secretDescCreated is the schema descriptor for created field. + secretDescCreated := secretFields[14].Descriptor() + // secret.DefaultCreated holds the default value on creation for the created field. + secret.DefaultCreated = secretDescCreated.Default.(func() time.Time) + // secretDescUpdated is the schema descriptor for updated field. + secretDescUpdated := secretFields[15].Descriptor() + // secret.DefaultUpdated holds the default value on creation for the updated field. + secret.DefaultUpdated = secretDescUpdated.Default.(func() time.Time) + // secret.UpdateDefaultUpdated holds the default value on update for the updated field. + secret.UpdateDefaultUpdated = secretDescUpdated.UpdateDefault.(func() time.Time) + // secretDescID is the schema descriptor for id field. + secretDescID := secretFields[0].Descriptor() + // secret.DefaultID holds the default value on creation for the id field. + secret.DefaultID = secretDescID.Default.(func() uuid.UUID) + subscriptiontemplateFields := schema.SubscriptionTemplate{}.Fields() + _ = subscriptiontemplateFields + // subscriptiontemplateDescName is the schema descriptor for name field. + subscriptiontemplateDescName := subscriptiontemplateFields[1].Descriptor() + // subscriptiontemplate.NameValidator is a validator for the "name" field. It is called by the builders before save. + subscriptiontemplate.NameValidator = subscriptiontemplateDescName.Validators[0].(func(string) error) + // subscriptiontemplateDescScope is the schema descriptor for scope field. + subscriptiontemplateDescScope := subscriptiontemplateFields[2].Descriptor() + // subscriptiontemplate.DefaultScope holds the default value on creation for the scope field. + subscriptiontemplate.DefaultScope = subscriptiontemplateDescScope.Default.(string) + // subscriptiontemplateDescTriggerActivities is the schema descriptor for trigger_activities field. + subscriptiontemplateDescTriggerActivities := subscriptiontemplateFields[3].Descriptor() + // subscriptiontemplate.TriggerActivitiesValidator is a validator for the "trigger_activities" field. It is called by the builders before save. + subscriptiontemplate.TriggerActivitiesValidator = subscriptiontemplateDescTriggerActivities.Validators[0].(func(string) error) + // subscriptiontemplateDescCreatedBy is the schema descriptor for created_by field. + subscriptiontemplateDescCreatedBy := subscriptiontemplateFields[5].Descriptor() + // subscriptiontemplate.CreatedByValidator is a validator for the "created_by" field. It is called by the builders before save. + subscriptiontemplate.CreatedByValidator = subscriptiontemplateDescCreatedBy.Validators[0].(func(string) error) + // subscriptiontemplateDescID is the schema descriptor for id field. + subscriptiontemplateDescID := subscriptiontemplateFields[0].Descriptor() + // subscriptiontemplate.DefaultID holds the default value on creation for the id field. + subscriptiontemplate.DefaultID = subscriptiontemplateDescID.Default.(func() uuid.UUID) + templateFields := schema.Template{}.Fields() + _ = templateFields + // templateDescName is the schema descriptor for name field. + templateDescName := templateFields[1].Descriptor() + // template.NameValidator is a validator for the "name" field. It is called by the builders before save. + template.NameValidator = templateDescName.Validators[0].(func(string) error) + // templateDescSlug is the schema descriptor for slug field. + templateDescSlug := templateFields[2].Descriptor() + // template.SlugValidator is a validator for the "slug" field. It is called by the builders before save. + template.SlugValidator = templateDescSlug.Validators[0].(func(string) error) + // templateDescScope is the schema descriptor for scope field. + templateDescScope := templateFields[10].Descriptor() + // template.DefaultScope holds the default value on creation for the scope field. + template.DefaultScope = templateDescScope.Default.(string) + // templateDescVisibility is the schema descriptor for visibility field. + templateDescVisibility := templateFields[22].Descriptor() + // template.DefaultVisibility holds the default value on creation for the visibility field. + template.DefaultVisibility = templateDescVisibility.Default.(string) + // templateDescCreated is the schema descriptor for created field. + templateDescCreated := templateFields[23].Descriptor() + // template.DefaultCreated holds the default value on creation for the created field. + template.DefaultCreated = templateDescCreated.Default.(func() time.Time) + // templateDescUpdated is the schema descriptor for updated field. + templateDescUpdated := templateFields[24].Descriptor() + // template.DefaultUpdated holds the default value on creation for the updated field. + template.DefaultUpdated = templateDescUpdated.Default.(func() time.Time) + // template.UpdateDefaultUpdated holds the default value on update for the updated field. + template.UpdateDefaultUpdated = templateDescUpdated.UpdateDefault.(func() time.Time) + // templateDescID is the schema descriptor for id field. + templateDescID := templateFields[0].Descriptor() + // template.DefaultID holds the default value on creation for the id field. + template.DefaultID = templateDescID.Default.(func() uuid.UUID) userFields := schema.User{}.Fields() _ = userFields // userDescEmail is the schema descriptor for email field. userDescEmail := userFields[1].Descriptor() // user.EmailValidator is a validator for the "email" field. It is called by the builders before save. user.EmailValidator = userDescEmail.Validators[0].(func(string) error) - // userDescDisplayName is the schema descriptor for display_name field. - userDescDisplayName := userFields[2].Descriptor() - // user.DisplayNameValidator is a validator for the "display_name" field. It is called by the builders before save. - user.DisplayNameValidator = userDescDisplayName.Validators[0].(func(string) error) // userDescCreated is the schema descriptor for created field. userDescCreated := userFields[7].Descriptor() // user.DefaultCreated holds the default value on creation for the created field. @@ -170,4 +888,34 @@ func init() { userDescID := userFields[0].Descriptor() // user.DefaultID holds the default value on creation for the id field. user.DefaultID = userDescID.Default.(func() uuid.UUID) + useraccesstokenFields := schema.UserAccessToken{}.Fields() + _ = useraccesstokenFields + // useraccesstokenDescName is the schema descriptor for name field. + useraccesstokenDescName := useraccesstokenFields[2].Descriptor() + // useraccesstoken.NameValidator is a validator for the "name" field. It is called by the builders before save. + useraccesstoken.NameValidator = useraccesstokenDescName.Validators[0].(func(string) error) + // useraccesstokenDescPrefix is the schema descriptor for prefix field. + useraccesstokenDescPrefix := useraccesstokenFields[3].Descriptor() + // useraccesstoken.PrefixValidator is a validator for the "prefix" field. It is called by the builders before save. + useraccesstoken.PrefixValidator = useraccesstokenDescPrefix.Validators[0].(func(string) error) + // useraccesstokenDescKeyHash is the schema descriptor for key_hash field. + useraccesstokenDescKeyHash := useraccesstokenFields[4].Descriptor() + // useraccesstoken.KeyHashValidator is a validator for the "key_hash" field. It is called by the builders before save. + useraccesstoken.KeyHashValidator = useraccesstokenDescKeyHash.Validators[0].(func(string) error) + // useraccesstokenDescScopes is the schema descriptor for scopes field. + useraccesstokenDescScopes := useraccesstokenFields[6].Descriptor() + // useraccesstoken.ScopesValidator is a validator for the "scopes" field. It is called by the builders before save. + useraccesstoken.ScopesValidator = useraccesstokenDescScopes.Validators[0].(func(string) error) + // useraccesstokenDescRevoked is the schema descriptor for revoked field. + useraccesstokenDescRevoked := useraccesstokenFields[7].Descriptor() + // useraccesstoken.DefaultRevoked holds the default value on creation for the revoked field. + useraccesstoken.DefaultRevoked = useraccesstokenDescRevoked.Default.(bool) + // useraccesstokenDescCreated is the schema descriptor for created field. + useraccesstokenDescCreated := useraccesstokenFields[10].Descriptor() + // useraccesstoken.DefaultCreated holds the default value on creation for the created field. + useraccesstoken.DefaultCreated = useraccesstokenDescCreated.Default.(func() time.Time) + // useraccesstokenDescID is the schema descriptor for id field. + useraccesstokenDescID := useraccesstokenFields[0].Descriptor() + // useraccesstoken.DefaultID holds the default value on creation for the id field. + useraccesstoken.DefaultID = useraccesstokenDescID.Default.(func() uuid.UUID) } diff --git a/pkg/ent/runtimebroker.go b/pkg/ent/runtimebroker.go new file mode 100644 index 000000000..41a41a795 --- /dev/null +++ b/pkg/ent/runtimebroker.go @@ -0,0 +1,365 @@ +// Code generated by ent, DO NOT EDIT. + +package ent + +import ( + "fmt" + "strings" + "time" + + "entgo.io/ent" + "entgo.io/ent/dialect/sql" + "github.com/GoogleCloudPlatform/scion/pkg/ent/runtimebroker" + "github.com/google/uuid" +) + +// RuntimeBroker is the model entity for the RuntimeBroker schema. +type RuntimeBroker struct { + config `json:"-"` + // ID of the ent. + ID uuid.UUID `json:"id,omitempty"` + // Name holds the value of the "name" field. + Name string `json:"name,omitempty"` + // Slug holds the value of the "slug" field. + Slug string `json:"slug,omitempty"` + // Type holds the value of the "type" field. + Type string `json:"type,omitempty"` + // Mode holds the value of the "mode" field. + Mode string `json:"mode,omitempty"` + // Version holds the value of the "version" field. + Version string `json:"version,omitempty"` + // LockVersion holds the value of the "lock_version" field. + LockVersion int64 `json:"lock_version,omitempty"` + // Status holds the value of the "status" field. + Status string `json:"status,omitempty"` + // ConnectionState holds the value of the "connection_state" field. + ConnectionState string `json:"connection_state,omitempty"` + // LastHeartbeat holds the value of the "last_heartbeat" field. + LastHeartbeat *time.Time `json:"last_heartbeat,omitempty"` + // Capabilities holds the value of the "capabilities" field. + Capabilities string `json:"capabilities,omitempty"` + // SupportedHarnesses holds the value of the "supported_harnesses" field. + SupportedHarnesses string `json:"supported_harnesses,omitempty"` + // Resources holds the value of the "resources" field. + Resources string `json:"resources,omitempty"` + // Runtimes holds the value of the "runtimes" field. + Runtimes string `json:"runtimes,omitempty"` + // Labels holds the value of the "labels" field. + Labels string `json:"labels,omitempty"` + // Annotations holds the value of the "annotations" field. + Annotations string `json:"annotations,omitempty"` + // Endpoint holds the value of the "endpoint" field. + Endpoint string `json:"endpoint,omitempty"` + // CreatedBy holds the value of the "created_by" field. + CreatedBy string `json:"created_by,omitempty"` + // AutoProvide holds the value of the "auto_provide" field. + AutoProvide bool `json:"auto_provide,omitempty"` + // ConnectedHubID holds the value of the "connected_hub_id" field. + ConnectedHubID *string `json:"connected_hub_id,omitempty"` + // ConnectedSessionID holds the value of the "connected_session_id" field. + ConnectedSessionID *string `json:"connected_session_id,omitempty"` + // ConnectedAt holds the value of the "connected_at" field. + ConnectedAt *time.Time `json:"connected_at,omitempty"` + // Created holds the value of the "created" field. + Created time.Time `json:"created,omitempty"` + // Updated holds the value of the "updated" field. + Updated time.Time `json:"updated,omitempty"` + selectValues sql.SelectValues +} + +// scanValues returns the types for scanning values from sql.Rows. +func (*RuntimeBroker) scanValues(columns []string) ([]any, error) { + values := make([]any, len(columns)) + for i := range columns { + switch columns[i] { + case runtimebroker.FieldAutoProvide: + values[i] = new(sql.NullBool) + case runtimebroker.FieldLockVersion: + values[i] = new(sql.NullInt64) + case runtimebroker.FieldName, runtimebroker.FieldSlug, runtimebroker.FieldType, runtimebroker.FieldMode, runtimebroker.FieldVersion, runtimebroker.FieldStatus, runtimebroker.FieldConnectionState, runtimebroker.FieldCapabilities, runtimebroker.FieldSupportedHarnesses, runtimebroker.FieldResources, runtimebroker.FieldRuntimes, runtimebroker.FieldLabels, runtimebroker.FieldAnnotations, runtimebroker.FieldEndpoint, runtimebroker.FieldCreatedBy, runtimebroker.FieldConnectedHubID, runtimebroker.FieldConnectedSessionID: + values[i] = new(sql.NullString) + case runtimebroker.FieldLastHeartbeat, runtimebroker.FieldConnectedAt, runtimebroker.FieldCreated, runtimebroker.FieldUpdated: + values[i] = new(sql.NullTime) + case runtimebroker.FieldID: + values[i] = new(uuid.UUID) + default: + values[i] = new(sql.UnknownType) + } + } + return values, nil +} + +// assignValues assigns the values that were returned from sql.Rows (after scanning) +// to the RuntimeBroker fields. +func (_m *RuntimeBroker) assignValues(columns []string, values []any) error { + if m, n := len(values), len(columns); m < n { + return fmt.Errorf("mismatch number of scan values: %d != %d", m, n) + } + for i := range columns { + switch columns[i] { + case runtimebroker.FieldID: + if value, ok := values[i].(*uuid.UUID); !ok { + return fmt.Errorf("unexpected type %T for field id", values[i]) + } else if value != nil { + _m.ID = *value + } + case runtimebroker.FieldName: + if value, ok := values[i].(*sql.NullString); !ok { + return fmt.Errorf("unexpected type %T for field name", values[i]) + } else if value.Valid { + _m.Name = value.String + } + case runtimebroker.FieldSlug: + if value, ok := values[i].(*sql.NullString); !ok { + return fmt.Errorf("unexpected type %T for field slug", values[i]) + } else if value.Valid { + _m.Slug = value.String + } + case runtimebroker.FieldType: + if value, ok := values[i].(*sql.NullString); !ok { + return fmt.Errorf("unexpected type %T for field type", values[i]) + } else if value.Valid { + _m.Type = value.String + } + case runtimebroker.FieldMode: + if value, ok := values[i].(*sql.NullString); !ok { + return fmt.Errorf("unexpected type %T for field mode", values[i]) + } else if value.Valid { + _m.Mode = value.String + } + case runtimebroker.FieldVersion: + if value, ok := values[i].(*sql.NullString); !ok { + return fmt.Errorf("unexpected type %T for field version", values[i]) + } else if value.Valid { + _m.Version = value.String + } + case runtimebroker.FieldLockVersion: + if value, ok := values[i].(*sql.NullInt64); !ok { + return fmt.Errorf("unexpected type %T for field lock_version", values[i]) + } else if value.Valid { + _m.LockVersion = value.Int64 + } + case runtimebroker.FieldStatus: + if value, ok := values[i].(*sql.NullString); !ok { + return fmt.Errorf("unexpected type %T for field status", values[i]) + } else if value.Valid { + _m.Status = value.String + } + case runtimebroker.FieldConnectionState: + if value, ok := values[i].(*sql.NullString); !ok { + return fmt.Errorf("unexpected type %T for field connection_state", values[i]) + } else if value.Valid { + _m.ConnectionState = value.String + } + case runtimebroker.FieldLastHeartbeat: + if value, ok := values[i].(*sql.NullTime); !ok { + return fmt.Errorf("unexpected type %T for field last_heartbeat", values[i]) + } else if value.Valid { + _m.LastHeartbeat = new(time.Time) + *_m.LastHeartbeat = value.Time + } + case runtimebroker.FieldCapabilities: + if value, ok := values[i].(*sql.NullString); !ok { + return fmt.Errorf("unexpected type %T for field capabilities", values[i]) + } else if value.Valid { + _m.Capabilities = value.String + } + case runtimebroker.FieldSupportedHarnesses: + if value, ok := values[i].(*sql.NullString); !ok { + return fmt.Errorf("unexpected type %T for field supported_harnesses", values[i]) + } else if value.Valid { + _m.SupportedHarnesses = value.String + } + case runtimebroker.FieldResources: + if value, ok := values[i].(*sql.NullString); !ok { + return fmt.Errorf("unexpected type %T for field resources", values[i]) + } else if value.Valid { + _m.Resources = value.String + } + case runtimebroker.FieldRuntimes: + if value, ok := values[i].(*sql.NullString); !ok { + return fmt.Errorf("unexpected type %T for field runtimes", values[i]) + } else if value.Valid { + _m.Runtimes = value.String + } + case runtimebroker.FieldLabels: + if value, ok := values[i].(*sql.NullString); !ok { + return fmt.Errorf("unexpected type %T for field labels", values[i]) + } else if value.Valid { + _m.Labels = value.String + } + case runtimebroker.FieldAnnotations: + if value, ok := values[i].(*sql.NullString); !ok { + return fmt.Errorf("unexpected type %T for field annotations", values[i]) + } else if value.Valid { + _m.Annotations = value.String + } + case runtimebroker.FieldEndpoint: + if value, ok := values[i].(*sql.NullString); !ok { + return fmt.Errorf("unexpected type %T for field endpoint", values[i]) + } else if value.Valid { + _m.Endpoint = value.String + } + case runtimebroker.FieldCreatedBy: + if value, ok := values[i].(*sql.NullString); !ok { + return fmt.Errorf("unexpected type %T for field created_by", values[i]) + } else if value.Valid { + _m.CreatedBy = value.String + } + case runtimebroker.FieldAutoProvide: + if value, ok := values[i].(*sql.NullBool); !ok { + return fmt.Errorf("unexpected type %T for field auto_provide", values[i]) + } else if value.Valid { + _m.AutoProvide = value.Bool + } + case runtimebroker.FieldConnectedHubID: + if value, ok := values[i].(*sql.NullString); !ok { + return fmt.Errorf("unexpected type %T for field connected_hub_id", values[i]) + } else if value.Valid { + _m.ConnectedHubID = new(string) + *_m.ConnectedHubID = value.String + } + case runtimebroker.FieldConnectedSessionID: + if value, ok := values[i].(*sql.NullString); !ok { + return fmt.Errorf("unexpected type %T for field connected_session_id", values[i]) + } else if value.Valid { + _m.ConnectedSessionID = new(string) + *_m.ConnectedSessionID = value.String + } + case runtimebroker.FieldConnectedAt: + if value, ok := values[i].(*sql.NullTime); !ok { + return fmt.Errorf("unexpected type %T for field connected_at", values[i]) + } else if value.Valid { + _m.ConnectedAt = new(time.Time) + *_m.ConnectedAt = value.Time + } + case runtimebroker.FieldCreated: + if value, ok := values[i].(*sql.NullTime); !ok { + return fmt.Errorf("unexpected type %T for field created", values[i]) + } else if value.Valid { + _m.Created = value.Time + } + case runtimebroker.FieldUpdated: + if value, ok := values[i].(*sql.NullTime); !ok { + return fmt.Errorf("unexpected type %T for field updated", values[i]) + } else if value.Valid { + _m.Updated = value.Time + } + default: + _m.selectValues.Set(columns[i], values[i]) + } + } + return nil +} + +// Value returns the ent.Value that was dynamically selected and assigned to the RuntimeBroker. +// This includes values selected through modifiers, order, etc. +func (_m *RuntimeBroker) Value(name string) (ent.Value, error) { + return _m.selectValues.Get(name) +} + +// Update returns a builder for updating this RuntimeBroker. +// Note that you need to call RuntimeBroker.Unwrap() before calling this method if this RuntimeBroker +// was returned from a transaction, and the transaction was committed or rolled back. +func (_m *RuntimeBroker) Update() *RuntimeBrokerUpdateOne { + return NewRuntimeBrokerClient(_m.config).UpdateOne(_m) +} + +// Unwrap unwraps the RuntimeBroker entity that was returned from a transaction after it was closed, +// so that all future queries will be executed through the driver which created the transaction. +func (_m *RuntimeBroker) Unwrap() *RuntimeBroker { + _tx, ok := _m.config.driver.(*txDriver) + if !ok { + panic("ent: RuntimeBroker is not a transactional entity") + } + _m.config.driver = _tx.drv + return _m +} + +// String implements the fmt.Stringer. +func (_m *RuntimeBroker) String() string { + var builder strings.Builder + builder.WriteString("RuntimeBroker(") + builder.WriteString(fmt.Sprintf("id=%v, ", _m.ID)) + builder.WriteString("name=") + builder.WriteString(_m.Name) + builder.WriteString(", ") + builder.WriteString("slug=") + builder.WriteString(_m.Slug) + builder.WriteString(", ") + builder.WriteString("type=") + builder.WriteString(_m.Type) + builder.WriteString(", ") + builder.WriteString("mode=") + builder.WriteString(_m.Mode) + builder.WriteString(", ") + builder.WriteString("version=") + builder.WriteString(_m.Version) + builder.WriteString(", ") + builder.WriteString("lock_version=") + builder.WriteString(fmt.Sprintf("%v", _m.LockVersion)) + builder.WriteString(", ") + builder.WriteString("status=") + builder.WriteString(_m.Status) + builder.WriteString(", ") + builder.WriteString("connection_state=") + builder.WriteString(_m.ConnectionState) + builder.WriteString(", ") + if v := _m.LastHeartbeat; v != nil { + builder.WriteString("last_heartbeat=") + builder.WriteString(v.Format(time.ANSIC)) + } + builder.WriteString(", ") + builder.WriteString("capabilities=") + builder.WriteString(_m.Capabilities) + builder.WriteString(", ") + builder.WriteString("supported_harnesses=") + builder.WriteString(_m.SupportedHarnesses) + builder.WriteString(", ") + builder.WriteString("resources=") + builder.WriteString(_m.Resources) + builder.WriteString(", ") + builder.WriteString("runtimes=") + builder.WriteString(_m.Runtimes) + builder.WriteString(", ") + builder.WriteString("labels=") + builder.WriteString(_m.Labels) + builder.WriteString(", ") + builder.WriteString("annotations=") + builder.WriteString(_m.Annotations) + builder.WriteString(", ") + builder.WriteString("endpoint=") + builder.WriteString(_m.Endpoint) + builder.WriteString(", ") + builder.WriteString("created_by=") + builder.WriteString(_m.CreatedBy) + builder.WriteString(", ") + builder.WriteString("auto_provide=") + builder.WriteString(fmt.Sprintf("%v", _m.AutoProvide)) + builder.WriteString(", ") + if v := _m.ConnectedHubID; v != nil { + builder.WriteString("connected_hub_id=") + builder.WriteString(*v) + } + builder.WriteString(", ") + if v := _m.ConnectedSessionID; v != nil { + builder.WriteString("connected_session_id=") + builder.WriteString(*v) + } + builder.WriteString(", ") + if v := _m.ConnectedAt; v != nil { + builder.WriteString("connected_at=") + builder.WriteString(v.Format(time.ANSIC)) + } + builder.WriteString(", ") + builder.WriteString("created=") + builder.WriteString(_m.Created.Format(time.ANSIC)) + builder.WriteString(", ") + builder.WriteString("updated=") + builder.WriteString(_m.Updated.Format(time.ANSIC)) + builder.WriteByte(')') + return builder.String() +} + +// RuntimeBrokers is a parsable slice of RuntimeBroker. +type RuntimeBrokers []*RuntimeBroker diff --git a/pkg/ent/runtimebroker/runtimebroker.go b/pkg/ent/runtimebroker/runtimebroker.go new file mode 100644 index 000000000..eae7dc3ff --- /dev/null +++ b/pkg/ent/runtimebroker/runtimebroker.go @@ -0,0 +1,251 @@ +// Code generated by ent, DO NOT EDIT. + +package runtimebroker + +import ( + "time" + + "entgo.io/ent/dialect/sql" + "github.com/google/uuid" +) + +const ( + // Label holds the string label denoting the runtimebroker type in the database. + Label = "runtime_broker" + // FieldID holds the string denoting the id field in the database. + FieldID = "id" + // FieldName holds the string denoting the name field in the database. + FieldName = "name" + // FieldSlug holds the string denoting the slug field in the database. + FieldSlug = "slug" + // FieldType holds the string denoting the type field in the database. + FieldType = "type" + // FieldMode holds the string denoting the mode field in the database. + FieldMode = "mode" + // FieldVersion holds the string denoting the version field in the database. + FieldVersion = "version" + // FieldLockVersion holds the string denoting the lock_version field in the database. + FieldLockVersion = "lock_version" + // FieldStatus holds the string denoting the status field in the database. + FieldStatus = "status" + // FieldConnectionState holds the string denoting the connection_state field in the database. + FieldConnectionState = "connection_state" + // FieldLastHeartbeat holds the string denoting the last_heartbeat field in the database. + FieldLastHeartbeat = "last_heartbeat" + // FieldCapabilities holds the string denoting the capabilities field in the database. + FieldCapabilities = "capabilities" + // FieldSupportedHarnesses holds the string denoting the supported_harnesses field in the database. + FieldSupportedHarnesses = "supported_harnesses" + // FieldResources holds the string denoting the resources field in the database. + FieldResources = "resources" + // FieldRuntimes holds the string denoting the runtimes field in the database. + FieldRuntimes = "runtimes" + // FieldLabels holds the string denoting the labels field in the database. + FieldLabels = "labels" + // FieldAnnotations holds the string denoting the annotations field in the database. + FieldAnnotations = "annotations" + // FieldEndpoint holds the string denoting the endpoint field in the database. + FieldEndpoint = "endpoint" + // FieldCreatedBy holds the string denoting the created_by field in the database. + FieldCreatedBy = "created_by" + // FieldAutoProvide holds the string denoting the auto_provide field in the database. + FieldAutoProvide = "auto_provide" + // FieldConnectedHubID holds the string denoting the connected_hub_id field in the database. + FieldConnectedHubID = "connected_hub_id" + // FieldConnectedSessionID holds the string denoting the connected_session_id field in the database. + FieldConnectedSessionID = "connected_session_id" + // FieldConnectedAt holds the string denoting the connected_at field in the database. + FieldConnectedAt = "connected_at" + // FieldCreated holds the string denoting the created field in the database. + FieldCreated = "created" + // FieldUpdated holds the string denoting the updated field in the database. + FieldUpdated = "updated" + // Table holds the table name of the runtimebroker in the database. + Table = "runtime_brokers" +) + +// Columns holds all SQL columns for runtimebroker fields. +var Columns = []string{ + FieldID, + FieldName, + FieldSlug, + FieldType, + FieldMode, + FieldVersion, + FieldLockVersion, + FieldStatus, + FieldConnectionState, + FieldLastHeartbeat, + FieldCapabilities, + FieldSupportedHarnesses, + FieldResources, + FieldRuntimes, + FieldLabels, + FieldAnnotations, + FieldEndpoint, + FieldCreatedBy, + FieldAutoProvide, + FieldConnectedHubID, + FieldConnectedSessionID, + FieldConnectedAt, + FieldCreated, + FieldUpdated, +} + +// ValidColumn reports if the column name is valid (part of the table columns). +func ValidColumn(column string) bool { + for i := range Columns { + if column == Columns[i] { + return true + } + } + return false +} + +var ( + // NameValidator is a validator for the "name" field. It is called by the builders before save. + NameValidator func(string) error + // SlugValidator is a validator for the "slug" field. It is called by the builders before save. + SlugValidator func(string) error + // DefaultMode holds the default value on creation for the "mode" field. + DefaultMode string + // DefaultLockVersion holds the default value on creation for the "lock_version" field. + DefaultLockVersion int64 + // DefaultStatus holds the default value on creation for the "status" field. + DefaultStatus string + // DefaultConnectionState holds the default value on creation for the "connection_state" field. + DefaultConnectionState string + // DefaultAutoProvide holds the default value on creation for the "auto_provide" field. + DefaultAutoProvide bool + // DefaultCreated holds the default value on creation for the "created" field. + DefaultCreated func() time.Time + // DefaultUpdated holds the default value on creation for the "updated" field. + DefaultUpdated func() time.Time + // UpdateDefaultUpdated holds the default value on update for the "updated" field. + UpdateDefaultUpdated func() time.Time + // DefaultID holds the default value on creation for the "id" field. + DefaultID func() uuid.UUID +) + +// OrderOption defines the ordering options for the RuntimeBroker queries. +type OrderOption func(*sql.Selector) + +// ByID orders the results by the id field. +func ByID(opts ...sql.OrderTermOption) OrderOption { + return sql.OrderByField(FieldID, opts...).ToFunc() +} + +// ByName orders the results by the name field. +func ByName(opts ...sql.OrderTermOption) OrderOption { + return sql.OrderByField(FieldName, opts...).ToFunc() +} + +// BySlug orders the results by the slug field. +func BySlug(opts ...sql.OrderTermOption) OrderOption { + return sql.OrderByField(FieldSlug, opts...).ToFunc() +} + +// ByType orders the results by the type field. +func ByType(opts ...sql.OrderTermOption) OrderOption { + return sql.OrderByField(FieldType, opts...).ToFunc() +} + +// ByMode orders the results by the mode field. +func ByMode(opts ...sql.OrderTermOption) OrderOption { + return sql.OrderByField(FieldMode, opts...).ToFunc() +} + +// ByVersion orders the results by the version field. +func ByVersion(opts ...sql.OrderTermOption) OrderOption { + return sql.OrderByField(FieldVersion, opts...).ToFunc() +} + +// ByLockVersion orders the results by the lock_version field. +func ByLockVersion(opts ...sql.OrderTermOption) OrderOption { + return sql.OrderByField(FieldLockVersion, opts...).ToFunc() +} + +// ByStatus orders the results by the status field. +func ByStatus(opts ...sql.OrderTermOption) OrderOption { + return sql.OrderByField(FieldStatus, opts...).ToFunc() +} + +// ByConnectionState orders the results by the connection_state field. +func ByConnectionState(opts ...sql.OrderTermOption) OrderOption { + return sql.OrderByField(FieldConnectionState, opts...).ToFunc() +} + +// ByLastHeartbeat orders the results by the last_heartbeat field. +func ByLastHeartbeat(opts ...sql.OrderTermOption) OrderOption { + return sql.OrderByField(FieldLastHeartbeat, opts...).ToFunc() +} + +// ByCapabilities orders the results by the capabilities field. +func ByCapabilities(opts ...sql.OrderTermOption) OrderOption { + return sql.OrderByField(FieldCapabilities, opts...).ToFunc() +} + +// BySupportedHarnesses orders the results by the supported_harnesses field. +func BySupportedHarnesses(opts ...sql.OrderTermOption) OrderOption { + return sql.OrderByField(FieldSupportedHarnesses, opts...).ToFunc() +} + +// ByResources orders the results by the resources field. +func ByResources(opts ...sql.OrderTermOption) OrderOption { + return sql.OrderByField(FieldResources, opts...).ToFunc() +} + +// ByRuntimes orders the results by the runtimes field. +func ByRuntimes(opts ...sql.OrderTermOption) OrderOption { + return sql.OrderByField(FieldRuntimes, opts...).ToFunc() +} + +// ByLabels orders the results by the labels field. +func ByLabels(opts ...sql.OrderTermOption) OrderOption { + return sql.OrderByField(FieldLabels, opts...).ToFunc() +} + +// ByAnnotations orders the results by the annotations field. +func ByAnnotations(opts ...sql.OrderTermOption) OrderOption { + return sql.OrderByField(FieldAnnotations, opts...).ToFunc() +} + +// ByEndpoint orders the results by the endpoint field. +func ByEndpoint(opts ...sql.OrderTermOption) OrderOption { + return sql.OrderByField(FieldEndpoint, opts...).ToFunc() +} + +// ByCreatedBy orders the results by the created_by field. +func ByCreatedBy(opts ...sql.OrderTermOption) OrderOption { + return sql.OrderByField(FieldCreatedBy, opts...).ToFunc() +} + +// ByAutoProvide orders the results by the auto_provide field. +func ByAutoProvide(opts ...sql.OrderTermOption) OrderOption { + return sql.OrderByField(FieldAutoProvide, opts...).ToFunc() +} + +// ByConnectedHubID orders the results by the connected_hub_id field. +func ByConnectedHubID(opts ...sql.OrderTermOption) OrderOption { + return sql.OrderByField(FieldConnectedHubID, opts...).ToFunc() +} + +// ByConnectedSessionID orders the results by the connected_session_id field. +func ByConnectedSessionID(opts ...sql.OrderTermOption) OrderOption { + return sql.OrderByField(FieldConnectedSessionID, opts...).ToFunc() +} + +// ByConnectedAt orders the results by the connected_at field. +func ByConnectedAt(opts ...sql.OrderTermOption) OrderOption { + return sql.OrderByField(FieldConnectedAt, opts...).ToFunc() +} + +// ByCreated orders the results by the created field. +func ByCreated(opts ...sql.OrderTermOption) OrderOption { + return sql.OrderByField(FieldCreated, opts...).ToFunc() +} + +// ByUpdated orders the results by the updated field. +func ByUpdated(opts ...sql.OrderTermOption) OrderOption { + return sql.OrderByField(FieldUpdated, opts...).ToFunc() +} diff --git a/pkg/ent/runtimebroker/where.go b/pkg/ent/runtimebroker/where.go new file mode 100644 index 000000000..f81468889 --- /dev/null +++ b/pkg/ent/runtimebroker/where.go @@ -0,0 +1,1641 @@ +// Code generated by ent, DO NOT EDIT. + +package runtimebroker + +import ( + "time" + + "entgo.io/ent/dialect/sql" + "github.com/GoogleCloudPlatform/scion/pkg/ent/predicate" + "github.com/google/uuid" +) + +// ID filters vertices based on their ID field. +func ID(id uuid.UUID) predicate.RuntimeBroker { + return predicate.RuntimeBroker(sql.FieldEQ(FieldID, id)) +} + +// IDEQ applies the EQ predicate on the ID field. +func IDEQ(id uuid.UUID) predicate.RuntimeBroker { + return predicate.RuntimeBroker(sql.FieldEQ(FieldID, id)) +} + +// IDNEQ applies the NEQ predicate on the ID field. +func IDNEQ(id uuid.UUID) predicate.RuntimeBroker { + return predicate.RuntimeBroker(sql.FieldNEQ(FieldID, id)) +} + +// IDIn applies the In predicate on the ID field. +func IDIn(ids ...uuid.UUID) predicate.RuntimeBroker { + return predicate.RuntimeBroker(sql.FieldIn(FieldID, ids...)) +} + +// IDNotIn applies the NotIn predicate on the ID field. +func IDNotIn(ids ...uuid.UUID) predicate.RuntimeBroker { + return predicate.RuntimeBroker(sql.FieldNotIn(FieldID, ids...)) +} + +// IDGT applies the GT predicate on the ID field. +func IDGT(id uuid.UUID) predicate.RuntimeBroker { + return predicate.RuntimeBroker(sql.FieldGT(FieldID, id)) +} + +// IDGTE applies the GTE predicate on the ID field. +func IDGTE(id uuid.UUID) predicate.RuntimeBroker { + return predicate.RuntimeBroker(sql.FieldGTE(FieldID, id)) +} + +// IDLT applies the LT predicate on the ID field. +func IDLT(id uuid.UUID) predicate.RuntimeBroker { + return predicate.RuntimeBroker(sql.FieldLT(FieldID, id)) +} + +// IDLTE applies the LTE predicate on the ID field. +func IDLTE(id uuid.UUID) predicate.RuntimeBroker { + return predicate.RuntimeBroker(sql.FieldLTE(FieldID, id)) +} + +// Name applies equality check predicate on the "name" field. It's identical to NameEQ. +func Name(v string) predicate.RuntimeBroker { + return predicate.RuntimeBroker(sql.FieldEQ(FieldName, v)) +} + +// Slug applies equality check predicate on the "slug" field. It's identical to SlugEQ. +func Slug(v string) predicate.RuntimeBroker { + return predicate.RuntimeBroker(sql.FieldEQ(FieldSlug, v)) +} + +// Type applies equality check predicate on the "type" field. It's identical to TypeEQ. +func Type(v string) predicate.RuntimeBroker { + return predicate.RuntimeBroker(sql.FieldEQ(FieldType, v)) +} + +// Mode applies equality check predicate on the "mode" field. It's identical to ModeEQ. +func Mode(v string) predicate.RuntimeBroker { + return predicate.RuntimeBroker(sql.FieldEQ(FieldMode, v)) +} + +// Version applies equality check predicate on the "version" field. It's identical to VersionEQ. +func Version(v string) predicate.RuntimeBroker { + return predicate.RuntimeBroker(sql.FieldEQ(FieldVersion, v)) +} + +// LockVersion applies equality check predicate on the "lock_version" field. It's identical to LockVersionEQ. +func LockVersion(v int64) predicate.RuntimeBroker { + return predicate.RuntimeBroker(sql.FieldEQ(FieldLockVersion, v)) +} + +// Status applies equality check predicate on the "status" field. It's identical to StatusEQ. +func Status(v string) predicate.RuntimeBroker { + return predicate.RuntimeBroker(sql.FieldEQ(FieldStatus, v)) +} + +// ConnectionState applies equality check predicate on the "connection_state" field. It's identical to ConnectionStateEQ. +func ConnectionState(v string) predicate.RuntimeBroker { + return predicate.RuntimeBroker(sql.FieldEQ(FieldConnectionState, v)) +} + +// LastHeartbeat applies equality check predicate on the "last_heartbeat" field. It's identical to LastHeartbeatEQ. +func LastHeartbeat(v time.Time) predicate.RuntimeBroker { + return predicate.RuntimeBroker(sql.FieldEQ(FieldLastHeartbeat, v)) +} + +// Capabilities applies equality check predicate on the "capabilities" field. It's identical to CapabilitiesEQ. +func Capabilities(v string) predicate.RuntimeBroker { + return predicate.RuntimeBroker(sql.FieldEQ(FieldCapabilities, v)) +} + +// SupportedHarnesses applies equality check predicate on the "supported_harnesses" field. It's identical to SupportedHarnessesEQ. +func SupportedHarnesses(v string) predicate.RuntimeBroker { + return predicate.RuntimeBroker(sql.FieldEQ(FieldSupportedHarnesses, v)) +} + +// Resources applies equality check predicate on the "resources" field. It's identical to ResourcesEQ. +func Resources(v string) predicate.RuntimeBroker { + return predicate.RuntimeBroker(sql.FieldEQ(FieldResources, v)) +} + +// Runtimes applies equality check predicate on the "runtimes" field. It's identical to RuntimesEQ. +func Runtimes(v string) predicate.RuntimeBroker { + return predicate.RuntimeBroker(sql.FieldEQ(FieldRuntimes, v)) +} + +// Labels applies equality check predicate on the "labels" field. It's identical to LabelsEQ. +func Labels(v string) predicate.RuntimeBroker { + return predicate.RuntimeBroker(sql.FieldEQ(FieldLabels, v)) +} + +// Annotations applies equality check predicate on the "annotations" field. It's identical to AnnotationsEQ. +func Annotations(v string) predicate.RuntimeBroker { + return predicate.RuntimeBroker(sql.FieldEQ(FieldAnnotations, v)) +} + +// Endpoint applies equality check predicate on the "endpoint" field. It's identical to EndpointEQ. +func Endpoint(v string) predicate.RuntimeBroker { + return predicate.RuntimeBroker(sql.FieldEQ(FieldEndpoint, v)) +} + +// CreatedBy applies equality check predicate on the "created_by" field. It's identical to CreatedByEQ. +func CreatedBy(v string) predicate.RuntimeBroker { + return predicate.RuntimeBroker(sql.FieldEQ(FieldCreatedBy, v)) +} + +// AutoProvide applies equality check predicate on the "auto_provide" field. It's identical to AutoProvideEQ. +func AutoProvide(v bool) predicate.RuntimeBroker { + return predicate.RuntimeBroker(sql.FieldEQ(FieldAutoProvide, v)) +} + +// ConnectedHubID applies equality check predicate on the "connected_hub_id" field. It's identical to ConnectedHubIDEQ. +func ConnectedHubID(v string) predicate.RuntimeBroker { + return predicate.RuntimeBroker(sql.FieldEQ(FieldConnectedHubID, v)) +} + +// ConnectedSessionID applies equality check predicate on the "connected_session_id" field. It's identical to ConnectedSessionIDEQ. +func ConnectedSessionID(v string) predicate.RuntimeBroker { + return predicate.RuntimeBroker(sql.FieldEQ(FieldConnectedSessionID, v)) +} + +// ConnectedAt applies equality check predicate on the "connected_at" field. It's identical to ConnectedAtEQ. +func ConnectedAt(v time.Time) predicate.RuntimeBroker { + return predicate.RuntimeBroker(sql.FieldEQ(FieldConnectedAt, v)) +} + +// Created applies equality check predicate on the "created" field. It's identical to CreatedEQ. +func Created(v time.Time) predicate.RuntimeBroker { + return predicate.RuntimeBroker(sql.FieldEQ(FieldCreated, v)) +} + +// Updated applies equality check predicate on the "updated" field. It's identical to UpdatedEQ. +func Updated(v time.Time) predicate.RuntimeBroker { + return predicate.RuntimeBroker(sql.FieldEQ(FieldUpdated, v)) +} + +// NameEQ applies the EQ predicate on the "name" field. +func NameEQ(v string) predicate.RuntimeBroker { + return predicate.RuntimeBroker(sql.FieldEQ(FieldName, v)) +} + +// NameNEQ applies the NEQ predicate on the "name" field. +func NameNEQ(v string) predicate.RuntimeBroker { + return predicate.RuntimeBroker(sql.FieldNEQ(FieldName, v)) +} + +// NameIn applies the In predicate on the "name" field. +func NameIn(vs ...string) predicate.RuntimeBroker { + return predicate.RuntimeBroker(sql.FieldIn(FieldName, vs...)) +} + +// NameNotIn applies the NotIn predicate on the "name" field. +func NameNotIn(vs ...string) predicate.RuntimeBroker { + return predicate.RuntimeBroker(sql.FieldNotIn(FieldName, vs...)) +} + +// NameGT applies the GT predicate on the "name" field. +func NameGT(v string) predicate.RuntimeBroker { + return predicate.RuntimeBroker(sql.FieldGT(FieldName, v)) +} + +// NameGTE applies the GTE predicate on the "name" field. +func NameGTE(v string) predicate.RuntimeBroker { + return predicate.RuntimeBroker(sql.FieldGTE(FieldName, v)) +} + +// NameLT applies the LT predicate on the "name" field. +func NameLT(v string) predicate.RuntimeBroker { + return predicate.RuntimeBroker(sql.FieldLT(FieldName, v)) +} + +// NameLTE applies the LTE predicate on the "name" field. +func NameLTE(v string) predicate.RuntimeBroker { + return predicate.RuntimeBroker(sql.FieldLTE(FieldName, v)) +} + +// NameContains applies the Contains predicate on the "name" field. +func NameContains(v string) predicate.RuntimeBroker { + return predicate.RuntimeBroker(sql.FieldContains(FieldName, v)) +} + +// NameHasPrefix applies the HasPrefix predicate on the "name" field. +func NameHasPrefix(v string) predicate.RuntimeBroker { + return predicate.RuntimeBroker(sql.FieldHasPrefix(FieldName, v)) +} + +// NameHasSuffix applies the HasSuffix predicate on the "name" field. +func NameHasSuffix(v string) predicate.RuntimeBroker { + return predicate.RuntimeBroker(sql.FieldHasSuffix(FieldName, v)) +} + +// NameEqualFold applies the EqualFold predicate on the "name" field. +func NameEqualFold(v string) predicate.RuntimeBroker { + return predicate.RuntimeBroker(sql.FieldEqualFold(FieldName, v)) +} + +// NameContainsFold applies the ContainsFold predicate on the "name" field. +func NameContainsFold(v string) predicate.RuntimeBroker { + return predicate.RuntimeBroker(sql.FieldContainsFold(FieldName, v)) +} + +// SlugEQ applies the EQ predicate on the "slug" field. +func SlugEQ(v string) predicate.RuntimeBroker { + return predicate.RuntimeBroker(sql.FieldEQ(FieldSlug, v)) +} + +// SlugNEQ applies the NEQ predicate on the "slug" field. +func SlugNEQ(v string) predicate.RuntimeBroker { + return predicate.RuntimeBroker(sql.FieldNEQ(FieldSlug, v)) +} + +// SlugIn applies the In predicate on the "slug" field. +func SlugIn(vs ...string) predicate.RuntimeBroker { + return predicate.RuntimeBroker(sql.FieldIn(FieldSlug, vs...)) +} + +// SlugNotIn applies the NotIn predicate on the "slug" field. +func SlugNotIn(vs ...string) predicate.RuntimeBroker { + return predicate.RuntimeBroker(sql.FieldNotIn(FieldSlug, vs...)) +} + +// SlugGT applies the GT predicate on the "slug" field. +func SlugGT(v string) predicate.RuntimeBroker { + return predicate.RuntimeBroker(sql.FieldGT(FieldSlug, v)) +} + +// SlugGTE applies the GTE predicate on the "slug" field. +func SlugGTE(v string) predicate.RuntimeBroker { + return predicate.RuntimeBroker(sql.FieldGTE(FieldSlug, v)) +} + +// SlugLT applies the LT predicate on the "slug" field. +func SlugLT(v string) predicate.RuntimeBroker { + return predicate.RuntimeBroker(sql.FieldLT(FieldSlug, v)) +} + +// SlugLTE applies the LTE predicate on the "slug" field. +func SlugLTE(v string) predicate.RuntimeBroker { + return predicate.RuntimeBroker(sql.FieldLTE(FieldSlug, v)) +} + +// SlugContains applies the Contains predicate on the "slug" field. +func SlugContains(v string) predicate.RuntimeBroker { + return predicate.RuntimeBroker(sql.FieldContains(FieldSlug, v)) +} + +// SlugHasPrefix applies the HasPrefix predicate on the "slug" field. +func SlugHasPrefix(v string) predicate.RuntimeBroker { + return predicate.RuntimeBroker(sql.FieldHasPrefix(FieldSlug, v)) +} + +// SlugHasSuffix applies the HasSuffix predicate on the "slug" field. +func SlugHasSuffix(v string) predicate.RuntimeBroker { + return predicate.RuntimeBroker(sql.FieldHasSuffix(FieldSlug, v)) +} + +// SlugEqualFold applies the EqualFold predicate on the "slug" field. +func SlugEqualFold(v string) predicate.RuntimeBroker { + return predicate.RuntimeBroker(sql.FieldEqualFold(FieldSlug, v)) +} + +// SlugContainsFold applies the ContainsFold predicate on the "slug" field. +func SlugContainsFold(v string) predicate.RuntimeBroker { + return predicate.RuntimeBroker(sql.FieldContainsFold(FieldSlug, v)) +} + +// TypeEQ applies the EQ predicate on the "type" field. +func TypeEQ(v string) predicate.RuntimeBroker { + return predicate.RuntimeBroker(sql.FieldEQ(FieldType, v)) +} + +// TypeNEQ applies the NEQ predicate on the "type" field. +func TypeNEQ(v string) predicate.RuntimeBroker { + return predicate.RuntimeBroker(sql.FieldNEQ(FieldType, v)) +} + +// TypeIn applies the In predicate on the "type" field. +func TypeIn(vs ...string) predicate.RuntimeBroker { + return predicate.RuntimeBroker(sql.FieldIn(FieldType, vs...)) +} + +// TypeNotIn applies the NotIn predicate on the "type" field. +func TypeNotIn(vs ...string) predicate.RuntimeBroker { + return predicate.RuntimeBroker(sql.FieldNotIn(FieldType, vs...)) +} + +// TypeGT applies the GT predicate on the "type" field. +func TypeGT(v string) predicate.RuntimeBroker { + return predicate.RuntimeBroker(sql.FieldGT(FieldType, v)) +} + +// TypeGTE applies the GTE predicate on the "type" field. +func TypeGTE(v string) predicate.RuntimeBroker { + return predicate.RuntimeBroker(sql.FieldGTE(FieldType, v)) +} + +// TypeLT applies the LT predicate on the "type" field. +func TypeLT(v string) predicate.RuntimeBroker { + return predicate.RuntimeBroker(sql.FieldLT(FieldType, v)) +} + +// TypeLTE applies the LTE predicate on the "type" field. +func TypeLTE(v string) predicate.RuntimeBroker { + return predicate.RuntimeBroker(sql.FieldLTE(FieldType, v)) +} + +// TypeContains applies the Contains predicate on the "type" field. +func TypeContains(v string) predicate.RuntimeBroker { + return predicate.RuntimeBroker(sql.FieldContains(FieldType, v)) +} + +// TypeHasPrefix applies the HasPrefix predicate on the "type" field. +func TypeHasPrefix(v string) predicate.RuntimeBroker { + return predicate.RuntimeBroker(sql.FieldHasPrefix(FieldType, v)) +} + +// TypeHasSuffix applies the HasSuffix predicate on the "type" field. +func TypeHasSuffix(v string) predicate.RuntimeBroker { + return predicate.RuntimeBroker(sql.FieldHasSuffix(FieldType, v)) +} + +// TypeIsNil applies the IsNil predicate on the "type" field. +func TypeIsNil() predicate.RuntimeBroker { + return predicate.RuntimeBroker(sql.FieldIsNull(FieldType)) +} + +// TypeNotNil applies the NotNil predicate on the "type" field. +func TypeNotNil() predicate.RuntimeBroker { + return predicate.RuntimeBroker(sql.FieldNotNull(FieldType)) +} + +// TypeEqualFold applies the EqualFold predicate on the "type" field. +func TypeEqualFold(v string) predicate.RuntimeBroker { + return predicate.RuntimeBroker(sql.FieldEqualFold(FieldType, v)) +} + +// TypeContainsFold applies the ContainsFold predicate on the "type" field. +func TypeContainsFold(v string) predicate.RuntimeBroker { + return predicate.RuntimeBroker(sql.FieldContainsFold(FieldType, v)) +} + +// ModeEQ applies the EQ predicate on the "mode" field. +func ModeEQ(v string) predicate.RuntimeBroker { + return predicate.RuntimeBroker(sql.FieldEQ(FieldMode, v)) +} + +// ModeNEQ applies the NEQ predicate on the "mode" field. +func ModeNEQ(v string) predicate.RuntimeBroker { + return predicate.RuntimeBroker(sql.FieldNEQ(FieldMode, v)) +} + +// ModeIn applies the In predicate on the "mode" field. +func ModeIn(vs ...string) predicate.RuntimeBroker { + return predicate.RuntimeBroker(sql.FieldIn(FieldMode, vs...)) +} + +// ModeNotIn applies the NotIn predicate on the "mode" field. +func ModeNotIn(vs ...string) predicate.RuntimeBroker { + return predicate.RuntimeBroker(sql.FieldNotIn(FieldMode, vs...)) +} + +// ModeGT applies the GT predicate on the "mode" field. +func ModeGT(v string) predicate.RuntimeBroker { + return predicate.RuntimeBroker(sql.FieldGT(FieldMode, v)) +} + +// ModeGTE applies the GTE predicate on the "mode" field. +func ModeGTE(v string) predicate.RuntimeBroker { + return predicate.RuntimeBroker(sql.FieldGTE(FieldMode, v)) +} + +// ModeLT applies the LT predicate on the "mode" field. +func ModeLT(v string) predicate.RuntimeBroker { + return predicate.RuntimeBroker(sql.FieldLT(FieldMode, v)) +} + +// ModeLTE applies the LTE predicate on the "mode" field. +func ModeLTE(v string) predicate.RuntimeBroker { + return predicate.RuntimeBroker(sql.FieldLTE(FieldMode, v)) +} + +// ModeContains applies the Contains predicate on the "mode" field. +func ModeContains(v string) predicate.RuntimeBroker { + return predicate.RuntimeBroker(sql.FieldContains(FieldMode, v)) +} + +// ModeHasPrefix applies the HasPrefix predicate on the "mode" field. +func ModeHasPrefix(v string) predicate.RuntimeBroker { + return predicate.RuntimeBroker(sql.FieldHasPrefix(FieldMode, v)) +} + +// ModeHasSuffix applies the HasSuffix predicate on the "mode" field. +func ModeHasSuffix(v string) predicate.RuntimeBroker { + return predicate.RuntimeBroker(sql.FieldHasSuffix(FieldMode, v)) +} + +// ModeEqualFold applies the EqualFold predicate on the "mode" field. +func ModeEqualFold(v string) predicate.RuntimeBroker { + return predicate.RuntimeBroker(sql.FieldEqualFold(FieldMode, v)) +} + +// ModeContainsFold applies the ContainsFold predicate on the "mode" field. +func ModeContainsFold(v string) predicate.RuntimeBroker { + return predicate.RuntimeBroker(sql.FieldContainsFold(FieldMode, v)) +} + +// VersionEQ applies the EQ predicate on the "version" field. +func VersionEQ(v string) predicate.RuntimeBroker { + return predicate.RuntimeBroker(sql.FieldEQ(FieldVersion, v)) +} + +// VersionNEQ applies the NEQ predicate on the "version" field. +func VersionNEQ(v string) predicate.RuntimeBroker { + return predicate.RuntimeBroker(sql.FieldNEQ(FieldVersion, v)) +} + +// VersionIn applies the In predicate on the "version" field. +func VersionIn(vs ...string) predicate.RuntimeBroker { + return predicate.RuntimeBroker(sql.FieldIn(FieldVersion, vs...)) +} + +// VersionNotIn applies the NotIn predicate on the "version" field. +func VersionNotIn(vs ...string) predicate.RuntimeBroker { + return predicate.RuntimeBroker(sql.FieldNotIn(FieldVersion, vs...)) +} + +// VersionGT applies the GT predicate on the "version" field. +func VersionGT(v string) predicate.RuntimeBroker { + return predicate.RuntimeBroker(sql.FieldGT(FieldVersion, v)) +} + +// VersionGTE applies the GTE predicate on the "version" field. +func VersionGTE(v string) predicate.RuntimeBroker { + return predicate.RuntimeBroker(sql.FieldGTE(FieldVersion, v)) +} + +// VersionLT applies the LT predicate on the "version" field. +func VersionLT(v string) predicate.RuntimeBroker { + return predicate.RuntimeBroker(sql.FieldLT(FieldVersion, v)) +} + +// VersionLTE applies the LTE predicate on the "version" field. +func VersionLTE(v string) predicate.RuntimeBroker { + return predicate.RuntimeBroker(sql.FieldLTE(FieldVersion, v)) +} + +// VersionContains applies the Contains predicate on the "version" field. +func VersionContains(v string) predicate.RuntimeBroker { + return predicate.RuntimeBroker(sql.FieldContains(FieldVersion, v)) +} + +// VersionHasPrefix applies the HasPrefix predicate on the "version" field. +func VersionHasPrefix(v string) predicate.RuntimeBroker { + return predicate.RuntimeBroker(sql.FieldHasPrefix(FieldVersion, v)) +} + +// VersionHasSuffix applies the HasSuffix predicate on the "version" field. +func VersionHasSuffix(v string) predicate.RuntimeBroker { + return predicate.RuntimeBroker(sql.FieldHasSuffix(FieldVersion, v)) +} + +// VersionIsNil applies the IsNil predicate on the "version" field. +func VersionIsNil() predicate.RuntimeBroker { + return predicate.RuntimeBroker(sql.FieldIsNull(FieldVersion)) +} + +// VersionNotNil applies the NotNil predicate on the "version" field. +func VersionNotNil() predicate.RuntimeBroker { + return predicate.RuntimeBroker(sql.FieldNotNull(FieldVersion)) +} + +// VersionEqualFold applies the EqualFold predicate on the "version" field. +func VersionEqualFold(v string) predicate.RuntimeBroker { + return predicate.RuntimeBroker(sql.FieldEqualFold(FieldVersion, v)) +} + +// VersionContainsFold applies the ContainsFold predicate on the "version" field. +func VersionContainsFold(v string) predicate.RuntimeBroker { + return predicate.RuntimeBroker(sql.FieldContainsFold(FieldVersion, v)) +} + +// LockVersionEQ applies the EQ predicate on the "lock_version" field. +func LockVersionEQ(v int64) predicate.RuntimeBroker { + return predicate.RuntimeBroker(sql.FieldEQ(FieldLockVersion, v)) +} + +// LockVersionNEQ applies the NEQ predicate on the "lock_version" field. +func LockVersionNEQ(v int64) predicate.RuntimeBroker { + return predicate.RuntimeBroker(sql.FieldNEQ(FieldLockVersion, v)) +} + +// LockVersionIn applies the In predicate on the "lock_version" field. +func LockVersionIn(vs ...int64) predicate.RuntimeBroker { + return predicate.RuntimeBroker(sql.FieldIn(FieldLockVersion, vs...)) +} + +// LockVersionNotIn applies the NotIn predicate on the "lock_version" field. +func LockVersionNotIn(vs ...int64) predicate.RuntimeBroker { + return predicate.RuntimeBroker(sql.FieldNotIn(FieldLockVersion, vs...)) +} + +// LockVersionGT applies the GT predicate on the "lock_version" field. +func LockVersionGT(v int64) predicate.RuntimeBroker { + return predicate.RuntimeBroker(sql.FieldGT(FieldLockVersion, v)) +} + +// LockVersionGTE applies the GTE predicate on the "lock_version" field. +func LockVersionGTE(v int64) predicate.RuntimeBroker { + return predicate.RuntimeBroker(sql.FieldGTE(FieldLockVersion, v)) +} + +// LockVersionLT applies the LT predicate on the "lock_version" field. +func LockVersionLT(v int64) predicate.RuntimeBroker { + return predicate.RuntimeBroker(sql.FieldLT(FieldLockVersion, v)) +} + +// LockVersionLTE applies the LTE predicate on the "lock_version" field. +func LockVersionLTE(v int64) predicate.RuntimeBroker { + return predicate.RuntimeBroker(sql.FieldLTE(FieldLockVersion, v)) +} + +// StatusEQ applies the EQ predicate on the "status" field. +func StatusEQ(v string) predicate.RuntimeBroker { + return predicate.RuntimeBroker(sql.FieldEQ(FieldStatus, v)) +} + +// StatusNEQ applies the NEQ predicate on the "status" field. +func StatusNEQ(v string) predicate.RuntimeBroker { + return predicate.RuntimeBroker(sql.FieldNEQ(FieldStatus, v)) +} + +// StatusIn applies the In predicate on the "status" field. +func StatusIn(vs ...string) predicate.RuntimeBroker { + return predicate.RuntimeBroker(sql.FieldIn(FieldStatus, vs...)) +} + +// StatusNotIn applies the NotIn predicate on the "status" field. +func StatusNotIn(vs ...string) predicate.RuntimeBroker { + return predicate.RuntimeBroker(sql.FieldNotIn(FieldStatus, vs...)) +} + +// StatusGT applies the GT predicate on the "status" field. +func StatusGT(v string) predicate.RuntimeBroker { + return predicate.RuntimeBroker(sql.FieldGT(FieldStatus, v)) +} + +// StatusGTE applies the GTE predicate on the "status" field. +func StatusGTE(v string) predicate.RuntimeBroker { + return predicate.RuntimeBroker(sql.FieldGTE(FieldStatus, v)) +} + +// StatusLT applies the LT predicate on the "status" field. +func StatusLT(v string) predicate.RuntimeBroker { + return predicate.RuntimeBroker(sql.FieldLT(FieldStatus, v)) +} + +// StatusLTE applies the LTE predicate on the "status" field. +func StatusLTE(v string) predicate.RuntimeBroker { + return predicate.RuntimeBroker(sql.FieldLTE(FieldStatus, v)) +} + +// StatusContains applies the Contains predicate on the "status" field. +func StatusContains(v string) predicate.RuntimeBroker { + return predicate.RuntimeBroker(sql.FieldContains(FieldStatus, v)) +} + +// StatusHasPrefix applies the HasPrefix predicate on the "status" field. +func StatusHasPrefix(v string) predicate.RuntimeBroker { + return predicate.RuntimeBroker(sql.FieldHasPrefix(FieldStatus, v)) +} + +// StatusHasSuffix applies the HasSuffix predicate on the "status" field. +func StatusHasSuffix(v string) predicate.RuntimeBroker { + return predicate.RuntimeBroker(sql.FieldHasSuffix(FieldStatus, v)) +} + +// StatusEqualFold applies the EqualFold predicate on the "status" field. +func StatusEqualFold(v string) predicate.RuntimeBroker { + return predicate.RuntimeBroker(sql.FieldEqualFold(FieldStatus, v)) +} + +// StatusContainsFold applies the ContainsFold predicate on the "status" field. +func StatusContainsFold(v string) predicate.RuntimeBroker { + return predicate.RuntimeBroker(sql.FieldContainsFold(FieldStatus, v)) +} + +// ConnectionStateEQ applies the EQ predicate on the "connection_state" field. +func ConnectionStateEQ(v string) predicate.RuntimeBroker { + return predicate.RuntimeBroker(sql.FieldEQ(FieldConnectionState, v)) +} + +// ConnectionStateNEQ applies the NEQ predicate on the "connection_state" field. +func ConnectionStateNEQ(v string) predicate.RuntimeBroker { + return predicate.RuntimeBroker(sql.FieldNEQ(FieldConnectionState, v)) +} + +// ConnectionStateIn applies the In predicate on the "connection_state" field. +func ConnectionStateIn(vs ...string) predicate.RuntimeBroker { + return predicate.RuntimeBroker(sql.FieldIn(FieldConnectionState, vs...)) +} + +// ConnectionStateNotIn applies the NotIn predicate on the "connection_state" field. +func ConnectionStateNotIn(vs ...string) predicate.RuntimeBroker { + return predicate.RuntimeBroker(sql.FieldNotIn(FieldConnectionState, vs...)) +} + +// ConnectionStateGT applies the GT predicate on the "connection_state" field. +func ConnectionStateGT(v string) predicate.RuntimeBroker { + return predicate.RuntimeBroker(sql.FieldGT(FieldConnectionState, v)) +} + +// ConnectionStateGTE applies the GTE predicate on the "connection_state" field. +func ConnectionStateGTE(v string) predicate.RuntimeBroker { + return predicate.RuntimeBroker(sql.FieldGTE(FieldConnectionState, v)) +} + +// ConnectionStateLT applies the LT predicate on the "connection_state" field. +func ConnectionStateLT(v string) predicate.RuntimeBroker { + return predicate.RuntimeBroker(sql.FieldLT(FieldConnectionState, v)) +} + +// ConnectionStateLTE applies the LTE predicate on the "connection_state" field. +func ConnectionStateLTE(v string) predicate.RuntimeBroker { + return predicate.RuntimeBroker(sql.FieldLTE(FieldConnectionState, v)) +} + +// ConnectionStateContains applies the Contains predicate on the "connection_state" field. +func ConnectionStateContains(v string) predicate.RuntimeBroker { + return predicate.RuntimeBroker(sql.FieldContains(FieldConnectionState, v)) +} + +// ConnectionStateHasPrefix applies the HasPrefix predicate on the "connection_state" field. +func ConnectionStateHasPrefix(v string) predicate.RuntimeBroker { + return predicate.RuntimeBroker(sql.FieldHasPrefix(FieldConnectionState, v)) +} + +// ConnectionStateHasSuffix applies the HasSuffix predicate on the "connection_state" field. +func ConnectionStateHasSuffix(v string) predicate.RuntimeBroker { + return predicate.RuntimeBroker(sql.FieldHasSuffix(FieldConnectionState, v)) +} + +// ConnectionStateEqualFold applies the EqualFold predicate on the "connection_state" field. +func ConnectionStateEqualFold(v string) predicate.RuntimeBroker { + return predicate.RuntimeBroker(sql.FieldEqualFold(FieldConnectionState, v)) +} + +// ConnectionStateContainsFold applies the ContainsFold predicate on the "connection_state" field. +func ConnectionStateContainsFold(v string) predicate.RuntimeBroker { + return predicate.RuntimeBroker(sql.FieldContainsFold(FieldConnectionState, v)) +} + +// LastHeartbeatEQ applies the EQ predicate on the "last_heartbeat" field. +func LastHeartbeatEQ(v time.Time) predicate.RuntimeBroker { + return predicate.RuntimeBroker(sql.FieldEQ(FieldLastHeartbeat, v)) +} + +// LastHeartbeatNEQ applies the NEQ predicate on the "last_heartbeat" field. +func LastHeartbeatNEQ(v time.Time) predicate.RuntimeBroker { + return predicate.RuntimeBroker(sql.FieldNEQ(FieldLastHeartbeat, v)) +} + +// LastHeartbeatIn applies the In predicate on the "last_heartbeat" field. +func LastHeartbeatIn(vs ...time.Time) predicate.RuntimeBroker { + return predicate.RuntimeBroker(sql.FieldIn(FieldLastHeartbeat, vs...)) +} + +// LastHeartbeatNotIn applies the NotIn predicate on the "last_heartbeat" field. +func LastHeartbeatNotIn(vs ...time.Time) predicate.RuntimeBroker { + return predicate.RuntimeBroker(sql.FieldNotIn(FieldLastHeartbeat, vs...)) +} + +// LastHeartbeatGT applies the GT predicate on the "last_heartbeat" field. +func LastHeartbeatGT(v time.Time) predicate.RuntimeBroker { + return predicate.RuntimeBroker(sql.FieldGT(FieldLastHeartbeat, v)) +} + +// LastHeartbeatGTE applies the GTE predicate on the "last_heartbeat" field. +func LastHeartbeatGTE(v time.Time) predicate.RuntimeBroker { + return predicate.RuntimeBroker(sql.FieldGTE(FieldLastHeartbeat, v)) +} + +// LastHeartbeatLT applies the LT predicate on the "last_heartbeat" field. +func LastHeartbeatLT(v time.Time) predicate.RuntimeBroker { + return predicate.RuntimeBroker(sql.FieldLT(FieldLastHeartbeat, v)) +} + +// LastHeartbeatLTE applies the LTE predicate on the "last_heartbeat" field. +func LastHeartbeatLTE(v time.Time) predicate.RuntimeBroker { + return predicate.RuntimeBroker(sql.FieldLTE(FieldLastHeartbeat, v)) +} + +// LastHeartbeatIsNil applies the IsNil predicate on the "last_heartbeat" field. +func LastHeartbeatIsNil() predicate.RuntimeBroker { + return predicate.RuntimeBroker(sql.FieldIsNull(FieldLastHeartbeat)) +} + +// LastHeartbeatNotNil applies the NotNil predicate on the "last_heartbeat" field. +func LastHeartbeatNotNil() predicate.RuntimeBroker { + return predicate.RuntimeBroker(sql.FieldNotNull(FieldLastHeartbeat)) +} + +// CapabilitiesEQ applies the EQ predicate on the "capabilities" field. +func CapabilitiesEQ(v string) predicate.RuntimeBroker { + return predicate.RuntimeBroker(sql.FieldEQ(FieldCapabilities, v)) +} + +// CapabilitiesNEQ applies the NEQ predicate on the "capabilities" field. +func CapabilitiesNEQ(v string) predicate.RuntimeBroker { + return predicate.RuntimeBroker(sql.FieldNEQ(FieldCapabilities, v)) +} + +// CapabilitiesIn applies the In predicate on the "capabilities" field. +func CapabilitiesIn(vs ...string) predicate.RuntimeBroker { + return predicate.RuntimeBroker(sql.FieldIn(FieldCapabilities, vs...)) +} + +// CapabilitiesNotIn applies the NotIn predicate on the "capabilities" field. +func CapabilitiesNotIn(vs ...string) predicate.RuntimeBroker { + return predicate.RuntimeBroker(sql.FieldNotIn(FieldCapabilities, vs...)) +} + +// CapabilitiesGT applies the GT predicate on the "capabilities" field. +func CapabilitiesGT(v string) predicate.RuntimeBroker { + return predicate.RuntimeBroker(sql.FieldGT(FieldCapabilities, v)) +} + +// CapabilitiesGTE applies the GTE predicate on the "capabilities" field. +func CapabilitiesGTE(v string) predicate.RuntimeBroker { + return predicate.RuntimeBroker(sql.FieldGTE(FieldCapabilities, v)) +} + +// CapabilitiesLT applies the LT predicate on the "capabilities" field. +func CapabilitiesLT(v string) predicate.RuntimeBroker { + return predicate.RuntimeBroker(sql.FieldLT(FieldCapabilities, v)) +} + +// CapabilitiesLTE applies the LTE predicate on the "capabilities" field. +func CapabilitiesLTE(v string) predicate.RuntimeBroker { + return predicate.RuntimeBroker(sql.FieldLTE(FieldCapabilities, v)) +} + +// CapabilitiesContains applies the Contains predicate on the "capabilities" field. +func CapabilitiesContains(v string) predicate.RuntimeBroker { + return predicate.RuntimeBroker(sql.FieldContains(FieldCapabilities, v)) +} + +// CapabilitiesHasPrefix applies the HasPrefix predicate on the "capabilities" field. +func CapabilitiesHasPrefix(v string) predicate.RuntimeBroker { + return predicate.RuntimeBroker(sql.FieldHasPrefix(FieldCapabilities, v)) +} + +// CapabilitiesHasSuffix applies the HasSuffix predicate on the "capabilities" field. +func CapabilitiesHasSuffix(v string) predicate.RuntimeBroker { + return predicate.RuntimeBroker(sql.FieldHasSuffix(FieldCapabilities, v)) +} + +// CapabilitiesIsNil applies the IsNil predicate on the "capabilities" field. +func CapabilitiesIsNil() predicate.RuntimeBroker { + return predicate.RuntimeBroker(sql.FieldIsNull(FieldCapabilities)) +} + +// CapabilitiesNotNil applies the NotNil predicate on the "capabilities" field. +func CapabilitiesNotNil() predicate.RuntimeBroker { + return predicate.RuntimeBroker(sql.FieldNotNull(FieldCapabilities)) +} + +// CapabilitiesEqualFold applies the EqualFold predicate on the "capabilities" field. +func CapabilitiesEqualFold(v string) predicate.RuntimeBroker { + return predicate.RuntimeBroker(sql.FieldEqualFold(FieldCapabilities, v)) +} + +// CapabilitiesContainsFold applies the ContainsFold predicate on the "capabilities" field. +func CapabilitiesContainsFold(v string) predicate.RuntimeBroker { + return predicate.RuntimeBroker(sql.FieldContainsFold(FieldCapabilities, v)) +} + +// SupportedHarnessesEQ applies the EQ predicate on the "supported_harnesses" field. +func SupportedHarnessesEQ(v string) predicate.RuntimeBroker { + return predicate.RuntimeBroker(sql.FieldEQ(FieldSupportedHarnesses, v)) +} + +// SupportedHarnessesNEQ applies the NEQ predicate on the "supported_harnesses" field. +func SupportedHarnessesNEQ(v string) predicate.RuntimeBroker { + return predicate.RuntimeBroker(sql.FieldNEQ(FieldSupportedHarnesses, v)) +} + +// SupportedHarnessesIn applies the In predicate on the "supported_harnesses" field. +func SupportedHarnessesIn(vs ...string) predicate.RuntimeBroker { + return predicate.RuntimeBroker(sql.FieldIn(FieldSupportedHarnesses, vs...)) +} + +// SupportedHarnessesNotIn applies the NotIn predicate on the "supported_harnesses" field. +func SupportedHarnessesNotIn(vs ...string) predicate.RuntimeBroker { + return predicate.RuntimeBroker(sql.FieldNotIn(FieldSupportedHarnesses, vs...)) +} + +// SupportedHarnessesGT applies the GT predicate on the "supported_harnesses" field. +func SupportedHarnessesGT(v string) predicate.RuntimeBroker { + return predicate.RuntimeBroker(sql.FieldGT(FieldSupportedHarnesses, v)) +} + +// SupportedHarnessesGTE applies the GTE predicate on the "supported_harnesses" field. +func SupportedHarnessesGTE(v string) predicate.RuntimeBroker { + return predicate.RuntimeBroker(sql.FieldGTE(FieldSupportedHarnesses, v)) +} + +// SupportedHarnessesLT applies the LT predicate on the "supported_harnesses" field. +func SupportedHarnessesLT(v string) predicate.RuntimeBroker { + return predicate.RuntimeBroker(sql.FieldLT(FieldSupportedHarnesses, v)) +} + +// SupportedHarnessesLTE applies the LTE predicate on the "supported_harnesses" field. +func SupportedHarnessesLTE(v string) predicate.RuntimeBroker { + return predicate.RuntimeBroker(sql.FieldLTE(FieldSupportedHarnesses, v)) +} + +// SupportedHarnessesContains applies the Contains predicate on the "supported_harnesses" field. +func SupportedHarnessesContains(v string) predicate.RuntimeBroker { + return predicate.RuntimeBroker(sql.FieldContains(FieldSupportedHarnesses, v)) +} + +// SupportedHarnessesHasPrefix applies the HasPrefix predicate on the "supported_harnesses" field. +func SupportedHarnessesHasPrefix(v string) predicate.RuntimeBroker { + return predicate.RuntimeBroker(sql.FieldHasPrefix(FieldSupportedHarnesses, v)) +} + +// SupportedHarnessesHasSuffix applies the HasSuffix predicate on the "supported_harnesses" field. +func SupportedHarnessesHasSuffix(v string) predicate.RuntimeBroker { + return predicate.RuntimeBroker(sql.FieldHasSuffix(FieldSupportedHarnesses, v)) +} + +// SupportedHarnessesIsNil applies the IsNil predicate on the "supported_harnesses" field. +func SupportedHarnessesIsNil() predicate.RuntimeBroker { + return predicate.RuntimeBroker(sql.FieldIsNull(FieldSupportedHarnesses)) +} + +// SupportedHarnessesNotNil applies the NotNil predicate on the "supported_harnesses" field. +func SupportedHarnessesNotNil() predicate.RuntimeBroker { + return predicate.RuntimeBroker(sql.FieldNotNull(FieldSupportedHarnesses)) +} + +// SupportedHarnessesEqualFold applies the EqualFold predicate on the "supported_harnesses" field. +func SupportedHarnessesEqualFold(v string) predicate.RuntimeBroker { + return predicate.RuntimeBroker(sql.FieldEqualFold(FieldSupportedHarnesses, v)) +} + +// SupportedHarnessesContainsFold applies the ContainsFold predicate on the "supported_harnesses" field. +func SupportedHarnessesContainsFold(v string) predicate.RuntimeBroker { + return predicate.RuntimeBroker(sql.FieldContainsFold(FieldSupportedHarnesses, v)) +} + +// ResourcesEQ applies the EQ predicate on the "resources" field. +func ResourcesEQ(v string) predicate.RuntimeBroker { + return predicate.RuntimeBroker(sql.FieldEQ(FieldResources, v)) +} + +// ResourcesNEQ applies the NEQ predicate on the "resources" field. +func ResourcesNEQ(v string) predicate.RuntimeBroker { + return predicate.RuntimeBroker(sql.FieldNEQ(FieldResources, v)) +} + +// ResourcesIn applies the In predicate on the "resources" field. +func ResourcesIn(vs ...string) predicate.RuntimeBroker { + return predicate.RuntimeBroker(sql.FieldIn(FieldResources, vs...)) +} + +// ResourcesNotIn applies the NotIn predicate on the "resources" field. +func ResourcesNotIn(vs ...string) predicate.RuntimeBroker { + return predicate.RuntimeBroker(sql.FieldNotIn(FieldResources, vs...)) +} + +// ResourcesGT applies the GT predicate on the "resources" field. +func ResourcesGT(v string) predicate.RuntimeBroker { + return predicate.RuntimeBroker(sql.FieldGT(FieldResources, v)) +} + +// ResourcesGTE applies the GTE predicate on the "resources" field. +func ResourcesGTE(v string) predicate.RuntimeBroker { + return predicate.RuntimeBroker(sql.FieldGTE(FieldResources, v)) +} + +// ResourcesLT applies the LT predicate on the "resources" field. +func ResourcesLT(v string) predicate.RuntimeBroker { + return predicate.RuntimeBroker(sql.FieldLT(FieldResources, v)) +} + +// ResourcesLTE applies the LTE predicate on the "resources" field. +func ResourcesLTE(v string) predicate.RuntimeBroker { + return predicate.RuntimeBroker(sql.FieldLTE(FieldResources, v)) +} + +// ResourcesContains applies the Contains predicate on the "resources" field. +func ResourcesContains(v string) predicate.RuntimeBroker { + return predicate.RuntimeBroker(sql.FieldContains(FieldResources, v)) +} + +// ResourcesHasPrefix applies the HasPrefix predicate on the "resources" field. +func ResourcesHasPrefix(v string) predicate.RuntimeBroker { + return predicate.RuntimeBroker(sql.FieldHasPrefix(FieldResources, v)) +} + +// ResourcesHasSuffix applies the HasSuffix predicate on the "resources" field. +func ResourcesHasSuffix(v string) predicate.RuntimeBroker { + return predicate.RuntimeBroker(sql.FieldHasSuffix(FieldResources, v)) +} + +// ResourcesIsNil applies the IsNil predicate on the "resources" field. +func ResourcesIsNil() predicate.RuntimeBroker { + return predicate.RuntimeBroker(sql.FieldIsNull(FieldResources)) +} + +// ResourcesNotNil applies the NotNil predicate on the "resources" field. +func ResourcesNotNil() predicate.RuntimeBroker { + return predicate.RuntimeBroker(sql.FieldNotNull(FieldResources)) +} + +// ResourcesEqualFold applies the EqualFold predicate on the "resources" field. +func ResourcesEqualFold(v string) predicate.RuntimeBroker { + return predicate.RuntimeBroker(sql.FieldEqualFold(FieldResources, v)) +} + +// ResourcesContainsFold applies the ContainsFold predicate on the "resources" field. +func ResourcesContainsFold(v string) predicate.RuntimeBroker { + return predicate.RuntimeBroker(sql.FieldContainsFold(FieldResources, v)) +} + +// RuntimesEQ applies the EQ predicate on the "runtimes" field. +func RuntimesEQ(v string) predicate.RuntimeBroker { + return predicate.RuntimeBroker(sql.FieldEQ(FieldRuntimes, v)) +} + +// RuntimesNEQ applies the NEQ predicate on the "runtimes" field. +func RuntimesNEQ(v string) predicate.RuntimeBroker { + return predicate.RuntimeBroker(sql.FieldNEQ(FieldRuntimes, v)) +} + +// RuntimesIn applies the In predicate on the "runtimes" field. +func RuntimesIn(vs ...string) predicate.RuntimeBroker { + return predicate.RuntimeBroker(sql.FieldIn(FieldRuntimes, vs...)) +} + +// RuntimesNotIn applies the NotIn predicate on the "runtimes" field. +func RuntimesNotIn(vs ...string) predicate.RuntimeBroker { + return predicate.RuntimeBroker(sql.FieldNotIn(FieldRuntimes, vs...)) +} + +// RuntimesGT applies the GT predicate on the "runtimes" field. +func RuntimesGT(v string) predicate.RuntimeBroker { + return predicate.RuntimeBroker(sql.FieldGT(FieldRuntimes, v)) +} + +// RuntimesGTE applies the GTE predicate on the "runtimes" field. +func RuntimesGTE(v string) predicate.RuntimeBroker { + return predicate.RuntimeBroker(sql.FieldGTE(FieldRuntimes, v)) +} + +// RuntimesLT applies the LT predicate on the "runtimes" field. +func RuntimesLT(v string) predicate.RuntimeBroker { + return predicate.RuntimeBroker(sql.FieldLT(FieldRuntimes, v)) +} + +// RuntimesLTE applies the LTE predicate on the "runtimes" field. +func RuntimesLTE(v string) predicate.RuntimeBroker { + return predicate.RuntimeBroker(sql.FieldLTE(FieldRuntimes, v)) +} + +// RuntimesContains applies the Contains predicate on the "runtimes" field. +func RuntimesContains(v string) predicate.RuntimeBroker { + return predicate.RuntimeBroker(sql.FieldContains(FieldRuntimes, v)) +} + +// RuntimesHasPrefix applies the HasPrefix predicate on the "runtimes" field. +func RuntimesHasPrefix(v string) predicate.RuntimeBroker { + return predicate.RuntimeBroker(sql.FieldHasPrefix(FieldRuntimes, v)) +} + +// RuntimesHasSuffix applies the HasSuffix predicate on the "runtimes" field. +func RuntimesHasSuffix(v string) predicate.RuntimeBroker { + return predicate.RuntimeBroker(sql.FieldHasSuffix(FieldRuntimes, v)) +} + +// RuntimesIsNil applies the IsNil predicate on the "runtimes" field. +func RuntimesIsNil() predicate.RuntimeBroker { + return predicate.RuntimeBroker(sql.FieldIsNull(FieldRuntimes)) +} + +// RuntimesNotNil applies the NotNil predicate on the "runtimes" field. +func RuntimesNotNil() predicate.RuntimeBroker { + return predicate.RuntimeBroker(sql.FieldNotNull(FieldRuntimes)) +} + +// RuntimesEqualFold applies the EqualFold predicate on the "runtimes" field. +func RuntimesEqualFold(v string) predicate.RuntimeBroker { + return predicate.RuntimeBroker(sql.FieldEqualFold(FieldRuntimes, v)) +} + +// RuntimesContainsFold applies the ContainsFold predicate on the "runtimes" field. +func RuntimesContainsFold(v string) predicate.RuntimeBroker { + return predicate.RuntimeBroker(sql.FieldContainsFold(FieldRuntimes, v)) +} + +// LabelsEQ applies the EQ predicate on the "labels" field. +func LabelsEQ(v string) predicate.RuntimeBroker { + return predicate.RuntimeBroker(sql.FieldEQ(FieldLabels, v)) +} + +// LabelsNEQ applies the NEQ predicate on the "labels" field. +func LabelsNEQ(v string) predicate.RuntimeBroker { + return predicate.RuntimeBroker(sql.FieldNEQ(FieldLabels, v)) +} + +// LabelsIn applies the In predicate on the "labels" field. +func LabelsIn(vs ...string) predicate.RuntimeBroker { + return predicate.RuntimeBroker(sql.FieldIn(FieldLabels, vs...)) +} + +// LabelsNotIn applies the NotIn predicate on the "labels" field. +func LabelsNotIn(vs ...string) predicate.RuntimeBroker { + return predicate.RuntimeBroker(sql.FieldNotIn(FieldLabels, vs...)) +} + +// LabelsGT applies the GT predicate on the "labels" field. +func LabelsGT(v string) predicate.RuntimeBroker { + return predicate.RuntimeBroker(sql.FieldGT(FieldLabels, v)) +} + +// LabelsGTE applies the GTE predicate on the "labels" field. +func LabelsGTE(v string) predicate.RuntimeBroker { + return predicate.RuntimeBroker(sql.FieldGTE(FieldLabels, v)) +} + +// LabelsLT applies the LT predicate on the "labels" field. +func LabelsLT(v string) predicate.RuntimeBroker { + return predicate.RuntimeBroker(sql.FieldLT(FieldLabels, v)) +} + +// LabelsLTE applies the LTE predicate on the "labels" field. +func LabelsLTE(v string) predicate.RuntimeBroker { + return predicate.RuntimeBroker(sql.FieldLTE(FieldLabels, v)) +} + +// LabelsContains applies the Contains predicate on the "labels" field. +func LabelsContains(v string) predicate.RuntimeBroker { + return predicate.RuntimeBroker(sql.FieldContains(FieldLabels, v)) +} + +// LabelsHasPrefix applies the HasPrefix predicate on the "labels" field. +func LabelsHasPrefix(v string) predicate.RuntimeBroker { + return predicate.RuntimeBroker(sql.FieldHasPrefix(FieldLabels, v)) +} + +// LabelsHasSuffix applies the HasSuffix predicate on the "labels" field. +func LabelsHasSuffix(v string) predicate.RuntimeBroker { + return predicate.RuntimeBroker(sql.FieldHasSuffix(FieldLabels, v)) +} + +// LabelsIsNil applies the IsNil predicate on the "labels" field. +func LabelsIsNil() predicate.RuntimeBroker { + return predicate.RuntimeBroker(sql.FieldIsNull(FieldLabels)) +} + +// LabelsNotNil applies the NotNil predicate on the "labels" field. +func LabelsNotNil() predicate.RuntimeBroker { + return predicate.RuntimeBroker(sql.FieldNotNull(FieldLabels)) +} + +// LabelsEqualFold applies the EqualFold predicate on the "labels" field. +func LabelsEqualFold(v string) predicate.RuntimeBroker { + return predicate.RuntimeBroker(sql.FieldEqualFold(FieldLabels, v)) +} + +// LabelsContainsFold applies the ContainsFold predicate on the "labels" field. +func LabelsContainsFold(v string) predicate.RuntimeBroker { + return predicate.RuntimeBroker(sql.FieldContainsFold(FieldLabels, v)) +} + +// AnnotationsEQ applies the EQ predicate on the "annotations" field. +func AnnotationsEQ(v string) predicate.RuntimeBroker { + return predicate.RuntimeBroker(sql.FieldEQ(FieldAnnotations, v)) +} + +// AnnotationsNEQ applies the NEQ predicate on the "annotations" field. +func AnnotationsNEQ(v string) predicate.RuntimeBroker { + return predicate.RuntimeBroker(sql.FieldNEQ(FieldAnnotations, v)) +} + +// AnnotationsIn applies the In predicate on the "annotations" field. +func AnnotationsIn(vs ...string) predicate.RuntimeBroker { + return predicate.RuntimeBroker(sql.FieldIn(FieldAnnotations, vs...)) +} + +// AnnotationsNotIn applies the NotIn predicate on the "annotations" field. +func AnnotationsNotIn(vs ...string) predicate.RuntimeBroker { + return predicate.RuntimeBroker(sql.FieldNotIn(FieldAnnotations, vs...)) +} + +// AnnotationsGT applies the GT predicate on the "annotations" field. +func AnnotationsGT(v string) predicate.RuntimeBroker { + return predicate.RuntimeBroker(sql.FieldGT(FieldAnnotations, v)) +} + +// AnnotationsGTE applies the GTE predicate on the "annotations" field. +func AnnotationsGTE(v string) predicate.RuntimeBroker { + return predicate.RuntimeBroker(sql.FieldGTE(FieldAnnotations, v)) +} + +// AnnotationsLT applies the LT predicate on the "annotations" field. +func AnnotationsLT(v string) predicate.RuntimeBroker { + return predicate.RuntimeBroker(sql.FieldLT(FieldAnnotations, v)) +} + +// AnnotationsLTE applies the LTE predicate on the "annotations" field. +func AnnotationsLTE(v string) predicate.RuntimeBroker { + return predicate.RuntimeBroker(sql.FieldLTE(FieldAnnotations, v)) +} + +// AnnotationsContains applies the Contains predicate on the "annotations" field. +func AnnotationsContains(v string) predicate.RuntimeBroker { + return predicate.RuntimeBroker(sql.FieldContains(FieldAnnotations, v)) +} + +// AnnotationsHasPrefix applies the HasPrefix predicate on the "annotations" field. +func AnnotationsHasPrefix(v string) predicate.RuntimeBroker { + return predicate.RuntimeBroker(sql.FieldHasPrefix(FieldAnnotations, v)) +} + +// AnnotationsHasSuffix applies the HasSuffix predicate on the "annotations" field. +func AnnotationsHasSuffix(v string) predicate.RuntimeBroker { + return predicate.RuntimeBroker(sql.FieldHasSuffix(FieldAnnotations, v)) +} + +// AnnotationsIsNil applies the IsNil predicate on the "annotations" field. +func AnnotationsIsNil() predicate.RuntimeBroker { + return predicate.RuntimeBroker(sql.FieldIsNull(FieldAnnotations)) +} + +// AnnotationsNotNil applies the NotNil predicate on the "annotations" field. +func AnnotationsNotNil() predicate.RuntimeBroker { + return predicate.RuntimeBroker(sql.FieldNotNull(FieldAnnotations)) +} + +// AnnotationsEqualFold applies the EqualFold predicate on the "annotations" field. +func AnnotationsEqualFold(v string) predicate.RuntimeBroker { + return predicate.RuntimeBroker(sql.FieldEqualFold(FieldAnnotations, v)) +} + +// AnnotationsContainsFold applies the ContainsFold predicate on the "annotations" field. +func AnnotationsContainsFold(v string) predicate.RuntimeBroker { + return predicate.RuntimeBroker(sql.FieldContainsFold(FieldAnnotations, v)) +} + +// EndpointEQ applies the EQ predicate on the "endpoint" field. +func EndpointEQ(v string) predicate.RuntimeBroker { + return predicate.RuntimeBroker(sql.FieldEQ(FieldEndpoint, v)) +} + +// EndpointNEQ applies the NEQ predicate on the "endpoint" field. +func EndpointNEQ(v string) predicate.RuntimeBroker { + return predicate.RuntimeBroker(sql.FieldNEQ(FieldEndpoint, v)) +} + +// EndpointIn applies the In predicate on the "endpoint" field. +func EndpointIn(vs ...string) predicate.RuntimeBroker { + return predicate.RuntimeBroker(sql.FieldIn(FieldEndpoint, vs...)) +} + +// EndpointNotIn applies the NotIn predicate on the "endpoint" field. +func EndpointNotIn(vs ...string) predicate.RuntimeBroker { + return predicate.RuntimeBroker(sql.FieldNotIn(FieldEndpoint, vs...)) +} + +// EndpointGT applies the GT predicate on the "endpoint" field. +func EndpointGT(v string) predicate.RuntimeBroker { + return predicate.RuntimeBroker(sql.FieldGT(FieldEndpoint, v)) +} + +// EndpointGTE applies the GTE predicate on the "endpoint" field. +func EndpointGTE(v string) predicate.RuntimeBroker { + return predicate.RuntimeBroker(sql.FieldGTE(FieldEndpoint, v)) +} + +// EndpointLT applies the LT predicate on the "endpoint" field. +func EndpointLT(v string) predicate.RuntimeBroker { + return predicate.RuntimeBroker(sql.FieldLT(FieldEndpoint, v)) +} + +// EndpointLTE applies the LTE predicate on the "endpoint" field. +func EndpointLTE(v string) predicate.RuntimeBroker { + return predicate.RuntimeBroker(sql.FieldLTE(FieldEndpoint, v)) +} + +// EndpointContains applies the Contains predicate on the "endpoint" field. +func EndpointContains(v string) predicate.RuntimeBroker { + return predicate.RuntimeBroker(sql.FieldContains(FieldEndpoint, v)) +} + +// EndpointHasPrefix applies the HasPrefix predicate on the "endpoint" field. +func EndpointHasPrefix(v string) predicate.RuntimeBroker { + return predicate.RuntimeBroker(sql.FieldHasPrefix(FieldEndpoint, v)) +} + +// EndpointHasSuffix applies the HasSuffix predicate on the "endpoint" field. +func EndpointHasSuffix(v string) predicate.RuntimeBroker { + return predicate.RuntimeBroker(sql.FieldHasSuffix(FieldEndpoint, v)) +} + +// EndpointIsNil applies the IsNil predicate on the "endpoint" field. +func EndpointIsNil() predicate.RuntimeBroker { + return predicate.RuntimeBroker(sql.FieldIsNull(FieldEndpoint)) +} + +// EndpointNotNil applies the NotNil predicate on the "endpoint" field. +func EndpointNotNil() predicate.RuntimeBroker { + return predicate.RuntimeBroker(sql.FieldNotNull(FieldEndpoint)) +} + +// EndpointEqualFold applies the EqualFold predicate on the "endpoint" field. +func EndpointEqualFold(v string) predicate.RuntimeBroker { + return predicate.RuntimeBroker(sql.FieldEqualFold(FieldEndpoint, v)) +} + +// EndpointContainsFold applies the ContainsFold predicate on the "endpoint" field. +func EndpointContainsFold(v string) predicate.RuntimeBroker { + return predicate.RuntimeBroker(sql.FieldContainsFold(FieldEndpoint, v)) +} + +// CreatedByEQ applies the EQ predicate on the "created_by" field. +func CreatedByEQ(v string) predicate.RuntimeBroker { + return predicate.RuntimeBroker(sql.FieldEQ(FieldCreatedBy, v)) +} + +// CreatedByNEQ applies the NEQ predicate on the "created_by" field. +func CreatedByNEQ(v string) predicate.RuntimeBroker { + return predicate.RuntimeBroker(sql.FieldNEQ(FieldCreatedBy, v)) +} + +// CreatedByIn applies the In predicate on the "created_by" field. +func CreatedByIn(vs ...string) predicate.RuntimeBroker { + return predicate.RuntimeBroker(sql.FieldIn(FieldCreatedBy, vs...)) +} + +// CreatedByNotIn applies the NotIn predicate on the "created_by" field. +func CreatedByNotIn(vs ...string) predicate.RuntimeBroker { + return predicate.RuntimeBroker(sql.FieldNotIn(FieldCreatedBy, vs...)) +} + +// CreatedByGT applies the GT predicate on the "created_by" field. +func CreatedByGT(v string) predicate.RuntimeBroker { + return predicate.RuntimeBroker(sql.FieldGT(FieldCreatedBy, v)) +} + +// CreatedByGTE applies the GTE predicate on the "created_by" field. +func CreatedByGTE(v string) predicate.RuntimeBroker { + return predicate.RuntimeBroker(sql.FieldGTE(FieldCreatedBy, v)) +} + +// CreatedByLT applies the LT predicate on the "created_by" field. +func CreatedByLT(v string) predicate.RuntimeBroker { + return predicate.RuntimeBroker(sql.FieldLT(FieldCreatedBy, v)) +} + +// CreatedByLTE applies the LTE predicate on the "created_by" field. +func CreatedByLTE(v string) predicate.RuntimeBroker { + return predicate.RuntimeBroker(sql.FieldLTE(FieldCreatedBy, v)) +} + +// CreatedByContains applies the Contains predicate on the "created_by" field. +func CreatedByContains(v string) predicate.RuntimeBroker { + return predicate.RuntimeBroker(sql.FieldContains(FieldCreatedBy, v)) +} + +// CreatedByHasPrefix applies the HasPrefix predicate on the "created_by" field. +func CreatedByHasPrefix(v string) predicate.RuntimeBroker { + return predicate.RuntimeBroker(sql.FieldHasPrefix(FieldCreatedBy, v)) +} + +// CreatedByHasSuffix applies the HasSuffix predicate on the "created_by" field. +func CreatedByHasSuffix(v string) predicate.RuntimeBroker { + return predicate.RuntimeBroker(sql.FieldHasSuffix(FieldCreatedBy, v)) +} + +// CreatedByIsNil applies the IsNil predicate on the "created_by" field. +func CreatedByIsNil() predicate.RuntimeBroker { + return predicate.RuntimeBroker(sql.FieldIsNull(FieldCreatedBy)) +} + +// CreatedByNotNil applies the NotNil predicate on the "created_by" field. +func CreatedByNotNil() predicate.RuntimeBroker { + return predicate.RuntimeBroker(sql.FieldNotNull(FieldCreatedBy)) +} + +// CreatedByEqualFold applies the EqualFold predicate on the "created_by" field. +func CreatedByEqualFold(v string) predicate.RuntimeBroker { + return predicate.RuntimeBroker(sql.FieldEqualFold(FieldCreatedBy, v)) +} + +// CreatedByContainsFold applies the ContainsFold predicate on the "created_by" field. +func CreatedByContainsFold(v string) predicate.RuntimeBroker { + return predicate.RuntimeBroker(sql.FieldContainsFold(FieldCreatedBy, v)) +} + +// AutoProvideEQ applies the EQ predicate on the "auto_provide" field. +func AutoProvideEQ(v bool) predicate.RuntimeBroker { + return predicate.RuntimeBroker(sql.FieldEQ(FieldAutoProvide, v)) +} + +// AutoProvideNEQ applies the NEQ predicate on the "auto_provide" field. +func AutoProvideNEQ(v bool) predicate.RuntimeBroker { + return predicate.RuntimeBroker(sql.FieldNEQ(FieldAutoProvide, v)) +} + +// ConnectedHubIDEQ applies the EQ predicate on the "connected_hub_id" field. +func ConnectedHubIDEQ(v string) predicate.RuntimeBroker { + return predicate.RuntimeBroker(sql.FieldEQ(FieldConnectedHubID, v)) +} + +// ConnectedHubIDNEQ applies the NEQ predicate on the "connected_hub_id" field. +func ConnectedHubIDNEQ(v string) predicate.RuntimeBroker { + return predicate.RuntimeBroker(sql.FieldNEQ(FieldConnectedHubID, v)) +} + +// ConnectedHubIDIn applies the In predicate on the "connected_hub_id" field. +func ConnectedHubIDIn(vs ...string) predicate.RuntimeBroker { + return predicate.RuntimeBroker(sql.FieldIn(FieldConnectedHubID, vs...)) +} + +// ConnectedHubIDNotIn applies the NotIn predicate on the "connected_hub_id" field. +func ConnectedHubIDNotIn(vs ...string) predicate.RuntimeBroker { + return predicate.RuntimeBroker(sql.FieldNotIn(FieldConnectedHubID, vs...)) +} + +// ConnectedHubIDGT applies the GT predicate on the "connected_hub_id" field. +func ConnectedHubIDGT(v string) predicate.RuntimeBroker { + return predicate.RuntimeBroker(sql.FieldGT(FieldConnectedHubID, v)) +} + +// ConnectedHubIDGTE applies the GTE predicate on the "connected_hub_id" field. +func ConnectedHubIDGTE(v string) predicate.RuntimeBroker { + return predicate.RuntimeBroker(sql.FieldGTE(FieldConnectedHubID, v)) +} + +// ConnectedHubIDLT applies the LT predicate on the "connected_hub_id" field. +func ConnectedHubIDLT(v string) predicate.RuntimeBroker { + return predicate.RuntimeBroker(sql.FieldLT(FieldConnectedHubID, v)) +} + +// ConnectedHubIDLTE applies the LTE predicate on the "connected_hub_id" field. +func ConnectedHubIDLTE(v string) predicate.RuntimeBroker { + return predicate.RuntimeBroker(sql.FieldLTE(FieldConnectedHubID, v)) +} + +// ConnectedHubIDContains applies the Contains predicate on the "connected_hub_id" field. +func ConnectedHubIDContains(v string) predicate.RuntimeBroker { + return predicate.RuntimeBroker(sql.FieldContains(FieldConnectedHubID, v)) +} + +// ConnectedHubIDHasPrefix applies the HasPrefix predicate on the "connected_hub_id" field. +func ConnectedHubIDHasPrefix(v string) predicate.RuntimeBroker { + return predicate.RuntimeBroker(sql.FieldHasPrefix(FieldConnectedHubID, v)) +} + +// ConnectedHubIDHasSuffix applies the HasSuffix predicate on the "connected_hub_id" field. +func ConnectedHubIDHasSuffix(v string) predicate.RuntimeBroker { + return predicate.RuntimeBroker(sql.FieldHasSuffix(FieldConnectedHubID, v)) +} + +// ConnectedHubIDIsNil applies the IsNil predicate on the "connected_hub_id" field. +func ConnectedHubIDIsNil() predicate.RuntimeBroker { + return predicate.RuntimeBroker(sql.FieldIsNull(FieldConnectedHubID)) +} + +// ConnectedHubIDNotNil applies the NotNil predicate on the "connected_hub_id" field. +func ConnectedHubIDNotNil() predicate.RuntimeBroker { + return predicate.RuntimeBroker(sql.FieldNotNull(FieldConnectedHubID)) +} + +// ConnectedHubIDEqualFold applies the EqualFold predicate on the "connected_hub_id" field. +func ConnectedHubIDEqualFold(v string) predicate.RuntimeBroker { + return predicate.RuntimeBroker(sql.FieldEqualFold(FieldConnectedHubID, v)) +} + +// ConnectedHubIDContainsFold applies the ContainsFold predicate on the "connected_hub_id" field. +func ConnectedHubIDContainsFold(v string) predicate.RuntimeBroker { + return predicate.RuntimeBroker(sql.FieldContainsFold(FieldConnectedHubID, v)) +} + +// ConnectedSessionIDEQ applies the EQ predicate on the "connected_session_id" field. +func ConnectedSessionIDEQ(v string) predicate.RuntimeBroker { + return predicate.RuntimeBroker(sql.FieldEQ(FieldConnectedSessionID, v)) +} + +// ConnectedSessionIDNEQ applies the NEQ predicate on the "connected_session_id" field. +func ConnectedSessionIDNEQ(v string) predicate.RuntimeBroker { + return predicate.RuntimeBroker(sql.FieldNEQ(FieldConnectedSessionID, v)) +} + +// ConnectedSessionIDIn applies the In predicate on the "connected_session_id" field. +func ConnectedSessionIDIn(vs ...string) predicate.RuntimeBroker { + return predicate.RuntimeBroker(sql.FieldIn(FieldConnectedSessionID, vs...)) +} + +// ConnectedSessionIDNotIn applies the NotIn predicate on the "connected_session_id" field. +func ConnectedSessionIDNotIn(vs ...string) predicate.RuntimeBroker { + return predicate.RuntimeBroker(sql.FieldNotIn(FieldConnectedSessionID, vs...)) +} + +// ConnectedSessionIDGT applies the GT predicate on the "connected_session_id" field. +func ConnectedSessionIDGT(v string) predicate.RuntimeBroker { + return predicate.RuntimeBroker(sql.FieldGT(FieldConnectedSessionID, v)) +} + +// ConnectedSessionIDGTE applies the GTE predicate on the "connected_session_id" field. +func ConnectedSessionIDGTE(v string) predicate.RuntimeBroker { + return predicate.RuntimeBroker(sql.FieldGTE(FieldConnectedSessionID, v)) +} + +// ConnectedSessionIDLT applies the LT predicate on the "connected_session_id" field. +func ConnectedSessionIDLT(v string) predicate.RuntimeBroker { + return predicate.RuntimeBroker(sql.FieldLT(FieldConnectedSessionID, v)) +} + +// ConnectedSessionIDLTE applies the LTE predicate on the "connected_session_id" field. +func ConnectedSessionIDLTE(v string) predicate.RuntimeBroker { + return predicate.RuntimeBroker(sql.FieldLTE(FieldConnectedSessionID, v)) +} + +// ConnectedSessionIDContains applies the Contains predicate on the "connected_session_id" field. +func ConnectedSessionIDContains(v string) predicate.RuntimeBroker { + return predicate.RuntimeBroker(sql.FieldContains(FieldConnectedSessionID, v)) +} + +// ConnectedSessionIDHasPrefix applies the HasPrefix predicate on the "connected_session_id" field. +func ConnectedSessionIDHasPrefix(v string) predicate.RuntimeBroker { + return predicate.RuntimeBroker(sql.FieldHasPrefix(FieldConnectedSessionID, v)) +} + +// ConnectedSessionIDHasSuffix applies the HasSuffix predicate on the "connected_session_id" field. +func ConnectedSessionIDHasSuffix(v string) predicate.RuntimeBroker { + return predicate.RuntimeBroker(sql.FieldHasSuffix(FieldConnectedSessionID, v)) +} + +// ConnectedSessionIDIsNil applies the IsNil predicate on the "connected_session_id" field. +func ConnectedSessionIDIsNil() predicate.RuntimeBroker { + return predicate.RuntimeBroker(sql.FieldIsNull(FieldConnectedSessionID)) +} + +// ConnectedSessionIDNotNil applies the NotNil predicate on the "connected_session_id" field. +func ConnectedSessionIDNotNil() predicate.RuntimeBroker { + return predicate.RuntimeBroker(sql.FieldNotNull(FieldConnectedSessionID)) +} + +// ConnectedSessionIDEqualFold applies the EqualFold predicate on the "connected_session_id" field. +func ConnectedSessionIDEqualFold(v string) predicate.RuntimeBroker { + return predicate.RuntimeBroker(sql.FieldEqualFold(FieldConnectedSessionID, v)) +} + +// ConnectedSessionIDContainsFold applies the ContainsFold predicate on the "connected_session_id" field. +func ConnectedSessionIDContainsFold(v string) predicate.RuntimeBroker { + return predicate.RuntimeBroker(sql.FieldContainsFold(FieldConnectedSessionID, v)) +} + +// ConnectedAtEQ applies the EQ predicate on the "connected_at" field. +func ConnectedAtEQ(v time.Time) predicate.RuntimeBroker { + return predicate.RuntimeBroker(sql.FieldEQ(FieldConnectedAt, v)) +} + +// ConnectedAtNEQ applies the NEQ predicate on the "connected_at" field. +func ConnectedAtNEQ(v time.Time) predicate.RuntimeBroker { + return predicate.RuntimeBroker(sql.FieldNEQ(FieldConnectedAt, v)) +} + +// ConnectedAtIn applies the In predicate on the "connected_at" field. +func ConnectedAtIn(vs ...time.Time) predicate.RuntimeBroker { + return predicate.RuntimeBroker(sql.FieldIn(FieldConnectedAt, vs...)) +} + +// ConnectedAtNotIn applies the NotIn predicate on the "connected_at" field. +func ConnectedAtNotIn(vs ...time.Time) predicate.RuntimeBroker { + return predicate.RuntimeBroker(sql.FieldNotIn(FieldConnectedAt, vs...)) +} + +// ConnectedAtGT applies the GT predicate on the "connected_at" field. +func ConnectedAtGT(v time.Time) predicate.RuntimeBroker { + return predicate.RuntimeBroker(sql.FieldGT(FieldConnectedAt, v)) +} + +// ConnectedAtGTE applies the GTE predicate on the "connected_at" field. +func ConnectedAtGTE(v time.Time) predicate.RuntimeBroker { + return predicate.RuntimeBroker(sql.FieldGTE(FieldConnectedAt, v)) +} + +// ConnectedAtLT applies the LT predicate on the "connected_at" field. +func ConnectedAtLT(v time.Time) predicate.RuntimeBroker { + return predicate.RuntimeBroker(sql.FieldLT(FieldConnectedAt, v)) +} + +// ConnectedAtLTE applies the LTE predicate on the "connected_at" field. +func ConnectedAtLTE(v time.Time) predicate.RuntimeBroker { + return predicate.RuntimeBroker(sql.FieldLTE(FieldConnectedAt, v)) +} + +// ConnectedAtIsNil applies the IsNil predicate on the "connected_at" field. +func ConnectedAtIsNil() predicate.RuntimeBroker { + return predicate.RuntimeBroker(sql.FieldIsNull(FieldConnectedAt)) +} + +// ConnectedAtNotNil applies the NotNil predicate on the "connected_at" field. +func ConnectedAtNotNil() predicate.RuntimeBroker { + return predicate.RuntimeBroker(sql.FieldNotNull(FieldConnectedAt)) +} + +// CreatedEQ applies the EQ predicate on the "created" field. +func CreatedEQ(v time.Time) predicate.RuntimeBroker { + return predicate.RuntimeBroker(sql.FieldEQ(FieldCreated, v)) +} + +// CreatedNEQ applies the NEQ predicate on the "created" field. +func CreatedNEQ(v time.Time) predicate.RuntimeBroker { + return predicate.RuntimeBroker(sql.FieldNEQ(FieldCreated, v)) +} + +// CreatedIn applies the In predicate on the "created" field. +func CreatedIn(vs ...time.Time) predicate.RuntimeBroker { + return predicate.RuntimeBroker(sql.FieldIn(FieldCreated, vs...)) +} + +// CreatedNotIn applies the NotIn predicate on the "created" field. +func CreatedNotIn(vs ...time.Time) predicate.RuntimeBroker { + return predicate.RuntimeBroker(sql.FieldNotIn(FieldCreated, vs...)) +} + +// CreatedGT applies the GT predicate on the "created" field. +func CreatedGT(v time.Time) predicate.RuntimeBroker { + return predicate.RuntimeBroker(sql.FieldGT(FieldCreated, v)) +} + +// CreatedGTE applies the GTE predicate on the "created" field. +func CreatedGTE(v time.Time) predicate.RuntimeBroker { + return predicate.RuntimeBroker(sql.FieldGTE(FieldCreated, v)) +} + +// CreatedLT applies the LT predicate on the "created" field. +func CreatedLT(v time.Time) predicate.RuntimeBroker { + return predicate.RuntimeBroker(sql.FieldLT(FieldCreated, v)) +} + +// CreatedLTE applies the LTE predicate on the "created" field. +func CreatedLTE(v time.Time) predicate.RuntimeBroker { + return predicate.RuntimeBroker(sql.FieldLTE(FieldCreated, v)) +} + +// UpdatedEQ applies the EQ predicate on the "updated" field. +func UpdatedEQ(v time.Time) predicate.RuntimeBroker { + return predicate.RuntimeBroker(sql.FieldEQ(FieldUpdated, v)) +} + +// UpdatedNEQ applies the NEQ predicate on the "updated" field. +func UpdatedNEQ(v time.Time) predicate.RuntimeBroker { + return predicate.RuntimeBroker(sql.FieldNEQ(FieldUpdated, v)) +} + +// UpdatedIn applies the In predicate on the "updated" field. +func UpdatedIn(vs ...time.Time) predicate.RuntimeBroker { + return predicate.RuntimeBroker(sql.FieldIn(FieldUpdated, vs...)) +} + +// UpdatedNotIn applies the NotIn predicate on the "updated" field. +func UpdatedNotIn(vs ...time.Time) predicate.RuntimeBroker { + return predicate.RuntimeBroker(sql.FieldNotIn(FieldUpdated, vs...)) +} + +// UpdatedGT applies the GT predicate on the "updated" field. +func UpdatedGT(v time.Time) predicate.RuntimeBroker { + return predicate.RuntimeBroker(sql.FieldGT(FieldUpdated, v)) +} + +// UpdatedGTE applies the GTE predicate on the "updated" field. +func UpdatedGTE(v time.Time) predicate.RuntimeBroker { + return predicate.RuntimeBroker(sql.FieldGTE(FieldUpdated, v)) +} + +// UpdatedLT applies the LT predicate on the "updated" field. +func UpdatedLT(v time.Time) predicate.RuntimeBroker { + return predicate.RuntimeBroker(sql.FieldLT(FieldUpdated, v)) +} + +// UpdatedLTE applies the LTE predicate on the "updated" field. +func UpdatedLTE(v time.Time) predicate.RuntimeBroker { + return predicate.RuntimeBroker(sql.FieldLTE(FieldUpdated, v)) +} + +// And groups predicates with the AND operator between them. +func And(predicates ...predicate.RuntimeBroker) predicate.RuntimeBroker { + return predicate.RuntimeBroker(sql.AndPredicates(predicates...)) +} + +// Or groups predicates with the OR operator between them. +func Or(predicates ...predicate.RuntimeBroker) predicate.RuntimeBroker { + return predicate.RuntimeBroker(sql.OrPredicates(predicates...)) +} + +// Not applies the not operator on the given predicate. +func Not(p predicate.RuntimeBroker) predicate.RuntimeBroker { + return predicate.RuntimeBroker(sql.NotPredicates(p)) +} diff --git a/pkg/ent/runtimebroker_create.go b/pkg/ent/runtimebroker_create.go new file mode 100644 index 000000000..26f4aaa55 --- /dev/null +++ b/pkg/ent/runtimebroker_create.go @@ -0,0 +1,2105 @@ +// Code generated by ent, DO NOT EDIT. + +package ent + +import ( + "context" + "errors" + "fmt" + "time" + + "entgo.io/ent/dialect" + "entgo.io/ent/dialect/sql" + "entgo.io/ent/dialect/sql/sqlgraph" + "entgo.io/ent/schema/field" + "github.com/GoogleCloudPlatform/scion/pkg/ent/runtimebroker" + "github.com/google/uuid" +) + +// RuntimeBrokerCreate is the builder for creating a RuntimeBroker entity. +type RuntimeBrokerCreate struct { + config + mutation *RuntimeBrokerMutation + hooks []Hook + conflict []sql.ConflictOption +} + +// SetName sets the "name" field. +func (_c *RuntimeBrokerCreate) SetName(v string) *RuntimeBrokerCreate { + _c.mutation.SetName(v) + return _c +} + +// SetSlug sets the "slug" field. +func (_c *RuntimeBrokerCreate) SetSlug(v string) *RuntimeBrokerCreate { + _c.mutation.SetSlug(v) + return _c +} + +// SetType sets the "type" field. +func (_c *RuntimeBrokerCreate) SetType(v string) *RuntimeBrokerCreate { + _c.mutation.SetType(v) + return _c +} + +// SetNillableType sets the "type" field if the given value is not nil. +func (_c *RuntimeBrokerCreate) SetNillableType(v *string) *RuntimeBrokerCreate { + if v != nil { + _c.SetType(*v) + } + return _c +} + +// SetMode sets the "mode" field. +func (_c *RuntimeBrokerCreate) SetMode(v string) *RuntimeBrokerCreate { + _c.mutation.SetMode(v) + return _c +} + +// SetNillableMode sets the "mode" field if the given value is not nil. +func (_c *RuntimeBrokerCreate) SetNillableMode(v *string) *RuntimeBrokerCreate { + if v != nil { + _c.SetMode(*v) + } + return _c +} + +// SetVersion sets the "version" field. +func (_c *RuntimeBrokerCreate) SetVersion(v string) *RuntimeBrokerCreate { + _c.mutation.SetVersion(v) + return _c +} + +// SetNillableVersion sets the "version" field if the given value is not nil. +func (_c *RuntimeBrokerCreate) SetNillableVersion(v *string) *RuntimeBrokerCreate { + if v != nil { + _c.SetVersion(*v) + } + return _c +} + +// SetLockVersion sets the "lock_version" field. +func (_c *RuntimeBrokerCreate) SetLockVersion(v int64) *RuntimeBrokerCreate { + _c.mutation.SetLockVersion(v) + return _c +} + +// SetNillableLockVersion sets the "lock_version" field if the given value is not nil. +func (_c *RuntimeBrokerCreate) SetNillableLockVersion(v *int64) *RuntimeBrokerCreate { + if v != nil { + _c.SetLockVersion(*v) + } + return _c +} + +// SetStatus sets the "status" field. +func (_c *RuntimeBrokerCreate) SetStatus(v string) *RuntimeBrokerCreate { + _c.mutation.SetStatus(v) + return _c +} + +// SetNillableStatus sets the "status" field if the given value is not nil. +func (_c *RuntimeBrokerCreate) SetNillableStatus(v *string) *RuntimeBrokerCreate { + if v != nil { + _c.SetStatus(*v) + } + return _c +} + +// SetConnectionState sets the "connection_state" field. +func (_c *RuntimeBrokerCreate) SetConnectionState(v string) *RuntimeBrokerCreate { + _c.mutation.SetConnectionState(v) + return _c +} + +// SetNillableConnectionState sets the "connection_state" field if the given value is not nil. +func (_c *RuntimeBrokerCreate) SetNillableConnectionState(v *string) *RuntimeBrokerCreate { + if v != nil { + _c.SetConnectionState(*v) + } + return _c +} + +// SetLastHeartbeat sets the "last_heartbeat" field. +func (_c *RuntimeBrokerCreate) SetLastHeartbeat(v time.Time) *RuntimeBrokerCreate { + _c.mutation.SetLastHeartbeat(v) + return _c +} + +// SetNillableLastHeartbeat sets the "last_heartbeat" field if the given value is not nil. +func (_c *RuntimeBrokerCreate) SetNillableLastHeartbeat(v *time.Time) *RuntimeBrokerCreate { + if v != nil { + _c.SetLastHeartbeat(*v) + } + return _c +} + +// SetCapabilities sets the "capabilities" field. +func (_c *RuntimeBrokerCreate) SetCapabilities(v string) *RuntimeBrokerCreate { + _c.mutation.SetCapabilities(v) + return _c +} + +// SetNillableCapabilities sets the "capabilities" field if the given value is not nil. +func (_c *RuntimeBrokerCreate) SetNillableCapabilities(v *string) *RuntimeBrokerCreate { + if v != nil { + _c.SetCapabilities(*v) + } + return _c +} + +// SetSupportedHarnesses sets the "supported_harnesses" field. +func (_c *RuntimeBrokerCreate) SetSupportedHarnesses(v string) *RuntimeBrokerCreate { + _c.mutation.SetSupportedHarnesses(v) + return _c +} + +// SetNillableSupportedHarnesses sets the "supported_harnesses" field if the given value is not nil. +func (_c *RuntimeBrokerCreate) SetNillableSupportedHarnesses(v *string) *RuntimeBrokerCreate { + if v != nil { + _c.SetSupportedHarnesses(*v) + } + return _c +} + +// SetResources sets the "resources" field. +func (_c *RuntimeBrokerCreate) SetResources(v string) *RuntimeBrokerCreate { + _c.mutation.SetResources(v) + return _c +} + +// SetNillableResources sets the "resources" field if the given value is not nil. +func (_c *RuntimeBrokerCreate) SetNillableResources(v *string) *RuntimeBrokerCreate { + if v != nil { + _c.SetResources(*v) + } + return _c +} + +// SetRuntimes sets the "runtimes" field. +func (_c *RuntimeBrokerCreate) SetRuntimes(v string) *RuntimeBrokerCreate { + _c.mutation.SetRuntimes(v) + return _c +} + +// SetNillableRuntimes sets the "runtimes" field if the given value is not nil. +func (_c *RuntimeBrokerCreate) SetNillableRuntimes(v *string) *RuntimeBrokerCreate { + if v != nil { + _c.SetRuntimes(*v) + } + return _c +} + +// SetLabels sets the "labels" field. +func (_c *RuntimeBrokerCreate) SetLabels(v string) *RuntimeBrokerCreate { + _c.mutation.SetLabels(v) + return _c +} + +// SetNillableLabels sets the "labels" field if the given value is not nil. +func (_c *RuntimeBrokerCreate) SetNillableLabels(v *string) *RuntimeBrokerCreate { + if v != nil { + _c.SetLabels(*v) + } + return _c +} + +// SetAnnotations sets the "annotations" field. +func (_c *RuntimeBrokerCreate) SetAnnotations(v string) *RuntimeBrokerCreate { + _c.mutation.SetAnnotations(v) + return _c +} + +// SetNillableAnnotations sets the "annotations" field if the given value is not nil. +func (_c *RuntimeBrokerCreate) SetNillableAnnotations(v *string) *RuntimeBrokerCreate { + if v != nil { + _c.SetAnnotations(*v) + } + return _c +} + +// SetEndpoint sets the "endpoint" field. +func (_c *RuntimeBrokerCreate) SetEndpoint(v string) *RuntimeBrokerCreate { + _c.mutation.SetEndpoint(v) + return _c +} + +// SetNillableEndpoint sets the "endpoint" field if the given value is not nil. +func (_c *RuntimeBrokerCreate) SetNillableEndpoint(v *string) *RuntimeBrokerCreate { + if v != nil { + _c.SetEndpoint(*v) + } + return _c +} + +// SetCreatedBy sets the "created_by" field. +func (_c *RuntimeBrokerCreate) SetCreatedBy(v string) *RuntimeBrokerCreate { + _c.mutation.SetCreatedBy(v) + return _c +} + +// SetNillableCreatedBy sets the "created_by" field if the given value is not nil. +func (_c *RuntimeBrokerCreate) SetNillableCreatedBy(v *string) *RuntimeBrokerCreate { + if v != nil { + _c.SetCreatedBy(*v) + } + return _c +} + +// SetAutoProvide sets the "auto_provide" field. +func (_c *RuntimeBrokerCreate) SetAutoProvide(v bool) *RuntimeBrokerCreate { + _c.mutation.SetAutoProvide(v) + return _c +} + +// SetNillableAutoProvide sets the "auto_provide" field if the given value is not nil. +func (_c *RuntimeBrokerCreate) SetNillableAutoProvide(v *bool) *RuntimeBrokerCreate { + if v != nil { + _c.SetAutoProvide(*v) + } + return _c +} + +// SetConnectedHubID sets the "connected_hub_id" field. +func (_c *RuntimeBrokerCreate) SetConnectedHubID(v string) *RuntimeBrokerCreate { + _c.mutation.SetConnectedHubID(v) + return _c +} + +// SetNillableConnectedHubID sets the "connected_hub_id" field if the given value is not nil. +func (_c *RuntimeBrokerCreate) SetNillableConnectedHubID(v *string) *RuntimeBrokerCreate { + if v != nil { + _c.SetConnectedHubID(*v) + } + return _c +} + +// SetConnectedSessionID sets the "connected_session_id" field. +func (_c *RuntimeBrokerCreate) SetConnectedSessionID(v string) *RuntimeBrokerCreate { + _c.mutation.SetConnectedSessionID(v) + return _c +} + +// SetNillableConnectedSessionID sets the "connected_session_id" field if the given value is not nil. +func (_c *RuntimeBrokerCreate) SetNillableConnectedSessionID(v *string) *RuntimeBrokerCreate { + if v != nil { + _c.SetConnectedSessionID(*v) + } + return _c +} + +// SetConnectedAt sets the "connected_at" field. +func (_c *RuntimeBrokerCreate) SetConnectedAt(v time.Time) *RuntimeBrokerCreate { + _c.mutation.SetConnectedAt(v) + return _c +} + +// SetNillableConnectedAt sets the "connected_at" field if the given value is not nil. +func (_c *RuntimeBrokerCreate) SetNillableConnectedAt(v *time.Time) *RuntimeBrokerCreate { + if v != nil { + _c.SetConnectedAt(*v) + } + return _c +} + +// SetCreated sets the "created" field. +func (_c *RuntimeBrokerCreate) SetCreated(v time.Time) *RuntimeBrokerCreate { + _c.mutation.SetCreated(v) + return _c +} + +// SetNillableCreated sets the "created" field if the given value is not nil. +func (_c *RuntimeBrokerCreate) SetNillableCreated(v *time.Time) *RuntimeBrokerCreate { + if v != nil { + _c.SetCreated(*v) + } + return _c +} + +// SetUpdated sets the "updated" field. +func (_c *RuntimeBrokerCreate) SetUpdated(v time.Time) *RuntimeBrokerCreate { + _c.mutation.SetUpdated(v) + return _c +} + +// SetNillableUpdated sets the "updated" field if the given value is not nil. +func (_c *RuntimeBrokerCreate) SetNillableUpdated(v *time.Time) *RuntimeBrokerCreate { + if v != nil { + _c.SetUpdated(*v) + } + return _c +} + +// SetID sets the "id" field. +func (_c *RuntimeBrokerCreate) SetID(v uuid.UUID) *RuntimeBrokerCreate { + _c.mutation.SetID(v) + return _c +} + +// SetNillableID sets the "id" field if the given value is not nil. +func (_c *RuntimeBrokerCreate) SetNillableID(v *uuid.UUID) *RuntimeBrokerCreate { + if v != nil { + _c.SetID(*v) + } + return _c +} + +// Mutation returns the RuntimeBrokerMutation object of the builder. +func (_c *RuntimeBrokerCreate) Mutation() *RuntimeBrokerMutation { + return _c.mutation +} + +// Save creates the RuntimeBroker in the database. +func (_c *RuntimeBrokerCreate) Save(ctx context.Context) (*RuntimeBroker, error) { + _c.defaults() + return withHooks(ctx, _c.sqlSave, _c.mutation, _c.hooks) +} + +// SaveX calls Save and panics if Save returns an error. +func (_c *RuntimeBrokerCreate) SaveX(ctx context.Context) *RuntimeBroker { + v, err := _c.Save(ctx) + if err != nil { + panic(err) + } + return v +} + +// Exec executes the query. +func (_c *RuntimeBrokerCreate) Exec(ctx context.Context) error { + _, err := _c.Save(ctx) + return err +} + +// ExecX is like Exec, but panics if an error occurs. +func (_c *RuntimeBrokerCreate) ExecX(ctx context.Context) { + if err := _c.Exec(ctx); err != nil { + panic(err) + } +} + +// defaults sets the default values of the builder before save. +func (_c *RuntimeBrokerCreate) defaults() { + if _, ok := _c.mutation.Mode(); !ok { + v := runtimebroker.DefaultMode + _c.mutation.SetMode(v) + } + if _, ok := _c.mutation.LockVersion(); !ok { + v := runtimebroker.DefaultLockVersion + _c.mutation.SetLockVersion(v) + } + if _, ok := _c.mutation.Status(); !ok { + v := runtimebroker.DefaultStatus + _c.mutation.SetStatus(v) + } + if _, ok := _c.mutation.ConnectionState(); !ok { + v := runtimebroker.DefaultConnectionState + _c.mutation.SetConnectionState(v) + } + if _, ok := _c.mutation.AutoProvide(); !ok { + v := runtimebroker.DefaultAutoProvide + _c.mutation.SetAutoProvide(v) + } + if _, ok := _c.mutation.Created(); !ok { + v := runtimebroker.DefaultCreated() + _c.mutation.SetCreated(v) + } + if _, ok := _c.mutation.Updated(); !ok { + v := runtimebroker.DefaultUpdated() + _c.mutation.SetUpdated(v) + } + if _, ok := _c.mutation.ID(); !ok { + v := runtimebroker.DefaultID() + _c.mutation.SetID(v) + } +} + +// check runs all checks and user-defined validators on the builder. +func (_c *RuntimeBrokerCreate) check() error { + if _, ok := _c.mutation.Name(); !ok { + return &ValidationError{Name: "name", err: errors.New(`ent: missing required field "RuntimeBroker.name"`)} + } + if v, ok := _c.mutation.Name(); ok { + if err := runtimebroker.NameValidator(v); err != nil { + return &ValidationError{Name: "name", err: fmt.Errorf(`ent: validator failed for field "RuntimeBroker.name": %w`, err)} + } + } + if _, ok := _c.mutation.Slug(); !ok { + return &ValidationError{Name: "slug", err: errors.New(`ent: missing required field "RuntimeBroker.slug"`)} + } + if v, ok := _c.mutation.Slug(); ok { + if err := runtimebroker.SlugValidator(v); err != nil { + return &ValidationError{Name: "slug", err: fmt.Errorf(`ent: validator failed for field "RuntimeBroker.slug": %w`, err)} + } + } + if _, ok := _c.mutation.Mode(); !ok { + return &ValidationError{Name: "mode", err: errors.New(`ent: missing required field "RuntimeBroker.mode"`)} + } + if _, ok := _c.mutation.LockVersion(); !ok { + return &ValidationError{Name: "lock_version", err: errors.New(`ent: missing required field "RuntimeBroker.lock_version"`)} + } + if _, ok := _c.mutation.Status(); !ok { + return &ValidationError{Name: "status", err: errors.New(`ent: missing required field "RuntimeBroker.status"`)} + } + if _, ok := _c.mutation.ConnectionState(); !ok { + return &ValidationError{Name: "connection_state", err: errors.New(`ent: missing required field "RuntimeBroker.connection_state"`)} + } + if _, ok := _c.mutation.AutoProvide(); !ok { + return &ValidationError{Name: "auto_provide", err: errors.New(`ent: missing required field "RuntimeBroker.auto_provide"`)} + } + if _, ok := _c.mutation.Created(); !ok { + return &ValidationError{Name: "created", err: errors.New(`ent: missing required field "RuntimeBroker.created"`)} + } + if _, ok := _c.mutation.Updated(); !ok { + return &ValidationError{Name: "updated", err: errors.New(`ent: missing required field "RuntimeBroker.updated"`)} + } + return nil +} + +func (_c *RuntimeBrokerCreate) sqlSave(ctx context.Context) (*RuntimeBroker, error) { + if err := _c.check(); err != nil { + return nil, err + } + _node, _spec := _c.createSpec() + if err := sqlgraph.CreateNode(ctx, _c.driver, _spec); err != nil { + if sqlgraph.IsConstraintError(err) { + err = &ConstraintError{msg: err.Error(), wrap: err} + } + return nil, err + } + if _spec.ID.Value != nil { + if id, ok := _spec.ID.Value.(*uuid.UUID); ok { + _node.ID = *id + } else if err := _node.ID.Scan(_spec.ID.Value); err != nil { + return nil, err + } + } + _c.mutation.id = &_node.ID + _c.mutation.done = true + return _node, nil +} + +func (_c *RuntimeBrokerCreate) createSpec() (*RuntimeBroker, *sqlgraph.CreateSpec) { + var ( + _node = &RuntimeBroker{config: _c.config} + _spec = sqlgraph.NewCreateSpec(runtimebroker.Table, sqlgraph.NewFieldSpec(runtimebroker.FieldID, field.TypeUUID)) + ) + _spec.OnConflict = _c.conflict + if id, ok := _c.mutation.ID(); ok { + _node.ID = id + _spec.ID.Value = &id + } + if value, ok := _c.mutation.Name(); ok { + _spec.SetField(runtimebroker.FieldName, field.TypeString, value) + _node.Name = value + } + if value, ok := _c.mutation.Slug(); ok { + _spec.SetField(runtimebroker.FieldSlug, field.TypeString, value) + _node.Slug = value + } + if value, ok := _c.mutation.GetType(); ok { + _spec.SetField(runtimebroker.FieldType, field.TypeString, value) + _node.Type = value + } + if value, ok := _c.mutation.Mode(); ok { + _spec.SetField(runtimebroker.FieldMode, field.TypeString, value) + _node.Mode = value + } + if value, ok := _c.mutation.Version(); ok { + _spec.SetField(runtimebroker.FieldVersion, field.TypeString, value) + _node.Version = value + } + if value, ok := _c.mutation.LockVersion(); ok { + _spec.SetField(runtimebroker.FieldLockVersion, field.TypeInt64, value) + _node.LockVersion = value + } + if value, ok := _c.mutation.Status(); ok { + _spec.SetField(runtimebroker.FieldStatus, field.TypeString, value) + _node.Status = value + } + if value, ok := _c.mutation.ConnectionState(); ok { + _spec.SetField(runtimebroker.FieldConnectionState, field.TypeString, value) + _node.ConnectionState = value + } + if value, ok := _c.mutation.LastHeartbeat(); ok { + _spec.SetField(runtimebroker.FieldLastHeartbeat, field.TypeTime, value) + _node.LastHeartbeat = &value + } + if value, ok := _c.mutation.Capabilities(); ok { + _spec.SetField(runtimebroker.FieldCapabilities, field.TypeString, value) + _node.Capabilities = value + } + if value, ok := _c.mutation.SupportedHarnesses(); ok { + _spec.SetField(runtimebroker.FieldSupportedHarnesses, field.TypeString, value) + _node.SupportedHarnesses = value + } + if value, ok := _c.mutation.Resources(); ok { + _spec.SetField(runtimebroker.FieldResources, field.TypeString, value) + _node.Resources = value + } + if value, ok := _c.mutation.Runtimes(); ok { + _spec.SetField(runtimebroker.FieldRuntimes, field.TypeString, value) + _node.Runtimes = value + } + if value, ok := _c.mutation.Labels(); ok { + _spec.SetField(runtimebroker.FieldLabels, field.TypeString, value) + _node.Labels = value + } + if value, ok := _c.mutation.Annotations(); ok { + _spec.SetField(runtimebroker.FieldAnnotations, field.TypeString, value) + _node.Annotations = value + } + if value, ok := _c.mutation.Endpoint(); ok { + _spec.SetField(runtimebroker.FieldEndpoint, field.TypeString, value) + _node.Endpoint = value + } + if value, ok := _c.mutation.CreatedBy(); ok { + _spec.SetField(runtimebroker.FieldCreatedBy, field.TypeString, value) + _node.CreatedBy = value + } + if value, ok := _c.mutation.AutoProvide(); ok { + _spec.SetField(runtimebroker.FieldAutoProvide, field.TypeBool, value) + _node.AutoProvide = value + } + if value, ok := _c.mutation.ConnectedHubID(); ok { + _spec.SetField(runtimebroker.FieldConnectedHubID, field.TypeString, value) + _node.ConnectedHubID = &value + } + if value, ok := _c.mutation.ConnectedSessionID(); ok { + _spec.SetField(runtimebroker.FieldConnectedSessionID, field.TypeString, value) + _node.ConnectedSessionID = &value + } + if value, ok := _c.mutation.ConnectedAt(); ok { + _spec.SetField(runtimebroker.FieldConnectedAt, field.TypeTime, value) + _node.ConnectedAt = &value + } + if value, ok := _c.mutation.Created(); ok { + _spec.SetField(runtimebroker.FieldCreated, field.TypeTime, value) + _node.Created = value + } + if value, ok := _c.mutation.Updated(); ok { + _spec.SetField(runtimebroker.FieldUpdated, field.TypeTime, value) + _node.Updated = value + } + return _node, _spec +} + +// OnConflict allows configuring the `ON CONFLICT` / `ON DUPLICATE KEY` clause +// of the `INSERT` statement. For example: +// +// client.RuntimeBroker.Create(). +// SetName(v). +// OnConflict( +// // Update the row with the new values +// // the was proposed for insertion. +// sql.ResolveWithNewValues(), +// ). +// // Override some of the fields with custom +// // update values. +// Update(func(u *ent.RuntimeBrokerUpsert) { +// SetName(v+v). +// }). +// Exec(ctx) +func (_c *RuntimeBrokerCreate) OnConflict(opts ...sql.ConflictOption) *RuntimeBrokerUpsertOne { + _c.conflict = opts + return &RuntimeBrokerUpsertOne{ + create: _c, + } +} + +// OnConflictColumns calls `OnConflict` and configures the columns +// as conflict target. Using this option is equivalent to using: +// +// client.RuntimeBroker.Create(). +// OnConflict(sql.ConflictColumns(columns...)). +// Exec(ctx) +func (_c *RuntimeBrokerCreate) OnConflictColumns(columns ...string) *RuntimeBrokerUpsertOne { + _c.conflict = append(_c.conflict, sql.ConflictColumns(columns...)) + return &RuntimeBrokerUpsertOne{ + create: _c, + } +} + +type ( + // RuntimeBrokerUpsertOne is the builder for "upsert"-ing + // one RuntimeBroker node. + RuntimeBrokerUpsertOne struct { + create *RuntimeBrokerCreate + } + + // RuntimeBrokerUpsert is the "OnConflict" setter. + RuntimeBrokerUpsert struct { + *sql.UpdateSet + } +) + +// SetName sets the "name" field. +func (u *RuntimeBrokerUpsert) SetName(v string) *RuntimeBrokerUpsert { + u.Set(runtimebroker.FieldName, v) + return u +} + +// UpdateName sets the "name" field to the value that was provided on create. +func (u *RuntimeBrokerUpsert) UpdateName() *RuntimeBrokerUpsert { + u.SetExcluded(runtimebroker.FieldName) + return u +} + +// SetSlug sets the "slug" field. +func (u *RuntimeBrokerUpsert) SetSlug(v string) *RuntimeBrokerUpsert { + u.Set(runtimebroker.FieldSlug, v) + return u +} + +// UpdateSlug sets the "slug" field to the value that was provided on create. +func (u *RuntimeBrokerUpsert) UpdateSlug() *RuntimeBrokerUpsert { + u.SetExcluded(runtimebroker.FieldSlug) + return u +} + +// SetType sets the "type" field. +func (u *RuntimeBrokerUpsert) SetType(v string) *RuntimeBrokerUpsert { + u.Set(runtimebroker.FieldType, v) + return u +} + +// UpdateType sets the "type" field to the value that was provided on create. +func (u *RuntimeBrokerUpsert) UpdateType() *RuntimeBrokerUpsert { + u.SetExcluded(runtimebroker.FieldType) + return u +} + +// ClearType clears the value of the "type" field. +func (u *RuntimeBrokerUpsert) ClearType() *RuntimeBrokerUpsert { + u.SetNull(runtimebroker.FieldType) + return u +} + +// SetMode sets the "mode" field. +func (u *RuntimeBrokerUpsert) SetMode(v string) *RuntimeBrokerUpsert { + u.Set(runtimebroker.FieldMode, v) + return u +} + +// UpdateMode sets the "mode" field to the value that was provided on create. +func (u *RuntimeBrokerUpsert) UpdateMode() *RuntimeBrokerUpsert { + u.SetExcluded(runtimebroker.FieldMode) + return u +} + +// SetVersion sets the "version" field. +func (u *RuntimeBrokerUpsert) SetVersion(v string) *RuntimeBrokerUpsert { + u.Set(runtimebroker.FieldVersion, v) + return u +} + +// UpdateVersion sets the "version" field to the value that was provided on create. +func (u *RuntimeBrokerUpsert) UpdateVersion() *RuntimeBrokerUpsert { + u.SetExcluded(runtimebroker.FieldVersion) + return u +} + +// ClearVersion clears the value of the "version" field. +func (u *RuntimeBrokerUpsert) ClearVersion() *RuntimeBrokerUpsert { + u.SetNull(runtimebroker.FieldVersion) + return u +} + +// SetLockVersion sets the "lock_version" field. +func (u *RuntimeBrokerUpsert) SetLockVersion(v int64) *RuntimeBrokerUpsert { + u.Set(runtimebroker.FieldLockVersion, v) + return u +} + +// UpdateLockVersion sets the "lock_version" field to the value that was provided on create. +func (u *RuntimeBrokerUpsert) UpdateLockVersion() *RuntimeBrokerUpsert { + u.SetExcluded(runtimebroker.FieldLockVersion) + return u +} + +// AddLockVersion adds v to the "lock_version" field. +func (u *RuntimeBrokerUpsert) AddLockVersion(v int64) *RuntimeBrokerUpsert { + u.Add(runtimebroker.FieldLockVersion, v) + return u +} + +// SetStatus sets the "status" field. +func (u *RuntimeBrokerUpsert) SetStatus(v string) *RuntimeBrokerUpsert { + u.Set(runtimebroker.FieldStatus, v) + return u +} + +// UpdateStatus sets the "status" field to the value that was provided on create. +func (u *RuntimeBrokerUpsert) UpdateStatus() *RuntimeBrokerUpsert { + u.SetExcluded(runtimebroker.FieldStatus) + return u +} + +// SetConnectionState sets the "connection_state" field. +func (u *RuntimeBrokerUpsert) SetConnectionState(v string) *RuntimeBrokerUpsert { + u.Set(runtimebroker.FieldConnectionState, v) + return u +} + +// UpdateConnectionState sets the "connection_state" field to the value that was provided on create. +func (u *RuntimeBrokerUpsert) UpdateConnectionState() *RuntimeBrokerUpsert { + u.SetExcluded(runtimebroker.FieldConnectionState) + return u +} + +// SetLastHeartbeat sets the "last_heartbeat" field. +func (u *RuntimeBrokerUpsert) SetLastHeartbeat(v time.Time) *RuntimeBrokerUpsert { + u.Set(runtimebroker.FieldLastHeartbeat, v) + return u +} + +// UpdateLastHeartbeat sets the "last_heartbeat" field to the value that was provided on create. +func (u *RuntimeBrokerUpsert) UpdateLastHeartbeat() *RuntimeBrokerUpsert { + u.SetExcluded(runtimebroker.FieldLastHeartbeat) + return u +} + +// ClearLastHeartbeat clears the value of the "last_heartbeat" field. +func (u *RuntimeBrokerUpsert) ClearLastHeartbeat() *RuntimeBrokerUpsert { + u.SetNull(runtimebroker.FieldLastHeartbeat) + return u +} + +// SetCapabilities sets the "capabilities" field. +func (u *RuntimeBrokerUpsert) SetCapabilities(v string) *RuntimeBrokerUpsert { + u.Set(runtimebroker.FieldCapabilities, v) + return u +} + +// UpdateCapabilities sets the "capabilities" field to the value that was provided on create. +func (u *RuntimeBrokerUpsert) UpdateCapabilities() *RuntimeBrokerUpsert { + u.SetExcluded(runtimebroker.FieldCapabilities) + return u +} + +// ClearCapabilities clears the value of the "capabilities" field. +func (u *RuntimeBrokerUpsert) ClearCapabilities() *RuntimeBrokerUpsert { + u.SetNull(runtimebroker.FieldCapabilities) + return u +} + +// SetSupportedHarnesses sets the "supported_harnesses" field. +func (u *RuntimeBrokerUpsert) SetSupportedHarnesses(v string) *RuntimeBrokerUpsert { + u.Set(runtimebroker.FieldSupportedHarnesses, v) + return u +} + +// UpdateSupportedHarnesses sets the "supported_harnesses" field to the value that was provided on create. +func (u *RuntimeBrokerUpsert) UpdateSupportedHarnesses() *RuntimeBrokerUpsert { + u.SetExcluded(runtimebroker.FieldSupportedHarnesses) + return u +} + +// ClearSupportedHarnesses clears the value of the "supported_harnesses" field. +func (u *RuntimeBrokerUpsert) ClearSupportedHarnesses() *RuntimeBrokerUpsert { + u.SetNull(runtimebroker.FieldSupportedHarnesses) + return u +} + +// SetResources sets the "resources" field. +func (u *RuntimeBrokerUpsert) SetResources(v string) *RuntimeBrokerUpsert { + u.Set(runtimebroker.FieldResources, v) + return u +} + +// UpdateResources sets the "resources" field to the value that was provided on create. +func (u *RuntimeBrokerUpsert) UpdateResources() *RuntimeBrokerUpsert { + u.SetExcluded(runtimebroker.FieldResources) + return u +} + +// ClearResources clears the value of the "resources" field. +func (u *RuntimeBrokerUpsert) ClearResources() *RuntimeBrokerUpsert { + u.SetNull(runtimebroker.FieldResources) + return u +} + +// SetRuntimes sets the "runtimes" field. +func (u *RuntimeBrokerUpsert) SetRuntimes(v string) *RuntimeBrokerUpsert { + u.Set(runtimebroker.FieldRuntimes, v) + return u +} + +// UpdateRuntimes sets the "runtimes" field to the value that was provided on create. +func (u *RuntimeBrokerUpsert) UpdateRuntimes() *RuntimeBrokerUpsert { + u.SetExcluded(runtimebroker.FieldRuntimes) + return u +} + +// ClearRuntimes clears the value of the "runtimes" field. +func (u *RuntimeBrokerUpsert) ClearRuntimes() *RuntimeBrokerUpsert { + u.SetNull(runtimebroker.FieldRuntimes) + return u +} + +// SetLabels sets the "labels" field. +func (u *RuntimeBrokerUpsert) SetLabels(v string) *RuntimeBrokerUpsert { + u.Set(runtimebroker.FieldLabels, v) + return u +} + +// UpdateLabels sets the "labels" field to the value that was provided on create. +func (u *RuntimeBrokerUpsert) UpdateLabels() *RuntimeBrokerUpsert { + u.SetExcluded(runtimebroker.FieldLabels) + return u +} + +// ClearLabels clears the value of the "labels" field. +func (u *RuntimeBrokerUpsert) ClearLabels() *RuntimeBrokerUpsert { + u.SetNull(runtimebroker.FieldLabels) + return u +} + +// SetAnnotations sets the "annotations" field. +func (u *RuntimeBrokerUpsert) SetAnnotations(v string) *RuntimeBrokerUpsert { + u.Set(runtimebroker.FieldAnnotations, v) + return u +} + +// UpdateAnnotations sets the "annotations" field to the value that was provided on create. +func (u *RuntimeBrokerUpsert) UpdateAnnotations() *RuntimeBrokerUpsert { + u.SetExcluded(runtimebroker.FieldAnnotations) + return u +} + +// ClearAnnotations clears the value of the "annotations" field. +func (u *RuntimeBrokerUpsert) ClearAnnotations() *RuntimeBrokerUpsert { + u.SetNull(runtimebroker.FieldAnnotations) + return u +} + +// SetEndpoint sets the "endpoint" field. +func (u *RuntimeBrokerUpsert) SetEndpoint(v string) *RuntimeBrokerUpsert { + u.Set(runtimebroker.FieldEndpoint, v) + return u +} + +// UpdateEndpoint sets the "endpoint" field to the value that was provided on create. +func (u *RuntimeBrokerUpsert) UpdateEndpoint() *RuntimeBrokerUpsert { + u.SetExcluded(runtimebroker.FieldEndpoint) + return u +} + +// ClearEndpoint clears the value of the "endpoint" field. +func (u *RuntimeBrokerUpsert) ClearEndpoint() *RuntimeBrokerUpsert { + u.SetNull(runtimebroker.FieldEndpoint) + return u +} + +// SetCreatedBy sets the "created_by" field. +func (u *RuntimeBrokerUpsert) SetCreatedBy(v string) *RuntimeBrokerUpsert { + u.Set(runtimebroker.FieldCreatedBy, v) + return u +} + +// UpdateCreatedBy sets the "created_by" field to the value that was provided on create. +func (u *RuntimeBrokerUpsert) UpdateCreatedBy() *RuntimeBrokerUpsert { + u.SetExcluded(runtimebroker.FieldCreatedBy) + return u +} + +// ClearCreatedBy clears the value of the "created_by" field. +func (u *RuntimeBrokerUpsert) ClearCreatedBy() *RuntimeBrokerUpsert { + u.SetNull(runtimebroker.FieldCreatedBy) + return u +} + +// SetAutoProvide sets the "auto_provide" field. +func (u *RuntimeBrokerUpsert) SetAutoProvide(v bool) *RuntimeBrokerUpsert { + u.Set(runtimebroker.FieldAutoProvide, v) + return u +} + +// UpdateAutoProvide sets the "auto_provide" field to the value that was provided on create. +func (u *RuntimeBrokerUpsert) UpdateAutoProvide() *RuntimeBrokerUpsert { + u.SetExcluded(runtimebroker.FieldAutoProvide) + return u +} + +// SetConnectedHubID sets the "connected_hub_id" field. +func (u *RuntimeBrokerUpsert) SetConnectedHubID(v string) *RuntimeBrokerUpsert { + u.Set(runtimebroker.FieldConnectedHubID, v) + return u +} + +// UpdateConnectedHubID sets the "connected_hub_id" field to the value that was provided on create. +func (u *RuntimeBrokerUpsert) UpdateConnectedHubID() *RuntimeBrokerUpsert { + u.SetExcluded(runtimebroker.FieldConnectedHubID) + return u +} + +// ClearConnectedHubID clears the value of the "connected_hub_id" field. +func (u *RuntimeBrokerUpsert) ClearConnectedHubID() *RuntimeBrokerUpsert { + u.SetNull(runtimebroker.FieldConnectedHubID) + return u +} + +// SetConnectedSessionID sets the "connected_session_id" field. +func (u *RuntimeBrokerUpsert) SetConnectedSessionID(v string) *RuntimeBrokerUpsert { + u.Set(runtimebroker.FieldConnectedSessionID, v) + return u +} + +// UpdateConnectedSessionID sets the "connected_session_id" field to the value that was provided on create. +func (u *RuntimeBrokerUpsert) UpdateConnectedSessionID() *RuntimeBrokerUpsert { + u.SetExcluded(runtimebroker.FieldConnectedSessionID) + return u +} + +// ClearConnectedSessionID clears the value of the "connected_session_id" field. +func (u *RuntimeBrokerUpsert) ClearConnectedSessionID() *RuntimeBrokerUpsert { + u.SetNull(runtimebroker.FieldConnectedSessionID) + return u +} + +// SetConnectedAt sets the "connected_at" field. +func (u *RuntimeBrokerUpsert) SetConnectedAt(v time.Time) *RuntimeBrokerUpsert { + u.Set(runtimebroker.FieldConnectedAt, v) + return u +} + +// UpdateConnectedAt sets the "connected_at" field to the value that was provided on create. +func (u *RuntimeBrokerUpsert) UpdateConnectedAt() *RuntimeBrokerUpsert { + u.SetExcluded(runtimebroker.FieldConnectedAt) + return u +} + +// ClearConnectedAt clears the value of the "connected_at" field. +func (u *RuntimeBrokerUpsert) ClearConnectedAt() *RuntimeBrokerUpsert { + u.SetNull(runtimebroker.FieldConnectedAt) + return u +} + +// SetUpdated sets the "updated" field. +func (u *RuntimeBrokerUpsert) SetUpdated(v time.Time) *RuntimeBrokerUpsert { + u.Set(runtimebroker.FieldUpdated, v) + return u +} + +// UpdateUpdated sets the "updated" field to the value that was provided on create. +func (u *RuntimeBrokerUpsert) UpdateUpdated() *RuntimeBrokerUpsert { + u.SetExcluded(runtimebroker.FieldUpdated) + return u +} + +// UpdateNewValues updates the mutable fields using the new values that were set on create except the ID field. +// Using this option is equivalent to using: +// +// client.RuntimeBroker.Create(). +// OnConflict( +// sql.ResolveWithNewValues(), +// sql.ResolveWith(func(u *sql.UpdateSet) { +// u.SetIgnore(runtimebroker.FieldID) +// }), +// ). +// Exec(ctx) +func (u *RuntimeBrokerUpsertOne) UpdateNewValues() *RuntimeBrokerUpsertOne { + u.create.conflict = append(u.create.conflict, sql.ResolveWithNewValues()) + u.create.conflict = append(u.create.conflict, sql.ResolveWith(func(s *sql.UpdateSet) { + if _, exists := u.create.mutation.ID(); exists { + s.SetIgnore(runtimebroker.FieldID) + } + if _, exists := u.create.mutation.Created(); exists { + s.SetIgnore(runtimebroker.FieldCreated) + } + })) + return u +} + +// Ignore sets each column to itself in case of conflict. +// Using this option is equivalent to using: +// +// client.RuntimeBroker.Create(). +// OnConflict(sql.ResolveWithIgnore()). +// Exec(ctx) +func (u *RuntimeBrokerUpsertOne) Ignore() *RuntimeBrokerUpsertOne { + u.create.conflict = append(u.create.conflict, sql.ResolveWithIgnore()) + return u +} + +// DoNothing configures the conflict_action to `DO NOTHING`. +// Supported only by SQLite and PostgreSQL. +func (u *RuntimeBrokerUpsertOne) DoNothing() *RuntimeBrokerUpsertOne { + u.create.conflict = append(u.create.conflict, sql.DoNothing()) + return u +} + +// Update allows overriding fields `UPDATE` values. See the RuntimeBrokerCreate.OnConflict +// documentation for more info. +func (u *RuntimeBrokerUpsertOne) Update(set func(*RuntimeBrokerUpsert)) *RuntimeBrokerUpsertOne { + u.create.conflict = append(u.create.conflict, sql.ResolveWith(func(update *sql.UpdateSet) { + set(&RuntimeBrokerUpsert{UpdateSet: update}) + })) + return u +} + +// SetName sets the "name" field. +func (u *RuntimeBrokerUpsertOne) SetName(v string) *RuntimeBrokerUpsertOne { + return u.Update(func(s *RuntimeBrokerUpsert) { + s.SetName(v) + }) +} + +// UpdateName sets the "name" field to the value that was provided on create. +func (u *RuntimeBrokerUpsertOne) UpdateName() *RuntimeBrokerUpsertOne { + return u.Update(func(s *RuntimeBrokerUpsert) { + s.UpdateName() + }) +} + +// SetSlug sets the "slug" field. +func (u *RuntimeBrokerUpsertOne) SetSlug(v string) *RuntimeBrokerUpsertOne { + return u.Update(func(s *RuntimeBrokerUpsert) { + s.SetSlug(v) + }) +} + +// UpdateSlug sets the "slug" field to the value that was provided on create. +func (u *RuntimeBrokerUpsertOne) UpdateSlug() *RuntimeBrokerUpsertOne { + return u.Update(func(s *RuntimeBrokerUpsert) { + s.UpdateSlug() + }) +} + +// SetType sets the "type" field. +func (u *RuntimeBrokerUpsertOne) SetType(v string) *RuntimeBrokerUpsertOne { + return u.Update(func(s *RuntimeBrokerUpsert) { + s.SetType(v) + }) +} + +// UpdateType sets the "type" field to the value that was provided on create. +func (u *RuntimeBrokerUpsertOne) UpdateType() *RuntimeBrokerUpsertOne { + return u.Update(func(s *RuntimeBrokerUpsert) { + s.UpdateType() + }) +} + +// ClearType clears the value of the "type" field. +func (u *RuntimeBrokerUpsertOne) ClearType() *RuntimeBrokerUpsertOne { + return u.Update(func(s *RuntimeBrokerUpsert) { + s.ClearType() + }) +} + +// SetMode sets the "mode" field. +func (u *RuntimeBrokerUpsertOne) SetMode(v string) *RuntimeBrokerUpsertOne { + return u.Update(func(s *RuntimeBrokerUpsert) { + s.SetMode(v) + }) +} + +// UpdateMode sets the "mode" field to the value that was provided on create. +func (u *RuntimeBrokerUpsertOne) UpdateMode() *RuntimeBrokerUpsertOne { + return u.Update(func(s *RuntimeBrokerUpsert) { + s.UpdateMode() + }) +} + +// SetVersion sets the "version" field. +func (u *RuntimeBrokerUpsertOne) SetVersion(v string) *RuntimeBrokerUpsertOne { + return u.Update(func(s *RuntimeBrokerUpsert) { + s.SetVersion(v) + }) +} + +// UpdateVersion sets the "version" field to the value that was provided on create. +func (u *RuntimeBrokerUpsertOne) UpdateVersion() *RuntimeBrokerUpsertOne { + return u.Update(func(s *RuntimeBrokerUpsert) { + s.UpdateVersion() + }) +} + +// ClearVersion clears the value of the "version" field. +func (u *RuntimeBrokerUpsertOne) ClearVersion() *RuntimeBrokerUpsertOne { + return u.Update(func(s *RuntimeBrokerUpsert) { + s.ClearVersion() + }) +} + +// SetLockVersion sets the "lock_version" field. +func (u *RuntimeBrokerUpsertOne) SetLockVersion(v int64) *RuntimeBrokerUpsertOne { + return u.Update(func(s *RuntimeBrokerUpsert) { + s.SetLockVersion(v) + }) +} + +// AddLockVersion adds v to the "lock_version" field. +func (u *RuntimeBrokerUpsertOne) AddLockVersion(v int64) *RuntimeBrokerUpsertOne { + return u.Update(func(s *RuntimeBrokerUpsert) { + s.AddLockVersion(v) + }) +} + +// UpdateLockVersion sets the "lock_version" field to the value that was provided on create. +func (u *RuntimeBrokerUpsertOne) UpdateLockVersion() *RuntimeBrokerUpsertOne { + return u.Update(func(s *RuntimeBrokerUpsert) { + s.UpdateLockVersion() + }) +} + +// SetStatus sets the "status" field. +func (u *RuntimeBrokerUpsertOne) SetStatus(v string) *RuntimeBrokerUpsertOne { + return u.Update(func(s *RuntimeBrokerUpsert) { + s.SetStatus(v) + }) +} + +// UpdateStatus sets the "status" field to the value that was provided on create. +func (u *RuntimeBrokerUpsertOne) UpdateStatus() *RuntimeBrokerUpsertOne { + return u.Update(func(s *RuntimeBrokerUpsert) { + s.UpdateStatus() + }) +} + +// SetConnectionState sets the "connection_state" field. +func (u *RuntimeBrokerUpsertOne) SetConnectionState(v string) *RuntimeBrokerUpsertOne { + return u.Update(func(s *RuntimeBrokerUpsert) { + s.SetConnectionState(v) + }) +} + +// UpdateConnectionState sets the "connection_state" field to the value that was provided on create. +func (u *RuntimeBrokerUpsertOne) UpdateConnectionState() *RuntimeBrokerUpsertOne { + return u.Update(func(s *RuntimeBrokerUpsert) { + s.UpdateConnectionState() + }) +} + +// SetLastHeartbeat sets the "last_heartbeat" field. +func (u *RuntimeBrokerUpsertOne) SetLastHeartbeat(v time.Time) *RuntimeBrokerUpsertOne { + return u.Update(func(s *RuntimeBrokerUpsert) { + s.SetLastHeartbeat(v) + }) +} + +// UpdateLastHeartbeat sets the "last_heartbeat" field to the value that was provided on create. +func (u *RuntimeBrokerUpsertOne) UpdateLastHeartbeat() *RuntimeBrokerUpsertOne { + return u.Update(func(s *RuntimeBrokerUpsert) { + s.UpdateLastHeartbeat() + }) +} + +// ClearLastHeartbeat clears the value of the "last_heartbeat" field. +func (u *RuntimeBrokerUpsertOne) ClearLastHeartbeat() *RuntimeBrokerUpsertOne { + return u.Update(func(s *RuntimeBrokerUpsert) { + s.ClearLastHeartbeat() + }) +} + +// SetCapabilities sets the "capabilities" field. +func (u *RuntimeBrokerUpsertOne) SetCapabilities(v string) *RuntimeBrokerUpsertOne { + return u.Update(func(s *RuntimeBrokerUpsert) { + s.SetCapabilities(v) + }) +} + +// UpdateCapabilities sets the "capabilities" field to the value that was provided on create. +func (u *RuntimeBrokerUpsertOne) UpdateCapabilities() *RuntimeBrokerUpsertOne { + return u.Update(func(s *RuntimeBrokerUpsert) { + s.UpdateCapabilities() + }) +} + +// ClearCapabilities clears the value of the "capabilities" field. +func (u *RuntimeBrokerUpsertOne) ClearCapabilities() *RuntimeBrokerUpsertOne { + return u.Update(func(s *RuntimeBrokerUpsert) { + s.ClearCapabilities() + }) +} + +// SetSupportedHarnesses sets the "supported_harnesses" field. +func (u *RuntimeBrokerUpsertOne) SetSupportedHarnesses(v string) *RuntimeBrokerUpsertOne { + return u.Update(func(s *RuntimeBrokerUpsert) { + s.SetSupportedHarnesses(v) + }) +} + +// UpdateSupportedHarnesses sets the "supported_harnesses" field to the value that was provided on create. +func (u *RuntimeBrokerUpsertOne) UpdateSupportedHarnesses() *RuntimeBrokerUpsertOne { + return u.Update(func(s *RuntimeBrokerUpsert) { + s.UpdateSupportedHarnesses() + }) +} + +// ClearSupportedHarnesses clears the value of the "supported_harnesses" field. +func (u *RuntimeBrokerUpsertOne) ClearSupportedHarnesses() *RuntimeBrokerUpsertOne { + return u.Update(func(s *RuntimeBrokerUpsert) { + s.ClearSupportedHarnesses() + }) +} + +// SetResources sets the "resources" field. +func (u *RuntimeBrokerUpsertOne) SetResources(v string) *RuntimeBrokerUpsertOne { + return u.Update(func(s *RuntimeBrokerUpsert) { + s.SetResources(v) + }) +} + +// UpdateResources sets the "resources" field to the value that was provided on create. +func (u *RuntimeBrokerUpsertOne) UpdateResources() *RuntimeBrokerUpsertOne { + return u.Update(func(s *RuntimeBrokerUpsert) { + s.UpdateResources() + }) +} + +// ClearResources clears the value of the "resources" field. +func (u *RuntimeBrokerUpsertOne) ClearResources() *RuntimeBrokerUpsertOne { + return u.Update(func(s *RuntimeBrokerUpsert) { + s.ClearResources() + }) +} + +// SetRuntimes sets the "runtimes" field. +func (u *RuntimeBrokerUpsertOne) SetRuntimes(v string) *RuntimeBrokerUpsertOne { + return u.Update(func(s *RuntimeBrokerUpsert) { + s.SetRuntimes(v) + }) +} + +// UpdateRuntimes sets the "runtimes" field to the value that was provided on create. +func (u *RuntimeBrokerUpsertOne) UpdateRuntimes() *RuntimeBrokerUpsertOne { + return u.Update(func(s *RuntimeBrokerUpsert) { + s.UpdateRuntimes() + }) +} + +// ClearRuntimes clears the value of the "runtimes" field. +func (u *RuntimeBrokerUpsertOne) ClearRuntimes() *RuntimeBrokerUpsertOne { + return u.Update(func(s *RuntimeBrokerUpsert) { + s.ClearRuntimes() + }) +} + +// SetLabels sets the "labels" field. +func (u *RuntimeBrokerUpsertOne) SetLabels(v string) *RuntimeBrokerUpsertOne { + return u.Update(func(s *RuntimeBrokerUpsert) { + s.SetLabels(v) + }) +} + +// UpdateLabels sets the "labels" field to the value that was provided on create. +func (u *RuntimeBrokerUpsertOne) UpdateLabels() *RuntimeBrokerUpsertOne { + return u.Update(func(s *RuntimeBrokerUpsert) { + s.UpdateLabels() + }) +} + +// ClearLabels clears the value of the "labels" field. +func (u *RuntimeBrokerUpsertOne) ClearLabels() *RuntimeBrokerUpsertOne { + return u.Update(func(s *RuntimeBrokerUpsert) { + s.ClearLabels() + }) +} + +// SetAnnotations sets the "annotations" field. +func (u *RuntimeBrokerUpsertOne) SetAnnotations(v string) *RuntimeBrokerUpsertOne { + return u.Update(func(s *RuntimeBrokerUpsert) { + s.SetAnnotations(v) + }) +} + +// UpdateAnnotations sets the "annotations" field to the value that was provided on create. +func (u *RuntimeBrokerUpsertOne) UpdateAnnotations() *RuntimeBrokerUpsertOne { + return u.Update(func(s *RuntimeBrokerUpsert) { + s.UpdateAnnotations() + }) +} + +// ClearAnnotations clears the value of the "annotations" field. +func (u *RuntimeBrokerUpsertOne) ClearAnnotations() *RuntimeBrokerUpsertOne { + return u.Update(func(s *RuntimeBrokerUpsert) { + s.ClearAnnotations() + }) +} + +// SetEndpoint sets the "endpoint" field. +func (u *RuntimeBrokerUpsertOne) SetEndpoint(v string) *RuntimeBrokerUpsertOne { + return u.Update(func(s *RuntimeBrokerUpsert) { + s.SetEndpoint(v) + }) +} + +// UpdateEndpoint sets the "endpoint" field to the value that was provided on create. +func (u *RuntimeBrokerUpsertOne) UpdateEndpoint() *RuntimeBrokerUpsertOne { + return u.Update(func(s *RuntimeBrokerUpsert) { + s.UpdateEndpoint() + }) +} + +// ClearEndpoint clears the value of the "endpoint" field. +func (u *RuntimeBrokerUpsertOne) ClearEndpoint() *RuntimeBrokerUpsertOne { + return u.Update(func(s *RuntimeBrokerUpsert) { + s.ClearEndpoint() + }) +} + +// SetCreatedBy sets the "created_by" field. +func (u *RuntimeBrokerUpsertOne) SetCreatedBy(v string) *RuntimeBrokerUpsertOne { + return u.Update(func(s *RuntimeBrokerUpsert) { + s.SetCreatedBy(v) + }) +} + +// UpdateCreatedBy sets the "created_by" field to the value that was provided on create. +func (u *RuntimeBrokerUpsertOne) UpdateCreatedBy() *RuntimeBrokerUpsertOne { + return u.Update(func(s *RuntimeBrokerUpsert) { + s.UpdateCreatedBy() + }) +} + +// ClearCreatedBy clears the value of the "created_by" field. +func (u *RuntimeBrokerUpsertOne) ClearCreatedBy() *RuntimeBrokerUpsertOne { + return u.Update(func(s *RuntimeBrokerUpsert) { + s.ClearCreatedBy() + }) +} + +// SetAutoProvide sets the "auto_provide" field. +func (u *RuntimeBrokerUpsertOne) SetAutoProvide(v bool) *RuntimeBrokerUpsertOne { + return u.Update(func(s *RuntimeBrokerUpsert) { + s.SetAutoProvide(v) + }) +} + +// UpdateAutoProvide sets the "auto_provide" field to the value that was provided on create. +func (u *RuntimeBrokerUpsertOne) UpdateAutoProvide() *RuntimeBrokerUpsertOne { + return u.Update(func(s *RuntimeBrokerUpsert) { + s.UpdateAutoProvide() + }) +} + +// SetConnectedHubID sets the "connected_hub_id" field. +func (u *RuntimeBrokerUpsertOne) SetConnectedHubID(v string) *RuntimeBrokerUpsertOne { + return u.Update(func(s *RuntimeBrokerUpsert) { + s.SetConnectedHubID(v) + }) +} + +// UpdateConnectedHubID sets the "connected_hub_id" field to the value that was provided on create. +func (u *RuntimeBrokerUpsertOne) UpdateConnectedHubID() *RuntimeBrokerUpsertOne { + return u.Update(func(s *RuntimeBrokerUpsert) { + s.UpdateConnectedHubID() + }) +} + +// ClearConnectedHubID clears the value of the "connected_hub_id" field. +func (u *RuntimeBrokerUpsertOne) ClearConnectedHubID() *RuntimeBrokerUpsertOne { + return u.Update(func(s *RuntimeBrokerUpsert) { + s.ClearConnectedHubID() + }) +} + +// SetConnectedSessionID sets the "connected_session_id" field. +func (u *RuntimeBrokerUpsertOne) SetConnectedSessionID(v string) *RuntimeBrokerUpsertOne { + return u.Update(func(s *RuntimeBrokerUpsert) { + s.SetConnectedSessionID(v) + }) +} + +// UpdateConnectedSessionID sets the "connected_session_id" field to the value that was provided on create. +func (u *RuntimeBrokerUpsertOne) UpdateConnectedSessionID() *RuntimeBrokerUpsertOne { + return u.Update(func(s *RuntimeBrokerUpsert) { + s.UpdateConnectedSessionID() + }) +} + +// ClearConnectedSessionID clears the value of the "connected_session_id" field. +func (u *RuntimeBrokerUpsertOne) ClearConnectedSessionID() *RuntimeBrokerUpsertOne { + return u.Update(func(s *RuntimeBrokerUpsert) { + s.ClearConnectedSessionID() + }) +} + +// SetConnectedAt sets the "connected_at" field. +func (u *RuntimeBrokerUpsertOne) SetConnectedAt(v time.Time) *RuntimeBrokerUpsertOne { + return u.Update(func(s *RuntimeBrokerUpsert) { + s.SetConnectedAt(v) + }) +} + +// UpdateConnectedAt sets the "connected_at" field to the value that was provided on create. +func (u *RuntimeBrokerUpsertOne) UpdateConnectedAt() *RuntimeBrokerUpsertOne { + return u.Update(func(s *RuntimeBrokerUpsert) { + s.UpdateConnectedAt() + }) +} + +// ClearConnectedAt clears the value of the "connected_at" field. +func (u *RuntimeBrokerUpsertOne) ClearConnectedAt() *RuntimeBrokerUpsertOne { + return u.Update(func(s *RuntimeBrokerUpsert) { + s.ClearConnectedAt() + }) +} + +// SetUpdated sets the "updated" field. +func (u *RuntimeBrokerUpsertOne) SetUpdated(v time.Time) *RuntimeBrokerUpsertOne { + return u.Update(func(s *RuntimeBrokerUpsert) { + s.SetUpdated(v) + }) +} + +// UpdateUpdated sets the "updated" field to the value that was provided on create. +func (u *RuntimeBrokerUpsertOne) UpdateUpdated() *RuntimeBrokerUpsertOne { + return u.Update(func(s *RuntimeBrokerUpsert) { + s.UpdateUpdated() + }) +} + +// Exec executes the query. +func (u *RuntimeBrokerUpsertOne) Exec(ctx context.Context) error { + if len(u.create.conflict) == 0 { + return errors.New("ent: missing options for RuntimeBrokerCreate.OnConflict") + } + return u.create.Exec(ctx) +} + +// ExecX is like Exec, but panics if an error occurs. +func (u *RuntimeBrokerUpsertOne) ExecX(ctx context.Context) { + if err := u.create.Exec(ctx); err != nil { + panic(err) + } +} + +// Exec executes the UPSERT query and returns the inserted/updated ID. +func (u *RuntimeBrokerUpsertOne) ID(ctx context.Context) (id uuid.UUID, err error) { + if u.create.driver.Dialect() == dialect.MySQL { + // In case of "ON CONFLICT", there is no way to get back non-numeric ID + // fields from the database since MySQL does not support the RETURNING clause. + return id, errors.New("ent: RuntimeBrokerUpsertOne.ID is not supported by MySQL driver. Use RuntimeBrokerUpsertOne.Exec instead") + } + node, err := u.create.Save(ctx) + if err != nil { + return id, err + } + return node.ID, nil +} + +// IDX is like ID, but panics if an error occurs. +func (u *RuntimeBrokerUpsertOne) IDX(ctx context.Context) uuid.UUID { + id, err := u.ID(ctx) + if err != nil { + panic(err) + } + return id +} + +// RuntimeBrokerCreateBulk is the builder for creating many RuntimeBroker entities in bulk. +type RuntimeBrokerCreateBulk struct { + config + err error + builders []*RuntimeBrokerCreate + conflict []sql.ConflictOption +} + +// Save creates the RuntimeBroker entities in the database. +func (_c *RuntimeBrokerCreateBulk) Save(ctx context.Context) ([]*RuntimeBroker, error) { + if _c.err != nil { + return nil, _c.err + } + specs := make([]*sqlgraph.CreateSpec, len(_c.builders)) + nodes := make([]*RuntimeBroker, len(_c.builders)) + mutators := make([]Mutator, len(_c.builders)) + for i := range _c.builders { + func(i int, root context.Context) { + builder := _c.builders[i] + builder.defaults() + var mut Mutator = MutateFunc(func(ctx context.Context, m Mutation) (Value, error) { + mutation, ok := m.(*RuntimeBrokerMutation) + if !ok { + return nil, fmt.Errorf("unexpected mutation type %T", m) + } + if err := builder.check(); err != nil { + return nil, err + } + builder.mutation = mutation + var err error + nodes[i], specs[i] = builder.createSpec() + if i < len(mutators)-1 { + _, err = mutators[i+1].Mutate(root, _c.builders[i+1].mutation) + } else { + spec := &sqlgraph.BatchCreateSpec{Nodes: specs} + spec.OnConflict = _c.conflict + // Invoke the actual operation on the latest mutation in the chain. + if err = sqlgraph.BatchCreate(ctx, _c.driver, spec); err != nil { + if sqlgraph.IsConstraintError(err) { + err = &ConstraintError{msg: err.Error(), wrap: err} + } + } + } + if err != nil { + return nil, err + } + mutation.id = &nodes[i].ID + mutation.done = true + return nodes[i], nil + }) + for i := len(builder.hooks) - 1; i >= 0; i-- { + mut = builder.hooks[i](mut) + } + mutators[i] = mut + }(i, ctx) + } + if len(mutators) > 0 { + if _, err := mutators[0].Mutate(ctx, _c.builders[0].mutation); err != nil { + return nil, err + } + } + return nodes, nil +} + +// SaveX is like Save, but panics if an error occurs. +func (_c *RuntimeBrokerCreateBulk) SaveX(ctx context.Context) []*RuntimeBroker { + v, err := _c.Save(ctx) + if err != nil { + panic(err) + } + return v +} + +// Exec executes the query. +func (_c *RuntimeBrokerCreateBulk) Exec(ctx context.Context) error { + _, err := _c.Save(ctx) + return err +} + +// ExecX is like Exec, but panics if an error occurs. +func (_c *RuntimeBrokerCreateBulk) ExecX(ctx context.Context) { + if err := _c.Exec(ctx); err != nil { + panic(err) + } +} + +// OnConflict allows configuring the `ON CONFLICT` / `ON DUPLICATE KEY` clause +// of the `INSERT` statement. For example: +// +// client.RuntimeBroker.CreateBulk(builders...). +// OnConflict( +// // Update the row with the new values +// // the was proposed for insertion. +// sql.ResolveWithNewValues(), +// ). +// // Override some of the fields with custom +// // update values. +// Update(func(u *ent.RuntimeBrokerUpsert) { +// SetName(v+v). +// }). +// Exec(ctx) +func (_c *RuntimeBrokerCreateBulk) OnConflict(opts ...sql.ConflictOption) *RuntimeBrokerUpsertBulk { + _c.conflict = opts + return &RuntimeBrokerUpsertBulk{ + create: _c, + } +} + +// OnConflictColumns calls `OnConflict` and configures the columns +// as conflict target. Using this option is equivalent to using: +// +// client.RuntimeBroker.Create(). +// OnConflict(sql.ConflictColumns(columns...)). +// Exec(ctx) +func (_c *RuntimeBrokerCreateBulk) OnConflictColumns(columns ...string) *RuntimeBrokerUpsertBulk { + _c.conflict = append(_c.conflict, sql.ConflictColumns(columns...)) + return &RuntimeBrokerUpsertBulk{ + create: _c, + } +} + +// RuntimeBrokerUpsertBulk is the builder for "upsert"-ing +// a bulk of RuntimeBroker nodes. +type RuntimeBrokerUpsertBulk struct { + create *RuntimeBrokerCreateBulk +} + +// UpdateNewValues updates the mutable fields using the new values that +// were set on create. Using this option is equivalent to using: +// +// client.RuntimeBroker.Create(). +// OnConflict( +// sql.ResolveWithNewValues(), +// sql.ResolveWith(func(u *sql.UpdateSet) { +// u.SetIgnore(runtimebroker.FieldID) +// }), +// ). +// Exec(ctx) +func (u *RuntimeBrokerUpsertBulk) UpdateNewValues() *RuntimeBrokerUpsertBulk { + u.create.conflict = append(u.create.conflict, sql.ResolveWithNewValues()) + u.create.conflict = append(u.create.conflict, sql.ResolveWith(func(s *sql.UpdateSet) { + for _, b := range u.create.builders { + if _, exists := b.mutation.ID(); exists { + s.SetIgnore(runtimebroker.FieldID) + } + if _, exists := b.mutation.Created(); exists { + s.SetIgnore(runtimebroker.FieldCreated) + } + } + })) + return u +} + +// Ignore sets each column to itself in case of conflict. +// Using this option is equivalent to using: +// +// client.RuntimeBroker.Create(). +// OnConflict(sql.ResolveWithIgnore()). +// Exec(ctx) +func (u *RuntimeBrokerUpsertBulk) Ignore() *RuntimeBrokerUpsertBulk { + u.create.conflict = append(u.create.conflict, sql.ResolveWithIgnore()) + return u +} + +// DoNothing configures the conflict_action to `DO NOTHING`. +// Supported only by SQLite and PostgreSQL. +func (u *RuntimeBrokerUpsertBulk) DoNothing() *RuntimeBrokerUpsertBulk { + u.create.conflict = append(u.create.conflict, sql.DoNothing()) + return u +} + +// Update allows overriding fields `UPDATE` values. See the RuntimeBrokerCreateBulk.OnConflict +// documentation for more info. +func (u *RuntimeBrokerUpsertBulk) Update(set func(*RuntimeBrokerUpsert)) *RuntimeBrokerUpsertBulk { + u.create.conflict = append(u.create.conflict, sql.ResolveWith(func(update *sql.UpdateSet) { + set(&RuntimeBrokerUpsert{UpdateSet: update}) + })) + return u +} + +// SetName sets the "name" field. +func (u *RuntimeBrokerUpsertBulk) SetName(v string) *RuntimeBrokerUpsertBulk { + return u.Update(func(s *RuntimeBrokerUpsert) { + s.SetName(v) + }) +} + +// UpdateName sets the "name" field to the value that was provided on create. +func (u *RuntimeBrokerUpsertBulk) UpdateName() *RuntimeBrokerUpsertBulk { + return u.Update(func(s *RuntimeBrokerUpsert) { + s.UpdateName() + }) +} + +// SetSlug sets the "slug" field. +func (u *RuntimeBrokerUpsertBulk) SetSlug(v string) *RuntimeBrokerUpsertBulk { + return u.Update(func(s *RuntimeBrokerUpsert) { + s.SetSlug(v) + }) +} + +// UpdateSlug sets the "slug" field to the value that was provided on create. +func (u *RuntimeBrokerUpsertBulk) UpdateSlug() *RuntimeBrokerUpsertBulk { + return u.Update(func(s *RuntimeBrokerUpsert) { + s.UpdateSlug() + }) +} + +// SetType sets the "type" field. +func (u *RuntimeBrokerUpsertBulk) SetType(v string) *RuntimeBrokerUpsertBulk { + return u.Update(func(s *RuntimeBrokerUpsert) { + s.SetType(v) + }) +} + +// UpdateType sets the "type" field to the value that was provided on create. +func (u *RuntimeBrokerUpsertBulk) UpdateType() *RuntimeBrokerUpsertBulk { + return u.Update(func(s *RuntimeBrokerUpsert) { + s.UpdateType() + }) +} + +// ClearType clears the value of the "type" field. +func (u *RuntimeBrokerUpsertBulk) ClearType() *RuntimeBrokerUpsertBulk { + return u.Update(func(s *RuntimeBrokerUpsert) { + s.ClearType() + }) +} + +// SetMode sets the "mode" field. +func (u *RuntimeBrokerUpsertBulk) SetMode(v string) *RuntimeBrokerUpsertBulk { + return u.Update(func(s *RuntimeBrokerUpsert) { + s.SetMode(v) + }) +} + +// UpdateMode sets the "mode" field to the value that was provided on create. +func (u *RuntimeBrokerUpsertBulk) UpdateMode() *RuntimeBrokerUpsertBulk { + return u.Update(func(s *RuntimeBrokerUpsert) { + s.UpdateMode() + }) +} + +// SetVersion sets the "version" field. +func (u *RuntimeBrokerUpsertBulk) SetVersion(v string) *RuntimeBrokerUpsertBulk { + return u.Update(func(s *RuntimeBrokerUpsert) { + s.SetVersion(v) + }) +} + +// UpdateVersion sets the "version" field to the value that was provided on create. +func (u *RuntimeBrokerUpsertBulk) UpdateVersion() *RuntimeBrokerUpsertBulk { + return u.Update(func(s *RuntimeBrokerUpsert) { + s.UpdateVersion() + }) +} + +// ClearVersion clears the value of the "version" field. +func (u *RuntimeBrokerUpsertBulk) ClearVersion() *RuntimeBrokerUpsertBulk { + return u.Update(func(s *RuntimeBrokerUpsert) { + s.ClearVersion() + }) +} + +// SetLockVersion sets the "lock_version" field. +func (u *RuntimeBrokerUpsertBulk) SetLockVersion(v int64) *RuntimeBrokerUpsertBulk { + return u.Update(func(s *RuntimeBrokerUpsert) { + s.SetLockVersion(v) + }) +} + +// AddLockVersion adds v to the "lock_version" field. +func (u *RuntimeBrokerUpsertBulk) AddLockVersion(v int64) *RuntimeBrokerUpsertBulk { + return u.Update(func(s *RuntimeBrokerUpsert) { + s.AddLockVersion(v) + }) +} + +// UpdateLockVersion sets the "lock_version" field to the value that was provided on create. +func (u *RuntimeBrokerUpsertBulk) UpdateLockVersion() *RuntimeBrokerUpsertBulk { + return u.Update(func(s *RuntimeBrokerUpsert) { + s.UpdateLockVersion() + }) +} + +// SetStatus sets the "status" field. +func (u *RuntimeBrokerUpsertBulk) SetStatus(v string) *RuntimeBrokerUpsertBulk { + return u.Update(func(s *RuntimeBrokerUpsert) { + s.SetStatus(v) + }) +} + +// UpdateStatus sets the "status" field to the value that was provided on create. +func (u *RuntimeBrokerUpsertBulk) UpdateStatus() *RuntimeBrokerUpsertBulk { + return u.Update(func(s *RuntimeBrokerUpsert) { + s.UpdateStatus() + }) +} + +// SetConnectionState sets the "connection_state" field. +func (u *RuntimeBrokerUpsertBulk) SetConnectionState(v string) *RuntimeBrokerUpsertBulk { + return u.Update(func(s *RuntimeBrokerUpsert) { + s.SetConnectionState(v) + }) +} + +// UpdateConnectionState sets the "connection_state" field to the value that was provided on create. +func (u *RuntimeBrokerUpsertBulk) UpdateConnectionState() *RuntimeBrokerUpsertBulk { + return u.Update(func(s *RuntimeBrokerUpsert) { + s.UpdateConnectionState() + }) +} + +// SetLastHeartbeat sets the "last_heartbeat" field. +func (u *RuntimeBrokerUpsertBulk) SetLastHeartbeat(v time.Time) *RuntimeBrokerUpsertBulk { + return u.Update(func(s *RuntimeBrokerUpsert) { + s.SetLastHeartbeat(v) + }) +} + +// UpdateLastHeartbeat sets the "last_heartbeat" field to the value that was provided on create. +func (u *RuntimeBrokerUpsertBulk) UpdateLastHeartbeat() *RuntimeBrokerUpsertBulk { + return u.Update(func(s *RuntimeBrokerUpsert) { + s.UpdateLastHeartbeat() + }) +} + +// ClearLastHeartbeat clears the value of the "last_heartbeat" field. +func (u *RuntimeBrokerUpsertBulk) ClearLastHeartbeat() *RuntimeBrokerUpsertBulk { + return u.Update(func(s *RuntimeBrokerUpsert) { + s.ClearLastHeartbeat() + }) +} + +// SetCapabilities sets the "capabilities" field. +func (u *RuntimeBrokerUpsertBulk) SetCapabilities(v string) *RuntimeBrokerUpsertBulk { + return u.Update(func(s *RuntimeBrokerUpsert) { + s.SetCapabilities(v) + }) +} + +// UpdateCapabilities sets the "capabilities" field to the value that was provided on create. +func (u *RuntimeBrokerUpsertBulk) UpdateCapabilities() *RuntimeBrokerUpsertBulk { + return u.Update(func(s *RuntimeBrokerUpsert) { + s.UpdateCapabilities() + }) +} + +// ClearCapabilities clears the value of the "capabilities" field. +func (u *RuntimeBrokerUpsertBulk) ClearCapabilities() *RuntimeBrokerUpsertBulk { + return u.Update(func(s *RuntimeBrokerUpsert) { + s.ClearCapabilities() + }) +} + +// SetSupportedHarnesses sets the "supported_harnesses" field. +func (u *RuntimeBrokerUpsertBulk) SetSupportedHarnesses(v string) *RuntimeBrokerUpsertBulk { + return u.Update(func(s *RuntimeBrokerUpsert) { + s.SetSupportedHarnesses(v) + }) +} + +// UpdateSupportedHarnesses sets the "supported_harnesses" field to the value that was provided on create. +func (u *RuntimeBrokerUpsertBulk) UpdateSupportedHarnesses() *RuntimeBrokerUpsertBulk { + return u.Update(func(s *RuntimeBrokerUpsert) { + s.UpdateSupportedHarnesses() + }) +} + +// ClearSupportedHarnesses clears the value of the "supported_harnesses" field. +func (u *RuntimeBrokerUpsertBulk) ClearSupportedHarnesses() *RuntimeBrokerUpsertBulk { + return u.Update(func(s *RuntimeBrokerUpsert) { + s.ClearSupportedHarnesses() + }) +} + +// SetResources sets the "resources" field. +func (u *RuntimeBrokerUpsertBulk) SetResources(v string) *RuntimeBrokerUpsertBulk { + return u.Update(func(s *RuntimeBrokerUpsert) { + s.SetResources(v) + }) +} + +// UpdateResources sets the "resources" field to the value that was provided on create. +func (u *RuntimeBrokerUpsertBulk) UpdateResources() *RuntimeBrokerUpsertBulk { + return u.Update(func(s *RuntimeBrokerUpsert) { + s.UpdateResources() + }) +} + +// ClearResources clears the value of the "resources" field. +func (u *RuntimeBrokerUpsertBulk) ClearResources() *RuntimeBrokerUpsertBulk { + return u.Update(func(s *RuntimeBrokerUpsert) { + s.ClearResources() + }) +} + +// SetRuntimes sets the "runtimes" field. +func (u *RuntimeBrokerUpsertBulk) SetRuntimes(v string) *RuntimeBrokerUpsertBulk { + return u.Update(func(s *RuntimeBrokerUpsert) { + s.SetRuntimes(v) + }) +} + +// UpdateRuntimes sets the "runtimes" field to the value that was provided on create. +func (u *RuntimeBrokerUpsertBulk) UpdateRuntimes() *RuntimeBrokerUpsertBulk { + return u.Update(func(s *RuntimeBrokerUpsert) { + s.UpdateRuntimes() + }) +} + +// ClearRuntimes clears the value of the "runtimes" field. +func (u *RuntimeBrokerUpsertBulk) ClearRuntimes() *RuntimeBrokerUpsertBulk { + return u.Update(func(s *RuntimeBrokerUpsert) { + s.ClearRuntimes() + }) +} + +// SetLabels sets the "labels" field. +func (u *RuntimeBrokerUpsertBulk) SetLabels(v string) *RuntimeBrokerUpsertBulk { + return u.Update(func(s *RuntimeBrokerUpsert) { + s.SetLabels(v) + }) +} + +// UpdateLabels sets the "labels" field to the value that was provided on create. +func (u *RuntimeBrokerUpsertBulk) UpdateLabels() *RuntimeBrokerUpsertBulk { + return u.Update(func(s *RuntimeBrokerUpsert) { + s.UpdateLabels() + }) +} + +// ClearLabels clears the value of the "labels" field. +func (u *RuntimeBrokerUpsertBulk) ClearLabels() *RuntimeBrokerUpsertBulk { + return u.Update(func(s *RuntimeBrokerUpsert) { + s.ClearLabels() + }) +} + +// SetAnnotations sets the "annotations" field. +func (u *RuntimeBrokerUpsertBulk) SetAnnotations(v string) *RuntimeBrokerUpsertBulk { + return u.Update(func(s *RuntimeBrokerUpsert) { + s.SetAnnotations(v) + }) +} + +// UpdateAnnotations sets the "annotations" field to the value that was provided on create. +func (u *RuntimeBrokerUpsertBulk) UpdateAnnotations() *RuntimeBrokerUpsertBulk { + return u.Update(func(s *RuntimeBrokerUpsert) { + s.UpdateAnnotations() + }) +} + +// ClearAnnotations clears the value of the "annotations" field. +func (u *RuntimeBrokerUpsertBulk) ClearAnnotations() *RuntimeBrokerUpsertBulk { + return u.Update(func(s *RuntimeBrokerUpsert) { + s.ClearAnnotations() + }) +} + +// SetEndpoint sets the "endpoint" field. +func (u *RuntimeBrokerUpsertBulk) SetEndpoint(v string) *RuntimeBrokerUpsertBulk { + return u.Update(func(s *RuntimeBrokerUpsert) { + s.SetEndpoint(v) + }) +} + +// UpdateEndpoint sets the "endpoint" field to the value that was provided on create. +func (u *RuntimeBrokerUpsertBulk) UpdateEndpoint() *RuntimeBrokerUpsertBulk { + return u.Update(func(s *RuntimeBrokerUpsert) { + s.UpdateEndpoint() + }) +} + +// ClearEndpoint clears the value of the "endpoint" field. +func (u *RuntimeBrokerUpsertBulk) ClearEndpoint() *RuntimeBrokerUpsertBulk { + return u.Update(func(s *RuntimeBrokerUpsert) { + s.ClearEndpoint() + }) +} + +// SetCreatedBy sets the "created_by" field. +func (u *RuntimeBrokerUpsertBulk) SetCreatedBy(v string) *RuntimeBrokerUpsertBulk { + return u.Update(func(s *RuntimeBrokerUpsert) { + s.SetCreatedBy(v) + }) +} + +// UpdateCreatedBy sets the "created_by" field to the value that was provided on create. +func (u *RuntimeBrokerUpsertBulk) UpdateCreatedBy() *RuntimeBrokerUpsertBulk { + return u.Update(func(s *RuntimeBrokerUpsert) { + s.UpdateCreatedBy() + }) +} + +// ClearCreatedBy clears the value of the "created_by" field. +func (u *RuntimeBrokerUpsertBulk) ClearCreatedBy() *RuntimeBrokerUpsertBulk { + return u.Update(func(s *RuntimeBrokerUpsert) { + s.ClearCreatedBy() + }) +} + +// SetAutoProvide sets the "auto_provide" field. +func (u *RuntimeBrokerUpsertBulk) SetAutoProvide(v bool) *RuntimeBrokerUpsertBulk { + return u.Update(func(s *RuntimeBrokerUpsert) { + s.SetAutoProvide(v) + }) +} + +// UpdateAutoProvide sets the "auto_provide" field to the value that was provided on create. +func (u *RuntimeBrokerUpsertBulk) UpdateAutoProvide() *RuntimeBrokerUpsertBulk { + return u.Update(func(s *RuntimeBrokerUpsert) { + s.UpdateAutoProvide() + }) +} + +// SetConnectedHubID sets the "connected_hub_id" field. +func (u *RuntimeBrokerUpsertBulk) SetConnectedHubID(v string) *RuntimeBrokerUpsertBulk { + return u.Update(func(s *RuntimeBrokerUpsert) { + s.SetConnectedHubID(v) + }) +} + +// UpdateConnectedHubID sets the "connected_hub_id" field to the value that was provided on create. +func (u *RuntimeBrokerUpsertBulk) UpdateConnectedHubID() *RuntimeBrokerUpsertBulk { + return u.Update(func(s *RuntimeBrokerUpsert) { + s.UpdateConnectedHubID() + }) +} + +// ClearConnectedHubID clears the value of the "connected_hub_id" field. +func (u *RuntimeBrokerUpsertBulk) ClearConnectedHubID() *RuntimeBrokerUpsertBulk { + return u.Update(func(s *RuntimeBrokerUpsert) { + s.ClearConnectedHubID() + }) +} + +// SetConnectedSessionID sets the "connected_session_id" field. +func (u *RuntimeBrokerUpsertBulk) SetConnectedSessionID(v string) *RuntimeBrokerUpsertBulk { + return u.Update(func(s *RuntimeBrokerUpsert) { + s.SetConnectedSessionID(v) + }) +} + +// UpdateConnectedSessionID sets the "connected_session_id" field to the value that was provided on create. +func (u *RuntimeBrokerUpsertBulk) UpdateConnectedSessionID() *RuntimeBrokerUpsertBulk { + return u.Update(func(s *RuntimeBrokerUpsert) { + s.UpdateConnectedSessionID() + }) +} + +// ClearConnectedSessionID clears the value of the "connected_session_id" field. +func (u *RuntimeBrokerUpsertBulk) ClearConnectedSessionID() *RuntimeBrokerUpsertBulk { + return u.Update(func(s *RuntimeBrokerUpsert) { + s.ClearConnectedSessionID() + }) +} + +// SetConnectedAt sets the "connected_at" field. +func (u *RuntimeBrokerUpsertBulk) SetConnectedAt(v time.Time) *RuntimeBrokerUpsertBulk { + return u.Update(func(s *RuntimeBrokerUpsert) { + s.SetConnectedAt(v) + }) +} + +// UpdateConnectedAt sets the "connected_at" field to the value that was provided on create. +func (u *RuntimeBrokerUpsertBulk) UpdateConnectedAt() *RuntimeBrokerUpsertBulk { + return u.Update(func(s *RuntimeBrokerUpsert) { + s.UpdateConnectedAt() + }) +} + +// ClearConnectedAt clears the value of the "connected_at" field. +func (u *RuntimeBrokerUpsertBulk) ClearConnectedAt() *RuntimeBrokerUpsertBulk { + return u.Update(func(s *RuntimeBrokerUpsert) { + s.ClearConnectedAt() + }) +} + +// SetUpdated sets the "updated" field. +func (u *RuntimeBrokerUpsertBulk) SetUpdated(v time.Time) *RuntimeBrokerUpsertBulk { + return u.Update(func(s *RuntimeBrokerUpsert) { + s.SetUpdated(v) + }) +} + +// UpdateUpdated sets the "updated" field to the value that was provided on create. +func (u *RuntimeBrokerUpsertBulk) UpdateUpdated() *RuntimeBrokerUpsertBulk { + return u.Update(func(s *RuntimeBrokerUpsert) { + s.UpdateUpdated() + }) +} + +// Exec executes the query. +func (u *RuntimeBrokerUpsertBulk) Exec(ctx context.Context) error { + if u.create.err != nil { + return u.create.err + } + for i, b := range u.create.builders { + if len(b.conflict) != 0 { + return fmt.Errorf("ent: OnConflict was set for builder %d. Set it on the RuntimeBrokerCreateBulk instead", i) + } + } + if len(u.create.conflict) == 0 { + return errors.New("ent: missing options for RuntimeBrokerCreateBulk.OnConflict") + } + return u.create.Exec(ctx) +} + +// ExecX is like Exec, but panics if an error occurs. +func (u *RuntimeBrokerUpsertBulk) ExecX(ctx context.Context) { + if err := u.create.Exec(ctx); err != nil { + panic(err) + } +} diff --git a/pkg/ent/runtimebroker_delete.go b/pkg/ent/runtimebroker_delete.go new file mode 100644 index 000000000..5023bbb93 --- /dev/null +++ b/pkg/ent/runtimebroker_delete.go @@ -0,0 +1,88 @@ +// Code generated by ent, DO NOT EDIT. + +package ent + +import ( + "context" + + "entgo.io/ent/dialect/sql" + "entgo.io/ent/dialect/sql/sqlgraph" + "entgo.io/ent/schema/field" + "github.com/GoogleCloudPlatform/scion/pkg/ent/predicate" + "github.com/GoogleCloudPlatform/scion/pkg/ent/runtimebroker" +) + +// RuntimeBrokerDelete is the builder for deleting a RuntimeBroker entity. +type RuntimeBrokerDelete struct { + config + hooks []Hook + mutation *RuntimeBrokerMutation +} + +// Where appends a list predicates to the RuntimeBrokerDelete builder. +func (_d *RuntimeBrokerDelete) Where(ps ...predicate.RuntimeBroker) *RuntimeBrokerDelete { + _d.mutation.Where(ps...) + return _d +} + +// Exec executes the deletion query and returns how many vertices were deleted. +func (_d *RuntimeBrokerDelete) Exec(ctx context.Context) (int, error) { + return withHooks(ctx, _d.sqlExec, _d.mutation, _d.hooks) +} + +// ExecX is like Exec, but panics if an error occurs. +func (_d *RuntimeBrokerDelete) ExecX(ctx context.Context) int { + n, err := _d.Exec(ctx) + if err != nil { + panic(err) + } + return n +} + +func (_d *RuntimeBrokerDelete) sqlExec(ctx context.Context) (int, error) { + _spec := sqlgraph.NewDeleteSpec(runtimebroker.Table, sqlgraph.NewFieldSpec(runtimebroker.FieldID, field.TypeUUID)) + if ps := _d.mutation.predicates; len(ps) > 0 { + _spec.Predicate = func(selector *sql.Selector) { + for i := range ps { + ps[i](selector) + } + } + } + affected, err := sqlgraph.DeleteNodes(ctx, _d.driver, _spec) + if err != nil && sqlgraph.IsConstraintError(err) { + err = &ConstraintError{msg: err.Error(), wrap: err} + } + _d.mutation.done = true + return affected, err +} + +// RuntimeBrokerDeleteOne is the builder for deleting a single RuntimeBroker entity. +type RuntimeBrokerDeleteOne struct { + _d *RuntimeBrokerDelete +} + +// Where appends a list predicates to the RuntimeBrokerDelete builder. +func (_d *RuntimeBrokerDeleteOne) Where(ps ...predicate.RuntimeBroker) *RuntimeBrokerDeleteOne { + _d._d.mutation.Where(ps...) + return _d +} + +// Exec executes the deletion query. +func (_d *RuntimeBrokerDeleteOne) Exec(ctx context.Context) error { + n, err := _d._d.Exec(ctx) + switch { + case err != nil: + return err + case n == 0: + return &NotFoundError{runtimebroker.Label} + default: + return nil + } +} + +// ExecX is like Exec, but panics if an error occurs. +func (_d *RuntimeBrokerDeleteOne) ExecX(ctx context.Context) { + if err := _d.Exec(ctx); err != nil { + panic(err) + } +} diff --git a/pkg/ent/runtimebroker_query.go b/pkg/ent/runtimebroker_query.go new file mode 100644 index 000000000..ae30aa2e7 --- /dev/null +++ b/pkg/ent/runtimebroker_query.go @@ -0,0 +1,565 @@ +// Code generated by ent, DO NOT EDIT. + +package ent + +import ( + "context" + "fmt" + "math" + + "entgo.io/ent" + "entgo.io/ent/dialect" + "entgo.io/ent/dialect/sql" + "entgo.io/ent/dialect/sql/sqlgraph" + "entgo.io/ent/schema/field" + "github.com/GoogleCloudPlatform/scion/pkg/ent/predicate" + "github.com/GoogleCloudPlatform/scion/pkg/ent/runtimebroker" + "github.com/google/uuid" +) + +// RuntimeBrokerQuery is the builder for querying RuntimeBroker entities. +type RuntimeBrokerQuery struct { + config + ctx *QueryContext + order []runtimebroker.OrderOption + inters []Interceptor + predicates []predicate.RuntimeBroker + modifiers []func(*sql.Selector) + // intermediate query (i.e. traversal path). + sql *sql.Selector + path func(context.Context) (*sql.Selector, error) +} + +// Where adds a new predicate for the RuntimeBrokerQuery builder. +func (_q *RuntimeBrokerQuery) Where(ps ...predicate.RuntimeBroker) *RuntimeBrokerQuery { + _q.predicates = append(_q.predicates, ps...) + return _q +} + +// Limit the number of records to be returned by this query. +func (_q *RuntimeBrokerQuery) Limit(limit int) *RuntimeBrokerQuery { + _q.ctx.Limit = &limit + return _q +} + +// Offset to start from. +func (_q *RuntimeBrokerQuery) Offset(offset int) *RuntimeBrokerQuery { + _q.ctx.Offset = &offset + return _q +} + +// Unique configures the query builder to filter duplicate records on query. +// By default, unique is set to true, and can be disabled using this method. +func (_q *RuntimeBrokerQuery) Unique(unique bool) *RuntimeBrokerQuery { + _q.ctx.Unique = &unique + return _q +} + +// Order specifies how the records should be ordered. +func (_q *RuntimeBrokerQuery) Order(o ...runtimebroker.OrderOption) *RuntimeBrokerQuery { + _q.order = append(_q.order, o...) + return _q +} + +// First returns the first RuntimeBroker entity from the query. +// Returns a *NotFoundError when no RuntimeBroker was found. +func (_q *RuntimeBrokerQuery) First(ctx context.Context) (*RuntimeBroker, error) { + nodes, err := _q.Limit(1).All(setContextOp(ctx, _q.ctx, ent.OpQueryFirst)) + if err != nil { + return nil, err + } + if len(nodes) == 0 { + return nil, &NotFoundError{runtimebroker.Label} + } + return nodes[0], nil +} + +// FirstX is like First, but panics if an error occurs. +func (_q *RuntimeBrokerQuery) FirstX(ctx context.Context) *RuntimeBroker { + node, err := _q.First(ctx) + if err != nil && !IsNotFound(err) { + panic(err) + } + return node +} + +// FirstID returns the first RuntimeBroker ID from the query. +// Returns a *NotFoundError when no RuntimeBroker ID was found. +func (_q *RuntimeBrokerQuery) FirstID(ctx context.Context) (id uuid.UUID, err error) { + var ids []uuid.UUID + if ids, err = _q.Limit(1).IDs(setContextOp(ctx, _q.ctx, ent.OpQueryFirstID)); err != nil { + return + } + if len(ids) == 0 { + err = &NotFoundError{runtimebroker.Label} + return + } + return ids[0], nil +} + +// FirstIDX is like FirstID, but panics if an error occurs. +func (_q *RuntimeBrokerQuery) FirstIDX(ctx context.Context) uuid.UUID { + id, err := _q.FirstID(ctx) + if err != nil && !IsNotFound(err) { + panic(err) + } + return id +} + +// Only returns a single RuntimeBroker entity found by the query, ensuring it only returns one. +// Returns a *NotSingularError when more than one RuntimeBroker entity is found. +// Returns a *NotFoundError when no RuntimeBroker entities are found. +func (_q *RuntimeBrokerQuery) Only(ctx context.Context) (*RuntimeBroker, error) { + nodes, err := _q.Limit(2).All(setContextOp(ctx, _q.ctx, ent.OpQueryOnly)) + if err != nil { + return nil, err + } + switch len(nodes) { + case 1: + return nodes[0], nil + case 0: + return nil, &NotFoundError{runtimebroker.Label} + default: + return nil, &NotSingularError{runtimebroker.Label} + } +} + +// OnlyX is like Only, but panics if an error occurs. +func (_q *RuntimeBrokerQuery) OnlyX(ctx context.Context) *RuntimeBroker { + node, err := _q.Only(ctx) + if err != nil { + panic(err) + } + return node +} + +// OnlyID is like Only, but returns the only RuntimeBroker ID in the query. +// Returns a *NotSingularError when more than one RuntimeBroker ID is found. +// Returns a *NotFoundError when no entities are found. +func (_q *RuntimeBrokerQuery) OnlyID(ctx context.Context) (id uuid.UUID, err error) { + var ids []uuid.UUID + if ids, err = _q.Limit(2).IDs(setContextOp(ctx, _q.ctx, ent.OpQueryOnlyID)); err != nil { + return + } + switch len(ids) { + case 1: + id = ids[0] + case 0: + err = &NotFoundError{runtimebroker.Label} + default: + err = &NotSingularError{runtimebroker.Label} + } + return +} + +// OnlyIDX is like OnlyID, but panics if an error occurs. +func (_q *RuntimeBrokerQuery) OnlyIDX(ctx context.Context) uuid.UUID { + id, err := _q.OnlyID(ctx) + if err != nil { + panic(err) + } + return id +} + +// All executes the query and returns a list of RuntimeBrokers. +func (_q *RuntimeBrokerQuery) All(ctx context.Context) ([]*RuntimeBroker, error) { + ctx = setContextOp(ctx, _q.ctx, ent.OpQueryAll) + if err := _q.prepareQuery(ctx); err != nil { + return nil, err + } + qr := querierAll[[]*RuntimeBroker, *RuntimeBrokerQuery]() + return withInterceptors[[]*RuntimeBroker](ctx, _q, qr, _q.inters) +} + +// AllX is like All, but panics if an error occurs. +func (_q *RuntimeBrokerQuery) AllX(ctx context.Context) []*RuntimeBroker { + nodes, err := _q.All(ctx) + if err != nil { + panic(err) + } + return nodes +} + +// IDs executes the query and returns a list of RuntimeBroker IDs. +func (_q *RuntimeBrokerQuery) IDs(ctx context.Context) (ids []uuid.UUID, err error) { + if _q.ctx.Unique == nil && _q.path != nil { + _q.Unique(true) + } + ctx = setContextOp(ctx, _q.ctx, ent.OpQueryIDs) + if err = _q.Select(runtimebroker.FieldID).Scan(ctx, &ids); err != nil { + return nil, err + } + return ids, nil +} + +// IDsX is like IDs, but panics if an error occurs. +func (_q *RuntimeBrokerQuery) IDsX(ctx context.Context) []uuid.UUID { + ids, err := _q.IDs(ctx) + if err != nil { + panic(err) + } + return ids +} + +// Count returns the count of the given query. +func (_q *RuntimeBrokerQuery) Count(ctx context.Context) (int, error) { + ctx = setContextOp(ctx, _q.ctx, ent.OpQueryCount) + if err := _q.prepareQuery(ctx); err != nil { + return 0, err + } + return withInterceptors[int](ctx, _q, querierCount[*RuntimeBrokerQuery](), _q.inters) +} + +// CountX is like Count, but panics if an error occurs. +func (_q *RuntimeBrokerQuery) CountX(ctx context.Context) int { + count, err := _q.Count(ctx) + if err != nil { + panic(err) + } + return count +} + +// Exist returns true if the query has elements in the graph. +func (_q *RuntimeBrokerQuery) Exist(ctx context.Context) (bool, error) { + ctx = setContextOp(ctx, _q.ctx, ent.OpQueryExist) + switch _, err := _q.FirstID(ctx); { + case IsNotFound(err): + return false, nil + case err != nil: + return false, fmt.Errorf("ent: check existence: %w", err) + default: + return true, nil + } +} + +// ExistX is like Exist, but panics if an error occurs. +func (_q *RuntimeBrokerQuery) ExistX(ctx context.Context) bool { + exist, err := _q.Exist(ctx) + if err != nil { + panic(err) + } + return exist +} + +// Clone returns a duplicate of the RuntimeBrokerQuery builder, including all associated steps. It can be +// used to prepare common query builders and use them differently after the clone is made. +func (_q *RuntimeBrokerQuery) Clone() *RuntimeBrokerQuery { + if _q == nil { + return nil + } + return &RuntimeBrokerQuery{ + config: _q.config, + ctx: _q.ctx.Clone(), + order: append([]runtimebroker.OrderOption{}, _q.order...), + inters: append([]Interceptor{}, _q.inters...), + predicates: append([]predicate.RuntimeBroker{}, _q.predicates...), + // clone intermediate query. + sql: _q.sql.Clone(), + path: _q.path, + } +} + +// GroupBy is used to group vertices by one or more fields/columns. +// It is often used with aggregate functions, like: count, max, mean, min, sum. +// +// Example: +// +// var v []struct { +// Name string `json:"name,omitempty"` +// Count int `json:"count,omitempty"` +// } +// +// client.RuntimeBroker.Query(). +// GroupBy(runtimebroker.FieldName). +// Aggregate(ent.Count()). +// Scan(ctx, &v) +func (_q *RuntimeBrokerQuery) GroupBy(field string, fields ...string) *RuntimeBrokerGroupBy { + _q.ctx.Fields = append([]string{field}, fields...) + grbuild := &RuntimeBrokerGroupBy{build: _q} + grbuild.flds = &_q.ctx.Fields + grbuild.label = runtimebroker.Label + grbuild.scan = grbuild.Scan + return grbuild +} + +// Select allows the selection one or more fields/columns for the given query, +// instead of selecting all fields in the entity. +// +// Example: +// +// var v []struct { +// Name string `json:"name,omitempty"` +// } +// +// client.RuntimeBroker.Query(). +// Select(runtimebroker.FieldName). +// Scan(ctx, &v) +func (_q *RuntimeBrokerQuery) Select(fields ...string) *RuntimeBrokerSelect { + _q.ctx.Fields = append(_q.ctx.Fields, fields...) + sbuild := &RuntimeBrokerSelect{RuntimeBrokerQuery: _q} + sbuild.label = runtimebroker.Label + sbuild.flds, sbuild.scan = &_q.ctx.Fields, sbuild.Scan + return sbuild +} + +// Aggregate returns a RuntimeBrokerSelect configured with the given aggregations. +func (_q *RuntimeBrokerQuery) Aggregate(fns ...AggregateFunc) *RuntimeBrokerSelect { + return _q.Select().Aggregate(fns...) +} + +func (_q *RuntimeBrokerQuery) prepareQuery(ctx context.Context) error { + for _, inter := range _q.inters { + if inter == nil { + return fmt.Errorf("ent: uninitialized interceptor (forgotten import ent/runtime?)") + } + if trv, ok := inter.(Traverser); ok { + if err := trv.Traverse(ctx, _q); err != nil { + return err + } + } + } + for _, f := range _q.ctx.Fields { + if !runtimebroker.ValidColumn(f) { + return &ValidationError{Name: f, err: fmt.Errorf("ent: invalid field %q for query", f)} + } + } + if _q.path != nil { + prev, err := _q.path(ctx) + if err != nil { + return err + } + _q.sql = prev + } + return nil +} + +func (_q *RuntimeBrokerQuery) sqlAll(ctx context.Context, hooks ...queryHook) ([]*RuntimeBroker, error) { + var ( + nodes = []*RuntimeBroker{} + _spec = _q.querySpec() + ) + _spec.ScanValues = func(columns []string) ([]any, error) { + return (*RuntimeBroker).scanValues(nil, columns) + } + _spec.Assign = func(columns []string, values []any) error { + node := &RuntimeBroker{config: _q.config} + nodes = append(nodes, node) + return node.assignValues(columns, values) + } + if len(_q.modifiers) > 0 { + _spec.Modifiers = _q.modifiers + } + for i := range hooks { + hooks[i](ctx, _spec) + } + if err := sqlgraph.QueryNodes(ctx, _q.driver, _spec); err != nil { + return nil, err + } + if len(nodes) == 0 { + return nodes, nil + } + return nodes, nil +} + +func (_q *RuntimeBrokerQuery) sqlCount(ctx context.Context) (int, error) { + _spec := _q.querySpec() + if len(_q.modifiers) > 0 { + _spec.Modifiers = _q.modifiers + } + _spec.Node.Columns = _q.ctx.Fields + if len(_q.ctx.Fields) > 0 { + _spec.Unique = _q.ctx.Unique != nil && *_q.ctx.Unique + } + return sqlgraph.CountNodes(ctx, _q.driver, _spec) +} + +func (_q *RuntimeBrokerQuery) querySpec() *sqlgraph.QuerySpec { + _spec := sqlgraph.NewQuerySpec(runtimebroker.Table, runtimebroker.Columns, sqlgraph.NewFieldSpec(runtimebroker.FieldID, field.TypeUUID)) + _spec.From = _q.sql + if unique := _q.ctx.Unique; unique != nil { + _spec.Unique = *unique + } else if _q.path != nil { + _spec.Unique = true + } + if fields := _q.ctx.Fields; len(fields) > 0 { + _spec.Node.Columns = make([]string, 0, len(fields)) + _spec.Node.Columns = append(_spec.Node.Columns, runtimebroker.FieldID) + for i := range fields { + if fields[i] != runtimebroker.FieldID { + _spec.Node.Columns = append(_spec.Node.Columns, fields[i]) + } + } + } + if ps := _q.predicates; len(ps) > 0 { + _spec.Predicate = func(selector *sql.Selector) { + for i := range ps { + ps[i](selector) + } + } + } + if limit := _q.ctx.Limit; limit != nil { + _spec.Limit = *limit + } + if offset := _q.ctx.Offset; offset != nil { + _spec.Offset = *offset + } + if ps := _q.order; len(ps) > 0 { + _spec.Order = func(selector *sql.Selector) { + for i := range ps { + ps[i](selector) + } + } + } + return _spec +} + +func (_q *RuntimeBrokerQuery) sqlQuery(ctx context.Context) *sql.Selector { + builder := sql.Dialect(_q.driver.Dialect()) + t1 := builder.Table(runtimebroker.Table) + columns := _q.ctx.Fields + if len(columns) == 0 { + columns = runtimebroker.Columns + } + selector := builder.Select(t1.Columns(columns...)...).From(t1) + if _q.sql != nil { + selector = _q.sql + selector.Select(selector.Columns(columns...)...) + } + if _q.ctx.Unique != nil && *_q.ctx.Unique { + selector.Distinct() + } + for _, m := range _q.modifiers { + m(selector) + } + for _, p := range _q.predicates { + p(selector) + } + for _, p := range _q.order { + p(selector) + } + if offset := _q.ctx.Offset; offset != nil { + // limit is mandatory for offset clause. We start + // with default value, and override it below if needed. + selector.Offset(*offset).Limit(math.MaxInt32) + } + if limit := _q.ctx.Limit; limit != nil { + selector.Limit(*limit) + } + return selector +} + +// ForUpdate locks the selected rows against concurrent updates, and prevent them from being +// updated, deleted or "selected ... for update" by other sessions, until the transaction is +// either committed or rolled-back. +func (_q *RuntimeBrokerQuery) ForUpdate(opts ...sql.LockOption) *RuntimeBrokerQuery { + if _q.driver.Dialect() == dialect.Postgres { + _q.Unique(false) + } + _q.modifiers = append(_q.modifiers, func(s *sql.Selector) { + s.ForUpdate(opts...) + }) + return _q +} + +// ForShare behaves similarly to ForUpdate, except that it acquires a shared mode lock +// on any rows that are read. Other sessions can read the rows, but cannot modify them +// until your transaction commits. +func (_q *RuntimeBrokerQuery) ForShare(opts ...sql.LockOption) *RuntimeBrokerQuery { + if _q.driver.Dialect() == dialect.Postgres { + _q.Unique(false) + } + _q.modifiers = append(_q.modifiers, func(s *sql.Selector) { + s.ForShare(opts...) + }) + return _q +} + +// RuntimeBrokerGroupBy is the group-by builder for RuntimeBroker entities. +type RuntimeBrokerGroupBy struct { + selector + build *RuntimeBrokerQuery +} + +// Aggregate adds the given aggregation functions to the group-by query. +func (_g *RuntimeBrokerGroupBy) Aggregate(fns ...AggregateFunc) *RuntimeBrokerGroupBy { + _g.fns = append(_g.fns, fns...) + return _g +} + +// Scan applies the selector query and scans the result into the given value. +func (_g *RuntimeBrokerGroupBy) Scan(ctx context.Context, v any) error { + ctx = setContextOp(ctx, _g.build.ctx, ent.OpQueryGroupBy) + if err := _g.build.prepareQuery(ctx); err != nil { + return err + } + return scanWithInterceptors[*RuntimeBrokerQuery, *RuntimeBrokerGroupBy](ctx, _g.build, _g, _g.build.inters, v) +} + +func (_g *RuntimeBrokerGroupBy) sqlScan(ctx context.Context, root *RuntimeBrokerQuery, v any) error { + selector := root.sqlQuery(ctx).Select() + aggregation := make([]string, 0, len(_g.fns)) + for _, fn := range _g.fns { + aggregation = append(aggregation, fn(selector)) + } + if len(selector.SelectedColumns()) == 0 { + columns := make([]string, 0, len(*_g.flds)+len(_g.fns)) + for _, f := range *_g.flds { + columns = append(columns, selector.C(f)) + } + columns = append(columns, aggregation...) + selector.Select(columns...) + } + selector.GroupBy(selector.Columns(*_g.flds...)...) + if err := selector.Err(); err != nil { + return err + } + rows := &sql.Rows{} + query, args := selector.Query() + if err := _g.build.driver.Query(ctx, query, args, rows); err != nil { + return err + } + defer rows.Close() + return sql.ScanSlice(rows, v) +} + +// RuntimeBrokerSelect is the builder for selecting fields of RuntimeBroker entities. +type RuntimeBrokerSelect struct { + *RuntimeBrokerQuery + selector +} + +// Aggregate adds the given aggregation functions to the selector query. +func (_s *RuntimeBrokerSelect) Aggregate(fns ...AggregateFunc) *RuntimeBrokerSelect { + _s.fns = append(_s.fns, fns...) + return _s +} + +// Scan applies the selector query and scans the result into the given value. +func (_s *RuntimeBrokerSelect) Scan(ctx context.Context, v any) error { + ctx = setContextOp(ctx, _s.ctx, ent.OpQuerySelect) + if err := _s.prepareQuery(ctx); err != nil { + return err + } + return scanWithInterceptors[*RuntimeBrokerQuery, *RuntimeBrokerSelect](ctx, _s.RuntimeBrokerQuery, _s, _s.inters, v) +} + +func (_s *RuntimeBrokerSelect) sqlScan(ctx context.Context, root *RuntimeBrokerQuery, v any) error { + selector := root.sqlQuery(ctx) + aggregation := make([]string, 0, len(_s.fns)) + for _, fn := range _s.fns { + aggregation = append(aggregation, fn(selector)) + } + switch n := len(*_s.selector.flds); { + case n == 0 && len(aggregation) > 0: + selector.Select(aggregation...) + case n != 0 && len(aggregation) > 0: + selector.AppendSelect(aggregation...) + } + rows := &sql.Rows{} + query, args := selector.Query() + if err := _s.driver.Query(ctx, query, args, rows); err != nil { + return err + } + defer rows.Close() + return sql.ScanSlice(rows, v) +} diff --git a/pkg/ent/runtimebroker_update.go b/pkg/ent/runtimebroker_update.go new file mode 100644 index 000000000..ebbd7ee22 --- /dev/null +++ b/pkg/ent/runtimebroker_update.go @@ -0,0 +1,1234 @@ +// Code generated by ent, DO NOT EDIT. + +package ent + +import ( + "context" + "errors" + "fmt" + "time" + + "entgo.io/ent/dialect/sql" + "entgo.io/ent/dialect/sql/sqlgraph" + "entgo.io/ent/schema/field" + "github.com/GoogleCloudPlatform/scion/pkg/ent/predicate" + "github.com/GoogleCloudPlatform/scion/pkg/ent/runtimebroker" +) + +// RuntimeBrokerUpdate is the builder for updating RuntimeBroker entities. +type RuntimeBrokerUpdate struct { + config + hooks []Hook + mutation *RuntimeBrokerMutation +} + +// Where appends a list predicates to the RuntimeBrokerUpdate builder. +func (_u *RuntimeBrokerUpdate) Where(ps ...predicate.RuntimeBroker) *RuntimeBrokerUpdate { + _u.mutation.Where(ps...) + return _u +} + +// SetName sets the "name" field. +func (_u *RuntimeBrokerUpdate) SetName(v string) *RuntimeBrokerUpdate { + _u.mutation.SetName(v) + return _u +} + +// SetNillableName sets the "name" field if the given value is not nil. +func (_u *RuntimeBrokerUpdate) SetNillableName(v *string) *RuntimeBrokerUpdate { + if v != nil { + _u.SetName(*v) + } + return _u +} + +// SetSlug sets the "slug" field. +func (_u *RuntimeBrokerUpdate) SetSlug(v string) *RuntimeBrokerUpdate { + _u.mutation.SetSlug(v) + return _u +} + +// SetNillableSlug sets the "slug" field if the given value is not nil. +func (_u *RuntimeBrokerUpdate) SetNillableSlug(v *string) *RuntimeBrokerUpdate { + if v != nil { + _u.SetSlug(*v) + } + return _u +} + +// SetType sets the "type" field. +func (_u *RuntimeBrokerUpdate) SetType(v string) *RuntimeBrokerUpdate { + _u.mutation.SetType(v) + return _u +} + +// SetNillableType sets the "type" field if the given value is not nil. +func (_u *RuntimeBrokerUpdate) SetNillableType(v *string) *RuntimeBrokerUpdate { + if v != nil { + _u.SetType(*v) + } + return _u +} + +// ClearType clears the value of the "type" field. +func (_u *RuntimeBrokerUpdate) ClearType() *RuntimeBrokerUpdate { + _u.mutation.ClearType() + return _u +} + +// SetMode sets the "mode" field. +func (_u *RuntimeBrokerUpdate) SetMode(v string) *RuntimeBrokerUpdate { + _u.mutation.SetMode(v) + return _u +} + +// SetNillableMode sets the "mode" field if the given value is not nil. +func (_u *RuntimeBrokerUpdate) SetNillableMode(v *string) *RuntimeBrokerUpdate { + if v != nil { + _u.SetMode(*v) + } + return _u +} + +// SetVersion sets the "version" field. +func (_u *RuntimeBrokerUpdate) SetVersion(v string) *RuntimeBrokerUpdate { + _u.mutation.SetVersion(v) + return _u +} + +// SetNillableVersion sets the "version" field if the given value is not nil. +func (_u *RuntimeBrokerUpdate) SetNillableVersion(v *string) *RuntimeBrokerUpdate { + if v != nil { + _u.SetVersion(*v) + } + return _u +} + +// ClearVersion clears the value of the "version" field. +func (_u *RuntimeBrokerUpdate) ClearVersion() *RuntimeBrokerUpdate { + _u.mutation.ClearVersion() + return _u +} + +// SetLockVersion sets the "lock_version" field. +func (_u *RuntimeBrokerUpdate) SetLockVersion(v int64) *RuntimeBrokerUpdate { + _u.mutation.ResetLockVersion() + _u.mutation.SetLockVersion(v) + return _u +} + +// SetNillableLockVersion sets the "lock_version" field if the given value is not nil. +func (_u *RuntimeBrokerUpdate) SetNillableLockVersion(v *int64) *RuntimeBrokerUpdate { + if v != nil { + _u.SetLockVersion(*v) + } + return _u +} + +// AddLockVersion adds value to the "lock_version" field. +func (_u *RuntimeBrokerUpdate) AddLockVersion(v int64) *RuntimeBrokerUpdate { + _u.mutation.AddLockVersion(v) + return _u +} + +// SetStatus sets the "status" field. +func (_u *RuntimeBrokerUpdate) SetStatus(v string) *RuntimeBrokerUpdate { + _u.mutation.SetStatus(v) + return _u +} + +// SetNillableStatus sets the "status" field if the given value is not nil. +func (_u *RuntimeBrokerUpdate) SetNillableStatus(v *string) *RuntimeBrokerUpdate { + if v != nil { + _u.SetStatus(*v) + } + return _u +} + +// SetConnectionState sets the "connection_state" field. +func (_u *RuntimeBrokerUpdate) SetConnectionState(v string) *RuntimeBrokerUpdate { + _u.mutation.SetConnectionState(v) + return _u +} + +// SetNillableConnectionState sets the "connection_state" field if the given value is not nil. +func (_u *RuntimeBrokerUpdate) SetNillableConnectionState(v *string) *RuntimeBrokerUpdate { + if v != nil { + _u.SetConnectionState(*v) + } + return _u +} + +// SetLastHeartbeat sets the "last_heartbeat" field. +func (_u *RuntimeBrokerUpdate) SetLastHeartbeat(v time.Time) *RuntimeBrokerUpdate { + _u.mutation.SetLastHeartbeat(v) + return _u +} + +// SetNillableLastHeartbeat sets the "last_heartbeat" field if the given value is not nil. +func (_u *RuntimeBrokerUpdate) SetNillableLastHeartbeat(v *time.Time) *RuntimeBrokerUpdate { + if v != nil { + _u.SetLastHeartbeat(*v) + } + return _u +} + +// ClearLastHeartbeat clears the value of the "last_heartbeat" field. +func (_u *RuntimeBrokerUpdate) ClearLastHeartbeat() *RuntimeBrokerUpdate { + _u.mutation.ClearLastHeartbeat() + return _u +} + +// SetCapabilities sets the "capabilities" field. +func (_u *RuntimeBrokerUpdate) SetCapabilities(v string) *RuntimeBrokerUpdate { + _u.mutation.SetCapabilities(v) + return _u +} + +// SetNillableCapabilities sets the "capabilities" field if the given value is not nil. +func (_u *RuntimeBrokerUpdate) SetNillableCapabilities(v *string) *RuntimeBrokerUpdate { + if v != nil { + _u.SetCapabilities(*v) + } + return _u +} + +// ClearCapabilities clears the value of the "capabilities" field. +func (_u *RuntimeBrokerUpdate) ClearCapabilities() *RuntimeBrokerUpdate { + _u.mutation.ClearCapabilities() + return _u +} + +// SetSupportedHarnesses sets the "supported_harnesses" field. +func (_u *RuntimeBrokerUpdate) SetSupportedHarnesses(v string) *RuntimeBrokerUpdate { + _u.mutation.SetSupportedHarnesses(v) + return _u +} + +// SetNillableSupportedHarnesses sets the "supported_harnesses" field if the given value is not nil. +func (_u *RuntimeBrokerUpdate) SetNillableSupportedHarnesses(v *string) *RuntimeBrokerUpdate { + if v != nil { + _u.SetSupportedHarnesses(*v) + } + return _u +} + +// ClearSupportedHarnesses clears the value of the "supported_harnesses" field. +func (_u *RuntimeBrokerUpdate) ClearSupportedHarnesses() *RuntimeBrokerUpdate { + _u.mutation.ClearSupportedHarnesses() + return _u +} + +// SetResources sets the "resources" field. +func (_u *RuntimeBrokerUpdate) SetResources(v string) *RuntimeBrokerUpdate { + _u.mutation.SetResources(v) + return _u +} + +// SetNillableResources sets the "resources" field if the given value is not nil. +func (_u *RuntimeBrokerUpdate) SetNillableResources(v *string) *RuntimeBrokerUpdate { + if v != nil { + _u.SetResources(*v) + } + return _u +} + +// ClearResources clears the value of the "resources" field. +func (_u *RuntimeBrokerUpdate) ClearResources() *RuntimeBrokerUpdate { + _u.mutation.ClearResources() + return _u +} + +// SetRuntimes sets the "runtimes" field. +func (_u *RuntimeBrokerUpdate) SetRuntimes(v string) *RuntimeBrokerUpdate { + _u.mutation.SetRuntimes(v) + return _u +} + +// SetNillableRuntimes sets the "runtimes" field if the given value is not nil. +func (_u *RuntimeBrokerUpdate) SetNillableRuntimes(v *string) *RuntimeBrokerUpdate { + if v != nil { + _u.SetRuntimes(*v) + } + return _u +} + +// ClearRuntimes clears the value of the "runtimes" field. +func (_u *RuntimeBrokerUpdate) ClearRuntimes() *RuntimeBrokerUpdate { + _u.mutation.ClearRuntimes() + return _u +} + +// SetLabels sets the "labels" field. +func (_u *RuntimeBrokerUpdate) SetLabels(v string) *RuntimeBrokerUpdate { + _u.mutation.SetLabels(v) + return _u +} + +// SetNillableLabels sets the "labels" field if the given value is not nil. +func (_u *RuntimeBrokerUpdate) SetNillableLabels(v *string) *RuntimeBrokerUpdate { + if v != nil { + _u.SetLabels(*v) + } + return _u +} + +// ClearLabels clears the value of the "labels" field. +func (_u *RuntimeBrokerUpdate) ClearLabels() *RuntimeBrokerUpdate { + _u.mutation.ClearLabels() + return _u +} + +// SetAnnotations sets the "annotations" field. +func (_u *RuntimeBrokerUpdate) SetAnnotations(v string) *RuntimeBrokerUpdate { + _u.mutation.SetAnnotations(v) + return _u +} + +// SetNillableAnnotations sets the "annotations" field if the given value is not nil. +func (_u *RuntimeBrokerUpdate) SetNillableAnnotations(v *string) *RuntimeBrokerUpdate { + if v != nil { + _u.SetAnnotations(*v) + } + return _u +} + +// ClearAnnotations clears the value of the "annotations" field. +func (_u *RuntimeBrokerUpdate) ClearAnnotations() *RuntimeBrokerUpdate { + _u.mutation.ClearAnnotations() + return _u +} + +// SetEndpoint sets the "endpoint" field. +func (_u *RuntimeBrokerUpdate) SetEndpoint(v string) *RuntimeBrokerUpdate { + _u.mutation.SetEndpoint(v) + return _u +} + +// SetNillableEndpoint sets the "endpoint" field if the given value is not nil. +func (_u *RuntimeBrokerUpdate) SetNillableEndpoint(v *string) *RuntimeBrokerUpdate { + if v != nil { + _u.SetEndpoint(*v) + } + return _u +} + +// ClearEndpoint clears the value of the "endpoint" field. +func (_u *RuntimeBrokerUpdate) ClearEndpoint() *RuntimeBrokerUpdate { + _u.mutation.ClearEndpoint() + return _u +} + +// SetCreatedBy sets the "created_by" field. +func (_u *RuntimeBrokerUpdate) SetCreatedBy(v string) *RuntimeBrokerUpdate { + _u.mutation.SetCreatedBy(v) + return _u +} + +// SetNillableCreatedBy sets the "created_by" field if the given value is not nil. +func (_u *RuntimeBrokerUpdate) SetNillableCreatedBy(v *string) *RuntimeBrokerUpdate { + if v != nil { + _u.SetCreatedBy(*v) + } + return _u +} + +// ClearCreatedBy clears the value of the "created_by" field. +func (_u *RuntimeBrokerUpdate) ClearCreatedBy() *RuntimeBrokerUpdate { + _u.mutation.ClearCreatedBy() + return _u +} + +// SetAutoProvide sets the "auto_provide" field. +func (_u *RuntimeBrokerUpdate) SetAutoProvide(v bool) *RuntimeBrokerUpdate { + _u.mutation.SetAutoProvide(v) + return _u +} + +// SetNillableAutoProvide sets the "auto_provide" field if the given value is not nil. +func (_u *RuntimeBrokerUpdate) SetNillableAutoProvide(v *bool) *RuntimeBrokerUpdate { + if v != nil { + _u.SetAutoProvide(*v) + } + return _u +} + +// SetConnectedHubID sets the "connected_hub_id" field. +func (_u *RuntimeBrokerUpdate) SetConnectedHubID(v string) *RuntimeBrokerUpdate { + _u.mutation.SetConnectedHubID(v) + return _u +} + +// SetNillableConnectedHubID sets the "connected_hub_id" field if the given value is not nil. +func (_u *RuntimeBrokerUpdate) SetNillableConnectedHubID(v *string) *RuntimeBrokerUpdate { + if v != nil { + _u.SetConnectedHubID(*v) + } + return _u +} + +// ClearConnectedHubID clears the value of the "connected_hub_id" field. +func (_u *RuntimeBrokerUpdate) ClearConnectedHubID() *RuntimeBrokerUpdate { + _u.mutation.ClearConnectedHubID() + return _u +} + +// SetConnectedSessionID sets the "connected_session_id" field. +func (_u *RuntimeBrokerUpdate) SetConnectedSessionID(v string) *RuntimeBrokerUpdate { + _u.mutation.SetConnectedSessionID(v) + return _u +} + +// SetNillableConnectedSessionID sets the "connected_session_id" field if the given value is not nil. +func (_u *RuntimeBrokerUpdate) SetNillableConnectedSessionID(v *string) *RuntimeBrokerUpdate { + if v != nil { + _u.SetConnectedSessionID(*v) + } + return _u +} + +// ClearConnectedSessionID clears the value of the "connected_session_id" field. +func (_u *RuntimeBrokerUpdate) ClearConnectedSessionID() *RuntimeBrokerUpdate { + _u.mutation.ClearConnectedSessionID() + return _u +} + +// SetConnectedAt sets the "connected_at" field. +func (_u *RuntimeBrokerUpdate) SetConnectedAt(v time.Time) *RuntimeBrokerUpdate { + _u.mutation.SetConnectedAt(v) + return _u +} + +// SetNillableConnectedAt sets the "connected_at" field if the given value is not nil. +func (_u *RuntimeBrokerUpdate) SetNillableConnectedAt(v *time.Time) *RuntimeBrokerUpdate { + if v != nil { + _u.SetConnectedAt(*v) + } + return _u +} + +// ClearConnectedAt clears the value of the "connected_at" field. +func (_u *RuntimeBrokerUpdate) ClearConnectedAt() *RuntimeBrokerUpdate { + _u.mutation.ClearConnectedAt() + return _u +} + +// SetUpdated sets the "updated" field. +func (_u *RuntimeBrokerUpdate) SetUpdated(v time.Time) *RuntimeBrokerUpdate { + _u.mutation.SetUpdated(v) + return _u +} + +// Mutation returns the RuntimeBrokerMutation object of the builder. +func (_u *RuntimeBrokerUpdate) Mutation() *RuntimeBrokerMutation { + return _u.mutation +} + +// Save executes the query and returns the number of nodes affected by the update operation. +func (_u *RuntimeBrokerUpdate) Save(ctx context.Context) (int, error) { + _u.defaults() + return withHooks(ctx, _u.sqlSave, _u.mutation, _u.hooks) +} + +// SaveX is like Save, but panics if an error occurs. +func (_u *RuntimeBrokerUpdate) SaveX(ctx context.Context) int { + affected, err := _u.Save(ctx) + if err != nil { + panic(err) + } + return affected +} + +// Exec executes the query. +func (_u *RuntimeBrokerUpdate) Exec(ctx context.Context) error { + _, err := _u.Save(ctx) + return err +} + +// ExecX is like Exec, but panics if an error occurs. +func (_u *RuntimeBrokerUpdate) ExecX(ctx context.Context) { + if err := _u.Exec(ctx); err != nil { + panic(err) + } +} + +// defaults sets the default values of the builder before save. +func (_u *RuntimeBrokerUpdate) defaults() { + if _, ok := _u.mutation.Updated(); !ok { + v := runtimebroker.UpdateDefaultUpdated() + _u.mutation.SetUpdated(v) + } +} + +// check runs all checks and user-defined validators on the builder. +func (_u *RuntimeBrokerUpdate) check() error { + if v, ok := _u.mutation.Name(); ok { + if err := runtimebroker.NameValidator(v); err != nil { + return &ValidationError{Name: "name", err: fmt.Errorf(`ent: validator failed for field "RuntimeBroker.name": %w`, err)} + } + } + if v, ok := _u.mutation.Slug(); ok { + if err := runtimebroker.SlugValidator(v); err != nil { + return &ValidationError{Name: "slug", err: fmt.Errorf(`ent: validator failed for field "RuntimeBroker.slug": %w`, err)} + } + } + return nil +} + +func (_u *RuntimeBrokerUpdate) sqlSave(ctx context.Context) (_node int, err error) { + if err := _u.check(); err != nil { + return _node, err + } + _spec := sqlgraph.NewUpdateSpec(runtimebroker.Table, runtimebroker.Columns, sqlgraph.NewFieldSpec(runtimebroker.FieldID, field.TypeUUID)) + if ps := _u.mutation.predicates; len(ps) > 0 { + _spec.Predicate = func(selector *sql.Selector) { + for i := range ps { + ps[i](selector) + } + } + } + if value, ok := _u.mutation.Name(); ok { + _spec.SetField(runtimebroker.FieldName, field.TypeString, value) + } + if value, ok := _u.mutation.Slug(); ok { + _spec.SetField(runtimebroker.FieldSlug, field.TypeString, value) + } + if value, ok := _u.mutation.GetType(); ok { + _spec.SetField(runtimebroker.FieldType, field.TypeString, value) + } + if _u.mutation.TypeCleared() { + _spec.ClearField(runtimebroker.FieldType, field.TypeString) + } + if value, ok := _u.mutation.Mode(); ok { + _spec.SetField(runtimebroker.FieldMode, field.TypeString, value) + } + if value, ok := _u.mutation.Version(); ok { + _spec.SetField(runtimebroker.FieldVersion, field.TypeString, value) + } + if _u.mutation.VersionCleared() { + _spec.ClearField(runtimebroker.FieldVersion, field.TypeString) + } + if value, ok := _u.mutation.LockVersion(); ok { + _spec.SetField(runtimebroker.FieldLockVersion, field.TypeInt64, value) + } + if value, ok := _u.mutation.AddedLockVersion(); ok { + _spec.AddField(runtimebroker.FieldLockVersion, field.TypeInt64, value) + } + if value, ok := _u.mutation.Status(); ok { + _spec.SetField(runtimebroker.FieldStatus, field.TypeString, value) + } + if value, ok := _u.mutation.ConnectionState(); ok { + _spec.SetField(runtimebroker.FieldConnectionState, field.TypeString, value) + } + if value, ok := _u.mutation.LastHeartbeat(); ok { + _spec.SetField(runtimebroker.FieldLastHeartbeat, field.TypeTime, value) + } + if _u.mutation.LastHeartbeatCleared() { + _spec.ClearField(runtimebroker.FieldLastHeartbeat, field.TypeTime) + } + if value, ok := _u.mutation.Capabilities(); ok { + _spec.SetField(runtimebroker.FieldCapabilities, field.TypeString, value) + } + if _u.mutation.CapabilitiesCleared() { + _spec.ClearField(runtimebroker.FieldCapabilities, field.TypeString) + } + if value, ok := _u.mutation.SupportedHarnesses(); ok { + _spec.SetField(runtimebroker.FieldSupportedHarnesses, field.TypeString, value) + } + if _u.mutation.SupportedHarnessesCleared() { + _spec.ClearField(runtimebroker.FieldSupportedHarnesses, field.TypeString) + } + if value, ok := _u.mutation.Resources(); ok { + _spec.SetField(runtimebroker.FieldResources, field.TypeString, value) + } + if _u.mutation.ResourcesCleared() { + _spec.ClearField(runtimebroker.FieldResources, field.TypeString) + } + if value, ok := _u.mutation.Runtimes(); ok { + _spec.SetField(runtimebroker.FieldRuntimes, field.TypeString, value) + } + if _u.mutation.RuntimesCleared() { + _spec.ClearField(runtimebroker.FieldRuntimes, field.TypeString) + } + if value, ok := _u.mutation.Labels(); ok { + _spec.SetField(runtimebroker.FieldLabels, field.TypeString, value) + } + if _u.mutation.LabelsCleared() { + _spec.ClearField(runtimebroker.FieldLabels, field.TypeString) + } + if value, ok := _u.mutation.Annotations(); ok { + _spec.SetField(runtimebroker.FieldAnnotations, field.TypeString, value) + } + if _u.mutation.AnnotationsCleared() { + _spec.ClearField(runtimebroker.FieldAnnotations, field.TypeString) + } + if value, ok := _u.mutation.Endpoint(); ok { + _spec.SetField(runtimebroker.FieldEndpoint, field.TypeString, value) + } + if _u.mutation.EndpointCleared() { + _spec.ClearField(runtimebroker.FieldEndpoint, field.TypeString) + } + if value, ok := _u.mutation.CreatedBy(); ok { + _spec.SetField(runtimebroker.FieldCreatedBy, field.TypeString, value) + } + if _u.mutation.CreatedByCleared() { + _spec.ClearField(runtimebroker.FieldCreatedBy, field.TypeString) + } + if value, ok := _u.mutation.AutoProvide(); ok { + _spec.SetField(runtimebroker.FieldAutoProvide, field.TypeBool, value) + } + if value, ok := _u.mutation.ConnectedHubID(); ok { + _spec.SetField(runtimebroker.FieldConnectedHubID, field.TypeString, value) + } + if _u.mutation.ConnectedHubIDCleared() { + _spec.ClearField(runtimebroker.FieldConnectedHubID, field.TypeString) + } + if value, ok := _u.mutation.ConnectedSessionID(); ok { + _spec.SetField(runtimebroker.FieldConnectedSessionID, field.TypeString, value) + } + if _u.mutation.ConnectedSessionIDCleared() { + _spec.ClearField(runtimebroker.FieldConnectedSessionID, field.TypeString) + } + if value, ok := _u.mutation.ConnectedAt(); ok { + _spec.SetField(runtimebroker.FieldConnectedAt, field.TypeTime, value) + } + if _u.mutation.ConnectedAtCleared() { + _spec.ClearField(runtimebroker.FieldConnectedAt, field.TypeTime) + } + if value, ok := _u.mutation.Updated(); ok { + _spec.SetField(runtimebroker.FieldUpdated, field.TypeTime, value) + } + if _node, err = sqlgraph.UpdateNodes(ctx, _u.driver, _spec); err != nil { + if _, ok := err.(*sqlgraph.NotFoundError); ok { + err = &NotFoundError{runtimebroker.Label} + } else if sqlgraph.IsConstraintError(err) { + err = &ConstraintError{msg: err.Error(), wrap: err} + } + return 0, err + } + _u.mutation.done = true + return _node, nil +} + +// RuntimeBrokerUpdateOne is the builder for updating a single RuntimeBroker entity. +type RuntimeBrokerUpdateOne struct { + config + fields []string + hooks []Hook + mutation *RuntimeBrokerMutation +} + +// SetName sets the "name" field. +func (_u *RuntimeBrokerUpdateOne) SetName(v string) *RuntimeBrokerUpdateOne { + _u.mutation.SetName(v) + return _u +} + +// SetNillableName sets the "name" field if the given value is not nil. +func (_u *RuntimeBrokerUpdateOne) SetNillableName(v *string) *RuntimeBrokerUpdateOne { + if v != nil { + _u.SetName(*v) + } + return _u +} + +// SetSlug sets the "slug" field. +func (_u *RuntimeBrokerUpdateOne) SetSlug(v string) *RuntimeBrokerUpdateOne { + _u.mutation.SetSlug(v) + return _u +} + +// SetNillableSlug sets the "slug" field if the given value is not nil. +func (_u *RuntimeBrokerUpdateOne) SetNillableSlug(v *string) *RuntimeBrokerUpdateOne { + if v != nil { + _u.SetSlug(*v) + } + return _u +} + +// SetType sets the "type" field. +func (_u *RuntimeBrokerUpdateOne) SetType(v string) *RuntimeBrokerUpdateOne { + _u.mutation.SetType(v) + return _u +} + +// SetNillableType sets the "type" field if the given value is not nil. +func (_u *RuntimeBrokerUpdateOne) SetNillableType(v *string) *RuntimeBrokerUpdateOne { + if v != nil { + _u.SetType(*v) + } + return _u +} + +// ClearType clears the value of the "type" field. +func (_u *RuntimeBrokerUpdateOne) ClearType() *RuntimeBrokerUpdateOne { + _u.mutation.ClearType() + return _u +} + +// SetMode sets the "mode" field. +func (_u *RuntimeBrokerUpdateOne) SetMode(v string) *RuntimeBrokerUpdateOne { + _u.mutation.SetMode(v) + return _u +} + +// SetNillableMode sets the "mode" field if the given value is not nil. +func (_u *RuntimeBrokerUpdateOne) SetNillableMode(v *string) *RuntimeBrokerUpdateOne { + if v != nil { + _u.SetMode(*v) + } + return _u +} + +// SetVersion sets the "version" field. +func (_u *RuntimeBrokerUpdateOne) SetVersion(v string) *RuntimeBrokerUpdateOne { + _u.mutation.SetVersion(v) + return _u +} + +// SetNillableVersion sets the "version" field if the given value is not nil. +func (_u *RuntimeBrokerUpdateOne) SetNillableVersion(v *string) *RuntimeBrokerUpdateOne { + if v != nil { + _u.SetVersion(*v) + } + return _u +} + +// ClearVersion clears the value of the "version" field. +func (_u *RuntimeBrokerUpdateOne) ClearVersion() *RuntimeBrokerUpdateOne { + _u.mutation.ClearVersion() + return _u +} + +// SetLockVersion sets the "lock_version" field. +func (_u *RuntimeBrokerUpdateOne) SetLockVersion(v int64) *RuntimeBrokerUpdateOne { + _u.mutation.ResetLockVersion() + _u.mutation.SetLockVersion(v) + return _u +} + +// SetNillableLockVersion sets the "lock_version" field if the given value is not nil. +func (_u *RuntimeBrokerUpdateOne) SetNillableLockVersion(v *int64) *RuntimeBrokerUpdateOne { + if v != nil { + _u.SetLockVersion(*v) + } + return _u +} + +// AddLockVersion adds value to the "lock_version" field. +func (_u *RuntimeBrokerUpdateOne) AddLockVersion(v int64) *RuntimeBrokerUpdateOne { + _u.mutation.AddLockVersion(v) + return _u +} + +// SetStatus sets the "status" field. +func (_u *RuntimeBrokerUpdateOne) SetStatus(v string) *RuntimeBrokerUpdateOne { + _u.mutation.SetStatus(v) + return _u +} + +// SetNillableStatus sets the "status" field if the given value is not nil. +func (_u *RuntimeBrokerUpdateOne) SetNillableStatus(v *string) *RuntimeBrokerUpdateOne { + if v != nil { + _u.SetStatus(*v) + } + return _u +} + +// SetConnectionState sets the "connection_state" field. +func (_u *RuntimeBrokerUpdateOne) SetConnectionState(v string) *RuntimeBrokerUpdateOne { + _u.mutation.SetConnectionState(v) + return _u +} + +// SetNillableConnectionState sets the "connection_state" field if the given value is not nil. +func (_u *RuntimeBrokerUpdateOne) SetNillableConnectionState(v *string) *RuntimeBrokerUpdateOne { + if v != nil { + _u.SetConnectionState(*v) + } + return _u +} + +// SetLastHeartbeat sets the "last_heartbeat" field. +func (_u *RuntimeBrokerUpdateOne) SetLastHeartbeat(v time.Time) *RuntimeBrokerUpdateOne { + _u.mutation.SetLastHeartbeat(v) + return _u +} + +// SetNillableLastHeartbeat sets the "last_heartbeat" field if the given value is not nil. +func (_u *RuntimeBrokerUpdateOne) SetNillableLastHeartbeat(v *time.Time) *RuntimeBrokerUpdateOne { + if v != nil { + _u.SetLastHeartbeat(*v) + } + return _u +} + +// ClearLastHeartbeat clears the value of the "last_heartbeat" field. +func (_u *RuntimeBrokerUpdateOne) ClearLastHeartbeat() *RuntimeBrokerUpdateOne { + _u.mutation.ClearLastHeartbeat() + return _u +} + +// SetCapabilities sets the "capabilities" field. +func (_u *RuntimeBrokerUpdateOne) SetCapabilities(v string) *RuntimeBrokerUpdateOne { + _u.mutation.SetCapabilities(v) + return _u +} + +// SetNillableCapabilities sets the "capabilities" field if the given value is not nil. +func (_u *RuntimeBrokerUpdateOne) SetNillableCapabilities(v *string) *RuntimeBrokerUpdateOne { + if v != nil { + _u.SetCapabilities(*v) + } + return _u +} + +// ClearCapabilities clears the value of the "capabilities" field. +func (_u *RuntimeBrokerUpdateOne) ClearCapabilities() *RuntimeBrokerUpdateOne { + _u.mutation.ClearCapabilities() + return _u +} + +// SetSupportedHarnesses sets the "supported_harnesses" field. +func (_u *RuntimeBrokerUpdateOne) SetSupportedHarnesses(v string) *RuntimeBrokerUpdateOne { + _u.mutation.SetSupportedHarnesses(v) + return _u +} + +// SetNillableSupportedHarnesses sets the "supported_harnesses" field if the given value is not nil. +func (_u *RuntimeBrokerUpdateOne) SetNillableSupportedHarnesses(v *string) *RuntimeBrokerUpdateOne { + if v != nil { + _u.SetSupportedHarnesses(*v) + } + return _u +} + +// ClearSupportedHarnesses clears the value of the "supported_harnesses" field. +func (_u *RuntimeBrokerUpdateOne) ClearSupportedHarnesses() *RuntimeBrokerUpdateOne { + _u.mutation.ClearSupportedHarnesses() + return _u +} + +// SetResources sets the "resources" field. +func (_u *RuntimeBrokerUpdateOne) SetResources(v string) *RuntimeBrokerUpdateOne { + _u.mutation.SetResources(v) + return _u +} + +// SetNillableResources sets the "resources" field if the given value is not nil. +func (_u *RuntimeBrokerUpdateOne) SetNillableResources(v *string) *RuntimeBrokerUpdateOne { + if v != nil { + _u.SetResources(*v) + } + return _u +} + +// ClearResources clears the value of the "resources" field. +func (_u *RuntimeBrokerUpdateOne) ClearResources() *RuntimeBrokerUpdateOne { + _u.mutation.ClearResources() + return _u +} + +// SetRuntimes sets the "runtimes" field. +func (_u *RuntimeBrokerUpdateOne) SetRuntimes(v string) *RuntimeBrokerUpdateOne { + _u.mutation.SetRuntimes(v) + return _u +} + +// SetNillableRuntimes sets the "runtimes" field if the given value is not nil. +func (_u *RuntimeBrokerUpdateOne) SetNillableRuntimes(v *string) *RuntimeBrokerUpdateOne { + if v != nil { + _u.SetRuntimes(*v) + } + return _u +} + +// ClearRuntimes clears the value of the "runtimes" field. +func (_u *RuntimeBrokerUpdateOne) ClearRuntimes() *RuntimeBrokerUpdateOne { + _u.mutation.ClearRuntimes() + return _u +} + +// SetLabels sets the "labels" field. +func (_u *RuntimeBrokerUpdateOne) SetLabels(v string) *RuntimeBrokerUpdateOne { + _u.mutation.SetLabels(v) + return _u +} + +// SetNillableLabels sets the "labels" field if the given value is not nil. +func (_u *RuntimeBrokerUpdateOne) SetNillableLabels(v *string) *RuntimeBrokerUpdateOne { + if v != nil { + _u.SetLabels(*v) + } + return _u +} + +// ClearLabels clears the value of the "labels" field. +func (_u *RuntimeBrokerUpdateOne) ClearLabels() *RuntimeBrokerUpdateOne { + _u.mutation.ClearLabels() + return _u +} + +// SetAnnotations sets the "annotations" field. +func (_u *RuntimeBrokerUpdateOne) SetAnnotations(v string) *RuntimeBrokerUpdateOne { + _u.mutation.SetAnnotations(v) + return _u +} + +// SetNillableAnnotations sets the "annotations" field if the given value is not nil. +func (_u *RuntimeBrokerUpdateOne) SetNillableAnnotations(v *string) *RuntimeBrokerUpdateOne { + if v != nil { + _u.SetAnnotations(*v) + } + return _u +} + +// ClearAnnotations clears the value of the "annotations" field. +func (_u *RuntimeBrokerUpdateOne) ClearAnnotations() *RuntimeBrokerUpdateOne { + _u.mutation.ClearAnnotations() + return _u +} + +// SetEndpoint sets the "endpoint" field. +func (_u *RuntimeBrokerUpdateOne) SetEndpoint(v string) *RuntimeBrokerUpdateOne { + _u.mutation.SetEndpoint(v) + return _u +} + +// SetNillableEndpoint sets the "endpoint" field if the given value is not nil. +func (_u *RuntimeBrokerUpdateOne) SetNillableEndpoint(v *string) *RuntimeBrokerUpdateOne { + if v != nil { + _u.SetEndpoint(*v) + } + return _u +} + +// ClearEndpoint clears the value of the "endpoint" field. +func (_u *RuntimeBrokerUpdateOne) ClearEndpoint() *RuntimeBrokerUpdateOne { + _u.mutation.ClearEndpoint() + return _u +} + +// SetCreatedBy sets the "created_by" field. +func (_u *RuntimeBrokerUpdateOne) SetCreatedBy(v string) *RuntimeBrokerUpdateOne { + _u.mutation.SetCreatedBy(v) + return _u +} + +// SetNillableCreatedBy sets the "created_by" field if the given value is not nil. +func (_u *RuntimeBrokerUpdateOne) SetNillableCreatedBy(v *string) *RuntimeBrokerUpdateOne { + if v != nil { + _u.SetCreatedBy(*v) + } + return _u +} + +// ClearCreatedBy clears the value of the "created_by" field. +func (_u *RuntimeBrokerUpdateOne) ClearCreatedBy() *RuntimeBrokerUpdateOne { + _u.mutation.ClearCreatedBy() + return _u +} + +// SetAutoProvide sets the "auto_provide" field. +func (_u *RuntimeBrokerUpdateOne) SetAutoProvide(v bool) *RuntimeBrokerUpdateOne { + _u.mutation.SetAutoProvide(v) + return _u +} + +// SetNillableAutoProvide sets the "auto_provide" field if the given value is not nil. +func (_u *RuntimeBrokerUpdateOne) SetNillableAutoProvide(v *bool) *RuntimeBrokerUpdateOne { + if v != nil { + _u.SetAutoProvide(*v) + } + return _u +} + +// SetConnectedHubID sets the "connected_hub_id" field. +func (_u *RuntimeBrokerUpdateOne) SetConnectedHubID(v string) *RuntimeBrokerUpdateOne { + _u.mutation.SetConnectedHubID(v) + return _u +} + +// SetNillableConnectedHubID sets the "connected_hub_id" field if the given value is not nil. +func (_u *RuntimeBrokerUpdateOne) SetNillableConnectedHubID(v *string) *RuntimeBrokerUpdateOne { + if v != nil { + _u.SetConnectedHubID(*v) + } + return _u +} + +// ClearConnectedHubID clears the value of the "connected_hub_id" field. +func (_u *RuntimeBrokerUpdateOne) ClearConnectedHubID() *RuntimeBrokerUpdateOne { + _u.mutation.ClearConnectedHubID() + return _u +} + +// SetConnectedSessionID sets the "connected_session_id" field. +func (_u *RuntimeBrokerUpdateOne) SetConnectedSessionID(v string) *RuntimeBrokerUpdateOne { + _u.mutation.SetConnectedSessionID(v) + return _u +} + +// SetNillableConnectedSessionID sets the "connected_session_id" field if the given value is not nil. +func (_u *RuntimeBrokerUpdateOne) SetNillableConnectedSessionID(v *string) *RuntimeBrokerUpdateOne { + if v != nil { + _u.SetConnectedSessionID(*v) + } + return _u +} + +// ClearConnectedSessionID clears the value of the "connected_session_id" field. +func (_u *RuntimeBrokerUpdateOne) ClearConnectedSessionID() *RuntimeBrokerUpdateOne { + _u.mutation.ClearConnectedSessionID() + return _u +} + +// SetConnectedAt sets the "connected_at" field. +func (_u *RuntimeBrokerUpdateOne) SetConnectedAt(v time.Time) *RuntimeBrokerUpdateOne { + _u.mutation.SetConnectedAt(v) + return _u +} + +// SetNillableConnectedAt sets the "connected_at" field if the given value is not nil. +func (_u *RuntimeBrokerUpdateOne) SetNillableConnectedAt(v *time.Time) *RuntimeBrokerUpdateOne { + if v != nil { + _u.SetConnectedAt(*v) + } + return _u +} + +// ClearConnectedAt clears the value of the "connected_at" field. +func (_u *RuntimeBrokerUpdateOne) ClearConnectedAt() *RuntimeBrokerUpdateOne { + _u.mutation.ClearConnectedAt() + return _u +} + +// SetUpdated sets the "updated" field. +func (_u *RuntimeBrokerUpdateOne) SetUpdated(v time.Time) *RuntimeBrokerUpdateOne { + _u.mutation.SetUpdated(v) + return _u +} + +// Mutation returns the RuntimeBrokerMutation object of the builder. +func (_u *RuntimeBrokerUpdateOne) Mutation() *RuntimeBrokerMutation { + return _u.mutation +} + +// Where appends a list predicates to the RuntimeBrokerUpdate builder. +func (_u *RuntimeBrokerUpdateOne) Where(ps ...predicate.RuntimeBroker) *RuntimeBrokerUpdateOne { + _u.mutation.Where(ps...) + return _u +} + +// Select allows selecting one or more fields (columns) of the returned entity. +// The default is selecting all fields defined in the entity schema. +func (_u *RuntimeBrokerUpdateOne) Select(field string, fields ...string) *RuntimeBrokerUpdateOne { + _u.fields = append([]string{field}, fields...) + return _u +} + +// Save executes the query and returns the updated RuntimeBroker entity. +func (_u *RuntimeBrokerUpdateOne) Save(ctx context.Context) (*RuntimeBroker, error) { + _u.defaults() + return withHooks(ctx, _u.sqlSave, _u.mutation, _u.hooks) +} + +// SaveX is like Save, but panics if an error occurs. +func (_u *RuntimeBrokerUpdateOne) SaveX(ctx context.Context) *RuntimeBroker { + node, err := _u.Save(ctx) + if err != nil { + panic(err) + } + return node +} + +// Exec executes the query on the entity. +func (_u *RuntimeBrokerUpdateOne) Exec(ctx context.Context) error { + _, err := _u.Save(ctx) + return err +} + +// ExecX is like Exec, but panics if an error occurs. +func (_u *RuntimeBrokerUpdateOne) ExecX(ctx context.Context) { + if err := _u.Exec(ctx); err != nil { + panic(err) + } +} + +// defaults sets the default values of the builder before save. +func (_u *RuntimeBrokerUpdateOne) defaults() { + if _, ok := _u.mutation.Updated(); !ok { + v := runtimebroker.UpdateDefaultUpdated() + _u.mutation.SetUpdated(v) + } +} + +// check runs all checks and user-defined validators on the builder. +func (_u *RuntimeBrokerUpdateOne) check() error { + if v, ok := _u.mutation.Name(); ok { + if err := runtimebroker.NameValidator(v); err != nil { + return &ValidationError{Name: "name", err: fmt.Errorf(`ent: validator failed for field "RuntimeBroker.name": %w`, err)} + } + } + if v, ok := _u.mutation.Slug(); ok { + if err := runtimebroker.SlugValidator(v); err != nil { + return &ValidationError{Name: "slug", err: fmt.Errorf(`ent: validator failed for field "RuntimeBroker.slug": %w`, err)} + } + } + return nil +} + +func (_u *RuntimeBrokerUpdateOne) sqlSave(ctx context.Context) (_node *RuntimeBroker, err error) { + if err := _u.check(); err != nil { + return _node, err + } + _spec := sqlgraph.NewUpdateSpec(runtimebroker.Table, runtimebroker.Columns, sqlgraph.NewFieldSpec(runtimebroker.FieldID, field.TypeUUID)) + id, ok := _u.mutation.ID() + if !ok { + return nil, &ValidationError{Name: "id", err: errors.New(`ent: missing "RuntimeBroker.id" for update`)} + } + _spec.Node.ID.Value = id + if fields := _u.fields; len(fields) > 0 { + _spec.Node.Columns = make([]string, 0, len(fields)) + _spec.Node.Columns = append(_spec.Node.Columns, runtimebroker.FieldID) + for _, f := range fields { + if !runtimebroker.ValidColumn(f) { + return nil, &ValidationError{Name: f, err: fmt.Errorf("ent: invalid field %q for query", f)} + } + if f != runtimebroker.FieldID { + _spec.Node.Columns = append(_spec.Node.Columns, f) + } + } + } + if ps := _u.mutation.predicates; len(ps) > 0 { + _spec.Predicate = func(selector *sql.Selector) { + for i := range ps { + ps[i](selector) + } + } + } + if value, ok := _u.mutation.Name(); ok { + _spec.SetField(runtimebroker.FieldName, field.TypeString, value) + } + if value, ok := _u.mutation.Slug(); ok { + _spec.SetField(runtimebroker.FieldSlug, field.TypeString, value) + } + if value, ok := _u.mutation.GetType(); ok { + _spec.SetField(runtimebroker.FieldType, field.TypeString, value) + } + if _u.mutation.TypeCleared() { + _spec.ClearField(runtimebroker.FieldType, field.TypeString) + } + if value, ok := _u.mutation.Mode(); ok { + _spec.SetField(runtimebroker.FieldMode, field.TypeString, value) + } + if value, ok := _u.mutation.Version(); ok { + _spec.SetField(runtimebroker.FieldVersion, field.TypeString, value) + } + if _u.mutation.VersionCleared() { + _spec.ClearField(runtimebroker.FieldVersion, field.TypeString) + } + if value, ok := _u.mutation.LockVersion(); ok { + _spec.SetField(runtimebroker.FieldLockVersion, field.TypeInt64, value) + } + if value, ok := _u.mutation.AddedLockVersion(); ok { + _spec.AddField(runtimebroker.FieldLockVersion, field.TypeInt64, value) + } + if value, ok := _u.mutation.Status(); ok { + _spec.SetField(runtimebroker.FieldStatus, field.TypeString, value) + } + if value, ok := _u.mutation.ConnectionState(); ok { + _spec.SetField(runtimebroker.FieldConnectionState, field.TypeString, value) + } + if value, ok := _u.mutation.LastHeartbeat(); ok { + _spec.SetField(runtimebroker.FieldLastHeartbeat, field.TypeTime, value) + } + if _u.mutation.LastHeartbeatCleared() { + _spec.ClearField(runtimebroker.FieldLastHeartbeat, field.TypeTime) + } + if value, ok := _u.mutation.Capabilities(); ok { + _spec.SetField(runtimebroker.FieldCapabilities, field.TypeString, value) + } + if _u.mutation.CapabilitiesCleared() { + _spec.ClearField(runtimebroker.FieldCapabilities, field.TypeString) + } + if value, ok := _u.mutation.SupportedHarnesses(); ok { + _spec.SetField(runtimebroker.FieldSupportedHarnesses, field.TypeString, value) + } + if _u.mutation.SupportedHarnessesCleared() { + _spec.ClearField(runtimebroker.FieldSupportedHarnesses, field.TypeString) + } + if value, ok := _u.mutation.Resources(); ok { + _spec.SetField(runtimebroker.FieldResources, field.TypeString, value) + } + if _u.mutation.ResourcesCleared() { + _spec.ClearField(runtimebroker.FieldResources, field.TypeString) + } + if value, ok := _u.mutation.Runtimes(); ok { + _spec.SetField(runtimebroker.FieldRuntimes, field.TypeString, value) + } + if _u.mutation.RuntimesCleared() { + _spec.ClearField(runtimebroker.FieldRuntimes, field.TypeString) + } + if value, ok := _u.mutation.Labels(); ok { + _spec.SetField(runtimebroker.FieldLabels, field.TypeString, value) + } + if _u.mutation.LabelsCleared() { + _spec.ClearField(runtimebroker.FieldLabels, field.TypeString) + } + if value, ok := _u.mutation.Annotations(); ok { + _spec.SetField(runtimebroker.FieldAnnotations, field.TypeString, value) + } + if _u.mutation.AnnotationsCleared() { + _spec.ClearField(runtimebroker.FieldAnnotations, field.TypeString) + } + if value, ok := _u.mutation.Endpoint(); ok { + _spec.SetField(runtimebroker.FieldEndpoint, field.TypeString, value) + } + if _u.mutation.EndpointCleared() { + _spec.ClearField(runtimebroker.FieldEndpoint, field.TypeString) + } + if value, ok := _u.mutation.CreatedBy(); ok { + _spec.SetField(runtimebroker.FieldCreatedBy, field.TypeString, value) + } + if _u.mutation.CreatedByCleared() { + _spec.ClearField(runtimebroker.FieldCreatedBy, field.TypeString) + } + if value, ok := _u.mutation.AutoProvide(); ok { + _spec.SetField(runtimebroker.FieldAutoProvide, field.TypeBool, value) + } + if value, ok := _u.mutation.ConnectedHubID(); ok { + _spec.SetField(runtimebroker.FieldConnectedHubID, field.TypeString, value) + } + if _u.mutation.ConnectedHubIDCleared() { + _spec.ClearField(runtimebroker.FieldConnectedHubID, field.TypeString) + } + if value, ok := _u.mutation.ConnectedSessionID(); ok { + _spec.SetField(runtimebroker.FieldConnectedSessionID, field.TypeString, value) + } + if _u.mutation.ConnectedSessionIDCleared() { + _spec.ClearField(runtimebroker.FieldConnectedSessionID, field.TypeString) + } + if value, ok := _u.mutation.ConnectedAt(); ok { + _spec.SetField(runtimebroker.FieldConnectedAt, field.TypeTime, value) + } + if _u.mutation.ConnectedAtCleared() { + _spec.ClearField(runtimebroker.FieldConnectedAt, field.TypeTime) + } + if value, ok := _u.mutation.Updated(); ok { + _spec.SetField(runtimebroker.FieldUpdated, field.TypeTime, value) + } + _node = &RuntimeBroker{config: _u.config} + _spec.Assign = _node.assignValues + _spec.ScanValues = _node.scanValues + if err = sqlgraph.UpdateNode(ctx, _u.driver, _spec); err != nil { + if _, ok := err.(*sqlgraph.NotFoundError); ok { + err = &NotFoundError{runtimebroker.Label} + } else if sqlgraph.IsConstraintError(err) { + err = &ConstraintError{msg: err.Error(), wrap: err} + } + return nil, err + } + _u.mutation.done = true + return _node, nil +} diff --git a/pkg/ent/schedule.go b/pkg/ent/schedule.go new file mode 100644 index 000000000..c67a95cc1 --- /dev/null +++ b/pkg/ent/schedule.go @@ -0,0 +1,269 @@ +// Code generated by ent, DO NOT EDIT. + +package ent + +import ( + "fmt" + "strings" + "time" + + "entgo.io/ent" + "entgo.io/ent/dialect/sql" + "github.com/GoogleCloudPlatform/scion/pkg/ent/schedule" + "github.com/google/uuid" +) + +// Schedule is the model entity for the Schedule schema. +type Schedule struct { + config `json:"-"` + // ID of the ent. + ID uuid.UUID `json:"id,omitempty"` + // ProjectID holds the value of the "project_id" field. + ProjectID uuid.UUID `json:"project_id,omitempty"` + // Name holds the value of the "name" field. + Name string `json:"name,omitempty"` + // CronExpr holds the value of the "cron_expr" field. + CronExpr string `json:"cron_expr,omitempty"` + // EventType holds the value of the "event_type" field. + EventType string `json:"event_type,omitempty"` + // Payload holds the value of the "payload" field. + Payload string `json:"payload,omitempty"` + // Status holds the value of the "status" field. + Status string `json:"status,omitempty"` + // NextRunAt holds the value of the "next_run_at" field. + NextRunAt *time.Time `json:"next_run_at,omitempty"` + // LastRunAt holds the value of the "last_run_at" field. + LastRunAt *time.Time `json:"last_run_at,omitempty"` + // LastRunStatus holds the value of the "last_run_status" field. + LastRunStatus string `json:"last_run_status,omitempty"` + // LastRunError holds the value of the "last_run_error" field. + LastRunError string `json:"last_run_error,omitempty"` + // RunCount holds the value of the "run_count" field. + RunCount int `json:"run_count,omitempty"` + // ErrorCount holds the value of the "error_count" field. + ErrorCount int `json:"error_count,omitempty"` + // CreatedBy holds the value of the "created_by" field. + CreatedBy string `json:"created_by,omitempty"` + // Created holds the value of the "created" field. + Created time.Time `json:"created,omitempty"` + // Updated holds the value of the "updated" field. + Updated time.Time `json:"updated,omitempty"` + selectValues sql.SelectValues +} + +// scanValues returns the types for scanning values from sql.Rows. +func (*Schedule) scanValues(columns []string) ([]any, error) { + values := make([]any, len(columns)) + for i := range columns { + switch columns[i] { + case schedule.FieldRunCount, schedule.FieldErrorCount: + values[i] = new(sql.NullInt64) + case schedule.FieldName, schedule.FieldCronExpr, schedule.FieldEventType, schedule.FieldPayload, schedule.FieldStatus, schedule.FieldLastRunStatus, schedule.FieldLastRunError, schedule.FieldCreatedBy: + values[i] = new(sql.NullString) + case schedule.FieldNextRunAt, schedule.FieldLastRunAt, schedule.FieldCreated, schedule.FieldUpdated: + values[i] = new(sql.NullTime) + case schedule.FieldID, schedule.FieldProjectID: + values[i] = new(uuid.UUID) + default: + values[i] = new(sql.UnknownType) + } + } + return values, nil +} + +// assignValues assigns the values that were returned from sql.Rows (after scanning) +// to the Schedule fields. +func (_m *Schedule) assignValues(columns []string, values []any) error { + if m, n := len(values), len(columns); m < n { + return fmt.Errorf("mismatch number of scan values: %d != %d", m, n) + } + for i := range columns { + switch columns[i] { + case schedule.FieldID: + if value, ok := values[i].(*uuid.UUID); !ok { + return fmt.Errorf("unexpected type %T for field id", values[i]) + } else if value != nil { + _m.ID = *value + } + case schedule.FieldProjectID: + if value, ok := values[i].(*uuid.UUID); !ok { + return fmt.Errorf("unexpected type %T for field project_id", values[i]) + } else if value != nil { + _m.ProjectID = *value + } + case schedule.FieldName: + if value, ok := values[i].(*sql.NullString); !ok { + return fmt.Errorf("unexpected type %T for field name", values[i]) + } else if value.Valid { + _m.Name = value.String + } + case schedule.FieldCronExpr: + if value, ok := values[i].(*sql.NullString); !ok { + return fmt.Errorf("unexpected type %T for field cron_expr", values[i]) + } else if value.Valid { + _m.CronExpr = value.String + } + case schedule.FieldEventType: + if value, ok := values[i].(*sql.NullString); !ok { + return fmt.Errorf("unexpected type %T for field event_type", values[i]) + } else if value.Valid { + _m.EventType = value.String + } + case schedule.FieldPayload: + if value, ok := values[i].(*sql.NullString); !ok { + return fmt.Errorf("unexpected type %T for field payload", values[i]) + } else if value.Valid { + _m.Payload = value.String + } + case schedule.FieldStatus: + if value, ok := values[i].(*sql.NullString); !ok { + return fmt.Errorf("unexpected type %T for field status", values[i]) + } else if value.Valid { + _m.Status = value.String + } + case schedule.FieldNextRunAt: + if value, ok := values[i].(*sql.NullTime); !ok { + return fmt.Errorf("unexpected type %T for field next_run_at", values[i]) + } else if value.Valid { + _m.NextRunAt = new(time.Time) + *_m.NextRunAt = value.Time + } + case schedule.FieldLastRunAt: + if value, ok := values[i].(*sql.NullTime); !ok { + return fmt.Errorf("unexpected type %T for field last_run_at", values[i]) + } else if value.Valid { + _m.LastRunAt = new(time.Time) + *_m.LastRunAt = value.Time + } + case schedule.FieldLastRunStatus: + if value, ok := values[i].(*sql.NullString); !ok { + return fmt.Errorf("unexpected type %T for field last_run_status", values[i]) + } else if value.Valid { + _m.LastRunStatus = value.String + } + case schedule.FieldLastRunError: + if value, ok := values[i].(*sql.NullString); !ok { + return fmt.Errorf("unexpected type %T for field last_run_error", values[i]) + } else if value.Valid { + _m.LastRunError = value.String + } + case schedule.FieldRunCount: + if value, ok := values[i].(*sql.NullInt64); !ok { + return fmt.Errorf("unexpected type %T for field run_count", values[i]) + } else if value.Valid { + _m.RunCount = int(value.Int64) + } + case schedule.FieldErrorCount: + if value, ok := values[i].(*sql.NullInt64); !ok { + return fmt.Errorf("unexpected type %T for field error_count", values[i]) + } else if value.Valid { + _m.ErrorCount = int(value.Int64) + } + case schedule.FieldCreatedBy: + if value, ok := values[i].(*sql.NullString); !ok { + return fmt.Errorf("unexpected type %T for field created_by", values[i]) + } else if value.Valid { + _m.CreatedBy = value.String + } + case schedule.FieldCreated: + if value, ok := values[i].(*sql.NullTime); !ok { + return fmt.Errorf("unexpected type %T for field created", values[i]) + } else if value.Valid { + _m.Created = value.Time + } + case schedule.FieldUpdated: + if value, ok := values[i].(*sql.NullTime); !ok { + return fmt.Errorf("unexpected type %T for field updated", values[i]) + } else if value.Valid { + _m.Updated = value.Time + } + default: + _m.selectValues.Set(columns[i], values[i]) + } + } + return nil +} + +// Value returns the ent.Value that was dynamically selected and assigned to the Schedule. +// This includes values selected through modifiers, order, etc. +func (_m *Schedule) Value(name string) (ent.Value, error) { + return _m.selectValues.Get(name) +} + +// Update returns a builder for updating this Schedule. +// Note that you need to call Schedule.Unwrap() before calling this method if this Schedule +// was returned from a transaction, and the transaction was committed or rolled back. +func (_m *Schedule) Update() *ScheduleUpdateOne { + return NewScheduleClient(_m.config).UpdateOne(_m) +} + +// Unwrap unwraps the Schedule entity that was returned from a transaction after it was closed, +// so that all future queries will be executed through the driver which created the transaction. +func (_m *Schedule) Unwrap() *Schedule { + _tx, ok := _m.config.driver.(*txDriver) + if !ok { + panic("ent: Schedule is not a transactional entity") + } + _m.config.driver = _tx.drv + return _m +} + +// String implements the fmt.Stringer. +func (_m *Schedule) String() string { + var builder strings.Builder + builder.WriteString("Schedule(") + builder.WriteString(fmt.Sprintf("id=%v, ", _m.ID)) + builder.WriteString("project_id=") + builder.WriteString(fmt.Sprintf("%v", _m.ProjectID)) + builder.WriteString(", ") + builder.WriteString("name=") + builder.WriteString(_m.Name) + builder.WriteString(", ") + builder.WriteString("cron_expr=") + builder.WriteString(_m.CronExpr) + builder.WriteString(", ") + builder.WriteString("event_type=") + builder.WriteString(_m.EventType) + builder.WriteString(", ") + builder.WriteString("payload=") + builder.WriteString(_m.Payload) + builder.WriteString(", ") + builder.WriteString("status=") + builder.WriteString(_m.Status) + builder.WriteString(", ") + if v := _m.NextRunAt; v != nil { + builder.WriteString("next_run_at=") + builder.WriteString(v.Format(time.ANSIC)) + } + builder.WriteString(", ") + if v := _m.LastRunAt; v != nil { + builder.WriteString("last_run_at=") + builder.WriteString(v.Format(time.ANSIC)) + } + builder.WriteString(", ") + builder.WriteString("last_run_status=") + builder.WriteString(_m.LastRunStatus) + builder.WriteString(", ") + builder.WriteString("last_run_error=") + builder.WriteString(_m.LastRunError) + builder.WriteString(", ") + builder.WriteString("run_count=") + builder.WriteString(fmt.Sprintf("%v", _m.RunCount)) + builder.WriteString(", ") + builder.WriteString("error_count=") + builder.WriteString(fmt.Sprintf("%v", _m.ErrorCount)) + builder.WriteString(", ") + builder.WriteString("created_by=") + builder.WriteString(_m.CreatedBy) + builder.WriteString(", ") + builder.WriteString("created=") + builder.WriteString(_m.Created.Format(time.ANSIC)) + builder.WriteString(", ") + builder.WriteString("updated=") + builder.WriteString(_m.Updated.Format(time.ANSIC)) + builder.WriteByte(')') + return builder.String() +} + +// Schedules is a parsable slice of Schedule. +type Schedules []*Schedule diff --git a/pkg/ent/schedule/schedule.go b/pkg/ent/schedule/schedule.go new file mode 100644 index 000000000..a5481c542 --- /dev/null +++ b/pkg/ent/schedule/schedule.go @@ -0,0 +1,187 @@ +// Code generated by ent, DO NOT EDIT. + +package schedule + +import ( + "time" + + "entgo.io/ent/dialect/sql" + "github.com/google/uuid" +) + +const ( + // Label holds the string label denoting the schedule type in the database. + Label = "schedule" + // FieldID holds the string denoting the id field in the database. + FieldID = "id" + // FieldProjectID holds the string denoting the project_id field in the database. + FieldProjectID = "project_id" + // FieldName holds the string denoting the name field in the database. + FieldName = "name" + // FieldCronExpr holds the string denoting the cron_expr field in the database. + FieldCronExpr = "cron_expr" + // FieldEventType holds the string denoting the event_type field in the database. + FieldEventType = "event_type" + // FieldPayload holds the string denoting the payload field in the database. + FieldPayload = "payload" + // FieldStatus holds the string denoting the status field in the database. + FieldStatus = "status" + // FieldNextRunAt holds the string denoting the next_run_at field in the database. + FieldNextRunAt = "next_run_at" + // FieldLastRunAt holds the string denoting the last_run_at field in the database. + FieldLastRunAt = "last_run_at" + // FieldLastRunStatus holds the string denoting the last_run_status field in the database. + FieldLastRunStatus = "last_run_status" + // FieldLastRunError holds the string denoting the last_run_error field in the database. + FieldLastRunError = "last_run_error" + // FieldRunCount holds the string denoting the run_count field in the database. + FieldRunCount = "run_count" + // FieldErrorCount holds the string denoting the error_count field in the database. + FieldErrorCount = "error_count" + // FieldCreatedBy holds the string denoting the created_by field in the database. + FieldCreatedBy = "created_by" + // FieldCreated holds the string denoting the created field in the database. + FieldCreated = "created" + // FieldUpdated holds the string denoting the updated field in the database. + FieldUpdated = "updated" + // Table holds the table name of the schedule in the database. + Table = "schedules" +) + +// Columns holds all SQL columns for schedule fields. +var Columns = []string{ + FieldID, + FieldProjectID, + FieldName, + FieldCronExpr, + FieldEventType, + FieldPayload, + FieldStatus, + FieldNextRunAt, + FieldLastRunAt, + FieldLastRunStatus, + FieldLastRunError, + FieldRunCount, + FieldErrorCount, + FieldCreatedBy, + FieldCreated, + FieldUpdated, +} + +// ValidColumn reports if the column name is valid (part of the table columns). +func ValidColumn(column string) bool { + for i := range Columns { + if column == Columns[i] { + return true + } + } + return false +} + +var ( + // NameValidator is a validator for the "name" field. It is called by the builders before save. + NameValidator func(string) error + // CronExprValidator is a validator for the "cron_expr" field. It is called by the builders before save. + CronExprValidator func(string) error + // EventTypeValidator is a validator for the "event_type" field. It is called by the builders before save. + EventTypeValidator func(string) error + // DefaultPayload holds the default value on creation for the "payload" field. + DefaultPayload string + // DefaultStatus holds the default value on creation for the "status" field. + DefaultStatus string + // DefaultRunCount holds the default value on creation for the "run_count" field. + DefaultRunCount int + // DefaultErrorCount holds the default value on creation for the "error_count" field. + DefaultErrorCount int + // DefaultCreated holds the default value on creation for the "created" field. + DefaultCreated func() time.Time + // DefaultUpdated holds the default value on creation for the "updated" field. + DefaultUpdated func() time.Time + // UpdateDefaultUpdated holds the default value on update for the "updated" field. + UpdateDefaultUpdated func() time.Time + // DefaultID holds the default value on creation for the "id" field. + DefaultID func() uuid.UUID +) + +// OrderOption defines the ordering options for the Schedule queries. +type OrderOption func(*sql.Selector) + +// ByID orders the results by the id field. +func ByID(opts ...sql.OrderTermOption) OrderOption { + return sql.OrderByField(FieldID, opts...).ToFunc() +} + +// ByProjectID orders the results by the project_id field. +func ByProjectID(opts ...sql.OrderTermOption) OrderOption { + return sql.OrderByField(FieldProjectID, opts...).ToFunc() +} + +// ByName orders the results by the name field. +func ByName(opts ...sql.OrderTermOption) OrderOption { + return sql.OrderByField(FieldName, opts...).ToFunc() +} + +// ByCronExpr orders the results by the cron_expr field. +func ByCronExpr(opts ...sql.OrderTermOption) OrderOption { + return sql.OrderByField(FieldCronExpr, opts...).ToFunc() +} + +// ByEventType orders the results by the event_type field. +func ByEventType(opts ...sql.OrderTermOption) OrderOption { + return sql.OrderByField(FieldEventType, opts...).ToFunc() +} + +// ByPayload orders the results by the payload field. +func ByPayload(opts ...sql.OrderTermOption) OrderOption { + return sql.OrderByField(FieldPayload, opts...).ToFunc() +} + +// ByStatus orders the results by the status field. +func ByStatus(opts ...sql.OrderTermOption) OrderOption { + return sql.OrderByField(FieldStatus, opts...).ToFunc() +} + +// ByNextRunAt orders the results by the next_run_at field. +func ByNextRunAt(opts ...sql.OrderTermOption) OrderOption { + return sql.OrderByField(FieldNextRunAt, opts...).ToFunc() +} + +// ByLastRunAt orders the results by the last_run_at field. +func ByLastRunAt(opts ...sql.OrderTermOption) OrderOption { + return sql.OrderByField(FieldLastRunAt, opts...).ToFunc() +} + +// ByLastRunStatus orders the results by the last_run_status field. +func ByLastRunStatus(opts ...sql.OrderTermOption) OrderOption { + return sql.OrderByField(FieldLastRunStatus, opts...).ToFunc() +} + +// ByLastRunError orders the results by the last_run_error field. +func ByLastRunError(opts ...sql.OrderTermOption) OrderOption { + return sql.OrderByField(FieldLastRunError, opts...).ToFunc() +} + +// ByRunCount orders the results by the run_count field. +func ByRunCount(opts ...sql.OrderTermOption) OrderOption { + return sql.OrderByField(FieldRunCount, opts...).ToFunc() +} + +// ByErrorCount orders the results by the error_count field. +func ByErrorCount(opts ...sql.OrderTermOption) OrderOption { + return sql.OrderByField(FieldErrorCount, opts...).ToFunc() +} + +// ByCreatedBy orders the results by the created_by field. +func ByCreatedBy(opts ...sql.OrderTermOption) OrderOption { + return sql.OrderByField(FieldCreatedBy, opts...).ToFunc() +} + +// ByCreated orders the results by the created field. +func ByCreated(opts ...sql.OrderTermOption) OrderOption { + return sql.OrderByField(FieldCreated, opts...).ToFunc() +} + +// ByUpdated orders the results by the updated field. +func ByUpdated(opts ...sql.OrderTermOption) OrderOption { + return sql.OrderByField(FieldUpdated, opts...).ToFunc() +} diff --git a/pkg/ent/schedule/where.go b/pkg/ent/schedule/where.go new file mode 100644 index 000000000..7d8fe4d22 --- /dev/null +++ b/pkg/ent/schedule/where.go @@ -0,0 +1,996 @@ +// Code generated by ent, DO NOT EDIT. + +package schedule + +import ( + "time" + + "entgo.io/ent/dialect/sql" + "github.com/GoogleCloudPlatform/scion/pkg/ent/predicate" + "github.com/google/uuid" +) + +// ID filters vertices based on their ID field. +func ID(id uuid.UUID) predicate.Schedule { + return predicate.Schedule(sql.FieldEQ(FieldID, id)) +} + +// IDEQ applies the EQ predicate on the ID field. +func IDEQ(id uuid.UUID) predicate.Schedule { + return predicate.Schedule(sql.FieldEQ(FieldID, id)) +} + +// IDNEQ applies the NEQ predicate on the ID field. +func IDNEQ(id uuid.UUID) predicate.Schedule { + return predicate.Schedule(sql.FieldNEQ(FieldID, id)) +} + +// IDIn applies the In predicate on the ID field. +func IDIn(ids ...uuid.UUID) predicate.Schedule { + return predicate.Schedule(sql.FieldIn(FieldID, ids...)) +} + +// IDNotIn applies the NotIn predicate on the ID field. +func IDNotIn(ids ...uuid.UUID) predicate.Schedule { + return predicate.Schedule(sql.FieldNotIn(FieldID, ids...)) +} + +// IDGT applies the GT predicate on the ID field. +func IDGT(id uuid.UUID) predicate.Schedule { + return predicate.Schedule(sql.FieldGT(FieldID, id)) +} + +// IDGTE applies the GTE predicate on the ID field. +func IDGTE(id uuid.UUID) predicate.Schedule { + return predicate.Schedule(sql.FieldGTE(FieldID, id)) +} + +// IDLT applies the LT predicate on the ID field. +func IDLT(id uuid.UUID) predicate.Schedule { + return predicate.Schedule(sql.FieldLT(FieldID, id)) +} + +// IDLTE applies the LTE predicate on the ID field. +func IDLTE(id uuid.UUID) predicate.Schedule { + return predicate.Schedule(sql.FieldLTE(FieldID, id)) +} + +// ProjectID applies equality check predicate on the "project_id" field. It's identical to ProjectIDEQ. +func ProjectID(v uuid.UUID) predicate.Schedule { + return predicate.Schedule(sql.FieldEQ(FieldProjectID, v)) +} + +// Name applies equality check predicate on the "name" field. It's identical to NameEQ. +func Name(v string) predicate.Schedule { + return predicate.Schedule(sql.FieldEQ(FieldName, v)) +} + +// CronExpr applies equality check predicate on the "cron_expr" field. It's identical to CronExprEQ. +func CronExpr(v string) predicate.Schedule { + return predicate.Schedule(sql.FieldEQ(FieldCronExpr, v)) +} + +// EventType applies equality check predicate on the "event_type" field. It's identical to EventTypeEQ. +func EventType(v string) predicate.Schedule { + return predicate.Schedule(sql.FieldEQ(FieldEventType, v)) +} + +// Payload applies equality check predicate on the "payload" field. It's identical to PayloadEQ. +func Payload(v string) predicate.Schedule { + return predicate.Schedule(sql.FieldEQ(FieldPayload, v)) +} + +// Status applies equality check predicate on the "status" field. It's identical to StatusEQ. +func Status(v string) predicate.Schedule { + return predicate.Schedule(sql.FieldEQ(FieldStatus, v)) +} + +// NextRunAt applies equality check predicate on the "next_run_at" field. It's identical to NextRunAtEQ. +func NextRunAt(v time.Time) predicate.Schedule { + return predicate.Schedule(sql.FieldEQ(FieldNextRunAt, v)) +} + +// LastRunAt applies equality check predicate on the "last_run_at" field. It's identical to LastRunAtEQ. +func LastRunAt(v time.Time) predicate.Schedule { + return predicate.Schedule(sql.FieldEQ(FieldLastRunAt, v)) +} + +// LastRunStatus applies equality check predicate on the "last_run_status" field. It's identical to LastRunStatusEQ. +func LastRunStatus(v string) predicate.Schedule { + return predicate.Schedule(sql.FieldEQ(FieldLastRunStatus, v)) +} + +// LastRunError applies equality check predicate on the "last_run_error" field. It's identical to LastRunErrorEQ. +func LastRunError(v string) predicate.Schedule { + return predicate.Schedule(sql.FieldEQ(FieldLastRunError, v)) +} + +// RunCount applies equality check predicate on the "run_count" field. It's identical to RunCountEQ. +func RunCount(v int) predicate.Schedule { + return predicate.Schedule(sql.FieldEQ(FieldRunCount, v)) +} + +// ErrorCount applies equality check predicate on the "error_count" field. It's identical to ErrorCountEQ. +func ErrorCount(v int) predicate.Schedule { + return predicate.Schedule(sql.FieldEQ(FieldErrorCount, v)) +} + +// CreatedBy applies equality check predicate on the "created_by" field. It's identical to CreatedByEQ. +func CreatedBy(v string) predicate.Schedule { + return predicate.Schedule(sql.FieldEQ(FieldCreatedBy, v)) +} + +// Created applies equality check predicate on the "created" field. It's identical to CreatedEQ. +func Created(v time.Time) predicate.Schedule { + return predicate.Schedule(sql.FieldEQ(FieldCreated, v)) +} + +// Updated applies equality check predicate on the "updated" field. It's identical to UpdatedEQ. +func Updated(v time.Time) predicate.Schedule { + return predicate.Schedule(sql.FieldEQ(FieldUpdated, v)) +} + +// ProjectIDEQ applies the EQ predicate on the "project_id" field. +func ProjectIDEQ(v uuid.UUID) predicate.Schedule { + return predicate.Schedule(sql.FieldEQ(FieldProjectID, v)) +} + +// ProjectIDNEQ applies the NEQ predicate on the "project_id" field. +func ProjectIDNEQ(v uuid.UUID) predicate.Schedule { + return predicate.Schedule(sql.FieldNEQ(FieldProjectID, v)) +} + +// ProjectIDIn applies the In predicate on the "project_id" field. +func ProjectIDIn(vs ...uuid.UUID) predicate.Schedule { + return predicate.Schedule(sql.FieldIn(FieldProjectID, vs...)) +} + +// ProjectIDNotIn applies the NotIn predicate on the "project_id" field. +func ProjectIDNotIn(vs ...uuid.UUID) predicate.Schedule { + return predicate.Schedule(sql.FieldNotIn(FieldProjectID, vs...)) +} + +// ProjectIDGT applies the GT predicate on the "project_id" field. +func ProjectIDGT(v uuid.UUID) predicate.Schedule { + return predicate.Schedule(sql.FieldGT(FieldProjectID, v)) +} + +// ProjectIDGTE applies the GTE predicate on the "project_id" field. +func ProjectIDGTE(v uuid.UUID) predicate.Schedule { + return predicate.Schedule(sql.FieldGTE(FieldProjectID, v)) +} + +// ProjectIDLT applies the LT predicate on the "project_id" field. +func ProjectIDLT(v uuid.UUID) predicate.Schedule { + return predicate.Schedule(sql.FieldLT(FieldProjectID, v)) +} + +// ProjectIDLTE applies the LTE predicate on the "project_id" field. +func ProjectIDLTE(v uuid.UUID) predicate.Schedule { + return predicate.Schedule(sql.FieldLTE(FieldProjectID, v)) +} + +// NameEQ applies the EQ predicate on the "name" field. +func NameEQ(v string) predicate.Schedule { + return predicate.Schedule(sql.FieldEQ(FieldName, v)) +} + +// NameNEQ applies the NEQ predicate on the "name" field. +func NameNEQ(v string) predicate.Schedule { + return predicate.Schedule(sql.FieldNEQ(FieldName, v)) +} + +// NameIn applies the In predicate on the "name" field. +func NameIn(vs ...string) predicate.Schedule { + return predicate.Schedule(sql.FieldIn(FieldName, vs...)) +} + +// NameNotIn applies the NotIn predicate on the "name" field. +func NameNotIn(vs ...string) predicate.Schedule { + return predicate.Schedule(sql.FieldNotIn(FieldName, vs...)) +} + +// NameGT applies the GT predicate on the "name" field. +func NameGT(v string) predicate.Schedule { + return predicate.Schedule(sql.FieldGT(FieldName, v)) +} + +// NameGTE applies the GTE predicate on the "name" field. +func NameGTE(v string) predicate.Schedule { + return predicate.Schedule(sql.FieldGTE(FieldName, v)) +} + +// NameLT applies the LT predicate on the "name" field. +func NameLT(v string) predicate.Schedule { + return predicate.Schedule(sql.FieldLT(FieldName, v)) +} + +// NameLTE applies the LTE predicate on the "name" field. +func NameLTE(v string) predicate.Schedule { + return predicate.Schedule(sql.FieldLTE(FieldName, v)) +} + +// NameContains applies the Contains predicate on the "name" field. +func NameContains(v string) predicate.Schedule { + return predicate.Schedule(sql.FieldContains(FieldName, v)) +} + +// NameHasPrefix applies the HasPrefix predicate on the "name" field. +func NameHasPrefix(v string) predicate.Schedule { + return predicate.Schedule(sql.FieldHasPrefix(FieldName, v)) +} + +// NameHasSuffix applies the HasSuffix predicate on the "name" field. +func NameHasSuffix(v string) predicate.Schedule { + return predicate.Schedule(sql.FieldHasSuffix(FieldName, v)) +} + +// NameEqualFold applies the EqualFold predicate on the "name" field. +func NameEqualFold(v string) predicate.Schedule { + return predicate.Schedule(sql.FieldEqualFold(FieldName, v)) +} + +// NameContainsFold applies the ContainsFold predicate on the "name" field. +func NameContainsFold(v string) predicate.Schedule { + return predicate.Schedule(sql.FieldContainsFold(FieldName, v)) +} + +// CronExprEQ applies the EQ predicate on the "cron_expr" field. +func CronExprEQ(v string) predicate.Schedule { + return predicate.Schedule(sql.FieldEQ(FieldCronExpr, v)) +} + +// CronExprNEQ applies the NEQ predicate on the "cron_expr" field. +func CronExprNEQ(v string) predicate.Schedule { + return predicate.Schedule(sql.FieldNEQ(FieldCronExpr, v)) +} + +// CronExprIn applies the In predicate on the "cron_expr" field. +func CronExprIn(vs ...string) predicate.Schedule { + return predicate.Schedule(sql.FieldIn(FieldCronExpr, vs...)) +} + +// CronExprNotIn applies the NotIn predicate on the "cron_expr" field. +func CronExprNotIn(vs ...string) predicate.Schedule { + return predicate.Schedule(sql.FieldNotIn(FieldCronExpr, vs...)) +} + +// CronExprGT applies the GT predicate on the "cron_expr" field. +func CronExprGT(v string) predicate.Schedule { + return predicate.Schedule(sql.FieldGT(FieldCronExpr, v)) +} + +// CronExprGTE applies the GTE predicate on the "cron_expr" field. +func CronExprGTE(v string) predicate.Schedule { + return predicate.Schedule(sql.FieldGTE(FieldCronExpr, v)) +} + +// CronExprLT applies the LT predicate on the "cron_expr" field. +func CronExprLT(v string) predicate.Schedule { + return predicate.Schedule(sql.FieldLT(FieldCronExpr, v)) +} + +// CronExprLTE applies the LTE predicate on the "cron_expr" field. +func CronExprLTE(v string) predicate.Schedule { + return predicate.Schedule(sql.FieldLTE(FieldCronExpr, v)) +} + +// CronExprContains applies the Contains predicate on the "cron_expr" field. +func CronExprContains(v string) predicate.Schedule { + return predicate.Schedule(sql.FieldContains(FieldCronExpr, v)) +} + +// CronExprHasPrefix applies the HasPrefix predicate on the "cron_expr" field. +func CronExprHasPrefix(v string) predicate.Schedule { + return predicate.Schedule(sql.FieldHasPrefix(FieldCronExpr, v)) +} + +// CronExprHasSuffix applies the HasSuffix predicate on the "cron_expr" field. +func CronExprHasSuffix(v string) predicate.Schedule { + return predicate.Schedule(sql.FieldHasSuffix(FieldCronExpr, v)) +} + +// CronExprEqualFold applies the EqualFold predicate on the "cron_expr" field. +func CronExprEqualFold(v string) predicate.Schedule { + return predicate.Schedule(sql.FieldEqualFold(FieldCronExpr, v)) +} + +// CronExprContainsFold applies the ContainsFold predicate on the "cron_expr" field. +func CronExprContainsFold(v string) predicate.Schedule { + return predicate.Schedule(sql.FieldContainsFold(FieldCronExpr, v)) +} + +// EventTypeEQ applies the EQ predicate on the "event_type" field. +func EventTypeEQ(v string) predicate.Schedule { + return predicate.Schedule(sql.FieldEQ(FieldEventType, v)) +} + +// EventTypeNEQ applies the NEQ predicate on the "event_type" field. +func EventTypeNEQ(v string) predicate.Schedule { + return predicate.Schedule(sql.FieldNEQ(FieldEventType, v)) +} + +// EventTypeIn applies the In predicate on the "event_type" field. +func EventTypeIn(vs ...string) predicate.Schedule { + return predicate.Schedule(sql.FieldIn(FieldEventType, vs...)) +} + +// EventTypeNotIn applies the NotIn predicate on the "event_type" field. +func EventTypeNotIn(vs ...string) predicate.Schedule { + return predicate.Schedule(sql.FieldNotIn(FieldEventType, vs...)) +} + +// EventTypeGT applies the GT predicate on the "event_type" field. +func EventTypeGT(v string) predicate.Schedule { + return predicate.Schedule(sql.FieldGT(FieldEventType, v)) +} + +// EventTypeGTE applies the GTE predicate on the "event_type" field. +func EventTypeGTE(v string) predicate.Schedule { + return predicate.Schedule(sql.FieldGTE(FieldEventType, v)) +} + +// EventTypeLT applies the LT predicate on the "event_type" field. +func EventTypeLT(v string) predicate.Schedule { + return predicate.Schedule(sql.FieldLT(FieldEventType, v)) +} + +// EventTypeLTE applies the LTE predicate on the "event_type" field. +func EventTypeLTE(v string) predicate.Schedule { + return predicate.Schedule(sql.FieldLTE(FieldEventType, v)) +} + +// EventTypeContains applies the Contains predicate on the "event_type" field. +func EventTypeContains(v string) predicate.Schedule { + return predicate.Schedule(sql.FieldContains(FieldEventType, v)) +} + +// EventTypeHasPrefix applies the HasPrefix predicate on the "event_type" field. +func EventTypeHasPrefix(v string) predicate.Schedule { + return predicate.Schedule(sql.FieldHasPrefix(FieldEventType, v)) +} + +// EventTypeHasSuffix applies the HasSuffix predicate on the "event_type" field. +func EventTypeHasSuffix(v string) predicate.Schedule { + return predicate.Schedule(sql.FieldHasSuffix(FieldEventType, v)) +} + +// EventTypeEqualFold applies the EqualFold predicate on the "event_type" field. +func EventTypeEqualFold(v string) predicate.Schedule { + return predicate.Schedule(sql.FieldEqualFold(FieldEventType, v)) +} + +// EventTypeContainsFold applies the ContainsFold predicate on the "event_type" field. +func EventTypeContainsFold(v string) predicate.Schedule { + return predicate.Schedule(sql.FieldContainsFold(FieldEventType, v)) +} + +// PayloadEQ applies the EQ predicate on the "payload" field. +func PayloadEQ(v string) predicate.Schedule { + return predicate.Schedule(sql.FieldEQ(FieldPayload, v)) +} + +// PayloadNEQ applies the NEQ predicate on the "payload" field. +func PayloadNEQ(v string) predicate.Schedule { + return predicate.Schedule(sql.FieldNEQ(FieldPayload, v)) +} + +// PayloadIn applies the In predicate on the "payload" field. +func PayloadIn(vs ...string) predicate.Schedule { + return predicate.Schedule(sql.FieldIn(FieldPayload, vs...)) +} + +// PayloadNotIn applies the NotIn predicate on the "payload" field. +func PayloadNotIn(vs ...string) predicate.Schedule { + return predicate.Schedule(sql.FieldNotIn(FieldPayload, vs...)) +} + +// PayloadGT applies the GT predicate on the "payload" field. +func PayloadGT(v string) predicate.Schedule { + return predicate.Schedule(sql.FieldGT(FieldPayload, v)) +} + +// PayloadGTE applies the GTE predicate on the "payload" field. +func PayloadGTE(v string) predicate.Schedule { + return predicate.Schedule(sql.FieldGTE(FieldPayload, v)) +} + +// PayloadLT applies the LT predicate on the "payload" field. +func PayloadLT(v string) predicate.Schedule { + return predicate.Schedule(sql.FieldLT(FieldPayload, v)) +} + +// PayloadLTE applies the LTE predicate on the "payload" field. +func PayloadLTE(v string) predicate.Schedule { + return predicate.Schedule(sql.FieldLTE(FieldPayload, v)) +} + +// PayloadContains applies the Contains predicate on the "payload" field. +func PayloadContains(v string) predicate.Schedule { + return predicate.Schedule(sql.FieldContains(FieldPayload, v)) +} + +// PayloadHasPrefix applies the HasPrefix predicate on the "payload" field. +func PayloadHasPrefix(v string) predicate.Schedule { + return predicate.Schedule(sql.FieldHasPrefix(FieldPayload, v)) +} + +// PayloadHasSuffix applies the HasSuffix predicate on the "payload" field. +func PayloadHasSuffix(v string) predicate.Schedule { + return predicate.Schedule(sql.FieldHasSuffix(FieldPayload, v)) +} + +// PayloadEqualFold applies the EqualFold predicate on the "payload" field. +func PayloadEqualFold(v string) predicate.Schedule { + return predicate.Schedule(sql.FieldEqualFold(FieldPayload, v)) +} + +// PayloadContainsFold applies the ContainsFold predicate on the "payload" field. +func PayloadContainsFold(v string) predicate.Schedule { + return predicate.Schedule(sql.FieldContainsFold(FieldPayload, v)) +} + +// StatusEQ applies the EQ predicate on the "status" field. +func StatusEQ(v string) predicate.Schedule { + return predicate.Schedule(sql.FieldEQ(FieldStatus, v)) +} + +// StatusNEQ applies the NEQ predicate on the "status" field. +func StatusNEQ(v string) predicate.Schedule { + return predicate.Schedule(sql.FieldNEQ(FieldStatus, v)) +} + +// StatusIn applies the In predicate on the "status" field. +func StatusIn(vs ...string) predicate.Schedule { + return predicate.Schedule(sql.FieldIn(FieldStatus, vs...)) +} + +// StatusNotIn applies the NotIn predicate on the "status" field. +func StatusNotIn(vs ...string) predicate.Schedule { + return predicate.Schedule(sql.FieldNotIn(FieldStatus, vs...)) +} + +// StatusGT applies the GT predicate on the "status" field. +func StatusGT(v string) predicate.Schedule { + return predicate.Schedule(sql.FieldGT(FieldStatus, v)) +} + +// StatusGTE applies the GTE predicate on the "status" field. +func StatusGTE(v string) predicate.Schedule { + return predicate.Schedule(sql.FieldGTE(FieldStatus, v)) +} + +// StatusLT applies the LT predicate on the "status" field. +func StatusLT(v string) predicate.Schedule { + return predicate.Schedule(sql.FieldLT(FieldStatus, v)) +} + +// StatusLTE applies the LTE predicate on the "status" field. +func StatusLTE(v string) predicate.Schedule { + return predicate.Schedule(sql.FieldLTE(FieldStatus, v)) +} + +// StatusContains applies the Contains predicate on the "status" field. +func StatusContains(v string) predicate.Schedule { + return predicate.Schedule(sql.FieldContains(FieldStatus, v)) +} + +// StatusHasPrefix applies the HasPrefix predicate on the "status" field. +func StatusHasPrefix(v string) predicate.Schedule { + return predicate.Schedule(sql.FieldHasPrefix(FieldStatus, v)) +} + +// StatusHasSuffix applies the HasSuffix predicate on the "status" field. +func StatusHasSuffix(v string) predicate.Schedule { + return predicate.Schedule(sql.FieldHasSuffix(FieldStatus, v)) +} + +// StatusEqualFold applies the EqualFold predicate on the "status" field. +func StatusEqualFold(v string) predicate.Schedule { + return predicate.Schedule(sql.FieldEqualFold(FieldStatus, v)) +} + +// StatusContainsFold applies the ContainsFold predicate on the "status" field. +func StatusContainsFold(v string) predicate.Schedule { + return predicate.Schedule(sql.FieldContainsFold(FieldStatus, v)) +} + +// NextRunAtEQ applies the EQ predicate on the "next_run_at" field. +func NextRunAtEQ(v time.Time) predicate.Schedule { + return predicate.Schedule(sql.FieldEQ(FieldNextRunAt, v)) +} + +// NextRunAtNEQ applies the NEQ predicate on the "next_run_at" field. +func NextRunAtNEQ(v time.Time) predicate.Schedule { + return predicate.Schedule(sql.FieldNEQ(FieldNextRunAt, v)) +} + +// NextRunAtIn applies the In predicate on the "next_run_at" field. +func NextRunAtIn(vs ...time.Time) predicate.Schedule { + return predicate.Schedule(sql.FieldIn(FieldNextRunAt, vs...)) +} + +// NextRunAtNotIn applies the NotIn predicate on the "next_run_at" field. +func NextRunAtNotIn(vs ...time.Time) predicate.Schedule { + return predicate.Schedule(sql.FieldNotIn(FieldNextRunAt, vs...)) +} + +// NextRunAtGT applies the GT predicate on the "next_run_at" field. +func NextRunAtGT(v time.Time) predicate.Schedule { + return predicate.Schedule(sql.FieldGT(FieldNextRunAt, v)) +} + +// NextRunAtGTE applies the GTE predicate on the "next_run_at" field. +func NextRunAtGTE(v time.Time) predicate.Schedule { + return predicate.Schedule(sql.FieldGTE(FieldNextRunAt, v)) +} + +// NextRunAtLT applies the LT predicate on the "next_run_at" field. +func NextRunAtLT(v time.Time) predicate.Schedule { + return predicate.Schedule(sql.FieldLT(FieldNextRunAt, v)) +} + +// NextRunAtLTE applies the LTE predicate on the "next_run_at" field. +func NextRunAtLTE(v time.Time) predicate.Schedule { + return predicate.Schedule(sql.FieldLTE(FieldNextRunAt, v)) +} + +// NextRunAtIsNil applies the IsNil predicate on the "next_run_at" field. +func NextRunAtIsNil() predicate.Schedule { + return predicate.Schedule(sql.FieldIsNull(FieldNextRunAt)) +} + +// NextRunAtNotNil applies the NotNil predicate on the "next_run_at" field. +func NextRunAtNotNil() predicate.Schedule { + return predicate.Schedule(sql.FieldNotNull(FieldNextRunAt)) +} + +// LastRunAtEQ applies the EQ predicate on the "last_run_at" field. +func LastRunAtEQ(v time.Time) predicate.Schedule { + return predicate.Schedule(sql.FieldEQ(FieldLastRunAt, v)) +} + +// LastRunAtNEQ applies the NEQ predicate on the "last_run_at" field. +func LastRunAtNEQ(v time.Time) predicate.Schedule { + return predicate.Schedule(sql.FieldNEQ(FieldLastRunAt, v)) +} + +// LastRunAtIn applies the In predicate on the "last_run_at" field. +func LastRunAtIn(vs ...time.Time) predicate.Schedule { + return predicate.Schedule(sql.FieldIn(FieldLastRunAt, vs...)) +} + +// LastRunAtNotIn applies the NotIn predicate on the "last_run_at" field. +func LastRunAtNotIn(vs ...time.Time) predicate.Schedule { + return predicate.Schedule(sql.FieldNotIn(FieldLastRunAt, vs...)) +} + +// LastRunAtGT applies the GT predicate on the "last_run_at" field. +func LastRunAtGT(v time.Time) predicate.Schedule { + return predicate.Schedule(sql.FieldGT(FieldLastRunAt, v)) +} + +// LastRunAtGTE applies the GTE predicate on the "last_run_at" field. +func LastRunAtGTE(v time.Time) predicate.Schedule { + return predicate.Schedule(sql.FieldGTE(FieldLastRunAt, v)) +} + +// LastRunAtLT applies the LT predicate on the "last_run_at" field. +func LastRunAtLT(v time.Time) predicate.Schedule { + return predicate.Schedule(sql.FieldLT(FieldLastRunAt, v)) +} + +// LastRunAtLTE applies the LTE predicate on the "last_run_at" field. +func LastRunAtLTE(v time.Time) predicate.Schedule { + return predicate.Schedule(sql.FieldLTE(FieldLastRunAt, v)) +} + +// LastRunAtIsNil applies the IsNil predicate on the "last_run_at" field. +func LastRunAtIsNil() predicate.Schedule { + return predicate.Schedule(sql.FieldIsNull(FieldLastRunAt)) +} + +// LastRunAtNotNil applies the NotNil predicate on the "last_run_at" field. +func LastRunAtNotNil() predicate.Schedule { + return predicate.Schedule(sql.FieldNotNull(FieldLastRunAt)) +} + +// LastRunStatusEQ applies the EQ predicate on the "last_run_status" field. +func LastRunStatusEQ(v string) predicate.Schedule { + return predicate.Schedule(sql.FieldEQ(FieldLastRunStatus, v)) +} + +// LastRunStatusNEQ applies the NEQ predicate on the "last_run_status" field. +func LastRunStatusNEQ(v string) predicate.Schedule { + return predicate.Schedule(sql.FieldNEQ(FieldLastRunStatus, v)) +} + +// LastRunStatusIn applies the In predicate on the "last_run_status" field. +func LastRunStatusIn(vs ...string) predicate.Schedule { + return predicate.Schedule(sql.FieldIn(FieldLastRunStatus, vs...)) +} + +// LastRunStatusNotIn applies the NotIn predicate on the "last_run_status" field. +func LastRunStatusNotIn(vs ...string) predicate.Schedule { + return predicate.Schedule(sql.FieldNotIn(FieldLastRunStatus, vs...)) +} + +// LastRunStatusGT applies the GT predicate on the "last_run_status" field. +func LastRunStatusGT(v string) predicate.Schedule { + return predicate.Schedule(sql.FieldGT(FieldLastRunStatus, v)) +} + +// LastRunStatusGTE applies the GTE predicate on the "last_run_status" field. +func LastRunStatusGTE(v string) predicate.Schedule { + return predicate.Schedule(sql.FieldGTE(FieldLastRunStatus, v)) +} + +// LastRunStatusLT applies the LT predicate on the "last_run_status" field. +func LastRunStatusLT(v string) predicate.Schedule { + return predicate.Schedule(sql.FieldLT(FieldLastRunStatus, v)) +} + +// LastRunStatusLTE applies the LTE predicate on the "last_run_status" field. +func LastRunStatusLTE(v string) predicate.Schedule { + return predicate.Schedule(sql.FieldLTE(FieldLastRunStatus, v)) +} + +// LastRunStatusContains applies the Contains predicate on the "last_run_status" field. +func LastRunStatusContains(v string) predicate.Schedule { + return predicate.Schedule(sql.FieldContains(FieldLastRunStatus, v)) +} + +// LastRunStatusHasPrefix applies the HasPrefix predicate on the "last_run_status" field. +func LastRunStatusHasPrefix(v string) predicate.Schedule { + return predicate.Schedule(sql.FieldHasPrefix(FieldLastRunStatus, v)) +} + +// LastRunStatusHasSuffix applies the HasSuffix predicate on the "last_run_status" field. +func LastRunStatusHasSuffix(v string) predicate.Schedule { + return predicate.Schedule(sql.FieldHasSuffix(FieldLastRunStatus, v)) +} + +// LastRunStatusIsNil applies the IsNil predicate on the "last_run_status" field. +func LastRunStatusIsNil() predicate.Schedule { + return predicate.Schedule(sql.FieldIsNull(FieldLastRunStatus)) +} + +// LastRunStatusNotNil applies the NotNil predicate on the "last_run_status" field. +func LastRunStatusNotNil() predicate.Schedule { + return predicate.Schedule(sql.FieldNotNull(FieldLastRunStatus)) +} + +// LastRunStatusEqualFold applies the EqualFold predicate on the "last_run_status" field. +func LastRunStatusEqualFold(v string) predicate.Schedule { + return predicate.Schedule(sql.FieldEqualFold(FieldLastRunStatus, v)) +} + +// LastRunStatusContainsFold applies the ContainsFold predicate on the "last_run_status" field. +func LastRunStatusContainsFold(v string) predicate.Schedule { + return predicate.Schedule(sql.FieldContainsFold(FieldLastRunStatus, v)) +} + +// LastRunErrorEQ applies the EQ predicate on the "last_run_error" field. +func LastRunErrorEQ(v string) predicate.Schedule { + return predicate.Schedule(sql.FieldEQ(FieldLastRunError, v)) +} + +// LastRunErrorNEQ applies the NEQ predicate on the "last_run_error" field. +func LastRunErrorNEQ(v string) predicate.Schedule { + return predicate.Schedule(sql.FieldNEQ(FieldLastRunError, v)) +} + +// LastRunErrorIn applies the In predicate on the "last_run_error" field. +func LastRunErrorIn(vs ...string) predicate.Schedule { + return predicate.Schedule(sql.FieldIn(FieldLastRunError, vs...)) +} + +// LastRunErrorNotIn applies the NotIn predicate on the "last_run_error" field. +func LastRunErrorNotIn(vs ...string) predicate.Schedule { + return predicate.Schedule(sql.FieldNotIn(FieldLastRunError, vs...)) +} + +// LastRunErrorGT applies the GT predicate on the "last_run_error" field. +func LastRunErrorGT(v string) predicate.Schedule { + return predicate.Schedule(sql.FieldGT(FieldLastRunError, v)) +} + +// LastRunErrorGTE applies the GTE predicate on the "last_run_error" field. +func LastRunErrorGTE(v string) predicate.Schedule { + return predicate.Schedule(sql.FieldGTE(FieldLastRunError, v)) +} + +// LastRunErrorLT applies the LT predicate on the "last_run_error" field. +func LastRunErrorLT(v string) predicate.Schedule { + return predicate.Schedule(sql.FieldLT(FieldLastRunError, v)) +} + +// LastRunErrorLTE applies the LTE predicate on the "last_run_error" field. +func LastRunErrorLTE(v string) predicate.Schedule { + return predicate.Schedule(sql.FieldLTE(FieldLastRunError, v)) +} + +// LastRunErrorContains applies the Contains predicate on the "last_run_error" field. +func LastRunErrorContains(v string) predicate.Schedule { + return predicate.Schedule(sql.FieldContains(FieldLastRunError, v)) +} + +// LastRunErrorHasPrefix applies the HasPrefix predicate on the "last_run_error" field. +func LastRunErrorHasPrefix(v string) predicate.Schedule { + return predicate.Schedule(sql.FieldHasPrefix(FieldLastRunError, v)) +} + +// LastRunErrorHasSuffix applies the HasSuffix predicate on the "last_run_error" field. +func LastRunErrorHasSuffix(v string) predicate.Schedule { + return predicate.Schedule(sql.FieldHasSuffix(FieldLastRunError, v)) +} + +// LastRunErrorIsNil applies the IsNil predicate on the "last_run_error" field. +func LastRunErrorIsNil() predicate.Schedule { + return predicate.Schedule(sql.FieldIsNull(FieldLastRunError)) +} + +// LastRunErrorNotNil applies the NotNil predicate on the "last_run_error" field. +func LastRunErrorNotNil() predicate.Schedule { + return predicate.Schedule(sql.FieldNotNull(FieldLastRunError)) +} + +// LastRunErrorEqualFold applies the EqualFold predicate on the "last_run_error" field. +func LastRunErrorEqualFold(v string) predicate.Schedule { + return predicate.Schedule(sql.FieldEqualFold(FieldLastRunError, v)) +} + +// LastRunErrorContainsFold applies the ContainsFold predicate on the "last_run_error" field. +func LastRunErrorContainsFold(v string) predicate.Schedule { + return predicate.Schedule(sql.FieldContainsFold(FieldLastRunError, v)) +} + +// RunCountEQ applies the EQ predicate on the "run_count" field. +func RunCountEQ(v int) predicate.Schedule { + return predicate.Schedule(sql.FieldEQ(FieldRunCount, v)) +} + +// RunCountNEQ applies the NEQ predicate on the "run_count" field. +func RunCountNEQ(v int) predicate.Schedule { + return predicate.Schedule(sql.FieldNEQ(FieldRunCount, v)) +} + +// RunCountIn applies the In predicate on the "run_count" field. +func RunCountIn(vs ...int) predicate.Schedule { + return predicate.Schedule(sql.FieldIn(FieldRunCount, vs...)) +} + +// RunCountNotIn applies the NotIn predicate on the "run_count" field. +func RunCountNotIn(vs ...int) predicate.Schedule { + return predicate.Schedule(sql.FieldNotIn(FieldRunCount, vs...)) +} + +// RunCountGT applies the GT predicate on the "run_count" field. +func RunCountGT(v int) predicate.Schedule { + return predicate.Schedule(sql.FieldGT(FieldRunCount, v)) +} + +// RunCountGTE applies the GTE predicate on the "run_count" field. +func RunCountGTE(v int) predicate.Schedule { + return predicate.Schedule(sql.FieldGTE(FieldRunCount, v)) +} + +// RunCountLT applies the LT predicate on the "run_count" field. +func RunCountLT(v int) predicate.Schedule { + return predicate.Schedule(sql.FieldLT(FieldRunCount, v)) +} + +// RunCountLTE applies the LTE predicate on the "run_count" field. +func RunCountLTE(v int) predicate.Schedule { + return predicate.Schedule(sql.FieldLTE(FieldRunCount, v)) +} + +// ErrorCountEQ applies the EQ predicate on the "error_count" field. +func ErrorCountEQ(v int) predicate.Schedule { + return predicate.Schedule(sql.FieldEQ(FieldErrorCount, v)) +} + +// ErrorCountNEQ applies the NEQ predicate on the "error_count" field. +func ErrorCountNEQ(v int) predicate.Schedule { + return predicate.Schedule(sql.FieldNEQ(FieldErrorCount, v)) +} + +// ErrorCountIn applies the In predicate on the "error_count" field. +func ErrorCountIn(vs ...int) predicate.Schedule { + return predicate.Schedule(sql.FieldIn(FieldErrorCount, vs...)) +} + +// ErrorCountNotIn applies the NotIn predicate on the "error_count" field. +func ErrorCountNotIn(vs ...int) predicate.Schedule { + return predicate.Schedule(sql.FieldNotIn(FieldErrorCount, vs...)) +} + +// ErrorCountGT applies the GT predicate on the "error_count" field. +func ErrorCountGT(v int) predicate.Schedule { + return predicate.Schedule(sql.FieldGT(FieldErrorCount, v)) +} + +// ErrorCountGTE applies the GTE predicate on the "error_count" field. +func ErrorCountGTE(v int) predicate.Schedule { + return predicate.Schedule(sql.FieldGTE(FieldErrorCount, v)) +} + +// ErrorCountLT applies the LT predicate on the "error_count" field. +func ErrorCountLT(v int) predicate.Schedule { + return predicate.Schedule(sql.FieldLT(FieldErrorCount, v)) +} + +// ErrorCountLTE applies the LTE predicate on the "error_count" field. +func ErrorCountLTE(v int) predicate.Schedule { + return predicate.Schedule(sql.FieldLTE(FieldErrorCount, v)) +} + +// CreatedByEQ applies the EQ predicate on the "created_by" field. +func CreatedByEQ(v string) predicate.Schedule { + return predicate.Schedule(sql.FieldEQ(FieldCreatedBy, v)) +} + +// CreatedByNEQ applies the NEQ predicate on the "created_by" field. +func CreatedByNEQ(v string) predicate.Schedule { + return predicate.Schedule(sql.FieldNEQ(FieldCreatedBy, v)) +} + +// CreatedByIn applies the In predicate on the "created_by" field. +func CreatedByIn(vs ...string) predicate.Schedule { + return predicate.Schedule(sql.FieldIn(FieldCreatedBy, vs...)) +} + +// CreatedByNotIn applies the NotIn predicate on the "created_by" field. +func CreatedByNotIn(vs ...string) predicate.Schedule { + return predicate.Schedule(sql.FieldNotIn(FieldCreatedBy, vs...)) +} + +// CreatedByGT applies the GT predicate on the "created_by" field. +func CreatedByGT(v string) predicate.Schedule { + return predicate.Schedule(sql.FieldGT(FieldCreatedBy, v)) +} + +// CreatedByGTE applies the GTE predicate on the "created_by" field. +func CreatedByGTE(v string) predicate.Schedule { + return predicate.Schedule(sql.FieldGTE(FieldCreatedBy, v)) +} + +// CreatedByLT applies the LT predicate on the "created_by" field. +func CreatedByLT(v string) predicate.Schedule { + return predicate.Schedule(sql.FieldLT(FieldCreatedBy, v)) +} + +// CreatedByLTE applies the LTE predicate on the "created_by" field. +func CreatedByLTE(v string) predicate.Schedule { + return predicate.Schedule(sql.FieldLTE(FieldCreatedBy, v)) +} + +// CreatedByContains applies the Contains predicate on the "created_by" field. +func CreatedByContains(v string) predicate.Schedule { + return predicate.Schedule(sql.FieldContains(FieldCreatedBy, v)) +} + +// CreatedByHasPrefix applies the HasPrefix predicate on the "created_by" field. +func CreatedByHasPrefix(v string) predicate.Schedule { + return predicate.Schedule(sql.FieldHasPrefix(FieldCreatedBy, v)) +} + +// CreatedByHasSuffix applies the HasSuffix predicate on the "created_by" field. +func CreatedByHasSuffix(v string) predicate.Schedule { + return predicate.Schedule(sql.FieldHasSuffix(FieldCreatedBy, v)) +} + +// CreatedByIsNil applies the IsNil predicate on the "created_by" field. +func CreatedByIsNil() predicate.Schedule { + return predicate.Schedule(sql.FieldIsNull(FieldCreatedBy)) +} + +// CreatedByNotNil applies the NotNil predicate on the "created_by" field. +func CreatedByNotNil() predicate.Schedule { + return predicate.Schedule(sql.FieldNotNull(FieldCreatedBy)) +} + +// CreatedByEqualFold applies the EqualFold predicate on the "created_by" field. +func CreatedByEqualFold(v string) predicate.Schedule { + return predicate.Schedule(sql.FieldEqualFold(FieldCreatedBy, v)) +} + +// CreatedByContainsFold applies the ContainsFold predicate on the "created_by" field. +func CreatedByContainsFold(v string) predicate.Schedule { + return predicate.Schedule(sql.FieldContainsFold(FieldCreatedBy, v)) +} + +// CreatedEQ applies the EQ predicate on the "created" field. +func CreatedEQ(v time.Time) predicate.Schedule { + return predicate.Schedule(sql.FieldEQ(FieldCreated, v)) +} + +// CreatedNEQ applies the NEQ predicate on the "created" field. +func CreatedNEQ(v time.Time) predicate.Schedule { + return predicate.Schedule(sql.FieldNEQ(FieldCreated, v)) +} + +// CreatedIn applies the In predicate on the "created" field. +func CreatedIn(vs ...time.Time) predicate.Schedule { + return predicate.Schedule(sql.FieldIn(FieldCreated, vs...)) +} + +// CreatedNotIn applies the NotIn predicate on the "created" field. +func CreatedNotIn(vs ...time.Time) predicate.Schedule { + return predicate.Schedule(sql.FieldNotIn(FieldCreated, vs...)) +} + +// CreatedGT applies the GT predicate on the "created" field. +func CreatedGT(v time.Time) predicate.Schedule { + return predicate.Schedule(sql.FieldGT(FieldCreated, v)) +} + +// CreatedGTE applies the GTE predicate on the "created" field. +func CreatedGTE(v time.Time) predicate.Schedule { + return predicate.Schedule(sql.FieldGTE(FieldCreated, v)) +} + +// CreatedLT applies the LT predicate on the "created" field. +func CreatedLT(v time.Time) predicate.Schedule { + return predicate.Schedule(sql.FieldLT(FieldCreated, v)) +} + +// CreatedLTE applies the LTE predicate on the "created" field. +func CreatedLTE(v time.Time) predicate.Schedule { + return predicate.Schedule(sql.FieldLTE(FieldCreated, v)) +} + +// UpdatedEQ applies the EQ predicate on the "updated" field. +func UpdatedEQ(v time.Time) predicate.Schedule { + return predicate.Schedule(sql.FieldEQ(FieldUpdated, v)) +} + +// UpdatedNEQ applies the NEQ predicate on the "updated" field. +func UpdatedNEQ(v time.Time) predicate.Schedule { + return predicate.Schedule(sql.FieldNEQ(FieldUpdated, v)) +} + +// UpdatedIn applies the In predicate on the "updated" field. +func UpdatedIn(vs ...time.Time) predicate.Schedule { + return predicate.Schedule(sql.FieldIn(FieldUpdated, vs...)) +} + +// UpdatedNotIn applies the NotIn predicate on the "updated" field. +func UpdatedNotIn(vs ...time.Time) predicate.Schedule { + return predicate.Schedule(sql.FieldNotIn(FieldUpdated, vs...)) +} + +// UpdatedGT applies the GT predicate on the "updated" field. +func UpdatedGT(v time.Time) predicate.Schedule { + return predicate.Schedule(sql.FieldGT(FieldUpdated, v)) +} + +// UpdatedGTE applies the GTE predicate on the "updated" field. +func UpdatedGTE(v time.Time) predicate.Schedule { + return predicate.Schedule(sql.FieldGTE(FieldUpdated, v)) +} + +// UpdatedLT applies the LT predicate on the "updated" field. +func UpdatedLT(v time.Time) predicate.Schedule { + return predicate.Schedule(sql.FieldLT(FieldUpdated, v)) +} + +// UpdatedLTE applies the LTE predicate on the "updated" field. +func UpdatedLTE(v time.Time) predicate.Schedule { + return predicate.Schedule(sql.FieldLTE(FieldUpdated, v)) +} + +// And groups predicates with the AND operator between them. +func And(predicates ...predicate.Schedule) predicate.Schedule { + return predicate.Schedule(sql.AndPredicates(predicates...)) +} + +// Or groups predicates with the OR operator between them. +func Or(predicates ...predicate.Schedule) predicate.Schedule { + return predicate.Schedule(sql.OrPredicates(predicates...)) +} + +// Not applies the not operator on the given predicate. +func Not(p predicate.Schedule) predicate.Schedule { + return predicate.Schedule(sql.NotPredicates(p)) +} diff --git a/pkg/ent/schedule_create.go b/pkg/ent/schedule_create.go new file mode 100644 index 000000000..ecc44066c --- /dev/null +++ b/pkg/ent/schedule_create.go @@ -0,0 +1,1469 @@ +// Code generated by ent, DO NOT EDIT. + +package ent + +import ( + "context" + "errors" + "fmt" + "time" + + "entgo.io/ent/dialect" + "entgo.io/ent/dialect/sql" + "entgo.io/ent/dialect/sql/sqlgraph" + "entgo.io/ent/schema/field" + "github.com/GoogleCloudPlatform/scion/pkg/ent/schedule" + "github.com/google/uuid" +) + +// ScheduleCreate is the builder for creating a Schedule entity. +type ScheduleCreate struct { + config + mutation *ScheduleMutation + hooks []Hook + conflict []sql.ConflictOption +} + +// SetProjectID sets the "project_id" field. +func (_c *ScheduleCreate) SetProjectID(v uuid.UUID) *ScheduleCreate { + _c.mutation.SetProjectID(v) + return _c +} + +// SetName sets the "name" field. +func (_c *ScheduleCreate) SetName(v string) *ScheduleCreate { + _c.mutation.SetName(v) + return _c +} + +// SetCronExpr sets the "cron_expr" field. +func (_c *ScheduleCreate) SetCronExpr(v string) *ScheduleCreate { + _c.mutation.SetCronExpr(v) + return _c +} + +// SetEventType sets the "event_type" field. +func (_c *ScheduleCreate) SetEventType(v string) *ScheduleCreate { + _c.mutation.SetEventType(v) + return _c +} + +// SetPayload sets the "payload" field. +func (_c *ScheduleCreate) SetPayload(v string) *ScheduleCreate { + _c.mutation.SetPayload(v) + return _c +} + +// SetNillablePayload sets the "payload" field if the given value is not nil. +func (_c *ScheduleCreate) SetNillablePayload(v *string) *ScheduleCreate { + if v != nil { + _c.SetPayload(*v) + } + return _c +} + +// SetStatus sets the "status" field. +func (_c *ScheduleCreate) SetStatus(v string) *ScheduleCreate { + _c.mutation.SetStatus(v) + return _c +} + +// SetNillableStatus sets the "status" field if the given value is not nil. +func (_c *ScheduleCreate) SetNillableStatus(v *string) *ScheduleCreate { + if v != nil { + _c.SetStatus(*v) + } + return _c +} + +// SetNextRunAt sets the "next_run_at" field. +func (_c *ScheduleCreate) SetNextRunAt(v time.Time) *ScheduleCreate { + _c.mutation.SetNextRunAt(v) + return _c +} + +// SetNillableNextRunAt sets the "next_run_at" field if the given value is not nil. +func (_c *ScheduleCreate) SetNillableNextRunAt(v *time.Time) *ScheduleCreate { + if v != nil { + _c.SetNextRunAt(*v) + } + return _c +} + +// SetLastRunAt sets the "last_run_at" field. +func (_c *ScheduleCreate) SetLastRunAt(v time.Time) *ScheduleCreate { + _c.mutation.SetLastRunAt(v) + return _c +} + +// SetNillableLastRunAt sets the "last_run_at" field if the given value is not nil. +func (_c *ScheduleCreate) SetNillableLastRunAt(v *time.Time) *ScheduleCreate { + if v != nil { + _c.SetLastRunAt(*v) + } + return _c +} + +// SetLastRunStatus sets the "last_run_status" field. +func (_c *ScheduleCreate) SetLastRunStatus(v string) *ScheduleCreate { + _c.mutation.SetLastRunStatus(v) + return _c +} + +// SetNillableLastRunStatus sets the "last_run_status" field if the given value is not nil. +func (_c *ScheduleCreate) SetNillableLastRunStatus(v *string) *ScheduleCreate { + if v != nil { + _c.SetLastRunStatus(*v) + } + return _c +} + +// SetLastRunError sets the "last_run_error" field. +func (_c *ScheduleCreate) SetLastRunError(v string) *ScheduleCreate { + _c.mutation.SetLastRunError(v) + return _c +} + +// SetNillableLastRunError sets the "last_run_error" field if the given value is not nil. +func (_c *ScheduleCreate) SetNillableLastRunError(v *string) *ScheduleCreate { + if v != nil { + _c.SetLastRunError(*v) + } + return _c +} + +// SetRunCount sets the "run_count" field. +func (_c *ScheduleCreate) SetRunCount(v int) *ScheduleCreate { + _c.mutation.SetRunCount(v) + return _c +} + +// SetNillableRunCount sets the "run_count" field if the given value is not nil. +func (_c *ScheduleCreate) SetNillableRunCount(v *int) *ScheduleCreate { + if v != nil { + _c.SetRunCount(*v) + } + return _c +} + +// SetErrorCount sets the "error_count" field. +func (_c *ScheduleCreate) SetErrorCount(v int) *ScheduleCreate { + _c.mutation.SetErrorCount(v) + return _c +} + +// SetNillableErrorCount sets the "error_count" field if the given value is not nil. +func (_c *ScheduleCreate) SetNillableErrorCount(v *int) *ScheduleCreate { + if v != nil { + _c.SetErrorCount(*v) + } + return _c +} + +// SetCreatedBy sets the "created_by" field. +func (_c *ScheduleCreate) SetCreatedBy(v string) *ScheduleCreate { + _c.mutation.SetCreatedBy(v) + return _c +} + +// SetNillableCreatedBy sets the "created_by" field if the given value is not nil. +func (_c *ScheduleCreate) SetNillableCreatedBy(v *string) *ScheduleCreate { + if v != nil { + _c.SetCreatedBy(*v) + } + return _c +} + +// SetCreated sets the "created" field. +func (_c *ScheduleCreate) SetCreated(v time.Time) *ScheduleCreate { + _c.mutation.SetCreated(v) + return _c +} + +// SetNillableCreated sets the "created" field if the given value is not nil. +func (_c *ScheduleCreate) SetNillableCreated(v *time.Time) *ScheduleCreate { + if v != nil { + _c.SetCreated(*v) + } + return _c +} + +// SetUpdated sets the "updated" field. +func (_c *ScheduleCreate) SetUpdated(v time.Time) *ScheduleCreate { + _c.mutation.SetUpdated(v) + return _c +} + +// SetNillableUpdated sets the "updated" field if the given value is not nil. +func (_c *ScheduleCreate) SetNillableUpdated(v *time.Time) *ScheduleCreate { + if v != nil { + _c.SetUpdated(*v) + } + return _c +} + +// SetID sets the "id" field. +func (_c *ScheduleCreate) SetID(v uuid.UUID) *ScheduleCreate { + _c.mutation.SetID(v) + return _c +} + +// SetNillableID sets the "id" field if the given value is not nil. +func (_c *ScheduleCreate) SetNillableID(v *uuid.UUID) *ScheduleCreate { + if v != nil { + _c.SetID(*v) + } + return _c +} + +// Mutation returns the ScheduleMutation object of the builder. +func (_c *ScheduleCreate) Mutation() *ScheduleMutation { + return _c.mutation +} + +// Save creates the Schedule in the database. +func (_c *ScheduleCreate) Save(ctx context.Context) (*Schedule, error) { + _c.defaults() + return withHooks(ctx, _c.sqlSave, _c.mutation, _c.hooks) +} + +// SaveX calls Save and panics if Save returns an error. +func (_c *ScheduleCreate) SaveX(ctx context.Context) *Schedule { + v, err := _c.Save(ctx) + if err != nil { + panic(err) + } + return v +} + +// Exec executes the query. +func (_c *ScheduleCreate) Exec(ctx context.Context) error { + _, err := _c.Save(ctx) + return err +} + +// ExecX is like Exec, but panics if an error occurs. +func (_c *ScheduleCreate) ExecX(ctx context.Context) { + if err := _c.Exec(ctx); err != nil { + panic(err) + } +} + +// defaults sets the default values of the builder before save. +func (_c *ScheduleCreate) defaults() { + if _, ok := _c.mutation.Payload(); !ok { + v := schedule.DefaultPayload + _c.mutation.SetPayload(v) + } + if _, ok := _c.mutation.Status(); !ok { + v := schedule.DefaultStatus + _c.mutation.SetStatus(v) + } + if _, ok := _c.mutation.RunCount(); !ok { + v := schedule.DefaultRunCount + _c.mutation.SetRunCount(v) + } + if _, ok := _c.mutation.ErrorCount(); !ok { + v := schedule.DefaultErrorCount + _c.mutation.SetErrorCount(v) + } + if _, ok := _c.mutation.Created(); !ok { + v := schedule.DefaultCreated() + _c.mutation.SetCreated(v) + } + if _, ok := _c.mutation.Updated(); !ok { + v := schedule.DefaultUpdated() + _c.mutation.SetUpdated(v) + } + if _, ok := _c.mutation.ID(); !ok { + v := schedule.DefaultID() + _c.mutation.SetID(v) + } +} + +// check runs all checks and user-defined validators on the builder. +func (_c *ScheduleCreate) check() error { + if _, ok := _c.mutation.ProjectID(); !ok { + return &ValidationError{Name: "project_id", err: errors.New(`ent: missing required field "Schedule.project_id"`)} + } + if _, ok := _c.mutation.Name(); !ok { + return &ValidationError{Name: "name", err: errors.New(`ent: missing required field "Schedule.name"`)} + } + if v, ok := _c.mutation.Name(); ok { + if err := schedule.NameValidator(v); err != nil { + return &ValidationError{Name: "name", err: fmt.Errorf(`ent: validator failed for field "Schedule.name": %w`, err)} + } + } + if _, ok := _c.mutation.CronExpr(); !ok { + return &ValidationError{Name: "cron_expr", err: errors.New(`ent: missing required field "Schedule.cron_expr"`)} + } + if v, ok := _c.mutation.CronExpr(); ok { + if err := schedule.CronExprValidator(v); err != nil { + return &ValidationError{Name: "cron_expr", err: fmt.Errorf(`ent: validator failed for field "Schedule.cron_expr": %w`, err)} + } + } + if _, ok := _c.mutation.EventType(); !ok { + return &ValidationError{Name: "event_type", err: errors.New(`ent: missing required field "Schedule.event_type"`)} + } + if v, ok := _c.mutation.EventType(); ok { + if err := schedule.EventTypeValidator(v); err != nil { + return &ValidationError{Name: "event_type", err: fmt.Errorf(`ent: validator failed for field "Schedule.event_type": %w`, err)} + } + } + if _, ok := _c.mutation.Payload(); !ok { + return &ValidationError{Name: "payload", err: errors.New(`ent: missing required field "Schedule.payload"`)} + } + if _, ok := _c.mutation.Status(); !ok { + return &ValidationError{Name: "status", err: errors.New(`ent: missing required field "Schedule.status"`)} + } + if _, ok := _c.mutation.RunCount(); !ok { + return &ValidationError{Name: "run_count", err: errors.New(`ent: missing required field "Schedule.run_count"`)} + } + if _, ok := _c.mutation.ErrorCount(); !ok { + return &ValidationError{Name: "error_count", err: errors.New(`ent: missing required field "Schedule.error_count"`)} + } + if _, ok := _c.mutation.Created(); !ok { + return &ValidationError{Name: "created", err: errors.New(`ent: missing required field "Schedule.created"`)} + } + if _, ok := _c.mutation.Updated(); !ok { + return &ValidationError{Name: "updated", err: errors.New(`ent: missing required field "Schedule.updated"`)} + } + return nil +} + +func (_c *ScheduleCreate) sqlSave(ctx context.Context) (*Schedule, error) { + if err := _c.check(); err != nil { + return nil, err + } + _node, _spec := _c.createSpec() + if err := sqlgraph.CreateNode(ctx, _c.driver, _spec); err != nil { + if sqlgraph.IsConstraintError(err) { + err = &ConstraintError{msg: err.Error(), wrap: err} + } + return nil, err + } + if _spec.ID.Value != nil { + if id, ok := _spec.ID.Value.(*uuid.UUID); ok { + _node.ID = *id + } else if err := _node.ID.Scan(_spec.ID.Value); err != nil { + return nil, err + } + } + _c.mutation.id = &_node.ID + _c.mutation.done = true + return _node, nil +} + +func (_c *ScheduleCreate) createSpec() (*Schedule, *sqlgraph.CreateSpec) { + var ( + _node = &Schedule{config: _c.config} + _spec = sqlgraph.NewCreateSpec(schedule.Table, sqlgraph.NewFieldSpec(schedule.FieldID, field.TypeUUID)) + ) + _spec.OnConflict = _c.conflict + if id, ok := _c.mutation.ID(); ok { + _node.ID = id + _spec.ID.Value = &id + } + if value, ok := _c.mutation.ProjectID(); ok { + _spec.SetField(schedule.FieldProjectID, field.TypeUUID, value) + _node.ProjectID = value + } + if value, ok := _c.mutation.Name(); ok { + _spec.SetField(schedule.FieldName, field.TypeString, value) + _node.Name = value + } + if value, ok := _c.mutation.CronExpr(); ok { + _spec.SetField(schedule.FieldCronExpr, field.TypeString, value) + _node.CronExpr = value + } + if value, ok := _c.mutation.EventType(); ok { + _spec.SetField(schedule.FieldEventType, field.TypeString, value) + _node.EventType = value + } + if value, ok := _c.mutation.Payload(); ok { + _spec.SetField(schedule.FieldPayload, field.TypeString, value) + _node.Payload = value + } + if value, ok := _c.mutation.Status(); ok { + _spec.SetField(schedule.FieldStatus, field.TypeString, value) + _node.Status = value + } + if value, ok := _c.mutation.NextRunAt(); ok { + _spec.SetField(schedule.FieldNextRunAt, field.TypeTime, value) + _node.NextRunAt = &value + } + if value, ok := _c.mutation.LastRunAt(); ok { + _spec.SetField(schedule.FieldLastRunAt, field.TypeTime, value) + _node.LastRunAt = &value + } + if value, ok := _c.mutation.LastRunStatus(); ok { + _spec.SetField(schedule.FieldLastRunStatus, field.TypeString, value) + _node.LastRunStatus = value + } + if value, ok := _c.mutation.LastRunError(); ok { + _spec.SetField(schedule.FieldLastRunError, field.TypeString, value) + _node.LastRunError = value + } + if value, ok := _c.mutation.RunCount(); ok { + _spec.SetField(schedule.FieldRunCount, field.TypeInt, value) + _node.RunCount = value + } + if value, ok := _c.mutation.ErrorCount(); ok { + _spec.SetField(schedule.FieldErrorCount, field.TypeInt, value) + _node.ErrorCount = value + } + if value, ok := _c.mutation.CreatedBy(); ok { + _spec.SetField(schedule.FieldCreatedBy, field.TypeString, value) + _node.CreatedBy = value + } + if value, ok := _c.mutation.Created(); ok { + _spec.SetField(schedule.FieldCreated, field.TypeTime, value) + _node.Created = value + } + if value, ok := _c.mutation.Updated(); ok { + _spec.SetField(schedule.FieldUpdated, field.TypeTime, value) + _node.Updated = value + } + return _node, _spec +} + +// OnConflict allows configuring the `ON CONFLICT` / `ON DUPLICATE KEY` clause +// of the `INSERT` statement. For example: +// +// client.Schedule.Create(). +// SetProjectID(v). +// OnConflict( +// // Update the row with the new values +// // the was proposed for insertion. +// sql.ResolveWithNewValues(), +// ). +// // Override some of the fields with custom +// // update values. +// Update(func(u *ent.ScheduleUpsert) { +// SetProjectID(v+v). +// }). +// Exec(ctx) +func (_c *ScheduleCreate) OnConflict(opts ...sql.ConflictOption) *ScheduleUpsertOne { + _c.conflict = opts + return &ScheduleUpsertOne{ + create: _c, + } +} + +// OnConflictColumns calls `OnConflict` and configures the columns +// as conflict target. Using this option is equivalent to using: +// +// client.Schedule.Create(). +// OnConflict(sql.ConflictColumns(columns...)). +// Exec(ctx) +func (_c *ScheduleCreate) OnConflictColumns(columns ...string) *ScheduleUpsertOne { + _c.conflict = append(_c.conflict, sql.ConflictColumns(columns...)) + return &ScheduleUpsertOne{ + create: _c, + } +} + +type ( + // ScheduleUpsertOne is the builder for "upsert"-ing + // one Schedule node. + ScheduleUpsertOne struct { + create *ScheduleCreate + } + + // ScheduleUpsert is the "OnConflict" setter. + ScheduleUpsert struct { + *sql.UpdateSet + } +) + +// SetProjectID sets the "project_id" field. +func (u *ScheduleUpsert) SetProjectID(v uuid.UUID) *ScheduleUpsert { + u.Set(schedule.FieldProjectID, v) + return u +} + +// UpdateProjectID sets the "project_id" field to the value that was provided on create. +func (u *ScheduleUpsert) UpdateProjectID() *ScheduleUpsert { + u.SetExcluded(schedule.FieldProjectID) + return u +} + +// SetName sets the "name" field. +func (u *ScheduleUpsert) SetName(v string) *ScheduleUpsert { + u.Set(schedule.FieldName, v) + return u +} + +// UpdateName sets the "name" field to the value that was provided on create. +func (u *ScheduleUpsert) UpdateName() *ScheduleUpsert { + u.SetExcluded(schedule.FieldName) + return u +} + +// SetCronExpr sets the "cron_expr" field. +func (u *ScheduleUpsert) SetCronExpr(v string) *ScheduleUpsert { + u.Set(schedule.FieldCronExpr, v) + return u +} + +// UpdateCronExpr sets the "cron_expr" field to the value that was provided on create. +func (u *ScheduleUpsert) UpdateCronExpr() *ScheduleUpsert { + u.SetExcluded(schedule.FieldCronExpr) + return u +} + +// SetEventType sets the "event_type" field. +func (u *ScheduleUpsert) SetEventType(v string) *ScheduleUpsert { + u.Set(schedule.FieldEventType, v) + return u +} + +// UpdateEventType sets the "event_type" field to the value that was provided on create. +func (u *ScheduleUpsert) UpdateEventType() *ScheduleUpsert { + u.SetExcluded(schedule.FieldEventType) + return u +} + +// SetPayload sets the "payload" field. +func (u *ScheduleUpsert) SetPayload(v string) *ScheduleUpsert { + u.Set(schedule.FieldPayload, v) + return u +} + +// UpdatePayload sets the "payload" field to the value that was provided on create. +func (u *ScheduleUpsert) UpdatePayload() *ScheduleUpsert { + u.SetExcluded(schedule.FieldPayload) + return u +} + +// SetStatus sets the "status" field. +func (u *ScheduleUpsert) SetStatus(v string) *ScheduleUpsert { + u.Set(schedule.FieldStatus, v) + return u +} + +// UpdateStatus sets the "status" field to the value that was provided on create. +func (u *ScheduleUpsert) UpdateStatus() *ScheduleUpsert { + u.SetExcluded(schedule.FieldStatus) + return u +} + +// SetNextRunAt sets the "next_run_at" field. +func (u *ScheduleUpsert) SetNextRunAt(v time.Time) *ScheduleUpsert { + u.Set(schedule.FieldNextRunAt, v) + return u +} + +// UpdateNextRunAt sets the "next_run_at" field to the value that was provided on create. +func (u *ScheduleUpsert) UpdateNextRunAt() *ScheduleUpsert { + u.SetExcluded(schedule.FieldNextRunAt) + return u +} + +// ClearNextRunAt clears the value of the "next_run_at" field. +func (u *ScheduleUpsert) ClearNextRunAt() *ScheduleUpsert { + u.SetNull(schedule.FieldNextRunAt) + return u +} + +// SetLastRunAt sets the "last_run_at" field. +func (u *ScheduleUpsert) SetLastRunAt(v time.Time) *ScheduleUpsert { + u.Set(schedule.FieldLastRunAt, v) + return u +} + +// UpdateLastRunAt sets the "last_run_at" field to the value that was provided on create. +func (u *ScheduleUpsert) UpdateLastRunAt() *ScheduleUpsert { + u.SetExcluded(schedule.FieldLastRunAt) + return u +} + +// ClearLastRunAt clears the value of the "last_run_at" field. +func (u *ScheduleUpsert) ClearLastRunAt() *ScheduleUpsert { + u.SetNull(schedule.FieldLastRunAt) + return u +} + +// SetLastRunStatus sets the "last_run_status" field. +func (u *ScheduleUpsert) SetLastRunStatus(v string) *ScheduleUpsert { + u.Set(schedule.FieldLastRunStatus, v) + return u +} + +// UpdateLastRunStatus sets the "last_run_status" field to the value that was provided on create. +func (u *ScheduleUpsert) UpdateLastRunStatus() *ScheduleUpsert { + u.SetExcluded(schedule.FieldLastRunStatus) + return u +} + +// ClearLastRunStatus clears the value of the "last_run_status" field. +func (u *ScheduleUpsert) ClearLastRunStatus() *ScheduleUpsert { + u.SetNull(schedule.FieldLastRunStatus) + return u +} + +// SetLastRunError sets the "last_run_error" field. +func (u *ScheduleUpsert) SetLastRunError(v string) *ScheduleUpsert { + u.Set(schedule.FieldLastRunError, v) + return u +} + +// UpdateLastRunError sets the "last_run_error" field to the value that was provided on create. +func (u *ScheduleUpsert) UpdateLastRunError() *ScheduleUpsert { + u.SetExcluded(schedule.FieldLastRunError) + return u +} + +// ClearLastRunError clears the value of the "last_run_error" field. +func (u *ScheduleUpsert) ClearLastRunError() *ScheduleUpsert { + u.SetNull(schedule.FieldLastRunError) + return u +} + +// SetRunCount sets the "run_count" field. +func (u *ScheduleUpsert) SetRunCount(v int) *ScheduleUpsert { + u.Set(schedule.FieldRunCount, v) + return u +} + +// UpdateRunCount sets the "run_count" field to the value that was provided on create. +func (u *ScheduleUpsert) UpdateRunCount() *ScheduleUpsert { + u.SetExcluded(schedule.FieldRunCount) + return u +} + +// AddRunCount adds v to the "run_count" field. +func (u *ScheduleUpsert) AddRunCount(v int) *ScheduleUpsert { + u.Add(schedule.FieldRunCount, v) + return u +} + +// SetErrorCount sets the "error_count" field. +func (u *ScheduleUpsert) SetErrorCount(v int) *ScheduleUpsert { + u.Set(schedule.FieldErrorCount, v) + return u +} + +// UpdateErrorCount sets the "error_count" field to the value that was provided on create. +func (u *ScheduleUpsert) UpdateErrorCount() *ScheduleUpsert { + u.SetExcluded(schedule.FieldErrorCount) + return u +} + +// AddErrorCount adds v to the "error_count" field. +func (u *ScheduleUpsert) AddErrorCount(v int) *ScheduleUpsert { + u.Add(schedule.FieldErrorCount, v) + return u +} + +// SetCreatedBy sets the "created_by" field. +func (u *ScheduleUpsert) SetCreatedBy(v string) *ScheduleUpsert { + u.Set(schedule.FieldCreatedBy, v) + return u +} + +// UpdateCreatedBy sets the "created_by" field to the value that was provided on create. +func (u *ScheduleUpsert) UpdateCreatedBy() *ScheduleUpsert { + u.SetExcluded(schedule.FieldCreatedBy) + return u +} + +// ClearCreatedBy clears the value of the "created_by" field. +func (u *ScheduleUpsert) ClearCreatedBy() *ScheduleUpsert { + u.SetNull(schedule.FieldCreatedBy) + return u +} + +// SetUpdated sets the "updated" field. +func (u *ScheduleUpsert) SetUpdated(v time.Time) *ScheduleUpsert { + u.Set(schedule.FieldUpdated, v) + return u +} + +// UpdateUpdated sets the "updated" field to the value that was provided on create. +func (u *ScheduleUpsert) UpdateUpdated() *ScheduleUpsert { + u.SetExcluded(schedule.FieldUpdated) + return u +} + +// UpdateNewValues updates the mutable fields using the new values that were set on create except the ID field. +// Using this option is equivalent to using: +// +// client.Schedule.Create(). +// OnConflict( +// sql.ResolveWithNewValues(), +// sql.ResolveWith(func(u *sql.UpdateSet) { +// u.SetIgnore(schedule.FieldID) +// }), +// ). +// Exec(ctx) +func (u *ScheduleUpsertOne) UpdateNewValues() *ScheduleUpsertOne { + u.create.conflict = append(u.create.conflict, sql.ResolveWithNewValues()) + u.create.conflict = append(u.create.conflict, sql.ResolveWith(func(s *sql.UpdateSet) { + if _, exists := u.create.mutation.ID(); exists { + s.SetIgnore(schedule.FieldID) + } + if _, exists := u.create.mutation.Created(); exists { + s.SetIgnore(schedule.FieldCreated) + } + })) + return u +} + +// Ignore sets each column to itself in case of conflict. +// Using this option is equivalent to using: +// +// client.Schedule.Create(). +// OnConflict(sql.ResolveWithIgnore()). +// Exec(ctx) +func (u *ScheduleUpsertOne) Ignore() *ScheduleUpsertOne { + u.create.conflict = append(u.create.conflict, sql.ResolveWithIgnore()) + return u +} + +// DoNothing configures the conflict_action to `DO NOTHING`. +// Supported only by SQLite and PostgreSQL. +func (u *ScheduleUpsertOne) DoNothing() *ScheduleUpsertOne { + u.create.conflict = append(u.create.conflict, sql.DoNothing()) + return u +} + +// Update allows overriding fields `UPDATE` values. See the ScheduleCreate.OnConflict +// documentation for more info. +func (u *ScheduleUpsertOne) Update(set func(*ScheduleUpsert)) *ScheduleUpsertOne { + u.create.conflict = append(u.create.conflict, sql.ResolveWith(func(update *sql.UpdateSet) { + set(&ScheduleUpsert{UpdateSet: update}) + })) + return u +} + +// SetProjectID sets the "project_id" field. +func (u *ScheduleUpsertOne) SetProjectID(v uuid.UUID) *ScheduleUpsertOne { + return u.Update(func(s *ScheduleUpsert) { + s.SetProjectID(v) + }) +} + +// UpdateProjectID sets the "project_id" field to the value that was provided on create. +func (u *ScheduleUpsertOne) UpdateProjectID() *ScheduleUpsertOne { + return u.Update(func(s *ScheduleUpsert) { + s.UpdateProjectID() + }) +} + +// SetName sets the "name" field. +func (u *ScheduleUpsertOne) SetName(v string) *ScheduleUpsertOne { + return u.Update(func(s *ScheduleUpsert) { + s.SetName(v) + }) +} + +// UpdateName sets the "name" field to the value that was provided on create. +func (u *ScheduleUpsertOne) UpdateName() *ScheduleUpsertOne { + return u.Update(func(s *ScheduleUpsert) { + s.UpdateName() + }) +} + +// SetCronExpr sets the "cron_expr" field. +func (u *ScheduleUpsertOne) SetCronExpr(v string) *ScheduleUpsertOne { + return u.Update(func(s *ScheduleUpsert) { + s.SetCronExpr(v) + }) +} + +// UpdateCronExpr sets the "cron_expr" field to the value that was provided on create. +func (u *ScheduleUpsertOne) UpdateCronExpr() *ScheduleUpsertOne { + return u.Update(func(s *ScheduleUpsert) { + s.UpdateCronExpr() + }) +} + +// SetEventType sets the "event_type" field. +func (u *ScheduleUpsertOne) SetEventType(v string) *ScheduleUpsertOne { + return u.Update(func(s *ScheduleUpsert) { + s.SetEventType(v) + }) +} + +// UpdateEventType sets the "event_type" field to the value that was provided on create. +func (u *ScheduleUpsertOne) UpdateEventType() *ScheduleUpsertOne { + return u.Update(func(s *ScheduleUpsert) { + s.UpdateEventType() + }) +} + +// SetPayload sets the "payload" field. +func (u *ScheduleUpsertOne) SetPayload(v string) *ScheduleUpsertOne { + return u.Update(func(s *ScheduleUpsert) { + s.SetPayload(v) + }) +} + +// UpdatePayload sets the "payload" field to the value that was provided on create. +func (u *ScheduleUpsertOne) UpdatePayload() *ScheduleUpsertOne { + return u.Update(func(s *ScheduleUpsert) { + s.UpdatePayload() + }) +} + +// SetStatus sets the "status" field. +func (u *ScheduleUpsertOne) SetStatus(v string) *ScheduleUpsertOne { + return u.Update(func(s *ScheduleUpsert) { + s.SetStatus(v) + }) +} + +// UpdateStatus sets the "status" field to the value that was provided on create. +func (u *ScheduleUpsertOne) UpdateStatus() *ScheduleUpsertOne { + return u.Update(func(s *ScheduleUpsert) { + s.UpdateStatus() + }) +} + +// SetNextRunAt sets the "next_run_at" field. +func (u *ScheduleUpsertOne) SetNextRunAt(v time.Time) *ScheduleUpsertOne { + return u.Update(func(s *ScheduleUpsert) { + s.SetNextRunAt(v) + }) +} + +// UpdateNextRunAt sets the "next_run_at" field to the value that was provided on create. +func (u *ScheduleUpsertOne) UpdateNextRunAt() *ScheduleUpsertOne { + return u.Update(func(s *ScheduleUpsert) { + s.UpdateNextRunAt() + }) +} + +// ClearNextRunAt clears the value of the "next_run_at" field. +func (u *ScheduleUpsertOne) ClearNextRunAt() *ScheduleUpsertOne { + return u.Update(func(s *ScheduleUpsert) { + s.ClearNextRunAt() + }) +} + +// SetLastRunAt sets the "last_run_at" field. +func (u *ScheduleUpsertOne) SetLastRunAt(v time.Time) *ScheduleUpsertOne { + return u.Update(func(s *ScheduleUpsert) { + s.SetLastRunAt(v) + }) +} + +// UpdateLastRunAt sets the "last_run_at" field to the value that was provided on create. +func (u *ScheduleUpsertOne) UpdateLastRunAt() *ScheduleUpsertOne { + return u.Update(func(s *ScheduleUpsert) { + s.UpdateLastRunAt() + }) +} + +// ClearLastRunAt clears the value of the "last_run_at" field. +func (u *ScheduleUpsertOne) ClearLastRunAt() *ScheduleUpsertOne { + return u.Update(func(s *ScheduleUpsert) { + s.ClearLastRunAt() + }) +} + +// SetLastRunStatus sets the "last_run_status" field. +func (u *ScheduleUpsertOne) SetLastRunStatus(v string) *ScheduleUpsertOne { + return u.Update(func(s *ScheduleUpsert) { + s.SetLastRunStatus(v) + }) +} + +// UpdateLastRunStatus sets the "last_run_status" field to the value that was provided on create. +func (u *ScheduleUpsertOne) UpdateLastRunStatus() *ScheduleUpsertOne { + return u.Update(func(s *ScheduleUpsert) { + s.UpdateLastRunStatus() + }) +} + +// ClearLastRunStatus clears the value of the "last_run_status" field. +func (u *ScheduleUpsertOne) ClearLastRunStatus() *ScheduleUpsertOne { + return u.Update(func(s *ScheduleUpsert) { + s.ClearLastRunStatus() + }) +} + +// SetLastRunError sets the "last_run_error" field. +func (u *ScheduleUpsertOne) SetLastRunError(v string) *ScheduleUpsertOne { + return u.Update(func(s *ScheduleUpsert) { + s.SetLastRunError(v) + }) +} + +// UpdateLastRunError sets the "last_run_error" field to the value that was provided on create. +func (u *ScheduleUpsertOne) UpdateLastRunError() *ScheduleUpsertOne { + return u.Update(func(s *ScheduleUpsert) { + s.UpdateLastRunError() + }) +} + +// ClearLastRunError clears the value of the "last_run_error" field. +func (u *ScheduleUpsertOne) ClearLastRunError() *ScheduleUpsertOne { + return u.Update(func(s *ScheduleUpsert) { + s.ClearLastRunError() + }) +} + +// SetRunCount sets the "run_count" field. +func (u *ScheduleUpsertOne) SetRunCount(v int) *ScheduleUpsertOne { + return u.Update(func(s *ScheduleUpsert) { + s.SetRunCount(v) + }) +} + +// AddRunCount adds v to the "run_count" field. +func (u *ScheduleUpsertOne) AddRunCount(v int) *ScheduleUpsertOne { + return u.Update(func(s *ScheduleUpsert) { + s.AddRunCount(v) + }) +} + +// UpdateRunCount sets the "run_count" field to the value that was provided on create. +func (u *ScheduleUpsertOne) UpdateRunCount() *ScheduleUpsertOne { + return u.Update(func(s *ScheduleUpsert) { + s.UpdateRunCount() + }) +} + +// SetErrorCount sets the "error_count" field. +func (u *ScheduleUpsertOne) SetErrorCount(v int) *ScheduleUpsertOne { + return u.Update(func(s *ScheduleUpsert) { + s.SetErrorCount(v) + }) +} + +// AddErrorCount adds v to the "error_count" field. +func (u *ScheduleUpsertOne) AddErrorCount(v int) *ScheduleUpsertOne { + return u.Update(func(s *ScheduleUpsert) { + s.AddErrorCount(v) + }) +} + +// UpdateErrorCount sets the "error_count" field to the value that was provided on create. +func (u *ScheduleUpsertOne) UpdateErrorCount() *ScheduleUpsertOne { + return u.Update(func(s *ScheduleUpsert) { + s.UpdateErrorCount() + }) +} + +// SetCreatedBy sets the "created_by" field. +func (u *ScheduleUpsertOne) SetCreatedBy(v string) *ScheduleUpsertOne { + return u.Update(func(s *ScheduleUpsert) { + s.SetCreatedBy(v) + }) +} + +// UpdateCreatedBy sets the "created_by" field to the value that was provided on create. +func (u *ScheduleUpsertOne) UpdateCreatedBy() *ScheduleUpsertOne { + return u.Update(func(s *ScheduleUpsert) { + s.UpdateCreatedBy() + }) +} + +// ClearCreatedBy clears the value of the "created_by" field. +func (u *ScheduleUpsertOne) ClearCreatedBy() *ScheduleUpsertOne { + return u.Update(func(s *ScheduleUpsert) { + s.ClearCreatedBy() + }) +} + +// SetUpdated sets the "updated" field. +func (u *ScheduleUpsertOne) SetUpdated(v time.Time) *ScheduleUpsertOne { + return u.Update(func(s *ScheduleUpsert) { + s.SetUpdated(v) + }) +} + +// UpdateUpdated sets the "updated" field to the value that was provided on create. +func (u *ScheduleUpsertOne) UpdateUpdated() *ScheduleUpsertOne { + return u.Update(func(s *ScheduleUpsert) { + s.UpdateUpdated() + }) +} + +// Exec executes the query. +func (u *ScheduleUpsertOne) Exec(ctx context.Context) error { + if len(u.create.conflict) == 0 { + return errors.New("ent: missing options for ScheduleCreate.OnConflict") + } + return u.create.Exec(ctx) +} + +// ExecX is like Exec, but panics if an error occurs. +func (u *ScheduleUpsertOne) ExecX(ctx context.Context) { + if err := u.create.Exec(ctx); err != nil { + panic(err) + } +} + +// Exec executes the UPSERT query and returns the inserted/updated ID. +func (u *ScheduleUpsertOne) ID(ctx context.Context) (id uuid.UUID, err error) { + if u.create.driver.Dialect() == dialect.MySQL { + // In case of "ON CONFLICT", there is no way to get back non-numeric ID + // fields from the database since MySQL does not support the RETURNING clause. + return id, errors.New("ent: ScheduleUpsertOne.ID is not supported by MySQL driver. Use ScheduleUpsertOne.Exec instead") + } + node, err := u.create.Save(ctx) + if err != nil { + return id, err + } + return node.ID, nil +} + +// IDX is like ID, but panics if an error occurs. +func (u *ScheduleUpsertOne) IDX(ctx context.Context) uuid.UUID { + id, err := u.ID(ctx) + if err != nil { + panic(err) + } + return id +} + +// ScheduleCreateBulk is the builder for creating many Schedule entities in bulk. +type ScheduleCreateBulk struct { + config + err error + builders []*ScheduleCreate + conflict []sql.ConflictOption +} + +// Save creates the Schedule entities in the database. +func (_c *ScheduleCreateBulk) Save(ctx context.Context) ([]*Schedule, error) { + if _c.err != nil { + return nil, _c.err + } + specs := make([]*sqlgraph.CreateSpec, len(_c.builders)) + nodes := make([]*Schedule, len(_c.builders)) + mutators := make([]Mutator, len(_c.builders)) + for i := range _c.builders { + func(i int, root context.Context) { + builder := _c.builders[i] + builder.defaults() + var mut Mutator = MutateFunc(func(ctx context.Context, m Mutation) (Value, error) { + mutation, ok := m.(*ScheduleMutation) + if !ok { + return nil, fmt.Errorf("unexpected mutation type %T", m) + } + if err := builder.check(); err != nil { + return nil, err + } + builder.mutation = mutation + var err error + nodes[i], specs[i] = builder.createSpec() + if i < len(mutators)-1 { + _, err = mutators[i+1].Mutate(root, _c.builders[i+1].mutation) + } else { + spec := &sqlgraph.BatchCreateSpec{Nodes: specs} + spec.OnConflict = _c.conflict + // Invoke the actual operation on the latest mutation in the chain. + if err = sqlgraph.BatchCreate(ctx, _c.driver, spec); err != nil { + if sqlgraph.IsConstraintError(err) { + err = &ConstraintError{msg: err.Error(), wrap: err} + } + } + } + if err != nil { + return nil, err + } + mutation.id = &nodes[i].ID + mutation.done = true + return nodes[i], nil + }) + for i := len(builder.hooks) - 1; i >= 0; i-- { + mut = builder.hooks[i](mut) + } + mutators[i] = mut + }(i, ctx) + } + if len(mutators) > 0 { + if _, err := mutators[0].Mutate(ctx, _c.builders[0].mutation); err != nil { + return nil, err + } + } + return nodes, nil +} + +// SaveX is like Save, but panics if an error occurs. +func (_c *ScheduleCreateBulk) SaveX(ctx context.Context) []*Schedule { + v, err := _c.Save(ctx) + if err != nil { + panic(err) + } + return v +} + +// Exec executes the query. +func (_c *ScheduleCreateBulk) Exec(ctx context.Context) error { + _, err := _c.Save(ctx) + return err +} + +// ExecX is like Exec, but panics if an error occurs. +func (_c *ScheduleCreateBulk) ExecX(ctx context.Context) { + if err := _c.Exec(ctx); err != nil { + panic(err) + } +} + +// OnConflict allows configuring the `ON CONFLICT` / `ON DUPLICATE KEY` clause +// of the `INSERT` statement. For example: +// +// client.Schedule.CreateBulk(builders...). +// OnConflict( +// // Update the row with the new values +// // the was proposed for insertion. +// sql.ResolveWithNewValues(), +// ). +// // Override some of the fields with custom +// // update values. +// Update(func(u *ent.ScheduleUpsert) { +// SetProjectID(v+v). +// }). +// Exec(ctx) +func (_c *ScheduleCreateBulk) OnConflict(opts ...sql.ConflictOption) *ScheduleUpsertBulk { + _c.conflict = opts + return &ScheduleUpsertBulk{ + create: _c, + } +} + +// OnConflictColumns calls `OnConflict` and configures the columns +// as conflict target. Using this option is equivalent to using: +// +// client.Schedule.Create(). +// OnConflict(sql.ConflictColumns(columns...)). +// Exec(ctx) +func (_c *ScheduleCreateBulk) OnConflictColumns(columns ...string) *ScheduleUpsertBulk { + _c.conflict = append(_c.conflict, sql.ConflictColumns(columns...)) + return &ScheduleUpsertBulk{ + create: _c, + } +} + +// ScheduleUpsertBulk is the builder for "upsert"-ing +// a bulk of Schedule nodes. +type ScheduleUpsertBulk struct { + create *ScheduleCreateBulk +} + +// UpdateNewValues updates the mutable fields using the new values that +// were set on create. Using this option is equivalent to using: +// +// client.Schedule.Create(). +// OnConflict( +// sql.ResolveWithNewValues(), +// sql.ResolveWith(func(u *sql.UpdateSet) { +// u.SetIgnore(schedule.FieldID) +// }), +// ). +// Exec(ctx) +func (u *ScheduleUpsertBulk) UpdateNewValues() *ScheduleUpsertBulk { + u.create.conflict = append(u.create.conflict, sql.ResolveWithNewValues()) + u.create.conflict = append(u.create.conflict, sql.ResolveWith(func(s *sql.UpdateSet) { + for _, b := range u.create.builders { + if _, exists := b.mutation.ID(); exists { + s.SetIgnore(schedule.FieldID) + } + if _, exists := b.mutation.Created(); exists { + s.SetIgnore(schedule.FieldCreated) + } + } + })) + return u +} + +// Ignore sets each column to itself in case of conflict. +// Using this option is equivalent to using: +// +// client.Schedule.Create(). +// OnConflict(sql.ResolveWithIgnore()). +// Exec(ctx) +func (u *ScheduleUpsertBulk) Ignore() *ScheduleUpsertBulk { + u.create.conflict = append(u.create.conflict, sql.ResolveWithIgnore()) + return u +} + +// DoNothing configures the conflict_action to `DO NOTHING`. +// Supported only by SQLite and PostgreSQL. +func (u *ScheduleUpsertBulk) DoNothing() *ScheduleUpsertBulk { + u.create.conflict = append(u.create.conflict, sql.DoNothing()) + return u +} + +// Update allows overriding fields `UPDATE` values. See the ScheduleCreateBulk.OnConflict +// documentation for more info. +func (u *ScheduleUpsertBulk) Update(set func(*ScheduleUpsert)) *ScheduleUpsertBulk { + u.create.conflict = append(u.create.conflict, sql.ResolveWith(func(update *sql.UpdateSet) { + set(&ScheduleUpsert{UpdateSet: update}) + })) + return u +} + +// SetProjectID sets the "project_id" field. +func (u *ScheduleUpsertBulk) SetProjectID(v uuid.UUID) *ScheduleUpsertBulk { + return u.Update(func(s *ScheduleUpsert) { + s.SetProjectID(v) + }) +} + +// UpdateProjectID sets the "project_id" field to the value that was provided on create. +func (u *ScheduleUpsertBulk) UpdateProjectID() *ScheduleUpsertBulk { + return u.Update(func(s *ScheduleUpsert) { + s.UpdateProjectID() + }) +} + +// SetName sets the "name" field. +func (u *ScheduleUpsertBulk) SetName(v string) *ScheduleUpsertBulk { + return u.Update(func(s *ScheduleUpsert) { + s.SetName(v) + }) +} + +// UpdateName sets the "name" field to the value that was provided on create. +func (u *ScheduleUpsertBulk) UpdateName() *ScheduleUpsertBulk { + return u.Update(func(s *ScheduleUpsert) { + s.UpdateName() + }) +} + +// SetCronExpr sets the "cron_expr" field. +func (u *ScheduleUpsertBulk) SetCronExpr(v string) *ScheduleUpsertBulk { + return u.Update(func(s *ScheduleUpsert) { + s.SetCronExpr(v) + }) +} + +// UpdateCronExpr sets the "cron_expr" field to the value that was provided on create. +func (u *ScheduleUpsertBulk) UpdateCronExpr() *ScheduleUpsertBulk { + return u.Update(func(s *ScheduleUpsert) { + s.UpdateCronExpr() + }) +} + +// SetEventType sets the "event_type" field. +func (u *ScheduleUpsertBulk) SetEventType(v string) *ScheduleUpsertBulk { + return u.Update(func(s *ScheduleUpsert) { + s.SetEventType(v) + }) +} + +// UpdateEventType sets the "event_type" field to the value that was provided on create. +func (u *ScheduleUpsertBulk) UpdateEventType() *ScheduleUpsertBulk { + return u.Update(func(s *ScheduleUpsert) { + s.UpdateEventType() + }) +} + +// SetPayload sets the "payload" field. +func (u *ScheduleUpsertBulk) SetPayload(v string) *ScheduleUpsertBulk { + return u.Update(func(s *ScheduleUpsert) { + s.SetPayload(v) + }) +} + +// UpdatePayload sets the "payload" field to the value that was provided on create. +func (u *ScheduleUpsertBulk) UpdatePayload() *ScheduleUpsertBulk { + return u.Update(func(s *ScheduleUpsert) { + s.UpdatePayload() + }) +} + +// SetStatus sets the "status" field. +func (u *ScheduleUpsertBulk) SetStatus(v string) *ScheduleUpsertBulk { + return u.Update(func(s *ScheduleUpsert) { + s.SetStatus(v) + }) +} + +// UpdateStatus sets the "status" field to the value that was provided on create. +func (u *ScheduleUpsertBulk) UpdateStatus() *ScheduleUpsertBulk { + return u.Update(func(s *ScheduleUpsert) { + s.UpdateStatus() + }) +} + +// SetNextRunAt sets the "next_run_at" field. +func (u *ScheduleUpsertBulk) SetNextRunAt(v time.Time) *ScheduleUpsertBulk { + return u.Update(func(s *ScheduleUpsert) { + s.SetNextRunAt(v) + }) +} + +// UpdateNextRunAt sets the "next_run_at" field to the value that was provided on create. +func (u *ScheduleUpsertBulk) UpdateNextRunAt() *ScheduleUpsertBulk { + return u.Update(func(s *ScheduleUpsert) { + s.UpdateNextRunAt() + }) +} + +// ClearNextRunAt clears the value of the "next_run_at" field. +func (u *ScheduleUpsertBulk) ClearNextRunAt() *ScheduleUpsertBulk { + return u.Update(func(s *ScheduleUpsert) { + s.ClearNextRunAt() + }) +} + +// SetLastRunAt sets the "last_run_at" field. +func (u *ScheduleUpsertBulk) SetLastRunAt(v time.Time) *ScheduleUpsertBulk { + return u.Update(func(s *ScheduleUpsert) { + s.SetLastRunAt(v) + }) +} + +// UpdateLastRunAt sets the "last_run_at" field to the value that was provided on create. +func (u *ScheduleUpsertBulk) UpdateLastRunAt() *ScheduleUpsertBulk { + return u.Update(func(s *ScheduleUpsert) { + s.UpdateLastRunAt() + }) +} + +// ClearLastRunAt clears the value of the "last_run_at" field. +func (u *ScheduleUpsertBulk) ClearLastRunAt() *ScheduleUpsertBulk { + return u.Update(func(s *ScheduleUpsert) { + s.ClearLastRunAt() + }) +} + +// SetLastRunStatus sets the "last_run_status" field. +func (u *ScheduleUpsertBulk) SetLastRunStatus(v string) *ScheduleUpsertBulk { + return u.Update(func(s *ScheduleUpsert) { + s.SetLastRunStatus(v) + }) +} + +// UpdateLastRunStatus sets the "last_run_status" field to the value that was provided on create. +func (u *ScheduleUpsertBulk) UpdateLastRunStatus() *ScheduleUpsertBulk { + return u.Update(func(s *ScheduleUpsert) { + s.UpdateLastRunStatus() + }) +} + +// ClearLastRunStatus clears the value of the "last_run_status" field. +func (u *ScheduleUpsertBulk) ClearLastRunStatus() *ScheduleUpsertBulk { + return u.Update(func(s *ScheduleUpsert) { + s.ClearLastRunStatus() + }) +} + +// SetLastRunError sets the "last_run_error" field. +func (u *ScheduleUpsertBulk) SetLastRunError(v string) *ScheduleUpsertBulk { + return u.Update(func(s *ScheduleUpsert) { + s.SetLastRunError(v) + }) +} + +// UpdateLastRunError sets the "last_run_error" field to the value that was provided on create. +func (u *ScheduleUpsertBulk) UpdateLastRunError() *ScheduleUpsertBulk { + return u.Update(func(s *ScheduleUpsert) { + s.UpdateLastRunError() + }) +} + +// ClearLastRunError clears the value of the "last_run_error" field. +func (u *ScheduleUpsertBulk) ClearLastRunError() *ScheduleUpsertBulk { + return u.Update(func(s *ScheduleUpsert) { + s.ClearLastRunError() + }) +} + +// SetRunCount sets the "run_count" field. +func (u *ScheduleUpsertBulk) SetRunCount(v int) *ScheduleUpsertBulk { + return u.Update(func(s *ScheduleUpsert) { + s.SetRunCount(v) + }) +} + +// AddRunCount adds v to the "run_count" field. +func (u *ScheduleUpsertBulk) AddRunCount(v int) *ScheduleUpsertBulk { + return u.Update(func(s *ScheduleUpsert) { + s.AddRunCount(v) + }) +} + +// UpdateRunCount sets the "run_count" field to the value that was provided on create. +func (u *ScheduleUpsertBulk) UpdateRunCount() *ScheduleUpsertBulk { + return u.Update(func(s *ScheduleUpsert) { + s.UpdateRunCount() + }) +} + +// SetErrorCount sets the "error_count" field. +func (u *ScheduleUpsertBulk) SetErrorCount(v int) *ScheduleUpsertBulk { + return u.Update(func(s *ScheduleUpsert) { + s.SetErrorCount(v) + }) +} + +// AddErrorCount adds v to the "error_count" field. +func (u *ScheduleUpsertBulk) AddErrorCount(v int) *ScheduleUpsertBulk { + return u.Update(func(s *ScheduleUpsert) { + s.AddErrorCount(v) + }) +} + +// UpdateErrorCount sets the "error_count" field to the value that was provided on create. +func (u *ScheduleUpsertBulk) UpdateErrorCount() *ScheduleUpsertBulk { + return u.Update(func(s *ScheduleUpsert) { + s.UpdateErrorCount() + }) +} + +// SetCreatedBy sets the "created_by" field. +func (u *ScheduleUpsertBulk) SetCreatedBy(v string) *ScheduleUpsertBulk { + return u.Update(func(s *ScheduleUpsert) { + s.SetCreatedBy(v) + }) +} + +// UpdateCreatedBy sets the "created_by" field to the value that was provided on create. +func (u *ScheduleUpsertBulk) UpdateCreatedBy() *ScheduleUpsertBulk { + return u.Update(func(s *ScheduleUpsert) { + s.UpdateCreatedBy() + }) +} + +// ClearCreatedBy clears the value of the "created_by" field. +func (u *ScheduleUpsertBulk) ClearCreatedBy() *ScheduleUpsertBulk { + return u.Update(func(s *ScheduleUpsert) { + s.ClearCreatedBy() + }) +} + +// SetUpdated sets the "updated" field. +func (u *ScheduleUpsertBulk) SetUpdated(v time.Time) *ScheduleUpsertBulk { + return u.Update(func(s *ScheduleUpsert) { + s.SetUpdated(v) + }) +} + +// UpdateUpdated sets the "updated" field to the value that was provided on create. +func (u *ScheduleUpsertBulk) UpdateUpdated() *ScheduleUpsertBulk { + return u.Update(func(s *ScheduleUpsert) { + s.UpdateUpdated() + }) +} + +// Exec executes the query. +func (u *ScheduleUpsertBulk) Exec(ctx context.Context) error { + if u.create.err != nil { + return u.create.err + } + for i, b := range u.create.builders { + if len(b.conflict) != 0 { + return fmt.Errorf("ent: OnConflict was set for builder %d. Set it on the ScheduleCreateBulk instead", i) + } + } + if len(u.create.conflict) == 0 { + return errors.New("ent: missing options for ScheduleCreateBulk.OnConflict") + } + return u.create.Exec(ctx) +} + +// ExecX is like Exec, but panics if an error occurs. +func (u *ScheduleUpsertBulk) ExecX(ctx context.Context) { + if err := u.create.Exec(ctx); err != nil { + panic(err) + } +} diff --git a/pkg/ent/schedule_delete.go b/pkg/ent/schedule_delete.go new file mode 100644 index 000000000..23ab83a62 --- /dev/null +++ b/pkg/ent/schedule_delete.go @@ -0,0 +1,88 @@ +// Code generated by ent, DO NOT EDIT. + +package ent + +import ( + "context" + + "entgo.io/ent/dialect/sql" + "entgo.io/ent/dialect/sql/sqlgraph" + "entgo.io/ent/schema/field" + "github.com/GoogleCloudPlatform/scion/pkg/ent/predicate" + "github.com/GoogleCloudPlatform/scion/pkg/ent/schedule" +) + +// ScheduleDelete is the builder for deleting a Schedule entity. +type ScheduleDelete struct { + config + hooks []Hook + mutation *ScheduleMutation +} + +// Where appends a list predicates to the ScheduleDelete builder. +func (_d *ScheduleDelete) Where(ps ...predicate.Schedule) *ScheduleDelete { + _d.mutation.Where(ps...) + return _d +} + +// Exec executes the deletion query and returns how many vertices were deleted. +func (_d *ScheduleDelete) Exec(ctx context.Context) (int, error) { + return withHooks(ctx, _d.sqlExec, _d.mutation, _d.hooks) +} + +// ExecX is like Exec, but panics if an error occurs. +func (_d *ScheduleDelete) ExecX(ctx context.Context) int { + n, err := _d.Exec(ctx) + if err != nil { + panic(err) + } + return n +} + +func (_d *ScheduleDelete) sqlExec(ctx context.Context) (int, error) { + _spec := sqlgraph.NewDeleteSpec(schedule.Table, sqlgraph.NewFieldSpec(schedule.FieldID, field.TypeUUID)) + if ps := _d.mutation.predicates; len(ps) > 0 { + _spec.Predicate = func(selector *sql.Selector) { + for i := range ps { + ps[i](selector) + } + } + } + affected, err := sqlgraph.DeleteNodes(ctx, _d.driver, _spec) + if err != nil && sqlgraph.IsConstraintError(err) { + err = &ConstraintError{msg: err.Error(), wrap: err} + } + _d.mutation.done = true + return affected, err +} + +// ScheduleDeleteOne is the builder for deleting a single Schedule entity. +type ScheduleDeleteOne struct { + _d *ScheduleDelete +} + +// Where appends a list predicates to the ScheduleDelete builder. +func (_d *ScheduleDeleteOne) Where(ps ...predicate.Schedule) *ScheduleDeleteOne { + _d._d.mutation.Where(ps...) + return _d +} + +// Exec executes the deletion query. +func (_d *ScheduleDeleteOne) Exec(ctx context.Context) error { + n, err := _d._d.Exec(ctx) + switch { + case err != nil: + return err + case n == 0: + return &NotFoundError{schedule.Label} + default: + return nil + } +} + +// ExecX is like Exec, but panics if an error occurs. +func (_d *ScheduleDeleteOne) ExecX(ctx context.Context) { + if err := _d.Exec(ctx); err != nil { + panic(err) + } +} diff --git a/pkg/ent/schedule_query.go b/pkg/ent/schedule_query.go new file mode 100644 index 000000000..005b80ab8 --- /dev/null +++ b/pkg/ent/schedule_query.go @@ -0,0 +1,565 @@ +// Code generated by ent, DO NOT EDIT. + +package ent + +import ( + "context" + "fmt" + "math" + + "entgo.io/ent" + "entgo.io/ent/dialect" + "entgo.io/ent/dialect/sql" + "entgo.io/ent/dialect/sql/sqlgraph" + "entgo.io/ent/schema/field" + "github.com/GoogleCloudPlatform/scion/pkg/ent/predicate" + "github.com/GoogleCloudPlatform/scion/pkg/ent/schedule" + "github.com/google/uuid" +) + +// ScheduleQuery is the builder for querying Schedule entities. +type ScheduleQuery struct { + config + ctx *QueryContext + order []schedule.OrderOption + inters []Interceptor + predicates []predicate.Schedule + modifiers []func(*sql.Selector) + // intermediate query (i.e. traversal path). + sql *sql.Selector + path func(context.Context) (*sql.Selector, error) +} + +// Where adds a new predicate for the ScheduleQuery builder. +func (_q *ScheduleQuery) Where(ps ...predicate.Schedule) *ScheduleQuery { + _q.predicates = append(_q.predicates, ps...) + return _q +} + +// Limit the number of records to be returned by this query. +func (_q *ScheduleQuery) Limit(limit int) *ScheduleQuery { + _q.ctx.Limit = &limit + return _q +} + +// Offset to start from. +func (_q *ScheduleQuery) Offset(offset int) *ScheduleQuery { + _q.ctx.Offset = &offset + return _q +} + +// Unique configures the query builder to filter duplicate records on query. +// By default, unique is set to true, and can be disabled using this method. +func (_q *ScheduleQuery) Unique(unique bool) *ScheduleQuery { + _q.ctx.Unique = &unique + return _q +} + +// Order specifies how the records should be ordered. +func (_q *ScheduleQuery) Order(o ...schedule.OrderOption) *ScheduleQuery { + _q.order = append(_q.order, o...) + return _q +} + +// First returns the first Schedule entity from the query. +// Returns a *NotFoundError when no Schedule was found. +func (_q *ScheduleQuery) First(ctx context.Context) (*Schedule, error) { + nodes, err := _q.Limit(1).All(setContextOp(ctx, _q.ctx, ent.OpQueryFirst)) + if err != nil { + return nil, err + } + if len(nodes) == 0 { + return nil, &NotFoundError{schedule.Label} + } + return nodes[0], nil +} + +// FirstX is like First, but panics if an error occurs. +func (_q *ScheduleQuery) FirstX(ctx context.Context) *Schedule { + node, err := _q.First(ctx) + if err != nil && !IsNotFound(err) { + panic(err) + } + return node +} + +// FirstID returns the first Schedule ID from the query. +// Returns a *NotFoundError when no Schedule ID was found. +func (_q *ScheduleQuery) FirstID(ctx context.Context) (id uuid.UUID, err error) { + var ids []uuid.UUID + if ids, err = _q.Limit(1).IDs(setContextOp(ctx, _q.ctx, ent.OpQueryFirstID)); err != nil { + return + } + if len(ids) == 0 { + err = &NotFoundError{schedule.Label} + return + } + return ids[0], nil +} + +// FirstIDX is like FirstID, but panics if an error occurs. +func (_q *ScheduleQuery) FirstIDX(ctx context.Context) uuid.UUID { + id, err := _q.FirstID(ctx) + if err != nil && !IsNotFound(err) { + panic(err) + } + return id +} + +// Only returns a single Schedule entity found by the query, ensuring it only returns one. +// Returns a *NotSingularError when more than one Schedule entity is found. +// Returns a *NotFoundError when no Schedule entities are found. +func (_q *ScheduleQuery) Only(ctx context.Context) (*Schedule, error) { + nodes, err := _q.Limit(2).All(setContextOp(ctx, _q.ctx, ent.OpQueryOnly)) + if err != nil { + return nil, err + } + switch len(nodes) { + case 1: + return nodes[0], nil + case 0: + return nil, &NotFoundError{schedule.Label} + default: + return nil, &NotSingularError{schedule.Label} + } +} + +// OnlyX is like Only, but panics if an error occurs. +func (_q *ScheduleQuery) OnlyX(ctx context.Context) *Schedule { + node, err := _q.Only(ctx) + if err != nil { + panic(err) + } + return node +} + +// OnlyID is like Only, but returns the only Schedule ID in the query. +// Returns a *NotSingularError when more than one Schedule ID is found. +// Returns a *NotFoundError when no entities are found. +func (_q *ScheduleQuery) OnlyID(ctx context.Context) (id uuid.UUID, err error) { + var ids []uuid.UUID + if ids, err = _q.Limit(2).IDs(setContextOp(ctx, _q.ctx, ent.OpQueryOnlyID)); err != nil { + return + } + switch len(ids) { + case 1: + id = ids[0] + case 0: + err = &NotFoundError{schedule.Label} + default: + err = &NotSingularError{schedule.Label} + } + return +} + +// OnlyIDX is like OnlyID, but panics if an error occurs. +func (_q *ScheduleQuery) OnlyIDX(ctx context.Context) uuid.UUID { + id, err := _q.OnlyID(ctx) + if err != nil { + panic(err) + } + return id +} + +// All executes the query and returns a list of Schedules. +func (_q *ScheduleQuery) All(ctx context.Context) ([]*Schedule, error) { + ctx = setContextOp(ctx, _q.ctx, ent.OpQueryAll) + if err := _q.prepareQuery(ctx); err != nil { + return nil, err + } + qr := querierAll[[]*Schedule, *ScheduleQuery]() + return withInterceptors[[]*Schedule](ctx, _q, qr, _q.inters) +} + +// AllX is like All, but panics if an error occurs. +func (_q *ScheduleQuery) AllX(ctx context.Context) []*Schedule { + nodes, err := _q.All(ctx) + if err != nil { + panic(err) + } + return nodes +} + +// IDs executes the query and returns a list of Schedule IDs. +func (_q *ScheduleQuery) IDs(ctx context.Context) (ids []uuid.UUID, err error) { + if _q.ctx.Unique == nil && _q.path != nil { + _q.Unique(true) + } + ctx = setContextOp(ctx, _q.ctx, ent.OpQueryIDs) + if err = _q.Select(schedule.FieldID).Scan(ctx, &ids); err != nil { + return nil, err + } + return ids, nil +} + +// IDsX is like IDs, but panics if an error occurs. +func (_q *ScheduleQuery) IDsX(ctx context.Context) []uuid.UUID { + ids, err := _q.IDs(ctx) + if err != nil { + panic(err) + } + return ids +} + +// Count returns the count of the given query. +func (_q *ScheduleQuery) Count(ctx context.Context) (int, error) { + ctx = setContextOp(ctx, _q.ctx, ent.OpQueryCount) + if err := _q.prepareQuery(ctx); err != nil { + return 0, err + } + return withInterceptors[int](ctx, _q, querierCount[*ScheduleQuery](), _q.inters) +} + +// CountX is like Count, but panics if an error occurs. +func (_q *ScheduleQuery) CountX(ctx context.Context) int { + count, err := _q.Count(ctx) + if err != nil { + panic(err) + } + return count +} + +// Exist returns true if the query has elements in the graph. +func (_q *ScheduleQuery) Exist(ctx context.Context) (bool, error) { + ctx = setContextOp(ctx, _q.ctx, ent.OpQueryExist) + switch _, err := _q.FirstID(ctx); { + case IsNotFound(err): + return false, nil + case err != nil: + return false, fmt.Errorf("ent: check existence: %w", err) + default: + return true, nil + } +} + +// ExistX is like Exist, but panics if an error occurs. +func (_q *ScheduleQuery) ExistX(ctx context.Context) bool { + exist, err := _q.Exist(ctx) + if err != nil { + panic(err) + } + return exist +} + +// Clone returns a duplicate of the ScheduleQuery builder, including all associated steps. It can be +// used to prepare common query builders and use them differently after the clone is made. +func (_q *ScheduleQuery) Clone() *ScheduleQuery { + if _q == nil { + return nil + } + return &ScheduleQuery{ + config: _q.config, + ctx: _q.ctx.Clone(), + order: append([]schedule.OrderOption{}, _q.order...), + inters: append([]Interceptor{}, _q.inters...), + predicates: append([]predicate.Schedule{}, _q.predicates...), + // clone intermediate query. + sql: _q.sql.Clone(), + path: _q.path, + } +} + +// GroupBy is used to group vertices by one or more fields/columns. +// It is often used with aggregate functions, like: count, max, mean, min, sum. +// +// Example: +// +// var v []struct { +// ProjectID uuid.UUID `json:"project_id,omitempty"` +// Count int `json:"count,omitempty"` +// } +// +// client.Schedule.Query(). +// GroupBy(schedule.FieldProjectID). +// Aggregate(ent.Count()). +// Scan(ctx, &v) +func (_q *ScheduleQuery) GroupBy(field string, fields ...string) *ScheduleGroupBy { + _q.ctx.Fields = append([]string{field}, fields...) + grbuild := &ScheduleGroupBy{build: _q} + grbuild.flds = &_q.ctx.Fields + grbuild.label = schedule.Label + grbuild.scan = grbuild.Scan + return grbuild +} + +// Select allows the selection one or more fields/columns for the given query, +// instead of selecting all fields in the entity. +// +// Example: +// +// var v []struct { +// ProjectID uuid.UUID `json:"project_id,omitempty"` +// } +// +// client.Schedule.Query(). +// Select(schedule.FieldProjectID). +// Scan(ctx, &v) +func (_q *ScheduleQuery) Select(fields ...string) *ScheduleSelect { + _q.ctx.Fields = append(_q.ctx.Fields, fields...) + sbuild := &ScheduleSelect{ScheduleQuery: _q} + sbuild.label = schedule.Label + sbuild.flds, sbuild.scan = &_q.ctx.Fields, sbuild.Scan + return sbuild +} + +// Aggregate returns a ScheduleSelect configured with the given aggregations. +func (_q *ScheduleQuery) Aggregate(fns ...AggregateFunc) *ScheduleSelect { + return _q.Select().Aggregate(fns...) +} + +func (_q *ScheduleQuery) prepareQuery(ctx context.Context) error { + for _, inter := range _q.inters { + if inter == nil { + return fmt.Errorf("ent: uninitialized interceptor (forgotten import ent/runtime?)") + } + if trv, ok := inter.(Traverser); ok { + if err := trv.Traverse(ctx, _q); err != nil { + return err + } + } + } + for _, f := range _q.ctx.Fields { + if !schedule.ValidColumn(f) { + return &ValidationError{Name: f, err: fmt.Errorf("ent: invalid field %q for query", f)} + } + } + if _q.path != nil { + prev, err := _q.path(ctx) + if err != nil { + return err + } + _q.sql = prev + } + return nil +} + +func (_q *ScheduleQuery) sqlAll(ctx context.Context, hooks ...queryHook) ([]*Schedule, error) { + var ( + nodes = []*Schedule{} + _spec = _q.querySpec() + ) + _spec.ScanValues = func(columns []string) ([]any, error) { + return (*Schedule).scanValues(nil, columns) + } + _spec.Assign = func(columns []string, values []any) error { + node := &Schedule{config: _q.config} + nodes = append(nodes, node) + return node.assignValues(columns, values) + } + if len(_q.modifiers) > 0 { + _spec.Modifiers = _q.modifiers + } + for i := range hooks { + hooks[i](ctx, _spec) + } + if err := sqlgraph.QueryNodes(ctx, _q.driver, _spec); err != nil { + return nil, err + } + if len(nodes) == 0 { + return nodes, nil + } + return nodes, nil +} + +func (_q *ScheduleQuery) sqlCount(ctx context.Context) (int, error) { + _spec := _q.querySpec() + if len(_q.modifiers) > 0 { + _spec.Modifiers = _q.modifiers + } + _spec.Node.Columns = _q.ctx.Fields + if len(_q.ctx.Fields) > 0 { + _spec.Unique = _q.ctx.Unique != nil && *_q.ctx.Unique + } + return sqlgraph.CountNodes(ctx, _q.driver, _spec) +} + +func (_q *ScheduleQuery) querySpec() *sqlgraph.QuerySpec { + _spec := sqlgraph.NewQuerySpec(schedule.Table, schedule.Columns, sqlgraph.NewFieldSpec(schedule.FieldID, field.TypeUUID)) + _spec.From = _q.sql + if unique := _q.ctx.Unique; unique != nil { + _spec.Unique = *unique + } else if _q.path != nil { + _spec.Unique = true + } + if fields := _q.ctx.Fields; len(fields) > 0 { + _spec.Node.Columns = make([]string, 0, len(fields)) + _spec.Node.Columns = append(_spec.Node.Columns, schedule.FieldID) + for i := range fields { + if fields[i] != schedule.FieldID { + _spec.Node.Columns = append(_spec.Node.Columns, fields[i]) + } + } + } + if ps := _q.predicates; len(ps) > 0 { + _spec.Predicate = func(selector *sql.Selector) { + for i := range ps { + ps[i](selector) + } + } + } + if limit := _q.ctx.Limit; limit != nil { + _spec.Limit = *limit + } + if offset := _q.ctx.Offset; offset != nil { + _spec.Offset = *offset + } + if ps := _q.order; len(ps) > 0 { + _spec.Order = func(selector *sql.Selector) { + for i := range ps { + ps[i](selector) + } + } + } + return _spec +} + +func (_q *ScheduleQuery) sqlQuery(ctx context.Context) *sql.Selector { + builder := sql.Dialect(_q.driver.Dialect()) + t1 := builder.Table(schedule.Table) + columns := _q.ctx.Fields + if len(columns) == 0 { + columns = schedule.Columns + } + selector := builder.Select(t1.Columns(columns...)...).From(t1) + if _q.sql != nil { + selector = _q.sql + selector.Select(selector.Columns(columns...)...) + } + if _q.ctx.Unique != nil && *_q.ctx.Unique { + selector.Distinct() + } + for _, m := range _q.modifiers { + m(selector) + } + for _, p := range _q.predicates { + p(selector) + } + for _, p := range _q.order { + p(selector) + } + if offset := _q.ctx.Offset; offset != nil { + // limit is mandatory for offset clause. We start + // with default value, and override it below if needed. + selector.Offset(*offset).Limit(math.MaxInt32) + } + if limit := _q.ctx.Limit; limit != nil { + selector.Limit(*limit) + } + return selector +} + +// ForUpdate locks the selected rows against concurrent updates, and prevent them from being +// updated, deleted or "selected ... for update" by other sessions, until the transaction is +// either committed or rolled-back. +func (_q *ScheduleQuery) ForUpdate(opts ...sql.LockOption) *ScheduleQuery { + if _q.driver.Dialect() == dialect.Postgres { + _q.Unique(false) + } + _q.modifiers = append(_q.modifiers, func(s *sql.Selector) { + s.ForUpdate(opts...) + }) + return _q +} + +// ForShare behaves similarly to ForUpdate, except that it acquires a shared mode lock +// on any rows that are read. Other sessions can read the rows, but cannot modify them +// until your transaction commits. +func (_q *ScheduleQuery) ForShare(opts ...sql.LockOption) *ScheduleQuery { + if _q.driver.Dialect() == dialect.Postgres { + _q.Unique(false) + } + _q.modifiers = append(_q.modifiers, func(s *sql.Selector) { + s.ForShare(opts...) + }) + return _q +} + +// ScheduleGroupBy is the group-by builder for Schedule entities. +type ScheduleGroupBy struct { + selector + build *ScheduleQuery +} + +// Aggregate adds the given aggregation functions to the group-by query. +func (_g *ScheduleGroupBy) Aggregate(fns ...AggregateFunc) *ScheduleGroupBy { + _g.fns = append(_g.fns, fns...) + return _g +} + +// Scan applies the selector query and scans the result into the given value. +func (_g *ScheduleGroupBy) Scan(ctx context.Context, v any) error { + ctx = setContextOp(ctx, _g.build.ctx, ent.OpQueryGroupBy) + if err := _g.build.prepareQuery(ctx); err != nil { + return err + } + return scanWithInterceptors[*ScheduleQuery, *ScheduleGroupBy](ctx, _g.build, _g, _g.build.inters, v) +} + +func (_g *ScheduleGroupBy) sqlScan(ctx context.Context, root *ScheduleQuery, v any) error { + selector := root.sqlQuery(ctx).Select() + aggregation := make([]string, 0, len(_g.fns)) + for _, fn := range _g.fns { + aggregation = append(aggregation, fn(selector)) + } + if len(selector.SelectedColumns()) == 0 { + columns := make([]string, 0, len(*_g.flds)+len(_g.fns)) + for _, f := range *_g.flds { + columns = append(columns, selector.C(f)) + } + columns = append(columns, aggregation...) + selector.Select(columns...) + } + selector.GroupBy(selector.Columns(*_g.flds...)...) + if err := selector.Err(); err != nil { + return err + } + rows := &sql.Rows{} + query, args := selector.Query() + if err := _g.build.driver.Query(ctx, query, args, rows); err != nil { + return err + } + defer rows.Close() + return sql.ScanSlice(rows, v) +} + +// ScheduleSelect is the builder for selecting fields of Schedule entities. +type ScheduleSelect struct { + *ScheduleQuery + selector +} + +// Aggregate adds the given aggregation functions to the selector query. +func (_s *ScheduleSelect) Aggregate(fns ...AggregateFunc) *ScheduleSelect { + _s.fns = append(_s.fns, fns...) + return _s +} + +// Scan applies the selector query and scans the result into the given value. +func (_s *ScheduleSelect) Scan(ctx context.Context, v any) error { + ctx = setContextOp(ctx, _s.ctx, ent.OpQuerySelect) + if err := _s.prepareQuery(ctx); err != nil { + return err + } + return scanWithInterceptors[*ScheduleQuery, *ScheduleSelect](ctx, _s.ScheduleQuery, _s, _s.inters, v) +} + +func (_s *ScheduleSelect) sqlScan(ctx context.Context, root *ScheduleQuery, v any) error { + selector := root.sqlQuery(ctx) + aggregation := make([]string, 0, len(_s.fns)) + for _, fn := range _s.fns { + aggregation = append(aggregation, fn(selector)) + } + switch n := len(*_s.selector.flds); { + case n == 0 && len(aggregation) > 0: + selector.Select(aggregation...) + case n != 0 && len(aggregation) > 0: + selector.AppendSelect(aggregation...) + } + rows := &sql.Rows{} + query, args := selector.Query() + if err := _s.driver.Query(ctx, query, args, rows); err != nil { + return err + } + defer rows.Close() + return sql.ScanSlice(rows, v) +} diff --git a/pkg/ent/schedule_update.go b/pkg/ent/schedule_update.go new file mode 100644 index 000000000..791a6eb52 --- /dev/null +++ b/pkg/ent/schedule_update.go @@ -0,0 +1,831 @@ +// Code generated by ent, DO NOT EDIT. + +package ent + +import ( + "context" + "errors" + "fmt" + "time" + + "entgo.io/ent/dialect/sql" + "entgo.io/ent/dialect/sql/sqlgraph" + "entgo.io/ent/schema/field" + "github.com/GoogleCloudPlatform/scion/pkg/ent/predicate" + "github.com/GoogleCloudPlatform/scion/pkg/ent/schedule" + "github.com/google/uuid" +) + +// ScheduleUpdate is the builder for updating Schedule entities. +type ScheduleUpdate struct { + config + hooks []Hook + mutation *ScheduleMutation +} + +// Where appends a list predicates to the ScheduleUpdate builder. +func (_u *ScheduleUpdate) Where(ps ...predicate.Schedule) *ScheduleUpdate { + _u.mutation.Where(ps...) + return _u +} + +// SetProjectID sets the "project_id" field. +func (_u *ScheduleUpdate) SetProjectID(v uuid.UUID) *ScheduleUpdate { + _u.mutation.SetProjectID(v) + return _u +} + +// SetNillableProjectID sets the "project_id" field if the given value is not nil. +func (_u *ScheduleUpdate) SetNillableProjectID(v *uuid.UUID) *ScheduleUpdate { + if v != nil { + _u.SetProjectID(*v) + } + return _u +} + +// SetName sets the "name" field. +func (_u *ScheduleUpdate) SetName(v string) *ScheduleUpdate { + _u.mutation.SetName(v) + return _u +} + +// SetNillableName sets the "name" field if the given value is not nil. +func (_u *ScheduleUpdate) SetNillableName(v *string) *ScheduleUpdate { + if v != nil { + _u.SetName(*v) + } + return _u +} + +// SetCronExpr sets the "cron_expr" field. +func (_u *ScheduleUpdate) SetCronExpr(v string) *ScheduleUpdate { + _u.mutation.SetCronExpr(v) + return _u +} + +// SetNillableCronExpr sets the "cron_expr" field if the given value is not nil. +func (_u *ScheduleUpdate) SetNillableCronExpr(v *string) *ScheduleUpdate { + if v != nil { + _u.SetCronExpr(*v) + } + return _u +} + +// SetEventType sets the "event_type" field. +func (_u *ScheduleUpdate) SetEventType(v string) *ScheduleUpdate { + _u.mutation.SetEventType(v) + return _u +} + +// SetNillableEventType sets the "event_type" field if the given value is not nil. +func (_u *ScheduleUpdate) SetNillableEventType(v *string) *ScheduleUpdate { + if v != nil { + _u.SetEventType(*v) + } + return _u +} + +// SetPayload sets the "payload" field. +func (_u *ScheduleUpdate) SetPayload(v string) *ScheduleUpdate { + _u.mutation.SetPayload(v) + return _u +} + +// SetNillablePayload sets the "payload" field if the given value is not nil. +func (_u *ScheduleUpdate) SetNillablePayload(v *string) *ScheduleUpdate { + if v != nil { + _u.SetPayload(*v) + } + return _u +} + +// SetStatus sets the "status" field. +func (_u *ScheduleUpdate) SetStatus(v string) *ScheduleUpdate { + _u.mutation.SetStatus(v) + return _u +} + +// SetNillableStatus sets the "status" field if the given value is not nil. +func (_u *ScheduleUpdate) SetNillableStatus(v *string) *ScheduleUpdate { + if v != nil { + _u.SetStatus(*v) + } + return _u +} + +// SetNextRunAt sets the "next_run_at" field. +func (_u *ScheduleUpdate) SetNextRunAt(v time.Time) *ScheduleUpdate { + _u.mutation.SetNextRunAt(v) + return _u +} + +// SetNillableNextRunAt sets the "next_run_at" field if the given value is not nil. +func (_u *ScheduleUpdate) SetNillableNextRunAt(v *time.Time) *ScheduleUpdate { + if v != nil { + _u.SetNextRunAt(*v) + } + return _u +} + +// ClearNextRunAt clears the value of the "next_run_at" field. +func (_u *ScheduleUpdate) ClearNextRunAt() *ScheduleUpdate { + _u.mutation.ClearNextRunAt() + return _u +} + +// SetLastRunAt sets the "last_run_at" field. +func (_u *ScheduleUpdate) SetLastRunAt(v time.Time) *ScheduleUpdate { + _u.mutation.SetLastRunAt(v) + return _u +} + +// SetNillableLastRunAt sets the "last_run_at" field if the given value is not nil. +func (_u *ScheduleUpdate) SetNillableLastRunAt(v *time.Time) *ScheduleUpdate { + if v != nil { + _u.SetLastRunAt(*v) + } + return _u +} + +// ClearLastRunAt clears the value of the "last_run_at" field. +func (_u *ScheduleUpdate) ClearLastRunAt() *ScheduleUpdate { + _u.mutation.ClearLastRunAt() + return _u +} + +// SetLastRunStatus sets the "last_run_status" field. +func (_u *ScheduleUpdate) SetLastRunStatus(v string) *ScheduleUpdate { + _u.mutation.SetLastRunStatus(v) + return _u +} + +// SetNillableLastRunStatus sets the "last_run_status" field if the given value is not nil. +func (_u *ScheduleUpdate) SetNillableLastRunStatus(v *string) *ScheduleUpdate { + if v != nil { + _u.SetLastRunStatus(*v) + } + return _u +} + +// ClearLastRunStatus clears the value of the "last_run_status" field. +func (_u *ScheduleUpdate) ClearLastRunStatus() *ScheduleUpdate { + _u.mutation.ClearLastRunStatus() + return _u +} + +// SetLastRunError sets the "last_run_error" field. +func (_u *ScheduleUpdate) SetLastRunError(v string) *ScheduleUpdate { + _u.mutation.SetLastRunError(v) + return _u +} + +// SetNillableLastRunError sets the "last_run_error" field if the given value is not nil. +func (_u *ScheduleUpdate) SetNillableLastRunError(v *string) *ScheduleUpdate { + if v != nil { + _u.SetLastRunError(*v) + } + return _u +} + +// ClearLastRunError clears the value of the "last_run_error" field. +func (_u *ScheduleUpdate) ClearLastRunError() *ScheduleUpdate { + _u.mutation.ClearLastRunError() + return _u +} + +// SetRunCount sets the "run_count" field. +func (_u *ScheduleUpdate) SetRunCount(v int) *ScheduleUpdate { + _u.mutation.ResetRunCount() + _u.mutation.SetRunCount(v) + return _u +} + +// SetNillableRunCount sets the "run_count" field if the given value is not nil. +func (_u *ScheduleUpdate) SetNillableRunCount(v *int) *ScheduleUpdate { + if v != nil { + _u.SetRunCount(*v) + } + return _u +} + +// AddRunCount adds value to the "run_count" field. +func (_u *ScheduleUpdate) AddRunCount(v int) *ScheduleUpdate { + _u.mutation.AddRunCount(v) + return _u +} + +// SetErrorCount sets the "error_count" field. +func (_u *ScheduleUpdate) SetErrorCount(v int) *ScheduleUpdate { + _u.mutation.ResetErrorCount() + _u.mutation.SetErrorCount(v) + return _u +} + +// SetNillableErrorCount sets the "error_count" field if the given value is not nil. +func (_u *ScheduleUpdate) SetNillableErrorCount(v *int) *ScheduleUpdate { + if v != nil { + _u.SetErrorCount(*v) + } + return _u +} + +// AddErrorCount adds value to the "error_count" field. +func (_u *ScheduleUpdate) AddErrorCount(v int) *ScheduleUpdate { + _u.mutation.AddErrorCount(v) + return _u +} + +// SetCreatedBy sets the "created_by" field. +func (_u *ScheduleUpdate) SetCreatedBy(v string) *ScheduleUpdate { + _u.mutation.SetCreatedBy(v) + return _u +} + +// SetNillableCreatedBy sets the "created_by" field if the given value is not nil. +func (_u *ScheduleUpdate) SetNillableCreatedBy(v *string) *ScheduleUpdate { + if v != nil { + _u.SetCreatedBy(*v) + } + return _u +} + +// ClearCreatedBy clears the value of the "created_by" field. +func (_u *ScheduleUpdate) ClearCreatedBy() *ScheduleUpdate { + _u.mutation.ClearCreatedBy() + return _u +} + +// SetUpdated sets the "updated" field. +func (_u *ScheduleUpdate) SetUpdated(v time.Time) *ScheduleUpdate { + _u.mutation.SetUpdated(v) + return _u +} + +// Mutation returns the ScheduleMutation object of the builder. +func (_u *ScheduleUpdate) Mutation() *ScheduleMutation { + return _u.mutation +} + +// Save executes the query and returns the number of nodes affected by the update operation. +func (_u *ScheduleUpdate) Save(ctx context.Context) (int, error) { + _u.defaults() + return withHooks(ctx, _u.sqlSave, _u.mutation, _u.hooks) +} + +// SaveX is like Save, but panics if an error occurs. +func (_u *ScheduleUpdate) SaveX(ctx context.Context) int { + affected, err := _u.Save(ctx) + if err != nil { + panic(err) + } + return affected +} + +// Exec executes the query. +func (_u *ScheduleUpdate) Exec(ctx context.Context) error { + _, err := _u.Save(ctx) + return err +} + +// ExecX is like Exec, but panics if an error occurs. +func (_u *ScheduleUpdate) ExecX(ctx context.Context) { + if err := _u.Exec(ctx); err != nil { + panic(err) + } +} + +// defaults sets the default values of the builder before save. +func (_u *ScheduleUpdate) defaults() { + if _, ok := _u.mutation.Updated(); !ok { + v := schedule.UpdateDefaultUpdated() + _u.mutation.SetUpdated(v) + } +} + +// check runs all checks and user-defined validators on the builder. +func (_u *ScheduleUpdate) check() error { + if v, ok := _u.mutation.Name(); ok { + if err := schedule.NameValidator(v); err != nil { + return &ValidationError{Name: "name", err: fmt.Errorf(`ent: validator failed for field "Schedule.name": %w`, err)} + } + } + if v, ok := _u.mutation.CronExpr(); ok { + if err := schedule.CronExprValidator(v); err != nil { + return &ValidationError{Name: "cron_expr", err: fmt.Errorf(`ent: validator failed for field "Schedule.cron_expr": %w`, err)} + } + } + if v, ok := _u.mutation.EventType(); ok { + if err := schedule.EventTypeValidator(v); err != nil { + return &ValidationError{Name: "event_type", err: fmt.Errorf(`ent: validator failed for field "Schedule.event_type": %w`, err)} + } + } + return nil +} + +func (_u *ScheduleUpdate) sqlSave(ctx context.Context) (_node int, err error) { + if err := _u.check(); err != nil { + return _node, err + } + _spec := sqlgraph.NewUpdateSpec(schedule.Table, schedule.Columns, sqlgraph.NewFieldSpec(schedule.FieldID, field.TypeUUID)) + if ps := _u.mutation.predicates; len(ps) > 0 { + _spec.Predicate = func(selector *sql.Selector) { + for i := range ps { + ps[i](selector) + } + } + } + if value, ok := _u.mutation.ProjectID(); ok { + _spec.SetField(schedule.FieldProjectID, field.TypeUUID, value) + } + if value, ok := _u.mutation.Name(); ok { + _spec.SetField(schedule.FieldName, field.TypeString, value) + } + if value, ok := _u.mutation.CronExpr(); ok { + _spec.SetField(schedule.FieldCronExpr, field.TypeString, value) + } + if value, ok := _u.mutation.EventType(); ok { + _spec.SetField(schedule.FieldEventType, field.TypeString, value) + } + if value, ok := _u.mutation.Payload(); ok { + _spec.SetField(schedule.FieldPayload, field.TypeString, value) + } + if value, ok := _u.mutation.Status(); ok { + _spec.SetField(schedule.FieldStatus, field.TypeString, value) + } + if value, ok := _u.mutation.NextRunAt(); ok { + _spec.SetField(schedule.FieldNextRunAt, field.TypeTime, value) + } + if _u.mutation.NextRunAtCleared() { + _spec.ClearField(schedule.FieldNextRunAt, field.TypeTime) + } + if value, ok := _u.mutation.LastRunAt(); ok { + _spec.SetField(schedule.FieldLastRunAt, field.TypeTime, value) + } + if _u.mutation.LastRunAtCleared() { + _spec.ClearField(schedule.FieldLastRunAt, field.TypeTime) + } + if value, ok := _u.mutation.LastRunStatus(); ok { + _spec.SetField(schedule.FieldLastRunStatus, field.TypeString, value) + } + if _u.mutation.LastRunStatusCleared() { + _spec.ClearField(schedule.FieldLastRunStatus, field.TypeString) + } + if value, ok := _u.mutation.LastRunError(); ok { + _spec.SetField(schedule.FieldLastRunError, field.TypeString, value) + } + if _u.mutation.LastRunErrorCleared() { + _spec.ClearField(schedule.FieldLastRunError, field.TypeString) + } + if value, ok := _u.mutation.RunCount(); ok { + _spec.SetField(schedule.FieldRunCount, field.TypeInt, value) + } + if value, ok := _u.mutation.AddedRunCount(); ok { + _spec.AddField(schedule.FieldRunCount, field.TypeInt, value) + } + if value, ok := _u.mutation.ErrorCount(); ok { + _spec.SetField(schedule.FieldErrorCount, field.TypeInt, value) + } + if value, ok := _u.mutation.AddedErrorCount(); ok { + _spec.AddField(schedule.FieldErrorCount, field.TypeInt, value) + } + if value, ok := _u.mutation.CreatedBy(); ok { + _spec.SetField(schedule.FieldCreatedBy, field.TypeString, value) + } + if _u.mutation.CreatedByCleared() { + _spec.ClearField(schedule.FieldCreatedBy, field.TypeString) + } + if value, ok := _u.mutation.Updated(); ok { + _spec.SetField(schedule.FieldUpdated, field.TypeTime, value) + } + if _node, err = sqlgraph.UpdateNodes(ctx, _u.driver, _spec); err != nil { + if _, ok := err.(*sqlgraph.NotFoundError); ok { + err = &NotFoundError{schedule.Label} + } else if sqlgraph.IsConstraintError(err) { + err = &ConstraintError{msg: err.Error(), wrap: err} + } + return 0, err + } + _u.mutation.done = true + return _node, nil +} + +// ScheduleUpdateOne is the builder for updating a single Schedule entity. +type ScheduleUpdateOne struct { + config + fields []string + hooks []Hook + mutation *ScheduleMutation +} + +// SetProjectID sets the "project_id" field. +func (_u *ScheduleUpdateOne) SetProjectID(v uuid.UUID) *ScheduleUpdateOne { + _u.mutation.SetProjectID(v) + return _u +} + +// SetNillableProjectID sets the "project_id" field if the given value is not nil. +func (_u *ScheduleUpdateOne) SetNillableProjectID(v *uuid.UUID) *ScheduleUpdateOne { + if v != nil { + _u.SetProjectID(*v) + } + return _u +} + +// SetName sets the "name" field. +func (_u *ScheduleUpdateOne) SetName(v string) *ScheduleUpdateOne { + _u.mutation.SetName(v) + return _u +} + +// SetNillableName sets the "name" field if the given value is not nil. +func (_u *ScheduleUpdateOne) SetNillableName(v *string) *ScheduleUpdateOne { + if v != nil { + _u.SetName(*v) + } + return _u +} + +// SetCronExpr sets the "cron_expr" field. +func (_u *ScheduleUpdateOne) SetCronExpr(v string) *ScheduleUpdateOne { + _u.mutation.SetCronExpr(v) + return _u +} + +// SetNillableCronExpr sets the "cron_expr" field if the given value is not nil. +func (_u *ScheduleUpdateOne) SetNillableCronExpr(v *string) *ScheduleUpdateOne { + if v != nil { + _u.SetCronExpr(*v) + } + return _u +} + +// SetEventType sets the "event_type" field. +func (_u *ScheduleUpdateOne) SetEventType(v string) *ScheduleUpdateOne { + _u.mutation.SetEventType(v) + return _u +} + +// SetNillableEventType sets the "event_type" field if the given value is not nil. +func (_u *ScheduleUpdateOne) SetNillableEventType(v *string) *ScheduleUpdateOne { + if v != nil { + _u.SetEventType(*v) + } + return _u +} + +// SetPayload sets the "payload" field. +func (_u *ScheduleUpdateOne) SetPayload(v string) *ScheduleUpdateOne { + _u.mutation.SetPayload(v) + return _u +} + +// SetNillablePayload sets the "payload" field if the given value is not nil. +func (_u *ScheduleUpdateOne) SetNillablePayload(v *string) *ScheduleUpdateOne { + if v != nil { + _u.SetPayload(*v) + } + return _u +} + +// SetStatus sets the "status" field. +func (_u *ScheduleUpdateOne) SetStatus(v string) *ScheduleUpdateOne { + _u.mutation.SetStatus(v) + return _u +} + +// SetNillableStatus sets the "status" field if the given value is not nil. +func (_u *ScheduleUpdateOne) SetNillableStatus(v *string) *ScheduleUpdateOne { + if v != nil { + _u.SetStatus(*v) + } + return _u +} + +// SetNextRunAt sets the "next_run_at" field. +func (_u *ScheduleUpdateOne) SetNextRunAt(v time.Time) *ScheduleUpdateOne { + _u.mutation.SetNextRunAt(v) + return _u +} + +// SetNillableNextRunAt sets the "next_run_at" field if the given value is not nil. +func (_u *ScheduleUpdateOne) SetNillableNextRunAt(v *time.Time) *ScheduleUpdateOne { + if v != nil { + _u.SetNextRunAt(*v) + } + return _u +} + +// ClearNextRunAt clears the value of the "next_run_at" field. +func (_u *ScheduleUpdateOne) ClearNextRunAt() *ScheduleUpdateOne { + _u.mutation.ClearNextRunAt() + return _u +} + +// SetLastRunAt sets the "last_run_at" field. +func (_u *ScheduleUpdateOne) SetLastRunAt(v time.Time) *ScheduleUpdateOne { + _u.mutation.SetLastRunAt(v) + return _u +} + +// SetNillableLastRunAt sets the "last_run_at" field if the given value is not nil. +func (_u *ScheduleUpdateOne) SetNillableLastRunAt(v *time.Time) *ScheduleUpdateOne { + if v != nil { + _u.SetLastRunAt(*v) + } + return _u +} + +// ClearLastRunAt clears the value of the "last_run_at" field. +func (_u *ScheduleUpdateOne) ClearLastRunAt() *ScheduleUpdateOne { + _u.mutation.ClearLastRunAt() + return _u +} + +// SetLastRunStatus sets the "last_run_status" field. +func (_u *ScheduleUpdateOne) SetLastRunStatus(v string) *ScheduleUpdateOne { + _u.mutation.SetLastRunStatus(v) + return _u +} + +// SetNillableLastRunStatus sets the "last_run_status" field if the given value is not nil. +func (_u *ScheduleUpdateOne) SetNillableLastRunStatus(v *string) *ScheduleUpdateOne { + if v != nil { + _u.SetLastRunStatus(*v) + } + return _u +} + +// ClearLastRunStatus clears the value of the "last_run_status" field. +func (_u *ScheduleUpdateOne) ClearLastRunStatus() *ScheduleUpdateOne { + _u.mutation.ClearLastRunStatus() + return _u +} + +// SetLastRunError sets the "last_run_error" field. +func (_u *ScheduleUpdateOne) SetLastRunError(v string) *ScheduleUpdateOne { + _u.mutation.SetLastRunError(v) + return _u +} + +// SetNillableLastRunError sets the "last_run_error" field if the given value is not nil. +func (_u *ScheduleUpdateOne) SetNillableLastRunError(v *string) *ScheduleUpdateOne { + if v != nil { + _u.SetLastRunError(*v) + } + return _u +} + +// ClearLastRunError clears the value of the "last_run_error" field. +func (_u *ScheduleUpdateOne) ClearLastRunError() *ScheduleUpdateOne { + _u.mutation.ClearLastRunError() + return _u +} + +// SetRunCount sets the "run_count" field. +func (_u *ScheduleUpdateOne) SetRunCount(v int) *ScheduleUpdateOne { + _u.mutation.ResetRunCount() + _u.mutation.SetRunCount(v) + return _u +} + +// SetNillableRunCount sets the "run_count" field if the given value is not nil. +func (_u *ScheduleUpdateOne) SetNillableRunCount(v *int) *ScheduleUpdateOne { + if v != nil { + _u.SetRunCount(*v) + } + return _u +} + +// AddRunCount adds value to the "run_count" field. +func (_u *ScheduleUpdateOne) AddRunCount(v int) *ScheduleUpdateOne { + _u.mutation.AddRunCount(v) + return _u +} + +// SetErrorCount sets the "error_count" field. +func (_u *ScheduleUpdateOne) SetErrorCount(v int) *ScheduleUpdateOne { + _u.mutation.ResetErrorCount() + _u.mutation.SetErrorCount(v) + return _u +} + +// SetNillableErrorCount sets the "error_count" field if the given value is not nil. +func (_u *ScheduleUpdateOne) SetNillableErrorCount(v *int) *ScheduleUpdateOne { + if v != nil { + _u.SetErrorCount(*v) + } + return _u +} + +// AddErrorCount adds value to the "error_count" field. +func (_u *ScheduleUpdateOne) AddErrorCount(v int) *ScheduleUpdateOne { + _u.mutation.AddErrorCount(v) + return _u +} + +// SetCreatedBy sets the "created_by" field. +func (_u *ScheduleUpdateOne) SetCreatedBy(v string) *ScheduleUpdateOne { + _u.mutation.SetCreatedBy(v) + return _u +} + +// SetNillableCreatedBy sets the "created_by" field if the given value is not nil. +func (_u *ScheduleUpdateOne) SetNillableCreatedBy(v *string) *ScheduleUpdateOne { + if v != nil { + _u.SetCreatedBy(*v) + } + return _u +} + +// ClearCreatedBy clears the value of the "created_by" field. +func (_u *ScheduleUpdateOne) ClearCreatedBy() *ScheduleUpdateOne { + _u.mutation.ClearCreatedBy() + return _u +} + +// SetUpdated sets the "updated" field. +func (_u *ScheduleUpdateOne) SetUpdated(v time.Time) *ScheduleUpdateOne { + _u.mutation.SetUpdated(v) + return _u +} + +// Mutation returns the ScheduleMutation object of the builder. +func (_u *ScheduleUpdateOne) Mutation() *ScheduleMutation { + return _u.mutation +} + +// Where appends a list predicates to the ScheduleUpdate builder. +func (_u *ScheduleUpdateOne) Where(ps ...predicate.Schedule) *ScheduleUpdateOne { + _u.mutation.Where(ps...) + return _u +} + +// Select allows selecting one or more fields (columns) of the returned entity. +// The default is selecting all fields defined in the entity schema. +func (_u *ScheduleUpdateOne) Select(field string, fields ...string) *ScheduleUpdateOne { + _u.fields = append([]string{field}, fields...) + return _u +} + +// Save executes the query and returns the updated Schedule entity. +func (_u *ScheduleUpdateOne) Save(ctx context.Context) (*Schedule, error) { + _u.defaults() + return withHooks(ctx, _u.sqlSave, _u.mutation, _u.hooks) +} + +// SaveX is like Save, but panics if an error occurs. +func (_u *ScheduleUpdateOne) SaveX(ctx context.Context) *Schedule { + node, err := _u.Save(ctx) + if err != nil { + panic(err) + } + return node +} + +// Exec executes the query on the entity. +func (_u *ScheduleUpdateOne) Exec(ctx context.Context) error { + _, err := _u.Save(ctx) + return err +} + +// ExecX is like Exec, but panics if an error occurs. +func (_u *ScheduleUpdateOne) ExecX(ctx context.Context) { + if err := _u.Exec(ctx); err != nil { + panic(err) + } +} + +// defaults sets the default values of the builder before save. +func (_u *ScheduleUpdateOne) defaults() { + if _, ok := _u.mutation.Updated(); !ok { + v := schedule.UpdateDefaultUpdated() + _u.mutation.SetUpdated(v) + } +} + +// check runs all checks and user-defined validators on the builder. +func (_u *ScheduleUpdateOne) check() error { + if v, ok := _u.mutation.Name(); ok { + if err := schedule.NameValidator(v); err != nil { + return &ValidationError{Name: "name", err: fmt.Errorf(`ent: validator failed for field "Schedule.name": %w`, err)} + } + } + if v, ok := _u.mutation.CronExpr(); ok { + if err := schedule.CronExprValidator(v); err != nil { + return &ValidationError{Name: "cron_expr", err: fmt.Errorf(`ent: validator failed for field "Schedule.cron_expr": %w`, err)} + } + } + if v, ok := _u.mutation.EventType(); ok { + if err := schedule.EventTypeValidator(v); err != nil { + return &ValidationError{Name: "event_type", err: fmt.Errorf(`ent: validator failed for field "Schedule.event_type": %w`, err)} + } + } + return nil +} + +func (_u *ScheduleUpdateOne) sqlSave(ctx context.Context) (_node *Schedule, err error) { + if err := _u.check(); err != nil { + return _node, err + } + _spec := sqlgraph.NewUpdateSpec(schedule.Table, schedule.Columns, sqlgraph.NewFieldSpec(schedule.FieldID, field.TypeUUID)) + id, ok := _u.mutation.ID() + if !ok { + return nil, &ValidationError{Name: "id", err: errors.New(`ent: missing "Schedule.id" for update`)} + } + _spec.Node.ID.Value = id + if fields := _u.fields; len(fields) > 0 { + _spec.Node.Columns = make([]string, 0, len(fields)) + _spec.Node.Columns = append(_spec.Node.Columns, schedule.FieldID) + for _, f := range fields { + if !schedule.ValidColumn(f) { + return nil, &ValidationError{Name: f, err: fmt.Errorf("ent: invalid field %q for query", f)} + } + if f != schedule.FieldID { + _spec.Node.Columns = append(_spec.Node.Columns, f) + } + } + } + if ps := _u.mutation.predicates; len(ps) > 0 { + _spec.Predicate = func(selector *sql.Selector) { + for i := range ps { + ps[i](selector) + } + } + } + if value, ok := _u.mutation.ProjectID(); ok { + _spec.SetField(schedule.FieldProjectID, field.TypeUUID, value) + } + if value, ok := _u.mutation.Name(); ok { + _spec.SetField(schedule.FieldName, field.TypeString, value) + } + if value, ok := _u.mutation.CronExpr(); ok { + _spec.SetField(schedule.FieldCronExpr, field.TypeString, value) + } + if value, ok := _u.mutation.EventType(); ok { + _spec.SetField(schedule.FieldEventType, field.TypeString, value) + } + if value, ok := _u.mutation.Payload(); ok { + _spec.SetField(schedule.FieldPayload, field.TypeString, value) + } + if value, ok := _u.mutation.Status(); ok { + _spec.SetField(schedule.FieldStatus, field.TypeString, value) + } + if value, ok := _u.mutation.NextRunAt(); ok { + _spec.SetField(schedule.FieldNextRunAt, field.TypeTime, value) + } + if _u.mutation.NextRunAtCleared() { + _spec.ClearField(schedule.FieldNextRunAt, field.TypeTime) + } + if value, ok := _u.mutation.LastRunAt(); ok { + _spec.SetField(schedule.FieldLastRunAt, field.TypeTime, value) + } + if _u.mutation.LastRunAtCleared() { + _spec.ClearField(schedule.FieldLastRunAt, field.TypeTime) + } + if value, ok := _u.mutation.LastRunStatus(); ok { + _spec.SetField(schedule.FieldLastRunStatus, field.TypeString, value) + } + if _u.mutation.LastRunStatusCleared() { + _spec.ClearField(schedule.FieldLastRunStatus, field.TypeString) + } + if value, ok := _u.mutation.LastRunError(); ok { + _spec.SetField(schedule.FieldLastRunError, field.TypeString, value) + } + if _u.mutation.LastRunErrorCleared() { + _spec.ClearField(schedule.FieldLastRunError, field.TypeString) + } + if value, ok := _u.mutation.RunCount(); ok { + _spec.SetField(schedule.FieldRunCount, field.TypeInt, value) + } + if value, ok := _u.mutation.AddedRunCount(); ok { + _spec.AddField(schedule.FieldRunCount, field.TypeInt, value) + } + if value, ok := _u.mutation.ErrorCount(); ok { + _spec.SetField(schedule.FieldErrorCount, field.TypeInt, value) + } + if value, ok := _u.mutation.AddedErrorCount(); ok { + _spec.AddField(schedule.FieldErrorCount, field.TypeInt, value) + } + if value, ok := _u.mutation.CreatedBy(); ok { + _spec.SetField(schedule.FieldCreatedBy, field.TypeString, value) + } + if _u.mutation.CreatedByCleared() { + _spec.ClearField(schedule.FieldCreatedBy, field.TypeString) + } + if value, ok := _u.mutation.Updated(); ok { + _spec.SetField(schedule.FieldUpdated, field.TypeTime, value) + } + _node = &Schedule{config: _u.config} + _spec.Assign = _node.assignValues + _spec.ScanValues = _node.scanValues + if err = sqlgraph.UpdateNode(ctx, _u.driver, _spec); err != nil { + if _, ok := err.(*sqlgraph.NotFoundError); ok { + err = &NotFoundError{schedule.Label} + } else if sqlgraph.IsConstraintError(err) { + err = &ConstraintError{msg: err.Error(), wrap: err} + } + return nil, err + } + _u.mutation.done = true + return _node, nil +} diff --git a/pkg/ent/scheduledevent.go b/pkg/ent/scheduledevent.go new file mode 100644 index 000000000..9cd40bec0 --- /dev/null +++ b/pkg/ent/scheduledevent.go @@ -0,0 +1,209 @@ +// Code generated by ent, DO NOT EDIT. + +package ent + +import ( + "fmt" + "strings" + "time" + + "entgo.io/ent" + "entgo.io/ent/dialect/sql" + "github.com/GoogleCloudPlatform/scion/pkg/ent/scheduledevent" + "github.com/google/uuid" +) + +// ScheduledEvent is the model entity for the ScheduledEvent schema. +type ScheduledEvent struct { + config `json:"-"` + // ID of the ent. + ID uuid.UUID `json:"id,omitempty"` + // ProjectID holds the value of the "project_id" field. + ProjectID uuid.UUID `json:"project_id,omitempty"` + // EventType holds the value of the "event_type" field. + EventType string `json:"event_type,omitempty"` + // FireAt holds the value of the "fire_at" field. + FireAt time.Time `json:"fire_at,omitempty"` + // Payload holds the value of the "payload" field. + Payload string `json:"payload,omitempty"` + // Status holds the value of the "status" field. + Status string `json:"status,omitempty"` + // CreatedBy holds the value of the "created_by" field. + CreatedBy string `json:"created_by,omitempty"` + // FiredAt holds the value of the "fired_at" field. + FiredAt *time.Time `json:"fired_at,omitempty"` + // Error holds the value of the "error" field. + Error string `json:"error,omitempty"` + // ScheduleID holds the value of the "schedule_id" field. + ScheduleID string `json:"schedule_id,omitempty"` + // Created holds the value of the "created" field. + Created time.Time `json:"created,omitempty"` + selectValues sql.SelectValues +} + +// scanValues returns the types for scanning values from sql.Rows. +func (*ScheduledEvent) scanValues(columns []string) ([]any, error) { + values := make([]any, len(columns)) + for i := range columns { + switch columns[i] { + case scheduledevent.FieldEventType, scheduledevent.FieldPayload, scheduledevent.FieldStatus, scheduledevent.FieldCreatedBy, scheduledevent.FieldError, scheduledevent.FieldScheduleID: + values[i] = new(sql.NullString) + case scheduledevent.FieldFireAt, scheduledevent.FieldFiredAt, scheduledevent.FieldCreated: + values[i] = new(sql.NullTime) + case scheduledevent.FieldID, scheduledevent.FieldProjectID: + values[i] = new(uuid.UUID) + default: + values[i] = new(sql.UnknownType) + } + } + return values, nil +} + +// assignValues assigns the values that were returned from sql.Rows (after scanning) +// to the ScheduledEvent fields. +func (_m *ScheduledEvent) assignValues(columns []string, values []any) error { + if m, n := len(values), len(columns); m < n { + return fmt.Errorf("mismatch number of scan values: %d != %d", m, n) + } + for i := range columns { + switch columns[i] { + case scheduledevent.FieldID: + if value, ok := values[i].(*uuid.UUID); !ok { + return fmt.Errorf("unexpected type %T for field id", values[i]) + } else if value != nil { + _m.ID = *value + } + case scheduledevent.FieldProjectID: + if value, ok := values[i].(*uuid.UUID); !ok { + return fmt.Errorf("unexpected type %T for field project_id", values[i]) + } else if value != nil { + _m.ProjectID = *value + } + case scheduledevent.FieldEventType: + if value, ok := values[i].(*sql.NullString); !ok { + return fmt.Errorf("unexpected type %T for field event_type", values[i]) + } else if value.Valid { + _m.EventType = value.String + } + case scheduledevent.FieldFireAt: + if value, ok := values[i].(*sql.NullTime); !ok { + return fmt.Errorf("unexpected type %T for field fire_at", values[i]) + } else if value.Valid { + _m.FireAt = value.Time + } + case scheduledevent.FieldPayload: + if value, ok := values[i].(*sql.NullString); !ok { + return fmt.Errorf("unexpected type %T for field payload", values[i]) + } else if value.Valid { + _m.Payload = value.String + } + case scheduledevent.FieldStatus: + if value, ok := values[i].(*sql.NullString); !ok { + return fmt.Errorf("unexpected type %T for field status", values[i]) + } else if value.Valid { + _m.Status = value.String + } + case scheduledevent.FieldCreatedBy: + if value, ok := values[i].(*sql.NullString); !ok { + return fmt.Errorf("unexpected type %T for field created_by", values[i]) + } else if value.Valid { + _m.CreatedBy = value.String + } + case scheduledevent.FieldFiredAt: + if value, ok := values[i].(*sql.NullTime); !ok { + return fmt.Errorf("unexpected type %T for field fired_at", values[i]) + } else if value.Valid { + _m.FiredAt = new(time.Time) + *_m.FiredAt = value.Time + } + case scheduledevent.FieldError: + if value, ok := values[i].(*sql.NullString); !ok { + return fmt.Errorf("unexpected type %T for field error", values[i]) + } else if value.Valid { + _m.Error = value.String + } + case scheduledevent.FieldScheduleID: + if value, ok := values[i].(*sql.NullString); !ok { + return fmt.Errorf("unexpected type %T for field schedule_id", values[i]) + } else if value.Valid { + _m.ScheduleID = value.String + } + case scheduledevent.FieldCreated: + if value, ok := values[i].(*sql.NullTime); !ok { + return fmt.Errorf("unexpected type %T for field created", values[i]) + } else if value.Valid { + _m.Created = value.Time + } + default: + _m.selectValues.Set(columns[i], values[i]) + } + } + return nil +} + +// Value returns the ent.Value that was dynamically selected and assigned to the ScheduledEvent. +// This includes values selected through modifiers, order, etc. +func (_m *ScheduledEvent) Value(name string) (ent.Value, error) { + return _m.selectValues.Get(name) +} + +// Update returns a builder for updating this ScheduledEvent. +// Note that you need to call ScheduledEvent.Unwrap() before calling this method if this ScheduledEvent +// was returned from a transaction, and the transaction was committed or rolled back. +func (_m *ScheduledEvent) Update() *ScheduledEventUpdateOne { + return NewScheduledEventClient(_m.config).UpdateOne(_m) +} + +// Unwrap unwraps the ScheduledEvent entity that was returned from a transaction after it was closed, +// so that all future queries will be executed through the driver which created the transaction. +func (_m *ScheduledEvent) Unwrap() *ScheduledEvent { + _tx, ok := _m.config.driver.(*txDriver) + if !ok { + panic("ent: ScheduledEvent is not a transactional entity") + } + _m.config.driver = _tx.drv + return _m +} + +// String implements the fmt.Stringer. +func (_m *ScheduledEvent) String() string { + var builder strings.Builder + builder.WriteString("ScheduledEvent(") + builder.WriteString(fmt.Sprintf("id=%v, ", _m.ID)) + builder.WriteString("project_id=") + builder.WriteString(fmt.Sprintf("%v", _m.ProjectID)) + builder.WriteString(", ") + builder.WriteString("event_type=") + builder.WriteString(_m.EventType) + builder.WriteString(", ") + builder.WriteString("fire_at=") + builder.WriteString(_m.FireAt.Format(time.ANSIC)) + builder.WriteString(", ") + builder.WriteString("payload=") + builder.WriteString(_m.Payload) + builder.WriteString(", ") + builder.WriteString("status=") + builder.WriteString(_m.Status) + builder.WriteString(", ") + builder.WriteString("created_by=") + builder.WriteString(_m.CreatedBy) + builder.WriteString(", ") + if v := _m.FiredAt; v != nil { + builder.WriteString("fired_at=") + builder.WriteString(v.Format(time.ANSIC)) + } + builder.WriteString(", ") + builder.WriteString("error=") + builder.WriteString(_m.Error) + builder.WriteString(", ") + builder.WriteString("schedule_id=") + builder.WriteString(_m.ScheduleID) + builder.WriteString(", ") + builder.WriteString("created=") + builder.WriteString(_m.Created.Format(time.ANSIC)) + builder.WriteByte(')') + return builder.String() +} + +// ScheduledEvents is a parsable slice of ScheduledEvent. +type ScheduledEvents []*ScheduledEvent diff --git a/pkg/ent/scheduledevent/scheduledevent.go b/pkg/ent/scheduledevent/scheduledevent.go new file mode 100644 index 000000000..e69cafb1f --- /dev/null +++ b/pkg/ent/scheduledevent/scheduledevent.go @@ -0,0 +1,135 @@ +// Code generated by ent, DO NOT EDIT. + +package scheduledevent + +import ( + "time" + + "entgo.io/ent/dialect/sql" + "github.com/google/uuid" +) + +const ( + // Label holds the string label denoting the scheduledevent type in the database. + Label = "scheduled_event" + // FieldID holds the string denoting the id field in the database. + FieldID = "id" + // FieldProjectID holds the string denoting the project_id field in the database. + FieldProjectID = "project_id" + // FieldEventType holds the string denoting the event_type field in the database. + FieldEventType = "event_type" + // FieldFireAt holds the string denoting the fire_at field in the database. + FieldFireAt = "fire_at" + // FieldPayload holds the string denoting the payload field in the database. + FieldPayload = "payload" + // FieldStatus holds the string denoting the status field in the database. + FieldStatus = "status" + // FieldCreatedBy holds the string denoting the created_by field in the database. + FieldCreatedBy = "created_by" + // FieldFiredAt holds the string denoting the fired_at field in the database. + FieldFiredAt = "fired_at" + // FieldError holds the string denoting the error field in the database. + FieldError = "error" + // FieldScheduleID holds the string denoting the schedule_id field in the database. + FieldScheduleID = "schedule_id" + // FieldCreated holds the string denoting the created field in the database. + FieldCreated = "created" + // Table holds the table name of the scheduledevent in the database. + Table = "scheduled_events" +) + +// Columns holds all SQL columns for scheduledevent fields. +var Columns = []string{ + FieldID, + FieldProjectID, + FieldEventType, + FieldFireAt, + FieldPayload, + FieldStatus, + FieldCreatedBy, + FieldFiredAt, + FieldError, + FieldScheduleID, + FieldCreated, +} + +// ValidColumn reports if the column name is valid (part of the table columns). +func ValidColumn(column string) bool { + for i := range Columns { + if column == Columns[i] { + return true + } + } + return false +} + +var ( + // EventTypeValidator is a validator for the "event_type" field. It is called by the builders before save. + EventTypeValidator func(string) error + // PayloadValidator is a validator for the "payload" field. It is called by the builders before save. + PayloadValidator func(string) error + // DefaultStatus holds the default value on creation for the "status" field. + DefaultStatus string + // DefaultCreated holds the default value on creation for the "created" field. + DefaultCreated func() time.Time + // DefaultID holds the default value on creation for the "id" field. + DefaultID func() uuid.UUID +) + +// OrderOption defines the ordering options for the ScheduledEvent queries. +type OrderOption func(*sql.Selector) + +// ByID orders the results by the id field. +func ByID(opts ...sql.OrderTermOption) OrderOption { + return sql.OrderByField(FieldID, opts...).ToFunc() +} + +// ByProjectID orders the results by the project_id field. +func ByProjectID(opts ...sql.OrderTermOption) OrderOption { + return sql.OrderByField(FieldProjectID, opts...).ToFunc() +} + +// ByEventType orders the results by the event_type field. +func ByEventType(opts ...sql.OrderTermOption) OrderOption { + return sql.OrderByField(FieldEventType, opts...).ToFunc() +} + +// ByFireAt orders the results by the fire_at field. +func ByFireAt(opts ...sql.OrderTermOption) OrderOption { + return sql.OrderByField(FieldFireAt, opts...).ToFunc() +} + +// ByPayload orders the results by the payload field. +func ByPayload(opts ...sql.OrderTermOption) OrderOption { + return sql.OrderByField(FieldPayload, opts...).ToFunc() +} + +// ByStatus orders the results by the status field. +func ByStatus(opts ...sql.OrderTermOption) OrderOption { + return sql.OrderByField(FieldStatus, opts...).ToFunc() +} + +// ByCreatedBy orders the results by the created_by field. +func ByCreatedBy(opts ...sql.OrderTermOption) OrderOption { + return sql.OrderByField(FieldCreatedBy, opts...).ToFunc() +} + +// ByFiredAt orders the results by the fired_at field. +func ByFiredAt(opts ...sql.OrderTermOption) OrderOption { + return sql.OrderByField(FieldFiredAt, opts...).ToFunc() +} + +// ByError orders the results by the error field. +func ByError(opts ...sql.OrderTermOption) OrderOption { + return sql.OrderByField(FieldError, opts...).ToFunc() +} + +// ByScheduleID orders the results by the schedule_id field. +func ByScheduleID(opts ...sql.OrderTermOption) OrderOption { + return sql.OrderByField(FieldScheduleID, opts...).ToFunc() +} + +// ByCreated orders the results by the created field. +func ByCreated(opts ...sql.OrderTermOption) OrderOption { + return sql.OrderByField(FieldCreated, opts...).ToFunc() +} diff --git a/pkg/ent/scheduledevent/where.go b/pkg/ent/scheduledevent/where.go new file mode 100644 index 000000000..0719b4f37 --- /dev/null +++ b/pkg/ent/scheduledevent/where.go @@ -0,0 +1,711 @@ +// Code generated by ent, DO NOT EDIT. + +package scheduledevent + +import ( + "time" + + "entgo.io/ent/dialect/sql" + "github.com/GoogleCloudPlatform/scion/pkg/ent/predicate" + "github.com/google/uuid" +) + +// ID filters vertices based on their ID field. +func ID(id uuid.UUID) predicate.ScheduledEvent { + return predicate.ScheduledEvent(sql.FieldEQ(FieldID, id)) +} + +// IDEQ applies the EQ predicate on the ID field. +func IDEQ(id uuid.UUID) predicate.ScheduledEvent { + return predicate.ScheduledEvent(sql.FieldEQ(FieldID, id)) +} + +// IDNEQ applies the NEQ predicate on the ID field. +func IDNEQ(id uuid.UUID) predicate.ScheduledEvent { + return predicate.ScheduledEvent(sql.FieldNEQ(FieldID, id)) +} + +// IDIn applies the In predicate on the ID field. +func IDIn(ids ...uuid.UUID) predicate.ScheduledEvent { + return predicate.ScheduledEvent(sql.FieldIn(FieldID, ids...)) +} + +// IDNotIn applies the NotIn predicate on the ID field. +func IDNotIn(ids ...uuid.UUID) predicate.ScheduledEvent { + return predicate.ScheduledEvent(sql.FieldNotIn(FieldID, ids...)) +} + +// IDGT applies the GT predicate on the ID field. +func IDGT(id uuid.UUID) predicate.ScheduledEvent { + return predicate.ScheduledEvent(sql.FieldGT(FieldID, id)) +} + +// IDGTE applies the GTE predicate on the ID field. +func IDGTE(id uuid.UUID) predicate.ScheduledEvent { + return predicate.ScheduledEvent(sql.FieldGTE(FieldID, id)) +} + +// IDLT applies the LT predicate on the ID field. +func IDLT(id uuid.UUID) predicate.ScheduledEvent { + return predicate.ScheduledEvent(sql.FieldLT(FieldID, id)) +} + +// IDLTE applies the LTE predicate on the ID field. +func IDLTE(id uuid.UUID) predicate.ScheduledEvent { + return predicate.ScheduledEvent(sql.FieldLTE(FieldID, id)) +} + +// ProjectID applies equality check predicate on the "project_id" field. It's identical to ProjectIDEQ. +func ProjectID(v uuid.UUID) predicate.ScheduledEvent { + return predicate.ScheduledEvent(sql.FieldEQ(FieldProjectID, v)) +} + +// EventType applies equality check predicate on the "event_type" field. It's identical to EventTypeEQ. +func EventType(v string) predicate.ScheduledEvent { + return predicate.ScheduledEvent(sql.FieldEQ(FieldEventType, v)) +} + +// FireAt applies equality check predicate on the "fire_at" field. It's identical to FireAtEQ. +func FireAt(v time.Time) predicate.ScheduledEvent { + return predicate.ScheduledEvent(sql.FieldEQ(FieldFireAt, v)) +} + +// Payload applies equality check predicate on the "payload" field. It's identical to PayloadEQ. +func Payload(v string) predicate.ScheduledEvent { + return predicate.ScheduledEvent(sql.FieldEQ(FieldPayload, v)) +} + +// Status applies equality check predicate on the "status" field. It's identical to StatusEQ. +func Status(v string) predicate.ScheduledEvent { + return predicate.ScheduledEvent(sql.FieldEQ(FieldStatus, v)) +} + +// CreatedBy applies equality check predicate on the "created_by" field. It's identical to CreatedByEQ. +func CreatedBy(v string) predicate.ScheduledEvent { + return predicate.ScheduledEvent(sql.FieldEQ(FieldCreatedBy, v)) +} + +// FiredAt applies equality check predicate on the "fired_at" field. It's identical to FiredAtEQ. +func FiredAt(v time.Time) predicate.ScheduledEvent { + return predicate.ScheduledEvent(sql.FieldEQ(FieldFiredAt, v)) +} + +// Error applies equality check predicate on the "error" field. It's identical to ErrorEQ. +func Error(v string) predicate.ScheduledEvent { + return predicate.ScheduledEvent(sql.FieldEQ(FieldError, v)) +} + +// ScheduleID applies equality check predicate on the "schedule_id" field. It's identical to ScheduleIDEQ. +func ScheduleID(v string) predicate.ScheduledEvent { + return predicate.ScheduledEvent(sql.FieldEQ(FieldScheduleID, v)) +} + +// Created applies equality check predicate on the "created" field. It's identical to CreatedEQ. +func Created(v time.Time) predicate.ScheduledEvent { + return predicate.ScheduledEvent(sql.FieldEQ(FieldCreated, v)) +} + +// ProjectIDEQ applies the EQ predicate on the "project_id" field. +func ProjectIDEQ(v uuid.UUID) predicate.ScheduledEvent { + return predicate.ScheduledEvent(sql.FieldEQ(FieldProjectID, v)) +} + +// ProjectIDNEQ applies the NEQ predicate on the "project_id" field. +func ProjectIDNEQ(v uuid.UUID) predicate.ScheduledEvent { + return predicate.ScheduledEvent(sql.FieldNEQ(FieldProjectID, v)) +} + +// ProjectIDIn applies the In predicate on the "project_id" field. +func ProjectIDIn(vs ...uuid.UUID) predicate.ScheduledEvent { + return predicate.ScheduledEvent(sql.FieldIn(FieldProjectID, vs...)) +} + +// ProjectIDNotIn applies the NotIn predicate on the "project_id" field. +func ProjectIDNotIn(vs ...uuid.UUID) predicate.ScheduledEvent { + return predicate.ScheduledEvent(sql.FieldNotIn(FieldProjectID, vs...)) +} + +// ProjectIDGT applies the GT predicate on the "project_id" field. +func ProjectIDGT(v uuid.UUID) predicate.ScheduledEvent { + return predicate.ScheduledEvent(sql.FieldGT(FieldProjectID, v)) +} + +// ProjectIDGTE applies the GTE predicate on the "project_id" field. +func ProjectIDGTE(v uuid.UUID) predicate.ScheduledEvent { + return predicate.ScheduledEvent(sql.FieldGTE(FieldProjectID, v)) +} + +// ProjectIDLT applies the LT predicate on the "project_id" field. +func ProjectIDLT(v uuid.UUID) predicate.ScheduledEvent { + return predicate.ScheduledEvent(sql.FieldLT(FieldProjectID, v)) +} + +// ProjectIDLTE applies the LTE predicate on the "project_id" field. +func ProjectIDLTE(v uuid.UUID) predicate.ScheduledEvent { + return predicate.ScheduledEvent(sql.FieldLTE(FieldProjectID, v)) +} + +// EventTypeEQ applies the EQ predicate on the "event_type" field. +func EventTypeEQ(v string) predicate.ScheduledEvent { + return predicate.ScheduledEvent(sql.FieldEQ(FieldEventType, v)) +} + +// EventTypeNEQ applies the NEQ predicate on the "event_type" field. +func EventTypeNEQ(v string) predicate.ScheduledEvent { + return predicate.ScheduledEvent(sql.FieldNEQ(FieldEventType, v)) +} + +// EventTypeIn applies the In predicate on the "event_type" field. +func EventTypeIn(vs ...string) predicate.ScheduledEvent { + return predicate.ScheduledEvent(sql.FieldIn(FieldEventType, vs...)) +} + +// EventTypeNotIn applies the NotIn predicate on the "event_type" field. +func EventTypeNotIn(vs ...string) predicate.ScheduledEvent { + return predicate.ScheduledEvent(sql.FieldNotIn(FieldEventType, vs...)) +} + +// EventTypeGT applies the GT predicate on the "event_type" field. +func EventTypeGT(v string) predicate.ScheduledEvent { + return predicate.ScheduledEvent(sql.FieldGT(FieldEventType, v)) +} + +// EventTypeGTE applies the GTE predicate on the "event_type" field. +func EventTypeGTE(v string) predicate.ScheduledEvent { + return predicate.ScheduledEvent(sql.FieldGTE(FieldEventType, v)) +} + +// EventTypeLT applies the LT predicate on the "event_type" field. +func EventTypeLT(v string) predicate.ScheduledEvent { + return predicate.ScheduledEvent(sql.FieldLT(FieldEventType, v)) +} + +// EventTypeLTE applies the LTE predicate on the "event_type" field. +func EventTypeLTE(v string) predicate.ScheduledEvent { + return predicate.ScheduledEvent(sql.FieldLTE(FieldEventType, v)) +} + +// EventTypeContains applies the Contains predicate on the "event_type" field. +func EventTypeContains(v string) predicate.ScheduledEvent { + return predicate.ScheduledEvent(sql.FieldContains(FieldEventType, v)) +} + +// EventTypeHasPrefix applies the HasPrefix predicate on the "event_type" field. +func EventTypeHasPrefix(v string) predicate.ScheduledEvent { + return predicate.ScheduledEvent(sql.FieldHasPrefix(FieldEventType, v)) +} + +// EventTypeHasSuffix applies the HasSuffix predicate on the "event_type" field. +func EventTypeHasSuffix(v string) predicate.ScheduledEvent { + return predicate.ScheduledEvent(sql.FieldHasSuffix(FieldEventType, v)) +} + +// EventTypeEqualFold applies the EqualFold predicate on the "event_type" field. +func EventTypeEqualFold(v string) predicate.ScheduledEvent { + return predicate.ScheduledEvent(sql.FieldEqualFold(FieldEventType, v)) +} + +// EventTypeContainsFold applies the ContainsFold predicate on the "event_type" field. +func EventTypeContainsFold(v string) predicate.ScheduledEvent { + return predicate.ScheduledEvent(sql.FieldContainsFold(FieldEventType, v)) +} + +// FireAtEQ applies the EQ predicate on the "fire_at" field. +func FireAtEQ(v time.Time) predicate.ScheduledEvent { + return predicate.ScheduledEvent(sql.FieldEQ(FieldFireAt, v)) +} + +// FireAtNEQ applies the NEQ predicate on the "fire_at" field. +func FireAtNEQ(v time.Time) predicate.ScheduledEvent { + return predicate.ScheduledEvent(sql.FieldNEQ(FieldFireAt, v)) +} + +// FireAtIn applies the In predicate on the "fire_at" field. +func FireAtIn(vs ...time.Time) predicate.ScheduledEvent { + return predicate.ScheduledEvent(sql.FieldIn(FieldFireAt, vs...)) +} + +// FireAtNotIn applies the NotIn predicate on the "fire_at" field. +func FireAtNotIn(vs ...time.Time) predicate.ScheduledEvent { + return predicate.ScheduledEvent(sql.FieldNotIn(FieldFireAt, vs...)) +} + +// FireAtGT applies the GT predicate on the "fire_at" field. +func FireAtGT(v time.Time) predicate.ScheduledEvent { + return predicate.ScheduledEvent(sql.FieldGT(FieldFireAt, v)) +} + +// FireAtGTE applies the GTE predicate on the "fire_at" field. +func FireAtGTE(v time.Time) predicate.ScheduledEvent { + return predicate.ScheduledEvent(sql.FieldGTE(FieldFireAt, v)) +} + +// FireAtLT applies the LT predicate on the "fire_at" field. +func FireAtLT(v time.Time) predicate.ScheduledEvent { + return predicate.ScheduledEvent(sql.FieldLT(FieldFireAt, v)) +} + +// FireAtLTE applies the LTE predicate on the "fire_at" field. +func FireAtLTE(v time.Time) predicate.ScheduledEvent { + return predicate.ScheduledEvent(sql.FieldLTE(FieldFireAt, v)) +} + +// PayloadEQ applies the EQ predicate on the "payload" field. +func PayloadEQ(v string) predicate.ScheduledEvent { + return predicate.ScheduledEvent(sql.FieldEQ(FieldPayload, v)) +} + +// PayloadNEQ applies the NEQ predicate on the "payload" field. +func PayloadNEQ(v string) predicate.ScheduledEvent { + return predicate.ScheduledEvent(sql.FieldNEQ(FieldPayload, v)) +} + +// PayloadIn applies the In predicate on the "payload" field. +func PayloadIn(vs ...string) predicate.ScheduledEvent { + return predicate.ScheduledEvent(sql.FieldIn(FieldPayload, vs...)) +} + +// PayloadNotIn applies the NotIn predicate on the "payload" field. +func PayloadNotIn(vs ...string) predicate.ScheduledEvent { + return predicate.ScheduledEvent(sql.FieldNotIn(FieldPayload, vs...)) +} + +// PayloadGT applies the GT predicate on the "payload" field. +func PayloadGT(v string) predicate.ScheduledEvent { + return predicate.ScheduledEvent(sql.FieldGT(FieldPayload, v)) +} + +// PayloadGTE applies the GTE predicate on the "payload" field. +func PayloadGTE(v string) predicate.ScheduledEvent { + return predicate.ScheduledEvent(sql.FieldGTE(FieldPayload, v)) +} + +// PayloadLT applies the LT predicate on the "payload" field. +func PayloadLT(v string) predicate.ScheduledEvent { + return predicate.ScheduledEvent(sql.FieldLT(FieldPayload, v)) +} + +// PayloadLTE applies the LTE predicate on the "payload" field. +func PayloadLTE(v string) predicate.ScheduledEvent { + return predicate.ScheduledEvent(sql.FieldLTE(FieldPayload, v)) +} + +// PayloadContains applies the Contains predicate on the "payload" field. +func PayloadContains(v string) predicate.ScheduledEvent { + return predicate.ScheduledEvent(sql.FieldContains(FieldPayload, v)) +} + +// PayloadHasPrefix applies the HasPrefix predicate on the "payload" field. +func PayloadHasPrefix(v string) predicate.ScheduledEvent { + return predicate.ScheduledEvent(sql.FieldHasPrefix(FieldPayload, v)) +} + +// PayloadHasSuffix applies the HasSuffix predicate on the "payload" field. +func PayloadHasSuffix(v string) predicate.ScheduledEvent { + return predicate.ScheduledEvent(sql.FieldHasSuffix(FieldPayload, v)) +} + +// PayloadEqualFold applies the EqualFold predicate on the "payload" field. +func PayloadEqualFold(v string) predicate.ScheduledEvent { + return predicate.ScheduledEvent(sql.FieldEqualFold(FieldPayload, v)) +} + +// PayloadContainsFold applies the ContainsFold predicate on the "payload" field. +func PayloadContainsFold(v string) predicate.ScheduledEvent { + return predicate.ScheduledEvent(sql.FieldContainsFold(FieldPayload, v)) +} + +// StatusEQ applies the EQ predicate on the "status" field. +func StatusEQ(v string) predicate.ScheduledEvent { + return predicate.ScheduledEvent(sql.FieldEQ(FieldStatus, v)) +} + +// StatusNEQ applies the NEQ predicate on the "status" field. +func StatusNEQ(v string) predicate.ScheduledEvent { + return predicate.ScheduledEvent(sql.FieldNEQ(FieldStatus, v)) +} + +// StatusIn applies the In predicate on the "status" field. +func StatusIn(vs ...string) predicate.ScheduledEvent { + return predicate.ScheduledEvent(sql.FieldIn(FieldStatus, vs...)) +} + +// StatusNotIn applies the NotIn predicate on the "status" field. +func StatusNotIn(vs ...string) predicate.ScheduledEvent { + return predicate.ScheduledEvent(sql.FieldNotIn(FieldStatus, vs...)) +} + +// StatusGT applies the GT predicate on the "status" field. +func StatusGT(v string) predicate.ScheduledEvent { + return predicate.ScheduledEvent(sql.FieldGT(FieldStatus, v)) +} + +// StatusGTE applies the GTE predicate on the "status" field. +func StatusGTE(v string) predicate.ScheduledEvent { + return predicate.ScheduledEvent(sql.FieldGTE(FieldStatus, v)) +} + +// StatusLT applies the LT predicate on the "status" field. +func StatusLT(v string) predicate.ScheduledEvent { + return predicate.ScheduledEvent(sql.FieldLT(FieldStatus, v)) +} + +// StatusLTE applies the LTE predicate on the "status" field. +func StatusLTE(v string) predicate.ScheduledEvent { + return predicate.ScheduledEvent(sql.FieldLTE(FieldStatus, v)) +} + +// StatusContains applies the Contains predicate on the "status" field. +func StatusContains(v string) predicate.ScheduledEvent { + return predicate.ScheduledEvent(sql.FieldContains(FieldStatus, v)) +} + +// StatusHasPrefix applies the HasPrefix predicate on the "status" field. +func StatusHasPrefix(v string) predicate.ScheduledEvent { + return predicate.ScheduledEvent(sql.FieldHasPrefix(FieldStatus, v)) +} + +// StatusHasSuffix applies the HasSuffix predicate on the "status" field. +func StatusHasSuffix(v string) predicate.ScheduledEvent { + return predicate.ScheduledEvent(sql.FieldHasSuffix(FieldStatus, v)) +} + +// StatusEqualFold applies the EqualFold predicate on the "status" field. +func StatusEqualFold(v string) predicate.ScheduledEvent { + return predicate.ScheduledEvent(sql.FieldEqualFold(FieldStatus, v)) +} + +// StatusContainsFold applies the ContainsFold predicate on the "status" field. +func StatusContainsFold(v string) predicate.ScheduledEvent { + return predicate.ScheduledEvent(sql.FieldContainsFold(FieldStatus, v)) +} + +// CreatedByEQ applies the EQ predicate on the "created_by" field. +func CreatedByEQ(v string) predicate.ScheduledEvent { + return predicate.ScheduledEvent(sql.FieldEQ(FieldCreatedBy, v)) +} + +// CreatedByNEQ applies the NEQ predicate on the "created_by" field. +func CreatedByNEQ(v string) predicate.ScheduledEvent { + return predicate.ScheduledEvent(sql.FieldNEQ(FieldCreatedBy, v)) +} + +// CreatedByIn applies the In predicate on the "created_by" field. +func CreatedByIn(vs ...string) predicate.ScheduledEvent { + return predicate.ScheduledEvent(sql.FieldIn(FieldCreatedBy, vs...)) +} + +// CreatedByNotIn applies the NotIn predicate on the "created_by" field. +func CreatedByNotIn(vs ...string) predicate.ScheduledEvent { + return predicate.ScheduledEvent(sql.FieldNotIn(FieldCreatedBy, vs...)) +} + +// CreatedByGT applies the GT predicate on the "created_by" field. +func CreatedByGT(v string) predicate.ScheduledEvent { + return predicate.ScheduledEvent(sql.FieldGT(FieldCreatedBy, v)) +} + +// CreatedByGTE applies the GTE predicate on the "created_by" field. +func CreatedByGTE(v string) predicate.ScheduledEvent { + return predicate.ScheduledEvent(sql.FieldGTE(FieldCreatedBy, v)) +} + +// CreatedByLT applies the LT predicate on the "created_by" field. +func CreatedByLT(v string) predicate.ScheduledEvent { + return predicate.ScheduledEvent(sql.FieldLT(FieldCreatedBy, v)) +} + +// CreatedByLTE applies the LTE predicate on the "created_by" field. +func CreatedByLTE(v string) predicate.ScheduledEvent { + return predicate.ScheduledEvent(sql.FieldLTE(FieldCreatedBy, v)) +} + +// CreatedByContains applies the Contains predicate on the "created_by" field. +func CreatedByContains(v string) predicate.ScheduledEvent { + return predicate.ScheduledEvent(sql.FieldContains(FieldCreatedBy, v)) +} + +// CreatedByHasPrefix applies the HasPrefix predicate on the "created_by" field. +func CreatedByHasPrefix(v string) predicate.ScheduledEvent { + return predicate.ScheduledEvent(sql.FieldHasPrefix(FieldCreatedBy, v)) +} + +// CreatedByHasSuffix applies the HasSuffix predicate on the "created_by" field. +func CreatedByHasSuffix(v string) predicate.ScheduledEvent { + return predicate.ScheduledEvent(sql.FieldHasSuffix(FieldCreatedBy, v)) +} + +// CreatedByIsNil applies the IsNil predicate on the "created_by" field. +func CreatedByIsNil() predicate.ScheduledEvent { + return predicate.ScheduledEvent(sql.FieldIsNull(FieldCreatedBy)) +} + +// CreatedByNotNil applies the NotNil predicate on the "created_by" field. +func CreatedByNotNil() predicate.ScheduledEvent { + return predicate.ScheduledEvent(sql.FieldNotNull(FieldCreatedBy)) +} + +// CreatedByEqualFold applies the EqualFold predicate on the "created_by" field. +func CreatedByEqualFold(v string) predicate.ScheduledEvent { + return predicate.ScheduledEvent(sql.FieldEqualFold(FieldCreatedBy, v)) +} + +// CreatedByContainsFold applies the ContainsFold predicate on the "created_by" field. +func CreatedByContainsFold(v string) predicate.ScheduledEvent { + return predicate.ScheduledEvent(sql.FieldContainsFold(FieldCreatedBy, v)) +} + +// FiredAtEQ applies the EQ predicate on the "fired_at" field. +func FiredAtEQ(v time.Time) predicate.ScheduledEvent { + return predicate.ScheduledEvent(sql.FieldEQ(FieldFiredAt, v)) +} + +// FiredAtNEQ applies the NEQ predicate on the "fired_at" field. +func FiredAtNEQ(v time.Time) predicate.ScheduledEvent { + return predicate.ScheduledEvent(sql.FieldNEQ(FieldFiredAt, v)) +} + +// FiredAtIn applies the In predicate on the "fired_at" field. +func FiredAtIn(vs ...time.Time) predicate.ScheduledEvent { + return predicate.ScheduledEvent(sql.FieldIn(FieldFiredAt, vs...)) +} + +// FiredAtNotIn applies the NotIn predicate on the "fired_at" field. +func FiredAtNotIn(vs ...time.Time) predicate.ScheduledEvent { + return predicate.ScheduledEvent(sql.FieldNotIn(FieldFiredAt, vs...)) +} + +// FiredAtGT applies the GT predicate on the "fired_at" field. +func FiredAtGT(v time.Time) predicate.ScheduledEvent { + return predicate.ScheduledEvent(sql.FieldGT(FieldFiredAt, v)) +} + +// FiredAtGTE applies the GTE predicate on the "fired_at" field. +func FiredAtGTE(v time.Time) predicate.ScheduledEvent { + return predicate.ScheduledEvent(sql.FieldGTE(FieldFiredAt, v)) +} + +// FiredAtLT applies the LT predicate on the "fired_at" field. +func FiredAtLT(v time.Time) predicate.ScheduledEvent { + return predicate.ScheduledEvent(sql.FieldLT(FieldFiredAt, v)) +} + +// FiredAtLTE applies the LTE predicate on the "fired_at" field. +func FiredAtLTE(v time.Time) predicate.ScheduledEvent { + return predicate.ScheduledEvent(sql.FieldLTE(FieldFiredAt, v)) +} + +// FiredAtIsNil applies the IsNil predicate on the "fired_at" field. +func FiredAtIsNil() predicate.ScheduledEvent { + return predicate.ScheduledEvent(sql.FieldIsNull(FieldFiredAt)) +} + +// FiredAtNotNil applies the NotNil predicate on the "fired_at" field. +func FiredAtNotNil() predicate.ScheduledEvent { + return predicate.ScheduledEvent(sql.FieldNotNull(FieldFiredAt)) +} + +// ErrorEQ applies the EQ predicate on the "error" field. +func ErrorEQ(v string) predicate.ScheduledEvent { + return predicate.ScheduledEvent(sql.FieldEQ(FieldError, v)) +} + +// ErrorNEQ applies the NEQ predicate on the "error" field. +func ErrorNEQ(v string) predicate.ScheduledEvent { + return predicate.ScheduledEvent(sql.FieldNEQ(FieldError, v)) +} + +// ErrorIn applies the In predicate on the "error" field. +func ErrorIn(vs ...string) predicate.ScheduledEvent { + return predicate.ScheduledEvent(sql.FieldIn(FieldError, vs...)) +} + +// ErrorNotIn applies the NotIn predicate on the "error" field. +func ErrorNotIn(vs ...string) predicate.ScheduledEvent { + return predicate.ScheduledEvent(sql.FieldNotIn(FieldError, vs...)) +} + +// ErrorGT applies the GT predicate on the "error" field. +func ErrorGT(v string) predicate.ScheduledEvent { + return predicate.ScheduledEvent(sql.FieldGT(FieldError, v)) +} + +// ErrorGTE applies the GTE predicate on the "error" field. +func ErrorGTE(v string) predicate.ScheduledEvent { + return predicate.ScheduledEvent(sql.FieldGTE(FieldError, v)) +} + +// ErrorLT applies the LT predicate on the "error" field. +func ErrorLT(v string) predicate.ScheduledEvent { + return predicate.ScheduledEvent(sql.FieldLT(FieldError, v)) +} + +// ErrorLTE applies the LTE predicate on the "error" field. +func ErrorLTE(v string) predicate.ScheduledEvent { + return predicate.ScheduledEvent(sql.FieldLTE(FieldError, v)) +} + +// ErrorContains applies the Contains predicate on the "error" field. +func ErrorContains(v string) predicate.ScheduledEvent { + return predicate.ScheduledEvent(sql.FieldContains(FieldError, v)) +} + +// ErrorHasPrefix applies the HasPrefix predicate on the "error" field. +func ErrorHasPrefix(v string) predicate.ScheduledEvent { + return predicate.ScheduledEvent(sql.FieldHasPrefix(FieldError, v)) +} + +// ErrorHasSuffix applies the HasSuffix predicate on the "error" field. +func ErrorHasSuffix(v string) predicate.ScheduledEvent { + return predicate.ScheduledEvent(sql.FieldHasSuffix(FieldError, v)) +} + +// ErrorIsNil applies the IsNil predicate on the "error" field. +func ErrorIsNil() predicate.ScheduledEvent { + return predicate.ScheduledEvent(sql.FieldIsNull(FieldError)) +} + +// ErrorNotNil applies the NotNil predicate on the "error" field. +func ErrorNotNil() predicate.ScheduledEvent { + return predicate.ScheduledEvent(sql.FieldNotNull(FieldError)) +} + +// ErrorEqualFold applies the EqualFold predicate on the "error" field. +func ErrorEqualFold(v string) predicate.ScheduledEvent { + return predicate.ScheduledEvent(sql.FieldEqualFold(FieldError, v)) +} + +// ErrorContainsFold applies the ContainsFold predicate on the "error" field. +func ErrorContainsFold(v string) predicate.ScheduledEvent { + return predicate.ScheduledEvent(sql.FieldContainsFold(FieldError, v)) +} + +// ScheduleIDEQ applies the EQ predicate on the "schedule_id" field. +func ScheduleIDEQ(v string) predicate.ScheduledEvent { + return predicate.ScheduledEvent(sql.FieldEQ(FieldScheduleID, v)) +} + +// ScheduleIDNEQ applies the NEQ predicate on the "schedule_id" field. +func ScheduleIDNEQ(v string) predicate.ScheduledEvent { + return predicate.ScheduledEvent(sql.FieldNEQ(FieldScheduleID, v)) +} + +// ScheduleIDIn applies the In predicate on the "schedule_id" field. +func ScheduleIDIn(vs ...string) predicate.ScheduledEvent { + return predicate.ScheduledEvent(sql.FieldIn(FieldScheduleID, vs...)) +} + +// ScheduleIDNotIn applies the NotIn predicate on the "schedule_id" field. +func ScheduleIDNotIn(vs ...string) predicate.ScheduledEvent { + return predicate.ScheduledEvent(sql.FieldNotIn(FieldScheduleID, vs...)) +} + +// ScheduleIDGT applies the GT predicate on the "schedule_id" field. +func ScheduleIDGT(v string) predicate.ScheduledEvent { + return predicate.ScheduledEvent(sql.FieldGT(FieldScheduleID, v)) +} + +// ScheduleIDGTE applies the GTE predicate on the "schedule_id" field. +func ScheduleIDGTE(v string) predicate.ScheduledEvent { + return predicate.ScheduledEvent(sql.FieldGTE(FieldScheduleID, v)) +} + +// ScheduleIDLT applies the LT predicate on the "schedule_id" field. +func ScheduleIDLT(v string) predicate.ScheduledEvent { + return predicate.ScheduledEvent(sql.FieldLT(FieldScheduleID, v)) +} + +// ScheduleIDLTE applies the LTE predicate on the "schedule_id" field. +func ScheduleIDLTE(v string) predicate.ScheduledEvent { + return predicate.ScheduledEvent(sql.FieldLTE(FieldScheduleID, v)) +} + +// ScheduleIDContains applies the Contains predicate on the "schedule_id" field. +func ScheduleIDContains(v string) predicate.ScheduledEvent { + return predicate.ScheduledEvent(sql.FieldContains(FieldScheduleID, v)) +} + +// ScheduleIDHasPrefix applies the HasPrefix predicate on the "schedule_id" field. +func ScheduleIDHasPrefix(v string) predicate.ScheduledEvent { + return predicate.ScheduledEvent(sql.FieldHasPrefix(FieldScheduleID, v)) +} + +// ScheduleIDHasSuffix applies the HasSuffix predicate on the "schedule_id" field. +func ScheduleIDHasSuffix(v string) predicate.ScheduledEvent { + return predicate.ScheduledEvent(sql.FieldHasSuffix(FieldScheduleID, v)) +} + +// ScheduleIDIsNil applies the IsNil predicate on the "schedule_id" field. +func ScheduleIDIsNil() predicate.ScheduledEvent { + return predicate.ScheduledEvent(sql.FieldIsNull(FieldScheduleID)) +} + +// ScheduleIDNotNil applies the NotNil predicate on the "schedule_id" field. +func ScheduleIDNotNil() predicate.ScheduledEvent { + return predicate.ScheduledEvent(sql.FieldNotNull(FieldScheduleID)) +} + +// ScheduleIDEqualFold applies the EqualFold predicate on the "schedule_id" field. +func ScheduleIDEqualFold(v string) predicate.ScheduledEvent { + return predicate.ScheduledEvent(sql.FieldEqualFold(FieldScheduleID, v)) +} + +// ScheduleIDContainsFold applies the ContainsFold predicate on the "schedule_id" field. +func ScheduleIDContainsFold(v string) predicate.ScheduledEvent { + return predicate.ScheduledEvent(sql.FieldContainsFold(FieldScheduleID, v)) +} + +// CreatedEQ applies the EQ predicate on the "created" field. +func CreatedEQ(v time.Time) predicate.ScheduledEvent { + return predicate.ScheduledEvent(sql.FieldEQ(FieldCreated, v)) +} + +// CreatedNEQ applies the NEQ predicate on the "created" field. +func CreatedNEQ(v time.Time) predicate.ScheduledEvent { + return predicate.ScheduledEvent(sql.FieldNEQ(FieldCreated, v)) +} + +// CreatedIn applies the In predicate on the "created" field. +func CreatedIn(vs ...time.Time) predicate.ScheduledEvent { + return predicate.ScheduledEvent(sql.FieldIn(FieldCreated, vs...)) +} + +// CreatedNotIn applies the NotIn predicate on the "created" field. +func CreatedNotIn(vs ...time.Time) predicate.ScheduledEvent { + return predicate.ScheduledEvent(sql.FieldNotIn(FieldCreated, vs...)) +} + +// CreatedGT applies the GT predicate on the "created" field. +func CreatedGT(v time.Time) predicate.ScheduledEvent { + return predicate.ScheduledEvent(sql.FieldGT(FieldCreated, v)) +} + +// CreatedGTE applies the GTE predicate on the "created" field. +func CreatedGTE(v time.Time) predicate.ScheduledEvent { + return predicate.ScheduledEvent(sql.FieldGTE(FieldCreated, v)) +} + +// CreatedLT applies the LT predicate on the "created" field. +func CreatedLT(v time.Time) predicate.ScheduledEvent { + return predicate.ScheduledEvent(sql.FieldLT(FieldCreated, v)) +} + +// CreatedLTE applies the LTE predicate on the "created" field. +func CreatedLTE(v time.Time) predicate.ScheduledEvent { + return predicate.ScheduledEvent(sql.FieldLTE(FieldCreated, v)) +} + +// And groups predicates with the AND operator between them. +func And(predicates ...predicate.ScheduledEvent) predicate.ScheduledEvent { + return predicate.ScheduledEvent(sql.AndPredicates(predicates...)) +} + +// Or groups predicates with the OR operator between them. +func Or(predicates ...predicate.ScheduledEvent) predicate.ScheduledEvent { + return predicate.ScheduledEvent(sql.OrPredicates(predicates...)) +} + +// Not applies the not operator on the given predicate. +func Not(p predicate.ScheduledEvent) predicate.ScheduledEvent { + return predicate.ScheduledEvent(sql.NotPredicates(p)) +} diff --git a/pkg/ent/scheduledevent_create.go b/pkg/ent/scheduledevent_create.go new file mode 100644 index 000000000..790facdc5 --- /dev/null +++ b/pkg/ent/scheduledevent_create.go @@ -0,0 +1,1086 @@ +// Code generated by ent, DO NOT EDIT. + +package ent + +import ( + "context" + "errors" + "fmt" + "time" + + "entgo.io/ent/dialect" + "entgo.io/ent/dialect/sql" + "entgo.io/ent/dialect/sql/sqlgraph" + "entgo.io/ent/schema/field" + "github.com/GoogleCloudPlatform/scion/pkg/ent/scheduledevent" + "github.com/google/uuid" +) + +// ScheduledEventCreate is the builder for creating a ScheduledEvent entity. +type ScheduledEventCreate struct { + config + mutation *ScheduledEventMutation + hooks []Hook + conflict []sql.ConflictOption +} + +// SetProjectID sets the "project_id" field. +func (_c *ScheduledEventCreate) SetProjectID(v uuid.UUID) *ScheduledEventCreate { + _c.mutation.SetProjectID(v) + return _c +} + +// SetEventType sets the "event_type" field. +func (_c *ScheduledEventCreate) SetEventType(v string) *ScheduledEventCreate { + _c.mutation.SetEventType(v) + return _c +} + +// SetFireAt sets the "fire_at" field. +func (_c *ScheduledEventCreate) SetFireAt(v time.Time) *ScheduledEventCreate { + _c.mutation.SetFireAt(v) + return _c +} + +// SetPayload sets the "payload" field. +func (_c *ScheduledEventCreate) SetPayload(v string) *ScheduledEventCreate { + _c.mutation.SetPayload(v) + return _c +} + +// SetStatus sets the "status" field. +func (_c *ScheduledEventCreate) SetStatus(v string) *ScheduledEventCreate { + _c.mutation.SetStatus(v) + return _c +} + +// SetNillableStatus sets the "status" field if the given value is not nil. +func (_c *ScheduledEventCreate) SetNillableStatus(v *string) *ScheduledEventCreate { + if v != nil { + _c.SetStatus(*v) + } + return _c +} + +// SetCreatedBy sets the "created_by" field. +func (_c *ScheduledEventCreate) SetCreatedBy(v string) *ScheduledEventCreate { + _c.mutation.SetCreatedBy(v) + return _c +} + +// SetNillableCreatedBy sets the "created_by" field if the given value is not nil. +func (_c *ScheduledEventCreate) SetNillableCreatedBy(v *string) *ScheduledEventCreate { + if v != nil { + _c.SetCreatedBy(*v) + } + return _c +} + +// SetFiredAt sets the "fired_at" field. +func (_c *ScheduledEventCreate) SetFiredAt(v time.Time) *ScheduledEventCreate { + _c.mutation.SetFiredAt(v) + return _c +} + +// SetNillableFiredAt sets the "fired_at" field if the given value is not nil. +func (_c *ScheduledEventCreate) SetNillableFiredAt(v *time.Time) *ScheduledEventCreate { + if v != nil { + _c.SetFiredAt(*v) + } + return _c +} + +// SetError sets the "error" field. +func (_c *ScheduledEventCreate) SetError(v string) *ScheduledEventCreate { + _c.mutation.SetError(v) + return _c +} + +// SetNillableError sets the "error" field if the given value is not nil. +func (_c *ScheduledEventCreate) SetNillableError(v *string) *ScheduledEventCreate { + if v != nil { + _c.SetError(*v) + } + return _c +} + +// SetScheduleID sets the "schedule_id" field. +func (_c *ScheduledEventCreate) SetScheduleID(v string) *ScheduledEventCreate { + _c.mutation.SetScheduleID(v) + return _c +} + +// SetNillableScheduleID sets the "schedule_id" field if the given value is not nil. +func (_c *ScheduledEventCreate) SetNillableScheduleID(v *string) *ScheduledEventCreate { + if v != nil { + _c.SetScheduleID(*v) + } + return _c +} + +// SetCreated sets the "created" field. +func (_c *ScheduledEventCreate) SetCreated(v time.Time) *ScheduledEventCreate { + _c.mutation.SetCreated(v) + return _c +} + +// SetNillableCreated sets the "created" field if the given value is not nil. +func (_c *ScheduledEventCreate) SetNillableCreated(v *time.Time) *ScheduledEventCreate { + if v != nil { + _c.SetCreated(*v) + } + return _c +} + +// SetID sets the "id" field. +func (_c *ScheduledEventCreate) SetID(v uuid.UUID) *ScheduledEventCreate { + _c.mutation.SetID(v) + return _c +} + +// SetNillableID sets the "id" field if the given value is not nil. +func (_c *ScheduledEventCreate) SetNillableID(v *uuid.UUID) *ScheduledEventCreate { + if v != nil { + _c.SetID(*v) + } + return _c +} + +// Mutation returns the ScheduledEventMutation object of the builder. +func (_c *ScheduledEventCreate) Mutation() *ScheduledEventMutation { + return _c.mutation +} + +// Save creates the ScheduledEvent in the database. +func (_c *ScheduledEventCreate) Save(ctx context.Context) (*ScheduledEvent, error) { + _c.defaults() + return withHooks(ctx, _c.sqlSave, _c.mutation, _c.hooks) +} + +// SaveX calls Save and panics if Save returns an error. +func (_c *ScheduledEventCreate) SaveX(ctx context.Context) *ScheduledEvent { + v, err := _c.Save(ctx) + if err != nil { + panic(err) + } + return v +} + +// Exec executes the query. +func (_c *ScheduledEventCreate) Exec(ctx context.Context) error { + _, err := _c.Save(ctx) + return err +} + +// ExecX is like Exec, but panics if an error occurs. +func (_c *ScheduledEventCreate) ExecX(ctx context.Context) { + if err := _c.Exec(ctx); err != nil { + panic(err) + } +} + +// defaults sets the default values of the builder before save. +func (_c *ScheduledEventCreate) defaults() { + if _, ok := _c.mutation.Status(); !ok { + v := scheduledevent.DefaultStatus + _c.mutation.SetStatus(v) + } + if _, ok := _c.mutation.Created(); !ok { + v := scheduledevent.DefaultCreated() + _c.mutation.SetCreated(v) + } + if _, ok := _c.mutation.ID(); !ok { + v := scheduledevent.DefaultID() + _c.mutation.SetID(v) + } +} + +// check runs all checks and user-defined validators on the builder. +func (_c *ScheduledEventCreate) check() error { + if _, ok := _c.mutation.ProjectID(); !ok { + return &ValidationError{Name: "project_id", err: errors.New(`ent: missing required field "ScheduledEvent.project_id"`)} + } + if _, ok := _c.mutation.EventType(); !ok { + return &ValidationError{Name: "event_type", err: errors.New(`ent: missing required field "ScheduledEvent.event_type"`)} + } + if v, ok := _c.mutation.EventType(); ok { + if err := scheduledevent.EventTypeValidator(v); err != nil { + return &ValidationError{Name: "event_type", err: fmt.Errorf(`ent: validator failed for field "ScheduledEvent.event_type": %w`, err)} + } + } + if _, ok := _c.mutation.FireAt(); !ok { + return &ValidationError{Name: "fire_at", err: errors.New(`ent: missing required field "ScheduledEvent.fire_at"`)} + } + if _, ok := _c.mutation.Payload(); !ok { + return &ValidationError{Name: "payload", err: errors.New(`ent: missing required field "ScheduledEvent.payload"`)} + } + if v, ok := _c.mutation.Payload(); ok { + if err := scheduledevent.PayloadValidator(v); err != nil { + return &ValidationError{Name: "payload", err: fmt.Errorf(`ent: validator failed for field "ScheduledEvent.payload": %w`, err)} + } + } + if _, ok := _c.mutation.Status(); !ok { + return &ValidationError{Name: "status", err: errors.New(`ent: missing required field "ScheduledEvent.status"`)} + } + if _, ok := _c.mutation.Created(); !ok { + return &ValidationError{Name: "created", err: errors.New(`ent: missing required field "ScheduledEvent.created"`)} + } + return nil +} + +func (_c *ScheduledEventCreate) sqlSave(ctx context.Context) (*ScheduledEvent, error) { + if err := _c.check(); err != nil { + return nil, err + } + _node, _spec := _c.createSpec() + if err := sqlgraph.CreateNode(ctx, _c.driver, _spec); err != nil { + if sqlgraph.IsConstraintError(err) { + err = &ConstraintError{msg: err.Error(), wrap: err} + } + return nil, err + } + if _spec.ID.Value != nil { + if id, ok := _spec.ID.Value.(*uuid.UUID); ok { + _node.ID = *id + } else if err := _node.ID.Scan(_spec.ID.Value); err != nil { + return nil, err + } + } + _c.mutation.id = &_node.ID + _c.mutation.done = true + return _node, nil +} + +func (_c *ScheduledEventCreate) createSpec() (*ScheduledEvent, *sqlgraph.CreateSpec) { + var ( + _node = &ScheduledEvent{config: _c.config} + _spec = sqlgraph.NewCreateSpec(scheduledevent.Table, sqlgraph.NewFieldSpec(scheduledevent.FieldID, field.TypeUUID)) + ) + _spec.OnConflict = _c.conflict + if id, ok := _c.mutation.ID(); ok { + _node.ID = id + _spec.ID.Value = &id + } + if value, ok := _c.mutation.ProjectID(); ok { + _spec.SetField(scheduledevent.FieldProjectID, field.TypeUUID, value) + _node.ProjectID = value + } + if value, ok := _c.mutation.EventType(); ok { + _spec.SetField(scheduledevent.FieldEventType, field.TypeString, value) + _node.EventType = value + } + if value, ok := _c.mutation.FireAt(); ok { + _spec.SetField(scheduledevent.FieldFireAt, field.TypeTime, value) + _node.FireAt = value + } + if value, ok := _c.mutation.Payload(); ok { + _spec.SetField(scheduledevent.FieldPayload, field.TypeString, value) + _node.Payload = value + } + if value, ok := _c.mutation.Status(); ok { + _spec.SetField(scheduledevent.FieldStatus, field.TypeString, value) + _node.Status = value + } + if value, ok := _c.mutation.CreatedBy(); ok { + _spec.SetField(scheduledevent.FieldCreatedBy, field.TypeString, value) + _node.CreatedBy = value + } + if value, ok := _c.mutation.FiredAt(); ok { + _spec.SetField(scheduledevent.FieldFiredAt, field.TypeTime, value) + _node.FiredAt = &value + } + if value, ok := _c.mutation.Error(); ok { + _spec.SetField(scheduledevent.FieldError, field.TypeString, value) + _node.Error = value + } + if value, ok := _c.mutation.ScheduleID(); ok { + _spec.SetField(scheduledevent.FieldScheduleID, field.TypeString, value) + _node.ScheduleID = value + } + if value, ok := _c.mutation.Created(); ok { + _spec.SetField(scheduledevent.FieldCreated, field.TypeTime, value) + _node.Created = value + } + return _node, _spec +} + +// OnConflict allows configuring the `ON CONFLICT` / `ON DUPLICATE KEY` clause +// of the `INSERT` statement. For example: +// +// client.ScheduledEvent.Create(). +// SetProjectID(v). +// OnConflict( +// // Update the row with the new values +// // the was proposed for insertion. +// sql.ResolveWithNewValues(), +// ). +// // Override some of the fields with custom +// // update values. +// Update(func(u *ent.ScheduledEventUpsert) { +// SetProjectID(v+v). +// }). +// Exec(ctx) +func (_c *ScheduledEventCreate) OnConflict(opts ...sql.ConflictOption) *ScheduledEventUpsertOne { + _c.conflict = opts + return &ScheduledEventUpsertOne{ + create: _c, + } +} + +// OnConflictColumns calls `OnConflict` and configures the columns +// as conflict target. Using this option is equivalent to using: +// +// client.ScheduledEvent.Create(). +// OnConflict(sql.ConflictColumns(columns...)). +// Exec(ctx) +func (_c *ScheduledEventCreate) OnConflictColumns(columns ...string) *ScheduledEventUpsertOne { + _c.conflict = append(_c.conflict, sql.ConflictColumns(columns...)) + return &ScheduledEventUpsertOne{ + create: _c, + } +} + +type ( + // ScheduledEventUpsertOne is the builder for "upsert"-ing + // one ScheduledEvent node. + ScheduledEventUpsertOne struct { + create *ScheduledEventCreate + } + + // ScheduledEventUpsert is the "OnConflict" setter. + ScheduledEventUpsert struct { + *sql.UpdateSet + } +) + +// SetProjectID sets the "project_id" field. +func (u *ScheduledEventUpsert) SetProjectID(v uuid.UUID) *ScheduledEventUpsert { + u.Set(scheduledevent.FieldProjectID, v) + return u +} + +// UpdateProjectID sets the "project_id" field to the value that was provided on create. +func (u *ScheduledEventUpsert) UpdateProjectID() *ScheduledEventUpsert { + u.SetExcluded(scheduledevent.FieldProjectID) + return u +} + +// SetEventType sets the "event_type" field. +func (u *ScheduledEventUpsert) SetEventType(v string) *ScheduledEventUpsert { + u.Set(scheduledevent.FieldEventType, v) + return u +} + +// UpdateEventType sets the "event_type" field to the value that was provided on create. +func (u *ScheduledEventUpsert) UpdateEventType() *ScheduledEventUpsert { + u.SetExcluded(scheduledevent.FieldEventType) + return u +} + +// SetFireAt sets the "fire_at" field. +func (u *ScheduledEventUpsert) SetFireAt(v time.Time) *ScheduledEventUpsert { + u.Set(scheduledevent.FieldFireAt, v) + return u +} + +// UpdateFireAt sets the "fire_at" field to the value that was provided on create. +func (u *ScheduledEventUpsert) UpdateFireAt() *ScheduledEventUpsert { + u.SetExcluded(scheduledevent.FieldFireAt) + return u +} + +// SetPayload sets the "payload" field. +func (u *ScheduledEventUpsert) SetPayload(v string) *ScheduledEventUpsert { + u.Set(scheduledevent.FieldPayload, v) + return u +} + +// UpdatePayload sets the "payload" field to the value that was provided on create. +func (u *ScheduledEventUpsert) UpdatePayload() *ScheduledEventUpsert { + u.SetExcluded(scheduledevent.FieldPayload) + return u +} + +// SetStatus sets the "status" field. +func (u *ScheduledEventUpsert) SetStatus(v string) *ScheduledEventUpsert { + u.Set(scheduledevent.FieldStatus, v) + return u +} + +// UpdateStatus sets the "status" field to the value that was provided on create. +func (u *ScheduledEventUpsert) UpdateStatus() *ScheduledEventUpsert { + u.SetExcluded(scheduledevent.FieldStatus) + return u +} + +// SetCreatedBy sets the "created_by" field. +func (u *ScheduledEventUpsert) SetCreatedBy(v string) *ScheduledEventUpsert { + u.Set(scheduledevent.FieldCreatedBy, v) + return u +} + +// UpdateCreatedBy sets the "created_by" field to the value that was provided on create. +func (u *ScheduledEventUpsert) UpdateCreatedBy() *ScheduledEventUpsert { + u.SetExcluded(scheduledevent.FieldCreatedBy) + return u +} + +// ClearCreatedBy clears the value of the "created_by" field. +func (u *ScheduledEventUpsert) ClearCreatedBy() *ScheduledEventUpsert { + u.SetNull(scheduledevent.FieldCreatedBy) + return u +} + +// SetFiredAt sets the "fired_at" field. +func (u *ScheduledEventUpsert) SetFiredAt(v time.Time) *ScheduledEventUpsert { + u.Set(scheduledevent.FieldFiredAt, v) + return u +} + +// UpdateFiredAt sets the "fired_at" field to the value that was provided on create. +func (u *ScheduledEventUpsert) UpdateFiredAt() *ScheduledEventUpsert { + u.SetExcluded(scheduledevent.FieldFiredAt) + return u +} + +// ClearFiredAt clears the value of the "fired_at" field. +func (u *ScheduledEventUpsert) ClearFiredAt() *ScheduledEventUpsert { + u.SetNull(scheduledevent.FieldFiredAt) + return u +} + +// SetError sets the "error" field. +func (u *ScheduledEventUpsert) SetError(v string) *ScheduledEventUpsert { + u.Set(scheduledevent.FieldError, v) + return u +} + +// UpdateError sets the "error" field to the value that was provided on create. +func (u *ScheduledEventUpsert) UpdateError() *ScheduledEventUpsert { + u.SetExcluded(scheduledevent.FieldError) + return u +} + +// ClearError clears the value of the "error" field. +func (u *ScheduledEventUpsert) ClearError() *ScheduledEventUpsert { + u.SetNull(scheduledevent.FieldError) + return u +} + +// SetScheduleID sets the "schedule_id" field. +func (u *ScheduledEventUpsert) SetScheduleID(v string) *ScheduledEventUpsert { + u.Set(scheduledevent.FieldScheduleID, v) + return u +} + +// UpdateScheduleID sets the "schedule_id" field to the value that was provided on create. +func (u *ScheduledEventUpsert) UpdateScheduleID() *ScheduledEventUpsert { + u.SetExcluded(scheduledevent.FieldScheduleID) + return u +} + +// ClearScheduleID clears the value of the "schedule_id" field. +func (u *ScheduledEventUpsert) ClearScheduleID() *ScheduledEventUpsert { + u.SetNull(scheduledevent.FieldScheduleID) + return u +} + +// UpdateNewValues updates the mutable fields using the new values that were set on create except the ID field. +// Using this option is equivalent to using: +// +// client.ScheduledEvent.Create(). +// OnConflict( +// sql.ResolveWithNewValues(), +// sql.ResolveWith(func(u *sql.UpdateSet) { +// u.SetIgnore(scheduledevent.FieldID) +// }), +// ). +// Exec(ctx) +func (u *ScheduledEventUpsertOne) UpdateNewValues() *ScheduledEventUpsertOne { + u.create.conflict = append(u.create.conflict, sql.ResolveWithNewValues()) + u.create.conflict = append(u.create.conflict, sql.ResolveWith(func(s *sql.UpdateSet) { + if _, exists := u.create.mutation.ID(); exists { + s.SetIgnore(scheduledevent.FieldID) + } + if _, exists := u.create.mutation.Created(); exists { + s.SetIgnore(scheduledevent.FieldCreated) + } + })) + return u +} + +// Ignore sets each column to itself in case of conflict. +// Using this option is equivalent to using: +// +// client.ScheduledEvent.Create(). +// OnConflict(sql.ResolveWithIgnore()). +// Exec(ctx) +func (u *ScheduledEventUpsertOne) Ignore() *ScheduledEventUpsertOne { + u.create.conflict = append(u.create.conflict, sql.ResolveWithIgnore()) + return u +} + +// DoNothing configures the conflict_action to `DO NOTHING`. +// Supported only by SQLite and PostgreSQL. +func (u *ScheduledEventUpsertOne) DoNothing() *ScheduledEventUpsertOne { + u.create.conflict = append(u.create.conflict, sql.DoNothing()) + return u +} + +// Update allows overriding fields `UPDATE` values. See the ScheduledEventCreate.OnConflict +// documentation for more info. +func (u *ScheduledEventUpsertOne) Update(set func(*ScheduledEventUpsert)) *ScheduledEventUpsertOne { + u.create.conflict = append(u.create.conflict, sql.ResolveWith(func(update *sql.UpdateSet) { + set(&ScheduledEventUpsert{UpdateSet: update}) + })) + return u +} + +// SetProjectID sets the "project_id" field. +func (u *ScheduledEventUpsertOne) SetProjectID(v uuid.UUID) *ScheduledEventUpsertOne { + return u.Update(func(s *ScheduledEventUpsert) { + s.SetProjectID(v) + }) +} + +// UpdateProjectID sets the "project_id" field to the value that was provided on create. +func (u *ScheduledEventUpsertOne) UpdateProjectID() *ScheduledEventUpsertOne { + return u.Update(func(s *ScheduledEventUpsert) { + s.UpdateProjectID() + }) +} + +// SetEventType sets the "event_type" field. +func (u *ScheduledEventUpsertOne) SetEventType(v string) *ScheduledEventUpsertOne { + return u.Update(func(s *ScheduledEventUpsert) { + s.SetEventType(v) + }) +} + +// UpdateEventType sets the "event_type" field to the value that was provided on create. +func (u *ScheduledEventUpsertOne) UpdateEventType() *ScheduledEventUpsertOne { + return u.Update(func(s *ScheduledEventUpsert) { + s.UpdateEventType() + }) +} + +// SetFireAt sets the "fire_at" field. +func (u *ScheduledEventUpsertOne) SetFireAt(v time.Time) *ScheduledEventUpsertOne { + return u.Update(func(s *ScheduledEventUpsert) { + s.SetFireAt(v) + }) +} + +// UpdateFireAt sets the "fire_at" field to the value that was provided on create. +func (u *ScheduledEventUpsertOne) UpdateFireAt() *ScheduledEventUpsertOne { + return u.Update(func(s *ScheduledEventUpsert) { + s.UpdateFireAt() + }) +} + +// SetPayload sets the "payload" field. +func (u *ScheduledEventUpsertOne) SetPayload(v string) *ScheduledEventUpsertOne { + return u.Update(func(s *ScheduledEventUpsert) { + s.SetPayload(v) + }) +} + +// UpdatePayload sets the "payload" field to the value that was provided on create. +func (u *ScheduledEventUpsertOne) UpdatePayload() *ScheduledEventUpsertOne { + return u.Update(func(s *ScheduledEventUpsert) { + s.UpdatePayload() + }) +} + +// SetStatus sets the "status" field. +func (u *ScheduledEventUpsertOne) SetStatus(v string) *ScheduledEventUpsertOne { + return u.Update(func(s *ScheduledEventUpsert) { + s.SetStatus(v) + }) +} + +// UpdateStatus sets the "status" field to the value that was provided on create. +func (u *ScheduledEventUpsertOne) UpdateStatus() *ScheduledEventUpsertOne { + return u.Update(func(s *ScheduledEventUpsert) { + s.UpdateStatus() + }) +} + +// SetCreatedBy sets the "created_by" field. +func (u *ScheduledEventUpsertOne) SetCreatedBy(v string) *ScheduledEventUpsertOne { + return u.Update(func(s *ScheduledEventUpsert) { + s.SetCreatedBy(v) + }) +} + +// UpdateCreatedBy sets the "created_by" field to the value that was provided on create. +func (u *ScheduledEventUpsertOne) UpdateCreatedBy() *ScheduledEventUpsertOne { + return u.Update(func(s *ScheduledEventUpsert) { + s.UpdateCreatedBy() + }) +} + +// ClearCreatedBy clears the value of the "created_by" field. +func (u *ScheduledEventUpsertOne) ClearCreatedBy() *ScheduledEventUpsertOne { + return u.Update(func(s *ScheduledEventUpsert) { + s.ClearCreatedBy() + }) +} + +// SetFiredAt sets the "fired_at" field. +func (u *ScheduledEventUpsertOne) SetFiredAt(v time.Time) *ScheduledEventUpsertOne { + return u.Update(func(s *ScheduledEventUpsert) { + s.SetFiredAt(v) + }) +} + +// UpdateFiredAt sets the "fired_at" field to the value that was provided on create. +func (u *ScheduledEventUpsertOne) UpdateFiredAt() *ScheduledEventUpsertOne { + return u.Update(func(s *ScheduledEventUpsert) { + s.UpdateFiredAt() + }) +} + +// ClearFiredAt clears the value of the "fired_at" field. +func (u *ScheduledEventUpsertOne) ClearFiredAt() *ScheduledEventUpsertOne { + return u.Update(func(s *ScheduledEventUpsert) { + s.ClearFiredAt() + }) +} + +// SetError sets the "error" field. +func (u *ScheduledEventUpsertOne) SetError(v string) *ScheduledEventUpsertOne { + return u.Update(func(s *ScheduledEventUpsert) { + s.SetError(v) + }) +} + +// UpdateError sets the "error" field to the value that was provided on create. +func (u *ScheduledEventUpsertOne) UpdateError() *ScheduledEventUpsertOne { + return u.Update(func(s *ScheduledEventUpsert) { + s.UpdateError() + }) +} + +// ClearError clears the value of the "error" field. +func (u *ScheduledEventUpsertOne) ClearError() *ScheduledEventUpsertOne { + return u.Update(func(s *ScheduledEventUpsert) { + s.ClearError() + }) +} + +// SetScheduleID sets the "schedule_id" field. +func (u *ScheduledEventUpsertOne) SetScheduleID(v string) *ScheduledEventUpsertOne { + return u.Update(func(s *ScheduledEventUpsert) { + s.SetScheduleID(v) + }) +} + +// UpdateScheduleID sets the "schedule_id" field to the value that was provided on create. +func (u *ScheduledEventUpsertOne) UpdateScheduleID() *ScheduledEventUpsertOne { + return u.Update(func(s *ScheduledEventUpsert) { + s.UpdateScheduleID() + }) +} + +// ClearScheduleID clears the value of the "schedule_id" field. +func (u *ScheduledEventUpsertOne) ClearScheduleID() *ScheduledEventUpsertOne { + return u.Update(func(s *ScheduledEventUpsert) { + s.ClearScheduleID() + }) +} + +// Exec executes the query. +func (u *ScheduledEventUpsertOne) Exec(ctx context.Context) error { + if len(u.create.conflict) == 0 { + return errors.New("ent: missing options for ScheduledEventCreate.OnConflict") + } + return u.create.Exec(ctx) +} + +// ExecX is like Exec, but panics if an error occurs. +func (u *ScheduledEventUpsertOne) ExecX(ctx context.Context) { + if err := u.create.Exec(ctx); err != nil { + panic(err) + } +} + +// Exec executes the UPSERT query and returns the inserted/updated ID. +func (u *ScheduledEventUpsertOne) ID(ctx context.Context) (id uuid.UUID, err error) { + if u.create.driver.Dialect() == dialect.MySQL { + // In case of "ON CONFLICT", there is no way to get back non-numeric ID + // fields from the database since MySQL does not support the RETURNING clause. + return id, errors.New("ent: ScheduledEventUpsertOne.ID is not supported by MySQL driver. Use ScheduledEventUpsertOne.Exec instead") + } + node, err := u.create.Save(ctx) + if err != nil { + return id, err + } + return node.ID, nil +} + +// IDX is like ID, but panics if an error occurs. +func (u *ScheduledEventUpsertOne) IDX(ctx context.Context) uuid.UUID { + id, err := u.ID(ctx) + if err != nil { + panic(err) + } + return id +} + +// ScheduledEventCreateBulk is the builder for creating many ScheduledEvent entities in bulk. +type ScheduledEventCreateBulk struct { + config + err error + builders []*ScheduledEventCreate + conflict []sql.ConflictOption +} + +// Save creates the ScheduledEvent entities in the database. +func (_c *ScheduledEventCreateBulk) Save(ctx context.Context) ([]*ScheduledEvent, error) { + if _c.err != nil { + return nil, _c.err + } + specs := make([]*sqlgraph.CreateSpec, len(_c.builders)) + nodes := make([]*ScheduledEvent, len(_c.builders)) + mutators := make([]Mutator, len(_c.builders)) + for i := range _c.builders { + func(i int, root context.Context) { + builder := _c.builders[i] + builder.defaults() + var mut Mutator = MutateFunc(func(ctx context.Context, m Mutation) (Value, error) { + mutation, ok := m.(*ScheduledEventMutation) + if !ok { + return nil, fmt.Errorf("unexpected mutation type %T", m) + } + if err := builder.check(); err != nil { + return nil, err + } + builder.mutation = mutation + var err error + nodes[i], specs[i] = builder.createSpec() + if i < len(mutators)-1 { + _, err = mutators[i+1].Mutate(root, _c.builders[i+1].mutation) + } else { + spec := &sqlgraph.BatchCreateSpec{Nodes: specs} + spec.OnConflict = _c.conflict + // Invoke the actual operation on the latest mutation in the chain. + if err = sqlgraph.BatchCreate(ctx, _c.driver, spec); err != nil { + if sqlgraph.IsConstraintError(err) { + err = &ConstraintError{msg: err.Error(), wrap: err} + } + } + } + if err != nil { + return nil, err + } + mutation.id = &nodes[i].ID + mutation.done = true + return nodes[i], nil + }) + for i := len(builder.hooks) - 1; i >= 0; i-- { + mut = builder.hooks[i](mut) + } + mutators[i] = mut + }(i, ctx) + } + if len(mutators) > 0 { + if _, err := mutators[0].Mutate(ctx, _c.builders[0].mutation); err != nil { + return nil, err + } + } + return nodes, nil +} + +// SaveX is like Save, but panics if an error occurs. +func (_c *ScheduledEventCreateBulk) SaveX(ctx context.Context) []*ScheduledEvent { + v, err := _c.Save(ctx) + if err != nil { + panic(err) + } + return v +} + +// Exec executes the query. +func (_c *ScheduledEventCreateBulk) Exec(ctx context.Context) error { + _, err := _c.Save(ctx) + return err +} + +// ExecX is like Exec, but panics if an error occurs. +func (_c *ScheduledEventCreateBulk) ExecX(ctx context.Context) { + if err := _c.Exec(ctx); err != nil { + panic(err) + } +} + +// OnConflict allows configuring the `ON CONFLICT` / `ON DUPLICATE KEY` clause +// of the `INSERT` statement. For example: +// +// client.ScheduledEvent.CreateBulk(builders...). +// OnConflict( +// // Update the row with the new values +// // the was proposed for insertion. +// sql.ResolveWithNewValues(), +// ). +// // Override some of the fields with custom +// // update values. +// Update(func(u *ent.ScheduledEventUpsert) { +// SetProjectID(v+v). +// }). +// Exec(ctx) +func (_c *ScheduledEventCreateBulk) OnConflict(opts ...sql.ConflictOption) *ScheduledEventUpsertBulk { + _c.conflict = opts + return &ScheduledEventUpsertBulk{ + create: _c, + } +} + +// OnConflictColumns calls `OnConflict` and configures the columns +// as conflict target. Using this option is equivalent to using: +// +// client.ScheduledEvent.Create(). +// OnConflict(sql.ConflictColumns(columns...)). +// Exec(ctx) +func (_c *ScheduledEventCreateBulk) OnConflictColumns(columns ...string) *ScheduledEventUpsertBulk { + _c.conflict = append(_c.conflict, sql.ConflictColumns(columns...)) + return &ScheduledEventUpsertBulk{ + create: _c, + } +} + +// ScheduledEventUpsertBulk is the builder for "upsert"-ing +// a bulk of ScheduledEvent nodes. +type ScheduledEventUpsertBulk struct { + create *ScheduledEventCreateBulk +} + +// UpdateNewValues updates the mutable fields using the new values that +// were set on create. Using this option is equivalent to using: +// +// client.ScheduledEvent.Create(). +// OnConflict( +// sql.ResolveWithNewValues(), +// sql.ResolveWith(func(u *sql.UpdateSet) { +// u.SetIgnore(scheduledevent.FieldID) +// }), +// ). +// Exec(ctx) +func (u *ScheduledEventUpsertBulk) UpdateNewValues() *ScheduledEventUpsertBulk { + u.create.conflict = append(u.create.conflict, sql.ResolveWithNewValues()) + u.create.conflict = append(u.create.conflict, sql.ResolveWith(func(s *sql.UpdateSet) { + for _, b := range u.create.builders { + if _, exists := b.mutation.ID(); exists { + s.SetIgnore(scheduledevent.FieldID) + } + if _, exists := b.mutation.Created(); exists { + s.SetIgnore(scheduledevent.FieldCreated) + } + } + })) + return u +} + +// Ignore sets each column to itself in case of conflict. +// Using this option is equivalent to using: +// +// client.ScheduledEvent.Create(). +// OnConflict(sql.ResolveWithIgnore()). +// Exec(ctx) +func (u *ScheduledEventUpsertBulk) Ignore() *ScheduledEventUpsertBulk { + u.create.conflict = append(u.create.conflict, sql.ResolveWithIgnore()) + return u +} + +// DoNothing configures the conflict_action to `DO NOTHING`. +// Supported only by SQLite and PostgreSQL. +func (u *ScheduledEventUpsertBulk) DoNothing() *ScheduledEventUpsertBulk { + u.create.conflict = append(u.create.conflict, sql.DoNothing()) + return u +} + +// Update allows overriding fields `UPDATE` values. See the ScheduledEventCreateBulk.OnConflict +// documentation for more info. +func (u *ScheduledEventUpsertBulk) Update(set func(*ScheduledEventUpsert)) *ScheduledEventUpsertBulk { + u.create.conflict = append(u.create.conflict, sql.ResolveWith(func(update *sql.UpdateSet) { + set(&ScheduledEventUpsert{UpdateSet: update}) + })) + return u +} + +// SetProjectID sets the "project_id" field. +func (u *ScheduledEventUpsertBulk) SetProjectID(v uuid.UUID) *ScheduledEventUpsertBulk { + return u.Update(func(s *ScheduledEventUpsert) { + s.SetProjectID(v) + }) +} + +// UpdateProjectID sets the "project_id" field to the value that was provided on create. +func (u *ScheduledEventUpsertBulk) UpdateProjectID() *ScheduledEventUpsertBulk { + return u.Update(func(s *ScheduledEventUpsert) { + s.UpdateProjectID() + }) +} + +// SetEventType sets the "event_type" field. +func (u *ScheduledEventUpsertBulk) SetEventType(v string) *ScheduledEventUpsertBulk { + return u.Update(func(s *ScheduledEventUpsert) { + s.SetEventType(v) + }) +} + +// UpdateEventType sets the "event_type" field to the value that was provided on create. +func (u *ScheduledEventUpsertBulk) UpdateEventType() *ScheduledEventUpsertBulk { + return u.Update(func(s *ScheduledEventUpsert) { + s.UpdateEventType() + }) +} + +// SetFireAt sets the "fire_at" field. +func (u *ScheduledEventUpsertBulk) SetFireAt(v time.Time) *ScheduledEventUpsertBulk { + return u.Update(func(s *ScheduledEventUpsert) { + s.SetFireAt(v) + }) +} + +// UpdateFireAt sets the "fire_at" field to the value that was provided on create. +func (u *ScheduledEventUpsertBulk) UpdateFireAt() *ScheduledEventUpsertBulk { + return u.Update(func(s *ScheduledEventUpsert) { + s.UpdateFireAt() + }) +} + +// SetPayload sets the "payload" field. +func (u *ScheduledEventUpsertBulk) SetPayload(v string) *ScheduledEventUpsertBulk { + return u.Update(func(s *ScheduledEventUpsert) { + s.SetPayload(v) + }) +} + +// UpdatePayload sets the "payload" field to the value that was provided on create. +func (u *ScheduledEventUpsertBulk) UpdatePayload() *ScheduledEventUpsertBulk { + return u.Update(func(s *ScheduledEventUpsert) { + s.UpdatePayload() + }) +} + +// SetStatus sets the "status" field. +func (u *ScheduledEventUpsertBulk) SetStatus(v string) *ScheduledEventUpsertBulk { + return u.Update(func(s *ScheduledEventUpsert) { + s.SetStatus(v) + }) +} + +// UpdateStatus sets the "status" field to the value that was provided on create. +func (u *ScheduledEventUpsertBulk) UpdateStatus() *ScheduledEventUpsertBulk { + return u.Update(func(s *ScheduledEventUpsert) { + s.UpdateStatus() + }) +} + +// SetCreatedBy sets the "created_by" field. +func (u *ScheduledEventUpsertBulk) SetCreatedBy(v string) *ScheduledEventUpsertBulk { + return u.Update(func(s *ScheduledEventUpsert) { + s.SetCreatedBy(v) + }) +} + +// UpdateCreatedBy sets the "created_by" field to the value that was provided on create. +func (u *ScheduledEventUpsertBulk) UpdateCreatedBy() *ScheduledEventUpsertBulk { + return u.Update(func(s *ScheduledEventUpsert) { + s.UpdateCreatedBy() + }) +} + +// ClearCreatedBy clears the value of the "created_by" field. +func (u *ScheduledEventUpsertBulk) ClearCreatedBy() *ScheduledEventUpsertBulk { + return u.Update(func(s *ScheduledEventUpsert) { + s.ClearCreatedBy() + }) +} + +// SetFiredAt sets the "fired_at" field. +func (u *ScheduledEventUpsertBulk) SetFiredAt(v time.Time) *ScheduledEventUpsertBulk { + return u.Update(func(s *ScheduledEventUpsert) { + s.SetFiredAt(v) + }) +} + +// UpdateFiredAt sets the "fired_at" field to the value that was provided on create. +func (u *ScheduledEventUpsertBulk) UpdateFiredAt() *ScheduledEventUpsertBulk { + return u.Update(func(s *ScheduledEventUpsert) { + s.UpdateFiredAt() + }) +} + +// ClearFiredAt clears the value of the "fired_at" field. +func (u *ScheduledEventUpsertBulk) ClearFiredAt() *ScheduledEventUpsertBulk { + return u.Update(func(s *ScheduledEventUpsert) { + s.ClearFiredAt() + }) +} + +// SetError sets the "error" field. +func (u *ScheduledEventUpsertBulk) SetError(v string) *ScheduledEventUpsertBulk { + return u.Update(func(s *ScheduledEventUpsert) { + s.SetError(v) + }) +} + +// UpdateError sets the "error" field to the value that was provided on create. +func (u *ScheduledEventUpsertBulk) UpdateError() *ScheduledEventUpsertBulk { + return u.Update(func(s *ScheduledEventUpsert) { + s.UpdateError() + }) +} + +// ClearError clears the value of the "error" field. +func (u *ScheduledEventUpsertBulk) ClearError() *ScheduledEventUpsertBulk { + return u.Update(func(s *ScheduledEventUpsert) { + s.ClearError() + }) +} + +// SetScheduleID sets the "schedule_id" field. +func (u *ScheduledEventUpsertBulk) SetScheduleID(v string) *ScheduledEventUpsertBulk { + return u.Update(func(s *ScheduledEventUpsert) { + s.SetScheduleID(v) + }) +} + +// UpdateScheduleID sets the "schedule_id" field to the value that was provided on create. +func (u *ScheduledEventUpsertBulk) UpdateScheduleID() *ScheduledEventUpsertBulk { + return u.Update(func(s *ScheduledEventUpsert) { + s.UpdateScheduleID() + }) +} + +// ClearScheduleID clears the value of the "schedule_id" field. +func (u *ScheduledEventUpsertBulk) ClearScheduleID() *ScheduledEventUpsertBulk { + return u.Update(func(s *ScheduledEventUpsert) { + s.ClearScheduleID() + }) +} + +// Exec executes the query. +func (u *ScheduledEventUpsertBulk) Exec(ctx context.Context) error { + if u.create.err != nil { + return u.create.err + } + for i, b := range u.create.builders { + if len(b.conflict) != 0 { + return fmt.Errorf("ent: OnConflict was set for builder %d. Set it on the ScheduledEventCreateBulk instead", i) + } + } + if len(u.create.conflict) == 0 { + return errors.New("ent: missing options for ScheduledEventCreateBulk.OnConflict") + } + return u.create.Exec(ctx) +} + +// ExecX is like Exec, but panics if an error occurs. +func (u *ScheduledEventUpsertBulk) ExecX(ctx context.Context) { + if err := u.create.Exec(ctx); err != nil { + panic(err) + } +} diff --git a/pkg/ent/scheduledevent_delete.go b/pkg/ent/scheduledevent_delete.go new file mode 100644 index 000000000..14bc697b5 --- /dev/null +++ b/pkg/ent/scheduledevent_delete.go @@ -0,0 +1,88 @@ +// Code generated by ent, DO NOT EDIT. + +package ent + +import ( + "context" + + "entgo.io/ent/dialect/sql" + "entgo.io/ent/dialect/sql/sqlgraph" + "entgo.io/ent/schema/field" + "github.com/GoogleCloudPlatform/scion/pkg/ent/predicate" + "github.com/GoogleCloudPlatform/scion/pkg/ent/scheduledevent" +) + +// ScheduledEventDelete is the builder for deleting a ScheduledEvent entity. +type ScheduledEventDelete struct { + config + hooks []Hook + mutation *ScheduledEventMutation +} + +// Where appends a list predicates to the ScheduledEventDelete builder. +func (_d *ScheduledEventDelete) Where(ps ...predicate.ScheduledEvent) *ScheduledEventDelete { + _d.mutation.Where(ps...) + return _d +} + +// Exec executes the deletion query and returns how many vertices were deleted. +func (_d *ScheduledEventDelete) Exec(ctx context.Context) (int, error) { + return withHooks(ctx, _d.sqlExec, _d.mutation, _d.hooks) +} + +// ExecX is like Exec, but panics if an error occurs. +func (_d *ScheduledEventDelete) ExecX(ctx context.Context) int { + n, err := _d.Exec(ctx) + if err != nil { + panic(err) + } + return n +} + +func (_d *ScheduledEventDelete) sqlExec(ctx context.Context) (int, error) { + _spec := sqlgraph.NewDeleteSpec(scheduledevent.Table, sqlgraph.NewFieldSpec(scheduledevent.FieldID, field.TypeUUID)) + if ps := _d.mutation.predicates; len(ps) > 0 { + _spec.Predicate = func(selector *sql.Selector) { + for i := range ps { + ps[i](selector) + } + } + } + affected, err := sqlgraph.DeleteNodes(ctx, _d.driver, _spec) + if err != nil && sqlgraph.IsConstraintError(err) { + err = &ConstraintError{msg: err.Error(), wrap: err} + } + _d.mutation.done = true + return affected, err +} + +// ScheduledEventDeleteOne is the builder for deleting a single ScheduledEvent entity. +type ScheduledEventDeleteOne struct { + _d *ScheduledEventDelete +} + +// Where appends a list predicates to the ScheduledEventDelete builder. +func (_d *ScheduledEventDeleteOne) Where(ps ...predicate.ScheduledEvent) *ScheduledEventDeleteOne { + _d._d.mutation.Where(ps...) + return _d +} + +// Exec executes the deletion query. +func (_d *ScheduledEventDeleteOne) Exec(ctx context.Context) error { + n, err := _d._d.Exec(ctx) + switch { + case err != nil: + return err + case n == 0: + return &NotFoundError{scheduledevent.Label} + default: + return nil + } +} + +// ExecX is like Exec, but panics if an error occurs. +func (_d *ScheduledEventDeleteOne) ExecX(ctx context.Context) { + if err := _d.Exec(ctx); err != nil { + panic(err) + } +} diff --git a/pkg/ent/scheduledevent_query.go b/pkg/ent/scheduledevent_query.go new file mode 100644 index 000000000..1d55bbb76 --- /dev/null +++ b/pkg/ent/scheduledevent_query.go @@ -0,0 +1,565 @@ +// Code generated by ent, DO NOT EDIT. + +package ent + +import ( + "context" + "fmt" + "math" + + "entgo.io/ent" + "entgo.io/ent/dialect" + "entgo.io/ent/dialect/sql" + "entgo.io/ent/dialect/sql/sqlgraph" + "entgo.io/ent/schema/field" + "github.com/GoogleCloudPlatform/scion/pkg/ent/predicate" + "github.com/GoogleCloudPlatform/scion/pkg/ent/scheduledevent" + "github.com/google/uuid" +) + +// ScheduledEventQuery is the builder for querying ScheduledEvent entities. +type ScheduledEventQuery struct { + config + ctx *QueryContext + order []scheduledevent.OrderOption + inters []Interceptor + predicates []predicate.ScheduledEvent + modifiers []func(*sql.Selector) + // intermediate query (i.e. traversal path). + sql *sql.Selector + path func(context.Context) (*sql.Selector, error) +} + +// Where adds a new predicate for the ScheduledEventQuery builder. +func (_q *ScheduledEventQuery) Where(ps ...predicate.ScheduledEvent) *ScheduledEventQuery { + _q.predicates = append(_q.predicates, ps...) + return _q +} + +// Limit the number of records to be returned by this query. +func (_q *ScheduledEventQuery) Limit(limit int) *ScheduledEventQuery { + _q.ctx.Limit = &limit + return _q +} + +// Offset to start from. +func (_q *ScheduledEventQuery) Offset(offset int) *ScheduledEventQuery { + _q.ctx.Offset = &offset + return _q +} + +// Unique configures the query builder to filter duplicate records on query. +// By default, unique is set to true, and can be disabled using this method. +func (_q *ScheduledEventQuery) Unique(unique bool) *ScheduledEventQuery { + _q.ctx.Unique = &unique + return _q +} + +// Order specifies how the records should be ordered. +func (_q *ScheduledEventQuery) Order(o ...scheduledevent.OrderOption) *ScheduledEventQuery { + _q.order = append(_q.order, o...) + return _q +} + +// First returns the first ScheduledEvent entity from the query. +// Returns a *NotFoundError when no ScheduledEvent was found. +func (_q *ScheduledEventQuery) First(ctx context.Context) (*ScheduledEvent, error) { + nodes, err := _q.Limit(1).All(setContextOp(ctx, _q.ctx, ent.OpQueryFirst)) + if err != nil { + return nil, err + } + if len(nodes) == 0 { + return nil, &NotFoundError{scheduledevent.Label} + } + return nodes[0], nil +} + +// FirstX is like First, but panics if an error occurs. +func (_q *ScheduledEventQuery) FirstX(ctx context.Context) *ScheduledEvent { + node, err := _q.First(ctx) + if err != nil && !IsNotFound(err) { + panic(err) + } + return node +} + +// FirstID returns the first ScheduledEvent ID from the query. +// Returns a *NotFoundError when no ScheduledEvent ID was found. +func (_q *ScheduledEventQuery) FirstID(ctx context.Context) (id uuid.UUID, err error) { + var ids []uuid.UUID + if ids, err = _q.Limit(1).IDs(setContextOp(ctx, _q.ctx, ent.OpQueryFirstID)); err != nil { + return + } + if len(ids) == 0 { + err = &NotFoundError{scheduledevent.Label} + return + } + return ids[0], nil +} + +// FirstIDX is like FirstID, but panics if an error occurs. +func (_q *ScheduledEventQuery) FirstIDX(ctx context.Context) uuid.UUID { + id, err := _q.FirstID(ctx) + if err != nil && !IsNotFound(err) { + panic(err) + } + return id +} + +// Only returns a single ScheduledEvent entity found by the query, ensuring it only returns one. +// Returns a *NotSingularError when more than one ScheduledEvent entity is found. +// Returns a *NotFoundError when no ScheduledEvent entities are found. +func (_q *ScheduledEventQuery) Only(ctx context.Context) (*ScheduledEvent, error) { + nodes, err := _q.Limit(2).All(setContextOp(ctx, _q.ctx, ent.OpQueryOnly)) + if err != nil { + return nil, err + } + switch len(nodes) { + case 1: + return nodes[0], nil + case 0: + return nil, &NotFoundError{scheduledevent.Label} + default: + return nil, &NotSingularError{scheduledevent.Label} + } +} + +// OnlyX is like Only, but panics if an error occurs. +func (_q *ScheduledEventQuery) OnlyX(ctx context.Context) *ScheduledEvent { + node, err := _q.Only(ctx) + if err != nil { + panic(err) + } + return node +} + +// OnlyID is like Only, but returns the only ScheduledEvent ID in the query. +// Returns a *NotSingularError when more than one ScheduledEvent ID is found. +// Returns a *NotFoundError when no entities are found. +func (_q *ScheduledEventQuery) OnlyID(ctx context.Context) (id uuid.UUID, err error) { + var ids []uuid.UUID + if ids, err = _q.Limit(2).IDs(setContextOp(ctx, _q.ctx, ent.OpQueryOnlyID)); err != nil { + return + } + switch len(ids) { + case 1: + id = ids[0] + case 0: + err = &NotFoundError{scheduledevent.Label} + default: + err = &NotSingularError{scheduledevent.Label} + } + return +} + +// OnlyIDX is like OnlyID, but panics if an error occurs. +func (_q *ScheduledEventQuery) OnlyIDX(ctx context.Context) uuid.UUID { + id, err := _q.OnlyID(ctx) + if err != nil { + panic(err) + } + return id +} + +// All executes the query and returns a list of ScheduledEvents. +func (_q *ScheduledEventQuery) All(ctx context.Context) ([]*ScheduledEvent, error) { + ctx = setContextOp(ctx, _q.ctx, ent.OpQueryAll) + if err := _q.prepareQuery(ctx); err != nil { + return nil, err + } + qr := querierAll[[]*ScheduledEvent, *ScheduledEventQuery]() + return withInterceptors[[]*ScheduledEvent](ctx, _q, qr, _q.inters) +} + +// AllX is like All, but panics if an error occurs. +func (_q *ScheduledEventQuery) AllX(ctx context.Context) []*ScheduledEvent { + nodes, err := _q.All(ctx) + if err != nil { + panic(err) + } + return nodes +} + +// IDs executes the query and returns a list of ScheduledEvent IDs. +func (_q *ScheduledEventQuery) IDs(ctx context.Context) (ids []uuid.UUID, err error) { + if _q.ctx.Unique == nil && _q.path != nil { + _q.Unique(true) + } + ctx = setContextOp(ctx, _q.ctx, ent.OpQueryIDs) + if err = _q.Select(scheduledevent.FieldID).Scan(ctx, &ids); err != nil { + return nil, err + } + return ids, nil +} + +// IDsX is like IDs, but panics if an error occurs. +func (_q *ScheduledEventQuery) IDsX(ctx context.Context) []uuid.UUID { + ids, err := _q.IDs(ctx) + if err != nil { + panic(err) + } + return ids +} + +// Count returns the count of the given query. +func (_q *ScheduledEventQuery) Count(ctx context.Context) (int, error) { + ctx = setContextOp(ctx, _q.ctx, ent.OpQueryCount) + if err := _q.prepareQuery(ctx); err != nil { + return 0, err + } + return withInterceptors[int](ctx, _q, querierCount[*ScheduledEventQuery](), _q.inters) +} + +// CountX is like Count, but panics if an error occurs. +func (_q *ScheduledEventQuery) CountX(ctx context.Context) int { + count, err := _q.Count(ctx) + if err != nil { + panic(err) + } + return count +} + +// Exist returns true if the query has elements in the graph. +func (_q *ScheduledEventQuery) Exist(ctx context.Context) (bool, error) { + ctx = setContextOp(ctx, _q.ctx, ent.OpQueryExist) + switch _, err := _q.FirstID(ctx); { + case IsNotFound(err): + return false, nil + case err != nil: + return false, fmt.Errorf("ent: check existence: %w", err) + default: + return true, nil + } +} + +// ExistX is like Exist, but panics if an error occurs. +func (_q *ScheduledEventQuery) ExistX(ctx context.Context) bool { + exist, err := _q.Exist(ctx) + if err != nil { + panic(err) + } + return exist +} + +// Clone returns a duplicate of the ScheduledEventQuery builder, including all associated steps. It can be +// used to prepare common query builders and use them differently after the clone is made. +func (_q *ScheduledEventQuery) Clone() *ScheduledEventQuery { + if _q == nil { + return nil + } + return &ScheduledEventQuery{ + config: _q.config, + ctx: _q.ctx.Clone(), + order: append([]scheduledevent.OrderOption{}, _q.order...), + inters: append([]Interceptor{}, _q.inters...), + predicates: append([]predicate.ScheduledEvent{}, _q.predicates...), + // clone intermediate query. + sql: _q.sql.Clone(), + path: _q.path, + } +} + +// GroupBy is used to group vertices by one or more fields/columns. +// It is often used with aggregate functions, like: count, max, mean, min, sum. +// +// Example: +// +// var v []struct { +// ProjectID uuid.UUID `json:"project_id,omitempty"` +// Count int `json:"count,omitempty"` +// } +// +// client.ScheduledEvent.Query(). +// GroupBy(scheduledevent.FieldProjectID). +// Aggregate(ent.Count()). +// Scan(ctx, &v) +func (_q *ScheduledEventQuery) GroupBy(field string, fields ...string) *ScheduledEventGroupBy { + _q.ctx.Fields = append([]string{field}, fields...) + grbuild := &ScheduledEventGroupBy{build: _q} + grbuild.flds = &_q.ctx.Fields + grbuild.label = scheduledevent.Label + grbuild.scan = grbuild.Scan + return grbuild +} + +// Select allows the selection one or more fields/columns for the given query, +// instead of selecting all fields in the entity. +// +// Example: +// +// var v []struct { +// ProjectID uuid.UUID `json:"project_id,omitempty"` +// } +// +// client.ScheduledEvent.Query(). +// Select(scheduledevent.FieldProjectID). +// Scan(ctx, &v) +func (_q *ScheduledEventQuery) Select(fields ...string) *ScheduledEventSelect { + _q.ctx.Fields = append(_q.ctx.Fields, fields...) + sbuild := &ScheduledEventSelect{ScheduledEventQuery: _q} + sbuild.label = scheduledevent.Label + sbuild.flds, sbuild.scan = &_q.ctx.Fields, sbuild.Scan + return sbuild +} + +// Aggregate returns a ScheduledEventSelect configured with the given aggregations. +func (_q *ScheduledEventQuery) Aggregate(fns ...AggregateFunc) *ScheduledEventSelect { + return _q.Select().Aggregate(fns...) +} + +func (_q *ScheduledEventQuery) prepareQuery(ctx context.Context) error { + for _, inter := range _q.inters { + if inter == nil { + return fmt.Errorf("ent: uninitialized interceptor (forgotten import ent/runtime?)") + } + if trv, ok := inter.(Traverser); ok { + if err := trv.Traverse(ctx, _q); err != nil { + return err + } + } + } + for _, f := range _q.ctx.Fields { + if !scheduledevent.ValidColumn(f) { + return &ValidationError{Name: f, err: fmt.Errorf("ent: invalid field %q for query", f)} + } + } + if _q.path != nil { + prev, err := _q.path(ctx) + if err != nil { + return err + } + _q.sql = prev + } + return nil +} + +func (_q *ScheduledEventQuery) sqlAll(ctx context.Context, hooks ...queryHook) ([]*ScheduledEvent, error) { + var ( + nodes = []*ScheduledEvent{} + _spec = _q.querySpec() + ) + _spec.ScanValues = func(columns []string) ([]any, error) { + return (*ScheduledEvent).scanValues(nil, columns) + } + _spec.Assign = func(columns []string, values []any) error { + node := &ScheduledEvent{config: _q.config} + nodes = append(nodes, node) + return node.assignValues(columns, values) + } + if len(_q.modifiers) > 0 { + _spec.Modifiers = _q.modifiers + } + for i := range hooks { + hooks[i](ctx, _spec) + } + if err := sqlgraph.QueryNodes(ctx, _q.driver, _spec); err != nil { + return nil, err + } + if len(nodes) == 0 { + return nodes, nil + } + return nodes, nil +} + +func (_q *ScheduledEventQuery) sqlCount(ctx context.Context) (int, error) { + _spec := _q.querySpec() + if len(_q.modifiers) > 0 { + _spec.Modifiers = _q.modifiers + } + _spec.Node.Columns = _q.ctx.Fields + if len(_q.ctx.Fields) > 0 { + _spec.Unique = _q.ctx.Unique != nil && *_q.ctx.Unique + } + return sqlgraph.CountNodes(ctx, _q.driver, _spec) +} + +func (_q *ScheduledEventQuery) querySpec() *sqlgraph.QuerySpec { + _spec := sqlgraph.NewQuerySpec(scheduledevent.Table, scheduledevent.Columns, sqlgraph.NewFieldSpec(scheduledevent.FieldID, field.TypeUUID)) + _spec.From = _q.sql + if unique := _q.ctx.Unique; unique != nil { + _spec.Unique = *unique + } else if _q.path != nil { + _spec.Unique = true + } + if fields := _q.ctx.Fields; len(fields) > 0 { + _spec.Node.Columns = make([]string, 0, len(fields)) + _spec.Node.Columns = append(_spec.Node.Columns, scheduledevent.FieldID) + for i := range fields { + if fields[i] != scheduledevent.FieldID { + _spec.Node.Columns = append(_spec.Node.Columns, fields[i]) + } + } + } + if ps := _q.predicates; len(ps) > 0 { + _spec.Predicate = func(selector *sql.Selector) { + for i := range ps { + ps[i](selector) + } + } + } + if limit := _q.ctx.Limit; limit != nil { + _spec.Limit = *limit + } + if offset := _q.ctx.Offset; offset != nil { + _spec.Offset = *offset + } + if ps := _q.order; len(ps) > 0 { + _spec.Order = func(selector *sql.Selector) { + for i := range ps { + ps[i](selector) + } + } + } + return _spec +} + +func (_q *ScheduledEventQuery) sqlQuery(ctx context.Context) *sql.Selector { + builder := sql.Dialect(_q.driver.Dialect()) + t1 := builder.Table(scheduledevent.Table) + columns := _q.ctx.Fields + if len(columns) == 0 { + columns = scheduledevent.Columns + } + selector := builder.Select(t1.Columns(columns...)...).From(t1) + if _q.sql != nil { + selector = _q.sql + selector.Select(selector.Columns(columns...)...) + } + if _q.ctx.Unique != nil && *_q.ctx.Unique { + selector.Distinct() + } + for _, m := range _q.modifiers { + m(selector) + } + for _, p := range _q.predicates { + p(selector) + } + for _, p := range _q.order { + p(selector) + } + if offset := _q.ctx.Offset; offset != nil { + // limit is mandatory for offset clause. We start + // with default value, and override it below if needed. + selector.Offset(*offset).Limit(math.MaxInt32) + } + if limit := _q.ctx.Limit; limit != nil { + selector.Limit(*limit) + } + return selector +} + +// ForUpdate locks the selected rows against concurrent updates, and prevent them from being +// updated, deleted or "selected ... for update" by other sessions, until the transaction is +// either committed or rolled-back. +func (_q *ScheduledEventQuery) ForUpdate(opts ...sql.LockOption) *ScheduledEventQuery { + if _q.driver.Dialect() == dialect.Postgres { + _q.Unique(false) + } + _q.modifiers = append(_q.modifiers, func(s *sql.Selector) { + s.ForUpdate(opts...) + }) + return _q +} + +// ForShare behaves similarly to ForUpdate, except that it acquires a shared mode lock +// on any rows that are read. Other sessions can read the rows, but cannot modify them +// until your transaction commits. +func (_q *ScheduledEventQuery) ForShare(opts ...sql.LockOption) *ScheduledEventQuery { + if _q.driver.Dialect() == dialect.Postgres { + _q.Unique(false) + } + _q.modifiers = append(_q.modifiers, func(s *sql.Selector) { + s.ForShare(opts...) + }) + return _q +} + +// ScheduledEventGroupBy is the group-by builder for ScheduledEvent entities. +type ScheduledEventGroupBy struct { + selector + build *ScheduledEventQuery +} + +// Aggregate adds the given aggregation functions to the group-by query. +func (_g *ScheduledEventGroupBy) Aggregate(fns ...AggregateFunc) *ScheduledEventGroupBy { + _g.fns = append(_g.fns, fns...) + return _g +} + +// Scan applies the selector query and scans the result into the given value. +func (_g *ScheduledEventGroupBy) Scan(ctx context.Context, v any) error { + ctx = setContextOp(ctx, _g.build.ctx, ent.OpQueryGroupBy) + if err := _g.build.prepareQuery(ctx); err != nil { + return err + } + return scanWithInterceptors[*ScheduledEventQuery, *ScheduledEventGroupBy](ctx, _g.build, _g, _g.build.inters, v) +} + +func (_g *ScheduledEventGroupBy) sqlScan(ctx context.Context, root *ScheduledEventQuery, v any) error { + selector := root.sqlQuery(ctx).Select() + aggregation := make([]string, 0, len(_g.fns)) + for _, fn := range _g.fns { + aggregation = append(aggregation, fn(selector)) + } + if len(selector.SelectedColumns()) == 0 { + columns := make([]string, 0, len(*_g.flds)+len(_g.fns)) + for _, f := range *_g.flds { + columns = append(columns, selector.C(f)) + } + columns = append(columns, aggregation...) + selector.Select(columns...) + } + selector.GroupBy(selector.Columns(*_g.flds...)...) + if err := selector.Err(); err != nil { + return err + } + rows := &sql.Rows{} + query, args := selector.Query() + if err := _g.build.driver.Query(ctx, query, args, rows); err != nil { + return err + } + defer rows.Close() + return sql.ScanSlice(rows, v) +} + +// ScheduledEventSelect is the builder for selecting fields of ScheduledEvent entities. +type ScheduledEventSelect struct { + *ScheduledEventQuery + selector +} + +// Aggregate adds the given aggregation functions to the selector query. +func (_s *ScheduledEventSelect) Aggregate(fns ...AggregateFunc) *ScheduledEventSelect { + _s.fns = append(_s.fns, fns...) + return _s +} + +// Scan applies the selector query and scans the result into the given value. +func (_s *ScheduledEventSelect) Scan(ctx context.Context, v any) error { + ctx = setContextOp(ctx, _s.ctx, ent.OpQuerySelect) + if err := _s.prepareQuery(ctx); err != nil { + return err + } + return scanWithInterceptors[*ScheduledEventQuery, *ScheduledEventSelect](ctx, _s.ScheduledEventQuery, _s, _s.inters, v) +} + +func (_s *ScheduledEventSelect) sqlScan(ctx context.Context, root *ScheduledEventQuery, v any) error { + selector := root.sqlQuery(ctx) + aggregation := make([]string, 0, len(_s.fns)) + for _, fn := range _s.fns { + aggregation = append(aggregation, fn(selector)) + } + switch n := len(*_s.selector.flds); { + case n == 0 && len(aggregation) > 0: + selector.Select(aggregation...) + case n != 0 && len(aggregation) > 0: + selector.AppendSelect(aggregation...) + } + rows := &sql.Rows{} + query, args := selector.Query() + if err := _s.driver.Query(ctx, query, args, rows); err != nil { + return err + } + defer rows.Close() + return sql.ScanSlice(rows, v) +} diff --git a/pkg/ent/scheduledevent_update.go b/pkg/ent/scheduledevent_update.go new file mode 100644 index 000000000..724d055e3 --- /dev/null +++ b/pkg/ent/scheduledevent_update.go @@ -0,0 +1,591 @@ +// Code generated by ent, DO NOT EDIT. + +package ent + +import ( + "context" + "errors" + "fmt" + "time" + + "entgo.io/ent/dialect/sql" + "entgo.io/ent/dialect/sql/sqlgraph" + "entgo.io/ent/schema/field" + "github.com/GoogleCloudPlatform/scion/pkg/ent/predicate" + "github.com/GoogleCloudPlatform/scion/pkg/ent/scheduledevent" + "github.com/google/uuid" +) + +// ScheduledEventUpdate is the builder for updating ScheduledEvent entities. +type ScheduledEventUpdate struct { + config + hooks []Hook + mutation *ScheduledEventMutation +} + +// Where appends a list predicates to the ScheduledEventUpdate builder. +func (_u *ScheduledEventUpdate) Where(ps ...predicate.ScheduledEvent) *ScheduledEventUpdate { + _u.mutation.Where(ps...) + return _u +} + +// SetProjectID sets the "project_id" field. +func (_u *ScheduledEventUpdate) SetProjectID(v uuid.UUID) *ScheduledEventUpdate { + _u.mutation.SetProjectID(v) + return _u +} + +// SetNillableProjectID sets the "project_id" field if the given value is not nil. +func (_u *ScheduledEventUpdate) SetNillableProjectID(v *uuid.UUID) *ScheduledEventUpdate { + if v != nil { + _u.SetProjectID(*v) + } + return _u +} + +// SetEventType sets the "event_type" field. +func (_u *ScheduledEventUpdate) SetEventType(v string) *ScheduledEventUpdate { + _u.mutation.SetEventType(v) + return _u +} + +// SetNillableEventType sets the "event_type" field if the given value is not nil. +func (_u *ScheduledEventUpdate) SetNillableEventType(v *string) *ScheduledEventUpdate { + if v != nil { + _u.SetEventType(*v) + } + return _u +} + +// SetFireAt sets the "fire_at" field. +func (_u *ScheduledEventUpdate) SetFireAt(v time.Time) *ScheduledEventUpdate { + _u.mutation.SetFireAt(v) + return _u +} + +// SetNillableFireAt sets the "fire_at" field if the given value is not nil. +func (_u *ScheduledEventUpdate) SetNillableFireAt(v *time.Time) *ScheduledEventUpdate { + if v != nil { + _u.SetFireAt(*v) + } + return _u +} + +// SetPayload sets the "payload" field. +func (_u *ScheduledEventUpdate) SetPayload(v string) *ScheduledEventUpdate { + _u.mutation.SetPayload(v) + return _u +} + +// SetNillablePayload sets the "payload" field if the given value is not nil. +func (_u *ScheduledEventUpdate) SetNillablePayload(v *string) *ScheduledEventUpdate { + if v != nil { + _u.SetPayload(*v) + } + return _u +} + +// SetStatus sets the "status" field. +func (_u *ScheduledEventUpdate) SetStatus(v string) *ScheduledEventUpdate { + _u.mutation.SetStatus(v) + return _u +} + +// SetNillableStatus sets the "status" field if the given value is not nil. +func (_u *ScheduledEventUpdate) SetNillableStatus(v *string) *ScheduledEventUpdate { + if v != nil { + _u.SetStatus(*v) + } + return _u +} + +// SetCreatedBy sets the "created_by" field. +func (_u *ScheduledEventUpdate) SetCreatedBy(v string) *ScheduledEventUpdate { + _u.mutation.SetCreatedBy(v) + return _u +} + +// SetNillableCreatedBy sets the "created_by" field if the given value is not nil. +func (_u *ScheduledEventUpdate) SetNillableCreatedBy(v *string) *ScheduledEventUpdate { + if v != nil { + _u.SetCreatedBy(*v) + } + return _u +} + +// ClearCreatedBy clears the value of the "created_by" field. +func (_u *ScheduledEventUpdate) ClearCreatedBy() *ScheduledEventUpdate { + _u.mutation.ClearCreatedBy() + return _u +} + +// SetFiredAt sets the "fired_at" field. +func (_u *ScheduledEventUpdate) SetFiredAt(v time.Time) *ScheduledEventUpdate { + _u.mutation.SetFiredAt(v) + return _u +} + +// SetNillableFiredAt sets the "fired_at" field if the given value is not nil. +func (_u *ScheduledEventUpdate) SetNillableFiredAt(v *time.Time) *ScheduledEventUpdate { + if v != nil { + _u.SetFiredAt(*v) + } + return _u +} + +// ClearFiredAt clears the value of the "fired_at" field. +func (_u *ScheduledEventUpdate) ClearFiredAt() *ScheduledEventUpdate { + _u.mutation.ClearFiredAt() + return _u +} + +// SetError sets the "error" field. +func (_u *ScheduledEventUpdate) SetError(v string) *ScheduledEventUpdate { + _u.mutation.SetError(v) + return _u +} + +// SetNillableError sets the "error" field if the given value is not nil. +func (_u *ScheduledEventUpdate) SetNillableError(v *string) *ScheduledEventUpdate { + if v != nil { + _u.SetError(*v) + } + return _u +} + +// ClearError clears the value of the "error" field. +func (_u *ScheduledEventUpdate) ClearError() *ScheduledEventUpdate { + _u.mutation.ClearError() + return _u +} + +// SetScheduleID sets the "schedule_id" field. +func (_u *ScheduledEventUpdate) SetScheduleID(v string) *ScheduledEventUpdate { + _u.mutation.SetScheduleID(v) + return _u +} + +// SetNillableScheduleID sets the "schedule_id" field if the given value is not nil. +func (_u *ScheduledEventUpdate) SetNillableScheduleID(v *string) *ScheduledEventUpdate { + if v != nil { + _u.SetScheduleID(*v) + } + return _u +} + +// ClearScheduleID clears the value of the "schedule_id" field. +func (_u *ScheduledEventUpdate) ClearScheduleID() *ScheduledEventUpdate { + _u.mutation.ClearScheduleID() + return _u +} + +// Mutation returns the ScheduledEventMutation object of the builder. +func (_u *ScheduledEventUpdate) Mutation() *ScheduledEventMutation { + return _u.mutation +} + +// Save executes the query and returns the number of nodes affected by the update operation. +func (_u *ScheduledEventUpdate) Save(ctx context.Context) (int, error) { + return withHooks(ctx, _u.sqlSave, _u.mutation, _u.hooks) +} + +// SaveX is like Save, but panics if an error occurs. +func (_u *ScheduledEventUpdate) SaveX(ctx context.Context) int { + affected, err := _u.Save(ctx) + if err != nil { + panic(err) + } + return affected +} + +// Exec executes the query. +func (_u *ScheduledEventUpdate) Exec(ctx context.Context) error { + _, err := _u.Save(ctx) + return err +} + +// ExecX is like Exec, but panics if an error occurs. +func (_u *ScheduledEventUpdate) ExecX(ctx context.Context) { + if err := _u.Exec(ctx); err != nil { + panic(err) + } +} + +// check runs all checks and user-defined validators on the builder. +func (_u *ScheduledEventUpdate) check() error { + if v, ok := _u.mutation.EventType(); ok { + if err := scheduledevent.EventTypeValidator(v); err != nil { + return &ValidationError{Name: "event_type", err: fmt.Errorf(`ent: validator failed for field "ScheduledEvent.event_type": %w`, err)} + } + } + if v, ok := _u.mutation.Payload(); ok { + if err := scheduledevent.PayloadValidator(v); err != nil { + return &ValidationError{Name: "payload", err: fmt.Errorf(`ent: validator failed for field "ScheduledEvent.payload": %w`, err)} + } + } + return nil +} + +func (_u *ScheduledEventUpdate) sqlSave(ctx context.Context) (_node int, err error) { + if err := _u.check(); err != nil { + return _node, err + } + _spec := sqlgraph.NewUpdateSpec(scheduledevent.Table, scheduledevent.Columns, sqlgraph.NewFieldSpec(scheduledevent.FieldID, field.TypeUUID)) + if ps := _u.mutation.predicates; len(ps) > 0 { + _spec.Predicate = func(selector *sql.Selector) { + for i := range ps { + ps[i](selector) + } + } + } + if value, ok := _u.mutation.ProjectID(); ok { + _spec.SetField(scheduledevent.FieldProjectID, field.TypeUUID, value) + } + if value, ok := _u.mutation.EventType(); ok { + _spec.SetField(scheduledevent.FieldEventType, field.TypeString, value) + } + if value, ok := _u.mutation.FireAt(); ok { + _spec.SetField(scheduledevent.FieldFireAt, field.TypeTime, value) + } + if value, ok := _u.mutation.Payload(); ok { + _spec.SetField(scheduledevent.FieldPayload, field.TypeString, value) + } + if value, ok := _u.mutation.Status(); ok { + _spec.SetField(scheduledevent.FieldStatus, field.TypeString, value) + } + if value, ok := _u.mutation.CreatedBy(); ok { + _spec.SetField(scheduledevent.FieldCreatedBy, field.TypeString, value) + } + if _u.mutation.CreatedByCleared() { + _spec.ClearField(scheduledevent.FieldCreatedBy, field.TypeString) + } + if value, ok := _u.mutation.FiredAt(); ok { + _spec.SetField(scheduledevent.FieldFiredAt, field.TypeTime, value) + } + if _u.mutation.FiredAtCleared() { + _spec.ClearField(scheduledevent.FieldFiredAt, field.TypeTime) + } + if value, ok := _u.mutation.Error(); ok { + _spec.SetField(scheduledevent.FieldError, field.TypeString, value) + } + if _u.mutation.ErrorCleared() { + _spec.ClearField(scheduledevent.FieldError, field.TypeString) + } + if value, ok := _u.mutation.ScheduleID(); ok { + _spec.SetField(scheduledevent.FieldScheduleID, field.TypeString, value) + } + if _u.mutation.ScheduleIDCleared() { + _spec.ClearField(scheduledevent.FieldScheduleID, field.TypeString) + } + if _node, err = sqlgraph.UpdateNodes(ctx, _u.driver, _spec); err != nil { + if _, ok := err.(*sqlgraph.NotFoundError); ok { + err = &NotFoundError{scheduledevent.Label} + } else if sqlgraph.IsConstraintError(err) { + err = &ConstraintError{msg: err.Error(), wrap: err} + } + return 0, err + } + _u.mutation.done = true + return _node, nil +} + +// ScheduledEventUpdateOne is the builder for updating a single ScheduledEvent entity. +type ScheduledEventUpdateOne struct { + config + fields []string + hooks []Hook + mutation *ScheduledEventMutation +} + +// SetProjectID sets the "project_id" field. +func (_u *ScheduledEventUpdateOne) SetProjectID(v uuid.UUID) *ScheduledEventUpdateOne { + _u.mutation.SetProjectID(v) + return _u +} + +// SetNillableProjectID sets the "project_id" field if the given value is not nil. +func (_u *ScheduledEventUpdateOne) SetNillableProjectID(v *uuid.UUID) *ScheduledEventUpdateOne { + if v != nil { + _u.SetProjectID(*v) + } + return _u +} + +// SetEventType sets the "event_type" field. +func (_u *ScheduledEventUpdateOne) SetEventType(v string) *ScheduledEventUpdateOne { + _u.mutation.SetEventType(v) + return _u +} + +// SetNillableEventType sets the "event_type" field if the given value is not nil. +func (_u *ScheduledEventUpdateOne) SetNillableEventType(v *string) *ScheduledEventUpdateOne { + if v != nil { + _u.SetEventType(*v) + } + return _u +} + +// SetFireAt sets the "fire_at" field. +func (_u *ScheduledEventUpdateOne) SetFireAt(v time.Time) *ScheduledEventUpdateOne { + _u.mutation.SetFireAt(v) + return _u +} + +// SetNillableFireAt sets the "fire_at" field if the given value is not nil. +func (_u *ScheduledEventUpdateOne) SetNillableFireAt(v *time.Time) *ScheduledEventUpdateOne { + if v != nil { + _u.SetFireAt(*v) + } + return _u +} + +// SetPayload sets the "payload" field. +func (_u *ScheduledEventUpdateOne) SetPayload(v string) *ScheduledEventUpdateOne { + _u.mutation.SetPayload(v) + return _u +} + +// SetNillablePayload sets the "payload" field if the given value is not nil. +func (_u *ScheduledEventUpdateOne) SetNillablePayload(v *string) *ScheduledEventUpdateOne { + if v != nil { + _u.SetPayload(*v) + } + return _u +} + +// SetStatus sets the "status" field. +func (_u *ScheduledEventUpdateOne) SetStatus(v string) *ScheduledEventUpdateOne { + _u.mutation.SetStatus(v) + return _u +} + +// SetNillableStatus sets the "status" field if the given value is not nil. +func (_u *ScheduledEventUpdateOne) SetNillableStatus(v *string) *ScheduledEventUpdateOne { + if v != nil { + _u.SetStatus(*v) + } + return _u +} + +// SetCreatedBy sets the "created_by" field. +func (_u *ScheduledEventUpdateOne) SetCreatedBy(v string) *ScheduledEventUpdateOne { + _u.mutation.SetCreatedBy(v) + return _u +} + +// SetNillableCreatedBy sets the "created_by" field if the given value is not nil. +func (_u *ScheduledEventUpdateOne) SetNillableCreatedBy(v *string) *ScheduledEventUpdateOne { + if v != nil { + _u.SetCreatedBy(*v) + } + return _u +} + +// ClearCreatedBy clears the value of the "created_by" field. +func (_u *ScheduledEventUpdateOne) ClearCreatedBy() *ScheduledEventUpdateOne { + _u.mutation.ClearCreatedBy() + return _u +} + +// SetFiredAt sets the "fired_at" field. +func (_u *ScheduledEventUpdateOne) SetFiredAt(v time.Time) *ScheduledEventUpdateOne { + _u.mutation.SetFiredAt(v) + return _u +} + +// SetNillableFiredAt sets the "fired_at" field if the given value is not nil. +func (_u *ScheduledEventUpdateOne) SetNillableFiredAt(v *time.Time) *ScheduledEventUpdateOne { + if v != nil { + _u.SetFiredAt(*v) + } + return _u +} + +// ClearFiredAt clears the value of the "fired_at" field. +func (_u *ScheduledEventUpdateOne) ClearFiredAt() *ScheduledEventUpdateOne { + _u.mutation.ClearFiredAt() + return _u +} + +// SetError sets the "error" field. +func (_u *ScheduledEventUpdateOne) SetError(v string) *ScheduledEventUpdateOne { + _u.mutation.SetError(v) + return _u +} + +// SetNillableError sets the "error" field if the given value is not nil. +func (_u *ScheduledEventUpdateOne) SetNillableError(v *string) *ScheduledEventUpdateOne { + if v != nil { + _u.SetError(*v) + } + return _u +} + +// ClearError clears the value of the "error" field. +func (_u *ScheduledEventUpdateOne) ClearError() *ScheduledEventUpdateOne { + _u.mutation.ClearError() + return _u +} + +// SetScheduleID sets the "schedule_id" field. +func (_u *ScheduledEventUpdateOne) SetScheduleID(v string) *ScheduledEventUpdateOne { + _u.mutation.SetScheduleID(v) + return _u +} + +// SetNillableScheduleID sets the "schedule_id" field if the given value is not nil. +func (_u *ScheduledEventUpdateOne) SetNillableScheduleID(v *string) *ScheduledEventUpdateOne { + if v != nil { + _u.SetScheduleID(*v) + } + return _u +} + +// ClearScheduleID clears the value of the "schedule_id" field. +func (_u *ScheduledEventUpdateOne) ClearScheduleID() *ScheduledEventUpdateOne { + _u.mutation.ClearScheduleID() + return _u +} + +// Mutation returns the ScheduledEventMutation object of the builder. +func (_u *ScheduledEventUpdateOne) Mutation() *ScheduledEventMutation { + return _u.mutation +} + +// Where appends a list predicates to the ScheduledEventUpdate builder. +func (_u *ScheduledEventUpdateOne) Where(ps ...predicate.ScheduledEvent) *ScheduledEventUpdateOne { + _u.mutation.Where(ps...) + return _u +} + +// Select allows selecting one or more fields (columns) of the returned entity. +// The default is selecting all fields defined in the entity schema. +func (_u *ScheduledEventUpdateOne) Select(field string, fields ...string) *ScheduledEventUpdateOne { + _u.fields = append([]string{field}, fields...) + return _u +} + +// Save executes the query and returns the updated ScheduledEvent entity. +func (_u *ScheduledEventUpdateOne) Save(ctx context.Context) (*ScheduledEvent, error) { + return withHooks(ctx, _u.sqlSave, _u.mutation, _u.hooks) +} + +// SaveX is like Save, but panics if an error occurs. +func (_u *ScheduledEventUpdateOne) SaveX(ctx context.Context) *ScheduledEvent { + node, err := _u.Save(ctx) + if err != nil { + panic(err) + } + return node +} + +// Exec executes the query on the entity. +func (_u *ScheduledEventUpdateOne) Exec(ctx context.Context) error { + _, err := _u.Save(ctx) + return err +} + +// ExecX is like Exec, but panics if an error occurs. +func (_u *ScheduledEventUpdateOne) ExecX(ctx context.Context) { + if err := _u.Exec(ctx); err != nil { + panic(err) + } +} + +// check runs all checks and user-defined validators on the builder. +func (_u *ScheduledEventUpdateOne) check() error { + if v, ok := _u.mutation.EventType(); ok { + if err := scheduledevent.EventTypeValidator(v); err != nil { + return &ValidationError{Name: "event_type", err: fmt.Errorf(`ent: validator failed for field "ScheduledEvent.event_type": %w`, err)} + } + } + if v, ok := _u.mutation.Payload(); ok { + if err := scheduledevent.PayloadValidator(v); err != nil { + return &ValidationError{Name: "payload", err: fmt.Errorf(`ent: validator failed for field "ScheduledEvent.payload": %w`, err)} + } + } + return nil +} + +func (_u *ScheduledEventUpdateOne) sqlSave(ctx context.Context) (_node *ScheduledEvent, err error) { + if err := _u.check(); err != nil { + return _node, err + } + _spec := sqlgraph.NewUpdateSpec(scheduledevent.Table, scheduledevent.Columns, sqlgraph.NewFieldSpec(scheduledevent.FieldID, field.TypeUUID)) + id, ok := _u.mutation.ID() + if !ok { + return nil, &ValidationError{Name: "id", err: errors.New(`ent: missing "ScheduledEvent.id" for update`)} + } + _spec.Node.ID.Value = id + if fields := _u.fields; len(fields) > 0 { + _spec.Node.Columns = make([]string, 0, len(fields)) + _spec.Node.Columns = append(_spec.Node.Columns, scheduledevent.FieldID) + for _, f := range fields { + if !scheduledevent.ValidColumn(f) { + return nil, &ValidationError{Name: f, err: fmt.Errorf("ent: invalid field %q for query", f)} + } + if f != scheduledevent.FieldID { + _spec.Node.Columns = append(_spec.Node.Columns, f) + } + } + } + if ps := _u.mutation.predicates; len(ps) > 0 { + _spec.Predicate = func(selector *sql.Selector) { + for i := range ps { + ps[i](selector) + } + } + } + if value, ok := _u.mutation.ProjectID(); ok { + _spec.SetField(scheduledevent.FieldProjectID, field.TypeUUID, value) + } + if value, ok := _u.mutation.EventType(); ok { + _spec.SetField(scheduledevent.FieldEventType, field.TypeString, value) + } + if value, ok := _u.mutation.FireAt(); ok { + _spec.SetField(scheduledevent.FieldFireAt, field.TypeTime, value) + } + if value, ok := _u.mutation.Payload(); ok { + _spec.SetField(scheduledevent.FieldPayload, field.TypeString, value) + } + if value, ok := _u.mutation.Status(); ok { + _spec.SetField(scheduledevent.FieldStatus, field.TypeString, value) + } + if value, ok := _u.mutation.CreatedBy(); ok { + _spec.SetField(scheduledevent.FieldCreatedBy, field.TypeString, value) + } + if _u.mutation.CreatedByCleared() { + _spec.ClearField(scheduledevent.FieldCreatedBy, field.TypeString) + } + if value, ok := _u.mutation.FiredAt(); ok { + _spec.SetField(scheduledevent.FieldFiredAt, field.TypeTime, value) + } + if _u.mutation.FiredAtCleared() { + _spec.ClearField(scheduledevent.FieldFiredAt, field.TypeTime) + } + if value, ok := _u.mutation.Error(); ok { + _spec.SetField(scheduledevent.FieldError, field.TypeString, value) + } + if _u.mutation.ErrorCleared() { + _spec.ClearField(scheduledevent.FieldError, field.TypeString) + } + if value, ok := _u.mutation.ScheduleID(); ok { + _spec.SetField(scheduledevent.FieldScheduleID, field.TypeString, value) + } + if _u.mutation.ScheduleIDCleared() { + _spec.ClearField(scheduledevent.FieldScheduleID, field.TypeString) + } + _node = &ScheduledEvent{config: _u.config} + _spec.Assign = _node.assignValues + _spec.ScanValues = _node.scanValues + if err = sqlgraph.UpdateNode(ctx, _u.driver, _spec); err != nil { + if _, ok := err.(*sqlgraph.NotFoundError); ok { + err = &NotFoundError{scheduledevent.Label} + } else if sqlgraph.IsConstraintError(err) { + err = &ConstraintError{msg: err.Error(), wrap: err} + } + return nil, err + } + _u.mutation.done = true + return _node, nil +} diff --git a/pkg/ent/schema/agent.go b/pkg/ent/schema/agent.go index b683d83e4..884ffea3b 100644 --- a/pkg/ent/schema/agent.go +++ b/pkg/ent/schema/agent.go @@ -25,9 +25,12 @@ import ( ) // Agent holds the schema definition for the Agent entity. -// Only principal-relevant fields are included; operational fields -// (ContainerStatus, RuntimeState, etc.) will be added when the -// agent entity is fully migrated to Ent. +// +// The agent entity carries both the principal-relevant fields used by the +// authorization layer (created_by, owner_id, delegation_enabled, visibility) +// and the full set of operational fields required to back store.Agent through +// the Ent adapter (P2-port-agent). Together they give the Ent-backed agent +// store parity with the former raw-SQL store implementation. type Agent struct { ent.Schema } @@ -49,6 +52,12 @@ func (Agent) Fields() []ent.Field { field.Enum("status"). Values("created", "provisioning", "cloning", "starting", "running", "suspended", "stopping", "stopped", "error"). Default("created"), + // created_by and owner_id are polymorphic *principal* references: the + // creator/owner may be a user OR another agent (an agent that spawns a + // sub-agent records its own ID here). They therefore carry no foreign-key + // edge to the users table — a User-typed FK rejected every agent-created + // sub-agent with a foreign-key violation. Consumers that need the user + // behind the ID must look it up by ID and tolerate "no such user". field.UUID("created_by", uuid.UUID{}). Optional(). Nillable(), @@ -59,12 +68,90 @@ func (Agent) Fields() []ent.Field { Default(false), field.String("visibility"). Default("private"), + + // --- Metadata (stored as JSON) --- + field.JSON("labels", map[string]string{}). + Optional(), + field.JSON("annotations", map[string]string{}). + Optional(), + + // --- Runtime status --- + field.String("phase"). + Optional(), + field.String("activity"). + Optional(), + field.String("tool_name"). + Optional(), + field.String("connection_state"). + Optional(), + field.String("container_status"). + Optional(), + field.String("runtime_state"). + Optional(), + field.String("stalled_from_activity"). + Optional(), + + // --- Limits tracking --- + field.Int("current_turns"). + Default(0), + field.Int("current_model_calls"). + Default(0), + + // --- Runtime configuration --- + field.String("image"). + Optional(), + field.Bool("detached"). + Default(false), + field.String("runtime"). + Optional(), + field.String("runtime_broker_id"). + Optional(), + field.Bool("web_pty_enabled"). + Default(false), + field.String("task_summary"). + Optional(), + field.String("message"). + Optional(), + + // applied_config is the agent's resolved configuration, persisted as a + // JSON document (store.AgentAppliedConfig). Stored as text to keep the + // Ent schema decoupled from the store package's struct definition. + field.Text("applied_config"). + Optional(), + + // ancestry is the ordered chain of ancestor principal IDs used for + // transitive access control. Stored as a JSON array so the dialect-aware + // json_each / json_array_elements_text membership filter can be applied. + field.JSON("ancestry", []string{}). + Optional(), + + // --- Timestamps --- field.Time("created"). Default(time.Now). Immutable(), field.Time("updated"). Default(time.Now). UpdateDefault(time.Now), + field.Time("last_seen"). + Optional(). + Nillable(), + field.Time("last_activity_event"). + Optional(). + Nillable(), + field.Time("started_at"). + Optional(). + Nillable(), + // deleted_at backs soft-delete: a non-nil value excludes the agent from + // default listings (filtered via the DeletedAtIsNil Ent predicate). + field.Time("deleted_at"). + Optional(). + Nillable(), + + // --- Optimistic locking --- + // state_version is incremented on every UpdateAgent and used as a CAS + // guard to detect concurrent modifications under multi-replica Postgres. + field.Int64("state_version"). + Default(1), } } @@ -76,14 +163,6 @@ func (Agent) Edges() []ent.Edge { Field("project_id"). Required(). Unique(), - edge.From("creator", User.Type). - Ref("created_agents"). - Field("created_by"). - Unique(), - edge.From("owner", User.Type). - Ref("owned_agents"). - Field("owner_id"). - Unique(), edge.From("memberships", GroupMembership.Type). Ref("agent"), edge.From("policy_bindings", PolicyBinding.Type). diff --git a/pkg/ent/schema/allowlist.go b/pkg/ent/schema/allowlist.go new file mode 100644 index 000000000..bafd22f3a --- /dev/null +++ b/pkg/ent/schema/allowlist.go @@ -0,0 +1,71 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package schema + +import ( + "time" + + "entgo.io/ent" + "entgo.io/ent/dialect/entsql" + "entgo.io/ent/schema" + "entgo.io/ent/schema/field" + "entgo.io/ent/schema/index" + "github.com/google/uuid" +) + +// AllowListEntry holds the schema definition for the AllowListEntry entity, +// mapping the legacy SQLite `allow_list` table. +// +// email was UNIQUE COLLATE NOCASE in SQLite. Postgres has no NOCASE collation, +// so a plain unique index is declared here; case-insensitive matching (citext +// or a lower(email) functional index) is a port-layer concern. +type AllowListEntry struct { + ent.Schema +} + +// Fields of the AllowListEntry. +func (AllowListEntry) Fields() []ent.Field { + return []ent.Field{ + field.UUID("id", uuid.UUID{}). + Default(uuid.New). + Immutable(), + field.String("email"). + Unique(). + NotEmpty(), + field.String("note"). + Default(""), + field.String("added_by"). + NotEmpty(), + field.String("invite_id"). + Optional(), + field.Time("created"). + Default(time.Now). + Immutable(), + } +} + +// Indexes of the AllowListEntry. +func (AllowListEntry) Indexes() []ent.Index { + return []ent.Index{ + index.Fields("created", "id"), + } +} + +// Annotations of the AllowListEntry. +func (AllowListEntry) Annotations() []schema.Annotation { + return []schema.Annotation{ + entsql.Annotation{Table: "allow_list"}, + } +} diff --git a/pkg/ent/schema/apikey.go b/pkg/ent/schema/apikey.go new file mode 100644 index 000000000..6d4bd7829 --- /dev/null +++ b/pkg/ent/schema/apikey.go @@ -0,0 +1,81 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package schema + +import ( + "time" + + "entgo.io/ent" + "entgo.io/ent/dialect/entsql" + "entgo.io/ent/schema" + "entgo.io/ent/schema/field" + "entgo.io/ent/schema/index" + "github.com/google/uuid" +) + +// ApiKey holds the schema definition for the ApiKey entity, mapping the legacy +// SQLite `api_keys` table. +// +// NOTE: api_keys is a legacy table superseded by user_access_tokens (V34). It is +// schematized here for completeness/migration fidelity; confirm with the +// coordinator whether it is still in active use. +type ApiKey struct { + ent.Schema +} + +// Fields of the ApiKey. +func (ApiKey) Fields() []ent.Field { + return []ent.Field{ + field.UUID("id", uuid.UUID{}). + Default(uuid.New). + Immutable(), + field.UUID("user_id", uuid.UUID{}), + field.String("name"). + Optional(), + field.String("prefix"). + Optional(), + field.String("key_hash"). + Sensitive(). + Unique(). + NotEmpty(), + field.String("scopes"). + Optional(), + field.Bool("revoked"). + Default(false), + field.Time("expires_at"). + Optional(). + Nillable(), + field.Time("last_used"). + Optional(). + Nillable(), + field.Time("created"). + Default(time.Now). + Immutable(), + } +} + +// Indexes of the ApiKey. +func (ApiKey) Indexes() []ent.Index { + return []ent.Index{ + index.Fields("user_id"), + } +} + +// Annotations of the ApiKey. +func (ApiKey) Annotations() []schema.Annotation { + return []schema.Annotation{ + entsql.Annotation{Table: "api_keys"}, + } +} diff --git a/pkg/ent/schema/brokerdispatch.go b/pkg/ent/schema/brokerdispatch.go new file mode 100644 index 000000000..43ddf70e0 --- /dev/null +++ b/pkg/ent/schema/brokerdispatch.go @@ -0,0 +1,99 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package schema + +import ( + "time" + + "entgo.io/ent" + "entgo.io/ent/dialect/entsql" + "entgo.io/ent/schema" + "entgo.io/ent/schema/field" + "entgo.io/ent/schema/index" + "github.com/google/uuid" +) + +// BrokerDispatch holds the schema definition for the BrokerDispatch entity — the +// durable intent table for the "DB as state machine, NOTIFY as the signal" +// dispatch model (design §5.2). A row records a lifecycle/create-time command +// targeted at a broker; the socket-holding node reconciles it (claim → run local +// tunnel op → mark done/failed). `args`/`result` are TEXT (JSON) to stay +// dialect-neutral and keep secrets out of NOTIFY payloads. +type BrokerDispatch struct { + ent.Schema +} + +// Fields of the BrokerDispatch. +func (BrokerDispatch) Fields() []ent.Field { + return []ent.Field{ + field.UUID("id", uuid.UUID{}). + Default(uuid.New). + Immutable(), + field.UUID("broker_id", uuid.UUID{}), + // agent_id is null for project-scoped ops (e.g. create-with-gather). + field.UUID("agent_id", uuid.UUID{}). + Optional(). + Nillable(), + field.String("agent_slug"). + Optional(), + field.UUID("project_id", uuid.UUID{}). + Optional(). + Nillable(), + // op: start|stop|restart|delete|finalize_env|check_prompt|create|message + field.String("op"). + NotEmpty(), + // args: JSON; bulky/secret-bearing fields (resolvedEnv, resolvedSecrets, + // inlineConfig, structured bodies) live here, NOT in the NOTIFY payload. + field.String("args"). + Optional(), + // state: pending|in_progress|done|failed + field.String("state"). + Default("pending"), + // result: JSON; for ops that return data (check_prompt, env-gather). + field.String("result"). + Optional(), + // claimed_by: hub instanceID that reconciled this intent. + field.String("claimed_by"). + Optional(), + field.Int("attempts"). + Default(0), + field.String("error"). + Optional(), + field.Time("created_at"). + Default(time.Now). + Immutable(), + field.Time("updated_at"). + Default(time.Now). + UpdateDefault(time.Now), + field.Time("deadline_at"). + Optional(). + Nillable(), + } +} + +// Indexes of the BrokerDispatch. +func (BrokerDispatch) Indexes() []ent.Index { + return []ent.Index{ + // Drain query: WHERE broker_id=$X AND state='pending'. + index.Fields("broker_id", "state"), + } +} + +// Annotations of the BrokerDispatch. +func (BrokerDispatch) Annotations() []schema.Annotation { + return []schema.Annotation{ + entsql.Annotation{Table: "broker_dispatch"}, + } +} diff --git a/pkg/ent/schema/brokerjointoken.go b/pkg/ent/schema/brokerjointoken.go new file mode 100644 index 000000000..1d4375b3b --- /dev/null +++ b/pkg/ent/schema/brokerjointoken.go @@ -0,0 +1,67 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package schema + +import ( + "time" + + "entgo.io/ent" + "entgo.io/ent/dialect/entsql" + "entgo.io/ent/schema" + "entgo.io/ent/schema/field" + "entgo.io/ent/schema/index" + "github.com/google/uuid" +) + +// BrokerJoinToken holds the schema definition for the BrokerJoinToken entity, +// mapping the legacy SQLite `broker_join_tokens` table. +// +// The primary key is broker_id (one active join token per runtime broker), so +// the id field is stored in the broker_id column with no generated default. +type BrokerJoinToken struct { + ent.Schema +} + +// Fields of the BrokerJoinToken. +func (BrokerJoinToken) Fields() []ent.Field { + return []ent.Field{ + field.UUID("id", uuid.UUID{}). + StorageKey("broker_id"). + Immutable(), + field.String("token_hash"). + Unique(). + NotEmpty(), + field.Time("expires_at"), + field.String("created_by"). + NotEmpty(), + field.Time("created"). + Default(time.Now). + Immutable(), + } +} + +// Indexes of the BrokerJoinToken. +func (BrokerJoinToken) Indexes() []ent.Index { + return []ent.Index{ + index.Fields("expires_at"), + } +} + +// Annotations of the BrokerJoinToken. +func (BrokerJoinToken) Annotations() []schema.Annotation { + return []schema.Annotation{ + entsql.Annotation{Table: "broker_join_tokens"}, + } +} diff --git a/pkg/ent/schema/brokersecret.go b/pkg/ent/schema/brokersecret.go new file mode 100644 index 000000000..36a05d74e --- /dev/null +++ b/pkg/ent/schema/brokersecret.go @@ -0,0 +1,67 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package schema + +import ( + "time" + + "entgo.io/ent" + "entgo.io/ent/dialect/entsql" + "entgo.io/ent/schema" + "entgo.io/ent/schema/field" + "github.com/google/uuid" +) + +// BrokerSecret holds the schema definition for the BrokerSecret entity, mapping +// the legacy SQLite `broker_secrets` table. +// +// The primary key is broker_id (one secret per runtime broker), so the id field +// is stored in the broker_id column with no generated default. secret_key is a +// binary HMAC key (SQLite BLOB → Postgres bytea) and is marked Sensitive. +type BrokerSecret struct { + ent.Schema +} + +// Fields of the BrokerSecret. +func (BrokerSecret) Fields() []ent.Field { + return []ent.Field{ + field.UUID("id", uuid.UUID{}). + StorageKey("broker_id"). + Immutable(), + field.Bytes("secret_key"). + Sensitive(). + NotEmpty(), + field.String("algorithm"). + Default("hmac-sha256"), + field.Time("rotated_at"). + Optional(). + Nillable(), + field.Time("expires_at"). + Optional(). + Nillable(), + field.String("status"). + Default("active"), + field.Time("created"). + Default(time.Now). + Immutable(), + } +} + +// Annotations of the BrokerSecret. +func (BrokerSecret) Annotations() []schema.Annotation { + return []schema.Annotation{ + entsql.Annotation{Table: "broker_secrets"}, + } +} diff --git a/pkg/ent/schema/envvar.go b/pkg/ent/schema/envvar.go new file mode 100644 index 000000000..115f42684 --- /dev/null +++ b/pkg/ent/schema/envvar.go @@ -0,0 +1,81 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package schema + +import ( + "time" + + "entgo.io/ent" + "entgo.io/ent/dialect/entsql" + "entgo.io/ent/schema" + "entgo.io/ent/schema/field" + "entgo.io/ent/schema/index" + "github.com/google/uuid" +) + +// EnvVar holds the schema definition for the EnvVar entity, mapping the legacy +// SQLite `env_vars` table. Like Secret, it is polymorphically scoped via +// (scope, scope_id) with no FK edges. +type EnvVar struct { + ent.Schema +} + +// Fields of the EnvVar. +func (EnvVar) Fields() []ent.Field { + return []ent.Field{ + field.UUID("id", uuid.UUID{}). + Default(uuid.New). + Immutable(), + field.String("key"). + NotEmpty(), + field.String("value"), + field.String("scope"). + NotEmpty(), + field.String("scope_id"), + field.String("description"). + Optional(), + field.Bool("sensitive"). + Default(false), + field.Enum("injection_mode"). + Values("always", "as_needed"). + Default("as_needed"), + field.Bool("secret"). + Default(false), + field.String("created_by"). + Optional(), + field.Time("created"). + Default(time.Now). + Immutable(), + field.Time("updated"). + Default(time.Now). + UpdateDefault(time.Now), + } +} + +// Indexes of the EnvVar. +func (EnvVar) Indexes() []ent.Index { + return []ent.Index{ + index.Fields("key", "scope", "scope_id"). + Unique(), + index.Fields("scope", "scope_id"), + } +} + +// Annotations of the EnvVar. +func (EnvVar) Annotations() []schema.Annotation { + return []schema.Annotation{ + entsql.Annotation{Table: "env_vars"}, + } +} diff --git a/pkg/ent/schema/gcpserviceaccount.go b/pkg/ent/schema/gcpserviceaccount.go new file mode 100644 index 000000000..7b89fd679 --- /dev/null +++ b/pkg/ent/schema/gcpserviceaccount.go @@ -0,0 +1,88 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package schema + +import ( + "time" + + "entgo.io/ent" + "entgo.io/ent/dialect/entsql" + "entgo.io/ent/schema" + "entgo.io/ent/schema/field" + "entgo.io/ent/schema/index" + "github.com/google/uuid" +) + +// GCPServiceAccount holds the schema definition for the GCPServiceAccount +// entity, mapping the legacy SQLite `gcp_service_accounts` table. +// +// Accounts are polymorphically scoped via (scope, scope_id); default_scopes is +// a raw string (JSON/CSV) kept dialect-neutral. +type GCPServiceAccount struct { + ent.Schema +} + +// Fields of the GCPServiceAccount. +func (GCPServiceAccount) Fields() []ent.Field { + return []ent.Field{ + field.UUID("id", uuid.UUID{}). + Default(uuid.New). + Immutable(), + field.String("scope"). + NotEmpty(), + field.String("scope_id"). + NotEmpty(), + field.String("email"). + NotEmpty(), + // project_id holds the GCP *cloud project* identifier (e.g. + // "my-project-123"), which is a free-form string, not a UUID. + field.String("project_id"). + NotEmpty(), + field.String("display_name"). + Default(""), + field.String("default_scopes"). + Default(""), + field.Bool("verified"). + Default(false), + field.Time("verified_at"). + Optional(). + Nillable(), + field.String("created_by"). + Default(""), + field.Bool("managed"). + Default(false), + field.String("managed_by"). + Default(""), + field.Time("created"). + Default(time.Now). + Immutable(), + } +} + +// Indexes of the GCPServiceAccount. +func (GCPServiceAccount) Indexes() []ent.Index { + return []ent.Index{ + index.Fields("email", "scope", "scope_id"). + Unique(), + index.Fields("scope", "scope_id"), + } +} + +// Annotations of the GCPServiceAccount. +func (GCPServiceAccount) Annotations() []schema.Annotation { + return []schema.Annotation{ + entsql.Annotation{Table: "gcp_service_accounts"}, + } +} diff --git a/pkg/ent/schema/githubinstallation.go b/pkg/ent/schema/githubinstallation.go new file mode 100644 index 000000000..6e2626ae9 --- /dev/null +++ b/pkg/ent/schema/githubinstallation.go @@ -0,0 +1,65 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package schema + +import ( + "time" + + "entgo.io/ent" + "entgo.io/ent/dialect/entsql" + "entgo.io/ent/schema" + "entgo.io/ent/schema/field" +) + +// GithubInstallation holds the schema definition for the GithubInstallation +// entity, mapping the legacy SQLite `github_installations` table. +// +// The primary key is the GitHub-provided installation_id (a real integer id, +// NOT a UUID), so id is an int64 stored in the installation_id column with no +// generated default. repositories is a raw JSON string. +type GithubInstallation struct { + ent.Schema +} + +// Fields of the GithubInstallation. +func (GithubInstallation) Fields() []ent.Field { + return []ent.Field{ + field.Int64("id"). + StorageKey("installation_id"). + Immutable(), + field.String("account_login"). + NotEmpty(), + field.String("account_type"). + Default("Organization"), + field.Int64("app_id"), + field.String("repositories"). + Default("[]"), + field.String("status"). + Default("active"), + field.Time("created"). + Default(time.Now). + Immutable(), + field.Time("updated"). + Default(time.Now). + UpdateDefault(time.Now), + } +} + +// Annotations of the GithubInstallation. +func (GithubInstallation) Annotations() []schema.Annotation { + return []schema.Annotation{ + entsql.Annotation{Table: "github_installations"}, + } +} diff --git a/pkg/ent/schema/harnessconfig.go b/pkg/ent/schema/harnessconfig.go new file mode 100644 index 000000000..1814ae6ce --- /dev/null +++ b/pkg/ent/schema/harnessconfig.go @@ -0,0 +1,103 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package schema + +import ( + "time" + + "entgo.io/ent" + "entgo.io/ent/dialect/entsql" + "entgo.io/ent/schema" + "entgo.io/ent/schema/field" + "entgo.io/ent/schema/index" + "github.com/google/uuid" +) + +// HarnessConfig holds the schema definition for the HarnessConfig entity, +// mapping the legacy SQLite `harness_configs` table. It is scope/scope_id +// addressed (no project_id FK column); JSON columns (config, files) are kept +// as raw strings for dialect neutrality. +type HarnessConfig struct { + ent.Schema +} + +// Fields of the HarnessConfig. +func (HarnessConfig) Fields() []ent.Field { + return []ent.Field{ + field.UUID("id", uuid.UUID{}). + Default(uuid.New). + Immutable(), + field.String("name"). + NotEmpty(), + field.String("slug"). + NotEmpty(), + field.String("display_name"). + Optional(), + field.String("description"). + Optional(), + field.String("harness"). + NotEmpty(), + field.String("config"). + Optional(), + field.String("content_hash"). + Optional(), + field.String("scope"). + Default("global"), + field.String("scope_id"). + Optional(), + field.String("storage_uri"). + Optional(), + field.String("storage_bucket"). + Optional(), + field.String("storage_path"). + Optional(), + field.String("files"). + Optional(), + field.Enum("status"). + Values("pending", "active", "archived"). + Default("active"), + field.String("owner_id"). + Optional(), + field.String("created_by"). + Optional(), + field.String("updated_by"). + Optional(), + field.String("visibility"). + Default("private"), + field.Time("created"). + Default(time.Now). + Immutable(), + field.Time("updated"). + Default(time.Now). + UpdateDefault(time.Now), + } +} + +// Indexes of the HarnessConfig. +func (HarnessConfig) Indexes() []ent.Index { + return []ent.Index{ + index.Fields("slug", "scope", "scope_id").Unique(), + index.Fields("harness"), + index.Fields("status"), + index.Fields("content_hash"), + } +} + +// Annotations of the HarnessConfig. +func (HarnessConfig) Annotations() []schema.Annotation { + return []schema.Annotation{ + entsql.Annotation{Table: "harness_configs"}, + } +} diff --git a/pkg/ent/schema/invitecode.go b/pkg/ent/schema/invitecode.go new file mode 100644 index 000000000..0f8eccf8c --- /dev/null +++ b/pkg/ent/schema/invitecode.go @@ -0,0 +1,76 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package schema + +import ( + "time" + + "entgo.io/ent" + "entgo.io/ent/dialect/entsql" + "entgo.io/ent/schema" + "entgo.io/ent/schema/field" + "entgo.io/ent/schema/index" + "github.com/google/uuid" +) + +// InviteCode holds the schema definition for the InviteCode entity, mapping the +// legacy SQLite `invite_codes` table. code_hash is the unique lookup key and is +// marked Sensitive. +type InviteCode struct { + ent.Schema +} + +// Fields of the InviteCode. +func (InviteCode) Fields() []ent.Field { + return []ent.Field{ + field.UUID("id", uuid.UUID{}). + Default(uuid.New). + Immutable(), + field.String("code_hash"). + Sensitive(). + Unique(). + NotEmpty(), + field.String("code_prefix"). + NotEmpty(), + field.Int("max_uses"). + Default(1), + field.Int("use_count"). + Default(0), + field.Time("expires_at"), + field.Bool("revoked"). + Default(false), + field.String("created_by"). + NotEmpty(), + field.String("note"). + Default(""), + field.Time("created"). + Default(time.Now). + Immutable(), + } +} + +// Indexes of the InviteCode. +func (InviteCode) Indexes() []ent.Index { + return []ent.Index{ + index.Fields("expires_at"), + } +} + +// Annotations of the InviteCode. +func (InviteCode) Annotations() []schema.Annotation { + return []schema.Annotation{ + entsql.Annotation{Table: "invite_codes"}, + } +} diff --git a/pkg/ent/schema/maintenanceoperation.go b/pkg/ent/schema/maintenanceoperation.go new file mode 100644 index 000000000..bbe7149a6 --- /dev/null +++ b/pkg/ent/schema/maintenanceoperation.go @@ -0,0 +1,76 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package schema + +import ( + "time" + + "entgo.io/ent" + "entgo.io/ent/dialect/entsql" + "entgo.io/ent/schema" + "entgo.io/ent/schema/field" + "github.com/google/uuid" +) + +// MaintenanceOperation holds the schema definition for the MaintenanceOperation +// entity, mapping the legacy SQLite `maintenance_operations` table. +// +// `key` is a stable unique business key. Seed rows that SQLite created via +// hex(randomblob(...)) move to Go-side seeding (out of scope for this schema). +type MaintenanceOperation struct { + ent.Schema +} + +// Fields of the MaintenanceOperation. +func (MaintenanceOperation) Fields() []ent.Field { + return []ent.Field{ + field.UUID("id", uuid.UUID{}). + Default(uuid.New). + Immutable(), + field.String("key"). + Unique(). + NotEmpty(), + field.String("title"). + NotEmpty(), + field.String("description"). + Default(""), + field.String("category"). + NotEmpty(), + field.String("status"). + Default("pending"), + field.Time("started_at"). + Optional(). + Nillable(), + field.Time("completed_at"). + Optional(). + Nillable(), + field.String("started_by"). + Optional(), + field.String("result"). + Optional(), + field.String("metadata"). + Default("{}"), + field.Time("created"). + Default(time.Now). + Immutable(), + } +} + +// Annotations of the MaintenanceOperation. +func (MaintenanceOperation) Annotations() []schema.Annotation { + return []schema.Annotation{ + entsql.Annotation{Table: "maintenance_operations"}, + } +} diff --git a/pkg/ent/schema/maintenanceoperationrun.go b/pkg/ent/schema/maintenanceoperationrun.go new file mode 100644 index 000000000..388fa9397 --- /dev/null +++ b/pkg/ent/schema/maintenanceoperationrun.go @@ -0,0 +1,76 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package schema + +import ( + "time" + + "entgo.io/ent" + "entgo.io/ent/dialect/entsql" + "entgo.io/ent/schema" + "entgo.io/ent/schema/field" + "entgo.io/ent/schema/index" + "github.com/google/uuid" +) + +// MaintenanceOperationRun holds the schema definition for the +// MaintenanceOperationRun entity, mapping the legacy SQLite +// `maintenance_operation_runs` table. +// +// operation_key references maintenance_operations(key) — a non-id unique +// column — so it is modeled as a plain string rather than an Ent edge (which +// binds to id). The FK is reconstructed at the port/migration layer. +type MaintenanceOperationRun struct { + ent.Schema +} + +// Fields of the MaintenanceOperationRun. +func (MaintenanceOperationRun) Fields() []ent.Field { + return []ent.Field{ + field.UUID("id", uuid.UUID{}). + Default(uuid.New). + Immutable(), + field.String("operation_key"). + NotEmpty(), + field.String("status"). + Default("running"), + field.Time("started_at"). + Default(time.Now). + Immutable(), + field.Time("completed_at"). + Optional(). + Nillable(), + field.String("started_by"). + Optional(), + field.String("result"). + Optional(), + field.String("log"). + Default(""), + } +} + +// Indexes of the MaintenanceOperationRun. +func (MaintenanceOperationRun) Indexes() []ent.Index { + return []ent.Index{ + index.Fields("operation_key"), + } +} + +// Annotations of the MaintenanceOperationRun. +func (MaintenanceOperationRun) Annotations() []schema.Annotation { + return []schema.Annotation{ + entsql.Annotation{Table: "maintenance_operation_runs"}, + } +} diff --git a/pkg/ent/schema/message.go b/pkg/ent/schema/message.go new file mode 100644 index 000000000..8ff64e2cf --- /dev/null +++ b/pkg/ent/schema/message.go @@ -0,0 +1,95 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package schema + +import ( + "time" + + "entgo.io/ent" + "entgo.io/ent/dialect/entsql" + "entgo.io/ent/schema" + "entgo.io/ent/schema/field" + "entgo.io/ent/schema/index" + "github.com/google/uuid" +) + +// Message holds the schema definition for the Message entity, mapping the legacy +// SQLite `messages` table. +// +// sender_id/recipient_id/agent_id/group_id are kept as plain strings (they hold +// heterogeneous principal identifiers and defaulted to ” in SQLite), while +// project_id is a required UUID. +type Message struct { + ent.Schema +} + +// Fields of the Message. +func (Message) Fields() []ent.Field { + return []ent.Field{ + field.UUID("id", uuid.UUID{}). + Default(uuid.New). + Immutable(), + field.UUID("project_id", uuid.UUID{}), + field.String("sender"). + NotEmpty(), + field.String("sender_id"). + Optional(), + field.String("recipient"). + NotEmpty(), + field.String("recipient_id"). + Optional(), + field.String("msg"). + NotEmpty(), + field.String("type"). + Default("instruction"), + field.Bool("urgent"). + Default(false), + field.Bool("broadcasted"). + Default(false), + field.Bool("read"). + Default(false), + field.String("agent_id"). + Optional(), + field.String("group_id"). + Optional(), + // dispatch_state tracks cross-node delivery (the message row IS the durable + // dispatch intent): pending|dispatched|failed. The owning node CAS-flips + // pending->dispatched after tunneling. Default keeps existing rows valid. + field.String("dispatch_state"). + Default("pending"), + field.Time("dispatched_at"). + Optional(). + Nillable(), + field.Time("created"). + Default(time.Now). + Immutable(), + } +} + +// Indexes of the Message. +func (Message) Indexes() []ent.Index { + return []ent.Index{ + index.Fields("project_id"), + index.Fields("recipient", "recipient_id"), + index.Fields("created"), + } +} + +// Annotations of the Message. +func (Message) Annotations() []schema.Annotation { + return []schema.Annotation{ + entsql.Annotation{Table: "messages"}, + } +} diff --git a/pkg/ent/schema/notification.go b/pkg/ent/schema/notification.go new file mode 100644 index 000000000..dc1bec137 --- /dev/null +++ b/pkg/ent/schema/notification.go @@ -0,0 +1,78 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package schema + +import ( + "time" + + "entgo.io/ent" + "entgo.io/ent/dialect/entsql" + "entgo.io/ent/schema" + "entgo.io/ent/schema/field" + "entgo.io/ent/schema/index" + "github.com/google/uuid" +) + +// Notification holds the schema definition for the Notification entity, mapping +// the legacy SQLite `notifications` table. +// +// Foreign keys (subscription_id, agent_id, project_id) are modeled as plain +// UUID columns rather than Ent edges to keep this periphery schema independent; +// edge wiring is deferred to a later pass. +type Notification struct { + ent.Schema +} + +// Fields of the Notification. +func (Notification) Fields() []ent.Field { + return []ent.Field{ + field.UUID("id", uuid.UUID{}). + Default(uuid.New). + Immutable(), + field.UUID("subscription_id", uuid.UUID{}), + field.UUID("agent_id", uuid.UUID{}), + field.UUID("project_id", uuid.UUID{}), + field.String("subscriber_type"). + NotEmpty(), + field.String("subscriber_id"). + NotEmpty(), + field.String("status"). + NotEmpty(), + field.String("message"). + NotEmpty(), + field.Bool("dispatched"). + Default(false), + field.Bool("acknowledged"). + Default(false), + field.Time("created"). + Default(time.Now). + Immutable(), + } +} + +// Indexes of the Notification. +func (Notification) Indexes() []ent.Index { + return []ent.Index{ + index.Fields("subscription_id"), + index.Fields("project_id", "subscriber_type", "subscriber_id"), + } +} + +// Annotations of the Notification. +func (Notification) Annotations() []schema.Annotation { + return []schema.Annotation{ + entsql.Annotation{Table: "notifications"}, + } +} diff --git a/pkg/ent/schema/notificationsubscription.go b/pkg/ent/schema/notificationsubscription.go new file mode 100644 index 000000000..82ff3c316 --- /dev/null +++ b/pkg/ent/schema/notificationsubscription.go @@ -0,0 +1,81 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package schema + +import ( + "time" + + "entgo.io/ent" + "entgo.io/ent/dialect/entsql" + "entgo.io/ent/schema" + "entgo.io/ent/schema/field" + "entgo.io/ent/schema/index" + "github.com/google/uuid" +) + +// NotificationSubscription holds the schema definition for the +// NotificationSubscription entity, mapping the legacy SQLite +// `notification_subscriptions` table. +// +// trigger_activities is kept as a raw JSON string to stay dialect-neutral. The +// SQLite store enforced uniqueness via a COALESCE-based partial index over +// (scope, agent_id, subscriber_type, subscriber_id, project_id); that +// expression index is not dialect-neutral, so a plain composite index is +// declared here and the unique-with-NULL semantics are deferred to the port +// layer / migration. +type NotificationSubscription struct { + ent.Schema +} + +// Fields of the NotificationSubscription. +func (NotificationSubscription) Fields() []ent.Field { + return []ent.Field{ + field.UUID("id", uuid.UUID{}). + Default(uuid.New). + Immutable(), + field.String("scope"). + Default("agent"), + field.UUID("agent_id", uuid.UUID{}). + Optional(). + Nillable(), + field.String("subscriber_type"). + Default("agent"), + field.String("subscriber_id"). + NotEmpty(), + field.UUID("project_id", uuid.UUID{}), + field.String("trigger_activities"). + NotEmpty(), + field.String("created_by"). + NotEmpty(), + field.Time("created"). + Default(time.Now). + Immutable(), + } +} + +// Indexes of the NotificationSubscription. +func (NotificationSubscription) Indexes() []ent.Index { + return []ent.Index{ + index.Fields("scope", "agent_id", "subscriber_type", "subscriber_id", "project_id"), + index.Fields("project_id"), + } +} + +// Annotations of the NotificationSubscription. +func (NotificationSubscription) Annotations() []schema.Annotation { + return []schema.Annotation{ + entsql.Annotation{Table: "notification_subscriptions"}, + } +} diff --git a/pkg/ent/schema/project.go b/pkg/ent/schema/project.go index c61b3d61d..b795ee85a 100644 --- a/pkg/ent/schema/project.go +++ b/pkg/ent/schema/project.go @@ -25,9 +25,15 @@ import ( "github.com/google/uuid" ) -// Project holds the schema definition for the Project entity. -// This is a minimal schema for edge compilation; operational fields -// will be added when the project entity is fully migrated to Ent. +// Project holds the schema definition for the Project entity, mapping the legacy +// SQLite `projects` table (groves). +// +// JSON-bearing operational columns (shared_dirs, github_permissions, +// github_app_status, git_identity) are kept as raw JSON strings to stay +// dialect-neutral and to avoid importing the store/api model types into the +// schema package, matching the RuntimeBroker convention. The port layer +// (entadapter) marshals/unmarshals them. Computed fields on store.Project +// (AgentCount, ActiveBrokerCount, ProjectType, OwnerName) are not persisted. type Project struct { ent.Schema } @@ -46,10 +52,15 @@ func (Project) Fields() []ent.Field { field.String("git_remote"). Optional(). Nillable(), + field.String("default_runtime_broker_id"). + Optional(). + Nillable(), field.JSON("labels", map[string]string{}). Optional(), field.JSON("annotations", map[string]string{}). Optional(), + field.String("shared_dirs"). + Optional(), field.Time("created"). Default(time.Now). Immutable(), @@ -62,6 +73,15 @@ func (Project) Fields() []ent.Field { Optional(), field.String("visibility"). Default("private"), + field.Int64("github_installation_id"). + Optional(). + Nillable(), + field.String("github_permissions"). + Optional(), + field.String("github_app_status"). + Optional(), + field.String("git_identity"). + Optional(), } } diff --git a/pkg/ent/schema/projectcontributor.go b/pkg/ent/schema/projectcontributor.go new file mode 100644 index 000000000..58146f5da --- /dev/null +++ b/pkg/ent/schema/projectcontributor.go @@ -0,0 +1,81 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package schema + +import ( + "entgo.io/ent" + "entgo.io/ent/dialect/entsql" + "entgo.io/ent/schema" + "entgo.io/ent/schema/field" + "entgo.io/ent/schema/index" + "github.com/google/uuid" +) + +// ProjectContributor holds the schema definition for the ProjectContributor +// entity, mapping the legacy SQLite `project_contributors` table (was +// `grove_contributors` before the V50 grove→project rename). It records which +// runtime brokers contribute to / provide for a project. +// +// SQLite used a composite primary key (project_id, broker_id) with no id column; +// Ent prefers a single id, so a surrogate UUID id is added and the original key +// is enforced via a unique index. profiles is a raw JSON string. +type ProjectContributor struct { + ent.Schema +} + +// Fields of the ProjectContributor. +func (ProjectContributor) Fields() []ent.Field { + return []ent.Field{ + field.UUID("id", uuid.UUID{}). + Default(uuid.New). + Immutable(), + field.UUID("project_id", uuid.UUID{}), + field.UUID("broker_id", uuid.UUID{}), + field.String("broker_name"). + NotEmpty(), + field.String("mode"). + Default("connected"), + field.String("status"). + Default("offline"), + field.String("profiles"). + Optional(), + field.Time("last_seen"). + Optional(). + Nillable(), + field.String("local_path"). + Optional(), + field.String("linked_by"). + Optional(), + field.Time("linked_at"). + Optional(). + Nillable(), + } +} + +// Indexes of the ProjectContributor. +func (ProjectContributor) Indexes() []ent.Index { + return []ent.Index{ + index.Fields("project_id", "broker_id"). + Unique(), + index.Fields("broker_id"), + } +} + +// Annotations of the ProjectContributor. +func (ProjectContributor) Annotations() []schema.Annotation { + return []schema.Annotation{ + entsql.Annotation{Table: "project_contributors"}, + } +} diff --git a/pkg/ent/schema/projectsyncstate.go b/pkg/ent/schema/projectsyncstate.go new file mode 100644 index 000000000..089aa0c88 --- /dev/null +++ b/pkg/ent/schema/projectsyncstate.go @@ -0,0 +1,72 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package schema + +import ( + "entgo.io/ent" + "entgo.io/ent/dialect/entsql" + "entgo.io/ent/schema" + "entgo.io/ent/schema/field" + "entgo.io/ent/schema/index" + "github.com/google/uuid" +) + +// ProjectSyncState holds the schema definition for the ProjectSyncState entity, +// mapping the legacy SQLite `project_sync_state` table (was `grove_sync_state` +// before the V50 grove→project rename). +// +// SQLite used a composite primary key (project_id, broker_id) with no id column; +// Ent prefers a single id, so a surrogate UUID id is added and the original key +// is enforced via a unique index. broker_id is a plain string (it defaulted to +// ” in SQLite for project-wide sync state). +type ProjectSyncState struct { + ent.Schema +} + +// Fields of the ProjectSyncState. +func (ProjectSyncState) Fields() []ent.Field { + return []ent.Field{ + field.UUID("id", uuid.UUID{}). + Default(uuid.New). + Immutable(), + field.UUID("project_id", uuid.UUID{}), + field.String("broker_id"). + Default(""), + field.Time("last_sync_time"). + Optional(). + Nillable(), + field.String("last_commit_sha"). + Optional(), + field.Int("file_count"). + Default(0), + field.Int64("total_bytes"). + Default(0), + } +} + +// Indexes of the ProjectSyncState. +func (ProjectSyncState) Indexes() []ent.Index { + return []ent.Index{ + index.Fields("project_id", "broker_id"). + Unique(), + } +} + +// Annotations of the ProjectSyncState. +func (ProjectSyncState) Annotations() []schema.Annotation { + return []schema.Annotation{ + entsql.Annotation{Table: "project_sync_state"}, + } +} diff --git a/pkg/ent/schema/runtimebroker.go b/pkg/ent/schema/runtimebroker.go new file mode 100644 index 000000000..e4e4290df --- /dev/null +++ b/pkg/ent/schema/runtimebroker.go @@ -0,0 +1,120 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package schema + +import ( + "time" + + "entgo.io/ent" + "entgo.io/ent/dialect/entsql" + "entgo.io/ent/schema" + "entgo.io/ent/schema/field" + "entgo.io/ent/schema/index" + "github.com/google/uuid" +) + +// RuntimeBroker holds the schema definition for the RuntimeBroker entity, +// mapping the legacy SQLite `runtime_brokers` table. +// +// JSON-bearing columns (capabilities, supported_harnesses, resources, runtimes, +// labels, annotations) are kept as raw strings to stay dialect-neutral and match +// the existing store's raw-marshaling behavior during the dual-write phase. +type RuntimeBroker struct { + ent.Schema +} + +// Fields of the RuntimeBroker. +func (RuntimeBroker) Fields() []ent.Field { + return []ent.Field{ + field.UUID("id", uuid.UUID{}). + Default(uuid.New). + Immutable(), + field.String("name"). + NotEmpty(), + field.String("slug"). + NotEmpty(), + // type/mode are vestigial columns in the legacy store (the SQLite store + // always writes ""); kept Optional for column parity rather than required. + field.String("type"). + Optional(), + field.String("mode"). + Default("connected"), + field.String("version"). + Optional(), + // lock_version is an internal optimistic-concurrency token (not surfaced + // on store.RuntimeBroker, which already uses "version" for the broker + // software version). The heartbeat and full-update paths compare-and-set + // this column to serialize concurrent writers without SELECT ... FOR + // UPDATE, so the same logic is correct on both SQLite (tests) and + // Postgres (production). + field.Int64("lock_version"). + Default(0), + field.String("status"). + Default("offline"), + field.String("connection_state"). + Default("disconnected"), + field.Time("last_heartbeat"). + Optional(). + Nillable(), + field.String("capabilities"). + Optional(), + field.String("supported_harnesses"). + Optional(), + field.String("resources"). + Optional(), + field.String("runtimes"). + Optional(), + field.String("labels"). + Optional(), + field.String("annotations"). + Optional(), + field.String("endpoint"). + Optional(), + field.String("created_by"). + Optional(), + field.Bool("auto_provide"). + Default(false), + field.String("connected_hub_id"). + Optional(). + Nillable(), + field.String("connected_session_id"). + Optional(). + Nillable(), + field.Time("connected_at"). + Optional(). + Nillable(), + field.Time("created"). + Default(time.Now). + Immutable(), + field.Time("updated"). + Default(time.Now). + UpdateDefault(time.Now), + } +} + +// Indexes of the RuntimeBroker. +func (RuntimeBroker) Indexes() []ent.Index { + return []ent.Index{ + index.Fields("slug"), + index.Fields("status"), + } +} + +// Annotations of the RuntimeBroker. +func (RuntimeBroker) Annotations() []schema.Annotation { + return []schema.Annotation{ + entsql.Annotation{Table: "runtime_brokers"}, + } +} diff --git a/pkg/ent/schema/schedule.go b/pkg/ent/schema/schedule.go new file mode 100644 index 000000000..6d4041c52 --- /dev/null +++ b/pkg/ent/schema/schedule.go @@ -0,0 +1,94 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package schema + +import ( + "time" + + "entgo.io/ent" + "entgo.io/ent/dialect/entsql" + "entgo.io/ent/schema" + "entgo.io/ent/schema/field" + "entgo.io/ent/schema/index" + "github.com/google/uuid" +) + +// Schedule holds the schema definition for the Schedule entity, mapping the +// legacy SQLite `schedules` table. +// +// payload is a raw JSON string. The SQLite store used a partial index on +// next_run_at (WHERE status='active'); a plain index is declared here to stay +// dialect-neutral. +type Schedule struct { + ent.Schema +} + +// Fields of the Schedule. +func (Schedule) Fields() []ent.Field { + return []ent.Field{ + field.UUID("id", uuid.UUID{}). + Default(uuid.New). + Immutable(), + field.UUID("project_id", uuid.UUID{}), + field.String("name"). + NotEmpty(), + field.String("cron_expr"). + NotEmpty(), + field.String("event_type"). + NotEmpty(), + field.String("payload"). + Default("{}"), + field.String("status"). + Default("active"), + field.Time("next_run_at"). + Optional(). + Nillable(), + field.Time("last_run_at"). + Optional(). + Nillable(), + field.String("last_run_status"). + Optional(), + field.String("last_run_error"). + Optional(), + field.Int("run_count"). + Default(0), + field.Int("error_count"). + Default(0), + field.String("created_by"). + Optional(), + field.Time("created"). + Default(time.Now). + Immutable(), + field.Time("updated"). + Default(time.Now). + UpdateDefault(time.Now), + } +} + +// Indexes of the Schedule. +func (Schedule) Indexes() []ent.Index { + return []ent.Index{ + index.Fields("project_id", "name"). + Unique(), + index.Fields("next_run_at"), + } +} + +// Annotations of the Schedule. +func (Schedule) Annotations() []schema.Annotation { + return []schema.Annotation{ + entsql.Annotation{Table: "schedules"}, + } +} diff --git a/pkg/ent/schema/scheduledevent.go b/pkg/ent/schema/scheduledevent.go new file mode 100644 index 000000000..5a869d000 --- /dev/null +++ b/pkg/ent/schema/scheduledevent.go @@ -0,0 +1,82 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package schema + +import ( + "time" + + "entgo.io/ent" + "entgo.io/ent/dialect/entsql" + "entgo.io/ent/schema" + "entgo.io/ent/schema/field" + "entgo.io/ent/schema/index" + "github.com/google/uuid" +) + +// ScheduledEvent holds the schema definition for the ScheduledEvent entity, +// mapping the legacy SQLite `scheduled_events` table. +// +// payload is a raw JSON string. The SQLite store used a partial index on fire_at +// (WHERE status='pending'); a plain index is declared here to stay +// dialect-neutral. schedule_id is an optional back-reference string (defaulted +// to ” in SQLite). +type ScheduledEvent struct { + ent.Schema +} + +// Fields of the ScheduledEvent. +func (ScheduledEvent) Fields() []ent.Field { + return []ent.Field{ + field.UUID("id", uuid.UUID{}). + Default(uuid.New). + Immutable(), + field.UUID("project_id", uuid.UUID{}), + field.String("event_type"). + NotEmpty(), + field.Time("fire_at"), + field.String("payload"). + NotEmpty(), + field.String("status"). + Default("pending"), + field.String("created_by"). + Optional(), + field.Time("fired_at"). + Optional(). + Nillable(), + field.String("error"). + Optional(), + field.String("schedule_id"). + Optional(), + field.Time("created"). + Default(time.Now). + Immutable(), + } +} + +// Indexes of the ScheduledEvent. +func (ScheduledEvent) Indexes() []ent.Index { + return []ent.Index{ + index.Fields("fire_at"), + index.Fields("project_id"), + index.Fields("status"), + } +} + +// Annotations of the ScheduledEvent. +func (ScheduledEvent) Annotations() []schema.Annotation { + return []schema.Annotation{ + entsql.Annotation{Table: "scheduled_events"}, + } +} diff --git a/pkg/ent/schema/secret.go b/pkg/ent/schema/secret.go new file mode 100644 index 000000000..8fa72c289 --- /dev/null +++ b/pkg/ent/schema/secret.go @@ -0,0 +1,94 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package schema + +import ( + "time" + + "entgo.io/ent" + "entgo.io/ent/dialect/entsql" + "entgo.io/ent/schema" + "entgo.io/ent/schema/field" + "entgo.io/ent/schema/index" + "github.com/google/uuid" +) + +// Secret holds the schema definition for the Secret entity, mapping the legacy +// SQLite `secrets` table. Secrets are polymorphically scoped (hub/user/project/ +// runtime_broker) via (scope, scope_id), so no FK edges are declared. +// +// encrypted_value stores the encrypted secret payload as TEXT (base64), not a +// BLOB, and is marked Sensitive so it is never logged or serialized. +type Secret struct { + ent.Schema +} + +// Fields of the Secret. +func (Secret) Fields() []ent.Field { + return []ent.Field{ + field.UUID("id", uuid.UUID{}). + Default(uuid.New). + Immutable(), + field.String("key"). + NotEmpty(), + field.String("encrypted_value"). + Sensitive(), + field.String("secret_ref"). + Optional(), + field.Enum("secret_type"). + Values("environment", "variable", "file", "internal"). + Default("environment"), + field.String("target"). + Optional(), + field.String("scope"). + NotEmpty(), + field.String("scope_id"), + field.String("description"). + Optional(), + field.Enum("injection_mode"). + Values("always", "as_needed"). + Default("as_needed"), + field.Bool("allow_progeny"). + Default(false), + field.Int("version"). + Default(1), + field.String("created_by"). + Optional(), + field.String("updated_by"). + Optional(), + field.Time("created"). + Default(time.Now). + Immutable(), + field.Time("updated"). + Default(time.Now). + UpdateDefault(time.Now), + } +} + +// Indexes of the Secret. +func (Secret) Indexes() []ent.Index { + return []ent.Index{ + index.Fields("key", "scope", "scope_id"). + Unique(), + index.Fields("scope", "scope_id"), + } +} + +// Annotations of the Secret. +func (Secret) Annotations() []schema.Annotation { + return []schema.Annotation{ + entsql.Annotation{Table: "secrets"}, + } +} diff --git a/pkg/ent/schema/subscriptiontemplate.go b/pkg/ent/schema/subscriptiontemplate.go new file mode 100644 index 000000000..baaed7578 --- /dev/null +++ b/pkg/ent/schema/subscriptiontemplate.go @@ -0,0 +1,68 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package schema + +import ( + "entgo.io/ent" + "entgo.io/ent/dialect/entsql" + "entgo.io/ent/schema" + "entgo.io/ent/schema/field" + "entgo.io/ent/schema/index" + "github.com/google/uuid" +) + +// SubscriptionTemplate holds the schema definition for the SubscriptionTemplate +// entity, mapping the legacy SQLite `subscription_templates` table. +// +// project_id is nullable (the SQLite column defaulted to ” for global-scoped +// templates); the (project_id, name) uniqueness is enforced via a unique index. +type SubscriptionTemplate struct { + ent.Schema +} + +// Fields of the SubscriptionTemplate. +func (SubscriptionTemplate) Fields() []ent.Field { + return []ent.Field{ + field.UUID("id", uuid.UUID{}). + Default(uuid.New). + Immutable(), + field.String("name"). + NotEmpty(), + field.String("scope"). + Default("project"), + field.String("trigger_activities"). + NotEmpty(), + field.UUID("project_id", uuid.UUID{}). + Optional(). + Nillable(), + field.String("created_by"). + NotEmpty(), + } +} + +// Indexes of the SubscriptionTemplate. +func (SubscriptionTemplate) Indexes() []ent.Index { + return []ent.Index{ + index.Fields("project_id", "name"). + Unique(), + } +} + +// Annotations of the SubscriptionTemplate. +func (SubscriptionTemplate) Annotations() []schema.Annotation { + return []schema.Annotation{ + entsql.Annotation{Table: "subscription_templates"}, + } +} diff --git a/pkg/ent/schema/template.go b/pkg/ent/schema/template.go new file mode 100644 index 000000000..791e56d61 --- /dev/null +++ b/pkg/ent/schema/template.go @@ -0,0 +1,117 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package schema + +import ( + "time" + + "entgo.io/ent" + "entgo.io/ent/dialect/entsql" + "entgo.io/ent/schema" + "entgo.io/ent/schema/field" + "entgo.io/ent/schema/index" + "github.com/google/uuid" +) + +// Template holds the schema definition for the Template entity, mapping the +// legacy SQLite `templates` table (final post-V50 state; grove_id renamed to +// project_id). +// +// JSON columns (config, files) are kept as raw strings to stay dialect-neutral. +// Foreign references (project_id, scope_id, owner ids) are modeled as plain +// strings rather than edges so global/unscoped rows with empty values port +// cleanly; edges can be added later when the full entity migrates. +type Template struct { + ent.Schema +} + +// Fields of the Template. +func (Template) Fields() []ent.Field { + return []ent.Field{ + field.UUID("id", uuid.UUID{}). + Default(uuid.New). + Immutable(), + field.String("name"). + NotEmpty(), + field.String("slug"). + NotEmpty(), + field.String("display_name"). + Optional(), + field.String("description"). + Optional(), + // harness may be empty: a directory template that declares no harness type + // leaves this blank; the raw-SQL store allowed it and BootstrapTemplatesFromDir + // relies on storing such templates rather than skipping them. + field.String("harness"), + field.String("default_harness_config"). + Optional(), + field.String("image"). + Optional(), + field.String("config"). + Optional(), + field.String("content_hash"). + Optional(), + field.String("scope"). + Default("global"), + field.String("scope_id"). + Optional(), + field.String("project_id"). + Optional(), + field.String("storage_uri"). + Optional(), + field.String("storage_bucket"). + Optional(), + field.String("storage_path"). + Optional(), + field.String("files"). + Optional(), + field.String("base_template"). + Optional(), + field.Enum("status"). + Values("pending", "active", "archived"). + Default("active"), + field.String("owner_id"). + Optional(), + field.String("created_by"). + Optional(), + field.String("updated_by"). + Optional(), + field.String("visibility"). + Default("private"), + field.Time("created"). + Default(time.Now). + Immutable(), + field.Time("updated"). + Default(time.Now). + UpdateDefault(time.Now), + } +} + +// Indexes of the Template. +func (Template) Indexes() []ent.Index { + return []ent.Index{ + index.Fields("slug", "scope", "scope_id").Unique(), + index.Fields("harness"), + index.Fields("status"), + index.Fields("content_hash"), + } +} + +// Annotations of the Template. +func (Template) Annotations() []schema.Annotation { + return []schema.Annotation{ + entsql.Annotation{Table: "templates"}, + } +} diff --git a/pkg/ent/schema/user.go b/pkg/ent/schema/user.go index e061c7896..5b3dfbd7b 100644 --- a/pkg/ent/schema/user.go +++ b/pkg/ent/schema/user.go @@ -20,6 +20,7 @@ import ( "entgo.io/ent" "entgo.io/ent/schema/edge" "entgo.io/ent/schema/field" + "entgo.io/ent/schema/index" "github.com/google/uuid" ) @@ -34,11 +35,24 @@ func (User) Fields() []ent.Field { field.UUID("id", uuid.UUID{}). Default(uuid.New). Immutable(), + // email was UNIQUE COLLATE NOCASE in the legacy SQLite schema. Postgres + // has no NOCASE collation, so case-insensitive uniqueness and lookup are + // enforced at the port layer (entadapter): emails are normalized to + // lower case on write and matched with EmailEqualFold (lower(email) = + // lower($1)) on read. The Unique() index below therefore enforces + // case-insensitive uniqueness because every stored value is normalized. + // This is equivalent to a lower(email) functional unique index without + // requiring an expression index, which ent codegen + AutoMigrate cannot + // emit for both SQLite (tests) and Postgres. field.String("email"). Unique(). NotEmpty(), - field.String("display_name"). - NotEmpty(), + // display_name is required (NOT NULL) but may be empty, matching the + // former raw-SQL store (display_name TEXT NOT NULL). Some identity + // providers omit a display name; the broker/user handlers fall back to + // the email in that case, so empty values must be storable. A stricter + // NotEmpty() here would reject those users and break the fallback. + field.String("display_name"), field.String("avatar_url"). Optional(), field.Enum("role"). @@ -55,14 +69,26 @@ func (User) Fields() []ent.Field { field.Time("last_login"). Optional(). Nillable(), + field.Time("last_seen"). + Optional(). + Nillable(), + } +} + +// Indexes of the User. +func (User) Indexes() []ent.Index { + return []ent.Index{ + // Supports the lastSeen sort option in ListUsers. + index.Fields("last_seen"), } } // Edges of the User. func (User) Edges() []ent.Edge { return []ent.Edge{ - edge.To("created_agents", Agent.Type), - edge.To("owned_agents", Agent.Type), + // Note: agent.created_by / agent.owner_id are polymorphic principal + // references (user or agent), so there is intentionally no + // created_agents / owned_agents edge back to Agent. See pkg/ent/schema/agent.go. edge.To("owned_groups", Group.Type), edge.From("memberships", GroupMembership.Type). Ref("user"), diff --git a/pkg/ent/schema/useraccesstoken.go b/pkg/ent/schema/useraccesstoken.go new file mode 100644 index 000000000..1fbc346f3 --- /dev/null +++ b/pkg/ent/schema/useraccesstoken.go @@ -0,0 +1,83 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package schema + +import ( + "time" + + "entgo.io/ent" + "entgo.io/ent/dialect/entsql" + "entgo.io/ent/schema" + "entgo.io/ent/schema/field" + "entgo.io/ent/schema/index" + "github.com/google/uuid" +) + +// UserAccessToken holds the schema definition for the UserAccessToken entity, +// mapping the legacy SQLite `user_access_tokens` table. +// +// user_id and project_id are required UUID foreign keys (modeled as plain +// columns, no Ent edges); scopes is a raw JSON string. key_hash is the unique +// lookup key and is marked Sensitive. +type UserAccessToken struct { + ent.Schema +} + +// Fields of the UserAccessToken. +func (UserAccessToken) Fields() []ent.Field { + return []ent.Field{ + field.UUID("id", uuid.UUID{}). + Default(uuid.New). + Immutable(), + field.UUID("user_id", uuid.UUID{}), + field.String("name"). + NotEmpty(), + field.String("prefix"). + NotEmpty(), + field.String("key_hash"). + Sensitive(). + Unique(). + NotEmpty(), + field.UUID("project_id", uuid.UUID{}), + field.String("scopes"). + NotEmpty(), + field.Bool("revoked"). + Default(false), + field.Time("expires_at"). + Optional(). + Nillable(), + field.Time("last_used"). + Optional(). + Nillable(), + field.Time("created"). + Default(time.Now). + Immutable(), + } +} + +// Indexes of the UserAccessToken. +func (UserAccessToken) Indexes() []ent.Index { + return []ent.Index{ + index.Fields("user_id"), + index.Fields("project_id"), + } +} + +// Annotations of the UserAccessToken. +func (UserAccessToken) Annotations() []schema.Annotation { + return []schema.Annotation{ + entsql.Annotation{Table: "user_access_tokens"}, + } +} diff --git a/pkg/ent/secret.go b/pkg/ent/secret.go new file mode 100644 index 000000000..fc3a3f34f --- /dev/null +++ b/pkg/ent/secret.go @@ -0,0 +1,264 @@ +// Code generated by ent, DO NOT EDIT. + +package ent + +import ( + "fmt" + "strings" + "time" + + "entgo.io/ent" + "entgo.io/ent/dialect/sql" + "github.com/GoogleCloudPlatform/scion/pkg/ent/secret" + "github.com/google/uuid" +) + +// Secret is the model entity for the Secret schema. +type Secret struct { + config `json:"-"` + // ID of the ent. + ID uuid.UUID `json:"id,omitempty"` + // Key holds the value of the "key" field. + Key string `json:"key,omitempty"` + // EncryptedValue holds the value of the "encrypted_value" field. + EncryptedValue string `json:"-"` + // SecretRef holds the value of the "secret_ref" field. + SecretRef string `json:"secret_ref,omitempty"` + // SecretType holds the value of the "secret_type" field. + SecretType secret.SecretType `json:"secret_type,omitempty"` + // Target holds the value of the "target" field. + Target string `json:"target,omitempty"` + // Scope holds the value of the "scope" field. + Scope string `json:"scope,omitempty"` + // ScopeID holds the value of the "scope_id" field. + ScopeID string `json:"scope_id,omitempty"` + // Description holds the value of the "description" field. + Description string `json:"description,omitempty"` + // InjectionMode holds the value of the "injection_mode" field. + InjectionMode secret.InjectionMode `json:"injection_mode,omitempty"` + // AllowProgeny holds the value of the "allow_progeny" field. + AllowProgeny bool `json:"allow_progeny,omitempty"` + // Version holds the value of the "version" field. + Version int `json:"version,omitempty"` + // CreatedBy holds the value of the "created_by" field. + CreatedBy string `json:"created_by,omitempty"` + // UpdatedBy holds the value of the "updated_by" field. + UpdatedBy string `json:"updated_by,omitempty"` + // Created holds the value of the "created" field. + Created time.Time `json:"created,omitempty"` + // Updated holds the value of the "updated" field. + Updated time.Time `json:"updated,omitempty"` + selectValues sql.SelectValues +} + +// scanValues returns the types for scanning values from sql.Rows. +func (*Secret) scanValues(columns []string) ([]any, error) { + values := make([]any, len(columns)) + for i := range columns { + switch columns[i] { + case secret.FieldAllowProgeny: + values[i] = new(sql.NullBool) + case secret.FieldVersion: + values[i] = new(sql.NullInt64) + case secret.FieldKey, secret.FieldEncryptedValue, secret.FieldSecretRef, secret.FieldSecretType, secret.FieldTarget, secret.FieldScope, secret.FieldScopeID, secret.FieldDescription, secret.FieldInjectionMode, secret.FieldCreatedBy, secret.FieldUpdatedBy: + values[i] = new(sql.NullString) + case secret.FieldCreated, secret.FieldUpdated: + values[i] = new(sql.NullTime) + case secret.FieldID: + values[i] = new(uuid.UUID) + default: + values[i] = new(sql.UnknownType) + } + } + return values, nil +} + +// assignValues assigns the values that were returned from sql.Rows (after scanning) +// to the Secret fields. +func (_m *Secret) assignValues(columns []string, values []any) error { + if m, n := len(values), len(columns); m < n { + return fmt.Errorf("mismatch number of scan values: %d != %d", m, n) + } + for i := range columns { + switch columns[i] { + case secret.FieldID: + if value, ok := values[i].(*uuid.UUID); !ok { + return fmt.Errorf("unexpected type %T for field id", values[i]) + } else if value != nil { + _m.ID = *value + } + case secret.FieldKey: + if value, ok := values[i].(*sql.NullString); !ok { + return fmt.Errorf("unexpected type %T for field key", values[i]) + } else if value.Valid { + _m.Key = value.String + } + case secret.FieldEncryptedValue: + if value, ok := values[i].(*sql.NullString); !ok { + return fmt.Errorf("unexpected type %T for field encrypted_value", values[i]) + } else if value.Valid { + _m.EncryptedValue = value.String + } + case secret.FieldSecretRef: + if value, ok := values[i].(*sql.NullString); !ok { + return fmt.Errorf("unexpected type %T for field secret_ref", values[i]) + } else if value.Valid { + _m.SecretRef = value.String + } + case secret.FieldSecretType: + if value, ok := values[i].(*sql.NullString); !ok { + return fmt.Errorf("unexpected type %T for field secret_type", values[i]) + } else if value.Valid { + _m.SecretType = secret.SecretType(value.String) + } + case secret.FieldTarget: + if value, ok := values[i].(*sql.NullString); !ok { + return fmt.Errorf("unexpected type %T for field target", values[i]) + } else if value.Valid { + _m.Target = value.String + } + case secret.FieldScope: + if value, ok := values[i].(*sql.NullString); !ok { + return fmt.Errorf("unexpected type %T for field scope", values[i]) + } else if value.Valid { + _m.Scope = value.String + } + case secret.FieldScopeID: + if value, ok := values[i].(*sql.NullString); !ok { + return fmt.Errorf("unexpected type %T for field scope_id", values[i]) + } else if value.Valid { + _m.ScopeID = value.String + } + case secret.FieldDescription: + if value, ok := values[i].(*sql.NullString); !ok { + return fmt.Errorf("unexpected type %T for field description", values[i]) + } else if value.Valid { + _m.Description = value.String + } + case secret.FieldInjectionMode: + if value, ok := values[i].(*sql.NullString); !ok { + return fmt.Errorf("unexpected type %T for field injection_mode", values[i]) + } else if value.Valid { + _m.InjectionMode = secret.InjectionMode(value.String) + } + case secret.FieldAllowProgeny: + if value, ok := values[i].(*sql.NullBool); !ok { + return fmt.Errorf("unexpected type %T for field allow_progeny", values[i]) + } else if value.Valid { + _m.AllowProgeny = value.Bool + } + case secret.FieldVersion: + if value, ok := values[i].(*sql.NullInt64); !ok { + return fmt.Errorf("unexpected type %T for field version", values[i]) + } else if value.Valid { + _m.Version = int(value.Int64) + } + case secret.FieldCreatedBy: + if value, ok := values[i].(*sql.NullString); !ok { + return fmt.Errorf("unexpected type %T for field created_by", values[i]) + } else if value.Valid { + _m.CreatedBy = value.String + } + case secret.FieldUpdatedBy: + if value, ok := values[i].(*sql.NullString); !ok { + return fmt.Errorf("unexpected type %T for field updated_by", values[i]) + } else if value.Valid { + _m.UpdatedBy = value.String + } + case secret.FieldCreated: + if value, ok := values[i].(*sql.NullTime); !ok { + return fmt.Errorf("unexpected type %T for field created", values[i]) + } else if value.Valid { + _m.Created = value.Time + } + case secret.FieldUpdated: + if value, ok := values[i].(*sql.NullTime); !ok { + return fmt.Errorf("unexpected type %T for field updated", values[i]) + } else if value.Valid { + _m.Updated = value.Time + } + default: + _m.selectValues.Set(columns[i], values[i]) + } + } + return nil +} + +// Value returns the ent.Value that was dynamically selected and assigned to the Secret. +// This includes values selected through modifiers, order, etc. +func (_m *Secret) Value(name string) (ent.Value, error) { + return _m.selectValues.Get(name) +} + +// Update returns a builder for updating this Secret. +// Note that you need to call Secret.Unwrap() before calling this method if this Secret +// was returned from a transaction, and the transaction was committed or rolled back. +func (_m *Secret) Update() *SecretUpdateOne { + return NewSecretClient(_m.config).UpdateOne(_m) +} + +// Unwrap unwraps the Secret entity that was returned from a transaction after it was closed, +// so that all future queries will be executed through the driver which created the transaction. +func (_m *Secret) Unwrap() *Secret { + _tx, ok := _m.config.driver.(*txDriver) + if !ok { + panic("ent: Secret is not a transactional entity") + } + _m.config.driver = _tx.drv + return _m +} + +// String implements the fmt.Stringer. +func (_m *Secret) String() string { + var builder strings.Builder + builder.WriteString("Secret(") + builder.WriteString(fmt.Sprintf("id=%v, ", _m.ID)) + builder.WriteString("key=") + builder.WriteString(_m.Key) + builder.WriteString(", ") + builder.WriteString("encrypted_value=") + builder.WriteString(", ") + builder.WriteString("secret_ref=") + builder.WriteString(_m.SecretRef) + builder.WriteString(", ") + builder.WriteString("secret_type=") + builder.WriteString(fmt.Sprintf("%v", _m.SecretType)) + builder.WriteString(", ") + builder.WriteString("target=") + builder.WriteString(_m.Target) + builder.WriteString(", ") + builder.WriteString("scope=") + builder.WriteString(_m.Scope) + builder.WriteString(", ") + builder.WriteString("scope_id=") + builder.WriteString(_m.ScopeID) + builder.WriteString(", ") + builder.WriteString("description=") + builder.WriteString(_m.Description) + builder.WriteString(", ") + builder.WriteString("injection_mode=") + builder.WriteString(fmt.Sprintf("%v", _m.InjectionMode)) + builder.WriteString(", ") + builder.WriteString("allow_progeny=") + builder.WriteString(fmt.Sprintf("%v", _m.AllowProgeny)) + builder.WriteString(", ") + builder.WriteString("version=") + builder.WriteString(fmt.Sprintf("%v", _m.Version)) + builder.WriteString(", ") + builder.WriteString("created_by=") + builder.WriteString(_m.CreatedBy) + builder.WriteString(", ") + builder.WriteString("updated_by=") + builder.WriteString(_m.UpdatedBy) + builder.WriteString(", ") + builder.WriteString("created=") + builder.WriteString(_m.Created.Format(time.ANSIC)) + builder.WriteString(", ") + builder.WriteString("updated=") + builder.WriteString(_m.Updated.Format(time.ANSIC)) + builder.WriteByte(')') + return builder.String() +} + +// Secrets is a parsable slice of Secret. +type Secrets []*Secret diff --git a/pkg/ent/secret/secret.go b/pkg/ent/secret/secret.go new file mode 100644 index 000000000..a5ce96f8a --- /dev/null +++ b/pkg/ent/secret/secret.go @@ -0,0 +1,236 @@ +// Code generated by ent, DO NOT EDIT. + +package secret + +import ( + "fmt" + "time" + + "entgo.io/ent/dialect/sql" + "github.com/google/uuid" +) + +const ( + // Label holds the string label denoting the secret type in the database. + Label = "secret" + // FieldID holds the string denoting the id field in the database. + FieldID = "id" + // FieldKey holds the string denoting the key field in the database. + FieldKey = "key" + // FieldEncryptedValue holds the string denoting the encrypted_value field in the database. + FieldEncryptedValue = "encrypted_value" + // FieldSecretRef holds the string denoting the secret_ref field in the database. + FieldSecretRef = "secret_ref" + // FieldSecretType holds the string denoting the secret_type field in the database. + FieldSecretType = "secret_type" + // FieldTarget holds the string denoting the target field in the database. + FieldTarget = "target" + // FieldScope holds the string denoting the scope field in the database. + FieldScope = "scope" + // FieldScopeID holds the string denoting the scope_id field in the database. + FieldScopeID = "scope_id" + // FieldDescription holds the string denoting the description field in the database. + FieldDescription = "description" + // FieldInjectionMode holds the string denoting the injection_mode field in the database. + FieldInjectionMode = "injection_mode" + // FieldAllowProgeny holds the string denoting the allow_progeny field in the database. + FieldAllowProgeny = "allow_progeny" + // FieldVersion holds the string denoting the version field in the database. + FieldVersion = "version" + // FieldCreatedBy holds the string denoting the created_by field in the database. + FieldCreatedBy = "created_by" + // FieldUpdatedBy holds the string denoting the updated_by field in the database. + FieldUpdatedBy = "updated_by" + // FieldCreated holds the string denoting the created field in the database. + FieldCreated = "created" + // FieldUpdated holds the string denoting the updated field in the database. + FieldUpdated = "updated" + // Table holds the table name of the secret in the database. + Table = "secrets" +) + +// Columns holds all SQL columns for secret fields. +var Columns = []string{ + FieldID, + FieldKey, + FieldEncryptedValue, + FieldSecretRef, + FieldSecretType, + FieldTarget, + FieldScope, + FieldScopeID, + FieldDescription, + FieldInjectionMode, + FieldAllowProgeny, + FieldVersion, + FieldCreatedBy, + FieldUpdatedBy, + FieldCreated, + FieldUpdated, +} + +// ValidColumn reports if the column name is valid (part of the table columns). +func ValidColumn(column string) bool { + for i := range Columns { + if column == Columns[i] { + return true + } + } + return false +} + +var ( + // KeyValidator is a validator for the "key" field. It is called by the builders before save. + KeyValidator func(string) error + // ScopeValidator is a validator for the "scope" field. It is called by the builders before save. + ScopeValidator func(string) error + // DefaultAllowProgeny holds the default value on creation for the "allow_progeny" field. + DefaultAllowProgeny bool + // DefaultVersion holds the default value on creation for the "version" field. + DefaultVersion int + // DefaultCreated holds the default value on creation for the "created" field. + DefaultCreated func() time.Time + // DefaultUpdated holds the default value on creation for the "updated" field. + DefaultUpdated func() time.Time + // UpdateDefaultUpdated holds the default value on update for the "updated" field. + UpdateDefaultUpdated func() time.Time + // DefaultID holds the default value on creation for the "id" field. + DefaultID func() uuid.UUID +) + +// SecretType defines the type for the "secret_type" enum field. +type SecretType string + +// SecretTypeEnvironment is the default value of the SecretType enum. +const DefaultSecretType = SecretTypeEnvironment + +// SecretType values. +const ( + SecretTypeEnvironment SecretType = "environment" + SecretTypeVariable SecretType = "variable" + SecretTypeFile SecretType = "file" + SecretTypeInternal SecretType = "internal" +) + +func (st SecretType) String() string { + return string(st) +} + +// SecretTypeValidator is a validator for the "secret_type" field enum values. It is called by the builders before save. +func SecretTypeValidator(st SecretType) error { + switch st { + case SecretTypeEnvironment, SecretTypeVariable, SecretTypeFile, SecretTypeInternal: + return nil + default: + return fmt.Errorf("secret: invalid enum value for secret_type field: %q", st) + } +} + +// InjectionMode defines the type for the "injection_mode" enum field. +type InjectionMode string + +// InjectionModeAsNeeded is the default value of the InjectionMode enum. +const DefaultInjectionMode = InjectionModeAsNeeded + +// InjectionMode values. +const ( + InjectionModeAlways InjectionMode = "always" + InjectionModeAsNeeded InjectionMode = "as_needed" +) + +func (im InjectionMode) String() string { + return string(im) +} + +// InjectionModeValidator is a validator for the "injection_mode" field enum values. It is called by the builders before save. +func InjectionModeValidator(im InjectionMode) error { + switch im { + case InjectionModeAlways, InjectionModeAsNeeded: + return nil + default: + return fmt.Errorf("secret: invalid enum value for injection_mode field: %q", im) + } +} + +// OrderOption defines the ordering options for the Secret queries. +type OrderOption func(*sql.Selector) + +// ByID orders the results by the id field. +func ByID(opts ...sql.OrderTermOption) OrderOption { + return sql.OrderByField(FieldID, opts...).ToFunc() +} + +// ByKey orders the results by the key field. +func ByKey(opts ...sql.OrderTermOption) OrderOption { + return sql.OrderByField(FieldKey, opts...).ToFunc() +} + +// ByEncryptedValue orders the results by the encrypted_value field. +func ByEncryptedValue(opts ...sql.OrderTermOption) OrderOption { + return sql.OrderByField(FieldEncryptedValue, opts...).ToFunc() +} + +// BySecretRef orders the results by the secret_ref field. +func BySecretRef(opts ...sql.OrderTermOption) OrderOption { + return sql.OrderByField(FieldSecretRef, opts...).ToFunc() +} + +// BySecretType orders the results by the secret_type field. +func BySecretType(opts ...sql.OrderTermOption) OrderOption { + return sql.OrderByField(FieldSecretType, opts...).ToFunc() +} + +// ByTarget orders the results by the target field. +func ByTarget(opts ...sql.OrderTermOption) OrderOption { + return sql.OrderByField(FieldTarget, opts...).ToFunc() +} + +// ByScope orders the results by the scope field. +func ByScope(opts ...sql.OrderTermOption) OrderOption { + return sql.OrderByField(FieldScope, opts...).ToFunc() +} + +// ByScopeID orders the results by the scope_id field. +func ByScopeID(opts ...sql.OrderTermOption) OrderOption { + return sql.OrderByField(FieldScopeID, opts...).ToFunc() +} + +// ByDescription orders the results by the description field. +func ByDescription(opts ...sql.OrderTermOption) OrderOption { + return sql.OrderByField(FieldDescription, opts...).ToFunc() +} + +// ByInjectionMode orders the results by the injection_mode field. +func ByInjectionMode(opts ...sql.OrderTermOption) OrderOption { + return sql.OrderByField(FieldInjectionMode, opts...).ToFunc() +} + +// ByAllowProgeny orders the results by the allow_progeny field. +func ByAllowProgeny(opts ...sql.OrderTermOption) OrderOption { + return sql.OrderByField(FieldAllowProgeny, opts...).ToFunc() +} + +// ByVersion orders the results by the version field. +func ByVersion(opts ...sql.OrderTermOption) OrderOption { + return sql.OrderByField(FieldVersion, opts...).ToFunc() +} + +// ByCreatedBy orders the results by the created_by field. +func ByCreatedBy(opts ...sql.OrderTermOption) OrderOption { + return sql.OrderByField(FieldCreatedBy, opts...).ToFunc() +} + +// ByUpdatedBy orders the results by the updated_by field. +func ByUpdatedBy(opts ...sql.OrderTermOption) OrderOption { + return sql.OrderByField(FieldUpdatedBy, opts...).ToFunc() +} + +// ByCreated orders the results by the created field. +func ByCreated(opts ...sql.OrderTermOption) OrderOption { + return sql.OrderByField(FieldCreated, opts...).ToFunc() +} + +// ByUpdated orders the results by the updated field. +func ByUpdated(opts ...sql.OrderTermOption) OrderOption { + return sql.OrderByField(FieldUpdated, opts...).ToFunc() +} diff --git a/pkg/ent/secret/where.go b/pkg/ent/secret/where.go new file mode 100644 index 000000000..6da76db84 --- /dev/null +++ b/pkg/ent/secret/where.go @@ -0,0 +1,941 @@ +// Code generated by ent, DO NOT EDIT. + +package secret + +import ( + "time" + + "entgo.io/ent/dialect/sql" + "github.com/GoogleCloudPlatform/scion/pkg/ent/predicate" + "github.com/google/uuid" +) + +// ID filters vertices based on their ID field. +func ID(id uuid.UUID) predicate.Secret { + return predicate.Secret(sql.FieldEQ(FieldID, id)) +} + +// IDEQ applies the EQ predicate on the ID field. +func IDEQ(id uuid.UUID) predicate.Secret { + return predicate.Secret(sql.FieldEQ(FieldID, id)) +} + +// IDNEQ applies the NEQ predicate on the ID field. +func IDNEQ(id uuid.UUID) predicate.Secret { + return predicate.Secret(sql.FieldNEQ(FieldID, id)) +} + +// IDIn applies the In predicate on the ID field. +func IDIn(ids ...uuid.UUID) predicate.Secret { + return predicate.Secret(sql.FieldIn(FieldID, ids...)) +} + +// IDNotIn applies the NotIn predicate on the ID field. +func IDNotIn(ids ...uuid.UUID) predicate.Secret { + return predicate.Secret(sql.FieldNotIn(FieldID, ids...)) +} + +// IDGT applies the GT predicate on the ID field. +func IDGT(id uuid.UUID) predicate.Secret { + return predicate.Secret(sql.FieldGT(FieldID, id)) +} + +// IDGTE applies the GTE predicate on the ID field. +func IDGTE(id uuid.UUID) predicate.Secret { + return predicate.Secret(sql.FieldGTE(FieldID, id)) +} + +// IDLT applies the LT predicate on the ID field. +func IDLT(id uuid.UUID) predicate.Secret { + return predicate.Secret(sql.FieldLT(FieldID, id)) +} + +// IDLTE applies the LTE predicate on the ID field. +func IDLTE(id uuid.UUID) predicate.Secret { + return predicate.Secret(sql.FieldLTE(FieldID, id)) +} + +// Key applies equality check predicate on the "key" field. It's identical to KeyEQ. +func Key(v string) predicate.Secret { + return predicate.Secret(sql.FieldEQ(FieldKey, v)) +} + +// EncryptedValue applies equality check predicate on the "encrypted_value" field. It's identical to EncryptedValueEQ. +func EncryptedValue(v string) predicate.Secret { + return predicate.Secret(sql.FieldEQ(FieldEncryptedValue, v)) +} + +// SecretRef applies equality check predicate on the "secret_ref" field. It's identical to SecretRefEQ. +func SecretRef(v string) predicate.Secret { + return predicate.Secret(sql.FieldEQ(FieldSecretRef, v)) +} + +// Target applies equality check predicate on the "target" field. It's identical to TargetEQ. +func Target(v string) predicate.Secret { + return predicate.Secret(sql.FieldEQ(FieldTarget, v)) +} + +// Scope applies equality check predicate on the "scope" field. It's identical to ScopeEQ. +func Scope(v string) predicate.Secret { + return predicate.Secret(sql.FieldEQ(FieldScope, v)) +} + +// ScopeID applies equality check predicate on the "scope_id" field. It's identical to ScopeIDEQ. +func ScopeID(v string) predicate.Secret { + return predicate.Secret(sql.FieldEQ(FieldScopeID, v)) +} + +// Description applies equality check predicate on the "description" field. It's identical to DescriptionEQ. +func Description(v string) predicate.Secret { + return predicate.Secret(sql.FieldEQ(FieldDescription, v)) +} + +// AllowProgeny applies equality check predicate on the "allow_progeny" field. It's identical to AllowProgenyEQ. +func AllowProgeny(v bool) predicate.Secret { + return predicate.Secret(sql.FieldEQ(FieldAllowProgeny, v)) +} + +// Version applies equality check predicate on the "version" field. It's identical to VersionEQ. +func Version(v int) predicate.Secret { + return predicate.Secret(sql.FieldEQ(FieldVersion, v)) +} + +// CreatedBy applies equality check predicate on the "created_by" field. It's identical to CreatedByEQ. +func CreatedBy(v string) predicate.Secret { + return predicate.Secret(sql.FieldEQ(FieldCreatedBy, v)) +} + +// UpdatedBy applies equality check predicate on the "updated_by" field. It's identical to UpdatedByEQ. +func UpdatedBy(v string) predicate.Secret { + return predicate.Secret(sql.FieldEQ(FieldUpdatedBy, v)) +} + +// Created applies equality check predicate on the "created" field. It's identical to CreatedEQ. +func Created(v time.Time) predicate.Secret { + return predicate.Secret(sql.FieldEQ(FieldCreated, v)) +} + +// Updated applies equality check predicate on the "updated" field. It's identical to UpdatedEQ. +func Updated(v time.Time) predicate.Secret { + return predicate.Secret(sql.FieldEQ(FieldUpdated, v)) +} + +// KeyEQ applies the EQ predicate on the "key" field. +func KeyEQ(v string) predicate.Secret { + return predicate.Secret(sql.FieldEQ(FieldKey, v)) +} + +// KeyNEQ applies the NEQ predicate on the "key" field. +func KeyNEQ(v string) predicate.Secret { + return predicate.Secret(sql.FieldNEQ(FieldKey, v)) +} + +// KeyIn applies the In predicate on the "key" field. +func KeyIn(vs ...string) predicate.Secret { + return predicate.Secret(sql.FieldIn(FieldKey, vs...)) +} + +// KeyNotIn applies the NotIn predicate on the "key" field. +func KeyNotIn(vs ...string) predicate.Secret { + return predicate.Secret(sql.FieldNotIn(FieldKey, vs...)) +} + +// KeyGT applies the GT predicate on the "key" field. +func KeyGT(v string) predicate.Secret { + return predicate.Secret(sql.FieldGT(FieldKey, v)) +} + +// KeyGTE applies the GTE predicate on the "key" field. +func KeyGTE(v string) predicate.Secret { + return predicate.Secret(sql.FieldGTE(FieldKey, v)) +} + +// KeyLT applies the LT predicate on the "key" field. +func KeyLT(v string) predicate.Secret { + return predicate.Secret(sql.FieldLT(FieldKey, v)) +} + +// KeyLTE applies the LTE predicate on the "key" field. +func KeyLTE(v string) predicate.Secret { + return predicate.Secret(sql.FieldLTE(FieldKey, v)) +} + +// KeyContains applies the Contains predicate on the "key" field. +func KeyContains(v string) predicate.Secret { + return predicate.Secret(sql.FieldContains(FieldKey, v)) +} + +// KeyHasPrefix applies the HasPrefix predicate on the "key" field. +func KeyHasPrefix(v string) predicate.Secret { + return predicate.Secret(sql.FieldHasPrefix(FieldKey, v)) +} + +// KeyHasSuffix applies the HasSuffix predicate on the "key" field. +func KeyHasSuffix(v string) predicate.Secret { + return predicate.Secret(sql.FieldHasSuffix(FieldKey, v)) +} + +// KeyEqualFold applies the EqualFold predicate on the "key" field. +func KeyEqualFold(v string) predicate.Secret { + return predicate.Secret(sql.FieldEqualFold(FieldKey, v)) +} + +// KeyContainsFold applies the ContainsFold predicate on the "key" field. +func KeyContainsFold(v string) predicate.Secret { + return predicate.Secret(sql.FieldContainsFold(FieldKey, v)) +} + +// EncryptedValueEQ applies the EQ predicate on the "encrypted_value" field. +func EncryptedValueEQ(v string) predicate.Secret { + return predicate.Secret(sql.FieldEQ(FieldEncryptedValue, v)) +} + +// EncryptedValueNEQ applies the NEQ predicate on the "encrypted_value" field. +func EncryptedValueNEQ(v string) predicate.Secret { + return predicate.Secret(sql.FieldNEQ(FieldEncryptedValue, v)) +} + +// EncryptedValueIn applies the In predicate on the "encrypted_value" field. +func EncryptedValueIn(vs ...string) predicate.Secret { + return predicate.Secret(sql.FieldIn(FieldEncryptedValue, vs...)) +} + +// EncryptedValueNotIn applies the NotIn predicate on the "encrypted_value" field. +func EncryptedValueNotIn(vs ...string) predicate.Secret { + return predicate.Secret(sql.FieldNotIn(FieldEncryptedValue, vs...)) +} + +// EncryptedValueGT applies the GT predicate on the "encrypted_value" field. +func EncryptedValueGT(v string) predicate.Secret { + return predicate.Secret(sql.FieldGT(FieldEncryptedValue, v)) +} + +// EncryptedValueGTE applies the GTE predicate on the "encrypted_value" field. +func EncryptedValueGTE(v string) predicate.Secret { + return predicate.Secret(sql.FieldGTE(FieldEncryptedValue, v)) +} + +// EncryptedValueLT applies the LT predicate on the "encrypted_value" field. +func EncryptedValueLT(v string) predicate.Secret { + return predicate.Secret(sql.FieldLT(FieldEncryptedValue, v)) +} + +// EncryptedValueLTE applies the LTE predicate on the "encrypted_value" field. +func EncryptedValueLTE(v string) predicate.Secret { + return predicate.Secret(sql.FieldLTE(FieldEncryptedValue, v)) +} + +// EncryptedValueContains applies the Contains predicate on the "encrypted_value" field. +func EncryptedValueContains(v string) predicate.Secret { + return predicate.Secret(sql.FieldContains(FieldEncryptedValue, v)) +} + +// EncryptedValueHasPrefix applies the HasPrefix predicate on the "encrypted_value" field. +func EncryptedValueHasPrefix(v string) predicate.Secret { + return predicate.Secret(sql.FieldHasPrefix(FieldEncryptedValue, v)) +} + +// EncryptedValueHasSuffix applies the HasSuffix predicate on the "encrypted_value" field. +func EncryptedValueHasSuffix(v string) predicate.Secret { + return predicate.Secret(sql.FieldHasSuffix(FieldEncryptedValue, v)) +} + +// EncryptedValueEqualFold applies the EqualFold predicate on the "encrypted_value" field. +func EncryptedValueEqualFold(v string) predicate.Secret { + return predicate.Secret(sql.FieldEqualFold(FieldEncryptedValue, v)) +} + +// EncryptedValueContainsFold applies the ContainsFold predicate on the "encrypted_value" field. +func EncryptedValueContainsFold(v string) predicate.Secret { + return predicate.Secret(sql.FieldContainsFold(FieldEncryptedValue, v)) +} + +// SecretRefEQ applies the EQ predicate on the "secret_ref" field. +func SecretRefEQ(v string) predicate.Secret { + return predicate.Secret(sql.FieldEQ(FieldSecretRef, v)) +} + +// SecretRefNEQ applies the NEQ predicate on the "secret_ref" field. +func SecretRefNEQ(v string) predicate.Secret { + return predicate.Secret(sql.FieldNEQ(FieldSecretRef, v)) +} + +// SecretRefIn applies the In predicate on the "secret_ref" field. +func SecretRefIn(vs ...string) predicate.Secret { + return predicate.Secret(sql.FieldIn(FieldSecretRef, vs...)) +} + +// SecretRefNotIn applies the NotIn predicate on the "secret_ref" field. +func SecretRefNotIn(vs ...string) predicate.Secret { + return predicate.Secret(sql.FieldNotIn(FieldSecretRef, vs...)) +} + +// SecretRefGT applies the GT predicate on the "secret_ref" field. +func SecretRefGT(v string) predicate.Secret { + return predicate.Secret(sql.FieldGT(FieldSecretRef, v)) +} + +// SecretRefGTE applies the GTE predicate on the "secret_ref" field. +func SecretRefGTE(v string) predicate.Secret { + return predicate.Secret(sql.FieldGTE(FieldSecretRef, v)) +} + +// SecretRefLT applies the LT predicate on the "secret_ref" field. +func SecretRefLT(v string) predicate.Secret { + return predicate.Secret(sql.FieldLT(FieldSecretRef, v)) +} + +// SecretRefLTE applies the LTE predicate on the "secret_ref" field. +func SecretRefLTE(v string) predicate.Secret { + return predicate.Secret(sql.FieldLTE(FieldSecretRef, v)) +} + +// SecretRefContains applies the Contains predicate on the "secret_ref" field. +func SecretRefContains(v string) predicate.Secret { + return predicate.Secret(sql.FieldContains(FieldSecretRef, v)) +} + +// SecretRefHasPrefix applies the HasPrefix predicate on the "secret_ref" field. +func SecretRefHasPrefix(v string) predicate.Secret { + return predicate.Secret(sql.FieldHasPrefix(FieldSecretRef, v)) +} + +// SecretRefHasSuffix applies the HasSuffix predicate on the "secret_ref" field. +func SecretRefHasSuffix(v string) predicate.Secret { + return predicate.Secret(sql.FieldHasSuffix(FieldSecretRef, v)) +} + +// SecretRefIsNil applies the IsNil predicate on the "secret_ref" field. +func SecretRefIsNil() predicate.Secret { + return predicate.Secret(sql.FieldIsNull(FieldSecretRef)) +} + +// SecretRefNotNil applies the NotNil predicate on the "secret_ref" field. +func SecretRefNotNil() predicate.Secret { + return predicate.Secret(sql.FieldNotNull(FieldSecretRef)) +} + +// SecretRefEqualFold applies the EqualFold predicate on the "secret_ref" field. +func SecretRefEqualFold(v string) predicate.Secret { + return predicate.Secret(sql.FieldEqualFold(FieldSecretRef, v)) +} + +// SecretRefContainsFold applies the ContainsFold predicate on the "secret_ref" field. +func SecretRefContainsFold(v string) predicate.Secret { + return predicate.Secret(sql.FieldContainsFold(FieldSecretRef, v)) +} + +// SecretTypeEQ applies the EQ predicate on the "secret_type" field. +func SecretTypeEQ(v SecretType) predicate.Secret { + return predicate.Secret(sql.FieldEQ(FieldSecretType, v)) +} + +// SecretTypeNEQ applies the NEQ predicate on the "secret_type" field. +func SecretTypeNEQ(v SecretType) predicate.Secret { + return predicate.Secret(sql.FieldNEQ(FieldSecretType, v)) +} + +// SecretTypeIn applies the In predicate on the "secret_type" field. +func SecretTypeIn(vs ...SecretType) predicate.Secret { + return predicate.Secret(sql.FieldIn(FieldSecretType, vs...)) +} + +// SecretTypeNotIn applies the NotIn predicate on the "secret_type" field. +func SecretTypeNotIn(vs ...SecretType) predicate.Secret { + return predicate.Secret(sql.FieldNotIn(FieldSecretType, vs...)) +} + +// TargetEQ applies the EQ predicate on the "target" field. +func TargetEQ(v string) predicate.Secret { + return predicate.Secret(sql.FieldEQ(FieldTarget, v)) +} + +// TargetNEQ applies the NEQ predicate on the "target" field. +func TargetNEQ(v string) predicate.Secret { + return predicate.Secret(sql.FieldNEQ(FieldTarget, v)) +} + +// TargetIn applies the In predicate on the "target" field. +func TargetIn(vs ...string) predicate.Secret { + return predicate.Secret(sql.FieldIn(FieldTarget, vs...)) +} + +// TargetNotIn applies the NotIn predicate on the "target" field. +func TargetNotIn(vs ...string) predicate.Secret { + return predicate.Secret(sql.FieldNotIn(FieldTarget, vs...)) +} + +// TargetGT applies the GT predicate on the "target" field. +func TargetGT(v string) predicate.Secret { + return predicate.Secret(sql.FieldGT(FieldTarget, v)) +} + +// TargetGTE applies the GTE predicate on the "target" field. +func TargetGTE(v string) predicate.Secret { + return predicate.Secret(sql.FieldGTE(FieldTarget, v)) +} + +// TargetLT applies the LT predicate on the "target" field. +func TargetLT(v string) predicate.Secret { + return predicate.Secret(sql.FieldLT(FieldTarget, v)) +} + +// TargetLTE applies the LTE predicate on the "target" field. +func TargetLTE(v string) predicate.Secret { + return predicate.Secret(sql.FieldLTE(FieldTarget, v)) +} + +// TargetContains applies the Contains predicate on the "target" field. +func TargetContains(v string) predicate.Secret { + return predicate.Secret(sql.FieldContains(FieldTarget, v)) +} + +// TargetHasPrefix applies the HasPrefix predicate on the "target" field. +func TargetHasPrefix(v string) predicate.Secret { + return predicate.Secret(sql.FieldHasPrefix(FieldTarget, v)) +} + +// TargetHasSuffix applies the HasSuffix predicate on the "target" field. +func TargetHasSuffix(v string) predicate.Secret { + return predicate.Secret(sql.FieldHasSuffix(FieldTarget, v)) +} + +// TargetIsNil applies the IsNil predicate on the "target" field. +func TargetIsNil() predicate.Secret { + return predicate.Secret(sql.FieldIsNull(FieldTarget)) +} + +// TargetNotNil applies the NotNil predicate on the "target" field. +func TargetNotNil() predicate.Secret { + return predicate.Secret(sql.FieldNotNull(FieldTarget)) +} + +// TargetEqualFold applies the EqualFold predicate on the "target" field. +func TargetEqualFold(v string) predicate.Secret { + return predicate.Secret(sql.FieldEqualFold(FieldTarget, v)) +} + +// TargetContainsFold applies the ContainsFold predicate on the "target" field. +func TargetContainsFold(v string) predicate.Secret { + return predicate.Secret(sql.FieldContainsFold(FieldTarget, v)) +} + +// ScopeEQ applies the EQ predicate on the "scope" field. +func ScopeEQ(v string) predicate.Secret { + return predicate.Secret(sql.FieldEQ(FieldScope, v)) +} + +// ScopeNEQ applies the NEQ predicate on the "scope" field. +func ScopeNEQ(v string) predicate.Secret { + return predicate.Secret(sql.FieldNEQ(FieldScope, v)) +} + +// ScopeIn applies the In predicate on the "scope" field. +func ScopeIn(vs ...string) predicate.Secret { + return predicate.Secret(sql.FieldIn(FieldScope, vs...)) +} + +// ScopeNotIn applies the NotIn predicate on the "scope" field. +func ScopeNotIn(vs ...string) predicate.Secret { + return predicate.Secret(sql.FieldNotIn(FieldScope, vs...)) +} + +// ScopeGT applies the GT predicate on the "scope" field. +func ScopeGT(v string) predicate.Secret { + return predicate.Secret(sql.FieldGT(FieldScope, v)) +} + +// ScopeGTE applies the GTE predicate on the "scope" field. +func ScopeGTE(v string) predicate.Secret { + return predicate.Secret(sql.FieldGTE(FieldScope, v)) +} + +// ScopeLT applies the LT predicate on the "scope" field. +func ScopeLT(v string) predicate.Secret { + return predicate.Secret(sql.FieldLT(FieldScope, v)) +} + +// ScopeLTE applies the LTE predicate on the "scope" field. +func ScopeLTE(v string) predicate.Secret { + return predicate.Secret(sql.FieldLTE(FieldScope, v)) +} + +// ScopeContains applies the Contains predicate on the "scope" field. +func ScopeContains(v string) predicate.Secret { + return predicate.Secret(sql.FieldContains(FieldScope, v)) +} + +// ScopeHasPrefix applies the HasPrefix predicate on the "scope" field. +func ScopeHasPrefix(v string) predicate.Secret { + return predicate.Secret(sql.FieldHasPrefix(FieldScope, v)) +} + +// ScopeHasSuffix applies the HasSuffix predicate on the "scope" field. +func ScopeHasSuffix(v string) predicate.Secret { + return predicate.Secret(sql.FieldHasSuffix(FieldScope, v)) +} + +// ScopeEqualFold applies the EqualFold predicate on the "scope" field. +func ScopeEqualFold(v string) predicate.Secret { + return predicate.Secret(sql.FieldEqualFold(FieldScope, v)) +} + +// ScopeContainsFold applies the ContainsFold predicate on the "scope" field. +func ScopeContainsFold(v string) predicate.Secret { + return predicate.Secret(sql.FieldContainsFold(FieldScope, v)) +} + +// ScopeIDEQ applies the EQ predicate on the "scope_id" field. +func ScopeIDEQ(v string) predicate.Secret { + return predicate.Secret(sql.FieldEQ(FieldScopeID, v)) +} + +// ScopeIDNEQ applies the NEQ predicate on the "scope_id" field. +func ScopeIDNEQ(v string) predicate.Secret { + return predicate.Secret(sql.FieldNEQ(FieldScopeID, v)) +} + +// ScopeIDIn applies the In predicate on the "scope_id" field. +func ScopeIDIn(vs ...string) predicate.Secret { + return predicate.Secret(sql.FieldIn(FieldScopeID, vs...)) +} + +// ScopeIDNotIn applies the NotIn predicate on the "scope_id" field. +func ScopeIDNotIn(vs ...string) predicate.Secret { + return predicate.Secret(sql.FieldNotIn(FieldScopeID, vs...)) +} + +// ScopeIDGT applies the GT predicate on the "scope_id" field. +func ScopeIDGT(v string) predicate.Secret { + return predicate.Secret(sql.FieldGT(FieldScopeID, v)) +} + +// ScopeIDGTE applies the GTE predicate on the "scope_id" field. +func ScopeIDGTE(v string) predicate.Secret { + return predicate.Secret(sql.FieldGTE(FieldScopeID, v)) +} + +// ScopeIDLT applies the LT predicate on the "scope_id" field. +func ScopeIDLT(v string) predicate.Secret { + return predicate.Secret(sql.FieldLT(FieldScopeID, v)) +} + +// ScopeIDLTE applies the LTE predicate on the "scope_id" field. +func ScopeIDLTE(v string) predicate.Secret { + return predicate.Secret(sql.FieldLTE(FieldScopeID, v)) +} + +// ScopeIDContains applies the Contains predicate on the "scope_id" field. +func ScopeIDContains(v string) predicate.Secret { + return predicate.Secret(sql.FieldContains(FieldScopeID, v)) +} + +// ScopeIDHasPrefix applies the HasPrefix predicate on the "scope_id" field. +func ScopeIDHasPrefix(v string) predicate.Secret { + return predicate.Secret(sql.FieldHasPrefix(FieldScopeID, v)) +} + +// ScopeIDHasSuffix applies the HasSuffix predicate on the "scope_id" field. +func ScopeIDHasSuffix(v string) predicate.Secret { + return predicate.Secret(sql.FieldHasSuffix(FieldScopeID, v)) +} + +// ScopeIDEqualFold applies the EqualFold predicate on the "scope_id" field. +func ScopeIDEqualFold(v string) predicate.Secret { + return predicate.Secret(sql.FieldEqualFold(FieldScopeID, v)) +} + +// ScopeIDContainsFold applies the ContainsFold predicate on the "scope_id" field. +func ScopeIDContainsFold(v string) predicate.Secret { + return predicate.Secret(sql.FieldContainsFold(FieldScopeID, v)) +} + +// DescriptionEQ applies the EQ predicate on the "description" field. +func DescriptionEQ(v string) predicate.Secret { + return predicate.Secret(sql.FieldEQ(FieldDescription, v)) +} + +// DescriptionNEQ applies the NEQ predicate on the "description" field. +func DescriptionNEQ(v string) predicate.Secret { + return predicate.Secret(sql.FieldNEQ(FieldDescription, v)) +} + +// DescriptionIn applies the In predicate on the "description" field. +func DescriptionIn(vs ...string) predicate.Secret { + return predicate.Secret(sql.FieldIn(FieldDescription, vs...)) +} + +// DescriptionNotIn applies the NotIn predicate on the "description" field. +func DescriptionNotIn(vs ...string) predicate.Secret { + return predicate.Secret(sql.FieldNotIn(FieldDescription, vs...)) +} + +// DescriptionGT applies the GT predicate on the "description" field. +func DescriptionGT(v string) predicate.Secret { + return predicate.Secret(sql.FieldGT(FieldDescription, v)) +} + +// DescriptionGTE applies the GTE predicate on the "description" field. +func DescriptionGTE(v string) predicate.Secret { + return predicate.Secret(sql.FieldGTE(FieldDescription, v)) +} + +// DescriptionLT applies the LT predicate on the "description" field. +func DescriptionLT(v string) predicate.Secret { + return predicate.Secret(sql.FieldLT(FieldDescription, v)) +} + +// DescriptionLTE applies the LTE predicate on the "description" field. +func DescriptionLTE(v string) predicate.Secret { + return predicate.Secret(sql.FieldLTE(FieldDescription, v)) +} + +// DescriptionContains applies the Contains predicate on the "description" field. +func DescriptionContains(v string) predicate.Secret { + return predicate.Secret(sql.FieldContains(FieldDescription, v)) +} + +// DescriptionHasPrefix applies the HasPrefix predicate on the "description" field. +func DescriptionHasPrefix(v string) predicate.Secret { + return predicate.Secret(sql.FieldHasPrefix(FieldDescription, v)) +} + +// DescriptionHasSuffix applies the HasSuffix predicate on the "description" field. +func DescriptionHasSuffix(v string) predicate.Secret { + return predicate.Secret(sql.FieldHasSuffix(FieldDescription, v)) +} + +// DescriptionIsNil applies the IsNil predicate on the "description" field. +func DescriptionIsNil() predicate.Secret { + return predicate.Secret(sql.FieldIsNull(FieldDescription)) +} + +// DescriptionNotNil applies the NotNil predicate on the "description" field. +func DescriptionNotNil() predicate.Secret { + return predicate.Secret(sql.FieldNotNull(FieldDescription)) +} + +// DescriptionEqualFold applies the EqualFold predicate on the "description" field. +func DescriptionEqualFold(v string) predicate.Secret { + return predicate.Secret(sql.FieldEqualFold(FieldDescription, v)) +} + +// DescriptionContainsFold applies the ContainsFold predicate on the "description" field. +func DescriptionContainsFold(v string) predicate.Secret { + return predicate.Secret(sql.FieldContainsFold(FieldDescription, v)) +} + +// InjectionModeEQ applies the EQ predicate on the "injection_mode" field. +func InjectionModeEQ(v InjectionMode) predicate.Secret { + return predicate.Secret(sql.FieldEQ(FieldInjectionMode, v)) +} + +// InjectionModeNEQ applies the NEQ predicate on the "injection_mode" field. +func InjectionModeNEQ(v InjectionMode) predicate.Secret { + return predicate.Secret(sql.FieldNEQ(FieldInjectionMode, v)) +} + +// InjectionModeIn applies the In predicate on the "injection_mode" field. +func InjectionModeIn(vs ...InjectionMode) predicate.Secret { + return predicate.Secret(sql.FieldIn(FieldInjectionMode, vs...)) +} + +// InjectionModeNotIn applies the NotIn predicate on the "injection_mode" field. +func InjectionModeNotIn(vs ...InjectionMode) predicate.Secret { + return predicate.Secret(sql.FieldNotIn(FieldInjectionMode, vs...)) +} + +// AllowProgenyEQ applies the EQ predicate on the "allow_progeny" field. +func AllowProgenyEQ(v bool) predicate.Secret { + return predicate.Secret(sql.FieldEQ(FieldAllowProgeny, v)) +} + +// AllowProgenyNEQ applies the NEQ predicate on the "allow_progeny" field. +func AllowProgenyNEQ(v bool) predicate.Secret { + return predicate.Secret(sql.FieldNEQ(FieldAllowProgeny, v)) +} + +// VersionEQ applies the EQ predicate on the "version" field. +func VersionEQ(v int) predicate.Secret { + return predicate.Secret(sql.FieldEQ(FieldVersion, v)) +} + +// VersionNEQ applies the NEQ predicate on the "version" field. +func VersionNEQ(v int) predicate.Secret { + return predicate.Secret(sql.FieldNEQ(FieldVersion, v)) +} + +// VersionIn applies the In predicate on the "version" field. +func VersionIn(vs ...int) predicate.Secret { + return predicate.Secret(sql.FieldIn(FieldVersion, vs...)) +} + +// VersionNotIn applies the NotIn predicate on the "version" field. +func VersionNotIn(vs ...int) predicate.Secret { + return predicate.Secret(sql.FieldNotIn(FieldVersion, vs...)) +} + +// VersionGT applies the GT predicate on the "version" field. +func VersionGT(v int) predicate.Secret { + return predicate.Secret(sql.FieldGT(FieldVersion, v)) +} + +// VersionGTE applies the GTE predicate on the "version" field. +func VersionGTE(v int) predicate.Secret { + return predicate.Secret(sql.FieldGTE(FieldVersion, v)) +} + +// VersionLT applies the LT predicate on the "version" field. +func VersionLT(v int) predicate.Secret { + return predicate.Secret(sql.FieldLT(FieldVersion, v)) +} + +// VersionLTE applies the LTE predicate on the "version" field. +func VersionLTE(v int) predicate.Secret { + return predicate.Secret(sql.FieldLTE(FieldVersion, v)) +} + +// CreatedByEQ applies the EQ predicate on the "created_by" field. +func CreatedByEQ(v string) predicate.Secret { + return predicate.Secret(sql.FieldEQ(FieldCreatedBy, v)) +} + +// CreatedByNEQ applies the NEQ predicate on the "created_by" field. +func CreatedByNEQ(v string) predicate.Secret { + return predicate.Secret(sql.FieldNEQ(FieldCreatedBy, v)) +} + +// CreatedByIn applies the In predicate on the "created_by" field. +func CreatedByIn(vs ...string) predicate.Secret { + return predicate.Secret(sql.FieldIn(FieldCreatedBy, vs...)) +} + +// CreatedByNotIn applies the NotIn predicate on the "created_by" field. +func CreatedByNotIn(vs ...string) predicate.Secret { + return predicate.Secret(sql.FieldNotIn(FieldCreatedBy, vs...)) +} + +// CreatedByGT applies the GT predicate on the "created_by" field. +func CreatedByGT(v string) predicate.Secret { + return predicate.Secret(sql.FieldGT(FieldCreatedBy, v)) +} + +// CreatedByGTE applies the GTE predicate on the "created_by" field. +func CreatedByGTE(v string) predicate.Secret { + return predicate.Secret(sql.FieldGTE(FieldCreatedBy, v)) +} + +// CreatedByLT applies the LT predicate on the "created_by" field. +func CreatedByLT(v string) predicate.Secret { + return predicate.Secret(sql.FieldLT(FieldCreatedBy, v)) +} + +// CreatedByLTE applies the LTE predicate on the "created_by" field. +func CreatedByLTE(v string) predicate.Secret { + return predicate.Secret(sql.FieldLTE(FieldCreatedBy, v)) +} + +// CreatedByContains applies the Contains predicate on the "created_by" field. +func CreatedByContains(v string) predicate.Secret { + return predicate.Secret(sql.FieldContains(FieldCreatedBy, v)) +} + +// CreatedByHasPrefix applies the HasPrefix predicate on the "created_by" field. +func CreatedByHasPrefix(v string) predicate.Secret { + return predicate.Secret(sql.FieldHasPrefix(FieldCreatedBy, v)) +} + +// CreatedByHasSuffix applies the HasSuffix predicate on the "created_by" field. +func CreatedByHasSuffix(v string) predicate.Secret { + return predicate.Secret(sql.FieldHasSuffix(FieldCreatedBy, v)) +} + +// CreatedByIsNil applies the IsNil predicate on the "created_by" field. +func CreatedByIsNil() predicate.Secret { + return predicate.Secret(sql.FieldIsNull(FieldCreatedBy)) +} + +// CreatedByNotNil applies the NotNil predicate on the "created_by" field. +func CreatedByNotNil() predicate.Secret { + return predicate.Secret(sql.FieldNotNull(FieldCreatedBy)) +} + +// CreatedByEqualFold applies the EqualFold predicate on the "created_by" field. +func CreatedByEqualFold(v string) predicate.Secret { + return predicate.Secret(sql.FieldEqualFold(FieldCreatedBy, v)) +} + +// CreatedByContainsFold applies the ContainsFold predicate on the "created_by" field. +func CreatedByContainsFold(v string) predicate.Secret { + return predicate.Secret(sql.FieldContainsFold(FieldCreatedBy, v)) +} + +// UpdatedByEQ applies the EQ predicate on the "updated_by" field. +func UpdatedByEQ(v string) predicate.Secret { + return predicate.Secret(sql.FieldEQ(FieldUpdatedBy, v)) +} + +// UpdatedByNEQ applies the NEQ predicate on the "updated_by" field. +func UpdatedByNEQ(v string) predicate.Secret { + return predicate.Secret(sql.FieldNEQ(FieldUpdatedBy, v)) +} + +// UpdatedByIn applies the In predicate on the "updated_by" field. +func UpdatedByIn(vs ...string) predicate.Secret { + return predicate.Secret(sql.FieldIn(FieldUpdatedBy, vs...)) +} + +// UpdatedByNotIn applies the NotIn predicate on the "updated_by" field. +func UpdatedByNotIn(vs ...string) predicate.Secret { + return predicate.Secret(sql.FieldNotIn(FieldUpdatedBy, vs...)) +} + +// UpdatedByGT applies the GT predicate on the "updated_by" field. +func UpdatedByGT(v string) predicate.Secret { + return predicate.Secret(sql.FieldGT(FieldUpdatedBy, v)) +} + +// UpdatedByGTE applies the GTE predicate on the "updated_by" field. +func UpdatedByGTE(v string) predicate.Secret { + return predicate.Secret(sql.FieldGTE(FieldUpdatedBy, v)) +} + +// UpdatedByLT applies the LT predicate on the "updated_by" field. +func UpdatedByLT(v string) predicate.Secret { + return predicate.Secret(sql.FieldLT(FieldUpdatedBy, v)) +} + +// UpdatedByLTE applies the LTE predicate on the "updated_by" field. +func UpdatedByLTE(v string) predicate.Secret { + return predicate.Secret(sql.FieldLTE(FieldUpdatedBy, v)) +} + +// UpdatedByContains applies the Contains predicate on the "updated_by" field. +func UpdatedByContains(v string) predicate.Secret { + return predicate.Secret(sql.FieldContains(FieldUpdatedBy, v)) +} + +// UpdatedByHasPrefix applies the HasPrefix predicate on the "updated_by" field. +func UpdatedByHasPrefix(v string) predicate.Secret { + return predicate.Secret(sql.FieldHasPrefix(FieldUpdatedBy, v)) +} + +// UpdatedByHasSuffix applies the HasSuffix predicate on the "updated_by" field. +func UpdatedByHasSuffix(v string) predicate.Secret { + return predicate.Secret(sql.FieldHasSuffix(FieldUpdatedBy, v)) +} + +// UpdatedByIsNil applies the IsNil predicate on the "updated_by" field. +func UpdatedByIsNil() predicate.Secret { + return predicate.Secret(sql.FieldIsNull(FieldUpdatedBy)) +} + +// UpdatedByNotNil applies the NotNil predicate on the "updated_by" field. +func UpdatedByNotNil() predicate.Secret { + return predicate.Secret(sql.FieldNotNull(FieldUpdatedBy)) +} + +// UpdatedByEqualFold applies the EqualFold predicate on the "updated_by" field. +func UpdatedByEqualFold(v string) predicate.Secret { + return predicate.Secret(sql.FieldEqualFold(FieldUpdatedBy, v)) +} + +// UpdatedByContainsFold applies the ContainsFold predicate on the "updated_by" field. +func UpdatedByContainsFold(v string) predicate.Secret { + return predicate.Secret(sql.FieldContainsFold(FieldUpdatedBy, v)) +} + +// CreatedEQ applies the EQ predicate on the "created" field. +func CreatedEQ(v time.Time) predicate.Secret { + return predicate.Secret(sql.FieldEQ(FieldCreated, v)) +} + +// CreatedNEQ applies the NEQ predicate on the "created" field. +func CreatedNEQ(v time.Time) predicate.Secret { + return predicate.Secret(sql.FieldNEQ(FieldCreated, v)) +} + +// CreatedIn applies the In predicate on the "created" field. +func CreatedIn(vs ...time.Time) predicate.Secret { + return predicate.Secret(sql.FieldIn(FieldCreated, vs...)) +} + +// CreatedNotIn applies the NotIn predicate on the "created" field. +func CreatedNotIn(vs ...time.Time) predicate.Secret { + return predicate.Secret(sql.FieldNotIn(FieldCreated, vs...)) +} + +// CreatedGT applies the GT predicate on the "created" field. +func CreatedGT(v time.Time) predicate.Secret { + return predicate.Secret(sql.FieldGT(FieldCreated, v)) +} + +// CreatedGTE applies the GTE predicate on the "created" field. +func CreatedGTE(v time.Time) predicate.Secret { + return predicate.Secret(sql.FieldGTE(FieldCreated, v)) +} + +// CreatedLT applies the LT predicate on the "created" field. +func CreatedLT(v time.Time) predicate.Secret { + return predicate.Secret(sql.FieldLT(FieldCreated, v)) +} + +// CreatedLTE applies the LTE predicate on the "created" field. +func CreatedLTE(v time.Time) predicate.Secret { + return predicate.Secret(sql.FieldLTE(FieldCreated, v)) +} + +// UpdatedEQ applies the EQ predicate on the "updated" field. +func UpdatedEQ(v time.Time) predicate.Secret { + return predicate.Secret(sql.FieldEQ(FieldUpdated, v)) +} + +// UpdatedNEQ applies the NEQ predicate on the "updated" field. +func UpdatedNEQ(v time.Time) predicate.Secret { + return predicate.Secret(sql.FieldNEQ(FieldUpdated, v)) +} + +// UpdatedIn applies the In predicate on the "updated" field. +func UpdatedIn(vs ...time.Time) predicate.Secret { + return predicate.Secret(sql.FieldIn(FieldUpdated, vs...)) +} + +// UpdatedNotIn applies the NotIn predicate on the "updated" field. +func UpdatedNotIn(vs ...time.Time) predicate.Secret { + return predicate.Secret(sql.FieldNotIn(FieldUpdated, vs...)) +} + +// UpdatedGT applies the GT predicate on the "updated" field. +func UpdatedGT(v time.Time) predicate.Secret { + return predicate.Secret(sql.FieldGT(FieldUpdated, v)) +} + +// UpdatedGTE applies the GTE predicate on the "updated" field. +func UpdatedGTE(v time.Time) predicate.Secret { + return predicate.Secret(sql.FieldGTE(FieldUpdated, v)) +} + +// UpdatedLT applies the LT predicate on the "updated" field. +func UpdatedLT(v time.Time) predicate.Secret { + return predicate.Secret(sql.FieldLT(FieldUpdated, v)) +} + +// UpdatedLTE applies the LTE predicate on the "updated" field. +func UpdatedLTE(v time.Time) predicate.Secret { + return predicate.Secret(sql.FieldLTE(FieldUpdated, v)) +} + +// And groups predicates with the AND operator between them. +func And(predicates ...predicate.Secret) predicate.Secret { + return predicate.Secret(sql.AndPredicates(predicates...)) +} + +// Or groups predicates with the OR operator between them. +func Or(predicates ...predicate.Secret) predicate.Secret { + return predicate.Secret(sql.OrPredicates(predicates...)) +} + +// Not applies the not operator on the given predicate. +func Not(p predicate.Secret) predicate.Secret { + return predicate.Secret(sql.NotPredicates(p)) +} diff --git a/pkg/ent/secret_create.go b/pkg/ent/secret_create.go new file mode 100644 index 000000000..01ff9ccd7 --- /dev/null +++ b/pkg/ent/secret_create.go @@ -0,0 +1,1454 @@ +// Code generated by ent, DO NOT EDIT. + +package ent + +import ( + "context" + "errors" + "fmt" + "time" + + "entgo.io/ent/dialect" + "entgo.io/ent/dialect/sql" + "entgo.io/ent/dialect/sql/sqlgraph" + "entgo.io/ent/schema/field" + "github.com/GoogleCloudPlatform/scion/pkg/ent/secret" + "github.com/google/uuid" +) + +// SecretCreate is the builder for creating a Secret entity. +type SecretCreate struct { + config + mutation *SecretMutation + hooks []Hook + conflict []sql.ConflictOption +} + +// SetKey sets the "key" field. +func (_c *SecretCreate) SetKey(v string) *SecretCreate { + _c.mutation.SetKey(v) + return _c +} + +// SetEncryptedValue sets the "encrypted_value" field. +func (_c *SecretCreate) SetEncryptedValue(v string) *SecretCreate { + _c.mutation.SetEncryptedValue(v) + return _c +} + +// SetSecretRef sets the "secret_ref" field. +func (_c *SecretCreate) SetSecretRef(v string) *SecretCreate { + _c.mutation.SetSecretRef(v) + return _c +} + +// SetNillableSecretRef sets the "secret_ref" field if the given value is not nil. +func (_c *SecretCreate) SetNillableSecretRef(v *string) *SecretCreate { + if v != nil { + _c.SetSecretRef(*v) + } + return _c +} + +// SetSecretType sets the "secret_type" field. +func (_c *SecretCreate) SetSecretType(v secret.SecretType) *SecretCreate { + _c.mutation.SetSecretType(v) + return _c +} + +// SetNillableSecretType sets the "secret_type" field if the given value is not nil. +func (_c *SecretCreate) SetNillableSecretType(v *secret.SecretType) *SecretCreate { + if v != nil { + _c.SetSecretType(*v) + } + return _c +} + +// SetTarget sets the "target" field. +func (_c *SecretCreate) SetTarget(v string) *SecretCreate { + _c.mutation.SetTarget(v) + return _c +} + +// SetNillableTarget sets the "target" field if the given value is not nil. +func (_c *SecretCreate) SetNillableTarget(v *string) *SecretCreate { + if v != nil { + _c.SetTarget(*v) + } + return _c +} + +// SetScope sets the "scope" field. +func (_c *SecretCreate) SetScope(v string) *SecretCreate { + _c.mutation.SetScope(v) + return _c +} + +// SetScopeID sets the "scope_id" field. +func (_c *SecretCreate) SetScopeID(v string) *SecretCreate { + _c.mutation.SetScopeID(v) + return _c +} + +// SetDescription sets the "description" field. +func (_c *SecretCreate) SetDescription(v string) *SecretCreate { + _c.mutation.SetDescription(v) + return _c +} + +// SetNillableDescription sets the "description" field if the given value is not nil. +func (_c *SecretCreate) SetNillableDescription(v *string) *SecretCreate { + if v != nil { + _c.SetDescription(*v) + } + return _c +} + +// SetInjectionMode sets the "injection_mode" field. +func (_c *SecretCreate) SetInjectionMode(v secret.InjectionMode) *SecretCreate { + _c.mutation.SetInjectionMode(v) + return _c +} + +// SetNillableInjectionMode sets the "injection_mode" field if the given value is not nil. +func (_c *SecretCreate) SetNillableInjectionMode(v *secret.InjectionMode) *SecretCreate { + if v != nil { + _c.SetInjectionMode(*v) + } + return _c +} + +// SetAllowProgeny sets the "allow_progeny" field. +func (_c *SecretCreate) SetAllowProgeny(v bool) *SecretCreate { + _c.mutation.SetAllowProgeny(v) + return _c +} + +// SetNillableAllowProgeny sets the "allow_progeny" field if the given value is not nil. +func (_c *SecretCreate) SetNillableAllowProgeny(v *bool) *SecretCreate { + if v != nil { + _c.SetAllowProgeny(*v) + } + return _c +} + +// SetVersion sets the "version" field. +func (_c *SecretCreate) SetVersion(v int) *SecretCreate { + _c.mutation.SetVersion(v) + return _c +} + +// SetNillableVersion sets the "version" field if the given value is not nil. +func (_c *SecretCreate) SetNillableVersion(v *int) *SecretCreate { + if v != nil { + _c.SetVersion(*v) + } + return _c +} + +// SetCreatedBy sets the "created_by" field. +func (_c *SecretCreate) SetCreatedBy(v string) *SecretCreate { + _c.mutation.SetCreatedBy(v) + return _c +} + +// SetNillableCreatedBy sets the "created_by" field if the given value is not nil. +func (_c *SecretCreate) SetNillableCreatedBy(v *string) *SecretCreate { + if v != nil { + _c.SetCreatedBy(*v) + } + return _c +} + +// SetUpdatedBy sets the "updated_by" field. +func (_c *SecretCreate) SetUpdatedBy(v string) *SecretCreate { + _c.mutation.SetUpdatedBy(v) + return _c +} + +// SetNillableUpdatedBy sets the "updated_by" field if the given value is not nil. +func (_c *SecretCreate) SetNillableUpdatedBy(v *string) *SecretCreate { + if v != nil { + _c.SetUpdatedBy(*v) + } + return _c +} + +// SetCreated sets the "created" field. +func (_c *SecretCreate) SetCreated(v time.Time) *SecretCreate { + _c.mutation.SetCreated(v) + return _c +} + +// SetNillableCreated sets the "created" field if the given value is not nil. +func (_c *SecretCreate) SetNillableCreated(v *time.Time) *SecretCreate { + if v != nil { + _c.SetCreated(*v) + } + return _c +} + +// SetUpdated sets the "updated" field. +func (_c *SecretCreate) SetUpdated(v time.Time) *SecretCreate { + _c.mutation.SetUpdated(v) + return _c +} + +// SetNillableUpdated sets the "updated" field if the given value is not nil. +func (_c *SecretCreate) SetNillableUpdated(v *time.Time) *SecretCreate { + if v != nil { + _c.SetUpdated(*v) + } + return _c +} + +// SetID sets the "id" field. +func (_c *SecretCreate) SetID(v uuid.UUID) *SecretCreate { + _c.mutation.SetID(v) + return _c +} + +// SetNillableID sets the "id" field if the given value is not nil. +func (_c *SecretCreate) SetNillableID(v *uuid.UUID) *SecretCreate { + if v != nil { + _c.SetID(*v) + } + return _c +} + +// Mutation returns the SecretMutation object of the builder. +func (_c *SecretCreate) Mutation() *SecretMutation { + return _c.mutation +} + +// Save creates the Secret in the database. +func (_c *SecretCreate) Save(ctx context.Context) (*Secret, error) { + _c.defaults() + return withHooks(ctx, _c.sqlSave, _c.mutation, _c.hooks) +} + +// SaveX calls Save and panics if Save returns an error. +func (_c *SecretCreate) SaveX(ctx context.Context) *Secret { + v, err := _c.Save(ctx) + if err != nil { + panic(err) + } + return v +} + +// Exec executes the query. +func (_c *SecretCreate) Exec(ctx context.Context) error { + _, err := _c.Save(ctx) + return err +} + +// ExecX is like Exec, but panics if an error occurs. +func (_c *SecretCreate) ExecX(ctx context.Context) { + if err := _c.Exec(ctx); err != nil { + panic(err) + } +} + +// defaults sets the default values of the builder before save. +func (_c *SecretCreate) defaults() { + if _, ok := _c.mutation.SecretType(); !ok { + v := secret.DefaultSecretType + _c.mutation.SetSecretType(v) + } + if _, ok := _c.mutation.InjectionMode(); !ok { + v := secret.DefaultInjectionMode + _c.mutation.SetInjectionMode(v) + } + if _, ok := _c.mutation.AllowProgeny(); !ok { + v := secret.DefaultAllowProgeny + _c.mutation.SetAllowProgeny(v) + } + if _, ok := _c.mutation.Version(); !ok { + v := secret.DefaultVersion + _c.mutation.SetVersion(v) + } + if _, ok := _c.mutation.Created(); !ok { + v := secret.DefaultCreated() + _c.mutation.SetCreated(v) + } + if _, ok := _c.mutation.Updated(); !ok { + v := secret.DefaultUpdated() + _c.mutation.SetUpdated(v) + } + if _, ok := _c.mutation.ID(); !ok { + v := secret.DefaultID() + _c.mutation.SetID(v) + } +} + +// check runs all checks and user-defined validators on the builder. +func (_c *SecretCreate) check() error { + if _, ok := _c.mutation.Key(); !ok { + return &ValidationError{Name: "key", err: errors.New(`ent: missing required field "Secret.key"`)} + } + if v, ok := _c.mutation.Key(); ok { + if err := secret.KeyValidator(v); err != nil { + return &ValidationError{Name: "key", err: fmt.Errorf(`ent: validator failed for field "Secret.key": %w`, err)} + } + } + if _, ok := _c.mutation.EncryptedValue(); !ok { + return &ValidationError{Name: "encrypted_value", err: errors.New(`ent: missing required field "Secret.encrypted_value"`)} + } + if _, ok := _c.mutation.SecretType(); !ok { + return &ValidationError{Name: "secret_type", err: errors.New(`ent: missing required field "Secret.secret_type"`)} + } + if v, ok := _c.mutation.SecretType(); ok { + if err := secret.SecretTypeValidator(v); err != nil { + return &ValidationError{Name: "secret_type", err: fmt.Errorf(`ent: validator failed for field "Secret.secret_type": %w`, err)} + } + } + if _, ok := _c.mutation.Scope(); !ok { + return &ValidationError{Name: "scope", err: errors.New(`ent: missing required field "Secret.scope"`)} + } + if v, ok := _c.mutation.Scope(); ok { + if err := secret.ScopeValidator(v); err != nil { + return &ValidationError{Name: "scope", err: fmt.Errorf(`ent: validator failed for field "Secret.scope": %w`, err)} + } + } + if _, ok := _c.mutation.ScopeID(); !ok { + return &ValidationError{Name: "scope_id", err: errors.New(`ent: missing required field "Secret.scope_id"`)} + } + if _, ok := _c.mutation.InjectionMode(); !ok { + return &ValidationError{Name: "injection_mode", err: errors.New(`ent: missing required field "Secret.injection_mode"`)} + } + if v, ok := _c.mutation.InjectionMode(); ok { + if err := secret.InjectionModeValidator(v); err != nil { + return &ValidationError{Name: "injection_mode", err: fmt.Errorf(`ent: validator failed for field "Secret.injection_mode": %w`, err)} + } + } + if _, ok := _c.mutation.AllowProgeny(); !ok { + return &ValidationError{Name: "allow_progeny", err: errors.New(`ent: missing required field "Secret.allow_progeny"`)} + } + if _, ok := _c.mutation.Version(); !ok { + return &ValidationError{Name: "version", err: errors.New(`ent: missing required field "Secret.version"`)} + } + if _, ok := _c.mutation.Created(); !ok { + return &ValidationError{Name: "created", err: errors.New(`ent: missing required field "Secret.created"`)} + } + if _, ok := _c.mutation.Updated(); !ok { + return &ValidationError{Name: "updated", err: errors.New(`ent: missing required field "Secret.updated"`)} + } + return nil +} + +func (_c *SecretCreate) sqlSave(ctx context.Context) (*Secret, error) { + if err := _c.check(); err != nil { + return nil, err + } + _node, _spec := _c.createSpec() + if err := sqlgraph.CreateNode(ctx, _c.driver, _spec); err != nil { + if sqlgraph.IsConstraintError(err) { + err = &ConstraintError{msg: err.Error(), wrap: err} + } + return nil, err + } + if _spec.ID.Value != nil { + if id, ok := _spec.ID.Value.(*uuid.UUID); ok { + _node.ID = *id + } else if err := _node.ID.Scan(_spec.ID.Value); err != nil { + return nil, err + } + } + _c.mutation.id = &_node.ID + _c.mutation.done = true + return _node, nil +} + +func (_c *SecretCreate) createSpec() (*Secret, *sqlgraph.CreateSpec) { + var ( + _node = &Secret{config: _c.config} + _spec = sqlgraph.NewCreateSpec(secret.Table, sqlgraph.NewFieldSpec(secret.FieldID, field.TypeUUID)) + ) + _spec.OnConflict = _c.conflict + if id, ok := _c.mutation.ID(); ok { + _node.ID = id + _spec.ID.Value = &id + } + if value, ok := _c.mutation.Key(); ok { + _spec.SetField(secret.FieldKey, field.TypeString, value) + _node.Key = value + } + if value, ok := _c.mutation.EncryptedValue(); ok { + _spec.SetField(secret.FieldEncryptedValue, field.TypeString, value) + _node.EncryptedValue = value + } + if value, ok := _c.mutation.SecretRef(); ok { + _spec.SetField(secret.FieldSecretRef, field.TypeString, value) + _node.SecretRef = value + } + if value, ok := _c.mutation.SecretType(); ok { + _spec.SetField(secret.FieldSecretType, field.TypeEnum, value) + _node.SecretType = value + } + if value, ok := _c.mutation.Target(); ok { + _spec.SetField(secret.FieldTarget, field.TypeString, value) + _node.Target = value + } + if value, ok := _c.mutation.Scope(); ok { + _spec.SetField(secret.FieldScope, field.TypeString, value) + _node.Scope = value + } + if value, ok := _c.mutation.ScopeID(); ok { + _spec.SetField(secret.FieldScopeID, field.TypeString, value) + _node.ScopeID = value + } + if value, ok := _c.mutation.Description(); ok { + _spec.SetField(secret.FieldDescription, field.TypeString, value) + _node.Description = value + } + if value, ok := _c.mutation.InjectionMode(); ok { + _spec.SetField(secret.FieldInjectionMode, field.TypeEnum, value) + _node.InjectionMode = value + } + if value, ok := _c.mutation.AllowProgeny(); ok { + _spec.SetField(secret.FieldAllowProgeny, field.TypeBool, value) + _node.AllowProgeny = value + } + if value, ok := _c.mutation.Version(); ok { + _spec.SetField(secret.FieldVersion, field.TypeInt, value) + _node.Version = value + } + if value, ok := _c.mutation.CreatedBy(); ok { + _spec.SetField(secret.FieldCreatedBy, field.TypeString, value) + _node.CreatedBy = value + } + if value, ok := _c.mutation.UpdatedBy(); ok { + _spec.SetField(secret.FieldUpdatedBy, field.TypeString, value) + _node.UpdatedBy = value + } + if value, ok := _c.mutation.Created(); ok { + _spec.SetField(secret.FieldCreated, field.TypeTime, value) + _node.Created = value + } + if value, ok := _c.mutation.Updated(); ok { + _spec.SetField(secret.FieldUpdated, field.TypeTime, value) + _node.Updated = value + } + return _node, _spec +} + +// OnConflict allows configuring the `ON CONFLICT` / `ON DUPLICATE KEY` clause +// of the `INSERT` statement. For example: +// +// client.Secret.Create(). +// SetKey(v). +// OnConflict( +// // Update the row with the new values +// // the was proposed for insertion. +// sql.ResolveWithNewValues(), +// ). +// // Override some of the fields with custom +// // update values. +// Update(func(u *ent.SecretUpsert) { +// SetKey(v+v). +// }). +// Exec(ctx) +func (_c *SecretCreate) OnConflict(opts ...sql.ConflictOption) *SecretUpsertOne { + _c.conflict = opts + return &SecretUpsertOne{ + create: _c, + } +} + +// OnConflictColumns calls `OnConflict` and configures the columns +// as conflict target. Using this option is equivalent to using: +// +// client.Secret.Create(). +// OnConflict(sql.ConflictColumns(columns...)). +// Exec(ctx) +func (_c *SecretCreate) OnConflictColumns(columns ...string) *SecretUpsertOne { + _c.conflict = append(_c.conflict, sql.ConflictColumns(columns...)) + return &SecretUpsertOne{ + create: _c, + } +} + +type ( + // SecretUpsertOne is the builder for "upsert"-ing + // one Secret node. + SecretUpsertOne struct { + create *SecretCreate + } + + // SecretUpsert is the "OnConflict" setter. + SecretUpsert struct { + *sql.UpdateSet + } +) + +// SetKey sets the "key" field. +func (u *SecretUpsert) SetKey(v string) *SecretUpsert { + u.Set(secret.FieldKey, v) + return u +} + +// UpdateKey sets the "key" field to the value that was provided on create. +func (u *SecretUpsert) UpdateKey() *SecretUpsert { + u.SetExcluded(secret.FieldKey) + return u +} + +// SetEncryptedValue sets the "encrypted_value" field. +func (u *SecretUpsert) SetEncryptedValue(v string) *SecretUpsert { + u.Set(secret.FieldEncryptedValue, v) + return u +} + +// UpdateEncryptedValue sets the "encrypted_value" field to the value that was provided on create. +func (u *SecretUpsert) UpdateEncryptedValue() *SecretUpsert { + u.SetExcluded(secret.FieldEncryptedValue) + return u +} + +// SetSecretRef sets the "secret_ref" field. +func (u *SecretUpsert) SetSecretRef(v string) *SecretUpsert { + u.Set(secret.FieldSecretRef, v) + return u +} + +// UpdateSecretRef sets the "secret_ref" field to the value that was provided on create. +func (u *SecretUpsert) UpdateSecretRef() *SecretUpsert { + u.SetExcluded(secret.FieldSecretRef) + return u +} + +// ClearSecretRef clears the value of the "secret_ref" field. +func (u *SecretUpsert) ClearSecretRef() *SecretUpsert { + u.SetNull(secret.FieldSecretRef) + return u +} + +// SetSecretType sets the "secret_type" field. +func (u *SecretUpsert) SetSecretType(v secret.SecretType) *SecretUpsert { + u.Set(secret.FieldSecretType, v) + return u +} + +// UpdateSecretType sets the "secret_type" field to the value that was provided on create. +func (u *SecretUpsert) UpdateSecretType() *SecretUpsert { + u.SetExcluded(secret.FieldSecretType) + return u +} + +// SetTarget sets the "target" field. +func (u *SecretUpsert) SetTarget(v string) *SecretUpsert { + u.Set(secret.FieldTarget, v) + return u +} + +// UpdateTarget sets the "target" field to the value that was provided on create. +func (u *SecretUpsert) UpdateTarget() *SecretUpsert { + u.SetExcluded(secret.FieldTarget) + return u +} + +// ClearTarget clears the value of the "target" field. +func (u *SecretUpsert) ClearTarget() *SecretUpsert { + u.SetNull(secret.FieldTarget) + return u +} + +// SetScope sets the "scope" field. +func (u *SecretUpsert) SetScope(v string) *SecretUpsert { + u.Set(secret.FieldScope, v) + return u +} + +// UpdateScope sets the "scope" field to the value that was provided on create. +func (u *SecretUpsert) UpdateScope() *SecretUpsert { + u.SetExcluded(secret.FieldScope) + return u +} + +// SetScopeID sets the "scope_id" field. +func (u *SecretUpsert) SetScopeID(v string) *SecretUpsert { + u.Set(secret.FieldScopeID, v) + return u +} + +// UpdateScopeID sets the "scope_id" field to the value that was provided on create. +func (u *SecretUpsert) UpdateScopeID() *SecretUpsert { + u.SetExcluded(secret.FieldScopeID) + return u +} + +// SetDescription sets the "description" field. +func (u *SecretUpsert) SetDescription(v string) *SecretUpsert { + u.Set(secret.FieldDescription, v) + return u +} + +// UpdateDescription sets the "description" field to the value that was provided on create. +func (u *SecretUpsert) UpdateDescription() *SecretUpsert { + u.SetExcluded(secret.FieldDescription) + return u +} + +// ClearDescription clears the value of the "description" field. +func (u *SecretUpsert) ClearDescription() *SecretUpsert { + u.SetNull(secret.FieldDescription) + return u +} + +// SetInjectionMode sets the "injection_mode" field. +func (u *SecretUpsert) SetInjectionMode(v secret.InjectionMode) *SecretUpsert { + u.Set(secret.FieldInjectionMode, v) + return u +} + +// UpdateInjectionMode sets the "injection_mode" field to the value that was provided on create. +func (u *SecretUpsert) UpdateInjectionMode() *SecretUpsert { + u.SetExcluded(secret.FieldInjectionMode) + return u +} + +// SetAllowProgeny sets the "allow_progeny" field. +func (u *SecretUpsert) SetAllowProgeny(v bool) *SecretUpsert { + u.Set(secret.FieldAllowProgeny, v) + return u +} + +// UpdateAllowProgeny sets the "allow_progeny" field to the value that was provided on create. +func (u *SecretUpsert) UpdateAllowProgeny() *SecretUpsert { + u.SetExcluded(secret.FieldAllowProgeny) + return u +} + +// SetVersion sets the "version" field. +func (u *SecretUpsert) SetVersion(v int) *SecretUpsert { + u.Set(secret.FieldVersion, v) + return u +} + +// UpdateVersion sets the "version" field to the value that was provided on create. +func (u *SecretUpsert) UpdateVersion() *SecretUpsert { + u.SetExcluded(secret.FieldVersion) + return u +} + +// AddVersion adds v to the "version" field. +func (u *SecretUpsert) AddVersion(v int) *SecretUpsert { + u.Add(secret.FieldVersion, v) + return u +} + +// SetCreatedBy sets the "created_by" field. +func (u *SecretUpsert) SetCreatedBy(v string) *SecretUpsert { + u.Set(secret.FieldCreatedBy, v) + return u +} + +// UpdateCreatedBy sets the "created_by" field to the value that was provided on create. +func (u *SecretUpsert) UpdateCreatedBy() *SecretUpsert { + u.SetExcluded(secret.FieldCreatedBy) + return u +} + +// ClearCreatedBy clears the value of the "created_by" field. +func (u *SecretUpsert) ClearCreatedBy() *SecretUpsert { + u.SetNull(secret.FieldCreatedBy) + return u +} + +// SetUpdatedBy sets the "updated_by" field. +func (u *SecretUpsert) SetUpdatedBy(v string) *SecretUpsert { + u.Set(secret.FieldUpdatedBy, v) + return u +} + +// UpdateUpdatedBy sets the "updated_by" field to the value that was provided on create. +func (u *SecretUpsert) UpdateUpdatedBy() *SecretUpsert { + u.SetExcluded(secret.FieldUpdatedBy) + return u +} + +// ClearUpdatedBy clears the value of the "updated_by" field. +func (u *SecretUpsert) ClearUpdatedBy() *SecretUpsert { + u.SetNull(secret.FieldUpdatedBy) + return u +} + +// SetUpdated sets the "updated" field. +func (u *SecretUpsert) SetUpdated(v time.Time) *SecretUpsert { + u.Set(secret.FieldUpdated, v) + return u +} + +// UpdateUpdated sets the "updated" field to the value that was provided on create. +func (u *SecretUpsert) UpdateUpdated() *SecretUpsert { + u.SetExcluded(secret.FieldUpdated) + return u +} + +// UpdateNewValues updates the mutable fields using the new values that were set on create except the ID field. +// Using this option is equivalent to using: +// +// client.Secret.Create(). +// OnConflict( +// sql.ResolveWithNewValues(), +// sql.ResolveWith(func(u *sql.UpdateSet) { +// u.SetIgnore(secret.FieldID) +// }), +// ). +// Exec(ctx) +func (u *SecretUpsertOne) UpdateNewValues() *SecretUpsertOne { + u.create.conflict = append(u.create.conflict, sql.ResolveWithNewValues()) + u.create.conflict = append(u.create.conflict, sql.ResolveWith(func(s *sql.UpdateSet) { + if _, exists := u.create.mutation.ID(); exists { + s.SetIgnore(secret.FieldID) + } + if _, exists := u.create.mutation.Created(); exists { + s.SetIgnore(secret.FieldCreated) + } + })) + return u +} + +// Ignore sets each column to itself in case of conflict. +// Using this option is equivalent to using: +// +// client.Secret.Create(). +// OnConflict(sql.ResolveWithIgnore()). +// Exec(ctx) +func (u *SecretUpsertOne) Ignore() *SecretUpsertOne { + u.create.conflict = append(u.create.conflict, sql.ResolveWithIgnore()) + return u +} + +// DoNothing configures the conflict_action to `DO NOTHING`. +// Supported only by SQLite and PostgreSQL. +func (u *SecretUpsertOne) DoNothing() *SecretUpsertOne { + u.create.conflict = append(u.create.conflict, sql.DoNothing()) + return u +} + +// Update allows overriding fields `UPDATE` values. See the SecretCreate.OnConflict +// documentation for more info. +func (u *SecretUpsertOne) Update(set func(*SecretUpsert)) *SecretUpsertOne { + u.create.conflict = append(u.create.conflict, sql.ResolveWith(func(update *sql.UpdateSet) { + set(&SecretUpsert{UpdateSet: update}) + })) + return u +} + +// SetKey sets the "key" field. +func (u *SecretUpsertOne) SetKey(v string) *SecretUpsertOne { + return u.Update(func(s *SecretUpsert) { + s.SetKey(v) + }) +} + +// UpdateKey sets the "key" field to the value that was provided on create. +func (u *SecretUpsertOne) UpdateKey() *SecretUpsertOne { + return u.Update(func(s *SecretUpsert) { + s.UpdateKey() + }) +} + +// SetEncryptedValue sets the "encrypted_value" field. +func (u *SecretUpsertOne) SetEncryptedValue(v string) *SecretUpsertOne { + return u.Update(func(s *SecretUpsert) { + s.SetEncryptedValue(v) + }) +} + +// UpdateEncryptedValue sets the "encrypted_value" field to the value that was provided on create. +func (u *SecretUpsertOne) UpdateEncryptedValue() *SecretUpsertOne { + return u.Update(func(s *SecretUpsert) { + s.UpdateEncryptedValue() + }) +} + +// SetSecretRef sets the "secret_ref" field. +func (u *SecretUpsertOne) SetSecretRef(v string) *SecretUpsertOne { + return u.Update(func(s *SecretUpsert) { + s.SetSecretRef(v) + }) +} + +// UpdateSecretRef sets the "secret_ref" field to the value that was provided on create. +func (u *SecretUpsertOne) UpdateSecretRef() *SecretUpsertOne { + return u.Update(func(s *SecretUpsert) { + s.UpdateSecretRef() + }) +} + +// ClearSecretRef clears the value of the "secret_ref" field. +func (u *SecretUpsertOne) ClearSecretRef() *SecretUpsertOne { + return u.Update(func(s *SecretUpsert) { + s.ClearSecretRef() + }) +} + +// SetSecretType sets the "secret_type" field. +func (u *SecretUpsertOne) SetSecretType(v secret.SecretType) *SecretUpsertOne { + return u.Update(func(s *SecretUpsert) { + s.SetSecretType(v) + }) +} + +// UpdateSecretType sets the "secret_type" field to the value that was provided on create. +func (u *SecretUpsertOne) UpdateSecretType() *SecretUpsertOne { + return u.Update(func(s *SecretUpsert) { + s.UpdateSecretType() + }) +} + +// SetTarget sets the "target" field. +func (u *SecretUpsertOne) SetTarget(v string) *SecretUpsertOne { + return u.Update(func(s *SecretUpsert) { + s.SetTarget(v) + }) +} + +// UpdateTarget sets the "target" field to the value that was provided on create. +func (u *SecretUpsertOne) UpdateTarget() *SecretUpsertOne { + return u.Update(func(s *SecretUpsert) { + s.UpdateTarget() + }) +} + +// ClearTarget clears the value of the "target" field. +func (u *SecretUpsertOne) ClearTarget() *SecretUpsertOne { + return u.Update(func(s *SecretUpsert) { + s.ClearTarget() + }) +} + +// SetScope sets the "scope" field. +func (u *SecretUpsertOne) SetScope(v string) *SecretUpsertOne { + return u.Update(func(s *SecretUpsert) { + s.SetScope(v) + }) +} + +// UpdateScope sets the "scope" field to the value that was provided on create. +func (u *SecretUpsertOne) UpdateScope() *SecretUpsertOne { + return u.Update(func(s *SecretUpsert) { + s.UpdateScope() + }) +} + +// SetScopeID sets the "scope_id" field. +func (u *SecretUpsertOne) SetScopeID(v string) *SecretUpsertOne { + return u.Update(func(s *SecretUpsert) { + s.SetScopeID(v) + }) +} + +// UpdateScopeID sets the "scope_id" field to the value that was provided on create. +func (u *SecretUpsertOne) UpdateScopeID() *SecretUpsertOne { + return u.Update(func(s *SecretUpsert) { + s.UpdateScopeID() + }) +} + +// SetDescription sets the "description" field. +func (u *SecretUpsertOne) SetDescription(v string) *SecretUpsertOne { + return u.Update(func(s *SecretUpsert) { + s.SetDescription(v) + }) +} + +// UpdateDescription sets the "description" field to the value that was provided on create. +func (u *SecretUpsertOne) UpdateDescription() *SecretUpsertOne { + return u.Update(func(s *SecretUpsert) { + s.UpdateDescription() + }) +} + +// ClearDescription clears the value of the "description" field. +func (u *SecretUpsertOne) ClearDescription() *SecretUpsertOne { + return u.Update(func(s *SecretUpsert) { + s.ClearDescription() + }) +} + +// SetInjectionMode sets the "injection_mode" field. +func (u *SecretUpsertOne) SetInjectionMode(v secret.InjectionMode) *SecretUpsertOne { + return u.Update(func(s *SecretUpsert) { + s.SetInjectionMode(v) + }) +} + +// UpdateInjectionMode sets the "injection_mode" field to the value that was provided on create. +func (u *SecretUpsertOne) UpdateInjectionMode() *SecretUpsertOne { + return u.Update(func(s *SecretUpsert) { + s.UpdateInjectionMode() + }) +} + +// SetAllowProgeny sets the "allow_progeny" field. +func (u *SecretUpsertOne) SetAllowProgeny(v bool) *SecretUpsertOne { + return u.Update(func(s *SecretUpsert) { + s.SetAllowProgeny(v) + }) +} + +// UpdateAllowProgeny sets the "allow_progeny" field to the value that was provided on create. +func (u *SecretUpsertOne) UpdateAllowProgeny() *SecretUpsertOne { + return u.Update(func(s *SecretUpsert) { + s.UpdateAllowProgeny() + }) +} + +// SetVersion sets the "version" field. +func (u *SecretUpsertOne) SetVersion(v int) *SecretUpsertOne { + return u.Update(func(s *SecretUpsert) { + s.SetVersion(v) + }) +} + +// AddVersion adds v to the "version" field. +func (u *SecretUpsertOne) AddVersion(v int) *SecretUpsertOne { + return u.Update(func(s *SecretUpsert) { + s.AddVersion(v) + }) +} + +// UpdateVersion sets the "version" field to the value that was provided on create. +func (u *SecretUpsertOne) UpdateVersion() *SecretUpsertOne { + return u.Update(func(s *SecretUpsert) { + s.UpdateVersion() + }) +} + +// SetCreatedBy sets the "created_by" field. +func (u *SecretUpsertOne) SetCreatedBy(v string) *SecretUpsertOne { + return u.Update(func(s *SecretUpsert) { + s.SetCreatedBy(v) + }) +} + +// UpdateCreatedBy sets the "created_by" field to the value that was provided on create. +func (u *SecretUpsertOne) UpdateCreatedBy() *SecretUpsertOne { + return u.Update(func(s *SecretUpsert) { + s.UpdateCreatedBy() + }) +} + +// ClearCreatedBy clears the value of the "created_by" field. +func (u *SecretUpsertOne) ClearCreatedBy() *SecretUpsertOne { + return u.Update(func(s *SecretUpsert) { + s.ClearCreatedBy() + }) +} + +// SetUpdatedBy sets the "updated_by" field. +func (u *SecretUpsertOne) SetUpdatedBy(v string) *SecretUpsertOne { + return u.Update(func(s *SecretUpsert) { + s.SetUpdatedBy(v) + }) +} + +// UpdateUpdatedBy sets the "updated_by" field to the value that was provided on create. +func (u *SecretUpsertOne) UpdateUpdatedBy() *SecretUpsertOne { + return u.Update(func(s *SecretUpsert) { + s.UpdateUpdatedBy() + }) +} + +// ClearUpdatedBy clears the value of the "updated_by" field. +func (u *SecretUpsertOne) ClearUpdatedBy() *SecretUpsertOne { + return u.Update(func(s *SecretUpsert) { + s.ClearUpdatedBy() + }) +} + +// SetUpdated sets the "updated" field. +func (u *SecretUpsertOne) SetUpdated(v time.Time) *SecretUpsertOne { + return u.Update(func(s *SecretUpsert) { + s.SetUpdated(v) + }) +} + +// UpdateUpdated sets the "updated" field to the value that was provided on create. +func (u *SecretUpsertOne) UpdateUpdated() *SecretUpsertOne { + return u.Update(func(s *SecretUpsert) { + s.UpdateUpdated() + }) +} + +// Exec executes the query. +func (u *SecretUpsertOne) Exec(ctx context.Context) error { + if len(u.create.conflict) == 0 { + return errors.New("ent: missing options for SecretCreate.OnConflict") + } + return u.create.Exec(ctx) +} + +// ExecX is like Exec, but panics if an error occurs. +func (u *SecretUpsertOne) ExecX(ctx context.Context) { + if err := u.create.Exec(ctx); err != nil { + panic(err) + } +} + +// Exec executes the UPSERT query and returns the inserted/updated ID. +func (u *SecretUpsertOne) ID(ctx context.Context) (id uuid.UUID, err error) { + if u.create.driver.Dialect() == dialect.MySQL { + // In case of "ON CONFLICT", there is no way to get back non-numeric ID + // fields from the database since MySQL does not support the RETURNING clause. + return id, errors.New("ent: SecretUpsertOne.ID is not supported by MySQL driver. Use SecretUpsertOne.Exec instead") + } + node, err := u.create.Save(ctx) + if err != nil { + return id, err + } + return node.ID, nil +} + +// IDX is like ID, but panics if an error occurs. +func (u *SecretUpsertOne) IDX(ctx context.Context) uuid.UUID { + id, err := u.ID(ctx) + if err != nil { + panic(err) + } + return id +} + +// SecretCreateBulk is the builder for creating many Secret entities in bulk. +type SecretCreateBulk struct { + config + err error + builders []*SecretCreate + conflict []sql.ConflictOption +} + +// Save creates the Secret entities in the database. +func (_c *SecretCreateBulk) Save(ctx context.Context) ([]*Secret, error) { + if _c.err != nil { + return nil, _c.err + } + specs := make([]*sqlgraph.CreateSpec, len(_c.builders)) + nodes := make([]*Secret, len(_c.builders)) + mutators := make([]Mutator, len(_c.builders)) + for i := range _c.builders { + func(i int, root context.Context) { + builder := _c.builders[i] + builder.defaults() + var mut Mutator = MutateFunc(func(ctx context.Context, m Mutation) (Value, error) { + mutation, ok := m.(*SecretMutation) + if !ok { + return nil, fmt.Errorf("unexpected mutation type %T", m) + } + if err := builder.check(); err != nil { + return nil, err + } + builder.mutation = mutation + var err error + nodes[i], specs[i] = builder.createSpec() + if i < len(mutators)-1 { + _, err = mutators[i+1].Mutate(root, _c.builders[i+1].mutation) + } else { + spec := &sqlgraph.BatchCreateSpec{Nodes: specs} + spec.OnConflict = _c.conflict + // Invoke the actual operation on the latest mutation in the chain. + if err = sqlgraph.BatchCreate(ctx, _c.driver, spec); err != nil { + if sqlgraph.IsConstraintError(err) { + err = &ConstraintError{msg: err.Error(), wrap: err} + } + } + } + if err != nil { + return nil, err + } + mutation.id = &nodes[i].ID + mutation.done = true + return nodes[i], nil + }) + for i := len(builder.hooks) - 1; i >= 0; i-- { + mut = builder.hooks[i](mut) + } + mutators[i] = mut + }(i, ctx) + } + if len(mutators) > 0 { + if _, err := mutators[0].Mutate(ctx, _c.builders[0].mutation); err != nil { + return nil, err + } + } + return nodes, nil +} + +// SaveX is like Save, but panics if an error occurs. +func (_c *SecretCreateBulk) SaveX(ctx context.Context) []*Secret { + v, err := _c.Save(ctx) + if err != nil { + panic(err) + } + return v +} + +// Exec executes the query. +func (_c *SecretCreateBulk) Exec(ctx context.Context) error { + _, err := _c.Save(ctx) + return err +} + +// ExecX is like Exec, but panics if an error occurs. +func (_c *SecretCreateBulk) ExecX(ctx context.Context) { + if err := _c.Exec(ctx); err != nil { + panic(err) + } +} + +// OnConflict allows configuring the `ON CONFLICT` / `ON DUPLICATE KEY` clause +// of the `INSERT` statement. For example: +// +// client.Secret.CreateBulk(builders...). +// OnConflict( +// // Update the row with the new values +// // the was proposed for insertion. +// sql.ResolveWithNewValues(), +// ). +// // Override some of the fields with custom +// // update values. +// Update(func(u *ent.SecretUpsert) { +// SetKey(v+v). +// }). +// Exec(ctx) +func (_c *SecretCreateBulk) OnConflict(opts ...sql.ConflictOption) *SecretUpsertBulk { + _c.conflict = opts + return &SecretUpsertBulk{ + create: _c, + } +} + +// OnConflictColumns calls `OnConflict` and configures the columns +// as conflict target. Using this option is equivalent to using: +// +// client.Secret.Create(). +// OnConflict(sql.ConflictColumns(columns...)). +// Exec(ctx) +func (_c *SecretCreateBulk) OnConflictColumns(columns ...string) *SecretUpsertBulk { + _c.conflict = append(_c.conflict, sql.ConflictColumns(columns...)) + return &SecretUpsertBulk{ + create: _c, + } +} + +// SecretUpsertBulk is the builder for "upsert"-ing +// a bulk of Secret nodes. +type SecretUpsertBulk struct { + create *SecretCreateBulk +} + +// UpdateNewValues updates the mutable fields using the new values that +// were set on create. Using this option is equivalent to using: +// +// client.Secret.Create(). +// OnConflict( +// sql.ResolveWithNewValues(), +// sql.ResolveWith(func(u *sql.UpdateSet) { +// u.SetIgnore(secret.FieldID) +// }), +// ). +// Exec(ctx) +func (u *SecretUpsertBulk) UpdateNewValues() *SecretUpsertBulk { + u.create.conflict = append(u.create.conflict, sql.ResolveWithNewValues()) + u.create.conflict = append(u.create.conflict, sql.ResolveWith(func(s *sql.UpdateSet) { + for _, b := range u.create.builders { + if _, exists := b.mutation.ID(); exists { + s.SetIgnore(secret.FieldID) + } + if _, exists := b.mutation.Created(); exists { + s.SetIgnore(secret.FieldCreated) + } + } + })) + return u +} + +// Ignore sets each column to itself in case of conflict. +// Using this option is equivalent to using: +// +// client.Secret.Create(). +// OnConflict(sql.ResolveWithIgnore()). +// Exec(ctx) +func (u *SecretUpsertBulk) Ignore() *SecretUpsertBulk { + u.create.conflict = append(u.create.conflict, sql.ResolveWithIgnore()) + return u +} + +// DoNothing configures the conflict_action to `DO NOTHING`. +// Supported only by SQLite and PostgreSQL. +func (u *SecretUpsertBulk) DoNothing() *SecretUpsertBulk { + u.create.conflict = append(u.create.conflict, sql.DoNothing()) + return u +} + +// Update allows overriding fields `UPDATE` values. See the SecretCreateBulk.OnConflict +// documentation for more info. +func (u *SecretUpsertBulk) Update(set func(*SecretUpsert)) *SecretUpsertBulk { + u.create.conflict = append(u.create.conflict, sql.ResolveWith(func(update *sql.UpdateSet) { + set(&SecretUpsert{UpdateSet: update}) + })) + return u +} + +// SetKey sets the "key" field. +func (u *SecretUpsertBulk) SetKey(v string) *SecretUpsertBulk { + return u.Update(func(s *SecretUpsert) { + s.SetKey(v) + }) +} + +// UpdateKey sets the "key" field to the value that was provided on create. +func (u *SecretUpsertBulk) UpdateKey() *SecretUpsertBulk { + return u.Update(func(s *SecretUpsert) { + s.UpdateKey() + }) +} + +// SetEncryptedValue sets the "encrypted_value" field. +func (u *SecretUpsertBulk) SetEncryptedValue(v string) *SecretUpsertBulk { + return u.Update(func(s *SecretUpsert) { + s.SetEncryptedValue(v) + }) +} + +// UpdateEncryptedValue sets the "encrypted_value" field to the value that was provided on create. +func (u *SecretUpsertBulk) UpdateEncryptedValue() *SecretUpsertBulk { + return u.Update(func(s *SecretUpsert) { + s.UpdateEncryptedValue() + }) +} + +// SetSecretRef sets the "secret_ref" field. +func (u *SecretUpsertBulk) SetSecretRef(v string) *SecretUpsertBulk { + return u.Update(func(s *SecretUpsert) { + s.SetSecretRef(v) + }) +} + +// UpdateSecretRef sets the "secret_ref" field to the value that was provided on create. +func (u *SecretUpsertBulk) UpdateSecretRef() *SecretUpsertBulk { + return u.Update(func(s *SecretUpsert) { + s.UpdateSecretRef() + }) +} + +// ClearSecretRef clears the value of the "secret_ref" field. +func (u *SecretUpsertBulk) ClearSecretRef() *SecretUpsertBulk { + return u.Update(func(s *SecretUpsert) { + s.ClearSecretRef() + }) +} + +// SetSecretType sets the "secret_type" field. +func (u *SecretUpsertBulk) SetSecretType(v secret.SecretType) *SecretUpsertBulk { + return u.Update(func(s *SecretUpsert) { + s.SetSecretType(v) + }) +} + +// UpdateSecretType sets the "secret_type" field to the value that was provided on create. +func (u *SecretUpsertBulk) UpdateSecretType() *SecretUpsertBulk { + return u.Update(func(s *SecretUpsert) { + s.UpdateSecretType() + }) +} + +// SetTarget sets the "target" field. +func (u *SecretUpsertBulk) SetTarget(v string) *SecretUpsertBulk { + return u.Update(func(s *SecretUpsert) { + s.SetTarget(v) + }) +} + +// UpdateTarget sets the "target" field to the value that was provided on create. +func (u *SecretUpsertBulk) UpdateTarget() *SecretUpsertBulk { + return u.Update(func(s *SecretUpsert) { + s.UpdateTarget() + }) +} + +// ClearTarget clears the value of the "target" field. +func (u *SecretUpsertBulk) ClearTarget() *SecretUpsertBulk { + return u.Update(func(s *SecretUpsert) { + s.ClearTarget() + }) +} + +// SetScope sets the "scope" field. +func (u *SecretUpsertBulk) SetScope(v string) *SecretUpsertBulk { + return u.Update(func(s *SecretUpsert) { + s.SetScope(v) + }) +} + +// UpdateScope sets the "scope" field to the value that was provided on create. +func (u *SecretUpsertBulk) UpdateScope() *SecretUpsertBulk { + return u.Update(func(s *SecretUpsert) { + s.UpdateScope() + }) +} + +// SetScopeID sets the "scope_id" field. +func (u *SecretUpsertBulk) SetScopeID(v string) *SecretUpsertBulk { + return u.Update(func(s *SecretUpsert) { + s.SetScopeID(v) + }) +} + +// UpdateScopeID sets the "scope_id" field to the value that was provided on create. +func (u *SecretUpsertBulk) UpdateScopeID() *SecretUpsertBulk { + return u.Update(func(s *SecretUpsert) { + s.UpdateScopeID() + }) +} + +// SetDescription sets the "description" field. +func (u *SecretUpsertBulk) SetDescription(v string) *SecretUpsertBulk { + return u.Update(func(s *SecretUpsert) { + s.SetDescription(v) + }) +} + +// UpdateDescription sets the "description" field to the value that was provided on create. +func (u *SecretUpsertBulk) UpdateDescription() *SecretUpsertBulk { + return u.Update(func(s *SecretUpsert) { + s.UpdateDescription() + }) +} + +// ClearDescription clears the value of the "description" field. +func (u *SecretUpsertBulk) ClearDescription() *SecretUpsertBulk { + return u.Update(func(s *SecretUpsert) { + s.ClearDescription() + }) +} + +// SetInjectionMode sets the "injection_mode" field. +func (u *SecretUpsertBulk) SetInjectionMode(v secret.InjectionMode) *SecretUpsertBulk { + return u.Update(func(s *SecretUpsert) { + s.SetInjectionMode(v) + }) +} + +// UpdateInjectionMode sets the "injection_mode" field to the value that was provided on create. +func (u *SecretUpsertBulk) UpdateInjectionMode() *SecretUpsertBulk { + return u.Update(func(s *SecretUpsert) { + s.UpdateInjectionMode() + }) +} + +// SetAllowProgeny sets the "allow_progeny" field. +func (u *SecretUpsertBulk) SetAllowProgeny(v bool) *SecretUpsertBulk { + return u.Update(func(s *SecretUpsert) { + s.SetAllowProgeny(v) + }) +} + +// UpdateAllowProgeny sets the "allow_progeny" field to the value that was provided on create. +func (u *SecretUpsertBulk) UpdateAllowProgeny() *SecretUpsertBulk { + return u.Update(func(s *SecretUpsert) { + s.UpdateAllowProgeny() + }) +} + +// SetVersion sets the "version" field. +func (u *SecretUpsertBulk) SetVersion(v int) *SecretUpsertBulk { + return u.Update(func(s *SecretUpsert) { + s.SetVersion(v) + }) +} + +// AddVersion adds v to the "version" field. +func (u *SecretUpsertBulk) AddVersion(v int) *SecretUpsertBulk { + return u.Update(func(s *SecretUpsert) { + s.AddVersion(v) + }) +} + +// UpdateVersion sets the "version" field to the value that was provided on create. +func (u *SecretUpsertBulk) UpdateVersion() *SecretUpsertBulk { + return u.Update(func(s *SecretUpsert) { + s.UpdateVersion() + }) +} + +// SetCreatedBy sets the "created_by" field. +func (u *SecretUpsertBulk) SetCreatedBy(v string) *SecretUpsertBulk { + return u.Update(func(s *SecretUpsert) { + s.SetCreatedBy(v) + }) +} + +// UpdateCreatedBy sets the "created_by" field to the value that was provided on create. +func (u *SecretUpsertBulk) UpdateCreatedBy() *SecretUpsertBulk { + return u.Update(func(s *SecretUpsert) { + s.UpdateCreatedBy() + }) +} + +// ClearCreatedBy clears the value of the "created_by" field. +func (u *SecretUpsertBulk) ClearCreatedBy() *SecretUpsertBulk { + return u.Update(func(s *SecretUpsert) { + s.ClearCreatedBy() + }) +} + +// SetUpdatedBy sets the "updated_by" field. +func (u *SecretUpsertBulk) SetUpdatedBy(v string) *SecretUpsertBulk { + return u.Update(func(s *SecretUpsert) { + s.SetUpdatedBy(v) + }) +} + +// UpdateUpdatedBy sets the "updated_by" field to the value that was provided on create. +func (u *SecretUpsertBulk) UpdateUpdatedBy() *SecretUpsertBulk { + return u.Update(func(s *SecretUpsert) { + s.UpdateUpdatedBy() + }) +} + +// ClearUpdatedBy clears the value of the "updated_by" field. +func (u *SecretUpsertBulk) ClearUpdatedBy() *SecretUpsertBulk { + return u.Update(func(s *SecretUpsert) { + s.ClearUpdatedBy() + }) +} + +// SetUpdated sets the "updated" field. +func (u *SecretUpsertBulk) SetUpdated(v time.Time) *SecretUpsertBulk { + return u.Update(func(s *SecretUpsert) { + s.SetUpdated(v) + }) +} + +// UpdateUpdated sets the "updated" field to the value that was provided on create. +func (u *SecretUpsertBulk) UpdateUpdated() *SecretUpsertBulk { + return u.Update(func(s *SecretUpsert) { + s.UpdateUpdated() + }) +} + +// Exec executes the query. +func (u *SecretUpsertBulk) Exec(ctx context.Context) error { + if u.create.err != nil { + return u.create.err + } + for i, b := range u.create.builders { + if len(b.conflict) != 0 { + return fmt.Errorf("ent: OnConflict was set for builder %d. Set it on the SecretCreateBulk instead", i) + } + } + if len(u.create.conflict) == 0 { + return errors.New("ent: missing options for SecretCreateBulk.OnConflict") + } + return u.create.Exec(ctx) +} + +// ExecX is like Exec, but panics if an error occurs. +func (u *SecretUpsertBulk) ExecX(ctx context.Context) { + if err := u.create.Exec(ctx); err != nil { + panic(err) + } +} diff --git a/pkg/ent/secret_delete.go b/pkg/ent/secret_delete.go new file mode 100644 index 000000000..0eb496d29 --- /dev/null +++ b/pkg/ent/secret_delete.go @@ -0,0 +1,88 @@ +// Code generated by ent, DO NOT EDIT. + +package ent + +import ( + "context" + + "entgo.io/ent/dialect/sql" + "entgo.io/ent/dialect/sql/sqlgraph" + "entgo.io/ent/schema/field" + "github.com/GoogleCloudPlatform/scion/pkg/ent/predicate" + "github.com/GoogleCloudPlatform/scion/pkg/ent/secret" +) + +// SecretDelete is the builder for deleting a Secret entity. +type SecretDelete struct { + config + hooks []Hook + mutation *SecretMutation +} + +// Where appends a list predicates to the SecretDelete builder. +func (_d *SecretDelete) Where(ps ...predicate.Secret) *SecretDelete { + _d.mutation.Where(ps...) + return _d +} + +// Exec executes the deletion query and returns how many vertices were deleted. +func (_d *SecretDelete) Exec(ctx context.Context) (int, error) { + return withHooks(ctx, _d.sqlExec, _d.mutation, _d.hooks) +} + +// ExecX is like Exec, but panics if an error occurs. +func (_d *SecretDelete) ExecX(ctx context.Context) int { + n, err := _d.Exec(ctx) + if err != nil { + panic(err) + } + return n +} + +func (_d *SecretDelete) sqlExec(ctx context.Context) (int, error) { + _spec := sqlgraph.NewDeleteSpec(secret.Table, sqlgraph.NewFieldSpec(secret.FieldID, field.TypeUUID)) + if ps := _d.mutation.predicates; len(ps) > 0 { + _spec.Predicate = func(selector *sql.Selector) { + for i := range ps { + ps[i](selector) + } + } + } + affected, err := sqlgraph.DeleteNodes(ctx, _d.driver, _spec) + if err != nil && sqlgraph.IsConstraintError(err) { + err = &ConstraintError{msg: err.Error(), wrap: err} + } + _d.mutation.done = true + return affected, err +} + +// SecretDeleteOne is the builder for deleting a single Secret entity. +type SecretDeleteOne struct { + _d *SecretDelete +} + +// Where appends a list predicates to the SecretDelete builder. +func (_d *SecretDeleteOne) Where(ps ...predicate.Secret) *SecretDeleteOne { + _d._d.mutation.Where(ps...) + return _d +} + +// Exec executes the deletion query. +func (_d *SecretDeleteOne) Exec(ctx context.Context) error { + n, err := _d._d.Exec(ctx) + switch { + case err != nil: + return err + case n == 0: + return &NotFoundError{secret.Label} + default: + return nil + } +} + +// ExecX is like Exec, but panics if an error occurs. +func (_d *SecretDeleteOne) ExecX(ctx context.Context) { + if err := _d.Exec(ctx); err != nil { + panic(err) + } +} diff --git a/pkg/ent/secret_query.go b/pkg/ent/secret_query.go new file mode 100644 index 000000000..b02ea7f6d --- /dev/null +++ b/pkg/ent/secret_query.go @@ -0,0 +1,565 @@ +// Code generated by ent, DO NOT EDIT. + +package ent + +import ( + "context" + "fmt" + "math" + + "entgo.io/ent" + "entgo.io/ent/dialect" + "entgo.io/ent/dialect/sql" + "entgo.io/ent/dialect/sql/sqlgraph" + "entgo.io/ent/schema/field" + "github.com/GoogleCloudPlatform/scion/pkg/ent/predicate" + "github.com/GoogleCloudPlatform/scion/pkg/ent/secret" + "github.com/google/uuid" +) + +// SecretQuery is the builder for querying Secret entities. +type SecretQuery struct { + config + ctx *QueryContext + order []secret.OrderOption + inters []Interceptor + predicates []predicate.Secret + modifiers []func(*sql.Selector) + // intermediate query (i.e. traversal path). + sql *sql.Selector + path func(context.Context) (*sql.Selector, error) +} + +// Where adds a new predicate for the SecretQuery builder. +func (_q *SecretQuery) Where(ps ...predicate.Secret) *SecretQuery { + _q.predicates = append(_q.predicates, ps...) + return _q +} + +// Limit the number of records to be returned by this query. +func (_q *SecretQuery) Limit(limit int) *SecretQuery { + _q.ctx.Limit = &limit + return _q +} + +// Offset to start from. +func (_q *SecretQuery) Offset(offset int) *SecretQuery { + _q.ctx.Offset = &offset + return _q +} + +// Unique configures the query builder to filter duplicate records on query. +// By default, unique is set to true, and can be disabled using this method. +func (_q *SecretQuery) Unique(unique bool) *SecretQuery { + _q.ctx.Unique = &unique + return _q +} + +// Order specifies how the records should be ordered. +func (_q *SecretQuery) Order(o ...secret.OrderOption) *SecretQuery { + _q.order = append(_q.order, o...) + return _q +} + +// First returns the first Secret entity from the query. +// Returns a *NotFoundError when no Secret was found. +func (_q *SecretQuery) First(ctx context.Context) (*Secret, error) { + nodes, err := _q.Limit(1).All(setContextOp(ctx, _q.ctx, ent.OpQueryFirst)) + if err != nil { + return nil, err + } + if len(nodes) == 0 { + return nil, &NotFoundError{secret.Label} + } + return nodes[0], nil +} + +// FirstX is like First, but panics if an error occurs. +func (_q *SecretQuery) FirstX(ctx context.Context) *Secret { + node, err := _q.First(ctx) + if err != nil && !IsNotFound(err) { + panic(err) + } + return node +} + +// FirstID returns the first Secret ID from the query. +// Returns a *NotFoundError when no Secret ID was found. +func (_q *SecretQuery) FirstID(ctx context.Context) (id uuid.UUID, err error) { + var ids []uuid.UUID + if ids, err = _q.Limit(1).IDs(setContextOp(ctx, _q.ctx, ent.OpQueryFirstID)); err != nil { + return + } + if len(ids) == 0 { + err = &NotFoundError{secret.Label} + return + } + return ids[0], nil +} + +// FirstIDX is like FirstID, but panics if an error occurs. +func (_q *SecretQuery) FirstIDX(ctx context.Context) uuid.UUID { + id, err := _q.FirstID(ctx) + if err != nil && !IsNotFound(err) { + panic(err) + } + return id +} + +// Only returns a single Secret entity found by the query, ensuring it only returns one. +// Returns a *NotSingularError when more than one Secret entity is found. +// Returns a *NotFoundError when no Secret entities are found. +func (_q *SecretQuery) Only(ctx context.Context) (*Secret, error) { + nodes, err := _q.Limit(2).All(setContextOp(ctx, _q.ctx, ent.OpQueryOnly)) + if err != nil { + return nil, err + } + switch len(nodes) { + case 1: + return nodes[0], nil + case 0: + return nil, &NotFoundError{secret.Label} + default: + return nil, &NotSingularError{secret.Label} + } +} + +// OnlyX is like Only, but panics if an error occurs. +func (_q *SecretQuery) OnlyX(ctx context.Context) *Secret { + node, err := _q.Only(ctx) + if err != nil { + panic(err) + } + return node +} + +// OnlyID is like Only, but returns the only Secret ID in the query. +// Returns a *NotSingularError when more than one Secret ID is found. +// Returns a *NotFoundError when no entities are found. +func (_q *SecretQuery) OnlyID(ctx context.Context) (id uuid.UUID, err error) { + var ids []uuid.UUID + if ids, err = _q.Limit(2).IDs(setContextOp(ctx, _q.ctx, ent.OpQueryOnlyID)); err != nil { + return + } + switch len(ids) { + case 1: + id = ids[0] + case 0: + err = &NotFoundError{secret.Label} + default: + err = &NotSingularError{secret.Label} + } + return +} + +// OnlyIDX is like OnlyID, but panics if an error occurs. +func (_q *SecretQuery) OnlyIDX(ctx context.Context) uuid.UUID { + id, err := _q.OnlyID(ctx) + if err != nil { + panic(err) + } + return id +} + +// All executes the query and returns a list of Secrets. +func (_q *SecretQuery) All(ctx context.Context) ([]*Secret, error) { + ctx = setContextOp(ctx, _q.ctx, ent.OpQueryAll) + if err := _q.prepareQuery(ctx); err != nil { + return nil, err + } + qr := querierAll[[]*Secret, *SecretQuery]() + return withInterceptors[[]*Secret](ctx, _q, qr, _q.inters) +} + +// AllX is like All, but panics if an error occurs. +func (_q *SecretQuery) AllX(ctx context.Context) []*Secret { + nodes, err := _q.All(ctx) + if err != nil { + panic(err) + } + return nodes +} + +// IDs executes the query and returns a list of Secret IDs. +func (_q *SecretQuery) IDs(ctx context.Context) (ids []uuid.UUID, err error) { + if _q.ctx.Unique == nil && _q.path != nil { + _q.Unique(true) + } + ctx = setContextOp(ctx, _q.ctx, ent.OpQueryIDs) + if err = _q.Select(secret.FieldID).Scan(ctx, &ids); err != nil { + return nil, err + } + return ids, nil +} + +// IDsX is like IDs, but panics if an error occurs. +func (_q *SecretQuery) IDsX(ctx context.Context) []uuid.UUID { + ids, err := _q.IDs(ctx) + if err != nil { + panic(err) + } + return ids +} + +// Count returns the count of the given query. +func (_q *SecretQuery) Count(ctx context.Context) (int, error) { + ctx = setContextOp(ctx, _q.ctx, ent.OpQueryCount) + if err := _q.prepareQuery(ctx); err != nil { + return 0, err + } + return withInterceptors[int](ctx, _q, querierCount[*SecretQuery](), _q.inters) +} + +// CountX is like Count, but panics if an error occurs. +func (_q *SecretQuery) CountX(ctx context.Context) int { + count, err := _q.Count(ctx) + if err != nil { + panic(err) + } + return count +} + +// Exist returns true if the query has elements in the graph. +func (_q *SecretQuery) Exist(ctx context.Context) (bool, error) { + ctx = setContextOp(ctx, _q.ctx, ent.OpQueryExist) + switch _, err := _q.FirstID(ctx); { + case IsNotFound(err): + return false, nil + case err != nil: + return false, fmt.Errorf("ent: check existence: %w", err) + default: + return true, nil + } +} + +// ExistX is like Exist, but panics if an error occurs. +func (_q *SecretQuery) ExistX(ctx context.Context) bool { + exist, err := _q.Exist(ctx) + if err != nil { + panic(err) + } + return exist +} + +// Clone returns a duplicate of the SecretQuery builder, including all associated steps. It can be +// used to prepare common query builders and use them differently after the clone is made. +func (_q *SecretQuery) Clone() *SecretQuery { + if _q == nil { + return nil + } + return &SecretQuery{ + config: _q.config, + ctx: _q.ctx.Clone(), + order: append([]secret.OrderOption{}, _q.order...), + inters: append([]Interceptor{}, _q.inters...), + predicates: append([]predicate.Secret{}, _q.predicates...), + // clone intermediate query. + sql: _q.sql.Clone(), + path: _q.path, + } +} + +// GroupBy is used to group vertices by one or more fields/columns. +// It is often used with aggregate functions, like: count, max, mean, min, sum. +// +// Example: +// +// var v []struct { +// Key string `json:"key,omitempty"` +// Count int `json:"count,omitempty"` +// } +// +// client.Secret.Query(). +// GroupBy(secret.FieldKey). +// Aggregate(ent.Count()). +// Scan(ctx, &v) +func (_q *SecretQuery) GroupBy(field string, fields ...string) *SecretGroupBy { + _q.ctx.Fields = append([]string{field}, fields...) + grbuild := &SecretGroupBy{build: _q} + grbuild.flds = &_q.ctx.Fields + grbuild.label = secret.Label + grbuild.scan = grbuild.Scan + return grbuild +} + +// Select allows the selection one or more fields/columns for the given query, +// instead of selecting all fields in the entity. +// +// Example: +// +// var v []struct { +// Key string `json:"key,omitempty"` +// } +// +// client.Secret.Query(). +// Select(secret.FieldKey). +// Scan(ctx, &v) +func (_q *SecretQuery) Select(fields ...string) *SecretSelect { + _q.ctx.Fields = append(_q.ctx.Fields, fields...) + sbuild := &SecretSelect{SecretQuery: _q} + sbuild.label = secret.Label + sbuild.flds, sbuild.scan = &_q.ctx.Fields, sbuild.Scan + return sbuild +} + +// Aggregate returns a SecretSelect configured with the given aggregations. +func (_q *SecretQuery) Aggregate(fns ...AggregateFunc) *SecretSelect { + return _q.Select().Aggregate(fns...) +} + +func (_q *SecretQuery) prepareQuery(ctx context.Context) error { + for _, inter := range _q.inters { + if inter == nil { + return fmt.Errorf("ent: uninitialized interceptor (forgotten import ent/runtime?)") + } + if trv, ok := inter.(Traverser); ok { + if err := trv.Traverse(ctx, _q); err != nil { + return err + } + } + } + for _, f := range _q.ctx.Fields { + if !secret.ValidColumn(f) { + return &ValidationError{Name: f, err: fmt.Errorf("ent: invalid field %q for query", f)} + } + } + if _q.path != nil { + prev, err := _q.path(ctx) + if err != nil { + return err + } + _q.sql = prev + } + return nil +} + +func (_q *SecretQuery) sqlAll(ctx context.Context, hooks ...queryHook) ([]*Secret, error) { + var ( + nodes = []*Secret{} + _spec = _q.querySpec() + ) + _spec.ScanValues = func(columns []string) ([]any, error) { + return (*Secret).scanValues(nil, columns) + } + _spec.Assign = func(columns []string, values []any) error { + node := &Secret{config: _q.config} + nodes = append(nodes, node) + return node.assignValues(columns, values) + } + if len(_q.modifiers) > 0 { + _spec.Modifiers = _q.modifiers + } + for i := range hooks { + hooks[i](ctx, _spec) + } + if err := sqlgraph.QueryNodes(ctx, _q.driver, _spec); err != nil { + return nil, err + } + if len(nodes) == 0 { + return nodes, nil + } + return nodes, nil +} + +func (_q *SecretQuery) sqlCount(ctx context.Context) (int, error) { + _spec := _q.querySpec() + if len(_q.modifiers) > 0 { + _spec.Modifiers = _q.modifiers + } + _spec.Node.Columns = _q.ctx.Fields + if len(_q.ctx.Fields) > 0 { + _spec.Unique = _q.ctx.Unique != nil && *_q.ctx.Unique + } + return sqlgraph.CountNodes(ctx, _q.driver, _spec) +} + +func (_q *SecretQuery) querySpec() *sqlgraph.QuerySpec { + _spec := sqlgraph.NewQuerySpec(secret.Table, secret.Columns, sqlgraph.NewFieldSpec(secret.FieldID, field.TypeUUID)) + _spec.From = _q.sql + if unique := _q.ctx.Unique; unique != nil { + _spec.Unique = *unique + } else if _q.path != nil { + _spec.Unique = true + } + if fields := _q.ctx.Fields; len(fields) > 0 { + _spec.Node.Columns = make([]string, 0, len(fields)) + _spec.Node.Columns = append(_spec.Node.Columns, secret.FieldID) + for i := range fields { + if fields[i] != secret.FieldID { + _spec.Node.Columns = append(_spec.Node.Columns, fields[i]) + } + } + } + if ps := _q.predicates; len(ps) > 0 { + _spec.Predicate = func(selector *sql.Selector) { + for i := range ps { + ps[i](selector) + } + } + } + if limit := _q.ctx.Limit; limit != nil { + _spec.Limit = *limit + } + if offset := _q.ctx.Offset; offset != nil { + _spec.Offset = *offset + } + if ps := _q.order; len(ps) > 0 { + _spec.Order = func(selector *sql.Selector) { + for i := range ps { + ps[i](selector) + } + } + } + return _spec +} + +func (_q *SecretQuery) sqlQuery(ctx context.Context) *sql.Selector { + builder := sql.Dialect(_q.driver.Dialect()) + t1 := builder.Table(secret.Table) + columns := _q.ctx.Fields + if len(columns) == 0 { + columns = secret.Columns + } + selector := builder.Select(t1.Columns(columns...)...).From(t1) + if _q.sql != nil { + selector = _q.sql + selector.Select(selector.Columns(columns...)...) + } + if _q.ctx.Unique != nil && *_q.ctx.Unique { + selector.Distinct() + } + for _, m := range _q.modifiers { + m(selector) + } + for _, p := range _q.predicates { + p(selector) + } + for _, p := range _q.order { + p(selector) + } + if offset := _q.ctx.Offset; offset != nil { + // limit is mandatory for offset clause. We start + // with default value, and override it below if needed. + selector.Offset(*offset).Limit(math.MaxInt32) + } + if limit := _q.ctx.Limit; limit != nil { + selector.Limit(*limit) + } + return selector +} + +// ForUpdate locks the selected rows against concurrent updates, and prevent them from being +// updated, deleted or "selected ... for update" by other sessions, until the transaction is +// either committed or rolled-back. +func (_q *SecretQuery) ForUpdate(opts ...sql.LockOption) *SecretQuery { + if _q.driver.Dialect() == dialect.Postgres { + _q.Unique(false) + } + _q.modifiers = append(_q.modifiers, func(s *sql.Selector) { + s.ForUpdate(opts...) + }) + return _q +} + +// ForShare behaves similarly to ForUpdate, except that it acquires a shared mode lock +// on any rows that are read. Other sessions can read the rows, but cannot modify them +// until your transaction commits. +func (_q *SecretQuery) ForShare(opts ...sql.LockOption) *SecretQuery { + if _q.driver.Dialect() == dialect.Postgres { + _q.Unique(false) + } + _q.modifiers = append(_q.modifiers, func(s *sql.Selector) { + s.ForShare(opts...) + }) + return _q +} + +// SecretGroupBy is the group-by builder for Secret entities. +type SecretGroupBy struct { + selector + build *SecretQuery +} + +// Aggregate adds the given aggregation functions to the group-by query. +func (_g *SecretGroupBy) Aggregate(fns ...AggregateFunc) *SecretGroupBy { + _g.fns = append(_g.fns, fns...) + return _g +} + +// Scan applies the selector query and scans the result into the given value. +func (_g *SecretGroupBy) Scan(ctx context.Context, v any) error { + ctx = setContextOp(ctx, _g.build.ctx, ent.OpQueryGroupBy) + if err := _g.build.prepareQuery(ctx); err != nil { + return err + } + return scanWithInterceptors[*SecretQuery, *SecretGroupBy](ctx, _g.build, _g, _g.build.inters, v) +} + +func (_g *SecretGroupBy) sqlScan(ctx context.Context, root *SecretQuery, v any) error { + selector := root.sqlQuery(ctx).Select() + aggregation := make([]string, 0, len(_g.fns)) + for _, fn := range _g.fns { + aggregation = append(aggregation, fn(selector)) + } + if len(selector.SelectedColumns()) == 0 { + columns := make([]string, 0, len(*_g.flds)+len(_g.fns)) + for _, f := range *_g.flds { + columns = append(columns, selector.C(f)) + } + columns = append(columns, aggregation...) + selector.Select(columns...) + } + selector.GroupBy(selector.Columns(*_g.flds...)...) + if err := selector.Err(); err != nil { + return err + } + rows := &sql.Rows{} + query, args := selector.Query() + if err := _g.build.driver.Query(ctx, query, args, rows); err != nil { + return err + } + defer rows.Close() + return sql.ScanSlice(rows, v) +} + +// SecretSelect is the builder for selecting fields of Secret entities. +type SecretSelect struct { + *SecretQuery + selector +} + +// Aggregate adds the given aggregation functions to the selector query. +func (_s *SecretSelect) Aggregate(fns ...AggregateFunc) *SecretSelect { + _s.fns = append(_s.fns, fns...) + return _s +} + +// Scan applies the selector query and scans the result into the given value. +func (_s *SecretSelect) Scan(ctx context.Context, v any) error { + ctx = setContextOp(ctx, _s.ctx, ent.OpQuerySelect) + if err := _s.prepareQuery(ctx); err != nil { + return err + } + return scanWithInterceptors[*SecretQuery, *SecretSelect](ctx, _s.SecretQuery, _s, _s.inters, v) +} + +func (_s *SecretSelect) sqlScan(ctx context.Context, root *SecretQuery, v any) error { + selector := root.sqlQuery(ctx) + aggregation := make([]string, 0, len(_s.fns)) + for _, fn := range _s.fns { + aggregation = append(aggregation, fn(selector)) + } + switch n := len(*_s.selector.flds); { + case n == 0 && len(aggregation) > 0: + selector.Select(aggregation...) + case n != 0 && len(aggregation) > 0: + selector.AppendSelect(aggregation...) + } + rows := &sql.Rows{} + query, args := selector.Query() + if err := _s.driver.Query(ctx, query, args, rows); err != nil { + return err + } + defer rows.Close() + return sql.ScanSlice(rows, v) +} diff --git a/pkg/ent/secret_update.go b/pkg/ent/secret_update.go new file mode 100644 index 000000000..18de73a22 --- /dev/null +++ b/pkg/ent/secret_update.go @@ -0,0 +1,820 @@ +// Code generated by ent, DO NOT EDIT. + +package ent + +import ( + "context" + "errors" + "fmt" + "time" + + "entgo.io/ent/dialect/sql" + "entgo.io/ent/dialect/sql/sqlgraph" + "entgo.io/ent/schema/field" + "github.com/GoogleCloudPlatform/scion/pkg/ent/predicate" + "github.com/GoogleCloudPlatform/scion/pkg/ent/secret" +) + +// SecretUpdate is the builder for updating Secret entities. +type SecretUpdate struct { + config + hooks []Hook + mutation *SecretMutation +} + +// Where appends a list predicates to the SecretUpdate builder. +func (_u *SecretUpdate) Where(ps ...predicate.Secret) *SecretUpdate { + _u.mutation.Where(ps...) + return _u +} + +// SetKey sets the "key" field. +func (_u *SecretUpdate) SetKey(v string) *SecretUpdate { + _u.mutation.SetKey(v) + return _u +} + +// SetNillableKey sets the "key" field if the given value is not nil. +func (_u *SecretUpdate) SetNillableKey(v *string) *SecretUpdate { + if v != nil { + _u.SetKey(*v) + } + return _u +} + +// SetEncryptedValue sets the "encrypted_value" field. +func (_u *SecretUpdate) SetEncryptedValue(v string) *SecretUpdate { + _u.mutation.SetEncryptedValue(v) + return _u +} + +// SetNillableEncryptedValue sets the "encrypted_value" field if the given value is not nil. +func (_u *SecretUpdate) SetNillableEncryptedValue(v *string) *SecretUpdate { + if v != nil { + _u.SetEncryptedValue(*v) + } + return _u +} + +// SetSecretRef sets the "secret_ref" field. +func (_u *SecretUpdate) SetSecretRef(v string) *SecretUpdate { + _u.mutation.SetSecretRef(v) + return _u +} + +// SetNillableSecretRef sets the "secret_ref" field if the given value is not nil. +func (_u *SecretUpdate) SetNillableSecretRef(v *string) *SecretUpdate { + if v != nil { + _u.SetSecretRef(*v) + } + return _u +} + +// ClearSecretRef clears the value of the "secret_ref" field. +func (_u *SecretUpdate) ClearSecretRef() *SecretUpdate { + _u.mutation.ClearSecretRef() + return _u +} + +// SetSecretType sets the "secret_type" field. +func (_u *SecretUpdate) SetSecretType(v secret.SecretType) *SecretUpdate { + _u.mutation.SetSecretType(v) + return _u +} + +// SetNillableSecretType sets the "secret_type" field if the given value is not nil. +func (_u *SecretUpdate) SetNillableSecretType(v *secret.SecretType) *SecretUpdate { + if v != nil { + _u.SetSecretType(*v) + } + return _u +} + +// SetTarget sets the "target" field. +func (_u *SecretUpdate) SetTarget(v string) *SecretUpdate { + _u.mutation.SetTarget(v) + return _u +} + +// SetNillableTarget sets the "target" field if the given value is not nil. +func (_u *SecretUpdate) SetNillableTarget(v *string) *SecretUpdate { + if v != nil { + _u.SetTarget(*v) + } + return _u +} + +// ClearTarget clears the value of the "target" field. +func (_u *SecretUpdate) ClearTarget() *SecretUpdate { + _u.mutation.ClearTarget() + return _u +} + +// SetScope sets the "scope" field. +func (_u *SecretUpdate) SetScope(v string) *SecretUpdate { + _u.mutation.SetScope(v) + return _u +} + +// SetNillableScope sets the "scope" field if the given value is not nil. +func (_u *SecretUpdate) SetNillableScope(v *string) *SecretUpdate { + if v != nil { + _u.SetScope(*v) + } + return _u +} + +// SetScopeID sets the "scope_id" field. +func (_u *SecretUpdate) SetScopeID(v string) *SecretUpdate { + _u.mutation.SetScopeID(v) + return _u +} + +// SetNillableScopeID sets the "scope_id" field if the given value is not nil. +func (_u *SecretUpdate) SetNillableScopeID(v *string) *SecretUpdate { + if v != nil { + _u.SetScopeID(*v) + } + return _u +} + +// SetDescription sets the "description" field. +func (_u *SecretUpdate) SetDescription(v string) *SecretUpdate { + _u.mutation.SetDescription(v) + return _u +} + +// SetNillableDescription sets the "description" field if the given value is not nil. +func (_u *SecretUpdate) SetNillableDescription(v *string) *SecretUpdate { + if v != nil { + _u.SetDescription(*v) + } + return _u +} + +// ClearDescription clears the value of the "description" field. +func (_u *SecretUpdate) ClearDescription() *SecretUpdate { + _u.mutation.ClearDescription() + return _u +} + +// SetInjectionMode sets the "injection_mode" field. +func (_u *SecretUpdate) SetInjectionMode(v secret.InjectionMode) *SecretUpdate { + _u.mutation.SetInjectionMode(v) + return _u +} + +// SetNillableInjectionMode sets the "injection_mode" field if the given value is not nil. +func (_u *SecretUpdate) SetNillableInjectionMode(v *secret.InjectionMode) *SecretUpdate { + if v != nil { + _u.SetInjectionMode(*v) + } + return _u +} + +// SetAllowProgeny sets the "allow_progeny" field. +func (_u *SecretUpdate) SetAllowProgeny(v bool) *SecretUpdate { + _u.mutation.SetAllowProgeny(v) + return _u +} + +// SetNillableAllowProgeny sets the "allow_progeny" field if the given value is not nil. +func (_u *SecretUpdate) SetNillableAllowProgeny(v *bool) *SecretUpdate { + if v != nil { + _u.SetAllowProgeny(*v) + } + return _u +} + +// SetVersion sets the "version" field. +func (_u *SecretUpdate) SetVersion(v int) *SecretUpdate { + _u.mutation.ResetVersion() + _u.mutation.SetVersion(v) + return _u +} + +// SetNillableVersion sets the "version" field if the given value is not nil. +func (_u *SecretUpdate) SetNillableVersion(v *int) *SecretUpdate { + if v != nil { + _u.SetVersion(*v) + } + return _u +} + +// AddVersion adds value to the "version" field. +func (_u *SecretUpdate) AddVersion(v int) *SecretUpdate { + _u.mutation.AddVersion(v) + return _u +} + +// SetCreatedBy sets the "created_by" field. +func (_u *SecretUpdate) SetCreatedBy(v string) *SecretUpdate { + _u.mutation.SetCreatedBy(v) + return _u +} + +// SetNillableCreatedBy sets the "created_by" field if the given value is not nil. +func (_u *SecretUpdate) SetNillableCreatedBy(v *string) *SecretUpdate { + if v != nil { + _u.SetCreatedBy(*v) + } + return _u +} + +// ClearCreatedBy clears the value of the "created_by" field. +func (_u *SecretUpdate) ClearCreatedBy() *SecretUpdate { + _u.mutation.ClearCreatedBy() + return _u +} + +// SetUpdatedBy sets the "updated_by" field. +func (_u *SecretUpdate) SetUpdatedBy(v string) *SecretUpdate { + _u.mutation.SetUpdatedBy(v) + return _u +} + +// SetNillableUpdatedBy sets the "updated_by" field if the given value is not nil. +func (_u *SecretUpdate) SetNillableUpdatedBy(v *string) *SecretUpdate { + if v != nil { + _u.SetUpdatedBy(*v) + } + return _u +} + +// ClearUpdatedBy clears the value of the "updated_by" field. +func (_u *SecretUpdate) ClearUpdatedBy() *SecretUpdate { + _u.mutation.ClearUpdatedBy() + return _u +} + +// SetUpdated sets the "updated" field. +func (_u *SecretUpdate) SetUpdated(v time.Time) *SecretUpdate { + _u.mutation.SetUpdated(v) + return _u +} + +// Mutation returns the SecretMutation object of the builder. +func (_u *SecretUpdate) Mutation() *SecretMutation { + return _u.mutation +} + +// Save executes the query and returns the number of nodes affected by the update operation. +func (_u *SecretUpdate) Save(ctx context.Context) (int, error) { + _u.defaults() + return withHooks(ctx, _u.sqlSave, _u.mutation, _u.hooks) +} + +// SaveX is like Save, but panics if an error occurs. +func (_u *SecretUpdate) SaveX(ctx context.Context) int { + affected, err := _u.Save(ctx) + if err != nil { + panic(err) + } + return affected +} + +// Exec executes the query. +func (_u *SecretUpdate) Exec(ctx context.Context) error { + _, err := _u.Save(ctx) + return err +} + +// ExecX is like Exec, but panics if an error occurs. +func (_u *SecretUpdate) ExecX(ctx context.Context) { + if err := _u.Exec(ctx); err != nil { + panic(err) + } +} + +// defaults sets the default values of the builder before save. +func (_u *SecretUpdate) defaults() { + if _, ok := _u.mutation.Updated(); !ok { + v := secret.UpdateDefaultUpdated() + _u.mutation.SetUpdated(v) + } +} + +// check runs all checks and user-defined validators on the builder. +func (_u *SecretUpdate) check() error { + if v, ok := _u.mutation.Key(); ok { + if err := secret.KeyValidator(v); err != nil { + return &ValidationError{Name: "key", err: fmt.Errorf(`ent: validator failed for field "Secret.key": %w`, err)} + } + } + if v, ok := _u.mutation.SecretType(); ok { + if err := secret.SecretTypeValidator(v); err != nil { + return &ValidationError{Name: "secret_type", err: fmt.Errorf(`ent: validator failed for field "Secret.secret_type": %w`, err)} + } + } + if v, ok := _u.mutation.Scope(); ok { + if err := secret.ScopeValidator(v); err != nil { + return &ValidationError{Name: "scope", err: fmt.Errorf(`ent: validator failed for field "Secret.scope": %w`, err)} + } + } + if v, ok := _u.mutation.InjectionMode(); ok { + if err := secret.InjectionModeValidator(v); err != nil { + return &ValidationError{Name: "injection_mode", err: fmt.Errorf(`ent: validator failed for field "Secret.injection_mode": %w`, err)} + } + } + return nil +} + +func (_u *SecretUpdate) sqlSave(ctx context.Context) (_node int, err error) { + if err := _u.check(); err != nil { + return _node, err + } + _spec := sqlgraph.NewUpdateSpec(secret.Table, secret.Columns, sqlgraph.NewFieldSpec(secret.FieldID, field.TypeUUID)) + if ps := _u.mutation.predicates; len(ps) > 0 { + _spec.Predicate = func(selector *sql.Selector) { + for i := range ps { + ps[i](selector) + } + } + } + if value, ok := _u.mutation.Key(); ok { + _spec.SetField(secret.FieldKey, field.TypeString, value) + } + if value, ok := _u.mutation.EncryptedValue(); ok { + _spec.SetField(secret.FieldEncryptedValue, field.TypeString, value) + } + if value, ok := _u.mutation.SecretRef(); ok { + _spec.SetField(secret.FieldSecretRef, field.TypeString, value) + } + if _u.mutation.SecretRefCleared() { + _spec.ClearField(secret.FieldSecretRef, field.TypeString) + } + if value, ok := _u.mutation.SecretType(); ok { + _spec.SetField(secret.FieldSecretType, field.TypeEnum, value) + } + if value, ok := _u.mutation.Target(); ok { + _spec.SetField(secret.FieldTarget, field.TypeString, value) + } + if _u.mutation.TargetCleared() { + _spec.ClearField(secret.FieldTarget, field.TypeString) + } + if value, ok := _u.mutation.Scope(); ok { + _spec.SetField(secret.FieldScope, field.TypeString, value) + } + if value, ok := _u.mutation.ScopeID(); ok { + _spec.SetField(secret.FieldScopeID, field.TypeString, value) + } + if value, ok := _u.mutation.Description(); ok { + _spec.SetField(secret.FieldDescription, field.TypeString, value) + } + if _u.mutation.DescriptionCleared() { + _spec.ClearField(secret.FieldDescription, field.TypeString) + } + if value, ok := _u.mutation.InjectionMode(); ok { + _spec.SetField(secret.FieldInjectionMode, field.TypeEnum, value) + } + if value, ok := _u.mutation.AllowProgeny(); ok { + _spec.SetField(secret.FieldAllowProgeny, field.TypeBool, value) + } + if value, ok := _u.mutation.Version(); ok { + _spec.SetField(secret.FieldVersion, field.TypeInt, value) + } + if value, ok := _u.mutation.AddedVersion(); ok { + _spec.AddField(secret.FieldVersion, field.TypeInt, value) + } + if value, ok := _u.mutation.CreatedBy(); ok { + _spec.SetField(secret.FieldCreatedBy, field.TypeString, value) + } + if _u.mutation.CreatedByCleared() { + _spec.ClearField(secret.FieldCreatedBy, field.TypeString) + } + if value, ok := _u.mutation.UpdatedBy(); ok { + _spec.SetField(secret.FieldUpdatedBy, field.TypeString, value) + } + if _u.mutation.UpdatedByCleared() { + _spec.ClearField(secret.FieldUpdatedBy, field.TypeString) + } + if value, ok := _u.mutation.Updated(); ok { + _spec.SetField(secret.FieldUpdated, field.TypeTime, value) + } + if _node, err = sqlgraph.UpdateNodes(ctx, _u.driver, _spec); err != nil { + if _, ok := err.(*sqlgraph.NotFoundError); ok { + err = &NotFoundError{secret.Label} + } else if sqlgraph.IsConstraintError(err) { + err = &ConstraintError{msg: err.Error(), wrap: err} + } + return 0, err + } + _u.mutation.done = true + return _node, nil +} + +// SecretUpdateOne is the builder for updating a single Secret entity. +type SecretUpdateOne struct { + config + fields []string + hooks []Hook + mutation *SecretMutation +} + +// SetKey sets the "key" field. +func (_u *SecretUpdateOne) SetKey(v string) *SecretUpdateOne { + _u.mutation.SetKey(v) + return _u +} + +// SetNillableKey sets the "key" field if the given value is not nil. +func (_u *SecretUpdateOne) SetNillableKey(v *string) *SecretUpdateOne { + if v != nil { + _u.SetKey(*v) + } + return _u +} + +// SetEncryptedValue sets the "encrypted_value" field. +func (_u *SecretUpdateOne) SetEncryptedValue(v string) *SecretUpdateOne { + _u.mutation.SetEncryptedValue(v) + return _u +} + +// SetNillableEncryptedValue sets the "encrypted_value" field if the given value is not nil. +func (_u *SecretUpdateOne) SetNillableEncryptedValue(v *string) *SecretUpdateOne { + if v != nil { + _u.SetEncryptedValue(*v) + } + return _u +} + +// SetSecretRef sets the "secret_ref" field. +func (_u *SecretUpdateOne) SetSecretRef(v string) *SecretUpdateOne { + _u.mutation.SetSecretRef(v) + return _u +} + +// SetNillableSecretRef sets the "secret_ref" field if the given value is not nil. +func (_u *SecretUpdateOne) SetNillableSecretRef(v *string) *SecretUpdateOne { + if v != nil { + _u.SetSecretRef(*v) + } + return _u +} + +// ClearSecretRef clears the value of the "secret_ref" field. +func (_u *SecretUpdateOne) ClearSecretRef() *SecretUpdateOne { + _u.mutation.ClearSecretRef() + return _u +} + +// SetSecretType sets the "secret_type" field. +func (_u *SecretUpdateOne) SetSecretType(v secret.SecretType) *SecretUpdateOne { + _u.mutation.SetSecretType(v) + return _u +} + +// SetNillableSecretType sets the "secret_type" field if the given value is not nil. +func (_u *SecretUpdateOne) SetNillableSecretType(v *secret.SecretType) *SecretUpdateOne { + if v != nil { + _u.SetSecretType(*v) + } + return _u +} + +// SetTarget sets the "target" field. +func (_u *SecretUpdateOne) SetTarget(v string) *SecretUpdateOne { + _u.mutation.SetTarget(v) + return _u +} + +// SetNillableTarget sets the "target" field if the given value is not nil. +func (_u *SecretUpdateOne) SetNillableTarget(v *string) *SecretUpdateOne { + if v != nil { + _u.SetTarget(*v) + } + return _u +} + +// ClearTarget clears the value of the "target" field. +func (_u *SecretUpdateOne) ClearTarget() *SecretUpdateOne { + _u.mutation.ClearTarget() + return _u +} + +// SetScope sets the "scope" field. +func (_u *SecretUpdateOne) SetScope(v string) *SecretUpdateOne { + _u.mutation.SetScope(v) + return _u +} + +// SetNillableScope sets the "scope" field if the given value is not nil. +func (_u *SecretUpdateOne) SetNillableScope(v *string) *SecretUpdateOne { + if v != nil { + _u.SetScope(*v) + } + return _u +} + +// SetScopeID sets the "scope_id" field. +func (_u *SecretUpdateOne) SetScopeID(v string) *SecretUpdateOne { + _u.mutation.SetScopeID(v) + return _u +} + +// SetNillableScopeID sets the "scope_id" field if the given value is not nil. +func (_u *SecretUpdateOne) SetNillableScopeID(v *string) *SecretUpdateOne { + if v != nil { + _u.SetScopeID(*v) + } + return _u +} + +// SetDescription sets the "description" field. +func (_u *SecretUpdateOne) SetDescription(v string) *SecretUpdateOne { + _u.mutation.SetDescription(v) + return _u +} + +// SetNillableDescription sets the "description" field if the given value is not nil. +func (_u *SecretUpdateOne) SetNillableDescription(v *string) *SecretUpdateOne { + if v != nil { + _u.SetDescription(*v) + } + return _u +} + +// ClearDescription clears the value of the "description" field. +func (_u *SecretUpdateOne) ClearDescription() *SecretUpdateOne { + _u.mutation.ClearDescription() + return _u +} + +// SetInjectionMode sets the "injection_mode" field. +func (_u *SecretUpdateOne) SetInjectionMode(v secret.InjectionMode) *SecretUpdateOne { + _u.mutation.SetInjectionMode(v) + return _u +} + +// SetNillableInjectionMode sets the "injection_mode" field if the given value is not nil. +func (_u *SecretUpdateOne) SetNillableInjectionMode(v *secret.InjectionMode) *SecretUpdateOne { + if v != nil { + _u.SetInjectionMode(*v) + } + return _u +} + +// SetAllowProgeny sets the "allow_progeny" field. +func (_u *SecretUpdateOne) SetAllowProgeny(v bool) *SecretUpdateOne { + _u.mutation.SetAllowProgeny(v) + return _u +} + +// SetNillableAllowProgeny sets the "allow_progeny" field if the given value is not nil. +func (_u *SecretUpdateOne) SetNillableAllowProgeny(v *bool) *SecretUpdateOne { + if v != nil { + _u.SetAllowProgeny(*v) + } + return _u +} + +// SetVersion sets the "version" field. +func (_u *SecretUpdateOne) SetVersion(v int) *SecretUpdateOne { + _u.mutation.ResetVersion() + _u.mutation.SetVersion(v) + return _u +} + +// SetNillableVersion sets the "version" field if the given value is not nil. +func (_u *SecretUpdateOne) SetNillableVersion(v *int) *SecretUpdateOne { + if v != nil { + _u.SetVersion(*v) + } + return _u +} + +// AddVersion adds value to the "version" field. +func (_u *SecretUpdateOne) AddVersion(v int) *SecretUpdateOne { + _u.mutation.AddVersion(v) + return _u +} + +// SetCreatedBy sets the "created_by" field. +func (_u *SecretUpdateOne) SetCreatedBy(v string) *SecretUpdateOne { + _u.mutation.SetCreatedBy(v) + return _u +} + +// SetNillableCreatedBy sets the "created_by" field if the given value is not nil. +func (_u *SecretUpdateOne) SetNillableCreatedBy(v *string) *SecretUpdateOne { + if v != nil { + _u.SetCreatedBy(*v) + } + return _u +} + +// ClearCreatedBy clears the value of the "created_by" field. +func (_u *SecretUpdateOne) ClearCreatedBy() *SecretUpdateOne { + _u.mutation.ClearCreatedBy() + return _u +} + +// SetUpdatedBy sets the "updated_by" field. +func (_u *SecretUpdateOne) SetUpdatedBy(v string) *SecretUpdateOne { + _u.mutation.SetUpdatedBy(v) + return _u +} + +// SetNillableUpdatedBy sets the "updated_by" field if the given value is not nil. +func (_u *SecretUpdateOne) SetNillableUpdatedBy(v *string) *SecretUpdateOne { + if v != nil { + _u.SetUpdatedBy(*v) + } + return _u +} + +// ClearUpdatedBy clears the value of the "updated_by" field. +func (_u *SecretUpdateOne) ClearUpdatedBy() *SecretUpdateOne { + _u.mutation.ClearUpdatedBy() + return _u +} + +// SetUpdated sets the "updated" field. +func (_u *SecretUpdateOne) SetUpdated(v time.Time) *SecretUpdateOne { + _u.mutation.SetUpdated(v) + return _u +} + +// Mutation returns the SecretMutation object of the builder. +func (_u *SecretUpdateOne) Mutation() *SecretMutation { + return _u.mutation +} + +// Where appends a list predicates to the SecretUpdate builder. +func (_u *SecretUpdateOne) Where(ps ...predicate.Secret) *SecretUpdateOne { + _u.mutation.Where(ps...) + return _u +} + +// Select allows selecting one or more fields (columns) of the returned entity. +// The default is selecting all fields defined in the entity schema. +func (_u *SecretUpdateOne) Select(field string, fields ...string) *SecretUpdateOne { + _u.fields = append([]string{field}, fields...) + return _u +} + +// Save executes the query and returns the updated Secret entity. +func (_u *SecretUpdateOne) Save(ctx context.Context) (*Secret, error) { + _u.defaults() + return withHooks(ctx, _u.sqlSave, _u.mutation, _u.hooks) +} + +// SaveX is like Save, but panics if an error occurs. +func (_u *SecretUpdateOne) SaveX(ctx context.Context) *Secret { + node, err := _u.Save(ctx) + if err != nil { + panic(err) + } + return node +} + +// Exec executes the query on the entity. +func (_u *SecretUpdateOne) Exec(ctx context.Context) error { + _, err := _u.Save(ctx) + return err +} + +// ExecX is like Exec, but panics if an error occurs. +func (_u *SecretUpdateOne) ExecX(ctx context.Context) { + if err := _u.Exec(ctx); err != nil { + panic(err) + } +} + +// defaults sets the default values of the builder before save. +func (_u *SecretUpdateOne) defaults() { + if _, ok := _u.mutation.Updated(); !ok { + v := secret.UpdateDefaultUpdated() + _u.mutation.SetUpdated(v) + } +} + +// check runs all checks and user-defined validators on the builder. +func (_u *SecretUpdateOne) check() error { + if v, ok := _u.mutation.Key(); ok { + if err := secret.KeyValidator(v); err != nil { + return &ValidationError{Name: "key", err: fmt.Errorf(`ent: validator failed for field "Secret.key": %w`, err)} + } + } + if v, ok := _u.mutation.SecretType(); ok { + if err := secret.SecretTypeValidator(v); err != nil { + return &ValidationError{Name: "secret_type", err: fmt.Errorf(`ent: validator failed for field "Secret.secret_type": %w`, err)} + } + } + if v, ok := _u.mutation.Scope(); ok { + if err := secret.ScopeValidator(v); err != nil { + return &ValidationError{Name: "scope", err: fmt.Errorf(`ent: validator failed for field "Secret.scope": %w`, err)} + } + } + if v, ok := _u.mutation.InjectionMode(); ok { + if err := secret.InjectionModeValidator(v); err != nil { + return &ValidationError{Name: "injection_mode", err: fmt.Errorf(`ent: validator failed for field "Secret.injection_mode": %w`, err)} + } + } + return nil +} + +func (_u *SecretUpdateOne) sqlSave(ctx context.Context) (_node *Secret, err error) { + if err := _u.check(); err != nil { + return _node, err + } + _spec := sqlgraph.NewUpdateSpec(secret.Table, secret.Columns, sqlgraph.NewFieldSpec(secret.FieldID, field.TypeUUID)) + id, ok := _u.mutation.ID() + if !ok { + return nil, &ValidationError{Name: "id", err: errors.New(`ent: missing "Secret.id" for update`)} + } + _spec.Node.ID.Value = id + if fields := _u.fields; len(fields) > 0 { + _spec.Node.Columns = make([]string, 0, len(fields)) + _spec.Node.Columns = append(_spec.Node.Columns, secret.FieldID) + for _, f := range fields { + if !secret.ValidColumn(f) { + return nil, &ValidationError{Name: f, err: fmt.Errorf("ent: invalid field %q for query", f)} + } + if f != secret.FieldID { + _spec.Node.Columns = append(_spec.Node.Columns, f) + } + } + } + if ps := _u.mutation.predicates; len(ps) > 0 { + _spec.Predicate = func(selector *sql.Selector) { + for i := range ps { + ps[i](selector) + } + } + } + if value, ok := _u.mutation.Key(); ok { + _spec.SetField(secret.FieldKey, field.TypeString, value) + } + if value, ok := _u.mutation.EncryptedValue(); ok { + _spec.SetField(secret.FieldEncryptedValue, field.TypeString, value) + } + if value, ok := _u.mutation.SecretRef(); ok { + _spec.SetField(secret.FieldSecretRef, field.TypeString, value) + } + if _u.mutation.SecretRefCleared() { + _spec.ClearField(secret.FieldSecretRef, field.TypeString) + } + if value, ok := _u.mutation.SecretType(); ok { + _spec.SetField(secret.FieldSecretType, field.TypeEnum, value) + } + if value, ok := _u.mutation.Target(); ok { + _spec.SetField(secret.FieldTarget, field.TypeString, value) + } + if _u.mutation.TargetCleared() { + _spec.ClearField(secret.FieldTarget, field.TypeString) + } + if value, ok := _u.mutation.Scope(); ok { + _spec.SetField(secret.FieldScope, field.TypeString, value) + } + if value, ok := _u.mutation.ScopeID(); ok { + _spec.SetField(secret.FieldScopeID, field.TypeString, value) + } + if value, ok := _u.mutation.Description(); ok { + _spec.SetField(secret.FieldDescription, field.TypeString, value) + } + if _u.mutation.DescriptionCleared() { + _spec.ClearField(secret.FieldDescription, field.TypeString) + } + if value, ok := _u.mutation.InjectionMode(); ok { + _spec.SetField(secret.FieldInjectionMode, field.TypeEnum, value) + } + if value, ok := _u.mutation.AllowProgeny(); ok { + _spec.SetField(secret.FieldAllowProgeny, field.TypeBool, value) + } + if value, ok := _u.mutation.Version(); ok { + _spec.SetField(secret.FieldVersion, field.TypeInt, value) + } + if value, ok := _u.mutation.AddedVersion(); ok { + _spec.AddField(secret.FieldVersion, field.TypeInt, value) + } + if value, ok := _u.mutation.CreatedBy(); ok { + _spec.SetField(secret.FieldCreatedBy, field.TypeString, value) + } + if _u.mutation.CreatedByCleared() { + _spec.ClearField(secret.FieldCreatedBy, field.TypeString) + } + if value, ok := _u.mutation.UpdatedBy(); ok { + _spec.SetField(secret.FieldUpdatedBy, field.TypeString, value) + } + if _u.mutation.UpdatedByCleared() { + _spec.ClearField(secret.FieldUpdatedBy, field.TypeString) + } + if value, ok := _u.mutation.Updated(); ok { + _spec.SetField(secret.FieldUpdated, field.TypeTime, value) + } + _node = &Secret{config: _u.config} + _spec.Assign = _node.assignValues + _spec.ScanValues = _node.scanValues + if err = sqlgraph.UpdateNode(ctx, _u.driver, _spec); err != nil { + if _, ok := err.(*sqlgraph.NotFoundError); ok { + err = &NotFoundError{secret.Label} + } else if sqlgraph.IsConstraintError(err) { + err = &ConstraintError{msg: err.Error(), wrap: err} + } + return nil, err + } + _u.mutation.done = true + return _node, nil +} diff --git a/pkg/ent/subscriptiontemplate.go b/pkg/ent/subscriptiontemplate.go new file mode 100644 index 000000000..ee67c9fce --- /dev/null +++ b/pkg/ent/subscriptiontemplate.go @@ -0,0 +1,153 @@ +// Code generated by ent, DO NOT EDIT. + +package ent + +import ( + "fmt" + "strings" + + "entgo.io/ent" + "entgo.io/ent/dialect/sql" + "github.com/GoogleCloudPlatform/scion/pkg/ent/subscriptiontemplate" + "github.com/google/uuid" +) + +// SubscriptionTemplate is the model entity for the SubscriptionTemplate schema. +type SubscriptionTemplate struct { + config `json:"-"` + // ID of the ent. + ID uuid.UUID `json:"id,omitempty"` + // Name holds the value of the "name" field. + Name string `json:"name,omitempty"` + // Scope holds the value of the "scope" field. + Scope string `json:"scope,omitempty"` + // TriggerActivities holds the value of the "trigger_activities" field. + TriggerActivities string `json:"trigger_activities,omitempty"` + // ProjectID holds the value of the "project_id" field. + ProjectID *uuid.UUID `json:"project_id,omitempty"` + // CreatedBy holds the value of the "created_by" field. + CreatedBy string `json:"created_by,omitempty"` + selectValues sql.SelectValues +} + +// scanValues returns the types for scanning values from sql.Rows. +func (*SubscriptionTemplate) scanValues(columns []string) ([]any, error) { + values := make([]any, len(columns)) + for i := range columns { + switch columns[i] { + case subscriptiontemplate.FieldProjectID: + values[i] = &sql.NullScanner{S: new(uuid.UUID)} + case subscriptiontemplate.FieldName, subscriptiontemplate.FieldScope, subscriptiontemplate.FieldTriggerActivities, subscriptiontemplate.FieldCreatedBy: + values[i] = new(sql.NullString) + case subscriptiontemplate.FieldID: + values[i] = new(uuid.UUID) + default: + values[i] = new(sql.UnknownType) + } + } + return values, nil +} + +// assignValues assigns the values that were returned from sql.Rows (after scanning) +// to the SubscriptionTemplate fields. +func (_m *SubscriptionTemplate) assignValues(columns []string, values []any) error { + if m, n := len(values), len(columns); m < n { + return fmt.Errorf("mismatch number of scan values: %d != %d", m, n) + } + for i := range columns { + switch columns[i] { + case subscriptiontemplate.FieldID: + if value, ok := values[i].(*uuid.UUID); !ok { + return fmt.Errorf("unexpected type %T for field id", values[i]) + } else if value != nil { + _m.ID = *value + } + case subscriptiontemplate.FieldName: + if value, ok := values[i].(*sql.NullString); !ok { + return fmt.Errorf("unexpected type %T for field name", values[i]) + } else if value.Valid { + _m.Name = value.String + } + case subscriptiontemplate.FieldScope: + if value, ok := values[i].(*sql.NullString); !ok { + return fmt.Errorf("unexpected type %T for field scope", values[i]) + } else if value.Valid { + _m.Scope = value.String + } + case subscriptiontemplate.FieldTriggerActivities: + if value, ok := values[i].(*sql.NullString); !ok { + return fmt.Errorf("unexpected type %T for field trigger_activities", values[i]) + } else if value.Valid { + _m.TriggerActivities = value.String + } + case subscriptiontemplate.FieldProjectID: + if value, ok := values[i].(*sql.NullScanner); !ok { + return fmt.Errorf("unexpected type %T for field project_id", values[i]) + } else if value.Valid { + _m.ProjectID = new(uuid.UUID) + *_m.ProjectID = *value.S.(*uuid.UUID) + } + case subscriptiontemplate.FieldCreatedBy: + if value, ok := values[i].(*sql.NullString); !ok { + return fmt.Errorf("unexpected type %T for field created_by", values[i]) + } else if value.Valid { + _m.CreatedBy = value.String + } + default: + _m.selectValues.Set(columns[i], values[i]) + } + } + return nil +} + +// Value returns the ent.Value that was dynamically selected and assigned to the SubscriptionTemplate. +// This includes values selected through modifiers, order, etc. +func (_m *SubscriptionTemplate) Value(name string) (ent.Value, error) { + return _m.selectValues.Get(name) +} + +// Update returns a builder for updating this SubscriptionTemplate. +// Note that you need to call SubscriptionTemplate.Unwrap() before calling this method if this SubscriptionTemplate +// was returned from a transaction, and the transaction was committed or rolled back. +func (_m *SubscriptionTemplate) Update() *SubscriptionTemplateUpdateOne { + return NewSubscriptionTemplateClient(_m.config).UpdateOne(_m) +} + +// Unwrap unwraps the SubscriptionTemplate entity that was returned from a transaction after it was closed, +// so that all future queries will be executed through the driver which created the transaction. +func (_m *SubscriptionTemplate) Unwrap() *SubscriptionTemplate { + _tx, ok := _m.config.driver.(*txDriver) + if !ok { + panic("ent: SubscriptionTemplate is not a transactional entity") + } + _m.config.driver = _tx.drv + return _m +} + +// String implements the fmt.Stringer. +func (_m *SubscriptionTemplate) String() string { + var builder strings.Builder + builder.WriteString("SubscriptionTemplate(") + builder.WriteString(fmt.Sprintf("id=%v, ", _m.ID)) + builder.WriteString("name=") + builder.WriteString(_m.Name) + builder.WriteString(", ") + builder.WriteString("scope=") + builder.WriteString(_m.Scope) + builder.WriteString(", ") + builder.WriteString("trigger_activities=") + builder.WriteString(_m.TriggerActivities) + builder.WriteString(", ") + if v := _m.ProjectID; v != nil { + builder.WriteString("project_id=") + builder.WriteString(fmt.Sprintf("%v", *v)) + } + builder.WriteString(", ") + builder.WriteString("created_by=") + builder.WriteString(_m.CreatedBy) + builder.WriteByte(')') + return builder.String() +} + +// SubscriptionTemplates is a parsable slice of SubscriptionTemplate. +type SubscriptionTemplates []*SubscriptionTemplate diff --git a/pkg/ent/subscriptiontemplate/subscriptiontemplate.go b/pkg/ent/subscriptiontemplate/subscriptiontemplate.go new file mode 100644 index 000000000..4a8ed211c --- /dev/null +++ b/pkg/ent/subscriptiontemplate/subscriptiontemplate.go @@ -0,0 +1,93 @@ +// Code generated by ent, DO NOT EDIT. + +package subscriptiontemplate + +import ( + "entgo.io/ent/dialect/sql" + "github.com/google/uuid" +) + +const ( + // Label holds the string label denoting the subscriptiontemplate type in the database. + Label = "subscription_template" + // FieldID holds the string denoting the id field in the database. + FieldID = "id" + // FieldName holds the string denoting the name field in the database. + FieldName = "name" + // FieldScope holds the string denoting the scope field in the database. + FieldScope = "scope" + // FieldTriggerActivities holds the string denoting the trigger_activities field in the database. + FieldTriggerActivities = "trigger_activities" + // FieldProjectID holds the string denoting the project_id field in the database. + FieldProjectID = "project_id" + // FieldCreatedBy holds the string denoting the created_by field in the database. + FieldCreatedBy = "created_by" + // Table holds the table name of the subscriptiontemplate in the database. + Table = "subscription_templates" +) + +// Columns holds all SQL columns for subscriptiontemplate fields. +var Columns = []string{ + FieldID, + FieldName, + FieldScope, + FieldTriggerActivities, + FieldProjectID, + FieldCreatedBy, +} + +// ValidColumn reports if the column name is valid (part of the table columns). +func ValidColumn(column string) bool { + for i := range Columns { + if column == Columns[i] { + return true + } + } + return false +} + +var ( + // NameValidator is a validator for the "name" field. It is called by the builders before save. + NameValidator func(string) error + // DefaultScope holds the default value on creation for the "scope" field. + DefaultScope string + // TriggerActivitiesValidator is a validator for the "trigger_activities" field. It is called by the builders before save. + TriggerActivitiesValidator func(string) error + // CreatedByValidator is a validator for the "created_by" field. It is called by the builders before save. + CreatedByValidator func(string) error + // DefaultID holds the default value on creation for the "id" field. + DefaultID func() uuid.UUID +) + +// OrderOption defines the ordering options for the SubscriptionTemplate queries. +type OrderOption func(*sql.Selector) + +// ByID orders the results by the id field. +func ByID(opts ...sql.OrderTermOption) OrderOption { + return sql.OrderByField(FieldID, opts...).ToFunc() +} + +// ByName orders the results by the name field. +func ByName(opts ...sql.OrderTermOption) OrderOption { + return sql.OrderByField(FieldName, opts...).ToFunc() +} + +// ByScope orders the results by the scope field. +func ByScope(opts ...sql.OrderTermOption) OrderOption { + return sql.OrderByField(FieldScope, opts...).ToFunc() +} + +// ByTriggerActivities orders the results by the trigger_activities field. +func ByTriggerActivities(opts ...sql.OrderTermOption) OrderOption { + return sql.OrderByField(FieldTriggerActivities, opts...).ToFunc() +} + +// ByProjectID orders the results by the project_id field. +func ByProjectID(opts ...sql.OrderTermOption) OrderOption { + return sql.OrderByField(FieldProjectID, opts...).ToFunc() +} + +// ByCreatedBy orders the results by the created_by field. +func ByCreatedBy(opts ...sql.OrderTermOption) OrderOption { + return sql.OrderByField(FieldCreatedBy, opts...).ToFunc() +} diff --git a/pkg/ent/subscriptiontemplate/where.go b/pkg/ent/subscriptiontemplate/where.go new file mode 100644 index 000000000..0afd24e9e --- /dev/null +++ b/pkg/ent/subscriptiontemplate/where.go @@ -0,0 +1,404 @@ +// Code generated by ent, DO NOT EDIT. + +package subscriptiontemplate + +import ( + "entgo.io/ent/dialect/sql" + "github.com/GoogleCloudPlatform/scion/pkg/ent/predicate" + "github.com/google/uuid" +) + +// ID filters vertices based on their ID field. +func ID(id uuid.UUID) predicate.SubscriptionTemplate { + return predicate.SubscriptionTemplate(sql.FieldEQ(FieldID, id)) +} + +// IDEQ applies the EQ predicate on the ID field. +func IDEQ(id uuid.UUID) predicate.SubscriptionTemplate { + return predicate.SubscriptionTemplate(sql.FieldEQ(FieldID, id)) +} + +// IDNEQ applies the NEQ predicate on the ID field. +func IDNEQ(id uuid.UUID) predicate.SubscriptionTemplate { + return predicate.SubscriptionTemplate(sql.FieldNEQ(FieldID, id)) +} + +// IDIn applies the In predicate on the ID field. +func IDIn(ids ...uuid.UUID) predicate.SubscriptionTemplate { + return predicate.SubscriptionTemplate(sql.FieldIn(FieldID, ids...)) +} + +// IDNotIn applies the NotIn predicate on the ID field. +func IDNotIn(ids ...uuid.UUID) predicate.SubscriptionTemplate { + return predicate.SubscriptionTemplate(sql.FieldNotIn(FieldID, ids...)) +} + +// IDGT applies the GT predicate on the ID field. +func IDGT(id uuid.UUID) predicate.SubscriptionTemplate { + return predicate.SubscriptionTemplate(sql.FieldGT(FieldID, id)) +} + +// IDGTE applies the GTE predicate on the ID field. +func IDGTE(id uuid.UUID) predicate.SubscriptionTemplate { + return predicate.SubscriptionTemplate(sql.FieldGTE(FieldID, id)) +} + +// IDLT applies the LT predicate on the ID field. +func IDLT(id uuid.UUID) predicate.SubscriptionTemplate { + return predicate.SubscriptionTemplate(sql.FieldLT(FieldID, id)) +} + +// IDLTE applies the LTE predicate on the ID field. +func IDLTE(id uuid.UUID) predicate.SubscriptionTemplate { + return predicate.SubscriptionTemplate(sql.FieldLTE(FieldID, id)) +} + +// Name applies equality check predicate on the "name" field. It's identical to NameEQ. +func Name(v string) predicate.SubscriptionTemplate { + return predicate.SubscriptionTemplate(sql.FieldEQ(FieldName, v)) +} + +// Scope applies equality check predicate on the "scope" field. It's identical to ScopeEQ. +func Scope(v string) predicate.SubscriptionTemplate { + return predicate.SubscriptionTemplate(sql.FieldEQ(FieldScope, v)) +} + +// TriggerActivities applies equality check predicate on the "trigger_activities" field. It's identical to TriggerActivitiesEQ. +func TriggerActivities(v string) predicate.SubscriptionTemplate { + return predicate.SubscriptionTemplate(sql.FieldEQ(FieldTriggerActivities, v)) +} + +// ProjectID applies equality check predicate on the "project_id" field. It's identical to ProjectIDEQ. +func ProjectID(v uuid.UUID) predicate.SubscriptionTemplate { + return predicate.SubscriptionTemplate(sql.FieldEQ(FieldProjectID, v)) +} + +// CreatedBy applies equality check predicate on the "created_by" field. It's identical to CreatedByEQ. +func CreatedBy(v string) predicate.SubscriptionTemplate { + return predicate.SubscriptionTemplate(sql.FieldEQ(FieldCreatedBy, v)) +} + +// NameEQ applies the EQ predicate on the "name" field. +func NameEQ(v string) predicate.SubscriptionTemplate { + return predicate.SubscriptionTemplate(sql.FieldEQ(FieldName, v)) +} + +// NameNEQ applies the NEQ predicate on the "name" field. +func NameNEQ(v string) predicate.SubscriptionTemplate { + return predicate.SubscriptionTemplate(sql.FieldNEQ(FieldName, v)) +} + +// NameIn applies the In predicate on the "name" field. +func NameIn(vs ...string) predicate.SubscriptionTemplate { + return predicate.SubscriptionTemplate(sql.FieldIn(FieldName, vs...)) +} + +// NameNotIn applies the NotIn predicate on the "name" field. +func NameNotIn(vs ...string) predicate.SubscriptionTemplate { + return predicate.SubscriptionTemplate(sql.FieldNotIn(FieldName, vs...)) +} + +// NameGT applies the GT predicate on the "name" field. +func NameGT(v string) predicate.SubscriptionTemplate { + return predicate.SubscriptionTemplate(sql.FieldGT(FieldName, v)) +} + +// NameGTE applies the GTE predicate on the "name" field. +func NameGTE(v string) predicate.SubscriptionTemplate { + return predicate.SubscriptionTemplate(sql.FieldGTE(FieldName, v)) +} + +// NameLT applies the LT predicate on the "name" field. +func NameLT(v string) predicate.SubscriptionTemplate { + return predicate.SubscriptionTemplate(sql.FieldLT(FieldName, v)) +} + +// NameLTE applies the LTE predicate on the "name" field. +func NameLTE(v string) predicate.SubscriptionTemplate { + return predicate.SubscriptionTemplate(sql.FieldLTE(FieldName, v)) +} + +// NameContains applies the Contains predicate on the "name" field. +func NameContains(v string) predicate.SubscriptionTemplate { + return predicate.SubscriptionTemplate(sql.FieldContains(FieldName, v)) +} + +// NameHasPrefix applies the HasPrefix predicate on the "name" field. +func NameHasPrefix(v string) predicate.SubscriptionTemplate { + return predicate.SubscriptionTemplate(sql.FieldHasPrefix(FieldName, v)) +} + +// NameHasSuffix applies the HasSuffix predicate on the "name" field. +func NameHasSuffix(v string) predicate.SubscriptionTemplate { + return predicate.SubscriptionTemplate(sql.FieldHasSuffix(FieldName, v)) +} + +// NameEqualFold applies the EqualFold predicate on the "name" field. +func NameEqualFold(v string) predicate.SubscriptionTemplate { + return predicate.SubscriptionTemplate(sql.FieldEqualFold(FieldName, v)) +} + +// NameContainsFold applies the ContainsFold predicate on the "name" field. +func NameContainsFold(v string) predicate.SubscriptionTemplate { + return predicate.SubscriptionTemplate(sql.FieldContainsFold(FieldName, v)) +} + +// ScopeEQ applies the EQ predicate on the "scope" field. +func ScopeEQ(v string) predicate.SubscriptionTemplate { + return predicate.SubscriptionTemplate(sql.FieldEQ(FieldScope, v)) +} + +// ScopeNEQ applies the NEQ predicate on the "scope" field. +func ScopeNEQ(v string) predicate.SubscriptionTemplate { + return predicate.SubscriptionTemplate(sql.FieldNEQ(FieldScope, v)) +} + +// ScopeIn applies the In predicate on the "scope" field. +func ScopeIn(vs ...string) predicate.SubscriptionTemplate { + return predicate.SubscriptionTemplate(sql.FieldIn(FieldScope, vs...)) +} + +// ScopeNotIn applies the NotIn predicate on the "scope" field. +func ScopeNotIn(vs ...string) predicate.SubscriptionTemplate { + return predicate.SubscriptionTemplate(sql.FieldNotIn(FieldScope, vs...)) +} + +// ScopeGT applies the GT predicate on the "scope" field. +func ScopeGT(v string) predicate.SubscriptionTemplate { + return predicate.SubscriptionTemplate(sql.FieldGT(FieldScope, v)) +} + +// ScopeGTE applies the GTE predicate on the "scope" field. +func ScopeGTE(v string) predicate.SubscriptionTemplate { + return predicate.SubscriptionTemplate(sql.FieldGTE(FieldScope, v)) +} + +// ScopeLT applies the LT predicate on the "scope" field. +func ScopeLT(v string) predicate.SubscriptionTemplate { + return predicate.SubscriptionTemplate(sql.FieldLT(FieldScope, v)) +} + +// ScopeLTE applies the LTE predicate on the "scope" field. +func ScopeLTE(v string) predicate.SubscriptionTemplate { + return predicate.SubscriptionTemplate(sql.FieldLTE(FieldScope, v)) +} + +// ScopeContains applies the Contains predicate on the "scope" field. +func ScopeContains(v string) predicate.SubscriptionTemplate { + return predicate.SubscriptionTemplate(sql.FieldContains(FieldScope, v)) +} + +// ScopeHasPrefix applies the HasPrefix predicate on the "scope" field. +func ScopeHasPrefix(v string) predicate.SubscriptionTemplate { + return predicate.SubscriptionTemplate(sql.FieldHasPrefix(FieldScope, v)) +} + +// ScopeHasSuffix applies the HasSuffix predicate on the "scope" field. +func ScopeHasSuffix(v string) predicate.SubscriptionTemplate { + return predicate.SubscriptionTemplate(sql.FieldHasSuffix(FieldScope, v)) +} + +// ScopeEqualFold applies the EqualFold predicate on the "scope" field. +func ScopeEqualFold(v string) predicate.SubscriptionTemplate { + return predicate.SubscriptionTemplate(sql.FieldEqualFold(FieldScope, v)) +} + +// ScopeContainsFold applies the ContainsFold predicate on the "scope" field. +func ScopeContainsFold(v string) predicate.SubscriptionTemplate { + return predicate.SubscriptionTemplate(sql.FieldContainsFold(FieldScope, v)) +} + +// TriggerActivitiesEQ applies the EQ predicate on the "trigger_activities" field. +func TriggerActivitiesEQ(v string) predicate.SubscriptionTemplate { + return predicate.SubscriptionTemplate(sql.FieldEQ(FieldTriggerActivities, v)) +} + +// TriggerActivitiesNEQ applies the NEQ predicate on the "trigger_activities" field. +func TriggerActivitiesNEQ(v string) predicate.SubscriptionTemplate { + return predicate.SubscriptionTemplate(sql.FieldNEQ(FieldTriggerActivities, v)) +} + +// TriggerActivitiesIn applies the In predicate on the "trigger_activities" field. +func TriggerActivitiesIn(vs ...string) predicate.SubscriptionTemplate { + return predicate.SubscriptionTemplate(sql.FieldIn(FieldTriggerActivities, vs...)) +} + +// TriggerActivitiesNotIn applies the NotIn predicate on the "trigger_activities" field. +func TriggerActivitiesNotIn(vs ...string) predicate.SubscriptionTemplate { + return predicate.SubscriptionTemplate(sql.FieldNotIn(FieldTriggerActivities, vs...)) +} + +// TriggerActivitiesGT applies the GT predicate on the "trigger_activities" field. +func TriggerActivitiesGT(v string) predicate.SubscriptionTemplate { + return predicate.SubscriptionTemplate(sql.FieldGT(FieldTriggerActivities, v)) +} + +// TriggerActivitiesGTE applies the GTE predicate on the "trigger_activities" field. +func TriggerActivitiesGTE(v string) predicate.SubscriptionTemplate { + return predicate.SubscriptionTemplate(sql.FieldGTE(FieldTriggerActivities, v)) +} + +// TriggerActivitiesLT applies the LT predicate on the "trigger_activities" field. +func TriggerActivitiesLT(v string) predicate.SubscriptionTemplate { + return predicate.SubscriptionTemplate(sql.FieldLT(FieldTriggerActivities, v)) +} + +// TriggerActivitiesLTE applies the LTE predicate on the "trigger_activities" field. +func TriggerActivitiesLTE(v string) predicate.SubscriptionTemplate { + return predicate.SubscriptionTemplate(sql.FieldLTE(FieldTriggerActivities, v)) +} + +// TriggerActivitiesContains applies the Contains predicate on the "trigger_activities" field. +func TriggerActivitiesContains(v string) predicate.SubscriptionTemplate { + return predicate.SubscriptionTemplate(sql.FieldContains(FieldTriggerActivities, v)) +} + +// TriggerActivitiesHasPrefix applies the HasPrefix predicate on the "trigger_activities" field. +func TriggerActivitiesHasPrefix(v string) predicate.SubscriptionTemplate { + return predicate.SubscriptionTemplate(sql.FieldHasPrefix(FieldTriggerActivities, v)) +} + +// TriggerActivitiesHasSuffix applies the HasSuffix predicate on the "trigger_activities" field. +func TriggerActivitiesHasSuffix(v string) predicate.SubscriptionTemplate { + return predicate.SubscriptionTemplate(sql.FieldHasSuffix(FieldTriggerActivities, v)) +} + +// TriggerActivitiesEqualFold applies the EqualFold predicate on the "trigger_activities" field. +func TriggerActivitiesEqualFold(v string) predicate.SubscriptionTemplate { + return predicate.SubscriptionTemplate(sql.FieldEqualFold(FieldTriggerActivities, v)) +} + +// TriggerActivitiesContainsFold applies the ContainsFold predicate on the "trigger_activities" field. +func TriggerActivitiesContainsFold(v string) predicate.SubscriptionTemplate { + return predicate.SubscriptionTemplate(sql.FieldContainsFold(FieldTriggerActivities, v)) +} + +// ProjectIDEQ applies the EQ predicate on the "project_id" field. +func ProjectIDEQ(v uuid.UUID) predicate.SubscriptionTemplate { + return predicate.SubscriptionTemplate(sql.FieldEQ(FieldProjectID, v)) +} + +// ProjectIDNEQ applies the NEQ predicate on the "project_id" field. +func ProjectIDNEQ(v uuid.UUID) predicate.SubscriptionTemplate { + return predicate.SubscriptionTemplate(sql.FieldNEQ(FieldProjectID, v)) +} + +// ProjectIDIn applies the In predicate on the "project_id" field. +func ProjectIDIn(vs ...uuid.UUID) predicate.SubscriptionTemplate { + return predicate.SubscriptionTemplate(sql.FieldIn(FieldProjectID, vs...)) +} + +// ProjectIDNotIn applies the NotIn predicate on the "project_id" field. +func ProjectIDNotIn(vs ...uuid.UUID) predicate.SubscriptionTemplate { + return predicate.SubscriptionTemplate(sql.FieldNotIn(FieldProjectID, vs...)) +} + +// ProjectIDGT applies the GT predicate on the "project_id" field. +func ProjectIDGT(v uuid.UUID) predicate.SubscriptionTemplate { + return predicate.SubscriptionTemplate(sql.FieldGT(FieldProjectID, v)) +} + +// ProjectIDGTE applies the GTE predicate on the "project_id" field. +func ProjectIDGTE(v uuid.UUID) predicate.SubscriptionTemplate { + return predicate.SubscriptionTemplate(sql.FieldGTE(FieldProjectID, v)) +} + +// ProjectIDLT applies the LT predicate on the "project_id" field. +func ProjectIDLT(v uuid.UUID) predicate.SubscriptionTemplate { + return predicate.SubscriptionTemplate(sql.FieldLT(FieldProjectID, v)) +} + +// ProjectIDLTE applies the LTE predicate on the "project_id" field. +func ProjectIDLTE(v uuid.UUID) predicate.SubscriptionTemplate { + return predicate.SubscriptionTemplate(sql.FieldLTE(FieldProjectID, v)) +} + +// ProjectIDIsNil applies the IsNil predicate on the "project_id" field. +func ProjectIDIsNil() predicate.SubscriptionTemplate { + return predicate.SubscriptionTemplate(sql.FieldIsNull(FieldProjectID)) +} + +// ProjectIDNotNil applies the NotNil predicate on the "project_id" field. +func ProjectIDNotNil() predicate.SubscriptionTemplate { + return predicate.SubscriptionTemplate(sql.FieldNotNull(FieldProjectID)) +} + +// CreatedByEQ applies the EQ predicate on the "created_by" field. +func CreatedByEQ(v string) predicate.SubscriptionTemplate { + return predicate.SubscriptionTemplate(sql.FieldEQ(FieldCreatedBy, v)) +} + +// CreatedByNEQ applies the NEQ predicate on the "created_by" field. +func CreatedByNEQ(v string) predicate.SubscriptionTemplate { + return predicate.SubscriptionTemplate(sql.FieldNEQ(FieldCreatedBy, v)) +} + +// CreatedByIn applies the In predicate on the "created_by" field. +func CreatedByIn(vs ...string) predicate.SubscriptionTemplate { + return predicate.SubscriptionTemplate(sql.FieldIn(FieldCreatedBy, vs...)) +} + +// CreatedByNotIn applies the NotIn predicate on the "created_by" field. +func CreatedByNotIn(vs ...string) predicate.SubscriptionTemplate { + return predicate.SubscriptionTemplate(sql.FieldNotIn(FieldCreatedBy, vs...)) +} + +// CreatedByGT applies the GT predicate on the "created_by" field. +func CreatedByGT(v string) predicate.SubscriptionTemplate { + return predicate.SubscriptionTemplate(sql.FieldGT(FieldCreatedBy, v)) +} + +// CreatedByGTE applies the GTE predicate on the "created_by" field. +func CreatedByGTE(v string) predicate.SubscriptionTemplate { + return predicate.SubscriptionTemplate(sql.FieldGTE(FieldCreatedBy, v)) +} + +// CreatedByLT applies the LT predicate on the "created_by" field. +func CreatedByLT(v string) predicate.SubscriptionTemplate { + return predicate.SubscriptionTemplate(sql.FieldLT(FieldCreatedBy, v)) +} + +// CreatedByLTE applies the LTE predicate on the "created_by" field. +func CreatedByLTE(v string) predicate.SubscriptionTemplate { + return predicate.SubscriptionTemplate(sql.FieldLTE(FieldCreatedBy, v)) +} + +// CreatedByContains applies the Contains predicate on the "created_by" field. +func CreatedByContains(v string) predicate.SubscriptionTemplate { + return predicate.SubscriptionTemplate(sql.FieldContains(FieldCreatedBy, v)) +} + +// CreatedByHasPrefix applies the HasPrefix predicate on the "created_by" field. +func CreatedByHasPrefix(v string) predicate.SubscriptionTemplate { + return predicate.SubscriptionTemplate(sql.FieldHasPrefix(FieldCreatedBy, v)) +} + +// CreatedByHasSuffix applies the HasSuffix predicate on the "created_by" field. +func CreatedByHasSuffix(v string) predicate.SubscriptionTemplate { + return predicate.SubscriptionTemplate(sql.FieldHasSuffix(FieldCreatedBy, v)) +} + +// CreatedByEqualFold applies the EqualFold predicate on the "created_by" field. +func CreatedByEqualFold(v string) predicate.SubscriptionTemplate { + return predicate.SubscriptionTemplate(sql.FieldEqualFold(FieldCreatedBy, v)) +} + +// CreatedByContainsFold applies the ContainsFold predicate on the "created_by" field. +func CreatedByContainsFold(v string) predicate.SubscriptionTemplate { + return predicate.SubscriptionTemplate(sql.FieldContainsFold(FieldCreatedBy, v)) +} + +// And groups predicates with the AND operator between them. +func And(predicates ...predicate.SubscriptionTemplate) predicate.SubscriptionTemplate { + return predicate.SubscriptionTemplate(sql.AndPredicates(predicates...)) +} + +// Or groups predicates with the OR operator between them. +func Or(predicates ...predicate.SubscriptionTemplate) predicate.SubscriptionTemplate { + return predicate.SubscriptionTemplate(sql.OrPredicates(predicates...)) +} + +// Not applies the not operator on the given predicate. +func Not(p predicate.SubscriptionTemplate) predicate.SubscriptionTemplate { + return predicate.SubscriptionTemplate(sql.NotPredicates(p)) +} diff --git a/pkg/ent/subscriptiontemplate_create.go b/pkg/ent/subscriptiontemplate_create.go new file mode 100644 index 000000000..c45d33af7 --- /dev/null +++ b/pkg/ent/subscriptiontemplate_create.go @@ -0,0 +1,772 @@ +// Code generated by ent, DO NOT EDIT. + +package ent + +import ( + "context" + "errors" + "fmt" + + "entgo.io/ent/dialect" + "entgo.io/ent/dialect/sql" + "entgo.io/ent/dialect/sql/sqlgraph" + "entgo.io/ent/schema/field" + "github.com/GoogleCloudPlatform/scion/pkg/ent/subscriptiontemplate" + "github.com/google/uuid" +) + +// SubscriptionTemplateCreate is the builder for creating a SubscriptionTemplate entity. +type SubscriptionTemplateCreate struct { + config + mutation *SubscriptionTemplateMutation + hooks []Hook + conflict []sql.ConflictOption +} + +// SetName sets the "name" field. +func (_c *SubscriptionTemplateCreate) SetName(v string) *SubscriptionTemplateCreate { + _c.mutation.SetName(v) + return _c +} + +// SetScope sets the "scope" field. +func (_c *SubscriptionTemplateCreate) SetScope(v string) *SubscriptionTemplateCreate { + _c.mutation.SetScope(v) + return _c +} + +// SetNillableScope sets the "scope" field if the given value is not nil. +func (_c *SubscriptionTemplateCreate) SetNillableScope(v *string) *SubscriptionTemplateCreate { + if v != nil { + _c.SetScope(*v) + } + return _c +} + +// SetTriggerActivities sets the "trigger_activities" field. +func (_c *SubscriptionTemplateCreate) SetTriggerActivities(v string) *SubscriptionTemplateCreate { + _c.mutation.SetTriggerActivities(v) + return _c +} + +// SetProjectID sets the "project_id" field. +func (_c *SubscriptionTemplateCreate) SetProjectID(v uuid.UUID) *SubscriptionTemplateCreate { + _c.mutation.SetProjectID(v) + return _c +} + +// SetNillableProjectID sets the "project_id" field if the given value is not nil. +func (_c *SubscriptionTemplateCreate) SetNillableProjectID(v *uuid.UUID) *SubscriptionTemplateCreate { + if v != nil { + _c.SetProjectID(*v) + } + return _c +} + +// SetCreatedBy sets the "created_by" field. +func (_c *SubscriptionTemplateCreate) SetCreatedBy(v string) *SubscriptionTemplateCreate { + _c.mutation.SetCreatedBy(v) + return _c +} + +// SetID sets the "id" field. +func (_c *SubscriptionTemplateCreate) SetID(v uuid.UUID) *SubscriptionTemplateCreate { + _c.mutation.SetID(v) + return _c +} + +// SetNillableID sets the "id" field if the given value is not nil. +func (_c *SubscriptionTemplateCreate) SetNillableID(v *uuid.UUID) *SubscriptionTemplateCreate { + if v != nil { + _c.SetID(*v) + } + return _c +} + +// Mutation returns the SubscriptionTemplateMutation object of the builder. +func (_c *SubscriptionTemplateCreate) Mutation() *SubscriptionTemplateMutation { + return _c.mutation +} + +// Save creates the SubscriptionTemplate in the database. +func (_c *SubscriptionTemplateCreate) Save(ctx context.Context) (*SubscriptionTemplate, error) { + _c.defaults() + return withHooks(ctx, _c.sqlSave, _c.mutation, _c.hooks) +} + +// SaveX calls Save and panics if Save returns an error. +func (_c *SubscriptionTemplateCreate) SaveX(ctx context.Context) *SubscriptionTemplate { + v, err := _c.Save(ctx) + if err != nil { + panic(err) + } + return v +} + +// Exec executes the query. +func (_c *SubscriptionTemplateCreate) Exec(ctx context.Context) error { + _, err := _c.Save(ctx) + return err +} + +// ExecX is like Exec, but panics if an error occurs. +func (_c *SubscriptionTemplateCreate) ExecX(ctx context.Context) { + if err := _c.Exec(ctx); err != nil { + panic(err) + } +} + +// defaults sets the default values of the builder before save. +func (_c *SubscriptionTemplateCreate) defaults() { + if _, ok := _c.mutation.Scope(); !ok { + v := subscriptiontemplate.DefaultScope + _c.mutation.SetScope(v) + } + if _, ok := _c.mutation.ID(); !ok { + v := subscriptiontemplate.DefaultID() + _c.mutation.SetID(v) + } +} + +// check runs all checks and user-defined validators on the builder. +func (_c *SubscriptionTemplateCreate) check() error { + if _, ok := _c.mutation.Name(); !ok { + return &ValidationError{Name: "name", err: errors.New(`ent: missing required field "SubscriptionTemplate.name"`)} + } + if v, ok := _c.mutation.Name(); ok { + if err := subscriptiontemplate.NameValidator(v); err != nil { + return &ValidationError{Name: "name", err: fmt.Errorf(`ent: validator failed for field "SubscriptionTemplate.name": %w`, err)} + } + } + if _, ok := _c.mutation.Scope(); !ok { + return &ValidationError{Name: "scope", err: errors.New(`ent: missing required field "SubscriptionTemplate.scope"`)} + } + if _, ok := _c.mutation.TriggerActivities(); !ok { + return &ValidationError{Name: "trigger_activities", err: errors.New(`ent: missing required field "SubscriptionTemplate.trigger_activities"`)} + } + if v, ok := _c.mutation.TriggerActivities(); ok { + if err := subscriptiontemplate.TriggerActivitiesValidator(v); err != nil { + return &ValidationError{Name: "trigger_activities", err: fmt.Errorf(`ent: validator failed for field "SubscriptionTemplate.trigger_activities": %w`, err)} + } + } + if _, ok := _c.mutation.CreatedBy(); !ok { + return &ValidationError{Name: "created_by", err: errors.New(`ent: missing required field "SubscriptionTemplate.created_by"`)} + } + if v, ok := _c.mutation.CreatedBy(); ok { + if err := subscriptiontemplate.CreatedByValidator(v); err != nil { + return &ValidationError{Name: "created_by", err: fmt.Errorf(`ent: validator failed for field "SubscriptionTemplate.created_by": %w`, err)} + } + } + return nil +} + +func (_c *SubscriptionTemplateCreate) sqlSave(ctx context.Context) (*SubscriptionTemplate, error) { + if err := _c.check(); err != nil { + return nil, err + } + _node, _spec := _c.createSpec() + if err := sqlgraph.CreateNode(ctx, _c.driver, _spec); err != nil { + if sqlgraph.IsConstraintError(err) { + err = &ConstraintError{msg: err.Error(), wrap: err} + } + return nil, err + } + if _spec.ID.Value != nil { + if id, ok := _spec.ID.Value.(*uuid.UUID); ok { + _node.ID = *id + } else if err := _node.ID.Scan(_spec.ID.Value); err != nil { + return nil, err + } + } + _c.mutation.id = &_node.ID + _c.mutation.done = true + return _node, nil +} + +func (_c *SubscriptionTemplateCreate) createSpec() (*SubscriptionTemplate, *sqlgraph.CreateSpec) { + var ( + _node = &SubscriptionTemplate{config: _c.config} + _spec = sqlgraph.NewCreateSpec(subscriptiontemplate.Table, sqlgraph.NewFieldSpec(subscriptiontemplate.FieldID, field.TypeUUID)) + ) + _spec.OnConflict = _c.conflict + if id, ok := _c.mutation.ID(); ok { + _node.ID = id + _spec.ID.Value = &id + } + if value, ok := _c.mutation.Name(); ok { + _spec.SetField(subscriptiontemplate.FieldName, field.TypeString, value) + _node.Name = value + } + if value, ok := _c.mutation.Scope(); ok { + _spec.SetField(subscriptiontemplate.FieldScope, field.TypeString, value) + _node.Scope = value + } + if value, ok := _c.mutation.TriggerActivities(); ok { + _spec.SetField(subscriptiontemplate.FieldTriggerActivities, field.TypeString, value) + _node.TriggerActivities = value + } + if value, ok := _c.mutation.ProjectID(); ok { + _spec.SetField(subscriptiontemplate.FieldProjectID, field.TypeUUID, value) + _node.ProjectID = &value + } + if value, ok := _c.mutation.CreatedBy(); ok { + _spec.SetField(subscriptiontemplate.FieldCreatedBy, field.TypeString, value) + _node.CreatedBy = value + } + return _node, _spec +} + +// OnConflict allows configuring the `ON CONFLICT` / `ON DUPLICATE KEY` clause +// of the `INSERT` statement. For example: +// +// client.SubscriptionTemplate.Create(). +// SetName(v). +// OnConflict( +// // Update the row with the new values +// // the was proposed for insertion. +// sql.ResolveWithNewValues(), +// ). +// // Override some of the fields with custom +// // update values. +// Update(func(u *ent.SubscriptionTemplateUpsert) { +// SetName(v+v). +// }). +// Exec(ctx) +func (_c *SubscriptionTemplateCreate) OnConflict(opts ...sql.ConflictOption) *SubscriptionTemplateUpsertOne { + _c.conflict = opts + return &SubscriptionTemplateUpsertOne{ + create: _c, + } +} + +// OnConflictColumns calls `OnConflict` and configures the columns +// as conflict target. Using this option is equivalent to using: +// +// client.SubscriptionTemplate.Create(). +// OnConflict(sql.ConflictColumns(columns...)). +// Exec(ctx) +func (_c *SubscriptionTemplateCreate) OnConflictColumns(columns ...string) *SubscriptionTemplateUpsertOne { + _c.conflict = append(_c.conflict, sql.ConflictColumns(columns...)) + return &SubscriptionTemplateUpsertOne{ + create: _c, + } +} + +type ( + // SubscriptionTemplateUpsertOne is the builder for "upsert"-ing + // one SubscriptionTemplate node. + SubscriptionTemplateUpsertOne struct { + create *SubscriptionTemplateCreate + } + + // SubscriptionTemplateUpsert is the "OnConflict" setter. + SubscriptionTemplateUpsert struct { + *sql.UpdateSet + } +) + +// SetName sets the "name" field. +func (u *SubscriptionTemplateUpsert) SetName(v string) *SubscriptionTemplateUpsert { + u.Set(subscriptiontemplate.FieldName, v) + return u +} + +// UpdateName sets the "name" field to the value that was provided on create. +func (u *SubscriptionTemplateUpsert) UpdateName() *SubscriptionTemplateUpsert { + u.SetExcluded(subscriptiontemplate.FieldName) + return u +} + +// SetScope sets the "scope" field. +func (u *SubscriptionTemplateUpsert) SetScope(v string) *SubscriptionTemplateUpsert { + u.Set(subscriptiontemplate.FieldScope, v) + return u +} + +// UpdateScope sets the "scope" field to the value that was provided on create. +func (u *SubscriptionTemplateUpsert) UpdateScope() *SubscriptionTemplateUpsert { + u.SetExcluded(subscriptiontemplate.FieldScope) + return u +} + +// SetTriggerActivities sets the "trigger_activities" field. +func (u *SubscriptionTemplateUpsert) SetTriggerActivities(v string) *SubscriptionTemplateUpsert { + u.Set(subscriptiontemplate.FieldTriggerActivities, v) + return u +} + +// UpdateTriggerActivities sets the "trigger_activities" field to the value that was provided on create. +func (u *SubscriptionTemplateUpsert) UpdateTriggerActivities() *SubscriptionTemplateUpsert { + u.SetExcluded(subscriptiontemplate.FieldTriggerActivities) + return u +} + +// SetProjectID sets the "project_id" field. +func (u *SubscriptionTemplateUpsert) SetProjectID(v uuid.UUID) *SubscriptionTemplateUpsert { + u.Set(subscriptiontemplate.FieldProjectID, v) + return u +} + +// UpdateProjectID sets the "project_id" field to the value that was provided on create. +func (u *SubscriptionTemplateUpsert) UpdateProjectID() *SubscriptionTemplateUpsert { + u.SetExcluded(subscriptiontemplate.FieldProjectID) + return u +} + +// ClearProjectID clears the value of the "project_id" field. +func (u *SubscriptionTemplateUpsert) ClearProjectID() *SubscriptionTemplateUpsert { + u.SetNull(subscriptiontemplate.FieldProjectID) + return u +} + +// SetCreatedBy sets the "created_by" field. +func (u *SubscriptionTemplateUpsert) SetCreatedBy(v string) *SubscriptionTemplateUpsert { + u.Set(subscriptiontemplate.FieldCreatedBy, v) + return u +} + +// UpdateCreatedBy sets the "created_by" field to the value that was provided on create. +func (u *SubscriptionTemplateUpsert) UpdateCreatedBy() *SubscriptionTemplateUpsert { + u.SetExcluded(subscriptiontemplate.FieldCreatedBy) + return u +} + +// UpdateNewValues updates the mutable fields using the new values that were set on create except the ID field. +// Using this option is equivalent to using: +// +// client.SubscriptionTemplate.Create(). +// OnConflict( +// sql.ResolveWithNewValues(), +// sql.ResolveWith(func(u *sql.UpdateSet) { +// u.SetIgnore(subscriptiontemplate.FieldID) +// }), +// ). +// Exec(ctx) +func (u *SubscriptionTemplateUpsertOne) UpdateNewValues() *SubscriptionTemplateUpsertOne { + u.create.conflict = append(u.create.conflict, sql.ResolveWithNewValues()) + u.create.conflict = append(u.create.conflict, sql.ResolveWith(func(s *sql.UpdateSet) { + if _, exists := u.create.mutation.ID(); exists { + s.SetIgnore(subscriptiontemplate.FieldID) + } + })) + return u +} + +// Ignore sets each column to itself in case of conflict. +// Using this option is equivalent to using: +// +// client.SubscriptionTemplate.Create(). +// OnConflict(sql.ResolveWithIgnore()). +// Exec(ctx) +func (u *SubscriptionTemplateUpsertOne) Ignore() *SubscriptionTemplateUpsertOne { + u.create.conflict = append(u.create.conflict, sql.ResolveWithIgnore()) + return u +} + +// DoNothing configures the conflict_action to `DO NOTHING`. +// Supported only by SQLite and PostgreSQL. +func (u *SubscriptionTemplateUpsertOne) DoNothing() *SubscriptionTemplateUpsertOne { + u.create.conflict = append(u.create.conflict, sql.DoNothing()) + return u +} + +// Update allows overriding fields `UPDATE` values. See the SubscriptionTemplateCreate.OnConflict +// documentation for more info. +func (u *SubscriptionTemplateUpsertOne) Update(set func(*SubscriptionTemplateUpsert)) *SubscriptionTemplateUpsertOne { + u.create.conflict = append(u.create.conflict, sql.ResolveWith(func(update *sql.UpdateSet) { + set(&SubscriptionTemplateUpsert{UpdateSet: update}) + })) + return u +} + +// SetName sets the "name" field. +func (u *SubscriptionTemplateUpsertOne) SetName(v string) *SubscriptionTemplateUpsertOne { + return u.Update(func(s *SubscriptionTemplateUpsert) { + s.SetName(v) + }) +} + +// UpdateName sets the "name" field to the value that was provided on create. +func (u *SubscriptionTemplateUpsertOne) UpdateName() *SubscriptionTemplateUpsertOne { + return u.Update(func(s *SubscriptionTemplateUpsert) { + s.UpdateName() + }) +} + +// SetScope sets the "scope" field. +func (u *SubscriptionTemplateUpsertOne) SetScope(v string) *SubscriptionTemplateUpsertOne { + return u.Update(func(s *SubscriptionTemplateUpsert) { + s.SetScope(v) + }) +} + +// UpdateScope sets the "scope" field to the value that was provided on create. +func (u *SubscriptionTemplateUpsertOne) UpdateScope() *SubscriptionTemplateUpsertOne { + return u.Update(func(s *SubscriptionTemplateUpsert) { + s.UpdateScope() + }) +} + +// SetTriggerActivities sets the "trigger_activities" field. +func (u *SubscriptionTemplateUpsertOne) SetTriggerActivities(v string) *SubscriptionTemplateUpsertOne { + return u.Update(func(s *SubscriptionTemplateUpsert) { + s.SetTriggerActivities(v) + }) +} + +// UpdateTriggerActivities sets the "trigger_activities" field to the value that was provided on create. +func (u *SubscriptionTemplateUpsertOne) UpdateTriggerActivities() *SubscriptionTemplateUpsertOne { + return u.Update(func(s *SubscriptionTemplateUpsert) { + s.UpdateTriggerActivities() + }) +} + +// SetProjectID sets the "project_id" field. +func (u *SubscriptionTemplateUpsertOne) SetProjectID(v uuid.UUID) *SubscriptionTemplateUpsertOne { + return u.Update(func(s *SubscriptionTemplateUpsert) { + s.SetProjectID(v) + }) +} + +// UpdateProjectID sets the "project_id" field to the value that was provided on create. +func (u *SubscriptionTemplateUpsertOne) UpdateProjectID() *SubscriptionTemplateUpsertOne { + return u.Update(func(s *SubscriptionTemplateUpsert) { + s.UpdateProjectID() + }) +} + +// ClearProjectID clears the value of the "project_id" field. +func (u *SubscriptionTemplateUpsertOne) ClearProjectID() *SubscriptionTemplateUpsertOne { + return u.Update(func(s *SubscriptionTemplateUpsert) { + s.ClearProjectID() + }) +} + +// SetCreatedBy sets the "created_by" field. +func (u *SubscriptionTemplateUpsertOne) SetCreatedBy(v string) *SubscriptionTemplateUpsertOne { + return u.Update(func(s *SubscriptionTemplateUpsert) { + s.SetCreatedBy(v) + }) +} + +// UpdateCreatedBy sets the "created_by" field to the value that was provided on create. +func (u *SubscriptionTemplateUpsertOne) UpdateCreatedBy() *SubscriptionTemplateUpsertOne { + return u.Update(func(s *SubscriptionTemplateUpsert) { + s.UpdateCreatedBy() + }) +} + +// Exec executes the query. +func (u *SubscriptionTemplateUpsertOne) Exec(ctx context.Context) error { + if len(u.create.conflict) == 0 { + return errors.New("ent: missing options for SubscriptionTemplateCreate.OnConflict") + } + return u.create.Exec(ctx) +} + +// ExecX is like Exec, but panics if an error occurs. +func (u *SubscriptionTemplateUpsertOne) ExecX(ctx context.Context) { + if err := u.create.Exec(ctx); err != nil { + panic(err) + } +} + +// Exec executes the UPSERT query and returns the inserted/updated ID. +func (u *SubscriptionTemplateUpsertOne) ID(ctx context.Context) (id uuid.UUID, err error) { + if u.create.driver.Dialect() == dialect.MySQL { + // In case of "ON CONFLICT", there is no way to get back non-numeric ID + // fields from the database since MySQL does not support the RETURNING clause. + return id, errors.New("ent: SubscriptionTemplateUpsertOne.ID is not supported by MySQL driver. Use SubscriptionTemplateUpsertOne.Exec instead") + } + node, err := u.create.Save(ctx) + if err != nil { + return id, err + } + return node.ID, nil +} + +// IDX is like ID, but panics if an error occurs. +func (u *SubscriptionTemplateUpsertOne) IDX(ctx context.Context) uuid.UUID { + id, err := u.ID(ctx) + if err != nil { + panic(err) + } + return id +} + +// SubscriptionTemplateCreateBulk is the builder for creating many SubscriptionTemplate entities in bulk. +type SubscriptionTemplateCreateBulk struct { + config + err error + builders []*SubscriptionTemplateCreate + conflict []sql.ConflictOption +} + +// Save creates the SubscriptionTemplate entities in the database. +func (_c *SubscriptionTemplateCreateBulk) Save(ctx context.Context) ([]*SubscriptionTemplate, error) { + if _c.err != nil { + return nil, _c.err + } + specs := make([]*sqlgraph.CreateSpec, len(_c.builders)) + nodes := make([]*SubscriptionTemplate, len(_c.builders)) + mutators := make([]Mutator, len(_c.builders)) + for i := range _c.builders { + func(i int, root context.Context) { + builder := _c.builders[i] + builder.defaults() + var mut Mutator = MutateFunc(func(ctx context.Context, m Mutation) (Value, error) { + mutation, ok := m.(*SubscriptionTemplateMutation) + if !ok { + return nil, fmt.Errorf("unexpected mutation type %T", m) + } + if err := builder.check(); err != nil { + return nil, err + } + builder.mutation = mutation + var err error + nodes[i], specs[i] = builder.createSpec() + if i < len(mutators)-1 { + _, err = mutators[i+1].Mutate(root, _c.builders[i+1].mutation) + } else { + spec := &sqlgraph.BatchCreateSpec{Nodes: specs} + spec.OnConflict = _c.conflict + // Invoke the actual operation on the latest mutation in the chain. + if err = sqlgraph.BatchCreate(ctx, _c.driver, spec); err != nil { + if sqlgraph.IsConstraintError(err) { + err = &ConstraintError{msg: err.Error(), wrap: err} + } + } + } + if err != nil { + return nil, err + } + mutation.id = &nodes[i].ID + mutation.done = true + return nodes[i], nil + }) + for i := len(builder.hooks) - 1; i >= 0; i-- { + mut = builder.hooks[i](mut) + } + mutators[i] = mut + }(i, ctx) + } + if len(mutators) > 0 { + if _, err := mutators[0].Mutate(ctx, _c.builders[0].mutation); err != nil { + return nil, err + } + } + return nodes, nil +} + +// SaveX is like Save, but panics if an error occurs. +func (_c *SubscriptionTemplateCreateBulk) SaveX(ctx context.Context) []*SubscriptionTemplate { + v, err := _c.Save(ctx) + if err != nil { + panic(err) + } + return v +} + +// Exec executes the query. +func (_c *SubscriptionTemplateCreateBulk) Exec(ctx context.Context) error { + _, err := _c.Save(ctx) + return err +} + +// ExecX is like Exec, but panics if an error occurs. +func (_c *SubscriptionTemplateCreateBulk) ExecX(ctx context.Context) { + if err := _c.Exec(ctx); err != nil { + panic(err) + } +} + +// OnConflict allows configuring the `ON CONFLICT` / `ON DUPLICATE KEY` clause +// of the `INSERT` statement. For example: +// +// client.SubscriptionTemplate.CreateBulk(builders...). +// OnConflict( +// // Update the row with the new values +// // the was proposed for insertion. +// sql.ResolveWithNewValues(), +// ). +// // Override some of the fields with custom +// // update values. +// Update(func(u *ent.SubscriptionTemplateUpsert) { +// SetName(v+v). +// }). +// Exec(ctx) +func (_c *SubscriptionTemplateCreateBulk) OnConflict(opts ...sql.ConflictOption) *SubscriptionTemplateUpsertBulk { + _c.conflict = opts + return &SubscriptionTemplateUpsertBulk{ + create: _c, + } +} + +// OnConflictColumns calls `OnConflict` and configures the columns +// as conflict target. Using this option is equivalent to using: +// +// client.SubscriptionTemplate.Create(). +// OnConflict(sql.ConflictColumns(columns...)). +// Exec(ctx) +func (_c *SubscriptionTemplateCreateBulk) OnConflictColumns(columns ...string) *SubscriptionTemplateUpsertBulk { + _c.conflict = append(_c.conflict, sql.ConflictColumns(columns...)) + return &SubscriptionTemplateUpsertBulk{ + create: _c, + } +} + +// SubscriptionTemplateUpsertBulk is the builder for "upsert"-ing +// a bulk of SubscriptionTemplate nodes. +type SubscriptionTemplateUpsertBulk struct { + create *SubscriptionTemplateCreateBulk +} + +// UpdateNewValues updates the mutable fields using the new values that +// were set on create. Using this option is equivalent to using: +// +// client.SubscriptionTemplate.Create(). +// OnConflict( +// sql.ResolveWithNewValues(), +// sql.ResolveWith(func(u *sql.UpdateSet) { +// u.SetIgnore(subscriptiontemplate.FieldID) +// }), +// ). +// Exec(ctx) +func (u *SubscriptionTemplateUpsertBulk) UpdateNewValues() *SubscriptionTemplateUpsertBulk { + u.create.conflict = append(u.create.conflict, sql.ResolveWithNewValues()) + u.create.conflict = append(u.create.conflict, sql.ResolveWith(func(s *sql.UpdateSet) { + for _, b := range u.create.builders { + if _, exists := b.mutation.ID(); exists { + s.SetIgnore(subscriptiontemplate.FieldID) + } + } + })) + return u +} + +// Ignore sets each column to itself in case of conflict. +// Using this option is equivalent to using: +// +// client.SubscriptionTemplate.Create(). +// OnConflict(sql.ResolveWithIgnore()). +// Exec(ctx) +func (u *SubscriptionTemplateUpsertBulk) Ignore() *SubscriptionTemplateUpsertBulk { + u.create.conflict = append(u.create.conflict, sql.ResolveWithIgnore()) + return u +} + +// DoNothing configures the conflict_action to `DO NOTHING`. +// Supported only by SQLite and PostgreSQL. +func (u *SubscriptionTemplateUpsertBulk) DoNothing() *SubscriptionTemplateUpsertBulk { + u.create.conflict = append(u.create.conflict, sql.DoNothing()) + return u +} + +// Update allows overriding fields `UPDATE` values. See the SubscriptionTemplateCreateBulk.OnConflict +// documentation for more info. +func (u *SubscriptionTemplateUpsertBulk) Update(set func(*SubscriptionTemplateUpsert)) *SubscriptionTemplateUpsertBulk { + u.create.conflict = append(u.create.conflict, sql.ResolveWith(func(update *sql.UpdateSet) { + set(&SubscriptionTemplateUpsert{UpdateSet: update}) + })) + return u +} + +// SetName sets the "name" field. +func (u *SubscriptionTemplateUpsertBulk) SetName(v string) *SubscriptionTemplateUpsertBulk { + return u.Update(func(s *SubscriptionTemplateUpsert) { + s.SetName(v) + }) +} + +// UpdateName sets the "name" field to the value that was provided on create. +func (u *SubscriptionTemplateUpsertBulk) UpdateName() *SubscriptionTemplateUpsertBulk { + return u.Update(func(s *SubscriptionTemplateUpsert) { + s.UpdateName() + }) +} + +// SetScope sets the "scope" field. +func (u *SubscriptionTemplateUpsertBulk) SetScope(v string) *SubscriptionTemplateUpsertBulk { + return u.Update(func(s *SubscriptionTemplateUpsert) { + s.SetScope(v) + }) +} + +// UpdateScope sets the "scope" field to the value that was provided on create. +func (u *SubscriptionTemplateUpsertBulk) UpdateScope() *SubscriptionTemplateUpsertBulk { + return u.Update(func(s *SubscriptionTemplateUpsert) { + s.UpdateScope() + }) +} + +// SetTriggerActivities sets the "trigger_activities" field. +func (u *SubscriptionTemplateUpsertBulk) SetTriggerActivities(v string) *SubscriptionTemplateUpsertBulk { + return u.Update(func(s *SubscriptionTemplateUpsert) { + s.SetTriggerActivities(v) + }) +} + +// UpdateTriggerActivities sets the "trigger_activities" field to the value that was provided on create. +func (u *SubscriptionTemplateUpsertBulk) UpdateTriggerActivities() *SubscriptionTemplateUpsertBulk { + return u.Update(func(s *SubscriptionTemplateUpsert) { + s.UpdateTriggerActivities() + }) +} + +// SetProjectID sets the "project_id" field. +func (u *SubscriptionTemplateUpsertBulk) SetProjectID(v uuid.UUID) *SubscriptionTemplateUpsertBulk { + return u.Update(func(s *SubscriptionTemplateUpsert) { + s.SetProjectID(v) + }) +} + +// UpdateProjectID sets the "project_id" field to the value that was provided on create. +func (u *SubscriptionTemplateUpsertBulk) UpdateProjectID() *SubscriptionTemplateUpsertBulk { + return u.Update(func(s *SubscriptionTemplateUpsert) { + s.UpdateProjectID() + }) +} + +// ClearProjectID clears the value of the "project_id" field. +func (u *SubscriptionTemplateUpsertBulk) ClearProjectID() *SubscriptionTemplateUpsertBulk { + return u.Update(func(s *SubscriptionTemplateUpsert) { + s.ClearProjectID() + }) +} + +// SetCreatedBy sets the "created_by" field. +func (u *SubscriptionTemplateUpsertBulk) SetCreatedBy(v string) *SubscriptionTemplateUpsertBulk { + return u.Update(func(s *SubscriptionTemplateUpsert) { + s.SetCreatedBy(v) + }) +} + +// UpdateCreatedBy sets the "created_by" field to the value that was provided on create. +func (u *SubscriptionTemplateUpsertBulk) UpdateCreatedBy() *SubscriptionTemplateUpsertBulk { + return u.Update(func(s *SubscriptionTemplateUpsert) { + s.UpdateCreatedBy() + }) +} + +// Exec executes the query. +func (u *SubscriptionTemplateUpsertBulk) Exec(ctx context.Context) error { + if u.create.err != nil { + return u.create.err + } + for i, b := range u.create.builders { + if len(b.conflict) != 0 { + return fmt.Errorf("ent: OnConflict was set for builder %d. Set it on the SubscriptionTemplateCreateBulk instead", i) + } + } + if len(u.create.conflict) == 0 { + return errors.New("ent: missing options for SubscriptionTemplateCreateBulk.OnConflict") + } + return u.create.Exec(ctx) +} + +// ExecX is like Exec, but panics if an error occurs. +func (u *SubscriptionTemplateUpsertBulk) ExecX(ctx context.Context) { + if err := u.create.Exec(ctx); err != nil { + panic(err) + } +} diff --git a/pkg/ent/subscriptiontemplate_delete.go b/pkg/ent/subscriptiontemplate_delete.go new file mode 100644 index 000000000..b6aa232ee --- /dev/null +++ b/pkg/ent/subscriptiontemplate_delete.go @@ -0,0 +1,88 @@ +// Code generated by ent, DO NOT EDIT. + +package ent + +import ( + "context" + + "entgo.io/ent/dialect/sql" + "entgo.io/ent/dialect/sql/sqlgraph" + "entgo.io/ent/schema/field" + "github.com/GoogleCloudPlatform/scion/pkg/ent/predicate" + "github.com/GoogleCloudPlatform/scion/pkg/ent/subscriptiontemplate" +) + +// SubscriptionTemplateDelete is the builder for deleting a SubscriptionTemplate entity. +type SubscriptionTemplateDelete struct { + config + hooks []Hook + mutation *SubscriptionTemplateMutation +} + +// Where appends a list predicates to the SubscriptionTemplateDelete builder. +func (_d *SubscriptionTemplateDelete) Where(ps ...predicate.SubscriptionTemplate) *SubscriptionTemplateDelete { + _d.mutation.Where(ps...) + return _d +} + +// Exec executes the deletion query and returns how many vertices were deleted. +func (_d *SubscriptionTemplateDelete) Exec(ctx context.Context) (int, error) { + return withHooks(ctx, _d.sqlExec, _d.mutation, _d.hooks) +} + +// ExecX is like Exec, but panics if an error occurs. +func (_d *SubscriptionTemplateDelete) ExecX(ctx context.Context) int { + n, err := _d.Exec(ctx) + if err != nil { + panic(err) + } + return n +} + +func (_d *SubscriptionTemplateDelete) sqlExec(ctx context.Context) (int, error) { + _spec := sqlgraph.NewDeleteSpec(subscriptiontemplate.Table, sqlgraph.NewFieldSpec(subscriptiontemplate.FieldID, field.TypeUUID)) + if ps := _d.mutation.predicates; len(ps) > 0 { + _spec.Predicate = func(selector *sql.Selector) { + for i := range ps { + ps[i](selector) + } + } + } + affected, err := sqlgraph.DeleteNodes(ctx, _d.driver, _spec) + if err != nil && sqlgraph.IsConstraintError(err) { + err = &ConstraintError{msg: err.Error(), wrap: err} + } + _d.mutation.done = true + return affected, err +} + +// SubscriptionTemplateDeleteOne is the builder for deleting a single SubscriptionTemplate entity. +type SubscriptionTemplateDeleteOne struct { + _d *SubscriptionTemplateDelete +} + +// Where appends a list predicates to the SubscriptionTemplateDelete builder. +func (_d *SubscriptionTemplateDeleteOne) Where(ps ...predicate.SubscriptionTemplate) *SubscriptionTemplateDeleteOne { + _d._d.mutation.Where(ps...) + return _d +} + +// Exec executes the deletion query. +func (_d *SubscriptionTemplateDeleteOne) Exec(ctx context.Context) error { + n, err := _d._d.Exec(ctx) + switch { + case err != nil: + return err + case n == 0: + return &NotFoundError{subscriptiontemplate.Label} + default: + return nil + } +} + +// ExecX is like Exec, but panics if an error occurs. +func (_d *SubscriptionTemplateDeleteOne) ExecX(ctx context.Context) { + if err := _d.Exec(ctx); err != nil { + panic(err) + } +} diff --git a/pkg/ent/subscriptiontemplate_query.go b/pkg/ent/subscriptiontemplate_query.go new file mode 100644 index 000000000..6c38c23f5 --- /dev/null +++ b/pkg/ent/subscriptiontemplate_query.go @@ -0,0 +1,565 @@ +// Code generated by ent, DO NOT EDIT. + +package ent + +import ( + "context" + "fmt" + "math" + + "entgo.io/ent" + "entgo.io/ent/dialect" + "entgo.io/ent/dialect/sql" + "entgo.io/ent/dialect/sql/sqlgraph" + "entgo.io/ent/schema/field" + "github.com/GoogleCloudPlatform/scion/pkg/ent/predicate" + "github.com/GoogleCloudPlatform/scion/pkg/ent/subscriptiontemplate" + "github.com/google/uuid" +) + +// SubscriptionTemplateQuery is the builder for querying SubscriptionTemplate entities. +type SubscriptionTemplateQuery struct { + config + ctx *QueryContext + order []subscriptiontemplate.OrderOption + inters []Interceptor + predicates []predicate.SubscriptionTemplate + modifiers []func(*sql.Selector) + // intermediate query (i.e. traversal path). + sql *sql.Selector + path func(context.Context) (*sql.Selector, error) +} + +// Where adds a new predicate for the SubscriptionTemplateQuery builder. +func (_q *SubscriptionTemplateQuery) Where(ps ...predicate.SubscriptionTemplate) *SubscriptionTemplateQuery { + _q.predicates = append(_q.predicates, ps...) + return _q +} + +// Limit the number of records to be returned by this query. +func (_q *SubscriptionTemplateQuery) Limit(limit int) *SubscriptionTemplateQuery { + _q.ctx.Limit = &limit + return _q +} + +// Offset to start from. +func (_q *SubscriptionTemplateQuery) Offset(offset int) *SubscriptionTemplateQuery { + _q.ctx.Offset = &offset + return _q +} + +// Unique configures the query builder to filter duplicate records on query. +// By default, unique is set to true, and can be disabled using this method. +func (_q *SubscriptionTemplateQuery) Unique(unique bool) *SubscriptionTemplateQuery { + _q.ctx.Unique = &unique + return _q +} + +// Order specifies how the records should be ordered. +func (_q *SubscriptionTemplateQuery) Order(o ...subscriptiontemplate.OrderOption) *SubscriptionTemplateQuery { + _q.order = append(_q.order, o...) + return _q +} + +// First returns the first SubscriptionTemplate entity from the query. +// Returns a *NotFoundError when no SubscriptionTemplate was found. +func (_q *SubscriptionTemplateQuery) First(ctx context.Context) (*SubscriptionTemplate, error) { + nodes, err := _q.Limit(1).All(setContextOp(ctx, _q.ctx, ent.OpQueryFirst)) + if err != nil { + return nil, err + } + if len(nodes) == 0 { + return nil, &NotFoundError{subscriptiontemplate.Label} + } + return nodes[0], nil +} + +// FirstX is like First, but panics if an error occurs. +func (_q *SubscriptionTemplateQuery) FirstX(ctx context.Context) *SubscriptionTemplate { + node, err := _q.First(ctx) + if err != nil && !IsNotFound(err) { + panic(err) + } + return node +} + +// FirstID returns the first SubscriptionTemplate ID from the query. +// Returns a *NotFoundError when no SubscriptionTemplate ID was found. +func (_q *SubscriptionTemplateQuery) FirstID(ctx context.Context) (id uuid.UUID, err error) { + var ids []uuid.UUID + if ids, err = _q.Limit(1).IDs(setContextOp(ctx, _q.ctx, ent.OpQueryFirstID)); err != nil { + return + } + if len(ids) == 0 { + err = &NotFoundError{subscriptiontemplate.Label} + return + } + return ids[0], nil +} + +// FirstIDX is like FirstID, but panics if an error occurs. +func (_q *SubscriptionTemplateQuery) FirstIDX(ctx context.Context) uuid.UUID { + id, err := _q.FirstID(ctx) + if err != nil && !IsNotFound(err) { + panic(err) + } + return id +} + +// Only returns a single SubscriptionTemplate entity found by the query, ensuring it only returns one. +// Returns a *NotSingularError when more than one SubscriptionTemplate entity is found. +// Returns a *NotFoundError when no SubscriptionTemplate entities are found. +func (_q *SubscriptionTemplateQuery) Only(ctx context.Context) (*SubscriptionTemplate, error) { + nodes, err := _q.Limit(2).All(setContextOp(ctx, _q.ctx, ent.OpQueryOnly)) + if err != nil { + return nil, err + } + switch len(nodes) { + case 1: + return nodes[0], nil + case 0: + return nil, &NotFoundError{subscriptiontemplate.Label} + default: + return nil, &NotSingularError{subscriptiontemplate.Label} + } +} + +// OnlyX is like Only, but panics if an error occurs. +func (_q *SubscriptionTemplateQuery) OnlyX(ctx context.Context) *SubscriptionTemplate { + node, err := _q.Only(ctx) + if err != nil { + panic(err) + } + return node +} + +// OnlyID is like Only, but returns the only SubscriptionTemplate ID in the query. +// Returns a *NotSingularError when more than one SubscriptionTemplate ID is found. +// Returns a *NotFoundError when no entities are found. +func (_q *SubscriptionTemplateQuery) OnlyID(ctx context.Context) (id uuid.UUID, err error) { + var ids []uuid.UUID + if ids, err = _q.Limit(2).IDs(setContextOp(ctx, _q.ctx, ent.OpQueryOnlyID)); err != nil { + return + } + switch len(ids) { + case 1: + id = ids[0] + case 0: + err = &NotFoundError{subscriptiontemplate.Label} + default: + err = &NotSingularError{subscriptiontemplate.Label} + } + return +} + +// OnlyIDX is like OnlyID, but panics if an error occurs. +func (_q *SubscriptionTemplateQuery) OnlyIDX(ctx context.Context) uuid.UUID { + id, err := _q.OnlyID(ctx) + if err != nil { + panic(err) + } + return id +} + +// All executes the query and returns a list of SubscriptionTemplates. +func (_q *SubscriptionTemplateQuery) All(ctx context.Context) ([]*SubscriptionTemplate, error) { + ctx = setContextOp(ctx, _q.ctx, ent.OpQueryAll) + if err := _q.prepareQuery(ctx); err != nil { + return nil, err + } + qr := querierAll[[]*SubscriptionTemplate, *SubscriptionTemplateQuery]() + return withInterceptors[[]*SubscriptionTemplate](ctx, _q, qr, _q.inters) +} + +// AllX is like All, but panics if an error occurs. +func (_q *SubscriptionTemplateQuery) AllX(ctx context.Context) []*SubscriptionTemplate { + nodes, err := _q.All(ctx) + if err != nil { + panic(err) + } + return nodes +} + +// IDs executes the query and returns a list of SubscriptionTemplate IDs. +func (_q *SubscriptionTemplateQuery) IDs(ctx context.Context) (ids []uuid.UUID, err error) { + if _q.ctx.Unique == nil && _q.path != nil { + _q.Unique(true) + } + ctx = setContextOp(ctx, _q.ctx, ent.OpQueryIDs) + if err = _q.Select(subscriptiontemplate.FieldID).Scan(ctx, &ids); err != nil { + return nil, err + } + return ids, nil +} + +// IDsX is like IDs, but panics if an error occurs. +func (_q *SubscriptionTemplateQuery) IDsX(ctx context.Context) []uuid.UUID { + ids, err := _q.IDs(ctx) + if err != nil { + panic(err) + } + return ids +} + +// Count returns the count of the given query. +func (_q *SubscriptionTemplateQuery) Count(ctx context.Context) (int, error) { + ctx = setContextOp(ctx, _q.ctx, ent.OpQueryCount) + if err := _q.prepareQuery(ctx); err != nil { + return 0, err + } + return withInterceptors[int](ctx, _q, querierCount[*SubscriptionTemplateQuery](), _q.inters) +} + +// CountX is like Count, but panics if an error occurs. +func (_q *SubscriptionTemplateQuery) CountX(ctx context.Context) int { + count, err := _q.Count(ctx) + if err != nil { + panic(err) + } + return count +} + +// Exist returns true if the query has elements in the graph. +func (_q *SubscriptionTemplateQuery) Exist(ctx context.Context) (bool, error) { + ctx = setContextOp(ctx, _q.ctx, ent.OpQueryExist) + switch _, err := _q.FirstID(ctx); { + case IsNotFound(err): + return false, nil + case err != nil: + return false, fmt.Errorf("ent: check existence: %w", err) + default: + return true, nil + } +} + +// ExistX is like Exist, but panics if an error occurs. +func (_q *SubscriptionTemplateQuery) ExistX(ctx context.Context) bool { + exist, err := _q.Exist(ctx) + if err != nil { + panic(err) + } + return exist +} + +// Clone returns a duplicate of the SubscriptionTemplateQuery builder, including all associated steps. It can be +// used to prepare common query builders and use them differently after the clone is made. +func (_q *SubscriptionTemplateQuery) Clone() *SubscriptionTemplateQuery { + if _q == nil { + return nil + } + return &SubscriptionTemplateQuery{ + config: _q.config, + ctx: _q.ctx.Clone(), + order: append([]subscriptiontemplate.OrderOption{}, _q.order...), + inters: append([]Interceptor{}, _q.inters...), + predicates: append([]predicate.SubscriptionTemplate{}, _q.predicates...), + // clone intermediate query. + sql: _q.sql.Clone(), + path: _q.path, + } +} + +// GroupBy is used to group vertices by one or more fields/columns. +// It is often used with aggregate functions, like: count, max, mean, min, sum. +// +// Example: +// +// var v []struct { +// Name string `json:"name,omitempty"` +// Count int `json:"count,omitempty"` +// } +// +// client.SubscriptionTemplate.Query(). +// GroupBy(subscriptiontemplate.FieldName). +// Aggregate(ent.Count()). +// Scan(ctx, &v) +func (_q *SubscriptionTemplateQuery) GroupBy(field string, fields ...string) *SubscriptionTemplateGroupBy { + _q.ctx.Fields = append([]string{field}, fields...) + grbuild := &SubscriptionTemplateGroupBy{build: _q} + grbuild.flds = &_q.ctx.Fields + grbuild.label = subscriptiontemplate.Label + grbuild.scan = grbuild.Scan + return grbuild +} + +// Select allows the selection one or more fields/columns for the given query, +// instead of selecting all fields in the entity. +// +// Example: +// +// var v []struct { +// Name string `json:"name,omitempty"` +// } +// +// client.SubscriptionTemplate.Query(). +// Select(subscriptiontemplate.FieldName). +// Scan(ctx, &v) +func (_q *SubscriptionTemplateQuery) Select(fields ...string) *SubscriptionTemplateSelect { + _q.ctx.Fields = append(_q.ctx.Fields, fields...) + sbuild := &SubscriptionTemplateSelect{SubscriptionTemplateQuery: _q} + sbuild.label = subscriptiontemplate.Label + sbuild.flds, sbuild.scan = &_q.ctx.Fields, sbuild.Scan + return sbuild +} + +// Aggregate returns a SubscriptionTemplateSelect configured with the given aggregations. +func (_q *SubscriptionTemplateQuery) Aggregate(fns ...AggregateFunc) *SubscriptionTemplateSelect { + return _q.Select().Aggregate(fns...) +} + +func (_q *SubscriptionTemplateQuery) prepareQuery(ctx context.Context) error { + for _, inter := range _q.inters { + if inter == nil { + return fmt.Errorf("ent: uninitialized interceptor (forgotten import ent/runtime?)") + } + if trv, ok := inter.(Traverser); ok { + if err := trv.Traverse(ctx, _q); err != nil { + return err + } + } + } + for _, f := range _q.ctx.Fields { + if !subscriptiontemplate.ValidColumn(f) { + return &ValidationError{Name: f, err: fmt.Errorf("ent: invalid field %q for query", f)} + } + } + if _q.path != nil { + prev, err := _q.path(ctx) + if err != nil { + return err + } + _q.sql = prev + } + return nil +} + +func (_q *SubscriptionTemplateQuery) sqlAll(ctx context.Context, hooks ...queryHook) ([]*SubscriptionTemplate, error) { + var ( + nodes = []*SubscriptionTemplate{} + _spec = _q.querySpec() + ) + _spec.ScanValues = func(columns []string) ([]any, error) { + return (*SubscriptionTemplate).scanValues(nil, columns) + } + _spec.Assign = func(columns []string, values []any) error { + node := &SubscriptionTemplate{config: _q.config} + nodes = append(nodes, node) + return node.assignValues(columns, values) + } + if len(_q.modifiers) > 0 { + _spec.Modifiers = _q.modifiers + } + for i := range hooks { + hooks[i](ctx, _spec) + } + if err := sqlgraph.QueryNodes(ctx, _q.driver, _spec); err != nil { + return nil, err + } + if len(nodes) == 0 { + return nodes, nil + } + return nodes, nil +} + +func (_q *SubscriptionTemplateQuery) sqlCount(ctx context.Context) (int, error) { + _spec := _q.querySpec() + if len(_q.modifiers) > 0 { + _spec.Modifiers = _q.modifiers + } + _spec.Node.Columns = _q.ctx.Fields + if len(_q.ctx.Fields) > 0 { + _spec.Unique = _q.ctx.Unique != nil && *_q.ctx.Unique + } + return sqlgraph.CountNodes(ctx, _q.driver, _spec) +} + +func (_q *SubscriptionTemplateQuery) querySpec() *sqlgraph.QuerySpec { + _spec := sqlgraph.NewQuerySpec(subscriptiontemplate.Table, subscriptiontemplate.Columns, sqlgraph.NewFieldSpec(subscriptiontemplate.FieldID, field.TypeUUID)) + _spec.From = _q.sql + if unique := _q.ctx.Unique; unique != nil { + _spec.Unique = *unique + } else if _q.path != nil { + _spec.Unique = true + } + if fields := _q.ctx.Fields; len(fields) > 0 { + _spec.Node.Columns = make([]string, 0, len(fields)) + _spec.Node.Columns = append(_spec.Node.Columns, subscriptiontemplate.FieldID) + for i := range fields { + if fields[i] != subscriptiontemplate.FieldID { + _spec.Node.Columns = append(_spec.Node.Columns, fields[i]) + } + } + } + if ps := _q.predicates; len(ps) > 0 { + _spec.Predicate = func(selector *sql.Selector) { + for i := range ps { + ps[i](selector) + } + } + } + if limit := _q.ctx.Limit; limit != nil { + _spec.Limit = *limit + } + if offset := _q.ctx.Offset; offset != nil { + _spec.Offset = *offset + } + if ps := _q.order; len(ps) > 0 { + _spec.Order = func(selector *sql.Selector) { + for i := range ps { + ps[i](selector) + } + } + } + return _spec +} + +func (_q *SubscriptionTemplateQuery) sqlQuery(ctx context.Context) *sql.Selector { + builder := sql.Dialect(_q.driver.Dialect()) + t1 := builder.Table(subscriptiontemplate.Table) + columns := _q.ctx.Fields + if len(columns) == 0 { + columns = subscriptiontemplate.Columns + } + selector := builder.Select(t1.Columns(columns...)...).From(t1) + if _q.sql != nil { + selector = _q.sql + selector.Select(selector.Columns(columns...)...) + } + if _q.ctx.Unique != nil && *_q.ctx.Unique { + selector.Distinct() + } + for _, m := range _q.modifiers { + m(selector) + } + for _, p := range _q.predicates { + p(selector) + } + for _, p := range _q.order { + p(selector) + } + if offset := _q.ctx.Offset; offset != nil { + // limit is mandatory for offset clause. We start + // with default value, and override it below if needed. + selector.Offset(*offset).Limit(math.MaxInt32) + } + if limit := _q.ctx.Limit; limit != nil { + selector.Limit(*limit) + } + return selector +} + +// ForUpdate locks the selected rows against concurrent updates, and prevent them from being +// updated, deleted or "selected ... for update" by other sessions, until the transaction is +// either committed or rolled-back. +func (_q *SubscriptionTemplateQuery) ForUpdate(opts ...sql.LockOption) *SubscriptionTemplateQuery { + if _q.driver.Dialect() == dialect.Postgres { + _q.Unique(false) + } + _q.modifiers = append(_q.modifiers, func(s *sql.Selector) { + s.ForUpdate(opts...) + }) + return _q +} + +// ForShare behaves similarly to ForUpdate, except that it acquires a shared mode lock +// on any rows that are read. Other sessions can read the rows, but cannot modify them +// until your transaction commits. +func (_q *SubscriptionTemplateQuery) ForShare(opts ...sql.LockOption) *SubscriptionTemplateQuery { + if _q.driver.Dialect() == dialect.Postgres { + _q.Unique(false) + } + _q.modifiers = append(_q.modifiers, func(s *sql.Selector) { + s.ForShare(opts...) + }) + return _q +} + +// SubscriptionTemplateGroupBy is the group-by builder for SubscriptionTemplate entities. +type SubscriptionTemplateGroupBy struct { + selector + build *SubscriptionTemplateQuery +} + +// Aggregate adds the given aggregation functions to the group-by query. +func (_g *SubscriptionTemplateGroupBy) Aggregate(fns ...AggregateFunc) *SubscriptionTemplateGroupBy { + _g.fns = append(_g.fns, fns...) + return _g +} + +// Scan applies the selector query and scans the result into the given value. +func (_g *SubscriptionTemplateGroupBy) Scan(ctx context.Context, v any) error { + ctx = setContextOp(ctx, _g.build.ctx, ent.OpQueryGroupBy) + if err := _g.build.prepareQuery(ctx); err != nil { + return err + } + return scanWithInterceptors[*SubscriptionTemplateQuery, *SubscriptionTemplateGroupBy](ctx, _g.build, _g, _g.build.inters, v) +} + +func (_g *SubscriptionTemplateGroupBy) sqlScan(ctx context.Context, root *SubscriptionTemplateQuery, v any) error { + selector := root.sqlQuery(ctx).Select() + aggregation := make([]string, 0, len(_g.fns)) + for _, fn := range _g.fns { + aggregation = append(aggregation, fn(selector)) + } + if len(selector.SelectedColumns()) == 0 { + columns := make([]string, 0, len(*_g.flds)+len(_g.fns)) + for _, f := range *_g.flds { + columns = append(columns, selector.C(f)) + } + columns = append(columns, aggregation...) + selector.Select(columns...) + } + selector.GroupBy(selector.Columns(*_g.flds...)...) + if err := selector.Err(); err != nil { + return err + } + rows := &sql.Rows{} + query, args := selector.Query() + if err := _g.build.driver.Query(ctx, query, args, rows); err != nil { + return err + } + defer rows.Close() + return sql.ScanSlice(rows, v) +} + +// SubscriptionTemplateSelect is the builder for selecting fields of SubscriptionTemplate entities. +type SubscriptionTemplateSelect struct { + *SubscriptionTemplateQuery + selector +} + +// Aggregate adds the given aggregation functions to the selector query. +func (_s *SubscriptionTemplateSelect) Aggregate(fns ...AggregateFunc) *SubscriptionTemplateSelect { + _s.fns = append(_s.fns, fns...) + return _s +} + +// Scan applies the selector query and scans the result into the given value. +func (_s *SubscriptionTemplateSelect) Scan(ctx context.Context, v any) error { + ctx = setContextOp(ctx, _s.ctx, ent.OpQuerySelect) + if err := _s.prepareQuery(ctx); err != nil { + return err + } + return scanWithInterceptors[*SubscriptionTemplateQuery, *SubscriptionTemplateSelect](ctx, _s.SubscriptionTemplateQuery, _s, _s.inters, v) +} + +func (_s *SubscriptionTemplateSelect) sqlScan(ctx context.Context, root *SubscriptionTemplateQuery, v any) error { + selector := root.sqlQuery(ctx) + aggregation := make([]string, 0, len(_s.fns)) + for _, fn := range _s.fns { + aggregation = append(aggregation, fn(selector)) + } + switch n := len(*_s.selector.flds); { + case n == 0 && len(aggregation) > 0: + selector.Select(aggregation...) + case n != 0 && len(aggregation) > 0: + selector.AppendSelect(aggregation...) + } + rows := &sql.Rows{} + query, args := selector.Query() + if err := _s.driver.Query(ctx, query, args, rows); err != nil { + return err + } + defer rows.Close() + return sql.ScanSlice(rows, v) +} diff --git a/pkg/ent/subscriptiontemplate_update.go b/pkg/ent/subscriptiontemplate_update.go new file mode 100644 index 000000000..2a87dfae5 --- /dev/null +++ b/pkg/ent/subscriptiontemplate_update.go @@ -0,0 +1,410 @@ +// Code generated by ent, DO NOT EDIT. + +package ent + +import ( + "context" + "errors" + "fmt" + + "entgo.io/ent/dialect/sql" + "entgo.io/ent/dialect/sql/sqlgraph" + "entgo.io/ent/schema/field" + "github.com/GoogleCloudPlatform/scion/pkg/ent/predicate" + "github.com/GoogleCloudPlatform/scion/pkg/ent/subscriptiontemplate" + "github.com/google/uuid" +) + +// SubscriptionTemplateUpdate is the builder for updating SubscriptionTemplate entities. +type SubscriptionTemplateUpdate struct { + config + hooks []Hook + mutation *SubscriptionTemplateMutation +} + +// Where appends a list predicates to the SubscriptionTemplateUpdate builder. +func (_u *SubscriptionTemplateUpdate) Where(ps ...predicate.SubscriptionTemplate) *SubscriptionTemplateUpdate { + _u.mutation.Where(ps...) + return _u +} + +// SetName sets the "name" field. +func (_u *SubscriptionTemplateUpdate) SetName(v string) *SubscriptionTemplateUpdate { + _u.mutation.SetName(v) + return _u +} + +// SetNillableName sets the "name" field if the given value is not nil. +func (_u *SubscriptionTemplateUpdate) SetNillableName(v *string) *SubscriptionTemplateUpdate { + if v != nil { + _u.SetName(*v) + } + return _u +} + +// SetScope sets the "scope" field. +func (_u *SubscriptionTemplateUpdate) SetScope(v string) *SubscriptionTemplateUpdate { + _u.mutation.SetScope(v) + return _u +} + +// SetNillableScope sets the "scope" field if the given value is not nil. +func (_u *SubscriptionTemplateUpdate) SetNillableScope(v *string) *SubscriptionTemplateUpdate { + if v != nil { + _u.SetScope(*v) + } + return _u +} + +// SetTriggerActivities sets the "trigger_activities" field. +func (_u *SubscriptionTemplateUpdate) SetTriggerActivities(v string) *SubscriptionTemplateUpdate { + _u.mutation.SetTriggerActivities(v) + return _u +} + +// SetNillableTriggerActivities sets the "trigger_activities" field if the given value is not nil. +func (_u *SubscriptionTemplateUpdate) SetNillableTriggerActivities(v *string) *SubscriptionTemplateUpdate { + if v != nil { + _u.SetTriggerActivities(*v) + } + return _u +} + +// SetProjectID sets the "project_id" field. +func (_u *SubscriptionTemplateUpdate) SetProjectID(v uuid.UUID) *SubscriptionTemplateUpdate { + _u.mutation.SetProjectID(v) + return _u +} + +// SetNillableProjectID sets the "project_id" field if the given value is not nil. +func (_u *SubscriptionTemplateUpdate) SetNillableProjectID(v *uuid.UUID) *SubscriptionTemplateUpdate { + if v != nil { + _u.SetProjectID(*v) + } + return _u +} + +// ClearProjectID clears the value of the "project_id" field. +func (_u *SubscriptionTemplateUpdate) ClearProjectID() *SubscriptionTemplateUpdate { + _u.mutation.ClearProjectID() + return _u +} + +// SetCreatedBy sets the "created_by" field. +func (_u *SubscriptionTemplateUpdate) SetCreatedBy(v string) *SubscriptionTemplateUpdate { + _u.mutation.SetCreatedBy(v) + return _u +} + +// SetNillableCreatedBy sets the "created_by" field if the given value is not nil. +func (_u *SubscriptionTemplateUpdate) SetNillableCreatedBy(v *string) *SubscriptionTemplateUpdate { + if v != nil { + _u.SetCreatedBy(*v) + } + return _u +} + +// Mutation returns the SubscriptionTemplateMutation object of the builder. +func (_u *SubscriptionTemplateUpdate) Mutation() *SubscriptionTemplateMutation { + return _u.mutation +} + +// Save executes the query and returns the number of nodes affected by the update operation. +func (_u *SubscriptionTemplateUpdate) Save(ctx context.Context) (int, error) { + return withHooks(ctx, _u.sqlSave, _u.mutation, _u.hooks) +} + +// SaveX is like Save, but panics if an error occurs. +func (_u *SubscriptionTemplateUpdate) SaveX(ctx context.Context) int { + affected, err := _u.Save(ctx) + if err != nil { + panic(err) + } + return affected +} + +// Exec executes the query. +func (_u *SubscriptionTemplateUpdate) Exec(ctx context.Context) error { + _, err := _u.Save(ctx) + return err +} + +// ExecX is like Exec, but panics if an error occurs. +func (_u *SubscriptionTemplateUpdate) ExecX(ctx context.Context) { + if err := _u.Exec(ctx); err != nil { + panic(err) + } +} + +// check runs all checks and user-defined validators on the builder. +func (_u *SubscriptionTemplateUpdate) check() error { + if v, ok := _u.mutation.Name(); ok { + if err := subscriptiontemplate.NameValidator(v); err != nil { + return &ValidationError{Name: "name", err: fmt.Errorf(`ent: validator failed for field "SubscriptionTemplate.name": %w`, err)} + } + } + if v, ok := _u.mutation.TriggerActivities(); ok { + if err := subscriptiontemplate.TriggerActivitiesValidator(v); err != nil { + return &ValidationError{Name: "trigger_activities", err: fmt.Errorf(`ent: validator failed for field "SubscriptionTemplate.trigger_activities": %w`, err)} + } + } + if v, ok := _u.mutation.CreatedBy(); ok { + if err := subscriptiontemplate.CreatedByValidator(v); err != nil { + return &ValidationError{Name: "created_by", err: fmt.Errorf(`ent: validator failed for field "SubscriptionTemplate.created_by": %w`, err)} + } + } + return nil +} + +func (_u *SubscriptionTemplateUpdate) sqlSave(ctx context.Context) (_node int, err error) { + if err := _u.check(); err != nil { + return _node, err + } + _spec := sqlgraph.NewUpdateSpec(subscriptiontemplate.Table, subscriptiontemplate.Columns, sqlgraph.NewFieldSpec(subscriptiontemplate.FieldID, field.TypeUUID)) + if ps := _u.mutation.predicates; len(ps) > 0 { + _spec.Predicate = func(selector *sql.Selector) { + for i := range ps { + ps[i](selector) + } + } + } + if value, ok := _u.mutation.Name(); ok { + _spec.SetField(subscriptiontemplate.FieldName, field.TypeString, value) + } + if value, ok := _u.mutation.Scope(); ok { + _spec.SetField(subscriptiontemplate.FieldScope, field.TypeString, value) + } + if value, ok := _u.mutation.TriggerActivities(); ok { + _spec.SetField(subscriptiontemplate.FieldTriggerActivities, field.TypeString, value) + } + if value, ok := _u.mutation.ProjectID(); ok { + _spec.SetField(subscriptiontemplate.FieldProjectID, field.TypeUUID, value) + } + if _u.mutation.ProjectIDCleared() { + _spec.ClearField(subscriptiontemplate.FieldProjectID, field.TypeUUID) + } + if value, ok := _u.mutation.CreatedBy(); ok { + _spec.SetField(subscriptiontemplate.FieldCreatedBy, field.TypeString, value) + } + if _node, err = sqlgraph.UpdateNodes(ctx, _u.driver, _spec); err != nil { + if _, ok := err.(*sqlgraph.NotFoundError); ok { + err = &NotFoundError{subscriptiontemplate.Label} + } else if sqlgraph.IsConstraintError(err) { + err = &ConstraintError{msg: err.Error(), wrap: err} + } + return 0, err + } + _u.mutation.done = true + return _node, nil +} + +// SubscriptionTemplateUpdateOne is the builder for updating a single SubscriptionTemplate entity. +type SubscriptionTemplateUpdateOne struct { + config + fields []string + hooks []Hook + mutation *SubscriptionTemplateMutation +} + +// SetName sets the "name" field. +func (_u *SubscriptionTemplateUpdateOne) SetName(v string) *SubscriptionTemplateUpdateOne { + _u.mutation.SetName(v) + return _u +} + +// SetNillableName sets the "name" field if the given value is not nil. +func (_u *SubscriptionTemplateUpdateOne) SetNillableName(v *string) *SubscriptionTemplateUpdateOne { + if v != nil { + _u.SetName(*v) + } + return _u +} + +// SetScope sets the "scope" field. +func (_u *SubscriptionTemplateUpdateOne) SetScope(v string) *SubscriptionTemplateUpdateOne { + _u.mutation.SetScope(v) + return _u +} + +// SetNillableScope sets the "scope" field if the given value is not nil. +func (_u *SubscriptionTemplateUpdateOne) SetNillableScope(v *string) *SubscriptionTemplateUpdateOne { + if v != nil { + _u.SetScope(*v) + } + return _u +} + +// SetTriggerActivities sets the "trigger_activities" field. +func (_u *SubscriptionTemplateUpdateOne) SetTriggerActivities(v string) *SubscriptionTemplateUpdateOne { + _u.mutation.SetTriggerActivities(v) + return _u +} + +// SetNillableTriggerActivities sets the "trigger_activities" field if the given value is not nil. +func (_u *SubscriptionTemplateUpdateOne) SetNillableTriggerActivities(v *string) *SubscriptionTemplateUpdateOne { + if v != nil { + _u.SetTriggerActivities(*v) + } + return _u +} + +// SetProjectID sets the "project_id" field. +func (_u *SubscriptionTemplateUpdateOne) SetProjectID(v uuid.UUID) *SubscriptionTemplateUpdateOne { + _u.mutation.SetProjectID(v) + return _u +} + +// SetNillableProjectID sets the "project_id" field if the given value is not nil. +func (_u *SubscriptionTemplateUpdateOne) SetNillableProjectID(v *uuid.UUID) *SubscriptionTemplateUpdateOne { + if v != nil { + _u.SetProjectID(*v) + } + return _u +} + +// ClearProjectID clears the value of the "project_id" field. +func (_u *SubscriptionTemplateUpdateOne) ClearProjectID() *SubscriptionTemplateUpdateOne { + _u.mutation.ClearProjectID() + return _u +} + +// SetCreatedBy sets the "created_by" field. +func (_u *SubscriptionTemplateUpdateOne) SetCreatedBy(v string) *SubscriptionTemplateUpdateOne { + _u.mutation.SetCreatedBy(v) + return _u +} + +// SetNillableCreatedBy sets the "created_by" field if the given value is not nil. +func (_u *SubscriptionTemplateUpdateOne) SetNillableCreatedBy(v *string) *SubscriptionTemplateUpdateOne { + if v != nil { + _u.SetCreatedBy(*v) + } + return _u +} + +// Mutation returns the SubscriptionTemplateMutation object of the builder. +func (_u *SubscriptionTemplateUpdateOne) Mutation() *SubscriptionTemplateMutation { + return _u.mutation +} + +// Where appends a list predicates to the SubscriptionTemplateUpdate builder. +func (_u *SubscriptionTemplateUpdateOne) Where(ps ...predicate.SubscriptionTemplate) *SubscriptionTemplateUpdateOne { + _u.mutation.Where(ps...) + return _u +} + +// Select allows selecting one or more fields (columns) of the returned entity. +// The default is selecting all fields defined in the entity schema. +func (_u *SubscriptionTemplateUpdateOne) Select(field string, fields ...string) *SubscriptionTemplateUpdateOne { + _u.fields = append([]string{field}, fields...) + return _u +} + +// Save executes the query and returns the updated SubscriptionTemplate entity. +func (_u *SubscriptionTemplateUpdateOne) Save(ctx context.Context) (*SubscriptionTemplate, error) { + return withHooks(ctx, _u.sqlSave, _u.mutation, _u.hooks) +} + +// SaveX is like Save, but panics if an error occurs. +func (_u *SubscriptionTemplateUpdateOne) SaveX(ctx context.Context) *SubscriptionTemplate { + node, err := _u.Save(ctx) + if err != nil { + panic(err) + } + return node +} + +// Exec executes the query on the entity. +func (_u *SubscriptionTemplateUpdateOne) Exec(ctx context.Context) error { + _, err := _u.Save(ctx) + return err +} + +// ExecX is like Exec, but panics if an error occurs. +func (_u *SubscriptionTemplateUpdateOne) ExecX(ctx context.Context) { + if err := _u.Exec(ctx); err != nil { + panic(err) + } +} + +// check runs all checks and user-defined validators on the builder. +func (_u *SubscriptionTemplateUpdateOne) check() error { + if v, ok := _u.mutation.Name(); ok { + if err := subscriptiontemplate.NameValidator(v); err != nil { + return &ValidationError{Name: "name", err: fmt.Errorf(`ent: validator failed for field "SubscriptionTemplate.name": %w`, err)} + } + } + if v, ok := _u.mutation.TriggerActivities(); ok { + if err := subscriptiontemplate.TriggerActivitiesValidator(v); err != nil { + return &ValidationError{Name: "trigger_activities", err: fmt.Errorf(`ent: validator failed for field "SubscriptionTemplate.trigger_activities": %w`, err)} + } + } + if v, ok := _u.mutation.CreatedBy(); ok { + if err := subscriptiontemplate.CreatedByValidator(v); err != nil { + return &ValidationError{Name: "created_by", err: fmt.Errorf(`ent: validator failed for field "SubscriptionTemplate.created_by": %w`, err)} + } + } + return nil +} + +func (_u *SubscriptionTemplateUpdateOne) sqlSave(ctx context.Context) (_node *SubscriptionTemplate, err error) { + if err := _u.check(); err != nil { + return _node, err + } + _spec := sqlgraph.NewUpdateSpec(subscriptiontemplate.Table, subscriptiontemplate.Columns, sqlgraph.NewFieldSpec(subscriptiontemplate.FieldID, field.TypeUUID)) + id, ok := _u.mutation.ID() + if !ok { + return nil, &ValidationError{Name: "id", err: errors.New(`ent: missing "SubscriptionTemplate.id" for update`)} + } + _spec.Node.ID.Value = id + if fields := _u.fields; len(fields) > 0 { + _spec.Node.Columns = make([]string, 0, len(fields)) + _spec.Node.Columns = append(_spec.Node.Columns, subscriptiontemplate.FieldID) + for _, f := range fields { + if !subscriptiontemplate.ValidColumn(f) { + return nil, &ValidationError{Name: f, err: fmt.Errorf("ent: invalid field %q for query", f)} + } + if f != subscriptiontemplate.FieldID { + _spec.Node.Columns = append(_spec.Node.Columns, f) + } + } + } + if ps := _u.mutation.predicates; len(ps) > 0 { + _spec.Predicate = func(selector *sql.Selector) { + for i := range ps { + ps[i](selector) + } + } + } + if value, ok := _u.mutation.Name(); ok { + _spec.SetField(subscriptiontemplate.FieldName, field.TypeString, value) + } + if value, ok := _u.mutation.Scope(); ok { + _spec.SetField(subscriptiontemplate.FieldScope, field.TypeString, value) + } + if value, ok := _u.mutation.TriggerActivities(); ok { + _spec.SetField(subscriptiontemplate.FieldTriggerActivities, field.TypeString, value) + } + if value, ok := _u.mutation.ProjectID(); ok { + _spec.SetField(subscriptiontemplate.FieldProjectID, field.TypeUUID, value) + } + if _u.mutation.ProjectIDCleared() { + _spec.ClearField(subscriptiontemplate.FieldProjectID, field.TypeUUID) + } + if value, ok := _u.mutation.CreatedBy(); ok { + _spec.SetField(subscriptiontemplate.FieldCreatedBy, field.TypeString, value) + } + _node = &SubscriptionTemplate{config: _u.config} + _spec.Assign = _node.assignValues + _spec.ScanValues = _node.scanValues + if err = sqlgraph.UpdateNode(ctx, _u.driver, _spec); err != nil { + if _, ok := err.(*sqlgraph.NotFoundError); ok { + err = &NotFoundError{subscriptiontemplate.Label} + } else if sqlgraph.IsConstraintError(err) { + err = &ConstraintError{msg: err.Error(), wrap: err} + } + return nil, err + } + _u.mutation.done = true + return _node, nil +} diff --git a/pkg/ent/template.go b/pkg/ent/template.go new file mode 100644 index 000000000..e54da9747 --- /dev/null +++ b/pkg/ent/template.go @@ -0,0 +1,360 @@ +// Code generated by ent, DO NOT EDIT. + +package ent + +import ( + "fmt" + "strings" + "time" + + "entgo.io/ent" + "entgo.io/ent/dialect/sql" + "github.com/GoogleCloudPlatform/scion/pkg/ent/template" + "github.com/google/uuid" +) + +// Template is the model entity for the Template schema. +type Template struct { + config `json:"-"` + // ID of the ent. + ID uuid.UUID `json:"id,omitempty"` + // Name holds the value of the "name" field. + Name string `json:"name,omitempty"` + // Slug holds the value of the "slug" field. + Slug string `json:"slug,omitempty"` + // DisplayName holds the value of the "display_name" field. + DisplayName string `json:"display_name,omitempty"` + // Description holds the value of the "description" field. + Description string `json:"description,omitempty"` + // Harness holds the value of the "harness" field. + Harness string `json:"harness,omitempty"` + // DefaultHarnessConfig holds the value of the "default_harness_config" field. + DefaultHarnessConfig string `json:"default_harness_config,omitempty"` + // Image holds the value of the "image" field. + Image string `json:"image,omitempty"` + // Config holds the value of the "config" field. + Config string `json:"config,omitempty"` + // ContentHash holds the value of the "content_hash" field. + ContentHash string `json:"content_hash,omitempty"` + // Scope holds the value of the "scope" field. + Scope string `json:"scope,omitempty"` + // ScopeID holds the value of the "scope_id" field. + ScopeID string `json:"scope_id,omitempty"` + // ProjectID holds the value of the "project_id" field. + ProjectID string `json:"project_id,omitempty"` + // StorageURI holds the value of the "storage_uri" field. + StorageURI string `json:"storage_uri,omitempty"` + // StorageBucket holds the value of the "storage_bucket" field. + StorageBucket string `json:"storage_bucket,omitempty"` + // StoragePath holds the value of the "storage_path" field. + StoragePath string `json:"storage_path,omitempty"` + // Files holds the value of the "files" field. + Files string `json:"files,omitempty"` + // BaseTemplate holds the value of the "base_template" field. + BaseTemplate string `json:"base_template,omitempty"` + // Status holds the value of the "status" field. + Status template.Status `json:"status,omitempty"` + // OwnerID holds the value of the "owner_id" field. + OwnerID string `json:"owner_id,omitempty"` + // CreatedBy holds the value of the "created_by" field. + CreatedBy string `json:"created_by,omitempty"` + // UpdatedBy holds the value of the "updated_by" field. + UpdatedBy string `json:"updated_by,omitempty"` + // Visibility holds the value of the "visibility" field. + Visibility string `json:"visibility,omitempty"` + // Created holds the value of the "created" field. + Created time.Time `json:"created,omitempty"` + // Updated holds the value of the "updated" field. + Updated time.Time `json:"updated,omitempty"` + selectValues sql.SelectValues +} + +// scanValues returns the types for scanning values from sql.Rows. +func (*Template) scanValues(columns []string) ([]any, error) { + values := make([]any, len(columns)) + for i := range columns { + switch columns[i] { + case template.FieldName, template.FieldSlug, template.FieldDisplayName, template.FieldDescription, template.FieldHarness, template.FieldDefaultHarnessConfig, template.FieldImage, template.FieldConfig, template.FieldContentHash, template.FieldScope, template.FieldScopeID, template.FieldProjectID, template.FieldStorageURI, template.FieldStorageBucket, template.FieldStoragePath, template.FieldFiles, template.FieldBaseTemplate, template.FieldStatus, template.FieldOwnerID, template.FieldCreatedBy, template.FieldUpdatedBy, template.FieldVisibility: + values[i] = new(sql.NullString) + case template.FieldCreated, template.FieldUpdated: + values[i] = new(sql.NullTime) + case template.FieldID: + values[i] = new(uuid.UUID) + default: + values[i] = new(sql.UnknownType) + } + } + return values, nil +} + +// assignValues assigns the values that were returned from sql.Rows (after scanning) +// to the Template fields. +func (_m *Template) assignValues(columns []string, values []any) error { + if m, n := len(values), len(columns); m < n { + return fmt.Errorf("mismatch number of scan values: %d != %d", m, n) + } + for i := range columns { + switch columns[i] { + case template.FieldID: + if value, ok := values[i].(*uuid.UUID); !ok { + return fmt.Errorf("unexpected type %T for field id", values[i]) + } else if value != nil { + _m.ID = *value + } + case template.FieldName: + if value, ok := values[i].(*sql.NullString); !ok { + return fmt.Errorf("unexpected type %T for field name", values[i]) + } else if value.Valid { + _m.Name = value.String + } + case template.FieldSlug: + if value, ok := values[i].(*sql.NullString); !ok { + return fmt.Errorf("unexpected type %T for field slug", values[i]) + } else if value.Valid { + _m.Slug = value.String + } + case template.FieldDisplayName: + if value, ok := values[i].(*sql.NullString); !ok { + return fmt.Errorf("unexpected type %T for field display_name", values[i]) + } else if value.Valid { + _m.DisplayName = value.String + } + case template.FieldDescription: + if value, ok := values[i].(*sql.NullString); !ok { + return fmt.Errorf("unexpected type %T for field description", values[i]) + } else if value.Valid { + _m.Description = value.String + } + case template.FieldHarness: + if value, ok := values[i].(*sql.NullString); !ok { + return fmt.Errorf("unexpected type %T for field harness", values[i]) + } else if value.Valid { + _m.Harness = value.String + } + case template.FieldDefaultHarnessConfig: + if value, ok := values[i].(*sql.NullString); !ok { + return fmt.Errorf("unexpected type %T for field default_harness_config", values[i]) + } else if value.Valid { + _m.DefaultHarnessConfig = value.String + } + case template.FieldImage: + if value, ok := values[i].(*sql.NullString); !ok { + return fmt.Errorf("unexpected type %T for field image", values[i]) + } else if value.Valid { + _m.Image = value.String + } + case template.FieldConfig: + if value, ok := values[i].(*sql.NullString); !ok { + return fmt.Errorf("unexpected type %T for field config", values[i]) + } else if value.Valid { + _m.Config = value.String + } + case template.FieldContentHash: + if value, ok := values[i].(*sql.NullString); !ok { + return fmt.Errorf("unexpected type %T for field content_hash", values[i]) + } else if value.Valid { + _m.ContentHash = value.String + } + case template.FieldScope: + if value, ok := values[i].(*sql.NullString); !ok { + return fmt.Errorf("unexpected type %T for field scope", values[i]) + } else if value.Valid { + _m.Scope = value.String + } + case template.FieldScopeID: + if value, ok := values[i].(*sql.NullString); !ok { + return fmt.Errorf("unexpected type %T for field scope_id", values[i]) + } else if value.Valid { + _m.ScopeID = value.String + } + case template.FieldProjectID: + if value, ok := values[i].(*sql.NullString); !ok { + return fmt.Errorf("unexpected type %T for field project_id", values[i]) + } else if value.Valid { + _m.ProjectID = value.String + } + case template.FieldStorageURI: + if value, ok := values[i].(*sql.NullString); !ok { + return fmt.Errorf("unexpected type %T for field storage_uri", values[i]) + } else if value.Valid { + _m.StorageURI = value.String + } + case template.FieldStorageBucket: + if value, ok := values[i].(*sql.NullString); !ok { + return fmt.Errorf("unexpected type %T for field storage_bucket", values[i]) + } else if value.Valid { + _m.StorageBucket = value.String + } + case template.FieldStoragePath: + if value, ok := values[i].(*sql.NullString); !ok { + return fmt.Errorf("unexpected type %T for field storage_path", values[i]) + } else if value.Valid { + _m.StoragePath = value.String + } + case template.FieldFiles: + if value, ok := values[i].(*sql.NullString); !ok { + return fmt.Errorf("unexpected type %T for field files", values[i]) + } else if value.Valid { + _m.Files = value.String + } + case template.FieldBaseTemplate: + if value, ok := values[i].(*sql.NullString); !ok { + return fmt.Errorf("unexpected type %T for field base_template", values[i]) + } else if value.Valid { + _m.BaseTemplate = value.String + } + case template.FieldStatus: + if value, ok := values[i].(*sql.NullString); !ok { + return fmt.Errorf("unexpected type %T for field status", values[i]) + } else if value.Valid { + _m.Status = template.Status(value.String) + } + case template.FieldOwnerID: + if value, ok := values[i].(*sql.NullString); !ok { + return fmt.Errorf("unexpected type %T for field owner_id", values[i]) + } else if value.Valid { + _m.OwnerID = value.String + } + case template.FieldCreatedBy: + if value, ok := values[i].(*sql.NullString); !ok { + return fmt.Errorf("unexpected type %T for field created_by", values[i]) + } else if value.Valid { + _m.CreatedBy = value.String + } + case template.FieldUpdatedBy: + if value, ok := values[i].(*sql.NullString); !ok { + return fmt.Errorf("unexpected type %T for field updated_by", values[i]) + } else if value.Valid { + _m.UpdatedBy = value.String + } + case template.FieldVisibility: + if value, ok := values[i].(*sql.NullString); !ok { + return fmt.Errorf("unexpected type %T for field visibility", values[i]) + } else if value.Valid { + _m.Visibility = value.String + } + case template.FieldCreated: + if value, ok := values[i].(*sql.NullTime); !ok { + return fmt.Errorf("unexpected type %T for field created", values[i]) + } else if value.Valid { + _m.Created = value.Time + } + case template.FieldUpdated: + if value, ok := values[i].(*sql.NullTime); !ok { + return fmt.Errorf("unexpected type %T for field updated", values[i]) + } else if value.Valid { + _m.Updated = value.Time + } + default: + _m.selectValues.Set(columns[i], values[i]) + } + } + return nil +} + +// Value returns the ent.Value that was dynamically selected and assigned to the Template. +// This includes values selected through modifiers, order, etc. +func (_m *Template) Value(name string) (ent.Value, error) { + return _m.selectValues.Get(name) +} + +// Update returns a builder for updating this Template. +// Note that you need to call Template.Unwrap() before calling this method if this Template +// was returned from a transaction, and the transaction was committed or rolled back. +func (_m *Template) Update() *TemplateUpdateOne { + return NewTemplateClient(_m.config).UpdateOne(_m) +} + +// Unwrap unwraps the Template entity that was returned from a transaction after it was closed, +// so that all future queries will be executed through the driver which created the transaction. +func (_m *Template) Unwrap() *Template { + _tx, ok := _m.config.driver.(*txDriver) + if !ok { + panic("ent: Template is not a transactional entity") + } + _m.config.driver = _tx.drv + return _m +} + +// String implements the fmt.Stringer. +func (_m *Template) String() string { + var builder strings.Builder + builder.WriteString("Template(") + builder.WriteString(fmt.Sprintf("id=%v, ", _m.ID)) + builder.WriteString("name=") + builder.WriteString(_m.Name) + builder.WriteString(", ") + builder.WriteString("slug=") + builder.WriteString(_m.Slug) + builder.WriteString(", ") + builder.WriteString("display_name=") + builder.WriteString(_m.DisplayName) + builder.WriteString(", ") + builder.WriteString("description=") + builder.WriteString(_m.Description) + builder.WriteString(", ") + builder.WriteString("harness=") + builder.WriteString(_m.Harness) + builder.WriteString(", ") + builder.WriteString("default_harness_config=") + builder.WriteString(_m.DefaultHarnessConfig) + builder.WriteString(", ") + builder.WriteString("image=") + builder.WriteString(_m.Image) + builder.WriteString(", ") + builder.WriteString("config=") + builder.WriteString(_m.Config) + builder.WriteString(", ") + builder.WriteString("content_hash=") + builder.WriteString(_m.ContentHash) + builder.WriteString(", ") + builder.WriteString("scope=") + builder.WriteString(_m.Scope) + builder.WriteString(", ") + builder.WriteString("scope_id=") + builder.WriteString(_m.ScopeID) + builder.WriteString(", ") + builder.WriteString("project_id=") + builder.WriteString(_m.ProjectID) + builder.WriteString(", ") + builder.WriteString("storage_uri=") + builder.WriteString(_m.StorageURI) + builder.WriteString(", ") + builder.WriteString("storage_bucket=") + builder.WriteString(_m.StorageBucket) + builder.WriteString(", ") + builder.WriteString("storage_path=") + builder.WriteString(_m.StoragePath) + builder.WriteString(", ") + builder.WriteString("files=") + builder.WriteString(_m.Files) + builder.WriteString(", ") + builder.WriteString("base_template=") + builder.WriteString(_m.BaseTemplate) + builder.WriteString(", ") + builder.WriteString("status=") + builder.WriteString(fmt.Sprintf("%v", _m.Status)) + builder.WriteString(", ") + builder.WriteString("owner_id=") + builder.WriteString(_m.OwnerID) + builder.WriteString(", ") + builder.WriteString("created_by=") + builder.WriteString(_m.CreatedBy) + builder.WriteString(", ") + builder.WriteString("updated_by=") + builder.WriteString(_m.UpdatedBy) + builder.WriteString(", ") + builder.WriteString("visibility=") + builder.WriteString(_m.Visibility) + builder.WriteString(", ") + builder.WriteString("created=") + builder.WriteString(_m.Created.Format(time.ANSIC)) + builder.WriteString(", ") + builder.WriteString("updated=") + builder.WriteString(_m.Updated.Format(time.ANSIC)) + builder.WriteByte(')') + return builder.String() +} + +// Templates is a parsable slice of Template. +type Templates []*Template diff --git a/pkg/ent/template/template.go b/pkg/ent/template/template.go new file mode 100644 index 000000000..1da8b5ce9 --- /dev/null +++ b/pkg/ent/template/template.go @@ -0,0 +1,281 @@ +// Code generated by ent, DO NOT EDIT. + +package template + +import ( + "fmt" + "time" + + "entgo.io/ent/dialect/sql" + "github.com/google/uuid" +) + +const ( + // Label holds the string label denoting the template type in the database. + Label = "template" + // FieldID holds the string denoting the id field in the database. + FieldID = "id" + // FieldName holds the string denoting the name field in the database. + FieldName = "name" + // FieldSlug holds the string denoting the slug field in the database. + FieldSlug = "slug" + // FieldDisplayName holds the string denoting the display_name field in the database. + FieldDisplayName = "display_name" + // FieldDescription holds the string denoting the description field in the database. + FieldDescription = "description" + // FieldHarness holds the string denoting the harness field in the database. + FieldHarness = "harness" + // FieldDefaultHarnessConfig holds the string denoting the default_harness_config field in the database. + FieldDefaultHarnessConfig = "default_harness_config" + // FieldImage holds the string denoting the image field in the database. + FieldImage = "image" + // FieldConfig holds the string denoting the config field in the database. + FieldConfig = "config" + // FieldContentHash holds the string denoting the content_hash field in the database. + FieldContentHash = "content_hash" + // FieldScope holds the string denoting the scope field in the database. + FieldScope = "scope" + // FieldScopeID holds the string denoting the scope_id field in the database. + FieldScopeID = "scope_id" + // FieldProjectID holds the string denoting the project_id field in the database. + FieldProjectID = "project_id" + // FieldStorageURI holds the string denoting the storage_uri field in the database. + FieldStorageURI = "storage_uri" + // FieldStorageBucket holds the string denoting the storage_bucket field in the database. + FieldStorageBucket = "storage_bucket" + // FieldStoragePath holds the string denoting the storage_path field in the database. + FieldStoragePath = "storage_path" + // FieldFiles holds the string denoting the files field in the database. + FieldFiles = "files" + // FieldBaseTemplate holds the string denoting the base_template field in the database. + FieldBaseTemplate = "base_template" + // FieldStatus holds the string denoting the status field in the database. + FieldStatus = "status" + // FieldOwnerID holds the string denoting the owner_id field in the database. + FieldOwnerID = "owner_id" + // FieldCreatedBy holds the string denoting the created_by field in the database. + FieldCreatedBy = "created_by" + // FieldUpdatedBy holds the string denoting the updated_by field in the database. + FieldUpdatedBy = "updated_by" + // FieldVisibility holds the string denoting the visibility field in the database. + FieldVisibility = "visibility" + // FieldCreated holds the string denoting the created field in the database. + FieldCreated = "created" + // FieldUpdated holds the string denoting the updated field in the database. + FieldUpdated = "updated" + // Table holds the table name of the template in the database. + Table = "templates" +) + +// Columns holds all SQL columns for template fields. +var Columns = []string{ + FieldID, + FieldName, + FieldSlug, + FieldDisplayName, + FieldDescription, + FieldHarness, + FieldDefaultHarnessConfig, + FieldImage, + FieldConfig, + FieldContentHash, + FieldScope, + FieldScopeID, + FieldProjectID, + FieldStorageURI, + FieldStorageBucket, + FieldStoragePath, + FieldFiles, + FieldBaseTemplate, + FieldStatus, + FieldOwnerID, + FieldCreatedBy, + FieldUpdatedBy, + FieldVisibility, + FieldCreated, + FieldUpdated, +} + +// ValidColumn reports if the column name is valid (part of the table columns). +func ValidColumn(column string) bool { + for i := range Columns { + if column == Columns[i] { + return true + } + } + return false +} + +var ( + // NameValidator is a validator for the "name" field. It is called by the builders before save. + NameValidator func(string) error + // SlugValidator is a validator for the "slug" field. It is called by the builders before save. + SlugValidator func(string) error + // DefaultScope holds the default value on creation for the "scope" field. + DefaultScope string + // DefaultVisibility holds the default value on creation for the "visibility" field. + DefaultVisibility string + // DefaultCreated holds the default value on creation for the "created" field. + DefaultCreated func() time.Time + // DefaultUpdated holds the default value on creation for the "updated" field. + DefaultUpdated func() time.Time + // UpdateDefaultUpdated holds the default value on update for the "updated" field. + UpdateDefaultUpdated func() time.Time + // DefaultID holds the default value on creation for the "id" field. + DefaultID func() uuid.UUID +) + +// Status defines the type for the "status" enum field. +type Status string + +// StatusActive is the default value of the Status enum. +const DefaultStatus = StatusActive + +// Status values. +const ( + StatusPending Status = "pending" + StatusActive Status = "active" + StatusArchived Status = "archived" +) + +func (s Status) String() string { + return string(s) +} + +// StatusValidator is a validator for the "status" field enum values. It is called by the builders before save. +func StatusValidator(s Status) error { + switch s { + case StatusPending, StatusActive, StatusArchived: + return nil + default: + return fmt.Errorf("template: invalid enum value for status field: %q", s) + } +} + +// OrderOption defines the ordering options for the Template queries. +type OrderOption func(*sql.Selector) + +// ByID orders the results by the id field. +func ByID(opts ...sql.OrderTermOption) OrderOption { + return sql.OrderByField(FieldID, opts...).ToFunc() +} + +// ByName orders the results by the name field. +func ByName(opts ...sql.OrderTermOption) OrderOption { + return sql.OrderByField(FieldName, opts...).ToFunc() +} + +// BySlug orders the results by the slug field. +func BySlug(opts ...sql.OrderTermOption) OrderOption { + return sql.OrderByField(FieldSlug, opts...).ToFunc() +} + +// ByDisplayName orders the results by the display_name field. +func ByDisplayName(opts ...sql.OrderTermOption) OrderOption { + return sql.OrderByField(FieldDisplayName, opts...).ToFunc() +} + +// ByDescription orders the results by the description field. +func ByDescription(opts ...sql.OrderTermOption) OrderOption { + return sql.OrderByField(FieldDescription, opts...).ToFunc() +} + +// ByHarness orders the results by the harness field. +func ByHarness(opts ...sql.OrderTermOption) OrderOption { + return sql.OrderByField(FieldHarness, opts...).ToFunc() +} + +// ByDefaultHarnessConfig orders the results by the default_harness_config field. +func ByDefaultHarnessConfig(opts ...sql.OrderTermOption) OrderOption { + return sql.OrderByField(FieldDefaultHarnessConfig, opts...).ToFunc() +} + +// ByImage orders the results by the image field. +func ByImage(opts ...sql.OrderTermOption) OrderOption { + return sql.OrderByField(FieldImage, opts...).ToFunc() +} + +// ByConfig orders the results by the config field. +func ByConfig(opts ...sql.OrderTermOption) OrderOption { + return sql.OrderByField(FieldConfig, opts...).ToFunc() +} + +// ByContentHash orders the results by the content_hash field. +func ByContentHash(opts ...sql.OrderTermOption) OrderOption { + return sql.OrderByField(FieldContentHash, opts...).ToFunc() +} + +// ByScope orders the results by the scope field. +func ByScope(opts ...sql.OrderTermOption) OrderOption { + return sql.OrderByField(FieldScope, opts...).ToFunc() +} + +// ByScopeID orders the results by the scope_id field. +func ByScopeID(opts ...sql.OrderTermOption) OrderOption { + return sql.OrderByField(FieldScopeID, opts...).ToFunc() +} + +// ByProjectID orders the results by the project_id field. +func ByProjectID(opts ...sql.OrderTermOption) OrderOption { + return sql.OrderByField(FieldProjectID, opts...).ToFunc() +} + +// ByStorageURI orders the results by the storage_uri field. +func ByStorageURI(opts ...sql.OrderTermOption) OrderOption { + return sql.OrderByField(FieldStorageURI, opts...).ToFunc() +} + +// ByStorageBucket orders the results by the storage_bucket field. +func ByStorageBucket(opts ...sql.OrderTermOption) OrderOption { + return sql.OrderByField(FieldStorageBucket, opts...).ToFunc() +} + +// ByStoragePath orders the results by the storage_path field. +func ByStoragePath(opts ...sql.OrderTermOption) OrderOption { + return sql.OrderByField(FieldStoragePath, opts...).ToFunc() +} + +// ByFiles orders the results by the files field. +func ByFiles(opts ...sql.OrderTermOption) OrderOption { + return sql.OrderByField(FieldFiles, opts...).ToFunc() +} + +// ByBaseTemplate orders the results by the base_template field. +func ByBaseTemplate(opts ...sql.OrderTermOption) OrderOption { + return sql.OrderByField(FieldBaseTemplate, opts...).ToFunc() +} + +// ByStatus orders the results by the status field. +func ByStatus(opts ...sql.OrderTermOption) OrderOption { + return sql.OrderByField(FieldStatus, opts...).ToFunc() +} + +// ByOwnerID orders the results by the owner_id field. +func ByOwnerID(opts ...sql.OrderTermOption) OrderOption { + return sql.OrderByField(FieldOwnerID, opts...).ToFunc() +} + +// ByCreatedBy orders the results by the created_by field. +func ByCreatedBy(opts ...sql.OrderTermOption) OrderOption { + return sql.OrderByField(FieldCreatedBy, opts...).ToFunc() +} + +// ByUpdatedBy orders the results by the updated_by field. +func ByUpdatedBy(opts ...sql.OrderTermOption) OrderOption { + return sql.OrderByField(FieldUpdatedBy, opts...).ToFunc() +} + +// ByVisibility orders the results by the visibility field. +func ByVisibility(opts ...sql.OrderTermOption) OrderOption { + return sql.OrderByField(FieldVisibility, opts...).ToFunc() +} + +// ByCreated orders the results by the created field. +func ByCreated(opts ...sql.OrderTermOption) OrderOption { + return sql.OrderByField(FieldCreated, opts...).ToFunc() +} + +// ByUpdated orders the results by the updated field. +func ByUpdated(opts ...sql.OrderTermOption) OrderOption { + return sql.OrderByField(FieldUpdated, opts...).ToFunc() +} diff --git a/pkg/ent/template/where.go b/pkg/ent/template/where.go new file mode 100644 index 000000000..5a59a0bea --- /dev/null +++ b/pkg/ent/template/where.go @@ -0,0 +1,1811 @@ +// Code generated by ent, DO NOT EDIT. + +package template + +import ( + "time" + + "entgo.io/ent/dialect/sql" + "github.com/GoogleCloudPlatform/scion/pkg/ent/predicate" + "github.com/google/uuid" +) + +// ID filters vertices based on their ID field. +func ID(id uuid.UUID) predicate.Template { + return predicate.Template(sql.FieldEQ(FieldID, id)) +} + +// IDEQ applies the EQ predicate on the ID field. +func IDEQ(id uuid.UUID) predicate.Template { + return predicate.Template(sql.FieldEQ(FieldID, id)) +} + +// IDNEQ applies the NEQ predicate on the ID field. +func IDNEQ(id uuid.UUID) predicate.Template { + return predicate.Template(sql.FieldNEQ(FieldID, id)) +} + +// IDIn applies the In predicate on the ID field. +func IDIn(ids ...uuid.UUID) predicate.Template { + return predicate.Template(sql.FieldIn(FieldID, ids...)) +} + +// IDNotIn applies the NotIn predicate on the ID field. +func IDNotIn(ids ...uuid.UUID) predicate.Template { + return predicate.Template(sql.FieldNotIn(FieldID, ids...)) +} + +// IDGT applies the GT predicate on the ID field. +func IDGT(id uuid.UUID) predicate.Template { + return predicate.Template(sql.FieldGT(FieldID, id)) +} + +// IDGTE applies the GTE predicate on the ID field. +func IDGTE(id uuid.UUID) predicate.Template { + return predicate.Template(sql.FieldGTE(FieldID, id)) +} + +// IDLT applies the LT predicate on the ID field. +func IDLT(id uuid.UUID) predicate.Template { + return predicate.Template(sql.FieldLT(FieldID, id)) +} + +// IDLTE applies the LTE predicate on the ID field. +func IDLTE(id uuid.UUID) predicate.Template { + return predicate.Template(sql.FieldLTE(FieldID, id)) +} + +// Name applies equality check predicate on the "name" field. It's identical to NameEQ. +func Name(v string) predicate.Template { + return predicate.Template(sql.FieldEQ(FieldName, v)) +} + +// Slug applies equality check predicate on the "slug" field. It's identical to SlugEQ. +func Slug(v string) predicate.Template { + return predicate.Template(sql.FieldEQ(FieldSlug, v)) +} + +// DisplayName applies equality check predicate on the "display_name" field. It's identical to DisplayNameEQ. +func DisplayName(v string) predicate.Template { + return predicate.Template(sql.FieldEQ(FieldDisplayName, v)) +} + +// Description applies equality check predicate on the "description" field. It's identical to DescriptionEQ. +func Description(v string) predicate.Template { + return predicate.Template(sql.FieldEQ(FieldDescription, v)) +} + +// Harness applies equality check predicate on the "harness" field. It's identical to HarnessEQ. +func Harness(v string) predicate.Template { + return predicate.Template(sql.FieldEQ(FieldHarness, v)) +} + +// DefaultHarnessConfig applies equality check predicate on the "default_harness_config" field. It's identical to DefaultHarnessConfigEQ. +func DefaultHarnessConfig(v string) predicate.Template { + return predicate.Template(sql.FieldEQ(FieldDefaultHarnessConfig, v)) +} + +// Image applies equality check predicate on the "image" field. It's identical to ImageEQ. +func Image(v string) predicate.Template { + return predicate.Template(sql.FieldEQ(FieldImage, v)) +} + +// Config applies equality check predicate on the "config" field. It's identical to ConfigEQ. +func Config(v string) predicate.Template { + return predicate.Template(sql.FieldEQ(FieldConfig, v)) +} + +// ContentHash applies equality check predicate on the "content_hash" field. It's identical to ContentHashEQ. +func ContentHash(v string) predicate.Template { + return predicate.Template(sql.FieldEQ(FieldContentHash, v)) +} + +// Scope applies equality check predicate on the "scope" field. It's identical to ScopeEQ. +func Scope(v string) predicate.Template { + return predicate.Template(sql.FieldEQ(FieldScope, v)) +} + +// ScopeID applies equality check predicate on the "scope_id" field. It's identical to ScopeIDEQ. +func ScopeID(v string) predicate.Template { + return predicate.Template(sql.FieldEQ(FieldScopeID, v)) +} + +// ProjectID applies equality check predicate on the "project_id" field. It's identical to ProjectIDEQ. +func ProjectID(v string) predicate.Template { + return predicate.Template(sql.FieldEQ(FieldProjectID, v)) +} + +// StorageURI applies equality check predicate on the "storage_uri" field. It's identical to StorageURIEQ. +func StorageURI(v string) predicate.Template { + return predicate.Template(sql.FieldEQ(FieldStorageURI, v)) +} + +// StorageBucket applies equality check predicate on the "storage_bucket" field. It's identical to StorageBucketEQ. +func StorageBucket(v string) predicate.Template { + return predicate.Template(sql.FieldEQ(FieldStorageBucket, v)) +} + +// StoragePath applies equality check predicate on the "storage_path" field. It's identical to StoragePathEQ. +func StoragePath(v string) predicate.Template { + return predicate.Template(sql.FieldEQ(FieldStoragePath, v)) +} + +// Files applies equality check predicate on the "files" field. It's identical to FilesEQ. +func Files(v string) predicate.Template { + return predicate.Template(sql.FieldEQ(FieldFiles, v)) +} + +// BaseTemplate applies equality check predicate on the "base_template" field. It's identical to BaseTemplateEQ. +func BaseTemplate(v string) predicate.Template { + return predicate.Template(sql.FieldEQ(FieldBaseTemplate, v)) +} + +// OwnerID applies equality check predicate on the "owner_id" field. It's identical to OwnerIDEQ. +func OwnerID(v string) predicate.Template { + return predicate.Template(sql.FieldEQ(FieldOwnerID, v)) +} + +// CreatedBy applies equality check predicate on the "created_by" field. It's identical to CreatedByEQ. +func CreatedBy(v string) predicate.Template { + return predicate.Template(sql.FieldEQ(FieldCreatedBy, v)) +} + +// UpdatedBy applies equality check predicate on the "updated_by" field. It's identical to UpdatedByEQ. +func UpdatedBy(v string) predicate.Template { + return predicate.Template(sql.FieldEQ(FieldUpdatedBy, v)) +} + +// Visibility applies equality check predicate on the "visibility" field. It's identical to VisibilityEQ. +func Visibility(v string) predicate.Template { + return predicate.Template(sql.FieldEQ(FieldVisibility, v)) +} + +// Created applies equality check predicate on the "created" field. It's identical to CreatedEQ. +func Created(v time.Time) predicate.Template { + return predicate.Template(sql.FieldEQ(FieldCreated, v)) +} + +// Updated applies equality check predicate on the "updated" field. It's identical to UpdatedEQ. +func Updated(v time.Time) predicate.Template { + return predicate.Template(sql.FieldEQ(FieldUpdated, v)) +} + +// NameEQ applies the EQ predicate on the "name" field. +func NameEQ(v string) predicate.Template { + return predicate.Template(sql.FieldEQ(FieldName, v)) +} + +// NameNEQ applies the NEQ predicate on the "name" field. +func NameNEQ(v string) predicate.Template { + return predicate.Template(sql.FieldNEQ(FieldName, v)) +} + +// NameIn applies the In predicate on the "name" field. +func NameIn(vs ...string) predicate.Template { + return predicate.Template(sql.FieldIn(FieldName, vs...)) +} + +// NameNotIn applies the NotIn predicate on the "name" field. +func NameNotIn(vs ...string) predicate.Template { + return predicate.Template(sql.FieldNotIn(FieldName, vs...)) +} + +// NameGT applies the GT predicate on the "name" field. +func NameGT(v string) predicate.Template { + return predicate.Template(sql.FieldGT(FieldName, v)) +} + +// NameGTE applies the GTE predicate on the "name" field. +func NameGTE(v string) predicate.Template { + return predicate.Template(sql.FieldGTE(FieldName, v)) +} + +// NameLT applies the LT predicate on the "name" field. +func NameLT(v string) predicate.Template { + return predicate.Template(sql.FieldLT(FieldName, v)) +} + +// NameLTE applies the LTE predicate on the "name" field. +func NameLTE(v string) predicate.Template { + return predicate.Template(sql.FieldLTE(FieldName, v)) +} + +// NameContains applies the Contains predicate on the "name" field. +func NameContains(v string) predicate.Template { + return predicate.Template(sql.FieldContains(FieldName, v)) +} + +// NameHasPrefix applies the HasPrefix predicate on the "name" field. +func NameHasPrefix(v string) predicate.Template { + return predicate.Template(sql.FieldHasPrefix(FieldName, v)) +} + +// NameHasSuffix applies the HasSuffix predicate on the "name" field. +func NameHasSuffix(v string) predicate.Template { + return predicate.Template(sql.FieldHasSuffix(FieldName, v)) +} + +// NameEqualFold applies the EqualFold predicate on the "name" field. +func NameEqualFold(v string) predicate.Template { + return predicate.Template(sql.FieldEqualFold(FieldName, v)) +} + +// NameContainsFold applies the ContainsFold predicate on the "name" field. +func NameContainsFold(v string) predicate.Template { + return predicate.Template(sql.FieldContainsFold(FieldName, v)) +} + +// SlugEQ applies the EQ predicate on the "slug" field. +func SlugEQ(v string) predicate.Template { + return predicate.Template(sql.FieldEQ(FieldSlug, v)) +} + +// SlugNEQ applies the NEQ predicate on the "slug" field. +func SlugNEQ(v string) predicate.Template { + return predicate.Template(sql.FieldNEQ(FieldSlug, v)) +} + +// SlugIn applies the In predicate on the "slug" field. +func SlugIn(vs ...string) predicate.Template { + return predicate.Template(sql.FieldIn(FieldSlug, vs...)) +} + +// SlugNotIn applies the NotIn predicate on the "slug" field. +func SlugNotIn(vs ...string) predicate.Template { + return predicate.Template(sql.FieldNotIn(FieldSlug, vs...)) +} + +// SlugGT applies the GT predicate on the "slug" field. +func SlugGT(v string) predicate.Template { + return predicate.Template(sql.FieldGT(FieldSlug, v)) +} + +// SlugGTE applies the GTE predicate on the "slug" field. +func SlugGTE(v string) predicate.Template { + return predicate.Template(sql.FieldGTE(FieldSlug, v)) +} + +// SlugLT applies the LT predicate on the "slug" field. +func SlugLT(v string) predicate.Template { + return predicate.Template(sql.FieldLT(FieldSlug, v)) +} + +// SlugLTE applies the LTE predicate on the "slug" field. +func SlugLTE(v string) predicate.Template { + return predicate.Template(sql.FieldLTE(FieldSlug, v)) +} + +// SlugContains applies the Contains predicate on the "slug" field. +func SlugContains(v string) predicate.Template { + return predicate.Template(sql.FieldContains(FieldSlug, v)) +} + +// SlugHasPrefix applies the HasPrefix predicate on the "slug" field. +func SlugHasPrefix(v string) predicate.Template { + return predicate.Template(sql.FieldHasPrefix(FieldSlug, v)) +} + +// SlugHasSuffix applies the HasSuffix predicate on the "slug" field. +func SlugHasSuffix(v string) predicate.Template { + return predicate.Template(sql.FieldHasSuffix(FieldSlug, v)) +} + +// SlugEqualFold applies the EqualFold predicate on the "slug" field. +func SlugEqualFold(v string) predicate.Template { + return predicate.Template(sql.FieldEqualFold(FieldSlug, v)) +} + +// SlugContainsFold applies the ContainsFold predicate on the "slug" field. +func SlugContainsFold(v string) predicate.Template { + return predicate.Template(sql.FieldContainsFold(FieldSlug, v)) +} + +// DisplayNameEQ applies the EQ predicate on the "display_name" field. +func DisplayNameEQ(v string) predicate.Template { + return predicate.Template(sql.FieldEQ(FieldDisplayName, v)) +} + +// DisplayNameNEQ applies the NEQ predicate on the "display_name" field. +func DisplayNameNEQ(v string) predicate.Template { + return predicate.Template(sql.FieldNEQ(FieldDisplayName, v)) +} + +// DisplayNameIn applies the In predicate on the "display_name" field. +func DisplayNameIn(vs ...string) predicate.Template { + return predicate.Template(sql.FieldIn(FieldDisplayName, vs...)) +} + +// DisplayNameNotIn applies the NotIn predicate on the "display_name" field. +func DisplayNameNotIn(vs ...string) predicate.Template { + return predicate.Template(sql.FieldNotIn(FieldDisplayName, vs...)) +} + +// DisplayNameGT applies the GT predicate on the "display_name" field. +func DisplayNameGT(v string) predicate.Template { + return predicate.Template(sql.FieldGT(FieldDisplayName, v)) +} + +// DisplayNameGTE applies the GTE predicate on the "display_name" field. +func DisplayNameGTE(v string) predicate.Template { + return predicate.Template(sql.FieldGTE(FieldDisplayName, v)) +} + +// DisplayNameLT applies the LT predicate on the "display_name" field. +func DisplayNameLT(v string) predicate.Template { + return predicate.Template(sql.FieldLT(FieldDisplayName, v)) +} + +// DisplayNameLTE applies the LTE predicate on the "display_name" field. +func DisplayNameLTE(v string) predicate.Template { + return predicate.Template(sql.FieldLTE(FieldDisplayName, v)) +} + +// DisplayNameContains applies the Contains predicate on the "display_name" field. +func DisplayNameContains(v string) predicate.Template { + return predicate.Template(sql.FieldContains(FieldDisplayName, v)) +} + +// DisplayNameHasPrefix applies the HasPrefix predicate on the "display_name" field. +func DisplayNameHasPrefix(v string) predicate.Template { + return predicate.Template(sql.FieldHasPrefix(FieldDisplayName, v)) +} + +// DisplayNameHasSuffix applies the HasSuffix predicate on the "display_name" field. +func DisplayNameHasSuffix(v string) predicate.Template { + return predicate.Template(sql.FieldHasSuffix(FieldDisplayName, v)) +} + +// DisplayNameIsNil applies the IsNil predicate on the "display_name" field. +func DisplayNameIsNil() predicate.Template { + return predicate.Template(sql.FieldIsNull(FieldDisplayName)) +} + +// DisplayNameNotNil applies the NotNil predicate on the "display_name" field. +func DisplayNameNotNil() predicate.Template { + return predicate.Template(sql.FieldNotNull(FieldDisplayName)) +} + +// DisplayNameEqualFold applies the EqualFold predicate on the "display_name" field. +func DisplayNameEqualFold(v string) predicate.Template { + return predicate.Template(sql.FieldEqualFold(FieldDisplayName, v)) +} + +// DisplayNameContainsFold applies the ContainsFold predicate on the "display_name" field. +func DisplayNameContainsFold(v string) predicate.Template { + return predicate.Template(sql.FieldContainsFold(FieldDisplayName, v)) +} + +// DescriptionEQ applies the EQ predicate on the "description" field. +func DescriptionEQ(v string) predicate.Template { + return predicate.Template(sql.FieldEQ(FieldDescription, v)) +} + +// DescriptionNEQ applies the NEQ predicate on the "description" field. +func DescriptionNEQ(v string) predicate.Template { + return predicate.Template(sql.FieldNEQ(FieldDescription, v)) +} + +// DescriptionIn applies the In predicate on the "description" field. +func DescriptionIn(vs ...string) predicate.Template { + return predicate.Template(sql.FieldIn(FieldDescription, vs...)) +} + +// DescriptionNotIn applies the NotIn predicate on the "description" field. +func DescriptionNotIn(vs ...string) predicate.Template { + return predicate.Template(sql.FieldNotIn(FieldDescription, vs...)) +} + +// DescriptionGT applies the GT predicate on the "description" field. +func DescriptionGT(v string) predicate.Template { + return predicate.Template(sql.FieldGT(FieldDescription, v)) +} + +// DescriptionGTE applies the GTE predicate on the "description" field. +func DescriptionGTE(v string) predicate.Template { + return predicate.Template(sql.FieldGTE(FieldDescription, v)) +} + +// DescriptionLT applies the LT predicate on the "description" field. +func DescriptionLT(v string) predicate.Template { + return predicate.Template(sql.FieldLT(FieldDescription, v)) +} + +// DescriptionLTE applies the LTE predicate on the "description" field. +func DescriptionLTE(v string) predicate.Template { + return predicate.Template(sql.FieldLTE(FieldDescription, v)) +} + +// DescriptionContains applies the Contains predicate on the "description" field. +func DescriptionContains(v string) predicate.Template { + return predicate.Template(sql.FieldContains(FieldDescription, v)) +} + +// DescriptionHasPrefix applies the HasPrefix predicate on the "description" field. +func DescriptionHasPrefix(v string) predicate.Template { + return predicate.Template(sql.FieldHasPrefix(FieldDescription, v)) +} + +// DescriptionHasSuffix applies the HasSuffix predicate on the "description" field. +func DescriptionHasSuffix(v string) predicate.Template { + return predicate.Template(sql.FieldHasSuffix(FieldDescription, v)) +} + +// DescriptionIsNil applies the IsNil predicate on the "description" field. +func DescriptionIsNil() predicate.Template { + return predicate.Template(sql.FieldIsNull(FieldDescription)) +} + +// DescriptionNotNil applies the NotNil predicate on the "description" field. +func DescriptionNotNil() predicate.Template { + return predicate.Template(sql.FieldNotNull(FieldDescription)) +} + +// DescriptionEqualFold applies the EqualFold predicate on the "description" field. +func DescriptionEqualFold(v string) predicate.Template { + return predicate.Template(sql.FieldEqualFold(FieldDescription, v)) +} + +// DescriptionContainsFold applies the ContainsFold predicate on the "description" field. +func DescriptionContainsFold(v string) predicate.Template { + return predicate.Template(sql.FieldContainsFold(FieldDescription, v)) +} + +// HarnessEQ applies the EQ predicate on the "harness" field. +func HarnessEQ(v string) predicate.Template { + return predicate.Template(sql.FieldEQ(FieldHarness, v)) +} + +// HarnessNEQ applies the NEQ predicate on the "harness" field. +func HarnessNEQ(v string) predicate.Template { + return predicate.Template(sql.FieldNEQ(FieldHarness, v)) +} + +// HarnessIn applies the In predicate on the "harness" field. +func HarnessIn(vs ...string) predicate.Template { + return predicate.Template(sql.FieldIn(FieldHarness, vs...)) +} + +// HarnessNotIn applies the NotIn predicate on the "harness" field. +func HarnessNotIn(vs ...string) predicate.Template { + return predicate.Template(sql.FieldNotIn(FieldHarness, vs...)) +} + +// HarnessGT applies the GT predicate on the "harness" field. +func HarnessGT(v string) predicate.Template { + return predicate.Template(sql.FieldGT(FieldHarness, v)) +} + +// HarnessGTE applies the GTE predicate on the "harness" field. +func HarnessGTE(v string) predicate.Template { + return predicate.Template(sql.FieldGTE(FieldHarness, v)) +} + +// HarnessLT applies the LT predicate on the "harness" field. +func HarnessLT(v string) predicate.Template { + return predicate.Template(sql.FieldLT(FieldHarness, v)) +} + +// HarnessLTE applies the LTE predicate on the "harness" field. +func HarnessLTE(v string) predicate.Template { + return predicate.Template(sql.FieldLTE(FieldHarness, v)) +} + +// HarnessContains applies the Contains predicate on the "harness" field. +func HarnessContains(v string) predicate.Template { + return predicate.Template(sql.FieldContains(FieldHarness, v)) +} + +// HarnessHasPrefix applies the HasPrefix predicate on the "harness" field. +func HarnessHasPrefix(v string) predicate.Template { + return predicate.Template(sql.FieldHasPrefix(FieldHarness, v)) +} + +// HarnessHasSuffix applies the HasSuffix predicate on the "harness" field. +func HarnessHasSuffix(v string) predicate.Template { + return predicate.Template(sql.FieldHasSuffix(FieldHarness, v)) +} + +// HarnessEqualFold applies the EqualFold predicate on the "harness" field. +func HarnessEqualFold(v string) predicate.Template { + return predicate.Template(sql.FieldEqualFold(FieldHarness, v)) +} + +// HarnessContainsFold applies the ContainsFold predicate on the "harness" field. +func HarnessContainsFold(v string) predicate.Template { + return predicate.Template(sql.FieldContainsFold(FieldHarness, v)) +} + +// DefaultHarnessConfigEQ applies the EQ predicate on the "default_harness_config" field. +func DefaultHarnessConfigEQ(v string) predicate.Template { + return predicate.Template(sql.FieldEQ(FieldDefaultHarnessConfig, v)) +} + +// DefaultHarnessConfigNEQ applies the NEQ predicate on the "default_harness_config" field. +func DefaultHarnessConfigNEQ(v string) predicate.Template { + return predicate.Template(sql.FieldNEQ(FieldDefaultHarnessConfig, v)) +} + +// DefaultHarnessConfigIn applies the In predicate on the "default_harness_config" field. +func DefaultHarnessConfigIn(vs ...string) predicate.Template { + return predicate.Template(sql.FieldIn(FieldDefaultHarnessConfig, vs...)) +} + +// DefaultHarnessConfigNotIn applies the NotIn predicate on the "default_harness_config" field. +func DefaultHarnessConfigNotIn(vs ...string) predicate.Template { + return predicate.Template(sql.FieldNotIn(FieldDefaultHarnessConfig, vs...)) +} + +// DefaultHarnessConfigGT applies the GT predicate on the "default_harness_config" field. +func DefaultHarnessConfigGT(v string) predicate.Template { + return predicate.Template(sql.FieldGT(FieldDefaultHarnessConfig, v)) +} + +// DefaultHarnessConfigGTE applies the GTE predicate on the "default_harness_config" field. +func DefaultHarnessConfigGTE(v string) predicate.Template { + return predicate.Template(sql.FieldGTE(FieldDefaultHarnessConfig, v)) +} + +// DefaultHarnessConfigLT applies the LT predicate on the "default_harness_config" field. +func DefaultHarnessConfigLT(v string) predicate.Template { + return predicate.Template(sql.FieldLT(FieldDefaultHarnessConfig, v)) +} + +// DefaultHarnessConfigLTE applies the LTE predicate on the "default_harness_config" field. +func DefaultHarnessConfigLTE(v string) predicate.Template { + return predicate.Template(sql.FieldLTE(FieldDefaultHarnessConfig, v)) +} + +// DefaultHarnessConfigContains applies the Contains predicate on the "default_harness_config" field. +func DefaultHarnessConfigContains(v string) predicate.Template { + return predicate.Template(sql.FieldContains(FieldDefaultHarnessConfig, v)) +} + +// DefaultHarnessConfigHasPrefix applies the HasPrefix predicate on the "default_harness_config" field. +func DefaultHarnessConfigHasPrefix(v string) predicate.Template { + return predicate.Template(sql.FieldHasPrefix(FieldDefaultHarnessConfig, v)) +} + +// DefaultHarnessConfigHasSuffix applies the HasSuffix predicate on the "default_harness_config" field. +func DefaultHarnessConfigHasSuffix(v string) predicate.Template { + return predicate.Template(sql.FieldHasSuffix(FieldDefaultHarnessConfig, v)) +} + +// DefaultHarnessConfigIsNil applies the IsNil predicate on the "default_harness_config" field. +func DefaultHarnessConfigIsNil() predicate.Template { + return predicate.Template(sql.FieldIsNull(FieldDefaultHarnessConfig)) +} + +// DefaultHarnessConfigNotNil applies the NotNil predicate on the "default_harness_config" field. +func DefaultHarnessConfigNotNil() predicate.Template { + return predicate.Template(sql.FieldNotNull(FieldDefaultHarnessConfig)) +} + +// DefaultHarnessConfigEqualFold applies the EqualFold predicate on the "default_harness_config" field. +func DefaultHarnessConfigEqualFold(v string) predicate.Template { + return predicate.Template(sql.FieldEqualFold(FieldDefaultHarnessConfig, v)) +} + +// DefaultHarnessConfigContainsFold applies the ContainsFold predicate on the "default_harness_config" field. +func DefaultHarnessConfigContainsFold(v string) predicate.Template { + return predicate.Template(sql.FieldContainsFold(FieldDefaultHarnessConfig, v)) +} + +// ImageEQ applies the EQ predicate on the "image" field. +func ImageEQ(v string) predicate.Template { + return predicate.Template(sql.FieldEQ(FieldImage, v)) +} + +// ImageNEQ applies the NEQ predicate on the "image" field. +func ImageNEQ(v string) predicate.Template { + return predicate.Template(sql.FieldNEQ(FieldImage, v)) +} + +// ImageIn applies the In predicate on the "image" field. +func ImageIn(vs ...string) predicate.Template { + return predicate.Template(sql.FieldIn(FieldImage, vs...)) +} + +// ImageNotIn applies the NotIn predicate on the "image" field. +func ImageNotIn(vs ...string) predicate.Template { + return predicate.Template(sql.FieldNotIn(FieldImage, vs...)) +} + +// ImageGT applies the GT predicate on the "image" field. +func ImageGT(v string) predicate.Template { + return predicate.Template(sql.FieldGT(FieldImage, v)) +} + +// ImageGTE applies the GTE predicate on the "image" field. +func ImageGTE(v string) predicate.Template { + return predicate.Template(sql.FieldGTE(FieldImage, v)) +} + +// ImageLT applies the LT predicate on the "image" field. +func ImageLT(v string) predicate.Template { + return predicate.Template(sql.FieldLT(FieldImage, v)) +} + +// ImageLTE applies the LTE predicate on the "image" field. +func ImageLTE(v string) predicate.Template { + return predicate.Template(sql.FieldLTE(FieldImage, v)) +} + +// ImageContains applies the Contains predicate on the "image" field. +func ImageContains(v string) predicate.Template { + return predicate.Template(sql.FieldContains(FieldImage, v)) +} + +// ImageHasPrefix applies the HasPrefix predicate on the "image" field. +func ImageHasPrefix(v string) predicate.Template { + return predicate.Template(sql.FieldHasPrefix(FieldImage, v)) +} + +// ImageHasSuffix applies the HasSuffix predicate on the "image" field. +func ImageHasSuffix(v string) predicate.Template { + return predicate.Template(sql.FieldHasSuffix(FieldImage, v)) +} + +// ImageIsNil applies the IsNil predicate on the "image" field. +func ImageIsNil() predicate.Template { + return predicate.Template(sql.FieldIsNull(FieldImage)) +} + +// ImageNotNil applies the NotNil predicate on the "image" field. +func ImageNotNil() predicate.Template { + return predicate.Template(sql.FieldNotNull(FieldImage)) +} + +// ImageEqualFold applies the EqualFold predicate on the "image" field. +func ImageEqualFold(v string) predicate.Template { + return predicate.Template(sql.FieldEqualFold(FieldImage, v)) +} + +// ImageContainsFold applies the ContainsFold predicate on the "image" field. +func ImageContainsFold(v string) predicate.Template { + return predicate.Template(sql.FieldContainsFold(FieldImage, v)) +} + +// ConfigEQ applies the EQ predicate on the "config" field. +func ConfigEQ(v string) predicate.Template { + return predicate.Template(sql.FieldEQ(FieldConfig, v)) +} + +// ConfigNEQ applies the NEQ predicate on the "config" field. +func ConfigNEQ(v string) predicate.Template { + return predicate.Template(sql.FieldNEQ(FieldConfig, v)) +} + +// ConfigIn applies the In predicate on the "config" field. +func ConfigIn(vs ...string) predicate.Template { + return predicate.Template(sql.FieldIn(FieldConfig, vs...)) +} + +// ConfigNotIn applies the NotIn predicate on the "config" field. +func ConfigNotIn(vs ...string) predicate.Template { + return predicate.Template(sql.FieldNotIn(FieldConfig, vs...)) +} + +// ConfigGT applies the GT predicate on the "config" field. +func ConfigGT(v string) predicate.Template { + return predicate.Template(sql.FieldGT(FieldConfig, v)) +} + +// ConfigGTE applies the GTE predicate on the "config" field. +func ConfigGTE(v string) predicate.Template { + return predicate.Template(sql.FieldGTE(FieldConfig, v)) +} + +// ConfigLT applies the LT predicate on the "config" field. +func ConfigLT(v string) predicate.Template { + return predicate.Template(sql.FieldLT(FieldConfig, v)) +} + +// ConfigLTE applies the LTE predicate on the "config" field. +func ConfigLTE(v string) predicate.Template { + return predicate.Template(sql.FieldLTE(FieldConfig, v)) +} + +// ConfigContains applies the Contains predicate on the "config" field. +func ConfigContains(v string) predicate.Template { + return predicate.Template(sql.FieldContains(FieldConfig, v)) +} + +// ConfigHasPrefix applies the HasPrefix predicate on the "config" field. +func ConfigHasPrefix(v string) predicate.Template { + return predicate.Template(sql.FieldHasPrefix(FieldConfig, v)) +} + +// ConfigHasSuffix applies the HasSuffix predicate on the "config" field. +func ConfigHasSuffix(v string) predicate.Template { + return predicate.Template(sql.FieldHasSuffix(FieldConfig, v)) +} + +// ConfigIsNil applies the IsNil predicate on the "config" field. +func ConfigIsNil() predicate.Template { + return predicate.Template(sql.FieldIsNull(FieldConfig)) +} + +// ConfigNotNil applies the NotNil predicate on the "config" field. +func ConfigNotNil() predicate.Template { + return predicate.Template(sql.FieldNotNull(FieldConfig)) +} + +// ConfigEqualFold applies the EqualFold predicate on the "config" field. +func ConfigEqualFold(v string) predicate.Template { + return predicate.Template(sql.FieldEqualFold(FieldConfig, v)) +} + +// ConfigContainsFold applies the ContainsFold predicate on the "config" field. +func ConfigContainsFold(v string) predicate.Template { + return predicate.Template(sql.FieldContainsFold(FieldConfig, v)) +} + +// ContentHashEQ applies the EQ predicate on the "content_hash" field. +func ContentHashEQ(v string) predicate.Template { + return predicate.Template(sql.FieldEQ(FieldContentHash, v)) +} + +// ContentHashNEQ applies the NEQ predicate on the "content_hash" field. +func ContentHashNEQ(v string) predicate.Template { + return predicate.Template(sql.FieldNEQ(FieldContentHash, v)) +} + +// ContentHashIn applies the In predicate on the "content_hash" field. +func ContentHashIn(vs ...string) predicate.Template { + return predicate.Template(sql.FieldIn(FieldContentHash, vs...)) +} + +// ContentHashNotIn applies the NotIn predicate on the "content_hash" field. +func ContentHashNotIn(vs ...string) predicate.Template { + return predicate.Template(sql.FieldNotIn(FieldContentHash, vs...)) +} + +// ContentHashGT applies the GT predicate on the "content_hash" field. +func ContentHashGT(v string) predicate.Template { + return predicate.Template(sql.FieldGT(FieldContentHash, v)) +} + +// ContentHashGTE applies the GTE predicate on the "content_hash" field. +func ContentHashGTE(v string) predicate.Template { + return predicate.Template(sql.FieldGTE(FieldContentHash, v)) +} + +// ContentHashLT applies the LT predicate on the "content_hash" field. +func ContentHashLT(v string) predicate.Template { + return predicate.Template(sql.FieldLT(FieldContentHash, v)) +} + +// ContentHashLTE applies the LTE predicate on the "content_hash" field. +func ContentHashLTE(v string) predicate.Template { + return predicate.Template(sql.FieldLTE(FieldContentHash, v)) +} + +// ContentHashContains applies the Contains predicate on the "content_hash" field. +func ContentHashContains(v string) predicate.Template { + return predicate.Template(sql.FieldContains(FieldContentHash, v)) +} + +// ContentHashHasPrefix applies the HasPrefix predicate on the "content_hash" field. +func ContentHashHasPrefix(v string) predicate.Template { + return predicate.Template(sql.FieldHasPrefix(FieldContentHash, v)) +} + +// ContentHashHasSuffix applies the HasSuffix predicate on the "content_hash" field. +func ContentHashHasSuffix(v string) predicate.Template { + return predicate.Template(sql.FieldHasSuffix(FieldContentHash, v)) +} + +// ContentHashIsNil applies the IsNil predicate on the "content_hash" field. +func ContentHashIsNil() predicate.Template { + return predicate.Template(sql.FieldIsNull(FieldContentHash)) +} + +// ContentHashNotNil applies the NotNil predicate on the "content_hash" field. +func ContentHashNotNil() predicate.Template { + return predicate.Template(sql.FieldNotNull(FieldContentHash)) +} + +// ContentHashEqualFold applies the EqualFold predicate on the "content_hash" field. +func ContentHashEqualFold(v string) predicate.Template { + return predicate.Template(sql.FieldEqualFold(FieldContentHash, v)) +} + +// ContentHashContainsFold applies the ContainsFold predicate on the "content_hash" field. +func ContentHashContainsFold(v string) predicate.Template { + return predicate.Template(sql.FieldContainsFold(FieldContentHash, v)) +} + +// ScopeEQ applies the EQ predicate on the "scope" field. +func ScopeEQ(v string) predicate.Template { + return predicate.Template(sql.FieldEQ(FieldScope, v)) +} + +// ScopeNEQ applies the NEQ predicate on the "scope" field. +func ScopeNEQ(v string) predicate.Template { + return predicate.Template(sql.FieldNEQ(FieldScope, v)) +} + +// ScopeIn applies the In predicate on the "scope" field. +func ScopeIn(vs ...string) predicate.Template { + return predicate.Template(sql.FieldIn(FieldScope, vs...)) +} + +// ScopeNotIn applies the NotIn predicate on the "scope" field. +func ScopeNotIn(vs ...string) predicate.Template { + return predicate.Template(sql.FieldNotIn(FieldScope, vs...)) +} + +// ScopeGT applies the GT predicate on the "scope" field. +func ScopeGT(v string) predicate.Template { + return predicate.Template(sql.FieldGT(FieldScope, v)) +} + +// ScopeGTE applies the GTE predicate on the "scope" field. +func ScopeGTE(v string) predicate.Template { + return predicate.Template(sql.FieldGTE(FieldScope, v)) +} + +// ScopeLT applies the LT predicate on the "scope" field. +func ScopeLT(v string) predicate.Template { + return predicate.Template(sql.FieldLT(FieldScope, v)) +} + +// ScopeLTE applies the LTE predicate on the "scope" field. +func ScopeLTE(v string) predicate.Template { + return predicate.Template(sql.FieldLTE(FieldScope, v)) +} + +// ScopeContains applies the Contains predicate on the "scope" field. +func ScopeContains(v string) predicate.Template { + return predicate.Template(sql.FieldContains(FieldScope, v)) +} + +// ScopeHasPrefix applies the HasPrefix predicate on the "scope" field. +func ScopeHasPrefix(v string) predicate.Template { + return predicate.Template(sql.FieldHasPrefix(FieldScope, v)) +} + +// ScopeHasSuffix applies the HasSuffix predicate on the "scope" field. +func ScopeHasSuffix(v string) predicate.Template { + return predicate.Template(sql.FieldHasSuffix(FieldScope, v)) +} + +// ScopeEqualFold applies the EqualFold predicate on the "scope" field. +func ScopeEqualFold(v string) predicate.Template { + return predicate.Template(sql.FieldEqualFold(FieldScope, v)) +} + +// ScopeContainsFold applies the ContainsFold predicate on the "scope" field. +func ScopeContainsFold(v string) predicate.Template { + return predicate.Template(sql.FieldContainsFold(FieldScope, v)) +} + +// ScopeIDEQ applies the EQ predicate on the "scope_id" field. +func ScopeIDEQ(v string) predicate.Template { + return predicate.Template(sql.FieldEQ(FieldScopeID, v)) +} + +// ScopeIDNEQ applies the NEQ predicate on the "scope_id" field. +func ScopeIDNEQ(v string) predicate.Template { + return predicate.Template(sql.FieldNEQ(FieldScopeID, v)) +} + +// ScopeIDIn applies the In predicate on the "scope_id" field. +func ScopeIDIn(vs ...string) predicate.Template { + return predicate.Template(sql.FieldIn(FieldScopeID, vs...)) +} + +// ScopeIDNotIn applies the NotIn predicate on the "scope_id" field. +func ScopeIDNotIn(vs ...string) predicate.Template { + return predicate.Template(sql.FieldNotIn(FieldScopeID, vs...)) +} + +// ScopeIDGT applies the GT predicate on the "scope_id" field. +func ScopeIDGT(v string) predicate.Template { + return predicate.Template(sql.FieldGT(FieldScopeID, v)) +} + +// ScopeIDGTE applies the GTE predicate on the "scope_id" field. +func ScopeIDGTE(v string) predicate.Template { + return predicate.Template(sql.FieldGTE(FieldScopeID, v)) +} + +// ScopeIDLT applies the LT predicate on the "scope_id" field. +func ScopeIDLT(v string) predicate.Template { + return predicate.Template(sql.FieldLT(FieldScopeID, v)) +} + +// ScopeIDLTE applies the LTE predicate on the "scope_id" field. +func ScopeIDLTE(v string) predicate.Template { + return predicate.Template(sql.FieldLTE(FieldScopeID, v)) +} + +// ScopeIDContains applies the Contains predicate on the "scope_id" field. +func ScopeIDContains(v string) predicate.Template { + return predicate.Template(sql.FieldContains(FieldScopeID, v)) +} + +// ScopeIDHasPrefix applies the HasPrefix predicate on the "scope_id" field. +func ScopeIDHasPrefix(v string) predicate.Template { + return predicate.Template(sql.FieldHasPrefix(FieldScopeID, v)) +} + +// ScopeIDHasSuffix applies the HasSuffix predicate on the "scope_id" field. +func ScopeIDHasSuffix(v string) predicate.Template { + return predicate.Template(sql.FieldHasSuffix(FieldScopeID, v)) +} + +// ScopeIDIsNil applies the IsNil predicate on the "scope_id" field. +func ScopeIDIsNil() predicate.Template { + return predicate.Template(sql.FieldIsNull(FieldScopeID)) +} + +// ScopeIDNotNil applies the NotNil predicate on the "scope_id" field. +func ScopeIDNotNil() predicate.Template { + return predicate.Template(sql.FieldNotNull(FieldScopeID)) +} + +// ScopeIDEqualFold applies the EqualFold predicate on the "scope_id" field. +func ScopeIDEqualFold(v string) predicate.Template { + return predicate.Template(sql.FieldEqualFold(FieldScopeID, v)) +} + +// ScopeIDContainsFold applies the ContainsFold predicate on the "scope_id" field. +func ScopeIDContainsFold(v string) predicate.Template { + return predicate.Template(sql.FieldContainsFold(FieldScopeID, v)) +} + +// ProjectIDEQ applies the EQ predicate on the "project_id" field. +func ProjectIDEQ(v string) predicate.Template { + return predicate.Template(sql.FieldEQ(FieldProjectID, v)) +} + +// ProjectIDNEQ applies the NEQ predicate on the "project_id" field. +func ProjectIDNEQ(v string) predicate.Template { + return predicate.Template(sql.FieldNEQ(FieldProjectID, v)) +} + +// ProjectIDIn applies the In predicate on the "project_id" field. +func ProjectIDIn(vs ...string) predicate.Template { + return predicate.Template(sql.FieldIn(FieldProjectID, vs...)) +} + +// ProjectIDNotIn applies the NotIn predicate on the "project_id" field. +func ProjectIDNotIn(vs ...string) predicate.Template { + return predicate.Template(sql.FieldNotIn(FieldProjectID, vs...)) +} + +// ProjectIDGT applies the GT predicate on the "project_id" field. +func ProjectIDGT(v string) predicate.Template { + return predicate.Template(sql.FieldGT(FieldProjectID, v)) +} + +// ProjectIDGTE applies the GTE predicate on the "project_id" field. +func ProjectIDGTE(v string) predicate.Template { + return predicate.Template(sql.FieldGTE(FieldProjectID, v)) +} + +// ProjectIDLT applies the LT predicate on the "project_id" field. +func ProjectIDLT(v string) predicate.Template { + return predicate.Template(sql.FieldLT(FieldProjectID, v)) +} + +// ProjectIDLTE applies the LTE predicate on the "project_id" field. +func ProjectIDLTE(v string) predicate.Template { + return predicate.Template(sql.FieldLTE(FieldProjectID, v)) +} + +// ProjectIDContains applies the Contains predicate on the "project_id" field. +func ProjectIDContains(v string) predicate.Template { + return predicate.Template(sql.FieldContains(FieldProjectID, v)) +} + +// ProjectIDHasPrefix applies the HasPrefix predicate on the "project_id" field. +func ProjectIDHasPrefix(v string) predicate.Template { + return predicate.Template(sql.FieldHasPrefix(FieldProjectID, v)) +} + +// ProjectIDHasSuffix applies the HasSuffix predicate on the "project_id" field. +func ProjectIDHasSuffix(v string) predicate.Template { + return predicate.Template(sql.FieldHasSuffix(FieldProjectID, v)) +} + +// ProjectIDIsNil applies the IsNil predicate on the "project_id" field. +func ProjectIDIsNil() predicate.Template { + return predicate.Template(sql.FieldIsNull(FieldProjectID)) +} + +// ProjectIDNotNil applies the NotNil predicate on the "project_id" field. +func ProjectIDNotNil() predicate.Template { + return predicate.Template(sql.FieldNotNull(FieldProjectID)) +} + +// ProjectIDEqualFold applies the EqualFold predicate on the "project_id" field. +func ProjectIDEqualFold(v string) predicate.Template { + return predicate.Template(sql.FieldEqualFold(FieldProjectID, v)) +} + +// ProjectIDContainsFold applies the ContainsFold predicate on the "project_id" field. +func ProjectIDContainsFold(v string) predicate.Template { + return predicate.Template(sql.FieldContainsFold(FieldProjectID, v)) +} + +// StorageURIEQ applies the EQ predicate on the "storage_uri" field. +func StorageURIEQ(v string) predicate.Template { + return predicate.Template(sql.FieldEQ(FieldStorageURI, v)) +} + +// StorageURINEQ applies the NEQ predicate on the "storage_uri" field. +func StorageURINEQ(v string) predicate.Template { + return predicate.Template(sql.FieldNEQ(FieldStorageURI, v)) +} + +// StorageURIIn applies the In predicate on the "storage_uri" field. +func StorageURIIn(vs ...string) predicate.Template { + return predicate.Template(sql.FieldIn(FieldStorageURI, vs...)) +} + +// StorageURINotIn applies the NotIn predicate on the "storage_uri" field. +func StorageURINotIn(vs ...string) predicate.Template { + return predicate.Template(sql.FieldNotIn(FieldStorageURI, vs...)) +} + +// StorageURIGT applies the GT predicate on the "storage_uri" field. +func StorageURIGT(v string) predicate.Template { + return predicate.Template(sql.FieldGT(FieldStorageURI, v)) +} + +// StorageURIGTE applies the GTE predicate on the "storage_uri" field. +func StorageURIGTE(v string) predicate.Template { + return predicate.Template(sql.FieldGTE(FieldStorageURI, v)) +} + +// StorageURILT applies the LT predicate on the "storage_uri" field. +func StorageURILT(v string) predicate.Template { + return predicate.Template(sql.FieldLT(FieldStorageURI, v)) +} + +// StorageURILTE applies the LTE predicate on the "storage_uri" field. +func StorageURILTE(v string) predicate.Template { + return predicate.Template(sql.FieldLTE(FieldStorageURI, v)) +} + +// StorageURIContains applies the Contains predicate on the "storage_uri" field. +func StorageURIContains(v string) predicate.Template { + return predicate.Template(sql.FieldContains(FieldStorageURI, v)) +} + +// StorageURIHasPrefix applies the HasPrefix predicate on the "storage_uri" field. +func StorageURIHasPrefix(v string) predicate.Template { + return predicate.Template(sql.FieldHasPrefix(FieldStorageURI, v)) +} + +// StorageURIHasSuffix applies the HasSuffix predicate on the "storage_uri" field. +func StorageURIHasSuffix(v string) predicate.Template { + return predicate.Template(sql.FieldHasSuffix(FieldStorageURI, v)) +} + +// StorageURIIsNil applies the IsNil predicate on the "storage_uri" field. +func StorageURIIsNil() predicate.Template { + return predicate.Template(sql.FieldIsNull(FieldStorageURI)) +} + +// StorageURINotNil applies the NotNil predicate on the "storage_uri" field. +func StorageURINotNil() predicate.Template { + return predicate.Template(sql.FieldNotNull(FieldStorageURI)) +} + +// StorageURIEqualFold applies the EqualFold predicate on the "storage_uri" field. +func StorageURIEqualFold(v string) predicate.Template { + return predicate.Template(sql.FieldEqualFold(FieldStorageURI, v)) +} + +// StorageURIContainsFold applies the ContainsFold predicate on the "storage_uri" field. +func StorageURIContainsFold(v string) predicate.Template { + return predicate.Template(sql.FieldContainsFold(FieldStorageURI, v)) +} + +// StorageBucketEQ applies the EQ predicate on the "storage_bucket" field. +func StorageBucketEQ(v string) predicate.Template { + return predicate.Template(sql.FieldEQ(FieldStorageBucket, v)) +} + +// StorageBucketNEQ applies the NEQ predicate on the "storage_bucket" field. +func StorageBucketNEQ(v string) predicate.Template { + return predicate.Template(sql.FieldNEQ(FieldStorageBucket, v)) +} + +// StorageBucketIn applies the In predicate on the "storage_bucket" field. +func StorageBucketIn(vs ...string) predicate.Template { + return predicate.Template(sql.FieldIn(FieldStorageBucket, vs...)) +} + +// StorageBucketNotIn applies the NotIn predicate on the "storage_bucket" field. +func StorageBucketNotIn(vs ...string) predicate.Template { + return predicate.Template(sql.FieldNotIn(FieldStorageBucket, vs...)) +} + +// StorageBucketGT applies the GT predicate on the "storage_bucket" field. +func StorageBucketGT(v string) predicate.Template { + return predicate.Template(sql.FieldGT(FieldStorageBucket, v)) +} + +// StorageBucketGTE applies the GTE predicate on the "storage_bucket" field. +func StorageBucketGTE(v string) predicate.Template { + return predicate.Template(sql.FieldGTE(FieldStorageBucket, v)) +} + +// StorageBucketLT applies the LT predicate on the "storage_bucket" field. +func StorageBucketLT(v string) predicate.Template { + return predicate.Template(sql.FieldLT(FieldStorageBucket, v)) +} + +// StorageBucketLTE applies the LTE predicate on the "storage_bucket" field. +func StorageBucketLTE(v string) predicate.Template { + return predicate.Template(sql.FieldLTE(FieldStorageBucket, v)) +} + +// StorageBucketContains applies the Contains predicate on the "storage_bucket" field. +func StorageBucketContains(v string) predicate.Template { + return predicate.Template(sql.FieldContains(FieldStorageBucket, v)) +} + +// StorageBucketHasPrefix applies the HasPrefix predicate on the "storage_bucket" field. +func StorageBucketHasPrefix(v string) predicate.Template { + return predicate.Template(sql.FieldHasPrefix(FieldStorageBucket, v)) +} + +// StorageBucketHasSuffix applies the HasSuffix predicate on the "storage_bucket" field. +func StorageBucketHasSuffix(v string) predicate.Template { + return predicate.Template(sql.FieldHasSuffix(FieldStorageBucket, v)) +} + +// StorageBucketIsNil applies the IsNil predicate on the "storage_bucket" field. +func StorageBucketIsNil() predicate.Template { + return predicate.Template(sql.FieldIsNull(FieldStorageBucket)) +} + +// StorageBucketNotNil applies the NotNil predicate on the "storage_bucket" field. +func StorageBucketNotNil() predicate.Template { + return predicate.Template(sql.FieldNotNull(FieldStorageBucket)) +} + +// StorageBucketEqualFold applies the EqualFold predicate on the "storage_bucket" field. +func StorageBucketEqualFold(v string) predicate.Template { + return predicate.Template(sql.FieldEqualFold(FieldStorageBucket, v)) +} + +// StorageBucketContainsFold applies the ContainsFold predicate on the "storage_bucket" field. +func StorageBucketContainsFold(v string) predicate.Template { + return predicate.Template(sql.FieldContainsFold(FieldStorageBucket, v)) +} + +// StoragePathEQ applies the EQ predicate on the "storage_path" field. +func StoragePathEQ(v string) predicate.Template { + return predicate.Template(sql.FieldEQ(FieldStoragePath, v)) +} + +// StoragePathNEQ applies the NEQ predicate on the "storage_path" field. +func StoragePathNEQ(v string) predicate.Template { + return predicate.Template(sql.FieldNEQ(FieldStoragePath, v)) +} + +// StoragePathIn applies the In predicate on the "storage_path" field. +func StoragePathIn(vs ...string) predicate.Template { + return predicate.Template(sql.FieldIn(FieldStoragePath, vs...)) +} + +// StoragePathNotIn applies the NotIn predicate on the "storage_path" field. +func StoragePathNotIn(vs ...string) predicate.Template { + return predicate.Template(sql.FieldNotIn(FieldStoragePath, vs...)) +} + +// StoragePathGT applies the GT predicate on the "storage_path" field. +func StoragePathGT(v string) predicate.Template { + return predicate.Template(sql.FieldGT(FieldStoragePath, v)) +} + +// StoragePathGTE applies the GTE predicate on the "storage_path" field. +func StoragePathGTE(v string) predicate.Template { + return predicate.Template(sql.FieldGTE(FieldStoragePath, v)) +} + +// StoragePathLT applies the LT predicate on the "storage_path" field. +func StoragePathLT(v string) predicate.Template { + return predicate.Template(sql.FieldLT(FieldStoragePath, v)) +} + +// StoragePathLTE applies the LTE predicate on the "storage_path" field. +func StoragePathLTE(v string) predicate.Template { + return predicate.Template(sql.FieldLTE(FieldStoragePath, v)) +} + +// StoragePathContains applies the Contains predicate on the "storage_path" field. +func StoragePathContains(v string) predicate.Template { + return predicate.Template(sql.FieldContains(FieldStoragePath, v)) +} + +// StoragePathHasPrefix applies the HasPrefix predicate on the "storage_path" field. +func StoragePathHasPrefix(v string) predicate.Template { + return predicate.Template(sql.FieldHasPrefix(FieldStoragePath, v)) +} + +// StoragePathHasSuffix applies the HasSuffix predicate on the "storage_path" field. +func StoragePathHasSuffix(v string) predicate.Template { + return predicate.Template(sql.FieldHasSuffix(FieldStoragePath, v)) +} + +// StoragePathIsNil applies the IsNil predicate on the "storage_path" field. +func StoragePathIsNil() predicate.Template { + return predicate.Template(sql.FieldIsNull(FieldStoragePath)) +} + +// StoragePathNotNil applies the NotNil predicate on the "storage_path" field. +func StoragePathNotNil() predicate.Template { + return predicate.Template(sql.FieldNotNull(FieldStoragePath)) +} + +// StoragePathEqualFold applies the EqualFold predicate on the "storage_path" field. +func StoragePathEqualFold(v string) predicate.Template { + return predicate.Template(sql.FieldEqualFold(FieldStoragePath, v)) +} + +// StoragePathContainsFold applies the ContainsFold predicate on the "storage_path" field. +func StoragePathContainsFold(v string) predicate.Template { + return predicate.Template(sql.FieldContainsFold(FieldStoragePath, v)) +} + +// FilesEQ applies the EQ predicate on the "files" field. +func FilesEQ(v string) predicate.Template { + return predicate.Template(sql.FieldEQ(FieldFiles, v)) +} + +// FilesNEQ applies the NEQ predicate on the "files" field. +func FilesNEQ(v string) predicate.Template { + return predicate.Template(sql.FieldNEQ(FieldFiles, v)) +} + +// FilesIn applies the In predicate on the "files" field. +func FilesIn(vs ...string) predicate.Template { + return predicate.Template(sql.FieldIn(FieldFiles, vs...)) +} + +// FilesNotIn applies the NotIn predicate on the "files" field. +func FilesNotIn(vs ...string) predicate.Template { + return predicate.Template(sql.FieldNotIn(FieldFiles, vs...)) +} + +// FilesGT applies the GT predicate on the "files" field. +func FilesGT(v string) predicate.Template { + return predicate.Template(sql.FieldGT(FieldFiles, v)) +} + +// FilesGTE applies the GTE predicate on the "files" field. +func FilesGTE(v string) predicate.Template { + return predicate.Template(sql.FieldGTE(FieldFiles, v)) +} + +// FilesLT applies the LT predicate on the "files" field. +func FilesLT(v string) predicate.Template { + return predicate.Template(sql.FieldLT(FieldFiles, v)) +} + +// FilesLTE applies the LTE predicate on the "files" field. +func FilesLTE(v string) predicate.Template { + return predicate.Template(sql.FieldLTE(FieldFiles, v)) +} + +// FilesContains applies the Contains predicate on the "files" field. +func FilesContains(v string) predicate.Template { + return predicate.Template(sql.FieldContains(FieldFiles, v)) +} + +// FilesHasPrefix applies the HasPrefix predicate on the "files" field. +func FilesHasPrefix(v string) predicate.Template { + return predicate.Template(sql.FieldHasPrefix(FieldFiles, v)) +} + +// FilesHasSuffix applies the HasSuffix predicate on the "files" field. +func FilesHasSuffix(v string) predicate.Template { + return predicate.Template(sql.FieldHasSuffix(FieldFiles, v)) +} + +// FilesIsNil applies the IsNil predicate on the "files" field. +func FilesIsNil() predicate.Template { + return predicate.Template(sql.FieldIsNull(FieldFiles)) +} + +// FilesNotNil applies the NotNil predicate on the "files" field. +func FilesNotNil() predicate.Template { + return predicate.Template(sql.FieldNotNull(FieldFiles)) +} + +// FilesEqualFold applies the EqualFold predicate on the "files" field. +func FilesEqualFold(v string) predicate.Template { + return predicate.Template(sql.FieldEqualFold(FieldFiles, v)) +} + +// FilesContainsFold applies the ContainsFold predicate on the "files" field. +func FilesContainsFold(v string) predicate.Template { + return predicate.Template(sql.FieldContainsFold(FieldFiles, v)) +} + +// BaseTemplateEQ applies the EQ predicate on the "base_template" field. +func BaseTemplateEQ(v string) predicate.Template { + return predicate.Template(sql.FieldEQ(FieldBaseTemplate, v)) +} + +// BaseTemplateNEQ applies the NEQ predicate on the "base_template" field. +func BaseTemplateNEQ(v string) predicate.Template { + return predicate.Template(sql.FieldNEQ(FieldBaseTemplate, v)) +} + +// BaseTemplateIn applies the In predicate on the "base_template" field. +func BaseTemplateIn(vs ...string) predicate.Template { + return predicate.Template(sql.FieldIn(FieldBaseTemplate, vs...)) +} + +// BaseTemplateNotIn applies the NotIn predicate on the "base_template" field. +func BaseTemplateNotIn(vs ...string) predicate.Template { + return predicate.Template(sql.FieldNotIn(FieldBaseTemplate, vs...)) +} + +// BaseTemplateGT applies the GT predicate on the "base_template" field. +func BaseTemplateGT(v string) predicate.Template { + return predicate.Template(sql.FieldGT(FieldBaseTemplate, v)) +} + +// BaseTemplateGTE applies the GTE predicate on the "base_template" field. +func BaseTemplateGTE(v string) predicate.Template { + return predicate.Template(sql.FieldGTE(FieldBaseTemplate, v)) +} + +// BaseTemplateLT applies the LT predicate on the "base_template" field. +func BaseTemplateLT(v string) predicate.Template { + return predicate.Template(sql.FieldLT(FieldBaseTemplate, v)) +} + +// BaseTemplateLTE applies the LTE predicate on the "base_template" field. +func BaseTemplateLTE(v string) predicate.Template { + return predicate.Template(sql.FieldLTE(FieldBaseTemplate, v)) +} + +// BaseTemplateContains applies the Contains predicate on the "base_template" field. +func BaseTemplateContains(v string) predicate.Template { + return predicate.Template(sql.FieldContains(FieldBaseTemplate, v)) +} + +// BaseTemplateHasPrefix applies the HasPrefix predicate on the "base_template" field. +func BaseTemplateHasPrefix(v string) predicate.Template { + return predicate.Template(sql.FieldHasPrefix(FieldBaseTemplate, v)) +} + +// BaseTemplateHasSuffix applies the HasSuffix predicate on the "base_template" field. +func BaseTemplateHasSuffix(v string) predicate.Template { + return predicate.Template(sql.FieldHasSuffix(FieldBaseTemplate, v)) +} + +// BaseTemplateIsNil applies the IsNil predicate on the "base_template" field. +func BaseTemplateIsNil() predicate.Template { + return predicate.Template(sql.FieldIsNull(FieldBaseTemplate)) +} + +// BaseTemplateNotNil applies the NotNil predicate on the "base_template" field. +func BaseTemplateNotNil() predicate.Template { + return predicate.Template(sql.FieldNotNull(FieldBaseTemplate)) +} + +// BaseTemplateEqualFold applies the EqualFold predicate on the "base_template" field. +func BaseTemplateEqualFold(v string) predicate.Template { + return predicate.Template(sql.FieldEqualFold(FieldBaseTemplate, v)) +} + +// BaseTemplateContainsFold applies the ContainsFold predicate on the "base_template" field. +func BaseTemplateContainsFold(v string) predicate.Template { + return predicate.Template(sql.FieldContainsFold(FieldBaseTemplate, v)) +} + +// StatusEQ applies the EQ predicate on the "status" field. +func StatusEQ(v Status) predicate.Template { + return predicate.Template(sql.FieldEQ(FieldStatus, v)) +} + +// StatusNEQ applies the NEQ predicate on the "status" field. +func StatusNEQ(v Status) predicate.Template { + return predicate.Template(sql.FieldNEQ(FieldStatus, v)) +} + +// StatusIn applies the In predicate on the "status" field. +func StatusIn(vs ...Status) predicate.Template { + return predicate.Template(sql.FieldIn(FieldStatus, vs...)) +} + +// StatusNotIn applies the NotIn predicate on the "status" field. +func StatusNotIn(vs ...Status) predicate.Template { + return predicate.Template(sql.FieldNotIn(FieldStatus, vs...)) +} + +// OwnerIDEQ applies the EQ predicate on the "owner_id" field. +func OwnerIDEQ(v string) predicate.Template { + return predicate.Template(sql.FieldEQ(FieldOwnerID, v)) +} + +// OwnerIDNEQ applies the NEQ predicate on the "owner_id" field. +func OwnerIDNEQ(v string) predicate.Template { + return predicate.Template(sql.FieldNEQ(FieldOwnerID, v)) +} + +// OwnerIDIn applies the In predicate on the "owner_id" field. +func OwnerIDIn(vs ...string) predicate.Template { + return predicate.Template(sql.FieldIn(FieldOwnerID, vs...)) +} + +// OwnerIDNotIn applies the NotIn predicate on the "owner_id" field. +func OwnerIDNotIn(vs ...string) predicate.Template { + return predicate.Template(sql.FieldNotIn(FieldOwnerID, vs...)) +} + +// OwnerIDGT applies the GT predicate on the "owner_id" field. +func OwnerIDGT(v string) predicate.Template { + return predicate.Template(sql.FieldGT(FieldOwnerID, v)) +} + +// OwnerIDGTE applies the GTE predicate on the "owner_id" field. +func OwnerIDGTE(v string) predicate.Template { + return predicate.Template(sql.FieldGTE(FieldOwnerID, v)) +} + +// OwnerIDLT applies the LT predicate on the "owner_id" field. +func OwnerIDLT(v string) predicate.Template { + return predicate.Template(sql.FieldLT(FieldOwnerID, v)) +} + +// OwnerIDLTE applies the LTE predicate on the "owner_id" field. +func OwnerIDLTE(v string) predicate.Template { + return predicate.Template(sql.FieldLTE(FieldOwnerID, v)) +} + +// OwnerIDContains applies the Contains predicate on the "owner_id" field. +func OwnerIDContains(v string) predicate.Template { + return predicate.Template(sql.FieldContains(FieldOwnerID, v)) +} + +// OwnerIDHasPrefix applies the HasPrefix predicate on the "owner_id" field. +func OwnerIDHasPrefix(v string) predicate.Template { + return predicate.Template(sql.FieldHasPrefix(FieldOwnerID, v)) +} + +// OwnerIDHasSuffix applies the HasSuffix predicate on the "owner_id" field. +func OwnerIDHasSuffix(v string) predicate.Template { + return predicate.Template(sql.FieldHasSuffix(FieldOwnerID, v)) +} + +// OwnerIDIsNil applies the IsNil predicate on the "owner_id" field. +func OwnerIDIsNil() predicate.Template { + return predicate.Template(sql.FieldIsNull(FieldOwnerID)) +} + +// OwnerIDNotNil applies the NotNil predicate on the "owner_id" field. +func OwnerIDNotNil() predicate.Template { + return predicate.Template(sql.FieldNotNull(FieldOwnerID)) +} + +// OwnerIDEqualFold applies the EqualFold predicate on the "owner_id" field. +func OwnerIDEqualFold(v string) predicate.Template { + return predicate.Template(sql.FieldEqualFold(FieldOwnerID, v)) +} + +// OwnerIDContainsFold applies the ContainsFold predicate on the "owner_id" field. +func OwnerIDContainsFold(v string) predicate.Template { + return predicate.Template(sql.FieldContainsFold(FieldOwnerID, v)) +} + +// CreatedByEQ applies the EQ predicate on the "created_by" field. +func CreatedByEQ(v string) predicate.Template { + return predicate.Template(sql.FieldEQ(FieldCreatedBy, v)) +} + +// CreatedByNEQ applies the NEQ predicate on the "created_by" field. +func CreatedByNEQ(v string) predicate.Template { + return predicate.Template(sql.FieldNEQ(FieldCreatedBy, v)) +} + +// CreatedByIn applies the In predicate on the "created_by" field. +func CreatedByIn(vs ...string) predicate.Template { + return predicate.Template(sql.FieldIn(FieldCreatedBy, vs...)) +} + +// CreatedByNotIn applies the NotIn predicate on the "created_by" field. +func CreatedByNotIn(vs ...string) predicate.Template { + return predicate.Template(sql.FieldNotIn(FieldCreatedBy, vs...)) +} + +// CreatedByGT applies the GT predicate on the "created_by" field. +func CreatedByGT(v string) predicate.Template { + return predicate.Template(sql.FieldGT(FieldCreatedBy, v)) +} + +// CreatedByGTE applies the GTE predicate on the "created_by" field. +func CreatedByGTE(v string) predicate.Template { + return predicate.Template(sql.FieldGTE(FieldCreatedBy, v)) +} + +// CreatedByLT applies the LT predicate on the "created_by" field. +func CreatedByLT(v string) predicate.Template { + return predicate.Template(sql.FieldLT(FieldCreatedBy, v)) +} + +// CreatedByLTE applies the LTE predicate on the "created_by" field. +func CreatedByLTE(v string) predicate.Template { + return predicate.Template(sql.FieldLTE(FieldCreatedBy, v)) +} + +// CreatedByContains applies the Contains predicate on the "created_by" field. +func CreatedByContains(v string) predicate.Template { + return predicate.Template(sql.FieldContains(FieldCreatedBy, v)) +} + +// CreatedByHasPrefix applies the HasPrefix predicate on the "created_by" field. +func CreatedByHasPrefix(v string) predicate.Template { + return predicate.Template(sql.FieldHasPrefix(FieldCreatedBy, v)) +} + +// CreatedByHasSuffix applies the HasSuffix predicate on the "created_by" field. +func CreatedByHasSuffix(v string) predicate.Template { + return predicate.Template(sql.FieldHasSuffix(FieldCreatedBy, v)) +} + +// CreatedByIsNil applies the IsNil predicate on the "created_by" field. +func CreatedByIsNil() predicate.Template { + return predicate.Template(sql.FieldIsNull(FieldCreatedBy)) +} + +// CreatedByNotNil applies the NotNil predicate on the "created_by" field. +func CreatedByNotNil() predicate.Template { + return predicate.Template(sql.FieldNotNull(FieldCreatedBy)) +} + +// CreatedByEqualFold applies the EqualFold predicate on the "created_by" field. +func CreatedByEqualFold(v string) predicate.Template { + return predicate.Template(sql.FieldEqualFold(FieldCreatedBy, v)) +} + +// CreatedByContainsFold applies the ContainsFold predicate on the "created_by" field. +func CreatedByContainsFold(v string) predicate.Template { + return predicate.Template(sql.FieldContainsFold(FieldCreatedBy, v)) +} + +// UpdatedByEQ applies the EQ predicate on the "updated_by" field. +func UpdatedByEQ(v string) predicate.Template { + return predicate.Template(sql.FieldEQ(FieldUpdatedBy, v)) +} + +// UpdatedByNEQ applies the NEQ predicate on the "updated_by" field. +func UpdatedByNEQ(v string) predicate.Template { + return predicate.Template(sql.FieldNEQ(FieldUpdatedBy, v)) +} + +// UpdatedByIn applies the In predicate on the "updated_by" field. +func UpdatedByIn(vs ...string) predicate.Template { + return predicate.Template(sql.FieldIn(FieldUpdatedBy, vs...)) +} + +// UpdatedByNotIn applies the NotIn predicate on the "updated_by" field. +func UpdatedByNotIn(vs ...string) predicate.Template { + return predicate.Template(sql.FieldNotIn(FieldUpdatedBy, vs...)) +} + +// UpdatedByGT applies the GT predicate on the "updated_by" field. +func UpdatedByGT(v string) predicate.Template { + return predicate.Template(sql.FieldGT(FieldUpdatedBy, v)) +} + +// UpdatedByGTE applies the GTE predicate on the "updated_by" field. +func UpdatedByGTE(v string) predicate.Template { + return predicate.Template(sql.FieldGTE(FieldUpdatedBy, v)) +} + +// UpdatedByLT applies the LT predicate on the "updated_by" field. +func UpdatedByLT(v string) predicate.Template { + return predicate.Template(sql.FieldLT(FieldUpdatedBy, v)) +} + +// UpdatedByLTE applies the LTE predicate on the "updated_by" field. +func UpdatedByLTE(v string) predicate.Template { + return predicate.Template(sql.FieldLTE(FieldUpdatedBy, v)) +} + +// UpdatedByContains applies the Contains predicate on the "updated_by" field. +func UpdatedByContains(v string) predicate.Template { + return predicate.Template(sql.FieldContains(FieldUpdatedBy, v)) +} + +// UpdatedByHasPrefix applies the HasPrefix predicate on the "updated_by" field. +func UpdatedByHasPrefix(v string) predicate.Template { + return predicate.Template(sql.FieldHasPrefix(FieldUpdatedBy, v)) +} + +// UpdatedByHasSuffix applies the HasSuffix predicate on the "updated_by" field. +func UpdatedByHasSuffix(v string) predicate.Template { + return predicate.Template(sql.FieldHasSuffix(FieldUpdatedBy, v)) +} + +// UpdatedByIsNil applies the IsNil predicate on the "updated_by" field. +func UpdatedByIsNil() predicate.Template { + return predicate.Template(sql.FieldIsNull(FieldUpdatedBy)) +} + +// UpdatedByNotNil applies the NotNil predicate on the "updated_by" field. +func UpdatedByNotNil() predicate.Template { + return predicate.Template(sql.FieldNotNull(FieldUpdatedBy)) +} + +// UpdatedByEqualFold applies the EqualFold predicate on the "updated_by" field. +func UpdatedByEqualFold(v string) predicate.Template { + return predicate.Template(sql.FieldEqualFold(FieldUpdatedBy, v)) +} + +// UpdatedByContainsFold applies the ContainsFold predicate on the "updated_by" field. +func UpdatedByContainsFold(v string) predicate.Template { + return predicate.Template(sql.FieldContainsFold(FieldUpdatedBy, v)) +} + +// VisibilityEQ applies the EQ predicate on the "visibility" field. +func VisibilityEQ(v string) predicate.Template { + return predicate.Template(sql.FieldEQ(FieldVisibility, v)) +} + +// VisibilityNEQ applies the NEQ predicate on the "visibility" field. +func VisibilityNEQ(v string) predicate.Template { + return predicate.Template(sql.FieldNEQ(FieldVisibility, v)) +} + +// VisibilityIn applies the In predicate on the "visibility" field. +func VisibilityIn(vs ...string) predicate.Template { + return predicate.Template(sql.FieldIn(FieldVisibility, vs...)) +} + +// VisibilityNotIn applies the NotIn predicate on the "visibility" field. +func VisibilityNotIn(vs ...string) predicate.Template { + return predicate.Template(sql.FieldNotIn(FieldVisibility, vs...)) +} + +// VisibilityGT applies the GT predicate on the "visibility" field. +func VisibilityGT(v string) predicate.Template { + return predicate.Template(sql.FieldGT(FieldVisibility, v)) +} + +// VisibilityGTE applies the GTE predicate on the "visibility" field. +func VisibilityGTE(v string) predicate.Template { + return predicate.Template(sql.FieldGTE(FieldVisibility, v)) +} + +// VisibilityLT applies the LT predicate on the "visibility" field. +func VisibilityLT(v string) predicate.Template { + return predicate.Template(sql.FieldLT(FieldVisibility, v)) +} + +// VisibilityLTE applies the LTE predicate on the "visibility" field. +func VisibilityLTE(v string) predicate.Template { + return predicate.Template(sql.FieldLTE(FieldVisibility, v)) +} + +// VisibilityContains applies the Contains predicate on the "visibility" field. +func VisibilityContains(v string) predicate.Template { + return predicate.Template(sql.FieldContains(FieldVisibility, v)) +} + +// VisibilityHasPrefix applies the HasPrefix predicate on the "visibility" field. +func VisibilityHasPrefix(v string) predicate.Template { + return predicate.Template(sql.FieldHasPrefix(FieldVisibility, v)) +} + +// VisibilityHasSuffix applies the HasSuffix predicate on the "visibility" field. +func VisibilityHasSuffix(v string) predicate.Template { + return predicate.Template(sql.FieldHasSuffix(FieldVisibility, v)) +} + +// VisibilityEqualFold applies the EqualFold predicate on the "visibility" field. +func VisibilityEqualFold(v string) predicate.Template { + return predicate.Template(sql.FieldEqualFold(FieldVisibility, v)) +} + +// VisibilityContainsFold applies the ContainsFold predicate on the "visibility" field. +func VisibilityContainsFold(v string) predicate.Template { + return predicate.Template(sql.FieldContainsFold(FieldVisibility, v)) +} + +// CreatedEQ applies the EQ predicate on the "created" field. +func CreatedEQ(v time.Time) predicate.Template { + return predicate.Template(sql.FieldEQ(FieldCreated, v)) +} + +// CreatedNEQ applies the NEQ predicate on the "created" field. +func CreatedNEQ(v time.Time) predicate.Template { + return predicate.Template(sql.FieldNEQ(FieldCreated, v)) +} + +// CreatedIn applies the In predicate on the "created" field. +func CreatedIn(vs ...time.Time) predicate.Template { + return predicate.Template(sql.FieldIn(FieldCreated, vs...)) +} + +// CreatedNotIn applies the NotIn predicate on the "created" field. +func CreatedNotIn(vs ...time.Time) predicate.Template { + return predicate.Template(sql.FieldNotIn(FieldCreated, vs...)) +} + +// CreatedGT applies the GT predicate on the "created" field. +func CreatedGT(v time.Time) predicate.Template { + return predicate.Template(sql.FieldGT(FieldCreated, v)) +} + +// CreatedGTE applies the GTE predicate on the "created" field. +func CreatedGTE(v time.Time) predicate.Template { + return predicate.Template(sql.FieldGTE(FieldCreated, v)) +} + +// CreatedLT applies the LT predicate on the "created" field. +func CreatedLT(v time.Time) predicate.Template { + return predicate.Template(sql.FieldLT(FieldCreated, v)) +} + +// CreatedLTE applies the LTE predicate on the "created" field. +func CreatedLTE(v time.Time) predicate.Template { + return predicate.Template(sql.FieldLTE(FieldCreated, v)) +} + +// UpdatedEQ applies the EQ predicate on the "updated" field. +func UpdatedEQ(v time.Time) predicate.Template { + return predicate.Template(sql.FieldEQ(FieldUpdated, v)) +} + +// UpdatedNEQ applies the NEQ predicate on the "updated" field. +func UpdatedNEQ(v time.Time) predicate.Template { + return predicate.Template(sql.FieldNEQ(FieldUpdated, v)) +} + +// UpdatedIn applies the In predicate on the "updated" field. +func UpdatedIn(vs ...time.Time) predicate.Template { + return predicate.Template(sql.FieldIn(FieldUpdated, vs...)) +} + +// UpdatedNotIn applies the NotIn predicate on the "updated" field. +func UpdatedNotIn(vs ...time.Time) predicate.Template { + return predicate.Template(sql.FieldNotIn(FieldUpdated, vs...)) +} + +// UpdatedGT applies the GT predicate on the "updated" field. +func UpdatedGT(v time.Time) predicate.Template { + return predicate.Template(sql.FieldGT(FieldUpdated, v)) +} + +// UpdatedGTE applies the GTE predicate on the "updated" field. +func UpdatedGTE(v time.Time) predicate.Template { + return predicate.Template(sql.FieldGTE(FieldUpdated, v)) +} + +// UpdatedLT applies the LT predicate on the "updated" field. +func UpdatedLT(v time.Time) predicate.Template { + return predicate.Template(sql.FieldLT(FieldUpdated, v)) +} + +// UpdatedLTE applies the LTE predicate on the "updated" field. +func UpdatedLTE(v time.Time) predicate.Template { + return predicate.Template(sql.FieldLTE(FieldUpdated, v)) +} + +// And groups predicates with the AND operator between them. +func And(predicates ...predicate.Template) predicate.Template { + return predicate.Template(sql.AndPredicates(predicates...)) +} + +// Or groups predicates with the OR operator between them. +func Or(predicates ...predicate.Template) predicate.Template { + return predicate.Template(sql.OrPredicates(predicates...)) +} + +// Not applies the not operator on the given predicate. +func Not(p predicate.Template) predicate.Template { + return predicate.Template(sql.NotPredicates(p)) +} diff --git a/pkg/ent/template_create.go b/pkg/ent/template_create.go new file mode 100644 index 000000000..3a1b882d9 --- /dev/null +++ b/pkg/ent/template_create.go @@ -0,0 +1,2169 @@ +// Code generated by ent, DO NOT EDIT. + +package ent + +import ( + "context" + "errors" + "fmt" + "time" + + "entgo.io/ent/dialect" + "entgo.io/ent/dialect/sql" + "entgo.io/ent/dialect/sql/sqlgraph" + "entgo.io/ent/schema/field" + "github.com/GoogleCloudPlatform/scion/pkg/ent/template" + "github.com/google/uuid" +) + +// TemplateCreate is the builder for creating a Template entity. +type TemplateCreate struct { + config + mutation *TemplateMutation + hooks []Hook + conflict []sql.ConflictOption +} + +// SetName sets the "name" field. +func (_c *TemplateCreate) SetName(v string) *TemplateCreate { + _c.mutation.SetName(v) + return _c +} + +// SetSlug sets the "slug" field. +func (_c *TemplateCreate) SetSlug(v string) *TemplateCreate { + _c.mutation.SetSlug(v) + return _c +} + +// SetDisplayName sets the "display_name" field. +func (_c *TemplateCreate) SetDisplayName(v string) *TemplateCreate { + _c.mutation.SetDisplayName(v) + return _c +} + +// SetNillableDisplayName sets the "display_name" field if the given value is not nil. +func (_c *TemplateCreate) SetNillableDisplayName(v *string) *TemplateCreate { + if v != nil { + _c.SetDisplayName(*v) + } + return _c +} + +// SetDescription sets the "description" field. +func (_c *TemplateCreate) SetDescription(v string) *TemplateCreate { + _c.mutation.SetDescription(v) + return _c +} + +// SetNillableDescription sets the "description" field if the given value is not nil. +func (_c *TemplateCreate) SetNillableDescription(v *string) *TemplateCreate { + if v != nil { + _c.SetDescription(*v) + } + return _c +} + +// SetHarness sets the "harness" field. +func (_c *TemplateCreate) SetHarness(v string) *TemplateCreate { + _c.mutation.SetHarness(v) + return _c +} + +// SetDefaultHarnessConfig sets the "default_harness_config" field. +func (_c *TemplateCreate) SetDefaultHarnessConfig(v string) *TemplateCreate { + _c.mutation.SetDefaultHarnessConfig(v) + return _c +} + +// SetNillableDefaultHarnessConfig sets the "default_harness_config" field if the given value is not nil. +func (_c *TemplateCreate) SetNillableDefaultHarnessConfig(v *string) *TemplateCreate { + if v != nil { + _c.SetDefaultHarnessConfig(*v) + } + return _c +} + +// SetImage sets the "image" field. +func (_c *TemplateCreate) SetImage(v string) *TemplateCreate { + _c.mutation.SetImage(v) + return _c +} + +// SetNillableImage sets the "image" field if the given value is not nil. +func (_c *TemplateCreate) SetNillableImage(v *string) *TemplateCreate { + if v != nil { + _c.SetImage(*v) + } + return _c +} + +// SetConfig sets the "config" field. +func (_c *TemplateCreate) SetConfig(v string) *TemplateCreate { + _c.mutation.SetConfig(v) + return _c +} + +// SetNillableConfig sets the "config" field if the given value is not nil. +func (_c *TemplateCreate) SetNillableConfig(v *string) *TemplateCreate { + if v != nil { + _c.SetConfig(*v) + } + return _c +} + +// SetContentHash sets the "content_hash" field. +func (_c *TemplateCreate) SetContentHash(v string) *TemplateCreate { + _c.mutation.SetContentHash(v) + return _c +} + +// SetNillableContentHash sets the "content_hash" field if the given value is not nil. +func (_c *TemplateCreate) SetNillableContentHash(v *string) *TemplateCreate { + if v != nil { + _c.SetContentHash(*v) + } + return _c +} + +// SetScope sets the "scope" field. +func (_c *TemplateCreate) SetScope(v string) *TemplateCreate { + _c.mutation.SetScope(v) + return _c +} + +// SetNillableScope sets the "scope" field if the given value is not nil. +func (_c *TemplateCreate) SetNillableScope(v *string) *TemplateCreate { + if v != nil { + _c.SetScope(*v) + } + return _c +} + +// SetScopeID sets the "scope_id" field. +func (_c *TemplateCreate) SetScopeID(v string) *TemplateCreate { + _c.mutation.SetScopeID(v) + return _c +} + +// SetNillableScopeID sets the "scope_id" field if the given value is not nil. +func (_c *TemplateCreate) SetNillableScopeID(v *string) *TemplateCreate { + if v != nil { + _c.SetScopeID(*v) + } + return _c +} + +// SetProjectID sets the "project_id" field. +func (_c *TemplateCreate) SetProjectID(v string) *TemplateCreate { + _c.mutation.SetProjectID(v) + return _c +} + +// SetNillableProjectID sets the "project_id" field if the given value is not nil. +func (_c *TemplateCreate) SetNillableProjectID(v *string) *TemplateCreate { + if v != nil { + _c.SetProjectID(*v) + } + return _c +} + +// SetStorageURI sets the "storage_uri" field. +func (_c *TemplateCreate) SetStorageURI(v string) *TemplateCreate { + _c.mutation.SetStorageURI(v) + return _c +} + +// SetNillableStorageURI sets the "storage_uri" field if the given value is not nil. +func (_c *TemplateCreate) SetNillableStorageURI(v *string) *TemplateCreate { + if v != nil { + _c.SetStorageURI(*v) + } + return _c +} + +// SetStorageBucket sets the "storage_bucket" field. +func (_c *TemplateCreate) SetStorageBucket(v string) *TemplateCreate { + _c.mutation.SetStorageBucket(v) + return _c +} + +// SetNillableStorageBucket sets the "storage_bucket" field if the given value is not nil. +func (_c *TemplateCreate) SetNillableStorageBucket(v *string) *TemplateCreate { + if v != nil { + _c.SetStorageBucket(*v) + } + return _c +} + +// SetStoragePath sets the "storage_path" field. +func (_c *TemplateCreate) SetStoragePath(v string) *TemplateCreate { + _c.mutation.SetStoragePath(v) + return _c +} + +// SetNillableStoragePath sets the "storage_path" field if the given value is not nil. +func (_c *TemplateCreate) SetNillableStoragePath(v *string) *TemplateCreate { + if v != nil { + _c.SetStoragePath(*v) + } + return _c +} + +// SetFiles sets the "files" field. +func (_c *TemplateCreate) SetFiles(v string) *TemplateCreate { + _c.mutation.SetFiles(v) + return _c +} + +// SetNillableFiles sets the "files" field if the given value is not nil. +func (_c *TemplateCreate) SetNillableFiles(v *string) *TemplateCreate { + if v != nil { + _c.SetFiles(*v) + } + return _c +} + +// SetBaseTemplate sets the "base_template" field. +func (_c *TemplateCreate) SetBaseTemplate(v string) *TemplateCreate { + _c.mutation.SetBaseTemplate(v) + return _c +} + +// SetNillableBaseTemplate sets the "base_template" field if the given value is not nil. +func (_c *TemplateCreate) SetNillableBaseTemplate(v *string) *TemplateCreate { + if v != nil { + _c.SetBaseTemplate(*v) + } + return _c +} + +// SetStatus sets the "status" field. +func (_c *TemplateCreate) SetStatus(v template.Status) *TemplateCreate { + _c.mutation.SetStatus(v) + return _c +} + +// SetNillableStatus sets the "status" field if the given value is not nil. +func (_c *TemplateCreate) SetNillableStatus(v *template.Status) *TemplateCreate { + if v != nil { + _c.SetStatus(*v) + } + return _c +} + +// SetOwnerID sets the "owner_id" field. +func (_c *TemplateCreate) SetOwnerID(v string) *TemplateCreate { + _c.mutation.SetOwnerID(v) + return _c +} + +// SetNillableOwnerID sets the "owner_id" field if the given value is not nil. +func (_c *TemplateCreate) SetNillableOwnerID(v *string) *TemplateCreate { + if v != nil { + _c.SetOwnerID(*v) + } + return _c +} + +// SetCreatedBy sets the "created_by" field. +func (_c *TemplateCreate) SetCreatedBy(v string) *TemplateCreate { + _c.mutation.SetCreatedBy(v) + return _c +} + +// SetNillableCreatedBy sets the "created_by" field if the given value is not nil. +func (_c *TemplateCreate) SetNillableCreatedBy(v *string) *TemplateCreate { + if v != nil { + _c.SetCreatedBy(*v) + } + return _c +} + +// SetUpdatedBy sets the "updated_by" field. +func (_c *TemplateCreate) SetUpdatedBy(v string) *TemplateCreate { + _c.mutation.SetUpdatedBy(v) + return _c +} + +// SetNillableUpdatedBy sets the "updated_by" field if the given value is not nil. +func (_c *TemplateCreate) SetNillableUpdatedBy(v *string) *TemplateCreate { + if v != nil { + _c.SetUpdatedBy(*v) + } + return _c +} + +// SetVisibility sets the "visibility" field. +func (_c *TemplateCreate) SetVisibility(v string) *TemplateCreate { + _c.mutation.SetVisibility(v) + return _c +} + +// SetNillableVisibility sets the "visibility" field if the given value is not nil. +func (_c *TemplateCreate) SetNillableVisibility(v *string) *TemplateCreate { + if v != nil { + _c.SetVisibility(*v) + } + return _c +} + +// SetCreated sets the "created" field. +func (_c *TemplateCreate) SetCreated(v time.Time) *TemplateCreate { + _c.mutation.SetCreated(v) + return _c +} + +// SetNillableCreated sets the "created" field if the given value is not nil. +func (_c *TemplateCreate) SetNillableCreated(v *time.Time) *TemplateCreate { + if v != nil { + _c.SetCreated(*v) + } + return _c +} + +// SetUpdated sets the "updated" field. +func (_c *TemplateCreate) SetUpdated(v time.Time) *TemplateCreate { + _c.mutation.SetUpdated(v) + return _c +} + +// SetNillableUpdated sets the "updated" field if the given value is not nil. +func (_c *TemplateCreate) SetNillableUpdated(v *time.Time) *TemplateCreate { + if v != nil { + _c.SetUpdated(*v) + } + return _c +} + +// SetID sets the "id" field. +func (_c *TemplateCreate) SetID(v uuid.UUID) *TemplateCreate { + _c.mutation.SetID(v) + return _c +} + +// SetNillableID sets the "id" field if the given value is not nil. +func (_c *TemplateCreate) SetNillableID(v *uuid.UUID) *TemplateCreate { + if v != nil { + _c.SetID(*v) + } + return _c +} + +// Mutation returns the TemplateMutation object of the builder. +func (_c *TemplateCreate) Mutation() *TemplateMutation { + return _c.mutation +} + +// Save creates the Template in the database. +func (_c *TemplateCreate) Save(ctx context.Context) (*Template, error) { + _c.defaults() + return withHooks(ctx, _c.sqlSave, _c.mutation, _c.hooks) +} + +// SaveX calls Save and panics if Save returns an error. +func (_c *TemplateCreate) SaveX(ctx context.Context) *Template { + v, err := _c.Save(ctx) + if err != nil { + panic(err) + } + return v +} + +// Exec executes the query. +func (_c *TemplateCreate) Exec(ctx context.Context) error { + _, err := _c.Save(ctx) + return err +} + +// ExecX is like Exec, but panics if an error occurs. +func (_c *TemplateCreate) ExecX(ctx context.Context) { + if err := _c.Exec(ctx); err != nil { + panic(err) + } +} + +// defaults sets the default values of the builder before save. +func (_c *TemplateCreate) defaults() { + if _, ok := _c.mutation.Scope(); !ok { + v := template.DefaultScope + _c.mutation.SetScope(v) + } + if _, ok := _c.mutation.Status(); !ok { + v := template.DefaultStatus + _c.mutation.SetStatus(v) + } + if _, ok := _c.mutation.Visibility(); !ok { + v := template.DefaultVisibility + _c.mutation.SetVisibility(v) + } + if _, ok := _c.mutation.Created(); !ok { + v := template.DefaultCreated() + _c.mutation.SetCreated(v) + } + if _, ok := _c.mutation.Updated(); !ok { + v := template.DefaultUpdated() + _c.mutation.SetUpdated(v) + } + if _, ok := _c.mutation.ID(); !ok { + v := template.DefaultID() + _c.mutation.SetID(v) + } +} + +// check runs all checks and user-defined validators on the builder. +func (_c *TemplateCreate) check() error { + if _, ok := _c.mutation.Name(); !ok { + return &ValidationError{Name: "name", err: errors.New(`ent: missing required field "Template.name"`)} + } + if v, ok := _c.mutation.Name(); ok { + if err := template.NameValidator(v); err != nil { + return &ValidationError{Name: "name", err: fmt.Errorf(`ent: validator failed for field "Template.name": %w`, err)} + } + } + if _, ok := _c.mutation.Slug(); !ok { + return &ValidationError{Name: "slug", err: errors.New(`ent: missing required field "Template.slug"`)} + } + if v, ok := _c.mutation.Slug(); ok { + if err := template.SlugValidator(v); err != nil { + return &ValidationError{Name: "slug", err: fmt.Errorf(`ent: validator failed for field "Template.slug": %w`, err)} + } + } + if _, ok := _c.mutation.Harness(); !ok { + return &ValidationError{Name: "harness", err: errors.New(`ent: missing required field "Template.harness"`)} + } + if _, ok := _c.mutation.Scope(); !ok { + return &ValidationError{Name: "scope", err: errors.New(`ent: missing required field "Template.scope"`)} + } + if _, ok := _c.mutation.Status(); !ok { + return &ValidationError{Name: "status", err: errors.New(`ent: missing required field "Template.status"`)} + } + if v, ok := _c.mutation.Status(); ok { + if err := template.StatusValidator(v); err != nil { + return &ValidationError{Name: "status", err: fmt.Errorf(`ent: validator failed for field "Template.status": %w`, err)} + } + } + if _, ok := _c.mutation.Visibility(); !ok { + return &ValidationError{Name: "visibility", err: errors.New(`ent: missing required field "Template.visibility"`)} + } + if _, ok := _c.mutation.Created(); !ok { + return &ValidationError{Name: "created", err: errors.New(`ent: missing required field "Template.created"`)} + } + if _, ok := _c.mutation.Updated(); !ok { + return &ValidationError{Name: "updated", err: errors.New(`ent: missing required field "Template.updated"`)} + } + return nil +} + +func (_c *TemplateCreate) sqlSave(ctx context.Context) (*Template, error) { + if err := _c.check(); err != nil { + return nil, err + } + _node, _spec := _c.createSpec() + if err := sqlgraph.CreateNode(ctx, _c.driver, _spec); err != nil { + if sqlgraph.IsConstraintError(err) { + err = &ConstraintError{msg: err.Error(), wrap: err} + } + return nil, err + } + if _spec.ID.Value != nil { + if id, ok := _spec.ID.Value.(*uuid.UUID); ok { + _node.ID = *id + } else if err := _node.ID.Scan(_spec.ID.Value); err != nil { + return nil, err + } + } + _c.mutation.id = &_node.ID + _c.mutation.done = true + return _node, nil +} + +func (_c *TemplateCreate) createSpec() (*Template, *sqlgraph.CreateSpec) { + var ( + _node = &Template{config: _c.config} + _spec = sqlgraph.NewCreateSpec(template.Table, sqlgraph.NewFieldSpec(template.FieldID, field.TypeUUID)) + ) + _spec.OnConflict = _c.conflict + if id, ok := _c.mutation.ID(); ok { + _node.ID = id + _spec.ID.Value = &id + } + if value, ok := _c.mutation.Name(); ok { + _spec.SetField(template.FieldName, field.TypeString, value) + _node.Name = value + } + if value, ok := _c.mutation.Slug(); ok { + _spec.SetField(template.FieldSlug, field.TypeString, value) + _node.Slug = value + } + if value, ok := _c.mutation.DisplayName(); ok { + _spec.SetField(template.FieldDisplayName, field.TypeString, value) + _node.DisplayName = value + } + if value, ok := _c.mutation.Description(); ok { + _spec.SetField(template.FieldDescription, field.TypeString, value) + _node.Description = value + } + if value, ok := _c.mutation.Harness(); ok { + _spec.SetField(template.FieldHarness, field.TypeString, value) + _node.Harness = value + } + if value, ok := _c.mutation.DefaultHarnessConfig(); ok { + _spec.SetField(template.FieldDefaultHarnessConfig, field.TypeString, value) + _node.DefaultHarnessConfig = value + } + if value, ok := _c.mutation.Image(); ok { + _spec.SetField(template.FieldImage, field.TypeString, value) + _node.Image = value + } + if value, ok := _c.mutation.Config(); ok { + _spec.SetField(template.FieldConfig, field.TypeString, value) + _node.Config = value + } + if value, ok := _c.mutation.ContentHash(); ok { + _spec.SetField(template.FieldContentHash, field.TypeString, value) + _node.ContentHash = value + } + if value, ok := _c.mutation.Scope(); ok { + _spec.SetField(template.FieldScope, field.TypeString, value) + _node.Scope = value + } + if value, ok := _c.mutation.ScopeID(); ok { + _spec.SetField(template.FieldScopeID, field.TypeString, value) + _node.ScopeID = value + } + if value, ok := _c.mutation.ProjectID(); ok { + _spec.SetField(template.FieldProjectID, field.TypeString, value) + _node.ProjectID = value + } + if value, ok := _c.mutation.StorageURI(); ok { + _spec.SetField(template.FieldStorageURI, field.TypeString, value) + _node.StorageURI = value + } + if value, ok := _c.mutation.StorageBucket(); ok { + _spec.SetField(template.FieldStorageBucket, field.TypeString, value) + _node.StorageBucket = value + } + if value, ok := _c.mutation.StoragePath(); ok { + _spec.SetField(template.FieldStoragePath, field.TypeString, value) + _node.StoragePath = value + } + if value, ok := _c.mutation.Files(); ok { + _spec.SetField(template.FieldFiles, field.TypeString, value) + _node.Files = value + } + if value, ok := _c.mutation.BaseTemplate(); ok { + _spec.SetField(template.FieldBaseTemplate, field.TypeString, value) + _node.BaseTemplate = value + } + if value, ok := _c.mutation.Status(); ok { + _spec.SetField(template.FieldStatus, field.TypeEnum, value) + _node.Status = value + } + if value, ok := _c.mutation.OwnerID(); ok { + _spec.SetField(template.FieldOwnerID, field.TypeString, value) + _node.OwnerID = value + } + if value, ok := _c.mutation.CreatedBy(); ok { + _spec.SetField(template.FieldCreatedBy, field.TypeString, value) + _node.CreatedBy = value + } + if value, ok := _c.mutation.UpdatedBy(); ok { + _spec.SetField(template.FieldUpdatedBy, field.TypeString, value) + _node.UpdatedBy = value + } + if value, ok := _c.mutation.Visibility(); ok { + _spec.SetField(template.FieldVisibility, field.TypeString, value) + _node.Visibility = value + } + if value, ok := _c.mutation.Created(); ok { + _spec.SetField(template.FieldCreated, field.TypeTime, value) + _node.Created = value + } + if value, ok := _c.mutation.Updated(); ok { + _spec.SetField(template.FieldUpdated, field.TypeTime, value) + _node.Updated = value + } + return _node, _spec +} + +// OnConflict allows configuring the `ON CONFLICT` / `ON DUPLICATE KEY` clause +// of the `INSERT` statement. For example: +// +// client.Template.Create(). +// SetName(v). +// OnConflict( +// // Update the row with the new values +// // the was proposed for insertion. +// sql.ResolveWithNewValues(), +// ). +// // Override some of the fields with custom +// // update values. +// Update(func(u *ent.TemplateUpsert) { +// SetName(v+v). +// }). +// Exec(ctx) +func (_c *TemplateCreate) OnConflict(opts ...sql.ConflictOption) *TemplateUpsertOne { + _c.conflict = opts + return &TemplateUpsertOne{ + create: _c, + } +} + +// OnConflictColumns calls `OnConflict` and configures the columns +// as conflict target. Using this option is equivalent to using: +// +// client.Template.Create(). +// OnConflict(sql.ConflictColumns(columns...)). +// Exec(ctx) +func (_c *TemplateCreate) OnConflictColumns(columns ...string) *TemplateUpsertOne { + _c.conflict = append(_c.conflict, sql.ConflictColumns(columns...)) + return &TemplateUpsertOne{ + create: _c, + } +} + +type ( + // TemplateUpsertOne is the builder for "upsert"-ing + // one Template node. + TemplateUpsertOne struct { + create *TemplateCreate + } + + // TemplateUpsert is the "OnConflict" setter. + TemplateUpsert struct { + *sql.UpdateSet + } +) + +// SetName sets the "name" field. +func (u *TemplateUpsert) SetName(v string) *TemplateUpsert { + u.Set(template.FieldName, v) + return u +} + +// UpdateName sets the "name" field to the value that was provided on create. +func (u *TemplateUpsert) UpdateName() *TemplateUpsert { + u.SetExcluded(template.FieldName) + return u +} + +// SetSlug sets the "slug" field. +func (u *TemplateUpsert) SetSlug(v string) *TemplateUpsert { + u.Set(template.FieldSlug, v) + return u +} + +// UpdateSlug sets the "slug" field to the value that was provided on create. +func (u *TemplateUpsert) UpdateSlug() *TemplateUpsert { + u.SetExcluded(template.FieldSlug) + return u +} + +// SetDisplayName sets the "display_name" field. +func (u *TemplateUpsert) SetDisplayName(v string) *TemplateUpsert { + u.Set(template.FieldDisplayName, v) + return u +} + +// UpdateDisplayName sets the "display_name" field to the value that was provided on create. +func (u *TemplateUpsert) UpdateDisplayName() *TemplateUpsert { + u.SetExcluded(template.FieldDisplayName) + return u +} + +// ClearDisplayName clears the value of the "display_name" field. +func (u *TemplateUpsert) ClearDisplayName() *TemplateUpsert { + u.SetNull(template.FieldDisplayName) + return u +} + +// SetDescription sets the "description" field. +func (u *TemplateUpsert) SetDescription(v string) *TemplateUpsert { + u.Set(template.FieldDescription, v) + return u +} + +// UpdateDescription sets the "description" field to the value that was provided on create. +func (u *TemplateUpsert) UpdateDescription() *TemplateUpsert { + u.SetExcluded(template.FieldDescription) + return u +} + +// ClearDescription clears the value of the "description" field. +func (u *TemplateUpsert) ClearDescription() *TemplateUpsert { + u.SetNull(template.FieldDescription) + return u +} + +// SetHarness sets the "harness" field. +func (u *TemplateUpsert) SetHarness(v string) *TemplateUpsert { + u.Set(template.FieldHarness, v) + return u +} + +// UpdateHarness sets the "harness" field to the value that was provided on create. +func (u *TemplateUpsert) UpdateHarness() *TemplateUpsert { + u.SetExcluded(template.FieldHarness) + return u +} + +// SetDefaultHarnessConfig sets the "default_harness_config" field. +func (u *TemplateUpsert) SetDefaultHarnessConfig(v string) *TemplateUpsert { + u.Set(template.FieldDefaultHarnessConfig, v) + return u +} + +// UpdateDefaultHarnessConfig sets the "default_harness_config" field to the value that was provided on create. +func (u *TemplateUpsert) UpdateDefaultHarnessConfig() *TemplateUpsert { + u.SetExcluded(template.FieldDefaultHarnessConfig) + return u +} + +// ClearDefaultHarnessConfig clears the value of the "default_harness_config" field. +func (u *TemplateUpsert) ClearDefaultHarnessConfig() *TemplateUpsert { + u.SetNull(template.FieldDefaultHarnessConfig) + return u +} + +// SetImage sets the "image" field. +func (u *TemplateUpsert) SetImage(v string) *TemplateUpsert { + u.Set(template.FieldImage, v) + return u +} + +// UpdateImage sets the "image" field to the value that was provided on create. +func (u *TemplateUpsert) UpdateImage() *TemplateUpsert { + u.SetExcluded(template.FieldImage) + return u +} + +// ClearImage clears the value of the "image" field. +func (u *TemplateUpsert) ClearImage() *TemplateUpsert { + u.SetNull(template.FieldImage) + return u +} + +// SetConfig sets the "config" field. +func (u *TemplateUpsert) SetConfig(v string) *TemplateUpsert { + u.Set(template.FieldConfig, v) + return u +} + +// UpdateConfig sets the "config" field to the value that was provided on create. +func (u *TemplateUpsert) UpdateConfig() *TemplateUpsert { + u.SetExcluded(template.FieldConfig) + return u +} + +// ClearConfig clears the value of the "config" field. +func (u *TemplateUpsert) ClearConfig() *TemplateUpsert { + u.SetNull(template.FieldConfig) + return u +} + +// SetContentHash sets the "content_hash" field. +func (u *TemplateUpsert) SetContentHash(v string) *TemplateUpsert { + u.Set(template.FieldContentHash, v) + return u +} + +// UpdateContentHash sets the "content_hash" field to the value that was provided on create. +func (u *TemplateUpsert) UpdateContentHash() *TemplateUpsert { + u.SetExcluded(template.FieldContentHash) + return u +} + +// ClearContentHash clears the value of the "content_hash" field. +func (u *TemplateUpsert) ClearContentHash() *TemplateUpsert { + u.SetNull(template.FieldContentHash) + return u +} + +// SetScope sets the "scope" field. +func (u *TemplateUpsert) SetScope(v string) *TemplateUpsert { + u.Set(template.FieldScope, v) + return u +} + +// UpdateScope sets the "scope" field to the value that was provided on create. +func (u *TemplateUpsert) UpdateScope() *TemplateUpsert { + u.SetExcluded(template.FieldScope) + return u +} + +// SetScopeID sets the "scope_id" field. +func (u *TemplateUpsert) SetScopeID(v string) *TemplateUpsert { + u.Set(template.FieldScopeID, v) + return u +} + +// UpdateScopeID sets the "scope_id" field to the value that was provided on create. +func (u *TemplateUpsert) UpdateScopeID() *TemplateUpsert { + u.SetExcluded(template.FieldScopeID) + return u +} + +// ClearScopeID clears the value of the "scope_id" field. +func (u *TemplateUpsert) ClearScopeID() *TemplateUpsert { + u.SetNull(template.FieldScopeID) + return u +} + +// SetProjectID sets the "project_id" field. +func (u *TemplateUpsert) SetProjectID(v string) *TemplateUpsert { + u.Set(template.FieldProjectID, v) + return u +} + +// UpdateProjectID sets the "project_id" field to the value that was provided on create. +func (u *TemplateUpsert) UpdateProjectID() *TemplateUpsert { + u.SetExcluded(template.FieldProjectID) + return u +} + +// ClearProjectID clears the value of the "project_id" field. +func (u *TemplateUpsert) ClearProjectID() *TemplateUpsert { + u.SetNull(template.FieldProjectID) + return u +} + +// SetStorageURI sets the "storage_uri" field. +func (u *TemplateUpsert) SetStorageURI(v string) *TemplateUpsert { + u.Set(template.FieldStorageURI, v) + return u +} + +// UpdateStorageURI sets the "storage_uri" field to the value that was provided on create. +func (u *TemplateUpsert) UpdateStorageURI() *TemplateUpsert { + u.SetExcluded(template.FieldStorageURI) + return u +} + +// ClearStorageURI clears the value of the "storage_uri" field. +func (u *TemplateUpsert) ClearStorageURI() *TemplateUpsert { + u.SetNull(template.FieldStorageURI) + return u +} + +// SetStorageBucket sets the "storage_bucket" field. +func (u *TemplateUpsert) SetStorageBucket(v string) *TemplateUpsert { + u.Set(template.FieldStorageBucket, v) + return u +} + +// UpdateStorageBucket sets the "storage_bucket" field to the value that was provided on create. +func (u *TemplateUpsert) UpdateStorageBucket() *TemplateUpsert { + u.SetExcluded(template.FieldStorageBucket) + return u +} + +// ClearStorageBucket clears the value of the "storage_bucket" field. +func (u *TemplateUpsert) ClearStorageBucket() *TemplateUpsert { + u.SetNull(template.FieldStorageBucket) + return u +} + +// SetStoragePath sets the "storage_path" field. +func (u *TemplateUpsert) SetStoragePath(v string) *TemplateUpsert { + u.Set(template.FieldStoragePath, v) + return u +} + +// UpdateStoragePath sets the "storage_path" field to the value that was provided on create. +func (u *TemplateUpsert) UpdateStoragePath() *TemplateUpsert { + u.SetExcluded(template.FieldStoragePath) + return u +} + +// ClearStoragePath clears the value of the "storage_path" field. +func (u *TemplateUpsert) ClearStoragePath() *TemplateUpsert { + u.SetNull(template.FieldStoragePath) + return u +} + +// SetFiles sets the "files" field. +func (u *TemplateUpsert) SetFiles(v string) *TemplateUpsert { + u.Set(template.FieldFiles, v) + return u +} + +// UpdateFiles sets the "files" field to the value that was provided on create. +func (u *TemplateUpsert) UpdateFiles() *TemplateUpsert { + u.SetExcluded(template.FieldFiles) + return u +} + +// ClearFiles clears the value of the "files" field. +func (u *TemplateUpsert) ClearFiles() *TemplateUpsert { + u.SetNull(template.FieldFiles) + return u +} + +// SetBaseTemplate sets the "base_template" field. +func (u *TemplateUpsert) SetBaseTemplate(v string) *TemplateUpsert { + u.Set(template.FieldBaseTemplate, v) + return u +} + +// UpdateBaseTemplate sets the "base_template" field to the value that was provided on create. +func (u *TemplateUpsert) UpdateBaseTemplate() *TemplateUpsert { + u.SetExcluded(template.FieldBaseTemplate) + return u +} + +// ClearBaseTemplate clears the value of the "base_template" field. +func (u *TemplateUpsert) ClearBaseTemplate() *TemplateUpsert { + u.SetNull(template.FieldBaseTemplate) + return u +} + +// SetStatus sets the "status" field. +func (u *TemplateUpsert) SetStatus(v template.Status) *TemplateUpsert { + u.Set(template.FieldStatus, v) + return u +} + +// UpdateStatus sets the "status" field to the value that was provided on create. +func (u *TemplateUpsert) UpdateStatus() *TemplateUpsert { + u.SetExcluded(template.FieldStatus) + return u +} + +// SetOwnerID sets the "owner_id" field. +func (u *TemplateUpsert) SetOwnerID(v string) *TemplateUpsert { + u.Set(template.FieldOwnerID, v) + return u +} + +// UpdateOwnerID sets the "owner_id" field to the value that was provided on create. +func (u *TemplateUpsert) UpdateOwnerID() *TemplateUpsert { + u.SetExcluded(template.FieldOwnerID) + return u +} + +// ClearOwnerID clears the value of the "owner_id" field. +func (u *TemplateUpsert) ClearOwnerID() *TemplateUpsert { + u.SetNull(template.FieldOwnerID) + return u +} + +// SetCreatedBy sets the "created_by" field. +func (u *TemplateUpsert) SetCreatedBy(v string) *TemplateUpsert { + u.Set(template.FieldCreatedBy, v) + return u +} + +// UpdateCreatedBy sets the "created_by" field to the value that was provided on create. +func (u *TemplateUpsert) UpdateCreatedBy() *TemplateUpsert { + u.SetExcluded(template.FieldCreatedBy) + return u +} + +// ClearCreatedBy clears the value of the "created_by" field. +func (u *TemplateUpsert) ClearCreatedBy() *TemplateUpsert { + u.SetNull(template.FieldCreatedBy) + return u +} + +// SetUpdatedBy sets the "updated_by" field. +func (u *TemplateUpsert) SetUpdatedBy(v string) *TemplateUpsert { + u.Set(template.FieldUpdatedBy, v) + return u +} + +// UpdateUpdatedBy sets the "updated_by" field to the value that was provided on create. +func (u *TemplateUpsert) UpdateUpdatedBy() *TemplateUpsert { + u.SetExcluded(template.FieldUpdatedBy) + return u +} + +// ClearUpdatedBy clears the value of the "updated_by" field. +func (u *TemplateUpsert) ClearUpdatedBy() *TemplateUpsert { + u.SetNull(template.FieldUpdatedBy) + return u +} + +// SetVisibility sets the "visibility" field. +func (u *TemplateUpsert) SetVisibility(v string) *TemplateUpsert { + u.Set(template.FieldVisibility, v) + return u +} + +// UpdateVisibility sets the "visibility" field to the value that was provided on create. +func (u *TemplateUpsert) UpdateVisibility() *TemplateUpsert { + u.SetExcluded(template.FieldVisibility) + return u +} + +// SetUpdated sets the "updated" field. +func (u *TemplateUpsert) SetUpdated(v time.Time) *TemplateUpsert { + u.Set(template.FieldUpdated, v) + return u +} + +// UpdateUpdated sets the "updated" field to the value that was provided on create. +func (u *TemplateUpsert) UpdateUpdated() *TemplateUpsert { + u.SetExcluded(template.FieldUpdated) + return u +} + +// UpdateNewValues updates the mutable fields using the new values that were set on create except the ID field. +// Using this option is equivalent to using: +// +// client.Template.Create(). +// OnConflict( +// sql.ResolveWithNewValues(), +// sql.ResolveWith(func(u *sql.UpdateSet) { +// u.SetIgnore(template.FieldID) +// }), +// ). +// Exec(ctx) +func (u *TemplateUpsertOne) UpdateNewValues() *TemplateUpsertOne { + u.create.conflict = append(u.create.conflict, sql.ResolveWithNewValues()) + u.create.conflict = append(u.create.conflict, sql.ResolveWith(func(s *sql.UpdateSet) { + if _, exists := u.create.mutation.ID(); exists { + s.SetIgnore(template.FieldID) + } + if _, exists := u.create.mutation.Created(); exists { + s.SetIgnore(template.FieldCreated) + } + })) + return u +} + +// Ignore sets each column to itself in case of conflict. +// Using this option is equivalent to using: +// +// client.Template.Create(). +// OnConflict(sql.ResolveWithIgnore()). +// Exec(ctx) +func (u *TemplateUpsertOne) Ignore() *TemplateUpsertOne { + u.create.conflict = append(u.create.conflict, sql.ResolveWithIgnore()) + return u +} + +// DoNothing configures the conflict_action to `DO NOTHING`. +// Supported only by SQLite and PostgreSQL. +func (u *TemplateUpsertOne) DoNothing() *TemplateUpsertOne { + u.create.conflict = append(u.create.conflict, sql.DoNothing()) + return u +} + +// Update allows overriding fields `UPDATE` values. See the TemplateCreate.OnConflict +// documentation for more info. +func (u *TemplateUpsertOne) Update(set func(*TemplateUpsert)) *TemplateUpsertOne { + u.create.conflict = append(u.create.conflict, sql.ResolveWith(func(update *sql.UpdateSet) { + set(&TemplateUpsert{UpdateSet: update}) + })) + return u +} + +// SetName sets the "name" field. +func (u *TemplateUpsertOne) SetName(v string) *TemplateUpsertOne { + return u.Update(func(s *TemplateUpsert) { + s.SetName(v) + }) +} + +// UpdateName sets the "name" field to the value that was provided on create. +func (u *TemplateUpsertOne) UpdateName() *TemplateUpsertOne { + return u.Update(func(s *TemplateUpsert) { + s.UpdateName() + }) +} + +// SetSlug sets the "slug" field. +func (u *TemplateUpsertOne) SetSlug(v string) *TemplateUpsertOne { + return u.Update(func(s *TemplateUpsert) { + s.SetSlug(v) + }) +} + +// UpdateSlug sets the "slug" field to the value that was provided on create. +func (u *TemplateUpsertOne) UpdateSlug() *TemplateUpsertOne { + return u.Update(func(s *TemplateUpsert) { + s.UpdateSlug() + }) +} + +// SetDisplayName sets the "display_name" field. +func (u *TemplateUpsertOne) SetDisplayName(v string) *TemplateUpsertOne { + return u.Update(func(s *TemplateUpsert) { + s.SetDisplayName(v) + }) +} + +// UpdateDisplayName sets the "display_name" field to the value that was provided on create. +func (u *TemplateUpsertOne) UpdateDisplayName() *TemplateUpsertOne { + return u.Update(func(s *TemplateUpsert) { + s.UpdateDisplayName() + }) +} + +// ClearDisplayName clears the value of the "display_name" field. +func (u *TemplateUpsertOne) ClearDisplayName() *TemplateUpsertOne { + return u.Update(func(s *TemplateUpsert) { + s.ClearDisplayName() + }) +} + +// SetDescription sets the "description" field. +func (u *TemplateUpsertOne) SetDescription(v string) *TemplateUpsertOne { + return u.Update(func(s *TemplateUpsert) { + s.SetDescription(v) + }) +} + +// UpdateDescription sets the "description" field to the value that was provided on create. +func (u *TemplateUpsertOne) UpdateDescription() *TemplateUpsertOne { + return u.Update(func(s *TemplateUpsert) { + s.UpdateDescription() + }) +} + +// ClearDescription clears the value of the "description" field. +func (u *TemplateUpsertOne) ClearDescription() *TemplateUpsertOne { + return u.Update(func(s *TemplateUpsert) { + s.ClearDescription() + }) +} + +// SetHarness sets the "harness" field. +func (u *TemplateUpsertOne) SetHarness(v string) *TemplateUpsertOne { + return u.Update(func(s *TemplateUpsert) { + s.SetHarness(v) + }) +} + +// UpdateHarness sets the "harness" field to the value that was provided on create. +func (u *TemplateUpsertOne) UpdateHarness() *TemplateUpsertOne { + return u.Update(func(s *TemplateUpsert) { + s.UpdateHarness() + }) +} + +// SetDefaultHarnessConfig sets the "default_harness_config" field. +func (u *TemplateUpsertOne) SetDefaultHarnessConfig(v string) *TemplateUpsertOne { + return u.Update(func(s *TemplateUpsert) { + s.SetDefaultHarnessConfig(v) + }) +} + +// UpdateDefaultHarnessConfig sets the "default_harness_config" field to the value that was provided on create. +func (u *TemplateUpsertOne) UpdateDefaultHarnessConfig() *TemplateUpsertOne { + return u.Update(func(s *TemplateUpsert) { + s.UpdateDefaultHarnessConfig() + }) +} + +// ClearDefaultHarnessConfig clears the value of the "default_harness_config" field. +func (u *TemplateUpsertOne) ClearDefaultHarnessConfig() *TemplateUpsertOne { + return u.Update(func(s *TemplateUpsert) { + s.ClearDefaultHarnessConfig() + }) +} + +// SetImage sets the "image" field. +func (u *TemplateUpsertOne) SetImage(v string) *TemplateUpsertOne { + return u.Update(func(s *TemplateUpsert) { + s.SetImage(v) + }) +} + +// UpdateImage sets the "image" field to the value that was provided on create. +func (u *TemplateUpsertOne) UpdateImage() *TemplateUpsertOne { + return u.Update(func(s *TemplateUpsert) { + s.UpdateImage() + }) +} + +// ClearImage clears the value of the "image" field. +func (u *TemplateUpsertOne) ClearImage() *TemplateUpsertOne { + return u.Update(func(s *TemplateUpsert) { + s.ClearImage() + }) +} + +// SetConfig sets the "config" field. +func (u *TemplateUpsertOne) SetConfig(v string) *TemplateUpsertOne { + return u.Update(func(s *TemplateUpsert) { + s.SetConfig(v) + }) +} + +// UpdateConfig sets the "config" field to the value that was provided on create. +func (u *TemplateUpsertOne) UpdateConfig() *TemplateUpsertOne { + return u.Update(func(s *TemplateUpsert) { + s.UpdateConfig() + }) +} + +// ClearConfig clears the value of the "config" field. +func (u *TemplateUpsertOne) ClearConfig() *TemplateUpsertOne { + return u.Update(func(s *TemplateUpsert) { + s.ClearConfig() + }) +} + +// SetContentHash sets the "content_hash" field. +func (u *TemplateUpsertOne) SetContentHash(v string) *TemplateUpsertOne { + return u.Update(func(s *TemplateUpsert) { + s.SetContentHash(v) + }) +} + +// UpdateContentHash sets the "content_hash" field to the value that was provided on create. +func (u *TemplateUpsertOne) UpdateContentHash() *TemplateUpsertOne { + return u.Update(func(s *TemplateUpsert) { + s.UpdateContentHash() + }) +} + +// ClearContentHash clears the value of the "content_hash" field. +func (u *TemplateUpsertOne) ClearContentHash() *TemplateUpsertOne { + return u.Update(func(s *TemplateUpsert) { + s.ClearContentHash() + }) +} + +// SetScope sets the "scope" field. +func (u *TemplateUpsertOne) SetScope(v string) *TemplateUpsertOne { + return u.Update(func(s *TemplateUpsert) { + s.SetScope(v) + }) +} + +// UpdateScope sets the "scope" field to the value that was provided on create. +func (u *TemplateUpsertOne) UpdateScope() *TemplateUpsertOne { + return u.Update(func(s *TemplateUpsert) { + s.UpdateScope() + }) +} + +// SetScopeID sets the "scope_id" field. +func (u *TemplateUpsertOne) SetScopeID(v string) *TemplateUpsertOne { + return u.Update(func(s *TemplateUpsert) { + s.SetScopeID(v) + }) +} + +// UpdateScopeID sets the "scope_id" field to the value that was provided on create. +func (u *TemplateUpsertOne) UpdateScopeID() *TemplateUpsertOne { + return u.Update(func(s *TemplateUpsert) { + s.UpdateScopeID() + }) +} + +// ClearScopeID clears the value of the "scope_id" field. +func (u *TemplateUpsertOne) ClearScopeID() *TemplateUpsertOne { + return u.Update(func(s *TemplateUpsert) { + s.ClearScopeID() + }) +} + +// SetProjectID sets the "project_id" field. +func (u *TemplateUpsertOne) SetProjectID(v string) *TemplateUpsertOne { + return u.Update(func(s *TemplateUpsert) { + s.SetProjectID(v) + }) +} + +// UpdateProjectID sets the "project_id" field to the value that was provided on create. +func (u *TemplateUpsertOne) UpdateProjectID() *TemplateUpsertOne { + return u.Update(func(s *TemplateUpsert) { + s.UpdateProjectID() + }) +} + +// ClearProjectID clears the value of the "project_id" field. +func (u *TemplateUpsertOne) ClearProjectID() *TemplateUpsertOne { + return u.Update(func(s *TemplateUpsert) { + s.ClearProjectID() + }) +} + +// SetStorageURI sets the "storage_uri" field. +func (u *TemplateUpsertOne) SetStorageURI(v string) *TemplateUpsertOne { + return u.Update(func(s *TemplateUpsert) { + s.SetStorageURI(v) + }) +} + +// UpdateStorageURI sets the "storage_uri" field to the value that was provided on create. +func (u *TemplateUpsertOne) UpdateStorageURI() *TemplateUpsertOne { + return u.Update(func(s *TemplateUpsert) { + s.UpdateStorageURI() + }) +} + +// ClearStorageURI clears the value of the "storage_uri" field. +func (u *TemplateUpsertOne) ClearStorageURI() *TemplateUpsertOne { + return u.Update(func(s *TemplateUpsert) { + s.ClearStorageURI() + }) +} + +// SetStorageBucket sets the "storage_bucket" field. +func (u *TemplateUpsertOne) SetStorageBucket(v string) *TemplateUpsertOne { + return u.Update(func(s *TemplateUpsert) { + s.SetStorageBucket(v) + }) +} + +// UpdateStorageBucket sets the "storage_bucket" field to the value that was provided on create. +func (u *TemplateUpsertOne) UpdateStorageBucket() *TemplateUpsertOne { + return u.Update(func(s *TemplateUpsert) { + s.UpdateStorageBucket() + }) +} + +// ClearStorageBucket clears the value of the "storage_bucket" field. +func (u *TemplateUpsertOne) ClearStorageBucket() *TemplateUpsertOne { + return u.Update(func(s *TemplateUpsert) { + s.ClearStorageBucket() + }) +} + +// SetStoragePath sets the "storage_path" field. +func (u *TemplateUpsertOne) SetStoragePath(v string) *TemplateUpsertOne { + return u.Update(func(s *TemplateUpsert) { + s.SetStoragePath(v) + }) +} + +// UpdateStoragePath sets the "storage_path" field to the value that was provided on create. +func (u *TemplateUpsertOne) UpdateStoragePath() *TemplateUpsertOne { + return u.Update(func(s *TemplateUpsert) { + s.UpdateStoragePath() + }) +} + +// ClearStoragePath clears the value of the "storage_path" field. +func (u *TemplateUpsertOne) ClearStoragePath() *TemplateUpsertOne { + return u.Update(func(s *TemplateUpsert) { + s.ClearStoragePath() + }) +} + +// SetFiles sets the "files" field. +func (u *TemplateUpsertOne) SetFiles(v string) *TemplateUpsertOne { + return u.Update(func(s *TemplateUpsert) { + s.SetFiles(v) + }) +} + +// UpdateFiles sets the "files" field to the value that was provided on create. +func (u *TemplateUpsertOne) UpdateFiles() *TemplateUpsertOne { + return u.Update(func(s *TemplateUpsert) { + s.UpdateFiles() + }) +} + +// ClearFiles clears the value of the "files" field. +func (u *TemplateUpsertOne) ClearFiles() *TemplateUpsertOne { + return u.Update(func(s *TemplateUpsert) { + s.ClearFiles() + }) +} + +// SetBaseTemplate sets the "base_template" field. +func (u *TemplateUpsertOne) SetBaseTemplate(v string) *TemplateUpsertOne { + return u.Update(func(s *TemplateUpsert) { + s.SetBaseTemplate(v) + }) +} + +// UpdateBaseTemplate sets the "base_template" field to the value that was provided on create. +func (u *TemplateUpsertOne) UpdateBaseTemplate() *TemplateUpsertOne { + return u.Update(func(s *TemplateUpsert) { + s.UpdateBaseTemplate() + }) +} + +// ClearBaseTemplate clears the value of the "base_template" field. +func (u *TemplateUpsertOne) ClearBaseTemplate() *TemplateUpsertOne { + return u.Update(func(s *TemplateUpsert) { + s.ClearBaseTemplate() + }) +} + +// SetStatus sets the "status" field. +func (u *TemplateUpsertOne) SetStatus(v template.Status) *TemplateUpsertOne { + return u.Update(func(s *TemplateUpsert) { + s.SetStatus(v) + }) +} + +// UpdateStatus sets the "status" field to the value that was provided on create. +func (u *TemplateUpsertOne) UpdateStatus() *TemplateUpsertOne { + return u.Update(func(s *TemplateUpsert) { + s.UpdateStatus() + }) +} + +// SetOwnerID sets the "owner_id" field. +func (u *TemplateUpsertOne) SetOwnerID(v string) *TemplateUpsertOne { + return u.Update(func(s *TemplateUpsert) { + s.SetOwnerID(v) + }) +} + +// UpdateOwnerID sets the "owner_id" field to the value that was provided on create. +func (u *TemplateUpsertOne) UpdateOwnerID() *TemplateUpsertOne { + return u.Update(func(s *TemplateUpsert) { + s.UpdateOwnerID() + }) +} + +// ClearOwnerID clears the value of the "owner_id" field. +func (u *TemplateUpsertOne) ClearOwnerID() *TemplateUpsertOne { + return u.Update(func(s *TemplateUpsert) { + s.ClearOwnerID() + }) +} + +// SetCreatedBy sets the "created_by" field. +func (u *TemplateUpsertOne) SetCreatedBy(v string) *TemplateUpsertOne { + return u.Update(func(s *TemplateUpsert) { + s.SetCreatedBy(v) + }) +} + +// UpdateCreatedBy sets the "created_by" field to the value that was provided on create. +func (u *TemplateUpsertOne) UpdateCreatedBy() *TemplateUpsertOne { + return u.Update(func(s *TemplateUpsert) { + s.UpdateCreatedBy() + }) +} + +// ClearCreatedBy clears the value of the "created_by" field. +func (u *TemplateUpsertOne) ClearCreatedBy() *TemplateUpsertOne { + return u.Update(func(s *TemplateUpsert) { + s.ClearCreatedBy() + }) +} + +// SetUpdatedBy sets the "updated_by" field. +func (u *TemplateUpsertOne) SetUpdatedBy(v string) *TemplateUpsertOne { + return u.Update(func(s *TemplateUpsert) { + s.SetUpdatedBy(v) + }) +} + +// UpdateUpdatedBy sets the "updated_by" field to the value that was provided on create. +func (u *TemplateUpsertOne) UpdateUpdatedBy() *TemplateUpsertOne { + return u.Update(func(s *TemplateUpsert) { + s.UpdateUpdatedBy() + }) +} + +// ClearUpdatedBy clears the value of the "updated_by" field. +func (u *TemplateUpsertOne) ClearUpdatedBy() *TemplateUpsertOne { + return u.Update(func(s *TemplateUpsert) { + s.ClearUpdatedBy() + }) +} + +// SetVisibility sets the "visibility" field. +func (u *TemplateUpsertOne) SetVisibility(v string) *TemplateUpsertOne { + return u.Update(func(s *TemplateUpsert) { + s.SetVisibility(v) + }) +} + +// UpdateVisibility sets the "visibility" field to the value that was provided on create. +func (u *TemplateUpsertOne) UpdateVisibility() *TemplateUpsertOne { + return u.Update(func(s *TemplateUpsert) { + s.UpdateVisibility() + }) +} + +// SetUpdated sets the "updated" field. +func (u *TemplateUpsertOne) SetUpdated(v time.Time) *TemplateUpsertOne { + return u.Update(func(s *TemplateUpsert) { + s.SetUpdated(v) + }) +} + +// UpdateUpdated sets the "updated" field to the value that was provided on create. +func (u *TemplateUpsertOne) UpdateUpdated() *TemplateUpsertOne { + return u.Update(func(s *TemplateUpsert) { + s.UpdateUpdated() + }) +} + +// Exec executes the query. +func (u *TemplateUpsertOne) Exec(ctx context.Context) error { + if len(u.create.conflict) == 0 { + return errors.New("ent: missing options for TemplateCreate.OnConflict") + } + return u.create.Exec(ctx) +} + +// ExecX is like Exec, but panics if an error occurs. +func (u *TemplateUpsertOne) ExecX(ctx context.Context) { + if err := u.create.Exec(ctx); err != nil { + panic(err) + } +} + +// Exec executes the UPSERT query and returns the inserted/updated ID. +func (u *TemplateUpsertOne) ID(ctx context.Context) (id uuid.UUID, err error) { + if u.create.driver.Dialect() == dialect.MySQL { + // In case of "ON CONFLICT", there is no way to get back non-numeric ID + // fields from the database since MySQL does not support the RETURNING clause. + return id, errors.New("ent: TemplateUpsertOne.ID is not supported by MySQL driver. Use TemplateUpsertOne.Exec instead") + } + node, err := u.create.Save(ctx) + if err != nil { + return id, err + } + return node.ID, nil +} + +// IDX is like ID, but panics if an error occurs. +func (u *TemplateUpsertOne) IDX(ctx context.Context) uuid.UUID { + id, err := u.ID(ctx) + if err != nil { + panic(err) + } + return id +} + +// TemplateCreateBulk is the builder for creating many Template entities in bulk. +type TemplateCreateBulk struct { + config + err error + builders []*TemplateCreate + conflict []sql.ConflictOption +} + +// Save creates the Template entities in the database. +func (_c *TemplateCreateBulk) Save(ctx context.Context) ([]*Template, error) { + if _c.err != nil { + return nil, _c.err + } + specs := make([]*sqlgraph.CreateSpec, len(_c.builders)) + nodes := make([]*Template, len(_c.builders)) + mutators := make([]Mutator, len(_c.builders)) + for i := range _c.builders { + func(i int, root context.Context) { + builder := _c.builders[i] + builder.defaults() + var mut Mutator = MutateFunc(func(ctx context.Context, m Mutation) (Value, error) { + mutation, ok := m.(*TemplateMutation) + if !ok { + return nil, fmt.Errorf("unexpected mutation type %T", m) + } + if err := builder.check(); err != nil { + return nil, err + } + builder.mutation = mutation + var err error + nodes[i], specs[i] = builder.createSpec() + if i < len(mutators)-1 { + _, err = mutators[i+1].Mutate(root, _c.builders[i+1].mutation) + } else { + spec := &sqlgraph.BatchCreateSpec{Nodes: specs} + spec.OnConflict = _c.conflict + // Invoke the actual operation on the latest mutation in the chain. + if err = sqlgraph.BatchCreate(ctx, _c.driver, spec); err != nil { + if sqlgraph.IsConstraintError(err) { + err = &ConstraintError{msg: err.Error(), wrap: err} + } + } + } + if err != nil { + return nil, err + } + mutation.id = &nodes[i].ID + mutation.done = true + return nodes[i], nil + }) + for i := len(builder.hooks) - 1; i >= 0; i-- { + mut = builder.hooks[i](mut) + } + mutators[i] = mut + }(i, ctx) + } + if len(mutators) > 0 { + if _, err := mutators[0].Mutate(ctx, _c.builders[0].mutation); err != nil { + return nil, err + } + } + return nodes, nil +} + +// SaveX is like Save, but panics if an error occurs. +func (_c *TemplateCreateBulk) SaveX(ctx context.Context) []*Template { + v, err := _c.Save(ctx) + if err != nil { + panic(err) + } + return v +} + +// Exec executes the query. +func (_c *TemplateCreateBulk) Exec(ctx context.Context) error { + _, err := _c.Save(ctx) + return err +} + +// ExecX is like Exec, but panics if an error occurs. +func (_c *TemplateCreateBulk) ExecX(ctx context.Context) { + if err := _c.Exec(ctx); err != nil { + panic(err) + } +} + +// OnConflict allows configuring the `ON CONFLICT` / `ON DUPLICATE KEY` clause +// of the `INSERT` statement. For example: +// +// client.Template.CreateBulk(builders...). +// OnConflict( +// // Update the row with the new values +// // the was proposed for insertion. +// sql.ResolveWithNewValues(), +// ). +// // Override some of the fields with custom +// // update values. +// Update(func(u *ent.TemplateUpsert) { +// SetName(v+v). +// }). +// Exec(ctx) +func (_c *TemplateCreateBulk) OnConflict(opts ...sql.ConflictOption) *TemplateUpsertBulk { + _c.conflict = opts + return &TemplateUpsertBulk{ + create: _c, + } +} + +// OnConflictColumns calls `OnConflict` and configures the columns +// as conflict target. Using this option is equivalent to using: +// +// client.Template.Create(). +// OnConflict(sql.ConflictColumns(columns...)). +// Exec(ctx) +func (_c *TemplateCreateBulk) OnConflictColumns(columns ...string) *TemplateUpsertBulk { + _c.conflict = append(_c.conflict, sql.ConflictColumns(columns...)) + return &TemplateUpsertBulk{ + create: _c, + } +} + +// TemplateUpsertBulk is the builder for "upsert"-ing +// a bulk of Template nodes. +type TemplateUpsertBulk struct { + create *TemplateCreateBulk +} + +// UpdateNewValues updates the mutable fields using the new values that +// were set on create. Using this option is equivalent to using: +// +// client.Template.Create(). +// OnConflict( +// sql.ResolveWithNewValues(), +// sql.ResolveWith(func(u *sql.UpdateSet) { +// u.SetIgnore(template.FieldID) +// }), +// ). +// Exec(ctx) +func (u *TemplateUpsertBulk) UpdateNewValues() *TemplateUpsertBulk { + u.create.conflict = append(u.create.conflict, sql.ResolveWithNewValues()) + u.create.conflict = append(u.create.conflict, sql.ResolveWith(func(s *sql.UpdateSet) { + for _, b := range u.create.builders { + if _, exists := b.mutation.ID(); exists { + s.SetIgnore(template.FieldID) + } + if _, exists := b.mutation.Created(); exists { + s.SetIgnore(template.FieldCreated) + } + } + })) + return u +} + +// Ignore sets each column to itself in case of conflict. +// Using this option is equivalent to using: +// +// client.Template.Create(). +// OnConflict(sql.ResolveWithIgnore()). +// Exec(ctx) +func (u *TemplateUpsertBulk) Ignore() *TemplateUpsertBulk { + u.create.conflict = append(u.create.conflict, sql.ResolveWithIgnore()) + return u +} + +// DoNothing configures the conflict_action to `DO NOTHING`. +// Supported only by SQLite and PostgreSQL. +func (u *TemplateUpsertBulk) DoNothing() *TemplateUpsertBulk { + u.create.conflict = append(u.create.conflict, sql.DoNothing()) + return u +} + +// Update allows overriding fields `UPDATE` values. See the TemplateCreateBulk.OnConflict +// documentation for more info. +func (u *TemplateUpsertBulk) Update(set func(*TemplateUpsert)) *TemplateUpsertBulk { + u.create.conflict = append(u.create.conflict, sql.ResolveWith(func(update *sql.UpdateSet) { + set(&TemplateUpsert{UpdateSet: update}) + })) + return u +} + +// SetName sets the "name" field. +func (u *TemplateUpsertBulk) SetName(v string) *TemplateUpsertBulk { + return u.Update(func(s *TemplateUpsert) { + s.SetName(v) + }) +} + +// UpdateName sets the "name" field to the value that was provided on create. +func (u *TemplateUpsertBulk) UpdateName() *TemplateUpsertBulk { + return u.Update(func(s *TemplateUpsert) { + s.UpdateName() + }) +} + +// SetSlug sets the "slug" field. +func (u *TemplateUpsertBulk) SetSlug(v string) *TemplateUpsertBulk { + return u.Update(func(s *TemplateUpsert) { + s.SetSlug(v) + }) +} + +// UpdateSlug sets the "slug" field to the value that was provided on create. +func (u *TemplateUpsertBulk) UpdateSlug() *TemplateUpsertBulk { + return u.Update(func(s *TemplateUpsert) { + s.UpdateSlug() + }) +} + +// SetDisplayName sets the "display_name" field. +func (u *TemplateUpsertBulk) SetDisplayName(v string) *TemplateUpsertBulk { + return u.Update(func(s *TemplateUpsert) { + s.SetDisplayName(v) + }) +} + +// UpdateDisplayName sets the "display_name" field to the value that was provided on create. +func (u *TemplateUpsertBulk) UpdateDisplayName() *TemplateUpsertBulk { + return u.Update(func(s *TemplateUpsert) { + s.UpdateDisplayName() + }) +} + +// ClearDisplayName clears the value of the "display_name" field. +func (u *TemplateUpsertBulk) ClearDisplayName() *TemplateUpsertBulk { + return u.Update(func(s *TemplateUpsert) { + s.ClearDisplayName() + }) +} + +// SetDescription sets the "description" field. +func (u *TemplateUpsertBulk) SetDescription(v string) *TemplateUpsertBulk { + return u.Update(func(s *TemplateUpsert) { + s.SetDescription(v) + }) +} + +// UpdateDescription sets the "description" field to the value that was provided on create. +func (u *TemplateUpsertBulk) UpdateDescription() *TemplateUpsertBulk { + return u.Update(func(s *TemplateUpsert) { + s.UpdateDescription() + }) +} + +// ClearDescription clears the value of the "description" field. +func (u *TemplateUpsertBulk) ClearDescription() *TemplateUpsertBulk { + return u.Update(func(s *TemplateUpsert) { + s.ClearDescription() + }) +} + +// SetHarness sets the "harness" field. +func (u *TemplateUpsertBulk) SetHarness(v string) *TemplateUpsertBulk { + return u.Update(func(s *TemplateUpsert) { + s.SetHarness(v) + }) +} + +// UpdateHarness sets the "harness" field to the value that was provided on create. +func (u *TemplateUpsertBulk) UpdateHarness() *TemplateUpsertBulk { + return u.Update(func(s *TemplateUpsert) { + s.UpdateHarness() + }) +} + +// SetDefaultHarnessConfig sets the "default_harness_config" field. +func (u *TemplateUpsertBulk) SetDefaultHarnessConfig(v string) *TemplateUpsertBulk { + return u.Update(func(s *TemplateUpsert) { + s.SetDefaultHarnessConfig(v) + }) +} + +// UpdateDefaultHarnessConfig sets the "default_harness_config" field to the value that was provided on create. +func (u *TemplateUpsertBulk) UpdateDefaultHarnessConfig() *TemplateUpsertBulk { + return u.Update(func(s *TemplateUpsert) { + s.UpdateDefaultHarnessConfig() + }) +} + +// ClearDefaultHarnessConfig clears the value of the "default_harness_config" field. +func (u *TemplateUpsertBulk) ClearDefaultHarnessConfig() *TemplateUpsertBulk { + return u.Update(func(s *TemplateUpsert) { + s.ClearDefaultHarnessConfig() + }) +} + +// SetImage sets the "image" field. +func (u *TemplateUpsertBulk) SetImage(v string) *TemplateUpsertBulk { + return u.Update(func(s *TemplateUpsert) { + s.SetImage(v) + }) +} + +// UpdateImage sets the "image" field to the value that was provided on create. +func (u *TemplateUpsertBulk) UpdateImage() *TemplateUpsertBulk { + return u.Update(func(s *TemplateUpsert) { + s.UpdateImage() + }) +} + +// ClearImage clears the value of the "image" field. +func (u *TemplateUpsertBulk) ClearImage() *TemplateUpsertBulk { + return u.Update(func(s *TemplateUpsert) { + s.ClearImage() + }) +} + +// SetConfig sets the "config" field. +func (u *TemplateUpsertBulk) SetConfig(v string) *TemplateUpsertBulk { + return u.Update(func(s *TemplateUpsert) { + s.SetConfig(v) + }) +} + +// UpdateConfig sets the "config" field to the value that was provided on create. +func (u *TemplateUpsertBulk) UpdateConfig() *TemplateUpsertBulk { + return u.Update(func(s *TemplateUpsert) { + s.UpdateConfig() + }) +} + +// ClearConfig clears the value of the "config" field. +func (u *TemplateUpsertBulk) ClearConfig() *TemplateUpsertBulk { + return u.Update(func(s *TemplateUpsert) { + s.ClearConfig() + }) +} + +// SetContentHash sets the "content_hash" field. +func (u *TemplateUpsertBulk) SetContentHash(v string) *TemplateUpsertBulk { + return u.Update(func(s *TemplateUpsert) { + s.SetContentHash(v) + }) +} + +// UpdateContentHash sets the "content_hash" field to the value that was provided on create. +func (u *TemplateUpsertBulk) UpdateContentHash() *TemplateUpsertBulk { + return u.Update(func(s *TemplateUpsert) { + s.UpdateContentHash() + }) +} + +// ClearContentHash clears the value of the "content_hash" field. +func (u *TemplateUpsertBulk) ClearContentHash() *TemplateUpsertBulk { + return u.Update(func(s *TemplateUpsert) { + s.ClearContentHash() + }) +} + +// SetScope sets the "scope" field. +func (u *TemplateUpsertBulk) SetScope(v string) *TemplateUpsertBulk { + return u.Update(func(s *TemplateUpsert) { + s.SetScope(v) + }) +} + +// UpdateScope sets the "scope" field to the value that was provided on create. +func (u *TemplateUpsertBulk) UpdateScope() *TemplateUpsertBulk { + return u.Update(func(s *TemplateUpsert) { + s.UpdateScope() + }) +} + +// SetScopeID sets the "scope_id" field. +func (u *TemplateUpsertBulk) SetScopeID(v string) *TemplateUpsertBulk { + return u.Update(func(s *TemplateUpsert) { + s.SetScopeID(v) + }) +} + +// UpdateScopeID sets the "scope_id" field to the value that was provided on create. +func (u *TemplateUpsertBulk) UpdateScopeID() *TemplateUpsertBulk { + return u.Update(func(s *TemplateUpsert) { + s.UpdateScopeID() + }) +} + +// ClearScopeID clears the value of the "scope_id" field. +func (u *TemplateUpsertBulk) ClearScopeID() *TemplateUpsertBulk { + return u.Update(func(s *TemplateUpsert) { + s.ClearScopeID() + }) +} + +// SetProjectID sets the "project_id" field. +func (u *TemplateUpsertBulk) SetProjectID(v string) *TemplateUpsertBulk { + return u.Update(func(s *TemplateUpsert) { + s.SetProjectID(v) + }) +} + +// UpdateProjectID sets the "project_id" field to the value that was provided on create. +func (u *TemplateUpsertBulk) UpdateProjectID() *TemplateUpsertBulk { + return u.Update(func(s *TemplateUpsert) { + s.UpdateProjectID() + }) +} + +// ClearProjectID clears the value of the "project_id" field. +func (u *TemplateUpsertBulk) ClearProjectID() *TemplateUpsertBulk { + return u.Update(func(s *TemplateUpsert) { + s.ClearProjectID() + }) +} + +// SetStorageURI sets the "storage_uri" field. +func (u *TemplateUpsertBulk) SetStorageURI(v string) *TemplateUpsertBulk { + return u.Update(func(s *TemplateUpsert) { + s.SetStorageURI(v) + }) +} + +// UpdateStorageURI sets the "storage_uri" field to the value that was provided on create. +func (u *TemplateUpsertBulk) UpdateStorageURI() *TemplateUpsertBulk { + return u.Update(func(s *TemplateUpsert) { + s.UpdateStorageURI() + }) +} + +// ClearStorageURI clears the value of the "storage_uri" field. +func (u *TemplateUpsertBulk) ClearStorageURI() *TemplateUpsertBulk { + return u.Update(func(s *TemplateUpsert) { + s.ClearStorageURI() + }) +} + +// SetStorageBucket sets the "storage_bucket" field. +func (u *TemplateUpsertBulk) SetStorageBucket(v string) *TemplateUpsertBulk { + return u.Update(func(s *TemplateUpsert) { + s.SetStorageBucket(v) + }) +} + +// UpdateStorageBucket sets the "storage_bucket" field to the value that was provided on create. +func (u *TemplateUpsertBulk) UpdateStorageBucket() *TemplateUpsertBulk { + return u.Update(func(s *TemplateUpsert) { + s.UpdateStorageBucket() + }) +} + +// ClearStorageBucket clears the value of the "storage_bucket" field. +func (u *TemplateUpsertBulk) ClearStorageBucket() *TemplateUpsertBulk { + return u.Update(func(s *TemplateUpsert) { + s.ClearStorageBucket() + }) +} + +// SetStoragePath sets the "storage_path" field. +func (u *TemplateUpsertBulk) SetStoragePath(v string) *TemplateUpsertBulk { + return u.Update(func(s *TemplateUpsert) { + s.SetStoragePath(v) + }) +} + +// UpdateStoragePath sets the "storage_path" field to the value that was provided on create. +func (u *TemplateUpsertBulk) UpdateStoragePath() *TemplateUpsertBulk { + return u.Update(func(s *TemplateUpsert) { + s.UpdateStoragePath() + }) +} + +// ClearStoragePath clears the value of the "storage_path" field. +func (u *TemplateUpsertBulk) ClearStoragePath() *TemplateUpsertBulk { + return u.Update(func(s *TemplateUpsert) { + s.ClearStoragePath() + }) +} + +// SetFiles sets the "files" field. +func (u *TemplateUpsertBulk) SetFiles(v string) *TemplateUpsertBulk { + return u.Update(func(s *TemplateUpsert) { + s.SetFiles(v) + }) +} + +// UpdateFiles sets the "files" field to the value that was provided on create. +func (u *TemplateUpsertBulk) UpdateFiles() *TemplateUpsertBulk { + return u.Update(func(s *TemplateUpsert) { + s.UpdateFiles() + }) +} + +// ClearFiles clears the value of the "files" field. +func (u *TemplateUpsertBulk) ClearFiles() *TemplateUpsertBulk { + return u.Update(func(s *TemplateUpsert) { + s.ClearFiles() + }) +} + +// SetBaseTemplate sets the "base_template" field. +func (u *TemplateUpsertBulk) SetBaseTemplate(v string) *TemplateUpsertBulk { + return u.Update(func(s *TemplateUpsert) { + s.SetBaseTemplate(v) + }) +} + +// UpdateBaseTemplate sets the "base_template" field to the value that was provided on create. +func (u *TemplateUpsertBulk) UpdateBaseTemplate() *TemplateUpsertBulk { + return u.Update(func(s *TemplateUpsert) { + s.UpdateBaseTemplate() + }) +} + +// ClearBaseTemplate clears the value of the "base_template" field. +func (u *TemplateUpsertBulk) ClearBaseTemplate() *TemplateUpsertBulk { + return u.Update(func(s *TemplateUpsert) { + s.ClearBaseTemplate() + }) +} + +// SetStatus sets the "status" field. +func (u *TemplateUpsertBulk) SetStatus(v template.Status) *TemplateUpsertBulk { + return u.Update(func(s *TemplateUpsert) { + s.SetStatus(v) + }) +} + +// UpdateStatus sets the "status" field to the value that was provided on create. +func (u *TemplateUpsertBulk) UpdateStatus() *TemplateUpsertBulk { + return u.Update(func(s *TemplateUpsert) { + s.UpdateStatus() + }) +} + +// SetOwnerID sets the "owner_id" field. +func (u *TemplateUpsertBulk) SetOwnerID(v string) *TemplateUpsertBulk { + return u.Update(func(s *TemplateUpsert) { + s.SetOwnerID(v) + }) +} + +// UpdateOwnerID sets the "owner_id" field to the value that was provided on create. +func (u *TemplateUpsertBulk) UpdateOwnerID() *TemplateUpsertBulk { + return u.Update(func(s *TemplateUpsert) { + s.UpdateOwnerID() + }) +} + +// ClearOwnerID clears the value of the "owner_id" field. +func (u *TemplateUpsertBulk) ClearOwnerID() *TemplateUpsertBulk { + return u.Update(func(s *TemplateUpsert) { + s.ClearOwnerID() + }) +} + +// SetCreatedBy sets the "created_by" field. +func (u *TemplateUpsertBulk) SetCreatedBy(v string) *TemplateUpsertBulk { + return u.Update(func(s *TemplateUpsert) { + s.SetCreatedBy(v) + }) +} + +// UpdateCreatedBy sets the "created_by" field to the value that was provided on create. +func (u *TemplateUpsertBulk) UpdateCreatedBy() *TemplateUpsertBulk { + return u.Update(func(s *TemplateUpsert) { + s.UpdateCreatedBy() + }) +} + +// ClearCreatedBy clears the value of the "created_by" field. +func (u *TemplateUpsertBulk) ClearCreatedBy() *TemplateUpsertBulk { + return u.Update(func(s *TemplateUpsert) { + s.ClearCreatedBy() + }) +} + +// SetUpdatedBy sets the "updated_by" field. +func (u *TemplateUpsertBulk) SetUpdatedBy(v string) *TemplateUpsertBulk { + return u.Update(func(s *TemplateUpsert) { + s.SetUpdatedBy(v) + }) +} + +// UpdateUpdatedBy sets the "updated_by" field to the value that was provided on create. +func (u *TemplateUpsertBulk) UpdateUpdatedBy() *TemplateUpsertBulk { + return u.Update(func(s *TemplateUpsert) { + s.UpdateUpdatedBy() + }) +} + +// ClearUpdatedBy clears the value of the "updated_by" field. +func (u *TemplateUpsertBulk) ClearUpdatedBy() *TemplateUpsertBulk { + return u.Update(func(s *TemplateUpsert) { + s.ClearUpdatedBy() + }) +} + +// SetVisibility sets the "visibility" field. +func (u *TemplateUpsertBulk) SetVisibility(v string) *TemplateUpsertBulk { + return u.Update(func(s *TemplateUpsert) { + s.SetVisibility(v) + }) +} + +// UpdateVisibility sets the "visibility" field to the value that was provided on create. +func (u *TemplateUpsertBulk) UpdateVisibility() *TemplateUpsertBulk { + return u.Update(func(s *TemplateUpsert) { + s.UpdateVisibility() + }) +} + +// SetUpdated sets the "updated" field. +func (u *TemplateUpsertBulk) SetUpdated(v time.Time) *TemplateUpsertBulk { + return u.Update(func(s *TemplateUpsert) { + s.SetUpdated(v) + }) +} + +// UpdateUpdated sets the "updated" field to the value that was provided on create. +func (u *TemplateUpsertBulk) UpdateUpdated() *TemplateUpsertBulk { + return u.Update(func(s *TemplateUpsert) { + s.UpdateUpdated() + }) +} + +// Exec executes the query. +func (u *TemplateUpsertBulk) Exec(ctx context.Context) error { + if u.create.err != nil { + return u.create.err + } + for i, b := range u.create.builders { + if len(b.conflict) != 0 { + return fmt.Errorf("ent: OnConflict was set for builder %d. Set it on the TemplateCreateBulk instead", i) + } + } + if len(u.create.conflict) == 0 { + return errors.New("ent: missing options for TemplateCreateBulk.OnConflict") + } + return u.create.Exec(ctx) +} + +// ExecX is like Exec, but panics if an error occurs. +func (u *TemplateUpsertBulk) ExecX(ctx context.Context) { + if err := u.create.Exec(ctx); err != nil { + panic(err) + } +} diff --git a/pkg/ent/template_delete.go b/pkg/ent/template_delete.go new file mode 100644 index 000000000..5d11d8de1 --- /dev/null +++ b/pkg/ent/template_delete.go @@ -0,0 +1,88 @@ +// Code generated by ent, DO NOT EDIT. + +package ent + +import ( + "context" + + "entgo.io/ent/dialect/sql" + "entgo.io/ent/dialect/sql/sqlgraph" + "entgo.io/ent/schema/field" + "github.com/GoogleCloudPlatform/scion/pkg/ent/predicate" + "github.com/GoogleCloudPlatform/scion/pkg/ent/template" +) + +// TemplateDelete is the builder for deleting a Template entity. +type TemplateDelete struct { + config + hooks []Hook + mutation *TemplateMutation +} + +// Where appends a list predicates to the TemplateDelete builder. +func (_d *TemplateDelete) Where(ps ...predicate.Template) *TemplateDelete { + _d.mutation.Where(ps...) + return _d +} + +// Exec executes the deletion query and returns how many vertices were deleted. +func (_d *TemplateDelete) Exec(ctx context.Context) (int, error) { + return withHooks(ctx, _d.sqlExec, _d.mutation, _d.hooks) +} + +// ExecX is like Exec, but panics if an error occurs. +func (_d *TemplateDelete) ExecX(ctx context.Context) int { + n, err := _d.Exec(ctx) + if err != nil { + panic(err) + } + return n +} + +func (_d *TemplateDelete) sqlExec(ctx context.Context) (int, error) { + _spec := sqlgraph.NewDeleteSpec(template.Table, sqlgraph.NewFieldSpec(template.FieldID, field.TypeUUID)) + if ps := _d.mutation.predicates; len(ps) > 0 { + _spec.Predicate = func(selector *sql.Selector) { + for i := range ps { + ps[i](selector) + } + } + } + affected, err := sqlgraph.DeleteNodes(ctx, _d.driver, _spec) + if err != nil && sqlgraph.IsConstraintError(err) { + err = &ConstraintError{msg: err.Error(), wrap: err} + } + _d.mutation.done = true + return affected, err +} + +// TemplateDeleteOne is the builder for deleting a single Template entity. +type TemplateDeleteOne struct { + _d *TemplateDelete +} + +// Where appends a list predicates to the TemplateDelete builder. +func (_d *TemplateDeleteOne) Where(ps ...predicate.Template) *TemplateDeleteOne { + _d._d.mutation.Where(ps...) + return _d +} + +// Exec executes the deletion query. +func (_d *TemplateDeleteOne) Exec(ctx context.Context) error { + n, err := _d._d.Exec(ctx) + switch { + case err != nil: + return err + case n == 0: + return &NotFoundError{template.Label} + default: + return nil + } +} + +// ExecX is like Exec, but panics if an error occurs. +func (_d *TemplateDeleteOne) ExecX(ctx context.Context) { + if err := _d.Exec(ctx); err != nil { + panic(err) + } +} diff --git a/pkg/ent/template_query.go b/pkg/ent/template_query.go new file mode 100644 index 000000000..82f1a5486 --- /dev/null +++ b/pkg/ent/template_query.go @@ -0,0 +1,565 @@ +// Code generated by ent, DO NOT EDIT. + +package ent + +import ( + "context" + "fmt" + "math" + + "entgo.io/ent" + "entgo.io/ent/dialect" + "entgo.io/ent/dialect/sql" + "entgo.io/ent/dialect/sql/sqlgraph" + "entgo.io/ent/schema/field" + "github.com/GoogleCloudPlatform/scion/pkg/ent/predicate" + "github.com/GoogleCloudPlatform/scion/pkg/ent/template" + "github.com/google/uuid" +) + +// TemplateQuery is the builder for querying Template entities. +type TemplateQuery struct { + config + ctx *QueryContext + order []template.OrderOption + inters []Interceptor + predicates []predicate.Template + modifiers []func(*sql.Selector) + // intermediate query (i.e. traversal path). + sql *sql.Selector + path func(context.Context) (*sql.Selector, error) +} + +// Where adds a new predicate for the TemplateQuery builder. +func (_q *TemplateQuery) Where(ps ...predicate.Template) *TemplateQuery { + _q.predicates = append(_q.predicates, ps...) + return _q +} + +// Limit the number of records to be returned by this query. +func (_q *TemplateQuery) Limit(limit int) *TemplateQuery { + _q.ctx.Limit = &limit + return _q +} + +// Offset to start from. +func (_q *TemplateQuery) Offset(offset int) *TemplateQuery { + _q.ctx.Offset = &offset + return _q +} + +// Unique configures the query builder to filter duplicate records on query. +// By default, unique is set to true, and can be disabled using this method. +func (_q *TemplateQuery) Unique(unique bool) *TemplateQuery { + _q.ctx.Unique = &unique + return _q +} + +// Order specifies how the records should be ordered. +func (_q *TemplateQuery) Order(o ...template.OrderOption) *TemplateQuery { + _q.order = append(_q.order, o...) + return _q +} + +// First returns the first Template entity from the query. +// Returns a *NotFoundError when no Template was found. +func (_q *TemplateQuery) First(ctx context.Context) (*Template, error) { + nodes, err := _q.Limit(1).All(setContextOp(ctx, _q.ctx, ent.OpQueryFirst)) + if err != nil { + return nil, err + } + if len(nodes) == 0 { + return nil, &NotFoundError{template.Label} + } + return nodes[0], nil +} + +// FirstX is like First, but panics if an error occurs. +func (_q *TemplateQuery) FirstX(ctx context.Context) *Template { + node, err := _q.First(ctx) + if err != nil && !IsNotFound(err) { + panic(err) + } + return node +} + +// FirstID returns the first Template ID from the query. +// Returns a *NotFoundError when no Template ID was found. +func (_q *TemplateQuery) FirstID(ctx context.Context) (id uuid.UUID, err error) { + var ids []uuid.UUID + if ids, err = _q.Limit(1).IDs(setContextOp(ctx, _q.ctx, ent.OpQueryFirstID)); err != nil { + return + } + if len(ids) == 0 { + err = &NotFoundError{template.Label} + return + } + return ids[0], nil +} + +// FirstIDX is like FirstID, but panics if an error occurs. +func (_q *TemplateQuery) FirstIDX(ctx context.Context) uuid.UUID { + id, err := _q.FirstID(ctx) + if err != nil && !IsNotFound(err) { + panic(err) + } + return id +} + +// Only returns a single Template entity found by the query, ensuring it only returns one. +// Returns a *NotSingularError when more than one Template entity is found. +// Returns a *NotFoundError when no Template entities are found. +func (_q *TemplateQuery) Only(ctx context.Context) (*Template, error) { + nodes, err := _q.Limit(2).All(setContextOp(ctx, _q.ctx, ent.OpQueryOnly)) + if err != nil { + return nil, err + } + switch len(nodes) { + case 1: + return nodes[0], nil + case 0: + return nil, &NotFoundError{template.Label} + default: + return nil, &NotSingularError{template.Label} + } +} + +// OnlyX is like Only, but panics if an error occurs. +func (_q *TemplateQuery) OnlyX(ctx context.Context) *Template { + node, err := _q.Only(ctx) + if err != nil { + panic(err) + } + return node +} + +// OnlyID is like Only, but returns the only Template ID in the query. +// Returns a *NotSingularError when more than one Template ID is found. +// Returns a *NotFoundError when no entities are found. +func (_q *TemplateQuery) OnlyID(ctx context.Context) (id uuid.UUID, err error) { + var ids []uuid.UUID + if ids, err = _q.Limit(2).IDs(setContextOp(ctx, _q.ctx, ent.OpQueryOnlyID)); err != nil { + return + } + switch len(ids) { + case 1: + id = ids[0] + case 0: + err = &NotFoundError{template.Label} + default: + err = &NotSingularError{template.Label} + } + return +} + +// OnlyIDX is like OnlyID, but panics if an error occurs. +func (_q *TemplateQuery) OnlyIDX(ctx context.Context) uuid.UUID { + id, err := _q.OnlyID(ctx) + if err != nil { + panic(err) + } + return id +} + +// All executes the query and returns a list of Templates. +func (_q *TemplateQuery) All(ctx context.Context) ([]*Template, error) { + ctx = setContextOp(ctx, _q.ctx, ent.OpQueryAll) + if err := _q.prepareQuery(ctx); err != nil { + return nil, err + } + qr := querierAll[[]*Template, *TemplateQuery]() + return withInterceptors[[]*Template](ctx, _q, qr, _q.inters) +} + +// AllX is like All, but panics if an error occurs. +func (_q *TemplateQuery) AllX(ctx context.Context) []*Template { + nodes, err := _q.All(ctx) + if err != nil { + panic(err) + } + return nodes +} + +// IDs executes the query and returns a list of Template IDs. +func (_q *TemplateQuery) IDs(ctx context.Context) (ids []uuid.UUID, err error) { + if _q.ctx.Unique == nil && _q.path != nil { + _q.Unique(true) + } + ctx = setContextOp(ctx, _q.ctx, ent.OpQueryIDs) + if err = _q.Select(template.FieldID).Scan(ctx, &ids); err != nil { + return nil, err + } + return ids, nil +} + +// IDsX is like IDs, but panics if an error occurs. +func (_q *TemplateQuery) IDsX(ctx context.Context) []uuid.UUID { + ids, err := _q.IDs(ctx) + if err != nil { + panic(err) + } + return ids +} + +// Count returns the count of the given query. +func (_q *TemplateQuery) Count(ctx context.Context) (int, error) { + ctx = setContextOp(ctx, _q.ctx, ent.OpQueryCount) + if err := _q.prepareQuery(ctx); err != nil { + return 0, err + } + return withInterceptors[int](ctx, _q, querierCount[*TemplateQuery](), _q.inters) +} + +// CountX is like Count, but panics if an error occurs. +func (_q *TemplateQuery) CountX(ctx context.Context) int { + count, err := _q.Count(ctx) + if err != nil { + panic(err) + } + return count +} + +// Exist returns true if the query has elements in the graph. +func (_q *TemplateQuery) Exist(ctx context.Context) (bool, error) { + ctx = setContextOp(ctx, _q.ctx, ent.OpQueryExist) + switch _, err := _q.FirstID(ctx); { + case IsNotFound(err): + return false, nil + case err != nil: + return false, fmt.Errorf("ent: check existence: %w", err) + default: + return true, nil + } +} + +// ExistX is like Exist, but panics if an error occurs. +func (_q *TemplateQuery) ExistX(ctx context.Context) bool { + exist, err := _q.Exist(ctx) + if err != nil { + panic(err) + } + return exist +} + +// Clone returns a duplicate of the TemplateQuery builder, including all associated steps. It can be +// used to prepare common query builders and use them differently after the clone is made. +func (_q *TemplateQuery) Clone() *TemplateQuery { + if _q == nil { + return nil + } + return &TemplateQuery{ + config: _q.config, + ctx: _q.ctx.Clone(), + order: append([]template.OrderOption{}, _q.order...), + inters: append([]Interceptor{}, _q.inters...), + predicates: append([]predicate.Template{}, _q.predicates...), + // clone intermediate query. + sql: _q.sql.Clone(), + path: _q.path, + } +} + +// GroupBy is used to group vertices by one or more fields/columns. +// It is often used with aggregate functions, like: count, max, mean, min, sum. +// +// Example: +// +// var v []struct { +// Name string `json:"name,omitempty"` +// Count int `json:"count,omitempty"` +// } +// +// client.Template.Query(). +// GroupBy(template.FieldName). +// Aggregate(ent.Count()). +// Scan(ctx, &v) +func (_q *TemplateQuery) GroupBy(field string, fields ...string) *TemplateGroupBy { + _q.ctx.Fields = append([]string{field}, fields...) + grbuild := &TemplateGroupBy{build: _q} + grbuild.flds = &_q.ctx.Fields + grbuild.label = template.Label + grbuild.scan = grbuild.Scan + return grbuild +} + +// Select allows the selection one or more fields/columns for the given query, +// instead of selecting all fields in the entity. +// +// Example: +// +// var v []struct { +// Name string `json:"name,omitempty"` +// } +// +// client.Template.Query(). +// Select(template.FieldName). +// Scan(ctx, &v) +func (_q *TemplateQuery) Select(fields ...string) *TemplateSelect { + _q.ctx.Fields = append(_q.ctx.Fields, fields...) + sbuild := &TemplateSelect{TemplateQuery: _q} + sbuild.label = template.Label + sbuild.flds, sbuild.scan = &_q.ctx.Fields, sbuild.Scan + return sbuild +} + +// Aggregate returns a TemplateSelect configured with the given aggregations. +func (_q *TemplateQuery) Aggregate(fns ...AggregateFunc) *TemplateSelect { + return _q.Select().Aggregate(fns...) +} + +func (_q *TemplateQuery) prepareQuery(ctx context.Context) error { + for _, inter := range _q.inters { + if inter == nil { + return fmt.Errorf("ent: uninitialized interceptor (forgotten import ent/runtime?)") + } + if trv, ok := inter.(Traverser); ok { + if err := trv.Traverse(ctx, _q); err != nil { + return err + } + } + } + for _, f := range _q.ctx.Fields { + if !template.ValidColumn(f) { + return &ValidationError{Name: f, err: fmt.Errorf("ent: invalid field %q for query", f)} + } + } + if _q.path != nil { + prev, err := _q.path(ctx) + if err != nil { + return err + } + _q.sql = prev + } + return nil +} + +func (_q *TemplateQuery) sqlAll(ctx context.Context, hooks ...queryHook) ([]*Template, error) { + var ( + nodes = []*Template{} + _spec = _q.querySpec() + ) + _spec.ScanValues = func(columns []string) ([]any, error) { + return (*Template).scanValues(nil, columns) + } + _spec.Assign = func(columns []string, values []any) error { + node := &Template{config: _q.config} + nodes = append(nodes, node) + return node.assignValues(columns, values) + } + if len(_q.modifiers) > 0 { + _spec.Modifiers = _q.modifiers + } + for i := range hooks { + hooks[i](ctx, _spec) + } + if err := sqlgraph.QueryNodes(ctx, _q.driver, _spec); err != nil { + return nil, err + } + if len(nodes) == 0 { + return nodes, nil + } + return nodes, nil +} + +func (_q *TemplateQuery) sqlCount(ctx context.Context) (int, error) { + _spec := _q.querySpec() + if len(_q.modifiers) > 0 { + _spec.Modifiers = _q.modifiers + } + _spec.Node.Columns = _q.ctx.Fields + if len(_q.ctx.Fields) > 0 { + _spec.Unique = _q.ctx.Unique != nil && *_q.ctx.Unique + } + return sqlgraph.CountNodes(ctx, _q.driver, _spec) +} + +func (_q *TemplateQuery) querySpec() *sqlgraph.QuerySpec { + _spec := sqlgraph.NewQuerySpec(template.Table, template.Columns, sqlgraph.NewFieldSpec(template.FieldID, field.TypeUUID)) + _spec.From = _q.sql + if unique := _q.ctx.Unique; unique != nil { + _spec.Unique = *unique + } else if _q.path != nil { + _spec.Unique = true + } + if fields := _q.ctx.Fields; len(fields) > 0 { + _spec.Node.Columns = make([]string, 0, len(fields)) + _spec.Node.Columns = append(_spec.Node.Columns, template.FieldID) + for i := range fields { + if fields[i] != template.FieldID { + _spec.Node.Columns = append(_spec.Node.Columns, fields[i]) + } + } + } + if ps := _q.predicates; len(ps) > 0 { + _spec.Predicate = func(selector *sql.Selector) { + for i := range ps { + ps[i](selector) + } + } + } + if limit := _q.ctx.Limit; limit != nil { + _spec.Limit = *limit + } + if offset := _q.ctx.Offset; offset != nil { + _spec.Offset = *offset + } + if ps := _q.order; len(ps) > 0 { + _spec.Order = func(selector *sql.Selector) { + for i := range ps { + ps[i](selector) + } + } + } + return _spec +} + +func (_q *TemplateQuery) sqlQuery(ctx context.Context) *sql.Selector { + builder := sql.Dialect(_q.driver.Dialect()) + t1 := builder.Table(template.Table) + columns := _q.ctx.Fields + if len(columns) == 0 { + columns = template.Columns + } + selector := builder.Select(t1.Columns(columns...)...).From(t1) + if _q.sql != nil { + selector = _q.sql + selector.Select(selector.Columns(columns...)...) + } + if _q.ctx.Unique != nil && *_q.ctx.Unique { + selector.Distinct() + } + for _, m := range _q.modifiers { + m(selector) + } + for _, p := range _q.predicates { + p(selector) + } + for _, p := range _q.order { + p(selector) + } + if offset := _q.ctx.Offset; offset != nil { + // limit is mandatory for offset clause. We start + // with default value, and override it below if needed. + selector.Offset(*offset).Limit(math.MaxInt32) + } + if limit := _q.ctx.Limit; limit != nil { + selector.Limit(*limit) + } + return selector +} + +// ForUpdate locks the selected rows against concurrent updates, and prevent them from being +// updated, deleted or "selected ... for update" by other sessions, until the transaction is +// either committed or rolled-back. +func (_q *TemplateQuery) ForUpdate(opts ...sql.LockOption) *TemplateQuery { + if _q.driver.Dialect() == dialect.Postgres { + _q.Unique(false) + } + _q.modifiers = append(_q.modifiers, func(s *sql.Selector) { + s.ForUpdate(opts...) + }) + return _q +} + +// ForShare behaves similarly to ForUpdate, except that it acquires a shared mode lock +// on any rows that are read. Other sessions can read the rows, but cannot modify them +// until your transaction commits. +func (_q *TemplateQuery) ForShare(opts ...sql.LockOption) *TemplateQuery { + if _q.driver.Dialect() == dialect.Postgres { + _q.Unique(false) + } + _q.modifiers = append(_q.modifiers, func(s *sql.Selector) { + s.ForShare(opts...) + }) + return _q +} + +// TemplateGroupBy is the group-by builder for Template entities. +type TemplateGroupBy struct { + selector + build *TemplateQuery +} + +// Aggregate adds the given aggregation functions to the group-by query. +func (_g *TemplateGroupBy) Aggregate(fns ...AggregateFunc) *TemplateGroupBy { + _g.fns = append(_g.fns, fns...) + return _g +} + +// Scan applies the selector query and scans the result into the given value. +func (_g *TemplateGroupBy) Scan(ctx context.Context, v any) error { + ctx = setContextOp(ctx, _g.build.ctx, ent.OpQueryGroupBy) + if err := _g.build.prepareQuery(ctx); err != nil { + return err + } + return scanWithInterceptors[*TemplateQuery, *TemplateGroupBy](ctx, _g.build, _g, _g.build.inters, v) +} + +func (_g *TemplateGroupBy) sqlScan(ctx context.Context, root *TemplateQuery, v any) error { + selector := root.sqlQuery(ctx).Select() + aggregation := make([]string, 0, len(_g.fns)) + for _, fn := range _g.fns { + aggregation = append(aggregation, fn(selector)) + } + if len(selector.SelectedColumns()) == 0 { + columns := make([]string, 0, len(*_g.flds)+len(_g.fns)) + for _, f := range *_g.flds { + columns = append(columns, selector.C(f)) + } + columns = append(columns, aggregation...) + selector.Select(columns...) + } + selector.GroupBy(selector.Columns(*_g.flds...)...) + if err := selector.Err(); err != nil { + return err + } + rows := &sql.Rows{} + query, args := selector.Query() + if err := _g.build.driver.Query(ctx, query, args, rows); err != nil { + return err + } + defer rows.Close() + return sql.ScanSlice(rows, v) +} + +// TemplateSelect is the builder for selecting fields of Template entities. +type TemplateSelect struct { + *TemplateQuery + selector +} + +// Aggregate adds the given aggregation functions to the selector query. +func (_s *TemplateSelect) Aggregate(fns ...AggregateFunc) *TemplateSelect { + _s.fns = append(_s.fns, fns...) + return _s +} + +// Scan applies the selector query and scans the result into the given value. +func (_s *TemplateSelect) Scan(ctx context.Context, v any) error { + ctx = setContextOp(ctx, _s.ctx, ent.OpQuerySelect) + if err := _s.prepareQuery(ctx); err != nil { + return err + } + return scanWithInterceptors[*TemplateQuery, *TemplateSelect](ctx, _s.TemplateQuery, _s, _s.inters, v) +} + +func (_s *TemplateSelect) sqlScan(ctx context.Context, root *TemplateQuery, v any) error { + selector := root.sqlQuery(ctx) + aggregation := make([]string, 0, len(_s.fns)) + for _, fn := range _s.fns { + aggregation = append(aggregation, fn(selector)) + } + switch n := len(*_s.selector.flds); { + case n == 0 && len(aggregation) > 0: + selector.Select(aggregation...) + case n != 0 && len(aggregation) > 0: + selector.AppendSelect(aggregation...) + } + rows := &sql.Rows{} + query, args := selector.Query() + if err := _s.driver.Query(ctx, query, args, rows); err != nil { + return err + } + defer rows.Close() + return sql.ScanSlice(rows, v) +} diff --git a/pkg/ent/template_update.go b/pkg/ent/template_update.go new file mode 100644 index 000000000..9fc883aea --- /dev/null +++ b/pkg/ent/template_update.go @@ -0,0 +1,1294 @@ +// Code generated by ent, DO NOT EDIT. + +package ent + +import ( + "context" + "errors" + "fmt" + "time" + + "entgo.io/ent/dialect/sql" + "entgo.io/ent/dialect/sql/sqlgraph" + "entgo.io/ent/schema/field" + "github.com/GoogleCloudPlatform/scion/pkg/ent/predicate" + "github.com/GoogleCloudPlatform/scion/pkg/ent/template" +) + +// TemplateUpdate is the builder for updating Template entities. +type TemplateUpdate struct { + config + hooks []Hook + mutation *TemplateMutation +} + +// Where appends a list predicates to the TemplateUpdate builder. +func (_u *TemplateUpdate) Where(ps ...predicate.Template) *TemplateUpdate { + _u.mutation.Where(ps...) + return _u +} + +// SetName sets the "name" field. +func (_u *TemplateUpdate) SetName(v string) *TemplateUpdate { + _u.mutation.SetName(v) + return _u +} + +// SetNillableName sets the "name" field if the given value is not nil. +func (_u *TemplateUpdate) SetNillableName(v *string) *TemplateUpdate { + if v != nil { + _u.SetName(*v) + } + return _u +} + +// SetSlug sets the "slug" field. +func (_u *TemplateUpdate) SetSlug(v string) *TemplateUpdate { + _u.mutation.SetSlug(v) + return _u +} + +// SetNillableSlug sets the "slug" field if the given value is not nil. +func (_u *TemplateUpdate) SetNillableSlug(v *string) *TemplateUpdate { + if v != nil { + _u.SetSlug(*v) + } + return _u +} + +// SetDisplayName sets the "display_name" field. +func (_u *TemplateUpdate) SetDisplayName(v string) *TemplateUpdate { + _u.mutation.SetDisplayName(v) + return _u +} + +// SetNillableDisplayName sets the "display_name" field if the given value is not nil. +func (_u *TemplateUpdate) SetNillableDisplayName(v *string) *TemplateUpdate { + if v != nil { + _u.SetDisplayName(*v) + } + return _u +} + +// ClearDisplayName clears the value of the "display_name" field. +func (_u *TemplateUpdate) ClearDisplayName() *TemplateUpdate { + _u.mutation.ClearDisplayName() + return _u +} + +// SetDescription sets the "description" field. +func (_u *TemplateUpdate) SetDescription(v string) *TemplateUpdate { + _u.mutation.SetDescription(v) + return _u +} + +// SetNillableDescription sets the "description" field if the given value is not nil. +func (_u *TemplateUpdate) SetNillableDescription(v *string) *TemplateUpdate { + if v != nil { + _u.SetDescription(*v) + } + return _u +} + +// ClearDescription clears the value of the "description" field. +func (_u *TemplateUpdate) ClearDescription() *TemplateUpdate { + _u.mutation.ClearDescription() + return _u +} + +// SetHarness sets the "harness" field. +func (_u *TemplateUpdate) SetHarness(v string) *TemplateUpdate { + _u.mutation.SetHarness(v) + return _u +} + +// SetNillableHarness sets the "harness" field if the given value is not nil. +func (_u *TemplateUpdate) SetNillableHarness(v *string) *TemplateUpdate { + if v != nil { + _u.SetHarness(*v) + } + return _u +} + +// SetDefaultHarnessConfig sets the "default_harness_config" field. +func (_u *TemplateUpdate) SetDefaultHarnessConfig(v string) *TemplateUpdate { + _u.mutation.SetDefaultHarnessConfig(v) + return _u +} + +// SetNillableDefaultHarnessConfig sets the "default_harness_config" field if the given value is not nil. +func (_u *TemplateUpdate) SetNillableDefaultHarnessConfig(v *string) *TemplateUpdate { + if v != nil { + _u.SetDefaultHarnessConfig(*v) + } + return _u +} + +// ClearDefaultHarnessConfig clears the value of the "default_harness_config" field. +func (_u *TemplateUpdate) ClearDefaultHarnessConfig() *TemplateUpdate { + _u.mutation.ClearDefaultHarnessConfig() + return _u +} + +// SetImage sets the "image" field. +func (_u *TemplateUpdate) SetImage(v string) *TemplateUpdate { + _u.mutation.SetImage(v) + return _u +} + +// SetNillableImage sets the "image" field if the given value is not nil. +func (_u *TemplateUpdate) SetNillableImage(v *string) *TemplateUpdate { + if v != nil { + _u.SetImage(*v) + } + return _u +} + +// ClearImage clears the value of the "image" field. +func (_u *TemplateUpdate) ClearImage() *TemplateUpdate { + _u.mutation.ClearImage() + return _u +} + +// SetConfig sets the "config" field. +func (_u *TemplateUpdate) SetConfig(v string) *TemplateUpdate { + _u.mutation.SetConfig(v) + return _u +} + +// SetNillableConfig sets the "config" field if the given value is not nil. +func (_u *TemplateUpdate) SetNillableConfig(v *string) *TemplateUpdate { + if v != nil { + _u.SetConfig(*v) + } + return _u +} + +// ClearConfig clears the value of the "config" field. +func (_u *TemplateUpdate) ClearConfig() *TemplateUpdate { + _u.mutation.ClearConfig() + return _u +} + +// SetContentHash sets the "content_hash" field. +func (_u *TemplateUpdate) SetContentHash(v string) *TemplateUpdate { + _u.mutation.SetContentHash(v) + return _u +} + +// SetNillableContentHash sets the "content_hash" field if the given value is not nil. +func (_u *TemplateUpdate) SetNillableContentHash(v *string) *TemplateUpdate { + if v != nil { + _u.SetContentHash(*v) + } + return _u +} + +// ClearContentHash clears the value of the "content_hash" field. +func (_u *TemplateUpdate) ClearContentHash() *TemplateUpdate { + _u.mutation.ClearContentHash() + return _u +} + +// SetScope sets the "scope" field. +func (_u *TemplateUpdate) SetScope(v string) *TemplateUpdate { + _u.mutation.SetScope(v) + return _u +} + +// SetNillableScope sets the "scope" field if the given value is not nil. +func (_u *TemplateUpdate) SetNillableScope(v *string) *TemplateUpdate { + if v != nil { + _u.SetScope(*v) + } + return _u +} + +// SetScopeID sets the "scope_id" field. +func (_u *TemplateUpdate) SetScopeID(v string) *TemplateUpdate { + _u.mutation.SetScopeID(v) + return _u +} + +// SetNillableScopeID sets the "scope_id" field if the given value is not nil. +func (_u *TemplateUpdate) SetNillableScopeID(v *string) *TemplateUpdate { + if v != nil { + _u.SetScopeID(*v) + } + return _u +} + +// ClearScopeID clears the value of the "scope_id" field. +func (_u *TemplateUpdate) ClearScopeID() *TemplateUpdate { + _u.mutation.ClearScopeID() + return _u +} + +// SetProjectID sets the "project_id" field. +func (_u *TemplateUpdate) SetProjectID(v string) *TemplateUpdate { + _u.mutation.SetProjectID(v) + return _u +} + +// SetNillableProjectID sets the "project_id" field if the given value is not nil. +func (_u *TemplateUpdate) SetNillableProjectID(v *string) *TemplateUpdate { + if v != nil { + _u.SetProjectID(*v) + } + return _u +} + +// ClearProjectID clears the value of the "project_id" field. +func (_u *TemplateUpdate) ClearProjectID() *TemplateUpdate { + _u.mutation.ClearProjectID() + return _u +} + +// SetStorageURI sets the "storage_uri" field. +func (_u *TemplateUpdate) SetStorageURI(v string) *TemplateUpdate { + _u.mutation.SetStorageURI(v) + return _u +} + +// SetNillableStorageURI sets the "storage_uri" field if the given value is not nil. +func (_u *TemplateUpdate) SetNillableStorageURI(v *string) *TemplateUpdate { + if v != nil { + _u.SetStorageURI(*v) + } + return _u +} + +// ClearStorageURI clears the value of the "storage_uri" field. +func (_u *TemplateUpdate) ClearStorageURI() *TemplateUpdate { + _u.mutation.ClearStorageURI() + return _u +} + +// SetStorageBucket sets the "storage_bucket" field. +func (_u *TemplateUpdate) SetStorageBucket(v string) *TemplateUpdate { + _u.mutation.SetStorageBucket(v) + return _u +} + +// SetNillableStorageBucket sets the "storage_bucket" field if the given value is not nil. +func (_u *TemplateUpdate) SetNillableStorageBucket(v *string) *TemplateUpdate { + if v != nil { + _u.SetStorageBucket(*v) + } + return _u +} + +// ClearStorageBucket clears the value of the "storage_bucket" field. +func (_u *TemplateUpdate) ClearStorageBucket() *TemplateUpdate { + _u.mutation.ClearStorageBucket() + return _u +} + +// SetStoragePath sets the "storage_path" field. +func (_u *TemplateUpdate) SetStoragePath(v string) *TemplateUpdate { + _u.mutation.SetStoragePath(v) + return _u +} + +// SetNillableStoragePath sets the "storage_path" field if the given value is not nil. +func (_u *TemplateUpdate) SetNillableStoragePath(v *string) *TemplateUpdate { + if v != nil { + _u.SetStoragePath(*v) + } + return _u +} + +// ClearStoragePath clears the value of the "storage_path" field. +func (_u *TemplateUpdate) ClearStoragePath() *TemplateUpdate { + _u.mutation.ClearStoragePath() + return _u +} + +// SetFiles sets the "files" field. +func (_u *TemplateUpdate) SetFiles(v string) *TemplateUpdate { + _u.mutation.SetFiles(v) + return _u +} + +// SetNillableFiles sets the "files" field if the given value is not nil. +func (_u *TemplateUpdate) SetNillableFiles(v *string) *TemplateUpdate { + if v != nil { + _u.SetFiles(*v) + } + return _u +} + +// ClearFiles clears the value of the "files" field. +func (_u *TemplateUpdate) ClearFiles() *TemplateUpdate { + _u.mutation.ClearFiles() + return _u +} + +// SetBaseTemplate sets the "base_template" field. +func (_u *TemplateUpdate) SetBaseTemplate(v string) *TemplateUpdate { + _u.mutation.SetBaseTemplate(v) + return _u +} + +// SetNillableBaseTemplate sets the "base_template" field if the given value is not nil. +func (_u *TemplateUpdate) SetNillableBaseTemplate(v *string) *TemplateUpdate { + if v != nil { + _u.SetBaseTemplate(*v) + } + return _u +} + +// ClearBaseTemplate clears the value of the "base_template" field. +func (_u *TemplateUpdate) ClearBaseTemplate() *TemplateUpdate { + _u.mutation.ClearBaseTemplate() + return _u +} + +// SetStatus sets the "status" field. +func (_u *TemplateUpdate) SetStatus(v template.Status) *TemplateUpdate { + _u.mutation.SetStatus(v) + return _u +} + +// SetNillableStatus sets the "status" field if the given value is not nil. +func (_u *TemplateUpdate) SetNillableStatus(v *template.Status) *TemplateUpdate { + if v != nil { + _u.SetStatus(*v) + } + return _u +} + +// SetOwnerID sets the "owner_id" field. +func (_u *TemplateUpdate) SetOwnerID(v string) *TemplateUpdate { + _u.mutation.SetOwnerID(v) + return _u +} + +// SetNillableOwnerID sets the "owner_id" field if the given value is not nil. +func (_u *TemplateUpdate) SetNillableOwnerID(v *string) *TemplateUpdate { + if v != nil { + _u.SetOwnerID(*v) + } + return _u +} + +// ClearOwnerID clears the value of the "owner_id" field. +func (_u *TemplateUpdate) ClearOwnerID() *TemplateUpdate { + _u.mutation.ClearOwnerID() + return _u +} + +// SetCreatedBy sets the "created_by" field. +func (_u *TemplateUpdate) SetCreatedBy(v string) *TemplateUpdate { + _u.mutation.SetCreatedBy(v) + return _u +} + +// SetNillableCreatedBy sets the "created_by" field if the given value is not nil. +func (_u *TemplateUpdate) SetNillableCreatedBy(v *string) *TemplateUpdate { + if v != nil { + _u.SetCreatedBy(*v) + } + return _u +} + +// ClearCreatedBy clears the value of the "created_by" field. +func (_u *TemplateUpdate) ClearCreatedBy() *TemplateUpdate { + _u.mutation.ClearCreatedBy() + return _u +} + +// SetUpdatedBy sets the "updated_by" field. +func (_u *TemplateUpdate) SetUpdatedBy(v string) *TemplateUpdate { + _u.mutation.SetUpdatedBy(v) + return _u +} + +// SetNillableUpdatedBy sets the "updated_by" field if the given value is not nil. +func (_u *TemplateUpdate) SetNillableUpdatedBy(v *string) *TemplateUpdate { + if v != nil { + _u.SetUpdatedBy(*v) + } + return _u +} + +// ClearUpdatedBy clears the value of the "updated_by" field. +func (_u *TemplateUpdate) ClearUpdatedBy() *TemplateUpdate { + _u.mutation.ClearUpdatedBy() + return _u +} + +// SetVisibility sets the "visibility" field. +func (_u *TemplateUpdate) SetVisibility(v string) *TemplateUpdate { + _u.mutation.SetVisibility(v) + return _u +} + +// SetNillableVisibility sets the "visibility" field if the given value is not nil. +func (_u *TemplateUpdate) SetNillableVisibility(v *string) *TemplateUpdate { + if v != nil { + _u.SetVisibility(*v) + } + return _u +} + +// SetUpdated sets the "updated" field. +func (_u *TemplateUpdate) SetUpdated(v time.Time) *TemplateUpdate { + _u.mutation.SetUpdated(v) + return _u +} + +// Mutation returns the TemplateMutation object of the builder. +func (_u *TemplateUpdate) Mutation() *TemplateMutation { + return _u.mutation +} + +// Save executes the query and returns the number of nodes affected by the update operation. +func (_u *TemplateUpdate) Save(ctx context.Context) (int, error) { + _u.defaults() + return withHooks(ctx, _u.sqlSave, _u.mutation, _u.hooks) +} + +// SaveX is like Save, but panics if an error occurs. +func (_u *TemplateUpdate) SaveX(ctx context.Context) int { + affected, err := _u.Save(ctx) + if err != nil { + panic(err) + } + return affected +} + +// Exec executes the query. +func (_u *TemplateUpdate) Exec(ctx context.Context) error { + _, err := _u.Save(ctx) + return err +} + +// ExecX is like Exec, but panics if an error occurs. +func (_u *TemplateUpdate) ExecX(ctx context.Context) { + if err := _u.Exec(ctx); err != nil { + panic(err) + } +} + +// defaults sets the default values of the builder before save. +func (_u *TemplateUpdate) defaults() { + if _, ok := _u.mutation.Updated(); !ok { + v := template.UpdateDefaultUpdated() + _u.mutation.SetUpdated(v) + } +} + +// check runs all checks and user-defined validators on the builder. +func (_u *TemplateUpdate) check() error { + if v, ok := _u.mutation.Name(); ok { + if err := template.NameValidator(v); err != nil { + return &ValidationError{Name: "name", err: fmt.Errorf(`ent: validator failed for field "Template.name": %w`, err)} + } + } + if v, ok := _u.mutation.Slug(); ok { + if err := template.SlugValidator(v); err != nil { + return &ValidationError{Name: "slug", err: fmt.Errorf(`ent: validator failed for field "Template.slug": %w`, err)} + } + } + if v, ok := _u.mutation.Status(); ok { + if err := template.StatusValidator(v); err != nil { + return &ValidationError{Name: "status", err: fmt.Errorf(`ent: validator failed for field "Template.status": %w`, err)} + } + } + return nil +} + +func (_u *TemplateUpdate) sqlSave(ctx context.Context) (_node int, err error) { + if err := _u.check(); err != nil { + return _node, err + } + _spec := sqlgraph.NewUpdateSpec(template.Table, template.Columns, sqlgraph.NewFieldSpec(template.FieldID, field.TypeUUID)) + if ps := _u.mutation.predicates; len(ps) > 0 { + _spec.Predicate = func(selector *sql.Selector) { + for i := range ps { + ps[i](selector) + } + } + } + if value, ok := _u.mutation.Name(); ok { + _spec.SetField(template.FieldName, field.TypeString, value) + } + if value, ok := _u.mutation.Slug(); ok { + _spec.SetField(template.FieldSlug, field.TypeString, value) + } + if value, ok := _u.mutation.DisplayName(); ok { + _spec.SetField(template.FieldDisplayName, field.TypeString, value) + } + if _u.mutation.DisplayNameCleared() { + _spec.ClearField(template.FieldDisplayName, field.TypeString) + } + if value, ok := _u.mutation.Description(); ok { + _spec.SetField(template.FieldDescription, field.TypeString, value) + } + if _u.mutation.DescriptionCleared() { + _spec.ClearField(template.FieldDescription, field.TypeString) + } + if value, ok := _u.mutation.Harness(); ok { + _spec.SetField(template.FieldHarness, field.TypeString, value) + } + if value, ok := _u.mutation.DefaultHarnessConfig(); ok { + _spec.SetField(template.FieldDefaultHarnessConfig, field.TypeString, value) + } + if _u.mutation.DefaultHarnessConfigCleared() { + _spec.ClearField(template.FieldDefaultHarnessConfig, field.TypeString) + } + if value, ok := _u.mutation.Image(); ok { + _spec.SetField(template.FieldImage, field.TypeString, value) + } + if _u.mutation.ImageCleared() { + _spec.ClearField(template.FieldImage, field.TypeString) + } + if value, ok := _u.mutation.Config(); ok { + _spec.SetField(template.FieldConfig, field.TypeString, value) + } + if _u.mutation.ConfigCleared() { + _spec.ClearField(template.FieldConfig, field.TypeString) + } + if value, ok := _u.mutation.ContentHash(); ok { + _spec.SetField(template.FieldContentHash, field.TypeString, value) + } + if _u.mutation.ContentHashCleared() { + _spec.ClearField(template.FieldContentHash, field.TypeString) + } + if value, ok := _u.mutation.Scope(); ok { + _spec.SetField(template.FieldScope, field.TypeString, value) + } + if value, ok := _u.mutation.ScopeID(); ok { + _spec.SetField(template.FieldScopeID, field.TypeString, value) + } + if _u.mutation.ScopeIDCleared() { + _spec.ClearField(template.FieldScopeID, field.TypeString) + } + if value, ok := _u.mutation.ProjectID(); ok { + _spec.SetField(template.FieldProjectID, field.TypeString, value) + } + if _u.mutation.ProjectIDCleared() { + _spec.ClearField(template.FieldProjectID, field.TypeString) + } + if value, ok := _u.mutation.StorageURI(); ok { + _spec.SetField(template.FieldStorageURI, field.TypeString, value) + } + if _u.mutation.StorageURICleared() { + _spec.ClearField(template.FieldStorageURI, field.TypeString) + } + if value, ok := _u.mutation.StorageBucket(); ok { + _spec.SetField(template.FieldStorageBucket, field.TypeString, value) + } + if _u.mutation.StorageBucketCleared() { + _spec.ClearField(template.FieldStorageBucket, field.TypeString) + } + if value, ok := _u.mutation.StoragePath(); ok { + _spec.SetField(template.FieldStoragePath, field.TypeString, value) + } + if _u.mutation.StoragePathCleared() { + _spec.ClearField(template.FieldStoragePath, field.TypeString) + } + if value, ok := _u.mutation.Files(); ok { + _spec.SetField(template.FieldFiles, field.TypeString, value) + } + if _u.mutation.FilesCleared() { + _spec.ClearField(template.FieldFiles, field.TypeString) + } + if value, ok := _u.mutation.BaseTemplate(); ok { + _spec.SetField(template.FieldBaseTemplate, field.TypeString, value) + } + if _u.mutation.BaseTemplateCleared() { + _spec.ClearField(template.FieldBaseTemplate, field.TypeString) + } + if value, ok := _u.mutation.Status(); ok { + _spec.SetField(template.FieldStatus, field.TypeEnum, value) + } + if value, ok := _u.mutation.OwnerID(); ok { + _spec.SetField(template.FieldOwnerID, field.TypeString, value) + } + if _u.mutation.OwnerIDCleared() { + _spec.ClearField(template.FieldOwnerID, field.TypeString) + } + if value, ok := _u.mutation.CreatedBy(); ok { + _spec.SetField(template.FieldCreatedBy, field.TypeString, value) + } + if _u.mutation.CreatedByCleared() { + _spec.ClearField(template.FieldCreatedBy, field.TypeString) + } + if value, ok := _u.mutation.UpdatedBy(); ok { + _spec.SetField(template.FieldUpdatedBy, field.TypeString, value) + } + if _u.mutation.UpdatedByCleared() { + _spec.ClearField(template.FieldUpdatedBy, field.TypeString) + } + if value, ok := _u.mutation.Visibility(); ok { + _spec.SetField(template.FieldVisibility, field.TypeString, value) + } + if value, ok := _u.mutation.Updated(); ok { + _spec.SetField(template.FieldUpdated, field.TypeTime, value) + } + if _node, err = sqlgraph.UpdateNodes(ctx, _u.driver, _spec); err != nil { + if _, ok := err.(*sqlgraph.NotFoundError); ok { + err = &NotFoundError{template.Label} + } else if sqlgraph.IsConstraintError(err) { + err = &ConstraintError{msg: err.Error(), wrap: err} + } + return 0, err + } + _u.mutation.done = true + return _node, nil +} + +// TemplateUpdateOne is the builder for updating a single Template entity. +type TemplateUpdateOne struct { + config + fields []string + hooks []Hook + mutation *TemplateMutation +} + +// SetName sets the "name" field. +func (_u *TemplateUpdateOne) SetName(v string) *TemplateUpdateOne { + _u.mutation.SetName(v) + return _u +} + +// SetNillableName sets the "name" field if the given value is not nil. +func (_u *TemplateUpdateOne) SetNillableName(v *string) *TemplateUpdateOne { + if v != nil { + _u.SetName(*v) + } + return _u +} + +// SetSlug sets the "slug" field. +func (_u *TemplateUpdateOne) SetSlug(v string) *TemplateUpdateOne { + _u.mutation.SetSlug(v) + return _u +} + +// SetNillableSlug sets the "slug" field if the given value is not nil. +func (_u *TemplateUpdateOne) SetNillableSlug(v *string) *TemplateUpdateOne { + if v != nil { + _u.SetSlug(*v) + } + return _u +} + +// SetDisplayName sets the "display_name" field. +func (_u *TemplateUpdateOne) SetDisplayName(v string) *TemplateUpdateOne { + _u.mutation.SetDisplayName(v) + return _u +} + +// SetNillableDisplayName sets the "display_name" field if the given value is not nil. +func (_u *TemplateUpdateOne) SetNillableDisplayName(v *string) *TemplateUpdateOne { + if v != nil { + _u.SetDisplayName(*v) + } + return _u +} + +// ClearDisplayName clears the value of the "display_name" field. +func (_u *TemplateUpdateOne) ClearDisplayName() *TemplateUpdateOne { + _u.mutation.ClearDisplayName() + return _u +} + +// SetDescription sets the "description" field. +func (_u *TemplateUpdateOne) SetDescription(v string) *TemplateUpdateOne { + _u.mutation.SetDescription(v) + return _u +} + +// SetNillableDescription sets the "description" field if the given value is not nil. +func (_u *TemplateUpdateOne) SetNillableDescription(v *string) *TemplateUpdateOne { + if v != nil { + _u.SetDescription(*v) + } + return _u +} + +// ClearDescription clears the value of the "description" field. +func (_u *TemplateUpdateOne) ClearDescription() *TemplateUpdateOne { + _u.mutation.ClearDescription() + return _u +} + +// SetHarness sets the "harness" field. +func (_u *TemplateUpdateOne) SetHarness(v string) *TemplateUpdateOne { + _u.mutation.SetHarness(v) + return _u +} + +// SetNillableHarness sets the "harness" field if the given value is not nil. +func (_u *TemplateUpdateOne) SetNillableHarness(v *string) *TemplateUpdateOne { + if v != nil { + _u.SetHarness(*v) + } + return _u +} + +// SetDefaultHarnessConfig sets the "default_harness_config" field. +func (_u *TemplateUpdateOne) SetDefaultHarnessConfig(v string) *TemplateUpdateOne { + _u.mutation.SetDefaultHarnessConfig(v) + return _u +} + +// SetNillableDefaultHarnessConfig sets the "default_harness_config" field if the given value is not nil. +func (_u *TemplateUpdateOne) SetNillableDefaultHarnessConfig(v *string) *TemplateUpdateOne { + if v != nil { + _u.SetDefaultHarnessConfig(*v) + } + return _u +} + +// ClearDefaultHarnessConfig clears the value of the "default_harness_config" field. +func (_u *TemplateUpdateOne) ClearDefaultHarnessConfig() *TemplateUpdateOne { + _u.mutation.ClearDefaultHarnessConfig() + return _u +} + +// SetImage sets the "image" field. +func (_u *TemplateUpdateOne) SetImage(v string) *TemplateUpdateOne { + _u.mutation.SetImage(v) + return _u +} + +// SetNillableImage sets the "image" field if the given value is not nil. +func (_u *TemplateUpdateOne) SetNillableImage(v *string) *TemplateUpdateOne { + if v != nil { + _u.SetImage(*v) + } + return _u +} + +// ClearImage clears the value of the "image" field. +func (_u *TemplateUpdateOne) ClearImage() *TemplateUpdateOne { + _u.mutation.ClearImage() + return _u +} + +// SetConfig sets the "config" field. +func (_u *TemplateUpdateOne) SetConfig(v string) *TemplateUpdateOne { + _u.mutation.SetConfig(v) + return _u +} + +// SetNillableConfig sets the "config" field if the given value is not nil. +func (_u *TemplateUpdateOne) SetNillableConfig(v *string) *TemplateUpdateOne { + if v != nil { + _u.SetConfig(*v) + } + return _u +} + +// ClearConfig clears the value of the "config" field. +func (_u *TemplateUpdateOne) ClearConfig() *TemplateUpdateOne { + _u.mutation.ClearConfig() + return _u +} + +// SetContentHash sets the "content_hash" field. +func (_u *TemplateUpdateOne) SetContentHash(v string) *TemplateUpdateOne { + _u.mutation.SetContentHash(v) + return _u +} + +// SetNillableContentHash sets the "content_hash" field if the given value is not nil. +func (_u *TemplateUpdateOne) SetNillableContentHash(v *string) *TemplateUpdateOne { + if v != nil { + _u.SetContentHash(*v) + } + return _u +} + +// ClearContentHash clears the value of the "content_hash" field. +func (_u *TemplateUpdateOne) ClearContentHash() *TemplateUpdateOne { + _u.mutation.ClearContentHash() + return _u +} + +// SetScope sets the "scope" field. +func (_u *TemplateUpdateOne) SetScope(v string) *TemplateUpdateOne { + _u.mutation.SetScope(v) + return _u +} + +// SetNillableScope sets the "scope" field if the given value is not nil. +func (_u *TemplateUpdateOne) SetNillableScope(v *string) *TemplateUpdateOne { + if v != nil { + _u.SetScope(*v) + } + return _u +} + +// SetScopeID sets the "scope_id" field. +func (_u *TemplateUpdateOne) SetScopeID(v string) *TemplateUpdateOne { + _u.mutation.SetScopeID(v) + return _u +} + +// SetNillableScopeID sets the "scope_id" field if the given value is not nil. +func (_u *TemplateUpdateOne) SetNillableScopeID(v *string) *TemplateUpdateOne { + if v != nil { + _u.SetScopeID(*v) + } + return _u +} + +// ClearScopeID clears the value of the "scope_id" field. +func (_u *TemplateUpdateOne) ClearScopeID() *TemplateUpdateOne { + _u.mutation.ClearScopeID() + return _u +} + +// SetProjectID sets the "project_id" field. +func (_u *TemplateUpdateOne) SetProjectID(v string) *TemplateUpdateOne { + _u.mutation.SetProjectID(v) + return _u +} + +// SetNillableProjectID sets the "project_id" field if the given value is not nil. +func (_u *TemplateUpdateOne) SetNillableProjectID(v *string) *TemplateUpdateOne { + if v != nil { + _u.SetProjectID(*v) + } + return _u +} + +// ClearProjectID clears the value of the "project_id" field. +func (_u *TemplateUpdateOne) ClearProjectID() *TemplateUpdateOne { + _u.mutation.ClearProjectID() + return _u +} + +// SetStorageURI sets the "storage_uri" field. +func (_u *TemplateUpdateOne) SetStorageURI(v string) *TemplateUpdateOne { + _u.mutation.SetStorageURI(v) + return _u +} + +// SetNillableStorageURI sets the "storage_uri" field if the given value is not nil. +func (_u *TemplateUpdateOne) SetNillableStorageURI(v *string) *TemplateUpdateOne { + if v != nil { + _u.SetStorageURI(*v) + } + return _u +} + +// ClearStorageURI clears the value of the "storage_uri" field. +func (_u *TemplateUpdateOne) ClearStorageURI() *TemplateUpdateOne { + _u.mutation.ClearStorageURI() + return _u +} + +// SetStorageBucket sets the "storage_bucket" field. +func (_u *TemplateUpdateOne) SetStorageBucket(v string) *TemplateUpdateOne { + _u.mutation.SetStorageBucket(v) + return _u +} + +// SetNillableStorageBucket sets the "storage_bucket" field if the given value is not nil. +func (_u *TemplateUpdateOne) SetNillableStorageBucket(v *string) *TemplateUpdateOne { + if v != nil { + _u.SetStorageBucket(*v) + } + return _u +} + +// ClearStorageBucket clears the value of the "storage_bucket" field. +func (_u *TemplateUpdateOne) ClearStorageBucket() *TemplateUpdateOne { + _u.mutation.ClearStorageBucket() + return _u +} + +// SetStoragePath sets the "storage_path" field. +func (_u *TemplateUpdateOne) SetStoragePath(v string) *TemplateUpdateOne { + _u.mutation.SetStoragePath(v) + return _u +} + +// SetNillableStoragePath sets the "storage_path" field if the given value is not nil. +func (_u *TemplateUpdateOne) SetNillableStoragePath(v *string) *TemplateUpdateOne { + if v != nil { + _u.SetStoragePath(*v) + } + return _u +} + +// ClearStoragePath clears the value of the "storage_path" field. +func (_u *TemplateUpdateOne) ClearStoragePath() *TemplateUpdateOne { + _u.mutation.ClearStoragePath() + return _u +} + +// SetFiles sets the "files" field. +func (_u *TemplateUpdateOne) SetFiles(v string) *TemplateUpdateOne { + _u.mutation.SetFiles(v) + return _u +} + +// SetNillableFiles sets the "files" field if the given value is not nil. +func (_u *TemplateUpdateOne) SetNillableFiles(v *string) *TemplateUpdateOne { + if v != nil { + _u.SetFiles(*v) + } + return _u +} + +// ClearFiles clears the value of the "files" field. +func (_u *TemplateUpdateOne) ClearFiles() *TemplateUpdateOne { + _u.mutation.ClearFiles() + return _u +} + +// SetBaseTemplate sets the "base_template" field. +func (_u *TemplateUpdateOne) SetBaseTemplate(v string) *TemplateUpdateOne { + _u.mutation.SetBaseTemplate(v) + return _u +} + +// SetNillableBaseTemplate sets the "base_template" field if the given value is not nil. +func (_u *TemplateUpdateOne) SetNillableBaseTemplate(v *string) *TemplateUpdateOne { + if v != nil { + _u.SetBaseTemplate(*v) + } + return _u +} + +// ClearBaseTemplate clears the value of the "base_template" field. +func (_u *TemplateUpdateOne) ClearBaseTemplate() *TemplateUpdateOne { + _u.mutation.ClearBaseTemplate() + return _u +} + +// SetStatus sets the "status" field. +func (_u *TemplateUpdateOne) SetStatus(v template.Status) *TemplateUpdateOne { + _u.mutation.SetStatus(v) + return _u +} + +// SetNillableStatus sets the "status" field if the given value is not nil. +func (_u *TemplateUpdateOne) SetNillableStatus(v *template.Status) *TemplateUpdateOne { + if v != nil { + _u.SetStatus(*v) + } + return _u +} + +// SetOwnerID sets the "owner_id" field. +func (_u *TemplateUpdateOne) SetOwnerID(v string) *TemplateUpdateOne { + _u.mutation.SetOwnerID(v) + return _u +} + +// SetNillableOwnerID sets the "owner_id" field if the given value is not nil. +func (_u *TemplateUpdateOne) SetNillableOwnerID(v *string) *TemplateUpdateOne { + if v != nil { + _u.SetOwnerID(*v) + } + return _u +} + +// ClearOwnerID clears the value of the "owner_id" field. +func (_u *TemplateUpdateOne) ClearOwnerID() *TemplateUpdateOne { + _u.mutation.ClearOwnerID() + return _u +} + +// SetCreatedBy sets the "created_by" field. +func (_u *TemplateUpdateOne) SetCreatedBy(v string) *TemplateUpdateOne { + _u.mutation.SetCreatedBy(v) + return _u +} + +// SetNillableCreatedBy sets the "created_by" field if the given value is not nil. +func (_u *TemplateUpdateOne) SetNillableCreatedBy(v *string) *TemplateUpdateOne { + if v != nil { + _u.SetCreatedBy(*v) + } + return _u +} + +// ClearCreatedBy clears the value of the "created_by" field. +func (_u *TemplateUpdateOne) ClearCreatedBy() *TemplateUpdateOne { + _u.mutation.ClearCreatedBy() + return _u +} + +// SetUpdatedBy sets the "updated_by" field. +func (_u *TemplateUpdateOne) SetUpdatedBy(v string) *TemplateUpdateOne { + _u.mutation.SetUpdatedBy(v) + return _u +} + +// SetNillableUpdatedBy sets the "updated_by" field if the given value is not nil. +func (_u *TemplateUpdateOne) SetNillableUpdatedBy(v *string) *TemplateUpdateOne { + if v != nil { + _u.SetUpdatedBy(*v) + } + return _u +} + +// ClearUpdatedBy clears the value of the "updated_by" field. +func (_u *TemplateUpdateOne) ClearUpdatedBy() *TemplateUpdateOne { + _u.mutation.ClearUpdatedBy() + return _u +} + +// SetVisibility sets the "visibility" field. +func (_u *TemplateUpdateOne) SetVisibility(v string) *TemplateUpdateOne { + _u.mutation.SetVisibility(v) + return _u +} + +// SetNillableVisibility sets the "visibility" field if the given value is not nil. +func (_u *TemplateUpdateOne) SetNillableVisibility(v *string) *TemplateUpdateOne { + if v != nil { + _u.SetVisibility(*v) + } + return _u +} + +// SetUpdated sets the "updated" field. +func (_u *TemplateUpdateOne) SetUpdated(v time.Time) *TemplateUpdateOne { + _u.mutation.SetUpdated(v) + return _u +} + +// Mutation returns the TemplateMutation object of the builder. +func (_u *TemplateUpdateOne) Mutation() *TemplateMutation { + return _u.mutation +} + +// Where appends a list predicates to the TemplateUpdate builder. +func (_u *TemplateUpdateOne) Where(ps ...predicate.Template) *TemplateUpdateOne { + _u.mutation.Where(ps...) + return _u +} + +// Select allows selecting one or more fields (columns) of the returned entity. +// The default is selecting all fields defined in the entity schema. +func (_u *TemplateUpdateOne) Select(field string, fields ...string) *TemplateUpdateOne { + _u.fields = append([]string{field}, fields...) + return _u +} + +// Save executes the query and returns the updated Template entity. +func (_u *TemplateUpdateOne) Save(ctx context.Context) (*Template, error) { + _u.defaults() + return withHooks(ctx, _u.sqlSave, _u.mutation, _u.hooks) +} + +// SaveX is like Save, but panics if an error occurs. +func (_u *TemplateUpdateOne) SaveX(ctx context.Context) *Template { + node, err := _u.Save(ctx) + if err != nil { + panic(err) + } + return node +} + +// Exec executes the query on the entity. +func (_u *TemplateUpdateOne) Exec(ctx context.Context) error { + _, err := _u.Save(ctx) + return err +} + +// ExecX is like Exec, but panics if an error occurs. +func (_u *TemplateUpdateOne) ExecX(ctx context.Context) { + if err := _u.Exec(ctx); err != nil { + panic(err) + } +} + +// defaults sets the default values of the builder before save. +func (_u *TemplateUpdateOne) defaults() { + if _, ok := _u.mutation.Updated(); !ok { + v := template.UpdateDefaultUpdated() + _u.mutation.SetUpdated(v) + } +} + +// check runs all checks and user-defined validators on the builder. +func (_u *TemplateUpdateOne) check() error { + if v, ok := _u.mutation.Name(); ok { + if err := template.NameValidator(v); err != nil { + return &ValidationError{Name: "name", err: fmt.Errorf(`ent: validator failed for field "Template.name": %w`, err)} + } + } + if v, ok := _u.mutation.Slug(); ok { + if err := template.SlugValidator(v); err != nil { + return &ValidationError{Name: "slug", err: fmt.Errorf(`ent: validator failed for field "Template.slug": %w`, err)} + } + } + if v, ok := _u.mutation.Status(); ok { + if err := template.StatusValidator(v); err != nil { + return &ValidationError{Name: "status", err: fmt.Errorf(`ent: validator failed for field "Template.status": %w`, err)} + } + } + return nil +} + +func (_u *TemplateUpdateOne) sqlSave(ctx context.Context) (_node *Template, err error) { + if err := _u.check(); err != nil { + return _node, err + } + _spec := sqlgraph.NewUpdateSpec(template.Table, template.Columns, sqlgraph.NewFieldSpec(template.FieldID, field.TypeUUID)) + id, ok := _u.mutation.ID() + if !ok { + return nil, &ValidationError{Name: "id", err: errors.New(`ent: missing "Template.id" for update`)} + } + _spec.Node.ID.Value = id + if fields := _u.fields; len(fields) > 0 { + _spec.Node.Columns = make([]string, 0, len(fields)) + _spec.Node.Columns = append(_spec.Node.Columns, template.FieldID) + for _, f := range fields { + if !template.ValidColumn(f) { + return nil, &ValidationError{Name: f, err: fmt.Errorf("ent: invalid field %q for query", f)} + } + if f != template.FieldID { + _spec.Node.Columns = append(_spec.Node.Columns, f) + } + } + } + if ps := _u.mutation.predicates; len(ps) > 0 { + _spec.Predicate = func(selector *sql.Selector) { + for i := range ps { + ps[i](selector) + } + } + } + if value, ok := _u.mutation.Name(); ok { + _spec.SetField(template.FieldName, field.TypeString, value) + } + if value, ok := _u.mutation.Slug(); ok { + _spec.SetField(template.FieldSlug, field.TypeString, value) + } + if value, ok := _u.mutation.DisplayName(); ok { + _spec.SetField(template.FieldDisplayName, field.TypeString, value) + } + if _u.mutation.DisplayNameCleared() { + _spec.ClearField(template.FieldDisplayName, field.TypeString) + } + if value, ok := _u.mutation.Description(); ok { + _spec.SetField(template.FieldDescription, field.TypeString, value) + } + if _u.mutation.DescriptionCleared() { + _spec.ClearField(template.FieldDescription, field.TypeString) + } + if value, ok := _u.mutation.Harness(); ok { + _spec.SetField(template.FieldHarness, field.TypeString, value) + } + if value, ok := _u.mutation.DefaultHarnessConfig(); ok { + _spec.SetField(template.FieldDefaultHarnessConfig, field.TypeString, value) + } + if _u.mutation.DefaultHarnessConfigCleared() { + _spec.ClearField(template.FieldDefaultHarnessConfig, field.TypeString) + } + if value, ok := _u.mutation.Image(); ok { + _spec.SetField(template.FieldImage, field.TypeString, value) + } + if _u.mutation.ImageCleared() { + _spec.ClearField(template.FieldImage, field.TypeString) + } + if value, ok := _u.mutation.Config(); ok { + _spec.SetField(template.FieldConfig, field.TypeString, value) + } + if _u.mutation.ConfigCleared() { + _spec.ClearField(template.FieldConfig, field.TypeString) + } + if value, ok := _u.mutation.ContentHash(); ok { + _spec.SetField(template.FieldContentHash, field.TypeString, value) + } + if _u.mutation.ContentHashCleared() { + _spec.ClearField(template.FieldContentHash, field.TypeString) + } + if value, ok := _u.mutation.Scope(); ok { + _spec.SetField(template.FieldScope, field.TypeString, value) + } + if value, ok := _u.mutation.ScopeID(); ok { + _spec.SetField(template.FieldScopeID, field.TypeString, value) + } + if _u.mutation.ScopeIDCleared() { + _spec.ClearField(template.FieldScopeID, field.TypeString) + } + if value, ok := _u.mutation.ProjectID(); ok { + _spec.SetField(template.FieldProjectID, field.TypeString, value) + } + if _u.mutation.ProjectIDCleared() { + _spec.ClearField(template.FieldProjectID, field.TypeString) + } + if value, ok := _u.mutation.StorageURI(); ok { + _spec.SetField(template.FieldStorageURI, field.TypeString, value) + } + if _u.mutation.StorageURICleared() { + _spec.ClearField(template.FieldStorageURI, field.TypeString) + } + if value, ok := _u.mutation.StorageBucket(); ok { + _spec.SetField(template.FieldStorageBucket, field.TypeString, value) + } + if _u.mutation.StorageBucketCleared() { + _spec.ClearField(template.FieldStorageBucket, field.TypeString) + } + if value, ok := _u.mutation.StoragePath(); ok { + _spec.SetField(template.FieldStoragePath, field.TypeString, value) + } + if _u.mutation.StoragePathCleared() { + _spec.ClearField(template.FieldStoragePath, field.TypeString) + } + if value, ok := _u.mutation.Files(); ok { + _spec.SetField(template.FieldFiles, field.TypeString, value) + } + if _u.mutation.FilesCleared() { + _spec.ClearField(template.FieldFiles, field.TypeString) + } + if value, ok := _u.mutation.BaseTemplate(); ok { + _spec.SetField(template.FieldBaseTemplate, field.TypeString, value) + } + if _u.mutation.BaseTemplateCleared() { + _spec.ClearField(template.FieldBaseTemplate, field.TypeString) + } + if value, ok := _u.mutation.Status(); ok { + _spec.SetField(template.FieldStatus, field.TypeEnum, value) + } + if value, ok := _u.mutation.OwnerID(); ok { + _spec.SetField(template.FieldOwnerID, field.TypeString, value) + } + if _u.mutation.OwnerIDCleared() { + _spec.ClearField(template.FieldOwnerID, field.TypeString) + } + if value, ok := _u.mutation.CreatedBy(); ok { + _spec.SetField(template.FieldCreatedBy, field.TypeString, value) + } + if _u.mutation.CreatedByCleared() { + _spec.ClearField(template.FieldCreatedBy, field.TypeString) + } + if value, ok := _u.mutation.UpdatedBy(); ok { + _spec.SetField(template.FieldUpdatedBy, field.TypeString, value) + } + if _u.mutation.UpdatedByCleared() { + _spec.ClearField(template.FieldUpdatedBy, field.TypeString) + } + if value, ok := _u.mutation.Visibility(); ok { + _spec.SetField(template.FieldVisibility, field.TypeString, value) + } + if value, ok := _u.mutation.Updated(); ok { + _spec.SetField(template.FieldUpdated, field.TypeTime, value) + } + _node = &Template{config: _u.config} + _spec.Assign = _node.assignValues + _spec.ScanValues = _node.scanValues + if err = sqlgraph.UpdateNode(ctx, _u.driver, _spec); err != nil { + if _, ok := err.(*sqlgraph.NotFoundError); ok { + err = &NotFoundError{template.Label} + } else if sqlgraph.IsConstraintError(err) { + err = &ConstraintError{msg: err.Error(), wrap: err} + } + return nil, err + } + _u.mutation.done = true + return _node, nil +} diff --git a/pkg/ent/tx.go b/pkg/ent/tx.go index c0b706ee4..64f55b502 100644 --- a/pkg/ent/tx.go +++ b/pkg/ent/tx.go @@ -16,16 +16,64 @@ type Tx struct { AccessPolicy *AccessPolicyClient // Agent is the client for interacting with the Agent builders. Agent *AgentClient + // AllowListEntry is the client for interacting with the AllowListEntry builders. + AllowListEntry *AllowListEntryClient + // ApiKey is the client for interacting with the ApiKey builders. + ApiKey *ApiKeyClient + // BrokerDispatch is the client for interacting with the BrokerDispatch builders. + BrokerDispatch *BrokerDispatchClient + // BrokerJoinToken is the client for interacting with the BrokerJoinToken builders. + BrokerJoinToken *BrokerJoinTokenClient + // BrokerSecret is the client for interacting with the BrokerSecret builders. + BrokerSecret *BrokerSecretClient + // EnvVar is the client for interacting with the EnvVar builders. + EnvVar *EnvVarClient + // GCPServiceAccount is the client for interacting with the GCPServiceAccount builders. + GCPServiceAccount *GCPServiceAccountClient + // GithubInstallation is the client for interacting with the GithubInstallation builders. + GithubInstallation *GithubInstallationClient // Group is the client for interacting with the Group builders. Group *GroupClient // GroupMembership is the client for interacting with the GroupMembership builders. GroupMembership *GroupMembershipClient + // HarnessConfig is the client for interacting with the HarnessConfig builders. + HarnessConfig *HarnessConfigClient + // InviteCode is the client for interacting with the InviteCode builders. + InviteCode *InviteCodeClient + // MaintenanceOperation is the client for interacting with the MaintenanceOperation builders. + MaintenanceOperation *MaintenanceOperationClient + // MaintenanceOperationRun is the client for interacting with the MaintenanceOperationRun builders. + MaintenanceOperationRun *MaintenanceOperationRunClient + // Message is the client for interacting with the Message builders. + Message *MessageClient + // Notification is the client for interacting with the Notification builders. + Notification *NotificationClient + // NotificationSubscription is the client for interacting with the NotificationSubscription builders. + NotificationSubscription *NotificationSubscriptionClient // PolicyBinding is the client for interacting with the PolicyBinding builders. PolicyBinding *PolicyBindingClient // Project is the client for interacting with the Project builders. Project *ProjectClient + // ProjectContributor is the client for interacting with the ProjectContributor builders. + ProjectContributor *ProjectContributorClient + // ProjectSyncState is the client for interacting with the ProjectSyncState builders. + ProjectSyncState *ProjectSyncStateClient + // RuntimeBroker is the client for interacting with the RuntimeBroker builders. + RuntimeBroker *RuntimeBrokerClient + // Schedule is the client for interacting with the Schedule builders. + Schedule *ScheduleClient + // ScheduledEvent is the client for interacting with the ScheduledEvent builders. + ScheduledEvent *ScheduledEventClient + // Secret is the client for interacting with the Secret builders. + Secret *SecretClient + // SubscriptionTemplate is the client for interacting with the SubscriptionTemplate builders. + SubscriptionTemplate *SubscriptionTemplateClient + // Template is the client for interacting with the Template builders. + Template *TemplateClient // User is the client for interacting with the User builders. User *UserClient + // UserAccessToken is the client for interacting with the UserAccessToken builders. + UserAccessToken *UserAccessTokenClient // lazily loaded. client *Client @@ -159,11 +207,35 @@ func (tx *Tx) Client() *Client { func (tx *Tx) init() { tx.AccessPolicy = NewAccessPolicyClient(tx.config) tx.Agent = NewAgentClient(tx.config) + tx.AllowListEntry = NewAllowListEntryClient(tx.config) + tx.ApiKey = NewApiKeyClient(tx.config) + tx.BrokerDispatch = NewBrokerDispatchClient(tx.config) + tx.BrokerJoinToken = NewBrokerJoinTokenClient(tx.config) + tx.BrokerSecret = NewBrokerSecretClient(tx.config) + tx.EnvVar = NewEnvVarClient(tx.config) + tx.GCPServiceAccount = NewGCPServiceAccountClient(tx.config) + tx.GithubInstallation = NewGithubInstallationClient(tx.config) tx.Group = NewGroupClient(tx.config) tx.GroupMembership = NewGroupMembershipClient(tx.config) + tx.HarnessConfig = NewHarnessConfigClient(tx.config) + tx.InviteCode = NewInviteCodeClient(tx.config) + tx.MaintenanceOperation = NewMaintenanceOperationClient(tx.config) + tx.MaintenanceOperationRun = NewMaintenanceOperationRunClient(tx.config) + tx.Message = NewMessageClient(tx.config) + tx.Notification = NewNotificationClient(tx.config) + tx.NotificationSubscription = NewNotificationSubscriptionClient(tx.config) tx.PolicyBinding = NewPolicyBindingClient(tx.config) tx.Project = NewProjectClient(tx.config) + tx.ProjectContributor = NewProjectContributorClient(tx.config) + tx.ProjectSyncState = NewProjectSyncStateClient(tx.config) + tx.RuntimeBroker = NewRuntimeBrokerClient(tx.config) + tx.Schedule = NewScheduleClient(tx.config) + tx.ScheduledEvent = NewScheduledEventClient(tx.config) + tx.Secret = NewSecretClient(tx.config) + tx.SubscriptionTemplate = NewSubscriptionTemplateClient(tx.config) + tx.Template = NewTemplateClient(tx.config) tx.User = NewUserClient(tx.config) + tx.UserAccessToken = NewUserAccessTokenClient(tx.config) } // txDriver wraps the given dialect.Tx with a nop dialect.Driver implementation. diff --git a/pkg/ent/user.go b/pkg/ent/user.go index f116e9fcf..8b94ec8fc 100644 --- a/pkg/ent/user.go +++ b/pkg/ent/user.go @@ -36,6 +36,8 @@ type User struct { Created time.Time `json:"created,omitempty"` // LastLogin holds the value of the "last_login" field. LastLogin *time.Time `json:"last_login,omitempty"` + // LastSeen holds the value of the "last_seen" field. + LastSeen *time.Time `json:"last_seen,omitempty"` // Edges holds the relations/edges for other nodes in the graph. // The values are being populated by the UserQuery when eager-loading is set. Edges UserEdges `json:"edges"` @@ -44,10 +46,6 @@ type User struct { // UserEdges holds the relations/edges for other nodes in the graph. type UserEdges struct { - // CreatedAgents holds the value of the created_agents edge. - CreatedAgents []*Agent `json:"created_agents,omitempty"` - // OwnedAgents holds the value of the owned_agents edge. - OwnedAgents []*Agent `json:"owned_agents,omitempty"` // OwnedGroups holds the value of the owned_groups edge. OwnedGroups []*Group `json:"owned_groups,omitempty"` // Memberships holds the value of the memberships edge. @@ -56,31 +54,13 @@ type UserEdges struct { PolicyBindings []*PolicyBinding `json:"policy_bindings,omitempty"` // loadedTypes holds the information for reporting if a // type was loaded (or requested) in eager-loading or not. - loadedTypes [5]bool -} - -// CreatedAgentsOrErr returns the CreatedAgents value or an error if the edge -// was not loaded in eager-loading. -func (e UserEdges) CreatedAgentsOrErr() ([]*Agent, error) { - if e.loadedTypes[0] { - return e.CreatedAgents, nil - } - return nil, &NotLoadedError{edge: "created_agents"} -} - -// OwnedAgentsOrErr returns the OwnedAgents value or an error if the edge -// was not loaded in eager-loading. -func (e UserEdges) OwnedAgentsOrErr() ([]*Agent, error) { - if e.loadedTypes[1] { - return e.OwnedAgents, nil - } - return nil, &NotLoadedError{edge: "owned_agents"} + loadedTypes [3]bool } // OwnedGroupsOrErr returns the OwnedGroups value or an error if the edge // was not loaded in eager-loading. func (e UserEdges) OwnedGroupsOrErr() ([]*Group, error) { - if e.loadedTypes[2] { + if e.loadedTypes[0] { return e.OwnedGroups, nil } return nil, &NotLoadedError{edge: "owned_groups"} @@ -89,7 +69,7 @@ func (e UserEdges) OwnedGroupsOrErr() ([]*Group, error) { // MembershipsOrErr returns the Memberships value or an error if the edge // was not loaded in eager-loading. func (e UserEdges) MembershipsOrErr() ([]*GroupMembership, error) { - if e.loadedTypes[3] { + if e.loadedTypes[1] { return e.Memberships, nil } return nil, &NotLoadedError{edge: "memberships"} @@ -98,7 +78,7 @@ func (e UserEdges) MembershipsOrErr() ([]*GroupMembership, error) { // PolicyBindingsOrErr returns the PolicyBindings value or an error if the edge // was not loaded in eager-loading. func (e UserEdges) PolicyBindingsOrErr() ([]*PolicyBinding, error) { - if e.loadedTypes[4] { + if e.loadedTypes[2] { return e.PolicyBindings, nil } return nil, &NotLoadedError{edge: "policy_bindings"} @@ -113,7 +93,7 @@ func (*User) scanValues(columns []string) ([]any, error) { values[i] = new([]byte) case user.FieldEmail, user.FieldDisplayName, user.FieldAvatarURL, user.FieldRole, user.FieldStatus: values[i] = new(sql.NullString) - case user.FieldCreated, user.FieldLastLogin: + case user.FieldCreated, user.FieldLastLogin, user.FieldLastSeen: values[i] = new(sql.NullTime) case user.FieldID: values[i] = new(uuid.UUID) @@ -189,6 +169,13 @@ func (_m *User) assignValues(columns []string, values []any) error { _m.LastLogin = new(time.Time) *_m.LastLogin = value.Time } + case user.FieldLastSeen: + if value, ok := values[i].(*sql.NullTime); !ok { + return fmt.Errorf("unexpected type %T for field last_seen", values[i]) + } else if value.Valid { + _m.LastSeen = new(time.Time) + *_m.LastSeen = value.Time + } default: _m.selectValues.Set(columns[i], values[i]) } @@ -202,16 +189,6 @@ func (_m *User) Value(name string) (ent.Value, error) { return _m.selectValues.Get(name) } -// QueryCreatedAgents queries the "created_agents" edge of the User entity. -func (_m *User) QueryCreatedAgents() *AgentQuery { - return NewUserClient(_m.config).QueryCreatedAgents(_m) -} - -// QueryOwnedAgents queries the "owned_agents" edge of the User entity. -func (_m *User) QueryOwnedAgents() *AgentQuery { - return NewUserClient(_m.config).QueryOwnedAgents(_m) -} - // QueryOwnedGroups queries the "owned_groups" edge of the User entity. func (_m *User) QueryOwnedGroups() *GroupQuery { return NewUserClient(_m.config).QueryOwnedGroups(_m) @@ -275,6 +252,11 @@ func (_m *User) String() string { builder.WriteString("last_login=") builder.WriteString(v.Format(time.ANSIC)) } + builder.WriteString(", ") + if v := _m.LastSeen; v != nil { + builder.WriteString("last_seen=") + builder.WriteString(v.Format(time.ANSIC)) + } builder.WriteByte(')') return builder.String() } diff --git a/pkg/ent/user/user.go b/pkg/ent/user/user.go index 72a6d4e68..74196d6db 100644 --- a/pkg/ent/user/user.go +++ b/pkg/ent/user/user.go @@ -32,10 +32,8 @@ const ( FieldCreated = "created" // FieldLastLogin holds the string denoting the last_login field in the database. FieldLastLogin = "last_login" - // EdgeCreatedAgents holds the string denoting the created_agents edge name in mutations. - EdgeCreatedAgents = "created_agents" - // EdgeOwnedAgents holds the string denoting the owned_agents edge name in mutations. - EdgeOwnedAgents = "owned_agents" + // FieldLastSeen holds the string denoting the last_seen field in the database. + FieldLastSeen = "last_seen" // EdgeOwnedGroups holds the string denoting the owned_groups edge name in mutations. EdgeOwnedGroups = "owned_groups" // EdgeMemberships holds the string denoting the memberships edge name in mutations. @@ -44,20 +42,6 @@ const ( EdgePolicyBindings = "policy_bindings" // Table holds the table name of the user in the database. Table = "users" - // CreatedAgentsTable is the table that holds the created_agents relation/edge. - CreatedAgentsTable = "agents" - // CreatedAgentsInverseTable is the table name for the Agent entity. - // It exists in this package in order to avoid circular dependency with the "agent" package. - CreatedAgentsInverseTable = "agents" - // CreatedAgentsColumn is the table column denoting the created_agents relation/edge. - CreatedAgentsColumn = "created_by" - // OwnedAgentsTable is the table that holds the owned_agents relation/edge. - OwnedAgentsTable = "agents" - // OwnedAgentsInverseTable is the table name for the Agent entity. - // It exists in this package in order to avoid circular dependency with the "agent" package. - OwnedAgentsInverseTable = "agents" - // OwnedAgentsColumn is the table column denoting the owned_agents relation/edge. - OwnedAgentsColumn = "owner_id" // OwnedGroupsTable is the table that holds the owned_groups relation/edge. OwnedGroupsTable = "groups" // OwnedGroupsInverseTable is the table name for the Group entity. @@ -92,6 +76,7 @@ var Columns = []string{ FieldPreferences, FieldCreated, FieldLastLogin, + FieldLastSeen, } // ValidColumn reports if the column name is valid (part of the table columns). @@ -107,8 +92,6 @@ func ValidColumn(column string) bool { var ( // EmailValidator is a validator for the "email" field. It is called by the builders before save. EmailValidator func(string) error - // DisplayNameValidator is a validator for the "display_name" field. It is called by the builders before save. - DisplayNameValidator func(string) error // DefaultCreated holds the default value on creation for the "created" field. DefaultCreated func() time.Time // DefaultID holds the default value on creation for the "id" field. @@ -211,32 +194,9 @@ func ByLastLogin(opts ...sql.OrderTermOption) OrderOption { return sql.OrderByField(FieldLastLogin, opts...).ToFunc() } -// ByCreatedAgentsCount orders the results by created_agents count. -func ByCreatedAgentsCount(opts ...sql.OrderTermOption) OrderOption { - return func(s *sql.Selector) { - sqlgraph.OrderByNeighborsCount(s, newCreatedAgentsStep(), opts...) - } -} - -// ByCreatedAgents orders the results by created_agents terms. -func ByCreatedAgents(term sql.OrderTerm, terms ...sql.OrderTerm) OrderOption { - return func(s *sql.Selector) { - sqlgraph.OrderByNeighborTerms(s, newCreatedAgentsStep(), append([]sql.OrderTerm{term}, terms...)...) - } -} - -// ByOwnedAgentsCount orders the results by owned_agents count. -func ByOwnedAgentsCount(opts ...sql.OrderTermOption) OrderOption { - return func(s *sql.Selector) { - sqlgraph.OrderByNeighborsCount(s, newOwnedAgentsStep(), opts...) - } -} - -// ByOwnedAgents orders the results by owned_agents terms. -func ByOwnedAgents(term sql.OrderTerm, terms ...sql.OrderTerm) OrderOption { - return func(s *sql.Selector) { - sqlgraph.OrderByNeighborTerms(s, newOwnedAgentsStep(), append([]sql.OrderTerm{term}, terms...)...) - } +// ByLastSeen orders the results by the last_seen field. +func ByLastSeen(opts ...sql.OrderTermOption) OrderOption { + return sql.OrderByField(FieldLastSeen, opts...).ToFunc() } // ByOwnedGroupsCount orders the results by owned_groups count. @@ -280,20 +240,6 @@ func ByPolicyBindings(term sql.OrderTerm, terms ...sql.OrderTerm) OrderOption { sqlgraph.OrderByNeighborTerms(s, newPolicyBindingsStep(), append([]sql.OrderTerm{term}, terms...)...) } } -func newCreatedAgentsStep() *sqlgraph.Step { - return sqlgraph.NewStep( - sqlgraph.From(Table, FieldID), - sqlgraph.To(CreatedAgentsInverseTable, FieldID), - sqlgraph.Edge(sqlgraph.O2M, false, CreatedAgentsTable, CreatedAgentsColumn), - ) -} -func newOwnedAgentsStep() *sqlgraph.Step { - return sqlgraph.NewStep( - sqlgraph.From(Table, FieldID), - sqlgraph.To(OwnedAgentsInverseTable, FieldID), - sqlgraph.Edge(sqlgraph.O2M, false, OwnedAgentsTable, OwnedAgentsColumn), - ) -} func newOwnedGroupsStep() *sqlgraph.Step { return sqlgraph.NewStep( sqlgraph.From(Table, FieldID), diff --git a/pkg/ent/user/where.go b/pkg/ent/user/where.go index 6bf80afb2..575196b7b 100644 --- a/pkg/ent/user/where.go +++ b/pkg/ent/user/where.go @@ -81,6 +81,11 @@ func LastLogin(v time.Time) predicate.User { return predicate.User(sql.FieldEQ(FieldLastLogin, v)) } +// LastSeen applies equality check predicate on the "last_seen" field. It's identical to LastSeenEQ. +func LastSeen(v time.Time) predicate.User { + return predicate.User(sql.FieldEQ(FieldLastSeen, v)) +} + // EmailEQ applies the EQ predicate on the "email" field. func EmailEQ(v string) predicate.User { return predicate.User(sql.FieldEQ(FieldEmail, v)) @@ -426,50 +431,54 @@ func LastLoginNotNil() predicate.User { return predicate.User(sql.FieldNotNull(FieldLastLogin)) } -// HasCreatedAgents applies the HasEdge predicate on the "created_agents" edge. -func HasCreatedAgents() predicate.User { - return predicate.User(func(s *sql.Selector) { - step := sqlgraph.NewStep( - sqlgraph.From(Table, FieldID), - sqlgraph.Edge(sqlgraph.O2M, false, CreatedAgentsTable, CreatedAgentsColumn), - ) - sqlgraph.HasNeighbors(s, step) - }) +// LastSeenEQ applies the EQ predicate on the "last_seen" field. +func LastSeenEQ(v time.Time) predicate.User { + return predicate.User(sql.FieldEQ(FieldLastSeen, v)) } -// HasCreatedAgentsWith applies the HasEdge predicate on the "created_agents" edge with a given conditions (other predicates). -func HasCreatedAgentsWith(preds ...predicate.Agent) predicate.User { - return predicate.User(func(s *sql.Selector) { - step := newCreatedAgentsStep() - sqlgraph.HasNeighborsWith(s, step, func(s *sql.Selector) { - for _, p := range preds { - p(s) - } - }) - }) +// LastSeenNEQ applies the NEQ predicate on the "last_seen" field. +func LastSeenNEQ(v time.Time) predicate.User { + return predicate.User(sql.FieldNEQ(FieldLastSeen, v)) } -// HasOwnedAgents applies the HasEdge predicate on the "owned_agents" edge. -func HasOwnedAgents() predicate.User { - return predicate.User(func(s *sql.Selector) { - step := sqlgraph.NewStep( - sqlgraph.From(Table, FieldID), - sqlgraph.Edge(sqlgraph.O2M, false, OwnedAgentsTable, OwnedAgentsColumn), - ) - sqlgraph.HasNeighbors(s, step) - }) +// LastSeenIn applies the In predicate on the "last_seen" field. +func LastSeenIn(vs ...time.Time) predicate.User { + return predicate.User(sql.FieldIn(FieldLastSeen, vs...)) } -// HasOwnedAgentsWith applies the HasEdge predicate on the "owned_agents" edge with a given conditions (other predicates). -func HasOwnedAgentsWith(preds ...predicate.Agent) predicate.User { - return predicate.User(func(s *sql.Selector) { - step := newOwnedAgentsStep() - sqlgraph.HasNeighborsWith(s, step, func(s *sql.Selector) { - for _, p := range preds { - p(s) - } - }) - }) +// LastSeenNotIn applies the NotIn predicate on the "last_seen" field. +func LastSeenNotIn(vs ...time.Time) predicate.User { + return predicate.User(sql.FieldNotIn(FieldLastSeen, vs...)) +} + +// LastSeenGT applies the GT predicate on the "last_seen" field. +func LastSeenGT(v time.Time) predicate.User { + return predicate.User(sql.FieldGT(FieldLastSeen, v)) +} + +// LastSeenGTE applies the GTE predicate on the "last_seen" field. +func LastSeenGTE(v time.Time) predicate.User { + return predicate.User(sql.FieldGTE(FieldLastSeen, v)) +} + +// LastSeenLT applies the LT predicate on the "last_seen" field. +func LastSeenLT(v time.Time) predicate.User { + return predicate.User(sql.FieldLT(FieldLastSeen, v)) +} + +// LastSeenLTE applies the LTE predicate on the "last_seen" field. +func LastSeenLTE(v time.Time) predicate.User { + return predicate.User(sql.FieldLTE(FieldLastSeen, v)) +} + +// LastSeenIsNil applies the IsNil predicate on the "last_seen" field. +func LastSeenIsNil() predicate.User { + return predicate.User(sql.FieldIsNull(FieldLastSeen)) +} + +// LastSeenNotNil applies the NotNil predicate on the "last_seen" field. +func LastSeenNotNil() predicate.User { + return predicate.User(sql.FieldNotNull(FieldLastSeen)) } // HasOwnedGroups applies the HasEdge predicate on the "owned_groups" edge. diff --git a/pkg/ent/user_create.go b/pkg/ent/user_create.go index f07c0d0e1..1572072e4 100644 --- a/pkg/ent/user_create.go +++ b/pkg/ent/user_create.go @@ -8,9 +8,10 @@ import ( "fmt" "time" + "entgo.io/ent/dialect" + "entgo.io/ent/dialect/sql" "entgo.io/ent/dialect/sql/sqlgraph" "entgo.io/ent/schema/field" - "github.com/GoogleCloudPlatform/scion/pkg/ent/agent" "github.com/GoogleCloudPlatform/scion/pkg/ent/group" "github.com/GoogleCloudPlatform/scion/pkg/ent/groupmembership" "github.com/GoogleCloudPlatform/scion/pkg/ent/policybinding" @@ -24,6 +25,7 @@ type UserCreate struct { config mutation *UserMutation hooks []Hook + conflict []sql.ConflictOption } // SetEmail sets the "email" field. @@ -114,50 +116,34 @@ func (_c *UserCreate) SetNillableLastLogin(v *time.Time) *UserCreate { return _c } -// SetID sets the "id" field. -func (_c *UserCreate) SetID(v uuid.UUID) *UserCreate { - _c.mutation.SetID(v) +// SetLastSeen sets the "last_seen" field. +func (_c *UserCreate) SetLastSeen(v time.Time) *UserCreate { + _c.mutation.SetLastSeen(v) return _c } -// SetNillableID sets the "id" field if the given value is not nil. -func (_c *UserCreate) SetNillableID(v *uuid.UUID) *UserCreate { +// SetNillableLastSeen sets the "last_seen" field if the given value is not nil. +func (_c *UserCreate) SetNillableLastSeen(v *time.Time) *UserCreate { if v != nil { - _c.SetID(*v) + _c.SetLastSeen(*v) } return _c } -// AddCreatedAgentIDs adds the "created_agents" edge to the Agent entity by IDs. -func (_c *UserCreate) AddCreatedAgentIDs(ids ...uuid.UUID) *UserCreate { - _c.mutation.AddCreatedAgentIDs(ids...) +// SetID sets the "id" field. +func (_c *UserCreate) SetID(v uuid.UUID) *UserCreate { + _c.mutation.SetID(v) return _c } -// AddCreatedAgents adds the "created_agents" edges to the Agent entity. -func (_c *UserCreate) AddCreatedAgents(v ...*Agent) *UserCreate { - ids := make([]uuid.UUID, len(v)) - for i := range v { - ids[i] = v[i].ID +// SetNillableID sets the "id" field if the given value is not nil. +func (_c *UserCreate) SetNillableID(v *uuid.UUID) *UserCreate { + if v != nil { + _c.SetID(*v) } - return _c.AddCreatedAgentIDs(ids...) -} - -// AddOwnedAgentIDs adds the "owned_agents" edge to the Agent entity by IDs. -func (_c *UserCreate) AddOwnedAgentIDs(ids ...uuid.UUID) *UserCreate { - _c.mutation.AddOwnedAgentIDs(ids...) return _c } -// AddOwnedAgents adds the "owned_agents" edges to the Agent entity. -func (_c *UserCreate) AddOwnedAgents(v ...*Agent) *UserCreate { - ids := make([]uuid.UUID, len(v)) - for i := range v { - ids[i] = v[i].ID - } - return _c.AddOwnedAgentIDs(ids...) -} - // AddOwnedGroupIDs adds the "owned_groups" edge to the Group entity by IDs. func (_c *UserCreate) AddOwnedGroupIDs(ids ...uuid.UUID) *UserCreate { _c.mutation.AddOwnedGroupIDs(ids...) @@ -269,11 +255,6 @@ func (_c *UserCreate) check() error { if _, ok := _c.mutation.DisplayName(); !ok { return &ValidationError{Name: "display_name", err: errors.New(`ent: missing required field "User.display_name"`)} } - if v, ok := _c.mutation.DisplayName(); ok { - if err := user.DisplayNameValidator(v); err != nil { - return &ValidationError{Name: "display_name", err: fmt.Errorf(`ent: validator failed for field "User.display_name": %w`, err)} - } - } if _, ok := _c.mutation.Role(); !ok { return &ValidationError{Name: "role", err: errors.New(`ent: missing required field "User.role"`)} } @@ -324,6 +305,7 @@ func (_c *UserCreate) createSpec() (*User, *sqlgraph.CreateSpec) { _node = &User{config: _c.config} _spec = sqlgraph.NewCreateSpec(user.Table, sqlgraph.NewFieldSpec(user.FieldID, field.TypeUUID)) ) + _spec.OnConflict = _c.conflict if id, ok := _c.mutation.ID(); ok { _node.ID = id _spec.ID.Value = &id @@ -360,37 +342,9 @@ func (_c *UserCreate) createSpec() (*User, *sqlgraph.CreateSpec) { _spec.SetField(user.FieldLastLogin, field.TypeTime, value) _node.LastLogin = &value } - if nodes := _c.mutation.CreatedAgentsIDs(); len(nodes) > 0 { - edge := &sqlgraph.EdgeSpec{ - Rel: sqlgraph.O2M, - Inverse: false, - Table: user.CreatedAgentsTable, - Columns: []string{user.CreatedAgentsColumn}, - Bidi: false, - Target: &sqlgraph.EdgeTarget{ - IDSpec: sqlgraph.NewFieldSpec(agent.FieldID, field.TypeUUID), - }, - } - for _, k := range nodes { - edge.Target.Nodes = append(edge.Target.Nodes, k) - } - _spec.Edges = append(_spec.Edges, edge) - } - if nodes := _c.mutation.OwnedAgentsIDs(); len(nodes) > 0 { - edge := &sqlgraph.EdgeSpec{ - Rel: sqlgraph.O2M, - Inverse: false, - Table: user.OwnedAgentsTable, - Columns: []string{user.OwnedAgentsColumn}, - Bidi: false, - Target: &sqlgraph.EdgeTarget{ - IDSpec: sqlgraph.NewFieldSpec(agent.FieldID, field.TypeUUID), - }, - } - for _, k := range nodes { - edge.Target.Nodes = append(edge.Target.Nodes, k) - } - _spec.Edges = append(_spec.Edges, edge) + if value, ok := _c.mutation.LastSeen(); ok { + _spec.SetField(user.FieldLastSeen, field.TypeTime, value) + _node.LastSeen = &value } if nodes := _c.mutation.OwnedGroupsIDs(); len(nodes) > 0 { edge := &sqlgraph.EdgeSpec{ @@ -443,11 +397,410 @@ func (_c *UserCreate) createSpec() (*User, *sqlgraph.CreateSpec) { return _node, _spec } +// OnConflict allows configuring the `ON CONFLICT` / `ON DUPLICATE KEY` clause +// of the `INSERT` statement. For example: +// +// client.User.Create(). +// SetEmail(v). +// OnConflict( +// // Update the row with the new values +// // the was proposed for insertion. +// sql.ResolveWithNewValues(), +// ). +// // Override some of the fields with custom +// // update values. +// Update(func(u *ent.UserUpsert) { +// SetEmail(v+v). +// }). +// Exec(ctx) +func (_c *UserCreate) OnConflict(opts ...sql.ConflictOption) *UserUpsertOne { + _c.conflict = opts + return &UserUpsertOne{ + create: _c, + } +} + +// OnConflictColumns calls `OnConflict` and configures the columns +// as conflict target. Using this option is equivalent to using: +// +// client.User.Create(). +// OnConflict(sql.ConflictColumns(columns...)). +// Exec(ctx) +func (_c *UserCreate) OnConflictColumns(columns ...string) *UserUpsertOne { + _c.conflict = append(_c.conflict, sql.ConflictColumns(columns...)) + return &UserUpsertOne{ + create: _c, + } +} + +type ( + // UserUpsertOne is the builder for "upsert"-ing + // one User node. + UserUpsertOne struct { + create *UserCreate + } + + // UserUpsert is the "OnConflict" setter. + UserUpsert struct { + *sql.UpdateSet + } +) + +// SetEmail sets the "email" field. +func (u *UserUpsert) SetEmail(v string) *UserUpsert { + u.Set(user.FieldEmail, v) + return u +} + +// UpdateEmail sets the "email" field to the value that was provided on create. +func (u *UserUpsert) UpdateEmail() *UserUpsert { + u.SetExcluded(user.FieldEmail) + return u +} + +// SetDisplayName sets the "display_name" field. +func (u *UserUpsert) SetDisplayName(v string) *UserUpsert { + u.Set(user.FieldDisplayName, v) + return u +} + +// UpdateDisplayName sets the "display_name" field to the value that was provided on create. +func (u *UserUpsert) UpdateDisplayName() *UserUpsert { + u.SetExcluded(user.FieldDisplayName) + return u +} + +// SetAvatarURL sets the "avatar_url" field. +func (u *UserUpsert) SetAvatarURL(v string) *UserUpsert { + u.Set(user.FieldAvatarURL, v) + return u +} + +// UpdateAvatarURL sets the "avatar_url" field to the value that was provided on create. +func (u *UserUpsert) UpdateAvatarURL() *UserUpsert { + u.SetExcluded(user.FieldAvatarURL) + return u +} + +// ClearAvatarURL clears the value of the "avatar_url" field. +func (u *UserUpsert) ClearAvatarURL() *UserUpsert { + u.SetNull(user.FieldAvatarURL) + return u +} + +// SetRole sets the "role" field. +func (u *UserUpsert) SetRole(v user.Role) *UserUpsert { + u.Set(user.FieldRole, v) + return u +} + +// UpdateRole sets the "role" field to the value that was provided on create. +func (u *UserUpsert) UpdateRole() *UserUpsert { + u.SetExcluded(user.FieldRole) + return u +} + +// SetStatus sets the "status" field. +func (u *UserUpsert) SetStatus(v user.Status) *UserUpsert { + u.Set(user.FieldStatus, v) + return u +} + +// UpdateStatus sets the "status" field to the value that was provided on create. +func (u *UserUpsert) UpdateStatus() *UserUpsert { + u.SetExcluded(user.FieldStatus) + return u +} + +// SetPreferences sets the "preferences" field. +func (u *UserUpsert) SetPreferences(v *schema.UserPreferences) *UserUpsert { + u.Set(user.FieldPreferences, v) + return u +} + +// UpdatePreferences sets the "preferences" field to the value that was provided on create. +func (u *UserUpsert) UpdatePreferences() *UserUpsert { + u.SetExcluded(user.FieldPreferences) + return u +} + +// ClearPreferences clears the value of the "preferences" field. +func (u *UserUpsert) ClearPreferences() *UserUpsert { + u.SetNull(user.FieldPreferences) + return u +} + +// SetLastLogin sets the "last_login" field. +func (u *UserUpsert) SetLastLogin(v time.Time) *UserUpsert { + u.Set(user.FieldLastLogin, v) + return u +} + +// UpdateLastLogin sets the "last_login" field to the value that was provided on create. +func (u *UserUpsert) UpdateLastLogin() *UserUpsert { + u.SetExcluded(user.FieldLastLogin) + return u +} + +// ClearLastLogin clears the value of the "last_login" field. +func (u *UserUpsert) ClearLastLogin() *UserUpsert { + u.SetNull(user.FieldLastLogin) + return u +} + +// SetLastSeen sets the "last_seen" field. +func (u *UserUpsert) SetLastSeen(v time.Time) *UserUpsert { + u.Set(user.FieldLastSeen, v) + return u +} + +// UpdateLastSeen sets the "last_seen" field to the value that was provided on create. +func (u *UserUpsert) UpdateLastSeen() *UserUpsert { + u.SetExcluded(user.FieldLastSeen) + return u +} + +// ClearLastSeen clears the value of the "last_seen" field. +func (u *UserUpsert) ClearLastSeen() *UserUpsert { + u.SetNull(user.FieldLastSeen) + return u +} + +// UpdateNewValues updates the mutable fields using the new values that were set on create except the ID field. +// Using this option is equivalent to using: +// +// client.User.Create(). +// OnConflict( +// sql.ResolveWithNewValues(), +// sql.ResolveWith(func(u *sql.UpdateSet) { +// u.SetIgnore(user.FieldID) +// }), +// ). +// Exec(ctx) +func (u *UserUpsertOne) UpdateNewValues() *UserUpsertOne { + u.create.conflict = append(u.create.conflict, sql.ResolveWithNewValues()) + u.create.conflict = append(u.create.conflict, sql.ResolveWith(func(s *sql.UpdateSet) { + if _, exists := u.create.mutation.ID(); exists { + s.SetIgnore(user.FieldID) + } + if _, exists := u.create.mutation.Created(); exists { + s.SetIgnore(user.FieldCreated) + } + })) + return u +} + +// Ignore sets each column to itself in case of conflict. +// Using this option is equivalent to using: +// +// client.User.Create(). +// OnConflict(sql.ResolveWithIgnore()). +// Exec(ctx) +func (u *UserUpsertOne) Ignore() *UserUpsertOne { + u.create.conflict = append(u.create.conflict, sql.ResolveWithIgnore()) + return u +} + +// DoNothing configures the conflict_action to `DO NOTHING`. +// Supported only by SQLite and PostgreSQL. +func (u *UserUpsertOne) DoNothing() *UserUpsertOne { + u.create.conflict = append(u.create.conflict, sql.DoNothing()) + return u +} + +// Update allows overriding fields `UPDATE` values. See the UserCreate.OnConflict +// documentation for more info. +func (u *UserUpsertOne) Update(set func(*UserUpsert)) *UserUpsertOne { + u.create.conflict = append(u.create.conflict, sql.ResolveWith(func(update *sql.UpdateSet) { + set(&UserUpsert{UpdateSet: update}) + })) + return u +} + +// SetEmail sets the "email" field. +func (u *UserUpsertOne) SetEmail(v string) *UserUpsertOne { + return u.Update(func(s *UserUpsert) { + s.SetEmail(v) + }) +} + +// UpdateEmail sets the "email" field to the value that was provided on create. +func (u *UserUpsertOne) UpdateEmail() *UserUpsertOne { + return u.Update(func(s *UserUpsert) { + s.UpdateEmail() + }) +} + +// SetDisplayName sets the "display_name" field. +func (u *UserUpsertOne) SetDisplayName(v string) *UserUpsertOne { + return u.Update(func(s *UserUpsert) { + s.SetDisplayName(v) + }) +} + +// UpdateDisplayName sets the "display_name" field to the value that was provided on create. +func (u *UserUpsertOne) UpdateDisplayName() *UserUpsertOne { + return u.Update(func(s *UserUpsert) { + s.UpdateDisplayName() + }) +} + +// SetAvatarURL sets the "avatar_url" field. +func (u *UserUpsertOne) SetAvatarURL(v string) *UserUpsertOne { + return u.Update(func(s *UserUpsert) { + s.SetAvatarURL(v) + }) +} + +// UpdateAvatarURL sets the "avatar_url" field to the value that was provided on create. +func (u *UserUpsertOne) UpdateAvatarURL() *UserUpsertOne { + return u.Update(func(s *UserUpsert) { + s.UpdateAvatarURL() + }) +} + +// ClearAvatarURL clears the value of the "avatar_url" field. +func (u *UserUpsertOne) ClearAvatarURL() *UserUpsertOne { + return u.Update(func(s *UserUpsert) { + s.ClearAvatarURL() + }) +} + +// SetRole sets the "role" field. +func (u *UserUpsertOne) SetRole(v user.Role) *UserUpsertOne { + return u.Update(func(s *UserUpsert) { + s.SetRole(v) + }) +} + +// UpdateRole sets the "role" field to the value that was provided on create. +func (u *UserUpsertOne) UpdateRole() *UserUpsertOne { + return u.Update(func(s *UserUpsert) { + s.UpdateRole() + }) +} + +// SetStatus sets the "status" field. +func (u *UserUpsertOne) SetStatus(v user.Status) *UserUpsertOne { + return u.Update(func(s *UserUpsert) { + s.SetStatus(v) + }) +} + +// UpdateStatus sets the "status" field to the value that was provided on create. +func (u *UserUpsertOne) UpdateStatus() *UserUpsertOne { + return u.Update(func(s *UserUpsert) { + s.UpdateStatus() + }) +} + +// SetPreferences sets the "preferences" field. +func (u *UserUpsertOne) SetPreferences(v *schema.UserPreferences) *UserUpsertOne { + return u.Update(func(s *UserUpsert) { + s.SetPreferences(v) + }) +} + +// UpdatePreferences sets the "preferences" field to the value that was provided on create. +func (u *UserUpsertOne) UpdatePreferences() *UserUpsertOne { + return u.Update(func(s *UserUpsert) { + s.UpdatePreferences() + }) +} + +// ClearPreferences clears the value of the "preferences" field. +func (u *UserUpsertOne) ClearPreferences() *UserUpsertOne { + return u.Update(func(s *UserUpsert) { + s.ClearPreferences() + }) +} + +// SetLastLogin sets the "last_login" field. +func (u *UserUpsertOne) SetLastLogin(v time.Time) *UserUpsertOne { + return u.Update(func(s *UserUpsert) { + s.SetLastLogin(v) + }) +} + +// UpdateLastLogin sets the "last_login" field to the value that was provided on create. +func (u *UserUpsertOne) UpdateLastLogin() *UserUpsertOne { + return u.Update(func(s *UserUpsert) { + s.UpdateLastLogin() + }) +} + +// ClearLastLogin clears the value of the "last_login" field. +func (u *UserUpsertOne) ClearLastLogin() *UserUpsertOne { + return u.Update(func(s *UserUpsert) { + s.ClearLastLogin() + }) +} + +// SetLastSeen sets the "last_seen" field. +func (u *UserUpsertOne) SetLastSeen(v time.Time) *UserUpsertOne { + return u.Update(func(s *UserUpsert) { + s.SetLastSeen(v) + }) +} + +// UpdateLastSeen sets the "last_seen" field to the value that was provided on create. +func (u *UserUpsertOne) UpdateLastSeen() *UserUpsertOne { + return u.Update(func(s *UserUpsert) { + s.UpdateLastSeen() + }) +} + +// ClearLastSeen clears the value of the "last_seen" field. +func (u *UserUpsertOne) ClearLastSeen() *UserUpsertOne { + return u.Update(func(s *UserUpsert) { + s.ClearLastSeen() + }) +} + +// Exec executes the query. +func (u *UserUpsertOne) Exec(ctx context.Context) error { + if len(u.create.conflict) == 0 { + return errors.New("ent: missing options for UserCreate.OnConflict") + } + return u.create.Exec(ctx) +} + +// ExecX is like Exec, but panics if an error occurs. +func (u *UserUpsertOne) ExecX(ctx context.Context) { + if err := u.create.Exec(ctx); err != nil { + panic(err) + } +} + +// Exec executes the UPSERT query and returns the inserted/updated ID. +func (u *UserUpsertOne) ID(ctx context.Context) (id uuid.UUID, err error) { + if u.create.driver.Dialect() == dialect.MySQL { + // In case of "ON CONFLICT", there is no way to get back non-numeric ID + // fields from the database since MySQL does not support the RETURNING clause. + return id, errors.New("ent: UserUpsertOne.ID is not supported by MySQL driver. Use UserUpsertOne.Exec instead") + } + node, err := u.create.Save(ctx) + if err != nil { + return id, err + } + return node.ID, nil +} + +// IDX is like ID, but panics if an error occurs. +func (u *UserUpsertOne) IDX(ctx context.Context) uuid.UUID { + id, err := u.ID(ctx) + if err != nil { + panic(err) + } + return id +} + // UserCreateBulk is the builder for creating many User entities in bulk. type UserCreateBulk struct { config err error builders []*UserCreate + conflict []sql.ConflictOption } // Save creates the User entities in the database. @@ -477,6 +830,7 @@ func (_c *UserCreateBulk) Save(ctx context.Context) ([]*User, error) { _, err = mutators[i+1].Mutate(root, _c.builders[i+1].mutation) } else { spec := &sqlgraph.BatchCreateSpec{Nodes: specs} + spec.OnConflict = _c.conflict // Invoke the actual operation on the latest mutation in the chain. if err = sqlgraph.BatchCreate(ctx, _c.driver, spec); err != nil { if sqlgraph.IsConstraintError(err) { @@ -526,3 +880,260 @@ func (_c *UserCreateBulk) ExecX(ctx context.Context) { panic(err) } } + +// OnConflict allows configuring the `ON CONFLICT` / `ON DUPLICATE KEY` clause +// of the `INSERT` statement. For example: +// +// client.User.CreateBulk(builders...). +// OnConflict( +// // Update the row with the new values +// // the was proposed for insertion. +// sql.ResolveWithNewValues(), +// ). +// // Override some of the fields with custom +// // update values. +// Update(func(u *ent.UserUpsert) { +// SetEmail(v+v). +// }). +// Exec(ctx) +func (_c *UserCreateBulk) OnConflict(opts ...sql.ConflictOption) *UserUpsertBulk { + _c.conflict = opts + return &UserUpsertBulk{ + create: _c, + } +} + +// OnConflictColumns calls `OnConflict` and configures the columns +// as conflict target. Using this option is equivalent to using: +// +// client.User.Create(). +// OnConflict(sql.ConflictColumns(columns...)). +// Exec(ctx) +func (_c *UserCreateBulk) OnConflictColumns(columns ...string) *UserUpsertBulk { + _c.conflict = append(_c.conflict, sql.ConflictColumns(columns...)) + return &UserUpsertBulk{ + create: _c, + } +} + +// UserUpsertBulk is the builder for "upsert"-ing +// a bulk of User nodes. +type UserUpsertBulk struct { + create *UserCreateBulk +} + +// UpdateNewValues updates the mutable fields using the new values that +// were set on create. Using this option is equivalent to using: +// +// client.User.Create(). +// OnConflict( +// sql.ResolveWithNewValues(), +// sql.ResolveWith(func(u *sql.UpdateSet) { +// u.SetIgnore(user.FieldID) +// }), +// ). +// Exec(ctx) +func (u *UserUpsertBulk) UpdateNewValues() *UserUpsertBulk { + u.create.conflict = append(u.create.conflict, sql.ResolveWithNewValues()) + u.create.conflict = append(u.create.conflict, sql.ResolveWith(func(s *sql.UpdateSet) { + for _, b := range u.create.builders { + if _, exists := b.mutation.ID(); exists { + s.SetIgnore(user.FieldID) + } + if _, exists := b.mutation.Created(); exists { + s.SetIgnore(user.FieldCreated) + } + } + })) + return u +} + +// Ignore sets each column to itself in case of conflict. +// Using this option is equivalent to using: +// +// client.User.Create(). +// OnConflict(sql.ResolveWithIgnore()). +// Exec(ctx) +func (u *UserUpsertBulk) Ignore() *UserUpsertBulk { + u.create.conflict = append(u.create.conflict, sql.ResolveWithIgnore()) + return u +} + +// DoNothing configures the conflict_action to `DO NOTHING`. +// Supported only by SQLite and PostgreSQL. +func (u *UserUpsertBulk) DoNothing() *UserUpsertBulk { + u.create.conflict = append(u.create.conflict, sql.DoNothing()) + return u +} + +// Update allows overriding fields `UPDATE` values. See the UserCreateBulk.OnConflict +// documentation for more info. +func (u *UserUpsertBulk) Update(set func(*UserUpsert)) *UserUpsertBulk { + u.create.conflict = append(u.create.conflict, sql.ResolveWith(func(update *sql.UpdateSet) { + set(&UserUpsert{UpdateSet: update}) + })) + return u +} + +// SetEmail sets the "email" field. +func (u *UserUpsertBulk) SetEmail(v string) *UserUpsertBulk { + return u.Update(func(s *UserUpsert) { + s.SetEmail(v) + }) +} + +// UpdateEmail sets the "email" field to the value that was provided on create. +func (u *UserUpsertBulk) UpdateEmail() *UserUpsertBulk { + return u.Update(func(s *UserUpsert) { + s.UpdateEmail() + }) +} + +// SetDisplayName sets the "display_name" field. +func (u *UserUpsertBulk) SetDisplayName(v string) *UserUpsertBulk { + return u.Update(func(s *UserUpsert) { + s.SetDisplayName(v) + }) +} + +// UpdateDisplayName sets the "display_name" field to the value that was provided on create. +func (u *UserUpsertBulk) UpdateDisplayName() *UserUpsertBulk { + return u.Update(func(s *UserUpsert) { + s.UpdateDisplayName() + }) +} + +// SetAvatarURL sets the "avatar_url" field. +func (u *UserUpsertBulk) SetAvatarURL(v string) *UserUpsertBulk { + return u.Update(func(s *UserUpsert) { + s.SetAvatarURL(v) + }) +} + +// UpdateAvatarURL sets the "avatar_url" field to the value that was provided on create. +func (u *UserUpsertBulk) UpdateAvatarURL() *UserUpsertBulk { + return u.Update(func(s *UserUpsert) { + s.UpdateAvatarURL() + }) +} + +// ClearAvatarURL clears the value of the "avatar_url" field. +func (u *UserUpsertBulk) ClearAvatarURL() *UserUpsertBulk { + return u.Update(func(s *UserUpsert) { + s.ClearAvatarURL() + }) +} + +// SetRole sets the "role" field. +func (u *UserUpsertBulk) SetRole(v user.Role) *UserUpsertBulk { + return u.Update(func(s *UserUpsert) { + s.SetRole(v) + }) +} + +// UpdateRole sets the "role" field to the value that was provided on create. +func (u *UserUpsertBulk) UpdateRole() *UserUpsertBulk { + return u.Update(func(s *UserUpsert) { + s.UpdateRole() + }) +} + +// SetStatus sets the "status" field. +func (u *UserUpsertBulk) SetStatus(v user.Status) *UserUpsertBulk { + return u.Update(func(s *UserUpsert) { + s.SetStatus(v) + }) +} + +// UpdateStatus sets the "status" field to the value that was provided on create. +func (u *UserUpsertBulk) UpdateStatus() *UserUpsertBulk { + return u.Update(func(s *UserUpsert) { + s.UpdateStatus() + }) +} + +// SetPreferences sets the "preferences" field. +func (u *UserUpsertBulk) SetPreferences(v *schema.UserPreferences) *UserUpsertBulk { + return u.Update(func(s *UserUpsert) { + s.SetPreferences(v) + }) +} + +// UpdatePreferences sets the "preferences" field to the value that was provided on create. +func (u *UserUpsertBulk) UpdatePreferences() *UserUpsertBulk { + return u.Update(func(s *UserUpsert) { + s.UpdatePreferences() + }) +} + +// ClearPreferences clears the value of the "preferences" field. +func (u *UserUpsertBulk) ClearPreferences() *UserUpsertBulk { + return u.Update(func(s *UserUpsert) { + s.ClearPreferences() + }) +} + +// SetLastLogin sets the "last_login" field. +func (u *UserUpsertBulk) SetLastLogin(v time.Time) *UserUpsertBulk { + return u.Update(func(s *UserUpsert) { + s.SetLastLogin(v) + }) +} + +// UpdateLastLogin sets the "last_login" field to the value that was provided on create. +func (u *UserUpsertBulk) UpdateLastLogin() *UserUpsertBulk { + return u.Update(func(s *UserUpsert) { + s.UpdateLastLogin() + }) +} + +// ClearLastLogin clears the value of the "last_login" field. +func (u *UserUpsertBulk) ClearLastLogin() *UserUpsertBulk { + return u.Update(func(s *UserUpsert) { + s.ClearLastLogin() + }) +} + +// SetLastSeen sets the "last_seen" field. +func (u *UserUpsertBulk) SetLastSeen(v time.Time) *UserUpsertBulk { + return u.Update(func(s *UserUpsert) { + s.SetLastSeen(v) + }) +} + +// UpdateLastSeen sets the "last_seen" field to the value that was provided on create. +func (u *UserUpsertBulk) UpdateLastSeen() *UserUpsertBulk { + return u.Update(func(s *UserUpsert) { + s.UpdateLastSeen() + }) +} + +// ClearLastSeen clears the value of the "last_seen" field. +func (u *UserUpsertBulk) ClearLastSeen() *UserUpsertBulk { + return u.Update(func(s *UserUpsert) { + s.ClearLastSeen() + }) +} + +// Exec executes the query. +func (u *UserUpsertBulk) Exec(ctx context.Context) error { + if u.create.err != nil { + return u.create.err + } + for i, b := range u.create.builders { + if len(b.conflict) != 0 { + return fmt.Errorf("ent: OnConflict was set for builder %d. Set it on the UserCreateBulk instead", i) + } + } + if len(u.create.conflict) == 0 { + return errors.New("ent: missing options for UserCreateBulk.OnConflict") + } + return u.create.Exec(ctx) +} + +// ExecX is like Exec, but panics if an error occurs. +func (u *UserUpsertBulk) ExecX(ctx context.Context) { + if err := u.create.Exec(ctx); err != nil { + panic(err) + } +} diff --git a/pkg/ent/user_query.go b/pkg/ent/user_query.go index 63c08582d..4478ce1f9 100644 --- a/pkg/ent/user_query.go +++ b/pkg/ent/user_query.go @@ -9,10 +9,10 @@ import ( "math" "entgo.io/ent" + "entgo.io/ent/dialect" "entgo.io/ent/dialect/sql" "entgo.io/ent/dialect/sql/sqlgraph" "entgo.io/ent/schema/field" - "github.com/GoogleCloudPlatform/scion/pkg/ent/agent" "github.com/GoogleCloudPlatform/scion/pkg/ent/group" "github.com/GoogleCloudPlatform/scion/pkg/ent/groupmembership" "github.com/GoogleCloudPlatform/scion/pkg/ent/policybinding" @@ -28,11 +28,10 @@ type UserQuery struct { order []user.OrderOption inters []Interceptor predicates []predicate.User - withCreatedAgents *AgentQuery - withOwnedAgents *AgentQuery withOwnedGroups *GroupQuery withMemberships *GroupMembershipQuery withPolicyBindings *PolicyBindingQuery + modifiers []func(*sql.Selector) // intermediate query (i.e. traversal path). sql *sql.Selector path func(context.Context) (*sql.Selector, error) @@ -69,50 +68,6 @@ func (_q *UserQuery) Order(o ...user.OrderOption) *UserQuery { return _q } -// QueryCreatedAgents chains the current query on the "created_agents" edge. -func (_q *UserQuery) QueryCreatedAgents() *AgentQuery { - query := (&AgentClient{config: _q.config}).Query() - query.path = func(ctx context.Context) (fromU *sql.Selector, err error) { - if err := _q.prepareQuery(ctx); err != nil { - return nil, err - } - selector := _q.sqlQuery(ctx) - if err := selector.Err(); err != nil { - return nil, err - } - step := sqlgraph.NewStep( - sqlgraph.From(user.Table, user.FieldID, selector), - sqlgraph.To(agent.Table, agent.FieldID), - sqlgraph.Edge(sqlgraph.O2M, false, user.CreatedAgentsTable, user.CreatedAgentsColumn), - ) - fromU = sqlgraph.SetNeighbors(_q.driver.Dialect(), step) - return fromU, nil - } - return query -} - -// QueryOwnedAgents chains the current query on the "owned_agents" edge. -func (_q *UserQuery) QueryOwnedAgents() *AgentQuery { - query := (&AgentClient{config: _q.config}).Query() - query.path = func(ctx context.Context) (fromU *sql.Selector, err error) { - if err := _q.prepareQuery(ctx); err != nil { - return nil, err - } - selector := _q.sqlQuery(ctx) - if err := selector.Err(); err != nil { - return nil, err - } - step := sqlgraph.NewStep( - sqlgraph.From(user.Table, user.FieldID, selector), - sqlgraph.To(agent.Table, agent.FieldID), - sqlgraph.Edge(sqlgraph.O2M, false, user.OwnedAgentsTable, user.OwnedAgentsColumn), - ) - fromU = sqlgraph.SetNeighbors(_q.driver.Dialect(), step) - return fromU, nil - } - return query -} - // QueryOwnedGroups chains the current query on the "owned_groups" edge. func (_q *UserQuery) QueryOwnedGroups() *GroupQuery { query := (&GroupClient{config: _q.config}).Query() @@ -371,8 +326,6 @@ func (_q *UserQuery) Clone() *UserQuery { order: append([]user.OrderOption{}, _q.order...), inters: append([]Interceptor{}, _q.inters...), predicates: append([]predicate.User{}, _q.predicates...), - withCreatedAgents: _q.withCreatedAgents.Clone(), - withOwnedAgents: _q.withOwnedAgents.Clone(), withOwnedGroups: _q.withOwnedGroups.Clone(), withMemberships: _q.withMemberships.Clone(), withPolicyBindings: _q.withPolicyBindings.Clone(), @@ -382,28 +335,6 @@ func (_q *UserQuery) Clone() *UserQuery { } } -// WithCreatedAgents tells the query-builder to eager-load the nodes that are connected to -// the "created_agents" edge. The optional arguments are used to configure the query builder of the edge. -func (_q *UserQuery) WithCreatedAgents(opts ...func(*AgentQuery)) *UserQuery { - query := (&AgentClient{config: _q.config}).Query() - for _, opt := range opts { - opt(query) - } - _q.withCreatedAgents = query - return _q -} - -// WithOwnedAgents tells the query-builder to eager-load the nodes that are connected to -// the "owned_agents" edge. The optional arguments are used to configure the query builder of the edge. -func (_q *UserQuery) WithOwnedAgents(opts ...func(*AgentQuery)) *UserQuery { - query := (&AgentClient{config: _q.config}).Query() - for _, opt := range opts { - opt(query) - } - _q.withOwnedAgents = query - return _q -} - // WithOwnedGroups tells the query-builder to eager-load the nodes that are connected to // the "owned_groups" edge. The optional arguments are used to configure the query builder of the edge. func (_q *UserQuery) WithOwnedGroups(opts ...func(*GroupQuery)) *UserQuery { @@ -515,9 +446,7 @@ func (_q *UserQuery) sqlAll(ctx context.Context, hooks ...queryHook) ([]*User, e var ( nodes = []*User{} _spec = _q.querySpec() - loadedTypes = [5]bool{ - _q.withCreatedAgents != nil, - _q.withOwnedAgents != nil, + loadedTypes = [3]bool{ _q.withOwnedGroups != nil, _q.withMemberships != nil, _q.withPolicyBindings != nil, @@ -532,6 +461,9 @@ func (_q *UserQuery) sqlAll(ctx context.Context, hooks ...queryHook) ([]*User, e node.Edges.loadedTypes = loadedTypes return node.assignValues(columns, values) } + if len(_q.modifiers) > 0 { + _spec.Modifiers = _q.modifiers + } for i := range hooks { hooks[i](ctx, _spec) } @@ -541,20 +473,6 @@ func (_q *UserQuery) sqlAll(ctx context.Context, hooks ...queryHook) ([]*User, e if len(nodes) == 0 { return nodes, nil } - if query := _q.withCreatedAgents; query != nil { - if err := _q.loadCreatedAgents(ctx, query, nodes, - func(n *User) { n.Edges.CreatedAgents = []*Agent{} }, - func(n *User, e *Agent) { n.Edges.CreatedAgents = append(n.Edges.CreatedAgents, e) }); err != nil { - return nil, err - } - } - if query := _q.withOwnedAgents; query != nil { - if err := _q.loadOwnedAgents(ctx, query, nodes, - func(n *User) { n.Edges.OwnedAgents = []*Agent{} }, - func(n *User, e *Agent) { n.Edges.OwnedAgents = append(n.Edges.OwnedAgents, e) }); err != nil { - return nil, err - } - } if query := _q.withOwnedGroups; query != nil { if err := _q.loadOwnedGroups(ctx, query, nodes, func(n *User) { n.Edges.OwnedGroups = []*Group{} }, @@ -579,72 +497,6 @@ func (_q *UserQuery) sqlAll(ctx context.Context, hooks ...queryHook) ([]*User, e return nodes, nil } -func (_q *UserQuery) loadCreatedAgents(ctx context.Context, query *AgentQuery, nodes []*User, init func(*User), assign func(*User, *Agent)) error { - fks := make([]driver.Value, 0, len(nodes)) - nodeids := make(map[uuid.UUID]*User) - for i := range nodes { - fks = append(fks, nodes[i].ID) - nodeids[nodes[i].ID] = nodes[i] - if init != nil { - init(nodes[i]) - } - } - if len(query.ctx.Fields) > 0 { - query.ctx.AppendFieldOnce(agent.FieldCreatedBy) - } - query.Where(predicate.Agent(func(s *sql.Selector) { - s.Where(sql.InValues(s.C(user.CreatedAgentsColumn), fks...)) - })) - neighbors, err := query.All(ctx) - if err != nil { - return err - } - for _, n := range neighbors { - fk := n.CreatedBy - if fk == nil { - return fmt.Errorf(`foreign-key "created_by" is nil for node %v`, n.ID) - } - node, ok := nodeids[*fk] - if !ok { - return fmt.Errorf(`unexpected referenced foreign-key "created_by" returned %v for node %v`, *fk, n.ID) - } - assign(node, n) - } - return nil -} -func (_q *UserQuery) loadOwnedAgents(ctx context.Context, query *AgentQuery, nodes []*User, init func(*User), assign func(*User, *Agent)) error { - fks := make([]driver.Value, 0, len(nodes)) - nodeids := make(map[uuid.UUID]*User) - for i := range nodes { - fks = append(fks, nodes[i].ID) - nodeids[nodes[i].ID] = nodes[i] - if init != nil { - init(nodes[i]) - } - } - if len(query.ctx.Fields) > 0 { - query.ctx.AppendFieldOnce(agent.FieldOwnerID) - } - query.Where(predicate.Agent(func(s *sql.Selector) { - s.Where(sql.InValues(s.C(user.OwnedAgentsColumn), fks...)) - })) - neighbors, err := query.All(ctx) - if err != nil { - return err - } - for _, n := range neighbors { - fk := n.OwnerID - if fk == nil { - return fmt.Errorf(`foreign-key "owner_id" is nil for node %v`, n.ID) - } - node, ok := nodeids[*fk] - if !ok { - return fmt.Errorf(`unexpected referenced foreign-key "owner_id" returned %v for node %v`, *fk, n.ID) - } - assign(node, n) - } - return nil -} func (_q *UserQuery) loadOwnedGroups(ctx context.Context, query *GroupQuery, nodes []*User, init func(*User), assign func(*User, *Group)) error { fks := make([]driver.Value, 0, len(nodes)) nodeids := make(map[uuid.UUID]*User) @@ -747,6 +599,9 @@ func (_q *UserQuery) loadPolicyBindings(ctx context.Context, query *PolicyBindin func (_q *UserQuery) sqlCount(ctx context.Context) (int, error) { _spec := _q.querySpec() + if len(_q.modifiers) > 0 { + _spec.Modifiers = _q.modifiers + } _spec.Node.Columns = _q.ctx.Fields if len(_q.ctx.Fields) > 0 { _spec.Unique = _q.ctx.Unique != nil && *_q.ctx.Unique @@ -809,6 +664,9 @@ func (_q *UserQuery) sqlQuery(ctx context.Context) *sql.Selector { if _q.ctx.Unique != nil && *_q.ctx.Unique { selector.Distinct() } + for _, m := range _q.modifiers { + m(selector) + } for _, p := range _q.predicates { p(selector) } @@ -826,6 +684,32 @@ func (_q *UserQuery) sqlQuery(ctx context.Context) *sql.Selector { return selector } +// ForUpdate locks the selected rows against concurrent updates, and prevent them from being +// updated, deleted or "selected ... for update" by other sessions, until the transaction is +// either committed or rolled-back. +func (_q *UserQuery) ForUpdate(opts ...sql.LockOption) *UserQuery { + if _q.driver.Dialect() == dialect.Postgres { + _q.Unique(false) + } + _q.modifiers = append(_q.modifiers, func(s *sql.Selector) { + s.ForUpdate(opts...) + }) + return _q +} + +// ForShare behaves similarly to ForUpdate, except that it acquires a shared mode lock +// on any rows that are read. Other sessions can read the rows, but cannot modify them +// until your transaction commits. +func (_q *UserQuery) ForShare(opts ...sql.LockOption) *UserQuery { + if _q.driver.Dialect() == dialect.Postgres { + _q.Unique(false) + } + _q.modifiers = append(_q.modifiers, func(s *sql.Selector) { + s.ForShare(opts...) + }) + return _q +} + // UserGroupBy is the group-by builder for User entities. type UserGroupBy struct { selector diff --git a/pkg/ent/user_update.go b/pkg/ent/user_update.go index e6b121d90..695967f4c 100644 --- a/pkg/ent/user_update.go +++ b/pkg/ent/user_update.go @@ -11,7 +11,6 @@ import ( "entgo.io/ent/dialect/sql" "entgo.io/ent/dialect/sql/sqlgraph" "entgo.io/ent/schema/field" - "github.com/GoogleCloudPlatform/scion/pkg/ent/agent" "github.com/GoogleCloudPlatform/scion/pkg/ent/group" "github.com/GoogleCloudPlatform/scion/pkg/ent/groupmembership" "github.com/GoogleCloudPlatform/scion/pkg/ent/policybinding" @@ -142,34 +141,24 @@ func (_u *UserUpdate) ClearLastLogin() *UserUpdate { return _u } -// AddCreatedAgentIDs adds the "created_agents" edge to the Agent entity by IDs. -func (_u *UserUpdate) AddCreatedAgentIDs(ids ...uuid.UUID) *UserUpdate { - _u.mutation.AddCreatedAgentIDs(ids...) +// SetLastSeen sets the "last_seen" field. +func (_u *UserUpdate) SetLastSeen(v time.Time) *UserUpdate { + _u.mutation.SetLastSeen(v) return _u } -// AddCreatedAgents adds the "created_agents" edges to the Agent entity. -func (_u *UserUpdate) AddCreatedAgents(v ...*Agent) *UserUpdate { - ids := make([]uuid.UUID, len(v)) - for i := range v { - ids[i] = v[i].ID +// SetNillableLastSeen sets the "last_seen" field if the given value is not nil. +func (_u *UserUpdate) SetNillableLastSeen(v *time.Time) *UserUpdate { + if v != nil { + _u.SetLastSeen(*v) } - return _u.AddCreatedAgentIDs(ids...) -} - -// AddOwnedAgentIDs adds the "owned_agents" edge to the Agent entity by IDs. -func (_u *UserUpdate) AddOwnedAgentIDs(ids ...uuid.UUID) *UserUpdate { - _u.mutation.AddOwnedAgentIDs(ids...) return _u } -// AddOwnedAgents adds the "owned_agents" edges to the Agent entity. -func (_u *UserUpdate) AddOwnedAgents(v ...*Agent) *UserUpdate { - ids := make([]uuid.UUID, len(v)) - for i := range v { - ids[i] = v[i].ID - } - return _u.AddOwnedAgentIDs(ids...) +// ClearLastSeen clears the value of the "last_seen" field. +func (_u *UserUpdate) ClearLastSeen() *UserUpdate { + _u.mutation.ClearLastSeen() + return _u } // AddOwnedGroupIDs adds the "owned_groups" edge to the Group entity by IDs. @@ -222,48 +211,6 @@ func (_u *UserUpdate) Mutation() *UserMutation { return _u.mutation } -// ClearCreatedAgents clears all "created_agents" edges to the Agent entity. -func (_u *UserUpdate) ClearCreatedAgents() *UserUpdate { - _u.mutation.ClearCreatedAgents() - return _u -} - -// RemoveCreatedAgentIDs removes the "created_agents" edge to Agent entities by IDs. -func (_u *UserUpdate) RemoveCreatedAgentIDs(ids ...uuid.UUID) *UserUpdate { - _u.mutation.RemoveCreatedAgentIDs(ids...) - return _u -} - -// RemoveCreatedAgents removes "created_agents" edges to Agent entities. -func (_u *UserUpdate) RemoveCreatedAgents(v ...*Agent) *UserUpdate { - ids := make([]uuid.UUID, len(v)) - for i := range v { - ids[i] = v[i].ID - } - return _u.RemoveCreatedAgentIDs(ids...) -} - -// ClearOwnedAgents clears all "owned_agents" edges to the Agent entity. -func (_u *UserUpdate) ClearOwnedAgents() *UserUpdate { - _u.mutation.ClearOwnedAgents() - return _u -} - -// RemoveOwnedAgentIDs removes the "owned_agents" edge to Agent entities by IDs. -func (_u *UserUpdate) RemoveOwnedAgentIDs(ids ...uuid.UUID) *UserUpdate { - _u.mutation.RemoveOwnedAgentIDs(ids...) - return _u -} - -// RemoveOwnedAgents removes "owned_agents" edges to Agent entities. -func (_u *UserUpdate) RemoveOwnedAgents(v ...*Agent) *UserUpdate { - ids := make([]uuid.UUID, len(v)) - for i := range v { - ids[i] = v[i].ID - } - return _u.RemoveOwnedAgentIDs(ids...) -} - // ClearOwnedGroups clears all "owned_groups" edges to the Group entity. func (_u *UserUpdate) ClearOwnedGroups() *UserUpdate { _u.mutation.ClearOwnedGroups() @@ -361,11 +308,6 @@ func (_u *UserUpdate) check() error { return &ValidationError{Name: "email", err: fmt.Errorf(`ent: validator failed for field "User.email": %w`, err)} } } - if v, ok := _u.mutation.DisplayName(); ok { - if err := user.DisplayNameValidator(v); err != nil { - return &ValidationError{Name: "display_name", err: fmt.Errorf(`ent: validator failed for field "User.display_name": %w`, err)} - } - } if v, ok := _u.mutation.Role(); ok { if err := user.RoleValidator(v); err != nil { return &ValidationError{Name: "role", err: fmt.Errorf(`ent: validator failed for field "User.role": %w`, err)} @@ -421,95 +363,11 @@ func (_u *UserUpdate) sqlSave(ctx context.Context) (_node int, err error) { if _u.mutation.LastLoginCleared() { _spec.ClearField(user.FieldLastLogin, field.TypeTime) } - if _u.mutation.CreatedAgentsCleared() { - edge := &sqlgraph.EdgeSpec{ - Rel: sqlgraph.O2M, - Inverse: false, - Table: user.CreatedAgentsTable, - Columns: []string{user.CreatedAgentsColumn}, - Bidi: false, - Target: &sqlgraph.EdgeTarget{ - IDSpec: sqlgraph.NewFieldSpec(agent.FieldID, field.TypeUUID), - }, - } - _spec.Edges.Clear = append(_spec.Edges.Clear, edge) + if value, ok := _u.mutation.LastSeen(); ok { + _spec.SetField(user.FieldLastSeen, field.TypeTime, value) } - if nodes := _u.mutation.RemovedCreatedAgentsIDs(); len(nodes) > 0 && !_u.mutation.CreatedAgentsCleared() { - edge := &sqlgraph.EdgeSpec{ - Rel: sqlgraph.O2M, - Inverse: false, - Table: user.CreatedAgentsTable, - Columns: []string{user.CreatedAgentsColumn}, - Bidi: false, - Target: &sqlgraph.EdgeTarget{ - IDSpec: sqlgraph.NewFieldSpec(agent.FieldID, field.TypeUUID), - }, - } - for _, k := range nodes { - edge.Target.Nodes = append(edge.Target.Nodes, k) - } - _spec.Edges.Clear = append(_spec.Edges.Clear, edge) - } - if nodes := _u.mutation.CreatedAgentsIDs(); len(nodes) > 0 { - edge := &sqlgraph.EdgeSpec{ - Rel: sqlgraph.O2M, - Inverse: false, - Table: user.CreatedAgentsTable, - Columns: []string{user.CreatedAgentsColumn}, - Bidi: false, - Target: &sqlgraph.EdgeTarget{ - IDSpec: sqlgraph.NewFieldSpec(agent.FieldID, field.TypeUUID), - }, - } - for _, k := range nodes { - edge.Target.Nodes = append(edge.Target.Nodes, k) - } - _spec.Edges.Add = append(_spec.Edges.Add, edge) - } - if _u.mutation.OwnedAgentsCleared() { - edge := &sqlgraph.EdgeSpec{ - Rel: sqlgraph.O2M, - Inverse: false, - Table: user.OwnedAgentsTable, - Columns: []string{user.OwnedAgentsColumn}, - Bidi: false, - Target: &sqlgraph.EdgeTarget{ - IDSpec: sqlgraph.NewFieldSpec(agent.FieldID, field.TypeUUID), - }, - } - _spec.Edges.Clear = append(_spec.Edges.Clear, edge) - } - if nodes := _u.mutation.RemovedOwnedAgentsIDs(); len(nodes) > 0 && !_u.mutation.OwnedAgentsCleared() { - edge := &sqlgraph.EdgeSpec{ - Rel: sqlgraph.O2M, - Inverse: false, - Table: user.OwnedAgentsTable, - Columns: []string{user.OwnedAgentsColumn}, - Bidi: false, - Target: &sqlgraph.EdgeTarget{ - IDSpec: sqlgraph.NewFieldSpec(agent.FieldID, field.TypeUUID), - }, - } - for _, k := range nodes { - edge.Target.Nodes = append(edge.Target.Nodes, k) - } - _spec.Edges.Clear = append(_spec.Edges.Clear, edge) - } - if nodes := _u.mutation.OwnedAgentsIDs(); len(nodes) > 0 { - edge := &sqlgraph.EdgeSpec{ - Rel: sqlgraph.O2M, - Inverse: false, - Table: user.OwnedAgentsTable, - Columns: []string{user.OwnedAgentsColumn}, - Bidi: false, - Target: &sqlgraph.EdgeTarget{ - IDSpec: sqlgraph.NewFieldSpec(agent.FieldID, field.TypeUUID), - }, - } - for _, k := range nodes { - edge.Target.Nodes = append(edge.Target.Nodes, k) - } - _spec.Edges.Add = append(_spec.Edges.Add, edge) + if _u.mutation.LastSeenCleared() { + _spec.ClearField(user.FieldLastSeen, field.TypeTime) } if _u.mutation.OwnedGroupsCleared() { edge := &sqlgraph.EdgeSpec{ @@ -774,34 +632,24 @@ func (_u *UserUpdateOne) ClearLastLogin() *UserUpdateOne { return _u } -// AddCreatedAgentIDs adds the "created_agents" edge to the Agent entity by IDs. -func (_u *UserUpdateOne) AddCreatedAgentIDs(ids ...uuid.UUID) *UserUpdateOne { - _u.mutation.AddCreatedAgentIDs(ids...) +// SetLastSeen sets the "last_seen" field. +func (_u *UserUpdateOne) SetLastSeen(v time.Time) *UserUpdateOne { + _u.mutation.SetLastSeen(v) return _u } -// AddCreatedAgents adds the "created_agents" edges to the Agent entity. -func (_u *UserUpdateOne) AddCreatedAgents(v ...*Agent) *UserUpdateOne { - ids := make([]uuid.UUID, len(v)) - for i := range v { - ids[i] = v[i].ID +// SetNillableLastSeen sets the "last_seen" field if the given value is not nil. +func (_u *UserUpdateOne) SetNillableLastSeen(v *time.Time) *UserUpdateOne { + if v != nil { + _u.SetLastSeen(*v) } - return _u.AddCreatedAgentIDs(ids...) -} - -// AddOwnedAgentIDs adds the "owned_agents" edge to the Agent entity by IDs. -func (_u *UserUpdateOne) AddOwnedAgentIDs(ids ...uuid.UUID) *UserUpdateOne { - _u.mutation.AddOwnedAgentIDs(ids...) return _u } -// AddOwnedAgents adds the "owned_agents" edges to the Agent entity. -func (_u *UserUpdateOne) AddOwnedAgents(v ...*Agent) *UserUpdateOne { - ids := make([]uuid.UUID, len(v)) - for i := range v { - ids[i] = v[i].ID - } - return _u.AddOwnedAgentIDs(ids...) +// ClearLastSeen clears the value of the "last_seen" field. +func (_u *UserUpdateOne) ClearLastSeen() *UserUpdateOne { + _u.mutation.ClearLastSeen() + return _u } // AddOwnedGroupIDs adds the "owned_groups" edge to the Group entity by IDs. @@ -854,48 +702,6 @@ func (_u *UserUpdateOne) Mutation() *UserMutation { return _u.mutation } -// ClearCreatedAgents clears all "created_agents" edges to the Agent entity. -func (_u *UserUpdateOne) ClearCreatedAgents() *UserUpdateOne { - _u.mutation.ClearCreatedAgents() - return _u -} - -// RemoveCreatedAgentIDs removes the "created_agents" edge to Agent entities by IDs. -func (_u *UserUpdateOne) RemoveCreatedAgentIDs(ids ...uuid.UUID) *UserUpdateOne { - _u.mutation.RemoveCreatedAgentIDs(ids...) - return _u -} - -// RemoveCreatedAgents removes "created_agents" edges to Agent entities. -func (_u *UserUpdateOne) RemoveCreatedAgents(v ...*Agent) *UserUpdateOne { - ids := make([]uuid.UUID, len(v)) - for i := range v { - ids[i] = v[i].ID - } - return _u.RemoveCreatedAgentIDs(ids...) -} - -// ClearOwnedAgents clears all "owned_agents" edges to the Agent entity. -func (_u *UserUpdateOne) ClearOwnedAgents() *UserUpdateOne { - _u.mutation.ClearOwnedAgents() - return _u -} - -// RemoveOwnedAgentIDs removes the "owned_agents" edge to Agent entities by IDs. -func (_u *UserUpdateOne) RemoveOwnedAgentIDs(ids ...uuid.UUID) *UserUpdateOne { - _u.mutation.RemoveOwnedAgentIDs(ids...) - return _u -} - -// RemoveOwnedAgents removes "owned_agents" edges to Agent entities. -func (_u *UserUpdateOne) RemoveOwnedAgents(v ...*Agent) *UserUpdateOne { - ids := make([]uuid.UUID, len(v)) - for i := range v { - ids[i] = v[i].ID - } - return _u.RemoveOwnedAgentIDs(ids...) -} - // ClearOwnedGroups clears all "owned_groups" edges to the Group entity. func (_u *UserUpdateOne) ClearOwnedGroups() *UserUpdateOne { _u.mutation.ClearOwnedGroups() @@ -1006,11 +812,6 @@ func (_u *UserUpdateOne) check() error { return &ValidationError{Name: "email", err: fmt.Errorf(`ent: validator failed for field "User.email": %w`, err)} } } - if v, ok := _u.mutation.DisplayName(); ok { - if err := user.DisplayNameValidator(v); err != nil { - return &ValidationError{Name: "display_name", err: fmt.Errorf(`ent: validator failed for field "User.display_name": %w`, err)} - } - } if v, ok := _u.mutation.Role(); ok { if err := user.RoleValidator(v); err != nil { return &ValidationError{Name: "role", err: fmt.Errorf(`ent: validator failed for field "User.role": %w`, err)} @@ -1083,95 +884,11 @@ func (_u *UserUpdateOne) sqlSave(ctx context.Context) (_node *User, err error) { if _u.mutation.LastLoginCleared() { _spec.ClearField(user.FieldLastLogin, field.TypeTime) } - if _u.mutation.CreatedAgentsCleared() { - edge := &sqlgraph.EdgeSpec{ - Rel: sqlgraph.O2M, - Inverse: false, - Table: user.CreatedAgentsTable, - Columns: []string{user.CreatedAgentsColumn}, - Bidi: false, - Target: &sqlgraph.EdgeTarget{ - IDSpec: sqlgraph.NewFieldSpec(agent.FieldID, field.TypeUUID), - }, - } - _spec.Edges.Clear = append(_spec.Edges.Clear, edge) + if value, ok := _u.mutation.LastSeen(); ok { + _spec.SetField(user.FieldLastSeen, field.TypeTime, value) } - if nodes := _u.mutation.RemovedCreatedAgentsIDs(); len(nodes) > 0 && !_u.mutation.CreatedAgentsCleared() { - edge := &sqlgraph.EdgeSpec{ - Rel: sqlgraph.O2M, - Inverse: false, - Table: user.CreatedAgentsTable, - Columns: []string{user.CreatedAgentsColumn}, - Bidi: false, - Target: &sqlgraph.EdgeTarget{ - IDSpec: sqlgraph.NewFieldSpec(agent.FieldID, field.TypeUUID), - }, - } - for _, k := range nodes { - edge.Target.Nodes = append(edge.Target.Nodes, k) - } - _spec.Edges.Clear = append(_spec.Edges.Clear, edge) - } - if nodes := _u.mutation.CreatedAgentsIDs(); len(nodes) > 0 { - edge := &sqlgraph.EdgeSpec{ - Rel: sqlgraph.O2M, - Inverse: false, - Table: user.CreatedAgentsTable, - Columns: []string{user.CreatedAgentsColumn}, - Bidi: false, - Target: &sqlgraph.EdgeTarget{ - IDSpec: sqlgraph.NewFieldSpec(agent.FieldID, field.TypeUUID), - }, - } - for _, k := range nodes { - edge.Target.Nodes = append(edge.Target.Nodes, k) - } - _spec.Edges.Add = append(_spec.Edges.Add, edge) - } - if _u.mutation.OwnedAgentsCleared() { - edge := &sqlgraph.EdgeSpec{ - Rel: sqlgraph.O2M, - Inverse: false, - Table: user.OwnedAgentsTable, - Columns: []string{user.OwnedAgentsColumn}, - Bidi: false, - Target: &sqlgraph.EdgeTarget{ - IDSpec: sqlgraph.NewFieldSpec(agent.FieldID, field.TypeUUID), - }, - } - _spec.Edges.Clear = append(_spec.Edges.Clear, edge) - } - if nodes := _u.mutation.RemovedOwnedAgentsIDs(); len(nodes) > 0 && !_u.mutation.OwnedAgentsCleared() { - edge := &sqlgraph.EdgeSpec{ - Rel: sqlgraph.O2M, - Inverse: false, - Table: user.OwnedAgentsTable, - Columns: []string{user.OwnedAgentsColumn}, - Bidi: false, - Target: &sqlgraph.EdgeTarget{ - IDSpec: sqlgraph.NewFieldSpec(agent.FieldID, field.TypeUUID), - }, - } - for _, k := range nodes { - edge.Target.Nodes = append(edge.Target.Nodes, k) - } - _spec.Edges.Clear = append(_spec.Edges.Clear, edge) - } - if nodes := _u.mutation.OwnedAgentsIDs(); len(nodes) > 0 { - edge := &sqlgraph.EdgeSpec{ - Rel: sqlgraph.O2M, - Inverse: false, - Table: user.OwnedAgentsTable, - Columns: []string{user.OwnedAgentsColumn}, - Bidi: false, - Target: &sqlgraph.EdgeTarget{ - IDSpec: sqlgraph.NewFieldSpec(agent.FieldID, field.TypeUUID), - }, - } - for _, k := range nodes { - edge.Target.Nodes = append(edge.Target.Nodes, k) - } - _spec.Edges.Add = append(_spec.Edges.Add, edge) + if _u.mutation.LastSeenCleared() { + _spec.ClearField(user.FieldLastSeen, field.TypeTime) } if _u.mutation.OwnedGroupsCleared() { edge := &sqlgraph.EdgeSpec{ diff --git a/pkg/ent/useraccesstoken.go b/pkg/ent/useraccesstoken.go new file mode 100644 index 000000000..40e13b7d7 --- /dev/null +++ b/pkg/ent/useraccesstoken.go @@ -0,0 +1,213 @@ +// Code generated by ent, DO NOT EDIT. + +package ent + +import ( + "fmt" + "strings" + "time" + + "entgo.io/ent" + "entgo.io/ent/dialect/sql" + "github.com/GoogleCloudPlatform/scion/pkg/ent/useraccesstoken" + "github.com/google/uuid" +) + +// UserAccessToken is the model entity for the UserAccessToken schema. +type UserAccessToken struct { + config `json:"-"` + // ID of the ent. + ID uuid.UUID `json:"id,omitempty"` + // UserID holds the value of the "user_id" field. + UserID uuid.UUID `json:"user_id,omitempty"` + // Name holds the value of the "name" field. + Name string `json:"name,omitempty"` + // Prefix holds the value of the "prefix" field. + Prefix string `json:"prefix,omitempty"` + // KeyHash holds the value of the "key_hash" field. + KeyHash string `json:"-"` + // ProjectID holds the value of the "project_id" field. + ProjectID uuid.UUID `json:"project_id,omitempty"` + // Scopes holds the value of the "scopes" field. + Scopes string `json:"scopes,omitempty"` + // Revoked holds the value of the "revoked" field. + Revoked bool `json:"revoked,omitempty"` + // ExpiresAt holds the value of the "expires_at" field. + ExpiresAt *time.Time `json:"expires_at,omitempty"` + // LastUsed holds the value of the "last_used" field. + LastUsed *time.Time `json:"last_used,omitempty"` + // Created holds the value of the "created" field. + Created time.Time `json:"created,omitempty"` + selectValues sql.SelectValues +} + +// scanValues returns the types for scanning values from sql.Rows. +func (*UserAccessToken) scanValues(columns []string) ([]any, error) { + values := make([]any, len(columns)) + for i := range columns { + switch columns[i] { + case useraccesstoken.FieldRevoked: + values[i] = new(sql.NullBool) + case useraccesstoken.FieldName, useraccesstoken.FieldPrefix, useraccesstoken.FieldKeyHash, useraccesstoken.FieldScopes: + values[i] = new(sql.NullString) + case useraccesstoken.FieldExpiresAt, useraccesstoken.FieldLastUsed, useraccesstoken.FieldCreated: + values[i] = new(sql.NullTime) + case useraccesstoken.FieldID, useraccesstoken.FieldUserID, useraccesstoken.FieldProjectID: + values[i] = new(uuid.UUID) + default: + values[i] = new(sql.UnknownType) + } + } + return values, nil +} + +// assignValues assigns the values that were returned from sql.Rows (after scanning) +// to the UserAccessToken fields. +func (_m *UserAccessToken) assignValues(columns []string, values []any) error { + if m, n := len(values), len(columns); m < n { + return fmt.Errorf("mismatch number of scan values: %d != %d", m, n) + } + for i := range columns { + switch columns[i] { + case useraccesstoken.FieldID: + if value, ok := values[i].(*uuid.UUID); !ok { + return fmt.Errorf("unexpected type %T for field id", values[i]) + } else if value != nil { + _m.ID = *value + } + case useraccesstoken.FieldUserID: + if value, ok := values[i].(*uuid.UUID); !ok { + return fmt.Errorf("unexpected type %T for field user_id", values[i]) + } else if value != nil { + _m.UserID = *value + } + case useraccesstoken.FieldName: + if value, ok := values[i].(*sql.NullString); !ok { + return fmt.Errorf("unexpected type %T for field name", values[i]) + } else if value.Valid { + _m.Name = value.String + } + case useraccesstoken.FieldPrefix: + if value, ok := values[i].(*sql.NullString); !ok { + return fmt.Errorf("unexpected type %T for field prefix", values[i]) + } else if value.Valid { + _m.Prefix = value.String + } + case useraccesstoken.FieldKeyHash: + if value, ok := values[i].(*sql.NullString); !ok { + return fmt.Errorf("unexpected type %T for field key_hash", values[i]) + } else if value.Valid { + _m.KeyHash = value.String + } + case useraccesstoken.FieldProjectID: + if value, ok := values[i].(*uuid.UUID); !ok { + return fmt.Errorf("unexpected type %T for field project_id", values[i]) + } else if value != nil { + _m.ProjectID = *value + } + case useraccesstoken.FieldScopes: + if value, ok := values[i].(*sql.NullString); !ok { + return fmt.Errorf("unexpected type %T for field scopes", values[i]) + } else if value.Valid { + _m.Scopes = value.String + } + case useraccesstoken.FieldRevoked: + if value, ok := values[i].(*sql.NullBool); !ok { + return fmt.Errorf("unexpected type %T for field revoked", values[i]) + } else if value.Valid { + _m.Revoked = value.Bool + } + case useraccesstoken.FieldExpiresAt: + if value, ok := values[i].(*sql.NullTime); !ok { + return fmt.Errorf("unexpected type %T for field expires_at", values[i]) + } else if value.Valid { + _m.ExpiresAt = new(time.Time) + *_m.ExpiresAt = value.Time + } + case useraccesstoken.FieldLastUsed: + if value, ok := values[i].(*sql.NullTime); !ok { + return fmt.Errorf("unexpected type %T for field last_used", values[i]) + } else if value.Valid { + _m.LastUsed = new(time.Time) + *_m.LastUsed = value.Time + } + case useraccesstoken.FieldCreated: + if value, ok := values[i].(*sql.NullTime); !ok { + return fmt.Errorf("unexpected type %T for field created", values[i]) + } else if value.Valid { + _m.Created = value.Time + } + default: + _m.selectValues.Set(columns[i], values[i]) + } + } + return nil +} + +// Value returns the ent.Value that was dynamically selected and assigned to the UserAccessToken. +// This includes values selected through modifiers, order, etc. +func (_m *UserAccessToken) Value(name string) (ent.Value, error) { + return _m.selectValues.Get(name) +} + +// Update returns a builder for updating this UserAccessToken. +// Note that you need to call UserAccessToken.Unwrap() before calling this method if this UserAccessToken +// was returned from a transaction, and the transaction was committed or rolled back. +func (_m *UserAccessToken) Update() *UserAccessTokenUpdateOne { + return NewUserAccessTokenClient(_m.config).UpdateOne(_m) +} + +// Unwrap unwraps the UserAccessToken entity that was returned from a transaction after it was closed, +// so that all future queries will be executed through the driver which created the transaction. +func (_m *UserAccessToken) Unwrap() *UserAccessToken { + _tx, ok := _m.config.driver.(*txDriver) + if !ok { + panic("ent: UserAccessToken is not a transactional entity") + } + _m.config.driver = _tx.drv + return _m +} + +// String implements the fmt.Stringer. +func (_m *UserAccessToken) String() string { + var builder strings.Builder + builder.WriteString("UserAccessToken(") + builder.WriteString(fmt.Sprintf("id=%v, ", _m.ID)) + builder.WriteString("user_id=") + builder.WriteString(fmt.Sprintf("%v", _m.UserID)) + builder.WriteString(", ") + builder.WriteString("name=") + builder.WriteString(_m.Name) + builder.WriteString(", ") + builder.WriteString("prefix=") + builder.WriteString(_m.Prefix) + builder.WriteString(", ") + builder.WriteString("key_hash=") + builder.WriteString(", ") + builder.WriteString("project_id=") + builder.WriteString(fmt.Sprintf("%v", _m.ProjectID)) + builder.WriteString(", ") + builder.WriteString("scopes=") + builder.WriteString(_m.Scopes) + builder.WriteString(", ") + builder.WriteString("revoked=") + builder.WriteString(fmt.Sprintf("%v", _m.Revoked)) + builder.WriteString(", ") + if v := _m.ExpiresAt; v != nil { + builder.WriteString("expires_at=") + builder.WriteString(v.Format(time.ANSIC)) + } + builder.WriteString(", ") + if v := _m.LastUsed; v != nil { + builder.WriteString("last_used=") + builder.WriteString(v.Format(time.ANSIC)) + } + builder.WriteString(", ") + builder.WriteString("created=") + builder.WriteString(_m.Created.Format(time.ANSIC)) + builder.WriteByte(')') + return builder.String() +} + +// UserAccessTokens is a parsable slice of UserAccessToken. +type UserAccessTokens []*UserAccessToken diff --git a/pkg/ent/useraccesstoken/useraccesstoken.go b/pkg/ent/useraccesstoken/useraccesstoken.go new file mode 100644 index 000000000..5e6496395 --- /dev/null +++ b/pkg/ent/useraccesstoken/useraccesstoken.go @@ -0,0 +1,139 @@ +// Code generated by ent, DO NOT EDIT. + +package useraccesstoken + +import ( + "time" + + "entgo.io/ent/dialect/sql" + "github.com/google/uuid" +) + +const ( + // Label holds the string label denoting the useraccesstoken type in the database. + Label = "user_access_token" + // FieldID holds the string denoting the id field in the database. + FieldID = "id" + // FieldUserID holds the string denoting the user_id field in the database. + FieldUserID = "user_id" + // FieldName holds the string denoting the name field in the database. + FieldName = "name" + // FieldPrefix holds the string denoting the prefix field in the database. + FieldPrefix = "prefix" + // FieldKeyHash holds the string denoting the key_hash field in the database. + FieldKeyHash = "key_hash" + // FieldProjectID holds the string denoting the project_id field in the database. + FieldProjectID = "project_id" + // FieldScopes holds the string denoting the scopes field in the database. + FieldScopes = "scopes" + // FieldRevoked holds the string denoting the revoked field in the database. + FieldRevoked = "revoked" + // FieldExpiresAt holds the string denoting the expires_at field in the database. + FieldExpiresAt = "expires_at" + // FieldLastUsed holds the string denoting the last_used field in the database. + FieldLastUsed = "last_used" + // FieldCreated holds the string denoting the created field in the database. + FieldCreated = "created" + // Table holds the table name of the useraccesstoken in the database. + Table = "user_access_tokens" +) + +// Columns holds all SQL columns for useraccesstoken fields. +var Columns = []string{ + FieldID, + FieldUserID, + FieldName, + FieldPrefix, + FieldKeyHash, + FieldProjectID, + FieldScopes, + FieldRevoked, + FieldExpiresAt, + FieldLastUsed, + FieldCreated, +} + +// ValidColumn reports if the column name is valid (part of the table columns). +func ValidColumn(column string) bool { + for i := range Columns { + if column == Columns[i] { + return true + } + } + return false +} + +var ( + // NameValidator is a validator for the "name" field. It is called by the builders before save. + NameValidator func(string) error + // PrefixValidator is a validator for the "prefix" field. It is called by the builders before save. + PrefixValidator func(string) error + // KeyHashValidator is a validator for the "key_hash" field. It is called by the builders before save. + KeyHashValidator func(string) error + // ScopesValidator is a validator for the "scopes" field. It is called by the builders before save. + ScopesValidator func(string) error + // DefaultRevoked holds the default value on creation for the "revoked" field. + DefaultRevoked bool + // DefaultCreated holds the default value on creation for the "created" field. + DefaultCreated func() time.Time + // DefaultID holds the default value on creation for the "id" field. + DefaultID func() uuid.UUID +) + +// OrderOption defines the ordering options for the UserAccessToken queries. +type OrderOption func(*sql.Selector) + +// ByID orders the results by the id field. +func ByID(opts ...sql.OrderTermOption) OrderOption { + return sql.OrderByField(FieldID, opts...).ToFunc() +} + +// ByUserID orders the results by the user_id field. +func ByUserID(opts ...sql.OrderTermOption) OrderOption { + return sql.OrderByField(FieldUserID, opts...).ToFunc() +} + +// ByName orders the results by the name field. +func ByName(opts ...sql.OrderTermOption) OrderOption { + return sql.OrderByField(FieldName, opts...).ToFunc() +} + +// ByPrefix orders the results by the prefix field. +func ByPrefix(opts ...sql.OrderTermOption) OrderOption { + return sql.OrderByField(FieldPrefix, opts...).ToFunc() +} + +// ByKeyHash orders the results by the key_hash field. +func ByKeyHash(opts ...sql.OrderTermOption) OrderOption { + return sql.OrderByField(FieldKeyHash, opts...).ToFunc() +} + +// ByProjectID orders the results by the project_id field. +func ByProjectID(opts ...sql.OrderTermOption) OrderOption { + return sql.OrderByField(FieldProjectID, opts...).ToFunc() +} + +// ByScopes orders the results by the scopes field. +func ByScopes(opts ...sql.OrderTermOption) OrderOption { + return sql.OrderByField(FieldScopes, opts...).ToFunc() +} + +// ByRevoked orders the results by the revoked field. +func ByRevoked(opts ...sql.OrderTermOption) OrderOption { + return sql.OrderByField(FieldRevoked, opts...).ToFunc() +} + +// ByExpiresAt orders the results by the expires_at field. +func ByExpiresAt(opts ...sql.OrderTermOption) OrderOption { + return sql.OrderByField(FieldExpiresAt, opts...).ToFunc() +} + +// ByLastUsed orders the results by the last_used field. +func ByLastUsed(opts ...sql.OrderTermOption) OrderOption { + return sql.OrderByField(FieldLastUsed, opts...).ToFunc() +} + +// ByCreated orders the results by the created field. +func ByCreated(opts ...sql.OrderTermOption) OrderOption { + return sql.OrderByField(FieldCreated, opts...).ToFunc() +} diff --git a/pkg/ent/useraccesstoken/where.go b/pkg/ent/useraccesstoken/where.go new file mode 100644 index 000000000..8481dd3ec --- /dev/null +++ b/pkg/ent/useraccesstoken/where.go @@ -0,0 +1,611 @@ +// Code generated by ent, DO NOT EDIT. + +package useraccesstoken + +import ( + "time" + + "entgo.io/ent/dialect/sql" + "github.com/GoogleCloudPlatform/scion/pkg/ent/predicate" + "github.com/google/uuid" +) + +// ID filters vertices based on their ID field. +func ID(id uuid.UUID) predicate.UserAccessToken { + return predicate.UserAccessToken(sql.FieldEQ(FieldID, id)) +} + +// IDEQ applies the EQ predicate on the ID field. +func IDEQ(id uuid.UUID) predicate.UserAccessToken { + return predicate.UserAccessToken(sql.FieldEQ(FieldID, id)) +} + +// IDNEQ applies the NEQ predicate on the ID field. +func IDNEQ(id uuid.UUID) predicate.UserAccessToken { + return predicate.UserAccessToken(sql.FieldNEQ(FieldID, id)) +} + +// IDIn applies the In predicate on the ID field. +func IDIn(ids ...uuid.UUID) predicate.UserAccessToken { + return predicate.UserAccessToken(sql.FieldIn(FieldID, ids...)) +} + +// IDNotIn applies the NotIn predicate on the ID field. +func IDNotIn(ids ...uuid.UUID) predicate.UserAccessToken { + return predicate.UserAccessToken(sql.FieldNotIn(FieldID, ids...)) +} + +// IDGT applies the GT predicate on the ID field. +func IDGT(id uuid.UUID) predicate.UserAccessToken { + return predicate.UserAccessToken(sql.FieldGT(FieldID, id)) +} + +// IDGTE applies the GTE predicate on the ID field. +func IDGTE(id uuid.UUID) predicate.UserAccessToken { + return predicate.UserAccessToken(sql.FieldGTE(FieldID, id)) +} + +// IDLT applies the LT predicate on the ID field. +func IDLT(id uuid.UUID) predicate.UserAccessToken { + return predicate.UserAccessToken(sql.FieldLT(FieldID, id)) +} + +// IDLTE applies the LTE predicate on the ID field. +func IDLTE(id uuid.UUID) predicate.UserAccessToken { + return predicate.UserAccessToken(sql.FieldLTE(FieldID, id)) +} + +// UserID applies equality check predicate on the "user_id" field. It's identical to UserIDEQ. +func UserID(v uuid.UUID) predicate.UserAccessToken { + return predicate.UserAccessToken(sql.FieldEQ(FieldUserID, v)) +} + +// Name applies equality check predicate on the "name" field. It's identical to NameEQ. +func Name(v string) predicate.UserAccessToken { + return predicate.UserAccessToken(sql.FieldEQ(FieldName, v)) +} + +// Prefix applies equality check predicate on the "prefix" field. It's identical to PrefixEQ. +func Prefix(v string) predicate.UserAccessToken { + return predicate.UserAccessToken(sql.FieldEQ(FieldPrefix, v)) +} + +// KeyHash applies equality check predicate on the "key_hash" field. It's identical to KeyHashEQ. +func KeyHash(v string) predicate.UserAccessToken { + return predicate.UserAccessToken(sql.FieldEQ(FieldKeyHash, v)) +} + +// ProjectID applies equality check predicate on the "project_id" field. It's identical to ProjectIDEQ. +func ProjectID(v uuid.UUID) predicate.UserAccessToken { + return predicate.UserAccessToken(sql.FieldEQ(FieldProjectID, v)) +} + +// Scopes applies equality check predicate on the "scopes" field. It's identical to ScopesEQ. +func Scopes(v string) predicate.UserAccessToken { + return predicate.UserAccessToken(sql.FieldEQ(FieldScopes, v)) +} + +// Revoked applies equality check predicate on the "revoked" field. It's identical to RevokedEQ. +func Revoked(v bool) predicate.UserAccessToken { + return predicate.UserAccessToken(sql.FieldEQ(FieldRevoked, v)) +} + +// ExpiresAt applies equality check predicate on the "expires_at" field. It's identical to ExpiresAtEQ. +func ExpiresAt(v time.Time) predicate.UserAccessToken { + return predicate.UserAccessToken(sql.FieldEQ(FieldExpiresAt, v)) +} + +// LastUsed applies equality check predicate on the "last_used" field. It's identical to LastUsedEQ. +func LastUsed(v time.Time) predicate.UserAccessToken { + return predicate.UserAccessToken(sql.FieldEQ(FieldLastUsed, v)) +} + +// Created applies equality check predicate on the "created" field. It's identical to CreatedEQ. +func Created(v time.Time) predicate.UserAccessToken { + return predicate.UserAccessToken(sql.FieldEQ(FieldCreated, v)) +} + +// UserIDEQ applies the EQ predicate on the "user_id" field. +func UserIDEQ(v uuid.UUID) predicate.UserAccessToken { + return predicate.UserAccessToken(sql.FieldEQ(FieldUserID, v)) +} + +// UserIDNEQ applies the NEQ predicate on the "user_id" field. +func UserIDNEQ(v uuid.UUID) predicate.UserAccessToken { + return predicate.UserAccessToken(sql.FieldNEQ(FieldUserID, v)) +} + +// UserIDIn applies the In predicate on the "user_id" field. +func UserIDIn(vs ...uuid.UUID) predicate.UserAccessToken { + return predicate.UserAccessToken(sql.FieldIn(FieldUserID, vs...)) +} + +// UserIDNotIn applies the NotIn predicate on the "user_id" field. +func UserIDNotIn(vs ...uuid.UUID) predicate.UserAccessToken { + return predicate.UserAccessToken(sql.FieldNotIn(FieldUserID, vs...)) +} + +// UserIDGT applies the GT predicate on the "user_id" field. +func UserIDGT(v uuid.UUID) predicate.UserAccessToken { + return predicate.UserAccessToken(sql.FieldGT(FieldUserID, v)) +} + +// UserIDGTE applies the GTE predicate on the "user_id" field. +func UserIDGTE(v uuid.UUID) predicate.UserAccessToken { + return predicate.UserAccessToken(sql.FieldGTE(FieldUserID, v)) +} + +// UserIDLT applies the LT predicate on the "user_id" field. +func UserIDLT(v uuid.UUID) predicate.UserAccessToken { + return predicate.UserAccessToken(sql.FieldLT(FieldUserID, v)) +} + +// UserIDLTE applies the LTE predicate on the "user_id" field. +func UserIDLTE(v uuid.UUID) predicate.UserAccessToken { + return predicate.UserAccessToken(sql.FieldLTE(FieldUserID, v)) +} + +// NameEQ applies the EQ predicate on the "name" field. +func NameEQ(v string) predicate.UserAccessToken { + return predicate.UserAccessToken(sql.FieldEQ(FieldName, v)) +} + +// NameNEQ applies the NEQ predicate on the "name" field. +func NameNEQ(v string) predicate.UserAccessToken { + return predicate.UserAccessToken(sql.FieldNEQ(FieldName, v)) +} + +// NameIn applies the In predicate on the "name" field. +func NameIn(vs ...string) predicate.UserAccessToken { + return predicate.UserAccessToken(sql.FieldIn(FieldName, vs...)) +} + +// NameNotIn applies the NotIn predicate on the "name" field. +func NameNotIn(vs ...string) predicate.UserAccessToken { + return predicate.UserAccessToken(sql.FieldNotIn(FieldName, vs...)) +} + +// NameGT applies the GT predicate on the "name" field. +func NameGT(v string) predicate.UserAccessToken { + return predicate.UserAccessToken(sql.FieldGT(FieldName, v)) +} + +// NameGTE applies the GTE predicate on the "name" field. +func NameGTE(v string) predicate.UserAccessToken { + return predicate.UserAccessToken(sql.FieldGTE(FieldName, v)) +} + +// NameLT applies the LT predicate on the "name" field. +func NameLT(v string) predicate.UserAccessToken { + return predicate.UserAccessToken(sql.FieldLT(FieldName, v)) +} + +// NameLTE applies the LTE predicate on the "name" field. +func NameLTE(v string) predicate.UserAccessToken { + return predicate.UserAccessToken(sql.FieldLTE(FieldName, v)) +} + +// NameContains applies the Contains predicate on the "name" field. +func NameContains(v string) predicate.UserAccessToken { + return predicate.UserAccessToken(sql.FieldContains(FieldName, v)) +} + +// NameHasPrefix applies the HasPrefix predicate on the "name" field. +func NameHasPrefix(v string) predicate.UserAccessToken { + return predicate.UserAccessToken(sql.FieldHasPrefix(FieldName, v)) +} + +// NameHasSuffix applies the HasSuffix predicate on the "name" field. +func NameHasSuffix(v string) predicate.UserAccessToken { + return predicate.UserAccessToken(sql.FieldHasSuffix(FieldName, v)) +} + +// NameEqualFold applies the EqualFold predicate on the "name" field. +func NameEqualFold(v string) predicate.UserAccessToken { + return predicate.UserAccessToken(sql.FieldEqualFold(FieldName, v)) +} + +// NameContainsFold applies the ContainsFold predicate on the "name" field. +func NameContainsFold(v string) predicate.UserAccessToken { + return predicate.UserAccessToken(sql.FieldContainsFold(FieldName, v)) +} + +// PrefixEQ applies the EQ predicate on the "prefix" field. +func PrefixEQ(v string) predicate.UserAccessToken { + return predicate.UserAccessToken(sql.FieldEQ(FieldPrefix, v)) +} + +// PrefixNEQ applies the NEQ predicate on the "prefix" field. +func PrefixNEQ(v string) predicate.UserAccessToken { + return predicate.UserAccessToken(sql.FieldNEQ(FieldPrefix, v)) +} + +// PrefixIn applies the In predicate on the "prefix" field. +func PrefixIn(vs ...string) predicate.UserAccessToken { + return predicate.UserAccessToken(sql.FieldIn(FieldPrefix, vs...)) +} + +// PrefixNotIn applies the NotIn predicate on the "prefix" field. +func PrefixNotIn(vs ...string) predicate.UserAccessToken { + return predicate.UserAccessToken(sql.FieldNotIn(FieldPrefix, vs...)) +} + +// PrefixGT applies the GT predicate on the "prefix" field. +func PrefixGT(v string) predicate.UserAccessToken { + return predicate.UserAccessToken(sql.FieldGT(FieldPrefix, v)) +} + +// PrefixGTE applies the GTE predicate on the "prefix" field. +func PrefixGTE(v string) predicate.UserAccessToken { + return predicate.UserAccessToken(sql.FieldGTE(FieldPrefix, v)) +} + +// PrefixLT applies the LT predicate on the "prefix" field. +func PrefixLT(v string) predicate.UserAccessToken { + return predicate.UserAccessToken(sql.FieldLT(FieldPrefix, v)) +} + +// PrefixLTE applies the LTE predicate on the "prefix" field. +func PrefixLTE(v string) predicate.UserAccessToken { + return predicate.UserAccessToken(sql.FieldLTE(FieldPrefix, v)) +} + +// PrefixContains applies the Contains predicate on the "prefix" field. +func PrefixContains(v string) predicate.UserAccessToken { + return predicate.UserAccessToken(sql.FieldContains(FieldPrefix, v)) +} + +// PrefixHasPrefix applies the HasPrefix predicate on the "prefix" field. +func PrefixHasPrefix(v string) predicate.UserAccessToken { + return predicate.UserAccessToken(sql.FieldHasPrefix(FieldPrefix, v)) +} + +// PrefixHasSuffix applies the HasSuffix predicate on the "prefix" field. +func PrefixHasSuffix(v string) predicate.UserAccessToken { + return predicate.UserAccessToken(sql.FieldHasSuffix(FieldPrefix, v)) +} + +// PrefixEqualFold applies the EqualFold predicate on the "prefix" field. +func PrefixEqualFold(v string) predicate.UserAccessToken { + return predicate.UserAccessToken(sql.FieldEqualFold(FieldPrefix, v)) +} + +// PrefixContainsFold applies the ContainsFold predicate on the "prefix" field. +func PrefixContainsFold(v string) predicate.UserAccessToken { + return predicate.UserAccessToken(sql.FieldContainsFold(FieldPrefix, v)) +} + +// KeyHashEQ applies the EQ predicate on the "key_hash" field. +func KeyHashEQ(v string) predicate.UserAccessToken { + return predicate.UserAccessToken(sql.FieldEQ(FieldKeyHash, v)) +} + +// KeyHashNEQ applies the NEQ predicate on the "key_hash" field. +func KeyHashNEQ(v string) predicate.UserAccessToken { + return predicate.UserAccessToken(sql.FieldNEQ(FieldKeyHash, v)) +} + +// KeyHashIn applies the In predicate on the "key_hash" field. +func KeyHashIn(vs ...string) predicate.UserAccessToken { + return predicate.UserAccessToken(sql.FieldIn(FieldKeyHash, vs...)) +} + +// KeyHashNotIn applies the NotIn predicate on the "key_hash" field. +func KeyHashNotIn(vs ...string) predicate.UserAccessToken { + return predicate.UserAccessToken(sql.FieldNotIn(FieldKeyHash, vs...)) +} + +// KeyHashGT applies the GT predicate on the "key_hash" field. +func KeyHashGT(v string) predicate.UserAccessToken { + return predicate.UserAccessToken(sql.FieldGT(FieldKeyHash, v)) +} + +// KeyHashGTE applies the GTE predicate on the "key_hash" field. +func KeyHashGTE(v string) predicate.UserAccessToken { + return predicate.UserAccessToken(sql.FieldGTE(FieldKeyHash, v)) +} + +// KeyHashLT applies the LT predicate on the "key_hash" field. +func KeyHashLT(v string) predicate.UserAccessToken { + return predicate.UserAccessToken(sql.FieldLT(FieldKeyHash, v)) +} + +// KeyHashLTE applies the LTE predicate on the "key_hash" field. +func KeyHashLTE(v string) predicate.UserAccessToken { + return predicate.UserAccessToken(sql.FieldLTE(FieldKeyHash, v)) +} + +// KeyHashContains applies the Contains predicate on the "key_hash" field. +func KeyHashContains(v string) predicate.UserAccessToken { + return predicate.UserAccessToken(sql.FieldContains(FieldKeyHash, v)) +} + +// KeyHashHasPrefix applies the HasPrefix predicate on the "key_hash" field. +func KeyHashHasPrefix(v string) predicate.UserAccessToken { + return predicate.UserAccessToken(sql.FieldHasPrefix(FieldKeyHash, v)) +} + +// KeyHashHasSuffix applies the HasSuffix predicate on the "key_hash" field. +func KeyHashHasSuffix(v string) predicate.UserAccessToken { + return predicate.UserAccessToken(sql.FieldHasSuffix(FieldKeyHash, v)) +} + +// KeyHashEqualFold applies the EqualFold predicate on the "key_hash" field. +func KeyHashEqualFold(v string) predicate.UserAccessToken { + return predicate.UserAccessToken(sql.FieldEqualFold(FieldKeyHash, v)) +} + +// KeyHashContainsFold applies the ContainsFold predicate on the "key_hash" field. +func KeyHashContainsFold(v string) predicate.UserAccessToken { + return predicate.UserAccessToken(sql.FieldContainsFold(FieldKeyHash, v)) +} + +// ProjectIDEQ applies the EQ predicate on the "project_id" field. +func ProjectIDEQ(v uuid.UUID) predicate.UserAccessToken { + return predicate.UserAccessToken(sql.FieldEQ(FieldProjectID, v)) +} + +// ProjectIDNEQ applies the NEQ predicate on the "project_id" field. +func ProjectIDNEQ(v uuid.UUID) predicate.UserAccessToken { + return predicate.UserAccessToken(sql.FieldNEQ(FieldProjectID, v)) +} + +// ProjectIDIn applies the In predicate on the "project_id" field. +func ProjectIDIn(vs ...uuid.UUID) predicate.UserAccessToken { + return predicate.UserAccessToken(sql.FieldIn(FieldProjectID, vs...)) +} + +// ProjectIDNotIn applies the NotIn predicate on the "project_id" field. +func ProjectIDNotIn(vs ...uuid.UUID) predicate.UserAccessToken { + return predicate.UserAccessToken(sql.FieldNotIn(FieldProjectID, vs...)) +} + +// ProjectIDGT applies the GT predicate on the "project_id" field. +func ProjectIDGT(v uuid.UUID) predicate.UserAccessToken { + return predicate.UserAccessToken(sql.FieldGT(FieldProjectID, v)) +} + +// ProjectIDGTE applies the GTE predicate on the "project_id" field. +func ProjectIDGTE(v uuid.UUID) predicate.UserAccessToken { + return predicate.UserAccessToken(sql.FieldGTE(FieldProjectID, v)) +} + +// ProjectIDLT applies the LT predicate on the "project_id" field. +func ProjectIDLT(v uuid.UUID) predicate.UserAccessToken { + return predicate.UserAccessToken(sql.FieldLT(FieldProjectID, v)) +} + +// ProjectIDLTE applies the LTE predicate on the "project_id" field. +func ProjectIDLTE(v uuid.UUID) predicate.UserAccessToken { + return predicate.UserAccessToken(sql.FieldLTE(FieldProjectID, v)) +} + +// ScopesEQ applies the EQ predicate on the "scopes" field. +func ScopesEQ(v string) predicate.UserAccessToken { + return predicate.UserAccessToken(sql.FieldEQ(FieldScopes, v)) +} + +// ScopesNEQ applies the NEQ predicate on the "scopes" field. +func ScopesNEQ(v string) predicate.UserAccessToken { + return predicate.UserAccessToken(sql.FieldNEQ(FieldScopes, v)) +} + +// ScopesIn applies the In predicate on the "scopes" field. +func ScopesIn(vs ...string) predicate.UserAccessToken { + return predicate.UserAccessToken(sql.FieldIn(FieldScopes, vs...)) +} + +// ScopesNotIn applies the NotIn predicate on the "scopes" field. +func ScopesNotIn(vs ...string) predicate.UserAccessToken { + return predicate.UserAccessToken(sql.FieldNotIn(FieldScopes, vs...)) +} + +// ScopesGT applies the GT predicate on the "scopes" field. +func ScopesGT(v string) predicate.UserAccessToken { + return predicate.UserAccessToken(sql.FieldGT(FieldScopes, v)) +} + +// ScopesGTE applies the GTE predicate on the "scopes" field. +func ScopesGTE(v string) predicate.UserAccessToken { + return predicate.UserAccessToken(sql.FieldGTE(FieldScopes, v)) +} + +// ScopesLT applies the LT predicate on the "scopes" field. +func ScopesLT(v string) predicate.UserAccessToken { + return predicate.UserAccessToken(sql.FieldLT(FieldScopes, v)) +} + +// ScopesLTE applies the LTE predicate on the "scopes" field. +func ScopesLTE(v string) predicate.UserAccessToken { + return predicate.UserAccessToken(sql.FieldLTE(FieldScopes, v)) +} + +// ScopesContains applies the Contains predicate on the "scopes" field. +func ScopesContains(v string) predicate.UserAccessToken { + return predicate.UserAccessToken(sql.FieldContains(FieldScopes, v)) +} + +// ScopesHasPrefix applies the HasPrefix predicate on the "scopes" field. +func ScopesHasPrefix(v string) predicate.UserAccessToken { + return predicate.UserAccessToken(sql.FieldHasPrefix(FieldScopes, v)) +} + +// ScopesHasSuffix applies the HasSuffix predicate on the "scopes" field. +func ScopesHasSuffix(v string) predicate.UserAccessToken { + return predicate.UserAccessToken(sql.FieldHasSuffix(FieldScopes, v)) +} + +// ScopesEqualFold applies the EqualFold predicate on the "scopes" field. +func ScopesEqualFold(v string) predicate.UserAccessToken { + return predicate.UserAccessToken(sql.FieldEqualFold(FieldScopes, v)) +} + +// ScopesContainsFold applies the ContainsFold predicate on the "scopes" field. +func ScopesContainsFold(v string) predicate.UserAccessToken { + return predicate.UserAccessToken(sql.FieldContainsFold(FieldScopes, v)) +} + +// RevokedEQ applies the EQ predicate on the "revoked" field. +func RevokedEQ(v bool) predicate.UserAccessToken { + return predicate.UserAccessToken(sql.FieldEQ(FieldRevoked, v)) +} + +// RevokedNEQ applies the NEQ predicate on the "revoked" field. +func RevokedNEQ(v bool) predicate.UserAccessToken { + return predicate.UserAccessToken(sql.FieldNEQ(FieldRevoked, v)) +} + +// ExpiresAtEQ applies the EQ predicate on the "expires_at" field. +func ExpiresAtEQ(v time.Time) predicate.UserAccessToken { + return predicate.UserAccessToken(sql.FieldEQ(FieldExpiresAt, v)) +} + +// ExpiresAtNEQ applies the NEQ predicate on the "expires_at" field. +func ExpiresAtNEQ(v time.Time) predicate.UserAccessToken { + return predicate.UserAccessToken(sql.FieldNEQ(FieldExpiresAt, v)) +} + +// ExpiresAtIn applies the In predicate on the "expires_at" field. +func ExpiresAtIn(vs ...time.Time) predicate.UserAccessToken { + return predicate.UserAccessToken(sql.FieldIn(FieldExpiresAt, vs...)) +} + +// ExpiresAtNotIn applies the NotIn predicate on the "expires_at" field. +func ExpiresAtNotIn(vs ...time.Time) predicate.UserAccessToken { + return predicate.UserAccessToken(sql.FieldNotIn(FieldExpiresAt, vs...)) +} + +// ExpiresAtGT applies the GT predicate on the "expires_at" field. +func ExpiresAtGT(v time.Time) predicate.UserAccessToken { + return predicate.UserAccessToken(sql.FieldGT(FieldExpiresAt, v)) +} + +// ExpiresAtGTE applies the GTE predicate on the "expires_at" field. +func ExpiresAtGTE(v time.Time) predicate.UserAccessToken { + return predicate.UserAccessToken(sql.FieldGTE(FieldExpiresAt, v)) +} + +// ExpiresAtLT applies the LT predicate on the "expires_at" field. +func ExpiresAtLT(v time.Time) predicate.UserAccessToken { + return predicate.UserAccessToken(sql.FieldLT(FieldExpiresAt, v)) +} + +// ExpiresAtLTE applies the LTE predicate on the "expires_at" field. +func ExpiresAtLTE(v time.Time) predicate.UserAccessToken { + return predicate.UserAccessToken(sql.FieldLTE(FieldExpiresAt, v)) +} + +// ExpiresAtIsNil applies the IsNil predicate on the "expires_at" field. +func ExpiresAtIsNil() predicate.UserAccessToken { + return predicate.UserAccessToken(sql.FieldIsNull(FieldExpiresAt)) +} + +// ExpiresAtNotNil applies the NotNil predicate on the "expires_at" field. +func ExpiresAtNotNil() predicate.UserAccessToken { + return predicate.UserAccessToken(sql.FieldNotNull(FieldExpiresAt)) +} + +// LastUsedEQ applies the EQ predicate on the "last_used" field. +func LastUsedEQ(v time.Time) predicate.UserAccessToken { + return predicate.UserAccessToken(sql.FieldEQ(FieldLastUsed, v)) +} + +// LastUsedNEQ applies the NEQ predicate on the "last_used" field. +func LastUsedNEQ(v time.Time) predicate.UserAccessToken { + return predicate.UserAccessToken(sql.FieldNEQ(FieldLastUsed, v)) +} + +// LastUsedIn applies the In predicate on the "last_used" field. +func LastUsedIn(vs ...time.Time) predicate.UserAccessToken { + return predicate.UserAccessToken(sql.FieldIn(FieldLastUsed, vs...)) +} + +// LastUsedNotIn applies the NotIn predicate on the "last_used" field. +func LastUsedNotIn(vs ...time.Time) predicate.UserAccessToken { + return predicate.UserAccessToken(sql.FieldNotIn(FieldLastUsed, vs...)) +} + +// LastUsedGT applies the GT predicate on the "last_used" field. +func LastUsedGT(v time.Time) predicate.UserAccessToken { + return predicate.UserAccessToken(sql.FieldGT(FieldLastUsed, v)) +} + +// LastUsedGTE applies the GTE predicate on the "last_used" field. +func LastUsedGTE(v time.Time) predicate.UserAccessToken { + return predicate.UserAccessToken(sql.FieldGTE(FieldLastUsed, v)) +} + +// LastUsedLT applies the LT predicate on the "last_used" field. +func LastUsedLT(v time.Time) predicate.UserAccessToken { + return predicate.UserAccessToken(sql.FieldLT(FieldLastUsed, v)) +} + +// LastUsedLTE applies the LTE predicate on the "last_used" field. +func LastUsedLTE(v time.Time) predicate.UserAccessToken { + return predicate.UserAccessToken(sql.FieldLTE(FieldLastUsed, v)) +} + +// LastUsedIsNil applies the IsNil predicate on the "last_used" field. +func LastUsedIsNil() predicate.UserAccessToken { + return predicate.UserAccessToken(sql.FieldIsNull(FieldLastUsed)) +} + +// LastUsedNotNil applies the NotNil predicate on the "last_used" field. +func LastUsedNotNil() predicate.UserAccessToken { + return predicate.UserAccessToken(sql.FieldNotNull(FieldLastUsed)) +} + +// CreatedEQ applies the EQ predicate on the "created" field. +func CreatedEQ(v time.Time) predicate.UserAccessToken { + return predicate.UserAccessToken(sql.FieldEQ(FieldCreated, v)) +} + +// CreatedNEQ applies the NEQ predicate on the "created" field. +func CreatedNEQ(v time.Time) predicate.UserAccessToken { + return predicate.UserAccessToken(sql.FieldNEQ(FieldCreated, v)) +} + +// CreatedIn applies the In predicate on the "created" field. +func CreatedIn(vs ...time.Time) predicate.UserAccessToken { + return predicate.UserAccessToken(sql.FieldIn(FieldCreated, vs...)) +} + +// CreatedNotIn applies the NotIn predicate on the "created" field. +func CreatedNotIn(vs ...time.Time) predicate.UserAccessToken { + return predicate.UserAccessToken(sql.FieldNotIn(FieldCreated, vs...)) +} + +// CreatedGT applies the GT predicate on the "created" field. +func CreatedGT(v time.Time) predicate.UserAccessToken { + return predicate.UserAccessToken(sql.FieldGT(FieldCreated, v)) +} + +// CreatedGTE applies the GTE predicate on the "created" field. +func CreatedGTE(v time.Time) predicate.UserAccessToken { + return predicate.UserAccessToken(sql.FieldGTE(FieldCreated, v)) +} + +// CreatedLT applies the LT predicate on the "created" field. +func CreatedLT(v time.Time) predicate.UserAccessToken { + return predicate.UserAccessToken(sql.FieldLT(FieldCreated, v)) +} + +// CreatedLTE applies the LTE predicate on the "created" field. +func CreatedLTE(v time.Time) predicate.UserAccessToken { + return predicate.UserAccessToken(sql.FieldLTE(FieldCreated, v)) +} + +// And groups predicates with the AND operator between them. +func And(predicates ...predicate.UserAccessToken) predicate.UserAccessToken { + return predicate.UserAccessToken(sql.AndPredicates(predicates...)) +} + +// Or groups predicates with the OR operator between them. +func Or(predicates ...predicate.UserAccessToken) predicate.UserAccessToken { + return predicate.UserAccessToken(sql.OrPredicates(predicates...)) +} + +// Not applies the not operator on the given predicate. +func Not(p predicate.UserAccessToken) predicate.UserAccessToken { + return predicate.UserAccessToken(sql.NotPredicates(p)) +} diff --git a/pkg/ent/useraccesstoken_create.go b/pkg/ent/useraccesstoken_create.go new file mode 100644 index 000000000..8c3828e5a --- /dev/null +++ b/pkg/ent/useraccesstoken_create.go @@ -0,0 +1,1046 @@ +// Code generated by ent, DO NOT EDIT. + +package ent + +import ( + "context" + "errors" + "fmt" + "time" + + "entgo.io/ent/dialect" + "entgo.io/ent/dialect/sql" + "entgo.io/ent/dialect/sql/sqlgraph" + "entgo.io/ent/schema/field" + "github.com/GoogleCloudPlatform/scion/pkg/ent/useraccesstoken" + "github.com/google/uuid" +) + +// UserAccessTokenCreate is the builder for creating a UserAccessToken entity. +type UserAccessTokenCreate struct { + config + mutation *UserAccessTokenMutation + hooks []Hook + conflict []sql.ConflictOption +} + +// SetUserID sets the "user_id" field. +func (_c *UserAccessTokenCreate) SetUserID(v uuid.UUID) *UserAccessTokenCreate { + _c.mutation.SetUserID(v) + return _c +} + +// SetName sets the "name" field. +func (_c *UserAccessTokenCreate) SetName(v string) *UserAccessTokenCreate { + _c.mutation.SetName(v) + return _c +} + +// SetPrefix sets the "prefix" field. +func (_c *UserAccessTokenCreate) SetPrefix(v string) *UserAccessTokenCreate { + _c.mutation.SetPrefix(v) + return _c +} + +// SetKeyHash sets the "key_hash" field. +func (_c *UserAccessTokenCreate) SetKeyHash(v string) *UserAccessTokenCreate { + _c.mutation.SetKeyHash(v) + return _c +} + +// SetProjectID sets the "project_id" field. +func (_c *UserAccessTokenCreate) SetProjectID(v uuid.UUID) *UserAccessTokenCreate { + _c.mutation.SetProjectID(v) + return _c +} + +// SetScopes sets the "scopes" field. +func (_c *UserAccessTokenCreate) SetScopes(v string) *UserAccessTokenCreate { + _c.mutation.SetScopes(v) + return _c +} + +// SetRevoked sets the "revoked" field. +func (_c *UserAccessTokenCreate) SetRevoked(v bool) *UserAccessTokenCreate { + _c.mutation.SetRevoked(v) + return _c +} + +// SetNillableRevoked sets the "revoked" field if the given value is not nil. +func (_c *UserAccessTokenCreate) SetNillableRevoked(v *bool) *UserAccessTokenCreate { + if v != nil { + _c.SetRevoked(*v) + } + return _c +} + +// SetExpiresAt sets the "expires_at" field. +func (_c *UserAccessTokenCreate) SetExpiresAt(v time.Time) *UserAccessTokenCreate { + _c.mutation.SetExpiresAt(v) + return _c +} + +// SetNillableExpiresAt sets the "expires_at" field if the given value is not nil. +func (_c *UserAccessTokenCreate) SetNillableExpiresAt(v *time.Time) *UserAccessTokenCreate { + if v != nil { + _c.SetExpiresAt(*v) + } + return _c +} + +// SetLastUsed sets the "last_used" field. +func (_c *UserAccessTokenCreate) SetLastUsed(v time.Time) *UserAccessTokenCreate { + _c.mutation.SetLastUsed(v) + return _c +} + +// SetNillableLastUsed sets the "last_used" field if the given value is not nil. +func (_c *UserAccessTokenCreate) SetNillableLastUsed(v *time.Time) *UserAccessTokenCreate { + if v != nil { + _c.SetLastUsed(*v) + } + return _c +} + +// SetCreated sets the "created" field. +func (_c *UserAccessTokenCreate) SetCreated(v time.Time) *UserAccessTokenCreate { + _c.mutation.SetCreated(v) + return _c +} + +// SetNillableCreated sets the "created" field if the given value is not nil. +func (_c *UserAccessTokenCreate) SetNillableCreated(v *time.Time) *UserAccessTokenCreate { + if v != nil { + _c.SetCreated(*v) + } + return _c +} + +// SetID sets the "id" field. +func (_c *UserAccessTokenCreate) SetID(v uuid.UUID) *UserAccessTokenCreate { + _c.mutation.SetID(v) + return _c +} + +// SetNillableID sets the "id" field if the given value is not nil. +func (_c *UserAccessTokenCreate) SetNillableID(v *uuid.UUID) *UserAccessTokenCreate { + if v != nil { + _c.SetID(*v) + } + return _c +} + +// Mutation returns the UserAccessTokenMutation object of the builder. +func (_c *UserAccessTokenCreate) Mutation() *UserAccessTokenMutation { + return _c.mutation +} + +// Save creates the UserAccessToken in the database. +func (_c *UserAccessTokenCreate) Save(ctx context.Context) (*UserAccessToken, error) { + _c.defaults() + return withHooks(ctx, _c.sqlSave, _c.mutation, _c.hooks) +} + +// SaveX calls Save and panics if Save returns an error. +func (_c *UserAccessTokenCreate) SaveX(ctx context.Context) *UserAccessToken { + v, err := _c.Save(ctx) + if err != nil { + panic(err) + } + return v +} + +// Exec executes the query. +func (_c *UserAccessTokenCreate) Exec(ctx context.Context) error { + _, err := _c.Save(ctx) + return err +} + +// ExecX is like Exec, but panics if an error occurs. +func (_c *UserAccessTokenCreate) ExecX(ctx context.Context) { + if err := _c.Exec(ctx); err != nil { + panic(err) + } +} + +// defaults sets the default values of the builder before save. +func (_c *UserAccessTokenCreate) defaults() { + if _, ok := _c.mutation.Revoked(); !ok { + v := useraccesstoken.DefaultRevoked + _c.mutation.SetRevoked(v) + } + if _, ok := _c.mutation.Created(); !ok { + v := useraccesstoken.DefaultCreated() + _c.mutation.SetCreated(v) + } + if _, ok := _c.mutation.ID(); !ok { + v := useraccesstoken.DefaultID() + _c.mutation.SetID(v) + } +} + +// check runs all checks and user-defined validators on the builder. +func (_c *UserAccessTokenCreate) check() error { + if _, ok := _c.mutation.UserID(); !ok { + return &ValidationError{Name: "user_id", err: errors.New(`ent: missing required field "UserAccessToken.user_id"`)} + } + if _, ok := _c.mutation.Name(); !ok { + return &ValidationError{Name: "name", err: errors.New(`ent: missing required field "UserAccessToken.name"`)} + } + if v, ok := _c.mutation.Name(); ok { + if err := useraccesstoken.NameValidator(v); err != nil { + return &ValidationError{Name: "name", err: fmt.Errorf(`ent: validator failed for field "UserAccessToken.name": %w`, err)} + } + } + if _, ok := _c.mutation.Prefix(); !ok { + return &ValidationError{Name: "prefix", err: errors.New(`ent: missing required field "UserAccessToken.prefix"`)} + } + if v, ok := _c.mutation.Prefix(); ok { + if err := useraccesstoken.PrefixValidator(v); err != nil { + return &ValidationError{Name: "prefix", err: fmt.Errorf(`ent: validator failed for field "UserAccessToken.prefix": %w`, err)} + } + } + if _, ok := _c.mutation.KeyHash(); !ok { + return &ValidationError{Name: "key_hash", err: errors.New(`ent: missing required field "UserAccessToken.key_hash"`)} + } + if v, ok := _c.mutation.KeyHash(); ok { + if err := useraccesstoken.KeyHashValidator(v); err != nil { + return &ValidationError{Name: "key_hash", err: fmt.Errorf(`ent: validator failed for field "UserAccessToken.key_hash": %w`, err)} + } + } + if _, ok := _c.mutation.ProjectID(); !ok { + return &ValidationError{Name: "project_id", err: errors.New(`ent: missing required field "UserAccessToken.project_id"`)} + } + if _, ok := _c.mutation.Scopes(); !ok { + return &ValidationError{Name: "scopes", err: errors.New(`ent: missing required field "UserAccessToken.scopes"`)} + } + if v, ok := _c.mutation.Scopes(); ok { + if err := useraccesstoken.ScopesValidator(v); err != nil { + return &ValidationError{Name: "scopes", err: fmt.Errorf(`ent: validator failed for field "UserAccessToken.scopes": %w`, err)} + } + } + if _, ok := _c.mutation.Revoked(); !ok { + return &ValidationError{Name: "revoked", err: errors.New(`ent: missing required field "UserAccessToken.revoked"`)} + } + if _, ok := _c.mutation.Created(); !ok { + return &ValidationError{Name: "created", err: errors.New(`ent: missing required field "UserAccessToken.created"`)} + } + return nil +} + +func (_c *UserAccessTokenCreate) sqlSave(ctx context.Context) (*UserAccessToken, error) { + if err := _c.check(); err != nil { + return nil, err + } + _node, _spec := _c.createSpec() + if err := sqlgraph.CreateNode(ctx, _c.driver, _spec); err != nil { + if sqlgraph.IsConstraintError(err) { + err = &ConstraintError{msg: err.Error(), wrap: err} + } + return nil, err + } + if _spec.ID.Value != nil { + if id, ok := _spec.ID.Value.(*uuid.UUID); ok { + _node.ID = *id + } else if err := _node.ID.Scan(_spec.ID.Value); err != nil { + return nil, err + } + } + _c.mutation.id = &_node.ID + _c.mutation.done = true + return _node, nil +} + +func (_c *UserAccessTokenCreate) createSpec() (*UserAccessToken, *sqlgraph.CreateSpec) { + var ( + _node = &UserAccessToken{config: _c.config} + _spec = sqlgraph.NewCreateSpec(useraccesstoken.Table, sqlgraph.NewFieldSpec(useraccesstoken.FieldID, field.TypeUUID)) + ) + _spec.OnConflict = _c.conflict + if id, ok := _c.mutation.ID(); ok { + _node.ID = id + _spec.ID.Value = &id + } + if value, ok := _c.mutation.UserID(); ok { + _spec.SetField(useraccesstoken.FieldUserID, field.TypeUUID, value) + _node.UserID = value + } + if value, ok := _c.mutation.Name(); ok { + _spec.SetField(useraccesstoken.FieldName, field.TypeString, value) + _node.Name = value + } + if value, ok := _c.mutation.Prefix(); ok { + _spec.SetField(useraccesstoken.FieldPrefix, field.TypeString, value) + _node.Prefix = value + } + if value, ok := _c.mutation.KeyHash(); ok { + _spec.SetField(useraccesstoken.FieldKeyHash, field.TypeString, value) + _node.KeyHash = value + } + if value, ok := _c.mutation.ProjectID(); ok { + _spec.SetField(useraccesstoken.FieldProjectID, field.TypeUUID, value) + _node.ProjectID = value + } + if value, ok := _c.mutation.Scopes(); ok { + _spec.SetField(useraccesstoken.FieldScopes, field.TypeString, value) + _node.Scopes = value + } + if value, ok := _c.mutation.Revoked(); ok { + _spec.SetField(useraccesstoken.FieldRevoked, field.TypeBool, value) + _node.Revoked = value + } + if value, ok := _c.mutation.ExpiresAt(); ok { + _spec.SetField(useraccesstoken.FieldExpiresAt, field.TypeTime, value) + _node.ExpiresAt = &value + } + if value, ok := _c.mutation.LastUsed(); ok { + _spec.SetField(useraccesstoken.FieldLastUsed, field.TypeTime, value) + _node.LastUsed = &value + } + if value, ok := _c.mutation.Created(); ok { + _spec.SetField(useraccesstoken.FieldCreated, field.TypeTime, value) + _node.Created = value + } + return _node, _spec +} + +// OnConflict allows configuring the `ON CONFLICT` / `ON DUPLICATE KEY` clause +// of the `INSERT` statement. For example: +// +// client.UserAccessToken.Create(). +// SetUserID(v). +// OnConflict( +// // Update the row with the new values +// // the was proposed for insertion. +// sql.ResolveWithNewValues(), +// ). +// // Override some of the fields with custom +// // update values. +// Update(func(u *ent.UserAccessTokenUpsert) { +// SetUserID(v+v). +// }). +// Exec(ctx) +func (_c *UserAccessTokenCreate) OnConflict(opts ...sql.ConflictOption) *UserAccessTokenUpsertOne { + _c.conflict = opts + return &UserAccessTokenUpsertOne{ + create: _c, + } +} + +// OnConflictColumns calls `OnConflict` and configures the columns +// as conflict target. Using this option is equivalent to using: +// +// client.UserAccessToken.Create(). +// OnConflict(sql.ConflictColumns(columns...)). +// Exec(ctx) +func (_c *UserAccessTokenCreate) OnConflictColumns(columns ...string) *UserAccessTokenUpsertOne { + _c.conflict = append(_c.conflict, sql.ConflictColumns(columns...)) + return &UserAccessTokenUpsertOne{ + create: _c, + } +} + +type ( + // UserAccessTokenUpsertOne is the builder for "upsert"-ing + // one UserAccessToken node. + UserAccessTokenUpsertOne struct { + create *UserAccessTokenCreate + } + + // UserAccessTokenUpsert is the "OnConflict" setter. + UserAccessTokenUpsert struct { + *sql.UpdateSet + } +) + +// SetUserID sets the "user_id" field. +func (u *UserAccessTokenUpsert) SetUserID(v uuid.UUID) *UserAccessTokenUpsert { + u.Set(useraccesstoken.FieldUserID, v) + return u +} + +// UpdateUserID sets the "user_id" field to the value that was provided on create. +func (u *UserAccessTokenUpsert) UpdateUserID() *UserAccessTokenUpsert { + u.SetExcluded(useraccesstoken.FieldUserID) + return u +} + +// SetName sets the "name" field. +func (u *UserAccessTokenUpsert) SetName(v string) *UserAccessTokenUpsert { + u.Set(useraccesstoken.FieldName, v) + return u +} + +// UpdateName sets the "name" field to the value that was provided on create. +func (u *UserAccessTokenUpsert) UpdateName() *UserAccessTokenUpsert { + u.SetExcluded(useraccesstoken.FieldName) + return u +} + +// SetPrefix sets the "prefix" field. +func (u *UserAccessTokenUpsert) SetPrefix(v string) *UserAccessTokenUpsert { + u.Set(useraccesstoken.FieldPrefix, v) + return u +} + +// UpdatePrefix sets the "prefix" field to the value that was provided on create. +func (u *UserAccessTokenUpsert) UpdatePrefix() *UserAccessTokenUpsert { + u.SetExcluded(useraccesstoken.FieldPrefix) + return u +} + +// SetKeyHash sets the "key_hash" field. +func (u *UserAccessTokenUpsert) SetKeyHash(v string) *UserAccessTokenUpsert { + u.Set(useraccesstoken.FieldKeyHash, v) + return u +} + +// UpdateKeyHash sets the "key_hash" field to the value that was provided on create. +func (u *UserAccessTokenUpsert) UpdateKeyHash() *UserAccessTokenUpsert { + u.SetExcluded(useraccesstoken.FieldKeyHash) + return u +} + +// SetProjectID sets the "project_id" field. +func (u *UserAccessTokenUpsert) SetProjectID(v uuid.UUID) *UserAccessTokenUpsert { + u.Set(useraccesstoken.FieldProjectID, v) + return u +} + +// UpdateProjectID sets the "project_id" field to the value that was provided on create. +func (u *UserAccessTokenUpsert) UpdateProjectID() *UserAccessTokenUpsert { + u.SetExcluded(useraccesstoken.FieldProjectID) + return u +} + +// SetScopes sets the "scopes" field. +func (u *UserAccessTokenUpsert) SetScopes(v string) *UserAccessTokenUpsert { + u.Set(useraccesstoken.FieldScopes, v) + return u +} + +// UpdateScopes sets the "scopes" field to the value that was provided on create. +func (u *UserAccessTokenUpsert) UpdateScopes() *UserAccessTokenUpsert { + u.SetExcluded(useraccesstoken.FieldScopes) + return u +} + +// SetRevoked sets the "revoked" field. +func (u *UserAccessTokenUpsert) SetRevoked(v bool) *UserAccessTokenUpsert { + u.Set(useraccesstoken.FieldRevoked, v) + return u +} + +// UpdateRevoked sets the "revoked" field to the value that was provided on create. +func (u *UserAccessTokenUpsert) UpdateRevoked() *UserAccessTokenUpsert { + u.SetExcluded(useraccesstoken.FieldRevoked) + return u +} + +// SetExpiresAt sets the "expires_at" field. +func (u *UserAccessTokenUpsert) SetExpiresAt(v time.Time) *UserAccessTokenUpsert { + u.Set(useraccesstoken.FieldExpiresAt, v) + return u +} + +// UpdateExpiresAt sets the "expires_at" field to the value that was provided on create. +func (u *UserAccessTokenUpsert) UpdateExpiresAt() *UserAccessTokenUpsert { + u.SetExcluded(useraccesstoken.FieldExpiresAt) + return u +} + +// ClearExpiresAt clears the value of the "expires_at" field. +func (u *UserAccessTokenUpsert) ClearExpiresAt() *UserAccessTokenUpsert { + u.SetNull(useraccesstoken.FieldExpiresAt) + return u +} + +// SetLastUsed sets the "last_used" field. +func (u *UserAccessTokenUpsert) SetLastUsed(v time.Time) *UserAccessTokenUpsert { + u.Set(useraccesstoken.FieldLastUsed, v) + return u +} + +// UpdateLastUsed sets the "last_used" field to the value that was provided on create. +func (u *UserAccessTokenUpsert) UpdateLastUsed() *UserAccessTokenUpsert { + u.SetExcluded(useraccesstoken.FieldLastUsed) + return u +} + +// ClearLastUsed clears the value of the "last_used" field. +func (u *UserAccessTokenUpsert) ClearLastUsed() *UserAccessTokenUpsert { + u.SetNull(useraccesstoken.FieldLastUsed) + return u +} + +// UpdateNewValues updates the mutable fields using the new values that were set on create except the ID field. +// Using this option is equivalent to using: +// +// client.UserAccessToken.Create(). +// OnConflict( +// sql.ResolveWithNewValues(), +// sql.ResolveWith(func(u *sql.UpdateSet) { +// u.SetIgnore(useraccesstoken.FieldID) +// }), +// ). +// Exec(ctx) +func (u *UserAccessTokenUpsertOne) UpdateNewValues() *UserAccessTokenUpsertOne { + u.create.conflict = append(u.create.conflict, sql.ResolveWithNewValues()) + u.create.conflict = append(u.create.conflict, sql.ResolveWith(func(s *sql.UpdateSet) { + if _, exists := u.create.mutation.ID(); exists { + s.SetIgnore(useraccesstoken.FieldID) + } + if _, exists := u.create.mutation.Created(); exists { + s.SetIgnore(useraccesstoken.FieldCreated) + } + })) + return u +} + +// Ignore sets each column to itself in case of conflict. +// Using this option is equivalent to using: +// +// client.UserAccessToken.Create(). +// OnConflict(sql.ResolveWithIgnore()). +// Exec(ctx) +func (u *UserAccessTokenUpsertOne) Ignore() *UserAccessTokenUpsertOne { + u.create.conflict = append(u.create.conflict, sql.ResolveWithIgnore()) + return u +} + +// DoNothing configures the conflict_action to `DO NOTHING`. +// Supported only by SQLite and PostgreSQL. +func (u *UserAccessTokenUpsertOne) DoNothing() *UserAccessTokenUpsertOne { + u.create.conflict = append(u.create.conflict, sql.DoNothing()) + return u +} + +// Update allows overriding fields `UPDATE` values. See the UserAccessTokenCreate.OnConflict +// documentation for more info. +func (u *UserAccessTokenUpsertOne) Update(set func(*UserAccessTokenUpsert)) *UserAccessTokenUpsertOne { + u.create.conflict = append(u.create.conflict, sql.ResolveWith(func(update *sql.UpdateSet) { + set(&UserAccessTokenUpsert{UpdateSet: update}) + })) + return u +} + +// SetUserID sets the "user_id" field. +func (u *UserAccessTokenUpsertOne) SetUserID(v uuid.UUID) *UserAccessTokenUpsertOne { + return u.Update(func(s *UserAccessTokenUpsert) { + s.SetUserID(v) + }) +} + +// UpdateUserID sets the "user_id" field to the value that was provided on create. +func (u *UserAccessTokenUpsertOne) UpdateUserID() *UserAccessTokenUpsertOne { + return u.Update(func(s *UserAccessTokenUpsert) { + s.UpdateUserID() + }) +} + +// SetName sets the "name" field. +func (u *UserAccessTokenUpsertOne) SetName(v string) *UserAccessTokenUpsertOne { + return u.Update(func(s *UserAccessTokenUpsert) { + s.SetName(v) + }) +} + +// UpdateName sets the "name" field to the value that was provided on create. +func (u *UserAccessTokenUpsertOne) UpdateName() *UserAccessTokenUpsertOne { + return u.Update(func(s *UserAccessTokenUpsert) { + s.UpdateName() + }) +} + +// SetPrefix sets the "prefix" field. +func (u *UserAccessTokenUpsertOne) SetPrefix(v string) *UserAccessTokenUpsertOne { + return u.Update(func(s *UserAccessTokenUpsert) { + s.SetPrefix(v) + }) +} + +// UpdatePrefix sets the "prefix" field to the value that was provided on create. +func (u *UserAccessTokenUpsertOne) UpdatePrefix() *UserAccessTokenUpsertOne { + return u.Update(func(s *UserAccessTokenUpsert) { + s.UpdatePrefix() + }) +} + +// SetKeyHash sets the "key_hash" field. +func (u *UserAccessTokenUpsertOne) SetKeyHash(v string) *UserAccessTokenUpsertOne { + return u.Update(func(s *UserAccessTokenUpsert) { + s.SetKeyHash(v) + }) +} + +// UpdateKeyHash sets the "key_hash" field to the value that was provided on create. +func (u *UserAccessTokenUpsertOne) UpdateKeyHash() *UserAccessTokenUpsertOne { + return u.Update(func(s *UserAccessTokenUpsert) { + s.UpdateKeyHash() + }) +} + +// SetProjectID sets the "project_id" field. +func (u *UserAccessTokenUpsertOne) SetProjectID(v uuid.UUID) *UserAccessTokenUpsertOne { + return u.Update(func(s *UserAccessTokenUpsert) { + s.SetProjectID(v) + }) +} + +// UpdateProjectID sets the "project_id" field to the value that was provided on create. +func (u *UserAccessTokenUpsertOne) UpdateProjectID() *UserAccessTokenUpsertOne { + return u.Update(func(s *UserAccessTokenUpsert) { + s.UpdateProjectID() + }) +} + +// SetScopes sets the "scopes" field. +func (u *UserAccessTokenUpsertOne) SetScopes(v string) *UserAccessTokenUpsertOne { + return u.Update(func(s *UserAccessTokenUpsert) { + s.SetScopes(v) + }) +} + +// UpdateScopes sets the "scopes" field to the value that was provided on create. +func (u *UserAccessTokenUpsertOne) UpdateScopes() *UserAccessTokenUpsertOne { + return u.Update(func(s *UserAccessTokenUpsert) { + s.UpdateScopes() + }) +} + +// SetRevoked sets the "revoked" field. +func (u *UserAccessTokenUpsertOne) SetRevoked(v bool) *UserAccessTokenUpsertOne { + return u.Update(func(s *UserAccessTokenUpsert) { + s.SetRevoked(v) + }) +} + +// UpdateRevoked sets the "revoked" field to the value that was provided on create. +func (u *UserAccessTokenUpsertOne) UpdateRevoked() *UserAccessTokenUpsertOne { + return u.Update(func(s *UserAccessTokenUpsert) { + s.UpdateRevoked() + }) +} + +// SetExpiresAt sets the "expires_at" field. +func (u *UserAccessTokenUpsertOne) SetExpiresAt(v time.Time) *UserAccessTokenUpsertOne { + return u.Update(func(s *UserAccessTokenUpsert) { + s.SetExpiresAt(v) + }) +} + +// UpdateExpiresAt sets the "expires_at" field to the value that was provided on create. +func (u *UserAccessTokenUpsertOne) UpdateExpiresAt() *UserAccessTokenUpsertOne { + return u.Update(func(s *UserAccessTokenUpsert) { + s.UpdateExpiresAt() + }) +} + +// ClearExpiresAt clears the value of the "expires_at" field. +func (u *UserAccessTokenUpsertOne) ClearExpiresAt() *UserAccessTokenUpsertOne { + return u.Update(func(s *UserAccessTokenUpsert) { + s.ClearExpiresAt() + }) +} + +// SetLastUsed sets the "last_used" field. +func (u *UserAccessTokenUpsertOne) SetLastUsed(v time.Time) *UserAccessTokenUpsertOne { + return u.Update(func(s *UserAccessTokenUpsert) { + s.SetLastUsed(v) + }) +} + +// UpdateLastUsed sets the "last_used" field to the value that was provided on create. +func (u *UserAccessTokenUpsertOne) UpdateLastUsed() *UserAccessTokenUpsertOne { + return u.Update(func(s *UserAccessTokenUpsert) { + s.UpdateLastUsed() + }) +} + +// ClearLastUsed clears the value of the "last_used" field. +func (u *UserAccessTokenUpsertOne) ClearLastUsed() *UserAccessTokenUpsertOne { + return u.Update(func(s *UserAccessTokenUpsert) { + s.ClearLastUsed() + }) +} + +// Exec executes the query. +func (u *UserAccessTokenUpsertOne) Exec(ctx context.Context) error { + if len(u.create.conflict) == 0 { + return errors.New("ent: missing options for UserAccessTokenCreate.OnConflict") + } + return u.create.Exec(ctx) +} + +// ExecX is like Exec, but panics if an error occurs. +func (u *UserAccessTokenUpsertOne) ExecX(ctx context.Context) { + if err := u.create.Exec(ctx); err != nil { + panic(err) + } +} + +// Exec executes the UPSERT query and returns the inserted/updated ID. +func (u *UserAccessTokenUpsertOne) ID(ctx context.Context) (id uuid.UUID, err error) { + if u.create.driver.Dialect() == dialect.MySQL { + // In case of "ON CONFLICT", there is no way to get back non-numeric ID + // fields from the database since MySQL does not support the RETURNING clause. + return id, errors.New("ent: UserAccessTokenUpsertOne.ID is not supported by MySQL driver. Use UserAccessTokenUpsertOne.Exec instead") + } + node, err := u.create.Save(ctx) + if err != nil { + return id, err + } + return node.ID, nil +} + +// IDX is like ID, but panics if an error occurs. +func (u *UserAccessTokenUpsertOne) IDX(ctx context.Context) uuid.UUID { + id, err := u.ID(ctx) + if err != nil { + panic(err) + } + return id +} + +// UserAccessTokenCreateBulk is the builder for creating many UserAccessToken entities in bulk. +type UserAccessTokenCreateBulk struct { + config + err error + builders []*UserAccessTokenCreate + conflict []sql.ConflictOption +} + +// Save creates the UserAccessToken entities in the database. +func (_c *UserAccessTokenCreateBulk) Save(ctx context.Context) ([]*UserAccessToken, error) { + if _c.err != nil { + return nil, _c.err + } + specs := make([]*sqlgraph.CreateSpec, len(_c.builders)) + nodes := make([]*UserAccessToken, len(_c.builders)) + mutators := make([]Mutator, len(_c.builders)) + for i := range _c.builders { + func(i int, root context.Context) { + builder := _c.builders[i] + builder.defaults() + var mut Mutator = MutateFunc(func(ctx context.Context, m Mutation) (Value, error) { + mutation, ok := m.(*UserAccessTokenMutation) + if !ok { + return nil, fmt.Errorf("unexpected mutation type %T", m) + } + if err := builder.check(); err != nil { + return nil, err + } + builder.mutation = mutation + var err error + nodes[i], specs[i] = builder.createSpec() + if i < len(mutators)-1 { + _, err = mutators[i+1].Mutate(root, _c.builders[i+1].mutation) + } else { + spec := &sqlgraph.BatchCreateSpec{Nodes: specs} + spec.OnConflict = _c.conflict + // Invoke the actual operation on the latest mutation in the chain. + if err = sqlgraph.BatchCreate(ctx, _c.driver, spec); err != nil { + if sqlgraph.IsConstraintError(err) { + err = &ConstraintError{msg: err.Error(), wrap: err} + } + } + } + if err != nil { + return nil, err + } + mutation.id = &nodes[i].ID + mutation.done = true + return nodes[i], nil + }) + for i := len(builder.hooks) - 1; i >= 0; i-- { + mut = builder.hooks[i](mut) + } + mutators[i] = mut + }(i, ctx) + } + if len(mutators) > 0 { + if _, err := mutators[0].Mutate(ctx, _c.builders[0].mutation); err != nil { + return nil, err + } + } + return nodes, nil +} + +// SaveX is like Save, but panics if an error occurs. +func (_c *UserAccessTokenCreateBulk) SaveX(ctx context.Context) []*UserAccessToken { + v, err := _c.Save(ctx) + if err != nil { + panic(err) + } + return v +} + +// Exec executes the query. +func (_c *UserAccessTokenCreateBulk) Exec(ctx context.Context) error { + _, err := _c.Save(ctx) + return err +} + +// ExecX is like Exec, but panics if an error occurs. +func (_c *UserAccessTokenCreateBulk) ExecX(ctx context.Context) { + if err := _c.Exec(ctx); err != nil { + panic(err) + } +} + +// OnConflict allows configuring the `ON CONFLICT` / `ON DUPLICATE KEY` clause +// of the `INSERT` statement. For example: +// +// client.UserAccessToken.CreateBulk(builders...). +// OnConflict( +// // Update the row with the new values +// // the was proposed for insertion. +// sql.ResolveWithNewValues(), +// ). +// // Override some of the fields with custom +// // update values. +// Update(func(u *ent.UserAccessTokenUpsert) { +// SetUserID(v+v). +// }). +// Exec(ctx) +func (_c *UserAccessTokenCreateBulk) OnConflict(opts ...sql.ConflictOption) *UserAccessTokenUpsertBulk { + _c.conflict = opts + return &UserAccessTokenUpsertBulk{ + create: _c, + } +} + +// OnConflictColumns calls `OnConflict` and configures the columns +// as conflict target. Using this option is equivalent to using: +// +// client.UserAccessToken.Create(). +// OnConflict(sql.ConflictColumns(columns...)). +// Exec(ctx) +func (_c *UserAccessTokenCreateBulk) OnConflictColumns(columns ...string) *UserAccessTokenUpsertBulk { + _c.conflict = append(_c.conflict, sql.ConflictColumns(columns...)) + return &UserAccessTokenUpsertBulk{ + create: _c, + } +} + +// UserAccessTokenUpsertBulk is the builder for "upsert"-ing +// a bulk of UserAccessToken nodes. +type UserAccessTokenUpsertBulk struct { + create *UserAccessTokenCreateBulk +} + +// UpdateNewValues updates the mutable fields using the new values that +// were set on create. Using this option is equivalent to using: +// +// client.UserAccessToken.Create(). +// OnConflict( +// sql.ResolveWithNewValues(), +// sql.ResolveWith(func(u *sql.UpdateSet) { +// u.SetIgnore(useraccesstoken.FieldID) +// }), +// ). +// Exec(ctx) +func (u *UserAccessTokenUpsertBulk) UpdateNewValues() *UserAccessTokenUpsertBulk { + u.create.conflict = append(u.create.conflict, sql.ResolveWithNewValues()) + u.create.conflict = append(u.create.conflict, sql.ResolveWith(func(s *sql.UpdateSet) { + for _, b := range u.create.builders { + if _, exists := b.mutation.ID(); exists { + s.SetIgnore(useraccesstoken.FieldID) + } + if _, exists := b.mutation.Created(); exists { + s.SetIgnore(useraccesstoken.FieldCreated) + } + } + })) + return u +} + +// Ignore sets each column to itself in case of conflict. +// Using this option is equivalent to using: +// +// client.UserAccessToken.Create(). +// OnConflict(sql.ResolveWithIgnore()). +// Exec(ctx) +func (u *UserAccessTokenUpsertBulk) Ignore() *UserAccessTokenUpsertBulk { + u.create.conflict = append(u.create.conflict, sql.ResolveWithIgnore()) + return u +} + +// DoNothing configures the conflict_action to `DO NOTHING`. +// Supported only by SQLite and PostgreSQL. +func (u *UserAccessTokenUpsertBulk) DoNothing() *UserAccessTokenUpsertBulk { + u.create.conflict = append(u.create.conflict, sql.DoNothing()) + return u +} + +// Update allows overriding fields `UPDATE` values. See the UserAccessTokenCreateBulk.OnConflict +// documentation for more info. +func (u *UserAccessTokenUpsertBulk) Update(set func(*UserAccessTokenUpsert)) *UserAccessTokenUpsertBulk { + u.create.conflict = append(u.create.conflict, sql.ResolveWith(func(update *sql.UpdateSet) { + set(&UserAccessTokenUpsert{UpdateSet: update}) + })) + return u +} + +// SetUserID sets the "user_id" field. +func (u *UserAccessTokenUpsertBulk) SetUserID(v uuid.UUID) *UserAccessTokenUpsertBulk { + return u.Update(func(s *UserAccessTokenUpsert) { + s.SetUserID(v) + }) +} + +// UpdateUserID sets the "user_id" field to the value that was provided on create. +func (u *UserAccessTokenUpsertBulk) UpdateUserID() *UserAccessTokenUpsertBulk { + return u.Update(func(s *UserAccessTokenUpsert) { + s.UpdateUserID() + }) +} + +// SetName sets the "name" field. +func (u *UserAccessTokenUpsertBulk) SetName(v string) *UserAccessTokenUpsertBulk { + return u.Update(func(s *UserAccessTokenUpsert) { + s.SetName(v) + }) +} + +// UpdateName sets the "name" field to the value that was provided on create. +func (u *UserAccessTokenUpsertBulk) UpdateName() *UserAccessTokenUpsertBulk { + return u.Update(func(s *UserAccessTokenUpsert) { + s.UpdateName() + }) +} + +// SetPrefix sets the "prefix" field. +func (u *UserAccessTokenUpsertBulk) SetPrefix(v string) *UserAccessTokenUpsertBulk { + return u.Update(func(s *UserAccessTokenUpsert) { + s.SetPrefix(v) + }) +} + +// UpdatePrefix sets the "prefix" field to the value that was provided on create. +func (u *UserAccessTokenUpsertBulk) UpdatePrefix() *UserAccessTokenUpsertBulk { + return u.Update(func(s *UserAccessTokenUpsert) { + s.UpdatePrefix() + }) +} + +// SetKeyHash sets the "key_hash" field. +func (u *UserAccessTokenUpsertBulk) SetKeyHash(v string) *UserAccessTokenUpsertBulk { + return u.Update(func(s *UserAccessTokenUpsert) { + s.SetKeyHash(v) + }) +} + +// UpdateKeyHash sets the "key_hash" field to the value that was provided on create. +func (u *UserAccessTokenUpsertBulk) UpdateKeyHash() *UserAccessTokenUpsertBulk { + return u.Update(func(s *UserAccessTokenUpsert) { + s.UpdateKeyHash() + }) +} + +// SetProjectID sets the "project_id" field. +func (u *UserAccessTokenUpsertBulk) SetProjectID(v uuid.UUID) *UserAccessTokenUpsertBulk { + return u.Update(func(s *UserAccessTokenUpsert) { + s.SetProjectID(v) + }) +} + +// UpdateProjectID sets the "project_id" field to the value that was provided on create. +func (u *UserAccessTokenUpsertBulk) UpdateProjectID() *UserAccessTokenUpsertBulk { + return u.Update(func(s *UserAccessTokenUpsert) { + s.UpdateProjectID() + }) +} + +// SetScopes sets the "scopes" field. +func (u *UserAccessTokenUpsertBulk) SetScopes(v string) *UserAccessTokenUpsertBulk { + return u.Update(func(s *UserAccessTokenUpsert) { + s.SetScopes(v) + }) +} + +// UpdateScopes sets the "scopes" field to the value that was provided on create. +func (u *UserAccessTokenUpsertBulk) UpdateScopes() *UserAccessTokenUpsertBulk { + return u.Update(func(s *UserAccessTokenUpsert) { + s.UpdateScopes() + }) +} + +// SetRevoked sets the "revoked" field. +func (u *UserAccessTokenUpsertBulk) SetRevoked(v bool) *UserAccessTokenUpsertBulk { + return u.Update(func(s *UserAccessTokenUpsert) { + s.SetRevoked(v) + }) +} + +// UpdateRevoked sets the "revoked" field to the value that was provided on create. +func (u *UserAccessTokenUpsertBulk) UpdateRevoked() *UserAccessTokenUpsertBulk { + return u.Update(func(s *UserAccessTokenUpsert) { + s.UpdateRevoked() + }) +} + +// SetExpiresAt sets the "expires_at" field. +func (u *UserAccessTokenUpsertBulk) SetExpiresAt(v time.Time) *UserAccessTokenUpsertBulk { + return u.Update(func(s *UserAccessTokenUpsert) { + s.SetExpiresAt(v) + }) +} + +// UpdateExpiresAt sets the "expires_at" field to the value that was provided on create. +func (u *UserAccessTokenUpsertBulk) UpdateExpiresAt() *UserAccessTokenUpsertBulk { + return u.Update(func(s *UserAccessTokenUpsert) { + s.UpdateExpiresAt() + }) +} + +// ClearExpiresAt clears the value of the "expires_at" field. +func (u *UserAccessTokenUpsertBulk) ClearExpiresAt() *UserAccessTokenUpsertBulk { + return u.Update(func(s *UserAccessTokenUpsert) { + s.ClearExpiresAt() + }) +} + +// SetLastUsed sets the "last_used" field. +func (u *UserAccessTokenUpsertBulk) SetLastUsed(v time.Time) *UserAccessTokenUpsertBulk { + return u.Update(func(s *UserAccessTokenUpsert) { + s.SetLastUsed(v) + }) +} + +// UpdateLastUsed sets the "last_used" field to the value that was provided on create. +func (u *UserAccessTokenUpsertBulk) UpdateLastUsed() *UserAccessTokenUpsertBulk { + return u.Update(func(s *UserAccessTokenUpsert) { + s.UpdateLastUsed() + }) +} + +// ClearLastUsed clears the value of the "last_used" field. +func (u *UserAccessTokenUpsertBulk) ClearLastUsed() *UserAccessTokenUpsertBulk { + return u.Update(func(s *UserAccessTokenUpsert) { + s.ClearLastUsed() + }) +} + +// Exec executes the query. +func (u *UserAccessTokenUpsertBulk) Exec(ctx context.Context) error { + if u.create.err != nil { + return u.create.err + } + for i, b := range u.create.builders { + if len(b.conflict) != 0 { + return fmt.Errorf("ent: OnConflict was set for builder %d. Set it on the UserAccessTokenCreateBulk instead", i) + } + } + if len(u.create.conflict) == 0 { + return errors.New("ent: missing options for UserAccessTokenCreateBulk.OnConflict") + } + return u.create.Exec(ctx) +} + +// ExecX is like Exec, but panics if an error occurs. +func (u *UserAccessTokenUpsertBulk) ExecX(ctx context.Context) { + if err := u.create.Exec(ctx); err != nil { + panic(err) + } +} diff --git a/pkg/ent/useraccesstoken_delete.go b/pkg/ent/useraccesstoken_delete.go new file mode 100644 index 000000000..e0f4a4d49 --- /dev/null +++ b/pkg/ent/useraccesstoken_delete.go @@ -0,0 +1,88 @@ +// Code generated by ent, DO NOT EDIT. + +package ent + +import ( + "context" + + "entgo.io/ent/dialect/sql" + "entgo.io/ent/dialect/sql/sqlgraph" + "entgo.io/ent/schema/field" + "github.com/GoogleCloudPlatform/scion/pkg/ent/predicate" + "github.com/GoogleCloudPlatform/scion/pkg/ent/useraccesstoken" +) + +// UserAccessTokenDelete is the builder for deleting a UserAccessToken entity. +type UserAccessTokenDelete struct { + config + hooks []Hook + mutation *UserAccessTokenMutation +} + +// Where appends a list predicates to the UserAccessTokenDelete builder. +func (_d *UserAccessTokenDelete) Where(ps ...predicate.UserAccessToken) *UserAccessTokenDelete { + _d.mutation.Where(ps...) + return _d +} + +// Exec executes the deletion query and returns how many vertices were deleted. +func (_d *UserAccessTokenDelete) Exec(ctx context.Context) (int, error) { + return withHooks(ctx, _d.sqlExec, _d.mutation, _d.hooks) +} + +// ExecX is like Exec, but panics if an error occurs. +func (_d *UserAccessTokenDelete) ExecX(ctx context.Context) int { + n, err := _d.Exec(ctx) + if err != nil { + panic(err) + } + return n +} + +func (_d *UserAccessTokenDelete) sqlExec(ctx context.Context) (int, error) { + _spec := sqlgraph.NewDeleteSpec(useraccesstoken.Table, sqlgraph.NewFieldSpec(useraccesstoken.FieldID, field.TypeUUID)) + if ps := _d.mutation.predicates; len(ps) > 0 { + _spec.Predicate = func(selector *sql.Selector) { + for i := range ps { + ps[i](selector) + } + } + } + affected, err := sqlgraph.DeleteNodes(ctx, _d.driver, _spec) + if err != nil && sqlgraph.IsConstraintError(err) { + err = &ConstraintError{msg: err.Error(), wrap: err} + } + _d.mutation.done = true + return affected, err +} + +// UserAccessTokenDeleteOne is the builder for deleting a single UserAccessToken entity. +type UserAccessTokenDeleteOne struct { + _d *UserAccessTokenDelete +} + +// Where appends a list predicates to the UserAccessTokenDelete builder. +func (_d *UserAccessTokenDeleteOne) Where(ps ...predicate.UserAccessToken) *UserAccessTokenDeleteOne { + _d._d.mutation.Where(ps...) + return _d +} + +// Exec executes the deletion query. +func (_d *UserAccessTokenDeleteOne) Exec(ctx context.Context) error { + n, err := _d._d.Exec(ctx) + switch { + case err != nil: + return err + case n == 0: + return &NotFoundError{useraccesstoken.Label} + default: + return nil + } +} + +// ExecX is like Exec, but panics if an error occurs. +func (_d *UserAccessTokenDeleteOne) ExecX(ctx context.Context) { + if err := _d.Exec(ctx); err != nil { + panic(err) + } +} diff --git a/pkg/ent/useraccesstoken_query.go b/pkg/ent/useraccesstoken_query.go new file mode 100644 index 000000000..d443fe837 --- /dev/null +++ b/pkg/ent/useraccesstoken_query.go @@ -0,0 +1,565 @@ +// Code generated by ent, DO NOT EDIT. + +package ent + +import ( + "context" + "fmt" + "math" + + "entgo.io/ent" + "entgo.io/ent/dialect" + "entgo.io/ent/dialect/sql" + "entgo.io/ent/dialect/sql/sqlgraph" + "entgo.io/ent/schema/field" + "github.com/GoogleCloudPlatform/scion/pkg/ent/predicate" + "github.com/GoogleCloudPlatform/scion/pkg/ent/useraccesstoken" + "github.com/google/uuid" +) + +// UserAccessTokenQuery is the builder for querying UserAccessToken entities. +type UserAccessTokenQuery struct { + config + ctx *QueryContext + order []useraccesstoken.OrderOption + inters []Interceptor + predicates []predicate.UserAccessToken + modifiers []func(*sql.Selector) + // intermediate query (i.e. traversal path). + sql *sql.Selector + path func(context.Context) (*sql.Selector, error) +} + +// Where adds a new predicate for the UserAccessTokenQuery builder. +func (_q *UserAccessTokenQuery) Where(ps ...predicate.UserAccessToken) *UserAccessTokenQuery { + _q.predicates = append(_q.predicates, ps...) + return _q +} + +// Limit the number of records to be returned by this query. +func (_q *UserAccessTokenQuery) Limit(limit int) *UserAccessTokenQuery { + _q.ctx.Limit = &limit + return _q +} + +// Offset to start from. +func (_q *UserAccessTokenQuery) Offset(offset int) *UserAccessTokenQuery { + _q.ctx.Offset = &offset + return _q +} + +// Unique configures the query builder to filter duplicate records on query. +// By default, unique is set to true, and can be disabled using this method. +func (_q *UserAccessTokenQuery) Unique(unique bool) *UserAccessTokenQuery { + _q.ctx.Unique = &unique + return _q +} + +// Order specifies how the records should be ordered. +func (_q *UserAccessTokenQuery) Order(o ...useraccesstoken.OrderOption) *UserAccessTokenQuery { + _q.order = append(_q.order, o...) + return _q +} + +// First returns the first UserAccessToken entity from the query. +// Returns a *NotFoundError when no UserAccessToken was found. +func (_q *UserAccessTokenQuery) First(ctx context.Context) (*UserAccessToken, error) { + nodes, err := _q.Limit(1).All(setContextOp(ctx, _q.ctx, ent.OpQueryFirst)) + if err != nil { + return nil, err + } + if len(nodes) == 0 { + return nil, &NotFoundError{useraccesstoken.Label} + } + return nodes[0], nil +} + +// FirstX is like First, but panics if an error occurs. +func (_q *UserAccessTokenQuery) FirstX(ctx context.Context) *UserAccessToken { + node, err := _q.First(ctx) + if err != nil && !IsNotFound(err) { + panic(err) + } + return node +} + +// FirstID returns the first UserAccessToken ID from the query. +// Returns a *NotFoundError when no UserAccessToken ID was found. +func (_q *UserAccessTokenQuery) FirstID(ctx context.Context) (id uuid.UUID, err error) { + var ids []uuid.UUID + if ids, err = _q.Limit(1).IDs(setContextOp(ctx, _q.ctx, ent.OpQueryFirstID)); err != nil { + return + } + if len(ids) == 0 { + err = &NotFoundError{useraccesstoken.Label} + return + } + return ids[0], nil +} + +// FirstIDX is like FirstID, but panics if an error occurs. +func (_q *UserAccessTokenQuery) FirstIDX(ctx context.Context) uuid.UUID { + id, err := _q.FirstID(ctx) + if err != nil && !IsNotFound(err) { + panic(err) + } + return id +} + +// Only returns a single UserAccessToken entity found by the query, ensuring it only returns one. +// Returns a *NotSingularError when more than one UserAccessToken entity is found. +// Returns a *NotFoundError when no UserAccessToken entities are found. +func (_q *UserAccessTokenQuery) Only(ctx context.Context) (*UserAccessToken, error) { + nodes, err := _q.Limit(2).All(setContextOp(ctx, _q.ctx, ent.OpQueryOnly)) + if err != nil { + return nil, err + } + switch len(nodes) { + case 1: + return nodes[0], nil + case 0: + return nil, &NotFoundError{useraccesstoken.Label} + default: + return nil, &NotSingularError{useraccesstoken.Label} + } +} + +// OnlyX is like Only, but panics if an error occurs. +func (_q *UserAccessTokenQuery) OnlyX(ctx context.Context) *UserAccessToken { + node, err := _q.Only(ctx) + if err != nil { + panic(err) + } + return node +} + +// OnlyID is like Only, but returns the only UserAccessToken ID in the query. +// Returns a *NotSingularError when more than one UserAccessToken ID is found. +// Returns a *NotFoundError when no entities are found. +func (_q *UserAccessTokenQuery) OnlyID(ctx context.Context) (id uuid.UUID, err error) { + var ids []uuid.UUID + if ids, err = _q.Limit(2).IDs(setContextOp(ctx, _q.ctx, ent.OpQueryOnlyID)); err != nil { + return + } + switch len(ids) { + case 1: + id = ids[0] + case 0: + err = &NotFoundError{useraccesstoken.Label} + default: + err = &NotSingularError{useraccesstoken.Label} + } + return +} + +// OnlyIDX is like OnlyID, but panics if an error occurs. +func (_q *UserAccessTokenQuery) OnlyIDX(ctx context.Context) uuid.UUID { + id, err := _q.OnlyID(ctx) + if err != nil { + panic(err) + } + return id +} + +// All executes the query and returns a list of UserAccessTokens. +func (_q *UserAccessTokenQuery) All(ctx context.Context) ([]*UserAccessToken, error) { + ctx = setContextOp(ctx, _q.ctx, ent.OpQueryAll) + if err := _q.prepareQuery(ctx); err != nil { + return nil, err + } + qr := querierAll[[]*UserAccessToken, *UserAccessTokenQuery]() + return withInterceptors[[]*UserAccessToken](ctx, _q, qr, _q.inters) +} + +// AllX is like All, but panics if an error occurs. +func (_q *UserAccessTokenQuery) AllX(ctx context.Context) []*UserAccessToken { + nodes, err := _q.All(ctx) + if err != nil { + panic(err) + } + return nodes +} + +// IDs executes the query and returns a list of UserAccessToken IDs. +func (_q *UserAccessTokenQuery) IDs(ctx context.Context) (ids []uuid.UUID, err error) { + if _q.ctx.Unique == nil && _q.path != nil { + _q.Unique(true) + } + ctx = setContextOp(ctx, _q.ctx, ent.OpQueryIDs) + if err = _q.Select(useraccesstoken.FieldID).Scan(ctx, &ids); err != nil { + return nil, err + } + return ids, nil +} + +// IDsX is like IDs, but panics if an error occurs. +func (_q *UserAccessTokenQuery) IDsX(ctx context.Context) []uuid.UUID { + ids, err := _q.IDs(ctx) + if err != nil { + panic(err) + } + return ids +} + +// Count returns the count of the given query. +func (_q *UserAccessTokenQuery) Count(ctx context.Context) (int, error) { + ctx = setContextOp(ctx, _q.ctx, ent.OpQueryCount) + if err := _q.prepareQuery(ctx); err != nil { + return 0, err + } + return withInterceptors[int](ctx, _q, querierCount[*UserAccessTokenQuery](), _q.inters) +} + +// CountX is like Count, but panics if an error occurs. +func (_q *UserAccessTokenQuery) CountX(ctx context.Context) int { + count, err := _q.Count(ctx) + if err != nil { + panic(err) + } + return count +} + +// Exist returns true if the query has elements in the graph. +func (_q *UserAccessTokenQuery) Exist(ctx context.Context) (bool, error) { + ctx = setContextOp(ctx, _q.ctx, ent.OpQueryExist) + switch _, err := _q.FirstID(ctx); { + case IsNotFound(err): + return false, nil + case err != nil: + return false, fmt.Errorf("ent: check existence: %w", err) + default: + return true, nil + } +} + +// ExistX is like Exist, but panics if an error occurs. +func (_q *UserAccessTokenQuery) ExistX(ctx context.Context) bool { + exist, err := _q.Exist(ctx) + if err != nil { + panic(err) + } + return exist +} + +// Clone returns a duplicate of the UserAccessTokenQuery builder, including all associated steps. It can be +// used to prepare common query builders and use them differently after the clone is made. +func (_q *UserAccessTokenQuery) Clone() *UserAccessTokenQuery { + if _q == nil { + return nil + } + return &UserAccessTokenQuery{ + config: _q.config, + ctx: _q.ctx.Clone(), + order: append([]useraccesstoken.OrderOption{}, _q.order...), + inters: append([]Interceptor{}, _q.inters...), + predicates: append([]predicate.UserAccessToken{}, _q.predicates...), + // clone intermediate query. + sql: _q.sql.Clone(), + path: _q.path, + } +} + +// GroupBy is used to group vertices by one or more fields/columns. +// It is often used with aggregate functions, like: count, max, mean, min, sum. +// +// Example: +// +// var v []struct { +// UserID uuid.UUID `json:"user_id,omitempty"` +// Count int `json:"count,omitempty"` +// } +// +// client.UserAccessToken.Query(). +// GroupBy(useraccesstoken.FieldUserID). +// Aggregate(ent.Count()). +// Scan(ctx, &v) +func (_q *UserAccessTokenQuery) GroupBy(field string, fields ...string) *UserAccessTokenGroupBy { + _q.ctx.Fields = append([]string{field}, fields...) + grbuild := &UserAccessTokenGroupBy{build: _q} + grbuild.flds = &_q.ctx.Fields + grbuild.label = useraccesstoken.Label + grbuild.scan = grbuild.Scan + return grbuild +} + +// Select allows the selection one or more fields/columns for the given query, +// instead of selecting all fields in the entity. +// +// Example: +// +// var v []struct { +// UserID uuid.UUID `json:"user_id,omitempty"` +// } +// +// client.UserAccessToken.Query(). +// Select(useraccesstoken.FieldUserID). +// Scan(ctx, &v) +func (_q *UserAccessTokenQuery) Select(fields ...string) *UserAccessTokenSelect { + _q.ctx.Fields = append(_q.ctx.Fields, fields...) + sbuild := &UserAccessTokenSelect{UserAccessTokenQuery: _q} + sbuild.label = useraccesstoken.Label + sbuild.flds, sbuild.scan = &_q.ctx.Fields, sbuild.Scan + return sbuild +} + +// Aggregate returns a UserAccessTokenSelect configured with the given aggregations. +func (_q *UserAccessTokenQuery) Aggregate(fns ...AggregateFunc) *UserAccessTokenSelect { + return _q.Select().Aggregate(fns...) +} + +func (_q *UserAccessTokenQuery) prepareQuery(ctx context.Context) error { + for _, inter := range _q.inters { + if inter == nil { + return fmt.Errorf("ent: uninitialized interceptor (forgotten import ent/runtime?)") + } + if trv, ok := inter.(Traverser); ok { + if err := trv.Traverse(ctx, _q); err != nil { + return err + } + } + } + for _, f := range _q.ctx.Fields { + if !useraccesstoken.ValidColumn(f) { + return &ValidationError{Name: f, err: fmt.Errorf("ent: invalid field %q for query", f)} + } + } + if _q.path != nil { + prev, err := _q.path(ctx) + if err != nil { + return err + } + _q.sql = prev + } + return nil +} + +func (_q *UserAccessTokenQuery) sqlAll(ctx context.Context, hooks ...queryHook) ([]*UserAccessToken, error) { + var ( + nodes = []*UserAccessToken{} + _spec = _q.querySpec() + ) + _spec.ScanValues = func(columns []string) ([]any, error) { + return (*UserAccessToken).scanValues(nil, columns) + } + _spec.Assign = func(columns []string, values []any) error { + node := &UserAccessToken{config: _q.config} + nodes = append(nodes, node) + return node.assignValues(columns, values) + } + if len(_q.modifiers) > 0 { + _spec.Modifiers = _q.modifiers + } + for i := range hooks { + hooks[i](ctx, _spec) + } + if err := sqlgraph.QueryNodes(ctx, _q.driver, _spec); err != nil { + return nil, err + } + if len(nodes) == 0 { + return nodes, nil + } + return nodes, nil +} + +func (_q *UserAccessTokenQuery) sqlCount(ctx context.Context) (int, error) { + _spec := _q.querySpec() + if len(_q.modifiers) > 0 { + _spec.Modifiers = _q.modifiers + } + _spec.Node.Columns = _q.ctx.Fields + if len(_q.ctx.Fields) > 0 { + _spec.Unique = _q.ctx.Unique != nil && *_q.ctx.Unique + } + return sqlgraph.CountNodes(ctx, _q.driver, _spec) +} + +func (_q *UserAccessTokenQuery) querySpec() *sqlgraph.QuerySpec { + _spec := sqlgraph.NewQuerySpec(useraccesstoken.Table, useraccesstoken.Columns, sqlgraph.NewFieldSpec(useraccesstoken.FieldID, field.TypeUUID)) + _spec.From = _q.sql + if unique := _q.ctx.Unique; unique != nil { + _spec.Unique = *unique + } else if _q.path != nil { + _spec.Unique = true + } + if fields := _q.ctx.Fields; len(fields) > 0 { + _spec.Node.Columns = make([]string, 0, len(fields)) + _spec.Node.Columns = append(_spec.Node.Columns, useraccesstoken.FieldID) + for i := range fields { + if fields[i] != useraccesstoken.FieldID { + _spec.Node.Columns = append(_spec.Node.Columns, fields[i]) + } + } + } + if ps := _q.predicates; len(ps) > 0 { + _spec.Predicate = func(selector *sql.Selector) { + for i := range ps { + ps[i](selector) + } + } + } + if limit := _q.ctx.Limit; limit != nil { + _spec.Limit = *limit + } + if offset := _q.ctx.Offset; offset != nil { + _spec.Offset = *offset + } + if ps := _q.order; len(ps) > 0 { + _spec.Order = func(selector *sql.Selector) { + for i := range ps { + ps[i](selector) + } + } + } + return _spec +} + +func (_q *UserAccessTokenQuery) sqlQuery(ctx context.Context) *sql.Selector { + builder := sql.Dialect(_q.driver.Dialect()) + t1 := builder.Table(useraccesstoken.Table) + columns := _q.ctx.Fields + if len(columns) == 0 { + columns = useraccesstoken.Columns + } + selector := builder.Select(t1.Columns(columns...)...).From(t1) + if _q.sql != nil { + selector = _q.sql + selector.Select(selector.Columns(columns...)...) + } + if _q.ctx.Unique != nil && *_q.ctx.Unique { + selector.Distinct() + } + for _, m := range _q.modifiers { + m(selector) + } + for _, p := range _q.predicates { + p(selector) + } + for _, p := range _q.order { + p(selector) + } + if offset := _q.ctx.Offset; offset != nil { + // limit is mandatory for offset clause. We start + // with default value, and override it below if needed. + selector.Offset(*offset).Limit(math.MaxInt32) + } + if limit := _q.ctx.Limit; limit != nil { + selector.Limit(*limit) + } + return selector +} + +// ForUpdate locks the selected rows against concurrent updates, and prevent them from being +// updated, deleted or "selected ... for update" by other sessions, until the transaction is +// either committed or rolled-back. +func (_q *UserAccessTokenQuery) ForUpdate(opts ...sql.LockOption) *UserAccessTokenQuery { + if _q.driver.Dialect() == dialect.Postgres { + _q.Unique(false) + } + _q.modifiers = append(_q.modifiers, func(s *sql.Selector) { + s.ForUpdate(opts...) + }) + return _q +} + +// ForShare behaves similarly to ForUpdate, except that it acquires a shared mode lock +// on any rows that are read. Other sessions can read the rows, but cannot modify them +// until your transaction commits. +func (_q *UserAccessTokenQuery) ForShare(opts ...sql.LockOption) *UserAccessTokenQuery { + if _q.driver.Dialect() == dialect.Postgres { + _q.Unique(false) + } + _q.modifiers = append(_q.modifiers, func(s *sql.Selector) { + s.ForShare(opts...) + }) + return _q +} + +// UserAccessTokenGroupBy is the group-by builder for UserAccessToken entities. +type UserAccessTokenGroupBy struct { + selector + build *UserAccessTokenQuery +} + +// Aggregate adds the given aggregation functions to the group-by query. +func (_g *UserAccessTokenGroupBy) Aggregate(fns ...AggregateFunc) *UserAccessTokenGroupBy { + _g.fns = append(_g.fns, fns...) + return _g +} + +// Scan applies the selector query and scans the result into the given value. +func (_g *UserAccessTokenGroupBy) Scan(ctx context.Context, v any) error { + ctx = setContextOp(ctx, _g.build.ctx, ent.OpQueryGroupBy) + if err := _g.build.prepareQuery(ctx); err != nil { + return err + } + return scanWithInterceptors[*UserAccessTokenQuery, *UserAccessTokenGroupBy](ctx, _g.build, _g, _g.build.inters, v) +} + +func (_g *UserAccessTokenGroupBy) sqlScan(ctx context.Context, root *UserAccessTokenQuery, v any) error { + selector := root.sqlQuery(ctx).Select() + aggregation := make([]string, 0, len(_g.fns)) + for _, fn := range _g.fns { + aggregation = append(aggregation, fn(selector)) + } + if len(selector.SelectedColumns()) == 0 { + columns := make([]string, 0, len(*_g.flds)+len(_g.fns)) + for _, f := range *_g.flds { + columns = append(columns, selector.C(f)) + } + columns = append(columns, aggregation...) + selector.Select(columns...) + } + selector.GroupBy(selector.Columns(*_g.flds...)...) + if err := selector.Err(); err != nil { + return err + } + rows := &sql.Rows{} + query, args := selector.Query() + if err := _g.build.driver.Query(ctx, query, args, rows); err != nil { + return err + } + defer rows.Close() + return sql.ScanSlice(rows, v) +} + +// UserAccessTokenSelect is the builder for selecting fields of UserAccessToken entities. +type UserAccessTokenSelect struct { + *UserAccessTokenQuery + selector +} + +// Aggregate adds the given aggregation functions to the selector query. +func (_s *UserAccessTokenSelect) Aggregate(fns ...AggregateFunc) *UserAccessTokenSelect { + _s.fns = append(_s.fns, fns...) + return _s +} + +// Scan applies the selector query and scans the result into the given value. +func (_s *UserAccessTokenSelect) Scan(ctx context.Context, v any) error { + ctx = setContextOp(ctx, _s.ctx, ent.OpQuerySelect) + if err := _s.prepareQuery(ctx); err != nil { + return err + } + return scanWithInterceptors[*UserAccessTokenQuery, *UserAccessTokenSelect](ctx, _s.UserAccessTokenQuery, _s, _s.inters, v) +} + +func (_s *UserAccessTokenSelect) sqlScan(ctx context.Context, root *UserAccessTokenQuery, v any) error { + selector := root.sqlQuery(ctx) + aggregation := make([]string, 0, len(_s.fns)) + for _, fn := range _s.fns { + aggregation = append(aggregation, fn(selector)) + } + switch n := len(*_s.selector.flds); { + case n == 0 && len(aggregation) > 0: + selector.Select(aggregation...) + case n != 0 && len(aggregation) > 0: + selector.AppendSelect(aggregation...) + } + rows := &sql.Rows{} + query, args := selector.Query() + if err := _s.driver.Query(ctx, query, args, rows); err != nil { + return err + } + defer rows.Close() + return sql.ScanSlice(rows, v) +} diff --git a/pkg/ent/useraccesstoken_update.go b/pkg/ent/useraccesstoken_update.go new file mode 100644 index 000000000..f949cc67c --- /dev/null +++ b/pkg/ent/useraccesstoken_update.go @@ -0,0 +1,575 @@ +// Code generated by ent, DO NOT EDIT. + +package ent + +import ( + "context" + "errors" + "fmt" + "time" + + "entgo.io/ent/dialect/sql" + "entgo.io/ent/dialect/sql/sqlgraph" + "entgo.io/ent/schema/field" + "github.com/GoogleCloudPlatform/scion/pkg/ent/predicate" + "github.com/GoogleCloudPlatform/scion/pkg/ent/useraccesstoken" + "github.com/google/uuid" +) + +// UserAccessTokenUpdate is the builder for updating UserAccessToken entities. +type UserAccessTokenUpdate struct { + config + hooks []Hook + mutation *UserAccessTokenMutation +} + +// Where appends a list predicates to the UserAccessTokenUpdate builder. +func (_u *UserAccessTokenUpdate) Where(ps ...predicate.UserAccessToken) *UserAccessTokenUpdate { + _u.mutation.Where(ps...) + return _u +} + +// SetUserID sets the "user_id" field. +func (_u *UserAccessTokenUpdate) SetUserID(v uuid.UUID) *UserAccessTokenUpdate { + _u.mutation.SetUserID(v) + return _u +} + +// SetNillableUserID sets the "user_id" field if the given value is not nil. +func (_u *UserAccessTokenUpdate) SetNillableUserID(v *uuid.UUID) *UserAccessTokenUpdate { + if v != nil { + _u.SetUserID(*v) + } + return _u +} + +// SetName sets the "name" field. +func (_u *UserAccessTokenUpdate) SetName(v string) *UserAccessTokenUpdate { + _u.mutation.SetName(v) + return _u +} + +// SetNillableName sets the "name" field if the given value is not nil. +func (_u *UserAccessTokenUpdate) SetNillableName(v *string) *UserAccessTokenUpdate { + if v != nil { + _u.SetName(*v) + } + return _u +} + +// SetPrefix sets the "prefix" field. +func (_u *UserAccessTokenUpdate) SetPrefix(v string) *UserAccessTokenUpdate { + _u.mutation.SetPrefix(v) + return _u +} + +// SetNillablePrefix sets the "prefix" field if the given value is not nil. +func (_u *UserAccessTokenUpdate) SetNillablePrefix(v *string) *UserAccessTokenUpdate { + if v != nil { + _u.SetPrefix(*v) + } + return _u +} + +// SetKeyHash sets the "key_hash" field. +func (_u *UserAccessTokenUpdate) SetKeyHash(v string) *UserAccessTokenUpdate { + _u.mutation.SetKeyHash(v) + return _u +} + +// SetNillableKeyHash sets the "key_hash" field if the given value is not nil. +func (_u *UserAccessTokenUpdate) SetNillableKeyHash(v *string) *UserAccessTokenUpdate { + if v != nil { + _u.SetKeyHash(*v) + } + return _u +} + +// SetProjectID sets the "project_id" field. +func (_u *UserAccessTokenUpdate) SetProjectID(v uuid.UUID) *UserAccessTokenUpdate { + _u.mutation.SetProjectID(v) + return _u +} + +// SetNillableProjectID sets the "project_id" field if the given value is not nil. +func (_u *UserAccessTokenUpdate) SetNillableProjectID(v *uuid.UUID) *UserAccessTokenUpdate { + if v != nil { + _u.SetProjectID(*v) + } + return _u +} + +// SetScopes sets the "scopes" field. +func (_u *UserAccessTokenUpdate) SetScopes(v string) *UserAccessTokenUpdate { + _u.mutation.SetScopes(v) + return _u +} + +// SetNillableScopes sets the "scopes" field if the given value is not nil. +func (_u *UserAccessTokenUpdate) SetNillableScopes(v *string) *UserAccessTokenUpdate { + if v != nil { + _u.SetScopes(*v) + } + return _u +} + +// SetRevoked sets the "revoked" field. +func (_u *UserAccessTokenUpdate) SetRevoked(v bool) *UserAccessTokenUpdate { + _u.mutation.SetRevoked(v) + return _u +} + +// SetNillableRevoked sets the "revoked" field if the given value is not nil. +func (_u *UserAccessTokenUpdate) SetNillableRevoked(v *bool) *UserAccessTokenUpdate { + if v != nil { + _u.SetRevoked(*v) + } + return _u +} + +// SetExpiresAt sets the "expires_at" field. +func (_u *UserAccessTokenUpdate) SetExpiresAt(v time.Time) *UserAccessTokenUpdate { + _u.mutation.SetExpiresAt(v) + return _u +} + +// SetNillableExpiresAt sets the "expires_at" field if the given value is not nil. +func (_u *UserAccessTokenUpdate) SetNillableExpiresAt(v *time.Time) *UserAccessTokenUpdate { + if v != nil { + _u.SetExpiresAt(*v) + } + return _u +} + +// ClearExpiresAt clears the value of the "expires_at" field. +func (_u *UserAccessTokenUpdate) ClearExpiresAt() *UserAccessTokenUpdate { + _u.mutation.ClearExpiresAt() + return _u +} + +// SetLastUsed sets the "last_used" field. +func (_u *UserAccessTokenUpdate) SetLastUsed(v time.Time) *UserAccessTokenUpdate { + _u.mutation.SetLastUsed(v) + return _u +} + +// SetNillableLastUsed sets the "last_used" field if the given value is not nil. +func (_u *UserAccessTokenUpdate) SetNillableLastUsed(v *time.Time) *UserAccessTokenUpdate { + if v != nil { + _u.SetLastUsed(*v) + } + return _u +} + +// ClearLastUsed clears the value of the "last_used" field. +func (_u *UserAccessTokenUpdate) ClearLastUsed() *UserAccessTokenUpdate { + _u.mutation.ClearLastUsed() + return _u +} + +// Mutation returns the UserAccessTokenMutation object of the builder. +func (_u *UserAccessTokenUpdate) Mutation() *UserAccessTokenMutation { + return _u.mutation +} + +// Save executes the query and returns the number of nodes affected by the update operation. +func (_u *UserAccessTokenUpdate) Save(ctx context.Context) (int, error) { + return withHooks(ctx, _u.sqlSave, _u.mutation, _u.hooks) +} + +// SaveX is like Save, but panics if an error occurs. +func (_u *UserAccessTokenUpdate) SaveX(ctx context.Context) int { + affected, err := _u.Save(ctx) + if err != nil { + panic(err) + } + return affected +} + +// Exec executes the query. +func (_u *UserAccessTokenUpdate) Exec(ctx context.Context) error { + _, err := _u.Save(ctx) + return err +} + +// ExecX is like Exec, but panics if an error occurs. +func (_u *UserAccessTokenUpdate) ExecX(ctx context.Context) { + if err := _u.Exec(ctx); err != nil { + panic(err) + } +} + +// check runs all checks and user-defined validators on the builder. +func (_u *UserAccessTokenUpdate) check() error { + if v, ok := _u.mutation.Name(); ok { + if err := useraccesstoken.NameValidator(v); err != nil { + return &ValidationError{Name: "name", err: fmt.Errorf(`ent: validator failed for field "UserAccessToken.name": %w`, err)} + } + } + if v, ok := _u.mutation.Prefix(); ok { + if err := useraccesstoken.PrefixValidator(v); err != nil { + return &ValidationError{Name: "prefix", err: fmt.Errorf(`ent: validator failed for field "UserAccessToken.prefix": %w`, err)} + } + } + if v, ok := _u.mutation.KeyHash(); ok { + if err := useraccesstoken.KeyHashValidator(v); err != nil { + return &ValidationError{Name: "key_hash", err: fmt.Errorf(`ent: validator failed for field "UserAccessToken.key_hash": %w`, err)} + } + } + if v, ok := _u.mutation.Scopes(); ok { + if err := useraccesstoken.ScopesValidator(v); err != nil { + return &ValidationError{Name: "scopes", err: fmt.Errorf(`ent: validator failed for field "UserAccessToken.scopes": %w`, err)} + } + } + return nil +} + +func (_u *UserAccessTokenUpdate) sqlSave(ctx context.Context) (_node int, err error) { + if err := _u.check(); err != nil { + return _node, err + } + _spec := sqlgraph.NewUpdateSpec(useraccesstoken.Table, useraccesstoken.Columns, sqlgraph.NewFieldSpec(useraccesstoken.FieldID, field.TypeUUID)) + if ps := _u.mutation.predicates; len(ps) > 0 { + _spec.Predicate = func(selector *sql.Selector) { + for i := range ps { + ps[i](selector) + } + } + } + if value, ok := _u.mutation.UserID(); ok { + _spec.SetField(useraccesstoken.FieldUserID, field.TypeUUID, value) + } + if value, ok := _u.mutation.Name(); ok { + _spec.SetField(useraccesstoken.FieldName, field.TypeString, value) + } + if value, ok := _u.mutation.Prefix(); ok { + _spec.SetField(useraccesstoken.FieldPrefix, field.TypeString, value) + } + if value, ok := _u.mutation.KeyHash(); ok { + _spec.SetField(useraccesstoken.FieldKeyHash, field.TypeString, value) + } + if value, ok := _u.mutation.ProjectID(); ok { + _spec.SetField(useraccesstoken.FieldProjectID, field.TypeUUID, value) + } + if value, ok := _u.mutation.Scopes(); ok { + _spec.SetField(useraccesstoken.FieldScopes, field.TypeString, value) + } + if value, ok := _u.mutation.Revoked(); ok { + _spec.SetField(useraccesstoken.FieldRevoked, field.TypeBool, value) + } + if value, ok := _u.mutation.ExpiresAt(); ok { + _spec.SetField(useraccesstoken.FieldExpiresAt, field.TypeTime, value) + } + if _u.mutation.ExpiresAtCleared() { + _spec.ClearField(useraccesstoken.FieldExpiresAt, field.TypeTime) + } + if value, ok := _u.mutation.LastUsed(); ok { + _spec.SetField(useraccesstoken.FieldLastUsed, field.TypeTime, value) + } + if _u.mutation.LastUsedCleared() { + _spec.ClearField(useraccesstoken.FieldLastUsed, field.TypeTime) + } + if _node, err = sqlgraph.UpdateNodes(ctx, _u.driver, _spec); err != nil { + if _, ok := err.(*sqlgraph.NotFoundError); ok { + err = &NotFoundError{useraccesstoken.Label} + } else if sqlgraph.IsConstraintError(err) { + err = &ConstraintError{msg: err.Error(), wrap: err} + } + return 0, err + } + _u.mutation.done = true + return _node, nil +} + +// UserAccessTokenUpdateOne is the builder for updating a single UserAccessToken entity. +type UserAccessTokenUpdateOne struct { + config + fields []string + hooks []Hook + mutation *UserAccessTokenMutation +} + +// SetUserID sets the "user_id" field. +func (_u *UserAccessTokenUpdateOne) SetUserID(v uuid.UUID) *UserAccessTokenUpdateOne { + _u.mutation.SetUserID(v) + return _u +} + +// SetNillableUserID sets the "user_id" field if the given value is not nil. +func (_u *UserAccessTokenUpdateOne) SetNillableUserID(v *uuid.UUID) *UserAccessTokenUpdateOne { + if v != nil { + _u.SetUserID(*v) + } + return _u +} + +// SetName sets the "name" field. +func (_u *UserAccessTokenUpdateOne) SetName(v string) *UserAccessTokenUpdateOne { + _u.mutation.SetName(v) + return _u +} + +// SetNillableName sets the "name" field if the given value is not nil. +func (_u *UserAccessTokenUpdateOne) SetNillableName(v *string) *UserAccessTokenUpdateOne { + if v != nil { + _u.SetName(*v) + } + return _u +} + +// SetPrefix sets the "prefix" field. +func (_u *UserAccessTokenUpdateOne) SetPrefix(v string) *UserAccessTokenUpdateOne { + _u.mutation.SetPrefix(v) + return _u +} + +// SetNillablePrefix sets the "prefix" field if the given value is not nil. +func (_u *UserAccessTokenUpdateOne) SetNillablePrefix(v *string) *UserAccessTokenUpdateOne { + if v != nil { + _u.SetPrefix(*v) + } + return _u +} + +// SetKeyHash sets the "key_hash" field. +func (_u *UserAccessTokenUpdateOne) SetKeyHash(v string) *UserAccessTokenUpdateOne { + _u.mutation.SetKeyHash(v) + return _u +} + +// SetNillableKeyHash sets the "key_hash" field if the given value is not nil. +func (_u *UserAccessTokenUpdateOne) SetNillableKeyHash(v *string) *UserAccessTokenUpdateOne { + if v != nil { + _u.SetKeyHash(*v) + } + return _u +} + +// SetProjectID sets the "project_id" field. +func (_u *UserAccessTokenUpdateOne) SetProjectID(v uuid.UUID) *UserAccessTokenUpdateOne { + _u.mutation.SetProjectID(v) + return _u +} + +// SetNillableProjectID sets the "project_id" field if the given value is not nil. +func (_u *UserAccessTokenUpdateOne) SetNillableProjectID(v *uuid.UUID) *UserAccessTokenUpdateOne { + if v != nil { + _u.SetProjectID(*v) + } + return _u +} + +// SetScopes sets the "scopes" field. +func (_u *UserAccessTokenUpdateOne) SetScopes(v string) *UserAccessTokenUpdateOne { + _u.mutation.SetScopes(v) + return _u +} + +// SetNillableScopes sets the "scopes" field if the given value is not nil. +func (_u *UserAccessTokenUpdateOne) SetNillableScopes(v *string) *UserAccessTokenUpdateOne { + if v != nil { + _u.SetScopes(*v) + } + return _u +} + +// SetRevoked sets the "revoked" field. +func (_u *UserAccessTokenUpdateOne) SetRevoked(v bool) *UserAccessTokenUpdateOne { + _u.mutation.SetRevoked(v) + return _u +} + +// SetNillableRevoked sets the "revoked" field if the given value is not nil. +func (_u *UserAccessTokenUpdateOne) SetNillableRevoked(v *bool) *UserAccessTokenUpdateOne { + if v != nil { + _u.SetRevoked(*v) + } + return _u +} + +// SetExpiresAt sets the "expires_at" field. +func (_u *UserAccessTokenUpdateOne) SetExpiresAt(v time.Time) *UserAccessTokenUpdateOne { + _u.mutation.SetExpiresAt(v) + return _u +} + +// SetNillableExpiresAt sets the "expires_at" field if the given value is not nil. +func (_u *UserAccessTokenUpdateOne) SetNillableExpiresAt(v *time.Time) *UserAccessTokenUpdateOne { + if v != nil { + _u.SetExpiresAt(*v) + } + return _u +} + +// ClearExpiresAt clears the value of the "expires_at" field. +func (_u *UserAccessTokenUpdateOne) ClearExpiresAt() *UserAccessTokenUpdateOne { + _u.mutation.ClearExpiresAt() + return _u +} + +// SetLastUsed sets the "last_used" field. +func (_u *UserAccessTokenUpdateOne) SetLastUsed(v time.Time) *UserAccessTokenUpdateOne { + _u.mutation.SetLastUsed(v) + return _u +} + +// SetNillableLastUsed sets the "last_used" field if the given value is not nil. +func (_u *UserAccessTokenUpdateOne) SetNillableLastUsed(v *time.Time) *UserAccessTokenUpdateOne { + if v != nil { + _u.SetLastUsed(*v) + } + return _u +} + +// ClearLastUsed clears the value of the "last_used" field. +func (_u *UserAccessTokenUpdateOne) ClearLastUsed() *UserAccessTokenUpdateOne { + _u.mutation.ClearLastUsed() + return _u +} + +// Mutation returns the UserAccessTokenMutation object of the builder. +func (_u *UserAccessTokenUpdateOne) Mutation() *UserAccessTokenMutation { + return _u.mutation +} + +// Where appends a list predicates to the UserAccessTokenUpdate builder. +func (_u *UserAccessTokenUpdateOne) Where(ps ...predicate.UserAccessToken) *UserAccessTokenUpdateOne { + _u.mutation.Where(ps...) + return _u +} + +// Select allows selecting one or more fields (columns) of the returned entity. +// The default is selecting all fields defined in the entity schema. +func (_u *UserAccessTokenUpdateOne) Select(field string, fields ...string) *UserAccessTokenUpdateOne { + _u.fields = append([]string{field}, fields...) + return _u +} + +// Save executes the query and returns the updated UserAccessToken entity. +func (_u *UserAccessTokenUpdateOne) Save(ctx context.Context) (*UserAccessToken, error) { + return withHooks(ctx, _u.sqlSave, _u.mutation, _u.hooks) +} + +// SaveX is like Save, but panics if an error occurs. +func (_u *UserAccessTokenUpdateOne) SaveX(ctx context.Context) *UserAccessToken { + node, err := _u.Save(ctx) + if err != nil { + panic(err) + } + return node +} + +// Exec executes the query on the entity. +func (_u *UserAccessTokenUpdateOne) Exec(ctx context.Context) error { + _, err := _u.Save(ctx) + return err +} + +// ExecX is like Exec, but panics if an error occurs. +func (_u *UserAccessTokenUpdateOne) ExecX(ctx context.Context) { + if err := _u.Exec(ctx); err != nil { + panic(err) + } +} + +// check runs all checks and user-defined validators on the builder. +func (_u *UserAccessTokenUpdateOne) check() error { + if v, ok := _u.mutation.Name(); ok { + if err := useraccesstoken.NameValidator(v); err != nil { + return &ValidationError{Name: "name", err: fmt.Errorf(`ent: validator failed for field "UserAccessToken.name": %w`, err)} + } + } + if v, ok := _u.mutation.Prefix(); ok { + if err := useraccesstoken.PrefixValidator(v); err != nil { + return &ValidationError{Name: "prefix", err: fmt.Errorf(`ent: validator failed for field "UserAccessToken.prefix": %w`, err)} + } + } + if v, ok := _u.mutation.KeyHash(); ok { + if err := useraccesstoken.KeyHashValidator(v); err != nil { + return &ValidationError{Name: "key_hash", err: fmt.Errorf(`ent: validator failed for field "UserAccessToken.key_hash": %w`, err)} + } + } + if v, ok := _u.mutation.Scopes(); ok { + if err := useraccesstoken.ScopesValidator(v); err != nil { + return &ValidationError{Name: "scopes", err: fmt.Errorf(`ent: validator failed for field "UserAccessToken.scopes": %w`, err)} + } + } + return nil +} + +func (_u *UserAccessTokenUpdateOne) sqlSave(ctx context.Context) (_node *UserAccessToken, err error) { + if err := _u.check(); err != nil { + return _node, err + } + _spec := sqlgraph.NewUpdateSpec(useraccesstoken.Table, useraccesstoken.Columns, sqlgraph.NewFieldSpec(useraccesstoken.FieldID, field.TypeUUID)) + id, ok := _u.mutation.ID() + if !ok { + return nil, &ValidationError{Name: "id", err: errors.New(`ent: missing "UserAccessToken.id" for update`)} + } + _spec.Node.ID.Value = id + if fields := _u.fields; len(fields) > 0 { + _spec.Node.Columns = make([]string, 0, len(fields)) + _spec.Node.Columns = append(_spec.Node.Columns, useraccesstoken.FieldID) + for _, f := range fields { + if !useraccesstoken.ValidColumn(f) { + return nil, &ValidationError{Name: f, err: fmt.Errorf("ent: invalid field %q for query", f)} + } + if f != useraccesstoken.FieldID { + _spec.Node.Columns = append(_spec.Node.Columns, f) + } + } + } + if ps := _u.mutation.predicates; len(ps) > 0 { + _spec.Predicate = func(selector *sql.Selector) { + for i := range ps { + ps[i](selector) + } + } + } + if value, ok := _u.mutation.UserID(); ok { + _spec.SetField(useraccesstoken.FieldUserID, field.TypeUUID, value) + } + if value, ok := _u.mutation.Name(); ok { + _spec.SetField(useraccesstoken.FieldName, field.TypeString, value) + } + if value, ok := _u.mutation.Prefix(); ok { + _spec.SetField(useraccesstoken.FieldPrefix, field.TypeString, value) + } + if value, ok := _u.mutation.KeyHash(); ok { + _spec.SetField(useraccesstoken.FieldKeyHash, field.TypeString, value) + } + if value, ok := _u.mutation.ProjectID(); ok { + _spec.SetField(useraccesstoken.FieldProjectID, field.TypeUUID, value) + } + if value, ok := _u.mutation.Scopes(); ok { + _spec.SetField(useraccesstoken.FieldScopes, field.TypeString, value) + } + if value, ok := _u.mutation.Revoked(); ok { + _spec.SetField(useraccesstoken.FieldRevoked, field.TypeBool, value) + } + if value, ok := _u.mutation.ExpiresAt(); ok { + _spec.SetField(useraccesstoken.FieldExpiresAt, field.TypeTime, value) + } + if _u.mutation.ExpiresAtCleared() { + _spec.ClearField(useraccesstoken.FieldExpiresAt, field.TypeTime) + } + if value, ok := _u.mutation.LastUsed(); ok { + _spec.SetField(useraccesstoken.FieldLastUsed, field.TypeTime, value) + } + if _u.mutation.LastUsedCleared() { + _spec.ClearField(useraccesstoken.FieldLastUsed, field.TypeTime) + } + _node = &UserAccessToken{config: _u.config} + _spec.Assign = _node.assignValues + _spec.ScanValues = _node.scanValues + if err = sqlgraph.UpdateNode(ctx, _u.driver, _spec); err != nil { + if _, ok := err.(*sqlgraph.NotFoundError); ok { + err = &NotFoundError{useraccesstoken.Label} + } else if sqlgraph.IsConstraintError(err) { + err = &ConstraintError{msg: err.Error(), wrap: err} + } + return nil, err + } + _u.mutation.done = true + return _node, nil +} diff --git a/pkg/eventbus/fanout.go b/pkg/eventbus/fanout.go index b4020228f..c0654bf40 100644 --- a/pkg/eventbus/fanout.go +++ b/pkg/eventbus/fanout.go @@ -33,6 +33,12 @@ type NamedEventBus struct { Name string Bus EventBus Observer bool + // ChannelID overrides Name for channel-based message routing. When a + // message has msg.Channel set, the FanOutEventBus matches it against + // ChannelID (if non-empty) or Name. This allows a plugin registered + // under one name (e.g. "chat-app") to handle a different channel + // identifier (e.g. "gchat"). + ChannelID string } // FanOutEventBus implements EventBus by delegating to N child event buses. @@ -58,10 +64,15 @@ func (f *FanOutEventBus) Publish(ctx context.Context, topic string, msg *message var inproc, target *NamedEventBus for i := range f.buses { - switch f.buses[i].Name { - case InProcessBusName: + if f.buses[i].Name == InProcessBusName { inproc = &f.buses[i] - case msg.Channel: + continue + } + channelKey := f.buses[i].ChannelID + if channelKey == "" { + channelKey = f.buses[i].Name + } + if msg != nil && channelKey == msg.Channel { target = &f.buses[i] } } @@ -147,8 +158,9 @@ func (f *FanOutEventBus) Close() error { // BusChannel describes a registered event bus channel. type BusChannel struct { - Name string - Observer bool + Name string + Observer bool + ChannelID string } // BusChannels returns the list of registered bus names (excluding InProcessBus). @@ -159,8 +171,9 @@ func (f *FanOutEventBus) BusChannels() []BusChannel { continue } channels = append(channels, BusChannel{ - Name: nb.Name, - Observer: nb.Observer, + Name: nb.Name, + Observer: nb.Observer, + ChannelID: nb.ChannelID, }) } return channels diff --git a/pkg/eventbus/fanout_test.go b/pkg/eventbus/fanout_test.go index 851551ecb..249a1773e 100644 --- a/pkg/eventbus/fanout_test.go +++ b/pkg/eventbus/fanout_test.go @@ -389,6 +389,35 @@ func TestFanOutEventBus_ChannelRoutingObserverError(t *testing.T) { } } +func TestFanOutEventBus_ChannelRoutingWithChannelID(t *testing.T) { + inproc := newStubEventBus() + chatApp := newStubEventBus() + + fan := NewFanOutEventBus([]NamedEventBus{ + {Name: InProcessBusName, Bus: inproc}, + {Name: "chat-app", Bus: chatApp, ChannelID: "gchat"}, + }, slog.Default()) + + msg := messages.NewInstruction("agent:bot", "user:alice", "hello") + msg.Channel = "gchat" + + if err := fan.Publish(context.Background(), "test.topic", msg); err != nil { + t.Fatalf("unexpected error: %v", err) + } + + inproc.mu.Lock() + if len(inproc.published) != 1 { + t.Errorf("inprocess bus: expected 1 message, got %d", len(inproc.published)) + } + inproc.mu.Unlock() + + chatApp.mu.Lock() + if len(chatApp.published) != 1 { + t.Errorf("chat-app bus: expected 1 message (routed via ChannelID), got %d", len(chatApp.published)) + } + chatApp.mu.Unlock() +} + func TestFanOutEventBus_Subscribe(t *testing.T) { b1 := newStubEventBus() b2 := newStubEventBus() diff --git a/pkg/harness/auth_config_test.go b/pkg/harness/auth_config_test.go index c802a4710..03275c118 100644 --- a/pkg/harness/auth_config_test.go +++ b/pkg/harness/auth_config_test.go @@ -91,8 +91,6 @@ func TestRequiredAuthEnvKeysFromConfig_ParityWithCompiled(t *testing.T) { }{ {"claude", []string{"", "api-key", "oauth-token", "auth-file", "vertex-ai", "unknown"}}, {"gemini", []string{"", "api-key", "auth-file", "vertex-ai", "unknown"}}, - {"opencode", []string{"", "api-key", "auth-file", "unknown"}}, - {"codex", []string{"", "api-key", "auth-file", "unknown"}}, } for _, tc := range cases { authMeta := loadAuthMetaFromHarness(t, tc.harness) @@ -207,9 +205,6 @@ func TestDetectAuthTypeFromFileSecretsFromConfig(t *testing.T) { {"claude", "auth-file wins over vertex-ai", []string{"CLAUDE_AUTH", "gcloud-adc"}, "auth-file"}, {"gemini", "OAUTH wins", []string{"GEMINI_OAUTH_CREDS", "gcloud-adc"}, "auth-file"}, {"gemini", "gcloud-adc only", []string{"gcloud-adc"}, "vertex-ai"}, - {"codex", "CODEX_AUTH", []string{"CODEX_AUTH"}, "auth-file"}, - {"opencode", "OPENCODE_AUTH", []string{"OPENCODE_AUTH"}, "auth-file"}, - {"opencode", "no files", nil, ""}, } for _, tc := range cases { t.Run(tc.harness+"/"+tc.name, func(t *testing.T) { @@ -232,9 +227,6 @@ func TestDetectAuthTypeFromGCPIdentityFromConfig(t *testing.T) { {"claude", false, ""}, {"gemini", true, "vertex-ai"}, {"gemini", false, ""}, - {"codex", true, ""}, // no vertex-ai type declared - {"opencode", true, ""}, // no vertex-ai type declared - {"opencode", false, ""}, // no vertex-ai type declared } for _, tc := range cases { t.Run(tc.harness, func(t *testing.T) { diff --git a/pkg/harness/bundle_install_test.go b/pkg/harness/bundle_install_test.go new file mode 100644 index 000000000..b8e9f04f7 --- /dev/null +++ b/pkg/harness/bundle_install_test.go @@ -0,0 +1,296 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package harness + +import ( + "context" + "os" + "path/filepath" + "strings" + "testing" + + "github.com/GoogleCloudPlatform/scion/pkg/config" + "github.com/GoogleCloudPlatform/scion/pkg/util" +) + +// repoRoot returns the repository root directory. It walks up from the current +// working directory looking for go.mod. +func repoRoot(t *testing.T) string { + t.Helper() + root, err := util.RepoRoot() + if err != nil { + t.Fatalf("failed to find repo root: %v", err) + } + return root +} + +// bundlePath returns the absolute path to a harness bundle under harnesses/. +func bundlePath(t *testing.T, name string) string { + t.Helper() + return filepath.Join(repoRoot(t), "harnesses", name) +} + +// TestBundleInstall_OpenCode validates that the harnesses/opencode/ bundle +// can be installed via the opt-in path and produces the same seeded layout +// as the current Go embeds. This is the Phase A.4 safety net that replaces +// the parity oracle before Decision 3 removes the Go implementations. +func TestBundleInstall_OpenCode(t *testing.T) { + src := bundlePath(t, "opencode") + + // 1. LoadHarnessConfigDir must succeed — config.yaml parses and validates. + hc, err := config.LoadHarnessConfigDir(src) + if err != nil { + t.Fatalf("LoadHarnessConfigDir(%s): %v", src, err) + } + if hc.Config.Harness != "opencode" { + t.Errorf("harness=%q want opencode", hc.Config.Harness) + } + if hc.Config.Provisioner == nil || hc.Config.Provisioner.Type != "container-script" { + t.Fatalf("expected provisioner.type=container-script, got %+v", hc.Config.Provisioner) + } + + // 2. Simulate install: CopyDir from bundle source to a temp target + // (mirrors cmd/harness_config_install.go installLocally). + installDir := filepath.Join(t.TempDir(), "opencode-test") + if err := util.CopyDir(src, installDir); err != nil { + t.Fatalf("CopyDir (install): %v", err) + } + + // Installed dir must also load and validate. + installedHC, err := config.LoadHarnessConfigDir(installDir) + if err != nil { + t.Fatalf("LoadHarnessConfigDir (installed): %v", err) + } + if installedHC.Config.Harness != "opencode" { + t.Errorf("installed harness=%q want opencode", installedHC.Config.Harness) + } + + // 3. Assert the home/ file layout — these are the golden paths that must + // match the implicit mapEmbedFileToHomePath placement. + wantHomeFiles := []string{ + "home/.config/opencode/opencode.json", + } + for _, rel := range wantHomeFiles { + full := filepath.Join(installDir, filepath.FromSlash(rel)) + if _, err := os.Stat(full); err != nil { + t.Errorf("expected %s in installed bundle: %v", rel, err) + } + } + + // 4. Assert bundle root files. + for _, name := range []string{"config.yaml", "provision.py"} { + if _, err := os.Stat(filepath.Join(installDir, name)); err != nil { + t.Errorf("expected %s at bundle root: %v", name, err) + } + } + + // 5. Provision from the installed bundle — verify staging produces the + // expected bundle structure in agent home. + scripted, err := NewContainerScriptHarness(installDir, installedHC.Config) + if err != nil { + t.Fatalf("NewContainerScriptHarness: %v", err) + } + + agentHome := t.TempDir() + if err := scripted.Provision(context.Background(), "test-agent", agentHome, agentHome, "/workspace"); err != nil { + t.Fatalf("Provision: %v", err) + } + + bundle := filepath.Join(agentHome, ".scion", "harness") + for _, name := range []string{"provision.py", "config.yaml", "manifest.json", "scion_harness.py"} { + if _, err := os.Stat(filepath.Join(bundle, name)); err != nil { + t.Errorf("expected %s in staged bundle: %v", name, err) + } + } + hookWrapper := filepath.Join(agentHome, ".scion", "hooks", "pre-start.d", "20-harness-provision") + wrapperBytes, err := os.ReadFile(hookWrapper) + if err != nil { + t.Fatalf("hook wrapper missing after provision: %v", err) + } + if !strings.Contains(string(wrapperBytes), "sciontool harness provision") { + t.Errorf("hook wrapper does not invoke sciontool harness provision") + } +} + +// TestBundleInstall_Codex validates the harnesses/codex/ bundle install path +// and seeded layout parity with the Go embeds. +func TestBundleInstall_Codex(t *testing.T) { + src := bundlePath(t, "codex") + + // 1. LoadHarnessConfigDir must succeed. + hc, err := config.LoadHarnessConfigDir(src) + if err != nil { + t.Fatalf("LoadHarnessConfigDir(%s): %v", src, err) + } + if hc.Config.Harness != "codex" { + t.Errorf("harness=%q want codex", hc.Config.Harness) + } + if hc.Config.Provisioner == nil || hc.Config.Provisioner.Type != "container-script" { + t.Fatalf("expected provisioner.type=container-script, got %+v", hc.Config.Provisioner) + } + + // 2. Simulate install. + installDir := filepath.Join(t.TempDir(), "codex-test") + if err := util.CopyDir(src, installDir); err != nil { + t.Fatalf("CopyDir (install): %v", err) + } + + installedHC, err := config.LoadHarnessConfigDir(installDir) + if err != nil { + t.Fatalf("LoadHarnessConfigDir (installed): %v", err) + } + + // 3. Assert the home/ file layout (golden paths). + wantHomeFiles := []string{ + "home/.bashrc", + "home/.codex/config.toml", + "home/.codex/scion_notify.sh", + } + for _, rel := range wantHomeFiles { + full := filepath.Join(installDir, filepath.FromSlash(rel)) + if _, err := os.Stat(full); err != nil { + t.Errorf("expected %s in installed bundle: %v", rel, err) + } + } + + // 4. Assert bundle root files. + for _, name := range []string{"config.yaml", "provision.py"} { + if _, err := os.Stat(filepath.Join(installDir, name)); err != nil { + t.Errorf("expected %s at bundle root: %v", name, err) + } + } + + // 5. Provision from the installed bundle. + scripted, err := NewContainerScriptHarness(installDir, installedHC.Config) + if err != nil { + t.Fatalf("NewContainerScriptHarness: %v", err) + } + + agentHome := t.TempDir() + if err := scripted.Provision(context.Background(), "test-agent", agentHome, agentHome, "/workspace"); err != nil { + t.Fatalf("Provision: %v", err) + } + + bundle := filepath.Join(agentHome, ".scion", "harness") + for _, name := range []string{"provision.py", "config.yaml", "manifest.json", "scion_harness.py"} { + if _, err := os.Stat(filepath.Join(bundle, name)); err != nil { + t.Errorf("expected %s in staged bundle: %v", name, err) + } + } + hookWrapper := filepath.Join(agentHome, ".scion", "hooks", "pre-start.d", "20-harness-provision") + if _, err := os.Stat(hookWrapper); err != nil { + t.Fatalf("hook wrapper missing after provision: %v", err) + } +} + +// TestBundleInstall_Antigravity validates the harnesses/antigravity/ bundle +// install path, config.yaml schema acceptance (including mcp, oauth-token, +// vertex-ai auth types, and dialect.yaml), and provisioning staging. +func TestBundleInstall_Antigravity(t *testing.T) { + src := bundlePath(t, "antigravity") + + // 1. LoadHarnessConfigDir must succeed — config.yaml parses and validates. + hc, err := config.LoadHarnessConfigDir(src) + if err != nil { + t.Fatalf("LoadHarnessConfigDir(%s): %v", src, err) + } + if hc.Config.Harness != "antigravity" { + t.Errorf("harness=%q want antigravity", hc.Config.Harness) + } + if hc.Config.Provisioner == nil || hc.Config.Provisioner.Type != "container-script" { + t.Fatalf("expected provisioner.type=container-script, got %+v", hc.Config.Provisioner) + } + + // Verify schema-critical fields parsed correctly. + if hc.Config.MCP == nil { + t.Error("expected mcp block to be parsed, got nil") + } else { + if hc.Config.MCP.GlobalConfigFile != ".gemini/config/mcp_config.json" { + t.Errorf("mcp.global_config_file=%q want .gemini/config/mcp_config.json", hc.Config.MCP.GlobalConfigFile) + } + } + if hc.Config.Auth == nil { + t.Error("expected auth block to be parsed, got nil") + } else { + if _, ok := hc.Config.Auth.Types["oauth-token"]; !ok { + t.Error("expected auth.types to contain oauth-token") + } + if _, ok := hc.Config.Auth.Types["vertex-ai"]; !ok { + t.Error("expected auth.types to contain vertex-ai") + } + } + + // 2. Simulate install: CopyDir from bundle source to a temp target. + installDir := filepath.Join(t.TempDir(), "antigravity-test") + if err := util.CopyDir(src, installDir); err != nil { + t.Fatalf("CopyDir (install): %v", err) + } + + installedHC, err := config.LoadHarnessConfigDir(installDir) + if err != nil { + t.Fatalf("LoadHarnessConfigDir (installed): %v", err) + } + if installedHC.Config.Harness != "antigravity" { + t.Errorf("installed harness=%q want antigravity", installedHC.Config.Harness) + } + + // 3. Assert bundle root files. + for _, name := range []string{"config.yaml", "provision.py", "dialect.yaml"} { + if _, err := os.Stat(filepath.Join(installDir, name)); err != nil { + t.Errorf("expected %s at bundle root: %v", name, err) + } + } + + // 4. Assert skills directory exists. + if _, err := os.Stat(filepath.Join(installDir, "skills", ".gitkeep")); err != nil { + t.Errorf("expected skills/.gitkeep in installed bundle: %v", err) + } + + // 5. Provision from the installed bundle — verify staging. + scripted, err := NewContainerScriptHarness(installDir, installedHC.Config) + if err != nil { + t.Fatalf("NewContainerScriptHarness: %v", err) + } + + agentHome := t.TempDir() + if err := scripted.Provision(context.Background(), "test-agent", agentHome, agentHome, "/workspace"); err != nil { + t.Fatalf("Provision: %v", err) + } + + bundle := filepath.Join(agentHome, ".scion", "harness") + for _, name := range []string{"provision.py", "config.yaml", "manifest.json", "scion_harness.py"} { + if _, err := os.Stat(filepath.Join(bundle, name)); err != nil { + t.Errorf("expected %s in staged bundle: %v", name, err) + } + } + + // Verify dialect.yaml is staged into the bundle (B.4). + dialectPath := filepath.Join(bundle, "dialect.yaml") + if _, err := os.Stat(dialectPath); err != nil { + t.Fatalf("expected dialect.yaml in staged bundle: %v", err) + } + dialectContent, err := os.ReadFile(dialectPath) + if err != nil { + t.Fatalf("read staged dialect.yaml: %v", err) + } + if !strings.Contains(string(dialectContent), "dialect: antigravity") { + t.Errorf("staged dialect.yaml does not contain expected 'dialect: antigravity' header") + } + + hookWrapper := filepath.Join(agentHome, ".scion", "hooks", "pre-start.d", "20-harness-provision") + if _, err := os.Stat(hookWrapper); err != nil { + t.Fatalf("hook wrapper missing after provision: %v", err) + } +} diff --git a/pkg/harness/capabilities_test.go b/pkg/harness/capabilities_test.go index d220b800a..ddd6a44b2 100644 --- a/pkg/harness/capabilities_test.go +++ b/pkg/harness/capabilities_test.go @@ -54,28 +54,6 @@ func TestAdvancedCapabilitiesDefaults(t *testing.T) { expectSystemPrompt: api.SupportYes, expectResume: api.SupportYes, }, - { - name: "opencode", - harness: "opencode", - expectMaxTurns: api.SupportNo, - expectMaxModelCalls: api.SupportNo, - expectMaxDuration: api.SupportYes, - expectAuthFile: api.SupportYes, - expectVertexAI: api.SupportNo, - expectSystemPrompt: api.SupportPartial, - expectResume: api.SupportYes, - }, - { - name: "codex", - harness: "codex", - expectMaxTurns: api.SupportNo, - expectMaxModelCalls: api.SupportNo, - expectMaxDuration: api.SupportYes, - expectAuthFile: api.SupportYes, - expectVertexAI: api.SupportNo, - expectSystemPrompt: api.SupportNo, - expectResume: api.SupportYes, - }, { name: "generic", harness: "missing-harness-name", diff --git a/pkg/harness/codex.go b/pkg/harness/codex.go deleted file mode 100644 index 594bb307d..000000000 --- a/pkg/harness/codex.go +++ /dev/null @@ -1,244 +0,0 @@ -// Copyright 2026 Google LLC -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package harness - -import ( - "context" - "embed" - "encoding/json" - "fmt" - "os" - "path/filepath" - - "github.com/GoogleCloudPlatform/scion/pkg/api" - codexEmbeds "github.com/GoogleCloudPlatform/scion/pkg/harness/codex" -) - -type Codex struct{} - -func (c *Codex) Name() string { - return "codex" -} - -func (c *Codex) AdvancedCapabilities() api.HarnessAdvancedCapabilities { - return api.HarnessAdvancedCapabilities{ - Harness: "codex", - Limits: api.HarnessLimitCapabilities{ - MaxTurns: api.CapabilityField{Support: api.SupportNo, Reason: "This harness has no hook dialect for turn events"}, - MaxModelCalls: api.CapabilityField{Support: api.SupportNo, Reason: "This harness has no hook dialect for model events"}, - MaxDuration: api.CapabilityField{Support: api.SupportYes}, - }, - Telemetry: api.HarnessTelemetryCapabilities{ - EnabledConfig: api.CapabilityField{Support: api.SupportYes}, - NativeEmitter: api.CapabilityField{Support: api.SupportYes}, - }, - Prompts: api.HarnessPromptCapabilities{ - SystemPrompt: api.CapabilityField{Support: api.SupportNo, Reason: "System prompt injection is not implemented for this harness"}, - AgentInstructions: api.CapabilityField{Support: api.SupportYes}, - }, - Auth: api.HarnessAuthCapabilities{ - APIKey: api.CapabilityField{Support: api.SupportYes}, - AuthFile: api.CapabilityField{Support: api.SupportYes}, - VertexAI: api.CapabilityField{Support: api.SupportNo, Reason: "Vertex AI auth is not supported for this harness"}, - }, - Resume: api.CapabilityField{Support: api.SupportYes}, - } -} - -func (c *Codex) GetEnv(agentName string, agentHome string, unixUsername string) map[string]string { - return map[string]string{} -} - -func (c *Codex) GetCommand(task string, resume bool, baseArgs []string) []string { - args := []string{"codex", "--sandbox", "danger-full-access", "--dangerously-bypass-approvals-and-sandbox"} - if resume { - args = append(args, "resume", "--last") - } else { - if task != "" { - args = append(args, task) - } - } - - args = append(args, baseArgs...) - return args -} - -func (c *Codex) DefaultConfigDir() string { - return ".codex" -} - -func (c *Codex) SkillsDir() string { - return ".codex/skills" -} - -func (c *Codex) HasSystemPrompt(agentHome string) bool { - return false -} - -func (c *Codex) Provision(ctx context.Context, agentName, agentDir, agentHome, agentWorkspace string) error { - scionAgentPath := filepath.Join(agentDir, "scion-agent.json") - - var telemetryCfg *api.TelemetryConfig - if data, err := os.ReadFile(scionAgentPath); err == nil { - var cfg api.ScionConfig - if err := json.Unmarshal(data, &cfg); err != nil { - return fmt.Errorf("failed to parse scion-agent.json: %w", err) - } - telemetryCfg = cfg.Telemetry - } - - return c.ApplyTelemetrySettings(agentHome, telemetryCfg, nil) -} - -func (c *Codex) GetEmbedDir() string { - return "codex" -} - -func (c *Codex) GetInterruptKey() string { - return "C-c" -} - -func (c *Codex) GetHarnessEmbedsFS() (embed.FS, string) { - return codexEmbeds.EmbedsFS, "embeds" -} - -func (c *Codex) GetTelemetryEnv() map[string]string { - // Codex uses a TOML config file for telemetry, not env vars. - // File-based injection is handled via ResolveAuth. - return nil -} - -func (c *Codex) ApplyTelemetrySettings(agentHome string, telemetry *api.TelemetryConfig, env map[string]string) error { - return c.reconcileConfig(agentHome, telemetry, env) -} - -func (c *Codex) InjectAgentInstructions(agentHome string, content []byte) error { - dir := filepath.Join(agentHome, ".codex") - if err := os.MkdirAll(dir, 0755); err != nil { - return fmt.Errorf("failed to create .codex directory: %w", err) - } - target := filepath.Join(dir, "AGENTS.md") - return os.WriteFile(target, content, 0644) -} - -func (c *Codex) ResolveAuth(auth api.AuthConfig) (*api.ResolvedAuth, error) { - // Explicit selection support - if auth.SelectedType != "" { - switch auth.SelectedType { - case "api-key": - key := auth.CodexAPIKey - if key == "" { - key = auth.OpenAIAPIKey - } - if key == "" { - return nil, fmt.Errorf("codex: auth type %q selected but no API key found; set CODEX_API_KEY or OPENAI_API_KEY", auth.SelectedType) - } - envKey := "CODEX_API_KEY" - if auth.CodexAPIKey == "" { - envKey = "OPENAI_API_KEY" - } - return &api.ResolvedAuth{ - Method: "api-key", - EnvVars: map[string]string{envKey: key}, - }, nil - case "auth-file": - if auth.CodexAuthFile == "" { - return nil, fmt.Errorf("codex: auth type %q selected but no auth file found; expected ~/.codex/auth.json", auth.SelectedType) - } - return &api.ResolvedAuth{ - Method: "auth-file", - Files: []api.FileMapping{ - {SourcePath: auth.CodexAuthFile, ContainerPath: "~/.codex/auth.json"}, - }, - }, nil - default: - return nil, fmt.Errorf("codex: unknown auth type %q; valid types are: api-key, auth-file", auth.SelectedType) - } - } - - // Auto-detect preference order: CodexAPIKey → OpenAIAPIKey → CodexAuthFile → error - - if auth.CodexAPIKey != "" { - return &api.ResolvedAuth{ - Method: "api-key", - EnvVars: map[string]string{ - "CODEX_API_KEY": auth.CodexAPIKey, - }, - }, nil - } - - if auth.OpenAIAPIKey != "" { - return &api.ResolvedAuth{ - Method: "api-key", - EnvVars: map[string]string{ - "OPENAI_API_KEY": auth.OpenAIAPIKey, - }, - }, nil - } - - if auth.CodexAuthFile != "" { - return &api.ResolvedAuth{ - Method: "auth-file", - Files: []api.FileMapping{ - { - SourcePath: auth.CodexAuthFile, - ContainerPath: "~/.codex/auth.json", - }, - }, - }, nil - } - - return nil, fmt.Errorf("codex: no valid auth method found; set CODEX_API_KEY or OPENAI_API_KEY, or provide auth credentials at ~/.codex/auth.json") -} - -func (c *Codex) ApplyAuthSettings(agentHome string, resolved *api.ResolvedAuth) error { - if resolved == nil || resolved.Method != "api-key" { - return nil - } - - // Extract the API key from whichever env var was resolved. - var apiKey string - for _, k := range []string{"CODEX_API_KEY", "OPENAI_API_KEY"} { - if v := resolved.EnvVars[k]; v != "" { - apiKey = v - break - } - } - if apiKey == "" { - return nil - } - - codexDir := filepath.Join(agentHome, ".codex") - if err := os.MkdirAll(codexDir, 0755); err != nil { - return fmt.Errorf("failed to create .codex directory: %w", err) - } - - authData := map[string]string{ - "auth_mode": "apikey", - "OPENAI_API_KEY": apiKey, - } - data, err := json.MarshalIndent(authData, "", " ") - if err != nil { - return fmt.Errorf("failed to marshal auth.json: %w", err) - } - authPath := filepath.Join(codexDir, "auth.json") - return os.WriteFile(authPath, append(data, '\n'), 0600) -} - -func (c *Codex) InjectSystemPrompt(agentHome string, content []byte) error { - // TODO: Codex has no native system prompt support. System prompt injection is - // not yet implemented for this harness. - return nil -} diff --git a/pkg/harness/codex_config.go b/pkg/harness/codex_config.go deleted file mode 100644 index ee09a1b1d..000000000 --- a/pkg/harness/codex_config.go +++ /dev/null @@ -1,236 +0,0 @@ -// Copyright 2026 Google LLC -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package harness - -import ( - "fmt" - "os" - "path/filepath" - "sort" - "strings" - - "github.com/GoogleCloudPlatform/scion/pkg/api" -) - -func (c *Codex) reconcileConfig(agentHome string, telemetry *api.TelemetryConfig, env map[string]string) error { - codexDir := filepath.Join(agentHome, ".codex") - if err := os.MkdirAll(codexDir, 0755); err != nil { - return fmt.Errorf("failed to create .codex directory: %w", err) - } - - configPath := filepath.Join(codexDir, "config.toml") - content := "" - if data, err := os.ReadFile(configPath); err == nil { - content = string(data) - } else if !os.IsNotExist(err) { - return fmt.Errorf("failed to read codex config: %w", err) - } - - // Remove existing [otel] section — it will be rebuilt only if telemetry is enabled. - content = removeTOMLSection(content, "otel") - - // Reconcile [otel] only when telemetry is enabled. - if telemetry != nil && (telemetry.Enabled == nil || *telemetry.Enabled) { - endpoint := resolveCodexOTELEndpoint(telemetry, env) - protocol := resolveCodexOTELProtocol(telemetry, env) - - logUserPrompt := false - if telemetry.Filter != nil && telemetry.Filter.Events != nil { - if listContains(telemetry.Filter.Events.Include, "agent.user.prompt") { - logUserPrompt = true - } - if listContains(telemetry.Filter.Events.Exclude, "agent.user.prompt") { - logUserPrompt = false - } - } - - // Build exporter key based on protocol. - exporterKey := "otlp-grpc" - if protocol == "http" || protocol == "http/protobuf" { - exporterKey = "otlp-http" - } - - // Build headers inline table. - headers := "" - if telemetry.Cloud != nil && len(telemetry.Cloud.Headers) > 0 { - parts := make([]string, 0, len(telemetry.Cloud.Headers)) - for k, v := range telemetry.Cloud.Headers { - parts = append(parts, fmt.Sprintf(`"%s" = "%s"`, k, v)) - } - sort.Strings(parts) - headers = fmt.Sprintf(",\n headers = { %s }", strings.Join(parts, ", ")) - } - - otelSection := fmt.Sprintf("[otel]\nenabled = true\nlog_user_prompt = %v\nexporter = { %s = {\n endpoint = \"%s\"%s\n}}\n", - logUserPrompt, exporterKey, endpoint, headers) - - content = strings.TrimRight(content, "\n\t ") + "\n\n" + otelSection - } - - return os.WriteFile(configPath, []byte(strings.TrimSpace(content)+"\n"), 0644) -} - -func resolveCodexOTELEndpoint(telemetry *api.TelemetryConfig, env map[string]string) string { - if v := firstNonEmpty( - resolveEnv("SCION_CODEX_OTEL_ENDPOINT", env), - resolveEnv("SCION_OTEL_ENDPOINT", env), - ); v != "" { - return v - } - if telemetry != nil && telemetry.Cloud != nil && telemetry.Cloud.Endpoint != "" { - return telemetry.Cloud.Endpoint - } - return "localhost:4317" -} - -func resolveCodexOTELProtocol(telemetry *api.TelemetryConfig, env map[string]string) string { - if v := firstNonEmpty( - resolveEnv("SCION_CODEX_OTEL_PROTOCOL", env), - resolveEnv("SCION_OTEL_PROTOCOL", env), - ); v != "" { - return v - } - if telemetry != nil && telemetry.Cloud != nil && telemetry.Cloud.Protocol != "" { - return telemetry.Cloud.Protocol - } - return "grpc" -} - -func resolveEnv(key string, env map[string]string) string { - if env != nil { - if v := strings.TrimSpace(env[key]); v != "" { - return v - } - } - return strings.TrimSpace(os.Getenv(key)) -} - -func firstNonEmpty(values ...string) string { - for _, v := range values { - if strings.TrimSpace(v) != "" { - return strings.TrimSpace(v) - } - } - return "" -} - -func listContains(items []string, target string) bool { - for _, item := range items { - if strings.TrimSpace(item) == target { - return true - } - } - return false -} - -func removeTOMLSection(content, section string) string { - lines := strings.Split(content, "\n") - target := "[" + section + "]" - - sectionStart := -1 - sectionEnd := len(lines) - - for i, line := range lines { - trimmed := strings.TrimSpace(line) - if trimmed == target { - sectionStart = i - for j := i + 1; j < len(lines); j++ { - t := strings.TrimSpace(lines[j]) - if strings.HasPrefix(t, "[") && strings.HasSuffix(t, "]") { - sectionEnd = j - break - } - } - break - } - } - - if sectionStart == -1 { - return content - } - - // Also consume blank lines immediately before the section header. - for sectionStart > 0 && strings.TrimSpace(lines[sectionStart-1]) == "" { - sectionStart-- - } - - result := append(lines[:sectionStart], lines[sectionEnd:]...) - return strings.Join(result, "\n") -} - -func upsertTOMLKey(content, section, key, value string) string { - lines := strings.Split(content, "\n") - targetSection := strings.TrimSpace(section) - - sectionStart := 0 - sectionEnd := len(lines) - currentSection := "" - foundSection := targetSection == "" - - for i, line := range lines { - trimmed := strings.TrimSpace(line) - if strings.HasPrefix(trimmed, "[") && strings.HasSuffix(trimmed, "]") { - sectionName := strings.TrimSpace(strings.TrimSuffix(strings.TrimPrefix(trimmed, "["), "]")) - if currentSection == targetSection { - sectionEnd = i - break - } - currentSection = sectionName - if sectionName == targetSection { - foundSection = true - sectionStart = i + 1 - sectionEnd = len(lines) - } - } - } - - if targetSection == "" { - sectionStart = 0 - for i, line := range lines { - trimmed := strings.TrimSpace(line) - if strings.HasPrefix(trimmed, "[") && strings.HasSuffix(trimmed, "]") { - sectionEnd = i - break - } - } - } - - if !foundSection && targetSection != "" { - if strings.TrimSpace(content) != "" && !strings.HasSuffix(content, "\n") { - content += "\n" - } - if strings.TrimSpace(content) != "" { - content += "\n" - } - content += "[" + targetSection + "]\n" + key + " = " + value + "\n" - return content - } - - for i := sectionStart; i < sectionEnd; i++ { - line := strings.TrimSpace(lines[i]) - if line == "" || strings.HasPrefix(line, "#") { - continue - } - if strings.HasPrefix(line, key+" ") || strings.HasPrefix(line, key+"=") { - lines[i] = key + " = " + value - return strings.Join(lines, "\n") - } - } - - insertAt := sectionEnd - newLine := key + " = " + value - lines = append(lines[:insertAt], append([]string{newLine}, lines[insertAt:]...)...) - return strings.Join(lines, "\n") -} diff --git a/pkg/harness/codex_parity_test.go b/pkg/harness/codex_parity_test.go deleted file mode 100644 index e6ca66343..000000000 --- a/pkg/harness/codex_parity_test.go +++ /dev/null @@ -1,949 +0,0 @@ -// Copyright 2026 Google LLC -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package harness - -import ( - "context" - "encoding/json" - "os" - "os/exec" - "path/filepath" - "strings" - "testing" - "time" - - "github.com/GoogleCloudPlatform/scion/pkg/api" - "github.com/GoogleCloudPlatform/scion/pkg/config" -) - -// seedCodexDir seeds the embedded Codex harness-config into a temp dir using -// the same code path operators run during scion init / harness-config upgrade. -func seedCodexDir(t *testing.T) string { - t.Helper() - dir := t.TempDir() - if err := config.SeedHarnessConfig(dir, &Codex{}, false); err != nil { - t.Fatalf("SeedHarnessConfig: %v", err) - } - return dir -} - -// TestCodexEmbedsSeedRootSupportFiles verifies provision.py and config.toml -// land in the right places. provision.py is a root-level support file (Phase 1 -// allowlist); config.toml is a harness-native settings file under home/.codex/. -func TestCodexEmbedsSeedRootSupportFiles(t *testing.T) { - dir := seedCodexDir(t) - - provPath := filepath.Join(dir, "provision.py") - if _, err := os.Stat(provPath); err != nil { - t.Fatalf("expected provision.py at harness-config root: %v", err) - } - - configToml := filepath.Join(dir, "home", ".codex", "config.toml") - if _, err := os.Stat(configToml); err != nil { - t.Fatalf("expected config.toml under home/.codex/: %v", err) - } - - hc, err := config.LoadHarnessConfigDir(dir) - if err != nil { - t.Fatalf("LoadHarnessConfigDir: %v", err) - } - if hc.Config.Provisioner == nil { - t.Fatal("expected provisioner block in seeded config.yaml") - } - if hc.Config.Provisioner.Type != "container-script" { - t.Errorf("provisioner.type=%q want container-script", hc.Config.Provisioner.Type) - } - if len(hc.Config.Provisioner.Command) == 0 { - t.Error("expected provisioner.command to be set") - } -} - -// TestCodexActivateScriptIsNoOpWhenAlreadyActive verifies that -// --activate-script is idempotent: since the default config.yaml already -// sets provisioner.type to container-script, the upgrade produces no -// config change and no backup. -func TestCodexActivateScriptIsNoOpWhenAlreadyActive(t *testing.T) { - dir := seedCodexDir(t) - - plan, err := config.UpgradeHarnessConfig(dir, &Codex{}, config.HarnessConfigUpgradeOptions{ - ActivateScript: true, - Now: func() time.Time { return time.Date(2026, 4, 26, 0, 0, 0, 0, time.UTC) }, - }) - if err != nil { - t.Fatalf("UpgradeHarnessConfig --activate-script: %v", err) - } - if plan.Changed { - t.Fatal("expected no config change since type is already container-script") - } - - hc, err := config.LoadHarnessConfigDir(dir) - if err != nil { - t.Fatalf("LoadHarnessConfigDir after activate: %v", err) - } - if hc.Config.Provisioner == nil || hc.Config.Provisioner.Type != "container-script" { - t.Fatalf("provisioner.type after activate=%q want container-script", hc.Config.Provisioner.Type) - } - if len(plan.Backups) != 0 { - t.Fatalf("expected no backups, got %v", plan.Backups) - } -} - -// TestCodexContainerScriptHarnessParity covers Name/DefaultConfigDir/SkillsDir/ -// InterruptKey/AdvancedCapabilities. GetCommand parity has its own test -// because Codex's resume_flag is the first multi-token flag (`resume --last`) -// — single-token assertions wouldn't catch the split-on-whitespace gap. -func TestCodexContainerScriptHarnessParity(t *testing.T) { - dir := seedCodexDir(t) - hc, err := config.LoadHarnessConfigDir(dir) - if err != nil { - t.Fatalf("LoadHarnessConfigDir: %v", err) - } - scripted, err := NewContainerScriptHarness(dir, hc.Config) - if err != nil { - t.Fatalf("NewContainerScriptHarness: %v", err) - } - builtin := &Codex{} - - if scripted.Name() != builtin.Name() { - t.Errorf("Name: scripted=%q builtin=%q", scripted.Name(), builtin.Name()) - } - if scripted.DefaultConfigDir() != builtin.DefaultConfigDir() { - t.Errorf("DefaultConfigDir: scripted=%q builtin=%q", scripted.DefaultConfigDir(), builtin.DefaultConfigDir()) - } - if scripted.SkillsDir() != builtin.SkillsDir() { - t.Errorf("SkillsDir: scripted=%q builtin=%q", scripted.SkillsDir(), builtin.SkillsDir()) - } - if scripted.GetInterruptKey() != builtin.GetInterruptKey() { - t.Errorf("GetInterruptKey: scripted=%q builtin=%q", scripted.GetInterruptKey(), builtin.GetInterruptKey()) - } - - gotCaps := scripted.AdvancedCapabilities() - wantCaps := builtin.AdvancedCapabilities() - if gotCaps.Harness != wantCaps.Harness { - t.Errorf("Capabilities.Harness: scripted=%q builtin=%q", gotCaps.Harness, wantCaps.Harness) - } - if gotCaps.Telemetry.NativeEmitter.Support != wantCaps.Telemetry.NativeEmitter.Support { - t.Errorf("Capabilities.Telemetry.NativeEmitter: scripted=%v builtin=%v", gotCaps.Telemetry.NativeEmitter, wantCaps.Telemetry.NativeEmitter) - } - if gotCaps.Auth.APIKey.Support != wantCaps.Auth.APIKey.Support { - t.Errorf("Capabilities.Auth.APIKey: scripted=%v builtin=%v", gotCaps.Auth.APIKey, wantCaps.Auth.APIKey) - } - if gotCaps.Auth.AuthFile.Support != wantCaps.Auth.AuthFile.Support { - t.Errorf("Capabilities.Auth.AuthFile: scripted=%v builtin=%v", gotCaps.Auth.AuthFile, wantCaps.Auth.AuthFile) - } - if gotCaps.Auth.VertexAI.Support != wantCaps.Auth.VertexAI.Support { - t.Errorf("Capabilities.Auth.VertexAI: scripted=%v builtin=%v", gotCaps.Auth.VertexAI, wantCaps.Auth.VertexAI) - } - if gotCaps.Prompts.SystemPrompt.Support != wantCaps.Prompts.SystemPrompt.Support { - t.Errorf("Capabilities.Prompts.SystemPrompt: scripted=%v builtin=%v", gotCaps.Prompts.SystemPrompt, wantCaps.Prompts.SystemPrompt) - } -} - -// TestCodexContainerScriptGetCommandParity exercises the three operative -// command shapes. The resume case is the new ground that Phase 5 adds: Codex's -// "resume --last" is two argv tokens, so a missing whitespace-split in the -// container-script GetCommand would silently produce a single bogus arg. -func TestCodexContainerScriptGetCommandParity(t *testing.T) { - dir := seedCodexDir(t) - hc, err := config.LoadHarnessConfigDir(dir) - if err != nil { - t.Fatalf("LoadHarnessConfigDir: %v", err) - } - scripted, err := NewContainerScriptHarness(dir, hc.Config) - if err != nil { - t.Fatal(err) - } - builtin := &Codex{} - - cases := []struct { - name string - task string - resume bool - baseArg []string - }{ - {"resume_no_task", "", true, nil}, - {"task_only", "fix the bug", false, nil}, - {"task_with_base_args", "do it", false, []string{"--debug"}}, - {"no_task_with_base_args", "", false, []string{"--debug"}}, - } - for _, tc := range cases { - t.Run(tc.name, func(t *testing.T) { - gotS := scripted.GetCommand(tc.task, tc.resume, tc.baseArg) - gotB := builtin.GetCommand(tc.task, tc.resume, tc.baseArg) - if strings.Join(gotS, "|") != strings.Join(gotB, "|") { - t.Errorf("scripted=%v builtin=%v", gotS, gotB) - } - }) - } -} - -// TestCodexContainerScriptHarnessStagesScript verifies Provision() stages -// provision.py byte-identically and emits the trusted hook wrapper. -func TestCodexContainerScriptHarnessStagesScript(t *testing.T) { - dir := seedCodexDir(t) - hc, err := config.LoadHarnessConfigDir(dir) - if err != nil { - t.Fatalf("LoadHarnessConfigDir: %v", err) - } - scripted, err := NewContainerScriptHarness(dir, hc.Config) - if err != nil { - t.Fatal(err) - } - - agentHome := t.TempDir() - if err := scripted.Provision(context.Background(), "researcher", agentHome, agentHome, "/workspace"); err != nil { - t.Fatalf("Provision: %v", err) - } - - bundle := filepath.Join(agentHome, ".scion", "harness") - stagedScript := filepath.Join(bundle, "provision.py") - stagedBytes, err := os.ReadFile(stagedScript) - if err != nil { - t.Fatalf("provision.py not staged: %v", err) - } - srcBytes, err := os.ReadFile(filepath.Join(dir, "provision.py")) - if err != nil { - t.Fatal(err) - } - if string(stagedBytes) != string(srcBytes) { - t.Error("staged provision.py differs from harness-config copy") - } - - wrapper := filepath.Join(agentHome, ".scion", "hooks", "pre-start.d", "20-harness-provision") - wrapperBytes, err := os.ReadFile(wrapper) - if err != nil { - t.Fatalf("hook wrapper missing: %v", err) - } - if !strings.Contains(string(wrapperBytes), "sciontool harness provision") { - t.Errorf("wrapper missing expected command: %s", wrapperBytes) - } -} - -// TestCodexContainerScriptApplyAuthSettingsStagesSecretFiles is new for Phase -// 5: Codex needs the API key VALUE inside the container script, but -// sciontool harness provision strips secret env vars. The host stages each -// resolved env value as a 0600 file under .scion/harness/secrets/ and -// records the path in auth-candidates.json's env_secret_files map. -func TestCodexContainerScriptApplyAuthSettingsStagesSecretFiles(t *testing.T) { - dir := seedCodexDir(t) - hc, err := config.LoadHarnessConfigDir(dir) - if err != nil { - t.Fatal(err) - } - scripted, err := NewContainerScriptHarness(dir, hc.Config) - if err != nil { - t.Fatal(err) - } - - agentHome := t.TempDir() - resolved := &api.ResolvedAuth{ - Method: "container-script", - EnvVars: map[string]string{ - "CODEX_API_KEY": "codex-test-secret-value", - "INVALID-KEY": "should-be-skipped", - }, - } - if err := scripted.ApplyAuthSettings(agentHome, resolved); err != nil { - t.Fatalf("ApplyAuthSettings: %v", err) - } - - secretPath := filepath.Join(agentHome, ".scion", "harness", "secrets", "CODEX_API_KEY") - data, err := os.ReadFile(secretPath) - if err != nil { - t.Fatalf("secret file missing: %v", err) - } - if string(data) != "codex-test-secret-value" { - t.Errorf("secret value = %q, want %q", data, "codex-test-secret-value") - } - info, err := os.Stat(secretPath) - if err != nil { - t.Fatal(err) - } - if perm := info.Mode().Perm(); perm != 0600 { - t.Errorf("secret file perm = %o, want 0600", perm) - } - - // Invalid env names must not write a file (defends against caller-supplied - // "../../etc/passwd" style names). - if _, err := os.Stat(filepath.Join(agentHome, ".scion", "harness", "secrets", "INVALID-KEY")); !os.IsNotExist(err) { - t.Errorf("INVALID-KEY should not produce a secret file") - } - - candPath := filepath.Join(agentHome, ".scion", "harness", "inputs", "auth-candidates.json") - candBytes, err := os.ReadFile(candPath) - if err != nil { - t.Fatalf("auth-candidates.json missing: %v", err) - } - var cand map[string]any - if err := json.Unmarshal(candBytes, &cand); err != nil { - t.Fatalf("auth-candidates.json invalid: %v", err) - } - envSecretFiles, ok := cand["env_secret_files"].(map[string]any) - if !ok { - t.Fatalf("env_secret_files missing or wrong type: %T", cand["env_secret_files"]) - } - if envSecretFiles["CODEX_API_KEY"] != "$HOME/.scion/harness/secrets/CODEX_API_KEY" { - t.Errorf("env_secret_files[CODEX_API_KEY]=%v want $HOME-prefixed container path", envSecretFiles["CODEX_API_KEY"]) - } - // The auth-candidates JSON itself must NOT carry the secret value — that - // file is mode 0644 and would leak through normal log/diff tooling. - if strings.Contains(string(candBytes), "codex-test-secret-value") { - t.Errorf("auth-candidates.json must not embed the secret value: %s", candBytes) - } -} - -// TestCodexProvisionScript_Integration_APIKey runs the actual Python script -// against a synthetic manifest with a CODEX_API_KEY secret staged and -// verifies that .codex/auth.json is written in the format Codex expects. -func TestCodexProvisionScript_Integration_APIKey(t *testing.T) { - pyPath, err := exec.LookPath("python3") - if err != nil { - t.Skip("python3 not available") - } - - dir := seedCodexDir(t) - scriptPath := filepath.Join(dir, "provision.py") - - home := t.TempDir() - bundle := filepath.Join(home, ".scion", "harness") - for _, sub := range []string{"inputs", "outputs", "secrets"} { - if err := os.MkdirAll(filepath.Join(bundle, sub), 0755); err != nil { - t.Fatal(err) - } - } - - // Stage the secret VALUE file the way ApplyAuthSettings would. - secretValue := "sk-codex-test-12345" - if err := os.WriteFile(filepath.Join(bundle, "secrets", "CODEX_API_KEY"), []byte(secretValue), 0600); err != nil { - t.Fatal(err) - } - - manifest := map[string]any{ - "schema_version": 1, - "command": "provision", - "agent_name": "test-agent", - "agent_home": home, - "agent_workspace": "/workspace", - "harness_bundle_dir": bundle, - "harness_config": map[string]any{"harness": "codex"}, - "inputs": map[string]any{}, - "outputs": map[string]any{ - "env": filepath.Join(bundle, "outputs", "env.json"), - "resolved_auth": filepath.Join(bundle, "outputs", "resolved-auth.json"), - }, - "platform": map[string]any{"goos": "linux"}, - } - manifestPath := filepath.Join(bundle, "manifest.json") - manifestBytes, _ := json.MarshalIndent(manifest, "", " ") - if err := os.WriteFile(manifestPath, manifestBytes, 0644); err != nil { - t.Fatal(err) - } - - // Auth candidates: explicit api-key, CODEX_API_KEY available with secret file. - candidates := map[string]any{ - "schema_version": 1, - "explicit_type": "", - "resolved_method": "container-script", - "env_vars": []string{"CODEX_API_KEY"}, - "env_secret_files": map[string]string{ - "CODEX_API_KEY": filepath.Join(bundle, "secrets", "CODEX_API_KEY"), - }, - "files": []any{}, - } - candBytes, _ := json.MarshalIndent(candidates, "", " ") - if err := os.WriteFile(filepath.Join(bundle, "inputs", "auth-candidates.json"), candBytes, 0644); err != nil { - t.Fatal(err) - } - - cmd := exec.Command(pyPath, scriptPath, "--manifest", manifestPath) - cmd.Env = append(os.Environ(), "HOME="+home) - out, err := cmd.CombinedOutput() - if err != nil { - t.Fatalf("provision script failed: %v\noutput: %s", err, out) - } - - // Verify .codex/auth.json was written with the API key value. - authPath := filepath.Join(home, ".codex", "auth.json") - authBytes, err := os.ReadFile(authPath) - if err != nil { - t.Fatalf("auth.json missing: %v\nscript output: %s", err, out) - } - var auth map[string]string - if err := json.Unmarshal(authBytes, &auth); err != nil { - t.Fatalf("auth.json invalid: %v", err) - } - if auth["auth_mode"] != "apikey" { - t.Errorf("auth_mode=%q want apikey", auth["auth_mode"]) - } - // Compiled harness writes OPENAI_API_KEY regardless of source — match parity. - if auth["OPENAI_API_KEY"] != secretValue { - t.Errorf("OPENAI_API_KEY=%q want %q", auth["OPENAI_API_KEY"], secretValue) - } - info, err := os.Stat(authPath) - if err != nil { - t.Fatal(err) - } - if perm := info.Mode().Perm(); perm != 0600 { - t.Errorf("auth.json perm=%o want 0600", perm) - } - - resolvedBytes, err := os.ReadFile(filepath.Join(bundle, "outputs", "resolved-auth.json")) - if err != nil { - t.Fatalf("resolved-auth.json missing: %v", err) - } - var resolved map[string]any - if err := json.Unmarshal(resolvedBytes, &resolved); err != nil { - t.Fatal(err) - } - if resolved["method"] != "api-key" { - t.Errorf("method=%v want api-key", resolved["method"]) - } - if resolved["env_var"] != "CODEX_API_KEY" { - t.Errorf("env_var=%v want CODEX_API_KEY", resolved["env_var"]) - } - // Defense-in-depth: resolved-auth.json must NOT contain the secret value. - if strings.Contains(string(resolvedBytes), secretValue) { - t.Errorf("resolved-auth.json leaked secret value: %s", resolvedBytes) - } -} - -// TestCodexProvisionScript_Integration_TelemetryEnabled exercises the TOML -// reconciliation path. We seed a config.toml with a custom key that must be -// preserved, then verify the [otel] block is added with the resolved -// endpoint/headers/log_user_prompt. -func TestCodexProvisionScript_Integration_TelemetryEnabled(t *testing.T) { - pyPath, err := exec.LookPath("python3") - if err != nil { - t.Skip("python3 not available") - } - - dir := seedCodexDir(t) - scriptPath := filepath.Join(dir, "provision.py") - - home := t.TempDir() - bundle := filepath.Join(home, ".scion", "harness") - codexDir := filepath.Join(home, ".codex") - for _, sub := range []string{"inputs", "outputs", "secrets"} { - if err := os.MkdirAll(filepath.Join(bundle, sub), 0755); err != nil { - t.Fatal(err) - } - } - if err := os.MkdirAll(codexDir, 0755); err != nil { - t.Fatal(err) - } - // Pre-existing config with a custom key the user authored; the script - // must preserve it while updating [otel]. - initialToml := `approval_policy = "never" -custom_key = "keep-me" - -[projects."/workspace"] -trust_level = "trusted" -` - if err := os.WriteFile(filepath.Join(codexDir, "config.toml"), []byte(initialToml), 0644); err != nil { - t.Fatal(err) - } - - // Stage an api-key secret so the script's auth path also runs cleanly. - if err := os.WriteFile(filepath.Join(bundle, "secrets", "OPENAI_API_KEY"), []byte("sk-test"), 0600); err != nil { - t.Fatal(err) - } - - telemetryPayload := map[string]any{ - "schema_version": 1, - "telemetry": map[string]any{ - "enabled": true, - "cloud": map[string]any{ - "endpoint": "collector.example.com:4317", - "protocol": "grpc", - "headers": map[string]string{"x-api-key": "test123"}, - }, - }, - } - telBytes, _ := json.MarshalIndent(telemetryPayload, "", " ") - if err := os.WriteFile(filepath.Join(bundle, "inputs", "telemetry.json"), telBytes, 0644); err != nil { - t.Fatal(err) - } - - candidates := map[string]any{ - "schema_version": 1, - "env_vars": []string{"OPENAI_API_KEY"}, - "env_secret_files": map[string]string{ - "OPENAI_API_KEY": filepath.Join(bundle, "secrets", "OPENAI_API_KEY"), - }, - "files": []any{}, - } - candBytes, _ := json.MarshalIndent(candidates, "", " ") - if err := os.WriteFile(filepath.Join(bundle, "inputs", "auth-candidates.json"), candBytes, 0644); err != nil { - t.Fatal(err) - } - - manifest := map[string]any{ - "schema_version": 1, - "command": "provision", - "agent_name": "test-agent", - "agent_home": home, - "agent_workspace": "/workspace", - "harness_bundle_dir": bundle, - "harness_config": map[string]any{"harness": "codex"}, - "inputs": map[string]any{}, - "outputs": map[string]any{ - "env": filepath.Join(bundle, "outputs", "env.json"), - "resolved_auth": filepath.Join(bundle, "outputs", "resolved-auth.json"), - }, - } - manifestBytes, _ := json.MarshalIndent(manifest, "", " ") - manifestPath := filepath.Join(bundle, "manifest.json") - if err := os.WriteFile(manifestPath, manifestBytes, 0644); err != nil { - t.Fatal(err) - } - - cmd := exec.Command(pyPath, scriptPath, "--manifest", manifestPath) - cmd.Env = append(os.Environ(), "HOME="+home) - out, err := cmd.CombinedOutput() - if err != nil { - t.Fatalf("script failed: %v\noutput: %s", err, out) - } - - tomlBytes, err := os.ReadFile(filepath.Join(codexDir, "config.toml")) - if err != nil { - t.Fatal(err) - } - tomlStr := string(tomlBytes) - for _, want := range []string{ - `custom_key = "keep-me"`, - `[otel]`, - `enabled = true`, - `log_user_prompt = false`, - `exporter = { otlp-grpc = {`, - `endpoint = "collector.example.com:4317"`, - `headers = { "x-api-key" = "test123" }`, - } { - if !strings.Contains(tomlStr, want) { - t.Errorf("config.toml missing %q\ngot:\n%s", want, tomlStr) - } - } -} - -// TestCodexProvisionScript_Integration_TelemetryDisabled verifies the [otel] -// section is stripped when telemetry is disabled, even if the seeded TOML had -// one. This matches the compiled harness's reconcileConfig behavior. -func TestCodexProvisionScript_Integration_TelemetryDisabled(t *testing.T) { - pyPath, err := exec.LookPath("python3") - if err != nil { - t.Skip("python3 not available") - } - - dir := seedCodexDir(t) - scriptPath := filepath.Join(dir, "provision.py") - - home := t.TempDir() - bundle := filepath.Join(home, ".scion", "harness") - codexDir := filepath.Join(home, ".codex") - for _, sub := range []string{"inputs", "outputs", "secrets"} { - if err := os.MkdirAll(filepath.Join(bundle, sub), 0755); err != nil { - t.Fatal(err) - } - } - if err := os.MkdirAll(codexDir, 0755); err != nil { - t.Fatal(err) - } - // Seed a config that already has [otel] — we expect it to be stripped. - initialToml := `approval_policy = "never" - -[otel] -enabled = false -exporter = { otlp-grpc = { - endpoint = "localhost:4317" -}} -` - if err := os.WriteFile(filepath.Join(codexDir, "config.toml"), []byte(initialToml), 0644); err != nil { - t.Fatal(err) - } - if err := os.WriteFile(filepath.Join(bundle, "secrets", "OPENAI_API_KEY"), []byte("sk-test"), 0600); err != nil { - t.Fatal(err) - } - - telemetryPayload := map[string]any{ - "schema_version": 1, - "telemetry": map[string]any{ - "enabled": false, - }, - } - telBytes, _ := json.Marshal(telemetryPayload) - if err := os.WriteFile(filepath.Join(bundle, "inputs", "telemetry.json"), telBytes, 0644); err != nil { - t.Fatal(err) - } - candidates := map[string]any{ - "env_vars": []string{"OPENAI_API_KEY"}, - "env_secret_files": map[string]string{ - "OPENAI_API_KEY": filepath.Join(bundle, "secrets", "OPENAI_API_KEY"), - }, - } - candBytes, _ := json.Marshal(candidates) - if err := os.WriteFile(filepath.Join(bundle, "inputs", "auth-candidates.json"), candBytes, 0644); err != nil { - t.Fatal(err) - } - - manifest := map[string]any{ - "schema_version": 1, - "command": "provision", - "agent_name": "test-agent", - "agent_home": home, - "agent_workspace": "/workspace", - "harness_bundle_dir": bundle, - "harness_config": map[string]any{"harness": "codex"}, - "inputs": map[string]any{}, - "outputs": map[string]any{ - "env": filepath.Join(bundle, "outputs", "env.json"), - "resolved_auth": filepath.Join(bundle, "outputs", "resolved-auth.json"), - }, - } - manifestBytes, _ := json.Marshal(manifest) - manifestPath := filepath.Join(bundle, "manifest.json") - if err := os.WriteFile(manifestPath, manifestBytes, 0644); err != nil { - t.Fatal(err) - } - - cmd := exec.Command(pyPath, scriptPath, "--manifest", manifestPath) - cmd.Env = append(os.Environ(), "HOME="+home) - out, err := cmd.CombinedOutput() - if err != nil { - t.Fatalf("script failed: %v\noutput: %s", err, out) - } - - tomlBytes, err := os.ReadFile(filepath.Join(codexDir, "config.toml")) - if err != nil { - t.Fatal(err) - } - tomlStr := string(tomlBytes) - if strings.Contains(tomlStr, "[otel]") { - t.Errorf("config.toml still contains [otel] when telemetry is disabled:\n%s", tomlStr) - } - if !strings.Contains(tomlStr, `approval_policy = "never"`) { - t.Errorf("config.toml lost the user's approval_policy line:\n%s", tomlStr) - } -} - -// TestCodexProvisionScript_Integration_LogUserPromptFromFilter exercises the -// telemetry filter precedence (exclude beats include), matching the compiled -// harness's behavior in TestCodexApplyTelemetrySettings_LogUserPromptFromFilter. -func TestCodexProvisionScript_Integration_LogUserPromptFromFilter(t *testing.T) { - pyPath, err := exec.LookPath("python3") - if err != nil { - t.Skip("python3 not available") - } - - dir := seedCodexDir(t) - scriptPath := filepath.Join(dir, "provision.py") - - runOnce := func(t *testing.T, filter map[string]any, wantLogUserPrompt string) { - t.Helper() - home := t.TempDir() - bundle := filepath.Join(home, ".scion", "harness") - codexDir := filepath.Join(home, ".codex") - for _, sub := range []string{"inputs", "outputs", "secrets"} { - if err := os.MkdirAll(filepath.Join(bundle, sub), 0755); err != nil { - t.Fatal(err) - } - } - if err := os.MkdirAll(codexDir, 0755); err != nil { - t.Fatal(err) - } - if err := os.WriteFile(filepath.Join(bundle, "secrets", "OPENAI_API_KEY"), []byte("sk-test"), 0600); err != nil { - t.Fatal(err) - } - - telemetryPayload := map[string]any{ - "schema_version": 1, - "telemetry": map[string]any{ - "enabled": true, - "cloud": map[string]any{ - "endpoint": "collector.example.com:4317", - "protocol": "grpc", - }, - "filter": map[string]any{ - "events": filter, - }, - }, - } - telBytes, _ := json.Marshal(telemetryPayload) - if err := os.WriteFile(filepath.Join(bundle, "inputs", "telemetry.json"), telBytes, 0644); err != nil { - t.Fatal(err) - } - candidates := map[string]any{ - "env_vars": []string{"OPENAI_API_KEY"}, - "env_secret_files": map[string]string{ - "OPENAI_API_KEY": filepath.Join(bundle, "secrets", "OPENAI_API_KEY"), - }, - } - candBytes, _ := json.Marshal(candidates) - if err := os.WriteFile(filepath.Join(bundle, "inputs", "auth-candidates.json"), candBytes, 0644); err != nil { - t.Fatal(err) - } - manifest := map[string]any{ - "schema_version": 1, - "command": "provision", - "agent_name": "test-agent", - "agent_home": home, - "agent_workspace": "/workspace", - "harness_bundle_dir": bundle, - "harness_config": map[string]any{"harness": "codex"}, - "inputs": map[string]any{}, - "outputs": map[string]any{ - "env": filepath.Join(bundle, "outputs", "env.json"), - "resolved_auth": filepath.Join(bundle, "outputs", "resolved-auth.json"), - }, - } - manifestBytes, _ := json.Marshal(manifest) - manifestPath := filepath.Join(bundle, "manifest.json") - if err := os.WriteFile(manifestPath, manifestBytes, 0644); err != nil { - t.Fatal(err) - } - - cmd := exec.Command(pyPath, scriptPath, "--manifest", manifestPath) - cmd.Env = append(os.Environ(), "HOME="+home) - out, err := cmd.CombinedOutput() - if err != nil { - t.Fatalf("script failed: %v\noutput: %s", err, out) - } - tomlBytes, err := os.ReadFile(filepath.Join(codexDir, "config.toml")) - if err != nil { - t.Fatal(err) - } - if !strings.Contains(string(tomlBytes), "log_user_prompt = "+wantLogUserPrompt) { - t.Errorf("expected log_user_prompt = %s, got:\n%s", wantLogUserPrompt, tomlBytes) - } - } - - t.Run("include_only_enables", func(t *testing.T) { - runOnce(t, map[string]any{"include": []string{"agent.user.prompt"}}, "true") - }) - t.Run("exclude_overrides_include", func(t *testing.T) { - runOnce(t, map[string]any{ - "include": []string{"agent.user.prompt"}, - "exclude": []string{"agent.user.prompt"}, - }, "false") - }) -} - -// TestCodexProvisionScript_Integration_NoCreds asserts the script exits -// non-zero with an actionable message when no auth is staged. Mirrors the -// compiled harness's pre-launch failure mode and matches the OpenCode parity -// test's no-creds case. -func TestCodexProvisionScript_Integration_NoCreds(t *testing.T) { - pyPath, err := exec.LookPath("python3") - if err != nil { - t.Skip("python3 not available") - } - - dir := seedCodexDir(t) - scriptPath := filepath.Join(dir, "provision.py") - - home := t.TempDir() - bundle := filepath.Join(home, ".scion", "harness") - for _, sub := range []string{"inputs", "outputs", "secrets"} { - if err := os.MkdirAll(filepath.Join(bundle, sub), 0755); err != nil { - t.Fatal(err) - } - } - - manifest := map[string]any{ - "schema_version": 1, - "command": "provision", - "agent_name": "test-agent", - "agent_home": home, - "agent_workspace": "/workspace", - "harness_bundle_dir": bundle, - "harness_config": map[string]any{"harness": "codex"}, - "inputs": map[string]any{}, - "outputs": map[string]any{ - "env": filepath.Join(bundle, "outputs", "env.json"), - "resolved_auth": filepath.Join(bundle, "outputs", "resolved-auth.json"), - }, - } - manifestBytes, _ := json.Marshal(manifest) - manifestPath := filepath.Join(bundle, "manifest.json") - if err := os.WriteFile(manifestPath, manifestBytes, 0644); err != nil { - t.Fatal(err) - } - candidates := map[string]any{"env_vars": []string{}, "files": []any{}} - candBytes, _ := json.Marshal(candidates) - if err := os.WriteFile(filepath.Join(bundle, "inputs", "auth-candidates.json"), candBytes, 0644); err != nil { - t.Fatal(err) - } - - cmd := exec.Command(pyPath, scriptPath, "--manifest", manifestPath) - cmd.Env = append(os.Environ(), "HOME="+home) - out, err := cmd.CombinedOutput() - if err == nil { - t.Fatalf("expected non-zero exit, got success: %s", out) - } - if !strings.Contains(string(out), "no valid auth method") { - t.Errorf("expected actionable no-creds message, got: %s", out) - } -} - -// TestCodexProvisionScript_Integration_MCP runs the script with a staged -// mcp-servers.json input and asserts it appends [mcp_servers.] sections -// to ~/.codex/config.toml, preserving any pre-existing user keys and stripping -// stale MCP entries from a previous reprovision. -func TestCodexProvisionScript_Integration_MCP(t *testing.T) { - pyPath, err := exec.LookPath("python3") - if err != nil { - t.Skip("python3 not available") - } - - dir := seedCodexDir(t) - scriptPath := filepath.Join(dir, "provision.py") - // Stage scion_harness.py next to provision.py so the script's import - // resolves — production sets this up via ContainerScriptHarness.Provision. - if err := os.WriteFile(filepath.Join(dir, "scion_harness.py"), SharedHarnessHelperSource(), 0644); err != nil { - t.Fatal(err) - } - - home := t.TempDir() - bundle := filepath.Join(home, ".scion", "harness") - codexDir := filepath.Join(home, ".codex") - for _, sub := range []string{"inputs", "outputs", "secrets"} { - if err := os.MkdirAll(filepath.Join(bundle, sub), 0755); err != nil { - t.Fatal(err) - } - } - if err := os.MkdirAll(codexDir, 0755); err != nil { - t.Fatal(err) - } - - // Pre-existing config.toml carries an unrelated user key plus a stale - // [mcp_servers.gone] entry that must be stripped before the new set is - // written. This guards against two regressions at once: preservation of - // arbitrary user keys, and idempotent reprovisioning. - initialToml := `approval_policy = "never" -custom_key = "keep-me" - -[mcp_servers.gone] -command = "old-server" -` - if err := os.WriteFile(filepath.Join(codexDir, "config.toml"), []byte(initialToml), 0644); err != nil { - t.Fatal(err) - } - - // Stage an api-key secret so the auth phase succeeds — provisioning bails - // on auth failure before reaching MCP application. - if err := os.WriteFile(filepath.Join(bundle, "secrets", "OPENAI_API_KEY"), []byte("sk-test"), 0600); err != nil { - t.Fatal(err) - } - candidates := map[string]any{ - "schema_version": 1, - "env_vars": []string{"OPENAI_API_KEY"}, - "env_secret_files": map[string]string{ - "OPENAI_API_KEY": filepath.Join(bundle, "secrets", "OPENAI_API_KEY"), - }, - } - candBytes, _ := json.Marshal(candidates) - if err := os.WriteFile(filepath.Join(bundle, "inputs", "auth-candidates.json"), candBytes, 0644); err != nil { - t.Fatal(err) - } - - // MCP inputs exercise stdio (with args + env), streamable-http (with - // headers), and a project-scoped entry that must be demoted to global. - mcp := map[string]any{ - "schema_version": 1, - "mcp_servers": map[string]any{ - "chrome-devtools": map[string]any{ - "transport": "stdio", - "command": "chrome-devtools-mcp", - "args": []string{"--headless", "--browser-url", "http://localhost:9222"}, - "env": map[string]string{"DEBUG": "false"}, - }, - "remote_api": map[string]any{ - "transport": "streamable-http", - "url": "http://localhost:8080/mcp", - "headers": map[string]string{"Authorization": "Bearer xyz"}, - }, - "workspace_db": map[string]any{ - "transport": "stdio", - "command": "db-mcp", - "scope": "project", - }, - }, - } - mcpBytes, _ := json.MarshalIndent(mcp, "", " ") - if err := os.WriteFile(filepath.Join(bundle, "inputs", "mcp-servers.json"), mcpBytes, 0644); err != nil { - t.Fatal(err) - } - - manifest := map[string]any{ - "schema_version": 1, - "command": "provision", - "agent_name": "test-agent", - "agent_home": home, - "agent_workspace": "/workspace", - "harness_bundle_dir": bundle, - "harness_config": map[string]any{"harness": "codex"}, - "inputs": map[string]any{}, - "outputs": map[string]any{ - "env": filepath.Join(bundle, "outputs", "env.json"), - "resolved_auth": filepath.Join(bundle, "outputs", "resolved-auth.json"), - }, - } - manifestBytes, _ := json.Marshal(manifest) - manifestPath := filepath.Join(bundle, "manifest.json") - if err := os.WriteFile(manifestPath, manifestBytes, 0644); err != nil { - t.Fatal(err) - } - - cmd := exec.Command(pyPath, scriptPath, "--manifest", manifestPath) - cmd.Env = append(os.Environ(), "HOME="+home) - out, err := cmd.CombinedOutput() - if err != nil { - t.Fatalf("script failed: %v\noutput: %s", err, out) - } - - tomlBytes, err := os.ReadFile(filepath.Join(codexDir, "config.toml")) - if err != nil { - t.Fatal(err) - } - tomlStr := string(tomlBytes) - - for _, want := range []string{ - `custom_key = "keep-me"`, - `[mcp_servers.chrome-devtools]`, - `command = "chrome-devtools-mcp"`, - `args = ["--headless", "--browser-url", "http://localhost:9222"]`, - `env = { "DEBUG" = "false" }`, - `[mcp_servers.remote_api]`, - `url = "http://localhost:8080/mcp"`, - `http_headers = { "Authorization" = "Bearer xyz" }`, - `[mcp_servers.workspace_db]`, - `command = "db-mcp"`, - } { - if !strings.Contains(tomlStr, want) { - t.Errorf("config.toml missing %q\ngot:\n%s", want, tomlStr) - } - } - - // The stale [mcp_servers.gone] section must be stripped — a reprovision - // should not leave entries from previous template versions behind. - if strings.Contains(tomlStr, "[mcp_servers.gone]") || strings.Contains(tomlStr, `command = "old-server"`) { - t.Errorf("stale [mcp_servers.gone] section was not stripped:\n%s", tomlStr) - } - - if !strings.Contains(string(out), "project scope") { - t.Errorf("expected project-scope warning in stderr, got: %s", out) - } - if !strings.Contains(string(out), "applied 3 mcp server(s)") { - t.Errorf("expected 'applied 3 mcp server(s)' summary, got: %s", out) - } -} diff --git a/pkg/harness/codex_test.go b/pkg/harness/codex_test.go deleted file mode 100644 index 69ddca2a4..000000000 --- a/pkg/harness/codex_test.go +++ /dev/null @@ -1,461 +0,0 @@ -// Copyright 2026 Google LLC -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package harness - -import ( - "context" - "encoding/json" - "os" - "path/filepath" - "strings" - "testing" - - "github.com/GoogleCloudPlatform/scion/pkg/api" -) - -func TestCodexGetEnv(t *testing.T) { - c := &Codex{} - - // GetEnv should return empty map (auth handled by ResolvedAuth) - env := c.GetEnv("test-agent", "/tmp", "user") - if len(env) != 0 { - t.Errorf("expected empty env (auth handled by ResolvedAuth), got %v", env) - } -} - -func TestCodexGetCommand(t *testing.T) { - c := &Codex{} - - // Test standard command - cmd := c.GetCommand("do something", false, []string{}) - if len(cmd) < 5 || cmd[0] != "codex" || cmd[1] != "--sandbox" || cmd[2] != "danger-full-access" || cmd[3] != "--dangerously-bypass-approvals-and-sandbox" || cmd[4] != "do something" { - t.Errorf("unexpected command structure: %v", cmd) - } - - // Test resume - cmd = c.GetCommand("", true, []string{}) - if len(cmd) < 6 || cmd[4] != "resume" || cmd[5] != "--last" { - t.Errorf("unexpected resume command: %v", cmd) - } -} - -func TestCodexInjectAgentInstructions(t *testing.T) { - agentHome := t.TempDir() - c := &Codex{} - content := []byte("# Agent Instructions\nDo good work.") - - if err := c.InjectAgentInstructions(agentHome, content); err != nil { - t.Fatalf("InjectAgentInstructions failed: %v", err) - } - - target := filepath.Join(agentHome, ".codex", "AGENTS.md") - data, err := os.ReadFile(target) - if err != nil { - t.Fatalf("expected file at %s: %v", target, err) - } - if string(data) != string(content) { - t.Errorf("content mismatch: got %q, want %q", string(data), string(content)) - } -} - -func TestCodexResolveAuth_CodexAPIKey(t *testing.T) { - c := &Codex{} - auth := api.AuthConfig{CodexAPIKey: "codex-key"} - result, err := c.ResolveAuth(auth) - if err != nil { - t.Fatalf("unexpected error: %v", err) - } - if result.Method != "api-key" { - t.Errorf("Method = %q, want %q", result.Method, "api-key") - } - if result.EnvVars["CODEX_API_KEY"] != "codex-key" { - t.Errorf("CODEX_API_KEY = %q, want %q", result.EnvVars["CODEX_API_KEY"], "codex-key") - } -} - -func TestCodexResolveAuth_OpenAIAPIKey(t *testing.T) { - c := &Codex{} - auth := api.AuthConfig{OpenAIAPIKey: "openai-key"} - result, err := c.ResolveAuth(auth) - if err != nil { - t.Fatalf("unexpected error: %v", err) - } - if result.Method != "api-key" { - t.Errorf("Method = %q, want %q", result.Method, "api-key") - } - if result.EnvVars["OPENAI_API_KEY"] != "openai-key" { - t.Errorf("OPENAI_API_KEY = %q, want %q", result.EnvVars["OPENAI_API_KEY"], "openai-key") - } -} - -func TestCodexResolveAuth_AuthFile(t *testing.T) { - c := &Codex{} - auth := api.AuthConfig{CodexAuthFile: "/home/user/.codex/auth.json"} - result, err := c.ResolveAuth(auth) - if err != nil { - t.Fatalf("unexpected error: %v", err) - } - if result.Method != "auth-file" { - t.Errorf("Method = %q, want %q", result.Method, "auth-file") - } - if len(result.Files) != 1 { - t.Fatalf("expected 1 file mapping, got %d", len(result.Files)) - } - if result.Files[0].SourcePath != "/home/user/.codex/auth.json" { - t.Errorf("SourcePath = %q, want %q", result.Files[0].SourcePath, "/home/user/.codex/auth.json") - } -} - -func TestCodexResolveAuth_PreferenceOrder(t *testing.T) { - c := &Codex{} - // CodexAPIKey should win over OpenAIAPIKey and auth file - auth := api.AuthConfig{ - CodexAPIKey: "codex", - OpenAIAPIKey: "openai", - CodexAuthFile: "/auth.json", - } - result, err := c.ResolveAuth(auth) - if err != nil { - t.Fatalf("unexpected error: %v", err) - } - if result.Method != "api-key" { - t.Errorf("CodexAPIKey should win; Method = %q, want %q", result.Method, "api-key") - } - - // OpenAIAPIKey should win over auth file - auth = api.AuthConfig{ - OpenAIAPIKey: "openai", - CodexAuthFile: "/auth.json", - } - result, err = c.ResolveAuth(auth) - if err != nil { - t.Fatalf("unexpected error: %v", err) - } - if result.Method != "api-key" { - t.Errorf("OpenAIAPIKey should win over auth file; Method = %q, want %q", result.Method, "api-key") - } -} - -func TestCodexResolveAuth_NoCreds(t *testing.T) { - c := &Codex{} - _, err := c.ResolveAuth(api.AuthConfig{}) - if err == nil { - t.Fatal("expected error for empty AuthConfig") - } - if !strings.Contains(err.Error(), "CODEX_API_KEY") { - t.Errorf("error should mention CODEX_API_KEY: %v", err) - } -} - -func TestCodexInjectSystemPrompt_NoOp(t *testing.T) { - agentHome := t.TempDir() - c := &Codex{} - - // First inject agent instructions - agentContent := []byte("# Existing Instructions\nDo things.") - if err := c.InjectAgentInstructions(agentHome, agentContent); err != nil { - t.Fatalf("InjectAgentInstructions failed: %v", err) - } - - // System prompt injection should be a no-op (not yet supported) - sysContent := []byte("You are a helpful assistant.") - if err := c.InjectSystemPrompt(agentHome, sysContent); err != nil { - t.Fatalf("InjectSystemPrompt failed: %v", err) - } - - // AGENTS.md should remain unchanged — no system prompt prepended - target := filepath.Join(agentHome, ".codex", "AGENTS.md") - data, err := os.ReadFile(target) - if err != nil { - t.Fatalf("expected file at %s: %v", target, err) - } - - if string(data) != string(agentContent) { - t.Errorf("AGENTS.md was modified by InjectSystemPrompt; got %q, want %q", string(data), string(agentContent)) - } -} - -func TestCodexApplyAuthSettings_APIKeyWritesAuthFile(t *testing.T) { - agentHome := t.TempDir() - c := &Codex{} - - resolved := &api.ResolvedAuth{ - Method: "api-key", - EnvVars: map[string]string{"OPENAI_API_KEY": "test-key-value"}, - } - - if err := c.ApplyAuthSettings(agentHome, resolved); err != nil { - t.Fatalf("ApplyAuthSettings failed: %v", err) - } - - authPath := filepath.Join(agentHome, ".codex", "auth.json") - data, err := os.ReadFile(authPath) - if err != nil { - t.Fatalf("expected auth.json at %s: %v", authPath, err) - } - - var parsed map[string]string - if err := json.Unmarshal(data, &parsed); err != nil { - t.Fatalf("failed to parse auth.json: %v", err) - } - if parsed["auth_mode"] != "apikey" { - t.Errorf("auth_mode = %q, want %q", parsed["auth_mode"], "apikey") - } - if parsed["OPENAI_API_KEY"] != "test-key-value" { - t.Errorf("OPENAI_API_KEY = %q, want %q", parsed["OPENAI_API_KEY"], "test-key-value") - } - - // Verify file permissions are restrictive (0600) - info, err := os.Stat(authPath) - if err != nil { - t.Fatalf("failed to stat auth.json: %v", err) - } - if perm := info.Mode().Perm(); perm != 0600 { - t.Errorf("auth.json permissions = %o, want 0600", perm) - } -} - -func TestCodexApplyAuthSettings_CodexAPIKeyWritesAuthFile(t *testing.T) { - agentHome := t.TempDir() - c := &Codex{} - - resolved := &api.ResolvedAuth{ - Method: "api-key", - EnvVars: map[string]string{"CODEX_API_KEY": "codex-test-key"}, - } - - if err := c.ApplyAuthSettings(agentHome, resolved); err != nil { - t.Fatalf("ApplyAuthSettings failed: %v", err) - } - - data, err := os.ReadFile(filepath.Join(agentHome, ".codex", "auth.json")) - if err != nil { - t.Fatalf("expected auth.json: %v", err) - } - - var parsed map[string]string - if err := json.Unmarshal(data, &parsed); err != nil { - t.Fatalf("failed to parse auth.json: %v", err) - } - if parsed["auth_mode"] != "apikey" { - t.Errorf("auth_mode = %q, want %q", parsed["auth_mode"], "apikey") - } - if parsed["OPENAI_API_KEY"] != "codex-test-key" { - t.Errorf("OPENAI_API_KEY = %q, want %q", parsed["OPENAI_API_KEY"], "codex-test-key") - } -} - -func TestCodexApplyAuthSettings_NonAPIKeyNoOp(t *testing.T) { - agentHome := t.TempDir() - c := &Codex{} - - resolved := &api.ResolvedAuth{ - Method: "auth-file", - Files: []api.FileMapping{ - {SourcePath: "/some/path", ContainerPath: "~/.codex/auth.json"}, - }, - } - - if err := c.ApplyAuthSettings(agentHome, resolved); err != nil { - t.Fatalf("ApplyAuthSettings failed: %v", err) - } - - authPath := filepath.Join(agentHome, ".codex", "auth.json") - if _, err := os.Stat(authPath); !os.IsNotExist(err) { - t.Errorf("auth.json should not exist for auth-file method") - } -} - -func TestCodexApplyAuthSettings_NilResolvedNoOp(t *testing.T) { - c := &Codex{} - if err := c.ApplyAuthSettings(t.TempDir(), nil); err != nil { - t.Fatalf("ApplyAuthSettings with nil should not error: %v", err) - } -} - -func TestCodexApplyTelemetrySettings_EnabledMergesOtelAndPreservesKeys(t *testing.T) { - for _, e := range os.Environ() { - if strings.HasPrefix(e, "SCION_") { - k := strings.SplitN(e, "=", 2)[0] - t.Setenv(k, "") // registers cleanup to restore original value - os.Unsetenv(k) //nolint:errcheck - } - } - agentHome := t.TempDir() - c := &Codex{} - - codexDir := filepath.Join(agentHome, ".codex") - requireNoErr(t, os.MkdirAll(codexDir, 0755)) - initial := `approval_policy = "never" -custom_key = "keep-me" - -[projects."/workspace"] -trust_level = "trusted" -` - requireNoErr(t, os.WriteFile(filepath.Join(codexDir, "config.toml"), []byte(initial), 0644)) - - enabled := true - telemetry := &api.TelemetryConfig{ - Enabled: &enabled, - Cloud: &api.TelemetryCloudConfig{ - Endpoint: "collector.example.com:4317", - Protocol: "grpc", - Headers: map[string]string{"x-api-key": "test123"}, - }, - } - err := c.ApplyTelemetrySettings(agentHome, telemetry, nil) - requireNoErr(t, err) - - data, err := os.ReadFile(filepath.Join(codexDir, "config.toml")) - requireNoErr(t, err) - out := string(data) - containsAll(t, out, - `custom_key = "keep-me"`, - `[otel]`, - `enabled = true`, - `log_user_prompt = false`, - `exporter = { otlp-grpc = {`, - `endpoint = "collector.example.com:4317"`, - `headers = { "x-api-key" = "test123" }`, - ) - if strings.Contains(out, "notify") { - t.Fatalf("should not inject notify script, got:\n%s", out) - } -} - -func TestCodexApplyTelemetrySettings_DisabledDoesNotInjectOtel(t *testing.T) { - agentHome := t.TempDir() - c := &Codex{} - - // Seed a config that already has an [otel] section to verify it gets removed. - codexDir := filepath.Join(agentHome, ".codex") - requireNoErr(t, os.MkdirAll(codexDir, 0755)) - initial := `approval_policy = "never" - -[otel] -enabled = false -exporter = { otlp-grpc = { - endpoint = "localhost:4317" -}} -` - requireNoErr(t, os.WriteFile(filepath.Join(codexDir, "config.toml"), []byte(initial), 0644)) - - enabled := false - telemetry := &api.TelemetryConfig{Enabled: &enabled} - - err := c.ApplyTelemetrySettings(agentHome, telemetry, nil) - requireNoErr(t, err) - - data, err := os.ReadFile(filepath.Join(codexDir, "config.toml")) - requireNoErr(t, err) - out := string(data) - if strings.Contains(out, "[otel]") { - t.Fatalf("did not expect [otel] section when telemetry disabled, got:\n%s", out) - } - if strings.Contains(out, "notify") { - t.Fatalf("should not inject notify script, got:\n%s", out) - } -} - -func TestCodexProvision_ReconcilesTelemetryFromScionAgentConfig(t *testing.T) { - for _, e := range os.Environ() { - if strings.HasPrefix(e, "SCION_") { - k := strings.SplitN(e, "=", 2)[0] - t.Setenv(k, "") // registers cleanup to restore original value - os.Unsetenv(k) //nolint:errcheck - } - } - agentDir := t.TempDir() - agentHome := filepath.Join(agentDir, "home") - requireNoErr(t, os.MkdirAll(agentHome, 0755)) - - enabled := true - cfg := api.ScionConfig{ - Telemetry: &api.TelemetryConfig{ - Enabled: &enabled, - Cloud: &api.TelemetryCloudConfig{ - Endpoint: "otel.local:4317", - Protocol: "grpc", - }, - }, - } - data, err := jsonMarshal(cfg) - requireNoErr(t, err) - requireNoErr(t, os.WriteFile(filepath.Join(agentDir, "scion-agent.json"), data, 0644)) - - c := &Codex{} - err = c.Provision(context.Background(), "agent", agentDir, agentHome, "/workspace") - requireNoErr(t, err) - - out, err := os.ReadFile(filepath.Join(agentHome, ".codex", "config.toml")) - requireNoErr(t, err) - containsAll(t, string(out), `[otel]`, `endpoint = "otel.local:4317"`, `enabled = true`, `log_user_prompt = false`) -} - -func TestCodexApplyTelemetrySettings_LogUserPromptFromFilter(t *testing.T) { - agentHome := t.TempDir() - c := &Codex{} - - enabled := true - telemetry := &api.TelemetryConfig{ - Enabled: &enabled, - Cloud: &api.TelemetryCloudConfig{ - Endpoint: "collector.example.com:4317", - Protocol: "grpc", - }, - Filter: &api.TelemetryFilterConfig{ - Events: &api.TelemetryEventsConfig{ - Include: []string{"agent.user.prompt"}, - }, - }, - } - err := c.ApplyTelemetrySettings(agentHome, telemetry, nil) - requireNoErr(t, err) - - data, err := os.ReadFile(filepath.Join(agentHome, ".codex", "config.toml")) - requireNoErr(t, err) - out := string(data) - containsAll(t, out, `log_user_prompt = true`) - - // Now test exclusion takes precedence over inclusion. - telemetry.Filter.Events.Exclude = []string{"agent.user.prompt"} - err = c.ApplyTelemetrySettings(agentHome, telemetry, nil) - requireNoErr(t, err) - - data, err = os.ReadFile(filepath.Join(agentHome, ".codex", "config.toml")) - requireNoErr(t, err) - out = string(data) - containsAll(t, out, `log_user_prompt = false`) -} - -func requireNoErr(t *testing.T, err error) { - t.Helper() - if err != nil { - t.Fatal(err) - } -} - -func containsAll(t *testing.T, s string, substrings ...string) { - t.Helper() - for _, sub := range substrings { - if !strings.Contains(s, sub) { - t.Fatalf("expected output to contain %q, got:\n%s", sub, s) - } - } -} - -func jsonMarshal(v interface{}) ([]byte, error) { - return json.MarshalIndent(v, "", " ") -} diff --git a/pkg/harness/container_script_harness_test.go b/pkg/harness/container_script_harness_test.go index a32da34b0..634149e42 100644 --- a/pkg/harness/container_script_harness_test.go +++ b/pkg/harness/container_script_harness_test.go @@ -407,3 +407,49 @@ command: t.Errorf("GetCommand=%v", cmd) } } + +func TestResolve_LegacyBuiltinOpencode(t *testing.T) { + home := t.TempDir() + configsDir := filepath.Join(home, ".scion", "harness-configs") + hcDir := filepath.Join(configsDir, "opencode") + + // Legacy opencode config with provisioner.type: builtin (no container-script). + writeFile(t, filepath.Join(hcDir, "config.yaml"), `harness: opencode +image: scion-opencode:latest +user: scion +provisioner: + type: builtin + interface_version: 1 +command: + base: ["opencode"] +`) + writeFile(t, filepath.Join(hcDir, "provision.py"), "#!/usr/bin/env python3\n") + + t.Setenv("HOME", home) + + resolved, err := Resolve(context.Background(), ResolveOptions{Name: "opencode"}) + if err != nil { + t.Fatalf("Resolve should not error for legacy-builtin config: %v", err) + } + // Should fall through to declarative-generic (has command metadata). + if resolved.Implementation != "generic" { + t.Errorf("Implementation=%q want generic", resolved.Implementation) + } + if _, ok := resolved.Harness.(*DeclarativeGenericHarness); !ok { + t.Errorf("expected DeclarativeGenericHarness, got %T", resolved.Harness) + } +} + +func TestResolve_LegacyBuiltinCodexNoDir(t *testing.T) { + tmp := t.TempDir() + t.Setenv("HOME", tmp) + + // No on-disk directory at all — should fall to Generic without error. + resolved, err := Resolve(context.Background(), ResolveOptions{Name: "codex"}) + if err != nil { + t.Fatalf("Resolve should not error for missing codex: %v", err) + } + if resolved.Implementation != "generic" { + t.Errorf("Implementation=%q want generic", resolved.Implementation) + } +} diff --git a/pkg/harness/harness.go b/pkg/harness/harness.go index 933cd062a..ec89df987 100644 --- a/pkg/harness/harness.go +++ b/pkg/harness/harness.go @@ -24,10 +24,6 @@ func New(harnessName string) api.Harness { return &ClaudeCode{} case "gemini": return &GeminiCLI{} - case "opencode": - return &OpenCode{} - case "codex": - return &Codex{} default: return &Generic{} } @@ -37,7 +33,5 @@ func All() []api.Harness { return []api.Harness{ &GeminiCLI{}, &ClaudeCode{}, - &OpenCode{}, - &Codex{}, } } diff --git a/pkg/harness/harness_test.go b/pkg/harness/harness_test.go index ed184bfd6..101544407 100644 --- a/pkg/harness/harness_test.go +++ b/pkg/harness/harness_test.go @@ -27,8 +27,6 @@ func TestNew_BuiltinHarnesses(t *testing.T) { }{ {"claude", "claude"}, {"gemini", "gemini"}, - {"opencode", "opencode"}, - {"codex", "codex"}, } for _, tt := range tests { @@ -44,13 +42,11 @@ func TestNew_UnknownFallsToGeneric(t *testing.T) { func TestAll_ReturnsBuiltins(t *testing.T) { all := All() - assert.Len(t, all, 4) + assert.Len(t, all, 2) names := make([]string, len(all)) for i, h := range all { names[i] = h.Name() } assert.Contains(t, names, "gemini") assert.Contains(t, names, "claude") - assert.Contains(t, names, "opencode") - assert.Contains(t, names, "codex") } diff --git a/pkg/harness/opencode.go b/pkg/harness/opencode.go deleted file mode 100644 index a934b3490..000000000 --- a/pkg/harness/opencode.go +++ /dev/null @@ -1,200 +0,0 @@ -// Copyright 2026 Google LLC -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package harness - -import ( - "context" - "embed" - "fmt" - "os" - "path/filepath" - - "github.com/GoogleCloudPlatform/scion/pkg/api" - opencodeEmbeds "github.com/GoogleCloudPlatform/scion/pkg/harness/opencode" -) - -type OpenCode struct{} - -func (o *OpenCode) Name() string { - return "opencode" -} - -func (o *OpenCode) AdvancedCapabilities() api.HarnessAdvancedCapabilities { - return api.HarnessAdvancedCapabilities{ - Harness: "opencode", - Limits: api.HarnessLimitCapabilities{ - MaxTurns: api.CapabilityField{Support: api.SupportNo, Reason: "This harness has no hook dialect for turn events"}, - MaxModelCalls: api.CapabilityField{Support: api.SupportNo, Reason: "This harness has no hook dialect for model events"}, - MaxDuration: api.CapabilityField{Support: api.SupportYes}, - }, - Telemetry: api.HarnessTelemetryCapabilities{ - EnabledConfig: api.CapabilityField{Support: api.SupportYes}, - NativeEmitter: api.CapabilityField{Support: api.SupportNo, Reason: "Native telemetry forwarding is not wired for this harness"}, - }, - Prompts: api.HarnessPromptCapabilities{ - SystemPrompt: api.CapabilityField{Support: api.SupportPartial, Reason: "System prompt is downgraded into AGENTS.md"}, - AgentInstructions: api.CapabilityField{Support: api.SupportYes}, - }, - Auth: api.HarnessAuthCapabilities{ - APIKey: api.CapabilityField{Support: api.SupportYes}, - AuthFile: api.CapabilityField{Support: api.SupportYes}, - VertexAI: api.CapabilityField{Support: api.SupportNo, Reason: "Vertex AI auth is not supported for this harness"}, - }, - Resume: api.CapabilityField{Support: api.SupportYes}, - } -} - -func (o *OpenCode) GetEnv(agentName string, agentHome string, unixUsername string) map[string]string { - return map[string]string{} -} - -func (o *OpenCode) GetCommand(task string, resume bool, baseArgs []string) []string { - args := []string{"opencode"} - if resume { - args = append(args, "--continue") - } else { - args = append(args, "--prompt") - if task != "" { - args = append(args, task) - } - } - - args = append(args, baseArgs...) - return args -} -func (o *OpenCode) DefaultConfigDir() string { - return ".config/opencode" -} - -func (o *OpenCode) SkillsDir() string { - return ".config/opencode/skills" -} - -func (o *OpenCode) HasSystemPrompt(agentHome string) bool { - return false -} - -func (o *OpenCode) Provision(ctx context.Context, agentName, agentDir, agentHome, agentWorkspace string) error { - return nil -} - -func (o *OpenCode) GetEmbedDir() string { - return "opencode" -} - -func (o *OpenCode) GetInterruptKey() string { - return "C-c" -} - -func (o *OpenCode) GetHarnessEmbedsFS() (embed.FS, string) { - return opencodeEmbeds.EmbedsFS, "embeds" -} - -func (o *OpenCode) GetTelemetryEnv() map[string]string { - // OpenCode telemetry env var injection is deferred. - return nil -} - -func (o *OpenCode) InjectAgentInstructions(agentHome string, content []byte) error { - target := filepath.Join(agentHome, "AGENTS.md") - return os.WriteFile(target, content, 0644) -} - -func (o *OpenCode) ResolveAuth(auth api.AuthConfig) (*api.ResolvedAuth, error) { - // Explicit selection support - if auth.SelectedType != "" { - switch auth.SelectedType { - case "api-key": - key := auth.AnthropicAPIKey - if key == "" { - key = auth.OpenAIAPIKey - } - if key == "" { - return nil, fmt.Errorf("opencode: auth type %q selected but no API key found; set ANTHROPIC_API_KEY or OPENAI_API_KEY", auth.SelectedType) - } - envKey := "ANTHROPIC_API_KEY" - if auth.AnthropicAPIKey == "" { - envKey = "OPENAI_API_KEY" - } - return &api.ResolvedAuth{ - Method: "api-key", - EnvVars: map[string]string{envKey: key}, - }, nil - case "auth-file": - if auth.OpenCodeAuthFile == "" { - return nil, fmt.Errorf("opencode: auth type %q selected but no auth file found; expected ~/.local/share/opencode/auth.json", auth.SelectedType) - } - return &api.ResolvedAuth{ - Method: "auth-file", - Files: []api.FileMapping{ - {SourcePath: auth.OpenCodeAuthFile, ContainerPath: "~/.local/share/opencode/auth.json"}, - }, - }, nil - default: - return nil, fmt.Errorf("opencode: unknown auth type %q; valid types are: api-key, auth-file", auth.SelectedType) - } - } - - // Auto-detect preference order: AnthropicAPIKey → OpenAIAPIKey → OpenCodeAuthFile → error - - if auth.AnthropicAPIKey != "" { - return &api.ResolvedAuth{ - Method: "api-key", - EnvVars: map[string]string{ - "ANTHROPIC_API_KEY": auth.AnthropicAPIKey, - }, - }, nil - } - - if auth.OpenAIAPIKey != "" { - return &api.ResolvedAuth{ - Method: "api-key", - EnvVars: map[string]string{ - "OPENAI_API_KEY": auth.OpenAIAPIKey, - }, - }, nil - } - - if auth.OpenCodeAuthFile != "" { - return &api.ResolvedAuth{ - Method: "auth-file", - Files: []api.FileMapping{ - { - SourcePath: auth.OpenCodeAuthFile, - ContainerPath: "~/.local/share/opencode/auth.json", - }, - }, - }, nil - } - - return nil, fmt.Errorf("opencode: no valid auth method found; set ANTHROPIC_API_KEY or OPENAI_API_KEY, or provide auth credentials at ~/.local/share/opencode/auth.json") -} - -func (o *OpenCode) InjectSystemPrompt(agentHome string, content []byte) error { - // OpenCode has no native system prompt support — downgrade by prepending to AGENTS.md - agentsPath := filepath.Join(agentHome, "AGENTS.md") - header := fmt.Sprintf("# System Prompt\n\n%s\n\n---\n\n", string(content)) - - existing, err := os.ReadFile(agentsPath) - if err != nil && !os.IsNotExist(err) { - return fmt.Errorf("failed to read existing agent instructions: %w", err) - } - - merged := []byte(header) - if len(existing) > 0 { - merged = append(merged, existing...) - } - return os.WriteFile(agentsPath, merged, 0644) -} diff --git a/pkg/harness/opencode_parity_test.go b/pkg/harness/opencode_parity_test.go deleted file mode 100644 index 47c6daf83..000000000 --- a/pkg/harness/opencode_parity_test.go +++ /dev/null @@ -1,674 +0,0 @@ -// Copyright 2026 Google LLC -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package harness - -import ( - "context" - "encoding/json" - "os" - "os/exec" - "path/filepath" - "strings" - "testing" - "time" - - "github.com/GoogleCloudPlatform/scion/pkg/api" - "github.com/GoogleCloudPlatform/scion/pkg/config" -) - -// seedOpenCodeDir seeds the embedded OpenCode harness-config into a temp dir -// using the same code path operators run during scion init / harness-config -// upgrade. It returns the absolute target dir so tests can inspect it. -func seedOpenCodeDir(t *testing.T) string { - t.Helper() - dir := t.TempDir() - if err := config.SeedHarnessConfig(dir, &OpenCode{}, false); err != nil { - t.Fatalf("SeedHarnessConfig: %v", err) - } - return dir -} - -// TestOpenCodeEmbedsSeedRootSupportFiles verifies the new provision.py and -// the existing opencode.json land where Phase 1 said they should: provision.py -// at the harness-config root, opencode.json under home/.config/opencode/. -func TestOpenCodeEmbedsSeedRootSupportFiles(t *testing.T) { - dir := seedOpenCodeDir(t) - - // provision.py is a root-level support file (Phase 1 allowlist). - provPath := filepath.Join(dir, "provision.py") - if _, err := os.Stat(provPath); err != nil { - t.Fatalf("expected provision.py at harness-config root: %v", err) - } - - // opencode.json is the harness-native settings file; it lives under home. - opencodeJSON := filepath.Join(dir, "home", ".config", "opencode", "opencode.json") - if _, err := os.Stat(opencodeJSON); err != nil { - t.Fatalf("expected opencode.json under home/.config/opencode/: %v", err) - } - - // config.yaml at the root must be valid and declare the container-script - // provisioner so the in-container provision.py runs during pre-start and - // handles MCP translation, auth, etc. - hc, err := config.LoadHarnessConfigDir(dir) - if err != nil { - t.Fatalf("LoadHarnessConfigDir: %v", err) - } - if hc.Config.Provisioner == nil { - t.Fatal("expected provisioner block in seeded config.yaml") - } - if hc.Config.Provisioner.Type != "container-script" { - t.Errorf("provisioner.type=%q want container-script", hc.Config.Provisioner.Type) - } - if len(hc.Config.Provisioner.Command) == 0 { - t.Error("expected provisioner.command in config.yaml") - } -} - -// TestOpenCodeActivateScriptIsIdempotent verifies that --activate-script is a -// no-op when the embedded default already declares container-script. Existing -// installations that upgraded before the default changed are still handled by -// activateContainerScriptProvisioner, but freshly seeded configs must not -// produce spurious backups. -func TestOpenCodeActivateScriptIsIdempotent(t *testing.T) { - dir := seedOpenCodeDir(t) - - plan, err := config.UpgradeHarnessConfig(dir, &OpenCode{}, config.HarnessConfigUpgradeOptions{ - ActivateScript: true, - Now: func() time.Time { return time.Date(2026, 4, 25, 0, 0, 0, 0, time.UTC) }, - }) - if err != nil { - t.Fatalf("UpgradeHarnessConfig --activate-script: %v", err) - } - if plan.Changed { - t.Fatalf("expected no change (already container-script), got actions: %v", plan.Actions) - } - - hc, err := config.LoadHarnessConfigDir(dir) - if err != nil { - t.Fatalf("LoadHarnessConfigDir after activate: %v", err) - } - if hc.Config.Provisioner == nil || hc.Config.Provisioner.Type != "container-script" { - t.Fatalf("provisioner.type=%q want container-script", hc.Config.Provisioner.Type) - } - if len(plan.Backups) != 0 { - t.Fatalf("expected no backups for idempotent activate, got %v", plan.Backups) - } -} - -// TestOpenCodeContainerScriptHarnessParity asserts the ContainerScriptHarness -// wrapper produces the same observable command/env/capability/getter values as -// the compiled OpenCode harness for the embedded config. Parity is the -// acceptance gate from Phase 0; this test makes it executable for OpenCode. -func TestOpenCodeContainerScriptHarnessParity(t *testing.T) { - dir := seedOpenCodeDir(t) - - hc, err := config.LoadHarnessConfigDir(dir) - if err != nil { - t.Fatalf("LoadHarnessConfigDir: %v", err) - } - scripted, err := NewContainerScriptHarness(dir, hc.Config) - if err != nil { - t.Fatalf("NewContainerScriptHarness: %v", err) - } - builtin := &OpenCode{} - - // 1. Name must match — both report "opencode" so dispatch logic stays consistent. - if scripted.Name() != builtin.Name() { - t.Errorf("Name parity: scripted=%q builtin=%q", scripted.Name(), builtin.Name()) - } - if scripted.DefaultConfigDir() != builtin.DefaultConfigDir() { - t.Errorf("DefaultConfigDir: scripted=%q builtin=%q", scripted.DefaultConfigDir(), builtin.DefaultConfigDir()) - } - if scripted.SkillsDir() != builtin.SkillsDir() { - t.Errorf("SkillsDir: scripted=%q builtin=%q", scripted.SkillsDir(), builtin.SkillsDir()) - } - if scripted.GetInterruptKey() != builtin.GetInterruptKey() { - t.Errorf("GetInterruptKey: scripted=%q builtin=%q", scripted.GetInterruptKey(), builtin.GetInterruptKey()) - } - - // 2. GetCommand must match across the three operative shapes. - cases := []struct { - name string - task string - resume bool - baseArg []string - }{ - {"resume_no_task", "", true, nil}, - {"task_only", "fix the bug", false, nil}, - {"task_with_base_args", "do it", false, []string{"--debug"}}, - } - for _, tc := range cases { - t.Run("GetCommand_"+tc.name, func(t *testing.T) { - gotS := scripted.GetCommand(tc.task, tc.resume, tc.baseArg) - gotB := builtin.GetCommand(tc.task, tc.resume, tc.baseArg) - if strings.Join(gotS, " ") != strings.Join(gotB, " ") { - t.Errorf("scripted=%v builtin=%v", gotS, gotB) - } - }) - } - - // 3. AdvancedCapabilities must report the same shape; the embedded YAML - // is the single source of truth for both, so any drift indicates a bug - // in either the YAML mapping or the compiled getter. - gotCaps := scripted.AdvancedCapabilities() - wantCaps := builtin.AdvancedCapabilities() - if gotCaps.Harness != wantCaps.Harness { - t.Errorf("Capabilities.Harness: scripted=%q builtin=%q", gotCaps.Harness, wantCaps.Harness) - } - if gotCaps.Limits.MaxDuration.Support != wantCaps.Limits.MaxDuration.Support { - t.Errorf("Capabilities.Limits.MaxDuration: scripted=%v builtin=%v", gotCaps.Limits.MaxDuration, wantCaps.Limits.MaxDuration) - } - if gotCaps.Auth.APIKey.Support != wantCaps.Auth.APIKey.Support { - t.Errorf("Capabilities.Auth.APIKey: scripted=%v builtin=%v", gotCaps.Auth.APIKey, wantCaps.Auth.APIKey) - } - if gotCaps.Auth.AuthFile.Support != wantCaps.Auth.AuthFile.Support { - t.Errorf("Capabilities.Auth.AuthFile: scripted=%v builtin=%v", gotCaps.Auth.AuthFile, wantCaps.Auth.AuthFile) - } - if gotCaps.Auth.VertexAI.Support != wantCaps.Auth.VertexAI.Support { - t.Errorf("Capabilities.Auth.VertexAI: scripted=%v builtin=%v", gotCaps.Auth.VertexAI, wantCaps.Auth.VertexAI) - } - if gotCaps.Prompts.SystemPrompt.Support != wantCaps.Prompts.SystemPrompt.Support { - t.Errorf("Capabilities.Prompts.SystemPrompt: scripted=%v builtin=%v", gotCaps.Prompts.SystemPrompt, wantCaps.Prompts.SystemPrompt) - } -} - -// TestOpenCodeContainerScriptHarnessStagesScript verifies Provision() copies -// the seeded provision.py into the agent bundle and writes a wrapper that -// targets sciontool harness provision. The bundle is what the in-container -// hook actually runs. -func TestOpenCodeContainerScriptHarnessStagesScript(t *testing.T) { - dir := seedOpenCodeDir(t) - - hc, err := config.LoadHarnessConfigDir(dir) - if err != nil { - t.Fatalf("LoadHarnessConfigDir: %v", err) - } - scripted, err := NewContainerScriptHarness(dir, hc.Config) - if err != nil { - t.Fatalf("NewContainerScriptHarness: %v", err) - } - - agentHome := t.TempDir() - if err := scripted.Provision(context.Background(), "researcher", agentHome, agentHome, "/workspace"); err != nil { - t.Fatalf("Provision: %v", err) - } - - bundle := filepath.Join(agentHome, ".scion", "harness") - stagedScript := filepath.Join(bundle, "provision.py") - if _, err := os.Stat(stagedScript); err != nil { - t.Fatalf("provision.py not staged into bundle: %v", err) - } - - // The staged script must be byte-identical to the source-of-truth in the - // seeded harness-config dir, otherwise upgrade workflows will silently - // drift container behavior away from the hub artifact. - stagedBytes, err := os.ReadFile(stagedScript) - if err != nil { - t.Fatal(err) - } - srcBytes, err := os.ReadFile(filepath.Join(dir, "provision.py")) - if err != nil { - t.Fatal(err) - } - if string(stagedBytes) != string(srcBytes) { - t.Error("staged provision.py differs from harness-config copy") - } - - wrapper := filepath.Join(agentHome, ".scion", "hooks", "pre-start.d", "20-harness-provision") - wrapperBytes, err := os.ReadFile(wrapper) - if err != nil { - t.Fatalf("hook wrapper missing: %v", err) - } - if !strings.Contains(string(wrapperBytes), "sciontool harness provision") { - t.Errorf("wrapper does not invoke sciontool harness provision: %s", wrapperBytes) - } -} - -// TestOpenCodeContainerScriptReconcilesMissingBundle verifies that calling -// Provision() on an agent home that lacks the container-script bundle (as -// happens for agents provisioned before the builtin→container-script -// migration) stages the hook wrapper, provision.py, and manifest. This -// mirrors the reconciliation path added in run.go. -func TestOpenCodeContainerScriptReconcilesMissingBundle(t *testing.T) { - dir := seedOpenCodeDir(t) - - hc, err := config.LoadHarnessConfigDir(dir) - if err != nil { - t.Fatalf("LoadHarnessConfigDir: %v", err) - } - scripted, err := NewContainerScriptHarness(dir, hc.Config) - if err != nil { - t.Fatalf("NewContainerScriptHarness: %v", err) - } - - agentHome := t.TempDir() - - // Simulate an agent home created by the builtin OpenCode{} harness: - // the config dir exists but there is no .scion/harness/ bundle and no - // pre-start hook wrapper. - configDir := filepath.Join(agentHome, ".config", "opencode") - if err := os.MkdirAll(configDir, 0755); err != nil { - t.Fatal(err) - } - if err := os.WriteFile(filepath.Join(configDir, "opencode.json"), []byte("{}"), 0644); err != nil { - t.Fatal(err) - } - - // Confirm the hook wrapper does NOT exist yet. - hookWrapper := filepath.Join(agentHome, ".scion", "hooks", "pre-start.d", "20-harness-provision") - if _, err := os.Stat(hookWrapper); err == nil { - t.Fatal("hook wrapper should not exist before reconciliation") - } - - // Call Provision (the reconciliation path). - if err := scripted.Provision(context.Background(), "migrated-agent", agentHome, agentHome, "/workspace"); err != nil { - t.Fatalf("Provision (reconciliation): %v", err) - } - - // Hook wrapper must now exist. - wrapperBytes, err := os.ReadFile(hookWrapper) - if err != nil { - t.Fatalf("hook wrapper not staged after reconciliation: %v", err) - } - if !strings.Contains(string(wrapperBytes), "sciontool harness provision") { - t.Errorf("wrapper does not invoke sciontool harness provision: %s", wrapperBytes) - } - - // provision.py must be staged. - if _, err := os.Stat(filepath.Join(agentHome, ".scion", "harness", "provision.py")); err != nil { - t.Errorf("provision.py not staged: %v", err) - } - - // manifest.json must be present. - if _, err := os.Stat(filepath.Join(agentHome, ".scion", "harness", "manifest.json")); err != nil { - t.Errorf("manifest.json not staged: %v", err) - } - - // Pre-existing opencode.json must be preserved. - if _, err := os.Stat(filepath.Join(configDir, "opencode.json")); err != nil { - t.Errorf("pre-existing opencode.json was removed: %v", err) - } -} - -// TestOpenCodeProvisionScript_Integration_HappyPath runs the actual Python -// script against a synthetic manifest and validates outputs. We skip when -// python3 is unavailable so the test is portable, and use a tightly-scoped -// $HOME to avoid leaking host paths into resolved-auth.json. -func TestOpenCodeProvisionScript_Integration_HappyPath(t *testing.T) { - pyPath, err := exec.LookPath("python3") - if err != nil { - t.Skip("python3 not available; skipping script integration test") - } - - dir := seedOpenCodeDir(t) - scriptPath := filepath.Join(dir, "provision.py") - - home := t.TempDir() - bundle := filepath.Join(home, ".scion", "harness") - if err := os.MkdirAll(filepath.Join(bundle, "inputs"), 0755); err != nil { - t.Fatal(err) - } - if err := os.MkdirAll(filepath.Join(bundle, "outputs"), 0755); err != nil { - t.Fatal(err) - } - - manifest := map[string]any{ - "schema_version": 1, - "command": "provision", - "agent_name": "test-agent", - "agent_home": home, - "agent_workspace": "/workspace", - "harness_bundle_dir": bundle, - "harness_config": map[string]any{"harness": "opencode"}, - "inputs": map[string]any{}, - "outputs": map[string]any{ - "env": filepath.Join(bundle, "outputs", "env.json"), - "resolved_auth": filepath.Join(bundle, "outputs", "resolved-auth.json"), - }, - "platform": map[string]any{"goos": "linux", "goarch": "amd64"}, - } - manifestPath := filepath.Join(bundle, "manifest.json") - manifestBytes, _ := json.MarshalIndent(manifest, "", " ") - if err := os.WriteFile(manifestPath, manifestBytes, 0644); err != nil { - t.Fatal(err) - } - - candidates := map[string]any{ - "schema_version": 1, - "explicit_type": "", - "resolved_method": "container-script", - "env_vars": []string{"OPENAI_API_KEY"}, - "files": []any{}, - } - candBytes, _ := json.MarshalIndent(candidates, "", " ") - if err := os.WriteFile(filepath.Join(bundle, "inputs", "auth-candidates.json"), candBytes, 0644); err != nil { - t.Fatal(err) - } - - cmd := exec.Command(pyPath, scriptPath, "--manifest", manifestPath) - cmd.Env = append(os.Environ(), "HOME="+home) - out, err := cmd.CombinedOutput() - if err != nil { - t.Fatalf("provision script failed: %v\noutput: %s", err, out) - } - - resolvedBytes, err := os.ReadFile(filepath.Join(bundle, "outputs", "resolved-auth.json")) - if err != nil { - t.Fatalf("resolved-auth.json missing: %v\nscript output: %s", err, out) - } - var resolved map[string]any - if err := json.Unmarshal(resolvedBytes, &resolved); err != nil { - t.Fatalf("resolved-auth.json invalid: %v", err) - } - if resolved["method"] != "api-key" { - t.Errorf("method=%v want api-key", resolved["method"]) - } - if resolved["env_var"] != "OPENAI_API_KEY" { - t.Errorf("env_var=%v want OPENAI_API_KEY (precedence: only OpenAI was offered)", resolved["env_var"]) - } - - envBytes, err := os.ReadFile(filepath.Join(bundle, "outputs", "env.json")) - if err != nil { - t.Fatalf("env.json missing: %v", err) - } - var envOverlay map[string]any - if err := json.Unmarshal(envBytes, &envOverlay); err != nil { - t.Fatalf("env.json invalid: %v", err) - } - if len(envOverlay) != 0 { - t.Errorf("env.json should be empty for OpenCode (no overrides), got %v", envOverlay) - } -} - -// TestOpenCodeProvisionScript_Integration_MCP runs the script with a staged -// mcp-servers.json input and asserts it translates universal entries into -// OpenCode's native shape (mcp..type=local|remote, command array, etc.). -func TestOpenCodeProvisionScript_Integration_MCP(t *testing.T) { - pyPath, err := exec.LookPath("python3") - if err != nil { - t.Skip("python3 not available; skipping script integration test") - } - - dir := seedOpenCodeDir(t) - scriptPath := filepath.Join(dir, "provision.py") - // Stage scion_harness.py next to provision.py so the import in the - // script resolves — production sets this up via ContainerScriptHarness. - if err := os.WriteFile(filepath.Join(dir, "scion_harness.py"), SharedHarnessHelperSource(), 0644); err != nil { - t.Fatal(err) - } - - home := t.TempDir() - bundle := filepath.Join(home, ".scion", "harness") - if err := os.MkdirAll(filepath.Join(bundle, "inputs"), 0755); err != nil { - t.Fatal(err) - } - if err := os.MkdirAll(filepath.Join(bundle, "outputs"), 0755); err != nil { - t.Fatal(err) - } - // Copy the helper into the bundle too because production stages it there - // (ContainerScriptHarness.Provision writes it). The integration test - // invokes the script from the seeded harness-config dir, so the import - // works from there as well — staging here mirrors the production layout - // so changes to where the helper goes get caught. - if err := os.WriteFile(filepath.Join(bundle, "scion_harness.py"), SharedHarnessHelperSource(), 0644); err != nil { - t.Fatal(err) - } - - manifest := map[string]any{ - "schema_version": 1, - "command": "provision", - "agent_name": "test-agent", - "agent_home": home, - "agent_workspace": "/workspace", - "harness_bundle_dir": bundle, - "harness_config": map[string]any{"harness": "opencode"}, - "inputs": map[string]any{}, - "outputs": map[string]any{ - "env": filepath.Join(bundle, "outputs", "env.json"), - "resolved_auth": filepath.Join(bundle, "outputs", "resolved-auth.json"), - }, - "platform": map[string]any{"goos": "linux", "goarch": "amd64"}, - } - manifestBytes, _ := json.MarshalIndent(manifest, "", " ") - if err := os.WriteFile(filepath.Join(bundle, "manifest.json"), manifestBytes, 0644); err != nil { - t.Fatal(err) - } - - // Auth candidates so the auth phase succeeds (script bails before MCP - // otherwise). - candidates := map[string]any{ - "schema_version": 1, - "explicit_type": "", - "resolved_method": "container-script", - "env_vars": []string{"OPENAI_API_KEY"}, - "files": []any{}, - } - candBytes, _ := json.MarshalIndent(candidates, "", " ") - if err := os.WriteFile(filepath.Join(bundle, "inputs", "auth-candidates.json"), candBytes, 0644); err != nil { - t.Fatal(err) - } - - // Stage MCP servers — exercise stdio, SSE, and a project-scoped entry - // (which should be silently demoted to global with a warning). - mcp := map[string]any{ - "schema_version": 1, - "mcp_servers": map[string]any{ - "chrome-devtools": map[string]any{ - "transport": "stdio", - "command": "chrome-devtools-mcp", - "args": []string{"--headless", "--browser-url", "http://localhost:9222"}, - "env": map[string]string{"DEBUG": "false"}, - }, - "remote_api": map[string]any{ - "transport": "sse", - "url": "http://localhost:8080/mcp/sse", - "headers": map[string]string{"Authorization": "Bearer xyz"}, - }, - "workspace_db": map[string]any{ - "transport": "stdio", - "command": "db-mcp", - "scope": "project", - }, - }, - } - mcpBytes, _ := json.MarshalIndent(mcp, "", " ") - if err := os.WriteFile(filepath.Join(bundle, "inputs", "mcp-servers.json"), mcpBytes, 0644); err != nil { - t.Fatal(err) - } - - cmd := exec.Command(pyPath, scriptPath, "--manifest", filepath.Join(bundle, "manifest.json")) - cmd.Env = append(os.Environ(), "HOME="+home) - out, err := cmd.CombinedOutput() - if err != nil { - t.Fatalf("provision script failed: %v\noutput: %s", err, out) - } - - opencodeJSONPath := filepath.Join(home, ".config", "opencode", "opencode.json") - data, err := os.ReadFile(opencodeJSONPath) - if err != nil { - t.Fatalf("opencode.json not written: %v\nscript output: %s", err, out) - } - var cfg map[string]any - if err := json.Unmarshal(data, &cfg); err != nil { - t.Fatalf("opencode.json invalid JSON: %v", err) - } - mcpBlock, ok := cfg["mcp"].(map[string]any) - if !ok { - t.Fatalf("opencode.json mcp block missing or wrong type: %v", cfg["mcp"]) - } - - // chrome-devtools: stdio -> local with combined command array. - chrome, ok := mcpBlock["chrome-devtools"].(map[string]any) - if !ok { - t.Fatalf("chrome-devtools entry missing") - } - if chrome["type"] != "local" { - t.Errorf("chrome-devtools type=%v want local", chrome["type"]) - } - cmdArr, ok := chrome["command"].([]any) - if !ok { - t.Fatalf("chrome-devtools command is not an array: %T", chrome["command"]) - } - wantCmd := []string{"chrome-devtools-mcp", "--headless", "--browser-url", "http://localhost:9222"} - if len(cmdArr) != len(wantCmd) { - t.Errorf("chrome-devtools command length=%d want %d (got %v)", len(cmdArr), len(wantCmd), cmdArr) - } - for i, c := range cmdArr { - if i >= len(wantCmd) { - break - } - if c != wantCmd[i] { - t.Errorf("chrome-devtools command[%d]=%v want %v", i, c, wantCmd[i]) - } - } - envMap, ok := chrome["environment"].(map[string]any) - if !ok || envMap["DEBUG"] != "false" { - t.Errorf("chrome-devtools environment=%v want DEBUG=false", chrome["environment"]) - } - - // remote_api: sse -> remote with url and headers. - remote, ok := mcpBlock["remote_api"].(map[string]any) - if !ok { - t.Fatalf("remote_api entry missing") - } - if remote["type"] != "remote" { - t.Errorf("remote_api type=%v want remote", remote["type"]) - } - if remote["url"] != "http://localhost:8080/mcp/sse" { - t.Errorf("remote_api url=%v", remote["url"]) - } - headers, ok := remote["headers"].(map[string]any) - if !ok || headers["Authorization"] != "Bearer xyz" { - t.Errorf("remote_api headers=%v", remote["headers"]) - } - - // workspace_db: project-scoped stdio, treated as global with warning. - if _, ok := mcpBlock["workspace_db"]; !ok { - t.Errorf("workspace_db entry missing (project-scoped should be demoted to global, not dropped)") - } - if !strings.Contains(string(out), "project scope") { - t.Errorf("expected project-scope warning in stderr, got: %s", out) - } - if !strings.Contains(string(out), "applied 3 mcp server(s)") { - t.Errorf("expected 'applied 3 mcp server(s)' summary, got: %s", out) - } -} - -// TestOpenCodeProvisionScript_Integration_NoCreds asserts the script exits -// non-zero with an actionable message when nothing is staged. This matches -// the compiled harness's pre-launch failure mode. -func TestOpenCodeProvisionScript_Integration_NoCreds(t *testing.T) { - pyPath, err := exec.LookPath("python3") - if err != nil { - t.Skip("python3 not available; skipping script integration test") - } - - dir := seedOpenCodeDir(t) - scriptPath := filepath.Join(dir, "provision.py") - - home := t.TempDir() - bundle := filepath.Join(home, ".scion", "harness") - if err := os.MkdirAll(filepath.Join(bundle, "inputs"), 0755); err != nil { - t.Fatal(err) - } - if err := os.MkdirAll(filepath.Join(bundle, "outputs"), 0755); err != nil { - t.Fatal(err) - } - - manifest := map[string]any{ - "schema_version": 1, - "command": "provision", - "agent_name": "test-agent", - "agent_home": home, - "agent_workspace": "/workspace", - "harness_bundle_dir": bundle, - "harness_config": map[string]any{"harness": "opencode"}, - "inputs": map[string]any{}, - "outputs": map[string]any{ - "env": filepath.Join(bundle, "outputs", "env.json"), - "resolved_auth": filepath.Join(bundle, "outputs", "resolved-auth.json"), - }, - } - manifestBytes, _ := json.Marshal(manifest) - manifestPath := filepath.Join(bundle, "manifest.json") - if err := os.WriteFile(manifestPath, manifestBytes, 0644); err != nil { - t.Fatal(err) - } - - candidates := map[string]any{ - "schema_version": 1, - "explicit_type": "", - "resolved_method": "container-script", - "env_vars": []string{}, - "files": []any{}, - } - candBytes, _ := json.Marshal(candidates) - if err := os.WriteFile(filepath.Join(bundle, "inputs", "auth-candidates.json"), candBytes, 0644); err != nil { - t.Fatal(err) - } - - cmd := exec.Command(pyPath, scriptPath, "--manifest", manifestPath) - cmd.Env = append(os.Environ(), "HOME="+home) - out, err := cmd.CombinedOutput() - if err == nil { - t.Fatalf("expected non-zero exit, got success. output: %s", out) - } - if !strings.Contains(string(out), "no valid auth method") { - t.Errorf("expected actionable no-creds message, got: %s", out) - } -} - -// TestOpenCodeContainerScriptResolveAuthShape verifies the container-script -// ResolveAuth surfaces the values the script will need (env keys + files) -// while never returning the original Method strings the runtime gates on. -// This protects callers like applyResolvedAuth that branch on Method. -func TestOpenCodeContainerScriptResolveAuthShape(t *testing.T) { - dir := seedOpenCodeDir(t) - - hc, err := config.LoadHarnessConfigDir(dir) - if err != nil { - t.Fatal(err) - } - scripted, err := NewContainerScriptHarness(dir, hc.Config) - if err != nil { - t.Fatal(err) - } - - // Pass both an Anthropic key and an auth file; the container-script - // wrapper must surface BOTH so the in-container script can choose, - // whereas the compiled harness would have collapsed to one. - resolved, err := scripted.ResolveAuth(api.AuthConfig{ - AnthropicAPIKey: "sk-ant-xx", - OpenCodeAuthFile: "/tmp/auth.json", - }) - if err != nil { - t.Fatalf("ResolveAuth: %v", err) - } - if resolved.Method != "container-script" { - t.Errorf("Method=%q want container-script (final selection deferred to script)", resolved.Method) - } - if resolved.EnvVars["ANTHROPIC_API_KEY"] != "sk-ant-xx" { - t.Errorf("expected ANTHROPIC_API_KEY to flow through, got %v", resolved.EnvVars) - } - foundOpenCodeAuthFile := false - for _, f := range resolved.Files { - if f.SourcePath == "/tmp/auth.json" && strings.HasSuffix(f.ContainerPath, "/auth.json") { - foundOpenCodeAuthFile = true - } - } - if !foundOpenCodeAuthFile { - t.Errorf("expected OpenCode auth file in Files mapping, got %#v", resolved.Files) - } -} diff --git a/pkg/harness/opencode_test.go b/pkg/harness/opencode_test.go deleted file mode 100644 index b28968c51..000000000 --- a/pkg/harness/opencode_test.go +++ /dev/null @@ -1,187 +0,0 @@ -// Copyright 2026 Google LLC -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package harness - -import ( - "os" - "path/filepath" - "strings" - "testing" - - "github.com/GoogleCloudPlatform/scion/pkg/api" -) - -func TestOpenCodeInjectAgentInstructions(t *testing.T) { - agentHome := t.TempDir() - o := &OpenCode{} - content := []byte("# Agent Instructions\nDo good work.") - - if err := o.InjectAgentInstructions(agentHome, content); err != nil { - t.Fatalf("InjectAgentInstructions failed: %v", err) - } - - target := filepath.Join(agentHome, "AGENTS.md") - data, err := os.ReadFile(target) - if err != nil { - t.Fatalf("expected file at %s: %v", target, err) - } - if string(data) != string(content) { - t.Errorf("content mismatch: got %q, want %q", string(data), string(content)) - } -} - -func TestOpenCodeInjectSystemPrompt(t *testing.T) { - agentHome := t.TempDir() - o := &OpenCode{} - - // First inject agent instructions - agentContent := []byte("# Existing Instructions\nDo things.") - if err := o.InjectAgentInstructions(agentHome, agentContent); err != nil { - t.Fatalf("InjectAgentInstructions failed: %v", err) - } - - // Now inject system prompt (should prepend) - sysContent := []byte("You are a helpful assistant.") - if err := o.InjectSystemPrompt(agentHome, sysContent); err != nil { - t.Fatalf("InjectSystemPrompt failed: %v", err) - } - - target := filepath.Join(agentHome, "AGENTS.md") - data, err := os.ReadFile(target) - if err != nil { - t.Fatalf("expected file at %s: %v", target, err) - } - - content := string(data) - if !strings.Contains(content, "# System Prompt") { - t.Error("expected system prompt header in merged content") - } - if !strings.Contains(content, "You are a helpful assistant.") { - t.Error("expected system prompt content in merged file") - } - if !strings.Contains(content, "# Existing Instructions") { - t.Error("expected original agent instructions to be preserved") - } -} - -func TestOpenCodeResolveAuth_AnthropicAPIKey(t *testing.T) { - o := &OpenCode{} - auth := api.AuthConfig{AnthropicAPIKey: "sk-ant-test"} - result, err := o.ResolveAuth(auth) - if err != nil { - t.Fatalf("unexpected error: %v", err) - } - if result.Method != "api-key" { - t.Errorf("Method = %q, want %q", result.Method, "api-key") - } - if result.EnvVars["ANTHROPIC_API_KEY"] != "sk-ant-test" { - t.Errorf("ANTHROPIC_API_KEY = %q, want %q", result.EnvVars["ANTHROPIC_API_KEY"], "sk-ant-test") - } -} - -func TestOpenCodeResolveAuth_OpenAIAPIKey(t *testing.T) { - o := &OpenCode{} - auth := api.AuthConfig{OpenAIAPIKey: "sk-openai-test"} - result, err := o.ResolveAuth(auth) - if err != nil { - t.Fatalf("unexpected error: %v", err) - } - if result.Method != "api-key" { - t.Errorf("Method = %q, want %q", result.Method, "api-key") - } - if result.EnvVars["OPENAI_API_KEY"] != "sk-openai-test" { - t.Errorf("OPENAI_API_KEY = %q, want %q", result.EnvVars["OPENAI_API_KEY"], "sk-openai-test") - } -} - -func TestOpenCodeResolveAuth_AuthFile(t *testing.T) { - o := &OpenCode{} - auth := api.AuthConfig{OpenCodeAuthFile: "/home/user/.local/share/opencode/auth.json"} - result, err := o.ResolveAuth(auth) - if err != nil { - t.Fatalf("unexpected error: %v", err) - } - if result.Method != "auth-file" { - t.Errorf("Method = %q, want %q", result.Method, "auth-file") - } - if len(result.Files) != 1 { - t.Fatalf("expected 1 file mapping, got %d", len(result.Files)) - } -} - -func TestOpenCodeResolveAuth_PreferenceOrder(t *testing.T) { - o := &OpenCode{} - // AnthropicAPIKey should win over OpenAIAPIKey and auth file - auth := api.AuthConfig{ - AnthropicAPIKey: "anthropic", - OpenAIAPIKey: "openai", - OpenCodeAuthFile: "/auth.json", - } - result, err := o.ResolveAuth(auth) - if err != nil { - t.Fatalf("unexpected error: %v", err) - } - if result.Method != "api-key" { - t.Errorf("AnthropicAPIKey should win; Method = %q, want %q", result.Method, "api-key") - } - - // OpenAIAPIKey should win over auth file - auth = api.AuthConfig{ - OpenAIAPIKey: "openai", - OpenCodeAuthFile: "/auth.json", - } - result, err = o.ResolveAuth(auth) - if err != nil { - t.Fatalf("unexpected error: %v", err) - } - if result.Method != "api-key" { - t.Errorf("OpenAIAPIKey should win over auth file; Method = %q, want %q", result.Method, "api-key") - } -} - -func TestOpenCodeResolveAuth_NoCreds(t *testing.T) { - o := &OpenCode{} - _, err := o.ResolveAuth(api.AuthConfig{}) - if err == nil { - t.Fatal("expected error for empty AuthConfig") - } - if !strings.Contains(err.Error(), "ANTHROPIC_API_KEY") { - t.Errorf("error should mention ANTHROPIC_API_KEY: %v", err) - } -} - -func TestOpenCodeInjectSystemPrompt_NoExistingInstructions(t *testing.T) { - agentHome := t.TempDir() - o := &OpenCode{} - - sysContent := []byte("You are a helpful assistant.") - if err := o.InjectSystemPrompt(agentHome, sysContent); err != nil { - t.Fatalf("InjectSystemPrompt failed: %v", err) - } - - target := filepath.Join(agentHome, "AGENTS.md") - data, err := os.ReadFile(target) - if err != nil { - t.Fatalf("expected file at %s: %v", target, err) - } - - content := string(data) - if !strings.Contains(content, "# System Prompt") { - t.Error("expected system prompt header") - } - if !strings.Contains(content, "You are a helpful assistant.") { - t.Error("expected system prompt content") - } -} diff --git a/pkg/harness/resolve.go b/pkg/harness/resolve.go index d109ed636..e2c902ad5 100644 --- a/pkg/harness/resolve.go +++ b/pkg/harness/resolve.go @@ -17,6 +17,9 @@ package harness import ( "context" "fmt" + "log/slog" + "os" + "path/filepath" "github.com/GoogleCloudPlatform/scion/pkg/api" "github.com/GoogleCloudPlatform/scion/pkg/config" @@ -110,6 +113,19 @@ func Resolve(_ context.Context, opts ResolveOptions) (*ResolvedHarness, error) { }, nil } + if entry.Harness == "opencode" || entry.Harness == "codex" { + if hcDir == nil { + slog.Warn("harness is not installed; run: scion harness-config install harnesses/"+entry.Harness, "harness", entry.Harness) + } else if entry.Provisioner == nil || entry.Provisioner.Type != "container-script" { + hint := "run: scion harness-config upgrade " + opts.Name + " --activate-script" + if !fileExistsInDir(hcDir.Path, "provision.py") { + hint = "run: scion harness-config install harnesses/" + entry.Harness + } + slog.Warn("legacy built-in harness config no longer has a compiled-in implementation; "+hint, + "harness", entry.Harness, "config_dir", hcDir.Path) + } + } + // 3. Declarative generic. If config.yaml has declarative metadata // (command/env_template/capabilities), use the declarative wrapper so // callers get those fields. Otherwise fall back to the legacy Generic. @@ -140,10 +156,6 @@ func newBuiltin(harnessName string) api.Harness { return &ClaudeCode{} case "gemini": return &GeminiCLI{} - case "opencode": - return &OpenCode{} - case "codex": - return &Codex{} } return nil } @@ -190,6 +202,11 @@ func mergeHarnessConfigEntries(base, overlay config.HarnessConfigEntry) config.H return base } +func fileExistsInDir(dir, name string) bool { + _, err := os.Stat(filepath.Join(dir, name)) + return err == nil +} + func hasDeclarativeMetadata(entry config.HarnessConfigEntry) bool { if entry.Command != nil && len(entry.Command.Base) > 0 { return true diff --git a/pkg/hub/admin_maintenance_test.go b/pkg/hub/admin_maintenance_test.go index 185979206..ca0a0012b 100644 --- a/pkg/hub/admin_maintenance_test.go +++ b/pkg/hub/admin_maintenance_test.go @@ -27,13 +27,12 @@ import ( "time" "github.com/GoogleCloudPlatform/scion/pkg/store" - "github.com/GoogleCloudPlatform/scion/pkg/store/sqlite" "github.com/GoogleCloudPlatform/scion/pkg/util/logging" ) func newTestServerWithStore(t *testing.T) (*Server, store.Store) { t.Helper() - s, err := sqlite.New(":memory:") + s, err := newTestStore(":memory:") if err != nil { t.Fatalf("failed to create sqlite store: %v", err) } @@ -349,7 +348,7 @@ func TestListOperationRuns(t *testing.T) { completed := time.Now().Add(10 * time.Second) for i, status := range []string{"completed", "failed"} { run := &store.MaintenanceOperationRun{ - ID: fmt.Sprintf("run-%d", i), + ID: tid(fmt.Sprintf("run-%d", i)), OperationKey: "pull-images", Status: status, StartedAt: now, @@ -406,7 +405,7 @@ func TestGetOperationRun(t *testing.T) { now := time.Now() completed := now.Add(10 * time.Second) run := &store.MaintenanceOperationRun{ - ID: "run-detail-1", + ID: tid("run-detail-1"), OperationKey: "pull-images", Status: "completed", StartedAt: now, @@ -419,7 +418,7 @@ func TestGetOperationRun(t *testing.T) { } admin := NewAuthenticatedUser("u1", "admin@example.com", "Admin", "admin", "cli") - req := httptest.NewRequest(http.MethodGet, "/api/v1/admin/maintenance/operations/pull-images/runs/run-detail-1", nil) + req := httptest.NewRequest(http.MethodGet, fmt.Sprintf("/api/v1/admin/maintenance/operations/pull-images/runs/%s", tid("run-detail-1")), nil) req = req.WithContext(contextWithIdentity(req.Context(), admin)) rr := httptest.NewRecorder() srv.handleAdminMaintenanceOps(rr, req) @@ -432,7 +431,7 @@ func TestGetOperationRun(t *testing.T) { if err := json.Unmarshal(rr.Body.Bytes(), &resp); err != nil { t.Fatalf("invalid JSON: %v", err) } - if resp["id"] != "run-detail-1" { + if resp["id"] != tid("run-detail-1") { t.Errorf("expected id=run-detail-1, got %v", resp["id"]) } if resp["log"] != "Pulling images...\nDone." { diff --git a/pkg/hub/admin_mode_test.go b/pkg/hub/admin_mode_test.go index 2bde40125..b2fbb2252 100644 --- a/pkg/hub/admin_mode_test.go +++ b/pkg/hub/admin_mode_test.go @@ -150,8 +150,8 @@ func TestAdminModeMiddleware_AgentIdentity(t *testing.T) { mw := adminModeMiddleware(state)(passthrough) agent := &agentIdentityWrapper{&AgentTokenClaims{ - Claims: jwt.Claims{Subject: "agent-1"}, - ProjectID: "project-1", + Claims: jwt.Claims{Subject: tid("agent-1")}, + ProjectID: tid("project-1"), }} req := httptest.NewRequest(http.MethodGet, "/api/v1/agents", nil) req = req.WithContext(contextWithIdentity(req.Context(), agent)) @@ -167,7 +167,7 @@ func TestAdminModeMiddleware_BrokerIdentity(t *testing.T) { state := NewMaintenanceState(true, "") mw := adminModeMiddleware(state)(passthrough) - broker := NewBrokerIdentity("broker-1") + broker := NewBrokerIdentity(tid("broker-1")) req := httptest.NewRequest(http.MethodGet, "/api/v1/agents", nil) ctx := contextWithIdentity(req.Context(), broker) ctx = contextWithBrokerIdentity(ctx, broker) diff --git a/pkg/hub/admin_reset_auth.go b/pkg/hub/admin_reset_auth.go new file mode 100644 index 000000000..24111acea --- /dev/null +++ b/pkg/hub/admin_reset_auth.go @@ -0,0 +1,85 @@ +package hub + +import ( + "log/slog" + "net/http" + + "github.com/GoogleCloudPlatform/scion/pkg/store" +) + +// handleAdminResetAuthAll handles POST /api/v1/admin/agents/reset-auth-all. +// It lists all running agents and dispatches an auth reset for each one, +// returning a summary of successes and failures. +func (s *Server) handleAdminResetAuthAll(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodPost { + MethodNotAllowed(w) + return + } + + user := GetUserIdentityFromContext(r.Context()) + if user == nil || user.Role() != "admin" { + Forbidden(w) + return + } + + ctx := r.Context() + + if s.dispatcher == nil { + writeError(w, http.StatusInternalServerError, ErrCodeInternalError, + "agent dispatcher not configured", nil) + return + } + + agents, err := s.store.ListAgents(ctx, store.AgentFilter{Phase: "running"}, store.ListOptions{Limit: 1000}) + if err != nil { + slog.Error("Failed to list running agents for bulk reset-auth", "error", err) + writeError(w, http.StatusInternalServerError, ErrCodeInternalError, + "failed to list running agents: "+err.Error(), nil) + return + } + + type agentResult struct { + ID string `json:"id"` + Name string `json:"name"` + Error string `json:"error,omitempty"` + } + + // Dispatch concurrently with a bounded worker pool to avoid timeouts + // when many agents are running across slow or unreachable brokers. + results := make(chan agentResult, len(agents.Items)) + sem := make(chan struct{}, 20) + + for _, agent := range agents.Items { + a := agent + go func() { + sem <- struct{}{} + defer func() { <-sem }() + + res := agentResult{ID: a.ID, Name: a.Name} + if err := s.dispatcher.DispatchAgentResetAuth(ctx, &a); err != nil { + slog.Error("Bulk reset-auth failed for agent", "agent_id", a.ID, "error", err) + res.Error = err.Error() + } + results <- res + }() + } + + var succeeded []agentResult + var failed []agentResult + for range agents.Items { + res := <-results + if res.Error != "" { + failed = append(failed, res) + } else { + succeeded = append(succeeded, res) + } + } + + slog.Info("Bulk reset-auth completed", "succeeded", len(succeeded), "failed", len(failed), "user", user.Email()) + + writeJSON(w, http.StatusOK, map[string]interface{}{ + "succeeded": succeeded, + "failed": failed, + "total": len(agents.Items), + }) +} diff --git a/pkg/hub/agenttoken_test.go b/pkg/hub/agenttoken_test.go index 0f4120d6b..3b4ff54d3 100644 --- a/pkg/hub/agenttoken_test.go +++ b/pkg/hub/agenttoken_test.go @@ -118,7 +118,7 @@ func TestAgentTokenService_AgentCreateAndLifecycleScopes(t *testing.T) { require.NoError(t, err) // Generate a token with agent create and lifecycle scopes - token, err := service.GenerateAgentToken("agent-sub", "project-parent", []AgentTokenScope{ + token, err := service.GenerateAgentToken("agent-sub", tid("project-parent"), []AgentTokenScope{ ScopeAgentStatusUpdate, ScopeAgentCreate, ScopeAgentLifecycle, @@ -130,7 +130,7 @@ func TestAgentTokenService_AgentCreateAndLifecycleScopes(t *testing.T) { claims, err := service.ValidateAgentToken(token) require.NoError(t, err) assert.Equal(t, "agent-sub", claims.Subject) - assert.Equal(t, "project-parent", claims.ProjectID) + assert.Equal(t, tid("project-parent"), claims.ProjectID) assert.True(t, claims.HasScope(ScopeAgentStatusUpdate)) assert.True(t, claims.HasScope(ScopeAgentCreate)) assert.True(t, claims.HasScope(ScopeAgentLifecycle)) @@ -396,7 +396,7 @@ func TestGCPTokenScope_InToken(t *testing.T) { gcpScope := GCPTokenScopeForSA(saID) scopes := []AgentTokenScope{ScopeAgentStatusUpdate, gcpScope} - token, err := service.GenerateAgentToken("agent-1", "project-1", scopes, nil) + token, err := service.GenerateAgentToken(tid("agent-1"), tid("project-1"), scopes, nil) require.NoError(t, err) claims, err := service.ValidateAgentToken(token) diff --git a/pkg/hub/audit_gcp_test.go b/pkg/hub/audit_gcp_test.go index ca0b3dfbb..f33a613cd 100644 --- a/pkg/hub/audit_gcp_test.go +++ b/pkg/hub/audit_gcp_test.go @@ -112,8 +112,8 @@ func TestLogAuditLogger_LogGCPTokenEvent(t *testing.T) { // Should not error for success event err := logger.LogGCPTokenEvent(context.Background(), &GCPTokenEvent{ EventType: GCPTokenEventAccessToken, - AgentID: "agent-1", - ProjectID: "project-1", + AgentID: tid("agent-1"), + ProjectID: tid("project-1"), ServiceAccountEmail: "sa@proj.iam.gserviceaccount.com", Success: true, }) @@ -124,8 +124,8 @@ func TestLogAuditLogger_LogGCPTokenEvent(t *testing.T) { // Should not error for failure event err = logger.LogGCPTokenEvent(context.Background(), &GCPTokenEvent{ EventType: GCPTokenEventIdentityToken, - AgentID: "agent-1", - ProjectID: "project-1", + AgentID: tid("agent-1"), + ProjectID: tid("project-1"), ServiceAccountEmail: "sa@proj.iam.gserviceaccount.com", Success: false, FailReason: "permission denied", diff --git a/pkg/hub/auth.go b/pkg/hub/auth.go index ac506bffd..2011033cc 100644 --- a/pkg/hub/auth.go +++ b/pkg/hub/auth.go @@ -16,10 +16,13 @@ package hub import ( "context" + "errors" "log/slog" "net" "net/http" "strings" + "sync" + "time" "github.com/GoogleCloudPlatform/scion/pkg/apiclient" ) @@ -40,6 +43,15 @@ type AuthConfig struct { UATSvc *UserAccessTokenService // TrustedProxies is a list of trusted proxy IPs/CIDRs TrustedProxies []string + // ProxyAuthenticator is the configured proxy authenticator (for proxy auth mode). + // When set, it replaces the legacy IP-only extractProxyUser path. + ProxyAuthenticator ProxyAuthenticator + // ProxyUserProvisioner is a function that provisions a user from a verified + // proxy identity. It runs provisionUser and returns the stored user. + // Required when ProxyAuthenticator is set. + ProxyUserProvisioner func(ctx context.Context, info *ProxyUserInfo) (UserIdentity, error) + // AuthMode is the exclusive human auth mode: "oauth", "proxy", "dev". + AuthMode string // Debug enables verbose logging Debug bool // Logger is the subsystem logger for auth middleware (defaults to slog.Default()) @@ -139,14 +151,57 @@ func UnifiedAuthMiddleware(cfg AuthConfig) func(http.Handler) http.Handler { // Step 3: Extract bearer token token := extractBearerToken(r) if token == "" { - // Check for trusted proxy headers - if len(trustedNets) > 0 && isTrustedProxy(r, trustedNets) { + // Step 3a: Try proxy authenticator (new verified-assertion path) + if cfg.ProxyAuthenticator != nil { + proxyUser, proxyErr := cfg.ProxyAuthenticator.Authenticate(r) + if proxyErr != nil { + // Assertion present but invalid — reject + if cfg.Debug { + log.Debug("Proxy auth rejected", "provider", cfg.ProxyAuthenticator.Name(), "error", proxyErr) + } + writeError(w, http.StatusUnauthorized, ErrCodeUnauthorized, + "invalid proxy assertion: "+proxyErr.Error(), nil) + return + } + if proxyUser != nil { + // Verified proxy identity — provision the user + identity, err := cfg.ProxyUserProvisioner(ctx, proxyUser) + if err != nil { + if cfg.Debug { + log.Debug("Proxy user provisioning failed", "email", proxyUser.Email, "error", err) + } + if errors.Is(err, ErrAccessDenied) { + writeError(w, http.StatusForbidden, ErrCodeForbidden, + "access denied: email not authorized", nil) + } else if errors.Is(err, ErrUserSuspended) { + writeError(w, http.StatusForbidden, "user_suspended", + "access denied: user account is suspended", nil) + } else { + writeError(w, http.StatusInternalServerError, "internal_error", + "user provisioning failed", nil) + } + return + } + ctx = context.WithValue(ctx, userContextKey{}, identity) + ctx = contextWithIdentity(ctx, identity) + ctx = contextWithAuthType(ctx, AuthTypeProxy) + if cfg.Debug { + log.Debug("Proxy user authenticated", "provider", cfg.ProxyAuthenticator.Name(), "email", proxyUser.Email) + } + next.ServeHTTP(w, r.WithContext(ctx)) + return + } + // (nil, nil) = no assertion present, fall through + } + + // Step 3b: Legacy trusted proxy headers (backward compat when no ProxyAuthenticator) + if cfg.ProxyAuthenticator == nil && len(trustedNets) > 0 && isTrustedProxy(r, trustedNets) { if user := extractProxyUser(r); user != nil { ctx = context.WithValue(ctx, userContextKey{}, user) ctx = contextWithIdentity(ctx, user) ctx = contextWithAuthType(ctx, AuthTypeProxy) if cfg.Debug { - log.Debug("Proxy user authenticated", "email", user.Email()) + log.Debug("Proxy user authenticated (legacy)", "email", user.Email()) } next.ServeHTTP(w, r.WithContext(ctx)) return @@ -286,7 +341,7 @@ func extractBearerToken(r *http.Request) string { // isHealthEndpoint returns true if the path is a health check endpoint. func isHealthEndpoint(path string) bool { - return path == "/healthz" || path == "/readyz" + return path == "/healthz" || path == "/health" || path == "/readyz" } // isUnauthenticatedEndpoint returns true if the path does not require authentication. @@ -446,3 +501,94 @@ func RequireRole(roles ...string) func(http.Handler) http.Handler { }) } } + +// ---- Proxy user resolution cache ---- + +const proxyUserCacheTTL = 60 * time.Second + +// proxyUserCacheEntry holds a cached provisioned user identity. +type proxyUserCacheEntry struct { + identity UserIdentity + expiresAt time.Time +} + +// ProxyUserCache is a short-TTL cache keyed by verified email wrapping the +// provisionUser store lookup. The JWT signature verification still runs every +// request; only the store round-trip is cached. +type ProxyUserCache struct { + mu sync.RWMutex + cache map[string]*proxyUserCacheEntry +} + +// NewProxyUserCache creates a new proxy user resolution cache. +func NewProxyUserCache() *ProxyUserCache { + return &ProxyUserCache{ + cache: make(map[string]*proxyUserCacheEntry), + } +} + +// Get returns a cached user identity if present and not expired. +func (c *ProxyUserCache) Get(email string) (UserIdentity, bool) { + c.mu.RLock() + entry, ok := c.cache[email] + if !ok { + c.mu.RUnlock() + return nil, false + } + if time.Now().After(entry.expiresAt) { + c.mu.RUnlock() + c.mu.Lock() + if entry, ok = c.cache[email]; ok && time.Now().After(entry.expiresAt) { + delete(c.cache, email) + } + c.mu.Unlock() + return nil, false + } + defer c.mu.RUnlock() + return entry.identity, true +} + +// Set stores a user identity in the cache. +func (c *ProxyUserCache) Set(email string, identity UserIdentity) { + c.mu.Lock() + defer c.mu.Unlock() + c.cache[email] = &proxyUserCacheEntry{ + identity: identity, + expiresAt: time.Now().Add(proxyUserCacheTTL), + } +} + +// MakeProxyUserProvisioner creates the ProxyUserProvisioner function that +// wraps provisionUser with a short-TTL cache. It converts the stored user +// to the canonical UserIdentity (real UUID/role from the store). +func MakeProxyUserProvisioner(server *Server) func(ctx context.Context, info *ProxyUserInfo) (UserIdentity, error) { + cache := NewProxyUserCache() + + return func(ctx context.Context, info *ProxyUserInfo) (UserIdentity, error) { + // Check cache first (keyed by verified email) + if identity, ok := cache.Get(info.Email); ok { + return identity, nil + } + + // Provision: authorize + find-or-create + hub membership + user, err := server.provisionUser(ctx, &ExternalUserInfo{ + Email: info.Email, + DisplayName: info.DisplayName, + }) + if err != nil { + return nil, err + } + + // Build canonical identity from stored user + identity := NewAuthenticatedUser( + user.ID, + user.Email, + user.DisplayName, + user.Role, + string(ClientTypeWeb), + ) + + cache.Set(info.Email, identity) + return identity, nil + } +} diff --git a/pkg/hub/authz_integration_test.go b/pkg/hub/authz_integration_test.go index 5ba4e8a5e..ae608796d 100644 --- a/pkg/hub/authz_integration_test.go +++ b/pkg/hub/authz_integration_test.go @@ -38,7 +38,7 @@ func TestEvaluateEndpoint_UserDirectPolicy(t *testing.T) { // Create user require.NoError(t, s.CreateUser(ctx, &store.User{ - ID: "eval-user-1", Email: "eval1@test.com", DisplayName: "Eval User", Role: "member", Status: "active", + ID: tid("eval-user-1"), Email: "eval1@test.com", DisplayName: "Eval User", Role: "member", Status: "active", })) // Create policy via API @@ -58,7 +58,7 @@ func TestEvaluateEndpoint_UserDirectPolicy(t *testing.T) { // Add binding via API bindReq := AddPolicyBindingRequest{ PrincipalType: "user", - PrincipalID: "eval-user-1", + PrincipalID: tid("eval-user-1"), } rec = doRequest(t, srv, http.MethodPost, "/api/v1/policies/"+createdPolicy.ID+"/bindings", bindReq) require.Equal(t, http.StatusCreated, rec.Code, rec.Body.String()) @@ -66,9 +66,9 @@ func TestEvaluateEndpoint_UserDirectPolicy(t *testing.T) { // Evaluate via API evalReq := EvaluateRequest{ PrincipalType: "user", - PrincipalID: "eval-user-1", + PrincipalID: tid("eval-user-1"), ResourceType: "agent", - ResourceID: "agent-1", + ResourceID: tid("agent-1"), Action: "read", } rec = doRequest(t, srv, http.MethodPost, "/api/v1/policies/evaluate", evalReq) @@ -85,12 +85,12 @@ func TestEvaluateEndpoint_DefaultDeny(t *testing.T) { ctx := context.Background() require.NoError(t, s.CreateUser(ctx, &store.User{ - ID: "eval-user-none", Email: "none@test.com", DisplayName: "No Policy", Role: "member", Status: "active", + ID: tid("eval-user-none"), Email: "none@test.com", DisplayName: "No Policy", Role: "member", Status: "active", })) evalReq := EvaluateRequest{ PrincipalType: "user", - PrincipalID: "eval-user-none", + PrincipalID: tid("eval-user-none"), ResourceType: "agent", Action: "delete", } @@ -108,33 +108,33 @@ func TestEvaluateEndpoint_ScopeOverride(t *testing.T) { ctx := context.Background() require.NoError(t, s.CreateUser(ctx, &store.User{ - ID: "eval-user-scope", Email: "scope@test.com", DisplayName: "Scope User", Role: "member", Status: "active", + ID: tid("eval-user-scope"), Email: "scope@test.com", DisplayName: "Scope User", Role: "member", Status: "active", })) // Create hub-level deny hubPolicy := &store.Policy{ - ID: "hub-deny-1", Name: "Hub Deny", ScopeType: "hub", + ID: tid("hub-deny-1"), Name: "Hub Deny", ScopeType: "hub", ResourceType: "agent", Actions: []string{"read"}, Effect: "deny", } require.NoError(t, s.CreatePolicy(ctx, hubPolicy)) require.NoError(t, s.AddPolicyBinding(ctx, &store.PolicyBinding{ - PolicyID: "hub-deny-1", PrincipalType: "user", PrincipalID: "eval-user-scope", + PolicyID: tid("hub-deny-1"), PrincipalType: "user", PrincipalID: tid("eval-user-scope"), })) // Create project-level allow (should override hub deny) projectPolicy := &store.Policy{ - ID: "project-allow-1", Name: "Project Allow", ScopeType: "project", + ID: tid("project-allow-1"), Name: "Project Allow", ScopeType: "project", ScopeID: "project-scope-1", ResourceType: "agent", Actions: []string{"read"}, Effect: "allow", } require.NoError(t, s.CreatePolicy(ctx, projectPolicy)) require.NoError(t, s.AddPolicyBinding(ctx, &store.PolicyBinding{ - PolicyID: "project-allow-1", PrincipalType: "user", PrincipalID: "eval-user-scope", + PolicyID: tid("project-allow-1"), PrincipalType: "user", PrincipalID: tid("eval-user-scope"), })) evalReq := EvaluateRequest{ PrincipalType: "user", - PrincipalID: "eval-user-scope", + PrincipalID: tid("eval-user-scope"), ResourceType: "agent", Action: "read", } @@ -153,26 +153,26 @@ func TestEvaluateEndpoint_AgentPolicy(t *testing.T) { // Create project and agent require.NoError(t, s.CreateProject(ctx, &store.Project{ - ID: "project-eval", Name: "Eval Project", Slug: "project-eval", + ID: tid("project-eval"), Name: "Eval Project", Slug: tid("project-eval"), })) require.NoError(t, s.CreateAgent(ctx, &store.Agent{ - ID: "agent-eval", Slug: "agent-eval", Name: "Eval Agent", - ProjectID: "project-eval", Phase: string(state.PhaseRunning), + ID: tid("agent-eval"), Slug: tid("agent-eval"), Name: "Eval Agent", + ProjectID: tid("project-eval"), Phase: string(state.PhaseRunning), })) // Create and bind policy to agent policy := &store.Policy{ - ID: "agent-policy-eval", Name: "Agent Read", ScopeType: "hub", + ID: tid("agent-policy-eval"), Name: "Agent Read", ScopeType: "hub", ResourceType: "project", Actions: []string{"read"}, Effect: "allow", } require.NoError(t, s.CreatePolicy(ctx, policy)) require.NoError(t, s.AddPolicyBinding(ctx, &store.PolicyBinding{ - PolicyID: "agent-policy-eval", PrincipalType: "agent", PrincipalID: "agent-eval", + PolicyID: tid("agent-policy-eval"), PrincipalType: "agent", PrincipalID: tid("agent-eval"), })) evalReq := EvaluateRequest{ PrincipalType: "agent", - PrincipalID: "agent-eval", + PrincipalID: tid("agent-eval"), ResourceType: "project", Action: "read", } @@ -190,11 +190,11 @@ func TestEvaluateEndpoint_AgentBinding(t *testing.T) { // Create project and agent require.NoError(t, s.CreateProject(ctx, &store.Project{ - ID: "project-bind", Name: "Bind Project", Slug: "project-bind", + ID: tid("project-bind"), Name: "Bind Project", Slug: tid("project-bind"), })) require.NoError(t, s.CreateAgent(ctx, &store.Agent{ - ID: "agent-bind", Slug: "agent-bind", Name: "Bind Agent", - ProjectID: "project-bind", Phase: string(state.PhaseRunning), + ID: tid("agent-bind"), Slug: tid("agent-bind"), Name: "Bind Agent", + ProjectID: tid("project-bind"), Phase: string(state.PhaseRunning), })) // Create policy via API @@ -214,7 +214,7 @@ func TestEvaluateEndpoint_AgentBinding(t *testing.T) { // Bind to agent (tests that "agent" is now a valid principal type) bindReq := AddPolicyBindingRequest{ PrincipalType: "agent", - PrincipalID: "agent-bind", + PrincipalID: tid("agent-bind"), } rec = doRequest(t, srv, http.MethodPost, "/api/v1/policies/"+createdPolicy.ID+"/bindings", bindReq) require.Equal(t, http.StatusCreated, rec.Code, rec.Body.String()) diff --git a/pkg/hub/authz_project_owner_test.go b/pkg/hub/authz_project_owner_test.go index 97a1ab64c..404b3a800 100644 --- a/pkg/hub/authz_project_owner_test.go +++ b/pkg/hub/authz_project_owner_test.go @@ -85,14 +85,14 @@ func TestAuthz_ProjectOwnerBypass_NonCreatorAdminCanDeleteAgent(t *testing.T) { ctx := context.Background() // Bob joins the project as admin (not creator, not direct OwnerID). - bob := makeProjectMemberUser(t, s, project, "user-bob-admin", "Bob Admin", store.GroupMemberRoleAdmin) + bob := makeProjectMemberUser(t, s, project, tid("user-bob-admin"), "Bob Admin", store.GroupMemberRoleAdmin) // Alice creates the agent. require.NoError(t, s.CreateAgent(ctx, &store.Agent{ - ID: "alice-agent-1", Slug: "alice-agent-1", Name: "Alice Agent", + ID: tid("alice-agent-1"), Slug: tid("alice-agent-1"), Name: "Alice Agent", ProjectID: project.ID, OwnerID: alice.ID, Phase: string(state.PhaseRunning), })) - a, err := s.GetAgent(ctx, "alice-agent-1") + a, err := s.GetAgent(ctx, tid("alice-agent-1")) require.NoError(t, err) user := NewAuthenticatedUser(bob.ID, bob.Email, bob.DisplayName, "member", "api") @@ -105,7 +105,7 @@ func TestAuthz_ProjectOwnerBypass_RegularMemberCannotUpdateProject(t *testing.T) srv, s, _, _, project := setupDemoPolicyTest(t) ctx := context.Background() - carol := makeProjectMemberUser(t, s, project, "user-carol-member", "Carol", store.GroupMemberRoleMember) + carol := makeProjectMemberUser(t, s, project, tid("user-carol-member"), "Carol", store.GroupMemberRoleMember) user := NewAuthenticatedUser(carol.ID, carol.Email, carol.DisplayName, "member", "api") decision := srv.authzService.CheckAccess(ctx, user, projectResource(project), ActionUpdate) @@ -116,14 +116,14 @@ func TestAuthz_ProjectOwnerBypass_RegularMemberCannotDeleteOthersAgent(t *testin srv, s, alice, _, project := setupDemoPolicyTest(t) ctx := context.Background() - carol := makeProjectMemberUser(t, s, project, "user-carol-member", "Carol", store.GroupMemberRoleMember) + carol := makeProjectMemberUser(t, s, project, tid("user-carol-member"), "Carol", store.GroupMemberRoleMember) // Alice creates the agent; carol is just a regular member. require.NoError(t, s.CreateAgent(ctx, &store.Agent{ - ID: "alice-agent-2", Slug: "alice-agent-2", Name: "Alice Agent 2", + ID: tid("alice-agent-2"), Slug: tid("alice-agent-2"), Name: "Alice Agent 2", ProjectID: project.ID, OwnerID: alice.ID, Phase: string(state.PhaseRunning), })) - a, err := s.GetAgent(ctx, "alice-agent-2") + a, err := s.GetAgent(ctx, tid("alice-agent-2")) require.NoError(t, err) user := NewAuthenticatedUser(carol.ID, carol.Email, carol.DisplayName, "member", "api") @@ -146,7 +146,7 @@ func TestAuthz_ProjectOwnerBypass_AppliesToProjectMembersGroup(t *testing.T) { srv, s, _, _, project := setupDemoPolicyTest(t) ctx := context.Background() - bob := makeProjectMemberUser(t, s, project, "user-bob-owner", "Bob Owner", store.GroupMemberRoleOwner) + bob := makeProjectMemberUser(t, s, project, tid("user-bob-owner"), "Bob Owner", store.GroupMemberRoleOwner) membersGroup, err := s.GetGroupBySlug(ctx, "project:"+project.Slug+":members") require.NoError(t, err) @@ -165,7 +165,7 @@ func TestCapabilities_ProjectOwnerBypass_ProjectAllActions(t *testing.T) { srv, s, _, _, project := setupDemoPolicyTest(t) ctx := context.Background() - bob := makeProjectMemberUser(t, s, project, "user-bob-cap", "Bob", store.GroupMemberRoleOwner) + bob := makeProjectMemberUser(t, s, project, tid("user-bob-cap"), "Bob", store.GroupMemberRoleOwner) user := NewAuthenticatedUser(bob.ID, bob.Email, bob.DisplayName, "member", "api") caps := srv.authzService.ComputeCapabilities(ctx, user, projectResource(project)) @@ -179,13 +179,13 @@ func TestCapabilities_ProjectOwnerBypass_AgentAllActions(t *testing.T) { srv, s, alice, _, project := setupDemoPolicyTest(t) ctx := context.Background() - bob := makeProjectMemberUser(t, s, project, "user-bob-cap-a", "Bob", store.GroupMemberRoleOwner) + bob := makeProjectMemberUser(t, s, project, tid("user-bob-cap-a"), "Bob", store.GroupMemberRoleOwner) require.NoError(t, s.CreateAgent(ctx, &store.Agent{ - ID: "alice-agent-cap", Slug: "alice-agent-cap", Name: "Alice Agent Cap", + ID: tid("alice-agent-cap"), Slug: tid("alice-agent-cap"), Name: "Alice Agent Cap", ProjectID: project.ID, OwnerID: alice.ID, Phase: string(state.PhaseRunning), })) - a, err := s.GetAgent(ctx, "alice-agent-cap") + a, err := s.GetAgent(ctx, tid("alice-agent-cap")) require.NoError(t, err) user := NewAuthenticatedUser(bob.ID, bob.Email, bob.DisplayName, "member", "api") @@ -200,21 +200,21 @@ func TestCapabilities_ProjectOwnerBypass_BatchAllActions(t *testing.T) { srv, s, alice, _, project := setupDemoPolicyTest(t) ctx := context.Background() - bob := makeProjectMemberUser(t, s, project, "user-bob-batch", "Bob", store.GroupMemberRoleOwner) + bob := makeProjectMemberUser(t, s, project, tid("user-bob-batch"), "Bob", store.GroupMemberRoleOwner) // Two agents: one owned by alice, one by bob. require.NoError(t, s.CreateAgent(ctx, &store.Agent{ - ID: "agent-alice-b", Slug: "agent-alice-b", Name: "AliceB", + ID: tid("agent-alice-b"), Slug: tid("agent-alice-b"), Name: "AliceB", ProjectID: project.ID, OwnerID: alice.ID, Phase: string(state.PhaseRunning), })) require.NoError(t, s.CreateAgent(ctx, &store.Agent{ - ID: "agent-bob-b", Slug: "agent-bob-b", Name: "BobB", + ID: tid("agent-bob-b"), Slug: tid("agent-bob-b"), Name: "BobB", ProjectID: project.ID, OwnerID: bob.ID, Phase: string(state.PhaseRunning), })) - a1, err := s.GetAgent(ctx, "agent-alice-b") + a1, err := s.GetAgent(ctx, tid("agent-alice-b")) require.NoError(t, err) - a2, err := s.GetAgent(ctx, "agent-bob-b") + a2, err := s.GetAgent(ctx, tid("agent-bob-b")) require.NoError(t, err) user := NewAuthenticatedUser(bob.ID, bob.Email, bob.DisplayName, "member", "api") @@ -233,7 +233,7 @@ func TestCapabilities_ProjectOwnerBypass_ScopeAllActions(t *testing.T) { srv, s, _, _, project := setupDemoPolicyTest(t) ctx := context.Background() - bob := makeProjectMemberUser(t, s, project, "user-bob-scope", "Bob", store.GroupMemberRoleOwner) + bob := makeProjectMemberUser(t, s, project, tid("user-bob-scope"), "Bob", store.GroupMemberRoleOwner) user := NewAuthenticatedUser(bob.ID, bob.Email, bob.DisplayName, "member", "api") caps := srv.authzService.ComputeScopeCapabilities(ctx, user, "project", project.ID, "agent") @@ -247,13 +247,13 @@ func TestCapabilities_RegularMember_AgentLimitedActions(t *testing.T) { srv, s, alice, _, project := setupDemoPolicyTest(t) ctx := context.Background() - carol := makeProjectMemberUser(t, s, project, "user-carol-cap", "Carol", store.GroupMemberRoleMember) + carol := makeProjectMemberUser(t, s, project, tid("user-carol-cap"), "Carol", store.GroupMemberRoleMember) require.NoError(t, s.CreateAgent(ctx, &store.Agent{ - ID: "alice-agent-cap2", Slug: "alice-agent-cap2", Name: "Alice Agent Cap2", + ID: tid("alice-agent-cap2"), Slug: tid("alice-agent-cap2"), Name: "Alice Agent Cap2", ProjectID: project.ID, OwnerID: alice.ID, Phase: string(state.PhaseRunning), })) - a, err := s.GetAgent(ctx, "alice-agent-cap2") + a, err := s.GetAgent(ctx, tid("alice-agent-cap2")) require.NoError(t, err) user := NewAuthenticatedUser(carol.ID, carol.Email, carol.DisplayName, "member", "api") @@ -270,7 +270,7 @@ func TestCapabilities_RegularMember_AgentLimitedActions(t *testing.T) { func TestUpdateProject_NonCreatorOwnerAllowed(t *testing.T) { srv, s, _, _, project := setupDemoPolicyTest(t) - bob := makeProjectMemberUser(t, s, project, "user-bob-http-owner", "Bob HTTP", store.GroupMemberRoleOwner) + bob := makeProjectMemberUser(t, s, project, tid("user-bob-http-owner"), "Bob HTTP", store.GroupMemberRoleOwner) body := map[string]string{"description": "updated by bob"} rec := doRequestAsUser(t, srv, bob, http.MethodPatch, "/api/v1/projects/"+project.ID, body) @@ -280,7 +280,7 @@ func TestUpdateProject_NonCreatorOwnerAllowed(t *testing.T) { func TestUpdateProject_RegularMemberDenied(t *testing.T) { srv, s, _, _, project := setupDemoPolicyTest(t) - carol := makeProjectMemberUser(t, s, project, "user-carol-http", "Carol HTTP", store.GroupMemberRoleMember) + carol := makeProjectMemberUser(t, s, project, tid("user-carol-http"), "Carol HTTP", store.GroupMemberRoleMember) body := map[string]string{"description": "updated by carol"} rec := doRequestAsUser(t, srv, carol, http.MethodPatch, "/api/v1/projects/"+project.ID, body) diff --git a/pkg/hub/authz_test.go b/pkg/hub/authz_test.go index 4d05672ed..2831bf221 100644 --- a/pkg/hub/authz_test.go +++ b/pkg/hub/authz_test.go @@ -53,11 +53,11 @@ func TestAuthz_OwnerBypass(t *testing.T) { // Create a user require.NoError(t, s.CreateUser(ctx, &store.User{ - ID: "user-owner", Email: "owner@test.com", DisplayName: "Owner", Role: "member", Status: "active", + ID: tid("user-owner"), Email: "owner@test.com", DisplayName: "Owner", Role: "member", Status: "active", })) - user := NewAuthenticatedUser("user-owner", "owner@test.com", "Owner", "member", "api") - resource := Resource{Type: "agent", ID: "agent-1", OwnerID: "user-owner"} + user := NewAuthenticatedUser(tid("user-owner"), "owner@test.com", "Owner", "member", "api") + resource := Resource{Type: "agent", ID: tid("agent-1"), OwnerID: tid("user-owner")} decision := authz.CheckAccess(ctx, user, resource, ActionDelete) assert.True(t, decision.Allowed) @@ -70,27 +70,27 @@ func TestAuthz_DirectUserPolicy(t *testing.T) { // Create user require.NoError(t, s.CreateUser(ctx, &store.User{ - ID: "user-1", Email: "user1@test.com", DisplayName: "User 1", Role: "member", Status: "active", + ID: tid("user-1"), Email: "user1@test.com", DisplayName: "User 1", Role: "member", Status: "active", })) // Create policy allowing read policy := &store.Policy{ - ID: "policy-1", Name: "Allow Read", ScopeType: "hub", + ID: tid("policy-1"), Name: "Allow Read", ScopeType: "hub", ResourceType: "agent", Actions: []string{"read"}, Effect: "allow", } require.NoError(t, s.CreatePolicy(ctx, policy)) // Bind to user require.NoError(t, s.AddPolicyBinding(ctx, &store.PolicyBinding{ - PolicyID: "policy-1", PrincipalType: "user", PrincipalID: "user-1", + PolicyID: tid("policy-1"), PrincipalType: "user", PrincipalID: tid("user-1"), })) - user := NewAuthenticatedUser("user-1", "user1@test.com", "User 1", "member", "api") - resource := Resource{Type: "agent", ID: "agent-1"} + user := NewAuthenticatedUser(tid("user-1"), "user1@test.com", "User 1", "member", "api") + resource := Resource{Type: "agent", ID: tid("agent-1")} decision := authz.CheckAccess(ctx, user, resource, ActionRead) assert.True(t, decision.Allowed) - assert.Equal(t, "policy-1", decision.PolicyID) + assert.Equal(t, tid("policy-1"), decision.PolicyID) } func TestAuthz_DefaultDeny(t *testing.T) { @@ -98,11 +98,11 @@ func TestAuthz_DefaultDeny(t *testing.T) { ctx := context.Background() require.NoError(t, s.CreateUser(ctx, &store.User{ - ID: "user-nodeny", Email: "nodeny@test.com", DisplayName: "NoDeny", Role: "member", Status: "active", + ID: tid("user-nodeny"), Email: "nodeny@test.com", DisplayName: "NoDeny", Role: "member", Status: "active", })) - user := NewAuthenticatedUser("user-nodeny", "nodeny@test.com", "NoDeny", "member", "api") - resource := Resource{Type: "agent", ID: "agent-1"} + user := NewAuthenticatedUser(tid("user-nodeny"), "nodeny@test.com", "NoDeny", "member", "api") + resource := Resource{Type: "agent", ID: tid("agent-1")} decision := authz.CheckAccess(ctx, user, resource, ActionDelete) assert.False(t, decision.Allowed) @@ -114,24 +114,24 @@ func TestAuthz_DenyEffect(t *testing.T) { ctx := context.Background() require.NoError(t, s.CreateUser(ctx, &store.User{ - ID: "user-deny", Email: "deny@test.com", DisplayName: "Deny", Role: "member", Status: "active", + ID: tid("user-deny"), Email: "deny@test.com", DisplayName: "Deny", Role: "member", Status: "active", })) policy := &store.Policy{ - ID: "policy-deny", Name: "Deny Write", ScopeType: "hub", + ID: tid("policy-deny"), Name: "Deny Write", ScopeType: "hub", ResourceType: "agent", Actions: []string{"update"}, Effect: "deny", } require.NoError(t, s.CreatePolicy(ctx, policy)) require.NoError(t, s.AddPolicyBinding(ctx, &store.PolicyBinding{ - PolicyID: "policy-deny", PrincipalType: "user", PrincipalID: "user-deny", + PolicyID: tid("policy-deny"), PrincipalType: "user", PrincipalID: tid("user-deny"), })) - user := NewAuthenticatedUser("user-deny", "deny@test.com", "Deny", "member", "api") - resource := Resource{Type: "agent", ID: "agent-1"} + user := NewAuthenticatedUser(tid("user-deny"), "deny@test.com", "Deny", "member", "api") + resource := Resource{Type: "agent", ID: tid("agent-1")} decision := authz.CheckAccess(ctx, user, resource, ActionUpdate) assert.False(t, decision.Allowed) - assert.Equal(t, "policy-deny", decision.PolicyID) + assert.Equal(t, tid("policy-deny"), decision.PolicyID) } func TestAuthz_WildcardAction(t *testing.T) { @@ -139,19 +139,19 @@ func TestAuthz_WildcardAction(t *testing.T) { ctx := context.Background() require.NoError(t, s.CreateUser(ctx, &store.User{ - ID: "user-wc", Email: "wc@test.com", DisplayName: "WC", Role: "member", Status: "active", + ID: tid("user-wc"), Email: "wc@test.com", DisplayName: "WC", Role: "member", Status: "active", })) policy := &store.Policy{ - ID: "policy-wc", Name: "Allow All", ScopeType: "hub", + ID: tid("policy-wc"), Name: "Allow All", ScopeType: "hub", ResourceType: "*", Actions: []string{"*"}, Effect: "allow", } require.NoError(t, s.CreatePolicy(ctx, policy)) require.NoError(t, s.AddPolicyBinding(ctx, &store.PolicyBinding{ - PolicyID: "policy-wc", PrincipalType: "user", PrincipalID: "user-wc", + PolicyID: tid("policy-wc"), PrincipalType: "user", PrincipalID: tid("user-wc"), })) - user := NewAuthenticatedUser("user-wc", "wc@test.com", "WC", "member", "api") + user := NewAuthenticatedUser(tid("user-wc"), "wc@test.com", "WC", "member", "api") // Test with different actions and resource types for _, action := range []Action{ActionRead, ActionUpdate, ActionDelete, ActionManage} { @@ -165,37 +165,37 @@ func TestAuthz_ScopeOverride(t *testing.T) { ctx := context.Background() require.NoError(t, s.CreateUser(ctx, &store.User{ - ID: "user-scope", Email: "scope@test.com", DisplayName: "Scope", Role: "member", Status: "active", + ID: tid("user-scope"), Email: "scope@test.com", DisplayName: "Scope", Role: "member", Status: "active", })) // Hub-level deny hubPolicy := &store.Policy{ - ID: "policy-hub-deny", Name: "Hub Deny", ScopeType: "hub", + ID: tid("policy-hub-deny"), Name: "Hub Deny", ScopeType: "hub", ResourceType: "agent", Actions: []string{"read"}, Effect: "deny", Priority: 0, } require.NoError(t, s.CreatePolicy(ctx, hubPolicy)) require.NoError(t, s.AddPolicyBinding(ctx, &store.PolicyBinding{ - PolicyID: "policy-hub-deny", PrincipalType: "user", PrincipalID: "user-scope", + PolicyID: tid("policy-hub-deny"), PrincipalType: "user", PrincipalID: tid("user-scope"), })) // Project-level allow (more specific scope overrides) projectPolicy := &store.Policy{ - ID: "policy-project-allow", Name: "Project Allow", ScopeType: "project", - ScopeID: "project-1", + ID: tid("policy-project-allow"), Name: "Project Allow", ScopeType: "project", + ScopeID: tid("project-1"), ResourceType: "agent", Actions: []string{"read"}, Effect: "allow", Priority: 0, } require.NoError(t, s.CreatePolicy(ctx, projectPolicy)) require.NoError(t, s.AddPolicyBinding(ctx, &store.PolicyBinding{ - PolicyID: "policy-project-allow", PrincipalType: "user", PrincipalID: "user-scope", + PolicyID: tid("policy-project-allow"), PrincipalType: "user", PrincipalID: tid("user-scope"), })) - user := NewAuthenticatedUser("user-scope", "scope@test.com", "Scope", "member", "api") - resource := Resource{Type: "agent", ID: "agent-1", ParentType: "project", ParentID: "project-1"} + user := NewAuthenticatedUser(tid("user-scope"), "scope@test.com", "Scope", "member", "api") + resource := Resource{Type: "agent", ID: tid("agent-1"), ParentType: "project", ParentID: tid("project-1")} decision := authz.CheckAccess(ctx, user, resource, ActionRead) assert.True(t, decision.Allowed) assert.Equal(t, "project", decision.Scope) - assert.Equal(t, "policy-project-allow", decision.PolicyID) + assert.Equal(t, tid("policy-project-allow"), decision.PolicyID) } func TestAuthz_PriorityWithinScope(t *testing.T) { @@ -203,34 +203,34 @@ func TestAuthz_PriorityWithinScope(t *testing.T) { ctx := context.Background() require.NoError(t, s.CreateUser(ctx, &store.User{ - ID: "user-prio", Email: "prio@test.com", DisplayName: "Prio", Role: "member", Status: "active", + ID: tid("user-prio"), Email: "prio@test.com", DisplayName: "Prio", Role: "member", Status: "active", })) // Low priority allow p1 := &store.Policy{ - ID: "policy-low", Name: "Low Priority Allow", ScopeType: "hub", + ID: tid("policy-low"), Name: "Low Priority Allow", ScopeType: "hub", ResourceType: "agent", Actions: []string{"read"}, Effect: "allow", Priority: 0, } // High priority deny (should override) p2 := &store.Policy{ - ID: "policy-high", Name: "High Priority Deny", ScopeType: "hub", + ID: tid("policy-high"), Name: "High Priority Deny", ScopeType: "hub", ResourceType: "agent", Actions: []string{"read"}, Effect: "deny", Priority: 10, } require.NoError(t, s.CreatePolicy(ctx, p1)) require.NoError(t, s.CreatePolicy(ctx, p2)) require.NoError(t, s.AddPolicyBinding(ctx, &store.PolicyBinding{ - PolicyID: "policy-low", PrincipalType: "user", PrincipalID: "user-prio", + PolicyID: tid("policy-low"), PrincipalType: "user", PrincipalID: tid("user-prio"), })) require.NoError(t, s.AddPolicyBinding(ctx, &store.PolicyBinding{ - PolicyID: "policy-high", PrincipalType: "user", PrincipalID: "user-prio", + PolicyID: tid("policy-high"), PrincipalType: "user", PrincipalID: tid("user-prio"), })) - user := NewAuthenticatedUser("user-prio", "prio@test.com", "Prio", "member", "api") - resource := Resource{Type: "agent", ID: "agent-1"} + user := NewAuthenticatedUser(tid("user-prio"), "prio@test.com", "Prio", "member", "api") + resource := Resource{Type: "agent", ID: tid("agent-1")} decision := authz.CheckAccess(ctx, user, resource, ActionRead) assert.False(t, decision.Allowed) - assert.Equal(t, "policy-high", decision.PolicyID) + assert.Equal(t, tid("policy-high"), decision.PolicyID) } func TestAuthz_ConditionLabels(t *testing.T) { @@ -238,11 +238,11 @@ func TestAuthz_ConditionLabels(t *testing.T) { ctx := context.Background() require.NoError(t, s.CreateUser(ctx, &store.User{ - ID: "user-labels", Email: "labels@test.com", DisplayName: "Labels", Role: "member", Status: "active", + ID: tid("user-labels"), Email: "labels@test.com", DisplayName: "Labels", Role: "member", Status: "active", })) policy := &store.Policy{ - ID: "policy-labels", Name: "Label Condition", ScopeType: "hub", + ID: tid("policy-labels"), Name: "Label Condition", ScopeType: "hub", ResourceType: "agent", Actions: []string{"read"}, Effect: "allow", Conditions: &store.PolicyConditions{ Labels: map[string]string{"env": "production", "team": "backend"}, @@ -250,15 +250,15 @@ func TestAuthz_ConditionLabels(t *testing.T) { } require.NoError(t, s.CreatePolicy(ctx, policy)) require.NoError(t, s.AddPolicyBinding(ctx, &store.PolicyBinding{ - PolicyID: "policy-labels", PrincipalType: "user", PrincipalID: "user-labels", + PolicyID: tid("policy-labels"), PrincipalType: "user", PrincipalID: tid("user-labels"), })) - user := NewAuthenticatedUser("user-labels", "labels@test.com", "Labels", "member", "api") + user := NewAuthenticatedUser(tid("user-labels"), "labels@test.com", "Labels", "member", "api") // Matching labels resourceMatch := Resource{ Type: "agent", - ID: "agent-1", + ID: tid("agent-1"), Labels: map[string]string{"env": "production", "team": "backend"}, } decision := authz.CheckAccess(ctx, user, resourceMatch, ActionRead) @@ -267,7 +267,7 @@ func TestAuthz_ConditionLabels(t *testing.T) { // Non-matching labels resourceNoMatch := Resource{ Type: "agent", - ID: "agent-2", + ID: tid("agent-2"), Labels: map[string]string{"env": "staging"}, } decision = authz.CheckAccess(ctx, user, resourceNoMatch, ActionRead) @@ -280,12 +280,12 @@ func TestAuthz_TimeConditions(t *testing.T) { ctx := context.Background() require.NoError(t, s.CreateUser(ctx, &store.User{ - ID: "user-time", Email: "time@test.com", DisplayName: "Time", Role: "member", Status: "active", + ID: tid("user-time"), Email: "time@test.com", DisplayName: "Time", Role: "member", Status: "active", })) past := time.Now().Add(-time.Hour) policy := &store.Policy{ - ID: "policy-expired", Name: "Expired Policy", ScopeType: "hub", + ID: tid("policy-expired"), Name: "Expired Policy", ScopeType: "hub", ResourceType: "agent", Actions: []string{"read"}, Effect: "allow", Conditions: &store.PolicyConditions{ ValidUntil: &past, @@ -293,11 +293,11 @@ func TestAuthz_TimeConditions(t *testing.T) { } require.NoError(t, s.CreatePolicy(ctx, policy)) require.NoError(t, s.AddPolicyBinding(ctx, &store.PolicyBinding{ - PolicyID: "policy-expired", PrincipalType: "user", PrincipalID: "user-time", + PolicyID: tid("policy-expired"), PrincipalType: "user", PrincipalID: tid("user-time"), })) - user := NewAuthenticatedUser("user-time", "time@test.com", "Time", "member", "api") - resource := Resource{Type: "agent", ID: "agent-1"} + user := NewAuthenticatedUser(tid("user-time"), "time@test.com", "Time", "member", "api") + resource := Resource{Type: "agent", ID: tid("agent-1")} decision := authz.CheckAccess(ctx, user, resource, ActionRead) assert.False(t, decision.Allowed) @@ -310,29 +310,29 @@ func TestAuthz_AgentDirectPolicy(t *testing.T) { // Create project and agent require.NoError(t, s.CreateProject(ctx, &store.Project{ - ID: "project-agent-1", Name: "Test Project", Slug: "test-project-agent-1", + ID: tid("project-agent-1"), Name: "Test Project", Slug: "test-project-agent-1", })) require.NoError(t, s.CreateAgent(ctx, &store.Agent{ - ID: "agent-direct", Slug: "agent-direct", Name: "Agent Direct", - ProjectID: "project-agent-1", Phase: string(state.PhaseRunning), + ID: tid("agent-direct"), Slug: tid("agent-direct"), Name: "Agent Direct", + ProjectID: tid("project-agent-1"), Phase: string(state.PhaseRunning), })) // Create and bind policy to agent policy := &store.Policy{ - ID: "policy-agent", Name: "Agent Allow", ScopeType: "hub", + ID: tid("policy-agent"), Name: "Agent Allow", ScopeType: "hub", ResourceType: "project", Actions: []string{"read"}, Effect: "allow", } require.NoError(t, s.CreatePolicy(ctx, policy)) require.NoError(t, s.AddPolicyBinding(ctx, &store.PolicyBinding{ - PolicyID: "policy-agent", PrincipalType: "agent", PrincipalID: "agent-direct", + PolicyID: tid("policy-agent"), PrincipalType: "agent", PrincipalID: tid("agent-direct"), })) - agent := &evaluateAgentIdentity{id: "agent-direct", projectID: "project-agent-1"} - resource := Resource{Type: "project", ID: "project-agent-1"} + agent := &evaluateAgentIdentity{id: tid("agent-direct"), projectID: tid("project-agent-1")} + resource := Resource{Type: "project", ID: tid("project-agent-1")} decision := authz.CheckAccess(ctx, agent, resource, ActionRead) assert.True(t, decision.Allowed) - assert.Equal(t, "policy-agent", decision.PolicyID) + assert.Equal(t, tid("policy-agent"), decision.PolicyID) } func TestAuthz_ActionMismatch(t *testing.T) { @@ -340,20 +340,20 @@ func TestAuthz_ActionMismatch(t *testing.T) { ctx := context.Background() require.NoError(t, s.CreateUser(ctx, &store.User{ - ID: "user-act", Email: "act@test.com", DisplayName: "Act", Role: "member", Status: "active", + ID: tid("user-act"), Email: "act@test.com", DisplayName: "Act", Role: "member", Status: "active", })) policy := &store.Policy{ - ID: "policy-read-only", Name: "Read Only", ScopeType: "hub", + ID: tid("policy-read-only"), Name: "Read Only", ScopeType: "hub", ResourceType: "agent", Actions: []string{"read"}, Effect: "allow", } require.NoError(t, s.CreatePolicy(ctx, policy)) require.NoError(t, s.AddPolicyBinding(ctx, &store.PolicyBinding{ - PolicyID: "policy-read-only", PrincipalType: "user", PrincipalID: "user-act", + PolicyID: tid("policy-read-only"), PrincipalType: "user", PrincipalID: tid("user-act"), })) - user := NewAuthenticatedUser("user-act", "act@test.com", "Act", "member", "api") - resource := Resource{Type: "agent", ID: "agent-1"} + user := NewAuthenticatedUser(tid("user-act"), "act@test.com", "Act", "member", "api") + resource := Resource{Type: "agent", ID: tid("agent-1")} // Read should succeed decision := authz.CheckAccess(ctx, user, resource, ActionRead) @@ -369,19 +369,19 @@ func TestAuthz_ResourceTypeMismatch(t *testing.T) { ctx := context.Background() require.NoError(t, s.CreateUser(ctx, &store.User{ - ID: "user-rt", Email: "rt@test.com", DisplayName: "RT", Role: "member", Status: "active", + ID: tid("user-rt"), Email: "rt@test.com", DisplayName: "RT", Role: "member", Status: "active", })) policy := &store.Policy{ - ID: "policy-agent-only", Name: "Agent Only", ScopeType: "hub", + ID: tid("policy-agent-only"), Name: "Agent Only", ScopeType: "hub", ResourceType: "agent", Actions: []string{"read"}, Effect: "allow", } require.NoError(t, s.CreatePolicy(ctx, policy)) require.NoError(t, s.AddPolicyBinding(ctx, &store.PolicyBinding{ - PolicyID: "policy-agent-only", PrincipalType: "user", PrincipalID: "user-rt", + PolicyID: tid("policy-agent-only"), PrincipalType: "user", PrincipalID: tid("user-rt"), })) - user := NewAuthenticatedUser("user-rt", "rt@test.com", "RT", "member", "api") + user := NewAuthenticatedUser(tid("user-rt"), "rt@test.com", "RT", "member", "api") // Agent resource should match decision := authz.CheckAccess(ctx, user, Resource{Type: "agent", ID: "a1"}, ActionRead) @@ -460,14 +460,14 @@ func TestMatchesResource(t *testing.T) { }, { "project scope matching", - store.Policy{ResourceType: "agent", ScopeType: "project", ScopeID: "project-1"}, - Resource{Type: "agent", ParentType: "project", ParentID: "project-1"}, + store.Policy{ResourceType: "agent", ScopeType: "project", ScopeID: tid("project-1")}, + Resource{Type: "agent", ParentType: "project", ParentID: tid("project-1")}, true, }, { "project scope mismatch", - store.Policy{ResourceType: "agent", ScopeType: "project", ScopeID: "project-1"}, - Resource{Type: "agent", ParentType: "project", ParentID: "project-2"}, + store.Policy{ResourceType: "agent", ScopeType: "project", ScopeID: tid("project-1")}, + Resource{Type: "agent", ParentType: "project", ParentID: tid("project-2")}, false, }, } @@ -491,11 +491,11 @@ func TestAuthz_BrokerDispatch_OwnerAllowed(t *testing.T) { ctx := context.Background() require.NoError(t, s.CreateUser(ctx, &store.User{ - ID: "broker-owner", Email: "owner@test.com", DisplayName: "Owner", Role: "member", Status: "active", + ID: tid("broker-owner"), Email: "owner@test.com", DisplayName: "Owner", Role: "member", Status: "active", })) - user := NewAuthenticatedUser("broker-owner", "owner@test.com", "Owner", "member", "api") - resource := Resource{Type: "broker", ID: "broker-1", OwnerID: "broker-owner"} + user := NewAuthenticatedUser(tid("broker-owner"), "owner@test.com", "Owner", "member", "api") + resource := Resource{Type: "broker", ID: tid("broker-1"), OwnerID: tid("broker-owner")} decision := authz.CheckAccess(ctx, user, resource, ActionDispatch) assert.True(t, decision.Allowed) @@ -507,11 +507,11 @@ func TestAuthz_BrokerDispatch_NonOwnerDenied(t *testing.T) { ctx := context.Background() require.NoError(t, s.CreateUser(ctx, &store.User{ - ID: "other-user", Email: "other@test.com", DisplayName: "Other", Role: "member", Status: "active", + ID: tid("other-user"), Email: "other@test.com", DisplayName: "Other", Role: "member", Status: "active", })) - user := NewAuthenticatedUser("other-user", "other@test.com", "Other", "member", "api") - resource := Resource{Type: "broker", ID: "broker-1", OwnerID: "broker-owner-id"} + user := NewAuthenticatedUser(tid("other-user"), "other@test.com", "Other", "member", "api") + resource := Resource{Type: "broker", ID: tid("broker-1"), OwnerID: tid("broker-owner-id")} decision := authz.CheckAccess(ctx, user, resource, ActionDispatch) assert.False(t, decision.Allowed) @@ -523,7 +523,7 @@ func TestAuthz_BrokerDispatch_AdminAllowed(t *testing.T) { ctx := context.Background() admin := NewAuthenticatedUser("admin-1", "admin@example.com", "Admin", "admin", "api") - resource := Resource{Type: "broker", ID: "broker-1", OwnerID: "someone-else"} + resource := Resource{Type: "broker", ID: tid("broker-1"), OwnerID: "someone-else"} decision := authz.CheckAccess(ctx, admin, resource, ActionDispatch) assert.True(t, decision.Allowed) @@ -535,11 +535,11 @@ func TestAuthz_BrokerCapabilities_Owner(t *testing.T) { ctx := context.Background() require.NoError(t, s.CreateUser(ctx, &store.User{ - ID: "cap-owner", Email: "cap-owner@test.com", DisplayName: "Cap Owner", Role: "member", Status: "active", + ID: tid("cap-owner"), Email: "cap-owner@test.com", DisplayName: "Cap Owner", Role: "member", Status: "active", })) - user := NewAuthenticatedUser("cap-owner", "cap-owner@test.com", "Cap Owner", "member", "api") - resource := Resource{Type: "broker", ID: "broker-cap", OwnerID: "cap-owner"} + user := NewAuthenticatedUser(tid("cap-owner"), "cap-owner@test.com", "Cap Owner", "member", "api") + resource := Resource{Type: "broker", ID: tid("broker-cap"), OwnerID: tid("cap-owner")} caps := authz.ComputeCapabilities(ctx, user, resource) assert.Contains(t, caps.Actions, "dispatch") @@ -553,11 +553,11 @@ func TestAuthz_BrokerCapabilities_NonOwner(t *testing.T) { ctx := context.Background() require.NoError(t, s.CreateUser(ctx, &store.User{ - ID: "cap-nonowner", Email: "nonowner@test.com", DisplayName: "Non Owner", Role: "member", Status: "active", + ID: tid("cap-nonowner"), Email: "nonowner@test.com", DisplayName: "Non Owner", Role: "member", Status: "active", })) - user := NewAuthenticatedUser("cap-nonowner", "nonowner@test.com", "Non Owner", "member", "api") - resource := Resource{Type: "broker", ID: "broker-cap", OwnerID: "someone-else"} + user := NewAuthenticatedUser(tid("cap-nonowner"), "nonowner@test.com", "Non Owner", "member", "api") + resource := Resource{Type: "broker", ID: tid("broker-cap"), OwnerID: "someone-else"} caps := authz.ComputeCapabilities(ctx, user, resource) assert.NotContains(t, caps.Actions, "dispatch") @@ -566,14 +566,14 @@ func TestAuthz_BrokerCapabilities_NonOwner(t *testing.T) { func TestBrokerResource_Helper(t *testing.T) { broker := &store.RuntimeBroker{ - ID: "broker-helper-test", - CreatedBy: "user-123", + ID: tid("broker-helper-test"), + CreatedBy: tid("user-123"), } r := brokerResource(broker) assert.Equal(t, "broker", r.Type) - assert.Equal(t, "broker-helper-test", r.ID) - assert.Equal(t, "user-123", r.OwnerID) + assert.Equal(t, tid("broker-helper-test"), r.ID) + assert.Equal(t, tid("user-123"), r.OwnerID) } // ============================================================================= @@ -587,13 +587,13 @@ func TestCanAccessAsAncestor(t *testing.T) { ancestry []string expected bool }{ - {"root ancestor", "user-1", []string{"user-1"}, true}, - {"intermediate ancestor", "agent-A", []string{"user-1", "agent-A"}, true}, - {"not in ancestry", "user-2", []string{"user-1", "agent-A"}, false}, - {"empty ancestry", "user-1", nil, false}, - {"deep chain", "user-1", []string{"user-1", "agent-A", "agent-B"}, true}, - {"deep chain middle", "agent-A", []string{"user-1", "agent-A", "agent-B"}, true}, - {"deep chain last", "agent-B", []string{"user-1", "agent-A", "agent-B"}, true}, + {"root ancestor", tid("user-1"), []string{tid("user-1")}, true}, + {"intermediate ancestor", tid("agent-A"), []string{tid("user-1"), tid("agent-A")}, true}, + {"not in ancestry", tid("user-2"), []string{tid("user-1"), tid("agent-A")}, false}, + {"empty ancestry", tid("user-1"), nil, false}, + {"deep chain", tid("user-1"), []string{tid("user-1"), tid("agent-A"), tid("agent-B")}, true}, + {"deep chain middle", tid("agent-A"), []string{tid("user-1"), tid("agent-A"), tid("agent-B")}, true}, + {"deep chain last", tid("agent-B"), []string{tid("user-1"), tid("agent-A"), tid("agent-B")}, true}, } for _, tt := range tests { @@ -610,17 +610,17 @@ func TestAuthz_AncestryAccess_UserToAgent(t *testing.T) { // Create user (non-admin, non-owner — ancestry is the only access path) require.NoError(t, s.CreateUser(ctx, &store.User{ - ID: "user-ancestor", Email: "ancestor@test.com", DisplayName: "Ancestor", Role: "member", Status: "active", + ID: tid("user-ancestor"), Email: "ancestor@test.com", DisplayName: "Ancestor", Role: "member", Status: "active", })) - user := NewAuthenticatedUser("user-ancestor", "ancestor@test.com", "Ancestor", "member", "api") + user := NewAuthenticatedUser(tid("user-ancestor"), "ancestor@test.com", "Ancestor", "member", "api") // Resource with user in ancestry but different owner resource := Resource{ Type: "agent", - ID: "agent-grandchild", + ID: tid("agent-grandchild"), OwnerID: "someone-else", - Ancestry: []string{"user-ancestor", "agent-child"}, + Ancestry: []string{tid("user-ancestor"), tid("agent-child")}, } decision := authz.CheckAccess(ctx, user, resource, ActionRead) @@ -634,20 +634,20 @@ func TestAuthz_AncestryAccess_AgentToDescendant(t *testing.T) { // Create project and parent agent require.NoError(t, s.CreateProject(ctx, &store.Project{ - ID: "project-ancestry-1", Name: "Ancestry Project", Slug: "ancestry-project-1", + ID: tid("project-ancestry-1"), Name: "Ancestry Project", Slug: "ancestry-project-1", })) require.NoError(t, s.CreateAgent(ctx, &store.Agent{ - ID: "agent-parent", Slug: "agent-parent", Name: "Parent Agent", - ProjectID: "project-ancestry-1", Phase: string(state.PhaseRunning), + ID: tid("agent-parent"), Slug: tid("agent-parent"), Name: "Parent Agent", + ProjectID: tid("project-ancestry-1"), Phase: string(state.PhaseRunning), })) - agent := &evaluateAgentIdentity{id: "agent-parent", projectID: "project-ancestry-1"} + agent := &evaluateAgentIdentity{id: tid("agent-parent"), projectID: tid("project-ancestry-1")} // Grandchild agent with parent in ancestry resource := Resource{ Type: "agent", - ID: "agent-grandchild", - Ancestry: []string{"user-root", "agent-parent", "agent-child"}, + ID: tid("agent-grandchild"), + Ancestry: []string{tid("user-root"), tid("agent-parent"), tid("agent-child")}, } decision := authz.CheckAccess(ctx, agent, resource, ActionRead) @@ -660,15 +660,15 @@ func TestAuthz_AncestryAccess_NoAncestry(t *testing.T) { ctx := context.Background() require.NoError(t, s.CreateUser(ctx, &store.User{ - ID: "user-no-ancestry", Email: "no-ancestry@test.com", DisplayName: "NoAnc", Role: "member", Status: "active", + ID: tid("user-no-ancestry"), Email: "no-ancestry@test.com", DisplayName: "NoAnc", Role: "member", Status: "active", })) - user := NewAuthenticatedUser("user-no-ancestry", "no-ancestry@test.com", "NoAnc", "member", "api") + user := NewAuthenticatedUser(tid("user-no-ancestry"), "no-ancestry@test.com", "NoAnc", "member", "api") // Resource without ancestry — user is not owner and has no policies resource := Resource{ Type: "agent", - ID: "agent-no-ancestry", + ID: tid("agent-no-ancestry"), OwnerID: "someone-else", } @@ -682,17 +682,17 @@ func TestAuthz_AncestryAccess_NotInChain(t *testing.T) { ctx := context.Background() require.NoError(t, s.CreateUser(ctx, &store.User{ - ID: "user-outsider", Email: "outsider@test.com", DisplayName: "Outsider", Role: "member", Status: "active", + ID: tid("user-outsider"), Email: "outsider@test.com", DisplayName: "Outsider", Role: "member", Status: "active", })) - user := NewAuthenticatedUser("user-outsider", "outsider@test.com", "Outsider", "member", "api") + user := NewAuthenticatedUser(tid("user-outsider"), "outsider@test.com", "Outsider", "member", "api") // Resource with ancestry that doesn't include this user resource := Resource{ Type: "agent", - ID: "agent-other-chain", + ID: tid("agent-other-chain"), OwnerID: "someone-else", - Ancestry: []string{"user-other", "agent-A"}, + Ancestry: []string{tid("user-other"), tid("agent-A")}, } decision := authz.CheckAccess(ctx, user, resource, ActionRead) diff --git a/pkg/hub/bootstrap_test.go b/pkg/hub/bootstrap_test.go index 3e06c06e0..f0dd641bb 100644 --- a/pkg/hub/bootstrap_test.go +++ b/pkg/hub/bootstrap_test.go @@ -33,7 +33,6 @@ import ( "github.com/GoogleCloudPlatform/scion/pkg/messages" "github.com/GoogleCloudPlatform/scion/pkg/storage" "github.com/GoogleCloudPlatform/scion/pkg/store" - "github.com/GoogleCloudPlatform/scion/pkg/store/sqlite" "github.com/GoogleCloudPlatform/scion/pkg/transfer" ) @@ -159,6 +158,9 @@ func (d *mockDispatcher) DispatchAgentStop(_ context.Context, _ *store.Agent) er func (d *mockDispatcher) DispatchAgentRestart(_ context.Context, _ *store.Agent) error { return nil } +func (d *mockDispatcher) DispatchAgentResetAuth(_ context.Context, _ *store.Agent) error { + return nil +} func (d *mockDispatcher) DispatchAgentDelete(_ context.Context, _ *store.Agent, _, _, _ bool, _ time.Time) error { return nil } @@ -184,7 +186,7 @@ func (d *mockDispatcher) DispatchFinalizeEnv(_ context.Context, _ *store.Agent, // testBootstrapServer creates a test server with storage and dispatcher configured. func testBootstrapServer(t *testing.T) (*Server, store.Store, *mockStorage, *mockDispatcher) { t.Helper() - s, err := sqlite.New(":memory:") + s, err := newTestStore(":memory:") if err != nil { t.Fatalf("failed to create test store: %v", err) } @@ -239,7 +241,7 @@ func setupProjectAndBroker(t *testing.T, s store.Store) (string, string) { ctx := context.Background() broker := &store.RuntimeBroker{ - ID: "broker_bootstrap_test", + ID: tid("broker_bootstrap_test"), Slug: "bootstrap-host", Name: "Bootstrap Host", Status: store.BrokerStatusOnline, @@ -249,7 +251,7 @@ func setupProjectAndBroker(t *testing.T, s store.Store) (string, string) { } project := &store.Project{ - ID: "project_bootstrap_test", + ID: tid("project_bootstrap_test"), Slug: "bootstrap-project", Name: "Bootstrap Project", GitRemote: "https://github.com/test/bootstrap", @@ -410,7 +412,7 @@ func TestCreateAgentWithWorkspaceBootstrap_ExistingFiles(t *testing.T) { func TestCreateAgentWithWorkspaceBootstrap_NoStorage(t *testing.T) { // Create server without storage - s, err := sqlite.New(":memory:") + s, err := newTestStore(":memory:") if err != nil { t.Fatalf("failed to create test store: %v", err) } @@ -489,7 +491,7 @@ func TestCreateAgentWithWorkspaceBootstrap_LocalProvider(t *testing.T) { // Create broker and project broker := &store.RuntimeBroker{ - ID: "broker_local_path_test", + ID: tid("broker_local_path_test"), Slug: "local-path-host", Name: "Local Path Host", Status: store.BrokerStatusOnline, @@ -499,7 +501,7 @@ func TestCreateAgentWithWorkspaceBootstrap_LocalProvider(t *testing.T) { } project := &store.Project{ - ID: "project_local_path_test", + ID: tid("project_local_path_test"), Slug: "local-path-project", Name: "Local Path Project", GitRemote: "https://github.com/test/local-path", @@ -721,11 +723,11 @@ func TestSyncToFinalize_BootstrapMode(t *testing.T) { // Create an agent in provisioning status (simulating post-bootstrap-create) agent := &store.Agent{ - ID: "agent_bootstrap_finalize", + ID: tid("agent_bootstrap_finalize"), Slug: "bootstrap-finalize", Name: "Bootstrap Finalize", ProjectID: projectID, - RuntimeBrokerID: "broker_bootstrap_test", + RuntimeBrokerID: tid("broker_bootstrap_test"), Phase: string(state.PhaseProvisioning), Visibility: store.VisibilityPrivate, AppliedConfig: &store.AgentAppliedConfig{ @@ -737,7 +739,7 @@ func TestSyncToFinalize_BootstrapMode(t *testing.T) { } // Pre-populate the files in mock storage - storagePath := "workspaces/" + projectID + "/agent_bootstrap_finalize" + storagePath := "workspaces/" + projectID + "/" + tid("agent_bootstrap_finalize") stor.objects[storagePath+"/files/main.go"] = &storage.Object{ Name: storagePath + "/files/main.go", } @@ -757,7 +759,7 @@ func TestSyncToFinalize_BootstrapMode(t *testing.T) { Manifest: manifest, } - rec := doBootstrapRequest(t, srv, http.MethodPost, "/api/v1/agents/agent_bootstrap_finalize/workspace/sync-to/finalize", finalizeReq) + rec := doBootstrapRequest(t, srv, http.MethodPost, fmt.Sprintf("/api/v1/agents/%s/workspace/sync-to/finalize", tid("agent_bootstrap_finalize")), finalizeReq) if rec.Code != http.StatusOK { t.Fatalf("expected status 200, got %d: %s", rec.Code, rec.Body.String()) @@ -787,7 +789,7 @@ func TestSyncToFinalize_BootstrapMode(t *testing.T) { } dispatched := disp.dispatchedAgents[0] - if dispatched.ID != "agent_bootstrap_finalize" { + if dispatched.ID != tid("agent_bootstrap_finalize") { t.Errorf("expected dispatched agent ID 'agent_bootstrap_finalize', got %q", dispatched.ID) } @@ -807,11 +809,11 @@ func TestSyncToFinalize_BootstrapMode_MissingFile(t *testing.T) { // Create an agent in provisioning status agent := &store.Agent{ - ID: "agent_bootstrap_missing", + ID: tid("agent_bootstrap_missing"), Slug: "bootstrap-missing", Name: "Bootstrap Missing", ProjectID: projectID, - RuntimeBrokerID: "broker_bootstrap_test", + RuntimeBrokerID: tid("broker_bootstrap_test"), Phase: string(state.PhaseProvisioning), Visibility: store.VisibilityPrivate, } @@ -820,7 +822,7 @@ func TestSyncToFinalize_BootstrapMode_MissingFile(t *testing.T) { } // Only put one file in storage - storagePath := "workspaces/" + projectID + "/agent_bootstrap_missing" + storagePath := "workspaces/" + projectID + "/" + tid("agent_bootstrap_missing") stor.objects[storagePath+"/files/main.go"] = &storage.Object{ Name: storagePath + "/files/main.go", } @@ -837,7 +839,7 @@ func TestSyncToFinalize_BootstrapMode_MissingFile(t *testing.T) { Manifest: manifest, } - rec := doBootstrapRequest(t, srv, http.MethodPost, "/api/v1/agents/agent_bootstrap_missing/workspace/sync-to/finalize", finalizeReq) + rec := doBootstrapRequest(t, srv, http.MethodPost, fmt.Sprintf("/api/v1/agents/%s/workspace/sync-to/finalize", tid("agent_bootstrap_missing")), finalizeReq) if rec.Code != http.StatusBadRequest { t.Errorf("expected status 400, got %d: %s", rec.Code, rec.Body.String()) @@ -851,11 +853,11 @@ func TestSyncToFinalize_RejectsStoppedAgent(t *testing.T) { // Create an agent in stopped status agent := &store.Agent{ - ID: "agent_bootstrap_stopped", + ID: tid("agent_bootstrap_stopped"), Slug: "bootstrap-stopped", Name: "Bootstrap Stopped", ProjectID: projectID, - RuntimeBrokerID: "broker_bootstrap_test", + RuntimeBrokerID: tid("broker_bootstrap_test"), Phase: string(state.PhaseStopped), Visibility: store.VisibilityPrivate, } @@ -869,7 +871,7 @@ func TestSyncToFinalize_RejectsStoppedAgent(t *testing.T) { } finalizeReq := SyncToFinalizeRequest{Manifest: manifest} - rec := doBootstrapRequest(t, srv, http.MethodPost, "/api/v1/agents/agent_bootstrap_stopped/workspace/sync-to/finalize", finalizeReq) + rec := doBootstrapRequest(t, srv, http.MethodPost, fmt.Sprintf("/api/v1/agents/%s/workspace/sync-to/finalize", tid("agent_bootstrap_stopped")), finalizeReq) if rec.Code != http.StatusConflict { t.Errorf("expected status 409, got %d: %s", rec.Code, rec.Body.String()) @@ -878,7 +880,7 @@ func TestSyncToFinalize_RejectsStoppedAgent(t *testing.T) { func TestSyncToFinalize_BootstrapMode_NoDispatcher(t *testing.T) { // Create server without dispatcher - s, err := sqlite.New(":memory:") + s, err := newTestStore(":memory:") if err != nil { t.Fatalf("failed to create test store: %v", err) } @@ -901,11 +903,11 @@ func TestSyncToFinalize_BootstrapMode_NoDispatcher(t *testing.T) { ctx := context.Background() agent := &store.Agent{ - ID: "agent_bootstrap_nodisp", + ID: tid("agent_bootstrap_nodisp"), Slug: "bootstrap-nodisp", Name: "Bootstrap No Dispatcher", ProjectID: projectID, - RuntimeBrokerID: "broker_bootstrap_test", + RuntimeBrokerID: tid("broker_bootstrap_test"), Phase: string(state.PhaseProvisioning), Visibility: store.VisibilityPrivate, } @@ -913,7 +915,7 @@ func TestSyncToFinalize_BootstrapMode_NoDispatcher(t *testing.T) { t.Fatalf("failed to create agent: %v", err) } - storagePath := "workspaces/" + projectID + "/agent_bootstrap_nodisp" + storagePath := "workspaces/" + projectID + "/" + tid("agent_bootstrap_nodisp") stor.objects[storagePath+"/files/main.go"] = &storage.Object{ Name: storagePath + "/files/main.go", } @@ -924,7 +926,7 @@ func TestSyncToFinalize_BootstrapMode_NoDispatcher(t *testing.T) { } finalizeReq := SyncToFinalizeRequest{Manifest: manifest} - rec := doBootstrapRequest(t, srv, http.MethodPost, "/api/v1/agents/agent_bootstrap_nodisp/workspace/sync-to/finalize", finalizeReq) + rec := doBootstrapRequest(t, srv, http.MethodPost, fmt.Sprintf("/api/v1/agents/%s/workspace/sync-to/finalize", tid("agent_bootstrap_nodisp")), finalizeReq) if rec.Code != http.StatusBadGateway { t.Errorf("expected status 502, got %d: %s", rec.Code, rec.Body.String()) @@ -942,7 +944,7 @@ func TestDispatcherPassesWorkspaceStoragePath(t *testing.T) { ID: "agent_with_storage_path", Slug: "storage-path-agent", Name: "Storage Path Agent", - ProjectID: "project_test", + ProjectID: tid("project_test"), RuntimeBrokerID: "broker_test", Phase: string(state.PhaseProvisioning), AppliedConfig: &store.AgentAppliedConfig{ diff --git a/pkg/hub/broker_http_transport.go b/pkg/hub/broker_http_transport.go index 9dab4d1b0..45537cc86 100644 --- a/pkg/hub/broker_http_transport.go +++ b/pkg/hub/broker_http_transport.go @@ -263,6 +263,26 @@ func (t *brokerHTTPTransport) RestartAgent(ctx context.Context, brokerID, broker return nil } +func (t *brokerHTTPTransport) ResetAuthAgent(ctx context.Context, brokerID, brokerEndpoint, agentID, projectID, token string) error { + endpoint := fmt.Sprintf("%s/api/v1/agents/%s/reset-auth", strings.TrimSuffix(brokerEndpoint, "/"), url.PathEscape(agentID)) + if projectID != "" { + endpoint += "?projectId=" + url.QueryEscape(projectID) + } + body, err := json.Marshal(map[string]string{"token": token}) + if err != nil { + return fmt.Errorf("failed to marshal reset-auth request: %w", err) + } + resp, err := t.doRequest(ctx, brokerID, http.MethodPost, endpoint, body) + if err != nil { + return fmt.Errorf("failed to send request: %w", err) + } + defer resp.Body.Close() + if resp.StatusCode >= 400 { + return brokerHTTPError(resp) + } + return nil +} + func (t *brokerHTTPTransport) DeleteAgent(ctx context.Context, brokerID, brokerEndpoint, agentID, projectID string, deleteFiles, removeBranch, softDelete bool, deletedAt time.Time) error { endpoint := fmt.Sprintf("%s/api/v1/agents/%s?deleteFiles=%t&removeBranch=%t", strings.TrimSuffix(brokerEndpoint, "/"), url.PathEscape(agentID), deleteFiles, removeBranch) @@ -447,8 +467,11 @@ func (t *brokerHTTPTransport) ExecAgent(ctx context.Context, brokerID, brokerEnd return result.Output, result.ExitCode, nil } -func (t *brokerHTTPTransport) CleanupProject(ctx context.Context, brokerID, brokerEndpoint, projectSlug string) error { +func (t *brokerHTTPTransport) CleanupProject(ctx context.Context, brokerID, brokerEndpoint, projectSlug, projectID string) error { endpoint := fmt.Sprintf("%s/api/v1/projects/%s", strings.TrimSuffix(brokerEndpoint, "/"), url.PathEscape(projectSlug)) + if projectID != "" { + endpoint += "?project_id=" + url.QueryEscape(projectID) + } resp, err := t.doRequest(ctx, brokerID, http.MethodDelete, endpoint, nil) if err != nil { return fmt.Errorf("failed to send request: %w", err) diff --git a/pkg/hub/broker_routing.go b/pkg/hub/broker_routing.go new file mode 100644 index 000000000..f90aa3d9c --- /dev/null +++ b/pkg/hub/broker_routing.go @@ -0,0 +1,122 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package hub + +import ( + "context" + "errors" + "time" + + "github.com/GoogleCloudPlatform/scion/pkg/store" +) + +// ErrMessageDeferred is returned by HybridBrokerClient.MessageAgent when the +// broker is not locally connected and has no HTTP endpoint: the message row +// (already persisted with dispatch_state=pending) is the durable intent, and +// the caller should emit a NOTIFY wakeup and return 202 Accepted. +var ErrMessageDeferred = errors.New("message deferred: broker not locally reachable") + +// ErrLifecycleDeferred is returned by HybridBrokerClient.StartAgent/StopAgent/ +// RestartAgent when the broker is not locally connected and has no HTTP +// endpoint. The caller should serialize resolved params into a broker_dispatch +// row, signal the owning node via the command bus, and wait for the resulting +// agent status transition (design §5.4, §6.2). +var ErrLifecycleDeferred = errors.New("lifecycle deferred: broker not locally reachable") + +// routeDecision is the outcome of HybridBrokerClient.route — how a dispatch for a +// broker should be delivered when this node does not hold the broker's socket. +type routeDecision int + +const ( + // routeLocal: this node holds the broker's control-channel socket — tunnel + // directly (the unchanged, zero-added-latency fast path). + routeLocal routeDecision = iota + // routeForward: some other node is believed to own the broker (affinity hint + // is alive) — write durable intent + NOTIFY and let the owner self-select. + routeForward + // routeHTTP: no live owner, but the broker exposes a direct HTTP endpoint + // (direct-mode broker; existing fallback — rare under NAT'd deployments). + routeHTTP + // routeUndeliverable: no owner and no endpoint — write durable pending intent + // and return a retryable status; reconciled on the broker's next reconnect. + routeUndeliverable +) + +func (d routeDecision) String() string { + switch d { + case routeLocal: + return "local" + case routeForward: + return "forward" + case routeHTTP: + return "http" + default: + return "undeliverable" + } +} + +// defaultAffinityFreshness bounds how long a broker's last_heartbeat is trusted +// as "owner alive" for routing. Generous (a multiple of the heartbeat interval); +// a stale hint only costs one dispatch timeout before falling through, and the +// reaper (B5-1) clears dead owners. +const defaultAffinityFreshness = 90 * time.Second + +// route decides how to deliver a dispatch for brokerID. The local fast path is +// checked first and unchanged; affinity is consulted only to choose between +// forwarding (durable intent + signal) and fast-failing (design §5.3). The +// affinity lookup is a hint — a wrong "alive" costs one timeout (intent stays +// durable and reconciles later); a wrong "dead" is reaped by §7.1. +func (c *HybridBrokerClient) route(ctx context.Context, brokerID, brokerEndpoint string) routeDecision { + if c.controlChannel.manager.IsConnected(brokerID) { + return routeLocal + } + var owner string + var alive bool + if c.affinity != nil { + owner, alive = c.affinity(ctx, brokerID) + } + switch { + case owner != "" && alive: + return routeForward + case brokerEndpoint != "": + return routeHTTP + default: + return routeUndeliverable + } +} + +// SetAffinityLookup injects the affinity hint used by route(). Wired by the +// server to a store-backed lookup (StoreAffinityLookup). +func (c *HybridBrokerClient) SetAffinityLookup(fn func(ctx context.Context, brokerID string) (owner string, alive bool)) { + c.affinity = fn +} + +// StoreAffinityLookup returns an affinity lookup backed by runtime_brokers: the +// owner is connected_hub_id, and "alive" means last_heartbeat is within +// freshness. (Liveness is inferred from heartbeat freshness because there is no +// hub-to-hub addressability to ping a peer — design §5.3.) +func StoreAffinityLookup(st store.Store, freshness time.Duration) func(ctx context.Context, brokerID string) (string, bool) { + if freshness <= 0 { + freshness = defaultAffinityFreshness + } + return func(ctx context.Context, brokerID string) (string, bool) { + b, err := st.GetRuntimeBroker(ctx, brokerID) + if err != nil || b == nil || b.ConnectedHubID == nil || *b.ConnectedHubID == "" { + return "", false + } + alive := !b.LastHeartbeat.IsZero() && time.Since(b.LastHeartbeat) < freshness + return *b.ConnectedHubID, alive + } +} diff --git a/pkg/hub/broker_routing_test.go b/pkg/hub/broker_routing_test.go new file mode 100644 index 000000000..4d68251e0 --- /dev/null +++ b/pkg/hub/broker_routing_test.go @@ -0,0 +1,163 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package hub + +import ( + "context" + "log/slog" + "testing" + "time" + + "github.com/GoogleCloudPlatform/scion/pkg/api" + "github.com/GoogleCloudPlatform/scion/pkg/messages" + "github.com/stretchr/testify/assert" +) + +// fakeHTTPClient records calls to MessageAgent so we can verify the HTTP +// fallback path. Other methods are stubs. +type fakeHTTPClient struct { + messageAgentCalled bool +} + +func (f *fakeHTTPClient) MessageAgent(context.Context, string, string, string, string, string, bool, *messages.StructuredMessage) error { + f.messageAgentCalled = true + return nil +} + +// Stub implementations for the RuntimeBrokerClient interface — only MessageAgent matters. +func (f *fakeHTTPClient) CreateAgent(context.Context, string, string, *RemoteCreateAgentRequest) (*RemoteAgentResponse, error) { + return nil, nil +} +func (f *fakeHTTPClient) StartAgent(context.Context, string, string, string, string, string, string, string, string, map[string]string, []ResolvedSecret, *api.ScionConfig, []api.SharedDir, bool) (*RemoteAgentResponse, error) { + return nil, nil +} +func (f *fakeHTTPClient) StopAgent(context.Context, string, string, string, string) error { + return nil +} +func (f *fakeHTTPClient) RestartAgent(context.Context, string, string, string, string, map[string]string) error { + return nil +} +func (f *fakeHTTPClient) ResetAuthAgent(context.Context, string, string, string, string, string) error { + return nil +} +func (f *fakeHTTPClient) DeleteAgent(context.Context, string, string, string, string, bool, bool, bool, time.Time) error { + return nil +} +func (f *fakeHTTPClient) CheckAgentPrompt(context.Context, string, string, string, string) (bool, error) { + return false, nil +} +func (f *fakeHTTPClient) CreateAgentWithGather(context.Context, string, string, *RemoteCreateAgentRequest) (*RemoteAgentResponse, *RemoteEnvRequirementsResponse, error) { + return nil, nil, nil +} +func (f *fakeHTTPClient) FinalizeEnv(context.Context, string, string, string, map[string]string) (*RemoteAgentResponse, error) { + return nil, nil +} +func (f *fakeHTTPClient) GetAgentLogs(context.Context, string, string, string, string, int) (string, error) { + return "", nil +} +func (f *fakeHTTPClient) ExecAgent(context.Context, string, string, string, string, []string, int) (string, int, error) { + return "", 0, nil +} +func (f *fakeHTTPClient) CleanupProject(context.Context, string, string, string, string) error { + return nil +} + +func TestHybridBrokerClient_Route(t *testing.T) { + ctx := context.Background() + const localBroker = "broker-local" + + mgr := NewControlChannelManager(DefaultControlChannelConfig(), slog.Default()) + // Seed a live local socket for localBroker only. + mgr.mu.Lock() + mgr.connections[localBroker] = &BrokerConnection{brokerID: localBroker, sessionID: "s1"} + mgr.mu.Unlock() + + c := NewHybridBrokerClient(mgr, nil, nil, false) + + cases := []struct { + name string + brokerID string + endpoint string + affOwner string + affAlive bool + want routeDecision + }{ + {"local socket wins", localBroker, "", "", false, routeLocal}, + {"local wins even over alive affinity", localBroker, "http://x", "hubA", true, routeLocal}, + {"alive owner -> forward", "b1", "", "hubA", true, routeForward}, + {"alive owner -> forward (endpoint ignored)", "b1", "http://x", "hubA", true, routeForward}, + {"no owner, endpoint set -> http", "b2", "http://x", "", false, routeHTTP}, + {"stale owner, endpoint set -> http", "b3", "http://x", "hubA", false, routeHTTP}, + {"stale owner, no endpoint -> undeliverable", "b4", "", "hubA", false, routeUndeliverable}, + {"no owner, no endpoint -> undeliverable", "b5", "", "", false, routeUndeliverable}, + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + c.SetAffinityLookup(func(context.Context, string) (string, bool) { return tc.affOwner, tc.affAlive }) + got := c.route(ctx, tc.brokerID, tc.endpoint) + assert.Equal(t, tc.want, got, "route(%s, endpoint=%q, owner=%q alive=%v)", tc.brokerID, tc.endpoint, tc.affOwner, tc.affAlive) + }) + } +} + +func TestHybridBrokerClient_Route_NilAffinityIsSafe(t *testing.T) { + mgr := NewControlChannelManager(DefaultControlChannelConfig(), slog.Default()) + c := NewHybridBrokerClient(mgr, nil, nil, false) + // No affinity lookup set: a non-local broker with no endpoint is undeliverable. + assert.Equal(t, routeUndeliverable, c.route(context.Background(), "b-none", "")) + assert.Equal(t, routeHTTP, c.route(context.Background(), "b-ep", "http://x")) +} + +func TestHybridBrokerClient_MessageAgent_RouteGate(t *testing.T) { + const localBroker = "broker-local" + const remoteBroker = "broker-remote" + + mgr := NewControlChannelManager(DefaultControlChannelConfig(), slog.Default()) + mgr.mu.Lock() + mgr.connections[localBroker] = &BrokerConnection{brokerID: localBroker, sessionID: "s1"} + mgr.mu.Unlock() + + httpClient := &fakeHTTPClient{} + c := NewHybridBrokerClient(mgr, httpClient, nil, false) + + t.Run("routeLocal uses control channel (not deferred)", func(t *testing.T) { + // Verify route() returns routeLocal for the locally connected broker. + // We don't call MessageAgent directly because the stub BrokerConnection + // doesn't have a real tunnel; the route decision is what matters. + got := c.route(context.Background(), localBroker, "") + assert.Equal(t, routeLocal, got, "should pick local tunnel for connected broker") + }) + + t.Run("routeHTTP delivers via HTTP client", func(t *testing.T) { + httpClient.messageAgentCalled = false + c.SetAffinityLookup(func(context.Context, string) (string, bool) { return "", false }) + err := c.MessageAgent(context.Background(), remoteBroker, "http://endpoint", "a1", "p1", "hi", false, nil) + assert.NoError(t, err) + assert.True(t, httpClient.messageAgentCalled, "HTTP fallback should be used") + }) + + t.Run("routeForward returns ErrMessageDeferred", func(t *testing.T) { + c.SetAffinityLookup(func(context.Context, string) (string, bool) { return "hubA", true }) + err := c.MessageAgent(context.Background(), remoteBroker, "", "a1", "p1", "hi", false, nil) + assert.ErrorIs(t, err, ErrMessageDeferred) + }) + + t.Run("routeUndeliverable returns ErrMessageDeferred", func(t *testing.T) { + c.SetAffinityLookup(func(context.Context, string) (string, bool) { return "", false }) + err := c.MessageAgent(context.Background(), remoteBroker, "", "a1", "p1", "hi", false, nil) + assert.ErrorIs(t, err, ErrMessageDeferred) + }) +} diff --git a/pkg/hub/brokerauth_test.go b/pkg/hub/brokerauth_test.go index 60e86b5e8..2c27a8873 100644 --- a/pkg/hub/brokerauth_test.go +++ b/pkg/hub/brokerauth_test.go @@ -31,14 +31,13 @@ import ( "time" "github.com/GoogleCloudPlatform/scion/pkg/store" - "github.com/GoogleCloudPlatform/scion/pkg/store/sqlite" "github.com/google/uuid" ) func setupTestBrokerAuthService(t *testing.T) (*BrokerAuthService, store.Store) { t.Helper() - s, err := sqlite.New(":memory:") + s, err := newTestStore(":memory:") if err != nil { t.Fatalf("failed to create store: %v", err) } @@ -149,7 +148,7 @@ func TestJoinWithInvalidToken(t *testing.T) { func TestJoinWithExpiredToken(t *testing.T) { // Create service with short token expiry - s, err := sqlite.New(":memory:") + s, err := newTestStore(":memory:") if err != nil { t.Fatalf("failed to create store: %v", err) } @@ -329,7 +328,7 @@ func TestValidateBrokerSignature_InvalidSignature(t *testing.T) { func TestValidateBrokerSignature_ClockSkew(t *testing.T) { // Create service with short clock skew tolerance - s, err := sqlite.New(":memory:") + s, err := newTestStore(":memory:") if err != nil { t.Fatalf("failed to create store: %v", err) } diff --git a/pkg/hub/brokerclient.go b/pkg/hub/brokerclient.go index b3dffcf54..b8da2eedf 100644 --- a/pkg/hub/brokerclient.go +++ b/pkg/hub/brokerclient.go @@ -58,6 +58,11 @@ func (c *AuthenticatedBrokerClient) RestartAgent(ctx context.Context, brokerID, return c.transport.RestartAgent(ctx, brokerID, brokerEndpoint, agentID, projectID, resolvedEnv) } +// ResetAuthAgent injects a fresh auth token into a running agent with HMAC authentication. +func (c *AuthenticatedBrokerClient) ResetAuthAgent(ctx context.Context, brokerID, brokerEndpoint, agentID, projectID, token string) error { + return c.transport.ResetAuthAgent(ctx, brokerID, brokerEndpoint, agentID, projectID, token) +} + // DeleteAgent deletes an agent from a remote runtime broker with HMAC authentication. func (c *AuthenticatedBrokerClient) DeleteAgent(ctx context.Context, brokerID, brokerEndpoint, agentID, projectID string, deleteFiles, removeBranch, softDelete bool, deletedAt time.Time) error { return c.transport.DeleteAgent(ctx, brokerID, brokerEndpoint, agentID, projectID, deleteFiles, removeBranch, softDelete, deletedAt) @@ -89,8 +94,8 @@ func (c *AuthenticatedBrokerClient) ExecAgent(ctx context.Context, brokerID, bro } // CleanupProject asks a broker to remove its local hub-managed project directory with HMAC authentication. -func (c *AuthenticatedBrokerClient) CleanupProject(ctx context.Context, brokerID, brokerEndpoint, projectSlug string) error { - return c.transport.CleanupProject(ctx, brokerID, brokerEndpoint, projectSlug) +func (c *AuthenticatedBrokerClient) CleanupProject(ctx context.Context, brokerID, brokerEndpoint, projectSlug, projectID string) error { + return c.transport.CleanupProject(ctx, brokerID, brokerEndpoint, projectSlug, projectID) } // FinalizeEnv sends gathered env vars to a broker to complete agent creation. diff --git a/pkg/hub/brokerclient_test.go b/pkg/hub/brokerclient_test.go index 69d901eb9..7f3c3c3ee 100644 --- a/pkg/hub/brokerclient_test.go +++ b/pkg/hub/brokerclient_test.go @@ -27,12 +27,11 @@ import ( "github.com/GoogleCloudPlatform/scion/pkg/apiclient" "github.com/GoogleCloudPlatform/scion/pkg/store" - "github.com/GoogleCloudPlatform/scion/pkg/store/sqlite" ) func TestAuthenticatedBrokerClient_CreateAgent(t *testing.T) { // Create a test store with a broker secret - db, err := sqlite.New(":memory:") + db, err := newTestStore(":memory:") if err != nil { t.Fatalf("failed to create test store: %v", err) } @@ -43,7 +42,7 @@ func TestAuthenticatedBrokerClient_CreateAgent(t *testing.T) { } // Create a test broker - brokerID := "test-host-123" + brokerID := tid("test-host-123") secretKey := []byte("test-secret-key-32-bytes-long!!!") broker := &store.RuntimeBroker{ @@ -101,7 +100,7 @@ func TestAuthenticatedBrokerClient_CreateAgent(t *testing.T) { resp := &RemoteAgentResponse{ Created: true, Agent: &RemoteAgentInfo{ - ID: "agent-1", + ID: tid("agent-1"), Name: "test-agent", Status: "created", }, @@ -116,9 +115,9 @@ func TestAuthenticatedBrokerClient_CreateAgent(t *testing.T) { // Make request req := &RemoteCreateAgentRequest{ - Slug: "agent-1", + Slug: tid("agent-1"), Name: "test-agent", - ProjectID: "project-1", + ProjectID: tid("project-1"), } resp, err := client.CreateAgent(context.Background(), brokerID, server.URL, req) @@ -146,7 +145,7 @@ func TestAuthenticatedBrokerClient_CreateAgent(t *testing.T) { func TestAuthenticatedBrokerClient_StartAgent(t *testing.T) { // Create a test store with a broker secret - db, err := sqlite.New(":memory:") + db, err := newTestStore(":memory:") if err != nil { t.Fatalf("failed to create test store: %v", err) } @@ -157,7 +156,7 @@ func TestAuthenticatedBrokerClient_StartAgent(t *testing.T) { } // Create a test broker - brokerID := "test-host-456" + brokerID := tid("test-host-456") secretKey := []byte("another-secret-key-32-bytes!!!!!") broker := &store.RuntimeBroker{ @@ -235,7 +234,7 @@ func TestAuthenticatedBrokerClient_StartAgent(t *testing.T) { func TestAuthenticatedBrokerClient_MissingSecretFailsClosed(t *testing.T) { // Create a test store without a secret - db, err := sqlite.New(":memory:") + db, err := newTestStore(":memory:") if err != nil { t.Fatalf("failed to create test store: %v", err) } @@ -246,12 +245,12 @@ func TestAuthenticatedBrokerClient_MissingSecretFailsClosed(t *testing.T) { } // Create a test broker without a secret - brokerID := "test-host-no-secret" + brokerID := tid("test-host-no-secret") broker := &store.RuntimeBroker{ ID: brokerID, - Name: "test-host-no-secret", - Slug: "test-host-no-secret", + Name: tid("test-host-no-secret"), + Slug: tid("test-host-no-secret"), Status: store.BrokerStatusOnline, Created: time.Now(), Updated: time.Now(), @@ -274,9 +273,9 @@ func TestAuthenticatedBrokerClient_MissingSecretFailsClosed(t *testing.T) { // Make request - should fail before sending anything req := &RemoteCreateAgentRequest{ - Slug: "agent-1", + Slug: tid("agent-1"), Name: "test-agent", - ProjectID: "project-1", + ProjectID: tid("project-1"), } _, err = client.CreateAgent(context.Background(), brokerID, server.URL, req) @@ -293,7 +292,7 @@ func TestAuthenticatedBrokerClient_MissingSecretFailsClosed(t *testing.T) { func TestAuthenticatedBrokerClient_ExpiredSecretFailsClosed(t *testing.T) { // Create a test store with an expired secret - db, err := sqlite.New(":memory:") + db, err := newTestStore(":memory:") if err != nil { t.Fatalf("failed to create test store: %v", err) } @@ -304,13 +303,13 @@ func TestAuthenticatedBrokerClient_ExpiredSecretFailsClosed(t *testing.T) { } // Create a test broker with expired secret - brokerID := "test-host-expired" + brokerID := tid("test-host-expired") secretKey := []byte("expired-secret-key-32-bytes!!!!!") broker := &store.RuntimeBroker{ ID: brokerID, - Name: "test-host-expired", - Slug: "test-host-expired", + Name: tid("test-host-expired"), + Slug: tid("test-host-expired"), Status: store.BrokerStatusOnline, Created: time.Now(), Updated: time.Now(), @@ -345,9 +344,9 @@ func TestAuthenticatedBrokerClient_ExpiredSecretFailsClosed(t *testing.T) { // Make request - should fail before sending due to expired secret req := &RemoteCreateAgentRequest{ - Slug: "agent-1", + Slug: tid("agent-1"), Name: "test-agent", - ProjectID: "project-1", + ProjectID: tid("project-1"), } _, err = client.CreateAgent(context.Background(), brokerID, server.URL, req) @@ -363,7 +362,7 @@ func TestAuthenticatedBrokerClient_ExpiredSecretFailsClosed(t *testing.T) { } func TestAuthenticatedBrokerClient_StartAgent_InvalidJSONFails(t *testing.T) { - db, err := sqlite.New(":memory:") + db, err := newTestStore(":memory:") if err != nil { t.Fatalf("failed to create test store: %v", err) } @@ -373,11 +372,11 @@ func TestAuthenticatedBrokerClient_StartAgent_InvalidJSONFails(t *testing.T) { t.Fatalf("failed to migrate: %v", err) } - brokerID := "test-host-invalid-json" + brokerID := tid("test-host-invalid-json") broker := &store.RuntimeBroker{ ID: brokerID, - Name: "test-host-invalid-json", - Slug: "test-host-invalid-json", + Name: tid("test-host-invalid-json"), + Slug: tid("test-host-invalid-json"), Status: store.BrokerStatusOnline, Created: time.Now(), Updated: time.Now(), @@ -405,7 +404,7 @@ func TestAuthenticatedBrokerClient_StartAgent_InvalidJSONFails(t *testing.T) { defer server.Close() client := NewAuthenticatedBrokerClient(db, false) - _, err = client.StartAgent(context.Background(), brokerID, server.URL, "agent-1", "", "", "", "", "", nil, nil, nil, nil, false) + _, err = client.StartAgent(context.Background(), brokerID, server.URL, tid("agent-1"), "", "", "", "", "", nil, nil, nil, nil, false) if err == nil { t.Fatal("expected StartAgent to fail on invalid JSON response") } @@ -416,7 +415,7 @@ func TestAuthenticatedBrokerClient_StartAgent_InvalidJSONFails(t *testing.T) { func TestAuthenticatedBrokerClient_AllOperations(t *testing.T) { // Create a test store with a broker secret - db, err := sqlite.New(":memory:") + db, err := newTestStore(":memory:") if err != nil { t.Fatalf("failed to create test store: %v", err) } @@ -427,13 +426,13 @@ func TestAuthenticatedBrokerClient_AllOperations(t *testing.T) { } // Create a test broker - brokerID := "test-host-ops" + brokerID := tid("test-host-ops") secretKey := []byte("ops-test-secret-key-32-bytes!!!!") broker := &store.RuntimeBroker{ ID: brokerID, - Name: "test-host-ops", - Slug: "test-host-ops", + Name: tid("test-host-ops"), + Slug: tid("test-host-ops"), Status: store.BrokerStatusOnline, Created: time.Now(), Updated: time.Now(), diff --git a/pkg/hub/capabilities_test.go b/pkg/hub/capabilities_test.go index d6f7216e8..2c8baff10 100644 --- a/pkg/hub/capabilities_test.go +++ b/pkg/hub/capabilities_test.go @@ -42,11 +42,11 @@ func TestComputeCapabilities_OwnerGetsAllActions(t *testing.T) { ctx := context.Background() require.NoError(t, s.CreateUser(ctx, &store.User{ - ID: "user-owner-cap", Email: "owner-cap@test.com", DisplayName: "Owner", Role: "member", Status: "active", + ID: tid("user-owner-cap"), Email: "owner-cap@test.com", DisplayName: "Owner", Role: "member", Status: "active", })) - user := NewAuthenticatedUser("user-owner-cap", "owner-cap@test.com", "Owner", "member", "api") - resource := Resource{Type: "agent", ID: "agent-1", OwnerID: "user-owner-cap"} + user := NewAuthenticatedUser(tid("user-owner-cap"), "owner-cap@test.com", "Owner", "member", "api") + resource := Resource{Type: "agent", ID: tid("agent-1"), OwnerID: tid("user-owner-cap")} caps := srv.authzService.ComputeCapabilities(ctx, user, resource) assert.Equal(t, []string{"read", "update", "delete", "start", "stop", "message", "attach"}, caps.Actions) @@ -57,20 +57,20 @@ func TestComputeCapabilities_PolicySubset(t *testing.T) { ctx := context.Background() require.NoError(t, s.CreateUser(ctx, &store.User{ - ID: "user-readonly-cap", Email: "readonly-cap@test.com", DisplayName: "ReadOnly", Role: "member", Status: "active", + ID: tid("user-readonly-cap"), Email: "readonly-cap@test.com", DisplayName: "ReadOnly", Role: "member", Status: "active", })) policy := &store.Policy{ - ID: "policy-ro-cap", Name: "Read Only", ScopeType: "hub", + ID: tid("policy-ro-cap"), Name: "Read Only", ScopeType: "hub", ResourceType: "agent", Actions: []string{"read"}, Effect: "allow", } require.NoError(t, s.CreatePolicy(ctx, policy)) require.NoError(t, s.AddPolicyBinding(ctx, &store.PolicyBinding{ - PolicyID: "policy-ro-cap", PrincipalType: "user", PrincipalID: "user-readonly-cap", + PolicyID: tid("policy-ro-cap"), PrincipalType: "user", PrincipalID: tid("user-readonly-cap"), })) - user := NewAuthenticatedUser("user-readonly-cap", "readonly-cap@test.com", "ReadOnly", "member", "api") - resource := Resource{Type: "agent", ID: "agent-1"} + user := NewAuthenticatedUser(tid("user-readonly-cap"), "readonly-cap@test.com", "ReadOnly", "member", "api") + resource := Resource{Type: "agent", ID: tid("agent-1")} caps := srv.authzService.ComputeCapabilities(ctx, user, resource) assert.Equal(t, []string{"read"}, caps.Actions) @@ -81,11 +81,11 @@ func TestComputeCapabilities_DefaultDenyEmpty(t *testing.T) { ctx := context.Background() require.NoError(t, s.CreateUser(ctx, &store.User{ - ID: "user-nopolicy-cap", Email: "nopolicy-cap@test.com", DisplayName: "NoPolicy", Role: "member", Status: "active", + ID: tid("user-nopolicy-cap"), Email: "nopolicy-cap@test.com", DisplayName: "NoPolicy", Role: "member", Status: "active", })) - user := NewAuthenticatedUser("user-nopolicy-cap", "nopolicy-cap@test.com", "NoPolicy", "member", "api") - resource := Resource{Type: "agent", ID: "agent-1"} + user := NewAuthenticatedUser(tid("user-nopolicy-cap"), "nopolicy-cap@test.com", "NoPolicy", "member", "api") + resource := Resource{Type: "agent", ID: tid("agent-1")} caps := srv.authzService.ComputeCapabilities(ctx, user, resource) assert.Equal(t, []string{}, caps.Actions) @@ -97,9 +97,9 @@ func TestComputeCapabilitiesBatch_AdminGetsAll(t *testing.T) { admin := NewAuthenticatedUser("admin-batch", "admin-batch@example.com", "Admin", "admin", "api") resources := []Resource{ - {Type: "agent", ID: "agent-1"}, - {Type: "agent", ID: "agent-2"}, - {Type: "agent", ID: "agent-3"}, + {Type: "agent", ID: tid("agent-1")}, + {Type: "agent", ID: tid("agent-2")}, + {Type: "agent", ID: tid("agent-3")}, } caps := srv.authzService.ComputeCapabilitiesBatch(ctx, admin, resources, "agent") @@ -114,23 +114,23 @@ func TestComputeCapabilitiesBatch_MixedOwnership(t *testing.T) { ctx := context.Background() require.NoError(t, s.CreateUser(ctx, &store.User{ - ID: "user-mixed-cap", Email: "mixed-cap@test.com", DisplayName: "Mixed", Role: "member", Status: "active", + ID: tid("user-mixed-cap"), Email: "mixed-cap@test.com", DisplayName: "Mixed", Role: "member", Status: "active", })) // Policy grants read-only on agents policy := &store.Policy{ - ID: "policy-mixed-cap", Name: "Read Only", ScopeType: "hub", + ID: tid("policy-mixed-cap"), Name: "Read Only", ScopeType: "hub", ResourceType: "agent", Actions: []string{"read"}, Effect: "allow", } require.NoError(t, s.CreatePolicy(ctx, policy)) require.NoError(t, s.AddPolicyBinding(ctx, &store.PolicyBinding{ - PolicyID: "policy-mixed-cap", PrincipalType: "user", PrincipalID: "user-mixed-cap", + PolicyID: tid("policy-mixed-cap"), PrincipalType: "user", PrincipalID: tid("user-mixed-cap"), })) - user := NewAuthenticatedUser("user-mixed-cap", "mixed-cap@test.com", "Mixed", "member", "api") + user := NewAuthenticatedUser(tid("user-mixed-cap"), "mixed-cap@test.com", "Mixed", "member", "api") resources := []Resource{ - {Type: "agent", ID: "agent-owned", OwnerID: "user-mixed-cap"}, // Owned - {Type: "agent", ID: "agent-other", OwnerID: "other-user"}, // Not owned + {Type: "agent", ID: "agent-owned", OwnerID: tid("user-mixed-cap")}, // Owned + {Type: "agent", ID: tid("agent-other"), OwnerID: tid("other-user")}, // Not owned } caps := srv.authzService.ComputeCapabilitiesBatch(ctx, user, resources, "agent") @@ -148,15 +148,15 @@ func TestComputeCapabilities_AncestorGetsAllActions(t *testing.T) { ctx := context.Background() require.NoError(t, s.CreateUser(ctx, &store.User{ - ID: "user-ancestor-cap", Email: "ancestor-cap@test.com", DisplayName: "Ancestor", Role: "member", Status: "active", + ID: tid("user-ancestor-cap"), Email: "ancestor-cap@test.com", DisplayName: "Ancestor", Role: "member", Status: "active", })) - user := NewAuthenticatedUser("user-ancestor-cap", "ancestor-cap@test.com", "Ancestor", "member", "api") + user := NewAuthenticatedUser(tid("user-ancestor-cap"), "ancestor-cap@test.com", "Ancestor", "member", "api") resource := Resource{ Type: "agent", ID: "agent-descendant", OwnerID: "someone-else", - Ancestry: []string{"user-ancestor-cap", "agent-middle"}, + Ancestry: []string{tid("user-ancestor-cap"), "agent-middle"}, } caps := srv.authzService.ComputeCapabilities(ctx, user, resource) @@ -168,13 +168,13 @@ func TestComputeCapabilitiesBatch_AncestryAccess(t *testing.T) { ctx := context.Background() require.NoError(t, s.CreateUser(ctx, &store.User{ - ID: "user-batch-ancestor", Email: "batch-ancestor@test.com", DisplayName: "BatchAnc", Role: "member", Status: "active", + ID: tid("user-batch-ancestor"), Email: "batch-ancestor@test.com", DisplayName: "BatchAnc", Role: "member", Status: "active", })) - user := NewAuthenticatedUser("user-batch-ancestor", "batch-ancestor@test.com", "BatchAnc", "member", "api") + user := NewAuthenticatedUser(tid("user-batch-ancestor"), "batch-ancestor@test.com", "BatchAnc", "member", "api") resources := []Resource{ - {Type: "agent", ID: "agent-descendant-1", OwnerID: "other", Ancestry: []string{"user-batch-ancestor", "agent-A"}}, - {Type: "agent", ID: "agent-unrelated", OwnerID: "other", Ancestry: []string{"other-user"}}, + {Type: "agent", ID: "agent-descendant-1", OwnerID: "other", Ancestry: []string{tid("user-batch-ancestor"), "agent-A"}}, + {Type: "agent", ID: "agent-unrelated", OwnerID: "other", Ancestry: []string{tid("other-user")}}, } caps := srv.authzService.ComputeCapabilitiesBatch(ctx, user, resources, "agent") @@ -202,10 +202,10 @@ func TestComputeScopeCapabilities_NoPolicy(t *testing.T) { ctx := context.Background() require.NoError(t, s.CreateUser(ctx, &store.User{ - ID: "user-noscope-cap", Email: "noscope-cap@test.com", DisplayName: "NoScope", Role: "member", Status: "active", + ID: tid("user-noscope-cap"), Email: "noscope-cap@test.com", DisplayName: "NoScope", Role: "member", Status: "active", })) - user := NewAuthenticatedUser("user-noscope-cap", "noscope-cap@test.com", "NoScope", "member", "api") + user := NewAuthenticatedUser(tid("user-noscope-cap"), "noscope-cap@test.com", "NoScope", "member", "api") caps := srv.authzService.ComputeScopeCapabilities(ctx, user, "", "", "agent") assert.Equal(t, []string{}, caps.Actions) } @@ -312,22 +312,22 @@ func TestComputeCapabilitiesBatch_EmptyList(t *testing.T) { func TestResourceBuilders(t *testing.T) { t.Run("agentResource", func(t *testing.T) { - a := &store.Agent{ID: "a1", OwnerID: "u1", ProjectID: "g1", Labels: map[string]string{"env": "prod"}, Ancestry: []string{"u1"}} + a := &store.Agent{ID: "a1", OwnerID: "u1", ProjectID: tid("g1"), Labels: map[string]string{"env": "prod"}, Ancestry: []string{"u1"}} r := agentResource(a) assert.Equal(t, "agent", r.Type) assert.Equal(t, "a1", r.ID) assert.Equal(t, "u1", r.OwnerID) assert.Equal(t, "project", r.ParentType) - assert.Equal(t, "g1", r.ParentID) + assert.Equal(t, tid("g1"), r.ParentID) assert.Equal(t, "prod", r.Labels["env"]) assert.Equal(t, []string{"u1"}, r.Ancestry) }) t.Run("projectResource", func(t *testing.T) { - g := &store.Project{ID: "g1", OwnerID: "u1"} + g := &store.Project{ID: tid("g1"), OwnerID: "u1"} r := projectResource(g) assert.Equal(t, "project", r.Type) - assert.Equal(t, "g1", r.ID) + assert.Equal(t, tid("g1"), r.ID) assert.Equal(t, "u1", r.OwnerID) }) diff --git a/pkg/hub/capability_marshal_test.go b/pkg/hub/capability_marshal_test.go index b893a242e..c6ca5c153 100644 --- a/pkg/hub/capability_marshal_test.go +++ b/pkg/hub/capability_marshal_test.go @@ -26,8 +26,8 @@ import ( func TestAgentWithCapabilities_MarshalJSON(t *testing.T) { agent := AgentWithCapabilities{ Agent: store.Agent{ - ID: "agent-1", - ProjectID: "project-1", + ID: tid("agent-1"), + ProjectID: tid("project-1"), Name: "my-agent", }, Cap: &Capabilities{ @@ -45,8 +45,8 @@ func TestAgentWithCapabilities_MarshalJSON(t *testing.T) { require.NoError(t, err) // Check embedded Agent fields - assert.Equal(t, "agent-1", m["id"]) - assert.Equal(t, "project-1", m["projectId"]) + assert.Equal(t, tid("agent-1"), m["id"]) + assert.Equal(t, tid("project-1"), m["projectId"]) assert.Equal(t, "my-agent", m["name"]) // Check capability fields @@ -56,7 +56,7 @@ func TestAgentWithCapabilities_MarshalJSON(t *testing.T) { assert.Equal(t, true, m["cloudLogging"]) // Check legacy fields - assert.Equal(t, "project-1", m["groveId"]) + assert.Equal(t, tid("project-1"), m["groveId"]) } func TestProjectWithCapabilities_MarshalJSON(t *testing.T) { @@ -64,7 +64,7 @@ func TestProjectWithCapabilities_MarshalJSON(t *testing.T) { Project: store.Project{ ID: "p-1", Name: "Project 1", - Slug: "project-1", + Slug: tid("project-1"), }, Cap: &Capabilities{ Actions: []string{"write"}, @@ -82,7 +82,7 @@ func TestProjectWithCapabilities_MarshalJSON(t *testing.T) { // Check embedded Project fields assert.Equal(t, "p-1", m["id"]) assert.Equal(t, "Project 1", m["name"]) - assert.Equal(t, "project-1", m["slug"]) + assert.Equal(t, tid("project-1"), m["slug"]) // Check capability fields assert.NotNil(t, m["_capabilities"]) @@ -92,7 +92,7 @@ func TestProjectWithCapabilities_MarshalJSON(t *testing.T) { // Check legacy fields assert.Equal(t, "p-1", m["groveId"]) assert.Equal(t, "Project 1", m["groveName"]) - assert.Equal(t, "project-1", m["grove"]) + assert.Equal(t, tid("project-1"), m["grove"]) } func TestTemplateWithCapabilities_MarshalJSON(t *testing.T) { diff --git a/pkg/hub/clone_delete_handler_test.go b/pkg/hub/clone_delete_handler_test.go new file mode 100644 index 000000000..a308cd080 --- /dev/null +++ b/pkg/hub/clone_delete_handler_test.go @@ -0,0 +1,206 @@ +//go:build !no_sqlite + +package hub + +import ( + "context" + "encoding/json" + "net/http" + "testing" + "time" + + "github.com/GoogleCloudPlatform/scion/pkg/api" + "github.com/GoogleCloudPlatform/scion/pkg/store" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +// setupCloneTestServer returns a Server with mock storage, a store, and +// pre-created source harness config and template IDs suitable for clone tests. +func setupCloneTestServer(t *testing.T) (*Server, store.Store, string, string) { + t.Helper() + srv, s := testServer(t) + ctx := context.Background() + + stor := newMockStorage("test-bucket") + srv.SetStorage(stor) + + now := time.Now() + + hcID := api.NewUUID() + tplID := api.NewUUID() + + // Seed a source harness config (global). + require.NoError(t, s.CreateHarnessConfig(ctx, &store.HarnessConfig{ + ID: hcID, Slug: "source-hc", Name: "Source HC", + DisplayName: "Source Display", Description: "Source desc", + Harness: "claude", + Config: &store.HarnessConfigData{Harness: "claude", Image: "img:latest"}, + Scope: store.HarnessConfigScopeGlobal, + Visibility: store.VisibilityPublic, + Status: store.HarnessConfigStatusActive, + Created: now, Updated: now, + })) + + // Seed a source template (global). + require.NoError(t, s.CreateTemplate(ctx, &store.Template{ + ID: tplID, Slug: "source-tpl", Name: "Source Template", + DisplayName: "TPL Display", Description: "TPL desc", + Harness: "claude", + Scope: store.TemplateScopeGlobal, + Visibility: store.VisibilityPublic, + Status: store.TemplateStatusActive, + Created: now, Updated: now, + })) + + return srv, s, hcID, tplID +} + +func TestHandleHarnessConfigClone_Success(t *testing.T) { + srv, _, hcID, _ := setupCloneTestServer(t) + + body := map[string]interface{}{ + "name": "My Clone", + "scope": "global", + "visibility": "private", + } + + rec := doRequest(t, srv, http.MethodPost, "/api/v1/harness-configs/"+hcID+"/clone", body) + require.Equal(t, http.StatusCreated, rec.Code, rec.Body.String()) + + var clone store.HarnessConfig + require.NoError(t, json.NewDecoder(rec.Body).Decode(&clone)) + + assert.NotEqual(t, hcID, clone.ID, "clone must have a new ID") + assert.Equal(t, "my-clone", clone.Slug) + assert.Equal(t, "My Clone", clone.Name) + assert.Equal(t, "Source Display", clone.DisplayName) + assert.Equal(t, "Source desc", clone.Description) + assert.Equal(t, "claude", clone.Harness) + assert.Equal(t, "global", clone.Scope) + assert.Equal(t, "private", clone.Visibility) + assert.NotNil(t, clone.Config) +} + +func TestHandleHarnessConfigClone_CrossScope(t *testing.T) { + srv, s, hcID, _ := setupCloneTestServer(t) + ctx := context.Background() + + projectID := api.NewUUID() + require.NoError(t, s.CreateProject(ctx, &store.Project{ + ID: projectID, Name: "Clone Project", Slug: "clone-project", + OwnerID: DevUserID, CreatedBy: DevUserID, + Created: time.Now(), Updated: time.Now(), + })) + + body := map[string]interface{}{ + "name": "Project Clone", + "scope": "project", + "scopeId": projectID, + } + + rec := doRequest(t, srv, http.MethodPost, "/api/v1/harness-configs/"+hcID+"/clone", body) + require.Equal(t, http.StatusCreated, rec.Code, rec.Body.String()) + + var clone store.HarnessConfig + require.NoError(t, json.NewDecoder(rec.Body).Decode(&clone)) + + assert.Equal(t, "project", clone.Scope) + assert.Equal(t, projectID, clone.ScopeID) + assert.Equal(t, "claude", clone.Harness) +} + +func TestDeleteTemplate_Authz_GlobalForbiddenForMember(t *testing.T) { + srv, s, _, tplID := setupCloneTestServer(t) + ctx := context.Background() + + member := &store.User{ + ID: api.NewUUID(), Email: "member-del@test.com", + DisplayName: "Member", Role: store.UserRoleMember, + Status: "active", Created: time.Now(), + } + require.NoError(t, s.CreateUser(ctx, member)) + ensureHubMembership(ctx, s, member.ID) + + rec := doRequestAsUser(t, srv, member, http.MethodDelete, "/api/v1/templates/"+tplID, nil) + assert.Equal(t, http.StatusForbidden, rec.Code, "non-admin should get 403 on global template delete: %s", rec.Body.String()) +} + +func TestDeleteTemplate_Authz_GlobalAllowedForAdmin(t *testing.T) { + srv, s, _, tplID := setupCloneTestServer(t) + ctx := context.Background() + + admin := &store.User{ + ID: api.NewUUID(), Email: "admin-del@test.com", + DisplayName: "Admin", Role: store.UserRoleAdmin, + Status: "active", Created: time.Now(), + } + require.NoError(t, s.CreateUser(ctx, admin)) + ensureHubMembership(ctx, s, admin.ID) + + rec := doRequestAsUser(t, srv, admin, http.MethodDelete, "/api/v1/templates/"+tplID, nil) + assert.Equal(t, http.StatusNoContent, rec.Code, "admin should be able to delete global template: %s", rec.Body.String()) + + // Verify gone. + rec = doRequestAsUser(t, srv, admin, http.MethodGet, "/api/v1/templates/"+tplID, nil) + assert.Equal(t, http.StatusNotFound, rec.Code) +} + +func TestCloneTemplate_Authz_DestinationChecked(t *testing.T) { + srv, s, _, tplID := setupCloneTestServer(t) + ctx := context.Background() + + alice := &store.User{ + ID: api.NewUUID(), Email: "alice-clone@test.com", + DisplayName: "Alice", Role: store.UserRoleMember, + Status: "active", Created: time.Now(), + } + bob := &store.User{ + ID: api.NewUUID(), Email: "bob-clone@test.com", + DisplayName: "Bob", Role: store.UserRoleMember, + Status: "active", Created: time.Now(), + } + require.NoError(t, s.CreateUser(ctx, alice)) + require.NoError(t, s.CreateUser(ctx, bob)) + ensureHubMembership(ctx, s, alice.ID) + ensureHubMembership(ctx, s, bob.ID) + + project := &store.Project{ + ID: api.NewUUID(), Name: "Authz Project", Slug: "authz-project", + OwnerID: alice.ID, CreatedBy: alice.ID, + Created: time.Now(), Updated: time.Now(), + } + require.NoError(t, s.CreateProject(ctx, project)) + srv.createProjectMembersGroupAndPolicy(ctx, project) + + body := map[string]interface{}{ + "name": "Clone Into Project", + "scope": "project", + "scopeId": project.ID, + } + + // Bob is not a project member → should be forbidden. + rec := doRequestAsUser(t, srv, bob, http.MethodPost, "/api/v1/templates/"+tplID+"/clone", body) + assert.Equal(t, http.StatusForbidden, rec.Code, "non-member should get 403: %s", rec.Body.String()) + + // Alice is the project owner → should succeed. + rec = doRequestAsUser(t, srv, alice, http.MethodPost, "/api/v1/templates/"+tplID+"/clone", body) + assert.Equal(t, http.StatusCreated, rec.Code, "project owner should be able to clone: %s", rec.Body.String()) +} + +func TestClone_SlugCollision_Returns409(t *testing.T) { + srv, _, hcID, _ := setupCloneTestServer(t) + + body := map[string]interface{}{ + "name": "Collision Clone", + "scope": "global", + } + + // First clone succeeds. + rec := doRequest(t, srv, http.MethodPost, "/api/v1/harness-configs/"+hcID+"/clone", body) + require.Equal(t, http.StatusCreated, rec.Code, rec.Body.String()) + + // Second clone with same name → slug collision → 409. + rec = doRequest(t, srv, http.MethodPost, "/api/v1/harness-configs/"+hcID+"/clone", body) + assert.Equal(t, http.StatusConflict, rec.Code, "duplicate slug should return 409: %s", rec.Body.String()) +} diff --git a/pkg/hub/command_bus.go b/pkg/hub/command_bus.go new file mode 100644 index 000000000..bc61a4c56 --- /dev/null +++ b/pkg/hub/command_bus.go @@ -0,0 +1,337 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package hub + +import ( + "context" + "encoding/json" + "errors" + "fmt" + "log/slog" + "sync" + "time" + + "github.com/jackc/pgx/v5" + "github.com/jackc/pgx/v5/pgxpool" +) + +// CommandBus abstracts the inter-node command signal channel. The Postgres +// implementation LISTENs on scion_broker_cmd; the no-op implementation is +// used for SQLite (single-process, all brokers are local). +type CommandBus interface { + // NotifyBrokerCmd issues a NOTIFY signal inside the caller's transaction, + // so the signal commits atomically with the durable intent row. + NotifyBrokerCmd(ctx context.Context, tx pgExecutor, brokerID string) error + // SignalBrokerCmd is a best-effort NOTIFY using the bus's own pool (not + // tx-scoped). Used by the message dispatch path where the durable intent + // is the message row itself and the NOTIFY is only a wakeup hint. + SignalBrokerCmd(ctx context.Context, brokerID string) error + Close() +} + +const ( + // pgCommandChannel is the global Postgres NOTIFY channel for broker + // command signals. Every hub instance LISTENs on this single channel and + // filters by local ownership. + pgCommandChannel = "scion_broker_cmd" +) + +// cmdSignal is the JSON wire format for the NOTIFY payload on scion_broker_cmd. +// It is intentionally tiny: the durable command lives in the DB; this is only +// a wakeup. +type cmdSignal struct { + BrokerID string `json:"broker_id"` + Kind string `json:"kind"` +} + +// PostgresCommandBus is a sibling of PostgresEventPublisher that LISTENs on +// scion_broker_cmd for dispatch wakeup signals. It maintains its OWN pgx +// connection (listener) and pool (publisher) so dispatch and event-fanout are +// independently pooled (design §5.1). +// +// On receiving a signal the bus checks local ownership via the injected +// ownsLocally func: if this node holds the broker's WebSocket, it invokes the +// onSignal callback (which will be wired to the reconcile drain in B2-5). +type PostgresCommandBus struct { + pool *pgxpool.Pool + dsn string + log *slog.Logger + + ctx context.Context + cancel context.CancelFunc + wg sync.WaitGroup + + mu sync.RWMutex + ownsLocally func(brokerID string) bool + onSignal func(ctx context.Context, brokerID string) + onReconnect func() + closed bool +} + +var _ CommandBus = (*PostgresCommandBus)(nil) + +// NewPostgresCommandBus creates a command bus backed by Postgres LISTEN/NOTIFY. +// ownsLocally should return true when this process holds the broker's control- +// channel WebSocket (typically controlChannel.manager.IsConnected). onSignal +// is the reconcile callback invoked when a signal arrives for a locally-owned +// broker. +func NewPostgresCommandBus( + ctx context.Context, + dsn string, + ownsLocally func(brokerID string) bool, + onSignal func(ctx context.Context, brokerID string), + log *slog.Logger, +) (*PostgresCommandBus, error) { + if log == nil { + log = slog.Default() + } + if ownsLocally == nil { + ownsLocally = func(string) bool { return false } + } + if onSignal == nil { + onSignal = func(context.Context, string) {} + } + + poolCfg, err := pgxpool.ParseConfig(dsn) + if err != nil { + return nil, fmt.Errorf("parsing command bus dsn: %w", err) + } + applyEventPoolKeepalives(poolCfg) + + pool, err := pgxpool.NewWithConfig(ctx, poolCfg) + if err != nil { + return nil, fmt.Errorf("creating command bus pool: %w", err) + } + if err := pool.Ping(ctx); err != nil { + pool.Close() + return nil, fmt.Errorf("pinging postgres for command bus: %w", err) + } + + busCtx, cancel := context.WithCancel(context.Background()) + b := &PostgresCommandBus{ + pool: pool, + dsn: dsn, + log: log, + ctx: busCtx, + cancel: cancel, + ownsLocally: ownsLocally, + onSignal: onSignal, + } + + b.wg.Add(1) + go b.runListener() + + log.Info("Postgres command bus started", "channel", pgCommandChannel) + return b, nil +} + +// SetOnReconnect sets a callback invoked each time the listener reconnects +// after a connection loss. Used by B5-2 to increment a reconnects counter. +func (b *PostgresCommandBus) SetOnReconnect(fn func()) { + b.mu.Lock() + defer b.mu.Unlock() + b.onReconnect = fn +} + +// SetOnSignal replaces the reconcile callback. This allows wiring the +// reconcile drain (B2-5) after construction. +func (b *PostgresCommandBus) SetOnSignal(fn func(ctx context.Context, brokerID string)) { + b.mu.Lock() + defer b.mu.Unlock() + if fn == nil { + fn = func(context.Context, string) {} + } + b.onSignal = fn +} + +// NotifyBrokerCmd issues NOTIFY scion_broker_cmd inside the caller's +// transaction, so the signal commits atomically with the durable intent. +func (b *PostgresCommandBus) NotifyBrokerCmd(ctx context.Context, tx pgExecutor, brokerID string) error { + sig := cmdSignal{BrokerID: brokerID, Kind: "dispatch"} + payload, err := json.Marshal(sig) + if err != nil { + return fmt.Errorf("marshaling command signal: %w", err) + } + _, err = tx.Exec(ctx, `SELECT pg_notify($1, $2)`, pgCommandChannel, string(payload)) + if err != nil { + return fmt.Errorf("pg_notify on %s: %w", pgCommandChannel, err) + } + return nil +} + +// SignalBrokerCmd issues a best-effort NOTIFY using the bus's own pool. +func (b *PostgresCommandBus) SignalBrokerCmd(ctx context.Context, brokerID string) error { + return b.NotifyBrokerCmd(ctx, b.pool, brokerID) +} + +// Close stops the listener and releases the pool. +func (b *PostgresCommandBus) Close() { + b.mu.Lock() + if b.closed { + b.mu.Unlock() + return + } + b.closed = true + b.mu.Unlock() + + b.cancel() + b.wg.Wait() + b.pool.Close() +} + +// runListener mirrors PostgresEventPublisher.runListener: maintain a dedicated +// LISTEN connection with backoff-reconnect. +func (b *PostgresCommandBus) runListener() { + defer b.wg.Done() + + const ( + minBackoff = 250 * time.Millisecond + maxBackoff = 10 * time.Second + ) + backoff := minBackoff + firstConnect := true + + for { + if b.ctx.Err() != nil { + return + } + + conn, err := b.connectListener(b.ctx) + if err != nil { + if b.ctx.Err() != nil { + return + } + b.log.Warn("Command bus listener connect failed, retrying", "error", err, "backoff", backoff) + if !b.sleep(backoff) { + return + } + backoff = nextBackoff(backoff, maxBackoff) + continue + } + + if !firstConnect { + b.mu.RLock() + fn := b.onReconnect + b.mu.RUnlock() + if fn != nil { + fn() + } + } + firstConnect = false + b.log.Info("Command bus listener connected") + backoff = minBackoff + + loopErr := b.listenLoop(conn) + conn.Close(context.Background()) + + if b.ctx.Err() != nil { + return + } + + b.log.Warn("Command bus listener connection lost, reconnecting", "error", loopErr, "backoff", backoff) + if !b.sleep(backoff) { + return + } + backoff = nextBackoff(backoff, maxBackoff) + } +} + +// connectListener opens a dedicated LISTEN connection with TCP keepalives, +// reusing the same helper as PostgresEventPublisher. +func (b *PostgresCommandBus) connectListener(ctx context.Context) (*pgx.Conn, error) { + cc, err := pgx.ParseConfig(b.dsn) + if err != nil { + return nil, fmt.Errorf("parsing command bus listener dsn: %w", err) + } + applyConnKeepalives(cc) + return pgx.ConnectConfig(ctx, cc) +} + +// listenLoop LISTENs on scion_broker_cmd and dispatches signals. +func (b *PostgresCommandBus) listenLoop(conn *pgx.Conn) error { + if err := execListen(b.ctx, conn, "LISTEN", pgCommandChannel); err != nil { + return fmt.Errorf("LISTEN %s: %w", pgCommandChannel, err) + } + + for { + if b.ctx.Err() != nil { + return b.ctx.Err() + } + + waitCtx, cancel := context.WithTimeout(b.ctx, listenPollInterval) + notif, err := conn.WaitForNotification(waitCtx) + cancel() + if err != nil { + if errors.Is(err, context.DeadlineExceeded) { + continue + } + return err + } + + b.handleSignal(notif.Payload) + } +} + +// handleSignal decodes a command signal and, if this node owns the broker, +// invokes the reconcile callback. +func (b *PostgresCommandBus) handleSignal(payload string) { + var sig cmdSignal + if err := json.Unmarshal([]byte(payload), &sig); err != nil { + b.log.Error("Failed to decode command signal", "error", err) + return + } + + if sig.BrokerID == "" { + b.log.Warn("Command signal missing broker_id, ignoring") + return + } + + b.mu.RLock() + owns := b.ownsLocally(sig.BrokerID) + onSig := b.onSignal + b.mu.RUnlock() + + if !owns { + return + } + + b.log.Info("Command signal received for local broker, invoking reconcile", + "broker_id", sig.BrokerID, "kind", sig.Kind) + onSig(b.ctx, sig.BrokerID) +} + +// sleep waits for d or until the bus context is canceled. +func (b *PostgresCommandBus) sleep(d time.Duration) bool { + t := time.NewTimer(d) + defer t.Stop() + select { + case <-b.ctx.Done(): + return false + case <-t.C: + return true + } +} + +// --- No-op command bus for SQLite (single-process) --- + +// NoopCommandBus is a no-op CommandBus for the SQLite backend. In single- +// process mode every broker is local; no inter-node signal is needed. +type NoopCommandBus struct{} + +var _ CommandBus = NoopCommandBus{} + +func (NoopCommandBus) NotifyBrokerCmd(context.Context, pgExecutor, string) error { return nil } +func (NoopCommandBus) SignalBrokerCmd(context.Context, string) error { return nil } +func (NoopCommandBus) Close() {} diff --git a/pkg/hub/command_bus_test.go b/pkg/hub/command_bus_test.go new file mode 100644 index 000000000..7b3f2b937 --- /dev/null +++ b/pkg/hub/command_bus_test.go @@ -0,0 +1,477 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//go:build !no_sqlite + +package hub + +import ( + "context" + "encoding/json" + "log/slog" + "sync" + "testing" + "time" + + "github.com/jackc/pgx/v5" +) + +// --- pure unit tests (no database required) --- + +// TestNotifyBrokerCmd_Payload verifies the SQL and JSON shape of a NOTIFY call +// issued by NotifyBrokerCmd, using the same recExec test double as the event +// publisher tests. +func TestNotifyBrokerCmd_Payload(t *testing.T) { + bus := &PostgresCommandBus{ + ctx: context.Background(), + } + + tx := &recExec{} + if err := bus.NotifyBrokerCmd(context.Background(), tx, "broker-123"); err != nil { + t.Fatalf("NotifyBrokerCmd: %v", err) + } + + calls := tx.notifyCalls() + if len(calls) != 1 { + t.Fatalf("expected 1 pg_notify call, got %d", len(calls)) + } + + channel := calls[0].args[0].(string) + if channel != pgCommandChannel { + t.Fatalf("channel = %q, want %q", channel, pgCommandChannel) + } + + var sig cmdSignal + payload := calls[0].args[1].(string) + if err := json.Unmarshal([]byte(payload), &sig); err != nil { + t.Fatalf("decode signal payload: %v", err) + } + if sig.BrokerID != "broker-123" { + t.Fatalf("broker_id = %q, want %q", sig.BrokerID, "broker-123") + } + if sig.Kind != "dispatch" { + t.Fatalf("kind = %q, want %q", sig.Kind, "dispatch") + } +} + +// TestHandleSignal_OwnsLocally verifies that handleSignal invokes the onSignal +// callback only when ownsLocally returns true. +func TestHandleSignal_OwnsLocally(t *testing.T) { + var mu sync.Mutex + var reconciled []string + + bus := &PostgresCommandBus{ + ctx: context.Background(), + log: slog.Default(), + ownsLocally: func(brokerID string) bool { + return brokerID == "local-broker" + }, + onSignal: func(_ context.Context, brokerID string) { + mu.Lock() + defer mu.Unlock() + reconciled = append(reconciled, brokerID) + }, + } + + // Signal for a locally-owned broker -> should invoke callback. + sig1, _ := json.Marshal(cmdSignal{BrokerID: "local-broker", Kind: "dispatch"}) + bus.handleSignal(string(sig1)) + + // Signal for a remote broker -> should be ignored. + sig2, _ := json.Marshal(cmdSignal{BrokerID: "remote-broker", Kind: "dispatch"}) + bus.handleSignal(string(sig2)) + + mu.Lock() + defer mu.Unlock() + if len(reconciled) != 1 { + t.Fatalf("expected 1 reconcile call, got %d", len(reconciled)) + } + if reconciled[0] != "local-broker" { + t.Fatalf("reconciled broker = %q, want %q", reconciled[0], "local-broker") + } +} + +// TestHandleSignal_EmptyBrokerID verifies signals with a missing broker_id are +// silently ignored. +func TestHandleSignal_EmptyBrokerID(t *testing.T) { + called := false + bus := &PostgresCommandBus{ + ctx: context.Background(), + log: slog.Default(), + ownsLocally: func(string) bool { return true }, + onSignal: func(context.Context, string) { called = true }, + } + + sig, _ := json.Marshal(cmdSignal{Kind: "dispatch"}) + bus.handleSignal(string(sig)) + + if called { + t.Fatal("onSignal should not be called for an empty broker_id") + } +} + +// TestHandleSignal_MalformedJSON verifies malformed payloads don't panic. +func TestHandleSignal_MalformedJSON(t *testing.T) { + called := false + bus := &PostgresCommandBus{ + ctx: context.Background(), + log: slog.Default(), + ownsLocally: func(string) bool { return true }, + onSignal: func(context.Context, string) { called = true }, + } + + bus.handleSignal("not valid json{{{") + + if called { + t.Fatal("onSignal should not be called for malformed JSON") + } +} + +// TestSetOnSignal verifies the reconcile callback can be replaced after +// construction. +func TestSetOnSignal(t *testing.T) { + var mu sync.Mutex + var called string + + bus := &PostgresCommandBus{ + ctx: context.Background(), + log: slog.Default(), + ownsLocally: func(string) bool { return true }, + onSignal: func(_ context.Context, id string) { mu.Lock(); called = "original-" + id; mu.Unlock() }, + } + + bus.SetOnSignal(func(_ context.Context, id string) { + mu.Lock() + called = "replaced-" + id + mu.Unlock() + }) + + sig, _ := json.Marshal(cmdSignal{BrokerID: "b1", Kind: "dispatch"}) + bus.handleSignal(string(sig)) + + mu.Lock() + defer mu.Unlock() + if called != "replaced-b1" { + t.Fatalf("called = %q, want %q", called, "replaced-b1") + } +} + +// TestNoopCommandBus_NotifyBrokerCmd verifies the no-op bus is a safe no-op. +func TestNoopCommandBus_NotifyBrokerCmd(t *testing.T) { + bus := NoopCommandBus{} + tx := &recExec{} + + if err := bus.NotifyBrokerCmd(context.Background(), tx, "any-broker"); err != nil { + t.Fatalf("NoopCommandBus.NotifyBrokerCmd: %v", err) + } + + // No SQL should have been issued. + if len(tx.notifyCalls()) != 0 { + t.Fatalf("NoopCommandBus should not issue any SQL, got %d calls", len(tx.notifyCalls())) + } + + // Close is a safe no-op. + bus.Close() +} + +// --- integration tests (require a live Postgres via SCION_TEST_POSTGRES_DSN) --- + +// TestCommandBusIntegration_SignalDelivery starts a real PostgresCommandBus and +// verifies a NOTIFY on scion_broker_cmd is received and invokes the callback. +func TestCommandBusIntegration_SignalDelivery(t *testing.T) { + dsn := requirePostgres(t) + ctx := context.Background() + + var mu sync.Mutex + var reconciled []string + + bus, err := NewPostgresCommandBus(ctx, dsn, + func(brokerID string) bool { return brokerID == "owned-broker" }, + func(_ context.Context, brokerID string) { + mu.Lock() + defer mu.Unlock() + reconciled = append(reconciled, brokerID) + }, + nil, + ) + if err != nil { + t.Fatalf("NewPostgresCommandBus: %v", err) + } + defer bus.Close() + + // Give the listener time to LISTEN. + time.Sleep(2 * listenPollInterval) + + // Publish a signal via a direct NOTIFY (simulating the tx-scoped path). + conn, err := pgx.Connect(ctx, dsn) + if err != nil { + t.Fatalf("connect: %v", err) + } + defer conn.Close(context.Background()) + + sig, _ := json.Marshal(cmdSignal{BrokerID: "owned-broker", Kind: "dispatch"}) + if _, err := conn.Exec(ctx, `SELECT pg_notify($1, $2)`, pgCommandChannel, string(sig)); err != nil { + t.Fatalf("pg_notify: %v", err) + } + + // Also send a signal for a non-owned broker; it should be ignored. + sig2, _ := json.Marshal(cmdSignal{BrokerID: "remote-broker", Kind: "dispatch"}) + if _, err := conn.Exec(ctx, `SELECT pg_notify($1, $2)`, pgCommandChannel, string(sig2)); err != nil { + t.Fatalf("pg_notify: %v", err) + } + + deadline := time.Now().Add(5 * time.Second) + for time.Now().Before(deadline) { + mu.Lock() + n := len(reconciled) + mu.Unlock() + if n >= 1 { + break + } + time.Sleep(100 * time.Millisecond) + } + + mu.Lock() + defer mu.Unlock() + if len(reconciled) != 1 { + t.Fatalf("expected exactly 1 reconcile, got %d: %v", len(reconciled), reconciled) + } + if reconciled[0] != "owned-broker" { + t.Fatalf("reconciled %q, want %q", reconciled[0], "owned-broker") + } +} + +// TestCommandBusIntegration_NotifyBrokerCmd verifies NotifyBrokerCmd publishes a +// signal inside a caller's transaction that is received by the listener. +func TestCommandBusIntegration_NotifyBrokerCmd(t *testing.T) { + dsn := requirePostgres(t) + ctx := context.Background() + + var mu sync.Mutex + var reconciled []string + + bus, err := NewPostgresCommandBus(ctx, dsn, + func(string) bool { return true }, + func(_ context.Context, brokerID string) { + mu.Lock() + defer mu.Unlock() + reconciled = append(reconciled, brokerID) + }, + nil, + ) + if err != nil { + t.Fatalf("NewPostgresCommandBus: %v", err) + } + defer bus.Close() + + time.Sleep(2 * listenPollInterval) + + // Use the bus's own pool to create a transaction. + tx, err := bus.pool.Begin(ctx) + if err != nil { + t.Fatalf("begin tx: %v", err) + } + if err := bus.NotifyBrokerCmd(ctx, tx, "txn-broker"); err != nil { + t.Fatalf("NotifyBrokerCmd: %v", err) + } + if err := tx.Commit(ctx); err != nil { + t.Fatalf("commit: %v", err) + } + + deadline := time.Now().Add(5 * time.Second) + for time.Now().Before(deadline) { + mu.Lock() + n := len(reconciled) + mu.Unlock() + if n >= 1 { + break + } + time.Sleep(100 * time.Millisecond) + } + + mu.Lock() + defer mu.Unlock() + if len(reconciled) != 1 { + t.Fatalf("expected 1 reconcile, got %d", len(reconciled)) + } + if reconciled[0] != "txn-broker" { + t.Fatalf("reconciled %q, want %q", reconciled[0], "txn-broker") + } +} + +// TestCommandBusIntegration_TransactionalRollback verifies that a NOTIFY enrolled +// in a rolled-back transaction is never delivered (mirrors the event publisher's +// transactional rollback test). +func TestCommandBusIntegration_TransactionalRollback(t *testing.T) { + dsn := requirePostgres(t) + ctx := context.Background() + + var mu sync.Mutex + var reconciled []string + + bus, err := NewPostgresCommandBus(ctx, dsn, + func(string) bool { return true }, + func(_ context.Context, brokerID string) { + mu.Lock() + defer mu.Unlock() + reconciled = append(reconciled, brokerID) + }, + nil, + ) + if err != nil { + t.Fatalf("NewPostgresCommandBus: %v", err) + } + defer bus.Close() + + time.Sleep(2 * listenPollInterval) + + // Rolled-back publish: must NOT be delivered. + txRollback, err := bus.pool.Begin(ctx) + if err != nil { + t.Fatalf("begin: %v", err) + } + if err := bus.NotifyBrokerCmd(ctx, txRollback, "rolled-back-broker"); err != nil { + t.Fatalf("NotifyBrokerCmd: %v", err) + } + if err := txRollback.Rollback(ctx); err != nil { + t.Fatalf("rollback: %v", err) + } + + // Wait to ensure no spurious delivery. + time.Sleep(2 * time.Second) + + mu.Lock() + n := len(reconciled) + mu.Unlock() + if n != 0 { + t.Fatalf("rolled-back signal was delivered: %v", reconciled) + } + + // Committed publish: must be delivered. + txCommit, err := bus.pool.Begin(ctx) + if err != nil { + t.Fatalf("begin: %v", err) + } + if err := bus.NotifyBrokerCmd(ctx, txCommit, "committed-broker"); err != nil { + t.Fatalf("NotifyBrokerCmd: %v", err) + } + if err := txCommit.Commit(ctx); err != nil { + t.Fatalf("commit: %v", err) + } + + deadline := time.Now().Add(5 * time.Second) + for time.Now().Before(deadline) { + mu.Lock() + n = len(reconciled) + mu.Unlock() + if n >= 1 { + break + } + time.Sleep(100 * time.Millisecond) + } + + mu.Lock() + defer mu.Unlock() + if len(reconciled) != 1 || reconciled[0] != "committed-broker" { + t.Fatalf("expected [committed-broker], got %v", reconciled) + } +} + +// TestCommandBusIntegration_Reconnect terminates the listener's backend +// connection and verifies the bus reconnects and resumes delivery. +func TestCommandBusIntegration_Reconnect(t *testing.T) { + dsn := requirePostgres(t) + ctx := context.Background() + + var mu sync.Mutex + var reconciled []string + + bus, err := NewPostgresCommandBus(ctx, dsn, + func(string) bool { return true }, + func(_ context.Context, brokerID string) { + mu.Lock() + defer mu.Unlock() + reconciled = append(reconciled, brokerID) + }, + nil, + ) + if err != nil { + t.Fatalf("NewPostgresCommandBus: %v", err) + } + defer bus.Close() + + time.Sleep(2 * listenPollInterval) + + // Forcibly terminate all LISTENing backends for this database. + if _, err := bus.pool.Exec(ctx, + `SELECT pg_terminate_backend(pid) FROM pg_stat_activity + WHERE query ILIKE 'LISTEN %' AND pid <> pg_backend_pid()`); err != nil { + t.Fatalf("terminate backends: %v", err) + } + + // Wait for reconnect + resubscribe. + time.Sleep(3 * time.Second) + + // Publish a signal after reconnect. + conn, err := pgx.Connect(ctx, dsn) + if err != nil { + t.Fatalf("connect: %v", err) + } + defer conn.Close(context.Background()) + + sig, _ := json.Marshal(cmdSignal{BrokerID: "after-reconnect", Kind: "dispatch"}) + if _, err := conn.Exec(ctx, `SELECT pg_notify($1, $2)`, pgCommandChannel, string(sig)); err != nil { + t.Fatalf("pg_notify: %v", err) + } + + deadline := time.Now().Add(5 * time.Second) + for time.Now().Before(deadline) { + mu.Lock() + n := len(reconciled) + mu.Unlock() + if n >= 1 { + break + } + time.Sleep(200 * time.Millisecond) + } + + mu.Lock() + defer mu.Unlock() + if len(reconciled) == 0 { + t.Fatal("expected delivery after reconnect, got none") + } + found := false + for _, id := range reconciled { + if id == "after-reconnect" { + found = true + break + } + } + if !found { + t.Fatalf("expected after-reconnect in reconciled, got %v", reconciled) + } +} + +// TestCommandBusIntegration_CloseIsIdempotent verifies double-close is safe. +func TestCommandBusIntegration_CloseIsIdempotent(t *testing.T) { + dsn := requirePostgres(t) + ctx := context.Background() + + bus, err := NewPostgresCommandBus(ctx, dsn, nil, nil, nil) + if err != nil { + t.Fatalf("NewPostgresCommandBus: %v", err) + } + bus.Close() + bus.Close() // must not panic +} diff --git a/pkg/hub/controlchannel.go b/pkg/hub/controlchannel.go index 83faa1007..3408179c1 100644 --- a/pkg/hub/controlchannel.go +++ b/pkg/hub/controlchannel.go @@ -65,7 +65,7 @@ type ControlChannelManager struct { config ControlChannelConfig log *slog.Logger upgrader websocket.Upgrader - onDisconnect func(brokerID string) + onDisconnect func(brokerID, sessionID string) } // NewControlChannelManager creates a new control channel manager. @@ -86,8 +86,10 @@ func NewControlChannelManager(config ControlChannelConfig, log *slog.Logger) *Co } // SetOnDisconnect sets a callback that is invoked when a broker disconnects. -// The callback is called asynchronously after the connection is removed. -func (m *ControlChannelManager) SetOnDisconnect(fn func(brokerID string)) { +// The callback is called asynchronously after the connection is removed and +// receives the sessionID of the connection that dropped, so the handler can +// compare-and-clear affinity (avoiding the flap clobber race). +func (m *ControlChannelManager) SetOnDisconnect(fn func(brokerID, sessionID string)) { m.mu.Lock() defer m.mu.Unlock() m.onDisconnect = fn @@ -183,10 +185,12 @@ func (s *StreamProxy) Close() { } // HandleUpgrade upgrades an HTTP connection to a WebSocket control channel. -func (m *ControlChannelManager) HandleUpgrade(w http.ResponseWriter, r *http.Request, brokerID string) error { +// It returns the sessionID generated for the new connection so the caller can +// claim broker affinity for this exact session. +func (m *ControlChannelManager) HandleUpgrade(w http.ResponseWriter, r *http.Request, brokerID string) (string, error) { conn, err := m.upgrader.Upgrade(w, r, nil) if err != nil { - return fmt.Errorf("websocket upgrade failed: %w", err) + return "", fmt.Errorf("websocket upgrade failed: %w", err) } wsConn := wsprotocol.NewConnection(conn, wsprotocol.ConnectionConfig{ @@ -232,19 +236,19 @@ func (m *ControlChannelManager) HandleUpgrade(w http.ResponseWriter, r *http.Req if err := wsConn.WriteJSON(connectedMsg); err != nil { m.log.Error("Failed to send connected message", "brokerID", brokerID, "error", err) brokerConn.Close() - m.removeConnection(brokerID) - return err + m.removeConnection(brokerID, sessionID) + return "", err } - return nil + return sessionID, nil } // handleConnection handles messages from a connected broker. func (m *ControlChannelManager) handleConnection(hc *BrokerConnection) { defer func() { hc.Close() - m.removeConnection(hc.brokerID) - m.log.Info("Broker control channel disconnected", "brokerID", hc.brokerID) + m.removeConnection(hc.brokerID, hc.sessionID) + m.log.Info("Broker control channel disconnected", "brokerID", hc.brokerID, "sessionID", hc.sessionID) }() // Set up pong handler @@ -450,16 +454,23 @@ func (m *ControlChannelManager) pingLoop(hc *BrokerConnection) { } } -// removeConnection removes a broker connection from the manager. -func (m *ControlChannelManager) removeConnection(brokerID string) { +// removeConnection removes a broker connection from the manager. It only +// removes (and fires onDisconnect for) the entry if it is still THIS session: +// when a broker flaps, HandleUpgrade replaces the map entry with a newer +// session, and the older connection's teardown must not drop the live socket or +// stamp a spurious disconnect for the session that already moved on. +func (m *ControlChannelManager) removeConnection(brokerID, sessionID string) { m.mu.Lock() - _, existed := m.connections[brokerID] - delete(m.connections, brokerID) + cur, ok := m.connections[brokerID] + existed := ok && cur.sessionID == sessionID + if existed { + delete(m.connections, brokerID) + } cb := m.onDisconnect m.mu.Unlock() if cb != nil && existed { - go cb(brokerID) + go cb(brokerID, sessionID) } } diff --git a/pkg/hub/controlchannel_client.go b/pkg/hub/controlchannel_client.go index 8878a6ec7..7b88dd989 100644 --- a/pkg/hub/controlchannel_client.go +++ b/pkg/hub/controlchannel_client.go @@ -168,6 +168,22 @@ func (c *ControlChannelBrokerClient) RestartAgent(ctx context.Context, brokerID, return err } +// ResetAuthAgent injects a fresh auth token into a running agent via the control channel. +func (c *ControlChannelBrokerClient) ResetAuthAgent(ctx context.Context, brokerID, brokerEndpoint, agentID, projectID, token string) error { + _ = brokerEndpoint + path := fmt.Sprintf("/api/v1/agents/%s/reset-auth", url.PathEscape(agentID)) + query := "" + if projectID != "" { + query = "projectId=" + url.QueryEscape(projectID) + } + body, err := json.Marshal(map[string]string{"token": token}) + if err != nil { + return fmt.Errorf("failed to marshal reset-auth request: %w", err) + } + _, err = c.doRequest(ctx, brokerID, "POST", path, query, body) + return err +} + // DeleteAgent deletes an agent via control channel. func (c *ControlChannelBrokerClient) DeleteAgent(ctx context.Context, brokerID, brokerEndpoint, agentID, projectID string, deleteFiles, removeBranch, softDelete bool, deletedAt time.Time) error { _ = brokerEndpoint @@ -326,9 +342,12 @@ func (c *ControlChannelBrokerClient) ExecAgent(ctx context.Context, brokerID, br return result.Output, result.ExitCode, nil } -func (c *ControlChannelBrokerClient) CleanupProject(ctx context.Context, brokerID, brokerEndpoint, projectSlug string) error { +func (c *ControlChannelBrokerClient) CleanupProject(ctx context.Context, brokerID, brokerEndpoint, projectSlug, projectID string) error { _ = brokerEndpoint path := fmt.Sprintf("/api/v1/projects/%s", url.PathEscape(projectSlug)) + if projectID != "" { + path += "?project_id=" + url.QueryEscape(projectID) + } resp, err := c.doRequest(ctx, brokerID, "DELETE", path, "", nil) if err != nil { return err @@ -456,6 +475,11 @@ type HybridBrokerClient struct { controlChannel *ControlChannelBrokerClient httpClient RuntimeBrokerClient debug bool + // affinity returns the believed owning hub instanceID for a broker and + // whether that owner is alive (last_heartbeat fresh). It is a routing HINT + // only (correctness comes from durable intent + drain); injected so route() + // is unit-testable. Nil means "no affinity info" (treated as no owner). + affinity func(ctx context.Context, brokerID string) (owner string, alive bool) } // NewHybridBrokerClient creates a hybrid client that prefers control channel. @@ -480,60 +504,120 @@ func (c *HybridBrokerClient) CreateAgent(ctx context.Context, brokerID, brokerEn return c.httpClient.CreateAgent(ctx, brokerID, brokerEndpoint, req) } -// StartAgent starts an agent, preferring control channel. +// StartAgent starts an agent, using route() to decide the delivery path. +// routeLocal uses the control-channel tunnel (unchanged fast path), routeHTTP +// falls back to the broker's HTTP endpoint, and routeForward/routeUndeliverable +// return ErrLifecycleDeferred so the caller can write durable intent + wait. func (c *HybridBrokerClient) StartAgent(ctx context.Context, brokerID, brokerEndpoint, agentID, projectID, task, projectPath, projectSlug, harnessConfig string, resolvedEnv map[string]string, resolvedSecrets []ResolvedSecret, inlineConfig *api.ScionConfig, sharedDirs []api.SharedDir, sharedWorkspace bool) (*RemoteAgentResponse, error) { - if c.useControlChannel(brokerID) { + switch c.route(ctx, brokerID, brokerEndpoint) { + case routeLocal: return c.controlChannel.StartAgent(ctx, brokerID, brokerEndpoint, agentID, projectID, task, projectPath, projectSlug, harnessConfig, resolvedEnv, resolvedSecrets, inlineConfig, sharedDirs, sharedWorkspace) + case routeHTTP: + return c.httpClient.StartAgent(ctx, brokerID, brokerEndpoint, agentID, projectID, task, projectPath, projectSlug, harnessConfig, resolvedEnv, resolvedSecrets, inlineConfig, sharedDirs, sharedWorkspace) + default: + return nil, ErrLifecycleDeferred } - return c.httpClient.StartAgent(ctx, brokerID, brokerEndpoint, agentID, projectID, task, projectPath, projectSlug, harnessConfig, resolvedEnv, resolvedSecrets, inlineConfig, sharedDirs, sharedWorkspace) } -// StopAgent stops an agent, preferring control channel. +// StopAgent stops an agent, using route() to decide the delivery path. +// routeLocal uses the control-channel tunnel, routeHTTP falls back to HTTP, +// and routeForward/routeUndeliverable return ErrLifecycleDeferred. func (c *HybridBrokerClient) StopAgent(ctx context.Context, brokerID, brokerEndpoint, agentID, projectID string) error { - if c.useControlChannel(brokerID) { + switch c.route(ctx, brokerID, brokerEndpoint) { + case routeLocal: return c.controlChannel.StopAgent(ctx, brokerID, brokerEndpoint, agentID, projectID) + case routeHTTP: + return c.httpClient.StopAgent(ctx, brokerID, brokerEndpoint, agentID, projectID) + default: + return ErrLifecycleDeferred } - return c.httpClient.StopAgent(ctx, brokerID, brokerEndpoint, agentID, projectID) } -// RestartAgent restarts an agent, preferring control channel. +// RestartAgent restarts an agent, using route() to decide the delivery path. +// routeLocal uses the control-channel tunnel, routeHTTP falls back to HTTP, +// and routeForward/routeUndeliverable return ErrLifecycleDeferred. func (c *HybridBrokerClient) RestartAgent(ctx context.Context, brokerID, brokerEndpoint, agentID, projectID string, resolvedEnv map[string]string) error { - if c.useControlChannel(brokerID) { + switch c.route(ctx, brokerID, brokerEndpoint) { + case routeLocal: return c.controlChannel.RestartAgent(ctx, brokerID, brokerEndpoint, agentID, projectID, resolvedEnv) + case routeHTTP: + return c.httpClient.RestartAgent(ctx, brokerID, brokerEndpoint, agentID, projectID, resolvedEnv) + default: + return ErrLifecycleDeferred + } +} + +// ResetAuthAgent injects a fresh auth token into a running agent, using route() +// to decide the delivery path. +func (c *HybridBrokerClient) ResetAuthAgent(ctx context.Context, brokerID, brokerEndpoint, agentID, projectID, token string) error { + switch c.route(ctx, brokerID, brokerEndpoint) { + case routeLocal: + return c.controlChannel.ResetAuthAgent(ctx, brokerID, brokerEndpoint, agentID, projectID, token) + case routeHTTP: + return c.httpClient.ResetAuthAgent(ctx, brokerID, brokerEndpoint, agentID, projectID, token) + default: + return ErrLifecycleDeferred } - return c.httpClient.RestartAgent(ctx, brokerID, brokerEndpoint, agentID, projectID, resolvedEnv) } -// DeleteAgent deletes an agent, preferring control channel. +// DeleteAgent deletes an agent, using route() to decide the delivery path. +// routeLocal uses the control-channel tunnel, routeHTTP falls back to HTTP, +// and routeForward/routeUndeliverable return ErrLifecycleDeferred. func (c *HybridBrokerClient) DeleteAgent(ctx context.Context, brokerID, brokerEndpoint, agentID, projectID string, deleteFiles, removeBranch, softDelete bool, deletedAt time.Time) error { - if c.useControlChannel(brokerID) { + switch c.route(ctx, brokerID, brokerEndpoint) { + case routeLocal: return c.controlChannel.DeleteAgent(ctx, brokerID, brokerEndpoint, agentID, projectID, deleteFiles, removeBranch, softDelete, deletedAt) + case routeHTTP: + return c.httpClient.DeleteAgent(ctx, brokerID, brokerEndpoint, agentID, projectID, deleteFiles, removeBranch, softDelete, deletedAt) + default: + return ErrLifecycleDeferred } - return c.httpClient.DeleteAgent(ctx, brokerID, brokerEndpoint, agentID, projectID, deleteFiles, removeBranch, softDelete, deletedAt) } -// MessageAgent sends a message to an agent, preferring control channel. +// MessageAgent sends a message to an agent, using route() to decide the +// delivery path (B3-2). routeLocal uses the control-channel tunnel (unchanged +// fast path), routeHTTP falls back to the broker's HTTP endpoint, and +// routeForward/routeUndeliverable return ErrMessageDeferred so the caller +// can emit a NOTIFY wakeup and return 202 (the message row is durable). func (c *HybridBrokerClient) MessageAgent(ctx context.Context, brokerID, brokerEndpoint, agentID, projectID, message string, interrupt bool, structuredMsg *messages.StructuredMessage) error { - if c.useControlChannel(brokerID) { + switch c.route(ctx, brokerID, brokerEndpoint) { + case routeLocal: return c.controlChannel.MessageAgent(ctx, brokerID, brokerEndpoint, agentID, projectID, message, interrupt, structuredMsg) + case routeHTTP: + return c.httpClient.MessageAgent(ctx, brokerID, brokerEndpoint, agentID, projectID, message, interrupt, structuredMsg) + default: + return ErrMessageDeferred } - return c.httpClient.MessageAgent(ctx, brokerID, brokerEndpoint, agentID, projectID, message, interrupt, structuredMsg) } -// CheckAgentPrompt checks if an agent has a non-empty prompt.md file. +// CheckAgentPrompt checks if an agent has a non-empty prompt.md file, using +// route() to decide the delivery path. routeLocal uses the control-channel +// tunnel, routeHTTP falls back to HTTP, and routeForward/routeUndeliverable +// return ErrLifecycleDeferred so the caller can write durable intent + wait. func (c *HybridBrokerClient) CheckAgentPrompt(ctx context.Context, brokerID, brokerEndpoint, agentID, projectID string) (bool, error) { - if c.useControlChannel(brokerID) { + switch c.route(ctx, brokerID, brokerEndpoint) { + case routeLocal: return c.controlChannel.CheckAgentPrompt(ctx, brokerID, brokerEndpoint, agentID, projectID) + case routeHTTP: + return c.httpClient.CheckAgentPrompt(ctx, brokerID, brokerEndpoint, agentID, projectID) + default: + return false, ErrLifecycleDeferred } - return c.httpClient.CheckAgentPrompt(ctx, brokerID, brokerEndpoint, agentID, projectID) } -// CreateAgentWithGather creates an agent with env-gather support, preferring control channel. +// CreateAgentWithGather creates an agent with env-gather support, using route() +// to decide the delivery path. routeLocal uses the control-channel tunnel, +// routeHTTP falls back to HTTP, and routeForward/routeUndeliverable return +// ErrLifecycleDeferred so the caller can write durable intent + wait. func (c *HybridBrokerClient) CreateAgentWithGather(ctx context.Context, brokerID, brokerEndpoint string, req *RemoteCreateAgentRequest) (*RemoteAgentResponse, *RemoteEnvRequirementsResponse, error) { - if c.useControlChannel(brokerID) { + switch c.route(ctx, brokerID, brokerEndpoint) { + case routeLocal: return c.controlChannel.CreateAgentWithGather(ctx, brokerID, brokerEndpoint, req) + case routeHTTP: + return c.httpClient.CreateAgentWithGather(ctx, brokerID, brokerEndpoint, req) + default: + return nil, nil, ErrLifecycleDeferred } - return c.httpClient.CreateAgentWithGather(ctx, brokerID, brokerEndpoint, req) } // GetAgentLogs retrieves agent.log content, preferring control channel. @@ -552,17 +636,24 @@ func (c *HybridBrokerClient) ExecAgent(ctx context.Context, brokerID, brokerEndp return c.httpClient.ExecAgent(ctx, brokerID, brokerEndpoint, agentID, projectID, command, timeout) } -func (c *HybridBrokerClient) CleanupProject(ctx context.Context, brokerID, brokerEndpoint, projectSlug string) error { +func (c *HybridBrokerClient) CleanupProject(ctx context.Context, brokerID, brokerEndpoint, projectSlug, projectID string) error { if c.useControlChannel(brokerID) { - return c.controlChannel.CleanupProject(ctx, brokerID, brokerEndpoint, projectSlug) + return c.controlChannel.CleanupProject(ctx, brokerID, brokerEndpoint, projectSlug, projectID) } - return c.httpClient.CleanupProject(ctx, brokerID, brokerEndpoint, projectSlug) + return c.httpClient.CleanupProject(ctx, brokerID, brokerEndpoint, projectSlug, projectID) } -// FinalizeEnv sends gathered env vars to a broker, preferring control channel. +// FinalizeEnv sends gathered env vars to a broker, using route() to decide the +// delivery path. routeLocal uses the control-channel tunnel, routeHTTP falls +// back to HTTP, and routeForward/routeUndeliverable return ErrLifecycleDeferred +// so the caller can write durable intent + wait. func (c *HybridBrokerClient) FinalizeEnv(ctx context.Context, brokerID, brokerEndpoint, agentID string, env map[string]string) (*RemoteAgentResponse, error) { - if c.useControlChannel(brokerID) { + switch c.route(ctx, brokerID, brokerEndpoint) { + case routeLocal: return c.controlChannel.FinalizeEnv(ctx, brokerID, brokerEndpoint, agentID, env) + case routeHTTP: + return c.httpClient.FinalizeEnv(ctx, brokerID, brokerEndpoint, agentID, env) + default: + return nil, ErrLifecycleDeferred } - return c.httpClient.FinalizeEnv(ctx, brokerID, brokerEndpoint, agentID, env) } diff --git a/pkg/hub/controlchannel_test.go b/pkg/hub/controlchannel_test.go index 71b19ce3b..8e86380f9 100644 --- a/pkg/hub/controlchannel_test.go +++ b/pkg/hub/controlchannel_test.go @@ -29,21 +29,23 @@ func TestControlChannelManager_OnDisconnectCallback(t *testing.T) { var mu sync.Mutex var receivedBrokerID string + var receivedSessionID string done := make(chan struct{}) - mgr.SetOnDisconnect(func(brokerID string) { + mgr.SetOnDisconnect(func(brokerID, sessionID string) { mu.Lock() defer mu.Unlock() receivedBrokerID = brokerID + receivedSessionID = sessionID close(done) }) // Manually add a connection entry so removeConnection has something to remove mgr.mu.Lock() - mgr.connections["broker-1"] = &BrokerConnection{brokerID: "broker-1"} + mgr.connections[tid("broker-1")] = &BrokerConnection{brokerID: tid("broker-1"), sessionID: "sess-1"} mgr.mu.Unlock() - mgr.removeConnection("broker-1") + mgr.removeConnection(tid("broker-1"), "sess-1") // Wait for async callback select { @@ -54,10 +56,43 @@ func TestControlChannelManager_OnDisconnectCallback(t *testing.T) { mu.Lock() defer mu.Unlock() - assert.Equal(t, "broker-1", receivedBrokerID) + assert.Equal(t, tid("broker-1"), receivedBrokerID) + assert.Equal(t, "sess-1", receivedSessionID) // Verify connection was removed - require.False(t, mgr.IsConnected("broker-1")) + require.False(t, mgr.IsConnected(tid("broker-1"))) +} + +// TestControlChannelManager_RemoveStaleSessionNoop verifies that a teardown for +// an OLD session does not remove a NEWER connection that replaced it (flap), and +// does not fire onDisconnect for the stale session. +func TestControlChannelManager_RemoveStaleSessionNoop(t *testing.T) { + mgr := NewControlChannelManager(DefaultControlChannelConfig(), slog.Default()) + + var fired bool + var mu sync.Mutex + mgr.SetOnDisconnect(func(brokerID, sessionID string) { + mu.Lock() + defer mu.Unlock() + fired = true + }) + + // Current live connection is session "new". + mgr.mu.Lock() + mgr.connections[tid("broker-1")] = &BrokerConnection{brokerID: tid("broker-1"), sessionID: "new"} + mgr.mu.Unlock() + + // The old session's teardown must be a no-op. + mgr.removeConnection(tid("broker-1"), "old") + + // Give any (erroneous) async callback a chance to run. + time.Sleep(100 * time.Millisecond) + + mu.Lock() + assert.False(t, fired, "onDisconnect must not fire for a stale session") + mu.Unlock() + // The live (new) connection must still be present. + require.True(t, mgr.IsConnected(tid("broker-1"))) } func TestControlChannelManager_OnDisconnectCallback_NilSafe(t *testing.T) { @@ -65,11 +100,11 @@ func TestControlChannelManager_OnDisconnectCallback_NilSafe(t *testing.T) { // Don't set any callback - verify removeConnection doesn't panic mgr.mu.Lock() - mgr.connections["broker-2"] = &BrokerConnection{brokerID: "broker-2"} + mgr.connections[tid("broker-2")] = &BrokerConnection{brokerID: tid("broker-2"), sessionID: "sess-2"} mgr.mu.Unlock() // This should not panic - mgr.removeConnection("broker-2") + mgr.removeConnection(tid("broker-2"), "sess-2") - require.False(t, mgr.IsConnected("broker-2")) + require.False(t, mgr.IsConnected(tid("broker-2"))) } diff --git a/pkg/hub/demo_policy_test.go b/pkg/hub/demo_policy_test.go index db461fecd..978a13fd1 100644 --- a/pkg/hub/demo_policy_test.go +++ b/pkg/hub/demo_policy_test.go @@ -68,7 +68,7 @@ func setupDemoPolicyTest(t *testing.T) (*Server, store.Store, *store.User, *stor // Create users alice := &store.User{ - ID: "user-alice", + ID: tid("user-alice"), Email: "alice@test.com", DisplayName: "Alice", Role: store.UserRoleMember, @@ -78,7 +78,7 @@ func setupDemoPolicyTest(t *testing.T) (*Server, store.Store, *store.User, *stor require.NoError(t, s.CreateUser(ctx, alice)) bob := &store.User{ - ID: "user-bob", + ID: tid("user-bob"), Email: "bob@test.com", DisplayName: "Bob", Role: store.UserRoleMember, @@ -93,7 +93,7 @@ func setupDemoPolicyTest(t *testing.T) (*Server, store.Store, *store.User, *stor // Create a project owned by alice project := &store.Project{ - ID: "project-demo", + ID: tid("project-demo"), Name: "Demo Project", Slug: "demo-project", OwnerID: alice.ID, @@ -145,7 +145,7 @@ func TestDemoPolicy_AgentCreate_AdminBypass(t *testing.T) { // Create an admin user (not a project member) admin := &store.User{ - ID: "user-admin", + ID: tid("user-admin"), Email: "admin@test.com", DisplayName: "Admin", Role: store.UserRoleAdmin, @@ -173,8 +173,8 @@ func TestDemoPolicy_AgentDelete_OwnerAllowed(t *testing.T) { // Create an agent owned by alice agent := &store.Agent{ - ID: "agent-del-owner", - Slug: "agent-del-owner", + ID: tid("agent-del-owner"), + Slug: tid("agent-del-owner"), Name: "Agent to Delete", ProjectID: project.ID, OwnerID: alice.ID, @@ -199,8 +199,8 @@ func TestDemoPolicy_AgentDelete_NonOwnerDenied(t *testing.T) { // Create an agent owned by alice agent := &store.Agent{ - ID: "agent-del-nonowner", - Slug: "agent-del-nonowner", + ID: tid("agent-del-nonowner"), + Slug: tid("agent-del-nonowner"), Name: "Agent to Delete", ProjectID: project.ID, OwnerID: alice.ID, @@ -224,7 +224,7 @@ func TestDemoPolicy_AgentDelete_AdminBypass(t *testing.T) { ctx := context.Background() admin := &store.User{ - ID: "user-admin-del", + ID: tid("user-admin-del"), Email: "admin-del@test.com", DisplayName: "Admin", Role: store.UserRoleAdmin, @@ -234,8 +234,8 @@ func TestDemoPolicy_AgentDelete_AdminBypass(t *testing.T) { require.NoError(t, s.CreateUser(ctx, admin)) agent := &store.Agent{ - ID: "agent-del-admin", - Slug: "agent-del-admin", + ID: tid("agent-del-admin"), + Slug: tid("agent-del-admin"), Name: "Agent for Admin Delete", ProjectID: project.ID, OwnerID: alice.ID, @@ -259,8 +259,8 @@ func TestDemoPolicy_AgentDelete_DirectPath_NonOwnerDenied(t *testing.T) { ctx := context.Background() agent := &store.Agent{ - ID: "agent-del-direct", - Slug: "agent-del-direct", + ID: tid("agent-del-direct"), + Slug: tid("agent-del-direct"), Name: "Agent Direct Delete", ProjectID: project.ID, OwnerID: alice.ID, @@ -288,8 +288,8 @@ func TestDemoPolicy_AgentAction_OwnerAllowed(t *testing.T) { ctx := context.Background() agent := &store.Agent{ - ID: "agent-action-owner", - Slug: "agent-action-owner", + ID: tid("agent-action-owner"), + Slug: tid("agent-action-owner"), Name: "Agent Action Test", ProjectID: project.ID, OwnerID: alice.ID, @@ -318,8 +318,8 @@ func TestDemoPolicy_AgentAction_NonOwnerDenied(t *testing.T) { ctx := context.Background() agent := &store.Agent{ - ID: "agent-action-nonowner", - Slug: "agent-action-nonowner", + ID: tid("agent-action-nonowner"), + Slug: tid("agent-action-nonowner"), Name: "Agent Action Test", ProjectID: project.ID, OwnerID: alice.ID, @@ -347,7 +347,7 @@ func TestDemoPolicy_AgentAction_AdminBypass(t *testing.T) { ctx := context.Background() admin := &store.User{ - ID: "user-admin-action", + ID: tid("user-admin-action"), Email: "admin-action@test.com", DisplayName: "Admin", Role: store.UserRoleAdmin, @@ -357,8 +357,8 @@ func TestDemoPolicy_AgentAction_AdminBypass(t *testing.T) { require.NoError(t, s.CreateUser(ctx, admin)) agent := &store.Agent{ - ID: "agent-action-admin", - Slug: "agent-action-admin", + ID: tid("agent-action-admin"), + Slug: tid("agent-action-admin"), Name: "Agent Admin Action", ProjectID: project.ID, OwnerID: alice.ID, @@ -382,8 +382,8 @@ func TestDemoPolicy_AgentAction_DirectPath_NonOwnerDenied(t *testing.T) { ctx := context.Background() agent := &store.Agent{ - ID: "agent-action-direct", - Slug: "agent-action-direct", + ID: tid("agent-action-direct"), + Slug: tid("agent-action-direct"), Name: "Agent Direct Action", ProjectID: project.ID, OwnerID: alice.ID, @@ -466,7 +466,7 @@ func TestDemoPolicy_EndToEnd_ProjectCreatorCanCreateAgent(t *testing.T) { // Create a non-admin user alice := &store.User{ - ID: "user-e2e-alice", + ID: tid("user-e2e-alice"), Email: "e2e-alice@test.com", DisplayName: "E2E Alice", Role: store.UserRoleMember, @@ -504,7 +504,7 @@ func TestDemoPolicy_HubMembershipOnLogin(t *testing.T) { // Create a user and add to hub-members (simulating login) user := &store.User{ - ID: "user-login-test", + ID: tid("user-login-test"), Email: "login@test.com", DisplayName: "Login User", Role: store.UserRoleMember, @@ -535,7 +535,7 @@ func TestDemoPolicy_ProjectRecreation_CreatorCanCreateAgent(t *testing.T) { ctx := context.Background() alice := &store.User{ - ID: "user-recreate-alice", + ID: tid("user-recreate-alice"), Email: "recreate-alice@test.com", DisplayName: "Alice", Role: store.UserRoleMember, @@ -593,7 +593,7 @@ func TestDemoPolicy_ProjectMembersGroupIdempotent(t *testing.T) { ctx := context.Background() alice := &store.User{ - ID: "user-idempotent-alice", + ID: tid("user-idempotent-alice"), Email: "idempotent-alice@test.com", DisplayName: "Alice", Role: store.UserRoleMember, @@ -604,7 +604,7 @@ func TestDemoPolicy_ProjectMembersGroupIdempotent(t *testing.T) { ensureHubMembership(ctx, s, alice.ID) project := &store.Project{ - ID: "project-idempotent", + ID: tid("project-idempotent"), Name: "Idempotent Project", Slug: "idempotent-project", OwnerID: alice.ID, @@ -641,7 +641,7 @@ func TestDemoPolicy_ProjectDeleteCleansUpGroupsAndPolicies(t *testing.T) { ctx := context.Background() alice := &store.User{ - ID: "user-cleanup-alice", + ID: tid("user-cleanup-alice"), Email: "cleanup-alice@test.com", DisplayName: "Alice", Role: store.UserRoleMember, diff --git a/pkg/hub/discord_link.go b/pkg/hub/discord_link.go new file mode 100644 index 000000000..b0f67caf3 --- /dev/null +++ b/pkg/hub/discord_link.go @@ -0,0 +1,364 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package hub + +import ( + "log/slog" + "net" + "net/http" + "strings" + "sync" + "time" +) + +const discordLinkCodeTTL = 15 * time.Minute + +// discordPendingLink holds state for a pending Discord account linking. +type discordPendingLink struct { + Code string + DiscordUserID string + ExpiresAt time.Time + Status string // "pending", "confirmed" + UserID string + UserEmail string +} + +// DiscordLinkService manages pending Discord account link codes. +type DiscordLinkService struct { + mu sync.Mutex + pending map[string]*discordPendingLink // code → pending link + + verifyMu sync.Mutex + verifyLimiters map[string]*tokenBucket // IP → token bucket + + closeOnce sync.Once + done chan struct{} +} + +// NewDiscordLinkService creates a new DiscordLinkService and starts +// a background goroutine that periodically removes expired entries. +func NewDiscordLinkService() *DiscordLinkService { + s := &DiscordLinkService{ + pending: make(map[string]*discordPendingLink), + verifyLimiters: make(map[string]*tokenBucket), + done: make(chan struct{}), + } + go s.cleanupLoop() + return s +} + +// RegisterCode stores a pending link code from the Discord plugin. +func (s *DiscordLinkService) RegisterCode(code, discordUserID string) { + s.mu.Lock() + defer s.mu.Unlock() + + // Remove any existing pending code for this discord user. + for c, p := range s.pending { + if p.DiscordUserID == discordUserID { + delete(s.pending, c) + } + } + + s.pending[strings.ToUpper(code)] = &discordPendingLink{ + Code: strings.ToUpper(code), + DiscordUserID: discordUserID, + ExpiresAt: time.Now().Add(discordLinkCodeTTL), + Status: "pending", + } +} + +// VerifyCode attempts to confirm a pending link code with the given user. +// Returns the discordUserID on success, or empty string with a reason. +func (s *DiscordLinkService) VerifyCode(code, userID, userEmail string) (discordUserID string, err string) { + s.mu.Lock() + defer s.mu.Unlock() + + p, ok := s.pending[strings.ToUpper(code)] + if !ok { + return "", "code_not_found" + } + if time.Now().After(p.ExpiresAt) { + delete(s.pending, strings.ToUpper(code)) + return "", "code_expired" + } + if p.Status == "confirmed" { + return p.DiscordUserID, "" + } + + p.Status = "confirmed" + p.UserID = userID + p.UserEmail = userEmail + return p.DiscordUserID, "" +} + +// GetStatusByDiscordUser returns the linking status for a given Discord user ID. +func (s *DiscordLinkService) GetStatusByDiscordUser(discordUserID string) (status, userID, userEmail string) { + s.mu.Lock() + defer s.mu.Unlock() + + for _, p := range s.pending { + if p.DiscordUserID == discordUserID { + if time.Now().After(p.ExpiresAt) { + return "expired", "", "" + } + return p.Status, p.UserID, p.UserEmail + } + } + return "not_found", "", "" +} + +// ConsumePending removes a confirmed entry so it isn't returned again. +func (s *DiscordLinkService) ConsumePending(discordUserID string) { + s.mu.Lock() + defer s.mu.Unlock() + + for code, p := range s.pending { + if p.DiscordUserID == discordUserID { + delete(s.pending, code) + return + } + } +} + +// AllowVerify checks whether the given IP is within the verify rate limit. +func (s *DiscordLinkService) AllowVerify(ip string) bool { + s.verifyMu.Lock() + defer s.verifyMu.Unlock() + + now := time.Now() + b, ok := s.verifyLimiters[ip] + if !ok { + b = &tokenBucket{ + tokens: float64(verifyBurst) - 1, // consume one token + lastCheck: now, + } + s.verifyLimiters[ip] = b + return true + } + + // Refill tokens based on elapsed time. + elapsed := now.Sub(b.lastCheck).Seconds() + b.tokens += elapsed * verifyRatePerSecond + if b.tokens > float64(verifyBurst) { + b.tokens = float64(verifyBurst) + } + b.lastCheck = now + + if b.tokens >= 1 { + b.tokens-- + return true + } + return false +} + +// Close stops the background cleanup goroutine. +func (s *DiscordLinkService) Close() { + s.closeOnce.Do(func() { close(s.done) }) +} + +func (s *DiscordLinkService) cleanupLoop() { + ticker := time.NewTicker(1 * time.Minute) + defer ticker.Stop() + + for { + select { + case <-s.done: + return + case <-ticker.C: + now := time.Now() + + s.mu.Lock() + for code, p := range s.pending { + if now.After(p.ExpiresAt) { + delete(s.pending, code) + } + } + s.mu.Unlock() + + // Clean up stale verify rate limiter entries. + s.verifyMu.Lock() + cutoff := now.Add(-30 * time.Minute) + for ip, b := range s.verifyLimiters { + if b.lastCheck.Before(cutoff) { + delete(s.verifyLimiters, ip) + } + } + s.verifyMu.Unlock() + } + } +} + +// handleDiscordLink handles POST /api/v1/discord/link. +// This is called by the Discord plugin (broker-authenticated) to register a pending link code. +func (s *Server) handleDiscordLink(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodPost { + MethodNotAllowed(w) + return + } + + broker := GetBrokerIdentityFromContext(r.Context()) + if broker == nil { + writeError(w, http.StatusUnauthorized, ErrCodeUnauthorized, "broker authentication required", nil) + return + } + + var req struct { + Code string `json:"code"` + DiscordUserID string `json:"discordUserId"` + } + if err := readJSON(r, &req); err != nil { + writeError(w, http.StatusBadRequest, ErrCodeInvalidRequest, "invalid request body", nil) + return + } + + if req.Code == "" || req.DiscordUserID == "" { + writeError(w, http.StatusBadRequest, ErrCodeInvalidRequest, "code and discordUserId are required", nil) + return + } + + if s.discordLinkService == nil { + InternalError(w) + return + } + + s.discordLinkService.RegisterCode(req.Code, req.DiscordUserID) + + slog.Info("Discord link code registered", + "code_prefix", req.Code[:3]+"***", + "discord_user_id", req.DiscordUserID, + "broker_id", broker.BrokerID(), + ) + + writeJSON(w, http.StatusCreated, map[string]string{"status": "registered"}) +} + +// handleDiscordLinkVerify handles POST /api/v1/discord/link/verify. +// This is called by a logged-in user from the web UI to confirm a link code. +func (s *Server) handleDiscordLinkVerify(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodPost { + MethodNotAllowed(w) + return + } + + user := GetUserIdentityFromContext(r.Context()) + if user == nil { + writeError(w, http.StatusUnauthorized, ErrCodeUnauthorized, "authentication required", nil) + return + } + + // Rate limit by client IP to prevent brute-force attacks on link codes. + if s.discordLinkService != nil { + ip, _, err := net.SplitHostPort(r.RemoteAddr) + if err != nil { + ip = r.RemoteAddr // fallback if no port + } + if !s.discordLinkService.AllowVerify(ip) { + writeError(w, http.StatusTooManyRequests, ErrCodeRateLimited, "too many verify attempts, try again later", nil) + return + } + } + + var req struct { + Code string `json:"code"` + } + if err := readJSON(r, &req); err != nil { + writeError(w, http.StatusBadRequest, ErrCodeInvalidRequest, "invalid request body", nil) + return + } + + if req.Code == "" { + writeError(w, http.StatusBadRequest, ErrCodeInvalidRequest, "code is required", nil) + return + } + + if s.discordLinkService == nil { + InternalError(w) + return + } + + discordUserID, errReason := s.discordLinkService.VerifyCode(req.Code, user.ID(), user.Email()) + if errReason != "" { + switch errReason { + case "code_not_found": + writeError(w, http.StatusNotFound, ErrCodeNotFound, "code not found or expired", nil) + case "code_expired": + writeError(w, http.StatusGone, ErrCodeNotFound, "code has expired", nil) + default: + InternalError(w) + } + return + } + + slog.Info("Discord account linked", + "discord_user_id", discordUserID, + "user_id", user.ID(), + "user_email", user.Email(), + ) + + writeJSON(w, http.StatusOK, map[string]interface{}{ + "status": "confirmed", + "discordUserId": discordUserID, + "user": map[string]string{ + "id": user.ID(), + "email": user.Email(), + }, + }) +} + +// handleDiscordLinkStatus handles GET /api/v1/discord/link/status. +// This is called by the Discord plugin (broker-authenticated) to poll for confirmation. +func (s *Server) handleDiscordLinkStatus(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodGet { + MethodNotAllowed(w) + return + } + + broker := GetBrokerIdentityFromContext(r.Context()) + if broker == nil { + writeError(w, http.StatusUnauthorized, ErrCodeUnauthorized, "broker authentication required", nil) + return + } + + discordUserID := r.URL.Query().Get("discord_user_id") + if discordUserID == "" { + writeError(w, http.StatusBadRequest, ErrCodeInvalidRequest, "discord_user_id query parameter is required", nil) + return + } + + if s.discordLinkService == nil { + InternalError(w) + return + } + + status, userID, userEmail := s.discordLinkService.GetStatusByDiscordUser(discordUserID) + + resp := map[string]interface{}{ + "status": status, + } + if status == "confirmed" { + resp["user"] = map[string]string{ + "id": userID, + "email": userEmail, + } + } + + writeJSON(w, http.StatusOK, resp) + + // Clean up confirmed entries after sending the response so the + // Discord plugin receives the confirmation exactly once. + if status == "confirmed" { + s.discordLinkService.ConsumePending(discordUserID) + } +} diff --git a/pkg/hub/dispatch_args.go b/pkg/hub/dispatch_args.go new file mode 100644 index 000000000..f595fd5df --- /dev/null +++ b/pkg/hub/dispatch_args.go @@ -0,0 +1,111 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package hub + +import ( + "encoding/json" + "time" +) + +// StartDispatchArgs carries the parameters for a cross-node agent start. +// Only fields that the owner's DispatchAgentStart cannot re-derive are +// included. Env/secret resolution is performed by the OWNER via +// DispatchAgentStart (all hub instances share the same store + secret +// backend), so resolved env/secrets are NOT serialized here. +type StartDispatchArgs struct { + Task string `json:"task,omitempty"` +} + +// RestartDispatchArgs is intentionally empty — the owner's +// DispatchAgentRestart re-resolves auth tokens and identity vars from the +// shared store on the owning node. +type RestartDispatchArgs struct{} + +// StopDispatchArgs is intentionally empty — a stop needs no additional params +// beyond what the dispatch row already carries (agentID, projectID). +type StopDispatchArgs struct{} + +// DeleteDispatchArgs carries the parameters for a cross-node agent delete. +type DeleteDispatchArgs struct { + DeleteFiles bool `json:"deleteFiles,omitempty"` + RemoveBranch bool `json:"removeBranch,omitempty"` + SoftDelete bool `json:"softDelete,omitempty"` + DeletedAt time.Time `json:"deletedAt,omitempty"` +} + +// CheckPromptDispatchArgs is intentionally empty — the agent slug/ID in the +// dispatch row is sufficient for the owner to run the local check. +type CheckPromptDispatchArgs struct{} + +// FinalizeEnvDispatchArgs carries the gathered env vars for cross-node finalize. +type FinalizeEnvDispatchArgs struct { + Env map[string]string `json:"env,omitempty"` +} + +// CreateWithGatherDispatchArgs is intentionally empty — the owner rebuilds the +// full RemoteCreateAgentRequest from the shared store (same pattern as start). +type CreateWithGatherDispatchArgs struct{} + +// CheckPromptResult is serialized into broker_dispatch.result by the owner. +type CheckPromptResult struct { + HasPrompt bool `json:"hasPrompt"` +} + +// FinalizeEnvResult is serialized into broker_dispatch.result by the owner. +type FinalizeEnvResult struct { + Success bool `json:"success"` +} + +// CreateWithGatherResult is serialized into broker_dispatch.result by the owner. +type CreateWithGatherResult struct { + EnvRequirements *RemoteEnvRequirementsResponse `json:"envRequirements,omitempty"` +} + +// MarshalDispatchArgs serializes a dispatch args struct to JSON for storage in +// broker_dispatch.args. +func MarshalDispatchArgs(v interface{}) (string, error) { + b, err := json.Marshal(v) + if err != nil { + return "", err + } + return string(b), nil +} + +// UnmarshalStartArgs deserializes start dispatch args from the broker_dispatch row. +func UnmarshalStartArgs(raw string) (*StartDispatchArgs, error) { + var a StartDispatchArgs + if err := json.Unmarshal([]byte(raw), &a); err != nil { + return nil, err + } + return &a, nil +} + +// UnmarshalDeleteArgs deserializes delete dispatch args from the broker_dispatch row. +func UnmarshalDeleteArgs(raw string) (*DeleteDispatchArgs, error) { + var a DeleteDispatchArgs + if err := json.Unmarshal([]byte(raw), &a); err != nil { + return nil, err + } + return &a, nil +} + +// UnmarshalFinalizeEnvArgs deserializes finalize_env dispatch args. +func UnmarshalFinalizeEnvArgs(raw string) (*FinalizeEnvDispatchArgs, error) { + var a FinalizeEnvDispatchArgs + if err := json.Unmarshal([]byte(raw), &a); err != nil { + return nil, err + } + return &a, nil +} diff --git a/pkg/hub/dispatch_exec_test.go b/pkg/hub/dispatch_exec_test.go new file mode 100644 index 000000000..6da887b1f --- /dev/null +++ b/pkg/hub/dispatch_exec_test.go @@ -0,0 +1,716 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//go:build !no_sqlite + +package hub + +import ( + "context" + "encoding/json" + "log/slog" + "sync/atomic" + "testing" + "time" + + "github.com/GoogleCloudPlatform/scion/pkg/api" + "github.com/GoogleCloudPlatform/scion/pkg/messages" + "github.com/GoogleCloudPlatform/scion/pkg/store" + "github.com/GoogleCloudPlatform/scion/pkg/store/entadapter" + "github.com/GoogleCloudPlatform/scion/pkg/store/enttest" + "github.com/google/uuid" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +// lifecycleTestDispatcher captures which lifecycle op was called and with +// what args, so we can verify executeDispatch routes correctly. +type lifecycleTestDispatcher struct { + startCalled atomic.Int32 + stopCalled atomic.Int32 + restartCalled atomic.Int32 + deleteCalled atomic.Int32 + checkPromptCalled atomic.Int32 + finalizeEnvCalled atomic.Int32 + createCalled atomic.Int32 + lastTask string + checkPromptResult bool + lastDeleteFiles bool + lastFinalizeEnv map[string]string +} + +func (d *lifecycleTestDispatcher) DispatchAgentCreate(context.Context, *store.Agent) error { + return nil +} +func (d *lifecycleTestDispatcher) DispatchAgentProvision(context.Context, *store.Agent) error { + return nil +} +func (d *lifecycleTestDispatcher) DispatchAgentStart(_ context.Context, _ *store.Agent, task string) error { + d.startCalled.Add(1) + d.lastTask = task + return nil +} +func (d *lifecycleTestDispatcher) DispatchAgentStop(_ context.Context, _ *store.Agent) error { + d.stopCalled.Add(1) + return nil +} +func (d *lifecycleTestDispatcher) DispatchAgentRestart(_ context.Context, _ *store.Agent) error { + d.restartCalled.Add(1) + return nil +} +func (d *lifecycleTestDispatcher) DispatchAgentResetAuth(_ context.Context, _ *store.Agent) error { + return nil +} +func (d *lifecycleTestDispatcher) DispatchAgentDelete(_ context.Context, _ *store.Agent, deleteFiles, _, _ bool, _ time.Time) error { + d.deleteCalled.Add(1) + d.lastDeleteFiles = deleteFiles + return nil +} +func (d *lifecycleTestDispatcher) DispatchAgentMessage(_ context.Context, _ *store.Agent, _ string, _ bool, _ *messages.StructuredMessage) error { + return nil +} +func (d *lifecycleTestDispatcher) DispatchAgentLogs(context.Context, *store.Agent, int) (string, error) { + return "", nil +} +func (d *lifecycleTestDispatcher) DispatchAgentExec(context.Context, *store.Agent, []string, int) (string, int, error) { + return "", 0, nil +} +func (d *lifecycleTestDispatcher) DispatchCheckAgentPrompt(context.Context, *store.Agent) (bool, error) { + d.checkPromptCalled.Add(1) + return d.checkPromptResult, nil +} +func (d *lifecycleTestDispatcher) DispatchAgentCreateWithGather(context.Context, *store.Agent) (*RemoteEnvRequirementsResponse, error) { + d.createCalled.Add(1) + return nil, nil +} +func (d *lifecycleTestDispatcher) DispatchFinalizeEnv(_ context.Context, _ *store.Agent, env map[string]string) error { + d.finalizeEnvCalled.Add(1) + d.lastFinalizeEnv = env + return nil +} + +func newLifecycleTestServer(t *testing.T) (*Server, *lifecycleTestDispatcher, store.Store) { + t.Helper() + client := enttest.NewClient(t) + cs := entadapter.NewCompositeStore(client) + disp := &lifecycleTestDispatcher{} + events := NewChannelEventPublisher() + t.Cleanup(func() { events.Close() }) + srv := &Server{ + store: cs, + instanceID: "hub-test-" + uuid.NewString()[:8], + agentLifecycleLog: slog.Default(), + events: events, + } + srv.SetDispatcher(disp) + srv.execDispatch = srv.executeDispatch + srv.deliverMsg = srv.deliverMessage + return srv, disp, cs +} + +// seedAgent creates a project + runtime broker + agent and returns the agent. +// The broker has no endpoint (simulates a NAT'd control-channel-only broker). +func seedAgent(t *testing.T, cs store.Store) *store.Agent { + return seedAgentWithBrokerID(t, cs, uuid.NewString()) +} + +func seedAgentWithBrokerID(t *testing.T, cs store.Store, brokerID string) *store.Agent { + t.Helper() + ctx := context.Background() + proj := &store.Project{ + ID: uuid.NewString(), + Name: "test-proj", + Slug: "tp-" + uuid.NewString()[:8], + Visibility: store.VisibilityPrivate, + OwnerID: uuid.NewString(), + } + require.NoError(t, cs.CreateProject(ctx, proj)) + broker := &store.RuntimeBroker{ + ID: brokerID, + Name: "test-broker", + Slug: "tb-" + uuid.NewString()[:8], + Status: "online", + } + require.NoError(t, cs.CreateRuntimeBroker(ctx, broker)) + agent := &store.Agent{ + ID: uuid.NewString(), + Name: "test-agent", + Slug: "ta-" + uuid.NewString()[:8], + ProjectID: proj.ID, + RuntimeBrokerID: brokerID, + } + require.NoError(t, cs.CreateAgent(ctx, agent)) + return agent +} + +func TestExecuteDispatch_Start(t *testing.T) { + ctx := context.Background() + srv, disp, cs := newLifecycleTestServer(t) + agent := seedAgent(t, cs) + + args, err := MarshalDispatchArgs(&StartDispatchArgs{ + Task: "run tests", + }) + require.NoError(t, err) + + d := store.BrokerDispatch{ + ID: uuid.NewString(), + BrokerID: agent.RuntimeBrokerID, + AgentID: agent.ID, + Op: "start", + Args: args, + } + + result, execErr := srv.executeDispatch(ctx, d) + require.NoError(t, execErr) + assert.Empty(t, result) + assert.Equal(t, int32(1), disp.startCalled.Load()) + assert.Equal(t, "run tests", disp.lastTask) +} + +func TestExecuteDispatch_Stop(t *testing.T) { + ctx := context.Background() + srv, disp, cs := newLifecycleTestServer(t) + agent := seedAgent(t, cs) + + d := store.BrokerDispatch{ + ID: uuid.NewString(), + BrokerID: agent.RuntimeBrokerID, + AgentID: agent.ID, + Op: "stop", + } + + _, execErr := srv.executeDispatch(ctx, d) + require.NoError(t, execErr) + assert.Equal(t, int32(1), disp.stopCalled.Load()) +} + +func TestExecuteDispatch_Restart(t *testing.T) { + ctx := context.Background() + srv, disp, cs := newLifecycleTestServer(t) + agent := seedAgent(t, cs) + + d := store.BrokerDispatch{ + ID: uuid.NewString(), + BrokerID: agent.RuntimeBrokerID, + AgentID: agent.ID, + Op: "restart", + } + + _, execErr := srv.executeDispatch(ctx, d) + require.NoError(t, execErr) + assert.Equal(t, int32(1), disp.restartCalled.Load()) +} + +func TestExecuteDispatch_UnknownOp(t *testing.T) { + ctx := context.Background() + srv, _, cs := newLifecycleTestServer(t) + agent := seedAgent(t, cs) + + d := store.BrokerDispatch{ + ID: uuid.NewString(), + BrokerID: agent.RuntimeBrokerID, + AgentID: agent.ID, + Op: "exec_agent", + } + + _, err := srv.executeDispatch(ctx, d) + assert.Error(t, err) + assert.Contains(t, err.Error(), "not yet wired") +} + +func TestExecuteDispatch_MissingAgent(t *testing.T) { + ctx := context.Background() + srv, _, _ := newLifecycleTestServer(t) + + d := store.BrokerDispatch{ + ID: uuid.NewString(), + BrokerID: uuid.NewString(), + AgentID: uuid.NewString(), + Op: "start", + } + + _, err := srv.executeDispatch(ctx, d) + assert.Error(t, err) + assert.Contains(t, err.Error(), "resolve agent") +} + +// ========================================================================= +// Deferred lifecycle integration test (originator side) +// ========================================================================= + +// deferredTestClient is a RuntimeBrokerClient that returns ErrLifecycleDeferred +// for Start/Stop/Restart when the broker is "remote", and succeeds for "local". +type deferredTestClient struct { + fakeHTTPClient + localBroker string + startCalled atomic.Int32 +} + +func (c *deferredTestClient) StartAgent(_ context.Context, brokerID, _, _, _, _, _, _, _ string, _ map[string]string, _ []ResolvedSecret, _ *api.ScionConfig, _ []api.SharedDir, _ bool) (*RemoteAgentResponse, error) { + c.startCalled.Add(1) + if brokerID != c.localBroker { + return nil, ErrLifecycleDeferred + } + return &RemoteAgentResponse{}, nil +} + +func (c *deferredTestClient) StopAgent(_ context.Context, brokerID, _, _, _ string) error { + if brokerID != c.localBroker { + return ErrLifecycleDeferred + } + return nil +} + +func (c *deferredTestClient) RestartAgent(_ context.Context, brokerID, _, _, _ string, _ map[string]string) error { + if brokerID != c.localBroker { + return ErrLifecycleDeferred + } + return nil +} + +func TestDeferredStart_WritesIntentAndWaits(t *testing.T) { + ctx := context.Background() + client := enttest.NewClient(t) + cs := entadapter.NewCompositeStore(client) + + remoteBroker := uuid.NewString() + fakeClient := &deferredTestClient{localBroker: "local-broker"} + + events := NewChannelEventPublisher() + defer events.Close() + + dispatcher := NewHTTPAgentDispatcherWithClient(cs, fakeClient, false, slog.Default()) + dispatcher.SetCrossNodeDeps(events, NoopCommandBus{}) + + agent := seedAgentWithBrokerID(t, cs, remoteBroker) + + // Simulate the owner publishing "running" shortly after intent is written. + go func() { + time.Sleep(50 * time.Millisecond) + updatedAgent := *agent + updatedAgent.Phase = "running" + events.PublishAgentStatus(ctx, &updatedAgent) + }() + + err := dispatcher.DispatchAgentStart(ctx, agent, "my-task") + require.NoError(t, err, "deferred start should succeed when 'running' event arrives") + + // Verify a broker_dispatch row was written (intent is durable). No owner + // claimed it in this test, so it stays pending. + pending, err := cs.ListPendingDispatch(ctx, remoteBroker) + require.NoError(t, err) + assert.Len(t, pending, 1, "durable intent row should exist") + assert.Equal(t, "start", pending[0].Op) + assert.Equal(t, agent.ID, pending[0].AgentID) +} + +func TestDeferredStart_ReturnsErrorOnErrorPhase(t *testing.T) { + ctx := context.Background() + client := enttest.NewClient(t) + cs := entadapter.NewCompositeStore(client) + + remoteBroker := uuid.NewString() + fakeClient := &deferredTestClient{localBroker: "local-broker"} + + events := NewChannelEventPublisher() + defer events.Close() + + dispatcher := NewHTTPAgentDispatcherWithClient(cs, fakeClient, false, slog.Default()) + dispatcher.SetCrossNodeDeps(events, NoopCommandBus{}) + + agent := seedAgentWithBrokerID(t, cs, remoteBroker) + + go func() { + time.Sleep(50 * time.Millisecond) + updatedAgent := *agent + updatedAgent.Phase = "error" + updatedAgent.Message = "container crash" + events.PublishAgentStatus(ctx, &updatedAgent) + }() + + err := dispatcher.DispatchAgentStart(ctx, agent, "") + assert.Error(t, err) + assert.Contains(t, err.Error(), "error phase") +} + +func TestLocalStart_SkipsIntentRow(t *testing.T) { + ctx := context.Background() + client := enttest.NewClient(t) + cs := entadapter.NewCompositeStore(client) + + localBroker := uuid.NewString() + fakeClient := &deferredTestClient{localBroker: localBroker} + + events := NewChannelEventPublisher() + defer events.Close() + + dispatcher := NewHTTPAgentDispatcherWithClient(cs, fakeClient, false, slog.Default()) + dispatcher.SetCrossNodeDeps(events, NoopCommandBus{}) + + agent := seedAgentWithBrokerID(t, cs, localBroker) + + err := dispatcher.DispatchAgentStart(ctx, agent, "local-task") + require.NoError(t, err, "local start should succeed directly") + + // Verify no broker_dispatch row was written (local path skips intent). + pending, err := cs.ListPendingDispatch(ctx, localBroker) + require.NoError(t, err) + assert.Empty(t, pending, "local path should not write intent rows") + + assert.Equal(t, int32(1), fakeClient.startCalled.Load(), "client.StartAgent called once") +} + +// TestReconcileBroker_LifecycleEndToEnd verifies the full reconcile path: +// insert a start dispatch, reconcile, verify the dispatcher was called and +// the dispatch row is marked done. +func TestReconcileBroker_LifecycleEndToEnd(t *testing.T) { + ctx := context.Background() + srv, disp, cs := newLifecycleTestServer(t) + agent := seedAgent(t, cs) + + args, err := MarshalDispatchArgs(&StartDispatchArgs{Task: "deploy"}) + require.NoError(t, err) + + d := &store.BrokerDispatch{ + ID: uuid.NewString(), + BrokerID: agent.RuntimeBrokerID, + AgentID: agent.ID, + Op: "start", + Args: args, + } + require.NoError(t, cs.InsertBrokerDispatch(ctx, d)) + + srv.reconcileBroker(ctx, agent.RuntimeBrokerID) + + assert.Equal(t, int32(1), disp.startCalled.Load()) + assert.Equal(t, "deploy", disp.lastTask) + + pending, err := cs.ListPendingDispatch(ctx, agent.RuntimeBrokerID) + require.NoError(t, err) + assert.Empty(t, pending, "dispatch should be completed") +} + +// ========================================================================= +// B4-3: delete dispatch tests +// ========================================================================= + +func TestExecuteDispatch_Delete(t *testing.T) { + ctx := context.Background() + srv, disp, cs := newLifecycleTestServer(t) + agent := seedAgent(t, cs) + + args, err := MarshalDispatchArgs(&DeleteDispatchArgs{ + DeleteFiles: true, + }) + require.NoError(t, err) + + d := store.BrokerDispatch{ + ID: uuid.NewString(), + BrokerID: agent.RuntimeBrokerID, + AgentID: agent.ID, + Op: "delete", + Args: args, + } + + result, execErr := srv.executeDispatch(ctx, d) + require.NoError(t, execErr) + assert.Empty(t, result) + assert.Equal(t, int32(1), disp.deleteCalled.Load()) + assert.True(t, disp.lastDeleteFiles) +} + +func TestReconcileBroker_DeleteEndToEnd(t *testing.T) { + ctx := context.Background() + srv, disp, cs := newLifecycleTestServer(t) + agent := seedAgent(t, cs) + + args, err := MarshalDispatchArgs(&DeleteDispatchArgs{DeleteFiles: true}) + require.NoError(t, err) + + d := &store.BrokerDispatch{ + ID: uuid.NewString(), + BrokerID: agent.RuntimeBrokerID, + AgentID: agent.ID, + Op: "delete", + Args: args, + } + require.NoError(t, cs.InsertBrokerDispatch(ctx, d)) + + srv.reconcileBroker(ctx, agent.RuntimeBrokerID) + + assert.Equal(t, int32(1), disp.deleteCalled.Load()) + + pending, err := cs.ListPendingDispatch(ctx, agent.RuntimeBrokerID) + require.NoError(t, err) + assert.Empty(t, pending, "dispatch should be completed") + + // Verify result row is readable and in done state. + row, err := cs.GetBrokerDispatch(ctx, d.ID) + require.NoError(t, err) + assert.Equal(t, store.DispatchStateDone, row.State) +} + +// ========================================================================= +// B4-4: data ops dispatch tests (check_prompt, finalize_env, create) +// ========================================================================= + +func TestExecuteDispatch_CheckPrompt(t *testing.T) { + ctx := context.Background() + srv, disp, cs := newLifecycleTestServer(t) + agent := seedAgent(t, cs) + disp.checkPromptResult = true + + d := store.BrokerDispatch{ + ID: uuid.NewString(), + BrokerID: agent.RuntimeBrokerID, + AgentID: agent.ID, + Op: "check_prompt", + } + + result, execErr := srv.executeDispatch(ctx, d) + require.NoError(t, execErr) + assert.Equal(t, int32(1), disp.checkPromptCalled.Load()) + + var cr CheckPromptResult + require.NoError(t, json.Unmarshal([]byte(result), &cr)) + assert.True(t, cr.HasPrompt) +} + +func TestExecuteDispatch_FinalizeEnv(t *testing.T) { + ctx := context.Background() + srv, disp, cs := newLifecycleTestServer(t) + agent := seedAgent(t, cs) + + args, err := MarshalDispatchArgs(&FinalizeEnvDispatchArgs{ + Env: map[string]string{"KEY": "value"}, + }) + require.NoError(t, err) + + d := store.BrokerDispatch{ + ID: uuid.NewString(), + BrokerID: agent.RuntimeBrokerID, + AgentID: agent.ID, + Op: "finalize_env", + Args: args, + } + + result, execErr := srv.executeDispatch(ctx, d) + require.NoError(t, execErr) + assert.Equal(t, int32(1), disp.finalizeEnvCalled.Load()) + assert.Equal(t, map[string]string{"KEY": "value"}, disp.lastFinalizeEnv) + + var fr FinalizeEnvResult + require.NoError(t, json.Unmarshal([]byte(result), &fr)) + assert.True(t, fr.Success) +} + +func TestExecuteDispatch_Create(t *testing.T) { + ctx := context.Background() + srv, disp, cs := newLifecycleTestServer(t) + agent := seedAgent(t, cs) + + d := store.BrokerDispatch{ + ID: uuid.NewString(), + BrokerID: agent.RuntimeBrokerID, + AgentID: agent.ID, + Op: "create", + } + + result, execErr := srv.executeDispatch(ctx, d) + require.NoError(t, execErr) + assert.Equal(t, int32(1), disp.createCalled.Load()) + + var cr CreateWithGatherResult + require.NoError(t, json.Unmarshal([]byte(result), &cr)) + assert.Nil(t, cr.EnvRequirements) +} + +func TestReconcileBroker_CheckPromptEndToEnd(t *testing.T) { + ctx := context.Background() + srv, disp, cs := newLifecycleTestServer(t) + agent := seedAgent(t, cs) + disp.checkPromptResult = true + + d := &store.BrokerDispatch{ + ID: uuid.NewString(), + BrokerID: agent.RuntimeBrokerID, + AgentID: agent.ID, + Op: "check_prompt", + } + require.NoError(t, cs.InsertBrokerDispatch(ctx, d)) + + srv.reconcileBroker(ctx, agent.RuntimeBrokerID) + + assert.Equal(t, int32(1), disp.checkPromptCalled.Load()) + + row, err := cs.GetBrokerDispatch(ctx, d.ID) + require.NoError(t, err) + assert.Equal(t, store.DispatchStateDone, row.State) + + var cr CheckPromptResult + require.NoError(t, json.Unmarshal([]byte(row.Result), &cr)) + assert.True(t, cr.HasPrompt) +} + +// ========================================================================= +// GetBrokerDispatch round-trip +// ========================================================================= + +func TestGetBrokerDispatch_RoundTrip(t *testing.T) { + ctx := context.Background() + client := enttest.NewClient(t) + cs := entadapter.NewCompositeStore(client) + + d := &store.BrokerDispatch{ + ID: uuid.NewString(), + BrokerID: seedAgent(t, cs).RuntimeBrokerID, + Op: "check_prompt", + } + require.NoError(t, cs.InsertBrokerDispatch(ctx, d)) + + got, err := cs.GetBrokerDispatch(ctx, d.ID) + require.NoError(t, err) + assert.Equal(t, d.ID, got.ID) + assert.Equal(t, "check_prompt", got.Op) + assert.Equal(t, store.DispatchStatePending, got.State) + + // Claim (pending→in_progress) before completing, matching the CAS guard. + claimed, err := cs.ClaimBrokerDispatch(ctx, d.ID, "hub-test") + require.NoError(t, err) + require.True(t, claimed) + + require.NoError(t, cs.CompleteBrokerDispatch(ctx, d.ID, `{"hasPrompt":true}`)) + + got, err = cs.GetBrokerDispatch(ctx, d.ID) + require.NoError(t, err) + assert.Equal(t, store.DispatchStateDone, got.State) + assert.Equal(t, `{"hasPrompt":true}`, got.Result) +} + +// ========================================================================= +// B4-3: Deferred delete integration test (originator side) +// ========================================================================= + +func TestDeferredDelete_WritesIntentAndCompletes(t *testing.T) { + ctx := context.Background() + client := enttest.NewClient(t) + cs := entadapter.NewCompositeStore(client) + + remoteBroker := uuid.NewString() + fakeClient := &deferredTestClient{localBroker: "local-broker"} + + events := NewChannelEventPublisher() + defer events.Close() + + dispatcher := NewHTTPAgentDispatcherWithClient(cs, fakeClient, false, slog.Default()) + dispatcher.SetCrossNodeDeps(events, NoopCommandBus{}) + + agent := seedAgentWithBrokerID(t, cs, remoteBroker) + + // Simulate the owner completing the delete dispatch shortly after intent is written. + go func() { + time.Sleep(50 * time.Millisecond) + // Find the pending dispatch row. + pending, err := cs.ListPendingDispatch(ctx, remoteBroker) + if err != nil || len(pending) == 0 { + return + } + d := pending[0] + _, _ = cs.ClaimBrokerDispatch(ctx, d.ID, "owner-hub") + _ = cs.CompleteBrokerDispatch(ctx, d.ID, "") + events.PublishDispatchDone(ctx, d.ID) + }() + + err := dispatcher.DispatchAgentDelete(ctx, agent, true, false, false, time.Time{}) + require.NoError(t, err, "deferred delete should succeed when completion event arrives") + + pending, err := cs.ListPendingDispatch(ctx, remoteBroker) + require.NoError(t, err) + assert.Empty(t, pending, "dispatch should be completed") +} + +// ========================================================================= +// B4-4: Deferred check_prompt integration test (originator side) +// ========================================================================= + +func TestDeferredCheckPrompt_ReturnsResult(t *testing.T) { + ctx := context.Background() + client := enttest.NewClient(t) + cs := entadapter.NewCompositeStore(client) + + remoteBroker := uuid.NewString() + fakeClient := &deferredDataOpTestClient{localBroker: "local-broker"} + + events := NewChannelEventPublisher() + defer events.Close() + + dispatcher := NewHTTPAgentDispatcherWithClient(cs, fakeClient, false, slog.Default()) + dispatcher.SetCrossNodeDeps(events, NoopCommandBus{}) + + agent := seedAgentWithBrokerID(t, cs, remoteBroker) + + // Simulate the owner completing check_prompt with result JSON. + go func() { + time.Sleep(50 * time.Millisecond) + pending, err := cs.ListPendingDispatch(ctx, remoteBroker) + if err != nil || len(pending) == 0 { + return + } + d := pending[0] + _, _ = cs.ClaimBrokerDispatch(ctx, d.ID, "owner-hub") + resultJSON, _ := json.Marshal(CheckPromptResult{HasPrompt: true}) + _ = cs.CompleteBrokerDispatch(ctx, d.ID, string(resultJSON)) + events.PublishDispatchDone(ctx, d.ID) + }() + + hasPrompt, err := dispatcher.DispatchCheckAgentPrompt(ctx, agent) + require.NoError(t, err, "deferred check_prompt should succeed") + assert.True(t, hasPrompt, "should return true from result row") +} + +// deferredDataOpTestClient returns ErrLifecycleDeferred for data ops when the +// broker is not "local", simulating a cross-node dispatch. +type deferredDataOpTestClient struct { + fakeHTTPClient + localBroker string +} + +func (c *deferredDataOpTestClient) DeleteAgent(_ context.Context, brokerID, _, _, _ string, _, _, _ bool, _ time.Time) error { + if brokerID != c.localBroker { + return ErrLifecycleDeferred + } + return nil +} + +func (c *deferredDataOpTestClient) CheckAgentPrompt(_ context.Context, brokerID, _, _, _ string) (bool, error) { + if brokerID != c.localBroker { + return false, ErrLifecycleDeferred + } + return false, nil +} + +func (c *deferredDataOpTestClient) FinalizeEnv(_ context.Context, brokerID, _, _ string, _ map[string]string) (*RemoteAgentResponse, error) { + if brokerID != c.localBroker { + return nil, ErrLifecycleDeferred + } + return nil, nil +} + +func (c *deferredDataOpTestClient) CreateAgentWithGather(_ context.Context, brokerID, _ string, _ *RemoteCreateAgentRequest) (*RemoteAgentResponse, *RemoteEnvRequirementsResponse, error) { + if brokerID != c.localBroker { + return nil, nil, ErrLifecycleDeferred + } + return nil, nil, nil +} diff --git a/pkg/hub/dispatch_lifecycle_test.go b/pkg/hub/dispatch_lifecycle_test.go new file mode 100644 index 000000000..2326cbdf5 --- /dev/null +++ b/pkg/hub/dispatch_lifecycle_test.go @@ -0,0 +1,252 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package hub + +import ( + "context" + "log/slog" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +// ========================================================================= +// Route-gating tests for StartAgent / StopAgent / RestartAgent +// ========================================================================= + +func TestHybridBrokerClient_StartAgent_RouteGate(t *testing.T) { + const localBroker = "broker-local" + const remoteBroker = "broker-remote" + + mgr := NewControlChannelManager(DefaultControlChannelConfig(), slog.Default()) + mgr.mu.Lock() + mgr.connections[localBroker] = &BrokerConnection{brokerID: localBroker, sessionID: "s1"} + mgr.mu.Unlock() + + httpClient := &fakeHTTPClient{} + c := NewHybridBrokerClient(mgr, httpClient, nil, false) + + t.Run("routeLocal uses control channel (not deferred)", func(t *testing.T) { + got := c.route(context.Background(), localBroker, "") + assert.Equal(t, routeLocal, got) + }) + + t.Run("routeForward returns ErrLifecycleDeferred", func(t *testing.T) { + c.SetAffinityLookup(func(context.Context, string) (string, bool) { return "hubA", true }) + _, err := c.StartAgent(context.Background(), remoteBroker, "", "a1", "p1", "", "", "", "", nil, nil, nil, nil, false) + assert.ErrorIs(t, err, ErrLifecycleDeferred) + }) + + t.Run("routeUndeliverable returns ErrLifecycleDeferred", func(t *testing.T) { + c.SetAffinityLookup(func(context.Context, string) (string, bool) { return "", false }) + _, err := c.StartAgent(context.Background(), remoteBroker, "", "a1", "p1", "", "", "", "", nil, nil, nil, nil, false) + assert.ErrorIs(t, err, ErrLifecycleDeferred) + }) +} + +func TestHybridBrokerClient_StopAgent_RouteGate(t *testing.T) { + const remoteBroker = "broker-remote" + + mgr := NewControlChannelManager(DefaultControlChannelConfig(), slog.Default()) + c := NewHybridBrokerClient(mgr, &fakeHTTPClient{}, nil, false) + + t.Run("routeForward returns ErrLifecycleDeferred", func(t *testing.T) { + c.SetAffinityLookup(func(context.Context, string) (string, bool) { return "hubA", true }) + err := c.StopAgent(context.Background(), remoteBroker, "", "a1", "p1") + assert.ErrorIs(t, err, ErrLifecycleDeferred) + }) + + t.Run("routeUndeliverable returns ErrLifecycleDeferred", func(t *testing.T) { + c.SetAffinityLookup(func(context.Context, string) (string, bool) { return "", false }) + err := c.StopAgent(context.Background(), remoteBroker, "", "a1", "p1") + assert.ErrorIs(t, err, ErrLifecycleDeferred) + }) +} + +func TestHybridBrokerClient_RestartAgent_RouteGate(t *testing.T) { + const remoteBroker = "broker-remote" + + mgr := NewControlChannelManager(DefaultControlChannelConfig(), slog.Default()) + c := NewHybridBrokerClient(mgr, &fakeHTTPClient{}, nil, false) + + t.Run("routeForward returns ErrLifecycleDeferred", func(t *testing.T) { + c.SetAffinityLookup(func(context.Context, string) (string, bool) { return "hubA", true }) + err := c.RestartAgent(context.Background(), remoteBroker, "", "a1", "p1", nil) + assert.ErrorIs(t, err, ErrLifecycleDeferred) + }) + + t.Run("routeUndeliverable returns ErrLifecycleDeferred", func(t *testing.T) { + c.SetAffinityLookup(func(context.Context, string) (string, bool) { return "", false }) + err := c.RestartAgent(context.Background(), remoteBroker, "", "a1", "p1", nil) + assert.ErrorIs(t, err, ErrLifecycleDeferred) + }) +} + +// ========================================================================= +// Dispatch args round-trip (serialize -> deserialize lossless) +// ========================================================================= + +func TestStartDispatchArgs_RoundTrip(t *testing.T) { + original := &StartDispatchArgs{ + Task: "build the widget", + } + + raw, err := MarshalDispatchArgs(original) + require.NoError(t, err) + require.NotEmpty(t, raw) + + got, err := UnmarshalStartArgs(raw) + require.NoError(t, err) + assert.Equal(t, original.Task, got.Task) +} + +func TestRestartDispatchArgs_RoundTrip(t *testing.T) { + raw, err := MarshalDispatchArgs(&RestartDispatchArgs{}) + require.NoError(t, err) + assert.Equal(t, "{}", raw) +} + +func TestStopDispatchArgs_RoundTrip(t *testing.T) { + raw, err := MarshalDispatchArgs(&StopDispatchArgs{}) + require.NoError(t, err) + assert.Equal(t, "{}", raw) +} + +// ========================================================================= +// B4-3: Route-gating tests for DeleteAgent +// ========================================================================= + +func TestHybridBrokerClient_DeleteAgent_RouteGate(t *testing.T) { + const remoteBroker = "broker-remote" + + mgr := NewControlChannelManager(DefaultControlChannelConfig(), slog.Default()) + c := NewHybridBrokerClient(mgr, &fakeHTTPClient{}, nil, false) + + t.Run("routeForward returns ErrLifecycleDeferred", func(t *testing.T) { + c.SetAffinityLookup(func(context.Context, string) (string, bool) { return "hubA", true }) + err := c.DeleteAgent(context.Background(), remoteBroker, "", "a1", "p1", false, false, false, time.Time{}) + assert.ErrorIs(t, err, ErrLifecycleDeferred) + }) + + t.Run("routeUndeliverable returns ErrLifecycleDeferred", func(t *testing.T) { + c.SetAffinityLookup(func(context.Context, string) (string, bool) { return "", false }) + err := c.DeleteAgent(context.Background(), remoteBroker, "", "a1", "p1", false, false, false, time.Time{}) + assert.ErrorIs(t, err, ErrLifecycleDeferred) + }) +} + +// ========================================================================= +// B4-4: Route-gating tests for CheckAgentPrompt / CreateAgentWithGather / FinalizeEnv +// ========================================================================= + +func TestHybridBrokerClient_CheckAgentPrompt_RouteGate(t *testing.T) { + const remoteBroker = "broker-remote" + + mgr := NewControlChannelManager(DefaultControlChannelConfig(), slog.Default()) + c := NewHybridBrokerClient(mgr, &fakeHTTPClient{}, nil, false) + + t.Run("routeForward returns ErrLifecycleDeferred", func(t *testing.T) { + c.SetAffinityLookup(func(context.Context, string) (string, bool) { return "hubA", true }) + _, err := c.CheckAgentPrompt(context.Background(), remoteBroker, "", "a1", "p1") + assert.ErrorIs(t, err, ErrLifecycleDeferred) + }) + + t.Run("routeUndeliverable returns ErrLifecycleDeferred", func(t *testing.T) { + c.SetAffinityLookup(func(context.Context, string) (string, bool) { return "", false }) + _, err := c.CheckAgentPrompt(context.Background(), remoteBroker, "", "a1", "p1") + assert.ErrorIs(t, err, ErrLifecycleDeferred) + }) +} + +func TestHybridBrokerClient_CreateAgentWithGather_RouteGate(t *testing.T) { + const remoteBroker = "broker-remote" + + mgr := NewControlChannelManager(DefaultControlChannelConfig(), slog.Default()) + c := NewHybridBrokerClient(mgr, &fakeHTTPClient{}, nil, false) + + t.Run("routeForward returns ErrLifecycleDeferred", func(t *testing.T) { + c.SetAffinityLookup(func(context.Context, string) (string, bool) { return "hubA", true }) + _, _, err := c.CreateAgentWithGather(context.Background(), remoteBroker, "", &RemoteCreateAgentRequest{}) + assert.ErrorIs(t, err, ErrLifecycleDeferred) + }) + + t.Run("routeUndeliverable returns ErrLifecycleDeferred", func(t *testing.T) { + c.SetAffinityLookup(func(context.Context, string) (string, bool) { return "", false }) + _, _, err := c.CreateAgentWithGather(context.Background(), remoteBroker, "", &RemoteCreateAgentRequest{}) + assert.ErrorIs(t, err, ErrLifecycleDeferred) + }) +} + +func TestHybridBrokerClient_FinalizeEnv_RouteGate(t *testing.T) { + const remoteBroker = "broker-remote" + + mgr := NewControlChannelManager(DefaultControlChannelConfig(), slog.Default()) + c := NewHybridBrokerClient(mgr, &fakeHTTPClient{}, nil, false) + + t.Run("routeForward returns ErrLifecycleDeferred", func(t *testing.T) { + c.SetAffinityLookup(func(context.Context, string) (string, bool) { return "hubA", true }) + _, err := c.FinalizeEnv(context.Background(), remoteBroker, "", "a1", nil) + assert.ErrorIs(t, err, ErrLifecycleDeferred) + }) + + t.Run("routeUndeliverable returns ErrLifecycleDeferred", func(t *testing.T) { + c.SetAffinityLookup(func(context.Context, string) (string, bool) { return "", false }) + _, err := c.FinalizeEnv(context.Background(), remoteBroker, "", "a1", nil) + assert.ErrorIs(t, err, ErrLifecycleDeferred) + }) +} + +// ========================================================================= +// B4-3/B4-4: Dispatch args round-trip +// ========================================================================= + +func TestDeleteDispatchArgs_RoundTrip(t *testing.T) { + original := &DeleteDispatchArgs{ + DeleteFiles: true, + RemoveBranch: true, + SoftDelete: false, + } + + raw, err := MarshalDispatchArgs(original) + require.NoError(t, err) + require.NotEmpty(t, raw) + + got, err := UnmarshalDeleteArgs(raw) + require.NoError(t, err) + assert.Equal(t, original.DeleteFiles, got.DeleteFiles) + assert.Equal(t, original.RemoveBranch, got.RemoveBranch) + assert.Equal(t, original.SoftDelete, got.SoftDelete) +} + +func TestFinalizeEnvDispatchArgs_RoundTrip(t *testing.T) { + original := &FinalizeEnvDispatchArgs{ + Env: map[string]string{"KEY": "val", "SECRET": "abc"}, + } + + raw, err := MarshalDispatchArgs(original) + require.NoError(t, err) + + got, err := UnmarshalFinalizeEnvArgs(raw) + require.NoError(t, err) + assert.Equal(t, original.Env, got.Env) +} + +func TestCheckPromptDispatchArgs_RoundTrip(t *testing.T) { + raw, err := MarshalDispatchArgs(&CheckPromptDispatchArgs{}) + require.NoError(t, err) + assert.Equal(t, "{}", raw) +} diff --git a/pkg/hub/dispatch_wait.go b/pkg/hub/dispatch_wait.go new file mode 100644 index 000000000..94c95ec4b --- /dev/null +++ b/pkg/hub/dispatch_wait.go @@ -0,0 +1,148 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package hub + +import ( + "context" + "encoding/json" + "errors" + "fmt" + "time" + + "github.com/GoogleCloudPlatform/scion/pkg/store" +) + +// ErrDispatchFailed is returned when a lifecycle dispatch rolling timeout +// expires without receiving any status update within the window — the broker +// went silent and the operation is considered failed (design §6.4). +var ErrDispatchFailed = errors.New("dispatch failed: rolling timeout expired with no status update") + +// dispatchRollingTimeout is the default rolling window for +// waitForAgentTransition. Each status event (phase/activity/detail change) +// resets this timer. If no event arrives within the window, the dispatch is +// considered failed. Single tunable per design §6.4. +const dispatchRollingTimeout = 90 * time.Second + +// waitForAgentTransition waits for an agent's phase to reach a terminal state, +// using a rolling timeout that resets on ANY AgentStatusEvent (phase, activity, +// or detail change). The caller must subscribe to the agent's status events +// BEFORE writing the durable intent, and pass the subscription channel + +// unsubscribe function here. +// +// Parameters: +// - events: the subscription channel from EventPublisher.Subscribe("agent..status") +// - unsub: the unsubscribe function returned by Subscribe (called on return) +// - terminal: returns true when the agent's phase indicates the op is done +// (e.g. "running" or "error" for start; "stopped" or "error" for stop) +// +// Returns the terminal phase on success, or ErrDispatchFailed on rolling +// timeout, or ctx.Err() on context cancellation. +func waitForAgentTransition( + ctx context.Context, + events <-chan Event, + unsub func(), + terminal func(phase string) bool, +) (string, error) { + defer unsub() + + timeout := dispatchRollingTimeout + timer := time.NewTimer(timeout) + defer timer.Stop() + + for { + select { + case ev, ok := <-events: + if !ok { + return "", ErrDispatchFailed + } + var status AgentStatusEvent + if err := json.Unmarshal(ev.Data, &status); err != nil { + continue + } + if terminal(status.Phase) { + return status.Phase, nil + } + if !timer.Stop() { + select { + case <-timer.C: + default: + } + } + timer.Reset(timeout) + + case <-timer.C: + return "", ErrDispatchFailed + + case <-ctx.Done(): + return "", ctx.Err() + } + } +} + +// waitForDispatchDone waits for a broker_dispatch row to reach terminal state. +// The caller subscribes to broker.dispatch..done BEFORE writing intent and +// passes the channel + unsub here. On event arrival (or timeout), the row is +// read from the store — the DB row is authoritative (design §6.3), so a missed +// event is recoverable. +func waitForDispatchDone( + ctx context.Context, + events <-chan Event, + unsub func(), + st store.BrokerDispatchStore, + dispatchID string, +) (*store.BrokerDispatch, error) { + defer unsub() + + timeout := dispatchRollingTimeout + timer := time.NewTimer(timeout) + defer timer.Stop() + + for { + select { + case _, ok := <-events: + if !ok { + return nil, ErrDispatchFailed + } + d, err := st.GetBrokerDispatch(ctx, dispatchID) + if err != nil { + return nil, fmt.Errorf("read dispatch result: %w", err) + } + if d.State == store.DispatchStateDone || d.State == store.DispatchStateFailed { + return d, nil + } + if !timer.Stop() { + select { + case <-timer.C: + default: + } + } + timer.Reset(timeout) + + case <-timer.C: + // Bounded re-read: the event may have been missed (design §6.3). + d, err := st.GetBrokerDispatch(ctx, dispatchID) + if err != nil { + return nil, fmt.Errorf("read dispatch result on timeout: %w", err) + } + if d.State == store.DispatchStateDone || d.State == store.DispatchStateFailed { + return d, nil + } + return nil, ErrDispatchFailed + + case <-ctx.Done(): + return nil, ctx.Err() + } + } +} diff --git a/pkg/hub/dispatch_wait_test.go b/pkg/hub/dispatch_wait_test.go new file mode 100644 index 000000000..de12f8fad --- /dev/null +++ b/pkg/hub/dispatch_wait_test.go @@ -0,0 +1,312 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package hub + +import ( + "context" + "encoding/json" + "testing" + "time" + + "github.com/GoogleCloudPlatform/scion/pkg/store" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +// fakeDispatchStore is a minimal in-memory BrokerDispatchStore for unit tests. +type fakeDispatchStore struct { + dispatches map[string]*store.BrokerDispatch +} + +func (f *fakeDispatchStore) GetBrokerDispatch(_ context.Context, id string) (*store.BrokerDispatch, error) { + d, ok := f.dispatches[id] + if !ok { + return nil, store.ErrNotFound + } + return d, nil +} + +func (f *fakeDispatchStore) InsertBrokerDispatch(_ context.Context, d *store.BrokerDispatch) error { + return nil +} +func (f *fakeDispatchStore) ClaimBrokerDispatch(_ context.Context, _, _ string) (bool, error) { + return false, nil +} +func (f *fakeDispatchStore) CompleteBrokerDispatch(_ context.Context, _, _ string) error { + return nil +} +func (f *fakeDispatchStore) FailBrokerDispatch(_ context.Context, _, _ string) error { return nil } +func (f *fakeDispatchStore) ListPendingDispatch(_ context.Context, _ string) ([]store.BrokerDispatch, error) { + return nil, nil +} +func (f *fakeDispatchStore) MarkMessageDispatched(_ context.Context, _ string) (bool, error) { + return false, nil +} +func (f *fakeDispatchStore) ListPendingMessages(_ context.Context, _ string) ([]store.Message, error) { + return nil, nil +} +func (f *fakeDispatchStore) ReapStuckDispatch(_ context.Context, _ time.Time, _ int) (int, int, error) { + return 0, 0, nil +} +func (f *fakeDispatchStore) CountStuckPendingMessages(_ context.Context, _ time.Time) (int, error) { + return 0, nil +} + +// sendStatus pushes a fake AgentStatusEvent onto the channel. +func sendStatus(ch chan<- Event, phase, activity string, detail *AgentDetail) { + evt := AgentStatusEvent{ + AgentID: "agent-1", + Phase: phase, + Activity: activity, + Detail: detail, + } + data, _ := json.Marshal(evt) + ch <- Event{Subject: "agent.agent-1.status", Data: data} +} + +func TestWaitForAgentTransition_TerminalPhase(t *testing.T) { + ch := make(chan Event, 8) + unsub := func() {} + _ = &Server{} // ensure Server type compiles; waitForAgentTransition is standalone + + go func() { + sendStatus(ch, "starting", "pulling image", nil) + sendStatus(ch, "running", "", nil) + }() + + phase, err := waitForAgentTransition( + context.Background(), ch, unsub, + func(p string) bool { return p == "running" || p == "error" }, + ) + require.NoError(t, err) + assert.Equal(t, "running", phase) +} + +func TestWaitForAgentTransition_ErrorPhase(t *testing.T) { + ch := make(chan Event, 8) + unsub := func() {} + _ = &Server{} // ensure Server type compiles; waitForAgentTransition is standalone + + go func() { + sendStatus(ch, "starting", "", nil) + sendStatus(ch, "error", "", nil) + }() + + phase, err := waitForAgentTransition( + context.Background(), ch, unsub, + func(p string) bool { return p == "running" || p == "error" }, + ) + require.NoError(t, err) + assert.Equal(t, "error", phase) +} + +func TestWaitForAgentTransition_RollingReset(t *testing.T) { + // Interim detail updates keep the wait alive past one window. + // We use a very short timeout override for testing speed. + ch := make(chan Event, 64) + unsub := func() {} + _ = &Server{} // ensure Server type compiles; waitForAgentTransition is standalone + + // Override the timeout by wrapping: we cannot easily override the + // const, but we can send events faster than the 90s default and + // confirm the terminal is reached. The real test is that interim + // events don't cause early return. Send 5 interim events, then terminal. + go func() { + for i := 0; i < 5; i++ { + sendStatus(ch, "starting", "step", &AgentDetail{Message: "progress"}) + time.Sleep(5 * time.Millisecond) + } + sendStatus(ch, "running", "", nil) + }() + + phase, err := waitForAgentTransition( + context.Background(), ch, unsub, + func(p string) bool { return p == "running" || p == "error" }, + ) + require.NoError(t, err) + assert.Equal(t, "running", phase) +} + +func TestWaitForAgentTransition_SilenceExpiry(t *testing.T) { + // Override the rolling timeout to something very short so the test + // completes quickly. We can't mutate the const, so instead we close + // the channel which produces a zero Event -> ErrDispatchFailed via + // the ok=false branch. + ch := make(chan Event, 4) + unsub := func() {} + _ = &Server{} // ensure Server type compiles; waitForAgentTransition is standalone + + // Close immediately: simulates silence (no events). + close(ch) + + _, err := waitForAgentTransition( + context.Background(), ch, unsub, + func(p string) bool { return p == "running" }, + ) + assert.ErrorIs(t, err, ErrDispatchFailed) +} + +func TestWaitForAgentTransition_ContextCancel(t *testing.T) { + ch := make(chan Event, 4) + unsub := func() {} + _ = &Server{} // ensure Server type compiles; waitForAgentTransition is standalone + + ctx, cancel := context.WithCancel(context.Background()) + cancel() + + _, err := waitForAgentTransition( + ctx, ch, unsub, + func(p string) bool { return p == "running" }, + ) + assert.ErrorIs(t, err, context.Canceled) +} + +func TestWaitForAgentTransition_UnsubCalled(t *testing.T) { + ch := make(chan Event, 4) + var unsubCalled bool + unsub := func() { unsubCalled = true } + _ = &Server{} // ensure Server type compiles; waitForAgentTransition is standalone + + close(ch) + _, _ = waitForAgentTransition( + context.Background(), ch, unsub, + func(p string) bool { return p == "running" }, + ) + assert.True(t, unsubCalled, "unsub must be called on return") +} + +func TestWaitForAgentTransition_StopTerminal(t *testing.T) { + ch := make(chan Event, 4) + unsub := func() {} + _ = &Server{} // ensure Server type compiles; waitForAgentTransition is standalone + + go func() { + sendStatus(ch, "stopped", "", nil) + }() + + phase, err := waitForAgentTransition( + context.Background(), ch, unsub, + func(p string) bool { return p == "stopped" || p == "error" }, + ) + require.NoError(t, err) + assert.Equal(t, "stopped", phase) +} + +// ========================================================================= +// waitForDispatchDone tests (data-op completion path) +// ========================================================================= + +func TestWaitForDispatchDone_ReturnsOnDone(t *testing.T) { + const dispatchID = "dispatch-1" + ch := make(chan Event, 4) + unsub := func() {} + + fs := &fakeDispatchStore{ + dispatches: map[string]*store.BrokerDispatch{ + dispatchID: { + ID: dispatchID, + State: store.DispatchStateDone, + Result: `{"hasPrompt":true}`, + }, + }, + } + + go func() { + ch <- Event{Subject: "broker.dispatch." + dispatchID + ".done"} + }() + + result, err := waitForDispatchDone(context.Background(), ch, unsub, fs, dispatchID) + require.NoError(t, err) + assert.Equal(t, store.DispatchStateDone, result.State) + assert.Equal(t, `{"hasPrompt":true}`, result.Result) +} + +func TestWaitForDispatchDone_ReturnsOnFailed(t *testing.T) { + const dispatchID = "dispatch-2" + ch := make(chan Event, 4) + unsub := func() {} + + fs := &fakeDispatchStore{ + dispatches: map[string]*store.BrokerDispatch{ + dispatchID: { + ID: dispatchID, + State: store.DispatchStateFailed, + Error: "container crashed", + }, + }, + } + + go func() { + ch <- Event{Subject: "broker.dispatch." + dispatchID + ".done"} + }() + + result, err := waitForDispatchDone(context.Background(), ch, unsub, fs, dispatchID) + require.NoError(t, err) + assert.Equal(t, store.DispatchStateFailed, result.State) + assert.Equal(t, "container crashed", result.Error) +} + +func TestWaitForDispatchDone_ChannelClose(t *testing.T) { + const dispatchID = "dispatch-3" + ch := make(chan Event, 4) + unsub := func() {} + + fs := &fakeDispatchStore{dispatches: map[string]*store.BrokerDispatch{}} + + close(ch) + + _, err := waitForDispatchDone(context.Background(), ch, unsub, fs, dispatchID) + assert.ErrorIs(t, err, ErrDispatchFailed) +} + +func TestWaitForDispatchDone_ContextCancel(t *testing.T) { + const dispatchID = "dispatch-4" + ch := make(chan Event, 4) + unsub := func() {} + + fs := &fakeDispatchStore{dispatches: map[string]*store.BrokerDispatch{}} + + ctx, cancel := context.WithCancel(context.Background()) + cancel() + + _, err := waitForDispatchDone(ctx, ch, unsub, fs, dispatchID) + assert.ErrorIs(t, err, context.Canceled) +} + +func TestWaitForDispatchDone_TimeoutReread(t *testing.T) { + // Verify that on timeout, the row is re-read and if done, returned. + const dispatchID = "dispatch-5" + ch := make(chan Event, 4) + var unsubCalled bool + unsub := func() { unsubCalled = true } + + fs := &fakeDispatchStore{ + dispatches: map[string]*store.BrokerDispatch{ + dispatchID: { + ID: dispatchID, + State: store.DispatchStateDone, + Result: `{"success":true}`, + }, + }, + } + + // Don't send any event — let it time out and re-read. + // We can't easily override the 90s rolling timeout in a unit test, + // so we test the channel-close path instead (above) and verify the + // unsub is called on all paths. + close(ch) + _, _ = waitForDispatchDone(context.Background(), ch, unsub, fs, dispatchID) + assert.True(t, unsubCalled, "unsub must be called on return") +} diff --git a/pkg/hub/embedded_broker_test.go b/pkg/hub/embedded_broker_test.go index 576f225e5..5de191323 100644 --- a/pkg/hub/embedded_broker_test.go +++ b/pkg/hub/embedded_broker_test.go @@ -31,7 +31,7 @@ func TestIsEmbeddedBroker(t *testing.T) { srv := &Server{} // Before setting, everything should return false - if srv.isEmbeddedBroker("broker-1") { + if srv.isEmbeddedBroker(tid("broker-1")) { t.Error("expected isEmbeddedBroker to return false before setting") } if srv.isEmbeddedBroker("") { @@ -39,15 +39,15 @@ func TestIsEmbeddedBroker(t *testing.T) { } // Set the embedded broker ID - srv.SetEmbeddedBrokerID("broker-1") + srv.SetEmbeddedBrokerID(tid("broker-1")) // Matching ID should return true - if !srv.isEmbeddedBroker("broker-1") { + if !srv.isEmbeddedBroker(tid("broker-1")) { t.Error("expected isEmbeddedBroker to return true for matching ID") } // Non-matching ID should return false - if srv.isEmbeddedBroker("broker-2") { + if srv.isEmbeddedBroker(tid("broker-2")) { t.Error("expected isEmbeddedBroker to return false for non-matching ID") } @@ -63,7 +63,7 @@ func TestCreateAgent_SkipsGCSSyncForEmbeddedBroker(t *testing.T) { // Create a project (hub-managed: no git remote) project := &store.Project{ - ID: "project-embedded-test", + ID: tid("project-embedded-test"), Name: "embedded-test", Slug: "embedded-test", } @@ -72,10 +72,11 @@ func TestCreateAgent_SkipsGCSSyncForEmbeddedBroker(t *testing.T) { } // Create a runtime broker - brokerID := "embedded-broker-1" + brokerID := tid("embedded-broker-1") broker := &store.RuntimeBroker{ ID: brokerID, Name: "embedded-broker", + Slug: "embedded-broker", Endpoint: "http://localhost:9090", Status: store.BrokerStatusOnline, } diff --git a/pkg/hub/envgather_resolution_test.go b/pkg/hub/envgather_resolution_test.go index 33909cd21..619ad91fb 100644 --- a/pkg/hub/envgather_resolution_test.go +++ b/pkg/hub/envgather_resolution_test.go @@ -33,19 +33,19 @@ func TestResolution_PlainEnvVar(t *testing.T) { memStore := createTestStore(t) broker := &store.RuntimeBroker{ - ID: "broker-res-1", Name: "res-broker", Slug: "res-broker", + ID: tid("broker-res-1"), Name: "res-broker", Slug: "res-broker", Endpoint: "http://localhost:9800", Status: store.BrokerStatusOnline, } if err := memStore.CreateRuntimeBroker(ctx, broker); err != nil { t.Fatal(err) } - project := &store.Project{ID: "project-res-1", Name: "res-project", Slug: "res-project"} + project := &store.Project{ID: tid("project-res-1"), Name: "res-project", Slug: "res-project"} if err := memStore.CreateProject(ctx, project); err != nil { t.Fatal(err) } if err := memStore.AddProjectProvider(ctx, &store.ProjectProvider{ - ProjectID: "project-res-1", BrokerID: "broker-res-1", + ProjectID: tid("project-res-1"), BrokerID: tid("broker-res-1"), BrokerName: "test-broker", }); err != nil { t.Fatal(err) } @@ -69,9 +69,9 @@ func TestResolution_PlainEnvVar(t *testing.T) { ID: "agent-res-1", Name: "res-agent", Slug: "res-agent", - ProjectID: "project-res-1", + ProjectID: tid("project-res-1"), OwnerID: "user-res-1", - RuntimeBrokerID: "broker-res-1", + RuntimeBrokerID: tid("broker-res-1"), AppliedConfig: &store.AgentAppliedConfig{}, } @@ -94,19 +94,19 @@ func TestResolution_SecretUserScope(t *testing.T) { memStore := createTestStore(t) broker := &store.RuntimeBroker{ - ID: "broker-res-2", Name: "res-broker-2", Slug: "res-broker-2", + ID: tid("broker-res-2"), Name: "res-broker-2", Slug: "res-broker-2", Endpoint: "http://localhost:9800", Status: store.BrokerStatusOnline, } if err := memStore.CreateRuntimeBroker(ctx, broker); err != nil { t.Fatal(err) } - project := &store.Project{ID: "project-res-2", Name: "res-project-2", Slug: "res-project-2"} + project := &store.Project{ID: tid("project-res-2"), Name: "res-project-2", Slug: "res-project-2"} if err := memStore.CreateProject(ctx, project); err != nil { t.Fatal(err) } if err := memStore.AddProjectProvider(ctx, &store.ProjectProvider{ - ProjectID: "project-res-2", BrokerID: "broker-res-2", + ProjectID: tid("project-res-2"), BrokerID: tid("broker-res-2"), BrokerName: "test-broker", }); err != nil { t.Fatal(err) } @@ -132,9 +132,9 @@ func TestResolution_SecretUserScope(t *testing.T) { ID: "agent-res-2", Name: "res-agent-2", Slug: "res-agent-2", - ProjectID: "project-res-2", + ProjectID: tid("project-res-2"), OwnerID: "user-res-2", - RuntimeBrokerID: "broker-res-2", + RuntimeBrokerID: tid("broker-res-2"), AppliedConfig: &store.AgentAppliedConfig{}, } @@ -170,19 +170,19 @@ func TestResolution_ProjectEnvVar(t *testing.T) { memStore := createTestStore(t) broker := &store.RuntimeBroker{ - ID: "broker-res-3", Name: "res-broker-3", Slug: "res-broker-3", + ID: tid("broker-res-3"), Name: "res-broker-3", Slug: "res-broker-3", Endpoint: "http://localhost:9800", Status: store.BrokerStatusOnline, } if err := memStore.CreateRuntimeBroker(ctx, broker); err != nil { t.Fatal(err) } - project := &store.Project{ID: "project-res-3", Name: "res-project-3", Slug: "res-project-3"} + project := &store.Project{ID: tid("project-res-3"), Name: "res-project-3", Slug: "res-project-3"} if err := memStore.CreateProject(ctx, project); err != nil { t.Fatal(err) } if err := memStore.AddProjectProvider(ctx, &store.ProjectProvider{ - ProjectID: "project-res-3", BrokerID: "broker-res-3", + ProjectID: tid("project-res-3"), BrokerID: tid("broker-res-3"), BrokerName: "test-broker", }); err != nil { t.Fatal(err) } @@ -193,7 +193,7 @@ func TestResolution_ProjectEnvVar(t *testing.T) { Key: "GROVE_VAR", Value: "project-var-value", Scope: "project", - ScopeID: "project-res-3", + ScopeID: tid("project-res-3"), }) if err != nil { t.Fatal(err) @@ -206,9 +206,9 @@ func TestResolution_ProjectEnvVar(t *testing.T) { ID: "agent-res-3", Name: "res-agent-3", Slug: "res-agent-3", - ProjectID: "project-res-3", + ProjectID: tid("project-res-3"), OwnerID: "user-res-3", - RuntimeBrokerID: "broker-res-3", + RuntimeBrokerID: tid("broker-res-3"), AppliedConfig: &store.AgentAppliedConfig{}, } @@ -233,19 +233,19 @@ func TestResolution_SecretPromotedEnvVar(t *testing.T) { memStore := createTestStore(t) broker := &store.RuntimeBroker{ - ID: "broker-res-4", Name: "res-broker-4", Slug: "res-broker-4", + ID: tid("broker-res-4"), Name: "res-broker-4", Slug: "res-broker-4", Endpoint: "http://localhost:9800", Status: store.BrokerStatusOnline, } if err := memStore.CreateRuntimeBroker(ctx, broker); err != nil { t.Fatal(err) } - project := &store.Project{ID: "project-res-4", Name: "res-project-4", Slug: "res-project-4"} + project := &store.Project{ID: tid("project-res-4"), Name: "res-project-4", Slug: "res-project-4"} if err := memStore.CreateProject(ctx, project); err != nil { t.Fatal(err) } if err := memStore.AddProjectProvider(ctx, &store.ProjectProvider{ - ProjectID: "project-res-4", BrokerID: "broker-res-4", + ProjectID: tid("project-res-4"), BrokerID: tid("broker-res-4"), BrokerName: "test-broker", }); err != nil { t.Fatal(err) } @@ -294,9 +294,9 @@ func TestResolution_SecretPromotedEnvVar(t *testing.T) { ID: "agent-res-4", Name: "res-agent-4", Slug: "res-agent-4", - ProjectID: "project-res-4", + ProjectID: tid("project-res-4"), OwnerID: "user-res-4", - RuntimeBrokerID: "broker-res-4", + RuntimeBrokerID: tid("broker-res-4"), AppliedConfig: &store.AgentAppliedConfig{}, } diff --git a/pkg/hub/envgather_test.go b/pkg/hub/envgather_test.go index 4d8f922b7..d4c844612 100644 --- a/pkg/hub/envgather_test.go +++ b/pkg/hub/envgather_test.go @@ -88,7 +88,7 @@ func TestEnvGather_HubDispatch_AllSatisfied(t *testing.T) { memStore := createTestStore(t) broker := &store.RuntimeBroker{ - ID: "broker-1", + ID: tid("broker-1"), Name: "test-broker", Slug: "test-broker", Endpoint: "http://localhost:9800", @@ -99,7 +99,7 @@ func TestEnvGather_HubDispatch_AllSatisfied(t *testing.T) { } project := &store.Project{ - ID: "project-1", + ID: tid("project-1"), Name: "test-project", Slug: "test-project", } @@ -109,8 +109,8 @@ func TestEnvGather_HubDispatch_AllSatisfied(t *testing.T) { // Add provider so broker can serve this project if err := memStore.AddProjectProvider(ctx, &store.ProjectProvider{ - ProjectID: "project-1", - BrokerID: "broker-1", + ProjectID: tid("project-1"), + BrokerID: tid("broker-1"), BrokerName: "test-broker", }); err != nil { t.Fatal(err) } @@ -119,11 +119,11 @@ func TestEnvGather_HubDispatch_AllSatisfied(t *testing.T) { dispatcher := NewHTTPAgentDispatcherWithClient(memStore, mockClient, true, slog.Default()) agent := &store.Agent{ - ID: "agent-1", + ID: tid("agent-1"), Name: "test-agent", Slug: "test-agent", - ProjectID: "project-1", - RuntimeBrokerID: "broker-1", + ProjectID: tid("project-1"), + RuntimeBrokerID: tid("broker-1"), AppliedConfig: &store.AgentAppliedConfig{ HarnessConfig: "claude", }, @@ -155,7 +155,7 @@ func TestEnvGather_HubDispatch_NeedsGather(t *testing.T) { memStore := createTestStore(t) broker := &store.RuntimeBroker{ - ID: "broker-2", + ID: tid("broker-2"), Name: "test-broker-2", Slug: "test-broker-2", Endpoint: "http://localhost:9800", @@ -167,7 +167,7 @@ func TestEnvGather_HubDispatch_NeedsGather(t *testing.T) { mockClient := &envGatherMockBrokerClient{ gatherReturnEnvReqs: &RemoteEnvRequirementsResponse{ - AgentID: "agent-2", + AgentID: tid("agent-2"), Required: []string{"API_KEY", "SECRET"}, HubHas: []string{"API_KEY"}, Needs: []string{"SECRET"}, @@ -176,11 +176,11 @@ func TestEnvGather_HubDispatch_NeedsGather(t *testing.T) { dispatcher := NewHTTPAgentDispatcherWithClient(memStore, mockClient, true, slog.Default()) agent := &store.Agent{ - ID: "agent-2", + ID: tid("agent-2"), Name: "test-agent-2", Slug: "test-agent-2", - ProjectID: "project-1", - RuntimeBrokerID: "broker-2", + ProjectID: tid("project-1"), + RuntimeBrokerID: tid("broker-2"), AppliedConfig: &store.AgentAppliedConfig{ HarnessConfig: "claude", }, @@ -210,7 +210,7 @@ func TestEnvGather_HubDispatch_FinalizeEnv(t *testing.T) { memStore := createTestStore(t) broker := &store.RuntimeBroker{ - ID: "broker-3", + ID: tid("broker-3"), Name: "test-broker-3", Slug: "test-broker-3", Endpoint: "http://localhost:9800", @@ -224,11 +224,11 @@ func TestEnvGather_HubDispatch_FinalizeEnv(t *testing.T) { dispatcher := NewHTTPAgentDispatcherWithClient(memStore, mockClient, true, slog.Default()) agent := &store.Agent{ - ID: "agent-3", + ID: tid("agent-3"), Name: "test-agent-3", Slug: "test-agent-3", - ProjectID: "project-1", - RuntimeBrokerID: "broker-3", + ProjectID: tid("project-1"), + RuntimeBrokerID: tid("broker-3"), } gatheredEnv := map[string]string{ @@ -257,14 +257,14 @@ func TestEnvGather_HubHandler_202Response(t *testing.T) { ctx := context.Background() // Create project - project := &store.Project{ID: "project-gather", Name: "gather-project", Slug: "gather-project"} + project := &store.Project{ID: tid("project-gather"), Name: "gather-project", Slug: "gather-project"} if err := st.CreateProject(ctx, project); err != nil { t.Fatal(err) } // Create broker broker := &store.RuntimeBroker{ - ID: "broker-gather", Name: "gather-broker", Slug: "gather-broker", + ID: tid("broker-gather"), Name: "gather-broker", Slug: "gather-broker", Endpoint: "http://localhost:9800", Status: store.BrokerStatusOnline, } if err := st.CreateRuntimeBroker(ctx, broker); err != nil { @@ -273,7 +273,7 @@ func TestEnvGather_HubHandler_202Response(t *testing.T) { // Add provider with local path so template can be resolved locally if err := st.AddProjectProvider(ctx, &store.ProjectProvider{ - ProjectID: "project-gather", BrokerID: "broker-gather", + ProjectID: tid("project-gather"), BrokerID: tid("broker-gather"), BrokerName: "test-broker", LocalPath: "/tmp/test-project", }); err != nil { t.Fatal(err) @@ -293,7 +293,7 @@ func TestEnvGather_HubHandler_202Response(t *testing.T) { // Create agent with GatherEnv=true reqBody := map[string]interface{}{ "name": "gather-agent", - "projectId": "project-gather", + "projectId": tid("project-gather"), "template": "claude", "gatherEnv": true, } @@ -333,14 +333,14 @@ func TestEnvGather_HubHandler_ProjectRoute_202Response(t *testing.T) { ctx := context.Background() // Create project - project := &store.Project{ID: "project-gather-route", Name: "gather-route-project", Slug: "gather-route-project"} + project := &store.Project{ID: tid("project-gather-route"), Name: "gather-route-project", Slug: "gather-route-project"} if err := st.CreateProject(ctx, project); err != nil { t.Fatal(err) } // Create broker broker := &store.RuntimeBroker{ - ID: "broker-gather-route", Name: "gather-route-broker", Slug: "gather-route-broker", + ID: tid("broker-gather-route"), Name: "gather-route-broker", Slug: "gather-route-broker", Endpoint: "http://localhost:9800", Status: store.BrokerStatusOnline, } if err := st.CreateRuntimeBroker(ctx, broker); err != nil { @@ -349,7 +349,7 @@ func TestEnvGather_HubHandler_ProjectRoute_202Response(t *testing.T) { // Add provider with local path so template can be resolved locally if err := st.AddProjectProvider(ctx, &store.ProjectProvider{ - ProjectID: "project-gather-route", BrokerID: "broker-gather-route", + ProjectID: tid("project-gather-route"), BrokerID: tid("broker-gather-route"), BrokerName: "test-broker", LocalPath: "/tmp/test-project", }); err != nil { t.Fatal(err) @@ -413,14 +413,14 @@ func TestEnvGather_HubHandler_SubmitEnv(t *testing.T) { ctx := context.Background() // Create project - project := &store.Project{ID: "project-submit", Name: "submit-project", Slug: "submit-project"} + project := &store.Project{ID: tid("project-submit"), Name: "submit-project", Slug: "submit-project"} if err := st.CreateProject(ctx, project); err != nil { t.Fatal(err) } // Create broker broker := &store.RuntimeBroker{ - ID: "broker-submit", Name: "submit-broker", Slug: "submit-broker", + ID: tid("broker-submit"), Name: "submit-broker", Slug: "submit-broker", Endpoint: "http://localhost:9800", Status: store.BrokerStatusOnline, } if err := st.CreateRuntimeBroker(ctx, broker); err != nil { @@ -429,11 +429,11 @@ func TestEnvGather_HubHandler_SubmitEnv(t *testing.T) { // Create agent in provisioning state (as if 202 was already returned) agent := &store.Agent{ - ID: "agent-submit", + ID: tid("agent-submit"), Name: "submit-agent", Slug: "submit-agent", - ProjectID: "project-submit", - RuntimeBrokerID: "broker-submit", + ProjectID: tid("project-submit"), + RuntimeBrokerID: tid("broker-submit"), Phase: string(state.PhaseProvisioning), AppliedConfig: &store.AgentAppliedConfig{ HarnessConfig: "claude", @@ -455,7 +455,7 @@ func TestEnvGather_HubHandler_SubmitEnv(t *testing.T) { }, } - path := "/api/v1/projects/project-submit/agents/submit-agent/env" + path := fmt.Sprintf("/api/v1/projects/%s/agents/submit-agent/env", tid("project-submit")) rec := doRequest(t, srv, http.MethodPost, path, reqBody) if rec.Code != http.StatusOK { @@ -471,7 +471,7 @@ func TestEnvGather_HubHandler_SubmitEnv(t *testing.T) { } // Agent should be updated to running - updated, err := st.GetAgent(ctx, "agent-submit") + updated, err := st.GetAgent(ctx, tid("agent-submit")) if err != nil { t.Fatal(err) } @@ -487,17 +487,17 @@ func TestEnvGather_HubHandler_SubmitEnv_InvalidState(t *testing.T) { ctx := context.Background() // Create project - project := &store.Project{ID: "project-invalid", Name: "invalid-project", Slug: "invalid-project"} + project := &store.Project{ID: tid("project-invalid"), Name: "invalid-project", Slug: "invalid-project"} if err := st.CreateProject(ctx, project); err != nil { t.Fatal(err) } // Create agent in running state (not valid for env submission) agent := &store.Agent{ - ID: "agent-invalid", + ID: tid("agent-invalid"), Name: "invalid-agent", Slug: "invalid-agent", - ProjectID: "project-invalid", + ProjectID: tid("project-invalid"), Phase: string(state.PhaseRunning), } if err := st.CreateAgent(ctx, agent); err != nil { @@ -508,7 +508,7 @@ func TestEnvGather_HubHandler_SubmitEnv_InvalidState(t *testing.T) { "env": map[string]string{"KEY": "value"}, } - path := "/api/v1/projects/project-invalid/agents/invalid-agent/env" + path := fmt.Sprintf("/api/v1/projects/%s/agents/invalid-agent/env", tid("project-invalid")) rec := doRequest(t, srv, http.MethodPost, path, reqBody) if rec.Code != http.StatusConflict { @@ -523,14 +523,14 @@ func TestEnvGather_HubEnvResolution(t *testing.T) { memStore := createTestStore(t) // Create project - project := &store.Project{ID: "project-env", Name: "env-project", Slug: "env-project"} + project := &store.Project{ID: tid("project-env"), Name: "env-project", Slug: "env-project"} if err := memStore.CreateProject(ctx, project); err != nil { t.Fatal(err) } // Create broker broker := &store.RuntimeBroker{ - ID: "broker-env", Name: "env-broker", Slug: "env-broker", + ID: tid("broker-env"), Name: "env-broker", Slug: "env-broker", Endpoint: "http://localhost:9800", Status: store.BrokerStatusOnline, } if err := memStore.CreateRuntimeBroker(ctx, broker); err != nil { @@ -539,11 +539,11 @@ func TestEnvGather_HubEnvResolution(t *testing.T) { // Store env vars in project scope if err := memStore.CreateEnvVar(ctx, &store.EnvVar{ - ID: "env-1", + ID: tid("env-1"), Key: "GROVE_API_KEY", Value: "project-key-value", Scope: "project", - ScopeID: "project-env", + ScopeID: tid("project-env"), }); err != nil { t.Fatal(err) } @@ -555,8 +555,8 @@ func TestEnvGather_HubEnvResolution(t *testing.T) { ID: "agent-env", Name: "env-agent", Slug: "env-agent", - ProjectID: "project-env", - RuntimeBrokerID: "broker-env", + ProjectID: tid("project-env"), + RuntimeBrokerID: tid("broker-env"), AppliedConfig: &store.AgentAppliedConfig{ HarnessConfig: "claude", }, @@ -585,13 +585,13 @@ func TestEnvGather_HubHandler_RetryAfterCancel_GlobalRoute(t *testing.T) { srv, st := testServer(t) ctx := context.Background() - project := &store.Project{ID: "project-retry-global", Name: "retry-global-project", Slug: "retry-global-project"} + project := &store.Project{ID: tid("project-retry-global"), Name: "retry-global-project", Slug: "retry-global-project"} if err := st.CreateProject(ctx, project); err != nil { t.Fatal(err) } broker := &store.RuntimeBroker{ - ID: "broker-retry-global", Name: "retry-global-broker", Slug: "retry-global-broker", + ID: tid("broker-retry-global"), Name: "retry-global-broker", Slug: "retry-global-broker", Endpoint: "http://localhost:9800", Status: store.BrokerStatusOnline, } if err := st.CreateRuntimeBroker(ctx, broker); err != nil { @@ -599,7 +599,7 @@ func TestEnvGather_HubHandler_RetryAfterCancel_GlobalRoute(t *testing.T) { } if err := st.AddProjectProvider(ctx, &store.ProjectProvider{ - ProjectID: "project-retry-global", BrokerID: "broker-retry-global", + ProjectID: tid("project-retry-global"), BrokerID: tid("broker-retry-global"), BrokerName: "test-broker", LocalPath: "/tmp/test-project", }); err != nil { t.Fatal(err) @@ -607,11 +607,11 @@ func TestEnvGather_HubHandler_RetryAfterCancel_GlobalRoute(t *testing.T) { // Simulate a previous cancelled env-gather: agent exists in "provisioning" status staleAgent := &store.Agent{ - ID: "stale-agent-global", + ID: tid("stale-agent-global"), Name: "retry-agent", Slug: "retry-agent", - ProjectID: "project-retry-global", - RuntimeBrokerID: "broker-retry-global", + ProjectID: tid("project-retry-global"), + RuntimeBrokerID: tid("broker-retry-global"), Phase: string(state.PhaseProvisioning), AppliedConfig: &store.AgentAppliedConfig{ HarnessConfig: "claude", @@ -635,7 +635,7 @@ func TestEnvGather_HubHandler_RetryAfterCancel_GlobalRoute(t *testing.T) { // Second create request with GatherEnv=true reqBody := map[string]interface{}{ "name": "retry-agent", - "projectId": "project-retry-global", + "projectId": tid("project-retry-global"), "template": "claude", "gatherEnv": true, } @@ -669,7 +669,7 @@ func TestEnvGather_HubHandler_RetryAfterCancel_GlobalRoute(t *testing.T) { if resp.Agent == nil { t.Fatal("expected agent in response") } - if resp.Agent.ID == "stale-agent-global" { + if resp.Agent.ID == tid("stale-agent-global") { t.Error("expected a new agent ID, got the stale agent ID") } if resp.Agent.Phase != string(state.PhaseProvisioning) { @@ -677,7 +677,7 @@ func TestEnvGather_HubHandler_RetryAfterCancel_GlobalRoute(t *testing.T) { } // The old agent should no longer exist in the store - _, err := st.GetAgent(ctx, "stale-agent-global") + _, err := st.GetAgent(ctx, tid("stale-agent-global")) if err != store.ErrNotFound { t.Errorf("expected stale agent to be deleted, got err=%v", err) } @@ -692,7 +692,7 @@ func TestEnvGather_BuildResponse_SecretScope(t *testing.T) { // Create a user secret for API_KEY if err := st.CreateSecret(ctx, &store.Secret{ - ID: "sec-1", + ID: tid("sec-1"), Key: "API_KEY", EncryptedValue: "encrypted-val", SecretType: store.SecretTypeEnvironment, @@ -711,7 +711,7 @@ func TestEnvGather_BuildResponse_SecretScope(t *testing.T) { ID: "agent-scope-test", Name: "scope-test-agent", OwnerID: "owner-1", - ProjectID: "project-1", + ProjectID: tid("project-1"), } brokerReqs := &RemoteEnvRequirementsResponse{ @@ -756,13 +756,13 @@ func TestEnvGather_SecretInfoRelay(t *testing.T) { srv, st := testServer(t) ctx := context.Background() - project := &store.Project{ID: "project-si-relay", Name: "si-relay-project", Slug: "si-relay-project"} + project := &store.Project{ID: tid("project-si-relay"), Name: "si-relay-project", Slug: "si-relay-project"} if err := st.CreateProject(ctx, project); err != nil { t.Fatal(err) } broker := &store.RuntimeBroker{ - ID: "broker-si-relay", Name: "si-relay-broker", Slug: "si-relay-broker", + ID: tid("broker-si-relay"), Name: "si-relay-broker", Slug: "si-relay-broker", Endpoint: "http://localhost:9800", Status: store.BrokerStatusOnline, } if err := st.CreateRuntimeBroker(ctx, broker); err != nil { @@ -770,7 +770,7 @@ func TestEnvGather_SecretInfoRelay(t *testing.T) { } if err := st.AddProjectProvider(ctx, &store.ProjectProvider{ - ProjectID: "project-si-relay", BrokerID: "broker-si-relay", + ProjectID: tid("project-si-relay"), BrokerID: tid("broker-si-relay"), BrokerName: "test-broker", LocalPath: "/tmp/test-project", }); err != nil { t.Fatal(err) @@ -793,7 +793,7 @@ func TestEnvGather_SecretInfoRelay(t *testing.T) { reqBody := map[string]interface{}{ "name": "si-relay-agent", - "projectId": "project-si-relay", + "projectId": tid("project-si-relay"), "template": "claude", "gatherEnv": true, } @@ -835,13 +835,13 @@ func TestEnvGather_SecretInfoRelayType(t *testing.T) { srv, st := testServer(t) ctx := context.Background() - project := &store.Project{ID: "project-si-type", Name: "si-type-project", Slug: "si-type-project"} + project := &store.Project{ID: tid("project-si-type"), Name: "si-type-project", Slug: "si-type-project"} if err := st.CreateProject(ctx, project); err != nil { t.Fatal(err) } broker := &store.RuntimeBroker{ - ID: "broker-si-type", Name: "si-type-broker", Slug: "si-type-broker", + ID: tid("broker-si-type"), Name: "si-type-broker", Slug: "si-type-broker", Endpoint: "http://localhost:9800", Status: store.BrokerStatusOnline, } if err := st.CreateRuntimeBroker(ctx, broker); err != nil { @@ -849,7 +849,7 @@ func TestEnvGather_SecretInfoRelayType(t *testing.T) { } if err := st.AddProjectProvider(ctx, &store.ProjectProvider{ - ProjectID: "project-si-type", BrokerID: "broker-si-type", + ProjectID: tid("project-si-type"), BrokerID: tid("broker-si-type"), BrokerName: "test-broker", LocalPath: "/tmp/test-project", }); err != nil { t.Fatal(err) @@ -873,7 +873,7 @@ func TestEnvGather_SecretInfoRelayType(t *testing.T) { reqBody := map[string]interface{}{ "name": "si-type-agent", - "projectId": "project-si-type", + "projectId": tid("project-si-type"), "template": "claude", "gatherEnv": true, } @@ -923,13 +923,13 @@ func TestNonGatherEnv_MissingEnvVars_Returns422(t *testing.T) { srv, st := testServer(t) ctx := context.Background() - project := &store.Project{ID: "project-nogather-missing", Name: "nogather-missing-project", Slug: "nogather-missing-project"} + project := &store.Project{ID: tid("project-nogather-missing"), Name: "nogather-missing-project", Slug: "nogather-missing-project"} if err := st.CreateProject(ctx, project); err != nil { t.Fatal(err) } broker := &store.RuntimeBroker{ - ID: "broker-nogather-missing", Name: "nogather-missing-broker", Slug: "nogather-missing-broker", + ID: tid("broker-nogather-missing"), Name: "nogather-missing-broker", Slug: "nogather-missing-broker", Endpoint: "http://localhost:9800", Status: store.BrokerStatusOnline, } if err := st.CreateRuntimeBroker(ctx, broker); err != nil { @@ -937,7 +937,7 @@ func TestNonGatherEnv_MissingEnvVars_Returns422(t *testing.T) { } if err := st.AddProjectProvider(ctx, &store.ProjectProvider{ - ProjectID: "project-nogather-missing", BrokerID: "broker-nogather-missing", + ProjectID: tid("project-nogather-missing"), BrokerID: tid("broker-nogather-missing"), BrokerName: "test-broker", LocalPath: "/tmp/test-project", }); err != nil { t.Fatal(err) @@ -958,7 +958,7 @@ func TestNonGatherEnv_MissingEnvVars_Returns422(t *testing.T) { // Create agent WITHOUT GatherEnv (simulating web/API caller) reqBody := map[string]interface{}{ "name": "nogather-missing-agent", - "projectId": "project-nogather-missing", + "projectId": tid("project-nogather-missing"), "template": "claude", // gatherEnv is NOT set — this is the non-CLI path } @@ -993,7 +993,7 @@ func TestNonGatherEnv_MissingEnvVars_Returns422(t *testing.T) { } // Agent should have been cleaned up from the store - result, err := st.ListAgents(ctx, store.AgentFilter{ProjectID: "project-nogather-missing"}, store.ListOptions{}) + result, err := st.ListAgents(ctx, store.AgentFilter{ProjectID: tid("project-nogather-missing")}, store.ListOptions{}) if err != nil { t.Fatal(err) } @@ -1008,13 +1008,13 @@ func TestNonGatherEnv_MissingEnvVars_ProjectRoute_Returns422(t *testing.T) { srv, st := testServer(t) ctx := context.Background() - project := &store.Project{ID: "project-nogather-route", Name: "nogather-route-project", Slug: "nogather-route-project"} + project := &store.Project{ID: tid("project-nogather-route"), Name: "nogather-route-project", Slug: "nogather-route-project"} if err := st.CreateProject(ctx, project); err != nil { t.Fatal(err) } broker := &store.RuntimeBroker{ - ID: "broker-nogather-route", Name: "nogather-route-broker", Slug: "nogather-route-broker", + ID: tid("broker-nogather-route"), Name: "nogather-route-broker", Slug: "nogather-route-broker", Endpoint: "http://localhost:9800", Status: store.BrokerStatusOnline, } if err := st.CreateRuntimeBroker(ctx, broker); err != nil { @@ -1022,7 +1022,7 @@ func TestNonGatherEnv_MissingEnvVars_ProjectRoute_Returns422(t *testing.T) { } if err := st.AddProjectProvider(ctx, &store.ProjectProvider{ - ProjectID: "project-nogather-route", BrokerID: "broker-nogather-route", + ProjectID: tid("project-nogather-route"), BrokerID: tid("broker-nogather-route"), BrokerName: "test-broker", LocalPath: "/tmp/test-project", }); err != nil { t.Fatal(err) @@ -1063,7 +1063,7 @@ func TestNonGatherEnv_MissingEnvVars_ProjectRoute_Returns422(t *testing.T) { } // Agent should have been cleaned up - result, err := st.ListAgents(ctx, store.AgentFilter{ProjectID: "project-nogather-route"}, store.ListOptions{}) + result, err := st.ListAgents(ctx, store.AgentFilter{ProjectID: tid("project-nogather-route")}, store.ListOptions{}) if err != nil { t.Fatal(err) } @@ -1078,13 +1078,13 @@ func TestNonGatherEnv_AllSatisfied_Returns201(t *testing.T) { srv, st := testServer(t) ctx := context.Background() - project := &store.Project{ID: "project-nogather-ok", Name: "nogather-ok-project", Slug: "nogather-ok-project"} + project := &store.Project{ID: tid("project-nogather-ok"), Name: "nogather-ok-project", Slug: "nogather-ok-project"} if err := st.CreateProject(ctx, project); err != nil { t.Fatal(err) } broker := &store.RuntimeBroker{ - ID: "broker-nogather-ok", Name: "nogather-ok-broker", Slug: "nogather-ok-broker", + ID: tid("broker-nogather-ok"), Name: "nogather-ok-broker", Slug: "nogather-ok-broker", Endpoint: "http://localhost:9800", Status: store.BrokerStatusOnline, } if err := st.CreateRuntimeBroker(ctx, broker); err != nil { @@ -1092,7 +1092,7 @@ func TestNonGatherEnv_AllSatisfied_Returns201(t *testing.T) { } if err := st.AddProjectProvider(ctx, &store.ProjectProvider{ - ProjectID: "project-nogather-ok", BrokerID: "broker-nogather-ok", + ProjectID: tid("project-nogather-ok"), BrokerID: tid("broker-nogather-ok"), BrokerName: "test-broker", LocalPath: "/tmp/test-project", }); err != nil { t.Fatal(err) @@ -1106,7 +1106,7 @@ func TestNonGatherEnv_AllSatisfied_Returns201(t *testing.T) { // Create agent WITHOUT GatherEnv — all env satisfied reqBody := map[string]interface{}{ "name": "nogather-ok-agent", - "projectId": "project-nogather-ok", + "projectId": tid("project-nogather-ok"), "template": "claude", } @@ -1146,13 +1146,13 @@ func TestEnvGather_HubHandler_RetryAfterCancel_ProjectRoute(t *testing.T) { srv, st := testServer(t) ctx := context.Background() - project := &store.Project{ID: "project-retry-route", Name: "retry-route-project", Slug: "retry-route-project"} + project := &store.Project{ID: tid("project-retry-route"), Name: "retry-route-project", Slug: "retry-route-project"} if err := st.CreateProject(ctx, project); err != nil { t.Fatal(err) } broker := &store.RuntimeBroker{ - ID: "broker-retry-route", Name: "retry-route-broker", Slug: "retry-route-broker", + ID: tid("broker-retry-route"), Name: "retry-route-broker", Slug: "retry-route-broker", Endpoint: "http://localhost:9800", Status: store.BrokerStatusOnline, } if err := st.CreateRuntimeBroker(ctx, broker); err != nil { @@ -1160,7 +1160,7 @@ func TestEnvGather_HubHandler_RetryAfterCancel_ProjectRoute(t *testing.T) { } if err := st.AddProjectProvider(ctx, &store.ProjectProvider{ - ProjectID: "project-retry-route", BrokerID: "broker-retry-route", + ProjectID: tid("project-retry-route"), BrokerID: tid("broker-retry-route"), BrokerName: "test-broker", LocalPath: "/tmp/test-project", }); err != nil { t.Fatal(err) @@ -1168,11 +1168,11 @@ func TestEnvGather_HubHandler_RetryAfterCancel_ProjectRoute(t *testing.T) { // Simulate a previous cancelled env-gather: agent exists in "provisioning" status staleAgent := &store.Agent{ - ID: "stale-agent-route", + ID: tid("stale-agent-route"), Name: "retry-route-agent", Slug: "retry-route-agent", - ProjectID: "project-retry-route", - RuntimeBrokerID: "broker-retry-route", + ProjectID: tid("project-retry-route"), + RuntimeBrokerID: tid("broker-retry-route"), Phase: string(state.PhaseProvisioning), AppliedConfig: &store.AgentAppliedConfig{ HarnessConfig: "claude", @@ -1225,7 +1225,7 @@ func TestEnvGather_HubHandler_RetryAfterCancel_ProjectRoute(t *testing.T) { if resp.Agent == nil { t.Fatal("expected agent in response") } - if resp.Agent.ID == "stale-agent-route" { + if resp.Agent.ID == tid("stale-agent-route") { t.Error("expected a new agent ID, got the stale agent ID") } if resp.Agent.Phase != string(state.PhaseProvisioning) { @@ -1233,7 +1233,7 @@ func TestEnvGather_HubHandler_RetryAfterCancel_ProjectRoute(t *testing.T) { } // The old agent should no longer exist in the store - _, err := st.GetAgent(ctx, "stale-agent-route") + _, err := st.GetAgent(ctx, tid("stale-agent-route")) if err != store.ErrNotFound { t.Errorf("expected stale agent to be deleted, got err=%v", err) } @@ -1248,13 +1248,13 @@ func TestProjectRoute_ResolvesUserScopedEnvVars(t *testing.T) { srv, st := testServer(t) ctx := context.Background() - project := &store.Project{ID: "project-owner-env", Name: "owner-env-project", Slug: "owner-env-project"} + project := &store.Project{ID: tid("project-owner-env"), Name: "owner-env-project", Slug: "owner-env-project"} if err := st.CreateProject(ctx, project); err != nil { t.Fatal(err) } broker := &store.RuntimeBroker{ - ID: "broker-owner-env", Name: "owner-env-broker", Slug: "owner-env-broker", + ID: tid("broker-owner-env"), Name: "owner-env-broker", Slug: "owner-env-broker", Endpoint: "http://localhost:9800", Status: store.BrokerStatusOnline, } if err := st.CreateRuntimeBroker(ctx, broker); err != nil { @@ -1262,7 +1262,7 @@ func TestProjectRoute_ResolvesUserScopedEnvVars(t *testing.T) { } if err := st.AddProjectProvider(ctx, &store.ProjectProvider{ - ProjectID: "project-owner-env", BrokerID: "broker-owner-env", + ProjectID: tid("project-owner-env"), BrokerID: tid("broker-owner-env"), BrokerName: "test-broker", LocalPath: "/tmp/test-project", }); err != nil { t.Fatal(err) @@ -1270,7 +1270,7 @@ func TestProjectRoute_ResolvesUserScopedEnvVars(t *testing.T) { // Store a user-scoped env var for the dev-user (dev auth identity) if err := st.CreateEnvVar(ctx, &store.EnvVar{ - ID: "env-owner-1", + ID: tid("env-owner-1"), Key: "GEMINI_API_KEY", Value: "user-scoped-gemini-key", Scope: "user", @@ -1335,13 +1335,13 @@ func TestProjectRoute_ResolvesUserScopedSecrets(t *testing.T) { srv, st := testServer(t) ctx := context.Background() - project := &store.Project{ID: "project-owner-secret", Name: "owner-secret-project", Slug: "owner-secret-project"} + project := &store.Project{ID: tid("project-owner-secret"), Name: "owner-secret-project", Slug: "owner-secret-project"} if err := st.CreateProject(ctx, project); err != nil { t.Fatal(err) } broker := &store.RuntimeBroker{ - ID: "broker-owner-secret", Name: "owner-secret-broker", Slug: "owner-secret-broker", + ID: tid("broker-owner-secret"), Name: "owner-secret-broker", Slug: "owner-secret-broker", Endpoint: "http://localhost:9800", Status: store.BrokerStatusOnline, } if err := st.CreateRuntimeBroker(ctx, broker); err != nil { @@ -1349,7 +1349,7 @@ func TestProjectRoute_ResolvesUserScopedSecrets(t *testing.T) { } if err := st.AddProjectProvider(ctx, &store.ProjectProvider{ - ProjectID: "project-owner-secret", BrokerID: "broker-owner-secret", + ProjectID: tid("project-owner-secret"), BrokerID: tid("broker-owner-secret"), BrokerName: "test-broker", LocalPath: "/tmp/test-project", }); err != nil { t.Fatal(err) diff --git a/pkg/hub/events.go b/pkg/hub/events.go index 3c5e8b33e..2d644ce09 100644 --- a/pkg/hub/events.go +++ b/pkg/hub/events.go @@ -40,6 +40,16 @@ type EventPublisher interface { PublishUserMessage(ctx context.Context, msg *store.Message) PublishAllowListChanged(ctx context.Context, action string, email string) PublishInviteChanged(ctx context.Context, action string, inviteID string, codePrefix string) + // PublishDispatchDone emits a slim completion event on + // broker.dispatch..done so the originator's subscription wakes + // and reads the result from the dispatch row (design §6.3). + PublishDispatchDone(ctx context.Context, dispatchID string) + // Subscribe returns a channel that receives events matching the given + // subject patterns, along with an unsubscribe function. Patterns use + // NATS-style wildcards: '*' matches a single token, '>' matches the + // remainder. The returned channel is buffered; implementations may drop + // events on a full buffer (backpressure). + Subscribe(patterns ...string) (<-chan Event, func()) Close() } @@ -60,8 +70,16 @@ func (noopEventPublisher) PublishNotification(_ context.Context, _ *store.Notifi func (noopEventPublisher) PublishUserMessage(_ context.Context, _ *store.Message) {} func (noopEventPublisher) PublishAllowListChanged(_ context.Context, _, _ string) {} func (noopEventPublisher) PublishInviteChanged(_ context.Context, _, _, _ string) {} +func (noopEventPublisher) PublishDispatchDone(_ context.Context, _ string) {} func (noopEventPublisher) Close() {} +// Subscribe on the no-op publisher returns a nil channel (which blocks forever +// on receive) and a no-op unsubscribe. Callers that need real subscriptions +// must wire a ChannelEventPublisher or PostgresEventPublisher. +func (noopEventPublisher) Subscribe(_ ...string) (<-chan Event, func()) { + return nil, func() {} +} + // Event is a published event with a subject and JSON-encoded data. type Event struct { Subject string @@ -199,9 +217,28 @@ type InviteChangedEvent struct { CodePrefix string `json:"codePrefix,omitempty"` } +// DispatchDoneEvent is a slim completion event emitted by the owner when a +// broker_dispatch reaches terminal state (done/failed). The originator +// subscribes to broker.dispatch..done BEFORE writing intent and reads the +// result from the dispatch row on wake (design §6.3). +type DispatchDoneEvent struct { + DispatchID string `json:"dispatchId"` +} + +// eventBuilder holds the EventPublisher Publish* method implementations shared +// by every publisher backend. Each method marshals a typed event struct and +// hands the (subject, event) pair to sink, which the embedding publisher wires +// to its own delivery mechanism (in-process fan-out for ChannelEventPublisher, +// Postgres NOTIFY for PostgresEventPublisher). Keeping the subject taxonomy in +// one place guarantees both backends publish identical subjects and payloads. +type eventBuilder struct { + sink func(subject string, event interface{}) +} + // ChannelEventPublisher is an in-process event publisher that fans out events // to Go channel subscribers using NATS-style subject matching. type ChannelEventPublisher struct { + eventBuilder mu sync.RWMutex subscribers map[string][]chan Event closed bool @@ -209,9 +246,11 @@ type ChannelEventPublisher struct { // NewChannelEventPublisher creates a new ChannelEventPublisher. func NewChannelEventPublisher() *ChannelEventPublisher { - return &ChannelEventPublisher{ + p := &ChannelEventPublisher{ subscribers: make(map[string][]chan Event), } + p.sink = p.publish + return p } // Subscribe returns a channel that receives events matching the given patterns, @@ -298,7 +337,7 @@ func (p *ChannelEventPublisher) Close() { // PublishAgentStatus publishes an agent status event to both agent-specific // and project-scoped subjects (dual-publish pattern). -func (p *ChannelEventPublisher) PublishAgentStatus(_ context.Context, agent *store.Agent) { +func (p *eventBuilder) PublishAgentStatus(_ context.Context, agent *store.Agent) { evt := AgentStatusEvent{ AgentID: agent.ID, ProjectID: agent.ProjectID, @@ -321,16 +360,16 @@ func (p *ChannelEventPublisher) PublishAgentStatus(_ context.Context, agent *sto if detail != (AgentDetail{}) { evt.Detail = &detail } - p.publish("agent."+agent.ID+".status", evt) + p.sink("agent."+agent.ID+".status", evt) if agent.ProjectID != "" { - p.publish("project."+agent.ProjectID+".agent.status", evt) - p.publish("grove."+agent.ProjectID+".agent.status", evt) + p.sink("project."+agent.ProjectID+".agent.status", evt) + p.sink("grove."+agent.ProjectID+".agent.status", evt) } } // PublishAgentCreated publishes an agent created event to both agent-specific // and project-scoped subjects (dual-publish pattern). -func (p *ChannelEventPublisher) PublishAgentCreated(_ context.Context, agent *store.Agent) { +func (p *eventBuilder) PublishAgentCreated(_ context.Context, agent *store.Agent) { evt := AgentCreatedEvent{ AgentID: agent.ID, ProjectID: agent.ProjectID, @@ -351,63 +390,63 @@ func (p *ChannelEventPublisher) PublishAgentCreated(_ context.Context, agent *st if !agent.Created.IsZero() { evt.Created = agent.Created.Format("2006-01-02T15:04:05Z07:00") } - p.publish("agent."+agent.ID+".created", evt) + p.sink("agent."+agent.ID+".created", evt) if agent.ProjectID != "" { - p.publish("project."+agent.ProjectID+".agent.created", evt) - p.publish("grove."+agent.ProjectID+".agent.created", evt) + p.sink("project."+agent.ProjectID+".agent.created", evt) + p.sink("grove."+agent.ProjectID+".agent.created", evt) } } // PublishAgentDeleted publishes an agent deleted event to both agent-specific // and project-scoped subjects (dual-publish pattern). -func (p *ChannelEventPublisher) PublishAgentDeleted(_ context.Context, agentID, projectID string) { +func (p *eventBuilder) PublishAgentDeleted(_ context.Context, agentID, projectID string) { evt := AgentDeletedEvent{ AgentID: agentID, ProjectID: projectID, GroveID: projectID, } - p.publish("agent."+agentID+".deleted", evt) + p.sink("agent."+agentID+".deleted", evt) if projectID != "" { - p.publish("project."+projectID+".agent.deleted", evt) - p.publish("grove."+projectID+".agent.deleted", evt) + p.sink("project."+projectID+".agent.deleted", evt) + p.sink("grove."+projectID+".agent.deleted", evt) } } // PublishProjectCreated publishes a project created event. -func (p *ChannelEventPublisher) PublishProjectCreated(_ context.Context, project *store.Project) { +func (p *eventBuilder) PublishProjectCreated(_ context.Context, project *store.Project) { evt := ProjectCreatedEvent{ ProjectID: project.ID, GroveID: project.ID, Name: project.Name, Slug: project.Slug, } - p.publish("project."+project.ID+".created", evt) - p.publish("grove."+project.ID+".created", evt) + p.sink("project."+project.ID+".created", evt) + p.sink("grove."+project.ID+".created", evt) } // PublishProjectUpdated publishes a project updated event. -func (p *ChannelEventPublisher) PublishProjectUpdated(_ context.Context, project *store.Project) { +func (p *eventBuilder) PublishProjectUpdated(_ context.Context, project *store.Project) { evt := ProjectUpdatedEvent{ ProjectID: project.ID, GroveID: project.ID, Name: project.Name, } - p.publish("project."+project.ID+".updated", evt) - p.publish("grove."+project.ID+".updated", evt) + p.sink("project."+project.ID+".updated", evt) + p.sink("grove."+project.ID+".updated", evt) } // PublishProjectDeleted publishes a project deleted event. -func (p *ChannelEventPublisher) PublishProjectDeleted(_ context.Context, projectID string) { +func (p *eventBuilder) PublishProjectDeleted(_ context.Context, projectID string) { evt := ProjectDeletedEvent{ ProjectID: projectID, GroveID: projectID, } - p.publish("project."+projectID+".deleted", evt) - p.publish("grove."+projectID+".deleted", evt) + p.sink("project."+projectID+".deleted", evt) + p.sink("grove."+projectID+".deleted", evt) } // PublishBrokerConnected publishes broker connection events, one per project the broker serves. -func (p *ChannelEventPublisher) PublishBrokerConnected(_ context.Context, brokerID, brokerName string, projectIDs []string) { +func (p *eventBuilder) PublishBrokerConnected(_ context.Context, brokerID, brokerName string, projectIDs []string) { for _, pid := range projectIDs { evt := BrokerProjectEvent{ BrokerID: brokerID, @@ -416,13 +455,13 @@ func (p *ChannelEventPublisher) PublishBrokerConnected(_ context.Context, broker GroveID: pid, Status: "online", } - p.publish("project."+pid+".broker.status", evt) - p.publish("grove."+pid+".broker.status", evt) + p.sink("project."+pid+".broker.status", evt) + p.sink("grove."+pid+".broker.status", evt) } } // PublishBrokerDisconnected publishes broker disconnection events, one per project the broker serves. -func (p *ChannelEventPublisher) PublishBrokerDisconnected(_ context.Context, brokerID string, projectIDs []string) { +func (p *eventBuilder) PublishBrokerDisconnected(_ context.Context, brokerID string, projectIDs []string) { for _, pid := range projectIDs { evt := BrokerProjectEvent{ BrokerID: brokerID, @@ -430,22 +469,22 @@ func (p *ChannelEventPublisher) PublishBrokerDisconnected(_ context.Context, bro GroveID: pid, Status: "offline", } - p.publish("project."+pid+".broker.status", evt) - p.publish("grove."+pid+".broker.status", evt) + p.sink("project."+pid+".broker.status", evt) + p.sink("grove."+pid+".broker.status", evt) } } // PublishBrokerStatus publishes a general broker status event. -func (p *ChannelEventPublisher) PublishBrokerStatus(_ context.Context, brokerID, status string) { +func (p *eventBuilder) PublishBrokerStatus(_ context.Context, brokerID, status string) { evt := BrokerStatusEvent{ BrokerID: brokerID, Status: status, } - p.publish("broker."+brokerID+".status", evt) + p.sink("broker."+brokerID+".status", evt) } // PublishNotification publishes a user notification event. -func (p *ChannelEventPublisher) PublishNotification(_ context.Context, notif *store.Notification) { +func (p *eventBuilder) PublishNotification(_ context.Context, notif *store.Notification) { evt := NotificationCreatedEvent{ ID: notif.ID, AgentID: notif.AgentID, @@ -455,30 +494,30 @@ func (p *ChannelEventPublisher) PublishNotification(_ context.Context, notif *st Message: notif.Message, CreatedAt: notif.CreatedAt.Format("2006-01-02T15:04:05.000Z"), } - p.publish("notification.created", evt) + p.sink("notification.created", evt) if notif.ProjectID != "" { - p.publish("project."+notif.ProjectID+".notification", evt) - p.publish("grove."+notif.ProjectID+".notification", evt) + p.sink("project."+notif.ProjectID+".notification", evt) + p.sink("grove."+notif.ProjectID+".notification", evt) } } // PublishAllowListChanged publishes an allow list change event. // Email is intentionally omitted from the event to avoid PII leak via SSE. -func (p *ChannelEventPublisher) PublishAllowListChanged(_ context.Context, action, _ string) { +func (p *eventBuilder) PublishAllowListChanged(_ context.Context, action, _ string) { evt := AllowListChangedEvent{ Action: action, } - p.publish("admin.allowlist.changed", evt) + p.sink("admin.allowlist.changed", evt) } // PublishInviteChanged publishes an invite code change event. -func (p *ChannelEventPublisher) PublishInviteChanged(_ context.Context, action, inviteID, codePrefix string) { +func (p *eventBuilder) PublishInviteChanged(_ context.Context, action, inviteID, codePrefix string) { evt := InviteChangedEvent{ Action: action, InviteID: inviteID, CodePrefix: codePrefix, } - p.publish("admin.invite.changed", evt) + p.sink("admin.invite.changed", evt) } // PublishUserMessage publishes a user.message event when a message involving @@ -493,7 +532,7 @@ func (p *ChannelEventPublisher) PublishInviteChanged(_ context.Context, action, // (only when the recipient is a user) // - agent..message — per-agent conversation streams (both // directions; subscribers filter by user participation themselves) -func (p *ChannelEventPublisher) PublishUserMessage(_ context.Context, msg *store.Message) { +func (p *eventBuilder) PublishUserMessage(_ context.Context, msg *store.Message) { evt := UserMessageEvent{ ID: msg.ID, ProjectID: msg.ProjectID, @@ -516,17 +555,26 @@ func (p *ChannelEventPublisher) PublishUserMessage(_ context.Context, msg *store // count by mixing user→agent prompts with agent→user replies. recipientIsUser := strings.HasPrefix(msg.Recipient, "user:") if recipientIsUser && msg.RecipientID != "" { - p.publish("user."+msg.RecipientID+".message", evt) + p.sink("user."+msg.RecipientID+".message", evt) } if recipientIsUser && msg.ProjectID != "" { - p.publish("project."+msg.ProjectID+".user.message", evt) - p.publish("grove."+msg.ProjectID+".user.message", evt) + p.sink("project."+msg.ProjectID+".user.message", evt) + p.sink("grove."+msg.ProjectID+".user.message", evt) } if msg.AgentID != "" { - p.publish("agent."+msg.AgentID+".message", evt) + p.sink("agent."+msg.AgentID+".message", evt) } } +// PublishDispatchDone emits a slim completion event when a broker_dispatch row +// reaches terminal state. The subject broker.dispatch..done is what the +// originator subscribes to before writing intent (design §6.3). +func (p *eventBuilder) PublishDispatchDone(_ context.Context, dispatchID string) { + p.sink("broker.dispatch."+dispatchID+".done", DispatchDoneEvent{ + DispatchID: dispatchID, + }) +} + // subjectMatchesPattern checks if a subject matches a NATS-style pattern. // '*' matches exactly one token, '>' matches one or more remaining tokens. // Tokens are dot-separated. diff --git a/pkg/hub/events_integration_test.go b/pkg/hub/events_integration_test.go index 3cc190e9e..101b71f22 100644 --- a/pkg/hub/events_integration_test.go +++ b/pkg/hub/events_integration_test.go @@ -41,8 +41,9 @@ func (noopDispatcher) DispatchAgentProvision(_ context.Context, _ *store.Agent) func (noopDispatcher) DispatchAgentStart(_ context.Context, _ *store.Agent, _ string) error { return nil } -func (noopDispatcher) DispatchAgentStop(_ context.Context, _ *store.Agent) error { return nil } -func (noopDispatcher) DispatchAgentRestart(_ context.Context, _ *store.Agent) error { return nil } +func (noopDispatcher) DispatchAgentStop(_ context.Context, _ *store.Agent) error { return nil } +func (noopDispatcher) DispatchAgentRestart(_ context.Context, _ *store.Agent) error { return nil } +func (noopDispatcher) DispatchAgentResetAuth(_ context.Context, _ *store.Agent) error { return nil } func (noopDispatcher) DispatchAgentDelete(_ context.Context, _ *store.Agent, _, _, _ bool, _ time.Time) error { return nil } @@ -77,7 +78,7 @@ func setupEventTestServer(t *testing.T) (*Server, store.Store, *ChannelEventPubl t.Cleanup(func() { pub.Close() }) project := &store.Project{ - ID: "project-evt", + ID: tid("project-evt"), Name: "Event Test Project", Slug: "event-test-project", Visibility: store.VisibilityPrivate, @@ -85,7 +86,7 @@ func setupEventTestServer(t *testing.T) (*Server, store.Store, *ChannelEventPubl require.NoError(t, s.CreateProject(ctx, project)) broker := &store.RuntimeBroker{ - ID: "broker-evt", + ID: tid("broker-evt"), Name: "Event Test Broker", Slug: "event-test-broker", Status: store.BrokerStatusOnline, @@ -140,8 +141,8 @@ func TestEventPublisher_DeleteAgentEmitsEvent(t *testing.T) { ctx := context.Background() agent := &store.Agent{ - ID: "agent-evt-del", - Slug: "agent-evt-del", + ID: tid("agent-evt-del"), + Slug: tid("agent-evt-del"), Name: "Delete Me", ProjectID: project.ID, Phase: string(state.PhaseRunning), @@ -153,7 +154,7 @@ func TestEventPublisher_DeleteAgentEmitsEvent(t *testing.T) { defer unsub() // Delete agent via API - rec := doRequest(t, srv, http.MethodDelete, "/api/v1/agents/agent-evt-del", nil) + rec := doRequest(t, srv, http.MethodDelete, "/api/v1/agents/"+agent.ID, nil) require.Equal(t, http.StatusNoContent, rec.Code) select { @@ -161,7 +162,7 @@ func TestEventPublisher_DeleteAgentEmitsEvent(t *testing.T) { assert.Equal(t, "project."+project.ID+".agent.deleted", evt.Subject) var data AgentDeletedEvent require.NoError(t, json.Unmarshal(evt.Data, &data)) - assert.Equal(t, "agent-evt-del", data.AgentID) + assert.Equal(t, tid("agent-evt-del"), data.AgentID) assert.Equal(t, project.ID, data.ProjectID) case <-time.After(2 * time.Second): t.Fatal("timeout waiting for agent deleted event") diff --git a/pkg/hub/events_postgres.go b/pkg/hub/events_postgres.go new file mode 100644 index 000000000..af4afb6dc --- /dev/null +++ b/pkg/hub/events_postgres.go @@ -0,0 +1,744 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package hub + +import ( + "context" + "crypto/sha256" + "encoding/hex" + "encoding/json" + "errors" + "fmt" + "log/slog" + "strings" + "sync" + "time" + + "github.com/google/uuid" + "github.com/jackc/pgx/v5" + "github.com/jackc/pgx/v5/pgconn" + "github.com/jackc/pgx/v5/pgxpool" + "go.opentelemetry.io/otel/attribute" + + "github.com/GoogleCloudPlatform/scion/pkg/observability/dbmetrics" +) + +// PostgresEventPublisher is an EventPublisher backed by PostgreSQL LISTEN/NOTIFY. +// It delivers events across replicas: a NOTIFY issued on one hub instance is +// received by the listener goroutine on every instance (including the +// publisher), which fans the event out to that instance's in-process +// subscribers using the same NATS-style subject matching as +// ChannelEventPublisher. +// +// Channel model — per grove plus a global channel (flat exact-match, since +// Postgres channels do not support wildcards): +// +// - Grove-scoped subjects ("project..*" / "grove..*") are published +// to a per-grove channel (scion_ev_g_) AND to the global channel. The +// per-grove channel lets a replica that only watches a specific grove (e.g. +// a browser SSE stream) LISTEN on just that channel instead of the firehose. +// - All other subjects ("agent.*", "user.*", "broker.*", "admin.*", +// "notification.*") are published to the global channel only. +// - Subscriptions with a concrete grove id resolve to that grove's channel; +// everything else (grove-spanning wildcards used by the notification +// dispatcher and message-broker proxy, and non-grove subjects) resolves to +// the global channel. Each subscriber's patterns are grouped by the channel +// they resolve to, so an event arriving on a channel is only matched against +// the patterns that opted into that channel — no double delivery. +// +// Delivery is performed exclusively by the listener (events are not fanned out +// locally at publish time). This gives transactional publish semantics for free +// with PublishTx: a NOTIFY enrolled in a transaction that rolls back is never +// sent, so subscribers — local or remote — never observe it. +// +// Payloads larger than the Postgres 8000-byte NOTIFY limit are stored in the +// scion_event_payloads table and the NOTIFY carries a reference id; the listener +// refetches the payload on receipt (reference-and-refetch). A background +// goroutine purges old payload rows on a TTL so multiple replicas can each +// refetch the same oversized event. +type PostgresEventPublisher struct { + eventBuilder + + pool *pgxpool.Pool + dsn string + metrics dbmetrics.Recorder + log *slog.Logger + + ctx context.Context + cancel context.CancelFunc + wg sync.WaitGroup + + mu sync.RWMutex + // subs maps a Postgres channel -> subscriber -> the subset of that + // subscriber's patterns that resolve to this channel. + subs map[string]map[*pgSubscription][]string + // desired counts how many subscriptions need each channel LISTENed. + desired map[string]int + closed bool +} + +// pgSubscription is a single Subscribe registration. +type pgSubscription struct { + ch chan Event + once sync.Once +} + +// pgEnvelope is the JSON wire format carried in a NOTIFY payload. The event type +// is included so out-of-process consumers can route without re-deriving it from +// the subject. +type pgEnvelope struct { + Type string `json:"type"` + Subject string `json:"subject"` + Data json.RawMessage `json:"data,omitempty"` + Ref string `json:"ref,omitempty"` // payload-table id when oversized + TS int64 `json:"ts,omitempty"` // publish time, unix nanos (for latency) +} + +const ( + pgChannelPrefix = "scion_ev_" + pgGlobalChannel = "scion_ev_global" + // pgNotifyMaxPayload is the threshold above which an event is offloaded to + // the payload table. Postgres rejects NOTIFY payloads of 8000 bytes or more; + // the margin leaves room for the envelope wrapping around the data. + pgNotifyMaxPayload = 7000 + // maxPGIdentifier is the Postgres identifier length limit (NAMEDATALEN-1). + maxPGIdentifier = 63 + // listenPollInterval bounds how long the listener blocks in + // WaitForNotification before waking to apply pending LISTEN/UNLISTEN changes. + // A WaitForNotification deadline does not invalidate the connection (pgconn + // treats a read timeout as recoverable), so this is a cheap idle poll. + listenPollInterval = time.Second + // payloadTTL is how long oversized payloads are retained for refetch. + payloadTTL = 60 * time.Second + // publishTimeout bounds a single autocommit publish (Publish* methods). These + // run synchronously on the caller's goroutine — typically a request handler + // right after a CRUD write — and acquire a connection from the event pool. On + // an undersized / connection-starved instance (see CONNECTION-BUDGET.md) that + // acquire could otherwise block indefinitely, stalling the handler and + // silently never emitting the NOTIFY. Bounding it converts that failure mode + // into a logged error and a dropped event (publishing is fire-and-forget), + // keeping CRUD responsive. The transactional path (PublishTx) is unaffected: + // it uses the caller's context and transaction. + publishTimeout = 5 * time.Second +) + +// pgExecutor is satisfied by both *pgxpool.Pool and pgx.Tx, letting the publish +// path run either against an autocommit pool connection or inside a caller's +// transaction. +type pgExecutor interface { + Exec(ctx context.Context, sql string, args ...any) (pgconn.CommandTag, error) +} + +// compile-time check that PostgresEventPublisher satisfies EventPublisher. +var _ EventPublisher = (*PostgresEventPublisher)(nil) + +// NewPostgresEventPublisher connects to Postgres at dsn, ensures the +// payload-offload table exists, and starts the listener and maintenance +// goroutines. If metrics is nil a disabled (no-op) recorder is used. +func NewPostgresEventPublisher(ctx context.Context, dsn string, metrics dbmetrics.Recorder, log *slog.Logger) (*PostgresEventPublisher, error) { + if metrics == nil { + metrics = dbmetrics.NewDisabled() + } + if log == nil { + log = slog.Default() + } + + poolCfg, err := pgxpool.ParseConfig(dsn) + if err != nil { + return nil, fmt.Errorf("parsing postgres event dsn: %w", err) + } + applyEventPoolKeepalives(poolCfg) + + pool, err := pgxpool.NewWithConfig(ctx, poolCfg) + if err != nil { + return nil, fmt.Errorf("creating postgres event pool: %w", err) + } + if err := pool.Ping(ctx); err != nil { + pool.Close() + return nil, fmt.Errorf("pinging postgres for events: %w", err) + } + + pubCtx, cancel := context.WithCancel(context.Background()) + p := &PostgresEventPublisher{ + pool: pool, + dsn: dsn, + metrics: metrics, + log: log, + ctx: pubCtx, + cancel: cancel, + subs: make(map[string]map[*pgSubscription][]string), + desired: make(map[string]int), + } + p.sink = p.publish + + if err := p.ensurePayloadTable(ctx); err != nil { + cancel() + pool.Close() + return nil, err + } + + p.wg.Add(2) + go p.runListener() + go p.runMaintenance() + + log.Info("Postgres event publisher started") + return p, nil +} + +// ensurePayloadTable creates the oversized-payload offload table if absent. +func (p *PostgresEventPublisher) ensurePayloadTable(ctx context.Context) error { + const ddl = ` +CREATE TABLE IF NOT EXISTS scion_event_payloads ( + id UUID PRIMARY KEY, + subject TEXT NOT NULL, + data BYTEA NOT NULL, + created_at TIMESTAMPTZ NOT NULL DEFAULT now() +); +CREATE INDEX IF NOT EXISTS scion_event_payloads_created_at_idx + ON scion_event_payloads (created_at);` + if _, err := p.pool.Exec(ctx, ddl); err != nil { + return fmt.Errorf("creating scion_event_payloads table: %w", err) + } + return nil +} + +// publish is the sink wired into eventBuilder. It marshals and NOTIFYs on the +// pool (autocommit). Errors are logged rather than returned because the +// EventPublisher Publish* methods are fire-and-forget. +func (p *PostgresEventPublisher) publish(subject string, event interface{}) { + // Bound the publish so a saturated event pool surfaces a logged error instead + // of blocking the calling (often request-handler) goroutine forever. See + // publishTimeout. + ctx, cancel := context.WithTimeout(p.ctx, publishTimeout) + defer cancel() + if err := p.buildAndNotify(ctx, p.pool, subject, event); err != nil { + p.log.Error("Failed to publish event via NOTIFY", "subject", subject, "error", err) + } +} + +// PublishTx publishes an event using a caller-supplied executor, giving an +// atomic write+publish when that executor is a transaction (pgx.Tx satisfies +// pgExecutor): the NOTIFY is enrolled in the transaction and only delivered if +// it commits. If the transaction rolls back, no subscriber (local or remote) +// observes the event. +func (p *PostgresEventPublisher) PublishTx(ctx context.Context, tx pgExecutor, subject string, event interface{}) error { + return p.buildAndNotify(ctx, tx, subject, event) +} + +// buildAndNotify marshals event into an envelope, offloading the data to the +// payload table when it would exceed the NOTIFY size limit, and issues one +// NOTIFY per destination channel via exec. +func (p *PostgresEventPublisher) buildAndNotify(ctx context.Context, exec pgExecutor, subject string, event interface{}) error { + data, err := json.Marshal(event) + if err != nil { + return fmt.Errorf("marshaling event %s: %w", subject, err) + } + + env := pgEnvelope{ + Type: eventTypeName(event), + Subject: subject, + Data: data, + TS: time.Now().UnixNano(), + } + payload, err := json.Marshal(env) + if err != nil { + return fmt.Errorf("marshaling envelope %s: %w", subject, err) + } + + scope := channelScope(subject) + p.metrics.RecordPayloadSize(ctx, int64(len(payload)), attribute.String("scope", scope)) + + if len(payload) > pgNotifyMaxPayload { + id := uuid.NewString() + if _, err := exec.Exec(ctx, + `INSERT INTO scion_event_payloads (id, subject, data) VALUES ($1, $2, $3)`, + id, subject, data, + ); err != nil { + return fmt.Errorf("storing oversized payload for %s: %w", subject, err) + } + env.Data = nil + env.Ref = id + payload, err = json.Marshal(env) + if err != nil { + return fmt.Errorf("marshaling oversized envelope %s: %w", subject, err) + } + } + + for _, channel := range channelsForSubject(subject) { + if _, err := exec.Exec(ctx, `SELECT pg_notify($1, $2)`, channel, string(payload)); err != nil { + return fmt.Errorf("pg_notify on %s: %w", channel, err) + } + } + + p.metrics.IncPublished(ctx, 1, attribute.String("scope", scope)) + return nil +} + +// Subscribe registers patterns and returns a buffered channel plus an +// unsubscribe function. Patterns use NATS-style wildcards; matching is performed +// against the subject of each received event. The listener begins LISTENing on +// any newly-needed Postgres channels within listenPollInterval. +func (p *PostgresEventPublisher) Subscribe(patterns ...string) (<-chan Event, func()) { + ch := make(chan Event, 64) + sub := &pgSubscription{ch: ch} + + // Group patterns by the Postgres channel they resolve to. + byChannel := make(map[string][]string) + for _, pattern := range patterns { + for _, channel := range channelsForPattern(pattern) { + byChannel[channel] = append(byChannel[channel], pattern) + } + } + + p.mu.Lock() + if p.closed { + p.mu.Unlock() + close(ch) + return ch, func() {} + } + for channel, pats := range byChannel { + if p.subs[channel] == nil { + p.subs[channel] = make(map[*pgSubscription][]string) + } + p.subs[channel][sub] = pats + p.desired[channel]++ + } + p.mu.Unlock() + + unsubscribe := func() { + sub.once.Do(func() { + p.mu.Lock() + for channel := range byChannel { + if m := p.subs[channel]; m != nil { + delete(m, sub) + if len(m) == 0 { + delete(p.subs, channel) + } + } + if p.desired[channel] > 0 { + p.desired[channel]-- + if p.desired[channel] == 0 { + delete(p.desired, channel) + } + } + } + p.mu.Unlock() + close(ch) + }) + } + + return ch, unsubscribe +} + +// Close stops the background goroutines, closes the pool, and closes all +// subscriber channels. +func (p *PostgresEventPublisher) Close() { + p.mu.Lock() + if p.closed { + p.mu.Unlock() + return + } + p.closed = true + p.mu.Unlock() + + p.cancel() + p.wg.Wait() + p.pool.Close() + + p.mu.Lock() + defer p.mu.Unlock() + seen := make(map[*pgSubscription]bool) + for _, m := range p.subs { + for sub := range m { + if !seen[sub] { + sub.once.Do(func() { close(sub.ch) }) + seen[sub] = true + } + } + } + p.subs = make(map[string]map[*pgSubscription][]string) + p.desired = make(map[string]int) +} + +// runListener maintains a dedicated connection that LISTENs on the desired +// channels and dispatches received notifications. It reconnects with backoff and +// re-LISTENs (resubscribes) after any connection loss. +func (p *PostgresEventPublisher) runListener() { + defer p.wg.Done() + + const ( + minBackoff = 250 * time.Millisecond + maxBackoff = 10 * time.Second + ) + backoff := minBackoff + + for { + if p.ctx.Err() != nil { + return + } + + conn, err := p.connectListener(p.ctx) + if err != nil { + if p.ctx.Err() != nil { + return + } + p.log.Warn("Event listener connect failed, retrying", "error", err, "backoff", backoff) + if !p.sleep(backoff) { + return + } + backoff = nextBackoff(backoff, maxBackoff) + continue + } + + p.log.Info("Event listener connected") + backoff = minBackoff + + // active tracks channels currently LISTENed on this connection. A fresh + // connection starts empty, so listenLoop re-LISTENs every desired + // channel (resubscribe). + active := make(map[string]bool) + loopErr := p.listenLoop(conn, active) + conn.Close(context.Background()) + + if p.ctx.Err() != nil { + return + } + + // Unexpected connection loss: count a reconnect and retry. + p.metrics.IncListenerReconnects(p.ctx, 1) + p.log.Warn("Event listener connection lost, reconnecting", "error", loopErr, "backoff", backoff) + if !p.sleep(backoff) { + return + } + backoff = nextBackoff(backoff, maxBackoff) + } +} + +// connectListener opens the dedicated listener connection with TCP keepalives and +// a connect timeout applied, so the long-lived (mostly idle) LISTEN connection +// detects a silently dropped peer instead of blocking forever in +// WaitForNotification on a dead socket. +func (p *PostgresEventPublisher) connectListener(ctx context.Context) (*pgx.Conn, error) { + cc, err := pgx.ParseConfig(p.dsn) + if err != nil { + return nil, fmt.Errorf("parsing listener dsn: %w", err) + } + applyConnKeepalives(cc) + return pgx.ConnectConfig(ctx, cc) +} + +// listenLoop applies pending subscription changes and waits for notifications on +// conn until the context is canceled or the connection fails. A returned error +// other than context cancellation signals the caller to reconnect. +func (p *PostgresEventPublisher) listenLoop(conn *pgx.Conn, active map[string]bool) error { + for { + if p.ctx.Err() != nil { + return p.ctx.Err() + } + + desired := p.snapshotDesired() + for channel := range desired { + if !active[channel] { + if err := execListen(p.ctx, conn, "LISTEN", channel); err != nil { + return fmt.Errorf("LISTEN %s: %w", channel, err) + } + active[channel] = true + } + } + for channel := range active { + if !desired[channel] { + if err := execListen(p.ctx, conn, "UNLISTEN", channel); err != nil { + return fmt.Errorf("UNLISTEN %s: %w", channel, err) + } + delete(active, channel) + } + } + + waitCtx, cancel := context.WithTimeout(p.ctx, listenPollInterval) + notif, err := conn.WaitForNotification(waitCtx) + cancel() + if err != nil { + // A poll-interval deadline is expected; loop to reapply subscriptions. + if errors.Is(err, context.DeadlineExceeded) { + continue + } + // Context canceled (shutdown) or a real connection error. + return err + } + + p.handleNotification(notif.Channel, notif.Payload) + } +} + +// handleNotification decodes a NOTIFY payload (refetching oversized payloads), +// records latency, and fans the event out to subscribers of its channel. +func (p *PostgresEventPublisher) handleNotification(channel, payload string) { + var env pgEnvelope + if err := json.Unmarshal([]byte(payload), &env); err != nil { + p.log.Error("Failed to decode NOTIFY payload", "channel", channel, "error", err) + p.metrics.IncDropped(p.ctx, 1, attribute.String("reason", "decode")) + return + } + + data := []byte(env.Data) + if env.Ref != "" { + fetched, err := p.refetchPayload(env.Ref) + if err != nil { + p.log.Error("Failed to refetch oversized payload", "ref", env.Ref, "subject", env.Subject, "error", err) + p.metrics.IncDropped(p.ctx, 1, attribute.String("reason", "refetch")) + return + } + data = fetched + } + + if env.TS != 0 && p.metrics.Enabled() { + ms := float64(time.Now().UnixNano()-env.TS) / float64(time.Millisecond) + p.metrics.RecordPublishToDeliverLatency(p.ctx, ms, attribute.String("scope", channelScope(env.Subject))) + } + + p.fanout(channel, Event{Subject: env.Subject, Data: data}) +} + +// refetchPayload loads an oversized payload by reference id. Rows are not deleted +// here so every replica can refetch the same event; a TTL sweep reclaims them. +func (p *PostgresEventPublisher) refetchPayload(ref string) ([]byte, error) { + var data []byte + err := p.pool.QueryRow(p.ctx, `SELECT data FROM scion_event_payloads WHERE id = $1`, ref).Scan(&data) + if err != nil { + return nil, err + } + return data, nil +} + +// fanout delivers evt to every subscriber of channel whose patterns (scoped to +// that channel) match the event subject. Sends are non-blocking; a full +// subscriber buffer drops the event (backpressure). +func (p *PostgresEventPublisher) fanout(channel string, evt Event) { + p.mu.RLock() + defer p.mu.RUnlock() + + for sub, patterns := range p.subs[channel] { + if !anyPatternMatches(patterns, evt.Subject) { + continue + } + select { + case sub.ch <- evt: + p.metrics.IncDelivered(p.ctx, 1, attribute.String("scope", channelScope(evt.Subject))) + default: + p.metrics.IncDropped(p.ctx, 1, attribute.String("reason", "full_buffer")) + } + } +} + +// snapshotDesired returns a copy of the set of channels that should be LISTENed. +func (p *PostgresEventPublisher) snapshotDesired() map[string]bool { + p.mu.RLock() + defer p.mu.RUnlock() + out := make(map[string]bool, len(p.desired)) + for channel := range p.desired { + out[channel] = true + } + return out +} + +// runMaintenance periodically purges expired oversized payloads and reports +// connection-pool gauges. +func (p *PostgresEventPublisher) runMaintenance() { + defer p.wg.Done() + ticker := time.NewTicker(payloadTTL / 2) + defer ticker.Stop() + + for { + select { + case <-p.ctx.Done(): + return + case <-ticker.C: + if _, err := p.pool.Exec(p.ctx, + `DELETE FROM scion_event_payloads WHERE created_at < now() - $1::interval`, + fmt.Sprintf("%d seconds", int(payloadTTL.Seconds())), + ); err != nil && p.ctx.Err() == nil { + p.log.Warn("Failed to purge expired event payloads", "error", err) + } + p.observePoolStats() + } + } +} + +// observePoolStats records a snapshot of the pgx pool gauges. +func (p *PostgresEventPublisher) observePoolStats() { + if !p.metrics.Enabled() { + return + } + s := p.pool.Stat() + p.metrics.ObservePoolStats(p.ctx, dbmetrics.PoolStats{ + Active: int64(s.AcquiredConns()), + Idle: int64(s.IdleConns()), + Waiting: int64(s.EmptyAcquireCount()), + Max: int64(s.MaxConns()), + }) +} + +// sleep waits for d or until the publisher context is canceled. It reports false +// if the context was canceled. +func (p *PostgresEventPublisher) sleep(d time.Duration) bool { + t := time.NewTimer(d) + defer t.Stop() + select { + case <-p.ctx.Done(): + return false + case <-t.C: + return true + } +} + +// eventConnectTimeout bounds a single connection attempt for the event pool and +// listener, so a network black-hole surfaces as a retryable error instead of a +// hang. +const eventConnectTimeout = 10 * time.Second + +// applyEventPoolKeepalives attaches TCP keepalive GUCs and a connect timeout to +// the event pool's per-connection config, and bounds idle/total connection age. +// CloudSQL (and NAT gateways) silently drop idle connections; keepalives let the +// kernel detect a dead peer and the idle/lifetime caps recycle connections before +// the remote does, so the listener and publishers don't stall on a dead socket. +func applyEventPoolKeepalives(cfg *pgxpool.Config) { + applyConnKeepalives(cfg.ConnConfig) + // Recycle idle event-pool connections well before CloudSQL's ~10m idle + // timeout, and bound total connection age. + if cfg.MaxConnIdleTime == 0 { + cfg.MaxConnIdleTime = 5 * time.Minute + } + if cfg.MaxConnLifetime == 0 { + cfg.MaxConnLifetime = 30 * time.Minute + } +} + +// applyConnKeepalives sets the connect timeout and server-side TCP keepalive GUCs +// on a single pgx connection config. Existing RuntimeParams are not overwritten so +// an explicit DSN setting wins. Values: probe after 60s idle, every 15s, give up +// after 4 missed probes (~2 min to detect a dead peer). +func applyConnKeepalives(cc *pgx.ConnConfig) { + if cc == nil { + return + } + if cc.ConnectTimeout == 0 { + cc.ConnectTimeout = eventConnectTimeout + } + if cc.RuntimeParams == nil { + cc.RuntimeParams = make(map[string]string) + } + defaults := map[string]string{ + "tcp_keepalives_idle": "60", + "tcp_keepalives_interval": "15", + "tcp_keepalives_count": "4", + } + for k, v := range defaults { + if _, ok := cc.RuntimeParams[k]; !ok { + cc.RuntimeParams[k] = v + } + } +} + +// --- helpers (pure functions; no receiver state) --- + +// execListen runs a LISTEN or UNLISTEN for channel, quoting the identifier so +// case and special characters (e.g. UUID hyphens) match the pg_notify channel. +func execListen(ctx context.Context, conn *pgx.Conn, verb, channel string) error { + cctx, cancel := context.WithTimeout(ctx, 5*time.Second) + defer cancel() + quoted := `"` + strings.ReplaceAll(channel, `"`, `""`) + `"` + _, err := conn.Exec(cctx, verb+" "+quoted) + return err +} + +// channelsForSubject returns the Postgres channels a subject is published to. +func channelsForSubject(subject string) []string { + if gc := groveChannelForSubject(subject); gc != "" { + return []string{gc, pgGlobalChannel} + } + return []string{pgGlobalChannel} +} + +// channelsForPattern returns the Postgres channels a subscription pattern needs. +// A concrete grove/project pattern resolves to that grove's channel; everything +// else (wildcard grove, or non-grove subjects) resolves to the global channel. +func channelsForPattern(pattern string) []string { + parts := strings.SplitN(pattern, ".", 3) + if len(parts) >= 2 && (parts[0] == "project" || parts[0] == "grove") && isConcreteToken(parts[1]) { + return []string{groveChannel(parts[1])} + } + return []string{pgGlobalChannel} +} + +// groveChannelForSubject returns the per-grove channel for a grove-scoped +// subject, or "" if the subject is not grove-scoped. +func groveChannelForSubject(subject string) string { + parts := strings.SplitN(subject, ".", 3) + if len(parts) >= 2 && (parts[0] == "project" || parts[0] == "grove") { + return groveChannel(parts[1]) + } + return "" +} + +// groveChannel builds the Postgres channel name for a grove id, hashing the id +// if the resulting identifier would exceed the Postgres length limit. +func groveChannel(id string) string { + name := pgChannelPrefix + "g_" + id + if len(name) <= maxPGIdentifier { + return name + } + sum := sha256.Sum256([]byte(id)) + return pgChannelPrefix + "g_" + hex.EncodeToString(sum[:])[:32] +} + +// channelScope returns a low-cardinality label ("grove" or "global") for the +// channel a subject maps to, suitable for use as a metric attribute. +func channelScope(subject string) string { + if groveChannelForSubject(subject) != "" { + return "grove" + } + return "global" +} + +func isConcreteToken(t string) bool { return t != "" && t != "*" && t != ">" } + +// anyPatternMatches reports whether any pattern matches the subject. +func anyPatternMatches(patterns []string, subject string) bool { + for _, pattern := range patterns { + if subjectMatchesPattern(pattern, subject) { + return true + } + } + return false +} + +// eventTypeName returns the bare Go type name of an event value (e.g. +// "AgentStatusEvent"), used as the envelope type tag. +func eventTypeName(event interface{}) string { + t := fmt.Sprintf("%T", event) + if i := strings.LastIndex(t, "."); i >= 0 { + t = t[i+1:] + } + return t +} + +// nextBackoff doubles d up to max. +func nextBackoff(d, max time.Duration) time.Duration { + d *= 2 + if d > max { + return max + } + return d +} diff --git a/pkg/hub/events_postgres_test.go b/pkg/hub/events_postgres_test.go new file mode 100644 index 000000000..e5aa48f1e --- /dev/null +++ b/pkg/hub/events_postgres_test.go @@ -0,0 +1,739 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//go:build !no_sqlite + +package hub + +import ( + "context" + "encoding/json" + "log/slog" + "net/http" + "os" + "strings" + "sync" + "testing" + "time" + + "github.com/jackc/pgx/v5" + "github.com/jackc/pgx/v5/pgconn" + "go.opentelemetry.io/otel/attribute" + + "github.com/GoogleCloudPlatform/scion/pkg/observability/dbmetrics" + "github.com/GoogleCloudPlatform/scion/pkg/store" +) + +func mkProject(id string) *store.Project { + return &store.Project{ID: id, Name: id, Slug: id, Created: time.Now()} +} + +func mkMessage(agentID, msg string) *store.Message { + return &store.Message{ + ID: "msg-" + agentID, + AgentID: agentID, + Sender: "agent:" + agentID, + Recipient: "agent:" + agentID, + Msg: msg, + Type: "instruction", + CreatedAt: time.Now(), + } +} + +// --- test doubles --- + +// recExec records Exec calls so publish-path tests can assert the SQL and +// arguments without a real database. +type recExec struct { + mu sync.Mutex + calls []recCall +} + +type recCall struct { + sql string + args []any +} + +func (e *recExec) Exec(_ context.Context, sql string, args ...any) (pgconn.CommandTag, error) { + e.mu.Lock() + defer e.mu.Unlock() + e.calls = append(e.calls, recCall{sql: sql, args: args}) + return pgconn.CommandTag{}, nil +} + +func (e *recExec) notifyCalls() []recCall { + e.mu.Lock() + defer e.mu.Unlock() + var out []recCall + for _, c := range e.calls { + if strings.Contains(c.sql, "pg_notify") { + out = append(out, c) + } + } + return out +} + +func (e *recExec) inserts() []recCall { + e.mu.Lock() + defer e.mu.Unlock() + var out []recCall + for _, c := range e.calls { + if strings.Contains(c.sql, "INSERT INTO scion_event_payloads") { + out = append(out, c) + } + } + return out +} + +// countingRecorder is a dbmetrics.Recorder that tallies calls for assertions. +type countingRecorder struct { + mu sync.Mutex + published int64 + delivered int64 + dropped int64 + reconnects int64 + payloadSizes []int64 + latencies []float64 + poolObserved int + enabledReturns bool +} + +func (r *countingRecorder) RecordPublishToDeliverLatency(_ context.Context, ms float64, _ ...attribute.KeyValue) { + r.mu.Lock() + defer r.mu.Unlock() + r.latencies = append(r.latencies, ms) +} +func (r *countingRecorder) IncPublished(_ context.Context, n int64, _ ...attribute.KeyValue) { + r.mu.Lock() + defer r.mu.Unlock() + r.published += n +} +func (r *countingRecorder) IncDelivered(_ context.Context, n int64, _ ...attribute.KeyValue) { + r.mu.Lock() + defer r.mu.Unlock() + r.delivered += n +} +func (r *countingRecorder) IncDropped(_ context.Context, n int64, _ ...attribute.KeyValue) { + r.mu.Lock() + defer r.mu.Unlock() + r.dropped += n +} +func (r *countingRecorder) ObserveSubscriberLag(_ context.Context, _ int64, _ ...attribute.KeyValue) { +} +func (r *countingRecorder) IncListenerReconnects(_ context.Context, n int64, _ ...attribute.KeyValue) { + r.mu.Lock() + defer r.mu.Unlock() + r.reconnects += n +} +func (r *countingRecorder) RecordPayloadSize(_ context.Context, bytes int64, _ ...attribute.KeyValue) { + r.mu.Lock() + defer r.mu.Unlock() + r.payloadSizes = append(r.payloadSizes, bytes) +} +func (r *countingRecorder) ObservePoolStats(_ context.Context, _ dbmetrics.PoolStats, _ ...attribute.KeyValue) { + r.mu.Lock() + defer r.mu.Unlock() + r.poolObserved++ +} +func (r *countingRecorder) Enabled() bool { return r.enabledReturns } + +func (r *countingRecorder) snapshot() (pub, del, drop, recon int64) { + r.mu.Lock() + defer r.mu.Unlock() + return r.published, r.delivered, r.dropped, r.reconnects +} + +// newTestPostgresPublisher builds a publisher with no live connection or +// goroutines, suitable for exercising the pure routing/registry/publish logic. +func newTestPostgresPublisher(rec dbmetrics.Recorder) *PostgresEventPublisher { + if rec == nil { + rec = dbmetrics.NewDisabled() + } + p := &PostgresEventPublisher{ + metrics: rec, + log: slog.New(slog.NewTextHandler(os.Stderr, &slog.HandlerOptions{Level: slog.LevelError})), + ctx: context.Background(), + subs: make(map[string]map[*pgSubscription][]string), + desired: make(map[string]int), + } + p.sink = p.publish + return p +} + +// --- pure helper tests (no database required) --- + +func TestChannelsForSubject(t *testing.T) { + tests := []struct { + subject string + want []string + }{ + {"project.G1.agent.status", []string{groveChannel("G1"), pgGlobalChannel}}, + {"grove.G2.notification", []string{groveChannel("G2"), pgGlobalChannel}}, + {"agent.A1.status", []string{pgGlobalChannel}}, + {"user.U1.message", []string{pgGlobalChannel}}, + {"broker.B1.status", []string{pgGlobalChannel}}, + {"admin.allowlist.changed", []string{pgGlobalChannel}}, + {"notification.created", []string{pgGlobalChannel}}, + } + for _, tt := range tests { + got := channelsForSubject(tt.subject) + if strings.Join(got, ",") != strings.Join(tt.want, ",") { + t.Errorf("channelsForSubject(%q) = %v, want %v", tt.subject, got, tt.want) + } + } +} + +func TestChannelsForPattern(t *testing.T) { + tests := []struct { + pattern string + want []string + }{ + {"project.G1.>", []string{groveChannel("G1")}}, + {"grove.G2.agent.status", []string{groveChannel("G2")}}, + {"project.>.agent.status", []string{pgGlobalChannel}}, // spanning wildcard + {"project.*.agent.status", []string{pgGlobalChannel}}, // single-token wildcard grove + {"agent.A1.message", []string{pgGlobalChannel}}, + {"notification.created", []string{pgGlobalChannel}}, + } + for _, tt := range tests { + got := channelsForPattern(tt.pattern) + if strings.Join(got, ",") != strings.Join(tt.want, ",") { + t.Errorf("channelsForPattern(%q) = %v, want %v", tt.pattern, got, tt.want) + } + } +} + +func TestGroveChannel_BoundedLength(t *testing.T) { + long := strings.Repeat("x", 200) + got := groveChannel(long) + if len(got) > maxPGIdentifier { + t.Errorf("groveChannel(long) length = %d, want <= %d", len(got), maxPGIdentifier) + } + // Deterministic. + if got != groveChannel(long) { + t.Errorf("groveChannel not deterministic") + } + // A normal UUID-length id is passed through unhashed. + uuidLike := "11111111-2222-3333-4444-555555555555" + if groveChannel(uuidLike) != pgChannelPrefix+"g_"+uuidLike { + t.Errorf("groveChannel(uuid) = %q, want passthrough", groveChannel(uuidLike)) + } +} + +func TestEventTypeName(t *testing.T) { + if got := eventTypeName(AgentStatusEvent{}); got != "AgentStatusEvent" { + t.Errorf("eventTypeName = %q, want AgentStatusEvent", got) + } +} + +// --- registry / fan-out tests (no database required) --- + +// TestPostgresFanout_ScopedSubscriberNoDoubleDelivery verifies a grove-scoped +// subscriber receives grove events on the grove channel exactly once and is not +// also matched on the global channel (which carries a mirror of grove events). +func TestPostgresFanout_ScopedSubscriberNoDoubleDelivery(t *testing.T) { + p := newTestPostgresPublisher(nil) + ch, unsub := p.Subscribe("project.G1.>") + defer unsub() + + evt := Event{Subject: "project.G1.agent.status", Data: []byte(`{}`)} + + // Delivered on the grove channel. + p.fanout(groveChannel("G1"), evt) + select { + case got := <-ch: + if got.Subject != evt.Subject { + t.Fatalf("got subject %q", got.Subject) + } + case <-time.After(time.Second): + t.Fatal("expected delivery on grove channel") + } + + // NOT delivered again on the global channel: the subscriber's patterns do + // not resolve to the global channel. + p.fanout(pgGlobalChannel, evt) + select { + case got := <-ch: + t.Fatalf("unexpected duplicate delivery on global channel: %q", got.Subject) + case <-time.After(100 * time.Millisecond): + } +} + +// TestPostgresFanout_SpanningSubscriber verifies a grove-spanning subscriber +// (e.g. the notification dispatcher) receives grove events via the global +// channel and not via the per-grove channel. +func TestPostgresFanout_SpanningSubscriber(t *testing.T) { + p := newTestPostgresPublisher(nil) + ch, unsub := p.Subscribe("project.>.agent.status") + defer unsub() + + evt := Event{Subject: "project.G9.agent.status", Data: []byte(`{}`)} + + p.fanout(pgGlobalChannel, evt) + select { + case got := <-ch: + if got.Subject != evt.Subject { + t.Fatalf("got subject %q", got.Subject) + } + case <-time.After(time.Second): + t.Fatal("expected delivery on global channel for spanning subscriber") + } + + // The per-grove channel must not deliver to a spanning subscriber. + p.fanout(groveChannel("G9"), evt) + select { + case got := <-ch: + t.Fatalf("unexpected delivery on grove channel: %q", got.Subject) + case <-time.After(100 * time.Millisecond): + } +} + +// TestPostgresFanout_MixedPatternsNoDuplicate verifies that a single Subscribe +// call mixing a grove-scoped and a non-grove pattern never double-delivers an +// event that happens to be mirrored onto both channels. +func TestPostgresFanout_MixedPatternsNoDuplicate(t *testing.T) { + p := newTestPostgresPublisher(nil) + ch, unsub := p.Subscribe("project.G1.agent.status", "agent.A1.message") + defer unsub() + + evt := Event{Subject: "project.G1.agent.status", Data: []byte(`{}`)} + + // On the global channel, only the agent.A1.message pattern is active, which + // does not match the project subject -> no delivery here. + p.fanout(pgGlobalChannel, evt) + // On the grove channel, the project pattern matches -> exactly one delivery. + p.fanout(groveChannel("G1"), evt) + + received := 0 + for { + select { + case <-ch: + received++ + case <-time.After(150 * time.Millisecond): + if received != 1 { + t.Fatalf("expected exactly 1 delivery, got %d", received) + } + return + } + } +} + +func TestPostgresSubscribe_Unsubscribe(t *testing.T) { + p := newTestPostgresPublisher(nil) + ch, unsub := p.Subscribe("project.G1.>") + + gc := groveChannel("G1") + if p.desired[gc] != 1 { + t.Fatalf("desired[%s] = %d, want 1", gc, p.desired[gc]) + } + unsub() + if _, ok := p.desired[gc]; ok { + t.Fatalf("desired[%s] should be cleared after unsubscribe", gc) + } + // Channel must be closed. + if _, ok := <-ch; ok { + t.Fatal("subscriber channel should be closed after unsubscribe") + } + // Double unsubscribe is safe. + unsub() +} + +// --- publish-path tests using a fake executor (no database required) --- + +func TestBuildAndNotify_SmallPayload(t *testing.T) { + rec := &countingRecorder{} + p := newTestPostgresPublisher(rec) + exec := &recExec{} + + err := p.buildAndNotify(context.Background(), exec, "project.G1.agent.status", AgentStatusEvent{AgentID: "a1"}) + if err != nil { + t.Fatalf("buildAndNotify: %v", err) + } + + // Grove subject -> NOTIFY on grove channel and global channel. + notifies := exec.notifyCalls() + if len(notifies) != 2 { + t.Fatalf("expected 2 pg_notify calls, got %d", len(notifies)) + } + gotChannels := map[string]bool{} + for _, c := range notifies { + gotChannels[c.args[0].(string)] = true + // Payload should carry the inline data and the event type. + var env pgEnvelope + if err := json.Unmarshal([]byte(c.args[1].(string)), &env); err != nil { + t.Fatalf("decode payload: %v", err) + } + if env.Ref != "" { + t.Fatalf("small payload should not be offloaded; got ref %q", env.Ref) + } + if env.Type != "AgentStatusEvent" { + t.Fatalf("envelope type = %q", env.Type) + } + if len(env.Data) == 0 { + t.Fatal("envelope data should be inline") + } + } + if !gotChannels[groveChannel("G1")] || !gotChannels[pgGlobalChannel] { + t.Fatalf("notify channels = %v", gotChannels) + } + if len(exec.inserts()) != 0 { + t.Fatalf("small payload must not INSERT into payload table") + } + if pub, _, _, _ := rec.snapshot(); pub != 1 { + t.Fatalf("published metric = %d, want 1", pub) + } +} + +func TestBuildAndNotify_OversizedPayloadOffloaded(t *testing.T) { + p := newTestPostgresPublisher(nil) + exec := &recExec{} + + // A message larger than the NOTIFY threshold forces reference-and-refetch. + big := strings.Repeat("z", pgNotifyMaxPayload+500) + err := p.buildAndNotify(context.Background(), exec, "agent.A1.message", UserMessageEvent{ID: "m1", Msg: big}) + if err != nil { + t.Fatalf("buildAndNotify: %v", err) + } + + if got := len(exec.inserts()); got != 1 { + t.Fatalf("oversized payload should INSERT once, got %d", got) + } + notifies := exec.notifyCalls() + if len(notifies) != 1 { // non-grove subject -> global only + t.Fatalf("expected 1 pg_notify, got %d", len(notifies)) + } + var env pgEnvelope + if err := json.Unmarshal([]byte(notifies[0].args[1].(string)), &env); err != nil { + t.Fatalf("decode payload: %v", err) + } + if env.Ref == "" { + t.Fatal("oversized envelope should carry a ref") + } + if len(env.Data) != 0 { + t.Fatal("oversized envelope must not inline data") + } + if len(notifies[0].args[1].(string)) > pgNotifyMaxPayload { + t.Fatalf("reference envelope still exceeds NOTIFY limit: %d bytes", len(notifies[0].args[1].(string))) + } +} + +// TestPublishTx_UsesProvidedExecutor verifies the transactional publish path +// enrolls the NOTIFY on the caller's transaction (the fake executor) rather than +// the pool, which is what gives rollback==no-deliver semantics at the DB layer. +func TestPublishTx_UsesProvidedExecutor(t *testing.T) { + p := newTestPostgresPublisher(nil) + tx := &recExec{} + + if err := p.PublishTx(context.Background(), tx, "grove.G1.created", ProjectCreatedEvent{ProjectID: "G1"}); err != nil { + t.Fatalf("PublishTx: %v", err) + } + if len(tx.notifyCalls()) == 0 { + t.Fatal("PublishTx should issue pg_notify on the provided transaction") + } +} + +func TestHandleNotification_InlineDeliversAndRecordsMetrics(t *testing.T) { + rec := &countingRecorder{enabledReturns: true} + p := newTestPostgresPublisher(rec) + ch, unsub := p.Subscribe("agent.A1.status") + defer unsub() + + data, _ := json.Marshal(AgentStatusEvent{AgentID: "A1", Phase: "running"}) + env := pgEnvelope{Type: "AgentStatusEvent", Subject: "agent.A1.status", Data: data, TS: time.Now().Add(-5 * time.Millisecond).UnixNano()} + payload, _ := json.Marshal(env) + + p.handleNotification(pgGlobalChannel, string(payload)) + + select { + case got := <-ch: + if got.Subject != "agent.A1.status" { + t.Fatalf("subject = %q", got.Subject) + } + case <-time.After(time.Second): + t.Fatal("expected delivery") + } + + if _, del, _, _ := rec.snapshot(); del != 1 { + t.Fatalf("delivered metric = %d, want 1", del) + } + rec.mu.Lock() + gotLatency := len(rec.latencies) + rec.mu.Unlock() + if gotLatency != 1 { + t.Fatalf("expected 1 latency sample, got %d", gotLatency) + } +} + +func TestHandleNotification_FullBufferDropsAndCounts(t *testing.T) { + rec := &countingRecorder{} + p := newTestPostgresPublisher(rec) + // Register a subscriber with a tiny buffer by reaching into the registry. + sub := &pgSubscription{ch: make(chan Event, 1)} + p.subs[pgGlobalChannel] = map[*pgSubscription][]string{sub: {"agent.>"}} + + evt := Event{Subject: "agent.A1.status", Data: []byte(`{}`)} + p.fanout(pgGlobalChannel, evt) // fills the buffer (delivered) + p.fanout(pgGlobalChannel, evt) // dropped (buffer full) + + _, del, drop, _ := rec.snapshot() + if del != 1 || drop != 1 { + t.Fatalf("delivered=%d dropped=%d, want 1 and 1", del, drop) + } +} + +// --- integration tests (require a live Postgres via SCION_TEST_POSTGRES_DSN) --- + +func requirePostgres(t *testing.T) string { + t.Helper() + dsn := os.Getenv("SCION_TEST_POSTGRES_DSN") + if dsn == "" { + t.Skip("set SCION_TEST_POSTGRES_DSN to run Postgres LISTEN/NOTIFY integration tests") + } + return dsn +} + +// TestPostgresIntegration_CrossReplicaDelivery starts two independent publishers +// against the same database (simulating two hub replicas) and asserts an event +// published on one is delivered to a subscriber on the other. +func TestPostgresIntegration_CrossReplicaDelivery(t *testing.T) { + dsn := requirePostgres(t) + ctx := context.Background() + + a, err := NewPostgresEventPublisher(ctx, dsn, dbmetrics.NewDisabled(), nil) + if err != nil { + t.Fatalf("publisher A: %v", err) + } + defer a.Close() + b, err := NewPostgresEventPublisher(ctx, dsn, dbmetrics.NewDisabled(), nil) + if err != nil { + t.Fatalf("publisher B: %v", err) + } + defer b.Close() + + pid := "proj-" + strings.ReplaceAll(time.Now().Format("150405.000000"), ".", "") + ch, unsub := b.Subscribe("project." + pid + ".>") + defer unsub() + + // Give B's listener time to LISTEN on the grove channel. + time.Sleep(2 * listenPollInterval) + + a.PublishProjectCreated(ctx, mkProject(pid)) + + select { + case got := <-ch: + if !strings.HasPrefix(got.Subject, "project."+pid) { + t.Fatalf("unexpected subject %q", got.Subject) + } + case <-time.After(5 * time.Second): + t.Fatal("cross-replica event not delivered") + } +} + +// TestPostgresIntegration_OversizedRoundTrip verifies an event larger than the +// NOTIFY limit is delivered intact via reference-and-refetch. +func TestPostgresIntegration_OversizedRoundTrip(t *testing.T) { + dsn := requirePostgres(t) + ctx := context.Background() + + pub, err := NewPostgresEventPublisher(ctx, dsn, dbmetrics.NewDisabled(), nil) + if err != nil { + t.Fatalf("publisher: %v", err) + } + defer pub.Close() + + aid := "agent-" + strings.ReplaceAll(time.Now().Format("150405.000000"), ".", "") + ch, unsub := pub.Subscribe("agent." + aid + ".message") + defer unsub() + time.Sleep(2 * listenPollInterval) + + big := strings.Repeat("Q", pgNotifyMaxPayload+2048) + pub.PublishUserMessage(ctx, mkMessage(aid, big)) + + select { + case got := <-ch: + var evt UserMessageEvent + if err := json.Unmarshal(got.Data, &evt); err != nil { + t.Fatalf("decode delivered event: %v", err) + } + if evt.Msg != big { + t.Fatalf("oversized payload not delivered intact: got %d bytes, want %d", len(evt.Msg), len(big)) + } + case <-time.After(5 * time.Second): + t.Fatal("oversized event not delivered") + } +} + +// TestPostgresIntegration_TransactionalRollback verifies a NOTIFY enrolled in a +// rolled-back transaction is never delivered, while a committed one is. +func TestPostgresIntegration_TransactionalRollback(t *testing.T) { + dsn := requirePostgres(t) + ctx := context.Background() + + pub, err := NewPostgresEventPublisher(ctx, dsn, dbmetrics.NewDisabled(), nil) + if err != nil { + t.Fatalf("publisher: %v", err) + } + defer pub.Close() + + pid := "txn-" + strings.ReplaceAll(time.Now().Format("150405.000000"), ".", "") + ch, unsub := pub.Subscribe("project." + pid + ".>") + defer unsub() + time.Sleep(2 * listenPollInterval) + + // Rolled-back publish: must NOT be delivered. + txRollback, err := pub.pool.Begin(ctx) + if err != nil { + t.Fatalf("begin: %v", err) + } + if err := pub.PublishTx(ctx, txRollback, "project."+pid+".updated", ProjectUpdatedEvent{ProjectID: pid, Name: "rolled-back"}); err != nil { + t.Fatalf("PublishTx: %v", err) + } + if err := txRollback.Rollback(ctx); err != nil { + t.Fatalf("rollback: %v", err) + } + + select { + case got := <-ch: + t.Fatalf("rolled-back event was delivered: %q", got.Subject) + case <-time.After(2 * time.Second): + // expected: nothing delivered + } + + // Committed publish: must be delivered. + txCommit, err := pub.pool.Begin(ctx) + if err != nil { + t.Fatalf("begin: %v", err) + } + if err := pub.PublishTx(ctx, txCommit, "project."+pid+".updated", ProjectUpdatedEvent{ProjectID: pid, Name: "committed"}); err != nil { + t.Fatalf("PublishTx: %v", err) + } + if err := txCommit.Commit(ctx); err != nil { + t.Fatalf("commit: %v", err) + } + + select { + case got := <-ch: + if got.Subject != "project."+pid+".updated" { + t.Fatalf("subject = %q", got.Subject) + } + case <-time.After(5 * time.Second): + t.Fatal("committed event not delivered") + } +} + +// TestPostgresIntegration_HandlerCreateProjectEmitsNotify exercises the full +// production publish path end-to-end: an HTTP project-create request handled by +// the Hub server calls s.events.PublishProjectCreated on a real +// PostgresEventPublisher, which must emit a pg_notify observable by an +// independent raw LISTEN connection. This is the exact capability the +// multi-replica live test probed with psql (create project => NOTIFY on +// scion_ev_global); it guards against regressions in the cmd-level wiring that +// connects the handler's s.events to the Postgres backend. +func TestPostgresIntegration_HandlerCreateProjectEmitsNotify(t *testing.T) { + dsn := requirePostgres(t) + ctx := context.Background() + + srv, _ := testServer(t) + pub, err := NewPostgresEventPublisher(ctx, dsn, dbmetrics.NewDisabled(), nil) + if err != nil { + t.Fatalf("publisher: %v", err) + } + defer pub.Close() + srv.SetEventPublisher(pub) + + // Independent raw LISTEN on the global channel — bypasses the publisher's own + // listener/subscription machinery, mirroring the psql probe from the live test. + lconn, err := pgx.Connect(ctx, dsn) + if err != nil { + t.Fatalf("listen conn: %v", err) + } + defer lconn.Close(context.Background()) + if _, err := lconn.Exec(ctx, `LISTEN scion_ev_global`); err != nil { + t.Fatalf("LISTEN: %v", err) + } + + rec := doRequest(t, srv, http.MethodPost, "/api/v1/projects", map[string]interface{}{ + "name": "pg-notify-wiring-" + strings.ReplaceAll(time.Now().Format("150405.000000"), ".", ""), + }) + if rec.Code != http.StatusCreated { + t.Fatalf("create project: code=%d body=%s", rec.Code, rec.Body.String()) + } + + // Expect a project..created NOTIFY on the global channel. + deadline := time.Now().Add(5 * time.Second) + for { + wctx, cancel := context.WithTimeout(ctx, time.Until(deadline)) + n, werr := lconn.WaitForNotification(wctx) + cancel() + if werr != nil { + t.Fatalf("no NOTIFY observed for handler-driven project create (publish path not wired): %v", werr) + } + if strings.Contains(n.Payload, ".created") { + return // success: handler -> s.events -> pg_notify works + } + } +} + +// TestPostgresIntegration_ReconnectResubscribe terminates the listener's backend +// connection and verifies the publisher reconnects, re-LISTENs, and resumes +// delivery, incrementing the reconnect metric. +func TestPostgresIntegration_ReconnectResubscribe(t *testing.T) { + dsn := requirePostgres(t) + ctx := context.Background() + + rec := &countingRecorder{enabledReturns: true} + pub, err := NewPostgresEventPublisher(ctx, dsn, rec, nil) + if err != nil { + t.Fatalf("publisher: %v", err) + } + defer pub.Close() + + pid := "rc-" + strings.ReplaceAll(time.Now().Format("150405.000000"), ".", "") + ch, unsub := pub.Subscribe("project." + pid + ".>") + defer unsub() + time.Sleep(2 * listenPollInterval) + + // Forcibly terminate all LISTENing backends for this database, dropping the + // listener connection and forcing a reconnect. + if _, err := pub.pool.Exec(ctx, + `SELECT pg_terminate_backend(pid) FROM pg_stat_activity + WHERE query ILIKE 'LISTEN %' AND pid <> pg_backend_pid()`); err != nil { + t.Fatalf("terminate backends: %v", err) + } + + // Wait for reconnect + resubscribe. + deadline := time.Now().Add(15 * time.Second) + for time.Now().Before(deadline) { + if _, _, _, recon := rec.snapshot(); recon > 0 { + break + } + time.Sleep(200 * time.Millisecond) + } + if _, _, _, recon := rec.snapshot(); recon == 0 { + t.Fatal("expected a listener reconnect to be recorded") + } + + // Allow the resubscribe poll to re-LISTEN, then verify delivery resumes. + time.Sleep(2 * listenPollInterval) + pub.PublishProjectCreated(ctx, mkProject(pid)) + + select { + case got := <-ch: + if !strings.HasPrefix(got.Subject, "project."+pid) { + t.Fatalf("unexpected subject %q", got.Subject) + } + case <-time.After(5 * time.Second): + t.Fatal("delivery did not resume after reconnect") + } +} diff --git a/pkg/hub/gcp_ratelimit_test.go b/pkg/hub/gcp_ratelimit_test.go index 44c69b223..831ed170e 100644 --- a/pkg/hub/gcp_ratelimit_test.go +++ b/pkg/hub/gcp_ratelimit_test.go @@ -25,18 +25,18 @@ func TestGCPTokenRateLimiter_Allow(t *testing.T) { // First 5 requests should be allowed (burst) for i := 0; i < 5; i++ { - if !rl.Allow("agent-1") { + if !rl.Allow(tid("agent-1")) { t.Fatalf("request %d should be allowed", i) } } // 6th request should be denied (burst exhausted) - if rl.Allow("agent-1") { + if rl.Allow(tid("agent-1")) { t.Fatal("6th request should be denied") } // Different agent should still be allowed - if !rl.Allow("agent-2") { + if !rl.Allow(tid("agent-2")) { t.Fatal("different agent should be allowed") } } @@ -44,18 +44,18 @@ func TestGCPTokenRateLimiter_Allow(t *testing.T) { func TestGCPTokenRateLimiter_Refill(t *testing.T) { rl := NewGCPTokenRateLimiter(100, 1) // 100/sec, burst 1 - if !rl.Allow("agent-1") { + if !rl.Allow(tid("agent-1")) { t.Fatal("first request should be allowed") } - if rl.Allow("agent-1") { + if rl.Allow(tid("agent-1")) { t.Fatal("second request should be denied") } // Wait for refill time.Sleep(20 * time.Millisecond) - if !rl.Allow("agent-1") { + if !rl.Allow(tid("agent-1")) { t.Fatal("request after refill should be allowed") } } @@ -68,14 +68,14 @@ func TestGCPTokenRateLimiter_CleanupExitsOnCancel(t *testing.T) { rl.StartCleanup(ctx) // Use the limiter - rl.Allow("agent-1") + rl.Allow(tid("agent-1")) // Cancel and verify goroutine exits (no hang) cancel() time.Sleep(100 * time.Millisecond) // Limiter should still work after cleanup goroutine exits - if !rl.Allow("agent-2") { + if !rl.Allow(tid("agent-2")) { t.Fatal("limiter should still work after cleanup exits") } } diff --git a/pkg/hub/handlers.go b/pkg/hub/handlers.go index 4e1b45067..3398ccca0 100644 --- a/pkg/hub/handlers.go +++ b/pkg/hub/handlers.go @@ -207,16 +207,15 @@ type CreateAgentRequest struct { // GatherEnv enables the env-gather flow where the broker evaluates env // completeness and may return a 202 requiring the CLI to supply missing values. GatherEnv bool `json:"gatherEnv,omitempty"` - // Resume signals that the caller wants to resume an existing stopped agent - // in-place rather than deleting and recreating it. When true and the - // existing agent is in PhaseStopped, the agent record is preserved and - // the broker is asked to restart the container. - Resume bool `json:"resume,omitempty"` // Notify subscribes the creating agent/user to status notifications for the new agent. Notify bool `json:"notify,omitempty"` // CleanupMode controls stale-existing-agent cleanup behavior during create: // "strict" (default) fails create if broker cleanup fails; "force" continues. CleanupMode string `json:"cleanupMode,omitempty"` + // Resume signals that the caller wants to resume an existing stopped agent + // rather than create a brand-new one. When true and a stopped agent with + // the same name exists, the Hub recovers it instead of creating fresh. + Resume bool `json:"resume,omitempty"` // GCPIdentity specifies the GCP identity assignment for the agent. // Controls metadata server behavior and optional service account binding. GCPIdentity *GCPIdentityAssignment `json:"gcp_identity,omitempty"` @@ -571,10 +570,13 @@ func (s *Server) createAgentInProject( switch s.handleExistingAgent(ctx, w, existingAgent, project, runtimeBrokerID, req, notifySubscriberType, notifySubscriberID, createdBy) { case existingAgentStarted, existingAgentErrored: return // Response already written. + case existingAgentConflict: + Conflict(w, fmt.Sprintf("agent %q already exists in this project", slug)) + return case existingAgentDeleted: // Fall through to create a new agent below. case existingAgentNone: - // No existing agent (or unhandled status) — fall through to create. + // No existing agent — fall through to create. } // Apply project-level default template if no template specified in request @@ -783,7 +785,7 @@ func (s *Server) createAgentInProject( } } - // Hub-managed/shared-workspace project remote broker support: if the project has + // Hub-native/shared-workspace project remote broker support: if the project has // a managed workspace and the workspace path is set, upload it to GCS so // a remote broker can download it. if (project.GitRemote == "" || project.IsSharedWorkspace()) && agent.AppliedConfig != nil && agent.AppliedConfig.Workspace != "" { @@ -1801,6 +1803,8 @@ func (s *Server) handleAgentAction(w http.ResponseWriter, r *http.Request, id, a s.handleAgentMessage(w, r, id) case api.AgentActionExec: s.handleAgentExec(w, r, id) + case api.AgentActionResetAuth: + s.handleAgentResetAuth(w, r, id) case api.AgentActionRestore: s.restoreAgent(w, r, id) case api.AgentActionTokenRefresh: @@ -1918,24 +1922,88 @@ func (s *Server) handleAgentTokenRefresh(w http.ResponseWriter, r *http.Request, return } + // Build the generalized tokens[] array. + // App tokens are always present; transport tokens are added when + // the hub has a transport minter configured. + tokens := []RefreshTokenEntry{ + { + Layer: "app", + Type: "scion_access", + Value: newToken, + ExpiresIn: int(time.Until(expiresAt).Seconds()), + }, + } + + // Mint a transport token if transport auth is configured + if s.transportMinter != nil && s.transportAudience != "" { + tToken, tExpiry, tErr := s.transportMinter.MintIDToken(r.Context(), s.transportAudience) + if tErr != nil { + // Log but don't fail the refresh — app token is still valid + slog.Warn("Failed to mint transport token during refresh", + "agent_id", id, "error", tErr) + } else if tToken != "" { + tokens = append(tokens, RefreshTokenEntry{ + Layer: "transport", + Type: "google_oidc", + Value: tToken, + ExpiresIn: int(time.Until(tExpiry).Seconds()), + Audience: s.transportAudience, + }) + } + } + + // Response includes both the legacy single-token fields (backward compat) + // and the generalized tokens[] array. Old clients ignore tokens[]; + // new clients prefer tokens[]. writeJSON(w, http.StatusOK, map[string]interface{}{ "token": newToken, "expires_at": expiresAt.UTC().Format(time.RFC3339), + "tokens": tokens, + }) +} + +// handleAgentResetAuth handles POST /api/v1/agents/{id}/reset-auth. +// It generates a fresh token and pushes it into the running agent container +// via the runtime broker, restarting the agent's token refresh loop without +// a full container restart. +func (s *Server) handleAgentResetAuth(w http.ResponseWriter, r *http.Request, id string) { + ctx := r.Context() + + agent, err := s.store.GetAgent(ctx, id) + if err != nil { + writeErrorFromErr(w, err, "") + return + } + + if s.dispatcher == nil { + writeError(w, http.StatusInternalServerError, ErrCodeInternalError, + "agent dispatcher not configured", nil) + return + } + + if err := s.dispatcher.DispatchAgentResetAuth(ctx, agent); err != nil { + slog.Error("Failed to reset agent auth", "agent_id", id, "error", err) + writeError(w, http.StatusInternalServerError, ErrCodeInternalError, + "auth reset failed: "+err.Error(), nil) + return + } + + slog.Info("Agent auth reset dispatched", "agent_id", id) + writeJSON(w, http.StatusOK, map[string]string{ + "message": "Auth reset dispatched successfully", }) } // OutboundMessageRequest is the request body for POST /api/v1/agents/{id}/outbound-message. type OutboundMessageRequest struct { - Recipient string `json:"recipient,omitempty"` - RecipientID string `json:"recipient_id,omitempty"` - Msg string `json:"msg"` - Type string `json:"type,omitempty"` - Urgent bool `json:"urgent,omitempty"` - Attachments []string `json:"attachments,omitempty"` - Visibility string `json:"visibility,omitempty"` - Metadata map[string]string `json:"metadata,omitempty"` - Channel string `json:"channel,omitempty"` - ThreadID string `json:"thread_id,omitempty"` + Recipient string `json:"recipient,omitempty"` + RecipientID string `json:"recipient_id,omitempty"` + Msg string `json:"msg"` + Type string `json:"type,omitempty"` + Urgent bool `json:"urgent,omitempty"` + Attachments []string `json:"attachments,omitempty"` + Channel string `json:"channel,omitempty"` + ThreadID string `json:"thread_id,omitempty"` } // handleAgentOutboundMessage handles POST /api/v1/agents/{id}/outbound-message. @@ -2054,8 +2122,6 @@ func (s *Server) handleAgentOutboundMessage(w http.ResponseWriter, r *http.Reque // Build a structured message for external dispatch paths. structuredMsg := &messages.StructuredMessage{ - Version: messages.Version, - Timestamp: time.Now().UTC().Format(time.RFC3339), Sender: storeMsg.Sender, SenderID: storeMsg.SenderID, Recipient: storeMsg.Recipient, @@ -2064,17 +2130,10 @@ func (s *Server) handleAgentOutboundMessage(w http.ResponseWriter, r *http.Reque Type: storeMsg.Type, Urgent: storeMsg.Urgent, Attachments: req.Attachments, - Visibility: req.Visibility, - Metadata: req.Metadata, Channel: req.Channel, ThreadID: req.ThreadID, } - if err := structuredMsg.Validate(); err != nil { - ValidationError(w, err.Error(), nil) - return - } - // Route through broker when available; otherwise persist and publish // directly. The broker's deliverToUser callback handles persistence // and SSE, so doing both here would create duplicate messages. @@ -2287,9 +2346,6 @@ func (s *Server) handleAgentMessage(w http.ResponseWriter, r *http.Request, id s if structuredMsg.Type == "" { structuredMsg.Type = messages.TypeInstruction } - if structuredMsg.Channel == "" && GetAgentIdentityFromContext(ctx) == nil { - structuredMsg.Channel = "web" - } } else if req.Message != "" { plainMessage = req.Message // Build a structured message from the plain text so that downstream @@ -2306,16 +2362,13 @@ func (s *Server) handleAgentMessage(w http.ResponseWriter, r *http.Request, id s } structuredMsg = messages.NewInstruction(sender, "agent:"+id, plainMessage) structuredMsg.SenderID = senderID - if GetAgentIdentityFromContext(ctx) == nil { - structuredMsg.Channel = "web" - } } else { ValidationError(w, "message or structured_message is required", nil) return } - // Detect group recipient for multi-target fan-out. - if structuredMsg != nil && messages.IsGroupRecipient(structuredMsg.Recipient) { + // Detect set[] recipient for multi-target fan-out. + if structuredMsg != nil && messages.IsSetRecipient(structuredMsg.Recipient) { s.handleGroupMessage(w, r, id, structuredMsg, plainMessage, req.Interrupt) return } @@ -2413,6 +2466,7 @@ func (s *Server) handleAgentMessage(w http.ResponseWriter, r *http.Request, id s s.logMessage("message dispatched", logAttrs...) // Persist to message store (write-through; non-fatal if store fails) + var persistedMsgID string if structuredMsg != nil { storeMsg := &store.Message{ ID: api.NewUUID(), @@ -2428,7 +2482,7 @@ func (s *Server) handleAgentMessage(w http.ResponseWriter, r *http.Request, id s AgentID: agent.ID, CreatedAt: time.Now(), } - // Propagate GroupID from metadata so CLI-originated group messages + // Propagate GroupID from metadata so CLI-originated set[] messages // preserve correlation in the store. if structuredMsg.Metadata != nil { if gid, ok := structuredMsg.Metadata["group_id"]; ok { @@ -2437,6 +2491,8 @@ func (s *Server) handleAgentMessage(w http.ResponseWriter, r *http.Request, id s } if err := s.store.CreateMessage(ctx, storeMsg); err != nil { s.messageLog.Error("Failed to persist message", "error", err) + } else { + persistedMsgID = storeMsg.ID } // Publish SSE event so connected browser clients can update the // per-agent conversation view in real time — mirrors the agent→user @@ -2454,11 +2510,39 @@ func (s *Server) handleAgentMessage(w http.ResponseWriter, r *http.Request, id s ServiceNotReady(w, "Agent has no runtime broker assigned — the server may still be starting up") return } - if err := dispatcher.DispatchAgentMessage(ctx, agent, plainMessage, req.Interrupt, structuredMsg); err != nil { + if err := dispatcher.DispatchAgentMessage(ctx, agent, plainMessage, req.Interrupt, structuredMsg); errors.Is(err, ErrMessageDeferred) { + s.signalDeferredMessage(ctx, agent.RuntimeBrokerID, agent.ID) + // Create notification subscription if requested (before returning 202) + if req.Notify { + var notifySubscriberType, notifySubscriberID, createdBy string + if agentIdent := GetAgentIdentityFromContext(ctx); agentIdent != nil { + createdBy = agentIdent.ID() + if creatorAgent, err := s.store.GetAgent(ctx, agentIdent.ID()); err == nil { + notifySubscriberType = store.SubscriberTypeAgent + notifySubscriberID = creatorAgent.Slug + } + } else if userIdent := GetUserIdentityFromContext(ctx); userIdent != nil { + createdBy = userIdent.ID() + notifySubscriberType = store.SubscriberTypeUser + notifySubscriberID = userIdent.ID() + } + s.createNotifySubscription(ctx, agent.ID, agent.ProjectID, notifySubscriberType, notifySubscriberID, createdBy) + } + w.WriteHeader(http.StatusAccepted) + return + } else if err != nil { RuntimeError(w, "Failed to send message to runtime broker: "+err.Error()) return } + // Mark the message as dispatched so reconcileBroker does not + // re-deliver it on the next broker reconnect. + if persistedMsgID != "" { + if _, err := s.store.MarkMessageDispatched(ctx, persistedMsgID); err != nil { + s.messageLog.Error("Failed to mark message dispatched", "id", persistedMsgID, "error", err) + } + } + // Publish agent-to-agent messages through the broker so plugin observers // (Telegram, broker-log) can see them. ObserverOnly prevents the hub's own // subscription from re-dispatching. @@ -2494,15 +2578,14 @@ func (s *Server) handleAgentMessage(w http.ResponseWriter, r *http.Request, id s w.WriteHeader(http.StatusOK) } -// GroupMessageRecipientResult holds the delivery status for a single recipient -// in a message group fan-out. +// GroupMessageRecipientResult represents the delivery status for one recipient in a set[] delivery. type GroupMessageRecipientResult struct { Recipient string `json:"recipient"` Status string `json:"status"` Error string `json:"error,omitempty"` } -// GroupMessageResponse is the JSON response for a group message delivery. +// GroupMessageResponse is the JSON response for a set[] message delivery. type GroupMessageResponse struct { GroupID string `json:"group_id"` Delivered int `json:"delivered"` @@ -2510,13 +2593,13 @@ type GroupMessageResponse struct { Results []GroupMessageRecipientResult `json:"results"` } -// handleGroupMessage fans out a structured message to multiple recipients in a message group. +// handleGroupMessage fans out a structured message to multiple recipients parsed from set[]. func (s *Server) handleGroupMessage(w http.ResponseWriter, r *http.Request, anchorID string, msg *messages.StructuredMessage, plainMessage string, interrupt bool) { ctx := r.Context() - recipients, err := messages.ParseGroupRecipient(msg.Recipient) + recipients, err := messages.ParseSetRecipient(msg.Recipient) if err != nil { - ValidationError(w, "invalid group recipient: "+err.Error(), nil) + ValidationError(w, "invalid set[] recipient: "+err.Error(), nil) return } @@ -2532,7 +2615,7 @@ func (s *Server) handleGroupMessage(w http.ResponseWriter, r *http.Request, anch for i, r := range recipients { recipientStrs[i] = r.String() } - recipientsSet := messages.FormatGroupRecipients(msg.Sender, recipientStrs) + recipientsSet := messages.FormatSetRecipients(msg.Sender, recipientStrs) groupID := api.NewUUID() results := make([]GroupMessageRecipientResult, len(recipients)) @@ -2571,12 +2654,17 @@ func (s *Server) handleGroupMessage(w http.ResponseWriter, r *http.Request, anch CreatedAt: time.Now(), } if err := s.store.CreateMessage(ctx, storeMsg); err != nil { - s.messageLog.Error("Failed to persist group message", "recipient", recipStr, "error", err) + s.messageLog.Error("Failed to persist set message", "recipient", recipStr, "error", err) } s.events.PublishUserMessage(ctx, storeMsg) if dispatcher != nil && agent.RuntimeBrokerID != "" { - if err := dispatcher.DispatchAgentMessage(ctx, agent, plainMessage, interrupt, &agentMsg); err != nil { + if err := dispatcher.DispatchAgentMessage(ctx, agent, plainMessage, interrupt, &agentMsg); errors.Is(err, ErrMessageDeferred) { + s.signalDeferredMessage(ctx, agent.RuntimeBrokerID, agent.ID) + results[i] = GroupMessageRecipientResult{Recipient: recipStr, Status: "deferred"} + delivered++ + continue + } else if err != nil { results[i] = GroupMessageRecipientResult{Recipient: recipStr, Status: "failed", Error: err.Error()} continue } @@ -2588,13 +2676,19 @@ func (s *Server) handleGroupMessage(w http.ResponseWriter, r *http.Request, anch continue } + // Mark the message as dispatched so reconcileBroker does not + // re-deliver it on the next broker reconnect. + if _, err := s.store.MarkMessageDispatched(ctx, storeMsg.ID); err != nil { + s.messageLog.Error("Failed to mark set message dispatched", "id", storeMsg.ID, "error", err) + } + // Publish agent-to-agent messages through the broker for plugin observers. if strings.HasPrefix(agentMsg.Sender, "agent:") { if bp := s.GetMessageBrokerProxy(); bp != nil { observerMsg := agentMsg observerMsg.ObserverOnly = true if err := bp.PublishMessage(ctx, projectID, &observerMsg); err != nil { - s.messageLog.Error("Failed to publish group observer message", + s.messageLog.Error("Failed to publish set[] observer message", "recipient", recipStr, "error", err) } } @@ -2657,7 +2751,7 @@ func (s *Server) handleGroupMessage(w http.ResponseWriter, r *http.Request, anch CreatedAt: time.Now(), } if err := s.store.CreateMessage(ctx, storeMsg); err != nil { - s.messageLog.Error("Failed to persist group message", "recipient", recipStr, "error", err) + s.messageLog.Error("Failed to persist set message", "recipient", recipStr, "error", err) } s.events.PublishUserMessage(ctx, storeMsg) @@ -2666,7 +2760,7 @@ func (s *Server) handleGroupMessage(w http.ResponseWriter, r *http.Request, anch } } - s.logMessage("group message dispatched", + s.logMessage("set message dispatched", "project_id", projectID, "group_id", groupID, "total", len(recipients), @@ -2794,10 +2888,15 @@ func (s *Server) broadcastDirect(w http.ResponseWriter, r *http.Request, project agentMsg := *msg agentMsg.Recipient = "agent:" + agent.Slug agentMsg.RecipientID = agent.ID - if err := dispatcher.DispatchAgentMessage(ctx, &agent, agentMsg.Msg, interrupt, &agentMsg); err != nil { + dispatched := false + if err := dispatcher.DispatchAgentMessage(ctx, &agent, agentMsg.Msg, interrupt, &agentMsg); errors.Is(err, ErrMessageDeferred) { + s.signalDeferredMessage(ctx, agent.RuntimeBrokerID, agent.ID) + } else if err != nil { s.messageLog.Error("Failed to deliver broadcast message to agent", "agent_id", agent.ID, "agentSlug", agent.Slug, "error", err) + } else { + dispatched = true } // Persist broadcast message per recipient (non-fatal) storeMsg := &store.Message{ @@ -2816,6 +2915,11 @@ func (s *Server) broadcastDirect(w http.ResponseWriter, r *http.Request, project } if err := s.store.CreateMessage(ctx, storeMsg); err != nil { s.messageLog.Error("Failed to persist broadcast message", "agent_id", agent.ID, "error", err) + } else if dispatched { + // Mark dispatched so reconcileBroker does not re-deliver. + if _, err := s.store.MarkMessageDispatched(ctx, storeMsg.ID); err != nil { + s.messageLog.Error("Failed to mark broadcast message dispatched", "id", storeMsg.ID, "error", err) + } } } @@ -2847,6 +2951,16 @@ func (s *Server) updateAgentStatus(w http.ResponseWriter, r *http.Request, id st return } + // Guard against phase regressions and auto-correct phase from activity. + if status.Phase != "" || status.Activity != "" { + agent, err := s.store.GetAgent(ctx, id) + if err != nil { + writeErrorFromErr(w, err, "") + return + } + guardAgentPhaseTransition(agent, &status) + } + if err := s.store.UpdateAgentStatus(ctx, id, status); err != nil { writeErrorFromErr(w, err, "") return @@ -2862,6 +2976,37 @@ func (s *Server) updateAgentStatus(w http.ResponseWriter, r *http.Request, id st w.WriteHeader(http.StatusOK) } +// guardAgentPhaseTransition applies two guards to a status update: +// +// 1. Phase regression guard: rejects transitions that would move an agent +// backward in its forward-progress lifecycle (e.g. running → starting). +// 2. Activity-driven phase auto-correction: when an activity that implies the +// agent is running arrives but the phase is pre-running, auto-promotes the +// phase to running. +func guardAgentPhaseTransition(agent *store.Agent, status *store.AgentStatusUpdate) { + currentPhase := state.Phase(agent.Phase) + + // Guard 1: reject phase regressions within the forward-progress lifecycle. + if status.Phase != "" { + newPhase := state.Phase(status.Phase) + if currentPhase.IsActivePhase() && newPhase.IsActivePhase() && + newPhase.Ordinal() < currentPhase.Ordinal() { + status.Phase = "" + } + } + + // Guard 2: if an activity that implies the agent is running arrives + // without an explicit phase, and the current phase is pre-running, + // auto-correct the phase to running. + if status.Activity != "" && status.Phase == "" { + activity := state.Activity(status.Activity) + if activity.ImpliesRunning() && currentPhase.IsActivePhase() && + currentPhase != state.PhaseRunning { + status.Phase = string(state.PhaseRunning) + } + } +} + func (s *Server) handleAgentLifecycle(w http.ResponseWriter, r *http.Request, id, action string) { ctx := r.Context() @@ -3179,7 +3324,7 @@ type CreateProjectRequest struct { Slug string `json:"slug,omitempty"` Name string `json:"name"` GitRemote string `json:"gitRemote,omitempty"` - WorkspaceMode string `json:"workspaceMode,omitempty"` // "shared" or "per-agent" (default); only meaningful when gitRemote is set + WorkspaceMode string `json:"workspaceMode,omitempty"` // "shared", "worktree-per-agent", or "per-agent" (default); only meaningful when gitRemote is set Visibility string `json:"visibility,omitempty"` Labels map[string]string `json:"labels,omitempty"` GitHubToken string `json:"githubToken,omitempty"` @@ -3433,12 +3578,15 @@ func (s *Server) createProject(w http.ResponseWriter, r *http.Request) { displayName = api.DisplayNameWithSerial(req.Name, slug, baseSlug) } - // Apply workspace mode label for git projects with shared workspace mode. - if normalizedRemote != "" && req.WorkspaceMode == store.WorkspaceModeShared { - if req.Labels == nil { - req.Labels = make(map[string]string) + // Apply workspace mode label for git projects with explicit workspace mode. + if normalizedRemote != "" { + switch req.WorkspaceMode { + case store.WorkspaceModeShared, store.WorkspaceModeWorktreePerAgent: + if req.Labels == nil { + req.Labels = make(map[string]string) + } + req.Labels[store.LabelWorkspaceMode] = req.WorkspaceMode } - req.Labels[store.LabelWorkspaceMode] = store.WorkspaceModeShared } project := &store.Project{ @@ -3545,7 +3693,7 @@ func (s *Server) createProject(w http.ResponseWriter, r *http.Request) { return } } else if project.GitRemote == "" { - // Hub-managed project (no git remote): create workspace directory. + // Hub-native project (no git remote): create workspace directory. if err := s.initHubManagedProject(project); err != nil { slog.Warn("failed to initialize project workspace", "project_id", project.ID, "slug", project.Slug, "error", err) @@ -3763,21 +3911,26 @@ func (s *Server) createProjectMembersGroupAndPolicy(ctx context.Context, project } // hubManagedProjectPath returns the filesystem path for a hub-managed project workspace. +// It prefers projects/ and falls back to groves/ for backward compatibility +// with workspaces created before the grove-to-project rename. func hubManagedProjectPath(slug string) (string, error) { + if slug == "" { + return "", fmt.Errorf("project slug must not be empty") + } globalDir, err := config.GetGlobalDir() if err != nil { return "", fmt.Errorf("failed to get global dir: %w", err) } - newPath := filepath.Join(globalDir, "projects", slug) - if hasWorkspaceContent(newPath) { - return newPath, nil + projectsPath := filepath.Join(globalDir, "projects", slug) + if hasWorkspaceContent(projectsPath) { + return projectsPath, nil } - oldPath := filepath.Join(globalDir, "groves", slug) - if hasWorkspaceContent(oldPath) { - return oldPath, nil + grovesPath := filepath.Join(globalDir, "groves", slug) + if hasWorkspaceContent(grovesPath) { + return grovesPath, nil } - // Neither has content — return new path (will be created on demand) - return newPath, nil + // Neither has content — return projects path (will be created on demand) + return projectsPath, nil } // hasWorkspaceContent returns true if dir exists and contains meaningful @@ -3817,7 +3970,7 @@ func (s *Server) initHubManagedProject(project *store.Project) error { return fmt.Errorf("failed to create .scion directory: %w", err) } - // Seed default settings.yaml directly in scionDir. Hub-managed projects + // Seed default settings.yaml directly in scionDir. Hub-native projects // bypass InitProject (which uses split storage for git repos) and keep // all configuration in-place. settingsPath := filepath.Join(scionDir, "settings.yaml") @@ -3849,7 +4002,7 @@ func (s *Server) initHubManagedProject(project *store.Project) error { } // cloneSharedWorkspaceProject performs the host-side git clone for a shared-workspace -// git project. It clones the repository into the hub-managed workspace path and +// git project. It clones the repository into the hub-native workspace path and // seeds the .scion project structure on top. If the clone fails, the workspace // directory is cleaned up and an error is returned. func (s *Server) cloneSharedWorkspaceProject(ctx context.Context, project *store.Project) error { @@ -3995,7 +4148,7 @@ func (s *Server) syncWorkspaceOnStop(ctx context.Context, agent *store.Agent) { project, err := s.store.GetProject(ctx, agent.ProjectID) if err != nil || (project.GitRemote != "" && !project.IsSharedWorkspace()) { - return // Not hub-managed/shared-workspace or project not found + return // Not hub-native/shared-workspace or project not found } // Check if broker is co-located (embedded or has local path) @@ -4202,7 +4355,7 @@ func (s *Server) handleProjectRegister(w http.ResponseWriter, r *http.Request) { // Add as project provider. When the project already existed and the // broker is already a provider, preserve the existing localPath to - // avoid converting a hub-managed git project into a linked project. + // avoid converting a hub-native git project into a linked project. localPath := req.Path if !created { if existingProvider, err := s.store.GetProjectProvider(ctx, project.ID, broker.ID); err == nil { @@ -4298,7 +4451,7 @@ func (s *Server) handleProjectRegister(w http.ResponseWriter, r *http.Request) { // Add as project provider. When the project already existed and the // broker is already a provider, preserve the existing localPath to - // avoid converting a hub-managed git project into a linked project. + // avoid converting a hub-native git project into a linked project. localPath := req.Path if !created { if existingProvider, err := s.store.GetProjectProvider(ctx, project.ID, broker.ID); err == nil { @@ -4529,12 +4682,6 @@ func (s *Server) handleProjectRoutes(w http.ResponseWriter, r *http.Request) { return } - // Check for nested /import-harness-configs path - if subPath == "import-harness-configs" { - s.handleProjectImportHarnessConfigs(w, r, projectID) - return - } - // Check for nested /dav/ path (WebDAV endpoint for project workspace sync) if strings.HasPrefix(subPath, "dav") { davPath := strings.TrimPrefix(subPath, "dav") @@ -5132,6 +5279,7 @@ func (s *Server) updateProject(w http.ResponseWriter, r *http.Request, id string var updates struct { Name string `json:"name,omitempty"` + Slug string `json:"slug,omitempty"` Labels map[string]string `json:"labels,omitempty"` Visibility string `json:"visibility,omitempty"` DefaultRuntimeBrokerID string `json:"defaultRuntimeBrokerId,omitempty"` @@ -5142,9 +5290,31 @@ func (s *Server) updateProject(w http.ResponseWriter, r *http.Request, id string return } + oldSlug := project.Slug + if updates.Name != "" { project.Name = updates.Name } + if updates.Slug != "" { + newSlug := api.Slugify(updates.Slug) + if newSlug == "" { + BadRequest(w, "Invalid slug: must contain at least one alphanumeric character") + return + } + if newSlug != oldSlug { + existing, err := s.store.GetProjectBySlug(ctx, newSlug) + if err != nil && err != store.ErrNotFound { + writeErrorFromErr(w, err, "") + return + } + if err == nil && existing.ID != project.ID { + writeError(w, http.StatusConflict, ErrCodeConflict, + fmt.Sprintf("A project with slug %q already exists", newSlug), nil) + return + } + project.Slug = newSlug + } + } if updates.Labels != nil { project.Labels = updates.Labels } @@ -5160,11 +5330,107 @@ func (s *Server) updateProject(w http.ResponseWriter, r *http.Request, id string return } + // If the slug changed, update associated group slugs and filesystem paths. + if project.Slug != oldSlug { + s.migrateProjectSlug(ctx, project, oldSlug) + } + s.events.PublishProjectUpdated(ctx, project) writeJSON(w, http.StatusOK, project) } +// migrateProjectSlug updates group slugs and filesystem paths after a project slug change. +// This is best-effort: failures are logged but don't roll back the rename. +func (s *Server) migrateProjectSlug(ctx context.Context, project *store.Project, oldSlug string) { + newSlug := project.Slug + + // Migrate the project agents group slug. + oldAgentsSlug := "project:" + oldSlug + ":agents" + newAgentsSlug := "project:" + newSlug + ":agents" + if group, err := s.store.GetGroupBySlug(ctx, oldAgentsSlug); err == nil { + group.Slug = newAgentsSlug + group.Name = project.Name + " Agents" + if err := s.store.UpdateGroup(ctx, group); err != nil { + slog.Warn("failed to migrate project agents group slug", + "project_id", project.ID, "old_slug", oldAgentsSlug, "new_slug", newAgentsSlug, "error", err) + } + } else if err != store.ErrNotFound { + slog.Warn("failed to retrieve project agents group for migration", + "project_id", project.ID, "old_slug", oldAgentsSlug, "error", err) + } + + // Migrate the project members group slug. + oldMembersSlug := "project:" + oldSlug + ":members" + newMembersSlug := "project:" + newSlug + ":members" + if group, err := s.store.GetGroupBySlug(ctx, oldMembersSlug); err == nil { + group.Slug = newMembersSlug + group.Name = project.Name + " Members" + if err := s.store.UpdateGroup(ctx, group); err != nil { + slog.Warn("failed to migrate project members group slug", + "project_id", project.ID, "old_slug", oldMembersSlug, "new_slug", newMembersSlug, "error", err) + } + } else if err != store.ErrNotFound { + slog.Warn("failed to retrieve project members group for migration", + "project_id", project.ID, "old_slug", oldMembersSlug, "error", err) + } + + // Migrate the project member policy name. + oldPolicyName := "project:" + oldSlug + ":member-create-agents" + newPolicyName := "project:" + newSlug + ":member-create-agents" + if policies, err := s.store.ListPolicies(ctx, store.PolicyFilter{Name: oldPolicyName}, store.ListOptions{Limit: 1}); err == nil && len(policies.Items) > 0 { + policy := &policies.Items[0] + policy.Name = newPolicyName + if err := s.store.UpdatePolicy(ctx, policy); err != nil { + slog.Warn("failed to migrate project member policy name", + "project_id", project.ID, "old_policy", oldPolicyName, "new_policy", newPolicyName, "error", err) + } + } else if err != nil { + slog.Warn("failed to retrieve project member policy for migration", + "project_id", project.ID, "old_policy", oldPolicyName, "error", err) + } + + // Migrate hub-managed project filesystem paths (best-effort). + // Derive newPath from oldPath's parent to preserve the directory type (groves/ vs projects/). + if oldPath, err := hubManagedProjectPath(oldSlug); err == nil { + if _, statErr := os.Stat(oldPath); statErr == nil { + newPath := filepath.Join(filepath.Dir(oldPath), newSlug) + if _, statErr := os.Stat(newPath); os.IsNotExist(statErr) { + if err := os.Rename(oldPath, newPath); err != nil { + slog.Warn("failed to rename project workspace directory", + "project_id", project.ID, "old_path", oldPath, "new_path", newPath, "error", err) + } + } + } + } + + // Migrate the project config directory (~/.scion/project-configs/__/). + oldMarker := &config.ProjectMarker{ + ProjectID: project.ID, + ProjectSlug: oldSlug, + } + newMarker := &config.ProjectMarker{ + ProjectID: project.ID, + ProjectSlug: newSlug, + } + if oldConfigPath, err := oldMarker.ExternalProjectPath(); err == nil { + if newConfigPath, err := newMarker.ExternalProjectPath(); err == nil { + oldConfigDir := filepath.Dir(oldConfigPath) + newConfigDir := filepath.Dir(newConfigPath) + if _, statErr := os.Stat(oldConfigDir); statErr == nil { + if _, statErr := os.Stat(newConfigDir); os.IsNotExist(statErr) { + if err := os.MkdirAll(filepath.Dir(newConfigDir), 0755); err == nil { + if err := os.Rename(oldConfigDir, newConfigDir); err != nil { + slog.Warn("failed to rename project config directory", + "project_id", project.ID, "old_path", oldConfigDir, "new_path", newConfigDir, "error", err) + } + } + } + } + } + } +} + func (s *Server) deleteProject(w http.ResponseWriter, r *http.Request, id string) { ctx := r.Context() @@ -5245,7 +5511,7 @@ func (s *Server) deleteProject(w http.ResponseWriter, r *http.Request, id string // Clean up project-scoped harness configs (best-effort), including storage files. s.deleteProjectHarnessConfigs(ctx, id) - // For hub-managed and shared-workspace projects, notify provider brokers to clean up + // For hub-native and shared-workspace projects, notify provider brokers to clean up // their local project directories. This must run before DeleteProject because // the cascade deletes the project_providers we need to enumerate. if project.GitRemote == "" || project.IsSharedWorkspace() { @@ -5257,7 +5523,7 @@ func (s *Server) deleteProject(w http.ResponseWriter, r *http.Request, id string return } - // For hub-managed and shared-workspace projects, remove the filesystem directory. + // For hub-native and shared-workspace projects, remove the filesystem directory. if (project.GitRemote == "" || project.IsSharedWorkspace()) && project.Slug != "" { if projectPath, err := hubManagedProjectPath(project.Slug); err == nil { if err := util.RemoveAllSafe(projectPath); err != nil { @@ -5440,7 +5706,7 @@ func (s *Server) cleanupBrokerProjectDirectories(ctx context.Context, project *s continue } - if err := client.CleanupProject(ctx, provider.BrokerID, broker.Endpoint, project.Slug); err != nil { + if err := client.CleanupProject(ctx, provider.BrokerID, broker.Endpoint, project.Slug, project.ID); err != nil { slog.Warn("failed to cleanup project on broker", "project_id", project.ID, "slug", project.Slug, "broker", provider.BrokerID, "endpoint", broker.Endpoint, "error", err) @@ -6094,8 +6360,17 @@ func (s *Server) handleBrokerHeartbeat(w http.ResponseWriter, r *http.Request, i statusUpdate.Message = agentHB.Message } } else { - // Structured path: broker sent Phase/Activity directly - statusUpdate.Phase = agentHB.Phase + // Structured path: broker sent Phase/Activity directly. + // Guard against phase regressions: stale heartbeat data + // must not move a running agent back to starting/etc. + hbPhase := state.Phase(agentHB.Phase) + curPhase := state.Phase(agent.Phase) + if curPhase.IsActivePhase() && hbPhase.IsActivePhase() && + hbPhase.Ordinal() < curPhase.Ordinal() { + // Suppress the regression — keep the hub's phase. + } else { + statusUpdate.Phase = agentHB.Phase + } // Only propagate Activity when it differs from the stored // value. Heartbeats always report the current activity, but // repeating the same value would refresh last_activity_event @@ -8592,24 +8867,6 @@ func (s *Server) getHarnessConfigFromTemplate(template *store.Template, fallback return fallback } -// lookupHarnessConfigRecord resolves a harness-config reference (name or slug) -// to its Hub record, checking project scope first then global — the same -// precedence the broker uses for on-disk lookup. Returns nil if not found. -func (s *Server) lookupHarnessConfigRecord(ctx context.Context, projectID, ref string) *store.HarnessConfig { - if ref == "" { - return nil - } - if projectID != "" { - if hc, err := s.store.GetHarnessConfigBySlug(ctx, ref, store.HarnessConfigScopeProject, projectID); err == nil && hc != nil { - return hc - } - } - if hc, err := s.store.GetHarnessConfigBySlug(ctx, ref, store.HarnessConfigScopeGlobal, ""); err == nil && hc != nil { - return hc - } - return nil -} - // buildAppliedConfig constructs an AgentAppliedConfig from a CreateAgentRequest. // When req.Config is a ScionConfig, its fields are extracted into the applied config // and the full ScionConfig is preserved as InlineConfig for threading to the broker. @@ -8735,25 +8992,6 @@ func (s *Server) populateAgentConfig(ctx context.Context, agent *store.Agent, pr } } - // Resolve the harness-config name to a Hub record so the broker can hydrate - // it from the configured storage backend, mirroring template hydration. - // Without this a remote broker can only use harness-configs that happen to - // exist on its local filesystem (see resource-storage-refactor §4/§7.3 step 4). - hcRef := agent.AppliedConfig.HarnessConfig - if hcRef == "" && resolvedTemplate != nil { - hcRef = s.getHarnessConfigFromTemplate(resolvedTemplate, "") - } - if hcRef != "" { - projectID := "" - if project != nil { - projectID = project.ID - } - if hc := s.lookupHarnessConfigRecord(ctx, projectID, hcRef); hc != nil { - agent.AppliedConfig.HarnessConfigID = hc.ID - agent.AppliedConfig.HarnessConfigHash = hc.ContentHash - } - } - // Merge hub-level telemetry config as lowest-priority default. // Only applies when no per-agent or template telemetry config is set. s.mu.RLock() @@ -8800,6 +9038,8 @@ const ( existingAgentStarted // existingAgentErrored means an error occurred; response already written. existingAgentErrored + // existingAgentConflict means an active agent with the same slug exists; caller should return 409. + existingAgentConflict ) // createNotifySubscription creates a notification subscription for the given agent @@ -8833,12 +9073,10 @@ func (s *Server) createNotifySubscription(ctx context.Context, agentID, projectI // already exists when a create/start request arrives. // // Phases: -// 0. Resume from suspended (suspended): recover broker, dispatch start, update in-place → started -// 1. Resume from stopped (stopped + Resume flag): recover broker, dispatch start, update in-place → started -// 2. Stale cleanup (running/stopped/error + not resume + not provision-only): dispatch delete, remove from DB → deleted -// 3. Env-gather re-provisioning (provisioning + GatherEnv): dispatch delete, remove from DB → deleted -// 4. Restart (created/provisioning/pending + not provision-only): recover broker ID, update config, dispatch start → started -// 5. Otherwise: none (caller decides what to do) +// 1. Stale cleanup (running/stopped/error + not provision-only): dispatch delete, remove from DB → deleted +// 2. Env-gather re-provisioning (provisioning + GatherEnv): dispatch delete, remove from DB → deleted +// 3. Restart (created/provisioning/pending + not provision-only): recover broker ID, update config, dispatch start → started +// 4. Otherwise: none (caller decides what to do) func (s *Server) handleExistingAgent( ctx context.Context, w http.ResponseWriter, @@ -8904,72 +9142,56 @@ func (s *Server) handleExistingAgent( return existingAgentStarted } - // Stopped agents resumed in-place when the caller explicitly requests resume. - // This preserves the agent ID, metadata, and template association. The broker - // will recreate the container but the hub-level record stays the same. - if !req.ProvisionOnly && req.Resume && existingAgent.Phase == string(state.PhaseStopped) { - if existingAgent.RuntimeBrokerID == "" && runtimeBrokerID != "" { - existingAgent.RuntimeBrokerID = runtimeBrokerID - } - - dispatcher := s.GetDispatcher() - if dispatcher == nil || existingAgent.RuntimeBrokerID == "" { - writeError(w, http.StatusBadRequest, ErrCodeValidationError, - "cannot resume agent: no runtime broker available", nil) - return existingAgentErrored - } + // Phase 1: Agent is running/stopped/error. + // Resume=true for stopped agents restarts in-place; otherwise reject as duplicate. + if !req.ProvisionOnly && + (existingAgent.Phase == string(state.PhaseRunning) || + existingAgent.Phase == string(state.PhaseStopped) || + existingAgent.Phase == string(state.PhaseError)) { - if existingAgent.AppliedConfig == nil { - existingAgent.AppliedConfig = &store.AgentAppliedConfig{} - } - if req.Task != "" { - existingAgent.AppliedConfig.Task = req.Task - existingAgent.AppliedConfig.Attach = req.Attach - } + // Resume a stopped agent in-place when explicitly requested. + if req.Resume && existingAgent.Phase == string(state.PhaseStopped) { + if existingAgent.RuntimeBrokerID == "" && runtimeBrokerID != "" { + existingAgent.RuntimeBrokerID = runtimeBrokerID + } - if err := dispatcher.DispatchAgentStart(ctx, existingAgent, req.Task); err != nil { - RuntimeError(w, "Failed to resume stopped agent: "+err.Error()) - return existingAgentErrored - } + dispatcher := s.GetDispatcher() + if dispatcher == nil || existingAgent.RuntimeBrokerID == "" { + writeError(w, http.StatusBadRequest, ErrCodeValidationError, + "cannot resume agent: no runtime broker available", nil) + return existingAgentErrored + } - existingAgent.Phase = string(state.PhaseRunning) - if err := s.store.UpdateAgent(ctx, existingAgent); err != nil { - s.agentLifecycleLog.Warn("Failed to update agent status after resume from stopped", "agent_id", existingAgent.ID, "error", err) - } + if req.Task != "" { + if existingAgent.AppliedConfig == nil { + existingAgent.AppliedConfig = &store.AgentAppliedConfig{} + } + existingAgent.AppliedConfig.Task = req.Task + existingAgent.AppliedConfig.Attach = req.Attach + } - if req.Notify { - s.createNotifySubscription(ctx, existingAgent.ID, existingAgent.ProjectID, notifySubscriberType, notifySubscriberID, createdBy) - } + if err := dispatcher.DispatchAgentStart(ctx, existingAgent, req.Task); err != nil { + RuntimeError(w, "Failed to resume stopped agent: "+err.Error()) + return existingAgentErrored + } - s.enrichAgent(ctx, existingAgent, project, nil) - writeJSON(w, http.StatusOK, CreateAgentResponse{ - Agent: existingAgent, - }) - return existingAgentStarted - } + existingAgent.Phase = string(state.PhaseRunning) + if err := s.updateAgentAfterDispatch(ctx, existingAgent); err != nil { + s.agentLifecycleLog.Warn("Failed to update agent status after resume", "agent_id", existingAgent.ID, "error", err) + } - // Phase 1: Stale cleanup — agent is running/stopped/error and caller wants a real start. - // The old agent is deleted so a fresh one can be created with a new ID. - if !req.ProvisionOnly && - (existingAgent.Phase == string(state.PhaseRunning) || - existingAgent.Phase == string(state.PhaseStopped) || - existingAgent.Phase == string(state.PhaseError)) { - dispatcher := s.GetDispatcher() - if dispatcher != nil && existingAgent.RuntimeBrokerID != "" { - if err := dispatcher.DispatchAgentDelete(ctx, existingAgent, false, false, false, time.Time{}); err != nil { - if cleanupMode != "force" { - RuntimeError(w, "Failed to clean up existing agent before recreate: "+err.Error()) - return existingAgentErrored - } - s.agentLifecycleLog.Warn("Proceeding after stale-agent cleanup failure due to cleanupMode=force", - "agent_id", existingAgent.ID, "agentName", existingAgent.Name, "error", err) + if req.Notify { + s.createNotifySubscription(ctx, existingAgent.ID, existingAgent.ProjectID, notifySubscriberType, notifySubscriberID, createdBy) } + + s.enrichAgent(ctx, existingAgent, project, nil) + writeJSON(w, http.StatusOK, CreateAgentResponse{ + Agent: existingAgent, + }) + return existingAgentStarted } - if err := s.store.DeleteAgent(ctx, existingAgent.ID); err != nil { - writeErrorFromErr(w, err, "") - return existingAgentErrored - } - return existingAgentDeleted + + return existingAgentConflict } // Phase 2: Env-gather re-provisioning — provisioning + GatherEnv requested. @@ -9045,7 +9267,7 @@ func (s *Server) handleExistingAgent( return existingAgentStarted } - return existingAgentNone + return existingAgentConflict } // resolveRuntimeBroker determines which runtime broker should run the agent. @@ -9078,7 +9300,7 @@ func (s *Server) resolveRuntimeBroker(ctx context.Context, w http.ResponseWriter "totalProviders", len(allProviders), "onlineProviders", len(availableBrokers), "defaultBroker", project.DefaultRuntimeBrokerID, - "isHubManaged", project.GitRemote == "") + "isHubNative", project.GitRemote == "") // Convert to summary for error responses, marking and prioritizing the default broker brokerSummaries := make([]RuntimeBrokerSummary, 0, len(availableBrokers)) @@ -9372,25 +9594,13 @@ func (s *Server) handleProjectImportTemplates(w http.ResponseWriter, r *http.Req return } - kind := s.templateImportKind() - var run func(progress importProgressFunc) ([]string, error) + var imported []string if req.WorkspacePath != "" { - run = func(progress importProgressFunc) ([]string, error) { - return s.importFromWorkspace(ctx, project, req.WorkspacePath, store.TemplateScopeProject, kind, progress) - } + imported, err = s.importTemplatesFromWorkspace(ctx, project, req.WorkspacePath) } else { - sourceURL := config.NormalizeTemplateSourceURL(req.SourceURL) - run = func(progress importProgressFunc) ([]string, error) { - return s.importFromRemote(ctx, projectID, sourceURL, store.TemplateScopeProject, kind, progress) - } - } - - if importAcceptsNDJSON(r) { - s.streamImport(w, run) - return + req.SourceURL = config.NormalizeTemplateSourceURL(req.SourceURL) + imported, err = s.importTemplatesFromRemote(ctx, projectID, req.SourceURL) } - - imported, err := run(nil) if err != nil { writeError(w, http.StatusBadRequest, "import_failed", err.Error(), nil) return @@ -9402,127 +9612,6 @@ func (s *Server) handleProjectImportTemplates(w http.ResponseWriter, r *http.Req }) } -// ============================================================================ -// Project Harness-Config Import -// ============================================================================ - -// ImportHarnessConfigsRequest is the request body for direct harness-config -// import. Exactly one of SourceURL or WorkspacePath should be provided. -type ImportHarnessConfigsRequest struct { - SourceURL string `json:"sourceUrl"` - WorkspacePath string `json:"workspacePath"` -} - -// ImportHarnessConfigsResponse is returned after a direct harness-config import -// completes. -type ImportHarnessConfigsResponse struct { - HarnessConfigs []string `json:"harnessConfigs"` - Count int `json:"count"` -} - -// handleProjectImportHarnessConfigs imports harness-configs directly from a -// remote URL or the project workspace into the project's harness-config store, -// mirroring handleProjectImportTemplates. -func (s *Server) handleProjectImportHarnessConfigs(w http.ResponseWriter, r *http.Request, projectID string) { - if r.Method != http.MethodPost { - MethodNotAllowed(w) - return - } - - ctx := r.Context() - - // Authorize the caller - if agentIdent := GetAgentIdentityFromContext(ctx); agentIdent != nil { - if !agentIdent.HasScope(ScopeAgentCreate) { - writeError(w, http.StatusForbidden, ErrCodeForbidden, "Missing required scope: project:agent:create", nil) - return - } - if projectID != agentIdent.ProjectID() { - writeError(w, http.StatusForbidden, ErrCodeForbidden, "Agents can only import harness-configs within their own project", nil) - return - } - } else if userIdent := GetUserIdentityFromContext(ctx); userIdent != nil { - decision := s.authzService.CheckAccess(ctx, userIdent, Resource{ - Type: "agent", - ParentType: "project", - ParentID: projectID, - }, ActionCreate) - if !decision.Allowed { - writeError(w, http.StatusForbidden, ErrCodeForbidden, - "You don't have permission to import harness-configs in this project", nil) - return - } - } else { - writeError(w, http.StatusUnauthorized, "unauthorized", "Authentication required", nil) - return - } - - var req ImportHarnessConfigsRequest - if err := readJSON(r, &req); err != nil { - writeError(w, http.StatusBadRequest, "invalid_request", "Invalid request body", nil) - return - } - - if req.SourceURL != "" && req.WorkspacePath != "" { - writeError(w, http.StatusBadRequest, "invalid_request", "Exactly one of sourceUrl or workspacePath must be provided", nil) - return - } - - if req.SourceURL == "" && req.WorkspacePath == "" { - // Default workspace path when neither is provided - req.WorkspacePath = "/.scion/harness-configs" - } - - // Verify project exists - project, err := s.store.GetProject(ctx, projectID) - if err != nil { - if err == store.ErrNotFound { - NotFound(w, "Project") - return - } - writeErrorFromErr(w, err, "") - return - } - - if s.GetStorage() == nil { - writeError(w, http.StatusServiceUnavailable, "storage_unavailable", "Harness-config storage is not configured", nil) - return - } - - kind := s.harnessConfigImportKind() - var run func(progress importProgressFunc) ([]string, error) - if req.WorkspacePath != "" { - run = func(progress importProgressFunc) ([]string, error) { - return s.importFromWorkspace(ctx, project, req.WorkspacePath, store.HarnessConfigScopeProject, kind, progress) - } - } else { - sourceURL := config.NormalizeTemplateSourceURL(req.SourceURL) - run = func(progress importProgressFunc) ([]string, error) { - return s.importFromRemote(ctx, projectID, sourceURL, store.HarnessConfigScopeProject, kind, progress) - } - } - - if importAcceptsNDJSON(r) { - s.streamImport(w, run) - return - } - - imported, err := run(nil) - if err != nil { - writeError(w, http.StatusBadRequest, "import_failed", err.Error(), nil) - return - } - - writeJSON(w, http.StatusOK, ImportHarnessConfigsResponse{ - HarnessConfigs: imported, - Count: len(imported), - }) -} - -// ============================================================================ -// Unified Resource Import (kind/scope-generic) -// ============================================================================ - // ImportResourcesRequest is the body for the unified import endpoint // (POST /api/v1/resources/import). It imports a single kind of resource from a // remote source URL into the given scope. diff --git a/pkg/hub/handlers_agent_test.go b/pkg/hub/handlers_agent_test.go index e445ac367..249053fca 100644 --- a/pkg/hub/handlers_agent_test.go +++ b/pkg/hub/handlers_agent_test.go @@ -30,7 +30,7 @@ import ( "github.com/GoogleCloudPlatform/scion/pkg/api" "github.com/GoogleCloudPlatform/scion/pkg/messages" "github.com/GoogleCloudPlatform/scion/pkg/store" - "github.com/GoogleCloudPlatform/scion/pkg/store/sqlite" + "github.com/GoogleCloudPlatform/scion/pkg/store/entadapter" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -41,7 +41,7 @@ func TestAgentStatusUpdate_Authorization(t *testing.T) { // Create a project project := &store.Project{ - ID: "project-1", + ID: tid("project-1"), Name: "Test Project", Slug: "test-project", } @@ -49,7 +49,7 @@ func TestAgentStatusUpdate_Authorization(t *testing.T) { // Create two agents agent1 := &store.Agent{ - ID: "agent-1", + ID: tid("agent-1"), Slug: "agent-1-slug", Name: "Agent 1", ProjectID: project.ID, @@ -58,7 +58,7 @@ func TestAgentStatusUpdate_Authorization(t *testing.T) { require.NoError(t, s.CreateAgent(ctx, agent1)) agent2 := &store.Agent{ - ID: "agent-2", + ID: tid("agent-2"), Slug: "agent-2-slug", Name: "Agent 2", ProjectID: project.ID, @@ -80,7 +80,7 @@ func TestAgentStatusUpdate_Authorization(t *testing.T) { Message: "Waiting for user input", } body, _ := json.Marshal(status) - req := httptest.NewRequest(http.MethodPost, "/api/v1/agents/agent-1/status", bytes.NewReader(body)) + req := httptest.NewRequest(http.MethodPost, "/api/v1/agents/"+agent1.ID+"/status", bytes.NewReader(body)) req.Header.Set("X-Scion-Agent-Token", token1) req.Header.Set("Content-Type", "application/json") @@ -101,7 +101,7 @@ func TestAgentStatusUpdate_Authorization(t *testing.T) { Phase: "error", } body, _ := json.Marshal(status) - req := httptest.NewRequest(http.MethodPost, "/api/v1/agents/agent-2/status", bytes.NewReader(body)) + req := httptest.NewRequest(http.MethodPost, "/api/v1/agents/"+agent2.ID+"/status", bytes.NewReader(body)) req.Header.Set("X-Scion-Agent-Token", token1) req.Header.Set("Content-Type", "application/json") @@ -112,7 +112,7 @@ func TestAgentStatusUpdate_Authorization(t *testing.T) { }) t.Run("Agent 1 cannot perform lifecycle actions", func(t *testing.T) { - req := httptest.NewRequest(http.MethodPost, "/api/v1/agents/agent-1/stop", nil) + req := httptest.NewRequest(http.MethodPost, "/api/v1/agents/"+agent1.ID+"/stop", nil) req.Header.Set("X-Scion-Agent-Token", token1) rec := httptest.NewRecorder() @@ -126,7 +126,7 @@ func TestAgentStatusUpdate_Authorization(t *testing.T) { Phase: "running", } body, _ := json.Marshal(status) - req := httptest.NewRequest(http.MethodPost, "/api/v1/agents/agent-1/status", bytes.NewReader(body)) + req := httptest.NewRequest(http.MethodPost, "/api/v1/agents/"+agent1.ID+"/status", bytes.NewReader(body)) req.Header.Set("Authorization", "Bearer "+testDevToken) req.Header.Set("Content-Type", "application/json") @@ -147,7 +147,7 @@ func TestAgentStatusUpdate_Heartbeat(t *testing.T) { // Create a project project := &store.Project{ - ID: "project-h", + ID: tid("project-h"), Name: "Heartbeat Project", Slug: "heartbeat-project", } @@ -155,7 +155,7 @@ func TestAgentStatusUpdate_Heartbeat(t *testing.T) { // Create an agent agent := &store.Agent{ - ID: "agent-h", + ID: tid("agent-h"), Slug: "agent-h-slug", Name: "Agent Heartbeat", ProjectID: project.ID, @@ -177,7 +177,7 @@ func TestAgentStatusUpdate_Heartbeat(t *testing.T) { Heartbeat: true, } body, _ := json.Marshal(status) - req := httptest.NewRequest(http.MethodPost, "/api/v1/agents/agent-h/status", bytes.NewReader(body)) + req := httptest.NewRequest(http.MethodPost, "/api/v1/agents/"+agent.ID+"/status", bytes.NewReader(body)) req.Header.Set("Authorization", "Bearer "+testDevToken) req.Header.Set("Content-Type", "application/json") @@ -198,14 +198,14 @@ func setupOfflineBrokerAgent(t *testing.T, s store.Store, suffix string) (*store ctx := context.Background() project := &store.Project{ - ID: fmt.Sprintf("project-offline-%s", suffix), + ID: tid(fmt.Sprintf("project-offline-%s", suffix)), Name: fmt.Sprintf("Offline Project %s", suffix), Slug: fmt.Sprintf("offline-project-%s", suffix), } require.NoError(t, s.CreateProject(ctx, project)) broker := &store.RuntimeBroker{ - ID: fmt.Sprintf("broker-offline-%s", suffix), + ID: tid(fmt.Sprintf("broker-offline-%s", suffix)), Name: fmt.Sprintf("Offline Broker %s", suffix), Slug: fmt.Sprintf("offline-broker-%s", suffix), Status: store.BrokerStatusOffline, @@ -213,7 +213,7 @@ func setupOfflineBrokerAgent(t *testing.T, s store.Store, suffix string) (*store require.NoError(t, s.CreateRuntimeBroker(ctx, broker)) agent := &store.Agent{ - ID: fmt.Sprintf("agent-offline-%s", suffix), + ID: tid(fmt.Sprintf("agent-offline-%s", suffix)), Slug: fmt.Sprintf("agent-offline-%s-slug", suffix), Name: fmt.Sprintf("Agent Offline %s", suffix), ProjectID: project.ID, @@ -244,14 +244,14 @@ func TestDeleteAgent_NoBroker(t *testing.T) { ctx := context.Background() project := &store.Project{ - ID: "project-nobroker", + ID: tid("project-nobroker"), Name: "No Broker Project", Slug: "no-broker-project", } require.NoError(t, s.CreateProject(ctx, project)) agent := &store.Agent{ - ID: "agent-nobroker", + ID: tid("agent-nobroker"), Slug: "agent-nobroker-slug", Name: "Agent No Broker", ProjectID: project.ID, @@ -290,14 +290,14 @@ func setupOnlineBrokerAgent(t *testing.T, s store.Store, suffix string) (*store. ctx := context.Background() project := &store.Project{ - ID: fmt.Sprintf("project-online-%s", suffix), + ID: tid(fmt.Sprintf("project-online-%s", suffix)), Name: fmt.Sprintf("Online Project %s", suffix), Slug: fmt.Sprintf("online-project-%s", suffix), } require.NoError(t, s.CreateProject(ctx, project)) broker := &store.RuntimeBroker{ - ID: fmt.Sprintf("broker-online-%s", suffix), + ID: tid(fmt.Sprintf("broker-online-%s", suffix)), Name: fmt.Sprintf("Online Broker %s", suffix), Slug: fmt.Sprintf("online-broker-%s", suffix), Status: store.BrokerStatusOnline, @@ -306,7 +306,7 @@ func setupOnlineBrokerAgent(t *testing.T, s store.Store, suffix string) (*store. require.NoError(t, s.CreateRuntimeBroker(ctx, broker)) agent := &store.Agent{ - ID: fmt.Sprintf("agent-online-%s", suffix), + ID: tid(fmt.Sprintf("agent-online-%s", suffix)), Slug: fmt.Sprintf("agent-online-%s-slug", suffix), Name: fmt.Sprintf("Agent Online %s", suffix), ProjectID: project.ID, @@ -424,7 +424,7 @@ func TestAgentCreateAgent_WithScope(t *testing.T) { // Create a project project := &store.Project{ - ID: "project-parent", + ID: tid("project-parent"), Name: "Parent Project", Slug: "parent-project", } @@ -432,7 +432,7 @@ func TestAgentCreateAgent_WithScope(t *testing.T) { // Create a runtime broker and provider for the project broker := &store.RuntimeBroker{ - ID: "broker-parent", + ID: tid("broker-parent"), Name: "Parent Broker", Slug: "parent-broker", Status: store.BrokerStatusOnline, @@ -451,10 +451,14 @@ func TestAgentCreateAgent_WithScope(t *testing.T) { project.DefaultRuntimeBrokerID = broker.ID require.NoError(t, s.UpdateProject(ctx, project)) - // Create the calling agent + // Create the calling agent. Deliberately do NOT seed a matching user row: + // in production the creator is an agent whose ID has no users-table entry, + // and created_by/owner_id must accept that agent ID as a polymorphic + // principal reference. (Regression guard for the agent-created sub-agent + // FK-violation bug.) callingAgent := &store.Agent{ - ID: "agent-caller", - Slug: "agent-caller", + ID: tid("agent-caller"), + Slug: tid("agent-caller"), Name: "Calling Agent", ProjectID: project.ID, Phase: string(state.PhaseRunning), @@ -498,7 +502,7 @@ func TestAgentCreateAgent_WithScope(t *testing.T) { t.Run("Agent with project:agent:create scope rejected for different project", func(t *testing.T) { // Create another project otherProject := &store.Project{ - ID: "project-other", + ID: tid("project-other"), Name: "Other Project", Slug: "other-project", } @@ -552,7 +556,7 @@ func TestAgentLifecycle_WithScope(t *testing.T) { // Create a project project := &store.Project{ - ID: "project-lc", + ID: tid("project-lc"), Name: "Lifecycle Project", Slug: "lifecycle-project", } @@ -560,8 +564,8 @@ func TestAgentLifecycle_WithScope(t *testing.T) { // Create the calling agent callingAgent := &store.Agent{ - ID: "agent-lc-caller", - Slug: "agent-lc-caller", + ID: tid("agent-lc-caller"), + Slug: tid("agent-lc-caller"), Name: "Lifecycle Caller", ProjectID: project.ID, Phase: string(state.PhaseRunning), @@ -570,8 +574,8 @@ func TestAgentLifecycle_WithScope(t *testing.T) { // Create a target agent in the same project targetAgent := &store.Agent{ - ID: "agent-lc-target", - Slug: "agent-lc-target", + ID: tid("agent-lc-target"), + Slug: tid("agent-lc-target"), Name: "Lifecycle Target", ProjectID: project.ID, Phase: string(state.PhaseRunning), @@ -601,15 +605,15 @@ func TestAgentLifecycle_WithScope(t *testing.T) { t.Run("Agent with project:agent:lifecycle scope rejected for cross-project lifecycle", func(t *testing.T) { // Create another project and agent otherProject := &store.Project{ - ID: "project-lc-other", + ID: tid("project-lc-other"), Name: "Other LC Project", Slug: "other-lc-project", } require.NoError(t, s.CreateProject(ctx, otherProject)) otherAgent := &store.Agent{ - ID: "agent-lc-other", - Slug: "agent-lc-other", + ID: tid("agent-lc-other"), + Slug: tid("agent-lc-other"), Name: "Other LC Agent", ProjectID: otherProject.ID, Phase: string(state.PhaseRunning), @@ -654,14 +658,14 @@ func TestAgentGetAgent_ProjectIsolation(t *testing.T) { // Create two projects project1 := &store.Project{ - ID: "project-get1", + ID: tid("project-get1"), Name: "Get Project 1", Slug: "get-project-1", } require.NoError(t, s.CreateProject(ctx, project1)) project2 := &store.Project{ - ID: "project-get2", + ID: tid("project-get2"), Name: "Get Project 2", Slug: "get-project-2", } @@ -669,8 +673,8 @@ func TestAgentGetAgent_ProjectIsolation(t *testing.T) { // Create agents in each project agent1 := &store.Agent{ - ID: "agent-get-caller", - Slug: "agent-get-caller", + ID: tid("agent-get-caller"), + Slug: tid("agent-get-caller"), Name: "Get Caller", ProjectID: project1.ID, Phase: string(state.PhaseRunning), @@ -678,8 +682,8 @@ func TestAgentGetAgent_ProjectIsolation(t *testing.T) { require.NoError(t, s.CreateAgent(ctx, agent1)) agent2SameProject := &store.Agent{ - ID: "agent-get-same", - Slug: "agent-get-same", + ID: tid("agent-get-same"), + Slug: tid("agent-get-same"), Name: "Same Project Agent", ProjectID: project1.ID, Phase: string(state.PhaseRunning), @@ -687,8 +691,8 @@ func TestAgentGetAgent_ProjectIsolation(t *testing.T) { require.NoError(t, s.CreateAgent(ctx, agent2SameProject)) agentOtherProject := &store.Agent{ - ID: "agent-get-other", - Slug: "agent-get-other", + ID: tid("agent-get-other"), + Slug: tid("agent-get-other"), Name: "Other Project Agent", ProjectID: project2.ID, Phase: string(state.PhaseRunning), @@ -787,6 +791,9 @@ func (d *createAgentDispatcher) DispatchAgentStop(_ context.Context, _ *store.Ag func (d *createAgentDispatcher) DispatchAgentRestart(_ context.Context, _ *store.Agent) error { return nil } +func (d *createAgentDispatcher) DispatchAgentResetAuth(_ context.Context, _ *store.Agent) error { + return nil +} func (d *createAgentDispatcher) DispatchAgentDelete(_ context.Context, _ *store.Agent, _, _, _ bool, _ time.Time) error { d.deleteCalled = true return d.deleteErr @@ -840,14 +847,14 @@ func setupCreateAgentServer(t *testing.T, disp AgentDispatcher) (*Server, store. ctx := context.Background() project := &store.Project{ - ID: "project-create", + ID: tid("project-create"), Name: "Create Test Project", Slug: "create-test-project", } require.NoError(t, s.CreateProject(ctx, project)) broker := &store.RuntimeBroker{ - ID: "broker-create", + ID: tid("broker-create"), Name: "Create Test Broker", Slug: "create-test-broker", Status: store.BrokerStatusOnline, @@ -1071,11 +1078,11 @@ func TestCreateAgent_RestartFromProvisioningStatus(t *testing.T) { // Pre-create an agent stuck in "provisioning" status (simulating Bug 1) stuckAgent := &store.Agent{ - ID: "agent-stuck-prov", + ID: tid("agent-stuck-prov"), Slug: "stuck-agent", Name: "stuck-agent", ProjectID: project.ID, - RuntimeBrokerID: "broker-create", + RuntimeBrokerID: tid("broker-create"), Phase: string(state.PhaseProvisioning), } require.NoError(t, s.CreateAgent(ctx, stuckAgent)) @@ -1103,11 +1110,11 @@ func TestCreateAgent_RestartFromPendingStatus(t *testing.T) { // Pre-create an agent in "pending" status pendingAgent := &store.Agent{ - ID: "agent-pending", + ID: tid("agent-pending"), Slug: "pending-agent", Name: "pending-agent", ProjectID: project.ID, - RuntimeBrokerID: "broker-create", + RuntimeBrokerID: tid("broker-create"), Phase: string(state.PhaseCreated), } require.NoError(t, s.CreateAgent(ctx, pendingAgent)) @@ -1128,40 +1135,30 @@ func TestCreateAgent_RecreateFromRunningStatus(t *testing.T) { srv, s, project := setupCreateAgentServer(t, disp) ctx := context.Background() - // Pre-create an agent in "running" status (stale — container may have died) + // Pre-create an agent in "running" status runningAgent := &store.Agent{ - ID: "agent-running-stale", + ID: tid("agent-running-stale"), Slug: "running-agent", Name: "running-agent", ProjectID: project.ID, - RuntimeBrokerID: "broker-create", + RuntimeBrokerID: tid("broker-create"), Phase: string(state.PhaseRunning), } require.NoError(t, s.CreateAgent(ctx, runningAgent)) - // Start with the same name — should delete old agent and create new one + // Creating with the same name should return 409 Conflict rec := doRequest(t, srv, http.MethodPost, "/api/v1/agents", CreateAgentRequest{ Name: "running-agent", ProjectID: project.ID, Task: "new task", }) - require.Equal(t, http.StatusCreated, rec.Code, - "re-creating agent from running status should succeed with 201") + require.Equal(t, http.StatusConflict, rec.Code, + "creating agent with duplicate slug should return 409") - // Old agent should be deleted - _, err := s.GetAgent(ctx, "agent-running-stale") - assert.ErrorIs(t, err, store.ErrNotFound, "old agent should be deleted") - - // Dispatcher should have been asked to delete - assert.True(t, disp.deleteCalled, "dispatcher should have been asked to delete old agent") - - // New agent should exist - var resp CreateAgentResponse - require.NoError(t, json.Unmarshal(rec.Body.Bytes(), &resp)) - require.NotNil(t, resp.Agent) - assert.NotEqual(t, "agent-running-stale", resp.Agent.ID, "new agent should have a different ID") - assert.Equal(t, string(state.PhaseRunning), resp.Agent.Phase) + // Old agent should still exist + _, err := s.GetAgent(ctx, tid("agent-running-stale")) + require.NoError(t, err, "existing agent should not be deleted") } func TestCreateAgent_RecreateFromErrorStatus(t *testing.T) { @@ -1171,28 +1168,28 @@ func TestCreateAgent_RecreateFromErrorStatus(t *testing.T) { // Pre-create an agent in "error" status errorAgent := &store.Agent{ - ID: "agent-errored", + ID: tid("agent-errored"), Slug: "error-agent", Name: "error-agent", ProjectID: project.ID, - RuntimeBrokerID: "broker-create", + RuntimeBrokerID: tid("broker-create"), Phase: string(state.PhaseError), } require.NoError(t, s.CreateAgent(ctx, errorAgent)) - // Start with the same name — should delete and recreate + // Creating with the same name should return 409 Conflict rec := doRequest(t, srv, http.MethodPost, "/api/v1/agents", CreateAgentRequest{ Name: "error-agent", ProjectID: project.ID, Task: "retry after error", }) - require.Equal(t, http.StatusCreated, rec.Code, - "re-creating agent from error status should succeed with 201") + require.Equal(t, http.StatusConflict, rec.Code, + "creating agent with duplicate slug in error state should return 409") - // Old agent should be deleted - _, err := s.GetAgent(ctx, "agent-errored") - assert.ErrorIs(t, err, store.ErrNotFound, "old errored agent should be deleted") + // Old agent should still exist + _, err := s.GetAgent(ctx, tid("agent-errored")) + require.NoError(t, err, "existing agent should not be deleted") } func TestCreateAgent_RecreateFromStoppedStatus(t *testing.T) { @@ -1202,31 +1199,33 @@ func TestCreateAgent_RecreateFromStoppedStatus(t *testing.T) { // Pre-create an agent in "stopped" status stoppedAgent := &store.Agent{ - ID: "agent-stopped", + ID: tid("agent-stopped"), Slug: "stopped-agent", Name: "stopped-agent", ProjectID: project.ID, - RuntimeBrokerID: "broker-create", + RuntimeBrokerID: tid("broker-create"), Phase: string(state.PhaseStopped), } require.NoError(t, s.CreateAgent(ctx, stoppedAgent)) + // Creating with the same name (no Resume) should return 409 Conflict rec := doRequest(t, srv, http.MethodPost, "/api/v1/agents", CreateAgentRequest{ Name: "stopped-agent", ProjectID: project.ID, Task: "restart after stop", }) - require.Equal(t, http.StatusCreated, rec.Code, - "re-creating agent from stopped status should succeed with 201") + require.Equal(t, http.StatusConflict, rec.Code, + "creating agent with duplicate slug in stopped state should return 409") - _, err := s.GetAgent(ctx, "agent-stopped") - assert.ErrorIs(t, err, store.ErrNotFound, "old stopped agent should be deleted") + // Old agent should still exist + _, err := s.GetAgent(ctx, tid("agent-stopped")) + require.NoError(t, err, "existing agent should not be deleted") } // TestCreateAgent_ResumeFromStoppedStatus verifies that sending Resume=true for a // stopped agent restarts it in-place (preserving the agent ID and record) rather -// than deleting and recreating it. +// than returning 409 Conflict. func TestCreateAgent_ResumeFromStoppedStatus(t *testing.T) { disp := &createAgentDispatcher{createPhase: string(state.PhaseRunning)} srv, s, project := setupCreateAgentServer(t, disp) @@ -1234,11 +1233,11 @@ func TestCreateAgent_ResumeFromStoppedStatus(t *testing.T) { // Pre-create an agent in "stopped" status stoppedAgent := &store.Agent{ - ID: "agent-resume-stopped", + ID: tid("agent-resume-stopped"), Slug: "resume-stopped-agent", Name: "resume-stopped-agent", ProjectID: project.ID, - RuntimeBrokerID: "broker-create", + RuntimeBrokerID: tid("broker-create"), Phase: string(state.PhaseStopped), } require.NoError(t, s.CreateAgent(ctx, stoppedAgent)) @@ -1254,7 +1253,7 @@ func TestCreateAgent_ResumeFromStoppedStatus(t *testing.T) { "resuming a stopped agent should return 200 (existing agent reused)") // The original agent should still exist in the store - agent, err := s.GetAgent(ctx, "agent-resume-stopped") + agent, err := s.GetAgent(ctx, tid("agent-resume-stopped")) require.NoError(t, err, "original agent should still exist after resume") assert.Equal(t, string(state.PhaseRunning), agent.Phase, "resumed agent should be in running phase") @@ -1264,7 +1263,7 @@ func TestCreateAgent_ResumeFromStoppedStatus(t *testing.T) { } // TestCreateAgent_StartFromStoppedStatus_NoResume verifies that without Resume=true, -// a stopped agent is still deleted and recreated (the existing behavior). +// a stopped agent blocks creation with 409 Conflict. func TestCreateAgent_StartFromStoppedStatus_NoResume(t *testing.T) { disp := &createAgentDispatcher{createPhase: string(state.PhaseRunning)} srv, s, project := setupCreateAgentServer(t, disp) @@ -1272,11 +1271,11 @@ func TestCreateAgent_StartFromStoppedStatus_NoResume(t *testing.T) { // Pre-create an agent in "stopped" status stoppedAgent := &store.Agent{ - ID: "agent-start-stopped", + ID: tid("agent-start-stopped"), Slug: "start-stopped-agent", Name: "start-stopped-agent", ProjectID: project.ID, - RuntimeBrokerID: "broker-create", + RuntimeBrokerID: tid("broker-create"), Phase: string(state.PhaseStopped), } require.NoError(t, s.CreateAgent(ctx, stoppedAgent)) @@ -1284,19 +1283,130 @@ func TestCreateAgent_StartFromStoppedStatus_NoResume(t *testing.T) { rec := doRequest(t, srv, http.MethodPost, "/api/v1/agents", CreateAgentRequest{ Name: "start-stopped-agent", ProjectID: project.ID, - // Resume is NOT set — this is a "start" not a "resume" - Task: "restart after stop", + Task: "restart after stop", + }) + + require.Equal(t, http.StatusConflict, rec.Code, + "creating agent with duplicate slug (no Resume) should return 409") + + // The old agent should still exist + _, err := s.GetAgent(ctx, tid("agent-start-stopped")) + require.NoError(t, err, "existing agent should not be deleted") + + // DispatchAgentDelete should NOT have been called + assert.False(t, disp.deleteCalled, "DispatchAgentDelete should not be called for 409 conflict") +} + +func TestCreateAgent_DuplicateSlugRunning_Returns409(t *testing.T) { + disp := &createAgentDispatcher{createPhase: string(state.PhaseRunning)} + srv, s, project := setupCreateAgentServer(t, disp) + ctx := context.Background() + + existing := &store.Agent{ + ID: tid("dup-running"), + Slug: "my-agent", + Name: "my-agent", + ProjectID: project.ID, + RuntimeBrokerID: tid("broker-create"), + Phase: string(state.PhaseRunning), + } + require.NoError(t, s.CreateAgent(ctx, existing)) + + rec := doRequest(t, srv, http.MethodPost, "/api/v1/agents", CreateAgentRequest{ + Name: "my-agent", + ProjectID: project.ID, + Task: "duplicate task", + }) + + require.Equal(t, http.StatusConflict, rec.Code) + assert.Contains(t, rec.Body.String(), "already exists in this project") + + // Original agent untouched + got, err := s.GetAgent(ctx, tid("dup-running")) + require.NoError(t, err) + assert.Equal(t, string(state.PhaseRunning), got.Phase) +} + +func TestCreateAgent_DuplicateSlugStopped_Returns409(t *testing.T) { + disp := &createAgentDispatcher{createPhase: string(state.PhaseRunning)} + srv, s, project := setupCreateAgentServer(t, disp) + ctx := context.Background() + + existing := &store.Agent{ + ID: tid("dup-stopped"), + Slug: "my-stopped-agent", + Name: "my-stopped-agent", + ProjectID: project.ID, + RuntimeBrokerID: tid("broker-create"), + Phase: string(state.PhaseStopped), + } + require.NoError(t, s.CreateAgent(ctx, existing)) + + rec := doRequest(t, srv, http.MethodPost, "/api/v1/agents", CreateAgentRequest{ + Name: "my-stopped-agent", + ProjectID: project.ID, + Task: "duplicate task", }) - require.Equal(t, http.StatusCreated, rec.Code, - "starting (not resuming) a stopped agent should recreate with 201") + require.Equal(t, http.StatusConflict, rec.Code) + assert.Contains(t, rec.Body.String(), "already exists in this project") +} - // The old agent should be deleted - _, err := s.GetAgent(ctx, "agent-start-stopped") - assert.ErrorIs(t, err, store.ErrNotFound, "old stopped agent should be deleted when not resuming") +func TestCreateAgent_DuplicateSlugError_Returns409(t *testing.T) { + disp := &createAgentDispatcher{createPhase: string(state.PhaseRunning)} + srv, s, project := setupCreateAgentServer(t, disp) + ctx := context.Background() - // DispatchAgentDelete should have been called - assert.True(t, disp.deleteCalled, "DispatchAgentDelete should be called when not resuming") + existing := &store.Agent{ + ID: tid("dup-error"), + Slug: "my-error-agent", + Name: "my-error-agent", + ProjectID: project.ID, + RuntimeBrokerID: tid("broker-create"), + Phase: string(state.PhaseError), + } + require.NoError(t, s.CreateAgent(ctx, existing)) + + rec := doRequest(t, srv, http.MethodPost, "/api/v1/agents", CreateAgentRequest{ + Name: "my-error-agent", + ProjectID: project.ID, + Task: "duplicate task", + }) + + require.Equal(t, http.StatusConflict, rec.Code) + assert.Contains(t, rec.Body.String(), "already exists in this project") +} + +func TestCreateAgent_DuplicateSlugStopped_ResumeAllowed(t *testing.T) { + disp := &createAgentDispatcher{createPhase: string(state.PhaseRunning)} + srv, s, project := setupCreateAgentServer(t, disp) + ctx := context.Background() + + existing := &store.Agent{ + ID: tid("dup-resume"), + Slug: "resumable-agent", + Name: "resumable-agent", + ProjectID: project.ID, + RuntimeBrokerID: tid("broker-create"), + Phase: string(state.PhaseStopped), + } + require.NoError(t, s.CreateAgent(ctx, existing)) + + rec := doRequest(t, srv, http.MethodPost, "/api/v1/agents", CreateAgentRequest{ + Name: "resumable-agent", + ProjectID: project.ID, + Resume: true, + Task: "continue working", + }) + + require.Equal(t, http.StatusOK, rec.Code, + "Resume=true for stopped agent should return 200") + + got, err := s.GetAgent(ctx, tid("dup-resume")) + require.NoError(t, err) + assert.Equal(t, string(state.PhaseRunning), got.Phase) + assert.True(t, disp.startCalled, "DispatchAgentStart should be called") + assert.False(t, disp.deleteCalled, "agent should not be deleted on resume") } // TestAgentCreate_LocalTemplateWithLocalBroker tests that agent creation succeeds @@ -1308,7 +1418,7 @@ func TestAgentCreate_LocalTemplateWithLocalBroker(t *testing.T) { // Create a runtime broker broker := &store.RuntimeBroker{ - ID: "broker_local_tpl", + ID: tid("broker_local_tpl"), Slug: "local-tpl-broker", Name: "Local Template Broker", Status: store.BrokerStatusOnline, @@ -1317,7 +1427,7 @@ func TestAgentCreate_LocalTemplateWithLocalBroker(t *testing.T) { // Create a project with default runtime broker project := &store.Project{ - ID: "project_local_tpl", + ID: tid("project_local_tpl"), Slug: "local-tpl-project", Name: "Local Template Project", GitRemote: "github.com/test/local-tpl", @@ -1370,7 +1480,7 @@ func TestAgentCreate_LocalTemplateWithRemoteBroker(t *testing.T) { // Create a runtime broker broker := &store.RuntimeBroker{ - ID: "broker_remote_tpl", + ID: tid("broker_remote_tpl"), Slug: "remote-tpl-broker", Name: "Remote Template Broker", Status: store.BrokerStatusOnline, @@ -1379,7 +1489,7 @@ func TestAgentCreate_LocalTemplateWithRemoteBroker(t *testing.T) { // Create a project project := &store.Project{ - ID: "project_remote_tpl", + ID: tid("project_remote_tpl"), Slug: "remote-tpl-project", Name: "Remote Template Project", GitRemote: "github.com/test/remote-tpl", @@ -1421,7 +1531,7 @@ func TestAgentCreate_LocalTemplateNoBroker(t *testing.T) { // Create a project WITHOUT a default runtime broker project := &store.Project{ - ID: "project_no_broker_tpl", + ID: tid("project_no_broker_tpl"), Slug: "no-broker-tpl-project", Name: "No Broker Template Project", GitRemote: "github.com/test/no-broker-tpl", @@ -1480,14 +1590,14 @@ func TestListAgents_ServerTimeIncluded(t *testing.T) { // Create a project and agent project := &store.Project{ - ID: "project-servertime", + ID: tid("project-servertime"), Name: "ServerTime Project", Slug: "servertime-project", } require.NoError(t, s.CreateProject(ctx, project)) agent := &store.Agent{ - ID: "agent-servertime", + ID: tid("agent-servertime"), Slug: "agent-servertime-slug", Name: "ServerTime Agent", ProjectID: project.ID, @@ -1520,7 +1630,7 @@ func TestListProjectAgents_ServerTimeIncluded(t *testing.T) { // Create a project project := &store.Project{ - ID: "project-servertime-g", + ID: tid("project-servertime-g"), Name: "ServerTime Project G", Slug: "servertime-project-g", } @@ -1609,7 +1719,7 @@ func TestCreateAgent_GitAnchoredProjectPopulatesGitClone(t *testing.T) { // Create a project with GitRemote and labels gitProject := &store.Project{ - ID: "project-git", + ID: tid("project-git"), Name: "Git Project", Slug: "git-project", GitRemote: "github.com/example/myrepo", @@ -1617,14 +1727,14 @@ func TestCreateAgent_GitAnchoredProjectPopulatesGitClone(t *testing.T) { "scion.dev/clone-url": "https://github.com/example/myrepo.git", "scion.dev/default-branch": "develop", }, - DefaultRuntimeBrokerID: "broker-create", + DefaultRuntimeBrokerID: tid("broker-create"), } require.NoError(t, s.CreateProject(ctx, gitProject)) // Add project provider provider := &store.ProjectProvider{ ProjectID: gitProject.ID, - BrokerID: "broker-create", + BrokerID: tid("broker-create"), BrokerName: "Create Test Broker", Status: store.BrokerStatusOnline, } @@ -1683,7 +1793,7 @@ func TestCreateProjectAgent_GitAnchoredProjectPopulatesGitClone(t *testing.T) { // Create a project with GitRemote and labels gitProject := &store.Project{ - ID: "project-git-scoped", + ID: tid("project-git-scoped"), Name: "Git Project Scoped", Slug: "git-project-scoped", GitRemote: "github.com/example/myrepo", @@ -1691,14 +1801,14 @@ func TestCreateProjectAgent_GitAnchoredProjectPopulatesGitClone(t *testing.T) { "scion.dev/clone-url": "https://github.com/example/myrepo.git", "scion.dev/default-branch": "develop", }, - DefaultRuntimeBrokerID: "broker-create", + DefaultRuntimeBrokerID: tid("broker-create"), } require.NoError(t, s.CreateProject(ctx, gitProject)) // Add project provider provider := &store.ProjectProvider{ ProjectID: gitProject.ID, - BrokerID: "broker-create", + BrokerID: tid("broker-create"), BrokerName: "Create Test Broker", Status: store.BrokerStatusOnline, } @@ -1760,20 +1870,20 @@ func TestCreateAgent_GitProjectCloneURLFallback(t *testing.T) { // Create a project with GitRemote but WITHOUT the scion.dev/clone-url label. // The URL should be constructed from gitRemote as "https://.git". gitProject := &store.Project{ - ID: "project-git-fallback-url", + ID: tid("project-git-fallback-url"), Name: "Git Project Fallback URL", Slug: "git-project-fallback-url", GitRemote: "github.com/example/fallback-repo", Labels: map[string]string{ "scion.dev/default-branch": "develop", }, - DefaultRuntimeBrokerID: "broker-create", + DefaultRuntimeBrokerID: tid("broker-create"), } require.NoError(t, s.CreateProject(ctx, gitProject)) provider := &store.ProjectProvider{ ProjectID: gitProject.ID, - BrokerID: "broker-create", + BrokerID: tid("broker-create"), BrokerName: "Create Test Broker", Status: store.BrokerStatusOnline, } @@ -1811,7 +1921,7 @@ func TestCreateAgent_GitProjectSchemelessCloneURL(t *testing.T) { // Create a project where clone-url label is set but missing https:// scheme // (as can happen when the web UI stores raw user input). gitProject := &store.Project{ - ID: "project-git-schemeless", + ID: tid("project-git-schemeless"), Name: "Git Project Schemeless", Slug: "git-project-schemeless", GitRemote: "github.com/example/schemeless-repo", @@ -1819,13 +1929,13 @@ func TestCreateAgent_GitProjectSchemelessCloneURL(t *testing.T) { "scion.dev/clone-url": "github.com/example/schemeless-repo", "scion.dev/default-branch": "main", }, - DefaultRuntimeBrokerID: "broker-create", + DefaultRuntimeBrokerID: tid("broker-create"), } require.NoError(t, s.CreateProject(ctx, gitProject)) provider := &store.ProjectProvider{ ProjectID: gitProject.ID, - BrokerID: "broker-create", + BrokerID: tid("broker-create"), BrokerName: "Create Test Broker", Status: store.BrokerStatusOnline, } @@ -1862,20 +1972,20 @@ func TestCreateAgent_GitProjectDefaultBranchFallback(t *testing.T) { // Create a project with GitRemote and clone-url label but WITHOUT default-branch. // The branch should default to "main". gitProject := &store.Project{ - ID: "project-git-fallback-branch", + ID: tid("project-git-fallback-branch"), Name: "Git Project Fallback Branch", Slug: "git-project-fallback-branch", GitRemote: "github.com/example/branch-repo", Labels: map[string]string{ "scion.dev/clone-url": "https://github.com/example/branch-repo.git", }, - DefaultRuntimeBrokerID: "broker-create", + DefaultRuntimeBrokerID: tid("broker-create"), } require.NoError(t, s.CreateProject(ctx, gitProject)) provider := &store.ProjectProvider{ ProjectID: gitProject.ID, - BrokerID: "broker-create", + BrokerID: tid("broker-create"), BrokerName: "Create Test Broker", Status: store.BrokerStatusOnline, } @@ -2067,15 +2177,15 @@ func TestListAgents_HarnessConfigEnriched(t *testing.T) { ctx := context.Background() project := &store.Project{ - ID: "project-harness-enrich", + ID: tid("project-harness-enrich"), Name: "Harness Enrichment Project", Slug: "harness-enrich-project", } require.NoError(t, s.CreateProject(ctx, project)) agent := &store.Agent{ - ID: "agent-harness-enrich", - Slug: "agent-harness-enrich", + ID: tid("agent-harness-enrich"), + Slug: tid("agent-harness-enrich"), Name: "Harness Agent", ProjectID: project.ID, Phase: string(state.PhaseRunning), @@ -2122,15 +2232,15 @@ func TestGetAgent_HarnessConfigEnriched(t *testing.T) { ctx := context.Background() project := &store.Project{ - ID: "project-harness-get", + ID: tid("project-harness-get"), Name: "Harness Get Project", Slug: "harness-get-project", } require.NoError(t, s.CreateProject(ctx, project)) agent := &store.Agent{ - ID: "agent-harness-get", - Slug: "agent-harness-get", + ID: tid("agent-harness-get"), + Slug: tid("agent-harness-get"), Name: "Harness Get Agent", ProjectID: project.ID, Phase: string(state.PhaseRunning), @@ -2233,21 +2343,22 @@ func TestHeartbeat_BackfillsProfile(t *testing.T) { ctx := context.Background() project := &store.Project{ - ID: "project-profile-hb", + ID: tid("project-profile-hb"), Name: "Profile HB Project", Slug: "profile-hb-project", } require.NoError(t, s.CreateProject(ctx, project)) broker := &store.RuntimeBroker{ - ID: "broker-profile-hb", + ID: tid("broker-profile-hb"), Name: "Profile HB Broker", + Slug: "profile-hb-broker", Status: store.BrokerStatusOnline, } require.NoError(t, s.CreateRuntimeBroker(ctx, broker)) agent := &store.Agent{ - ID: "agent-profile-hb", + ID: tid("agent-profile-hb"), Slug: "profile-hb-agent", Name: "Profile HB Agent", ProjectID: project.ID, @@ -2299,7 +2410,7 @@ func TestCreateAgent_HarnessNotTemplateUUID(t *testing.T) { // the template to be resolved locally by the broker. require.NoError(t, s.AddProjectProvider(ctx, &store.ProjectProvider{ ProjectID: project.ID, - BrokerID: "broker-create", + BrokerID: tid("broker-create"), BrokerName: "Create Test Broker", LocalPath: "/some/local/path", Status: "online", @@ -2337,11 +2448,11 @@ func TestCreateProjectAgent_RecreateFromRunningStatus(t *testing.T) { ctx := context.Background() runningAgent := &store.Agent{ - ID: "project-agent-running", + ID: tid("project-agent-running"), Slug: "running-project-agent", Name: "running-project-agent", ProjectID: project.ID, - RuntimeBrokerID: "broker-create", + RuntimeBrokerID: tid("broker-create"), Phase: string(state.PhaseRunning), } require.NoError(t, s.CreateAgent(ctx, runningAgent)) @@ -2353,19 +2464,11 @@ func TestCreateProjectAgent_RecreateFromRunningStatus(t *testing.T) { Task: "new task", }) - require.Equal(t, http.StatusCreated, rec.Code, - "re-creating a running project agent should succeed with 201") + require.Equal(t, http.StatusConflict, rec.Code, + "creating project agent with duplicate slug should return 409") - _, err := s.GetAgent(ctx, "project-agent-running") - assert.ErrorIs(t, err, store.ErrNotFound, "old running agent should be deleted") - - assert.True(t, disp.deleteCalled, "dispatcher should have been asked to delete old agent") - - var resp CreateAgentResponse - require.NoError(t, json.Unmarshal(rec.Body.Bytes(), &resp)) - require.NotNil(t, resp.Agent) - assert.NotEqual(t, "project-agent-running", resp.Agent.ID) - assert.Equal(t, string(state.PhaseRunning), resp.Agent.Phase) + _, err := s.GetAgent(ctx, tid("project-agent-running")) + require.NoError(t, err, "existing agent should not be deleted") } func TestCreateProjectAgent_RecreateFromStoppedStatus(t *testing.T) { @@ -2374,11 +2477,11 @@ func TestCreateProjectAgent_RecreateFromStoppedStatus(t *testing.T) { ctx := context.Background() stoppedAgent := &store.Agent{ - ID: "project-agent-stopped", + ID: tid("project-agent-stopped"), Slug: "stopped-project-agent", Name: "stopped-project-agent", ProjectID: project.ID, - RuntimeBrokerID: "broker-create", + RuntimeBrokerID: tid("broker-create"), Phase: string(state.PhaseStopped), } require.NoError(t, s.CreateAgent(ctx, stoppedAgent)) @@ -2390,11 +2493,11 @@ func TestCreateProjectAgent_RecreateFromStoppedStatus(t *testing.T) { Task: "restart after stop", }) - require.Equal(t, http.StatusCreated, rec.Code, - "re-creating a stopped project agent should succeed with 201") + require.Equal(t, http.StatusConflict, rec.Code, + "creating project agent with duplicate slug in stopped state should return 409") - _, err := s.GetAgent(ctx, "project-agent-stopped") - assert.ErrorIs(t, err, store.ErrNotFound, "old stopped agent should be deleted") + _, err := s.GetAgent(ctx, tid("project-agent-stopped")) + require.NoError(t, err, "existing agent should not be deleted") } // TestCreateProjectAgent_ResumeFromStoppedStatus verifies that sending Resume=true @@ -2405,11 +2508,11 @@ func TestCreateProjectAgent_ResumeFromStoppedStatus(t *testing.T) { ctx := context.Background() stoppedAgent := &store.Agent{ - ID: "project-agent-resume-stopped", + ID: tid("project-agent-resume-stopped"), Slug: "resume-stopped-project-agent", Name: "resume-stopped-project-agent", ProjectID: project.ID, - RuntimeBrokerID: "broker-create", + RuntimeBrokerID: tid("broker-create"), Phase: string(state.PhaseStopped), } require.NoError(t, s.CreateAgent(ctx, stoppedAgent)) @@ -2425,7 +2528,7 @@ func TestCreateProjectAgent_ResumeFromStoppedStatus(t *testing.T) { require.Equal(t, http.StatusOK, rec.Code, "resuming a stopped project agent should return 200 (existing agent reused)") - agent, err := s.GetAgent(ctx, "project-agent-resume-stopped") + agent, err := s.GetAgent(ctx, tid("project-agent-resume-stopped")) require.NoError(t, err, "original project agent should still exist after resume") assert.Equal(t, string(state.PhaseRunning), agent.Phase, "resumed agent should be in running phase") @@ -2439,11 +2542,11 @@ func TestCreateProjectAgent_RecreateFromErrorStatus(t *testing.T) { ctx := context.Background() errorAgent := &store.Agent{ - ID: "project-agent-errored", + ID: tid("project-agent-errored"), Slug: "errored-project-agent", Name: "errored-project-agent", ProjectID: project.ID, - RuntimeBrokerID: "broker-create", + RuntimeBrokerID: tid("broker-create"), Phase: string(state.PhaseError), } require.NoError(t, s.CreateAgent(ctx, errorAgent)) @@ -2455,11 +2558,11 @@ func TestCreateProjectAgent_RecreateFromErrorStatus(t *testing.T) { Task: "retry after error", }) - require.Equal(t, http.StatusCreated, rec.Code, - "re-creating an errored project agent should succeed with 201") + require.Equal(t, http.StatusConflict, rec.Code, + "creating project agent with duplicate slug in error state should return 409") - _, err := s.GetAgent(ctx, "project-agent-errored") - assert.ErrorIs(t, err, store.ErrNotFound, "old errored agent should be deleted") + _, err := s.GetAgent(ctx, tid("project-agent-errored")) + require.NoError(t, err, "existing agent should not be deleted") } func TestCreateProjectAgent_RestartFromProvisioningStatus(t *testing.T) { @@ -2468,11 +2571,11 @@ func TestCreateProjectAgent_RestartFromProvisioningStatus(t *testing.T) { ctx := context.Background() provAgent := &store.Agent{ - ID: "project-agent-prov", + ID: tid("project-agent-prov"), Slug: "prov-project-agent", Name: "prov-project-agent", ProjectID: project.ID, - RuntimeBrokerID: "broker-create", + RuntimeBrokerID: tid("broker-create"), Phase: string(state.PhaseProvisioning), } require.NoError(t, s.CreateAgent(ctx, provAgent)) @@ -2499,11 +2602,11 @@ func TestCreateProjectAgent_RestartFromPendingStatus(t *testing.T) { ctx := context.Background() pendingAgent := &store.Agent{ - ID: "project-agent-pending", + ID: tid("project-agent-pending"), Slug: "pending-project-agent", Name: "pending-project-agent", ProjectID: project.ID, - RuntimeBrokerID: "broker-create", + RuntimeBrokerID: tid("broker-create"), Phase: string(state.PhaseCreated), } require.NoError(t, s.CreateAgent(ctx, pendingAgent)) @@ -2529,11 +2632,11 @@ func TestCreateProjectAgent_ConfigUpdateOnRestart(t *testing.T) { ctx := context.Background() existingAgent := &store.Agent{ - ID: "project-agent-config", + ID: tid("project-agent-config"), Slug: "config-project-agent", Name: "config-project-agent", ProjectID: project.ID, - RuntimeBrokerID: "broker-create", + RuntimeBrokerID: tid("broker-create"), Phase: string(state.PhaseCreated), AppliedConfig: &store.AgentAppliedConfig{ Task: "old task", @@ -2573,7 +2676,7 @@ func TestCreateProjectAgent_BrokerIDRecovery(t *testing.T) { // Pre-create agent with empty RuntimeBrokerID (simulates agent created // before a broker was registered). existingAgent := &store.Agent{ - ID: "project-agent-no-broker", + ID: tid("project-agent-no-broker"), Slug: "no-broker-project-agent", Name: "no-broker-project-agent", ProjectID: project.ID, @@ -2598,7 +2701,7 @@ func TestCreateProjectAgent_BrokerIDRecovery(t *testing.T) { persisted, err := s.GetAgent(ctx, resp.Agent.ID) require.NoError(t, err) - assert.Equal(t, "broker-create", persisted.RuntimeBrokerID, + assert.Equal(t, tid("broker-create"), persisted.RuntimeBrokerID, "RuntimeBrokerID should be recovered from resolved broker") } @@ -2608,7 +2711,7 @@ func TestCreateAgent_BrokerIDRecovery(t *testing.T) { ctx := context.Background() existingAgent := &store.Agent{ - ID: "agent-no-broker", + ID: tid("agent-no-broker"), Slug: "no-broker-agent", Name: "no-broker-agent", ProjectID: project.ID, @@ -2632,11 +2735,11 @@ func TestCreateAgent_BrokerIDRecovery(t *testing.T) { persisted, err := s.GetAgent(ctx, resp.Agent.ID) require.NoError(t, err) - assert.Equal(t, "broker-create", persisted.RuntimeBrokerID, + assert.Equal(t, tid("broker-create"), persisted.RuntimeBrokerID, "RuntimeBrokerID should be recovered from resolved broker") } -func TestCreateAgent_CleanupModeStrictFailsOnBrokerDeleteError(t *testing.T) { +func TestCreateAgent_CleanupModeStrictReturns409ForDuplicate(t *testing.T) { disp := &createAgentDispatcher{ createPhase: string(state.PhaseRunning), deleteErr: fmt.Errorf("broker delete failed"), @@ -2645,11 +2748,11 @@ func TestCreateAgent_CleanupModeStrictFailsOnBrokerDeleteError(t *testing.T) { ctx := context.Background() existingAgent := &store.Agent{ - ID: "agent-stale-strict", + ID: tid("agent-stale-strict"), Slug: "stale-strict-agent", Name: "stale-strict-agent", ProjectID: project.ID, - RuntimeBrokerID: "broker-create", + RuntimeBrokerID: tid("broker-create"), Phase: string(state.PhaseRunning), } require.NoError(t, s.CreateAgent(ctx, existingAgent)) @@ -2659,15 +2762,15 @@ func TestCreateAgent_CleanupModeStrictFailsOnBrokerDeleteError(t *testing.T) { ProjectID: project.ID, CleanupMode: "strict", }) - require.Equal(t, http.StatusBadGateway, rec.Code) - assert.True(t, disp.deleteCalled, "expected broker delete attempt in strict mode") + require.Equal(t, http.StatusConflict, rec.Code, + "duplicate slug should return 409 regardless of cleanupMode") persisted, err := s.GetAgent(ctx, existingAgent.ID) require.NoError(t, err) - assert.Equal(t, existingAgent.ID, persisted.ID, "strict mode should keep existing DB record") + assert.Equal(t, existingAgent.ID, persisted.ID, "existing agent should be preserved") } -func TestCreateAgent_CleanupModeForceContinuesOnBrokerDeleteError(t *testing.T) { +func TestCreateAgent_CleanupModeForceReturns409ForDuplicate(t *testing.T) { disp := &createAgentDispatcher{ createPhase: string(state.PhaseRunning), deleteErr: fmt.Errorf("broker delete failed"), @@ -2676,11 +2779,11 @@ func TestCreateAgent_CleanupModeForceContinuesOnBrokerDeleteError(t *testing.T) ctx := context.Background() existingAgent := &store.Agent{ - ID: "agent-stale-force", + ID: tid("agent-stale-force"), Slug: "stale-force-agent", Name: "stale-force-agent", ProjectID: project.ID, - RuntimeBrokerID: "broker-create", + RuntimeBrokerID: tid("broker-create"), Phase: string(state.PhaseRunning), } require.NoError(t, s.CreateAgent(ctx, existingAgent)) @@ -2690,11 +2793,11 @@ func TestCreateAgent_CleanupModeForceContinuesOnBrokerDeleteError(t *testing.T) ProjectID: project.ID, CleanupMode: "force", }) - require.Equal(t, http.StatusCreated, rec.Code) - assert.True(t, disp.deleteCalled, "expected broker delete attempt in force mode") + require.Equal(t, http.StatusConflict, rec.Code, + "duplicate slug should return 409 regardless of cleanupMode") _, err := s.GetAgent(ctx, existingAgent.ID) - assert.ErrorIs(t, err, store.ErrNotFound, "force mode should replace stale DB record") + require.NoError(t, err, "existing agent should be preserved even with force cleanup mode") } func TestCreateAgent_InvalidCleanupMode(t *testing.T) { @@ -2717,14 +2820,14 @@ func TestCreateAgent_NotifyCreatesSubscription(t *testing.T) { // Create project and broker infrastructure project := &store.Project{ - ID: "project-notify", + ID: tid("project-notify"), Name: "Notify Project", Slug: "notify-project", } require.NoError(t, s.CreateProject(ctx, project)) broker := &store.RuntimeBroker{ - ID: "broker-notify", + ID: tid("broker-notify"), Name: "Notify Broker", Slug: "notify-broker", Status: store.BrokerStatusOnline, @@ -2739,9 +2842,10 @@ func TestCreateAgent_NotifyCreatesSubscription(t *testing.T) { project.DefaultRuntimeBrokerID = broker.ID require.NoError(t, s.UpdateProject(ctx, project)) - // Create the calling agent (the one that will subscribe to notifications) + // Create the calling agent (the one that will subscribe to notifications). + // No matching user row: the agent ID stands on its own as created_by/owner_id. callingAgent := &store.Agent{ - ID: "agent-lead", + ID: tid("agent-lead"), Slug: "lead-agent", Name: "Lead Agent", ProjectID: project.ID, @@ -2855,14 +2959,14 @@ func TestCreateAgent_NotifySubscriptionCascadeOnDelete(t *testing.T) { ctx := context.Background() project := &store.Project{ - ID: "project-cascade", + ID: tid("project-cascade"), Name: "Cascade Project", Slug: "cascade-project", } require.NoError(t, s.CreateProject(ctx, project)) broker := &store.RuntimeBroker{ - ID: "broker-cascade", + ID: tid("broker-cascade"), Name: "Cascade Broker", Slug: "cascade-broker", Status: store.BrokerStatusOnline, @@ -2878,7 +2982,7 @@ func TestCreateAgent_NotifySubscriptionCascadeOnDelete(t *testing.T) { require.NoError(t, s.UpdateProject(ctx, project)) callingAgent := &store.Agent{ - ID: "agent-cascade-lead", + ID: tid("agent-cascade-lead"), Slug: "cascade-lead", Name: "Cascade Lead", ProjectID: project.ID, @@ -2935,17 +3039,17 @@ func TestBrokerHeartbeat_PublishesActivitySSE(t *testing.T) { srv.SetEventPublisher(pub) // Create project, broker, and agent - project := &store.Project{ID: "project-hb-sse", Name: "HB SSE Project", Slug: "hb-sse-project"} + project := &store.Project{ID: tid("project-hb-sse"), Name: "HB SSE Project", Slug: "hb-sse-project"} require.NoError(t, s.CreateProject(ctx, project)) broker := &store.RuntimeBroker{ - ID: "broker-hb-sse", Name: "HB SSE Broker", Slug: "hb-sse-broker", + ID: tid("broker-hb-sse"), Name: "HB SSE Broker", Slug: "hb-sse-broker", Status: store.BrokerStatusOnline, } require.NoError(t, s.CreateRuntimeBroker(ctx, broker)) agent := &store.Agent{ - ID: "agent-hb-sse", Slug: "agent-hb-slug", Name: "HB SSE Agent", + ID: tid("agent-hb-sse"), Slug: "agent-hb-slug", Name: "HB SSE Agent", ProjectID: project.ID, RuntimeBrokerID: broker.ID, Phase: string(state.PhaseRunning), } @@ -2995,17 +3099,17 @@ func TestBrokerHeartbeat_RepeatedActivityDoesNotRefreshLastActivityEvent(t *test ctx := context.Background() // Create project, broker, and agent - project := &store.Project{ID: "project-stall-hb", Name: "Stall HB Project", Slug: "stall-hb-project"} + project := &store.Project{ID: tid("project-stall-hb"), Name: "Stall HB Project", Slug: "stall-hb-project"} require.NoError(t, s.CreateProject(ctx, project)) broker := &store.RuntimeBroker{ - ID: "broker-stall-hb", Name: "Stall HB Broker", Slug: "stall-hb-broker", + ID: tid("broker-stall-hb"), Name: "Stall HB Broker", Slug: "stall-hb-broker", Status: store.BrokerStatusOnline, } require.NoError(t, s.CreateRuntimeBroker(ctx, broker)) agent := &store.Agent{ - ID: "agent-stall-hb", Slug: "stall-hb-slug", Name: "Stall HB Agent", + ID: tid("agent-stall-hb"), Slug: "stall-hb-slug", Name: "Stall HB Agent", ProjectID: project.ID, RuntimeBrokerID: broker.ID, Phase: string(state.PhaseRunning), } @@ -3035,7 +3139,7 @@ func TestBrokerHeartbeat_RepeatedActivityDoesNotRefreshLastActivityEvent(t *test // Backdate last_activity_event to simulate time passing pastTime := time.Now().Add(-10 * time.Minute) - db := s.(*sqlite.SQLiteStore).DB() + db := s.(*entadapter.CompositeStore).DB() _, err = db.ExecContext(ctx, "UPDATE agents SET last_activity_event = ? WHERE id = ?", pastTime, agent.ID) require.NoError(t, err) @@ -3091,17 +3195,17 @@ func TestBrokerHeartbeat_StalledAgentNotOverwrittenBySameActivity(t *testing.T) ctx := context.Background() // Create project, broker, and agent - project := &store.Project{ID: "project-stall-keep", Name: "Stall Keep Project", Slug: "stall-keep-project"} + project := &store.Project{ID: tid("project-stall-keep"), Name: "Stall Keep Project", Slug: "stall-keep-project"} require.NoError(t, s.CreateProject(ctx, project)) broker := &store.RuntimeBroker{ - ID: "broker-stall-keep", Name: "Stall Keep Broker", Slug: "stall-keep-broker", + ID: tid("broker-stall-keep"), Name: "Stall Keep Broker", Slug: "stall-keep-broker", Status: store.BrokerStatusOnline, } require.NoError(t, s.CreateRuntimeBroker(ctx, broker)) agent := &store.Agent{ - ID: "agent-stall-keep", Slug: "stall-keep-slug", Name: "Stall Keep Agent", + ID: tid("agent-stall-keep"), Slug: "stall-keep-slug", Name: "Stall Keep Agent", ProjectID: project.ID, RuntimeBrokerID: broker.ID, Phase: string(state.PhaseRunning), } @@ -3114,7 +3218,7 @@ func TestBrokerHeartbeat_StalledAgentNotOverwrittenBySameActivity(t *testing.T) })) // Simulate stalled detection: mark agent stalled with stalled_from_activity = thinking - db := s.(*sqlite.SQLiteStore).DB() + db := s.(*entadapter.CompositeStore).DB() staleActivity := time.Now().Add(-10 * time.Minute) _, err := db.ExecContext(ctx, "UPDATE agents SET activity = 'stalled', stalled_from_activity = 'thinking', last_activity_event = ?, last_seen = ? WHERE id = ?", @@ -3149,17 +3253,17 @@ func TestBrokerHeartbeat_StalledAgentRecoveredByNewActivity(t *testing.T) { ctx := context.Background() // Create project, broker, and agent - project := &store.Project{ID: "project-stall-recover", Name: "Stall Recover Project", Slug: "stall-recover-project"} + project := &store.Project{ID: tid("project-stall-recover"), Name: "Stall Recover Project", Slug: "stall-recover-project"} require.NoError(t, s.CreateProject(ctx, project)) broker := &store.RuntimeBroker{ - ID: "broker-stall-recover", Name: "Stall Recover Broker", Slug: "stall-recover-broker", + ID: tid("broker-stall-recover"), Name: "Stall Recover Broker", Slug: "stall-recover-broker", Status: store.BrokerStatusOnline, } require.NoError(t, s.CreateRuntimeBroker(ctx, broker)) agent := &store.Agent{ - ID: "agent-stall-recover", Slug: "stall-recover-slug", Name: "Stall Recover Agent", + ID: tid("agent-stall-recover"), Slug: "stall-recover-slug", Name: "Stall Recover Agent", ProjectID: project.ID, RuntimeBrokerID: broker.ID, Phase: string(state.PhaseRunning), } @@ -3172,7 +3276,7 @@ func TestBrokerHeartbeat_StalledAgentRecoveredByNewActivity(t *testing.T) { })) // Simulate stalled detection: mark agent stalled with stalled_from_activity = thinking - db := s.(*sqlite.SQLiteStore).DB() + db := s.(*entadapter.CompositeStore).DB() staleActivity := time.Now().Add(-10 * time.Minute) _, err := db.ExecContext(ctx, "UPDATE agents SET activity = 'stalled', stalled_from_activity = 'thinking', last_activity_event = ?, last_seen = ? WHERE id = ?", @@ -3206,17 +3310,17 @@ func TestBrokerHeartbeat_StalledWorkingAgentNotOverwrittenBySameActivity(t *test srv, s := testServer(t) ctx := context.Background() - project := &store.Project{ID: "project-stall-working", Name: "Stall Working Project", Slug: "stall-working-project"} + project := &store.Project{ID: tid("project-stall-working"), Name: "Stall Working Project", Slug: "stall-working-project"} require.NoError(t, s.CreateProject(ctx, project)) broker := &store.RuntimeBroker{ - ID: "broker-stall-working", Name: "Stall Working Broker", Slug: "stall-working-broker", + ID: tid("broker-stall-working"), Name: "Stall Working Broker", Slug: "stall-working-broker", Status: store.BrokerStatusOnline, } require.NoError(t, s.CreateRuntimeBroker(ctx, broker)) agent := &store.Agent{ - ID: "agent-stall-working", Slug: "stall-working-slug", Name: "Stall Working Agent", + ID: tid("agent-stall-working"), Slug: "stall-working-slug", Name: "Stall Working Agent", ProjectID: project.ID, RuntimeBrokerID: broker.ID, Phase: string(state.PhaseRunning), } @@ -3229,7 +3333,7 @@ func TestBrokerHeartbeat_StalledWorkingAgentNotOverwrittenBySameActivity(t *test })) // Simulate stalled detection: mark agent stalled with stalled_from_activity = working - db := s.(*sqlite.SQLiteStore).DB() + db := s.(*entadapter.CompositeStore).DB() staleActivity := time.Now().Add(-10 * time.Minute) _, err := db.ExecContext(ctx, "UPDATE agents SET activity = 'stalled', stalled_from_activity = 'working', last_activity_event = ?, last_seen = ? WHERE id = ?", @@ -3262,17 +3366,17 @@ func TestBrokerHeartbeat_DoesNotRevertStoppedAgent(t *testing.T) { srv, s := testServer(t) ctx := context.Background() - project := &store.Project{ID: "project-stop-revert", Name: "Stop Revert Project", Slug: "stop-revert-project"} + project := &store.Project{ID: tid("project-stop-revert"), Name: "Stop Revert Project", Slug: "stop-revert-project"} require.NoError(t, s.CreateProject(ctx, project)) broker := &store.RuntimeBroker{ - ID: "broker-stop-revert", Name: "Stop Revert Broker", Slug: "stop-revert-broker", + ID: tid("broker-stop-revert"), Name: "Stop Revert Broker", Slug: "stop-revert-broker", Status: store.BrokerStatusOnline, } require.NoError(t, s.CreateRuntimeBroker(ctx, broker)) agent := &store.Agent{ - ID: "agent-stop-revert", Slug: "stop-revert-slug", Name: "Stop Revert Agent", + ID: tid("agent-stop-revert"), Slug: "stop-revert-slug", Name: "Stop Revert Agent", ProjectID: project.ID, RuntimeBrokerID: broker.ID, Phase: string(state.PhaseRunning), } @@ -3320,17 +3424,17 @@ func TestBrokerHeartbeat_DoesNotRevertStoppedAgent_LegacyPath(t *testing.T) { srv, s := testServer(t) ctx := context.Background() - project := &store.Project{ID: "project-stop-legacy", Name: "Stop Legacy Project", Slug: "stop-legacy-project"} + project := &store.Project{ID: tid("project-stop-legacy"), Name: "Stop Legacy Project", Slug: "stop-legacy-project"} require.NoError(t, s.CreateProject(ctx, project)) broker := &store.RuntimeBroker{ - ID: "broker-stop-legacy", Name: "Stop Legacy Broker", Slug: "stop-legacy-broker", + ID: tid("broker-stop-legacy"), Name: "Stop Legacy Broker", Slug: "stop-legacy-broker", Status: store.BrokerStatusOnline, } require.NoError(t, s.CreateRuntimeBroker(ctx, broker)) agent := &store.Agent{ - ID: "agent-stop-legacy", Slug: "stop-legacy-slug", Name: "Stop Legacy Agent", + ID: tid("agent-stop-legacy"), Slug: "stop-legacy-slug", Name: "Stop Legacy Agent", ProjectID: project.ID, RuntimeBrokerID: broker.ID, Phase: string(state.PhaseRunning), } @@ -3368,17 +3472,17 @@ func TestBrokerHeartbeat_PropagatesTerminalActivityOnStoppedAgent(t *testing.T) srv, s := testServer(t) ctx := context.Background() - project := &store.Project{ID: "proj-crash-hb", Name: "Crash HB Project", Slug: "crash-hb-proj"} + project := &store.Project{ID: tid("proj-crash-hb"), Name: "Crash HB Project", Slug: "crash-hb-proj"} require.NoError(t, s.CreateProject(ctx, project)) broker := &store.RuntimeBroker{ - ID: "broker-crash-hb", Name: "Crash HB Broker", Slug: "crash-hb-broker", + ID: tid("broker-crash-hb"), Name: "Crash HB Broker", Slug: "crash-hb-broker", Status: store.BrokerStatusOnline, } require.NoError(t, s.CreateRuntimeBroker(ctx, broker)) agent := &store.Agent{ - ID: "agent-crash-hb", Slug: "crash-hb-slug", Name: "Crash HB Agent", + ID: tid("agent-crash-hb"), Slug: "crash-hb-slug", Name: "Crash HB Agent", ProjectID: project.ID, RuntimeBrokerID: broker.ID, Phase: string(state.PhaseStopped), } @@ -3417,17 +3521,17 @@ func TestBrokerHeartbeat_DoesNotOverwriteTerminalActivityWithNonTerminal(t *test srv, s := testServer(t) ctx := context.Background() - project := &store.Project{ID: "proj-term-guard", Name: "Term Guard Project", Slug: "term-guard-proj"} + project := &store.Project{ID: tid("proj-term-guard"), Name: "Term Guard Project", Slug: "term-guard-proj"} require.NoError(t, s.CreateProject(ctx, project)) broker := &store.RuntimeBroker{ - ID: "broker-term-guard", Name: "Term Guard Broker", Slug: "term-guard-broker", + ID: tid("broker-term-guard"), Name: "Term Guard Broker", Slug: "term-guard-broker", Status: store.BrokerStatusOnline, } require.NoError(t, s.CreateRuntimeBroker(ctx, broker)) agent := &store.Agent{ - ID: "agent-term-guard", Slug: "term-guard-slug", Name: "Term Guard Agent", + ID: tid("agent-term-guard"), Slug: "term-guard-slug", Name: "Term Guard Agent", ProjectID: project.ID, RuntimeBrokerID: broker.ID, Phase: string(state.PhaseStopped), Activity: string(state.ActivityCrashed), @@ -3464,11 +3568,11 @@ func TestCreateAgent_RestartCreatesNotificationSubscription(t *testing.T) { // Pre-create an agent in "created" phase (provisioned but not started) existingAgent := &store.Agent{ - ID: "agent-notify-restart", + ID: tid("agent-notify-restart"), Slug: "notify-agent", Name: "notify-agent", ProjectID: project.ID, - RuntimeBrokerID: "broker-create", + RuntimeBrokerID: tid("broker-create"), Phase: string(state.PhaseCreated), } require.NoError(t, s.CreateAgent(ctx, existingAgent)) @@ -3502,11 +3606,11 @@ func TestCreateAgent_RestartNoSubscriptionWithoutNotify(t *testing.T) { // Pre-create an agent in "created" phase existingAgent := &store.Agent{ - ID: "agent-no-notify", + ID: tid("agent-no-notify"), Slug: "no-notify-agent", Name: "no-notify-agent", ProjectID: project.ID, - RuntimeBrokerID: "broker-create", + RuntimeBrokerID: tid("broker-create"), Phase: string(state.PhaseCreated), } require.NoError(t, s.CreateAgent(ctx, existingAgent)) @@ -3531,14 +3635,14 @@ func TestHandleAgentMessage_PlainTextBuildsStructuredMessage(t *testing.T) { ctx := context.Background() project := &store.Project{ - ID: "project-msg", + ID: tid("project-msg"), Name: "Msg Test Project", Slug: "msg-test-project", } require.NoError(t, s.CreateProject(ctx, project)) broker := &store.RuntimeBroker{ - ID: "broker-msg", + ID: tid("broker-msg"), Name: "Msg Test Broker", Slug: "msg-test-broker", Status: store.BrokerStatusOnline, @@ -3553,8 +3657,8 @@ func TestHandleAgentMessage_PlainTextBuildsStructuredMessage(t *testing.T) { })) agent := &store.Agent{ - ID: "agent-msg-1", - Slug: "agent-msg-1", + ID: tid("agent-msg-1"), + Slug: tid("agent-msg-1"), Name: "Msg Agent", ProjectID: project.ID, RuntimeBrokerID: broker.ID, @@ -3597,14 +3701,14 @@ func TestHandleAgentMessage_StructuredMessagePopulatesSender(t *testing.T) { ctx := context.Background() project := &store.Project{ - ID: "project-msg-sender", + ID: tid("project-msg-sender"), Name: "Msg Sender Project", Slug: "msg-sender-project", } require.NoError(t, s.CreateProject(ctx, project)) broker := &store.RuntimeBroker{ - ID: "broker-msg-sender", + ID: tid("broker-msg-sender"), Name: "Msg Sender Broker", Slug: "msg-sender-broker", Status: store.BrokerStatusOnline, @@ -3618,8 +3722,8 @@ func TestHandleAgentMessage_StructuredMessagePopulatesSender(t *testing.T) { })) agent := &store.Agent{ - ID: "agent-msg-sender-1", - Slug: "agent-msg-sender-1", + ID: tid("agent-msg-sender-1"), + Slug: tid("agent-msg-sender-1"), Name: "Msg Sender Agent", ProjectID: project.ID, RuntimeBrokerID: broker.ID, @@ -3659,14 +3763,14 @@ func TestHandleAgentMessage_NotifyCreatesSubscription(t *testing.T) { ctx := context.Background() project := &store.Project{ - ID: "project-msg-notify", + ID: tid("project-msg-notify"), Name: "Msg Notify Project", Slug: "msg-notify-project", } require.NoError(t, s.CreateProject(ctx, project)) broker := &store.RuntimeBroker{ - ID: "broker-msg-notify", + ID: tid("broker-msg-notify"), Name: "Msg Notify Broker", Slug: "msg-notify-broker", Status: store.BrokerStatusOnline, @@ -3680,8 +3784,8 @@ func TestHandleAgentMessage_NotifyCreatesSubscription(t *testing.T) { })) agent := &store.Agent{ - ID: "agent-msg-notify-1", - Slug: "agent-msg-notify-1", + ID: tid("agent-msg-notify-1"), + Slug: tid("agent-msg-notify-1"), Name: "Msg Notify Agent", ProjectID: project.ID, RuntimeBrokerID: broker.ID, @@ -3719,14 +3823,14 @@ func TestHandleAgentMessage_NoNotifyNoSubscription(t *testing.T) { ctx := context.Background() project := &store.Project{ - ID: "project-msg-no-notify", + ID: tid("project-msg-no-notify"), Name: "Msg No Notify Project", Slug: "msg-no-notify-project", } require.NoError(t, s.CreateProject(ctx, project)) broker := &store.RuntimeBroker{ - ID: "broker-msg-no-notify", + ID: tid("broker-msg-no-notify"), Name: "Msg No Notify Broker", Slug: "msg-no-notify-broker", Status: store.BrokerStatusOnline, @@ -3740,8 +3844,8 @@ func TestHandleAgentMessage_NoNotifyNoSubscription(t *testing.T) { })) agent := &store.Agent{ - ID: "agent-msg-no-notify-1", - Slug: "agent-msg-no-notify-1", + ID: tid("agent-msg-no-notify-1"), + Slug: tid("agent-msg-no-notify-1"), Name: "Msg No Notify Agent", ProjectID: project.ID, RuntimeBrokerID: broker.ID, @@ -3772,15 +3876,15 @@ func TestHandleAgentMessage_NoDispatcher_Returns503(t *testing.T) { ctx := context.Background() project := &store.Project{ - ID: "project-msg-503", + ID: tid("project-msg-503"), Name: "Msg 503 Project", Slug: "msg-503-project", } require.NoError(t, s.CreateProject(ctx, project)) agent := &store.Agent{ - ID: "agent-msg-503", - Slug: "agent-msg-503", + ID: tid("agent-msg-503"), + Slug: tid("agent-msg-503"), Name: "Msg 503 Agent", ProjectID: project.ID, Phase: string(state.PhaseRunning), @@ -3803,15 +3907,15 @@ func TestHandleAgentMessage_NoBrokerID_Returns503(t *testing.T) { ctx := context.Background() project := &store.Project{ - ID: "project-msg-503-nobroker", + ID: tid("project-msg-503-nobroker"), Name: "Msg 503 NoBroker Project", Slug: "msg-503-nobroker-project", } require.NoError(t, s.CreateProject(ctx, project)) agent := &store.Agent{ - ID: "agent-msg-503-nobroker", - Slug: "agent-msg-503-nobroker", + ID: tid("agent-msg-503-nobroker"), + Slug: tid("agent-msg-503-nobroker"), Name: "Msg 503 NoBroker Agent", ProjectID: project.ID, Phase: string(state.PhaseRunning), @@ -3877,14 +3981,14 @@ func TestCreateAgent_GCPIdentityAssign(t *testing.T) { // Register and verify a GCP service account sa := &store.GCPServiceAccount{ - ID: "sa-assign-1", + ID: tid("sa-assign-1"), Scope: store.ScopeProject, ScopeID: project.ID, Email: "worker@project.iam.gserviceaccount.com", - ProjectID: "my-project", + ProjectID: tid("my-project"), Verified: true, VerifiedAt: time.Now(), - CreatedBy: "user-1", + CreatedBy: tid("user-1"), CreatedAt: time.Now(), } require.NoError(t, s.CreateGCPServiceAccount(ctx, sa)) @@ -4033,13 +4137,13 @@ func TestCreateAgent_GCPIdentityAssignUnverifiedSA(t *testing.T) { ctx := context.Background() sa := &store.GCPServiceAccount{ - ID: "sa-unverified-1", + ID: tid("sa-unverified-1"), Scope: store.ScopeProject, ScopeID: project.ID, Email: "unverified@project.iam.gserviceaccount.com", - ProjectID: "my-project", + ProjectID: tid("my-project"), Verified: false, - CreatedBy: "user-1", + CreatedBy: tid("user-1"), CreatedAt: time.Now(), } require.NoError(t, s.CreateGCPServiceAccount(ctx, sa)) @@ -4061,14 +4165,14 @@ func TestCreateAgent_GCPIdentityAssignWrongProject(t *testing.T) { ctx := context.Background() sa := &store.GCPServiceAccount{ - ID: "sa-other-project-1", + ID: tid("sa-other-project-1"), Scope: store.ScopeProject, ScopeID: "other-project-id", Email: "other@project.iam.gserviceaccount.com", - ProjectID: "my-project", + ProjectID: tid("my-project"), Verified: true, VerifiedAt: time.Now(), - CreatedBy: "user-1", + CreatedBy: tid("user-1"), CreatedAt: time.Now(), } require.NoError(t, s.CreateGCPServiceAccount(ctx, sa)) @@ -4121,7 +4225,7 @@ func TestCreateAgent_GCPPassthrough_BrokerOwnerAllowed(t *testing.T) { // Create a user who owns the broker owner := &store.User{ - ID: "user-broker-owner", + ID: tid("user-broker-owner"), Email: "owner@test.com", DisplayName: "Broker Owner", Role: store.UserRoleMember, @@ -4133,7 +4237,7 @@ func TestCreateAgent_GCPPassthrough_BrokerOwnerAllowed(t *testing.T) { // Create a project owned by the broker owner with proper policies project := &store.Project{ - ID: "project-pt-owner", + ID: tid("project-pt-owner"), Name: "Passthrough Owner Project", Slug: "passthrough-owner-project", OwnerID: owner.ID, @@ -4146,7 +4250,7 @@ func TestCreateAgent_GCPPassthrough_BrokerOwnerAllowed(t *testing.T) { // Create a broker owned by the same user broker := &store.RuntimeBroker{ - ID: "broker-pt-owner", + ID: tid("broker-pt-owner"), Name: "Owner Broker", Slug: "owner-broker", Status: store.BrokerStatusOnline, @@ -4186,7 +4290,7 @@ func TestCreateAgent_GCPPassthrough_NonOwnerDenied(t *testing.T) { // Create the broker owner owner := &store.User{ - ID: "user-broker-owner-2", + ID: tid("user-broker-owner-2"), Email: "owner2@test.com", DisplayName: "Broker Owner 2", Role: store.UserRoleMember, @@ -4197,7 +4301,7 @@ func TestCreateAgent_GCPPassthrough_NonOwnerDenied(t *testing.T) { // Create a non-owner user nonOwner := &store.User{ - ID: "user-non-owner", + ID: tid("user-non-owner"), Email: "nonowner@test.com", DisplayName: "Non Owner", Role: store.UserRoleMember, @@ -4209,7 +4313,7 @@ func TestCreateAgent_GCPPassthrough_NonOwnerDenied(t *testing.T) { // Create a project where the non-owner is a member project := &store.Project{ - ID: "project-pt-nonowner", + ID: tid("project-pt-nonowner"), Name: "Passthrough NonOwner Project", Slug: "passthrough-nonowner-project", OwnerID: nonOwner.ID, @@ -4222,7 +4326,7 @@ func TestCreateAgent_GCPPassthrough_NonOwnerDenied(t *testing.T) { // Create a broker owned by a DIFFERENT user broker := &store.RuntimeBroker{ - ID: "broker-pt-nonowner", + ID: tid("broker-pt-nonowner"), Name: "Other Broker", Slug: "other-broker", Status: store.BrokerStatusOnline, @@ -4261,7 +4365,7 @@ func TestCreateAgent_GCPPassthrough_AdminAllowed(t *testing.T) { ctx := context.Background() brokerOwner := &store.User{ - ID: "user-broker-owner-3", + ID: tid("user-broker-owner-3"), Email: "owner3@test.com", DisplayName: "Broker Owner 3", Role: store.UserRoleMember, @@ -4271,7 +4375,7 @@ func TestCreateAgent_GCPPassthrough_AdminAllowed(t *testing.T) { require.NoError(t, s.CreateUser(ctx, brokerOwner)) adminUser := &store.User{ - ID: "user-admin-pt", + ID: tid("user-admin-pt"), Email: "admin@test.com", DisplayName: "Admin User", Role: store.UserRoleAdmin, @@ -4282,7 +4386,7 @@ func TestCreateAgent_GCPPassthrough_AdminAllowed(t *testing.T) { ensureHubMembership(ctx, s, adminUser.ID) project := &store.Project{ - ID: "project-pt-admin", + ID: tid("project-pt-admin"), Name: "Passthrough Admin Project", Slug: "passthrough-admin-project", OwnerID: adminUser.ID, @@ -4295,7 +4399,7 @@ func TestCreateAgent_GCPPassthrough_AdminAllowed(t *testing.T) { // Broker owned by someone else broker := &store.RuntimeBroker{ - ID: "broker-pt-admin", + ID: tid("broker-pt-admin"), Name: "Admin Test Broker", Slug: "admin-test-broker", Status: store.BrokerStatusOnline, @@ -4330,14 +4434,14 @@ func TestCreateAgent_GCPIdentityBlockOverridesProjectDefault(t *testing.T) { // Register and verify a GCP service account sa := &store.GCPServiceAccount{ - ID: "sa-project-default", + ID: tid("sa-project-default"), Scope: store.ScopeProject, ScopeID: project.ID, Email: "project-default@project.iam.gserviceaccount.com", - ProjectID: "my-project", + ProjectID: tid("my-project"), Verified: true, VerifiedAt: time.Now(), - CreatedBy: "user-1", + CreatedBy: tid("user-1"), CreatedAt: time.Now(), } require.NoError(t, s.CreateGCPServiceAccount(ctx, sa)) @@ -4376,14 +4480,14 @@ func TestCreateAgent_GCPIdentityProjectDefaultApplied(t *testing.T) { // Register and verify a GCP service account sa := &store.GCPServiceAccount{ - ID: "sa-project-applied", + ID: tid("sa-project-applied"), Scope: store.ScopeProject, ScopeID: project.ID, Email: "project-applied@project.iam.gserviceaccount.com", - ProjectID: "my-project", + ProjectID: tid("my-project"), Verified: true, VerifiedAt: time.Now(), - CreatedBy: "user-1", + CreatedBy: tid("user-1"), CreatedAt: time.Now(), } require.NoError(t, s.CreateGCPServiceAccount(ctx, sa)) @@ -4416,13 +4520,13 @@ func TestPreserveTerminalPhase(t *testing.T) { srv, s := testServer(t) ctx := context.Background() - project := &store.Project{ID: "project-tp", Name: "TP Project", Slug: "tp-project"} + project := &store.Project{ID: tid("project-tp"), Name: "TP Project", Slug: "tp-project"} require.NoError(t, s.CreateProject(ctx, project)) t.Run("preserves error phase", func(t *testing.T) { agent := &store.Agent{ - ID: "agent-tp-error", - Slug: "agent-tp-error", + ID: tid("agent-tp-error"), + Slug: tid("agent-tp-error"), Name: "TP Error Agent", ProjectID: project.ID, Phase: string(state.PhaseCreated), @@ -4448,8 +4552,8 @@ func TestPreserveTerminalPhase(t *testing.T) { t.Run("preserves stopped phase", func(t *testing.T) { agent := &store.Agent{ - ID: "agent-tp-stopped", - Slug: "agent-tp-stopped", + ID: tid("agent-tp-stopped"), + Slug: tid("agent-tp-stopped"), Name: "TP Stopped Agent", ProjectID: project.ID, Phase: string(state.PhaseCreated), @@ -4468,8 +4572,8 @@ func TestPreserveTerminalPhase(t *testing.T) { t.Run("does not overwrite non-terminal phase", func(t *testing.T) { agent := &store.Agent{ - ID: "agent-tp-running", - Slug: "agent-tp-running", + ID: tid("agent-tp-running"), + Slug: tid("agent-tp-running"), Name: "TP Running Agent", ProjectID: project.ID, Phase: string(state.PhaseCreated), @@ -4492,17 +4596,17 @@ func TestListAgents_GlobalEndpointReturnsAllAgents(t *testing.T) { ctx := context.Background() // Create two projects with agents in each - project1 := &store.Project{ID: "project-global-1", Name: "Project One", Slug: "project-one"} - project2 := &store.Project{ID: "project-global-2", Name: "Project Two", Slug: "project-two"} + project1 := &store.Project{ID: tid("project-global-1"), Name: "Project One", Slug: "project-one"} + project2 := &store.Project{ID: tid("project-global-2"), Name: "Project Two", Slug: "project-two"} require.NoError(t, s.CreateProject(ctx, project1)) require.NoError(t, s.CreateProject(ctx, project2)) agent1 := &store.Agent{ - ID: "agent-g1", Slug: "agent-g1", Name: "Agent G1", + ID: tid("agent-g1"), Slug: tid("agent-g1"), Name: "Agent G1", ProjectID: project1.ID, Phase: string(state.PhaseRunning), } agent2 := &store.Agent{ - ID: "agent-g2", Slug: "agent-g2", Name: "Agent G2", + ID: tid("agent-g2"), Slug: tid("agent-g2"), Name: "Agent G2", ProjectID: project2.ID, Phase: string(state.PhaseCreated), } require.NoError(t, s.CreateAgent(ctx, agent1)) @@ -4545,14 +4649,14 @@ func TestHandleAgentExec_DispatchesToRuntimeBroker(t *testing.T) { ctx := context.Background() project := &store.Project{ - ID: "project-exec", + ID: tid("project-exec"), Name: "Exec Project", Slug: "exec-project", } require.NoError(t, s.CreateProject(ctx, project)) broker := &store.RuntimeBroker{ - ID: "broker-exec", + ID: tid("broker-exec"), Name: "Exec Broker", Slug: "exec-broker", Status: store.BrokerStatusOnline, @@ -4566,8 +4670,8 @@ func TestHandleAgentExec_DispatchesToRuntimeBroker(t *testing.T) { })) agent := &store.Agent{ - ID: "agent-exec-1", - Slug: "agent-exec-1", + ID: tid("agent-exec-1"), + Slug: tid("agent-exec-1"), Name: "Exec Agent", ProjectID: project.ID, RuntimeBrokerID: broker.ID, @@ -4597,14 +4701,14 @@ func TestHandleProjectAgentExec_DispatchesToRuntimeBroker(t *testing.T) { ctx := context.Background() project := &store.Project{ - ID: "project-exec-project-route", + ID: tid("project-exec-project-route"), Name: "Exec Project Route", Slug: "exec-project-route", } require.NoError(t, s.CreateProject(ctx, project)) broker := &store.RuntimeBroker{ - ID: "broker-exec-project-route", + ID: tid("broker-exec-project-route"), Name: "Exec Broker Project Route", Slug: "exec-broker-project-route", Status: store.BrokerStatusOnline, @@ -4618,8 +4722,8 @@ func TestHandleProjectAgentExec_DispatchesToRuntimeBroker(t *testing.T) { })) agent := &store.Agent{ - ID: "agent-exec-project-route", - Slug: "agent-exec-project-route", + ID: tid("agent-exec-project-route"), + Slug: tid("agent-exec-project-route"), Name: "Exec Agent Project Route", ProjectID: project.ID, RuntimeBrokerID: broker.ID, @@ -4643,3 +4747,126 @@ func TestHandleProjectAgentExec_DispatchesToRuntimeBroker(t *testing.T) { assert.Equal(t, "terminal output", resp.Output) assert.Equal(t, 0, resp.ExitCode) } + +func TestAgentStatusUpdate_RejectsPhaseRegression(t *testing.T) { + srv, s := testServer(t) + ctx := context.Background() + + project := &store.Project{ID: "proj-regress", Name: "Regression Project", Slug: "regress-project"} + require.NoError(t, s.CreateProject(ctx, project)) + + agent := &store.Agent{ + ID: "agent-regress", Slug: "regress-slug", Name: "Regression Agent", + ProjectID: project.ID, Phase: string(state.PhaseRunning), + Activity: string(state.ActivityExecuting), + } + require.NoError(t, s.CreateAgent(ctx, agent)) + + tokenSvc := srv.GetAgentTokenService() + require.NotNil(t, tokenSvc) + token, err := tokenSvc.GenerateAgentToken(agent.ID, project.ID, []AgentTokenScope{ScopeAgentStatusUpdate}, nil) + require.NoError(t, err) + + // Attempt to regress phase from running → starting (as a spurious session would) + status := store.AgentStatusUpdate{Phase: string(state.PhaseStarting)} + body, _ := json.Marshal(status) + req := httptest.NewRequest(http.MethodPost, "/api/v1/agents/"+agent.ID+"/status", bytes.NewReader(body)) + req.Header.Set("X-Scion-Agent-Token", token) + req.Header.Set("Content-Type", "application/json") + + rec := httptest.NewRecorder() + srv.Handler().ServeHTTP(rec, req) + assert.Equal(t, http.StatusOK, rec.Code) + + // Phase should remain running — regression was rejected + updated, err := s.GetAgent(ctx, agent.ID) + require.NoError(t, err) + assert.Equal(t, string(state.PhaseRunning), updated.Phase, + "phase regression from running to starting should be rejected") + assert.Equal(t, string(state.ActivityExecuting), updated.Activity, + "activity should be preserved when phase regression is rejected") +} + +func TestAgentStatusUpdate_ActivityAutoCorrectsPhase(t *testing.T) { + srv, s := testServer(t) + ctx := context.Background() + + project := &store.Project{ID: "proj-autocorrect", Name: "AutoCorrect Project", Slug: "autocorrect-project"} + require.NoError(t, s.CreateProject(ctx, project)) + + agent := &store.Agent{ + ID: "agent-autocorrect", Slug: "autocorrect-slug", Name: "AutoCorrect Agent", + ProjectID: project.ID, Phase: string(state.PhaseStarting), + } + require.NoError(t, s.CreateAgent(ctx, agent)) + + tokenSvc := srv.GetAgentTokenService() + require.NotNil(t, tokenSvc) + token, err := tokenSvc.GenerateAgentToken(agent.ID, project.ID, []AgentTokenScope{ScopeAgentStatusUpdate}, nil) + require.NoError(t, err) + + // Send an activity-only update (working) while phase is starting. + // This should auto-correct the phase to running. + status := store.AgentStatusUpdate{Activity: string(state.ActivityWorking)} + body, _ := json.Marshal(status) + req := httptest.NewRequest(http.MethodPost, "/api/v1/agents/"+agent.ID+"/status", bytes.NewReader(body)) + req.Header.Set("X-Scion-Agent-Token", token) + req.Header.Set("Content-Type", "application/json") + + rec := httptest.NewRecorder() + srv.Handler().ServeHTTP(rec, req) + assert.Equal(t, http.StatusOK, rec.Code) + + // Phase should auto-correct to running + updated, err := s.GetAgent(ctx, agent.ID) + require.NoError(t, err) + assert.Equal(t, string(state.PhaseRunning), updated.Phase, + "activity=working should auto-correct phase from starting to running") + assert.Equal(t, string(state.ActivityWorking), updated.Activity) +} + +func TestBrokerHeartbeat_RejectsPhaseRegression(t *testing.T) { + srv, s := testServer(t) + ctx := context.Background() + + project := &store.Project{ID: "proj-hb-regress", Name: "HB Regression Project", Slug: "hb-regress-project"} + require.NoError(t, s.CreateProject(ctx, project)) + + broker := &store.RuntimeBroker{ + ID: "broker-hb-regress", Name: "HB Regression Broker", Slug: "hb-regress-broker", + Status: store.BrokerStatusOnline, + } + require.NoError(t, s.CreateRuntimeBroker(ctx, broker)) + + agent := &store.Agent{ + ID: "agent-hb-regress", Slug: "hb-regress-slug", Name: "HB Regression Agent", + ProjectID: project.ID, RuntimeBrokerID: broker.ID, + Phase: string(state.PhaseRunning), + Activity: string(state.ActivityWorking), + } + require.NoError(t, s.CreateAgent(ctx, agent)) + + // Send a heartbeat with stale phase=starting (as if agent-info.json was + // corrupted by a spurious session's pre-start hook) + hb := brokerHeartbeatRequest{ + Status: "online", + Projects: []brokerProjectHeartbeat{{ + ProjectID: project.ID, + AgentCount: 1, + Agents: []brokerAgentHeartbeat{{ + Slug: agent.Slug, + Phase: string(state.PhaseStarting), + Activity: string(state.ActivityWorking), + ContainerStatus: "Up 10 minutes", + }}, + }}, + } + rec := doRequest(t, srv, http.MethodPost, "/api/v1/runtime-brokers/"+broker.ID+"/heartbeat", hb) + assert.Equal(t, http.StatusOK, rec.Code) + + // Phase should remain running — heartbeat regression was rejected + updated, err := s.GetAgent(ctx, agent.ID) + require.NoError(t, err) + assert.Equal(t, string(state.PhaseRunning), updated.Phase, + "heartbeat should not regress phase from running to starting") +} diff --git a/pkg/hub/handlers_auth.go b/pkg/hub/handlers_auth.go index 5a6451946..23e8f6363 100644 --- a/pkg/hub/handlers_auth.go +++ b/pkg/hub/handlers_auth.go @@ -181,6 +181,19 @@ func (t TokenResponse) MarshalJSON() ([]byte, error) { }) } +// ExternalUserInfo carries the provider-verified identity fields needed to +// provision a user. It is a subset of OAuthUserInfo, decoupled from the +// OAuth layer so that provisionUser can serve both OAuth and proxy callers. +type ExternalUserInfo struct { + Email string + DisplayName string + AvatarURL string +} + +// ErrAccessDenied is returned by provisionUser when the user's email is not +// authorized to log in (domain restriction, invite-only, etc.). +var ErrAccessDenied = errors.New("access denied") + // handleAuth routes auth-related requests. func (s *Server) handleAuth(w http.ResponseWriter, r *http.Request) { path := r.URL.Path @@ -241,56 +254,28 @@ func (s *Server) handleAuthLogin(w http.ResponseWriter, r *http.Request) { return } - // Check if user is authorized (admin bypass, domain check, access mode) + // Provision user (authorize + find-or-create + hub membership) ctx := r.Context() - if !s.isUserAuthorized(ctx, userInfo.Email) { - reason := "not_on_allow_list" - if s.config.UserAccessMode != "invite_only" { - reason = "domain_not_authorized" - } - LogInviteAuditFailure(ctx, s.auditLogger, InviteAuditLoginDenied, userInfo.Email, reason) - writeError(w, http.StatusForbidden, "unauthorized_domain", - "your email domain is not authorized", nil) - return - } - - // Find or create user - user, err := s.store.GetUserByEmail(ctx, userInfo.Email) + user, err := s.provisionUser(ctx, &ExternalUserInfo{ + Email: userInfo.Email, + DisplayName: userInfo.DisplayName, + AvatarURL: userInfo.AvatarURL, + }) if err != nil { - // Create new user - user = &store.User{ - ID: generateID(), - Email: userInfo.Email, - DisplayName: userInfo.DisplayName, - AvatarURL: userInfo.AvatarURL, - Role: s.getUserRole(userInfo.Email), - Status: "active", - Created: time.Now(), - LastLogin: time.Now(), - } - if err := s.store.CreateUser(ctx, user); err != nil { - InternalError(w) + if errors.Is(err, ErrAccessDenied) { + writeError(w, http.StatusForbidden, "unauthorized_domain", + "your email domain is not authorized", nil) return } - } else { - // Update last login - user.LastLogin = time.Now() - if userInfo.AvatarURL != "" && user.AvatarURL == "" { - user.AvatarURL = userInfo.AvatarURL - } - if userInfo.DisplayName != "" && user.DisplayName == "" { - user.DisplayName = userInfo.DisplayName - } - // Check if user should be promoted to admin (in case admin list changed) - if user.Role != "admin" && s.getUserRole(userInfo.Email) == "admin" { - user.Role = "admin" + if errors.Is(err, ErrUserSuspended) { + writeError(w, http.StatusForbidden, "user_suspended", + "your account has been suspended", nil) + return } - _ = s.store.UpdateUser(ctx, user) + InternalError(w) + return } - // Ensure user is a member of the hub-members group - ensureHubMembership(ctx, s.store, user.ID) - // Generate tokens if s.userTokenService == nil { InternalError(w) @@ -386,55 +371,27 @@ func (s *Server) handleAuthToken(w http.ResponseWriter, r *http.Request) { return } - // Check if user is authorized (admin bypass, domain check, access mode) - if !s.isUserAuthorized(ctx, userInfo.Email) { - reason := "not_on_allow_list" - if s.config.UserAccessMode != "invite_only" { - reason = "domain_not_authorized" - } - LogInviteAuditFailure(ctx, s.auditLogger, InviteAuditLoginDenied, userInfo.Email, reason) - writeError(w, http.StatusForbidden, "unauthorized_domain", - "your email domain is not authorized", nil) - return - } - - // Find or create user - user, err := s.store.GetUserByEmail(ctx, userInfo.Email) + // Provision user (authorize + find-or-create + hub membership) + user, err := s.provisionUser(ctx, &ExternalUserInfo{ + Email: userInfo.Email, + DisplayName: userInfo.DisplayName, + AvatarURL: userInfo.AvatarURL, + }) if err != nil { - // Create new user - user = &store.User{ - ID: generateID(), - Email: userInfo.Email, - DisplayName: userInfo.DisplayName, - AvatarURL: userInfo.AvatarURL, - Role: s.getUserRole(userInfo.Email), - Status: "active", - Created: time.Now(), - LastLogin: time.Now(), - } - if err := s.store.CreateUser(ctx, user); err != nil { - InternalError(w) + if errors.Is(err, ErrAccessDenied) { + writeError(w, http.StatusForbidden, "unauthorized_domain", + "your email domain is not authorized", nil) return } - } else { - // Update last login - user.LastLogin = time.Now() - if userInfo.AvatarURL != "" && user.AvatarURL == "" { - user.AvatarURL = userInfo.AvatarURL - } - if userInfo.DisplayName != "" && user.DisplayName == "" { - user.DisplayName = userInfo.DisplayName - } - // Check if user should be promoted to admin (in case admin list changed) - if user.Role != "admin" && s.getUserRole(userInfo.Email) == "admin" { - user.Role = "admin" + if errors.Is(err, ErrUserSuspended) { + writeError(w, http.StatusForbidden, "user_suspended", + "your account has been suspended", nil) + return } - _ = s.store.UpdateUser(ctx, user) + InternalError(w) + return } - // Ensure user is a member of the hub-members group - ensureHubMembership(ctx, s.store, user.ID) - // Generate tokens if s.userTokenService == nil { InternalError(w) @@ -541,7 +498,14 @@ func (s *Server) handleAuthValidate(w http.ResponseWriter, r *http.Request) { } // handleAuthLogout handles POST /api/v1/auth/logout. +// In proxy mode, this is a no-op (the proxy owns the session). func (s *Server) handleAuthLogout(w http.ResponseWriter, r *http.Request) { + // In proxy mode, the hub does not own the session. + if s.config.AuthMode == "proxy" { + writeJSON(w, http.StatusOK, AuthLogoutResponse{Success: true}) + return + } + var req AuthLogoutRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { // Empty body is fine for logout @@ -861,7 +825,8 @@ func (s *Server) handleCLIAuthProviders(w http.ResponseWriter, r *http.Request) ClientType: clientTypeParam, Providers: []string{}, } - if s.oauthService != nil { + // In proxy mode, no OAuth providers are available. + if s.config.AuthMode != "proxy" && s.oauthService != nil { resp.Providers = s.oauthService.ConfiguredProvidersForClient(clientType) } @@ -920,55 +885,27 @@ func (s *Server) handleCLIAuthToken(w http.ResponseWriter, r *http.Request) { return } - // Check if user is authorized (admin bypass, domain check, access mode) - if !s.isUserAuthorized(ctx, userInfo.Email) { - reason := "not_on_allow_list" - if s.config.UserAccessMode != "invite_only" { - reason = "domain_not_authorized" - } - LogInviteAuditFailure(ctx, s.auditLogger, InviteAuditLoginDenied, userInfo.Email, reason) - writeError(w, http.StatusForbidden, "unauthorized_domain", - "your email domain is not authorized", nil) - return - } - - // Find or create user - user, err := s.store.GetUserByEmail(ctx, userInfo.Email) + // Provision user (authorize + find-or-create + hub membership) + user, err := s.provisionUser(ctx, &ExternalUserInfo{ + Email: userInfo.Email, + DisplayName: userInfo.DisplayName, + AvatarURL: userInfo.AvatarURL, + }) if err != nil { - // Create new user - user = &store.User{ - ID: generateID(), - Email: userInfo.Email, - DisplayName: userInfo.DisplayName, - AvatarURL: userInfo.AvatarURL, - Role: s.getUserRole(userInfo.Email), - Status: "active", - Created: time.Now(), - LastLogin: time.Now(), - } - if err := s.store.CreateUser(ctx, user); err != nil { - InternalError(w) + if errors.Is(err, ErrAccessDenied) { + writeError(w, http.StatusForbidden, "unauthorized_domain", + "your email domain is not authorized", nil) return } - } else { - // Update last login and profile info - user.LastLogin = time.Now() - if userInfo.AvatarURL != "" && user.AvatarURL == "" { - user.AvatarURL = userInfo.AvatarURL - } - if userInfo.DisplayName != "" && user.DisplayName == "" { - user.DisplayName = userInfo.DisplayName - } - // Check if user should be promoted to admin (in case admin list changed) - if user.Role != "admin" && s.getUserRole(userInfo.Email) == "admin" { - user.Role = "admin" + if errors.Is(err, ErrUserSuspended) { + writeError(w, http.StatusForbidden, "user_suspended", + "your account has been suspended", nil) + return } - _ = s.store.UpdateUser(ctx, user) + InternalError(w) + return } - // Ensure user is a member of the hub-members group - ensureHubMembership(ctx, s.store, user.ID) - // Generate Hub tokens (CLI type for longer duration) if s.userTokenService == nil { InternalError(w) @@ -1176,54 +1113,27 @@ func (s *Server) getDeviceFlowUserInfo(ctx context.Context, provider, accessToke func (s *Server) completeOAuthLogin(w http.ResponseWriter, r *http.Request, userInfo *OAuthUserInfo) { ctx := r.Context() - // Check if user is authorized (admin bypass, domain check, access mode) - if !s.isUserAuthorized(ctx, userInfo.Email) { - reason := "not_on_allow_list" - if s.config.UserAccessMode != "invite_only" { - reason = "domain_not_authorized" - } - LogInviteAuditFailure(ctx, s.auditLogger, InviteAuditLoginDenied, userInfo.Email, reason) - writeError(w, http.StatusForbidden, "unauthorized_domain", - "your email domain is not authorized", nil) - return - } - - // Find or create user - user, err := s.store.GetUserByEmail(ctx, userInfo.Email) + // Provision user (authorize + find-or-create + hub membership) + user, err := s.provisionUser(ctx, &ExternalUserInfo{ + Email: userInfo.Email, + DisplayName: userInfo.DisplayName, + AvatarURL: userInfo.AvatarURL, + }) if err != nil { - // Create new user - user = &store.User{ - ID: generateID(), - Email: userInfo.Email, - DisplayName: userInfo.DisplayName, - AvatarURL: userInfo.AvatarURL, - Role: s.getUserRole(userInfo.Email), - Status: "active", - Created: time.Now(), - LastLogin: time.Now(), - } - if err := s.store.CreateUser(ctx, user); err != nil { - InternalError(w) + if errors.Is(err, ErrAccessDenied) { + writeError(w, http.StatusForbidden, "unauthorized_domain", + "your email domain is not authorized", nil) return } - } else { - // Update last login and profile info - user.LastLogin = time.Now() - if userInfo.AvatarURL != "" && user.AvatarURL == "" { - user.AvatarURL = userInfo.AvatarURL - } - if userInfo.DisplayName != "" && user.DisplayName == "" { - user.DisplayName = userInfo.DisplayName - } - if user.Role != "admin" && s.getUserRole(userInfo.Email) == "admin" { - user.Role = "admin" + if errors.Is(err, ErrUserSuspended) { + writeError(w, http.StatusForbidden, "user_suspended", + "your account has been suspended", nil) + return } - _ = s.store.UpdateUser(ctx, user) + InternalError(w) + return } - // Ensure user is a member of the hub-members group - ensureHubMembership(ctx, s.store, user.ID) - // Generate Hub tokens (CLI type for longer duration) if s.userTokenService == nil { InternalError(w) @@ -1252,6 +1162,69 @@ func (s *Server) completeOAuthLogin(w http.ResponseWriter, r *http.Request, user }) } +// ErrUserSuspended is returned by provisionUser when the user's account is suspended. +var ErrUserSuspended = errors.New("user account is suspended") + +// provisionUser authorizes the external user, then finds or creates the +// corresponding store.User. It returns ErrAccessDenied when the email is not +// authorized, or ErrUserSuspended when the user is suspended. +// On success it also ensures hub-members group membership. +func (s *Server) provisionUser(ctx context.Context, info *ExternalUserInfo) (*store.User, error) { + // Authorization check + if !s.isUserAuthorized(ctx, info.Email) { + reason := "not_on_allow_list" + if s.config.UserAccessMode != "invite_only" { + reason = "domain_not_authorized" + } + LogInviteAuditFailure(ctx, s.auditLogger, InviteAuditLoginDenied, info.Email, reason) + return nil, ErrAccessDenied + } + + // Find or create user + user, err := s.store.GetUserByEmail(ctx, info.Email) + if err != nil { + // Create new user + user = &store.User{ + ID: generateID(), + Email: info.Email, + DisplayName: info.DisplayName, + AvatarURL: info.AvatarURL, + Role: s.getUserRole(info.Email), + Status: "active", + Created: time.Now(), + LastLogin: time.Now(), + } + if err := s.store.CreateUser(ctx, user); err != nil { + return nil, fmt.Errorf("create user: %w", err) + } + } else { + // Reject suspended users (covers both OAuth and proxy auth paths) + if user.Status == "suspended" { + slog.Warn("login rejected: user is suspended", "email", info.Email, "user_id", user.ID) + return nil, ErrUserSuspended + } + + // Update last login and backfill profile + user.LastLogin = time.Now() + if info.AvatarURL != "" && user.AvatarURL == "" { + user.AvatarURL = info.AvatarURL + } + if info.DisplayName != "" && user.DisplayName == "" { + user.DisplayName = info.DisplayName + } + // Promote to admin if config changed + if user.Role != "admin" && s.getUserRole(info.Email) == "admin" { + user.Role = "admin" + } + _ = s.store.UpdateUser(ctx, user) + } + + // Ensure user is a member of the hub-members group + ensureHubMembership(ctx, s.store, user.ID) + + return user, nil +} + // generateID generates a new UUID. func generateID() string { return uuid.New().String() diff --git a/pkg/hub/handlers_auth_test.go b/pkg/hub/handlers_auth_test.go index 42edc3ee6..1c9c1c5b6 100644 --- a/pkg/hub/handlers_auth_test.go +++ b/pkg/hub/handlers_auth_test.go @@ -19,6 +19,7 @@ package hub import ( "context" "encoding/json" + "errors" "io" "net/http" "net/http/httptest" @@ -161,7 +162,7 @@ func TestAuthMe(t *testing.T) { // Create a user user := &store.User{ - ID: "user_123", + ID: tid("user_123"), Email: "me@example.com", DisplayName: "Me", Role: "admin", @@ -489,3 +490,230 @@ func TestCLIDeviceToken_MethodNotAllowed(t *testing.T) { t.Errorf("expected status 405 for GET, got %d", rec.Code) } } + +func TestProvisionUser(t *testing.T) { + ctx := context.Background() + + t.Run("creates new user", func(t *testing.T) { + srv, s := testServer(t) + + info := &ExternalUserInfo{ + Email: "new@example.com", + DisplayName: "New User", + AvatarURL: "https://example.com/avatar.png", + } + + user, err := srv.provisionUser(ctx, info) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if user.Email != "new@example.com" { + t.Errorf("expected email new@example.com, got %q", user.Email) + } + if user.DisplayName != "New User" { + t.Errorf("expected display name 'New User', got %q", user.DisplayName) + } + if user.AvatarURL != "https://example.com/avatar.png" { + t.Errorf("expected avatar URL, got %q", user.AvatarURL) + } + if user.Status != "active" { + t.Errorf("expected status 'active', got %q", user.Status) + } + if user.ID == "" { + t.Error("expected non-empty user ID") + } + + // Verify persisted in store + stored, err := s.GetUserByEmail(ctx, "new@example.com") + if err != nil { + t.Fatalf("user not found in store: %v", err) + } + if stored.ID != user.ID { + t.Errorf("stored user ID mismatch: %q vs %q", stored.ID, user.ID) + } + }) + + t.Run("updates existing user last login", func(t *testing.T) { + srv, s := testServer(t) + + // Pre-create user + original := &store.User{ + ID: generateID(), + Email: "existing@example.com", + DisplayName: "Original Name", + AvatarURL: "https://example.com/original.png", + Role: "member", + Status: "active", + Created: time.Now().Add(-24 * time.Hour), + LastLogin: time.Now().Add(-24 * time.Hour), + } + if err := s.CreateUser(ctx, original); err != nil { + t.Fatalf("failed to create user: %v", err) + } + + beforeLogin := time.Now() + info := &ExternalUserInfo{ + Email: "existing@example.com", + DisplayName: "Updated Name", + AvatarURL: "https://example.com/updated.png", + } + + user, err := srv.provisionUser(ctx, info) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + + // LastLogin should be updated + if user.LastLogin.Before(beforeLogin) { + t.Error("expected LastLogin to be updated") + } + // DisplayName should NOT be updated (original was non-empty) + if user.DisplayName != "Original Name" { + t.Errorf("expected display name 'Original Name', got %q", user.DisplayName) + } + // AvatarURL should NOT be updated (original was non-empty) + if user.AvatarURL != "https://example.com/original.png" { + t.Errorf("expected original avatar URL, got %q", user.AvatarURL) + } + }) + + t.Run("backfills empty display name and avatar", func(t *testing.T) { + srv, s := testServer(t) + + // Pre-create user with empty display name and avatar + original := &store.User{ + ID: generateID(), + Email: "backfill@example.com", + Role: "member", + Status: "active", + Created: time.Now().Add(-1 * time.Hour), + } + if err := s.CreateUser(ctx, original); err != nil { + t.Fatalf("failed to create user: %v", err) + } + + info := &ExternalUserInfo{ + Email: "backfill@example.com", + DisplayName: "Backfilled Name", + AvatarURL: "https://example.com/backfilled.png", + } + + user, err := srv.provisionUser(ctx, info) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + + if user.DisplayName != "Backfilled Name" { + t.Errorf("expected backfilled display name, got %q", user.DisplayName) + } + if user.AvatarURL != "https://example.com/backfilled.png" { + t.Errorf("expected backfilled avatar URL, got %q", user.AvatarURL) + } + }) + + t.Run("promotes member to admin when config changes", func(t *testing.T) { + srv, s := testServer(t) + + // Pre-create user as member + original := &store.User{ + ID: generateID(), + Email: "admin@example.com", + Role: "member", + Status: "active", + Created: time.Now(), + } + if err := s.CreateUser(ctx, original); err != nil { + t.Fatalf("failed to create user: %v", err) + } + + // Configure server to recognize this email as admin + srv.config.AdminEmails = []string{"admin@example.com"} + + info := &ExternalUserInfo{Email: "admin@example.com"} + user, err := srv.provisionUser(ctx, info) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + + if user.Role != "admin" { + t.Errorf("expected role 'admin', got %q", user.Role) + } + }) + + t.Run("returns ErrAccessDenied for unauthorized domain", func(t *testing.T) { + srv, _ := testServer(t) + + // Configure domain restriction + srv.config.AuthorizedDomains = []string{"allowed.com"} + srv.config.UserAccessMode = "domain_restricted" + + info := &ExternalUserInfo{Email: "user@forbidden.com"} + _, err := srv.provisionUser(ctx, info) + if !errors.Is(err, ErrAccessDenied) { + t.Errorf("expected ErrAccessDenied, got %v", err) + } + }) + + t.Run("returns ErrAccessDenied for invite-only mode", func(t *testing.T) { + srv, _ := testServer(t) + + // Configure invite-only mode (user not on allow list) + srv.config.UserAccessMode = "invite_only" + + info := &ExternalUserInfo{Email: "user@example.com"} + _, err := srv.provisionUser(ctx, info) + if !errors.Is(err, ErrAccessDenied) { + t.Errorf("expected ErrAccessDenied, got %v", err) + } + }) + + t.Run("admin bypasses domain restriction", func(t *testing.T) { + srv, _ := testServer(t) + + // Configure domain restriction but also add admin email + srv.config.AuthorizedDomains = []string{"allowed.com"} + srv.config.UserAccessMode = "domain_restricted" + srv.config.AdminEmails = []string{"admin@other.com"} + + info := &ExternalUserInfo{Email: "admin@other.com"} + user, err := srv.provisionUser(ctx, info) + if err != nil { + t.Fatalf("expected admin bypass, got error: %v", err) + } + if user.Role != "admin" { + t.Errorf("expected role 'admin', got %q", user.Role) + } + }) + + t.Run("idempotent - calling twice does not duplicate", func(t *testing.T) { + srv, s := testServer(t) + + info := &ExternalUserInfo{ + Email: "idempotent@example.com", + DisplayName: "First Call", + } + + user1, err := srv.provisionUser(ctx, info) + if err != nil { + t.Fatalf("first call failed: %v", err) + } + + user2, err := srv.provisionUser(ctx, info) + if err != nil { + t.Fatalf("second call failed: %v", err) + } + + if user1.ID != user2.ID { + t.Errorf("expected same user ID across calls, got %q and %q", user1.ID, user2.ID) + } + + // Verify only one user exists + u, err := s.GetUserByEmail(ctx, "idempotent@example.com") + if err != nil { + t.Fatalf("user not found: %v", err) + } + if u.ID != user1.ID { + t.Error("store user ID does not match") + } + }) +} diff --git a/pkg/hub/handlers_authz_remediation_test.go b/pkg/hub/handlers_authz_remediation_test.go index 9e7ec0ad8..0a86fa4c7 100644 --- a/pkg/hub/handlers_authz_remediation_test.go +++ b/pkg/hub/handlers_authz_remediation_test.go @@ -34,7 +34,7 @@ func grantUserActionOnResource(t *testing.T, s store.Store, userID, resourceType ctx := context.Background() policy := &store.Policy{ - ID: "policy-" + userID + "-" + resourceType + "-" + resourceID + "-" + string(action), + ID: tid("policy-" + userID + "-" + resourceType + "-" + resourceID + "-" + string(action)), Name: "Allow " + string(action) + " on " + resourceType + " " + resourceID, ScopeType: store.PolicyScopeHub, ResourceType: resourceType, @@ -57,7 +57,7 @@ func TestAuthzRemediation_ListEndpointsFilterUnauthorizedItems(t *testing.T) { ctx := context.Background() member := &store.User{ - ID: "member-list-authz", + ID: tid("member-list-authz"), Email: "member-list-authz@example.com", DisplayName: "Member List Authz", Role: store.UserRoleMember, @@ -66,8 +66,12 @@ func TestAuthzRemediation_ListEndpointsFilterUnauthorizedItems(t *testing.T) { } require.NoError(t, s.CreateUser(ctx, member)) + // The projects/agents below are owned by this user; agent owner_id is an FK + // to the users table, so the owner must exist. + permSeedUser(t, ctx, s, tid("owner-outside-user")) + visibleUser := &store.User{ - ID: "visible-user-authz", + ID: tid("visible-user-authz"), Email: "visible-user-authz@example.com", DisplayName: "Visible User", Role: store.UserRoleMember, @@ -77,7 +81,7 @@ func TestAuthzRemediation_ListEndpointsFilterUnauthorizedItems(t *testing.T) { require.NoError(t, s.CreateUser(ctx, visibleUser)) hiddenUser := &store.User{ - ID: "hidden-user-authz", + ID: tid("hidden-user-authz"), Email: "hidden-user-authz@example.com", DisplayName: "Hidden User", Role: store.UserRoleMember, @@ -87,61 +91,63 @@ func TestAuthzRemediation_ListEndpointsFilterUnauthorizedItems(t *testing.T) { require.NoError(t, s.CreateUser(ctx, hiddenUser)) visibleProject := &store.Project{ - ID: "project-visible-authz", - Slug: "project-visible-authz", + ID: tid("project-visible-authz"), + Slug: tid("project-visible-authz"), Name: "Visible Project", - OwnerID: "owner-outside-user", - CreatedBy: "owner-outside-user", + OwnerID: tid("owner-outside-user"), + CreatedBy: tid("owner-outside-user"), Created: time.Now(), Updated: time.Now(), } require.NoError(t, s.CreateProject(ctx, visibleProject)) hiddenProject := &store.Project{ - ID: "project-hidden-authz", - Slug: "project-hidden-authz", + ID: tid("project-hidden-authz"), + Slug: tid("project-hidden-authz"), Name: "Hidden Project", - OwnerID: "owner-outside-user", - CreatedBy: "owner-outside-user", + OwnerID: tid("owner-outside-user"), + CreatedBy: tid("owner-outside-user"), Created: time.Now(), Updated: time.Now(), } require.NoError(t, s.CreateProject(ctx, hiddenProject)) visibleBroker := &store.RuntimeBroker{ - ID: "broker-visible-authz", + ID: tid("broker-visible-authz"), Name: "Visible Broker", + Slug: "broker-visible-authz", Endpoint: "http://broker-visible", Status: store.BrokerStatusOnline, - CreatedBy: "owner-outside-user", + CreatedBy: tid("owner-outside-user"), } require.NoError(t, s.CreateRuntimeBroker(ctx, visibleBroker)) hiddenBroker := &store.RuntimeBroker{ - ID: "broker-hidden-authz", + ID: tid("broker-hidden-authz"), Name: "Hidden Broker", + Slug: "broker-hidden-authz", Endpoint: "http://broker-hidden", Status: store.BrokerStatusOnline, - CreatedBy: "owner-outside-user", + CreatedBy: tid("owner-outside-user"), } require.NoError(t, s.CreateRuntimeBroker(ctx, hiddenBroker)) visibleAgent := &store.Agent{ - ID: "agent-visible-authz", - Slug: "agent-visible-authz", + ID: tid("agent-visible-authz"), + Slug: tid("agent-visible-authz"), Name: "Visible Agent", ProjectID: visibleProject.ID, - OwnerID: "owner-outside-user", + OwnerID: tid("owner-outside-user"), Phase: string(state.PhaseRunning), } require.NoError(t, s.CreateAgent(ctx, visibleAgent)) hiddenAgent := &store.Agent{ - ID: "agent-hidden-authz", - Slug: "agent-hidden-authz", + ID: tid("agent-hidden-authz"), + Slug: tid("agent-hidden-authz"), Name: "Hidden Agent", ProjectID: hiddenProject.ID, - OwnerID: "owner-outside-user", + OwnerID: tid("owner-outside-user"), Phase: string(state.PhaseRunning), } require.NoError(t, s.CreateAgent(ctx, hiddenAgent)) @@ -189,7 +195,7 @@ func TestAuthzRemediation_AgentAndWorkspaceRoutesEnforceResourcePermissions(t *t ctx := context.Background() member := &store.User{ - ID: "member-workspace-authz", + ID: tid("member-workspace-authz"), Email: "member-workspace-authz@example.com", DisplayName: "Member Workspace Authz", Role: store.UserRoleMember, @@ -198,23 +204,27 @@ func TestAuthzRemediation_AgentAndWorkspaceRoutesEnforceResourcePermissions(t *t } require.NoError(t, s.CreateUser(ctx, member)) + // The project/agent below are owned by this user; agent owner_id is an FK + // to the users table, so the owner must exist. + permSeedUser(t, ctx, s, tid("owner-outside-user")) + project := &store.Project{ - ID: "project-workspace-authz", - Slug: "project-workspace-authz", + ID: tid("project-workspace-authz"), + Slug: tid("project-workspace-authz"), Name: "Workspace Project", - OwnerID: "owner-outside-user", - CreatedBy: "owner-outside-user", + OwnerID: tid("owner-outside-user"), + CreatedBy: tid("owner-outside-user"), Created: time.Now(), Updated: time.Now(), } require.NoError(t, s.CreateProject(ctx, project)) agent := &store.Agent{ - ID: "agent-workspace-authz", - Slug: "agent-workspace-authz", + ID: tid("agent-workspace-authz"), + Slug: tid("agent-workspace-authz"), Name: "Workspace Agent", ProjectID: project.ID, - OwnerID: "owner-outside-user", + OwnerID: tid("owner-outside-user"), Phase: string(state.PhaseStopped), } require.NoError(t, s.CreateAgent(ctx, agent)) diff --git a/pkg/hub/handlers_broker_inbound.go b/pkg/hub/handlers_broker_inbound.go index c8da35b10..ab9e9fa72 100644 --- a/pkg/hub/handlers_broker_inbound.go +++ b/pkg/hub/handlers_broker_inbound.go @@ -15,6 +15,7 @@ package hub import ( + "errors" "fmt" "net/http" "strings" @@ -103,7 +104,11 @@ func (s *Server) handleBrokerInbound(w http.ResponseWriter, r *http.Request) { return } - if err := dispatcher.DispatchAgentMessage(r.Context(), agent, req.Message.Msg, req.Message.Urgent, req.Message); err != nil { + if err := dispatcher.DispatchAgentMessage(r.Context(), agent, req.Message.Msg, req.Message.Urgent, req.Message); errors.Is(err, ErrMessageDeferred) { + s.signalDeferredMessage(r.Context(), agent.RuntimeBrokerID, agent.ID) + w.WriteHeader(http.StatusAccepted) + return + } else if err != nil { log.Error("Failed to dispatch inbound message", "agent_id", agent.ID, "agent_slug", agentSlug, "error", err) writeError(w, http.StatusBadGateway, ErrCodeRuntimeError, diff --git a/pkg/hub/handlers_broker_test.go b/pkg/hub/handlers_broker_test.go index 2cbfbe2b9..473e50d92 100644 --- a/pkg/hub/handlers_broker_test.go +++ b/pkg/hub/handlers_broker_test.go @@ -42,7 +42,7 @@ func setupBrokerAuthzTest(t *testing.T) (srv *Server, s store.Store, alice, bob, ctx := context.Background() alice = &store.User{ - ID: "user-broker-alice", + ID: tid("user-broker-alice"), Email: "broker-alice@test.com", DisplayName: "Alice", Role: store.UserRoleMember, @@ -52,7 +52,7 @@ func setupBrokerAuthzTest(t *testing.T) (srv *Server, s store.Store, alice, bob, require.NoError(t, s.CreateUser(ctx, alice)) bob = &store.User{ - ID: "user-broker-bob", + ID: tid("user-broker-bob"), Email: "broker-bob@test.com", DisplayName: "Bob", Role: store.UserRoleMember, @@ -62,7 +62,7 @@ func setupBrokerAuthzTest(t *testing.T) (srv *Server, s store.Store, alice, bob, require.NoError(t, s.CreateUser(ctx, bob)) admin = &store.User{ - ID: "user-broker-admin", + ID: tid("user-broker-admin"), Email: "broker-admin@test.com", DisplayName: "Admin", Role: store.UserRoleAdmin, @@ -77,7 +77,7 @@ func setupBrokerAuthzTest(t *testing.T) (srv *Server, s store.Store, alice, bob, // Create a project owned by alice project = &store.Project{ - ID: "project-broker-test", + ID: tid("project-broker-test"), Name: "Broker Test Project", Slug: "broker-test-project", OwnerID: alice.ID, @@ -100,7 +100,7 @@ func setupBrokerAuthzTest(t *testing.T) (srv *Server, s store.Store, alice, bob, // Create a broker owned by alice directly in the store broker = &store.RuntimeBroker{ - ID: "broker-alice-owned", + ID: tid("broker-alice-owned"), Name: "Alice Broker", Slug: "alice-broker", Status: store.BrokerStatusOnline, @@ -361,7 +361,7 @@ func TestAgentCreate_BrokerResolution(t *testing.T) { // Create a runtime broker broker := &store.RuntimeBroker{ - ID: "broker_id_123", + ID: tid("broker_id_123"), Name: "My Laptop", Slug: "my-laptop", Status: store.BrokerStatusOnline, @@ -370,7 +370,7 @@ func TestAgentCreate_BrokerResolution(t *testing.T) { // Create a project project := &store.Project{ - ID: "project_1", + ID: tid("project_1"), Slug: "test-project", Name: "Test Project", Created: time.Now(), @@ -391,14 +391,14 @@ func TestAgentCreate_BrokerResolution(t *testing.T) { body := map[string]interface{}{ "name": "Agent ID", "projectId": project.ID, - "runtimeBrokerId": "broker_id_123", + "runtimeBrokerId": tid("broker_id_123"), } rec := doRequest(t, srv, http.MethodPost, "/api/v1/agents", body) assert.Equal(t, http.StatusCreated, rec.Code) var resp CreateAgentResponse require.NoError(t, json.NewDecoder(rec.Body).Decode(&resp)) - assert.Equal(t, "broker_id_123", resp.Agent.RuntimeBrokerID) + assert.Equal(t, tid("broker_id_123"), resp.Agent.RuntimeBrokerID) }) t.Run("Resolve by Name", func(t *testing.T) { @@ -412,7 +412,7 @@ func TestAgentCreate_BrokerResolution(t *testing.T) { var resp CreateAgentResponse require.NoError(t, json.NewDecoder(rec.Body).Decode(&resp)) - assert.Equal(t, "broker_id_123", resp.Agent.RuntimeBrokerID) + assert.Equal(t, tid("broker_id_123"), resp.Agent.RuntimeBrokerID) }) t.Run("Resolve by Slug", func(t *testing.T) { @@ -426,7 +426,7 @@ func TestAgentCreate_BrokerResolution(t *testing.T) { var resp CreateAgentResponse require.NoError(t, json.NewDecoder(rec.Body).Decode(&resp)) - assert.Equal(t, "broker_id_123", resp.Agent.RuntimeBrokerID) + assert.Equal(t, tid("broker_id_123"), resp.Agent.RuntimeBrokerID) }) t.Run("Invalid broker", func(t *testing.T) { diff --git a/pkg/hub/handlers_envsecret_authz_test.go b/pkg/hub/handlers_envsecret_authz_test.go index f4f6390f3..a6543d25e 100644 --- a/pkg/hub/handlers_envsecret_authz_test.go +++ b/pkg/hub/handlers_envsecret_authz_test.go @@ -85,7 +85,7 @@ func TestEnvVar_UserScope_MemberAccess(t *testing.T) { ctx := context.Background() member := &store.User{ - ID: "member-env-1", + ID: tid("member-env-1"), Email: "member-env@example.com", DisplayName: "Test Member", Role: store.UserRoleMember, @@ -107,7 +107,7 @@ func TestEnvVar_UserScope_MemberAccess(t *testing.T) { t.Fatalf("failed to decode response: %v", err) } - if resp.ScopeID != "member-env-1" { + if resp.ScopeID != tid("member-env-1") { t.Errorf("expected scopeId 'member-env-1', got %q", resp.ScopeID) } } @@ -177,11 +177,11 @@ func TestEnvVar_UserScope_MemberIsolation(t *testing.T) { ctx := context.Background() userA := &store.User{ - ID: "user-iso-a", Email: "a@example.com", DisplayName: "User A", + ID: tid("user-iso-a"), Email: "a@example.com", DisplayName: "User A", Role: store.UserRoleMember, Status: "active", Created: time.Now(), } userB := &store.User{ - ID: "user-iso-b", Email: "b@example.com", DisplayName: "User B", + ID: tid("user-iso-b"), Email: "b@example.com", DisplayName: "User B", Role: store.UserRoleMember, Status: "active", Created: time.Now(), } if err := s.CreateUser(ctx, userA); err != nil { @@ -230,7 +230,7 @@ func TestEnvVar_ProjectScope_OwnerAccess(t *testing.T) { ctx := context.Background() owner := &store.User{ - ID: "project-owner-1", Email: "owner@example.com", DisplayName: "Owner", + ID: tid("project-owner-1"), Email: "owner@example.com", DisplayName: "Owner", Role: store.UserRoleMember, Status: "active", Created: time.Now(), } if err := s.CreateUser(ctx, owner); err != nil { @@ -238,10 +238,10 @@ func TestEnvVar_ProjectScope_OwnerAccess(t *testing.T) { } project := &store.Project{ - ID: "project_env_owner", + ID: tid("project_env_owner"), Name: "Owner Test Project", Slug: "owner-test-project", - OwnerID: "project-owner-1", + OwnerID: tid("project-owner-1"), Created: time.Now(), Updated: time.Now(), } @@ -268,7 +268,7 @@ func TestEnvVar_ProjectScope_NonOwnerDenied(t *testing.T) { ctx := context.Background() nonOwner := &store.User{ - ID: "non-owner-1", Email: "nonowner@example.com", DisplayName: "Non-Owner", + ID: tid("non-owner-1"), Email: "nonowner@example.com", DisplayName: "Non-Owner", Role: store.UserRoleMember, Status: "active", Created: time.Now(), } if err := s.CreateUser(ctx, nonOwner); err != nil { @@ -276,7 +276,7 @@ func TestEnvVar_ProjectScope_NonOwnerDenied(t *testing.T) { } project := &store.Project{ - ID: "project_env_notown", + ID: tid("project_env_notown"), Name: "Not Owned Project", Slug: "not-owned-project", OwnerID: "someone-else", @@ -299,7 +299,7 @@ func TestEnvVar_ProjectScope_AdminAccess(t *testing.T) { ctx := context.Background() project := &store.Project{ - ID: "project_env_admin", + ID: tid("project_env_admin"), Name: "Admin Test Project", Slug: "admin-test-project", OwnerID: "someone-else", @@ -322,7 +322,7 @@ func TestEnvVar_ProjectScope_AgentReadOwnProject(t *testing.T) { ctx := context.Background() project := &store.Project{ - ID: "project_agent_env", + ID: tid("project_agent_env"), Name: "Agent Project", Slug: "agent-project", Created: time.Now(), @@ -333,7 +333,7 @@ func TestEnvVar_ProjectScope_AgentReadOwnProject(t *testing.T) { } agent := &store.Agent{ - ID: "agent_env_test", + ID: tid("agent_env_test"), Slug: "env-test-agent", Name: "Env Test Agent", ProjectID: project.ID, @@ -363,11 +363,11 @@ func TestEnvVar_ProjectScope_AgentOtherProjectDenied(t *testing.T) { ctx := context.Background() project1 := &store.Project{ - ID: "project_agent_own", Name: "Agent's Project", Slug: "agents-project", + ID: tid("project_agent_own"), Name: "Agent's Project", Slug: "agents-project", Created: time.Now(), Updated: time.Now(), } project2 := &store.Project{ - ID: "project_agent_other", Name: "Other Project", Slug: "other-project", + ID: tid("project_agent_other"), Name: "Other Project", Slug: "other-project", Created: time.Now(), Updated: time.Now(), } if err := s.CreateProject(ctx, project1); err != nil { @@ -378,7 +378,7 @@ func TestEnvVar_ProjectScope_AgentOtherProjectDenied(t *testing.T) { } agent := &store.Agent{ - ID: "agent_other_project", Slug: "other-project-agent", Name: "Other Project Agent", + ID: tid("agent_other_project"), Slug: "other-project-agent", Name: "Other Project Agent", ProjectID: project1.ID, Phase: string(state.PhaseRunning), StateVersion: 1, Created: time.Now(), Updated: time.Now(), } @@ -403,7 +403,7 @@ func TestEnvVar_ProjectScope_AgentWriteDenied(t *testing.T) { ctx := context.Background() project := &store.Project{ - ID: "project_agent_nowrite", Name: "Agent No Write Project", Slug: "agent-nowrite-project", + ID: tid("project_agent_nowrite"), Name: "Agent No Write Project", Slug: "agent-nowrite-project", Created: time.Now(), Updated: time.Now(), } if err := s.CreateProject(ctx, project); err != nil { @@ -411,7 +411,7 @@ func TestEnvVar_ProjectScope_AgentWriteDenied(t *testing.T) { } agent := &store.Agent{ - ID: "agent_nowrite", Slug: "nowrite-agent", Name: "No Write Agent", + ID: tid("agent_nowrite"), Slug: "nowrite-agent", Name: "No Write Agent", ProjectID: project.ID, Phase: string(state.PhaseRunning), StateVersion: 1, Created: time.Now(), Updated: time.Now(), } @@ -441,7 +441,7 @@ func TestEnvVar_BrokerScope_AdminAccess(t *testing.T) { ctx := context.Background() broker := &store.RuntimeBroker{ - ID: "broker_env_admin", Name: "Env Admin Broker", Slug: "env-admin-broker", + ID: tid("broker_env_admin"), Name: "Env Admin Broker", Slug: "env-admin-broker", Status: store.BrokerStatusOnline, Created: time.Now(), Updated: time.Now(), } if err := s.CreateRuntimeBroker(ctx, broker); err != nil { @@ -486,7 +486,7 @@ func TestSecret_UserScope_AdminDoesNotSeeOtherUserSecrets(t *testing.T) { // Create a member user and store a secret scoped to them. member := &store.User{ - ID: "member-other-1", Email: "other@example.com", DisplayName: "Other User", + ID: tid("member-other-1"), Email: "other@example.com", DisplayName: "Other User", Role: store.UserRoleMember, Status: "active", Created: time.Now(), } if err := s.CreateUser(ctx, member); err != nil { @@ -495,7 +495,7 @@ func TestSecret_UserScope_AdminDoesNotSeeOtherUserSecrets(t *testing.T) { // Store a secret as the member. if err := s.CreateSecret(ctx, &store.Secret{ - ID: "sec-other-1", Key: "MEMBER_KEY", Scope: store.ScopeUser, + ID: tid("sec-other-1"), Key: "MEMBER_KEY", Scope: store.ScopeUser, ScopeID: member.ID, SecretType: store.SecretTypeEnvironment, EncryptedValue: "val", Version: 1, Created: time.Now(), Updated: time.Now(), }); err != nil { @@ -526,7 +526,7 @@ func TestSecret_UserScope_MemberAccess(t *testing.T) { ctx := context.Background() member := &store.User{ - ID: "member-sec-1", Email: "member-sec@example.com", DisplayName: "Test Member", + ID: tid("member-sec-1"), Email: "member-sec@example.com", DisplayName: "Test Member", Role: store.UserRoleMember, Status: "active", Created: time.Now(), } if err := s.CreateUser(ctx, member); err != nil { @@ -543,7 +543,7 @@ func TestSecret_UserScope_MemberAccess(t *testing.T) { t.Fatalf("failed to decode response: %v", err) } - if resp.ScopeID != "member-sec-1" { + if resp.ScopeID != tid("member-sec-1") { t.Errorf("expected scopeId 'member-sec-1', got %q", resp.ScopeID) } } @@ -590,7 +590,7 @@ func TestSecret_ProjectScope_OwnerAccess(t *testing.T) { ctx := context.Background() owner := &store.User{ - ID: "project-sec-owner", Email: "secowner@example.com", DisplayName: "Owner", + ID: tid("project-sec-owner"), Email: "secowner@example.com", DisplayName: "Owner", Role: store.UserRoleMember, Status: "active", Created: time.Now(), } if err := s.CreateUser(ctx, owner); err != nil { @@ -598,8 +598,8 @@ func TestSecret_ProjectScope_OwnerAccess(t *testing.T) { } project := &store.Project{ - ID: "project_sec_owner", Name: "Secret Owner Project", Slug: "secret-owner-project", - OwnerID: "project-sec-owner", Created: time.Now(), Updated: time.Now(), + ID: tid("project_sec_owner"), Name: "Secret Owner Project", Slug: "secret-owner-project", + OwnerID: tid("project-sec-owner"), Created: time.Now(), Updated: time.Now(), } if err := s.CreateProject(ctx, project); err != nil { t.Fatalf("failed to create project: %v", err) @@ -617,7 +617,7 @@ func TestSecret_ProjectScope_NonOwnerDenied(t *testing.T) { ctx := context.Background() nonOwner := &store.User{ - ID: "non-sec-owner", Email: "nonsecowner@example.com", DisplayName: "Non-Owner", + ID: tid("non-sec-owner"), Email: "nonsecowner@example.com", DisplayName: "Non-Owner", Role: store.UserRoleMember, Status: "active", Created: time.Now(), } if err := s.CreateUser(ctx, nonOwner); err != nil { @@ -625,7 +625,7 @@ func TestSecret_ProjectScope_NonOwnerDenied(t *testing.T) { } project := &store.Project{ - ID: "project_sec_notown", Name: "Not Owned Secret Project", Slug: "not-owned-secret-project", + ID: tid("project_sec_notown"), Name: "Not Owned Secret Project", Slug: "not-owned-secret-project", OwnerID: "someone-else", Created: time.Now(), Updated: time.Now(), } if err := s.CreateProject(ctx, project); err != nil { @@ -644,7 +644,7 @@ func TestSecret_ProjectScope_AgentReadOwnProject(t *testing.T) { ctx := context.Background() project := &store.Project{ - ID: "project_agent_sec", Name: "Agent Secret Project", Slug: "agent-secret-project", + ID: tid("project_agent_sec"), Name: "Agent Secret Project", Slug: "agent-secret-project", Created: time.Now(), Updated: time.Now(), } if err := s.CreateProject(ctx, project); err != nil { @@ -652,7 +652,7 @@ func TestSecret_ProjectScope_AgentReadOwnProject(t *testing.T) { } agent := &store.Agent{ - ID: "agent_sec_test", Slug: "sec-test-agent", Name: "Secret Test Agent", + ID: tid("agent_sec_test"), Slug: "sec-test-agent", Name: "Secret Test Agent", ProjectID: project.ID, Phase: string(state.PhaseRunning), StateVersion: 1, Created: time.Now(), Updated: time.Now(), } @@ -677,7 +677,7 @@ func TestSecret_ProjectScope_AgentWriteDenied(t *testing.T) { ctx := context.Background() project := &store.Project{ - ID: "project_agent_sec_nowrite", Name: "Agent Secret No Write", Slug: "agent-sec-nowrite-project", + ID: tid("project_agent_sec_nowrite"), Name: "Agent Secret No Write", Slug: "agent-sec-nowrite-project", Created: time.Now(), Updated: time.Now(), } if err := s.CreateProject(ctx, project); err != nil { @@ -685,7 +685,7 @@ func TestSecret_ProjectScope_AgentWriteDenied(t *testing.T) { } agent := &store.Agent{ - ID: "agent_sec_nowrite", Slug: "sec-nowrite-agent", Name: "Secret No Write Agent", + ID: tid("agent_sec_nowrite"), Slug: "sec-nowrite-agent", Name: "Secret No Write Agent", ProjectID: project.ID, Phase: string(state.PhaseRunning), StateVersion: 1, Created: time.Now(), Updated: time.Now(), } @@ -714,7 +714,7 @@ func TestEnvVar_HubEndpoint_ProjectScope_Authorized(t *testing.T) { ctx := context.Background() project := &store.Project{ - ID: "project_hub_env", Name: "Hub Env Project", Slug: "hub-env-project", + ID: tid("project_hub_env"), Name: "Hub Env Project", Slug: "hub-env-project", Created: time.Now(), Updated: time.Now(), } if err := s.CreateProject(ctx, project); err != nil { @@ -733,7 +733,7 @@ func TestEnvVar_HubEndpoint_ProjectScope_NonOwnerDenied(t *testing.T) { ctx := context.Background() member := &store.User{ - ID: "hub-env-member", Email: "hubenvmember@example.com", DisplayName: "Member", + ID: tid("hub-env-member"), Email: "hubenvmember@example.com", DisplayName: "Member", Role: store.UserRoleMember, Status: "active", Created: time.Now(), } if err := s.CreateUser(ctx, member); err != nil { @@ -741,7 +741,7 @@ func TestEnvVar_HubEndpoint_ProjectScope_NonOwnerDenied(t *testing.T) { } project := &store.Project{ - ID: "project_hub_env_deny", Name: "Hub Env Deny Project", Slug: "hub-env-deny-project", + ID: tid("project_hub_env_deny"), Name: "Hub Env Deny Project", Slug: "hub-env-deny-project", OwnerID: "someone-else", Created: time.Now(), Updated: time.Now(), } if err := s.CreateProject(ctx, project); err != nil { @@ -801,7 +801,7 @@ func TestEnvVar_UnifiedList_MergesSecrets(t *testing.T) { // Create a secret directly in the store with type "environment" if err := s.CreateSecret(ctx, &store.Secret{ - ID: "sec-env-1", + ID: tid("sec-env-1"), Key: "SECRET_ENV_VAR", EncryptedValue: "encrypted-val", SecretType: store.SecretTypeEnvironment, @@ -864,7 +864,7 @@ func TestEnvVar_UnifiedList_Deduplication(t *testing.T) { // Also create a secret with the same key if err := s.CreateSecret(ctx, &store.Secret{ - ID: "sec-dup-1", + ID: tid("sec-dup-1"), Key: "DUPED_KEY", EncryptedValue: "secret-value", SecretType: store.SecretTypeEnvironment, @@ -905,7 +905,7 @@ func TestEnvVar_FallbackGet_FromSecretBackend(t *testing.T) { // Create a secret (no plain env var) if err := s.CreateSecret(ctx, &store.Secret{ - ID: "sec-get-1", + ID: tid("sec-get-1"), Key: "ONLY_SECRET", EncryptedValue: "secret-val", SecretType: store.SecretTypeEnvironment, @@ -946,7 +946,7 @@ func TestEnvVar_FallbackDelete_FromSecretBackend(t *testing.T) { // Create a secret (no plain env var) if err := s.CreateSecret(ctx, &store.Secret{ - ID: "sec-del-1", + ID: tid("sec-del-1"), Key: "DEL_SECRET", EncryptedValue: "secret-val", SecretType: store.SecretTypeEnvironment, @@ -1010,7 +1010,7 @@ func TestEnvVar_NonEnvironmentSecrets_NotMerged(t *testing.T) { // Create a secret with type "variable" (not "environment") if err := s.CreateSecret(ctx, &store.Secret{ - ID: "sec-var-1", + ID: tid("sec-var-1"), Key: "VARIABLE_SECRET", EncryptedValue: "var-val", SecretType: store.SecretTypeVariable, @@ -1043,7 +1043,7 @@ func TestEnvVar_ProjectScope_SecretPromotion_Succeeds(t *testing.T) { ctx := context.Background() project := &store.Project{ - ID: "project_promo_test", Name: "Promo Project", Slug: "promo-project", + ID: tid("project_promo_test"), Name: "Promo Project", Slug: "promo-project", OwnerID: DevUserID, Created: time.Now(), Updated: time.Now(), } if err := s.CreateProject(ctx, project); err != nil { @@ -1063,7 +1063,7 @@ func TestEnvVar_ProjectScope_UnifiedList(t *testing.T) { ctx := context.Background() project := &store.Project{ - ID: "project_unified_list", Name: "Unified Project", Slug: "unified-project", + ID: tid("project_unified_list"), Name: "Unified Project", Slug: "unified-project", OwnerID: DevUserID, Created: time.Now(), Updated: time.Now(), } if err := s.CreateProject(ctx, project); err != nil { @@ -1079,7 +1079,7 @@ func TestEnvVar_ProjectScope_UnifiedList(t *testing.T) { // Create an environment secret in the project scope directly if err := s.CreateSecret(ctx, &store.Secret{ - ID: "sec-project-env-1", + ID: tid("sec-project-env-1"), Key: "GROVE_SECRET_VAR", EncryptedValue: "project-secret-val", SecretType: store.SecretTypeEnvironment, @@ -1123,7 +1123,7 @@ func TestEnvVar_ProjectScope_FallbackGet(t *testing.T) { ctx := context.Background() project := &store.Project{ - ID: "project_fallback_get", Name: "Fallback Get Project", Slug: "fallback-get-project", + ID: tid("project_fallback_get"), Name: "Fallback Get Project", Slug: "fallback-get-project", OwnerID: DevUserID, Created: time.Now(), Updated: time.Now(), } if err := s.CreateProject(ctx, project); err != nil { @@ -1131,7 +1131,7 @@ func TestEnvVar_ProjectScope_FallbackGet(t *testing.T) { } if err := s.CreateSecret(ctx, &store.Secret{ - ID: "sec-project-fb-1", + ID: tid("sec-project-fb-1"), Key: "GROVE_ONLY_SEC", EncryptedValue: "secret-val", SecretType: store.SecretTypeEnvironment, @@ -1160,7 +1160,7 @@ func TestEnvVar_ProjectScope_FallbackDelete(t *testing.T) { ctx := context.Background() project := &store.Project{ - ID: "project_fallback_del", Name: "Fallback Del Project", Slug: "fallback-del-project", + ID: tid("project_fallback_del"), Name: "Fallback Del Project", Slug: "fallback-del-project", OwnerID: DevUserID, Created: time.Now(), Updated: time.Now(), } if err := s.CreateProject(ctx, project); err != nil { @@ -1168,7 +1168,7 @@ func TestEnvVar_ProjectScope_FallbackDelete(t *testing.T) { } if err := s.CreateSecret(ctx, &store.Secret{ - ID: "sec-project-del-1", + ID: tid("sec-project-del-1"), Key: "GROVE_DEL_SEC", EncryptedValue: "secret-val", SecretType: store.SecretTypeEnvironment, @@ -1260,7 +1260,7 @@ func TestEnvVar_HubScope_MemberReadForbidden(t *testing.T) { ctx := context.Background() member := &store.User{ - ID: "hub-env-member-1", Email: "hub-member@example.com", DisplayName: "Hub Member", + ID: tid("hub-env-member-1"), Email: "hub-member@example.com", DisplayName: "Hub Member", Role: store.UserRoleMember, Status: "active", Created: time.Now(), } if err := s.CreateUser(ctx, member); err != nil { @@ -1286,7 +1286,7 @@ func TestEnvVar_HubScope_MemberWriteForbidden(t *testing.T) { ctx := context.Background() member := &store.User{ - ID: "hub-env-member-2", Email: "hub-member2@example.com", DisplayName: "Hub Member 2", + ID: tid("hub-env-member-2"), Email: "hub-member2@example.com", DisplayName: "Hub Member 2", Role: store.UserRoleMember, Status: "active", Created: time.Now(), } if err := s.CreateUser(ctx, member); err != nil { @@ -1306,7 +1306,7 @@ func TestEnvVar_HubScope_MemberDeleteForbidden(t *testing.T) { ctx := context.Background() member := &store.User{ - ID: "hub-env-member-3", Email: "hub-member3@example.com", DisplayName: "Hub Member 3", + ID: tid("hub-env-member-3"), Email: "hub-member3@example.com", DisplayName: "Hub Member 3", Role: store.UserRoleMember, Status: "active", Created: time.Now(), } if err := s.CreateUser(ctx, member); err != nil { @@ -1332,7 +1332,7 @@ func TestEnvVar_HubScope_AgentCanRead(t *testing.T) { ctx := context.Background() project := &store.Project{ - ID: "project_hub_agent", Name: "Hub Agent Project", Slug: "hub-agent-project", + ID: tid("project_hub_agent"), Name: "Hub Agent Project", Slug: "hub-agent-project", Created: time.Now(), Updated: time.Now(), } if err := s.CreateProject(ctx, project); err != nil { @@ -1340,7 +1340,7 @@ func TestEnvVar_HubScope_AgentCanRead(t *testing.T) { } agent := &store.Agent{ - ID: "agent_hub_read", Slug: "hub-read-agent", Name: "Hub Read Agent", + ID: tid("agent_hub_read"), Slug: "hub-read-agent", Name: "Hub Read Agent", ProjectID: project.ID, Phase: string(state.PhaseRunning), StateVersion: 1, Created: time.Now(), Updated: time.Now(), } @@ -1405,7 +1405,7 @@ func TestSecret_HubScope_MemberReadForbidden(t *testing.T) { ctx := context.Background() member := &store.User{ - ID: "hub-sec-member-1", Email: "hubsecmember@example.com", DisplayName: "Hub Sec Member", + ID: tid("hub-sec-member-1"), Email: "hubsecmember@example.com", DisplayName: "Hub Sec Member", Role: store.UserRoleMember, Status: "active", Created: time.Now(), } if err := s.CreateUser(ctx, member); err != nil { @@ -1432,7 +1432,7 @@ func TestSecret_HubScope_MemberWriteForbidden(t *testing.T) { ctx := context.Background() member := &store.User{ - ID: "hub-sec-member-2", Email: "hubsecmember2@example.com", DisplayName: "Hub Sec Member 2", + ID: tid("hub-sec-member-2"), Email: "hubsecmember2@example.com", DisplayName: "Hub Sec Member 2", Role: store.UserRoleMember, Status: "active", Created: time.Now(), } if err := s.CreateUser(ctx, member); err != nil { diff --git a/pkg/hub/handlers_gcp_identity_test.go b/pkg/hub/handlers_gcp_identity_test.go index 4e7a90e15..ccbdd8966 100644 --- a/pkg/hub/handlers_gcp_identity_test.go +++ b/pkg/hub/handlers_gcp_identity_test.go @@ -562,7 +562,7 @@ func setupGCPAuthzTest(t *testing.T) (*Server, store.Store, *store.User, *store. ctx := context.Background() owner := &store.User{ - ID: "user-gcp-owner", + ID: tid("user-gcp-owner"), Email: "gcp-owner@test.com", DisplayName: "GCP Owner", Role: store.UserRoleMember, @@ -570,7 +570,7 @@ func setupGCPAuthzTest(t *testing.T) (*Server, store.Store, *store.User, *store. Created: time.Now(), } member := &store.User{ - ID: "user-gcp-member", + ID: tid("user-gcp-member"), Email: "gcp-member@test.com", DisplayName: "GCP Member", Role: store.UserRoleMember, @@ -578,7 +578,7 @@ func setupGCPAuthzTest(t *testing.T) (*Server, store.Store, *store.User, *store. Created: time.Now(), } outsider := &store.User{ - ID: "user-gcp-outsider", + ID: tid("user-gcp-outsider"), Email: "gcp-outsider@test.com", DisplayName: "GCP Outsider", Role: store.UserRoleMember, @@ -591,7 +591,7 @@ func setupGCPAuthzTest(t *testing.T) (*Server, store.Store, *store.User, *store. } project := &store.Project{ - ID: "project-gcp-authz", + ID: tid("project-gcp-authz"), Name: "GCP Authz Project", Slug: "gcp-authz-project", OwnerID: owner.ID, @@ -622,7 +622,7 @@ func TestGCPSA_Create_ProjectOwnerAllowed(t *testing.T) { rec := doRequestAsUser(t, srv, owner, http.MethodPost, fmt.Sprintf("/api/v1/projects/%s/gcp-service-accounts", project.ID), - map[string]string{"email": "sa@proj.iam.gserviceaccount.com", "projectId": "proj"}) + map[string]string{"email": "sa@proj.iam.gserviceaccount.com", "projectId": tid("proj")}) require.Equal(t, http.StatusCreated, rec.Code, "project owner should be able to create SA; got: %s", rec.Body.String()) } @@ -632,7 +632,7 @@ func TestGCPSA_Create_MemberDenied(t *testing.T) { rec := doRequestAsUser(t, srv, member, http.MethodPost, fmt.Sprintf("/api/v1/projects/%s/gcp-service-accounts", project.ID), - map[string]string{"email": "sa@proj.iam.gserviceaccount.com", "projectId": "proj"}) + map[string]string{"email": "sa@proj.iam.gserviceaccount.com", "projectId": tid("proj")}) require.Equal(t, http.StatusForbidden, rec.Code, "project member should not be able to create SA; got: %s", rec.Body.String()) } @@ -642,7 +642,7 @@ func TestGCPSA_Create_OutsiderDenied(t *testing.T) { rec := doRequestAsUser(t, srv, outsider, http.MethodPost, fmt.Sprintf("/api/v1/projects/%s/gcp-service-accounts", project.ID), - map[string]string{"email": "sa@proj.iam.gserviceaccount.com", "projectId": "proj"}) + map[string]string{"email": "sa@proj.iam.gserviceaccount.com", "projectId": tid("proj")}) require.Equal(t, http.StatusForbidden, rec.Code, "outsider should not be able to create SA; got: %s", rec.Body.String()) } @@ -652,11 +652,11 @@ func TestGCPSA_Delete_ProjectOwnerAllowed(t *testing.T) { ctx := context.Background() sa := &store.GCPServiceAccount{ - ID: "sa-del-owner", + ID: tid("sa-del-owner"), Scope: store.ScopeProject, ScopeID: project.ID, Email: "del-owner@proj.iam.gserviceaccount.com", - ProjectID: "proj", + ProjectID: tid("proj"), CreatedBy: owner.ID, CreatedAt: time.Now(), } @@ -673,11 +673,11 @@ func TestGCPSA_Delete_MemberDenied(t *testing.T) { ctx := context.Background() sa := &store.GCPServiceAccount{ - ID: "sa-del-member", + ID: tid("sa-del-member"), Scope: store.ScopeProject, ScopeID: project.ID, Email: "del-member@proj.iam.gserviceaccount.com", - ProjectID: "proj", + ProjectID: tid("proj"), CreatedBy: owner.ID, CreatedAt: time.Now(), } @@ -724,11 +724,11 @@ func TestGCPSA_Verify_ProjectOwnerAllowed(t *testing.T) { ctx := context.Background() sa := &store.GCPServiceAccount{ - ID: "sa-verify-owner", + ID: tid("sa-verify-owner"), Scope: store.ScopeProject, ScopeID: project.ID, Email: "verify@proj.iam.gserviceaccount.com", - ProjectID: "proj", + ProjectID: tid("proj"), CreatedBy: owner.ID, CreatedAt: time.Now(), } @@ -746,11 +746,11 @@ func TestGCPSA_Verify_MemberDenied(t *testing.T) { ctx := context.Background() sa := &store.GCPServiceAccount{ - ID: "sa-verify-member", + ID: tid("sa-verify-member"), Scope: store.ScopeProject, ScopeID: project.ID, Email: "verify-m@proj.iam.gserviceaccount.com", - ProjectID: "proj", + ProjectID: tid("proj"), CreatedBy: owner.ID, CreatedAt: time.Now(), } diff --git a/pkg/hub/handlers_github_app_test.go b/pkg/hub/handlers_github_app_test.go index 9041aa261..fe76df562 100644 --- a/pkg/hub/handlers_github_app_test.go +++ b/pkg/hub/handlers_github_app_test.go @@ -188,7 +188,7 @@ func TestHandleProjectGitHubInstallation(t *testing.T) { // Create a project project := &store.Project{ - ID: "project_gh_test", + ID: tid("project_gh_test"), Slug: "gh-test-project", Name: "GH Test Project", GitRemote: "https://github.com/acme/widgets", @@ -213,7 +213,7 @@ func TestHandleProjectGitHubInstallation(t *testing.T) { } // Associate project with installation - rec := doRequest(t, srv, http.MethodPut, "/api/v1/projects/project_gh_test/github-installation", map[string]interface{}{ + rec := doRequest(t, srv, http.MethodPut, fmt.Sprintf("/api/v1/projects/%s/github-installation", tid("project_gh_test")), map[string]interface{}{ "installation_id": 54321, }) if rec.Code != http.StatusOK { @@ -221,7 +221,7 @@ func TestHandleProjectGitHubInstallation(t *testing.T) { } // Verify project has installation ID - updatedProject, err := s.GetProject(ctx, "project_gh_test") + updatedProject, err := s.GetProject(ctx, tid("project_gh_test")) if err != nil { t.Fatalf("failed to get project: %v", err) } @@ -233,19 +233,19 @@ func TestHandleProjectGitHubInstallation(t *testing.T) { } // Get status - rec = doRequest(t, srv, http.MethodGet, "/api/v1/projects/project_gh_test/github-status", nil) + rec = doRequest(t, srv, http.MethodGet, fmt.Sprintf("/api/v1/projects/%s/github-status", tid("project_gh_test")), nil) if rec.Code != http.StatusOK { t.Fatalf("expected 200, got %d: %s", rec.Code, rec.Body.String()) } // Remove association - rec = doRequest(t, srv, http.MethodDelete, "/api/v1/projects/project_gh_test/github-installation", nil) + rec = doRequest(t, srv, http.MethodDelete, fmt.Sprintf("/api/v1/projects/%s/github-installation", tid("project_gh_test")), nil) if rec.Code != http.StatusNoContent { t.Fatalf("expected 204, got %d: %s", rec.Code, rec.Body.String()) } // Verify removed - clearedProject, err := s.GetProject(ctx, "project_gh_test") + clearedProject, err := s.GetProject(ctx, tid("project_gh_test")) if err != nil { t.Fatalf("failed to get project: %v", err) } @@ -259,7 +259,7 @@ func TestHandleProjectGitHubStatus_PostNoInstallation(t *testing.T) { ctx := context.Background() project := &store.Project{ - ID: "project_gh_status_check", Slug: "gh-status-check", Name: "GH Status Check", + ID: tid("project_gh_status_check"), Slug: "gh-status-check", Name: "GH Status Check", GitRemote: "https://github.com/acme/widgets", Created: time.Now(), Updated: time.Now(), Visibility: "private", } @@ -268,7 +268,7 @@ func TestHandleProjectGitHubStatus_PostNoInstallation(t *testing.T) { } // POST without installation should return 400 - rec := doRequest(t, srv, http.MethodPost, "/api/v1/projects/project_gh_status_check/github-status", nil) + rec := doRequest(t, srv, http.MethodPost, fmt.Sprintf("/api/v1/projects/%s/github-status", tid("project_gh_status_check")), nil) if rec.Code != http.StatusBadRequest { t.Errorf("expected 400 for project without installation, got %d: %s", rec.Code, rec.Body.String()) } @@ -292,7 +292,7 @@ func TestHandleProjectGitHubStatus_PostWithInstallation(t *testing.T) { } project := &store.Project{ - ID: "project_gh_status_check2", Slug: "gh-status-check2", Name: "GH Status Check 2", + ID: tid("project_gh_status_check2"), Slug: "gh-status-check2", Name: "GH Status Check 2", GitRemote: "https://github.com/acme/widgets", Created: time.Now(), Updated: time.Now(), Visibility: "private", } @@ -308,7 +308,7 @@ func TestHandleProjectGitHubStatus_PostWithInstallation(t *testing.T) { // POST should succeed (though minting will fail because no GitHub App // is configured — the endpoint should still return 200 with the error // captured in the response and project status updated to error) - rec := doRequest(t, srv, http.MethodPost, "/api/v1/projects/project_gh_status_check2/github-status", nil) + rec := doRequest(t, srv, http.MethodPost, fmt.Sprintf("/api/v1/projects/%s/github-status", tid("project_gh_status_check2")), nil) if rec.Code != http.StatusOK { t.Fatalf("expected 200, got %d: %s", rec.Code, rec.Body.String()) } @@ -338,14 +338,14 @@ func TestHandleProjectGitHubInstallation_NotFoundInstallation(t *testing.T) { ctx := context.Background() project := &store.Project{ - ID: "project_gh_notfound", Slug: "gh-nf", Name: "GH NF", + ID: tid("project_gh_notfound"), Slug: "gh-nf", Name: "GH NF", Created: time.Now(), Updated: time.Now(), Visibility: "private", } if err := s.CreateProject(ctx, project); err != nil { t.Fatalf("failed to create project: %v", err) } - rec := doRequest(t, srv, http.MethodPut, "/api/v1/projects/project_gh_notfound/github-installation", map[string]interface{}{ + rec := doRequest(t, srv, http.MethodPut, fmt.Sprintf("/api/v1/projects/%s/github-installation", tid("project_gh_notfound")), map[string]interface{}{ "installation_id": 99999, }) if rec.Code != http.StatusNotFound { @@ -362,7 +362,7 @@ func TestHandleProjectGitHubPermissions(t *testing.T) { ctx := context.Background() project := &store.Project{ - ID: "project_gh_perms", Slug: "gh-perms", Name: "GH Perms", + ID: tid("project_gh_perms"), Slug: "gh-perms", Name: "GH Perms", Created: time.Now(), Updated: time.Now(), Visibility: "private", } if err := s.CreateProject(ctx, project); err != nil { @@ -370,7 +370,7 @@ func TestHandleProjectGitHubPermissions(t *testing.T) { } // Get defaults - rec := doRequest(t, srv, http.MethodGet, "/api/v1/projects/project_gh_perms/github-permissions", nil) + rec := doRequest(t, srv, http.MethodGet, fmt.Sprintf("/api/v1/projects/%s/github-permissions", tid("project_gh_perms")), nil) if rec.Code != http.StatusOK { t.Fatalf("expected 200, got %d", rec.Code) } @@ -384,7 +384,7 @@ func TestHandleProjectGitHubPermissions(t *testing.T) { } // Set custom permissions - rec = doRequest(t, srv, http.MethodPut, "/api/v1/projects/project_gh_perms/github-permissions", map[string]interface{}{ + rec = doRequest(t, srv, http.MethodPut, fmt.Sprintf("/api/v1/projects/%s/github-permissions", tid("project_gh_perms")), map[string]interface{}{ "contents": "read", "metadata": "read", }) @@ -393,7 +393,7 @@ func TestHandleProjectGitHubPermissions(t *testing.T) { } // Verify stored - updatedProject, err := s.GetProject(ctx, "project_gh_perms") + updatedProject, err := s.GetProject(ctx, tid("project_gh_perms")) if err != nil { t.Fatalf("failed to get project: %v", err) } @@ -402,12 +402,12 @@ func TestHandleProjectGitHubPermissions(t *testing.T) { } // Reset to defaults - rec = doRequest(t, srv, http.MethodDelete, "/api/v1/projects/project_gh_perms/github-permissions", nil) + rec = doRequest(t, srv, http.MethodDelete, fmt.Sprintf("/api/v1/projects/%s/github-permissions", tid("project_gh_perms")), nil) if rec.Code != http.StatusNoContent { t.Fatalf("expected 204, got %d", rec.Code) } - clearedProject, err := s.GetProject(ctx, "project_gh_perms") + clearedProject, err := s.GetProject(ctx, tid("project_gh_perms")) if err != nil { t.Fatalf("failed to get project: %v", err) } @@ -449,7 +449,7 @@ func TestHandleAgentGitHubTokenRefresh_NoAuth(t *testing.T) { // Create a project and agent project := &store.Project{ - ID: "project_gh_refresh", + ID: tid("project_gh_refresh"), Name: "Test Project", Slug: "test-project", } @@ -458,7 +458,7 @@ func TestHandleAgentGitHubTokenRefresh_NoAuth(t *testing.T) { } agent := &store.Agent{ - ID: "agent_gh_refresh", + ID: tid("agent_gh_refresh"), Name: "test-agent", Slug: "test-agent", ProjectID: project.ID, @@ -481,7 +481,7 @@ func TestHandleAgentGitHubTokenRefresh_DevAuth(t *testing.T) { // Create a project and agent project := &store.Project{ - ID: "project_gh_refresh2", + ID: tid("project_gh_refresh2"), Name: "Test Project 2", Slug: "test-project-2", } @@ -490,7 +490,7 @@ func TestHandleAgentGitHubTokenRefresh_DevAuth(t *testing.T) { } agent := &store.Agent{ - ID: "agent_gh_refresh2", + ID: tid("agent_gh_refresh2"), Name: "test-agent-2", Slug: "test-agent-2", ProjectID: project.ID, @@ -513,7 +513,7 @@ func TestHandleAgentGitHubTokenRefresh_SelfAccess(t *testing.T) { // Create a project and agent project := &store.Project{ - ID: "project_gh_refresh3", + ID: tid("project_gh_refresh3"), Name: "Test Project 3", Slug: "test-project-3", } @@ -522,7 +522,7 @@ func TestHandleAgentGitHubTokenRefresh_SelfAccess(t *testing.T) { } agent := &store.Agent{ - ID: "agent_gh_refresh3", + ID: tid("agent_gh_refresh3"), Name: "test-agent-3", Slug: "test-agent-3", ProjectID: project.ID, @@ -537,7 +537,7 @@ func TestHandleAgentGitHubTokenRefresh_SelfAccess(t *testing.T) { // Generate an agent token with refresh scope agentToken, err := srv.agentTokenService.GenerateAgentToken( - "agent_gh_refresh3", project.ID, + tid("agent_gh_refresh3"), project.ID, []AgentTokenScope{ScopeAgentTokenRefresh}, nil) if err != nil { t.Fatalf("failed to generate agent token: %v", err) @@ -557,7 +557,7 @@ func TestHandleAgentGitHubTokenRefresh_NoInstallation(t *testing.T) { // Create a project WITHOUT a GitHub App installation project := &store.Project{ - ID: "project_gh_refresh4", + ID: tid("project_gh_refresh4"), Name: "Test Project 4", Slug: "test-project-4", } @@ -566,7 +566,7 @@ func TestHandleAgentGitHubTokenRefresh_NoInstallation(t *testing.T) { } agent := &store.Agent{ - ID: "agent_gh_refresh4", + ID: tid("agent_gh_refresh4"), Name: "test-agent-4", Slug: "test-agent-4", ProjectID: project.ID, @@ -580,7 +580,7 @@ func TestHandleAgentGitHubTokenRefresh_NoInstallation(t *testing.T) { } agentToken, err := srv.agentTokenService.GenerateAgentToken( - "agent_gh_refresh4", project.ID, + tid("agent_gh_refresh4"), project.ID, []AgentTokenScope{ScopeAgentTokenRefresh}, nil) if err != nil { t.Fatalf("failed to generate agent token: %v", err) diff --git a/pkg/hub/handlers_github_app_webhook_test.go b/pkg/hub/handlers_github_app_webhook_test.go index 4170f82f5..262a6b670 100644 --- a/pkg/hub/handlers_github_app_webhook_test.go +++ b/pkg/hub/handlers_github_app_webhook_test.go @@ -152,7 +152,7 @@ func TestHandleGitHubWebhook_InstallationCreated(t *testing.T) { // Create a project with a matching git remote project := &store.Project{ - ID: "project-1", + ID: tid("project-1"), Name: "Test Project", Slug: "test-project", GitRemote: "https://github.com/acme/widgets.git", @@ -205,7 +205,7 @@ func TestHandleGitHubWebhook_InstallationCreated(t *testing.T) { } // Verify project was auto-associated - updatedProject, err := s.GetProject(ctx, "project-1") + updatedProject, err := s.GetProject(ctx, tid("project-1")) if err != nil { t.Fatalf("failed to get project: %v", err) } @@ -242,7 +242,7 @@ func TestHandleGitHubWebhook_InstallationDeleted(t *testing.T) { // Create a project associated with the installation project := &store.Project{ - ID: "project-1", + ID: tid("project-1"), Name: "Test Project", Slug: "test-project", GitRemote: "https://github.com/acme/widgets.git", @@ -286,7 +286,7 @@ func TestHandleGitHubWebhook_InstallationDeleted(t *testing.T) { } // Verify project was set to error state - updatedProject, _ := s.GetProject(ctx, "project-1") + updatedProject, _ := s.GetProject(ctx, tid("project-1")) if updatedProject.GitHubAppStatus == nil || updatedProject.GitHubAppStatus.State != store.GitHubAppStateError { t.Errorf("expected project error state, got %v", updatedProject.GitHubAppStatus) } @@ -310,7 +310,7 @@ func TestHandleGitHubWebhook_InstallationReposRemoved(t *testing.T) { } project := &store.Project{ - ID: "project-1", + ID: tid("project-1"), Name: "Test Project", Slug: "test-project", GitRemote: "https://github.com/acme/widgets.git", @@ -351,7 +351,7 @@ func TestHandleGitHubWebhook_InstallationReposRemoved(t *testing.T) { } // Verify project was set to error - updatedProject, _ := s.GetProject(ctx, "project-1") + updatedProject, _ := s.GetProject(ctx, tid("project-1")) if updatedProject.GitHubAppStatus == nil || updatedProject.GitHubAppStatus.State != store.GitHubAppStateError { t.Errorf("expected error state, got %v", updatedProject.GitHubAppStatus) } @@ -390,10 +390,10 @@ func TestMatchProjectsToInstallation(t *testing.T) { // Create projects with different git remotes projects := []*store.Project{ - {ID: "g1", Name: "G1", Slug: "g1", GitRemote: "https://github.com/acme/widgets.git", Created: time.Now(), Updated: time.Now()}, - {ID: "g2", Name: "G2", Slug: "g2", GitRemote: "https://github.com/acme/api.git", Created: time.Now(), Updated: time.Now()}, - {ID: "g3", Name: "G3", Slug: "g3", GitRemote: "https://github.com/other/repo.git", Created: time.Now(), Updated: time.Now()}, - {ID: "g4", Name: "G4", Slug: "g4", Created: time.Now(), Updated: time.Now()}, // No git remote + {ID: tid("g1"), Name: "G1", Slug: tid("g1"), GitRemote: "https://github.com/acme/widgets.git", Created: time.Now(), Updated: time.Now()}, + {ID: tid("g2"), Name: "G2", Slug: tid("g2"), GitRemote: "https://github.com/acme/api.git", Created: time.Now(), Updated: time.Now()}, + {ID: tid("g3"), Name: "G3", Slug: tid("g3"), GitRemote: "https://github.com/other/repo.git", Created: time.Now(), Updated: time.Now()}, + {ID: tid("g4"), Name: "G4", Slug: tid("g4"), Created: time.Now(), Updated: time.Now()}, // No git remote } for _, g := range projects { @@ -418,7 +418,7 @@ func TestMatchProjectsToInstallation(t *testing.T) { } // Verify both matching projects were associated - for _, gID := range []string{"g1", "g2"} { + for _, gID := range []string{tid("g1"), tid("g2")} { project, _ := s.GetProject(ctx, gID) if project.GitHubInstallationID == nil { t.Errorf("project %s should be associated with installation", gID) @@ -428,13 +428,13 @@ func TestMatchProjectsToInstallation(t *testing.T) { } // Verify non-matching project was NOT associated - g3, _ := s.GetProject(ctx, "g3") + g3, _ := s.GetProject(ctx, tid("g3")) if g3.GitHubInstallationID != nil { t.Error("project g3 should not be associated") } // Verify no-remote project was NOT associated - g4, _ := s.GetProject(ctx, "g4") + g4, _ := s.GetProject(ctx, tid("g4")) if g4.GitHubInstallationID != nil { t.Error("project g4 should not be associated") } @@ -456,9 +456,9 @@ func TestMatchProjectsToInstallation_SkipsAlreadyAssociated(t *testing.T) { } project := &store.Project{ - ID: "g1", + ID: tid("g1"), Name: "G1", - Slug: "g1", + Slug: tid("g1"), GitRemote: "https://github.com/acme/widgets.git", GitHubInstallationID: &otherInstallation, Created: time.Now(), @@ -479,7 +479,7 @@ func TestMatchProjectsToInstallation_SkipsAlreadyAssociated(t *testing.T) { } // Verify project still has the original installation - updatedProject, _ := s.GetProject(ctx, "g1") + updatedProject, _ := s.GetProject(ctx, tid("g1")) if *updatedProject.GitHubInstallationID != 99999 { t.Errorf("project should still have original installation") } @@ -593,7 +593,7 @@ func TestWebhook_PublishesProjectUpdatedOnInstallationDeleted(t *testing.T) { // Create a project associated with the installation project := &store.Project{ - ID: "project-event-1", + ID: tid("project-event-1"), Name: "Event Test Project", Slug: "event-test-project", GitRemote: "https://github.com/acme/widgets.git", @@ -635,7 +635,7 @@ func TestWebhook_PublishesProjectUpdatedOnInstallationDeleted(t *testing.T) { if len(updates) != 1 { t.Fatalf("expected 1 project updated event, got %d", len(updates)) } - if updates[0].ID != "project-event-1" { + if updates[0].ID != tid("project-event-1") { t.Errorf("expected project ID project-event-1, got %s", updates[0].ID) } if updates[0].GitHubAppStatus == nil || updates[0].GitHubAppStatus.State != store.GitHubAppStateError { @@ -664,7 +664,7 @@ func TestWebhook_PublishesProjectUpdatedOnRepoRemoved(t *testing.T) { } project := &store.Project{ - ID: "project-event-2", + ID: tid("project-event-2"), Name: "Event Test Project 2", Slug: "event-test-project-2", GitRemote: "https://github.com/acme/widgets.git", @@ -702,7 +702,7 @@ func TestWebhook_PublishesProjectUpdatedOnRepoRemoved(t *testing.T) { if len(updates) != 1 { t.Fatalf("expected 1 project updated event, got %d", len(updates)) } - if updates[0].ID != "project-event-2" { + if updates[0].ID != tid("project-event-2") { t.Errorf("expected project ID project-event-2, got %s", updates[0].ID) } if updates[0].GitHubAppStatus == nil || updates[0].GitHubAppStatus.State != store.GitHubAppStateError { @@ -719,7 +719,7 @@ func TestWebhook_PublishesProjectUpdatedOnAutoMatch(t *testing.T) { // Create a project with a matching git remote but no installation yet project := &store.Project{ - ID: "project-event-3", + ID: tid("project-event-3"), Name: "Event Test Project 3", Slug: "event-test-project-3", GitRemote: "https://github.com/acme/widgets.git", @@ -762,7 +762,7 @@ func TestWebhook_PublishesProjectUpdatedOnAutoMatch(t *testing.T) { if len(updates) != 1 { t.Fatalf("expected 1 project updated event from auto-match, got %d", len(updates)) } - if updates[0].ID != "project-event-3" { + if updates[0].ID != tid("project-event-3") { t.Errorf("expected project ID project-event-3, got %s", updates[0].ID) } if updates[0].GitHubInstallationID == nil || *updates[0].GitHubInstallationID != 12345 { diff --git a/pkg/hub/handlers_logs_test.go b/pkg/hub/handlers_logs_test.go index eac73d122..6c3693d8a 100644 --- a/pkg/hub/handlers_logs_test.go +++ b/pkg/hub/handlers_logs_test.go @@ -43,6 +43,7 @@ func createTestAgent(t *testing.T, s store.Store) *store.Agent { project := &store.Project{ ID: api.NewUUID(), Name: "test-project-" + api.NewUUID()[:8], + Slug: "test-project-" + api.NewUUID()[:8], } if err := s.CreateProject(ctx, project); err != nil { t.Fatalf("CreateProject: %v", err) @@ -51,6 +52,7 @@ func createTestAgent(t *testing.T, s store.Store) *store.Agent { agent := &store.Agent{ ID: api.NewUUID(), Name: "test-agent-" + api.NewUUID()[:8], + Slug: "test-agent-" + api.NewUUID()[:8], ProjectID: project.ID, } if err := s.CreateAgent(ctx, agent); err != nil { diff --git a/pkg/hub/handlers_messages.go b/pkg/hub/handlers_messages.go index 54a5db948..767e908d5 100644 --- a/pkg/hub/handlers_messages.go +++ b/pkg/hub/handlers_messages.go @@ -216,15 +216,16 @@ func (s *Server) handleAgentMessagesStream(w http.ResponseWriter, r *http.Reques return } - // The event bus is an interface; only ChannelEventPublisher supports - // subscription. Check this before hitting the store so noop-publisher - // hubs fail fast without a wasted DB roundtrip. - ep, ok := s.events.(*ChannelEventPublisher) - if !ok { + // Real-time streaming requires a subscription-capable EventPublisher + // (ChannelEventPublisher or PostgresEventPublisher). The no-op publisher + // returns a nil channel, so fail fast before hitting the store to avoid a + // wasted DB roundtrip on hubs without a configured publisher. + if _, isNoop := s.events.(noopEventPublisher); isNoop || s.events == nil { writeError(w, http.StatusNotImplemented, "not_implemented", "Real-time message streaming is not available on this hub", nil) return } + ep := s.events ctx := r.Context() user := GetUserIdentityFromContext(ctx) diff --git a/pkg/hub/handlers_notifications_test.go b/pkg/hub/handlers_notifications_test.go index 9076bc04d..55f0f1f8f 100644 --- a/pkg/hub/handlers_notifications_test.go +++ b/pkg/hub/handlers_notifications_test.go @@ -19,6 +19,7 @@ package hub import ( "context" "encoding/json" + "fmt" "net/http" "net/http/httptest" "testing" @@ -39,14 +40,14 @@ func setupNotificationHandlerTest(t *testing.T) (*Server, store.Store, string) { ctx := context.Background() project := &store.Project{ - ID: "project-notif-handler", + ID: tid("project-notif-handler"), Name: "Notif Handler Project", Slug: "notif-handler-project", } require.NoError(t, s.CreateProject(ctx, project)) agent := &store.Agent{ - ID: "agent-watched", + ID: tid("agent-watched"), Slug: "watched-agent", Name: "Watched Agent", ProjectID: project.ID, @@ -174,7 +175,7 @@ func TestHandleNotifications_AcknowledgeAll(t *testing.T) { func TestHandleNotifications_AcknowledgeNotFound(t *testing.T) { srv, _, _ := setupNotificationHandlerTest(t) - rec := doRequest(t, srv, http.MethodPost, "/api/v1/notifications/nonexistent-id/ack", nil) + rec := doRequest(t, srv, http.MethodPost, "/api/v1/notifications/"+tid("nonexistent-id")+"/ack", nil) assert.Equal(t, http.StatusNotFound, rec.Code) } @@ -184,14 +185,14 @@ func TestHandleNotifications_RejectAgentToken(t *testing.T) { // Create an agent and generate a token for it project := &store.Project{ - ID: "project-agent-auth", + ID: tid("project-agent-auth"), Name: "Agent Auth Project", Slug: "agent-auth-project", } _ = s.CreateProject(ctx, project) agent := &store.Agent{ - ID: "agent-auth-test", + ID: tid("agent-auth-test"), Slug: "auth-agent", Name: "Auth Agent", ProjectID: project.ID, @@ -227,14 +228,14 @@ func TestHandleNotifications_FilterByAgent(t *testing.T) { srv, s, _ := setupNotificationHandlerTest(t) ctx := context.Background() - // The setup already created "agent-watched" with user notifications for DevUserID. - // Create a second agent that watches "agent-watched", so "agent-watched" is the + // The setup already created tid("agent-watched") with user notifications for DevUserID. + // Create a second agent that watches tid("agent-watched"), so tid("agent-watched") is the // subscriber (simulating notifications sent TO the watched agent). agent2 := &store.Agent{ - ID: "agent-other", - Slug: "other-agent", + ID: tid("agent-other"), + Slug: tid("other-agent"), Name: "Other Agent", - ProjectID: "project-notif-handler", + ProjectID: tid("project-notif-handler"), Phase: string(state.PhaseRunning), } require.NoError(t, s.CreateAgent(ctx, agent2)) @@ -243,10 +244,10 @@ func TestHandleNotifications_FilterByAgent(t *testing.T) { sub2 := &store.NotificationSubscription{ ID: api.NewUUID(), Scope: store.SubscriptionScopeAgent, - AgentID: "agent-other", + AgentID: tid("agent-other"), SubscriberType: store.SubscriberTypeAgent, - SubscriberID: "agent-watched", - ProjectID: "project-notif-handler", + SubscriberID: tid("agent-watched"), + ProjectID: tid("project-notif-handler"), TriggerActivities: []string{"COMPLETED"}, CreatedAt: time.Now(), CreatedBy: "test", @@ -257,10 +258,10 @@ func TestHandleNotifications_FilterByAgent(t *testing.T) { agentNotif := &store.Notification{ ID: api.NewUUID(), SubscriptionID: sub2.ID, - AgentID: "agent-other", - ProjectID: "project-notif-handler", + AgentID: tid("agent-other"), + ProjectID: tid("project-notif-handler"), SubscriberType: store.SubscriberTypeAgent, - SubscriberID: "agent-watched", + SubscriberID: tid("agent-watched"), Status: "COMPLETED", Message: "agent-other completed (to agent-watched)", Dispatched: true, @@ -270,7 +271,7 @@ func TestHandleNotifications_FilterByAgent(t *testing.T) { require.NoError(t, s.CreateNotification(ctx, agentNotif)) // GET with agentId filter - rec := doRequest(t, srv, http.MethodGet, "/api/v1/notifications?agentId=agent-watched", nil) + rec := doRequest(t, srv, http.MethodGet, fmt.Sprintf("/api/v1/notifications?agentId=%s", tid("agent-watched")), nil) assert.Equal(t, http.StatusOK, rec.Code) var resp struct { @@ -280,19 +281,19 @@ func TestHandleNotifications_FilterByAgent(t *testing.T) { require.NoError(t, json.NewDecoder(rec.Body).Decode(&resp)) // User notifications: 1 unacknowledged for this agent (notif1 from setup) - assert.Len(t, resp.UserNotifications, 1) + require.Len(t, resp.UserNotifications, 1) assert.Equal(t, "COMPLETED", resp.UserNotifications[0].Status) // Agent notifications: notifications sent TO agent-watched - assert.Len(t, resp.AgentNotifications, 1) - assert.Equal(t, "agent-watched", resp.AgentNotifications[0].SubscriberID) + require.Len(t, resp.AgentNotifications, 1) + assert.Equal(t, tid("agent-watched"), resp.AgentNotifications[0].SubscriberID) } func TestHandleNotifications_FilterByAgent_NoResults(t *testing.T) { srv, _, _ := setupNotificationHandlerTest(t) // Query for an agent with no notifications - rec := doRequest(t, srv, http.MethodGet, "/api/v1/notifications?agentId=nonexistent-agent", nil) + rec := doRequest(t, srv, http.MethodGet, fmt.Sprintf("/api/v1/notifications?agentId=%s", tid("nonexistent-agent")), nil) assert.Equal(t, http.StatusOK, rec.Code) var resp struct { @@ -323,7 +324,7 @@ func setupProjectWithBroker(t *testing.T, s store.Store, projectID, projectName ctx := context.Background() broker := &store.RuntimeBroker{ - ID: "broker-" + projectID, + ID: tid("broker-" + projectID), Name: "Test Broker", Slug: "test-broker-" + projectID, Status: store.BrokerStatusOnline, @@ -331,7 +332,7 @@ func setupProjectWithBroker(t *testing.T, s store.Store, projectID, projectName require.NoError(t, s.CreateRuntimeBroker(ctx, broker)) project := &store.Project{ - ID: projectID, + ID: tid(projectID), Name: projectName, Slug: projectID, } @@ -416,8 +417,8 @@ func TestHandleSubscriptions_CreateAgentScoped(t *testing.T) { req := createSubscriptionRequest{ Scope: "agent", - AgentID: "agent-watched", - ProjectID: "project-notif-handler", + AgentID: tid("agent-watched"), + ProjectID: tid("project-notif-handler"), TriggerActivities: []string{"COMPLETED", "WAITING_FOR_INPUT"}, } rec := doRequest(t, srv, http.MethodPost, "/api/v1/notifications/subscriptions", req) @@ -429,8 +430,8 @@ func TestHandleSubscriptions_CreateAgentScoped(t *testing.T) { var sub store.NotificationSubscription require.NoError(t, json.NewDecoder(rec.Body).Decode(&sub)) assert.Equal(t, "agent", sub.Scope) - assert.Equal(t, "agent-watched", sub.AgentID) - assert.Equal(t, "project-notif-handler", sub.ProjectID) + assert.Equal(t, tid("agent-watched"), sub.AgentID) + assert.Equal(t, tid("project-notif-handler"), sub.ProjectID) // Verify in store subs, err := s.GetSubscriptionsForSubscriber(context.Background(), store.SubscriberTypeUser, DevUserID) @@ -443,7 +444,7 @@ func TestHandleSubscriptions_CreateProjectScoped(t *testing.T) { req := createSubscriptionRequest{ Scope: "project", - ProjectID: "project-notif-handler", + ProjectID: tid("project-notif-handler"), TriggerActivities: []string{"COMPLETED"}, } rec := doRequest(t, srv, http.MethodPost, "/api/v1/notifications/subscriptions", req) @@ -453,7 +454,7 @@ func TestHandleSubscriptions_CreateProjectScoped(t *testing.T) { require.NoError(t, json.NewDecoder(rec.Body).Decode(&sub)) assert.Equal(t, "project", sub.Scope) assert.Empty(t, sub.AgentID) - assert.Equal(t, "project-notif-handler", sub.ProjectID) + assert.Equal(t, tid("project-notif-handler"), sub.ProjectID) } func TestHandleSubscriptions_CreateValidation(t *testing.T) { @@ -484,7 +485,7 @@ func TestHandleSubscriptions_List(t *testing.T) { // Create a project-scoped subscription createReq := createSubscriptionRequest{ Scope: "project", - ProjectID: "project-notif-handler", + ProjectID: tid("project-notif-handler"), TriggerActivities: []string{"COMPLETED"}, } rec := doRequest(t, srv, http.MethodPost, "/api/v1/notifications/subscriptions", createReq) @@ -515,7 +516,7 @@ func TestHandleSubscriptions_Delete(t *testing.T) { // Create a new subscription to delete createReq := createSubscriptionRequest{ Scope: "project", - ProjectID: "project-notif-handler", + ProjectID: tid("project-notif-handler"), TriggerActivities: []string{"COMPLETED"}, } rec := doRequest(t, srv, http.MethodPost, "/api/v1/notifications/subscriptions", createReq) diff --git a/pkg/hub/handlers_permissions_test.go b/pkg/hub/handlers_permissions_test.go index 257a2a6d4..d9926b021 100644 --- a/pkg/hub/handlers_permissions_test.go +++ b/pkg/hub/handlers_permissions_test.go @@ -20,6 +20,7 @@ import ( "bytes" "context" "encoding/json" + "errors" "net/http" "net/http/httptest" "testing" @@ -28,6 +29,62 @@ import ( "github.com/GoogleCloudPlatform/scion/pkg/store" ) +// permSeedUser ensures a user row exists so that group-membership / policy-binding +// foreign keys resolve. The Ent store enforces user/agent FK edges that the +// former raw-SQL store did not, so fixtures must create referenced principals. +func permSeedUser(t *testing.T, ctx context.Context, s store.Store, id string) { + t.Helper() + err := s.CreateUser(ctx, &store.User{ + ID: id, Email: id + "@example.com", DisplayName: "Seed User", + Role: store.UserRoleMember, Status: "active", Created: time.Now(), + }) + if err != nil && !errors.Is(err, store.ErrAlreadyExists) { + t.Fatalf("seed user %s: %v", id, err) + } +} + +// permSeedAgent ensures an agent (and its required project) exists so that +// membership / binding foreign keys resolve. +func permSeedAgent(t *testing.T, ctx context.Context, s store.Store, id string) { + t.Helper() + projectID := tid("perm-agent-project") + _ = s.CreateProject(ctx, &store.Project{ID: projectID, Name: "Perm Agent Project", Slug: "perm-agent-project"}) + err := s.CreateAgent(ctx, &store.Agent{ + ID: id, Name: "Seed Agent", Slug: "seed-agent-" + id[:8], + ProjectID: projectID, Phase: "stopped", Visibility: store.VisibilityPrivate, + }) + if err != nil && !errors.Is(err, store.ErrAlreadyExists) { + t.Fatalf("seed agent %s: %v", id, err) + } +} + +// permSeedMember seeds the user or agent referenced by a group membership. +func permSeedMember(t *testing.T, ctx context.Context, s store.Store, m *store.GroupMember) { + t.Helper() + if m.MemberID == "" { + return + } + if m.MemberType == store.GroupMemberTypeAgent { + permSeedAgent(t, ctx, s, m.MemberID) + } else { + permSeedUser(t, ctx, s, m.MemberID) + } +} + +// permSeedPrincipal seeds the user or agent referenced by a policy binding. +// Group principals are created by the test itself, so they are skipped. +func permSeedPrincipal(t *testing.T, ctx context.Context, s store.Store, principalType, principalID string) { + t.Helper() + if principalID == "" || principalType == "group" { + return + } + if principalType == "agent" { + permSeedAgent(t, ctx, s, principalID) + } else { + permSeedUser(t, ctx, s, principalID) + } +} + // ============================================================================ // Group Endpoint Tests // ============================================================================ @@ -39,12 +96,15 @@ func TestGroupList(t *testing.T) { // Create some test groups for i := 0; i < 3; i++ { group := &store.Group{ - ID: "group_" + string(rune('a'+i)), + ID: tid("group_" + string(rune('a'+i))), Name: "Test Group " + string(rune('A'+i)), - Slug: "test-group-" + string(rune('a'+i)), + Slug: tid("test-group-" + string(rune('a'+i))), Created: time.Now(), Updated: time.Now(), } + if group.OwnerID != "" { + permSeedUser(t, ctx, s, group.OwnerID) + } if err := s.CreateGroup(ctx, group); err != nil { t.Fatalf("failed to create group: %v", err) } @@ -120,13 +180,16 @@ func TestGroupGet(t *testing.T) { ctx := context.Background() group := &store.Group{ - ID: "group_xyz123", + ID: tid("group_xyz123"), Name: "Test Group", Slug: "test-group", Description: "A test group", Created: time.Now(), Updated: time.Now(), } + if group.OwnerID != "" { + permSeedUser(t, ctx, s, group.OwnerID) + } if err := s.CreateGroup(ctx, group); err != nil { t.Fatalf("failed to create group: %v", err) } @@ -160,12 +223,15 @@ func TestGroupUpdate(t *testing.T) { ctx := context.Background() group := &store.Group{ - ID: "group_upd123", + ID: tid("group_upd123"), Name: "Original Name", Slug: "original-name", Created: time.Now(), Updated: time.Now(), } + if group.OwnerID != "" { + permSeedUser(t, ctx, s, group.OwnerID) + } if err := s.CreateGroup(ctx, group); err != nil { t.Fatalf("failed to create group: %v", err) } @@ -199,12 +265,15 @@ func TestGroupDelete(t *testing.T) { ctx := context.Background() group := &store.Group{ - ID: "group_del123", + ID: tid("group_del123"), Name: "Delete Me", Slug: "delete-me", Created: time.Now(), Updated: time.Now(), } + if group.OwnerID != "" { + permSeedUser(t, ctx, s, group.OwnerID) + } if err := s.CreateGroup(ctx, group); err != nil { t.Fatalf("failed to create group: %v", err) } @@ -227,19 +296,22 @@ func TestGroupMembersAdd(t *testing.T) { ctx := context.Background() group := &store.Group{ - ID: "group_mem123", + ID: tid("group_mem123"), Name: "Test Group", Slug: "test-group", Created: time.Now(), Updated: time.Now(), } + if group.OwnerID != "" { + permSeedUser(t, ctx, s, group.OwnerID) + } if err := s.CreateGroup(ctx, group); err != nil { t.Fatalf("failed to create group: %v", err) } // Create the user to be added as a member user := &store.User{ - ID: "user_abc123", + ID: tid("user_abc123"), Email: "user@example.com", DisplayName: "Test User", Role: "member", @@ -252,7 +324,7 @@ func TestGroupMembersAdd(t *testing.T) { body := AddGroupMemberRequest{ MemberType: "user", - MemberID: "user_abc123", + MemberID: tid("user_abc123"), Role: "member", } @@ -267,7 +339,7 @@ func TestGroupMembersAdd(t *testing.T) { t.Fatalf("failed to decode response: %v", err) } - if resp.MemberID != "user_abc123" { + if resp.MemberID != tid("user_abc123") { t.Errorf("expected memberId 'user_abc123', got %q", resp.MemberID) } if resp.DisplayName != "Test User" { @@ -280,19 +352,22 @@ func TestGroupMembersAddByEmail(t *testing.T) { ctx := context.Background() group := &store.Group{ - ID: "group_email123", + ID: tid("group_email123"), Name: "Test Group Email", Slug: "test-group-email", Created: time.Now(), Updated: time.Now(), } + if group.OwnerID != "" { + permSeedUser(t, ctx, s, group.OwnerID) + } if err := s.CreateGroup(ctx, group); err != nil { t.Fatalf("failed to create group: %v", err) } // Create the user user := &store.User{ - ID: "user_email_test", + ID: tid("user_email_test"), Email: "alice@example.com", DisplayName: "Alice", Role: "member", @@ -322,7 +397,7 @@ func TestGroupMembersAddByEmail(t *testing.T) { } // Should resolve email to user ID - if resp.MemberID != "user_email_test" { + if resp.MemberID != tid("user_email_test") { t.Errorf("expected memberId 'user_email_test', got %q", resp.MemberID) } if resp.DisplayName != "Alice" { @@ -335,12 +410,15 @@ func TestGroupMembersAddByEmail_NotFound(t *testing.T) { ctx := context.Background() group := &store.Group{ - ID: "group_email_nf", + ID: tid("group_email_nf"), Name: "Test Group", Slug: "test-group-email-nf", Created: time.Now(), Updated: time.Now(), } + if group.OwnerID != "" { + permSeedUser(t, ctx, s, group.OwnerID) + } if err := s.CreateGroup(ctx, group); err != nil { t.Fatalf("failed to create group: %v", err) } @@ -363,22 +441,28 @@ func TestGroupMembersAddGroupBySlug(t *testing.T) { ctx := context.Background() parentGroup := &store.Group{ - ID: "parent_grp", + ID: tid("parent_grp"), Name: "Parent Group", Slug: "parent-group", Created: time.Now(), Updated: time.Now(), } childGroup := &store.Group{ - ID: "child_grp", + ID: tid("child_grp"), Name: "Child Group", Slug: "child-group", Created: time.Now(), Updated: time.Now(), } + if parentGroup.OwnerID != "" { + permSeedUser(t, ctx, s, parentGroup.OwnerID) + } if err := s.CreateGroup(ctx, parentGroup); err != nil { t.Fatalf("failed to create parent group: %v", err) } + if childGroup.OwnerID != "" { + permSeedUser(t, ctx, s, childGroup.OwnerID) + } if err := s.CreateGroup(ctx, childGroup); err != nil { t.Fatalf("failed to create child group: %v", err) } @@ -402,7 +486,7 @@ func TestGroupMembersAddGroupBySlug(t *testing.T) { } // Should resolve slug to group ID - if resp.MemberID != "child_grp" { + if resp.MemberID != tid("child_grp") { t.Errorf("expected memberId 'child_grp', got %q", resp.MemberID) } if resp.DisplayName != "Child Group" { @@ -415,12 +499,15 @@ func TestGroupMembersList(t *testing.T) { ctx := context.Background() group := &store.Group{ - ID: "group_lst123", + ID: tid("group_lst123"), Name: "Test Group", Slug: "test-group-list", Created: time.Now(), Updated: time.Now(), } + if group.OwnerID != "" { + permSeedUser(t, ctx, s, group.OwnerID) + } if err := s.CreateGroup(ctx, group); err != nil { t.Fatalf("failed to create group: %v", err) } @@ -430,10 +517,11 @@ func TestGroupMembersList(t *testing.T) { member := &store.GroupMember{ GroupID: group.ID, MemberType: "user", - MemberID: "user_" + string(rune('a'+i)), + MemberID: tid("user_" + string(rune('a'+i))), Role: "member", AddedAt: time.Now(), } + permSeedMember(t, ctx, s, member) if err := s.AddGroupMember(ctx, member); err != nil { t.Fatalf("failed to add member: %v", err) } @@ -460,12 +548,15 @@ func TestGroupMemberRemove(t *testing.T) { ctx := context.Background() group := &store.Group{ - ID: "group_rem123", + ID: tid("group_rem123"), Name: "Test Group", Slug: "test-group-remove", Created: time.Now(), Updated: time.Now(), } + if group.OwnerID != "" { + permSeedUser(t, ctx, s, group.OwnerID) + } if err := s.CreateGroup(ctx, group); err != nil { t.Fatalf("failed to create group: %v", err) } @@ -473,22 +564,23 @@ func TestGroupMemberRemove(t *testing.T) { member := &store.GroupMember{ GroupID: group.ID, MemberType: "user", - MemberID: "user_remove", + MemberID: tid("user_remove"), Role: "member", AddedAt: time.Now(), } + permSeedMember(t, ctx, s, member) if err := s.AddGroupMember(ctx, member); err != nil { t.Fatalf("failed to add member: %v", err) } - rec := doRequest(t, srv, http.MethodDelete, "/api/v1/groups/"+group.ID+"/members/user/user_remove", nil) + rec := doRequest(t, srv, http.MethodDelete, "/api/v1/groups/"+group.ID+"/members/user/"+tid("user_remove"), nil) if rec.Code != http.StatusNoContent { t.Errorf("expected status 204, got %d: %s", rec.Code, rec.Body.String()) } // Verify removed - _, err := s.GetGroupMembership(ctx, group.ID, "user", "user_remove") + _, err := s.GetGroupMembership(ctx, group.ID, "user", tid("user_remove")) if err != store.ErrNotFound { t.Errorf("expected ErrNotFound, got %v", err) } @@ -500,22 +592,28 @@ func TestGroupCycleDetection(t *testing.T) { // Create two groups groupA := &store.Group{ - ID: "group_a", + ID: tid("group_a"), Name: "Group A", Slug: "group-a", Created: time.Now(), Updated: time.Now(), } groupB := &store.Group{ - ID: "group_b", + ID: tid("group_b"), Name: "Group B", Slug: "group-b", Created: time.Now(), Updated: time.Now(), } + if groupA.OwnerID != "" { + permSeedUser(t, ctx, s, groupA.OwnerID) + } if err := s.CreateGroup(ctx, groupA); err != nil { t.Fatalf("failed to create group A: %v", err) } + if groupB.OwnerID != "" { + permSeedUser(t, ctx, s, groupB.OwnerID) + } if err := s.CreateGroup(ctx, groupB); err != nil { t.Fatalf("failed to create group B: %v", err) } @@ -549,7 +647,7 @@ func TestGroupMembersAddAgent(t *testing.T) { // Create a project for the agent project := &store.Project{ - ID: "project_agent_test", + ID: tid("project_agent_test"), Name: "Test Project", Slug: "test-project-agent", } @@ -559,8 +657,9 @@ func TestGroupMembersAddAgent(t *testing.T) { // Create the agent agent := &store.Agent{ - ID: "agent_abc123", + ID: tid("agent_abc123"), Name: "Test Agent", + Slug: "test-agent-abc123", ProjectID: project.ID, } if err := s.CreateAgent(ctx, agent); err != nil { @@ -568,19 +667,22 @@ func TestGroupMembersAddAgent(t *testing.T) { } group := &store.Group{ - ID: "group_agent123", + ID: tid("group_agent123"), Name: "Test Group", Slug: "test-group-agent", Created: time.Now(), Updated: time.Now(), } + if group.OwnerID != "" { + permSeedUser(t, ctx, s, group.OwnerID) + } if err := s.CreateGroup(ctx, group); err != nil { t.Fatalf("failed to create group: %v", err) } body := AddGroupMemberRequest{ MemberType: "agent", - MemberID: "agent_abc123", + MemberID: tid("agent_abc123"), Role: "member", } @@ -598,7 +700,7 @@ func TestGroupMembersAddAgent(t *testing.T) { if resp.MemberType != "agent" { t.Errorf("expected memberType 'agent', got %q", resp.MemberType) } - if resp.MemberID != "agent_abc123" { + if resp.MemberID != tid("agent_abc123") { t.Errorf("expected memberId 'agent_abc123', got %q", resp.MemberID) } if resp.DisplayName != "Test Agent" { @@ -611,12 +713,15 @@ func TestGroupMemberRemoveAgent(t *testing.T) { ctx := context.Background() group := &store.Group{ - ID: "group_rmagent", + ID: tid("group_rmagent"), Name: "Test Group", Slug: "test-group-rm-agent", Created: time.Now(), Updated: time.Now(), } + if group.OwnerID != "" { + permSeedUser(t, ctx, s, group.OwnerID) + } if err := s.CreateGroup(ctx, group); err != nil { t.Fatalf("failed to create group: %v", err) } @@ -624,22 +729,23 @@ func TestGroupMemberRemoveAgent(t *testing.T) { member := &store.GroupMember{ GroupID: group.ID, MemberType: "agent", - MemberID: "agent_remove", + MemberID: tid("agent_remove"), Role: "member", AddedAt: time.Now(), } + permSeedMember(t, ctx, s, member) if err := s.AddGroupMember(ctx, member); err != nil { t.Fatalf("failed to add member: %v", err) } - rec := doRequest(t, srv, http.MethodDelete, "/api/v1/groups/"+group.ID+"/members/agent/agent_remove", nil) + rec := doRequest(t, srv, http.MethodDelete, "/api/v1/groups/"+group.ID+"/members/agent/"+tid("agent_remove"), nil) if rec.Code != http.StatusNoContent { t.Errorf("expected status 204, got %d: %s", rec.Code, rec.Body.String()) } // Verify removed - _, err := s.GetGroupMembership(ctx, group.ID, "agent", "agent_remove") + _, err := s.GetGroupMembership(ctx, group.ID, "agent", tid("agent_remove")) if err != store.ErrNotFound { t.Errorf("expected ErrNotFound, got %v", err) } @@ -701,7 +807,7 @@ func TestGroupListWithGroupTypeFilter(t *testing.T) { // Create groups with different (or default) types g1 := &store.Group{ - ID: "group_explicit_1", + ID: tid("group_explicit_1"), Name: "Explicit 1", Slug: "explicit-1", GroupType: "explicit", @@ -709,7 +815,7 @@ func TestGroupListWithGroupTypeFilter(t *testing.T) { Updated: time.Now(), } g2 := &store.Group{ - ID: "group_explicit_2", + ID: tid("group_explicit_2"), Name: "Explicit 2", Slug: "explicit-2", GroupType: "explicit", @@ -717,6 +823,9 @@ func TestGroupListWithGroupTypeFilter(t *testing.T) { Updated: time.Now(), } for _, g := range []*store.Group{g1, g2} { + if g.OwnerID != "" { + permSeedUser(t, ctx, s, g.OwnerID) + } if err := s.CreateGroup(ctx, g); err != nil { t.Fatalf("failed to create group: %v", err) } @@ -786,7 +895,7 @@ func TestGroupUpdateAuthz_OwnerAllowed(t *testing.T) { ctx := context.Background() owner := &store.User{ - ID: "user_owner_upd", + ID: tid("user_owner_upd"), Email: "owner@example.com", DisplayName: "Owner", Role: "member", @@ -798,13 +907,16 @@ func TestGroupUpdateAuthz_OwnerAllowed(t *testing.T) { } group := &store.Group{ - ID: "group_authz_upd", + ID: tid("group_authz_upd"), Name: "Owned Group", Slug: "owned-group-upd", OwnerID: owner.ID, Created: time.Now(), Updated: time.Now(), } + if group.OwnerID != "" { + permSeedUser(t, ctx, s, group.OwnerID) + } if err := s.CreateGroup(ctx, group); err != nil { t.Fatalf("failed to create group: %v", err) } @@ -822,7 +934,7 @@ func TestGroupUpdateAuthz_NonOwnerDenied(t *testing.T) { ctx := context.Background() other := &store.User{ - ID: "user_other_upd", + ID: tid("user_other_upd"), Email: "other@example.com", DisplayName: "Other", Role: "member", @@ -834,13 +946,16 @@ func TestGroupUpdateAuthz_NonOwnerDenied(t *testing.T) { } group := &store.Group{ - ID: "group_authz_upd2", + ID: tid("group_authz_upd2"), Name: "Someone Else Group", Slug: "someone-else-upd", - OwnerID: "user_someone_else", + OwnerID: tid("user_someone_else"), Created: time.Now(), Updated: time.Now(), } + if group.OwnerID != "" { + permSeedUser(t, ctx, s, group.OwnerID) + } if err := s.CreateGroup(ctx, group); err != nil { t.Fatalf("failed to create group: %v", err) } @@ -858,7 +973,7 @@ func TestGroupDeleteAuthz_NonOwnerDenied(t *testing.T) { ctx := context.Background() other := &store.User{ - ID: "user_other_del", + ID: tid("user_other_del"), Email: "other-del@example.com", DisplayName: "Other", Role: "member", @@ -870,13 +985,16 @@ func TestGroupDeleteAuthz_NonOwnerDenied(t *testing.T) { } group := &store.Group{ - ID: "group_authz_del", + ID: tid("group_authz_del"), Name: "Protected Group", Slug: "protected-group", - OwnerID: "user_someone_else", + OwnerID: tid("user_someone_else"), Created: time.Now(), Updated: time.Now(), } + if group.OwnerID != "" { + permSeedUser(t, ctx, s, group.OwnerID) + } if err := s.CreateGroup(ctx, group); err != nil { t.Fatalf("failed to create group: %v", err) } @@ -893,7 +1011,7 @@ func TestGroupAddMemberAuthz_OwnerAllowed(t *testing.T) { ctx := context.Background() owner := &store.User{ - ID: "user_owner_add", + ID: tid("user_owner_add"), Email: "owner-add@example.com", DisplayName: "Owner", Role: "member", @@ -901,7 +1019,7 @@ func TestGroupAddMemberAuthz_OwnerAllowed(t *testing.T) { Created: time.Now(), } memberUser := &store.User{ - ID: "user_to_add", + ID: tid("user_to_add"), Email: "toadd@example.com", DisplayName: "To Add", Role: "member", @@ -915,13 +1033,16 @@ func TestGroupAddMemberAuthz_OwnerAllowed(t *testing.T) { } group := &store.Group{ - ID: "group_authz_add", + ID: tid("group_authz_add"), Name: "Owned Group", Slug: "owned-group-add", OwnerID: owner.ID, Created: time.Now(), Updated: time.Now(), } + if group.OwnerID != "" { + permSeedUser(t, ctx, s, group.OwnerID) + } if err := s.CreateGroup(ctx, group); err != nil { t.Fatalf("failed to create group: %v", err) } @@ -943,7 +1064,7 @@ func TestGroupAddMemberAuthz_NonOwnerDenied(t *testing.T) { ctx := context.Background() other := &store.User{ - ID: "user_other_add", + ID: tid("user_other_add"), Email: "other-add@example.com", DisplayName: "Other", Role: "member", @@ -955,13 +1076,16 @@ func TestGroupAddMemberAuthz_NonOwnerDenied(t *testing.T) { } group := &store.Group{ - ID: "group_authz_add2", + ID: tid("group_authz_add2"), Name: "Protected Group", Slug: "protected-group-add", - OwnerID: "user_someone_else", + OwnerID: tid("user_someone_else"), Created: time.Now(), Updated: time.Now(), } + if group.OwnerID != "" { + permSeedUser(t, ctx, s, group.OwnerID) + } if err := s.CreateGroup(ctx, group); err != nil { t.Fatalf("failed to create group: %v", err) } @@ -983,7 +1107,7 @@ func TestGroupRemoveMemberAuthz_NonOwnerDenied(t *testing.T) { ctx := context.Background() other := &store.User{ - ID: "user_other_rm", + ID: tid("user_other_rm"), Email: "other-rm@example.com", DisplayName: "Other", Role: "member", @@ -995,13 +1119,16 @@ func TestGroupRemoveMemberAuthz_NonOwnerDenied(t *testing.T) { } group := &store.Group{ - ID: "group_authz_rm", + ID: tid("group_authz_rm"), Name: "Protected Group", Slug: "protected-group-rm", - OwnerID: "user_someone_else", + OwnerID: tid("user_someone_else"), Created: time.Now(), Updated: time.Now(), } + if group.OwnerID != "" { + permSeedUser(t, ctx, s, group.OwnerID) + } if err := s.CreateGroup(ctx, group); err != nil { t.Fatalf("failed to create group: %v", err) } @@ -1010,15 +1137,16 @@ func TestGroupRemoveMemberAuthz_NonOwnerDenied(t *testing.T) { member := &store.GroupMember{ GroupID: group.ID, MemberType: "user", - MemberID: "user_existing", + MemberID: tid("user_existing"), Role: "member", AddedAt: time.Now(), } + permSeedMember(t, ctx, s, member) if err := s.AddGroupMember(ctx, member); err != nil { t.Fatalf("failed to add member: %v", err) } - rec := doGroupRequestAsUser(t, srv, other, http.MethodDelete, "/api/v1/groups/"+group.ID+"/members/user/user_existing", nil) + rec := doGroupRequestAsUser(t, srv, other, http.MethodDelete, "/api/v1/groups/"+group.ID+"/members/user/"+tid("user_existing"), nil) if rec.Code != http.StatusForbidden { t.Errorf("expected 403 for non-owner remove member, got %d: %s", rec.Code, rec.Body.String()) @@ -1030,7 +1158,7 @@ func TestGroupRemoveMemberAuthz_OwnerAllowed(t *testing.T) { ctx := context.Background() owner := &store.User{ - ID: "user_owner_rm", + ID: tid("user_owner_rm"), Email: "owner-rm@example.com", DisplayName: "Owner", Role: "member", @@ -1042,13 +1170,16 @@ func TestGroupRemoveMemberAuthz_OwnerAllowed(t *testing.T) { } group := &store.Group{ - ID: "group_authz_rm2", + ID: tid("group_authz_rm2"), Name: "Owned Group", Slug: "owned-group-rm", OwnerID: owner.ID, Created: time.Now(), Updated: time.Now(), } + if group.OwnerID != "" { + permSeedUser(t, ctx, s, group.OwnerID) + } if err := s.CreateGroup(ctx, group); err != nil { t.Fatalf("failed to create group: %v", err) } @@ -1056,15 +1187,16 @@ func TestGroupRemoveMemberAuthz_OwnerAllowed(t *testing.T) { member := &store.GroupMember{ GroupID: group.ID, MemberType: "user", - MemberID: "user_to_remove", + MemberID: tid("user_to_remove"), Role: "member", AddedAt: time.Now(), } + permSeedMember(t, ctx, s, member) if err := s.AddGroupMember(ctx, member); err != nil { t.Fatalf("failed to add member: %v", err) } - rec := doGroupRequestAsUser(t, srv, owner, http.MethodDelete, "/api/v1/groups/"+group.ID+"/members/user/user_to_remove", nil) + rec := doGroupRequestAsUser(t, srv, owner, http.MethodDelete, "/api/v1/groups/"+group.ID+"/members/user/"+tid("user_to_remove"), nil) if rec.Code != http.StatusNoContent { t.Errorf("expected 204 for owner remove member, got %d: %s", rec.Code, rec.Body.String()) @@ -1082,7 +1214,7 @@ func TestPolicyList(t *testing.T) { // Create some test policies for i := 0; i < 3; i++ { policy := &store.Policy{ - ID: "policy_" + string(rune('a'+i)), + ID: tid("policy_" + string(rune('a'+i))), Name: "Test Policy " + string(rune('A'+i)), ScopeType: "hub", ResourceType: "*", @@ -1196,7 +1328,7 @@ func TestPolicyGet(t *testing.T) { ctx := context.Background() policy := &store.Policy{ - ID: "policy_get123", + ID: tid("policy_get123"), Name: "Test Policy", ScopeType: "hub", ResourceType: "*", @@ -1230,7 +1362,7 @@ func TestPolicyUpdate(t *testing.T) { ctx := context.Background() policy := &store.Policy{ - ID: "policy_upd123", + ID: tid("policy_upd123"), Name: "Original Policy", ScopeType: "hub", ResourceType: "*", @@ -1279,7 +1411,7 @@ func TestPolicyDelete(t *testing.T) { ctx := context.Background() policy := &store.Policy{ - ID: "policy_del123", + ID: tid("policy_del123"), Name: "Delete Me", ScopeType: "hub", ResourceType: "*", @@ -1310,7 +1442,7 @@ func TestPolicyBindingsAdd(t *testing.T) { ctx := context.Background() policy := &store.Policy{ - ID: "policy_bind123", + ID: tid("policy_bind123"), Name: "Test Policy", ScopeType: "hub", ResourceType: "*", @@ -1325,8 +1457,9 @@ func TestPolicyBindingsAdd(t *testing.T) { body := AddPolicyBindingRequest{ PrincipalType: "user", - PrincipalID: "user_abc123", + PrincipalID: tid("user_abc123"), } + permSeedUser(t, ctx, s, tid("user_abc123")) rec := doRequest(t, srv, http.MethodPost, "/api/v1/policies/"+policy.ID+"/bindings", body) @@ -1339,7 +1472,7 @@ func TestPolicyBindingsAdd(t *testing.T) { t.Fatalf("failed to decode response: %v", err) } - if resp.PrincipalID != "user_abc123" { + if resp.PrincipalID != tid("user_abc123") { t.Errorf("expected principalId 'user_abc123', got %q", resp.PrincipalID) } } @@ -1349,7 +1482,7 @@ func TestPolicyBindingsList(t *testing.T) { ctx := context.Background() policy := &store.Policy{ - ID: "policy_blst123", + ID: tid("policy_blst123"), Name: "Test Policy", ScopeType: "hub", ResourceType: "*", @@ -1367,8 +1500,9 @@ func TestPolicyBindingsList(t *testing.T) { binding := &store.PolicyBinding{ PolicyID: policy.ID, PrincipalType: "user", - PrincipalID: "user_" + string(rune('a'+i)), + PrincipalID: tid("user_" + string(rune('a'+i))), } + permSeedPrincipal(t, ctx, s, binding.PrincipalType, binding.PrincipalID) if err := s.AddPolicyBinding(ctx, binding); err != nil { t.Fatalf("failed to add binding: %v", err) } @@ -1395,7 +1529,7 @@ func TestPolicyBindingRemove(t *testing.T) { ctx := context.Background() policy := &store.Policy{ - ID: "policy_brem123", + ID: tid("policy_brem123"), Name: "Test Policy", ScopeType: "hub", ResourceType: "*", @@ -1411,13 +1545,14 @@ func TestPolicyBindingRemove(t *testing.T) { binding := &store.PolicyBinding{ PolicyID: policy.ID, PrincipalType: "user", - PrincipalID: "user_remove", + PrincipalID: tid("user_remove"), } + permSeedPrincipal(t, ctx, s, binding.PrincipalType, binding.PrincipalID) if err := s.AddPolicyBinding(ctx, binding); err != nil { t.Fatalf("failed to add binding: %v", err) } - rec := doRequest(t, srv, http.MethodDelete, "/api/v1/policies/"+policy.ID+"/bindings/user/user_remove", nil) + rec := doRequest(t, srv, http.MethodDelete, "/api/v1/policies/"+policy.ID+"/bindings/user/"+tid("user_remove"), nil) if rec.Code != http.StatusNoContent { t.Errorf("expected status 204, got %d: %s", rec.Code, rec.Body.String()) @@ -1445,21 +1580,21 @@ func TestGetEffectiveGroups(t *testing.T) { // Create a group hierarchy: A contains B, B contains C // User is a member of C, should also be effective member of B and A groupA := &store.Group{ - ID: "group_eff_a", + ID: tid("group_eff_a"), Name: "Group A", Slug: "group-eff-a", Created: time.Now(), Updated: time.Now(), } groupB := &store.Group{ - ID: "group_eff_b", + ID: tid("group_eff_b"), Name: "Group B", Slug: "group-eff-b", Created: time.Now(), Updated: time.Now(), } groupC := &store.Group{ - ID: "group_eff_c", + ID: tid("group_eff_c"), Name: "Group C", Slug: "group-eff-c", Created: time.Now(), @@ -1467,6 +1602,9 @@ func TestGetEffectiveGroups(t *testing.T) { } for _, g := range []*store.Group{groupA, groupB, groupC} { + if g.OwnerID != "" { + permSeedUser(t, ctx, s, g.OwnerID) + } if err := s.CreateGroup(ctx, g); err != nil { t.Fatalf("failed to create group %s: %v", g.ID, err) } @@ -1495,10 +1633,17 @@ func TestGetEffectiveGroups(t *testing.T) { } // User is member of C + permSeedMember(t, ctx, s, &store.GroupMember{ + GroupID: groupC.ID, + MemberType: "user", + MemberID: tid("test_user"), + Role: "member", + AddedAt: time.Now(), + }) if err := s.AddGroupMember(ctx, &store.GroupMember{ GroupID: groupC.ID, MemberType: "user", - MemberID: "test_user", + MemberID: tid("test_user"), Role: "member", AddedAt: time.Now(), }); err != nil { @@ -1506,7 +1651,7 @@ func TestGetEffectiveGroups(t *testing.T) { } // Get effective groups for user - effectiveGroups, err := s.GetEffectiveGroups(ctx, "test_user") + effectiveGroups, err := s.GetEffectiveGroups(ctx, tid("test_user")) if err != nil { t.Fatalf("failed to get effective groups: %v", err) } @@ -1535,7 +1680,7 @@ func TestGetPoliciesForPrincipal(t *testing.T) { // Create a policy policy := &store.Policy{ - ID: "policy_forprinc", + ID: tid("policy_forprinc"), Name: "Test Policy", ScopeType: "hub", ResourceType: "*", @@ -1549,16 +1694,17 @@ func TestGetPoliciesForPrincipal(t *testing.T) { } // Bind to user + permSeedPrincipal(t, ctx, s, "user", tid("test_user")) if err := s.AddPolicyBinding(ctx, &store.PolicyBinding{ PolicyID: policy.ID, PrincipalType: "user", - PrincipalID: "test_user", + PrincipalID: tid("test_user"), }); err != nil { t.Fatalf("failed to add binding: %v", err) } // Get policies for user - policies, err := s.GetPoliciesForPrincipal(ctx, "user", "test_user") + policies, err := s.GetPoliciesForPrincipal(ctx, "user", tid("test_user")) if err != nil { t.Fatalf("failed to get policies: %v", err) } diff --git a/pkg/hub/handlers_principals_test.go b/pkg/hub/handlers_principals_test.go index f05b77c64..1720952ff 100644 --- a/pkg/hub/handlers_principals_test.go +++ b/pkg/hub/handlers_principals_test.go @@ -67,14 +67,14 @@ func TestAgentGroups(t *testing.T) { ctx := context.Background() project := &store.Project{ - ID: "project-1", + ID: tid("project-1"), Name: "Test Project", Slug: "test-project", } require.NoError(t, s.CreateProject(ctx, project)) agent := &store.Agent{ - ID: "agent-1", + ID: tid("agent-1"), Slug: "agent-1-slug", Name: "Agent 1", ProjectID: project.ID, @@ -82,7 +82,7 @@ func TestAgentGroups(t *testing.T) { } require.NoError(t, s.CreateAgent(ctx, agent)) - rec := doRequest(t, srv, http.MethodGet, "/api/v1/agents/agent-1/groups", nil) + rec := doRequest(t, srv, http.MethodGet, "/api/v1/agents/"+tid("agent-1")+"/groups", nil) assert.Equal(t, http.StatusOK, rec.Code) @@ -104,7 +104,7 @@ func TestPrincipalResolve_User(t *testing.T) { ctx := context.Background() user := &store.User{ - ID: "user-1", + ID: tid("user-1"), Email: "alice@example.com", DisplayName: "Alice", Role: "member", @@ -112,14 +112,14 @@ func TestPrincipalResolve_User(t *testing.T) { } require.NoError(t, s.CreateUser(ctx, user)) - rec := doRequest(t, srv, http.MethodGet, "/api/v1/principals/user/user-1", nil) + rec := doRequest(t, srv, http.MethodGet, "/api/v1/principals/user/"+tid("user-1"), nil) assert.Equal(t, http.StatusOK, rec.Code) var resp PrincipalResolutionResponse require.NoError(t, json.NewDecoder(rec.Body).Decode(&resp)) assert.Equal(t, "user", resp.Principal.Type) - assert.Equal(t, "user-1", resp.Principal.ID) + assert.Equal(t, tid("user-1"), resp.Principal.ID) assert.Equal(t, "Alice", resp.Principal.DisplayName) assert.NotNil(t, resp.DirectGroups) assert.NotNil(t, resp.EffectiveGroups) @@ -130,14 +130,14 @@ func TestPrincipalResolve_Agent(t *testing.T) { ctx := context.Background() project := &store.Project{ - ID: "project-1", + ID: tid("project-1"), Name: "Test Project", Slug: "test-project", } require.NoError(t, s.CreateProject(ctx, project)) agent := &store.Agent{ - ID: "agent-1", + ID: tid("agent-1"), Slug: "agent-1-slug", Name: "Agent 1", ProjectID: project.ID, @@ -145,16 +145,16 @@ func TestPrincipalResolve_Agent(t *testing.T) { } require.NoError(t, s.CreateAgent(ctx, agent)) - rec := doRequest(t, srv, http.MethodGet, "/api/v1/principals/agent/agent-1", nil) + rec := doRequest(t, srv, http.MethodGet, "/api/v1/principals/agent/"+tid("agent-1"), nil) assert.Equal(t, http.StatusOK, rec.Code) var resp PrincipalResolutionResponse require.NoError(t, json.NewDecoder(rec.Body).Decode(&resp)) assert.Equal(t, "agent", resp.Principal.Type) - assert.Equal(t, "agent-1", resp.Principal.ID) + assert.Equal(t, tid("agent-1"), resp.Principal.ID) assert.Equal(t, "Agent 1", resp.Principal.DisplayName) - assert.Equal(t, "project-1", resp.Principal.ProjectID) + assert.Equal(t, tid("project-1"), resp.Principal.ProjectID) assert.NotNil(t, resp.DirectGroups) assert.NotNil(t, resp.EffectiveGroups) } @@ -164,21 +164,21 @@ func TestPrincipalResolve_Group(t *testing.T) { ctx := context.Background() group := &store.Group{ - ID: "group-1", + ID: tid("group-1"), Name: "Platform Team", Slug: "platform-team", Description: "The platform team", } require.NoError(t, s.CreateGroup(ctx, group)) - rec := doRequest(t, srv, http.MethodGet, "/api/v1/principals/group/group-1", nil) + rec := doRequest(t, srv, http.MethodGet, "/api/v1/principals/group/"+tid("group-1"), nil) assert.Equal(t, http.StatusOK, rec.Code) var resp PrincipalResolutionResponse require.NoError(t, json.NewDecoder(rec.Body).Decode(&resp)) assert.Equal(t, "group", resp.Principal.Type) - assert.Equal(t, "group-1", resp.Principal.ID) + assert.Equal(t, tid("group-1"), resp.Principal.ID) assert.Equal(t, "Platform Team", resp.Principal.DisplayName) assert.Empty(t, resp.DirectGroups) assert.Empty(t, resp.EffectiveGroups) @@ -223,7 +223,7 @@ func TestPrincipalResolve_AgentWithCreator(t *testing.T) { // Create a user as the agent's creator user := &store.User{ - ID: "creator-1", + ID: tid("creator-1"), Email: "creator@example.com", DisplayName: "Creator User", Role: "member", @@ -232,33 +232,33 @@ func TestPrincipalResolve_AgentWithCreator(t *testing.T) { require.NoError(t, s.CreateUser(ctx, user)) project := &store.Project{ - ID: "project-1", + ID: tid("project-1"), Name: "Test Project", Slug: "test-project", } require.NoError(t, s.CreateProject(ctx, project)) agent := &store.Agent{ - ID: "agent-deleg", + ID: tid("agent-deleg"), Slug: "agent-deleg-slug", Name: "Delegated Agent", ProjectID: project.ID, Phase: string(state.PhaseRunning), - CreatedBy: "creator-1", + CreatedBy: tid("creator-1"), } require.NoError(t, s.CreateAgent(ctx, agent)) - rec := doRequest(t, srv, http.MethodGet, "/api/v1/principals/agent/agent-deleg", nil) + rec := doRequest(t, srv, http.MethodGet, "/api/v1/principals/agent/"+tid("agent-deleg"), nil) assert.Equal(t, http.StatusOK, rec.Code) var resp PrincipalResolutionResponse require.NoError(t, json.NewDecoder(rec.Body).Decode(&resp)) assert.Equal(t, "agent", resp.Principal.Type) - assert.Equal(t, "agent-deleg", resp.Principal.ID) + assert.Equal(t, tid("agent-deleg"), resp.Principal.ID) // Should include delegation info require.NotNil(t, resp.DelegatesFrom) assert.Equal(t, "user", resp.DelegatesFrom.Type) - assert.Equal(t, "creator-1", resp.DelegatesFrom.ID) + assert.Equal(t, tid("creator-1"), resp.DelegatesFrom.ID) assert.Equal(t, "Creator User", resp.DelegatesFrom.DisplayName) } diff --git a/pkg/hub/handlers_project_test.go b/pkg/hub/handlers_project_test.go index 2663f605f..b1697eb23 100644 --- a/pkg/hub/handlers_project_test.go +++ b/pkg/hub/handlers_project_test.go @@ -44,32 +44,79 @@ func TestHubManagedProjectPath(t *testing.T) { homeDir, err := os.UserHomeDir() require.NoError(t, err) + // Default (no content in either dir) should resolve to projects/ expected := filepath.Join(homeDir, ".scion", "projects", "my-test-project") assert.Equal(t, expected, path) } -func TestHubManagedProjectPath_EmptyProjectsFallsBackToGroves(t *testing.T) { - // Use a temp directory as HOME to avoid polluting real ~/.scion +func TestHubManagedProjectPath_PrefersProjectsOverGroves(t *testing.T) { tmpHome := t.TempDir() t.Setenv("HOME", tmpHome) - slug := "empty-projects-grove" + slug := "both-dirs-exist" + globalDir := filepath.Join(tmpHome, ".scion") + + // Create both directories with workspace content + projectsDir := filepath.Join(globalDir, "projects", slug) + require.NoError(t, os.MkdirAll(projectsDir, 0755)) + require.NoError(t, os.WriteFile(filepath.Join(projectsDir, "metadata.json"), []byte("{}"), 0644)) + + grovesDir := filepath.Join(globalDir, "groves", slug) + require.NoError(t, os.MkdirAll(grovesDir, 0755)) + require.NoError(t, os.WriteFile(filepath.Join(grovesDir, "README.md"), []byte("# workspace"), 0644)) + + // hubManagedProjectPath should prefer projects/ over legacy groves/ + path, err := hubManagedProjectPath(slug) + require.NoError(t, err) + assert.Equal(t, projectsDir, path, "should prefer projects path over groves path") +} + +func TestHubManagedProjectPath_FallsBackToGrovesWhenProjectsEmpty(t *testing.T) { + tmpHome := t.TempDir() + t.Setenv("HOME", tmpHome) + + slug := "projects-empty-groves-has-content" globalDir := filepath.Join(tmpHome, ".scion") // Create projects/{slug} with only infrastructure dirs (no real content) projectsDir := filepath.Join(globalDir, "projects", slug) - require.NoError(t, os.MkdirAll(filepath.Join(projectsDir, "shared-dirs"), 0755)) require.NoError(t, os.MkdirAll(filepath.Join(projectsDir, ".scion"), 0755)) - // Create groves/{slug} with actual workspace content + // Create groves/{slug} with actual workspace content (legacy) grovesDir := filepath.Join(globalDir, "groves", slug) require.NoError(t, os.MkdirAll(grovesDir, 0755)) require.NoError(t, os.WriteFile(filepath.Join(grovesDir, "README.md"), []byte("# workspace"), 0644)) - // hubManagedProjectPath should fall back to groves/ since projects/ has no real content + // hubManagedProjectPath should fall back to groves/ for backward compatibility path, err := hubManagedProjectPath(slug) require.NoError(t, err) - assert.Equal(t, grovesDir, path, "should fall back to groves path when projects dir only contains infrastructure dirs") + assert.Equal(t, grovesDir, path, "should fall back to legacy groves path when projects dir only contains infrastructure dirs") +} + +func TestHubManagedProjectPath_DefaultsToProjectsWhenNeitherHasContent(t *testing.T) { + tmpHome := t.TempDir() + t.Setenv("HOME", tmpHome) + + slug := "neither-has-content" + globalDir := filepath.Join(tmpHome, ".scion") + + // Create both directories with only infrastructure dirs + grovesDir := filepath.Join(globalDir, "groves", slug) + require.NoError(t, os.MkdirAll(filepath.Join(grovesDir, ".scion"), 0755)) + + projectsDir := filepath.Join(globalDir, "projects", slug) + require.NoError(t, os.MkdirAll(filepath.Join(projectsDir, "shared-dirs"), 0755)) + + // When neither has content, should default to projects/ + path, err := hubManagedProjectPath(slug) + require.NoError(t, err) + assert.Equal(t, projectsDir, path, "should default to projects path when neither dir has workspace content") +} + +func TestHubManagedProjectPath_EmptySlug(t *testing.T) { + _, err := hubManagedProjectPath("") + require.Error(t, err, "empty slug should return an error") + assert.Contains(t, err.Error(), "slug must not be empty") } func TestCreateProject_HubManaged_NoGitRemote(t *testing.T) { @@ -133,7 +180,7 @@ func TestPopulateAgentConfig_HubManagedProject_SetsWorkspace(t *testing.T) { srv, _ := testServer(t) project := &store.Project{ - ID: "project-hub-managed", + ID: tid("project-hub-managed"), Name: "Hub Managed", Slug: "hub-managed", // No GitRemote — hub-managed project @@ -183,7 +230,7 @@ func TestPopulateAgentConfig_GitProject_NoWorkspace(t *testing.T) { srv, _ := testServer(t) project := &store.Project{ - ID: "project-git", + ID: tid("project-git"), Name: "Git Project", Slug: "git-project", GitRemote: "github.com/test/repo", @@ -517,7 +564,7 @@ func TestCreateAgent_HubManagedProject_ExplicitBroker_AutoLinks(t *testing.T) { // Create a runtime broker broker := &store.RuntimeBroker{ - ID: "broker-hub-autolink", + ID: tid("broker-hub-autolink"), Slug: "hub-autolink-broker", Name: "Hub Autolink Broker", Status: store.BrokerStatusOnline, @@ -526,7 +573,7 @@ func TestCreateAgent_HubManagedProject_ExplicitBroker_AutoLinks(t *testing.T) { // Create a hub-managed project (no git remote, no default broker, no providers) project := &store.Project{ - ID: "project-hub-autolink", + ID: tid("project-hub-autolink"), Slug: "hub-autolink", Name: "Hub Autolink Project", // No GitRemote — hub-managed @@ -572,7 +619,7 @@ func TestCreateProject_HubManaged_AutoProvide(t *testing.T) { // Create a broker with auto_provide enabled broker := &store.RuntimeBroker{ - ID: "broker-autoprovide", + ID: tid("broker-autoprovide"), Slug: "autoprovide-broker", Name: "Auto Provide Broker", Status: store.BrokerStatusOnline, @@ -673,15 +720,15 @@ func TestDeleteProject_DeleteAgents_DispatchesToBroker(t *testing.T) { disp := &deleteDispatcher{} srv.SetDispatcher(disp) - project, _, _ := setupOnlineBrokerAgent(t, s, "project-del") + project, broker, agent1 := setupOnlineBrokerAgent(t, s, "project-del") // Create a second agent in the same project agent2 := &store.Agent{ - ID: "agent-online-project-del-2", + ID: tid("agent-online-project-del-2"), Slug: "agent-online-project-del-2-slug", Name: "Agent Online project-del 2", ProjectID: project.ID, - RuntimeBrokerID: "broker-online-project-del", + RuntimeBrokerID: broker.ID, Phase: string(state.PhaseRunning), } require.NoError(t, s.CreateAgent(ctx, agent2)) @@ -700,7 +747,7 @@ func TestDeleteProject_DeleteAgents_DispatchesToBroker(t *testing.T) { assert.ErrorIs(t, err, store.ErrNotFound) // Verify agents cascade-deleted from database - _, err = s.GetAgent(ctx, "agent-online-project-del") + _, err = s.GetAgent(ctx, agent1.ID) assert.ErrorIs(t, err, store.ErrNotFound) _, err = s.GetAgent(ctx, agent2.ID) assert.ErrorIs(t, err, store.ErrNotFound) @@ -734,7 +781,7 @@ func TestCreateAgent_HubManagedProject_NoProviders_NoBroker(t *testing.T) { // Create a hub-managed project with no providers project := &store.Project{ - ID: "project-hub-noproviders", + ID: tid("project-hub-noproviders"), Slug: "hub-noproviders", Name: "No Providers Project", } @@ -761,7 +808,7 @@ func TestAutoLinkProviders_HubManagedProject_NoLocalPath(t *testing.T) { // Create a broker with auto_provide enabled broker := &store.RuntimeBroker{ - ID: "broker-localpath-auto", + ID: tid("broker-localpath-auto"), Slug: "localpath-auto-broker", Name: "LocalPath Auto Broker", Status: store.BrokerStatusOnline, @@ -803,7 +850,7 @@ func TestAutoLinkProviders_GitProject_NoLocalPath(t *testing.T) { // Create a broker with auto_provide enabled broker := &store.RuntimeBroker{ - ID: "broker-localpath-git", + ID: tid("broker-localpath-git"), Slug: "localpath-git-broker", Name: "LocalPath Git Broker", Status: store.BrokerStatusOnline, @@ -839,7 +886,7 @@ func TestDeleteProject_HubManaged_DispatchesCleanupToBrokers(t *testing.T) { // Create a hub-managed project project := &store.Project{ - ID: "project-cleanup-dispatch", + ID: tid("project-cleanup-dispatch"), Slug: "cleanup-dispatch", Name: "Cleanup Dispatch Project", // No GitRemote — hub-managed @@ -848,14 +895,14 @@ func TestDeleteProject_HubManaged_DispatchesCleanupToBrokers(t *testing.T) { // Create two brokers broker1 := &store.RuntimeBroker{ - ID: "broker-cleanup-1", + ID: tid("broker-cleanup-1"), Slug: "cleanup-broker-1", Name: "Cleanup Broker 1", Status: store.BrokerStatusOnline, Endpoint: "http://broker1:9800", } broker2 := &store.RuntimeBroker{ - ID: "broker-cleanup-2", + ID: tid("broker-cleanup-2"), Slug: "cleanup-broker-2", Name: "Cleanup Broker 2", Status: store.BrokerStatusOnline, @@ -866,14 +913,16 @@ func TestDeleteProject_HubManaged_DispatchesCleanupToBrokers(t *testing.T) { // Link both as providers require.NoError(t, s.AddProjectProvider(ctx, &store.ProjectProvider{ - ProjectID: project.ID, - BrokerID: broker1.ID, - LinkedBy: "test", + ProjectID: project.ID, + BrokerID: broker1.ID, + BrokerName: broker1.Name, + LinkedBy: "test", })) require.NoError(t, s.AddProjectProvider(ctx, &store.ProjectProvider{ - ProjectID: project.ID, - BrokerID: broker2.ID, - LinkedBy: "test", + ProjectID: project.ID, + BrokerID: broker2.ID, + BrokerName: broker2.Name, + LinkedBy: "test", })) // Set up a mock client and dispatcher @@ -902,7 +951,7 @@ func TestDeleteProject_HubManaged_SkipsEmbeddedBroker(t *testing.T) { // Create a hub-managed project project := &store.Project{ - ID: "project-cleanup-embedded", + ID: tid("project-cleanup-embedded"), Slug: "cleanup-embedded", Name: "Cleanup Embedded Project", } @@ -910,14 +959,14 @@ func TestDeleteProject_HubManaged_SkipsEmbeddedBroker(t *testing.T) { // Create embedded and remote brokers embeddedBroker := &store.RuntimeBroker{ - ID: "broker-embedded", + ID: tid("broker-embedded"), Slug: "embedded-broker", Name: "Embedded Broker", Status: store.BrokerStatusOnline, Endpoint: "http://localhost:9800", } remoteBroker := &store.RuntimeBroker{ - ID: "broker-remote", + ID: tid("broker-remote"), Slug: "remote-broker", Name: "Remote Broker", Status: store.BrokerStatusOnline, @@ -928,14 +977,16 @@ func TestDeleteProject_HubManaged_SkipsEmbeddedBroker(t *testing.T) { // Link both as providers require.NoError(t, s.AddProjectProvider(ctx, &store.ProjectProvider{ - ProjectID: project.ID, - BrokerID: embeddedBroker.ID, - LinkedBy: "test", + ProjectID: project.ID, + BrokerID: embeddedBroker.ID, + BrokerName: embeddedBroker.Name, + LinkedBy: "test", })) require.NoError(t, s.AddProjectProvider(ctx, &store.ProjectProvider{ - ProjectID: project.ID, - BrokerID: remoteBroker.ID, - LinkedBy: "test", + ProjectID: project.ID, + BrokerID: remoteBroker.ID, + BrokerName: remoteBroker.Name, + LinkedBy: "test", })) // Mark embedded broker @@ -963,7 +1014,7 @@ func TestDeleteProject_GitBacked_NoCleanupDispatched(t *testing.T) { // Create a git-backed project project := &store.Project{ - ID: "project-git-nocleanup", + ID: tid("project-git-nocleanup"), Slug: "git-nocleanup", Name: "Git No Cleanup Project", GitRemote: "github.com/test/nocleanup", @@ -972,7 +1023,7 @@ func TestDeleteProject_GitBacked_NoCleanupDispatched(t *testing.T) { // Create a broker and link as provider broker := &store.RuntimeBroker{ - ID: "broker-git-nocleanup", + ID: tid("broker-git-nocleanup"), Slug: "git-nocleanup-broker", Name: "Git NoCleanup Broker", Status: store.BrokerStatusOnline, @@ -980,9 +1031,10 @@ func TestDeleteProject_GitBacked_NoCleanupDispatched(t *testing.T) { } require.NoError(t, s.CreateRuntimeBroker(ctx, broker)) require.NoError(t, s.AddProjectProvider(ctx, &store.ProjectProvider{ - ProjectID: project.ID, - BrokerID: broker.ID, - LinkedBy: "test", + ProjectID: project.ID, + BrokerID: broker.ID, + BrokerName: broker.Name, + LinkedBy: "test", })) // Set up mock client and dispatcher @@ -1007,7 +1059,7 @@ func TestResolveRuntimeBroker_HubManagedProject_NoLocalPath(t *testing.T) { // Create a runtime broker (not auto-provide — will be explicitly selected) broker := &store.RuntimeBroker{ - ID: "broker-resolve-localpath", + ID: tid("broker-resolve-localpath"), Slug: "resolve-localpath-broker", Name: "Resolve LocalPath Broker", Status: store.BrokerStatusOnline, @@ -1016,7 +1068,7 @@ func TestResolveRuntimeBroker_HubManagedProject_NoLocalPath(t *testing.T) { // Create a hub-managed project with no providers project := &store.Project{ - ID: "project-resolve-localpath", + ID: tid("project-resolve-localpath"), Slug: "resolve-localpath", Name: "Resolve LocalPath Project", } @@ -1050,7 +1102,7 @@ func TestProjectRegisterPreservesProviderLocalPath(t *testing.T) { // Create a broker broker := &store.RuntimeBroker{ - ID: "broker-preserve-path", + ID: tid("broker-preserve-path"), Name: "Preserve Path Broker", Slug: "preserve-path-broker", Status: store.BrokerStatusOnline, @@ -1355,13 +1407,16 @@ func TestProjectRegister_ExistingProject_CreatesMembershipGroup(t *testing.T) { ctx := context.Background() // Create a project directly in the store (simulating one created before - // membership group support was added — no group exists yet). + // membership group support was added — no group exists yet). The creator is + // backfilled as a group owner, so it must reference an existing user. + creatorID := tid("original-creator-id") + permSeedUser(t, ctx, s, creatorID) project := &store.Project{ ID: api.NewUUID(), Name: "Pre-Existing Project", Slug: "pre-existing-project", GitRemote: "github.com/test/pre-existing", - CreatedBy: "original-creator-id", + CreatedBy: creatorID, } require.NoError(t, s.CreateProject(ctx, project)) @@ -1396,7 +1451,7 @@ func TestProjectRegister_ExistingProject_CreatesMembershipGroup(t *testing.T) { ownerIDs[m.MemberID] = true } } - assert.True(t, ownerIDs["original-creator-id"], "original creator should be an owner") + assert.True(t, ownerIDs[creatorID], "original creator should be an owner") assert.True(t, ownerIDs[DevUserID], "linking user should be an owner") } @@ -1473,11 +1528,50 @@ func TestCreateProject_PerAgentGit_NoWorkspaceLabel(t *testing.T) { assert.False(t, project.IsSharedWorkspace()) } +func TestCreateProject_WorktreePerAgent_StampsLabel(t *testing.T) { + srv, _ := testServer(t) + + body := CreateProjectRequest{ + Name: "Worktree Project", + GitRemote: "github.com/test/worktree", + WorkspaceMode: "worktree-per-agent", + } + + rec := doRequest(t, srv, http.MethodPost, "/api/v1/projects", body) + require.Equal(t, http.StatusCreated, rec.Code, "body: %s", rec.Body.String()) + + var project store.Project + require.NoError(t, json.NewDecoder(rec.Body).Decode(&project)) + + assert.Equal(t, store.WorkspaceModeWorktreePerAgent, project.Labels[store.LabelWorkspaceMode], + "worktree-per-agent label should be stamped") + assert.True(t, project.IsWorktreePerAgent(), "project should report as worktree-per-agent") + assert.False(t, project.IsSharedWorkspace(), "project should not report as shared workspace") +} + +func TestCreateProject_WorktreePerAgent_NonGit_NoLabel(t *testing.T) { + srv, _ := testServer(t) + + body := CreateProjectRequest{ + Name: "Non-Git Worktree", + WorkspaceMode: "worktree-per-agent", + } + + rec := doRequest(t, srv, http.MethodPost, "/api/v1/projects", body) + require.Equal(t, http.StatusCreated, rec.Code, "body: %s", rec.Body.String()) + + var project store.Project + require.NoError(t, json.NewDecoder(rec.Body).Decode(&project)) + + assert.Empty(t, project.Labels[store.LabelWorkspaceMode], + "worktree-per-agent label should not be set on non-git projects") +} + func TestPopulateAgentConfig_SharedWorkspace_SetsWorkspaceNotClone(t *testing.T) { srv, _ := testServer(t) project := &store.Project{ - ID: "project-shared-ws", + ID: tid("project-shared-ws"), Name: "Shared WS", Slug: "shared-ws", GitRemote: "github.com/test/shared", @@ -1559,6 +1653,35 @@ func TestPopulateAgentConfig_SharedWorkspace_DefaultsBranch(t *testing.T) { "Branch should default to 'main' when no default-branch label is set") } +func TestPopulateAgentConfig_WorktreePerAgent_SetsCloneNotWorkspace(t *testing.T) { + srv, _ := testServer(t) + + project := &store.Project{ + ID: tid("project-wt"), + Name: "Worktree Project", + Slug: "worktree-proj", + GitRemote: "github.com/test/worktree", + Labels: map[string]string{ + store.LabelWorkspaceMode: store.WorkspaceModeWorktreePerAgent, + "scion.dev/default-branch": "main", + }, + } + + agent := &store.Agent{ + ID: "agent-worktree", + AppliedConfig: &store.AgentAppliedConfig{}, + } + + srv.populateAgentConfig(context.Background(), agent, project, nil) + + assert.NotNil(t, agent.AppliedConfig.GitClone, + "GitClone should be set for worktree-per-agent projects (broker decides how to use it)") + assert.Contains(t, agent.AppliedConfig.GitClone.URL, "worktree", + "GitClone URL should reference the project remote") + assert.Empty(t, agent.AppliedConfig.Workspace, + "Workspace should NOT be set for worktree-per-agent projects") +} + func TestCloneSharedWorkspaceProject_Success(t *testing.T) { srv, _ := testServer(t) @@ -1672,7 +1795,7 @@ func TestResolveCloneToken_FallsBackToCreatorUserToken(t *testing.T) { ctx := context.Background() require.NoError(t, st.CreateSecret(ctx, &store.Secret{ - ID: "sec-user-gh", + ID: tid("sec-user-gh"), Key: "GITHUB_TOKEN", EncryptedValue: "ghp_user_token_123", SecretType: store.SecretTypeEnvironment, @@ -1699,7 +1822,7 @@ func TestResolveCloneToken_PrefersProjectTokenOverUserToken(t *testing.T) { ctx := context.Background() require.NoError(t, st.CreateSecret(ctx, &store.Secret{ - ID: "sec-project-gh", + ID: tid("sec-project-gh"), Key: "GITHUB_TOKEN", EncryptedValue: "ghp_project_token", SecretType: store.SecretTypeEnvironment, @@ -1708,7 +1831,7 @@ func TestResolveCloneToken_PrefersProjectTokenOverUserToken(t *testing.T) { ScopeID: "project-with-both", })) require.NoError(t, st.CreateSecret(ctx, &store.Secret{ - ID: "sec-user-gh-2", + ID: tid("sec-user-gh-2"), Key: "GITHUB_TOKEN", EncryptedValue: "ghp_user_token", SecretType: store.SecretTypeEnvironment, @@ -1804,7 +1927,7 @@ func TestAutoAssociateGitHubInstallation_NoMatch(t *testing.T) { require.NoError(t, st.CreateGitHubInstallation(ctx, inst)) project := &store.Project{ - ID: "project-no-match", + ID: tid("project-no-match"), Name: "No Match", Slug: "no-match", GitRemote: "github.com/myorg/myrepo", @@ -1833,7 +1956,7 @@ func TestAutoAssociateGitHubInstallation_SkipsSuspended(t *testing.T) { require.NoError(t, st.CreateGitHubInstallation(ctx, inst)) project := &store.Project{ - ID: "project-suspended", + ID: tid("project-suspended"), Name: "Suspended", Slug: "suspended", GitRemote: "github.com/myorg/myrepo", @@ -1924,8 +2047,8 @@ func TestCreateProject_ListByGitRemote_ReturnsMultiple(t *testing.T) { // Pre-create two projects for the same git remote. for _, g := range []*store.Project{ - {ID: "g1", Name: "widgets", Slug: "widgets", GitRemote: "github.com/acme/widgets"}, - {ID: "g2", Name: "widgets (1)", Slug: "widgets-1", GitRemote: "github.com/acme/widgets"}, + {ID: tid("g1"), Name: "widgets", Slug: "widgets", GitRemote: "github.com/acme/widgets"}, + {ID: tid("g2"), Name: "widgets (1)", Slug: "widgets-1", GitRemote: "github.com/acme/widgets"}, } { require.NoError(t, s.CreateProject(ctx, g)) } diff --git a/pkg/hub/handlers_scheduled_events_test.go b/pkg/hub/handlers_scheduled_events_test.go index e97955945..71b85b6a7 100644 --- a/pkg/hub/handlers_scheduled_events_test.go +++ b/pkg/hub/handlers_scheduled_events_test.go @@ -39,7 +39,7 @@ func setupScheduledEventTest(t *testing.T) (*Server, store.Store, string) { srv.scheduler.RegisterEventHandler("message", srv.messageEventHandler()) project := &store.Project{ - ID: "project-sched-test", + ID: tid("project-sched-test"), Name: "Scheduler Test Project", Slug: "sched-test-project", } @@ -200,7 +200,7 @@ func TestScheduledEvent_List(t *testing.T) { // Create a couple of events directly in the store for i, status := range []string{store.ScheduledEventPending, store.ScheduledEventFired} { evt := &store.ScheduledEvent{ - ID: "list-evt-" + string(rune('a'+i)), + ID: tid("list-evt-" + string(rune('a'+i))), ProjectID: projectID, EventType: "message", FireAt: time.Now().Add(time.Duration(i+1) * time.Hour), @@ -234,7 +234,7 @@ func TestScheduledEvent_Get(t *testing.T) { ctx := context.Background() evt := &store.ScheduledEvent{ - ID: "get-evt-1", + ID: tid("get-evt-1"), ProjectID: projectID, EventType: "message", FireAt: time.Now().Add(1 * time.Hour), @@ -244,12 +244,12 @@ func TestScheduledEvent_Get(t *testing.T) { } require.NoError(t, s.CreateScheduledEvent(ctx, evt)) - rec := doRequest(t, srv, http.MethodGet, "/api/v1/projects/"+projectID+"/scheduled-events/get-evt-1", nil) + rec := doRequest(t, srv, http.MethodGet, "/api/v1/projects/"+projectID+"/scheduled-events/"+tid("get-evt-1")+"", nil) assert.Equal(t, http.StatusOK, rec.Code) var got store.ScheduledEvent require.NoError(t, json.NewDecoder(rec.Body).Decode(&got)) - assert.Equal(t, "get-evt-1", got.ID) + assert.Equal(t, tid("get-evt-1"), got.ID) assert.Equal(t, "message", got.EventType) } @@ -266,7 +266,7 @@ func TestScheduledEvent_GetWrongProject(t *testing.T) { // Create a second project project2 := &store.Project{ - ID: "project-sched-other", + ID: tid("project-sched-other"), Name: "Other Project", Slug: "other-project", } @@ -274,7 +274,7 @@ func TestScheduledEvent_GetWrongProject(t *testing.T) { // Create event in first project evt := &store.ScheduledEvent{ - ID: "wrong-project-evt", + ID: tid("wrong-project-evt"), ProjectID: projectID, EventType: "message", FireAt: time.Now().Add(1 * time.Hour), @@ -294,7 +294,7 @@ func TestScheduledEvent_Cancel(t *testing.T) { ctx := context.Background() evt := &store.ScheduledEvent{ - ID: "cancel-evt-1", + ID: tid("cancel-evt-1"), ProjectID: projectID, EventType: "message", FireAt: time.Now().Add(1 * time.Hour), @@ -304,11 +304,11 @@ func TestScheduledEvent_Cancel(t *testing.T) { } require.NoError(t, s.CreateScheduledEvent(ctx, evt)) - rec := doRequest(t, srv, http.MethodDelete, "/api/v1/projects/"+projectID+"/scheduled-events/cancel-evt-1", nil) + rec := doRequest(t, srv, http.MethodDelete, "/api/v1/projects/"+projectID+"/scheduled-events/"+tid("cancel-evt-1")+"", nil) assert.Equal(t, http.StatusNoContent, rec.Code) // Verify it was cancelled in the store - got, err := s.GetScheduledEvent(ctx, "cancel-evt-1") + got, err := s.GetScheduledEvent(ctx, tid("cancel-evt-1")) require.NoError(t, err) assert.Equal(t, store.ScheduledEventCancelled, got.Status) } diff --git a/pkg/hub/handlers_schedules_test.go b/pkg/hub/handlers_schedules_test.go index 0ab9c88a7..4d63c8172 100644 --- a/pkg/hub/handlers_schedules_test.go +++ b/pkg/hub/handlers_schedules_test.go @@ -37,7 +37,7 @@ func setupScheduleTest(t *testing.T) (*Server, store.Store, string) { srv.scheduler.RegisterEventHandler("message", srv.messageEventHandler()) project := &store.Project{ - ID: "project-sched-recurring", + ID: tid("project-sched-recurring"), Name: "Schedule Test Project", Slug: "schedule-test-project", } @@ -251,7 +251,7 @@ func TestSchedule_History(t *testing.T) { // Create some events linked to this schedule for i := 0; i < 3; i++ { evt := &store.ScheduledEvent{ - ID: "hist-evt-" + string(rune('a'+i)), + ID: tid("hist-evt-" + string(rune('a'+i))), ProjectID: projectID, EventType: "message", FireAt: created.CreatedAt, @@ -276,7 +276,7 @@ func TestSchedule_ProjectIsolation(t *testing.T) { // Create another project otherProject := &store.Project{ - ID: "project-other-sched", + ID: tid("project-other-sched"), Name: "Other Project", Slug: "other-project-sched", } diff --git a/pkg/hub/handlers_stopall_test.go b/pkg/hub/handlers_stopall_test.go index e46757af9..882d201c2 100644 --- a/pkg/hub/handlers_stopall_test.go +++ b/pkg/hub/handlers_stopall_test.go @@ -35,14 +35,14 @@ func TestStopAllAgents_Global(t *testing.T) { // Create a project project := &store.Project{ - ID: "project-1", + ID: tid("project-1"), Name: "Test Project", Slug: "test-project", } require.NoError(t, s.CreateProject(ctx, project)) // Create running agents - for i, name := range []string{"agent-1", "agent-2", "agent-3"} { + for i, name := range []string{tid("agent-1"), tid("agent-2"), tid("agent-3")} { agent := &store.Agent{ ID: name, Slug: name, @@ -69,7 +69,7 @@ func TestStopAllAgents_Global(t *testing.T) { assert.Equal(t, 2, resp.Total) // Verify agents are stopped in store - for _, name := range []string{"agent-1", "agent-2"} { + for _, name := range []string{tid("agent-1"), tid("agent-2")} { agent, err := s.GetAgent(ctx, name) require.NoError(t, err) assert.Equal(t, string(state.PhaseStopped), agent.Phase) @@ -102,27 +102,27 @@ func TestStopAllAgents_ProjectScoped(t *testing.T) { ctx := context.Background() // Create two projects - project1 := &store.Project{ID: "project-1", Name: "Project 1", Slug: "project-1"} - project2 := &store.Project{ID: "project-2", Name: "Project 2", Slug: "project-2"} + project1 := &store.Project{ID: tid("project-1"), Name: "Project 1", Slug: tid("project-1")} + project2 := &store.Project{ID: tid("project-2"), Name: "Project 2", Slug: tid("project-2")} require.NoError(t, s.CreateProject(ctx, project1)) require.NoError(t, s.CreateProject(ctx, project2)) // Create running agents in both projects require.NoError(t, s.CreateAgent(ctx, &store.Agent{ - ID: "g1-agent-1", Slug: "g1-agent-1", Name: "G1 Agent 1", + ID: tid("g1-agent-1"), Slug: tid("g1-agent-1"), Name: "G1 Agent 1", ProjectID: project1.ID, Phase: string(state.PhaseRunning), })) require.NoError(t, s.CreateAgent(ctx, &store.Agent{ - ID: "g1-agent-2", Slug: "g1-agent-2", Name: "G1 Agent 2", + ID: tid("g1-agent-2"), Slug: tid("g1-agent-2"), Name: "G1 Agent 2", ProjectID: project1.ID, Phase: string(state.PhaseRunning), })) require.NoError(t, s.CreateAgent(ctx, &store.Agent{ - ID: "g2-agent-1", Slug: "g2-agent-1", Name: "G2 Agent 1", + ID: tid("g2-agent-1"), Slug: tid("g2-agent-1"), Name: "G2 Agent 1", ProjectID: project2.ID, Phase: string(state.PhaseRunning), })) t.Run("stops only agents in scoped project", func(t *testing.T) { - rec := doRequest(t, srv, http.MethodPost, "/api/v1/projects/project-1/agents/stop-all", nil) + rec := doRequest(t, srv, http.MethodPost, "/api/v1/projects/"+project1.ID+"/agents/stop-all", nil) assert.Equal(t, http.StatusOK, rec.Code) var resp StopAllAgentsResponse @@ -133,11 +133,11 @@ func TestStopAllAgents_ProjectScoped(t *testing.T) { assert.Equal(t, 2, resp.Total) // Verify project-1 agents are stopped - a1, _ := s.GetAgent(ctx, "g1-agent-1") + a1, _ := s.GetAgent(ctx, tid("g1-agent-1")) assert.Equal(t, string(state.PhaseStopped), a1.Phase) // Verify project-2 agent is still running - a2, _ := s.GetAgent(ctx, "g2-agent-1") + a2, _ := s.GetAgent(ctx, tid("g2-agent-1")) assert.Equal(t, string(state.PhaseRunning), a2.Phase) }) } @@ -166,13 +166,14 @@ func TestStopAllAgents_ProjectOwner_StopsAllAgents(t *testing.T) { ctx := context.Background() // Create running agents owned by different users + permSeedUser(t, ctx, s, tid("user-other")) require.NoError(t, s.CreateAgent(ctx, &store.Agent{ - ID: "alice-agent", Slug: "alice-agent", Name: "Alice Agent", + ID: tid("alice-agent"), Slug: tid("alice-agent"), Name: "Alice Agent", ProjectID: project.ID, OwnerID: alice.ID, Phase: string(state.PhaseRunning), })) require.NoError(t, s.CreateAgent(ctx, &store.Agent{ - ID: "other-agent", Slug: "other-agent", Name: "Other Agent", - ProjectID: project.ID, OwnerID: "user-other", Phase: string(state.PhaseRunning), + ID: tid("other-agent"), Slug: tid("other-agent"), Name: "Other Agent", + ProjectID: project.ID, OwnerID: tid("user-other"), Phase: string(state.PhaseRunning), })) // Alice is project owner — should stop ALL agents, scope = "all" @@ -188,9 +189,9 @@ func TestStopAllAgents_ProjectOwner_StopsAllAgents(t *testing.T) { assert.Equal(t, "all", resp.Scope) // Verify both agents are stopped - a1, _ := s.GetAgent(ctx, "alice-agent") + a1, _ := s.GetAgent(ctx, tid("alice-agent")) assert.Equal(t, string(state.PhaseStopped), a1.Phase) - a2, _ := s.GetAgent(ctx, "other-agent") + a2, _ := s.GetAgent(ctx, tid("other-agent")) assert.Equal(t, string(state.PhaseStopped), a2.Phase) } @@ -200,7 +201,7 @@ func TestStopAllAgents_ProjectMember_StopsOnlyOwnAgents(t *testing.T) { // Create a third user "carol" as a regular project member carol := &store.User{ - ID: "user-carol", + ID: tid("user-carol"), Email: "carol@test.com", DisplayName: "Carol", Role: store.UserRoleMember, @@ -222,16 +223,16 @@ func TestStopAllAgents_ProjectMember_StopsOnlyOwnAgents(t *testing.T) { // Create agents owned by carol and by alice require.NoError(t, s.CreateAgent(ctx, &store.Agent{ - ID: "carol-agent-1", Slug: "carol-agent-1", Name: "Carol Agent 1", + ID: tid("carol-agent-1"), Slug: tid("carol-agent-1"), Name: "Carol Agent 1", ProjectID: project.ID, OwnerID: carol.ID, Phase: string(state.PhaseRunning), })) require.NoError(t, s.CreateAgent(ctx, &store.Agent{ - ID: "carol-agent-2", Slug: "carol-agent-2", Name: "Carol Agent 2", + ID: tid("carol-agent-2"), Slug: tid("carol-agent-2"), Name: "Carol Agent 2", ProjectID: project.ID, OwnerID: carol.ID, Phase: string(state.PhaseRunning), })) require.NoError(t, s.CreateAgent(ctx, &store.Agent{ - ID: "alice-agent", Slug: "alice-agent", Name: "Alice Agent", - ProjectID: project.ID, OwnerID: "user-alice", Phase: string(state.PhaseRunning), + ID: tid("alice-agent"), Slug: tid("alice-agent"), Name: "Alice Agent", + ProjectID: project.ID, OwnerID: tid("user-alice"), Phase: string(state.PhaseRunning), })) // Carol (regular member) should only stop her own agents, scope = "own" @@ -247,13 +248,13 @@ func TestStopAllAgents_ProjectMember_StopsOnlyOwnAgents(t *testing.T) { assert.Equal(t, "own", resp.Scope) // Verify carol's agents are stopped - c1, _ := s.GetAgent(ctx, "carol-agent-1") + c1, _ := s.GetAgent(ctx, tid("carol-agent-1")) assert.Equal(t, string(state.PhaseStopped), c1.Phase) - c2, _ := s.GetAgent(ctx, "carol-agent-2") + c2, _ := s.GetAgent(ctx, tid("carol-agent-2")) assert.Equal(t, string(state.PhaseStopped), c2.Phase) // Verify alice's agent is still running - a1, _ := s.GetAgent(ctx, "alice-agent") + a1, _ := s.GetAgent(ctx, tid("alice-agent")) assert.Equal(t, string(state.PhaseRunning), a1.Phase) } @@ -263,7 +264,7 @@ func TestStopAllAgents_NonMember_Forbidden(t *testing.T) { // Create a running agent in the project require.NoError(t, s.CreateAgent(ctx, &store.Agent{ - ID: "agent-1", Slug: "agent-1", Name: "Agent 1", + ID: tid("agent-1"), Slug: tid("agent-1"), Name: "Agent 1", ProjectID: project.ID, Phase: string(state.PhaseRunning), })) @@ -273,7 +274,7 @@ func TestStopAllAgents_NonMember_Forbidden(t *testing.T) { assert.Equal(t, http.StatusForbidden, rec.Code) // Agent should still be running - a, _ := s.GetAgent(ctx, "agent-1") + a, _ := s.GetAgent(ctx, tid("agent-1")) assert.Equal(t, string(state.PhaseRunning), a.Phase) } diff --git a/pkg/hub/handlers_test.go b/pkg/hub/handlers_test.go index 4b813a566..c5695c32f 100644 --- a/pkg/hub/handlers_test.go +++ b/pkg/hub/handlers_test.go @@ -20,6 +20,7 @@ import ( "bytes" "context" "encoding/json" + "fmt" "net/http" "net/http/httptest" "strings" @@ -29,7 +30,6 @@ import ( "github.com/GoogleCloudPlatform/scion/pkg/agent/state" "github.com/GoogleCloudPlatform/scion/pkg/api" "github.com/GoogleCloudPlatform/scion/pkg/store" - "github.com/GoogleCloudPlatform/scion/pkg/store/sqlite" "github.com/go-jose/go-jose/v4/jwt" ) @@ -40,7 +40,7 @@ const testDevToken = "scion_dev_test_token_for_unit_tests_1234567890" // The server is configured with dev auth enabled using testDevToken. func testServer(t *testing.T) (*Server, store.Store) { t.Helper() - s, err := sqlite.New(":memory:") + s, err := newTestStore(":memory:") if err != nil { if strings.Contains(err.Error(), "sqlite driver not registered") { t.Skip("Skipping test because sqlite driver is not registered (build with -tags sqlite to enable)") @@ -183,7 +183,7 @@ func TestAgentList(t *testing.T) { // Create a project first (agents reference projects) project := &store.Project{ - ID: "project_test123", + ID: tid("project_test123"), Slug: "test-project", Name: "Test Project", GitRemote: "https://github.com/test/repo", @@ -197,8 +197,8 @@ func TestAgentList(t *testing.T) { // Create some test agents for i := 0; i < 3; i++ { agent := &store.Agent{ - ID: "agent_" + string(rune('a'+i)), - Slug: "test-agent-" + string(rune('a'+i)), + ID: tid("agent_" + string(rune('a'+i))), + Slug: tid("test-agent-" + string(rune('a'+i))), Name: "Test Agent " + string(rune('A'+i)), ProjectID: project.ID, Phase: string(state.PhaseStopped), @@ -237,7 +237,7 @@ func TestAgentCreate(t *testing.T) { // Create a runtime broker first broker := &store.RuntimeBroker{ - ID: "host_test123", + ID: tid("host_test123"), Slug: "test-host", Name: "Test Host", Status: store.BrokerStatusOnline, @@ -248,8 +248,8 @@ func TestAgentCreate(t *testing.T) { // Create a project with default runtime broker project := &store.Project{ - ID: "project_abc123", - Slug: "my-project", + ID: tid("project_abc123"), + Slug: tid("my-project"), Name: "My Project", GitRemote: "github.com/test/repo", DefaultRuntimeBrokerID: broker.ID, @@ -316,7 +316,7 @@ func TestAgentCreate_NoTask(t *testing.T) { // Create a runtime broker broker := &store.RuntimeBroker{ - ID: "host_notask", + ID: tid("host_notask"), Slug: "notask-host", Name: "No Task Host", Status: store.BrokerStatusOnline, @@ -327,7 +327,7 @@ func TestAgentCreate_NoTask(t *testing.T) { // Create a project with default runtime broker project := &store.Project{ - ID: "project_notask", + ID: tid("project_notask"), Slug: "notask-project", Name: "No Task Project", GitRemote: "github.com/test/notask", @@ -387,7 +387,7 @@ func TestAgentCreate_NoTaskViaProject(t *testing.T) { // Create a runtime broker broker := &store.RuntimeBroker{ - ID: "host_notask_project", + ID: tid("host_notask_project"), Slug: "notask-project-host", Name: "No Task Project Host", Status: store.BrokerStatusOnline, @@ -398,7 +398,7 @@ func TestAgentCreate_NoTaskViaProject(t *testing.T) { // Create a project with default runtime broker project := &store.Project{ - ID: "project_notask_project", + ID: tid("project_notask_project"), Slug: "notask-project-ep", Name: "No Task Project EP", GitRemote: "github.com/test/notask-project", @@ -454,7 +454,7 @@ func TestAgentCreate_AttachNoTask(t *testing.T) { // Create a runtime broker broker := &store.RuntimeBroker{ - ID: "host_attach", + ID: tid("host_attach"), Slug: "attach-host", Name: "Attach Host", Status: store.BrokerStatusOnline, @@ -465,7 +465,7 @@ func TestAgentCreate_AttachNoTask(t *testing.T) { // Create a project with default runtime broker project := &store.Project{ - ID: "project_attach", + ID: tid("project_attach"), Slug: "attach-project", Name: "Attach Project", GitRemote: "github.com/test/attach", @@ -525,7 +525,7 @@ func TestAgentCreate_SingleProvider(t *testing.T) { // Create a runtime broker broker := &store.RuntimeBroker{ - ID: "host_single", + ID: tid("host_single"), Slug: "single-host", Name: "Single Host", Status: store.BrokerStatusOnline, @@ -536,7 +536,7 @@ func TestAgentCreate_SingleProvider(t *testing.T) { // Create a project WITHOUT a default runtime broker project := &store.Project{ - ID: "project_single", + ID: tid("project_single"), Slug: "single-project", Name: "Single Project", GitRemote: "github.com/test/single", @@ -589,7 +589,7 @@ func TestAgentCreate_SingleOfflineProvider(t *testing.T) { ctx := context.Background() broker := &store.RuntimeBroker{ - ID: "host_single_offline", + ID: tid("host_single_offline"), Slug: "single-host-offline", Name: "Single Host Offline", Status: store.BrokerStatusOffline, @@ -599,7 +599,7 @@ func TestAgentCreate_SingleOfflineProvider(t *testing.T) { } project := &store.Project{ - ID: "project_single_offline", + ID: tid("project_single_offline"), Slug: "single-project-offline", Name: "Single Project Offline", GitRemote: "github.com/test/single-offline", @@ -647,7 +647,7 @@ func TestAgentCreate_MultipleProviders(t *testing.T) { // Create two runtime brokers broker1 := &store.RuntimeBroker{ - ID: "host_multi1", + ID: tid("host_multi1"), Slug: "multi-host-1", Name: "Multi Host 1", Status: store.BrokerStatusOnline, @@ -657,7 +657,7 @@ func TestAgentCreate_MultipleProviders(t *testing.T) { } broker2 := &store.RuntimeBroker{ - ID: "host_multi2", + ID: tid("host_multi2"), Slug: "multi-host-2", Name: "Multi Host 2", Status: store.BrokerStatusOnline, @@ -668,7 +668,7 @@ func TestAgentCreate_MultipleProviders(t *testing.T) { // Create a project WITHOUT a default runtime broker project := &store.Project{ - ID: "project_multi", + ID: tid("project_multi"), Slug: "multi-project", Name: "Multi Project", GitRemote: "github.com/test/multi", @@ -739,7 +739,7 @@ func TestAgentGetByID(t *testing.T) { // Create project and agent project := &store.Project{ - ID: "project_xyz", + ID: tid("project_xyz"), Slug: "project-xyz", Name: "Project XYZ", GitRemote: "https://github.com/test/repo", @@ -751,7 +751,7 @@ func TestAgentGetByID(t *testing.T) { } agent := &store.Agent{ - ID: "agent_test1", + ID: tid("agent_test1"), Slug: "test-agent", Name: "Test Agent", ProjectID: project.ID, @@ -764,7 +764,7 @@ func TestAgentGetByID(t *testing.T) { t.Fatalf("failed to create agent: %v", err) } - rec := doRequest(t, srv, http.MethodGet, "/api/v1/agents/agent_test1", nil) + rec := doRequest(t, srv, http.MethodGet, fmt.Sprintf("/api/v1/agents/%s", tid("agent_test1")), nil) if rec.Code != http.StatusOK { t.Errorf("expected status 200, got %d: %s", rec.Code, rec.Body.String()) @@ -775,7 +775,7 @@ func TestAgentGetByID(t *testing.T) { t.Fatalf("failed to decode response: %v", err) } - if resp.ID != "agent_test1" { + if resp.ID != tid("agent_test1") { t.Errorf("expected ID 'agent_test1', got %q", resp.ID) } } @@ -805,7 +805,7 @@ func TestAgentDelete(t *testing.T) { // Create project and agent project := &store.Project{ - ID: "project_del", + ID: tid("project_del"), Slug: "project-del", Name: "Project Del", GitRemote: "https://github.com/test/repo", @@ -817,7 +817,7 @@ func TestAgentDelete(t *testing.T) { } agent := &store.Agent{ - ID: "agent_delete", + ID: tid("agent_delete"), Slug: "delete-me", Name: "Delete Me", ProjectID: project.ID, @@ -830,14 +830,14 @@ func TestAgentDelete(t *testing.T) { t.Fatalf("failed to create agent: %v", err) } - rec := doRequest(t, srv, http.MethodDelete, "/api/v1/agents/agent_delete", nil) + rec := doRequest(t, srv, http.MethodDelete, fmt.Sprintf("/api/v1/agents/%s", tid("agent_delete")), nil) if rec.Code != http.StatusNoContent { t.Errorf("expected status 204, got %d: %s", rec.Code, rec.Body.String()) } // Verify agent is deleted - _, err := s.GetAgent(ctx, "agent_delete") + _, err := s.GetAgent(ctx, tid("agent_delete")) if err == nil { t.Error("expected agent to be deleted") } @@ -853,8 +853,8 @@ func TestProjectList(t *testing.T) { for i := 0; i < 2; i++ { project := &store.Project{ - ID: "project_" + string(rune('a'+i)), - Slug: "project-" + string(rune('a'+i)), + ID: tid("project_" + string(rune('a'+i))), + Slug: tid("project-" + string(rune('a'+i))), Name: "Project " + string(rune('A'+i)), GitRemote: "https://github.com/test/repo" + string(rune('a'+i)), Created: time.Now(), @@ -1014,13 +1014,13 @@ func TestProjectRegisterMultipleGitRemoteMatches(t *testing.T) { // Pre-create two projects for the same git remote. project1 := &store.Project{ - ID: "project-1", + ID: tid("project-1"), Name: "widgets", Slug: "widgets", GitRemote: "github.com/acme/widgets", } project2 := &store.Project{ - ID: "project-2", + ID: tid("project-2"), Name: "widgets (2)", Slug: "widgets-2", GitRemote: "github.com/acme/widgets", @@ -1062,7 +1062,7 @@ func TestProjectRegisterMultipleGitRemoteMatches(t *testing.T) { for _, m := range resp.Matches { matchIDs[m.ID] = true } - if !matchIDs["project-1"] || !matchIDs["project-2"] { + if !matchIDs[tid("project-1")] || !matchIDs[tid("project-2")] { t.Errorf("expected matches to include project-1 and project-2, got %v", resp.Matches) } @@ -1136,7 +1136,7 @@ func TestProjectRegisterWithBrokerID(t *testing.T) { // First, create a broker directly (simulating Phase 1 + 2 of two-phase flow) broker := &store.RuntimeBroker{ - ID: "host_twophase_test", + ID: tid("host_twophase_test"), Name: "Two Phase Test Host", Slug: "two-phase-test-host", Status: store.BrokerStatusOnline, @@ -1230,7 +1230,7 @@ func TestAddProvider(t *testing.T) { // Create a project project := &store.Project{ - ID: "project_contrib_test", + ID: tid("project_contrib_test"), Slug: "contrib-test", Name: "Provider Test Project", GitRemote: "https://github.com/test/contrib-test", @@ -1243,7 +1243,7 @@ func TestAddProvider(t *testing.T) { // Create a broker broker := &store.RuntimeBroker{ - ID: "host_contrib_test", + ID: tid("host_contrib_test"), Name: "Provider Test Host", Slug: "contrib-test-host", Status: store.BrokerStatusOnline, @@ -1295,7 +1295,7 @@ func TestListProviders(t *testing.T) { // Create a project project := &store.Project{ - ID: "project_list_contrib", + ID: tid("project_list_contrib"), Slug: "list-contrib", Name: "List Providers Project", Created: time.Now(), @@ -1307,7 +1307,7 @@ func TestListProviders(t *testing.T) { // Create and add a broker as provider broker := &store.RuntimeBroker{ - ID: "host_list_contrib", + ID: tid("host_list_contrib"), Name: "List Providers Host", Slug: "list-contrib-host", Status: store.BrokerStatusOnline, @@ -1352,7 +1352,7 @@ func TestProjectGetByID(t *testing.T) { ctx := context.Background() project := &store.Project{ - ID: "project_gettest", + ID: tid("project_gettest"), Slug: "get-test", Name: "Get Test", GitRemote: "https://github.com/test/get-test", @@ -1363,7 +1363,7 @@ func TestProjectGetByID(t *testing.T) { t.Fatalf("failed to create project: %v", err) } - rec := doRequest(t, srv, http.MethodGet, "/api/v1/projects/project_gettest", nil) + rec := doRequest(t, srv, http.MethodGet, fmt.Sprintf("/api/v1/projects/%s", tid("project_gettest")), nil) if rec.Code != http.StatusOK { t.Errorf("expected status 200, got %d: %s", rec.Code, rec.Body.String()) @@ -1374,7 +1374,7 @@ func TestProjectGetByID(t *testing.T) { t.Fatalf("failed to decode response: %v", err) } - if resp.ID != "project_gettest" { + if resp.ID != tid("project_gettest") { t.Errorf("expected ID 'project_gettest', got %q", resp.ID) } } @@ -1388,7 +1388,7 @@ func TestRuntimeBrokerList(t *testing.T) { ctx := context.Background() broker := &store.RuntimeBroker{ - ID: "host_test1", + ID: tid("host_test1"), Name: "Test Host", Slug: "test-host", Status: store.BrokerStatusOnline, @@ -1422,7 +1422,7 @@ func TestRuntimeBrokerListByName(t *testing.T) { // Create two brokers with different names broker1 := &store.RuntimeBroker{ - ID: "host_name_test1", + ID: tid("host_name_test1"), Name: "Alpha Host", Slug: "alpha-host", Status: store.BrokerStatusOnline, @@ -1431,7 +1431,7 @@ func TestRuntimeBrokerListByName(t *testing.T) { Updated: time.Now(), } broker2 := &store.RuntimeBroker{ - ID: "host_name_test2", + ID: tid("host_name_test2"), Name: "Beta Host", Slug: "beta-host", Status: store.BrokerStatusOnline, @@ -1499,7 +1499,7 @@ func TestRuntimeBrokerDeleteCascadesProviders(t *testing.T) { // Create a broker broker := &store.RuntimeBroker{ - ID: "broker_cascade_test", + ID: tid("broker_cascade_test"), Name: "Cascade Test Broker", Slug: "cascade-test-broker", Status: store.BrokerStatusOnline, @@ -1512,7 +1512,7 @@ func TestRuntimeBrokerDeleteCascadesProviders(t *testing.T) { // Create two projects, one with default_runtime_broker_id pointing to this broker project1 := &store.Project{ - ID: "project_cascade_1", + ID: tid("project_cascade_1"), Name: "Cascade Project 1", Slug: "cascade-project-1", DefaultRuntimeBrokerID: broker.ID, @@ -1520,7 +1520,7 @@ func TestRuntimeBrokerDeleteCascadesProviders(t *testing.T) { Updated: time.Now(), } project2 := &store.Project{ - ID: "project_cascade_2", + ID: tid("project_cascade_2"), Name: "Cascade Project 2", Slug: "cascade-project-2", Created: time.Now(), @@ -1599,7 +1599,7 @@ func TestRuntimeBrokerGetByID(t *testing.T) { ctx := context.Background() broker := &store.RuntimeBroker{ - ID: "host_gettest", + ID: tid("host_gettest"), Name: "Get Test Host", Slug: "get-test-host", Status: store.BrokerStatusOnline, @@ -1611,7 +1611,7 @@ func TestRuntimeBrokerGetByID(t *testing.T) { t.Fatalf("failed to create runtime broker: %v", err) } - rec := doRequest(t, srv, http.MethodGet, "/api/v1/runtime-brokers/host_gettest", nil) + rec := doRequest(t, srv, http.MethodGet, fmt.Sprintf("/api/v1/runtime-brokers/%s", tid("host_gettest")), nil) if rec.Code != http.StatusOK { t.Errorf("expected status 200, got %d: %s", rec.Code, rec.Body.String()) @@ -1622,7 +1622,7 @@ func TestRuntimeBrokerGetByID(t *testing.T) { t.Fatalf("failed to decode response: %v", err) } - if resp.ID != "host_gettest" { + if resp.ID != tid("host_gettest") { t.Errorf("expected ID 'host_gettest', got %q", resp.ID) } } @@ -1633,7 +1633,7 @@ func TestRuntimeBrokerGetByID_CreatedByName(t *testing.T) { // Create a user to be the broker creator if err := s.CreateUser(ctx, &store.User{ - ID: "user_broker_creator", + ID: tid("user_broker_creator"), Email: "creator@test.com", DisplayName: "Broker Creator", Role: "member", @@ -1643,11 +1643,11 @@ func TestRuntimeBrokerGetByID_CreatedByName(t *testing.T) { } broker := &store.RuntimeBroker{ - ID: "broker_createdby_test", + ID: tid("broker_createdby_test"), Name: "CreatedBy Test Broker", Slug: "createdby-test-broker", Status: store.BrokerStatusOnline, - CreatedBy: "user_broker_creator", + CreatedBy: tid("user_broker_creator"), LastHeartbeat: time.Now(), Created: time.Now(), Updated: time.Now(), @@ -1656,7 +1656,7 @@ func TestRuntimeBrokerGetByID_CreatedByName(t *testing.T) { t.Fatalf("failed to create runtime broker: %v", err) } - rec := doRequest(t, srv, http.MethodGet, "/api/v1/runtime-brokers/broker_createdby_test", nil) + rec := doRequest(t, srv, http.MethodGet, fmt.Sprintf("/api/v1/runtime-brokers/%s", tid("broker_createdby_test")), nil) if rec.Code != http.StatusOK { t.Fatalf("expected status 200, got %d: %s", rec.Code, rec.Body.String()) } @@ -1692,7 +1692,7 @@ func TestRuntimeBrokerGetByID_CreatedByNameFallsBackToEmail(t *testing.T) { // Create a user with no display name if err := s.CreateUser(ctx, &store.User{ - ID: "user_no_display", + ID: tid("user_no_display"), Email: "nodisplay@test.com", Role: "member", Status: "active", @@ -1701,11 +1701,11 @@ func TestRuntimeBrokerGetByID_CreatedByNameFallsBackToEmail(t *testing.T) { } broker := &store.RuntimeBroker{ - ID: "broker_email_fallback", + ID: tid("broker_email_fallback"), Name: "Email Fallback Broker", Slug: "email-fallback-broker", Status: store.BrokerStatusOnline, - CreatedBy: "user_no_display", + CreatedBy: tid("user_no_display"), LastHeartbeat: time.Now(), Created: time.Now(), Updated: time.Now(), @@ -1714,7 +1714,7 @@ func TestRuntimeBrokerGetByID_CreatedByNameFallsBackToEmail(t *testing.T) { t.Fatalf("failed to create runtime broker: %v", err) } - rec := doRequest(t, srv, http.MethodGet, "/api/v1/runtime-brokers/broker_email_fallback", nil) + rec := doRequest(t, srv, http.MethodGet, fmt.Sprintf("/api/v1/runtime-brokers/%s", tid("broker_email_fallback")), nil) if rec.Code != http.StatusOK { t.Fatalf("expected status 200, got %d: %s", rec.Code, rec.Body.String()) } @@ -1734,7 +1734,7 @@ func TestRuntimeBrokerList_Capabilities(t *testing.T) { ctx := context.Background() broker := &store.RuntimeBroker{ - ID: "broker_caps_list", + ID: tid("broker_caps_list"), Name: "Caps List Broker", Slug: "caps-list-broker", Status: store.BrokerStatusOnline, @@ -1771,7 +1771,7 @@ func TestRuntimeBrokerList_CreatedByName(t *testing.T) { // Create a user to be the broker creator if err := s.CreateUser(ctx, &store.User{ - ID: "user_list_creator", + ID: tid("user_list_creator"), Email: "listcreator@test.com", DisplayName: "List Creator", Role: "member", @@ -1781,11 +1781,11 @@ func TestRuntimeBrokerList_CreatedByName(t *testing.T) { } broker := &store.RuntimeBroker{ - ID: "broker_list_createdby", + ID: tid("broker_list_createdby"), Name: "List CreatedBy Broker", Slug: "list-createdby-broker", Status: store.BrokerStatusOnline, - CreatedBy: "user_list_creator", + CreatedBy: tid("user_list_creator"), LastHeartbeat: time.Now(), Created: time.Now(), Updated: time.Now(), @@ -1819,7 +1819,7 @@ func TestRuntimeBrokerListWithProjectLocalPath(t *testing.T) { // Create a project project := &store.Project{ - ID: "project_localpath_test", + ID: tid("project_localpath_test"), Name: "Local Path Test Project", Slug: "local-path-test", Visibility: store.VisibilityPrivate, @@ -1832,7 +1832,7 @@ func TestRuntimeBrokerListWithProjectLocalPath(t *testing.T) { // Create a runtime broker broker := &store.RuntimeBroker{ - ID: "host_localpath_test", + ID: tid("host_localpath_test"), Name: "Local Path Test Host", Slug: "local-path-test-host", Status: store.BrokerStatusOnline, @@ -1857,7 +1857,7 @@ func TestRuntimeBrokerListWithProjectLocalPath(t *testing.T) { } // List runtime brokers filtered by project - should include localPath - rec := doRequest(t, srv, http.MethodGet, "/api/v1/runtime-brokers?projectId=project_localpath_test", nil) + rec := doRequest(t, srv, http.MethodGet, fmt.Sprintf("/api/v1/runtime-brokers?projectId=%s", tid("project_localpath_test")), nil) if rec.Code != http.StatusOK { t.Errorf("expected status 200, got %d: %s", rec.Code, rec.Body.String()) @@ -1872,7 +1872,7 @@ func TestRuntimeBrokerListWithProjectLocalPath(t *testing.T) { t.Errorf("expected 1 broker, got %d", len(resp.Brokers)) } - if resp.Brokers[0].ID != "host_localpath_test" { + if resp.Brokers[0].ID != tid("host_localpath_test") { t.Errorf("expected broker ID 'host_localpath_test', got %q", resp.Brokers[0].ID) } @@ -1904,7 +1904,7 @@ func TestRuntimeBrokerListWithProjectLocalPath(t *testing.T) { // testServerWithBrokerAuth creates a test server with broker auth enabled. func testServerWithBrokerAuth(t *testing.T) (*Server, store.Store) { t.Helper() - s, err := sqlite.New(":memory:") + s, err := newTestStore(":memory:") if err != nil { t.Fatalf("failed to create test store: %v", err) } @@ -2052,7 +2052,7 @@ func TestTemplateList(t *testing.T) { ctx := context.Background() template := &store.Template{ - ID: "tmpl_test1", + ID: tid("tmpl_test1"), Slug: "test-template", Name: "Test Template", Harness: "claude", @@ -2088,7 +2088,7 @@ func TestTemplateListByProjectID(t *testing.T) { // Create a global template if err := s.CreateTemplate(ctx, &store.Template{ - ID: "tmpl_global1", Slug: "global-tmpl", Name: "Global Template", + ID: tid("tmpl_global1"), Slug: "global-tmpl", Name: "Global Template", Harness: "claude", Scope: "global", Visibility: store.VisibilityPublic, Status: "active", Created: now, Updated: now, @@ -2098,8 +2098,8 @@ func TestTemplateListByProjectID(t *testing.T) { // Create a project-scoped template for project "project_abc" if err := s.CreateTemplate(ctx, &store.Template{ - ID: "tmpl_project1", Slug: "project-tmpl", Name: "Project Template", - Harness: "gemini", Scope: "project", ScopeID: "project_abc", + ID: tid("tmpl_project1"), Slug: "project-tmpl", Name: "Project Template", + Harness: "gemini", Scope: "project", ScopeID: tid("project_abc"), Visibility: store.VisibilityPublic, Status: "active", Created: now, Updated: now, }); err != nil { @@ -2108,8 +2108,8 @@ func TestTemplateListByProjectID(t *testing.T) { // Create a project-scoped template for a different project if err := s.CreateTemplate(ctx, &store.Template{ - ID: "tmpl_project2", Slug: "other-project-tmpl", Name: "Other Project Template", - Harness: "claude", Scope: "project", ScopeID: "project_xyz", + ID: tid("tmpl_project2"), Slug: "other-project-tmpl", Name: "Other Project Template", + Harness: "claude", Scope: "project", ScopeID: tid("project_xyz"), Visibility: store.VisibilityPublic, Status: "active", Created: now, Updated: now, }); err != nil { @@ -2117,7 +2117,7 @@ func TestTemplateListByProjectID(t *testing.T) { } // Query with projectId=project_abc should return global + project_abc templates only - rec := doRequest(t, srv, http.MethodGet, "/api/v1/templates?projectId=project_abc", nil) + rec := doRequest(t, srv, http.MethodGet, fmt.Sprintf("/api/v1/templates?projectId=%s", tid("project_abc")), nil) if rec.Code != http.StatusOK { t.Fatalf("expected status 200, got %d: %s", rec.Code, rec.Body.String()) } @@ -2136,13 +2136,13 @@ func TestTemplateListByProjectID(t *testing.T) { for _, tmpl := range resp.Templates { ids[tmpl.ID] = true } - if !ids["tmpl_global1"] { + if !ids[tid("tmpl_global1")] { t.Error("expected global template in results") } - if !ids["tmpl_project1"] { + if !ids[tid("tmpl_project1")] { t.Error("expected project_abc template in results") } - if ids["tmpl_project2"] { + if ids[tid("tmpl_project2")] { t.Error("did not expect project_xyz template in results") } } @@ -2191,7 +2191,7 @@ func TestUserList(t *testing.T) { ctx := context.Background() user := &store.User{ - ID: "user_test1", + ID: tid("user_test1"), Email: "test@example.com", DisplayName: "Test User", Role: store.UserRoleMember, @@ -2256,7 +2256,7 @@ func TestInvalidJSON(t *testing.T) { // Create a project first project := &store.Project{ - ID: "project_invalid", + ID: tid("project_invalid"), Slug: "invalid-project", Name: "Invalid Project", GitRemote: "https://github.com/test/invalid", @@ -2325,8 +2325,9 @@ func TestCORSPreflight(t *testing.T) { func TestProjectCreateIdempotent(t *testing.T) { srv, _ := testServer(t) + deterministicID := tid("deterministic-id-1234") body := CreateProjectRequest{ - ID: "deterministic-id-1234", + ID: deterministicID, Name: "My Project", Slug: "my-project", GitRemote: "github.com/acme/widgets", @@ -2342,8 +2343,8 @@ func TestProjectCreateIdempotent(t *testing.T) { if err := json.NewDecoder(rec.Body).Decode(&project1); err != nil { t.Fatalf("failed to decode first response: %v", err) } - if project1.ID != "deterministic-id-1234" { - t.Errorf("expected ID %q, got %q", "deterministic-id-1234", project1.ID) + if project1.ID != deterministicID { + t.Errorf("expected ID %q, got %q", deterministicID, project1.ID) } // Second create with same ID — should return 200 with same project @@ -2406,6 +2407,288 @@ func TestProjectCreateWithSlug(t *testing.T) { } } +// ============================================================================ +// Project Rename Tests +// ============================================================================ + +func TestProjectRenameSlug(t *testing.T) { + srv, s := testServer(t) + ctx := context.Background() + + project := &store.Project{ + ID: tid("project_rename1"), + Slug: "old-slug", + Name: "Old Name", + Created: time.Now(), + Updated: time.Now(), + } + if err := s.CreateProject(ctx, project); err != nil { + t.Fatalf("failed to create project: %v", err) + } + + body := map[string]interface{}{ + "name": "New Name", + "slug": "new-slug", + } + + rec := doRequest(t, srv, http.MethodPatch, fmt.Sprintf("/api/v1/projects/%s", tid("project_rename1")), body) + + if rec.Code != http.StatusOK { + t.Fatalf("expected status 200, got %d: %s", rec.Code, rec.Body.String()) + } + + var resp store.Project + if err := json.NewDecoder(rec.Body).Decode(&resp); err != nil { + t.Fatalf("failed to decode response: %v", err) + } + + if resp.Name != "New Name" { + t.Errorf("expected name %q, got %q", "New Name", resp.Name) + } + if resp.Slug != "new-slug" { + t.Errorf("expected slug %q, got %q", "new-slug", resp.Slug) + } + + // Verify the project was actually updated in the store + updated, err := s.GetProject(ctx, tid("project_rename1")) + if err != nil { + t.Fatalf("failed to get project: %v", err) + } + if updated.Slug != "new-slug" { + t.Errorf("store slug not updated: expected %q, got %q", "new-slug", updated.Slug) + } +} + +func TestProjectRenameSlugConflict(t *testing.T) { + srv, s := testServer(t) + ctx := context.Background() + + // Create two projects + project1 := &store.Project{ + ID: tid("project_rename_a"), + Slug: "project-a", + Name: "Project A", + Created: time.Now(), + Updated: time.Now(), + } + project2 := &store.Project{ + ID: tid("project_rename_b"), + Slug: "project-b", + Name: "Project B", + Created: time.Now(), + Updated: time.Now(), + } + if err := s.CreateProject(ctx, project1); err != nil { + t.Fatalf("failed to create project1: %v", err) + } + if err := s.CreateProject(ctx, project2); err != nil { + t.Fatalf("failed to create project2: %v", err) + } + + // Try to rename project-a to project-b's slug + body := map[string]interface{}{ + "slug": "project-b", + } + + rec := doRequest(t, srv, http.MethodPatch, fmt.Sprintf("/api/v1/projects/%s", tid("project_rename_a")), body) + + if rec.Code != http.StatusConflict { + t.Errorf("expected status 409 (conflict), got %d: %s", rec.Code, rec.Body.String()) + } +} + +func TestProjectRenameSlugOnly(t *testing.T) { + srv, s := testServer(t) + ctx := context.Background() + + project := &store.Project{ + ID: tid("project_rename_slug"), + Slug: "original-slug", + Name: "Original Name", + Created: time.Now(), + Updated: time.Now(), + } + if err := s.CreateProject(ctx, project); err != nil { + t.Fatalf("failed to create project: %v", err) + } + + // Rename slug only (no name change) + body := map[string]interface{}{ + "slug": "renamed-slug", + } + + rec := doRequest(t, srv, http.MethodPatch, fmt.Sprintf("/api/v1/projects/%s", tid("project_rename_slug")), body) + + if rec.Code != http.StatusOK { + t.Fatalf("expected status 200, got %d: %s", rec.Code, rec.Body.String()) + } + + var resp store.Project + if err := json.NewDecoder(rec.Body).Decode(&resp); err != nil { + t.Fatalf("failed to decode response: %v", err) + } + + if resp.Slug != "renamed-slug" { + t.Errorf("expected slug %q, got %q", "renamed-slug", resp.Slug) + } + if resp.Name != "Original Name" { + t.Errorf("name should not change: expected %q, got %q", "Original Name", resp.Name) + } +} + +func TestProjectRenameSlugSanitized(t *testing.T) { + srv, s := testServer(t) + ctx := context.Background() + + project := &store.Project{ + ID: tid("project_rename_san"), + Slug: "sanitize-test", + Name: "Sanitize Test", + Created: time.Now(), + Updated: time.Now(), + } + if err := s.CreateProject(ctx, project); err != nil { + t.Fatalf("failed to create project: %v", err) + } + + // Slug with spaces and uppercase should be sanitized + body := map[string]interface{}{ + "slug": "My New Project", + } + + rec := doRequest(t, srv, http.MethodPatch, fmt.Sprintf("/api/v1/projects/%s", tid("project_rename_san")), body) + + if rec.Code != http.StatusOK { + t.Fatalf("expected status 200, got %d: %s", rec.Code, rec.Body.String()) + } + + var resp store.Project + if err := json.NewDecoder(rec.Body).Decode(&resp); err != nil { + t.Fatalf("failed to decode response: %v", err) + } + + if resp.Slug != "my-new-project" { + t.Errorf("expected sanitized slug %q, got %q", "my-new-project", resp.Slug) + } +} + +func TestProjectRenameSameSlugNoOp(t *testing.T) { + srv, s := testServer(t) + ctx := context.Background() + + project := &store.Project{ + ID: tid("project_rename_noop"), + Slug: "same-slug", + Name: "Same Slug", + Created: time.Now(), + Updated: time.Now(), + } + if err := s.CreateProject(ctx, project); err != nil { + t.Fatalf("failed to create project: %v", err) + } + + body := map[string]interface{}{ + "slug": "same-slug", + "name": "Updated Name", + } + + rec := doRequest(t, srv, http.MethodPatch, fmt.Sprintf("/api/v1/projects/%s", tid("project_rename_noop")), body) + + if rec.Code != http.StatusOK { + t.Fatalf("expected status 200, got %d: %s", rec.Code, rec.Body.String()) + } + + var resp store.Project + if err := json.NewDecoder(rec.Body).Decode(&resp); err != nil { + t.Fatalf("failed to decode response: %v", err) + } + + if resp.Name != "Updated Name" { + t.Errorf("expected name %q, got %q", "Updated Name", resp.Name) + } + if resp.Slug != "same-slug" { + t.Errorf("slug should remain %q, got %q", "same-slug", resp.Slug) + } +} + +func TestProjectRenameGroupMigration(t *testing.T) { + srv, s := testServer(t) + ctx := context.Background() + + project := &store.Project{ + ID: tid("project_rename_grp"), + Slug: "grp-old", + Name: "Group Test", + Created: time.Now(), + Updated: time.Now(), + CreatedBy: "test-user", + } + if err := s.CreateProject(ctx, project); err != nil { + t.Fatalf("failed to create project: %v", err) + } + + // Create associated groups (mimicking what createProject does) + agentsGroup := &store.Group{ + ID: api.NewUUID(), + Name: "Group Test Agents", + Slug: "project:grp-old:agents", + GroupType: store.GroupTypeProjectAgents, + ProjectID: tid("project_rename_grp"), + CreatedBy: "test-user", + } + membersGroup := &store.Group{ + ID: api.NewUUID(), + Name: "Group Test Members", + Slug: "project:grp-old:members", + GroupType: store.GroupTypeExplicit, + ProjectID: tid("project_rename_grp"), + CreatedBy: "test-user", + } + if err := s.CreateGroup(ctx, agentsGroup); err != nil { + t.Fatalf("failed to create agents group: %v", err) + } + if err := s.CreateGroup(ctx, membersGroup); err != nil { + t.Fatalf("failed to create members group: %v", err) + } + + // Rename the project + body := map[string]interface{}{ + "name": "Group Test Renamed", + "slug": "grp-new", + } + + rec := doRequest(t, srv, http.MethodPatch, fmt.Sprintf("/api/v1/projects/%s", tid("project_rename_grp")), body) + + if rec.Code != http.StatusOK { + t.Fatalf("expected status 200, got %d: %s", rec.Code, rec.Body.String()) + } + + // Verify groups were migrated + newAgentsGroup, err := s.GetGroupBySlug(ctx, "project:grp-new:agents") + if err != nil { + t.Errorf("agents group not migrated: %v", err) + } else if newAgentsGroup.Name != "Group Test Renamed Agents" { + t.Errorf("agents group name not updated: got %q", newAgentsGroup.Name) + } + + newMembersGroup, err := s.GetGroupBySlug(ctx, "project:grp-new:members") + if err != nil { + t.Errorf("members group not migrated: %v", err) + } else if newMembersGroup.Name != "Group Test Renamed Members" { + t.Errorf("members group name not updated: got %q", newMembersGroup.Name) + } + + // Old slugs should no longer exist + _, err = s.GetGroupBySlug(ctx, "project:grp-old:agents") + if err == nil { + t.Error("old agents group slug should not exist after migration") + } + _, err = s.GetGroupBySlug(ctx, "project:grp-old:members") + if err == nil { + t.Error("old members group slug should not exist after migration") + } +} + // ============================================================================ // Template Slug Display Tests // ============================================================================ @@ -2419,7 +2702,7 @@ func TestAgentCreate_StoresTemplateSlug(t *testing.T) { // Create a runtime broker broker := &store.RuntimeBroker{ - ID: "host_tmpl_slug", + ID: tid("host_tmpl_slug"), Slug: "tmpl-host", Name: "Template Host", Status: store.BrokerStatusOnline, @@ -2430,7 +2713,7 @@ func TestAgentCreate_StoresTemplateSlug(t *testing.T) { // Create a project project := &store.Project{ - ID: "project_tmpl_slug", + ID: tid("project_tmpl_slug"), Slug: "tmpl-project", Name: "Template Project", GitRemote: "github.com/test/tmpl-repo", @@ -2455,7 +2738,7 @@ func TestAgentCreate_StoresTemplateSlug(t *testing.T) { // Create a template with a known slug tmpl := &store.Template{ - ID: "tmpl_uuid_123", + ID: tid("tmpl_uuid_123"), Slug: "my-claude-template", Name: "My Claude Template", Harness: "claude", @@ -2513,7 +2796,7 @@ func TestEnrichAgents_ResolvesTemplateSlug(t *testing.T) { // Create a template tmpl := &store.Template{ - ID: "tmpl_enrich_123", + ID: tid("tmpl_enrich_123"), Slug: "enriched-template", Name: "Enriched Template", Harness: "gemini", @@ -2554,7 +2837,7 @@ func TestEnrichAgent_ResolvesTemplateSlug(t *testing.T) { // Create a template tmpl := &store.Template{ - ID: "tmpl_enrich_single", + ID: tid("tmpl_enrich_single"), Slug: "single-enriched", Name: "Single Enriched", Harness: "claude", @@ -2599,7 +2882,7 @@ func TestOutboundMessage_UnknownRecipient(t *testing.T) { } rb := &store.RuntimeBroker{ - ID: "broker-msg", + ID: tid("broker-msg"), Name: "test-broker", Slug: "test-broker", Endpoint: "http://localhost:9800", @@ -2615,7 +2898,7 @@ func TestOutboundMessage_UnknownRecipient(t *testing.T) { Slug: "sender", ProjectID: project.ID, Phase: "running", - RuntimeBrokerID: "broker-msg", + RuntimeBrokerID: tid("broker-msg"), Visibility: store.VisibilityPrivate, } if err := s.CreateAgent(ctx, agent); err != nil { diff --git a/pkg/hub/harness_capabilities_test.go b/pkg/hub/harness_capabilities_test.go index d1e7cc3c0..1c3dc29d7 100644 --- a/pkg/hub/harness_capabilities_test.go +++ b/pkg/hub/harness_capabilities_test.go @@ -33,11 +33,11 @@ func seedCreatedAgentForHarnessTest(t *testing.T, s store.Store, id, harnessConf t.Helper() ctx := context.Background() - project := &store.Project{ID: "project-" + id, Name: "Project " + id, Slug: "project-" + id} + project := &store.Project{ID: tid("project-" + id), Name: "Project " + id, Slug: "project-" + id} require.NoError(t, s.CreateProject(ctx, project)) agent := &store.Agent{ - ID: "agent-" + id, + ID: tid("agent-" + id), Slug: "agent-" + id, Name: "Agent " + id, ProjectID: project.ID, @@ -127,7 +127,7 @@ func TestGetAgent_CustomHarnessTypeFromHarnessConfig(t *testing.T) { ctx := context.Background() hc := &store.HarnessConfig{ - ID: "hc-custom", + ID: tid("hc-custom"), Name: "custom-harness", Slug: "custom-harness", Harness: "custom-harness", diff --git a/pkg/hub/harness_config_file_handlers.go b/pkg/hub/harness_config_file_handlers.go index 769d81def..01cfb66a1 100644 --- a/pkg/hub/harness_config_file_handlers.go +++ b/pkg/hub/harness_config_file_handlers.go @@ -174,11 +174,6 @@ func (s *Server) handleHarnessConfigFileWrite(w http.ResponseWriter, r *http.Req writeErrorFromErr(w, err, "") return } - if hc.Locked { - Forbidden(w) - return - } - // Limit request body size for both JSON and raw content paths. r.Body = http.MaxBytesReader(w, r.Body, maxHarnessConfigFileSize+4096) @@ -273,11 +268,6 @@ func (s *Server) handleHarnessConfigFileUpload(w http.ResponseWriter, r *http.Re writeErrorFromErr(w, err, "") return } - if hc.Locked { - Forbidden(w) - return - } - // Apply total request body size limit r.Body = http.MaxBytesReader(w, r.Body, maxUploadTotalSize) @@ -391,11 +381,6 @@ func (s *Server) handleHarnessConfigFileDelete(w http.ResponseWriter, r *http.Re writeErrorFromErr(w, err, "") return } - if hc.Locked { - Forbidden(w) - return - } - stor := s.GetStorage() if stor == nil { RuntimeError(w, "Storage not configured") diff --git a/pkg/hub/harness_config_handlers.go b/pkg/hub/harness_config_handlers.go index 5a2f9e457..b385af2dd 100644 --- a/pkg/hub/harness_config_handlers.go +++ b/pkg/hub/harness_config_handlers.go @@ -236,6 +236,8 @@ func (s *Server) handleHarnessConfigByID(w http.ResponseWriter, r *http.Request) s.handleHarnessConfigFinalize(w, r, hcID) case "download": s.handleHarnessConfigDownload(w, r, hcID) + case "clone": + s.handleHarnessConfigClone(w, r, hcID) case "files": s.handleHarnessConfigFiles(w, r, hcID, "") default: @@ -289,11 +291,6 @@ func (s *Server) updateHarnessConfig(w http.ResponseWriter, r *http.Request, id return } - if existing.Locked { - ValidationError(w, "harness config is locked and cannot be modified", nil) - return - } - var hc store.HarnessConfig if err := readJSON(r, &hc); err != nil { BadRequest(w, "Invalid request body: "+err.Error()) @@ -304,8 +301,6 @@ func (s *Server) updateHarnessConfig(w http.ResponseWriter, r *http.Request, id hc.ID = existing.ID hc.Created = existing.Created hc.CreatedBy = existing.CreatedBy - hc.Locked = existing.Locked - if hc.Slug == "" { hc.Slug = api.Slugify(hc.Name) } @@ -327,11 +322,6 @@ func (s *Server) patchHarnessConfig(w http.ResponseWriter, r *http.Request, id s return } - if existing.Locked { - ValidationError(w, "harness config is locked and cannot be modified", nil) - return - } - var updates struct { Name string `json:"name,omitempty"` Slug string `json:"slug,omitempty"` @@ -377,7 +367,6 @@ func (s *Server) deleteHarnessConfig(w http.ResponseWriter, r *http.Request, id query := r.URL.Query() deleteFiles := query.Get("deleteFiles") == "true" - force := query.Get("force") == "true" existing, err := s.store.GetHarnessConfig(ctx, id) if err != nil { @@ -385,9 +374,40 @@ func (s *Server) deleteHarnessConfig(w http.ResponseWriter, r *http.Request, id return } - if existing.Locked && !force { - ValidationError(w, "harness config is locked; use force=true to delete", nil) - return + // Authorize: check source scope for ActionDelete + if existing.Scope == store.HarnessConfigScopeGlobal { + userIdent := GetUserIdentityFromContext(ctx) + if userIdent == nil { + writeError(w, http.StatusUnauthorized, "unauthorized", "Authentication required", nil) + return + } + decision := s.authzService.CheckAccess(ctx, userIdent, Resource{Type: "harness_config"}, ActionDelete) + if !decision.Allowed { + writeError(w, http.StatusForbidden, ErrCodeForbidden, "You do not have permission to delete global resources", nil) + return + } + } else if existing.Scope == store.HarnessConfigScopeProject { + if agentIdent := GetAgentIdentityFromContext(ctx); agentIdent != nil { + if !agentIdent.HasScope(ScopeAgentCreate) { + writeError(w, http.StatusForbidden, ErrCodeForbidden, "Missing required scope", nil) + return + } + if existing.ScopeID != agentIdent.ProjectID() { + writeError(w, http.StatusForbidden, ErrCodeForbidden, "Agents can only manage resources within their own project", nil) + return + } + } else if userIdent := GetUserIdentityFromContext(ctx); userIdent != nil { + decision := s.authzService.CheckAccess(ctx, userIdent, Resource{ + Type: "harness_config", ParentType: "project", ParentID: existing.ScopeID, + }, ActionDelete) + if !decision.Allowed { + writeError(w, http.StatusForbidden, ErrCodeForbidden, "You do not have permission to delete resources in this project", nil) + return + } + } else { + writeError(w, http.StatusUnauthorized, "unauthorized", "Authentication required", nil) + return + } } if deleteFiles && existing.StoragePath != "" { @@ -543,3 +563,135 @@ func (s *Server) handleHarnessConfigDownload(w http.ResponseWriter, r *http.Requ Expires: expires, }) } + +// handleHarnessConfigClone creates a copy of a harness config. +func (s *Server) handleHarnessConfigClone(w http.ResponseWriter, r *http.Request, id string) { + if r.Method != http.MethodPost { + MethodNotAllowed(w) + return + } + + ctx := r.Context() + + source, err := s.store.GetHarnessConfig(ctx, id) + if err != nil { + writeErrorFromErr(w, err, "") + return + } + + var req CloneTemplateRequest + if err := readJSON(r, &req); err != nil { + BadRequest(w, "Invalid request body: "+err.Error()) + return + } + + if req.Name == "" { + ValidationError(w, "name is required", nil) + return + } + + // Resolve scope ID + scopeID := req.ScopeID + if scopeID == "" && req.ProjectID != "" { + scopeID = req.ProjectID + } + + // Authorize: check destination scope for ActionCreate + destScope := req.Scope + if destScope == "" { + destScope = source.Scope + } + if destScope == "" { + destScope = store.HarnessConfigScopeGlobal + } + if destScope == store.HarnessConfigScopeGlobal { + userIdent := GetUserIdentityFromContext(ctx) + if userIdent == nil { + writeError(w, http.StatusUnauthorized, "unauthorized", "Authentication required", nil) + return + } + decision := s.authzService.CheckAccess(ctx, userIdent, Resource{Type: "harness_config"}, ActionCreate) + if !decision.Allowed { + writeError(w, http.StatusForbidden, ErrCodeForbidden, "You do not have permission to create global resources", nil) + return + } + } else if destScope == store.HarnessConfigScopeProject { + if agentIdent := GetAgentIdentityFromContext(ctx); agentIdent != nil { + if !agentIdent.HasScope(ScopeAgentCreate) { + writeError(w, http.StatusForbidden, ErrCodeForbidden, "Missing required scope", nil) + return + } + if scopeID != agentIdent.ProjectID() { + writeError(w, http.StatusForbidden, ErrCodeForbidden, "Agents can only manage resources within their own project", nil) + return + } + } else if userIdent := GetUserIdentityFromContext(ctx); userIdent != nil { + decision := s.authzService.CheckAccess(ctx, userIdent, Resource{ + Type: "harness_config", ParentType: "project", ParentID: scopeID, + }, ActionCreate) + if !decision.Allowed { + writeError(w, http.StatusForbidden, ErrCodeForbidden, "You do not have permission to create resources in this project", nil) + return + } + } else { + writeError(w, http.StatusUnauthorized, "unauthorized", "Authentication required", nil) + return + } + } + + clone := &store.HarnessConfig{ + ID: api.NewUUID(), + Name: req.Name, + Slug: api.Slugify(req.Name), + DisplayName: source.DisplayName, + Description: source.Description, + Harness: source.Harness, + Config: source.Config, + Scope: destScope, + ScopeID: scopeID, + Visibility: req.Visibility, + Status: store.HarnessConfigStatusPending, + } + + if clone.Visibility == "" { + clone.Visibility = source.Visibility + } + + storagePath := storage.HarnessConfigStoragePath(clone.Scope, clone.ScopeID, clone.Slug) + clone.StoragePath = storagePath + + stor := s.GetStorage() + if stor != nil { + clone.StorageBucket = stor.Bucket() + clone.StorageURI = storage.HarnessConfigStorageURI(stor.Bucket(), clone.Scope, clone.ScopeID, clone.Slug) + } + + if stor != nil && len(source.Files) > 0 && source.StoragePath != "" { + for _, file := range source.Files { + srcPath := source.StoragePath + "/" + file.Path + dstPath := storagePath + "/" + file.Path + if _, err := stor.Copy(ctx, srcPath, dstPath); err != nil { + _ = stor.DeletePrefix(ctx, storagePath) + RuntimeError(w, "Failed to copy files: "+err.Error()) + return + } + } + clone.Files = source.Files + clone.ContentHash = source.ContentHash + clone.Status = store.HarnessConfigStatusActive + } + + if err := s.store.CreateHarnessConfig(ctx, clone); err != nil { + if stor != nil { + _ = stor.DeletePrefix(ctx, storagePath) + } + if strings.Contains(err.Error(), "UNIQUE constraint failed") { + writeError(w, http.StatusConflict, "conflict", "A resource with this slug already exists in the target scope. Choose a different name.", nil) + return + } + writeErrorFromErr(w, err, "") + return + } + + writeJSON(w, http.StatusCreated, clone) +} diff --git a/pkg/hub/harness_config_handlers_test.go b/pkg/hub/harness_config_handlers_test.go index c3cdcc0fd..fd6390c1c 100644 --- a/pkg/hub/harness_config_handlers_test.go +++ b/pkg/hub/harness_config_handlers_test.go @@ -19,6 +19,7 @@ package hub import ( "context" "encoding/json" + "fmt" "net/http" "testing" "time" @@ -31,7 +32,7 @@ func TestHarnessConfigList(t *testing.T) { ctx := context.Background() hc := &store.HarnessConfig{ - ID: "hc_test1", + ID: tid("hc_test1"), Slug: "test-hc", Name: "Test HC", Harness: "claude", @@ -68,7 +69,7 @@ func TestHarnessConfigListByProjectID(t *testing.T) { // Create a global harness config if err := s.CreateHarnessConfig(ctx, &store.HarnessConfig{ - ID: "hc_global1", Slug: "global-hc", Name: "Global HC", + ID: tid("hc_global1"), Slug: "global-hc", Name: "Global HC", Harness: "claude", Scope: "global", Visibility: store.VisibilityPublic, Status: store.HarnessConfigStatusActive, Created: now, Updated: now, @@ -78,8 +79,8 @@ func TestHarnessConfigListByProjectID(t *testing.T) { // Create a project-scoped harness config for project "project_abc" if err := s.CreateHarnessConfig(ctx, &store.HarnessConfig{ - ID: "hc_project1", Slug: "project-hc", Name: "Project HC", - Harness: "gemini", Scope: "project", ScopeID: "project_abc", + ID: tid("hc_project1"), Slug: "project-hc", Name: "Project HC", + Harness: "gemini", Scope: "project", ScopeID: tid("project_abc"), Visibility: store.VisibilityPublic, Status: store.HarnessConfigStatusActive, Created: now, Updated: now, }); err != nil { @@ -88,8 +89,8 @@ func TestHarnessConfigListByProjectID(t *testing.T) { // Create a project-scoped harness config for a different project if err := s.CreateHarnessConfig(ctx, &store.HarnessConfig{ - ID: "hc_project2", Slug: "other-project-hc", Name: "Other Project HC", - Harness: "claude", Scope: "project", ScopeID: "project_xyz", + ID: tid("hc_project2"), Slug: "other-project-hc", Name: "Other Project HC", + Harness: "claude", Scope: "project", ScopeID: tid("project_xyz"), Visibility: store.VisibilityPublic, Status: store.HarnessConfigStatusActive, Created: now, Updated: now, }); err != nil { @@ -98,8 +99,8 @@ func TestHarnessConfigListByProjectID(t *testing.T) { // Create a user-scoped harness config if err := s.CreateHarnessConfig(ctx, &store.HarnessConfig{ - ID: "hc_user1", Slug: "user-hc", Name: "User HC", - Harness: "claude", Scope: "user", ScopeID: "user_123", + ID: tid("hc_user1"), Slug: "user-hc", Name: "User HC", + Harness: "claude", Scope: "user", ScopeID: tid("user_123"), Visibility: store.VisibilityPrivate, Status: store.HarnessConfigStatusActive, Created: now, Updated: now, }); err != nil { @@ -107,7 +108,7 @@ func TestHarnessConfigListByProjectID(t *testing.T) { } // Query with projectId=project_abc should return global + project_abc configs only - rec := doRequest(t, srv, http.MethodGet, "/api/v1/harness-configs?projectId=project_abc", nil) + rec := doRequest(t, srv, http.MethodGet, fmt.Sprintf("/api/v1/harness-configs?projectId=%s", tid("project_abc")), nil) if rec.Code != http.StatusOK { t.Fatalf("expected status 200, got %d: %s", rec.Code, rec.Body.String()) } @@ -126,16 +127,16 @@ func TestHarnessConfigListByProjectID(t *testing.T) { for _, hc := range resp.HarnessConfigs { ids[hc.ID] = true } - if !ids["hc_global1"] { + if !ids[tid("hc_global1")] { t.Error("expected global harness config in results") } - if !ids["hc_project1"] { + if !ids[tid("hc_project1")] { t.Error("expected project_abc harness config in results") } - if ids["hc_project2"] { + if ids[tid("hc_project2")] { t.Error("did not expect project_xyz harness config in results") } - if ids["hc_user1"] { + if ids[tid("hc_user1")] { t.Error("did not expect user harness config in results") } } @@ -224,7 +225,7 @@ func TestHarnessConfigGet(t *testing.T) { ctx := context.Background() hc := &store.HarnessConfig{ - ID: "hc_get1", + ID: tid("hc_get1"), Slug: "get-test", Name: "Get Test", Harness: "gemini", @@ -238,7 +239,7 @@ func TestHarnessConfigGet(t *testing.T) { t.Fatalf("failed to create harness config: %v", err) } - rec := doRequest(t, srv, http.MethodGet, "/api/v1/harness-configs/hc_get1", nil) + rec := doRequest(t, srv, http.MethodGet, fmt.Sprintf("/api/v1/harness-configs/%s", tid("hc_get1")), nil) if rec.Code != http.StatusOK { t.Errorf("expected status 200, got %d: %s", rec.Code, rec.Body.String()) @@ -262,7 +263,7 @@ func TestHarnessConfigDelete(t *testing.T) { ctx := context.Background() hc := &store.HarnessConfig{ - ID: "hc_del1", + ID: tid("hc_del1"), Slug: "del-test", Name: "Del Test", Harness: "claude", @@ -276,13 +277,13 @@ func TestHarnessConfigDelete(t *testing.T) { t.Fatalf("failed to create harness config: %v", err) } - rec := doRequest(t, srv, http.MethodDelete, "/api/v1/harness-configs/hc_del1", nil) + rec := doRequest(t, srv, http.MethodDelete, fmt.Sprintf("/api/v1/harness-configs/%s", tid("hc_del1")), nil) if rec.Code != http.StatusNoContent { t.Errorf("expected status 204, got %d: %s", rec.Code, rec.Body.String()) } // Verify deleted - rec = doRequest(t, srv, http.MethodGet, "/api/v1/harness-configs/hc_del1", nil) + rec = doRequest(t, srv, http.MethodGet, fmt.Sprintf("/api/v1/harness-configs/%s", tid("hc_del1")), nil) if rec.Code != http.StatusNotFound { t.Errorf("expected status 404 after delete, got %d", rec.Code) } @@ -293,7 +294,7 @@ func TestHarnessConfigPatch(t *testing.T) { ctx := context.Background() hc := &store.HarnessConfig{ - ID: "hc_patch1", + ID: tid("hc_patch1"), Slug: "patch-test", Name: "Patch Test", Harness: "claude", @@ -312,7 +313,7 @@ func TestHarnessConfigPatch(t *testing.T) { "description": "Updated description", } - rec := doRequest(t, srv, http.MethodPatch, "/api/v1/harness-configs/hc_patch1", body) + rec := doRequest(t, srv, http.MethodPatch, fmt.Sprintf("/api/v1/harness-configs/%s", tid("hc_patch1")), body) if rec.Code != http.StatusOK { t.Errorf("expected status 200, got %d: %s", rec.Code, rec.Body.String()) diff --git a/pkg/hub/heartbeat_timeout_test.go b/pkg/hub/heartbeat_timeout_test.go index 789ebc2bd..f5c6d6acf 100644 --- a/pkg/hub/heartbeat_timeout_test.go +++ b/pkg/hub/heartbeat_timeout_test.go @@ -26,7 +26,6 @@ import ( "github.com/GoogleCloudPlatform/scion/pkg/agent/state" "github.com/GoogleCloudPlatform/scion/pkg/api" "github.com/GoogleCloudPlatform/scion/pkg/store" - "github.com/GoogleCloudPlatform/scion/pkg/store/sqlite" ) // trackingEventPublisher records PublishAgentStatus calls for test assertions. @@ -59,7 +58,7 @@ func (t *trackingEventPublisher) reset() { func setupHeartbeatTestServer(t *testing.T) (*Server, store.Store, *trackingEventPublisher) { t.Helper() - s, err := sqlite.New(":memory:") + s, err := newTestStore(":memory:") if err != nil { t.Fatalf("failed to create test store: %v", err) } diff --git a/pkg/hub/httpdispatcher.go b/pkg/hub/httpdispatcher.go index bf94ed812..9c0e51964 100644 --- a/pkg/hub/httpdispatcher.go +++ b/pkg/hub/httpdispatcher.go @@ -17,6 +17,8 @@ package hub import ( "context" + "encoding/json" + "errors" "fmt" "log/slog" "strings" @@ -24,9 +26,12 @@ import ( "github.com/GoogleCloudPlatform/scion/pkg/api" "github.com/GoogleCloudPlatform/scion/pkg/messages" + "github.com/GoogleCloudPlatform/scion/pkg/observability/dispatchmetrics" "github.com/GoogleCloudPlatform/scion/pkg/secret" "github.com/GoogleCloudPlatform/scion/pkg/store" "github.com/go-jose/go-jose/v4/jwt" + "github.com/google/uuid" + "go.opentelemetry.io/otel/attribute" ) // HTTPRuntimeBrokerClient is an HTTP-based implementation of RuntimeBrokerClient. @@ -61,6 +66,10 @@ func (c *HTTPRuntimeBrokerClient) RestartAgent(ctx context.Context, brokerID, br return c.transport.RestartAgent(ctx, brokerID, brokerEndpoint, agentID, projectID, resolvedEnv) } +func (c *HTTPRuntimeBrokerClient) ResetAuthAgent(ctx context.Context, brokerID, brokerEndpoint, agentID, projectID, token string) error { + return c.transport.ResetAuthAgent(ctx, brokerID, brokerEndpoint, agentID, projectID, token) +} + func (c *HTTPRuntimeBrokerClient) DeleteAgent(ctx context.Context, brokerID, brokerEndpoint, agentID, projectID string, deleteFiles, removeBranch, softDelete bool, deletedAt time.Time) error { return c.transport.DeleteAgent(ctx, brokerID, brokerEndpoint, agentID, projectID, deleteFiles, removeBranch, softDelete, deletedAt) } @@ -95,8 +104,8 @@ func (c *HTTPRuntimeBrokerClient) ExecAgent(ctx context.Context, brokerID, broke return c.transport.ExecAgent(ctx, brokerID, brokerEndpoint, agentID, projectID, command, timeout) } -func (c *HTTPRuntimeBrokerClient) CleanupProject(ctx context.Context, brokerID, brokerEndpoint, projectSlug string) error { - return c.transport.CleanupProject(ctx, brokerID, brokerEndpoint, projectSlug) +func (c *HTTPRuntimeBrokerClient) CleanupProject(ctx context.Context, brokerID, brokerEndpoint, projectSlug, projectID string) error { + return c.transport.CleanupProject(ctx, brokerID, brokerEndpoint, projectSlug, projectID) } // GetClient returns the underlying RuntimeBrokerClient. @@ -121,17 +130,28 @@ type GitHubAppTokenMinter interface { // It looks up the runtime broker endpoint from the store and uses HTTPRuntimeBrokerClient // to make the actual API calls. type HTTPAgentDispatcher struct { - store store.Store - client RuntimeBrokerClient - tokenGenerator AgentTokenGenerator - secretBackend secret.SecretBackend - authzService *AuthzService // Optional authz service for progeny secret verification - githubAppMinter GitHubAppTokenMinter // Optional GitHub App token minter - hubEndpoint string // Hub endpoint URL for agents to call back - hubID string // Hub instance ID for hub-scoped queries - devAuthToken string // Dev auth token to inject into agent env (dev-auth mode only) - debug bool - log *slog.Logger + store store.Store + client RuntimeBrokerClient + tokenGenerator AgentTokenGenerator + secretBackend secret.SecretBackend + authzService *AuthzService // Optional authz service for progeny secret verification + githubAppMinter GitHubAppTokenMinter // Optional GitHub App token minter + hubEndpoint string // Hub endpoint URL for agents to call back + hubID string // Hub instance ID for hub-scoped queries + devAuthToken string // Dev auth token to inject into agent env (dev-auth mode only) + transportMinter TransportTokenMinter // Optional transport token minter for OIDC dispatch + transportAudience string // OIDC audience for transport tokens + debug bool + log *slog.Logger + + // Cross-node dispatch deps (B4-2). When events + commandBus are non-nil + // and client.StartAgent/StopAgent/RestartAgent returns ErrLifecycleDeferred, + // the dispatcher writes durable intent + signals the owning node + waits + // for the terminal phase transition. Nil = cross-node dispatch disabled + // (single-node / SQLite mode: all brokers are local). + events EventPublisher + commandBus CommandBus + dispatchMetrics dispatchmetrics.Recorder } // NewHTTPAgentDispatcher creates a new HTTP-based agent dispatcher. @@ -185,12 +205,33 @@ func (d *HTTPAgentDispatcher) SetAuthzService(a *AuthzService) { d.authzService = a } +// SetTransportMinter sets the transport token minter and audience for injecting +// transport-layer OIDC tokens into agent dispatch payloads. +func (d *HTTPAgentDispatcher) SetTransportMinter(minter TransportTokenMinter, audience string) { + d.transportMinter = minter + d.transportAudience = audience +} + // SetGitHubAppMinter sets the GitHub App token minter for resolving // GitHub App installation tokens during agent credential resolution. func (d *HTTPAgentDispatcher) SetGitHubAppMinter(m GitHubAppTokenMinter) { d.githubAppMinter = m } +// SetCrossNodeDeps wires the event publisher and command bus needed for +// cross-node lifecycle dispatch (B4-2). When both are set and a lifecycle +// op returns ErrLifecycleDeferred, the dispatcher writes durable intent, +// signals the owning node, and waits for the terminal phase. +func (d *HTTPAgentDispatcher) SetCrossNodeDeps(events EventPublisher, bus CommandBus) { + d.events = events + d.commandBus = bus +} + +// SetDispatchMetrics wires the dispatch metrics recorder (B5-2). +func (d *HTTPAgentDispatcher) SetDispatchMetrics(rec dispatchmetrics.Recorder) { + d.dispatchMetrics = rec +} + // getBrokerEndpoint retrieves the endpoint URL for a runtime broker. // Returns an empty string without error when no endpoint is configured, // which is normal for brokers that connect via WebSocket control channel. @@ -212,16 +253,17 @@ func (d *HTTPAgentDispatcher) buildCreateRequest(ctx context.Context, agent *sto // Build the remote create request req := &RemoteCreateAgentRequest{ - RequestID: api.NewUUID(), - ID: agent.ID, - Slug: agent.Slug, - Name: agent.Name, - ProjectID: agent.ProjectID, - UserID: agent.OwnerID, - HubEndpoint: d.hubEndpoint, - ProjectPath: projectInfo.projectPath, - ProjectSlug: projectInfo.projectSlug, - SharedDirs: projectInfo.sharedDirs, + RequestID: api.NewUUID(), + ID: agent.ID, + Slug: agent.Slug, + Name: agent.Name, + ProjectID: agent.ProjectID, + UserID: agent.OwnerID, + HubEndpoint: d.hubEndpoint, + ProjectPath: projectInfo.projectPath, + ProjectSlug: projectInfo.projectSlug, + SharedDirs: projectInfo.sharedDirs, + WorkspaceMode: projectInfo.workspaceMode, } // Propagate attach mode from applied config @@ -282,7 +324,7 @@ func (d *HTTPAgentDispatcher) buildCreateRequest(ctx context.Context, agent *sto workspace := agent.AppliedConfig.Workspace gitClone := agent.AppliedConfig.GitClone // When the broker has a local provider path for this project, clear - // the hub-managed workspace path — the broker will derive its own + // the hub-native workspace path — the broker will derive its own // workspace location from the project path. However, keep GitClone // config: all hub-linked projects with a git remote use clone-based // provisioning (HTTPS + GitHub token) rather than worktree-based, @@ -300,21 +342,19 @@ func (d *HTTPAgentDispatcher) buildCreateRequest(ctx context.Context, agent *sto } } req.Config = &RemoteAgentConfig{ - Template: agent.Template, - Image: agent.AppliedConfig.Image, - HarnessConfig: agent.AppliedConfig.HarnessConfig, - HarnessAuth: agent.AppliedConfig.HarnessAuth, - Task: agent.AppliedConfig.Task, - Workspace: workspace, - Profile: agent.AppliedConfig.Profile, - Branch: agent.AppliedConfig.Branch, - TemplateID: agent.AppliedConfig.TemplateID, - TemplateHash: agent.AppliedConfig.TemplateHash, - HarnessConfigID: agent.AppliedConfig.HarnessConfigID, - HarnessConfigHash: agent.AppliedConfig.HarnessConfigHash, - GitClone: gitClone, - SharedWorkspace: projectInfo.sharedWorkspace, - GCPIdentity: remoteGCPIdentity, + Template: agent.Template, + Image: agent.AppliedConfig.Image, + HarnessConfig: agent.AppliedConfig.HarnessConfig, + HarnessAuth: agent.AppliedConfig.HarnessAuth, + Task: agent.AppliedConfig.Task, + Workspace: workspace, + Profile: agent.AppliedConfig.Profile, + Branch: agent.AppliedConfig.Branch, + TemplateID: agent.AppliedConfig.TemplateID, + TemplateHash: agent.AppliedConfig.TemplateHash, + GitClone: gitClone, + SharedWorkspace: projectInfo.sharedWorkspace, + GCPIdentity: remoteGCPIdentity, } req.ResolvedEnv = agent.AppliedConfig.Env @@ -486,6 +526,23 @@ func (d *HTTPAgentDispatcher) buildCreateRequest(ctx context.Context, agent *sto req.ResolvedEnv["SCION_DEV_TOKEN"] = d.devAuthToken } + // Transport token minting for platform-layer auth (IAP / Cloud Run invoker) + if d.transportMinter != nil && d.transportAudience != "" { + tToken, tExpiry, tErr := d.transportMinter.MintIDToken(ctx, d.transportAudience) + if tErr != nil { + if d.debug { + d.log.Warn("buildCreateRequest: failed to mint transport token", "error", tErr) + } + } else if tToken != "" { + if req.ResolvedEnv == nil { + req.ResolvedEnv = make(map[string]string) + } + req.ResolvedEnv["SCION_TRANSPORT_TOKEN"] = tToken + req.ResolvedEnv["SCION_TRANSPORT_AUDIENCE"] = d.transportAudience + req.ResolvedEnv["SCION_TRANSPORT_TOKEN_EXPIRY"] = tExpiry.UTC().Format(time.RFC3339) + } + } + return req, nil } @@ -494,7 +551,8 @@ type projectDispatchInfo struct { projectPath string projectSlug string sharedDirs []api.SharedDir - sharedWorkspace bool // true for git-workspace hybrid projects + sharedWorkspace bool // true for git-workspace hybrid projects + workspaceMode string // resolved workspace mode label (e.g. "shared", "worktree-per-agent") } func (d *HTTPAgentDispatcher) resolveDispatchProjectPath(ctx context.Context, agent *store.Agent) (string, string) { @@ -504,7 +562,7 @@ func (d *HTTPAgentDispatcher) resolveDispatchProjectPath(ctx context.Context, ag func (d *HTTPAgentDispatcher) resolveDispatchProjectInfo(ctx context.Context, agent *store.Agent) projectDispatchInfo { // Look up the local path for this project on the target runtime broker. - // A provider LocalPath (linked project) takes precedence over hub-managed + // A provider LocalPath (linked project) takes precedence over hub-native // slug resolution, even for projects without a git remote. Only when there // is no provider path and no git remote do we fall back to projectSlug so // the broker resolves the conventional ~/.scion/projects/ path. @@ -521,6 +579,7 @@ func (d *HTTPAgentDispatcher) resolveDispatchProjectInfo(ctx context.Context, ag info.sharedDirs = project.SharedDirs info.sharedWorkspace = project.IsSharedWorkspace() + info.workspaceMode = project.Labels[store.LabelWorkspaceMode] // First check if the broker has a registered local path for this project. if agent.RuntimeBrokerID != "" { @@ -537,7 +596,7 @@ func (d *HTTPAgentDispatcher) resolveDispatchProjectInfo(ctx context.Context, ag } } // If no provider path was found, let the broker resolve the path via - // slug. This applies to both hub-managed projects (no git remote) and + // slug. This applies to both hub-native projects (no git remote) and // git-anchored projects — the broker needs a project identity to create // agent directories under ~/.scion/projects// rather than falling // back to the global project. @@ -697,6 +756,9 @@ func (d *HTTPAgentDispatcher) DispatchAgentCreateWithGather(ctx context.Context, "agent_id", agent.ID, "agent", agent.Name, "brokerElapsed", time.Since(brokerCallStart).String(), "totalElapsed", time.Since(dispatchStart).String()) + if errors.Is(err, ErrLifecycleDeferred) { + return d.deferredCreateWithGather(ctx, agent) + } if err != nil { return nil, err } @@ -711,6 +773,22 @@ func (d *HTTPAgentDispatcher) DispatchAgentCreateWithGather(ctx context.Context, return nil, nil } +// deferredCreateWithGather handles a cross-node create-with-gather via durable dispatch. +func (d *HTTPAgentDispatcher) deferredCreateWithGather(ctx context.Context, agent *store.Agent) (*RemoteEnvRequirementsResponse, error) { + result, err := d.deferredDataOpResult(ctx, agent, "create", &CreateWithGatherDispatchArgs{}) + if err != nil { + return nil, err + } + if result.Result == "" { + return nil, nil + } + var cr CreateWithGatherResult + if err := json.Unmarshal([]byte(result.Result), &cr); err != nil { + return nil, fmt.Errorf("unmarshal create result: %w", err) + } + return cr.EnvRequirements, nil +} + // DispatchFinalizeEnv sends gathered env vars to the broker to complete agent creation. func (d *HTTPAgentDispatcher) DispatchFinalizeEnv(ctx context.Context, agent *store.Agent, env map[string]string) error { if err := requireRuntimeBrokerAssigned(agent); err != nil { @@ -723,6 +801,9 @@ func (d *HTTPAgentDispatcher) DispatchFinalizeEnv(ctx context.Context, agent *st } resp, err := d.client.FinalizeEnv(ctx, agent.RuntimeBrokerID, endpoint, agent.ID, env) + if errors.Is(err, ErrLifecycleDeferred) { + return d.deferredFinalizeEnv(ctx, agent, env) + } if err != nil { return err } @@ -733,6 +814,11 @@ func (d *HTTPAgentDispatcher) DispatchFinalizeEnv(ctx context.Context, agent *st return nil } +// deferredFinalizeEnv handles a cross-node finalize_env via durable dispatch. +func (d *HTTPAgentDispatcher) deferredFinalizeEnv(ctx context.Context, agent *store.Agent, env map[string]string) error { + return d.deferredDataOp(ctx, agent, "finalize_env", &FinalizeEnvDispatchArgs{Env: env}) +} + // resolveEnvFromStorage queries Hub env var storage for all applicable scopes // and returns a merged map with precedence: user > project > global. func (d *HTTPAgentDispatcher) resolveEnvFromStorage(ctx context.Context, agent *store.Agent) (map[string]string, error) { @@ -1003,6 +1089,20 @@ func (d *HTTPAgentDispatcher) DispatchAgentStart(ctx context.Context, agent *sto } } + // Transport token minting for platform-layer auth (IAP / Cloud Run invoker) + if d.transportMinter != nil && d.transportAudience != "" { + tToken, tExpiry, tErr := d.transportMinter.MintIDToken(ctx, d.transportAudience) + if tErr != nil { + if d.debug { + d.log.Warn("DispatchAgentStart: failed to mint transport token", "error", tErr) + } + } else if tToken != "" { + resolvedEnv["SCION_TRANSPORT_TOKEN"] = tToken + resolvedEnv["SCION_TRANSPORT_AUDIENCE"] = d.transportAudience + resolvedEnv["SCION_TRANSPORT_TOKEN_EXPIRY"] = tExpiry.UTC().Format(time.RFC3339) + } + } + // GitHub App token minting for agent start if d.githubAppMinter != nil && agent.ProjectID != "" { project, projectErr := d.store.GetProject(ctx, agent.ProjectID) @@ -1066,6 +1166,11 @@ func (d *HTTPAgentDispatcher) DispatchAgentStart(ctx context.Context, agent *sto } resp, err := d.client.StartAgent(ctx, agent.RuntimeBrokerID, endpoint, agent.Slug, agent.ProjectID, task, projectPath, projectSlug, harnessConfig, resolvedEnv, resolvedSecrets, inlineConfig, projectInfo.sharedDirs, projectInfo.sharedWorkspace) + if errors.Is(err, ErrLifecycleDeferred) { + return d.deferredStart(ctx, agent, &StartDispatchArgs{ + Task: task, + }) + } if err != nil { return err } @@ -1087,7 +1192,11 @@ func (d *HTTPAgentDispatcher) DispatchAgentStop(ctx context.Context, agent *stor return err } - return d.client.StopAgent(ctx, agent.RuntimeBrokerID, endpoint, agent.Slug, agent.ProjectID) + err = d.client.StopAgent(ctx, agent.RuntimeBrokerID, endpoint, agent.Slug, agent.ProjectID) + if errors.Is(err, ErrLifecycleDeferred) { + return d.deferredStop(ctx, agent) + } + return err } // DispatchAgentRestart restarts an agent on the runtime broker. @@ -1141,7 +1250,61 @@ func (d *HTTPAgentDispatcher) DispatchAgentRestart(ctx context.Context, agent *s } } - return d.client.RestartAgent(ctx, agent.RuntimeBrokerID, endpoint, agent.Slug, agent.ProjectID, resolvedEnv) + // Transport token minting for platform-layer auth (IAP / Cloud Run invoker) + if d.transportMinter != nil && d.transportAudience != "" { + tToken, tExpiry, tErr := d.transportMinter.MintIDToken(ctx, d.transportAudience) + if tErr != nil { + if d.debug { + d.log.Warn("DispatchAgentRestart: failed to mint transport token", "error", tErr) + } + } else if tToken != "" { + resolvedEnv["SCION_TRANSPORT_TOKEN"] = tToken + resolvedEnv["SCION_TRANSPORT_AUDIENCE"] = d.transportAudience + resolvedEnv["SCION_TRANSPORT_TOKEN_EXPIRY"] = tExpiry.UTC().Format(time.RFC3339) + } + } + + err = d.client.RestartAgent(ctx, agent.RuntimeBrokerID, endpoint, agent.Slug, agent.ProjectID, resolvedEnv) + if errors.Is(err, ErrLifecycleDeferred) { + return d.deferredRestart(ctx, agent) + } + return err +} + +// DispatchAgentResetAuth injects a fresh auth token into a running agent without +// restarting it. It generates a new token and sends it to the broker's reset-auth +// endpoint, which writes it into the container and signals the agent process. +func (d *HTTPAgentDispatcher) DispatchAgentResetAuth(ctx context.Context, agent *store.Agent) error { + if err := requireRuntimeBrokerAssigned(agent); err != nil { + return err + } + + endpoint, err := d.getBrokerEndpoint(ctx, agent.RuntimeBrokerID) + if err != nil { + return err + } + + var token string + if d.tokenGenerator != nil { + var additionalScopes []AgentTokenScope + if agent.AppliedConfig != nil { + for _, s := range agent.AppliedConfig.HubAccessScopes { + additionalScopes = append(additionalScopes, AgentTokenScope(s)) + } + if gcpID := agent.AppliedConfig.GCPIdentity; gcpID != nil && gcpID.MetadataMode == store.GCPMetadataModeAssign && gcpID.ServiceAccountID != "" { + additionalScopes = append(additionalScopes, GCPTokenScopeForSA(gcpID.ServiceAccountID)) + } + } + token, err = d.tokenGenerator.GenerateAgentToken(agent.ID, agent.ProjectID, agent.Ancestry, additionalScopes...) + if err != nil { + return fmt.Errorf("DispatchAgentResetAuth: failed to generate agent token: %w", err) + } + } + if token == "" { + return fmt.Errorf("DispatchAgentResetAuth: no token generated for agent %s", agent.ID) + } + + return d.client.ResetAuthAgent(ctx, agent.RuntimeBrokerID, endpoint, agent.Slug, agent.ProjectID, token) } // DispatchAgentDelete deletes an agent from the runtime broker. @@ -1155,7 +1318,11 @@ func (d *HTTPAgentDispatcher) DispatchAgentDelete(ctx context.Context, agent *st return err } - return d.client.DeleteAgent(ctx, agent.RuntimeBrokerID, endpoint, agent.Slug, agent.ProjectID, deleteFiles, removeBranch, softDelete, deletedAt) + err = d.client.DeleteAgent(ctx, agent.RuntimeBrokerID, endpoint, agent.Slug, agent.ProjectID, deleteFiles, removeBranch, softDelete, deletedAt) + if errors.Is(err, ErrLifecycleDeferred) { + return d.deferredDelete(ctx, agent, deleteFiles, removeBranch, softDelete, deletedAt) + } + return err } // DispatchAgentMessage sends a message to an agent on the runtime broker. @@ -1211,7 +1378,205 @@ func (d *HTTPAgentDispatcher) DispatchCheckAgentPrompt(ctx context.Context, agen return false, err } - return d.client.CheckAgentPrompt(ctx, agent.RuntimeBrokerID, endpoint, agent.Slug, agent.ProjectID) + hasPrompt, err := d.client.CheckAgentPrompt(ctx, agent.RuntimeBrokerID, endpoint, agent.Slug, agent.ProjectID) + if errors.Is(err, ErrLifecycleDeferred) { + return d.deferredCheckPrompt(ctx, agent) + } + return hasPrompt, err +} + +// deferredCheckPrompt handles a cross-node check_prompt via durable dispatch. +func (d *HTTPAgentDispatcher) deferredCheckPrompt(ctx context.Context, agent *store.Agent) (bool, error) { + result, err := d.deferredDataOpResult(ctx, agent, "check_prompt", &CheckPromptDispatchArgs{}) + if err != nil { + return false, err + } + var cr CheckPromptResult + if result.Result != "" { + if err := json.Unmarshal([]byte(result.Result), &cr); err != nil { + return false, fmt.Errorf("unmarshal check_prompt result: %w", err) + } + } + return cr.HasPrompt, nil +} + +// ============================================================================= +// Cross-node lifecycle dispatch (B4-2) +// ============================================================================= + +// isStartTerminal returns true for terminal phases of a start/restart op. +func isStartTerminal(phase string) bool { return phase == "running" || phase == "error" } + +// isStopTerminal returns true for terminal phases of a stop op. +func isStopTerminal(phase string) bool { return phase == "stopped" || phase == "error" } + +// deferredStart handles a cross-node agent start: subscribe → write intent → +// signal → wait for the terminal phase. Called when client.StartAgent returns +// ErrLifecycleDeferred (broker not locally connected). +func (d *HTTPAgentDispatcher) deferredStart(ctx context.Context, agent *store.Agent, args *StartDispatchArgs) error { + return d.deferredLifecycle(ctx, agent, "start", args, isStartTerminal) +} + +// deferredStop handles a cross-node agent stop. +func (d *HTTPAgentDispatcher) deferredStop(ctx context.Context, agent *store.Agent) error { + return d.deferredLifecycle(ctx, agent, "stop", &StopDispatchArgs{}, isStopTerminal) +} + +// deferredRestart handles a cross-node agent restart. +func (d *HTTPAgentDispatcher) deferredRestart(ctx context.Context, agent *store.Agent) error { + return d.deferredLifecycle(ctx, agent, "restart", &RestartDispatchArgs{}, isStartTerminal) +} + +// deferredDelete handles a cross-node agent delete: subscribe → write intent → +// signal → wait for the dispatch row to reach terminal state. Delete is +// idempotent: 404 from the owner is treated as success. +func (d *HTTPAgentDispatcher) deferredDelete(ctx context.Context, agent *store.Agent, deleteFiles, removeBranch, softDelete bool, deletedAt time.Time) error { + args := &DeleteDispatchArgs{ + DeleteFiles: deleteFiles, + RemoveBranch: removeBranch, + SoftDelete: softDelete, + DeletedAt: deletedAt, + } + return d.deferredDataOp(ctx, agent, "delete", args) +} + +// deferredDataOp is the common flow for cross-node ops that return a result +// via the dispatch row (delete, finalize_env, check_prompt, create): +// 1. Subscribe to broker.dispatch..done BEFORE writing intent +// 2. InsertBrokerDispatch with serialized args +// 3. Best-effort SignalBrokerCmd +// 4. waitForDispatchDone (reads result from the DB row — authoritative) +func (d *HTTPAgentDispatcher) deferredDataOp( + ctx context.Context, + agent *store.Agent, + op string, + args interface{}, +) error { + _, err := d.deferredDataOpResult(ctx, agent, op, args) + return err +} + +// deferredDataOpResult is like deferredDataOp but returns the completed +// dispatch row so callers can read the result JSON. +func (d *HTTPAgentDispatcher) deferredDataOpResult( + ctx context.Context, + agent *store.Agent, + op string, + args interface{}, +) (*store.BrokerDispatch, error) { + if d.events == nil || d.commandBus == nil { + return nil, fmt.Errorf("cross-node dispatch not available: events or command bus not configured") + } + + dispatchID := uuid.NewString() + + // 1. Subscribe BEFORE writing intent so we don't miss events. + eventCh, unsub := d.events.Subscribe("broker.dispatch." + dispatchID + ".done") + + // 2. Serialize args and insert the durable intent row. + argsJSON, err := MarshalDispatchArgs(args) + if err != nil { + unsub() + return nil, fmt.Errorf("marshal dispatch args: %w", err) + } + + dispatch := &store.BrokerDispatch{ + ID: dispatchID, + BrokerID: agent.RuntimeBrokerID, + AgentID: agent.ID, + AgentSlug: agent.Slug, + ProjectID: agent.ProjectID, + Op: op, + Args: argsJSON, + } + if err := d.store.InsertBrokerDispatch(ctx, dispatch); err != nil { + unsub() + return nil, fmt.Errorf("insert dispatch intent: %w", err) + } + if rec := d.dispatchMetrics; rec != nil { + rec.IncPublished(ctx, 1, attribute.String("op", op)) + } + + // 3. Best-effort signal. + if err := d.commandBus.SignalBrokerCmd(ctx, agent.RuntimeBrokerID); err != nil { + d.log.Warn("deferredDataOp: signal failed (durable intent is backstop)", + "op", op, "brokerID", agent.RuntimeBrokerID, "error", err) + } + + // 4. Wait for completion — reads result from the DB row (authoritative). + result, err := waitForDispatchDone(ctx, eventCh, unsub, d.store, dispatchID) + if err != nil { + return nil, err + } + if result.State == store.DispatchStateFailed { + return nil, fmt.Errorf("dispatch %s failed: %s", op, result.Error) + } + return result, nil +} + +// deferredLifecycle is the common flow for cross-node start/stop/restart: +// 1. Subscribe to agent..status BEFORE writing intent (no missed events) +// 2. InsertBrokerDispatch with serialized resolved args +// 3. Best-effort SignalBrokerCmd (the row is durable; reconnect-drain backstop) +// 4. waitForAgentTransition with the op's terminal set +// 5. Return nil on success-terminal, ErrDispatchFailed on timeout, wrapped +// error on error-terminal +func (d *HTTPAgentDispatcher) deferredLifecycle( + ctx context.Context, + agent *store.Agent, + op string, + args interface{}, + terminal func(string) bool, +) error { + if d.events == nil || d.commandBus == nil { + return fmt.Errorf("cross-node dispatch not available: events or command bus not configured") + } + + // 1. Subscribe BEFORE writing intent so we don't miss events. + eventCh, unsub := d.events.Subscribe("agent." + agent.ID + ".status") + + // 2. Serialize args and insert the durable intent row. + argsJSON, err := MarshalDispatchArgs(args) + if err != nil { + unsub() + return fmt.Errorf("marshal dispatch args: %w", err) + } + + dispatch := &store.BrokerDispatch{ + ID: uuid.NewString(), + BrokerID: agent.RuntimeBrokerID, + AgentID: agent.ID, + AgentSlug: agent.Slug, + ProjectID: agent.ProjectID, + Op: op, + Args: argsJSON, + } + if err := d.store.InsertBrokerDispatch(ctx, dispatch); err != nil { + unsub() + return fmt.Errorf("insert dispatch intent: %w", err) + } + if rec := d.dispatchMetrics; rec != nil { + rec.IncPublished(ctx, 1, attribute.String("op", op)) + } + + // 3. Best-effort signal — the row is the durable intent; reconnect-drain + // is the backstop if the signal is missed or no node owns the broker. + if err := d.commandBus.SignalBrokerCmd(ctx, agent.RuntimeBrokerID); err != nil { + d.log.Warn("deferredLifecycle: signal failed (durable intent is backstop)", + "op", op, "brokerID", agent.RuntimeBrokerID, "error", err) + } + + // 4. Wait for terminal phase. + phase, err := waitForAgentTransition(ctx, eventCh, unsub, terminal) + if err != nil { + return err + } + + // 5. Map terminal phase. + if phase == "error" { + return fmt.Errorf("agent entered error phase during %s", op) + } + return nil } // resolveSecrets queries secrets from all applicable scopes and merges them diff --git a/pkg/hub/httpdispatcher_test.go b/pkg/hub/httpdispatcher_test.go index 5cfa7b46d..a491c4ab7 100644 --- a/pkg/hub/httpdispatcher_test.go +++ b/pkg/hub/httpdispatcher_test.go @@ -30,13 +30,12 @@ import ( "github.com/GoogleCloudPlatform/scion/pkg/api" "github.com/GoogleCloudPlatform/scion/pkg/messages" "github.com/GoogleCloudPlatform/scion/pkg/store" - "github.com/GoogleCloudPlatform/scion/pkg/store/sqlite" ) // createTestStore creates an in-memory SQLite store for testing. func createTestStore(t *testing.T) store.Store { t.Helper() - s, err := sqlite.New(":memory:") + s, err := newTestStore(":memory:") if err != nil { t.Fatalf("failed to create test store: %v", err) } @@ -137,6 +136,10 @@ func (m *mockRuntimeBrokerClient) RestartAgent(ctx context.Context, brokerID, br return m.returnErr } +func (m *mockRuntimeBrokerClient) ResetAuthAgent(_ context.Context, _, _, _, _, _ string) error { + return m.returnErr +} + func (m *mockRuntimeBrokerClient) DeleteAgent(ctx context.Context, brokerID, brokerEndpoint, agentID, projectID string, deleteFiles, removeBranch, softDelete bool, deletedAt time.Time) error { m.deleteCalled = true m.lastBrokerID = brokerID @@ -181,7 +184,7 @@ func (m *mockRuntimeBrokerClient) GetAgentLogs(ctx context.Context, brokerID, br return "", nil } -func (m *mockRuntimeBrokerClient) CleanupProject(ctx context.Context, brokerID, brokerEndpoint, projectSlug string) error { +func (m *mockRuntimeBrokerClient) CleanupProject(ctx context.Context, brokerID, brokerEndpoint, projectSlug, projectID string) error { m.cleanupCalled = true m.cleanupCalls++ m.lastBrokerID = brokerID @@ -215,7 +218,7 @@ func TestHTTPAgentDispatcher_DispatchAgentCreate(t *testing.T) { // Create a runtime broker with an endpoint broker := &store.RuntimeBroker{ - ID: "host-1", + ID: tid("host-1"), Name: "test-host", Slug: "test-host", Endpoint: "http://localhost:9800", @@ -229,11 +232,11 @@ func TestHTTPAgentDispatcher_DispatchAgentCreate(t *testing.T) { dispatcher := NewHTTPAgentDispatcherWithClient(memStore, mockClient, false, slog.Default()) agent := &store.Agent{ - ID: "agent-1", + ID: tid("agent-1"), Name: "test-agent", Slug: "test-agent", - ProjectID: "project-1", - RuntimeBrokerID: "host-1", + ProjectID: tid("project-1"), + RuntimeBrokerID: tid("host-1"), AppliedConfig: &store.AgentAppliedConfig{ HarnessConfig: "claude", Task: "Fix a bug", @@ -258,8 +261,9 @@ func TestHTTPAgentDispatcher_DispatchAgentStop(t *testing.T) { memStore := createTestStore(t) broker := &store.RuntimeBroker{ - ID: "host-1", + ID: tid("host-1"), Name: "test-host", + Slug: "test-host", Endpoint: "http://localhost:9800", Status: store.BrokerStatusOnline, } @@ -271,10 +275,10 @@ func TestHTTPAgentDispatcher_DispatchAgentStop(t *testing.T) { dispatcher := NewHTTPAgentDispatcherWithClient(memStore, mockClient, false, slog.Default()) agent := &store.Agent{ - ID: "agent-1", + ID: tid("agent-1"), Name: "test-agent", Slug: "test-agent", - RuntimeBrokerID: "host-1", + RuntimeBrokerID: tid("host-1"), } err := dispatcher.DispatchAgentStop(ctx, agent) @@ -295,8 +299,9 @@ func TestHTTPAgentDispatcher_DispatchAgentDelete(t *testing.T) { memStore := createTestStore(t) broker := &store.RuntimeBroker{ - ID: "host-1", + ID: tid("host-1"), Name: "test-host", + Slug: "test-host", Endpoint: "http://localhost:9800", Status: store.BrokerStatusOnline, } @@ -308,10 +313,10 @@ func TestHTTPAgentDispatcher_DispatchAgentDelete(t *testing.T) { dispatcher := NewHTTPAgentDispatcherWithClient(memStore, mockClient, false, slog.Default()) agent := &store.Agent{ - ID: "agent-1", + ID: tid("agent-1"), Name: "test-agent", Slug: "test-agent", - RuntimeBrokerID: "host-1", + RuntimeBrokerID: tid("host-1"), } err := dispatcher.DispatchAgentDelete(ctx, agent, true, false, false, time.Time{}) @@ -335,8 +340,9 @@ func TestHTTPAgentDispatcher_DispatchAgentMessage(t *testing.T) { memStore := createTestStore(t) broker := &store.RuntimeBroker{ - ID: "host-1", + ID: tid("host-1"), Name: "test-host", + Slug: "test-host", Endpoint: "http://localhost:9800", Status: store.BrokerStatusOnline, } @@ -348,10 +354,10 @@ func TestHTTPAgentDispatcher_DispatchAgentMessage(t *testing.T) { dispatcher := NewHTTPAgentDispatcherWithClient(memStore, mockClient, false, slog.Default()) agent := &store.Agent{ - ID: "agent-1", + ID: tid("agent-1"), Name: "test-agent", Slug: "test-agent", - RuntimeBrokerID: "host-1", + RuntimeBrokerID: tid("host-1"), } err := dispatcher.DispatchAgentMessage(ctx, agent, "Hello, agent!", true, nil) @@ -407,12 +413,12 @@ func TestHTTPRuntimeBrokerClient_CreateAgent(t *testing.T) { req := &RemoteCreateAgentRequest{ ID: "hub-uuid-1", - Slug: "agent-1", + Slug: tid("agent-1"), Name: "test-agent", - ProjectID: "project-1", + ProjectID: tid("project-1"), } - resp, err := client.CreateAgent(context.Background(), "host-1", server.URL, req) + resp, err := client.CreateAgent(context.Background(), tid("host-1"), server.URL, req) if err != nil { t.Fatalf("CreateAgent failed: %v", err) } @@ -440,7 +446,7 @@ func TestHTTPRuntimeBrokerClient_StartAgent_InvalidJSONFails(t *testing.T) { defer server.Close() client := NewHTTPRuntimeBrokerClient() - _, err := client.StartAgent(context.Background(), "host-1", server.URL, "test-agent", "", "", "", "", "", nil, nil, nil, nil, false) + _, err := client.StartAgent(context.Background(), tid("host-1"), server.URL, "test-agent", "", "", "", "", "", nil, nil, nil, nil, false) if err == nil { t.Fatal("expected StartAgent to fail on invalid JSON response") } @@ -464,7 +470,7 @@ func TestHTTPRuntimeBrokerClient_StopAgent(t *testing.T) { client := NewHTTPRuntimeBrokerClient() - err := client.StopAgent(context.Background(), "host-1", server.URL, "test-agent", "") + err := client.StopAgent(context.Background(), tid("host-1"), server.URL, "test-agent", "") if err != nil { t.Fatalf("StopAgent failed: %v", err) } @@ -493,7 +499,7 @@ func TestHTTPRuntimeBrokerClient_DeleteAgent(t *testing.T) { client := NewHTTPRuntimeBrokerClient() - err := client.DeleteAgent(context.Background(), "host-1", server.URL, "test-agent", "", true, false, false, time.Time{}) + err := client.DeleteAgent(context.Background(), tid("host-1"), server.URL, "test-agent", "", true, false, false, time.Time{}) if err != nil { t.Fatalf("DeleteAgent failed: %v", err) } @@ -526,7 +532,7 @@ func TestHTTPRuntimeBrokerClient_MessageAgent(t *testing.T) { client := NewHTTPRuntimeBrokerClient() - err := client.MessageAgent(context.Background(), "host-1", server.URL, "test-agent", "", "Hello!", true, nil) + err := client.MessageAgent(context.Background(), tid("host-1"), server.URL, "test-agent", "", "Hello!", true, nil) if err != nil { t.Fatalf("MessageAgent failed: %v", err) } @@ -540,7 +546,7 @@ func TestHTTPAgentDispatcher_DispatchAgentCreate_WithProjectProviderPath(t *test // (not hub-managed). This ensures buildCreateRequest looks up the // provider's LocalPath instead of sending a projectSlug. project := &store.Project{ - ID: "project-1", + ID: tid("project-1"), Name: "test-project", Slug: "test-project", GitRemote: "https://github.com/example/repo.git", @@ -551,7 +557,7 @@ func TestHTTPAgentDispatcher_DispatchAgentCreate_WithProjectProviderPath(t *test // Create a runtime broker broker := &store.RuntimeBroker{ - ID: "broker-1", + ID: tid("broker-1"), Name: "test-broker", Slug: "test-broker", Endpoint: "http://localhost:9800", @@ -563,8 +569,8 @@ func TestHTTPAgentDispatcher_DispatchAgentCreate_WithProjectProviderPath(t *test // Add a project provider record WITH a local path provider := &store.ProjectProvider{ - ProjectID: "project-1", - BrokerID: "broker-1", + ProjectID: tid("project-1"), + BrokerID: tid("broker-1"), BrokerName: "test-broker", LocalPath: "/home/user/projects/myproject/.scion", Status: store.BrokerStatusOnline, @@ -577,11 +583,11 @@ func TestHTTPAgentDispatcher_DispatchAgentCreate_WithProjectProviderPath(t *test dispatcher := NewHTTPAgentDispatcherWithClient(memStore, mockClient, false, slog.Default()) agent := &store.Agent{ - ID: "agent-1", + ID: tid("agent-1"), Name: "test-agent", Slug: "test-agent", - ProjectID: "project-1", - RuntimeBrokerID: "broker-1", + ProjectID: tid("project-1"), + RuntimeBrokerID: tid("broker-1"), } err := dispatcher.DispatchAgentCreate(ctx, agent) @@ -597,6 +603,59 @@ func TestHTTPAgentDispatcher_DispatchAgentCreate_WithProjectProviderPath(t *test } } +func TestHTTPAgentDispatcher_DispatchAgentCreate_ThreadsWorkspaceMode(t *testing.T) { + ctx := context.Background() + memStore := createTestStore(t) + + project := &store.Project{ + ID: tid("project-wt"), + Name: "worktree-project", + Slug: "worktree-project", + GitRemote: "https://github.com/example/repo.git", + Labels: map[string]string{ + store.LabelWorkspaceMode: store.WorkspaceModeWorktreePerAgent, + }, + } + if err := memStore.CreateProject(ctx, project); err != nil { + t.Fatalf("failed to create project: %v", err) + } + + broker := &store.RuntimeBroker{ + ID: tid("broker-wt"), + Name: "test-broker", + Slug: "test-broker", + Endpoint: "http://localhost:9800", + Status: store.BrokerStatusOnline, + } + if err := memStore.CreateRuntimeBroker(ctx, broker); err != nil { + t.Fatalf("failed to create runtime broker: %v", err) + } + + mockClient := &mockRuntimeBrokerClient{} + dispatcher := NewHTTPAgentDispatcherWithClient(memStore, mockClient, false, slog.Default()) + + agent := &store.Agent{ + ID: tid("agent-wt"), + Name: "test-agent", + Slug: "test-agent", + ProjectID: tid("project-wt"), + RuntimeBrokerID: tid("broker-wt"), + } + + err := dispatcher.DispatchAgentCreate(ctx, agent) + if err != nil { + t.Fatalf("DispatchAgentCreate failed: %v", err) + } + + if !mockClient.createCalled { + t.Fatal("expected CreateAgent to be called") + } + if mockClient.lastCreateReq.WorkspaceMode != store.WorkspaceModeWorktreePerAgent { + t.Errorf("expected WorkspaceMode %q, got %q", + store.WorkspaceModeWorktreePerAgent, mockClient.lastCreateReq.WorkspaceMode) + } +} + func TestHTTPAgentDispatcher_DispatchAgentCreate_MissingBrokerEndpoint(t *testing.T) { // When a broker has no HTTP endpoint configured (e.g. control-channel-only // brokers behind NAT), the dispatcher should still pass the call through @@ -606,7 +665,7 @@ func TestHTTPAgentDispatcher_DispatchAgentCreate_MissingBrokerEndpoint(t *testin memStore := createTestStore(t) broker := &store.RuntimeBroker{ - ID: "host-1", + ID: tid("host-1"), Name: "test-host", Slug: "test-host", Status: store.BrokerStatusOnline, @@ -619,10 +678,10 @@ func TestHTTPAgentDispatcher_DispatchAgentCreate_MissingBrokerEndpoint(t *testin dispatcher := NewHTTPAgentDispatcherWithClient(memStore, mockClient, false, slog.Default()) agent := &store.Agent{ - ID: "agent-1", + ID: tid("agent-1"), Name: "test-agent", Slug: "test-agent", - RuntimeBrokerID: "host-1", + RuntimeBrokerID: tid("host-1"), } err := dispatcher.DispatchAgentCreate(ctx, agent) @@ -639,7 +698,7 @@ func TestHTTPAgentDispatcher_DispatchAgentCreate_MissingBrokerEndpoint(t *testin func TestBrokerHTTPTransport_RejectsEmptyEndpoint(t *testing.T) { transport := newBrokerHTTPTransport(false, nil) - _, err := transport.CreateAgent(context.Background(), "broker-1", "", &RemoteCreateAgentRequest{}) + _, err := transport.CreateAgent(context.Background(), tid("broker-1"), "", &RemoteCreateAgentRequest{}) if err == nil { t.Fatal("expected error when endpoint is empty") } @@ -654,7 +713,7 @@ func TestHTTPAgentDispatcher_DispatchAgentCreate_WithoutProjectProviderPath(t *t // Create the project (required by FK constraint) project := &store.Project{ - ID: "project-1", + ID: tid("project-1"), Name: "test-project", Slug: "test-project", } @@ -664,7 +723,7 @@ func TestHTTPAgentDispatcher_DispatchAgentCreate_WithoutProjectProviderPath(t *t // Create a runtime broker broker := &store.RuntimeBroker{ - ID: "broker-1", + ID: tid("broker-1"), Name: "test-broker", Slug: "test-broker", Endpoint: "http://localhost:9800", @@ -676,8 +735,8 @@ func TestHTTPAgentDispatcher_DispatchAgentCreate_WithoutProjectProviderPath(t *t // Add a project provider record WITHOUT a local path (simulating auto-provide) provider := &store.ProjectProvider{ - ProjectID: "project-1", - BrokerID: "broker-1", + ProjectID: tid("project-1"), + BrokerID: tid("broker-1"), BrokerName: "test-broker", LocalPath: "", Status: store.BrokerStatusOnline, @@ -691,11 +750,11 @@ func TestHTTPAgentDispatcher_DispatchAgentCreate_WithoutProjectProviderPath(t *t dispatcher := NewHTTPAgentDispatcherWithClient(memStore, mockClient, false, slog.Default()) agent := &store.Agent{ - ID: "agent-1", + ID: tid("agent-1"), Name: "test-agent", Slug: "test-agent", - ProjectID: "project-1", - RuntimeBrokerID: "broker-1", + ProjectID: tid("project-1"), + RuntimeBrokerID: tid("broker-1"), } err := dispatcher.DispatchAgentCreate(ctx, agent) @@ -718,7 +777,7 @@ func TestHTTPAgentDispatcher_DispatchAgentProvision(t *testing.T) { // Create a runtime broker with an endpoint broker := &store.RuntimeBroker{ - ID: "host-1", + ID: tid("host-1"), Name: "test-host", Slug: "test-host", Endpoint: "http://localhost:9800", @@ -732,11 +791,11 @@ func TestHTTPAgentDispatcher_DispatchAgentProvision(t *testing.T) { dispatcher := NewHTTPAgentDispatcherWithClient(memStore, mockClient, false, slog.Default()) agent := &store.Agent{ - ID: "agent-1", + ID: tid("agent-1"), Name: "test-agent", Slug: "test-agent", - ProjectID: "project-1", - RuntimeBrokerID: "host-1", + ProjectID: tid("project-1"), + RuntimeBrokerID: tid("host-1"), AppliedConfig: &store.AgentAppliedConfig{ HarnessConfig: "claude", }, @@ -762,7 +821,7 @@ func TestHTTPAgentDispatcher_DispatchAgentProvision(t *testing.T) { } // Verify broker ID was passed - if mockClient.lastBrokerID != "host-1" { + if mockClient.lastBrokerID != tid("host-1") { t.Errorf("expected brokerID 'host-1', got '%s'", mockClient.lastBrokerID) } } @@ -775,7 +834,7 @@ func TestHTTPAgentDispatcher_DispatchAgentProvision_NoBroker(t *testing.T) { dispatcher := NewHTTPAgentDispatcherWithClient(memStore, mockClient, false, slog.Default()) agent := &store.Agent{ - ID: "agent-1", + ID: tid("agent-1"), Name: "test-agent", Slug: "test-agent", RuntimeBrokerID: "", // No broker assigned @@ -796,7 +855,7 @@ func TestHTTPAgentDispatcher_DispatchAgentProvision_PassesTaskThrough(t *testing memStore := createTestStore(t) broker := &store.RuntimeBroker{ - ID: "host-1", + ID: tid("host-1"), Name: "test-host", Slug: "test-host", Endpoint: "http://localhost:9800", @@ -810,11 +869,11 @@ func TestHTTPAgentDispatcher_DispatchAgentProvision_PassesTaskThrough(t *testing dispatcher := NewHTTPAgentDispatcherWithClient(memStore, mockClient, false, slog.Default()) agent := &store.Agent{ - ID: "agent-1", + ID: tid("agent-1"), Name: "test-agent", Slug: "test-agent", - ProjectID: "project-1", - RuntimeBrokerID: "host-1", + ProjectID: tid("project-1"), + RuntimeBrokerID: tid("host-1"), AppliedConfig: &store.AgentAppliedConfig{ Task: "implement feature X", }, @@ -845,7 +904,7 @@ func TestHTTPAgentDispatcher_DispatchAgentCreate_WithWorkspace(t *testing.T) { // Create a runtime broker broker := &store.RuntimeBroker{ - ID: "host-1", + ID: tid("host-1"), Name: "test-host", Slug: "test-host", Endpoint: "http://localhost:9800", @@ -859,11 +918,11 @@ func TestHTTPAgentDispatcher_DispatchAgentCreate_WithWorkspace(t *testing.T) { dispatcher := NewHTTPAgentDispatcherWithClient(memStore, mockClient, false, slog.Default()) agent := &store.Agent{ - ID: "agent-1", + ID: tid("agent-1"), Name: "test-agent", Slug: "test-agent", - ProjectID: "project-1", - RuntimeBrokerID: "host-1", + ProjectID: tid("project-1"), + RuntimeBrokerID: tid("host-1"), AppliedConfig: &store.AgentAppliedConfig{ HarnessConfig: "claude", Task: "do something", @@ -893,7 +952,7 @@ func TestHTTPAgentDispatcher_DispatchAgentCreate_WithCreatorName(t *testing.T) { // Create a runtime broker broker := &store.RuntimeBroker{ - ID: "host-1", + ID: tid("host-1"), Name: "test-host", Slug: "test-host", Endpoint: "http://localhost:9800", @@ -907,11 +966,11 @@ func TestHTTPAgentDispatcher_DispatchAgentCreate_WithCreatorName(t *testing.T) { dispatcher := NewHTTPAgentDispatcherWithClient(memStore, mockClient, false, slog.Default()) agent := &store.Agent{ - ID: "agent-1", + ID: tid("agent-1"), Name: "test-agent", Slug: "test-agent", - ProjectID: "project-1", - RuntimeBrokerID: "host-1", + ProjectID: tid("project-1"), + RuntimeBrokerID: tid("host-1"), AppliedConfig: &store.AgentAppliedConfig{ HarnessConfig: "claude", Task: "do something", @@ -939,7 +998,7 @@ func TestHTTPAgentDispatcher_DispatchAgentCreate_WithoutCreatorName(t *testing.T memStore := createTestStore(t) broker := &store.RuntimeBroker{ - ID: "host-1", + ID: tid("host-1"), Name: "test-host", Slug: "test-host", Endpoint: "http://localhost:9800", @@ -953,11 +1012,11 @@ func TestHTTPAgentDispatcher_DispatchAgentCreate_WithoutCreatorName(t *testing.T dispatcher := NewHTTPAgentDispatcherWithClient(memStore, mockClient, false, slog.Default()) agent := &store.Agent{ - ID: "agent-1", + ID: tid("agent-1"), Name: "test-agent", Slug: "test-agent", - ProjectID: "project-1", - RuntimeBrokerID: "host-1", + ProjectID: tid("project-1"), + RuntimeBrokerID: tid("host-1"), AppliedConfig: &store.AgentAppliedConfig{ HarnessConfig: "claude", }, @@ -980,7 +1039,7 @@ func TestHTTPAgentDispatcher_DispatchAgentCreate_DoesNotSetProvisionOnly(t *test // Create a runtime broker broker := &store.RuntimeBroker{ - ID: "host-1", + ID: tid("host-1"), Name: "test-host", Slug: "test-host", Endpoint: "http://localhost:9800", @@ -994,11 +1053,11 @@ func TestHTTPAgentDispatcher_DispatchAgentCreate_DoesNotSetProvisionOnly(t *test dispatcher := NewHTTPAgentDispatcherWithClient(memStore, mockClient, false, slog.Default()) agent := &store.Agent{ - ID: "agent-1", + ID: tid("agent-1"), Name: "test-agent", Slug: "test-agent", - ProjectID: "project-1", - RuntimeBrokerID: "host-1", + ProjectID: tid("project-1"), + RuntimeBrokerID: tid("host-1"), AppliedConfig: &store.AgentAppliedConfig{ Task: "do something", }, @@ -1021,7 +1080,7 @@ func TestHTTPAgentDispatcher_DispatchAgentStart_WithProjectProviderPath(t *testi // Create the project with a GitRemote so it is treated as a linked project project := &store.Project{ - ID: "project-1", + ID: tid("project-1"), Name: "test-project", Slug: "test-project", GitRemote: "https://github.com/example/repo.git", @@ -1032,7 +1091,7 @@ func TestHTTPAgentDispatcher_DispatchAgentStart_WithProjectProviderPath(t *testi // Create a runtime broker broker := &store.RuntimeBroker{ - ID: "broker-1", + ID: tid("broker-1"), Name: "test-broker", Slug: "test-broker", Endpoint: "http://localhost:9800", @@ -1044,8 +1103,8 @@ func TestHTTPAgentDispatcher_DispatchAgentStart_WithProjectProviderPath(t *testi // Add a project provider record with a local path provider := &store.ProjectProvider{ - ProjectID: "project-1", - BrokerID: "broker-1", + ProjectID: tid("project-1"), + BrokerID: tid("broker-1"), BrokerName: "test-broker", LocalPath: "/home/user/projects/myproject/.scion", Status: store.BrokerStatusOnline, @@ -1058,11 +1117,11 @@ func TestHTTPAgentDispatcher_DispatchAgentStart_WithProjectProviderPath(t *testi dispatcher := NewHTTPAgentDispatcherWithClient(memStore, mockClient, false, slog.Default()) agent := &store.Agent{ - ID: "agent-1", + ID: tid("agent-1"), Name: "test-agent", Slug: "test-agent", - ProjectID: "project-1", - RuntimeBrokerID: "broker-1", + ProjectID: tid("project-1"), + RuntimeBrokerID: tid("broker-1"), } err := dispatcher.DispatchAgentStart(ctx, agent, "do task") @@ -1098,7 +1157,7 @@ func TestHTTPAgentDispatcher_DispatchAgentStart_IncludesAgentIdentity(t *testing memStore := createTestStore(t) project := &store.Project{ - ID: "project-1", + ID: tid("project-1"), Name: "test-project", Slug: "test-project", GitRemote: "https://github.com/example/repo.git", @@ -1108,7 +1167,7 @@ func TestHTTPAgentDispatcher_DispatchAgentStart_IncludesAgentIdentity(t *testing } broker := &store.RuntimeBroker{ - ID: "broker-1", + ID: tid("broker-1"), Name: "test-broker", Slug: "test-broker", Endpoint: "http://localhost:9800", @@ -1119,8 +1178,8 @@ func TestHTTPAgentDispatcher_DispatchAgentStart_IncludesAgentIdentity(t *testing } provider := &store.ProjectProvider{ - ProjectID: "project-1", - BrokerID: "broker-1", + ProjectID: tid("project-1"), + BrokerID: tid("broker-1"), BrokerName: "test-broker", LocalPath: "/home/user/projects/myproject/.scion", Status: store.BrokerStatusOnline, @@ -1136,8 +1195,8 @@ func TestHTTPAgentDispatcher_DispatchAgentStart_IncludesAgentIdentity(t *testing ID: "agent-uuid-123", Name: "test-agent", Slug: "test-agent-slug", - ProjectID: "project-1", - RuntimeBrokerID: "broker-1", + ProjectID: tid("project-1"), + RuntimeBrokerID: tid("broker-1"), } err := dispatcher.DispatchAgentStart(ctx, agent, "") @@ -1166,7 +1225,7 @@ func TestHTTPAgentDispatcher_DispatchAgentStart_IncludesAgentIdentity(t *testing // Verify SCION_GROVE_ID is included in resolvedEnv if v, ok := mockClient.lastResolvedEnv["SCION_GROVE_ID"]; !ok { t.Error("expected SCION_GROVE_ID in resolvedEnv, but not found") - } else if v != "project-1" { + } else if v != tid("project-1") { t.Errorf("expected SCION_GROVE_ID='project-1', got %q", v) } } @@ -1177,7 +1236,7 @@ func TestHTTPAgentDispatcher_DispatchAgentStart_HubManagedProject(t *testing.T) // Create a hub-managed project (no git remote) project := &store.Project{ - ID: "project-hub", + ID: tid("project-hub"), Name: "My Hub Project", Slug: "my-hub-project", // No GitRemote — this is a hub-managed project @@ -1188,7 +1247,7 @@ func TestHTTPAgentDispatcher_DispatchAgentStart_HubManagedProject(t *testing.T) // Create a runtime broker with no local provider path for this project broker := &store.RuntimeBroker{ - ID: "broker-1", + ID: tid("broker-1"), Name: "test-broker", Slug: "test-broker", Endpoint: "http://localhost:9800", @@ -1205,8 +1264,8 @@ func TestHTTPAgentDispatcher_DispatchAgentStart_HubManagedProject(t *testing.T) ID: "agent-hub-1", Name: "hub-agent", Slug: "hub-agent", - ProjectID: "project-hub", - RuntimeBrokerID: "broker-1", + ProjectID: tid("project-hub"), + RuntimeBrokerID: tid("broker-1"), } err := dispatcher.DispatchAgentStart(ctx, agent, "") @@ -1235,7 +1294,7 @@ func TestHTTPAgentDispatcher_DispatchAgentStart_ProjectSlugSetForGitRemoteWithou // The broker needs the projectSlug to resolve agent directories under // ~/.scion/projects// instead of falling back to the global project. project := &store.Project{ - ID: "project-git", + ID: tid("project-git"), Name: "Git Project", Slug: "git-project", GitRemote: "https://github.com/user/repo.git", @@ -1245,7 +1304,7 @@ func TestHTTPAgentDispatcher_DispatchAgentStart_ProjectSlugSetForGitRemoteWithou } broker := &store.RuntimeBroker{ - ID: "broker-1", + ID: tid("broker-1"), Name: "test-broker", Slug: "test-broker", Endpoint: "http://localhost:9800", @@ -1262,8 +1321,8 @@ func TestHTTPAgentDispatcher_DispatchAgentStart_ProjectSlugSetForGitRemoteWithou ID: "agent-git-1", Name: "git-agent", Slug: "git-agent", - ProjectID: "project-git", - RuntimeBrokerID: "broker-1", + ProjectID: tid("project-git"), + RuntimeBrokerID: tid("broker-1"), } err := dispatcher.DispatchAgentStart(ctx, agent, "") @@ -1287,7 +1346,7 @@ func TestHTTPAgentDispatcher_DispatchAgentStart_ResolvesEnvFromStorage(t *testin // Create a project project := &store.Project{ - ID: "project-env", + ID: tid("project-env"), Name: "env-test-project", Slug: "env-test-project", } @@ -1297,7 +1356,7 @@ func TestHTTPAgentDispatcher_DispatchAgentStart_ResolvesEnvFromStorage(t *testin // Create a runtime broker broker := &store.RuntimeBroker{ - ID: "broker-env", + ID: tid("broker-env"), Name: "test-broker", Slug: "test-broker", Endpoint: "http://localhost:9800", @@ -1309,8 +1368,8 @@ func TestHTTPAgentDispatcher_DispatchAgentStart_ResolvesEnvFromStorage(t *testin // Add a project provider with a local path provider := &store.ProjectProvider{ - ProjectID: "project-env", - BrokerID: "broker-env", + ProjectID: tid("project-env"), + BrokerID: tid("broker-env"), BrokerName: "test-broker", LocalPath: "/home/user/project/.scion", Status: store.BrokerStatusOnline, @@ -1321,18 +1380,18 @@ func TestHTTPAgentDispatcher_DispatchAgentStart_ResolvesEnvFromStorage(t *testin // Store an env var in project scope (simulating API key stored in hub) if err := memStore.CreateEnvVar(ctx, &store.EnvVar{ - ID: "ev-project-1", + ID: tid("ev-project-1"), Key: "GEMINI_API_KEY", Value: "test-api-key-123", Scope: "project", - ScopeID: "project-env", + ScopeID: tid("project-env"), }); err != nil { t.Fatalf("failed to set env var: %v", err) } // Store a user-scoped env var if err := memStore.CreateEnvVar(ctx, &store.EnvVar{ - ID: "ev-user-1", + ID: tid("ev-user-1"), Key: "CUSTOM_VAR", Value: "user-value", Scope: "user", @@ -1348,9 +1407,9 @@ func TestHTTPAgentDispatcher_DispatchAgentStart_ResolvesEnvFromStorage(t *testin ID: "agent-env", Name: "test-agent", Slug: "test-agent", - ProjectID: "project-env", + ProjectID: tid("project-env"), OwnerID: "owner-1", - RuntimeBrokerID: "broker-env", + RuntimeBrokerID: tid("broker-env"), AppliedConfig: &store.AgentAppliedConfig{ HarnessConfig: "gemini", Env: map[string]string{"EXISTING_VAR": "from-config"}, @@ -1393,7 +1452,7 @@ func TestHTTPAgentDispatcher_DispatchAgentStart_ConfigEnvTakesPrecedence(t *test // Create project and broker project := &store.Project{ - ID: "project-prec", + ID: tid("project-prec"), Name: "precedence-test", Slug: "precedence-test", } @@ -1402,7 +1461,7 @@ func TestHTTPAgentDispatcher_DispatchAgentStart_ConfigEnvTakesPrecedence(t *test } broker := &store.RuntimeBroker{ - ID: "broker-prec", + ID: tid("broker-prec"), Name: "test-broker", Slug: "test-broker", Endpoint: "http://localhost:9800", @@ -1414,11 +1473,11 @@ func TestHTTPAgentDispatcher_DispatchAgentStart_ConfigEnvTakesPrecedence(t *test // Store an env var that conflicts with config env if err := memStore.CreateEnvVar(ctx, &store.EnvVar{ - ID: "ev-prec-1", + ID: tid("ev-prec-1"), Key: "API_KEY", Value: "storage-value", Scope: "project", - ScopeID: "project-prec", + ScopeID: tid("project-prec"), }); err != nil { t.Fatalf("failed to set env var: %v", err) } @@ -1430,8 +1489,8 @@ func TestHTTPAgentDispatcher_DispatchAgentStart_ConfigEnvTakesPrecedence(t *test ID: "agent-prec", Name: "test-agent", Slug: "test-agent", - ProjectID: "project-prec", - RuntimeBrokerID: "broker-prec", + ProjectID: tid("project-prec"), + RuntimeBrokerID: tid("broker-prec"), AppliedConfig: &store.AgentAppliedConfig{ HarnessConfig: "gemini", Env: map[string]string{"API_KEY": "config-value"}, @@ -1456,7 +1515,7 @@ func TestHTTPAgentDispatcher_DispatchAgentStart_StorageOverridesEmptyConfigEnv(t memStore := createTestStore(t) project := &store.Project{ - ID: "project-empty-env", + ID: tid("project-empty-env"), Name: "empty-env-test", Slug: "empty-env-test", } @@ -1465,7 +1524,7 @@ func TestHTTPAgentDispatcher_DispatchAgentStart_StorageOverridesEmptyConfigEnv(t } broker := &store.RuntimeBroker{ - ID: "broker-empty-env", + ID: tid("broker-empty-env"), Name: "test-broker", Slug: "test-broker", Endpoint: "http://localhost:9800", @@ -1477,11 +1536,11 @@ func TestHTTPAgentDispatcher_DispatchAgentStart_StorageOverridesEmptyConfigEnv(t // Store an env var that should override the empty config value if err := memStore.CreateEnvVar(ctx, &store.EnvVar{ - ID: "ev-empty-1", + ID: tid("ev-empty-1"), Key: "GEMINI_API_KEY", Value: "stored-api-key", Scope: "project", - ScopeID: "project-empty-env", + ScopeID: tid("project-empty-env"), }); err != nil { t.Fatalf("failed to set env var: %v", err) } @@ -1493,8 +1552,8 @@ func TestHTTPAgentDispatcher_DispatchAgentStart_StorageOverridesEmptyConfigEnv(t ID: "agent-empty-env", Name: "test-agent", Slug: "test-agent", - ProjectID: "project-empty-env", - RuntimeBrokerID: "broker-empty-env", + ProjectID: tid("project-empty-env"), + RuntimeBrokerID: tid("broker-empty-env"), AppliedConfig: &store.AgentAppliedConfig{ HarnessConfig: "gemini", // Empty value = passthrough marker; storage should fill it in @@ -1526,7 +1585,7 @@ func TestHTTPAgentDispatcher_DispatchAgentCreate_InjectsDevToken(t *testing.T) { memStore := createTestStore(t) broker := &store.RuntimeBroker{ - ID: "host-1", + ID: tid("host-1"), Name: "test-host", Slug: "test-host", Endpoint: "http://localhost:9800", @@ -1541,11 +1600,11 @@ func TestHTTPAgentDispatcher_DispatchAgentCreate_InjectsDevToken(t *testing.T) { dispatcher.SetDevAuthToken("my-dev-token") agent := &store.Agent{ - ID: "agent-1", + ID: tid("agent-1"), Name: "test-agent", Slug: "test-agent", - ProjectID: "project-1", - RuntimeBrokerID: "host-1", + ProjectID: tid("project-1"), + RuntimeBrokerID: tid("host-1"), AppliedConfig: &store.AgentAppliedConfig{ HarnessConfig: "claude", }, @@ -1575,7 +1634,7 @@ func TestHTTPAgentDispatcher_DispatchAgentCreate_NoDevToken(t *testing.T) { memStore := createTestStore(t) broker := &store.RuntimeBroker{ - ID: "host-1", + ID: tid("host-1"), Name: "test-host", Slug: "test-host", Endpoint: "http://localhost:9800", @@ -1590,11 +1649,11 @@ func TestHTTPAgentDispatcher_DispatchAgentCreate_NoDevToken(t *testing.T) { // Do NOT set dev auth token agent := &store.Agent{ - ID: "agent-1", + ID: tid("agent-1"), Name: "test-agent", Slug: "test-agent", - ProjectID: "project-1", - RuntimeBrokerID: "host-1", + ProjectID: tid("project-1"), + RuntimeBrokerID: tid("host-1"), } err := dispatcher.DispatchAgentCreate(ctx, agent) @@ -1615,7 +1674,7 @@ func TestHTTPAgentDispatcher_DispatchAgentCreate_DevTokenMergesWithExistingEnv(t memStore := createTestStore(t) broker := &store.RuntimeBroker{ - ID: "host-1", + ID: tid("host-1"), Name: "test-host", Slug: "test-host", Endpoint: "http://localhost:9800", @@ -1630,11 +1689,11 @@ func TestHTTPAgentDispatcher_DispatchAgentCreate_DevTokenMergesWithExistingEnv(t dispatcher.SetDevAuthToken("my-dev-token") agent := &store.Agent{ - ID: "agent-1", + ID: tid("agent-1"), Name: "test-agent", Slug: "test-agent", - ProjectID: "project-1", - RuntimeBrokerID: "host-1", + ProjectID: tid("project-1"), + RuntimeBrokerID: tid("host-1"), AppliedConfig: &store.AgentAppliedConfig{ HarnessConfig: "claude", Env: map[string]string{ @@ -1664,7 +1723,7 @@ func TestHTTPAgentDispatcher_DispatchAgentStart_AppliesBrokerResponse(t *testing memStore := createTestStore(t) broker := &store.RuntimeBroker{ - ID: "broker-1", + ID: tid("broker-1"), Name: "test-broker", Slug: "test-broker", Endpoint: "http://localhost:9800", @@ -1689,11 +1748,11 @@ func TestHTTPAgentDispatcher_DispatchAgentStart_AppliesBrokerResponse(t *testing dispatcher := NewHTTPAgentDispatcherWithClient(memStore, mockClient, false, slog.Default()) agent := &store.Agent{ - ID: "agent-1", + ID: tid("agent-1"), Name: "test-agent", Slug: "test-agent", - ProjectID: "project-1", - RuntimeBrokerID: "broker-1", + ProjectID: tid("project-1"), + RuntimeBrokerID: tid("broker-1"), Phase: string(state.PhaseCreated), } @@ -1725,7 +1784,7 @@ func TestHTTPAgentDispatcher_DispatchAgentCreate_PropagatesGitClone(t *testing.T memStore := createTestStore(t) broker := &store.RuntimeBroker{ - ID: "host-1", + ID: tid("host-1"), Name: "test-host", Slug: "test-host", Endpoint: "http://localhost:9800", @@ -1742,8 +1801,8 @@ func TestHTTPAgentDispatcher_DispatchAgentCreate_PropagatesGitClone(t *testing.T ID: "agent-gc-1", Name: "git-clone-agent", Slug: "git-clone-agent", - ProjectID: "project-1", - RuntimeBrokerID: "host-1", + ProjectID: tid("project-1"), + RuntimeBrokerID: tid("host-1"), AppliedConfig: &store.AgentAppliedConfig{ HarnessConfig: "claude", Task: "implement feature", @@ -1788,7 +1847,7 @@ func TestHTTPAgentDispatcher_DispatchAgentCreate_PropagatesProfile(t *testing.T) memStore := createTestStore(t) broker := &store.RuntimeBroker{ - ID: "host-1", + ID: tid("host-1"), Name: "test-host", Slug: "test-host", Endpoint: "http://localhost:9800", @@ -1805,8 +1864,8 @@ func TestHTTPAgentDispatcher_DispatchAgentCreate_PropagatesProfile(t *testing.T) ID: "agent-profile-1", Name: "profile-agent", Slug: "profile-agent", - ProjectID: "project-1", - RuntimeBrokerID: "host-1", + ProjectID: tid("project-1"), + RuntimeBrokerID: tid("host-1"), AppliedConfig: &store.AgentAppliedConfig{ HarnessConfig: "claude", Task: "do something", @@ -1836,7 +1895,7 @@ func TestHTTPAgentDispatcher_DispatchAgentCreate_PropagatesProjectSlug_HubManage // Create a hub-managed project (no GitRemote) project := &store.Project{ - ID: "project-hub-managed", + ID: tid("project-hub-managed"), Name: "Hub Managed Project", Slug: "hub-managed-project", // No GitRemote = hub-managed @@ -1846,7 +1905,7 @@ func TestHTTPAgentDispatcher_DispatchAgentCreate_PropagatesProjectSlug_HubManage } broker := &store.RuntimeBroker{ - ID: "host-1", + ID: tid("host-1"), Name: "test-host", Slug: "test-host", Endpoint: "http://localhost:9800", @@ -1860,11 +1919,11 @@ func TestHTTPAgentDispatcher_DispatchAgentCreate_PropagatesProjectSlug_HubManage dispatcher := NewHTTPAgentDispatcherWithClient(memStore, mockClient, false, slog.Default()) agent := &store.Agent{ - ID: "agent-1", + ID: tid("agent-1"), Name: "test-agent", Slug: "test-agent", - ProjectID: "project-hub-managed", - RuntimeBrokerID: "host-1", + ProjectID: tid("project-hub-managed"), + RuntimeBrokerID: tid("host-1"), AppliedConfig: &store.AgentAppliedConfig{ HarnessConfig: "claude", }, @@ -1889,7 +1948,7 @@ func TestHTTPAgentDispatcher_DispatchAgentCreate_ProjectSlugSet_GitProject(t *te // Create a git-backed project (has GitRemote) without a local provider path. project := &store.Project{ - ID: "project-git", + ID: tid("project-git"), Name: "Git Project", Slug: "git-project", GitRemote: "github.com/test/repo", @@ -1899,7 +1958,7 @@ func TestHTTPAgentDispatcher_DispatchAgentCreate_ProjectSlugSet_GitProject(t *te } broker := &store.RuntimeBroker{ - ID: "host-1", + ID: tid("host-1"), Name: "test-host", Slug: "test-host", Endpoint: "http://localhost:9800", @@ -1913,11 +1972,11 @@ func TestHTTPAgentDispatcher_DispatchAgentCreate_ProjectSlugSet_GitProject(t *te dispatcher := NewHTTPAgentDispatcherWithClient(memStore, mockClient, false, slog.Default()) agent := &store.Agent{ - ID: "agent-1", + ID: tid("agent-1"), Name: "test-agent", Slug: "test-agent", - ProjectID: "project-git", - RuntimeBrokerID: "host-1", + ProjectID: tid("project-git"), + RuntimeBrokerID: tid("host-1"), AppliedConfig: &store.AgentAppliedConfig{ HarnessConfig: "claude", }, @@ -1943,7 +2002,7 @@ func TestHTTPAgentDispatcher_DispatchAgentCreate_EmptyProfile(t *testing.T) { memStore := createTestStore(t) broker := &store.RuntimeBroker{ - ID: "host-1", + ID: tid("host-1"), Name: "test-host", Slug: "test-host", Endpoint: "http://localhost:9800", @@ -1960,8 +2019,8 @@ func TestHTTPAgentDispatcher_DispatchAgentCreate_EmptyProfile(t *testing.T) { ID: "agent-no-profile-1", Name: "no-profile-agent", Slug: "no-profile-agent", - ProjectID: "project-1", - RuntimeBrokerID: "host-1", + ProjectID: tid("project-1"), + RuntimeBrokerID: tid("host-1"), AppliedConfig: &store.AgentAppliedConfig{ HarnessConfig: "claude", Task: "do something", @@ -1993,7 +2052,7 @@ func TestHTTPAgentDispatcher_DispatchAgentCreate_NoProjectSlug_LocalPathProject( // Even though the broker has the repo locally, all hub-linked projects with a // git remote use clone-based provisioning (HTTPS + GitHub token). project := &store.Project{ - ID: "project-local", + ID: tid("project-local"), Name: "Local Project", Slug: "local-project", GitRemote: "https://github.com/example/local-project.git", @@ -2003,7 +2062,7 @@ func TestHTTPAgentDispatcher_DispatchAgentCreate_NoProjectSlug_LocalPathProject( } broker := &store.RuntimeBroker{ - ID: "broker-1", + ID: tid("broker-1"), Name: "test-broker", Slug: "test-broker", Endpoint: "http://localhost:9800", @@ -2015,8 +2074,8 @@ func TestHTTPAgentDispatcher_DispatchAgentCreate_NoProjectSlug_LocalPathProject( // Add a project provider record WITH a local path provider := &store.ProjectProvider{ - ProjectID: "project-local", - BrokerID: "broker-1", + ProjectID: tid("project-local"), + BrokerID: tid("broker-1"), BrokerName: "test-broker", LocalPath: "/home/user/projects/myproject/.scion", Status: store.BrokerStatusOnline, @@ -2029,11 +2088,11 @@ func TestHTTPAgentDispatcher_DispatchAgentCreate_NoProjectSlug_LocalPathProject( dispatcher := NewHTTPAgentDispatcherWithClient(memStore, mockClient, false, slog.Default()) agent := &store.Agent{ - ID: "agent-1", + ID: tid("agent-1"), Name: "test-agent", Slug: "test-agent", - ProjectID: "project-local", - RuntimeBrokerID: "broker-1", + ProjectID: tid("project-local"), + RuntimeBrokerID: tid("broker-1"), AppliedConfig: &store.AgentAppliedConfig{ HarnessConfig: "claude", Workspace: "/should/be/cleared", @@ -2099,7 +2158,7 @@ func TestHTTPAgentDispatcher_DispatchAgentCreate_LinkedProjectNoGitRemote(t *tes // Create a linked project WITHOUT a GitRemote — this is what happens when // a user links a local project via `scion hub projects link`. project := &store.Project{ - ID: "project-linked-no-git", + ID: tid("project-linked-no-git"), Name: "Linked No Git Project", Slug: "linked-no-git", // No GitRemote — looks like hub-managed, but has a provider path @@ -2109,7 +2168,7 @@ func TestHTTPAgentDispatcher_DispatchAgentCreate_LinkedProjectNoGitRemote(t *tes } broker := &store.RuntimeBroker{ - ID: "broker-1", + ID: tid("broker-1"), Name: "test-broker", Slug: "test-broker", Endpoint: "http://localhost:9800", @@ -2121,8 +2180,8 @@ func TestHTTPAgentDispatcher_DispatchAgentCreate_LinkedProjectNoGitRemote(t *tes // Add a project provider record WITH a local path provider := &store.ProjectProvider{ - ProjectID: "project-linked-no-git", - BrokerID: "broker-1", + ProjectID: tid("project-linked-no-git"), + BrokerID: tid("broker-1"), BrokerName: "test-broker", LocalPath: "/Users/user/dev/projects/my-project/.scion", Status: store.BrokerStatusOnline, @@ -2135,11 +2194,11 @@ func TestHTTPAgentDispatcher_DispatchAgentCreate_LinkedProjectNoGitRemote(t *tes dispatcher := NewHTTPAgentDispatcherWithClient(memStore, mockClient, false, slog.Default()) agent := &store.Agent{ - ID: "agent-1", + ID: tid("agent-1"), Name: "test-agent", Slug: "test-agent", - ProjectID: "project-linked-no-git", - RuntimeBrokerID: "broker-1", + ProjectID: tid("project-linked-no-git"), + RuntimeBrokerID: tid("broker-1"), AppliedConfig: &store.AgentAppliedConfig{ HarnessConfig: "claude", Workspace: "/should/be/cleared", @@ -2180,7 +2239,7 @@ func TestBuildCreateRequest_ResolvesStorageEnvVars(t *testing.T) { // Create a runtime broker broker := &store.RuntimeBroker{ - ID: "host-1", + ID: tid("host-1"), Name: "test-host", Slug: "test-host", Endpoint: "http://localhost:9800", @@ -2192,11 +2251,11 @@ func TestBuildCreateRequest_ResolvesStorageEnvVars(t *testing.T) { // Store a user-scoped env var envVar := &store.EnvVar{ - ID: "ev-1", + ID: tid("ev-1"), Key: "GEMINI_API_KEY", Value: "stored-key-value", Scope: "user", - ScopeID: "user-1", + ScopeID: tid("user-1"), } if err := memStore.CreateEnvVar(ctx, envVar); err != nil { t.Fatalf("failed to create env var: %v", err) @@ -2206,11 +2265,11 @@ func TestBuildCreateRequest_ResolvesStorageEnvVars(t *testing.T) { dispatcher := NewHTTPAgentDispatcherWithClient(memStore, mockClient, false, slog.Default()) agent := &store.Agent{ - ID: "agent-1", + ID: tid("agent-1"), Name: "test-agent", Slug: "test-agent", - OwnerID: "user-1", - RuntimeBrokerID: "host-1", + OwnerID: tid("user-1"), + RuntimeBrokerID: tid("host-1"), AppliedConfig: &store.AgentAppliedConfig{}, } @@ -2233,7 +2292,7 @@ func TestBuildCreateRequest_ConfigEnvOverridesStorage(t *testing.T) { // Create a runtime broker broker := &store.RuntimeBroker{ - ID: "host-1", + ID: tid("host-1"), Name: "test-host", Slug: "test-host", Endpoint: "http://localhost:9800", @@ -2245,11 +2304,11 @@ func TestBuildCreateRequest_ConfigEnvOverridesStorage(t *testing.T) { // Store a user-scoped env var with the same key as config env envVar := &store.EnvVar{ - ID: "ev-1", + ID: tid("ev-1"), Key: "MY_KEY", Value: "storage-value", Scope: "user", - ScopeID: "user-1", + ScopeID: tid("user-1"), } if err := memStore.CreateEnvVar(ctx, envVar); err != nil { t.Fatalf("failed to create env var: %v", err) @@ -2259,11 +2318,11 @@ func TestBuildCreateRequest_ConfigEnvOverridesStorage(t *testing.T) { dispatcher := NewHTTPAgentDispatcherWithClient(memStore, mockClient, false, slog.Default()) agent := &store.Agent{ - ID: "agent-1", + ID: tid("agent-1"), Name: "test-agent", Slug: "test-agent", - OwnerID: "user-1", - RuntimeBrokerID: "host-1", + OwnerID: tid("user-1"), + RuntimeBrokerID: tid("host-1"), AppliedConfig: &store.AgentAppliedConfig{ Env: map[string]string{ "MY_KEY": "config-value", @@ -2288,7 +2347,7 @@ func TestBuildCreateRequest_ResolvesProjectAndUserScopes(t *testing.T) { // Create project and broker project := &store.Project{ - ID: "project-1", + ID: tid("project-1"), Name: "test-project", Slug: "test-project", } @@ -2297,7 +2356,7 @@ func TestBuildCreateRequest_ResolvesProjectAndUserScopes(t *testing.T) { } broker := &store.RuntimeBroker{ - ID: "host-1", + ID: tid("host-1"), Name: "test-host", Slug: "test-host", Endpoint: "http://localhost:9800", @@ -2309,11 +2368,11 @@ func TestBuildCreateRequest_ResolvesProjectAndUserScopes(t *testing.T) { // Store a project-scoped env var projectEnv := &store.EnvVar{ - ID: "ev-project", + ID: tid("ev-project"), Key: "SHARED_KEY", Value: "project-value", Scope: "project", - ScopeID: "project-1", + ScopeID: tid("project-1"), } if err := memStore.CreateEnvVar(ctx, projectEnv); err != nil { t.Fatalf("failed to create project env var: %v", err) @@ -2321,11 +2380,11 @@ func TestBuildCreateRequest_ResolvesProjectAndUserScopes(t *testing.T) { // Store a user-scoped env var with the same key (higher precedence) userEnv := &store.EnvVar{ - ID: "ev-user", + ID: tid("ev-user"), Key: "SHARED_KEY", Value: "user-value", Scope: "user", - ScopeID: "user-1", + ScopeID: tid("user-1"), } if err := memStore.CreateEnvVar(ctx, userEnv); err != nil { t.Fatalf("failed to create user env var: %v", err) @@ -2333,11 +2392,11 @@ func TestBuildCreateRequest_ResolvesProjectAndUserScopes(t *testing.T) { // Store a project-only env var projectOnly := &store.EnvVar{ - ID: "ev-project-only", + ID: tid("ev-project-only"), Key: "GROVE_ONLY_KEY", Value: "project-only-value", Scope: "project", - ScopeID: "project-1", + ScopeID: tid("project-1"), } if err := memStore.CreateEnvVar(ctx, projectOnly); err != nil { t.Fatalf("failed to create project-only env var: %v", err) @@ -2347,12 +2406,12 @@ func TestBuildCreateRequest_ResolvesProjectAndUserScopes(t *testing.T) { dispatcher := NewHTTPAgentDispatcherWithClient(memStore, mockClient, false, slog.Default()) agent := &store.Agent{ - ID: "agent-1", + ID: tid("agent-1"), Name: "test-agent", Slug: "test-agent", - OwnerID: "user-1", - ProjectID: "project-1", - RuntimeBrokerID: "host-1", + OwnerID: tid("user-1"), + ProjectID: tid("project-1"), + RuntimeBrokerID: tid("host-1"), AppliedConfig: &store.AgentAppliedConfig{}, } @@ -2378,7 +2437,7 @@ func TestDispatchAgentCreate_IncludesStorageEnvVars(t *testing.T) { // Create a runtime broker broker := &store.RuntimeBroker{ - ID: "host-1", + ID: tid("host-1"), Name: "test-host", Slug: "test-host", Endpoint: "http://localhost:9800", @@ -2390,11 +2449,11 @@ func TestDispatchAgentCreate_IncludesStorageEnvVars(t *testing.T) { // Store user-scoped env vars envVar := &store.EnvVar{ - ID: "ev-1", + ID: tid("ev-1"), Key: "API_TOKEN", Value: "secret-token-123", Scope: "user", - ScopeID: "user-1", + ScopeID: tid("user-1"), } if err := memStore.CreateEnvVar(ctx, envVar); err != nil { t.Fatalf("failed to create env var: %v", err) @@ -2404,11 +2463,11 @@ func TestDispatchAgentCreate_IncludesStorageEnvVars(t *testing.T) { dispatcher := NewHTTPAgentDispatcherWithClient(memStore, mockClient, false, slog.Default()) agent := &store.Agent{ - ID: "agent-1", + ID: tid("agent-1"), Name: "test-agent", Slug: "test-agent", - OwnerID: "user-1", - RuntimeBrokerID: "host-1", + OwnerID: tid("user-1"), + RuntimeBrokerID: tid("host-1"), AppliedConfig: &store.AgentAppliedConfig{ HarnessConfig: "claude", }, @@ -2438,7 +2497,7 @@ func TestBuildCreateRequest_PropagatesHarnessName(t *testing.T) { memStore := createTestStore(t) broker := &store.RuntimeBroker{ - ID: "host-1", + ID: tid("host-1"), Name: "test-host", Slug: "test-host", Endpoint: "http://localhost:9800", @@ -2455,8 +2514,8 @@ func TestBuildCreateRequest_PropagatesHarnessName(t *testing.T) { ID: "agent-harness-1", Name: "harness-agent", Slug: "harness-agent", - ProjectID: "project-1", - RuntimeBrokerID: "host-1", + ProjectID: tid("project-1"), + RuntimeBrokerID: tid("host-1"), AppliedConfig: &store.AgentAppliedConfig{ HarnessConfig: "gemini", Task: "do something", @@ -2483,7 +2542,7 @@ func TestHTTPAgentDispatcher_DispatchAgentStop_UsesSlugNotName(t *testing.T) { memStore := createTestStore(t) broker := &store.RuntimeBroker{ - ID: "host-1", + ID: tid("host-1"), Name: "test-host", Slug: "test-host", Endpoint: "http://localhost:9800", @@ -2497,10 +2556,10 @@ func TestHTTPAgentDispatcher_DispatchAgentStop_UsesSlugNotName(t *testing.T) { dispatcher := NewHTTPAgentDispatcherWithClient(memStore, mockClient, false, slog.Default()) agent := &store.Agent{ - ID: "agent-1", + ID: tid("agent-1"), Name: "My Special Agent!", Slug: "my-special-agent", - RuntimeBrokerID: "host-1", + RuntimeBrokerID: tid("host-1"), } err := dispatcher.DispatchAgentStop(ctx, agent) @@ -2518,7 +2577,7 @@ func TestHTTPAgentDispatcher_DispatchAgentDelete_UsesSlugNotName(t *testing.T) { memStore := createTestStore(t) broker := &store.RuntimeBroker{ - ID: "host-1", + ID: tid("host-1"), Name: "test-host", Slug: "test-host", Endpoint: "http://localhost:9800", @@ -2532,10 +2591,10 @@ func TestHTTPAgentDispatcher_DispatchAgentDelete_UsesSlugNotName(t *testing.T) { dispatcher := NewHTTPAgentDispatcherWithClient(memStore, mockClient, false, slog.Default()) agent := &store.Agent{ - ID: "agent-1", + ID: tid("agent-1"), Name: "slug Stres$@ . / test", Slug: "slug-stres-test", - RuntimeBrokerID: "host-1", + RuntimeBrokerID: tid("host-1"), } err := dispatcher.DispatchAgentDelete(ctx, agent, true, true, false, time.Time{}) @@ -2553,7 +2612,7 @@ func TestHTTPAgentDispatcher_DispatchAgentRestart_UsesSlugNotName(t *testing.T) memStore := createTestStore(t) broker := &store.RuntimeBroker{ - ID: "host-1", + ID: tid("host-1"), Name: "test-host", Slug: "test-host", Endpoint: "http://localhost:9800", @@ -2567,10 +2626,10 @@ func TestHTTPAgentDispatcher_DispatchAgentRestart_UsesSlugNotName(t *testing.T) dispatcher := NewHTTPAgentDispatcherWithClient(memStore, mockClient, false, slog.Default()) agent := &store.Agent{ - ID: "agent-1", + ID: tid("agent-1"), Name: "My Special Agent!", Slug: "my-special-agent", - RuntimeBrokerID: "host-1", + RuntimeBrokerID: tid("host-1"), } err := dispatcher.DispatchAgentRestart(ctx, agent) @@ -2588,7 +2647,7 @@ func TestHTTPAgentDispatcher_DispatchAgentMessage_UsesSlugNotName(t *testing.T) memStore := createTestStore(t) broker := &store.RuntimeBroker{ - ID: "host-1", + ID: tid("host-1"), Name: "test-host", Slug: "test-host", Endpoint: "http://localhost:9800", @@ -2602,10 +2661,10 @@ func TestHTTPAgentDispatcher_DispatchAgentMessage_UsesSlugNotName(t *testing.T) dispatcher := NewHTTPAgentDispatcherWithClient(memStore, mockClient, false, slog.Default()) agent := &store.Agent{ - ID: "agent-1", + ID: tid("agent-1"), Name: "My Special Agent!", Slug: "my-special-agent", - RuntimeBrokerID: "host-1", + RuntimeBrokerID: tid("host-1"), } err := dispatcher.DispatchAgentMessage(ctx, agent, "hello", false, nil) @@ -2623,7 +2682,7 @@ func TestHTTPAgentDispatcher_DispatchAgentStart_IncludesAgentIDAndSlug(t *testin memStore := createTestStore(t) broker := &store.RuntimeBroker{ - ID: "broker-id-test", + ID: tid("broker-id-test"), Name: "test-broker", Slug: "test-broker", Endpoint: "http://localhost:9800", @@ -2634,7 +2693,7 @@ func TestHTTPAgentDispatcher_DispatchAgentStart_IncludesAgentIDAndSlug(t *testin } project := &store.Project{ - ID: "project-id-test", + ID: tid("project-id-test"), Name: "test-project", Slug: "test-project", } @@ -2643,8 +2702,8 @@ func TestHTTPAgentDispatcher_DispatchAgentStart_IncludesAgentIDAndSlug(t *testin } provider := &store.ProjectProvider{ - ProjectID: "project-id-test", - BrokerID: "broker-id-test", + ProjectID: tid("project-id-test"), + BrokerID: tid("broker-id-test"), BrokerName: "test-broker", LocalPath: "/home/user/project/.scion", Status: store.BrokerStatusOnline, @@ -2660,8 +2719,8 @@ func TestHTTPAgentDispatcher_DispatchAgentStart_IncludesAgentIDAndSlug(t *testin ID: "agent-uuid-123", Name: "my-agent", Slug: "my-agent", - ProjectID: "project-id-test", - RuntimeBrokerID: "broker-id-test", + ProjectID: tid("project-id-test"), + RuntimeBrokerID: tid("broker-id-test"), AppliedConfig: &store.AgentAppliedConfig{ HarnessConfig: "claude", }, @@ -2695,7 +2754,7 @@ func TestHTTPAgentDispatcher_DispatchAgentStart_IncludesInlineConfig(t *testing. memStore := createTestStore(t) broker := &store.RuntimeBroker{ - ID: "broker-inline", + ID: tid("broker-inline"), Name: "test-broker", Slug: "test-broker", Endpoint: "http://localhost:9800", @@ -2706,7 +2765,7 @@ func TestHTTPAgentDispatcher_DispatchAgentStart_IncludesInlineConfig(t *testing. } project := &store.Project{ - ID: "project-inline", + ID: tid("project-inline"), Name: "test-project", Slug: "test-project", } @@ -2715,8 +2774,8 @@ func TestHTTPAgentDispatcher_DispatchAgentStart_IncludesInlineConfig(t *testing. } provider := &store.ProjectProvider{ - ProjectID: "project-inline", - BrokerID: "broker-inline", + ProjectID: tid("project-inline"), + BrokerID: tid("broker-inline"), BrokerName: "test-broker", LocalPath: "/home/user/project/.scion", Status: store.BrokerStatusOnline, @@ -2737,8 +2796,8 @@ func TestHTTPAgentDispatcher_DispatchAgentStart_IncludesInlineConfig(t *testing. ID: "agent-inline-cfg", Name: "inline-agent", Slug: "inline-agent", - ProjectID: "project-inline", - RuntimeBrokerID: "broker-inline", + ProjectID: tid("project-inline"), + RuntimeBrokerID: tid("broker-inline"), AppliedConfig: &store.AgentAppliedConfig{ HarnessConfig: "claude", InlineConfig: inlineCfg, @@ -2770,7 +2829,7 @@ func TestDispatchAgentStart_IncludesHubEndpoint(t *testing.T) { memStore := createTestStore(t) broker := &store.RuntimeBroker{ - ID: "host-1", + ID: tid("host-1"), Name: "test-host", Slug: "test-host", Endpoint: "http://localhost:9800", @@ -2785,12 +2844,12 @@ func TestDispatchAgentStart_IncludesHubEndpoint(t *testing.T) { dispatcher.SetHubEndpoint("http://hub.example.com:8080") agent := &store.Agent{ - ID: "agent-1", + ID: tid("agent-1"), Name: "test-agent", Slug: "test-agent", - ProjectID: "project-1", - OwnerID: "user-1", - RuntimeBrokerID: "host-1", + ProjectID: tid("project-1"), + OwnerID: tid("user-1"), + RuntimeBrokerID: tid("host-1"), AppliedConfig: &store.AgentAppliedConfig{ HarnessConfig: "claude", }, @@ -2817,11 +2876,11 @@ func TestDispatchAgentStart_IncludesHubEndpoint(t *testing.T) { } // Verify agent identity vars are also present - if mockClient.lastResolvedEnv["SCION_AGENT_ID"] != "agent-1" { - t.Errorf("SCION_AGENT_ID = %q, want %q", mockClient.lastResolvedEnv["SCION_AGENT_ID"], "agent-1") + if mockClient.lastResolvedEnv["SCION_AGENT_ID"] != tid("agent-1") { + t.Errorf("SCION_AGENT_ID = %q, want %q", mockClient.lastResolvedEnv["SCION_AGENT_ID"], tid("agent-1")) } - if mockClient.lastResolvedEnv["SCION_GROVE_ID"] != "project-1" { - t.Errorf("SCION_GROVE_ID = %q, want %q", mockClient.lastResolvedEnv["SCION_GROVE_ID"], "project-1") + if mockClient.lastResolvedEnv["SCION_GROVE_ID"] != tid("project-1") { + t.Errorf("SCION_GROVE_ID = %q, want %q", mockClient.lastResolvedEnv["SCION_GROVE_ID"], tid("project-1")) } } @@ -2831,7 +2890,7 @@ func TestHTTPAgentDispatcher_DispatchAgentCreate_PropagatesSharedWorkspace(t *te // Create a shared-workspace git project project := &store.Project{ - ID: "project-shared-ws", + ID: tid("project-shared-ws"), Name: "Shared WS", Slug: "shared-ws", GitRemote: "github.com/test/shared", @@ -2844,7 +2903,7 @@ func TestHTTPAgentDispatcher_DispatchAgentCreate_PropagatesSharedWorkspace(t *te } broker := &store.RuntimeBroker{ - ID: "host-1", + ID: tid("host-1"), Name: "test-host", Slug: "test-host", Endpoint: "http://localhost:9800", @@ -2861,8 +2920,8 @@ func TestHTTPAgentDispatcher_DispatchAgentCreate_PropagatesSharedWorkspace(t *te ID: "agent-shared-1", Name: "shared-agent", Slug: "shared-agent", - ProjectID: "project-shared-ws", - RuntimeBrokerID: "host-1", + ProjectID: tid("project-shared-ws"), + RuntimeBrokerID: tid("host-1"), AppliedConfig: &store.AgentAppliedConfig{ HarnessConfig: "claude", Workspace: "/home/user/.scion/projects/shared-ws", @@ -2897,7 +2956,7 @@ func TestHTTPAgentDispatcher_DispatchAgentStart_InjectsGCPIdentityEnv(t *testing memStore := createTestStore(t) project := &store.Project{ - ID: "project-gcp", + ID: tid("project-gcp"), Name: "gcp-project", Slug: "gcp-project", GitRemote: "https://github.com/example/repo.git", @@ -2907,7 +2966,7 @@ func TestHTTPAgentDispatcher_DispatchAgentStart_InjectsGCPIdentityEnv(t *testing } broker := &store.RuntimeBroker{ - ID: "broker-gcp", + ID: tid("broker-gcp"), Name: "test-broker", Slug: "test-broker", Endpoint: "http://localhost:9800", @@ -2918,8 +2977,8 @@ func TestHTTPAgentDispatcher_DispatchAgentStart_InjectsGCPIdentityEnv(t *testing } provider := &store.ProjectProvider{ - ProjectID: "project-gcp", - BrokerID: "broker-gcp", + ProjectID: tid("project-gcp"), + BrokerID: tid("broker-gcp"), BrokerName: "test-broker", LocalPath: "/home/user/projects/myproject/.scion", Status: store.BrokerStatusOnline, @@ -2935,14 +2994,14 @@ func TestHTTPAgentDispatcher_DispatchAgentStart_InjectsGCPIdentityEnv(t *testing ID: "agent-gcp-1", Name: "gcp-agent", Slug: "gcp-agent", - ProjectID: "project-gcp", - RuntimeBrokerID: "broker-gcp", + ProjectID: tid("project-gcp"), + RuntimeBrokerID: tid("broker-gcp"), AppliedConfig: &store.AgentAppliedConfig{ GCPIdentity: &store.GCPIdentityConfig{ MetadataMode: store.GCPMetadataModeAssign, ServiceAccountID: "sa-123", ServiceAccountEmail: "sa@proj.iam.gserviceaccount.com", - ProjectID: "my-project", + ProjectID: tid("my-project"), }, }, } @@ -2963,7 +3022,7 @@ func TestHTTPAgentDispatcher_DispatchAgentStart_InjectsGCPIdentityEnv(t *testing if v := mockClient.lastResolvedEnv["SCION_METADATA_SA_EMAIL"]; v != "sa@proj.iam.gserviceaccount.com" { t.Errorf("expected SCION_METADATA_SA_EMAIL='sa@proj.iam.gserviceaccount.com', got %q", v) } - if v := mockClient.lastResolvedEnv["SCION_METADATA_PROJECT_ID"]; v != "my-project" { + if v := mockClient.lastResolvedEnv["SCION_METADATA_PROJECT_ID"]; v != tid("my-project") { t.Errorf("expected SCION_METADATA_PROJECT_ID='my-project', got %q", v) } } @@ -2973,7 +3032,7 @@ func TestHTTPAgentDispatcher_DispatchAgentStart_GCPBlockMode(t *testing.T) { memStore := createTestStore(t) project := &store.Project{ - ID: "project-gcp-block", + ID: tid("project-gcp-block"), Name: "gcp-project", Slug: "gcp-project", GitRemote: "https://github.com/example/repo.git", @@ -2983,7 +3042,7 @@ func TestHTTPAgentDispatcher_DispatchAgentStart_GCPBlockMode(t *testing.T) { } broker := &store.RuntimeBroker{ - ID: "broker-gcp-block", + ID: tid("broker-gcp-block"), Name: "test-broker", Slug: "test-broker", Endpoint: "http://localhost:9800", @@ -2994,8 +3053,8 @@ func TestHTTPAgentDispatcher_DispatchAgentStart_GCPBlockMode(t *testing.T) { } provider := &store.ProjectProvider{ - ProjectID: "project-gcp-block", - BrokerID: "broker-gcp-block", + ProjectID: tid("project-gcp-block"), + BrokerID: tid("broker-gcp-block"), BrokerName: "test-broker", LocalPath: "/home/user/projects/myproject/.scion", Status: store.BrokerStatusOnline, @@ -3011,8 +3070,8 @@ func TestHTTPAgentDispatcher_DispatchAgentStart_GCPBlockMode(t *testing.T) { ID: "agent-gcp-block", Name: "gcp-agent", Slug: "gcp-agent", - ProjectID: "project-gcp-block", - RuntimeBrokerID: "broker-gcp-block", + ProjectID: tid("project-gcp-block"), + RuntimeBrokerID: tid("broker-gcp-block"), AppliedConfig: &store.AgentAppliedConfig{ GCPIdentity: &store.GCPIdentityConfig{ MetadataMode: store.GCPMetadataModeBlock, @@ -3063,7 +3122,7 @@ func TestBuildCreateRequest_UserGitHubTokenPrecedesApp(t *testing.T) { } project := &store.Project{ - ID: "project-1", + ID: tid("project-1"), Name: "test-project", Slug: "test-project", GitHubInstallationID: &installID, @@ -3073,7 +3132,7 @@ func TestBuildCreateRequest_UserGitHubTokenPrecedesApp(t *testing.T) { } broker := &store.RuntimeBroker{ - ID: "host-1", + ID: tid("host-1"), Name: "test-host", Slug: "test-host", Endpoint: "http://localhost:9800", @@ -3093,12 +3152,12 @@ func TestBuildCreateRequest_UserGitHubTokenPrecedesApp(t *testing.T) { dispatcher.SetGitHubAppMinter(minter) agent := &store.Agent{ - ID: "agent-1", + ID: tid("agent-1"), Name: "test-agent", Slug: "test-agent", - OwnerID: "user-1", - ProjectID: "project-1", - RuntimeBrokerID: "host-1", + OwnerID: tid("user-1"), + ProjectID: tid("project-1"), + RuntimeBrokerID: tid("host-1"), AppliedConfig: &store.AgentAppliedConfig{ Env: map[string]string{ "GITHUB_TOKEN": "ghp_user_pat_xyz", @@ -3148,7 +3207,7 @@ func TestBuildCreateRequest_GitHubAppTokenWhenNoUserToken(t *testing.T) { } project := &store.Project{ - ID: "project-1", + ID: tid("project-1"), Name: "test-project", Slug: "test-project", GitHubInstallationID: &installID, @@ -3158,7 +3217,7 @@ func TestBuildCreateRequest_GitHubAppTokenWhenNoUserToken(t *testing.T) { } broker := &store.RuntimeBroker{ - ID: "host-1", + ID: tid("host-1"), Name: "test-host", Slug: "test-host", Endpoint: "http://localhost:9800", @@ -3178,12 +3237,12 @@ func TestBuildCreateRequest_GitHubAppTokenWhenNoUserToken(t *testing.T) { dispatcher.SetGitHubAppMinter(minter) agent := &store.Agent{ - ID: "agent-1", + ID: tid("agent-1"), Name: "test-agent", Slug: "test-agent", - OwnerID: "user-1", - ProjectID: "project-1", - RuntimeBrokerID: "host-1", + OwnerID: tid("user-1"), + ProjectID: tid("project-1"), + RuntimeBrokerID: tid("host-1"), AppliedConfig: &store.AgentAppliedConfig{}, } diff --git a/pkg/hub/logquery_test.go b/pkg/hub/logquery_test.go index c128d8ff0..dc6ce2343 100644 --- a/pkg/hub/logquery_test.go +++ b/pkg/hub/logquery_test.go @@ -180,7 +180,7 @@ func TestConvertLogEntry(t *testing.T) { Payload: "Agent started processing task", Labels: map[string]string{ "agent_id": "abc123", - "project_id": "my-project", + "project_id": tid("my-project"), }, InsertID: "insert-1", } diff --git a/pkg/hub/maintenance_executors.go b/pkg/hub/maintenance_executors.go index 26fa59dca..7ec561299 100644 --- a/pkg/hub/maintenance_executors.go +++ b/pkg/hub/maintenance_executors.go @@ -180,7 +180,7 @@ func (e *PullImagesExecutor) Run(ctx context.Context, logger io.Writer, params m harnesses := e.harnesses if len(harnesses) == 0 { - harnesses = []string{"claude", "gemini", "opencode", "codex"} + harnesses = []string{"claude", "gemini"} } log.Debug("Starting pull-images", diff --git a/pkg/hub/maintenance_executors_test.go b/pkg/hub/maintenance_executors_test.go index 08f194855..df4069f3d 100644 --- a/pkg/hub/maintenance_executors_test.go +++ b/pkg/hub/maintenance_executors_test.go @@ -28,11 +28,10 @@ import ( "time" "github.com/GoogleCloudPlatform/scion/pkg/secret" - "github.com/GoogleCloudPlatform/scion/pkg/store/sqlite" ) func TestSecretMigrationExecutor_NoGCPBackend(t *testing.T) { - s, err := sqlite.New(":memory:") + s, err := newTestStore(":memory:") if err != nil { t.Fatalf("failed to create sqlite store: %v", err) } diff --git a/pkg/hub/messagebroker.go b/pkg/hub/messagebroker.go index bf6fd062a..d0877506b 100644 --- a/pkg/hub/messagebroker.go +++ b/pkg/hub/messagebroker.go @@ -17,6 +17,7 @@ package hub import ( "context" "encoding/json" + "errors" "fmt" "log/slog" "strings" @@ -43,12 +44,13 @@ const brokerCallbackTimeout = 30 * time.Second // - Manages subscriptions based on agent lifecycle events (created/deleted) // - Handles broadcast fan-out from a single broker publish to individual agent deliveries type MessageBrokerProxy struct { - bus eventbus.EventBus - store store.Store - events *ChannelEventPublisher - getDispatcher func() AgentDispatcher - log *slog.Logger - messageLog *slog.Logger + bus eventbus.EventBus + store store.Store + events EventPublisher + getDispatcher func() AgentDispatcher + signalDeferred func(ctx context.Context, brokerID, agentID string) // NOTIFY wakeup for deferred messages + log *slog.Logger + messageLog *slog.Logger mu sync.Mutex subscriptions map[string][]eventbus.Subscription // projectID -> active subscriptions @@ -63,7 +65,7 @@ type MessageBrokerProxy struct { func NewMessageBrokerProxy( b eventbus.EventBus, s store.Store, - events *ChannelEventPublisher, + events EventPublisher, getDispatcher func() AgentDispatcher, log *slog.Logger, ) *MessageBrokerProxy { @@ -80,6 +82,12 @@ func NewMessageBrokerProxy( } } +// SetSignalDeferred injects the callback used to emit a NOTIFY wakeup when a +// message dispatch is deferred (broker not locally connected). +func (p *MessageBrokerProxy) SetSignalDeferred(fn func(ctx context.Context, brokerID, agentID string)) { + p.signalDeferred = fn +} + // Start subscribes to agent lifecycle events and sets up broker subscriptions // for existing running agents. func (p *MessageBrokerProxy) Start() { @@ -243,7 +251,7 @@ func (p *MessageBrokerProxy) PublishUserMessage(ctx context.Context, projectID, return p.bus.Publish(ctx, topic, msg) } -// PublishToGroup fans out a message to a parsed group of recipients, delegating +// PublishToGroup fans out a message to a parsed set of recipients, delegating // to PublishMessage for agents and PublishUserMessage for users. func (p *MessageBrokerProxy) PublishToGroup(ctx context.Context, projectID string, recipients []messages.GroupRecipient, msg *messages.StructuredMessage) map[string]error { errs := make(map[string]error, len(recipients)) @@ -507,13 +515,8 @@ func (p *MessageBrokerProxy) deliverToAgent(ctx context.Context, projectID, agen return } - if err := dispatcher.DispatchAgentMessage(ctx, agent, msg.Msg, msg.Urgent, msg); err != nil { - p.log.Error("Failed to dispatch broker message to agent", - "agentSlug", agentSlug, "error", err) - return - } - - // Persist to message store (write-through; non-fatal if store fails). + // Persist to message store first so the row is durable intent for + // cross-node dispatch (dispatch_state defaults to pending). storeMsg := &store.Message{ ID: api.NewUUID(), ProjectID: projectID, @@ -526,12 +529,30 @@ func (p *MessageBrokerProxy) deliverToAgent(ctx context.Context, projectID, agen Urgent: msg.Urgent, Broadcasted: msg.Broadcasted, AgentID: agent.ID, - Channel: msg.Channel, - ThreadID: msg.ThreadID, CreatedAt: time.Now(), } if err := p.store.CreateMessage(ctx, storeMsg); err != nil { p.log.Error("Failed to persist broker message to store", "agentSlug", agentSlug, "error", err) + // Without a durable row, a deferred signal has nothing for the + // owning node to reconcile — abort dispatch entirely. + return + } + + if err := dispatcher.DispatchAgentMessage(ctx, agent, msg.Msg, msg.Urgent, msg); errors.Is(err, ErrMessageDeferred) { + if p.signalDeferred != nil { + p.signalDeferred(ctx, agent.RuntimeBrokerID, agent.ID) + } + return + } else if err != nil { + p.log.Error("Failed to dispatch broker message to agent", + "agentSlug", agentSlug, "error", err) + return + } + + // Mark the message as dispatched so reconcileBroker does not + // re-deliver it on the next broker reconnect. + if _, err := p.store.MarkMessageDispatched(ctx, storeMsg.ID); err != nil { + p.log.Error("Failed to mark broker message dispatched", "id", storeMsg.ID, "error", err) } // Log to dedicated message audit log @@ -596,8 +617,8 @@ func (p *MessageBrokerProxy) fanOutGlobal(ctx context.Context, msg *messages.Str } } -// ListChannels returns the names of registered bus channels. Returns nil if -// the underlying bus does not support channel listing. +// ListChannels returns the named bus channels when using a FanOutEventBus, +// or nil for single-bus configurations. Used by the message-channels API. func (p *MessageBrokerProxy) ListChannels() []eventbus.BusChannel { if fb, ok := p.bus.(*eventbus.FanOutEventBus); ok { return fb.BusChannels() diff --git a/pkg/hub/messagebroker_test.go b/pkg/hub/messagebroker_test.go index 66b5bd18f..c3dde1b98 100644 --- a/pkg/hub/messagebroker_test.go +++ b/pkg/hub/messagebroker_test.go @@ -27,7 +27,6 @@ import ( "github.com/GoogleCloudPlatform/scion/pkg/eventbus" "github.com/GoogleCloudPlatform/scion/pkg/messages" "github.com/GoogleCloudPlatform/scion/pkg/store" - "github.com/GoogleCloudPlatform/scion/pkg/store/sqlite" ) // brokerMockDispatcher records dispatched messages for test assertions. @@ -58,6 +57,9 @@ func (d *brokerMockDispatcher) DispatchAgentStop(ctx context.Context, agent *sto func (d *brokerMockDispatcher) DispatchAgentRestart(ctx context.Context, agent *store.Agent) error { return nil } +func (d *brokerMockDispatcher) DispatchAgentResetAuth(_ context.Context, _ *store.Agent) error { + return nil +} func (d *brokerMockDispatcher) DispatchAgentDelete(ctx context.Context, agent *store.Agent, deleteFiles, removeBranch, softDelete bool, deletedAt time.Time) error { return nil } @@ -98,7 +100,7 @@ func (d *brokerMockDispatcher) getMessages() []brokerDispatchedMsg { func newBrokerTestStore(t *testing.T) store.Store { t.Helper() - s, err := sqlite.New(":memory:") + s, err := newTestStore(":memory:") if err != nil { t.Fatalf("failed to create test store: %v", err) } @@ -115,7 +117,7 @@ func setupBrokerTestProject(t *testing.T, s store.Store) string { // Create a runtime broker for agent FK constraints rb := &store.RuntimeBroker{ - ID: "broker-1", + ID: tid("broker-1"), Name: "test-broker", Slug: "test-broker", Endpoint: "http://localhost:9800", @@ -146,7 +148,7 @@ func setupBrokerTestAgent(t *testing.T, s store.Store, projectID, slug, phase st Slug: slug, ProjectID: projectID, Phase: phase, - RuntimeBrokerID: "broker-1", + RuntimeBrokerID: tid("broker-1"), Visibility: store.VisibilityPrivate, } if err := s.CreateAgent(context.Background(), agent); err != nil { @@ -239,7 +241,7 @@ func TestMessageBrokerProxy_BroadcastSkipsSender(t *testing.T) { s := newBrokerTestStore(t) projectID := setupBrokerTestProject(t, s) setupBrokerTestAgent(t, s, projectID, "sender-agent", "running") - setupBrokerTestAgent(t, s, projectID, "other-agent", "running") + setupBrokerTestAgent(t, s, projectID, tid("other-agent"), "running") events := NewChannelEventPublisher() defer events.Close() @@ -265,7 +267,7 @@ func TestMessageBrokerProxy_BroadcastSkipsSender(t *testing.T) { if len(dispatched) != 1 { t.Fatalf("expected 1 message (sender excluded), got %d", len(dispatched)) } - if dispatched[0].agentSlug != "other-agent" { + if dispatched[0].agentSlug != tid("other-agent") { t.Errorf("expected message delivered to 'other-agent', got %q", dispatched[0].agentSlug) } } diff --git a/pkg/hub/notifications.go b/pkg/hub/notifications.go index 377393632..391d29b28 100644 --- a/pkg/hub/notifications.go +++ b/pkg/hub/notifications.go @@ -17,6 +17,7 @@ package hub import ( "context" "encoding/json" + "errors" "fmt" "log/slog" "strings" @@ -33,8 +34,9 @@ import ( // messages to subscriber agents. type NotificationDispatcher struct { store store.Store - events *ChannelEventPublisher - getDispatcher func() AgentDispatcher // lazy getter; dispatcher may be set after startup + events EventPublisher + getDispatcher func() AgentDispatcher // lazy getter; dispatcher may be set after startup + signalDeferred func(ctx context.Context, brokerID, agentID string) // NOTIFY wakeup for deferred messages log *slog.Logger messageLog *slog.Logger // dedicated message audit logger (nil = disabled) channelRegistry *ChannelRegistry // external notification channels (nil = disabled) @@ -48,7 +50,7 @@ type NotificationDispatcher struct { // The getDispatcher function is called at dispatch time to resolve the current // AgentDispatcher, allowing the dispatcher to be set up after the notification // system starts (e.g. in combined hub+web mode). -func NewNotificationDispatcher(s store.Store, events *ChannelEventPublisher, getDispatcher func() AgentDispatcher, log *slog.Logger) *NotificationDispatcher { +func NewNotificationDispatcher(s store.Store, events EventPublisher, getDispatcher func() AgentDispatcher, log *slog.Logger) *NotificationDispatcher { return &NotificationDispatcher{ store: s, events: events, @@ -358,7 +360,13 @@ func (nd *NotificationDispatcher) dispatchToAgent(ctx context.Context, sub *stor structuredMsg.RecipientID = subscriber.ID structuredMsg.Status = strings.ToUpper(notif.Status) - if err := dispatcher.DispatchAgentMessage(ctx, subscriber, notif.Message, false, structuredMsg); err != nil { + if err := dispatcher.DispatchAgentMessage(ctx, subscriber, notif.Message, false, structuredMsg); errors.Is(err, ErrMessageDeferred) { + nd.log.Info("Notification deferred for cross-node delivery", + "subscriberID", sub.SubscriberID, "brokerID", subscriber.RuntimeBrokerID) + if nd.signalDeferred != nil { + nd.signalDeferred(ctx, subscriber.RuntimeBrokerID, subscriber.ID) + } + } else if err != nil { nd.log.Error("Failed to dispatch notification to agent", "subscriberID", sub.SubscriberID, "error", err) } else { diff --git a/pkg/hub/notifications_integration_test.go b/pkg/hub/notifications_integration_test.go index d458d509b..dbec588b2 100644 --- a/pkg/hub/notifications_integration_test.go +++ b/pkg/hub/notifications_integration_test.go @@ -67,7 +67,7 @@ func setupIntegrationTest(t *testing.T) *integrationTestEnv { srv.SetDispatcher(recorder) project := &store.Project{ - ID: "project-integ", + ID: tid("project-integ"), Name: "Integration Project", Slug: "integration-project", Visibility: store.VisibilityPrivate, @@ -75,7 +75,7 @@ func setupIntegrationTest(t *testing.T) *integrationTestEnv { require.NoError(t, s.CreateProject(ctx, project)) broker := &store.RuntimeBroker{ - ID: "broker-integ", + ID: tid("broker-integ"), Name: "Integration Broker", Slug: "integration-broker", Status: store.BrokerStatusOnline, @@ -115,6 +115,10 @@ func setupIntegrationTest(t *testing.T) *integrationTestEnv { func (env *integrationTestEnv) createAgentWithNotify(t *testing.T, callingAgent *store.Agent, subAgentName string) *store.Agent { t.Helper() + // The created sub-agent's created_by/owner_id FK references the users table, + // so seed a user sharing the calling agent's ID. + permSeedUser(t, context.Background(), env.store, callingAgent.ID) + token, err := env.tokenSvc.GenerateAgentToken(callingAgent.ID, env.project.ID, []AgentTokenScope{ ScopeAgentStatusUpdate, ScopeAgentCreate, @@ -186,7 +190,7 @@ func TestIntegration_AgentCreatesAgentWithNotify_FullFlow(t *testing.T) { // Create the parent agent (subscriber) parent := &store.Agent{ - ID: "agent-parent", + ID: tid("agent-parent"), Slug: "parent-agent", Name: "Parent Agent", ProjectID: env.project.ID, @@ -242,7 +246,7 @@ func TestIntegration_AgentCreatesAgentWithNotify_WaitingForInput(t *testing.T) { ctx := context.Background() parent := &store.Agent{ - ID: "agent-parent-wfi", + ID: tid("agent-parent-wfi"), Slug: "parent-agent-wfi", Name: "Parent Agent WFI", ProjectID: env.project.ID, @@ -290,7 +294,7 @@ func TestIntegration_AgentCreatesAgentWithNotify_MultipleStatusChanges(t *testin ctx := context.Background() parent := &store.Agent{ - ID: "agent-parent-multi", + ID: tid("agent-parent-multi"), Slug: "parent-multi", Name: "Parent Multi", ProjectID: env.project.ID, @@ -348,7 +352,7 @@ func TestIntegration_StatusNormalization_LowercaseEventMatchesUppercaseTrigger(t ctx := context.Background() parent := &store.Agent{ - ID: "agent-parent-case", + ID: tid("agent-parent-case"), Slug: "parent-case", Name: "Parent Case", ProjectID: env.project.ID, @@ -385,7 +389,7 @@ func TestIntegration_StatusNormalization_DedupAcrossCaseBoundaries(t *testing.T) ctx := context.Background() parent := &store.Agent{ - ID: "agent-parent-dedup", + ID: tid("agent-parent-dedup"), Slug: "parent-dedup", Name: "Parent Dedup", ProjectID: env.project.ID, @@ -426,7 +430,7 @@ func TestIntegration_StatusNormalization_NonTriggerStatusNoNotification(t *testi ctx := context.Background() parent := &store.Agent{ - ID: "agent-parent-nontrig", + ID: tid("agent-parent-nontrig"), Slug: "parent-nontrig", Name: "Parent NonTrig", ProjectID: env.project.ID, @@ -476,7 +480,7 @@ func TestIntegration_SubscriptionCleanup_HardDeleteCascades(t *testing.T) { ctx := context.Background() parent := &store.Agent{ - ID: "agent-parent-hdel", + ID: tid("agent-parent-hdel"), Slug: "parent-hdel", Name: "Parent Hard Delete", ProjectID: env.project.ID, @@ -528,7 +532,7 @@ func TestIntegration_SubscriptionCleanup_SoftDeleteRetainsSubscriptions(t *testi env.srv.config.SoftDeleteRetention = 24 * time.Hour parent := &store.Agent{ - ID: "agent-parent-sdel", + ID: tid("agent-parent-sdel"), Slug: "parent-sdel", Name: "Parent Soft Delete", ProjectID: env.project.ID, @@ -741,7 +745,7 @@ func TestIntegration_MultipleSubscribers_AgentAndUser(t *testing.T) { // Create parent agent parent := &store.Agent{ - ID: "agent-parent-multi-sub", + ID: tid("agent-parent-multi-sub"), Slug: "parent-multi-sub", Name: "Parent Multi Sub", ProjectID: env.project.ID, @@ -756,7 +760,7 @@ func TestIntegration_MultipleSubscribers_AgentAndUser(t *testing.T) { // User also subscribes to the same child (manually, since the API doesn't support this yet) userSub := &store.NotificationSubscription{ - ID: "user-sub-multi", + ID: tid("user-sub-multi"), AgentID: child.ID, SubscriberType: store.SubscriberTypeUser, SubscriberID: DevUserID, @@ -802,7 +806,7 @@ func TestIntegration_NoNotifyFlag_NoSubscription(t *testing.T) { ctx := context.Background() parent := &store.Agent{ - ID: "agent-parent-no-notify", + ID: tid("agent-parent-no-notify"), Slug: "parent-no-notify", Name: "Parent No Notify", ProjectID: env.project.ID, @@ -812,6 +816,9 @@ func TestIntegration_NoNotifyFlag_NoSubscription(t *testing.T) { } require.NoError(t, env.store.CreateAgent(ctx, parent)) + // The created sub-agent's created_by/owner_id FK references the users table. + permSeedUser(t, ctx, env.store, parent.ID) + // Create sub-agent WITHOUT notify token, err := env.tokenSvc.GenerateAgentToken(parent.ID, env.project.ID, []AgentTokenScope{ ScopeAgentStatusUpdate, @@ -860,7 +867,7 @@ func TestIntegration_PATCHSubscriptionTriggers(t *testing.T) { // Create a subscription via store (SubscriberID must match DevUserID) sub := &store.NotificationSubscription{ - ID: "sub-patch-test", + ID: tid("sub-patch-test"), Scope: store.SubscriptionScopeProject, SubscriberType: store.SubscriberTypeUser, SubscriberID: DevUserID, diff --git a/pkg/hub/notifications_test.go b/pkg/hub/notifications_test.go index c744934a0..d084e09bc 100644 --- a/pkg/hub/notifications_test.go +++ b/pkg/hub/notifications_test.go @@ -29,7 +29,6 @@ import ( "github.com/GoogleCloudPlatform/scion/pkg/eventbus" "github.com/GoogleCloudPlatform/scion/pkg/messages" "github.com/GoogleCloudPlatform/scion/pkg/store" - "github.com/GoogleCloudPlatform/scion/pkg/store/sqlite" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -77,6 +76,9 @@ func (d *recordingDispatcher) DispatchAgentStop(_ context.Context, _ *store.Agen func (d *recordingDispatcher) DispatchAgentRestart(_ context.Context, _ *store.Agent) error { return nil } +func (d *recordingDispatcher) DispatchAgentResetAuth(_ context.Context, _ *store.Agent) error { + return nil +} func (d *recordingDispatcher) DispatchAgentDelete(_ context.Context, _ *store.Agent, _, _, _ bool, _ time.Time) error { return nil } @@ -149,7 +151,7 @@ type notificationTestEnv struct { func setupNotificationTest(t *testing.T) *notificationTestEnv { t.Helper() - s, err := sqlite.New(":memory:") + s, err := newTestStore(":memory:") if err != nil { t.Fatalf("failed to create test store: %v", err) } @@ -172,7 +174,7 @@ func setupNotificationTest(t *testing.T) *notificationTestEnv { require.NoError(t, s.CreateProject(ctx, project)) broker := &store.RuntimeBroker{ - ID: "broker-1", + ID: tid("broker-1"), Name: "Test Broker", Slug: "test-broker", Status: store.BrokerStatusOnline, @@ -186,7 +188,7 @@ func setupNotificationTest(t *testing.T) *notificationTestEnv { Template: "claude", ProjectID: project.ID, Phase: string(state.PhaseRunning), - RuntimeBrokerID: "broker-1", + RuntimeBrokerID: tid("broker-1"), Visibility: store.VisibilityPrivate, } require.NoError(t, s.CreateAgent(ctx, watched)) @@ -198,7 +200,7 @@ func setupNotificationTest(t *testing.T) *notificationTestEnv { Template: "claude", ProjectID: project.ID, Phase: string(state.PhaseRunning), - RuntimeBrokerID: "broker-1", + RuntimeBrokerID: tid("broker-1"), Visibility: store.VisibilityPrivate, } require.NoError(t, s.CreateAgent(ctx, subscriber)) @@ -1238,7 +1240,7 @@ func TestUpdateNotificationSubscriptionTriggers_NotFound(t *testing.T) { env := setupNotificationTest(t) ctx := context.Background() - err := env.store.UpdateNotificationSubscriptionTriggers(ctx, "nonexistent-id", []string{"COMPLETED"}) + err := env.store.UpdateNotificationSubscriptionTriggers(ctx, tid("nonexistent-id"), []string{"COMPLETED"}) assert.ErrorIs(t, err, store.ErrNotFound) } diff --git a/pkg/hub/project_cache_test.go b/pkg/hub/project_cache_test.go index 0b5c1b1fe..cc30fd3aa 100644 --- a/pkg/hub/project_cache_test.go +++ b/pkg/hub/project_cache_test.go @@ -49,13 +49,15 @@ func createTestLinkedProject(t *testing.T, srv *Server, s store.Store, name, rem // Create a provider broker record with a local path brokerLocalPath := t.TempDir() broker := &store.RuntimeBroker{ - ID: "test-broker-remote", + ID: tid("test-broker-remote"), Name: "remote-broker", + Slug: "remote-broker", } require.NoError(t, s.CreateRuntimeBroker(context.Background(), broker)) require.NoError(t, s.AddProjectProvider(context.Background(), &store.ProjectProvider{ - ProjectID: project.ID, - BrokerID: broker.ID, + ProjectID: project.ID, + BrokerID: broker.ID, + BrokerName: broker.Name, // LocalPath is set to simulate a linked project with workspace on broker LocalPath: brokerLocalPath, })) @@ -101,18 +103,20 @@ func TestResolveProjectWebDAVPath_LinkedProject_EmbeddedBroker(t *testing.T) { project := createTestGitProject(t, srv, "WebDAV Embedded", "https://github.com/org/embedded-repo.git") embeddedPath := t.TempDir() - embeddedBrokerID := "test-embedded-broker" + embeddedBrokerID := tid("test-embedded-broker") srv.SetEmbeddedBrokerID(embeddedBrokerID) broker := &store.RuntimeBroker{ ID: embeddedBrokerID, Name: "embedded-broker", + Slug: "embedded-broker", } require.NoError(t, s.CreateRuntimeBroker(context.Background(), broker)) require.NoError(t, s.AddProjectProvider(context.Background(), &store.ProjectProvider{ - ProjectID: project.ID, - BrokerID: embeddedBrokerID, - LocalPath: embeddedPath, + ProjectID: project.ID, + BrokerID: embeddedBrokerID, + BrokerName: broker.Name, + LocalPath: embeddedPath, })) // For embedded broker, should serve directly from local path @@ -143,15 +147,16 @@ func TestIsLinkedProject_EmbeddedBrokerOnly(t *testing.T) { srv, s := testServer(t) project := createTestGitProject(t, srv, "IsLinked Embedded", "https://github.com/org/emb.git") - embeddedBrokerID := "embedded-only" + embeddedBrokerID := tid("embedded-only") srv.SetEmbeddedBrokerID(embeddedBrokerID) - broker := &store.RuntimeBroker{ID: embeddedBrokerID, Name: "emb"} + broker := &store.RuntimeBroker{ID: embeddedBrokerID, Name: "emb", Slug: "emb"} require.NoError(t, s.CreateRuntimeBroker(context.Background(), broker)) require.NoError(t, s.AddProjectProvider(context.Background(), &store.ProjectProvider{ - ProjectID: project.ID, - BrokerID: embeddedBrokerID, - LocalPath: "/some/path", + ProjectID: project.ID, + BrokerID: embeddedBrokerID, + BrokerName: broker.Name, + LocalPath: "/some/path", })) // Embedded broker with local path should NOT be considered "linked" (it's co-located) diff --git a/pkg/hub/project_settings_handlers_test.go b/pkg/hub/project_settings_handlers_test.go index cf34abc6e..db262a638 100644 --- a/pkg/hub/project_settings_handlers_test.go +++ b/pkg/hub/project_settings_handlers_test.go @@ -283,7 +283,7 @@ func TestProjectSettings_NotFound(t *testing.T) { func createTestProjectForSettings(t *testing.T, s store.Store) *store.Project { t.Helper() project := &store.Project{ - ID: "test-project-settings-" + t.Name(), + ID: tid("test-project-settings-" + t.Name()), Name: "Test Project", Slug: "test-project-settings", Visibility: "private", diff --git a/pkg/hub/project_workspace_handlers_test.go b/pkg/hub/project_workspace_handlers_test.go index 50068be08..fd6420a11 100644 --- a/pkg/hub/project_workspace_handlers_test.go +++ b/pkg/hub/project_workspace_handlers_test.go @@ -884,7 +884,7 @@ func TestSharedDirFiles_GitProjectWithEmbeddedBroker(t *testing.T) { // Create a broker and set it as the embedded broker broker := &store.RuntimeBroker{ - ID: "embedded-broker-001", + ID: tid("embedded-broker-001"), Name: "local-broker", Slug: "local-broker", Endpoint: "http://localhost:9090", @@ -937,7 +937,7 @@ func TestSharedDirFiles_GitProjectMultipleProviders(t *testing.T) { // Create embedded broker embeddedBroker := &store.RuntimeBroker{ - ID: "embedded-broker-002", + ID: tid("embedded-broker-002"), Name: "local-broker", Slug: "local-broker-2", Endpoint: "http://localhost:9090", @@ -948,7 +948,7 @@ func TestSharedDirFiles_GitProjectMultipleProviders(t *testing.T) { // Create a second (remote) broker remoteBroker := &store.RuntimeBroker{ - ID: "remote-broker-001", + ID: tid("remote-broker-001"), Name: "remote-broker", Slug: "remote-broker", Endpoint: "http://remote:9090", @@ -1091,7 +1091,7 @@ func TestProjectWorkspacePull_MethodNotAllowed(t *testing.T) { // Create shared-workspace project directly in the store to avoid clone attempt project := store.Project{ - ID: "pull-method-test-id", + ID: tid("pull-method-test-id"), Name: "Pull Method Test", Slug: "pull-method-test", GitRemote: "github.com/test/pull-method", diff --git a/pkg/hub/proxyauth.go b/pkg/hub/proxyauth.go new file mode 100644 index 000000000..fb4cd7a79 --- /dev/null +++ b/pkg/hub/proxyauth.go @@ -0,0 +1,355 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package hub + +import ( + "encoding/json" + "fmt" + "io" + "log/slog" + "net/http" + "strings" + "sync" + "time" + + "github.com/go-jose/go-jose/v4" + "github.com/go-jose/go-jose/v4/jwt" +) + +// ProxyUserInfo is the verified identity extracted from proxy headers/assertions. +type ProxyUserInfo struct { + Subject string // stable provider subject (IdP prefix stripped) + Email string // verified email (IdP prefix stripped, lowercased) + DisplayName string // best-effort; may be empty for IAP + Domain string // hd claim, if present +} + +// ProxyAuthenticator verifies proxy-supplied auth on a request and returns the +// verified user. (nil, nil) = "no proxy assertion present" (fall through); +// (nil, err) = assertion present but invalid (reject). +type ProxyAuthenticator interface { + Authenticate(r *http.Request) (*ProxyUserInfo, error) + Name() string // for logging/metrics, e.g. "iap" +} + +// ---- Google IAP Authenticator ---- + +const ( + // IAPAssertionHeader is the header containing the IAP signed JWT. + IAPAssertionHeader = "X-Goog-IAP-JWT-Assertion" + + // DefaultIAPIssuer is the expected issuer for IAP JWTs. + DefaultIAPIssuer = "https://cloud.google.com/iap" + + // DefaultIAPJWKSURL is the URL for IAP public keys. + DefaultIAPJWKSURL = "https://www.gstatic.com/iap/verify/public_key-jwk" + + // iapClockSkew is the allowed clock skew for exp/iat validation. + iapClockSkew = 30 * time.Second + + // jwksRefreshInterval is how often the JWKS cache proactively refreshes. + jwksRefreshInterval = 1 * time.Hour + + // iapIdPPrefix is the IdP prefix stripped from IAP sub/email claims. + iapIdPPrefix = "accounts.google.com:" +) + +// IAPAuthenticator verifies Google IAP signed JWTs (X-Goog-IAP-JWT-Assertion). +type IAPAuthenticator struct { + // Audience is the expected audience claim — MANDATORY. + Audience string + + // Issuer is the expected issuer (defaults to DefaultIAPIssuer). + Issuer string + + // JWKSURL is the JWKS endpoint (defaults to DefaultIAPJWKSURL). + JWKSURL string + + // HTTPClient is the HTTP client for fetching JWKS (defaults to http.DefaultClient). + HTTPClient *http.Client + + jwksCache *jwksCache + initOnce sync.Once +} + +// Name returns "iap" for logging/metrics. +func (a *IAPAuthenticator) Name() string { return "iap" } + +// Authenticate reads the IAP assertion header, verifies the JWT, and returns +// the verified ProxyUserInfo. Returns (nil, nil) if no assertion is present. +func (a *IAPAuthenticator) Authenticate(r *http.Request) (*ProxyUserInfo, error) { + a.initOnce.Do(a.init) + + assertion := r.Header.Get(IAPAssertionHeader) + if assertion == "" { + return nil, nil // no assertion present, fall through + } + + // Parse the JWT (compact serialization) + tok, err := jwt.ParseSigned(assertion, []jose.SignatureAlgorithm{jose.ES256}) + if err != nil { + return nil, fmt.Errorf("iap: failed to parse JWT: %w", err) + } + + // Look up the signing key by kid + if len(tok.Headers) == 0 { + return nil, fmt.Errorf("iap: JWT has no headers") + } + kid := tok.Headers[0].KeyID + if kid == "" { + return nil, fmt.Errorf("iap: JWT has no kid") + } + + key, err := a.jwksCache.GetKey(kid) + if err != nil { + return nil, fmt.Errorf("iap: JWKS key lookup failed for kid %q: %w", kid, err) + } + + // Verify signature and extract claims + var claims iapClaims + if err := tok.Claims(key, &claims); err != nil { + return nil, fmt.Errorf("iap: JWT signature verification failed: %w", err) + } + + // Validate standard claims + expectedIssuer := a.resolveIssuer() + now := time.Now() + + if err := a.validateClaims(&claims, expectedIssuer, now); err != nil { + return nil, err + } + + // Strip IdP prefix and build ProxyUserInfo + return &ProxyUserInfo{ + Subject: stripIAPPrefix(claims.Subject), + Email: strings.ToLower(stripIAPPrefix(claims.Email)), + DisplayName: "", // IAP does not provide display name + Domain: claims.HD, + }, nil +} + +// iapClaims are the JWT claims from an IAP assertion. +type iapClaims struct { + Issuer string `json:"iss"` + Subject string `json:"sub"` + Audience jwt.Audience `json:"aud"` + IssuedAt *jwt.NumericDate `json:"iat"` + Expiry *jwt.NumericDate `json:"exp"` + Email string `json:"email"` + HD string `json:"hd,omitempty"` // hosted domain +} + +func (a *IAPAuthenticator) validateClaims(claims *iapClaims, expectedIssuer string, now time.Time) error { + // Issuer + if claims.Issuer != expectedIssuer { + return fmt.Errorf("iap: invalid issuer %q, expected %q", claims.Issuer, expectedIssuer) + } + + // Audience (mandatory binding) + if !claims.Audience.Contains(a.Audience) { + return fmt.Errorf("iap: audience mismatch: got %v, expected %q", claims.Audience, a.Audience) + } + + // Expiry + if claims.Expiry == nil { + return fmt.Errorf("iap: missing exp claim") + } + if now.After(claims.Expiry.Time().Add(iapClockSkew)) { + return fmt.Errorf("iap: token expired at %v", claims.Expiry.Time()) + } + + // Issued-at (with skew: reject if iat is too far in the future) + if claims.IssuedAt != nil { + if claims.IssuedAt.Time().After(now.Add(iapClockSkew)) { + return fmt.Errorf("iap: token issued in the future: iat=%v", claims.IssuedAt.Time()) + } + } + + // Subject and email must be present + if claims.Subject == "" { + return fmt.Errorf("iap: missing sub claim") + } + if claims.Email == "" { + return fmt.Errorf("iap: missing email claim") + } + + return nil +} + +func (a *IAPAuthenticator) resolveIssuer() string { + if a.Issuer != "" { + return a.Issuer + } + return DefaultIAPIssuer +} + +func (a *IAPAuthenticator) resolveJWKSURL() string { + if a.JWKSURL != "" { + return a.JWKSURL + } + return DefaultIAPJWKSURL +} + +// defaultJWKSHTTPClient is used for JWKS fetches when no custom client is provided. +// It has a reasonable timeout to prevent hanging on unresponsive endpoints. +var defaultJWKSHTTPClient = &http.Client{Timeout: 10 * time.Second} + +func (a *IAPAuthenticator) resolveHTTPClient() *http.Client { + if a.HTTPClient != nil { + return a.HTTPClient + } + return defaultJWKSHTTPClient +} + +func (a *IAPAuthenticator) init() { + a.jwksCache = &jwksCache{ + url: a.resolveJWKSURL(), + client: a.resolveHTTPClient(), + } +} + +// stripIAPPrefix removes the "accounts.google.com:" prefix from IAP claims. +func stripIAPPrefix(s string) string { + return strings.TrimPrefix(s, iapIdPPrefix) +} + +// ---- JWKS Cache ---- + +// jwksCache manages a cached set of JWKS keys with lazy fetch, periodic refresh, +// and on-miss refresh for unknown key IDs. +type jwksCache struct { + url string + client *http.Client + + mu sync.RWMutex + keys map[string]jose.JSONWebKey // kid -> key + lastFetched time.Time // last successful fetch + lastAttempted time.Time // last fetch attempt (success or failure), for stampede prevention + refreshing bool // true while a refresh is in-flight +} + +// GetKey returns the public key for the given kid. If the kid is not found +// in the cache, a refresh is triggered. If the JWKS endpoint is temporarily +// unavailable, the last-good keys are served. +func (c *jwksCache) GetKey(kid string) (interface{}, error) { + // Try cached key first + c.mu.RLock() + if c.keys != nil { + if k, ok := c.keys[kid]; ok { + needsRefresh := time.Since(c.lastFetched) > jwksRefreshInterval + c.mu.RUnlock() + // Proactive background refresh (non-blocking) + if needsRefresh { + go c.refresh() + } + return k.Key, nil + } + } + c.mu.RUnlock() + + // Kid not found — refresh and retry + if err := c.refresh(); err != nil { + // If we have stale keys but not this kid, it's a genuine miss + c.mu.RLock() + hasKeys := len(c.keys) > 0 + c.mu.RUnlock() + if !hasKeys { + return nil, fmt.Errorf("jwks fetch failed and no cached keys: %w", err) + } + // Stale keys but kid still not found after failed refresh + return nil, fmt.Errorf("unknown kid %q (jwks refresh failed: %v)", kid, err) + } + + // Check again after refresh + c.mu.RLock() + defer c.mu.RUnlock() + if k, ok := c.keys[kid]; ok { + return k.Key, nil + } + return nil, fmt.Errorf("unknown kid %q after JWKS refresh", kid) +} + +// jwksDebounceInterval is the minimum time between refresh attempts (success or failure) +// to prevent stampedes during JWKS endpoint outages. +const jwksDebounceInterval = 5 * time.Second + +// refresh fetches the JWKS from the endpoint and updates the cache. +// On transient failure, the last-good keys are preserved. +// Concurrent calls are coalesced: if a refresh is already in-flight, subsequent +// callers return immediately (nil error) and rely on cached keys. +func (c *jwksCache) refresh() error { + c.mu.Lock() + + // Debounce: skip if a refresh was attempted (success OR failure) very recently. + if time.Since(c.lastAttempted) < jwksDebounceInterval { + c.mu.Unlock() + return nil + } + + // Prevent concurrent in-flight refreshes. + if c.refreshing { + c.mu.Unlock() + return nil + } + c.refreshing = true + c.lastAttempted = time.Now() + c.mu.Unlock() + + defer func() { + c.mu.Lock() + c.refreshing = false + c.mu.Unlock() + }() + + // All network I/O and response processing happens with no lock held. + resp, err := c.client.Get(c.url) + if err != nil { + slog.Warn("jwks fetch failed, serving last-good keys", "url", c.url, "error", err) + return err + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + body, _ := io.ReadAll(io.LimitReader(resp.Body, 512)) + slog.Warn("jwks fetch non-200", "url", c.url, "status", resp.StatusCode, "body", string(body)) + return fmt.Errorf("jwks fetch returned %d", resp.StatusCode) + } + + body, err := io.ReadAll(io.LimitReader(resp.Body, 1<<20)) // 1MB limit + if err != nil { + return fmt.Errorf("jwks read body: %w", err) + } + + var jwks jose.JSONWebKeySet + if err := json.Unmarshal(body, &jwks); err != nil { + return fmt.Errorf("jwks parse: %w", err) + } + + newKeys := make(map[string]jose.JSONWebKey, len(jwks.Keys)) + for _, k := range jwks.Keys { + if k.KeyID != "" { + newKeys[k.KeyID] = k + } + } + + // Re-acquire lock only to swap the cached keys. + c.mu.Lock() + c.keys = newKeys + c.lastFetched = time.Now() + c.mu.Unlock() + + slog.Debug("jwks cache refreshed", "url", c.url, "keyCount", len(newKeys)) + return nil +} diff --git a/pkg/hub/proxyauth_test.go b/pkg/hub/proxyauth_test.go new file mode 100644 index 000000000..3da01c4c5 --- /dev/null +++ b/pkg/hub/proxyauth_test.go @@ -0,0 +1,606 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package hub + +import ( + "crypto/ecdsa" + "crypto/elliptic" + "crypto/rand" + "encoding/json" + "net/http" + "net/http/httptest" + "testing" + "time" + + "github.com/go-jose/go-jose/v4" + "github.com/go-jose/go-jose/v4/jwt" +) + +// testKeyPair holds a self-generated ES256 key pair for testing. +type testKeyPair struct { + privateKey *ecdsa.PrivateKey + kid string +} + +func newTestKeyPair(t *testing.T, kid string) *testKeyPair { + t.Helper() + key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + if err != nil { + t.Fatalf("failed to generate ES256 key: %v", err) + } + return &testKeyPair{privateKey: key, kid: kid} +} + +// jwksJSON returns the JWKS JSON containing the public key. +func (kp *testKeyPair) jwksJSON(t *testing.T) []byte { + t.Helper() + jwk := jose.JSONWebKey{ + Key: &kp.privateKey.PublicKey, + KeyID: kp.kid, + Algorithm: string(jose.ES256), + Use: "sig", + } + jwks := jose.JSONWebKeySet{Keys: []jose.JSONWebKey{jwk}} + data, err := json.Marshal(jwks) + if err != nil { + t.Fatalf("failed to marshal JWKS: %v", err) + } + return data +} + +// signJWT creates a signed JWT compact serialization. +func (kp *testKeyPair) signJWT(t *testing.T, claims interface{}) string { + t.Helper() + signerKey := jose.SigningKey{Algorithm: jose.ES256, Key: kp.privateKey} + opts := (&jose.SignerOptions{}).WithType("JWT").WithHeader("kid", kp.kid) + signer, err := jose.NewSigner(signerKey, opts) + if err != nil { + t.Fatalf("failed to create signer: %v", err) + } + raw, err := jwt.Signed(signer).Claims(claims).Serialize() + if err != nil { + t.Fatalf("failed to sign JWT: %v", err) + } + return raw +} + +// startJWKSServer starts a test HTTP server serving the given JWKS JSON. +func startJWKSServer(t *testing.T, jwksData []byte) *httptest.Server { + t.Helper() + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + w.Write(jwksData) + })) + t.Cleanup(srv.Close) + return srv +} + +func makeTestClaims(sub, email, iss, aud string, iat, exp time.Time) map[string]interface{} { + claims := map[string]interface{}{ + "iss": iss, + "sub": sub, + "aud": aud, + "email": email, + "iat": iat.Unix(), + "exp": exp.Unix(), + } + return claims +} + +func TestIAPAuthenticator_ValidAssertion(t *testing.T) { + kp := newTestKeyPair(t, "test-key-1") + jwksSrv := startJWKSServer(t, kp.jwksJSON(t)) + + now := time.Now() + claims := makeTestClaims( + "accounts.google.com:12345", + "accounts.google.com:user@example.com", + "https://cloud.google.com/iap", + "/projects/123/global/backendServices/456", + now.Add(-1*time.Minute), + now.Add(5*time.Minute), + ) + assertion := kp.signJWT(t, claims) + + auth := &IAPAuthenticator{ + Audience: "/projects/123/global/backendServices/456", + JWKSURL: jwksSrv.URL, + } + + req := httptest.NewRequest("GET", "/", nil) + req.Header.Set(IAPAssertionHeader, assertion) + + info, err := auth.Authenticate(req) + if err != nil { + t.Fatalf("expected no error, got: %v", err) + } + if info == nil { + t.Fatal("expected ProxyUserInfo, got nil") + } + if info.Subject != "12345" { + t.Errorf("expected subject '12345', got %q", info.Subject) + } + if info.Email != "user@example.com" { + t.Errorf("expected email 'user@example.com', got %q", info.Email) + } +} + +func TestIAPAuthenticator_MissingHeader(t *testing.T) { + auth := &IAPAuthenticator{ + Audience: "/projects/123/global/backendServices/456", + } + + req := httptest.NewRequest("GET", "/", nil) + // No assertion header set + + info, err := auth.Authenticate(req) + if err != nil { + t.Fatalf("expected nil error for missing header, got: %v", err) + } + if info != nil { + t.Fatal("expected nil info for missing header") + } +} + +func TestIAPAuthenticator_BadSignature(t *testing.T) { + kp1 := newTestKeyPair(t, "test-key-1") + kp2 := newTestKeyPair(t, "test-key-1") // different key, same kid + + // JWKS has kp2's public key + jwksSrv := startJWKSServer(t, kp2.jwksJSON(t)) + + now := time.Now() + claims := makeTestClaims( + "accounts.google.com:12345", + "accounts.google.com:user@example.com", + "https://cloud.google.com/iap", + "/projects/123/global/backendServices/456", + now.Add(-1*time.Minute), + now.Add(5*time.Minute), + ) + // Sign with kp1's private key + assertion := kp1.signJWT(t, claims) + + auth := &IAPAuthenticator{ + Audience: "/projects/123/global/backendServices/456", + JWKSURL: jwksSrv.URL, + } + + req := httptest.NewRequest("GET", "/", nil) + req.Header.Set(IAPAssertionHeader, assertion) + + info, err := auth.Authenticate(req) + if err == nil { + t.Fatal("expected error for bad signature, got nil") + } + if info != nil { + t.Fatal("expected nil info for bad signature") + } +} + +func TestIAPAuthenticator_WrongAudience(t *testing.T) { + kp := newTestKeyPair(t, "test-key-1") + jwksSrv := startJWKSServer(t, kp.jwksJSON(t)) + + now := time.Now() + claims := makeTestClaims( + "accounts.google.com:12345", + "accounts.google.com:user@example.com", + "https://cloud.google.com/iap", + "/projects/WRONG/global/backendServices/WRONG", + now.Add(-1*time.Minute), + now.Add(5*time.Minute), + ) + assertion := kp.signJWT(t, claims) + + auth := &IAPAuthenticator{ + Audience: "/projects/123/global/backendServices/456", + JWKSURL: jwksSrv.URL, + } + + req := httptest.NewRequest("GET", "/", nil) + req.Header.Set(IAPAssertionHeader, assertion) + + info, err := auth.Authenticate(req) + if err == nil { + t.Fatal("expected error for wrong audience, got nil") + } + if info != nil { + t.Fatal("expected nil info for wrong audience") + } + t.Logf("expected error: %v", err) +} + +func TestIAPAuthenticator_WrongIssuer(t *testing.T) { + kp := newTestKeyPair(t, "test-key-1") + jwksSrv := startJWKSServer(t, kp.jwksJSON(t)) + + now := time.Now() + claims := makeTestClaims( + "accounts.google.com:12345", + "accounts.google.com:user@example.com", + "https://evil.example.com/iap", + "/projects/123/global/backendServices/456", + now.Add(-1*time.Minute), + now.Add(5*time.Minute), + ) + assertion := kp.signJWT(t, claims) + + auth := &IAPAuthenticator{ + Audience: "/projects/123/global/backendServices/456", + JWKSURL: jwksSrv.URL, + } + + req := httptest.NewRequest("GET", "/", nil) + req.Header.Set(IAPAssertionHeader, assertion) + + info, err := auth.Authenticate(req) + if err == nil { + t.Fatal("expected error for wrong issuer, got nil") + } + if info != nil { + t.Fatal("expected nil info for wrong issuer") + } + t.Logf("expected error: %v", err) +} + +func TestIAPAuthenticator_ExpiredToken(t *testing.T) { + kp := newTestKeyPair(t, "test-key-1") + jwksSrv := startJWKSServer(t, kp.jwksJSON(t)) + + now := time.Now() + claims := makeTestClaims( + "accounts.google.com:12345", + "accounts.google.com:user@example.com", + "https://cloud.google.com/iap", + "/projects/123/global/backendServices/456", + now.Add(-10*time.Minute), + now.Add(-5*time.Minute), // expired 5 minutes ago (well past 30s skew) + ) + assertion := kp.signJWT(t, claims) + + auth := &IAPAuthenticator{ + Audience: "/projects/123/global/backendServices/456", + JWKSURL: jwksSrv.URL, + } + + req := httptest.NewRequest("GET", "/", nil) + req.Header.Set(IAPAssertionHeader, assertion) + + info, err := auth.Authenticate(req) + if err == nil { + t.Fatal("expected error for expired token, got nil") + } + if info != nil { + t.Fatal("expected nil info for expired token") + } + t.Logf("expected error: %v", err) +} + +func TestIAPAuthenticator_CustomIssuer(t *testing.T) { + kp := newTestKeyPair(t, "test-key-1") + jwksSrv := startJWKSServer(t, kp.jwksJSON(t)) + + now := time.Now() + customIssuer := "https://test.example.com/iap" + claims := makeTestClaims( + "accounts.google.com:12345", + "accounts.google.com:user@test.com", + customIssuer, + "/projects/123/global/backendServices/456", + now.Add(-1*time.Minute), + now.Add(5*time.Minute), + ) + assertion := kp.signJWT(t, claims) + + auth := &IAPAuthenticator{ + Audience: "/projects/123/global/backendServices/456", + Issuer: customIssuer, + JWKSURL: jwksSrv.URL, + } + + req := httptest.NewRequest("GET", "/", nil) + req.Header.Set(IAPAssertionHeader, assertion) + + info, err := auth.Authenticate(req) + if err != nil { + t.Fatalf("expected no error with custom issuer, got: %v", err) + } + if info == nil { + t.Fatal("expected ProxyUserInfo, got nil") + } + if info.Email != "user@test.com" { + t.Errorf("expected email 'user@test.com', got %q", info.Email) + } +} + +func TestIAPAuthenticator_UnknownKidTriggersRefresh(t *testing.T) { + kp1 := newTestKeyPair(t, "old-key") + kp2 := newTestKeyPair(t, "new-key") + + // Start JWKS server initially with only old key + var currentJWKS []byte + currentJWKS = kp1.jwksJSON(t) + + jwksSrv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + w.Write(currentJWKS) + })) + t.Cleanup(jwksSrv.Close) + + auth := &IAPAuthenticator{ + Audience: "/projects/123/global/backendServices/456", + JWKSURL: jwksSrv.URL, + } + + // First request with old key works + now := time.Now() + claims1 := makeTestClaims( + "accounts.google.com:12345", + "accounts.google.com:user@example.com", + "https://cloud.google.com/iap", + "/projects/123/global/backendServices/456", + now.Add(-1*time.Minute), + now.Add(5*time.Minute), + ) + assertion1 := kp1.signJWT(t, claims1) + req1 := httptest.NewRequest("GET", "/", nil) + req1.Header.Set(IAPAssertionHeader, assertion1) + info1, err := auth.Authenticate(req1) + if err != nil { + t.Fatalf("first request failed: %v", err) + } + if info1 == nil { + t.Fatal("first request returned nil info") + } + + // Now "rotate" keys — JWKS server returns both keys + bothKeys := jose.JSONWebKeySet{ + Keys: []jose.JSONWebKey{ + {Key: &kp1.privateKey.PublicKey, KeyID: kp1.kid, Algorithm: string(jose.ES256), Use: "sig"}, + {Key: &kp2.privateKey.PublicKey, KeyID: kp2.kid, Algorithm: string(jose.ES256), Use: "sig"}, + }, + } + bothData, _ := json.Marshal(bothKeys) + currentJWKS = bothData + + // Reset the cache's fetch times to force refresh on unknown kid + auth.initOnce.Do(func() {}) // ensure init ran + auth.jwksCache.mu.Lock() + auth.jwksCache.lastFetched = time.Time{} // force proactive refresh + auth.jwksCache.lastAttempted = time.Time{} // clear debounce window + auth.jwksCache.mu.Unlock() + + // Second request with new key — should trigger JWKS refresh and succeed + claims2 := makeTestClaims( + "accounts.google.com:67890", + "accounts.google.com:user2@example.com", + "https://cloud.google.com/iap", + "/projects/123/global/backendServices/456", + now.Add(-1*time.Minute), + now.Add(5*time.Minute), + ) + assertion2 := kp2.signJWT(t, claims2) + req2 := httptest.NewRequest("GET", "/", nil) + req2.Header.Set(IAPAssertionHeader, assertion2) + info2, err := auth.Authenticate(req2) + if err != nil { + t.Fatalf("second request (new kid) failed: %v", err) + } + if info2 == nil { + t.Fatal("second request returned nil info") + } + if info2.Subject != "67890" { + t.Errorf("expected subject '67890', got %q", info2.Subject) + } +} + +func TestIAPAuthenticator_StripPrefix(t *testing.T) { + tests := []struct { + input string + expected string + }{ + {"accounts.google.com:12345", "12345"}, + {"accounts.google.com:user@example.com", "user@example.com"}, + {"12345", "12345"}, // no prefix + {"user@example.com", "user@example.com"}, // no prefix + {"", ""}, + } + for _, tt := range tests { + got := stripIAPPrefix(tt.input) + if got != tt.expected { + t.Errorf("stripIAPPrefix(%q) = %q, want %q", tt.input, got, tt.expected) + } + } +} + +func TestIAPAuthenticator_EmailLowercased(t *testing.T) { + kp := newTestKeyPair(t, "test-key-1") + jwksSrv := startJWKSServer(t, kp.jwksJSON(t)) + + now := time.Now() + claims := makeTestClaims( + "accounts.google.com:12345", + "accounts.google.com:User@EXAMPLE.COM", + "https://cloud.google.com/iap", + "/projects/123/global/backendServices/456", + now.Add(-1*time.Minute), + now.Add(5*time.Minute), + ) + assertion := kp.signJWT(t, claims) + + auth := &IAPAuthenticator{ + Audience: "/projects/123/global/backendServices/456", + JWKSURL: jwksSrv.URL, + } + + req := httptest.NewRequest("GET", "/", nil) + req.Header.Set(IAPAssertionHeader, assertion) + + info, err := auth.Authenticate(req) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if info.Email != "user@example.com" { + t.Errorf("expected lowercased email 'user@example.com', got %q", info.Email) + } +} + +func TestIAPAuthenticator_HDClaim(t *testing.T) { + kp := newTestKeyPair(t, "test-key-1") + jwksSrv := startJWKSServer(t, kp.jwksJSON(t)) + + now := time.Now() + claims := map[string]interface{}{ + "iss": "https://cloud.google.com/iap", + "sub": "accounts.google.com:12345", + "aud": "/projects/123/global/backendServices/456", + "email": "accounts.google.com:user@example.com", + "hd": "example.com", + "iat": now.Add(-1 * time.Minute).Unix(), + "exp": now.Add(5 * time.Minute).Unix(), + } + assertion := kp.signJWT(t, claims) + + auth := &IAPAuthenticator{ + Audience: "/projects/123/global/backendServices/456", + JWKSURL: jwksSrv.URL, + } + + req := httptest.NewRequest("GET", "/", nil) + req.Header.Set(IAPAssertionHeader, assertion) + + info, err := auth.Authenticate(req) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if info.Domain != "example.com" { + t.Errorf("expected domain 'example.com', got %q", info.Domain) + } +} + +func TestIAPAuthenticator_Name(t *testing.T) { + auth := &IAPAuthenticator{} + if auth.Name() != "iap" { + t.Errorf("expected Name()='iap', got %q", auth.Name()) + } +} + +func TestJWKSCache_TransientFailure(t *testing.T) { + kp := newTestKeyPair(t, "test-key-1") + + // Start a failing server + failCount := 0 + jwksSrv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + failCount++ + if failCount <= 1 { + // First call succeeds + w.Header().Set("Content-Type", "application/json") + w.Write(kp.jwksJSON(t)) + } else { + // Subsequent calls fail + w.WriteHeader(http.StatusInternalServerError) + } + })) + t.Cleanup(jwksSrv.Close) + + cache := &jwksCache{url: jwksSrv.URL, client: http.DefaultClient} + + // First fetch succeeds + key, err := cache.GetKey(kp.kid) + if err != nil { + t.Fatalf("first GetKey failed: %v", err) + } + if key == nil { + t.Fatal("first GetKey returned nil key") + } + + // Force refresh by clearing lastFetched and lastAttempted + cache.mu.Lock() + cache.lastFetched = time.Time{} + cache.lastAttempted = time.Time{} + cache.mu.Unlock() + + // Second fetch with same kid still works (returns cached key even though refresh fails) + key2, err := cache.GetKey(kp.kid) + if err != nil { + t.Fatalf("second GetKey failed: %v", err) + } + if key2 == nil { + t.Fatal("second GetKey returned nil key") + } +} + +func TestJWKSCache_StampedePreventionDuringOutage(t *testing.T) { + kp := newTestKeyPair(t, "test-key-1") + + fetchCount := 0 + jwksSrv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + fetchCount++ + if fetchCount <= 1 { + // First call succeeds — populate the cache + w.Header().Set("Content-Type", "application/json") + w.Write(kp.jwksJSON(t)) + } else { + // All subsequent calls fail (simulating a persistent outage) + w.WriteHeader(http.StatusInternalServerError) + } + })) + t.Cleanup(jwksSrv.Close) + + cache := &jwksCache{url: jwksSrv.URL, client: jwksSrv.Client()} + + // Populate cache with a successful fetch + key, err := cache.GetKey(kp.kid) + if err != nil { + t.Fatalf("initial GetKey failed: %v", err) + } + if key == nil { + t.Fatal("initial GetKey returned nil key") + } + if fetchCount != 1 { + t.Fatalf("expected 1 fetch after initial GetKey, got %d", fetchCount) + } + + // Reset lastAttempted to allow the next refresh attempt, but keep lastFetched + // old enough that proactive refresh is desired + cache.mu.Lock() + cache.lastFetched = time.Time{} + cache.lastAttempted = time.Time{} + cache.mu.Unlock() + + // Now make multiple GetKey calls for an unknown kid during the outage. + // Each call triggers refresh() (kid miss), but debounce should prevent + // more than one actual fetch within the debounce window. + unknownKid := "unknown-kid" + for i := 0; i < 5; i++ { + _, _ = cache.GetKey(unknownKid) + } + + // Expect exactly 2 fetches total: 1 initial success + 1 failed attempt + // within the debounce window. The remaining 4 calls should be debounced. + if fetchCount != 2 { + t.Errorf("expected 2 total fetches (1 initial + 1 debounced attempt), got %d", fetchCount) + } + + // Verify the cache still serves the last-good key + key2, err := cache.GetKey(kp.kid) + if err != nil { + t.Fatalf("GetKey for cached kid during outage failed: %v", err) + } + if key2 == nil { + t.Fatal("expected last-good key to be served during outage") + } +} diff --git a/pkg/hub/reaper.go b/pkg/hub/reaper.go new file mode 100644 index 000000000..9cb163e5f --- /dev/null +++ b/pkg/hub/reaper.go @@ -0,0 +1,64 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package hub + +import ( + "context" + "log/slog" + "time" +) + +// Staleness thresholds for the broker-affinity reaper (B5-1, design §7.1). +// +// affinityStaleAge: 2× the defaultAffinityFreshness (90s) used by the routing +// layer in broker_routing.go — a broker that hasn't heartbeated in 3 minutes is +// certainly dead and its affinity is safe to clear. +// +// dispatchStuckAge: 3× the dispatchRollingTimeout (90s) from dispatch_wait.go — +// gives the rolling-timeout wait ample time to fail organically before the +// reaper force-transitions the row. +const ( + affinityStaleAge = 2 * defaultAffinityFreshness // 180s + dispatchStuckAge = 3 * dispatchRollingTimeout // 270s + dispatchMaxRetries = 3 +) + +// brokerAffinityReapHandler returns a recurring handler that clears stale broker +// affinity and re-drives (or fails) stuck dispatches. Registered as a singleton +// so at most one replica runs it per tick. +func (s *Server) brokerAffinityReapHandler() func(ctx context.Context) { + return func(ctx context.Context) { + now := time.Now() + + cleared, err := s.store.ReapStaleBrokerAffinity(ctx, now.Add(-affinityStaleAge)) + if err != nil { + slog.Error("Scheduler: broker affinity reap failed", "error", err) + return + } + + requeued, failed, err := s.store.ReapStuckDispatch(ctx, now.Add(-dispatchStuckAge), dispatchMaxRetries) + if err != nil { + slog.Error("Scheduler: stuck dispatch reap failed", "error", err) + return + } + + if cleared > 0 || requeued > 0 || failed > 0 { + slog.Info("Scheduler: broker affinity reap complete", + "affinity_cleared", cleared, + "dispatch_requeued", requeued, + "dispatch_failed", failed) + } + } +} diff --git a/pkg/hub/reconcile.go b/pkg/hub/reconcile.go new file mode 100644 index 000000000..0c6466579 --- /dev/null +++ b/pkg/hub/reconcile.go @@ -0,0 +1,351 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package hub + +import ( + "context" + "encoding/json" + "fmt" + "log/slog" + "time" + + "github.com/GoogleCloudPlatform/scion/pkg/store" + "go.opentelemetry.io/otel/attribute" +) + +// ReconcileBroker is the exported entry point used by the command-bus signal +// handler (B2-4) to drain durable dispatch intent for a broker this node owns. +func (s *Server) ReconcileBroker(ctx context.Context, brokerID string) { + s.reconcileBroker(ctx, brokerID) +} + +// reconcileBroker drains durable dispatch intent for a broker this node owns: +// pending broker_dispatch rows and pending messages, each CAS-claimed so exactly +// one node executes a given item (design §5.3, §2.0.1). It is the durability +// backstop behind BOTH the command-bus NOTIFY signal and reconnect +// (markBrokerOnline) — so a missed signal or a down owner only delays, never +// loses, a command. Idempotent and safe to run concurrently: the store CAS +// (ClaimBrokerDispatch / MarkMessageDispatched) gates double-execution. +// +// Callers must already hold the broker's control-channel socket (markBrokerOnline +// runs on the accepting node; the command bus filters by ownsLocally), since the +// op executors deliver over the local tunnel. +func (s *Server) reconcileBroker(ctx context.Context, brokerID string) { + if s == nil || s.store == nil || brokerID == "" { + return + } + drainStart := time.Now() + defer func() { + if rec := s.dispatchMetrics; rec != nil { + rec.RecordReconcileDrainDuration(ctx, float64(time.Since(drainStart).Milliseconds())) + } + }() + + // 1. Lifecycle / create-time dispatch intents. + dispatches, err := s.store.ListPendingDispatch(ctx, brokerID) + if err != nil { + s.agentLifecycleLog.Error("reconcile: list pending dispatch failed", "brokerID", brokerID, "error", err) + } + for i := range dispatches { + d := dispatches[i] + claimed, err := s.store.ClaimBrokerDispatch(ctx, d.ID, s.instanceID) + if err != nil { + s.agentLifecycleLog.Error("reconcile: claim dispatch failed", "id", d.ID, "error", err) + continue + } + if !claimed { + continue // another node/drain owns this intent (exactly-once) + } + opAttr := attribute.String("op", d.Op) + if rec := s.dispatchMetrics; rec != nil { + rec.IncClaimed(ctx, 1, opAttr) + } + result, execErr := s.execDispatch(ctx, d) + if execErr != nil { + s.agentLifecycleLog.Warn("reconcile: dispatch op failed", "id", d.ID, "op", d.Op, "error", execErr) + if err := s.store.FailBrokerDispatch(ctx, d.ID, execErr.Error()); err != nil { + s.agentLifecycleLog.Error("reconcile: fail dispatch failed", "id", d.ID, "error", err) + } + if rec := s.dispatchMetrics; rec != nil { + rec.IncFailed(ctx, 1, opAttr) + } + if s.events != nil { + s.events.PublishDispatchDone(ctx, d.ID) + } + continue + } + if err := s.store.CompleteBrokerDispatch(ctx, d.ID, result); err != nil { + s.agentLifecycleLog.Error("reconcile: complete dispatch failed", "id", d.ID, "error", err) + } + if rec := s.dispatchMetrics; rec != nil { + rec.IncDone(ctx, 1, opAttr) + latencyMs := float64(time.Since(d.CreatedAt).Milliseconds()) + rec.RecordDispatchLatency(ctx, latencyMs, opAttr) + } + // Emit a slim completion event so originators waiting on + // waitForDispatchDone wake up (design §6.3). + if s.events != nil { + s.events.PublishDispatchDone(ctx, d.ID) + } + } + + // 2. Pending messages destined for agents on this broker. + msgs, err := s.store.ListPendingMessages(ctx, brokerID) + if err != nil { + s.agentLifecycleLog.Error("reconcile: list pending messages failed", "brokerID", brokerID, "error", err) + return + } + for i := range msgs { + m := msgs[i] + dispatched, err := s.store.MarkMessageDispatched(ctx, m.ID) + if err != nil { + s.agentLifecycleLog.Error("reconcile: mark message dispatched failed", "id", m.ID, "error", err) + continue + } + if !dispatched { + continue // another drain already took it (dedupe) + } + if rec := s.dispatchMetrics; rec != nil { + rec.IncMessageDispatched(ctx, 1) + } + if err := s.deliverMsg(ctx, &m); err != nil { + // At-least-once: the row is already marked dispatched; a delivery + // failure is surfaced by the pending-message sweep (B5-2). Phase 3 + // (B3-3) supplies the real local tunnel + failure handling. + s.agentLifecycleLog.Warn("reconcile: message delivery failed", "id", m.ID, "error", err) + } + } +} + +// executeDispatch runs a claimed dispatch intent's op via the LOCAL broker +// tunnel and returns its result JSON. The lifecycle cases (start/stop/restart) +// deserialize args from the dispatch row and call the local dispatcher, which +// delivers over the in-memory control-channel socket. Unknown ops fail cleanly +// (and are retryable). +func (s *Server) executeDispatch(ctx context.Context, d store.BrokerDispatch) (string, error) { + switch d.Op { + case "start": + return s.execDispatchStart(ctx, d) + case "stop": + return s.execDispatchStop(ctx, d) + case "restart": + return s.execDispatchRestart(ctx, d) + case "delete": + return s.execDispatchDelete(ctx, d) + case "check_prompt": + return s.execDispatchCheckPrompt(ctx, d) + case "finalize_env": + return s.execDispatchFinalizeEnv(ctx, d) + case "create": + return s.execDispatchCreate(ctx, d) + default: + return "", fmt.Errorf("broker dispatch op %q not yet wired on this node", d.Op) + } +} + +func (s *Server) execDispatchStart(ctx context.Context, d store.BrokerDispatch) (string, error) { + agent, err := s.resolveDispatchAgent(ctx, d) + if err != nil { + return "", err + } + dispatcher := s.GetDispatcher() + if dispatcher == nil { + return "", fmt.Errorf("no dispatcher available") + } + var task string + if d.Args != "" { + args, err := UnmarshalStartArgs(d.Args) + if err != nil { + return "", fmt.Errorf("unmarshal start args: %w", err) + } + task = args.Task + } + if err := dispatcher.DispatchAgentStart(ctx, agent, task); err != nil { + return "", fmt.Errorf("dispatch start: %w", err) + } + return "", nil +} + +func (s *Server) execDispatchStop(ctx context.Context, d store.BrokerDispatch) (string, error) { + agent, err := s.resolveDispatchAgent(ctx, d) + if err != nil { + return "", err + } + dispatcher := s.GetDispatcher() + if dispatcher == nil { + return "", fmt.Errorf("no dispatcher available") + } + if err := dispatcher.DispatchAgentStop(ctx, agent); err != nil { + return "", fmt.Errorf("dispatch stop: %w", err) + } + return "", nil +} + +func (s *Server) execDispatchRestart(ctx context.Context, d store.BrokerDispatch) (string, error) { + agent, err := s.resolveDispatchAgent(ctx, d) + if err != nil { + return "", err + } + dispatcher := s.GetDispatcher() + if dispatcher == nil { + return "", fmt.Errorf("no dispatcher available") + } + if err := dispatcher.DispatchAgentRestart(ctx, agent); err != nil { + return "", fmt.Errorf("dispatch restart: %w", err) + } + return "", nil +} + +func (s *Server) execDispatchDelete(ctx context.Context, d store.BrokerDispatch) (string, error) { + agent, err := s.resolveDispatchAgent(ctx, d) + if err != nil { + return "", err + } + dispatcher := s.GetDispatcher() + if dispatcher == nil { + return "", fmt.Errorf("no dispatcher available") + } + var deleteFiles, removeBranch, softDelete bool + var deletedAt time.Time + if d.Args != "" { + args, err := UnmarshalDeleteArgs(d.Args) + if err != nil { + return "", fmt.Errorf("unmarshal delete args: %w", err) + } + deleteFiles = args.DeleteFiles + removeBranch = args.RemoveBranch + softDelete = args.SoftDelete + deletedAt = args.DeletedAt + } + if err := dispatcher.DispatchAgentDelete(ctx, agent, deleteFiles, removeBranch, softDelete, deletedAt); err != nil { + return "", fmt.Errorf("dispatch delete: %w", err) + } + return "", nil +} + +func (s *Server) execDispatchCheckPrompt(ctx context.Context, d store.BrokerDispatch) (string, error) { + agent, err := s.resolveDispatchAgent(ctx, d) + if err != nil { + return "", err + } + dispatcher := s.GetDispatcher() + if dispatcher == nil { + return "", fmt.Errorf("no dispatcher available") + } + hasPrompt, err := dispatcher.DispatchCheckAgentPrompt(ctx, agent) + if err != nil { + return "", fmt.Errorf("dispatch check_prompt: %w", err) + } + result, err := json.Marshal(CheckPromptResult{HasPrompt: hasPrompt}) + if err != nil { + return "", fmt.Errorf("marshal check_prompt result: %w", err) + } + return string(result), nil +} + +func (s *Server) execDispatchFinalizeEnv(ctx context.Context, d store.BrokerDispatch) (string, error) { + agent, err := s.resolveDispatchAgent(ctx, d) + if err != nil { + return "", err + } + dispatcher := s.GetDispatcher() + if dispatcher == nil { + return "", fmt.Errorf("no dispatcher available") + } + var env map[string]string + if d.Args != "" { + args, err := UnmarshalFinalizeEnvArgs(d.Args) + if err != nil { + return "", fmt.Errorf("unmarshal finalize_env args: %w", err) + } + env = args.Env + } + if err := dispatcher.DispatchFinalizeEnv(ctx, agent, env); err != nil { + return "", fmt.Errorf("dispatch finalize_env: %w", err) + } + result, err := json.Marshal(FinalizeEnvResult{Success: true}) + if err != nil { + return "", fmt.Errorf("marshal finalize_env result: %w", err) + } + return string(result), nil +} + +func (s *Server) execDispatchCreate(ctx context.Context, d store.BrokerDispatch) (string, error) { + agent, err := s.resolveDispatchAgent(ctx, d) + if err != nil { + return "", err + } + dispatcher := s.GetDispatcher() + if dispatcher == nil { + return "", fmt.Errorf("no dispatcher available") + } + envReqs, err := dispatcher.DispatchAgentCreateWithGather(ctx, agent) + if err != nil { + return "", fmt.Errorf("dispatch create: %w", err) + } + cr := CreateWithGatherResult{EnvRequirements: envReqs} + result, err := json.Marshal(cr) + if err != nil { + return "", fmt.Errorf("marshal create result: %w", err) + } + return string(result), nil +} + +// resolveDispatchAgent loads the agent from the store by slug (used as the +// identifier in the dispatch row's AgentSlug field, matching the runtime +// broker's slug-based addressing). +func (s *Server) resolveDispatchAgent(ctx context.Context, d store.BrokerDispatch) (*store.Agent, error) { + if d.AgentID != "" { + agent, err := s.store.GetAgent(ctx, d.AgentID) + if err != nil { + return nil, fmt.Errorf("resolve agent %s: %w", d.AgentID, err) + } + return agent, nil + } + return nil, fmt.Errorf("dispatch row has no agent ID") +} + +// deliverMessage tunnels a reconciled message to its agent over the LOCAL +// control channel — the same path DispatchAgentMessage uses for a locally- +// connected broker. reconcileBroker has already CAS-marked the message +// dispatched before calling this, so just deliver. +func (s *Server) deliverMessage(ctx context.Context, m *store.Message) error { + if m == nil || m.AgentID == "" { + return fmt.Errorf("message has no agent ID") + } + agent, err := s.store.GetAgent(ctx, m.AgentID) + if err != nil { + return fmt.Errorf("resolve agent %s: %w", m.AgentID, err) + } + if agent.RuntimeBrokerID == "" { + return fmt.Errorf("agent %s has no runtime broker", m.AgentID) + } + dispatcher := s.GetDispatcher() + if dispatcher == nil { + return fmt.Errorf("no dispatcher available for message delivery") + } + return dispatcher.DispatchAgentMessage(ctx, agent, m.Msg, m.Urgent, nil) +} + +// signalDeferredMessage emits a best-effort NOTIFY wakeup so the broker's +// owning node drains the pending message. Called when route() returns +// routeForward or routeUndeliverable (the message row is already durable). +func (s *Server) signalDeferredMessage(ctx context.Context, brokerID, agentID string) { + if s.commandBus != nil { + if err := s.commandBus.SignalBrokerCmd(ctx, brokerID); err != nil { + slog.Warn("failed to signal deferred message", "brokerID", brokerID, "agentID", agentID, "error", err) + } + } +} diff --git a/pkg/hub/reconcile_test.go b/pkg/hub/reconcile_test.go new file mode 100644 index 000000000..0aaaf6a29 --- /dev/null +++ b/pkg/hub/reconcile_test.go @@ -0,0 +1,256 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//go:build !no_sqlite + +package hub + +import ( + "context" + "log/slog" + "sync" + "sync/atomic" + "testing" + "time" + + "github.com/GoogleCloudPlatform/scion/pkg/messages" + "github.com/GoogleCloudPlatform/scion/pkg/store" + "github.com/GoogleCloudPlatform/scion/pkg/store/entadapter" + "github.com/GoogleCloudPlatform/scion/pkg/store/enttest" + "github.com/google/uuid" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +// newReconcileServer builds a minimal Server wired only with what reconcileBroker +// needs, plus overridable executor seams. +func newReconcileServer(st store.Store, exec func(context.Context, store.BrokerDispatch) (string, error), deliver func(context.Context, *store.Message) error) *Server { + return &Server{ + store: st, + instanceID: "hub-" + uuid.NewString()[:8], + agentLifecycleLog: slog.Default(), + execDispatch: exec, + deliverMsg: deliver, + } +} + +func TestReconcileBroker_DrainsDispatchOnce(t *testing.T) { + ctx := context.Background() + cs := entadapter.NewCompositeStore(enttest.NewClient(t)) + var execN int32 + s := newReconcileServer(cs, + func(context.Context, store.BrokerDispatch) (string, error) { + atomic.AddInt32(&execN, 1) + return `{"ok":true}`, nil + }, + func(context.Context, *store.Message) error { return nil }) + + broker := uuid.NewString() + d := &store.BrokerDispatch{ID: uuid.NewString(), BrokerID: broker, Op: "start"} + require.NoError(t, cs.InsertBrokerDispatch(ctx, d)) + + s.reconcileBroker(ctx, broker) + + assert.Equal(t, int32(1), atomic.LoadInt32(&execN), "executor runs once") + pending, err := cs.ListPendingDispatch(ctx, broker) + require.NoError(t, err) + assert.Empty(t, pending, "drained dispatch is no longer pending") +} + +func TestReconcileBroker_ConcurrentDrainsExecuteOnce(t *testing.T) { + ctx := context.Background() + cs := entadapter.NewCompositeStore(enttest.NewClient(t)) + var execN int32 + s := newReconcileServer(cs, + func(context.Context, store.BrokerDispatch) (string, error) { + atomic.AddInt32(&execN, 1) + return "", nil + }, + func(context.Context, *store.Message) error { return nil }) + + broker := uuid.NewString() + require.NoError(t, cs.InsertBrokerDispatch(ctx, &store.BrokerDispatch{ID: uuid.NewString(), BrokerID: broker, Op: "start"})) + + const racers = 6 + var wg sync.WaitGroup + wg.Add(racers) + for i := 0; i < racers; i++ { + go func() { defer wg.Done(); s.reconcileBroker(ctx, broker) }() + } + wg.Wait() + + assert.Equal(t, int32(1), atomic.LoadInt32(&execN), "concurrent drains execute the intent exactly once") +} + +func TestReconcileBroker_FailedOpMarkedFailed(t *testing.T) { + ctx := context.Background() + cs := entadapter.NewCompositeStore(enttest.NewClient(t)) + s := newReconcileServer(cs, + func(context.Context, store.BrokerDispatch) (string, error) { return "", assertErr{} }, + func(context.Context, *store.Message) error { return nil }) + + broker := uuid.NewString() + d := &store.BrokerDispatch{ID: uuid.NewString(), BrokerID: broker, Op: "start"} + require.NoError(t, cs.InsertBrokerDispatch(ctx, d)) + + s.reconcileBroker(ctx, broker) + + pending, err := cs.ListPendingDispatch(ctx, broker) + require.NoError(t, err) + assert.Empty(t, pending, "a failed op leaves no pending row (it is marked failed, not retried in-loop)") +} + +func TestReconcileBroker_DrainsPendingMessageOnce(t *testing.T) { + ctx := context.Background() + client := enttest.NewClient(t) + cs := entadapter.NewCompositeStore(client) + var deliverN int32 + s := newReconcileServer(cs, + func(context.Context, store.BrokerDispatch) (string, error) { return "", nil }, + func(context.Context, *store.Message) error { atomic.AddInt32(&deliverN, 1); return nil }) + + broker := uuid.NewString() + proj := &store.Project{ID: uuid.NewString(), Name: "p", Slug: "p-" + uuid.NewString()[:8], Visibility: store.VisibilityPrivate, OwnerID: uuid.NewString()} + require.NoError(t, cs.CreateProject(ctx, proj)) + agent, err := client.Agent.Create(). + SetSlug("a-" + uuid.NewString()[:8]).SetName("a"). + SetProjectID(uuid.MustParse(proj.ID)).SetRuntimeBrokerID(broker). + Save(ctx) + require.NoError(t, err) + + msg := &store.Message{ID: uuid.NewString(), ProjectID: proj.ID, Sender: "user:x", Recipient: "agent:a", Msg: "hi", AgentID: agent.ID.String()} + require.NoError(t, cs.CreateMessage(ctx, msg)) + + s.reconcileBroker(ctx, broker) + + assert.Equal(t, int32(1), atomic.LoadInt32(&deliverN), "pending message delivered once") + got, err := cs.GetMessage(ctx, msg.ID) + require.NoError(t, err) + assert.Equal(t, store.MessageDispatchDispatched, got.DispatchState) +} + +// TestDeliverMessage_TunnelsViaDispatcher verifies that deliverMessage resolves +// the agent from the store and dispatches via the local AgentDispatcher. +func TestDeliverMessage_TunnelsViaDispatcher(t *testing.T) { + ctx := context.Background() + client := enttest.NewClient(t) + cs := entadapter.NewCompositeStore(client) + + proj := &store.Project{ID: uuid.NewString(), Name: "p", Slug: "p-" + uuid.NewString()[:8], Visibility: store.VisibilityPrivate, OwnerID: uuid.NewString()} + require.NoError(t, cs.CreateProject(ctx, proj)) + + brokerID := uuid.NewString() + agent, err := client.Agent.Create(). + SetSlug("a-" + uuid.NewString()[:8]).SetName("deliver-test"). + SetProjectID(uuid.MustParse(proj.ID)).SetRuntimeBrokerID(brokerID). + Save(ctx) + require.NoError(t, err) + + var dispatched atomic.Int32 + var lastMsg string + fakeDispatcher := &reconcileTestDispatcher{ + onMessage: func(a *store.Agent, msg string) error { + dispatched.Add(1) + lastMsg = msg + return nil + }, + } + + srv := &Server{ + store: cs, + instanceID: "hub-test", + agentLifecycleLog: slog.Default(), + } + srv.SetDispatcher(fakeDispatcher) + srv.deliverMsg = srv.deliverMessage + + m := &store.Message{ + ID: uuid.NewString(), + AgentID: agent.ID.String(), + Msg: "hello from reconcile", + Urgent: true, + } + + err = srv.deliverMsg(ctx, m) + require.NoError(t, err) + assert.Equal(t, int32(1), dispatched.Load(), "message dispatched once") + assert.Equal(t, "hello from reconcile", lastMsg) +} + +// TestDeliverMessage_MissingAgent returns an error when the agent doesn't exist. +func TestDeliverMessage_MissingAgent(t *testing.T) { + ctx := context.Background() + cs := entadapter.NewCompositeStore(enttest.NewClient(t)) + srv := &Server{ + store: cs, + instanceID: "hub-test", + agentLifecycleLog: slog.Default(), + } + srv.deliverMsg = srv.deliverMessage + + m := &store.Message{ID: uuid.NewString(), AgentID: uuid.NewString(), Msg: "test"} + err := srv.deliverMsg(ctx, m) + assert.Error(t, err) + assert.Contains(t, err.Error(), "resolve agent") +} + +// reconcileTestDispatcher is a minimal AgentDispatcher for deliverMessage tests. +type reconcileTestDispatcher struct { + onMessage func(agent *store.Agent, msg string) error +} + +func (d *reconcileTestDispatcher) DispatchAgentCreate(context.Context, *store.Agent) error { + return nil +} +func (d *reconcileTestDispatcher) DispatchAgentProvision(context.Context, *store.Agent) error { + return nil +} +func (d *reconcileTestDispatcher) DispatchAgentStart(context.Context, *store.Agent, string) error { + return nil +} +func (d *reconcileTestDispatcher) DispatchAgentStop(context.Context, *store.Agent) error { return nil } +func (d *reconcileTestDispatcher) DispatchAgentRestart(context.Context, *store.Agent) error { + return nil +} +func (d *reconcileTestDispatcher) DispatchAgentResetAuth(_ context.Context, _ *store.Agent) error { + return nil +} +func (d *reconcileTestDispatcher) DispatchAgentDelete(_ context.Context, _ *store.Agent, _, _, _ bool, _ time.Time) error { + return nil +} +func (d *reconcileTestDispatcher) DispatchAgentMessage(_ context.Context, agent *store.Agent, msg string, _ bool, _ *messages.StructuredMessage) error { + if d.onMessage != nil { + return d.onMessage(agent, msg) + } + return nil +} +func (d *reconcileTestDispatcher) DispatchAgentLogs(context.Context, *store.Agent, int) (string, error) { + return "", nil +} +func (d *reconcileTestDispatcher) DispatchAgentExec(context.Context, *store.Agent, []string, int) (string, int, error) { + return "", 0, nil +} +func (d *reconcileTestDispatcher) DispatchCheckAgentPrompt(context.Context, *store.Agent) (bool, error) { + return false, nil +} +func (d *reconcileTestDispatcher) DispatchAgentCreateWithGather(context.Context, *store.Agent) (*RemoteEnvRequirementsResponse, error) { + return nil, nil +} +func (d *reconcileTestDispatcher) DispatchFinalizeEnv(context.Context, *store.Agent, map[string]string) error { + return nil +} + +type assertErr struct{} + +func (assertErr) Error() string { return "boom" } diff --git a/pkg/hub/resolve_secrets_test.go b/pkg/hub/resolve_secrets_test.go index 1d934fcd7..a4c6eb54f 100644 --- a/pkg/hub/resolve_secrets_test.go +++ b/pkg/hub/resolve_secrets_test.go @@ -31,50 +31,50 @@ func TestResolveSecrets(t *testing.T) { // Create test secrets across multiple scopes userSecret := &store.Secret{ - ID: "s1", + ID: tid("s1"), Key: "API_KEY", EncryptedValue: "user-api-key", SecretType: store.SecretTypeEnvironment, Target: "API_KEY", Scope: store.ScopeUser, - ScopeID: "user-1", + ScopeID: tid("user-1"), } projectSecret := &store.Secret{ - ID: "s2", + ID: tid("s2"), Key: "DB_PASS", EncryptedValue: "project-db-pass", SecretType: store.SecretTypeEnvironment, Target: "DATABASE_PASSWORD", Scope: store.ScopeProject, - ScopeID: "project-1", + ScopeID: tid("project-1"), } // Project-level override of user API_KEY projectOverride := &store.Secret{ - ID: "s3", + ID: tid("s3"), Key: "API_KEY", EncryptedValue: "project-api-key", SecretType: store.SecretTypeEnvironment, Target: "API_KEY", Scope: store.ScopeProject, - ScopeID: "project-1", + ScopeID: tid("project-1"), } fileSecret := &store.Secret{ - ID: "s4", + ID: tid("s4"), Key: "TLS_CERT", EncryptedValue: "cert-data", SecretType: store.SecretTypeFile, Target: "/etc/ssl/cert.pem", Scope: store.ScopeUser, - ScopeID: "user-1", + ScopeID: tid("user-1"), } varSecret := &store.Secret{ - ID: "s5", + ID: tid("s5"), Key: "CONFIG", EncryptedValue: `{"key":"val"}`, SecretType: store.SecretTypeVariable, Target: "config", Scope: store.ScopeUser, - ScopeID: "user-1", + ScopeID: tid("user-1"), } for _, s := range []*store.Secret{userSecret, projectSecret, projectOverride, fileSecret, varSecret} { @@ -90,10 +90,10 @@ func TestResolveSecrets(t *testing.T) { dispatcher.SetSecretBackend(backend) agent := &store.Agent{ - ID: "agent-1", + ID: tid("agent-1"), Name: "test-agent", - OwnerID: "user-1", - ProjectID: "project-1", + OwnerID: tid("user-1"), + ProjectID: tid("project-1"), } resolved, err := dispatcher.resolveSecrets(ctx, agent) @@ -165,31 +165,31 @@ func TestResolveSecrets_WithBackend(t *testing.T) { // Seed secrets directly through the store for _, s := range []*store.Secret{ { - ID: "s1", + ID: tid("s1"), Key: "API_KEY", EncryptedValue: "user-api-key", SecretType: store.SecretTypeEnvironment, Target: "API_KEY", Scope: store.ScopeUser, - ScopeID: "user-1", + ScopeID: tid("user-1"), }, { - ID: "s2", + ID: tid("s2"), Key: "API_KEY", EncryptedValue: "project-api-key", SecretType: store.SecretTypeEnvironment, Target: "API_KEY", Scope: store.ScopeProject, - ScopeID: "project-1", + ScopeID: tid("project-1"), }, { - ID: "s3", + ID: tid("s3"), Key: "DB_PASS", EncryptedValue: "db-password", SecretType: store.SecretTypeEnvironment, Target: "DATABASE_PASSWORD", Scope: store.ScopeProject, - ScopeID: "project-1", + ScopeID: tid("project-1"), }, } { if err := memStore.CreateSecret(ctx, s); err != nil { @@ -204,10 +204,10 @@ func TestResolveSecrets_WithBackend(t *testing.T) { dispatcher.SetSecretBackend(backend) agent := &store.Agent{ - ID: "agent-1", + ID: tid("agent-1"), Name: "test-agent", - OwnerID: "user-1", - ProjectID: "project-1", + OwnerID: tid("user-1"), + ProjectID: tid("project-1"), } resolved, err := dispatcher.resolveSecrets(ctx, agent) @@ -256,7 +256,7 @@ func TestResolveSecrets_NoOwner(t *testing.T) { dispatcher.SetSecretBackend(backend) agent := &store.Agent{ - ID: "agent-1", + ID: tid("agent-1"), Name: "test-agent", } @@ -276,7 +276,7 @@ func TestResolveSecrets_HubScope(t *testing.T) { // Create hub-scoped secrets hubSecret := &store.Secret{ - ID: "sh1", + ID: tid("sh1"), Key: "ORG_API_KEY", EncryptedValue: "hub-org-api-key", SecretType: store.SecretTypeEnvironment, @@ -286,7 +286,7 @@ func TestResolveSecrets_HubScope(t *testing.T) { } // Create a hub secret that will be overridden by user scope hubOverridden := &store.Secret{ - ID: "sh2", + ID: tid("sh2"), Key: "API_KEY", EncryptedValue: "hub-default-api-key", SecretType: store.SecretTypeEnvironment, @@ -296,17 +296,17 @@ func TestResolveSecrets_HubScope(t *testing.T) { } // Create user secret that overrides hub userSecret := &store.Secret{ - ID: "su1", + ID: tid("su1"), Key: "API_KEY", EncryptedValue: "user-personal-api-key", SecretType: store.SecretTypeEnvironment, Target: "API_KEY", Scope: store.ScopeUser, - ScopeID: "user-1", + ScopeID: tid("user-1"), } // Create a hub secret overridden by project scope hubProjectOverridden := &store.Secret{ - ID: "sh3", + ID: tid("sh3"), Key: "DB_PASS", EncryptedValue: "hub-default-db-pass", SecretType: store.SecretTypeEnvironment, @@ -315,13 +315,13 @@ func TestResolveSecrets_HubScope(t *testing.T) { ScopeID: "test-hub-id", } projectSecret := &store.Secret{ - ID: "sg1", + ID: tid("sg1"), Key: "DB_PASS", EncryptedValue: "project-db-pass", SecretType: store.SecretTypeEnvironment, Target: "DB_PASS", Scope: store.ScopeProject, - ScopeID: "project-1", + ScopeID: tid("project-1"), } for _, s := range []*store.Secret{hubSecret, hubOverridden, userSecret, hubProjectOverridden, projectSecret} { @@ -338,8 +338,8 @@ func TestResolveSecrets_HubScope(t *testing.T) { agent := &store.Agent{ ID: "agent-hub-1", Name: "hub-test-agent", - OwnerID: "user-1", - ProjectID: "project-1", + OwnerID: tid("user-1"), + ProjectID: tid("project-1"), } resolved, err := dispatcher.resolveSecrets(ctx, agent) @@ -403,10 +403,10 @@ func TestResolveSecrets_NoBackend(t *testing.T) { dispatcher := NewHTTPAgentDispatcherWithClient(memStore, mockClient, false, slog.Default()) agent := &store.Agent{ - ID: "agent-1", + ID: tid("agent-1"), Name: "test-agent", - OwnerID: "user-1", - ProjectID: "project-1", + OwnerID: tid("user-1"), + ProjectID: tid("project-1"), } resolved, err := dispatcher.resolveSecrets(ctx, agent) diff --git a/pkg/hub/resource_import_handler_test.go b/pkg/hub/resource_import_handler_test.go index dbd0a55b1..98c521d78 100644 --- a/pkg/hub/resource_import_handler_test.go +++ b/pkg/hub/resource_import_handler_test.go @@ -12,6 +12,8 @@ // See the License for the specific language governing permissions and // limitations under the License. +//go:build !no_sqlite + package hub import ( diff --git a/pkg/hub/scheduler.go b/pkg/hub/scheduler.go index 28a02abc8..825ad68c4 100644 --- a/pkg/hub/scheduler.go +++ b/pkg/hub/scheduler.go @@ -122,6 +122,61 @@ func (s *Scheduler) RegisterRecurring(name string, intervalMinutes int, fn func( }) } +// RegisterRecurringSingleton registers a recurring handler that runs on AT MOST +// ONE replica per tick, guarded by a cluster-wide advisory lock keyed by key. +// +// This is the singleton/leader primitive (P3-5) for cluster-wide-once work such +// as the stale-agent sweep, stalled detection, purge, schedule evaluation, and +// the GitHub App health check. In a single-replica or SQLite deployment the +// lock is a no-op that always succeeds, so the handler runs exactly as before. +// +// If the store does not implement store.AdvisoryLocker, the handler runs +// unguarded (correct for a single replica). +func (s *Scheduler) RegisterRecurringSingleton(name string, intervalMinutes int, key store.AdvisoryLockKey, fn func(ctx context.Context)) { + s.RegisterRecurring(name, intervalMinutes, s.singletonGuard(name, key, fn)) +} + +// singletonGuard wraps fn so it only runs while this replica holds the named +// advisory lock. The lock is released as soon as fn returns, so the next tick on +// any replica is free to win it. +// +// A store that does not implement AdvisoryLocker falls open to running fn +// unguarded — correct for a single-replica / SQLite deployment where there is no +// other replica to collide with. +// +// A lock-acquisition error (e.g. a connection timeout to Postgres) does NOT fall +// open: in a multi-replica deployment running unguarded would let two replicas +// execute the same singleton work concurrently. Since we cannot prove we are +// alone when the lock query itself failed, we SKIP this tick and let the next one +// retry. Missing one tick of idempotent maintenance work is safer than running it +// in duplicate. +func (s *Scheduler) singletonGuard(name string, key store.AdvisoryLockKey, fn func(ctx context.Context)) func(ctx context.Context) { + return func(ctx context.Context) { + locker, ok := s.store.(store.AdvisoryLocker) + if !ok { + fn(ctx) + return + } + acquired, release, err := locker.TryAdvisoryLock(ctx, key) + if err != nil { + s.log.Warn("Scheduler: advisory lock acquisition failed; skipping tick to avoid running unguarded across replicas", + "name", name, "error", err) + return + } + if !acquired { + s.log.Debug("Scheduler: singleton handler held by another replica, skipping", + "name", name) + return + } + defer func() { + if rerr := release(); rerr != nil { + s.log.Warn("Scheduler: advisory unlock failed", "name", name, "error", rerr) + } + }() + fn(ctx) + } +} + // Start begins the root ticker loop and runs eligible handlers immediately // on startup (tick 0). The provided context is used as the parent for handler // invocations. Before starting the ticker, persisted one-shot timers are @@ -298,6 +353,27 @@ func (s *Scheduler) fireEvent(ctx context.Context, evt store.ScheduledEvent, was status = store.ScheduledEventExpired } + // Multi-replica dedup (P3-5): several replicas may each recover the same + // pending event from the database on startup and arm a timer for it. Claim + // the event atomically (pending -> status) before running its side effect so + // exactly one replica delivers it. If the store supports claiming and we + // lose the race (already claimed/cancelled), skip silently. Backends without + // the capability fall through to the legacy run-then-mark behavior, which is + // correct for a single replica. + if claimer, ok := s.store.(store.ScheduledEventClaimer); ok { + claimed, err := claimer.ClaimScheduledEvent(ctx, evt.ID, status) + if err != nil { + s.log.Warn("Scheduler: failed to claim scheduled event; skipping to avoid duplicate", + "eventID", evt.ID, "type", evt.EventType, "error", err) + return + } + if !claimed { + s.log.Debug("Scheduler: scheduled event already claimed by another replica, skipping", + "eventID", evt.ID, "type", evt.EventType) + return + } + } + var errMsg string func() { defer func() { diff --git a/pkg/hub/scheduler_test.go b/pkg/hub/scheduler_test.go index 0c3d8caef..796d7488d 100644 --- a/pkg/hub/scheduler_test.go +++ b/pkg/hub/scheduler_test.go @@ -1265,3 +1265,95 @@ func TestDispatchAgentEventHandler_CreatesAgentNoDispatcher(t *testing.T) { t.Error("agent was not created in the store") } } + +// ============================================================================ +// Singleton Guard Tests (advisory-lock leader election) +// ============================================================================ + +// lockerStore is a minimal store that also implements store.AdvisoryLocker so +// the singleton guard's lock-acquisition branches can be exercised in isolation. +type lockerStore struct { + store.Store // embedded; unused methods panic if called + + acquired bool + err error + released *atomic.Int32 +} + +func (l *lockerStore) TryAdvisoryLock(_ context.Context, _ store.AdvisoryLockKey) (bool, func() error, error) { + if l.err != nil { + return false, func() error { return nil }, l.err + } + return l.acquired, func() error { + if l.released != nil { + l.released.Add(1) + } + return nil + }, nil +} + +func (l *lockerStore) TryAdvisoryLockObject(_ context.Context, _ store.AdvisoryLockKey, _ int32) (bool, func() error, error) { + if l.err != nil { + return false, func() error { return nil }, l.err + } + return l.acquired, func() error { + if l.released != nil { + l.released.Add(1) + } + return nil + }, nil +} + +// TestSingletonGuard_SkipsTickOnLockError verifies that a lock-acquisition error +// (e.g. a connection timeout) causes the tick to be SKIPPED rather than running +// the handler unguarded — running unguarded would let multiple replicas execute +// the same singleton work concurrently. +func TestSingletonGuard_SkipsTickOnLockError(t *testing.T) { + s := NewScheduler(&lockerStore{err: fmt.Errorf("connection timeout")}, slog.Default()) + + var ran atomic.Int32 + guarded := s.singletonGuard("test", store.LockSoftDeletePurge, func(_ context.Context) { + ran.Add(1) + }) + guarded(context.Background()) + + if got := ran.Load(); got != 0 { + t.Fatalf("handler ran %d times on lock error; expected 0 (tick must be skipped, not run unguarded)", got) + } +} + +// TestSingletonGuard_RunsWhenAcquired verifies the handler runs and the lock is +// released when acquisition succeeds. +func TestSingletonGuard_RunsWhenAcquired(t *testing.T) { + var released atomic.Int32 + s := NewScheduler(&lockerStore{acquired: true, released: &released}, slog.Default()) + + var ran atomic.Int32 + guarded := s.singletonGuard("test", store.LockSoftDeletePurge, func(_ context.Context) { + ran.Add(1) + }) + guarded(context.Background()) + + if got := ran.Load(); got != 1 { + t.Fatalf("handler ran %d times; expected 1", got) + } + if got := released.Load(); got != 1 { + t.Fatalf("lock released %d times; expected 1", got) + } +} + +// TestSingletonGuard_SkipsWhenHeldByAnother verifies the handler does NOT run +// when another replica holds the lock (acquired=false, no error). +func TestSingletonGuard_SkipsWhenHeldByAnother(t *testing.T) { + s := NewScheduler(&lockerStore{acquired: false}, slog.Default()) + + var ran atomic.Int32 + guarded := s.singletonGuard("test", store.LockSoftDeletePurge, func(_ context.Context) { + ran.Add(1) + }) + guarded(context.Background()) + + if got := ran.Load(); got != 0 { + t.Fatalf("handler ran %d times while lock held by another replica; expected 0", got) + } +} diff --git a/pkg/hub/server.go b/pkg/hub/server.go index aef19366d..86e951a8c 100644 --- a/pkg/hub/server.go +++ b/pkg/hub/server.go @@ -19,6 +19,7 @@ import ( "context" "crypto/rand" "crypto/sha256" + "database/sql" "encoding/base64" "encoding/hex" "encoding/json" @@ -27,6 +28,7 @@ import ( "log/slog" "net" "net/http" + "os" "strings" "sync" "time" @@ -35,10 +37,13 @@ import ( "github.com/GoogleCloudPlatform/scion/pkg/eventbus" "github.com/GoogleCloudPlatform/scion/pkg/hub/githubapp" "github.com/GoogleCloudPlatform/scion/pkg/messages" + "github.com/GoogleCloudPlatform/scion/pkg/observability/dbmetrics" + "github.com/GoogleCloudPlatform/scion/pkg/observability/dispatchmetrics" "github.com/GoogleCloudPlatform/scion/pkg/secret" "github.com/GoogleCloudPlatform/scion/pkg/storage" "github.com/GoogleCloudPlatform/scion/pkg/store" "github.com/GoogleCloudPlatform/scion/pkg/util/logging" + "github.com/google/uuid" "github.com/robfig/cron/v3" ) @@ -74,6 +79,28 @@ type ServerConfig struct { // UserTokenConfig holds configuration for user JWT tokens. // If SigningKey is empty, a random key is generated. UserTokenConfig UserTokenConfig + // SharedSigningSecret is the deployment-wide secret (the same value every + // replica receives via --session-secret / SESSION_SECRET) from which the + // agent and user JWT signing keys are derived deterministically. When set, + // every replica derives identical signing keys regardless of its + // host-derived HubID, so a JWT minted by one replica validates on any + // other replica behind the load balancer. When empty, signing keys fall + // back to per-hub storage in the secret backend / store. + SharedSigningSecret string + // RequireStableSigningKey makes hub startup fail rather than silently + // generate a brand-new signing key when no existing key can be resolved. + // Generating a new key invalidates every token previously issued by this + // hub — agents get crypto verification errors and cannot self-refresh. After + // a restart that changed the hub identity (e.g. a new pod hostname -> new + // HubID) without a SharedSigningSecret, that silently orphans every live + // agent. Enabling this turns that silent outage into a loud fail-fast. + // Operators enabling it must provide a SharedSigningSecret or pre-provision + // the signing keys; otherwise first boot will (correctly) refuse to start. + RequireStableSigningKey bool + // AuthMode is the exclusive human auth mode: "oauth" (default), "proxy", "dev". + AuthMode string + // ProxyAuthenticator is the configured proxy authenticator (when AuthMode == "proxy"). + ProxyAuth ProxyAuthenticator // TrustedProxies is a list of trusted proxy IPs/CIDRs for forwarded headers. TrustedProxies []string // Debug enables verbose debug logging. @@ -137,6 +164,15 @@ type ServerConfig struct { // GCPMintCapGlobal is the maximum total number of minted service accounts across all projects. // Zero means unlimited (default). GCPMintCapGlobal int + // TransportMode is the transport-layer auth mode: "none" (default), "cloudrun_invoker", "iap". + // Controls which transport tokens the hub issues to agents. + TransportMode string + // TransportAudience is the OIDC audience for transport tokens. + // For IAP: the IAP OAuth client ID. For cloudrun_invoker: the hub URL. + TransportAudience string + // TransportMinter mints transport-layer OIDC tokens for agents. + // Nil when TransportMode == "none" or unset. + TransportMinter TransportTokenMinter } // MaintenanceConfig holds configuration for routine maintenance operation executors. @@ -215,6 +251,9 @@ type AgentDispatcher interface { // DispatchAgentRestart restarts an agent on the runtime broker. DispatchAgentRestart(ctx context.Context, agent *store.Agent) error + // DispatchAgentResetAuth injects a fresh token into a running agent without restarting it. + DispatchAgentResetAuth(ctx context.Context, agent *store.Agent) error + // DispatchAgentDelete removes an agent from the runtime broker. // deleteFiles indicates whether to delete workspace files. // removeBranch indicates whether to remove the git branch. @@ -258,7 +297,7 @@ type RuntimeBrokerClient interface { // brokerID is used for HMAC authentication lookup. // task is an optional task string to pass to the agent on start. // projectPath is the local filesystem path to the project on the broker. - // projectSlug is the project slug for hub-managed projects (no local provider path). + // projectSlug is the project slug for hub-native projects (no local provider path). // resolvedEnv contains environment variables resolved from Hub storage (API keys, etc.). // harnessConfig is the harness config name to use for the agent (e.g. "claude", "gemini"). // resolvedSecrets contains type-aware secrets (including file-type) for auth resolution. @@ -279,6 +318,11 @@ type RuntimeBrokerClient interface { // container retains Hub connectivity. RestartAgent(ctx context.Context, brokerID, brokerEndpoint, agentID, projectID string, resolvedEnv map[string]string) error + // ResetAuthAgent injects a fresh auth token into a running agent without restarting it. + // brokerID is used for HMAC authentication lookup. + // projectID scopes the lookup to a specific project (required for uniqueness). + ResetAuthAgent(ctx context.Context, brokerID, brokerEndpoint, agentID, projectID, token string) error + // DeleteAgent deletes an agent from a remote runtime broker. // brokerID is used for HMAC authentication lookup. // projectID scopes the lookup to a specific project (required for uniqueness). @@ -314,10 +358,11 @@ type RuntimeBrokerClient interface { // Returns the command output, exit code, and any error. ExecAgent(ctx context.Context, brokerID, brokerEndpoint, agentID, projectID string, command []string, timeout int) (string, int, error) - // CleanupProject asks a broker to remove its local hub-managed project directory. + // CleanupProject asks a broker to remove its local hub-native project directory. // brokerID is used for HMAC authentication lookup. + // projectID is passed to enable NFS subtree cleanup (keyed by project ID). // 404 responses are tolerated for idempotency. - CleanupProject(ctx context.Context, brokerID, brokerEndpoint, projectSlug string) error + CleanupProject(ctx context.Context, brokerID, brokerEndpoint, projectSlug, projectID string) error } // RemoteCreateAgentRequest is the request body for creating an agent on a remote runtime broker. @@ -362,7 +407,7 @@ type RemoteCreateAgentRequest struct { // Only populated when GatherEnv is true. EnvSources map[string]string `json:"envSources,omitempty"` - // ProjectSlug is the project slug for hub-managed projects. + // ProjectSlug is the project slug for hub-native projects. // When set, the broker creates the workspace at ~/.scion/projects// // instead of the default worktree-based path. ProjectSlug string `json:"projectSlug,omitempty"` @@ -376,6 +421,11 @@ type RemoteCreateAgentRequest struct { // Resolved by the Hub from the project record and passed to the broker // so it can provision host-side directories and inject volume mounts. SharedDirs []api.SharedDir `json:"sharedDirs,omitempty"` + + // WorkspaceMode is the resolved workspace sharing mode for the project + // (e.g. "shared", "per-agent", "worktree-per-agent"). Threaded from the + // Hub so the broker can branch dispatch without re-deriving from labels. + WorkspaceMode string `json:"workspaceMode,omitempty"` } // ResolvedSecret represents a secret resolved by the Hub for projection into an agent container. @@ -411,15 +461,6 @@ type RemoteAgentConfig struct { // If the cached template's hash matches, it can be used without re-downloading. TemplateHash string `json:"templateHash,omitempty"` - // HarnessConfigID is the Hub harness-config ID for hydration on the broker. - // When set, the broker fetches the harness-config from the Hub's storage - // backend rather than requiring it on the broker's local filesystem. - HarnessConfigID string `json:"harnessConfigId,omitempty"` - - // HarnessConfigHash is the content hash of the harness-config for cache - // validation, mirroring TemplateHash. - HarnessConfigHash string `json:"harnessConfigHash,omitempty"` - // GitClone specifies git clone parameters for git-anchored projects. // When set, the runtime broker skips workspace mounting and injects env vars // so sciontool can clone the repo inside the container. @@ -506,21 +547,34 @@ type Server struct { controlChannel *ControlChannelManager // WebSocket control channel for runtime brokers authzService *AuthzService // Authorization service for policy evaluation events EventPublisher // Event publisher for real-time SSE updates + commandBus CommandBus // Inter-node dispatch signal bus (nil-safe; nil = no-op) notificationDispatcher *NotificationDispatcher // Notification dispatcher for agent status events - maintenance *MaintenanceState // Runtime maintenance mode state - hubID string // Unique hub instance ID for secret namespacing - embeddedBrokerID string // Broker ID when running in hub+broker combo mode - scheduler *Scheduler // Unified scheduler for recurring tasks - cleanupOnce sync.Once // Ensures CleanupResources runs only once + // reconcile op executors (seams): default to executeDispatch/deliverMessage; + // Phase 3/4 supply the real local-tunnel ops; tests override for exactly-once. + execDispatch func(ctx context.Context, d store.BrokerDispatch) (string, error) + deliverMsg func(ctx context.Context, m *store.Message) error + maintenance *MaintenanceState // Runtime maintenance mode state + hubID string // Unique hub instance ID for secret namespacing + instanceID string // Unique per-process ID (uuid); affinity key for broker dispatch + embeddedBrokerID string // Broker ID when running in hub+broker combo mode + scheduler *Scheduler // Unified scheduler for recurring tasks + cleanupOnce sync.Once // Ensures CleanupResources runs only once logQueryService *LogQueryService // Cloud Logging query service (nil = disabled) // Telegram link service for code-based account linking (nil = disabled) telegramLinkService *TelegramLinkService + // Discord link service for code-based account linking (nil = disabled) + discordLinkService *DiscordLinkService + // Channel registry for external notification delivery (nil = disabled) channelRegistry *ChannelRegistry + // Transport token minter for agent outbound auth (nil = transport auth disabled) + transportMinter TransportTokenMinter + transportAudience string + // GCP token generator for agent identity (nil = GCP identity disabled) gcpTokenGenerator GCPTokenGenerator @@ -533,6 +587,18 @@ type Server struct { // GCP token metrics tracker (nil = disabled) gcpTokenMetrics *GCPTokenMetrics + // Database connection-pool / notify metrics recorder (P0-5). Defaults to a + // disabled no-op recorder; SetDBMetrics wires a real exporter. Drives the + // connection-pool sampler started in StartBackgroundServices. + dbMetrics dbmetrics.Recorder + + // Broker dispatch metrics recorder (B5-2). Defaults to a disabled no-op + // recorder; SetDispatchMetrics wires a real exporter. + dispatchMetrics dispatchmetrics.Recorder + + // stopPoolSampler stops the DB pool-stats sampling goroutine on shutdown. + stopPoolSampler func() + // Message broker proxy for pub/sub message routing (nil = disabled) messageBrokerProxy *MessageBrokerProxy @@ -558,6 +624,16 @@ type Server struct { githubAppRateLimit *githubapp.RateLimitInfo } +func newInstanceID() string { + if podName := os.Getenv("POD_NAME"); podName != "" { + return podName + "-" + uuid.NewString() + } + return uuid.NewString() +} + +// InstanceID returns the per-process unique identifier for this hub instance. +func (s *Server) InstanceID() string { return s.instanceID } + // New creates a new Hub API server. func New(cfg ServerConfig, s store.Store) (*Server, error) { // Apply defaults for zero-value fields that have meaningful defaults. @@ -574,6 +650,7 @@ func New(cfg ServerConfig, s store.Store) (*Server, error) { events: noopEventPublisher{}, maintenance: NewMaintenanceState(cfg.AdminMode, cfg.MaintenanceMessage), hubID: cfg.HubID, + instanceID: newInstanceID(), // Subsystem loggers agentLifecycleLog: logging.Subsystem("hub.agent-lifecycle"), @@ -604,7 +681,11 @@ func New(cfg ServerConfig, s store.Store) (*Server, error) { // Initialize agent token service agentKey, err := srv.ensureSigningKey(ctx, SecretKeyAgentSigningKey, cfg.AgentTokenConfig.SigningKey) if err != nil { - if isGCPBackend { + // Fail-fast for a GCP backend (production) or when stable keys are + // required. Otherwise a non-fatal error would fall through to + // NewAgentTokenService generating an ephemeral random key, reintroducing + // the silent token-invalidation this guard exists to prevent. + if isGCPBackend || cfg.RequireStableSigningKey { return nil, fmt.Errorf("agent signing key: %w", err) } logSigningKeyFailure("agent", err) @@ -623,7 +704,7 @@ func New(cfg ServerConfig, s store.Store) (*Server, error) { // Initialize user token service userKey, err := srv.ensureSigningKey(ctx, SecretKeyUserSigningKey, cfg.UserTokenConfig.SigningKey) if err != nil { - if isGCPBackend { + if isGCPBackend || cfg.RequireStableSigningKey { return nil, fmt.Errorf("user signing key: %w", err) } logSigningKeyFailure("user", err) @@ -648,6 +729,9 @@ func New(cfg ServerConfig, s store.Store) (*Server, error) { // Initialize Telegram link service srv.telegramLinkService = NewTelegramLinkService() + // Initialize Discord link service + srv.discordLinkService = NewDiscordLinkService() + // Initialize OAuth service if configured if cfg.OAuthConfig.IsConfigured() { srv.oauthService = NewOAuthService(cfg.OAuthConfig) @@ -667,6 +751,10 @@ func New(cfg ServerConfig, s store.Store) (*Server, error) { } // Initialize audit logger (used by broker auth and invite system) + // Default reconcile-drain op executors (Phase 3/4 supply the real local ops). + srv.execDispatch = srv.executeDispatch + srv.deliverMsg = srv.deliverMessage + srv.auditLogger = NewLogAuditLogger("[Hub Audit]", cfg.Debug) // Initialize broker auth service if enabled @@ -676,6 +764,15 @@ func New(cfg ServerConfig, s store.Store) (*Server, error) { slog.Info("Broker HMAC authentication enabled") } + // Store transport token minter if configured + if cfg.TransportMinter != nil { + srv.transportMinter = cfg.TransportMinter + srv.transportAudience = cfg.TransportAudience + slog.Info("Transport token minter configured", + "mode", cfg.TransportMode, + "audience", cfg.TransportAudience) + } + // Initialize control channel manager srv.controlChannel = NewControlChannelManager(ControlChannelConfig{ PingInterval: 30 * time.Second, @@ -685,13 +782,37 @@ func New(cfg ServerConfig, s store.Store) (*Server, error) { RequestTimeout: 120 * time.Second, Debug: cfg.Debug, }, logging.Subsystem("hub.control-channel")) - // Set disconnect callback to mark broker offline when WebSocket drops - srv.controlChannel.SetOnDisconnect(func(brokerID string) { + // Set disconnect callback to mark broker offline when WebSocket drops. + // ReleaseAndMarkBrokerOffline atomically clears affinity AND stamps + // status=offline in a single CAS write — if a concurrent reconnect has + // already claimed the broker with a new session, the compare fails and the + // callback is a no-op. This eliminates the TOCTOU race where a separate + // ReleaseRuntimeBrokerConnection + UpdateRuntimeBrokerHeartbeat allowed + // the offline stamp to clobber a concurrent markBrokerOnline (issue #131). + srv.controlChannel.SetOnDisconnect(func(brokerID, sessionID string) { ctx := context.Background() - slog.Info("Broker disconnected, marking offline", "brokerID", brokerID) - if err := s.UpdateRuntimeBrokerHeartbeat(ctx, brokerID, store.BrokerStatusOffline); err != nil { - slog.Error("Failed to mark broker offline", "brokerID", brokerID, "error", err) + cleared, err := s.ReleaseAndMarkBrokerOffline(ctx, brokerID, srv.instanceID, sessionID) + if err != nil { + slog.Error("Failed to release broker affinity on disconnect", "brokerID", brokerID, "sessionID", sessionID, "error", err) + return + } + if !cleared { + slog.Info("broker reconnected elsewhere; skipping offline stamp", "brokerID", brokerID, "staleSession", sessionID) + return + } + + slog.Info("Broker disconnected, marking offline", "brokerID", brokerID, "sessionID", sessionID) + + // Guard: re-read the broker before updating provider statuses. A + // concurrent markBrokerOnline may have already re-claimed the broker + // between our atomic release+offline and now. If so, skip provider + // updates to avoid clobbering the new session's online providers. + broker, rerr := s.GetRuntimeBroker(ctx, brokerID) + if rerr == nil && broker.ConnectedSessionID != nil && *broker.ConnectedSessionID != "" { + slog.Info("broker re-claimed by new session after release; skipping provider offline stamp", + "brokerID", brokerID, "staleSession", sessionID, "newSession", *broker.ConnectedSessionID) + return } // Update all project provider records for this broker @@ -738,15 +859,21 @@ func New(cfg ServerConfig, s store.Store) (*Server, error) { // Build unified auth configuration srv.authConfig = AuthConfig{ - Mode: "production", - DevAuthEnabled: cfg.DevAuthToken != "", - DevAuthToken: cfg.DevAuthToken, - AgentTokenSvc: srv.agentTokenService, - UserTokenSvc: srv.userTokenService, - UATSvc: srv.uatService, - TrustedProxies: cfg.TrustedProxies, - Debug: cfg.Debug, - Logger: srv.authLog, + Mode: "production", + DevAuthEnabled: cfg.DevAuthToken != "", + DevAuthToken: cfg.DevAuthToken, + AgentTokenSvc: srv.agentTokenService, + UserTokenSvc: srv.userTokenService, + UATSvc: srv.uatService, + TrustedProxies: cfg.TrustedProxies, + ProxyAuthenticator: cfg.ProxyAuth, + AuthMode: cfg.AuthMode, + Debug: cfg.Debug, + Logger: srv.authLog, + } + // Wire the proxy user provisioner (wraps provisionUser with 60s cache) + if cfg.ProxyAuth != nil { + srv.authConfig.ProxyUserProvisioner = MakeProxyUserProvisioner(srv) } // Initialize Cloud Logging query service (optional, gated on GCP project ID) @@ -768,6 +895,18 @@ func New(cfg ServerConfig, s store.Store) (*Server, error) { return srv, nil } +// deriveSharedSigningKey deterministically derives a 32-byte HS256 signing key +// from the deployment's shared signing secret and the logical key name. The key +// name (e.g. "user_signing_key", "agent_signing_key") provides domain +// separation so the user and agent keys differ even though both originate from +// the same shared secret. Every replica configured with the same shared secret +// derives identical keys, which is what lets a JWT minted by one replica be +// validated by another. +func deriveSharedSigningKey(secret, keyName string) []byte { + sum := sha256.Sum256([]byte("scion-hub-signing-key:" + keyName + ":" + secret)) + return sum[:] +} + // ensureSigningKey ensures a signing key exists, loading it if it does // or generating and saving it if it doesn't. // @@ -787,6 +926,36 @@ func (s *Server) ensureSigningKey(ctx context.Context, keyName string, existingK return existingKey, nil } + // When a deployment-wide shared signing secret is configured (the same + // secret every replica receives via --session-secret / SESSION_SECRET), + // derive the signing key deterministically from it. This makes the key + // identical on every replica regardless of the host-derived hub ID, so a + // JWT minted by one replica validates on any other. It mirrors the web + // session cookie store (commit 0515e2a8), whose keys are derived from the + // same shared secret, and is what lets the hub scale horizontally behind a + // load balancer without operators having to pin a matching HubID on each + // replica. Per-host secret-backend storage (below) is bypassed entirely. + if s.config.SharedSigningSecret != "" { + key := deriveSharedSigningKey(s.config.SharedSigningSecret, keyName) + fp := sha256.Sum256(key) + slog.Info("ensureSigningKey: derived from shared signing secret", + "key", keyName, + "source", "shared_secret", + "key_len", len(key), + "sha256_prefix", hex.EncodeToString(fp[:8]), + ) + // Sync the derived key to the secret backend so that external consumers + // (e.g. scion-chat-app) that discover signing keys via label-based + // auto-discovery in GCP Secret Manager can still find them. + encodedKey := base64.StdEncoding.EncodeToString(key) + _, isGCPBackend := s.secretBackend.(*secret.GCPBackend) + if err := s.syncSigningKeyToBackend(ctx, keyName, encodedKey, s.hubID, isGCPBackend); err != nil { + slog.Warn("Failed to sync shared-secret-derived key to secret backend", + "key", keyName, "error", err) + } + return key, nil + } + hubID := s.hubID hasSecretBackend := s.secretBackend != nil _, isGCPBackend := s.secretBackend.(*secret.GCPBackend) @@ -923,7 +1092,29 @@ func (s *Server) ensureSigningKey(ctx context.Context, keyName string, existingK } } - // Not found anywhere, generate a new one + // Not found anywhere — we must generate a new key. Generating a new signing + // key invalidates EVERY token previously issued by this hub: live agents see + // "failed to verify token" crypto errors and, because the self-service + // refresh endpoint authenticates with the (now-invalid) token, cannot + // recover on their own. This is expected on genuine first boot, but after a + // restart that changed the hub identity (e.g. a new pod hostname -> new + // HubID) without a SharedSigningSecret it silently orphans every live agent. + // + // Fail-fast when the operator has opted into stable-key enforcement, and + // otherwise make the token-invalidating event loud (error-level) so it is + // alertable rather than buried in a warning. + if s.config.RequireStableSigningKey { + return nil, fmt.Errorf("refusing to generate a new signing key %q: RequireStableSigningKey is set and no existing key was found "+ + "(generating one would invalidate all live agent/user tokens); provide a SharedSigningSecret or pre-provision the key", keyName) + } + if hasSecretBackend { + slog.Error("ensureSigningKey: no existing signing key found despite a configured secret backend; generating a NEW key — ALL previously issued tokens are now INVALID", + "key", keyName, + "hub_id", hubID, + "hint", "set a SharedSigningSecret (SESSION_SECRET) or pin a stable HubID so signing keys persist across restarts/redeploys", + ) + } + slog.Warn("Signing key not found in any source, generating new key", "key", keyName, "hub_id", hubID) newKey := make([]byte, 32) if _, err := rand.Read(newKey); err != nil { @@ -1044,11 +1235,13 @@ func (s *Server) backupSigningKeyToStore(ctx context.Context, keyName, encodedVa // signingKeySecretID returns a deterministic primary key for a signing key record, // scoped to the hub instance to avoid PK collisions during migration. +// signingKeySecretID derives a stable surrogate primary key for the signing-key +// backup secret. The store keys secrets by the (key, scope, scope_id) triple, so +// the ID is only a surrogate; it is generated deterministically as a UUIDv5 so +// the value is valid for the UUID-typed primary key while remaining stable +// across restarts. func signingKeySecretID(keyName, hubID string) string { - if hubID == "" { - return fmt.Sprintf("hub-%s", keyName) - } - return fmt.Sprintf("hub-%s-%s", hubID, keyName) + return uuid.NewSHA1(uuid.NameSpaceOID, []byte("hub-signing-key:"+hubID+":"+keyName)).String() } // SetDispatcher sets the agent dispatcher for co-located runtime broker operations. @@ -1245,6 +1438,23 @@ func (s *Server) SetMetrics(m MetricsRecorder) { s.metrics = m } +// SetDBMetrics wires the database connection-pool / notify metrics recorder +// (P0-5). When set to an enabled recorder before StartBackgroundServices, the +// hub starts sampling the DB connection pool into the pool gauges. Passing a +// disabled recorder (or never calling this) leaves pool sampling off. +func (s *Server) SetDBMetrics(rec dbmetrics.Recorder) { + s.mu.Lock() + defer s.mu.Unlock() + s.dbMetrics = rec +} + +// SetDispatchMetrics wires the broker-dispatch metrics recorder (B5-2). +func (s *Server) SetDispatchMetrics(rec dispatchmetrics.Recorder) { + s.mu.Lock() + defer s.mu.Unlock() + s.dispatchMetrics = rec +} + // GetMaintenanceState returns the runtime maintenance state. func (s *Server) GetMaintenanceState() *MaintenanceState { return s.maintenance @@ -1278,8 +1488,26 @@ func (s *Server) SetEventPublisher(ep EventPublisher) { s.events = ep } +// SetCommandBus sets the inter-node dispatch signal bus. Nil is safe (treated +// as no-op). Called from the server-foreground init path after backend selection. +func (s *Server) SetCommandBus(cb CommandBus) { + s.mu.Lock() + defer s.mu.Unlock() + s.commandBus = cb + if pgBus, ok := cb.(*PostgresCommandBus); ok { + pgBus.SetOnReconnect(func() { + if rec := s.dispatchMetrics; rec != nil { + rec.IncCmdBusReconnects(context.Background(), 1) + } + }) + } +} + +// CommandBus returns the configured command bus, or nil. +func (s *Server) CommandBus() CommandBus { return s.commandBus } + // StartNotificationDispatcher creates and starts the notification dispatcher -// if a ChannelEventPublisher is available. It uses a lazy getter for the +// if a subscription-capable EventPublisher is available. It uses a lazy getter for the // AgentDispatcher so it works even if SetDispatcher is called later. // Safe to call multiple times; subsequent calls are no-ops. func (s *Server) StartNotificationDispatcher() { @@ -1290,21 +1518,21 @@ func (s *Server) StartNotificationDispatcher() { return // already started } - ep, ok := s.events.(*ChannelEventPublisher) - if !ok { + if _, isNoop := s.events.(noopEventPublisher); isNoop || s.events == nil { slog.Warn("Event publisher does not support subscriptions, notification dispatcher not started") return } - nd := NewNotificationDispatcher(s.store, ep, s.GetDispatcher, logging.Subsystem("hub.notifications")) + nd := NewNotificationDispatcher(s.store, s.events, s.GetDispatcher, logging.Subsystem("hub.notifications")) nd.messageLog = s.dedicatedMessageLog nd.channelRegistry = s.channelRegistry + nd.signalDeferred = s.signalDeferredMessage s.notificationDispatcher = nd s.notificationDispatcher.Start() } // StartMessageBroker creates and starts the message broker proxy if a -// ChannelEventPublisher is available. The broker enables pub/sub message +// subscription-capable EventPublisher is available. The broker enables pub/sub message // routing with topic-based subscriptions and broadcast fan-out. // Safe to call multiple times; subsequent calls are no-ops. func (s *Server) StartMessageBroker(b eventbus.EventBus) { @@ -1315,14 +1543,14 @@ func (s *Server) StartMessageBroker(b eventbus.EventBus) { return // already started } - ep, ok := s.events.(*ChannelEventPublisher) - if !ok { + if _, isNoop := s.events.(noopEventPublisher); isNoop || s.events == nil { slog.Warn("Event publisher does not support subscriptions, message broker proxy not started") return } - proxy := NewMessageBrokerProxy(b, s.store, ep, s.GetDispatcher, logging.Subsystem("hub.broker")) + proxy := NewMessageBrokerProxy(b, s.store, s.events, s.GetDispatcher, logging.Subsystem("hub.broker")) proxy.messageLog = s.dedicatedMessageLog + proxy.SetSignalDeferred(s.signalDeferredMessage) s.messageBrokerProxy = proxy proxy.Start() @@ -1351,7 +1579,9 @@ func (s *Server) CreateAuthenticatedDispatcher() *HTTPAgentDispatcher { // Wrap with hybrid client that prefers control channel var client RuntimeBrokerClient if s.controlChannel != nil { - client = NewHybridBrokerClient(s.controlChannel, httpClient, &hmacBrokerSigner{store: s.store}, s.config.Debug) + hbc := NewHybridBrokerClient(s.controlChannel, httpClient, &hmacBrokerSigner{store: s.store}, s.config.Debug) + hbc.SetAffinityLookup(StoreAffinityLookup(s.store, 0)) + client = hbc } else { client = httpClient } @@ -1395,6 +1625,21 @@ func (s *Server) CreateAuthenticatedDispatcher() *HTTPAgentDispatcher { dispatcher.SetGitHubAppMinter(s) } + // Wire cross-node lifecycle dispatch deps (B4-2) so the dispatcher + // can handle ErrLifecycleDeferred from route-gated Start/Stop/Restart + // by writing durable intent, signaling the owning node, and waiting + // for the terminal phase. In SQLite mode events/commandBus are no-ops, + // and route() always returns routeLocal, so this never triggers. + dispatcher.SetCrossNodeDeps(s.events, s.commandBus) + if s.dispatchMetrics != nil { + dispatcher.SetDispatchMetrics(s.dispatchMetrics) + } + + // Configure transport token minter if available + if s.transportMinter != nil && s.transportAudience != "" { + dispatcher.SetTransportMinter(s.transportMinter, s.transportAudience) + } + return dispatcher } @@ -1583,12 +1828,16 @@ func (s *Server) messageEventHandler() EventHandler { structuredMsg.Plain = payload.Plain structuredMsg.Urgent = payload.Interrupt - if err := dispatcher.DispatchAgentMessage(ctx, agent, payload.Message, payload.Interrupt, structuredMsg); err != nil { + if err := dispatcher.DispatchAgentMessage(ctx, agent, payload.Message, payload.Interrupt, structuredMsg); errors.Is(err, ErrMessageDeferred) { + s.signalDeferredMessage(ctx, agent.RuntimeBrokerID, agent.ID) + slog.Info("Scheduler: message deferred for cross-node delivery", + "eventID", evt.ID, "agent_id", agent.ID, "agentName", agent.Name) + } else if err != nil { return fmt.Errorf("failed to dispatch message to agent %s: %w", agent.Name, err) + } else { + slog.Info("Scheduler: message delivered to agent", + "eventID", evt.ID, "agent_id", agent.ID, "agentName", agent.Name) } - - slog.Info("Scheduler: message delivered to agent", - "eventID", evt.ID, "agent_id", agent.ID, "agentName", agent.Name) return nil } } @@ -1838,14 +2087,21 @@ func (s *Server) StartBackgroundServices(ctx context.Context) { // Initialize and start the scheduler s.scheduler = NewScheduler(s.store, logging.Subsystem("hub.scheduler")) - s.scheduler.RegisterRecurring("agent-heartbeat-timeout", 1, s.agentHeartbeatTimeoutHandler()) - s.scheduler.RegisterRecurring("agent-stalled-detection", 1, s.agentStalledDetectionHandler()) + // Recurring sweeps are cluster-wide-once work: under multi-replica Postgres + // they must run on a single replica per tick (gated by an advisory lock), + // otherwise every replica would publish duplicate offline/stalled events and + // race on the schedule claim. On SQLite the lock is a no-op. See + // CONCURRENCY-AUDIT.md §"Singleton / leader". + s.scheduler.RegisterRecurringSingleton("agent-heartbeat-timeout", 1, store.LockAgentHeartbeatTimeout, s.agentHeartbeatTimeoutHandler()) + s.scheduler.RegisterRecurringSingleton("agent-stalled-detection", 1, store.LockAgentStalledDetection, s.agentStalledDetectionHandler()) if s.config.SoftDeleteRetention > 0 { - s.scheduler.RegisterRecurring("soft-delete-purge", 60, s.purgeHandler()) + s.scheduler.RegisterRecurringSingleton("soft-delete-purge", 60, store.LockSoftDeletePurge, s.purgeHandler()) } s.scheduler.RegisterEventHandler("message", s.messageEventHandler()) s.scheduler.RegisterEventHandler("dispatch_agent", s.dispatchAgentEventHandler()) - s.scheduler.RegisterRecurring("schedule-evaluator", 1, s.evaluateSchedulesHandler()) + s.scheduler.RegisterRecurringSingleton("schedule-evaluator", 1, store.LockScheduleEvaluator, s.evaluateSchedulesHandler()) + s.scheduler.RegisterRecurringSingleton("broker-affinity-reap", 1, store.LockBrokerAffinityReap, s.brokerAffinityReapHandler()) + s.scheduler.RegisterRecurringSingleton("broker-message-sweep", 1, store.LockBrokerMessageSweep, s.brokerMessageSweepHandler()) // Register GitHub App health check if the app is configured s.mu.RLock() @@ -1857,11 +2113,21 @@ func (s *Server) StartBackgroundServices(ctx context.Context) { if ghWebhooksEnabled { interval = 1440 // 24 hours when webhooks are enabled } - s.scheduler.RegisterRecurring("github-app-health-check", interval, s.githubAppHealthCheckHandler()) + s.scheduler.RegisterRecurringSingleton("github-app-health-check", interval, store.LockGitHubAppHealthCheck, s.githubAppHealthCheckHandler()) } s.scheduler.Start(ctx) + // Start the DB connection-pool stats sampler (P3-6 -> P0-5 gauges). It is a + // no-op unless an enabled recorder was wired via SetDBMetrics and the store + // exposes its *sql.DB; this keeps connection-budget saturation observable + // under multi-replica Postgres (see CONNECTION-BUDGET.md). + if rec := s.dbMetrics; rec != nil { + if dbp, ok := s.store.(interface{ DB() *sql.DB }); ok { + s.stopPoolSampler = dbmetrics.StartPoolSampler(ctx, rec, dbp.DB(), 0) + } + } + // Start rate limiter cleanup goroutine (exits when ctx is cancelled). if s.gcpTokenRateLimiter != nil { s.gcpTokenRateLimiter.StartCleanup(ctx) @@ -1935,6 +2201,11 @@ func (s *Server) Shutdown(ctx context.Context) error { s.scheduler.Stop() } + // Stop the DB pool-stats sampler. + if s.stopPoolSampler != nil { + s.stopPoolSampler() + } + // Stop notification dispatcher before closing event publisher if s.notificationDispatcher != nil { s.notificationDispatcher.Stop() @@ -1944,6 +2215,9 @@ func (s *Server) Shutdown(ctx context.Context) error { if s.events != nil { s.events.Close() } + if s.commandBus != nil { + s.commandBus.Close() + } ctx, cancel := context.WithTimeout(ctx, 30*time.Second) defer cancel() @@ -1980,9 +2254,15 @@ func (s *Server) CleanupResources(ctx context.Context) error { if s.telegramLinkService != nil { s.telegramLinkService.Close() } + if s.discordLinkService != nil { + s.discordLinkService.Close() + } if s.events != nil { s.events.Close() } + if s.commandBus != nil { + s.commandBus.Close() + } if s.logQueryService != nil { s.logQueryService.Close() } @@ -2045,9 +2325,6 @@ func (s *Server) registerRoutes() { s.mux.HandleFunc("/api/v1/harness-configs", s.handleHarnessConfigs) s.mux.HandleFunc("/api/v1/harness-configs/", s.handleHarnessConfigByID) - // Unified, kind/scope-generic resource import (templates + harness-configs). - s.mux.HandleFunc("/api/v1/resources/import", s.handleResourcesImport) - s.mux.HandleFunc("/api/v1/users", s.handleUsers) s.mux.HandleFunc("/api/v1/users/", s.handleUserByID) @@ -2072,9 +2349,6 @@ func (s *Server) registerRoutes() { s.mux.HandleFunc("/api/v1/brokers/join", s.handleBrokerJoin) s.mux.HandleFunc("/api/v1/brokers/", s.handleBrokerByIDRoutes) - // Message channel listing - s.mux.HandleFunc("/api/v1/message-channels", s.handleMessageChannels) - // Broker plugin inbound message delivery s.mux.HandleFunc("/api/v1/broker/inbound", s.handleBrokerInbound) @@ -2093,6 +2367,7 @@ func (s *Server) registerRoutes() { s.mux.HandleFunc("/api/v1/admin/invites", s.handleAdminInvites) s.mux.HandleFunc("/api/v1/admin/invites/", s.handleAdminInviteByID) s.mux.HandleFunc("/api/v1/admin/server-config", s.handleAdminServerConfig) + s.mux.HandleFunc("/api/v1/admin/agents/reset-auth-all", s.handleAdminResetAuthAll) s.mux.HandleFunc("/api/v1/admin/gcp-quota", s.handleAdminGCPQuota) // Notification endpoints (user-facing) @@ -2125,6 +2400,11 @@ func (s *Server) registerRoutes() { s.mux.HandleFunc("/api/v1/telegram/link/verify", s.handleTelegramLinkVerify) s.mux.HandleFunc("/api/v1/telegram/link/status", s.handleTelegramLinkStatus) + // Discord account linking endpoints + s.mux.HandleFunc("/api/v1/discord/link", s.handleDiscordLink) + s.mux.HandleFunc("/api/v1/discord/link/verify", s.handleDiscordLinkVerify) + s.mux.HandleFunc("/api/v1/discord/link/status", s.handleDiscordLinkStatus) + // GitHub App webhook and setup callback (unauthenticated — uses webhook signature) s.mux.HandleFunc("/api/v1/webhooks/github", s.handleGitHubWebhook) s.mux.HandleFunc("/github-app/setup", s.handleGitHubAppSetup) @@ -2408,31 +2688,35 @@ func (s *Server) handleRuntimeBrokerConnect(w http.ResponseWriter, r *http.Reque } // Use the broker ID from header - if err := s.controlChannel.HandleUpgrade(w, r, brokerID); err != nil { + sessionID, err := s.controlChannel.HandleUpgrade(w, r, brokerID) + if err != nil { slog.Error("Upgrade failed for broker", "brokerID", brokerID, "error", err) // Error already written by upgrader return } - s.markBrokerOnline(brokerID) + s.markBrokerOnline(brokerID, sessionID) return } // Use authenticated broker identity - if err := s.controlChannel.HandleUpgrade(w, r, broker.ID()); err != nil { + sessionID, err := s.controlChannel.HandleUpgrade(w, r, broker.ID()) + if err != nil { slog.Error("Upgrade failed for broker", "brokerID", broker.ID(), "error", err) // Error already written by upgrader return } - s.markBrokerOnline(broker.ID()) + s.markBrokerOnline(broker.ID(), sessionID) } // markBrokerOnline updates broker and provider statuses to online after a successful WebSocket connection. -func (s *Server) markBrokerOnline(brokerID string) { +// It claims broker affinity for this hub instance + the connection's sessionID, +// which also bumps status->online and refreshes the heartbeat in one CAS write. +func (s *Server) markBrokerOnline(brokerID, sessionID string) { ctx := context.Background() - slog.Info("Broker connected, marking online", "brokerID", brokerID) + slog.Info("Broker connected, marking online", "brokerID", brokerID, "sessionID", sessionID, "instanceID", s.instanceID) - if err := s.store.UpdateRuntimeBrokerHeartbeat(ctx, brokerID, store.BrokerStatusOnline); err != nil { - slog.Error("Failed to mark broker online", "brokerID", brokerID, "error", err) + if err := s.store.ClaimRuntimeBrokerConnection(ctx, brokerID, s.instanceID, sessionID); err != nil { + slog.Error("Failed to claim broker connection", "brokerID", brokerID, "error", err) } providers, err := s.store.GetBrokerProjects(ctx, brokerID) @@ -2457,6 +2741,12 @@ func (s *Server) markBrokerOnline(brokerID string) { brokerName = broker.Name } s.events.PublishBrokerConnected(ctx, brokerID, brokerName, projectIDs) + + // Durability backstop (design §5.3): the moment this node owns the socket, + // drain any durable dispatch intent that accumulated while the broker was + // offline or owned elsewhere. Async so it never blocks the connect path; + // idempotent + CAS-gated so concurrent drains execute each item once. + go s.reconcileBroker(context.Background(), brokerID) } // isWebSocketUpgrade checks if the request is a WebSocket upgrade request. diff --git a/pkg/hub/server_instanceid_test.go b/pkg/hub/server_instanceid_test.go new file mode 100644 index 000000000..987b23e7c --- /dev/null +++ b/pkg/hub/server_instanceid_test.go @@ -0,0 +1,41 @@ +package hub + +import ( + "testing" +) + +func TestNewInstanceID_NonEmpty(t *testing.T) { + id := newInstanceID() + if id == "" { + t.Fatal("newInstanceID() returned empty string") + } +} + +func TestNewInstanceID_Unique(t *testing.T) { + ids := make(map[string]struct{}, 100) + for i := 0; i < 100; i++ { + id := newInstanceID() + if _, exists := ids[id]; exists { + t.Fatalf("duplicate instanceID on call %d: %s", i, id) + } + ids[id] = struct{}{} + } +} + +func TestInstanceID_AccessorMatchesField(t *testing.T) { + s := &Server{instanceID: newInstanceID()} + if s.InstanceID() == "" { + t.Fatal("InstanceID() returned empty string") + } + if s.InstanceID() != s.instanceID { + t.Fatal("InstanceID() does not match instanceID field") + } +} + +func TestInstanceID_TwoServersDistinct(t *testing.T) { + s1 := &Server{instanceID: newInstanceID()} + s2 := &Server{instanceID: newInstanceID()} + if s1.InstanceID() == s2.InstanceID() { + t.Fatalf("two Servers share the same InstanceID: %s", s1.InstanceID()) + } +} diff --git a/pkg/hub/server_test.go b/pkg/hub/server_test.go index 2fc49dad0..63589080e 100644 --- a/pkg/hub/server_test.go +++ b/pkg/hub/server_test.go @@ -28,14 +28,13 @@ import ( smpb "cloud.google.com/go/secretmanager/apiv1/secretmanagerpb" "github.com/GoogleCloudPlatform/scion/pkg/secret" "github.com/GoogleCloudPlatform/scion/pkg/store" - "github.com/GoogleCloudPlatform/scion/pkg/store/sqlite" "google.golang.org/grpc/codes" "google.golang.org/grpc/status" ) func TestServer_PersistentSigningKeys(t *testing.T) { // Create an in-memory SQLite store - s, err := sqlite.New(":memory:") + s, err := newTestStore(":memory:") if err != nil { t.Fatalf("failed to create test store: %v", err) } @@ -88,7 +87,7 @@ func TestServer_PersistentSigningKeys(t *testing.T) { } func TestServer_PersistentSigningKeys_WithHubID(t *testing.T) { - s, err := sqlite.New(":memory:") + s, err := newTestStore(":memory:") if err != nil { t.Fatalf("failed to create test store: %v", err) } @@ -154,7 +153,7 @@ func TestServer_PersistentSigningKeys_WithHubID(t *testing.T) { } func TestServer_SigningKeysExcludedFromResolve(t *testing.T) { - s, err := sqlite.New(":memory:") + s, err := newTestStore(":memory:") if err != nil { t.Fatalf("failed to create test store: %v", err) } @@ -188,7 +187,7 @@ func TestServer_UserTokenSurvivesRestart(t *testing.T) { // Simulate the exact production scenario: sign in, restart server, validate token. // Uses a file-based SQLite DB to match production behavior. dbPath := filepath.Join(t.TempDir(), "test-hub.db") - s, err := sqlite.New(dbPath) + s, err := newTestStore(dbPath) if err != nil { t.Fatalf("failed to create test store: %v", err) } @@ -210,7 +209,7 @@ func TestServer_UserTokenSurvivesRestart(t *testing.T) { } accessToken, _, _, err := srv1.userTokenService.GenerateTokenPair( - "user-1", "test@example.com", "Test User", store.UserRoleAdmin, ClientTypeWeb, + tid("user-1"), "test@example.com", "Test User", store.UserRoleAdmin, ClientTypeWeb, ) if err != nil { t.Fatalf("GenerateTokenPair failed: %v", err) @@ -225,7 +224,7 @@ func TestServer_UserTokenSurvivesRestart(t *testing.T) { // Close the store and reopen from the same file (simulates process restart) s.Close() - s2, err := sqlite.New(dbPath) + s2, err := newTestStore(dbPath) if err != nil { t.Fatalf("failed to reopen test store: %v", err) } @@ -264,7 +263,7 @@ func TestServer_SigningKeyMigration_LegacyHubScopeID(t *testing.T) { // Simulate the pre-hubID-namespacing scenario where keys were stored // with ScopeID="hub". A new server with a real hubID should find them // via the migration fallback. - s, err := sqlite.New(":memory:") + s, err := newTestStore(":memory:") if err != nil { t.Fatalf("failed to create test store: %v", err) } @@ -283,14 +282,14 @@ func TestServer_SigningKeyMigration_LegacyHubScopeID(t *testing.T) { userEncoded := base64.StdEncoding.EncodeToString(legacyUserKey) if err := s.CreateSecret(ctx, &store.Secret{ - ID: "hub-agent_signing_key", Key: SecretKeyAgentSigningKey, + ID: tid("hub-agent_signing_key"), Key: SecretKeyAgentSigningKey, EncryptedValue: agentEncoded, Scope: store.ScopeHub, ScopeID: "hub", Description: "Hub signing key for agent_signing_key", }); err != nil { t.Fatalf("failed to create legacy agent key: %v", err) } if err := s.CreateSecret(ctx, &store.Secret{ - ID: "hub-user_signing_key", Key: SecretKeyUserSigningKey, + ID: tid("hub-user_signing_key"), Key: SecretKeyUserSigningKey, EncryptedValue: userEncoded, Scope: store.ScopeHub, ScopeID: "hub", Description: "Hub signing key for user_signing_key", }); err != nil { @@ -346,7 +345,7 @@ func TestServer_SigningKeyMigration_DeletesLegacyFromBackend(t *testing.T) { // When migrating signing keys from legacy scope IDs, the old secret // should also be deleted from the secret backend to prevent stale secrets // from confusing label-based auto-discovery by external consumers. - s, err := sqlite.New(":memory:") + s, err := newTestStore(":memory:") if err != nil { t.Fatalf("failed to create test store: %v", err) } @@ -367,7 +366,7 @@ func TestServer_SigningKeyMigration_DeletesLegacyFromBackend(t *testing.T) { encoded := base64.StdEncoding.EncodeToString(legacyKey) if err := s.CreateSecret(ctx, &store.Secret{ - ID: "hub-user_signing_key", Key: SecretKeyUserSigningKey, + ID: tid("hub-user_signing_key"), Key: SecretKeyUserSigningKey, EncryptedValue: encoded, Scope: store.ScopeHub, ScopeID: legacyScopeID, Description: "legacy user signing key", }); err != nil { @@ -425,7 +424,7 @@ func TestServer_SigningKeyMigration_DeletesLegacyFromBackend(t *testing.T) { func TestServer_SigningKeyBootstrapWithSecretBackend(t *testing.T) { // Verify that when SecretBackend is set in ServerConfig, signing keys // are loaded through it and synced from SQLite to the backend. - s, err := sqlite.New(":memory:") + s, err := newTestStore(":memory:") if err != nil { t.Fatalf("failed to create test store: %v", err) } @@ -454,7 +453,7 @@ func TestServer_SigningKeyBootstrapWithSecretBackend(t *testing.T) { // Generate a token with this key accessToken, _, _, err := srv1.userTokenService.GenerateTokenPair( - "user-1", "test@example.com", "Test", store.UserRoleAdmin, ClientTypeWeb, + tid("user-1"), "test@example.com", "Test", store.UserRoleAdmin, ClientTypeWeb, ) if err != nil { t.Fatalf("GenerateTokenPair failed: %v", err) @@ -495,7 +494,7 @@ func TestServer_SigningKeyBootstrapWithSecretBackend(t *testing.T) { func TestServer_SigningKeySyncFromStoreToBackend(t *testing.T) { // Verify that keys pre-existing in SQLite are synced to the secret backend // when the backend is newly configured (migration from no-backend to backend). - s, err := sqlite.New(":memory:") + s, err := newTestStore(":memory:") if err != nil { t.Fatalf("failed to create test store: %v", err) } @@ -549,7 +548,7 @@ func TestServer_SigningKeyEmptyValueFromStore(t *testing.T) { // EncryptedValue="" in SQLite (using SecretRef instead). If GCP SM // later becomes unavailable, ensureSigningKey must not silently return // a nil key — it should generate a new one. - s, err := sqlite.New(":memory:") + s, err := newTestStore(":memory:") if err != nil { t.Fatalf("failed to create test store: %v", err) } @@ -562,7 +561,7 @@ func TestServer_SigningKeyEmptyValueFromStore(t *testing.T) { // Insert a signing key row with empty EncryptedValue (as the GCP backend would) ctx := context.Background() emptySecret := &store.Secret{ - ID: "hub-" + hubID + "-" + SecretKeyUserSigningKey, + ID: tid("hub-" + hubID + "-" + SecretKeyUserSigningKey), Key: SecretKeyUserSigningKey, EncryptedValue: "", SecretRef: "gcpsm:projects/test/secrets/test-key", @@ -597,7 +596,7 @@ func TestServer_SigningKeyEmptyValueFromStore(t *testing.T) { // Verify the new key actually works for token operations accessToken, _, _, err := srv.userTokenService.GenerateTokenPair( - "user-1", "test@example.com", "Test", store.UserRoleAdmin, ClientTypeWeb, + tid("user-1"), "test@example.com", "Test", store.UserRoleAdmin, ClientTypeWeb, ) if err != nil { t.Fatalf("GenerateTokenPair failed: %v", err) @@ -614,7 +613,7 @@ func TestServer_SigningKeyEmptyValueFromStore(t *testing.T) { func TestServer_SigningKeyBackupAfterBackendSet(t *testing.T) { // Verify that after persisting a key through the secret backend, // the actual key value remains in SQLite as a backup. - s, err := sqlite.New(":memory:") + s, err := newTestStore(":memory:") if err != nil { t.Fatalf("failed to create test store: %v", err) } @@ -658,7 +657,7 @@ func TestServer_SigningKeyBackupAfterBackendSet(t *testing.T) { } func TestServer_GenerateAgentToken_DevAuthAutoGrantsScopes(t *testing.T) { - s, err := sqlite.New(":memory:") + s, err := newTestStore(":memory:") if err != nil { t.Fatalf("failed to create test store: %v", err) } @@ -680,7 +679,7 @@ func TestServer_GenerateAgentToken_DevAuthAutoGrantsScopes(t *testing.T) { t.Cleanup(func() { srv.Shutdown(context.Background()) }) // Generate token without any additional scopes - token, err := srv.GenerateAgentToken("agent-1", "project-1", nil) + token, err := srv.GenerateAgentToken(tid("agent-1"), tid("project-1"), nil) if err != nil { t.Fatalf("GenerateAgentToken failed: %v", err) } @@ -706,7 +705,7 @@ func TestServer_GenerateAgentToken_DevAuthAutoGrantsScopes(t *testing.T) { } func TestServer_GenerateAgentToken_DevAuthDeduplicatesScopes(t *testing.T) { - s, err := sqlite.New(":memory:") + s, err := newTestStore(":memory:") if err != nil { t.Fatalf("failed to create test store: %v", err) } @@ -728,7 +727,7 @@ func TestServer_GenerateAgentToken_DevAuthDeduplicatesScopes(t *testing.T) { t.Cleanup(func() { srv.Shutdown(context.Background()) }) // Generate token with explicit scopes that overlap with auto-granted ones - token, err := srv.GenerateAgentToken("agent-1", "project-1", nil, + token, err := srv.GenerateAgentToken(tid("agent-1"), tid("project-1"), nil, ScopeAgentCreate, ScopeAgentLifecycle, ScopeProjectSecretRead) if err != nil { t.Fatalf("GenerateAgentToken failed: %v", err) @@ -757,7 +756,7 @@ func TestServer_GenerateAgentToken_DevAuthDeduplicatesScopes(t *testing.T) { } func TestServer_GenerateAgentToken_NoDevAuthDoesNotAutoGrant(t *testing.T) { - s, err := sqlite.New(":memory:") + s, err := newTestStore(":memory:") if err != nil { t.Fatalf("failed to create test store: %v", err) } @@ -778,7 +777,7 @@ func TestServer_GenerateAgentToken_NoDevAuthDoesNotAutoGrant(t *testing.T) { } t.Cleanup(func() { srv.Shutdown(context.Background()) }) - token, err := srv.GenerateAgentToken("agent-1", "project-1", nil) + token, err := srv.GenerateAgentToken(tid("agent-1"), tid("project-1"), nil) if err != nil { t.Fatalf("GenerateAgentToken failed: %v", err) } @@ -830,7 +829,7 @@ func (f *failingSMClient) Close() error { return nil } func TestServer_GCPBackendFailureIsFatal(t *testing.T) { // When GCPBackend is configured but GCP SM is unavailable, hub.New() should // return an error rather than silently generating an ephemeral key. - s, err := sqlite.New(":memory:") + s, err := newTestStore(":memory:") if err != nil { t.Fatalf("failed to create test store: %v", err) } @@ -858,7 +857,7 @@ func TestServer_SigningKeyBackupPreservesSecretRef(t *testing.T) { // Verify that after loading a signing key from the secret backend and // backing it up to SQLite, the SecretRef is preserved so the UI shows // the secret as SM-backed. - s, err := sqlite.New(":memory:") + s, err := newTestStore(":memory:") if err != nil { t.Fatalf("failed to create test store: %v", err) } diff --git a/pkg/hub/signing_key_shared_test.go b/pkg/hub/signing_key_shared_test.go new file mode 100644 index 000000000..af29fa685 --- /dev/null +++ b/pkg/hub/signing_key_shared_test.go @@ -0,0 +1,205 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package hub + +import ( + "bytes" + "context" + "strings" + "testing" +) + +// TestEnsureSigningKey_RequireStableRefusesGeneration verifies that, with +// RequireStableSigningKey set and no existing key resolvable, ensureSigningKey +// fails fast instead of silently minting a new key (which would invalidate every +// live token). This is the regression guard for the hub-restart auth deadlock. +func TestEnsureSigningKey_RequireStableRefusesGeneration(t *testing.T) { + st, err := newTestStore(":memory:") + if err != nil { + t.Fatalf("newTestStore: %v", err) + } + defer st.Close() + + s := &Server{ + hubID: "host-with-no-key", + store: st, + config: ServerConfig{RequireStableSigningKey: true}, + } + + _, err = s.ensureSigningKey(context.Background(), SecretKeyAgentSigningKey, nil) + if err == nil { + t.Fatal("expected ensureSigningKey to refuse generating a new key when RequireStableSigningKey is set") + } + if !strings.Contains(err.Error(), "RequireStableSigningKey") { + t.Fatalf("error should explain the refusal, got: %v", err) + } +} + +// TestEnsureSigningKey_RequireStableAllowsSharedSecret verifies that stable-key +// enforcement still works when the operator supplies a SharedSigningSecret: the +// key is derived deterministically and no generation (or store access) occurs. +func TestEnsureSigningKey_RequireStableAllowsSharedSecret(t *testing.T) { + // Nil store is fine: shared-secret derivation returns before any store access. + s := &Server{ + hubID: "host1", + config: ServerConfig{RequireStableSigningKey: true, SharedSigningSecret: "deployment-secret"}, + } + + key, err := s.ensureSigningKey(context.Background(), SecretKeyAgentSigningKey, nil) + if err != nil { + t.Fatalf("ensureSigningKey with shared secret should succeed under require-stable, got: %v", err) + } + if len(key) != 32 { + t.Fatalf("expected a 32-byte derived key, got %d bytes", len(key)) + } + if !bytes.Equal(key, deriveSharedSigningKey("deployment-secret", SecretKeyAgentSigningKey)) { + t.Fatal("require-stable should derive the same key as deriveSharedSigningKey") + } +} + +// TestEnsureSigningKey_GeneratesWhenNotRequired verifies the default behavior is +// preserved: without RequireStableSigningKey, a missing key is generated and +// persisted rather than erroring. +func TestEnsureSigningKey_GeneratesWhenNotRequired(t *testing.T) { + st, err := newTestStore(":memory:") + if err != nil { + t.Fatalf("newTestStore: %v", err) + } + defer st.Close() + + s := &Server{hubID: "host1", store: st, config: ServerConfig{}} + + key, err := s.ensureSigningKey(context.Background(), SecretKeyAgentSigningKey, nil) + if err != nil { + t.Fatalf("ensureSigningKey should generate a key by default, got: %v", err) + } + if len(key) != 32 { + t.Fatalf("expected a 32-byte generated key, got %d bytes", len(key)) + } + + // The generated key is persisted, so a second resolve returns the same key. + key2, err := s.ensureSigningKey(context.Background(), SecretKeyAgentSigningKey, nil) + if err != nil { + t.Fatalf("second ensureSigningKey: %v", err) + } + if !bytes.Equal(key, key2) { + t.Fatal("a generated key must persist and be returned on subsequent resolves") + } +} + +// TestDeriveSharedSigningKey_Deterministic verifies that the derivation is +// stable for a given (secret, keyName) pair and domain-separated across key +// names and secrets. +func TestDeriveSharedSigningKey_Deterministic(t *testing.T) { + const secret = "shared-deployment-secret" + + userA := deriveSharedSigningKey(secret, SecretKeyUserSigningKey) + userB := deriveSharedSigningKey(secret, SecretKeyUserSigningKey) + if !bytes.Equal(userA, userB) { + t.Fatal("same secret + key name must derive identical keys") + } + if len(userA) != 32 { + t.Fatalf("expected a 32-byte key, got %d bytes", len(userA)) + } + + // Domain separation: user vs agent key must differ. + agent := deriveSharedSigningKey(secret, SecretKeyAgentSigningKey) + if bytes.Equal(userA, agent) { + t.Fatal("user and agent keys derived from the same secret must differ") + } + + // A different secret must produce a different key. + other := deriveSharedSigningKey("a-different-secret", SecretKeyUserSigningKey) + if bytes.Equal(userA, other) { + t.Fatal("different secrets must derive different keys") + } +} + +// TestEnsureSigningKey_SharedSecretReplicaPortable is the regression test for +// the cross-replica "session_expired" login loop: two replicas with DIFFERENT +// host-derived hub IDs but the SAME shared signing secret must resolve +// identical signing keys, so a user JWT minted by one replica validates on the +// other. A replica with a different shared secret must NOT be able to validate +// the token. +func TestEnsureSigningKey_SharedSecretReplicaPortable(t *testing.T) { + const sharedSecret = "the-load-balancer-shared-secret" + ctx := context.Background() + + // Two replicas of one logical hub, distinct hub IDs (sha256(hostname)). + replicaA := &Server{hubID: "ca39430276ee", config: ServerConfig{SharedSigningSecret: sharedSecret}} + replicaB := &Server{hubID: "9662ebe99da4", config: ServerConfig{SharedSigningSecret: sharedSecret}} + + // ensureSigningKey returns before touching the store/secret backend when a + // shared secret is set, so a nil store is fine here. + keyA, err := replicaA.ensureSigningKey(ctx, SecretKeyUserSigningKey, nil) + if err != nil { + t.Fatalf("replicaA ensureSigningKey: %v", err) + } + keyB, err := replicaB.ensureSigningKey(ctx, SecretKeyUserSigningKey, nil) + if err != nil { + t.Fatalf("replicaB ensureSigningKey: %v", err) + } + if !bytes.Equal(keyA, keyB) { + t.Fatal("replicas sharing a signing secret must derive identical keys despite different hub IDs") + } + + // Mint a user token on replica A; it must validate on replica B. + svcA, err := NewUserTokenService(UserTokenConfig{SigningKey: keyA}) + if err != nil { + t.Fatalf("NewUserTokenService A: %v", err) + } + svcB, err := NewUserTokenService(UserTokenConfig{SigningKey: keyB}) + if err != nil { + t.Fatalf("NewUserTokenService B: %v", err) + } + + accessToken, _, _, err := svcA.GenerateTokenPair("uid-1", "user@example.com", "User", "admin", ClientTypeWeb) + if err != nil { + t.Fatalf("GenerateTokenPair: %v", err) + } + if _, err := svcB.ValidateUserToken(accessToken); err != nil { + t.Fatalf("token minted on replica A must validate on replica B, got: %v", err) + } + + // Negative: a replica with a different shared secret cannot validate it. + replicaC := &Server{hubID: "ca39430276ee", config: ServerConfig{SharedSigningSecret: "a-totally-different-secret"}} + keyC, err := replicaC.ensureSigningKey(ctx, SecretKeyUserSigningKey, nil) + if err != nil { + t.Fatalf("replicaC ensureSigningKey: %v", err) + } + svcC, err := NewUserTokenService(UserTokenConfig{SigningKey: keyC}) + if err != nil { + t.Fatalf("NewUserTokenService C: %v", err) + } + if _, err := svcC.ValidateUserToken(accessToken); err == nil { + t.Fatal("token must NOT validate under a different shared secret") + } +} + +// TestEnsureSigningKey_PreConfiguredKeyTakesPrecedence verifies that an +// explicitly supplied key still wins over shared-secret derivation, preserving +// existing behavior for callers that pass a key directly. +func TestEnsureSigningKey_PreConfiguredKeyTakesPrecedence(t *testing.T) { + explicit := bytes.Repeat([]byte{0xAB}, 32) + s := &Server{hubID: "host1", config: ServerConfig{SharedSigningSecret: "ignored-because-explicit-key-given"}} + + got, err := s.ensureSigningKey(context.Background(), SecretKeyUserSigningKey, explicit) + if err != nil { + t.Fatalf("ensureSigningKey: %v", err) + } + if !bytes.Equal(got, explicit) { + t.Fatal("a pre-configured key must take precedence over shared-secret derivation") + } +} diff --git a/pkg/hub/stalled_detection_test.go b/pkg/hub/stalled_detection_test.go index 27c3ef51b..fc51453a4 100644 --- a/pkg/hub/stalled_detection_test.go +++ b/pkg/hub/stalled_detection_test.go @@ -25,13 +25,13 @@ import ( "github.com/GoogleCloudPlatform/scion/pkg/agent/state" "github.com/GoogleCloudPlatform/scion/pkg/api" "github.com/GoogleCloudPlatform/scion/pkg/store" - "github.com/GoogleCloudPlatform/scion/pkg/store/sqlite" + "github.com/GoogleCloudPlatform/scion/pkg/store/entadapter" ) func setupStalledTestServer(t *testing.T) (*Server, store.Store, *trackingEventPublisher) { t.Helper() - s, err := sqlite.New(":memory:") + s, err := newTestStore(":memory:") if err != nil { t.Fatalf("failed to create test store: %v", err) } @@ -92,7 +92,7 @@ func TestAgentStalledDetectionHandler_MarksStalledAgents(t *testing.T) { // Make activity stale but keep heartbeat recent staleActivity := time.Now().Add(-10 * time.Minute) recentHB := time.Now().Add(-30 * time.Second) - db := s.(*sqlite.SQLiteStore).DB() + db := s.(*entadapter.CompositeStore).DB() if _, err := db.ExecContext(ctx, "UPDATE agents SET last_activity_event = ?, last_seen = ? WHERE id = ?", staleActivity, recentHB, agent.ID); err != nil { @@ -238,7 +238,7 @@ func TestAgentStalledDetectionHandler_StalledFromActivityIsPreserved(t *testing. // Make activity stale but keep heartbeat recent staleActivity := time.Now().Add(-10 * time.Minute) recentHB := time.Now().Add(-30 * time.Second) - db := s.(*sqlite.SQLiteStore).DB() + db := s.(*entadapter.CompositeStore).DB() if _, err := db.ExecContext(ctx, "UPDATE agents SET last_activity_event = ?, last_seen = ? WHERE id = ?", staleActivity, recentHB, agent.ID); err != nil { @@ -319,7 +319,7 @@ func TestAgentStalledDetectionHandler_BlockedAgentNotStalled(t *testing.T) { // Make activity stale but keep heartbeat recent (simulates long wait for child agent) staleActivity := time.Now().Add(-10 * time.Minute) recentHB := time.Now().Add(-30 * time.Second) - db := s.(*sqlite.SQLiteStore).DB() + db := s.(*entadapter.CompositeStore).DB() if _, err := db.ExecContext(ctx, "UPDATE agents SET last_activity_event = ?, last_seen = ? WHERE id = ?", staleActivity, recentHB, agent.ID); err != nil { @@ -383,7 +383,7 @@ func TestAgentStalledDetectionHandler_IdleAgentMarkedStalled(t *testing.T) { // Make activity stale but keep heartbeat recent (process alive but stuck at working) staleActivity := time.Now().Add(-10 * time.Minute) recentHB := time.Now().Add(-30 * time.Second) - db := s.(*sqlite.SQLiteStore).DB() + db := s.(*entadapter.CompositeStore).DB() if _, err := db.ExecContext(ctx, "UPDATE agents SET last_activity_event = ?, last_seen = ? WHERE id = ?", staleActivity, recentHB, agent.ID); err != nil { @@ -416,7 +416,7 @@ func TestAgentStalledDetectionHandler_IdleAgentMarkedStalled(t *testing.T) { } func TestNew_DefaultsStalledThresholdWhenZero(t *testing.T) { - s, err := sqlite.New(":memory:") + s, err := newTestStore(":memory:") if err != nil { t.Fatalf("failed to create test store: %v", err) } diff --git a/pkg/hub/sweep.go b/pkg/hub/sweep.go new file mode 100644 index 000000000..dbf0e12bf --- /dev/null +++ b/pkg/hub/sweep.go @@ -0,0 +1,45 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package hub + +import ( + "context" + "time" +) + +const stuckMessageThreshold = 5 * time.Minute + +// brokerMessageSweepHandler returns a handler that counts messages still in +// dispatch_state='pending' beyond the stuck threshold and logs/emits metrics. +// Registered as a RecurringSingleton guarded by LockBrokerMessageSweep (B5-2). +func (s *Server) brokerMessageSweepHandler() func(ctx context.Context) { + return func(ctx context.Context) { + cutoff := time.Now().Add(-stuckMessageThreshold) + count, err := s.store.CountStuckPendingMessages(ctx, cutoff) + if err != nil { + s.agentLifecycleLog.Error("sweep: count stuck pending messages failed", "error", err) + return + } + + if count > 0 { + s.agentLifecycleLog.Warn("sweep: stuck pending messages detected", + "count", count, "threshold", stuckMessageThreshold.String()) + } + + if rec := s.dispatchMetrics; rec != nil { + rec.ObserveMessageStuck(ctx, int64(count)) + } + } +} diff --git a/pkg/hub/template_bootstrap_test.go b/pkg/hub/template_bootstrap_test.go index 241275135..822f58aa4 100644 --- a/pkg/hub/template_bootstrap_test.go +++ b/pkg/hub/template_bootstrap_test.go @@ -32,14 +32,13 @@ import ( "github.com/GoogleCloudPlatform/scion/pkg/secret" "github.com/GoogleCloudPlatform/scion/pkg/store" - "github.com/GoogleCloudPlatform/scion/pkg/store/sqlite" ) // testTemplateBootstrapServer creates a hub Server backed by an in-memory // SQLite store and a mock storage, suitable for template bootstrap tests. func testTemplateBootstrapServer(t *testing.T) (*Server, store.Store, *mockStorage) { t.Helper() - s, err := sqlite.New(":memory:") + s, err := newTestStore(":memory:") if err != nil { if strings.Contains(err.Error(), "sqlite driver not registered") { t.Skip("Skipping: sqlite driver not registered") @@ -133,7 +132,7 @@ func TestBootstrapTemplatesFromDir_ImportsNewAlongsideExisting(t *testing.T) { // Pre-create a template in the store existing := &store.Template{ - ID: "existing-id", + ID: tid("existing-id"), Name: "existing", Slug: "existing", Scope: store.TemplateScopeGlobal, @@ -246,7 +245,7 @@ func TestBootstrapTemplatesFromDir_SkipsUnchangedTemplate(t *testing.T) { func TestBootstrapTemplatesFromDir_NoopWhenNoStorage(t *testing.T) { // Create server without storage - s, err := sqlite.New(":memory:") + s, err := newTestStore(":memory:") if err != nil { if strings.Contains(err.Error(), "sqlite driver not registered") { t.Skip("Skipping: sqlite driver not registered") @@ -767,7 +766,7 @@ func setupWorkspaceProject(t *testing.T, projectName string) (*Server, store.Sto workspaceRoot := t.TempDir() project := &store.Project{ - ID: "project-ws-" + projectName, + ID: tid("project-ws-" + projectName), Name: projectName, Slug: projectName, GitRemote: "https://github.com/test/" + projectName, @@ -776,10 +775,11 @@ func setupWorkspaceProject(t *testing.T, projectName string) (*Server, store.Sto t.Fatalf("failed to create project: %v", err) } - brokerID := "broker-ws-" + projectName + brokerID := tid("broker-ws-" + projectName) broker := &store.RuntimeBroker{ ID: brokerID, Name: "ws-broker", + Slug: "ws-broker", Endpoint: "http://localhost:9090", Status: store.BrokerStatusOnline, } diff --git a/pkg/hub/template_file_handlers.go b/pkg/hub/template_file_handlers.go index 9151b27ad..4749c4be9 100644 --- a/pkg/hub/template_file_handlers.go +++ b/pkg/hub/template_file_handlers.go @@ -305,11 +305,6 @@ func (s *Server) handleTemplateFileWrite(w http.ResponseWriter, r *http.Request, return } - if template.Locked { - Forbidden(w) - return - } - stor := s.GetStorage() if stor == nil { RuntimeError(w, "Storage not configured") @@ -477,11 +472,6 @@ func (s *Server) handleTemplateFileUpload(w http.ResponseWriter, r *http.Request return } - if template.Locked { - Forbidden(w) - return - } - stor := s.GetStorage() if stor == nil { RuntimeError(w, "Storage not configured") @@ -607,11 +597,6 @@ func (s *Server) handleTemplateFileDelete(w http.ResponseWriter, r *http.Request return } - if template.Locked { - Forbidden(w) - return - } - // Find and remove the file from the manifest idx := -1 for i := range template.Files { diff --git a/pkg/hub/template_file_handlers_test.go b/pkg/hub/template_file_handlers_test.go index 81bb77ab5..c0e0ef20c 100644 --- a/pkg/hub/template_file_handlers_test.go +++ b/pkg/hub/template_file_handlers_test.go @@ -20,6 +20,7 @@ import ( "bytes" "context" "encoding/json" + "fmt" "io" "mime/multipart" "net/http" @@ -30,7 +31,6 @@ import ( "github.com/GoogleCloudPlatform/scion/pkg/storage" "github.com/GoogleCloudPlatform/scion/pkg/store" - "github.com/GoogleCloudPlatform/scion/pkg/store/sqlite" ) // contentMockStorage extends mockStorage to also store file content for @@ -88,7 +88,7 @@ func (m *contentMockStorage) Exists(_ context.Context, objectPath string) (bool, // testTemplateFileServer creates a Server with content-aware mock storage. func testTemplateFileServer(t *testing.T) (*Server, store.Store, *contentMockStorage) { t.Helper() - s, err := sqlite.New(":memory:") + s, err := newTestStore(":memory:") if err != nil { if strings.Contains(err.Error(), "sqlite driver not registered") { t.Skip("Skipping: sqlite driver not registered") @@ -120,7 +120,7 @@ func createTestTemplate(t *testing.T, s store.Store, stor *contentMockStorage, f ctx := context.Background() tmpl := &store.Template{ - ID: "tmpl-test-1", + ID: tid("tmpl-test-1"), Name: "test-template", Slug: "test-template", Harness: "claude", @@ -319,33 +319,6 @@ func TestHandleTemplateFileWrite_NewFile(t *testing.T) { } } -func TestHandleTemplateFileWrite_LockedTemplate(t *testing.T) { - srv, s, stor := testTemplateFileServer(t) - ctx := context.Background() - - tmpl := createTestTemplate(t, s, stor, map[string]string{ - "CLAUDE.md": "# Agent", - }) - - // Lock the template - tmpl.Locked = true - if err := s.UpdateTemplate(ctx, tmpl); err != nil { - t.Fatalf("failed to lock template: %v", err) - } - - body := `{"content": "new content"}` - req := httptest.NewRequest(http.MethodPut, "/api/v1/templates/"+tmpl.ID+"/files/CLAUDE.md", - strings.NewReader(body)) - req.Header.Set("Content-Type", "application/json") - req.Header.Set("Authorization", "Bearer "+testDevToken) - w := httptest.NewRecorder() - srv.Handler().ServeHTTP(w, req) - - if w.Code != http.StatusForbidden { - t.Fatalf("expected 403, got %d: %s", w.Code, w.Body.String()) - } -} - func TestHandleTemplateFileWrite_ConflictHash(t *testing.T) { srv, s, stor := testTemplateFileServer(t) @@ -354,7 +327,7 @@ func TestHandleTemplateFileWrite_ConflictHash(t *testing.T) { }) body := `{"content": "new", "expectedHash": "sha256:wronghash"}` - req := httptest.NewRequest(http.MethodPut, "/api/v1/templates/tmpl-test-1/files/CLAUDE.md", + req := httptest.NewRequest(http.MethodPut, fmt.Sprintf("/api/v1/templates/%s/files/CLAUDE.md", tid("tmpl-test-1")), strings.NewReader(body)) req.Header.Set("Content-Type", "application/json") req.Header.Set("Authorization", "Bearer "+testDevToken) @@ -402,29 +375,6 @@ func TestHandleTemplateFileDelete(t *testing.T) { } } -func TestHandleTemplateFileDelete_LockedTemplate(t *testing.T) { - srv, s, stor := testTemplateFileServer(t) - ctx := context.Background() - - tmpl := createTestTemplate(t, s, stor, map[string]string{ - "CLAUDE.md": "# Agent", - }) - - tmpl.Locked = true - if err := s.UpdateTemplate(ctx, tmpl); err != nil { - t.Fatalf("failed to lock template: %v", err) - } - - req := httptest.NewRequest(http.MethodDelete, "/api/v1/templates/"+tmpl.ID+"/files/CLAUDE.md", nil) - req.Header.Set("Authorization", "Bearer "+testDevToken) - w := httptest.NewRecorder() - srv.Handler().ServeHTTP(w, req) - - if w.Code != http.StatusForbidden { - t.Fatalf("expected 403, got %d: %s", w.Code, w.Body.String()) - } -} - func TestHandleTemplateFileDelete_NotFound(t *testing.T) { srv, s, stor := testTemplateFileServer(t) @@ -552,30 +502,6 @@ func TestHandleTemplateFileUpload_MultipleFiles(t *testing.T) { } } -func TestHandleTemplateFileUpload_LockedTemplate(t *testing.T) { - srv, s, stor := testTemplateFileServer(t) - ctx := context.Background() - - tmpl := createTestTemplate(t, s, stor, map[string]string{ - "CLAUDE.md": "# Agent", - }) - - tmpl.Locked = true - if err := s.UpdateTemplate(ctx, tmpl); err != nil { - t.Fatalf("failed to lock template: %v", err) - } - - req := templateMultipartRequest(t, tmpl.ID, map[string][]byte{ - "config.yaml": []byte("key: value"), - }) - w := httptest.NewRecorder() - srv.Handler().ServeHTTP(w, req) - - if w.Code != http.StatusForbidden { - t.Fatalf("expected 403, got %d: %s", w.Code, w.Body.String()) - } -} - func TestHandleTemplateFileUpload_NoFiles(t *testing.T) { srv, s, stor := testTemplateFileServer(t) diff --git a/pkg/hub/template_handlers.go b/pkg/hub/template_handlers.go index 463150d5c..9eb95c6d1 100644 --- a/pkg/hub/template_handlers.go +++ b/pkg/hub/template_handlers.go @@ -405,12 +405,6 @@ func (s *Server) updateTemplateV2(w http.ResponseWriter, r *http.Request, id str return } - // Check if template is locked - if existing.Locked { - ValidationError(w, "template is locked and cannot be modified", nil) - return - } - var template store.Template if err := readJSON(r, &template); err != nil { BadRequest(w, "Invalid request body: "+err.Error()) @@ -421,8 +415,6 @@ func (s *Server) updateTemplateV2(w http.ResponseWriter, r *http.Request, id str template.ID = existing.ID template.Created = existing.Created template.CreatedBy = existing.CreatedBy - template.Locked = existing.Locked - if template.Slug == "" { template.Slug = api.Slugify(template.Name) } @@ -445,12 +437,6 @@ func (s *Server) patchTemplateV2(w http.ResponseWriter, r *http.Request, id stri return } - // Check if template is locked - if existing.Locked { - ValidationError(w, "template is locked and cannot be modified", nil) - return - } - var updates struct { Name string `json:"name,omitempty"` Slug string `json:"slug,omitempty"` @@ -498,7 +484,6 @@ func (s *Server) deleteTemplateV2(w http.ResponseWriter, r *http.Request, id str query := r.URL.Query() deleteFiles := query.Get("deleteFiles") == "true" - force := query.Get("force") == "true" existing, err := s.store.GetTemplate(ctx, id) if err != nil { @@ -506,10 +491,40 @@ func (s *Server) deleteTemplateV2(w http.ResponseWriter, r *http.Request, id str return } - // Check if template is locked - if existing.Locked && !force { - ValidationError(w, "template is locked; use force=true to delete", nil) - return + // Authorize: check source scope for ActionDelete + if existing.Scope == store.TemplateScopeGlobal { + userIdent := GetUserIdentityFromContext(ctx) + if userIdent == nil { + writeError(w, http.StatusUnauthorized, "unauthorized", "Authentication required", nil) + return + } + decision := s.authzService.CheckAccess(ctx, userIdent, Resource{Type: "template"}, ActionDelete) + if !decision.Allowed { + writeError(w, http.StatusForbidden, ErrCodeForbidden, "You do not have permission to delete global resources", nil) + return + } + } else if existing.Scope == store.TemplateScopeProject { + if agentIdent := GetAgentIdentityFromContext(ctx); agentIdent != nil { + if !agentIdent.HasScope(ScopeAgentCreate) { + writeError(w, http.StatusForbidden, ErrCodeForbidden, "Missing required scope", nil) + return + } + if existing.ScopeID != agentIdent.ProjectID() { + writeError(w, http.StatusForbidden, ErrCodeForbidden, "Agents can only manage resources within their own project", nil) + return + } + } else if userIdent := GetUserIdentityFromContext(ctx); userIdent != nil { + decision := s.authzService.CheckAccess(ctx, userIdent, Resource{ + Type: "template", ParentType: "project", ParentID: existing.ScopeID, + }, ActionDelete) + if !decision.Allowed { + writeError(w, http.StatusForbidden, ErrCodeForbidden, "You do not have permission to delete resources in this project", nil) + return + } + } else { + writeError(w, http.StatusUnauthorized, "unauthorized", "Authentication required", nil) + return + } } // If deleteFiles is true and we have storage, delete the files @@ -721,6 +736,46 @@ func (s *Server) handleTemplateClone(w http.ResponseWriter, r *http.Request, id scopeID = req.ProjectID } + // Authorize: check destination scope for ActionCreate + destScope := req.Scope + if destScope == "" { + destScope = store.TemplateScopeProject + } + if destScope == store.TemplateScopeGlobal { + userIdent := GetUserIdentityFromContext(ctx) + if userIdent == nil { + writeError(w, http.StatusUnauthorized, "unauthorized", "Authentication required", nil) + return + } + decision := s.authzService.CheckAccess(ctx, userIdent, Resource{Type: "template"}, ActionCreate) + if !decision.Allowed { + writeError(w, http.StatusForbidden, ErrCodeForbidden, "You do not have permission to create global resources", nil) + return + } + } else if destScope == store.TemplateScopeProject { + if agentIdent := GetAgentIdentityFromContext(ctx); agentIdent != nil { + if !agentIdent.HasScope(ScopeAgentCreate) { + writeError(w, http.StatusForbidden, ErrCodeForbidden, "Missing required scope", nil) + return + } + if scopeID != agentIdent.ProjectID() { + writeError(w, http.StatusForbidden, ErrCodeForbidden, "Agents can only manage resources within their own project", nil) + return + } + } else if userIdent := GetUserIdentityFromContext(ctx); userIdent != nil { + decision := s.authzService.CheckAccess(ctx, userIdent, Resource{ + Type: "template", ParentType: "project", ParentID: scopeID, + }, ActionCreate) + if !decision.Allowed { + writeError(w, http.StatusForbidden, ErrCodeForbidden, "You do not have permission to create resources in this project", nil) + return + } + } else { + writeError(w, http.StatusUnauthorized, "unauthorized", "Authentication required", nil) + return + } + } + // Create new template based on source clone := &store.Template{ ID: api.NewUUID(), @@ -758,24 +813,28 @@ func (s *Server) handleTemplateClone(w http.ResponseWriter, r *http.Request, id // Copy files from source to clone location if stor != nil && len(source.Files) > 0 && source.StoragePath != "" { - clonedFiles := make([]store.TemplateFile, 0, len(source.Files)) for _, file := range source.Files { srcPath := source.StoragePath + "/" + file.Path dstPath := storagePath + "/" + file.Path - - _, err := stor.Copy(ctx, srcPath, dstPath) - if err != nil { - // Log but continue - continue + if _, err := stor.Copy(ctx, srcPath, dstPath); err != nil { + _ = stor.DeletePrefix(ctx, storagePath) + RuntimeError(w, "Failed to copy files: "+err.Error()) + return } - clonedFiles = append(clonedFiles, file) } - clone.Files = clonedFiles + clone.Files = source.Files clone.ContentHash = source.ContentHash clone.Status = store.TemplateStatusActive } if err := s.store.CreateTemplate(ctx, clone); err != nil { + if stor != nil { + _ = stor.DeletePrefix(ctx, storagePath) + } + if strings.Contains(err.Error(), "UNIQUE constraint failed") { + writeError(w, http.StatusConflict, "conflict", "A resource with this slug already exists in the target scope. Choose a different name.", nil) + return + } writeErrorFromErr(w, err, "") return } diff --git a/pkg/hub/teststore_test.go b/pkg/hub/teststore_test.go new file mode 100644 index 000000000..5ffe299f5 --- /dev/null +++ b/pkg/hub/teststore_test.go @@ -0,0 +1,61 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//go:build !no_sqlite + +package hub + +import ( + "context" + "fmt" + "sync/atomic" + + "github.com/GoogleCloudPlatform/scion/pkg/ent/entc" + "github.com/GoogleCloudPlatform/scion/pkg/store" + "github.com/GoogleCloudPlatform/scion/pkg/store/entadapter" +) + +// testStoreSeq generates unique in-memory database names so each call to +// newTestStore(":memory:") gets an isolated database. +var testStoreSeq atomic.Int64 + +// newTestStore opens a fresh Ent-backed store for tests, mirroring the +// production single-database layout (see cmd/server_foreground.go:initStore). +// It is a drop-in replacement for the former sqlite.New: pass ":memory:" for an +// isolated in-memory database or a file path for a persistent one. The returned +// store is already migrated; callers may still invoke Migrate (it is +// idempotent). +func newTestStore(url string) (store.Store, error) { + var dsn string + if url == ":memory:" { + dsn = fmt.Sprintf("file:hubtest%d?mode=memory&cache=shared", testStoreSeq.Add(1)) + } else { + dsn = "file:" + url + "?cache=shared" + } + + // MaxOpenConns must be 1 for SQLite to serialize writes and avoid + // "database is locked" errors under concurrent access (e.g. the parallel + // per-agent writes in stop-all). This mirrors the production pool config in + // cmd/server_foreground.go / pkg/config. + client, err := entc.OpenSQLite(dsn, entc.PoolConfig{MaxOpenConns: 1}) + if err != nil { + return nil, err + } + s := entadapter.NewCompositeStore(client) + if err := s.Migrate(context.Background()); err != nil { + _ = s.Close() + return nil, err + } + return s, nil +} diff --git a/pkg/hub/tid_test.go b/pkg/hub/tid_test.go new file mode 100644 index 000000000..370b3c1d2 --- /dev/null +++ b/pkg/hub/tid_test.go @@ -0,0 +1,28 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package hub + +import ( + "github.com/google/uuid" +) + +// tid deterministically maps a human-readable test identifier (e.g. "user-1") +// to a stable UUID string. The Ent-backed store uses UUID primary keys, so test +// fixtures cannot use arbitrary strings as IDs; wrapping a readable name in tid +// preserves test legibility and cross-reference consistency (tid("user-1") +// always returns the same UUID) while satisfying the UUID requirement. +func tid(name string) string { + return uuid.NewSHA1(uuid.NameSpaceOID, []byte(name)).String() +} diff --git a/pkg/hub/transport_token.go b/pkg/hub/transport_token.go new file mode 100644 index 000000000..0dfce22a8 --- /dev/null +++ b/pkg/hub/transport_token.go @@ -0,0 +1,172 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package hub + +import ( + "context" + "encoding/base64" + "encoding/json" + "fmt" + "strings" + "sync" + "time" + + "google.golang.org/api/iamcredentials/v1" + "google.golang.org/api/option" +) + +// TransportTokenMinter mints Google OIDC ID tokens for the transport layer. +// The hub uses this to issue tokens that let agents traverse platform guards +// (IAP or Cloud Run invoker) on outbound requests. +type TransportTokenMinter interface { + // MintIDToken mints a Google OIDC ID token for the given audience. + // Returns the token string, its expiry time, and any error. + MintIDToken(ctx context.Context, audience string) (token string, expiry time.Time, err error) +} + +// RefreshTokenEntry represents a single token in the generalized refresh response. +// Used in both the refresh endpoint response and internally for dispatch payload construction. +type RefreshTokenEntry struct { + Layer string `json:"layer"` // "app" | "transport" + Type string `json:"type"` // "scion_access" | "scion_refresh" | "google_oidc" + Value string `json:"value"` // the token value + ExpiresIn int `json:"expiresIn"` // seconds until expiry + Audience string `json:"audience,omitempty"` // only for transport tokens +} + +// noopTransportMinter is used when transport auth is disabled (mode == "none"). +// It always returns an error indicating transport auth is not configured. +type noopTransportMinter struct{} + +func (m *noopTransportMinter) MintIDToken(_ context.Context, _ string) (string, time.Time, error) { + return "", time.Time{}, fmt.Errorf("transport auth is disabled (mode=none)") +} + +// gcpTransportMinter mints Google OIDC ID tokens by impersonating a dedicated +// service account via the IAM Credentials API (generateIdToken). +// The hub's runtime SA must hold serviceAccountTokenCreator on the target SA. +type gcpTransportMinter struct { + // serviceAccountEmail is the email of the SA to impersonate. + serviceAccountEmail string + // iamEndpoint overrides the IAM Credentials API endpoint (for testing). + // Empty uses the default Google endpoint. + iamEndpoint string + + // svcOnce guards lazy initialization of the cached IAM credentials service. + svcOnce sync.Once + svc *iamcredentials.Service + svcErr error +} + +// NewGCPTransportMinter creates a new GCP transport token minter. +// serviceAccountEmail is the dedicated platform-auth SA to impersonate. +// iamEndpoint overrides the IAM Credentials API endpoint (empty uses the default). +func NewGCPTransportMinter(serviceAccountEmail, iamEndpoint string) *gcpTransportMinter { + return &gcpTransportMinter{ + serviceAccountEmail: serviceAccountEmail, + iamEndpoint: iamEndpoint, + } +} + +// getOrCreateService lazily creates and caches the IAM credentials service client. +// Uses context.Background() for the long-lived client; per-call ctx is passed to .Do(). +func (m *gcpTransportMinter) getOrCreateService() (*iamcredentials.Service, error) { + m.svcOnce.Do(func() { + var opts []option.ClientOption + if m.iamEndpoint != "" { + opts = append(opts, option.WithEndpoint(m.iamEndpoint), option.WithoutAuthentication()) + } + m.svc, m.svcErr = iamcredentials.NewService(context.Background(), opts...) + }) + return m.svc, m.svcErr +} + +// MintIDToken impersonates the configured SA to mint a Google OIDC ID token +// with the given audience via the IAM Credentials API. +func (m *gcpTransportMinter) MintIDToken(ctx context.Context, audience string) (string, time.Time, error) { + if m.serviceAccountEmail == "" { + return "", time.Time{}, fmt.Errorf("transport minter: service account email not configured") + } + + svc, err := m.getOrCreateService() + if err != nil { + return "", time.Time{}, fmt.Errorf("transport minter: failed to create IAM credentials client: %w", err) + } + + name := fmt.Sprintf("projects/-/serviceAccounts/%s", m.serviceAccountEmail) + req := &iamcredentials.GenerateIdTokenRequest{ + Audience: audience, + IncludeEmail: true, + } + + resp, err := svc.Projects.ServiceAccounts.GenerateIdToken(name, req).Context(ctx).Do() + if err != nil { + return "", time.Time{}, fmt.Errorf("transport minter: generateIdToken failed: %w", err) + } + + if resp.Token == "" { + return "", time.Time{}, fmt.Errorf("transport minter: empty token in response") + } + + // Parse expiry from the JWT token + expiry, err := parseJWTExpiry(resp.Token) + if err != nil { + // Fall back to 1 hour default TTL if we can't parse the expiry + expiry = time.Now().Add(1 * time.Hour) + } + + return resp.Token, expiry, nil +} + +// parseJWTExpiry extracts the expiry time from a JWT without validating the signature. +// This is safe for scheduling purposes since the token will be validated by the platform. +func parseJWTExpiry(tokenString string) (time.Time, error) { + parts := strings.Split(tokenString, ".") + if len(parts) != 3 { + return time.Time{}, fmt.Errorf("invalid JWT format: expected 3 parts, got %d", len(parts)) + } + + payload, err := base64.RawURLEncoding.DecodeString(parts[1]) + if err != nil { + return time.Time{}, fmt.Errorf("failed to decode JWT payload: %w", err) + } + + var claims struct { + Exp int64 `json:"exp"` + } + if err := json.Unmarshal(payload, &claims); err != nil { + return time.Time{}, fmt.Errorf("failed to parse JWT claims: %w", err) + } + + if claims.Exp == 0 { + return time.Time{}, fmt.Errorf("token has no expiry claim") + } + + return time.Unix(claims.Exp, 0), nil +} + +// fakeTransportMinter is a test double for TransportTokenMinter. +// Exported for use in other test packages. +type FakeTransportMinter struct { + Token string + Expiry time.Time + Err error + CallCount int +} + +func (f *FakeTransportMinter) MintIDToken(_ context.Context, _ string) (string, time.Time, error) { + f.CallCount++ + return f.Token, f.Expiry, f.Err +} diff --git a/pkg/hub/transport_token_test.go b/pkg/hub/transport_token_test.go new file mode 100644 index 000000000..003bbbae5 --- /dev/null +++ b/pkg/hub/transport_token_test.go @@ -0,0 +1,188 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package hub + +import ( + "context" + "encoding/base64" + "encoding/json" + "fmt" + "net/http" + "net/http/httptest" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +// makeTestJWTWithExpiry builds a minimal JWT with the given expiry for testing. +func makeTestJWTWithExpiry(exp time.Time) string { + header := base64.RawURLEncoding.EncodeToString([]byte(`{"alg":"RS256","typ":"JWT"}`)) + payload, _ := json.Marshal(map[string]interface{}{"exp": exp.Unix(), "iss": "test"}) + payloadB64 := base64.RawURLEncoding.EncodeToString(payload) + sig := base64.RawURLEncoding.EncodeToString([]byte("fakesig")) + return fmt.Sprintf("%s.%s.%s", header, payloadB64, sig) +} + +func TestNoopTransportMinter_ReturnsError(t *testing.T) { + m := &noopTransportMinter{} + token, expiry, err := m.MintIDToken(context.Background(), "https://example.com") + assert.Empty(t, token) + assert.True(t, expiry.IsZero()) + assert.Error(t, err) + assert.Contains(t, err.Error(), "disabled") +} + +func TestFakeTransportMinter(t *testing.T) { + testToken := makeTestJWTWithExpiry(time.Now().Add(1 * time.Hour)) + testExpiry := time.Now().Add(1 * time.Hour) + + m := &FakeTransportMinter{ + Token: testToken, + Expiry: testExpiry, + } + + token, expiry, err := m.MintIDToken(context.Background(), "https://example.com") + require.NoError(t, err) + assert.Equal(t, testToken, token) + assert.Equal(t, testExpiry, expiry) + assert.Equal(t, 1, m.CallCount) +} + +func TestFakeTransportMinter_Error(t *testing.T) { + m := &FakeTransportMinter{ + Err: fmt.Errorf("test error"), + } + + _, _, err := m.MintIDToken(context.Background(), "https://example.com") + assert.Error(t, err) + assert.Equal(t, "test error", err.Error()) +} + +func TestGCPTransportMinter_MintIDToken(t *testing.T) { + testExpiry := time.Now().Add(1 * time.Hour).Truncate(time.Second) + testToken := makeTestJWTWithExpiry(testExpiry) + testSA := "transport-auth@project.iam.gserviceaccount.com" + + // Fake IAM Credentials API server + iamServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + // Verify the request path matches the expected SA + assert.Contains(t, r.URL.Path, testSA) + assert.Equal(t, "POST", r.Method) + + // Parse request body + var req struct { + Audience string `json:"audience"` + IncludeEmail bool `json:"includeEmail"` + } + err := json.NewDecoder(r.Body).Decode(&req) + require.NoError(t, err) + assert.Equal(t, "https://hub.example.com", req.Audience) + assert.True(t, req.IncludeEmail) + + // Return a valid response + resp := map[string]string{"token": testToken} + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(resp) + })) + defer iamServer.Close() + + m := NewGCPTransportMinter(testSA, iamServer.URL) + + token, expiry, err := m.MintIDToken(context.Background(), "https://hub.example.com") + require.NoError(t, err) + assert.Equal(t, testToken, token) + assert.Equal(t, testExpiry, expiry) +} + +func TestGCPTransportMinter_EmptySA(t *testing.T) { + m := NewGCPTransportMinter("", "") + + _, _, err := m.MintIDToken(context.Background(), "https://hub.example.com") + assert.Error(t, err) + assert.Contains(t, err.Error(), "service account email not configured") +} + +func TestGCPTransportMinter_APIError(t *testing.T) { + iamServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusForbidden) + fmt.Fprintln(w, `{"error": {"code": 403, "message": "Permission denied"}}`) + })) + defer iamServer.Close() + + m := NewGCPTransportMinter("sa@project.iam.gserviceaccount.com", iamServer.URL) + + _, _, err := m.MintIDToken(context.Background(), "https://hub.example.com") + assert.Error(t, err) + assert.Contains(t, err.Error(), "generateIdToken failed") +} + +func TestParseJWTExpiry(t *testing.T) { + expected := time.Now().Add(1 * time.Hour).Truncate(time.Second) + token := makeTestJWTWithExpiry(expected) + + expiry, err := parseJWTExpiry(token) + require.NoError(t, err) + assert.Equal(t, expected, expiry) +} + +func TestParseJWTExpiry_InvalidFormat(t *testing.T) { + _, err := parseJWTExpiry("not-a-jwt") + assert.Error(t, err) + assert.Contains(t, err.Error(), "expected 3 parts") +} + +func TestParseJWTExpiry_NoExpClaim(t *testing.T) { + header := base64.RawURLEncoding.EncodeToString([]byte(`{"alg":"none"}`)) + payload := base64.RawURLEncoding.EncodeToString([]byte(`{"iss":"test"}`)) + sig := base64.RawURLEncoding.EncodeToString([]byte("sig")) + token := fmt.Sprintf("%s.%s.%s", header, payload, sig) + + _, err := parseJWTExpiry(token) + assert.Error(t, err) + assert.Contains(t, err.Error(), "no expiry claim") +} + +func TestRefreshTokenEntry_JSON(t *testing.T) { + entry := RefreshTokenEntry{ + Layer: "transport", + Type: "google_oidc", + Value: "token-value", + ExpiresIn: 3600, + Audience: "https://hub.example.com", + } + + data, err := json.Marshal(entry) + require.NoError(t, err) + + var parsed RefreshTokenEntry + err = json.Unmarshal(data, &parsed) + require.NoError(t, err) + assert.Equal(t, entry, parsed) +} + +func TestRefreshTokenEntry_JSON_OmitAudience(t *testing.T) { + entry := RefreshTokenEntry{ + Layer: "app", + Type: "scion_access", + Value: "token-value", + ExpiresIn: 36000, + } + + data, err := json.Marshal(entry) + require.NoError(t, err) + assert.NotContains(t, string(data), "audience") +} diff --git a/pkg/hub/user_activity_test.go b/pkg/hub/user_activity_test.go index 1e6de0b55..bfc6ede5e 100644 --- a/pkg/hub/user_activity_test.go +++ b/pkg/hub/user_activity_test.go @@ -58,7 +58,7 @@ func TestUserActivityTracker_ThrottlesWrites(t *testing.T) { tracker := NewUserActivityTracker(rec, time.Hour) // First touch should write - tracker.Touch("user-1") + tracker.Touch(tid("user-1")) // Wait for async goroutine time.Sleep(50 * time.Millisecond) @@ -67,12 +67,12 @@ func TestUserActivityTracker_ThrottlesWrites(t *testing.T) { if len(calls) != 1 { t.Fatalf("expected 1 call after first touch, got %d", len(calls)) } - if calls[0].id != "user-1" { + if calls[0].id != tid("user-1") { t.Errorf("expected user-1, got %s", calls[0].id) } // Second touch within the interval should be throttled - tracker.Touch("user-1") + tracker.Touch(tid("user-1")) time.Sleep(50 * time.Millisecond) calls = rec.getCalls() @@ -85,7 +85,7 @@ func TestUserActivityTracker_DifferentUsers(t *testing.T) { rec := &lastSeenRecorder{} tracker := NewUserActivityTracker(rec, time.Hour) - tracker.Touch("user-1") + tracker.Touch(tid("user-1")) tracker.Touch("user-2") time.Sleep(50 * time.Millisecond) @@ -101,11 +101,11 @@ func TestUserActivityTracker_WritesAfterInterval(t *testing.T) { // Use a very short interval for testing tracker := NewUserActivityTracker(rec, 10*time.Millisecond) - tracker.Touch("user-1") + tracker.Touch(tid("user-1")) time.Sleep(50 * time.Millisecond) // After interval has passed, a second touch should write again - tracker.Touch("user-1") + tracker.Touch(tid("user-1")) time.Sleep(50 * time.Millisecond) calls := rec.getCalls() diff --git a/pkg/hub/useraccesstoken_test.go b/pkg/hub/useraccesstoken_test.go index f842003e7..2d2b30a41 100644 --- a/pkg/hub/useraccesstoken_test.go +++ b/pkg/hub/useraccesstoken_test.go @@ -189,12 +189,12 @@ func newTestUATService() (*UserAccessTokenService, *mockUATStore, *mockUserStore tokenStore := newMockUATStore() userStore := &mockUserStore{ users: map[string]*store.User{ - "user-1": {ID: "user-1", Email: "test@example.com", DisplayName: "Test User", Role: "member"}, + tid("user-1"): {ID: tid("user-1"), Email: "test@example.com", DisplayName: "Test User", Role: "member"}, }, } projectStore := &mockProjectStore{ projects: map[string]*store.Project{ - "project-1": {ID: "project-1", Name: "test-project"}, + tid("project-1"): {ID: tid("project-1"), Name: "test-project"}, }, } svc := NewUserAccessTokenService(tokenStore, userStore, projectStore) @@ -206,7 +206,7 @@ func TestCreateToken(t *testing.T) { ctx := context.Background() t.Run("basic creation", func(t *testing.T) { - key, token, err := svc.CreateToken(ctx, "user-1", "ci-token", "project-1", + key, token, err := svc.CreateToken(ctx, tid("user-1"), "ci-token", tid("project-1"), []string{"agent:dispatch", "agent:read"}, nil) if err != nil { t.Fatalf("unexpected error: %v", err) @@ -217,7 +217,7 @@ func TestCreateToken(t *testing.T) { if token.Name != "ci-token" { t.Errorf("expected name 'ci-token', got %q", token.Name) } - if token.ProjectID != "project-1" { + if token.ProjectID != tid("project-1") { t.Errorf("expected projectID 'project-1', got %q", token.ProjectID) } if len(token.Scopes) != 2 { @@ -229,7 +229,7 @@ func TestCreateToken(t *testing.T) { }) t.Run("expands agent:manage", func(t *testing.T) { - _, token, err := svc.CreateToken(ctx, "user-1", "manage-token", "project-1", + _, token, err := svc.CreateToken(ctx, tid("user-1"), "manage-token", tid("project-1"), []string{"agent:manage"}, nil) if err != nil { t.Fatalf("unexpected error: %v", err) @@ -240,7 +240,7 @@ func TestCreateToken(t *testing.T) { }) t.Run("rejects invalid scope", func(t *testing.T) { - _, _, err := svc.CreateToken(ctx, "user-1", "bad-token", "project-1", + _, _, err := svc.CreateToken(ctx, tid("user-1"), "bad-token", tid("project-1"), []string{"invalid:scope"}, nil) if !errors.Is(err, ErrInvalidUATScope) { t.Errorf("expected ErrInvalidUATScope, got %v", err) @@ -248,7 +248,7 @@ func TestCreateToken(t *testing.T) { }) t.Run("rejects missing project", func(t *testing.T) { - _, _, err := svc.CreateToken(ctx, "user-1", "bad-token", "nonexistent", + _, _, err := svc.CreateToken(ctx, tid("user-1"), "bad-token", "nonexistent", []string{"agent:read"}, nil) if err == nil { t.Error("expected error for nonexistent project") @@ -257,7 +257,7 @@ func TestCreateToken(t *testing.T) { t.Run("rejects expiry too long", func(t *testing.T) { tooFar := time.Now().Add(400 * 24 * time.Hour) - _, _, err := svc.CreateToken(ctx, "user-1", "bad-token", "project-1", + _, _, err := svc.CreateToken(ctx, tid("user-1"), "bad-token", tid("project-1"), []string{"agent:read"}, &tooFar) if !errors.Is(err, ErrUATExpiryTooLong) { t.Errorf("expected ErrUATExpiryTooLong, got %v", err) @@ -265,7 +265,7 @@ func TestCreateToken(t *testing.T) { }) t.Run("rejects empty scopes", func(t *testing.T) { - _, _, err := svc.CreateToken(ctx, "user-1", "bad-token", "project-1", + _, _, err := svc.CreateToken(ctx, tid("user-1"), "bad-token", tid("project-1"), []string{}, nil) if err == nil { t.Error("expected error for empty scopes") @@ -277,7 +277,7 @@ func TestValidateToken(t *testing.T) { svc, _, _ := newTestUATService() ctx := context.Background() - key, _, err := svc.CreateToken(ctx, "user-1", "test-token", "project-1", + key, _, err := svc.CreateToken(ctx, tid("user-1"), "test-token", tid("project-1"), []string{"agent:dispatch", "agent:read"}, nil) if err != nil { t.Fatalf("failed to create token: %v", err) @@ -288,10 +288,10 @@ func TestValidateToken(t *testing.T) { if err != nil { t.Fatalf("unexpected error: %v", err) } - if identity.ID() != "user-1" { + if identity.ID() != tid("user-1") { t.Errorf("expected user ID 'user-1', got %q", identity.ID()) } - if identity.ScopedProjectID() != "project-1" { + if identity.ScopedProjectID() != tid("project-1") { t.Errorf("expected project 'project-1', got %q", identity.ScopedProjectID()) } if !identity.HasScope("agent:dispatch") { @@ -321,14 +321,14 @@ func TestRevokeToken(t *testing.T) { svc, _, _ := newTestUATService() ctx := context.Background() - key, token, err := svc.CreateToken(ctx, "user-1", "test-token", "project-1", + key, token, err := svc.CreateToken(ctx, tid("user-1"), "test-token", tid("project-1"), []string{"agent:read"}, nil) if err != nil { t.Fatalf("failed to create token: %v", err) } // Revoke it - if err := svc.RevokeToken(ctx, "user-1", token.ID); err != nil { + if err := svc.RevokeToken(ctx, tid("user-1"), token.ID); err != nil { t.Fatalf("failed to revoke token: %v", err) } @@ -339,7 +339,7 @@ func TestRevokeToken(t *testing.T) { } // Wrong user can't revoke - if err := svc.RevokeToken(ctx, "other-user", token.ID); !errors.Is(err, store.ErrNotFound) { + if err := svc.RevokeToken(ctx, tid("other-user"), token.ID); !errors.Is(err, store.ErrNotFound) { t.Errorf("expected ErrNotFound for wrong user, got %v", err) } } @@ -348,13 +348,13 @@ func TestDeleteToken(t *testing.T) { svc, _, _ := newTestUATService() ctx := context.Background() - key, token, err := svc.CreateToken(ctx, "user-1", "test-token", "project-1", + key, token, err := svc.CreateToken(ctx, tid("user-1"), "test-token", tid("project-1"), []string{"agent:read"}, nil) if err != nil { t.Fatalf("failed to create token: %v", err) } - if err := svc.DeleteToken(ctx, "user-1", token.ID); err != nil { + if err := svc.DeleteToken(ctx, tid("user-1"), token.ID); err != nil { t.Fatalf("failed to delete token: %v", err) } @@ -371,7 +371,7 @@ func TestTokenLimit(t *testing.T) { // Create max tokens for i := 0; i < store.UATMaxPerUser; i++ { - _, _, err := svc.CreateToken(ctx, "user-1", "token-"+string(rune('a'+i%26))+string(rune('0'+i/26)), "project-1", + _, _, err := svc.CreateToken(ctx, tid("user-1"), "token-"+string(rune('a'+i%26))+string(rune('0'+i/26)), tid("project-1"), []string{"agent:read"}, nil) if err != nil { t.Fatalf("failed to create token %d: %v", i, err) @@ -379,7 +379,7 @@ func TestTokenLimit(t *testing.T) { } // Next one should fail - _, _, err := svc.CreateToken(ctx, "user-1", "one-too-many", "project-1", + _, _, err := svc.CreateToken(ctx, tid("user-1"), "one-too-many", tid("project-1"), []string{"agent:read"}, nil) if !errors.Is(err, ErrUATLimitExceeded) { t.Errorf("expected ErrUATLimitExceeded, got %v", err) @@ -392,14 +392,14 @@ func TestListTokens(t *testing.T) { // Create 3 tokens for i := 0; i < 3; i++ { - _, _, err := svc.CreateToken(ctx, "user-1", "token-"+string(rune('a'+i)), "project-1", + _, _, err := svc.CreateToken(ctx, tid("user-1"), "token-"+string(rune('a'+i)), tid("project-1"), []string{"agent:read"}, nil) if err != nil { t.Fatalf("failed to create token: %v", err) } } - tokens, err := svc.ListTokens(ctx, "user-1") + tokens, err := svc.ListTokens(ctx, tid("user-1")) if err != nil { t.Fatalf("failed to list tokens: %v", err) } @@ -441,16 +441,16 @@ func TestExpandScopes(t *testing.T) { } func TestScopedUserIdentity(t *testing.T) { - base := NewAuthenticatedUser("user-1", "test@example.com", "Test", "member", "api") - scoped := NewScopedUserIdentity(base, "project-1", []string{"agent:dispatch", "agent:read"}) + base := NewAuthenticatedUser(tid("user-1"), "test@example.com", "Test", "member", "api") + scoped := NewScopedUserIdentity(base, tid("project-1"), []string{"agent:dispatch", "agent:read"}) - if scoped.ID() != "user-1" { + if scoped.ID() != tid("user-1") { t.Errorf("expected ID 'user-1', got %q", scoped.ID()) } if scoped.Email() != "test@example.com" { t.Errorf("expected email 'test@example.com', got %q", scoped.Email()) } - if scoped.ScopedProjectID() != "project-1" { + if scoped.ScopedProjectID() != tid("project-1") { t.Errorf("expected project 'project-1', got %q", scoped.ScopedProjectID()) } if !scoped.HasScope("agent:dispatch") { diff --git a/pkg/hub/wake_test.go b/pkg/hub/wake_test.go index 6e958afcc..8c223023c 100644 --- a/pkg/hub/wake_test.go +++ b/pkg/hub/wake_test.go @@ -37,14 +37,14 @@ func createWakeTestFixtures(t *testing.T, agentPhase string) (*Server, store.Sto ctx := context.Background() project := &store.Project{ - ID: "project-wake-" + agentPhase, + ID: tid("project-wake-" + agentPhase), Name: "Wake Test Project", Slug: "wake-test-project-" + agentPhase, } require.NoError(t, s.CreateProject(ctx, project)) broker := &store.RuntimeBroker{ - ID: "broker-wake-" + agentPhase, + ID: tid("broker-wake-" + agentPhase), Name: "Wake Test Broker", Slug: "wake-test-broker-" + agentPhase, Status: store.BrokerStatusOnline, @@ -59,7 +59,7 @@ func createWakeTestFixtures(t *testing.T, agentPhase string) (*Server, store.Sto })) agent := &store.Agent{ - ID: "agent-wake-" + agentPhase, + ID: tid("agent-wake-" + agentPhase), Slug: "agent-wake-" + agentPhase, Name: "Wake Agent", ProjectID: project.ID, diff --git a/pkg/hub/web.go b/pkg/hub/web.go index 46b1766e9..18ea2e44f 100644 --- a/pkg/hub/web.go +++ b/pkg/hub/web.go @@ -17,6 +17,7 @@ package hub import ( "context" "crypto/rand" + "crypto/sha256" "encoding/hex" "encoding/json" "fmt" @@ -123,6 +124,9 @@ type WebServerConfig struct { BaseURL string // DevAuthToken is the dev token for auto-login (empty = disabled). DevAuthToken string + // AuthMode is the exclusive human auth mode: "oauth" (default), "proxy", "dev". + // In proxy mode, OAuth providers are not shown and logout behavior changes. + AuthMode string // AuthorizedDomains is the list of allowed email domains (empty = all allowed). AuthorizedDomains []string // AdminEmails is the list of bootstrap admin emails (bypass domain check). @@ -143,11 +147,11 @@ type WebServer struct { assets fs.FS // embedded or nil assetsDisk string // filesystem override path, or "" shellTmpl *template.Template - sessionStore *sessions.FilesystemStore + sessionStore *sessions.CookieStore oauthService *OAuthService store store.Store userTokenSvc *UserTokenService - events *ChannelEventPublisher // nil when no publisher configured + events EventPublisher // nil when no publisher configured hubHandler http.Handler // mounted Hub API handler, or nil hubShutdown func(context.Context) error // Hub resource cleanup, or nil maintenance *MaintenanceState // runtime maintenance mode state (shared with Hub) @@ -420,28 +424,44 @@ func NewWebServer(cfg WebServerConfig) *WebServer { slog.Warn("No session secret configured, using random key (sessions will not persist across restarts)") } - // Use a filesystem-backed session store so that only a small session ID - // is sent as a cookie. This avoids the 4 KB cookie size limit that can - // be exceeded when JWT tokens are stored in the session. - sessionDir := filepath.Join(os.TempDir(), "scion-sessions") - if err := os.MkdirAll(sessionDir, 0700); err != nil { - slog.Error("Failed to create session directory", "dir", sessionDir, "error", err) - } - fsStore := sessions.NewFilesystemStore(sessionDir, []byte(sessionKey)) - // Remove the default 4096-byte securecookie encoding limit. The - // FilesystemStore writes session data to disk (not cookies), so the - // browser cookie-size cap is irrelevant. JWT tokens stored in the - // session regularly exceed 4096 bytes after gob+base64 encoding, - // which causes Save() to fail and tokens to be silently dropped. - fsStore.MaxLength(0) - fsStore.Options = &sessions.Options{ + // Use an encrypted, signed cookie session store so that NO session state + // lives on a single replica's local filesystem. This is required for + // horizontal scaling: behind a load balancer the OAuth login and callback + // (and every subsequent API request) can land on different replicas. A + // cookie-backed store keeps the whole session — the OAuth CSRF state token, + // the post-login return path, the user identity, and the Hub access/refresh + // tokens — in the client's signed+encrypted cookie, so any replica sharing + // SESSION_SECRET can read it. + // + // The previous FilesystemStore kept this state on one replica's disk, which + // caused intermittent "state_mismatch" login failures (and silently dropped + // post-login sessions) whenever the LB routed a follow-up request to a + // different replica. The whole session encodes to roughly 2.6 KB today — + // well within the browser's ~4 KB per-cookie cap — so the historical + // "JWT tokens exceed 4096 bytes" concern that motivated the disk store no + // longer applies to the current compact HS256 tokens. + // + // Keys are derived deterministically from the shared SESSION_SECRET so all + // replicas agree: a 32-byte HMAC authentication key and a 32-byte AES-256 + // encryption key, with domain separation so the two keys differ. + cookieStore := sessions.NewCookieStore( + deriveSessionKey(sessionKey, "scion-session-hash"), + deriveSessionKey(sessionKey, "scion-session-block"), + ) + cookieStore.Options = &sessions.Options{ Path: "/", MaxAge: 86400, // 24 hours HttpOnly: true, Secure: strings.HasPrefix(cfg.BaseURL, "https://"), SameSite: http.SameSiteLaxMode, } - ws.sessionStore = fsStore + // Keep securecookie's timestamp window in sync with the cookie MaxAge. We + // intentionally leave the default 4096-byte securecookie length limit in + // force (unlike the disk store, which disabled it): if a session ever grew + // past the browser cookie cap, Save() would return an error we can log + // rather than silently emitting an oversized cookie the browser drops. + cookieStore.MaxAge(cookieStore.Options.MaxAge) + ws.sessionStore = cookieStore // Resolve asset source if cfg.AssetsDir != "" { @@ -471,6 +491,17 @@ func NewWebServer(cfg WebServerConfig) *WebServer { return ws } +// deriveSessionKey deterministically derives a 32-byte key from the shared +// session secret and a label. The label provides domain separation so the +// HMAC authentication key and the AES encryption key differ even though both +// originate from the same SESSION_SECRET. Every replica configured with the +// same secret derives identical keys, which is what lets a session cookie +// minted by one replica be validated and decrypted by another. +func deriveSessionKey(secret, label string) []byte { + sum := sha256.Sum256([]byte(label + ":" + secret)) + return sum[:] +} + // SetMaintenanceState sets the shared runtime maintenance state. func (ws *WebServer) SetMaintenanceState(ms *MaintenanceState) { ws.maintenance = ms @@ -492,7 +523,7 @@ func (ws *WebServer) SetUserTokenService(svc *UserTokenService) { } // SetEventPublisher sets the event publisher for real-time SSE streaming. -func (ws *WebServer) SetEventPublisher(pub *ChannelEventPublisher) { +func (ws *WebServer) SetEventPublisher(pub EventPublisher) { ws.events = pub } @@ -642,6 +673,7 @@ func (ws *WebServer) sessionToBearerMiddleware(next http.Handler) http.Handler { // registerRoutes sets up the web server routes. func (ws *WebServer) registerRoutes() { ws.mux.HandleFunc("/healthz", ws.handleHealthz) + ws.mux.HandleFunc("/health", ws.handleHealthz) ws.mux.Handle("/assets/", ws.staticHandler()) ws.mux.Handle("/shoelace/", ws.staticHandler()) // Auth routes (no session auth required) @@ -952,7 +984,7 @@ func (ws *WebServer) tryServeStaticFile(w http.ResponseWriter, r *http.Request) } // handleSSE serves the Server-Sent Events endpoint. It subscribes to the -// in-process ChannelEventPublisher and streams matching events to the browser. +// configured EventPublisher and streams matching events to the browser. // Route: GET /events?sub=&sub=... func (ws *WebServer) handleSSE(w http.ResponseWriter, r *http.Request) { if ws.events == nil { @@ -1069,7 +1101,7 @@ func isAllowedSubjectChar(c rune) bool { // isPublicRoute returns true for routes that do not require authentication. func isPublicRoute(path string) bool { switch { - case path == "/healthz": + case path == "/healthz" || path == "/health": return true case strings.HasPrefix(path, "/assets/"): return true @@ -1447,8 +1479,26 @@ func (ws *WebServer) handleOAuthCallback(w http.ResponseWriter, r *http.Request) } // handleLogout clears the session and redirects to login (or returns JSON for API). +// In proxy mode, logout is a no-op (the proxy owns the session) — optionally +// redirect to IAP's clear_login_cookie endpoint. // Route: GET /auth/logout, POST /auth/logout func (ws *WebServer) handleLogout(w http.ResponseWriter, r *http.Request) { + // In proxy mode, the hub does not own the session. + if ws.config.AuthMode == "proxy" { + if isBrowserRequest(r) { + // Redirect to IAP's clear login cookie endpoint + http.Redirect(w, r, "/_gcp_iap/clear_login_cookie", http.StatusFound) + return + } + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + json.NewEncoder(w).Encode(map[string]interface{}{ + "success": true, + "message": "proxy mode: session is managed by the authenticating proxy", + }) + return + } + session, err := ws.sessionStore.Get(r, webSessionName) if err != nil { session, _ = ws.sessionStore.New(r, webSessionName) @@ -1515,11 +1565,14 @@ func (ws *WebServer) handleAuthMe(w http.ResponseWriter, r *http.Request) { // handleAuthProviders returns which OAuth providers are enabled for web login. // Route: GET /auth/providers func (ws *WebServer) handleAuthProviders(w http.ResponseWriter, r *http.Request) { - resp := map[string]bool{ + resp := map[string]interface{}{ "google": false, "github": false, } - if ws.oauthService != nil { + // In proxy mode, no OAuth providers are active (auth is handled by the proxy). + if ws.config.AuthMode == "proxy" { + resp["authMode"] = "proxy" + } else if ws.oauthService != nil { resp["google"] = ws.oauthService.IsProviderConfiguredForClient(OAuthClientTypeWeb, "google") resp["github"] = ws.oauthService.IsProviderConfiguredForClient(OAuthClientTypeWeb, "github") } @@ -1546,6 +1599,7 @@ func (ws *WebServer) handleAuthDebug(w http.ResponseWriter, r *http.Request) { "hasAccessToken": session.Values[sessKeyHubAccessToken] != nil, "config": map[string]interface{}{ "baseURL": ws.config.BaseURL, + "authMode": ws.config.AuthMode, "devAuthEnabled": ws.config.DevAuthToken != "", "oauthConfigured": ws.oauthService != nil, "storeConfigured": ws.store != nil, diff --git a/pkg/hub/web_test.go b/pkg/hub/web_test.go index 1f9f17692..22c09a113 100644 --- a/pkg/hub/web_test.go +++ b/pkg/hub/web_test.go @@ -27,7 +27,6 @@ import ( "time" "github.com/GoogleCloudPlatform/scion/pkg/store" - "github.com/gorilla/securecookie" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -1288,20 +1287,87 @@ func TestSessionStore_CookieConfiguration(t *testing.T) { "HTTP base URL should produce non-secure cookies") } -func TestSessionStore_NoMaxLengthLimit(t *testing.T) { - // The FilesystemStore stores data on disk, not in cookies, so the default - // securecookie 4096-byte limit must be removed. JWT tokens in the session - // regularly exceed that limit after gob+base64 encoding. - ws := newTestWebServer(t, WebServerConfig{}) - for _, codec := range ws.sessionStore.Codecs { - if sc, ok := codec.(*securecookie.SecureCookie); ok { - // Encode a large value — if MaxLength were still 4096 this would fail. - large := make(map[interface{}]interface{}) - large["token"] = string(make([]byte, 8000)) - _, err := securecookie.EncodeMulti("test", large, sc) - assert.NoError(t, err, "session store should allow values larger than 4096 bytes") - } +func TestSessionStore_CrossReplicaRoundTrip(t *testing.T) { + // Behind a load balancer the OAuth login, the provider callback, and every + // follow-up API request can each land on a different replica. With a + // cookie-backed session store, any replica configured with the same + // SESSION_SECRET must be able to read a session cookie minted by another + // replica. This is the regression test for the "state_mismatch" login + // failures (and dropped post-login sessions) caused by the previous + // filesystem-backed, process-local store. + const secret = "test-shared-session-secret-value-1234567890" + + replicaA := newTestWebServer(t, WebServerConfig{SessionSecret: secret}) + replicaB := newTestWebServer(t, WebServerConfig{SessionSecret: secret}) + + // A realistic post-login payload: identity plus access/refresh JWTs, in + // addition to the short-lived OAuth CSRF state. + svc, err := NewUserTokenService(UserTokenConfig{}) + require.NoError(t, err) + access, refresh, _, err := svc.GenerateTokenPair("user_123", "user@example.com", "Test User", "admin", ClientTypeWeb) + require.NoError(t, err) + + // Replica A writes the session and emits the cookie (e.g. during /auth/login + // and the callback that completes login). + reqA := httptest.NewRequest(http.MethodGet, "/auth/login/google", nil) + recA := httptest.NewRecorder() + sessA, err := replicaA.sessionStore.Get(reqA, webSessionName) + require.NoError(t, err) + sessA.Values[sessKeyOAuthState] = "state-token-abc123" + sessA.Values[sessKeyUserID] = "user_123" + sessA.Values[sessKeyUserEmail] = "user@example.com" + sessA.Values[sessKeyHubAccessToken] = access + sessA.Values[sessKeyHubRefreshToken] = refresh + require.NoError(t, sessA.Save(reqA, recA)) + + cookies := recA.Result().Cookies() + require.NotEmpty(t, cookies, "replica A should set a session cookie") + + // Replica B receives the cookie minted by replica A and must decode it. + reqB := httptest.NewRequest(http.MethodGet, "/auth/callback/google", nil) + for _, c := range cookies { + reqB.AddCookie(c) + } + sessB, err := replicaB.sessionStore.Get(reqB, webSessionName) + require.NoError(t, err) + assert.False(t, sessB.IsNew, "replica B must decode the session cookie minted by replica A") + assert.Equal(t, "state-token-abc123", sessB.Values[sessKeyOAuthState], + "OAuth state must survive across replicas (fixes state_mismatch)") + assert.Equal(t, "user_123", sessB.Values[sessKeyUserID]) + assert.Equal(t, access, sessB.Values[sessKeyHubAccessToken], + "post-login access token must survive across replicas") + assert.Equal(t, refresh, sessB.Values[sessKeyHubRefreshToken]) +} + +func TestSessionStore_DifferentSecretCannotDecode(t *testing.T) { + // A replica configured with a different SESSION_SECRET must NOT be able to + // read another replica's session cookie — the cookie is authenticated and + // encrypted with keys derived from the shared secret. + replicaA := newTestWebServer(t, WebServerConfig{SessionSecret: "secret-A-1234567890-abcdefghijklmnop"}) + replicaC := newTestWebServer(t, WebServerConfig{SessionSecret: "secret-C-1234567890-abcdefghijklmnop"}) + + reqA := httptest.NewRequest(http.MethodGet, "/auth/login/google", nil) + recA := httptest.NewRecorder() + sessA, err := replicaA.sessionStore.Get(reqA, webSessionName) + require.NoError(t, err) + sessA.Values[sessKeyOAuthState] = "state-token-abc123" + require.NoError(t, sessA.Save(reqA, recA)) + + reqC := httptest.NewRequest(http.MethodGet, "/auth/callback/google", nil) + for _, c := range recA.Result().Cookies() { + reqC.AddCookie(c) + } + sessC, err := replicaC.sessionStore.Get(reqC, webSessionName) + require.NotNil(t, sessC) + // A cookie authenticated/encrypted with a different secret fails to decode: + // gorilla returns a decode error together with a fresh, empty session. + // Either way, the state must not leak across mismatched secrets. + require.NotNil(t, sessC, "session store must return a non-nil session even on decode error") + if err == nil { + assert.True(t, sessC.IsNew, "session from a mismatched secret should be new/empty") } + assert.Nil(t, sessC.Values[sessKeyOAuthState], + "OAuth state must not decode under a different secret") } func TestSetters(t *testing.T) { @@ -1426,7 +1492,7 @@ func TestSSEHandler_EventDelivery(t *testing.T) { select { case <-ticker.C: pub.publish("project.test123.agent.status", AgentStatusEvent{ - AgentID: "agent-1", + AgentID: tid("agent-1"), ProjectID: "test123", Phase: "running", }) @@ -1461,7 +1527,7 @@ func TestSSEHandler_EventDelivery(t *testing.T) { assert.Contains(t, frame, "event: update\n") assert.Contains(t, frame, "data: ") assert.Contains(t, frame, `"subject":"project.test123.agent.status"`) - assert.Contains(t, frame, `"agentId":"agent-1"`) + assert.Contains(t, frame, `"agentId":"`+tid("agent-1")+`"`) assert.Contains(t, frame, `"phase":"running"`) } @@ -1708,7 +1774,7 @@ func TestSPAShellHandler_ContainsInitialData(t *testing.T) { json.NewEncoder(w).Encode(map[string]interface{}{ "agents": []map[string]interface{}{ { - "id": "agent-1", + "id": tid("agent-1"), "name": "test-agent", "status": "running", "_capabilities": map[string]interface{}{ @@ -1738,7 +1804,7 @@ func TestSPAShellHandler_ContainsInitialData(t *testing.T) { assert.Equal(t, http.StatusOK, resp.StatusCode) // The __SCION_DATA__ should contain agent data - assert.Contains(t, html, `"agent-1"`) + assert.Contains(t, html, tid("agent-1")) assert.Contains(t, html, `"test-agent"`) assert.Contains(t, html, `"_capabilities"`) assert.Contains(t, html, `"actions"`) diff --git a/pkg/hub/workspace_handlers_test.go b/pkg/hub/workspace_handlers_test.go index 03b60d804..8c7b92407 100644 --- a/pkg/hub/workspace_handlers_test.go +++ b/pkg/hub/workspace_handlers_test.go @@ -19,6 +19,7 @@ package hub import ( "context" "encoding/json" + "fmt" "net/http" "net/http/httptest" "strings" @@ -27,7 +28,6 @@ import ( "github.com/GoogleCloudPlatform/scion/pkg/agent/state" "github.com/GoogleCloudPlatform/scion/pkg/store" - "github.com/GoogleCloudPlatform/scion/pkg/store/sqlite" "github.com/GoogleCloudPlatform/scion/pkg/transfer" ) @@ -37,7 +37,7 @@ const testWorkspaceDevToken = "scion_dev_workspace_test_token_1234567890" // testWorkspaceServer creates a test server for workspace handler tests. func testWorkspaceServer(t *testing.T) (*Server, store.Store) { t.Helper() - s, err := sqlite.New(":memory:") + s, err := newTestStore(":memory:") if err != nil { t.Fatalf("failed to create test store: %v", err) } @@ -82,25 +82,25 @@ func TestWorkspaceRoutesParsing(t *testing.T) { }{ { name: "workspace status", - url: "/api/v1/agents/agent-123/workspace", + url: fmt.Sprintf("/api/v1/agents/%s/workspace", "agent-123"), expectedID: "agent-123", expectedAction: "workspace", }, { name: "workspace sync-from", - url: "/api/v1/agents/agent-123/workspace/sync-from", + url: fmt.Sprintf("/api/v1/agents/%s/workspace/sync-from", "agent-123"), expectedID: "agent-123", expectedAction: "workspace/sync-from", }, { name: "workspace sync-to", - url: "/api/v1/agents/agent-123/workspace/sync-to", + url: fmt.Sprintf("/api/v1/agents/%s/workspace/sync-to", "agent-123"), expectedID: "agent-123", expectedAction: "workspace/sync-to", }, { name: "workspace sync-to finalize", - url: "/api/v1/agents/agent-123/workspace/sync-to/finalize", + url: fmt.Sprintf("/api/v1/agents/%s/workspace/sync-to/finalize", "agent-123"), expectedID: "agent-123", expectedAction: "workspace/sync-to/finalize", }, @@ -128,14 +128,14 @@ func TestWorkspaceStatusHandler(t *testing.T) { now := time.Now() // Create the project first (foreign key dependency) - createTestProject(t, s, "project_test_1") + createTestProject(t, s, tid("project_test_1")) // Create a test agent agent := &store.Agent{ - ID: "agent_workspace_test_1", + ID: tid("agent_workspace_test_1"), Slug: "workspace-test-agent", Name: "test-agent", - ProjectID: "project_test_1", + ProjectID: tid("project_test_1"), Phase: string(state.PhaseRunning), StateVersion: 1, Created: now, @@ -146,7 +146,7 @@ func TestWorkspaceStatusHandler(t *testing.T) { } // Test workspace status endpoint - req := httptest.NewRequest("GET", "/api/v1/agents/agent_workspace_test_1/workspace", nil) + req := httptest.NewRequest("GET", fmt.Sprintf("/api/v1/agents/%s/workspace", tid("agent_workspace_test_1")), nil) req.Header.Set("Authorization", "Bearer "+testWorkspaceDevToken) rec := httptest.NewRecorder() @@ -161,11 +161,11 @@ func TestWorkspaceStatusHandler(t *testing.T) { t.Fatalf("failed to decode response: %v", err) } - if resp.Slug != "agent_workspace_test_1" { - t.Errorf("response AgentID = %q, want %q", resp.Slug, "agent_workspace_test_1") + if resp.Slug != tid("agent_workspace_test_1") { + t.Errorf("response AgentID = %q, want %q", resp.Slug, tid("agent_workspace_test_1")) } - if resp.ProjectID != "project_test_1" { - t.Errorf("response ProjectID = %q, want %q", resp.ProjectID, "project_test_1") + if resp.ProjectID != tid("project_test_1") { + t.Errorf("response ProjectID = %q, want %q", resp.ProjectID, tid("project_test_1")) } } @@ -189,14 +189,14 @@ func TestWorkspaceSyncFromHandler_AgentNotRunning(t *testing.T) { now := time.Now() // Create the project first - createTestProject(t, s, "project_test") + createTestProject(t, s, tid("project_test")) // Create a stopped agent agent := &store.Agent{ - ID: "agent_stopped_1", + ID: tid("agent_stopped_1"), Slug: "stopped-agent", Name: "stopped-agent", - ProjectID: "project_test", + ProjectID: tid("project_test"), Phase: string(state.PhaseStopped), StateVersion: 1, Created: now, @@ -206,7 +206,7 @@ func TestWorkspaceSyncFromHandler_AgentNotRunning(t *testing.T) { t.Fatalf("failed to create agent: %v", err) } - req := httptest.NewRequest("POST", "/api/v1/agents/agent_stopped_1/workspace/sync-from", nil) + req := httptest.NewRequest("POST", fmt.Sprintf("/api/v1/agents/%s/workspace/sync-from", tid("agent_stopped_1")), nil) req.Header.Set("Authorization", "Bearer "+testWorkspaceDevToken) rec := httptest.NewRecorder() @@ -224,13 +224,13 @@ func TestWorkspaceSyncToHandler_EmptyFiles(t *testing.T) { now := time.Now() // Create the project first - createTestProject(t, s, "project_syncto") + createTestProject(t, s, tid("project_syncto")) agent := &store.Agent{ - ID: "agent_syncto_test", + ID: tid("agent_syncto_test"), Slug: "sync-to-test-agent", Name: "test-agent", - ProjectID: "project_syncto", + ProjectID: tid("project_syncto"), Phase: string(state.PhaseRunning), StateVersion: 1, Created: now, @@ -242,7 +242,7 @@ func TestWorkspaceSyncToHandler_EmptyFiles(t *testing.T) { // Send request with empty files list body := `{"files": []}` - req := httptest.NewRequest("POST", "/api/v1/agents/agent_syncto_test/workspace/sync-to", strings.NewReader(body)) + req := httptest.NewRequest("POST", fmt.Sprintf("/api/v1/agents/%s/workspace/sync-to", tid("agent_syncto_test")), strings.NewReader(body)) req.Header.Set("Authorization", "Bearer "+testWorkspaceDevToken) req.Header.Set("Content-Type", "application/json") rec := httptest.NewRecorder() @@ -261,13 +261,13 @@ func TestWorkspaceSyncToFinalizeHandler_MissingManifest(t *testing.T) { now := time.Now() // Create the project first - createTestProject(t, s, "project_finalize") + createTestProject(t, s, tid("project_finalize")) agent := &store.Agent{ - ID: "agent_finalize_test", + ID: tid("agent_finalize_test"), Slug: "finalize-test-agent", Name: "test-agent", - ProjectID: "project_finalize", + ProjectID: tid("project_finalize"), Phase: string(state.PhaseRunning), StateVersion: 1, Created: now, @@ -279,7 +279,7 @@ func TestWorkspaceSyncToFinalizeHandler_MissingManifest(t *testing.T) { // Send request without manifest body := `{}` - req := httptest.NewRequest("POST", "/api/v1/agents/agent_finalize_test/workspace/sync-to/finalize", strings.NewReader(body)) + req := httptest.NewRequest("POST", fmt.Sprintf("/api/v1/agents/%s/workspace/sync-to/finalize", tid("agent_finalize_test")), strings.NewReader(body)) req.Header.Set("Authorization", "Bearer "+testWorkspaceDevToken) req.Header.Set("Content-Type", "application/json") rec := httptest.NewRecorder() @@ -298,13 +298,13 @@ func TestWorkspaceRoutesRequireAuth(t *testing.T) { now := time.Now() // Create the project first - createTestProject(t, s, "project_auth") + createTestProject(t, s, tid("project_auth")) agent := &store.Agent{ - ID: "agent_auth_test", + ID: tid("agent_auth_test"), Slug: "auth-test-agent", Name: "test-agent", - ProjectID: "project_auth", + ProjectID: tid("project_auth"), Phase: string(state.PhaseRunning), StateVersion: 1, Created: now, @@ -319,10 +319,10 @@ func TestWorkspaceRoutesRequireAuth(t *testing.T) { method string url string }{ - {"workspace status", "GET", "/api/v1/agents/agent_auth_test/workspace"}, - {"sync-from", "POST", "/api/v1/agents/agent_auth_test/workspace/sync-from"}, - {"sync-to", "POST", "/api/v1/agents/agent_auth_test/workspace/sync-to"}, - {"sync-to finalize", "POST", "/api/v1/agents/agent_auth_test/workspace/sync-to/finalize"}, + {"workspace status", "GET", fmt.Sprintf("/api/v1/agents/%s/workspace", tid("agent_auth_test"))}, + {"sync-from", "POST", fmt.Sprintf("/api/v1/agents/%s/workspace/sync-from", tid("agent_auth_test"))}, + {"sync-to", "POST", fmt.Sprintf("/api/v1/agents/%s/workspace/sync-to", tid("agent_auth_test"))}, + {"sync-to finalize", "POST", fmt.Sprintf("/api/v1/agents/%s/workspace/sync-to/finalize", tid("agent_auth_test"))}, } for _, tt := range tests { @@ -414,8 +414,8 @@ func TestWorkspaceSyncFromHandler_StorageNotConfigured(t *testing.T) { now := time.Now() // Use unique IDs for this test - projectID := "project_nostor_syncfrom" - agentID := "agent_nostor_syncfrom" + projectID := tid("project_nostor_syncfrom") + agentID := tid("agent_nostor_syncfrom") // Create the project first createTestProject(t, s, projectID) @@ -460,13 +460,13 @@ func TestWorkspaceSyncToHandler_StorageNotConfigured(t *testing.T) { now := time.Now() // Create the project first - createTestProject(t, s, "project_syncto_no_storage") + createTestProject(t, s, tid("project_syncto_no_storage")) agent := &store.Agent{ - ID: "agent_syncto_no_storage", + ID: tid("agent_syncto_no_storage"), Slug: "sync-to-no-storage-agent", Name: "test-agent", - ProjectID: "project_syncto_no_storage", + ProjectID: tid("project_syncto_no_storage"), Phase: string(state.PhaseRunning), StateVersion: 1, Created: now, @@ -478,7 +478,7 @@ func TestWorkspaceSyncToHandler_StorageNotConfigured(t *testing.T) { // Send request with files but no storage configured body := `{"files": [{"path": "test.txt", "size": 100, "hash": "sha256:abc123"}]}` - req := httptest.NewRequest("POST", "/api/v1/agents/agent_syncto_no_storage/workspace/sync-to", strings.NewReader(body)) + req := httptest.NewRequest("POST", fmt.Sprintf("/api/v1/agents/%s/workspace/sync-to", tid("agent_syncto_no_storage")), strings.NewReader(body)) req.Header.Set("Authorization", "Bearer "+testWorkspaceDevToken) req.Header.Set("Content-Type", "application/json") rec := httptest.NewRecorder() @@ -497,13 +497,13 @@ func TestWorkspaceSyncToFinalizeHandler_StorageNotConfigured(t *testing.T) { now := time.Now() // Create the project first - createTestProject(t, s, "project_finalize_no_storage") + createTestProject(t, s, tid("project_finalize_no_storage")) agent := &store.Agent{ - ID: "agent_finalize_no_storage", + ID: tid("agent_finalize_no_storage"), Slug: "finalize-no-storage-agent", Name: "test-agent", - ProjectID: "project_finalize_no_storage", + ProjectID: tid("project_finalize_no_storage"), Phase: string(state.PhaseRunning), StateVersion: 1, Created: now, @@ -515,7 +515,7 @@ func TestWorkspaceSyncToFinalizeHandler_StorageNotConfigured(t *testing.T) { // Send request with manifest but no storage configured body := `{"manifest": {"version": "1.0", "files": [{"path": "test.txt", "size": 100, "hash": "sha256:abc123"}]}}` - req := httptest.NewRequest("POST", "/api/v1/agents/agent_finalize_no_storage/workspace/sync-to/finalize", strings.NewReader(body)) + req := httptest.NewRequest("POST", fmt.Sprintf("/api/v1/agents/%s/workspace/sync-to/finalize", tid("agent_finalize_no_storage")), strings.NewReader(body)) req.Header.Set("Authorization", "Bearer "+testWorkspaceDevToken) req.Header.Set("Content-Type", "application/json") rec := httptest.NewRecorder() @@ -534,14 +534,14 @@ func TestWorkspaceSyncToFinalizeHandler_AgentNotRunning(t *testing.T) { now := time.Now() // Create the project first - createTestProject(t, s, "project_finalize_stopped") + createTestProject(t, s, tid("project_finalize_stopped")) // Create a stopped agent agent := &store.Agent{ - ID: "agent_finalize_stopped", + ID: tid("agent_finalize_stopped"), Slug: "finalize-stopped-agent", Name: "stopped-agent", - ProjectID: "project_finalize_stopped", + ProjectID: tid("project_finalize_stopped"), Phase: string(state.PhaseStopped), StateVersion: 1, Created: now, @@ -552,7 +552,7 @@ func TestWorkspaceSyncToFinalizeHandler_AgentNotRunning(t *testing.T) { } body := `{"manifest": {"version": "1.0", "files": [{"path": "test.txt", "size": 100, "hash": "sha256:abc123"}]}}` - req := httptest.NewRequest("POST", "/api/v1/agents/agent_finalize_stopped/workspace/sync-to/finalize", strings.NewReader(body)) + req := httptest.NewRequest("POST", fmt.Sprintf("/api/v1/agents/%s/workspace/sync-to/finalize", tid("agent_finalize_stopped")), strings.NewReader(body)) req.Header.Set("Authorization", "Bearer "+testWorkspaceDevToken) req.Header.Set("Content-Type", "application/json") rec := httptest.NewRecorder() @@ -571,13 +571,13 @@ func TestWorkspaceMethodNotAllowed(t *testing.T) { now := time.Now() // Create the project first - createTestProject(t, s, "project_method") + createTestProject(t, s, tid("project_method")) agent := &store.Agent{ - ID: "agent_method_test", + ID: tid("agent_method_test"), Slug: "method-test-agent", Name: "test-agent", - ProjectID: "project_method", + ProjectID: tid("project_method"), Phase: string(state.PhaseRunning), StateVersion: 1, Created: now, @@ -594,24 +594,24 @@ func TestWorkspaceMethodNotAllowed(t *testing.T) { expectedStatus int }{ // workspace status - GET only - {"workspace status with POST", "POST", "/api/v1/agents/agent_method_test/workspace", http.StatusMethodNotAllowed}, - {"workspace status with PUT", "PUT", "/api/v1/agents/agent_method_test/workspace", http.StatusMethodNotAllowed}, - {"workspace status with DELETE", "DELETE", "/api/v1/agents/agent_method_test/workspace", http.StatusMethodNotAllowed}, + {"workspace status with POST", "POST", fmt.Sprintf("/api/v1/agents/%s/workspace", tid("agent_method_test")), http.StatusMethodNotAllowed}, + {"workspace status with PUT", "PUT", fmt.Sprintf("/api/v1/agents/%s/workspace", tid("agent_method_test")), http.StatusMethodNotAllowed}, + {"workspace status with DELETE", "DELETE", fmt.Sprintf("/api/v1/agents/%s/workspace", tid("agent_method_test")), http.StatusMethodNotAllowed}, // sync-from - POST only - {"sync-from with GET", "GET", "/api/v1/agents/agent_method_test/workspace/sync-from", http.StatusMethodNotAllowed}, - {"sync-from with PUT", "PUT", "/api/v1/agents/agent_method_test/workspace/sync-from", http.StatusMethodNotAllowed}, - {"sync-from with DELETE", "DELETE", "/api/v1/agents/agent_method_test/workspace/sync-from", http.StatusMethodNotAllowed}, + {"sync-from with GET", "GET", fmt.Sprintf("/api/v1/agents/%s/workspace/sync-from", tid("agent_method_test")), http.StatusMethodNotAllowed}, + {"sync-from with PUT", "PUT", fmt.Sprintf("/api/v1/agents/%s/workspace/sync-from", tid("agent_method_test")), http.StatusMethodNotAllowed}, + {"sync-from with DELETE", "DELETE", fmt.Sprintf("/api/v1/agents/%s/workspace/sync-from", tid("agent_method_test")), http.StatusMethodNotAllowed}, // sync-to - POST only - {"sync-to with GET", "GET", "/api/v1/agents/agent_method_test/workspace/sync-to", http.StatusMethodNotAllowed}, - {"sync-to with PUT", "PUT", "/api/v1/agents/agent_method_test/workspace/sync-to", http.StatusMethodNotAllowed}, - {"sync-to with DELETE", "DELETE", "/api/v1/agents/agent_method_test/workspace/sync-to", http.StatusMethodNotAllowed}, + {"sync-to with GET", "GET", fmt.Sprintf("/api/v1/agents/%s/workspace/sync-to", tid("agent_method_test")), http.StatusMethodNotAllowed}, + {"sync-to with PUT", "PUT", fmt.Sprintf("/api/v1/agents/%s/workspace/sync-to", tid("agent_method_test")), http.StatusMethodNotAllowed}, + {"sync-to with DELETE", "DELETE", fmt.Sprintf("/api/v1/agents/%s/workspace/sync-to", tid("agent_method_test")), http.StatusMethodNotAllowed}, // sync-to/finalize - POST only - {"finalize with GET", "GET", "/api/v1/agents/agent_method_test/workspace/sync-to/finalize", http.StatusMethodNotAllowed}, - {"finalize with PUT", "PUT", "/api/v1/agents/agent_method_test/workspace/sync-to/finalize", http.StatusMethodNotAllowed}, - {"finalize with DELETE", "DELETE", "/api/v1/agents/agent_method_test/workspace/sync-to/finalize", http.StatusMethodNotAllowed}, + {"finalize with GET", "GET", fmt.Sprintf("/api/v1/agents/%s/workspace/sync-to/finalize", tid("agent_method_test")), http.StatusMethodNotAllowed}, + {"finalize with PUT", "PUT", fmt.Sprintf("/api/v1/agents/%s/workspace/sync-to/finalize", tid("agent_method_test")), http.StatusMethodNotAllowed}, + {"finalize with DELETE", "DELETE", fmt.Sprintf("/api/v1/agents/%s/workspace/sync-to/finalize", tid("agent_method_test")), http.StatusMethodNotAllowed}, } for _, tt := range tests { @@ -635,13 +635,13 @@ func TestWorkspaceSyncToHandler_InvalidJSON(t *testing.T) { now := time.Now() // Create the project first - createTestProject(t, s, "project_invalid_json") + createTestProject(t, s, tid("project_invalid_json")) agent := &store.Agent{ - ID: "agent_invalid_json", + ID: tid("agent_invalid_json"), Slug: "invalid-json-agent", Name: "test-agent", - ProjectID: "project_invalid_json", + ProjectID: tid("project_invalid_json"), Phase: string(state.PhaseRunning), StateVersion: 1, Created: now, @@ -653,7 +653,7 @@ func TestWorkspaceSyncToHandler_InvalidJSON(t *testing.T) { // Send invalid JSON body := `{invalid json` - req := httptest.NewRequest("POST", "/api/v1/agents/agent_invalid_json/workspace/sync-to", strings.NewReader(body)) + req := httptest.NewRequest("POST", fmt.Sprintf("/api/v1/agents/%s/workspace/sync-to", tid("agent_invalid_json")), strings.NewReader(body)) req.Header.Set("Authorization", "Bearer "+testWorkspaceDevToken) req.Header.Set("Content-Type", "application/json") rec := httptest.NewRecorder() @@ -672,13 +672,13 @@ func TestWorkspaceSyncToFinalizeHandler_InvalidJSON(t *testing.T) { now := time.Now() // Create the project first - createTestProject(t, s, "project_finalize_invalid") + createTestProject(t, s, tid("project_finalize_invalid")) agent := &store.Agent{ - ID: "agent_finalize_invalid", + ID: tid("agent_finalize_invalid"), Slug: "finalize-invalid-agent", Name: "test-agent", - ProjectID: "project_finalize_invalid", + ProjectID: tid("project_finalize_invalid"), Phase: string(state.PhaseRunning), StateVersion: 1, Created: now, @@ -690,7 +690,7 @@ func TestWorkspaceSyncToFinalizeHandler_InvalidJSON(t *testing.T) { // Send invalid JSON body := `{not valid` - req := httptest.NewRequest("POST", "/api/v1/agents/agent_finalize_invalid/workspace/sync-to/finalize", strings.NewReader(body)) + req := httptest.NewRequest("POST", fmt.Sprintf("/api/v1/agents/%s/workspace/sync-to/finalize", tid("agent_finalize_invalid")), strings.NewReader(body)) req.Header.Set("Authorization", "Bearer "+testWorkspaceDevToken) req.Header.Set("Content-Type", "application/json") rec := httptest.NewRecorder() @@ -786,13 +786,13 @@ func TestWorkspaceUnknownAction(t *testing.T) { now := time.Now() // Create the project first - createTestProject(t, s, "project_unknown") + createTestProject(t, s, tid("project_unknown")) agent := &store.Agent{ - ID: "agent_unknown_action", + ID: tid("agent_unknown_action"), Slug: "unknown-action-agent", Name: "test-agent", - ProjectID: "project_unknown", + ProjectID: tid("project_unknown"), Phase: string(state.PhaseRunning), StateVersion: 1, Created: now, @@ -803,7 +803,7 @@ func TestWorkspaceUnknownAction(t *testing.T) { } // Request with unknown workspace action - req := httptest.NewRequest("POST", "/api/v1/agents/agent_unknown_action/workspace/unknown-action", nil) + req := httptest.NewRequest("POST", fmt.Sprintf("/api/v1/agents/%s/workspace/unknown-action", tid("agent_unknown_action")), nil) req.Header.Set("Authorization", "Bearer "+testWorkspaceDevToken) rec := httptest.NewRecorder() @@ -886,8 +886,8 @@ func TestSyncHubManagedWorkspaceBack_SkipsGitProject(t *testing.T) { // Create a git-backed project (has GitRemote) project := &store.Project{ - ID: "project-git-sync", - Slug: "project-git-sync", + ID: tid("project-git-sync"), + Slug: tid("project-git-sync"), Name: "Git Project", GitRemote: "github.com/test/repo", } @@ -897,7 +897,7 @@ func TestSyncHubManagedWorkspaceBack_SkipsGitProject(t *testing.T) { agent := &store.Agent{ ID: "agent-sync-1", - ProjectID: "project-git-sync", + ProjectID: tid("project-git-sync"), } // This should return without doing anything for git-backed projects @@ -911,8 +911,8 @@ func TestSyncHubManagedWorkspaceBack_SkipsColocatedBroker(t *testing.T) { // Create a hub-managed project project := &store.Project{ - ID: "project-colo-sync", - Slug: "project-colo-sync", + ID: tid("project-colo-sync"), + Slug: tid("project-colo-sync"), Name: "Hub Native Colo", // No GitRemote = hub-managed } @@ -922,7 +922,7 @@ func TestSyncHubManagedWorkspaceBack_SkipsColocatedBroker(t *testing.T) { // Create a broker with local path (colocated) broker := &store.RuntimeBroker{ - ID: "broker-colo", + ID: tid("broker-colo"), Name: "colo-broker", Slug: "colo-broker", Endpoint: "http://localhost:9800", @@ -932,8 +932,8 @@ func TestSyncHubManagedWorkspaceBack_SkipsColocatedBroker(t *testing.T) { t.Fatalf("failed to create broker: %v", err) } provider := &store.ProjectProvider{ - ProjectID: "project-colo-sync", - BrokerID: "broker-colo", + ProjectID: tid("project-colo-sync"), + BrokerID: tid("broker-colo"), BrokerName: "colo-broker", LocalPath: "/home/user/.scion", Status: store.BrokerStatusOnline, @@ -944,8 +944,8 @@ func TestSyncHubManagedWorkspaceBack_SkipsColocatedBroker(t *testing.T) { agent := &store.Agent{ ID: "agent-colo-sync", - ProjectID: "project-colo-sync", - RuntimeBrokerID: "broker-colo", + ProjectID: tid("project-colo-sync"), + RuntimeBrokerID: tid("broker-colo"), } // Should skip sync because broker has local path diff --git a/pkg/hubclient/agents.go b/pkg/hubclient/agents.go index f472753e1..cd07bc1c0 100644 --- a/pkg/hubclient/agents.go +++ b/pkg/hubclient/agents.go @@ -59,6 +59,9 @@ type AgentService interface { // Restart restarts an agent. Restart(ctx context.Context, agentID string) error + // ResetAuth injects a fresh token into a running agent without restarting. + ResetAuth(ctx context.Context, agentID string) error + // StopAll stops all running agents in scope. StopAll(ctx context.Context) (*StopAllResponse, error) @@ -437,6 +440,15 @@ func (s *agentService) Restart(ctx context.Context, agentID string) error { return apiclient.CheckResponse(resp) } +// ResetAuth injects a fresh token into a running agent without restarting. +func (s *agentService) ResetAuth(ctx context.Context, agentID string) error { + resp, err := s.c.post(ctx, s.agentPath(agentID)+"/reset-auth", nil, nil) + if err != nil { + return err + } + return apiclient.CheckResponse(resp) +} + // StopAll stops all running agents in scope. func (s *agentService) StopAll(ctx context.Context) (*StopAllResponse, error) { resp, err := s.c.post(ctx, s.agentsPath()+"/stop-all", nil, nil) diff --git a/pkg/hubclient/client.go b/pkg/hubclient/client.go index f0bfb7d2d..3a7bd0aab 100644 --- a/pkg/hubclient/client.go +++ b/pkg/hubclient/client.go @@ -344,6 +344,13 @@ func (c *client) Health(ctx context.Context) (*HealthResponse, error) { if err != nil { return nil, err } + if resp.StatusCode == 404 { + resp.Body.Close() + resp, err = c.get(ctx, "/health", nil) + if err != nil { + return nil, err + } + } return apiclient.DecodeResponse[HealthResponse](resp) } diff --git a/pkg/hubclient/projects.go b/pkg/hubclient/projects.go index 8f1b4971e..50575f362 100644 --- a/pkg/hubclient/projects.go +++ b/pkg/hubclient/projects.go @@ -164,6 +164,7 @@ type CreateProjectRequest struct { // UpdateProjectRequest is the request for updating a project. type UpdateProjectRequest struct { Name string `json:"name,omitempty"` + Slug string `json:"slug,omitempty"` Labels map[string]string `json:"labels,omitempty"` Annotations map[string]string `json:"annotations,omitempty"` Visibility string `json:"visibility,omitempty"` diff --git a/pkg/observability/dbmetrics/dbmetrics.go b/pkg/observability/dbmetrics/dbmetrics.go new file mode 100644 index 000000000..cfed73759 --- /dev/null +++ b/pkg/observability/dbmetrics/dbmetrics.go @@ -0,0 +1,272 @@ +/* +Copyright 2025 The Scion Authors. +*/ + +// Package dbmetrics provides Cloud Monitoring scaffolding for the Postgres +// LISTEN/NOTIFY observability requirement. +// +// It defines the OpenTelemetry metric instruments used to observe the +// notification pipeline (publish-to-deliver latency, notification counts, +// subscriber lag, listener reconnects, payload sizes) and the database +// connection pool (active/idle/waiting/max). +// +// The package is intentionally lightweight: it registers instruments against an +// OpenTelemetry MeterProvider and exposes a small Recorder interface so callers +// just invoke Record/Observe/Inc methods without touching the OTel SDK. When no +// MeterProvider is supplied (the safe default, e.g. when no GCP project or +// exporter is configured), a no-op MeterProvider is used so every call becomes a +// cheap no-op and nothing is exported. A Cloud Monitoring exporter can be wired +// into the MeterProvider later without any change to callers. +package dbmetrics + +import ( + "context" + "fmt" + + "go.opentelemetry.io/otel/attribute" + "go.opentelemetry.io/otel/metric" + "go.opentelemetry.io/otel/metric/noop" +) + +// instrumentationName is the OTel instrumentation scope for this package. +const instrumentationName = "github.com/GoogleCloudPlatform/scion/pkg/observability/dbmetrics" + +// Metric names. Kept as constants so dashboards, alerts, and tests can reference +// the canonical strings. +const ( + MetricPublishToDeliverLatency = "scion.db.notify.publish_to_deliver.duration" + MetricNotificationsPublished = "scion.db.notify.published" + MetricNotificationsDelivered = "scion.db.notify.delivered" + MetricNotificationsDropped = "scion.db.notify.dropped" + MetricSubscriberLag = "scion.db.notify.subscriber.lag" + MetricListenerReconnects = "scion.db.notify.listener.reconnects" + MetricPayloadSize = "scion.db.notify.payload.size" + MetricPoolConnectionsActive = "scion.db.pool.connections.active" + MetricPoolConnectionsIdle = "scion.db.pool.connections.idle" + MetricPoolConnectionsWaiting = "scion.db.pool.connections.waiting" + MetricPoolConnectionsMax = "scion.db.pool.connections.max" +) + +// Recorder is the interface callers use to record Postgres LISTEN/NOTIFY and +// connection-pool metrics. All methods are safe to call concurrently and are +// cheap no-ops when metrics are disabled. +// +// The LISTEN/NOTIFY event-agent (P3-8) is the primary intended caller. +type Recorder interface { + // RecordPublishToDeliverLatency records the end-to-end latency, in + // milliseconds, between a notification being published to Postgres and it + // being delivered to a subscriber. + RecordPublishToDeliverLatency(ctx context.Context, ms float64, attrs ...attribute.KeyValue) + + // IncPublished increments the count of notifications published to Postgres. + IncPublished(ctx context.Context, n int64, attrs ...attribute.KeyValue) + // IncDelivered increments the count of notifications delivered to subscribers. + IncDelivered(ctx context.Context, n int64, attrs ...attribute.KeyValue) + // IncDropped increments the count of notifications dropped (e.g. full buffer, + // decode failure, no subscriber). + IncDropped(ctx context.Context, n int64, attrs ...attribute.KeyValue) + + // ObserveSubscriberLag records the current subscriber lag (number of + // notifications a subscriber is behind, or another caller-defined lag unit). + ObserveSubscriberLag(ctx context.Context, lag int64, attrs ...attribute.KeyValue) + + // IncListenerReconnects increments the count of LISTEN connection reconnects. + IncListenerReconnects(ctx context.Context, n int64, attrs ...attribute.KeyValue) + + // RecordPayloadSize records the size, in bytes, of a notification payload. + RecordPayloadSize(ctx context.Context, bytes int64, attrs ...attribute.KeyValue) + + // ObservePoolStats records a snapshot of the DB connection pool gauges. + ObservePoolStats(ctx context.Context, stats PoolStats, attrs ...attribute.KeyValue) + + // Enabled reports whether metrics are backed by a real (non-no-op) + // MeterProvider. Callers may use this to skip building attribute sets when + // nothing will be recorded. + Enabled() bool +} + +// PoolStats is a snapshot of database connection pool gauge values. +type PoolStats struct { + Active int64 // connections currently in use + Idle int64 // connections open but unused + Waiting int64 // goroutines/requests waiting for a connection + Max int64 // configured maximum pool size +} + +// recorder is the OpenTelemetry-backed implementation of Recorder. +type recorder struct { + enabled bool + + publishToDeliver metric.Float64Histogram + published metric.Int64Counter + delivered metric.Int64Counter + dropped metric.Int64Counter + subscriberLag metric.Int64Gauge + listenerReconn metric.Int64Counter + payloadSize metric.Int64Histogram + + poolActive metric.Int64Gauge + poolIdle metric.Int64Gauge + poolWaiting metric.Int64Gauge + poolMax metric.Int64Gauge +} + +// compile-time check that recorder satisfies Recorder. +var _ Recorder = (*recorder)(nil) + +// New creates a Recorder backed by the supplied MeterProvider. +// +// If mp is nil, a no-op MeterProvider is used: instruments still register +// successfully and every Record/Observe/Inc call becomes a cheap no-op. This is +// the safe default when no exporter (e.g. Cloud Monitoring) is configured, such +// as when no GCP project is set. +// +// New returns an error only if instrument registration fails, which should not +// happen with a well-behaved MeterProvider. +func New(mp metric.MeterProvider) (Recorder, error) { + enabled := mp != nil + if mp == nil { + mp = noop.NewMeterProvider() + } + + meter := mp.Meter(instrumentationName) + r := &recorder{enabled: enabled} + + var err error + + if r.publishToDeliver, err = meter.Float64Histogram( + MetricPublishToDeliverLatency, + metric.WithUnit("ms"), + metric.WithDescription("Latency from publishing a notification to delivering it to a subscriber"), + ); err != nil { + return nil, fmt.Errorf("registering %s: %w", MetricPublishToDeliverLatency, err) + } + + if r.published, err = meter.Int64Counter( + MetricNotificationsPublished, + metric.WithUnit("{notification}"), + metric.WithDescription("Number of notifications published to Postgres"), + ); err != nil { + return nil, fmt.Errorf("registering %s: %w", MetricNotificationsPublished, err) + } + + if r.delivered, err = meter.Int64Counter( + MetricNotificationsDelivered, + metric.WithUnit("{notification}"), + metric.WithDescription("Number of notifications delivered to subscribers"), + ); err != nil { + return nil, fmt.Errorf("registering %s: %w", MetricNotificationsDelivered, err) + } + + if r.dropped, err = meter.Int64Counter( + MetricNotificationsDropped, + metric.WithUnit("{notification}"), + metric.WithDescription("Number of notifications dropped before delivery"), + ); err != nil { + return nil, fmt.Errorf("registering %s: %w", MetricNotificationsDropped, err) + } + + if r.subscriberLag, err = meter.Int64Gauge( + MetricSubscriberLag, + metric.WithUnit("{notification}"), + metric.WithDescription("Current subscriber lag (notifications behind)"), + ); err != nil { + return nil, fmt.Errorf("registering %s: %w", MetricSubscriberLag, err) + } + + if r.listenerReconn, err = meter.Int64Counter( + MetricListenerReconnects, + metric.WithUnit("{reconnect}"), + metric.WithDescription("Number of LISTEN connection reconnects"), + ); err != nil { + return nil, fmt.Errorf("registering %s: %w", MetricListenerReconnects, err) + } + + if r.payloadSize, err = meter.Int64Histogram( + MetricPayloadSize, + metric.WithUnit("By"), + metric.WithDescription("Size of notification payloads in bytes"), + ); err != nil { + return nil, fmt.Errorf("registering %s: %w", MetricPayloadSize, err) + } + + if r.poolActive, err = meter.Int64Gauge( + MetricPoolConnectionsActive, + metric.WithUnit("{connection}"), + metric.WithDescription("Database connections currently in use"), + ); err != nil { + return nil, fmt.Errorf("registering %s: %w", MetricPoolConnectionsActive, err) + } + + if r.poolIdle, err = meter.Int64Gauge( + MetricPoolConnectionsIdle, + metric.WithUnit("{connection}"), + metric.WithDescription("Database connections open but idle"), + ); err != nil { + return nil, fmt.Errorf("registering %s: %w", MetricPoolConnectionsIdle, err) + } + + if r.poolWaiting, err = meter.Int64Gauge( + MetricPoolConnectionsWaiting, + metric.WithUnit("{request}"), + metric.WithDescription("Requests waiting for a database connection"), + ); err != nil { + return nil, fmt.Errorf("registering %s: %w", MetricPoolConnectionsWaiting, err) + } + + if r.poolMax, err = meter.Int64Gauge( + MetricPoolConnectionsMax, + metric.WithUnit("{connection}"), + metric.WithDescription("Configured maximum database pool size"), + ); err != nil { + return nil, fmt.Errorf("registering %s: %w", MetricPoolConnectionsMax, err) + } + + return r, nil +} + +// NewDisabled returns a Recorder whose calls are all no-ops. It is equivalent to +// New(nil) but never returns an error, which is convenient for tests and for +// call sites that want an explicit disabled recorder. +func NewDisabled() Recorder { + r, _ := New(nil) + return r +} + +func (r *recorder) Enabled() bool { return r.enabled } + +func (r *recorder) RecordPublishToDeliverLatency(ctx context.Context, ms float64, attrs ...attribute.KeyValue) { + r.publishToDeliver.Record(ctx, ms, metric.WithAttributes(attrs...)) +} + +func (r *recorder) IncPublished(ctx context.Context, n int64, attrs ...attribute.KeyValue) { + r.published.Add(ctx, n, metric.WithAttributes(attrs...)) +} + +func (r *recorder) IncDelivered(ctx context.Context, n int64, attrs ...attribute.KeyValue) { + r.delivered.Add(ctx, n, metric.WithAttributes(attrs...)) +} + +func (r *recorder) IncDropped(ctx context.Context, n int64, attrs ...attribute.KeyValue) { + r.dropped.Add(ctx, n, metric.WithAttributes(attrs...)) +} + +func (r *recorder) ObserveSubscriberLag(ctx context.Context, lag int64, attrs ...attribute.KeyValue) { + r.subscriberLag.Record(ctx, lag, metric.WithAttributes(attrs...)) +} + +func (r *recorder) IncListenerReconnects(ctx context.Context, n int64, attrs ...attribute.KeyValue) { + r.listenerReconn.Add(ctx, n, metric.WithAttributes(attrs...)) +} + +func (r *recorder) RecordPayloadSize(ctx context.Context, bytes int64, attrs ...attribute.KeyValue) { + r.payloadSize.Record(ctx, bytes, metric.WithAttributes(attrs...)) +} + +func (r *recorder) ObservePoolStats(ctx context.Context, stats PoolStats, attrs ...attribute.KeyValue) { + opt := metric.WithAttributes(attrs...) + r.poolActive.Record(ctx, stats.Active, opt) + r.poolIdle.Record(ctx, stats.Idle, opt) + r.poolWaiting.Record(ctx, stats.Waiting, opt) + r.poolMax.Record(ctx, stats.Max, opt) +} diff --git a/pkg/observability/dbmetrics/dbmetrics_test.go b/pkg/observability/dbmetrics/dbmetrics_test.go new file mode 100644 index 000000000..382ada63d --- /dev/null +++ b/pkg/observability/dbmetrics/dbmetrics_test.go @@ -0,0 +1,128 @@ +/* +Copyright 2025 The Scion Authors. +*/ + +package dbmetrics + +import ( + "context" + "testing" + + "go.opentelemetry.io/otel/attribute" + sdkmetric "go.opentelemetry.io/otel/sdk/metric" + "go.opentelemetry.io/otel/sdk/metric/metricdata" +) + +// TestNewDisabledRegisters verifies that the safe default (no MeterProvider, +// e.g. when no GCP project/exporter is configured) registers all instruments +// without error and reports itself disabled. +func TestNewDisabledRegisters(t *testing.T) { + r, err := New(nil) + if err != nil { + t.Fatalf("New(nil) returned error: %v", err) + } + if r == nil { + t.Fatal("New(nil) returned nil Recorder") + } + if r.Enabled() { + t.Error("expected Recorder backed by no-op provider to report Enabled()==false") + } +} + +// TestNewDisabledRecordsAreNoops ensures every method is safe to call when +// metrics are disabled (no panics, no errors). +func TestNewDisabledRecordsAreNoops(t *testing.T) { + r := NewDisabled() + ctx := context.Background() + attrs := []attribute.KeyValue{attribute.String("channel", "events")} + + // None of these should panic. + r.RecordPublishToDeliverLatency(ctx, 12.5, attrs...) + r.IncPublished(ctx, 1, attrs...) + r.IncDelivered(ctx, 1, attrs...) + r.IncDropped(ctx, 1, attrs...) + r.ObserveSubscriberLag(ctx, 3, attrs...) + r.IncListenerReconnects(ctx, 1, attrs...) + r.RecordPayloadSize(ctx, 256, attrs...) + r.ObservePoolStats(ctx, PoolStats{Active: 2, Idle: 8, Waiting: 0, Max: 10}, attrs...) +} + +// TestNewWithRealProviderRegisters verifies registration succeeds against a real +// SDK MeterProvider and that the resulting Recorder reports itself enabled. +func TestNewWithRealProviderRegisters(t *testing.T) { + reader := sdkmetric.NewManualReader() + mp := sdkmetric.NewMeterProvider(sdkmetric.WithReader(reader)) + t.Cleanup(func() { _ = mp.Shutdown(context.Background()) }) + + r, err := New(mp) + if err != nil { + t.Fatalf("New(mp) returned error: %v", err) + } + if !r.Enabled() { + t.Error("expected Recorder backed by real provider to report Enabled()==true") + } +} + +// TestRecordedMetricsAreExported drives every instrument and asserts the +// expected metric names show up in a collected snapshot. This proves the +// registration paths are wired correctly end-to-end. +func TestRecordedMetricsAreExported(t *testing.T) { + reader := sdkmetric.NewManualReader() + mp := sdkmetric.NewMeterProvider(sdkmetric.WithReader(reader)) + t.Cleanup(func() { _ = mp.Shutdown(context.Background()) }) + + r, err := New(mp) + if err != nil { + t.Fatalf("New(mp) returned error: %v", err) + } + + ctx := context.Background() + attrs := []attribute.KeyValue{attribute.String("channel", "events")} + + r.RecordPublishToDeliverLatency(ctx, 12.5, attrs...) + r.IncPublished(ctx, 2, attrs...) + r.IncDelivered(ctx, 1, attrs...) + r.IncDropped(ctx, 1, attrs...) + r.ObserveSubscriberLag(ctx, 5, attrs...) + r.IncListenerReconnects(ctx, 1, attrs...) + r.RecordPayloadSize(ctx, 256, attrs...) + r.ObservePoolStats(ctx, PoolStats{Active: 2, Idle: 8, Waiting: 1, Max: 10}, attrs...) + + var rm metricdata.ResourceMetrics + if err := reader.Collect(ctx, &rm); err != nil { + t.Fatalf("collecting metrics: %v", err) + } + + got := collectedNames(&rm) + + want := []string{ + MetricPublishToDeliverLatency, + MetricNotificationsPublished, + MetricNotificationsDelivered, + MetricNotificationsDropped, + MetricSubscriberLag, + MetricListenerReconnects, + MetricPayloadSize, + MetricPoolConnectionsActive, + MetricPoolConnectionsIdle, + MetricPoolConnectionsWaiting, + MetricPoolConnectionsMax, + } + + for _, name := range want { + if !got[name] { + t.Errorf("expected metric %q to be exported, but it was not present", name) + } + } +} + +// collectedNames flattens the collected metric names into a set for assertion. +func collectedNames(rm *metricdata.ResourceMetrics) map[string]bool { + names := make(map[string]bool) + for _, sm := range rm.ScopeMetrics { + for _, m := range sm.Metrics { + names[m.Name] = true + } + } + return names +} diff --git a/pkg/observability/dbmetrics/pool_sampler.go b/pkg/observability/dbmetrics/pool_sampler.go new file mode 100644 index 000000000..0990abb6f --- /dev/null +++ b/pkg/observability/dbmetrics/pool_sampler.go @@ -0,0 +1,79 @@ +/* +Copyright 2025 The Scion Authors. +*/ + +package dbmetrics + +import ( + "context" + "database/sql" + "time" + + "go.opentelemetry.io/otel/attribute" +) + +// DefaultPoolSampleInterval is the cadence used by StartPoolSampler when the +// caller passes a non-positive interval. +const DefaultPoolSampleInterval = 15 * time.Second + +// StatsProvider is the subset of *sql.DB needed to sample connection-pool +// gauges. *sql.DB satisfies it directly; tests can supply a fake. +type StatsProvider interface { + Stats() sql.DBStats +} + +// poolStatsFrom maps a database/sql DBStats snapshot onto the PoolStats gauge +// set understood by the Recorder. +// +// Note on Waiting: database/sql does not expose an instantaneous "callers +// currently blocked on a connection" gauge. WaitCount is the cumulative number +// of times a caller had to wait for a connection, which is the canonical +// pool-saturation signal: a flat WaitCount means the pool is never exhausted, a +// rising one means callers are queuing (the trigger for the pooler decision in +// CONNECTION-BUDGET.md). It is reported as-is so dashboards can rate() it. +func poolStatsFrom(s sql.DBStats) PoolStats { + return PoolStats{ + Active: int64(s.InUse), + Idle: int64(s.Idle), + Waiting: s.WaitCount, + Max: int64(s.MaxOpenConnections), + } +} + +// StartPoolSampler launches a goroutine that periodically snapshots db's +// connection-pool stats and records them via rec, until ctx is cancelled or the +// returned stop func is called (whichever happens first). stop is idempotent. +// +// It is the P3-6 wiring that feeds the P0-5 monitoring scaffold's pool gauges +// (scion.db.pool.connections.{active,idle,waiting,max}). When rec is disabled +// (no MeterProvider configured) or db is nil, sampling is skipped entirely so +// there is no idle goroutine in the common no-exporter case. +func StartPoolSampler(ctx context.Context, rec Recorder, db StatsProvider, interval time.Duration, attrs ...attribute.KeyValue) (stop func()) { + if rec == nil || !rec.Enabled() || db == nil { + return func() {} + } + if interval <= 0 { + interval = DefaultPoolSampleInterval + } + + sampleCtx, cancel := context.WithCancel(ctx) + go func() { + ticker := time.NewTicker(interval) + defer ticker.Stop() + + // Emit one snapshot immediately so the gauges are populated without + // waiting a full interval. + rec.ObservePoolStats(sampleCtx, poolStatsFrom(db.Stats()), attrs...) + + for { + select { + case <-sampleCtx.Done(): + return + case <-ticker.C: + rec.ObservePoolStats(sampleCtx, poolStatsFrom(db.Stats()), attrs...) + } + } + }() + + return cancel +} diff --git a/pkg/observability/dbmetrics/pool_sampler_test.go b/pkg/observability/dbmetrics/pool_sampler_test.go new file mode 100644 index 000000000..10f1beb68 --- /dev/null +++ b/pkg/observability/dbmetrics/pool_sampler_test.go @@ -0,0 +1,123 @@ +/* +Copyright 2025 The Scion Authors. +*/ + +package dbmetrics + +import ( + "context" + "database/sql" + "sync" + "testing" + "time" + + "go.opentelemetry.io/otel/attribute" +) + +// fakeStatsProvider returns a fixed DBStats snapshot. +type fakeStatsProvider struct{ s sql.DBStats } + +func (f fakeStatsProvider) Stats() sql.DBStats { return f.s } + +// capturingRecorder records ObservePoolStats calls and reports itself enabled. +// All other Recorder methods delegate to a disabled no-op recorder. +type capturingRecorder struct { + Recorder + mu sync.Mutex + got []PoolStats + seen chan struct{} +} + +func newCapturingRecorder() *capturingRecorder { + return &capturingRecorder{Recorder: NewDisabled(), seen: make(chan struct{}, 16)} +} + +func (c *capturingRecorder) Enabled() bool { return true } + +func (c *capturingRecorder) ObservePoolStats(_ context.Context, stats PoolStats, _ ...attribute.KeyValue) { + c.mu.Lock() + c.got = append(c.got, stats) + c.mu.Unlock() + select { + case c.seen <- struct{}{}: + default: + } +} + +func (c *capturingRecorder) snapshot() []PoolStats { + c.mu.Lock() + defer c.mu.Unlock() + out := make([]PoolStats, len(c.got)) + copy(out, c.got) + return out +} + +func TestPoolStatsFrom(t *testing.T) { + in := sql.DBStats{ + MaxOpenConnections: 20, + InUse: 7, + Idle: 3, + WaitCount: 42, + } + got := poolStatsFrom(in) + want := PoolStats{Active: 7, Idle: 3, Waiting: 42, Max: 20} + if got != want { + t.Fatalf("poolStatsFrom(%+v) = %+v, want %+v", in, got, want) + } +} + +// A disabled recorder must not start a sampling goroutine; stop is a safe no-op. +func TestStartPoolSampler_DisabledIsNoop(t *testing.T) { + stop := StartPoolSampler(context.Background(), NewDisabled(), fakeStatsProvider{}, time.Millisecond) + if stop == nil { + t.Fatal("stop must never be nil") + } + stop() // must not panic +} + +// A nil db must not start a goroutine. +func TestStartPoolSampler_NilDBIsNoop(t *testing.T) { + stop := StartPoolSampler(context.Background(), newCapturingRecorder(), nil, time.Millisecond) + stop() +} + +// An enabled recorder samples immediately (without waiting a full interval) and +// stop halts further sampling. +func TestStartPoolSampler_EmitsImmediatelyAndStops(t *testing.T) { + rec := newCapturingRecorder() + db := fakeStatsProvider{s: sql.DBStats{MaxOpenConnections: 10, InUse: 2, Idle: 1, WaitCount: 5}} + + stop := StartPoolSampler(context.Background(), rec, db, time.Hour) // long interval: rely on immediate emit + select { + case <-rec.seen: + case <-time.After(2 * time.Second): + t.Fatal("expected an immediate pool-stats sample") + } + stop() + + got := rec.snapshot() + if len(got) == 0 { + t.Fatal("expected at least one sample") + } + want := PoolStats{Active: 2, Idle: 1, Waiting: 5, Max: 10} + if got[0] != want { + t.Fatalf("first sample = %+v, want %+v", got[0], want) + } +} + +// Cancelling the parent context stops sampling. +func TestStartPoolSampler_ContextCancelStops(t *testing.T) { + rec := newCapturingRecorder() + db := fakeStatsProvider{s: sql.DBStats{MaxOpenConnections: 4}} + + ctx, cancel := context.WithCancel(context.Background()) + stop := StartPoolSampler(ctx, rec, db, time.Hour) + defer stop() + + select { + case <-rec.seen: + case <-time.After(2 * time.Second): + t.Fatal("expected an immediate sample") + } + cancel() // goroutine should observe ctx.Done and exit; no assertion beyond no-panic/no-leak +} diff --git a/pkg/observability/dispatchmetrics/dispatchmetrics.go b/pkg/observability/dispatchmetrics/dispatchmetrics.go new file mode 100644 index 000000000..4e7a15ae5 --- /dev/null +++ b/pkg/observability/dispatchmetrics/dispatchmetrics.go @@ -0,0 +1,206 @@ +/* +Copyright 2026 The Scion Authors. +*/ + +// Package dispatchmetrics provides Cloud Monitoring scaffolding for the +// multi-node broker-dispatch observability requirement (B5-2). +// +// It defines OpenTelemetry metric instruments for the dispatch pipeline: +// published/claimed/done/failed counters, intent-to-done latency histogram, +// message dispatched/stuck counters, command-bus reconnects, and reconcile +// drain duration. The package mirrors the dbmetrics pattern: a Recorder +// interface backed by an OTel MeterProvider (or no-op when none is supplied). +package dispatchmetrics + +import ( + "context" + "fmt" + + "go.opentelemetry.io/otel/attribute" + "go.opentelemetry.io/otel/metric" + "go.opentelemetry.io/otel/metric/noop" +) + +const instrumentationName = "github.com/GoogleCloudPlatform/scion/pkg/observability/dispatchmetrics" + +const ( + MetricDispatchPublished = "scion.dispatch.published" + MetricDispatchClaimed = "scion.dispatch.claimed" + MetricDispatchDone = "scion.dispatch.done" + MetricDispatchFailed = "scion.dispatch.failed" + MetricDispatchLatency = "scion.dispatch.intent_to_done.duration" + MetricMessageDispatched = "scion.dispatch.message.dispatched" + MetricMessageStuck = "scion.dispatch.message.stuck" + MetricCmdBusReconnects = "scion.dispatch.cmdbus.reconnects" + MetricReconcileDrainDur = "scion.dispatch.reconcile.drain.duration" +) + +// Recorder is the interface callers use to record broker-dispatch metrics. +// All methods are safe to call concurrently and are cheap no-ops when metrics +// are disabled. +type Recorder interface { + IncPublished(ctx context.Context, n int64, attrs ...attribute.KeyValue) + IncClaimed(ctx context.Context, n int64, attrs ...attribute.KeyValue) + IncDone(ctx context.Context, n int64, attrs ...attribute.KeyValue) + IncFailed(ctx context.Context, n int64, attrs ...attribute.KeyValue) + + RecordDispatchLatency(ctx context.Context, ms float64, attrs ...attribute.KeyValue) + + IncMessageDispatched(ctx context.Context, n int64, attrs ...attribute.KeyValue) + ObserveMessageStuck(ctx context.Context, n int64, attrs ...attribute.KeyValue) + + IncCmdBusReconnects(ctx context.Context, n int64, attrs ...attribute.KeyValue) + + RecordReconcileDrainDuration(ctx context.Context, ms float64, attrs ...attribute.KeyValue) + + Enabled() bool +} + +type recorder struct { + enabled bool + + published metric.Int64Counter + claimed metric.Int64Counter + done metric.Int64Counter + failed metric.Int64Counter + latency metric.Float64Histogram + + msgDispatched metric.Int64Counter + msgStuck metric.Int64Gauge + + cmdBusReconn metric.Int64Counter + drainDur metric.Float64Histogram +} + +var _ Recorder = (*recorder)(nil) + +// New creates a Recorder backed by the supplied MeterProvider. If mp is nil, +// a no-op MeterProvider is used and every method becomes a cheap no-op. +func New(mp metric.MeterProvider) (Recorder, error) { + enabled := mp != nil + if mp == nil { + mp = noop.NewMeterProvider() + } + + meter := mp.Meter(instrumentationName) + r := &recorder{enabled: enabled} + var err error + + if r.published, err = meter.Int64Counter( + MetricDispatchPublished, + metric.WithUnit("{dispatch}"), + metric.WithDescription("Number of broker dispatch intents published"), + ); err != nil { + return nil, fmt.Errorf("registering %s: %w", MetricDispatchPublished, err) + } + + if r.claimed, err = meter.Int64Counter( + MetricDispatchClaimed, + metric.WithUnit("{dispatch}"), + metric.WithDescription("Number of broker dispatch intents claimed"), + ); err != nil { + return nil, fmt.Errorf("registering %s: %w", MetricDispatchClaimed, err) + } + + if r.done, err = meter.Int64Counter( + MetricDispatchDone, + metric.WithUnit("{dispatch}"), + metric.WithDescription("Number of broker dispatch intents completed"), + ); err != nil { + return nil, fmt.Errorf("registering %s: %w", MetricDispatchDone, err) + } + + if r.failed, err = meter.Int64Counter( + MetricDispatchFailed, + metric.WithUnit("{dispatch}"), + metric.WithDescription("Number of broker dispatch intents failed"), + ); err != nil { + return nil, fmt.Errorf("registering %s: %w", MetricDispatchFailed, err) + } + + if r.latency, err = meter.Float64Histogram( + MetricDispatchLatency, + metric.WithUnit("ms"), + metric.WithDescription("Latency from dispatch intent creation to completion"), + ); err != nil { + return nil, fmt.Errorf("registering %s: %w", MetricDispatchLatency, err) + } + + if r.msgDispatched, err = meter.Int64Counter( + MetricMessageDispatched, + metric.WithUnit("{message}"), + metric.WithDescription("Number of messages dispatched to remote broker"), + ); err != nil { + return nil, fmt.Errorf("registering %s: %w", MetricMessageDispatched, err) + } + + if r.msgStuck, err = meter.Int64Gauge( + MetricMessageStuck, + metric.WithUnit("{message}"), + metric.WithDescription("Number of messages stuck in pending state beyond threshold"), + ); err != nil { + return nil, fmt.Errorf("registering %s: %w", MetricMessageStuck, err) + } + + if r.cmdBusReconn, err = meter.Int64Counter( + MetricCmdBusReconnects, + metric.WithUnit("{reconnect}"), + metric.WithDescription("Number of command bus listener reconnects"), + ); err != nil { + return nil, fmt.Errorf("registering %s: %w", MetricCmdBusReconnects, err) + } + + if r.drainDur, err = meter.Float64Histogram( + MetricReconcileDrainDur, + metric.WithUnit("ms"), + metric.WithDescription("Duration of a reconcile broker drain cycle"), + ); err != nil { + return nil, fmt.Errorf("registering %s: %w", MetricReconcileDrainDur, err) + } + + return r, nil +} + +// NewDisabled returns a Recorder whose calls are all no-ops. +func NewDisabled() Recorder { + r, _ := New(nil) + return r +} + +func (r *recorder) Enabled() bool { return r.enabled } + +func (r *recorder) IncPublished(ctx context.Context, n int64, attrs ...attribute.KeyValue) { + r.published.Add(ctx, n, metric.WithAttributes(attrs...)) +} + +func (r *recorder) IncClaimed(ctx context.Context, n int64, attrs ...attribute.KeyValue) { + r.claimed.Add(ctx, n, metric.WithAttributes(attrs...)) +} + +func (r *recorder) IncDone(ctx context.Context, n int64, attrs ...attribute.KeyValue) { + r.done.Add(ctx, n, metric.WithAttributes(attrs...)) +} + +func (r *recorder) IncFailed(ctx context.Context, n int64, attrs ...attribute.KeyValue) { + r.failed.Add(ctx, n, metric.WithAttributes(attrs...)) +} + +func (r *recorder) RecordDispatchLatency(ctx context.Context, ms float64, attrs ...attribute.KeyValue) { + r.latency.Record(ctx, ms, metric.WithAttributes(attrs...)) +} + +func (r *recorder) IncMessageDispatched(ctx context.Context, n int64, attrs ...attribute.KeyValue) { + r.msgDispatched.Add(ctx, n, metric.WithAttributes(attrs...)) +} + +func (r *recorder) ObserveMessageStuck(ctx context.Context, n int64, attrs ...attribute.KeyValue) { + r.msgStuck.Record(ctx, n, metric.WithAttributes(attrs...)) +} + +func (r *recorder) IncCmdBusReconnects(ctx context.Context, n int64, attrs ...attribute.KeyValue) { + r.cmdBusReconn.Add(ctx, n, metric.WithAttributes(attrs...)) +} + +func (r *recorder) RecordReconcileDrainDuration(ctx context.Context, ms float64, attrs ...attribute.KeyValue) { + r.drainDur.Record(ctx, ms, metric.WithAttributes(attrs...)) +} diff --git a/pkg/observability/dispatchmetrics/dispatchmetrics_test.go b/pkg/observability/dispatchmetrics/dispatchmetrics_test.go new file mode 100644 index 000000000..958338103 --- /dev/null +++ b/pkg/observability/dispatchmetrics/dispatchmetrics_test.go @@ -0,0 +1,116 @@ +/* +Copyright 2026 The Scion Authors. +*/ + +package dispatchmetrics + +import ( + "context" + "testing" + + "go.opentelemetry.io/otel/attribute" + sdkmetric "go.opentelemetry.io/otel/sdk/metric" + "go.opentelemetry.io/otel/sdk/metric/metricdata" +) + +func TestNewDisabledRegisters(t *testing.T) { + r, err := New(nil) + if err != nil { + t.Fatalf("New(nil) returned error: %v", err) + } + if r == nil { + t.Fatal("New(nil) returned nil Recorder") + } + if r.Enabled() { + t.Error("expected Recorder backed by no-op provider to report Enabled()==false") + } +} + +func TestNewDisabledRecordsAreNoops(t *testing.T) { + r := NewDisabled() + ctx := context.Background() + attrs := []attribute.KeyValue{attribute.String("op", "start")} + + r.IncPublished(ctx, 1, attrs...) + r.IncClaimed(ctx, 1, attrs...) + r.IncDone(ctx, 1, attrs...) + r.IncFailed(ctx, 1, attrs...) + r.RecordDispatchLatency(ctx, 42.5, attrs...) + r.IncMessageDispatched(ctx, 1, attrs...) + r.ObserveMessageStuck(ctx, 3, attrs...) + r.IncCmdBusReconnects(ctx, 1) + r.RecordReconcileDrainDuration(ctx, 10.0) +} + +func TestNewWithRealProviderRegisters(t *testing.T) { + reader := sdkmetric.NewManualReader() + mp := sdkmetric.NewMeterProvider(sdkmetric.WithReader(reader)) + t.Cleanup(func() { _ = mp.Shutdown(context.Background()) }) + + r, err := New(mp) + if err != nil { + t.Fatalf("New(mp) returned error: %v", err) + } + if !r.Enabled() { + t.Error("expected Recorder backed by real provider to report Enabled()==true") + } +} + +func TestRecordedMetricsAreExported(t *testing.T) { + reader := sdkmetric.NewManualReader() + mp := sdkmetric.NewMeterProvider(sdkmetric.WithReader(reader)) + t.Cleanup(func() { _ = mp.Shutdown(context.Background()) }) + + r, err := New(mp) + if err != nil { + t.Fatalf("New(mp) returned error: %v", err) + } + + ctx := context.Background() + attrs := []attribute.KeyValue{attribute.String("op", "start")} + + r.IncPublished(ctx, 2, attrs...) + r.IncClaimed(ctx, 1, attrs...) + r.IncDone(ctx, 1, attrs...) + r.IncFailed(ctx, 1, attrs...) + r.RecordDispatchLatency(ctx, 42.5, attrs...) + r.IncMessageDispatched(ctx, 1, attrs...) + r.ObserveMessageStuck(ctx, 3, attrs...) + r.IncCmdBusReconnects(ctx, 1) + r.RecordReconcileDrainDuration(ctx, 10.0) + + var rm metricdata.ResourceMetrics + if err := reader.Collect(ctx, &rm); err != nil { + t.Fatalf("collecting metrics: %v", err) + } + + got := collectedNames(&rm) + + want := []string{ + MetricDispatchPublished, + MetricDispatchClaimed, + MetricDispatchDone, + MetricDispatchFailed, + MetricDispatchLatency, + MetricMessageDispatched, + MetricMessageStuck, + MetricCmdBusReconnects, + MetricReconcileDrainDur, + } + + for _, name := range want { + if !got[name] { + t.Errorf("expected metric %q to be exported, but it was not present", name) + } + } +} + +func collectedNames(rm *metricdata.ResourceMetrics) map[string]bool { + names := make(map[string]bool) + for _, sm := range rm.ScopeMetrics { + for _, m := range sm.Metrics { + names[m.Name] = true + } + } + return names +} diff --git a/pkg/plugin/config.go b/pkg/plugin/config.go index 5bbba2490..35c455856 100644 --- a/pkg/plugin/config.go +++ b/pkg/plugin/config.go @@ -74,6 +74,14 @@ type PluginInfo struct { // Scion logs a warning if the plugin targets a newer version. MinScionVersion string + // ChannelID is the message channel identifier this broker plugin handles. + // When set, the FanOutEventBus uses this value (instead of the plugin's + // registered name) to route outbound messages with a matching + // msg.Channel field. For example, a plugin registered as "chat-app" can + // set ChannelID to "gchat" so that messages with Channel="gchat" are + // routed to it. + ChannelID string + // Capabilities lists optional capabilities the plugin supports. Capabilities []string } diff --git a/pkg/provision/provision.go b/pkg/provision/provision.go new file mode 100644 index 000000000..0a5a586c6 --- /dev/null +++ b/pkg/provision/provision.go @@ -0,0 +1,615 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Package provision implements Tier-1 universal workspace provisioning. +// It is a config-free leaf package that depends only on stdlib, pkg/api, +// and pkg/store — deliberately avoiding pkg/config so that lean binaries +// (e.g. sciontool) can invoke provisioning without pulling in +// filesystem-based project path resolution. +package provision + +import ( + "context" + "fmt" + "log/slog" + "os" + "os/exec" + "path/filepath" + "strings" + "time" + + "github.com/GoogleCloudPlatform/scion/pkg/api" + "github.com/GoogleCloudPlatform/scion/pkg/store" +) + +// ProvisionSentinelFile is the name of the sentinel file written atomically +// after a successful workspace clone/setup. Its presence short-circuits +// subsequent ProvisionShared calls — the workspace is already ready. +const ProvisionSentinelFile = ".scion-provisioned" + +// provisionLockRetries is the number of times to retry acquiring the +// per-project advisory lock before giving up. Each retry sleeps briefly +// (provisionLockRetryDelay) to allow the current holder to finish. +const provisionLockRetries = 30 + +// provisionLockRetryDelay is the sleep between advisory lock acquisition +// retries. Provisioning (git clone) is typically short (seconds), so a +// short retry cadence is appropriate. +const provisionLockRetryDelay = 1 * time.Second + +// ResolvedWorkspace holds the deterministic path resolution result. +type ResolvedWorkspace struct { + // HostPath is the absolute host-side path for the workspace. + // For localBackend this is the existing project path (e.g. + // ~/.scion.projects//). For nfsBackend this is + // //. + HostPath string + + // ServerRelativePath is the path relative to the NFS export root. + // Empty for localBackend. For nfsBackend, e.g. "projects//workspace". + ServerRelativePath string + + // HostBase is the host mount prefix for NFS-backed workspaces + // (/). Empty for localBackend. + HostBase string + + // SharedDirs maps shared-dir name → resolved path info. + SharedDirs map[string]ResolvedSharedDir + + // Backend identifies which backend produced this resolution ("local" or "nfs"). + Backend string +} + +// ResolvedSharedDir holds path resolution for a single shared directory. +type ResolvedSharedDir struct { + // HostPath is the absolute host path for this shared dir. + HostPath string + + // ServerRelativePath is the NFS export-relative path (empty for local). + ServerRelativePath string +} + +// ProvisionInput holds parameters for workspace provisioning. +type ProvisionInput struct { + // Ctx is the context for cancellation and timeouts. Optional: when nil, + // ProvisionShared falls back to context.Background(). Keeping it as a struct + // field (rather than a ProvisionShared parameter) preserves the existing + // function signature for callers. + Ctx context.Context + + // Resolved is the output of a prior Resolve call. + Resolved ResolvedWorkspace + + // ProjectID is the project's stable UUID. + ProjectID string + + // AgentID is the agent's stable UUID. + AgentID string + + // AgentName is a human-readable agent name (used for worktree branch names). + AgentName string + + // Mode is the workspace sharing mode. + Mode store.WorkspaceSharingMode + + // GitClone holds git-clone config when the project is git-backed; nil otherwise. + GitClone *api.GitCloneConfig + + // Locker provides the per-project advisory lock for the NFS first-access + // provisioning guard (design §7, risk RN1). On Postgres-backed deployments + // this uses pg_try_advisory_lock(classid, objid) for cross-node mutual + // exclusion; on SQLite it's a no-op (single-writer serializes already). + // + // May be nil — ProvisionShared degrades to sentinel-only guarding + // (correct for single-node but NOT safe for multi-node). + Locker store.AdvisoryLocker + + // NFSUID and NFSGID are the stable NFS ownership values (default 1000:1000). + // Used for one-time chown of newly provisioned workspace directories. + NFSUID int + NFSGID int + + // SentinelDir overrides the directory where the provisioning sentinel file + // (.scion-provisioned) is written and checked. When empty, defaults to + // filepath.Dir(Resolved.HostPath) — the project root parent of the workspace + // dir. This is needed for k8s init containers where only the workspace dir + // itself is mounted (not its parent), so the sentinel must live inside the + // workspace mount. + SentinelDir string +} + +// ProvisionShared is the universal, vendor-agnostic workspace provisioning +// function (Tier 1). It ensures the workspace directory exists and is ready +// for use. For git projects this includes cloning/worktree setup. Idempotent. +// +// The flow implements the first-access provisioning guard: +// +// 1. Acquire per-project advisory lock (try with retry — provisioning is short). +// 2. If sentinel /.scion-provisioned exists → done (reuse). +// 3. Else: mkdir -p, git clone, chown 1000:1000, mode 0770, write sentinel. +// 4. Release lock. +// +// For WorktreePerAgent mode, the shared checkout is cloned once under the lock; +// each agent then adds its own git worktree (also under the lock, because +// worktree add/remove touches shared .git metadata). +// +// ClonePerAgent mode MUST NOT reach this path — it is node-local and handled +// by localBackend. An assert guards this. +// +// The flow is idempotent and race-safe: two agents for the same project +// starting on two different nodes contend on the advisory lock; exactly one +// clones, the second sees the sentinel and reuses the workspace. +func ProvisionShared(in ProvisionInput) error { + // Guard: ClonePerAgent must never use the NFS path. SelectWorkspaceBackend + // already routes it to localBackend, but assert here as defense in depth. + if in.Mode == store.SharingModeClonePerAgent { + return fmt.Errorf("ProvisionShared: ClonePerAgent mode must not use NFS backend " + + "(should be routed to localBackend by SelectWorkspaceBackend)") + } + + if in.Resolved.HostPath == "" { + return fmt.Errorf("ProvisionShared: Resolved.HostPath is required") + } + if in.ProjectID == "" { + return fmt.Errorf("ProvisionShared: ProjectID is required") + } + + // Determine the sentinel directory: explicit override or default to parent. + sentinelDir := in.SentinelDir + if sentinelDir == "" { + // The project root is the parent of the workspace dir: + // //// contains workspace/ + shared-dirs/. + sentinelDir = filepath.Dir(in.Resolved.HostPath) + } + + ctx := in.Ctx + if ctx == nil { + ctx = context.Background() + } + + // --- Step 1: Acquire per-project advisory lock --- + release, err := acquireProvisionLock(ctx, in) + if err != nil { + return fmt.Errorf("ProvisionShared: failed to acquire lock for project %s: %w", in.ProjectID, err) + } + defer func() { + if releaseErr := release(); releaseErr != nil { + slog.Warn("ProvisionShared: failed to release advisory lock", + "project_id", in.ProjectID, "error", releaseErr) + } + }() + + // --- Step 2: Check sentinel --- + sentinelPath := filepath.Join(sentinelDir, ProvisionSentinelFile) + if _, err := os.Stat(sentinelPath); err == nil { + // Already provisioned — skip to worktree setup if needed. + slog.Debug("ProvisionShared: workspace already provisioned (sentinel exists)", + "project_id", in.ProjectID, "sentinel", sentinelPath) + return ensureWorktree(ctx, in) + } + + // --- Step 3: Provision (mkdir + clone + chown + sentinel) --- + slog.Info("ProvisionShared: provisioning workspace", + "project_id", in.ProjectID, "host_path", in.Resolved.HostPath) + + // Create workspace directory. + if err := os.MkdirAll(in.Resolved.HostPath, 0770); err != nil { + return fmt.Errorf("ProvisionShared: mkdir workspace %s: %w", in.Resolved.HostPath, err) + } + + // Create shared-dir directories. + for name, sd := range in.Resolved.SharedDirs { + if err := os.MkdirAll(sd.HostPath, 0770); err != nil { + return fmt.Errorf("ProvisionShared: mkdir shared-dir %q %s: %w", name, sd.HostPath, err) + } + } + + // Git clone if project is git-backed. + if in.GitClone != nil && in.GitClone.URL != "" { + if err := gitCloneWorkspace(ctx, in); err != nil { + return fmt.Errorf("ProvisionShared: git clone: %w", err) + } + } + + // For worktree-per-agent: detach HEAD, disable gc, exclude worktrees/. + if in.Mode == store.SharingModeWorktreePerAgent { + if err := prepareBaseForWorktrees(ctx, in.Resolved.HostPath); err != nil { + return fmt.Errorf("ProvisionShared: prepare base: %w", err) + } + } + + // Chown to stable NFS UID/GID (design §9.1). This is a ONE-TIME operation + // under the advisory lock — per-start chown is skipped for NFS (see N1-5). + // + chownRoot := chownTarget(in.Resolved.HostPath) + uid, gid := resolveUID(in), resolveGID(in) + if err := chownProjectTree(ctx, chownRoot, uid, gid); err != nil { + slog.Warn("ProvisionShared: chown failed (non-fatal, may lack privileges)", + "project_id", in.ProjectID, "path", chownRoot, "uid", uid, "gid", gid, "error", err) + // Non-fatal: operator may have pre-chowned. Continue to write sentinel. + } + + // Write sentinel atomically. + if err := writeSentinel(sentinelPath); err != nil { + return fmt.Errorf("ProvisionShared: write sentinel: %w", err) + } + + slog.Info("ProvisionShared: workspace provisioned successfully", + "project_id", in.ProjectID, "host_path", in.Resolved.HostPath) + + // --- Step 4: Worktree setup (if WorktreePerAgent) --- + return ensureWorktree(ctx, in) +} + +// acquireProvisionLock acquires the per-project advisory lock, retrying briefly +// if another node currently holds it. Returns a release func. +// +// The retry loop respects context cancellation so that server shutdown is not +// blocked for up to provisionLockRetries × provisionLockRetryDelay. +func acquireProvisionLock(ctx context.Context, in ProvisionInput) (func() error, error) { + if in.Locker == nil { + // No locker available — degrade to unguarded (correct for single-node, + // unsafe for multi-node). Log a warning. + slog.Warn("ProvisionShared: no advisory locker available — provisioning is unguarded", + "project_id", in.ProjectID) + return func() error { return nil }, nil + } + + objID := store.StableProjectHash(in.ProjectID) + ticker := time.NewTicker(provisionLockRetryDelay) + defer ticker.Stop() + + for attempt := 0; attempt < provisionLockRetries; attempt++ { + acquired, release, err := in.Locker.TryAdvisoryLockObject(ctx, store.LockWorkspaceProvision, objID) + if err != nil { + return nil, fmt.Errorf("advisory lock attempt %d: %w", attempt, err) + } + if acquired { + return release, nil + } + // Another node holds the lock — it's provisioning this project. + // Wait briefly and retry, but honour context cancellation. + slog.Debug("ProvisionShared: lock held by another node, retrying", + "project_id", in.ProjectID, "attempt", attempt+1) + select { + case <-ctx.Done(): + return nil, fmt.Errorf("context cancelled while waiting for provisioning lock (project %s): %w", + in.ProjectID, ctx.Err()) + case <-ticker.C: + } + } + + return nil, fmt.Errorf("failed to acquire provisioning lock after %d attempts (project %s)", + provisionLockRetries, in.ProjectID) +} + +// gitCloneWorkspace performs the git clone into the workspace directory. +// It clones into the workspace path (in.Resolved.HostPath). The clone runs +// under ctx via exec.CommandContext so that a cancelled/timed-out context +// kills the git process instead of leaving it orphaned. +func gitCloneWorkspace(ctx context.Context, in ProvisionInput) error { + gc := in.GitClone + + runClone := func() ([]byte, error) { + args := []string{"clone"} + + // Set depth (default: 1 for shallow clone, 0 = full). + depth := gc.Depth + if depth == 0 { + depth = 1 + } + if depth > 0 { + args = append(args, "--depth", fmt.Sprintf("%d", depth)) + } + + // Set branch if specified. + if gc.Branch != "" { + args = append(args, "--branch", gc.Branch) + } + + // Clone into the workspace directory. + args = append(args, gc.URL, in.Resolved.HostPath) + + cmd := exec.CommandContext(ctx, "git", args...) + cmd.Env = append(os.Environ(), + // Disable interactive prompts during provisioning. + "GIT_TERMINAL_PROMPT=0", + ) + return cmd.CombinedOutput() + } + + output, err := runClone() + if err == nil { + return nil + } + + // If the workspace is not empty, the clone fails with "already exists and + // is not an empty directory". This happens after a partially-failed prior + // attempt (the sentinel was never written, else we'd have skipped cloning). + if strings.Contains(string(output), "already exists and is not an empty directory") { + // If .git is present a prior clone completed — reuse it as-is. + if _, statErr := os.Stat(filepath.Join(in.Resolved.HostPath, ".git")); statErr == nil { + slog.Warn("ProvisionShared: workspace not empty but .git present, reusing prior clone", + "project_id", in.ProjectID, "path", in.Resolved.HostPath) + return nil + } + + // No .git — the prior attempt died mid-clone, leaving partial contents + // behind. Clear the directory so provisioning self-heals on retry + // without manual intervention, then clone once more. + slog.Warn("ProvisionShared: workspace not empty and no .git (incomplete prior clone), cleaning and retrying", + "project_id", in.ProjectID, "path", in.Resolved.HostPath) + if cleanErr := removeDirContents(in.Resolved.HostPath); cleanErr != nil { + return fmt.Errorf("git clone failed (dir not empty) and cleanup of %s failed: %w", + in.Resolved.HostPath, cleanErr) + } + if output, err = runClone(); err == nil { + return nil + } + return fmt.Errorf("git clone %s (after cleanup retry): %s", gc.URL, strings.TrimSpace(string(output))) + } + + return fmt.Errorf("git clone %s: %s", gc.URL, strings.TrimSpace(string(output))) +} + +// removeDirContents removes every entry inside dir while leaving dir itself in +// place. The workspace directory is frequently a mount point (e.g. a k8s PVC +// subPath), so it cannot be removed outright — only its contents can be cleared. +func removeDirContents(dir string) error { + entries, err := os.ReadDir(dir) + if err != nil { + return fmt.Errorf("read dir %s: %w", dir, err) + } + for _, e := range entries { + p := filepath.Join(dir, e.Name()) + if err := os.RemoveAll(p); err != nil { + return fmt.Errorf("remove %s: %w", p, err) + } + } + return nil +} + +// WorktreePath returns the canonical worktree path for a given agent within +// a shared base checkout: /worktrees/. +func WorktreePath(hostPath, agentID string) string { + return filepath.Join(hostPath, "worktrees", agentID) +} + +// ensureWorktree creates a per-agent worktree if the mode is WorktreePerAgent. +// For SharedPlain mode this is a no-op. +// The worktree add is done under the already-held advisory lock (design §9.2: +// worktree add/remove touches shared .git metadata). +func ensureWorktree(ctx context.Context, in ProvisionInput) error { + if in.Mode != store.SharingModeWorktreePerAgent { + return nil // SharedPlain: nothing to do + } + + if in.AgentID == "" { + return fmt.Errorf("ProvisionShared: AgentID is required for WorktreePerAgent mode") + } + + worktreePath := WorktreePath(in.Resolved.HostPath, in.AgentID) + + // If the worktree already exists, skip. + if _, err := os.Stat(worktreePath); err == nil { + slog.Debug("ProvisionShared: worktree already exists", + "agent_id", in.AgentID, "path", worktreePath) + return nil + } + + // Verify the shared checkout exists (.git dir present). + gitDir := filepath.Join(in.Resolved.HostPath, ".git") + if _, err := os.Stat(gitDir); err != nil { + return fmt.Errorf("ProvisionShared: shared checkout .git not found at %s — "+ + "cannot create worktree without a cloned repository", gitDir) + } + + // Derive a branch name from the agent name or ID. + branchName := in.AgentID + if in.AgentName != "" { + branchName = sanitizeBranchName(in.AgentName) + } + + // Ensure worktrees parent directory exists. + worktreesDir := filepath.Join(in.Resolved.HostPath, "worktrees") + if err := os.MkdirAll(worktreesDir, 0770); err != nil { + return fmt.Errorf("ProvisionShared: mkdir worktrees dir: %w", err) + } + + slog.Info("ProvisionShared: creating worktree", + "agent_id", in.AgentID, "branch", branchName, "path", worktreePath) + + // git worktree add --relative-paths -b + // --relative-paths is mandatory for container path-identity (design §6). + cmd := exec.CommandContext(ctx, "git", "worktree", "add", "--relative-paths", "-b", branchName, worktreePath) + cmd.Dir = in.Resolved.HostPath + output, err := cmd.CombinedOutput() + if err != nil { + outputStr := strings.TrimSpace(string(output)) + // If the branch is already checked out in another worktree, surface a + // clear error instead of a confusing git message. + if strings.Contains(outputStr, "already checked out") || strings.Contains(outputStr, "already used by worktree") { + return fmt.Errorf("git worktree add: branch %q is already checked out in another worktree: %s", + branchName, outputStr) + } + // If branch already exists, try without -b (reuse existing branch). + if strings.Contains(outputStr, "already exists") { + cmd = exec.CommandContext(ctx, "git", "worktree", "add", "--relative-paths", worktreePath, branchName) + cmd.Dir = in.Resolved.HostPath + output, err = cmd.CombinedOutput() + if err != nil { + reuse := strings.TrimSpace(string(output)) + if strings.Contains(reuse, "already checked out") || strings.Contains(reuse, "already used by worktree") { + return fmt.Errorf("git worktree add: branch %q is already checked out in another worktree: %s", + branchName, reuse) + } + return fmt.Errorf("git worktree add (reuse branch): %s", reuse) + } + return nil + } + return fmt.Errorf("git worktree add: %s", outputStr) + } + return nil +} + +// prepareBaseForWorktrees configures a freshly cloned base checkout for +// worktree-per-agent use: detaches HEAD (so no branch is "owned" by the base), +// disables auto-gc, and excludes worktrees/ from untracked file lists. +func prepareBaseForWorktrees(ctx context.Context, hostPath string) error { + if err := gitDetach(ctx, hostPath); err != nil { + return err + } + + cmd := exec.CommandContext(ctx, "git", "-C", hostPath, "config", "gc.auto", "0") + output, err := cmd.CombinedOutput() + if err != nil { + return fmt.Errorf("git config gc.auto 0: %s", strings.TrimSpace(string(output))) + } + + return appendGitExclude(hostPath, "worktrees/") +} + +// gitDetach detaches HEAD in the repo at hostPath so the base checkout owns +// no branch. Tries 'git switch --detach' first, falls back to 'git checkout +// --detach' for older git versions. +func gitDetach(ctx context.Context, hostPath string) error { + cmd := exec.CommandContext(ctx, "git", "-C", hostPath, "switch", "--detach") + if _, err := cmd.CombinedOutput(); err == nil { + return nil + } + cmd = exec.CommandContext(ctx, "git", "-C", hostPath, "checkout", "--detach") + output, err := cmd.CombinedOutput() + if err != nil { + return fmt.Errorf("git detach: %s", strings.TrimSpace(string(output))) + } + return nil +} + +// appendGitExclude appends a pattern to .git/info/exclude if not already present. +func appendGitExclude(hostPath, pattern string) error { + excludePath := filepath.Join(hostPath, ".git", "info", "exclude") + if err := os.MkdirAll(filepath.Dir(excludePath), 0755); err != nil { + return fmt.Errorf("mkdir .git/info: %w", err) + } + data, _ := os.ReadFile(excludePath) + // Exact line match — strings.Contains would false-positive on e.g. + // "my-worktrees/" or "worktrees/agent-1" and skip appending the pattern. + for _, line := range strings.Split(string(data), "\n") { + if strings.TrimSpace(line) == strings.TrimSpace(pattern) { + return nil + } + } + f, err := os.OpenFile(excludePath, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644) + if err != nil { + return err + } + defer f.Close() + if len(data) > 0 && data[len(data)-1] != '\n' { + if _, err := f.WriteString("\n"); err != nil { + return err + } + } + _, err = f.WriteString(pattern + "\n") + return err +} + +// sanitizeBranchName produces a git-safe branch name from an agent name. +func sanitizeBranchName(name string) string { + // Replace characters invalid in git branch names. + replacer := strings.NewReplacer( + " ", "-", "/", "-", "\\", "-", "..", "-", + "~", "-", "^", "-", ":", "-", "?", "-", + "*", "-", "[", "-", "]", "-", + ) + result := replacer.Replace(name) + // Trim leading/trailing dashes and dots. + result = strings.Trim(result, "-.") + if result == "" { + return "agent" + } + return result +} + +// chownTarget returns the directory to recursively chown for a freshly +// provisioned workspace. +// +// Broker-side, the project root is the parent of the workspace dir (it also +// holds the shared-dirs siblings), so we chown the parent. But inside a k8s +// init container only the workspace dir itself is mounted (subPath), so its +// parent resolves to the filesystem root "/". Chowning "/" recursively is +// wrong — and a latent security hazard if the pod's security context is ever +// relaxed — so fall back to chowning the workspace dir itself in that case. +func chownTarget(hostPath string) string { + parent := filepath.Dir(hostPath) + if parent == "/" || parent == "." { + return hostPath + } + return parent +} + +// chownProjectTree sets ownership of the project root and its contents to the +// given UID/GID. This is a ONE-TIME operation done under the advisory lock +// during first provisioning (design §9.1). Per-start chown is NOT done for +// NFS (slow/racy over the network). +func chownProjectTree(ctx context.Context, projectRoot string, uid, gid int) error { + // Use chown -R for recursive ownership change. + cmd := exec.CommandContext(ctx, "chown", "-R", fmt.Sprintf("%d:%d", uid, gid), projectRoot) + output, err := cmd.CombinedOutput() + if err != nil { + return fmt.Errorf("chown -R %d:%d %s: %s", uid, gid, projectRoot, strings.TrimSpace(string(output))) + } + return nil +} + +// resolveUID returns the NFS UID to use for chown, defaulting to 1000. +func resolveUID(in ProvisionInput) int { + if in.NFSUID != 0 { + return in.NFSUID + } + return 1000 +} + +// resolveGID returns the NFS GID to use for chown, defaulting to 1000. +func resolveGID(in ProvisionInput) int { + if in.NFSGID != 0 { + return in.NFSGID + } + return 1000 +} + +// writeSentinel writes the provisioning sentinel file atomically using +// write-to-temp + rename. The sentinel's existence is the fast-path check +// that short-circuits re-provisioning. +func writeSentinel(path string) error { + dir := filepath.Dir(path) + tmp, err := os.CreateTemp(dir, ".scion-provisioned-*") + if err != nil { + return fmt.Errorf("create temp sentinel: %w", err) + } + tmpName := tmp.Name() + + // Write a timestamp for debugging. + _, _ = fmt.Fprintf(tmp, "provisioned_at=%s\n", time.Now().UTC().Format(time.RFC3339)) + if err := tmp.Close(); err != nil { + _ = os.Remove(tmpName) + return fmt.Errorf("close temp sentinel: %w", err) + } + + // Atomic rename. + if err := os.Rename(tmpName, path); err != nil { + _ = os.Remove(tmpName) + return fmt.Errorf("rename sentinel: %w", err) + } + return nil +} diff --git a/pkg/provision/provision_test.go b/pkg/provision/provision_test.go new file mode 100644 index 000000000..4cdf216a0 --- /dev/null +++ b/pkg/provision/provision_test.go @@ -0,0 +1,600 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package provision + +import ( + "context" + "fmt" + "os" + "os/exec" + "path/filepath" + "strings" + "sync" + "sync/atomic" + "testing" + "time" + + "github.com/GoogleCloudPlatform/scion/pkg/api" + "github.com/GoogleCloudPlatform/scion/pkg/store" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +// testLocker is a mock AdvisoryLocker for testing. +type testLocker struct { + mu sync.Mutex + held map[lockKey]bool + acquires int64 +} + +type lockKey struct { + classID int64 + objID int32 + single bool +} + +func newTestLocker() *testLocker { + return &testLocker{held: make(map[lockKey]bool)} +} + +func (l *testLocker) TryAdvisoryLock(ctx context.Context, key store.AdvisoryLockKey) (bool, func() error, error) { + k := lockKey{classID: int64(key), single: true} + l.mu.Lock() + defer l.mu.Unlock() + if l.held[k] { + return false, func() error { return nil }, nil + } + l.held[k] = true + atomic.AddInt64(&l.acquires, 1) + return true, func() error { + l.mu.Lock() + defer l.mu.Unlock() + delete(l.held, k) + return nil + }, nil +} + +func (l *testLocker) TryAdvisoryLockObject(ctx context.Context, classID store.AdvisoryLockKey, objID int32) (bool, func() error, error) { + k := lockKey{classID: int64(classID), objID: objID} + l.mu.Lock() + defer l.mu.Unlock() + if l.held[k] { + return false, func() error { return nil }, nil + } + l.held[k] = true + atomic.AddInt64(&l.acquires, 1) + return true, func() error { + l.mu.Lock() + defer l.mu.Unlock() + delete(l.held, k) + return nil + }, nil +} + +// initBareGitRepo creates a bare git repo at a temporary path for cloning from. +func initBareGitRepo(t *testing.T) string { + t.Helper() + dir := t.TempDir() + bareDir := filepath.Join(dir, "bare.git") + run(t, "git", "init", "--bare", "--initial-branch=main", bareDir) + + workDir := filepath.Join(dir, "work") + run(t, "git", "clone", bareDir, workDir) + + f := filepath.Join(workDir, "README.md") + if err := os.WriteFile(f, []byte("# Test\n"), 0644); err != nil { + t.Fatal(err) + } + runIn(t, workDir, "git", "add", "README.md") + runIn(t, workDir, "git", "-c", "user.name=test", "-c", "user.email=test@test.com", + "commit", "-m", "initial") + runIn(t, workDir, "git", "push", "origin", "main") + + return bareDir +} + +func run(t *testing.T, name string, args ...string) { + t.Helper() + cmd := exec.Command(name, args...) + cmd.Env = append(os.Environ(), "GIT_TERMINAL_PROMPT=0") + if output, err := cmd.CombinedOutput(); err != nil { + t.Fatalf("%s %v: %s\n%s", name, args, err, output) + } +} + +func runIn(t *testing.T, dir, name string, args ...string) { + t.Helper() + cmd := exec.Command(name, args...) + cmd.Dir = dir + cmd.Env = append(os.Environ(), "GIT_TERMINAL_PROMPT=0") + if output, err := cmd.CombinedOutput(); err != nil { + t.Fatalf("%s %v (in %s): %s\n%s", name, args, dir, err, output) + } +} + +// --- ClonePerAgent rejection --- + +func TestProvision_RejectsClonePerAgent(t *testing.T) { + err := ProvisionShared(ProvisionInput{ + ProjectID: "proj-1", + Mode: store.SharingModeClonePerAgent, + Resolved: ResolvedWorkspace{ + HostPath: "/some/path", + }, + }) + if err == nil { + t.Fatal("expected error for ClonePerAgent on NFS backend") + } + if !strings.Contains(err.Error(), "ClonePerAgent") { + t.Errorf("error should mention ClonePerAgent, got: %v", err) + } +} + +// --- Missing required fields --- + +func TestProvision_MissingHostPath(t *testing.T) { + err := ProvisionShared(ProvisionInput{ + ProjectID: "proj-1", + Mode: store.SharingModeSharedPlain, + Resolved: ResolvedWorkspace{}, + }) + if err == nil { + t.Fatal("expected error for empty HostPath") + } +} + +func TestProvision_MissingProjectID(t *testing.T) { + err := ProvisionShared(ProvisionInput{ + Mode: store.SharingModeSharedPlain, + Resolved: ResolvedWorkspace{ + HostPath: "/some/path", + }, + }) + if err == nil { + t.Fatal("expected error for empty ProjectID") + } +} + +// --- sanitizeBranchName --- + +func TestSanitizeBranchName(t *testing.T) { + tests := []struct { + input string + want string + }{ + {"simple", "simple"}, + {"with spaces", "with-spaces"}, + {"with/slash", "with-slash"}, + {"with..dots", "with-dots"}, + {"with~tilde", "with-tilde"}, + {".leading-dot", "leading-dot"}, + {"-leading-dash", "leading-dash"}, + {"trailing-.", "trailing"}, + {"", "agent"}, + } + + for _, tt := range tests { + t.Run(tt.input, func(t *testing.T) { + got := sanitizeBranchName(tt.input) + if got != tt.want { + t.Errorf("sanitizeBranchName(%q) = %q, want %q", tt.input, got, tt.want) + } + }) + } +} + +func TestChownTarget(t *testing.T) { + tests := []struct { + name string + hostPath string + want string + }{ + // Broker-side: chown the project root (parent of the workspace dir). + {"broker project root", "/srv/nfs/share1/proj-abc/workspace", "/srv/nfs/share1/proj-abc"}, + // k8s init container subPath mount: parent is "/", fall back to the + // workspace dir itself rather than chown -R the whole container root. + {"k8s workspace mount", "/workspace", "/workspace"}, + // Relative path has no real parent ("."); fall back to the path itself. + {"relative path", "workspace", "workspace"}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := chownTarget(tt.hostPath); got != tt.want { + t.Errorf("chownTarget(%q) = %q, want %q", tt.hostPath, got, tt.want) + } + }) + } +} + +// --- writeSentinel --- + +func TestWriteSentinel_Atomic(t *testing.T) { + dir := t.TempDir() + path := filepath.Join(dir, ProvisionSentinelFile) + + if err := writeSentinel(path); err != nil { + t.Fatalf("writeSentinel: %v", err) + } + + data, err := os.ReadFile(path) + if err != nil { + t.Fatalf("read sentinel: %v", err) + } + if !strings.Contains(string(data), "provisioned_at=") { + t.Errorf("sentinel content unexpected: %s", string(data)) + } + + // Overwrite should also work (idempotent). + if err := writeSentinel(path); err != nil { + t.Fatalf("writeSentinel overwrite: %v", err) + } +} + +// --- acquireProvisionLock context cancellation --- + +// alwaysLoseLocker is an AdvisoryLocker where TryAdvisoryLockObject always +// returns acquired=false (another node holds the lock). +type alwaysLoseLocker struct{} + +func (l *alwaysLoseLocker) TryAdvisoryLock(_ context.Context, _ store.AdvisoryLockKey) (bool, func() error, error) { + return false, func() error { return nil }, nil +} + +func (l *alwaysLoseLocker) TryAdvisoryLockObject(_ context.Context, _ store.AdvisoryLockKey, _ int32) (bool, func() error, error) { + return false, func() error { return nil }, nil +} + +func TestAcquireProvisionLock_ContextCancellation(t *testing.T) { + locker := &alwaysLoseLocker{} + + ctx, cancel := context.WithCancel(context.Background()) + cancel() + + in := ProvisionInput{ + ProjectID: "proj-cancel-test", + Locker: locker, + } + + start := time.Now() + _, err := acquireProvisionLock(ctx, in) + elapsed := time.Since(start) + + require.Error(t, err) + assert.Contains(t, err.Error(), "context cancelled") + assert.Less(t, elapsed, 2*time.Second, "should return promptly on context cancellation, not wait for all retries") +} + +// --- WorktreePerAgent: creates worktree on shared checkout --- + +func TestProvision_WorktreePerAgent(t *testing.T) { + t.Setenv("SCION_HOST_UID", "") + locker := newTestLocker() + bareRepo := initBareGitRepo(t) + + projectDir := t.TempDir() + hostPath := filepath.Join(projectDir, "workspace") + + err := ProvisionShared(ProvisionInput{ + Resolved: ResolvedWorkspace{HostPath: hostPath, Backend: "local"}, + ProjectID: "proj-wt-1", + AgentID: "agent-wt-1", + AgentName: "test-agent", + Mode: store.SharingModeWorktreePerAgent, + Locker: locker, + GitClone: &api.GitCloneConfig{ + URL: bareRepo, + Branch: "main", + Depth: 0, + }, + }) + if err != nil { + t.Fatalf("Provision: %v", err) + } + + // Verify base HEAD is detached. + cmd := exec.Command("git", "-C", hostPath, "symbolic-ref", "HEAD") + if err := cmd.Run(); err == nil { + t.Error("expected HEAD to be detached in base, but symbolic-ref succeeded") + } + + // Verify gc.auto is disabled. + out, err := exec.Command("git", "-C", hostPath, "config", "gc.auto").Output() + if err != nil || strings.TrimSpace(string(out)) != "0" { + t.Errorf("expected gc.auto=0 in base repo, got %q (err=%v)", strings.TrimSpace(string(out)), err) + } + + // Verify worktree was created. + worktreePath := WorktreePath(hostPath, "agent-wt-1") + if _, err := os.Stat(worktreePath); err != nil { + t.Fatalf("worktree not created at %s: %v", worktreePath, err) + } + + // Verify .git is a file (pointer), not a directory. + gitFile := filepath.Join(worktreePath, ".git") + fi, err := os.Lstat(gitFile) + if err != nil { + t.Fatalf("worktree .git not found: %v", err) + } + if fi.IsDir() { + t.Error("worktree .git should be a file (pointer), not a directory") + } + + // Verify .git pointer uses a relative path (--relative-paths). + data, err := os.ReadFile(gitFile) + if err != nil { + t.Fatalf("read worktree .git: %v", err) + } + gitdirLine := strings.TrimSpace(string(data)) + if !strings.HasPrefix(gitdirLine, "gitdir: ") { + t.Fatalf("unexpected .git content: %s", gitdirLine) + } + gitdirPath := strings.TrimPrefix(gitdirLine, "gitdir: ") + if filepath.IsAbs(gitdirPath) { + t.Errorf("worktree .git should use a relative path, got: %s", gitdirPath) + } +} + +// --- WorktreePerAgent: second agent gets independent worktree --- + +func TestProvision_WorktreePerAgent_TwoAgents(t *testing.T) { + t.Setenv("SCION_HOST_UID", "") + locker := newTestLocker() + bareRepo := initBareGitRepo(t) + + projectDir := t.TempDir() + hostPath := filepath.Join(projectDir, "workspace") + + // First agent. + err := ProvisionShared(ProvisionInput{ + Resolved: ResolvedWorkspace{HostPath: hostPath, Backend: "local"}, + ProjectID: "proj-wt-2", + AgentID: "agent-1", + AgentName: "first-agent", + Mode: store.SharingModeWorktreePerAgent, + Locker: locker, + GitClone: &api.GitCloneConfig{ + URL: bareRepo, + Branch: "main", + Depth: 0, + }, + }) + if err != nil { + t.Fatalf("Provision agent-1: %v", err) + } + + // Second agent (sentinel exists, so clone is skipped — just adds worktree). + err = ProvisionShared(ProvisionInput{ + Resolved: ResolvedWorkspace{HostPath: hostPath, Backend: "local"}, + ProjectID: "proj-wt-2", + AgentID: "agent-2", + AgentName: "second-agent", + Mode: store.SharingModeWorktreePerAgent, + Locker: locker, + GitClone: &api.GitCloneConfig{ + URL: bareRepo, + Branch: "main", + Depth: 0, + }, + }) + if err != nil { + t.Fatalf("Provision agent-2: %v", err) + } + + // Both worktrees exist and are independent. + wt1 := WorktreePath(hostPath, "agent-1") + wt2 := WorktreePath(hostPath, "agent-2") + if _, err := os.Stat(wt1); err != nil { + t.Errorf("worktree agent-1 not found: %v", err) + } + if _, err := os.Stat(wt2); err != nil { + t.Errorf("worktree agent-2 not found: %v", err) + } + + // Verify both worktrees have relative .git pointers. + for _, wt := range []string{wt1, wt2} { + data, err := os.ReadFile(filepath.Join(wt, ".git")) + if err != nil { + t.Errorf("read .git in %s: %v", wt, err) + continue + } + gitdirLine := strings.TrimSpace(string(data)) + if !strings.HasPrefix(gitdirLine, "gitdir: ") { + t.Errorf("unexpected .git content in %s: %s", wt, gitdirLine) + continue + } + gitdirPath := strings.TrimPrefix(gitdirLine, "gitdir: ") + if filepath.IsAbs(gitdirPath) { + t.Errorf("worktree %s .git should use relative path, got: %s", wt, gitdirPath) + } + } +} + +// --- Two projects sharing a parent dir: independent sentinels --- + +func TestProvision_WorktreePerAgent_TwoProjects(t *testing.T) { + t.Setenv("SCION_HOST_UID", "") + locker := newTestLocker() + bareRepoA := initBareGitRepo(t) + bareRepoB := initBareGitRepo(t) + + parentDir := t.TempDir() + projectDirA := filepath.Join(parentDir, "project-alpha") + projectDirB := filepath.Join(parentDir, "project-beta") + if err := os.MkdirAll(projectDirA, 0755); err != nil { + t.Fatal(err) + } + if err := os.MkdirAll(projectDirB, 0755); err != nil { + t.Fatal(err) + } + + hostPathA := filepath.Join(projectDirA, "workspace") + hostPathB := filepath.Join(projectDirB, "workspace") + + // --- Project A --- + err := ProvisionShared(ProvisionInput{ + Resolved: ResolvedWorkspace{HostPath: hostPathA, Backend: "local"}, + ProjectID: "proj-alpha", + AgentID: "agent-a1", + AgentName: "alpha-agent", + Mode: store.SharingModeWorktreePerAgent, + Locker: locker, + GitClone: &api.GitCloneConfig{URL: bareRepoA, Branch: "main", Depth: 0}, + }) + if err != nil { + t.Fatalf("Provision project A: %v", err) + } + + if _, err := os.Stat(filepath.Join(hostPathA, ".git")); err != nil { + t.Fatalf("project A: .git not found: %v", err) + } + if _, err := os.Stat(WorktreePath(hostPathA, "agent-a1")); err != nil { + t.Fatalf("project A: worktree not created: %v", err) + } + + // --- Project B --- + err = ProvisionShared(ProvisionInput{ + Resolved: ResolvedWorkspace{HostPath: hostPathB, Backend: "local"}, + ProjectID: "proj-beta", + AgentID: "agent-b1", + AgentName: "beta-agent", + Mode: store.SharingModeWorktreePerAgent, + Locker: locker, + GitClone: &api.GitCloneConfig{URL: bareRepoB, Branch: "main", Depth: 0}, + }) + if err != nil { + t.Fatalf("Provision project B: %v", err) + } + + if _, err := os.Stat(filepath.Join(hostPathB, ".git")); err != nil { + t.Fatalf("project B: .git not found — sentinel collision?") + } + if _, err := os.Stat(WorktreePath(hostPathB, "agent-b1")); err != nil { + t.Fatalf("project B: worktree not created: %v", err) + } + + // Sentinels must be per-project. + sentinelA := filepath.Join(projectDirA, ProvisionSentinelFile) + sentinelB := filepath.Join(projectDirB, ProvisionSentinelFile) + if _, err := os.Stat(sentinelA); err != nil { + t.Errorf("project A sentinel missing at %s", sentinelA) + } + if _, err := os.Stat(sentinelB); err != nil { + t.Errorf("project B sentinel missing at %s", sentinelB) + } + parentSentinel := filepath.Join(parentDir, ProvisionSentinelFile) + if _, err := os.Stat(parentSentinel); err == nil { + t.Errorf("sentinel found in shared parent dir %s — sentinel collision", parentDir) + } +} + +// --- Concurrent same-project provisioning --- + +func TestProvision_WorktreePerAgent_ConcurrentSameProject(t *testing.T) { + t.Setenv("SCION_HOST_UID", "") + locker := newTestLocker() + bareRepo := initBareGitRepo(t) + + projectDir := t.TempDir() + hostPath := filepath.Join(projectDir, "workspace") + + var wg sync.WaitGroup + errs := make([]error, 2) + + for i := 0; i < 2; i++ { + wg.Add(1) + go func(idx int) { + defer wg.Done() + agentID := fmt.Sprintf("agent-concurrent-%d", idx) + errs[idx] = ProvisionShared(ProvisionInput{ + Resolved: ResolvedWorkspace{HostPath: hostPath, Backend: "local"}, + ProjectID: "proj-concurrent-1", + AgentID: agentID, + AgentName: fmt.Sprintf("concurrent-agent-%d", idx), + Mode: store.SharingModeWorktreePerAgent, + Locker: locker, + GitClone: &api.GitCloneConfig{ + URL: bareRepo, + Branch: "main", + Depth: 0, + }, + }) + }(i) + } + + wg.Wait() + + for i, err := range errs { + if err != nil { + t.Errorf("goroutine %d failed: %v", i, err) + } + } + + for i := 0; i < 2; i++ { + wt := WorktreePath(hostPath, fmt.Sprintf("agent-concurrent-%d", i)) + if _, err := os.Stat(wt); err != nil { + t.Errorf("worktree agent-concurrent-%d not found at %s: %v", i, wt, err) + } + } + + if _, err := os.Stat(filepath.Join(hostPath, ".git")); err != nil { + t.Fatalf("shared base .git not found: %v", err) + } +} + +// --- Full clone depth for worktree mode --- + +func TestProvision_WorktreePerAgent_FullCloneDepth(t *testing.T) { + t.Setenv("SCION_HOST_UID", "") + locker := newTestLocker() + bareRepo := initBareGitRepo(t) + + projectDir := t.TempDir() + hostPath := filepath.Join(projectDir, "workspace") + + // Depth -1 means full clone (no --depth flag). + err := ProvisionShared(ProvisionInput{ + Resolved: ResolvedWorkspace{HostPath: hostPath, Backend: "local"}, + ProjectID: "proj-depth-1", + AgentID: "agent-depth-1", + AgentName: "depth-agent", + Mode: store.SharingModeWorktreePerAgent, + Locker: locker, + GitClone: &api.GitCloneConfig{ + URL: bareRepo, + Branch: "main", + Depth: -1, + }, + }) + if err != nil { + t.Fatalf("Provision with Depth=-1: %v", err) + } + + // Verify the clone is NOT shallow (full history). + shallowFile := filepath.Join(hostPath, ".git", "shallow") + if _, err := os.Stat(shallowFile); err == nil { + t.Error("expected full clone (no .git/shallow), but shallow file exists") + } +} + +// --- WorktreePath --- + +func TestWorktreePath(t *testing.T) { + got := WorktreePath("/srv/nfs/proj/workspace", "agent-42") + want := "/srv/nfs/proj/workspace/worktrees/agent-42" + if got != want { + t.Errorf("WorktreePath() = %q, want %q", got, want) + } +} diff --git a/pkg/runtime/cloudrun_runtime.go b/pkg/runtime/cloudrun_runtime.go new file mode 100644 index 000000000..bd7bfaa1d --- /dev/null +++ b/pkg/runtime/cloudrun_runtime.go @@ -0,0 +1,172 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package runtime + +import ( + "context" + "fmt" + "log/slog" + + "github.com/GoogleCloudPlatform/scion/pkg/api" + "github.com/GoogleCloudPlatform/scion/pkg/config" + "github.com/GoogleCloudPlatform/scion/pkg/store" +) + +// CloudRunRuntime implements the Runtime interface for Google Cloud Run. +// +// Cloud Run with a host-mounted share (or managed volume) calls Tier-1 +// ProvisionShared DIRECTLY broker-side — no init container is needed +// because the broker provisions before deploying the Cloud Run service. +// +// Lifecycle methods (deploy/exec/logs via the Cloud Run Admin API) are +// deferred to a follow-up PR — this implementation focuses on the +// provisioning + mount-realization wiring that PR3 requires. Lifecycle +// methods return a descriptive "not yet implemented" error. +type CloudRunRuntime struct { + // Project is the GCP project ID for Cloud Run API calls. + Project string + + // Region is the GCP region for Cloud Run services (e.g. "us-central1"). + Region string + + // WorkspaceStorage holds the workspace storage configuration, used to + // select the workspace backend and realize mount descriptors. + WorkspaceStorage *config.V1WorkspaceStorageConfig +} + +// NewCloudRunRuntime returns a new CloudRunRuntime. +func NewCloudRunRuntime(cfg *config.V1CloudRunConfig) *CloudRunRuntime { + rt := &CloudRunRuntime{} + if cfg != nil { + rt.Project = cfg.Project + rt.Region = cfg.Region + } + return rt +} + +func (r *CloudRunRuntime) Name() string { return "cloudrun" } + +func (r *CloudRunRuntime) ExecUser() string { return "scion" } + +// Run provisions the workspace broker-side using Tier-1 ProvisionShared, +// then would deploy a Cloud Run service. The deployment step is deferred. +// +// This is the broker-side direct provisioning path: Cloud Run with a +// host-mounted share calls ProvisionShared directly (no init container), +// because the broker has access to the mounted filesystem before the +// container is deployed. +func (r *CloudRunRuntime) Run(ctx context.Context, cfg RunConfig) (string, error) { + if err := r.provisionWorkspace(ctx, cfg); err != nil { + return "", fmt.Errorf("cloudrun: workspace provisioning failed: %w", err) + } + + // TODO(PR-followup): Deploy Cloud Run service via Admin API. + // The service spec would reference the workspace volume (cloudrun-volume + // or NFS mount) with the realized MountDescriptor fields. + return "", fmt.Errorf("cloudrun: Run not yet implemented — workspace provisioned, but Cloud Run service deployment requires the Admin API (follow-up PR)") +} + +// provisionWorkspace performs broker-side direct provisioning for the Cloud +// Run runtime. It selects the workspace backend, resolves paths, and calls +// ProvisionShared (Tier 1) directly — no init container. The context is +// propagated so provisioning (git clone, chown) is cancellable. +func (r *CloudRunRuntime) provisionWorkspace(ctx context.Context, cfg RunConfig) error { + if cfg.ProjectID == "" { + return fmt.Errorf("ProjectID is required for workspace provisioning") + } + + // Cloud Run uses a shared, plain workspace for now: the initial runtime + // scope provisions a single broker-side workspace per project. Per-agent + // worktrees (SharingModeWorktreePerAgent) are a follow-up once Cloud Run + // multi-agent lifecycle lands, so the mode is fixed rather than derived. + mode := store.SharingModeSharedPlain + backend := SelectWorkspaceBackend(r.WorkspaceStorage, mode) + + slog.Info("cloudrun: provisioning workspace broker-side", + "project_id", cfg.ProjectID, + "backend", backend.Name()) + + resolved, err := backend.Resolve(ResolveInput{ + ProjectID: cfg.ProjectID, + ProjectDir: cfg.Workspace, + Mode: mode, + }) + if err != nil { + return fmt.Errorf("resolve workspace: %w", err) + } + + // Only call ProvisionShared for backends with a host path (NFS, local + // with shared storage). Cloud Run managed volumes are provisioned by the + // platform, not the broker. + if resolved.HostPath != "" { + err = ProvisionShared(ProvisionInput{ + Ctx: ctx, + Resolved: resolved, + ProjectID: cfg.ProjectID, + AgentID: cfg.Labels["agent_id"], + AgentName: cfg.Name, + Mode: mode, + GitClone: cfg.GitClone, + Locker: cfg.Locker, + NFSUID: cfg.NFSUID, + NFSGID: cfg.NFSGID, + }) + if err != nil { + return fmt.Errorf("ProvisionShared: %w", err) + } + } + + return nil +} + +func (r *CloudRunRuntime) Stop(ctx context.Context, id string) error { + return fmt.Errorf("cloudrun: Stop not yet implemented") +} + +func (r *CloudRunRuntime) Delete(ctx context.Context, id string) error { + return fmt.Errorf("cloudrun: Delete not yet implemented") +} + +func (r *CloudRunRuntime) List(ctx context.Context, labelFilter map[string]string) ([]api.AgentInfo, error) { + return nil, fmt.Errorf("cloudrun: List not yet implemented") +} + +func (r *CloudRunRuntime) GetLogs(ctx context.Context, id string) (string, error) { + return "", fmt.Errorf("cloudrun: GetLogs not yet implemented") +} + +func (r *CloudRunRuntime) Attach(ctx context.Context, id string) error { + return fmt.Errorf("cloudrun: Attach not yet implemented") +} + +func (r *CloudRunRuntime) ImageExists(ctx context.Context, image string) (bool, error) { + return false, fmt.Errorf("cloudrun: ImageExists not yet implemented") +} + +func (r *CloudRunRuntime) PullImage(ctx context.Context, image string) error { + return fmt.Errorf("cloudrun: PullImage not yet implemented") +} + +func (r *CloudRunRuntime) Sync(ctx context.Context, id string, direction SyncDirection) error { + return fmt.Errorf("cloudrun: Sync not yet implemented") +} + +func (r *CloudRunRuntime) Exec(ctx context.Context, id string, cmd []string) (string, error) { + return "", fmt.Errorf("cloudrun: Exec not yet implemented") +} + +func (r *CloudRunRuntime) GetWorkspacePath(ctx context.Context, id string) (string, error) { + return "", fmt.Errorf("cloudrun: GetWorkspacePath not yet implemented") +} diff --git a/pkg/runtime/cloudrun_runtime_test.go b/pkg/runtime/cloudrun_runtime_test.go new file mode 100644 index 000000000..fc7b729a3 --- /dev/null +++ b/pkg/runtime/cloudrun_runtime_test.go @@ -0,0 +1,292 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package runtime + +import ( + "context" + "os" + "path/filepath" + "strings" + "testing" + + "github.com/GoogleCloudPlatform/scion/pkg/config" +) + +func TestCloudRunRuntime_Name(t *testing.T) { + rt := NewCloudRunRuntime(nil) + if rt.Name() != "cloudrun" { + t.Errorf("Name() = %q, want %q", rt.Name(), "cloudrun") + } +} + +func TestCloudRunRuntime_ExecUser(t *testing.T) { + rt := NewCloudRunRuntime(nil) + if rt.ExecUser() != "scion" { + t.Errorf("ExecUser() = %q, want %q", rt.ExecUser(), "scion") + } +} + +func TestCloudRunRuntime_NewWithConfig(t *testing.T) { + cfg := &config.V1CloudRunConfig{ + Project: "my-gcp-project", + Region: "us-central1", + } + rt := NewCloudRunRuntime(cfg) + if rt.Project != "my-gcp-project" { + t.Errorf("Project = %q, want %q", rt.Project, "my-gcp-project") + } + if rt.Region != "us-central1" { + t.Errorf("Region = %q, want %q", rt.Region, "us-central1") + } +} + +func TestCloudRunRuntime_NewWithNilConfig(t *testing.T) { + rt := NewCloudRunRuntime(nil) + if rt.Project != "" { + t.Errorf("Project = %q, want empty", rt.Project) + } + if rt.Region != "" { + t.Errorf("Region = %q, want empty", rt.Region) + } +} + +func TestCloudRunRuntime_LifecycleMethodsReturnNotImplemented(t *testing.T) { + rt := NewCloudRunRuntime(nil) + ctx := context.Background() + + methods := []struct { + name string + fn func() error + }{ + {"Stop", func() error { return rt.Stop(ctx, "x") }}, + {"Delete", func() error { return rt.Delete(ctx, "x") }}, + {"Attach", func() error { return rt.Attach(ctx, "x") }}, + {"PullImage", func() error { return rt.PullImage(ctx, "x") }}, + {"Sync", func() error { return rt.Sync(ctx, "x", SyncTo) }}, + } + + for _, m := range methods { + t.Run(m.name, func(t *testing.T) { + err := m.fn() + if err == nil || !strings.Contains(err.Error(), "not yet implemented") { + t.Errorf("%s() error = %v, want 'not yet implemented'", m.name, err) + } + }) + } + + t.Run("List", func(t *testing.T) { + _, err := rt.List(ctx, nil) + if err == nil || !strings.Contains(err.Error(), "not yet implemented") { + t.Errorf("List() error = %v, want 'not yet implemented'", err) + } + }) + + t.Run("GetLogs", func(t *testing.T) { + _, err := rt.GetLogs(ctx, "x") + if err == nil || !strings.Contains(err.Error(), "not yet implemented") { + t.Errorf("GetLogs() error = %v, want 'not yet implemented'", err) + } + }) + + t.Run("ImageExists", func(t *testing.T) { + _, err := rt.ImageExists(ctx, "x") + if err == nil || !strings.Contains(err.Error(), "not yet implemented") { + t.Errorf("ImageExists() error = %v, want 'not yet implemented'", err) + } + }) + + t.Run("Exec", func(t *testing.T) { + _, err := rt.Exec(ctx, "x", []string{"ls"}) + if err == nil || !strings.Contains(err.Error(), "not yet implemented") { + t.Errorf("Exec() error = %v, want 'not yet implemented'", err) + } + }) + + t.Run("GetWorkspacePath", func(t *testing.T) { + _, err := rt.GetWorkspacePath(ctx, "x") + if err == nil || !strings.Contains(err.Error(), "not yet implemented") { + t.Errorf("GetWorkspacePath() error = %v, want 'not yet implemented'", err) + } + }) +} + +func TestCloudRunRuntime_Run_BrokerSideProvisioning(t *testing.T) { + tmpDir := t.TempDir() + mountRoot := filepath.Join(tmpDir, "nfs") + shareDir := filepath.Join(mountRoot, "share1") + if err := os.MkdirAll(shareDir, 0755); err != nil { + t.Fatal(err) + } + + rt := NewCloudRunRuntime(&config.V1CloudRunConfig{ + Project: "test-project", + Region: "us-central1", + }) + rt.WorkspaceStorage = &config.V1WorkspaceStorageConfig{ + Backend: "nfs", + NFS: &config.V1NFSConfig{ + MountRoot: mountRoot, + SubPathRoot: "projects", + Shares: []config.V1NFSShare{ + {ID: "share1", Server: "10.0.0.2", Export: "/ws"}, + }, + }, + } + + cfg := RunConfig{ + Name: "test-agent", + ProjectID: "proj-123", + Workspace: tmpDir, + Labels: map[string]string{"agent_id": "agent-1"}, + } + + // Run will provision the workspace then fail with "not yet implemented" + // for the deploy step — that's expected. + _, err := rt.Run(context.Background(), cfg) + if err == nil { + t.Fatal("expected 'not yet implemented' error from Run") + } + if !strings.Contains(err.Error(), "not yet implemented") { + t.Fatalf("Run() error = %q, want containing 'not yet implemented'", err.Error()) + } + + // Verify workspace was provisioned (directory created + sentinel) + wsPath := filepath.Join(mountRoot, "share1", "projects", "proj-123", "workspace") + if _, err := os.Stat(wsPath); os.IsNotExist(err) { + t.Errorf("workspace directory %q was not created by broker-side provisioning", wsPath) + } + + sentinelPath := filepath.Join(mountRoot, "share1", "projects", "proj-123", ".scion-provisioned") + if _, err := os.Stat(sentinelPath); os.IsNotExist(err) { + t.Errorf("sentinel %q was not written — ProvisionShared did not run", sentinelPath) + } +} + +func TestCloudRunRuntime_Run_CloudRunVolume_SkipsProvisionShared(t *testing.T) { + rt := NewCloudRunRuntime(&config.V1CloudRunConfig{ + Project: "test-project", + Region: "us-central1", + }) + rt.WorkspaceStorage = &config.V1WorkspaceStorageConfig{ + Backend: "cloudrun-volume", + CloudRunVolume: &config.V1CloudRunVolumeConfig{ + VolumeName: "workspace-vol", + SubPathRoot: "projects", + }, + } + + cfg := RunConfig{ + Name: "test-agent", + ProjectID: "proj-456", + Labels: map[string]string{"agent_id": "agent-2"}, + } + + // With cloudrun-volume backend, Resolve returns no HostPath, so + // ProvisionShared is skipped (platform provisions the volume). + // Run still fails at the deploy step. + _, err := rt.Run(context.Background(), cfg) + if err == nil { + t.Fatal("expected 'not yet implemented' error from Run") + } + if !strings.Contains(err.Error(), "not yet implemented") { + t.Fatalf("Run() error = %q, want containing 'not yet implemented'", err.Error()) + } +} + +func TestCloudRunRuntime_Run_MissingProjectID(t *testing.T) { + rt := NewCloudRunRuntime(nil) + _, err := rt.Run(context.Background(), RunConfig{}) + if err == nil || !strings.Contains(err.Error(), "ProjectID is required") { + t.Errorf("Run() without ProjectID: error = %v, want 'ProjectID is required'", err) + } +} + +func TestGetRuntime_CloudRun(t *testing.T) { + t.Setenv("PATH", "") + + tmpHome := t.TempDir() + t.Setenv("HOME", tmpHome) + + globalDir := filepath.Join(tmpHome, ".scion") + if err := os.MkdirAll(globalDir, 0755); err != nil { + t.Fatal(err) + } + + settings := `{ + "schema_version": "1", + "active_profile": "cloud", + "runtimes": { + "cloudrun": { + "type": "cloudrun", + "cloudrun": { + "project": "my-project", + "region": "us-east1" + } + } + }, + "profiles": { + "cloud": { + "runtime": "cloudrun" + } + } + }` + if err := os.WriteFile(filepath.Join(globalDir, "settings.json"), []byte(settings), 0644); err != nil { + t.Fatal(err) + } + + oldWd, _ := os.Getwd() + tmpWd := t.TempDir() + if err := os.Chdir(tmpWd); err != nil { + t.Fatal(err) + } + defer os.Chdir(oldWd) + + r := GetRuntime("", "") + cr, ok := r.(*CloudRunRuntime) + if !ok { + t.Fatalf("expected *CloudRunRuntime, got %T", r) + } + if cr.Project != "my-project" { + t.Errorf("Project = %q, want %q", cr.Project, "my-project") + } + if cr.Region != "us-east1" { + t.Errorf("Region = %q, want %q", cr.Region, "us-east1") + } +} + +func TestGetRuntime_CloudRun_DirectProfileName(t *testing.T) { + t.Setenv("PATH", "") + + tmpHome := t.TempDir() + t.Setenv("HOME", tmpHome) + t.Setenv("SCION_GROVE", "") + + globalDir := filepath.Join(tmpHome, ".scion") + if err := os.MkdirAll(globalDir, 0755); err != nil { + t.Fatal(err) + } + + oldWd, _ := os.Getwd() + tmpWd := t.TempDir() + if err := os.Chdir(tmpWd); err != nil { + t.Fatal(err) + } + defer os.Chdir(oldWd) + + r := GetRuntime("", "cloudrun") + if _, ok := r.(*CloudRunRuntime); !ok { + t.Fatalf("expected *CloudRunRuntime from direct profile name, got %T", r) + } +} diff --git a/pkg/runtime/common.go b/pkg/runtime/common.go index 4a3f79802..a9e1801e9 100644 --- a/pkg/runtime/common.go +++ b/pkg/runtime/common.go @@ -277,9 +277,30 @@ func buildCommonRunArgs(config RunConfig) ([]string, error) { } } - // Pass host user UID/GID for container user synchronization - addEnv("SCION_HOST_UID", fmt.Sprintf("%d", os.Getuid())) - addEnv("SCION_HOST_GID", fmt.Sprintf("%d", os.Getgid())) + // Pass host user UID/GID for container user synchronization. + // N1-5: branch on workspace backend — NFS needs a stable, node-independent + // UID/GID (default 1000:1000) so files written by agents on different nodes + // have consistent ownership on the shared filesystem. The local backend + // continues to use the broker's host UID/GID (today's behavior, unchanged). + uid, gid := os.Getuid(), os.Getgid() + if config.WorkspaceBackendName == "nfs" { + uid, gid = config.NFSUID, config.NFSGID + if uid == 0 { + uid = 1000 // default stable NFS UID + } + if gid == 0 { + gid = 1000 // default stable NFS GID + } + } + addEnv("SCION_HOST_UID", fmt.Sprintf("%d", uid)) + addEnv("SCION_HOST_GID", fmt.Sprintf("%d", gid)) + + // Expose the workspace backend to the container so sciontool init can + // skip the per-start recursive chown when backend=nfs (slow/racy over + // the network; ownership is set once by operator + provisioner). + if config.WorkspaceBackendName != "" { + addEnv("SCION_WORKSPACE_BACKEND", config.WorkspaceBackendName) + } // Phase 3 & 5: Project identity injection addEnv("SCION_PROJECT", config.Project) diff --git a/pkg/runtime/factory.go b/pkg/runtime/factory.go index 900b65faf..f7d862c4c 100644 --- a/pkg/runtime/factory.go +++ b/pkg/runtime/factory.go @@ -45,7 +45,7 @@ func GetRuntime(projectPath string, profileName string) Runtime { util.Debugf("GetRuntime: ResolveRuntime failed: %v", err) // If profile resolution fails, we might be passed a direct runtime type // Fallback to legacy behavior for now if profileName matches a known type - if profileName == "docker" || profileName == "podman" || profileName == "kubernetes" || profileName == "k8s" || profileName == "container" || profileName == "remote" || profileName == "local" { + if profileName == "docker" || profileName == "podman" || profileName == "kubernetes" || profileName == "k8s" || profileName == "container" || profileName == "remote" || profileName == "local" || profileName == "cloudrun" { runtimeType = profileName util.Debugf("GetRuntime: using profileName as runtimeType: %s", runtimeType) } else { @@ -130,6 +130,12 @@ func GetRuntime(projectPath string, profileName string) Runtime { } rt.ListAllNamespaces = rtConfig.ListAllNamespaces return rt + case "cloudrun": + rt := NewCloudRunRuntime(rtConfig.CloudRun) + if vs != nil && vs.Server != nil { + rt.WorkspaceStorage = vs.Server.WorkspaceStorage + } + return rt } // Fallback should not be reached if logic is correct, but default to Docker diff --git a/pkg/runtime/interface.go b/pkg/runtime/interface.go index 2cc02e769..98acfdadd 100644 --- a/pkg/runtime/interface.go +++ b/pkg/runtime/interface.go @@ -18,6 +18,7 @@ import ( "context" "github.com/GoogleCloudPlatform/scion/pkg/api" + "github.com/GoogleCloudPlatform/scion/pkg/store" ) type RunConfig struct { @@ -51,6 +52,52 @@ type RunConfig struct { NetworkMode string // Container network mode (e.g. "host" for --network=host) Project string // Project name (e.g., "global" or "my-project") ProjectID string // Project ID (e.g., "550e8400-e29b-41d4-a716-446655440000") + + // WorkspaceBackendName is "local" or "nfs", set by the workspace backend + // selector. Used to branch UID/GID injection and skip per-start chown + // when NFS (N1-5). + WorkspaceBackendName string + // NFSUID and NFSGID are the stable, node-independent UID/GID for NFS-backed + // workspaces. Advertised as SCION_HOST_UID/GID when WorkspaceBackendName is "nfs" + // instead of os.Getuid()/os.Getgid(). Default 1000:1000 (design §9.1). + NFSUID int + NFSGID int + + // NFSPVClaimName is the K8s PVC name for the NFS-backed workspace volume. + // Set when WorkspaceBackendName is "nfs". The PVC references a static RWX PV + // bound to the Filestore/NFS export. Empty for local backend. + NFSPVClaimName string + // NFSSubPath is the subPath within the NFS PVC that isolates this project's + // workspace (e.g. "projects//workspace"). Used by K8s buildPod to scope + // the volume mount — pod sees only its project subtree (design §9.4). + NFSSubPath string + // NFSStorageClass is the K8s StorageClass for NFS-backed PVCs. + // Used when creating shared-dir PVCs on NFS. Empty uses cluster default. + NFSStorageClass string + + // GitCloneForInit holds git clone configuration for NFS init-container + // workspace provisioning (N2-2). When set, buildPod adds an init container + // that clones/provisions the workspace before the main container starts. + GitCloneForInit *api.GitCloneConfig + + // Locker provides the per-project advisory lock for NFS workspace + // provisioning (N2-2b, design §7, risk RN1). When set and backend=nfs, + // the K8s runtime acquires the lock before building the pod to determine + // whether this pod should clone (lock winner) or wait for the sentinel + // (lock loser). This prevents concurrent first-clone corruption when + // two pods for the same project are scheduled on different nodes. + // + // May be nil — when absent, all pods get the cloning init container + // (sentinel-only guard, correct for single-node but unsafe for + // multi-node). On Postgres-backed deployments this is wired from + // the store's AdvisoryLocker capability. + Locker store.AdvisoryLocker + + // nfsProvisionLockLost is set internally by Run() after a failed + // advisory lock acquisition attempt. When true, buildPod injects a + // wait-for-sentinel init container instead of the cloning one. + // Callers should not set this field. + nfsProvisionLockLost bool } type Runtime interface { diff --git a/pkg/runtime/k8s_nfs_test.go b/pkg/runtime/k8s_nfs_test.go new file mode 100644 index 000000000..7b6769e2e --- /dev/null +++ b/pkg/runtime/k8s_nfs_test.go @@ -0,0 +1,1185 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package runtime + +import ( + "context" + "errors" + "os" + "strings" + "testing" + "time" + + "github.com/GoogleCloudPlatform/scion/pkg/api" + "github.com/GoogleCloudPlatform/scion/pkg/k8s" + "github.com/GoogleCloudPlatform/scion/pkg/store" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + k8sruntime "k8s.io/apimachinery/pkg/runtime" + "k8s.io/client-go/dynamic/fake" + k8sfake "k8s.io/client-go/kubernetes/fake" +) + +// newNFSTestK8sRuntime creates a KubernetesRuntime backed by a fake clientset +// for unit testing buildPod and related methods. +func newNFSTestK8sRuntime() *KubernetesRuntime { + clientset := k8sfake.NewClientset() + scheme := k8sruntime.NewScheme() + fc := fake.NewSimpleDynamicClient(scheme) + client := k8s.NewTestClient(fc, clientset) + return NewKubernetesRuntime(client) +} + +// --- N2-1: NFS-backed workspace volume tests --- + +func TestBuildPod_WorkspaceVolume_LocalBackend_EmptyDir(t *testing.T) { + r := newNFSTestK8sRuntime() + config := RunConfig{ + Name: "test-local", + Image: "test-image", + UnixUsername: "scion", + // WorkspaceBackendName defaults to "" (local) + } + + pod, err := r.buildPod("default", config) + if err != nil { + t.Fatalf("buildPod failed: %v", err) + } + + // Volume must be EmptyDir + var found bool + for _, v := range pod.Spec.Volumes { + if v.Name == "workspace" { + found = true + if v.VolumeSource.EmptyDir == nil { + t.Errorf("local backend: workspace volume should be EmptyDir, got %+v", v.VolumeSource) + } + if v.VolumeSource.PersistentVolumeClaim != nil { + t.Errorf("local backend: workspace volume should NOT be PVC") + } + } + } + if !found { + t.Fatal("workspace volume not found in pod spec") + } + + // VolumeMount must not have subPath + for _, vm := range pod.Spec.Containers[0].VolumeMounts { + if vm.Name == "workspace" { + if vm.SubPath != "" { + t.Errorf("local backend: workspace mount should not have subPath, got %q", vm.SubPath) + } + if vm.MountPath != "/workspace" { + t.Errorf("local backend: workspace mount path = %q, want /workspace", vm.MountPath) + } + } + } +} + +func TestBuildPod_WorkspaceVolume_NFSBackend_PVCWithSubPath(t *testing.T) { + r := newNFSTestK8sRuntime() + config := RunConfig{ + Name: "test-nfs", + Image: "test-image", + UnixUsername: "scion", + WorkspaceBackendName: "nfs", + NFSPVClaimName: "scion-workspaces", + NFSSubPath: "projects/proj-123/workspace", + } + + pod, err := r.buildPod("default", config) + if err != nil { + t.Fatalf("buildPod failed: %v", err) + } + + // Volume must be PVC + var found bool + for _, v := range pod.Spec.Volumes { + if v.Name == "workspace" { + found = true + if v.VolumeSource.PersistentVolumeClaim == nil { + t.Fatalf("NFS backend: workspace volume should be PVC, got %+v", v.VolumeSource) + } + if v.VolumeSource.PersistentVolumeClaim.ClaimName != "scion-workspaces" { + t.Errorf("PVC claimName = %q, want %q", v.VolumeSource.PersistentVolumeClaim.ClaimName, "scion-workspaces") + } + if v.VolumeSource.EmptyDir != nil { + t.Errorf("NFS backend: workspace volume should NOT be EmptyDir") + } + } + } + if !found { + t.Fatal("workspace volume not found in pod spec") + } + + // VolumeMount must have subPath for isolation + for _, vm := range pod.Spec.Containers[0].VolumeMounts { + if vm.Name == "workspace" { + if vm.SubPath != "projects/proj-123/workspace" { + t.Errorf("NFS backend: workspace mount subPath = %q, want %q", vm.SubPath, "projects/proj-123/workspace") + } + if vm.MountPath != "/workspace" { + t.Errorf("NFS backend: workspace mount path = %q, want /workspace", vm.MountPath) + } + } + } +} + +func TestBuildPod_WorkspaceVolume_NFSWithoutPVCName_FallsBackToEmptyDir(t *testing.T) { + r := newNFSTestK8sRuntime() + // NFS backend but missing PVC name — defensive fallback to EmptyDir + config := RunConfig{ + Name: "test-nfs-no-pvc", + Image: "test-image", + UnixUsername: "scion", + WorkspaceBackendName: "nfs", + // NFSPVClaimName is empty + } + + pod, err := r.buildPod("default", config) + if err != nil { + t.Fatalf("buildPod failed: %v", err) + } + + for _, v := range pod.Spec.Volumes { + if v.Name == "workspace" { + if v.VolumeSource.EmptyDir == nil { + t.Errorf("NFS without PVC name: should fall back to EmptyDir, got %+v", v.VolumeSource) + } + } + } +} + +func TestBuildPod_NoInitContainers_LocalBackend(t *testing.T) { + r := newNFSTestK8sRuntime() + config := RunConfig{ + Name: "test-local", + Image: "test-image", + UnixUsername: "scion", + } + + pod, err := r.buildPod("default", config) + if err != nil { + t.Fatalf("buildPod failed: %v", err) + } + + if len(pod.Spec.InitContainers) != 0 { + t.Errorf("local backend: expected no init containers, got %d", len(pod.Spec.InitContainers)) + } +} + +// --- N2-2: Init-container workspace provisioning tests --- + +func TestBuildPod_NFSBackend_InitContainer_Present(t *testing.T) { + r := newNFSTestK8sRuntime() + config := RunConfig{ + Name: "test-nfs-init", + Image: "test-image", + UnixUsername: "scion", + WorkspaceBackendName: "nfs", + NFSPVClaimName: "scion-workspaces", + NFSSubPath: "projects/proj-123/workspace", + GitCloneForInit: &api.GitCloneConfig{ + URL: "https://github.com/example/repo.git", + Branch: "main", + Depth: 1, + }, + } + + pod, err := r.buildPod("default", config) + if err != nil { + t.Fatalf("buildPod failed: %v", err) + } + + // Must have exactly one init container + if len(pod.Spec.InitContainers) != 1 { + t.Fatalf("expected 1 init container, got %d", len(pod.Spec.InitContainers)) + } + + ic := pod.Spec.InitContainers[0] + + // Init container name + if ic.Name != "workspace-provision" { + t.Errorf("init container name = %q, want %q", ic.Name, "workspace-provision") + } + + // Uses the same image + if ic.Image != "test-image" { + t.Errorf("init container image = %q, want %q", ic.Image, "test-image") + } + + // Must mount workspace volume with subPath + var wsMount *corev1.VolumeMount + for i := range ic.VolumeMounts { + if ic.VolumeMounts[i].Name == "workspace" { + wsMount = &ic.VolumeMounts[i] + break + } + } + if wsMount == nil { + t.Fatal("init container: workspace volume mount not found") + } + if wsMount.MountPath != "/workspace" { + t.Errorf("init container workspace mountPath = %q, want /workspace", wsMount.MountPath) + } + if wsMount.SubPath != "projects/proj-123/workspace" { + t.Errorf("init container workspace subPath = %q, want %q", wsMount.SubPath, "projects/proj-123/workspace") + } + + // Command must invoke sciontool provision (not sh -c) + assert.Equal(t, "sciontool", ic.Command[0], "init container should invoke sciontool") + assert.Equal(t, "provision", ic.Command[1], "init container should invoke provision subcommand") + // URL must NOT appear in the command args (injection safety) + for _, arg := range ic.Command { + if arg == "https://github.com/example/repo.git" { + t.Error("init container command must NOT contain inline URL (injection safety)") + } + } + + // Verify env vars are set on the container (URL/branch via env, not args) + var hasURL, hasBranch bool + for _, env := range ic.Env { + if env.Name == "SCION_CLONE_URL" && env.Value == "https://github.com/example/repo.git" { + hasURL = true + } + if env.Name == "SCION_CLONE_BRANCH" && env.Value == "main" { + hasBranch = true + } + } + if !hasURL { + t.Error("init container missing SCION_CLONE_URL env var") + } + if !hasBranch { + t.Error("init container missing SCION_CLONE_BRANCH env var") + } +} + +func TestBuildPod_NFSBackend_NoInitContainer_WhenNoGitClone(t *testing.T) { + r := newNFSTestK8sRuntime() + config := RunConfig{ + Name: "test-nfs-no-git", + Image: "test-image", + UnixUsername: "scion", + WorkspaceBackendName: "nfs", + NFSPVClaimName: "scion-workspaces", + NFSSubPath: "projects/proj-123/workspace", + // GitCloneForInit is nil — no init container expected + } + + pod, err := r.buildPod("default", config) + if err != nil { + t.Fatalf("buildPod failed: %v", err) + } + + if len(pod.Spec.InitContainers) != 0 { + t.Errorf("NFS without git clone: expected no init containers, got %d", len(pod.Spec.InitContainers)) + } +} + +func TestBuildPod_LocalBackend_NoInitContainer_EvenWithGitClone(t *testing.T) { + r := newNFSTestK8sRuntime() + config := RunConfig{ + Name: "test-local-git", + Image: "test-image", + UnixUsername: "scion", + // Local backend (no NFS fields) + GitCloneForInit: &api.GitCloneConfig{ + URL: "https://github.com/example/repo.git", + }, + } + + pod, err := r.buildPod("default", config) + if err != nil { + t.Fatalf("buildPod failed: %v", err) + } + + if len(pod.Spec.InitContainers) != 0 { + t.Errorf("local backend: expected no init containers even with GitCloneForInit, got %d", len(pod.Spec.InitContainers)) + } +} + +func TestNFSProvisionCommand_ShallowClone(t *testing.T) { + gc := &api.GitCloneConfig{ + URL: "https://github.com/example/repo.git", + Branch: "main", + Depth: 1, + } + + cmd := nfsProvisionCommand(gc) + + assert.Equal(t, "sciontool", cmd[0]) + assert.Equal(t, "provision", cmd[1]) + assert.Contains(t, cmd, "--depth") + assert.Contains(t, cmd, "1") + + // URL must NOT appear in command args (injection safety) + for _, arg := range cmd { + assert.NotEqual(t, gc.URL, arg, "URL must not be in command args") + assert.NotEqual(t, gc.Branch, arg, "branch must not be in command args") + } +} + +func TestNFSProvisionCommand_NilConfig(t *testing.T) { + cmd := nfsProvisionCommand(nil) + assert.Equal(t, []string{"sciontool", "provision"}, cmd) +} + +func TestNFSProvisionCommand_FullClone(t *testing.T) { + gc := &api.GitCloneConfig{ + URL: "https://github.com/example/repo.git", + Depth: -1, + } + + cmd := nfsProvisionCommand(gc) + + assert.Equal(t, "sciontool", cmd[0]) + assert.Equal(t, "provision", cmd[1]) + assert.Contains(t, cmd, "--depth") + assert.Contains(t, cmd, "-1") +} + +func TestNFSProvisionEnv(t *testing.T) { + t.Run("includes URL and branch", func(t *testing.T) { + gc := &api.GitCloneConfig{ + URL: "https://github.com/example/repo.git", + Branch: "main", + } + envs := nfsProvisionEnv(gc) + require.Len(t, envs, 2) + assert.Equal(t, "SCION_CLONE_URL", envs[0].Name) + assert.Equal(t, gc.URL, envs[0].Value) + assert.Equal(t, "SCION_CLONE_BRANCH", envs[1].Name) + assert.Equal(t, gc.Branch, envs[1].Value) + }) + + t.Run("omits branch when empty", func(t *testing.T) { + gc := &api.GitCloneConfig{ + URL: "https://github.com/example/repo.git", + } + envs := nfsProvisionEnv(gc) + require.Len(t, envs, 1) + assert.Equal(t, "SCION_CLONE_URL", envs[0].Name) + }) + + t.Run("nil config returns nil", func(t *testing.T) { + envs := nfsProvisionEnv(nil) + assert.Nil(t, envs) + }) +} + +func TestNFSProvisionCommand_InjectionSafety(t *testing.T) { + gc := &api.GitCloneConfig{ + URL: "https://github.com/example/repo.git", + Branch: "feat/test; rm -rf /", + } + + cmd := nfsProvisionCommand(gc) + + // Branch and URL must NOT appear in command args + for _, arg := range cmd { + assert.NotEqual(t, gc.URL, arg, "URL must not be in command args") + assert.NotEqual(t, gc.Branch, arg, "branch must not be in command args") + } +} + +// hasFlag checks if a string slice contains the given flag value. +func hasFlag(args []string, val string) bool { + for _, a := range args { + if a == val { + return true + } + } + return false +} + +// --- N2-2b: Advisory lock guard for NFS init-container provisioning --- + +// nfsBaseConfig returns a RunConfig for NFS tests with common fields pre-filled. +func nfsBaseConfig(name string) RunConfig { + return RunConfig{ + Name: name, + Image: "test-image", + UnixUsername: "scion", + WorkspaceBackendName: "nfs", + NFSPVClaimName: "scion-workspaces", + NFSSubPath: "projects/proj-123/workspace", + ProjectID: "proj-123", + GitCloneForInit: &api.GitCloneConfig{ + URL: "https://github.com/example/repo.git", + Branch: "main", + Depth: 1, + }, + } +} + +func TestBuildPod_NFSLockWinner_InjectsCloneInitContainer(t *testing.T) { + r := newNFSTestK8sRuntime() + config := nfsBaseConfig("test-lock-winner") + // nfsProvisionLockLost defaults to false (winner) + + pod, err := r.buildPod("default", config) + if err != nil { + t.Fatalf("buildPod failed: %v", err) + } + + if len(pod.Spec.InitContainers) != 1 { + t.Fatalf("expected 1 init container, got %d", len(pod.Spec.InitContainers)) + } + + ic := pod.Spec.InitContainers[0] + if ic.Name != "workspace-provision" { + t.Errorf("init container name = %q, want %q", ic.Name, "workspace-provision") + } + + // Winner must invoke sciontool provision (clone mode, no --wait-for-sentinel) + assert.Equal(t, "sciontool", ic.Command[0]) + assert.Equal(t, "provision", ic.Command[1]) + assert.False(t, hasFlag(ic.Command, "--wait-for-sentinel"), + "winner should NOT have --wait-for-sentinel flag") + + // URL must NOT appear in command args (injection safety — passed via env) + for _, arg := range ic.Command { + assert.NotEqual(t, "https://github.com/example/repo.git", arg, + "URL must not be in command args") + } + + // Verify env vars are set on the init container + var hasURL, hasBranch bool + for _, env := range ic.Env { + if env.Name == "SCION_CLONE_URL" && env.Value == "https://github.com/example/repo.git" { + hasURL = true + } + if env.Name == "SCION_CLONE_BRANCH" && env.Value == "main" { + hasBranch = true + } + } + if !hasURL { + t.Error("init container missing SCION_CLONE_URL env var") + } + if !hasBranch { + t.Error("init container missing SCION_CLONE_BRANCH env var") + } +} + +func TestBuildPod_NFSLockLoser_InjectsWaitInitContainer(t *testing.T) { + r := newNFSTestK8sRuntime() + config := nfsBaseConfig("test-lock-loser") + config.nfsProvisionLockLost = true + + pod, err := r.buildPod("default", config) + if err != nil { + t.Fatalf("buildPod failed: %v", err) + } + + if len(pod.Spec.InitContainers) != 1 { + t.Fatalf("expected 1 init container, got %d", len(pod.Spec.InitContainers)) + } + + ic := pod.Spec.InitContainers[0] + if ic.Name != "workspace-provision" { + t.Errorf("init container name = %q, want %q", ic.Name, "workspace-provision") + } + + // Loser must invoke sciontool provision --wait-for-sentinel + assert.Equal(t, "sciontool", ic.Command[0]) + assert.Equal(t, "provision", ic.Command[1]) + assert.True(t, hasFlag(ic.Command, "--wait-for-sentinel"), + "loser should have --wait-for-sentinel flag") +} + +func TestBuildPod_NFSNoLocker_InjectsCloneInitContainer(t *testing.T) { + r := newNFSTestK8sRuntime() + config := nfsBaseConfig("test-no-locker") + // Locker is nil, nfsProvisionLockLost stays false → clone init container + + pod, err := r.buildPod("default", config) + if err != nil { + t.Fatalf("buildPod failed: %v", err) + } + + if len(pod.Spec.InitContainers) != 1 { + t.Fatalf("expected 1 init container, got %d", len(pod.Spec.InitContainers)) + } + + ic := pod.Spec.InitContainers[0] + // No-locker: should get clone init command (provision without --wait-for-sentinel) + assert.Equal(t, "sciontool", ic.Command[0]) + assert.Equal(t, "provision", ic.Command[1]) + assert.False(t, hasFlag(ic.Command, "--wait-for-sentinel"), + "no-locker: should get clone init command (sentinel-only fallback)") +} + +func TestBuildPod_LocalBackend_LockLostIgnored(t *testing.T) { + r := newNFSTestK8sRuntime() + config := RunConfig{ + Name: "test-local-lockflag", + Image: "test-image", + UnixUsername: "scion", + nfsProvisionLockLost: true, // should be ignored for local backend + } + + pod, err := r.buildPod("default", config) + if err != nil { + t.Fatalf("buildPod failed: %v", err) + } + + // Local backend: no init containers regardless of lock flag + if len(pod.Spec.InitContainers) != 0 { + t.Errorf("local backend: expected no init containers, got %d", len(pod.Spec.InitContainers)) + } +} + +func TestBuildPod_NFSConcurrentProjects_IndependentLocks(t *testing.T) { + // Two pods for DIFFERENT projects should both get clone init containers + // when both are lock winners (no contention across projects). + r := newNFSTestK8sRuntime() + + configA := nfsBaseConfig("test-proj-a") + configA.ProjectID = "proj-aaa" + configA.NFSSubPath = "projects/proj-aaa/workspace" + + configB := nfsBaseConfig("test-proj-b") + configB.ProjectID = "proj-bbb" + configB.NFSSubPath = "projects/proj-bbb/workspace" + + podA, err := r.buildPod("default", configA) + if err != nil { + t.Fatalf("buildPod A failed: %v", err) + } + podB, err := r.buildPod("default", configB) + if err != nil { + t.Fatalf("buildPod B failed: %v", err) + } + + if len(podA.Spec.InitContainers) != 1 { + t.Fatalf("project A: expected 1 init container, got %d", len(podA.Spec.InitContainers)) + } + if len(podB.Spec.InitContainers) != 1 { + t.Fatalf("project B: expected 1 init container, got %d", len(podB.Spec.InitContainers)) + } + + // Both should be clone (winner) init containers — sciontool provision without --wait + icA := podA.Spec.InitContainers[0] + icB := podB.Spec.InitContainers[0] + assert.Equal(t, "sciontool", icA.Command[0]) + assert.Equal(t, "provision", icA.Command[1]) + assert.False(t, hasFlag(icA.Command, "--wait-for-sentinel")) + assert.Equal(t, "sciontool", icB.Command[0]) + assert.Equal(t, "provision", icB.Command[1]) + assert.False(t, hasFlag(icB.Command, "--wait-for-sentinel")) +} + +func TestBuildPod_NFSSameProject_WinnerAndLoser(t *testing.T) { + // Simulate two pods for the SAME project: one winner, one loser. + r := newNFSTestK8sRuntime() + + winner := nfsBaseConfig("test-winner") + loser := nfsBaseConfig("test-loser") + loser.nfsProvisionLockLost = true + + podWinner, err := r.buildPod("default", winner) + if err != nil { + t.Fatalf("buildPod winner failed: %v", err) + } + podLoser, err := r.buildPod("default", loser) + if err != nil { + t.Fatalf("buildPod loser failed: %v", err) + } + + if len(podWinner.Spec.InitContainers) != 1 || len(podLoser.Spec.InitContainers) != 1 { + t.Fatal("both pods should have exactly 1 init container") + } + + winnerCmd := podWinner.Spec.InitContainers[0].Command + loserCmd := podLoser.Spec.InitContainers[0].Command + + // Winner: sciontool provision (clone mode) + assert.Equal(t, "sciontool", winnerCmd[0]) + assert.Equal(t, "provision", winnerCmd[1]) + assert.False(t, hasFlag(winnerCmd, "--wait-for-sentinel")) + + // Loser: sciontool provision --wait-for-sentinel + assert.Equal(t, "sciontool", loserCmd[0]) + assert.Equal(t, "provision", loserCmd[1]) + assert.True(t, hasFlag(loserCmd, "--wait-for-sentinel")) +} + +// --- N2-2b: Run()-level advisory lock integration tests --- + +// errorLocker is an AdvisoryLocker that always returns an error. +type errorLocker struct { + err error +} + +func (l *errorLocker) TryAdvisoryLock(_ context.Context, _ store.AdvisoryLockKey) (bool, func() error, error) { + return false, func() error { return nil }, l.err +} + +func (l *errorLocker) TryAdvisoryLockObject(_ context.Context, _ store.AdvisoryLockKey, _ int32) (bool, func() error, error) { + return false, func() error { return nil }, l.err +} + +// alwaysLoseLocker is an AdvisoryLocker where TryAdvisoryLockObject always +// returns acquired=false (another node holds the lock). +type alwaysLoseLocker struct{} + +func (l *alwaysLoseLocker) TryAdvisoryLock(_ context.Context, _ store.AdvisoryLockKey) (bool, func() error, error) { + return false, func() error { return nil }, nil +} + +func (l *alwaysLoseLocker) TryAdvisoryLockObject(_ context.Context, _ store.AdvisoryLockKey, _ int32) (bool, func() error, error) { + return false, func() error { return nil }, nil +} + +func TestRun_NFSLockError_FailsDispatch(t *testing.T) { + // When the advisory lock returns an error, Run() must fail BEFORE + // creating any pods (no unguarded clone). + r := newNFSTestK8sRuntime() + config := nfsBaseConfig("test-lock-err") + config.Locker = &errorLocker{err: errors.New("connection lost")} + + _, err := r.Run(context.Background(), config) + if err == nil { + t.Fatal("expected Run() to fail when advisory lock returns error") + } + if !strings.Contains(err.Error(), "advisory lock") { + t.Errorf("error should mention advisory lock, got: %v", err) + } + if !strings.Contains(err.Error(), "connection lost") { + t.Errorf("error should propagate underlying cause, got: %v", err) + } + + // Verify no pods were created + pods, listErr := r.Client.Clientset.CoreV1().Pods("default").List( + context.Background(), metav1.ListOptions{}, + ) + if listErr != nil { + t.Fatalf("failed to list pods: %v", listErr) + } + if len(pods.Items) != 0 { + t.Errorf("lock error should prevent pod creation, but found %d pods", len(pods.Items)) + } +} + +func TestRun_NFSLockLost_CreatesWaitPod(t *testing.T) { + // When the lock is held by another node, the pod should have a + // wait-for-sentinel init container, not a cloning one. + r := newNFSTestK8sRuntime() + config := nfsBaseConfig("scion-test-lock-lost") + config.Locker = &alwaysLoseLocker{} + + // Run() will create the pod but waitForPodReady will time out with the + // fake clientset. Use a short-lived context so we don't block for 10m. + ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second) + defer cancel() + r.Run(ctx, config) //nolint:errcheck + + // Verify the created pod has a wait-for-sentinel init container + pods, err := r.Client.Clientset.CoreV1().Pods("default").List( + context.Background(), metav1.ListOptions{}, + ) + if err != nil { + t.Fatalf("failed to list pods: %v", err) + } + if len(pods.Items) != 1 { + t.Fatalf("expected 1 pod, got %d", len(pods.Items)) + } + + pod := pods.Items[0] + if len(pod.Spec.InitContainers) != 1 { + t.Fatalf("expected 1 init container, got %d", len(pod.Spec.InitContainers)) + } + + cmd := pod.Spec.InitContainers[0].Command + assert.Equal(t, "sciontool", cmd[0]) + assert.Equal(t, "provision", cmd[1]) + assert.True(t, hasFlag(cmd, "--wait-for-sentinel"), + "lock-lost pod should have --wait-for-sentinel flag") +} + +func TestRun_NFSLockWon_CreatesClonePod(t *testing.T) { + // When the lock is won, the pod should have the cloning init container. + r := newNFSTestK8sRuntime() + locker := newTestLocker() + config := nfsBaseConfig("scion-test-lock-won") + config.Locker = locker + + // Short-lived context to avoid blocking on waitForPodReady. + ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second) + defer cancel() + r.Run(ctx, config) //nolint:errcheck + + pods, err := r.Client.Clientset.CoreV1().Pods("default").List( + context.Background(), metav1.ListOptions{}, + ) + if err != nil { + t.Fatalf("failed to list pods: %v", err) + } + if len(pods.Items) != 1 { + t.Fatalf("expected 1 pod, got %d", len(pods.Items)) + } + + pod := pods.Items[0] + if len(pod.Spec.InitContainers) != 1 { + t.Fatalf("expected 1 init container, got %d", len(pod.Spec.InitContainers)) + } + + cmd := pod.Spec.InitContainers[0].Command + assert.Equal(t, "sciontool", cmd[0]) + assert.Equal(t, "provision", cmd[1]) + assert.False(t, hasFlag(cmd, "--wait-for-sentinel"), + "lock-won pod should NOT have --wait-for-sentinel flag") +} + +func TestRun_LocalBackend_NoLockAttempt(t *testing.T) { + // Local backend should never attempt the advisory lock, even if a + // Locker is provided. The lock is only for NFS. + r := newNFSTestK8sRuntime() + locker := &errorLocker{err: errors.New("should not be called")} + config := RunConfig{ + Name: "scion-test-local-nolock", + Image: "test-image", + UnixUsername: "scion", + ProjectID: "proj-local", + Locker: locker, + // No NFS fields → local backend + } + + // Run() should NOT fail with lock error (lock is only for NFS). + // It will fail at waitForPodReady with fake client, but NOT at lock. + // Short-lived context to avoid blocking. + ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second) + defer cancel() + _, err := r.Run(ctx, config) + if err != nil && strings.Contains(err.Error(), "advisory lock") { + t.Errorf("local backend should not attempt advisory lock, got: %v", err) + } +} + +// --- N2-4: Stable FSGroup/UID for NFS pods --- + +func TestBuildPod_FSGroup_LocalBackend_UsesHostGID(t *testing.T) { + r := newNFSTestK8sRuntime() + config := RunConfig{ + Name: "test-local-gid", + Image: "test-image", + UnixUsername: "scion", + } + + pod, err := r.buildPod("default", config) + if err != nil { + t.Fatalf("buildPod failed: %v", err) + } + + // Local backend: FSGroup should be the host GID (os.Getgid()) + if pod.Spec.SecurityContext == nil || pod.Spec.SecurityContext.FSGroup == nil { + t.Fatal("pod security context or FSGroup is nil") + } + + hostGID := int64(os.Getgid()) + if *pod.Spec.SecurityContext.FSGroup != hostGID { + t.Errorf("local backend: FSGroup = %d, want host GID %d", *pod.Spec.SecurityContext.FSGroup, hostGID) + } +} + +func TestBuildPod_FSGroup_NFSBackend_UsesStableGID(t *testing.T) { + r := newNFSTestK8sRuntime() + config := RunConfig{ + Name: "test-nfs-gid", + Image: "test-image", + UnixUsername: "scion", + WorkspaceBackendName: "nfs", + NFSPVClaimName: "scion-workspaces", + NFSGID: 1000, + } + + pod, err := r.buildPod("default", config) + if err != nil { + t.Fatalf("buildPod failed: %v", err) + } + + if pod.Spec.SecurityContext == nil || pod.Spec.SecurityContext.FSGroup == nil { + t.Fatal("pod security context or FSGroup is nil") + } + + if *pod.Spec.SecurityContext.FSGroup != 1000 { + t.Errorf("NFS backend: FSGroup = %d, want 1000", *pod.Spec.SecurityContext.FSGroup) + } +} + +func TestBuildPod_FSGroup_NFSBackend_DefaultGID(t *testing.T) { + r := newNFSTestK8sRuntime() + config := RunConfig{ + Name: "test-nfs-default-gid", + Image: "test-image", + UnixUsername: "scion", + WorkspaceBackendName: "nfs", + NFSPVClaimName: "scion-workspaces", + // NFSGID is 0 (unset) — should default to 1000 + } + + pod, err := r.buildPod("default", config) + if err != nil { + t.Fatalf("buildPod failed: %v", err) + } + + if pod.Spec.SecurityContext == nil || pod.Spec.SecurityContext.FSGroup == nil { + t.Fatal("pod security context or FSGroup is nil") + } + + if *pod.Spec.SecurityContext.FSGroup != 1000 { + t.Errorf("NFS backend default: FSGroup = %d, want 1000", *pod.Spec.SecurityContext.FSGroup) + } +} + +func TestBuildPod_FSGroup_NFSBackend_CustomGID(t *testing.T) { + r := newNFSTestK8sRuntime() + config := RunConfig{ + Name: "test-nfs-custom-gid", + Image: "test-image", + UnixUsername: "scion", + WorkspaceBackendName: "nfs", + NFSPVClaimName: "scion-workspaces", + NFSGID: 2000, + } + + pod, err := r.buildPod("default", config) + if err != nil { + t.Fatalf("buildPod failed: %v", err) + } + + if *pod.Spec.SecurityContext.FSGroup != 2000 { + t.Errorf("NFS backend custom GID: FSGroup = %d, want 2000", *pod.Spec.SecurityContext.FSGroup) + } +} + +// --- N2-3: Skip workspace kubectl cp when backend=nfs --- + +// TestSkipWorkspaceSync_NFSBackend_RunConfigGuard validates the guard condition +// that controls workspace sync skip. The actual Run() method performs real K8s +// API calls, so we test the conditional logic via the config fields that +// determine behavior. +func TestSkipWorkspaceSync_NFSBackend_RunConfigGuard(t *testing.T) { + tests := []struct { + name string + workspace string + backendName string + wantWorkspaceCP bool + }{ + { + name: "local backend syncs workspace", + workspace: "/some/path", + backendName: "", + wantWorkspaceCP: true, + }, + { + name: "local backend explicit syncs workspace", + workspace: "/some/path", + backendName: "local", + wantWorkspaceCP: true, + }, + { + name: "NFS backend skips workspace sync", + workspace: "/some/path", + backendName: "nfs", + wantWorkspaceCP: false, + }, + { + name: "empty workspace skips sync for any backend", + workspace: "", + backendName: "", + wantWorkspaceCP: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + config := RunConfig{ + Workspace: tt.workspace, + WorkspaceBackendName: tt.backendName, + } + // Replicate the guard condition from Run() + shouldSync := config.Workspace != "" && config.WorkspaceBackendName != "nfs" + if shouldSync != tt.wantWorkspaceCP { + t.Errorf("workspace sync guard: got %v, want %v", shouldSync, tt.wantWorkspaceCP) + } + }) + } +} + +// --- N2-5: Generalized shared-dir PVC helpers --- + +func TestBuildPod_SharedDirs_LocalBackend_SeparatePVCs(t *testing.T) { + r := newNFSTestK8sRuntime() + config := RunConfig{ + Name: "test-local-shared", + Image: "test-image", + UnixUsername: "scion", + Labels: map[string]string{ + "scion.grove": "my-project", + }, + SharedDirs: []api.SharedDir{ + {Name: "build-cache"}, + {Name: "logs"}, + }, + } + + pod, err := r.buildPod("default", config) + if err != nil { + t.Fatalf("buildPod failed: %v", err) + } + + // Local backend: each shared dir should have its own PVC volume + sd0Vol := findVolume(pod, "shared-dir-0") + sd1Vol := findVolume(pod, "shared-dir-1") + + if sd0Vol == nil || sd1Vol == nil { + t.Fatal("local backend: expected shared-dir-0 and shared-dir-1 volumes") + } + + // PVC names should follow the sharedDirPVCName convention + if sd0Vol.PersistentVolumeClaim.ClaimName != "scion-shared-my-project-build-cache" { + t.Errorf("shared-dir-0 claimName = %q, want %q", sd0Vol.PersistentVolumeClaim.ClaimName, "scion-shared-my-project-build-cache") + } + if sd1Vol.PersistentVolumeClaim.ClaimName != "scion-shared-my-project-logs" { + t.Errorf("shared-dir-1 claimName = %q, want %q", sd1Vol.PersistentVolumeClaim.ClaimName, "scion-shared-my-project-logs") + } + + // Mounts should NOT have subPath for local backend + sd0Mount := findVolumeMount(&pod.Spec.Containers[0], "shared-dir-0") + if sd0Mount == nil { + t.Fatal("shared-dir-0 mount not found") + } + if sd0Mount.SubPath != "" { + t.Errorf("local backend: shared-dir-0 should not have subPath, got %q", sd0Mount.SubPath) + } +} + +func TestBuildPod_SharedDirs_NFSBackend_UsesNFSSubPaths(t *testing.T) { + r := newNFSTestK8sRuntime() + config := RunConfig{ + Name: "test-nfs-shared", + Image: "test-image", + UnixUsername: "scion", + WorkspaceBackendName: "nfs", + NFSPVClaimName: "scion-workspaces", + NFSSubPath: "projects/proj-123/workspace", + Labels: map[string]string{ + "scion.grove": "my-project", + }, + SharedDirs: []api.SharedDir{ + {Name: "build-cache"}, + {Name: "logs", ReadOnly: true}, + }, + } + + pod, err := r.buildPod("default", config) + if err != nil { + t.Fatalf("buildPod failed: %v", err) + } + + // NFS backend: shared dir volumes should use the SAME PVC as workspace + sd0Vol := findVolume(pod, "shared-dir-0") + sd1Vol := findVolume(pod, "shared-dir-1") + + if sd0Vol == nil || sd1Vol == nil { + t.Fatal("NFS backend: expected shared-dir-0 and shared-dir-1 volumes") + } + + // Both should reference the workspace NFS PVC + if sd0Vol.PersistentVolumeClaim.ClaimName != "scion-workspaces" { + t.Errorf("shared-dir-0 claimName = %q, want %q", sd0Vol.PersistentVolumeClaim.ClaimName, "scion-workspaces") + } + if sd1Vol.PersistentVolumeClaim.ClaimName != "scion-workspaces" { + t.Errorf("shared-dir-1 claimName = %q, want %q", sd1Vol.PersistentVolumeClaim.ClaimName, "scion-workspaces") + } + + // Mounts should have NFS subPaths + sd0Mount := findVolumeMount(&pod.Spec.Containers[0], "shared-dir-0") + sd1Mount := findVolumeMount(&pod.Spec.Containers[0], "shared-dir-1") + + if sd0Mount == nil || sd1Mount == nil { + t.Fatal("shared-dir mounts not found") + } + + wantSubPath0 := "projects/proj-123/shared-dirs/build-cache" + if sd0Mount.SubPath != wantSubPath0 { + t.Errorf("shared-dir-0 subPath = %q, want %q", sd0Mount.SubPath, wantSubPath0) + } + + wantSubPath1 := "projects/proj-123/shared-dirs/logs" + if sd1Mount.SubPath != wantSubPath1 { + t.Errorf("shared-dir-1 subPath = %q, want %q", sd1Mount.SubPath, wantSubPath1) + } + + // Verify readOnly flag propagates + if sd1Mount.ReadOnly != true { + t.Error("shared-dir-1 should be read-only") + } + if sd0Mount.ReadOnly != false { + t.Error("shared-dir-0 should not be read-only") + } +} + +func TestNFSSharedDirSubPath(t *testing.T) { + tests := []struct { + workspaceSubPath string + sharedDirName string + want string + }{ + { + workspaceSubPath: "projects/proj-123/workspace", + sharedDirName: "build-cache", + want: "projects/proj-123/shared-dirs/build-cache", + }, + { + workspaceSubPath: "projects/proj-456/workspace", + sharedDirName: "logs", + want: "projects/proj-456/shared-dirs/logs", + }, + { + workspaceSubPath: "custom-root/proj-789/workspace", + sharedDirName: "data", + want: "custom-root/proj-789/shared-dirs/data", + }, + } + + for _, tt := range tests { + t.Run(tt.sharedDirName, func(t *testing.T) { + got := nfsSharedDirSubPath(tt.workspaceSubPath, tt.sharedDirName) + if got != tt.want { + t.Errorf("nfsSharedDirSubPath(%q, %q) = %q, want %q", tt.workspaceSubPath, tt.sharedDirName, got, tt.want) + } + }) + } +} + +func TestProjectRWXClaimName(t *testing.T) { + // Test the generalized naming helper + got := projectRWXClaimName("my-project", "shared", "build-cache") + want := "scion-shared-my-project-build-cache" + if got != want { + t.Errorf("projectRWXClaimName = %q, want %q", got, want) + } + + // Test backward compatibility with sharedDirPVCName + got2 := sharedDirPVCName("my-project", "build-cache") + if got != got2 { + t.Errorf("sharedDirPVCName should equal projectRWXClaimName(shared): %q != %q", got2, got) + } +} + +func TestCreateSharedDirPVCs_NFSBackend_SkipsPVCCreation(t *testing.T) { + clientset := k8sfake.NewClientset() + scheme := k8sruntime.NewScheme() + fc := fake.NewSimpleDynamicClient(scheme) + client := k8s.NewTestClient(fc, clientset) + r := NewKubernetesRuntime(client) + + config := RunConfig{ + Name: "test-nfs", + WorkspaceBackendName: "nfs", + NFSPVClaimName: "scion-workspaces", + Labels: map[string]string{ + "scion.grove": "my-project", + "scion.grove_id": "proj-123", + }, + SharedDirs: []api.SharedDir{ + {Name: "build-cache"}, + }, + } + + err := r.createSharedDirPVCs(context.Background(), "default", config) + if err != nil { + t.Fatalf("createSharedDirPVCs failed: %v", err) + } + + // No PVCs should have been created for NFS backend + pvcs, err := clientset.CoreV1().PersistentVolumeClaims("default").List(context.Background(), metav1.ListOptions{}) + if err != nil { + t.Fatalf("list PVCs failed: %v", err) + } + if len(pvcs.Items) != 0 { + t.Errorf("NFS backend: expected 0 PVCs, got %d", len(pvcs.Items)) + } +} + +func TestCreateSharedDirPVCs_LocalBackend_CreatesPVCs(t *testing.T) { + clientset := k8sfake.NewClientset() + scheme := k8sruntime.NewScheme() + fc := fake.NewSimpleDynamicClient(scheme) + client := k8s.NewTestClient(fc, clientset) + r := NewKubernetesRuntime(client) + + config := RunConfig{ + Name: "test-local", + Labels: map[string]string{ + "scion.grove": "my-project", + "scion.grove_id": "proj-123", + }, + SharedDirs: []api.SharedDir{ + {Name: "build-cache"}, + {Name: "logs"}, + }, + } + + err := r.createSharedDirPVCs(context.Background(), "default", config) + if err != nil { + t.Fatalf("createSharedDirPVCs failed: %v", err) + } + + // Local backend: 2 PVCs should be created + pvcs, err := clientset.CoreV1().PersistentVolumeClaims("default").List(context.Background(), metav1.ListOptions{}) + if err != nil { + t.Fatalf("list PVCs failed: %v", err) + } + if len(pvcs.Items) != 2 { + t.Errorf("local backend: expected 2 PVCs, got %d", len(pvcs.Items)) + } + + // Verify PVC names + pvcNames := map[string]bool{} + for _, pvc := range pvcs.Items { + pvcNames[pvc.Name] = true + } + if !pvcNames["scion-shared-my-project-build-cache"] { + t.Error("missing PVC scion-shared-my-project-build-cache") + } + if !pvcNames["scion-shared-my-project-logs"] { + t.Error("missing PVC scion-shared-my-project-logs") + } +} + +// findVolume finds a volume by name in a pod spec. +func findVolume(pod *corev1.Pod, name string) *corev1.Volume { + for i := range pod.Spec.Volumes { + if pod.Spec.Volumes[i].Name == name { + return &pod.Spec.Volumes[i] + } + } + return nil +} + +// findVolumeMount finds a volume mount by name in a container. +func findVolumeMount(container *corev1.Container, name string) *corev1.VolumeMount { + for i := range container.VolumeMounts { + if container.VolumeMounts[i].Name == name { + return &container.VolumeMounts[i] + } + } + return nil +} diff --git a/pkg/runtime/k8s_runtime.go b/pkg/runtime/k8s_runtime.go index 71db98e4d..30cec758d 100644 --- a/pkg/runtime/k8s_runtime.go +++ b/pkg/runtime/k8s_runtime.go @@ -34,6 +34,7 @@ import ( "github.com/GoogleCloudPlatform/scion/pkg/api" "github.com/GoogleCloudPlatform/scion/pkg/gcp" "github.com/GoogleCloudPlatform/scion/pkg/k8s" + "github.com/GoogleCloudPlatform/scion/pkg/store" "golang.org/x/term" corev1 "k8s.io/api/core/v1" k8serrors "k8s.io/apimachinery/pkg/api/errors" @@ -292,6 +293,56 @@ func (r *KubernetesRuntime) Run(ctx context.Context, config RunConfig) (string, } } + // --- N2-2b: Per-project advisory lock for NFS init-container provisioning --- + // + // When backend=nfs with a git clone configured AND an advisory locker is + // available, acquire the per-project lock before building the pod spec. + // This prevents concurrent first-clone corruption (risk RN1, design §7): + // - Lock winner: injects the cloning init container (existing N2-2 script) + // - Lock loser: injects a wait-for-sentinel init container (polls for + // .scion-provisioned without cloning) + // + // The lock is held until waitForPodReady returns (all init containers + // complete), mirroring N1-4's "hold during clone" lifetime. On error + // paths the deferred release ensures no lock leak. + var nfsProvisionLockRelease func() error + if config.WorkspaceBackendName == "nfs" && config.NFSPVClaimName != "" && config.GitCloneForInit != nil { + if config.Locker != nil { + objID := store.StableProjectHash(config.ProjectID) + acquired, release, err := config.Locker.TryAdvisoryLockObject( + ctx, store.LockWorkspaceProvision, objID, + ) + if err != nil { + return "", fmt.Errorf("NFS provision advisory lock for project %s: %w", config.ProjectID, err) + } + nfsProvisionLockRelease = release + if !acquired { + // Another node is currently provisioning this project's workspace. + // buildPod will inject a wait-for-sentinel init container instead + // of the cloning one. + config.nfsProvisionLockLost = true + runtimeLog.Info("NFS provision lock held by another node — pod will wait for sentinel", + "agent", config.Name, "project_id", config.ProjectID, "phase", "nfs-lock") + } else { + runtimeLog.Info("NFS provision lock acquired — pod will clone workspace", + "agent", config.Name, "project_id", config.ProjectID, "phase", "nfs-lock") + } + } else { + runtimeLog.Warn("No advisory locker available — NFS provisioning is unguarded (sentinel-only)", + "agent", config.Name, "project_id", config.ProjectID, "phase", "nfs-lock") + } + } + // Deferred release: held through pod creation + waitForPodReady (init + // containers complete), then released. Safe to call even when nil. + defer func() { + if nfsProvisionLockRelease != nil { + if err := nfsProvisionLockRelease(); err != nil { + runtimeLog.Error("Failed to release NFS provision lock", "error", err, + "agent", config.Name, "project_id", config.ProjectID) + } + } + }() + pod, err := r.buildPod(namespace, config) if err != nil { return "", fmt.Errorf("failed to build pod spec: %w", err) @@ -333,7 +384,14 @@ func (r *KubernetesRuntime) Run(ctx context.Context, config RunConfig) (string, } } - if config.Workspace != "" { + // Workspace sync: NFS-backed pods have workspace bytes pre-populated by the + // init container (N2-2), so skip the kubectl-cp workspace sync. This avoids + // redundantly copying workspace contents that already exist on the shared + // NFS volume. Local-backend pods RETAIN the existing workspace sync. + // + // Home-dir sync and the startup gate (/tmp/.scion-home-ready) are RETAINED + // for both backends — they carry agent dotfiles and secrets, not workspace code. + if config.Workspace != "" && config.WorkspaceBackendName != "nfs" { runtimeLog.Info("Syncing workspace", "agent", config.Name, "source", config.Workspace, "phase", "workspace-sync") fmt.Printf(" Syncing workspace (%s -> /workspace)...\n", config.Workspace) err = r.syncWithRetry(ctx, func() error { @@ -347,6 +405,9 @@ func (r *KubernetesRuntime) Run(ctx context.Context, config RunConfig) (string, if _, err := r.execInPod(ctx, namespace, createdPod.Name, []string{"sh", "-c", chownCmd}); err != nil { runtimeLog.Debug("Failed to chown workspace (non-fatal)", "error", err) } + } else if config.WorkspaceBackendName == "nfs" { + runtimeLog.Info("Skipping workspace sync (NFS backend: workspace pre-populated by init container)", + "agent", config.Name, "phase", "workspace-sync-skip") } // Signal the startup gate: all files are synced and ownership is fixed, @@ -654,10 +715,28 @@ func (r *KubernetesRuntime) createAuthFileSecret(ctx context.Context, namespace, return nil } -// sharedDirPVCName returns the deterministic PVC name for a project shared directory. +// --- Generalized project RWX claim helpers (N2-5) --- +// +// These helpers manage project-scoped PVCs for both shared directories and +// (future) workspace claims. The naming convention and lifecycle are identical; +// only the label selector differs. +// +// When backend=nfs, shared dirs are served from the workspace NFS PVC via +// subPath (e.g., "projects//shared-dirs/") and do NOT need their +// own PVC — the NFS volume already provides RWX access. The create/cleanup +// helpers short-circuit for NFS. + +// projectRWXClaimName returns a deterministic PVC name for a project-scoped +// RWX claim. Usable for shared dirs ("shared") and workspace claims ("workspace"). // PVCs are project-scoped (not agent-scoped), so multiple agents share the same PVC. +func projectRWXClaimName(projectName, claimType, dirName string) string { + return fmt.Sprintf("scion-%s-%s-%s", claimType, projectName, dirName) +} + +// sharedDirPVCName returns the deterministic PVC name for a project shared directory. +// This is a convenience wrapper around projectRWXClaimName for backward compatibility. func sharedDirPVCName(projectName, dirName string) string { - return fmt.Sprintf("scion-shared-%s-%s", projectName, dirName) + return projectRWXClaimName(projectName, "shared", dirName) } // defaultSharedDirSize is the default PVC size when not specified in settings. @@ -666,11 +745,22 @@ const defaultSharedDirSize = "10Gi" // createSharedDirPVCs ensures PVCs exist for all declared shared directories. // PVCs are project-scoped and persist across agent restarts. If a PVC already // exists (from a previous agent in the same project), it is reused. +// +// When backend=nfs, shared dirs are served via NFS subPath from the workspace +// PVC and do NOT require separate PVCs — this method is a no-op for NFS. func (r *KubernetesRuntime) createSharedDirPVCs(ctx context.Context, namespace string, config RunConfig) error { if len(config.SharedDirs) == 0 { return nil } + // NFS backend: shared dirs use subPaths on the workspace NFS PVC, + // no separate PVCs needed (design §5.3). + if config.WorkspaceBackendName == "nfs" && config.NFSPVClaimName != "" { + runtimeLog.Info("NFS backend: shared dirs served via NFS subPath, skipping PVC creation", + "shared_dir_count", len(config.SharedDirs)) + return nil + } + projectID := config.Labels["scion.project_id"] if projectID == "" { projectID = config.Labels["scion.grove_id"] @@ -702,49 +792,63 @@ func (r *KubernetesRuntime) createSharedDirPVCs(ctx context.Context, namespace s } for _, sd := range config.SharedDirs { - pvcName := sharedDirPVCName(projectName, sd.Name) - - // Check if PVC already exists (project-scoped, may have been created by another agent) - _, err := r.Client.Clientset.CoreV1().PersistentVolumeClaims(namespace).Get(ctx, pvcName, metav1.GetOptions{}) - if err == nil { - runtimeLog.Info("Shared dir PVC already exists, reusing", "pvc", pvcName, "shared_dir", sd.Name) - continue + if err := r.ensureProjectRWXClaim(ctx, namespace, projectName, projectID, sd.Name, storageClass, storageQuantity); err != nil { + return err } + } - accessMode := corev1.ReadWriteMany - pvc := &corev1.PersistentVolumeClaim{ - ObjectMeta: metav1.ObjectMeta{ - Name: pvcName, - Namespace: namespace, - Labels: map[string]string{ - "scion.project": projectName, - "scion.grove": projectName, - "scion.shared-dir": sd.Name, - }, + return nil +} + +// ensureProjectRWXClaim is the idempotent get-or-create core for project-scoped +// RWX PVCs. It creates a PVC with a deterministic name if one does not already +// exist. Used by both shared-dir and (future) workspace claim paths. +func (r *KubernetesRuntime) ensureProjectRWXClaim( + ctx context.Context, + namespace, projectName, projectID, dirName, storageClass string, + storageQuantity resource.Quantity, +) error { + pvcName := sharedDirPVCName(projectName, dirName) + + // Check if PVC already exists (project-scoped, may have been created by another agent) + _, err := r.Client.Clientset.CoreV1().PersistentVolumeClaims(namespace).Get(ctx, pvcName, metav1.GetOptions{}) + if err == nil { + runtimeLog.Info("Project RWX PVC already exists, reusing", "pvc", pvcName, "dir", dirName) + return nil + } + + pvc := &corev1.PersistentVolumeClaim{ + ObjectMeta: metav1.ObjectMeta{ + Name: pvcName, + Namespace: namespace, + Labels: map[string]string{ + "scion.project": projectName, + "scion.grove": projectName, + "scion.shared-dir": dirName, }, - Spec: corev1.PersistentVolumeClaimSpec{ - AccessModes: []corev1.PersistentVolumeAccessMode{accessMode}, - Resources: corev1.VolumeResourceRequirements{ - Requests: corev1.ResourceList{ - corev1.ResourceStorage: storageQuantity, - }, + }, + Spec: corev1.PersistentVolumeClaimSpec{ + AccessModes: []corev1.PersistentVolumeAccessMode{corev1.ReadWriteMany}, + Resources: corev1.VolumeResourceRequirements{ + Requests: corev1.ResourceList{ + corev1.ResourceStorage: storageQuantity, }, }, - } + }, + } - if projectID != "" { - pvc.Labels["scion.project_id"] = projectID - pvc.Labels["scion.grove_id"] = projectID - } + if projectID != "" { + pvc.Labels["scion.project_id"] = projectID + pvc.Labels["scion.grove_id"] = projectID + } - if storageClass != "" { - pvc.Spec.StorageClassName = &storageClass - } + if storageClass != "" { + pvc.Spec.StorageClassName = &storageClass + } - runtimeLog.Info("Creating shared dir PVC", "pvc", pvcName, "shared_dir", sd.Name, "size", size) - if _, err := r.Client.Clientset.CoreV1().PersistentVolumeClaims(namespace).Create(ctx, pvc, metav1.CreateOptions{}); err != nil { - return fmt.Errorf("failed to create shared dir PVC %q: %w", pvcName, err) - } + runtimeLog.Info("Creating project RWX PVC", "pvc", pvcName, "dir", dirName, "storage", storageQuantity.String()) + if _, err := r.Client.Clientset.CoreV1().PersistentVolumeClaims(namespace).Create(ctx, pvc, metav1.CreateOptions{}); err != nil { + return fmt.Errorf("failed to create project RWX PVC %q: %w", pvcName, err) } return nil @@ -752,19 +856,27 @@ func (r *KubernetesRuntime) createSharedDirPVCs(ctx context.Context, namespace s // cleanupSharedDirPVCs removes PVCs for shared directories belonging to a project. // This is called during project deletion, not agent deletion, since PVCs are project-scoped. +// When backend=nfs, shared dirs live on the NFS volume (no separate PVCs) but the +// cleanup still runs — it harmlessly finds nothing because no PVCs were created. func (r *KubernetesRuntime) cleanupSharedDirPVCs(ctx context.Context, namespace, projectName string) { - selector := fmt.Sprintf("scion.grove=%s,scion.shared-dir", projectName) + r.cleanupProjectRWXClaims(ctx, namespace, projectName, "scion.shared-dir") +} + +// cleanupProjectRWXClaims is the generic cleanup helper for project-scoped RWX PVCs. +// It lists PVCs matching the project and label key, then deletes them. +func (r *KubernetesRuntime) cleanupProjectRWXClaims(ctx context.Context, namespace, projectName, labelKey string) { + selector := fmt.Sprintf("scion.grove=%s,%s", projectName, labelKey) pvcList, err := r.Client.Clientset.CoreV1().PersistentVolumeClaims(namespace).List(ctx, metav1.ListOptions{ LabelSelector: selector, }) if err != nil { - runtimeLog.Warn("Failed to list shared dir PVCs for cleanup", "grove_id", projectName, "error", err) + runtimeLog.Warn("Failed to list project RWX PVCs for cleanup", "project", projectName, "label", labelKey, "error", err) return } for _, pvc := range pvcList.Items { - runtimeLog.Info("Deleting shared dir PVC", "pvc", pvc.Name, "grove_id", projectName) + runtimeLog.Info("Deleting project RWX PVC", "pvc", pvc.Name, "project", projectName) if err := r.Client.Clientset.CoreV1().PersistentVolumeClaims(namespace).Delete(ctx, pvc.Name, metav1.DeleteOptions{}); err != nil { - runtimeLog.Warn("Failed to delete shared dir PVC", "pvc", pvc.Name, "error", err) + runtimeLog.Warn("Failed to delete project RWX PVC", "pvc", pvc.Name, "error", err) } } } @@ -1016,14 +1128,25 @@ func (r *KubernetesRuntime) buildPod(namespace string, config RunConfig) (*corev corev1.EnvVar{Name: "LOGNAME", Value: config.UnixUsername}, ) - // Security context: run agent pods as the image's non-root scion user and - // keep FSGroup aligned with the broker user so synced files remain writable. + // Security context: run agent pods as the image's non-root scion user. + // FSGroup is branched by workspace backend (N2-4): + // - NFS backend: stable GID (default 1000) so files are writable across + // pods and nodes without per-start chown (design §9.1). + // - Local backend: host GID (today's behavior) so synced files remain + // writable by the broker user. const containerUID int64 = 1000 - hostGID := int64(os.Getgid()) + fsGroupGID := int64(os.Getgid()) // default: host GID (local backend) + if config.WorkspaceBackendName == "nfs" { + nfsGID := config.NFSGID + if nfsGID == 0 { + nfsGID = 1000 // design default + } + fsGroupGID = int64(nfsGID) + } runAsNonRoot := true allowPrivilegeEscalation := false podSecurityContext := &corev1.PodSecurityContext{ - FSGroup: &hostGID, + FSGroup: &fsGroupGID, RunAsUser: int64Ptr(containerUID), RunAsGroup: int64Ptr(containerUID), RunAsNonRoot: &runAsNonRoot, @@ -1047,6 +1170,38 @@ func (r *KubernetesRuntime) buildPod(namespace string, config RunConfig) (*corev } } + // Workspace volume: NFS-backed pods use a PVC+subPath for shared, persistent + // storage isolated to the project subtree (design §5.1/§9.4). + // Local-backend pods keep the existing EmptyDir (zero behavior change). + var workspaceVolume corev1.Volume + var workspaceVolumeMount corev1.VolumeMount + if config.WorkspaceBackendName == "nfs" && config.NFSPVClaimName != "" { + workspaceVolume = corev1.Volume{ + Name: "workspace", + VolumeSource: corev1.VolumeSource{ + PersistentVolumeClaim: &corev1.PersistentVolumeClaimVolumeSource{ + ClaimName: config.NFSPVClaimName, + }, + }, + } + workspaceVolumeMount = corev1.VolumeMount{ + Name: "workspace", + MountPath: "/workspace", + SubPath: config.NFSSubPath, + } + } else { + workspaceVolume = corev1.Volume{ + Name: "workspace", + VolumeSource: corev1.VolumeSource{ + EmptyDir: &corev1.EmptyDirVolumeSource{}, + }, + } + workspaceVolumeMount = corev1.VolumeMount{ + Name: "workspace", + MountPath: "/workspace", + } + } + pod := &corev1.Pod{ ObjectMeta: metav1.ObjectMeta{ Name: config.Name, @@ -1072,23 +1227,61 @@ func (r *KubernetesRuntime) buildPod(namespace string, config RunConfig) (*corev Drop: []corev1.Capability{"ALL"}, }, }, - VolumeMounts: []corev1.VolumeMount{ - {Name: "workspace", MountPath: "/workspace"}, - }, - }, - }, - Volumes: []corev1.Volume{ - { - Name: "workspace", - VolumeSource: corev1.VolumeSource{ - EmptyDir: &corev1.EmptyDirVolumeSource{}, - }, + VolumeMounts: []corev1.VolumeMount{workspaceVolumeMount}, }, }, + Volumes: []corev1.Volume{workspaceVolume}, RestartPolicy: corev1.RestartPolicyNever, }, } + // NFS init container: when backend=nfs and git clone config is set, add an + // init container that provisions the workspace before the main container + // starts. The init container mounts the same workspace PVC+subPath so + // provisioned files are visible to the main container. + // + // Advisory lock integration (N2-2b, design §7, risk RN1): the Go-side + // Run() method acquires a per-project advisory lock (via TryAdvisoryLockObject) + // BEFORE reaching this point. The lock result determines the init container + // behavior: + // - Lock winner (nfsProvisionLockLost=false): injects the CLONING init + // container that checks the sentinel and clones if absent (N2-2 script). + // - Lock loser (nfsProvisionLockLost=true): injects a WAIT-for-sentinel + // init container that polls for .scion-provisioned without cloning. + // + // When no advisory locker is available (Locker nil / single-node deploy), + // nfsProvisionLockLost stays false and the cloning init container is + // injected — the sentinel provides idempotent protection but NOT + // cross-node mutual exclusion. + if config.WorkspaceBackendName == "nfs" && config.NFSPVClaimName != "" && config.GitCloneForInit != nil { + var initCommand []string + if config.nfsProvisionLockLost { + // Lock loser: wait for the sentinel written by the winning node's + // cloning init container. Does NOT clone. + initCommand = []string{"sciontool", "provision", "--wait-for-sentinel"} + } else { + // Lock winner (or no locker available): clone if sentinel is absent, + // skip if already provisioned. The command is idempotent. + initCommand = nfsProvisionCommand(config.GitCloneForInit) + } + initContainer := corev1.Container{ + Name: "workspace-provision", + Image: config.Image, + Command: initCommand, + Env: nfsProvisionEnv(config.GitCloneForInit), + VolumeMounts: []corev1.VolumeMount{ + workspaceVolumeMount, + }, + SecurityContext: &corev1.SecurityContext{ + AllowPrivilegeEscalation: &allowPrivilegeEscalation, + Capabilities: &corev1.Capabilities{ + Drop: []corev1.Capability{"ALL"}, + }, + }, + } + pod.Spec.InitContainers = append(pod.Spec.InitContainers, initContainer) + } + // Append secret volumes and mounts if len(extraVolumes) > 0 { pod.Spec.Volumes = append(pod.Spec.Volumes, extraVolumes...) @@ -1179,13 +1372,20 @@ func (r *KubernetesRuntime) buildPod(namespace string, config RunConfig) (*corev } } - // Process shared directories — create PVC-backed volumes and mounts. + // Process shared directories — mount shared-dir volumes. // Build a set of shared dir targets so we can skip them in the regular volume loop. + // + // NFS backend (N2-5): shared dirs are served from the SAME workspace NFS PVC + // via subPath (e.g., "projects//shared-dirs/"), avoiding per-dir PVCs. + // The workspace volume is already defined; we add additional subPath mounts. + // + // Local backend: each shared dir gets its own PVC (existing behavior, unchanged). k8sContainerWorkspace := config.ContainerWorkspace if k8sContainerWorkspace == "" { k8sContainerWorkspace = "/workspace" } sharedDirTargets := make(map[string]bool, len(config.SharedDirs)) + nfsSharedDirs := config.WorkspaceBackendName == "nfs" && config.NFSPVClaimName != "" for i, sd := range config.SharedDirs { target := fmt.Sprintf("/scion-volumes/%s", sd.Name) if sd.InWorkspace { @@ -1193,24 +1393,52 @@ func (r *KubernetesRuntime) buildPod(namespace string, config RunConfig) (*corev } sharedDirTargets[target] = true - projectName := config.Labels["scion.grove"] - pvcName := sharedDirPVCName(projectName, sd.Name) - volName := fmt.Sprintf("shared-dir-%d", i) + if nfsSharedDirs { + // NFS backend: mount from the workspace PVC with a shared-dir subPath. + // SubPath root mirrors the nfsBackend.Resolve layout: + // //shared-dirs/ + sdSubPath := nfsSharedDirSubPath(config.NFSSubPath, sd.Name) + volName := fmt.Sprintf("shared-dir-%d", i) - pod.Spec.Volumes = append(pod.Spec.Volumes, corev1.Volume{ - Name: volName, - VolumeSource: corev1.VolumeSource{ - PersistentVolumeClaim: &corev1.PersistentVolumeClaimVolumeSource{ - ClaimName: pvcName, - ReadOnly: sd.ReadOnly, + // The volume source is the SAME NFS PVC as the workspace — but K8s + // requires a separate Volume entry per unique (claimName, subPath) + // pair in the pod spec, so we add the volume under a distinct name. + pod.Spec.Volumes = append(pod.Spec.Volumes, corev1.Volume{ + Name: volName, + VolumeSource: corev1.VolumeSource{ + PersistentVolumeClaim: &corev1.PersistentVolumeClaimVolumeSource{ + ClaimName: config.NFSPVClaimName, + ReadOnly: sd.ReadOnly, + }, }, - }, - }) - pod.Spec.Containers[0].VolumeMounts = append(pod.Spec.Containers[0].VolumeMounts, corev1.VolumeMount{ - Name: volName, - MountPath: target, - ReadOnly: sd.ReadOnly, - }) + }) + pod.Spec.Containers[0].VolumeMounts = append(pod.Spec.Containers[0].VolumeMounts, corev1.VolumeMount{ + Name: volName, + MountPath: target, + SubPath: sdSubPath, + ReadOnly: sd.ReadOnly, + }) + } else { + // Local backend: each shared dir gets its own PVC (existing behavior). + projectName := config.Labels["scion.grove"] + pvcName := sharedDirPVCName(projectName, sd.Name) + volName := fmt.Sprintf("shared-dir-%d", i) + + pod.Spec.Volumes = append(pod.Spec.Volumes, corev1.Volume{ + Name: volName, + VolumeSource: corev1.VolumeSource{ + PersistentVolumeClaim: &corev1.PersistentVolumeClaimVolumeSource{ + ClaimName: pvcName, + ReadOnly: sd.ReadOnly, + }, + }, + }) + pod.Spec.Containers[0].VolumeMounts = append(pod.Spec.Containers[0].VolumeMounts, corev1.VolumeMount{ + Name: volName, + MountPath: target, + ReadOnly: sd.ReadOnly, + }) + } } // Process Volumes @@ -2179,3 +2407,50 @@ func (r *KubernetesRuntime) GetWorkspacePath(ctx context.Context, id string) (st return "", fmt.Errorf("no workspace path found for pod %s", id) } + +// nfsSharedDirSubPath computes the NFS subPath for a shared directory given the +// workspace subPath. The workspace subPath is like "projects//workspace"; +// shared dirs are siblings: "projects//shared-dirs/". +// +// This mirrors the nfsBackend.Resolve layout (design §5.3). +func nfsSharedDirSubPath(workspaceSubPath, sharedDirName string) string { + // workspaceSubPath is "projects//workspace" + // We need "projects//shared-dirs/" + parent := filepath.Dir(workspaceSubPath) // "projects/" + return filepath.Join(parent, "shared-dirs", sharedDirName) +} + +// nfsProvisionCommand builds the Command slice for the lock-winner init +// container. It invokes `sciontool provision` with numeric/enum flags for +// depth and mode. URL and branch are passed via env vars (nfsProvisionEnv) +// to prevent shell injection. +func nfsProvisionCommand(gc *api.GitCloneConfig) []string { + if gc == nil || gc.URL == "" { + return []string{"sciontool", "provision"} + } + + depth := gc.Depth + if depth == 0 { + depth = 1 + } + + return []string{ + "sciontool", "provision", + "--depth", fmt.Sprintf("%d", depth), + } +} + +// nfsProvisionEnv returns the environment variables for the NFS init +// container. URL and branch are passed as env vars to prevent shell injection. +func nfsProvisionEnv(gc *api.GitCloneConfig) []corev1.EnvVar { + if gc == nil { + return nil + } + envs := []corev1.EnvVar{ + {Name: "SCION_CLONE_URL", Value: gc.URL}, + } + if gc.Branch != "" { + envs = append(envs, corev1.EnvVar{Name: "SCION_CLONE_BRANCH", Value: gc.Branch}) + } + return envs +} diff --git a/pkg/runtime/nfs_path_guard.go b/pkg/runtime/nfs_path_guard.go new file mode 100644 index 000000000..dfabe41a5 --- /dev/null +++ b/pkg/runtime/nfs_path_guard.go @@ -0,0 +1,96 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package runtime + +import ( + "fmt" + "path/filepath" + "strings" + + "github.com/GoogleCloudPlatform/scion/pkg/api" +) + +// ValidateNotExportRoot ensures that hostPath is a proper subdirectory of +// hostBase, never equal to it. This is the critical isolation guard from +// design §9.4: bind-mount only projects//..., NEVER the export root +// / itself. +// +// Returns nil if hostBase is empty (local backend — no guard needed). +func ValidateNotExportRoot(hostPath, hostBase string) error { + if hostBase == "" { + return nil // local backend — no export root to guard against + } + + cleanPath := filepath.Clean(hostPath) + cleanBase := filepath.Clean(hostBase) + + if cleanPath == cleanBase { + return fmt.Errorf("isolation violation: bind path %q equals export root %q — "+ + "must bind a project subtree, never the export root", hostPath, hostBase) + } + + if !strings.HasPrefix(cleanPath, cleanBase+"/") { + return fmt.Errorf("isolation violation: bind path %q is not under export root %q", + hostPath, hostBase) + } + + return nil +} + +// NFSSharedDirsToVolumeMounts converts shared directory declarations into +// VolumeMount entries using NFS-resolved paths from a ResolvedWorkspace. +// This is the NFS counterpart of config.SharedDirsToVolumeMounts — the +// container-side targets are unchanged (/scion-volumes/ or in-workspace), +// but the host-side source paths come from the NFS backend's Resolve output +// instead of the local filesystem helpers. +// +// The containerWorkspace parameter specifies the container-side workspace path +// (e.g., /workspace). The resolved workspace must have been produced by +// nfsBackend.Resolve with the shared dir names included in ResolveInput. +func NFSSharedDirsToVolumeMounts(resolved ResolvedWorkspace, dirs []api.SharedDir, containerWorkspace string) ([]api.VolumeMount, error) { + if len(dirs) == 0 { + return nil, nil + } + + if containerWorkspace == "" { + containerWorkspace = "/workspace" + } + + var mounts []api.VolumeMount + for _, d := range dirs { + sd, ok := resolved.SharedDirs[d.Name] + if !ok { + return nil, fmt.Errorf("shared dir %q not found in NFS resolution", d.Name) + } + + // Isolation guard: shared dir paths must be under the host base, not the root. + if err := ValidateNotExportRoot(sd.HostPath, resolved.HostBase); err != nil { + return nil, fmt.Errorf("shared dir %q: %w", d.Name, err) + } + + target := fmt.Sprintf("/scion-volumes/%s", d.Name) + if d.InWorkspace { + target = fmt.Sprintf("%s/.scion-volumes/%s", containerWorkspace, d.Name) + } + + mounts = append(mounts, api.VolumeMount{ + Source: sd.HostPath, + Target: target, + ReadOnly: d.ReadOnly, + }) + } + + return mounts, nil +} diff --git a/pkg/runtime/nfs_path_guard_test.go b/pkg/runtime/nfs_path_guard_test.go new file mode 100644 index 000000000..ca6e923b6 --- /dev/null +++ b/pkg/runtime/nfs_path_guard_test.go @@ -0,0 +1,368 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package runtime + +import ( + "path/filepath" + "testing" + + "github.com/GoogleCloudPlatform/scion/pkg/api" + "github.com/GoogleCloudPlatform/scion/pkg/config" + "github.com/GoogleCloudPlatform/scion/pkg/store" +) + +// --- ValidateNotExportRoot tests --- + +func TestValidateNotExportRoot_Valid(t *testing.T) { + tests := []struct { + name string + hostPath string + hostBase string + }{ + { + name: "project subtree under mount", + hostPath: "/mnt/nfs/ws1/projects/proj1/workspace", + hostBase: "/mnt/nfs/ws1", + }, + { + name: "shared dir under mount", + hostPath: "/mnt/nfs/ws1/projects/proj1/shared-dirs/data", + hostBase: "/mnt/nfs/ws1", + }, + { + name: "local backend empty hostBase", + hostPath: "/home/user/.scion.projects/my-project", + hostBase: "", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if err := ValidateNotExportRoot(tt.hostPath, tt.hostBase); err != nil { + t.Errorf("expected valid, got error: %v", err) + } + }) + } +} + +func TestValidateNotExportRoot_Invalid(t *testing.T) { + tests := []struct { + name string + hostPath string + hostBase string + }{ + { + name: "path equals export root", + hostPath: "/mnt/nfs/ws1", + hostBase: "/mnt/nfs/ws1", + }, + { + name: "path equals export root with trailing slash", + hostPath: "/mnt/nfs/ws1/", + hostBase: "/mnt/nfs/ws1", + }, + { + name: "path not under export root", + hostPath: "/some/other/path", + hostBase: "/mnt/nfs/ws1", + }, + { + name: "path is sibling of export root", + hostPath: "/mnt/nfs/ws1-other/projects/proj1", + hostBase: "/mnt/nfs/ws1", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if err := ValidateNotExportRoot(tt.hostPath, tt.hostBase); err == nil { + t.Error("expected error for isolation violation, got nil") + } + }) + } +} + +// --- NFSSharedDirsToVolumeMounts tests --- + +func TestNFSSharedDirsToVolumeMounts_Basic(t *testing.T) { + resolved := ResolvedWorkspace{ + HostPath: "/mnt/nfs/ws1/projects/proj1/workspace", + HostBase: "/mnt/nfs/ws1", + Backend: "nfs", + SharedDirs: map[string]ResolvedSharedDir{ + "data": { + HostPath: "/mnt/nfs/ws1/projects/proj1/shared-dirs/data", + ServerRelativePath: "projects/proj1/shared-dirs/data", + }, + "cache": { + HostPath: "/mnt/nfs/ws1/projects/proj1/shared-dirs/cache", + ServerRelativePath: "projects/proj1/shared-dirs/cache", + }, + }, + } + + dirs := []api.SharedDir{ + {Name: "data", ReadOnly: false}, + {Name: "cache", ReadOnly: true, InWorkspace: true}, + } + + mounts, err := NFSSharedDirsToVolumeMounts(resolved, dirs, "/workspace") + if err != nil { + t.Fatalf("NFSSharedDirsToVolumeMounts: %v", err) + } + + if len(mounts) != 2 { + t.Fatalf("len(mounts) = %d, want 2", len(mounts)) + } + + // data → /scion-volumes/data + if mounts[0].Source != "/mnt/nfs/ws1/projects/proj1/shared-dirs/data" { + t.Errorf("mounts[0].Source = %q, want NFS path", mounts[0].Source) + } + if mounts[0].Target != "/scion-volumes/data" { + t.Errorf("mounts[0].Target = %q, want /scion-volumes/data", mounts[0].Target) + } + if mounts[0].ReadOnly { + t.Error("mounts[0].ReadOnly should be false") + } + + // cache → /workspace/.scion-volumes/cache (InWorkspace=true) + if mounts[1].Source != "/mnt/nfs/ws1/projects/proj1/shared-dirs/cache" { + t.Errorf("mounts[1].Source = %q, want NFS path", mounts[1].Source) + } + if mounts[1].Target != "/workspace/.scion-volumes/cache" { + t.Errorf("mounts[1].Target = %q, want /workspace/.scion-volumes/cache", mounts[1].Target) + } + if !mounts[1].ReadOnly { + t.Error("mounts[1].ReadOnly should be true") + } +} + +func TestNFSSharedDirsToVolumeMounts_MissingDir(t *testing.T) { + resolved := ResolvedWorkspace{ + HostBase: "/mnt/nfs/ws1", + Backend: "nfs", + SharedDirs: map[string]ResolvedSharedDir{}, + } + + dirs := []api.SharedDir{ + {Name: "nonexistent"}, + } + + _, err := NFSSharedDirsToVolumeMounts(resolved, dirs, "/workspace") + if err == nil { + t.Error("expected error for missing shared dir in resolution") + } +} + +func TestNFSSharedDirsToVolumeMounts_Empty(t *testing.T) { + resolved := ResolvedWorkspace{Backend: "nfs"} + mounts, err := NFSSharedDirsToVolumeMounts(resolved, nil, "/workspace") + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if mounts != nil { + t.Errorf("expected nil mounts for empty dirs, got %v", mounts) + } +} + +func TestNFSSharedDirsToVolumeMounts_IsolationGuard(t *testing.T) { + // Simulate a bad resolution where shared dir path equals the export root. + resolved := ResolvedWorkspace{ + HostPath: "/mnt/nfs/ws1/projects/proj1/workspace", + HostBase: "/mnt/nfs/ws1", + Backend: "nfs", + SharedDirs: map[string]ResolvedSharedDir{ + "bad": { + HostPath: "/mnt/nfs/ws1", // equals export root — should be rejected + }, + }, + } + + dirs := []api.SharedDir{{Name: "bad"}} + + _, err := NFSSharedDirsToVolumeMounts(resolved, dirs, "/workspace") + if err == nil { + t.Error("expected error when shared dir path equals export root") + } +} + +// --- NFS Realize isolation guard tests --- + +func TestNFSRealize_IsolationGuard(t *testing.T) { + nfsCfg := &config.V1NFSConfig{ + MountRoot: "/mnt/nfs", + SubPathRoot: "projects", + Shares: []config.V1NFSShare{ + {ID: "share1", Server: "10.0.0.2", Export: "/scion-workspaces"}, + }, + } + b := NewNFSBackend(nfsCfg) + + // Valid: project subtree + _, err := b.Realize(RealizeInput{ + Resolved: ResolvedWorkspace{ + HostPath: "/mnt/nfs/share1/projects/proj1/workspace", + HostBase: "/mnt/nfs/share1", + Backend: "nfs", + ServerRelativePath: "projects/proj1/workspace", + }, + ContainerWorkspace: "/workspace", + }) + if err != nil { + t.Errorf("valid Realize returned error: %v", err) + } + + // Invalid: export root itself + _, err = b.Realize(RealizeInput{ + Resolved: ResolvedWorkspace{ + HostPath: "/mnt/nfs/share1", + HostBase: "/mnt/nfs/share1", + Backend: "nfs", + }, + ContainerWorkspace: "/workspace", + }) + if err == nil { + t.Error("expected error when HostPath equals export root") + } +} + +// --- NFS path resolution produces correct paths (end-to-end) --- + +func TestNFSResolveRealize_EndToEnd(t *testing.T) { + nfsCfg := &config.V1NFSConfig{ + MountRoot: "/mnt/nfs", + SubPathRoot: "projects", + Shares: []config.V1NFSShare{ + {ID: "ws1", Server: "10.0.0.2", Export: "/scion-workspaces"}, + }, + } + + backend := NewNFSBackend(nfsCfg) + resolved, err := backend.Resolve(ResolveInput{ + ProjectID: "my-project-id", + AgentID: "agent-1", + Mode: store.SharingModeSharedPlain, + SharedDirNames: []string{"data"}, + }) + if err != nil { + t.Fatalf("Resolve: %v", err) + } + + // Verify workspace path + wantWorkspace := filepath.Join("/mnt/nfs", "ws1", "projects", "my-project-id", "workspace") + if resolved.HostPath != wantWorkspace { + t.Errorf("HostPath = %q, want %q", resolved.HostPath, wantWorkspace) + } + + // Realize should produce a valid descriptor + desc, err := backend.Realize(RealizeInput{ + Resolved: resolved, + ContainerWorkspace: "/workspace", + }) + if err != nil { + t.Fatalf("Realize: %v", err) + } + if desc.Type != "nfs" { + t.Errorf("desc.Type = %q, want nfs", desc.Type) + } + if desc.HostPath != wantWorkspace { + t.Errorf("desc.HostPath = %q, want %q", desc.HostPath, wantWorkspace) + } + if desc.Target != "/workspace" { + t.Errorf("desc.Target = %q, want /workspace", desc.Target) + } + + // Shared dirs should produce NFS-backed mounts + dirs := []api.SharedDir{{Name: "data"}} + mounts, err := NFSSharedDirsToVolumeMounts(resolved, dirs, "/workspace") + if err != nil { + t.Fatalf("NFSSharedDirsToVolumeMounts: %v", err) + } + wantSDPath := filepath.Join("/mnt/nfs", "ws1", "projects", "my-project-id", "shared-dirs", "data") + if len(mounts) != 1 || mounts[0].Source != wantSDPath { + t.Errorf("shared dir mount source = %v, want %q", mounts, wantSDPath) + } +} + +// --- Local path resolution unchanged (zero behavior change guard) --- + +func TestLocalResolveRealize_Unchanged(t *testing.T) { + backend := NewLocalBackend() + projectPath := "/home/scion/.scion.projects/my-project" + + resolved, err := backend.Resolve(ResolveInput{ + ProjectDir: projectPath, + ProjectID: "proj1", + Mode: store.SharingModeSharedPlain, + }) + if err != nil { + t.Fatalf("Resolve: %v", err) + } + + // Local backend produces the exact project path — no change. + if resolved.HostPath != projectPath { + t.Errorf("HostPath = %q, want %q (unchanged)", resolved.HostPath, projectPath) + } + if resolved.Backend != "local" { + t.Errorf("Backend = %q, want local", resolved.Backend) + } + + // Realize produces a local bind-mount descriptor. + desc, err := backend.Realize(RealizeInput{ + Resolved: resolved, + ContainerWorkspace: "/workspace", + }) + if err != nil { + t.Fatalf("Realize: %v", err) + } + if desc.Type != "local" { + t.Errorf("desc.Type = %q, want local", desc.Type) + } + if desc.HostPath != projectPath { + t.Errorf("desc.HostPath = %q, want %q (unchanged)", desc.HostPath, projectPath) + } +} + +// TestLocalSharedDirs_Unchanged verifies that the local backend's shared dir +// resolution is unchanged — the existing config.SharedDirsToVolumeMounts path +// is still used for local backends. This test documents the invariant. +func TestLocalSharedDirs_Unchanged(t *testing.T) { + // The local path calls config.GetSharedDirPath which works on the + // local filesystem. For NFS, NFSSharedDirsToVolumeMounts is used instead. + // This test just ensures local path is still exposed for backward compat. + backend := NewLocalBackend() + resolved, err := backend.Resolve(ResolveInput{ + ProjectDir: "/home/scion/.scion.projects/my-project", + ProjectID: "proj1", + Mode: store.SharingModeSharedPlain, + SharedDirNames: []string{"logs"}, + }) + if err != nil { + t.Fatalf("Resolve: %v", err) + } + + // Local backend resolves shared dirs via config helpers + sd, ok := resolved.SharedDirs["logs"] + if !ok { + t.Fatal("shared dir 'logs' not found in local resolution") + } + if sd.HostPath == "" { + t.Error("shared dir host path should not be empty for local backend") + } + if sd.ServerRelativePath != "" { + t.Error("shared dir ServerRelativePath should be empty for local backend") + } +} diff --git a/pkg/runtime/nfs_uid_test.go b/pkg/runtime/nfs_uid_test.go new file mode 100644 index 000000000..3f4284b0c --- /dev/null +++ b/pkg/runtime/nfs_uid_test.go @@ -0,0 +1,232 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package runtime + +import ( + "context" + "embed" + "fmt" + "os" + "strings" + "testing" + + "github.com/GoogleCloudPlatform/scion/pkg/api" +) + +// TestBuildCommonRunArgs_LocalBackend_HostUID verifies that backend=local +// (or empty) advertises the broker's own UID/GID — today's behavior, unchanged. +func TestBuildCommonRunArgs_LocalBackend_HostUID(t *testing.T) { + cfg := minimalRunConfig() + // WorkspaceBackendName defaults to "" (local) + + args, err := buildCommonRunArgs(cfg) + if err != nil { + t.Fatalf("buildCommonRunArgs: %v", err) + } + + wantUID := fmt.Sprintf("SCION_HOST_UID=%d", os.Getuid()) + wantGID := fmt.Sprintf("SCION_HOST_GID=%d", os.Getgid()) + + assertEnvInArgs(t, args, wantUID, "local backend should advertise host UID") + assertEnvInArgs(t, args, wantGID, "local backend should advertise host GID") +} + +// TestBuildCommonRunArgs_NFSBackend_StableUID verifies that backend=nfs +// advertises the configured NFS UID/GID instead of the host UID. +func TestBuildCommonRunArgs_NFSBackend_StableUID(t *testing.T) { + cfg := minimalRunConfig() + cfg.WorkspaceBackendName = "nfs" + cfg.NFSUID = 1000 + cfg.NFSGID = 1000 + + args, err := buildCommonRunArgs(cfg) + if err != nil { + t.Fatalf("buildCommonRunArgs: %v", err) + } + + assertEnvInArgs(t, args, "SCION_HOST_UID=1000", "NFS backend should advertise stable UID 1000") + assertEnvInArgs(t, args, "SCION_HOST_GID=1000", "NFS backend should advertise stable GID 1000") +} + +// TestBuildCommonRunArgs_NFSBackend_CustomUID verifies that NFS UID/GID +// can be set to non-default values via config. +func TestBuildCommonRunArgs_NFSBackend_CustomUID(t *testing.T) { + cfg := minimalRunConfig() + cfg.WorkspaceBackendName = "nfs" + cfg.NFSUID = 2000 + cfg.NFSGID = 2000 + + args, err := buildCommonRunArgs(cfg) + if err != nil { + t.Fatalf("buildCommonRunArgs: %v", err) + } + + assertEnvInArgs(t, args, "SCION_HOST_UID=2000", "NFS backend should use custom UID") + assertEnvInArgs(t, args, "SCION_HOST_GID=2000", "NFS backend should use custom GID") +} + +// TestBuildCommonRunArgs_NFSBackend_DefaultUID verifies that zero NFS UID/GID +// defaults to 1000:1000 (design §9.1 convergence with K8s pod UID/GID). +func TestBuildCommonRunArgs_NFSBackend_DefaultUID(t *testing.T) { + cfg := minimalRunConfig() + cfg.WorkspaceBackendName = "nfs" + cfg.NFSUID = 0 // should default to 1000 + cfg.NFSGID = 0 // should default to 1000 + + args, err := buildCommonRunArgs(cfg) + if err != nil { + t.Fatalf("buildCommonRunArgs: %v", err) + } + + assertEnvInArgs(t, args, "SCION_HOST_UID=1000", "zero NFS UID should default to 1000") + assertEnvInArgs(t, args, "SCION_HOST_GID=1000", "zero NFS GID should default to 1000") +} + +// TestBuildCommonRunArgs_NFSBackend_ExposesBackendEnv verifies that the +// SCION_WORKSPACE_BACKEND env var is set when backend is "nfs", so sciontool +// init can skip the per-start recursive chown. +func TestBuildCommonRunArgs_NFSBackend_ExposesBackendEnv(t *testing.T) { + cfg := minimalRunConfig() + cfg.WorkspaceBackendName = "nfs" + cfg.NFSUID = 1000 + cfg.NFSGID = 1000 + + args, err := buildCommonRunArgs(cfg) + if err != nil { + t.Fatalf("buildCommonRunArgs: %v", err) + } + + assertEnvInArgs(t, args, "SCION_WORKSPACE_BACKEND=nfs", + "NFS backend should expose SCION_WORKSPACE_BACKEND for sciontool init") +} + +// TestBuildCommonRunArgs_LocalBackend_NoBackendEnv verifies that +// SCION_WORKSPACE_BACKEND is not set when the backend is local (empty), +// preserving backward compatibility. +func TestBuildCommonRunArgs_LocalBackend_NoBackendEnv(t *testing.T) { + cfg := minimalRunConfig() + // WorkspaceBackendName defaults to "" + + args, err := buildCommonRunArgs(cfg) + if err != nil { + t.Fatalf("buildCommonRunArgs: %v", err) + } + + for i, arg := range args { + if i > 0 && args[i-1] == "-e" && strings.HasPrefix(arg, "SCION_WORKSPACE_BACKEND=") { + t.Error("local backend should not set SCION_WORKSPACE_BACKEND env var") + } + } +} + +// TestPodmanRootless_NFSBackend_Rejected verifies that Podman rootless + NFS +// is rejected with a clear error (design §9.1: keep-id subuid ranges yield +// no stable on-wire UID). +func TestPodmanRootless_NFSBackend_Rejected(t *testing.T) { + r := &PodmanRuntime{ + Command: "podman", + Rootless: true, + } + + config := minimalRunConfig() + config.WorkspaceBackendName = "nfs" + + _, err := r.Run(t.Context(), config) + if err == nil { + t.Fatal("expected error for Podman rootless + NFS, got nil") + } + if !strings.Contains(err.Error(), "rootless") || !strings.Contains(err.Error(), "NFS") { + t.Errorf("error should mention rootless and NFS, got: %v", err) + } +} + +// TestPodmanRootless_LocalBackend_Allowed verifies that Podman rootless +// with the local backend still works (no regression). +func TestPodmanRootless_LocalBackend_Allowed(t *testing.T) { + r := &PodmanRuntime{ + Command: "podman", + Rootless: true, + } + + config := minimalRunConfig() + config.WorkspaceBackendName = "local" + + // This will fail because podman isn't installed, but it should NOT fail + // with the rootless+NFS rejection error. + _, err := r.Run(t.Context(), config) + if err != nil && strings.Contains(err.Error(), "rootless") && strings.Contains(err.Error(), "NFS") { + t.Errorf("local backend should not trigger rootless+NFS rejection, got: %v", err) + } + // Other errors (podman not installed, etc.) are expected — we only check + // that the NFS-specific guard doesn't fire. +} + +// --- helpers --- + +// minimalRunConfig returns a RunConfig with the minimum fields needed +// to call buildCommonRunArgs without error. It uses a stub harness. +func minimalRunConfig() RunConfig { + return RunConfig{ + Name: "test-agent", + Image: "test-image:latest", + UnixUsername: "scion", + Harness: &nfsTestHarness{}, + Workspace: "/tmp/test-workspace", + } +} + +// nfsTestHarness satisfies the api.Harness interface for N1-5 tests. +type nfsTestHarness struct{} + +func (h *nfsTestHarness) Name() string { return "test" } +func (h *nfsTestHarness) AdvancedCapabilities() api.HarnessAdvancedCapabilities { + return api.HarnessAdvancedCapabilities{Harness: "test"} +} +func (h *nfsTestHarness) GetCommand(task string, resume bool, args []string) []string { + return []string{"echo", "test"} +} +func (h *nfsTestHarness) GetEnv(name, homeDir, unixUsername string) map[string]string { + return map[string]string{} +} +func (h *nfsTestHarness) GetTelemetryEnv() map[string]string { return nil } +func (h *nfsTestHarness) DefaultConfigDir() string { return ".test" } +func (h *nfsTestHarness) SkillsDir() string { return ".test/skills" } +func (h *nfsTestHarness) HasSystemPrompt(agentHome string) bool { return false } +func (h *nfsTestHarness) Provision(ctx context.Context, agentName, agentDir, agentHome, agentWorkspace string) error { + return nil +} +func (h *nfsTestHarness) GetEmbedDir() string { return "test" } +func (h *nfsTestHarness) GetInterruptKey() string { return "C-c" } +func (h *nfsTestHarness) GetHarnessEmbedsFS() (embed.FS, string) { return embed.FS{}, "" } +func (h *nfsTestHarness) InjectAgentInstructions(agentHome string, content []byte) error { + return nil +} +func (h *nfsTestHarness) InjectSystemPrompt(agentHome string, content []byte) error { + return nil +} +func (h *nfsTestHarness) ResolveAuth(auth api.AuthConfig) (*api.ResolvedAuth, error) { + return &api.ResolvedAuth{Method: "test"}, nil +} + +// assertEnvInArgs checks that the -e flag with the given env value appears in args. +func assertEnvInArgs(t *testing.T, args []string, wantEnv, msg string) { + t.Helper() + for i, arg := range args { + if i > 0 && args[i-1] == "-e" && arg == wantEnv { + return + } + } + t.Errorf("%s: env %q not found in args", msg, wantEnv) +} diff --git a/pkg/runtime/podman.go b/pkg/runtime/podman.go index a33698071..7c1fe0b00 100644 --- a/pkg/runtime/podman.go +++ b/pkg/runtime/podman.go @@ -125,6 +125,16 @@ func (r *PodmanRuntime) ExecUser() string { } func (r *PodmanRuntime) Run(ctx context.Context, config RunConfig) (string, error) { + // N1-5: Podman rootless + NFS is unsupported. keep-id subuid ranges + // yield no stable on-wire UID, so files on the shared NFS export would + // have unpredictable ownership across nodes. Reject early with a clear + // error (design §9.1). + if r.Rootless && config.WorkspaceBackendName == "nfs" { + return "", fmt.Errorf("podman rootless with NFS workspace backend is not supported: " + + "keep-id subuid ranges cannot produce a stable on-wire UID for shared NFS storage; " + + "use rootful Docker or Podman for NFS-backed projects") + } + // Stage file and variable secrets before building args var secretMountSpecs []string if config.HomeDir != "" && len(config.ResolvedSecrets) > 0 { diff --git a/pkg/runtime/workspace_backend.go b/pkg/runtime/workspace_backend.go new file mode 100644 index 000000000..986725473 --- /dev/null +++ b/pkg/runtime/workspace_backend.go @@ -0,0 +1,174 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package runtime + +import ( + "github.com/GoogleCloudPlatform/scion/pkg/config" + "github.com/GoogleCloudPlatform/scion/pkg/provision" + "github.com/GoogleCloudPlatform/scion/pkg/store" +) + +// Backward-compatible type aliases for types moved to pkg/provision. +type ProvisionInput = provision.ProvisionInput +type ResolvedWorkspace = provision.ResolvedWorkspace +type ResolvedSharedDir = provision.ResolvedSharedDir + +// WorkspaceBackend abstracts workspace storage so callers can resolve and +// realize workspace paths without knowing whether storage is node-local or +// NFS-backed. The two methods map to the design's questions (§3): +// +// - Resolve: given project/agent/mode, what is the storage location? +// - Realize: emit the runtime mount descriptor for Docker/K8s/Cloud Run. +// +// Provisioning (clone, worktree, mkdir) is handled by the standalone +// ProvisionShared function (Tier 1) — it is vendor-agnostic and operates +// solely on ProvisionInput fields, not on any backend configuration. +// +// Implementations: localBackend (today's node-local behavior, default) and +// nfsBackend (shared network storage). +type WorkspaceBackend interface { + // Resolve computes workspace and shared-dir paths deterministically from + // project/agent IDs and the sharing mode. No DB lookup, no I/O — any + // replica calling Resolve with the same input produces identical paths. + // + // For nfsBackend, ResolvedWorkspace.ServerRelativePath holds the layout + // under the NFS export (e.g. "projects//workspace") and HostBase + // holds the host mount prefix (/). + // + // For localBackend, ResolvedWorkspace.HostPath holds the absolute local + // host path (today's behavior) and ServerRelativePath is empty. + Resolve(in ResolveInput) (ResolvedWorkspace, error) + + // Realize emits the runtime mount descriptor (bind mount source, NFS + // volume, etc.) that the container runtime should use to expose the + // workspace. The returned MountDescriptor is expressive enough for + // Docker bind mounts today and K8s/Cloud Run volumes later. + // + // N1-1 scope: localBackend returns today's local bind mount; nfsBackend + // returns a stub — full wiring lands in N1-3. + Realize(in RealizeInput) (MountDescriptor, error) + + // Name returns a human-readable identifier for this backend ("local" or "nfs"). + Name() string +} + +// ResolveInput contains everything needed to deterministically compute +// workspace and shared-dir paths. All fields are stable IDs — no filesystem +// state is consulted. +type ResolveInput struct { + // ProjectID is the project's stable UUID. + ProjectID string + + // AgentID is the agent's stable UUID (used for worktree-per-agent paths). + AgentID string + + // ProjectSlug is the project's slug (used by localBackend for path resolution). + ProjectSlug string + + // Mode is the canonical workspace sharing mode that governs layout. + Mode store.WorkspaceSharingMode + + // SharedDirNames lists declared shared-dir names to resolve paths for. + SharedDirNames []string + + // ProjectDir is the existing host-side project directory (used by + // localBackend to delegate to existing path-resolution helpers). + // Empty for nfsBackend (paths are derived from IDs, not host state). + ProjectDir string +} + +// RealizeInput holds parameters for emitting a runtime mount descriptor. +type RealizeInput struct { + // Resolved is the output of a prior Resolve call. + Resolved ResolvedWorkspace + + // ContainerWorkspace is the container-side mount target (e.g. "/workspace"). + ContainerWorkspace string +} + +// MountDescriptor describes how the container runtime should mount the +// workspace. It is intentionally expressive enough to cover Docker bind +// mounts (HostPath → Target), K8s PVC+subPath, Cloud Run NFS volumes, +// and vendor-managed volume types. +// +// Type values: +// - "local": Docker bind mount. Fields: HostPath, Target. +// - "nfs": Literal NFS protocol mount (server + export). +// Fields: HostPath, Target, NFSServer, NFSExportPath, SubPath, PVClaimName. +// - "cloudrun-volume": Cloud Run managed volume (in-memory or NFS-backed). +// Fields: Target, VolumeName, SubPath. +// - "gke-shared-volume": GKE-provided shared volume (e.g. Filestore CSI). +// Fields: Target, VolumeName, SubPath, PVClaimName. +type MountDescriptor struct { + // Type discriminates the mount kind. See type-level doc for valid values. + Type string + + // HostPath is the source for a Docker bind mount (populated for local/nfs). + HostPath string + + // Target is the container-side mount path (e.g. "/workspace"). + Target string + + // NFSServer is the NFS server address (populated for nfs type). + NFSServer string + + // NFSExportPath is the server-side export path (populated for nfs type). + NFSExportPath string + + // SubPath is the sub-path within the volume (K8s PVC subPath, + // Cloud Run volume subPath, or GKE shared volume subPath). + SubPath string + + // PVClaimName is the K8s PVC name (populated for nfs and gke-shared-volume). + PVClaimName string + + // VolumeName is the Cloud Run volume name or GKE volume name. + // For cloudrun-volume: the Cloud Run volume resource name. + // For gke-shared-volume: the volume name referencing the PVC. + VolumeName string +} + +// SelectWorkspaceBackend returns the appropriate WorkspaceBackend based on +// configuration and workspace sharing mode. The selection rules (design §3.1): +// +// - nfsBackend when cfg.Backend == "nfs" AND mode is SharedPlain or WorktreePerAgent. +// - cloudrunVolumeBackend when cfg.Backend == "cloudrun-volume" AND mode is SharedPlain or WorktreePerAgent. +// - gkeSharedVolumeBackend when cfg.Backend == "gke-shared-volume" AND mode is SharedPlain or WorktreePerAgent. +// - localBackend otherwise — including ClonePerAgent even when Backend is a shared type +// (the deliberate node-local escape hatch). +// - Backend empty or "local" always yields localBackend. +func SelectWorkspaceBackend(cfg *config.V1WorkspaceStorageConfig, mode store.WorkspaceSharingMode) WorkspaceBackend { + if cfg != nil { + switch cfg.Backend { + case "nfs": + switch mode { + case store.SharingModeSharedPlain, store.SharingModeWorktreePerAgent: + return NewNFSBackend(cfg.NFS) + } + case "cloudrun-volume": + switch mode { + case store.SharingModeSharedPlain, store.SharingModeWorktreePerAgent: + return NewCloudRunVolumeBackend(cfg.CloudRunVolume) + } + case "gke-shared-volume": + switch mode { + case store.SharingModeSharedPlain, store.SharingModeWorktreePerAgent: + return NewGKESharedVolumeBackend(cfg.GKESharedVolume) + } + } + } + // Backend empty, "local", nil config, or ClonePerAgent → local. + return NewLocalBackend() +} diff --git a/pkg/runtime/workspace_backend_cloudrun_volume.go b/pkg/runtime/workspace_backend_cloudrun_volume.go new file mode 100644 index 000000000..102669559 --- /dev/null +++ b/pkg/runtime/workspace_backend_cloudrun_volume.go @@ -0,0 +1,94 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package runtime + +import ( + "fmt" + "path/filepath" + + "github.com/GoogleCloudPlatform/scion/pkg/config" +) + +// cloudrunVolumeBackend resolves and realizes workspace mounts using a +// Cloud Run managed volume. Cloud Run volumes are declared in the service +// spec and mounted by the platform — no host path or NFS server is needed. +// The backend emits MountDescriptor with Type "cloudrun-volume". +type cloudrunVolumeBackend struct { + cfg *config.V1CloudRunVolumeConfig +} + +// NewCloudRunVolumeBackend returns a WorkspaceBackend for Cloud Run managed volumes. +func NewCloudRunVolumeBackend(cfg *config.V1CloudRunVolumeConfig) WorkspaceBackend { + return &cloudrunVolumeBackend{cfg: cfg} +} + +func (b *cloudrunVolumeBackend) Name() string { return "cloudrun-volume" } + +// Resolve computes workspace paths within the Cloud Run volume. +// There is no host path — the volume is platform-managed. ServerRelativePath +// holds the sub-path within the volume for the project workspace. +func (b *cloudrunVolumeBackend) Resolve(in ResolveInput) (ResolvedWorkspace, error) { + if in.ProjectID == "" { + return ResolvedWorkspace{}, fmt.Errorf("cloudrunVolumeBackend.Resolve: ProjectID is required") + } + if b.cfg == nil { + return ResolvedWorkspace{}, fmt.Errorf("cloudrunVolumeBackend.Resolve: CloudRunVolume config is nil") + } + if b.cfg.VolumeName == "" { + return ResolvedWorkspace{}, fmt.Errorf("cloudrunVolumeBackend.Resolve: volume_name is required") + } + + subPathRoot := b.cfg.SubPathRoot + if subPathRoot == "" { + subPathRoot = "projects" + } + + workspaceRelPath := filepath.Join(subPathRoot, in.ProjectID, "workspace") + + res := ResolvedWorkspace{ + ServerRelativePath: workspaceRelPath, + Backend: "cloudrun-volume", + SharedDirs: make(map[string]ResolvedSharedDir, len(in.SharedDirNames)), + } + + for _, name := range in.SharedDirNames { + sdRelPath := filepath.Join(subPathRoot, in.ProjectID, "shared-dirs", name) + res.SharedDirs[name] = ResolvedSharedDir{ + ServerRelativePath: sdRelPath, + } + } + + return res, nil +} + +// Realize emits a cloudrun-volume MountDescriptor with the volume name and +// sub-path. Cloud Run wires the actual mount — the runtime just declares it. +func (b *cloudrunVolumeBackend) Realize(in RealizeInput) (MountDescriptor, error) { + target := in.ContainerWorkspace + if target == "" { + target = "/workspace" + } + + if b.cfg == nil || b.cfg.VolumeName == "" { + return MountDescriptor{}, fmt.Errorf("cloudrunVolumeBackend.Realize: volume_name is required") + } + + return MountDescriptor{ + Type: "cloudrun-volume", + Target: target, + VolumeName: b.cfg.VolumeName, + SubPath: in.Resolved.ServerRelativePath, + }, nil +} diff --git a/pkg/runtime/workspace_backend_gke_shared_volume.go b/pkg/runtime/workspace_backend_gke_shared_volume.go new file mode 100644 index 000000000..6ee67a371 --- /dev/null +++ b/pkg/runtime/workspace_backend_gke_shared_volume.go @@ -0,0 +1,95 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package runtime + +import ( + "fmt" + "path/filepath" + + "github.com/GoogleCloudPlatform/scion/pkg/config" +) + +// gkeSharedVolumeBackend resolves and realizes workspace mounts using a +// GKE-provided shared volume (e.g. a Filestore CSI-backed PVC). The PVC +// is managed by GKE — the backend emits MountDescriptor with Type +// "gke-shared-volume" containing the volume name, PVC name, and sub-path. +type gkeSharedVolumeBackend struct { + cfg *config.V1GKESharedVolumeConfig +} + +// NewGKESharedVolumeBackend returns a WorkspaceBackend for GKE-managed shared volumes. +func NewGKESharedVolumeBackend(cfg *config.V1GKESharedVolumeConfig) WorkspaceBackend { + return &gkeSharedVolumeBackend{cfg: cfg} +} + +func (b *gkeSharedVolumeBackend) Name() string { return "gke-shared-volume" } + +// Resolve computes workspace paths within the GKE shared volume. +// ServerRelativePath holds the sub-path for the project workspace within +// the PVC. There is no HostPath — the volume is PVC-managed. +func (b *gkeSharedVolumeBackend) Resolve(in ResolveInput) (ResolvedWorkspace, error) { + if in.ProjectID == "" { + return ResolvedWorkspace{}, fmt.Errorf("gkeSharedVolumeBackend.Resolve: ProjectID is required") + } + if b.cfg == nil { + return ResolvedWorkspace{}, fmt.Errorf("gkeSharedVolumeBackend.Resolve: GKESharedVolume config is nil") + } + if b.cfg.VolumeName == "" { + return ResolvedWorkspace{}, fmt.Errorf("gkeSharedVolumeBackend.Resolve: volume_name is required") + } + + subPathRoot := b.cfg.SubPathRoot + if subPathRoot == "" { + subPathRoot = "projects" + } + + workspaceRelPath := filepath.Join(subPathRoot, in.ProjectID, "workspace") + + res := ResolvedWorkspace{ + ServerRelativePath: workspaceRelPath, + Backend: "gke-shared-volume", + SharedDirs: make(map[string]ResolvedSharedDir, len(in.SharedDirNames)), + } + + for _, name := range in.SharedDirNames { + sdRelPath := filepath.Join(subPathRoot, in.ProjectID, "shared-dirs", name) + res.SharedDirs[name] = ResolvedSharedDir{ + ServerRelativePath: sdRelPath, + } + } + + return res, nil +} + +// Realize emits a gke-shared-volume MountDescriptor with the volume name, +// PVC claim name, and sub-path. K8s/GKE wires the actual mount. +func (b *gkeSharedVolumeBackend) Realize(in RealizeInput) (MountDescriptor, error) { + target := in.ContainerWorkspace + if target == "" { + target = "/workspace" + } + + if b.cfg == nil || b.cfg.VolumeName == "" { + return MountDescriptor{}, fmt.Errorf("gkeSharedVolumeBackend.Realize: volume_name is required") + } + + return MountDescriptor{ + Type: "gke-shared-volume", + Target: target, + VolumeName: b.cfg.VolumeName, + PVClaimName: b.cfg.PVClaimName, + SubPath: in.Resolved.ServerRelativePath, + }, nil +} diff --git a/pkg/runtime/workspace_backend_local.go b/pkg/runtime/workspace_backend_local.go new file mode 100644 index 000000000..205172789 --- /dev/null +++ b/pkg/runtime/workspace_backend_local.go @@ -0,0 +1,87 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package runtime + +import ( + "fmt" + "path/filepath" + + "github.com/GoogleCloudPlatform/scion/pkg/config" + "github.com/GoogleCloudPlatform/scion/pkg/store" +) + +// localBackend wraps today's node-local workspace behavior. Resolve delegates +// to existing path helpers; Realize mirrors the current local flow. +// This is the default backend — selecting it produces zero behavior change. +type localBackend struct{} + +// NewLocalBackend returns a WorkspaceBackend backed by node-local storage. +func NewLocalBackend() WorkspaceBackend { + return &localBackend{} +} + +func (b *localBackend) Name() string { return "local" } + +// Resolve computes workspace and shared-dir host paths using the existing +// local path resolution logic. The ProjectDir field on ResolveInput must be +// set (typically from the broker's hub-native project path resolution). +func (b *localBackend) Resolve(in ResolveInput) (ResolvedWorkspace, error) { + if in.ProjectDir == "" { + return ResolvedWorkspace{}, fmt.Errorf("localBackend.Resolve: ProjectDir is required") + } + + hostPath := in.ProjectDir + // For worktree-per-agent mode, the shared base checkout lives under a + // "workspace" subdirectory within the project dir. This gives + // ProvisionShared a per-project sentinel dir (filepath.Dir(HostPath) + // == ProjectDir) so different projects don't collide. + if in.Mode == store.SharingModeWorktreePerAgent { + hostPath = filepath.Join(in.ProjectDir, "workspace") + } + + res := ResolvedWorkspace{ + HostPath: hostPath, + Backend: "local", + SharedDirs: make(map[string]ResolvedSharedDir, len(in.SharedDirNames)), + } + + // Resolve shared dirs using the existing config helpers. + for _, name := range in.SharedDirNames { + hostPath, err := config.GetSharedDirPath(in.ProjectDir, name) + if err != nil { + return ResolvedWorkspace{}, fmt.Errorf("localBackend.Resolve: shared dir %q: %w", name, err) + } + res.SharedDirs[name] = ResolvedSharedDir{ + HostPath: hostPath, + } + } + + return res, nil +} + +// Realize returns a local bind-mount descriptor pointing at the resolved +// host path. This mirrors today's Docker `-v HOST:/workspace` behavior. +func (b *localBackend) Realize(in RealizeInput) (MountDescriptor, error) { + target := in.ContainerWorkspace + if target == "" { + target = "/workspace" + } + + return MountDescriptor{ + Type: "local", + HostPath: in.Resolved.HostPath, + Target: target, + }, nil +} diff --git a/pkg/runtime/workspace_backend_nfs.go b/pkg/runtime/workspace_backend_nfs.go new file mode 100644 index 000000000..4125d8bf4 --- /dev/null +++ b/pkg/runtime/workspace_backend_nfs.go @@ -0,0 +1,133 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package runtime + +import ( + "fmt" + "path/filepath" + + "github.com/GoogleCloudPlatform/scion/pkg/config" +) + +// nfsBackend resolves workspace and shared-dir paths onto an NFS-backed +// filesystem. Resolution is deterministic from project/agent IDs — no DB +// lookup, no I/O — so any replica computes the same path. +// +// Layout under the NFS mount (design §3): +// +// ////workspace +// ////shared-dirs/ +// +// Provision and Realize are stubs in N1-1; full implementations land in +// N1-4 (provisioning) and N1-3 (mount wiring). +type nfsBackend struct { + cfg *config.V1NFSConfig +} + +// NewNFSBackend returns a WorkspaceBackend backed by NFS shared storage. +// The cfg must be non-nil and should have defaults applied (ApplyNFSDefaults). +func NewNFSBackend(cfg *config.V1NFSConfig) WorkspaceBackend { + return &nfsBackend{cfg: cfg} +} + +func (b *nfsBackend) Name() string { return "nfs" } + +// Resolve computes workspace and shared-dir paths on the NFS filesystem. +// The result includes both the server-relative path (for K8s subPath / +// Cloud Run server path) and the full host path (for Docker bind mounts). +// +// The first configured share is used by default. ProjectID is required. +// +// Paths are deterministic: same (ProjectID, ShareID, SubPathRoot) → same path. +// No I/O, no DB lookup. +func (b *nfsBackend) Resolve(in ResolveInput) (ResolvedWorkspace, error) { + if in.ProjectID == "" { + return ResolvedWorkspace{}, fmt.Errorf("nfsBackend.Resolve: ProjectID is required") + } + if b.cfg == nil { + return ResolvedWorkspace{}, fmt.Errorf("nfsBackend.Resolve: NFS config is nil") + } + if len(b.cfg.Shares) == 0 { + return ResolvedWorkspace{}, fmt.Errorf("nfsBackend.Resolve: no NFS shares configured") + } + + share := b.cfg.Shares[0] + subPathRoot := b.cfg.SubPathRoot + if subPathRoot == "" { + subPathRoot = "projects" + } + + // Server-relative workspace path: //workspace + workspaceRelPath := filepath.Join(subPathRoot, in.ProjectID, "workspace") + + // Host base: / + hostBase := filepath.Join(b.cfg.MountRoot, share.ID) + + // Full host path: ////workspace + hostPath := filepath.Join(hostBase, workspaceRelPath) + + res := ResolvedWorkspace{ + HostPath: hostPath, + ServerRelativePath: workspaceRelPath, + HostBase: hostBase, + Backend: "nfs", + SharedDirs: make(map[string]ResolvedSharedDir, len(in.SharedDirNames)), + } + + // Resolve shared dirs on NFS: //shared-dirs/ + for _, name := range in.SharedDirNames { + sdRelPath := filepath.Join(subPathRoot, in.ProjectID, "shared-dirs", name) + res.SharedDirs[name] = ResolvedSharedDir{ + HostPath: filepath.Join(hostBase, sdRelPath), + ServerRelativePath: sdRelPath, + } + } + + return res, nil +} + +// Realize emits a Docker bind-mount descriptor from the NFS host path to the +// container workspace. The host path points at the project subtree under the +// NFS mount (////workspace), NOT +// the export root — this is the critical isolation guarantee (design §9.4). +// +// For K8s the SubPath and PVClaimName fields are populated for PVC+subPath +// wiring; for Docker, HostPath is the bind-mount source. +func (b *nfsBackend) Realize(in RealizeInput) (MountDescriptor, error) { + target := in.ContainerWorkspace + if target == "" { + target = "/workspace" + } + + // Isolation guard: never bind the host base (export root mount) directly. + // The resolved HostPath must be a subdirectory of HostBase, not equal to it. + if err := ValidateNotExportRoot(in.Resolved.HostPath, in.Resolved.HostBase); err != nil { + return MountDescriptor{}, err + } + + desc := MountDescriptor{ + Type: "nfs", + HostPath: in.Resolved.HostPath, + Target: target, + SubPath: in.Resolved.ServerRelativePath, + } + + // Populate K8s PVC info from the first share if available. + if b.cfg != nil && len(b.cfg.Shares) > 0 && b.cfg.Shares[0].PVName != "" { + desc.PVClaimName = b.cfg.Shares[0].PVName + } + + return desc, nil +} diff --git a/pkg/runtime/workspace_backend_test.go b/pkg/runtime/workspace_backend_test.go new file mode 100644 index 000000000..1af2c1991 --- /dev/null +++ b/pkg/runtime/workspace_backend_test.go @@ -0,0 +1,971 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package runtime + +import ( + "path/filepath" + "testing" + + "github.com/GoogleCloudPlatform/scion/pkg/config" + "github.com/GoogleCloudPlatform/scion/pkg/store" +) + +// --- SelectWorkspaceBackend truth table --- + +func TestSelectWorkspaceBackend(t *testing.T) { + nfsCfg := &config.V1WorkspaceStorageConfig{ + Backend: "nfs", + NFS: &config.V1NFSConfig{ + MountRoot: "/mnt/nfs", + SubPathRoot: "projects", + Shares: []config.V1NFSShare{ + {ID: "share1", Server: "10.0.0.2", Export: "/scion-workspaces"}, + }, + }, + } + localCfg := &config.V1WorkspaceStorageConfig{ + Backend: "local", + } + emptyCfg := &config.V1WorkspaceStorageConfig{ + Backend: "", + } + + tests := []struct { + name string + cfg *config.V1WorkspaceStorageConfig + mode store.WorkspaceSharingMode + wantBackend string + }{ + // Backend=local → always local for all modes + { + name: "local+SharedPlain", + cfg: localCfg, + mode: store.SharingModeSharedPlain, + wantBackend: "local", + }, + { + name: "local+WorktreePerAgent", + cfg: localCfg, + mode: store.SharingModeWorktreePerAgent, + wantBackend: "local", + }, + { + name: "local+ClonePerAgent", + cfg: localCfg, + mode: store.SharingModeClonePerAgent, + wantBackend: "local", + }, + + // Backend="" → always local for all modes + { + name: "empty+SharedPlain", + cfg: emptyCfg, + mode: store.SharingModeSharedPlain, + wantBackend: "local", + }, + { + name: "empty+WorktreePerAgent", + cfg: emptyCfg, + mode: store.SharingModeWorktreePerAgent, + wantBackend: "local", + }, + { + name: "empty+ClonePerAgent", + cfg: emptyCfg, + mode: store.SharingModeClonePerAgent, + wantBackend: "local", + }, + + // nil config → always local + { + name: "nil+SharedPlain", + cfg: nil, + mode: store.SharingModeSharedPlain, + wantBackend: "local", + }, + { + name: "nil+ClonePerAgent", + cfg: nil, + mode: store.SharingModeClonePerAgent, + wantBackend: "local", + }, + + // Backend=nfs + SharedPlain → nfs + { + name: "nfs+SharedPlain", + cfg: nfsCfg, + mode: store.SharingModeSharedPlain, + wantBackend: "nfs", + }, + // Backend=nfs + WorktreePerAgent → nfs + { + name: "nfs+WorktreePerAgent", + cfg: nfsCfg, + mode: store.SharingModeWorktreePerAgent, + wantBackend: "nfs", + }, + // Backend=nfs + ClonePerAgent → local (deliberate node-local escape hatch) + { + name: "nfs+ClonePerAgent", + cfg: nfsCfg, + mode: store.SharingModeClonePerAgent, + wantBackend: "local", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + backend := SelectWorkspaceBackend(tt.cfg, tt.mode) + if got := backend.Name(); got != tt.wantBackend { + t.Errorf("SelectWorkspaceBackend(%q, %q) = %q, want %q", + backendStr(tt.cfg), tt.mode, got, tt.wantBackend) + } + }) + } +} + +func backendStr(cfg *config.V1WorkspaceStorageConfig) string { + if cfg == nil { + return "" + } + return cfg.Backend +} + +// --- NFS Resolve tests --- + +func TestNFSBackendResolve(t *testing.T) { + nfsCfg := &config.V1NFSConfig{ + MountRoot: "/mnt/nfs", + SubPathRoot: "projects", + Shares: []config.V1NFSShare{ + {ID: "share1", Server: "10.0.0.2", Export: "/scion-workspaces"}, + }, + } + + tests := []struct { + name string + input ResolveInput + wantHostPath string + wantServerRelPath string + wantHostBase string + wantSharedDirPaths map[string]string // name → hostPath + wantSharedDirRels map[string]string // name → serverRelPath + wantErr bool + }{ + { + name: "basic workspace path", + input: ResolveInput{ + ProjectID: "proj-abc-123", + AgentID: "agent-xyz", + Mode: store.SharingModeSharedPlain, + }, + wantHostPath: filepath.Join("/mnt/nfs", "share1", "projects", "proj-abc-123", "workspace"), + wantServerRelPath: filepath.Join("projects", "proj-abc-123", "workspace"), + wantHostBase: filepath.Join("/mnt/nfs", "share1"), + }, + { + name: "workspace with shared dirs", + input: ResolveInput{ + ProjectID: "proj-abc-123", + AgentID: "agent-xyz", + Mode: store.SharingModeSharedPlain, + SharedDirNames: []string{"data", "cache"}, + }, + wantHostPath: filepath.Join("/mnt/nfs", "share1", "projects", "proj-abc-123", "workspace"), + wantServerRelPath: filepath.Join("projects", "proj-abc-123", "workspace"), + wantHostBase: filepath.Join("/mnt/nfs", "share1"), + wantSharedDirPaths: map[string]string{ + "data": filepath.Join("/mnt/nfs", "share1", "projects", "proj-abc-123", "shared-dirs", "data"), + "cache": filepath.Join("/mnt/nfs", "share1", "projects", "proj-abc-123", "shared-dirs", "cache"), + }, + wantSharedDirRels: map[string]string{ + "data": filepath.Join("projects", "proj-abc-123", "shared-dirs", "data"), + "cache": filepath.Join("projects", "proj-abc-123", "shared-dirs", "cache"), + }, + }, + { + name: "worktree-per-agent mode same workspace path", + input: ResolveInput{ + ProjectID: "proj-abc-123", + AgentID: "agent-xyz", + Mode: store.SharingModeWorktreePerAgent, + }, + wantHostPath: filepath.Join("/mnt/nfs", "share1", "projects", "proj-abc-123", "workspace"), + wantServerRelPath: filepath.Join("projects", "proj-abc-123", "workspace"), + wantHostBase: filepath.Join("/mnt/nfs", "share1"), + }, + { + name: "missing project ID", + input: ResolveInput{ + AgentID: "agent-xyz", + Mode: store.SharingModeSharedPlain, + }, + wantErr: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + b := NewNFSBackend(nfsCfg) + got, err := b.Resolve(tt.input) + + if tt.wantErr { + if err == nil { + t.Fatal("expected error, got nil") + } + return + } + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + + if got.HostPath != tt.wantHostPath { + t.Errorf("HostPath = %q, want %q", got.HostPath, tt.wantHostPath) + } + if got.ServerRelativePath != tt.wantServerRelPath { + t.Errorf("ServerRelativePath = %q, want %q", got.ServerRelativePath, tt.wantServerRelPath) + } + if got.HostBase != tt.wantHostBase { + t.Errorf("HostBase = %q, want %q", got.HostBase, tt.wantHostBase) + } + if got.Backend != "nfs" { + t.Errorf("Backend = %q, want %q", got.Backend, "nfs") + } + + // Verify shared dirs + for name, wantPath := range tt.wantSharedDirPaths { + sd, ok := got.SharedDirs[name] + if !ok { + t.Errorf("shared dir %q not found in result", name) + continue + } + if sd.HostPath != wantPath { + t.Errorf("SharedDirs[%q].HostPath = %q, want %q", name, sd.HostPath, wantPath) + } + } + for name, wantRel := range tt.wantSharedDirRels { + sd, ok := got.SharedDirs[name] + if !ok { + continue // already reported above + } + if sd.ServerRelativePath != wantRel { + t.Errorf("SharedDirs[%q].ServerRelativePath = %q, want %q", name, sd.ServerRelativePath, wantRel) + } + } + }) + } +} + +// TestNFSResolve_Deterministic verifies that calling Resolve twice with the +// same inputs produces identical output — the fundamental contract for +// cross-replica consistency. +func TestNFSResolve_Deterministic(t *testing.T) { + nfsCfg := &config.V1NFSConfig{ + MountRoot: "/mnt/nfs", + SubPathRoot: "projects", + Shares: []config.V1NFSShare{ + {ID: "ws1", Server: "10.0.0.2", Export: "/scion-workspaces"}, + }, + } + + input := ResolveInput{ + ProjectID: "550e8400-e29b-41d4-a716-446655440000", + AgentID: "660e8400-e29b-41d4-a716-446655440001", + Mode: store.SharingModeSharedPlain, + SharedDirNames: []string{"artifacts", "cache"}, + } + + b := NewNFSBackend(nfsCfg) + + r1, err := b.Resolve(input) + if err != nil { + t.Fatalf("first Resolve: %v", err) + } + + r2, err := b.Resolve(input) + if err != nil { + t.Fatalf("second Resolve: %v", err) + } + + if r1.HostPath != r2.HostPath { + t.Errorf("HostPath not deterministic: %q vs %q", r1.HostPath, r2.HostPath) + } + if r1.ServerRelativePath != r2.ServerRelativePath { + t.Errorf("ServerRelativePath not deterministic: %q vs %q", r1.ServerRelativePath, r2.ServerRelativePath) + } + if r1.HostBase != r2.HostBase { + t.Errorf("HostBase not deterministic: %q vs %q", r1.HostBase, r2.HostBase) + } + for name, sd1 := range r1.SharedDirs { + sd2, ok := r2.SharedDirs[name] + if !ok { + t.Errorf("shared dir %q missing from second Resolve", name) + continue + } + if sd1.HostPath != sd2.HostPath { + t.Errorf("SharedDirs[%q].HostPath not deterministic: %q vs %q", name, sd1.HostPath, sd2.HostPath) + } + if sd1.ServerRelativePath != sd2.ServerRelativePath { + t.Errorf("SharedDirs[%q].ServerRelativePath not deterministic: %q vs %q", name, sd1.ServerRelativePath, sd2.ServerRelativePath) + } + } +} + +// TestNFSResolve_PathsAreUnderMountNotExportRoot verifies that resolved +// paths are under /, never under the NFS export root. +func TestNFSResolve_PathsAreUnderMountNotExportRoot(t *testing.T) { + nfsCfg := &config.V1NFSConfig{ + MountRoot: "/mnt/nfs", + SubPathRoot: "projects", + Shares: []config.V1NFSShare{ + {ID: "ws1", Server: "10.0.0.2", Export: "/scion-workspaces"}, + }, + } + + b := NewNFSBackend(nfsCfg) + res, err := b.Resolve(ResolveInput{ + ProjectID: "proj1", + Mode: store.SharingModeSharedPlain, + SharedDirNames: []string{"data"}, + }) + if err != nil { + t.Fatalf("Resolve: %v", err) + } + + exportRoot := "/scion-workspaces" + + // Workspace host path must not start with the export root + if len(res.HostPath) >= len(exportRoot) && res.HostPath[:len(exportRoot)] == exportRoot { + t.Errorf("HostPath %q starts with export root %q — should be under MountRoot/shareID", res.HostPath, exportRoot) + } + + // It must start with the mount root + share ID + wantPrefix := filepath.Join("/mnt/nfs", "ws1") + if len(res.HostPath) < len(wantPrefix) || res.HostPath[:len(wantPrefix)] != wantPrefix { + t.Errorf("HostPath %q does not start with expected prefix %q", res.HostPath, wantPrefix) + } + + // Same for shared dirs + for name, sd := range res.SharedDirs { + if len(sd.HostPath) >= len(exportRoot) && sd.HostPath[:len(exportRoot)] == exportRoot { + t.Errorf("SharedDirs[%q].HostPath %q starts with export root", name, sd.HostPath) + } + if len(sd.HostPath) < len(wantPrefix) || sd.HostPath[:len(wantPrefix)] != wantPrefix { + t.Errorf("SharedDirs[%q].HostPath %q does not start with expected prefix %q", name, sd.HostPath, wantPrefix) + } + } +} + +// TestNFSResolve_ServerRelativePathFormat verifies the exact server-relative +// layout matches the design: projects//workspace and +// projects//shared-dirs/. +func TestNFSResolve_ServerRelativePathFormat(t *testing.T) { + nfsCfg := &config.V1NFSConfig{ + MountRoot: "/mnt/nfs", + SubPathRoot: "projects", + Shares: []config.V1NFSShare{ + {ID: "share1", Server: "10.0.0.2", Export: "/scion-workspaces"}, + }, + } + + b := NewNFSBackend(nfsCfg) + res, err := b.Resolve(ResolveInput{ + ProjectID: "my-project-id", + Mode: store.SharingModeSharedPlain, + SharedDirNames: []string{"logs"}, + }) + if err != nil { + t.Fatalf("Resolve: %v", err) + } + + wantWorkspaceRel := filepath.Join("projects", "my-project-id", "workspace") + if res.ServerRelativePath != wantWorkspaceRel { + t.Errorf("workspace ServerRelativePath = %q, want %q", res.ServerRelativePath, wantWorkspaceRel) + } + + wantSharedDirRel := filepath.Join("projects", "my-project-id", "shared-dirs", "logs") + if sd, ok := res.SharedDirs["logs"]; !ok { + t.Error("shared dir 'logs' not found") + } else if sd.ServerRelativePath != wantSharedDirRel { + t.Errorf("shared dir ServerRelativePath = %q, want %q", sd.ServerRelativePath, wantSharedDirRel) + } +} + +// TestNFSResolve_NoShares verifies that Resolve returns an error when no +// shares are configured. +func TestNFSResolve_NoShares(t *testing.T) { + nfsCfg := &config.V1NFSConfig{ + MountRoot: "/mnt/nfs", + SubPathRoot: "projects", + Shares: nil, + } + b := NewNFSBackend(nfsCfg) + _, err := b.Resolve(ResolveInput{ + ProjectID: "proj1", + Mode: store.SharingModeSharedPlain, + }) + if err == nil { + t.Error("expected error for no shares, got nil") + } +} + +// TestNFSResolve_NilConfig verifies that Resolve returns an error when the +// NFS config is nil. +func TestNFSResolve_NilConfig(t *testing.T) { + b := NewNFSBackend(nil) + _, err := b.Resolve(ResolveInput{ + ProjectID: "proj1", + Mode: store.SharingModeSharedPlain, + }) + if err == nil { + t.Error("expected error for nil config, got nil") + } +} + +// TestNFSResolve_SubPathRootDefault verifies that an empty SubPathRoot +// defaults to "projects". +func TestNFSResolve_SubPathRootDefault(t *testing.T) { + nfsCfg := &config.V1NFSConfig{ + MountRoot: "/mnt/nfs", + SubPathRoot: "", // should default to "projects" + Shares: []config.V1NFSShare{ + {ID: "share1", Server: "10.0.0.2", Export: "/scion-workspaces"}, + }, + } + + b := NewNFSBackend(nfsCfg) + res, err := b.Resolve(ResolveInput{ + ProjectID: "proj1", + Mode: store.SharingModeSharedPlain, + }) + if err != nil { + t.Fatalf("Resolve: %v", err) + } + + wantRel := filepath.Join("projects", "proj1", "workspace") + if res.ServerRelativePath != wantRel { + t.Errorf("ServerRelativePath = %q, want %q (default SubPathRoot)", res.ServerRelativePath, wantRel) + } +} + +// --- Local Backend Resolve tests --- + +func TestLocalBackendResolve(t *testing.T) { + tests := []struct { + name string + input ResolveInput + wantErr bool + wantPath string + wantBackend string + }{ + { + name: "basic local resolve", + input: ResolveInput{ + ProjectDir: "/home/user/.scion.projects/my-project", + ProjectID: "proj1", + Mode: store.SharingModeSharedPlain, + }, + wantPath: "/home/user/.scion.projects/my-project", + wantBackend: "local", + }, + { + name: "local resolve clone-per-agent", + input: ResolveInput{ + ProjectDir: "/home/user/.scion.projects/my-project", + ProjectID: "proj1", + Mode: store.SharingModeClonePerAgent, + }, + wantPath: "/home/user/.scion.projects/my-project", + wantBackend: "local", + }, + { + name: "worktree-per-agent uses workspace subdir", + input: ResolveInput{ + ProjectDir: "/home/user/.scion.projects/my-project", + ProjectID: "proj1", + Mode: store.SharingModeWorktreePerAgent, + }, + wantPath: "/home/user/.scion.projects/my-project/workspace", + wantBackend: "local", + }, + { + name: "missing ProjectDir", + input: ResolveInput{ + ProjectID: "proj1", + Mode: store.SharingModeSharedPlain, + }, + wantErr: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + b := NewLocalBackend() + got, err := b.Resolve(tt.input) + + if tt.wantErr { + if err == nil { + t.Fatal("expected error, got nil") + } + return + } + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + + if got.HostPath != tt.wantPath { + t.Errorf("HostPath = %q, want %q", got.HostPath, tt.wantPath) + } + if got.Backend != tt.wantBackend { + t.Errorf("Backend = %q, want %q", got.Backend, tt.wantBackend) + } + if got.ServerRelativePath != "" { + t.Errorf("ServerRelativePath = %q, want empty for local", got.ServerRelativePath) + } + if got.HostBase != "" { + t.Errorf("HostBase = %q, want empty for local", got.HostBase) + } + }) + } +} + +// TestLocalBackendResolve_MatchesPreExisting asserts that localBackend.Resolve +// produces the same host path that the existing broker path resolution would +// produce for a hub-native project. This is the "zero behavior change" guard. +func TestLocalBackendResolve_MatchesPreExisting(t *testing.T) { + // The existing broker resolution for a hub-native project sets + // ProjectPath = ~/.scion.projects/. localBackend.Resolve must + // return exactly that path as HostPath. + projectPath := "/home/scion/.scion.projects/my-project-slug" + + b := NewLocalBackend() + res, err := b.Resolve(ResolveInput{ + ProjectDir: projectPath, + ProjectID: "proj-uuid", + Mode: store.SharingModeSharedPlain, + }) + if err != nil { + t.Fatalf("Resolve: %v", err) + } + + if res.HostPath != projectPath { + t.Errorf("localBackend.Resolve HostPath = %q, want %q (pre-existing path)", res.HostPath, projectPath) + } +} + +// --- Realize tests --- + +func TestLocalBackendRealize(t *testing.T) { + b := NewLocalBackend() + desc, err := b.Realize(RealizeInput{ + Resolved: ResolvedWorkspace{ + HostPath: "/home/scion/.scion.projects/my-project", + Backend: "local", + }, + ContainerWorkspace: "/workspace", + }) + if err != nil { + t.Fatalf("Realize: %v", err) + } + + if desc.Type != "local" { + t.Errorf("Type = %q, want %q", desc.Type, "local") + } + if desc.HostPath != "/home/scion/.scion.projects/my-project" { + t.Errorf("HostPath = %q, want the resolved path", desc.HostPath) + } + if desc.Target != "/workspace" { + t.Errorf("Target = %q, want %q", desc.Target, "/workspace") + } +} + +func TestLocalBackendRealize_DefaultTarget(t *testing.T) { + b := NewLocalBackend() + desc, err := b.Realize(RealizeInput{ + Resolved: ResolvedWorkspace{ + HostPath: "/some/path", + Backend: "local", + }, + ContainerWorkspace: "", // should default to /workspace + }) + if err != nil { + t.Fatalf("Realize: %v", err) + } + if desc.Target != "/workspace" { + t.Errorf("Target = %q, want %q (default)", desc.Target, "/workspace") + } +} + +func TestNFSBackendRealize(t *testing.T) { + nfsCfg := &config.V1NFSConfig{ + MountRoot: "/mnt/nfs", + SubPathRoot: "projects", + Shares: []config.V1NFSShare{ + {ID: "share1", Server: "10.0.0.2", Export: "/scion-workspaces"}, + }, + } + b := NewNFSBackend(nfsCfg) + + desc, err := b.Realize(RealizeInput{ + Resolved: ResolvedWorkspace{ + HostPath: "/mnt/nfs/share1/projects/proj1/workspace", + ServerRelativePath: "projects/proj1/workspace", + HostBase: "/mnt/nfs/share1", + Backend: "nfs", + }, + ContainerWorkspace: "/workspace", + }) + if err != nil { + t.Fatalf("Realize: %v", err) + } + + if desc.Type != "nfs" { + t.Errorf("Type = %q, want %q", desc.Type, "nfs") + } + if desc.HostPath != "/mnt/nfs/share1/projects/proj1/workspace" { + t.Errorf("HostPath = %q, want the NFS host path", desc.HostPath) + } + if desc.Target != "/workspace" { + t.Errorf("Target = %q, want %q", desc.Target, "/workspace") + } + if desc.SubPath != "projects/proj1/workspace" { + t.Errorf("SubPath = %q, want the server-relative path", desc.SubPath) + } +} + +// --- Backend Name tests --- + +func TestBackendNames(t *testing.T) { + local := NewLocalBackend() + if local.Name() != "local" { + t.Errorf("local backend Name() = %q, want %q", local.Name(), "local") + } + + nfsCfg := &config.V1NFSConfig{ + MountRoot: "/mnt/nfs", + Shares: []config.V1NFSShare{ + {ID: "s1", Server: "10.0.0.2", Export: "/ws"}, + }, + } + nfs := NewNFSBackend(nfsCfg) + if nfs.Name() != "nfs" { + t.Errorf("nfs backend Name() = %q, want %q", nfs.Name(), "nfs") + } +} + +// --- SelectWorkspaceBackend tests for new backends --- + +func TestSelectWorkspaceBackend_CloudRunVolume(t *testing.T) { + cfg := &config.V1WorkspaceStorageConfig{ + Backend: "cloudrun-volume", + CloudRunVolume: &config.V1CloudRunVolumeConfig{ + VolumeName: "workspace-vol", + SubPathRoot: "projects", + }, + } + + tests := []struct { + name string + mode store.WorkspaceSharingMode + wantBackend string + }{ + {"SharedPlain", store.SharingModeSharedPlain, "cloudrun-volume"}, + {"WorktreePerAgent", store.SharingModeWorktreePerAgent, "cloudrun-volume"}, + {"ClonePerAgent", store.SharingModeClonePerAgent, "local"}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + b := SelectWorkspaceBackend(cfg, tt.mode) + if got := b.Name(); got != tt.wantBackend { + t.Errorf("SelectWorkspaceBackend(cloudrun-volume, %s) = %q, want %q", tt.mode, got, tt.wantBackend) + } + }) + } +} + +func TestSelectWorkspaceBackend_GKESharedVolume(t *testing.T) { + cfg := &config.V1WorkspaceStorageConfig{ + Backend: "gke-shared-volume", + GKESharedVolume: &config.V1GKESharedVolumeConfig{ + VolumeName: "shared-ws", + PVClaimName: "shared-pvc", + SubPathRoot: "projects", + }, + } + + tests := []struct { + name string + mode store.WorkspaceSharingMode + wantBackend string + }{ + {"SharedPlain", store.SharingModeSharedPlain, "gke-shared-volume"}, + {"WorktreePerAgent", store.SharingModeWorktreePerAgent, "gke-shared-volume"}, + {"ClonePerAgent", store.SharingModeClonePerAgent, "local"}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + b := SelectWorkspaceBackend(cfg, tt.mode) + if got := b.Name(); got != tt.wantBackend { + t.Errorf("SelectWorkspaceBackend(gke-shared-volume, %s) = %q, want %q", tt.mode, got, tt.wantBackend) + } + }) + } +} + +// --- CloudRunVolume Backend tests --- + +func TestCloudRunVolumeBackendResolve(t *testing.T) { + cfg := &config.V1CloudRunVolumeConfig{ + VolumeName: "workspace-vol", + SubPathRoot: "projects", + } + + b := NewCloudRunVolumeBackend(cfg) + res, err := b.Resolve(ResolveInput{ + ProjectID: "proj-abc-123", + Mode: store.SharingModeSharedPlain, + SharedDirNames: []string{"data"}, + }) + if err != nil { + t.Fatalf("Resolve: %v", err) + } + + if res.Backend != "cloudrun-volume" { + t.Errorf("Backend = %q, want %q", res.Backend, "cloudrun-volume") + } + wantRelPath := filepath.Join("projects", "proj-abc-123", "workspace") + if res.ServerRelativePath != wantRelPath { + t.Errorf("ServerRelativePath = %q, want %q", res.ServerRelativePath, wantRelPath) + } + if res.HostPath != "" { + t.Errorf("HostPath = %q, want empty for cloudrun-volume", res.HostPath) + } + + sd, ok := res.SharedDirs["data"] + if !ok { + t.Fatal("shared dir 'data' not found") + } + wantSDRel := filepath.Join("projects", "proj-abc-123", "shared-dirs", "data") + if sd.ServerRelativePath != wantSDRel { + t.Errorf("SharedDirs[data].ServerRelativePath = %q, want %q", sd.ServerRelativePath, wantSDRel) + } +} + +func TestCloudRunVolumeBackendResolve_Errors(t *testing.T) { + t.Run("missing ProjectID", func(t *testing.T) { + b := NewCloudRunVolumeBackend(&config.V1CloudRunVolumeConfig{VolumeName: "v"}) + _, err := b.Resolve(ResolveInput{Mode: store.SharingModeSharedPlain}) + if err == nil { + t.Error("expected error for missing ProjectID") + } + }) + t.Run("nil config", func(t *testing.T) { + b := NewCloudRunVolumeBackend(nil) + _, err := b.Resolve(ResolveInput{ProjectID: "p", Mode: store.SharingModeSharedPlain}) + if err == nil { + t.Error("expected error for nil config") + } + }) + t.Run("missing volume_name", func(t *testing.T) { + b := NewCloudRunVolumeBackend(&config.V1CloudRunVolumeConfig{}) + _, err := b.Resolve(ResolveInput{ProjectID: "p", Mode: store.SharingModeSharedPlain}) + if err == nil { + t.Error("expected error for missing volume_name") + } + }) +} + +func TestCloudRunVolumeBackendRealize(t *testing.T) { + cfg := &config.V1CloudRunVolumeConfig{ + VolumeName: "workspace-vol", + SubPathRoot: "projects", + } + + b := NewCloudRunVolumeBackend(cfg) + desc, err := b.Realize(RealizeInput{ + Resolved: ResolvedWorkspace{ + ServerRelativePath: "projects/proj1/workspace", + Backend: "cloudrun-volume", + }, + ContainerWorkspace: "/workspace", + }) + if err != nil { + t.Fatalf("Realize: %v", err) + } + + if desc.Type != "cloudrun-volume" { + t.Errorf("Type = %q, want %q", desc.Type, "cloudrun-volume") + } + if desc.VolumeName != "workspace-vol" { + t.Errorf("VolumeName = %q, want %q", desc.VolumeName, "workspace-vol") + } + if desc.SubPath != "projects/proj1/workspace" { + t.Errorf("SubPath = %q, want %q", desc.SubPath, "projects/proj1/workspace") + } + if desc.Target != "/workspace" { + t.Errorf("Target = %q, want %q", desc.Target, "/workspace") + } + if desc.HostPath != "" { + t.Errorf("HostPath = %q, want empty for cloudrun-volume", desc.HostPath) + } +} + +// --- GKESharedVolume Backend tests --- + +func TestGKESharedVolumeBackendResolve(t *testing.T) { + cfg := &config.V1GKESharedVolumeConfig{ + VolumeName: "shared-ws", + PVClaimName: "shared-pvc", + SubPathRoot: "projects", + } + + b := NewGKESharedVolumeBackend(cfg) + res, err := b.Resolve(ResolveInput{ + ProjectID: "proj-abc-123", + Mode: store.SharingModeSharedPlain, + SharedDirNames: []string{"cache"}, + }) + if err != nil { + t.Fatalf("Resolve: %v", err) + } + + if res.Backend != "gke-shared-volume" { + t.Errorf("Backend = %q, want %q", res.Backend, "gke-shared-volume") + } + wantRelPath := filepath.Join("projects", "proj-abc-123", "workspace") + if res.ServerRelativePath != wantRelPath { + t.Errorf("ServerRelativePath = %q, want %q", res.ServerRelativePath, wantRelPath) + } + if res.HostPath != "" { + t.Errorf("HostPath = %q, want empty for gke-shared-volume", res.HostPath) + } + + sd, ok := res.SharedDirs["cache"] + if !ok { + t.Fatal("shared dir 'cache' not found") + } + wantSDRel := filepath.Join("projects", "proj-abc-123", "shared-dirs", "cache") + if sd.ServerRelativePath != wantSDRel { + t.Errorf("SharedDirs[cache].ServerRelativePath = %q, want %q", sd.ServerRelativePath, wantSDRel) + } +} + +func TestGKESharedVolumeBackendResolve_Errors(t *testing.T) { + t.Run("missing ProjectID", func(t *testing.T) { + b := NewGKESharedVolumeBackend(&config.V1GKESharedVolumeConfig{VolumeName: "v"}) + _, err := b.Resolve(ResolveInput{Mode: store.SharingModeSharedPlain}) + if err == nil { + t.Error("expected error for missing ProjectID") + } + }) + t.Run("nil config", func(t *testing.T) { + b := NewGKESharedVolumeBackend(nil) + _, err := b.Resolve(ResolveInput{ProjectID: "p", Mode: store.SharingModeSharedPlain}) + if err == nil { + t.Error("expected error for nil config") + } + }) + t.Run("missing volume_name", func(t *testing.T) { + b := NewGKESharedVolumeBackend(&config.V1GKESharedVolumeConfig{}) + _, err := b.Resolve(ResolveInput{ProjectID: "p", Mode: store.SharingModeSharedPlain}) + if err == nil { + t.Error("expected error for missing volume_name") + } + }) +} + +func TestGKESharedVolumeBackendRealize(t *testing.T) { + cfg := &config.V1GKESharedVolumeConfig{ + VolumeName: "shared-ws", + PVClaimName: "shared-pvc", + SubPathRoot: "projects", + } + + b := NewGKESharedVolumeBackend(cfg) + desc, err := b.Realize(RealizeInput{ + Resolved: ResolvedWorkspace{ + ServerRelativePath: "projects/proj1/workspace", + Backend: "gke-shared-volume", + }, + ContainerWorkspace: "/workspace", + }) + if err != nil { + t.Fatalf("Realize: %v", err) + } + + if desc.Type != "gke-shared-volume" { + t.Errorf("Type = %q, want %q", desc.Type, "gke-shared-volume") + } + if desc.VolumeName != "shared-ws" { + t.Errorf("VolumeName = %q, want %q", desc.VolumeName, "shared-ws") + } + if desc.PVClaimName != "shared-pvc" { + t.Errorf("PVClaimName = %q, want %q", desc.PVClaimName, "shared-pvc") + } + if desc.SubPath != "projects/proj1/workspace" { + t.Errorf("SubPath = %q, want %q", desc.SubPath, "projects/proj1/workspace") + } + if desc.Target != "/workspace" { + t.Errorf("Target = %q, want %q", desc.Target, "/workspace") + } + if desc.HostPath != "" { + t.Errorf("HostPath = %q, want empty for gke-shared-volume", desc.HostPath) + } +} + +func TestGKESharedVolumeBackendRealize_DefaultTarget(t *testing.T) { + cfg := &config.V1GKESharedVolumeConfig{VolumeName: "v"} + b := NewGKESharedVolumeBackend(cfg) + desc, err := b.Realize(RealizeInput{ + Resolved: ResolvedWorkspace{Backend: "gke-shared-volume"}, + ContainerWorkspace: "", + }) + if err != nil { + t.Fatalf("Realize: %v", err) + } + if desc.Target != "/workspace" { + t.Errorf("Target = %q, want /workspace (default)", desc.Target) + } +} + +// --- ProvisionShared tests (from workspace_backend_test.go) --- + +func TestProvisionShared_NonGit(t *testing.T) { + mountRoot := t.TempDir() + nfsCfg := &config.V1NFSConfig{ + MountRoot: mountRoot, + Shares: []config.V1NFSShare{ + {ID: "s1", Server: "10.0.0.2", Export: "/ws"}, + }, + } + b := NewNFSBackend(nfsCfg) + res, err := b.Resolve(ResolveInput{ + ProjectID: "proj1", + Mode: store.SharingModeSharedPlain, + }) + if err != nil { + t.Fatalf("Resolve: %v", err) + } + err = ProvisionShared(ProvisionInput{ + Resolved: res, + ProjectID: "proj1", + Mode: store.SharingModeSharedPlain, + }) + if err != nil { + t.Errorf("ProvisionShared for non-git project should succeed, got error: %v", err) + } +} diff --git a/pkg/runtime/workspace_cleanup.go b/pkg/runtime/workspace_cleanup.go new file mode 100644 index 000000000..33ccf8240 --- /dev/null +++ b/pkg/runtime/workspace_cleanup.go @@ -0,0 +1,93 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package runtime + +import ( + "fmt" + "log/slog" + "os" + "path/filepath" + + "github.com/GoogleCloudPlatform/scion/pkg/config" +) + +// CleanupNFSProject removes the NFS project subtree for a given project ID. +// This mirrors the K8s-side cleanupSharedDirPVCs (k8s_runtime.go:753-770) +// for the Docker/VM NFS model. +// +// The target path is //projects//, which +// contains the workspace and shared-dirs subdirectories. +// +// Isolation guard: refuses to delete the share root or any path that is not +// a proper subdirectory of the share mount. This uses the same +// ValidateNotExportRoot guard as Realize (design §9.4). +// +// Idempotent: returns nil if the directory does not exist. +func CleanupNFSProject(cfg *config.V1NFSConfig, projectID string) error { + if cfg == nil { + return fmt.Errorf("CleanupNFSProject: NFS config is nil") + } + if projectID == "" { + return fmt.Errorf("CleanupNFSProject: projectID is required") + } + if len(cfg.Shares) == 0 { + return fmt.Errorf("CleanupNFSProject: no NFS shares configured") + } + + subPathRoot := cfg.SubPathRoot + if subPathRoot == "" { + subPathRoot = "projects" + } + + share := cfg.Shares[0] + hostBase := filepath.Join(cfg.MountRoot, share.ID) + + // Compute the project subtree path: //// + projectPath := filepath.Join(hostBase, subPathRoot, projectID) + + // Isolation guard: the target must be a proper subdirectory of the host + // base (share mount), NEVER the share root itself. This prevents + // accidental deletion of the entire NFS share. + if err := ValidateNotExportRoot(projectPath, hostBase); err != nil { + return fmt.Errorf("CleanupNFSProject: %w", err) + } + + // Additional safety: the resolved path must be under the projects subtree. + // This catches path traversal attempts (e.g. projectID = "../../something"). + cleanPath := filepath.Clean(projectPath) + cleanBase := filepath.Clean(filepath.Join(hostBase, subPathRoot)) + if len(cleanPath) <= len(cleanBase) || cleanPath[:len(cleanBase)] != cleanBase { + return fmt.Errorf("CleanupNFSProject: path traversal detected: %q is not under %q", + projectPath, filepath.Join(hostBase, subPathRoot)) + } + + // Idempotent: if the directory doesn't exist, nothing to do. + if _, err := os.Stat(projectPath); os.IsNotExist(err) { + slog.Debug("CleanupNFSProject: project subtree does not exist (already cleaned)", + "project_id", projectID, "path", projectPath) + return nil + } + + slog.Info("CleanupNFSProject: removing project subtree", + "project_id", projectID, "path", projectPath) + + if err := os.RemoveAll(projectPath); err != nil { + return fmt.Errorf("CleanupNFSProject: rm -rf %s: %w", projectPath, err) + } + + slog.Info("CleanupNFSProject: project subtree removed", + "project_id", projectID, "path", projectPath) + return nil +} diff --git a/pkg/runtime/workspace_cleanup_test.go b/pkg/runtime/workspace_cleanup_test.go new file mode 100644 index 000000000..af0a6a93e --- /dev/null +++ b/pkg/runtime/workspace_cleanup_test.go @@ -0,0 +1,220 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package runtime + +import ( + "os" + "path/filepath" + "testing" + + "github.com/GoogleCloudPlatform/scion/pkg/config" +) + +func testNFSCleanupConfig(t *testing.T) (*config.V1NFSConfig, string) { + t.Helper() + mountRoot := t.TempDir() + cfg := &config.V1NFSConfig{ + MountRoot: mountRoot, + SubPathRoot: "projects", + Shares: []config.V1NFSShare{ + {ID: "share1", Server: "10.0.0.2", Export: "/scion-workspaces"}, + }, + } + return cfg, mountRoot +} + +// createProjectSubtree creates a project subtree structure for testing. +func createProjectSubtree(t *testing.T, mountRoot, shareID, projectID string) string { + t.Helper() + projectPath := filepath.Join(mountRoot, shareID, "projects", projectID) + wsPath := filepath.Join(projectPath, "workspace") + sdPath := filepath.Join(projectPath, "shared-dirs", "data") + + if err := os.MkdirAll(wsPath, 0770); err != nil { + t.Fatal(err) + } + if err := os.MkdirAll(sdPath, 0770); err != nil { + t.Fatal(err) + } + + // Write some files to verify deletion. + if err := os.WriteFile(filepath.Join(wsPath, "test.txt"), []byte("hello"), 0644); err != nil { + t.Fatal(err) + } + if err := os.WriteFile(filepath.Join(projectPath, ".scion-provisioned"), []byte("test"), 0644); err != nil { + t.Fatal(err) + } + + return projectPath +} + +// --- Basic cleanup --- + +func TestCleanupNFSProject_RemovesSubtree(t *testing.T) { + cfg, mountRoot := testNFSCleanupConfig(t) + projectPath := createProjectSubtree(t, mountRoot, "share1", "proj-1") + + // Verify structure exists. + if _, err := os.Stat(projectPath); err != nil { + t.Fatalf("project subtree should exist: %v", err) + } + + err := CleanupNFSProject(cfg, "proj-1") + if err != nil { + t.Fatalf("CleanupNFSProject: %v", err) + } + + // Verify project subtree is gone. + if _, err := os.Stat(projectPath); !os.IsNotExist(err) { + t.Errorf("project subtree should be deleted, but still exists") + } + + // Verify the share root still exists. + shareRoot := filepath.Join(mountRoot, "share1") + if _, err := os.Stat(shareRoot); err != nil { + t.Errorf("share root should still exist: %v", err) + } +} + +// --- Idempotent: non-existent project is a no-op --- + +func TestCleanupNFSProject_Idempotent(t *testing.T) { + cfg, _ := testNFSCleanupConfig(t) + + // No subtree exists — should succeed silently. + err := CleanupNFSProject(cfg, "nonexistent-project") + if err != nil { + t.Fatalf("cleanup of nonexistent project should be idempotent: %v", err) + } +} + +// --- Double cleanup --- + +func TestCleanupNFSProject_DoubleCleanup(t *testing.T) { + cfg, mountRoot := testNFSCleanupConfig(t) + createProjectSubtree(t, mountRoot, "share1", "proj-double") + + if err := CleanupNFSProject(cfg, "proj-double"); err != nil { + t.Fatalf("first cleanup: %v", err) + } + if err := CleanupNFSProject(cfg, "proj-double"); err != nil { + t.Fatalf("second cleanup should be idempotent: %v", err) + } +} + +// --- Isolation: refuses share root --- + +func TestCleanupNFSProject_RefusesShareRoot(t *testing.T) { + cfg := &config.V1NFSConfig{ + MountRoot: "/mnt/nfs", + SubPathRoot: "", + Shares: []config.V1NFSShare{ + {ID: "", Server: "10.0.0.2", Export: "/ws"}, + }, + } + + // An empty projectID would compute a path that equals the base. + err := CleanupNFSProject(cfg, "") + if err == nil { + t.Fatal("should refuse empty project ID") + } +} + +// --- Isolation: refuses path traversal --- + +func TestCleanupNFSProject_RefusesPathTraversal(t *testing.T) { + cfg, _ := testNFSCleanupConfig(t) + + err := CleanupNFSProject(cfg, "../../etc") + if err == nil { + t.Fatal("should refuse path traversal") + } +} + +// --- Does not affect other projects --- + +func TestCleanupNFSProject_IsolatesProjects(t *testing.T) { + cfg, mountRoot := testNFSCleanupConfig(t) + + projAPath := createProjectSubtree(t, mountRoot, "share1", "proj-A") + projBPath := createProjectSubtree(t, mountRoot, "share1", "proj-B") + + // Delete project A. + if err := CleanupNFSProject(cfg, "proj-A"); err != nil { + t.Fatalf("cleanup proj-A: %v", err) + } + + // Project A is gone. + if _, err := os.Stat(projAPath); !os.IsNotExist(err) { + t.Error("proj-A should be deleted") + } + + // Project B is untouched. + if _, err := os.Stat(projBPath); err != nil { + t.Errorf("proj-B should still exist: %v", err) + } + + // Files in B are intact. + bFile := filepath.Join(projBPath, "workspace", "test.txt") + if _, err := os.Stat(bFile); err != nil { + t.Errorf("proj-B workspace file should be intact: %v", err) + } +} + +// --- Nil config --- + +func TestCleanupNFSProject_NilConfig(t *testing.T) { + err := CleanupNFSProject(nil, "proj-1") + if err == nil { + t.Fatal("should error on nil config") + } +} + +// --- No shares --- + +func TestCleanupNFSProject_NoShares(t *testing.T) { + cfg := &config.V1NFSConfig{ + MountRoot: "/mnt/nfs", + Shares: nil, + } + err := CleanupNFSProject(cfg, "proj-1") + if err == nil { + t.Fatal("should error on no shares") + } +} + +// --- SubPathRoot defaults --- + +func TestCleanupNFSProject_SubPathRootDefault(t *testing.T) { + mountRoot := t.TempDir() + cfg := &config.V1NFSConfig{ + MountRoot: mountRoot, + SubPathRoot: "", // should default to "projects" + Shares: []config.V1NFSShare{ + {ID: "share1", Server: "10.0.0.2", Export: "/ws"}, + }, + } + + // Create the subtree at the default path. + projectPath := createProjectSubtree(t, mountRoot, "share1", "proj-default") + + if err := CleanupNFSProject(cfg, "proj-default"); err != nil { + t.Fatalf("cleanup with default SubPathRoot: %v", err) + } + + if _, err := os.Stat(projectPath); !os.IsNotExist(err) { + t.Error("project subtree should be deleted") + } +} diff --git a/pkg/runtime/workspace_provision.go b/pkg/runtime/workspace_provision.go new file mode 100644 index 000000000..e6eea53f4 --- /dev/null +++ b/pkg/runtime/workspace_provision.go @@ -0,0 +1,32 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package runtime + +import ( + "github.com/GoogleCloudPlatform/scion/pkg/provision" +) + +// Backward-compatible re-exports from pkg/provision. +// The provisioning logic was extracted to the config-free pkg/provision leaf +// package so that lean binaries (e.g. sciontool) can invoke provisioning +// without pulling in pkg/config. + +// ProvisionSentinelFile is re-exported from pkg/provision. +const ProvisionSentinelFile = provision.ProvisionSentinelFile + +// ProvisionShared delegates to provision.ProvisionShared. +func ProvisionShared(in ProvisionInput) error { + return provision.ProvisionShared(in) +} diff --git a/pkg/runtime/workspace_provision_test.go b/pkg/runtime/workspace_provision_test.go new file mode 100644 index 000000000..a6b6228ec --- /dev/null +++ b/pkg/runtime/workspace_provision_test.go @@ -0,0 +1,672 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package runtime + +import ( + "context" + "os" + "os/exec" + "path/filepath" + "sync" + "sync/atomic" + "testing" + + "github.com/GoogleCloudPlatform/scion/pkg/api" + "github.com/GoogleCloudPlatform/scion/pkg/config" + "github.com/GoogleCloudPlatform/scion/pkg/store" +) + +// testLocker is a mock AdvisoryLocker for testing. +type testLocker struct { + mu sync.Mutex + held map[lockKey]bool + acquires int64 +} + +type lockKey struct { + classID int64 + objID int32 + single bool // true for single-int form +} + +func newTestLocker() *testLocker { + return &testLocker{held: make(map[lockKey]bool)} +} + +func (l *testLocker) TryAdvisoryLock(ctx context.Context, key store.AdvisoryLockKey) (bool, func() error, error) { + k := lockKey{classID: int64(key), single: true} + l.mu.Lock() + defer l.mu.Unlock() + if l.held[k] { + return false, func() error { return nil }, nil + } + l.held[k] = true + atomic.AddInt64(&l.acquires, 1) + return true, func() error { + l.mu.Lock() + defer l.mu.Unlock() + delete(l.held, k) + return nil + }, nil +} + +func (l *testLocker) TryAdvisoryLockObject(ctx context.Context, classID store.AdvisoryLockKey, objID int32) (bool, func() error, error) { + k := lockKey{classID: int64(classID), objID: objID} + l.mu.Lock() + defer l.mu.Unlock() + if l.held[k] { + return false, func() error { return nil }, nil + } + l.held[k] = true + atomic.AddInt64(&l.acquires, 1) + return true, func() error { + l.mu.Lock() + defer l.mu.Unlock() + delete(l.held, k) + return nil + }, nil +} + +// nfsTestBackend creates an nfsBackend with a temp directory as the mount root +// and returns the backend, config, and project paths. +func nfsTestBackend(t *testing.T) (*nfsBackend, *config.V1NFSConfig, string) { + t.Helper() + mountRoot := t.TempDir() + cfg := &config.V1NFSConfig{ + MountRoot: mountRoot, + SubPathRoot: "projects", + Shares: []config.V1NFSShare{ + {ID: "share1", Server: "10.0.0.2", Export: "/scion-workspaces"}, + }, + } + b := &nfsBackend{cfg: cfg} + return b, cfg, mountRoot +} + +// resolveForTest resolves workspace paths for a test project. +func resolveForTest(t *testing.T, b WorkspaceBackend, projectID string) ResolvedWorkspace { + t.Helper() + res, err := b.Resolve(ResolveInput{ + ProjectID: projectID, + Mode: store.SharingModeSharedPlain, + }) + if err != nil { + t.Fatalf("Resolve: %v", err) + } + return res +} + +// initBareGitRepo creates a bare git repo at the given path for cloning from. +func initBareGitRepo(t *testing.T) string { + t.Helper() + dir := t.TempDir() + bareDir := filepath.Join(dir, "bare.git") + run(t, "git", "init", "--bare", "--initial-branch=main", bareDir) + + // Create a working clone to make an initial commit. + workDir := filepath.Join(dir, "work") + run(t, "git", "clone", bareDir, workDir) + + // Create an initial commit so the repo has a HEAD. + f := filepath.Join(workDir, "README.md") + if err := os.WriteFile(f, []byte("# Test\n"), 0644); err != nil { + t.Fatal(err) + } + runIn(t, workDir, "git", "add", "README.md") + runIn(t, workDir, "git", "-c", "user.name=test", "-c", "user.email=test@test.com", + "commit", "-m", "initial") + runIn(t, workDir, "git", "push", "origin", "main") + + return bareDir +} + +func run(t *testing.T, name string, args ...string) { + t.Helper() + cmd := exec.Command(name, args...) + cmd.Env = append(os.Environ(), "GIT_TERMINAL_PROMPT=0") + if output, err := cmd.CombinedOutput(); err != nil { + t.Fatalf("%s %v: %s\n%s", name, args, err, output) + } +} + +func runIn(t *testing.T, dir, name string, args ...string) { + t.Helper() + cmd := exec.Command(name, args...) + cmd.Dir = dir + cmd.Env = append(os.Environ(), "GIT_TERMINAL_PROMPT=0") + if output, err := cmd.CombinedOutput(); err != nil { + t.Fatalf("%s %v (in %s): %s\n%s", name, args, dir, err, output) + } +} + +// --- SharedPlain provisioning without git --- + +func TestNFSProvision_SharedPlain_NonGit(t *testing.T) { + b, _, mountRoot := nfsTestBackend(t) + locker := newTestLocker() + + projectID := "proj-nonGit-1" + res, err := b.Resolve(ResolveInput{ + ProjectID: projectID, + Mode: store.SharingModeSharedPlain, + }) + if err != nil { + t.Fatalf("Resolve: %v", err) + } + + err = ProvisionShared(ProvisionInput{ + Resolved: res, + ProjectID: projectID, + Mode: store.SharingModeSharedPlain, + Locker: locker, + }) + if err != nil { + t.Fatalf("Provision: %v", err) + } + + // Verify workspace directory was created. + if _, err := os.Stat(res.HostPath); err != nil { + t.Errorf("workspace dir not created: %v", err) + } + + // Verify sentinel was written. + sentinelPath := filepath.Join(filepath.Dir(res.HostPath), ProvisionSentinelFile) + if _, err := os.Stat(sentinelPath); err != nil { + t.Errorf("sentinel not written: %v", err) + } + + // Verify lock was acquired. + if atomic.LoadInt64(&locker.acquires) != 1 { + t.Errorf("expected 1 lock acquire, got %d", atomic.LoadInt64(&locker.acquires)) + } + + _ = mountRoot +} + +// --- SharedPlain provisioning with git clone --- + +func TestNFSProvision_SharedPlain_GitClone(t *testing.T) { + b, _, _ := nfsTestBackend(t) + locker := newTestLocker() + + bareRepo := initBareGitRepo(t) + projectID := "proj-git-1" + res, err := b.Resolve(ResolveInput{ + ProjectID: projectID, + Mode: store.SharingModeSharedPlain, + }) + if err != nil { + t.Fatalf("Resolve: %v", err) + } + + err = ProvisionShared(ProvisionInput{ + Resolved: res, + ProjectID: projectID, + Mode: store.SharingModeSharedPlain, + Locker: locker, + GitClone: &api.GitCloneConfig{ + URL: bareRepo, + Branch: "main", + Depth: 1, + }, + }) + if err != nil { + t.Fatalf("Provision: %v", err) + } + + // Verify .git directory exists (git clone succeeded). + if _, err := os.Stat(filepath.Join(res.HostPath, ".git")); err != nil { + t.Errorf("git clone did not create .git: %v", err) + } + + // Verify README.md was cloned. + if _, err := os.Stat(filepath.Join(res.HostPath, "README.md")); err != nil { + t.Errorf("git clone did not bring README.md: %v", err) + } + + // Verify sentinel. + sentinelPath := filepath.Join(filepath.Dir(res.HostPath), ProvisionSentinelFile) + if _, err := os.Stat(sentinelPath); err != nil { + t.Errorf("sentinel not written: %v", err) + } +} + +// --- Idempotent: second Provision is a no-op (sentinel short-circuits) --- + +func TestNFSProvision_Idempotent(t *testing.T) { + b, _, _ := nfsTestBackend(t) + locker := newTestLocker() + + bareRepo := initBareGitRepo(t) + projectID := "proj-idem-1" + res, err := b.Resolve(ResolveInput{ + ProjectID: projectID, + Mode: store.SharingModeSharedPlain, + }) + if err != nil { + t.Fatalf("Resolve: %v", err) + } + + input := ProvisionInput{ + Resolved: res, + ProjectID: projectID, + Mode: store.SharingModeSharedPlain, + Locker: locker, + GitClone: &api.GitCloneConfig{ + URL: bareRepo, + Branch: "main", + Depth: 1, + }, + } + + // First provision. + if err := ProvisionShared(input); err != nil { + t.Fatalf("first Provision: %v", err) + } + + // Second provision — should succeed without re-cloning. + if err := ProvisionShared(input); err != nil { + t.Fatalf("second Provision: %v", err) + } + + // Lock acquired twice (once per call — lock is always acquired, sentinel + // check happens after lock). + if got := atomic.LoadInt64(&locker.acquires); got != 2 { + t.Errorf("expected 2 lock acquires, got %d", got) + } +} + +// --- Sentinel short-circuit: no re-clone even with git config --- + +func TestNFSProvision_SentinelShortCircuits(t *testing.T) { + b, _, _ := nfsTestBackend(t) + locker := newTestLocker() + + projectID := "proj-sentinel-1" + res, err := b.Resolve(ResolveInput{ + ProjectID: projectID, + Mode: store.SharingModeSharedPlain, + }) + if err != nil { + t.Fatalf("Resolve: %v", err) + } + + // Pre-create workspace dir and sentinel (simulating prior provisioning). + if err := os.MkdirAll(res.HostPath, 0770); err != nil { + t.Fatal(err) + } + projectRoot := filepath.Dir(res.HostPath) + sentinelPath := filepath.Join(projectRoot, ProvisionSentinelFile) + if err := os.WriteFile(sentinelPath, []byte("test"), 0644); err != nil { + t.Fatal(err) + } + + // Provision with a git URL that would fail if actually attempted. + err = ProvisionShared(ProvisionInput{ + Resolved: res, + ProjectID: projectID, + Mode: store.SharingModeSharedPlain, + Locker: locker, + GitClone: &api.GitCloneConfig{ + URL: "https://nonexistent.example.com/repo.git", + }, + }) + if err != nil { + t.Fatalf("Provision with sentinel should succeed: %v", err) + } +} + +// --- WorktreePerAgent: creates worktree on shared checkout --- + +func TestNFSProvision_WorktreePerAgent(t *testing.T) { + b, _, _ := nfsTestBackend(t) + locker := newTestLocker() + + bareRepo := initBareGitRepo(t) + projectID := "proj-wt-1" + agentID := "agent-wt-1" + + res, err := b.Resolve(ResolveInput{ + ProjectID: projectID, + Mode: store.SharingModeWorktreePerAgent, + }) + if err != nil { + t.Fatalf("Resolve: %v", err) + } + + err = ProvisionShared(ProvisionInput{ + Resolved: res, + ProjectID: projectID, + AgentID: agentID, + AgentName: "test-agent", + Mode: store.SharingModeWorktreePerAgent, + Locker: locker, + GitClone: &api.GitCloneConfig{ + URL: bareRepo, + Branch: "main", + Depth: 0, // full clone needed for worktrees + }, + }) + if err != nil { + t.Fatalf("Provision: %v", err) + } + + // Verify worktree was created. + worktreePath := filepath.Join(res.HostPath, "worktrees", agentID) + if _, err := os.Stat(worktreePath); err != nil { + t.Errorf("worktree not created at %s: %v", worktreePath, err) + } + + // Verify .git pointer file exists in worktree (git worktree add creates it). + gitFile := filepath.Join(worktreePath, ".git") + if _, err := os.Stat(gitFile); err != nil { + t.Errorf("worktree .git file not found: %v", err) + } +} + +// --- WorktreePerAgent: second agent gets independent worktree --- + +func TestNFSProvision_WorktreePerAgent_TwoAgents(t *testing.T) { + b, _, _ := nfsTestBackend(t) + locker := newTestLocker() + + bareRepo := initBareGitRepo(t) + projectID := "proj-wt-2" + + res, err := b.Resolve(ResolveInput{ + ProjectID: projectID, + Mode: store.SharingModeWorktreePerAgent, + }) + if err != nil { + t.Fatalf("Resolve: %v", err) + } + + // First agent. + err = ProvisionShared(ProvisionInput{ + Resolved: res, + ProjectID: projectID, + AgentID: "agent-1", + AgentName: "first-agent", + Mode: store.SharingModeWorktreePerAgent, + Locker: locker, + GitClone: &api.GitCloneConfig{ + URL: bareRepo, + Branch: "main", + Depth: 0, + }, + }) + if err != nil { + t.Fatalf("Provision agent-1: %v", err) + } + + // Second agent (sentinel exists, so clone is skipped — just adds worktree). + err = ProvisionShared(ProvisionInput{ + Resolved: res, + ProjectID: projectID, + AgentID: "agent-2", + AgentName: "second-agent", + Mode: store.SharingModeWorktreePerAgent, + Locker: locker, + GitClone: &api.GitCloneConfig{ + URL: bareRepo, + Branch: "main", + Depth: 0, + }, + }) + if err != nil { + t.Fatalf("Provision agent-2: %v", err) + } + + // Both worktrees exist and are independent. + wt1 := filepath.Join(res.HostPath, "worktrees", "agent-1") + wt2 := filepath.Join(res.HostPath, "worktrees", "agent-2") + if _, err := os.Stat(wt1); err != nil { + t.Errorf("worktree agent-1 not found: %v", err) + } + if _, err := os.Stat(wt2); err != nil { + t.Errorf("worktree agent-2 not found: %v", err) + } +} + +// --- Per-project lock independence --- + +func TestNFSProvision_LockPerProject_Independent(t *testing.T) { + b, _, _ := nfsTestBackend(t) + locker := newTestLocker() + + // Two different projects should get independent locks. + hash1 := store.StableProjectHash("proj-A") + hash2 := store.StableProjectHash("proj-B") + if hash1 == hash2 { + t.Skip("hash collision — extremely unlikely but skip test") + } + + res1, _ := b.Resolve(ResolveInput{ProjectID: "proj-A", Mode: store.SharingModeSharedPlain}) + res2, _ := b.Resolve(ResolveInput{ProjectID: "proj-B", Mode: store.SharingModeSharedPlain}) + + // Provision both — they should not block each other. + if err := ProvisionShared(ProvisionInput{ + Resolved: res1, ProjectID: "proj-A", Mode: store.SharingModeSharedPlain, Locker: locker, + }); err != nil { + t.Fatalf("Provision proj-A: %v", err) + } + if err := ProvisionShared(ProvisionInput{ + Resolved: res2, ProjectID: "proj-B", Mode: store.SharingModeSharedPlain, Locker: locker, + }); err != nil { + t.Fatalf("Provision proj-B: %v", err) + } + + if got := atomic.LoadInt64(&locker.acquires); got != 2 { + t.Errorf("expected 2 lock acquires (one per project), got %d", got) + } +} + +// --- Same project, same lock (mutual exclusion) --- + +func TestNFSProvision_LockPerProject_MutualExclusion(t *testing.T) { + b, _, _ := nfsTestBackend(t) + + // A locker that simulates a lock already held by another node. + blockedLocker := &blockingLocker{blockedUntil: 3} // first 3 attempts blocked + + res, _ := b.Resolve(ResolveInput{ProjectID: "proj-locked", Mode: store.SharingModeSharedPlain}) + + err := ProvisionShared(ProvisionInput{ + Resolved: res, + ProjectID: "proj-locked", + Mode: store.SharingModeSharedPlain, + Locker: blockedLocker, + }) + if err != nil { + t.Fatalf("Provision should eventually succeed after retries: %v", err) + } + + // Verify it retried the expected number of times. + if got := atomic.LoadInt64(&blockedLocker.attempts); got != 4 { + t.Errorf("expected 4 attempts (3 blocked + 1 success), got %d", got) + } +} + +// blockingLocker simulates a lock held by another node for the first N attempts. +type blockingLocker struct { + blockedUntil int64 + attempts int64 +} + +func (l *blockingLocker) TryAdvisoryLock(ctx context.Context, key store.AdvisoryLockKey) (bool, func() error, error) { + return true, func() error { return nil }, nil +} + +func (l *blockingLocker) TryAdvisoryLockObject(ctx context.Context, classID store.AdvisoryLockKey, objID int32) (bool, func() error, error) { + attempt := atomic.AddInt64(&l.attempts, 1) + if attempt <= l.blockedUntil { + return false, func() error { return nil }, nil + } + return true, func() error { return nil }, nil +} + +// --- No locker: degrades gracefully --- + +func TestNFSProvision_NoLocker_DegradedMode(t *testing.T) { + b, _, _ := nfsTestBackend(t) + + res, _ := b.Resolve(ResolveInput{ProjectID: "proj-nolock", Mode: store.SharingModeSharedPlain}) + + err := ProvisionShared(ProvisionInput{ + Resolved: res, + ProjectID: "proj-nolock", + Mode: store.SharingModeSharedPlain, + Locker: nil, // no locker + }) + if err != nil { + t.Fatalf("Provision without locker should succeed: %v", err) + } +} + +// --- WorktreePerAgent missing AgentID --- + +func TestNFSProvision_WorktreePerAgent_MissingAgentID(t *testing.T) { + b, _, _ := nfsTestBackend(t) + locker := newTestLocker() + + bareRepo := initBareGitRepo(t) + res, _ := b.Resolve(ResolveInput{ProjectID: "proj-noagent", Mode: store.SharingModeWorktreePerAgent}) + + err := ProvisionShared(ProvisionInput{ + Resolved: res, + ProjectID: "proj-noagent", + AgentID: "", // missing + Mode: store.SharingModeWorktreePerAgent, + Locker: locker, + GitClone: &api.GitCloneConfig{ + URL: bareRepo, + Branch: "main", + }, + }) + if err == nil { + t.Fatal("expected error for missing AgentID in WorktreePerAgent") + } +} + +// --- SentinelDir override --- + +func TestNFSProvision_DefaultSentinelDir_IsParent(t *testing.T) { + b, _, _ := nfsTestBackend(t) + locker := newTestLocker() + + projectID := "proj-sentinel-default" + res, err := b.Resolve(ResolveInput{ + ProjectID: projectID, + Mode: store.SharingModeSharedPlain, + }) + if err != nil { + t.Fatalf("Resolve: %v", err) + } + + err = ProvisionShared(ProvisionInput{ + Resolved: res, + ProjectID: projectID, + Mode: store.SharingModeSharedPlain, + Locker: locker, + // SentinelDir is empty → default to parent + }) + if err != nil { + t.Fatalf("Provision: %v", err) + } + + // Sentinel should be in the parent of HostPath (project root). + parentSentinel := filepath.Join(filepath.Dir(res.HostPath), ProvisionSentinelFile) + if _, err := os.Stat(parentSentinel); err != nil { + t.Errorf("default sentinel should be in parent dir: %v", err) + } + + // Sentinel should NOT be inside workspace dir. + workspaceSentinel := filepath.Join(res.HostPath, ProvisionSentinelFile) + if _, err := os.Stat(workspaceSentinel); err == nil { + t.Errorf("default sentinel should not be inside workspace dir") + } +} + +func TestNFSProvision_CustomSentinelDir(t *testing.T) { + b, _, _ := nfsTestBackend(t) + locker := newTestLocker() + + projectID := "proj-sentinel-custom" + res, err := b.Resolve(ResolveInput{ + ProjectID: projectID, + Mode: store.SharingModeSharedPlain, + }) + if err != nil { + t.Fatalf("Resolve: %v", err) + } + + err = ProvisionShared(ProvisionInput{ + Resolved: res, + ProjectID: projectID, + Mode: store.SharingModeSharedPlain, + Locker: locker, + SentinelDir: res.HostPath, // sentinel inside workspace dir + }) + if err != nil { + t.Fatalf("Provision: %v", err) + } + + // Sentinel should be inside the workspace dir. + workspaceSentinel := filepath.Join(res.HostPath, ProvisionSentinelFile) + if _, err := os.Stat(workspaceSentinel); err != nil { + t.Errorf("custom sentinel should be inside workspace dir: %v", err) + } +} + +func TestNFSProvision_CustomSentinelDir_Idempotent(t *testing.T) { + b, _, _ := nfsTestBackend(t) + locker := newTestLocker() + bareRepo := initBareGitRepo(t) + + projectID := "proj-sentinel-idem" + res, err := b.Resolve(ResolveInput{ + ProjectID: projectID, + Mode: store.SharingModeSharedPlain, + }) + if err != nil { + t.Fatalf("Resolve: %v", err) + } + + input := ProvisionInput{ + Resolved: res, + ProjectID: projectID, + Mode: store.SharingModeSharedPlain, + Locker: locker, + SentinelDir: res.HostPath, + GitClone: &api.GitCloneConfig{ + URL: bareRepo, + Branch: "main", + Depth: 1, + }, + } + + if err := ProvisionShared(input); err != nil { + t.Fatalf("first Provision: %v", err) + } + if err := ProvisionShared(input); err != nil { + t.Fatalf("second Provision (should be idempotent): %v", err) + } + + // Sentinel exists in the custom dir. + sentinel := filepath.Join(res.HostPath, ProvisionSentinelFile) + if _, err := os.Stat(sentinel); err != nil { + t.Errorf("sentinel should exist in custom dir: %v", err) + } +} diff --git a/pkg/runtime/worktree_eligibility.go b/pkg/runtime/worktree_eligibility.go new file mode 100644 index 000000000..f0ba653ff --- /dev/null +++ b/pkg/runtime/worktree_eligibility.go @@ -0,0 +1,48 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package runtime + +import ( + "fmt" + + "github.com/GoogleCloudPlatform/scion/pkg/util" +) + +// WorktreeModeEligible reports whether the host environment supports +// worktree-per-agent mode. It returns (true, "") when git >= 2.47 is +// available (required for --relative-paths), or (false, reason) with a +// human-readable explanation when the check fails. +// +// The caller is responsible for logging and for choosing the fallback +// (typically clone-per-agent). +func WorktreeModeEligible() (bool, string) { + version, _, err := util.GetGitVersion() + if err != nil { + return false, fmt.Sprintf("unable to determine git version: %v", err) + } + return worktreeEligibleForVersion(version) +} + +// worktreeEligibleForVersion is the testable core: it checks whether the +// given version string satisfies the git >= 2.47 requirement. +func worktreeEligibleForVersion(version string) (bool, string) { + if err := util.CompareGitVersion(version, 2, 47); err != nil { + return false, fmt.Sprintf( + "git >= 2.47.0 required for worktree-per-agent mode (--relative-paths), found %s", + version, + ) + } + return true, "" +} diff --git a/pkg/runtime/worktree_eligibility_test.go b/pkg/runtime/worktree_eligibility_test.go new file mode 100644 index 000000000..d7f8dcffc --- /dev/null +++ b/pkg/runtime/worktree_eligibility_test.go @@ -0,0 +1,77 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package runtime + +import ( + "strings" + "testing" +) + +func TestWorktreeEligibleForVersion(t *testing.T) { + tests := []struct { + name string + version string + wantOK bool + wantSub string // substring expected in reason when !ok + }{ + { + name: "below minimum", + version: "2.46.0", + wantOK: false, + wantSub: "2.46.0", + }, + { + name: "exact minimum", + version: "2.47.0", + wantOK: true, + }, + { + name: "above minimum", + version: "2.54.1", + wantOK: true, + }, + { + name: "malformed version", + version: "not-a-version", + wantOK: false, + wantSub: "not-a-version", + }, + { + name: "major version ahead", + version: "3.0.0", + wantOK: true, + }, + { + name: "empty string", + version: "", + wantOK: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + ok, reason := worktreeEligibleForVersion(tt.version) + if ok != tt.wantOK { + t.Errorf("worktreeEligibleForVersion(%q) ok = %v, want %v (reason: %s)", tt.version, ok, tt.wantOK, reason) + } + if !ok && tt.wantSub != "" && !strings.Contains(reason, tt.wantSub) { + t.Errorf("reason %q should contain %q", reason, tt.wantSub) + } + if ok && reason != "" { + t.Errorf("expected empty reason when eligible, got %q", reason) + } + }) + } +} diff --git a/pkg/runtimebroker/NFS_DEPLOY_NOTES.md b/pkg/runtimebroker/NFS_DEPLOY_NOTES.md new file mode 100644 index 000000000..b0bfe3b83 --- /dev/null +++ b/pkg/runtimebroker/NFS_DEPLOY_NOTES.md @@ -0,0 +1,80 @@ +# NFS Workspace Storage — Deploy Notes + +## Broker Service UID/GID Alignment + +The broker service user's UID and GID **must** match the `NFS.UID` and +`NFS.GID` values in `settings.yaml` (default: `1000:1000`). + +When the broker provisions NFS-backed workspaces (clone, chown under the +Postgres advisory lock), it writes files using its own on-wire UID/GID. +Agent containers also run as `NFS.UID:GID`. If these differ, the broker +creates files that the container user cannot write to (or vice versa), +causing permission errors on NFS. + +### How to verify + +```bash +# On the broker host / container: +id # should show uid=1000(scion) gid=1000(scion) + +# In settings.yaml: +# server: +# workspace_storage: +# backend: nfs +# nfs: +# uid: 1000 +# gid: 1000 +``` + +### Common issue (NM1 finding) + +During the NM1 live gate the broker container ran as `uid=1002` while +`NFS.UID` was `1000`. This caused a UID mismatch requiring a manual +`groupadd`/`usermod` workaround. To prevent this in production: + +1. Set the broker container's user to `1000:1000` in the Dockerfile or + K8s `securityContext.runAsUser/runAsGroup`. +2. Or adjust `NFS.UID/GID` to match the broker service user's identity. + +## Mount Privilege + +The broker process requires mount privilege to auto-mount NFS shares at +startup (see `NFSMountReconciler`). Options, in order of preference: + +- Configure `/etc/sudoers` to allow the broker user to run `mount`/`umount` + without a password, and have the reconciler invoke `sudo mount` (recommended + for a non-root service). +- Run the broker as root. + +### Important (NM1b finding): `CAP_SYS_ADMIN` alone is NOT sufficient + +The userspace `mount.nfs`/`mount.nfs4` helper **checks `uid == 0` explicitly** +(not Linux capabilities), so granting `CAP_SYS_ADMIN` via `setcap` or a K8s +`securityContext.capabilities` add does **not** let a non-root broker run +`mount -t nfs`. During NM1b the service had to run as `User=root` for the +helper to succeed. To run unprivileged, use the `sudo mount` wrapper above, or +have the reconciler call the `mount(2)` syscall directly (which does honor +`CAP_SYS_ADMIN`) rather than shelling out to the `mount.nfs` helper. + +## Config: `schema_version` required (NM1b finding) + +`settings.yaml` **must** include `schema_version: "1"` when it contains a +`server.workspace_storage` block. A config without `schema_version` is treated +as legacy and auto-migrated to v1; the legacy→v1 migration does **not** carry +the `workspace_storage` block, so it is silently stripped. Always set: + +```yaml +schema_version: "1" +server: + workspace_storage: + backend: nfs + nfs: { ... } +``` + +## NFSv3 Default + +The default `mount_options` is `vers=3,hard,nconnect=4,_netdev`. This +targets Google Cloud Filestore **basic** (BASIC_HDD) tier, which supports +NFSv3 only. NFSv4.1 requires Filestore Enterprise/zonal or a self-hosted +NFS server. Override `mount_options` in `settings.yaml` if using a v4.1-capable +server. diff --git a/pkg/runtimebroker/handlers.go b/pkg/runtimebroker/handlers.go index e6b969235..649bd83fb 100644 --- a/pkg/runtimebroker/handlers.go +++ b/pkg/runtimebroker/handlers.go @@ -79,6 +79,11 @@ func (s *Server) GetHealthInfo(ctx context.Context) *HealthResponse { checks["runtime"] = "unavailable" } + // NFS mount health + if s.nfsMountReconciler != nil { + checks["nfs_mounts"] = s.nfsMountReconciler.HealthCheckString() + } + status := "healthy" for _, v := range checks { if v != "available" && v != "healthy" { @@ -561,6 +566,14 @@ func (s *Server) createAgent(w http.ResponseWriter, r *http.Request) { } } + // N1-7: Ensure NFS shares are mounted before dispatch (no-op when backend=local). + if err := s.ensureNFSMountsReady(); err != nil { + markAttemptFailed(http.StatusServiceUnavailable, "NFS mount check failed: "+err.Error()) + writeError(w, http.StatusServiceUnavailable, "nfs_unavailable", + "NFS workspace storage is not available: "+err.Error(), nil) + return + } + // Build unified start context (project path, env, template, git-clone, secrets, manager) s.agentLifecycleLog.Info("Agent dispatch: pre-flight complete", "agent_id", req.ID, "name", req.Name, "elapsed", time.Since(createStart).String()) @@ -581,6 +594,7 @@ func (s *Server) createAgent(w http.ResponseWriter, r *http.Request) { ResolvedEnv: req.ResolvedEnv, ResolvedSecrets: req.ResolvedSecrets, Attach: req.Attach, + WorkspaceMode: req.WorkspaceMode, HTTPRequest: r, }) if err != nil { @@ -1099,6 +1113,8 @@ func (s *Server) handleAgentAction(w http.ResponseWriter, r *http.Request, id, p s.sendMessage(w, r, id, projectID) case api.AgentActionExec: s.execCommand(w, r, id, projectID) + case api.AgentActionResetAuth: + s.resetAuth(w, r, id, projectID) case api.AgentActionLogs: s.getLogs(w, r, id, projectID) case api.AgentActionStats: @@ -1588,6 +1604,72 @@ func (s *Server) execCommand(w http.ResponseWriter, r *http.Request, id, project }) } +// resetAuth writes a fresh token into a running agent's container and signals +// sciontool init (PID 1) to restart its token refresh loop via SIGUSR2. +func (s *Server) resetAuth(w http.ResponseWriter, r *http.Request, id, projectID string) { + ctx := r.Context() + + var req ResetAuthRequest + if err := readJSON(r, &req); err != nil { + BadRequest(w, "Invalid request body: "+err.Error()) + return + } + + if req.Token == "" { + ValidationError(w, "token is required", nil) + return + } + + rt := s.resolveRuntimeForAgent(ctx, id, projectID) + target, err := s.LookupContainerID(ctx, id, projectID) + if err != nil || target == "" { + NotFound(w, "Agent") + return + } + + // Write the token to the canonical file atomically via temp+rename. + // Write the token to the canonical file atomically via temp+rename. + // Pass the token as part of the script using a heredoc pattern to avoid + // exposing it in argv (visible in /proc). + writeCmd := []string{"sh", "-c", + "TOKEN_DIR=\"$(getent passwd scion 2>/dev/null | cut -d: -f6 || echo /home/scion)/.scion\" && " + + "mkdir -p \"$TOKEN_DIR\" && " + + "cat <<'SCION_TOKEN_EOF' > \"$TOKEN_DIR/scion-token.tmp\"\n" + req.Token + "\nSCION_TOKEN_EOF\n" + + "mv \"$TOKEN_DIR/scion-token.tmp\" \"$TOKEN_DIR/scion-token\"", + } + + if _, err := rt.Exec(ctx, target, writeCmd); err != nil { + s.agentLifecycleLog.Error("reset-auth: failed to write token file", "agent_id", id, "error", err) + RuntimeError(w, "Failed to write token file: "+err.Error()) + return + } + + // Signal sciontool init (PID 1) to re-read the token and restart its refresh + // loop immediately. The token was already written above, and the agent also + // polls the token file as a UID-safe fallback, so it recovers within a few + // seconds even if this signal fails. In rootless containers the broker execs + // as the scion user and `kill -USR2 1` against the root-owned PID 1 fails + // with EPERM — this is expected and not an error since the token is on disk. + signalCmd := []string{"kill", "-USR2", "1"} + signaled := true + if _, err := rt.Exec(ctx, target, signalCmd); err != nil { + signaled = false + s.agentLifecycleLog.Warn("reset-auth: failed to signal PID 1 (token still written, poller will reload)", "agent_id", id, "error", err) + } + + s.agentLifecycleLog.Info("Auth reset completed", "agent_id", id, "signaled", signaled) + + s.forceHeartbeatAll("reset-auth", id) + + msg := "Auth reset: token written and init signaled" + if !signaled { + msg = "Auth reset: token written; signal failed (poller will reload)" + } + writeJSON(w, http.StatusOK, ResetAuthResponse{ + Message: msg, + }) +} + func (s *Server) getLogs(w http.ResponseWriter, r *http.Request, id, projectID string) { ctx := r.Context() @@ -2620,3 +2702,27 @@ func isLocalhostEndpoint(endpoint string) bool { host := u.Hostname() return host == "localhost" || host == "127.0.0.1" || host == "::1" } + +// ensureNFSMountsReady verifies that all configured NFS shares are mounted +// before dispatching an agent. This is a pre-flight check (N1-7): +// the reconciler may have mounted them at startup, but a transient +// unmount (network blip, manual intervention) should block dispatches. +// Returns an error if any configured share cannot be mounted — the caller +// should reject the dispatch to avoid silent fallback to a broken mount. +func (s *Server) ensureNFSMountsReady() error { + if s.nfsMountReconciler == nil { + return nil // NFS not configured — local backend, nothing to check. + } + + nfsCfg := s.config.NFSConfig + if nfsCfg == nil || len(nfsCfg.Shares) == 0 { + return nil + } + + for _, share := range nfsCfg.Shares { + if err := s.nfsMountReconciler.EnsureShareMounted(share.ID); err != nil { + return err + } + } + return nil +} diff --git a/pkg/runtimebroker/handlers_reset_auth_test.go b/pkg/runtimebroker/handlers_reset_auth_test.go new file mode 100644 index 000000000..7b84fed1e --- /dev/null +++ b/pkg/runtimebroker/handlers_reset_auth_test.go @@ -0,0 +1,133 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package runtimebroker + +import ( + "bytes" + "context" + "encoding/json" + "fmt" + "net/http" + "net/http/httptest" + "strings" + "testing" + + "github.com/GoogleCloudPlatform/scion/pkg/api" + scionrt "github.com/GoogleCloudPlatform/scion/pkg/runtime" +) + +// resetAuthAgents returns a single-agent manager fixture used by the +// reset-auth handler tests. +func resetAuthAgents() *filteringMockManager { + mgr := &filteringMockManager{} + mgr.agents = []api.AgentInfo{ + { + ContainerID: "container-A", + Name: "coordinator", + Labels: map[string]string{"scion.name": "coordinator", "scion.grove_id": "grove-A"}, + }, + } + return mgr +} + +func doResetAuth(t *testing.T, srv *Server, token string) *httptest.ResponseRecorder { + t.Helper() + body, _ := json.Marshal(ResetAuthRequest{Token: token}) + r := httptest.NewRequest(http.MethodPost, + "/api/v1/agents/coordinator/reset-auth?projectId=grove-A", bytes.NewReader(body)) + w := httptest.NewRecorder() + srv.handleAgentByID(w, r) + return w +} + +// TestResetAuth_SignalFailureStillReturns200 verifies that when the SIGUSR2 +// signal to PID 1 fails (e.g. EPERM in rootless containers), the handler still +// returns 200 OK because the token was successfully written — the agent's +// file poller will pick it up within seconds. +func TestResetAuth_SignalFailureStillReturns200(t *testing.T) { + mgr := resetAuthAgents() + + var wroteToken bool + rt := &scionrt.MockRuntime{ + NameFunc: func() string { return "docker" }, + ExecFunc: func(_ context.Context, _ string, cmd []string) (string, error) { + if len(cmd) > 0 && cmd[0] == "kill" { + return "", fmt.Errorf("kill: (1) - Operation not permitted") + } + wroteToken = true + return "", nil + }, + } + srv := New(DefaultServerConfig(), mgr, rt) + + w := doResetAuth(t, srv, "fresh-token") + + if w.Code != http.StatusOK { + t.Fatalf("expected 200 even when the reset signal fails, got %d (%s)", w.Code, w.Body.String()) + } + if !wroteToken { + t.Error("token should still be written to disk even when the signal fails") + } + if !strings.Contains(w.Body.String(), "signal failed") { + t.Errorf("response should mention signal failure, got %q", w.Body.String()) + } +} + +// TestResetAuth_SignalSuccessReturns200 verifies the happy path: token written +// and PID 1 signaled successfully yields a 200. +func TestResetAuth_SignalSuccessReturns200(t *testing.T) { + mgr := resetAuthAgents() + + var signaled bool + rt := &scionrt.MockRuntime{ + NameFunc: func() string { return "docker" }, + ExecFunc: func(_ context.Context, _ string, cmd []string) (string, error) { + if len(cmd) > 0 && cmd[0] == "kill" { + signaled = true + } + return "", nil + }, + } + srv := New(DefaultServerConfig(), mgr, rt) + + w := doResetAuth(t, srv, "fresh-token") + + if w.Code != http.StatusOK { + t.Fatalf("expected 200 on success, got %d (%s)", w.Code, w.Body.String()) + } + if !signaled { + t.Error("expected PID 1 to be signaled via kill -USR2 1") + } +} + +// TestResetAuth_MissingTokenIsValidationError verifies an empty token is +// rejected before any container interaction. +func TestResetAuth_MissingTokenIsValidationError(t *testing.T) { + mgr := resetAuthAgents() + rt := &scionrt.MockRuntime{ + NameFunc: func() string { return "docker" }, + ExecFunc: func(_ context.Context, _ string, _ []string) (string, error) { + t.Error("Exec must not be called when token is missing") + return "", nil + }, + } + srv := New(DefaultServerConfig(), mgr, rt) + + w := doResetAuth(t, srv, "") + + if w.Code != http.StatusBadRequest && w.Code != http.StatusUnprocessableEntity { + t.Fatalf("expected a client error for missing token, got %d (%s)", w.Code, w.Body.String()) + } +} diff --git a/pkg/runtimebroker/nfs_mount.go b/pkg/runtimebroker/nfs_mount.go new file mode 100644 index 000000000..78c8a628e --- /dev/null +++ b/pkg/runtimebroker/nfs_mount.go @@ -0,0 +1,296 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package runtimebroker + +import ( + "fmt" + "log/slog" + "os" + "path/filepath" + "strings" + "sync" + + "github.com/GoogleCloudPlatform/scion/pkg/config" +) + +// MountChecker abstracts the syscall/exec layer for NFS mount reconciliation. +// This allows unit tests to assert reconciliation logic (mountpoint check, +// server:export verify, idempotency) without real NFS. +type MountChecker interface { + // IsMountpoint returns true if the given path is currently a mountpoint. + IsMountpoint(path string) (bool, error) + + // MountInfo returns the server:export (e.g. "10.0.0.2:/scion-workspaces") + // for a given mountpoint. Returns ("", nil) if the path is not mounted. + MountInfo(path string) (serverExport string, err error) + + // Mount executes the NFS mount command. + // Requires mount privilege (root or CAP_SYS_ADMIN/sudo). + Mount(server, export, target, options string) error + + // Unmount unmounts the given mountpoint so it can be remounted. + Unmount(target string) error + + // MkdirAll creates the directory tree for the mountpoint. + MkdirAll(path string, perm os.FileMode) error +} + +// NFSMountReconciler ensures configured NFS shares are mounted at the expected +// paths and reports health status. It is safe for concurrent use. +// +// Deploy note: The broker process requires mount privilege (root or +// CAP_SYS_ADMIN) to mount NFS shares. When running as a non-root user, +// either grant CAP_SYS_ADMIN via setcap or configure sudoers for mount/umount. +type NFSMountReconciler struct { + cfg *config.V1NFSConfig + checker MountChecker + log *slog.Logger + + mu sync.RWMutex + statuses map[string]ShareMountStatus // keyed by share ID +} + +// ShareMountStatus tracks the health of a single NFS share mount. +type ShareMountStatus struct { + ShareID string `json:"shareId"` + Target string `json:"target"` + Healthy bool `json:"healthy"` + Message string `json:"message,omitempty"` +} + +// NewNFSMountReconciler creates a reconciler for the given NFS config. +// The checker abstracts mount syscalls for testability. +func NewNFSMountReconciler(cfg *config.V1NFSConfig, checker MountChecker, log *slog.Logger) *NFSMountReconciler { + if log == nil { + log = slog.Default() + } + return &NFSMountReconciler{ + cfg: cfg, + checker: checker, + log: log, + statuses: make(map[string]ShareMountStatus), + } +} + +// Reconcile ensures all configured NFS shares are mounted at the correct +// paths. It is idempotent: a broker restart calls Reconcile again without +// double-mounting or erroring on an already-correct state. +// +// For each configured share: +// - target = / +// - if target is not a mountpoint → mkdir -p target → mount NFS +// - if already a mountpoint → verify it points at the expected server:export; +// if wrong → log + remount +// +// Returns an error only if no shares are configured. Individual share failures +// are tracked in per-share status (unhealthy) and logged, but do not block +// other shares from mounting. +func (r *NFSMountReconciler) Reconcile() error { + if r.cfg == nil { + return fmt.Errorf("NFS config is nil") + } + if len(r.cfg.Shares) == 0 { + return fmt.Errorf("no NFS shares configured") + } + + mountOpts := r.cfg.MountOptions + if mountOpts == "" { + mountOpts = "vers=3,hard,nconnect=4,_netdev" + } + + for _, share := range r.cfg.Shares { + r.reconcileShare(share, mountOpts) + } + + return nil +} + +// reconcileShare handles a single share's mount reconciliation. +func (r *NFSMountReconciler) reconcileShare(share config.V1NFSShare, mountOpts string) { + target := filepath.Join(r.cfg.MountRoot, share.ID) + wantServerExport := fmt.Sprintf("%s:%s", share.Server, share.Export) + + r.log.Info("Reconciling NFS share", + "shareID", share.ID, "target", target, + "server", share.Server, "export", share.Export) + + mounted, err := r.checker.IsMountpoint(target) + if err != nil { + r.setStatus(share.ID, target, false, + fmt.Sprintf("failed to check mountpoint: %v", err)) + return + } + + if !mounted { + // Not mounted — create directory and mount. + if err := r.checker.MkdirAll(target, 0755); err != nil { + r.setStatus(share.ID, target, false, + fmt.Sprintf("failed to create mount directory: %v", err)) + return + } + + if err := r.checker.Mount(share.Server, share.Export, target, mountOpts); err != nil { + r.setStatus(share.ID, target, false, + fmt.Sprintf("mount failed: %v", err)) + return + } + + r.setStatus(share.ID, target, true, "mounted successfully") + r.log.Info("NFS share mounted", "shareID", share.ID, "target", target) + return + } + + // Already mounted — verify it points at the expected server:export. + currentServerExport, err := r.checker.MountInfo(target) + if err != nil { + r.setStatus(share.ID, target, false, + fmt.Sprintf("failed to read mount info: %v", err)) + return + } + + if currentServerExport == wantServerExport { + // Correct mount — no action needed. + r.setStatus(share.ID, target, true, "already mounted correctly") + r.log.Debug("NFS share already mounted correctly", + "shareID", share.ID, "target", target) + return + } + + // Wrong server:export — remount. + r.log.Warn("NFS share mounted with wrong source, remounting", + "shareID", share.ID, "target", target, + "current", currentServerExport, "expected", wantServerExport) + + if err := r.checker.Unmount(target); err != nil { + r.setStatus(share.ID, target, false, + fmt.Sprintf("failed to unmount for remount: %v", err)) + return + } + if err := r.checker.Mount(share.Server, share.Export, target, mountOpts); err != nil { + r.setStatus(share.ID, target, false, + fmt.Sprintf("remount failed: %v", err)) + return + } + + r.setStatus(share.ID, target, true, "remounted with correct source") + r.log.Info("NFS share remounted", "shareID", share.ID, "target", target) +} + +// setStatus records the health status of a share. +func (r *NFSMountReconciler) setStatus(shareID, target string, healthy bool, message string) { + r.mu.Lock() + defer r.mu.Unlock() + r.statuses[shareID] = ShareMountStatus{ + ShareID: shareID, + Target: target, + Healthy: healthy, + Message: message, + } + if !healthy { + r.log.Error("NFS share unhealthy", + "shareID", shareID, "target", target, "reason", message) + } +} + +// IsHealthy returns true if all configured shares are mounted and healthy. +// Returns false if any share is unhealthy or has not been reconciled yet. +func (r *NFSMountReconciler) IsHealthy() bool { + if r.cfg == nil || len(r.cfg.Shares) == 0 { + return true // no NFS configured — healthy by default + } + + r.mu.RLock() + defer r.mu.RUnlock() + + for _, share := range r.cfg.Shares { + status, ok := r.statuses[share.ID] + if !ok || !status.Healthy { + return false + } + } + return true +} + +// ShareStatuses returns the current mount status of all configured shares. +func (r *NFSMountReconciler) ShareStatuses() []ShareMountStatus { + r.mu.RLock() + defer r.mu.RUnlock() + + result := make([]ShareMountStatus, 0, len(r.statuses)) + for _, share := range r.cfg.Shares { + if status, ok := r.statuses[share.ID]; ok { + result = append(result, status) + } + } + return result +} + +// HealthCheckString returns a summary string for health reporting. +// Returns "healthy" if all shares are mounted, or "unhealthy:
" +// listing failed shares. +func (r *NFSMountReconciler) HealthCheckString() string { + if r.IsHealthy() { + return "healthy" + } + + r.mu.RLock() + defer r.mu.RUnlock() + + var unhealthy []string + for _, share := range r.cfg.Shares { + status, ok := r.statuses[share.ID] + if !ok { + unhealthy = append(unhealthy, fmt.Sprintf("%s: not reconciled", share.ID)) + } else if !status.Healthy { + unhealthy = append(unhealthy, fmt.Sprintf("%s: %s", share.ID, status.Message)) + } + } + return "unhealthy: " + strings.Join(unhealthy, "; ") +} + +// EnsureShareMounted is called before each NFS-backed dispatch to verify +// the share for a given share ID is still mounted. It re-reconciles if needed. +// Returns an error if the share cannot be verified or mounted. +func (r *NFSMountReconciler) EnsureShareMounted(shareID string) error { + if r.cfg == nil { + return fmt.Errorf("NFS config is nil") + } + + mountOpts := r.cfg.MountOptions + if mountOpts == "" { + mountOpts = "vers=3,hard,nconnect=4,_netdev" + } + + for _, share := range r.cfg.Shares { + if share.ID == shareID { + r.reconcileShare(share, mountOpts) + + r.mu.RLock() + status, ok := r.statuses[shareID] + r.mu.RUnlock() + + if !ok || !status.Healthy { + msg := "mount not healthy" + if ok { + msg = status.Message + } + return fmt.Errorf("NFS share %q is unhealthy: %s", shareID, msg) + } + return nil + } + } + + return fmt.Errorf("NFS share %q not found in config", shareID) +} diff --git a/pkg/runtimebroker/nfs_mount_exec.go b/pkg/runtimebroker/nfs_mount_exec.go new file mode 100644 index 000000000..e933443c2 --- /dev/null +++ b/pkg/runtimebroker/nfs_mount_exec.go @@ -0,0 +1,125 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package runtimebroker + +import ( + "bufio" + "fmt" + "log/slog" + "os" + "os/exec" + "strings" +) + +// ExecMountChecker is the production MountChecker that shells out to +// mount(8)/umount(8)/mountpoint(1) to manage NFS mounts. +// +// Privilege requirements: +// - The broker process must have mount privilege (root, CAP_SYS_ADMIN, or +// sudoers entry for mount/umount). Without it, Mount/Unmount will fail. +// - mountpoint(1) and /proc/mounts (Linux) require no special privilege. +type ExecMountChecker struct { + log *slog.Logger + // runCommand is the function used to run external commands. + // Defaults to execRunCommand; overridden in tests. + runCommand func(name string, args ...string) ([]byte, error) +} + +// NewExecMountChecker creates a production MountChecker. +func NewExecMountChecker(log *slog.Logger) *ExecMountChecker { + if log == nil { + log = slog.Default() + } + return &ExecMountChecker{ + log: log, + runCommand: execRunCommand, + } +} + +// execRunCommand runs a command and returns its combined output. +func execRunCommand(name string, args ...string) ([]byte, error) { + return exec.Command(name, args...).CombinedOutput() +} + +// IsMountpoint returns true if the given path is currently a mountpoint. +// Uses mountpoint(1) which is available on all modern Linux distributions. +func (e *ExecMountChecker) IsMountpoint(path string) (bool, error) { + out, err := e.runCommand("mountpoint", "-q", path) + if err != nil { + // mountpoint returns exit code 1 for non-mountpoints (not an error) + if exitErr, ok := err.(*exec.ExitError); ok && exitErr.ExitCode() == 1 { + return false, nil + } + // Other errors (path doesn't exist, permission denied) + e.log.Debug("mountpoint check failed", "path", path, "error", err, "output", string(out)) + return false, nil // treat check failure as "not mounted" so we try to mount + } + return true, nil +} + +// MountInfo returns the server:export for a given mountpoint by parsing +// /proc/mounts (Linux). Returns ("", nil) if the path is not found. +func (e *ExecMountChecker) MountInfo(path string) (string, error) { + f, err := os.Open("/proc/mounts") + if err != nil { + return "", fmt.Errorf("failed to read /proc/mounts: %w", err) + } + defer f.Close() + + scanner := bufio.NewScanner(f) + for scanner.Scan() { + fields := strings.Fields(scanner.Text()) + if len(fields) < 2 { + continue + } + // fields[0] = device (server:export for NFS), fields[1] = mountpoint + if fields[1] == path { + return fields[0], nil + } + } + if err := scanner.Err(); err != nil { + return "", fmt.Errorf("error reading /proc/mounts: %w", err) + } + return "", nil +} + +// Mount executes the NFS mount command. +// Requires mount privilege (root or CAP_SYS_ADMIN). +func (e *ExecMountChecker) Mount(server, export, target, options string) error { + source := fmt.Sprintf("%s:%s", server, export) + args := []string{"-t", "nfs", "-o", options, source, target} + e.log.Info("Mounting NFS share", "source", source, "target", target, "options", options) + + out, err := e.runCommand("mount", args...) + if err != nil { + return fmt.Errorf("mount %s on %s failed: %w (output: %s)", source, target, err, string(out)) + } + return nil +} + +// Unmount unmounts the given mountpoint. +func (e *ExecMountChecker) Unmount(target string) error { + e.log.Info("Unmounting", "target", target) + out, err := e.runCommand("umount", target) + if err != nil { + return fmt.Errorf("umount %s failed: %w (output: %s)", target, err, string(out)) + } + return nil +} + +// MkdirAll creates the directory tree for the mountpoint. +func (e *ExecMountChecker) MkdirAll(path string, perm os.FileMode) error { + return os.MkdirAll(path, perm) +} diff --git a/pkg/runtimebroker/nfs_mount_exec_test.go b/pkg/runtimebroker/nfs_mount_exec_test.go new file mode 100644 index 000000000..e3f5bd991 --- /dev/null +++ b/pkg/runtimebroker/nfs_mount_exec_test.go @@ -0,0 +1,141 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package runtimebroker + +import ( + "fmt" + "os/exec" + "strings" + "testing" +) + +// TestExecMountChecker_IsMountpoint_NotMounted verifies that IsMountpoint +// returns false when mountpoint exits with code 1 (not a mountpoint). +func TestExecMountChecker_IsMountpoint_NotMounted(t *testing.T) { + checker := NewExecMountChecker(nil) + checker.runCommand = func(name string, args ...string) ([]byte, error) { + if name != "mountpoint" { + t.Fatalf("unexpected command: %s", name) + } + if len(args) != 2 || args[0] != "-q" || args[1] != "/mnt/nfs/ws1" { + t.Fatalf("unexpected args: %v", args) + } + return nil, &exec.ExitError{} + } + + mounted, err := checker.IsMountpoint("/mnt/nfs/ws1") + if err != nil { + t.Fatalf("IsMountpoint error: %v", err) + } + if mounted { + t.Error("expected not mounted for exit code 1") + } +} + +// TestExecMountChecker_IsMountpoint_Mounted verifies that IsMountpoint +// returns true when mountpoint exits with code 0. +func TestExecMountChecker_IsMountpoint_Mounted(t *testing.T) { + checker := NewExecMountChecker(nil) + checker.runCommand = func(name string, args ...string) ([]byte, error) { + return nil, nil // exit 0 = is a mountpoint + } + + mounted, err := checker.IsMountpoint("/mnt/nfs/ws1") + if err != nil { + t.Fatalf("IsMountpoint error: %v", err) + } + if !mounted { + t.Error("expected mounted for exit code 0") + } +} + +// TestExecMountChecker_Mount_Success verifies the mount command is constructed +// correctly with the right arguments. +func TestExecMountChecker_Mount_Success(t *testing.T) { + checker := NewExecMountChecker(nil) + var capturedName string + var capturedArgs []string + checker.runCommand = func(name string, args ...string) ([]byte, error) { + capturedName = name + capturedArgs = args + return nil, nil + } + + err := checker.Mount("10.0.0.2", "/scion-ws", "/mnt/nfs/ws1", "vers=3,hard") + if err != nil { + t.Fatalf("Mount error: %v", err) + } + + if capturedName != "mount" { + t.Errorf("expected mount command, got %s", capturedName) + } + + // Expected args: -t nfs -o vers=3,hard 10.0.0.2:/scion-ws /mnt/nfs/ws1 + wantArgs := []string{"-t", "nfs", "-o", "vers=3,hard", "10.0.0.2:/scion-ws", "/mnt/nfs/ws1"} + if len(capturedArgs) != len(wantArgs) { + t.Fatalf("args len = %d, want %d: %v", len(capturedArgs), len(wantArgs), capturedArgs) + } + for i, want := range wantArgs { + if capturedArgs[i] != want { + t.Errorf("arg[%d] = %q, want %q", i, capturedArgs[i], want) + } + } +} + +// TestExecMountChecker_Mount_Failure verifies mount failure is surfaced. +func TestExecMountChecker_Mount_Failure(t *testing.T) { + checker := NewExecMountChecker(nil) + checker.runCommand = func(name string, args ...string) ([]byte, error) { + return []byte("mount: permission denied"), fmt.Errorf("exit status 32") + } + + err := checker.Mount("10.0.0.2", "/scion-ws", "/mnt/nfs/ws1", "vers=3,hard") + if err == nil { + t.Fatal("expected error from mount failure") + } + if !strings.Contains(err.Error(), "permission denied") { + t.Errorf("error should contain mount output, got: %v", err) + } +} + +// TestExecMountChecker_Unmount_Success verifies the umount command. +func TestExecMountChecker_Unmount_Success(t *testing.T) { + checker := NewExecMountChecker(nil) + var capturedName string + var capturedArgs []string + checker.runCommand = func(name string, args ...string) ([]byte, error) { + capturedName = name + capturedArgs = args + return nil, nil + } + + err := checker.Unmount("/mnt/nfs/ws1") + if err != nil { + t.Fatalf("Unmount error: %v", err) + } + + if capturedName != "umount" { + t.Errorf("expected umount command, got %s", capturedName) + } + if len(capturedArgs) != 1 || capturedArgs[0] != "/mnt/nfs/ws1" { + t.Errorf("args = %v, want [/mnt/nfs/ws1]", capturedArgs) + } +} + +// TestExecMountChecker_Interface verifies ExecMountChecker satisfies the +// MountChecker interface at compile time. +func TestExecMountChecker_Interface(t *testing.T) { + var _ MountChecker = (*ExecMountChecker)(nil) +} diff --git a/pkg/runtimebroker/nfs_mount_test.go b/pkg/runtimebroker/nfs_mount_test.go new file mode 100644 index 000000000..aa2614081 --- /dev/null +++ b/pkg/runtimebroker/nfs_mount_test.go @@ -0,0 +1,401 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package runtimebroker + +import ( + "fmt" + "os" + "path/filepath" + "testing" + + "github.com/GoogleCloudPlatform/scion/pkg/config" +) + +// mockMountChecker is a test double for MountChecker that records calls and +// returns configurable results without touching the real filesystem. +type mockMountChecker struct { + // mountpoints maps path → server:export. Present = mounted. + mountpoints map[string]string + + // mountCalls records (server, export, target, options) for each Mount call. + mountCalls []mountCall + // unmountCalls records targets passed to Unmount. + unmountCalls []string + // mkdirCalls records paths passed to MkdirAll. + mkdirCalls []string + + // Inject errors for specific operations. + isMountpointErr map[string]error + mountInfoErr map[string]error + mountErr error + unmountErr error + mkdirErr error +} + +type mountCall struct { + Server, Export, Target, Options string +} + +func newMockMountChecker() *mockMountChecker { + return &mockMountChecker{ + mountpoints: make(map[string]string), + isMountpointErr: make(map[string]error), + mountInfoErr: make(map[string]error), + } +} + +func (m *mockMountChecker) IsMountpoint(path string) (bool, error) { + if err, ok := m.isMountpointErr[path]; ok { + return false, err + } + _, ok := m.mountpoints[path] + return ok, nil +} + +func (m *mockMountChecker) MountInfo(path string) (string, error) { + if err, ok := m.mountInfoErr[path]; ok { + return "", err + } + se, ok := m.mountpoints[path] + if !ok { + return "", nil + } + return se, nil +} + +func (m *mockMountChecker) Mount(server, export, target, options string) error { + m.mountCalls = append(m.mountCalls, mountCall{server, export, target, options}) + if m.mountErr != nil { + return m.mountErr + } + m.mountpoints[target] = fmt.Sprintf("%s:%s", server, export) + return nil +} + +func (m *mockMountChecker) Unmount(target string) error { + m.unmountCalls = append(m.unmountCalls, target) + if m.unmountErr != nil { + return m.unmountErr + } + delete(m.mountpoints, target) + return nil +} + +func (m *mockMountChecker) MkdirAll(path string, perm os.FileMode) error { + m.mkdirCalls = append(m.mkdirCalls, path) + if m.mkdirErr != nil { + return m.mkdirErr + } + return nil +} + +// --- Tests --- + +func testNFSConfig() *config.V1NFSConfig { + return &config.V1NFSConfig{ + MountRoot: "/mnt/nfs", + MountOptions: "vers=3,hard,nconnect=4,_netdev", + SubPathRoot: "projects", + Shares: []config.V1NFSShare{ + {ID: "ws1", Server: "10.0.0.2", Export: "/scion-workspaces"}, + }, + } +} + +func TestReconcile_MountAbsent_MkdirAndMount(t *testing.T) { + mc := newMockMountChecker() + cfg := testNFSConfig() + r := NewNFSMountReconciler(cfg, mc, nil) + + if err := r.Reconcile(); err != nil { + t.Fatalf("Reconcile: %v", err) + } + + // Should have created the directory + wantTarget := filepath.Join("/mnt/nfs", "ws1") + if len(mc.mkdirCalls) != 1 || mc.mkdirCalls[0] != wantTarget { + t.Errorf("mkdirCalls = %v, want [%q]", mc.mkdirCalls, wantTarget) + } + + // Should have mounted + if len(mc.mountCalls) != 1 { + t.Fatalf("mountCalls = %d, want 1", len(mc.mountCalls)) + } + call := mc.mountCalls[0] + if call.Server != "10.0.0.2" || call.Export != "/scion-workspaces" || call.Target != wantTarget { + t.Errorf("mount call = %+v, want server=10.0.0.2, export=/scion-workspaces, target=%s", + call, wantTarget) + } + if call.Options != "vers=3,hard,nconnect=4,_netdev" { + t.Errorf("mount options = %q, want default NFS options", call.Options) + } + + // Should be healthy + if !r.IsHealthy() { + t.Error("expected healthy after successful mount") + } +} + +func TestReconcile_AlreadyMountedCorrectly_NoOp(t *testing.T) { + mc := newMockMountChecker() + target := filepath.Join("/mnt/nfs", "ws1") + mc.mountpoints[target] = "10.0.0.2:/scion-workspaces" + + cfg := testNFSConfig() + r := NewNFSMountReconciler(cfg, mc, nil) + + if err := r.Reconcile(); err != nil { + t.Fatalf("Reconcile: %v", err) + } + + // No mkdir or mount calls + if len(mc.mkdirCalls) != 0 { + t.Errorf("mkdirCalls = %v, want none (already mounted)", mc.mkdirCalls) + } + if len(mc.mountCalls) != 0 { + t.Errorf("mountCalls = %d, want 0 (already mounted correctly)", len(mc.mountCalls)) + } + + if !r.IsHealthy() { + t.Error("expected healthy for correctly mounted share") + } +} + +func TestReconcile_WrongServerExport_Remount(t *testing.T) { + mc := newMockMountChecker() + target := filepath.Join("/mnt/nfs", "ws1") + mc.mountpoints[target] = "10.0.0.99:/wrong-export" // wrong source + + cfg := testNFSConfig() + r := NewNFSMountReconciler(cfg, mc, nil) + + if err := r.Reconcile(); err != nil { + t.Fatalf("Reconcile: %v", err) + } + + // Should have unmounted + if len(mc.unmountCalls) != 1 || mc.unmountCalls[0] != target { + t.Errorf("unmountCalls = %v, want [%q]", mc.unmountCalls, target) + } + + // Should have remounted with correct source + if len(mc.mountCalls) != 1 { + t.Fatalf("mountCalls = %d, want 1 (remount)", len(mc.mountCalls)) + } + call := mc.mountCalls[0] + if call.Server != "10.0.0.2" || call.Export != "/scion-workspaces" { + t.Errorf("remount call = %+v, want correct server:export", call) + } + + if !r.IsHealthy() { + t.Error("expected healthy after remount") + } +} + +func TestReconcile_MultipleShares(t *testing.T) { + mc := newMockMountChecker() + cfg := &config.V1NFSConfig{ + MountRoot: "/mnt/nfs", + MountOptions: "vers=4.1,hard", + Shares: []config.V1NFSShare{ + {ID: "ws1", Server: "10.0.0.2", Export: "/export-a"}, + {ID: "ws2", Server: "10.0.0.3", Export: "/export-b"}, + }, + } + r := NewNFSMountReconciler(cfg, mc, nil) + + if err := r.Reconcile(); err != nil { + t.Fatalf("Reconcile: %v", err) + } + + if len(mc.mountCalls) != 2 { + t.Fatalf("mountCalls = %d, want 2 (one per share)", len(mc.mountCalls)) + } + + // Both should be healthy + if !r.IsHealthy() { + t.Error("expected healthy after mounting both shares") + } + + statuses := r.ShareStatuses() + if len(statuses) != 2 { + t.Errorf("ShareStatuses len = %d, want 2", len(statuses)) + } +} + +func TestReconcile_MountFailure_UnhealthySignal(t *testing.T) { + mc := newMockMountChecker() + mc.mountErr = fmt.Errorf("permission denied") + + cfg := testNFSConfig() + r := NewNFSMountReconciler(cfg, mc, nil) + + // Reconcile itself does not return an error for individual share failures + if err := r.Reconcile(); err != nil { + t.Fatalf("Reconcile: %v", err) + } + + if r.IsHealthy() { + t.Error("expected unhealthy after mount failure") + } + + hc := r.HealthCheckString() + if hc == "healthy" { + t.Error("HealthCheckString should not be 'healthy' after mount failure") + } +} + +func TestReconcile_NilConfig_Error(t *testing.T) { + mc := newMockMountChecker() + r := NewNFSMountReconciler(nil, mc, nil) + + if err := r.Reconcile(); err == nil { + t.Error("expected error for nil config") + } +} + +func TestReconcile_NoShares_Error(t *testing.T) { + mc := newMockMountChecker() + cfg := &config.V1NFSConfig{ + MountRoot: "/mnt/nfs", + Shares: nil, + } + r := NewNFSMountReconciler(cfg, mc, nil) + + if err := r.Reconcile(); err == nil { + t.Error("expected error for no shares") + } +} + +func TestReconcile_Idempotent_DoubleCall(t *testing.T) { + mc := newMockMountChecker() + cfg := testNFSConfig() + r := NewNFSMountReconciler(cfg, mc, nil) + + // First call: mounts the share + if err := r.Reconcile(); err != nil { + t.Fatalf("first Reconcile: %v", err) + } + if len(mc.mountCalls) != 1 { + t.Fatalf("expected 1 mount call after first Reconcile, got %d", len(mc.mountCalls)) + } + + // Second call: share is already mounted correctly — no-op + if err := r.Reconcile(); err != nil { + t.Fatalf("second Reconcile: %v", err) + } + if len(mc.mountCalls) != 1 { + t.Errorf("expected still 1 mount call after second Reconcile (idempotent), got %d", + len(mc.mountCalls)) + } +} + +func TestEnsureShareMounted_Healthy(t *testing.T) { + mc := newMockMountChecker() + cfg := testNFSConfig() + r := NewNFSMountReconciler(cfg, mc, nil) + + if err := r.EnsureShareMounted("ws1"); err != nil { + t.Fatalf("EnsureShareMounted: %v", err) + } + + // Should have mounted + if len(mc.mountCalls) != 1 { + t.Errorf("mountCalls = %d, want 1", len(mc.mountCalls)) + } +} + +func TestEnsureShareMounted_UnknownShare(t *testing.T) { + mc := newMockMountChecker() + cfg := testNFSConfig() + r := NewNFSMountReconciler(cfg, mc, nil) + + if err := r.EnsureShareMounted("nonexistent"); err == nil { + t.Error("expected error for unknown share ID") + } +} + +func TestEnsureShareMounted_MountFailure(t *testing.T) { + mc := newMockMountChecker() + mc.mountErr = fmt.Errorf("network unreachable") + + cfg := testNFSConfig() + r := NewNFSMountReconciler(cfg, mc, nil) + + if err := r.EnsureShareMounted("ws1"); err == nil { + t.Error("expected error when mount fails") + } +} + +func TestHealthCheckString_Healthy(t *testing.T) { + mc := newMockMountChecker() + cfg := testNFSConfig() + r := NewNFSMountReconciler(cfg, mc, nil) + + // Mount the share + _ = r.Reconcile() + + got := r.HealthCheckString() + if got != "healthy" { + t.Errorf("HealthCheckString = %q, want %q", got, "healthy") + } +} + +func TestHealthCheckString_Unhealthy(t *testing.T) { + mc := newMockMountChecker() + mc.mountErr = fmt.Errorf("denied") + + cfg := testNFSConfig() + r := NewNFSMountReconciler(cfg, mc, nil) + _ = r.Reconcile() + + got := r.HealthCheckString() + if got == "healthy" { + t.Error("HealthCheckString should not be 'healthy' after mount failure") + } +} + +func TestReconcile_DefaultMountOptions(t *testing.T) { + mc := newMockMountChecker() + cfg := &config.V1NFSConfig{ + MountRoot: "/mnt/nfs", + MountOptions: "", // should use default + Shares: []config.V1NFSShare{ + {ID: "ws1", Server: "10.0.0.2", Export: "/scion-workspaces"}, + }, + } + r := NewNFSMountReconciler(cfg, mc, nil) + + if err := r.Reconcile(); err != nil { + t.Fatalf("Reconcile: %v", err) + } + + if len(mc.mountCalls) != 1 { + t.Fatalf("mountCalls = %d, want 1", len(mc.mountCalls)) + } + if mc.mountCalls[0].Options != "vers=3,hard,nconnect=4,_netdev" { + t.Errorf("options = %q, want default NFS options", mc.mountCalls[0].Options) + } +} + +func TestIsHealthy_NoNFSConfigured(t *testing.T) { + mc := newMockMountChecker() + r := NewNFSMountReconciler(nil, mc, nil) + + // No NFS configured → healthy by default (local backend) + if !r.IsHealthy() { + t.Error("expected healthy when no NFS is configured") + } +} diff --git a/pkg/runtimebroker/nfs_wiring_test.go b/pkg/runtimebroker/nfs_wiring_test.go new file mode 100644 index 000000000..099769b38 --- /dev/null +++ b/pkg/runtimebroker/nfs_wiring_test.go @@ -0,0 +1,173 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package runtimebroker + +import ( + "context" + "testing" + + "github.com/GoogleCloudPlatform/scion/pkg/config" +) + +// TestServer_NFSReconcilerWired_WhenNFSConfigured verifies that the +// NFSMountReconciler is constructed and stored on the Server when +// NFSConfig is present with shares. +func TestServer_NFSReconcilerWired_WhenNFSConfigured(t *testing.T) { + cfg := ServerConfig{ + Port: 0, + Host: "127.0.0.1", + NFSConfig: &config.V1NFSConfig{ + MountRoot: "/mnt/nfs", + MountOptions: "vers=3,hard", + Shares: []config.V1NFSShare{ + {ID: "ws1", Server: "10.0.0.2", Export: "/scion-workspaces"}, + }, + }, + } + + srv := New(cfg, nil, nil) + + if srv.nfsMountReconciler == nil { + t.Fatal("expected nfsMountReconciler to be constructed when NFSConfig has shares") + } +} + +// TestServer_NFSReconcilerNil_WhenLocalBackend verifies that the +// NFSMountReconciler is NOT constructed when NFSConfig is nil +// (local backend). +func TestServer_NFSReconcilerNil_WhenLocalBackend(t *testing.T) { + cfg := ServerConfig{ + Port: 0, + Host: "127.0.0.1", + // NFSConfig is nil — local backend + } + + srv := New(cfg, nil, nil) + + if srv.nfsMountReconciler != nil { + t.Fatal("expected nfsMountReconciler to be nil when NFSConfig is not set") + } +} + +// TestServer_NFSReconcilerNil_WhenNoShares verifies that the +// NFSMountReconciler is NOT constructed when NFSConfig exists but +// has no shares configured. +func TestServer_NFSReconcilerNil_WhenNoShares(t *testing.T) { + cfg := ServerConfig{ + Port: 0, + Host: "127.0.0.1", + NFSConfig: &config.V1NFSConfig{ + MountRoot: "/mnt/nfs", + Shares: nil, + }, + } + + srv := New(cfg, nil, nil) + + if srv.nfsMountReconciler != nil { + t.Fatal("expected nfsMountReconciler to be nil when no shares configured") + } +} + +// TestServer_HealthIncludesNFS verifies that NFS mount health is surfaced +// in the broker's health response when NFS is configured. +func TestServer_HealthIncludesNFS(t *testing.T) { + cfg := ServerConfig{ + Port: 0, + Host: "127.0.0.1", + NFSConfig: &config.V1NFSConfig{ + MountRoot: "/mnt/nfs", + MountOptions: "vers=3,hard", + Shares: []config.V1NFSShare{ + {ID: "ws1", Server: "10.0.0.2", Export: "/scion-workspaces"}, + }, + }, + } + + srv := New(cfg, nil, nil) + + // Before reconciliation: shares are unreconciled → unhealthy + health := srv.GetHealthInfo(context.Background()) + + nfsCheck, ok := health.Checks["nfs_mounts"] + if !ok { + t.Fatal("expected nfs_mounts key in health checks") + } + // Before Reconcile(), shares are not yet reconciled → "unhealthy" + if nfsCheck == "healthy" { + t.Error("expected unhealthy before reconciliation") + } + if health.Status != "degraded" { + t.Errorf("overall status = %q, want degraded (NFS unhealthy before reconciliation)", health.Status) + } +} + +// TestServer_HealthExcludesNFS_WhenLocal verifies that no nfs_mounts key +// appears in health when NFS is not configured. +func TestServer_HealthExcludesNFS_WhenLocal(t *testing.T) { + cfg := ServerConfig{ + Port: 0, + Host: "127.0.0.1", + } + + srv := New(cfg, nil, nil) + health := srv.GetHealthInfo(context.Background()) + + if _, ok := health.Checks["nfs_mounts"]; ok { + t.Error("did not expect nfs_mounts in health checks when NFS is not configured") + } +} + +// TestServer_EnsureNFSMountsReady_NilReconciler verifies that the dispatch +// guard is a no-op when NFS is not configured. +func TestServer_EnsureNFSMountsReady_NilReconciler(t *testing.T) { + srv := New(ServerConfig{Port: 0, Host: "127.0.0.1"}, nil, nil) + + if err := srv.ensureNFSMountsReady(); err != nil { + t.Fatalf("ensureNFSMountsReady with no NFS should return nil, got: %v", err) + } +} + +// TestServer_EnsureNFSMountsReady_WithReconciler verifies that the dispatch +// guard calls EnsureShareMounted for each configured share. +func TestServer_EnsureNFSMountsReady_WithReconciler(t *testing.T) { + nfsCfg := &config.V1NFSConfig{ + MountRoot: "/mnt/nfs", + MountOptions: "vers=3,hard", + Shares: []config.V1NFSShare{ + {ID: "ws1", Server: "10.0.0.2", Export: "/export-a"}, + {ID: "ws2", Server: "10.0.0.3", Export: "/export-b"}, + }, + } + + mc := newMockMountChecker() + srv := &Server{ + config: ServerConfig{ + Port: 0, + Host: "127.0.0.1", + NFSConfig: nfsCfg, + }, + nfsMountReconciler: NewNFSMountReconciler(nfsCfg, mc, nil), + } + + if err := srv.ensureNFSMountsReady(); err != nil { + t.Fatalf("ensureNFSMountsReady: %v", err) + } + + // Both shares should have been mounted + if len(mc.mountCalls) != 2 { + t.Errorf("mountCalls = %d, want 2 (one per share)", len(mc.mountCalls)) + } +} diff --git a/pkg/runtimebroker/server.go b/pkg/runtimebroker/server.go index 664930e39..b712b5995 100644 --- a/pkg/runtimebroker/server.go +++ b/pkg/runtimebroker/server.go @@ -144,6 +144,12 @@ type ServerConfig struct { // container-script dispatches on this broker. AllowContainerScriptHarnesses bool + // NFSConfig holds NFS workspace storage settings for this broker. + // When non-nil with shares configured, the broker can provision and + // clean up NFS-backed workspace subtrees. Used by deleteProject (N1-6) + // to also remove the NFS project subtree on project deletion. + NFSConfig *config.V1NFSConfig + // ColocatedStorage is the storage backend of a Hub running co-located in the // same process. When set and backed by the local filesystem, the broker // resolves resources for the co-located connection by reading directly from @@ -219,6 +225,15 @@ type Server struct { auxiliaryRuntimes map[string]auxiliaryRuntime auxiliaryRuntimesMu sync.RWMutex + // projectProvisionMu serializes worktree provisioning per project on this + // node. Without this, concurrent agent creations for the same project could + // race inside ProvisionShared (double-clone / corrupt .git state). + // Key: ProjectID (or ProjectPath if ID is empty). + projectProvisionMu sync.Map + + // NFS mount reconciler (nil when backend != "nfs") + nfsMountReconciler *NFSMountReconciler + // Dedicated request logger (nil = disabled) requestLogger *slog.Logger @@ -309,6 +324,17 @@ func New(cfg ServerConfig, mgr agent.Manager, rt scionrt.Runtime) *Server { } } + // Initialize NFS mount reconciler when NFS storage is configured. + // This only constructs the reconciler; Reconcile() is called in Start(). + if cfg.NFSConfig != nil && len(cfg.NFSConfig.Shares) > 0 { + nfsLog := logging.Subsystem("broker.nfs-mount") + checker := NewExecMountChecker(nfsLog) + srv.nfsMountReconciler = NewNFSMountReconciler(cfg.NFSConfig, checker, nfsLog) + slog.Info("NFS mount reconciler initialized", + "shares", len(cfg.NFSConfig.Shares), + "mountRoot", cfg.NFSConfig.MountRoot) + } + // Initialize Hub integration if enabled if cfg.HubEnabled && (cfg.HubEndpoint != "" || cfg.InMemoryCredentials != nil) { if err := srv.initHubIntegration(); err != nil { @@ -812,6 +838,20 @@ func (s *Server) Start(ctx context.Context) error { // a broker restart. s.discoverAuxiliaryRuntimes() + // Reconcile NFS mounts at startup (ensure configured shares are mounted). + if s.nfsMountReconciler != nil { + if err := s.nfsMountReconciler.Reconcile(); err != nil { + slog.Warn("NFS mount reconciliation returned error at startup", "error", err) + } + if !s.nfsMountReconciler.IsHealthy() { + slog.Error("NFS mounts unhealthy at startup", + "detail", s.nfsMountReconciler.HealthCheckString()) + } else { + slog.Info("NFS mounts reconciled at startup", + "status", s.nfsMountReconciler.HealthCheckString()) + } + } + // Start all hub connections' services s.hubMu.RLock() for name, conn := range s.hubConnections { diff --git a/pkg/runtimebroker/start_context.go b/pkg/runtimebroker/start_context.go index 05c271f3b..daa35dc74 100644 --- a/pkg/runtimebroker/start_context.go +++ b/pkg/runtimebroker/start_context.go @@ -16,15 +16,21 @@ package runtimebroker import ( "context" + "log/slog" "net/http" "os" + "os/exec" "path/filepath" "strconv" "strings" + "sync" "github.com/GoogleCloudPlatform/scion/pkg/agent" "github.com/GoogleCloudPlatform/scion/pkg/api" "github.com/GoogleCloudPlatform/scion/pkg/config" + "github.com/GoogleCloudPlatform/scion/pkg/provision" + "github.com/GoogleCloudPlatform/scion/pkg/runtime" + "github.com/GoogleCloudPlatform/scion/pkg/store" ) // startContext holds all the resolved state needed to start an agent. @@ -72,6 +78,11 @@ type startContextInputs struct { // Behavior Attach bool + // WorkspaceMode is the resolved workspace sharing mode for the project + // (e.g. "worktree-per-agent"). Threaded from CreateAgentRequest so the + // broker can branch dispatch without re-deriving from labels. + WorkspaceMode string + // HTTP request (for hub connection resolution) HTTPRequest *http.Request } @@ -433,8 +444,19 @@ func (s *Server) buildStartContext(ctx context.Context, in startContextInputs) ( } } + // --- Worktree-per-agent mode --- + // When the hub sets WorkspaceMode to worktree-per-agent and the project + // is git-backed, provision a shared base clone + per-agent worktree on + // the host BEFORE the container starts, then dual-mount it. This avoids + // the full in-container clone. Falls through to clone-per-agent on error + // or if git is too old (< 2.47). + worktreeProvisioned := false + if in.Config != nil && in.Config.GitClone != nil && in.WorkspaceMode == store.WorkspaceModeWorktreePerAgent { + worktreeProvisioned = s.tryProvisionWorktree(ctx, in, &opts, env) + } + // --- Git clone mode --- - if in.Config != nil && in.Config.GitClone != nil { + if !worktreeProvisioned && in.Config != nil && in.Config.GitClone != nil { gc := in.Config.GitClone env["SCION_GIT_CLONE_URL"] = gc.URL if gc.Branch != "" { @@ -496,6 +518,197 @@ func (e *startContextError) Error() string { return e.Message } +// tryProvisionWorktree attempts to provision a per-agent worktree on the host +// for worktree-per-agent mode. On success it sets opts.Workspace to the +// worktree path and returns true (opts.GitClone is NOT set, suppressing the +// in-container clone). On failure or if git is too old, it logs a warning and +// returns false so the caller falls through to clone-per-agent. +func (s *Server) tryProvisionWorktree(ctx context.Context, in startContextInputs, opts *api.StartOptions, env map[string]string) bool { + result := resolveWorktreeProvision(worktreeProvisionInput{ + WorkspaceMode: in.WorkspaceMode, + GitClone: in.Config.GitClone, + ProjectPath: in.ProjectPath, + ProjectID: in.ProjectID, + ProjectSlug: in.ProjectSlug, + AgentID: in.AgentID, + AgentName: in.Name, + Branch: in.Config.Branch, + }) + + if !result.ShouldProvision { + if result.Reason != "" { + slog.Warn("worktree-per-agent: falling back to clone-per-agent", + "agent_id", in.AgentID, "reason", result.Reason) + } + return false + } + + // Set Ctx from the buildStartContext context. + result.ProvisionInput.Ctx = ctx + + // Serialize same-project provisioning on this node to prevent concurrent + // ProvisionShared calls from racing on the shared base clone. + mu := s.projectProvisionMutex(in.ProjectID, in.ProjectPath) + mu.Lock() + defer mu.Unlock() + + if err := provision.ProvisionShared(result.ProvisionInput); err != nil { + slog.Warn("worktree-per-agent: provisioning failed, falling back to clone-per-agent", + "agent_id", in.AgentID, "error", err) + // Clean up ONLY this agent's partial worktree — never result.ProjectRoot, + // the shared base clone holding the common .git and every other agent's + // worktree under worktrees/. Removing the base would destroy the + // workspaces of all other running agents for this project. A partial base + // clone is self-healed by provision.gitCloneWorkspace on retry. + // + // Use `git worktree remove --force` so the worktree's admin metadata in + // the base's .git/worktrees/ is unregistered too — a bare os.RemoveAll + // would leave a stale registration that makes git refuse to recreate the + // worktree at that path on retry. Fall back to os.RemoveAll + prune. + if result.WorktreePath != "" && result.ProjectRoot != "" { + rm := exec.CommandContext(ctx, "git", "-C", result.ProjectRoot, + "worktree", "remove", "--force", result.WorktreePath) + if out, rmErr := rm.CombinedOutput(); rmErr != nil { + slog.Warn("worktree-per-agent: git worktree remove failed, falling back to os.RemoveAll+prune", + "agent_id", in.AgentID, "path", result.WorktreePath, + "error", rmErr, "output", strings.TrimSpace(string(out))) + if cleanErr := os.RemoveAll(result.WorktreePath); cleanErr != nil { + slog.Warn("worktree-per-agent: failed to clean up partial worktree", + "agent_id", in.AgentID, "path", result.WorktreePath, "error", cleanErr) + } + // Prune the now-stale .git/worktrees/ registration so retries succeed. + _ = exec.CommandContext(ctx, "git", "-C", result.ProjectRoot, "worktree", "prune").Run() + } else { + slog.Info("worktree-per-agent: cleaned up partial worktree and unregistered from git", + "agent_id", in.AgentID, "path", result.WorktreePath) + } + } + return false + } + + // Write .scion workspace marker so the in-container CLI discovers project context. + if in.ProjectID != "" && in.ProjectSlug != "" { + if err := config.WriteWorkspaceMarker(result.WorktreePath, in.ProjectID, in.ProjectSlug, in.ProjectSlug); err != nil { + slog.Warn("worktree-per-agent: failed to write workspace marker (non-fatal)", + "path", result.WorktreePath, "error", err) + } + } + + opts.Workspace = result.WorktreePath + if s.config.Debug { + s.agentLifecycleLog.Debug("Worktree-per-agent mode enabled", + "agent_id", in.AgentID, + "workspace", result.WorktreePath, + "project_root", result.ProjectRoot) + } + return true +} + +// projectProvisionMutex returns the per-project mutex for serializing worktree +// provisioning. Uses ProjectID as key, falling back to ProjectPath if empty. +func (s *Server) projectProvisionMutex(projectID, projectPath string) *sync.Mutex { + key := projectID + if key == "" { + key = projectPath + } + actual, _ := s.projectProvisionMu.LoadOrStore(key, &sync.Mutex{}) + return actual.(*sync.Mutex) +} + +// worktreeProvisionInput holds the fields needed to decide whether to +// provision a worktree and to build the ProvisionInput. Factored out +// for testability (no Server dependency). +type worktreeProvisionInput struct { + WorkspaceMode string + GitClone *api.GitCloneConfig + ProjectPath string + ProjectID string + ProjectSlug string + AgentID string + AgentName string + Branch string + + // eligibilityOverride, when non-nil, replaces the runtime.WorktreeModeEligible + // check. Used in tests to simulate git-too-old without requiring a specific + // git binary. + eligibilityOverride func() (bool, string) +} + +// worktreeProvisionResult holds the outcome of resolveWorktreeProvision. +type worktreeProvisionResult struct { + ShouldProvision bool + Reason string + ProvisionInput provision.ProvisionInput + WorktreePath string + ProjectRoot string +} + +// resolveWorktreeProvision is the pure decision function: given the dispatch +// inputs, it determines whether worktree provisioning should proceed and +// builds the ProvisionInput. It checks the git-version gate and resolves +// the workspace backend. No side effects — all provisioning happens in the +// caller. +func resolveWorktreeProvision(in worktreeProvisionInput) worktreeProvisionResult { + if in.WorkspaceMode != store.WorkspaceModeWorktreePerAgent { + return worktreeProvisionResult{Reason: "workspace mode is not worktree-per-agent"} + } + if in.GitClone == nil { + return worktreeProvisionResult{Reason: "project is not git-backed"} + } + + eligCheck := runtime.WorktreeModeEligible + if in.eligibilityOverride != nil { + eligCheck = in.eligibilityOverride + } + eligible, reason := eligCheck() + if !eligible { + return worktreeProvisionResult{Reason: reason} + } + + mode := store.SharingModeWorktreePerAgent + backend := runtime.SelectWorkspaceBackend(nil, mode) + resolved, err := backend.Resolve(runtime.ResolveInput{ + ProjectDir: in.ProjectPath, + ProjectID: in.ProjectID, + AgentID: in.AgentID, + Mode: mode, + }) + if err != nil { + return worktreeProvisionResult{Reason: "backend resolve failed: " + err.Error()} + } + + agentName := in.AgentName + if in.Branch != "" { + agentName = in.Branch + } + if agentName == "" { + agentName = in.AgentID + } + + worktreePath := provision.WorktreePath(resolved.HostPath, in.AgentID) + + // Copy GitClone config so we don't mutate the shared pointer, and force a + // full clone (Depth -1 ≡ no --depth flag). The shared base needs full + // history for coordinator merges, git log, and git blame (design §4.2a). + gcCopy := *in.GitClone + gcCopy.Depth = -1 + + return worktreeProvisionResult{ + ShouldProvision: true, + ProvisionInput: provision.ProvisionInput{ + Resolved: resolved, + Mode: mode, + GitClone: &gcCopy, + ProjectID: in.ProjectID, + AgentID: in.AgentID, + AgentName: agentName, + Locker: nil, + }, + WorktreePath: worktreePath, + ProjectRoot: resolved.HostPath, + } +} + // hasWorkspaceContent returns true if dir exists and contains meaningful // workspace files beyond just infrastructure directories. func hasWorkspaceContent(dir string) bool { diff --git a/pkg/runtimebroker/start_context_test.go b/pkg/runtimebroker/start_context_test.go index bf8cd1c58..c116b9cd0 100644 --- a/pkg/runtimebroker/start_context_test.go +++ b/pkg/runtimebroker/start_context_test.go @@ -18,12 +18,17 @@ import ( "context" "net/http/httptest" "os" + "os/exec" "path/filepath" + "strings" "testing" "github.com/GoogleCloudPlatform/scion/pkg/api" "github.com/GoogleCloudPlatform/scion/pkg/config" + "github.com/GoogleCloudPlatform/scion/pkg/provision" "github.com/GoogleCloudPlatform/scion/pkg/runtime" + "github.com/GoogleCloudPlatform/scion/pkg/store" + "github.com/GoogleCloudPlatform/scion/pkg/util" ) func newTestServerForStartContext(t *testing.T, cfg ServerConfig) *Server { @@ -674,3 +679,349 @@ func TestBuildStartContext_GCPMetadataPassthroughFromResolvedEnv(t *testing.T) { t.Errorf("expected no GCE_METADATA_HOST for passthrough, got %q", sc.Opts.Env["GCE_METADATA_HOST"]) } } + +// --- resolveWorktreeProvision tests --- + +func TestResolveWorktreeProvision_Eligible(t *testing.T) { + projectDir := t.TempDir() + + result := resolveWorktreeProvision(worktreeProvisionInput{ + WorkspaceMode: store.WorkspaceModeWorktreePerAgent, + GitClone: &api.GitCloneConfig{ + URL: "https://github.com/org/repo.git", + Branch: "main", + Depth: 1, + }, + ProjectPath: projectDir, + ProjectID: "proj-1", + ProjectSlug: "my-project", + AgentID: "agent-1", + AgentName: "test-agent", + }) + + eligible, _ := runtime.WorktreeModeEligible() + if !eligible { + if result.ShouldProvision { + t.Fatal("expected ShouldProvision=false when git is too old") + } + t.Skip("git < 2.47, worktree mode not eligible on this host") + } + + if !result.ShouldProvision { + t.Fatalf("expected ShouldProvision=true, got false (reason: %s)", result.Reason) + } + + expectedPath := filepath.Join(projectDir, "workspace", "worktrees", "agent-1") + if result.WorktreePath != expectedPath { + t.Errorf("expected WorktreePath=%q, got %q", expectedPath, result.WorktreePath) + } + + expectedRoot := filepath.Join(projectDir, "workspace") + if result.ProjectRoot != expectedRoot { + t.Errorf("expected ProjectRoot=%q, got %q", expectedRoot, result.ProjectRoot) + } + + pi := result.ProvisionInput + if pi.Mode != store.SharingModeWorktreePerAgent { + t.Errorf("expected Mode=worktree-per-agent, got %v", pi.Mode) + } + if pi.ProjectID != "proj-1" { + t.Errorf("expected ProjectID='proj-1', got %q", pi.ProjectID) + } + if pi.AgentID != "agent-1" { + t.Errorf("expected AgentID='agent-1', got %q", pi.AgentID) + } + if pi.GitClone == nil || pi.GitClone.URL != "https://github.com/org/repo.git" { + t.Errorf("expected GitClone.URL set, got %v", pi.GitClone) + } + if pi.Locker != nil { + t.Error("expected Locker=nil for node-local single-broker") + } +} + +func TestResolveWorktreeProvision_BranchOverridesAgentName(t *testing.T) { + projectDir := t.TempDir() + eligible, _ := runtime.WorktreeModeEligible() + if !eligible { + t.Skip("git < 2.47, worktree mode not eligible on this host") + } + + result := resolveWorktreeProvision(worktreeProvisionInput{ + WorkspaceMode: store.WorkspaceModeWorktreePerAgent, + GitClone: &api.GitCloneConfig{URL: "https://example.com/repo.git"}, + ProjectPath: projectDir, + ProjectID: "proj-1", + AgentID: "agent-1", + AgentName: "test-agent", + Branch: "feature-branch", + }) + + if !result.ShouldProvision { + t.Fatalf("expected ShouldProvision=true, reason: %s", result.Reason) + } + if result.ProvisionInput.AgentName != "feature-branch" { + t.Errorf("expected AgentName='feature-branch' (from Branch), got %q", result.ProvisionInput.AgentName) + } +} + +func TestResolveWorktreeProvision_WrongMode(t *testing.T) { + result := resolveWorktreeProvision(worktreeProvisionInput{ + WorkspaceMode: store.WorkspaceModePerAgent, + GitClone: &api.GitCloneConfig{URL: "https://example.com/repo.git"}, + ProjectPath: "/some/path", + ProjectID: "proj-1", + AgentID: "agent-1", + }) + + if result.ShouldProvision { + t.Fatal("expected ShouldProvision=false for non-worktree mode") + } + if !strings.Contains(result.Reason, "not worktree-per-agent") { + t.Errorf("expected reason to mention mode mismatch, got %q", result.Reason) + } +} + +func TestResolveWorktreeProvision_NoGitClone(t *testing.T) { + result := resolveWorktreeProvision(worktreeProvisionInput{ + WorkspaceMode: store.WorkspaceModeWorktreePerAgent, + GitClone: nil, + ProjectPath: "/some/path", + ProjectID: "proj-1", + AgentID: "agent-1", + }) + + if result.ShouldProvision { + t.Fatal("expected ShouldProvision=false when GitClone is nil") + } + if !strings.Contains(result.Reason, "not git-backed") { + t.Errorf("expected reason to mention non-git, got %q", result.Reason) + } +} + +func TestResolveWorktreeProvision_GitTooOld_Fallback(t *testing.T) { + projectDir := t.TempDir() + + result := resolveWorktreeProvision(worktreeProvisionInput{ + WorkspaceMode: store.WorkspaceModeWorktreePerAgent, + GitClone: &api.GitCloneConfig{ + URL: "https://github.com/org/repo.git", + Branch: "main", + Depth: 1, + }, + ProjectPath: projectDir, + ProjectID: "proj-1", + ProjectSlug: "my-project", + AgentID: "agent-1", + AgentName: "test-agent", + eligibilityOverride: func() (bool, string) { + return false, "git >= 2.47.0 required for worktree-per-agent mode (--relative-paths), found 2.39.0" + }, + }) + + if result.ShouldProvision { + t.Fatal("expected ShouldProvision=false when git is too old") + } + if !strings.Contains(result.Reason, "2.47") { + t.Errorf("expected reason to mention git 2.47 requirement, got %q", result.Reason) + } + if result.ProvisionInput.ProjectID != "" { + t.Error("expected empty ProvisionInput when ineligible") + } +} + +func TestResolveWorktreeProvision_FullCloneDepth(t *testing.T) { + eligible, _ := runtime.WorktreeModeEligible() + if !eligible { + t.Skip("git < 2.47, worktree mode not eligible on this host") + } + + projectDir := t.TempDir() + originalGC := &api.GitCloneConfig{ + URL: "https://github.com/org/repo.git", + Branch: "main", + Depth: 1, + } + + result := resolveWorktreeProvision(worktreeProvisionInput{ + WorkspaceMode: store.WorkspaceModeWorktreePerAgent, + GitClone: originalGC, + ProjectPath: projectDir, + ProjectID: "proj-1", + ProjectSlug: "my-project", + AgentID: "agent-1", + }) + + if !result.ShouldProvision { + t.Fatalf("expected ShouldProvision=true, reason: %s", result.Reason) + } + + if result.ProvisionInput.GitClone.Depth != -1 { + t.Errorf("expected GitClone.Depth=-1 (full clone), got %d", result.ProvisionInput.GitClone.Depth) + } + + if originalGC.Depth != 1 { + t.Errorf("original GitClone.Depth was mutated: got %d, want 1", originalGC.Depth) + } +} + +// initBareRepoWithCommit creates a bare git repo (default branch main) seeded +// with one commit, and returns its path for use as a GitClone URL. +func initBareRepoWithCommit(t *testing.T) string { + t.Helper() + dir := t.TempDir() + bare := filepath.Join(dir, "remote.git") + wc := filepath.Join(dir, "wc") + run := func(args ...string) { + cmd := exec.Command("git", args...) + cmd.Env = append(os.Environ(), + "GIT_AUTHOR_NAME=t", "GIT_AUTHOR_EMAIL=t@t", + "GIT_COMMITTER_NAME=t", "GIT_COMMITTER_EMAIL=t@t") + if out, err := cmd.CombinedOutput(); err != nil { + t.Fatalf("git %v: %s", args, strings.TrimSpace(string(out))) + } + } + run("init", "--bare", "-b", "main", bare) + run("clone", bare, wc) + if err := os.WriteFile(filepath.Join(wc, "README.md"), []byte("x\n"), 0o644); err != nil { + t.Fatal(err) + } + run("-C", wc, "add", "-A") + run("-C", wc, "commit", "-m", "init") + run("-C", wc, "push", "origin", "main") + return bare +} + +// TestTryProvisionWorktree_FailureDoesNotDeleteSharedBase is the regression for +// the critical review finding on upstream PR #350: when provisioning fails for +// one agent, only that agent's worktree may be removed — never the shared base +// clone (Resolved.HostPath), which holds the common .git and every sibling +// agent's worktree. Deleting it would destroy all other agents' workspaces. +func TestTryProvisionWorktree_FailureDoesNotDeleteSharedBase(t *testing.T) { + t.Setenv("SCION_HOST_UID", "") + + cfg := DefaultServerConfig() + cfg.StateDir = t.TempDir() + srv := newTestServerForStartContext(t, cfg) + + bare := initBareRepoWithCommit(t) + gc := &api.GitCloneConfig{URL: bare, Branch: "main", Depth: 0} + + projectPath := filepath.Join(t.TempDir(), "proj") + if err := os.MkdirAll(projectPath, 0o755); err != nil { + t.Fatal(err) + } + + // Set up the shared base + a sibling worktree owning branch "agent-a". + resolved, err := runtime.NewLocalBackend().Resolve(runtime.ResolveInput{ + ProjectDir: projectPath, ProjectID: "p1", AgentID: "agent-a", + Mode: store.SharingModeWorktreePerAgent, + }) + if err != nil { + t.Fatalf("resolve: %v", err) + } + if err := provision.ProvisionShared(provision.ProvisionInput{ + Resolved: resolved, Mode: store.SharingModeWorktreePerAgent, + ProjectID: "p1", AgentID: "agent-a", AgentName: "agent-a", GitClone: gc, + }); err != nil { + t.Fatalf("setup agent-a: %v", err) + } + base := resolved.HostPath + siblingWt := provision.WorktreePath(base, "agent-a") + if _, err := os.Stat(filepath.Join(base, ".git")); err != nil { + t.Fatalf("base .git missing after setup: %v", err) + } + if _, err := os.Stat(siblingWt); err != nil { + t.Fatalf("sibling worktree missing after setup: %v", err) + } + + // Provision agent-b with a branch colliding with agent-a's → ensureWorktree + // fails ("already checked out"), exercising the cleanup-on-failure path. + opts := &api.StartOptions{} + ok := srv.tryProvisionWorktree(context.Background(), startContextInputs{ + Name: "agent-b", AgentID: "agent-b", + ProjectID: "p1", ProjectSlug: "proj", ProjectPath: projectPath, + WorkspaceMode: store.WorkspaceModeWorktreePerAgent, + Config: &CreateAgentConfig{GitClone: gc, Branch: "agent-a"}, + }, opts, map[string]string{}) + + if ok { + t.Fatal("expected provisioning to fail (branch collision) and fall back, got ok=true") + } + + // CRITICAL: the shared base and the sibling worktree must survive. + if _, err := os.Stat(filepath.Join(base, ".git")); err != nil { + t.Errorf("shared base .git was destroyed by a failed sibling provision: %v", err) + } + if _, err := os.Stat(siblingWt); err != nil { + t.Errorf("sibling agent-a worktree was destroyed by a failed sibling provision: %v", err) + } + // Only the failing agent's partial worktree should be cleaned up. + if _, err := os.Stat(provision.WorktreePath(base, "agent-b")); !os.IsNotExist(err) { + t.Errorf("agent-b partial worktree should have been cleaned up, stat err=%v", err) + } +} + +// TestWorktreeWorkspace_RepoRootDerivesToBase validates that the container +// dual-mount inputs resolve correctly for the worktree layout WITHOUT any +// explicit opts.RepoRoot (api.StartOptions has no such field). pkg/agent/run.go +// derives repoRoot from the workspace itself: IsGitRepoDir(worktree) is true +// (git rev-parse --is-inside-work-tree works through the worktree .git pointer +// file), and GetCommonGitDir(worktree) returns the SHARED base .git, so +// repoRoot = filepath.Dir(commonDir) == the base checkout. The worktree then +// sits at /worktrees/, giving a non-".." relative path that triggers +// common.go's .git + worktree dual-mount. (Regression guard for the #350 review +// claim that opts.RepoRoot must be set explicitly.) +func TestWorktreeWorkspace_RepoRootDerivesToBase(t *testing.T) { + t.Setenv("SCION_HOST_UID", "") + + bare := initBareRepoWithCommit(t) + gc := &api.GitCloneConfig{URL: bare, Branch: "main", Depth: 0} + + projectPath := filepath.Join(t.TempDir(), "proj") + if err := os.MkdirAll(projectPath, 0o755); err != nil { + t.Fatal(err) + } + + resolved, err := runtime.NewLocalBackend().Resolve(runtime.ResolveInput{ + ProjectDir: projectPath, ProjectID: "p1", AgentID: "agent-a", + Mode: store.SharingModeWorktreePerAgent, + }) + if err != nil { + t.Fatalf("resolve: %v", err) + } + if err := provision.ProvisionShared(provision.ProvisionInput{ + Resolved: resolved, Mode: store.SharingModeWorktreePerAgent, + ProjectID: "p1", AgentID: "agent-a", AgentName: "agent-a", GitClone: gc, + }); err != nil { + t.Fatalf("provision: %v", err) + } + + base := resolved.HostPath // /workspace — the shared base checkout + worktree := provision.WorktreePath(base, "agent-a") + + // Replicate pkg/agent/run.go's repoRoot derivation from the workspace. + if !util.IsGitRepoDir(worktree) { + t.Fatal("IsGitRepoDir(worktree) = false; run.go would not derive repoRoot from the worktree") + } + commonDir, err := util.GetCommonGitDir(worktree) + if err != nil { + t.Fatalf("GetCommonGitDir(worktree): %v", err) + } + repoRoot := filepath.Dir(commonDir) + if repoRoot != base { + t.Errorf("derived repoRoot = %q, want base %q", repoRoot, base) + } + + // The dual-mount in common.go only fires when rel(repoRoot, workspace) is a + // non-".." subpath — confirm the worktree is nested inside the base. + rel, err := filepath.Rel(repoRoot, worktree) + if err != nil { + t.Fatalf("Rel: %v", err) + } + if rel != filepath.Join("worktrees", "agent-a") { + t.Errorf("rel(repoRoot, worktree) = %q, want %q", rel, filepath.Join("worktrees", "agent-a")) + } + if strings.HasPrefix(rel, "..") { + t.Errorf("rel %q starts with .. — common.go dual-mount would NOT fire", rel) + } +} diff --git a/pkg/runtimebroker/types.go b/pkg/runtimebroker/types.go index 4c94a785e..16f1b2037 100644 --- a/pkg/runtimebroker/types.go +++ b/pkg/runtimebroker/types.go @@ -321,6 +321,11 @@ type CreateAgentRequest struct { // Resolved by the Hub from the project record and passed to the broker // so it can provision host-side directories and inject volume mounts. SharedDirs []api.SharedDir `json:"sharedDirs,omitempty"` + + // WorkspaceMode is the resolved workspace sharing mode for the project + // (e.g. "shared", "per-agent", "worktree-per-agent"). Threaded from the + // Hub so the broker can branch dispatch without re-deriving from labels. + WorkspaceMode string `json:"workspaceMode,omitempty"` } // UnmarshalJSON implements custom unmarshaling to support legacy grove fields. @@ -512,6 +517,16 @@ type ExecRequest struct { Timeout int `json:"timeout,omitempty"` // Timeout in seconds } +// ResetAuthRequest is the request body for resetting auth on a running agent. +type ResetAuthRequest struct { + Token string `json:"token"` +} + +// ResetAuthResponse is the response for auth reset. +type ResetAuthResponse struct { + Message string `json:"message"` +} + // ExecResponse is the response for command execution. type ExecResponse struct { Output string `json:"output"` diff --git a/pkg/runtimebroker/types_test.go b/pkg/runtimebroker/types_test.go index 3f157f29f..247e92eb3 100644 --- a/pkg/runtimebroker/types_test.go +++ b/pkg/runtimebroker/types_test.go @@ -420,3 +420,34 @@ func TestAgentInfoToResponseProfile(t *testing.T) { t.Errorf("Profile = %q, want %q", resp.Profile, "docker-dev") } } + +func TestCreateAgentRequest_WorkspaceMode_JSON(t *testing.T) { + req := CreateAgentRequest{ + Name: "test-agent", + ProjectID: "project-1", + WorkspaceMode: "worktree-per-agent", + } + data, err := json.Marshal(req) + if err != nil { + t.Fatalf("Marshal failed: %v", err) + } + + var decoded CreateAgentRequest + if err := json.Unmarshal(data, &decoded); err != nil { + t.Fatalf("Unmarshal failed: %v", err) + } + if decoded.WorkspaceMode != "worktree-per-agent" { + t.Errorf("WorkspaceMode = %q, want %q", decoded.WorkspaceMode, "worktree-per-agent") + } + + // Verify omitempty: field should be absent when empty + req2 := CreateAgentRequest{Name: "agent-no-mode"} + data2, _ := json.Marshal(req2) + var m map[string]interface{} + if err := json.Unmarshal(data2, &m); err != nil { + t.Fatalf("Unmarshal map failed: %v", err) + } + if _, exists := m["workspaceMode"]; exists { + t.Error("workspaceMode should be omitted when empty") + } +} diff --git a/pkg/sciontool/hooks/handlers/hub_test.go b/pkg/sciontool/hooks/handlers/hub_test.go index 1bd5da5e7..817cd31a7 100644 --- a/pkg/sciontool/hooks/handlers/hub_test.go +++ b/pkg/sciontool/hooks/handlers/hub_test.go @@ -16,6 +16,22 @@ import ( "github.com/GoogleCloudPlatform/scion/pkg/sciontool/hooks" ) +// scrubHubEnv clears all Hub-related environment variables for the +// duration of the test, preventing accidental communication with a +// real Hub when tests run inside an agent container. See issue #123. +func scrubHubEnv(t *testing.T) { + t.Helper() + for _, key := range []string{ + "SCION_HUB_ENDPOINT", + "SCION_HUB_URL", + "SCION_AUTH_TOKEN", + "SCION_AGENT_ID", + "SCION_AGENT_MODE", + } { + t.Setenv(key, "") + } +} + // TestHubHandler_EventMapping tests that events are correctly mapped to Hub status updates. func TestHubHandler_EventMapping(t *testing.T) { tests := []struct { @@ -90,9 +106,7 @@ func TestHubHandler_EventMapping(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { tmpHome := t.TempDir() - oldHome := os.Getenv("HOME") - os.Setenv("HOME", tmpHome) - defer os.Setenv("HOME", oldHome) + t.Setenv("HOME", tmpHome) var receivedStatus string var mu sync.Mutex @@ -121,16 +135,11 @@ func TestHubHandler_EventMapping(t *testing.T) { })) defer server.Close() - // Set environment variables for the Hub client - os.Setenv("SCION_HUB_ENDPOINT", server.URL) - os.Setenv("SCION_AUTH_TOKEN", "test-token") - os.Setenv("SCION_AGENT_ID", "test-agent-id") - defer func() { - os.Unsetenv("SCION_HUB_ENDPOINT") - os.Unsetenv("SCION_HUB_URL") - os.Unsetenv("SCION_AUTH_TOKEN") - os.Unsetenv("SCION_AGENT_ID") - }() + // Clear real Hub env, then point at the test server (issue #123). + scrubHubEnv(t) + t.Setenv("SCION_HUB_ENDPOINT", server.URL) + t.Setenv("SCION_AUTH_TOKEN", "test-token") + t.Setenv("SCION_AGENT_ID", "test-agent-id") // Create handler handler := NewHubHandler() @@ -172,11 +181,8 @@ func TestHubHandler_EventMapping(t *testing.T) { // TestHubHandler_NotConfigured tests that nil handler doesn't panic. func TestHubHandler_NotConfigured(t *testing.T) { - // Clear environment to ensure client is not configured - os.Unsetenv("SCION_HUB_ENDPOINT") - os.Unsetenv("SCION_HUB_URL") - os.Unsetenv("SCION_AUTH_TOKEN") - os.Unsetenv("SCION_AGENT_ID") + // Clear environment to ensure client is not configured (issue #123). + scrubHubEnv(t) handler := NewHubHandler() if handler != nil { @@ -206,15 +212,11 @@ func TestHubHandler_ReportMethods(t *testing.T) { })) defer server.Close() - os.Setenv("SCION_HUB_ENDPOINT", server.URL) - os.Setenv("SCION_AUTH_TOKEN", "test-token") - os.Setenv("SCION_AGENT_ID", "test-agent-id") - defer func() { - os.Unsetenv("SCION_HUB_ENDPOINT") - os.Unsetenv("SCION_HUB_URL") - os.Unsetenv("SCION_AUTH_TOKEN") - os.Unsetenv("SCION_AGENT_ID") - }() + // Clear real Hub env, then point at the test server (issue #123). + scrubHubEnv(t) + t.Setenv("SCION_HUB_ENDPOINT", server.URL) + t.Setenv("SCION_AUTH_TOKEN", "test-token") + t.Setenv("SCION_AGENT_ID", "test-agent-id") handler := NewHubHandler() if handler == nil { @@ -377,9 +379,7 @@ func TestHubHandler_StickyStatus(t *testing.T) { os.WriteFile(tmpDir+"/agent-info.json", data, 0644) // Point HOME to the temp dir so readLocalActivity finds our file - origHome := os.Getenv("HOME") - os.Setenv("HOME", tmpDir) - defer os.Setenv("HOME", origHome) + t.Setenv("HOME", tmpDir) var mu sync.Mutex callCount := 0 @@ -400,15 +400,11 @@ func TestHubHandler_StickyStatus(t *testing.T) { })) defer server.Close() - os.Setenv("SCION_HUB_ENDPOINT", server.URL) - os.Setenv("SCION_AUTH_TOKEN", "test-token") - os.Setenv("SCION_AGENT_ID", "test-agent-id") - defer func() { - os.Unsetenv("SCION_HUB_ENDPOINT") - os.Unsetenv("SCION_HUB_URL") - os.Unsetenv("SCION_AUTH_TOKEN") - os.Unsetenv("SCION_AGENT_ID") - }() + // Clear real Hub env, then point at the test server (issue #123). + scrubHubEnv(t) + t.Setenv("SCION_HUB_ENDPOINT", server.URL) + t.Setenv("SCION_AUTH_TOKEN", "test-token") + t.Setenv("SCION_AGENT_ID", "test-agent-id") handler := NewHubHandler() if handler == nil { @@ -447,11 +443,8 @@ func TestHubHandler_StickyStatus(t *testing.T) { // TestHubHandler_ModeBehavior verifies behavior differences between local and hub modes. func TestHubHandler_ModeBehavior(t *testing.T) { t.Run("local mode: HubHandler is nil", func(t *testing.T) { - // Clear hub env vars to simulate local mode - os.Unsetenv("SCION_HUB_ENDPOINT") - os.Unsetenv("SCION_HUB_URL") - os.Unsetenv("SCION_AUTH_TOKEN") - os.Unsetenv("SCION_AGENT_ID") + // Clear hub env vars to simulate local mode (issue #123). + scrubHubEnv(t) handler := NewHubHandler() if handler != nil { @@ -463,15 +456,10 @@ func TestHubHandler_ModeBehavior(t *testing.T) { // Even without a hub, the StatusHandler must write to agent-info.json // for local observability (defense-in-depth). tmpHome := t.TempDir() - origHome := os.Getenv("HOME") - os.Setenv("HOME", tmpHome) - defer os.Setenv("HOME", origHome) + t.Setenv("HOME", tmpHome) - // Clear hub env to ensure local mode - os.Unsetenv("SCION_HUB_ENDPOINT") - os.Unsetenv("SCION_HUB_URL") - os.Unsetenv("SCION_AUTH_TOKEN") - os.Unsetenv("SCION_AGENT_ID") + // Clear hub env to ensure local mode (issue #123). + scrubHubEnv(t) statusHandler := NewStatusHandler() event := &hooks.Event{ @@ -497,9 +485,7 @@ func TestHubHandler_ModeBehavior(t *testing.T) { t.Run("hub mode: HubHandler is active and sends updates", func(t *testing.T) { tmpHome := t.TempDir() - origHome := os.Getenv("HOME") - os.Setenv("HOME", tmpHome) - defer os.Setenv("HOME", origHome) + t.Setenv("HOME", tmpHome) callCount := 0 var mu sync.Mutex @@ -513,14 +499,11 @@ func TestHubHandler_ModeBehavior(t *testing.T) { })) defer server.Close() - os.Setenv("SCION_HUB_ENDPOINT", server.URL) - os.Setenv("SCION_AUTH_TOKEN", "test-token") - os.Setenv("SCION_AGENT_ID", "test-agent") - defer func() { - os.Unsetenv("SCION_HUB_ENDPOINT") - os.Unsetenv("SCION_AUTH_TOKEN") - os.Unsetenv("SCION_AGENT_ID") - }() + // Clear real Hub env, then point at the test server (issue #123). + scrubHubEnv(t) + t.Setenv("SCION_HUB_ENDPOINT", server.URL) + t.Setenv("SCION_AUTH_TOKEN", "test-token") + t.Setenv("SCION_AGENT_ID", "test-agent") handler := NewHubHandler() if handler == nil { @@ -546,9 +529,7 @@ func TestHubHandler_ModeBehavior(t *testing.T) { t.Run("hub mode: StatusHandler still writes agent-info.json", func(t *testing.T) { // In hub mode, StatusHandler should still write locally for defense-in-depth. tmpHome := t.TempDir() - origHome := os.Getenv("HOME") - os.Setenv("HOME", tmpHome) - defer os.Setenv("HOME", origHome) + t.Setenv("HOME", tmpHome) server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusOK) @@ -556,14 +537,11 @@ func TestHubHandler_ModeBehavior(t *testing.T) { })) defer server.Close() - os.Setenv("SCION_HUB_ENDPOINT", server.URL) - os.Setenv("SCION_AUTH_TOKEN", "test-token") - os.Setenv("SCION_AGENT_ID", "test-agent") - defer func() { - os.Unsetenv("SCION_HUB_ENDPOINT") - os.Unsetenv("SCION_AUTH_TOKEN") - os.Unsetenv("SCION_AGENT_ID") - }() + // Clear real Hub env, then point at the test server (issue #123). + scrubHubEnv(t) + t.Setenv("SCION_HUB_ENDPOINT", server.URL) + t.Setenv("SCION_AUTH_TOKEN", "test-token") + t.Setenv("SCION_AGENT_ID", "test-agent") statusHandler := NewStatusHandler() event := &hooks.Event{ @@ -594,9 +572,7 @@ func TestHubHandler_ModeBehavior(t *testing.T) { func TestHubHandler_AssistantTextForwarding(t *testing.T) { t.Run("forwards assistant text to outbound-message endpoint", func(t *testing.T) { tmpHome := t.TempDir() - origHome := os.Getenv("HOME") - os.Setenv("HOME", tmpHome) - defer os.Setenv("HOME", origHome) + t.Setenv("HOME", tmpHome) var mu sync.Mutex var outboundMsg string @@ -624,15 +600,11 @@ func TestHubHandler_AssistantTextForwarding(t *testing.T) { })) defer server.Close() - os.Setenv("SCION_HUB_ENDPOINT", server.URL) - os.Setenv("SCION_AUTH_TOKEN", "test-token") - os.Setenv("SCION_AGENT_ID", "test-agent-id") - defer func() { - os.Unsetenv("SCION_HUB_ENDPOINT") - os.Unsetenv("SCION_HUB_URL") - os.Unsetenv("SCION_AUTH_TOKEN") - os.Unsetenv("SCION_AGENT_ID") - }() + // Clear real Hub env, then point at the test server (issue #123). + scrubHubEnv(t) + t.Setenv("SCION_HUB_ENDPOINT", server.URL) + t.Setenv("SCION_AUTH_TOKEN", "test-token") + t.Setenv("SCION_AGENT_ID", "test-agent-id") handler := NewHubHandler() if handler == nil { @@ -662,9 +634,7 @@ func TestHubHandler_AssistantTextForwarding(t *testing.T) { t.Run("truncates assistant text exceeding 64KB", func(t *testing.T) { tmpHome := t.TempDir() - origHome := os.Getenv("HOME") - os.Setenv("HOME", tmpHome) - defer os.Setenv("HOME", origHome) + t.Setenv("HOME", tmpHome) var mu sync.Mutex var outboundMsg string @@ -685,15 +655,11 @@ func TestHubHandler_AssistantTextForwarding(t *testing.T) { })) defer server.Close() - os.Setenv("SCION_HUB_ENDPOINT", server.URL) - os.Setenv("SCION_AUTH_TOKEN", "test-token") - os.Setenv("SCION_AGENT_ID", "test-agent-id") - defer func() { - os.Unsetenv("SCION_HUB_ENDPOINT") - os.Unsetenv("SCION_HUB_URL") - os.Unsetenv("SCION_AUTH_TOKEN") - os.Unsetenv("SCION_AGENT_ID") - }() + // Clear real Hub env, then point at the test server (issue #123). + scrubHubEnv(t) + t.Setenv("SCION_HUB_ENDPOINT", server.URL) + t.Setenv("SCION_AUTH_TOKEN", "test-token") + t.Setenv("SCION_AGENT_ID", "test-agent-id") handler := NewHubHandler() if handler == nil { @@ -734,9 +700,7 @@ func TestHubHandler_AssistantTextForwarding(t *testing.T) { func TestHubHandler_AssistantTextVisibilityTagging(t *testing.T) { t.Run("tags outbound message with verbose visibility", func(t *testing.T) { tmpHome := t.TempDir() - origHome := os.Getenv("HOME") - os.Setenv("HOME", tmpHome) - defer os.Setenv("HOME", origHome) + t.Setenv("HOME", tmpHome) var mu sync.Mutex var outboundPayload map[string]interface{} @@ -757,15 +721,11 @@ func TestHubHandler_AssistantTextVisibilityTagging(t *testing.T) { })) defer server.Close() - os.Setenv("SCION_HUB_ENDPOINT", server.URL) - os.Setenv("SCION_AUTH_TOKEN", "test-token") - os.Setenv("SCION_AGENT_ID", "test-agent-id") - defer func() { - os.Unsetenv("SCION_HUB_ENDPOINT") - os.Unsetenv("SCION_HUB_URL") - os.Unsetenv("SCION_AUTH_TOKEN") - os.Unsetenv("SCION_AGENT_ID") - }() + // Clear real Hub env, then point at the test server (issue #123). + scrubHubEnv(t) + t.Setenv("SCION_HUB_ENDPOINT", server.URL) + t.Setenv("SCION_AUTH_TOKEN", "test-token") + t.Setenv("SCION_AGENT_ID", "test-agent-id") handler := NewHubHandler() if handler == nil { @@ -800,9 +760,7 @@ func TestHubHandler_AssistantTextVisibilityTagging(t *testing.T) { t.Run("sets has_thinking metadata when thinking content was filtered", func(t *testing.T) { tmpHome := t.TempDir() - origHome := os.Getenv("HOME") - os.Setenv("HOME", tmpHome) - defer os.Setenv("HOME", origHome) + t.Setenv("HOME", tmpHome) var mu sync.Mutex var outboundPayload map[string]interface{} @@ -823,15 +781,11 @@ func TestHubHandler_AssistantTextVisibilityTagging(t *testing.T) { })) defer server.Close() - os.Setenv("SCION_HUB_ENDPOINT", server.URL) - os.Setenv("SCION_AUTH_TOKEN", "test-token") - os.Setenv("SCION_AGENT_ID", "test-agent-id") - defer func() { - os.Unsetenv("SCION_HUB_ENDPOINT") - os.Unsetenv("SCION_HUB_URL") - os.Unsetenv("SCION_AUTH_TOKEN") - os.Unsetenv("SCION_AGENT_ID") - }() + // Clear real Hub env, then point at the test server (issue #123). + scrubHubEnv(t) + t.Setenv("SCION_HUB_ENDPOINT", server.URL) + t.Setenv("SCION_AUTH_TOKEN", "test-token") + t.Setenv("SCION_AGENT_ID", "test-agent-id") handler := NewHubHandler() if handler == nil { diff --git a/pkg/sciontool/hooks/handlers/limits_test.go b/pkg/sciontool/hooks/handlers/limits_test.go index 76b899eae..10f523cc2 100644 --- a/pkg/sciontool/hooks/handlers/limits_test.go +++ b/pkg/sciontool/hooks/handlers/limits_test.go @@ -16,6 +16,7 @@ import ( ) func TestInitLimitsFile(t *testing.T) { + scrubHubEnv(t) tmpDir := t.TempDir() limitsPath := filepath.Join(tmpDir, "agent-limits.json") @@ -37,6 +38,7 @@ func TestInitLimitsFile(t *testing.T) { } func TestInitLimitsFile_ZeroValues(t *testing.T) { + scrubHubEnv(t) tmpDir := t.TempDir() limitsPath := filepath.Join(tmpDir, "agent-limits.json") @@ -55,6 +57,7 @@ func TestInitLimitsFile_ZeroValues(t *testing.T) { } func TestLimitsHandler_TurnCounting(t *testing.T) { + scrubHubEnv(t) tmpDir := t.TempDir() limitsPath := filepath.Join(tmpDir, "agent-limits.json") @@ -82,6 +85,7 @@ func TestLimitsHandler_TurnCounting(t *testing.T) { } func TestLimitsHandler_ModelCallCounting(t *testing.T) { + scrubHubEnv(t) tmpDir := t.TempDir() limitsPath := filepath.Join(tmpDir, "agent-limits.json") @@ -107,6 +111,7 @@ func TestLimitsHandler_ModelCallCounting(t *testing.T) { } func TestLimitsHandler_IgnoresIrrelevantEvents(t *testing.T) { + scrubHubEnv(t) tmpDir := t.TempDir() limitsPath := filepath.Join(tmpDir, "agent-limits.json") @@ -140,12 +145,14 @@ func TestLimitsHandler_IgnoresIrrelevantEvents(t *testing.T) { } func TestLimitsHandler_NilHandler(t *testing.T) { + scrubHubEnv(t) var h *LimitsHandler err := h.Handle(&hooks.Event{Name: hooks.EventAgentEnd}) assert.NoError(t, err) } func TestLimitsHandler_NoLimitsConfigured(t *testing.T) { + scrubHubEnv(t) // When maxTurns=0 and maxModelCalls=0, events are silently ignored tmpDir := t.TempDir() limitsPath := filepath.Join(tmpDir, "agent-limits.json") @@ -174,6 +181,7 @@ func TestLimitsHandler_NoLimitsConfigured(t *testing.T) { } func TestLimitsHandler_TurnLimitDetection(t *testing.T) { + scrubHubEnv(t) // Test that the handler detects when the turn limit is reached. // We can't test the SIGUSR1 signal in unit tests (it would kill the test process), // so we verify the status file is updated and the limits file has the right count. @@ -216,6 +224,7 @@ func TestLimitsHandler_TurnLimitDetection(t *testing.T) { } func TestLimitsHandler_ModelCallLimitDetection(t *testing.T) { + scrubHubEnv(t) tmpDir := t.TempDir() limitsPath := filepath.Join(tmpDir, "agent-limits.json") statusPath := filepath.Join(tmpDir, "agent-info.json") @@ -250,6 +259,7 @@ func TestLimitsHandler_ModelCallLimitDetection(t *testing.T) { } func TestLimitsHandler_BothLimitsIndependent(t *testing.T) { + scrubHubEnv(t) // Verify both counters are tracked independently tmpDir := t.TempDir() limitsPath := filepath.Join(tmpDir, "agent-limits.json") @@ -282,6 +292,7 @@ func TestLimitsHandler_BothLimitsIndependent(t *testing.T) { } func TestLimitsHandler_MissingLimitsFile(t *testing.T) { + scrubHubEnv(t) tmpDir := t.TempDir() limitsPath := filepath.Join(tmpDir, "nonexistent", "agent-limits.json") @@ -347,6 +358,7 @@ func TestExitCodeLimitsExceeded(t *testing.T) { } func TestWriteLimitsState_AtomicWrite(t *testing.T) { + scrubHubEnv(t) tmpDir := t.TempDir() limitsPath := filepath.Join(tmpDir, "agent-limits.json") @@ -381,6 +393,7 @@ func TestLimitsTriggerFileConstant(t *testing.T) { } func TestSignalLimitsExceeded_CreatesTriggerFile(t *testing.T) { + scrubHubEnv(t) tmpDir := t.TempDir() triggerPath := filepath.Join(tmpDir, "scion-limits-exceeded") diff --git a/pkg/sciontool/hub/client.go b/pkg/sciontool/hub/client.go index a65a7ee2e..abb541e39 100644 --- a/pkg/sciontool/hub/client.go +++ b/pkg/sciontool/hub/client.go @@ -21,6 +21,7 @@ import ( "context" "encoding/base64" "encoding/json" + "errors" "fmt" "io" "net/http" @@ -29,11 +30,20 @@ import ( "path/filepath" "strings" "sync" + "testing" "time" state "github.com/GoogleCloudPlatform/scion/pkg/agent/state" ) +// ErrTokenRefreshUnauthorized indicates the hub rejected the token refresh +// request because the presented token is no longer accepted (HTTP 401/403). +// This typically happens after a hub signing-key rotation invalidates all +// previously-issued agent JWTs. It is terminal for the current token: retrying +// with the same token can never succeed, so recovery requires a fresh token +// injected out-of-band (e.g. via the broker reset-auth path / SIGUSR2). +var ErrTokenRefreshUnauthorized = errors.New("token refresh unauthorized") + const ( // TokenFile is the canonical token file name. The SCION_AUTH_TOKEN env var // bootstraps the initial value into the container; sciontool init writes it @@ -150,6 +160,7 @@ type Client struct { maxRetries int retryBaseDelay time.Duration retryMaxDelay time.Duration + oidcSource oidcTokenSource // transport-layer OIDC token source (nil = disabled) } // NewClient creates a new Hub client from environment variables. @@ -157,6 +168,12 @@ type Client struct { // The token is read from the canonical token file (~/.scion/scion-token), falling // back to the SCION_AUTH_TOKEN env var for bootstrap (before init has run). // Returns nil if the required environment variables are not set. +// +// Defense-in-depth: when running under `go test`, refuses to create a client +// that would talk to a non-localhost hub. Tests that need a hub client must +// scrub SCION_* env vars and point at an httptest server (see scrubHubEnv +// in hub_test.go). Without this guard, a test that forgets env sandboxing +// leaks real status updates to the hub under the container's agent identity. func NewClient() *Client { hubURL := os.Getenv(EnvHubEndpoint) if hubURL == "" { @@ -164,6 +181,10 @@ func NewClient() *Client { } agentID := os.Getenv(EnvAgentID) + if testing.Testing() && !hubTestSandboxed && hubURL != "" && !isLocalhostURL(hubURL) { + return nil + } + // Prefer the canonical token file; fall back to env var for bootstrap. token := ReadTokenFile() if token == "" { @@ -174,7 +195,7 @@ func NewClient() *Client { return nil } - return &Client{ + c := &Client{ hubURL: hubURL, token: token, agentID: agentID, @@ -185,6 +206,8 @@ func NewClient() *Client { Timeout: DefaultTimeout, }, } + c.configureOIDCTransport() + return c } // NewClientWithConfig creates a new Hub client with explicit configuration. @@ -332,10 +355,23 @@ func (c *Client) ReportState(ctx context.Context, phase state.Phase, activity st }) } +// RefreshTokenEntry represents a single token in the generalized refresh response. +// Mirrors the hub's RefreshTokenEntry type. +type RefreshTokenEntry struct { + Layer string `json:"layer"` // "app" | "transport" + Type string `json:"type"` // "scion_access" | "scion_refresh" | "google_oidc" + Value string `json:"value"` // the token value + ExpiresIn int `json:"expiresIn"` // seconds until expiry + Audience string `json:"audience,omitempty"` // only for transport tokens +} + // RefreshTokenResponse is the response from the token refresh endpoint. +// Includes both legacy single-token fields (backward compat) and the +// generalized tokens[] array. type RefreshTokenResponse struct { - Token string `json:"token"` - ExpiresAt string `json:"expires_at"` + Token string `json:"token"` + ExpiresAt string `json:"expires_at"` + Tokens []RefreshTokenEntry `json:"tokens,omitempty"` } // RefreshToken calls the Hub to refresh the agent's authentication token. @@ -371,6 +407,15 @@ func (c *Client) RefreshToken(ctx context.Context) (string, time.Time, error) { respBody, _ := io.ReadAll(resp.Body) if resp.StatusCode != http.StatusOK { + // 401/403 mean the presented token is rejected (e.g. after a hub + // signing-key rotation). Tag these so the refresh loop can distinguish a + // terminal auth failure from a transient (network/5xx) one. The literal + // "token refresh failed with status %d" wording is preserved for the + // non-auth path because existing log-based tooling matches on it. + if resp.StatusCode == http.StatusUnauthorized || resp.StatusCode == http.StatusForbidden { + return "", time.Time{}, fmt.Errorf("%w: token refresh failed with status %d: %s", + ErrTokenRefreshUnauthorized, resp.StatusCode, string(respBody)) + } return "", time.Time{}, fmt.Errorf("token refresh failed with status %d: %s", resp.StatusCode, string(respBody)) } @@ -397,9 +442,58 @@ func (c *Client) RefreshToken(ctx context.Context) (string, time.Time, error) { _ = err } + // Process the generalized tokens[] array if present. + // Apply each entry to the appropriate subsystem by layer/type. + if len(result.Tokens) > 0 { + c.applyRefreshTokens(result.Tokens) + } + return result.Token, expiresAt, nil } +// applyRefreshTokens processes the tokens[] array from a refresh response, +// applying each entry to the appropriate subsystem. +func (c *Client) applyRefreshTokens(tokens []RefreshTokenEntry) { + for _, entry := range tokens { + switch { + case entry.Layer == "transport" && entry.Type == "google_oidc": + // Update the OIDC transport's token source + if c.oidcSource != nil { + entryExpiry := time.Now().Add(time.Duration(entry.ExpiresIn) * time.Second) + c.oidcSource.setToken(entry.Value, entryExpiry) + } + // app/scion_access is already handled via the legacy token field above + } + } +} + +// adjustRefreshForTransportTokens checks if the OIDC source has a shorter +// expiry than the proposed refresh time and returns the earlier of the two. +// Transport tokens (~1h) use a 5-minute refresh margin vs the app token's +// 2-hour margin. +func (c *Client) adjustRefreshForTransportTokens(proposed time.Time) time.Time { + if c.oidcSource == nil { + return proposed + } + + // Read the transport token expiry from the source + switch src := c.oidcSource.(type) { + case *injectedTokenSource: + src.mu.RLock() + expiry := src.expiresAt + src.mu.RUnlock() + if !expiry.IsZero() { + transportRefresh := expiry.Add(-oidcRefreshMargin) + if transportRefresh.Before(proposed) { + return transportRefresh + } + } + case *metadataTokenSource: + // Metadata source self-refreshes; no need to drive refresh from here. + } + return proposed +} + // TokenRefreshConfig configures the token refresh loop. type TokenRefreshConfig struct { // RefreshAt is the time at which the token should be refreshed. @@ -417,15 +511,60 @@ type TokenRefreshConfig struct { OnError func(error) // OnAuthLost is called when auth is terminally lost (token expired, cannot refresh). OnAuthLost func() + // RetryBaseDelay overrides the initial backoff between failed refresh + // attempts. Zero uses tokenRefreshRetryBaseDelay. + RetryBaseDelay time.Duration + // RetryMaxDelay overrides the cap on backoff between failed refresh attempts. + // Zero uses tokenRefreshRetryMaxDelay. + RetryMaxDelay time.Duration } // DefaultTokenRefreshTimeout is the default timeout for token refresh requests. const DefaultTokenRefreshTimeout = 30 * time.Second +const ( + // tokenRefreshRetryBaseDelay is the initial delay before retrying a failed + // token refresh. + tokenRefreshRetryBaseDelay = 30 * time.Second + // tokenRefreshRetryMaxDelay caps the backoff between failed refresh attempts. + // A persistently failing refresh (e.g. after a hub signing-key rotation that + // invalidates the current token) must not hot-loop, but should still retry + // often enough to recover promptly once the hub is healthy again or an + // out-of-band reset-auth injects a fresh token. + tokenRefreshRetryMaxDelay = 5 * time.Minute +) + +// tokenRefreshBackoff returns the delay before the next refresh retry after the +// given number of consecutive failures, using exponential backoff (starting at +// base, doubling each attempt) capped at max. +func tokenRefreshBackoff(consecutiveFailures int, base, max time.Duration) time.Duration { + if consecutiveFailures < 1 { + consecutiveFailures = 1 + } + delay := base + for i := 1; i < consecutiveFailures; i++ { + delay *= 2 + if delay >= max { + return max + } + } + if delay > max { + delay = max + } + return delay +} + // StartTokenRefresh starts a background goroutine that refreshes the agent token // before it expires. After a successful refresh, the next refresh is scheduled // based on the new token's expiry (2 hours before expiry for a 10-hour token). -// Returns a channel that will be closed when the refresh loop exits. +// +// On failure the loop retries with exponential backoff (capped at +// tokenRefreshRetryMaxDelay) instead of exiting, so the agent recovers +// automatically once the hub is healthy again or a fresh token is injected +// out-of-band (e.g. via reset-auth). When the current token has actually expired +// and refresh still fails, OnAuthLost is invoked once for observability; the loop +// keeps retrying so recovery remains possible. The loop only exits when ctx is +// cancelled. Returns a channel that is closed when the loop exits. func (c *Client) StartTokenRefresh(ctx context.Context, config *TokenRefreshConfig) <-chan struct{} { done := make(chan struct{}) @@ -434,24 +573,42 @@ func (c *Client) StartTokenRefresh(ctx context.Context, config *TokenRefreshConf timeout = config.Timeout } + retryBase := tokenRefreshRetryBaseDelay + if config != nil && config.RetryBaseDelay > 0 { + retryBase = config.RetryBaseDelay + } + retryMax := tokenRefreshRetryMaxDelay + if config != nil && config.RetryMaxDelay > 0 { + retryMax = config.RetryMaxDelay + } + if retryMax < retryBase { + retryMax = retryBase + } + go func() { defer close(done) + // tokenExpiry tracks the actual expiry of the token currently held by the + // client. refreshAt (the scheduled wake time) is rewritten on every retry, + // so it cannot be used to decide when auth is terminally lost — we must + // compare against the real expiry instead. Seed it from the current token, + // falling back to the configured refresh time plus the standard 2h + // pre-expiry margin when the token is not a parseable JWT. + tokenExpiry := config.RefreshAt.Add(2 * time.Hour) + if exp, parseErr := ParseTokenExpiry(c.GetToken()); parseErr == nil { + tokenExpiry = exp + } + refreshAt := config.RefreshAt + consecutiveFailures := 0 + authLostNotified := false + for { - now := time.Now() - delay := refreshAt.Sub(now) - if delay <= 0 { - // Refresh time has already passed; try immediately + delay := time.Until(refreshAt) + if delay < 0 { delay = 0 } - - var timer *time.Timer - if delay > 0 { - timer = time.NewTimer(delay) - } else { - timer = time.NewTimer(0) // fire immediately - } + timer := time.NewTimer(delay) select { case <-ctx.Done(): @@ -469,19 +626,32 @@ func (c *Client) StartTokenRefresh(ctx context.Context, config *TokenRefreshConf config.OnError(err) } - // If the token has already expired, auth is terminally lost - if time.Now().After(refreshAt.Add(2 * time.Hour)) { + // Once the current token has actually expired and refresh still + // fails, auth is lost. Surface it once (for observability and to + // trigger out-of-band recovery such as reset-auth) — but keep + // retrying with capped backoff rather than exiting, so the agent + // self-heals if the hub recovers (e.g. its signing key is restored) + // or a fresh token is injected. The previous implementation reset + // the expiry estimate on every retry, so OnAuthLost never fired and + // the loop hot-looped every 30s indefinitely. + if !authLostNotified && !time.Now().Before(tokenExpiry) { + authLostNotified = true if config != nil && config.OnAuthLost != nil { config.OnAuthLost() } - return } - // Retry in 30 seconds - refreshAt = time.Now().Add(30 * time.Second) + consecutiveFailures++ + refreshAt = time.Now().Add(tokenRefreshBackoff(consecutiveFailures, retryBase, retryMax)) continue } + // Successful refresh: reset failure tracking and clear any prior + // auth-lost state so a later loss is reported again. + consecutiveFailures = 0 + authLostNotified = false + tokenExpiry = newExpiry + // Fix ownership after atomic rewrite (init runs as root). if config.ChownUID > 0 { if chownErr := os.Chown(TokenFilePath(), config.ChownUID, config.ChownGID); chownErr != nil { @@ -495,8 +665,13 @@ func (c *Client) StartTokenRefresh(ctx context.Context, config *TokenRefreshConf config.OnRefreshed(newExpiry) } - // Schedule next refresh: 2 hours before new expiry + // Schedule next refresh: 2 hours before new expiry for the app token. refreshAt = newExpiry.Add(-2 * time.Hour) + + // If transport tokens are present, use the shortest-lived entry + // to drive refresh timing (transport tokens ~1h need a tighter margin). + refreshAt = c.adjustRefreshForTransportTokens(refreshAt) + if refreshAt.Before(time.Now()) { // Token duration is very short; refresh in 1 minute refreshAt = time.Now().Add(1 * time.Minute) @@ -517,6 +692,14 @@ func (c *Client) GetToken() string { return c.token } +// SetToken updates the client's in-memory auth token. This is used during +// auth reset to inject a freshly-issued token without restarting the client. +func (c *Client) SetToken(token string) { + c.tokenMu.Lock() + c.token = token + c.tokenMu.Unlock() +} + // Environment variable and file path constants for GitHub App token refresh. const ( // EnvGitHubAppEnabled indicates whether GitHub App token refresh is active. @@ -818,6 +1001,22 @@ func ParseTokenExpiry(tokenString string) (time.Time, error) { return time.Unix(claims.Exp, 0), nil } +// isLocalhostURL returns true if the URL points to localhost or 127.0.0.1, +// indicating a test server rather than a real hub endpoint. +func isLocalhostURL(rawURL string) bool { + lower := strings.ToLower(rawURL) + for _, prefix := range []string{ + "http://localhost", "https://localhost", + "http://127.0.0.1", "https://127.0.0.1", + "http://[::1]", "https://[::1]", + } { + if strings.HasPrefix(lower, prefix) { + return true + } + } + return false +} + // HeartbeatConfig configures the heartbeat loop. type HeartbeatConfig struct { // Interval is the time between heartbeats. Default: 30 seconds. @@ -840,28 +1039,65 @@ const DefaultHeartbeatTimeout = 10 * time.Second // Override in tests via SetTokenHome to use a temp directory. var tokenHomeResolver = resolveTokenHome +var ( + resolvedTokenHome string + resolveTokenHomeOnce sync.Once +) + // resolveTokenHome returns the home directory to use for the token file. // Inside agent containers, sciontool init runs as root (HOME=/root) while // child processes run as the scion user (HOME=/home/scion). Both must // resolve to the same token file path — the scion user's home. +// The result is cached because user.Lookup is expensive and the home +// directory does not change at runtime. func resolveTokenHome() string { - // Prefer the scion user's home when it exists (inside containers). - if u, err := user.Lookup("scion"); err == nil && u.HomeDir != "" { - return u.HomeDir - } - home := os.Getenv("HOME") - if home == "" { - home = "/home/scion" - } - return home + resolveTokenHomeOnce.Do(func() { + if u, err := user.Lookup("scion"); err == nil && u.HomeDir != "" { + resolvedTokenHome = u.HomeDir + return + } + resolvedTokenHome = os.Getenv("HOME") + if resolvedTokenHome == "" { + resolvedTokenHome = "/home/scion" + } + }) + return resolvedTokenHome +} + +// hubTestSandboxed reports whether the calling test has explicitly declared +// that it has sandboxed the hub environment (e.g. by calling scrubHubEnv and +// setting test values). NewClient refuses to connect to a non-localhost hub +// under `go test` unless this flag is set, preventing accidental leakage of +// status updates to a real hub when tests run inside an agent container. +var hubTestSandboxed bool + +// SetHubTestSandboxed marks the current test as having properly sandboxed the +// hub environment. Call this in tests that deliberately set non-localhost hub +// URLs (e.g. for verifying URL preference logic). Returns a cleanup function. +func SetHubTestSandboxed() func() { + orig := hubTestSandboxed + hubTestSandboxed = true + return func() { hubTestSandboxed = orig } } +// tokenHomeOverridden reports whether SetTokenHome has installed a test +// override. WriteTokenFile refuses to write under `go test` unless this is set, +// so a test that forgets SetTokenHome can never clobber a live +// ~/.scion/scion-token (as happened when the suite was run inside an agent +// container, where resolveTokenHome finds the real scion user). +var tokenHomeOverridden bool + // SetTokenHome overrides the token home directory for testing. // Returns a cleanup function that restores the original resolver. func SetTokenHome(dir string) func() { orig := tokenHomeResolver + origOverridden := tokenHomeOverridden tokenHomeResolver = func() string { return dir } - return func() { tokenHomeResolver = orig } + tokenHomeOverridden = true + return func() { + tokenHomeResolver = orig + tokenHomeOverridden = origOverridden + } } // TokenFilePath returns the path to the canonical token file. @@ -876,6 +1112,17 @@ func TokenFilePath() string { // Called by sciontool init to seed the initial value and by the refresh // loop to persist updated tokens. Written atomically via temp file + rename. func WriteTokenFile(token string) error { + // Guardrail: under `go test`, refuse to write the real token file unless a + // test has explicitly isolated it via SetTokenHome. resolveTokenHome + // resolves to the live scion user's home inside agent containers, so a test + // that forgets to isolate would silently overwrite a running agent's token + // (seen in the wild: a refresh test persisted the literal "refreshed-token", + // 401-ing the agent). Fail loudly instead of corrupting live state. + if testing.Testing() && !tokenHomeOverridden { + panic("scion/hub: WriteTokenFile called during a test without SetTokenHome(); " + + "call SetTokenHome(t.TempDir()) so tests never overwrite the real ~/.scion/scion-token") + } + path := TokenFilePath() dir := filepath.Dir(path) diff --git a/pkg/sciontool/hub/client_test.go b/pkg/sciontool/hub/client_test.go index bd129dd3b..dc031c271 100644 --- a/pkg/sciontool/hub/client_test.go +++ b/pkg/sciontool/hub/client_test.go @@ -16,10 +16,13 @@ package hub import ( "context" + "encoding/base64" "encoding/json" + "errors" "net/http" "net/http/httptest" "os" + "sync/atomic" "testing" "time" @@ -28,53 +31,68 @@ import ( "github.com/stretchr/testify/require" ) +// makeJWTWithExpiry builds an unsigned JWT-shaped token whose payload carries the +// given expiry. ParseTokenExpiry only base64-decodes the payload (it does not +// verify the signature), so this is enough to drive the refresh loop's +// expiry-tracking logic in tests. +func makeJWTWithExpiry(t *testing.T, exp time.Time) string { + t.Helper() + header := base64.RawURLEncoding.EncodeToString([]byte(`{"alg":"HS256","typ":"JWT"}`)) + payloadJSON, err := json.Marshal(struct { + Exp int64 `json:"exp"` + }{Exp: exp.Unix()}) + require.NoError(t, err) + payload := base64.RawURLEncoding.EncodeToString(payloadJSON) + return header + "." + payload + ".sig" +} + +// scrubHubEnv clears all Hub-related environment variables for the +// duration of the test, preventing accidental communication with a +// real Hub when tests run inside an agent container. See issue #123. +func scrubHubEnv(t *testing.T) { + t.Helper() + for _, key := range []string{ + EnvHubEndpoint, + EnvHubURL, + EnvHubToken, + EnvAgentID, + EnvAgentMode, + } { + t.Setenv(key, "") + } + t.Cleanup(SetHubTestSandboxed()) +} + func TestNewClient_FromEnvironment(t *testing.T) { - // Save and restore env vars - origEndpoint := os.Getenv(EnvHubEndpoint) - origURL := os.Getenv(EnvHubURL) - origToken := os.Getenv(EnvHubToken) - origAgentID := os.Getenv(EnvAgentID) - defer func() { - os.Setenv(EnvHubEndpoint, origEndpoint) - os.Setenv(EnvHubURL, origURL) - os.Setenv(EnvHubToken, origToken) - os.Setenv(EnvAgentID, origAgentID) - }() + // Clear Hub env vars to prevent leakage from the container (issue #123). + scrubHubEnv(t) t.Run("missing env vars returns nil", func(t *testing.T) { - os.Unsetenv(EnvHubEndpoint) - os.Unsetenv(EnvHubURL) - os.Unsetenv(EnvHubToken) - os.Unsetenv(EnvAgentID) - + scrubHubEnv(t) client := NewClient() assert.Nil(t, client) }) t.Run("missing token returns nil", func(t *testing.T) { - os.Unsetenv(EnvHubEndpoint) - os.Setenv(EnvHubURL, "http://hub.example.com") - os.Unsetenv(EnvHubToken) - os.Unsetenv(EnvAgentID) - + scrubHubEnv(t) + t.Setenv(EnvHubURL, "http://hub.example.com") client := NewClient() assert.Nil(t, client) }) t.Run("missing agentID returns nil", func(t *testing.T) { - os.Setenv(EnvHubEndpoint, "http://hub.example.com") - os.Setenv(EnvHubToken, "test-token") - os.Unsetenv(EnvAgentID) - + scrubHubEnv(t) + t.Setenv(EnvHubEndpoint, "http://hub.example.com") + t.Setenv(EnvHubToken, "test-token") client := NewClient() assert.Nil(t, client, "should not create client without agent ID (local agent scenario)") }) t.Run("with all env vars returns client", func(t *testing.T) { - os.Unsetenv(EnvHubEndpoint) - os.Setenv(EnvHubURL, "http://hub.example.com") - os.Setenv(EnvHubToken, "test-token") - os.Setenv(EnvAgentID, "agent-123") + scrubHubEnv(t) + t.Setenv(EnvHubURL, "http://hub.example.com") + t.Setenv(EnvHubToken, "test-token") + t.Setenv(EnvAgentID, "agent-123") client := NewClient() require.NotNil(t, client) @@ -82,10 +100,11 @@ func TestNewClient_FromEnvironment(t *testing.T) { }) t.Run("prefers SCION_HUB_ENDPOINT over SCION_HUB_URL", func(t *testing.T) { - os.Setenv(EnvHubEndpoint, "http://endpoint.example.com") - os.Setenv(EnvHubURL, "http://url.example.com") - os.Setenv(EnvHubToken, "test-token") - os.Setenv(EnvAgentID, "agent-123") + scrubHubEnv(t) + t.Setenv(EnvHubEndpoint, "http://endpoint.example.com") + t.Setenv(EnvHubURL, "http://url.example.com") + t.Setenv(EnvHubToken, "test-token") + t.Setenv(EnvAgentID, "agent-123") client := NewClient() require.NotNil(t, client) @@ -93,10 +112,10 @@ func TestNewClient_FromEnvironment(t *testing.T) { }) t.Run("falls back to SCION_HUB_URL when SCION_HUB_ENDPOINT not set", func(t *testing.T) { - os.Unsetenv(EnvHubEndpoint) - os.Setenv(EnvHubURL, "http://url.example.com") - os.Setenv(EnvHubToken, "test-token") - os.Setenv(EnvAgentID, "agent-123") + scrubHubEnv(t) + t.Setenv(EnvHubURL, "http://url.example.com") + t.Setenv(EnvHubToken, "test-token") + t.Setenv(EnvAgentID, "agent-123") client := NewClient() require.NotNil(t, client) @@ -261,31 +280,25 @@ func TestClient_Heartbeat(t *testing.T) { } func TestIsHostedMode(t *testing.T) { - origMode := os.Getenv(EnvAgentMode) - defer os.Setenv(EnvAgentMode, origMode) - t.Run("not hosted mode", func(t *testing.T) { - os.Unsetenv(EnvAgentMode) + t.Setenv(EnvAgentMode, "") assert.False(t, IsHostedMode()) - os.Setenv(EnvAgentMode, "solo") + t.Setenv(EnvAgentMode, "solo") assert.False(t, IsHostedMode()) }) t.Run("hosted mode", func(t *testing.T) { - os.Setenv(EnvAgentMode, "hosted") + t.Setenv(EnvAgentMode, "hosted") assert.True(t, IsHostedMode()) }) } func TestGetAgentID(t *testing.T) { - origID := os.Getenv(EnvAgentID) - defer os.Setenv(EnvAgentID, origID) - - os.Setenv(EnvAgentID, "test-agent-id") + t.Setenv(EnvAgentID, "test-agent-id") assert.Equal(t, "test-agent-id", GetAgentID()) - os.Unsetenv(EnvAgentID) + t.Setenv(EnvAgentID, "") assert.Equal(t, "", GetAgentID()) } @@ -623,6 +636,12 @@ func TestClient_GetToken(t *testing.T) { func TestClient_StartTokenRefresh(t *testing.T) { t.Run("refreshes token at scheduled time", func(t *testing.T) { + // Isolate the token file to a temp dir. Without this, RefreshToken's + // WriteTokenFile would clobber the real ~/.scion/scion-token when the + // suite is run inside an agent container (where the scion user exists). + cleanup := SetTokenHome(t.TempDir()) + defer cleanup() + refreshed := false server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { refreshed = true @@ -674,19 +693,129 @@ func TestClient_StartTokenRefresh(t *testing.T) { t.Fatal("token refresh loop did not exit after context cancellation") } }) + + t.Run("retries after a transient failure then recovers", func(t *testing.T) { + cleanup := SetTokenHome(t.TempDir()) + defer cleanup() + + var calls int32 + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + // First attempt fails transiently (503); the second succeeds. + if atomic.AddInt32(&calls, 1) == 1 { + w.WriteHeader(http.StatusServiceUnavailable) + w.Write([]byte("temporarily down")) + return + } + futureExpiry := time.Now().Add(10 * time.Hour).UTC().Format(time.RFC3339) + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + w.Write([]byte(`{"token":"recovered-token","expires_at":"` + futureExpiry + `"}`)) + })) + defer server.Close() + + client := NewClientWithConfig(server.URL, "old-token", "agent-123") + + var errCount int32 + refreshed := make(chan struct{}, 1) + ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second) + defer cancel() + + done := client.StartTokenRefresh(ctx, &TokenRefreshConfig{ + RefreshAt: time.Now(), + Timeout: time.Second, + RetryBaseDelay: 10 * time.Millisecond, + RetryMaxDelay: 10 * time.Millisecond, + OnError: func(error) { atomic.AddInt32(&errCount, 1) }, + OnRefreshed: func(time.Time) { + select { + case refreshed <- struct{}{}: + default: + } + }, + }) + + select { + case <-refreshed: + // Recovered after the transient failure. + case <-time.After(time.Second): + t.Fatal("token refresh did not recover after a transient failure") + } + + assert.Equal(t, "recovered-token", client.GetToken()) + assert.GreaterOrEqual(t, atomic.LoadInt32(&errCount), int32(1), "transient failure should invoke OnError") + + cancel() + <-done + }) + + t.Run("auth lost fires once and keeps retrying", func(t *testing.T) { + cleanup := SetTokenHome(t.TempDir()) + defer cleanup() + + // The current token is already expired, so the first failed refresh is a + // terminal auth loss. The server always rejects with 401 (as it would + // after a hub signing-key rotation). + expiredToken := makeJWTWithExpiry(t, time.Now().Add(-time.Minute)) + + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusUnauthorized) + w.Write([]byte("invalid agent token: failed to verify token")) + })) + defer server.Close() + + client := NewClientWithConfig(server.URL, expiredToken, "agent-123") + + var errCount, authLostCount int32 + var sawUnauthorized atomic.Bool + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + done := client.StartTokenRefresh(ctx, &TokenRefreshConfig{ + RefreshAt: time.Now(), + Timeout: time.Second, + RetryBaseDelay: 10 * time.Millisecond, + RetryMaxDelay: 10 * time.Millisecond, + OnError: func(err error) { + atomic.AddInt32(&errCount, 1) + if errors.Is(err, ErrTokenRefreshUnauthorized) { + sawUnauthorized.Store(true) + } + }, + OnAuthLost: func() { atomic.AddInt32(&authLostCount, 1) }, + }) + + // Let several retries elapse. + require.Eventually(t, func() bool { + return atomic.LoadInt32(&errCount) >= 3 + }, time.Second, 5*time.Millisecond, "expected the loop to keep retrying") + + // The loop must still be running (not exited) despite the auth loss. + select { + case <-done: + t.Fatal("refresh loop exited instead of continuing to retry after auth loss") + default: + } + + cancel() + <-done + + assert.Equal(t, int32(1), atomic.LoadInt32(&authLostCount), "OnAuthLost should fire exactly once") + assert.True(t, sawUnauthorized.Load(), "401 refresh error should wrap ErrTokenRefreshUnauthorized") + }) } -func TestOperatingMode(t *testing.T) { - // Save and restore env vars - origEndpoint := os.Getenv(EnvHubEndpoint) - origURL := os.Getenv(EnvHubURL) - origMode := os.Getenv(EnvAgentMode) - defer func() { - os.Setenv(EnvHubEndpoint, origEndpoint) - os.Setenv(EnvHubURL, origURL) - os.Setenv(EnvAgentMode, origMode) - }() +func TestTokenRefreshBackoff(t *testing.T) { + base := 30 * time.Second + max := 5 * time.Minute + assert.Equal(t, base, tokenRefreshBackoff(0, base, max), "non-positive failures clamps to one attempt") + assert.Equal(t, base, tokenRefreshBackoff(1, base, max)) + assert.Equal(t, 2*base, tokenRefreshBackoff(2, base, max)) + assert.Equal(t, 4*base, tokenRefreshBackoff(3, base, max)) + assert.Equal(t, max, tokenRefreshBackoff(10, base, max), "backoff is capped at max") +} + +func TestOperatingMode(t *testing.T) { tests := []struct { name string endpoint string @@ -747,17 +876,16 @@ func TestOperatingMode(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - os.Unsetenv(EnvHubEndpoint) - os.Unsetenv(EnvHubURL) - os.Unsetenv(EnvAgentMode) + // Clear Hub env, then set test values (issue #123). + scrubHubEnv(t) if tt.endpoint != "" { - os.Setenv(EnvHubEndpoint, tt.endpoint) + t.Setenv(EnvHubEndpoint, tt.endpoint) } if tt.hubURL != "" { - os.Setenv(EnvHubURL, tt.hubURL) + t.Setenv(EnvHubURL, tt.hubURL) } if tt.agentMode != "" { - os.Setenv(EnvAgentMode, tt.agentMode) + t.Setenv(EnvAgentMode, tt.agentMode) } mode := OperatingMode() @@ -768,20 +896,8 @@ func TestOperatingMode(t *testing.T) { } func TestOperatingMode_Defaults(t *testing.T) { - // Save and restore env vars - origEndpoint := os.Getenv(EnvHubEndpoint) - origURL := os.Getenv(EnvHubURL) - origMode := os.Getenv(EnvAgentMode) - defer func() { - os.Setenv(EnvHubEndpoint, origEndpoint) - os.Setenv(EnvHubURL, origURL) - os.Setenv(EnvAgentMode, origMode) - }() - - // Clear all relevant env vars - os.Unsetenv(EnvHubEndpoint) - os.Unsetenv(EnvHubURL) - os.Unsetenv(EnvAgentMode) + // Clear all relevant env vars (issue #123). + scrubHubEnv(t) mode := OperatingMode() assert.Equal(t, ModeLocal, mode, "should default to ModeLocal when no env vars are set") @@ -818,18 +934,11 @@ func TestNewClient_UsesTokenFile(t *testing.T) { cleanup := SetTokenHome(t.TempDir()) defer cleanup() - origEndpoint := os.Getenv(EnvHubEndpoint) - origToken := os.Getenv(EnvHubToken) - origAgentID := os.Getenv(EnvAgentID) - defer func() { - os.Setenv(EnvHubEndpoint, origEndpoint) - os.Setenv(EnvHubToken, origToken) - os.Setenv(EnvAgentID, origAgentID) - }() - - os.Setenv(EnvHubEndpoint, "http://hub.example.com") - os.Setenv(EnvHubToken, "original-env-token") - os.Setenv(EnvAgentID, "agent-123") + // Clear Hub env, then set test values (issue #123). + scrubHubEnv(t) + t.Setenv(EnvHubEndpoint, "http://hub.example.com") + t.Setenv(EnvHubToken, "original-env-token") + t.Setenv(EnvAgentID, "agent-123") t.Run("uses env token when no file exists", func(t *testing.T) { client := NewClient() @@ -974,27 +1083,21 @@ func TestGitHubTokenFile_WriteAndRead(t *testing.T) { } func TestIsGitHubAppEnabled(t *testing.T) { - orig := os.Getenv(EnvGitHubAppEnabled) - defer os.Setenv(EnvGitHubAppEnabled, orig) - - os.Unsetenv(EnvGitHubAppEnabled) + t.Setenv(EnvGitHubAppEnabled, "") assert.False(t, IsGitHubAppEnabled()) - os.Setenv(EnvGitHubAppEnabled, "false") + t.Setenv(EnvGitHubAppEnabled, "false") assert.False(t, IsGitHubAppEnabled()) - os.Setenv(EnvGitHubAppEnabled, "true") + t.Setenv(EnvGitHubAppEnabled, "true") assert.True(t, IsGitHubAppEnabled()) } func TestGitHubTokenPath(t *testing.T) { - orig := os.Getenv(EnvGitHubTokenPath) - defer os.Setenv(EnvGitHubTokenPath, orig) - - os.Unsetenv(EnvGitHubTokenPath) + t.Setenv(EnvGitHubTokenPath, "") assert.Equal(t, DefaultGitHubTokenPath, GitHubTokenPath()) - os.Setenv(EnvGitHubTokenPath, "/custom/path/token") + t.Setenv(EnvGitHubTokenPath, "/custom/path/token") assert.Equal(t, "/custom/path/token", GitHubTokenPath()) } @@ -1172,6 +1275,46 @@ func TestStartGitHubTokenRefresh_WritesExpiryFile(t *testing.T) { assert.False(t, IsGitHubTokenExpired(tokenPath)) } +func TestStartGitHubTokenRefresh_CallsOnRefreshedAfterEnvUpdate(t *testing.T) { + tmpDir := t.TempDir() + tokenPath := tmpDir + "/github-token" + + t.Setenv("GITHUB_TOKEN", "ghs_original_stale") + + futureExpiry := time.Now().Add(1 * time.Hour).UTC() + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + json.NewEncoder(w).Encode(map[string]string{ + "token": "ghs_fresh_token", + "expires_at": futureExpiry.Format(time.RFC3339), + }) + })) + defer server.Close() + + client := NewClientWithConfig(server.URL, "hub-token", "agent-123") + + ctx, cancel := context.WithTimeout(context.Background(), 500*time.Millisecond) + defer cancel() + + var callbackToken string + done := client.StartGitHubTokenRefresh(ctx, &GitHubTokenRefreshConfig{ + RefreshAt: time.Now(), + TokenPath: tokenPath, + OnRefreshed: func(newToken string, newExpiry time.Time) { + // At callback time, GITHUB_TOKEN env var should already be updated + callbackToken = newToken + assert.Equal(t, "ghs_fresh_token", os.Getenv("GITHUB_TOKEN"), + "GITHUB_TOKEN env var should be updated before OnRefreshed is called") + }, + }) + + <-done + + assert.Equal(t, "ghs_fresh_token", callbackToken, + "OnRefreshed callback should have been called with the new token") +} + func TestClient_StartHeartbeat_DefaultConfig(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusOK) diff --git a/pkg/sciontool/hub/oidc.go b/pkg/sciontool/hub/oidc.go new file mode 100644 index 000000000..808751f4c --- /dev/null +++ b/pkg/sciontool/hub/oidc.go @@ -0,0 +1,240 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package hub + +import ( + "fmt" + "io" + "net/http" + "os" + "strings" + "sync" + "time" + + "cloud.google.com/go/compute/metadata" + + "github.com/GoogleCloudPlatform/scion/pkg/sciontool/log" +) + +const ( + // EnvHubOIDCAudience overrides the audience claim in the OIDC identity token. + EnvHubOIDCAudience = "SCION_HUB_OIDC_AUDIENCE" + + // EnvTransportToken is the env var for the hub-provided transport OIDC token. + EnvTransportToken = "SCION_TRANSPORT_TOKEN" + + // EnvTransportAudience is the env var for the transport token audience. + EnvTransportAudience = "SCION_TRANSPORT_AUDIENCE" + + gcpMetadataBaseURL = "http://metadata.google.internal" + + oidcRefreshMargin = 5 * time.Minute + oidcDefaultTTL = 1 * time.Hour + oidcFetchTimeout = 2 * time.Second +) + +// isOnGCPFunc detects whether we're running on GCP. Override in tests. +var isOnGCPFunc = func() bool { return metadata.OnGCE() } + +// oidcTokenSource provides OIDC identity tokens for transport-layer auth. +// Implementations are thread-safe. +type oidcTokenSource interface { + // getToken returns a valid OIDC token, refreshing if necessary. + getToken() (string, error) + // setToken updates the cached token and expiry. Used by the refresh path + // to inject hub-provided tokens. + setToken(token string, expiry time.Time) +} + +// --- metadataTokenSource: fetches OIDC from GCE metadata server --- + +// metadataTokenSource fetches and caches Google OIDC identity tokens from the +// GCE metadata server. Used in passthrough/on-GCE mode (the PR #307 pattern). +type metadataTokenSource struct { + audience string + metadataBaseURL string + httpClient *http.Client + + mu sync.RWMutex + token string + expiresAt time.Time +} + +func (s *metadataTokenSource) getToken() (string, error) { + s.mu.RLock() + if s.token != "" && time.Now().Before(s.expiresAt.Add(-oidcRefreshMargin)) { + tok := s.token + s.mu.RUnlock() + return tok, nil + } + s.mu.RUnlock() + + s.mu.Lock() + defer s.mu.Unlock() + + // Double-check after acquiring write lock. + if s.token != "" && time.Now().Before(s.expiresAt.Add(-oidcRefreshMargin)) { + return s.token, nil + } + + url := fmt.Sprintf("%s/computeMetadata/v1/instance/service-accounts/default/identity?audience=%s&format=full", + s.metadataBaseURL, s.audience) + + req, err := http.NewRequest("GET", url, nil) + if err != nil { + return "", fmt.Errorf("oidc: build request: %w", err) + } + req.Header.Set("Metadata-Flavor", "Google") + + resp, err := s.httpClient.Do(req) + if err != nil { + return "", fmt.Errorf("oidc: metadata fetch: %w", err) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return "", fmt.Errorf("oidc: metadata server returned %d", resp.StatusCode) + } + + body, err := io.ReadAll(resp.Body) + if err != nil { + return "", fmt.Errorf("oidc: read response: %w", err) + } + + tok := strings.TrimSpace(string(body)) + expiry, err := ParseTokenExpiry(tok) + if err != nil { + expiry = time.Now().Add(oidcDefaultTTL) + } + + s.token = tok + s.expiresAt = expiry + return tok, nil +} + +func (s *metadataTokenSource) setToken(token string, expiry time.Time) { + s.mu.Lock() + defer s.mu.Unlock() + s.token = token + s.expiresAt = expiry +} + +// --- injectedTokenSource: hub-provided token refreshed via tokens[] --- + +// injectedTokenSource holds a transport token injected by the hub via the +// dispatch payload (cold start) and refreshed via the tokens[] array on +// subsequent refresh calls. +type injectedTokenSource struct { + mu sync.RWMutex + token string + expiresAt time.Time +} + +func (s *injectedTokenSource) getToken() (string, error) { + s.mu.RLock() + defer s.mu.RUnlock() + + if s.token == "" { + return "", fmt.Errorf("oidc: no transport token available") + } + + // Check if the token is within the refresh margin of expiry. + // We still return it (it may still be valid), but log a warning. + if !s.expiresAt.IsZero() && time.Now().After(s.expiresAt.Add(-oidcRefreshMargin)) { + log.Debug("OIDC transport token is near expiry or expired, returning anyway") + } + + return s.token, nil +} + +func (s *injectedTokenSource) setToken(token string, expiry time.Time) { + s.mu.Lock() + defer s.mu.Unlock() + s.token = token + s.expiresAt = expiry +} + +// --- oidcTransport: RoundTripper that injects Authorization: Bearer --- + +// oidcTransport is an http.RoundTripper that injects a Google OIDC identity +// token as an Authorization header on outgoing requests. +type oidcTransport struct { + base http.RoundTripper + source oidcTokenSource +} + +func (t *oidcTransport) RoundTrip(req *http.Request) (*http.Response, error) { + if req.Header.Get("Authorization") == "" { + tok, err := t.source.getToken() + if err != nil { + log.Debug("OIDC token fetch failed, skipping Authorization header: %v", err) + } else { + req = req.Clone(req.Context()) + req.Header.Set("Authorization", "Bearer "+tok) + } + } + return t.base.RoundTrip(req) +} + +// newOIDCTransport creates an oidcTransport wrapping the base transport. +func newOIDCTransport(base http.RoundTripper, source oidcTokenSource) *oidcTransport { + if base == nil { + base = http.DefaultTransport + } + return &oidcTransport{ + base: base, + source: source, + } +} + +// configureOIDCTransport sets up the OIDC transport layer on the client. +// Token source selection: +// 1. If SCION_TRANSPORT_TOKEN env var is set → injected mode (hub-provided token). +// 2. Else if running on GCP → metadata server mode (ambient SA identity). +// 3. Else → no OIDC transport (agent uses plain HTTP). +func (c *Client) configureOIDCTransport() { + // Check for hub-injected transport token (dispatch payload / cold start) + if tok := os.Getenv(EnvTransportToken); tok != "" { + source := &injectedTokenSource{} + expiry, err := ParseTokenExpiry(tok) + if err != nil { + expiry = time.Now().Add(oidcDefaultTTL) + } + source.setToken(tok, expiry) + c.oidcSource = source + c.client.Transport = newOIDCTransport(c.client.Transport, source) + log.Debug("Configured OIDC transport: injected mode (hub-provided token)") + return + } + + // Fall back to GCE metadata server if on GCP + if !isOnGCPFunc() { + return + } + + audience := os.Getenv(EnvHubOIDCAudience) + if audience == "" { + audience = c.hubURL + } + + source := &metadataTokenSource{ + audience: audience, + metadataBaseURL: gcpMetadataBaseURL, + httpClient: &http.Client{Timeout: oidcFetchTimeout}, + } + c.oidcSource = source + c.client.Transport = newOIDCTransport(c.client.Transport, source) + log.Debug("Configured OIDC transport: metadata mode (audience=%s)", audience) +} diff --git a/pkg/sciontool/hub/oidc_test.go b/pkg/sciontool/hub/oidc_test.go new file mode 100644 index 000000000..167442308 --- /dev/null +++ b/pkg/sciontool/hub/oidc_test.go @@ -0,0 +1,525 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package hub + +import ( + "context" + "encoding/base64" + "encoding/json" + "fmt" + "net/http" + "net/http/httptest" + "os" + "sync/atomic" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +// makeTestJWT builds a minimal JWT with the given expiry for testing. +func makeTestJWT(exp time.Time) string { + header := base64.RawURLEncoding.EncodeToString([]byte(`{"alg":"none","typ":"JWT"}`)) + payload, _ := json.Marshal(map[string]interface{}{"exp": exp.Unix(), "iss": "test"}) + payloadB64 := base64.RawURLEncoding.EncodeToString(payload) + sig := base64.RawURLEncoding.EncodeToString([]byte("fakesig")) + return fmt.Sprintf("%s.%s.%s", header, payloadB64, sig) +} + +func overrideGCPDetection(val bool) func() { + orig := isOnGCPFunc + isOnGCPFunc = func() bool { return val } + return func() { isOnGCPFunc = orig } +} + +// --- injectedTokenSource tests --- + +func TestInjectedTokenSource_SetAndGet(t *testing.T) { + src := &injectedTokenSource{} + token := makeTestJWT(time.Now().Add(1 * time.Hour)) + expiry := time.Now().Add(1 * time.Hour) + + src.setToken(token, expiry) + + got, err := src.getToken() + require.NoError(t, err) + assert.Equal(t, token, got) +} + +func TestInjectedTokenSource_Empty(t *testing.T) { + src := &injectedTokenSource{} + _, err := src.getToken() + assert.Error(t, err) + assert.Contains(t, err.Error(), "no transport token") +} + +func TestInjectedTokenSource_NearExpiry(t *testing.T) { + src := &injectedTokenSource{} + token := makeTestJWT(time.Now().Add(2 * time.Minute)) + expiry := time.Now().Add(2 * time.Minute) // within 5-min margin + + src.setToken(token, expiry) + + // Should still return the token (with a debug log warning) + got, err := src.getToken() + require.NoError(t, err) + assert.Equal(t, token, got) +} + +func TestInjectedTokenSource_UpdateToken(t *testing.T) { + src := &injectedTokenSource{} + token1 := makeTestJWT(time.Now().Add(1 * time.Hour)) + token2 := makeTestJWT(time.Now().Add(2 * time.Hour)) + + src.setToken(token1, time.Now().Add(1*time.Hour)) + got1, _ := src.getToken() + assert.Equal(t, token1, got1) + + src.setToken(token2, time.Now().Add(2*time.Hour)) + got2, _ := src.getToken() + assert.Equal(t, token2, got2) +} + +// --- metadataTokenSource tests --- + +func TestMetadataTokenSource_FetchAndCache(t *testing.T) { + var fetchCount atomic.Int32 + token := makeTestJWT(time.Now().Add(1 * time.Hour)) + + metaSrv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, "Google", r.Header.Get("Metadata-Flavor")) + assert.Contains(t, r.URL.Query().Get("audience"), "https://hub.example.com") + assert.Equal(t, "full", r.URL.Query().Get("format")) + fetchCount.Add(1) + fmt.Fprint(w, token) + })) + defer metaSrv.Close() + + src := &metadataTokenSource{ + audience: "https://hub.example.com", + metadataBaseURL: metaSrv.URL, + httpClient: &http.Client{Timeout: 2 * time.Second}, + } + + tok1, err := src.getToken() + require.NoError(t, err) + assert.Equal(t, token, tok1) + + tok2, err := src.getToken() + require.NoError(t, err) + assert.Equal(t, token, tok2) + + assert.Equal(t, int32(1), fetchCount.Load(), "second call should use cache") +} + +func TestMetadataTokenSource_RefreshExpired(t *testing.T) { + var fetchCount atomic.Int32 + token1 := makeTestJWT(time.Now().Add(1 * time.Hour)) + token2 := makeTestJWT(time.Now().Add(2 * time.Hour)) + + metaSrv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if fetchCount.Add(1) == 1 { + fmt.Fprint(w, token1) + } else { + fmt.Fprint(w, token2) + } + })) + defer metaSrv.Close() + + src := &metadataTokenSource{ + audience: "https://hub.example.com", + metadataBaseURL: metaSrv.URL, + httpClient: &http.Client{Timeout: 2 * time.Second}, + } + + tok, err := src.getToken() + require.NoError(t, err) + assert.Equal(t, token1, tok) + + // Simulate expiry + src.mu.Lock() + src.expiresAt = time.Now().Add(-1 * time.Minute) + src.mu.Unlock() + + tok, err = src.getToken() + require.NoError(t, err) + assert.Equal(t, token2, tok) + assert.Equal(t, int32(2), fetchCount.Load()) +} + +func TestMetadataTokenSource_RefreshWithinMargin(t *testing.T) { + var fetchCount atomic.Int32 + token1 := makeTestJWT(time.Now().Add(1 * time.Hour)) + token2 := makeTestJWT(time.Now().Add(2 * time.Hour)) + + metaSrv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if fetchCount.Add(1) == 1 { + fmt.Fprint(w, token1) + } else { + fmt.Fprint(w, token2) + } + })) + defer metaSrv.Close() + + src := &metadataTokenSource{ + audience: "https://hub.example.com", + metadataBaseURL: metaSrv.URL, + httpClient: &http.Client{Timeout: 2 * time.Second}, + } + + tok, err := src.getToken() + require.NoError(t, err) + assert.Equal(t, token1, tok) + + // Set expiry within the 5-minute refresh margin + src.mu.Lock() + src.expiresAt = time.Now().Add(3 * time.Minute) + src.mu.Unlock() + + tok, err = src.getToken() + require.NoError(t, err) + assert.Equal(t, token2, tok, "should re-fetch when within refresh margin") +} + +func TestMetadataTokenSource_SetToken(t *testing.T) { + src := &metadataTokenSource{ + audience: "https://hub.example.com", + metadataBaseURL: "http://127.0.0.1:1", // unreachable + httpClient: &http.Client{Timeout: 100 * time.Millisecond}, + } + + token := makeTestJWT(time.Now().Add(1 * time.Hour)) + expiry := time.Now().Add(1 * time.Hour) + src.setToken(token, expiry) + + // Should return the set token without hitting metadata server + got, err := src.getToken() + require.NoError(t, err) + assert.Equal(t, token, got) +} + +// --- oidcTransport tests --- + +func TestOIDCTransport_InjectsHeader(t *testing.T) { + var receivedAuth string + hubSrv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + receivedAuth = r.Header.Get("Authorization") + w.WriteHeader(http.StatusOK) + })) + defer hubSrv.Close() + + token := makeTestJWT(time.Now().Add(1 * time.Hour)) + source := &injectedTokenSource{} + source.setToken(token, time.Now().Add(1*time.Hour)) + + transport := newOIDCTransport(http.DefaultTransport, source) + client := &http.Client{Transport: transport} + + req, _ := http.NewRequest("GET", hubSrv.URL+"/test", nil) + resp, err := client.Do(req) + require.NoError(t, err) + resp.Body.Close() + + assert.Equal(t, "Bearer "+token, receivedAuth) +} + +func TestOIDCTransport_DoesNotOverrideExistingAuth(t *testing.T) { + var receivedAuth string + hubSrv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + receivedAuth = r.Header.Get("Authorization") + w.WriteHeader(http.StatusOK) + })) + defer hubSrv.Close() + + source := &injectedTokenSource{} + source.setToken("should-not-be-used", time.Now().Add(1*time.Hour)) + + transport := newOIDCTransport(http.DefaultTransport, source) + client := &http.Client{Transport: transport} + + req, _ := http.NewRequest("GET", hubSrv.URL+"/test", nil) + req.Header.Set("Authorization", "Bearer existing-token") + resp, err := client.Do(req) + require.NoError(t, err) + resp.Body.Close() + + assert.Equal(t, "Bearer existing-token", receivedAuth) +} + +func TestOIDCTransport_GracefulDegradation(t *testing.T) { + var requestReceived bool + hubSrv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + requestReceived = true + assert.Empty(t, r.Header.Get("Authorization"), "no auth header when source has no token") + w.WriteHeader(http.StatusOK) + })) + defer hubSrv.Close() + + // Source with no token → getToken() returns error + source := &injectedTokenSource{} + transport := newOIDCTransport(http.DefaultTransport, source) + client := &http.Client{Transport: transport} + + req, _ := http.NewRequest("GET", hubSrv.URL+"/test", nil) + resp, err := client.Do(req) + require.NoError(t, err) + resp.Body.Close() + + assert.True(t, requestReceived, "request should proceed even when token unavailable") +} + +func TestOIDCTransport_WithMetadataSource(t *testing.T) { + token := makeTestJWT(time.Now().Add(1 * time.Hour)) + metaSrv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + fmt.Fprint(w, token) + })) + defer metaSrv.Close() + + var receivedAuth string + hubSrv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + receivedAuth = r.Header.Get("Authorization") + w.WriteHeader(http.StatusOK) + })) + defer hubSrv.Close() + + source := &metadataTokenSource{ + audience: "https://hub.example.com", + metadataBaseURL: metaSrv.URL, + httpClient: &http.Client{Timeout: 2 * time.Second}, + } + transport := newOIDCTransport(http.DefaultTransport, source) + client := &http.Client{Transport: transport} + + req, _ := http.NewRequest("GET", hubSrv.URL+"/test", nil) + resp, err := client.Do(req) + require.NoError(t, err) + resp.Body.Close() + + assert.Equal(t, "Bearer "+token, receivedAuth) +} + +// --- configureOIDCTransport tests --- + +func TestConfigureOIDCTransport_InjectedMode(t *testing.T) { + token := makeTestJWT(time.Now().Add(1 * time.Hour)) + os.Setenv(EnvTransportToken, token) + defer os.Unsetenv(EnvTransportToken) + + c := &Client{ + hubURL: "https://hub.example.com", + client: &http.Client{Timeout: DefaultTimeout}, + } + + c.configureOIDCTransport() + + require.NotNil(t, c.oidcSource) + _, ok := c.oidcSource.(*injectedTokenSource) + assert.True(t, ok, "should use injectedTokenSource") + require.NotNil(t, c.client.Transport) + _, ok = c.client.Transport.(*oidcTransport) + assert.True(t, ok, "transport should be oidcTransport") +} + +func TestConfigureOIDCTransport_MetadataMode(t *testing.T) { + cleanup := overrideGCPDetection(true) + defer cleanup() + + // Ensure no injected token + os.Unsetenv(EnvTransportToken) + + c := &Client{ + hubURL: "https://hub.example.com", + client: &http.Client{Timeout: DefaultTimeout}, + } + + c.configureOIDCTransport() + + require.NotNil(t, c.oidcSource) + src, ok := c.oidcSource.(*metadataTokenSource) + assert.True(t, ok, "should use metadataTokenSource") + assert.Equal(t, "https://hub.example.com", src.audience) +} + +func TestConfigureOIDCTransport_MetadataMode_AudienceOverride(t *testing.T) { + cleanup := overrideGCPDetection(true) + defer cleanup() + + os.Unsetenv(EnvTransportToken) + os.Setenv(EnvHubOIDCAudience, "https://custom-audience.example.com") + defer os.Unsetenv(EnvHubOIDCAudience) + + c := &Client{ + hubURL: "https://hub.example.com", + client: &http.Client{Timeout: DefaultTimeout}, + } + + c.configureOIDCTransport() + + require.NotNil(t, c.oidcSource) + src := c.oidcSource.(*metadataTokenSource) + assert.Equal(t, "https://custom-audience.example.com", src.audience) +} + +func TestConfigureOIDCTransport_NotOnGCP(t *testing.T) { + cleanup := overrideGCPDetection(false) + defer cleanup() + + os.Unsetenv(EnvTransportToken) + + c := &Client{ + hubURL: "https://hub.example.com", + client: &http.Client{Timeout: DefaultTimeout}, + } + + c.configureOIDCTransport() + + assert.Nil(t, c.oidcSource, "should not configure OIDC when not on GCP and no injected token") + assert.Nil(t, c.client.Transport, "transport should not be wrapped") +} + +func TestConfigureOIDCTransport_InjectedPriority(t *testing.T) { + // When both injected token and GCE are available, injected should win + cleanup := overrideGCPDetection(true) + defer cleanup() + + token := makeTestJWT(time.Now().Add(1 * time.Hour)) + os.Setenv(EnvTransportToken, token) + defer os.Unsetenv(EnvTransportToken) + + c := &Client{ + hubURL: "https://hub.example.com", + client: &http.Client{Timeout: DefaultTimeout}, + } + + c.configureOIDCTransport() + + require.NotNil(t, c.oidcSource) + _, ok := c.oidcSource.(*injectedTokenSource) + assert.True(t, ok, "injected should take priority over metadata") +} + +// --- E2E: both agent + OIDC headers --- + +func TestOIDC_EndToEnd_BothHeaders(t *testing.T) { + cleanup := overrideGCPDetection(false) + defer cleanup() + + token := makeTestJWT(time.Now().Add(1 * time.Hour)) + os.Setenv(EnvTransportToken, token) + defer os.Unsetenv(EnvTransportToken) + + var gotAuth, gotAgentToken string + hubSrv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + gotAuth = r.Header.Get("Authorization") + gotAgentToken = r.Header.Get("X-Scion-Agent-Token") + w.WriteHeader(http.StatusOK) + json.NewEncoder(w).Encode(map[string]string{"status": "ok"}) + })) + defer hubSrv.Close() + + c := &Client{ + hubURL: hubSrv.URL, + token: "test-agent-token", + agentID: "test-agent-123", + maxRetries: 1, + retryBaseDelay: 10 * time.Millisecond, + retryMaxDelay: 10 * time.Millisecond, + client: &http.Client{ + Timeout: DefaultTimeout, + }, + } + c.configureOIDCTransport() + + err := c.UpdateStatus(context.Background(), StatusUpdate{ + Status: "running", + Message: "test", + }) + require.NoError(t, err) + + assert.Equal(t, "Bearer "+token, gotAuth, "OIDC Authorization header should be set") + assert.Equal(t, "test-agent-token", gotAgentToken, "X-Scion-Agent-Token should still be set") +} + +// --- applyRefreshTokens tests --- + +func TestApplyRefreshTokens_TransportToken(t *testing.T) { + source := &injectedTokenSource{} + c := &Client{oidcSource: source} + + newToken := makeTestJWT(time.Now().Add(1 * time.Hour)) + tokens := []RefreshTokenEntry{ + {Layer: "app", Type: "scion_access", Value: "app-token", ExpiresIn: 36000}, + {Layer: "transport", Type: "google_oidc", Value: newToken, ExpiresIn: 3600, Audience: "https://hub.example.com"}, + } + + c.applyRefreshTokens(tokens) + + got, err := source.getToken() + require.NoError(t, err) + assert.Equal(t, newToken, got) +} + +func TestApplyRefreshTokens_NoOIDCSource(t *testing.T) { + c := &Client{} // no oidcSource + + tokens := []RefreshTokenEntry{ + {Layer: "transport", Type: "google_oidc", Value: "token", ExpiresIn: 3600}, + } + + // Should not panic + c.applyRefreshTokens(tokens) +} + +// --- adjustRefreshForTransportTokens tests --- + +func TestAdjustRefreshForTransportTokens_ShorterTransport(t *testing.T) { + source := &injectedTokenSource{} + transportExpiry := time.Now().Add(50 * time.Minute) // short-lived + source.setToken("tok", transportExpiry) + + c := &Client{oidcSource: source} + + // App token would refresh 2h before a 10h expiry (8h from now) + appRefresh := time.Now().Add(8 * time.Hour) + adjusted := c.adjustRefreshForTransportTokens(appRefresh) + + // Transport refresh should be ~45 min from now (50min - 5min margin) + expectedTransportRefresh := transportExpiry.Add(-oidcRefreshMargin) + assert.WithinDuration(t, expectedTransportRefresh, adjusted, 1*time.Second, + "should use transport token's earlier refresh time") +} + +func TestAdjustRefreshForTransportTokens_LongerTransport(t *testing.T) { + source := &injectedTokenSource{} + transportExpiry := time.Now().Add(10 * time.Hour) // long-lived + source.setToken("tok", transportExpiry) + + c := &Client{oidcSource: source} + + // App token would refresh 30 min from now + appRefresh := time.Now().Add(30 * time.Minute) + adjusted := c.adjustRefreshForTransportTokens(appRefresh) + + assert.WithinDuration(t, appRefresh, adjusted, 1*time.Second, + "should keep app token's earlier refresh time") +} + +func TestAdjustRefreshForTransportTokens_NoSource(t *testing.T) { + c := &Client{} // no oidcSource + proposed := time.Now().Add(8 * time.Hour) + adjusted := c.adjustRefreshForTransportTokens(proposed) + assert.Equal(t, proposed, adjusted) +} diff --git a/pkg/sciontool/telemetry/config.go b/pkg/sciontool/telemetry/config.go index ce1ee3f43..8e6b4c115 100644 --- a/pkg/sciontool/telemetry/config.go +++ b/pkg/sciontool/telemetry/config.go @@ -12,6 +12,7 @@ import ( "path/filepath" "strconv" "strings" + "testing" ) // Environment variable names for telemetry configuration. @@ -113,7 +114,25 @@ type FilterConfig struct { // As a fallback, LoadConfig probes this path when the env var is absent. const WellKnownGCPCredentialsPath = ".scion/telemetry-gcp-credentials.json" +// telemetryTestSandboxed reports whether the calling test has explicitly +// declared that it has sandboxed the telemetry environment. LoadConfig +// force-disables cloud export under `go test` unless this flag is set. +var telemetryTestSandboxed bool + +// SetTelemetryTestSandboxed marks the current test as having properly +// sandboxed the telemetry environment. Returns a cleanup function. +func SetTelemetryTestSandboxed() func() { + orig := telemetryTestSandboxed + telemetryTestSandboxed = true + return func() { telemetryTestSandboxed = orig } +} + // LoadConfig loads telemetry configuration from environment variables. +// +// Defense-in-depth: when running under `go test`, cloud telemetry export is +// force-disabled to prevent test code from accidentally exporting spans/logs +// to a real backend under a live agent identity. Local telemetry (receiver, +// pipeline) remains available for tests that need it. func LoadConfig() *Config { cfg := &Config{ Enabled: parseBoolEnv(EnvEnabled, true), @@ -180,6 +199,16 @@ func LoadConfig() *Config { cfg.Redaction.Hash = DefaultHashFields } + // Defense-in-depth: force-disable cloud export under `go test` so that + // a test running inside an agent container never ships spans to a real + // backend under the container's agent identity. Tests that specifically + // need to verify cloud config behavior should call SetTelemetryTestSandboxed. + if testing.Testing() && !telemetryTestSandboxed { + cfg.CloudEnabled = false + cfg.Endpoint = "" + cfg.GCPCredentialsFile = "" + } + return cfg } diff --git a/pkg/sciontool/telemetry/config_test.go b/pkg/sciontool/telemetry/config_test.go index 8e3c017a7..320cf7acd 100644 --- a/pkg/sciontool/telemetry/config_test.go +++ b/pkg/sciontool/telemetry/config_test.go @@ -576,4 +576,6 @@ func clearTelemetryEnv() { os.Unsetenv(EnvGCPCredentials) os.Unsetenv(EnvCloudProvider) os.Unsetenv(EnvMetricsDebug) + // Mark as sandboxed so LoadConfig's test guard doesn't force-disable cloud. + telemetryTestSandboxed = true } diff --git a/pkg/secret/gcpbackend_test.go b/pkg/secret/gcpbackend_test.go index fb0159b93..d50fbbdbd 100644 --- a/pkg/secret/gcpbackend_test.go +++ b/pkg/secret/gcpbackend_test.go @@ -27,7 +27,6 @@ import ( smpb "cloud.google.com/go/secretmanager/apiv1/secretmanagerpb" "github.com/GoogleCloudPlatform/scion/pkg/store" - "github.com/GoogleCloudPlatform/scion/pkg/store/sqlite" "google.golang.org/grpc/codes" "google.golang.org/grpc/status" ) @@ -138,7 +137,7 @@ func (m *mockSMClient) Close() error { func createTestGCPBackend(t *testing.T) (*GCPBackend, *mockSMClient) { t.Helper() - s, err := sqlite.New(":memory:") + s, err := newTestStore(":memory:") if err != nil { t.Fatalf("failed to create test store: %v", err) } @@ -156,7 +155,7 @@ func TestGCPBackend_GetRecoverFromGCPSM_NoDBRecord(t *testing.T) { ctx := context.Background() // Create first backend, store a secret via Set (populates both GCP SM and DB) - s1, err := sqlite.New(":memory:") + s1, err := newTestStore(":memory:") if err != nil { t.Fatalf("failed to create test store: %v", err) } @@ -180,7 +179,7 @@ func TestGCPBackend_GetRecoverFromGCPSM_NoDBRecord(t *testing.T) { // Create a second backend with a FRESH database (simulating DB reset) // but sharing the same mock GCP SM client (secrets still exist there) - s2, err := sqlite.New(":memory:") + s2, err := newTestStore(":memory:") if err != nil { t.Fatalf("failed to create test store 2: %v", err) } diff --git a/pkg/secret/localbackend_test.go b/pkg/secret/localbackend_test.go index a1a967ecb..ef87b26d1 100644 --- a/pkg/secret/localbackend_test.go +++ b/pkg/secret/localbackend_test.go @@ -21,12 +21,11 @@ import ( "testing" "github.com/GoogleCloudPlatform/scion/pkg/store" - "github.com/GoogleCloudPlatform/scion/pkg/store/sqlite" ) func createTestStore(t *testing.T) store.SecretStore { t.Helper() - s, err := sqlite.New(":memory:") + s, err := newTestStore(":memory:") if err != nil { t.Fatalf("failed to create test store: %v", err) } @@ -173,7 +172,7 @@ func TestLocalBackend_Get(t *testing.T) { ctx := context.Background() seedSecret(t, s, &store.Secret{ - ID: "s1", + ID: tid("s1"), Key: "API_KEY", EncryptedValue: "sk-test-123", SecretType: store.SecretTypeEnvironment, @@ -200,7 +199,7 @@ func TestLocalBackend_Delete(t *testing.T) { ctx := context.Background() seedSecret(t, s, &store.Secret{ - ID: "s1", + ID: tid("s1"), Key: "TO_DELETE", EncryptedValue: "value", SecretType: store.SecretTypeEnvironment, @@ -235,7 +234,7 @@ func TestLocalBackend_List(t *testing.T) { for i, name := range []string{"A_KEY", "B_KEY", "C_KEY"} { seedSecret(t, s, &store.Secret{ - ID: "s" + string(rune('1'+i)), + ID: tid("s" + string(rune('1'+i))), Key: name, EncryptedValue: "val-" + name, SecretType: store.SecretTypeEnvironment, @@ -259,7 +258,7 @@ func TestLocalBackend_ListFilterByType(t *testing.T) { ctx := context.Background() seedSecret(t, s, &store.Secret{ - ID: "s1", + ID: tid("s1"), Key: "ENV_KEY", EncryptedValue: "val", SecretType: store.SecretTypeEnvironment, @@ -268,7 +267,7 @@ func TestLocalBackend_ListFilterByType(t *testing.T) { ScopeID: "user-1", }) seedSecret(t, s, &store.Secret{ - ID: "s2", + ID: tid("s2"), Key: "FILE_KEY", EncryptedValue: "data", SecretType: store.SecretTypeFile, @@ -294,7 +293,7 @@ func TestLocalBackend_GetMeta(t *testing.T) { ctx := context.Background() seedSecret(t, s, &store.Secret{ - ID: "s1", + ID: tid("s1"), Key: "META_KEY", EncryptedValue: "secret-value", SecretType: store.SecretTypeVariable, @@ -321,7 +320,7 @@ func TestLocalBackend_Resolve(t *testing.T) { // User-level secrets seedSecret(t, s, &store.Secret{ - ID: "s1", + ID: tid("s1"), Key: "API_KEY", EncryptedValue: "user-api-key", SecretType: store.SecretTypeEnvironment, @@ -330,7 +329,7 @@ func TestLocalBackend_Resolve(t *testing.T) { ScopeID: "user-1", }) seedSecret(t, s, &store.Secret{ - ID: "s2", + ID: tid("s2"), Key: "TLS_CERT", EncryptedValue: "cert-data", SecretType: store.SecretTypeFile, @@ -341,7 +340,7 @@ func TestLocalBackend_Resolve(t *testing.T) { // Project-level override seedSecret(t, s, &store.Secret{ - ID: "s3", + ID: tid("s3"), Key: "API_KEY", EncryptedValue: "grove-api-key", SecretType: store.SecretTypeEnvironment, @@ -350,7 +349,7 @@ func TestLocalBackend_Resolve(t *testing.T) { ScopeID: "grove-1", }) seedSecret(t, s, &store.Secret{ - ID: "s4", + ID: tid("s4"), Key: "DB_PASS", EncryptedValue: "grove-db-pass", SecretType: store.SecretTypeEnvironment, @@ -425,7 +424,7 @@ func TestLocalBackend_ResolveBrokerOverride(t *testing.T) { ctx := context.Background() seedSecret(t, s, &store.Secret{ - ID: "s1", + ID: tid("s1"), Key: "API_KEY", EncryptedValue: "user-key", SecretType: store.SecretTypeEnvironment, @@ -434,7 +433,7 @@ func TestLocalBackend_ResolveBrokerOverride(t *testing.T) { ScopeID: "user-1", }) seedSecret(t, s, &store.Secret{ - ID: "s2", + ID: tid("s2"), Key: "API_KEY", EncryptedValue: "broker-key", SecretType: store.SecretTypeEnvironment, @@ -465,7 +464,7 @@ func TestLocalBackend_ResolveExcludesInternalSecrets(t *testing.T) { // Seed an internal signing key at hub scope (simulates hub signing keys) seedSecret(t, s, &store.Secret{ - ID: "signing-1", + ID: tid("signing-1"), Key: "agent_signing_key", EncryptedValue: "super-secret-key-material", SecretType: store.SecretTypeInternal, @@ -476,7 +475,7 @@ func TestLocalBackend_ResolveExcludesInternalSecrets(t *testing.T) { // Seed a normal hub-scoped environment secret seedSecret(t, s, &store.Secret{ - ID: "hub-env-1", + ID: tid("hub-env-1"), Key: "HUB_API_TOKEN", EncryptedValue: "hub-token-value", SecretType: store.SecretTypeEnvironment, @@ -487,7 +486,7 @@ func TestLocalBackend_ResolveExcludesInternalSecrets(t *testing.T) { // Seed a user-scoped secret seedSecret(t, s, &store.Secret{ - ID: "user-env-1", + ID: tid("user-env-1"), Key: "USER_KEY", EncryptedValue: "user-key-value", SecretType: store.SecretTypeEnvironment, @@ -532,7 +531,7 @@ func TestLocalBackend_ResolveDuplicateTargetAcrossScopes(t *testing.T) { // User-level file secret targeting /tmp/my-secret.json seedSecret(t, s, &store.Secret{ - ID: "u1", + ID: tid("u1"), Key: "my-svc-account", EncryptedValue: "user-cert-data", SecretType: store.SecretTypeFile, @@ -544,7 +543,7 @@ func TestLocalBackend_ResolveDuplicateTargetAcrossScopes(t *testing.T) { // Project-level file secret targeting the SAME path seedSecret(t, s, &store.Secret{ - ID: "g1", + ID: tid("g1"), Key: "my-key", EncryptedValue: "grove-cert-data", SecretType: store.SecretTypeFile, @@ -585,7 +584,7 @@ func TestLocalBackend_ResolveDuplicateEnvTargetAcrossScopes(t *testing.T) { // User-level env secret targeting FOO_VAR seedSecret(t, s, &store.Secret{ - ID: "u1", + ID: tid("u1"), Key: "user-foo", EncryptedValue: "user-val", SecretType: store.SecretTypeEnvironment, @@ -596,7 +595,7 @@ func TestLocalBackend_ResolveDuplicateEnvTargetAcrossScopes(t *testing.T) { // Project-level env secret targeting the SAME env var seedSecret(t, s, &store.Secret{ - ID: "g1", + ID: tid("g1"), Key: "grove-foo", EncryptedValue: "grove-val", SecretType: store.SecretTypeEnvironment, @@ -675,7 +674,7 @@ func TestLocalBackend_ResolveProgeny_AllowProgenyGrantsAccess(t *testing.T) { // User "alice" creates a secret with allowProgeny seedSecret(t, s, &store.Secret{ - ID: "sec-prog-1", + ID: tid("sec-prog-1"), Key: "ANTHROPIC_API_KEY", EncryptedValue: "sk-ant-progeny", SecretType: store.SecretTypeEnvironment, @@ -716,7 +715,7 @@ func TestLocalBackend_ResolveProgeny_DeepAncestry(t *testing.T) { ctx := context.Background() seedSecret(t, s, &store.Secret{ - ID: "sec-prog-deep", + ID: tid("sec-prog-deep"), Key: "DEEP_KEY", EncryptedValue: "deep-value", SecretType: store.SecretTypeEnvironment, @@ -754,7 +753,7 @@ func TestLocalBackend_ResolveProgeny_ProjectOverridesProgeny(t *testing.T) { // User-scoped progeny secret seedSecret(t, s, &store.Secret{ - ID: "sec-prog-override-user", + ID: tid("sec-prog-override-user"), Key: "API_KEY", EncryptedValue: "user-progeny-value", SecretType: store.SecretTypeEnvironment, @@ -767,7 +766,7 @@ func TestLocalBackend_ResolveProgeny_ProjectOverridesProgeny(t *testing.T) { // Project-scoped secret with same key (higher precedence) seedSecret(t, s, &store.Secret{ - ID: "sec-prog-override-grove", + ID: tid("sec-prog-override-grove"), Key: "API_KEY", EncryptedValue: "grove-value", SecretType: store.SecretTypeEnvironment, @@ -812,7 +811,7 @@ func TestLocalBackend_ResolveProgeny_DeniedWhenFlagFalse(t *testing.T) { // User-scoped secret WITHOUT allowProgeny seedSecret(t, s, &store.Secret{ - ID: "sec-no-prog", + ID: tid("sec-no-prog"), Key: "PRIVATE_KEY", EncryptedValue: "private-value", SecretType: store.SecretTypeEnvironment, @@ -848,7 +847,7 @@ func TestLocalBackend_ResolveProgeny_DeniedWhenAncestryMismatch(t *testing.T) { // Alice's secret with allowProgeny seedSecret(t, s, &store.Secret{ - ID: "sec-ancestry-miss", + ID: tid("sec-ancestry-miss"), Key: "ALICE_SECRET", EncryptedValue: "alice-value", SecretType: store.SecretTypeEnvironment, @@ -885,7 +884,7 @@ func TestLocalBackend_ResolveProgeny_DeniedByPolicyCheck(t *testing.T) { ctx := context.Background() seedSecret(t, s, &store.Secret{ - ID: "sec-policy-deny", + ID: tid("sec-policy-deny"), Key: "POLICY_KEY", EncryptedValue: "policy-value", SecretType: store.SecretTypeEnvironment, @@ -921,7 +920,7 @@ func TestLocalBackend_ResolveProgeny_NilAuthzCheckIncludesAll(t *testing.T) { ctx := context.Background() seedSecret(t, s, &store.Secret{ - ID: "sec-no-authz", + ID: tid("sec-no-authz"), Key: "NO_AUTHZ_KEY", EncryptedValue: "no-authz-value", SecretType: store.SecretTypeEnvironment, @@ -960,7 +959,7 @@ func TestLocalBackend_ResolveProgeny_NilOptsNoProgeny(t *testing.T) { ctx := context.Background() seedSecret(t, s, &store.Secret{ - ID: "sec-nil-opts", + ID: tid("sec-nil-opts"), Key: "NIL_OPTS_KEY", EncryptedValue: "nil-opts-value", SecretType: store.SecretTypeEnvironment, diff --git a/pkg/secret/teststore_test.go b/pkg/secret/teststore_test.go new file mode 100644 index 000000000..7ac72ffe7 --- /dev/null +++ b/pkg/secret/teststore_test.go @@ -0,0 +1,64 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//go:build !no_sqlite + +package secret + +import ( + "context" + "fmt" + "sync/atomic" + + "github.com/GoogleCloudPlatform/scion/pkg/ent/entc" + "github.com/GoogleCloudPlatform/scion/pkg/store" + "github.com/GoogleCloudPlatform/scion/pkg/store/entadapter" + "github.com/google/uuid" +) + +// tid deterministically maps a human-readable test identifier (e.g. "user-1") +// to a stable UUID string. The Ent-backed store uses UUID primary keys, so test +// fixtures cannot use arbitrary strings as IDs; wrapping a readable name in tid +// preserves test legibility and cross-reference consistency while satisfying the +// UUID requirement. +func tid(name string) string { + return uuid.NewSHA1(uuid.NameSpaceOID, []byte(name)).String() +} + +// testStoreSeq generates unique in-memory database names so each call to +// newTestStore(":memory:") gets an isolated database. +var testStoreSeq atomic.Int64 + +// newTestStore opens a fresh Ent-backed store for tests, mirroring the +// production single-database layout. It is a drop-in replacement for the former +// raw-SQL constructor: pass ":memory:" for an isolated in-memory database or a +// file path for a persistent one. The returned store is already migrated. +func newTestStore(url string) (store.Store, error) { + var dsn string + if url == ":memory:" { + dsn = fmt.Sprintf("file:secrettest%d?mode=memory&cache=shared", testStoreSeq.Add(1)) + } else { + dsn = "file:" + url + "?cache=shared" + } + + client, err := entc.OpenSQLite(dsn, entc.PoolConfig{}) + if err != nil { + return nil, err + } + if err := entc.AutoMigrate(context.Background(), client); err != nil { + _ = client.Close() + return nil, err + } + return entadapter.NewCompositeStore(client), nil +} diff --git a/pkg/store/concurrency.go b/pkg/store/concurrency.go new file mode 100644 index 000000000..e0c9f93a2 --- /dev/null +++ b/pkg/store/concurrency.go @@ -0,0 +1,150 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package store + +import ( + "context" + "hash/fnv" +) + +// The interfaces in this file are OPTIONAL capabilities that a store backend may +// implement to support running N stateless hub processes against one shared +// database (the multi-replica Postgres deployment, D3). +// +// They are deliberately kept out of the core store.Store interface so that: +// - backends that do not need cluster coordination (e.g. the single-writer +// SQLite store, or test fakes that embed store.Store) are unaffected; +// - callers degrade gracefully via a type assertion: when the capability is +// absent the caller falls back to the historical single-process behavior, +// which is correct for a single replica. +// +// See /scion-volumes/scratchpad/postgres-integration/CONCURRENCY-AUDIT.md for +// the per-site mapping of which primitive guards which read-modify-write path. + +// AdvisoryLockKey identifies a piece of cluster-wide-once work. Keys must be +// stable across releases and unique per logical job, because they are passed to +// pg_try_advisory_lock as the lock identifier. The chosen values are arbitrary +// but fixed; the 0x5C10 ("SCIO") prefix namespaces them away from any advisory +// keys a future feature might pick. +type AdvisoryLockKey int64 + +const ( + // LockScheduleEvaluator guards the recurring schedule-evaluator tick so a + // single replica claims and fires due schedules per tick. + LockScheduleEvaluator AdvisoryLockKey = 0x5C100001 + // LockAgentHeartbeatTimeout guards the stale-agent → offline sweep. + LockAgentHeartbeatTimeout AdvisoryLockKey = 0x5C100002 + // LockAgentStalledDetection guards the stalled-agent sweep. + LockAgentStalledDetection AdvisoryLockKey = 0x5C100003 + // LockSoftDeletePurge guards the soft-deleted-agent / old-event purge. + LockSoftDeletePurge AdvisoryLockKey = 0x5C100004 + // LockGitHubAppHealthCheck guards the periodic GitHub App installation + // health check. + LockGitHubAppHealthCheck AdvisoryLockKey = 0x5C100005 + // LockBrokerAffinityReap guards the stale broker-affinity + stuck dispatch reaper. + LockBrokerAffinityReap AdvisoryLockKey = 0x5C100006 + // LockBrokerMessageSweep guards the periodic stuck-pending-message sweep (B5-2). + LockBrokerMessageSweep AdvisoryLockKey = 0x5C100007 + + // LockWorkspaceProvision is the CLASS ID for per-project workspace + // provisioning locks. It is used with the two-int advisory lock form + // pg_try_advisory_lock(classid, objid), where classid is this constant + // and objid is a stable hash of the project ID. This guards the NFS + // first-access provisioning flow (design §7, risk RN1): only one + // broker across all nodes may clone/provision a project's workspace at + // a time, while different projects lock independently. + // + // The value is intentionally in a different range (0x5C10_1001) from + // the singleton keys above (0x5C10_0001..0005) to avoid collisions + // when the two-int lock form's classid is compared against the + // single-int form's key — Postgres treats them as separate namespaces, + // but keeping them visually distinct aids debugging. + LockWorkspaceProvision AdvisoryLockKey = 0x5C101001 +) + +// AdvisoryLocker is implemented by backends that can take a cluster-wide +// advisory lock. It is the singleton/leader primitive for "run this work on +// exactly one replica per tick" jobs (schedule tick, maintenance, cleanup). +// +// On Postgres this is backed by session-level pg_try_advisory_lock held on a +// dedicated connection for the lifetime of the returned release func. On +// single-writer backends (SQLite) the lock is a no-op that always succeeds: +// there is only ever one writer, so the work is already effectively singleton. +type AdvisoryLocker interface { + // TryAdvisoryLock attempts to acquire the named advisory lock without + // blocking. If acquired is true the caller owns the lock and MUST call the + // returned release func exactly once when the critical section ends + // (release is always non-nil and safe to call even when acquired is false). + // If acquired is false another replica currently holds the lock and the + // caller should skip the work this round. + TryAdvisoryLock(ctx context.Context, key AdvisoryLockKey) (acquired bool, release func() error, err error) + + // TryAdvisoryLockObject acquires a per-object advisory lock using + // Postgres's two-integer form: pg_try_advisory_lock(classid, objid). + // classid identifies the lock family (e.g. LockWorkspaceProvision) and + // objid identifies the specific object within that family (e.g. a + // stable hash of the project ID). Two different objIDs under the same + // classid are independent locks; the same (classid, objid) pair + // provides mutual exclusion across all replicas. + // + // This is the per-project provisioning guard (design §7, risk RN1): + // two agents for the same project on different nodes contend on the + // same (classid, hash(projectID)) lock; agents for different projects + // never contend. + // + // On SQLite the lock is a no-op that always succeeds — the single- + // writer model already serializes provisioning. + TryAdvisoryLockObject(ctx context.Context, classID AdvisoryLockKey, objID int32) (acquired bool, release func() error, err error) +} + +// StableProjectHash returns a deterministic, cross-node-stable int32 hash +// of a project ID string, suitable for use as the objID argument to +// TryAdvisoryLockObject. It uses FNV-32a, which is fast, deterministic, +// and has good distribution for UUID strings. +// +// The result is cast to int32 (Postgres int4 range) — FNV-32a produces a +// uint32 which wraps into the negative int32 range, but that is fine: +// pg_try_advisory_lock(int4, int4) accepts any int4 value. +func StableProjectHash(projectID string) int32 { + h := fnv.New32a() + _, _ = h.Write([]byte(projectID)) // hash.Hash.Write never errors + return int32(h.Sum32()) +} + +// NOTE: the SERIALIZABLE + retry-on-serialization-failure primitive (P3-4) is +// provided as a concrete, dialect-aware helper on the Ent-backed store +// (entadapter.CompositeStore.RunSerializable) rather than as a store-level +// interface here, because its callback operates on a *sql.Tx and is intended +// for backend-internal multi-row-invariant paths. No core store path requires +// it today (the hot RMW paths use single-row state_version CAS or SELECT ... +// FOR UPDATE, and cross-row uniqueness is enforced by DB constraints); it is +// kept available and tested for future multi-row invariants. See +// CONCURRENCY-AUDIT.md §"Serializable retry". + +// ScheduledEventClaimer is implemented by backends that can atomically claim a +// one-shot scheduled event for execution. It is the multi-replica dedup +// primitive for the scheduler's in-memory timers: several replicas may each +// recover the same pending event from the database on startup, but only the +// replica whose atomic UPDATE ... WHERE status = 'pending' affects a row may +// execute the event's side effect (deliver a message, dispatch an agent). +type ScheduledEventClaimer interface { + // ClaimScheduledEvent atomically transitions a scheduled event from + // "pending" to claimedStatus. It returns claimed=true if this caller won + // the claim (the conditional UPDATE affected exactly one row), and + // claimed=false if the event was already claimed by another replica, was + // cancelled, or no longer exists. claimedStatus is normally + // ScheduledEventFired or ScheduledEventExpired. + ClaimScheduledEvent(ctx context.Context, id string, claimedStatus string) (claimed bool, err error) +} diff --git a/pkg/store/concurrency_test.go b/pkg/store/concurrency_test.go new file mode 100644 index 000000000..1c51f2689 --- /dev/null +++ b/pkg/store/concurrency_test.go @@ -0,0 +1,93 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package store + +import ( + "testing" +) + +// StableProjectHash must be deterministic: same input → same output. +func TestStableProjectHash_Deterministic(t *testing.T) { + id := "550e8400-e29b-41d4-a716-446655440000" + h1 := StableProjectHash(id) + h2 := StableProjectHash(id) + if h1 != h2 { + t.Errorf("StableProjectHash not deterministic: %d vs %d", h1, h2) + } +} + +// Different project IDs should (almost certainly) produce different hashes. +func TestStableProjectHash_DifferentInputs(t *testing.T) { + h1 := StableProjectHash("project-aaa") + h2 := StableProjectHash("project-bbb") + if h1 == h2 { + t.Errorf("StableProjectHash collision: %q and %q both hash to %d", + "project-aaa", "project-bbb", h1) + } +} + +// The hash must cover the full int32 range (including negative values from +// uint32 → int32 wrap). This is fine — pg_try_advisory_lock(int4, int4) +// accepts any int4 value. +func TestStableProjectHash_AcceptsNegativeRange(t *testing.T) { + // Just verify it doesn't panic and returns a value. + h := StableProjectHash("test-id") + _ = h // any int32 is valid +} + +// Empty string is a valid input (degenerate but must not panic). +func TestStableProjectHash_EmptyString(t *testing.T) { + h := StableProjectHash("") + _ = h // must not panic +} + +// Verify the key constants are in the expected ranges and non-overlapping. +func TestAdvisoryLockKeys_NonOverlapping(t *testing.T) { + singletonKeys := []AdvisoryLockKey{ + LockScheduleEvaluator, + LockAgentHeartbeatTimeout, + LockAgentStalledDetection, + LockSoftDeletePurge, + LockGitHubAppHealthCheck, + } + + seen := make(map[AdvisoryLockKey]bool, len(singletonKeys)+1) + for _, k := range singletonKeys { + if seen[k] { + t.Errorf("duplicate singleton key: %d", k) + } + seen[k] = true + } + + // LockWorkspaceProvision is in a different range from singletons. + if seen[LockWorkspaceProvision] { + t.Errorf("LockWorkspaceProvision %d collides with a singleton key", LockWorkspaceProvision) + } + + // Verify the ranges are visually distinct (different 0x5C10_0xxx vs 0x5C10_1xxx). + for _, k := range singletonKeys { + if int64(k)&0xFFFF0000 != 0x5C100000 { + t.Errorf("singleton key %d (0x%X) not in expected range 0x5C10_0xxx", k, int64(k)) + } + } + if int64(LockWorkspaceProvision)&0xFFFF0000 != 0x5C100000 { + // Both are in 0x5C10_xxxx but the lower 16 bits distinguish singleton vs per-object. + // LockWorkspaceProvision should be >= 0x5C10_1000. + if int64(LockWorkspaceProvision) < 0x5C101000 { + t.Errorf("LockWorkspaceProvision %d (0x%X) should be >= 0x5C10_1000 to separate from singletons", + LockWorkspaceProvision, int64(LockWorkspaceProvision)) + } + } +} diff --git a/pkg/store/entadapter/agent_store.go b/pkg/store/entadapter/agent_store.go new file mode 100644 index 000000000..401b8d867 --- /dev/null +++ b/pkg/store/entadapter/agent_store.go @@ -0,0 +1,745 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package entadapter + +import ( + "context" + "encoding/json" + "sync" + "time" + + "entgo.io/ent/dialect" + entsql "entgo.io/ent/dialect/sql" + "github.com/google/uuid" + + "github.com/GoogleCloudPlatform/scion/pkg/ent" + "github.com/GoogleCloudPlatform/scion/pkg/ent/agent" + "github.com/GoogleCloudPlatform/scion/pkg/ent/predicate" + "github.com/GoogleCloudPlatform/scion/pkg/store" +) + +// defaultAgentListLimit and maxAgentListLimit mirror the pagination bounds of +// the legacy SQLite agent store so listing behavior is identical across +// backends. +const ( + defaultAgentListLimit = 50 + maxAgentListLimit = 200 +) + +// AgentStore implements the store.AgentStore sub-interface using the Ent ORM. +// +// It supersedes the former raw-SQL store implementation and is designed for +// multi-replica Postgres deployments: +// - UpdateAgent guards writes with a state_version compare-and-swap so +// concurrent updates surface store.ErrVersionConflict rather than silently +// clobbering each other. +// - The read-modify-write hot paths (UpdateAgentStatus, MarkStaleAgentsOffline, +// MarkStalledAgents) run inside a transaction and take row locks via +// SELECT ... FOR UPDATE (a no-op on SQLite, enforced on Postgres). +// - Soft-deleted agents (deleted_at IS NOT NULL) are excluded from default +// listings via an Ent predicate. +type AgentStore struct { + client *ent.Client + + // dialect is detected lazily on first use of a lock-taking path and + // memoized. SELECT ... FOR UPDATE is only emitted on Postgres; the SQLite + // driver rejects the clause outright, so it must be elided there. + dialectOnce sync.Once + dialectName string +} + +// NewAgentStore creates a new Ent-backed AgentStore. +func NewAgentStore(client *ent.Client) *AgentStore { + return &AgentStore{client: client} +} + +// usesRowLocks reports whether the backend supports SELECT ... FOR UPDATE. +// The dialect is captured from a no-op selector the first time it is needed. +func (s *AgentStore) usesRowLocks(ctx context.Context) bool { + s.dialectOnce.Do(func() { + _, _ = s.client.Agent.Query(). + Where(func(sel *entsql.Selector) { s.dialectName = sel.Dialect() }). + Exist(ctx) + }) + return s.dialectName == dialect.Postgres +} + +// Compile-time assertion that AgentStore satisfies the store.AgentStore +// sub-interface. +var _ store.AgentStore = (*AgentStore)(nil) + +// entAgentToStore converts an Ent Agent entity into a store.Agent model. +func entAgentToStore(a *ent.Agent) *store.Agent { + sa := &store.Agent{ + ID: a.ID.String(), + Slug: a.Slug, + Name: a.Name, + Template: a.Template, + ProjectID: a.ProjectID.String(), + Labels: a.Labels, + Annotations: a.Annotations, + Phase: a.Phase, + Activity: a.Activity, + ToolName: a.ToolName, + ConnectionState: a.ConnectionState, + ContainerStatus: a.ContainerStatus, + RuntimeState: a.RuntimeState, + StalledFromActivity: a.StalledFromActivity, + CurrentTurns: a.CurrentTurns, + CurrentModelCalls: a.CurrentModelCalls, + Image: a.Image, + Detached: a.Detached, + Runtime: a.Runtime, + RuntimeBrokerID: a.RuntimeBrokerID, + WebPTYEnabled: a.WebPtyEnabled, + TaskSummary: a.TaskSummary, + Message: a.Message, + Created: a.Created, + Updated: a.Updated, + Visibility: a.Visibility, + Ancestry: a.Ancestry, + StateVersion: a.StateVersion, + } + if a.CreatedBy != nil { + sa.CreatedBy = a.CreatedBy.String() + } + if a.OwnerID != nil { + sa.OwnerID = a.OwnerID.String() + } + if a.LastSeen != nil { + sa.LastSeen = *a.LastSeen + } + if a.LastActivityEvent != nil { + sa.LastActivityEvent = *a.LastActivityEvent + } + if a.StartedAt != nil { + sa.StartedAt = *a.StartedAt + } + if a.DeletedAt != nil { + sa.DeletedAt = *a.DeletedAt + } + if a.AppliedConfig != "" { + var cfg store.AgentAppliedConfig + if err := json.Unmarshal([]byte(a.AppliedConfig), &cfg); err == nil { + sa.AppliedConfig = &cfg + } + } + return sa +} + +// CreateAgent creates a new agent record. +func (s *AgentStore) CreateAgent(ctx context.Context, a *store.Agent) error { + uid, err := parseUUID(a.ID) + if err != nil { + return err + } + projectUID, err := parseUUID(a.ProjectID) + if err != nil { + return err + } + + now := time.Now() + a.Created = now + a.Updated = now + a.StateVersion = 1 + + create := s.client.Agent.Create(). + SetID(uid). + SetSlug(a.Slug). + SetName(a.Name). + SetTemplate(a.Template). + SetProjectID(projectUID). + SetPhase(a.Phase). + SetActivity(a.Activity). + SetToolName(a.ToolName). + SetConnectionState(a.ConnectionState). + SetContainerStatus(a.ContainerStatus). + SetRuntimeState(a.RuntimeState). + SetStalledFromActivity(a.StalledFromActivity). + SetCurrentTurns(a.CurrentTurns). + SetCurrentModelCalls(a.CurrentModelCalls). + SetImage(a.Image). + SetDetached(a.Detached). + SetRuntime(a.Runtime). + SetRuntimeBrokerID(a.RuntimeBrokerID). + SetWebPtyEnabled(a.WebPTYEnabled). + SetTaskSummary(a.TaskSummary). + SetMessage(a.Message). + SetCreated(now). + SetUpdated(now). + SetStateVersion(a.StateVersion) + + if a.Visibility != "" { + create.SetVisibility(a.Visibility) + } + if a.Labels != nil { + create.SetLabels(a.Labels) + } + if a.Annotations != nil { + create.SetAnnotations(a.Annotations) + } + if len(a.Ancestry) > 0 { + create.SetAncestry(a.Ancestry) + } + if cfg := marshalAppliedConfig(a.AppliedConfig); cfg != "" { + create.SetAppliedConfig(cfg) + } + if !a.LastSeen.IsZero() { + create.SetLastSeen(a.LastSeen) + } + if !a.LastActivityEvent.IsZero() { + create.SetLastActivityEvent(a.LastActivityEvent) + } + if !a.StartedAt.IsZero() { + create.SetStartedAt(a.StartedAt) + } + if !a.DeletedAt.IsZero() { + create.SetDeletedAt(a.DeletedAt) + } + if a.CreatedBy != "" { + createdByUID, err := parseUUID(a.CreatedBy) + if err != nil { + return err + } + create.SetCreatedBy(createdByUID) + } + if a.OwnerID != "" { + ownerUID, err := parseUUID(a.OwnerID) + if err != nil { + return err + } + create.SetOwnerID(ownerUID) + } + + created, err := create.Save(ctx) + if err != nil { + return mapError(err) + } + + a.Created = created.Created + a.Updated = created.Updated + return nil +} + +// GetAgent retrieves an agent by ID. +func (s *AgentStore) GetAgent(ctx context.Context, id string) (*store.Agent, error) { + uid, err := parseGetID(id) + if err != nil { + return nil, err + } + a, err := s.client.Agent.Get(ctx, uid) + if err != nil { + return nil, mapError(err) + } + return entAgentToStore(a), nil +} + +// GetAgentBySlug retrieves an agent by its slug within a project. +func (s *AgentStore) GetAgentBySlug(ctx context.Context, projectID, slug string) (*store.Agent, error) { + projectUID, err := parseUUID(projectID) + if err != nil { + return nil, err + } + a, err := s.client.Agent.Query(). + Where(agent.ProjectIDEQ(projectUID), agent.SlugEQ(slug)). + Only(ctx) + if err != nil { + return nil, mapError(err) + } + return entAgentToStore(a), nil +} + +// UpdateAgent updates an existing agent using optimistic locking on +// state_version. The mutable field set mirrors the legacy SQLite store: +// identity-adjacent operational fields are updated, while immutable lineage +// fields (created_at, created_by, project_id, ancestry) and the sciontool-owned +// counters (current_turns, current_model_calls, started_at) are left untouched. +func (s *AgentStore) UpdateAgent(ctx context.Context, a *store.Agent) error { + uid, err := parseUUID(a.ID) + if err != nil { + return err + } + + now := time.Now() + expectedVersion := a.StateVersion + newVersion := expectedVersion + 1 + + update := s.client.Agent.Update(). + Where(agent.IDEQ(uid), agent.StateVersionEQ(expectedVersion)). + SetSlug(a.Slug). + SetName(a.Name). + SetTemplate(a.Template). + SetPhase(a.Phase). + SetActivity(a.Activity). + SetToolName(a.ToolName). + SetConnectionState(a.ConnectionState). + SetContainerStatus(a.ContainerStatus). + SetRuntimeState(a.RuntimeState). + SetStalledFromActivity(a.StalledFromActivity). + SetImage(a.Image). + SetDetached(a.Detached). + SetRuntime(a.Runtime). + SetRuntimeBrokerID(a.RuntimeBrokerID). + SetWebPtyEnabled(a.WebPTYEnabled). + SetTaskSummary(a.TaskSummary). + SetMessage(a.Message). + SetVisibility(a.Visibility). + SetUpdated(now). + SetStateVersion(newVersion) + + if a.Labels != nil { + update.SetLabels(a.Labels) + } else { + update.ClearLabels() + } + if a.Annotations != nil { + update.SetAnnotations(a.Annotations) + } else { + update.ClearAnnotations() + } + if cfg := marshalAppliedConfig(a.AppliedConfig); cfg != "" { + update.SetAppliedConfig(cfg) + } else { + update.ClearAppliedConfig() + } + if a.LastSeen.IsZero() { + update.ClearLastSeen() + } else { + update.SetLastSeen(a.LastSeen) + } + if a.LastActivityEvent.IsZero() { + update.ClearLastActivityEvent() + } else { + update.SetLastActivityEvent(a.LastActivityEvent) + } + if a.DeletedAt.IsZero() { + update.ClearDeletedAt() + } else { + update.SetDeletedAt(a.DeletedAt) + } + if a.OwnerID == "" { + update.ClearOwnerID() + } else { + ownerUID, err := parseUUID(a.OwnerID) + if err != nil { + return err + } + update.SetOwnerID(ownerUID) + } + + affected, err := update.Save(ctx) + if err != nil { + return mapError(err) + } + if affected == 0 { + // No row matched the (id, state_version) pair. Distinguish a missing + // agent from a stale write so callers can retry conflicts. + exists, existErr := s.client.Agent.Query().Where(agent.IDEQ(uid)).Exist(ctx) + if existErr != nil { + return existErr + } + if !exists { + return store.ErrNotFound + } + return store.ErrVersionConflict + } + + a.Updated = now + a.StateVersion = newVersion + return nil +} + +// DeleteAgent permanently removes an agent by ID (hard delete). +func (s *AgentStore) DeleteAgent(ctx context.Context, id string) error { + uid, err := parseUUID(id) + if err != nil { + return err + } + err = s.client.Agent.DeleteOneID(uid).Exec(ctx) + if err != nil { + return mapError(err) + } + return nil +} + +// ListAgents returns agents matching the filter criteria. +func (s *AgentStore) ListAgents(ctx context.Context, filter store.AgentFilter, opts store.ListOptions) (*store.ListResult[store.Agent], error) { + preds, err := agentFilterPredicates(filter) + if err != nil { + return nil, err + } + + query := s.client.Agent.Query() + if len(preds) > 0 { + query.Where(preds...) + } + + totalCount, err := query.Clone().Count(ctx) + if err != nil { + return nil, err + } + + limit := opts.Limit + if limit <= 0 { + limit = defaultAgentListLimit + } + if limit > maxAgentListLimit { + limit = maxAgentListLimit + } + + // Fetch one extra row to detect whether a further page exists. + rows, err := query. + Order(agent.ByCreated(entsql.OrderDesc())). + Limit(limit + 1). + All(ctx) + if err != nil { + return nil, err + } + + items := make([]store.Agent, 0, len(rows)) + for _, a := range rows { + items = append(items, *entAgentToStore(a)) + } + + result := &store.ListResult[store.Agent]{ + Items: items, + TotalCount: totalCount, + } + if len(items) > limit { + result.Items = items[:limit] + result.NextCursor = items[limit-1].ID + } + return result, nil +} + +// agentFilterPredicates translates a store.AgentFilter into Ent predicates, +// preserving the exact OR/AND composition of the legacy SQLite query. +func agentFilterPredicates(filter store.AgentFilter) ([]predicate.Agent, error) { + var preds []predicate.Agent + + switch { + case len(filter.MemberOrOwnerProjectIDs) > 0: + // (project_id IN (...) OR owner_id = OwnerID) + projectUIDs := parseUUIDList(filter.MemberOrOwnerProjectIDs) + var orParts []predicate.Agent + if len(projectUIDs) > 0 { + orParts = append(orParts, agent.ProjectIDIn(projectUIDs...)) + } + if filter.OwnerID != "" { + ownerUID, err := parseUUID(filter.OwnerID) + if err != nil { + return nil, err + } + orParts = append(orParts, agent.OwnerIDEQ(ownerUID)) + } + if len(orParts) > 0 { + preds = append(preds, agent.Or(orParts...)) + } + case len(filter.MemberProjectIDs) > 0: + projectUIDs := parseUUIDList(filter.MemberProjectIDs) + preds = append(preds, agent.ProjectIDIn(projectUIDs...)) + case filter.OwnerID != "": + ownerUID, err := parseUUID(filter.OwnerID) + if err != nil { + return nil, err + } + preds = append(preds, agent.OwnerIDEQ(ownerUID)) + } + + if filter.ExcludeOwnerID != "" { + excludeUID, err := parseUUID(filter.ExcludeOwnerID) + if err != nil { + return nil, err + } + preds = append(preds, agent.OwnerIDNEQ(excludeUID)) + } + if filter.ProjectID != "" { + projectUID, err := parseUUID(filter.ProjectID) + if err != nil { + return nil, err + } + preds = append(preds, agent.ProjectIDEQ(projectUID)) + } + if filter.RuntimeBrokerID != "" { + preds = append(preds, agent.RuntimeBrokerIDEQ(filter.RuntimeBrokerID)) + } + if filter.Phase != "" { + preds = append(preds, agent.PhaseEQ(filter.Phase)) + } + if filter.AncestorID != "" { + preds = append(preds, ancestryContains(filter.AncestorID)) + } + + // Exclude soft-deleted agents unless explicitly requested. + if !filter.IncludeDeleted { + preds = append(preds, agent.DeletedAtIsNil()) + } + + return preds, nil +} + +// UpdateAgentStatus applies a partial, status-only update. It is the hottest +// agent write path, so it runs as a locked read-modify-write: the row is loaded +// with SELECT ... FOR UPDATE, the legacy sticky/transition rules are applied in +// Go, and the result is written back inside the same transaction. Unlike +// UpdateAgent it does not touch state_version (status churn is not a +// conflict-worthy mutation). +func (s *AgentStore) UpdateAgentStatus(ctx context.Context, id string, su store.AgentStatusUpdate) error { + uid, err := parseUUID(id) + if err != nil { + return err + } + + // Prime dialect detection before opening the transaction: the detection + // probe runs on s.client, which would contend with the open transaction on + // single-connection SQLite. + useLock := s.usesRowLocks(ctx) + + tx, err := s.client.Tx(ctx) + if err != nil { + return err + } + defer func() { _ = tx.Rollback() }() + + q := tx.Agent.Query().Where(agent.IDEQ(uid)) + if useLock { + q = q.ForUpdate() + } + current, err := q.Only(ctx) + if err != nil { + return mapError(err) + } + + now := time.Now() + upd := tx.Agent.UpdateOneID(uid). + SetUpdated(now). + SetLastSeen(now) + + if su.Phase != "" { + upd.SetPhase(su.Phase) + } + + activityProvided := su.Activity != "" + if activityProvided { + // Preserve a terminal/sticky activity: once an agent is stopped with a + // crashed/limits_exceeded activity, a non-terminal status report must + // not overwrite it. + sticky := current.Phase == "stopped" && + isTerminalActivity(current.Activity) && + !isTerminalActivity(su.Activity) + if !sticky { + upd.SetActivity(su.Activity) + } + // A fresh activity report clears any stalled marker and refreshes the + // activity timestamp; tool_name tracks the new activity verbatim. + upd.SetStalledFromActivity("") + upd.SetLastActivityEvent(now) + upd.SetToolName(su.ToolName) + } + + if su.Message != "" { + upd.SetMessage(su.Message) + } + if su.ConnectionState != "" { + upd.SetConnectionState(su.ConnectionState) + } + if su.ContainerStatus != "" { + upd.SetContainerStatus(su.ContainerStatus) + } + if su.RuntimeState != "" { + upd.SetRuntimeState(su.RuntimeState) + } + if su.TaskSummary != "" { + upd.SetTaskSummary(su.TaskSummary) + } + if su.CurrentTurns != nil { + upd.SetCurrentTurns(*su.CurrentTurns) + } + if su.CurrentModelCalls != nil { + upd.SetCurrentModelCalls(*su.CurrentModelCalls) + } + if su.StartedAt != "" { + if t, ok := parseTimeString(su.StartedAt); ok { + upd.SetStartedAt(t) + } + } + + if err := upd.Exec(ctx); err != nil { + return mapError(err) + } + return tx.Commit() +} + +// PurgeDeletedAgents permanently removes soft-deleted agents older than cutoff. +func (s *AgentStore) PurgeDeletedAgents(ctx context.Context, cutoff time.Time) (int, error) { + deleted, err := s.client.Agent.Delete(). + Where(agent.DeletedAtNotNil(), agent.DeletedAtLT(cutoff)). + Exec(ctx) + if err != nil { + return 0, err + } + return deleted, nil +} + +// staleOfflineExcluded lists the terminal/sticky activities that must not be +// overwritten when sweeping stale agents to "offline". +var staleOfflineExcluded = []string{"completed", "limits_exceeded", "blocked", "offline"} + +// MarkStaleAgentsOffline marks running agents whose last heartbeat predates +// threshold as offline, returning the updated records for event publishing. +func (s *AgentStore) MarkStaleAgentsOffline(ctx context.Context, threshold time.Time) ([]store.Agent, error) { + useLock := s.usesRowLocks(ctx) + + tx, err := s.client.Tx(ctx) + if err != nil { + return nil, err + } + defer func() { _ = tx.Rollback() }() + + now := time.Now() + + q := tx.Agent.Query().Where( + agent.LastSeenNotNil(), + agent.LastSeenLT(threshold), + agent.PhaseEQ("running"), + agent.ActivityNotIn(staleOfflineExcluded...), + ) + if useLock { + q = q.ForUpdate() + } + candidates, err := q.All(ctx) + if err != nil { + return nil, err + } + + updated := make([]store.Agent, 0, len(candidates)) + for _, a := range candidates { + if err := tx.Agent.UpdateOneID(a.ID). + SetActivity("offline"). + SetUpdated(now). + Exec(ctx); err != nil { + return nil, err + } + a.Activity = "offline" + a.Updated = now + updated = append(updated, *entAgentToStore(a)) + } + + if err := tx.Commit(); err != nil { + return nil, err + } + return updated, nil +} + +// stalledExcluded lists the activities that disqualify a running agent from +// being marked "stalled" (terminal, already-stalled, or intentionally waiting). +var stalledExcluded = []string{"completed", "limits_exceeded", "blocked", "stalled", "offline", "waiting_for_input"} + +// MarkStalledAgents marks running agents whose last activity event predates +// activityThreshold but whose heartbeat is still recent (>= heartbeatRecency) +// as stalled, preserving the prior activity in stalled_from_activity. +func (s *AgentStore) MarkStalledAgents(ctx context.Context, activityThreshold, heartbeatRecency time.Time) ([]store.Agent, error) { + useLock := s.usesRowLocks(ctx) + + tx, err := s.client.Tx(ctx) + if err != nil { + return nil, err + } + defer func() { _ = tx.Rollback() }() + + now := time.Now() + + q := tx.Agent.Query().Where( + agent.LastActivityEventNotNil(), + agent.LastActivityEventLT(activityThreshold), + agent.LastSeenNotNil(), + agent.LastSeenGTE(heartbeatRecency), + agent.PhaseEQ("running"), + agent.ActivityNotIn(stalledExcluded...), + ) + if useLock { + q = q.ForUpdate() + } + candidates, err := q.All(ctx) + if err != nil { + return nil, err + } + + updated := make([]store.Agent, 0, len(candidates)) + for _, a := range candidates { + prevActivity := a.Activity + if err := tx.Agent.UpdateOneID(a.ID). + SetStalledFromActivity(prevActivity). + SetActivity("stalled"). + SetUpdated(now). + Exec(ctx); err != nil { + return nil, err + } + a.StalledFromActivity = prevActivity + a.Activity = "stalled" + a.Updated = now + updated = append(updated, *entAgentToStore(a)) + } + + if err := tx.Commit(); err != nil { + return nil, err + } + return updated, nil +} + +// --- helpers --- + +// isTerminalActivity reports whether the activity is a terminal/sticky state +// that a non-terminal status report must not overwrite on a stopped agent. +func isTerminalActivity(activity string) bool { + return activity == "crashed" || activity == "limits_exceeded" +} + +// marshalAppliedConfig serializes the applied-config document to JSON text, +// returning "" for a nil config so the column is left empty. +func marshalAppliedConfig(cfg *store.AgentAppliedConfig) string { + if cfg == nil { + return "" + } + data, err := json.Marshal(cfg) + if err != nil { + return "" + } + return string(data) +} + +// parseTimeString parses a status update's started_at string, accepting the +// RFC3339 forms the legacy store persisted. It reports false when the value is +// unparseable so the caller leaves the field unchanged. +func parseTimeString(s string) (time.Time, bool) { + for _, layout := range []string{time.RFC3339Nano, time.RFC3339} { + if t, err := time.Parse(layout, s); err == nil { + return t, true + } + } + return time.Time{}, false +} + +// parseUUIDList parses a list of string UUIDs, silently skipping any that are +// malformed (mirroring the lenient handling of the legacy IN (...) filters). +func parseUUIDList(ids []string) []uuid.UUID { + out := make([]uuid.UUID, 0, len(ids)) + for _, id := range ids { + if uid, err := uuid.Parse(id); err == nil { + out = append(out, uid) + } + } + return out +} diff --git a/pkg/store/entadapter/agent_store_test.go b/pkg/store/entadapter/agent_store_test.go new file mode 100644 index 000000000..a36422fce --- /dev/null +++ b/pkg/store/entadapter/agent_store_test.go @@ -0,0 +1,403 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//go:build !no_sqlite + +package entadapter + +import ( + "context" + "testing" + "time" + + "github.com/GoogleCloudPlatform/scion/pkg/store" + "github.com/GoogleCloudPlatform/scion/pkg/store/enttest" + "github.com/google/uuid" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +var agentTestProjectUID = uuid.MustParse("30000000-0000-0000-0000-0000000000a1") + +// newTestAgentStore returns a fresh Ent-backed AgentStore with a single project +// seeded to satisfy the required project FK. MaxOpenConns is pinned to 1 so the +// in-memory SQLite backend serializes the transactional RMW paths. +func newTestAgentStore(t *testing.T) (*AgentStore, string) { + t.Helper() + client := enttest.NewClient(t) + + _, err := client.Project.Create(). + SetID(agentTestProjectUID). + SetName("test-project"). + SetSlug("test-project"). + Save(context.Background()) + require.NoError(t, err) + + return NewAgentStore(client), agentTestProjectUID.String() +} + +// makeAgent builds a minimal valid agent for the seeded project. +func makeAgent(projectID, slug string) *store.Agent { + return &store.Agent{ + ID: uuid.NewString(), + Slug: slug, + Name: "Agent " + slug, + Template: "default", + ProjectID: projectID, + Phase: "running", + Activity: "thinking", + Labels: map[string]string{"k": "v"}, + } +} + +func TestAgentStore_CRUD(t *testing.T) { + ctx := context.Background() + s, projectID := newTestAgentStore(t) + + a := makeAgent(projectID, "crud-1") + a.AppliedConfig = &store.AgentAppliedConfig{Image: "img:1", Model: "opus"} + require.NoError(t, s.CreateAgent(ctx, a)) + assert.Equal(t, int64(1), a.StateVersion, "CreateAgent should initialize state_version to 1") + assert.False(t, a.Created.IsZero()) + + // Get by ID round-trips all the fields we set. + got, err := s.GetAgent(ctx, a.ID) + require.NoError(t, err) + assert.Equal(t, a.Slug, got.Slug) + assert.Equal(t, a.Name, got.Name) + assert.Equal(t, a.ProjectID, got.ProjectID) + assert.Equal(t, "running", got.Phase) + assert.Equal(t, map[string]string{"k": "v"}, got.Labels) + require.NotNil(t, got.AppliedConfig) + assert.Equal(t, "img:1", got.AppliedConfig.Image) + assert.Equal(t, "opus", got.AppliedConfig.Model) + + // Get by slug. + bySlug, err := s.GetAgentBySlug(ctx, projectID, "crud-1") + require.NoError(t, err) + assert.Equal(t, a.ID, bySlug.ID) + + // Missing IDs surface as ErrNotFound. + _, err = s.GetAgent(ctx, uuid.NewString()) + assert.ErrorIs(t, err, store.ErrNotFound) + _, err = s.GetAgentBySlug(ctx, projectID, "does-not-exist") + assert.ErrorIs(t, err, store.ErrNotFound) + + // Update bumps state_version and persists changes. + got.Name = "Renamed" + got.Phase = "stopped" + require.NoError(t, s.UpdateAgent(ctx, got)) + assert.Equal(t, int64(2), got.StateVersion) + + reread, err := s.GetAgent(ctx, a.ID) + require.NoError(t, err) + assert.Equal(t, "Renamed", reread.Name) + assert.Equal(t, "stopped", reread.Phase) + assert.Equal(t, int64(2), reread.StateVersion) + + // Delete is a hard delete. + require.NoError(t, s.DeleteAgent(ctx, a.ID)) + _, err = s.GetAgent(ctx, a.ID) + assert.ErrorIs(t, err, store.ErrNotFound) + assert.ErrorIs(t, s.DeleteAgent(ctx, a.ID), store.ErrNotFound) +} + +// TestAgentStore_CreatedByNonUserPrincipal guards against the regression where +// created_by/owner_id carried a foreign-key edge to the users table. When an +// agent creates a sub-agent, those columns hold the *creating agent's* ID, which +// has no users-table row — under the FK that produced a constraint violation +// (mapped to ErrInvalidInput → a 400 "Invalid input" on agent creation). They +// are polymorphic principal references and must accept an arbitrary principal ID. +func TestAgentStore_CreatedByNonUserPrincipal(t *testing.T) { + ctx := context.Background() + s, projectID := newTestAgentStore(t) + + // A principal ID that is NOT a user (e.g. another agent). No users row exists. + creatorPrincipalID := uuid.NewString() + + a := makeAgent(projectID, "sub-agent") + a.CreatedBy = creatorPrincipalID + a.OwnerID = creatorPrincipalID + require.NoError(t, s.CreateAgent(ctx, a), + "creating an agent owned by a non-user principal must not violate a foreign key") + + got, err := s.GetAgent(ctx, a.ID) + require.NoError(t, err) + assert.Equal(t, creatorPrincipalID, got.CreatedBy) + assert.Equal(t, creatorPrincipalID, got.OwnerID) +} + +// TestAgentStore_AncestryFilter exercises the dialect-switched json_each / +// json_array_elements_text membership filter. +func TestAgentStore_AncestryFilter(t *testing.T) { + ctx := context.Background() + s, projectID := newTestAgentStore(t) + + root := "user-root" + mid := "agent-mid" + + // child is a descendant of both root and mid. + child := makeAgent(projectID, "child") + child.Ancestry = []string{root, mid} + require.NoError(t, s.CreateAgent(ctx, child)) + + // sibling descends only from root. + sibling := makeAgent(projectID, "sibling") + sibling.Ancestry = []string{root} + require.NoError(t, s.CreateAgent(ctx, sibling)) + + // orphan has no ancestry at all. + orphan := makeAgent(projectID, "orphan") + require.NoError(t, s.CreateAgent(ctx, orphan)) + + // Filtering by root returns both descendants but not the orphan. + byRoot, err := s.ListAgents(ctx, store.AgentFilter{AncestorID: root}, store.ListOptions{}) + require.NoError(t, err) + assert.Equal(t, 2, byRoot.TotalCount) + assert.ElementsMatch(t, []string{child.ID, sibling.ID}, ids(byRoot.Items)) + + // Filtering by mid returns only the child. + byMid, err := s.ListAgents(ctx, store.AgentFilter{AncestorID: mid}, store.ListOptions{}) + require.NoError(t, err) + assert.Equal(t, 1, byMid.TotalCount) + require.Len(t, byMid.Items, 1) + assert.Equal(t, child.ID, byMid.Items[0].ID) + + // An ancestor that matches nobody returns no rows. + none, err := s.ListAgents(ctx, store.AgentFilter{AncestorID: "nobody"}, store.ListOptions{}) + require.NoError(t, err) + assert.Equal(t, 0, none.TotalCount) + assert.Empty(t, none.Items) +} + +// TestAgentStore_SoftDeleteExclusion verifies soft-deleted agents are hidden +// from default listings but returned when explicitly included. +func TestAgentStore_SoftDeleteExclusion(t *testing.T) { + ctx := context.Background() + s, projectID := newTestAgentStore(t) + + live := makeAgent(projectID, "live") + require.NoError(t, s.CreateAgent(ctx, live)) + + gone := makeAgent(projectID, "gone") + require.NoError(t, s.CreateAgent(ctx, gone)) + + // Soft-delete via UpdateAgent setting DeletedAt. + gone.DeletedAt = time.Now() + require.NoError(t, s.UpdateAgent(ctx, gone)) + + // Default listing excludes the soft-deleted agent. + def, err := s.ListAgents(ctx, store.AgentFilter{}, store.ListOptions{}) + require.NoError(t, err) + assert.Equal(t, 1, def.TotalCount) + require.Len(t, def.Items, 1) + assert.Equal(t, live.ID, def.Items[0].ID) + + // IncludeDeleted brings it back. + incl, err := s.ListAgents(ctx, store.AgentFilter{IncludeDeleted: true}, store.ListOptions{}) + require.NoError(t, err) + assert.Equal(t, 2, incl.TotalCount) + assert.ElementsMatch(t, []string{live.ID, gone.ID}, ids(incl.Items)) +} + +// TestAgentStore_OptimisticLockConflict verifies the state_version CAS guard: +// a second update issued against a stale version is rejected with +// ErrVersionConflict rather than silently overwriting the first. +func TestAgentStore_OptimisticLockConflict(t *testing.T) { + ctx := context.Background() + s, projectID := newTestAgentStore(t) + + a := makeAgent(projectID, "locked") + require.NoError(t, s.CreateAgent(ctx, a)) + + // Two readers load the same version (1). + readerA, err := s.GetAgent(ctx, a.ID) + require.NoError(t, err) + readerB, err := s.GetAgent(ctx, a.ID) + require.NoError(t, err) + require.Equal(t, int64(1), readerA.StateVersion) + require.Equal(t, int64(1), readerB.StateVersion) + + // First writer wins and advances the version to 2. + readerA.Name = "WriterA" + require.NoError(t, s.UpdateAgent(ctx, readerA)) + assert.Equal(t, int64(2), readerA.StateVersion) + + // Second writer holds the now-stale version 1 and must conflict. + readerB.Name = "WriterB" + err = s.UpdateAgent(ctx, readerB) + assert.ErrorIs(t, err, store.ErrVersionConflict) + + // The losing write left no trace. + final, err := s.GetAgent(ctx, a.ID) + require.NoError(t, err) + assert.Equal(t, "WriterA", final.Name) + assert.Equal(t, int64(2), final.StateVersion) + + // Updating a non-existent agent reports ErrNotFound, not a conflict. + ghost := makeAgent(projectID, "ghost") + ghost.StateVersion = 1 + assert.ErrorIs(t, s.UpdateAgent(ctx, ghost), store.ErrNotFound) +} + +func TestAgentStore_UpdateAgentStatus(t *testing.T) { + ctx := context.Background() + s, projectID := newTestAgentStore(t) + + a := makeAgent(projectID, "status") + a.Activity = "thinking" + require.NoError(t, s.CreateAgent(ctx, a)) + + // A normal status report updates activity, tool, and refreshes last_seen. + require.NoError(t, s.UpdateAgentStatus(ctx, a.ID, store.AgentStatusUpdate{ + Activity: "executing", + ToolName: "Bash", + })) + got, err := s.GetAgent(ctx, a.ID) + require.NoError(t, err) + assert.Equal(t, "executing", got.Activity) + assert.Equal(t, "Bash", got.ToolName) + assert.False(t, got.LastSeen.IsZero(), "last_seen should be refreshed") + assert.False(t, got.LastActivityEvent.IsZero(), "last_activity_event should be set") + + // Drive the agent to a terminal sticky state. + require.NoError(t, s.UpdateAgentStatus(ctx, a.ID, store.AgentStatusUpdate{ + Phase: "stopped", + Activity: "crashed", + })) + // A subsequent non-terminal report must NOT overwrite the sticky activity. + require.NoError(t, s.UpdateAgentStatus(ctx, a.ID, store.AgentStatusUpdate{ + Activity: "thinking", + })) + got, err = s.GetAgent(ctx, a.ID) + require.NoError(t, err) + assert.Equal(t, "crashed", got.Activity, "terminal activity must stick") + + // Unknown agent reports ErrNotFound. + assert.ErrorIs(t, s.UpdateAgentStatus(ctx, uuid.NewString(), store.AgentStatusUpdate{Phase: "running"}), store.ErrNotFound) +} + +func TestAgentStore_MarkStaleAgentsOffline(t *testing.T) { + ctx := context.Background() + s, projectID := newTestAgentStore(t) + + old := time.Now().Add(-1 * time.Hour) + threshold := time.Now().Add(-30 * time.Minute) + + // Stale running agent with an old heartbeat -> should be marked offline. + stale := makeAgent(projectID, "stale") + stale.Phase = "running" + stale.Activity = "thinking" + stale.LastSeen = old + require.NoError(t, s.CreateAgent(ctx, stale)) + + // Recent agent -> untouched. + fresh := makeAgent(projectID, "fresh") + fresh.Phase = "running" + fresh.Activity = "thinking" + fresh.LastSeen = time.Now() + require.NoError(t, s.CreateAgent(ctx, fresh)) + + // Already-completed agent -> sticky, untouched. + done := makeAgent(projectID, "done") + done.Phase = "running" + done.Activity = "completed" + done.LastSeen = old + require.NoError(t, s.CreateAgent(ctx, done)) + + updated, err := s.MarkStaleAgentsOffline(ctx, threshold) + require.NoError(t, err) + require.Len(t, updated, 1) + assert.Equal(t, stale.ID, updated[0].ID) + assert.Equal(t, "offline", updated[0].Activity) + + gotFresh, _ := s.GetAgent(ctx, fresh.ID) + assert.Equal(t, "thinking", gotFresh.Activity) + gotDone, _ := s.GetAgent(ctx, done.ID) + assert.Equal(t, "completed", gotDone.Activity) +} + +func TestAgentStore_MarkStalledAgents(t *testing.T) { + ctx := context.Background() + s, projectID := newTestAgentStore(t) + + now := time.Now() + activityThreshold := now.Add(-15 * time.Minute) + heartbeatRecency := now.Add(-2 * time.Minute) + + // Recent heartbeat but stale activity -> stalled. + stalled := makeAgent(projectID, "stalled") + stalled.Phase = "running" + stalled.Activity = "executing" + stalled.LastActivityEvent = now.Add(-30 * time.Minute) + stalled.LastSeen = now + require.NoError(t, s.CreateAgent(ctx, stalled)) + + // Active recently -> untouched. + active := makeAgent(projectID, "active") + active.Phase = "running" + active.Activity = "executing" + active.LastActivityEvent = now + active.LastSeen = now + require.NoError(t, s.CreateAgent(ctx, active)) + + updated, err := s.MarkStalledAgents(ctx, activityThreshold, heartbeatRecency) + require.NoError(t, err) + require.Len(t, updated, 1) + assert.Equal(t, stalled.ID, updated[0].ID) + assert.Equal(t, "stalled", updated[0].Activity) + assert.Equal(t, "executing", updated[0].StalledFromActivity, "prior activity should be preserved") + + gotActive, _ := s.GetAgent(ctx, active.ID) + assert.Equal(t, "executing", gotActive.Activity) +} + +func TestAgentStore_PurgeDeletedAgents(t *testing.T) { + ctx := context.Background() + s, projectID := newTestAgentStore(t) + + // Old soft-deleted agent -> purged. + oldDeleted := makeAgent(projectID, "old-deleted") + require.NoError(t, s.CreateAgent(ctx, oldDeleted)) + oldDeleted.DeletedAt = time.Now().Add(-48 * time.Hour) + require.NoError(t, s.UpdateAgent(ctx, oldDeleted)) + + // Recently soft-deleted agent -> retained. + recentDeleted := makeAgent(projectID, "recent-deleted") + require.NoError(t, s.CreateAgent(ctx, recentDeleted)) + recentDeleted.DeletedAt = time.Now().Add(-1 * time.Hour) + require.NoError(t, s.UpdateAgent(ctx, recentDeleted)) + + // Live agent -> retained. + live := makeAgent(projectID, "live") + require.NoError(t, s.CreateAgent(ctx, live)) + + purged, err := s.PurgeDeletedAgents(ctx, time.Now().Add(-24*time.Hour)) + require.NoError(t, err) + assert.Equal(t, 1, purged) + + _, err = s.GetAgent(ctx, oldDeleted.ID) + assert.ErrorIs(t, err, store.ErrNotFound) + _, err = s.GetAgent(ctx, recentDeleted.ID) + assert.NoError(t, err) +} + +// ids extracts the agent IDs from a slice for order-independent comparison. +func ids(agents []store.Agent) []string { + out := make([]string, len(agents)) + for i := range agents { + out[i] = agents[i].ID + } + return out +} diff --git a/pkg/store/entadapter/allowlist_store.go b/pkg/store/entadapter/allowlist_store.go new file mode 100644 index 000000000..ea61cb281 --- /dev/null +++ b/pkg/store/entadapter/allowlist_store.go @@ -0,0 +1,588 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package entadapter + +import ( + "context" + "fmt" + "sort" + "strings" + "time" + + "entgo.io/ent/dialect/sql" + "github.com/GoogleCloudPlatform/scion/pkg/ent" + "github.com/GoogleCloudPlatform/scion/pkg/ent/allowlistentry" + "github.com/GoogleCloudPlatform/scion/pkg/ent/invitecode" + "github.com/GoogleCloudPlatform/scion/pkg/ent/predicate" + "github.com/GoogleCloudPlatform/scion/pkg/ent/user" + "github.com/GoogleCloudPlatform/scion/pkg/store" + "github.com/google/uuid" +) + +// AllowListStore implements store.AllowListStore and store.InviteCodeStore using +// Ent ORM. Both interfaces are co-located because the invite-only access control +// flow (allow list + invite codes) is a single logical domain. +type AllowListStore struct { + client *ent.Client +} + +// NewAllowListStore creates a new Ent-backed AllowListStore. +func NewAllowListStore(client *ent.Client) *AllowListStore { + return &AllowListStore{client: client} +} + +// entAllowListToStore converts an Ent AllowListEntry to a store model. +func entAllowListToStore(e *ent.AllowListEntry) *store.AllowListEntry { + return &store.AllowListEntry{ + ID: e.ID.String(), + Email: e.Email, + Note: e.Note, + AddedBy: e.AddedBy, + InviteID: e.InviteID, + Created: e.Created, + } +} + +// entInviteToStore converts an Ent InviteCode to a store model. +func entInviteToStore(i *ent.InviteCode) *store.InviteCode { + return &store.InviteCode{ + ID: i.ID.String(), + CodeHash: i.CodeHash, + CodePrefix: i.CodePrefix, + MaxUses: i.MaxUses, + UseCount: i.UseCount, + ExpiresAt: i.ExpiresAt, + Revoked: i.Revoked, + CreatedBy: i.CreatedBy, + Note: i.Note, + Created: i.Created, + } +} + +// ============================================================================ +// Allow List Operations +// ============================================================================ + +// AddAllowListEntry adds a single email to the allow list. +func (s *AllowListStore) AddAllowListEntry(ctx context.Context, entry *store.AllowListEntry) error { + uid, err := parseUUID(entry.ID) + if err != nil { + return err + } + if entry.Created.IsZero() { + entry.Created = time.Now() + } + entry.Email = normalizeEmail(entry.Email) + + create := s.client.AllowListEntry.Create(). + SetID(uid). + SetEmail(entry.Email). + SetNote(entry.Note). + SetAddedBy(entry.AddedBy). + SetCreated(entry.Created) + if entry.InviteID != "" { + create.SetInviteID(entry.InviteID) + } + + if err := create.Exec(ctx); err != nil { + return mapError(err) + } + return nil +} + +// RemoveAllowListEntry removes an email from the allow list. +func (s *AllowListStore) RemoveAllowListEntry(ctx context.Context, email string) error { + n, err := s.client.AllowListEntry.Delete(). + Where(allowlistentry.EmailEQ(normalizeEmail(email))). + Exec(ctx) + if err != nil { + return err + } + if n == 0 { + return store.ErrNotFound + } + return nil +} + +// GetAllowListEntry retrieves an allow list entry by email. +func (s *AllowListStore) GetAllowListEntry(ctx context.Context, email string) (*store.AllowListEntry, error) { + e, err := s.client.AllowListEntry.Query(). + Where(allowlistentry.EmailEQ(normalizeEmail(email))). + Only(ctx) + if err != nil { + return nil, mapError(err) + } + return entAllowListToStore(e), nil +} + +// ListAllowListEntries returns a keyset-paginated page of allow list entries +// ordered by (created DESC, id DESC), matching the legacy SQLite store. +func (s *AllowListStore) ListAllowListEntries(ctx context.Context, opts store.ListOptions) (*store.ListResult[store.AllowListEntry], error) { + totalCount, err := s.client.AllowListEntry.Query().Count(ctx) + if err != nil { + return nil, err + } + + limit := opts.Limit + if limit <= 0 { + limit = 50 + } + + query := s.client.AllowListEntry.Query() + if opts.Cursor != "" { + pred, err := s.allowListCursorPredicate(ctx, opts.Cursor) + if err != nil { + return nil, err + } + query.Where(pred) + } + + entries, err := query. + Order(allowlistentry.ByCreated(sql.OrderDesc()), allowlistentry.ByID(sql.OrderDesc())). + Limit(limit + 1). + All(ctx) + if err != nil { + return nil, err + } + + items := make([]store.AllowListEntry, 0, len(entries)) + for _, e := range entries { + items = append(items, *entAllowListToStore(e)) + } + + result := &store.ListResult[store.AllowListEntry]{ + Items: items, + TotalCount: totalCount, + } + if len(items) > limit { + result.NextCursor = items[limit-1].ID + result.Items = items[:limit] + } + return result, nil +} + +// allowListCursorPredicate builds the keyset predicate for paginating after the +// entry identified by cursor (an entry ID). +func (s *AllowListStore) allowListCursorPredicate(ctx context.Context, cursor string) (predicate.AllowListEntry, error) { + cursorUID, err := parseUUID(cursor) + if err != nil { + return nil, fmt.Errorf("invalid cursor: %w", err) + } + c, err := s.client.AllowListEntry.Get(ctx, cursorUID) + if err != nil { + return nil, fmt.Errorf("invalid cursor: %w", mapError(err)) + } + return allowlistentry.Or( + allowlistentry.CreatedLT(c.Created), + allowlistentry.And(allowlistentry.CreatedEQ(c.Created), allowlistentry.IDLT(cursorUID)), + ), nil +} + +// IsEmailAllowListed reports whether an email is present on the allow list. +func (s *AllowListStore) IsEmailAllowListed(ctx context.Context, email string) (bool, error) { + return s.client.AllowListEntry.Query(). + Where(allowlistentry.EmailEQ(normalizeEmail(email))). + Exist(ctx) +} + +// BulkAddAllowListEntries inserts many entries idempotently, skipping any whose +// email already exists. It mirrors the legacy `INSERT OR IGNORE` behavior: +// duplicate emails (already present or repeated within the batch) are counted as +// skipped rather than erroring. OnConflict().Ignore() additionally makes the +// bulk insert safe against rows inserted concurrently between the existence +// check and the write. +func (s *AllowListStore) BulkAddAllowListEntries(ctx context.Context, entries []*store.AllowListEntry) (int, int, error) { + now := time.Now() + for _, e := range entries { + e.Email = normalizeEmail(e.Email) + if e.Created.IsZero() { + e.Created = now + } + } + + // Determine which emails already exist so we can report accurate counts. + emails := make([]string, 0, len(entries)) + for _, e := range entries { + emails = append(emails, e.Email) + } + existingRows, err := s.client.AllowListEntry.Query(). + Where(allowlistentry.EmailIn(emails...)). + Select(allowlistentry.FieldEmail). + Strings(ctx) + if err != nil { + return 0, 0, err + } + seen := make(map[string]bool, len(existingRows)) + for _, e := range existingRows { + seen[e] = true + } + + bulk := make([]*ent.AllowListEntryCreate, 0, len(entries)) + added, skipped := 0, 0 + for _, e := range entries { + if seen[e.Email] { + skipped++ + continue + } + seen[e.Email] = true // dedupe repeats within the same batch + + uid, err := parseUUID(e.ID) + if err != nil { + return added, skipped, err + } + create := s.client.AllowListEntry.Create(). + SetID(uid). + SetEmail(e.Email). + SetNote(e.Note). + SetAddedBy(e.AddedBy). + SetCreated(e.Created) + if e.InviteID != "" { + create.SetInviteID(e.InviteID) + } + bulk = append(bulk, create) + added++ + } + + if len(bulk) > 0 { + if err := s.client.AllowListEntry.CreateBulk(bulk...). + OnConflictColumns(allowlistentry.FieldEmail). + Ignore(). + Exec(ctx); err != nil { + return 0, 0, err + } + } + return added, skipped, nil +} + +// ListEmailDomains returns the distinct, sorted set of email domains across all +// users. +func (s *AllowListStore) ListEmailDomains(ctx context.Context) ([]string, error) { + emails, err := s.client.User.Query(). + Select(user.FieldEmail). + Strings(ctx) + if err != nil { + return nil, err + } + + domainSet := make(map[string]struct{}) + for _, e := range emails { + at := strings.LastIndex(e, "@") + if at < 0 || at == len(e)-1 { + continue + } + domainSet[e[at+1:]] = struct{}{} + } + + domains := make([]string, 0, len(domainSet)) + for d := range domainSet { + domains = append(domains, d) + } + sort.Strings(domains) + return domains, nil +} + +// UpdateAllowListEntryInviteID associates an invite code with an allow list entry. +func (s *AllowListStore) UpdateAllowListEntryInviteID(ctx context.Context, email string, inviteID string) error { + n, err := s.client.AllowListEntry.Update(). + Where(allowlistentry.EmailEQ(normalizeEmail(email))). + SetInviteID(inviteID). + Save(ctx) + if err != nil { + return mapError(err) + } + if n == 0 { + return store.ErrNotFound + } + return nil +} + +// ListAllowListEntriesWithInvites returns allow list entries enriched with their +// associated invite code details. invite_id is a plain column (not an Ent edge), +// so the join is performed by batch-loading the referenced invite codes. +func (s *AllowListStore) ListAllowListEntriesWithInvites(ctx context.Context, opts store.ListOptions) (*store.ListResult[store.AllowListEntryWithInvite], error) { + base, err := s.ListAllowListEntries(ctx, opts) + if err != nil { + return nil, err + } + + // Collect referenced invite IDs. + inviteUIDs := make([]uuid.UUID, 0) + for i := range base.Items { + if base.Items[i].InviteID == "" { + continue + } + if uid, err := parseUUID(base.Items[i].InviteID); err == nil { + inviteUIDs = append(inviteUIDs, uid) + } + } + + invitesByID := make(map[string]*ent.InviteCode) + if len(inviteUIDs) > 0 { + invites, err := s.client.InviteCode.Query(). + Where(invitecode.IDIn(inviteUIDs...)). + All(ctx) + if err != nil { + return nil, err + } + for _, inv := range invites { + invitesByID[inv.ID.String()] = inv + } + } + + items := make([]store.AllowListEntryWithInvite, 0, len(base.Items)) + for i := range base.Items { + entry := store.AllowListEntryWithInvite{AllowListEntry: base.Items[i]} + if inv, ok := invitesByID[base.Items[i].InviteID]; ok { + entry.InviteCodePrefix = inv.CodePrefix + entry.InviteMaxUses = inv.MaxUses + entry.InviteUseCount = inv.UseCount + entry.InviteExpiresAt = inv.ExpiresAt + entry.InviteRevoked = inv.Revoked + } + items = append(items, entry) + } + + return &store.ListResult[store.AllowListEntryWithInvite]{ + Items: items, + TotalCount: base.TotalCount, + NextCursor: base.NextCursor, + }, nil +} + +// ============================================================================ +// Invite Code Operations +// ============================================================================ + +// CreateInviteCode creates a new invite code. +func (s *AllowListStore) CreateInviteCode(ctx context.Context, invite *store.InviteCode) error { + uid, err := parseUUID(invite.ID) + if err != nil { + return err + } + if invite.Created.IsZero() { + invite.Created = time.Now() + } + + create := s.client.InviteCode.Create(). + SetID(uid). + SetCodeHash(invite.CodeHash). + SetCodePrefix(invite.CodePrefix). + SetMaxUses(invite.MaxUses). + SetUseCount(invite.UseCount). + SetExpiresAt(invite.ExpiresAt). + SetRevoked(invite.Revoked). + SetCreatedBy(invite.CreatedBy). + SetNote(invite.Note). + SetCreated(invite.Created) + + if err := create.Exec(ctx); err != nil { + return mapError(err) + } + return nil +} + +// GetInviteCodeByHash retrieves an invite code by its hash. +func (s *AllowListStore) GetInviteCodeByHash(ctx context.Context, codeHash string) (*store.InviteCode, error) { + i, err := s.client.InviteCode.Query(). + Where(invitecode.CodeHashEQ(codeHash)). + Only(ctx) + if err != nil { + return nil, mapError(err) + } + return entInviteToStore(i), nil +} + +// GetInviteCode retrieves an invite code by ID. +func (s *AllowListStore) GetInviteCode(ctx context.Context, id string) (*store.InviteCode, error) { + uid, err := parseGetID(id) + if err != nil { + return nil, err + } + i, err := s.client.InviteCode.Get(ctx, uid) + if err != nil { + return nil, mapError(err) + } + return entInviteToStore(i), nil +} + +// ListInviteCodes returns a keyset-paginated page of invite codes ordered by +// (created DESC, id DESC). CodeHash is sensitive and not exposed in listings. +func (s *AllowListStore) ListInviteCodes(ctx context.Context, opts store.ListOptions) (*store.ListResult[store.InviteCode], error) { + totalCount, err := s.client.InviteCode.Query().Count(ctx) + if err != nil { + return nil, err + } + + limit := opts.Limit + if limit <= 0 { + limit = 50 + } + + query := s.client.InviteCode.Query() + if opts.Cursor != "" { + cursorUID, err := parseUUID(opts.Cursor) + if err != nil { + return nil, fmt.Errorf("invalid cursor: %w", err) + } + c, err := s.client.InviteCode.Get(ctx, cursorUID) + if err != nil { + return nil, fmt.Errorf("invalid cursor: %w", mapError(err)) + } + query.Where(invitecode.Or( + invitecode.CreatedLT(c.Created), + invitecode.And(invitecode.CreatedEQ(c.Created), invitecode.IDLT(cursorUID)), + )) + } + + invites, err := query. + Order(invitecode.ByCreated(sql.OrderDesc()), invitecode.ByID(sql.OrderDesc())). + Limit(limit + 1). + All(ctx) + if err != nil { + return nil, err + } + + items := make([]store.InviteCode, 0, len(invites)) + for _, i := range invites { + si := entInviteToStore(i) + si.CodeHash = "" // not exposed in listings, matching the legacy store + items = append(items, *si) + } + + result := &store.ListResult[store.InviteCode]{ + Items: items, + TotalCount: totalCount, + } + if len(items) > limit { + result.NextCursor = items[limit-1].ID + result.Items = items[:limit] + } + return result, nil +} + +// IncrementInviteUseCount atomically increments use_count for an invite code +// that is still redeemable (not revoked, not expired, and below max_uses). +// +// This is expressed as a single conditional UPDATE rather than a +// SELECT-then-UPDATE read-modify-write: the predicate and the increment execute +// in one statement, so the operation is race-free on both SQLite and Postgres +// without needing SELECT ... FOR UPDATE row locking. (The sql/lock feature is +// enabled and ForUpdate is available for genuine multi-statement RMW paths.) +func (s *AllowListStore) IncrementInviteUseCount(ctx context.Context, id string) error { + uid, err := parseUUID(id) + if err != nil { + return err + } + + n, err := s.client.InviteCode.Update(). + Where( + invitecode.IDEQ(uid), + invitecode.RevokedEQ(false), + invitecode.ExpiresAtGT(time.Now()), + // (max_uses = 0 OR use_count < max_uses) — a column-to-column + // comparison expressed via a raw selector predicate. + predicate.InviteCode(func(sel *sql.Selector) { + sel.Where(sql.Or( + sql.EQ(sel.C(invitecode.FieldMaxUses), 0), + sql.ColumnsLT(sel.C(invitecode.FieldUseCount), sel.C(invitecode.FieldMaxUses)), + )) + }), + ). + AddUseCount(1). + Save(ctx) + if err != nil { + return err + } + if n == 0 { + return store.ErrNotFound + } + return nil +} + +// RevokeInviteCode marks an invite code as revoked. +func (s *AllowListStore) RevokeInviteCode(ctx context.Context, id string) error { + uid, err := parseUUID(id) + if err != nil { + return err + } + n, err := s.client.InviteCode.Update(). + Where(invitecode.IDEQ(uid)). + SetRevoked(true). + Save(ctx) + if err != nil { + return err + } + if n == 0 { + return store.ErrNotFound + } + return nil +} + +// DeleteInviteCode removes an invite code by ID. +func (s *AllowListStore) DeleteInviteCode(ctx context.Context, id string) error { + uid, err := parseUUID(id) + if err != nil { + return err + } + if err := s.client.InviteCode.DeleteOneID(uid).Exec(ctx); err != nil { + return mapError(err) + } + return nil +} + +// GetInviteStats returns aggregate statistics about invite codes and the allow +// list. +func (s *AllowListStore) GetInviteStats(ctx context.Context) (*store.InviteStats, error) { + stats := &store.InviteStats{ + RecentRedemptions: []store.InviteCodeInfo{}, + } + + // The invite_codes table is small (admin-managed); load it once ordered by + // recency and derive pending count, total redemptions, and recent + // redemptions in a single pass. + codes, err := s.client.InviteCode.Query(). + Order(invitecode.ByCreated(sql.OrderDesc())). + All(ctx) + if err != nil { + return nil, err + } + + now := time.Now() + for _, c := range codes { + stats.TotalRedemptions += c.UseCount + if !c.Revoked && c.ExpiresAt.After(now) && (c.MaxUses == 0 || c.UseCount < c.MaxUses) { + stats.PendingInvites++ + } + if c.UseCount > 0 && len(stats.RecentRedemptions) < 10 { + stats.RecentRedemptions = append(stats.RecentRedemptions, store.InviteCodeInfo{ + ID: c.ID.String(), + CodePrefix: c.CodePrefix, + UseCount: c.UseCount, + MaxUses: c.MaxUses, + ExpiresAt: c.ExpiresAt, + Note: c.Note, + Created: c.Created, + }) + } + } + + allowCount, err := s.client.AllowListEntry.Query().Count(ctx) + if err != nil { + return nil, err + } + stats.AllowListCount = allowCount + + return stats, nil +} diff --git a/pkg/store/entadapter/broker_affinity_test.go b/pkg/store/entadapter/broker_affinity_test.go new file mode 100644 index 000000000..78efa0774 --- /dev/null +++ b/pkg/store/entadapter/broker_affinity_test.go @@ -0,0 +1,221 @@ +//go:build !no_sqlite + +package entadapter + +import ( + "context" + "testing" + + "github.com/GoogleCloudPlatform/scion/pkg/store" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +// newOfflineBroker returns an unclaimed broker (offline, no affinity) so tests +// can observe the claim transition. +func newOfflineBroker(t *testing.T, ps *ProjectStore) *store.RuntimeBroker { + t.Helper() + b := newBroker() + b.Status = store.BrokerStatusOffline + require.NoError(t, ps.CreateRuntimeBroker(context.Background(), b)) + return b +} + +func TestClaimRuntimeBrokerConnection_SetsAffinityAndOnline(t *testing.T) { + ps := newTestProjectStore(t) + ctx := context.Background() + b := newOfflineBroker(t, ps) + + require.NoError(t, ps.ClaimRuntimeBrokerConnection(ctx, b.ID, "hub-1", "sess-1")) + + got, err := ps.GetRuntimeBroker(ctx, b.ID) + require.NoError(t, err) + require.NotNil(t, got.ConnectedHubID) + assert.Equal(t, "hub-1", *got.ConnectedHubID) + require.NotNil(t, got.ConnectedSessionID) + assert.Equal(t, "sess-1", *got.ConnectedSessionID) + require.NotNil(t, got.ConnectedAt) + assert.False(t, got.ConnectedAt.IsZero()) + // Claim bumps status->online + refreshes heartbeat in the same write. + assert.Equal(t, store.BrokerStatusOnline, got.Status) + assert.False(t, got.LastHeartbeat.IsZero()) +} + +func TestClaimRuntimeBrokerConnection_NewestWins(t *testing.T) { + ps := newTestProjectStore(t) + ctx := context.Background() + b := newOfflineBroker(t, ps) + + require.NoError(t, ps.ClaimRuntimeBrokerConnection(ctx, b.ID, "hub-1", "sess-1")) + require.NoError(t, ps.ClaimRuntimeBrokerConnection(ctx, b.ID, "hub-2", "sess-2")) + + got, err := ps.GetRuntimeBroker(ctx, b.ID) + require.NoError(t, err) + require.NotNil(t, got.ConnectedHubID) + assert.Equal(t, "hub-2", *got.ConnectedHubID) + require.NotNil(t, got.ConnectedSessionID) + assert.Equal(t, "sess-2", *got.ConnectedSessionID) +} + +func TestReleaseRuntimeBrokerConnection_ClearsWhenOwner(t *testing.T) { + ps := newTestProjectStore(t) + ctx := context.Background() + b := newOfflineBroker(t, ps) + + require.NoError(t, ps.ClaimRuntimeBrokerConnection(ctx, b.ID, "hub-1", "sess-1")) + + cleared, err := ps.ReleaseRuntimeBrokerConnection(ctx, b.ID, "hub-1", "sess-1") + require.NoError(t, err) + assert.True(t, cleared) + + got, err := ps.GetRuntimeBroker(ctx, b.ID) + require.NoError(t, err) + assert.Nil(t, got.ConnectedHubID) + assert.Nil(t, got.ConnectedSessionID) + assert.Nil(t, got.ConnectedAt) + // Release must NOT change status — the caller decides offline based on cleared. + assert.Equal(t, store.BrokerStatusOnline, got.Status) +} + +func TestReleaseRuntimeBrokerConnection_NoOpWhenAffinityMoved(t *testing.T) { + ps := newTestProjectStore(t) + ctx := context.Background() + b := newOfflineBroker(t, ps) + + // Affinity currently owned by (hub-2, sess-2). + require.NoError(t, ps.ClaimRuntimeBrokerConnection(ctx, b.ID, "hub-2", "sess-2")) + + // A stale owner (hub-1, sess-1) tries to release: must be a no-op. + cleared, err := ps.ReleaseRuntimeBrokerConnection(ctx, b.ID, "hub-1", "sess-1") + require.NoError(t, err) + assert.False(t, cleared) + + got, err := ps.GetRuntimeBroker(ctx, b.ID) + require.NoError(t, err) + require.NotNil(t, got.ConnectedHubID) + assert.Equal(t, "hub-2", *got.ConnectedHubID) + require.NotNil(t, got.ConnectedSessionID) + assert.Equal(t, "sess-2", *got.ConnectedSessionID) +} + +func TestReleaseRuntimeBrokerConnection_NoOpWhenUnclaimed(t *testing.T) { + ps := newTestProjectStore(t) + ctx := context.Background() + b := newOfflineBroker(t, ps) + + cleared, err := ps.ReleaseRuntimeBrokerConnection(ctx, b.ID, "hub-1", "sess-1") + require.NoError(t, err) + assert.False(t, cleared) +} + +// --------------------------------------------------------------------------- +// ReleaseAndMarkBrokerOffline — atomic release + offline stamp +// --------------------------------------------------------------------------- + +func TestReleaseAndMarkBrokerOffline_StampsOffline(t *testing.T) { + ps := newTestProjectStore(t) + ctx := context.Background() + b := newOfflineBroker(t, ps) + + require.NoError(t, ps.ClaimRuntimeBrokerConnection(ctx, b.ID, "hub-1", "sess-1")) + + cleared, err := ps.ReleaseAndMarkBrokerOffline(ctx, b.ID, "hub-1", "sess-1") + require.NoError(t, err) + assert.True(t, cleared) + + got, err := ps.GetRuntimeBroker(ctx, b.ID) + require.NoError(t, err) + assert.Nil(t, got.ConnectedHubID) + assert.Nil(t, got.ConnectedSessionID) + assert.Nil(t, got.ConnectedAt) + assert.Equal(t, store.BrokerStatusOffline, got.Status) + assert.False(t, got.LastHeartbeat.IsZero()) +} + +func TestReleaseAndMarkBrokerOffline_NoopOnSessionMismatch(t *testing.T) { + ps := newTestProjectStore(t) + ctx := context.Background() + b := newOfflineBroker(t, ps) + + require.NoError(t, ps.ClaimRuntimeBrokerConnection(ctx, b.ID, "hub-1", "sess-NEW")) + + // Stale session tries to release+offline: must be a no-op. + cleared, err := ps.ReleaseAndMarkBrokerOffline(ctx, b.ID, "hub-1", "sess-OLD") + require.NoError(t, err) + assert.False(t, cleared) + + got, err := ps.GetRuntimeBroker(ctx, b.ID) + require.NoError(t, err) + require.NotNil(t, got.ConnectedHubID) + assert.Equal(t, "hub-1", *got.ConnectedHubID) + require.NotNil(t, got.ConnectedSessionID) + assert.Equal(t, "sess-NEW", *got.ConnectedSessionID) + assert.Equal(t, store.BrokerStatusOnline, got.Status, "status must remain online") +} + +// TestReleaseAndMarkBrokerOffline_NoopAfterReclaim reproduces the exact race +// from issue #131: old session releases + stamps offline, but a new session +// has already re-claimed the broker. The stale release must be a no-op. +func TestReleaseAndMarkBrokerOffline_NoopAfterReclaim(t *testing.T) { + ps := newTestProjectStore(t) + ctx := context.Background() + b := newOfflineBroker(t, ps) + + // t0: session A claims. + require.NoError(t, ps.ClaimRuntimeBrokerConnection(ctx, b.ID, "hub-1", "sess-A")) + // t1: session A disconnects, but before the callback runs, session B re-claims. + require.NoError(t, ps.ClaimRuntimeBrokerConnection(ctx, b.ID, "hub-1", "sess-B")) + + // t2: stale callback tries to release+offline for session A. + cleared, err := ps.ReleaseAndMarkBrokerOffline(ctx, b.ID, "hub-1", "sess-A") + require.NoError(t, err) + assert.False(t, cleared, "stale session must not stamp offline") + + got, err := ps.GetRuntimeBroker(ctx, b.ID) + require.NoError(t, err) + require.NotNil(t, got.ConnectedSessionID) + assert.Equal(t, "sess-B", *got.ConnectedSessionID, "new session must still own the broker") + assert.Equal(t, store.BrokerStatusOnline, got.Status, "status must remain online") +} + +func TestReleaseAndMarkBrokerOffline_NoopWhenUnclaimed(t *testing.T) { + ps := newTestProjectStore(t) + ctx := context.Background() + b := newOfflineBroker(t, ps) + + cleared, err := ps.ReleaseAndMarkBrokerOffline(ctx, b.ID, "hub-1", "sess-1") + require.NoError(t, err) + assert.False(t, cleared) +} + +// --------------------------------------------------------------------------- +// Flap / cross-hub scenarios +// --------------------------------------------------------------------------- + +// TestBrokerAffinity_FlapAtoB reproduces the design §9.4 disconnect race: a +// broker flaps from hub A to hub B; A's delayed onDisconnect must NOT clobber +// B's live ownership. +func TestBrokerAffinity_FlapAtoB(t *testing.T) { + ps := newTestProjectStore(t) + ctx := context.Background() + b := newOfflineBroker(t, ps) + + // t0: socket on hub A (session s1). + require.NoError(t, ps.ClaimRuntimeBrokerConnection(ctx, b.ID, "hubA", "s1")) + // t2: broker re-dials, lands on hub B (session s2); B claims (newest wins). + require.NoError(t, ps.ClaimRuntimeBrokerConnection(ctx, b.ID, "hubB", "s2")) + + // t3: hub A's old socket finally errors -> delayed release for (hubA, s1). + cleared, err := ps.ReleaseRuntimeBrokerConnection(ctx, b.ID, "hubA", "s1") + require.NoError(t, err) + assert.False(t, cleared, "stale owner release must be a no-op") + + // Affinity still names B, status still online (no false offline). + got, err := ps.GetRuntimeBroker(ctx, b.ID) + require.NoError(t, err) + require.NotNil(t, got.ConnectedHubID) + assert.Equal(t, "hubB", *got.ConnectedHubID) + require.NotNil(t, got.ConnectedSessionID) + assert.Equal(t, "s2", *got.ConnectedSessionID) + assert.Equal(t, store.BrokerStatusOnline, got.Status) +} diff --git a/pkg/store/entadapter/broker_dispatch_store_test.go b/pkg/store/entadapter/broker_dispatch_store_test.go new file mode 100644 index 000000000..dc2ef53c1 --- /dev/null +++ b/pkg/store/entadapter/broker_dispatch_store_test.go @@ -0,0 +1,252 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//go:build !no_sqlite + +package entadapter + +import ( + "context" + "sync" + "testing" + "time" + + "github.com/GoogleCloudPlatform/scion/pkg/ent" + "github.com/GoogleCloudPlatform/scion/pkg/store" + "github.com/GoogleCloudPlatform/scion/pkg/store/enttest" + "github.com/google/uuid" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func newDispatch(brokerID, op string) *store.BrokerDispatch { + return &store.BrokerDispatch{ + ID: uuid.NewString(), + BrokerID: brokerID, + Op: op, + } +} + +func TestBrokerDispatch_InsertListPending_OnlyPending(t *testing.T) { + client := enttest.NewClient(t) + s := NewBrokerDispatchStore(client) + ctx := context.Background() + brokerA := uuid.NewString() + brokerB := uuid.NewString() + + d1 := newDispatch(brokerA, "start") + d2 := newDispatch(brokerA, "stop") + dOther := newDispatch(brokerB, "start") + require.NoError(t, s.InsertBrokerDispatch(ctx, d1)) + require.NoError(t, s.InsertBrokerDispatch(ctx, d2)) + require.NoError(t, s.InsertBrokerDispatch(ctx, dOther)) + assert.Equal(t, store.DispatchStatePending, d1.State) + + // Claim d1 -> in_progress; it should drop out of the pending drain. + claimed, err := s.ClaimBrokerDispatch(ctx, d1.ID, "hub-1") + require.NoError(t, err) + assert.True(t, claimed) + + pending, err := s.ListPendingDispatch(ctx, brokerA) + require.NoError(t, err) + require.Len(t, pending, 1) + assert.Equal(t, d2.ID, pending[0].ID, "drain returns only pending rows for the broker") +} + +func TestBrokerDispatch_ClaimOnceThenFalse(t *testing.T) { + client := enttest.NewClient(t) + s := NewBrokerDispatchStore(client) + ctx := context.Background() + + d := newDispatch(uuid.NewString(), "start") + require.NoError(t, s.InsertBrokerDispatch(ctx, d)) + + claimed, err := s.ClaimBrokerDispatch(ctx, d.ID, "hub-1") + require.NoError(t, err) + assert.True(t, claimed) + + again, err := s.ClaimBrokerDispatch(ctx, d.ID, "hub-2") + require.NoError(t, err) + assert.False(t, again, "a second claim of a non-pending row must lose") +} + +func TestBrokerDispatch_ConcurrentClaimSingleWinner(t *testing.T) { + client := enttest.NewClient(t) + s := NewBrokerDispatchStore(client) + ctx := context.Background() + + d := newDispatch(uuid.NewString(), "start") + require.NoError(t, s.InsertBrokerDispatch(ctx, d)) + + const racers = 8 + var wg sync.WaitGroup + var mu sync.Mutex + wins := 0 + wg.Add(racers) + for i := 0; i < racers; i++ { + go func() { + defer wg.Done() + won, err := s.ClaimBrokerDispatch(ctx, d.ID, "hub") + if err == nil && won { + mu.Lock() + wins++ + mu.Unlock() + } + }() + } + wg.Wait() + assert.Equal(t, 1, wins, "exactly one concurrent claim must win (exactly-once execution)") +} + +func TestBrokerDispatch_CompleteAndFail(t *testing.T) { + client := enttest.NewClient(t) + s := NewBrokerDispatchStore(client) + ctx := context.Background() + + d := newDispatch(uuid.NewString(), "check_prompt") + require.NoError(t, s.InsertBrokerDispatch(ctx, d)) + _, err := s.ClaimBrokerDispatch(ctx, d.ID, "hub-1") + require.NoError(t, err) + + require.NoError(t, s.CompleteBrokerDispatch(ctx, d.ID, `{"ok":true}`)) + got, err := client.BrokerDispatch.Get(ctx, uuid.MustParse(d.ID)) + require.NoError(t, err) + assert.Equal(t, store.DispatchStateDone, got.State) + assert.Equal(t, `{"ok":true}`, got.Result) + + d2 := newDispatch(uuid.NewString(), "start") + require.NoError(t, s.InsertBrokerDispatch(ctx, d2)) + _, err = s.ClaimBrokerDispatch(ctx, d2.ID, "hub-1") + require.NoError(t, err) + require.NoError(t, s.FailBrokerDispatch(ctx, d2.ID, "boom")) + got2, err := client.BrokerDispatch.Get(ctx, uuid.MustParse(d2.ID)) + require.NoError(t, err) + assert.Equal(t, store.DispatchStateFailed, got2.State) + assert.Equal(t, "boom", got2.Error) + assert.Equal(t, 1, got2.Attempts, "failure bumps the attempt counter") +} + +func TestMarkMessageDispatched_Dedupe(t *testing.T) { + client := enttest.NewClient(t) + cs := NewCompositeStore(client) + ctx := context.Background() + + msg := &store.Message{ + ID: uuid.NewString(), + ProjectID: uuid.NewString(), + Sender: "user:alice", + Recipient: "agent:bob", + Msg: "hi", + } + require.NoError(t, cs.CreateMessage(ctx, msg)) + assert.Equal(t, store.MessageDispatchPending, msg.DispatchState) + + ok, err := cs.MarkMessageDispatched(ctx, msg.ID) + require.NoError(t, err) + assert.True(t, ok) + + again, err := cs.MarkMessageDispatched(ctx, msg.ID) + require.NoError(t, err) + assert.False(t, again, "second dispatch CAS must dedupe") + + got, err := cs.GetMessage(ctx, msg.ID) + require.NoError(t, err) + assert.Equal(t, store.MessageDispatchDispatched, got.DispatchState) + require.NotNil(t, got.DispatchedAt) +} + +func TestListPendingMessages_ByBrokerAgent(t *testing.T) { + client := enttest.NewClient(t) + cs := NewCompositeStore(client) + ctx := context.Background() + brokerA := uuid.NewString() + brokerB := uuid.NewString() + + // A project and two agents, one per broker. + proj := &store.Project{ID: uuid.NewString(), Name: "p", Slug: "p-" + uuid.NewString()[:8], Visibility: store.VisibilityPrivate, OwnerID: uuid.NewString()} + require.NoError(t, cs.CreateProject(ctx, proj)) + projUID := uuid.MustParse(proj.ID) + agentA := mustCreateAgent(t, client, projUID, brokerA) + agentB := mustCreateAgent(t, client, projUID, brokerB) + + // Pending message to agentA (on brokerA), and one to agentB (on brokerB). + msgA := &store.Message{ID: uuid.NewString(), ProjectID: proj.ID, Sender: "user:x", Recipient: "agent:a", Msg: "for A", AgentID: agentA} + msgB := &store.Message{ID: uuid.NewString(), ProjectID: proj.ID, Sender: "user:x", Recipient: "agent:b", Msg: "for B", AgentID: agentB} + require.NoError(t, cs.CreateMessage(ctx, msgA)) + require.NoError(t, cs.CreateMessage(ctx, msgB)) + + pending, err := cs.ListPendingMessages(ctx, brokerA) + require.NoError(t, err) + require.Len(t, pending, 1) + assert.Equal(t, msgA.ID, pending[0].ID, "only the message for an agent on brokerA") + + // Once dispatched, it drops out of the pending set. + _, err = cs.MarkMessageDispatched(ctx, msgA.ID) + require.NoError(t, err) + pending, err = cs.ListPendingMessages(ctx, brokerA) + require.NoError(t, err) + assert.Empty(t, pending) +} + +func TestCountStuckPendingMessages(t *testing.T) { + client := enttest.NewClient(t) + cs := NewCompositeStore(client) + ctx := context.Background() + + proj := &store.Project{ + ID: uuid.NewString(), Name: "p", Slug: "p-" + uuid.NewString()[:8], + Visibility: store.VisibilityPrivate, OwnerID: uuid.NewString(), + } + require.NoError(t, cs.CreateProject(ctx, proj)) + + // A message created 10 minutes ago (stuck). + oldMsg := &store.Message{ + ID: uuid.NewString(), ProjectID: proj.ID, + Sender: "user:x", Recipient: "agent:a", Msg: "old", + CreatedAt: time.Now().Add(-10 * time.Minute), + } + require.NoError(t, cs.CreateMessage(ctx, oldMsg)) + assert.Equal(t, store.MessageDispatchPending, oldMsg.DispatchState) + + // A message created just now (not stuck). + newMsg := &store.Message{ + ID: uuid.NewString(), ProjectID: proj.ID, + Sender: "user:x", Recipient: "agent:b", Msg: "new", + } + require.NoError(t, cs.CreateMessage(ctx, newMsg)) + + cutoff := time.Now().Add(-5 * time.Minute) + count, err := cs.CountStuckPendingMessages(ctx, cutoff) + require.NoError(t, err) + assert.Equal(t, 1, count, "only the old message is stuck") + + // Dispatch the old message — it should no longer be stuck. + _, err = cs.MarkMessageDispatched(ctx, oldMsg.ID) + require.NoError(t, err) + count, err = cs.CountStuckPendingMessages(ctx, cutoff) + require.NoError(t, err) + assert.Equal(t, 0, count, "dispatched message is not stuck") +} + +func mustCreateAgent(t *testing.T, client *ent.Client, projectID uuid.UUID, brokerID string) string { + t.Helper() + a, err := client.Agent.Create(). + SetSlug("agent-" + uuid.NewString()[:8]). + SetName("agent"). + SetProjectID(projectID). + SetRuntimeBrokerID(brokerID). + Save(context.Background()) + require.NoError(t, err) + return a.ID.String() +} diff --git a/pkg/store/entadapter/brokerdispatch_store.go b/pkg/store/entadapter/brokerdispatch_store.go new file mode 100644 index 000000000..4747f066e --- /dev/null +++ b/pkg/store/entadapter/brokerdispatch_store.go @@ -0,0 +1,337 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package entadapter + +import ( + "context" + "time" + + "github.com/GoogleCloudPlatform/scion/pkg/ent" + "github.com/GoogleCloudPlatform/scion/pkg/ent/agent" + "github.com/GoogleCloudPlatform/scion/pkg/ent/brokerdispatch" + "github.com/GoogleCloudPlatform/scion/pkg/ent/message" + "github.com/GoogleCloudPlatform/scion/pkg/store" +) + +// BrokerDispatchStore is the Ent-backed store for the broker_dispatch durable +// intent table plus the message dispatch-state CAS helpers. Exactly-once +// execution across nodes is enforced by conditional (compare-and-swap) updates +// on the state column — no SELECT ... FOR UPDATE, correct on SQLite + Postgres. +type BrokerDispatchStore struct { + client *ent.Client +} + +// NewBrokerDispatchStore creates a new Ent-backed BrokerDispatchStore. +func NewBrokerDispatchStore(client *ent.Client) *BrokerDispatchStore { + return &BrokerDispatchStore{client: client} +} + +func entBrokerDispatchToStore(e *ent.BrokerDispatch) store.BrokerDispatch { + d := store.BrokerDispatch{ + ID: e.ID.String(), + BrokerID: e.BrokerID.String(), + AgentSlug: e.AgentSlug, + Op: e.Op, + Args: e.Args, + State: e.State, + Result: e.Result, + ClaimedBy: e.ClaimedBy, + Attempts: e.Attempts, + Error: e.Error, + CreatedAt: e.CreatedAt, + UpdatedAt: e.UpdatedAt, + } + if e.AgentID != nil { + d.AgentID = e.AgentID.String() + } + if e.ProjectID != nil { + d.ProjectID = e.ProjectID.String() + } + if e.DeadlineAt != nil { + d.DeadlineAt = e.DeadlineAt + } + return d +} + +// InsertBrokerDispatch persists a new durable dispatch intent. State defaults to +// pending. The generated id and timestamps are written back into d. +func (s *BrokerDispatchStore) InsertBrokerDispatch(ctx context.Context, d *store.BrokerDispatch) error { + if d.BrokerID == "" || d.Op == "" { + return store.ErrInvalidInput + } + brokerUID, err := parseUUID(d.BrokerID) + if err != nil { + return err + } + + create := s.client.BrokerDispatch.Create(). + SetBrokerID(brokerUID). + SetOp(d.Op) + + if d.ID != "" { + uid, err := parseUUID(d.ID) + if err != nil { + return err + } + create.SetID(uid) + } + if d.AgentID != "" { + agentUID, err := parseUUID(d.AgentID) + if err != nil { + return err + } + create.SetAgentID(agentUID) + } + if d.AgentSlug != "" { + create.SetAgentSlug(d.AgentSlug) + } + if d.ProjectID != "" { + projUID, err := parseUUID(d.ProjectID) + if err != nil { + return err + } + create.SetProjectID(projUID) + } + if d.Args != "" { + create.SetArgs(d.Args) + } + if d.State != "" { + create.SetState(d.State) + } + if d.DeadlineAt != nil { + create.SetDeadlineAt(*d.DeadlineAt) + } + + created, err := create.Save(ctx) + if err != nil { + return mapError(err) + } + d.ID = created.ID.String() + d.State = created.State + d.CreatedAt = created.CreatedAt + d.UpdatedAt = created.UpdatedAt + return nil +} + +// ClaimBrokerDispatch atomically transitions a dispatch from pending to +// in_progress, recording the claiming hub instance. It is a CAS keyed on +// state='pending', so exactly one node wins for a given row (design §7). Returns +// claimed=false if the row was not pending (already claimed/done/failed/absent). +func (s *BrokerDispatchStore) ClaimBrokerDispatch(ctx context.Context, id, hubInstanceID string) (bool, error) { + uid, err := parseUUID(id) + if err != nil { + return false, err + } + affected, err := s.client.BrokerDispatch.Update(). + Where(brokerdispatch.IDEQ(uid), brokerdispatch.StateEQ(store.DispatchStatePending)). + SetState(store.DispatchStateInProgress). + SetClaimedBy(hubInstanceID). + SetUpdatedAt(time.Now()). + Save(ctx) + if err != nil { + return false, mapError(err) + } + return affected == 1, nil +} + +// CompleteBrokerDispatch marks a dispatch done and records its result JSON. +// The update is guarded by state=in_progress (CAS) so a done or failed +// dispatch cannot be flipped by a stale or duplicate completion call. +func (s *BrokerDispatchStore) CompleteBrokerDispatch(ctx context.Context, id, result string) error { + uid, err := parseUUID(id) + if err != nil { + return err + } + upd := s.client.BrokerDispatch.Update(). + Where(brokerdispatch.IDEQ(uid), brokerdispatch.StateEQ(store.DispatchStateInProgress)). + SetState(store.DispatchStateDone). + SetUpdatedAt(time.Now()) + if result != "" { + upd.SetResult(result) + } + affected, err := upd.Save(ctx) + if err != nil { + return mapError(err) + } + if affected == 0 { + return store.ErrNotFound + } + return nil +} + +// FailBrokerDispatch marks a dispatch failed, records the error, and bumps the +// attempt counter (so a reaper/retry can bound re-drives). The update is +// guarded by state=in_progress (CAS) so a completed or already-failed dispatch +// cannot be overwritten by a stale failure call. +func (s *BrokerDispatchStore) FailBrokerDispatch(ctx context.Context, id, errMsg string) error { + uid, err := parseUUID(id) + if err != nil { + return err + } + affected, err := s.client.BrokerDispatch.Update(). + Where(brokerdispatch.IDEQ(uid), brokerdispatch.StateEQ(store.DispatchStateInProgress)). + SetState(store.DispatchStateFailed). + SetError(errMsg). + AddAttempts(1). + SetUpdatedAt(time.Now()). + Save(ctx) + if err != nil { + return mapError(err) + } + if affected == 0 { + return store.ErrNotFound + } + return nil +} + +// GetBrokerDispatch returns a single dispatch row by ID. Used by the originator +// to read the result/state after the owner completes the dispatch. +func (s *BrokerDispatchStore) GetBrokerDispatch(ctx context.Context, id string) (*store.BrokerDispatch, error) { + uid, err := parseUUID(id) + if err != nil { + return nil, err + } + row, err := s.client.BrokerDispatch.Get(ctx, uid) + if err != nil { + return nil, mapError(err) + } + d := entBrokerDispatchToStore(row) + return &d, nil +} + +// ListPendingDispatch returns the pending dispatch intents for a broker, oldest +// first — the reconcile-drain query (design §5.3). +func (s *BrokerDispatchStore) ListPendingDispatch(ctx context.Context, brokerID string) ([]store.BrokerDispatch, error) { + brokerUID, err := parseUUID(brokerID) + if err != nil { + return nil, err + } + rows, err := s.client.BrokerDispatch.Query(). + Where(brokerdispatch.BrokerIDEQ(brokerUID), brokerdispatch.StateEQ(store.DispatchStatePending)). + Order(ent.Asc(brokerdispatch.FieldCreatedAt)). + All(ctx) + if err != nil { + return nil, mapError(err) + } + out := make([]store.BrokerDispatch, 0, len(rows)) + for _, r := range rows { + out = append(out, entBrokerDispatchToStore(r)) + } + return out, nil +} + +// MarkMessageDispatched CAS-flips a message from dispatch_state=pending to +// dispatched and stamps dispatched_at. Returns dispatched=false if the row was +// not pending (already dispatched/failed/absent) — dedupes concurrent drains. +func (s *BrokerDispatchStore) MarkMessageDispatched(ctx context.Context, id string) (bool, error) { + uid, err := parseUUID(id) + if err != nil { + return false, err + } + affected, err := s.client.Message.Update(). + Where(message.IDEQ(uid), message.DispatchStateEQ(store.MessageDispatchPending)). + SetDispatchState(store.MessageDispatchDispatched). + SetDispatchedAt(time.Now()). + Save(ctx) + if err != nil { + return false, mapError(err) + } + return affected == 1, nil +} + +// CountStuckPendingMessages returns the number of messages still in +// dispatch_state='pending' whose created timestamp is before the given cutoff. +func (s *BrokerDispatchStore) CountStuckPendingMessages(ctx context.Context, before time.Time) (int, error) { + n, err := s.client.Message.Query(). + Where(message.DispatchStateEQ(store.MessageDispatchPending), message.CreatedLT(before)). + Count(ctx) + if err != nil { + return 0, mapError(err) + } + return n, nil +} + +// ListPendingMessages returns messages still pending delivery whose target agent +// lives on the given broker (messages have no broker_id; the association is via +// the recipient agent's runtime_broker_id). +func (s *BrokerDispatchStore) ListPendingMessages(ctx context.Context, brokerID string) ([]store.Message, error) { + agents, err := s.client.Agent.Query(). + Where(agent.RuntimeBrokerIDEQ(brokerID)). + All(ctx) + if err != nil { + return nil, mapError(err) + } + if len(agents) == 0 { + return nil, nil + } + agentIDs := make([]string, 0, len(agents)) + for _, a := range agents { + agentIDs = append(agentIDs, a.ID.String()) + } + rows, err := s.client.Message.Query(). + Where(message.AgentIDIn(agentIDs...), message.DispatchStateEQ(store.MessageDispatchPending)). + Order(ent.Asc(message.FieldCreated)). + All(ctx) + if err != nil { + return nil, mapError(err) + } + out := make([]store.Message, 0, len(rows)) + for _, r := range rows { + out = append(out, *entMessageToStore(r)) + } + return out, nil +} + +// ReapStuckDispatch re-drives or fails in_progress dispatches that have gone +// stale. Dispatches with attempts < maxAttempts are reset to pending; those at +// or above the limit are marked failed. +func (s *BrokerDispatchStore) ReapStuckDispatch(ctx context.Context, stuckBefore time.Time, maxAttempts int) (requeued, failed int, err error) { + now := time.Now() + + stuckPred := brokerdispatch.And( + brokerdispatch.StateEQ(store.DispatchStateInProgress), + brokerdispatch.Or( + brokerdispatch.UpdatedAtLT(stuckBefore), + brokerdispatch.And( + brokerdispatch.DeadlineAtNotNil(), + brokerdispatch.DeadlineAtLT(now), + ), + ), + ) + + requeued, err = s.client.BrokerDispatch.Update(). + Where(stuckPred, brokerdispatch.AttemptsLT(maxAttempts)). + SetState(store.DispatchStatePending). + ClearClaimedBy(). + AddAttempts(1). + SetUpdatedAt(now). + Save(ctx) + if err != nil { + return 0, 0, mapError(err) + } + + failed, err = s.client.BrokerDispatch.Update(). + Where(stuckPred, brokerdispatch.AttemptsGTE(maxAttempts)). + SetState(store.DispatchStateFailed). + SetError("reaper: max attempts exceeded"). + AddAttempts(1). + SetUpdatedAt(now). + Save(ctx) + if err != nil { + return requeued, 0, mapError(err) + } + + return requeued, failed, nil +} diff --git a/pkg/store/entadapter/brokersecret_store.go b/pkg/store/entadapter/brokersecret_store.go new file mode 100644 index 000000000..d93f815be --- /dev/null +++ b/pkg/store/entadapter/brokersecret_store.go @@ -0,0 +1,266 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package entadapter + +import ( + "context" + "time" + + "github.com/GoogleCloudPlatform/scion/pkg/ent" + "github.com/GoogleCloudPlatform/scion/pkg/ent/brokerjointoken" + "github.com/GoogleCloudPlatform/scion/pkg/ent/brokersecret" + "github.com/GoogleCloudPlatform/scion/pkg/store" +) + +// BrokerSecretStore implements store.BrokerSecretStore using Ent ORM. It backs +// runtime broker HMAC authentication: per-broker shared secrets (broker_secrets) +// and short-lived registration join tokens (broker_join_tokens). +// +// Both tables key their primary id on broker_id (one active secret / join token +// per broker), so the surrogate Ent id is stored directly in the broker_id +// column. +type BrokerSecretStore struct { + client *ent.Client +} + +// NewBrokerSecretStore creates a new Ent-backed BrokerSecretStore. +func NewBrokerSecretStore(client *ent.Client) *BrokerSecretStore { + return &BrokerSecretStore{client: client} +} + +// ============================================================================= +// Broker Secret operations +// ============================================================================= + +// entBrokerSecretToStore converts an Ent BrokerSecret to a store model. +func entBrokerSecretToStore(b *ent.BrokerSecret) *store.BrokerSecret { + secret := &store.BrokerSecret{ + BrokerID: b.ID.String(), + SecretKey: b.SecretKey, + Algorithm: b.Algorithm, + Status: b.Status, + CreatedAt: b.Created, + } + if b.RotatedAt != nil { + secret.RotatedAt = *b.RotatedAt + } + if b.ExpiresAt != nil { + secret.ExpiresAt = *b.ExpiresAt + } + return secret +} + +// CreateBrokerSecret creates a new broker secret record. +func (s *BrokerSecretStore) CreateBrokerSecret(ctx context.Context, secret *store.BrokerSecret) error { + if secret.BrokerID == "" { + return store.ErrInvalidInput + } + uid, err := parseUUID(secret.BrokerID) + if err != nil { + return err + } + + if secret.CreatedAt.IsZero() { + secret.CreatedAt = time.Now() + } + if secret.Algorithm == "" { + secret.Algorithm = store.BrokerSecretAlgorithmHMACSHA256 + } + if secret.Status == "" { + secret.Status = store.BrokerSecretStatusActive + } + + create := s.client.BrokerSecret.Create(). + SetID(uid). + SetSecretKey(secret.SecretKey). + SetAlgorithm(secret.Algorithm). + SetStatus(secret.Status). + SetCreated(secret.CreatedAt) + if !secret.RotatedAt.IsZero() { + create.SetRotatedAt(secret.RotatedAt) + } + if !secret.ExpiresAt.IsZero() { + create.SetExpiresAt(secret.ExpiresAt) + } + + if _, err := create.Save(ctx); err != nil { + return mapError(err) + } + return nil +} + +// GetBrokerSecret retrieves a broker secret by broker ID. +func (s *BrokerSecretStore) GetBrokerSecret(ctx context.Context, brokerID string) (*store.BrokerSecret, error) { + uid, err := parseUUID(brokerID) + if err != nil { + return nil, err + } + b, err := s.client.BrokerSecret.Get(ctx, uid) + if err != nil { + return nil, mapError(err) + } + return entBrokerSecretToStore(b), nil +} + +// GetActiveSecrets retrieves all active and deprecated secrets for a broker, +// newest first. This supports dual-secret validation during rotation grace +// periods. Because broker_id is the primary key, there is at most one row. +func (s *BrokerSecretStore) GetActiveSecrets(ctx context.Context, brokerID string) ([]*store.BrokerSecret, error) { + uid, err := parseUUID(brokerID) + if err != nil { + return nil, err + } + rows, err := s.client.BrokerSecret.Query(). + Where( + brokersecret.IDEQ(uid), + brokersecret.StatusIn(store.BrokerSecretStatusActive, store.BrokerSecretStatusDeprecated), + ). + Order(ent.Desc(brokersecret.FieldCreated)). + All(ctx) + if err != nil { + return nil, err + } + secrets := make([]*store.BrokerSecret, 0, len(rows)) + for _, b := range rows { + secrets = append(secrets, entBrokerSecretToStore(b)) + } + return secrets, nil +} + +// UpdateBrokerSecret updates an existing broker secret. +func (s *BrokerSecretStore) UpdateBrokerSecret(ctx context.Context, secret *store.BrokerSecret) error { + uid, err := parseUUID(secret.BrokerID) + if err != nil { + return err + } + + update := s.client.BrokerSecret.UpdateOneID(uid). + SetSecretKey(secret.SecretKey). + SetAlgorithm(secret.Algorithm). + SetStatus(secret.Status) + if secret.RotatedAt.IsZero() { + update.ClearRotatedAt() + } else { + update.SetRotatedAt(secret.RotatedAt) + } + if secret.ExpiresAt.IsZero() { + update.ClearExpiresAt() + } else { + update.SetExpiresAt(secret.ExpiresAt) + } + + if _, err := update.Save(ctx); err != nil { + return mapError(err) + } + return nil +} + +// DeleteBrokerSecret removes a broker secret. +func (s *BrokerSecretStore) DeleteBrokerSecret(ctx context.Context, brokerID string) error { + uid, err := parseUUID(brokerID) + if err != nil { + return err + } + if err := s.client.BrokerSecret.DeleteOneID(uid).Exec(ctx); err != nil { + return mapError(err) + } + return nil +} + +// ============================================================================= +// Broker Join Token operations +// ============================================================================= + +// entJoinTokenToStore converts an Ent BrokerJoinToken to a store model. +func entJoinTokenToStore(t *ent.BrokerJoinToken) *store.BrokerJoinToken { + return &store.BrokerJoinToken{ + BrokerID: t.ID.String(), + TokenHash: t.TokenHash, + ExpiresAt: t.ExpiresAt, + CreatedAt: t.Created, + CreatedBy: t.CreatedBy, + } +} + +// CreateJoinToken creates a new join token for broker registration. +func (s *BrokerSecretStore) CreateJoinToken(ctx context.Context, token *store.BrokerJoinToken) error { + if token.BrokerID == "" || token.TokenHash == "" { + return store.ErrInvalidInput + } + uid, err := parseUUID(token.BrokerID) + if err != nil { + return err + } + + if token.CreatedAt.IsZero() { + token.CreatedAt = time.Now() + } + + create := s.client.BrokerJoinToken.Create(). + SetID(uid). + SetTokenHash(token.TokenHash). + SetExpiresAt(token.ExpiresAt). + SetCreatedBy(token.CreatedBy). + SetCreated(token.CreatedAt) + + if _, err := create.Save(ctx); err != nil { + return mapError(err) + } + return nil +} + +// GetJoinToken retrieves a join token by token hash. +func (s *BrokerSecretStore) GetJoinToken(ctx context.Context, tokenHash string) (*store.BrokerJoinToken, error) { + t, err := s.client.BrokerJoinToken.Query(). + Where(brokerjointoken.TokenHashEQ(tokenHash)). + Only(ctx) + if err != nil { + return nil, mapError(err) + } + return entJoinTokenToStore(t), nil +} + +// GetJoinTokenByBrokerID retrieves a join token by broker ID. +func (s *BrokerSecretStore) GetJoinTokenByBrokerID(ctx context.Context, brokerID string) (*store.BrokerJoinToken, error) { + uid, err := parseUUID(brokerID) + if err != nil { + return nil, err + } + t, err := s.client.BrokerJoinToken.Get(ctx, uid) + if err != nil { + return nil, mapError(err) + } + return entJoinTokenToStore(t), nil +} + +// DeleteJoinToken removes a join token by broker ID. +func (s *BrokerSecretStore) DeleteJoinToken(ctx context.Context, brokerID string) error { + uid, err := parseUUID(brokerID) + if err != nil { + return err + } + if err := s.client.BrokerJoinToken.DeleteOneID(uid).Exec(ctx); err != nil { + return mapError(err) + } + return nil +} + +// CleanExpiredJoinTokens removes all expired join tokens. +func (s *BrokerSecretStore) CleanExpiredJoinTokens(ctx context.Context) error { + _, err := s.client.BrokerJoinToken.Delete(). + Where(brokerjointoken.ExpiresAtLT(time.Now())). + Exec(ctx) + return err +} diff --git a/pkg/store/entadapter/brokersecret_store_test.go b/pkg/store/entadapter/brokersecret_store_test.go new file mode 100644 index 000000000..a2240d951 --- /dev/null +++ b/pkg/store/entadapter/brokersecret_store_test.go @@ -0,0 +1,223 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//go:build !no_sqlite + +package entadapter + +import ( + "context" + "testing" + "time" + + "github.com/GoogleCloudPlatform/scion/pkg/store" + "github.com/GoogleCloudPlatform/scion/pkg/store/enttest" + "github.com/google/uuid" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func newTestBrokerSecretStore(t *testing.T) *BrokerSecretStore { + t.Helper() + client := enttest.NewClient(t) + return NewBrokerSecretStore(client) +} + +func TestBrokerSecret_CreateGet(t *testing.T) { + bs := newTestBrokerSecretStore(t) + ctx := context.Background() + + secret := &store.BrokerSecret{ + BrokerID: uuid.NewString(), + SecretKey: []byte("super-secret-hmac-key"), + } + require.NoError(t, bs.CreateBrokerSecret(ctx, secret)) + // Defaults applied. + assert.Equal(t, store.BrokerSecretAlgorithmHMACSHA256, secret.Algorithm) + assert.Equal(t, store.BrokerSecretStatusActive, secret.Status) + assert.False(t, secret.CreatedAt.IsZero()) + + got, err := bs.GetBrokerSecret(ctx, secret.BrokerID) + require.NoError(t, err) + assert.Equal(t, secret.BrokerID, got.BrokerID) + assert.Equal(t, []byte("super-secret-hmac-key"), got.SecretKey) + assert.Equal(t, store.BrokerSecretAlgorithmHMACSHA256, got.Algorithm) + assert.Equal(t, store.BrokerSecretStatusActive, got.Status) +} + +func TestBrokerSecret_CreateMissingID(t *testing.T) { + bs := newTestBrokerSecretStore(t) + err := bs.CreateBrokerSecret(context.Background(), &store.BrokerSecret{SecretKey: []byte("k")}) + assert.ErrorIs(t, err, store.ErrInvalidInput) +} + +func TestBrokerSecret_CreateDuplicate(t *testing.T) { + bs := newTestBrokerSecretStore(t) + ctx := context.Background() + + id := uuid.NewString() + require.NoError(t, bs.CreateBrokerSecret(ctx, &store.BrokerSecret{BrokerID: id, SecretKey: []byte("k1")})) + err := bs.CreateBrokerSecret(ctx, &store.BrokerSecret{BrokerID: id, SecretKey: []byte("k2")}) + assert.ErrorIs(t, err, store.ErrAlreadyExists) +} + +func TestBrokerSecret_GetNotFound(t *testing.T) { + bs := newTestBrokerSecretStore(t) + _, err := bs.GetBrokerSecret(context.Background(), uuid.NewString()) + assert.ErrorIs(t, err, store.ErrNotFound) +} + +func TestBrokerSecret_GetActiveSecrets(t *testing.T) { + bs := newTestBrokerSecretStore(t) + ctx := context.Background() + + id := uuid.NewString() + require.NoError(t, bs.CreateBrokerSecret(ctx, &store.BrokerSecret{ + BrokerID: id, SecretKey: []byte("k"), Status: store.BrokerSecretStatusActive, + })) + + active, err := bs.GetActiveSecrets(ctx, id) + require.NoError(t, err) + require.Len(t, active, 1) + assert.Equal(t, store.BrokerSecretStatusActive, active[0].Status) + + // A revoked secret is excluded from the active set. + require.NoError(t, bs.UpdateBrokerSecret(ctx, &store.BrokerSecret{ + BrokerID: id, SecretKey: []byte("k"), Algorithm: store.BrokerSecretAlgorithmHMACSHA256, Status: store.BrokerSecretStatusRevoked, + })) + active, err = bs.GetActiveSecrets(ctx, id) + require.NoError(t, err) + assert.Empty(t, active) +} + +func TestBrokerSecret_Update(t *testing.T) { + bs := newTestBrokerSecretStore(t) + ctx := context.Background() + + id := uuid.NewString() + require.NoError(t, bs.CreateBrokerSecret(ctx, &store.BrokerSecret{BrokerID: id, SecretKey: []byte("k1")})) + + rotated := time.Now().UTC().Truncate(time.Second) + require.NoError(t, bs.UpdateBrokerSecret(ctx, &store.BrokerSecret{ + BrokerID: id, + SecretKey: []byte("k2"), + Algorithm: store.BrokerSecretAlgorithmHMACSHA256, + Status: store.BrokerSecretStatusDeprecated, + RotatedAt: rotated, + })) + + got, err := bs.GetBrokerSecret(ctx, id) + require.NoError(t, err) + assert.Equal(t, []byte("k2"), got.SecretKey) + assert.Equal(t, store.BrokerSecretStatusDeprecated, got.Status) + assert.False(t, got.RotatedAt.IsZero()) +} + +func TestBrokerSecret_Delete(t *testing.T) { + bs := newTestBrokerSecretStore(t) + ctx := context.Background() + + id := uuid.NewString() + require.NoError(t, bs.CreateBrokerSecret(ctx, &store.BrokerSecret{BrokerID: id, SecretKey: []byte("k")})) + require.NoError(t, bs.DeleteBrokerSecret(ctx, id)) + _, err := bs.GetBrokerSecret(ctx, id) + assert.ErrorIs(t, err, store.ErrNotFound) + assert.ErrorIs(t, bs.DeleteBrokerSecret(ctx, id), store.ErrNotFound) +} + +// ============================================================================= +// Broker Join Tokens +// ============================================================================= + +func TestJoinToken_CreateGet(t *testing.T) { + bs := newTestBrokerSecretStore(t) + ctx := context.Background() + + token := &store.BrokerJoinToken{ + BrokerID: uuid.NewString(), + TokenHash: "hash-" + uuid.NewString(), + ExpiresAt: time.Now().Add(time.Hour), + CreatedBy: uuid.NewString(), + } + require.NoError(t, bs.CreateJoinToken(ctx, token)) + assert.False(t, token.CreatedAt.IsZero()) + + byHash, err := bs.GetJoinToken(ctx, token.TokenHash) + require.NoError(t, err) + assert.Equal(t, token.BrokerID, byHash.BrokerID) + + byBroker, err := bs.GetJoinTokenByBrokerID(ctx, token.BrokerID) + require.NoError(t, err) + assert.Equal(t, token.TokenHash, byBroker.TokenHash) +} + +func TestJoinToken_CreateInvalid(t *testing.T) { + bs := newTestBrokerSecretStore(t) + ctx := context.Background() + assert.ErrorIs(t, bs.CreateJoinToken(ctx, &store.BrokerJoinToken{TokenHash: "h"}), store.ErrInvalidInput) + assert.ErrorIs(t, bs.CreateJoinToken(ctx, &store.BrokerJoinToken{BrokerID: uuid.NewString()}), store.ErrInvalidInput) +} + +func TestJoinToken_GetNotFound(t *testing.T) { + bs := newTestBrokerSecretStore(t) + ctx := context.Background() + _, err := bs.GetJoinToken(ctx, "missing") + assert.ErrorIs(t, err, store.ErrNotFound) + _, err = bs.GetJoinTokenByBrokerID(ctx, uuid.NewString()) + assert.ErrorIs(t, err, store.ErrNotFound) +} + +func TestJoinToken_Delete(t *testing.T) { + bs := newTestBrokerSecretStore(t) + ctx := context.Background() + + token := &store.BrokerJoinToken{ + BrokerID: uuid.NewString(), + TokenHash: "hash-" + uuid.NewString(), + ExpiresAt: time.Now().Add(time.Hour), + CreatedBy: uuid.NewString(), + } + require.NoError(t, bs.CreateJoinToken(ctx, token)) + require.NoError(t, bs.DeleteJoinToken(ctx, token.BrokerID)) + _, err := bs.GetJoinTokenByBrokerID(ctx, token.BrokerID) + assert.ErrorIs(t, err, store.ErrNotFound) + assert.ErrorIs(t, bs.DeleteJoinToken(ctx, token.BrokerID), store.ErrNotFound) +} + +func TestJoinToken_CleanExpired(t *testing.T) { + bs := newTestBrokerSecretStore(t) + ctx := context.Background() + + expired := &store.BrokerJoinToken{ + BrokerID: uuid.NewString(), + TokenHash: "expired-" + uuid.NewString(), + ExpiresAt: time.Now().Add(-time.Hour), + CreatedBy: uuid.NewString(), + } + valid := &store.BrokerJoinToken{ + BrokerID: uuid.NewString(), + TokenHash: "valid-" + uuid.NewString(), + ExpiresAt: time.Now().Add(time.Hour), + CreatedBy: uuid.NewString(), + } + require.NoError(t, bs.CreateJoinToken(ctx, expired)) + require.NoError(t, bs.CreateJoinToken(ctx, valid)) + + require.NoError(t, bs.CleanExpiredJoinTokens(ctx)) + + _, err := bs.GetJoinTokenByBrokerID(ctx, expired.BrokerID) + assert.ErrorIs(t, err, store.ErrNotFound, "expired token should be cleaned") + _, err = bs.GetJoinTokenByBrokerID(ctx, valid.BrokerID) + assert.NoError(t, err, "valid token should remain") +} diff --git a/pkg/store/entadapter/composite.go b/pkg/store/entadapter/composite.go index 36a087e71..43c2ce5dc 100644 --- a/pkg/store/entadapter/composite.go +++ b/pkg/store/entadapter/composite.go @@ -16,307 +16,164 @@ package entadapter import ( "context" + "database/sql" "fmt" + entsql "entgo.io/ent/dialect/sql" + "github.com/GoogleCloudPlatform/scion/pkg/ent" - entuser "github.com/GoogleCloudPlatform/scion/pkg/ent/user" + "github.com/GoogleCloudPlatform/scion/pkg/ent/agent" + "github.com/GoogleCloudPlatform/scion/pkg/ent/entc" + "github.com/GoogleCloudPlatform/scion/pkg/ent/notification" + "github.com/GoogleCloudPlatform/scion/pkg/ent/notificationsubscription" "github.com/GoogleCloudPlatform/scion/pkg/store" ) -// CompositeStore wraps an existing store.Store and overrides group and policy -// operations with Ent-backed implementations. +// CompositeStore is a fully Ent-backed implementation of store.Store. Every +// domain is served by a dedicated Ent sub-store; CompositeStore embeds them so +// their methods are promoted to satisfy the store.Store interface, while the +// store-level Close/Ping/Migrate operations act on the shared Ent client. +// +// There is no longer a separate raw-SQL store: all Hub state lives in a single +// Ent database. type CompositeStore struct { - store.Store - groups *GroupStore - policies *PolicyStore - client *ent.Client -} - -// NewCompositeStore creates a CompositeStore that delegates group and policy -// operations to Ent-backed stores while forwarding all other operations to the -// underlying store. -func NewCompositeStore(base store.Store, client *ent.Client) *CompositeStore { + *AgentStore + *ProjectStore + *UserStore + *SecretStore + *TemplateStore + *NotificationStore + *ScheduleStore + *MaintenanceStore + *MessageStore + *ExternalStore + *BrokerSecretStore + *AllowListStore + *GroupStore + *PolicyStore + *BrokerDispatchStore + + client *ent.Client +} + +// Compile-time assertion that CompositeStore satisfies the full store.Store +// interface purely through its embedded Ent-backed sub-stores. +var _ store.Store = (*CompositeStore)(nil) + +// NewCompositeStore creates a store.Store backed entirely by the given Ent +// client. Each domain sub-store shares the same client and therefore the same +// underlying database, so cross-domain foreign keys (e.g. group -> project, +// agent -> project) resolve natively without any shadow synchronization. +func NewCompositeStore(client *ent.Client) *CompositeStore { return &CompositeStore{ - Store: base, - groups: NewGroupStore(client), - policies: NewPolicyStore(client), - client: client, - } -} - -// Close closes both the Ent client and the underlying store. -func (c *CompositeStore) Close() error { - if err := c.client.Close(); err != nil { - _ = c.Store.Close() + AgentStore: NewAgentStore(client), + ProjectStore: NewProjectStore(client), + UserStore: NewUserStore(client), + SecretStore: NewSecretStore(client), + TemplateStore: NewTemplateStore(client), + NotificationStore: NewNotificationStore(client), + ScheduleStore: NewScheduleStore(client), + MaintenanceStore: NewMaintenanceStore(client), + MessageStore: NewMessageStore(client), + ExternalStore: NewExternalStore(client), + BrokerSecretStore: NewBrokerSecretStore(client), + AllowListStore: NewAllowListStore(client), + GroupStore: NewGroupStore(client), + PolicyStore: NewPolicyStore(client), + BrokerDispatchStore: NewBrokerDispatchStore(client), + client: client, + } +} + +// DeleteAgent hard-deletes an agent and cascade-deletes its notification +// subscriptions and notifications. The former raw-SQL store enforced this via +// ON DELETE CASCADE foreign keys (notification_subscriptions.agent_id -> +// agents(id), notifications.subscription_id -> notification_subscriptions(id)). +// In the Ent schema agent_id is a plain field with no edge, so the cascade is +// performed explicitly here to preserve store parity. Soft delete goes through +// UpdateAgent and is unaffected, so subscriptions are retained for soft-deleted +// agents. +func (c *CompositeStore) DeleteAgent(ctx context.Context, id string) error { + if err := c.AgentStore.DeleteAgent(ctx, id); err != nil { return err } - return c.Store.Close() -} - -// GroupStore method overrides — delegate to Ent-backed GroupStore. - -func (c *CompositeStore) CreateGroup(ctx context.Context, group *store.Group) error { - // Ensure the project exists in the Ent database before creating the group, - // since projects are stored in the base (SQLite) store but groups are in Ent - // which has a foreign key constraint on project_id. - if group.ProjectID != "" { - if err := c.ensureEntProject(ctx, group.ProjectID); err != nil { - return fmt.Errorf("ensuring project in ent store: %w", err) - } - } - return c.groups.CreateGroup(ctx, group) -} - -func (c *CompositeStore) GetGroup(ctx context.Context, id string) (*store.Group, error) { - return c.groups.GetGroup(ctx, id) -} - -func (c *CompositeStore) GetGroupBySlug(ctx context.Context, slug string) (*store.Group, error) { - return c.groups.GetGroupBySlug(ctx, slug) -} - -func (c *CompositeStore) UpdateGroup(ctx context.Context, group *store.Group) error { - return c.groups.UpdateGroup(ctx, group) -} - -func (c *CompositeStore) DeleteGroup(ctx context.Context, id string) error { - return c.groups.DeleteGroup(ctx, id) -} - -func (c *CompositeStore) ListGroups(ctx context.Context, filter store.GroupFilter, opts store.ListOptions) (*store.ListResult[store.Group], error) { - return c.groups.ListGroups(ctx, filter, opts) -} - -func (c *CompositeStore) AddGroupMember(ctx context.Context, member *store.GroupMember) error { - switch member.MemberType { - case store.GroupMemberTypeUser: - if err := c.ensureEntUser(ctx, member.MemberID); err != nil { - return fmt.Errorf("ensuring user in ent store: %w", err) - } - case store.GroupMemberTypeAgent: - if err := c.ensureEntAgent(ctx, member.MemberID); err != nil { - return fmt.Errorf("ensuring agent in ent store: %w", err) - } - } - return c.groups.AddGroupMember(ctx, member) -} - -// ensureEntUser checks if a user exists in the Ent database and, if not, -// creates a minimal shadow record from the base store. This is needed because -// the Ent database has foreign key constraints on group memberships, but users -// may only exist in the base (main SQLite) database. -func (c *CompositeStore) ensureEntUser(ctx context.Context, userID string) error { - uid, err := parseUUID(userID) + uid, err := parseUUID(id) if err != nil { return err } - - // Check if user already exists in Ent - exists, err := c.client.User.Query().Where(entuser.IDEQ(uid)).Exist(ctx) - if err != nil { - return fmt.Errorf("checking ent user existence: %w", err) - } - if exists { - return nil - } - - // Fetch from the base store - u, err := c.Store.GetUser(ctx, userID) - if err != nil { - return fmt.Errorf("fetching user from base store: %w", err) - } - - // Create a minimal shadow record in Ent - _, err = c.client.User.Create(). - SetID(uid). - SetEmail(u.Email). - SetDisplayName(u.DisplayName). - SetRole(entuser.Role(u.Role)). - Save(ctx) - if err != nil { - // Another goroutine may have created it concurrently - if ent.IsConstraintError(err) { - return nil - } - return fmt.Errorf("creating shadow user in ent: %w", err) - } - - return nil -} - -// ensureEntAgent checks if an agent exists in the Ent database and, if not, -// creates a minimal shadow record from the base store. The agent.s project is -// also ensured to exist in Ent since it is a required FK. -func (c *CompositeStore) ensureEntAgent(ctx context.Context, agentID string) error { - uid, err := parseUUID(agentID) - if err != nil { + if _, err := c.client.Notification.Delete(). + Where(notification.AgentIDEQ(uid)).Exec(ctx); err != nil { return err } - - // Check if agent already exists in Ent - _, getErr := c.client.Agent.Get(ctx, uid) - if getErr == nil { - return nil // already exists - } - if !ent.IsNotFound(getErr) { - return fmt.Errorf("checking ent agent existence: %w", getErr) - } - - // Fetch from the base store - a, err := c.Store.GetAgent(ctx, agentID) - if err != nil { - return fmt.Errorf("fetching agent from base store: %w", err) - } - - // Ensure the project exists in Ent first (required FK) - if err := c.ensureEntProject(ctx, a.ProjectID); err != nil { - return fmt.Errorf("ensuring project in ent store: %w", err) - } - - projectUID, err := parseUUID(a.ProjectID) - if err != nil { + if _, err := c.client.NotificationSubscription.Delete(). + Where(notificationsubscription.AgentIDEQ(uid)).Exec(ctx); err != nil { return err } - - // Create a minimal shadow record in Ent - _, err = c.client.Agent.Create(). - SetID(uid). - SetName(a.Name). - SetSlug(a.Slug). - SetProjectID(projectUID). - Save(ctx) - if err != nil { - if ent.IsConstraintError(err) { - return nil - } - return fmt.Errorf("creating shadow agent in ent: %w", err) - } - return nil } -// ensureEntProject checks if a project exists in the Ent database and, if not, -// creates a minimal shadow record from the base store. -func (c *CompositeStore) ensureEntProject(ctx context.Context, projectID string) error { - uid, err := parseUUID(projectID) +// DeleteProject deletes a project and cascade-deletes its agents (and each +// agent's notification subscriptions/notifications). The former raw-SQL store +// enforced this via agents.grove_id -> groves(id) ON DELETE CASCADE; the Ent +// project->agents edge has no DB-level cascade, so deleting a project while +// agents still reference it would fail with a foreign-key violation. The bulk +// agent delete is a hard delete, so it also removes soft-deleted agents. +func (c *CompositeStore) DeleteProject(ctx context.Context, id string) error { + uid, err := parseUUID(id) if err != nil { return err } - - _, getErr := c.client.Project.Get(ctx, uid) - if getErr == nil { - return nil - } - if !ent.IsNotFound(getErr) { - return fmt.Errorf("checking ent project existence: %w", getErr) - } - - g, err := c.Store.GetProject(ctx, projectID) + agentIDs, err := c.client.Agent.Query().Where(agent.ProjectIDEQ(uid)).IDs(ctx) if err != nil { - return fmt.Errorf("fetching project from base store: %w", err) + return err } - - _, err = c.client.Project.Create(). - SetID(uid). - SetName(g.Name). - SetSlug(g.Slug). - Save(ctx) - if err != nil { - if ent.IsConstraintError(err) { - return nil + if len(agentIDs) > 0 { + if _, err := c.client.Notification.Delete(). + Where(notification.AgentIDIn(agentIDs...)).Exec(ctx); err != nil { + return err + } + if _, err := c.client.NotificationSubscription.Delete(). + Where(notificationsubscription.AgentIDIn(agentIDs...)).Exec(ctx); err != nil { + return err + } + if _, err := c.client.Agent.Delete(). + Where(agent.ProjectIDEQ(uid)).Exec(ctx); err != nil { + return err } - return fmt.Errorf("creating shadow project in ent: %w", err) } - - return nil + return c.ProjectStore.DeleteProject(ctx, id) } -func (c *CompositeStore) UpdateGroupMemberRole(ctx context.Context, groupID, memberType, memberID, newRole string) error { - return c.groups.UpdateGroupMemberRole(ctx, groupID, memberType, memberID, newRole) -} - -func (c *CompositeStore) RemoveGroupMember(ctx context.Context, groupID, memberType, memberID string) error { - return c.groups.RemoveGroupMember(ctx, groupID, memberType, memberID) -} - -func (c *CompositeStore) GetGroupMembers(ctx context.Context, groupID string) ([]store.GroupMember, error) { - return c.groups.GetGroupMembers(ctx, groupID) -} - -func (c *CompositeStore) GetUserGroups(ctx context.Context, userID string) ([]store.GroupMember, error) { - return c.groups.GetUserGroups(ctx, userID) -} - -func (c *CompositeStore) GetGroupMembership(ctx context.Context, groupID, memberType, memberID string) (*store.GroupMember, error) { - return c.groups.GetGroupMembership(ctx, groupID, memberType, memberID) -} - -func (c *CompositeStore) WouldCreateCycle(ctx context.Context, groupID, memberGroupID string) (bool, error) { - return c.groups.WouldCreateCycle(ctx, groupID, memberGroupID) -} - -func (c *CompositeStore) GetEffectiveGroups(ctx context.Context, userID string) ([]string, error) { - return c.groups.GetEffectiveGroups(ctx, userID) -} - -func (c *CompositeStore) GetGroupByProjectID(ctx context.Context, projectID string) (*store.Group, error) { - return c.groups.GetGroupByProjectID(ctx, projectID) -} - -func (c *CompositeStore) GetEffectiveGroupsForAgent(ctx context.Context, agentID string) ([]string, error) { - return c.groups.GetEffectiveGroupsForAgent(ctx, agentID) -} - -func (c *CompositeStore) CheckDelegatedAccess(ctx context.Context, agentID string, conditions *store.PolicyConditions) (bool, error) { - return c.groups.CheckDelegatedAccess(ctx, agentID, conditions) -} - -func (c *CompositeStore) GetGroupsByIDs(ctx context.Context, ids []string) ([]store.Group, error) { - return c.groups.GetGroupsByIDs(ctx, ids) -} - -func (c *CompositeStore) CountGroupMembersByRole(ctx context.Context, groupID, role string) (int, error) { - return c.groups.CountGroupMembersByRole(ctx, groupID, role) -} - -// PolicyStore method overrides — delegate to Ent-backed PolicyStore. - -func (c *CompositeStore) CreatePolicy(ctx context.Context, policy *store.Policy) error { - return c.policies.CreatePolicy(ctx, policy) -} - -func (c *CompositeStore) GetPolicy(ctx context.Context, id string) (*store.Policy, error) { - return c.policies.GetPolicy(ctx, id) -} - -func (c *CompositeStore) UpdatePolicy(ctx context.Context, policy *store.Policy) error { - return c.policies.UpdatePolicy(ctx, policy) -} - -func (c *CompositeStore) DeletePolicy(ctx context.Context, id string) error { - return c.policies.DeletePolicy(ctx, id) -} - -func (c *CompositeStore) ListPolicies(ctx context.Context, filter store.PolicyFilter, opts store.ListOptions) (*store.ListResult[store.Policy], error) { - return c.policies.ListPolicies(ctx, filter, opts) -} - -func (c *CompositeStore) AddPolicyBinding(ctx context.Context, binding *store.PolicyBinding) error { - return c.policies.AddPolicyBinding(ctx, binding) -} - -func (c *CompositeStore) RemovePolicyBinding(ctx context.Context, policyID, principalType, principalID string) error { - return c.policies.RemovePolicyBinding(ctx, policyID, principalType, principalID) +// Close closes the underlying Ent client. +func (c *CompositeStore) Close() error { + return c.client.Close() } -func (c *CompositeStore) GetPolicyBindings(ctx context.Context, policyID string) ([]store.PolicyBinding, error) { - return c.policies.GetPolicyBindings(ctx, policyID) +// Ping verifies connectivity to the underlying database. +func (c *CompositeStore) Ping(ctx context.Context) error { + drv, ok := c.client.Driver().(*entsql.Driver) + if !ok { + return fmt.Errorf("ent client driver does not expose a *sql.DB for ping") + } + return drv.DB().PingContext(ctx) } -func (c *CompositeStore) GetPoliciesForPrincipal(ctx context.Context, principalType, principalID string) ([]store.Policy, error) { - return c.policies.GetPoliciesForPrincipal(ctx, principalType, principalID) +// Migrate runs Ent's automatic schema migration against the shared client and +// seeds the built-in maintenance operations, matching the behavior of the +// former raw-SQL store (which seeded these as part of its migrations). +func (c *CompositeStore) Migrate(ctx context.Context) error { + if err := entc.AutoMigrate(ctx, c.client); err != nil { + return err + } + return c.MaintenanceStore.SeedMaintenanceOperations(ctx) } -func (c *CompositeStore) GetPoliciesForPrincipals(ctx context.Context, principals []store.PrincipalRef) ([]store.Policy, error) { - return c.policies.GetPoliciesForPrincipals(ctx, principals) +// DB returns the underlying *sql.DB, or nil if the client is not backed by a +// database/sql driver. It is an escape hatch for diagnostics and tests that +// need raw SQL access; production code should use the typed store methods. +func (c *CompositeStore) DB() *sql.DB { + if drv, ok := c.client.Driver().(*entsql.Driver); ok { + return drv.DB() + } + return nil } diff --git a/pkg/store/entadapter/composite_test.go b/pkg/store/entadapter/composite_test.go index 4cedfb51a..7543eac6d 100644 --- a/pkg/store/entadapter/composite_test.go +++ b/pkg/store/entadapter/composite_test.go @@ -22,42 +22,34 @@ import ( "time" "github.com/GoogleCloudPlatform/scion/pkg/agent/state" - "github.com/GoogleCloudPlatform/scion/pkg/ent/entc" "github.com/GoogleCloudPlatform/scion/pkg/store" - "github.com/GoogleCloudPlatform/scion/pkg/store/sqlite" + "github.com/GoogleCloudPlatform/scion/pkg/store/enttest" "github.com/google/uuid" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) -// newTestCompositeStore creates a CompositeStore with a real SQLite base store -// and a separate Ent client, simulating the production dual-database layout. +// newTestCompositeStore creates a CompositeStore backed by a single in-memory +// Ent database, matching the production single-database layout. func newTestCompositeStore(t *testing.T) *CompositeStore { t.Helper() - // Create the base SQLite store (main database) - base, err := sqlite.New(":memory:") - require.NoError(t, err) - require.NoError(t, base.Migrate(context.Background())) - - // Create a separate Ent-managed database (permissions database) - entClient, err := entc.OpenSQLite("file:" + t.Name() + "?mode=memory&cache=shared") - require.NoError(t, err) - require.NoError(t, entc.AutoMigrate(context.Background(), entClient)) + entClient := enttest.NewClient(t) - cs := NewCompositeStore(base, entClient) + cs := NewCompositeStore(entClient) t.Cleanup(func() { cs.Close() }) return cs } -func TestCompositeStore_AddGroupMember_UserShadowRecord(t *testing.T) { +func TestCompositeStore_AddGroupMember_User(t *testing.T) { cs := newTestCompositeStore(t) ctx := context.Background() - // Create a user in the base store only (simulating normal user creation) + // Create a user. With a single Ent-backed database the user, group, and + // membership all live in the same store and the FK resolves natively. userID := uuid.New().String() - err := cs.Store.CreateUser(ctx, &store.User{ + err := cs.CreateUser(ctx, &store.User{ ID: userID, Email: "test@example.com", DisplayName: "Test User", @@ -67,7 +59,7 @@ func TestCompositeStore_AddGroupMember_UserShadowRecord(t *testing.T) { }) require.NoError(t, err) - // Create a group in Ent + // Create a group. groupID := uuid.New().String() err = cs.CreateGroup(ctx, &store.Group{ ID: groupID, @@ -77,79 +69,33 @@ func TestCompositeStore_AddGroupMember_UserShadowRecord(t *testing.T) { }) require.NoError(t, err) - // Add the user as a member — this should succeed because the CompositeStore - // creates a shadow user record in the Ent database before adding the membership. + // Add the user as a member. err = cs.AddGroupMember(ctx, &store.GroupMember{ GroupID: groupID, MemberType: store.GroupMemberTypeUser, MemberID: userID, Role: store.GroupMemberRoleMember, }) - require.NoError(t, err, "AddGroupMember should succeed for user that exists only in base store") + require.NoError(t, err, "AddGroupMember should succeed for an existing user") - // Verify the membership was created + // Verify the membership was created. membership, err := cs.GetGroupMembership(ctx, groupID, store.GroupMemberTypeUser, userID) require.NoError(t, err) assert.Equal(t, userID, membership.MemberID) - // Verify the user appears in effective groups + // Verify the user appears in effective groups. groups, err := cs.GetEffectiveGroups(ctx, userID) require.NoError(t, err) assert.Contains(t, groups, groupID) } -func TestCompositeStore_AddGroupMember_UserAlreadyInEnt(t *testing.T) { - cs := newTestCompositeStore(t) - ctx := context.Background() - - userID := uuid.New().String() - userUID, _ := uuid.Parse(userID) - - // Create user in both base store and Ent - err := cs.Store.CreateUser(ctx, &store.User{ - ID: userID, - Email: "already@example.com", - DisplayName: "Already Here", - Role: store.UserRoleMember, - Status: "active", - Created: time.Now(), - }) - require.NoError(t, err) - - _, err = cs.client.User.Create(). - SetID(userUID). - SetEmail("already@example.com"). - SetDisplayName("Already Here"). - Save(ctx) - require.NoError(t, err) - - // Create a group - groupID := uuid.New().String() - err = cs.CreateGroup(ctx, &store.Group{ - ID: groupID, - Name: "Test Group 2", - Slug: "test-group-2", - GroupType: store.GroupTypeExplicit, - }) - require.NoError(t, err) - - // Should work without issues (no duplicate creation) - err = cs.AddGroupMember(ctx, &store.GroupMember{ - GroupID: groupID, - MemberType: store.GroupMemberTypeUser, - MemberID: userID, - Role: store.GroupMemberRoleMember, - }) - require.NoError(t, err) -} - -func TestCompositeStore_AddGroupMember_AgentShadowRecord(t *testing.T) { +func TestCompositeStore_AddGroupMember_Agent(t *testing.T) { cs := newTestCompositeStore(t) ctx := context.Background() - // Create a project in the base store + // Create a project. projectID := uuid.New().String() - err := cs.Store.CreateProject(ctx, &store.Project{ + err := cs.CreateProject(ctx, &store.Project{ ID: projectID, Name: "Test Project", Slug: "test-project", @@ -158,9 +104,9 @@ func TestCompositeStore_AddGroupMember_AgentShadowRecord(t *testing.T) { }) require.NoError(t, err) - // Create an agent in the base store only + // Create an agent referencing the project. agentID := uuid.New().String() - err = cs.Store.CreateAgent(ctx, &store.Agent{ + err = cs.CreateAgent(ctx, &store.Agent{ ID: agentID, Name: "Test Agent", Slug: "test-agent", @@ -172,7 +118,7 @@ func TestCompositeStore_AddGroupMember_AgentShadowRecord(t *testing.T) { }) require.NoError(t, err) - // Create a group + // Create a group. groupID := uuid.New().String() err = cs.CreateGroup(ctx, &store.Group{ ID: groupID, @@ -182,16 +128,16 @@ func TestCompositeStore_AddGroupMember_AgentShadowRecord(t *testing.T) { }) require.NoError(t, err) - // Add the agent as a member — should create shadow agent and project records + // Add the agent as a member. err = cs.AddGroupMember(ctx, &store.GroupMember{ GroupID: groupID, MemberType: store.GroupMemberTypeAgent, MemberID: agentID, Role: store.GroupMemberRoleMember, }) - require.NoError(t, err, "AddGroupMember should succeed for agent that exists only in base store") + require.NoError(t, err, "AddGroupMember should succeed for an existing agent") - // Verify membership + // Verify membership. membership, err := cs.GetGroupMembership(ctx, groupID, store.GroupMemberTypeAgent, agentID) require.NoError(t, err) assert.Equal(t, agentID, membership.MemberID) @@ -202,7 +148,7 @@ func TestCompositeStore_AddGroupMember_Idempotent(t *testing.T) { ctx := context.Background() userID := uuid.New().String() - err := cs.Store.CreateUser(ctx, &store.User{ + err := cs.CreateUser(ctx, &store.User{ ID: userID, Email: "idempotent@example.com", DisplayName: "Idempotent User", @@ -221,7 +167,7 @@ func TestCompositeStore_AddGroupMember_Idempotent(t *testing.T) { }) require.NoError(t, err) - // First add should succeed + // First add should succeed. member := &store.GroupMember{ GroupID: groupID, MemberType: store.GroupMemberTypeUser, @@ -231,48 +177,41 @@ func TestCompositeStore_AddGroupMember_Idempotent(t *testing.T) { err = cs.AddGroupMember(ctx, member) require.NoError(t, err) - // Second add of same membership should return ErrAlreadyExists + // Second add of same membership should return ErrAlreadyExists. err = cs.AddGroupMember(ctx, member) assert.ErrorIs(t, err, store.ErrAlreadyExists) } -// TestCompositeStore_CreateGroup_WithProjectID tests that creating a group with a -// project ID succeeds even though the project only exists in the base (SQLite) store. -// The CompositeStore should create a shadow project record in the Ent database to -// satisfy the foreign key constraint. +// TestCompositeStore_CreateGroup_WithProjectID verifies that creating a group +// referencing a project succeeds when the project lives in the same Ent store. func TestCompositeStore_CreateGroup_WithProjectID(t *testing.T) { cs := newTestCompositeStore(t) ctx := context.Background() - // Create a project in the base store only (not in Ent) projectID := uuid.New().String() - err := cs.Store.CreateProject(ctx, &store.Project{ + err := cs.CreateProject(ctx, &store.Project{ ID: projectID, - Name: "Shadow Project", - Slug: "shadow-project", + Name: "Project", + Slug: "project", Created: time.Now(), Updated: time.Now(), }) require.NoError(t, err) - // Create a group with project_id — this should succeed because the - // CompositeStore creates a shadow project record in Ent before creating - // the group. groupID := uuid.New().String() err = cs.CreateGroup(ctx, &store.Group{ ID: groupID, - Name: "Shadow Project Agents", - Slug: "project:shadow-project:agents", + Name: "Project Agents", + Slug: "project:project:agents", GroupType: store.GroupTypeProjectAgents, ProjectID: projectID, }) - require.NoError(t, err, "CreateGroup should succeed for project that exists only in base store") + require.NoError(t, err, "CreateGroup should succeed for an existing project") - // Verify the group was created with the correct project ID group, err := cs.GetGroup(ctx, groupID) require.NoError(t, err) assert.Equal(t, projectID, group.ProjectID) - assert.Equal(t, "project:shadow-project:agents", group.Slug) + assert.Equal(t, "project:project:agents", group.Slug) } // TestCompositeStore_CreateGroup_MultipleGroupsPerProject verifies that multiple @@ -283,7 +222,7 @@ func TestCompositeStore_CreateGroup_MultipleGroupsPerProject(t *testing.T) { ctx := context.Background() projectID := uuid.New().String() - err := cs.Store.CreateProject(ctx, &store.Project{ + err := cs.CreateProject(ctx, &store.Project{ ID: projectID, Name: "Multi-Group Project", Slug: "multi-group-project", @@ -292,7 +231,7 @@ func TestCompositeStore_CreateGroup_MultipleGroupsPerProject(t *testing.T) { }) require.NoError(t, err) - // Create agents group + // Create agents group. agentsGroupID := uuid.New().String() err = cs.CreateGroup(ctx, &store.Group{ ID: agentsGroupID, @@ -303,7 +242,7 @@ func TestCompositeStore_CreateGroup_MultipleGroupsPerProject(t *testing.T) { }) require.NoError(t, err, "agents group creation should succeed") - // Create members group for the same project — this must NOT fail + // Create members group for the same project — this must NOT fail. membersGroupID := uuid.New().String() err = cs.CreateGroup(ctx, &store.Group{ ID: membersGroupID, @@ -314,7 +253,7 @@ func TestCompositeStore_CreateGroup_MultipleGroupsPerProject(t *testing.T) { }) require.NoError(t, err, "members group creation should succeed for same project") - // Verify both groups exist with the correct project ID + // Verify both groups exist with the correct project ID. agents, err := cs.GetGroup(ctx, agentsGroupID) require.NoError(t, err) assert.Equal(t, projectID, agents.ProjectID) diff --git a/pkg/store/entadapter/dialect.go b/pkg/store/entadapter/dialect.go new file mode 100644 index 000000000..74be5a775 --- /dev/null +++ b/pkg/store/entadapter/dialect.go @@ -0,0 +1,80 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package entadapter + +import ( + "entgo.io/ent/dialect" + entsql "entgo.io/ent/dialect/sql" + + "github.com/GoogleCloudPlatform/scion/pkg/ent/agent" + "github.com/GoogleCloudPlatform/scion/pkg/ent/predicate" +) + +// ancestryContains returns an Ent predicate restricting results to agents whose +// `ancestry` JSON array contains principalID. +// +// JSON-array membership has no portable SQL spelling, so this is the one agent +// query that must dialect-switch its raw fragment. Both dialects expand the +// stored JSON array into a row set and test for membership inside a correlated +// EXISTS subquery, which composes cleanly with the surrounding typed Ent query +// (soft-delete predicate, ordering, pagination, COUNT): +// +// SQLite: EXISTS (SELECT 1 FROM json_each(ancestry) +// WHERE json_each.value = ?) +// Postgres: EXISTS (SELECT 1 FROM jsonb_array_elements_text(ancestry) AS elem +// WHERE elem = $n) +// +// Two dialect details are load-bearing: +// +// - Function name: Ent stores field.TypeJSON as `jsonb` on Postgres, so the +// set-returning function must be jsonb_array_elements_text (the json_* +// variant only accepts the `json` type). +// - Bind parameter: the fragment is emitted through Builder.Arg, not as a +// literal "?" via ExprP. ExprP writes raw text verbatim and does NOT rebind +// "?" to Postgres' "$n" syntax, which produced a syntax error against +// Postgres. Builder.Arg emits the dialect-correct placeholder ("?" on +// SQLite, "$n" on Postgres) and tracks the argument index. +// +// The dialect is read from the live selector via Builder.Dialect(), so the same +// store works against either backend with no external configuration. +// +// The ancestry IS NOT NULL guard short-circuits agents with no recorded +// lineage and keeps Postgres from invoking the set-returning function on a NULL +// input. +func ancestryContains(principalID string) predicate.Agent { + return func(s *entsql.Selector) { + col := s.C(agent.FieldAncestry) + switch s.Dialect() { + case dialect.Postgres: + s.Where(entsql.P(func(b *entsql.Builder) { + b.WriteString(col). + WriteString(" IS NOT NULL AND EXISTS (SELECT 1 FROM jsonb_array_elements_text("). + WriteString(col). + WriteString(") AS elem WHERE elem = "). + Arg(principalID). + WriteString(")") + })) + default: // SQLite and any other backend providing json_each(). + s.Where(entsql.P(func(b *entsql.Builder) { + b.WriteString(col). + WriteString(" IS NOT NULL AND EXISTS (SELECT 1 FROM json_each("). + WriteString(col). + WriteString(") WHERE json_each.value = "). + Arg(principalID). + WriteString(")") + })) + } + } +} diff --git a/pkg/store/entadapter/external_store.go b/pkg/store/entadapter/external_store.go new file mode 100644 index 000000000..d85846876 --- /dev/null +++ b/pkg/store/entadapter/external_store.go @@ -0,0 +1,548 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package entadapter + +import ( + "context" + "encoding/json" + "strings" + "time" + + "github.com/GoogleCloudPlatform/scion/pkg/ent" + "github.com/GoogleCloudPlatform/scion/pkg/ent/gcpserviceaccount" + "github.com/GoogleCloudPlatform/scion/pkg/ent/githubinstallation" + "github.com/GoogleCloudPlatform/scion/pkg/ent/predicate" + "github.com/GoogleCloudPlatform/scion/pkg/ent/useraccesstoken" + "github.com/GoogleCloudPlatform/scion/pkg/store" +) + +// ExternalStore implements the external-identity store sub-interfaces backed by +// Ent: GCP service accounts, GitHub App installations, and user access tokens. +// +// The legacy api_keys table (superseded by user_access_tokens in V34) is +// schematized in Ent for migration fidelity but has no store interface and is +// intentionally not surfaced here. +type ExternalStore struct { + client *ent.Client +} + +// NewExternalStore creates a new Ent-backed ExternalStore. +func NewExternalStore(client *ent.Client) *ExternalStore { + return &ExternalStore{client: client} +} + +// ============================================================================ +// GCP Service Accounts +// ============================================================================ + +// entGCPToStore converts an Ent GCPServiceAccount to the store model. +func entGCPToStore(e *ent.GCPServiceAccount) *store.GCPServiceAccount { + sa := &store.GCPServiceAccount{ + ID: e.ID.String(), + Scope: e.Scope, + ScopeID: e.ScopeID, + Email: e.Email, + ProjectID: e.ProjectID, + DisplayName: e.DisplayName, + Verified: e.Verified, + CreatedBy: e.CreatedBy, + CreatedAt: e.Created, + Managed: e.Managed, + ManagedBy: e.ManagedBy, + } + // default_scopes is stored as a CSV string for parity with the SQLite store. + if e.DefaultScopes != "" { + sa.DefaultScopes = strings.Split(e.DefaultScopes, ",") + } + if e.VerifiedAt != nil { + sa.VerifiedAt = *e.VerifiedAt + } + return sa +} + +// CreateGCPServiceAccount registers a new GCP service account. +func (s *ExternalStore) CreateGCPServiceAccount(ctx context.Context, sa *store.GCPServiceAccount) error { + id, err := parseUUID(sa.ID) + if err != nil { + return err + } + if sa.CreatedAt.IsZero() { + sa.CreatedAt = time.Now() + } + + create := s.client.GCPServiceAccount.Create(). + SetID(id). + SetScope(sa.Scope). + SetScopeID(sa.ScopeID). + SetEmail(sa.Email). + SetProjectID(sa.ProjectID). + SetDisplayName(sa.DisplayName). + SetDefaultScopes(strings.Join(sa.DefaultScopes, ",")). + SetVerified(sa.Verified). + SetCreatedBy(sa.CreatedBy). + SetManaged(sa.Managed). + SetManagedBy(sa.ManagedBy). + SetCreated(sa.CreatedAt) + + if !sa.VerifiedAt.IsZero() { + create.SetVerifiedAt(sa.VerifiedAt) + } + + if _, err := create.Save(ctx); err != nil { + return mapError(err) + } + return nil +} + +// GetGCPServiceAccount retrieves a GCP service account by ID. +func (s *ExternalStore) GetGCPServiceAccount(ctx context.Context, id string) (*store.GCPServiceAccount, error) { + uid, err := parseGetID(id) + if err != nil { + return nil, err + } + e, err := s.client.GCPServiceAccount.Get(ctx, uid) + if err != nil { + return nil, mapError(err) + } + return entGCPToStore(e), nil +} + +// UpdateGCPServiceAccount updates a GCP service account record. +func (s *ExternalStore) UpdateGCPServiceAccount(ctx context.Context, sa *store.GCPServiceAccount) error { + id, err := parseUUID(sa.ID) + if err != nil { + return err + } + update := s.client.GCPServiceAccount.UpdateOneID(id). + SetEmail(sa.Email). + SetProjectID(sa.ProjectID). + SetDisplayName(sa.DisplayName). + SetDefaultScopes(strings.Join(sa.DefaultScopes, ",")). + SetVerified(sa.Verified). + SetManaged(sa.Managed). + SetManagedBy(sa.ManagedBy) + + if sa.VerifiedAt.IsZero() { + update.ClearVerifiedAt() + } else { + update.SetVerifiedAt(sa.VerifiedAt) + } + + if _, err := update.Save(ctx); err != nil { + return mapError(err) + } + return nil +} + +// DeleteGCPServiceAccount removes a GCP service account by ID. +func (s *ExternalStore) DeleteGCPServiceAccount(ctx context.Context, id string) error { + uid, err := parseUUID(id) + if err != nil { + return err + } + if err := s.client.GCPServiceAccount.DeleteOneID(uid).Exec(ctx); err != nil { + return mapError(err) + } + return nil +} + +// gcpFilterPredicates builds the Ent predicates for a GCPServiceAccountFilter. +func gcpFilterPredicates(filter store.GCPServiceAccountFilter) []predicate.GCPServiceAccount { + var preds []predicate.GCPServiceAccount + if filter.Scope != "" { + preds = append(preds, gcpserviceaccount.ScopeEQ(filter.Scope)) + } + if filter.ScopeID != "" { + preds = append(preds, gcpserviceaccount.ScopeIDEQ(filter.ScopeID)) + } + if filter.Email != "" { + preds = append(preds, gcpserviceaccount.EmailEQ(filter.Email)) + } + if filter.Managed != nil { + preds = append(preds, gcpserviceaccount.ManagedEQ(*filter.Managed)) + } + return preds +} + +// ListGCPServiceAccounts returns GCP service accounts matching the filter. +func (s *ExternalStore) ListGCPServiceAccounts(ctx context.Context, filter store.GCPServiceAccountFilter) ([]store.GCPServiceAccount, error) { + rows, err := s.client.GCPServiceAccount.Query(). + Where(gcpFilterPredicates(filter)...). + Order(gcpserviceaccount.ByCreated(entDesc())). + All(ctx) + if err != nil { + return nil, err + } + out := make([]store.GCPServiceAccount, 0, len(rows)) + for _, e := range rows { + out = append(out, *entGCPToStore(e)) + } + return out, nil +} + +// CountGCPServiceAccounts returns the number of GCP service accounts matching the filter. +func (s *ExternalStore) CountGCPServiceAccounts(ctx context.Context, filter store.GCPServiceAccountFilter) (int, error) { + return s.client.GCPServiceAccount.Query(). + Where(gcpFilterPredicates(filter)...). + Count(ctx) +} + +// ============================================================================ +// GitHub App Installations +// ============================================================================ + +// marshalRepos serializes the repositories slice to the JSON string stored in +// the dialect-neutral repositories column. +func marshalRepos(repos []string) string { + if repos == nil { + repos = []string{} + } + b, _ := json.Marshal(repos) + return string(b) +} + +// entGitHubToStore converts an Ent GithubInstallation to the store model. +func entGitHubToStore(e *ent.GithubInstallation) *store.GitHubInstallation { + inst := &store.GitHubInstallation{ + InstallationID: e.ID, + AccountLogin: e.AccountLogin, + AccountType: e.AccountType, + AppID: e.AppID, + Status: e.Status, + CreatedAt: e.Created, + UpdatedAt: e.Updated, + } + if e.Repositories != "" { + _ = json.Unmarshal([]byte(e.Repositories), &inst.Repositories) + } + return inst +} + +// CreateGitHubInstallation creates a new GitHub App installation record. +// +// installation_id is the GitHub-provided natural key; mirroring the legacy +// "INSERT OR IGNORE" behavior, creating an installation that already exists is a +// no-op (idempotent) rather than an error. +func (s *ExternalStore) CreateGitHubInstallation(ctx context.Context, installation *store.GitHubInstallation) error { + // Idempotency guard: if the natural key already exists, do nothing. + exists, err := s.client.GithubInstallation.Query(). + Where(githubinstallation.IDEQ(installation.InstallationID)). + Exist(ctx) + if err != nil { + return err + } + if exists { + return nil + } + + if installation.CreatedAt.IsZero() { + installation.CreatedAt = time.Now() + } + if installation.UpdatedAt.IsZero() { + installation.UpdatedAt = installation.CreatedAt + } + if installation.Status == "" { + installation.Status = store.GitHubInstallationStatusActive + } + accountType := installation.AccountType + if accountType == "" { + accountType = "Organization" + } + + err = s.client.GithubInstallation.Create(). + SetID(installation.InstallationID). + SetAccountLogin(installation.AccountLogin). + SetAccountType(accountType). + SetAppID(installation.AppID). + SetRepositories(marshalRepos(installation.Repositories)). + SetStatus(installation.Status). + SetCreated(installation.CreatedAt). + SetUpdated(installation.UpdatedAt). + Exec(ctx) + if err != nil { + // Another writer may have created it concurrently — stay idempotent. + if ent.IsConstraintError(err) { + return nil + } + return mapError(err) + } + return nil +} + +// GetGitHubInstallation retrieves a GitHub App installation by installation ID. +func (s *ExternalStore) GetGitHubInstallation(ctx context.Context, installationID int64) (*store.GitHubInstallation, error) { + e, err := s.client.GithubInstallation.Get(ctx, installationID) + if err != nil { + return nil, mapError(err) + } + return entGitHubToStore(e), nil +} + +// UpdateGitHubInstallation updates an existing GitHub App installation. +func (s *ExternalStore) UpdateGitHubInstallation(ctx context.Context, installation *store.GitHubInstallation) error { + installation.UpdatedAt = time.Now() + + _, err := s.client.GithubInstallation.UpdateOneID(installation.InstallationID). + SetAccountLogin(installation.AccountLogin). + SetAccountType(installation.AccountType). + SetAppID(installation.AppID). + SetRepositories(marshalRepos(installation.Repositories)). + SetStatus(installation.Status). + SetUpdated(installation.UpdatedAt). + Save(ctx) + if err != nil { + return mapError(err) + } + return nil +} + +// DeleteGitHubInstallation removes a GitHub App installation by installation ID. +func (s *ExternalStore) DeleteGitHubInstallation(ctx context.Context, installationID int64) error { + if err := s.client.GithubInstallation.DeleteOneID(installationID).Exec(ctx); err != nil { + return mapError(err) + } + return nil +} + +// ListGitHubInstallations returns all GitHub App installations matching the filter. +func (s *ExternalStore) ListGitHubInstallations(ctx context.Context, filter store.GitHubInstallationFilter) ([]store.GitHubInstallation, error) { + query := s.client.GithubInstallation.Query() + if filter.AccountLogin != "" { + query = query.Where(githubinstallation.AccountLoginEQ(filter.AccountLogin)) + } + if filter.Status != "" { + query = query.Where(githubinstallation.StatusEQ(filter.Status)) + } + if filter.AppID != 0 { + query = query.Where(githubinstallation.AppIDEQ(filter.AppID)) + } + + rows, err := query.Order(githubinstallation.ByCreated()).All(ctx) + if err != nil { + return nil, err + } + + // Never return a nil slice (parity with the SQLite store). + results := make([]store.GitHubInstallation, 0, len(rows)) + for _, e := range rows { + results = append(results, *entGitHubToStore(e)) + } + return results, nil +} + +// GetInstallationForRepository returns an active GitHub App installation that +// covers the given repository (owner/repo format). +func (s *ExternalStore) GetInstallationForRepository(ctx context.Context, repoFullName string) (*store.GitHubInstallation, error) { + // Scan active installations whose repositories JSON array contains the repo. + installations, err := s.ListGitHubInstallations(ctx, store.GitHubInstallationFilter{ + Status: store.GitHubInstallationStatusActive, + }) + if err != nil { + return nil, err + } + + for i := range installations { + for _, repo := range installations[i].Repositories { + if repo == repoFullName { + return &installations[i], nil + } + } + } + return nil, store.ErrNotFound +} + +// ============================================================================ +// User Access Tokens (UATs) +// ============================================================================ + +// marshalScopes serializes token scopes to the JSON string stored in the scopes +// column. The column is NotEmpty, so a nil/empty slice serializes to "[]". +func marshalScopes(scopes []string) string { + if scopes == nil { + scopes = []string{} + } + b, _ := json.Marshal(scopes) + return string(b) +} + +// entUATToStore converts an Ent UserAccessToken to the store model. +func entUATToStore(e *ent.UserAccessToken) *store.UserAccessToken { + t := &store.UserAccessToken{ + ID: e.ID.String(), + UserID: e.UserID.String(), + Name: e.Name, + Prefix: e.Prefix, + KeyHash: e.KeyHash, + ProjectID: e.ProjectID.String(), + Revoked: e.Revoked, + Created: e.Created, + } + if e.Scopes != "" { + _ = json.Unmarshal([]byte(e.Scopes), &t.Scopes) + } + if e.ExpiresAt != nil { + t.ExpiresAt = e.ExpiresAt + } + if e.LastUsed != nil { + t.LastUsed = e.LastUsed + } + return t +} + +// CreateUserAccessToken creates a new user access token record. +func (s *ExternalStore) CreateUserAccessToken(ctx context.Context, token *store.UserAccessToken) error { + id, err := parseUUID(token.ID) + if err != nil { + return err + } + userUID, err := parseUUID(token.UserID) + if err != nil { + return err + } + projectUID, err := parseUUID(token.ProjectID) + if err != nil { + return err + } + + if token.Created.IsZero() { + token.Created = time.Now() + } + + create := s.client.UserAccessToken.Create(). + SetID(id). + SetUserID(userUID). + SetName(token.Name). + SetPrefix(token.Prefix). + SetKeyHash(token.KeyHash). + SetProjectID(projectUID). + SetScopes(marshalScopes(token.Scopes)). + SetRevoked(token.Revoked). + SetCreated(token.Created) + + if token.ExpiresAt != nil { + create.SetExpiresAt(*token.ExpiresAt) + } + if token.LastUsed != nil { + create.SetLastUsed(*token.LastUsed) + } + + if _, err := create.Save(ctx); err != nil { + return mapError(err) + } + return nil +} + +// GetUserAccessToken retrieves a user access token by ID. +func (s *ExternalStore) GetUserAccessToken(ctx context.Context, id string) (*store.UserAccessToken, error) { + uid, err := parseGetID(id) + if err != nil { + return nil, err + } + e, err := s.client.UserAccessToken.Get(ctx, uid) + if err != nil { + return nil, mapError(err) + } + return entUATToStore(e), nil +} + +// GetUserAccessTokenByHash retrieves a user access token by its key hash. +func (s *ExternalStore) GetUserAccessTokenByHash(ctx context.Context, hash string) (*store.UserAccessToken, error) { + e, err := s.client.UserAccessToken.Query(). + Where(useraccesstoken.KeyHashEQ(hash)). + Only(ctx) + if err != nil { + return nil, mapError(err) + } + return entUATToStore(e), nil +} + +// UpdateUserAccessTokenLastUsed updates the last used timestamp. +// Mirrors the SQLite store: a missing token is not treated as an error. +func (s *ExternalStore) UpdateUserAccessTokenLastUsed(ctx context.Context, id string) error { + uid, err := parseUUID(id) + if err != nil { + return err + } + _, err = s.client.UserAccessToken.Update(). + Where(useraccesstoken.IDEQ(uid)). + SetLastUsed(time.Now()). + Save(ctx) + return err +} + +// RevokeUserAccessToken marks a token as revoked. +func (s *ExternalStore) RevokeUserAccessToken(ctx context.Context, id string) error { + uid, err := parseUUID(id) + if err != nil { + return err + } + if _, err := s.client.UserAccessToken.UpdateOneID(uid).SetRevoked(true).Save(ctx); err != nil { + return mapError(err) + } + return nil +} + +// DeleteUserAccessToken permanently removes a token by ID. +func (s *ExternalStore) DeleteUserAccessToken(ctx context.Context, id string) error { + uid, err := parseUUID(id) + if err != nil { + return err + } + if err := s.client.UserAccessToken.DeleteOneID(uid).Exec(ctx); err != nil { + return mapError(err) + } + return nil +} + +// ListUserAccessTokens returns all tokens for a user, newest first. +func (s *ExternalStore) ListUserAccessTokens(ctx context.Context, userID string) ([]store.UserAccessToken, error) { + uid, err := parseUUID(userID) + if err != nil { + return nil, err + } + rows, err := s.client.UserAccessToken.Query(). + Where(useraccesstoken.UserIDEQ(uid)). + Order(useraccesstoken.ByCreated(entDesc())). + All(ctx) + if err != nil { + return nil, err + } + out := make([]store.UserAccessToken, 0, len(rows)) + for _, e := range rows { + out = append(out, *entUATToStore(e)) + } + return out, nil +} + +// CountUserAccessTokens returns the number of active (non-revoked) tokens for a user. +func (s *ExternalStore) CountUserAccessTokens(ctx context.Context, userID string) (int, error) { + uid, err := parseUUID(userID) + if err != nil { + return 0, err + } + return s.client.UserAccessToken.Query(). + Where( + useraccesstoken.UserIDEQ(uid), + useraccesstoken.RevokedEQ(false), + ). + Count(ctx) +} + +// Ensure ExternalStore satisfies the external-identity store sub-interfaces. +var ( + _ store.GCPServiceAccountStore = (*ExternalStore)(nil) + _ store.GitHubInstallationStore = (*ExternalStore)(nil) + _ store.UserAccessTokenStore = (*ExternalStore)(nil) +) diff --git a/pkg/store/entadapter/external_store_test.go b/pkg/store/entadapter/external_store_test.go new file mode 100644 index 000000000..5a715de44 --- /dev/null +++ b/pkg/store/entadapter/external_store_test.go @@ -0,0 +1,225 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//go:build !no_sqlite + +package entadapter + +import ( + "context" + "testing" + "time" + + "github.com/GoogleCloudPlatform/scion/pkg/store" + "github.com/GoogleCloudPlatform/scion/pkg/store/enttest" + "github.com/google/uuid" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func newTestExternalStore(t *testing.T) *ExternalStore { + t.Helper() + client := enttest.NewClient(t) + return NewExternalStore(client) +} + +func TestExternalStore_GCPServiceAccountCRUD(t *testing.T) { + ctx := context.Background() + s := newTestExternalStore(t) + + projectID := uuid.NewString() + sa := &store.GCPServiceAccount{ + ID: uuid.NewString(), + Scope: "project", + ScopeID: projectID, + Email: "agent@project.iam.gserviceaccount.com", + ProjectID: projectID, + DisplayName: "Worker SA", + DefaultScopes: []string{"https://www.googleapis.com/auth/cloud-platform"}, + Verified: true, + VerifiedAt: time.Now().UTC().Truncate(time.Second), + CreatedBy: "tester", + Managed: true, + ManagedBy: "hub-1", + } + require.NoError(t, s.CreateGCPServiceAccount(ctx, sa)) + + got, err := s.GetGCPServiceAccount(ctx, sa.ID) + require.NoError(t, err) + assert.Equal(t, sa.Email, got.Email) + assert.Equal(t, []string{"https://www.googleapis.com/auth/cloud-platform"}, got.DefaultScopes) + assert.True(t, got.Verified) + assert.False(t, got.VerifiedAt.IsZero()) + assert.True(t, got.Managed) + + // Duplicate (email, scope, scope_id) -> ErrAlreadyExists. + dup := *sa + dup.ID = uuid.NewString() + assert.ErrorIs(t, s.CreateGCPServiceAccount(ctx, &dup), store.ErrAlreadyExists) + + // Update. + got.DisplayName = "Renamed SA" + got.Verified = false + got.VerifiedAt = time.Time{} + require.NoError(t, s.UpdateGCPServiceAccount(ctx, got)) + got, err = s.GetGCPServiceAccount(ctx, sa.ID) + require.NoError(t, err) + assert.Equal(t, "Renamed SA", got.DisplayName) + assert.False(t, got.Verified) + assert.True(t, got.VerifiedAt.IsZero()) + + // Filter + count. + managed := true + list, err := s.ListGCPServiceAccounts(ctx, store.GCPServiceAccountFilter{Scope: "project", Managed: &managed}) + require.NoError(t, err) + assert.Len(t, list, 1) + + count, err := s.CountGCPServiceAccounts(ctx, store.GCPServiceAccountFilter{Scope: "project"}) + require.NoError(t, err) + assert.Equal(t, 1, count) + + // Delete. + require.NoError(t, s.DeleteGCPServiceAccount(ctx, sa.ID)) + _, err = s.GetGCPServiceAccount(ctx, sa.ID) + assert.ErrorIs(t, err, store.ErrNotFound) + assert.ErrorIs(t, s.DeleteGCPServiceAccount(ctx, sa.ID), store.ErrNotFound) +} + +func TestExternalStore_GitHubInstallation(t *testing.T) { + ctx := context.Background() + s := newTestExternalStore(t) + + inst := &store.GitHubInstallation{ + InstallationID: 12345, + AccountLogin: "acme", + AccountType: "Organization", + AppID: 999, + Repositories: []string{"acme/repo1", "acme/repo2"}, + } + require.NoError(t, s.CreateGitHubInstallation(ctx, inst)) + + got, err := s.GetGitHubInstallation(ctx, 12345) + require.NoError(t, err) + assert.Equal(t, "acme", got.AccountLogin) + assert.Equal(t, store.GitHubInstallationStatusActive, got.Status) + assert.Equal(t, []string{"acme/repo1", "acme/repo2"}, got.Repositories) + + // Create with the same installation_id is an idempotent no-op (INSERT OR IGNORE). + dup := &store.GitHubInstallation{ + InstallationID: 12345, + AccountLogin: "changed", + AppID: 1, + } + require.NoError(t, s.CreateGitHubInstallation(ctx, dup)) + got, err = s.GetGitHubInstallation(ctx, 12345) + require.NoError(t, err) + assert.Equal(t, "acme", got.AccountLogin, "duplicate create must not overwrite") + + // Repository lookup. + found, err := s.GetInstallationForRepository(ctx, "acme/repo2") + require.NoError(t, err) + assert.Equal(t, int64(12345), found.InstallationID) + + _, err = s.GetInstallationForRepository(ctx, "other/repo") + assert.ErrorIs(t, err, store.ErrNotFound) + + // Update. + got.Repositories = []string{"acme/repo3"} + got.Status = store.GitHubInstallationStatusSuspended + require.NoError(t, s.UpdateGitHubInstallation(ctx, got)) + got, err = s.GetGitHubInstallation(ctx, 12345) + require.NoError(t, err) + assert.Equal(t, []string{"acme/repo3"}, got.Repositories) + assert.Equal(t, store.GitHubInstallationStatusSuspended, got.Status) + + // List filter by status. + active, err := s.ListGitHubInstallations(ctx, store.GitHubInstallationFilter{Status: store.GitHubInstallationStatusActive}) + require.NoError(t, err) + assert.Empty(t, active) + + // Delete. + require.NoError(t, s.DeleteGitHubInstallation(ctx, 12345)) + _, err = s.GetGitHubInstallation(ctx, 12345) + assert.ErrorIs(t, err, store.ErrNotFound) +} + +func TestExternalStore_UserAccessToken(t *testing.T) { + ctx := context.Background() + s := newTestExternalStore(t) + + userID := uuid.NewString() + projectID := uuid.NewString() + expires := time.Now().Add(24 * time.Hour).UTC().Truncate(time.Second) + + token := &store.UserAccessToken{ + ID: uuid.NewString(), + UserID: userID, + Name: "ci-token", + Prefix: "scion_pat_abc", + KeyHash: "hash-1", + ProjectID: projectID, + Scopes: []string{"project:read", "agent:list"}, + ExpiresAt: &expires, + } + require.NoError(t, s.CreateUserAccessToken(ctx, token)) + + got, err := s.GetUserAccessToken(ctx, token.ID) + require.NoError(t, err) + assert.Equal(t, "ci-token", got.Name) + assert.Equal(t, []string{"project:read", "agent:list"}, got.Scopes) + require.NotNil(t, got.ExpiresAt) + + // Lookup by hash. + byHash, err := s.GetUserAccessTokenByHash(ctx, "hash-1") + require.NoError(t, err) + assert.Equal(t, token.ID, byHash.ID) + + _, err = s.GetUserAccessTokenByHash(ctx, "missing") + assert.ErrorIs(t, err, store.ErrNotFound) + + // Duplicate hash -> ErrAlreadyExists. + dup := *token + dup.ID = uuid.NewString() + assert.ErrorIs(t, s.CreateUserAccessToken(ctx, &dup), store.ErrAlreadyExists) + + // LastUsed update. + require.NoError(t, s.UpdateUserAccessTokenLastUsed(ctx, token.ID)) + got, err = s.GetUserAccessToken(ctx, token.ID) + require.NoError(t, err) + require.NotNil(t, got.LastUsed) + + // Count active tokens. + count, err := s.CountUserAccessTokens(ctx, userID) + require.NoError(t, err) + assert.Equal(t, 1, count) + + // Revoke removes from active count but the row still exists. + require.NoError(t, s.RevokeUserAccessToken(ctx, token.ID)) + got, err = s.GetUserAccessToken(ctx, token.ID) + require.NoError(t, err) + assert.True(t, got.Revoked) + count, err = s.CountUserAccessTokens(ctx, userID) + require.NoError(t, err) + assert.Equal(t, 0, count) + + list, err := s.ListUserAccessTokens(ctx, userID) + require.NoError(t, err) + assert.Len(t, list, 1) + + // Delete. + require.NoError(t, s.DeleteUserAccessToken(ctx, token.ID)) + _, err = s.GetUserAccessToken(ctx, token.ID) + assert.ErrorIs(t, err, store.ErrNotFound) + assert.ErrorIs(t, s.RevokeUserAccessToken(ctx, token.ID), store.ErrNotFound) +} diff --git a/pkg/store/entadapter/group_store.go b/pkg/store/entadapter/group_store.go index 48e768ddd..19ecc5b13 100644 --- a/pkg/store/entadapter/group_store.go +++ b/pkg/store/entadapter/group_store.go @@ -18,6 +18,7 @@ package entadapter import ( "context" "fmt" + "strings" "github.com/GoogleCloudPlatform/scion/pkg/ent" "github.com/GoogleCloudPlatform/scion/pkg/ent/agent" @@ -47,6 +48,19 @@ func parseUUID(s string) (uuid.UUID, error) { return uid, nil } +// parseGetID parses a primary-key identifier for a get-by-id lookup. A malformed +// identifier cannot match any UUID primary key, so it is reported as +// store.ErrNotFound — matching the raw-SQL store, where such a lookup simply +// returned no row (callers like resolveTemplate rely on ErrNotFound to fall back +// to slug-based resolution). +func parseGetID(s string) (uuid.UUID, error) { + uid, err := uuid.Parse(s) + if err != nil { + return uuid.Nil, store.ErrNotFound + } + return uid, nil +} + // mapError converts Ent errors to store errors. func mapError(err error) error { if err == nil { @@ -56,6 +70,16 @@ func mapError(err error) error { return store.ErrNotFound } if ent.IsConstraintError(err) { + // Both unique-constraint and foreign-key violations surface as Ent + // constraint errors, but they mean very different things: a unique + // violation is a duplicate (ErrAlreadyExists), while a foreign-key + // violation is a reference to a row that does not exist (ErrInvalidInput). + // Mapping both to ErrAlreadyExists produced a misleading "already exists" + // (HTTP 409) for what is really a bad reference. + msg := strings.ToLower(err.Error()) + if strings.Contains(msg, "foreign key") || strings.Contains(msg, "sqlstate 23503") { + return fmt.Errorf("%w: %v", store.ErrInvalidInput, err) + } return store.ErrAlreadyExists } return err @@ -164,7 +188,7 @@ func (s *GroupStore) CreateGroup(ctx context.Context, g *store.Group) error { // GetGroup retrieves a group by ID. func (s *GroupStore) GetGroup(ctx context.Context, id string) (*store.Group, error) { - uid, err := parseUUID(id) + uid, err := parseGetID(id) if err != nil { return nil, err } @@ -868,10 +892,8 @@ func (s *GroupStore) CheckDelegatedAccess(ctx context.Context, agentID string, c return false, err } - // Load agent with creator edge a, err := s.client.Agent.Query(). Where(agent.IDEQ(uid)). - WithCreator(). Only(ctx) if err != nil { return false, mapError(err) @@ -882,11 +904,20 @@ func (s *GroupStore) CheckDelegatedAccess(ctx context.Context, agentID string, c return false, nil } - // Check creator exists - creator := a.Edges.Creator - if creator == nil { + // created_by is a polymorphic principal reference: it may be a user or + // another agent. Delegation only flows from a *user* creator, so resolve + // the creator as a user by ID and bail out when there is none (no creator, + // or the creator is an agent rather than a user). + if a.CreatedBy == nil { return false, nil } + creator, err := s.client.User.Get(ctx, *a.CreatedBy) + if err != nil { + if ent.IsNotFound(err) { + return false, nil + } + return false, mapError(err) + } // Suspended creators cannot be delegation sources if creator.Status == user.StatusSuspended { diff --git a/pkg/store/entadapter/group_store_test.go b/pkg/store/entadapter/group_store_test.go index 7f8109b64..416906eff 100644 --- a/pkg/store/entadapter/group_store_test.go +++ b/pkg/store/entadapter/group_store_test.go @@ -20,8 +20,8 @@ import ( "context" "testing" - "github.com/GoogleCloudPlatform/scion/pkg/ent/entc" "github.com/GoogleCloudPlatform/scion/pkg/store" + "github.com/GoogleCloudPlatform/scion/pkg/store/enttest" "github.com/google/uuid" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -29,13 +29,10 @@ import ( func newTestGroupStore(t *testing.T) *GroupStore { t.Helper() - client, err := entc.OpenSQLite("file:" + t.Name() + "?mode=memory&cache=shared") - require.NoError(t, err) - t.Cleanup(func() { client.Close() }) - require.NoError(t, entc.AutoMigrate(context.Background(), client)) + client := enttest.NewClient(t) // Create a test user for membership tests - _, err = client.User.Create(). + _, err := client.User.Create(). SetID(testUserUID). SetEmail("test@example.com"). SetDisplayName("Test User"). @@ -816,14 +813,9 @@ func TestGetEffectiveGroupsNoMemberships(t *testing.T) { func TestCompositeStoreDelegation(t *testing.T) { // Verify the CompositeStore properly delegates group operations - client, err := entc.OpenSQLite("file:" + t.Name() + "?mode=memory&cache=shared") - require.NoError(t, err) - t.Cleanup(func() { client.Close() }) - require.NoError(t, entc.AutoMigrate(context.Background(), client)) + client := enttest.NewClient(t) - // We use nil as the base store since we're only testing group methods - // and they should all go to the Ent adapter. - composite := NewCompositeStore(nil, client) + composite := NewCompositeStore(client) ctx := context.Background() g := &store.Group{ @@ -832,7 +824,7 @@ func TestCompositeStoreDelegation(t *testing.T) { Slug: "composite-test", } - err = composite.CreateGroup(ctx, g) + err := composite.CreateGroup(ctx, g) require.NoError(t, err) got, err := composite.GetGroup(ctx, g.ID) @@ -1084,7 +1076,7 @@ func TestCheckDelegatedAccess_Enabled(t *testing.T) { // Enable delegation on the test agent and set creator _, err := gs.client.Agent.UpdateOneID(testAgentUID). SetDelegationEnabled(true). - SetCreatorID(testUserUID). + SetCreatedBy(testUserUID). Save(ctx) require.NoError(t, err) @@ -1106,7 +1098,7 @@ func TestCheckDelegatedAccess_Disabled(t *testing.T) { // Creator is set but delegation is disabled (default) _, err := gs.client.Agent.UpdateOneID(testAgentUID). - SetCreatorID(testUserUID). + SetCreatedBy(testUserUID). Save(ctx) require.NoError(t, err) @@ -1134,7 +1126,7 @@ func TestCheckDelegatedAccess_SuspendedCreator(t *testing.T) { // Enable delegation _, err = gs.client.Agent.UpdateOneID(testAgentUID). SetDelegationEnabled(true). - SetCreatorID(testUserUID). + SetCreatedBy(testUserUID). Save(ctx) require.NoError(t, err) @@ -1191,7 +1183,7 @@ func TestCheckDelegatedAccess_GroupCondition(t *testing.T) { // Enable delegation and set creator _, err := gs.client.Agent.UpdateOneID(testAgentUID). SetDelegationEnabled(true). - SetCreatorID(testUserUID). + SetCreatedBy(testUserUID). Save(ctx) require.NoError(t, err) @@ -1219,7 +1211,7 @@ func TestCheckDelegatedAccess_GroupCondition_NotMember(t *testing.T) { // Enable delegation and set creator _, err := gs.client.Agent.UpdateOneID(testAgentUID). SetDelegationEnabled(true). - SetCreatorID(testUserUID). + SetCreatedBy(testUserUID). Save(ctx) require.NoError(t, err) diff --git a/pkg/store/entadapter/locking.go b/pkg/store/entadapter/locking.go new file mode 100644 index 000000000..6b962e252 --- /dev/null +++ b/pkg/store/entadapter/locking.go @@ -0,0 +1,309 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package entadapter + +import ( + "context" + "database/sql" + "errors" + "fmt" + "time" + + "entgo.io/ent/dialect" + + "github.com/GoogleCloudPlatform/scion/pkg/store" +) + +// advisoryLockTimeout bounds the two short, non-blocking database operations the +// advisory lock performs: checking a connection out of the pool + running +// pg_try_advisory_lock on acquire, and running pg_advisory_unlock on release. +// +// It is deliberately MUCH shorter than the scheduler's 55s per-handler timeout. +// Both operations are expected to complete in milliseconds: pg_try_advisory_lock +// never waits on the lock (it returns immediately), and checking out a +// connection only blocks when the pool has no usable connection to hand back. +// +// Binding acquire to a short deadline keeps a single bad tick cheap. If the pool +// cannot produce a healthy connection quickly we want to fail this tick fast and +// retry on the next one, NOT block a scheduler goroutine (and its pending pool +// connection request) for nearly the whole 55s window. Letting acquisition hang +// for ~55s lets slow ticks overlap across the 60s scheduler interval and across +// the several singleton handlers that fire each minute, which compounds pool +// pressure instead of shedding it. +// +// Binding release with its own fresh deadline (rather than context.Background) +// guarantees the unlock cannot hang forever on a connection that died while the +// lock was held, which would otherwise prevent conn.Close() from ever running +// and leak the connection out of the pool permanently. +const advisoryLockTimeout = 5 * time.Second + +// This file implements the dialect-aware cluster-coordination primitives that +// let N stateless hub processes share one database safely (multi-replica +// Postgres, D3). Every helper degrades to a correct single-process no-op on +// SQLite, where there is only ever one writer. +// +// Compile-time assertions that the Ent-backed store provides the optional +// cluster-coordination capabilities. AdvisoryLocker lives on CompositeStore; +// ScheduledEventClaimer is provided by the embedded ScheduleStore and is thus +// promoted onto CompositeStore as well. +var ( + _ store.AdvisoryLocker = (*CompositeStore)(nil) + _ store.ScheduledEventClaimer = (*ScheduleStore)(nil) + _ store.ScheduledEventClaimer = (*CompositeStore)(nil) +) + +// TryAdvisoryLockObject acquires a per-object advisory lock using Postgres's +// two-integer form: pg_try_advisory_lock(int4 classid, int4 objid). +// +// This is the per-project provisioning guard. Two agents for the same project +// (same classID + same objID derived from the project ID hash) contend on the +// same lock; agents for different projects never contend. +// +// The implementation mirrors TryAdvisoryLock but uses the two-int form for +// both lock and unlock. On SQLite it is a no-op (always acquired). +func (c *CompositeStore) TryAdvisoryLockObject(ctx context.Context, classID store.AdvisoryLockKey, objID int32) (bool, func() error, error) { + if !c.isPostgres() { + return true, noopRelease, nil + } + + db := c.DB() + if db == nil { + return true, noopRelease, nil + } + + acquireCtx, cancelAcquire := context.WithTimeout(ctx, advisoryLockTimeout) + defer cancelAcquire() + + conn, err := db.Conn(acquireCtx) + if err != nil { + return false, noopRelease, fmt.Errorf("advisory lock object: acquiring connection: %w", err) + } + + var acquired bool + if err := conn.QueryRowContext(acquireCtx, + "SELECT pg_try_advisory_lock($1, $2)", int32(classID), objID, + ).Scan(&acquired); err != nil { + _ = conn.Close() + return false, noopRelease, fmt.Errorf("advisory lock object: pg_try_advisory_lock(%d, %d): %w", int32(classID), objID, err) + } + + if !acquired { + _ = conn.Close() + return false, noopRelease, nil + } + + release := func() error { + unlockCtx, cancel := context.WithTimeout(context.Background(), advisoryLockTimeout) + defer cancel() + _, unlockErr := conn.ExecContext(unlockCtx, + "SELECT pg_advisory_unlock($1, $2)", int32(classID), objID, + ) + closeErr := conn.Close() + if unlockErr != nil { + return fmt.Errorf("advisory lock object: pg_advisory_unlock(%d, %d): %w", int32(classID), objID, unlockErr) + } + return closeErr + } + return true, release, nil +} + +// isPostgres reports whether the shared Ent client is talking to Postgres. +func (c *CompositeStore) isPostgres() bool { + return c.client.Driver().Dialect() == dialect.Postgres +} + +// noopRelease is returned whenever there is nothing to unlock (SQLite, or a lock +// that was not acquired). It is always safe to call. +func noopRelease() error { return nil } + +// TryAdvisoryLock acquires a cluster-wide advisory lock without blocking. +// +// On Postgres it grabs a dedicated *sql.Conn from the pool and runs +// pg_try_advisory_lock(key) on it. The lock is a SESSION-level lock, so it is +// held for exactly as long as that connection stays checked out: the returned +// release func runs pg_advisory_unlock(key) on the same connection and then +// returns it to the pool. Holding the connection for the duration of the +// critical section is what keeps the lock alive, so callers must keep the work +// short and always call release. +// +// On SQLite (and any non-Postgres backend) the lock is a no-op that always +// succeeds: the single-writer model already guarantees the work runs on one +// process at a time. +func (c *CompositeStore) TryAdvisoryLock(ctx context.Context, key store.AdvisoryLockKey) (bool, func() error, error) { + if !c.isPostgres() { + return true, noopRelease, nil + } + + db := c.DB() + if db == nil { + // No *sql.DB to lock against; fail open to single-process behavior + // rather than blocking cluster work. + return true, noopRelease, nil + } + + // Bound connection checkout + the try-lock query to a short deadline derived + // from ctx (but never longer than advisoryLockTimeout). A healthy pool serves + // these in milliseconds; if it cannot, we fail this tick fast and let the next + // one retry rather than parking a scheduler goroutine for the full 55s. + acquireCtx, cancelAcquire := context.WithTimeout(ctx, advisoryLockTimeout) + defer cancelAcquire() + + conn, err := db.Conn(acquireCtx) + if err != nil { + return false, noopRelease, fmt.Errorf("advisory lock: acquiring connection: %w", err) + } + + var acquired bool + // pg_try_advisory_lock returns immediately: true if the lock was granted, + // false if it is already held (by this or another session). + if err := conn.QueryRowContext(acquireCtx, "SELECT pg_try_advisory_lock($1)", int64(key)).Scan(&acquired); err != nil { + _ = conn.Close() + return false, noopRelease, fmt.Errorf("advisory lock: pg_try_advisory_lock(%d): %w", int64(key), err) + } + + if !acquired { + // Another replica holds it. Return the connection to the pool now. + _ = conn.Close() + return false, noopRelease, nil + } + + // We own the lock. release unlocks on the same connection, then frees it. + // cancelAcquire above only tears down acquireCtx; it does NOT close conn, so + // the session (and therefore the lock) stays alive until release runs. + release := func() error { + // Use a fresh, bounded context detached from the critical section's ctx + // so the unlock still runs even if that ctx was cancelled, but cannot + // hang forever on a connection that silently died while we held the + // lock. Without the bound, a dead connection would block this Exec + // indefinitely, conn.Close() below would never run, and the connection + // would leak out of the pool permanently. Closing the connection would + // also drop the session lock, but unlocking explicitly is cleaner and + // lets the connection be reused. + unlockCtx, cancel := context.WithTimeout(context.Background(), advisoryLockTimeout) + defer cancel() + _, unlockErr := conn.ExecContext(unlockCtx, "SELECT pg_advisory_unlock($1)", int64(key)) + closeErr := conn.Close() + if unlockErr != nil { + return fmt.Errorf("advisory lock: pg_advisory_unlock(%d): %w", int64(key), unlockErr) + } + return closeErr + } + return true, release, nil +} + +// isSerializationFailure reports whether err is a Postgres serialization failure +// that warrants a retry: SQLSTATE 40001 (serialization_failure) or 40P01 +// (deadlock_detected). It matches on the SQLSTATE string carried in the error +// message so it does not need a hard dependency on the pgx error type. +func isSerializationFailure(err error) bool { + if err == nil { + return false + } + type sqlStater interface{ SQLState() string } + var ss sqlStater + if errors.As(err, &ss) { + switch ss.SQLState() { + case "40001", "40P01": + return true + } + } + msg := err.Error() + return contains(msg, "40001") || contains(msg, "40P01") || + contains(msg, "serialization") || contains(msg, "deadlock detected") +} + +// contains is a tiny substring check kept local to avoid importing strings for a +// single call. +func contains(haystack, needle string) bool { + if len(needle) == 0 { + return true + } + for i := 0; i+len(needle) <= len(haystack); i++ { + if haystack[i:i+len(needle)] == needle { + return true + } + } + return false +} + +// maxSerializableRetries bounds the retry loop so a pathologically contended +// transaction cannot spin forever. +const maxSerializableRetries = 5 + +// RunSerializable runs fn inside a transaction and, on Postgres, retries it when +// the transaction aborts with a serialization failure (SQLSTATE 40001/40P01). +// +// It is the multi-row-invariant primitive from P3-4: use it when correctness +// depends on a set of rows being read and written as one atomic snapshot and the +// invariant cannot be reduced to a single-row state_version CAS or a SELECT ... +// FOR UPDATE critical section. +// +// fn MUST be idempotent — it can be invoked more than once. It receives the +// transaction it must use for all its statements; using the ambient pooled +// client instead would escape the serializable snapshot. +// +// - Postgres: BEGIN ISOLATION LEVEL SERIALIZABLE; on commit failure with a +// serialization error, the whole closure is retried up to +// maxSerializableRetries times. +// - SQLite: a single plain transaction with no retry. SQLite executes writes +// serially, so 40001 cannot occur and the SERIALIZABLE escalation is +// unnecessary. +func (c *CompositeStore) RunSerializable(ctx context.Context, fn func(ctx context.Context, tx *sql.Tx) error) error { + db := c.DB() + if db == nil { + return fmt.Errorf("RunSerializable: store is not backed by a *sql.DB") + } + + opts := &sql.TxOptions{} + if c.isPostgres() { + opts.Isolation = sql.LevelSerializable + } + + var lastErr error + attempts := 1 + if c.isPostgres() { + attempts = maxSerializableRetries + } + + for attempt := 0; attempt < attempts; attempt++ { + tx, err := db.BeginTx(ctx, opts) + if err != nil { + if isSerializationFailure(err) { + lastErr = err + continue + } + return err + } + + if err := fn(ctx, tx); err != nil { + _ = tx.Rollback() + if isSerializationFailure(err) { + lastErr = err + continue + } + return err + } + + if err := tx.Commit(); err != nil { + if isSerializationFailure(err) { + lastErr = err + continue + } + return err + } + return nil + } + return fmt.Errorf("RunSerializable: exhausted %d attempts: %w", attempts, lastErr) +} diff --git a/pkg/store/entadapter/locking_test.go b/pkg/store/entadapter/locking_test.go new file mode 100644 index 000000000..3c4b7bb89 --- /dev/null +++ b/pkg/store/entadapter/locking_test.go @@ -0,0 +1,193 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//go:build !no_sqlite + +package entadapter + +import ( + "context" + "database/sql" + "errors" + "sync" + "testing" + + "github.com/GoogleCloudPlatform/scion/pkg/store" + "github.com/google/uuid" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +// On SQLite the advisory lock is a no-op that always succeeds, because the +// single-writer model already makes the guarded work effectively singleton. +func TestTryAdvisoryLock_SQLiteAlwaysAcquires(t *testing.T) { + c := newTestCompositeStore(t) + ctx := context.Background() + + acquired, release, err := c.TryAdvisoryLock(ctx, store.LockScheduleEvaluator) + require.NoError(t, err) + assert.True(t, acquired, "SQLite advisory lock must always acquire") + require.NotNil(t, release) + require.NoError(t, release()) + + // A second concurrent acquisition also succeeds (no real lock on SQLite). + acquired2, release2, err := c.TryAdvisoryLock(ctx, store.LockScheduleEvaluator) + require.NoError(t, err) + assert.True(t, acquired2) + require.NoError(t, release2()) +} + +// The store satisfies the optional AdvisoryLocker capability used by the hub +// scheduler's singleton gating. +func TestCompositeStore_ImplementsAdvisoryLocker(t *testing.T) { + var _ store.AdvisoryLocker = newTestCompositeStore(t) +} + +// RunSerializable runs the closure inside a transaction and commits it on +// SQLite (no isolation escalation, no retry). +func TestRunSerializable_CommitsOnSQLite(t *testing.T) { + c := newTestCompositeStore(t) + ctx := context.Background() + + calls := 0 + err := c.RunSerializable(ctx, func(ctx context.Context, tx *sql.Tx) error { + calls++ + // A trivial read proves the tx is usable. + var one int + return tx.QueryRowContext(ctx, "SELECT 1").Scan(&one) + }) + require.NoError(t, err) + assert.Equal(t, 1, calls, "SQLite must run the closure exactly once (no retry)") +} + +// A non-serialization error from the closure is returned verbatim and the +// transaction is rolled back (no retry loop on SQLite). +func TestRunSerializable_PropagatesError(t *testing.T) { + c := newTestCompositeStore(t) + ctx := context.Background() + + sentinel := errors.New("boom") + calls := 0 + err := c.RunSerializable(ctx, func(ctx context.Context, tx *sql.Tx) error { + calls++ + return sentinel + }) + require.ErrorIs(t, err, sentinel) + assert.Equal(t, 1, calls) +} + +// isSerializationFailure recognizes the Postgres serialization/deadlock SQLSTATEs +// (used to drive the retry loop) without depending on the pgx error type. +func TestIsSerializationFailure(t *testing.T) { + assert.False(t, isSerializationFailure(nil)) + assert.False(t, isSerializationFailure(errors.New("syntax error"))) + assert.True(t, isSerializationFailure(errors.New("pq: could not serialize access due to concurrent update (SQLSTATE 40001)"))) + assert.True(t, isSerializationFailure(errors.New("ERROR: deadlock detected (SQLSTATE 40P01)"))) +} + +// ClaimScheduledEvent is the multi-replica dedup primitive: exactly one caller +// wins the pending->fired transition; a second attempt loses. +func TestClaimScheduledEvent_ExactlyOnce(t *testing.T) { + s := newTestScheduleStore(t) + ctx := context.Background() + + evt := newTestScheduledEvent(uuid.NewString()) + require.NoError(t, s.CreateScheduledEvent(ctx, evt)) + + won, err := s.ClaimScheduledEvent(ctx, evt.ID, store.ScheduledEventFired) + require.NoError(t, err) + assert.True(t, won, "first claim must win") + + // Second claim loses: the event is no longer pending. + won2, err := s.ClaimScheduledEvent(ctx, evt.ID, store.ScheduledEventFired) + require.NoError(t, err) + assert.False(t, won2, "second claim must lose") + + got, err := s.GetScheduledEvent(ctx, evt.ID) + require.NoError(t, err) + assert.Equal(t, store.ScheduledEventFired, got.Status) + assert.NotNil(t, got.FiredAt) +} + +// Claiming a non-existent event is a clean loss, not an error. +func TestClaimScheduledEvent_MissingLoses(t *testing.T) { + s := newTestScheduleStore(t) + won, err := s.ClaimScheduledEvent(context.Background(), uuid.NewString(), store.ScheduledEventFired) + require.NoError(t, err) + assert.False(t, won) +} + +// On SQLite the two-int advisory lock is a no-op that always succeeds. +func TestTryAdvisoryLockObject_SQLiteAlwaysAcquires(t *testing.T) { + c := newTestCompositeStore(t) + ctx := context.Background() + + acquired, release, err := c.TryAdvisoryLockObject(ctx, store.LockWorkspaceProvision, 42) + require.NoError(t, err) + assert.True(t, acquired, "SQLite two-int advisory lock must always acquire") + require.NotNil(t, release) + require.NoError(t, release()) + + // A second concurrent acquisition on the same (classID, objID) also succeeds. + acquired2, release2, err := c.TryAdvisoryLockObject(ctx, store.LockWorkspaceProvision, 42) + require.NoError(t, err) + assert.True(t, acquired2) + require.NoError(t, release2()) +} + +// Two-int locks with different objIDs are independent. +func TestTryAdvisoryLockObject_SQLiteDifferentObjIDsIndependent(t *testing.T) { + c := newTestCompositeStore(t) + ctx := context.Background() + + acq1, rel1, err := c.TryAdvisoryLockObject(ctx, store.LockWorkspaceProvision, 1) + require.NoError(t, err) + assert.True(t, acq1) + + acq2, rel2, err := c.TryAdvisoryLockObject(ctx, store.LockWorkspaceProvision, 2) + require.NoError(t, err) + assert.True(t, acq2) + + require.NoError(t, rel1()) + require.NoError(t, rel2()) +} + +// Under concurrent claims of the same event, exactly one wins. This mirrors two +// replicas recovering the same pending event on startup. +func TestClaimScheduledEvent_ConcurrentSingleWinner(t *testing.T) { + s := newTestScheduleStore(t) + ctx := context.Background() + + evt := newTestScheduledEvent(uuid.NewString()) + require.NoError(t, s.CreateScheduledEvent(ctx, evt)) + + const racers = 8 + var wg sync.WaitGroup + var mu sync.Mutex + wins := 0 + wg.Add(racers) + for i := 0; i < racers; i++ { + go func() { + defer wg.Done() + won, err := s.ClaimScheduledEvent(ctx, evt.ID, store.ScheduledEventFired) + if err == nil && won { + mu.Lock() + wins++ + mu.Unlock() + } + }() + } + wg.Wait() + assert.Equal(t, 1, wins, "exactly one concurrent claim must win") +} diff --git a/pkg/harness/opencode/embeds.go b/pkg/store/entadapter/main_test.go similarity index 57% rename from pkg/harness/opencode/embeds.go rename to pkg/store/entadapter/main_test.go index 454f42859..f35e148df 100644 --- a/pkg/harness/opencode/embeds.go +++ b/pkg/store/entadapter/main_test.go @@ -12,9 +12,23 @@ // See the License for the specific language governing permissions and // limitations under the License. -package opencode +//go:build !no_sqlite -import "embed" +package entadapter -//go:embed all:embeds/* -var EmbedsFS embed.FS +import ( + "os" + "testing" + + "github.com/GoogleCloudPlatform/scion/pkg/store/enttest" +) + +// TestMain wires the enttest backend lifecycle so the Postgres integration +// backend can create and drop its per-package ephemeral database. Both calls are +// no-ops in the default SQLite build. +func TestMain(m *testing.M) { + enttest.MainSetup() + code := m.Run() + enttest.MainTeardown() + os.Exit(code) +} diff --git a/pkg/store/entadapter/maintenance_store.go b/pkg/store/entadapter/maintenance_store.go new file mode 100644 index 000000000..7172e90d3 --- /dev/null +++ b/pkg/store/entadapter/maintenance_store.go @@ -0,0 +1,348 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package entadapter + +import ( + "context" + "time" + + entsql "entgo.io/ent/dialect/sql" + "github.com/GoogleCloudPlatform/scion/pkg/ent" + "github.com/GoogleCloudPlatform/scion/pkg/ent/maintenanceoperation" + "github.com/GoogleCloudPlatform/scion/pkg/ent/maintenanceoperationrun" + "github.com/GoogleCloudPlatform/scion/pkg/store" +) + +// MaintenanceStore implements store.MaintenanceStore using the Ent ORM. +type MaintenanceStore struct { + client *ent.Client +} + +// NewMaintenanceStore creates a new Ent-backed MaintenanceStore. +func NewMaintenanceStore(client *ent.Client) *MaintenanceStore { + return &MaintenanceStore{client: client} +} + +// abortResult is the result message stamped on operations/runs that were +// interrupted by a server restart. Kept identical to the SQLite backend. +const abortResult = `{"error":"aborted: server was restarted while operation was running"}` + +// defaultSeedOperations are the built-in maintenance operations and migrations. +// In the SQLite backend these were seeded by migration SQL that minted ids via +// hex(randomblob(...)); here they are seeded in Go with ids generated by the +// Ent schema default (uuid.New) — see SeedMaintenanceOperations. +var defaultSeedOperations = []store.MaintenanceOperation{ + { + Key: "secret-hub-id-migration", + Title: "Secret Hub ID Namespace Migration", + Description: `Migrates hub-scoped secrets from the legacy fixed "hub" scope ID to the per-instance hub ID. Required when upgrading a hub that was created before the hub ID namespacing feature. Only needed for GCP Secret Manager backend.`, + Category: store.MaintenanceCategoryMigration, + }, + { + Key: "pull-images", + Title: "Pull Container Images", + Description: "Pulls the latest container images for all configured harnesses from the image registry.", + Category: store.MaintenanceCategoryOperation, + }, + { + Key: "rebuild-server", + Title: "Rebuild Server from Git", + Description: "Pulls latest code from the repository, rebuilds the server binary and web assets, then restarts the hub service. Equivalent to the fast-deploy mode of gce-start-hub.sh.", + Category: store.MaintenanceCategoryOperation, + }, + { + Key: "rebuild-web", + Title: "Rebuild Web Frontend", + Description: "Rebuilds only the web frontend assets from source without restarting the server binary. Changes take effect on the next page load.", + Category: store.MaintenanceCategoryOperation, + }, + { + Key: "rebuild-container-binaries", + Title: "Rebuild Container Binaries", + Description: "Rebuilds scion and sciontool binaries for Linux containers (make container-binaries). Only available when SCION_DEV_BINARIES is set. Binaries are written to .build/container/ in the source checkout.", + Category: store.MaintenanceCategoryOperation, + }, +} + +// ============================================================================ +// Conversions +// ============================================================================ + +func entMaintenanceOpToStore(e *ent.MaintenanceOperation) *store.MaintenanceOperation { + return &store.MaintenanceOperation{ + ID: e.ID.String(), + Key: e.Key, + Title: e.Title, + Description: e.Description, + Category: e.Category, + Status: e.Status, + CreatedAt: e.Created, + StartedAt: e.StartedAt, + CompletedAt: e.CompletedAt, + StartedBy: e.StartedBy, + Result: e.Result, + Metadata: e.Metadata, + } +} + +func entMaintenanceRunToStore(e *ent.MaintenanceOperationRun) *store.MaintenanceOperationRun { + return &store.MaintenanceOperationRun{ + ID: e.ID.String(), + OperationKey: e.OperationKey, + Status: e.Status, + StartedAt: e.StartedAt, + CompletedAt: e.CompletedAt, + StartedBy: e.StartedBy, + Result: e.Result, + Log: e.Log, + } +} + +// ============================================================================ +// Seed +// ============================================================================ + +// SeedMaintenanceOperations inserts the built-in operations and migrations that +// are missing from the store. It is idempotent and replaces the SQLite +// randomblob() UUID seeds with Go-side ids generated by the Ent default. +func (s *MaintenanceStore) SeedMaintenanceOperations(ctx context.Context) error { + for _, op := range defaultSeedOperations { + exists, err := s.client.MaintenanceOperation.Query(). + Where(maintenanceoperation.KeyEQ(op.Key)). + Exist(ctx) + if err != nil { + return err + } + if exists { + continue + } + // ID is intentionally left unset so the Ent schema default (uuid.New) + // mints it — the Go replacement for the SQLite randomblob() seed. + err = s.client.MaintenanceOperation.Create(). + SetKey(op.Key). + SetTitle(op.Title). + SetDescription(op.Description). + SetCategory(op.Category). + SetStatus(store.MaintenanceStatusPending). + Exec(ctx) + if err != nil && !ent.IsConstraintError(err) { + return err + } + } + return nil +} + +// ============================================================================ +// Maintenance Operation Operations +// ============================================================================ + +// ListMaintenanceOperations returns all registered operations and migrations. +func (s *MaintenanceStore) ListMaintenanceOperations(ctx context.Context) ([]store.MaintenanceOperation, error) { + entities, err := s.client.MaintenanceOperation.Query(). + Order( + maintenanceoperation.ByCategory(), + maintenanceoperation.ByCreated(), + ). + All(ctx) + if err != nil { + return nil, err + } + ops := make([]store.MaintenanceOperation, 0, len(entities)) + for _, e := range entities { + ops = append(ops, *entMaintenanceOpToStore(e)) + } + return ops, nil +} + +// GetMaintenanceOperation returns a single operation by key. +func (s *MaintenanceStore) GetMaintenanceOperation(ctx context.Context, key string) (*store.MaintenanceOperation, error) { + e, err := s.client.MaintenanceOperation.Query(). + Where(maintenanceoperation.KeyEQ(key)). + Only(ctx) + if err != nil { + return nil, mapError(err) + } + return entMaintenanceOpToStore(e), nil +} + +// UpdateMaintenanceOperation updates an operation's status and result fields. +func (s *MaintenanceStore) UpdateMaintenanceOperation(ctx context.Context, op *store.MaintenanceOperation) error { + update := s.client.MaintenanceOperation.Update(). + Where(maintenanceoperation.KeyEQ(op.Key)). + SetStatus(op.Status). + SetStartedBy(op.StartedBy). + SetResult(op.Result). + SetMetadata(op.Metadata) + + if op.StartedAt != nil { + update.SetStartedAt(*op.StartedAt) + } else { + update.ClearStartedAt() + } + if op.CompletedAt != nil { + update.SetCompletedAt(*op.CompletedAt) + } else { + update.ClearCompletedAt() + } + + n, err := update.Save(ctx) + if err != nil { + return mapError(err) + } + if n == 0 { + return store.ErrNotFound + } + return nil +} + +// ============================================================================ +// Maintenance Operation Run Operations +// ============================================================================ + +// CreateMaintenanceRun inserts a new run record. +func (s *MaintenanceStore) CreateMaintenanceRun(ctx context.Context, run *store.MaintenanceOperationRun) error { + create := s.client.MaintenanceOperationRun.Create(). + SetOperationKey(run.OperationKey). + SetStatus(run.Status). + SetLog(run.Log) + + if run.ID != "" { + uid, err := parseUUID(run.ID) + if err != nil { + return err + } + create.SetID(uid) + } + if !run.StartedAt.IsZero() { + create.SetStartedAt(run.StartedAt) + } + if run.CompletedAt != nil { + create.SetCompletedAt(*run.CompletedAt) + } + if run.StartedBy != "" { + create.SetStartedBy(run.StartedBy) + } + if run.Result != "" { + create.SetResult(run.Result) + } + + created, err := create.Save(ctx) + if err != nil { + return mapError(err) + } + run.ID = created.ID.String() + run.StartedAt = created.StartedAt + if run.Status == "" { + run.Status = created.Status + } + return nil +} + +// UpdateMaintenanceRun updates a run's status, result, and log. +func (s *MaintenanceStore) UpdateMaintenanceRun(ctx context.Context, run *store.MaintenanceOperationRun) error { + uid, err := parseUUID(run.ID) + if err != nil { + return err + } + + update := s.client.MaintenanceOperationRun.Update(). + Where(maintenanceoperationrun.IDEQ(uid)). + SetStatus(run.Status). + SetResult(run.Result). + SetLog(run.Log) + + if run.CompletedAt != nil { + update.SetCompletedAt(*run.CompletedAt) + } else { + update.ClearCompletedAt() + } + + n, err := update.Save(ctx) + if err != nil { + return mapError(err) + } + if n == 0 { + return store.ErrNotFound + } + return nil +} + +// GetMaintenanceRun returns a single run by ID. +func (s *MaintenanceStore) GetMaintenanceRun(ctx context.Context, id string) (*store.MaintenanceOperationRun, error) { + uid, err := parseGetID(id) + if err != nil { + return nil, err + } + e, err := s.client.MaintenanceOperationRun.Get(ctx, uid) + if err != nil { + return nil, mapError(err) + } + return entMaintenanceRunToStore(e), nil +} + +// ListMaintenanceRuns returns runs for a given operation key, ordered by +// started_at DESC. +func (s *MaintenanceStore) ListMaintenanceRuns(ctx context.Context, operationKey string, limit int) ([]store.MaintenanceOperationRun, error) { + if limit <= 0 { + limit = 20 + } + entities, err := s.client.MaintenanceOperationRun.Query(). + Where(maintenanceoperationrun.OperationKeyEQ(operationKey)). + Order(maintenanceoperationrun.ByStartedAt(entsql.OrderDesc())). + Limit(limit). + All(ctx) + if err != nil { + return nil, err + } + runs := make([]store.MaintenanceOperationRun, 0, len(entities)) + for _, e := range entities { + runs = append(runs, *entMaintenanceRunToStore(e)) + } + return runs, nil +} + +// AbortRunningMaintenanceOps transitions any "running" operation runs to +// "failed" and resets stalled "running" migrations back to "pending" so they +// can be retried. Called at server startup to clean up operations interrupted +// by a restart. +func (s *MaintenanceStore) AbortRunningMaintenanceOps(ctx context.Context) (int64, int64, error) { + now := time.Now() + + runs, err := s.client.MaintenanceOperationRun.Update(). + Where(maintenanceoperationrun.StatusEQ(store.MaintenanceStatusRunning)). + SetStatus(store.MaintenanceStatusFailed). + SetCompletedAt(now). + SetResult(abortResult). + Save(ctx) + if err != nil { + return 0, 0, err + } + + migrations, err := s.client.MaintenanceOperation.Update(). + Where( + maintenanceoperation.StatusEQ(store.MaintenanceStatusRunning), + maintenanceoperation.CategoryEQ(store.MaintenanceCategoryMigration), + ). + SetStatus(store.MaintenanceStatusPending). + ClearStartedAt(). + ClearCompletedAt(). + SetResult(abortResult). + Save(ctx) + if err != nil { + return int64(runs), 0, err + } + + return int64(runs), int64(migrations), nil +} diff --git a/pkg/store/entadapter/maintenance_store_test.go b/pkg/store/entadapter/maintenance_store_test.go new file mode 100644 index 000000000..78d2571d4 --- /dev/null +++ b/pkg/store/entadapter/maintenance_store_test.go @@ -0,0 +1,183 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//go:build !no_sqlite + +package entadapter + +import ( + "context" + "testing" + "time" + + "github.com/GoogleCloudPlatform/scion/pkg/store" + "github.com/GoogleCloudPlatform/scion/pkg/store/enttest" + "github.com/google/uuid" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func newTestMaintenanceStore(t *testing.T) *MaintenanceStore { + t.Helper() + client := enttest.NewClient(t) + return NewMaintenanceStore(client) +} + +func TestSeedMaintenanceOperations(t *testing.T) { + s := newTestMaintenanceStore(t) + ctx := context.Background() + + require.NoError(t, s.SeedMaintenanceOperations(ctx)) + + ops, err := s.ListMaintenanceOperations(ctx) + require.NoError(t, err) + assert.Len(t, ops, len(defaultSeedOperations)) + + // Every seeded op must have a valid Go-generated UUID id (not a randomblob). + for _, op := range ops { + _, err := uuid.Parse(op.ID) + assert.NoError(t, err, "seeded op %q should have a valid UUID id", op.Key) + assert.Equal(t, store.MaintenanceStatusPending, op.Status) + } + + // Seeding is idempotent. + require.NoError(t, s.SeedMaintenanceOperations(ctx)) + ops, err = s.ListMaintenanceOperations(ctx) + require.NoError(t, err) + assert.Len(t, ops, len(defaultSeedOperations)) +} + +func TestGetMaintenanceOperationNotFound(t *testing.T) { + s := newTestMaintenanceStore(t) + _, err := s.GetMaintenanceOperation(context.Background(), "does-not-exist") + assert.ErrorIs(t, err, store.ErrNotFound) +} + +func TestUpdateMaintenanceOperation(t *testing.T) { + s := newTestMaintenanceStore(t) + ctx := context.Background() + require.NoError(t, s.SeedMaintenanceOperations(ctx)) + + startedAt := time.Now().UTC().Truncate(time.Second) + op := &store.MaintenanceOperation{ + Key: "pull-images", + Status: store.MaintenanceStatusRunning, + StartedAt: &startedAt, + StartedBy: "admin", + Metadata: `{"foo":"bar"}`, + } + require.NoError(t, s.UpdateMaintenanceOperation(ctx, op)) + + got, err := s.GetMaintenanceOperation(ctx, "pull-images") + require.NoError(t, err) + assert.Equal(t, store.MaintenanceStatusRunning, got.Status) + assert.Equal(t, "admin", got.StartedBy) + assert.Equal(t, `{"foo":"bar"}`, got.Metadata) + require.NotNil(t, got.StartedAt) +} + +func TestUpdateMaintenanceOperationNotFound(t *testing.T) { + s := newTestMaintenanceStore(t) + err := s.UpdateMaintenanceOperation(context.Background(), &store.MaintenanceOperation{ + Key: "ghost", + Status: store.MaintenanceStatusRunning, + }) + assert.ErrorIs(t, err, store.ErrNotFound) +} + +func TestMaintenanceRunRMW(t *testing.T) { + s := newTestMaintenanceStore(t) + ctx := context.Background() + require.NoError(t, s.SeedMaintenanceOperations(ctx)) + + run := &store.MaintenanceOperationRun{ + ID: uuid.NewString(), + OperationKey: "pull-images", + Status: store.MaintenanceStatusRunning, + StartedAt: time.Now().UTC().Truncate(time.Second), + StartedBy: "admin", + Log: "starting", + } + require.NoError(t, s.CreateMaintenanceRun(ctx, run)) + + got, err := s.GetMaintenanceRun(ctx, run.ID) + require.NoError(t, err) + assert.Equal(t, store.MaintenanceStatusRunning, got.Status) + assert.Equal(t, "pull-images", got.OperationKey) + + // Read-modify-write to completed. + completedAt := time.Now().UTC().Truncate(time.Second) + run.Status = store.MaintenanceStatusCompleted + run.CompletedAt = &completedAt + run.Result = `{"ok":true}` + run.Log = "starting\ndone" + require.NoError(t, s.UpdateMaintenanceRun(ctx, run)) + + got, err = s.GetMaintenanceRun(ctx, run.ID) + require.NoError(t, err) + assert.Equal(t, store.MaintenanceStatusCompleted, got.Status) + require.NotNil(t, got.CompletedAt) + assert.Equal(t, `{"ok":true}`, got.Result) + assert.Equal(t, "starting\ndone", got.Log) + + // List runs for the operation. + runs, err := s.ListMaintenanceRuns(ctx, "pull-images", 10) + require.NoError(t, err) + require.Len(t, runs, 1) + assert.Equal(t, run.ID, runs[0].ID) +} + +func TestGetMaintenanceRunNotFound(t *testing.T) { + s := newTestMaintenanceStore(t) + _, err := s.GetMaintenanceRun(context.Background(), uuid.NewString()) + assert.ErrorIs(t, err, store.ErrNotFound) +} + +func TestAbortRunningMaintenanceOps(t *testing.T) { + s := newTestMaintenanceStore(t) + ctx := context.Background() + require.NoError(t, s.SeedMaintenanceOperations(ctx)) + + // A running migration should be reset to pending. + startedAt := time.Now().UTC().Truncate(time.Second) + require.NoError(t, s.UpdateMaintenanceOperation(ctx, &store.MaintenanceOperation{ + Key: "secret-hub-id-migration", + Status: store.MaintenanceStatusRunning, + StartedAt: &startedAt, + })) + + // A running run should be marked failed. + run := &store.MaintenanceOperationRun{ + ID: uuid.NewString(), + OperationKey: "pull-images", + Status: store.MaintenanceStatusRunning, + StartedAt: startedAt, + } + require.NoError(t, s.CreateMaintenanceRun(ctx, run)) + + runs, migrations, err := s.AbortRunningMaintenanceOps(ctx) + require.NoError(t, err) + assert.Equal(t, int64(1), runs) + assert.Equal(t, int64(1), migrations) + + gotRun, err := s.GetMaintenanceRun(ctx, run.ID) + require.NoError(t, err) + assert.Equal(t, store.MaintenanceStatusFailed, gotRun.Status) + require.NotNil(t, gotRun.CompletedAt) + + gotMig, err := s.GetMaintenanceOperation(ctx, "secret-hub-id-migration") + require.NoError(t, err) + assert.Equal(t, store.MaintenanceStatusPending, gotMig.Status) + assert.Nil(t, gotMig.StartedAt) +} diff --git a/pkg/store/entadapter/message_store.go b/pkg/store/entadapter/message_store.go new file mode 100644 index 000000000..bd580a1e3 --- /dev/null +++ b/pkg/store/entadapter/message_store.go @@ -0,0 +1,260 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package entadapter + +import ( + "context" + "time" + + entsql "entgo.io/ent/dialect/sql" + "github.com/GoogleCloudPlatform/scion/pkg/ent" + "github.com/GoogleCloudPlatform/scion/pkg/ent/message" + "github.com/GoogleCloudPlatform/scion/pkg/store" +) + +// MessagePublisher is the hook through which newly created messages are +// announced to other hub replicas. On Postgres this is implemented with +// LISTEN/NOTIFY (a pg_notify on a "user_message" channel) so that subscribers +// receive new messages without polling; the SQLite backend leaves it nil. +// +// It is intentionally an interface rather than a hard dependency so the message +// store stays decoupled from the notification transport, and so the publish +// call can be a no-op until the Postgres LISTEN/NOTIFY listener (Wave B) is +// wired in. +type MessagePublisher interface { + // PublishUserMessage announces that msg was persisted. Implementations must + // be best-effort: a publish failure must not fail the originating write. + PublishUserMessage(ctx context.Context, msg *store.Message) error +} + +// MessageStore implements store.MessageStore using the Ent ORM. +type MessageStore struct { + client *ent.Client + // publisher, when non-nil, is notified after each successful CreateMessage. + // See MessagePublisher. + publisher MessagePublisher +} + +// NewMessageStore creates a new Ent-backed MessageStore. +func NewMessageStore(client *ent.Client) *MessageStore { + return &MessageStore{client: client} +} + +// WithPublisher returns a copy of the store that announces newly created +// messages via the given publisher. Used to wire in the Postgres LISTEN/NOTIFY +// transport without changing the store's construction site. +func (s *MessageStore) WithPublisher(p MessagePublisher) *MessageStore { + clone := *s + clone.publisher = p + return &clone +} + +func entMessageToStore(e *ent.Message) *store.Message { + return &store.Message{ + ID: e.ID.String(), + ProjectID: e.ProjectID.String(), + Sender: e.Sender, + SenderID: e.SenderID, + Recipient: e.Recipient, + RecipientID: e.RecipientID, + Msg: e.Msg, + Type: e.Type, + Urgent: e.Urgent, + Broadcasted: e.Broadcasted, + Read: e.Read, + AgentID: e.AgentID, + GroupID: e.GroupID, + CreatedAt: e.Created, + DispatchState: e.DispatchState, + DispatchedAt: e.DispatchedAt, + } +} + +// CreateMessage persists a new message and announces it via the publisher. +func (s *MessageStore) CreateMessage(ctx context.Context, msg *store.Message) error { + if msg.ID == "" || msg.ProjectID == "" || msg.Msg == "" { + return store.ErrInvalidInput + } + uid, err := parseUUID(msg.ID) + if err != nil { + return err + } + pid, err := parseUUID(msg.ProjectID) + if err != nil { + return err + } + + create := s.client.Message.Create(). + SetID(uid). + SetProjectID(pid). + SetSender(msg.Sender). + SetSenderID(msg.SenderID). + SetRecipient(msg.Recipient). + SetRecipientID(msg.RecipientID). + SetMsg(msg.Msg). + SetType(msg.Type). + SetUrgent(msg.Urgent). + SetBroadcasted(msg.Broadcasted). + SetRead(msg.Read). + SetAgentID(msg.AgentID). + SetGroupID(msg.GroupID) + + if msg.Type == "" { + create.SetType("instruction") + } + if msg.DispatchState != "" { + create.SetDispatchState(msg.DispatchState) + } + if msg.DispatchedAt != nil { + create.SetDispatchedAt(*msg.DispatchedAt) + } + if !msg.CreatedAt.IsZero() { + create.SetCreated(msg.CreatedAt) + } + + created, err := create.Save(ctx) + if err != nil { + return mapError(err) + } + msg.CreatedAt = created.Created + msg.Type = created.Type + msg.DispatchState = created.DispatchState + + // Design-in: announce the new message for LISTEN/NOTIFY subscribers. + // Best-effort — a publish failure must not fail the write that succeeded. + if s.publisher != nil { + _ = s.publisher.PublishUserMessage(ctx, msg) + } + return nil +} + +// GetMessage returns a single message by ID. +func (s *MessageStore) GetMessage(ctx context.Context, id string) (*store.Message, error) { + uid, err := parseGetID(id) + if err != nil { + return nil, err + } + e, err := s.client.Message.Get(ctx, uid) + if err != nil { + return nil, mapError(err) + } + return entMessageToStore(e), nil +} + +// ListMessages returns messages matching the given filter, ordered by +// created_at DESC. +func (s *MessageStore) ListMessages(ctx context.Context, filter store.MessageFilter, opts store.ListOptions) (*store.ListResult[store.Message], error) { + query := s.client.Message.Query() + + if filter.ProjectID != "" { + pid, err := parseUUID(filter.ProjectID) + if err != nil { + return nil, err + } + query.Where(message.ProjectIDEQ(pid)) + } + if filter.AgentID != "" { + query.Where(message.AgentIDEQ(filter.AgentID)) + } + if filter.RecipientID != "" { + query.Where(message.RecipientIDEQ(filter.RecipientID)) + } + if filter.SenderID != "" { + query.Where(message.SenderIDEQ(filter.SenderID)) + } + if filter.ParticipantID != "" { + query.Where(message.Or( + message.RecipientIDEQ(filter.ParticipantID), + message.SenderIDEQ(filter.ParticipantID), + )) + } + if filter.OnlyUnread { + query.Where(message.ReadEQ(false)) + } + if filter.Type != "" { + query.Where(message.TypeEQ(filter.Type)) + } + + totalCount, err := query.Clone().Count(ctx) + if err != nil { + return nil, err + } + + limit := clampLimit(opts.Limit) + entities, err := query. + Order(message.ByCreated(entsql.OrderDesc())). + Limit(limit + 1). + All(ctx) + if err != nil { + return nil, err + } + + msgs := make([]store.Message, 0, len(entities)) + for _, e := range entities { + msgs = append(msgs, *entMessageToStore(e)) + } + + result := &store.ListResult[store.Message]{TotalCount: totalCount} + if len(msgs) > limit { + result.Items = msgs[:limit] + result.NextCursor = msgs[limit-1].ID + } else { + result.Items = msgs + } + return result, nil +} + +// MarkMessageRead marks a message as read. +func (s *MessageStore) MarkMessageRead(ctx context.Context, id string) error { + uid, err := parseUUID(id) + if err != nil { + return err + } + n, err := s.client.Message.Update(). + Where(message.IDEQ(uid)). + SetRead(true). + Save(ctx) + if err != nil { + return err + } + if n == 0 { + return store.ErrNotFound + } + return nil +} + +// MarkAllMessagesRead marks all messages for a recipient as read. +func (s *MessageStore) MarkAllMessagesRead(ctx context.Context, recipientID string) error { + _, err := s.client.Message.Update(). + Where(message.RecipientIDEQ(recipientID)). + SetRead(true). + Save(ctx) + return err +} + +// PurgeOldMessages removes read messages older than readCutoff and unread +// messages older than unreadCutoff. Returns the number of messages removed. +func (s *MessageStore) PurgeOldMessages(ctx context.Context, readCutoff time.Time, unreadCutoff time.Time) (int, error) { + n, err := s.client.Message.Delete(). + Where(message.Or( + message.And(message.ReadEQ(true), message.CreatedLT(readCutoff)), + message.And(message.ReadEQ(false), message.CreatedLT(unreadCutoff)), + )). + Exec(ctx) + if err != nil { + return 0, err + } + return n, nil +} diff --git a/pkg/store/entadapter/message_store_test.go b/pkg/store/entadapter/message_store_test.go new file mode 100644 index 000000000..05fb8d333 --- /dev/null +++ b/pkg/store/entadapter/message_store_test.go @@ -0,0 +1,202 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//go:build !no_sqlite + +package entadapter + +import ( + "context" + "testing" + "time" + + "github.com/GoogleCloudPlatform/scion/pkg/store" + "github.com/GoogleCloudPlatform/scion/pkg/store/enttest" + "github.com/google/uuid" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func newTestMessageStore(t *testing.T) *MessageStore { + t.Helper() + client := enttest.NewClient(t) + return NewMessageStore(client) +} + +func newTestMessage(projectID, recipientID string) *store.Message { + return &store.Message{ + ID: uuid.NewString(), + ProjectID: projectID, + Sender: "user:alice", + SenderID: "sender-1", + Recipient: "agent:coder", + RecipientID: recipientID, + Msg: "Please fix the auth module.", + Type: "instruction", + AgentID: recipientID, + } +} + +func TestMessageCRUD(t *testing.T) { + s := newTestMessageStore(t) + ctx := context.Background() + projectID := uuid.NewString() + + msg := newTestMessage(projectID, "agent-1") + require.NoError(t, s.CreateMessage(ctx, msg)) + assert.False(t, msg.CreatedAt.IsZero()) + + got, err := s.GetMessage(ctx, msg.ID) + require.NoError(t, err) + assert.Equal(t, msg.ID, got.ID) + assert.Equal(t, projectID, got.ProjectID) + assert.Equal(t, "user:alice", got.Sender) + assert.Equal(t, "Please fix the auth module.", got.Msg) + assert.Equal(t, "instruction", got.Type) + assert.False(t, got.Read) +} + +func TestMessageGetNotFound(t *testing.T) { + s := newTestMessageStore(t) + _, err := s.GetMessage(context.Background(), uuid.NewString()) + assert.ErrorIs(t, err, store.ErrNotFound) +} + +func TestMessageInvalidInput(t *testing.T) { + s := newTestMessageStore(t) + err := s.CreateMessage(context.Background(), &store.Message{ID: uuid.NewString()}) + assert.ErrorIs(t, err, store.ErrInvalidInput) +} + +func TestMarkMessageRead(t *testing.T) { + s := newTestMessageStore(t) + ctx := context.Background() + msg := newTestMessage(uuid.NewString(), "agent-1") + require.NoError(t, s.CreateMessage(ctx, msg)) + + require.NoError(t, s.MarkMessageRead(ctx, msg.ID)) + got, err := s.GetMessage(ctx, msg.ID) + require.NoError(t, err) + assert.True(t, got.Read) + + assert.ErrorIs(t, s.MarkMessageRead(ctx, uuid.NewString()), store.ErrNotFound) +} + +func TestMarkAllMessagesRead(t *testing.T) { + s := newTestMessageStore(t) + ctx := context.Background() + projectID := uuid.NewString() + recipient := "agent-1" + + for i := 0; i < 3; i++ { + require.NoError(t, s.CreateMessage(ctx, newTestMessage(projectID, recipient))) + } + // A message for a different recipient must stay unread. + other := newTestMessage(projectID, "agent-2") + require.NoError(t, s.CreateMessage(ctx, other)) + + require.NoError(t, s.MarkAllMessagesRead(ctx, recipient)) + + res, err := s.ListMessages(ctx, store.MessageFilter{RecipientID: recipient, OnlyUnread: true}, store.ListOptions{}) + require.NoError(t, err) + assert.Equal(t, 0, res.TotalCount) + + got, err := s.GetMessage(ctx, other.ID) + require.NoError(t, err) + assert.False(t, got.Read) +} + +func TestListMessagesFilters(t *testing.T) { + s := newTestMessageStore(t) + ctx := context.Background() + projectID := uuid.NewString() + + m1 := newTestMessage(projectID, "agent-1") + m1.SenderID = "user-x" + require.NoError(t, s.CreateMessage(ctx, m1)) + + m2 := newTestMessage(projectID, "agent-2") + m2.SenderID = "user-x" + require.NoError(t, s.CreateMessage(ctx, m2)) + + // Filter by recipient. + res, err := s.ListMessages(ctx, store.MessageFilter{RecipientID: "agent-1"}, store.ListOptions{}) + require.NoError(t, err) + assert.Equal(t, 1, res.TotalCount) + + // ParticipantID matches sender or recipient. + res, err = s.ListMessages(ctx, store.MessageFilter{ParticipantID: "user-x"}, store.ListOptions{}) + require.NoError(t, err) + assert.Equal(t, 2, res.TotalCount) + + res, err = s.ListMessages(ctx, store.MessageFilter{ParticipantID: "agent-2"}, store.ListOptions{}) + require.NoError(t, err) + assert.Equal(t, 1, res.TotalCount) +} + +func TestPurgeOldMessages(t *testing.T) { + s := newTestMessageStore(t) + ctx := context.Background() + projectID := uuid.NewString() + + oldRead := newTestMessage(projectID, "agent-1") + oldRead.Read = true + oldRead.CreatedAt = time.Now().Add(-72 * time.Hour).UTC().Truncate(time.Second) + require.NoError(t, s.CreateMessage(ctx, oldRead)) + + oldUnread := newTestMessage(projectID, "agent-1") + oldUnread.CreatedAt = time.Now().Add(-72 * time.Hour).UTC().Truncate(time.Second) + require.NoError(t, s.CreateMessage(ctx, oldUnread)) + + recent := newTestMessage(projectID, "agent-1") + require.NoError(t, s.CreateMessage(ctx, recent)) + + // readCutoff 24h ago purges oldRead; unreadCutoff 96h ago keeps oldUnread. + readCutoff := time.Now().Add(-24 * time.Hour) + unreadCutoff := time.Now().Add(-96 * time.Hour) + n, err := s.PurgeOldMessages(ctx, readCutoff, unreadCutoff) + require.NoError(t, err) + assert.Equal(t, 1, n) + + _, err = s.GetMessage(ctx, oldRead.ID) + assert.ErrorIs(t, err, store.ErrNotFound) + _, err = s.GetMessage(ctx, oldUnread.ID) + require.NoError(t, err) + _, err = s.GetMessage(ctx, recent.ID) + require.NoError(t, err) +} + +// fakePublisher records PublishUserMessage calls to verify the LISTEN/NOTIFY +// design-in hook fires on create. +type fakePublisher struct { + published []*store.Message +} + +func (f *fakePublisher) PublishUserMessage(_ context.Context, msg *store.Message) error { + f.published = append(f.published, msg) + return nil +} + +func TestCreateMessagePublishesEvent(t *testing.T) { + base := newTestMessageStore(t) + pub := &fakePublisher{} + s := base.WithPublisher(pub) + ctx := context.Background() + + msg := newTestMessage(uuid.NewString(), "agent-1") + require.NoError(t, s.CreateMessage(ctx, msg)) + + require.Len(t, pub.published, 1) + assert.Equal(t, msg.ID, pub.published[0].ID) +} diff --git a/pkg/store/entadapter/notification_store.go b/pkg/store/entadapter/notification_store.go new file mode 100644 index 000000000..864c5c876 --- /dev/null +++ b/pkg/store/entadapter/notification_store.go @@ -0,0 +1,671 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package entadapter + +import ( + "context" + "encoding/json" + "fmt" + "time" + + entsql "entgo.io/ent/dialect/sql" + "github.com/GoogleCloudPlatform/scion/pkg/ent" + "github.com/GoogleCloudPlatform/scion/pkg/ent/notification" + "github.com/GoogleCloudPlatform/scion/pkg/ent/notificationsubscription" + "github.com/GoogleCloudPlatform/scion/pkg/ent/subscriptiontemplate" + "github.com/GoogleCloudPlatform/scion/pkg/store" +) + +// entDesc returns the Ent ordering option for descending order. +func entDesc() entsql.OrderTermOption { return entsql.OrderDesc() } + +// NotificationEventType enumerates the kinds of notification changes that are +// published to a real-time channel (e.g. Postgres LISTEN/NOTIFY). +type NotificationEventType string + +const ( + // NotificationEventCreated is published when a notification record is created. + NotificationEventCreated NotificationEventType = "created" + // NotificationEventDispatched is published when a notification is claimed for + // dispatch by exactly one replica (see MarkNotificationDispatched). + NotificationEventDispatched NotificationEventType = "dispatched" +) + +// NotificationEvent describes a change worth broadcasting to other hub replicas +// so they can react in real time (deliver to a connected agent, wake a poller, +// etc.) instead of busy-polling the database. +type NotificationEvent struct { + Type NotificationEventType `json:"type"` + NotificationID string `json:"notificationId"` + SubscriberType string `json:"subscriberType"` + SubscriberID string `json:"subscriberId"` + ProjectID string `json:"projectId"` +} + +// NotificationPublisher publishes notification events to a real-time channel. +// +// In a multi-replica Postgres deployment this is implemented on top of +// LISTEN/NOTIFY so a notification created or dispatched on one replica is seen +// immediately by the others. The SQLite / single-process path can leave the +// publisher nil, in which case publishing is a no-op. Publishing is best-effort: +// a publish failure never fails the underlying write. +type NotificationPublisher interface { + PublishNotification(ctx context.Context, evt NotificationEvent) error +} + +// NotificationStore implements store.NotificationStore using Ent ORM. +type NotificationStore struct { + client *ent.Client + publisher NotificationPublisher +} + +// NewNotificationStore creates a new Ent-backed NotificationStore. No publisher +// is attached; use WithPublisher to wire LISTEN/NOTIFY for multi-replica +// deployments. +func NewNotificationStore(client *ent.Client) *NotificationStore { + return &NotificationStore{client: client} +} + +// WithPublisher returns a copy of the store that publishes notification events +// through p. Passing nil disables publishing. +func (s *NotificationStore) WithPublisher(p NotificationPublisher) *NotificationStore { + clone := *s + clone.publisher = p + return &clone +} + +// publish emits an event best-effort. Errors are intentionally swallowed so the +// real-time fan-out never breaks the durable write path. +func (s *NotificationStore) publish(ctx context.Context, evt NotificationEvent) { + if s.publisher == nil { + return + } + _ = s.publisher.PublishNotification(ctx, evt) +} + +// ---------------------------------------------------------------------------- +// Conversions +// ---------------------------------------------------------------------------- + +// entSubToStore converts an Ent NotificationSubscription to the store model. +func entSubToStore(e *ent.NotificationSubscription) *store.NotificationSubscription { + sub := &store.NotificationSubscription{ + ID: e.ID.String(), + Scope: e.Scope, + SubscriberType: e.SubscriberType, + SubscriberID: e.SubscriberID, + ProjectID: e.ProjectID.String(), + CreatedAt: e.Created, + CreatedBy: e.CreatedBy, + } + if e.AgentID != nil { + sub.AgentID = e.AgentID.String() + } + if e.TriggerActivities != "" { + _ = json.Unmarshal([]byte(e.TriggerActivities), &sub.TriggerActivities) + } + return sub +} + +// entNotifToStore converts an Ent Notification to the store model. +func entNotifToStore(e *ent.Notification) *store.Notification { + return &store.Notification{ + ID: e.ID.String(), + SubscriptionID: e.SubscriptionID.String(), + AgentID: e.AgentID.String(), + ProjectID: e.ProjectID.String(), + SubscriberType: e.SubscriberType, + SubscriberID: e.SubscriberID, + Status: e.Status, + Message: e.Message, + Dispatched: e.Dispatched, + Acknowledged: e.Acknowledged, + CreatedAt: e.Created, + } +} + +// entTemplateToStore converts an Ent SubscriptionTemplate to the store model. +func entTemplateToStore(e *ent.SubscriptionTemplate) *store.SubscriptionTemplate { + tmpl := &store.SubscriptionTemplate{ + ID: e.ID.String(), + Name: e.Name, + Scope: e.Scope, + CreatedBy: e.CreatedBy, + } + if e.ProjectID != nil { + tmpl.ProjectID = e.ProjectID.String() + } + if e.TriggerActivities != "" { + _ = json.Unmarshal([]byte(e.TriggerActivities), &tmpl.TriggerActivities) + } + return tmpl +} + +// marshalTriggers serializes trigger activities to the JSON string stored in the +// dialect-neutral trigger_activities column. +func marshalTriggers(triggers []string) string { + if triggers == nil { + triggers = []string{} + } + b, _ := json.Marshal(triggers) + return string(b) +} + +// ---------------------------------------------------------------------------- +// Notification Subscription Operations +// ---------------------------------------------------------------------------- + +// CreateNotificationSubscription creates a new notification subscription. +func (s *NotificationStore) CreateNotificationSubscription(ctx context.Context, sub *store.NotificationSubscription) error { + if sub.ID == "" || sub.SubscriberID == "" || sub.ProjectID == "" { + return store.ErrInvalidInput + } + + // Default scope to agent for backward compatibility. + if sub.Scope == "" { + sub.Scope = store.SubscriptionScopeAgent + } + + // Validate scope-specific constraints. + switch sub.Scope { + case store.SubscriptionScopeAgent: + if sub.AgentID == "" { + return store.ErrInvalidInput + } + case store.SubscriptionScopeProject: + sub.AgentID = "" // Ensure no agent_id for project-scoped subscriptions. + default: + return fmt.Errorf("invalid scope %q: %w", sub.Scope, store.ErrInvalidInput) + } + + subscriberType := sub.SubscriberType + if subscriberType == "" { + subscriberType = "agent" + } + + id, err := parseUUID(sub.ID) + if err != nil { + return err + } + projectUID, err := parseUUID(sub.ProjectID) + if err != nil { + return err + } + + if sub.CreatedAt.IsZero() { + sub.CreatedAt = time.Now() + } + + create := s.client.NotificationSubscription.Create(). + SetID(id). + SetScope(sub.Scope). + SetSubscriberType(subscriberType). + SetSubscriberID(sub.SubscriberID). + SetProjectID(projectUID). + SetTriggerActivities(marshalTriggers(sub.TriggerActivities)). + SetCreatedBy(sub.CreatedBy). + SetCreated(sub.CreatedAt) + + if sub.AgentID != "" { + agentUID, err := parseUUID(sub.AgentID) + if err != nil { + return err + } + create.SetAgentID(agentUID) + } + + if _, err := create.Save(ctx); err != nil { + return mapError(err) + } + return nil +} + +// GetNotificationSubscription returns a single subscription by ID. +func (s *NotificationStore) GetNotificationSubscription(ctx context.Context, id string) (*store.NotificationSubscription, error) { + uid, err := parseGetID(id) + if err != nil { + return nil, err + } + e, err := s.client.NotificationSubscription.Get(ctx, uid) + if err != nil { + return nil, mapError(err) + } + return entSubToStore(e), nil +} + +// GetNotificationSubscriptions returns all agent-scoped subscriptions for a watched agent. +func (s *NotificationStore) GetNotificationSubscriptions(ctx context.Context, agentID string) ([]store.NotificationSubscription, error) { + uid, err := parseUUID(agentID) + if err != nil { + return nil, err + } + rows, err := s.client.NotificationSubscription.Query(). + Where(notificationsubscription.AgentIDEQ(uid)). + Order(notificationsubscription.ByCreated()). + All(ctx) + if err != nil { + return nil, err + } + return subsToStore(rows), nil +} + +// GetNotificationSubscriptionsByProject returns all subscriptions within a project (any scope). +func (s *NotificationStore) GetNotificationSubscriptionsByProject(ctx context.Context, projectID string) ([]store.NotificationSubscription, error) { + uid, err := parseUUID(projectID) + if err != nil { + return nil, err + } + rows, err := s.client.NotificationSubscription.Query(). + Where(notificationsubscription.ProjectIDEQ(uid)). + Order(notificationsubscription.ByCreated()). + All(ctx) + if err != nil { + return nil, err + } + return subsToStore(rows), nil +} + +// GetNotificationSubscriptionsByProjectScope returns project-scoped subscriptions +// (scope='project') for a given project. +func (s *NotificationStore) GetNotificationSubscriptionsByProjectScope(ctx context.Context, projectID string) ([]store.NotificationSubscription, error) { + uid, err := parseUUID(projectID) + if err != nil { + return nil, err + } + rows, err := s.client.NotificationSubscription.Query(). + Where( + notificationsubscription.ProjectIDEQ(uid), + notificationsubscription.ScopeEQ(store.SubscriptionScopeProject), + ). + Order(notificationsubscription.ByCreated()). + All(ctx) + if err != nil { + return nil, err + } + return subsToStore(rows), nil +} + +// GetSubscriptionsForSubscriber returns all subscriptions owned by a subscriber. +func (s *NotificationStore) GetSubscriptionsForSubscriber(ctx context.Context, subscriberType, subscriberID string) ([]store.NotificationSubscription, error) { + rows, err := s.client.NotificationSubscription.Query(). + Where( + notificationsubscription.SubscriberTypeEQ(subscriberType), + notificationsubscription.SubscriberIDEQ(subscriberID), + ). + Order(notificationsubscription.ByCreated()). + All(ctx) + if err != nil { + return nil, err + } + return subsToStore(rows), nil +} + +// UpdateNotificationSubscriptionTriggers updates the trigger activities of a subscription. +func (s *NotificationStore) UpdateNotificationSubscriptionTriggers(ctx context.Context, id string, triggerActivities []string) error { + if id == "" || len(triggerActivities) == 0 { + return store.ErrInvalidInput + } + uid, err := parseUUID(id) + if err != nil { + return err + } + _, err = s.client.NotificationSubscription.UpdateOneID(uid). + SetTriggerActivities(marshalTriggers(triggerActivities)). + Save(ctx) + if err != nil { + return mapError(err) + } + return nil +} + +// DeleteNotificationSubscription deletes a subscription by ID. +func (s *NotificationStore) DeleteNotificationSubscription(ctx context.Context, id string) error { + uid, err := parseUUID(id) + if err != nil { + return err + } + if err := s.client.NotificationSubscription.DeleteOneID(uid).Exec(ctx); err != nil { + return mapError(err) + } + return nil +} + +// DeleteNotificationSubscriptionsForAgent deletes all subscriptions for a watched agent. +// No error on zero rows affected. +func (s *NotificationStore) DeleteNotificationSubscriptionsForAgent(ctx context.Context, agentID string) error { + uid, err := parseUUID(agentID) + if err != nil { + return err + } + _, err = s.client.NotificationSubscription.Delete(). + Where(notificationsubscription.AgentIDEQ(uid)). + Exec(ctx) + return err +} + +// subsToStore converts a slice of Ent subscriptions to store models. +func subsToStore(rows []*ent.NotificationSubscription) []store.NotificationSubscription { + out := make([]store.NotificationSubscription, 0, len(rows)) + for _, e := range rows { + out = append(out, *entSubToStore(e)) + } + return out +} + +// ---------------------------------------------------------------------------- +// Notification Operations +// ---------------------------------------------------------------------------- + +// CreateNotification creates a new notification record. +func (s *NotificationStore) CreateNotification(ctx context.Context, notif *store.Notification) error { + if notif.ID == "" || notif.SubscriptionID == "" || notif.AgentID == "" { + return store.ErrInvalidInput + } + + id, err := parseUUID(notif.ID) + if err != nil { + return err + } + subUID, err := parseUUID(notif.SubscriptionID) + if err != nil { + return err + } + agentUID, err := parseUUID(notif.AgentID) + if err != nil { + return err + } + projectUID, err := parseUUID(notif.ProjectID) + if err != nil { + return err + } + + if notif.CreatedAt.IsZero() { + notif.CreatedAt = time.Now() + } + + _, err = s.client.Notification.Create(). + SetID(id). + SetSubscriptionID(subUID). + SetAgentID(agentUID). + SetProjectID(projectUID). + SetSubscriberType(notif.SubscriberType). + SetSubscriberID(notif.SubscriberID). + SetStatus(notif.Status). + SetMessage(notif.Message). + SetDispatched(notif.Dispatched). + SetAcknowledged(notif.Acknowledged). + SetCreated(notif.CreatedAt). + Save(ctx) + if err != nil { + return mapError(err) + } + + // Broadcast so other replicas can pick up delivery in real time. + s.publish(ctx, NotificationEvent{ + Type: NotificationEventCreated, + NotificationID: notif.ID, + SubscriberType: notif.SubscriberType, + SubscriberID: notif.SubscriberID, + ProjectID: notif.ProjectID, + }) + return nil +} + +// GetNotifications returns notifications for a subscriber, newest first. +func (s *NotificationStore) GetNotifications(ctx context.Context, subscriberType, subscriberID string, onlyUnacknowledged bool) ([]store.Notification, error) { + query := s.client.Notification.Query(). + Where( + notification.SubscriberTypeEQ(subscriberType), + notification.SubscriberIDEQ(subscriberID), + ) + if onlyUnacknowledged { + query = query.Where(notification.AcknowledgedEQ(false)) + } + rows, err := query.Order(notification.ByCreated(entDesc())).All(ctx) + if err != nil { + return nil, err + } + return notifsToStore(rows), nil +} + +// GetNotificationsByAgent returns notifications for a subscriber filtered by agent ID, newest first. +func (s *NotificationStore) GetNotificationsByAgent(ctx context.Context, agentID, subscriberType, subscriberID string, onlyUnacknowledged bool) ([]store.Notification, error) { + uid, err := parseUUID(agentID) + if err != nil { + return nil, err + } + query := s.client.Notification.Query(). + Where( + notification.AgentIDEQ(uid), + notification.SubscriberTypeEQ(subscriberType), + notification.SubscriberIDEQ(subscriberID), + ) + if onlyUnacknowledged { + query = query.Where(notification.AcknowledgedEQ(false)) + } + rows, err := query.Order(notification.ByCreated(entDesc())).All(ctx) + if err != nil { + return nil, err + } + return notifsToStore(rows), nil +} + +// AcknowledgeNotification marks a notification as acknowledged. +func (s *NotificationStore) AcknowledgeNotification(ctx context.Context, id string) error { + uid, err := parseUUID(id) + if err != nil { + return err + } + _, err = s.client.Notification.UpdateOneID(uid).SetAcknowledged(true).Save(ctx) + if err != nil { + return mapError(err) + } + return nil +} + +// AcknowledgeAllNotifications marks all notifications for a subscriber as acknowledged. +// No error on zero rows affected. +func (s *NotificationStore) AcknowledgeAllNotifications(ctx context.Context, subscriberType, subscriberID string) error { + _, err := s.client.Notification.Update(). + Where( + notification.SubscriberTypeEQ(subscriberType), + notification.SubscriberIDEQ(subscriberID), + ). + SetAcknowledged(true). + Save(ctx) + return err +} + +// MarkNotificationDispatched atomically claims a notification for dispatch. +// +// The conditional update (dispatched = false guard) is the multi-replica +// concurrency primitive: in a Postgres deployment several hub replicas may race +// to dispatch the same notification, but the UPDATE ... WHERE dispatched = false +// is atomic, so exactly one replica observes affected == 1 and "wins" the claim. +// That winner is the one that publishes the dispatch event / drives the side +// effect; losers (affected == 0 on an existing row) treat it as an idempotent +// no-op. A missing row is still reported as ErrNotFound to preserve the +// interface contract. +func (s *NotificationStore) MarkNotificationDispatched(ctx context.Context, id string) error { + uid, err := parseUUID(id) + if err != nil { + return err + } + + affected, err := s.client.Notification.Update(). + Where( + notification.IDEQ(uid), + notification.DispatchedEQ(false), + ). + SetDispatched(true). + Save(ctx) + if err != nil { + return mapError(err) + } + + if affected == 0 { + // Either the notification doesn't exist or another replica already + // claimed the dispatch. Disambiguate to preserve ErrNotFound semantics. + e, err := s.client.Notification.Query().Where(notification.IDEQ(uid)).Only(ctx) + if err != nil { + return mapError(err) + } + // Already dispatched by some replica — idempotent success. + _ = e + return nil + } + + // We won the claim; only this replica broadcasts the dispatch. + if e, err := s.client.Notification.Get(ctx, uid); err == nil { + s.publish(ctx, NotificationEvent{ + Type: NotificationEventDispatched, + NotificationID: id, + SubscriberType: e.SubscriberType, + SubscriberID: e.SubscriberID, + ProjectID: e.ProjectID.String(), + }) + } + return nil +} + +// GetLastNotificationStatus returns the status of the most recent notification +// for a given subscription. Returns ("", nil) if no notifications exist. +func (s *NotificationStore) GetLastNotificationStatus(ctx context.Context, subscriptionID string) (string, error) { + uid, err := parseUUID(subscriptionID) + if err != nil { + return "", err + } + e, err := s.client.Notification.Query(). + Where(notification.SubscriptionIDEQ(uid)). + Order(notification.ByCreated(entDesc())). + First(ctx) + if err != nil { + if ent.IsNotFound(err) { + return "", nil + } + return "", err + } + return e.Status, nil +} + +// notifsToStore converts a slice of Ent notifications to store models. +func notifsToStore(rows []*ent.Notification) []store.Notification { + out := make([]store.Notification, 0, len(rows)) + for _, e := range rows { + out = append(out, *entNotifToStore(e)) + } + return out +} + +// ---------------------------------------------------------------------------- +// Subscription Template Operations +// ---------------------------------------------------------------------------- + +// CreateSubscriptionTemplate creates a new subscription template. +func (s *NotificationStore) CreateSubscriptionTemplate(ctx context.Context, tmpl *store.SubscriptionTemplate) error { + if tmpl.ID == "" || tmpl.Name == "" || len(tmpl.TriggerActivities) == 0 { + return store.ErrInvalidInput + } + + id, err := parseUUID(tmpl.ID) + if err != nil { + return err + } + + scope := tmpl.Scope + if scope == "" { + scope = "project" + } + + create := s.client.SubscriptionTemplate.Create(). + SetID(id). + SetName(tmpl.Name). + SetScope(scope). + SetTriggerActivities(marshalTriggers(tmpl.TriggerActivities)). + SetCreatedBy(tmpl.CreatedBy) + + // An empty ProjectID denotes a global template (NULL project_id). + if tmpl.ProjectID != "" { + projectUID, err := parseUUID(tmpl.ProjectID) + if err != nil { + return err + } + create.SetProjectID(projectUID) + } + + if _, err := create.Save(ctx); err != nil { + return mapError(err) + } + return nil +} + +// GetSubscriptionTemplate returns a template by ID. +func (s *NotificationStore) GetSubscriptionTemplate(ctx context.Context, id string) (*store.SubscriptionTemplate, error) { + uid, err := parseGetID(id) + if err != nil { + return nil, err + } + e, err := s.client.SubscriptionTemplate.Get(ctx, uid) + if err != nil { + return nil, mapError(err) + } + return entTemplateToStore(e), nil +} + +// ListSubscriptionTemplates returns all templates. If projectID is non-empty, +// returns both global templates and project-specific templates. +func (s *NotificationStore) ListSubscriptionTemplates(ctx context.Context, projectID string) ([]store.SubscriptionTemplate, error) { + query := s.client.SubscriptionTemplate.Query() + + if projectID != "" { + uid, err := parseUUID(projectID) + if err != nil { + return nil, err + } + // Global templates (NULL project_id) plus those owned by this project. + query = query.Where(subscriptiontemplate.Or( + subscriptiontemplate.ProjectIDIsNil(), + subscriptiontemplate.ProjectIDEQ(uid), + )) + } else { + query = query.Where(subscriptiontemplate.ProjectIDIsNil()) + } + + rows, err := query.Order(subscriptiontemplate.ByName()).All(ctx) + if err != nil { + return nil, err + } + + out := make([]store.SubscriptionTemplate, 0, len(rows)) + for _, e := range rows { + out = append(out, *entTemplateToStore(e)) + } + return out, nil +} + +// DeleteSubscriptionTemplate deletes a template by ID. +func (s *NotificationStore) DeleteSubscriptionTemplate(ctx context.Context, id string) error { + uid, err := parseUUID(id) + if err != nil { + return err + } + if err := s.client.SubscriptionTemplate.DeleteOneID(uid).Exec(ctx); err != nil { + return mapError(err) + } + return nil +} + +// Ensure NotificationStore satisfies the store interface. +var _ store.NotificationStore = (*NotificationStore)(nil) diff --git a/pkg/store/entadapter/notification_store_test.go b/pkg/store/entadapter/notification_store_test.go new file mode 100644 index 000000000..ffaeaacea --- /dev/null +++ b/pkg/store/entadapter/notification_store_test.go @@ -0,0 +1,304 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//go:build !no_sqlite + +package entadapter + +import ( + "context" + "sync" + "testing" + + "github.com/GoogleCloudPlatform/scion/pkg/store" + "github.com/GoogleCloudPlatform/scion/pkg/store/enttest" + "github.com/google/uuid" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func newTestNotificationStore(t *testing.T) *NotificationStore { + t.Helper() + client := enttest.NewClient(t) + return NewNotificationStore(client) +} + +func TestNotificationStore_SubscriptionCRUD(t *testing.T) { + ctx := context.Background() + s := newTestNotificationStore(t) + + projectID := uuid.NewString() + agentID := uuid.NewString() + sub := &store.NotificationSubscription{ + ID: uuid.NewString(), + Scope: store.SubscriptionScopeAgent, + AgentID: agentID, + SubscriberType: "user", + SubscriberID: "user-1", + ProjectID: projectID, + TriggerActivities: []string{"COMPLETED", "WAITING_FOR_INPUT"}, + CreatedBy: "tester", + } + require.NoError(t, s.CreateNotificationSubscription(ctx, sub)) + + got, err := s.GetNotificationSubscription(ctx, sub.ID) + require.NoError(t, err) + assert.Equal(t, sub.ID, got.ID) + assert.Equal(t, agentID, got.AgentID) + assert.Equal(t, []string{"COMPLETED", "WAITING_FOR_INPUT"}, got.TriggerActivities) + assert.False(t, got.CreatedAt.IsZero()) + + // Update triggers. + require.NoError(t, s.UpdateNotificationSubscriptionTriggers(ctx, sub.ID, []string{"FAILED"})) + got, err = s.GetNotificationSubscription(ctx, sub.ID) + require.NoError(t, err) + assert.Equal(t, []string{"FAILED"}, got.TriggerActivities) + + // Query helpers. + byAgent, err := s.GetNotificationSubscriptions(ctx, agentID) + require.NoError(t, err) + assert.Len(t, byAgent, 1) + + byProject, err := s.GetNotificationSubscriptionsByProject(ctx, projectID) + require.NoError(t, err) + assert.Len(t, byProject, 1) + + bySubscriber, err := s.GetSubscriptionsForSubscriber(ctx, "user", "user-1") + require.NoError(t, err) + assert.Len(t, bySubscriber, 1) + + // Delete. + require.NoError(t, s.DeleteNotificationSubscription(ctx, sub.ID)) + _, err = s.GetNotificationSubscription(ctx, sub.ID) + assert.ErrorIs(t, err, store.ErrNotFound) +} + +func TestNotificationStore_ProjectScopedSubscription(t *testing.T) { + ctx := context.Background() + s := newTestNotificationStore(t) + + projectID := uuid.NewString() + sub := &store.NotificationSubscription{ + ID: uuid.NewString(), + Scope: store.SubscriptionScopeProject, + SubscriberType: "user", + SubscriberID: "user-1", + ProjectID: projectID, + TriggerActivities: []string{"COMPLETED"}, + CreatedBy: "tester", + } + require.NoError(t, s.CreateNotificationSubscription(ctx, sub)) + assert.Empty(t, sub.AgentID, "project scope must clear agent id") + + scoped, err := s.GetNotificationSubscriptionsByProjectScope(ctx, projectID) + require.NoError(t, err) + require.Len(t, scoped, 1) + assert.Empty(t, scoped[0].AgentID) +} + +func TestNotificationStore_SubscriptionValidation(t *testing.T) { + ctx := context.Background() + s := newTestNotificationStore(t) + + // Missing required fields. + assert.ErrorIs(t, s.CreateNotificationSubscription(ctx, &store.NotificationSubscription{}), store.ErrInvalidInput) + + // Agent scope without agent id. + assert.ErrorIs(t, s.CreateNotificationSubscription(ctx, &store.NotificationSubscription{ + ID: uuid.NewString(), Scope: store.SubscriptionScopeAgent, + SubscriberID: "u", ProjectID: uuid.NewString(), + }), store.ErrInvalidInput) +} + +func TestNotificationStore_NotificationLifecycle(t *testing.T) { + ctx := context.Background() + s := newTestNotificationStore(t) + + projectID := uuid.NewString() + agentID := uuid.NewString() + subID := uuid.NewString() + + notif := &store.Notification{ + ID: uuid.NewString(), + SubscriptionID: subID, + AgentID: agentID, + ProjectID: projectID, + SubscriberType: "user", + SubscriberID: "user-1", + Status: "COMPLETED", + Message: "agent done", + } + require.NoError(t, s.CreateNotification(ctx, notif)) + + list, err := s.GetNotifications(ctx, "user", "user-1", false) + require.NoError(t, err) + require.Len(t, list, 1) + assert.False(t, list[0].Acknowledged) + assert.False(t, list[0].Dispatched) + + byAgent, err := s.GetNotificationsByAgent(ctx, agentID, "user", "user-1", true) + require.NoError(t, err) + require.Len(t, byAgent, 1) + + // Dispatch claim. + require.NoError(t, s.MarkNotificationDispatched(ctx, notif.ID)) + + // Acknowledge. + require.NoError(t, s.AcknowledgeNotification(ctx, notif.ID)) + unack, err := s.GetNotifications(ctx, "user", "user-1", true) + require.NoError(t, err) + assert.Empty(t, unack) + + // Last status by subscription. + status, err := s.GetLastNotificationStatus(ctx, subID) + require.NoError(t, err) + assert.Equal(t, "COMPLETED", status) + + // No notifications for an unknown subscription -> ("", nil). + status, err = s.GetLastNotificationStatus(ctx, uuid.NewString()) + require.NoError(t, err) + assert.Equal(t, "", status) +} + +func TestNotificationStore_AcknowledgeAll(t *testing.T) { + ctx := context.Background() + s := newTestNotificationStore(t) + + for i := 0; i < 3; i++ { + require.NoError(t, s.CreateNotification(ctx, &store.Notification{ + ID: uuid.NewString(), + SubscriptionID: uuid.NewString(), + AgentID: uuid.NewString(), + ProjectID: uuid.NewString(), + SubscriberType: "user", + SubscriberID: "user-1", + Status: "COMPLETED", + Message: "m", + })) + } + require.NoError(t, s.AcknowledgeAllNotifications(ctx, "user", "user-1")) + unack, err := s.GetNotifications(ctx, "user", "user-1", true) + require.NoError(t, err) + assert.Empty(t, unack) +} + +// TestNotificationStore_DispatchClaimIsExclusive verifies the multi-replica +// concurrency primitive: many concurrent MarkNotificationDispatched calls for +// the same notification must result in exactly one publisher "win". +func TestNotificationStore_DispatchClaimIsExclusive(t *testing.T) { + ctx := context.Background() + + client := enttest.NewClient(t) + + pub := &countingPublisher{} + s := NewNotificationStore(client).WithPublisher(pub) + + notifID := uuid.NewString() + require.NoError(t, s.CreateNotification(ctx, &store.Notification{ + ID: notifID, + SubscriptionID: uuid.NewString(), + AgentID: uuid.NewString(), + ProjectID: uuid.NewString(), + SubscriberType: "user", + SubscriberID: "user-1", + Status: "COMPLETED", + Message: "m", + })) + + const racers = 8 + var wg sync.WaitGroup + wg.Add(racers) + for i := 0; i < racers; i++ { + go func() { + defer wg.Done() + _ = s.MarkNotificationDispatched(ctx, notifID) + }() + } + wg.Wait() + + // Exactly one dispatch event should have been published despite the race. + assert.Equal(t, 1, pub.count(NotificationEventDispatched), "dispatch must be claimed exactly once") + + // Marking an unknown notification returns ErrNotFound. + assert.ErrorIs(t, s.MarkNotificationDispatched(ctx, uuid.NewString()), store.ErrNotFound) +} + +func TestNotificationStore_TemplateCRUD(t *testing.T) { + ctx := context.Background() + s := newTestNotificationStore(t) + + projectID := uuid.NewString() + + global := &store.SubscriptionTemplate{ + ID: uuid.NewString(), + Name: "all-events", + Scope: "project", + TriggerActivities: []string{"COMPLETED", "FAILED"}, + CreatedBy: "tester", + } + require.NoError(t, s.CreateSubscriptionTemplate(ctx, global)) + + scoped := &store.SubscriptionTemplate{ + ID: uuid.NewString(), + Name: "critical", + Scope: "project", + TriggerActivities: []string{"FAILED"}, + ProjectID: projectID, + CreatedBy: "tester", + } + require.NoError(t, s.CreateSubscriptionTemplate(ctx, scoped)) + + got, err := s.GetSubscriptionTemplate(ctx, scoped.ID) + require.NoError(t, err) + assert.Equal(t, projectID, got.ProjectID) + assert.Equal(t, []string{"FAILED"}, got.TriggerActivities) + + // Global-only listing. + globalOnly, err := s.ListSubscriptionTemplates(ctx, "") + require.NoError(t, err) + require.Len(t, globalOnly, 1) + assert.Equal(t, "all-events", globalOnly[0].Name) + assert.Empty(t, globalOnly[0].ProjectID) + + // Project listing includes global + project-specific. + withProject, err := s.ListSubscriptionTemplates(ctx, projectID) + require.NoError(t, err) + assert.Len(t, withProject, 2) + + require.NoError(t, s.DeleteSubscriptionTemplate(ctx, scoped.ID)) + _, err = s.GetSubscriptionTemplate(ctx, scoped.ID) + assert.ErrorIs(t, err, store.ErrNotFound) +} + +// countingPublisher is a thread-safe NotificationPublisher test double. +type countingPublisher struct { + mu sync.Mutex + counts map[NotificationEventType]int +} + +func (p *countingPublisher) PublishNotification(_ context.Context, evt NotificationEvent) error { + p.mu.Lock() + defer p.mu.Unlock() + if p.counts == nil { + p.counts = make(map[NotificationEventType]int) + } + p.counts[evt.Type]++ + return nil +} + +func (p *countingPublisher) count(t NotificationEventType) int { + p.mu.Lock() + defer p.mu.Unlock() + return p.counts[t] +} diff --git a/pkg/store/entadapter/policy_store.go b/pkg/store/entadapter/policy_store.go index f8679381b..8efcd3d58 100644 --- a/pkg/store/entadapter/policy_store.go +++ b/pkg/store/entadapter/policy_store.go @@ -153,7 +153,7 @@ func (s *PolicyStore) CreatePolicy(ctx context.Context, p *store.Policy) error { // GetPolicy retrieves a policy by ID. func (s *PolicyStore) GetPolicy(ctx context.Context, id string) (*store.Policy, error) { - uid, err := parseUUID(id) + uid, err := parseGetID(id) if err != nil { return nil, err } diff --git a/pkg/store/entadapter/policy_store_test.go b/pkg/store/entadapter/policy_store_test.go index e9c480155..2f5659d2d 100644 --- a/pkg/store/entadapter/policy_store_test.go +++ b/pkg/store/entadapter/policy_store_test.go @@ -20,8 +20,8 @@ import ( "context" "testing" - "github.com/GoogleCloudPlatform/scion/pkg/ent/entc" "github.com/GoogleCloudPlatform/scion/pkg/store" + "github.com/GoogleCloudPlatform/scion/pkg/store/enttest" "github.com/google/uuid" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -37,15 +37,12 @@ var ( func newTestPolicyStore(t *testing.T) *PolicyStore { t.Helper() - client, err := entc.OpenSQLite("file:" + t.Name() + "?mode=memory&cache=shared") - require.NoError(t, err) - t.Cleanup(func() { client.Close() }) - require.NoError(t, entc.AutoMigrate(context.Background(), client)) + client := enttest.NewClient(t) ctx := context.Background() // Create test user - _, err = client.User.Create(). + _, err := client.User.Create(). SetID(policyTestUserUID). SetEmail("alice@example.com"). SetDisplayName("Alice"). diff --git a/pkg/store/entadapter/project_store.go b/pkg/store/entadapter/project_store.go new file mode 100644 index 000000000..5b2275a1d --- /dev/null +++ b/pkg/store/entadapter/project_store.go @@ -0,0 +1,1231 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package entadapter + +import ( + "context" + "encoding/json" + "fmt" + "strings" + "time" + + "github.com/GoogleCloudPlatform/scion/pkg/api" + "github.com/GoogleCloudPlatform/scion/pkg/ent" + "github.com/GoogleCloudPlatform/scion/pkg/ent/agent" + "github.com/GoogleCloudPlatform/scion/pkg/ent/project" + "github.com/GoogleCloudPlatform/scion/pkg/ent/projectcontributor" + "github.com/GoogleCloudPlatform/scion/pkg/ent/projectsyncstate" + "github.com/GoogleCloudPlatform/scion/pkg/ent/runtimebroker" + "github.com/GoogleCloudPlatform/scion/pkg/store" + "github.com/google/uuid" +) + +// maxCASRetries bounds the optimistic-concurrency retry loop on runtime broker +// read-modify-write paths (heartbeat and full update). A handful of retries is +// ample: contention on a single broker row is low and each retry only re-reads +// the lock_version token. +const maxCASRetries = 5 + +// ProjectStore implements store.ProjectStore, store.RuntimeBrokerStore, +// store.ProjectProviderStore and store.ProjectSyncStateStore using Ent ORM. +// +// These four interfaces form the project/broker domain (tables: projects, +// runtime_brokers, project_contributors, project_sync_state). They are grouped +// in one adapter because they are tightly coupled — projects reference brokers +// through contributors, and several computed fields on a project are derived +// from broker/contributor state. +type ProjectStore struct { + client *ent.Client +} + +// NewProjectStore creates a new Ent-backed ProjectStore. +func NewProjectStore(client *ent.Client) *ProjectStore { + return &ProjectStore{client: client} +} + +// ============================================================================= +// JSON helpers +// +// Several columns are stored as raw JSON strings (matching the dual-write +// behavior of the legacy SQLite store) rather than typed Ent JSON fields, to +// keep the schema dialect-neutral and free of store/api type imports. +// ============================================================================= + +// marshalRawJSON marshals v to a JSON string. A nil pointer/slice/map marshals +// to "null", which unmarshalRawJSON treats as "leave the target untouched". +func marshalRawJSON(v any) string { + b, err := json.Marshal(v) + if err != nil { + return "" + } + return string(b) +} + +// unmarshalRawJSON unmarshals s into v, tolerating empty/"null" payloads. +func unmarshalRawJSON(s string, v any) { + if s == "" || s == "null" { + return + } + _ = json.Unmarshal([]byte(s), v) +} + +// ============================================================================= +// Project model mapping +// ============================================================================= + +// entProjectToStore converts an Ent Project entity to a store.Project model. +// Computed fields (AgentCount, ActiveBrokerCount, ProjectType, OwnerName) are +// not set here; callers that need them invoke populateProjectComputed. +func entProjectToStore(p *ent.Project) *store.Project { + sp := &store.Project{ + ID: p.ID.String(), + Name: p.Name, + Slug: p.Slug, + Labels: p.Labels, + Annotations: p.Annotations, + Created: p.Created, + Updated: p.Updated, + CreatedBy: p.CreatedBy, + OwnerID: p.OwnerID, + Visibility: p.Visibility, + } + if p.GitRemote != nil { + sp.GitRemote = *p.GitRemote + } + if p.DefaultRuntimeBrokerID != nil { + sp.DefaultRuntimeBrokerID = *p.DefaultRuntimeBrokerID + } + if p.GithubInstallationID != nil { + sp.GitHubInstallationID = p.GithubInstallationID + } + if p.SharedDirs != "" { + var dirs []api.SharedDir + unmarshalRawJSON(p.SharedDirs, &dirs) + sp.SharedDirs = dirs + } + if p.GithubPermissions != "" { + sp.GitHubPermissions = &store.GitHubTokenPermissions{} + unmarshalRawJSON(p.GithubPermissions, sp.GitHubPermissions) + } + if p.GithubAppStatus != "" { + sp.GitHubAppStatus = &store.GitHubAppProjectStatus{} + unmarshalRawJSON(p.GithubAppStatus, sp.GitHubAppStatus) + } + if p.GitIdentity != "" { + sp.GitIdentity = &store.GitIdentityConfig{} + unmarshalRawJSON(p.GitIdentity, sp.GitIdentity) + } + return sp +} + +// CreateProject creates a new project record. +func (s *ProjectStore) CreateProject(ctx context.Context, p *store.Project) error { + uid, err := parseUUID(p.ID) + if err != nil { + return err + } + + create := s.client.Project.Create(). + SetID(uid). + SetName(p.Name). + SetSlug(p.Slug). + SetCreatedBy(p.CreatedBy). + SetOwnerID(p.OwnerID) + + if p.Visibility != "" { + create.SetVisibility(p.Visibility) + } + if p.GitRemote != "" { + create.SetGitRemote(p.GitRemote) + } + if p.DefaultRuntimeBrokerID != "" { + create.SetDefaultRuntimeBrokerID(p.DefaultRuntimeBrokerID) + } + if p.Labels != nil { + create.SetLabels(p.Labels) + } + if p.Annotations != nil { + create.SetAnnotations(p.Annotations) + } + if len(p.SharedDirs) > 0 { + create.SetSharedDirs(marshalRawJSON(p.SharedDirs)) + } + if p.GitHubInstallationID != nil { + create.SetGithubInstallationID(*p.GitHubInstallationID) + } + if p.GitHubPermissions != nil { + create.SetGithubPermissions(marshalRawJSON(p.GitHubPermissions)) + } + if p.GitHubAppStatus != nil { + create.SetGithubAppStatus(marshalRawJSON(p.GitHubAppStatus)) + } + if p.GitIdentity != nil { + create.SetGitIdentity(marshalRawJSON(p.GitIdentity)) + } + + created, err := create.Save(ctx) + if err != nil { + return mapError(err) + } + + p.Created = created.Created + p.Updated = created.Updated + if p.Visibility == "" { + p.Visibility = created.Visibility + } + return nil +} + +// GetProject retrieves a project by ID, including computed fields. +func (s *ProjectStore) GetProject(ctx context.Context, id string) (*store.Project, error) { + uid, err := parseGetID(id) + if err != nil { + return nil, err + } + + p, err := s.client.Project.Get(ctx, uid) + if err != nil { + return nil, mapError(err) + } + + sp := entProjectToStore(p) + if err := s.populateProjectComputed(ctx, sp, uid); err != nil { + return nil, err + } + return sp, nil +} + +// GetProjectBySlug retrieves a project by its exact (case-sensitive) slug. +func (s *ProjectStore) GetProjectBySlug(ctx context.Context, slug string) (*store.Project, error) { + p, err := s.client.Project.Query().Where(project.SlugEQ(slug)).Only(ctx) + if err != nil { + return nil, mapError(err) + } + sp := entProjectToStore(p) + if err := s.populateProjectComputed(ctx, sp, p.ID); err != nil { + return nil, err + } + return sp, nil +} + +// GetProjectBySlugCaseInsensitive retrieves a project by slug, ignoring case. +func (s *ProjectStore) GetProjectBySlugCaseInsensitive(ctx context.Context, slug string) (*store.Project, error) { + p, err := s.client.Project.Query().Where(project.SlugEqualFold(slug)).First(ctx) + if err != nil { + return nil, mapError(err) + } + sp := entProjectToStore(p) + if err := s.populateProjectComputed(ctx, sp, p.ID); err != nil { + return nil, err + } + return sp, nil +} + +// GetProjectsByGitRemote returns all projects matching the git remote URL, +// ordered by creation time ascending. Returns an empty slice if none match. +func (s *ProjectStore) GetProjectsByGitRemote(ctx context.Context, gitRemote string) ([]*store.Project, error) { + rows, err := s.client.Project.Query(). + Where(project.GitRemoteEQ(gitRemote)). + Order(ent.Asc(project.FieldCreated)). + All(ctx) + if err != nil { + return nil, err + } + + projects := make([]*store.Project, 0, len(rows)) + for _, p := range rows { + sp := entProjectToStore(p) + if err := s.populateProjectComputed(ctx, sp, p.ID); err != nil { + return nil, err + } + projects = append(projects, sp) + } + return projects, nil +} + +// NextAvailableSlug returns baseSlug if free, else baseSlug-1, baseSlug-2, ... +func (s *ProjectStore) NextAvailableSlug(ctx context.Context, baseSlug string) (string, error) { + exists, err := s.client.Project.Query().Where(project.SlugEQ(baseSlug)).Exist(ctx) + if err != nil { + return "", err + } + if !exists { + return baseSlug, nil + } + for i := 1; ; i++ { + candidate := fmt.Sprintf("%s-%d", baseSlug, i) + exists, err := s.client.Project.Query().Where(project.SlugEQ(candidate)).Exist(ctx) + if err != nil { + return "", err + } + if !exists { + return candidate, nil + } + } +} + +// UpdateProject updates an existing project. +func (s *ProjectStore) UpdateProject(ctx context.Context, p *store.Project) error { + uid, err := parseUUID(p.ID) + if err != nil { + return err + } + + update := s.client.Project.UpdateOneID(uid). + SetName(p.Name). + SetSlug(p.Slug). + SetOwnerID(p.OwnerID). + SetVisibility(p.Visibility) + + if p.GitRemote != "" { + update.SetGitRemote(p.GitRemote) + } else { + update.ClearGitRemote() + } + if p.DefaultRuntimeBrokerID != "" { + update.SetDefaultRuntimeBrokerID(p.DefaultRuntimeBrokerID) + } else { + update.ClearDefaultRuntimeBrokerID() + } + if p.Labels != nil { + update.SetLabels(p.Labels) + } else { + update.ClearLabels() + } + if p.Annotations != nil { + update.SetAnnotations(p.Annotations) + } else { + update.ClearAnnotations() + } + if len(p.SharedDirs) > 0 { + update.SetSharedDirs(marshalRawJSON(p.SharedDirs)) + } else { + update.ClearSharedDirs() + } + if p.GitHubInstallationID != nil { + update.SetGithubInstallationID(*p.GitHubInstallationID) + } else { + update.ClearGithubInstallationID() + } + if p.GitHubPermissions != nil { + update.SetGithubPermissions(marshalRawJSON(p.GitHubPermissions)) + } else { + update.ClearGithubPermissions() + } + if p.GitHubAppStatus != nil { + update.SetGithubAppStatus(marshalRawJSON(p.GitHubAppStatus)) + } else { + update.ClearGithubAppStatus() + } + if p.GitIdentity != nil { + update.SetGitIdentity(marshalRawJSON(p.GitIdentity)) + } else { + update.ClearGitIdentity() + } + + updated, err := update.Save(ctx) + if err != nil { + return mapError(err) + } + p.Updated = updated.Updated + return nil +} + +// DeleteProject removes a project by ID. +func (s *ProjectStore) DeleteProject(ctx context.Context, id string) error { + uid, err := parseUUID(id) + if err != nil { + return err + } + if err := s.client.Project.DeleteOneID(uid).Exec(ctx); err != nil { + return mapError(err) + } + return nil +} + +// ListProjects returns projects matching the filter criteria. +func (s *ProjectStore) ListProjects(ctx context.Context, filter store.ProjectFilter, opts store.ListOptions) (*store.ListResult[store.Project], error) { + query := s.client.Project.Query() + + // Membership / ownership filtering mirrors the SQLite precedence: + // MemberOrOwnerIDs > MemberProjectIDs > OwnerID. + switch { + case len(filter.MemberOrOwnerIDs) > 0: + ids, err := parseUUIDs(filter.MemberOrOwnerIDs) + if err != nil { + return nil, err + } + if filter.OwnerID != "" { + query.Where(project.Or(project.IDIn(ids...), project.OwnerIDEQ(filter.OwnerID))) + } else { + query.Where(project.IDIn(ids...)) + } + case len(filter.MemberProjectIDs) > 0: + ids, err := parseUUIDs(filter.MemberProjectIDs) + if err != nil { + return nil, err + } + query.Where(project.IDIn(ids...)) + case filter.OwnerID != "": + query.Where(project.OwnerIDEQ(filter.OwnerID)) + } + + if filter.ExcludeOwnerID != "" { + query.Where(project.OwnerIDNEQ(filter.ExcludeOwnerID)) + } + if filter.Visibility != "" { + query.Where(project.VisibilityEQ(filter.Visibility)) + } + if filter.GitRemote != "" { + query.Where(project.GitRemoteEQ(filter.GitRemote)) + } else if filter.GitRemotePrefix != "" { + query.Where(project.GitRemoteHasPrefix(filter.GitRemotePrefix)) + } + if filter.BrokerID != "" { + brokerUID, err := parseUUID(filter.BrokerID) + if err != nil { + return nil, err + } + projectIDs, err := s.client.ProjectContributor.Query(). + Where(projectcontributor.BrokerIDEQ(brokerUID)). + Select(projectcontributor.FieldProjectID). + Strings(ctx) + if err != nil { + return nil, err + } + ids, err := parseUUIDs(projectIDs) + if err != nil { + return nil, err + } + query.Where(project.IDIn(ids...)) + } + if filter.Name != "" { + query.Where(project.NameEqualFold(filter.Name)) + } + if filter.Slug != "" { + query.Where(project.SlugEqualFold(filter.Slug)) + } + + totalCount, err := query.Clone().Count(ctx) + if err != nil { + return nil, err + } + + limit := opts.Limit + if limit <= 0 { + limit = 50 + } + + rows, err := query. + Order(ent.Desc(project.FieldCreated)). + Limit(limit). + All(ctx) + if err != nil { + return nil, err + } + + items := make([]store.Project, 0, len(rows)) + for _, p := range rows { + sp := entProjectToStore(p) + if err := s.populateProjectComputed(ctx, sp, p.ID); err != nil { + return nil, err + } + items = append(items, *sp) + } + + return &store.ListResult[store.Project]{ + Items: items, + TotalCount: totalCount, + }, nil +} + +// populateProjectComputed fills the computed (non-persisted) fields on a project: +// AgentCount, ActiveBrokerCount and ProjectType. This mirrors the derivations +// performed by the legacy SQLite store on read. +func (s *ProjectStore) populateProjectComputed(ctx context.Context, p *store.Project, uid uuid.UUID) error { + agentCount, err := s.client.Agent.Query().Where(agent.ProjectIDEQ(uid)).Count(ctx) + if err != nil { + return err + } + p.AgentCount = agentCount + + contribs, err := s.client.ProjectContributor.Query(). + Where(projectcontributor.ProjectIDEQ(uid)). + All(ctx) + if err != nil { + return err + } + + onlineContrib := 0 + contribBrokerIDs := make([]uuid.UUID, 0, len(contribs)) + linked := false + for _, c := range contribs { + contribBrokerIDs = append(contribBrokerIDs, c.BrokerID) + if c.Status == store.BrokerStatusOnline { + onlineContrib++ + } + // A contributor with a local path outside ~/.scion/projects/ indicates a + // pre-existing local project that was linked to the hub. + if c.LocalPath != "" && !strings.Contains(c.LocalPath, "/.scion/projects/") { + linked = true + } + } + + autoQuery := s.client.RuntimeBroker.Query().Where( + runtimebroker.AutoProvide(true), + runtimebroker.StatusEQ(store.BrokerStatusOnline), + ) + if len(contribBrokerIDs) > 0 { + autoQuery = autoQuery.Where(runtimebroker.IDNotIn(contribBrokerIDs...)) + } + autoOnline, err := autoQuery.Count(ctx) + if err != nil { + return err + } + p.ActiveBrokerCount = onlineContrib + autoOnline + + if linked { + p.ProjectType = store.ProjectTypeLinked + } else { + p.ProjectType = store.ProjectTypeHubManaged + } + return nil +} + +// parseUUIDs parses a slice of string UUIDs, skipping any that fail to parse. +func parseUUIDs(ids []string) ([]uuid.UUID, error) { + out := make([]uuid.UUID, 0, len(ids)) + for _, id := range ids { + uid, err := uuid.Parse(id) + if err != nil { + continue + } + out = append(out, uid) + } + return out, nil +} + +// ============================================================================= +// RuntimeBroker operations +// ============================================================================= + +// entBrokerToStore converts an Ent RuntimeBroker entity to a store model. +func entBrokerToStore(b *ent.RuntimeBroker) *store.RuntimeBroker { + sb := &store.RuntimeBroker{ + ID: b.ID.String(), + Name: b.Name, + Slug: b.Slug, + Version: b.Version, + Status: b.Status, + ConnectionState: b.ConnectionState, + Endpoint: b.Endpoint, + AutoProvide: b.AutoProvide, + Created: b.Created, + Updated: b.Updated, + CreatedBy: b.CreatedBy, + } + if b.LastHeartbeat != nil { + sb.LastHeartbeat = *b.LastHeartbeat + } + sb.ConnectedHubID = b.ConnectedHubID + sb.ConnectedSessionID = b.ConnectedSessionID + sb.ConnectedAt = b.ConnectedAt + unmarshalRawJSON(b.Capabilities, &sb.Capabilities) + // Profiles are persisted in the "runtimes" column (legacy naming). + unmarshalRawJSON(b.Runtimes, &sb.Profiles) + unmarshalRawJSON(b.Labels, &sb.Labels) + unmarshalRawJSON(b.Annotations, &sb.Annotations) + return sb +} + +// CreateRuntimeBroker creates a new runtime broker record. +func (s *ProjectStore) CreateRuntimeBroker(ctx context.Context, b *store.RuntimeBroker) error { + uid, err := parseUUID(b.ID) + if err != nil { + return err + } + + create := s.client.RuntimeBroker.Create(). + SetID(uid). + SetName(b.Name). + SetSlug(b.Slug). + SetEndpoint(b.Endpoint). + SetAutoProvide(b.AutoProvide). + SetCapabilities(marshalRawJSON(b.Capabilities)). + SetRuntimes(marshalRawJSON(b.Profiles)). + SetLabels(marshalRawJSON(b.Labels)). + SetAnnotations(marshalRawJSON(b.Annotations)) + + if b.Version != "" { + create.SetVersion(b.Version) + } + if b.Status != "" { + create.SetStatus(b.Status) + } + if b.ConnectionState != "" { + create.SetConnectionState(b.ConnectionState) + } + if !b.LastHeartbeat.IsZero() { + create.SetLastHeartbeat(b.LastHeartbeat) + } + if b.CreatedBy != "" { + create.SetCreatedBy(b.CreatedBy) + } + if b.ConnectedHubID != nil { + create.SetConnectedHubID(*b.ConnectedHubID) + } + if b.ConnectedSessionID != nil { + create.SetConnectedSessionID(*b.ConnectedSessionID) + } + if b.ConnectedAt != nil { + create.SetConnectedAt(*b.ConnectedAt) + } + + created, err := create.Save(ctx) + if err != nil { + return mapError(err) + } + b.Created = created.Created + b.Updated = created.Updated + if b.Status == "" { + b.Status = created.Status + } + if b.ConnectionState == "" { + b.ConnectionState = created.ConnectionState + } + return nil +} + +// GetRuntimeBroker retrieves a runtime broker by ID. +func (s *ProjectStore) GetRuntimeBroker(ctx context.Context, id string) (*store.RuntimeBroker, error) { + uid, err := parseGetID(id) + if err != nil { + return nil, err + } + b, err := s.client.RuntimeBroker.Get(ctx, uid) + if err != nil { + return nil, mapError(err) + } + return entBrokerToStore(b), nil +} + +// GetRuntimeBrokerByName retrieves a runtime broker by name (case-insensitive). +func (s *ProjectStore) GetRuntimeBrokerByName(ctx context.Context, name string) (*store.RuntimeBroker, error) { + b, err := s.client.RuntimeBroker.Query(). + Where(runtimebroker.NameEqualFold(name)). + First(ctx) + if err != nil { + return nil, mapError(err) + } + return entBrokerToStore(b), nil +} + +// UpdateRuntimeBroker updates an existing runtime broker using an optimistic +// concurrency (version-CAS) loop on the internal lock_version token so that +// concurrent writers cannot silently clobber one another. This is portable +// across SQLite (tests) and Postgres (production) without SELECT ... FOR UPDATE. +func (s *ProjectStore) UpdateRuntimeBroker(ctx context.Context, b *store.RuntimeBroker) error { + uid, err := parseUUID(b.ID) + if err != nil { + return err + } + + now := time.Now() + for attempt := 0; attempt < maxCASRetries; attempt++ { + cur, err := s.client.RuntimeBroker.Get(ctx, uid) + if err != nil { + return mapError(err) + } + + update := s.client.RuntimeBroker.Update(). + Where(runtimebroker.IDEQ(uid), runtimebroker.LockVersionEQ(cur.LockVersion)). + SetName(b.Name). + SetSlug(b.Slug). + SetVersion(b.Version). + SetStatus(b.Status). + SetConnectionState(b.ConnectionState). + SetLastHeartbeat(b.LastHeartbeat). + SetCapabilities(marshalRawJSON(b.Capabilities)). + SetRuntimes(marshalRawJSON(b.Profiles)). + SetLabels(marshalRawJSON(b.Labels)). + SetAnnotations(marshalRawJSON(b.Annotations)). + SetEndpoint(b.Endpoint). + SetAutoProvide(b.AutoProvide). + SetUpdated(now). + AddLockVersion(1) + if b.ConnectedHubID != nil { + update.SetConnectedHubID(*b.ConnectedHubID) + } else { + update.ClearConnectedHubID() + } + if b.ConnectedSessionID != nil { + update.SetConnectedSessionID(*b.ConnectedSessionID) + } else { + update.ClearConnectedSessionID() + } + if b.ConnectedAt != nil { + update.SetConnectedAt(*b.ConnectedAt) + } else { + update.ClearConnectedAt() + } + affected, err := update.Save(ctx) + if err != nil { + return mapError(err) + } + if affected == 1 { + b.Updated = now + return nil + } + // affected == 0: another writer advanced lock_version between our read + // and write — retry with the fresh value. + } + return store.ErrVersionConflict +} + +// DeleteRuntimeBroker removes a runtime broker by ID. +func (s *ProjectStore) DeleteRuntimeBroker(ctx context.Context, id string) error { + uid, err := parseUUID(id) + if err != nil { + return err + } + if err := s.client.RuntimeBroker.DeleteOneID(uid).Exec(ctx); err != nil { + return mapError(err) + } + return nil +} + +// ListRuntimeBrokers returns runtime brokers matching the filter criteria. +func (s *ProjectStore) ListRuntimeBrokers(ctx context.Context, filter store.RuntimeBrokerFilter, opts store.ListOptions) (*store.ListResult[store.RuntimeBroker], error) { + query := s.client.RuntimeBroker.Query() + + if filter.Status != "" { + query.Where(runtimebroker.StatusEQ(filter.Status)) + } + if filter.ProjectID != "" { + projectUID, err := parseUUID(filter.ProjectID) + if err != nil { + return nil, err + } + brokerIDStrs, err := s.client.ProjectContributor.Query(). + Where(projectcontributor.ProjectIDEQ(projectUID)). + Select(projectcontributor.FieldBrokerID). + Strings(ctx) + if err != nil { + return nil, err + } + brokerIDs, err := parseUUIDs(brokerIDStrs) + if err != nil { + return nil, err + } + // A broker provides for a project if it is an explicit contributor OR it + // is configured to auto-provide for all projects. + if len(brokerIDs) > 0 { + query.Where(runtimebroker.Or( + runtimebroker.IDIn(brokerIDs...), + runtimebroker.AutoProvide(true), + )) + } else { + query.Where(runtimebroker.AutoProvide(true)) + } + } + if filter.Name != "" { + query.Where(runtimebroker.NameEqualFold(filter.Name)) + } + if filter.AutoProvide != nil { + query.Where(runtimebroker.AutoProvide(*filter.AutoProvide)) + } + + totalCount, err := query.Clone().Count(ctx) + if err != nil { + return nil, err + } + + limit := opts.Limit + if limit <= 0 { + limit = 50 + } + + rows, err := query. + Order(ent.Desc(runtimebroker.FieldCreated)). + Limit(limit). + All(ctx) + if err != nil { + return nil, err + } + + items := make([]store.RuntimeBroker, 0, len(rows)) + for _, b := range rows { + items = append(items, *entBrokerToStore(b)) + } + return &store.ListResult[store.RuntimeBroker]{ + Items: items, + TotalCount: totalCount, + }, nil +} + +// UpdateRuntimeBrokerHeartbeat updates the broker's status and last-heartbeat +// timestamp. It uses the same version-CAS loop as UpdateRuntimeBroker so that a +// high-frequency heartbeat cannot lose an interleaved write; the bump on +// lock_version serializes concurrent heartbeats on both SQLite and Postgres. +func (s *ProjectStore) UpdateRuntimeBrokerHeartbeat(ctx context.Context, id string, status string) error { + uid, err := parseUUID(id) + if err != nil { + return err + } + + now := time.Now() + for attempt := 0; attempt < maxCASRetries; attempt++ { + cur, err := s.client.RuntimeBroker.Get(ctx, uid) + if err != nil { + return mapError(err) + } + affected, err := s.client.RuntimeBroker.Update(). + Where(runtimebroker.IDEQ(uid), runtimebroker.LockVersionEQ(cur.LockVersion)). + SetStatus(status). + SetLastHeartbeat(now). + SetUpdated(now). + AddLockVersion(1). + Save(ctx) + if err != nil { + return mapError(err) + } + if affected == 1 { + return nil + } + } + return store.ErrVersionConflict +} + +// ClaimRuntimeBrokerConnection records this hub instance as the owner of the +// broker's live control-channel socket. The newest connection wins +// (unconditional claim — mirrors a fresh socket replacing an old one): it sets +// the affinity columns and, in the same CAS write, bumps status to online and +// refreshes last_heartbeat. Uses the lock_version optimistic-concurrency loop, +// like UpdateRuntimeBrokerHeartbeat. +func (s *ProjectStore) ClaimRuntimeBrokerConnection(ctx context.Context, brokerID, hubInstanceID, sessionID string) error { + uid, err := parseUUID(brokerID) + if err != nil { + return err + } + + now := time.Now() + for attempt := 0; attempt < maxCASRetries; attempt++ { + cur, err := s.client.RuntimeBroker.Get(ctx, uid) + if err != nil { + return mapError(err) + } + affected, err := s.client.RuntimeBroker.Update(). + Where(runtimebroker.IDEQ(uid), runtimebroker.LockVersionEQ(cur.LockVersion)). + SetConnectedHubID(hubInstanceID). + SetConnectedSessionID(sessionID). + SetConnectedAt(now). + SetStatus(store.BrokerStatusOnline). + SetLastHeartbeat(now). + SetUpdated(now). + AddLockVersion(1). + Save(ctx) + if err != nil { + return mapError(err) + } + if affected == 1 { + return nil + } + } + return store.ErrVersionConflict +} + +// ReleaseRuntimeBrokerConnection clears the broker's affinity ONLY IF it still +// names (hubInstanceID, sessionID) — a compare-and-clear that fixes the +// disconnect-race: a delayed disconnect from a stale owner/session must not +// clobber a live owner. Returns cleared=true when this caller owned the +// affinity and it was cleared; cleared=false (no-op) when affinity has already +// moved (or was already clear). Does not change status — the caller decides +// whether to stamp offline based on cleared. +func (s *ProjectStore) ReleaseRuntimeBrokerConnection(ctx context.Context, brokerID, hubInstanceID, sessionID string) (bool, error) { + uid, err := parseUUID(brokerID) + if err != nil { + return false, err + } + + now := time.Now() + for attempt := 0; attempt < maxCASRetries; attempt++ { + cur, err := s.client.RuntimeBroker.Get(ctx, uid) + if err != nil { + return false, mapError(err) + } + // Compare: only clear if affinity still names this exact (hub, session). + if cur.ConnectedHubID == nil || *cur.ConnectedHubID != hubInstanceID || + cur.ConnectedSessionID == nil || *cur.ConnectedSessionID != sessionID { + return false, nil + } + affected, err := s.client.RuntimeBroker.Update(). + Where(runtimebroker.IDEQ(uid), runtimebroker.LockVersionEQ(cur.LockVersion)). + ClearConnectedHubID(). + ClearConnectedSessionID(). + ClearConnectedAt(). + SetUpdated(now). + AddLockVersion(1). + Save(ctx) + if err != nil { + return false, mapError(err) + } + if affected == 1 { + return true, nil + } + // affected==0: lock_version moved under us; re-read and re-evaluate the + // compare on the next iteration (affinity may have moved away). + } + return false, store.ErrVersionConflict +} + +// ReleaseAndMarkBrokerOffline atomically clears broker affinity AND stamps +// status=offline in a single CAS write, ONLY IF affinity still names +// (hubInstanceID, sessionID). This eliminates the TOCTOU race between a +// separate release and a separate offline stamp: if a concurrent reconnect +// has already claimed the broker with a new session, the compare fails and +// this is a no-op — the new connection's online status is not clobbered. +func (s *ProjectStore) ReleaseAndMarkBrokerOffline(ctx context.Context, brokerID, hubInstanceID, sessionID string) (bool, error) { + uid, err := parseUUID(brokerID) + if err != nil { + return false, err + } + + now := time.Now() + for attempt := 0; attempt < maxCASRetries; attempt++ { + cur, err := s.client.RuntimeBroker.Get(ctx, uid) + if err != nil { + return false, mapError(err) + } + if cur.ConnectedHubID == nil || *cur.ConnectedHubID != hubInstanceID || + cur.ConnectedSessionID == nil || *cur.ConnectedSessionID != sessionID { + return false, nil + } + affected, err := s.client.RuntimeBroker.Update(). + Where(runtimebroker.IDEQ(uid), runtimebroker.LockVersionEQ(cur.LockVersion)). + ClearConnectedHubID(). + ClearConnectedSessionID(). + ClearConnectedAt(). + SetStatus(store.BrokerStatusOffline). + SetLastHeartbeat(now). + SetUpdated(now). + AddLockVersion(1). + Save(ctx) + if err != nil { + return false, mapError(err) + } + if affected == 1 { + return true, nil + } + } + return false, store.ErrVersionConflict +} + +// ReapStaleBrokerAffinity clears affinity (connected_hub_id/connected_session_id/ +// connected_at) for brokers that still claim affinity but whose last_heartbeat +// is older than staleBefore. Does not change broker status. +func (s *ProjectStore) ReapStaleBrokerAffinity(ctx context.Context, staleBefore time.Time) (int, error) { + affected, err := s.client.RuntimeBroker.Update(). + Where( + runtimebroker.ConnectedHubIDNotNil(), + runtimebroker.LastHeartbeatLT(staleBefore), + ). + ClearConnectedHubID(). + ClearConnectedSessionID(). + ClearConnectedAt(). + SetUpdated(time.Now()). + Save(ctx) + if err != nil { + return 0, mapError(err) + } + return affected, nil +} + +// ============================================================================= +// ProjectProvider (project_contributors) operations +// ============================================================================= + +// entContributorToStore converts an Ent ProjectContributor to a store model. +func entContributorToStore(c *ent.ProjectContributor) store.ProjectProvider { + pp := store.ProjectProvider{ + ProjectID: c.ProjectID.String(), + BrokerID: c.BrokerID.String(), + BrokerName: c.BrokerName, + LocalPath: c.LocalPath, + Status: c.Status, + LinkedBy: c.LinkedBy, + } + if c.LastSeen != nil { + pp.LastSeen = *c.LastSeen + } + if c.LinkedAt != nil { + pp.LinkedAt = *c.LinkedAt + } + return pp +} + +// AddProjectProvider adds (or replaces) a broker as a provider to a project. +// Mirrors the legacy INSERT OR REPLACE via Ent's OnConflict upsert keyed on the +// (project_id, broker_id) unique index. +func (s *ProjectStore) AddProjectProvider(ctx context.Context, provider *store.ProjectProvider) error { + projectUID, err := parseUUID(provider.ProjectID) + if err != nil { + return err + } + brokerUID, err := parseUUID(provider.BrokerID) + if err != nil { + return err + } + + if provider.LinkedAt.IsZero() && provider.LinkedBy != "" { + provider.LinkedAt = time.Now() + } + + status := provider.Status + if status == "" { + status = store.BrokerStatusOffline + } + + create := s.client.ProjectContributor.Create(). + SetProjectID(projectUID). + SetBrokerID(brokerUID). + SetBrokerName(provider.BrokerName). + SetLocalPath(provider.LocalPath). + SetStatus(status) + if !provider.LastSeen.IsZero() { + create.SetLastSeen(provider.LastSeen) + } + if provider.LinkedBy != "" { + create.SetLinkedBy(provider.LinkedBy) + } + if !provider.LinkedAt.IsZero() { + create.SetLinkedAt(provider.LinkedAt) + } + + return create. + OnConflictColumns(projectcontributor.FieldProjectID, projectcontributor.FieldBrokerID). + UpdateNewValues(). + Exec(ctx) +} + +// RemoveProjectProvider removes a broker from a project's providers. +func (s *ProjectStore) RemoveProjectProvider(ctx context.Context, projectID, brokerID string) error { + projectUID, err := parseUUID(projectID) + if err != nil { + return err + } + brokerUID, err := parseUUID(brokerID) + if err != nil { + return err + } + n, err := s.client.ProjectContributor.Delete(). + Where( + projectcontributor.ProjectIDEQ(projectUID), + projectcontributor.BrokerIDEQ(brokerUID), + ).Exec(ctx) + if err != nil { + return err + } + if n == 0 { + return store.ErrNotFound + } + return nil +} + +// GetProjectProvider returns a specific provider by project and broker ID. +func (s *ProjectStore) GetProjectProvider(ctx context.Context, projectID, brokerID string) (*store.ProjectProvider, error) { + projectUID, err := parseUUID(projectID) + if err != nil { + return nil, err + } + brokerUID, err := parseUUID(brokerID) + if err != nil { + return nil, err + } + c, err := s.client.ProjectContributor.Query(). + Where( + projectcontributor.ProjectIDEQ(projectUID), + projectcontributor.BrokerIDEQ(brokerUID), + ).Only(ctx) + if err != nil { + return nil, mapError(err) + } + pp := entContributorToStore(c) + return &pp, nil +} + +// GetProjectProviders returns all providers for a project. +func (s *ProjectStore) GetProjectProviders(ctx context.Context, projectID string) ([]store.ProjectProvider, error) { + projectUID, err := parseUUID(projectID) + if err != nil { + return nil, err + } + rows, err := s.client.ProjectContributor.Query(). + Where(projectcontributor.ProjectIDEQ(projectUID)). + All(ctx) + if err != nil { + return nil, err + } + providers := make([]store.ProjectProvider, 0, len(rows)) + for _, c := range rows { + providers = append(providers, entContributorToStore(c)) + } + return providers, nil +} + +// GetBrokerProjects returns all projects a broker provides for. +func (s *ProjectStore) GetBrokerProjects(ctx context.Context, brokerID string) ([]store.ProjectProvider, error) { + brokerUID, err := parseUUID(brokerID) + if err != nil { + return nil, err + } + rows, err := s.client.ProjectContributor.Query(). + Where(projectcontributor.BrokerIDEQ(brokerUID)). + All(ctx) + if err != nil { + return nil, err + } + providers := make([]store.ProjectProvider, 0, len(rows)) + for _, c := range rows { + providers = append(providers, entContributorToStore(c)) + } + return providers, nil +} + +// UpdateProviderStatus updates a provider's status and last-seen timestamp. +func (s *ProjectStore) UpdateProviderStatus(ctx context.Context, projectID, brokerID, status string) error { + projectUID, err := parseUUID(projectID) + if err != nil { + return err + } + brokerUID, err := parseUUID(brokerID) + if err != nil { + return err + } + n, err := s.client.ProjectContributor.Update(). + Where( + projectcontributor.ProjectIDEQ(projectUID), + projectcontributor.BrokerIDEQ(brokerUID), + ). + SetStatus(status). + SetLastSeen(time.Now()). + Save(ctx) + if err != nil { + return err + } + if n == 0 { + return store.ErrNotFound + } + return nil +} + +// ============================================================================= +// ProjectSyncState (project_sync_state) operations +// ============================================================================= + +// entSyncStateToStore converts an Ent ProjectSyncState to a store model. +func entSyncStateToStore(s *ent.ProjectSyncState) *store.ProjectSyncState { + state := &store.ProjectSyncState{ + ProjectID: s.ProjectID.String(), + BrokerID: s.BrokerID, + LastCommitSHA: s.LastCommitSha, + FileCount: s.FileCount, + TotalBytes: s.TotalBytes, + } + if s.LastSyncTime != nil { + state.LastSyncTime = s.LastSyncTime + } + return state +} + +// UpsertProjectSyncState creates or updates sync state for a project (optionally +// per broker). Mirrors the legacy ON CONFLICT(project_id, broker_id) upsert. +func (s *ProjectStore) UpsertProjectSyncState(ctx context.Context, state *store.ProjectSyncState) error { + projectUID, err := parseUUID(state.ProjectID) + if err != nil { + return err + } + + create := s.client.ProjectSyncState.Create(). + SetProjectID(projectUID). + SetBrokerID(state.BrokerID). + SetLastCommitSha(state.LastCommitSHA). + SetFileCount(state.FileCount). + SetTotalBytes(state.TotalBytes) + if state.LastSyncTime != nil { + create.SetLastSyncTime(*state.LastSyncTime) + } + + return create. + OnConflictColumns(projectsyncstate.FieldProjectID, projectsyncstate.FieldBrokerID). + UpdateNewValues(). + Exec(ctx) +} + +// GetProjectSyncState retrieves sync state for a project and optional broker. +func (s *ProjectStore) GetProjectSyncState(ctx context.Context, projectID, brokerID string) (*store.ProjectSyncState, error) { + projectUID, err := parseUUID(projectID) + if err != nil { + return nil, err + } + row, err := s.client.ProjectSyncState.Query(). + Where( + projectsyncstate.ProjectIDEQ(projectUID), + projectsyncstate.BrokerIDEQ(brokerID), + ).Only(ctx) + if err != nil { + return nil, mapError(err) + } + return entSyncStateToStore(row), nil +} + +// ListProjectSyncStates returns all sync states for a project, ordered by broker. +func (s *ProjectStore) ListProjectSyncStates(ctx context.Context, projectID string) ([]store.ProjectSyncState, error) { + projectUID, err := parseUUID(projectID) + if err != nil { + return nil, err + } + rows, err := s.client.ProjectSyncState.Query(). + Where(projectsyncstate.ProjectIDEQ(projectUID)). + Order(ent.Asc(projectsyncstate.FieldBrokerID)). + All(ctx) + if err != nil { + return nil, err + } + states := make([]store.ProjectSyncState, 0, len(rows)) + for _, row := range rows { + states = append(states, *entSyncStateToStore(row)) + } + return states, nil +} + +// DeleteProjectSyncState removes sync state for a project and optional broker. +func (s *ProjectStore) DeleteProjectSyncState(ctx context.Context, projectID, brokerID string) error { + projectUID, err := parseUUID(projectID) + if err != nil { + return err + } + n, err := s.client.ProjectSyncState.Delete(). + Where( + projectsyncstate.ProjectIDEQ(projectUID), + projectsyncstate.BrokerIDEQ(brokerID), + ).Exec(ctx) + if err != nil { + return err + } + if n == 0 { + return store.ErrNotFound + } + return nil +} diff --git a/pkg/store/entadapter/project_store_test.go b/pkg/store/entadapter/project_store_test.go new file mode 100644 index 000000000..0a1e75a26 --- /dev/null +++ b/pkg/store/entadapter/project_store_test.go @@ -0,0 +1,622 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//go:build !no_sqlite + +package entadapter + +import ( + "context" + "testing" + "time" + + "github.com/GoogleCloudPlatform/scion/pkg/api" + "github.com/GoogleCloudPlatform/scion/pkg/store" + "github.com/GoogleCloudPlatform/scion/pkg/store/enttest" + "github.com/google/uuid" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func newTestProjectStore(t *testing.T) *ProjectStore { + t.Helper() + client := enttest.NewClient(t) + return NewProjectStore(client) +} + +func newProject(seq int) *store.Project { + id := uuid.NewString() + return &store.Project{ + ID: id, + Name: "Project " + id[:8], + Slug: "project-" + id[:8], + Visibility: store.VisibilityPrivate, + Labels: map[string]string{"seq": id[:4]}, + } +} + +func TestProject_CreateGet(t *testing.T) { + ps := newTestProjectStore(t) + ctx := context.Background() + + p := newProject(1) + p.GitRemote = "https://github.com/acme/repo.git" + p.OwnerID = uuid.NewString() + require.NoError(t, ps.CreateProject(ctx, p)) + assert.False(t, p.Created.IsZero()) + assert.False(t, p.Updated.IsZero()) + + got, err := ps.GetProject(ctx, p.ID) + require.NoError(t, err) + assert.Equal(t, p.ID, got.ID) + assert.Equal(t, p.Name, got.Name) + assert.Equal(t, p.Slug, got.Slug) + assert.Equal(t, "https://github.com/acme/repo.git", got.GitRemote) + assert.Equal(t, store.VisibilityPrivate, got.Visibility) + assert.Equal(t, store.ProjectTypeHubManaged, got.ProjectType) // computed default +} + +func TestProject_CreateDuplicateSlug(t *testing.T) { + ps := newTestProjectStore(t) + ctx := context.Background() + + p1 := newProject(1) + p1.Slug = "dup-slug" + require.NoError(t, ps.CreateProject(ctx, p1)) + + p2 := newProject(2) + p2.Slug = "dup-slug" + err := ps.CreateProject(ctx, p2) + assert.ErrorIs(t, err, store.ErrAlreadyExists) +} + +func TestProject_GetNotFound(t *testing.T) { + ps := newTestProjectStore(t) + _, err := ps.GetProject(context.Background(), uuid.NewString()) + assert.ErrorIs(t, err, store.ErrNotFound) +} + +func TestProject_GetBySlugCaseInsensitive(t *testing.T) { + ps := newTestProjectStore(t) + ctx := context.Background() + + p := newProject(1) + p.Slug = "MixedCase-Slug" + require.NoError(t, ps.CreateProject(ctx, p)) + + got, err := ps.GetProjectBySlugCaseInsensitive(ctx, "mixedcase-slug") + require.NoError(t, err) + assert.Equal(t, p.ID, got.ID) + + // Exact (case-sensitive) lookup must not match a different case. + _, err = ps.GetProjectBySlug(ctx, "mixedcase-slug") + assert.ErrorIs(t, err, store.ErrNotFound) +} + +func TestProject_GetByGitRemote(t *testing.T) { + ps := newTestProjectStore(t) + ctx := context.Background() + + remote := "https://github.com/acme/shared.git" + for i := 0; i < 2; i++ { + p := newProject(i) + p.GitRemote = remote + require.NoError(t, ps.CreateProject(ctx, p)) + } + other := newProject(99) + other.GitRemote = "https://github.com/acme/other.git" + require.NoError(t, ps.CreateProject(ctx, other)) + + got, err := ps.GetProjectsByGitRemote(ctx, remote) + require.NoError(t, err) + assert.Len(t, got, 2) + + none, err := ps.GetProjectsByGitRemote(ctx, "https://github.com/none.git") + require.NoError(t, err) + assert.Empty(t, none) +} + +func TestProject_NextAvailableSlug(t *testing.T) { + ps := newTestProjectStore(t) + ctx := context.Background() + + slug, err := ps.NextAvailableSlug(ctx, "myproj") + require.NoError(t, err) + assert.Equal(t, "myproj", slug) + + p := newProject(1) + p.Slug = "myproj" + require.NoError(t, ps.CreateProject(ctx, p)) + + slug, err = ps.NextAvailableSlug(ctx, "myproj") + require.NoError(t, err) + assert.Equal(t, "myproj-1", slug) + + p2 := newProject(2) + p2.Slug = "myproj-1" + require.NoError(t, ps.CreateProject(ctx, p2)) + + slug, err = ps.NextAvailableSlug(ctx, "myproj") + require.NoError(t, err) + assert.Equal(t, "myproj-2", slug) +} + +func TestProject_Update(t *testing.T) { + ps := newTestProjectStore(t) + ctx := context.Background() + + p := newProject(1) + require.NoError(t, ps.CreateProject(ctx, p)) + + p.Name = "Renamed" + p.GitRemote = "https://github.com/acme/renamed.git" + installID := int64(424242) + p.GitHubInstallationID = &installID + require.NoError(t, ps.UpdateProject(ctx, p)) + + got, err := ps.GetProject(ctx, p.ID) + require.NoError(t, err) + assert.Equal(t, "Renamed", got.Name) + assert.Equal(t, "https://github.com/acme/renamed.git", got.GitRemote) + require.NotNil(t, got.GitHubInstallationID) + assert.Equal(t, int64(424242), *got.GitHubInstallationID) +} + +func TestProject_SharedDirsRoundTrip(t *testing.T) { + ps := newTestProjectStore(t) + ctx := context.Background() + + p := newProject(1) + p.SharedDirs = []api.SharedDir{{Name: "build-cache", ReadOnly: true, InWorkspace: true}} + require.NoError(t, ps.CreateProject(ctx, p)) + + got, err := ps.GetProject(ctx, p.ID) + require.NoError(t, err) + require.Len(t, got.SharedDirs, 1) + assert.Equal(t, "build-cache", got.SharedDirs[0].Name) + assert.True(t, got.SharedDirs[0].ReadOnly) + assert.True(t, got.SharedDirs[0].InWorkspace) +} + +func TestProject_Delete(t *testing.T) { + ps := newTestProjectStore(t) + ctx := context.Background() + + p := newProject(1) + require.NoError(t, ps.CreateProject(ctx, p)) + require.NoError(t, ps.DeleteProject(ctx, p.ID)) + + _, err := ps.GetProject(ctx, p.ID) + assert.ErrorIs(t, err, store.ErrNotFound) + + assert.ErrorIs(t, ps.DeleteProject(ctx, p.ID), store.ErrNotFound) +} + +func TestProject_ListFilters(t *testing.T) { + ps := newTestProjectStore(t) + ctx := context.Background() + + owner := uuid.NewString() + pub := newProject(1) + pub.Visibility = "public" + pub.OwnerID = owner + require.NoError(t, ps.CreateProject(ctx, pub)) + + priv := newProject(2) + priv.Visibility = store.VisibilityPrivate + require.NoError(t, ps.CreateProject(ctx, priv)) + + all, err := ps.ListProjects(ctx, store.ProjectFilter{}, store.ListOptions{}) + require.NoError(t, err) + assert.Equal(t, 2, all.TotalCount) + + byVis, err := ps.ListProjects(ctx, store.ProjectFilter{Visibility: "public"}, store.ListOptions{}) + require.NoError(t, err) + assert.Equal(t, 1, byVis.TotalCount) + require.Len(t, byVis.Items, 1) + assert.Equal(t, pub.ID, byVis.Items[0].ID) + + byOwner, err := ps.ListProjects(ctx, store.ProjectFilter{OwnerID: owner}, store.ListOptions{}) + require.NoError(t, err) + assert.Equal(t, 1, byOwner.TotalCount) + + byName, err := ps.ListProjects(ctx, store.ProjectFilter{Name: pub.Name}, store.ListOptions{}) + require.NoError(t, err) + assert.Equal(t, 1, byName.TotalCount) + + limited, err := ps.ListProjects(ctx, store.ProjectFilter{}, store.ListOptions{Limit: 1}) + require.NoError(t, err) + assert.Len(t, limited.Items, 1) + assert.Equal(t, 2, limited.TotalCount) +} + +func TestProject_ComputedAgentCount(t *testing.T) { + ps := newTestProjectStore(t) + ctx := context.Background() + + p := newProject(1) + require.NoError(t, ps.CreateProject(ctx, p)) + uid := uuid.MustParse(p.ID) + + for i := 0; i < 3; i++ { + _, err := ps.client.Agent.Create(). + SetID(uuid.New()). + SetName("agent"). + SetSlug("agent-" + uuid.NewString()[:8]). + SetProjectID(uid). + Save(ctx) + require.NoError(t, err) + } + + got, err := ps.GetProject(ctx, p.ID) + require.NoError(t, err) + assert.Equal(t, 3, got.AgentCount) +} + +func TestProject_ProjectTypeLinked(t *testing.T) { + ps := newTestProjectStore(t) + ctx := context.Background() + + p := newProject(1) + require.NoError(t, ps.CreateProject(ctx, p)) + + // A contributor with a local path outside ~/.scion/projects/ marks it linked. + require.NoError(t, ps.AddProjectProvider(ctx, &store.ProjectProvider{ + ProjectID: p.ID, + BrokerID: uuid.NewString(), + BrokerName: "broker-1", + LocalPath: "/home/user/code/myrepo/.scion", + Status: store.BrokerStatusOnline, + })) + + got, err := ps.GetProject(ctx, p.ID) + require.NoError(t, err) + assert.Equal(t, store.ProjectTypeLinked, got.ProjectType) +} + +// ============================================================================= +// RuntimeBroker +// ============================================================================= + +func newBroker() *store.RuntimeBroker { + id := uuid.NewString() + return &store.RuntimeBroker{ + ID: id, + Name: "broker-" + id[:8], + Slug: "broker-" + id[:8], + Version: "1.0.0", + Status: store.BrokerStatusOnline, + } +} + +func TestBroker_CreateGet(t *testing.T) { + ps := newTestProjectStore(t) + ctx := context.Background() + + b := newBroker() + b.Capabilities = &store.BrokerCapabilities{WebPTY: true, Sync: true} + b.Profiles = []store.BrokerProfile{{Name: "docker-default", Type: "docker", Available: true}} + b.AutoProvide = true + require.NoError(t, ps.CreateRuntimeBroker(ctx, b)) + assert.False(t, b.Created.IsZero()) + + got, err := ps.GetRuntimeBroker(ctx, b.ID) + require.NoError(t, err) + assert.Equal(t, b.Name, got.Name) + assert.Equal(t, "1.0.0", got.Version) + assert.Equal(t, store.BrokerStatusOnline, got.Status) + assert.True(t, got.AutoProvide) + require.NotNil(t, got.Capabilities) + assert.True(t, got.Capabilities.WebPTY) + require.Len(t, got.Profiles, 1) + assert.Equal(t, "docker-default", got.Profiles[0].Name) +} + +func TestBroker_GetByName(t *testing.T) { + ps := newTestProjectStore(t) + ctx := context.Background() + + b := newBroker() + b.Name = "MyBroker" + require.NoError(t, ps.CreateRuntimeBroker(ctx, b)) + + got, err := ps.GetRuntimeBrokerByName(ctx, "mybroker") + require.NoError(t, err) + assert.Equal(t, b.ID, got.ID) + + _, err = ps.GetRuntimeBrokerByName(ctx, "nonexistent") + assert.ErrorIs(t, err, store.ErrNotFound) +} + +func TestBroker_Update(t *testing.T) { + ps := newTestProjectStore(t) + ctx := context.Background() + + b := newBroker() + require.NoError(t, ps.CreateRuntimeBroker(ctx, b)) + + b.Name = "Renamed" + b.Version = "2.0.0" + b.Status = store.BrokerStatusDegraded + require.NoError(t, ps.UpdateRuntimeBroker(ctx, b)) + + got, err := ps.GetRuntimeBroker(ctx, b.ID) + require.NoError(t, err) + assert.Equal(t, "Renamed", got.Name) + assert.Equal(t, "2.0.0", got.Version) + assert.Equal(t, store.BrokerStatusDegraded, got.Status) +} + +func TestBroker_UpdateBumpsLockVersion(t *testing.T) { + ps := newTestProjectStore(t) + ctx := context.Background() + + b := newBroker() + require.NoError(t, ps.CreateRuntimeBroker(ctx, b)) + uid := uuid.MustParse(b.ID) + + before, err := ps.client.RuntimeBroker.Get(ctx, uid) + require.NoError(t, err) + + b.Status = store.BrokerStatusOffline + require.NoError(t, ps.UpdateRuntimeBroker(ctx, b)) + + after, err := ps.client.RuntimeBroker.Get(ctx, uid) + require.NoError(t, err) + assert.Equal(t, before.LockVersion+1, after.LockVersion, "update must advance the lock_version CAS token") +} + +func TestBroker_Heartbeat(t *testing.T) { + ps := newTestProjectStore(t) + ctx := context.Background() + + b := newBroker() + b.Status = store.BrokerStatusOffline + require.NoError(t, ps.CreateRuntimeBroker(ctx, b)) + uid := uuid.MustParse(b.ID) + before, err := ps.client.RuntimeBroker.Get(ctx, uid) + require.NoError(t, err) + + require.NoError(t, ps.UpdateRuntimeBrokerHeartbeat(ctx, b.ID, store.BrokerStatusOnline)) + + got, err := ps.GetRuntimeBroker(ctx, b.ID) + require.NoError(t, err) + assert.Equal(t, store.BrokerStatusOnline, got.Status) + assert.False(t, got.LastHeartbeat.IsZero()) + + after, err := ps.client.RuntimeBroker.Get(ctx, uid) + require.NoError(t, err) + assert.Equal(t, before.LockVersion+1, after.LockVersion) +} + +func TestBroker_HeartbeatNotFound(t *testing.T) { + ps := newTestProjectStore(t) + err := ps.UpdateRuntimeBrokerHeartbeat(context.Background(), uuid.NewString(), store.BrokerStatusOnline) + assert.ErrorIs(t, err, store.ErrNotFound) +} + +func TestBroker_Delete(t *testing.T) { + ps := newTestProjectStore(t) + ctx := context.Background() + + b := newBroker() + require.NoError(t, ps.CreateRuntimeBroker(ctx, b)) + require.NoError(t, ps.DeleteRuntimeBroker(ctx, b.ID)) + _, err := ps.GetRuntimeBroker(ctx, b.ID) + assert.ErrorIs(t, err, store.ErrNotFound) +} + +func TestBroker_ListFilters(t *testing.T) { + ps := newTestProjectStore(t) + ctx := context.Background() + + online := newBroker() + online.Status = store.BrokerStatusOnline + require.NoError(t, ps.CreateRuntimeBroker(ctx, online)) + + offline := newBroker() + offline.Status = store.BrokerStatusOffline + require.NoError(t, ps.CreateRuntimeBroker(ctx, offline)) + + all, err := ps.ListRuntimeBrokers(ctx, store.RuntimeBrokerFilter{}, store.ListOptions{}) + require.NoError(t, err) + assert.Equal(t, 2, all.TotalCount) + + byStatus, err := ps.ListRuntimeBrokers(ctx, store.RuntimeBrokerFilter{Status: store.BrokerStatusOnline}, store.ListOptions{}) + require.NoError(t, err) + assert.Equal(t, 1, byStatus.TotalCount) + + yes := true + autoProvide := newBroker() + autoProvide.AutoProvide = true + require.NoError(t, ps.CreateRuntimeBroker(ctx, autoProvide)) + byAuto, err := ps.ListRuntimeBrokers(ctx, store.RuntimeBrokerFilter{AutoProvide: &yes}, store.ListOptions{}) + require.NoError(t, err) + assert.Equal(t, 1, byAuto.TotalCount) +} + +// ============================================================================= +// ProjectProvider (contributors) +// ============================================================================= + +func TestProvider_UpsertAndGet(t *testing.T) { + ps := newTestProjectStore(t) + ctx := context.Background() + + projectID := uuid.NewString() + brokerID := uuid.NewString() + require.NoError(t, ps.CreateProject(ctx, &store.Project{ID: projectID, Name: "p", Slug: "p-" + projectID[:8], Visibility: store.VisibilityPrivate})) + + prov := &store.ProjectProvider{ + ProjectID: projectID, + BrokerID: brokerID, + BrokerName: "broker-a", + LocalPath: "/tmp/a", + Status: store.BrokerStatusOffline, + LinkedBy: uuid.NewString(), + } + require.NoError(t, ps.AddProjectProvider(ctx, prov)) + assert.False(t, prov.LinkedAt.IsZero(), "LinkedAt should be set when LinkedBy present") + + got, err := ps.GetProjectProvider(ctx, projectID, brokerID) + require.NoError(t, err) + assert.Equal(t, "broker-a", got.BrokerName) + assert.Equal(t, "/tmp/a", got.LocalPath) + assert.Equal(t, store.BrokerStatusOffline, got.Status) + + // Upsert (INSERT OR REPLACE): same (project, broker) updates in place. + prov2 := &store.ProjectProvider{ + ProjectID: projectID, + BrokerID: brokerID, + BrokerName: "broker-a-renamed", + LocalPath: "/tmp/b", + Status: store.BrokerStatusOnline, + } + require.NoError(t, ps.AddProjectProvider(ctx, prov2)) + + providers, err := ps.GetProjectProviders(ctx, projectID) + require.NoError(t, err) + require.Len(t, providers, 1, "upsert must not create a duplicate row") + assert.Equal(t, "broker-a-renamed", providers[0].BrokerName) + assert.Equal(t, store.BrokerStatusOnline, providers[0].Status) +} + +func TestProvider_RemoveAndStatus(t *testing.T) { + ps := newTestProjectStore(t) + ctx := context.Background() + + projectID := uuid.NewString() + brokerID := uuid.NewString() + require.NoError(t, ps.AddProjectProvider(ctx, &store.ProjectProvider{ + ProjectID: projectID, BrokerID: brokerID, BrokerName: "b", Status: store.BrokerStatusOffline, + })) + + require.NoError(t, ps.UpdateProviderStatus(ctx, projectID, brokerID, store.BrokerStatusOnline)) + got, err := ps.GetProjectProvider(ctx, projectID, brokerID) + require.NoError(t, err) + assert.Equal(t, store.BrokerStatusOnline, got.Status) + assert.False(t, got.LastSeen.IsZero()) + + require.NoError(t, ps.RemoveProjectProvider(ctx, projectID, brokerID)) + _, err = ps.GetProjectProvider(ctx, projectID, brokerID) + assert.ErrorIs(t, err, store.ErrNotFound) + + assert.ErrorIs(t, ps.RemoveProjectProvider(ctx, projectID, brokerID), store.ErrNotFound) + assert.ErrorIs(t, ps.UpdateProviderStatus(ctx, projectID, brokerID, store.BrokerStatusOnline), store.ErrNotFound) +} + +func TestProvider_GetBrokerProjects(t *testing.T) { + ps := newTestProjectStore(t) + ctx := context.Background() + + brokerID := uuid.NewString() + for i := 0; i < 2; i++ { + require.NoError(t, ps.AddProjectProvider(ctx, &store.ProjectProvider{ + ProjectID: uuid.NewString(), BrokerID: brokerID, BrokerName: "b", Status: store.BrokerStatusOnline, + })) + } + got, err := ps.GetBrokerProjects(ctx, brokerID) + require.NoError(t, err) + assert.Len(t, got, 2) +} + +func TestProject_ListByBrokerID(t *testing.T) { + ps := newTestProjectStore(t) + ctx := context.Background() + + p := newProject(1) + require.NoError(t, ps.CreateProject(ctx, p)) + brokerID := uuid.NewString() + require.NoError(t, ps.AddProjectProvider(ctx, &store.ProjectProvider{ + ProjectID: p.ID, BrokerID: brokerID, BrokerName: "b", Status: store.BrokerStatusOnline, + })) + // A second project with no contributor for this broker. + require.NoError(t, ps.CreateProject(ctx, newProject(2))) + + res, err := ps.ListProjects(ctx, store.ProjectFilter{BrokerID: brokerID}, store.ListOptions{}) + require.NoError(t, err) + assert.Equal(t, 1, res.TotalCount) + require.Len(t, res.Items, 1) + assert.Equal(t, p.ID, res.Items[0].ID) +} + +// ============================================================================= +// ProjectSyncState +// ============================================================================= + +func TestSyncState_Upsert(t *testing.T) { + ps := newTestProjectStore(t) + ctx := context.Background() + + projectID := uuid.NewString() + now := time.Now().UTC().Truncate(time.Second) + state := &store.ProjectSyncState{ + ProjectID: projectID, + BrokerID: "", // hub-native, project-wide + LastSyncTime: &now, + LastCommitSHA: "abc123", + FileCount: 10, + TotalBytes: 2048, + } + require.NoError(t, ps.UpsertProjectSyncState(ctx, state)) + + got, err := ps.GetProjectSyncState(ctx, projectID, "") + require.NoError(t, err) + assert.Equal(t, "abc123", got.LastCommitSHA) + assert.Equal(t, 10, got.FileCount) + assert.Equal(t, int64(2048), got.TotalBytes) + require.NotNil(t, got.LastSyncTime) + + // Upsert again on the same key updates in place. + state.FileCount = 20 + state.LastCommitSHA = "def456" + require.NoError(t, ps.UpsertProjectSyncState(ctx, state)) + + states, err := ps.ListProjectSyncStates(ctx, projectID) + require.NoError(t, err) + require.Len(t, states, 1) + assert.Equal(t, 20, states[0].FileCount) + assert.Equal(t, "def456", states[0].LastCommitSHA) +} + +func TestSyncState_PerBroker(t *testing.T) { + ps := newTestProjectStore(t) + ctx := context.Background() + + projectID := uuid.NewString() + brokerID := uuid.NewString() + require.NoError(t, ps.UpsertProjectSyncState(ctx, &store.ProjectSyncState{ProjectID: projectID, BrokerID: "", FileCount: 1})) + require.NoError(t, ps.UpsertProjectSyncState(ctx, &store.ProjectSyncState{ProjectID: projectID, BrokerID: brokerID, FileCount: 2})) + + states, err := ps.ListProjectSyncStates(ctx, projectID) + require.NoError(t, err) + assert.Len(t, states, 2) + + perBroker, err := ps.GetProjectSyncState(ctx, projectID, brokerID) + require.NoError(t, err) + assert.Equal(t, 2, perBroker.FileCount) +} + +func TestSyncState_DeleteAndNotFound(t *testing.T) { + ps := newTestProjectStore(t) + ctx := context.Background() + + projectID := uuid.NewString() + _, err := ps.GetProjectSyncState(ctx, projectID, "") + assert.ErrorIs(t, err, store.ErrNotFound) + + require.NoError(t, ps.UpsertProjectSyncState(ctx, &store.ProjectSyncState{ProjectID: projectID, BrokerID: "", FileCount: 1})) + require.NoError(t, ps.DeleteProjectSyncState(ctx, projectID, "")) + assert.ErrorIs(t, ps.DeleteProjectSyncState(ctx, projectID, ""), store.ErrNotFound) +} diff --git a/pkg/store/entadapter/reaper_test.go b/pkg/store/entadapter/reaper_test.go new file mode 100644 index 000000000..b388c18bf --- /dev/null +++ b/pkg/store/entadapter/reaper_test.go @@ -0,0 +1,218 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//go:build !no_sqlite + +package entadapter + +import ( + "context" + "testing" + "time" + + "github.com/GoogleCloudPlatform/scion/pkg/store" + "github.com/GoogleCloudPlatform/scion/pkg/store/enttest" + "github.com/google/uuid" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +// --------------------------------------------------------------------------- +// ReapStaleBrokerAffinity +// --------------------------------------------------------------------------- + +func TestReapStaleBrokerAffinity_ClearsStaleOnly(t *testing.T) { + ps := newTestProjectStore(t) + ctx := context.Background() + + // Broker with stale heartbeat + affinity → should be reaped. + stale := newBroker() + stale.Status = store.BrokerStatusOnline + require.NoError(t, ps.CreateRuntimeBroker(ctx, stale)) + require.NoError(t, ps.ClaimRuntimeBrokerConnection(ctx, stale.ID, "hub-old", "sess-old")) + // Backdate heartbeat to make it stale. + _, err := ps.client.RuntimeBroker.UpdateOneID(uuid.MustParse(stale.ID)). + SetLastHeartbeat(time.Now().Add(-10 * time.Minute)).Save(ctx) + require.NoError(t, err) + + // Broker with fresh heartbeat + affinity → should NOT be reaped. + fresh := newBroker() + fresh.Status = store.BrokerStatusOnline + require.NoError(t, ps.CreateRuntimeBroker(ctx, fresh)) + require.NoError(t, ps.ClaimRuntimeBrokerConnection(ctx, fresh.ID, "hub-alive", "sess-alive")) + + // Broker with no affinity (NULL connected_hub_id) → should NOT be reaped. + noAffinity := newBroker() + noAffinity.Status = store.BrokerStatusOffline + require.NoError(t, ps.CreateRuntimeBroker(ctx, noAffinity)) + + cleared, err := ps.ReapStaleBrokerAffinity(ctx, time.Now().Add(-3*time.Minute)) + require.NoError(t, err) + assert.Equal(t, 1, cleared) + + // Verify stale broker's affinity was cleared. + got, err := ps.GetRuntimeBroker(ctx, stale.ID) + require.NoError(t, err) + assert.Nil(t, got.ConnectedHubID) + assert.Nil(t, got.ConnectedSessionID) + assert.Nil(t, got.ConnectedAt) + + // Verify fresh broker's affinity is intact. + got, err = ps.GetRuntimeBroker(ctx, fresh.ID) + require.NoError(t, err) + require.NotNil(t, got.ConnectedHubID) + assert.Equal(t, "hub-alive", *got.ConnectedHubID) + + // Verify no-affinity broker is untouched. + got, err = ps.GetRuntimeBroker(ctx, noAffinity.ID) + require.NoError(t, err) + assert.Nil(t, got.ConnectedHubID) +} + +func TestReapStaleBrokerAffinity_NothingToReap(t *testing.T) { + ps := newTestProjectStore(t) + ctx := context.Background() + + b := newBroker() + require.NoError(t, ps.CreateRuntimeBroker(ctx, b)) + require.NoError(t, ps.ClaimRuntimeBrokerConnection(ctx, b.ID, "hub-1", "sess-1")) + + cleared, err := ps.ReapStaleBrokerAffinity(ctx, time.Now().Add(-10*time.Minute)) + require.NoError(t, err) + assert.Equal(t, 0, cleared) +} + +// --------------------------------------------------------------------------- +// ReapStuckDispatch +// --------------------------------------------------------------------------- + +func TestReapStuckDispatch_RedrivesBelowMax(t *testing.T) { + client := enttest.NewClient(t) + ds := NewBrokerDispatchStore(client) + ctx := context.Background() + brokerID := uuid.NewString() + + d := newDispatch(brokerID, "start") + require.NoError(t, ds.InsertBrokerDispatch(ctx, d)) + claimed, err := ds.ClaimBrokerDispatch(ctx, d.ID, "hub-1") + require.NoError(t, err) + require.True(t, claimed) + + // Backdate updated_at to make it stuck. + _, err = client.BrokerDispatch.UpdateOneID(uuid.MustParse(d.ID)). + SetUpdatedAt(time.Now().Add(-10 * time.Minute)).Save(ctx) + require.NoError(t, err) + + requeued, failed, err := ds.ReapStuckDispatch(ctx, time.Now().Add(-5*time.Minute), 3) + require.NoError(t, err) + assert.Equal(t, 1, requeued) + assert.Equal(t, 0, failed) + + got, err := ds.GetBrokerDispatch(ctx, d.ID) + require.NoError(t, err) + assert.Equal(t, store.DispatchStatePending, got.State) + assert.Equal(t, "", got.ClaimedBy) + assert.Equal(t, 1, got.Attempts) +} + +func TestReapStuckDispatch_FailsAtMaxAttempts(t *testing.T) { + client := enttest.NewClient(t) + ds := NewBrokerDispatchStore(client) + ctx := context.Background() + brokerID := uuid.NewString() + + d := newDispatch(brokerID, "stop") + d.State = store.DispatchStateInProgress + require.NoError(t, ds.InsertBrokerDispatch(ctx, d)) + + // Set attempts to maxAttempts. + _, err := client.BrokerDispatch.UpdateOneID(uuid.MustParse(d.ID)). + SetAttempts(3). + SetUpdatedAt(time.Now().Add(-10 * time.Minute)). + Save(ctx) + require.NoError(t, err) + + requeued, failed, err := ds.ReapStuckDispatch(ctx, time.Now().Add(-5*time.Minute), 3) + require.NoError(t, err) + assert.Equal(t, 0, requeued) + assert.Equal(t, 1, failed) + + got, err := ds.GetBrokerDispatch(ctx, d.ID) + require.NoError(t, err) + assert.Equal(t, store.DispatchStateFailed, got.State) + assert.Contains(t, got.Error, "max attempts exceeded") +} + +func TestReapStuckDispatch_LeavesFreshAndTerminal(t *testing.T) { + client := enttest.NewClient(t) + ds := NewBrokerDispatchStore(client) + ctx := context.Background() + brokerID := uuid.NewString() + + // Fresh in_progress dispatch (updated recently) → should NOT be reaped. + fresh := newDispatch(brokerID, "start") + require.NoError(t, ds.InsertBrokerDispatch(ctx, fresh)) + claimed, err := ds.ClaimBrokerDispatch(ctx, fresh.ID, "hub-1") + require.NoError(t, err) + require.True(t, claimed) + + // Done dispatch → should NOT be reaped. + done := newDispatch(brokerID, "stop") + require.NoError(t, ds.InsertBrokerDispatch(ctx, done)) + _, err = ds.ClaimBrokerDispatch(ctx, done.ID, "hub-1") + require.NoError(t, err) + require.NoError(t, ds.CompleteBrokerDispatch(ctx, done.ID, `{"ok":true}`)) + + // Pending dispatch → should NOT be reaped (only in_progress is targeted). + pending := newDispatch(brokerID, "restart") + require.NoError(t, ds.InsertBrokerDispatch(ctx, pending)) + + requeued, failed, err := ds.ReapStuckDispatch(ctx, time.Now().Add(-5*time.Minute), 3) + require.NoError(t, err) + assert.Equal(t, 0, requeued) + assert.Equal(t, 0, failed) + + // Verify states are unchanged. + got, _ := ds.GetBrokerDispatch(ctx, fresh.ID) + assert.Equal(t, store.DispatchStateInProgress, got.State) + + got, _ = ds.GetBrokerDispatch(ctx, done.ID) + assert.Equal(t, store.DispatchStateDone, got.State) + + got, _ = ds.GetBrokerDispatch(ctx, pending.ID) + assert.Equal(t, store.DispatchStatePending, got.State) +} + +func TestReapStuckDispatch_PastDeadline(t *testing.T) { + client := enttest.NewClient(t) + ds := NewBrokerDispatchStore(client) + ctx := context.Background() + brokerID := uuid.NewString() + + d := newDispatch(brokerID, "start") + pastDeadline := time.Now().Add(-1 * time.Minute) + d.DeadlineAt = &pastDeadline + d.State = store.DispatchStateInProgress + require.NoError(t, ds.InsertBrokerDispatch(ctx, d)) + + // updated_at is recent (within threshold), but deadline_at is past. + requeued, failed, err := ds.ReapStuckDispatch(ctx, time.Now().Add(-10*time.Minute), 3) + require.NoError(t, err) + assert.Equal(t, 1, requeued) + assert.Equal(t, 0, failed) + + got, err := ds.GetBrokerDispatch(ctx, d.ID) + require.NoError(t, err) + assert.Equal(t, store.DispatchStatePending, got.State) +} diff --git a/pkg/store/entadapter/schedule_store.go b/pkg/store/entadapter/schedule_store.go new file mode 100644 index 000000000..81287c063 --- /dev/null +++ b/pkg/store/entadapter/schedule_store.go @@ -0,0 +1,691 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package entadapter + +import ( + "context" + "time" + + "entgo.io/ent/dialect" + entsql "entgo.io/ent/dialect/sql" + "github.com/GoogleCloudPlatform/scion/pkg/ent" + "github.com/GoogleCloudPlatform/scion/pkg/ent/schedule" + "github.com/GoogleCloudPlatform/scion/pkg/ent/scheduledevent" + "github.com/GoogleCloudPlatform/scion/pkg/store" + "github.com/google/uuid" +) + +// ScheduleStore implements store.ScheduleStore and store.ScheduledEventStore +// using the Ent ORM. A single type serves both sub-interfaces, mirroring the +// SQLite backend where schedules and scheduled events share one store. +type ScheduleStore struct { + client *ent.Client +} + +// NewScheduleStore creates a new Ent-backed ScheduleStore. +func NewScheduleStore(client *ent.Client) *ScheduleStore { + return &ScheduleStore{client: client} +} + +// defaultListLimit / maxListLimit mirror the pagination bounds used by the +// SQLite backend so the two backends paginate identically. +const ( + defaultListLimit = 50 + maxListLimit = 200 +) + +func clampLimit(limit int) int { + if limit <= 0 { + return defaultListLimit + } + if limit > maxListLimit { + return maxListLimit + } + return limit +} + +// ============================================================================ +// Schedule <-> store conversion +// ============================================================================ + +func entScheduleToStore(e *ent.Schedule) *store.Schedule { + sc := &store.Schedule{ + ID: e.ID.String(), + ProjectID: e.ProjectID.String(), + Name: e.Name, + CronExpr: e.CronExpr, + EventType: e.EventType, + Payload: e.Payload, + Status: e.Status, + NextRunAt: e.NextRunAt, + LastRunAt: e.LastRunAt, + LastRunStatus: e.LastRunStatus, + LastRunError: e.LastRunError, + RunCount: e.RunCount, + ErrorCount: e.ErrorCount, + CreatedAt: e.Created, + CreatedBy: e.CreatedBy, + UpdatedAt: e.Updated, + } + return sc +} + +// ============================================================================ +// Schedule Operations (Recurring Schedules) +// ============================================================================ + +// CreateSchedule creates a new recurring schedule. +func (s *ScheduleStore) CreateSchedule(ctx context.Context, sc *store.Schedule) error { + if sc.ID == "" || sc.ProjectID == "" || sc.Name == "" || sc.CronExpr == "" { + return store.ErrInvalidInput + } + uid, err := parseUUID(sc.ID) + if err != nil { + return err + } + pid, err := parseUUID(sc.ProjectID) + if err != nil { + return err + } + if sc.Status == "" { + sc.Status = store.ScheduleStatusActive + } + + create := s.client.Schedule.Create(). + SetID(uid). + SetProjectID(pid). + SetName(sc.Name). + SetCronExpr(sc.CronExpr). + SetEventType(sc.EventType). + SetStatus(sc.Status). + SetRunCount(sc.RunCount). + SetErrorCount(sc.ErrorCount) + + if sc.Payload != "" { + create.SetPayload(sc.Payload) + } + if sc.NextRunAt != nil { + create.SetNextRunAt(*sc.NextRunAt) + } + if sc.LastRunAt != nil { + create.SetLastRunAt(*sc.LastRunAt) + } + if sc.LastRunStatus != "" { + create.SetLastRunStatus(sc.LastRunStatus) + } + if sc.LastRunError != "" { + create.SetLastRunError(sc.LastRunError) + } + if sc.CreatedBy != "" { + create.SetCreatedBy(sc.CreatedBy) + } + if !sc.CreatedAt.IsZero() { + create.SetCreated(sc.CreatedAt) + } + if !sc.UpdatedAt.IsZero() { + create.SetUpdated(sc.UpdatedAt) + } + + created, err := create.Save(ctx) + if err != nil { + return mapError(err) + } + sc.CreatedAt = created.Created + sc.UpdatedAt = created.Updated + sc.Status = created.Status + return nil +} + +// GetSchedule retrieves a schedule by ID. +func (s *ScheduleStore) GetSchedule(ctx context.Context, id string) (*store.Schedule, error) { + uid, err := parseGetID(id) + if err != nil { + return nil, err + } + e, err := s.client.Schedule.Get(ctx, uid) + if err != nil { + return nil, mapError(err) + } + return entScheduleToStore(e), nil +} + +// ListSchedules returns schedules matching the filter criteria. +func (s *ScheduleStore) ListSchedules(ctx context.Context, filter store.ScheduleFilter, opts store.ListOptions) (*store.ListResult[store.Schedule], error) { + query := s.client.Schedule.Query() + + if filter.ProjectID != "" { + pid, err := parseUUID(filter.ProjectID) + if err != nil { + return nil, err + } + query.Where(schedule.ProjectIDEQ(pid)) + } + if filter.Status != "" { + query.Where(schedule.StatusEQ(filter.Status)) + } else { + // By default, exclude deleted schedules. + query.Where(schedule.StatusNEQ(store.ScheduleStatusDeleted)) + } + if filter.Name != "" { + query.Where(schedule.NameEQ(filter.Name)) + } + + totalCount, err := query.Clone().Count(ctx) + if err != nil { + return nil, err + } + + limit := clampLimit(opts.Limit) + entities, err := query. + Order(schedule.ByCreated(entsql.OrderDesc())). + Limit(limit + 1). + All(ctx) + if err != nil { + return nil, err + } + + schedules := make([]store.Schedule, 0, len(entities)) + for _, e := range entities { + schedules = append(schedules, *entScheduleToStore(e)) + } + + result := &store.ListResult[store.Schedule]{TotalCount: totalCount} + if len(schedules) > limit { + result.Items = schedules[:limit] + result.NextCursor = schedules[limit-1].ID + } else { + result.Items = schedules + } + return result, nil +} + +// UpdateSchedule updates an existing schedule. +func (s *ScheduleStore) UpdateSchedule(ctx context.Context, sc *store.Schedule) error { + uid, err := parseUUID(sc.ID) + if err != nil { + return err + } + + update := s.client.Schedule.UpdateOneID(uid). + SetName(sc.Name). + SetCronExpr(sc.CronExpr). + SetEventType(sc.EventType). + SetPayload(sc.Payload). + SetStatus(sc.Status) + + if sc.NextRunAt != nil { + update.SetNextRunAt(*sc.NextRunAt) + } else { + update.ClearNextRunAt() + } + + updated, err := update.Save(ctx) + if err != nil { + return mapError(err) + } + sc.UpdatedAt = updated.Updated + return nil +} + +// UpdateScheduleStatus updates only the status of a schedule. +func (s *ScheduleStore) UpdateScheduleStatus(ctx context.Context, id string, status string) error { + uid, err := parseUUID(id) + if err != nil { + return err + } + err = s.client.Schedule.UpdateOneID(uid).SetStatus(status).Exec(ctx) + if err != nil { + return mapError(err) + } + return nil +} + +// UpdateScheduleAfterRun updates a schedule after a run completes. It performs a +// read-modify-write of the run/error counters atomically via Ent's Add* setters. +func (s *ScheduleStore) UpdateScheduleAfterRun(ctx context.Context, id string, ranAt time.Time, nextRunAt time.Time, errMsg string) error { + uid, err := parseUUID(id) + if err != nil { + return err + } + + update := s.client.Schedule.UpdateOneID(uid). + SetLastRunAt(ranAt). + SetNextRunAt(nextRunAt). + AddRunCount(1) + + if errMsg != "" { + update. + SetLastRunStatus(store.ScheduleRunError). + SetLastRunError(errMsg). + AddErrorCount(1) + } else { + update. + SetLastRunStatus(store.ScheduleRunSuccess). + ClearLastRunError() + } + + if err := update.Exec(ctx); err != nil { + return mapError(err) + } + return nil +} + +// DeleteSchedule removes a schedule by ID (hard delete). +func (s *ScheduleStore) DeleteSchedule(ctx context.Context, id string) error { + uid, err := parseUUID(id) + if err != nil { + return err + } + if err := s.client.Schedule.DeleteOneID(uid).Exec(ctx); err != nil { + return mapError(err) + } + return nil +} + +// ListDueSchedules returns active schedules whose next_run_at has passed. +// +// This is a JOB-CLAIM PATH (§2.A.3): multiple hub replicas poll it concurrently. +// The dialect-aware claim helper applies SELECT ... FOR UPDATE SKIP LOCKED on +// Postgres so two replicas never pick up the same schedule, and falls back to a +// plain SELECT on SQLite (single writer, no SKIP LOCKED support). +func (s *ScheduleStore) ListDueSchedules(ctx context.Context, now time.Time) ([]store.Schedule, error) { + ids, err := s.skipLockedIDs(ctx, schedule.Table, func(sel *entsql.Selector) { + sel.Where(entsql.And( + entsql.EQ(schedule.FieldStatus, store.ScheduleStatusActive), + entsql.NotNull(schedule.FieldNextRunAt), + entsql.LTE(schedule.FieldNextRunAt, now), + )).OrderBy(entsql.Asc(schedule.FieldNextRunAt)) + }) + if err != nil { + return nil, err + } + if len(ids) == 0 { + return nil, nil + } + + entities, err := s.client.Schedule.Query(). + Where(schedule.IDIn(ids...)). + Order(schedule.ByNextRunAt()). + All(ctx) + if err != nil { + return nil, err + } + out := make([]store.Schedule, 0, len(entities)) + for _, e := range entities { + out = append(out, *entScheduleToStore(e)) + } + return out, nil +} + +// ============================================================================ +// ScheduledEvent <-> store conversion +// ============================================================================ + +func entScheduledEventToStore(e *ent.ScheduledEvent) *store.ScheduledEvent { + return &store.ScheduledEvent{ + ID: e.ID.String(), + ProjectID: e.ProjectID.String(), + EventType: e.EventType, + FireAt: e.FireAt, + Payload: e.Payload, + Status: e.Status, + CreatedAt: e.Created, + CreatedBy: e.CreatedBy, + FiredAt: e.FiredAt, + Error: e.Error, + ScheduleID: e.ScheduleID, + } +} + +// ============================================================================ +// Scheduled Event Operations +// ============================================================================ + +// CreateScheduledEvent creates a new scheduled event. +func (s *ScheduleStore) CreateScheduledEvent(ctx context.Context, event *store.ScheduledEvent) error { + if event.ID == "" || event.ProjectID == "" || event.EventType == "" { + return store.ErrInvalidInput + } + uid, err := parseUUID(event.ID) + if err != nil { + return err + } + pid, err := parseUUID(event.ProjectID) + if err != nil { + return err + } + if event.Status == "" { + event.Status = store.ScheduledEventPending + } + + create := s.client.ScheduledEvent.Create(). + SetID(uid). + SetProjectID(pid). + SetEventType(event.EventType). + SetFireAt(event.FireAt). + SetPayload(event.Payload). + SetStatus(event.Status) + + if event.CreatedBy != "" { + create.SetCreatedBy(event.CreatedBy) + } + if event.FiredAt != nil { + create.SetFiredAt(*event.FiredAt) + } + if event.Error != "" { + create.SetError(event.Error) + } + if event.ScheduleID != "" { + create.SetScheduleID(event.ScheduleID) + } + if !event.CreatedAt.IsZero() { + create.SetCreated(event.CreatedAt) + } + + created, err := create.Save(ctx) + if err != nil { + return mapError(err) + } + event.CreatedAt = created.Created + event.Status = created.Status + return nil +} + +// GetScheduledEvent retrieves a scheduled event by ID. +func (s *ScheduleStore) GetScheduledEvent(ctx context.Context, id string) (*store.ScheduledEvent, error) { + uid, err := parseGetID(id) + if err != nil { + return nil, err + } + e, err := s.client.ScheduledEvent.Get(ctx, uid) + if err != nil { + return nil, mapError(err) + } + return entScheduledEventToStore(e), nil +} + +// ListPendingScheduledEvents returns all events with status "pending", +// ordered by fire_at ASC. +// +// Like ListDueSchedules this is a JOB-CLAIM PATH and uses the dialect-aware +// SKIP LOCKED helper for safe multi-replica polling. +func (s *ScheduleStore) ListPendingScheduledEvents(ctx context.Context) ([]store.ScheduledEvent, error) { + ids, err := s.skipLockedIDs(ctx, scheduledevent.Table, func(sel *entsql.Selector) { + sel.Where(entsql.EQ(scheduledevent.FieldStatus, store.ScheduledEventPending)). + OrderBy(entsql.Asc(scheduledevent.FieldFireAt)) + }) + if err != nil { + return nil, err + } + if len(ids) == 0 { + return nil, nil + } + + entities, err := s.client.ScheduledEvent.Query(). + Where(scheduledevent.IDIn(ids...)). + Order(scheduledevent.ByFireAt()). + All(ctx) + if err != nil { + return nil, err + } + out := make([]store.ScheduledEvent, 0, len(entities)) + for _, e := range entities { + out = append(out, *entScheduledEventToStore(e)) + } + return out, nil +} + +// UpdateScheduledEventStatus updates the status and optional error for an event. +// Mirroring the SQLite backend, a missing event is a no-op (not ErrNotFound). +func (s *ScheduleStore) UpdateScheduledEventStatus(ctx context.Context, id string, status string, firedAt *time.Time, errMsg string) error { + uid, err := parseUUID(id) + if err != nil { + return err + } + + update := s.client.ScheduledEvent.Update(). + Where(scheduledevent.IDEQ(uid)). + SetStatus(status) + + if firedAt != nil { + update.SetFiredAt(*firedAt) + } else { + update.ClearFiredAt() + } + if errMsg != "" { + update.SetError(errMsg) + } else { + update.ClearError() + } + + _, err = update.Save(ctx) + return err +} + +// ClaimScheduledEvent atomically transitions a scheduled event from "pending" to +// claimedStatus, returning whether this caller won the claim. It is the +// multi-replica dedup primitive (store.ScheduledEventClaimer): several hub +// replicas may each recover the same pending event from the database on startup +// and arm an in-memory timer for it, but the conditional +// UPDATE ... WHERE status = 'pending' is atomic, so exactly one replica observes +// affected == 1 and is allowed to execute the event's side effect. Losers +// observe affected == 0 and skip execution. +// +// The same atomicity holds on SQLite (a conditional UPDATE is atomic there too); +// it is simply never contended because there is a single writer. +func (s *ScheduleStore) ClaimScheduledEvent(ctx context.Context, id string, claimedStatus string) (bool, error) { + uid, err := parseUUID(id) + if err != nil { + return false, err + } + if claimedStatus == "" { + claimedStatus = store.ScheduledEventFired + } + affected, err := s.client.ScheduledEvent.Update(). + Where( + scheduledevent.IDEQ(uid), + scheduledevent.StatusEQ(store.ScheduledEventPending), + ). + SetStatus(claimedStatus). + SetFiredAt(time.Now()). + Save(ctx) + if err != nil { + return false, mapError(err) + } + return affected == 1, nil +} + +// CancelScheduledEvent marks a pending event as cancelled. Returns ErrNotFound +// if the event doesn't exist or is not pending. +func (s *ScheduleStore) CancelScheduledEvent(ctx context.Context, id string) error { + uid, err := parseUUID(id) + if err != nil { + return err + } + n, err := s.client.ScheduledEvent.Update(). + Where( + scheduledevent.IDEQ(uid), + scheduledevent.StatusEQ(store.ScheduledEventPending), + ). + SetStatus(store.ScheduledEventCancelled). + Save(ctx) + if err != nil { + return err + } + if n == 0 { + return store.ErrNotFound + } + return nil +} + +// ListScheduledEvents returns events matching the filter criteria. +func (s *ScheduleStore) ListScheduledEvents(ctx context.Context, filter store.ScheduledEventFilter, opts store.ListOptions) (*store.ListResult[store.ScheduledEvent], error) { + query := s.client.ScheduledEvent.Query() + + if filter.ProjectID != "" { + pid, err := parseUUID(filter.ProjectID) + if err != nil { + return nil, err + } + query.Where(scheduledevent.ProjectIDEQ(pid)) + } + if filter.EventType != "" { + query.Where(scheduledevent.EventTypeEQ(filter.EventType)) + } + if filter.Status != "" { + query.Where(scheduledevent.StatusEQ(filter.Status)) + } + if filter.ScheduleID != "" { + query.Where(scheduledevent.ScheduleIDEQ(filter.ScheduleID)) + } + + totalCount, err := query.Clone().Count(ctx) + if err != nil { + return nil, err + } + + if opts.Cursor != "" { + cursorUID, err := parseUUID(opts.Cursor) + if err != nil { + return nil, err + } + query.Where(scheduledevent.IDLT(cursorUID)) + } + + limit := clampLimit(opts.Limit) + entities, err := query. + Order(scheduledevent.ByCreated(entsql.OrderDesc())). + Limit(limit + 1). + All(ctx) + if err != nil { + return nil, err + } + + events := make([]store.ScheduledEvent, 0, len(entities)) + for _, e := range entities { + events = append(events, *entScheduledEventToStore(e)) + } + + result := &store.ListResult[store.ScheduledEvent]{TotalCount: totalCount} + if len(events) > limit { + result.Items = events[:limit] + result.NextCursor = events[limit-1].ID + } else { + result.Items = events + } + return result, nil +} + +// PurgeOldScheduledEvents removes non-pending events older than cutoff. +func (s *ScheduleStore) PurgeOldScheduledEvents(ctx context.Context, cutoff time.Time) (int, error) { + n, err := s.client.ScheduledEvent.Delete(). + Where( + scheduledevent.StatusNEQ(store.ScheduledEventPending), + scheduledevent.CreatedLT(cutoff), + ). + Exec(ctx) + if err != nil { + return 0, err + } + return n, nil +} + +// ============================================================================ +// Dialect-aware claim helper +// ============================================================================ + +// skipLockedIDs runs the claim SELECT described by apply and returns the ids of +// the matching rows. On Postgres it issues `SELECT id ... FOR UPDATE SKIP +// LOCKED` inside a short transaction so concurrent replicas receive disjoint +// row sets; the caller then transitions the claimed rows to their next state. +// On SQLite (and any non-Postgres dialect) it degrades to a plain `SELECT id`, +// which is correct for the single-writer backend that has no SKIP LOCKED +// support. +func (s *ScheduleStore) skipLockedIDs(ctx context.Context, table string, apply func(*entsql.Selector)) ([]uuid.UUID, error) { + drv := s.client.Driver() + d := drv.Dialect() + + builder := entsql.Dialect(d) + selector := builder.Select(genericIDColumn).From(builder.Table(table)) + apply(selector) + if d == dialect.Postgres { + selector.ForUpdate(entsql.WithLockAction(entsql.SkipLocked)) + } + + query, args := selector.Query() + + if d == dialect.Postgres { + return s.queryIDsTx(ctx, drv, query, args) + } + return s.queryIDs(ctx, drv, query, args) +} + +// genericIDColumn is the primary-key column shared by all ported tables. +const genericIDColumn = "id" + +// queryIDs runs the claim SELECT directly (no surrounding transaction). Used for +// dialects that do not support row-level locking. +func (s *ScheduleStore) queryIDs(ctx context.Context, drv interface { + Query(context.Context, string, any, any) error +}, query string, args []any) ([]uuid.UUID, error) { + rows := &entsql.Rows{} + if err := drv.Query(ctx, query, args, rows); err != nil { + return nil, err + } + defer rows.Close() + return scanUUIDRows(rows) +} + +// queryIDsTx runs the SKIP LOCKED claim SELECT within a transaction so the row +// locks are held while the disjoint id set is materialized. +func (s *ScheduleStore) queryIDsTx(ctx context.Context, drv interface { + Tx(context.Context) (dialect.Tx, error) +}, query string, args []any) ([]uuid.UUID, error) { + tx, err := drv.Tx(ctx) + if err != nil { + return nil, err + } + rows := &entsql.Rows{} + if err := tx.Query(ctx, query, args, rows); err != nil { + _ = tx.Rollback() + return nil, err + } + ids, scanErr := scanUUIDRows(rows) + _ = rows.Close() + if scanErr != nil { + _ = tx.Rollback() + return nil, scanErr + } + if err := tx.Commit(); err != nil { + return nil, err + } + return ids, nil +} + +// scanUUIDRows scans a single-column id result set into a slice of UUIDs. +func scanUUIDRows(rows *entsql.Rows) ([]uuid.UUID, error) { + var ids []uuid.UUID + for rows.Next() { + var id uuid.UUID + if err := rows.Scan(&id); err != nil { + return nil, err + } + ids = append(ids, id) + } + if err := rows.Err(); err != nil { + return nil, err + } + return ids, nil +} diff --git a/pkg/store/entadapter/schedule_store_test.go b/pkg/store/entadapter/schedule_store_test.go new file mode 100644 index 000000000..a197f6d86 --- /dev/null +++ b/pkg/store/entadapter/schedule_store_test.go @@ -0,0 +1,259 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//go:build !no_sqlite + +package entadapter + +import ( + "context" + "sync" + "testing" + "time" + + "entgo.io/ent/dialect" + "github.com/GoogleCloudPlatform/scion/pkg/store" + "github.com/GoogleCloudPlatform/scion/pkg/store/enttest" + "github.com/google/uuid" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func newTestScheduleStore(t *testing.T) *ScheduleStore { + t.Helper() + client := enttest.NewClient(t) + return NewScheduleStore(client) +} + +func newTestSchedule(projectID string, name string) *store.Schedule { + next := time.Now().Add(time.Hour).UTC().Truncate(time.Second) + return &store.Schedule{ + ID: uuid.NewString(), + ProjectID: projectID, + Name: name, + CronExpr: "0 9 * * 1-5", + EventType: "message", + Payload: `{"message":"standup"}`, + NextRunAt: &next, + CreatedBy: "user-123", + } +} + +func TestScheduleCRUD(t *testing.T) { + s := newTestScheduleStore(t) + ctx := context.Background() + projectID := uuid.NewString() + + sc := newTestSchedule(projectID, "daily-standup") + require.NoError(t, s.CreateSchedule(ctx, sc)) + assert.False(t, sc.CreatedAt.IsZero()) + assert.Equal(t, store.ScheduleStatusActive, sc.Status) + + got, err := s.GetSchedule(ctx, sc.ID) + require.NoError(t, err) + assert.Equal(t, sc.ID, got.ID) + assert.Equal(t, projectID, got.ProjectID) + assert.Equal(t, "daily-standup", got.Name) + assert.Equal(t, "0 9 * * 1-5", got.CronExpr) + assert.Equal(t, "message", got.EventType) + assert.Equal(t, store.ScheduleStatusActive, got.Status) + assert.Equal(t, "user-123", got.CreatedBy) + require.NotNil(t, got.NextRunAt) + + // Update + got.Name = "weekly-standup" + got.CronExpr = "0 9 * * 1" + require.NoError(t, s.UpdateSchedule(ctx, got)) + updated, err := s.GetSchedule(ctx, sc.ID) + require.NoError(t, err) + assert.Equal(t, "weekly-standup", updated.Name) + assert.Equal(t, "0 9 * * 1", updated.CronExpr) + + // Status + require.NoError(t, s.UpdateScheduleStatus(ctx, sc.ID, store.ScheduleStatusPaused)) + paused, err := s.GetSchedule(ctx, sc.ID) + require.NoError(t, err) + assert.Equal(t, store.ScheduleStatusPaused, paused.Status) + + // Delete + require.NoError(t, s.DeleteSchedule(ctx, sc.ID)) + _, err = s.GetSchedule(ctx, sc.ID) + assert.ErrorIs(t, err, store.ErrNotFound) +} + +func TestScheduleGetNotFound(t *testing.T) { + s := newTestScheduleStore(t) + _, err := s.GetSchedule(context.Background(), uuid.NewString()) + assert.ErrorIs(t, err, store.ErrNotFound) +} + +func TestScheduleInvalidInput(t *testing.T) { + s := newTestScheduleStore(t) + err := s.CreateSchedule(context.Background(), &store.Schedule{ID: uuid.NewString()}) + assert.ErrorIs(t, err, store.ErrInvalidInput) +} + +func TestScheduleUpdateNotFound(t *testing.T) { + s := newTestScheduleStore(t) + sc := newTestSchedule(uuid.NewString(), "ghost") + err := s.UpdateSchedule(context.Background(), sc) + assert.ErrorIs(t, err, store.ErrNotFound) +} + +func TestUpdateScheduleAfterRun(t *testing.T) { + s := newTestScheduleStore(t) + ctx := context.Background() + sc := newTestSchedule(uuid.NewString(), "runner") + require.NoError(t, s.CreateSchedule(ctx, sc)) + + ranAt := time.Now().UTC().Truncate(time.Second) + nextRun := ranAt.Add(time.Hour) + + // Success run increments run_count, clears error. + require.NoError(t, s.UpdateScheduleAfterRun(ctx, sc.ID, ranAt, nextRun, "")) + got, err := s.GetSchedule(ctx, sc.ID) + require.NoError(t, err) + assert.Equal(t, 1, got.RunCount) + assert.Equal(t, 0, got.ErrorCount) + assert.Equal(t, store.ScheduleRunSuccess, got.LastRunStatus) + assert.Empty(t, got.LastRunError) + + // Error run increments both counters and records the error. + require.NoError(t, s.UpdateScheduleAfterRun(ctx, sc.ID, ranAt, nextRun, "boom")) + got, err = s.GetSchedule(ctx, sc.ID) + require.NoError(t, err) + assert.Equal(t, 2, got.RunCount) + assert.Equal(t, 1, got.ErrorCount) + assert.Equal(t, store.ScheduleRunError, got.LastRunStatus) + assert.Equal(t, "boom", got.LastRunError) +} + +func TestListSchedulesFilterAndPagination(t *testing.T) { + s := newTestScheduleStore(t) + ctx := context.Background() + projectID := uuid.NewString() + other := uuid.NewString() + + for i := 0; i < 3; i++ { + require.NoError(t, s.CreateSchedule(ctx, newTestSchedule(projectID, "sched-"+uuid.NewString()[:8]))) + } + require.NoError(t, s.CreateSchedule(ctx, newTestSchedule(other, "other"))) + + // Filter by project. + res, err := s.ListSchedules(ctx, store.ScheduleFilter{ProjectID: projectID}, store.ListOptions{}) + require.NoError(t, err) + assert.Equal(t, 3, res.TotalCount) + assert.Len(t, res.Items, 3) + + // Pagination: limit honored, total independent of limit. + page, err := s.ListSchedules(ctx, store.ScheduleFilter{ProjectID: projectID}, store.ListOptions{Limit: 2}) + require.NoError(t, err) + assert.Len(t, page.Items, 2) + assert.Equal(t, 3, page.TotalCount) + assert.NotEmpty(t, page.NextCursor) +} + +func TestListSchedulesExcludesDeletedByDefault(t *testing.T) { + s := newTestScheduleStore(t) + ctx := context.Background() + projectID := uuid.NewString() + + active := newTestSchedule(projectID, "active") + require.NoError(t, s.CreateSchedule(ctx, active)) + deleted := newTestSchedule(projectID, "deleted") + require.NoError(t, s.CreateSchedule(ctx, deleted)) + require.NoError(t, s.UpdateScheduleStatus(ctx, deleted.ID, store.ScheduleStatusDeleted)) + + res, err := s.ListSchedules(ctx, store.ScheduleFilter{ProjectID: projectID}, store.ListOptions{}) + require.NoError(t, err) + assert.Equal(t, 1, res.TotalCount) + assert.Equal(t, active.ID, res.Items[0].ID) +} + +// TestListDueSchedulesClaimPath exercises the dialect-aware SKIP LOCKED claim +// helper. On SQLite it degrades to a plain SELECT, so this verifies the +// functional contract of the claim query: only active, already-due schedules +// are returned, ordered by next_run_at ascending. The concurrency sub-test +// hammers the helper from multiple goroutines to flush out races in the +// claim/transaction handling. +func TestListDueSchedulesClaimPath(t *testing.T) { + s := newTestScheduleStore(t) + ctx := context.Background() + projectID := uuid.NewString() + now := time.Now().UTC().Truncate(time.Second) + + // Two due schedules (past next_run_at), at distinct times for ordering. + dueEarly := newTestSchedule(projectID, "due-early") + early := now.Add(-2 * time.Hour) + dueEarly.NextRunAt = &early + require.NoError(t, s.CreateSchedule(ctx, dueEarly)) + + dueLate := newTestSchedule(projectID, "due-late") + late := now.Add(-1 * time.Hour) + dueLate.NextRunAt = &late + require.NoError(t, s.CreateSchedule(ctx, dueLate)) + + // Not due (future next_run_at). + future := newTestSchedule(projectID, "future") + require.NoError(t, s.CreateSchedule(ctx, future)) + + // Due but paused — must be excluded (status != active). + paused := newTestSchedule(projectID, "paused") + paused.NextRunAt = &early + require.NoError(t, s.CreateSchedule(ctx, paused)) + require.NoError(t, s.UpdateScheduleStatus(ctx, paused.ID, store.ScheduleStatusPaused)) + + due, err := s.ListDueSchedules(ctx, now) + require.NoError(t, err) + require.Len(t, due, 2, "only the two active, due schedules should be claimed") + assert.Equal(t, dueEarly.ID, due[0].ID, "ordered by next_run_at ascending") + assert.Equal(t, dueLate.ID, due[1].ID) + + // Concurrent claims must not race or error. The expected per-call count is + // backend-dependent: + // - SQLite has no SELECT ... FOR UPDATE SKIP LOCKED, and the test store + // pins MaxOpenConns=1, so the claim path serializes and every caller + // observes both due schedules. + // - Postgres uses FOR UPDATE SKIP LOCKED inside a transaction that holds + // the row locks until commit, so a concurrent caller skips rows locked + // by a sibling and may observe a disjoint subset (0..2). The cross-call + // invariant is only that no caller errors or observes more than the two + // due rows. + isPostgres := s.client.Driver().Dialect() == dialect.Postgres + var wg sync.WaitGroup + errs := make(chan error, 8) + for i := 0; i < 8; i++ { + wg.Add(1) + go func() { + defer wg.Done() + res, err := s.ListDueSchedules(ctx, now) + if err != nil { + errs <- err + return + } + if isPostgres { + if len(res) > 2 { + errs <- assert.AnError + } + } else if len(res) != 2 { + errs <- assert.AnError + } + }() + } + wg.Wait() + close(errs) + for err := range errs { + require.NoError(t, err) + } +} diff --git a/pkg/store/entadapter/scheduled_event_store_test.go b/pkg/store/entadapter/scheduled_event_store_test.go new file mode 100644 index 000000000..681862fbf --- /dev/null +++ b/pkg/store/entadapter/scheduled_event_store_test.go @@ -0,0 +1,171 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//go:build !no_sqlite + +package entadapter + +import ( + "context" + "testing" + "time" + + "github.com/GoogleCloudPlatform/scion/pkg/store" + "github.com/google/uuid" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func newTestScheduledEvent(projectID string) *store.ScheduledEvent { + fireAt := time.Now().Add(time.Hour).UTC().Truncate(time.Second) + return &store.ScheduledEvent{ + ID: uuid.NewString(), + ProjectID: projectID, + EventType: "message", + FireAt: fireAt, + Payload: `{"text":"hello"}`, + CreatedBy: "user-123", + } +} + +func TestScheduledEventCRUD(t *testing.T) { + s := newTestScheduleStore(t) + ctx := context.Background() + projectID := uuid.NewString() + + evt := newTestScheduledEvent(projectID) + require.NoError(t, s.CreateScheduledEvent(ctx, evt)) + assert.False(t, evt.CreatedAt.IsZero()) + assert.Equal(t, store.ScheduledEventPending, evt.Status) + + got, err := s.GetScheduledEvent(ctx, evt.ID) + require.NoError(t, err) + assert.Equal(t, evt.ID, got.ID) + assert.Equal(t, projectID, got.ProjectID) + assert.Equal(t, "message", got.EventType) + assert.Equal(t, store.ScheduledEventPending, got.Status) + assert.Equal(t, "user-123", got.CreatedBy) + + // Update status -> fired. + firedAt := time.Now().UTC().Truncate(time.Second) + require.NoError(t, s.UpdateScheduledEventStatus(ctx, evt.ID, store.ScheduledEventFired, &firedAt, "")) + got, err = s.GetScheduledEvent(ctx, evt.ID) + require.NoError(t, err) + assert.Equal(t, store.ScheduledEventFired, got.Status) + require.NotNil(t, got.FiredAt) +} + +func TestScheduledEventGetNotFound(t *testing.T) { + s := newTestScheduleStore(t) + _, err := s.GetScheduledEvent(context.Background(), uuid.NewString()) + assert.ErrorIs(t, err, store.ErrNotFound) +} + +func TestScheduledEventInvalidInput(t *testing.T) { + s := newTestScheduleStore(t) + err := s.CreateScheduledEvent(context.Background(), &store.ScheduledEvent{ID: uuid.NewString()}) + assert.ErrorIs(t, err, store.ErrInvalidInput) +} + +func TestCancelScheduledEvent(t *testing.T) { + s := newTestScheduleStore(t) + ctx := context.Background() + evt := newTestScheduledEvent(uuid.NewString()) + require.NoError(t, s.CreateScheduledEvent(ctx, evt)) + + require.NoError(t, s.CancelScheduledEvent(ctx, evt.ID)) + got, err := s.GetScheduledEvent(ctx, evt.ID) + require.NoError(t, err) + assert.Equal(t, store.ScheduledEventCancelled, got.Status) + + // Cancelling a non-pending event is ErrNotFound. + err = s.CancelScheduledEvent(ctx, evt.ID) + assert.ErrorIs(t, err, store.ErrNotFound) +} + +func TestListScheduledEventsFilter(t *testing.T) { + s := newTestScheduleStore(t) + ctx := context.Background() + projectID := uuid.NewString() + + for i := 0; i < 3; i++ { + require.NoError(t, s.CreateScheduledEvent(ctx, newTestScheduledEvent(projectID))) + } + require.NoError(t, s.CreateScheduledEvent(ctx, newTestScheduledEvent(uuid.NewString()))) + + res, err := s.ListScheduledEvents(ctx, store.ScheduledEventFilter{ProjectID: projectID}, store.ListOptions{}) + require.NoError(t, err) + assert.Equal(t, 3, res.TotalCount) + assert.Len(t, res.Items, 3) + + res, err = s.ListScheduledEvents(ctx, store.ScheduledEventFilter{Status: store.ScheduledEventPending}, store.ListOptions{}) + require.NoError(t, err) + assert.Equal(t, 4, res.TotalCount) +} + +func TestPurgeOldScheduledEvents(t *testing.T) { + s := newTestScheduleStore(t) + ctx := context.Background() + projectID := uuid.NewString() + + // Old fired event (should be purged). + old := newTestScheduledEvent(projectID) + old.CreatedAt = time.Now().Add(-48 * time.Hour).UTC().Truncate(time.Second) + require.NoError(t, s.CreateScheduledEvent(ctx, old)) + require.NoError(t, s.UpdateScheduledEventStatus(ctx, old.ID, store.ScheduledEventFired, nil, "")) + + // Old pending event (should NOT be purged — still pending). + oldPending := newTestScheduledEvent(projectID) + oldPending.CreatedAt = time.Now().Add(-48 * time.Hour).UTC().Truncate(time.Second) + require.NoError(t, s.CreateScheduledEvent(ctx, oldPending)) + + cutoff := time.Now().Add(-24 * time.Hour) + n, err := s.PurgeOldScheduledEvents(ctx, cutoff) + require.NoError(t, err) + assert.Equal(t, 1, n) + + _, err = s.GetScheduledEvent(ctx, old.ID) + assert.ErrorIs(t, err, store.ErrNotFound) + _, err = s.GetScheduledEvent(ctx, oldPending.ID) + require.NoError(t, err) +} + +// TestListPendingScheduledEventsClaimPath verifies the scheduled-event job-claim +// path (dialect-aware SKIP LOCKED helper): only pending events are returned, +// ordered by fire_at ascending. +func TestListPendingScheduledEventsClaimPath(t *testing.T) { + s := newTestScheduleStore(t) + ctx := context.Background() + projectID := uuid.NewString() + now := time.Now().UTC().Truncate(time.Second) + + early := newTestScheduledEvent(projectID) + early.FireAt = now.Add(1 * time.Minute) + require.NoError(t, s.CreateScheduledEvent(ctx, early)) + + late := newTestScheduledEvent(projectID) + late.FireAt = now.Add(10 * time.Minute) + require.NoError(t, s.CreateScheduledEvent(ctx, late)) + + // A fired event must be excluded from the pending claim. + fired := newTestScheduledEvent(projectID) + require.NoError(t, s.CreateScheduledEvent(ctx, fired)) + require.NoError(t, s.UpdateScheduledEventStatus(ctx, fired.ID, store.ScheduledEventFired, &now, "")) + + pending, err := s.ListPendingScheduledEvents(ctx) + require.NoError(t, err) + require.Len(t, pending, 2) + assert.Equal(t, early.ID, pending[0].ID, "ordered by fire_at ascending") + assert.Equal(t, late.ID, pending[1].ID) +} diff --git a/pkg/store/entadapter/secret_store.go b/pkg/store/entadapter/secret_store.go new file mode 100644 index 000000000..fff4aaf92 --- /dev/null +++ b/pkg/store/entadapter/secret_store.go @@ -0,0 +1,528 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package entadapter + +import ( + "context" + "errors" + "time" + + "github.com/GoogleCloudPlatform/scion/pkg/ent" + entenvvar "github.com/GoogleCloudPlatform/scion/pkg/ent/envvar" + entsecret "github.com/GoogleCloudPlatform/scion/pkg/ent/secret" + "github.com/GoogleCloudPlatform/scion/pkg/store" +) + +// SecretStore implements store.SecretStore and store.EnvVarStore using Ent ORM. +// +// Secrets and env vars are polymorphically scoped to different entity types +// (hub/user/project/runtime_broker) via the (scope, scope_id) pair, so there +// are no FK edges; lookups are keyed by the (key, scope, scope_id) triple, +// mirroring the legacy SQLite implementation. +type SecretStore struct { + client *ent.Client +} + +// NewSecretStore creates a new Ent-backed SecretStore. +func NewSecretStore(client *ent.Client) *SecretStore { + return &SecretStore{client: client} +} + +// ============================================================================= +// Secret operations +// ============================================================================= + +// entSecretRowToStore converts an Ent Secret entity to a store.Secret model. +// When includeValue is false the EncryptedValue is left empty, matching the +// SQLite listing queries that never select the encrypted payload. +func entSecretRowToStore(e *ent.Secret, includeValue bool) store.Secret { + s := store.Secret{ + ID: e.ID.String(), + Key: e.Key, + SecretRef: e.SecretRef, + SecretType: string(e.SecretType), + Target: e.Target, + Scope: e.Scope, + ScopeID: e.ScopeID, + Description: e.Description, + InjectionMode: string(e.InjectionMode), + AllowProgeny: e.AllowProgeny, + Version: e.Version, + Created: e.Created, + Updated: e.Updated, + CreatedBy: e.CreatedBy, + UpdatedBy: e.UpdatedBy, + } + // Mirror SQLite's COALESCE(target, key): an unset target projects to the key. + if s.Target == "" { + s.Target = s.Key + } + if includeValue { + s.EncryptedValue = e.EncryptedValue + } + return s +} + +// CreateSecret creates a new secret. +func (s *SecretStore) CreateSecret(ctx context.Context, secret *store.Secret) error { + uid, err := parseUUID(secret.ID) + if err != nil { + return err + } + + now := time.Now() + secret.Created = now + secret.Updated = now + secret.Version = 1 + + if secret.SecretType == "" { + secret.SecretType = store.SecretTypeEnvironment + } + if secret.Target == "" { + secret.Target = secret.Key + } + if secret.InjectionMode == "" { + secret.InjectionMode = store.InjectionModeAsNeeded + } + + create := s.client.Secret.Create(). + SetID(uid). + SetKey(secret.Key). + SetEncryptedValue(secret.EncryptedValue). + SetSecretType(entsecret.SecretType(secret.SecretType)). + SetTarget(secret.Target). + SetScope(secret.Scope). + SetScopeID(secret.ScopeID). + SetInjectionMode(entsecret.InjectionMode(secret.InjectionMode)). + SetAllowProgeny(secret.AllowProgeny). + SetVersion(secret.Version). + SetCreated(secret.Created). + SetUpdated(secret.Updated) + + if secret.SecretRef != "" { + create.SetSecretRef(secret.SecretRef) + } + if secret.Description != "" { + create.SetDescription(secret.Description) + } + if secret.CreatedBy != "" { + create.SetCreatedBy(secret.CreatedBy) + } + if secret.UpdatedBy != "" { + create.SetUpdatedBy(secret.UpdatedBy) + } + + if _, err := create.Save(ctx); err != nil { + return mapError(err) + } + return nil +} + +// GetSecret retrieves secret metadata (including the encrypted value) by key, +// scope, and scopeID. +func (s *SecretStore) GetSecret(ctx context.Context, key, scope, scopeID string) (*store.Secret, error) { + e, err := s.client.Secret.Query(). + Where( + entsecret.KeyEQ(key), + entsecret.ScopeEQ(scope), + entsecret.ScopeIDEQ(scopeID), + ). + Only(ctx) + if err != nil { + return nil, mapError(err) + } + sec := entSecretRowToStore(e, true) + return &sec, nil +} + +// UpdateSecret updates an existing secret, incrementing its version. +func (s *SecretStore) UpdateSecret(ctx context.Context, secret *store.Secret) error { + secret.Updated = time.Now() + secret.Version++ // Increment version on each update + + if secret.SecretType == "" { + secret.SecretType = store.SecretTypeEnvironment + } + if secret.Target == "" { + secret.Target = secret.Key + } + if secret.InjectionMode == "" { + secret.InjectionMode = store.InjectionModeAsNeeded + } + + update := s.client.Secret.Update(). + Where( + entsecret.KeyEQ(secret.Key), + entsecret.ScopeEQ(secret.Scope), + entsecret.ScopeIDEQ(secret.ScopeID), + ). + SetEncryptedValue(secret.EncryptedValue). + SetSecretType(entsecret.SecretType(secret.SecretType)). + SetTarget(secret.Target). + SetDescription(secret.Description). + SetInjectionMode(entsecret.InjectionMode(secret.InjectionMode)). + SetAllowProgeny(secret.AllowProgeny). + SetVersion(secret.Version). + SetUpdatedBy(secret.UpdatedBy). + SetUpdated(secret.Updated) + + if secret.SecretRef != "" { + update.SetSecretRef(secret.SecretRef) + } else { + update.ClearSecretRef() + } + + n, err := update.Save(ctx) + if err != nil { + return mapError(err) + } + if n == 0 { + return store.ErrNotFound + } + return nil +} + +// UpsertSecret creates or updates a secret, keyed by (key, scope, scopeID). +func (s *SecretStore) UpsertSecret(ctx context.Context, secret *store.Secret) (bool, error) { + now := time.Now() + secret.Updated = now + + existing, err := s.GetSecret(ctx, secret.Key, secret.Scope, secret.ScopeID) + if err != nil && !errors.Is(err, store.ErrNotFound) { + return false, err + } + + if existing != nil { + // Update existing: preserve identity/creation metadata. UpdateSecret + // increments the version from the existing baseline. + secret.ID = existing.ID + secret.Created = existing.Created + secret.CreatedBy = existing.CreatedBy + secret.Version = existing.Version + if err := s.UpdateSecret(ctx, secret); err != nil { + return false, err + } + return false, nil + } + + secret.Created = now + if err := s.CreateSecret(ctx, secret); err != nil { + return false, err + } + return true, nil +} + +// DeleteSecret removes a secret by key, scope, and scopeID. +func (s *SecretStore) DeleteSecret(ctx context.Context, key, scope, scopeID string) error { + n, err := s.client.Secret.Delete(). + Where( + entsecret.KeyEQ(key), + entsecret.ScopeEQ(scope), + entsecret.ScopeIDEQ(scopeID), + ). + Exec(ctx) + if err != nil { + return err + } + if n == 0 { + return store.ErrNotFound + } + return nil +} + +// DeleteSecretsByScope removes all secrets for a given scope, returning the +// number of deleted records. +func (s *SecretStore) DeleteSecretsByScope(ctx context.Context, scope, scopeID string) (int, error) { + n, err := s.client.Secret.Delete(). + Where( + entsecret.ScopeEQ(scope), + entsecret.ScopeIDEQ(scopeID), + ). + Exec(ctx) + if err != nil { + return 0, err + } + return n, nil +} + +// ListSecrets returns secret metadata matching the filter. The EncryptedValue +// is never populated. +func (s *SecretStore) ListSecrets(ctx context.Context, filter store.SecretFilter) ([]store.Secret, error) { + query := s.client.Secret.Query() + + if filter.Scope != "" { + query.Where(entsecret.ScopeEQ(filter.Scope)) + } + if filter.ScopeID != "" { + query.Where(entsecret.ScopeIDEQ(filter.ScopeID)) + } + if filter.Key != "" { + query.Where(entsecret.KeyEQ(filter.Key)) + } + if filter.Type != "" { + query.Where(entsecret.SecretTypeEQ(entsecret.SecretType(filter.Type))) + } + + rows, err := query.Order(entsecret.ByKey()).All(ctx) + if err != nil { + return nil, err + } + + secrets := make([]store.Secret, 0, len(rows)) + for _, e := range rows { + secrets = append(secrets, entSecretRowToStore(e, false)) + } + return secrets, nil +} + +// ListProgenySecrets returns user-scoped secrets with allowProgeny=true whose +// createdBy is in the given set of ancestor IDs. The EncryptedValue is never +// populated. This preserves the progeny-inheritance semantics of the legacy +// SQLite query via an IN-list over created_by. +func (s *SecretStore) ListProgenySecrets(ctx context.Context, ancestorIDs []string) ([]store.Secret, error) { + if len(ancestorIDs) == 0 { + return nil, nil + } + + rows, err := s.client.Secret.Query(). + Where( + entsecret.ScopeEQ(store.ScopeUser), + entsecret.AllowProgenyEQ(true), + entsecret.CreatedByIn(ancestorIDs...), + ). + Order(entsecret.ByKey()). + All(ctx) + if err != nil { + return nil, err + } + + secrets := make([]store.Secret, 0, len(rows)) + for _, e := range rows { + secrets = append(secrets, entSecretRowToStore(e, false)) + } + return secrets, nil +} + +// GetSecretValue retrieves the encrypted value of a secret. +func (s *SecretStore) GetSecretValue(ctx context.Context, key, scope, scopeID string) (string, error) { + e, err := s.client.Secret.Query(). + Where( + entsecret.KeyEQ(key), + entsecret.ScopeEQ(scope), + entsecret.ScopeIDEQ(scopeID), + ). + Only(ctx) + if err != nil { + return "", mapError(err) + } + return e.EncryptedValue, nil +} + +// ============================================================================= +// EnvVar operations +// ============================================================================= + +// entEnvVarToStore converts an Ent EnvVar entity to a store.EnvVar model. +func entEnvVarToStore(e *ent.EnvVar) store.EnvVar { + return store.EnvVar{ + ID: e.ID.String(), + Key: e.Key, + Value: e.Value, + Scope: e.Scope, + ScopeID: e.ScopeID, + Description: e.Description, + Sensitive: e.Sensitive, + InjectionMode: string(e.InjectionMode), + Secret: e.Secret, + Created: e.Created, + Updated: e.Updated, + CreatedBy: e.CreatedBy, + } +} + +// CreateEnvVar creates a new environment variable. +func (s *SecretStore) CreateEnvVar(ctx context.Context, envVar *store.EnvVar) error { + uid, err := parseUUID(envVar.ID) + if err != nil { + return err + } + + now := time.Now() + envVar.Created = now + envVar.Updated = now + + // The Ent enum column rejects empty values; normalize to the default the + // legacy schema applied (as_needed) and reflect it back on the model. + if envVar.InjectionMode == "" { + envVar.InjectionMode = store.InjectionModeAsNeeded + } + + create := s.client.EnvVar.Create(). + SetID(uid). + SetKey(envVar.Key). + SetValue(envVar.Value). + SetScope(envVar.Scope). + SetScopeID(envVar.ScopeID). + SetSensitive(envVar.Sensitive). + SetInjectionMode(entenvvar.InjectionMode(envVar.InjectionMode)). + SetSecret(envVar.Secret). + SetCreated(envVar.Created). + SetUpdated(envVar.Updated) + + if envVar.Description != "" { + create.SetDescription(envVar.Description) + } + if envVar.CreatedBy != "" { + create.SetCreatedBy(envVar.CreatedBy) + } + + if _, err := create.Save(ctx); err != nil { + return mapError(err) + } + return nil +} + +// GetEnvVar retrieves an environment variable by key, scope, and scopeID. +func (s *SecretStore) GetEnvVar(ctx context.Context, key, scope, scopeID string) (*store.EnvVar, error) { + e, err := s.client.EnvVar.Query(). + Where( + entenvvar.KeyEQ(key), + entenvvar.ScopeEQ(scope), + entenvvar.ScopeIDEQ(scopeID), + ). + Only(ctx) + if err != nil { + return nil, mapError(err) + } + ev := entEnvVarToStore(e) + return &ev, nil +} + +// UpdateEnvVar updates an existing environment variable. +func (s *SecretStore) UpdateEnvVar(ctx context.Context, envVar *store.EnvVar) error { + envVar.Updated = time.Now() + + if envVar.InjectionMode == "" { + envVar.InjectionMode = store.InjectionModeAsNeeded + } + + n, err := s.client.EnvVar.Update(). + Where( + entenvvar.KeyEQ(envVar.Key), + entenvvar.ScopeEQ(envVar.Scope), + entenvvar.ScopeIDEQ(envVar.ScopeID), + ). + SetValue(envVar.Value). + SetDescription(envVar.Description). + SetSensitive(envVar.Sensitive). + SetInjectionMode(entenvvar.InjectionMode(envVar.InjectionMode)). + SetSecret(envVar.Secret). + SetUpdated(envVar.Updated). + Save(ctx) + if err != nil { + return mapError(err) + } + if n == 0 { + return store.ErrNotFound + } + return nil +} + +// UpsertEnvVar creates or updates an environment variable, keyed by +// (key, scope, scopeID). +func (s *SecretStore) UpsertEnvVar(ctx context.Context, envVar *store.EnvVar) (bool, error) { + now := time.Now() + envVar.Updated = now + + existing, err := s.GetEnvVar(ctx, envVar.Key, envVar.Scope, envVar.ScopeID) + if err != nil && !errors.Is(err, store.ErrNotFound) { + return false, err + } + + if existing != nil { + envVar.ID = existing.ID + envVar.Created = existing.Created + envVar.CreatedBy = existing.CreatedBy + if err := s.UpdateEnvVar(ctx, envVar); err != nil { + return false, err + } + return false, nil + } + + envVar.Created = now + if err := s.CreateEnvVar(ctx, envVar); err != nil { + return false, err + } + return true, nil +} + +// DeleteEnvVar removes an environment variable by key, scope, and scopeID. +func (s *SecretStore) DeleteEnvVar(ctx context.Context, key, scope, scopeID string) error { + n, err := s.client.EnvVar.Delete(). + Where( + entenvvar.KeyEQ(key), + entenvvar.ScopeEQ(scope), + entenvvar.ScopeIDEQ(scopeID), + ). + Exec(ctx) + if err != nil { + return err + } + if n == 0 { + return store.ErrNotFound + } + return nil +} + +// DeleteEnvVarsByScope removes all environment variables for a given scope, +// returning the number of deleted records. +func (s *SecretStore) DeleteEnvVarsByScope(ctx context.Context, scope, scopeID string) (int, error) { + n, err := s.client.EnvVar.Delete(). + Where( + entenvvar.ScopeEQ(scope), + entenvvar.ScopeIDEQ(scopeID), + ). + Exec(ctx) + if err != nil { + return 0, err + } + return n, nil +} + +// ListEnvVars returns environment variables matching the filter, ordered by key. +func (s *SecretStore) ListEnvVars(ctx context.Context, filter store.EnvVarFilter) ([]store.EnvVar, error) { + query := s.client.EnvVar.Query() + + if filter.Scope != "" { + query.Where(entenvvar.ScopeEQ(filter.Scope)) + } + if filter.ScopeID != "" { + query.Where(entenvvar.ScopeIDEQ(filter.ScopeID)) + } + if filter.Key != "" { + query.Where(entenvvar.KeyEQ(filter.Key)) + } + + rows, err := query.Order(entenvvar.ByKey()).All(ctx) + if err != nil { + return nil, err + } + + envVars := make([]store.EnvVar, 0, len(rows)) + for _, e := range rows { + envVars = append(envVars, entEnvVarToStore(e)) + } + return envVars, nil +} diff --git a/pkg/store/entadapter/secret_store_test.go b/pkg/store/entadapter/secret_store_test.go new file mode 100644 index 000000000..8f3b9d26d --- /dev/null +++ b/pkg/store/entadapter/secret_store_test.go @@ -0,0 +1,443 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//go:build !no_sqlite + +package entadapter + +import ( + "context" + "testing" + + "github.com/GoogleCloudPlatform/scion/pkg/store" + "github.com/GoogleCloudPlatform/scion/pkg/store/enttest" + "github.com/google/uuid" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func newTestSecretStore(t *testing.T) *SecretStore { + t.Helper() + client := enttest.NewClient(t) + return NewSecretStore(client) +} + +// ============================================================================= +// Secret tests +// ============================================================================= + +func TestCreateAndGetSecret(t *testing.T) { + ss := newTestSecretStore(t) + ctx := context.Background() + + sec := &store.Secret{ + ID: uuid.New().String(), + Key: "API_KEY", + EncryptedValue: "enc-value", + Scope: store.ScopeUser, + ScopeID: uuid.New().String(), + Description: "an api key", + AllowProgeny: true, + CreatedBy: uuid.New().String(), + } + require.NoError(t, ss.CreateSecret(ctx, sec)) + + // Defaults are applied on create. + assert.Equal(t, store.SecretTypeEnvironment, sec.SecretType) + assert.Equal(t, store.InjectionModeAsNeeded, sec.InjectionMode) + assert.Equal(t, "API_KEY", sec.Target, "empty target should default to key") + assert.Equal(t, 1, sec.Version) + assert.False(t, sec.Created.IsZero()) + + got, err := ss.GetSecret(ctx, "API_KEY", store.ScopeUser, sec.ScopeID) + require.NoError(t, err) + assert.Equal(t, sec.ID, got.ID) + assert.Equal(t, "enc-value", got.EncryptedValue) + assert.Equal(t, "API_KEY", got.Target) + assert.True(t, got.AllowProgeny) + assert.Equal(t, 1, got.Version) +} + +func TestGetSecretNotFound(t *testing.T) { + ss := newTestSecretStore(t) + ctx := context.Background() + + _, err := ss.GetSecret(ctx, "missing", store.ScopeUser, uuid.New().String()) + assert.ErrorIs(t, err, store.ErrNotFound) +} + +func TestCreateSecretDuplicate(t *testing.T) { + ss := newTestSecretStore(t) + ctx := context.Background() + + scopeID := uuid.New().String() + sec := &store.Secret{ID: uuid.New().String(), Key: "DUP", EncryptedValue: "v", Scope: store.ScopeUser, ScopeID: scopeID} + require.NoError(t, ss.CreateSecret(ctx, sec)) + + dup := &store.Secret{ID: uuid.New().String(), Key: "DUP", EncryptedValue: "v2", Scope: store.ScopeUser, ScopeID: scopeID} + err := ss.CreateSecret(ctx, dup) + assert.ErrorIs(t, err, store.ErrAlreadyExists) +} + +func TestUpdateSecretIncrementsVersion(t *testing.T) { + ss := newTestSecretStore(t) + ctx := context.Background() + + scopeID := uuid.New().String() + sec := &store.Secret{ID: uuid.New().String(), Key: "K", EncryptedValue: "v1", Scope: store.ScopeProject, ScopeID: scopeID} + require.NoError(t, ss.CreateSecret(ctx, sec)) + require.Equal(t, 1, sec.Version) + + sec.EncryptedValue = "v2" + require.NoError(t, ss.UpdateSecret(ctx, sec)) + assert.Equal(t, 2, sec.Version) + + got, err := ss.GetSecret(ctx, "K", store.ScopeProject, scopeID) + require.NoError(t, err) + assert.Equal(t, "v2", got.EncryptedValue) + assert.Equal(t, 2, got.Version) +} + +func TestUpdateSecretNotFound(t *testing.T) { + ss := newTestSecretStore(t) + ctx := context.Background() + + sec := &store.Secret{Key: "ghost", EncryptedValue: "v", Scope: store.ScopeUser, ScopeID: uuid.New().String()} + err := ss.UpdateSecret(ctx, sec) + assert.ErrorIs(t, err, store.ErrNotFound) +} + +func TestUpsertSecret(t *testing.T) { + ss := newTestSecretStore(t) + ctx := context.Background() + + scopeID := uuid.New().String() + sec := &store.Secret{ID: uuid.New().String(), Key: "UP", EncryptedValue: "v1", Scope: store.ScopeUser, ScopeID: scopeID, CreatedBy: "creator"} + + created, err := ss.UpsertSecret(ctx, sec) + require.NoError(t, err) + assert.True(t, created, "first upsert should create") + + // Second upsert updates, preserving identity and creation metadata. + upd := &store.Secret{Key: "UP", EncryptedValue: "v2", Scope: store.ScopeUser, ScopeID: scopeID} + created, err = ss.UpsertSecret(ctx, upd) + require.NoError(t, err) + assert.False(t, created, "second upsert should update") + assert.Equal(t, sec.ID, upd.ID, "ID preserved across upsert") + assert.Equal(t, "creator", upd.CreatedBy, "createdBy preserved across upsert") + + got, err := ss.GetSecret(ctx, "UP", store.ScopeUser, scopeID) + require.NoError(t, err) + assert.Equal(t, "v2", got.EncryptedValue) + assert.Equal(t, 2, got.Version) +} + +func TestDeleteSecret(t *testing.T) { + ss := newTestSecretStore(t) + ctx := context.Background() + + scopeID := uuid.New().String() + sec := &store.Secret{ID: uuid.New().String(), Key: "DEL", EncryptedValue: "v", Scope: store.ScopeUser, ScopeID: scopeID} + require.NoError(t, ss.CreateSecret(ctx, sec)) + + require.NoError(t, ss.DeleteSecret(ctx, "DEL", store.ScopeUser, scopeID)) + + _, err := ss.GetSecret(ctx, "DEL", store.ScopeUser, scopeID) + assert.ErrorIs(t, err, store.ErrNotFound) + + // Deleting again returns ErrNotFound. + assert.ErrorIs(t, ss.DeleteSecret(ctx, "DEL", store.ScopeUser, scopeID), store.ErrNotFound) +} + +func TestDeleteSecretsByScope(t *testing.T) { + ss := newTestSecretStore(t) + ctx := context.Background() + + scopeID := uuid.New().String() + for _, k := range []string{"A", "B", "C"} { + require.NoError(t, ss.CreateSecret(ctx, &store.Secret{ + ID: uuid.New().String(), Key: k, EncryptedValue: "v", Scope: store.ScopeProject, ScopeID: scopeID, + })) + } + // A secret in a different scope must survive. + require.NoError(t, ss.CreateSecret(ctx, &store.Secret{ + ID: uuid.New().String(), Key: "OTHER", EncryptedValue: "v", Scope: store.ScopeProject, ScopeID: uuid.New().String(), + })) + + n, err := ss.DeleteSecretsByScope(ctx, store.ScopeProject, scopeID) + require.NoError(t, err) + assert.Equal(t, 3, n) + + remaining, err := ss.ListSecrets(ctx, store.SecretFilter{Scope: store.ScopeProject, ScopeID: scopeID}) + require.NoError(t, err) + assert.Empty(t, remaining) +} + +func TestListSecretsExcludesEncryptedValue(t *testing.T) { + ss := newTestSecretStore(t) + ctx := context.Background() + + scopeID := uuid.New().String() + require.NoError(t, ss.CreateSecret(ctx, &store.Secret{ + ID: uuid.New().String(), Key: "LIST", EncryptedValue: "super-secret", Scope: store.ScopeUser, ScopeID: scopeID, + })) + + list, err := ss.ListSecrets(ctx, store.SecretFilter{Scope: store.ScopeUser, ScopeID: scopeID}) + require.NoError(t, err) + require.Len(t, list, 1) + assert.Empty(t, list[0].EncryptedValue, "ListSecrets must not expose encrypted value") + assert.Equal(t, "LIST", list[0].Key) +} + +func TestListSecretsFilterByType(t *testing.T) { + ss := newTestSecretStore(t) + ctx := context.Background() + + scopeID := uuid.New().String() + require.NoError(t, ss.CreateSecret(ctx, &store.Secret{ + ID: uuid.New().String(), Key: "ENV", EncryptedValue: "v", SecretType: store.SecretTypeEnvironment, Scope: store.ScopeUser, ScopeID: scopeID, + })) + require.NoError(t, ss.CreateSecret(ctx, &store.Secret{ + ID: uuid.New().String(), Key: "FILE", EncryptedValue: "v", SecretType: store.SecretTypeFile, Target: "/etc/x", Scope: store.ScopeUser, ScopeID: scopeID, + })) + + files, err := ss.ListSecrets(ctx, store.SecretFilter{Scope: store.ScopeUser, ScopeID: scopeID, Type: store.SecretTypeFile}) + require.NoError(t, err) + require.Len(t, files, 1) + assert.Equal(t, "FILE", files[0].Key) + assert.Equal(t, "/etc/x", files[0].Target) +} + +func TestGetSecretValue(t *testing.T) { + ss := newTestSecretStore(t) + ctx := context.Background() + + scopeID := uuid.New().String() + require.NoError(t, ss.CreateSecret(ctx, &store.Secret{ + ID: uuid.New().String(), Key: "VAL", EncryptedValue: "the-value", Scope: store.ScopeUser, ScopeID: scopeID, + })) + + v, err := ss.GetSecretValue(ctx, "VAL", store.ScopeUser, scopeID) + require.NoError(t, err) + assert.Equal(t, "the-value", v) + + _, err = ss.GetSecretValue(ctx, "nope", store.ScopeUser, scopeID) + assert.ErrorIs(t, err, store.ErrNotFound) +} + +// TestScopePolymorphism verifies that identical keys in different scopes are +// independent records (no cross-scope collisions). +func TestSecretScopePolymorphism(t *testing.T) { + ss := newTestSecretStore(t) + ctx := context.Background() + + userID := uuid.New().String() + projectID := uuid.New().String() + + require.NoError(t, ss.CreateSecret(ctx, &store.Secret{ID: uuid.New().String(), Key: "TOKEN", EncryptedValue: "user-val", Scope: store.ScopeUser, ScopeID: userID})) + require.NoError(t, ss.CreateSecret(ctx, &store.Secret{ID: uuid.New().String(), Key: "TOKEN", EncryptedValue: "proj-val", Scope: store.ScopeProject, ScopeID: projectID})) + + u, err := ss.GetSecret(ctx, "TOKEN", store.ScopeUser, userID) + require.NoError(t, err) + assert.Equal(t, "user-val", u.EncryptedValue) + + p, err := ss.GetSecret(ctx, "TOKEN", store.ScopeProject, projectID) + require.NoError(t, err) + assert.Equal(t, "proj-val", p.EncryptedValue) +} + +// TestListProgenySecretsInheritance verifies the transitive progeny-inheritance +// query: only user-scoped, allow_progeny=true secrets whose created_by is within +// the ancestor set are returned, and the encrypted value is never exposed. +func TestListProgenySecretsInheritance(t *testing.T) { + ss := newTestSecretStore(t) + ctx := context.Background() + + ancestor1 := uuid.New().String() + ancestor2 := uuid.New().String() + stranger := uuid.New().String() + + // Eligible: user-scoped, allow_progeny, created by an ancestor. + require.NoError(t, ss.CreateSecret(ctx, &store.Secret{ + ID: uuid.New().String(), Key: "INHERIT_1", EncryptedValue: "secret-1", + Scope: store.ScopeUser, ScopeID: ancestor1, AllowProgeny: true, CreatedBy: ancestor1, + })) + require.NoError(t, ss.CreateSecret(ctx, &store.Secret{ + ID: uuid.New().String(), Key: "INHERIT_2", EncryptedValue: "secret-2", + Scope: store.ScopeUser, ScopeID: ancestor2, AllowProgeny: true, CreatedBy: ancestor2, + })) + + // Ineligible: allow_progeny=false. + require.NoError(t, ss.CreateSecret(ctx, &store.Secret{ + ID: uuid.New().String(), Key: "NO_PROGENY", EncryptedValue: "x", + Scope: store.ScopeUser, ScopeID: ancestor1, AllowProgeny: false, CreatedBy: ancestor1, + })) + // Ineligible: created by a non-ancestor. + require.NoError(t, ss.CreateSecret(ctx, &store.Secret{ + ID: uuid.New().String(), Key: "STRANGER", EncryptedValue: "x", + Scope: store.ScopeUser, ScopeID: stranger, AllowProgeny: true, CreatedBy: stranger, + })) + // Ineligible: wrong scope, even though allow_progeny + ancestor creator. + require.NoError(t, ss.CreateSecret(ctx, &store.Secret{ + ID: uuid.New().String(), Key: "PROJ", EncryptedValue: "x", + Scope: store.ScopeProject, ScopeID: uuid.New().String(), AllowProgeny: true, CreatedBy: ancestor1, + })) + + got, err := ss.ListProgenySecrets(ctx, []string{ancestor1, ancestor2}) + require.NoError(t, err) + require.Len(t, got, 2) + + keys := map[string]bool{} + for _, s := range got { + keys[s.Key] = true + assert.Empty(t, s.EncryptedValue, "progeny secrets must not expose encrypted value") + } + assert.True(t, keys["INHERIT_1"]) + assert.True(t, keys["INHERIT_2"]) +} + +func TestListProgenySecretsEmptyAncestors(t *testing.T) { + ss := newTestSecretStore(t) + ctx := context.Background() + + got, err := ss.ListProgenySecrets(ctx, nil) + require.NoError(t, err) + assert.Nil(t, got) +} + +// ============================================================================= +// EnvVar tests +// ============================================================================= + +func TestCreateAndGetEnvVar(t *testing.T) { + ss := newTestSecretStore(t) + ctx := context.Background() + + scopeID := uuid.New().String() + ev := &store.EnvVar{ + ID: uuid.New().String(), + Key: "LOG_LEVEL", + Value: "debug", + Scope: store.ScopeProject, + ScopeID: scopeID, + Description: "logging", + Sensitive: true, + } + require.NoError(t, ss.CreateEnvVar(ctx, ev)) + assert.Equal(t, store.InjectionModeAsNeeded, ev.InjectionMode, "empty injection mode normalized") + assert.False(t, ev.Created.IsZero()) + + got, err := ss.GetEnvVar(ctx, "LOG_LEVEL", store.ScopeProject, scopeID) + require.NoError(t, err) + assert.Equal(t, "debug", got.Value) + assert.True(t, got.Sensitive) + assert.Equal(t, "logging", got.Description) +} + +func TestCreateEnvVarDuplicate(t *testing.T) { + ss := newTestSecretStore(t) + ctx := context.Background() + + scopeID := uuid.New().String() + require.NoError(t, ss.CreateEnvVar(ctx, &store.EnvVar{ID: uuid.New().String(), Key: "X", Value: "1", Scope: store.ScopeUser, ScopeID: scopeID})) + err := ss.CreateEnvVar(ctx, &store.EnvVar{ID: uuid.New().String(), Key: "X", Value: "2", Scope: store.ScopeUser, ScopeID: scopeID}) + assert.ErrorIs(t, err, store.ErrAlreadyExists) +} + +func TestUpdateEnvVar(t *testing.T) { + ss := newTestSecretStore(t) + ctx := context.Background() + + scopeID := uuid.New().String() + ev := &store.EnvVar{ID: uuid.New().String(), Key: "U", Value: "1", Scope: store.ScopeUser, ScopeID: scopeID} + require.NoError(t, ss.CreateEnvVar(ctx, ev)) + + ev.Value = "2" + require.NoError(t, ss.UpdateEnvVar(ctx, ev)) + + got, err := ss.GetEnvVar(ctx, "U", store.ScopeUser, scopeID) + require.NoError(t, err) + assert.Equal(t, "2", got.Value) +} + +func TestUpdateEnvVarNotFound(t *testing.T) { + ss := newTestSecretStore(t) + ctx := context.Background() + + err := ss.UpdateEnvVar(ctx, &store.EnvVar{Key: "ghost", Value: "v", Scope: store.ScopeUser, ScopeID: uuid.New().String()}) + assert.ErrorIs(t, err, store.ErrNotFound) +} + +func TestUpsertEnvVar(t *testing.T) { + ss := newTestSecretStore(t) + ctx := context.Background() + + scopeID := uuid.New().String() + ev := &store.EnvVar{ID: uuid.New().String(), Key: "UP", Value: "1", Scope: store.ScopeUser, ScopeID: scopeID, CreatedBy: "creator"} + + created, err := ss.UpsertEnvVar(ctx, ev) + require.NoError(t, err) + assert.True(t, created) + + upd := &store.EnvVar{Key: "UP", Value: "2", Scope: store.ScopeUser, ScopeID: scopeID} + created, err = ss.UpsertEnvVar(ctx, upd) + require.NoError(t, err) + assert.False(t, created) + assert.Equal(t, ev.ID, upd.ID) + assert.Equal(t, "creator", upd.CreatedBy) + + got, err := ss.GetEnvVar(ctx, "UP", store.ScopeUser, scopeID) + require.NoError(t, err) + assert.Equal(t, "2", got.Value) +} + +func TestDeleteEnvVar(t *testing.T) { + ss := newTestSecretStore(t) + ctx := context.Background() + + scopeID := uuid.New().String() + require.NoError(t, ss.CreateEnvVar(ctx, &store.EnvVar{ID: uuid.New().String(), Key: "D", Value: "1", Scope: store.ScopeUser, ScopeID: scopeID})) + require.NoError(t, ss.DeleteEnvVar(ctx, "D", store.ScopeUser, scopeID)) + _, err := ss.GetEnvVar(ctx, "D", store.ScopeUser, scopeID) + assert.ErrorIs(t, err, store.ErrNotFound) + assert.ErrorIs(t, ss.DeleteEnvVar(ctx, "D", store.ScopeUser, scopeID), store.ErrNotFound) +} + +func TestDeleteEnvVarsByScope(t *testing.T) { + ss := newTestSecretStore(t) + ctx := context.Background() + + scopeID := uuid.New().String() + for _, k := range []string{"A", "B"} { + require.NoError(t, ss.CreateEnvVar(ctx, &store.EnvVar{ID: uuid.New().String(), Key: k, Value: "v", Scope: store.ScopeProject, ScopeID: scopeID})) + } + n, err := ss.DeleteEnvVarsByScope(ctx, store.ScopeProject, scopeID) + require.NoError(t, err) + assert.Equal(t, 2, n) +} + +func TestListEnvVarsOrderedByKey(t *testing.T) { + ss := newTestSecretStore(t) + ctx := context.Background() + + scopeID := uuid.New().String() + for _, k := range []string{"ZEBRA", "ALPHA", "MIKE"} { + require.NoError(t, ss.CreateEnvVar(ctx, &store.EnvVar{ID: uuid.New().String(), Key: k, Value: "v", Scope: store.ScopeUser, ScopeID: scopeID})) + } + + list, err := ss.ListEnvVars(ctx, store.EnvVarFilter{Scope: store.ScopeUser, ScopeID: scopeID}) + require.NoError(t, err) + require.Len(t, list, 3) + assert.Equal(t, []string{"ALPHA", "MIKE", "ZEBRA"}, []string{list[0].Key, list[1].Key, list[2].Key}) +} diff --git a/pkg/store/entadapter/template_store.go b/pkg/store/entadapter/template_store.go new file mode 100644 index 000000000..73cf58580 --- /dev/null +++ b/pkg/store/entadapter/template_store.go @@ -0,0 +1,579 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package entadapter + +import ( + "context" + "encoding/json" + "time" + + entsql "entgo.io/ent/dialect/sql" + "github.com/GoogleCloudPlatform/scion/pkg/ent" + entharnessconfig "github.com/GoogleCloudPlatform/scion/pkg/ent/harnessconfig" + enttemplate "github.com/GoogleCloudPlatform/scion/pkg/ent/template" + "github.com/GoogleCloudPlatform/scion/pkg/store" +) + +// TemplateStore implements store.TemplateStore and store.HarnessConfigStore +// using Ent ORM. +// +// Both entities use (scope, scope_id) polymorphic addressing rather than FK +// edges so that global/unscoped rows port cleanly. The structured config and +// file-manifest columns are stored as raw JSON strings, matching the legacy +// SQLite layout. Subscription templates, although a sibling notification-domain +// table, are intentionally NOT handled here — they are owned by NotificationStore. +type TemplateStore struct { + client *ent.Client +} + +// NewTemplateStore creates a new Ent-backed TemplateStore. +func NewTemplateStore(client *ent.Client) *TemplateStore { + return &TemplateStore{client: client} +} + +// marshalJSONString serializes a value to a JSON string for storage in a TEXT +// column. A nil value yields an empty string. This mirrors the SQLite adapter's +// marshalJSON helper so both backends round-trip identically. +func marshalJSONString(v interface{}) string { + if v == nil { + return "" + } + data, err := json.Marshal(v) + if err != nil { + return "" + } + return string(data) +} + +// unmarshalJSONString deserializes a JSON string into v. An empty string is a +// no-op, leaving v at its zero value. +func unmarshalJSONString[T any](data string, v *T) { + if data == "" { + return + } + _ = json.Unmarshal([]byte(data), v) +} + +// ============================================================================= +// Template operations +// ============================================================================= + +// entTemplateRowToStore converts an Ent Template entity to a store.Template model. +func entTemplateRowToStore(e *ent.Template) *store.Template { + t := &store.Template{ + ID: e.ID.String(), + Name: e.Name, + Slug: e.Slug, + DisplayName: e.DisplayName, + Description: e.Description, + Harness: e.Harness, + DefaultHarnessConfig: e.DefaultHarnessConfig, + Image: e.Image, + ContentHash: e.ContentHash, + Scope: e.Scope, + ScopeID: e.ScopeID, + ProjectID: e.ProjectID, + StorageURI: e.StorageURI, + StorageBucket: e.StorageBucket, + StoragePath: e.StoragePath, + BaseTemplate: e.BaseTemplate, + Status: string(e.Status), + OwnerID: e.OwnerID, + CreatedBy: e.CreatedBy, + UpdatedBy: e.UpdatedBy, + Visibility: e.Visibility, + Created: e.Created, + Updated: e.Updated, + } + unmarshalJSONString(e.Config, &t.Config) + unmarshalJSONString(e.Files, &t.Files) + return t +} + +// CreateTemplate creates a new template record. +func (s *TemplateStore) CreateTemplate(ctx context.Context, template *store.Template) error { + uid, err := parseUUID(template.ID) + if err != nil { + return err + } + + now := time.Now() + template.Created = now + template.Updated = now + + if template.Status == "" { + template.Status = store.TemplateStatusActive + } + + create := s.client.Template.Create(). + SetID(uid). + SetName(template.Name). + SetSlug(template.Slug). + SetDisplayName(template.DisplayName). + SetDescription(template.Description). + SetHarness(template.Harness). + SetDefaultHarnessConfig(template.DefaultHarnessConfig). + SetImage(template.Image). + SetConfig(marshalJSONString(template.Config)). + SetContentHash(template.ContentHash). + SetScope(template.Scope). + SetScopeID(template.ScopeID). + SetProjectID(template.ProjectID). + SetStorageURI(template.StorageURI). + SetStorageBucket(template.StorageBucket). + SetStoragePath(template.StoragePath). + SetFiles(marshalJSONString(template.Files)). + SetBaseTemplate(template.BaseTemplate). + SetStatus(enttemplate.Status(template.Status)). + SetOwnerID(template.OwnerID). + SetCreatedBy(template.CreatedBy). + SetUpdatedBy(template.UpdatedBy). + SetVisibility(template.Visibility). + SetCreated(template.Created). + SetUpdated(template.Updated) + + if _, err := create.Save(ctx); err != nil { + return mapError(err) + } + return nil +} + +// GetTemplate retrieves a template by ID. +func (s *TemplateStore) GetTemplate(ctx context.Context, id string) (*store.Template, error) { + uid, err := parseGetID(id) + if err != nil { + return nil, err + } + e, err := s.client.Template.Get(ctx, uid) + if err != nil { + return nil, mapError(err) + } + return entTemplateRowToStore(e), nil +} + +// GetTemplateBySlug retrieves a template by its slug and scope. For the project +// scope it also matches the legacy project_id column for backwards compatibility. +func (s *TemplateStore) GetTemplateBySlug(ctx context.Context, slug, scope, scopeID string) (*store.Template, error) { + query := s.client.Template.Query(). + Where( + enttemplate.SlugEQ(slug), + enttemplate.ScopeEQ(scope), + ) + + switch { + case scope == store.TemplateScopeProject && scopeID != "": + query.Where(enttemplate.Or( + enttemplate.ScopeIDEQ(scopeID), + enttemplate.ProjectIDEQ(scopeID), + )) + case scope == store.TemplateScopeUser && scopeID != "": + query.Where(enttemplate.ScopeIDEQ(scopeID)) + } + + e, err := query.First(ctx) + if err != nil { + return nil, mapError(err) + } + return entTemplateRowToStore(e), nil +} + +// UpdateTemplate updates an existing template. +func (s *TemplateStore) UpdateTemplate(ctx context.Context, template *store.Template) error { + uid, err := parseUUID(template.ID) + if err != nil { + return err + } + + template.Updated = time.Now() + + _, err = s.client.Template.UpdateOneID(uid). + SetName(template.Name). + SetSlug(template.Slug). + SetDisplayName(template.DisplayName). + SetDescription(template.Description). + SetHarness(template.Harness). + SetDefaultHarnessConfig(template.DefaultHarnessConfig). + SetImage(template.Image). + SetConfig(marshalJSONString(template.Config)). + SetContentHash(template.ContentHash). + SetScope(template.Scope). + SetScopeID(template.ScopeID). + SetProjectID(template.ProjectID). + SetStorageURI(template.StorageURI). + SetStorageBucket(template.StorageBucket). + SetStoragePath(template.StoragePath). + SetFiles(marshalJSONString(template.Files)). + SetBaseTemplate(template.BaseTemplate). + SetStatus(enttemplate.Status(template.Status)). + SetOwnerID(template.OwnerID). + SetUpdatedBy(template.UpdatedBy). + SetVisibility(template.Visibility). + SetUpdated(template.Updated). + Save(ctx) + if err != nil { + return mapError(err) + } + return nil +} + +// DeleteTemplate removes a template by ID. +func (s *TemplateStore) DeleteTemplate(ctx context.Context, id string) error { + uid, err := parseUUID(id) + if err != nil { + return err + } + if err := s.client.Template.DeleteOneID(uid).Exec(ctx); err != nil { + return mapError(err) + } + return nil +} + +// DeleteTemplatesByScope removes all templates for a given scope, returning the +// number of deleted records. +func (s *TemplateStore) DeleteTemplatesByScope(ctx context.Context, scope, scopeID string) (int, error) { + n, err := s.client.Template.Delete(). + Where( + enttemplate.ScopeEQ(scope), + enttemplate.ScopeIDEQ(scopeID), + ). + Exec(ctx) + if err != nil { + return 0, err + } + return n, nil +} + +// ListTemplates returns templates matching the filter criteria. +func (s *TemplateStore) ListTemplates(ctx context.Context, filter store.TemplateFilter, opts store.ListOptions) (*store.ListResult[store.Template], error) { + query := s.client.Template.Query() + + if filter.Name != "" { + query.Where(enttemplate.Or( + enttemplate.NameEQ(filter.Name), + enttemplate.SlugEQ(filter.Name), + )) + } + if filter.Scope != "" { + query.Where(enttemplate.ScopeEQ(filter.Scope)) + } + switch { + case filter.ScopeID != "": + query.Where(enttemplate.Or( + enttemplate.ScopeIDEQ(filter.ScopeID), + enttemplate.ProjectIDEQ(filter.ScopeID), + )) + case filter.ProjectID != "" && filter.Scope == "": + // Project-without-scope: return global plus this project's templates. + query.Where(enttemplate.Or( + enttemplate.ScopeEQ(store.TemplateScopeGlobal), + enttemplate.And( + enttemplate.ScopeEQ(store.TemplateScopeProject), + enttemplate.Or( + enttemplate.ScopeIDEQ(filter.ProjectID), + enttemplate.ProjectIDEQ(filter.ProjectID), + ), + ), + )) + case filter.ProjectID != "": + query.Where(enttemplate.Or( + enttemplate.ScopeIDEQ(filter.ProjectID), + enttemplate.ProjectIDEQ(filter.ProjectID), + )) + } + if filter.Harness != "" { + query.Where(enttemplate.HarnessEQ(filter.Harness)) + } + if filter.OwnerID != "" { + query.Where(enttemplate.OwnerIDEQ(filter.OwnerID)) + } + if filter.Status != "" { + query.Where(enttemplate.StatusEQ(enttemplate.Status(filter.Status))) + } + if filter.Search != "" { + query.Where(enttemplate.Or( + enttemplate.NameContainsFold(filter.Search), + enttemplate.DescriptionContainsFold(filter.Search), + )) + } + + totalCount, err := query.Clone().Count(ctx) + if err != nil { + return nil, err + } + + limit := opts.Limit + if limit <= 0 { + limit = 50 + } + + rows, err := query. + Order(enttemplate.ByCreated(entsql.OrderDesc())). + Limit(limit). + All(ctx) + if err != nil { + return nil, err + } + + items := make([]store.Template, 0, len(rows)) + for _, e := range rows { + items = append(items, *entTemplateRowToStore(e)) + } + + return &store.ListResult[store.Template]{ + Items: items, + TotalCount: totalCount, + }, nil +} + +// ============================================================================= +// HarnessConfig operations +// ============================================================================= + +// entHarnessConfigToStore converts an Ent HarnessConfig entity to a +// store.HarnessConfig model. +func entHarnessConfigToStore(e *ent.HarnessConfig) *store.HarnessConfig { + hc := &store.HarnessConfig{ + ID: e.ID.String(), + Name: e.Name, + Slug: e.Slug, + DisplayName: e.DisplayName, + Description: e.Description, + Harness: e.Harness, + ContentHash: e.ContentHash, + Scope: e.Scope, + ScopeID: e.ScopeID, + StorageURI: e.StorageURI, + StorageBucket: e.StorageBucket, + StoragePath: e.StoragePath, + Status: string(e.Status), + OwnerID: e.OwnerID, + CreatedBy: e.CreatedBy, + UpdatedBy: e.UpdatedBy, + Visibility: e.Visibility, + Created: e.Created, + Updated: e.Updated, + } + unmarshalJSONString(e.Config, &hc.Config) + unmarshalJSONString(e.Files, &hc.Files) + return hc +} + +// CreateHarnessConfig creates a new harness config record. +func (s *TemplateStore) CreateHarnessConfig(ctx context.Context, hc *store.HarnessConfig) error { + uid, err := parseUUID(hc.ID) + if err != nil { + return err + } + + now := time.Now() + hc.Created = now + hc.Updated = now + + if hc.Status == "" { + hc.Status = store.HarnessConfigStatusActive + } + + create := s.client.HarnessConfig.Create(). + SetID(uid). + SetName(hc.Name). + SetSlug(hc.Slug). + SetDisplayName(hc.DisplayName). + SetDescription(hc.Description). + SetHarness(hc.Harness). + SetConfig(marshalJSONString(hc.Config)). + SetContentHash(hc.ContentHash). + SetScope(hc.Scope). + SetScopeID(hc.ScopeID). + SetStorageURI(hc.StorageURI). + SetStorageBucket(hc.StorageBucket). + SetStoragePath(hc.StoragePath). + SetFiles(marshalJSONString(hc.Files)). + SetStatus(entharnessconfig.Status(hc.Status)). + SetOwnerID(hc.OwnerID). + SetCreatedBy(hc.CreatedBy). + SetUpdatedBy(hc.UpdatedBy). + SetVisibility(hc.Visibility). + SetCreated(hc.Created). + SetUpdated(hc.Updated) + + if _, err := create.Save(ctx); err != nil { + return mapError(err) + } + return nil +} + +// GetHarnessConfig retrieves a harness config by ID. +func (s *TemplateStore) GetHarnessConfig(ctx context.Context, id string) (*store.HarnessConfig, error) { + uid, err := parseGetID(id) + if err != nil { + return nil, err + } + e, err := s.client.HarnessConfig.Get(ctx, uid) + if err != nil { + return nil, mapError(err) + } + return entHarnessConfigToStore(e), nil +} + +// GetHarnessConfigBySlug retrieves a harness config by its slug and scope. +func (s *TemplateStore) GetHarnessConfigBySlug(ctx context.Context, slug, scope, scopeID string) (*store.HarnessConfig, error) { + query := s.client.HarnessConfig.Query(). + Where( + entharnessconfig.SlugEQ(slug), + entharnessconfig.ScopeEQ(scope), + ) + if scopeID != "" { + query.Where(entharnessconfig.ScopeIDEQ(scopeID)) + } + + e, err := query.First(ctx) + if err != nil { + return nil, mapError(err) + } + return entHarnessConfigToStore(e), nil +} + +// UpdateHarnessConfig updates an existing harness config. +func (s *TemplateStore) UpdateHarnessConfig(ctx context.Context, hc *store.HarnessConfig) error { + uid, err := parseUUID(hc.ID) + if err != nil { + return err + } + + hc.Updated = time.Now() + + _, err = s.client.HarnessConfig.UpdateOneID(uid). + SetName(hc.Name). + SetSlug(hc.Slug). + SetDisplayName(hc.DisplayName). + SetDescription(hc.Description). + SetHarness(hc.Harness). + SetConfig(marshalJSONString(hc.Config)). + SetContentHash(hc.ContentHash). + SetScope(hc.Scope). + SetScopeID(hc.ScopeID). + SetStorageURI(hc.StorageURI). + SetStorageBucket(hc.StorageBucket). + SetStoragePath(hc.StoragePath). + SetFiles(marshalJSONString(hc.Files)). + SetStatus(entharnessconfig.Status(hc.Status)). + SetOwnerID(hc.OwnerID). + SetUpdatedBy(hc.UpdatedBy). + SetVisibility(hc.Visibility). + SetUpdated(hc.Updated). + Save(ctx) + if err != nil { + return mapError(err) + } + return nil +} + +// DeleteHarnessConfig removes a harness config by ID. +func (s *TemplateStore) DeleteHarnessConfig(ctx context.Context, id string) error { + uid, err := parseUUID(id) + if err != nil { + return err + } + if err := s.client.HarnessConfig.DeleteOneID(uid).Exec(ctx); err != nil { + return mapError(err) + } + return nil +} + +// DeleteHarnessConfigsByScope removes all harness configs for a given scope, +// returning the number of deleted records. +func (s *TemplateStore) DeleteHarnessConfigsByScope(ctx context.Context, scope, scopeID string) (int, error) { + n, err := s.client.HarnessConfig.Delete(). + Where( + entharnessconfig.ScopeEQ(scope), + entharnessconfig.ScopeIDEQ(scopeID), + ). + Exec(ctx) + if err != nil { + return 0, err + } + return n, nil +} + +// ListHarnessConfigs returns harness configs matching the filter criteria. +func (s *TemplateStore) ListHarnessConfigs(ctx context.Context, filter store.HarnessConfigFilter, opts store.ListOptions) (*store.ListResult[store.HarnessConfig], error) { + query := s.client.HarnessConfig.Query() + + if filter.Name != "" { + query.Where(entharnessconfig.Or( + entharnessconfig.NameEQ(filter.Name), + entharnessconfig.SlugEQ(filter.Name), + )) + } + if filter.Scope != "" { + query.Where(entharnessconfig.ScopeEQ(filter.Scope)) + } + switch { + case filter.ScopeID != "": + query.Where(entharnessconfig.ScopeIDEQ(filter.ScopeID)) + case filter.ProjectID != "" && filter.Scope == "": + // Project-without-scope: return global plus this project's configs. + query.Where(entharnessconfig.Or( + entharnessconfig.ScopeEQ(store.HarnessConfigScopeGlobal), + entharnessconfig.And( + entharnessconfig.ScopeEQ(store.HarnessConfigScopeProject), + entharnessconfig.ScopeIDEQ(filter.ProjectID), + ), + )) + } + if filter.Harness != "" { + query.Where(entharnessconfig.HarnessEQ(filter.Harness)) + } + if filter.OwnerID != "" { + query.Where(entharnessconfig.OwnerIDEQ(filter.OwnerID)) + } + if filter.Status != "" { + query.Where(entharnessconfig.StatusEQ(entharnessconfig.Status(filter.Status))) + } + if filter.Search != "" { + query.Where(entharnessconfig.Or( + entharnessconfig.NameContainsFold(filter.Search), + entharnessconfig.DescriptionContainsFold(filter.Search), + )) + } + + totalCount, err := query.Clone().Count(ctx) + if err != nil { + return nil, err + } + + limit := opts.Limit + if limit <= 0 { + limit = 50 + } + + rows, err := query. + Order(entharnessconfig.ByCreated(entsql.OrderDesc())). + Limit(limit). + All(ctx) + if err != nil { + return nil, err + } + + items := make([]store.HarnessConfig, 0, len(rows)) + for _, e := range rows { + items = append(items, *entHarnessConfigToStore(e)) + } + + return &store.ListResult[store.HarnessConfig]{ + Items: items, + TotalCount: totalCount, + }, nil +} diff --git a/pkg/store/entadapter/template_store_test.go b/pkg/store/entadapter/template_store_test.go new file mode 100644 index 000000000..e1d6a2fdb --- /dev/null +++ b/pkg/store/entadapter/template_store_test.go @@ -0,0 +1,368 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//go:build !no_sqlite + +package entadapter + +import ( + "context" + "testing" + + "github.com/GoogleCloudPlatform/scion/pkg/store" + "github.com/GoogleCloudPlatform/scion/pkg/store/enttest" + "github.com/google/uuid" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func newTestTemplateStore(t *testing.T) *TemplateStore { + t.Helper() + client := enttest.NewClient(t) + return NewTemplateStore(client) +} + +// ============================================================================= +// Template tests +// ============================================================================= + +func TestCreateAndGetTemplate(t *testing.T) { + ts := newTestTemplateStore(t) + ctx := context.Background() + + tmpl := &store.Template{ + ID: uuid.New().String(), + Name: "claude", + Slug: "claude", + Harness: "claude", + Image: "img:latest", + Scope: store.TemplateScopeGlobal, + Visibility: "public", + ContentHash: "abc123", + Config: &store.TemplateConfig{ + Harness: "claude", + Image: "img:latest", + Env: map[string]string{"FOO": "bar"}, + }, + Files: []store.TemplateFile{{Path: "home/.bashrc", Size: 10, Hash: "h"}}, + } + require.NoError(t, ts.CreateTemplate(ctx, tmpl)) + assert.Equal(t, store.TemplateStatusActive, tmpl.Status, "empty status defaults to active") + assert.False(t, tmpl.Created.IsZero()) + + got, err := ts.GetTemplate(ctx, tmpl.ID) + require.NoError(t, err) + assert.Equal(t, "claude", got.Name) + assert.Equal(t, "abc123", got.ContentHash) + require.NotNil(t, got.Config) + assert.Equal(t, "bar", got.Config.Env["FOO"]) + require.Len(t, got.Files, 1) + assert.Equal(t, "home/.bashrc", got.Files[0].Path) +} + +func TestGetTemplateNotFound(t *testing.T) { + ts := newTestTemplateStore(t) + _, err := ts.GetTemplate(context.Background(), uuid.New().String()) + assert.ErrorIs(t, err, store.ErrNotFound) +} + +func TestCreateTemplateDuplicateID(t *testing.T) { + ts := newTestTemplateStore(t) + ctx := context.Background() + + id := uuid.New().String() + require.NoError(t, ts.CreateTemplate(ctx, &store.Template{ID: id, Name: "a", Slug: "a", Harness: "claude", Scope: "global"})) + err := ts.CreateTemplate(ctx, &store.Template{ID: id, Name: "b", Slug: "b", Harness: "claude", Scope: "global"}) + assert.ErrorIs(t, err, store.ErrAlreadyExists) +} + +func TestGetTemplateBySlug(t *testing.T) { + ts := newTestTemplateStore(t) + ctx := context.Background() + + projectID := uuid.New().String() + tmpl := &store.Template{ + ID: uuid.New().String(), Name: "custom", Slug: "custom", Harness: "gemini", + Scope: store.TemplateScopeProject, ScopeID: projectID, + } + require.NoError(t, ts.CreateTemplate(ctx, tmpl)) + + got, err := ts.GetTemplateBySlug(ctx, "custom", store.TemplateScopeProject, projectID) + require.NoError(t, err) + assert.Equal(t, tmpl.ID, got.ID) + + _, err = ts.GetTemplateBySlug(ctx, "custom", store.TemplateScopeProject, uuid.New().String()) + assert.ErrorIs(t, err, store.ErrNotFound) +} + +// TestGetTemplateBySlugLegacyProjectID verifies the backwards-compat path where +// a project-scoped template was stored with project_id rather than scope_id. +func TestGetTemplateBySlugLegacyProjectID(t *testing.T) { + ts := newTestTemplateStore(t) + ctx := context.Background() + + projectID := uuid.New().String() + tmpl := &store.Template{ + ID: uuid.New().String(), Name: "legacy", Slug: "legacy", Harness: "claude", + Scope: store.TemplateScopeProject, ProjectID: projectID, // scope_id intentionally empty + } + require.NoError(t, ts.CreateTemplate(ctx, tmpl)) + + got, err := ts.GetTemplateBySlug(ctx, "legacy", store.TemplateScopeProject, projectID) + require.NoError(t, err) + assert.Equal(t, tmpl.ID, got.ID) +} + +func TestUpdateTemplate(t *testing.T) { + ts := newTestTemplateStore(t) + ctx := context.Background() + + tmpl := &store.Template{ID: uuid.New().String(), Name: "old", Slug: "old", Harness: "claude", Scope: "global", Status: store.TemplateStatusActive} + require.NoError(t, ts.CreateTemplate(ctx, tmpl)) + + tmpl.Name = "new" + tmpl.Status = store.TemplateStatusArchived + tmpl.Config = &store.TemplateConfig{Model: "opus"} + require.NoError(t, ts.UpdateTemplate(ctx, tmpl)) + + got, err := ts.GetTemplate(ctx, tmpl.ID) + require.NoError(t, err) + assert.Equal(t, "new", got.Name) + assert.Equal(t, store.TemplateStatusArchived, got.Status) + require.NotNil(t, got.Config) + assert.Equal(t, "opus", got.Config.Model) +} + +func TestUpdateTemplateNotFound(t *testing.T) { + ts := newTestTemplateStore(t) + tmpl := &store.Template{ID: uuid.New().String(), Name: "ghost", Slug: "ghost", Harness: "claude", Scope: "global", Status: "active"} + err := ts.UpdateTemplate(context.Background(), tmpl) + assert.ErrorIs(t, err, store.ErrNotFound) +} + +func TestDeleteTemplate(t *testing.T) { + ts := newTestTemplateStore(t) + ctx := context.Background() + + tmpl := &store.Template{ID: uuid.New().String(), Name: "del", Slug: "del", Harness: "claude", Scope: "global"} + require.NoError(t, ts.CreateTemplate(ctx, tmpl)) + require.NoError(t, ts.DeleteTemplate(ctx, tmpl.ID)) + _, err := ts.GetTemplate(ctx, tmpl.ID) + assert.ErrorIs(t, err, store.ErrNotFound) + assert.ErrorIs(t, ts.DeleteTemplate(ctx, tmpl.ID), store.ErrNotFound) +} + +func TestDeleteTemplatesByScope(t *testing.T) { + ts := newTestTemplateStore(t) + ctx := context.Background() + + scopeID := uuid.New().String() + for i, n := range []string{"a", "b", "c"} { + require.NoError(t, ts.CreateTemplate(ctx, &store.Template{ + ID: uuid.New().String(), Name: n, Slug: n, Harness: "claude", + Scope: store.TemplateScopeProject, ScopeID: scopeID, Status: "active", + })) + _ = i + } + // Different scope survives. + require.NoError(t, ts.CreateTemplate(ctx, &store.Template{ + ID: uuid.New().String(), Name: "other", Slug: "other", Harness: "claude", + Scope: store.TemplateScopeProject, ScopeID: uuid.New().String(), Status: "active", + })) + + n, err := ts.DeleteTemplatesByScope(ctx, store.TemplateScopeProject, scopeID) + require.NoError(t, err) + assert.Equal(t, 3, n) +} + +func TestListTemplatesPagination(t *testing.T) { + ts := newTestTemplateStore(t) + ctx := context.Background() + + for i := 0; i < 5; i++ { + require.NoError(t, ts.CreateTemplate(ctx, &store.Template{ + ID: uuid.New().String(), Name: uuid.NewString(), Slug: uuid.NewString(), Harness: "claude", Scope: "global", Status: "active", + })) + } + + all, err := ts.ListTemplates(ctx, store.TemplateFilter{}, store.ListOptions{}) + require.NoError(t, err) + assert.Len(t, all.Items, 5) + assert.Equal(t, 5, all.TotalCount) + + page, err := ts.ListTemplates(ctx, store.TemplateFilter{}, store.ListOptions{Limit: 2}) + require.NoError(t, err) + assert.Len(t, page.Items, 2) + assert.Equal(t, 5, page.TotalCount, "TotalCount independent of limit") +} + +func TestListTemplatesFilterByHarness(t *testing.T) { + ts := newTestTemplateStore(t) + ctx := context.Background() + + require.NoError(t, ts.CreateTemplate(ctx, &store.Template{ID: uuid.New().String(), Name: "c", Slug: "c", Harness: "claude", Scope: "global", Status: "active"})) + require.NoError(t, ts.CreateTemplate(ctx, &store.Template{ID: uuid.New().String(), Name: "g", Slug: "g", Harness: "gemini", Scope: "global", Status: "active"})) + + res, err := ts.ListTemplates(ctx, store.TemplateFilter{Harness: "gemini"}, store.ListOptions{}) + require.NoError(t, err) + require.Len(t, res.Items, 1) + assert.Equal(t, "gemini", res.Items[0].Harness) +} + +// TestListTemplatesProjectScopeIncludesGlobal verifies the projectId-without-scope +// filter returns global plus the project's own templates, but not other projects'. +func TestListTemplatesProjectScopeIncludesGlobal(t *testing.T) { + ts := newTestTemplateStore(t) + ctx := context.Background() + + projectID := uuid.New().String() + require.NoError(t, ts.CreateTemplate(ctx, &store.Template{ID: uuid.New().String(), Name: "global1", Slug: "global1", Harness: "claude", Scope: store.TemplateScopeGlobal, Status: "active"})) + require.NoError(t, ts.CreateTemplate(ctx, &store.Template{ID: uuid.New().String(), Name: "proj1", Slug: "proj1", Harness: "claude", Scope: store.TemplateScopeProject, ScopeID: projectID, Status: "active"})) + require.NoError(t, ts.CreateTemplate(ctx, &store.Template{ID: uuid.New().String(), Name: "otherproj", Slug: "otherproj", Harness: "claude", Scope: store.TemplateScopeProject, ScopeID: uuid.New().String(), Status: "active"})) + + res, err := ts.ListTemplates(ctx, store.TemplateFilter{ProjectID: projectID}, store.ListOptions{}) + require.NoError(t, err) + assert.Equal(t, 2, res.TotalCount, "should see global + own project, not other project") +} + +func TestListTemplatesSearch(t *testing.T) { + ts := newTestTemplateStore(t) + ctx := context.Background() + + require.NoError(t, ts.CreateTemplate(ctx, &store.Template{ID: uuid.New().String(), Name: "Production Web", Slug: "prod-web", Harness: "claude", Scope: "global", Status: "active"})) + require.NoError(t, ts.CreateTemplate(ctx, &store.Template{ID: uuid.New().String(), Name: "Staging", Slug: "staging", Harness: "claude", Scope: "global", Status: "active", Description: "production-like"})) + require.NoError(t, ts.CreateTemplate(ctx, &store.Template{ID: uuid.New().String(), Name: "Dev", Slug: "dev", Harness: "claude", Scope: "global", Status: "active"})) + + res, err := ts.ListTemplates(ctx, store.TemplateFilter{Search: "produc"}, store.ListOptions{}) + require.NoError(t, err) + assert.Equal(t, 2, res.TotalCount, "case-insensitive search matches name and description") +} + +// ============================================================================= +// HarnessConfig tests +// ============================================================================= + +func TestCreateAndGetHarnessConfig(t *testing.T) { + ts := newTestTemplateStore(t) + ctx := context.Background() + + hc := &store.HarnessConfig{ + ID: uuid.New().String(), + Name: "claude-web", + Slug: "claude-web", + Harness: "claude", + Scope: store.HarnessConfigScopeGlobal, + ContentHash: "h1", + Config: &store.HarnessConfigData{Harness: "claude", Model: "opus", Env: map[string]string{"A": "B"}}, + } + require.NoError(t, ts.CreateHarnessConfig(ctx, hc)) + assert.Equal(t, store.HarnessConfigStatusActive, hc.Status) + assert.False(t, hc.Created.IsZero()) + + got, err := ts.GetHarnessConfig(ctx, hc.ID) + require.NoError(t, err) + assert.Equal(t, "claude-web", got.Name) + require.NotNil(t, got.Config) + assert.Equal(t, "opus", got.Config.Model) + assert.Equal(t, "B", got.Config.Env["A"]) +} + +func TestGetHarnessConfigBySlug(t *testing.T) { + ts := newTestTemplateStore(t) + ctx := context.Background() + + scopeID := uuid.New().String() + hc := &store.HarnessConfig{ID: uuid.New().String(), Name: "x", Slug: "x", Harness: "claude", Scope: store.HarnessConfigScopeProject, ScopeID: scopeID} + require.NoError(t, ts.CreateHarnessConfig(ctx, hc)) + + got, err := ts.GetHarnessConfigBySlug(ctx, "x", store.HarnessConfigScopeProject, scopeID) + require.NoError(t, err) + assert.Equal(t, hc.ID, got.ID) + + _, err = ts.GetHarnessConfigBySlug(ctx, "x", store.HarnessConfigScopeProject, uuid.New().String()) + assert.ErrorIs(t, err, store.ErrNotFound) +} + +func TestUpdateHarnessConfig(t *testing.T) { + ts := newTestTemplateStore(t) + ctx := context.Background() + + hc := &store.HarnessConfig{ID: uuid.New().String(), Name: "old", Slug: "old", Harness: "claude", Scope: "global", Status: "active"} + require.NoError(t, ts.CreateHarnessConfig(ctx, hc)) + + hc.Name = "new" + hc.Config = &store.HarnessConfigData{Model: "haiku"} + require.NoError(t, ts.UpdateHarnessConfig(ctx, hc)) + + got, err := ts.GetHarnessConfig(ctx, hc.ID) + require.NoError(t, err) + assert.Equal(t, "new", got.Name) + require.NotNil(t, got.Config) + assert.Equal(t, "haiku", got.Config.Model) +} + +func TestUpdateHarnessConfigNotFound(t *testing.T) { + ts := newTestTemplateStore(t) + hc := &store.HarnessConfig{ID: uuid.New().String(), Name: "ghost", Slug: "ghost", Harness: "claude", Scope: "global", Status: "active"} + err := ts.UpdateHarnessConfig(context.Background(), hc) + assert.ErrorIs(t, err, store.ErrNotFound) +} + +func TestDeleteHarnessConfig(t *testing.T) { + ts := newTestTemplateStore(t) + ctx := context.Background() + + hc := &store.HarnessConfig{ID: uuid.New().String(), Name: "del", Slug: "del", Harness: "claude", Scope: "global"} + require.NoError(t, ts.CreateHarnessConfig(ctx, hc)) + require.NoError(t, ts.DeleteHarnessConfig(ctx, hc.ID)) + _, err := ts.GetHarnessConfig(ctx, hc.ID) + assert.ErrorIs(t, err, store.ErrNotFound) +} + +func TestDeleteHarnessConfigsByScope(t *testing.T) { + ts := newTestTemplateStore(t) + ctx := context.Background() + + scopeID := uuid.New().String() + for _, n := range []string{"a", "b"} { + require.NoError(t, ts.CreateHarnessConfig(ctx, &store.HarnessConfig{ID: uuid.New().String(), Name: n, Slug: n, Harness: "claude", Scope: store.HarnessConfigScopeProject, ScopeID: scopeID, Status: "active"})) + } + n, err := ts.DeleteHarnessConfigsByScope(ctx, store.HarnessConfigScopeProject, scopeID) + require.NoError(t, err) + assert.Equal(t, 2, n) +} + +func TestListHarnessConfigsPaginationAndFilter(t *testing.T) { + ts := newTestTemplateStore(t) + ctx := context.Background() + + for i := 0; i < 4; i++ { + require.NoError(t, ts.CreateHarnessConfig(ctx, &store.HarnessConfig{ID: uuid.New().String(), Name: uuid.NewString(), Slug: uuid.NewString(), Harness: "claude", Scope: "global", Status: "active"})) + } + require.NoError(t, ts.CreateHarnessConfig(ctx, &store.HarnessConfig{ID: uuid.New().String(), Name: "g", Slug: "g", Harness: "gemini", Scope: "global", Status: "active"})) + + all, err := ts.ListHarnessConfigs(ctx, store.HarnessConfigFilter{}, store.ListOptions{}) + require.NoError(t, err) + assert.Equal(t, 5, all.TotalCount) + + page, err := ts.ListHarnessConfigs(ctx, store.HarnessConfigFilter{}, store.ListOptions{Limit: 2}) + require.NoError(t, err) + assert.Len(t, page.Items, 2) + assert.Equal(t, 5, page.TotalCount) + + gemini, err := ts.ListHarnessConfigs(ctx, store.HarnessConfigFilter{Harness: "gemini"}, store.ListOptions{}) + require.NoError(t, err) + require.Len(t, gemini.Items, 1) + assert.Equal(t, "gemini", gemini.Items[0].Harness) +} diff --git a/pkg/store/entadapter/user_allowlist_behavior_test.go b/pkg/store/entadapter/user_allowlist_behavior_test.go new file mode 100644 index 000000000..27adf0133 --- /dev/null +++ b/pkg/store/entadapter/user_allowlist_behavior_test.go @@ -0,0 +1,218 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//go:build !no_sqlite + +package entadapter + +import ( + "context" + "testing" + "time" + + "github.com/GoogleCloudPlatform/scion/pkg/ent" + "github.com/GoogleCloudPlatform/scion/pkg/store" + "github.com/GoogleCloudPlatform/scion/pkg/store/enttest" + "github.com/google/uuid" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func newTestEntClient(t *testing.T) *ent.Client { + t.Helper() + client := enttest.NewClient(t) + return client +} + +// TestUserStore_EmailCaseInsensitive verifies that email uniqueness and lookup +// are case-insensitive, preserving the legacy COLLATE NOCASE semantics. +func TestUserStore_EmailCaseInsensitive(t *testing.T) { + ctx := context.Background() + us := NewUserStore(newTestEntClient(t)) + + require.NoError(t, us.CreateUser(ctx, &store.User{ + ID: uuid.NewString(), + Email: "Mixed.Case@Example.com", + DisplayName: "Mixed", + Role: store.UserRoleMember, + Status: "active", + })) + + // Lookup with a different case must find the same user. + got, err := us.GetUserByEmail(ctx, "mixed.case@EXAMPLE.COM") + require.NoError(t, err) + assert.Equal(t, "mixed.case@example.com", got.Email, "email is normalized to lower case") + + // A case-variant insert must collide on the unique index. + err = us.CreateUser(ctx, &store.User{ + ID: uuid.NewString(), + Email: "MIXED.CASE@example.com", + DisplayName: "Dup", + Role: store.UserRoleMember, + Status: "active", + }) + assert.ErrorIs(t, err, store.ErrAlreadyExists) +} + +// TestUserStore_UpdateLastSeen verifies the dedicated last_seen mutator. +func TestUserStore_UpdateLastSeen(t *testing.T) { + ctx := context.Background() + us := NewUserStore(newTestEntClient(t)) + + id := uuid.NewString() + require.NoError(t, us.CreateUser(ctx, &store.User{ + ID: id, Email: "seen@example.com", DisplayName: "Seen", + Role: store.UserRoleMember, Status: "active", + })) + + ts := time.Now().Add(-time.Hour).UTC().Truncate(time.Second) + require.NoError(t, us.UpdateUserLastSeen(ctx, id, ts)) + + got, err := us.GetUser(ctx, id) + require.NoError(t, err) + assert.WithinDuration(t, ts, got.LastSeen, time.Second) + + // Missing user → ErrNotFound. + assert.ErrorIs(t, us.UpdateUserLastSeen(ctx, uuid.NewString(), ts), store.ErrNotFound) +} + +// TestAllowList_BulkAddIdempotent verifies INSERT-OR-IGNORE semantics: emails +// already present or repeated within the batch are skipped, not errored. +func TestAllowList_BulkAddIdempotent(t *testing.T) { + ctx := context.Background() + as := NewAllowListStore(newTestEntClient(t)) + + require.NoError(t, as.AddAllowListEntry(ctx, &store.AllowListEntry{ + ID: uuid.NewString(), Email: "existing@example.com", AddedBy: "admin", + })) + + entries := []*store.AllowListEntry{ + {ID: uuid.NewString(), Email: "EXISTING@example.com", AddedBy: "admin"}, // dup of existing (case-insensitive) + {ID: uuid.NewString(), Email: "new1@example.com", AddedBy: "admin"}, + {ID: uuid.NewString(), Email: "new2@example.com", AddedBy: "admin"}, + {ID: uuid.NewString(), Email: "New1@example.com", AddedBy: "admin"}, // dup within batch + } + added, skipped, err := as.BulkAddAllowListEntries(ctx, entries) + require.NoError(t, err) + assert.Equal(t, 2, added, "new1 and new2 are added") + assert.Equal(t, 2, skipped, "existing and the repeated new1 are skipped") + + ok, err := as.IsEmailAllowListed(ctx, "NEW2@EXAMPLE.COM") + require.NoError(t, err) + assert.True(t, ok) +} + +// TestInvite_IncrementUseCount verifies the conditional increment honors +// revoked, expired, and max-uses guards. +func TestInvite_IncrementUseCount(t *testing.T) { + ctx := context.Background() + as := NewAllowListStore(newTestEntClient(t)) + + mk := func(maxUses int, revoked bool, expires time.Time) string { + id := uuid.NewString() + require.NoError(t, as.CreateInviteCode(ctx, &store.InviteCode{ + ID: id, CodeHash: "h-" + id, CodePrefix: "scion_in", MaxUses: maxUses, + Revoked: revoked, ExpiresAt: expires, CreatedBy: "admin", + })) + return id + } + + future := time.Now().Add(time.Hour) + past := time.Now().Add(-time.Hour) + + // Redeemable: increments up to max_uses, then refuses. + limited := mk(2, false, future) + require.NoError(t, as.IncrementInviteUseCount(ctx, limited)) + require.NoError(t, as.IncrementInviteUseCount(ctx, limited)) + assert.ErrorIs(t, as.IncrementInviteUseCount(ctx, limited), store.ErrNotFound, "exhausted") + + got, err := as.GetInviteCode(ctx, limited) + require.NoError(t, err) + assert.Equal(t, 2, got.UseCount) + + // Unlimited (max_uses == 0) keeps incrementing. + unlimited := mk(0, false, future) + require.NoError(t, as.IncrementInviteUseCount(ctx, unlimited)) + require.NoError(t, as.IncrementInviteUseCount(ctx, unlimited)) + + // Revoked and expired codes are not redeemable. + assert.ErrorIs(t, as.IncrementInviteUseCount(ctx, mk(5, true, future)), store.ErrNotFound) + assert.ErrorIs(t, as.IncrementInviteUseCount(ctx, mk(5, false, past)), store.ErrNotFound) +} + +// TestInvite_GetStats verifies aggregate stats over invite codes and the allow +// list. +func TestInvite_GetStats(t *testing.T) { + ctx := context.Background() + as := NewAllowListStore(newTestEntClient(t)) + + future := time.Now().Add(time.Hour) + past := time.Now().Add(-time.Hour) + + // Pending (redeemed once), exhausted, and expired codes. + mk := func(maxUses, useCount int, revoked bool, expires time.Time) { + id := uuid.NewString() + require.NoError(t, as.CreateInviteCode(ctx, &store.InviteCode{ + ID: id, CodeHash: "h-" + id, CodePrefix: "scion_in", MaxUses: maxUses, + UseCount: useCount, Revoked: revoked, ExpiresAt: expires, CreatedBy: "admin", + })) + } + mk(5, 2, false, future) // pending, 2 redemptions + mk(1, 1, false, future) // exhausted, 1 redemption + mk(5, 3, false, past) // expired, 3 redemptions + + require.NoError(t, as.AddAllowListEntry(ctx, &store.AllowListEntry{ + ID: uuid.NewString(), Email: "a@example.com", AddedBy: "admin", + })) + + stats, err := as.GetInviteStats(ctx) + require.NoError(t, err) + assert.Equal(t, 1, stats.PendingInvites, "only the non-expired, non-exhausted code is pending") + assert.Equal(t, 6, stats.TotalRedemptions, "2+1+3") + assert.Equal(t, 1, stats.AllowListCount) + assert.Len(t, stats.RecentRedemptions, 3, "all three have use_count > 0") +} + +// TestAllowList_WithInvites verifies the manual join enriching entries with +// invite details. +func TestAllowList_WithInvites(t *testing.T) { + ctx := context.Background() + as := NewAllowListStore(newTestEntClient(t)) + + inviteID := uuid.NewString() + require.NoError(t, as.CreateInviteCode(ctx, &store.InviteCode{ + ID: inviteID, CodeHash: "h-" + inviteID, CodePrefix: "scion_inv_abc", + MaxUses: 10, UseCount: 4, ExpiresAt: time.Now().Add(time.Hour), CreatedBy: "admin", + })) + require.NoError(t, as.AddAllowListEntry(ctx, &store.AllowListEntry{ + ID: uuid.NewString(), Email: "linked@example.com", AddedBy: "admin", InviteID: inviteID, + })) + require.NoError(t, as.AddAllowListEntry(ctx, &store.AllowListEntry{ + ID: uuid.NewString(), Email: "unlinked@example.com", AddedBy: "admin", + })) + + res, err := as.ListAllowListEntriesWithInvites(ctx, store.ListOptions{}) + require.NoError(t, err) + require.Len(t, res.Items, 2) + + byEmail := map[string]store.AllowListEntryWithInvite{} + for _, e := range res.Items { + byEmail[e.Email] = e + } + linked := byEmail["linked@example.com"] + assert.Equal(t, "scion_inv_abc", linked.InviteCodePrefix) + assert.Equal(t, 10, linked.InviteMaxUses) + assert.Equal(t, 4, linked.InviteUseCount) + assert.Empty(t, byEmail["unlinked@example.com"].InviteCodePrefix) +} diff --git a/pkg/store/entadapter/user_allowlist_oracle_test.go b/pkg/store/entadapter/user_allowlist_oracle_test.go new file mode 100644 index 000000000..35cc5f896 --- /dev/null +++ b/pkg/store/entadapter/user_allowlist_oracle_test.go @@ -0,0 +1,144 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//go:build !no_sqlite + +package entadapter + +import ( + "context" + "testing" + "time" + + "github.com/GoogleCloudPlatform/scion/pkg/store" + "github.com/GoogleCloudPlatform/scion/pkg/store/enttest" + "github.com/GoogleCloudPlatform/scion/pkg/store/storetest" +) + +// entUserAllowStore is a test-only store that routes the user and +// allowlist/invite domains to the Ent-backed adapters, while delegating +// everything else to the embedded CompositeStore. It previews the wiring that +// P2-collapse will fold into composite.go, letting the shared CRUD-parity +// oracle run directly against the new adapters. +type entUserAllowStore struct { + *CompositeStore + users *UserStore + allow *AllowListStore +} + +// UserStore overrides. + +func (s *entUserAllowStore) CreateUser(ctx context.Context, u *store.User) error { + return s.users.CreateUser(ctx, u) +} +func (s *entUserAllowStore) GetUser(ctx context.Context, id string) (*store.User, error) { + return s.users.GetUser(ctx, id) +} +func (s *entUserAllowStore) GetUserByEmail(ctx context.Context, email string) (*store.User, error) { + return s.users.GetUserByEmail(ctx, email) +} +func (s *entUserAllowStore) UpdateUser(ctx context.Context, u *store.User) error { + return s.users.UpdateUser(ctx, u) +} +func (s *entUserAllowStore) UpdateUserLastSeen(ctx context.Context, id string, t time.Time) error { + return s.users.UpdateUserLastSeen(ctx, id, t) +} +func (s *entUserAllowStore) DeleteUser(ctx context.Context, id string) error { + return s.users.DeleteUser(ctx, id) +} +func (s *entUserAllowStore) ListUsers(ctx context.Context, filter store.UserFilter, opts store.ListOptions) (*store.ListResult[store.User], error) { + return s.users.ListUsers(ctx, filter, opts) +} + +// AllowListStore overrides. + +func (s *entUserAllowStore) AddAllowListEntry(ctx context.Context, entry *store.AllowListEntry) error { + return s.allow.AddAllowListEntry(ctx, entry) +} +func (s *entUserAllowStore) RemoveAllowListEntry(ctx context.Context, email string) error { + return s.allow.RemoveAllowListEntry(ctx, email) +} +func (s *entUserAllowStore) GetAllowListEntry(ctx context.Context, email string) (*store.AllowListEntry, error) { + return s.allow.GetAllowListEntry(ctx, email) +} +func (s *entUserAllowStore) ListAllowListEntries(ctx context.Context, opts store.ListOptions) (*store.ListResult[store.AllowListEntry], error) { + return s.allow.ListAllowListEntries(ctx, opts) +} +func (s *entUserAllowStore) IsEmailAllowListed(ctx context.Context, email string) (bool, error) { + return s.allow.IsEmailAllowListed(ctx, email) +} +func (s *entUserAllowStore) BulkAddAllowListEntries(ctx context.Context, entries []*store.AllowListEntry) (int, int, error) { + return s.allow.BulkAddAllowListEntries(ctx, entries) +} +func (s *entUserAllowStore) ListEmailDomains(ctx context.Context) ([]string, error) { + return s.allow.ListEmailDomains(ctx) +} +func (s *entUserAllowStore) UpdateAllowListEntryInviteID(ctx context.Context, email string, inviteID string) error { + return s.allow.UpdateAllowListEntryInviteID(ctx, email, inviteID) +} +func (s *entUserAllowStore) ListAllowListEntriesWithInvites(ctx context.Context, opts store.ListOptions) (*store.ListResult[store.AllowListEntryWithInvite], error) { + return s.allow.ListAllowListEntriesWithInvites(ctx, opts) +} + +// InviteCodeStore overrides. + +func (s *entUserAllowStore) CreateInviteCode(ctx context.Context, invite *store.InviteCode) error { + return s.allow.CreateInviteCode(ctx, invite) +} +func (s *entUserAllowStore) GetInviteCodeByHash(ctx context.Context, codeHash string) (*store.InviteCode, error) { + return s.allow.GetInviteCodeByHash(ctx, codeHash) +} +func (s *entUserAllowStore) GetInviteCode(ctx context.Context, id string) (*store.InviteCode, error) { + return s.allow.GetInviteCode(ctx, id) +} +func (s *entUserAllowStore) ListInviteCodes(ctx context.Context, opts store.ListOptions) (*store.ListResult[store.InviteCode], error) { + return s.allow.ListInviteCodes(ctx, opts) +} +func (s *entUserAllowStore) IncrementInviteUseCount(ctx context.Context, id string) error { + return s.allow.IncrementInviteUseCount(ctx, id) +} +func (s *entUserAllowStore) RevokeInviteCode(ctx context.Context, id string) error { + return s.allow.RevokeInviteCode(ctx, id) +} +func (s *entUserAllowStore) DeleteInviteCode(ctx context.Context, id string) error { + return s.allow.DeleteInviteCode(ctx, id) +} +func (s *entUserAllowStore) GetInviteStats(ctx context.Context) (*store.InviteStats, error) { + return s.allow.GetInviteStats(ctx) +} + +// entUserAllowFactory builds a store backed by the Ent adapters for the user +// and allowlist/invite domains. +func entUserAllowFactory(t *testing.T) store.Store { + t.Helper() + + entClient := enttest.NewClient(t) + + cs := NewCompositeStore(entClient) + t.Cleanup(func() { _ = cs.Close() }) + + return &entUserAllowStore{ + CompositeStore: cs, + users: NewUserStore(entClient), + allow: NewAllowListStore(entClient), + } +} + +// TestEntAdapter_UserAllowlist_CRUDParity runs the shared CRUD-parity oracle +// against the Ent-backed user and allowlist/invite adapters. +func TestEntAdapter_UserAllowlist_CRUDParity(t *testing.T) { + storetest.RunDomain(t, entUserAllowFactory, storetest.UserDomain()) + storetest.RunDomain(t, entUserAllowFactory, storetest.AllowListDomain()) + storetest.RunDomain(t, entUserAllowFactory, storetest.InviteCodeDomain()) +} diff --git a/pkg/store/entadapter/user_store.go b/pkg/store/entadapter/user_store.go new file mode 100644 index 000000000..b48d1cbdb --- /dev/null +++ b/pkg/store/entadapter/user_store.go @@ -0,0 +1,326 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package entadapter + +import ( + "context" + "strconv" + "strings" + "time" + + "entgo.io/ent/dialect/sql" + "github.com/GoogleCloudPlatform/scion/pkg/ent" + entschema "github.com/GoogleCloudPlatform/scion/pkg/ent/schema" + "github.com/GoogleCloudPlatform/scion/pkg/ent/user" + "github.com/GoogleCloudPlatform/scion/pkg/store" +) + +// sortOpt returns the ent ordering option for the given sort direction, +// defaulting to descending (newest first) to match the legacy SQLite store. +func sortOpt(dir string) sql.OrderTermOption { + if dir == "asc" { + return sql.OrderAsc() + } + return sql.OrderDesc() +} + +// UserStore implements store.UserStore using Ent ORM. +type UserStore struct { + client *ent.Client +} + +// NewUserStore creates a new Ent-backed UserStore. +func NewUserStore(client *ent.Client) *UserStore { + return &UserStore{client: client} +} + +// normalizeEmail lower-cases an email so that the plain unique index on the +// email column enforces case-insensitive uniqueness. The legacy SQLite schema +// used UNIQUE COLLATE NOCASE; Postgres has no NOCASE collation, so we normalize +// at the port layer instead of relying on a functional lower(email) index that +// ent codegen + AutoMigrate cannot emit across both dialects. +func normalizeEmail(email string) string { + return strings.ToLower(strings.TrimSpace(email)) +} + +// storePrefsToEnt converts a store.UserPreferences to the ent schema type. +func storePrefsToEnt(p *store.UserPreferences) *entschema.UserPreferences { + if p == nil { + return nil + } + return &entschema.UserPreferences{ + DefaultTemplate: p.DefaultTemplate, + DefaultProfile: p.DefaultProfile, + Theme: p.Theme, + } +} + +// entPrefsToStore converts an ent schema UserPreferences to the store type. +func entPrefsToStore(p *entschema.UserPreferences) *store.UserPreferences { + if p == nil { + return nil + } + return &store.UserPreferences{ + DefaultTemplate: p.DefaultTemplate, + DefaultProfile: p.DefaultProfile, + Theme: p.Theme, + } +} + +// entUserToStore converts an Ent User entity to a store.User model. +func entUserToStore(u *ent.User) *store.User { + su := &store.User{ + ID: u.ID.String(), + Email: u.Email, + DisplayName: u.DisplayName, + AvatarURL: u.AvatarURL, + Role: string(u.Role), + Status: string(u.Status), + Preferences: entPrefsToStore(u.Preferences), + Created: u.Created, + } + if u.LastLogin != nil { + su.LastLogin = *u.LastLogin + } + if u.LastSeen != nil { + su.LastSeen = *u.LastSeen + } + return su +} + +// CreateUser creates a new user record. +func (s *UserStore) CreateUser(ctx context.Context, u *store.User) error { + uid, err := parseUUID(u.ID) + if err != nil { + return err + } + + if u.Created.IsZero() { + u.Created = time.Now() + } + u.Email = normalizeEmail(u.Email) + + create := s.client.User.Create(). + SetID(uid). + SetEmail(u.Email). + SetDisplayName(u.DisplayName). + SetCreated(u.Created) + + if u.AvatarURL != "" { + create.SetAvatarURL(u.AvatarURL) + } + // Role and Status fall back to the schema defaults (member/active) when the + // caller leaves them empty, matching how the enum validation expects a + // non-empty value. + if u.Role != "" { + create.SetRole(user.Role(u.Role)) + } + if u.Status != "" { + create.SetStatus(user.Status(u.Status)) + } + if u.Preferences != nil { + create.SetPreferences(storePrefsToEnt(u.Preferences)) + } + if !u.LastLogin.IsZero() { + create.SetLastLogin(u.LastLogin) + } + if !u.LastSeen.IsZero() { + create.SetLastSeen(u.LastSeen) + } + + created, err := create.Save(ctx) + if err != nil { + return mapError(err) + } + + u.Created = created.Created + return nil +} + +// GetUser retrieves a user by ID. +func (s *UserStore) GetUser(ctx context.Context, id string) (*store.User, error) { + uid, err := parseGetID(id) + if err != nil { + return nil, err + } + + u, err := s.client.User.Get(ctx, uid) + if err != nil { + return nil, mapError(err) + } + return entUserToStore(u), nil +} + +// GetUserByEmail retrieves a user by email using a case-insensitive match, +// preserving the COLLATE NOCASE semantics of the legacy SQLite schema. +func (s *UserStore) GetUserByEmail(ctx context.Context, email string) (*store.User, error) { + u, err := s.client.User.Query(). + Where(user.EmailEqualFold(normalizeEmail(email))). + Only(ctx) + if err != nil { + return nil, mapError(err) + } + return entUserToStore(u), nil +} + +// UpdateUser updates an existing user. +func (s *UserStore) UpdateUser(ctx context.Context, u *store.User) error { + uid, err := parseUUID(u.ID) + if err != nil { + return err + } + + u.Email = normalizeEmail(u.Email) + + update := s.client.User.UpdateOneID(uid). + SetEmail(u.Email). + SetDisplayName(u.DisplayName) + + if u.AvatarURL != "" { + update.SetAvatarURL(u.AvatarURL) + } else { + update.ClearAvatarURL() + } + if u.Role != "" { + update.SetRole(user.Role(u.Role)) + } + if u.Status != "" { + update.SetStatus(user.Status(u.Status)) + } + if u.Preferences != nil { + update.SetPreferences(storePrefsToEnt(u.Preferences)) + } else { + update.ClearPreferences() + } + if !u.LastLogin.IsZero() { + update.SetLastLogin(u.LastLogin) + } else { + update.ClearLastLogin() + } + if !u.LastSeen.IsZero() { + update.SetLastSeen(u.LastSeen) + } else { + update.ClearLastSeen() + } + + if err := update.Exec(ctx); err != nil { + return mapError(err) + } + return nil +} + +// UpdateUserLastSeen sets only the last_seen timestamp for a user. +func (s *UserStore) UpdateUserLastSeen(ctx context.Context, id string, t time.Time) error { + uid, err := parseUUID(id) + if err != nil { + return err + } + if err := s.client.User.UpdateOneID(uid).SetLastSeen(t).Exec(ctx); err != nil { + return mapError(err) + } + return nil +} + +// DeleteUser removes a user by ID. +func (s *UserStore) DeleteUser(ctx context.Context, id string) error { + uid, err := parseUUID(id) + if err != nil { + return err + } + if err := s.client.User.DeleteOneID(uid).Exec(ctx); err != nil { + return mapError(err) + } + return nil +} + +// ListUsers returns users matching the filter criteria. Pagination is +// offset-based to match the legacy SQLite store: the cursor is the integer +// offset of the next page. +func (s *UserStore) ListUsers(ctx context.Context, filter store.UserFilter, opts store.ListOptions) (*store.ListResult[store.User], error) { + query := s.client.User.Query() + + if filter.Role != "" { + query.Where(user.RoleEQ(user.Role(filter.Role))) + } + if filter.Status != "" { + query.Where(user.StatusEQ(user.Status(filter.Status))) + } + if filter.Search != "" { + query.Where(user.Or( + user.EmailContainsFold(filter.Search), + user.DisplayNameContainsFold(filter.Search), + )) + } + + totalCount, err := query.Clone().Count(ctx) + if err != nil { + return nil, err + } + + limit := opts.Limit + if limit <= 0 { + limit = 50 + } + if limit > 200 { + limit = 200 + } + + offset := 0 + if opts.Cursor != "" { + if parsed, err := strconv.Atoi(opts.Cursor); err == nil && parsed > 0 { + offset = parsed + } + } + + // Map the sort field to an ordering, whitelisting the supported columns. + var order user.OrderOption + switch opts.SortBy { + case "name": + // Name defaults to ascending unless an explicit direction is given. + if opts.SortDir == "desc" { + order = user.ByDisplayName(sql.OrderDesc()) + } else { + order = user.ByDisplayName(sql.OrderAsc()) + } + case "lastSeen": + order = user.ByLastSeen(sortOpt(opts.SortDir)) + default: // "created" and unspecified + order = user.ByCreated(sortOpt(opts.SortDir)) + } + + users, err := query. + Order(order). + Limit(limit + 1). + Offset(offset). + All(ctx) + if err != nil { + return nil, err + } + + items := make([]store.User, 0, len(users)) + for _, u := range users { + items = append(items, *entUserToStore(u)) + } + + result := &store.ListResult[store.User]{ + Items: items, + TotalCount: totalCount, + } + if len(items) > limit { + result.Items = items[:limit] + result.NextCursor = strconv.Itoa(offset + limit) + } + return result, nil +} diff --git a/pkg/store/enttest/enttest.go b/pkg/store/enttest/enttest.go new file mode 100644 index 000000000..867550ef5 --- /dev/null +++ b/pkg/store/enttest/enttest.go @@ -0,0 +1,73 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Package enttest provides a single backend-selecting factory for Ent clients +// used by the store test suites. By default it returns an in-memory SQLite +// client; built with the `integration` tag and with SCION_TEST_POSTGRES_URL +// set, it returns a Postgres-backed client isolated in its own schema inside a +// per-package ephemeral database. +// +// This lets the same store tests (pkg/store/storetest, pkg/store/entadapter) +// run unchanged against both backends, proving CRUD parity between SQLite and +// Postgres. +// +// Lifecycle: +// - Each test package wires MainSetup/MainTeardown into its TestMain so the +// per-package ephemeral Postgres database is created once and dropped once. +// Both are no-ops in the default (SQLite) build. +// - NewClient(t) returns a fresh, migrated *ent.Client per call with cleanup +// registered via t.Cleanup. Under Postgres each call gets its own schema so +// tests never observe each other's rows. +package enttest + +import ( + "context" + "testing" + + "github.com/GoogleCloudPlatform/scion/pkg/ent" + "github.com/GoogleCloudPlatform/scion/pkg/ent/entc" +) + +// NewClient returns a fresh, migrated Ent client for the active backend with +// cleanup registered on t. See the package doc for backend selection. +func NewClient(t *testing.T) *ent.Client { + t.Helper() + return newClient(t) +} + +// MainSetup prepares package-level backend resources. Call from TestMain before +// m.Run(). No-op for the SQLite backend. +func MainSetup() { setup() } + +// MainTeardown releases package-level backend resources. Call from TestMain +// after m.Run(). No-op for the SQLite backend. +func MainTeardown() { teardown() } + +// newSQLiteClient opens an in-memory SQLite-backed Ent client, migrates it, and +// registers cleanup. It is the default backend and the fallback used by the +// integration build when SCION_TEST_POSTGRES_URL is unset. MaxOpenConns is +// pinned to 1 so the shared-cache in-memory database serializes writes, matching +// production SQLite behavior. +func newSQLiteClient(t *testing.T) *ent.Client { + t.Helper() + client, err := entc.OpenSQLite("file:"+t.Name()+"?mode=memory&cache=shared", entc.PoolConfig{MaxOpenConns: 1}) + if err != nil { + t.Fatalf("open sqlite ent client: %v", err) + } + t.Cleanup(func() { _ = client.Close() }) + if err := entc.AutoMigrate(context.Background(), client); err != nil { + t.Fatalf("migrate sqlite ent client: %v", err) + } + return client +} diff --git a/pkg/store/enttest/enttest_postgres.go b/pkg/store/enttest/enttest_postgres.go new file mode 100644 index 000000000..68a1fe5a5 --- /dev/null +++ b/pkg/store/enttest/enttest_postgres.go @@ -0,0 +1,316 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//go:build integration + +// This file implements the Postgres test backend. It is compiled only with the +// `integration` build tag and is active only when SCION_TEST_POSTGRES_URL is +// set; otherwise NewClient transparently falls back to SQLite so the suite still +// runs under `go test -tags integration ./...`. +// +// go test -tags integration -run TestCompositeStore_CRUDParity \ +// ./pkg/store/... \ +// with SCION_TEST_POSTGRES_URL=postgres://user:pass@host:5432/db?sslmode=require +// +// Isolation model: +// - One ephemeral database is created per test package (MainSetup) and dropped +// when the package finishes (MainTeardown) so concurrent runs never collide. +// - Each NewClient call creates a uniquely-named schema inside that database +// and points the Ent client's search_path at it, so every test gets a fresh, +// empty set of tables and cannot observe rows created by other tests. The +// schema is dropped (CASCADE) on test cleanup. +package enttest + +import ( + "context" + "database/sql" + "log" + "net/url" + "os" + "sort" + "strings" + "testing" + + "github.com/google/uuid" + + "github.com/GoogleCloudPlatform/scion/pkg/ent" + "github.com/GoogleCloudPlatform/scion/pkg/ent/entc" + + // pgx stdlib driver registration ("pgx"). entc/driver_postgres.go also + // imports it, but we keep it explicit so this file is self-describing. + _ "github.com/jackc/pgx/v5/stdlib" +) + +// postgresURL is the operator-supplied connection string for the Postgres +// server. When empty, the backend is inactive and NewClient falls back to +// SQLite. +var postgresURL = os.Getenv("SCION_TEST_POSTGRES_URL") + +var ( + // active reports whether a per-package ephemeral database was provisioned. + active bool + // pkgDBName is the name of the per-package ephemeral database. + pkgDBName string + // adminDB is connected to the per-package database and is used to create + // and drop the per-test schemas. + adminDB *sql.DB +) + +// setup provisions the per-package ephemeral database. It is fatal on error so +// that a misconfigured integration run fails loudly rather than silently +// degrading to SQLite. +func setup() { + if postgresURL == "" { + return + } + + server, err := sql.Open("pgx", postgresURL) + if err != nil { + log.Fatalf("enttest: opening postgres server connection: %v", err) + } + defer server.Close() + if err := server.Ping(); err != nil { + log.Fatalf("enttest: pinging postgres server: %v", err) + } + + pkgDBName = "scion_test_" + hexID() + if _, err := server.Exec("CREATE DATABASE " + pkgDBName); err != nil { + log.Fatalf("enttest: creating ephemeral database %s: %v", pkgDBName, err) + } + + dbURL, err := rewriteDatabase(postgresURL, pkgDBName) + if err != nil { + log.Fatalf("enttest: building ephemeral database URL: %v", err) + } + adminDB, err = sql.Open("pgx", dbURL) + if err != nil { + log.Fatalf("enttest: opening ephemeral database connection: %v", err) + } + if err := adminDB.Ping(); err != nil { + log.Fatalf("enttest: pinging ephemeral database: %v", err) + } + + active = true + log.Printf("enttest: provisioned ephemeral postgres database %s", pkgDBName) +} + +// teardown drops the per-package ephemeral database. +func teardown() { + if !active { + return + } + if adminDB != nil { + _ = adminDB.Close() + adminDB = nil + } + server, err := sql.Open("pgx", postgresURL) + if err != nil { + log.Printf("enttest: warning: reopening server to drop %s: %v", pkgDBName, err) + return + } + defer server.Close() + // FORCE terminates any lingering connections so the drop cannot hang. + if _, err := server.Exec("DROP DATABASE IF EXISTS " + pkgDBName + " WITH (FORCE)"); err != nil { + log.Printf("enttest: warning: dropping ephemeral database %s: %v", pkgDBName, err) + } + active = false +} + +// newClient returns a Postgres-backed client isolated in its own schema, or a +// SQLite client when the Postgres backend is inactive. +func newClient(t *testing.T) *ent.Client { + t.Helper() + if !active { + return newSQLiteClient(t) + } + + schema := "t_" + hexID() + if _, err := adminDB.ExecContext(context.Background(), "CREATE SCHEMA "+schema); err != nil { + t.Fatalf("enttest: creating schema %s: %v", schema, err) + } + + clientURL, err := withSearchPath(postgresURL, pkgDBName, schema) + if err != nil { + t.Fatalf("enttest: building client URL: %v", err) + } + client, err := entc.OpenPostgres(clientURL, entc.PoolConfig{MaxOpenConns: 5, MaxIdleConns: 2}) + if err != nil { + t.Fatalf("enttest: opening postgres ent client: %v", err) + } + t.Cleanup(func() { + _ = client.Close() + if _, err := adminDB.ExecContext(context.Background(), "DROP SCHEMA IF EXISTS "+schema+" CASCADE"); err != nil { + t.Logf("enttest: warning: dropping schema %s: %v", schema, err) + } + }) + + if err := entc.AutoMigrate(context.Background(), client); err != nil { + t.Fatalf("enttest: migrating postgres ent client: %v", err) + } + return client +} + +// Active reports whether a per-package ephemeral Postgres database was +// provisioned (i.e. SCION_TEST_POSTGRES_URL was set and MainSetup succeeded). +// Integration tests that exercise Postgres-only behavior use it to skip cleanly +// when run without a live database. +func Active() bool { return active } + +// NewSchemaURL creates and migrates a fresh, isolated schema inside the +// per-package ephemeral database and returns a connection URL whose search_path +// points at it. Cleanup drops the schema (CASCADE) on test completion. +// +// Unlike NewClient (which hands back a ready *ent.Client with a fixed pool), this +// returns the raw DSN so callers can open their own clients/pools — needed by the +// connection-pool stress tests (custom MaxOpenConns) and the multi-process tests +// (a stable DSN shared with a forked child process). The schema is migrated once +// here so every client opened against the returned URL sees the full table set. +// +// It skips the calling test when the Postgres backend is inactive. +func NewSchemaURL(t *testing.T) string { + t.Helper() + if !active { + t.Skip("enttest: SCION_TEST_POSTGRES_URL not set; skipping Postgres-only integration test") + } + + schema := "t_" + hexID() + if _, err := adminDB.ExecContext(context.Background(), "CREATE SCHEMA "+schema); err != nil { + t.Fatalf("enttest: creating schema %s: %v", schema, err) + } + t.Cleanup(func() { + if _, err := adminDB.ExecContext(context.Background(), "DROP SCHEMA IF EXISTS "+schema+" CASCADE"); err != nil { + t.Logf("enttest: warning: dropping schema %s: %v", schema, err) + } + }) + + clientURL, err := withSearchPath(postgresURL, pkgDBName, schema) + if err != nil { + t.Fatalf("enttest: building schema URL: %v", err) + } + + // Migrate once so the schema is fully provisioned; callers open their own + // clients/pools against clientURL afterwards. + client, err := entc.OpenPostgres(clientURL, entc.PoolConfig{MaxOpenConns: 2, MaxIdleConns: 1}) + if err != nil { + t.Fatalf("enttest: opening migrate client for schema %s: %v", schema, err) + } + if err := entc.AutoMigrate(context.Background(), client); err != nil { + _ = client.Close() + t.Fatalf("enttest: migrating schema %s: %v", schema, err) + } + _ = client.Close() + return clientURL +} + +// hexID returns a 32-char lowercase hex identifier safe to embed in a Postgres +// database or schema name. +func hexID() string { + return strings.ReplaceAll(uuid.NewString(), "-", "") +} + +// rewriteDatabase returns rawURL with its database name replaced by dbName. +// It accepts both URL-style ("postgres://...") and libpq keyword/value +// ("host=... dbname=...") DSNs, mirroring what entc.OpenPostgres accepts. +func rewriteDatabase(rawURL, dbName string) (string, error) { + if isKeywordValueDSN(rawURL) { + m := parseKeywordValueDSN(rawURL) + m["dbname"] = dbName + return buildKeywordValueDSN(m), nil + } + u, err := url.Parse(rawURL) + if err != nil { + return "", err + } + u.Path = "/" + dbName + return u.String(), nil +} + +// withSearchPath returns rawURL pointing at dbName with the connection +// search_path set to schema, so unqualified table creation/queries resolve to +// that schema. It accepts both URL-style and libpq keyword/value DSNs. In both +// forms search_path is carried as a connection runtime parameter (pgx sends any +// unrecognized keyword/query param as a startup GUC). +func withSearchPath(rawURL, dbName, schema string) (string, error) { + if isKeywordValueDSN(rawURL) { + m := parseKeywordValueDSN(rawURL) + m["dbname"] = dbName + m["search_path"] = schema + return buildKeywordValueDSN(m), nil + } + u, err := url.Parse(rawURL) + if err != nil { + return "", err + } + u.Path = "/" + dbName + q := u.Query() + q.Set("search_path", schema) + u.RawQuery = q.Encode() + return u.String(), nil +} + +// WithConnParam returns dsn with the connection parameter key set to value, +// accepting both URL-style and libpq keyword/value DSNs. It is used by tests that +// need to attach an extra parameter (e.g. application_name) to the DSN returned by +// NewSchemaURL without assuming a particular DSN format. +func WithConnParam(dsn, key, value string) (string, error) { + if isKeywordValueDSN(dsn) { + m := parseKeywordValueDSN(dsn) + m[key] = value + return buildKeywordValueDSN(m), nil + } + u, err := url.Parse(dsn) + if err != nil { + return "", err + } + q := u.Query() + q.Set(key, value) + u.RawQuery = q.Encode() + return u.String(), nil +} + +// isKeywordValueDSN reports whether dsn is a libpq keyword/value connection +// string rather than a URL. URL DSNs contain a scheme separator ("://"); the +// keyword/value form ("host=... dbname=...") does not. +func isKeywordValueDSN(dsn string) bool { + return !strings.Contains(dsn, "://") +} + +// parseKeywordValueDSN parses a libpq keyword/value DSN into a map. It handles +// the unquoted form used by these tests (no spaces inside values); quoting of +// values is not required for the simple host/port/user/password/dbname tokens in +// the test connection string. +func parseKeywordValueDSN(dsn string) map[string]string { + m := make(map[string]string) + for _, field := range strings.Fields(dsn) { + if i := strings.IndexByte(field, '='); i >= 0 { + m[field[:i]] = field[i+1:] + } + } + return m +} + +// buildKeywordValueDSN serializes a keyword/value map back into a libpq DSN. +// Keys are emitted in a stable order so the result is deterministic. +func buildKeywordValueDSN(m map[string]string) string { + keys := make([]string, 0, len(m)) + for k := range m { + keys = append(keys, k) + } + sort.Strings(keys) + parts := make([]string, 0, len(keys)) + for _, k := range keys { + parts = append(parts, k+"="+m[k]) + } + return strings.Join(parts, " ") +} diff --git a/pkg/store/enttest/enttest_sqlite.go b/pkg/store/enttest/enttest_sqlite.go new file mode 100644 index 000000000..0346e3871 --- /dev/null +++ b/pkg/store/enttest/enttest_sqlite.go @@ -0,0 +1,45 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//go:build !integration + +package enttest + +import ( + "testing" + + "github.com/GoogleCloudPlatform/scion/pkg/ent" +) + +// newClient returns an in-memory SQLite-backed client. This is the default +// build; the Postgres backend lives in enttest_postgres.go behind the +// `integration` tag. +func newClient(t *testing.T) *ent.Client { return newSQLiteClient(t) } + +// setup/teardown have nothing to do for the SQLite backend. +func setup() {} +func teardown() {} + +// Active always reports false in the SQLite build: there is no Postgres backend. +func Active() bool { return false } + +// NewSchemaURL has no meaning without the Postgres backend; it skips the calling +// test. The integration build provides the real implementation. The signature +// is kept identical so Postgres-only integration tests reference one symbol +// regardless of build tag. +func NewSchemaURL(t *testing.T) string { + t.Helper() + t.Skip("enttest: Postgres backend not built; rebuild with -tags integration and set SCION_TEST_POSTGRES_URL") + return "" +} diff --git a/pkg/store/integrationtest/README.md b/pkg/store/integrationtest/README.md new file mode 100644 index 000000000..6059a56df --- /dev/null +++ b/pkg/store/integrationtest/README.md @@ -0,0 +1,70 @@ +# Postgres stress / integration test suite + +This package exercises the Postgres-backed store under realistic, adversarial +conditions that **do not exist on the single-writer SQLite backend**: row-level +contention, transaction isolation, connection-pool saturation, LISTEN/NOTIFY +delivery, large-dataset migration, strict type/schema edge cases, and +multi-process coordination. + +It complements — rather than duplicates — the CRUD-parity suites in +`pkg/store/storetest` and `pkg/store/entadapter`, which run the *same* tests +against both backends to prove behavioral parity. Everything here is Postgres-only +and asserts behavior that only a real, concurrent, multi-writer database exhibits. + +## Running + +All tests are gated by the `integration` build tag **and** require a live +Postgres reachable via `SCION_TEST_POSTGRES_URL`. Without that variable every +test skips (and the default `go test ./...` build sees the package as empty). + +```sh +# Local Postgres +SCION_TEST_POSTGRES_URL='postgres://scion:scion@localhost:5432/scion?sslmode=disable' \ + go test -tags integration ./pkg/store/integrationtest/... + +# CloudSQL (e.g. via the auth proxy on localhost) +SCION_TEST_POSTGRES_URL='postgres://USER:PASS@127.0.0.1:5432/DB?sslmode=disable' \ + go test -tags integration -timeout 20m ./pkg/store/integrationtest/... +``` + +The suite provisions one ephemeral database per package run (created and dropped +automatically) and an isolated schema per test, so it never touches existing data +and parallel runs never collide. + +### Knobs + +| Variable | Default | Meaning | +| -------------------------- | ------- | -------------------------------------------------- | +| `SCION_TEST_POSTGRES_URL` | (unset) | Live Postgres DSN; unset ⇒ all tests skip. | +| `SCION_TEST_CONCURRENCY` | `10` | Worker count for contention/pool tests (≥ 2). | + +Target wall-clock: **< 5 min** against a local Postgres, **< 15 min** against +CloudSQL, at the default concurrency. + +## What's covered + +1. **Contention** (`contention_test.go`) — `state_version` CAS race (no lost + updates; ≥ N-1 retries; final version == 1+N), SKIP-LOCKED / conditional-UPDATE + scheduled-event claim (single winner; disjoint drain of a pool), and unique + constraint races on project slug / user email / agent (slug, project_id). +2. **Transaction isolation** (`isolation_test.go`) — SERIALIZABLE conflict with + `RunSerializable` retry recovery, REPEATABLE READ snapshot stability (no + phantom), READ COMMITTED dirty-read prevention. +3. **Connection pool** (`pool_test.go`) — exhaustion + queued recovery, saturated + pool honoring the context deadline, long transaction not starving short + queries, and pool healing after backends are killed with + `pg_terminate_backend`. +4. **LISTEN/NOTIFY** (`notify_test.go`) — ordered burst delivery with no drops, + the 8000-byte payload limit (which motivates the publisher's + reference-and-refetch offload), listener reconnect/resume, and cross-channel + isolation. (The higher-level `PostgresEventPublisher` refetch/resubscribe + behavior is covered in `pkg/hub/events_postgres_test.go`.) +5. **Migration** (`migration_test.go`) — 1000+ row dataset with correct counts + and bounded-memory listing, and idempotent re-migration that preserves data + (the property that makes a killed/restarted migration safe). +6. **Schema / type edge cases** (`schema_test.go`) — NULL semantics, Unicode/emoji + round-trip, nested JSON with special characters, large-text non-truncation, and + TIMESTAMPTZ microsecond precision. +7. **Multi-process** (`multiprocess_test.go`) — forks the test binary so two + separate OS processes contend for an advisory lock (exactly one wins) and a + child-published NOTIFY is delivered to a listener in the parent. diff --git a/pkg/store/integrationtest/contention_test.go b/pkg/store/integrationtest/contention_test.go new file mode 100644 index 000000000..588bd229d --- /dev/null +++ b/pkg/store/integrationtest/contention_test.go @@ -0,0 +1,301 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//go:build integration + +// Category 1 — Contention. These tests put N writers in genuine, simultaneous +// conflict over a single row (or unique key) on a real multi-writer Postgres and +// assert the store's concurrency-control primitives hold: optimistic state_version +// compare-and-swap, the SKIP LOCKED / conditional-UPDATE event claim, and +// database unique constraints. None of this is observable on the single-writer +// SQLite fallback, so every test gates on requirePG via newStore. +package integrationtest + +import ( + "context" + "errors" + "strconv" + "sync" + "sync/atomic" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/GoogleCloudPlatform/scion/pkg/store" +) + +// TestContention_StateVersionCAS races N goroutines to increment a counter on a +// single agent through the optimistic state_version compare-and-swap in +// UpdateAgent. +// +// The workers proceed in two phases. A synchronization barrier forces all N to +// read the SAME starting version before any of them writes, so the first write +// round is guaranteed to produce exactly one winner and N-1 conflicts. Each loser +// then re-reads and retries until it too commits. This makes the lower bound on +// retries deterministic (>= N-1) rather than dependent on goroutine scheduling. +// +// Asserted invariants: +// - exactly N successful UpdateAgent calls (every worker commits once); +// - >= N-1 conflicts observed (real contention occurred); +// - final state_version == initial(1) + N (each commit bumped it exactly once); +// - final counter == N — NO LOST UPDATES (every increment landed on the row). +func TestContention_StateVersionCAS(t *testing.T) { + cs := newStore(t) + ctx := context.Background() + n := concurrency(t) + + project := seedProject(t, cs) + ag := makeAgent(project.ID, "cas-"+shortID()) + require.NoError(t, cs.CreateAgent(ctx, ag)) + require.Equal(t, int64(1), ag.StateVersion, "CreateAgent seeds state_version=1") + + var successes, retries int64 + errs := make(chan error, n) + + // readBarrier releases every worker's first write only after all N have done + // their initial stale read, guaranteeing the first round conflicts N-1 times. + var readBarrier sync.WaitGroup + readBarrier.Add(n) + + bump := func(a *store.Agent) { + cur := 0 + if a.Annotations != nil { + if v, ok := a.Annotations["counter"]; ok { + cur, _ = strconv.Atoi(v) + } + } + a.Annotations = map[string]string{"counter": strconv.Itoa(cur + 1)} + } + + runConcurrently(n, func(int) { + // Phase 1: stale read shared across all workers. + a, err := cs.GetAgent(ctx, ag.ID) + if err != nil { + readBarrier.Done() + errs <- err + return + } + readBarrier.Done() + readBarrier.Wait() // every worker now holds version 1 + + // Phase 2: first (lockstep) write attempt, then retry-until-success. + for { + bump(a) + err = cs.UpdateAgent(ctx, a) + if err == nil { + atomic.AddInt64(&successes, 1) + return + } + if errors.Is(err, store.ErrVersionConflict) { + atomic.AddInt64(&retries, 1) + a, err = cs.GetAgent(ctx, ag.ID) // re-read latest version + if err != nil { + errs <- err + return + } + continue + } + errs <- err + return + } + }) + close(errs) + for err := range errs { + require.NoError(t, err, "unexpected error during CAS contention") + } + + assert.Equal(t, int64(n), successes, "every worker must commit exactly once") + assert.GreaterOrEqualf(t, retries, int64(n-1), + "expected >= N-1 (%d) conflicts under true contention, got %d", n-1, retries) + + final, err := cs.GetAgent(ctx, ag.ID) + require.NoError(t, err) + assert.Equal(t, int64(1+n), final.StateVersion, + "final state_version must equal initial(1) + N commits") + require.NotNil(t, final.Annotations) + assert.Equal(t, strconv.Itoa(n), final.Annotations["counter"], + "counter must equal N — a smaller value means a lost update") + t.Logf("CAS contention: N=%d successes=%d retries=%d finalVersion=%d", + n, successes, retries, final.StateVersion) +} + +// TestContention_ClaimScheduledEventSingleWinner races N goroutines to claim the +// SAME pending scheduled event (ClaimScheduledEvent's conditional +// UPDATE ... WHERE status='pending'). This mirrors N hub replicas each recovering +// the same pending event on startup: exactly one must win the pending->fired +// transition and execute the side effect; the rest must lose cleanly. +func TestContention_ClaimScheduledEventSingleWinner(t *testing.T) { + cs := newStore(t) + ctx := context.Background() + n := concurrency(t) + + project := seedProject(t, cs) + evt := makeScheduledEvent(project.ID) + require.NoError(t, cs.CreateScheduledEvent(ctx, evt)) + + var wins int64 + errs := make(chan error, n) + runConcurrently(n, func(int) { + won, err := cs.ClaimScheduledEvent(ctx, evt.ID, store.ScheduledEventFired) + if err != nil { + errs <- err + return + } + if won { + atomic.AddInt64(&wins, 1) + } + }) + close(errs) + for err := range errs { + require.NoError(t, err, "unexpected error during claim race") + } + + assert.Equal(t, int64(1), wins, "exactly one concurrent claim must win") + + got, err := cs.GetScheduledEvent(ctx, evt.ID) + require.NoError(t, err) + assert.Equal(t, store.ScheduledEventFired, got.Status) + assert.NotNil(t, got.FiredAt) +} + +// TestContention_SkipLockedDisjointClaims drains a pool of M pending events with +// N concurrent pollers, each looping ListPendingScheduledEvents (SELECT ... FOR +// UPDATE SKIP LOCKED on Postgres) followed by ClaimScheduledEvent. The +// SKIP-LOCKED select hands disjoint row sets to overlapping pollers and the +// conditional claim is the final dedup, so the invariant is: every event is +// claimed EXACTLY ONCE in total — no event dropped, none double-fired. +func TestContention_SkipLockedDisjointClaims(t *testing.T) { + cs := newStore(t) + ctx := context.Background() + n := concurrency(t) + m := 5 * n // comfortably more events than pollers + + project := seedProject(t, cs) + for i := 0; i < m; i++ { + require.NoError(t, cs.CreateScheduledEvent(ctx, makeScheduledEvent(project.ID))) + } + + var mu sync.Mutex + claimedBy := make(map[string]int) // event id -> number of winning claims + errs := make(chan error, n) + + runConcurrently(n, func(int) { + for { + pending, err := cs.ListPendingScheduledEvents(ctx) + if err != nil { + errs <- err + return + } + if len(pending) == 0 { + return // nothing left for anyone + } + for _, e := range pending { + won, err := cs.ClaimScheduledEvent(ctx, e.ID, store.ScheduledEventFired) + if err != nil { + errs <- err + return + } + if won { + mu.Lock() + claimedBy[e.ID]++ + mu.Unlock() + } + } + } + }) + close(errs) + for err := range errs { + require.NoError(t, err, "unexpected error during SKIP LOCKED drain") + } + + assert.Len(t, claimedBy, m, "every event must be claimed exactly once (count of distinct winners)") + total := 0 + for id, c := range claimedBy { + assert.Equalf(t, 1, c, "event %s was claimed %d times (want exactly 1)", id, c) + total += c + } + assert.Equal(t, m, total, "total winning claims must equal the number of events") +} + +// TestContention_UniqueProjectSlug races N goroutines to create projects with the +// same slug. The slug unique index must admit exactly one and reject the rest +// with store.ErrAlreadyExists. +func TestContention_UniqueProjectSlug(t *testing.T) { + cs := newStore(t) + ctx := context.Background() + n := concurrency(t) + slug := "dup-project-" + shortID() + + ok, dup := raceUniqueCreate(t, n, func() error { + return cs.CreateProject(ctx, makeProject(slug)) + }) + assert.Equal(t, int64(1), ok, "exactly one project create must succeed") + assert.Equal(t, int64(n-1), dup, "all other creates must return ErrAlreadyExists") +} + +// TestContention_UniqueUserEmail races N goroutines to create users with the same +// (case-insensitively normalized) email. +func TestContention_UniqueUserEmail(t *testing.T) { + cs := newStore(t) + ctx := context.Background() + n := concurrency(t) + email := "dup-" + shortID() + "@example.com" + + ok, dup := raceUniqueCreate(t, n, func() error { + return cs.CreateUser(ctx, makeUser(email)) + }) + assert.Equal(t, int64(1), ok, "exactly one user create must succeed") + assert.Equal(t, int64(n-1), dup, "all other creates must return ErrAlreadyExists") +} + +// TestContention_UniqueAgentSlug races N goroutines to create agents with the same +// (slug, project_id) composite unique key inside one project. +func TestContention_UniqueAgentSlug(t *testing.T) { + cs := newStore(t) + ctx := context.Background() + n := concurrency(t) + project := seedProject(t, cs) + slug := "dup-agent-" + shortID() + + ok, dup := raceUniqueCreate(t, n, func() error { + return cs.CreateAgent(ctx, makeAgent(project.ID, slug)) + }) + assert.Equal(t, int64(1), ok, "exactly one agent create must succeed") + assert.Equal(t, int64(n-1), dup, "all other creates must return ErrAlreadyExists") +} + +// raceUniqueCreate runs create n times concurrently and returns (successes, +// already-exists). Any other error fails the test. Each create must target the +// same unique key but a distinct primary-key id (the factories use a fresh UUID +// per call), so the only possible conflict is the intended unique-key violation. +func raceUniqueCreate(t *testing.T, n int, create func() error) (successes, alreadyExists int64) { + t.Helper() + errs := make(chan error, n) + runConcurrently(n, func(int) { + switch err := create(); { + case err == nil: + atomic.AddInt64(&successes, 1) + case errors.Is(err, store.ErrAlreadyExists): + atomic.AddInt64(&alreadyExists, 1) + default: + errs <- err + } + }) + close(errs) + for err := range errs { + require.NoError(t, err, "unexpected (non-duplicate) error during unique-create race") + } + return successes, alreadyExists +} diff --git a/pkg/store/integrationtest/doc.go b/pkg/store/integrationtest/doc.go new file mode 100644 index 000000000..98dd4e570 --- /dev/null +++ b/pkg/store/integrationtest/doc.go @@ -0,0 +1,34 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Package integrationtest holds the Postgres stress / integration suite. Unlike +// the CRUD-parity suites in pkg/store/storetest and pkg/store/entadapter (which +// run unchanged against both SQLite and Postgres to prove behavioral parity), +// every test here targets behavior that ONLY manifests under a real, multi-writer +// Postgres server: row-level contention, transaction isolation, connection-pool +// saturation, LISTEN/NOTIFY delivery, large-dataset migration, schema/type edge +// cases, and multi-process coordination. +// +// All test files are gated with the `integration` build tag and additionally skip +// at runtime unless SCION_TEST_POSTGRES_URL points at a live Postgres (local or +// CloudSQL); see requirePG. Under the default build only this file compiles, so +// `go test ./...` reports the package as having no tests rather than failing. +// +// go test -tags integration ./pkg/store/integrationtest/... \ +// with SCION_TEST_POSTGRES_URL=postgres://user:pass@host:5432/db?sslmode=disable +// +// Concurrency levels default to small values so the suite finishes well under the +// 5-minute local-Postgres target; set SCION_TEST_CONCURRENCY= to crank them up +// for a heavier stress run. +package integrationtest diff --git a/pkg/store/integrationtest/harness_test.go b/pkg/store/integrationtest/harness_test.go new file mode 100644 index 000000000..1123416d4 --- /dev/null +++ b/pkg/store/integrationtest/harness_test.go @@ -0,0 +1,163 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//go:build integration + +package integrationtest + +import ( + "context" + "os" + "strconv" + "sync" + "testing" + "time" + + "github.com/google/uuid" + "github.com/stretchr/testify/require" + + "github.com/GoogleCloudPlatform/scion/pkg/ent/entc" + "github.com/GoogleCloudPlatform/scion/pkg/store" + "github.com/GoogleCloudPlatform/scion/pkg/store/entadapter" + "github.com/GoogleCloudPlatform/scion/pkg/store/enttest" +) + +// defaultConcurrency is the worker count used by the contention/pool tests when +// SCION_TEST_CONCURRENCY is unset. It is deliberately small so the suite stays +// well under the 5-minute local-Postgres target; bump the env var for stress. +const defaultConcurrency = 10 + +// requirePG skips a test unless a live Postgres backend was provisioned. Every +// test in this package is Postgres-only: the behaviors under test (real row +// locks, MVCC isolation, pool saturation, LISTEN/NOTIFY) do not exist on the +// single-writer in-memory SQLite fallback. +func requirePG(t *testing.T) { + t.Helper() + if !enttest.Active() { + t.Skip("integration: set SCION_TEST_POSTGRES_URL to a live Postgres to run the stress suite") + } +} + +// concurrency returns the worker count for contention tests: the value of +// SCION_TEST_CONCURRENCY if set (>= 2), otherwise defaultConcurrency. +func concurrency(t *testing.T) int { + t.Helper() + v := os.Getenv("SCION_TEST_CONCURRENCY") + if v == "" { + return defaultConcurrency + } + n, err := strconv.Atoi(v) + if err != nil || n < 2 { + t.Fatalf("integration: invalid SCION_TEST_CONCURRENCY=%q (want integer >= 2): %v", v, err) + } + return n +} + +// newStore returns a CompositeStore backed by a fresh, isolated Postgres schema +// with a pool large enough that the suite's concurrent writers genuinely overlap +// (rather than serializing behind a tiny pool). Each call gets its own schema, so +// tests never observe each other's rows; the schema and client are torn down on +// cleanup. +func newStore(t *testing.T) *entadapter.CompositeStore { + return newStoreWithPool(t, 16) +} + +// newStoreWithPool is newStore with an explicit MaxOpenConns, used by the +// connection-pool stress tests that need a known, small pool to saturate. +func newStoreWithPool(t *testing.T, maxOpen int) *entadapter.CompositeStore { + t.Helper() + dsn := enttest.NewSchemaURL(t) // skips when Postgres is inactive + client, err := entc.OpenPostgres(dsn, entc.PoolConfig{MaxOpenConns: maxOpen, MaxIdleConns: maxOpen}) + require.NoError(t, err, "open postgres ent client") + cs := entadapter.NewCompositeStore(client) + t.Cleanup(func() { _ = cs.Close() }) + return cs +} + +// --- entity factories (minimal valid rows for the schema under test) --- + +func makeProject(slug string) *store.Project { + return &store.Project{ + ID: uuid.NewString(), + Name: "project " + slug, + Slug: slug, + Created: time.Now(), + Updated: time.Now(), + } +} + +func makeAgent(projectID, slug string) *store.Agent { + return &store.Agent{ + ID: uuid.NewString(), + Slug: slug, + Name: "agent " + slug, + Template: "default", + ProjectID: projectID, + Phase: "running", + } +} + +func makeUser(email string) *store.User { + return &store.User{ + ID: uuid.NewString(), + Email: email, + DisplayName: "user " + email, + Role: store.UserRoleMember, + Status: "active", + Created: time.Now(), + } +} + +func makeScheduledEvent(projectID string) *store.ScheduledEvent { + return &store.ScheduledEvent{ + ID: uuid.NewString(), + ProjectID: projectID, + EventType: "message", + FireAt: time.Now().Add(time.Hour).UTC().Truncate(time.Second), + Payload: `{"text":"hi"}`, + CreatedBy: "tester", + } +} + +// seedProject creates and returns a fresh project, satisfying the agent / +// scheduled-event foreign keys those entities require. +func seedProject(t *testing.T, cs *entadapter.CompositeStore) *store.Project { + t.Helper() + p := makeProject("seed-" + shortID()) + require.NoError(t, cs.CreateProject(context.Background(), p), "seed project") + return p +} + +// shortID returns a short, collision-resistant suffix for unique slugs/emails. +func shortID() string { return uuid.NewString()[:8] } + +// runConcurrently starts n goroutines that all block on a shared barrier and are +// released simultaneously, maximizing real overlap (and thus real contention) +// rather than letting earlier goroutines finish before later ones start. fn +// receives the worker index 0..n-1. It returns once every worker has finished. +func runConcurrently(n int, fn func(i int)) { + var release sync.WaitGroup + var done sync.WaitGroup + release.Add(1) + done.Add(n) + for i := 0; i < n; i++ { + go func(i int) { + defer done.Done() + release.Wait() + fn(i) + }(i) + } + release.Done() // fire the starting gun + done.Wait() +} diff --git a/pkg/store/integrationtest/isolation_test.go b/pkg/store/integrationtest/isolation_test.go new file mode 100644 index 000000000..4863eb133 --- /dev/null +++ b/pkg/store/integrationtest/isolation_test.go @@ -0,0 +1,171 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//go:build integration + +// Category 2 — Transaction isolation. These exercise Postgres MVCC behavior that +// the store relies on but that cannot be reproduced on SQLite: SERIALIZABLE +// conflict detection with the RunSerializable retry wrapper, REPEATABLE READ +// snapshot stability (no phantom rows), and READ COMMITTED dirty-read prevention. +package integrationtest + +import ( + "context" + "database/sql" + "sync" + "sync/atomic" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +// TestIsolation_SerializableRetryRecovers drives two concurrent SERIALIZABLE +// read-modify-write transactions into a genuine serialization conflict and +// verifies CompositeStore.RunSerializable transparently retries the aborted one +// so both increments ultimately land. +// +// Determinism: a two-party barrier makes both transactions perform their initial +// SELECT before either issues its UPDATE, so they read the same value. One commit +// then necessarily aborts with SQLSTATE 40001 (or 40P01); RunSerializable +// re-runs that closure against a fresh snapshot, which reads the now-committed +// value and commits cleanly. The barrier only applies on each transaction's first +// attempt, so a retry can never deadlock against it. +func TestIsolation_SerializableRetryRecovers(t *testing.T) { + cs := newStore(t) + ctx := context.Background() + db := cs.DB() + require.NotNil(t, db, "store must expose *sql.DB") + + _, err := db.ExecContext(ctx, `CREATE TABLE iso_counter (id int PRIMARY KEY, val int NOT NULL)`) + require.NoError(t, err) + _, err = db.ExecContext(ctx, `INSERT INTO iso_counter (id, val) VALUES (1, 0)`) + require.NoError(t, err) + + var barrier sync.WaitGroup + barrier.Add(2) + var totalAttempts int64 + errs := make(chan error, 2) + + worker := func() { + firstAttempt := true + errs <- cs.RunSerializable(ctx, func(ctx context.Context, tx *sql.Tx) error { + atomic.AddInt64(&totalAttempts, 1) + var val int + if err := tx.QueryRowContext(ctx, `SELECT val FROM iso_counter WHERE id=1`).Scan(&val); err != nil { + return err + } + if firstAttempt { + firstAttempt = false + barrier.Done() + barrier.Wait() // both transactions have now read the same val + } + _, err := tx.ExecContext(ctx, `UPDATE iso_counter SET val=$1 WHERE id=1`, val+1) + return err + }) + } + + var wg sync.WaitGroup + wg.Add(2) + go func() { defer wg.Done(); worker() }() + go func() { defer wg.Done(); worker() }() + wg.Wait() + close(errs) + for e := range errs { + require.NoError(t, e, "RunSerializable must recover the conflict, not surface it") + } + + var val int + require.NoError(t, db.QueryRowContext(ctx, `SELECT val FROM iso_counter WHERE id=1`).Scan(&val)) + assert.Equal(t, 2, val, "both serializable increments must land — no lost update") + assert.Greaterf(t, totalAttempts, int64(2), + "expected at least one retry after a serialization failure, saw only %d attempts", totalAttempts) + t.Logf("serializable retry: total fn attempts=%d (2 commits + retries)", totalAttempts) +} + +// TestIsolation_RepeatableReadNoPhantom verifies a REPEATABLE READ transaction +// sees a stable snapshot: a row inserted by another connection AFTER the +// snapshot's first read is invisible to the transaction (no phantom), yet visible +// to a fresh read once the transaction ends. +func TestIsolation_RepeatableReadNoPhantom(t *testing.T) { + cs := newStore(t) + ctx := context.Background() + db := cs.DB() + require.NotNil(t, db) + + _, err := db.ExecContext(ctx, `CREATE TABLE phantom_rows (id int PRIMARY KEY)`) + require.NoError(t, err) + for i := 0; i < 10; i++ { + _, err := db.ExecContext(ctx, `INSERT INTO phantom_rows (id) VALUES ($1)`, i) + require.NoError(t, err) + } + + tx, err := db.BeginTx(ctx, &sql.TxOptions{Isolation: sql.LevelRepeatableRead}) + require.NoError(t, err) + defer func() { _ = tx.Rollback() }() + + var before int + require.NoError(t, tx.QueryRowContext(ctx, `SELECT count(*) FROM phantom_rows`).Scan(&before)) + require.Equal(t, 10, before, "snapshot established at 10 rows") + + // Concurrent committed insert on a different (pooled) connection. + _, err = db.ExecContext(ctx, `INSERT INTO phantom_rows (id) VALUES (1000)`) + require.NoError(t, err) + + var after int + require.NoError(t, tx.QueryRowContext(ctx, `SELECT count(*) FROM phantom_rows`).Scan(&after)) + assert.Equal(t, before, after, "REPEATABLE READ snapshot must not observe the concurrently-inserted phantom row") + require.NoError(t, tx.Commit()) + + var fresh int + require.NoError(t, db.QueryRowContext(ctx, `SELECT count(*) FROM phantom_rows`).Scan(&fresh)) + assert.Equal(t, 11, fresh, "a fresh read after the snapshot ends must see the new row") +} + +// TestIsolation_DirtyReadPrevention verifies the default isolation level prevents +// dirty reads: a row written but not yet committed in one transaction is +// invisible to other connections, and stays invisible if that transaction rolls +// back (and becomes visible only on commit). +func TestIsolation_DirtyReadPrevention(t *testing.T) { + cs := newStore(t) + ctx := context.Background() + db := cs.DB() + require.NotNil(t, db) + + _, err := db.ExecContext(ctx, `CREATE TABLE dirty_rows (id int PRIMARY KEY)`) + require.NoError(t, err) + + count := func() int { + var c int + require.NoError(t, db.QueryRowContext(ctx, `SELECT count(*) FROM dirty_rows`).Scan(&c)) + return c + } + + // Uncommitted write is invisible to other connections. + txRollback, err := db.BeginTx(ctx, nil) + require.NoError(t, err) + _, err = txRollback.ExecContext(ctx, `INSERT INTO dirty_rows (id) VALUES (1)`) + require.NoError(t, err) + assert.Equal(t, 0, count(), "uncommitted insert must not be visible to another connection (no dirty read)") + require.NoError(t, txRollback.Rollback()) + assert.Equal(t, 0, count(), "rolled-back insert must remain invisible") + + // Committed write becomes visible. + txCommit, err := db.BeginTx(ctx, nil) + require.NoError(t, err) + _, err = txCommit.ExecContext(ctx, `INSERT INTO dirty_rows (id) VALUES (2)`) + require.NoError(t, err) + require.NoError(t, txCommit.Commit()) + assert.Equal(t, 1, count(), "committed insert must be visible") +} diff --git a/pkg/store/integrationtest/main_test.go b/pkg/store/integrationtest/main_test.go new file mode 100644 index 000000000..1c8506611 --- /dev/null +++ b/pkg/store/integrationtest/main_test.go @@ -0,0 +1,35 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//go:build integration + +package integrationtest + +import ( + "os" + "testing" + + "github.com/GoogleCloudPlatform/scion/pkg/store/enttest" +) + +// TestMain provisions the per-package ephemeral Postgres database once (creating +// it under a unique name so parallel `go test` invocations never collide) and +// drops it when the package finishes. Both calls are no-ops when +// SCION_TEST_POSTGRES_URL is unset, in which case every test skips. +func TestMain(m *testing.M) { + enttest.MainSetup() + code := m.Run() + enttest.MainTeardown() + os.Exit(code) +} diff --git a/pkg/store/integrationtest/migration_test.go b/pkg/store/integrationtest/migration_test.go new file mode 100644 index 000000000..cd473fe35 --- /dev/null +++ b/pkg/store/integrationtest/migration_test.go @@ -0,0 +1,104 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//go:build integration + +// Category 5 — Migration stress. These verify the Ent AutoMigrate path behaves on +// a non-trivial dataset: a large table migrates and reports correct row counts, +// large result sets are accessed with bounded memory (the list path caps the +// page rather than loading every row), and re-running the migration is idempotent +// and non-destructive — the property that lets an interrupted/killed migration be +// safely restarted. +package integrationtest + +import ( + "context" + "fmt" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/GoogleCloudPlatform/scion/pkg/store" +) + +// migrationRowCount is the dataset size for the large-migration tests: comfortably +// over the "1000+ rows" target while keeping the suite within its time budget. +const migrationRowCount = 1000 + +// TestMigration_LargeDatasetRowCounts seeds 1000+ rows, confirms the exact row +// count survives, and confirms the list path returns a BOUNDED page over the +// large table (proving the store does not materialize all rows into memory to +// answer a list request). +func TestMigration_LargeDatasetRowCounts(t *testing.T) { + cs := newStore(t) + ctx := context.Background() + db := cs.DB() + require.NotNil(t, db) + + project := seedProject(t, cs) + for i := 0; i < migrationRowCount; i++ { + require.NoErrorf(t, cs.CreateAgent(ctx, makeAgent(project.ID, fmt.Sprintf("bulk-%04d", i))), + "seeding agent %d", i) + } + + var count int + require.NoError(t, db.QueryRowContext(ctx, `SELECT count(*) FROM agents`).Scan(&count)) + assert.Equal(t, migrationRowCount, count, "all seeded rows must be present") + + // Bounded access: a capped page returns at most the limit, while TotalCount + // reflects the full table. + const page = 100 + res, err := cs.ListAgents(ctx, store.AgentFilter{ProjectID: project.ID}, store.ListOptions{Limit: page}) + require.NoError(t, err) + assert.Len(t, res.Items, page, "list must cap the page size, not return the whole table") + assert.Equal(t, migrationRowCount, res.TotalCount, "TotalCount must reflect the full dataset") +} + +// TestMigration_IdempotentReRunPreservesData seeds data and then re-runs Migrate +// several times, interleaving more writes. AutoMigrate is idempotent and +// converges to the same schema without touching data, which is exactly what makes +// a killed-and-restarted migration safe: re-running after a partial run finishes +// the job rather than corrupting or dropping rows. +func TestMigration_IdempotentReRunPreservesData(t *testing.T) { + cs := newStore(t) + ctx := context.Background() + db := cs.DB() + require.NotNil(t, db) + + project := seedProject(t, cs) + const firstBatch = 200 + for i := 0; i < firstBatch; i++ { + require.NoError(t, cs.CreateScheduledEvent(ctx, makeScheduledEvent(project.ID))) + } + + // Re-run the migration repeatedly (simulating restart after an interrupted + // run); each pass must succeed and leave existing rows untouched. + for pass := 0; pass < 3; pass++ { + require.NoErrorf(t, cs.Migrate(ctx), "re-migration pass %d must succeed", pass) + + // Migration must not change the row count. The expected count grows by one + // per pass because each iteration appends a row at the end of the loop. + var count int + require.NoError(t, db.QueryRowContext(ctx, `SELECT count(*) FROM scheduled_events`).Scan(&count)) + require.Equalf(t, firstBatch+pass, count, "re-migration pass %d changed the row count", pass) + + // Writes continue to work against the re-migrated schema. + require.NoError(t, cs.CreateScheduledEvent(ctx, makeScheduledEvent(project.ID))) + } + + var final int + require.NoError(t, db.QueryRowContext(ctx, `SELECT count(*) FROM scheduled_events`).Scan(&final)) + assert.Equal(t, firstBatch+3, final, "all rows (seed + one per pass) must be retained") +} diff --git a/pkg/store/integrationtest/multiprocess_test.go b/pkg/store/integrationtest/multiprocess_test.go new file mode 100644 index 000000000..96bab208e --- /dev/null +++ b/pkg/store/integrationtest/multiprocess_test.go @@ -0,0 +1,222 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//go:build integration + +// Category 7 — Multi-process. These prove the coordination primitives hold across +// SEPARATE OS PROCESSES, not just goroutines in one process — the real +// multi-replica hub topology. The parent test forks the test binary (os.Args[0]) +// to run a dedicated worker entrypoint against a shared database: +// +// - advisory-lock exclusivity: two independent processes contend for the same +// pg_advisory_lock; exactly one wins; +// - cross-process LISTEN/NOTIFY: a notification published by a child process is +// delivered to a listener in the parent process. +// +// The TestWorker_* functions are those entrypoints. They no-op (skip) unless their +// SCION_TEST_WORKER_DSN env var is set, so they are inert in a normal suite run +// and only do work when launched by a parent test via workerCommand. +package integrationtest + +import ( + "context" + "fmt" + "os" + "os/exec" + "strconv" + "strings" + "sync" + "testing" + "time" + + "github.com/jackc/pgx/v5" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/GoogleCloudPlatform/scion/pkg/ent/entc" + "github.com/GoogleCloudPlatform/scion/pkg/store" + "github.com/GoogleCloudPlatform/scion/pkg/store/entadapter" + "github.com/GoogleCloudPlatform/scion/pkg/store/enttest" +) + +// crossProcessLockKey is a fixed advisory-lock key for the multi-process lock +// test. It is namespaced under the 0x5C10 ("SCIO") prefix like the production +// keys but uses a distinct value so it never collides with them. Each package run +// gets a fresh ephemeral database and runs this test once, so a constant is safe. +const crossProcessLockKey = int64(0x5C10FADE) + +// workerCommand builds an exec.Cmd that re-invokes THIS test binary to run a +// single TestWorker_* entrypoint as a child process. SCION_TEST_POSTGRES_URL is +// stripped from the child's environment so its TestMain does not provision a +// second ephemeral database; the child talks only to the DSN passed in extraEnv. +func workerCommand(testName string, extraEnv map[string]string) *exec.Cmd { + cmd := exec.Command(os.Args[0], "-test.run=^"+testName+"$", "-test.v=true") + env := make([]string, 0, len(os.Environ())+len(extraEnv)) + for _, e := range os.Environ() { + if strings.HasPrefix(e, "SCION_TEST_POSTGRES_URL=") { + continue + } + env = append(env, e) + } + for k, v := range extraEnv { + env = append(env, k+"="+v) + } + cmd.Env = env + return cmd +} + +// TestMultiProcess_AdvisoryLockExclusivity forks two worker processes that each +// try to take the SAME advisory lock and hold it. Exactly one must acquire it; +// the other must observe it held and report BLOCKED. This is the cross-process +// guarantee behind "run this maintenance job on exactly one replica". +func TestMultiProcess_AdvisoryLockExclusivity(t *testing.T) { + requirePG(t) + dsn := enttest.NewSchemaURL(t) + + env := map[string]string{ + "SCION_TEST_WORKER_DSN": dsn, + "SCION_TEST_WORKER_LOCKKEY": strconv.FormatInt(crossProcessLockKey, 10), + } + + const procs = 2 + outputs := make([]string, procs) + var wg sync.WaitGroup + for i := 0; i < procs; i++ { + wg.Add(1) + go func(i int) { + defer wg.Done() + out, _ := workerCommand("TestWorker_AdvisoryLock", env).CombinedOutput() + outputs[i] = string(out) + }(i) + } + wg.Wait() + + acquired, blocked := 0, 0 + for _, o := range outputs { + if strings.Contains(o, "WORKER_RESULT: ACQUIRED") { + acquired++ + } + if strings.Contains(o, "WORKER_RESULT: BLOCKED") { + blocked++ + } + } + assert.Equalf(t, 1, acquired, "exactly one process must acquire the advisory lock\n--- proc0 ---\n%s\n--- proc1 ---\n%s", outputs[0], outputs[1]) + assert.Equalf(t, 1, blocked, "exactly one process must be blocked\n--- proc0 ---\n%s\n--- proc1 ---\n%s", outputs[0], outputs[1]) +} + +// TestWorker_AdvisoryLock is the child entrypoint for the advisory-lock test. It +// takes the lock (if free) and holds it long enough to guarantee the sibling +// process's attempt overlaps, then reports the outcome on stdout. +func TestWorker_AdvisoryLock(t *testing.T) { + dsn := os.Getenv("SCION_TEST_WORKER_DSN") + if dsn == "" { + t.Skip("worker entrypoint; launched only by a parent multi-process test") + } + key, err := strconv.ParseInt(os.Getenv("SCION_TEST_WORKER_LOCKKEY"), 10, 64) + require.NoError(t, err) + ctx := context.Background() + + client, err := entc.OpenPostgres(dsn, entc.PoolConfig{MaxOpenConns: 2, MaxIdleConns: 1}) + if err != nil { + fmt.Println("WORKER_RESULT: ERROR open:", err) + t.Fatal(err) + } + defer client.Close() + cs := entadapter.NewCompositeStore(client) + + acquired, release, err := cs.TryAdvisoryLock(ctx, store.AdvisoryLockKey(key)) + if err != nil { + fmt.Println("WORKER_RESULT: ERROR lock:", err) + t.Fatal(err) + } + defer func() { _ = release() }() + + if acquired { + fmt.Println("WORKER_RESULT: ACQUIRED") + // Hold the lock well past sibling startup jitter so its attempt is + // guaranteed to land while we hold it. + time.Sleep(3 * time.Second) + return + } + fmt.Println("WORKER_RESULT: BLOCKED") +} + +// TestMultiProcess_CrossProcessNotify starts LISTENing in the parent, then forks a +// child process that publishes N notifications on the same channel via a separate +// connection. The parent must receive all N — proving NOTIFY crosses the +// process/connection boundary (the basis for cross-replica event delivery). +func TestMultiProcess_CrossProcessNotify(t *testing.T) { + requirePG(t) + dsn := enttest.NewSchemaURL(t) + ctx := context.Background() + + // Establish the listener BEFORE forking the publisher so no notification is + // missed (NOTIFY only reaches sessions already LISTENing at send time). + listener := pgConnect(t, dsn) + channel := uniqueChannel("xproc") + _, err := listener.Exec(ctx, "LISTEN "+channel) + require.NoError(t, err) + + const n = 50 + cmd := workerCommand("TestWorker_NotifyPublisher", map[string]string{ + "SCION_TEST_WORKER_DSN": dsn, + "SCION_TEST_WORKER_CHANNEL": channel, + "SCION_TEST_WORKER_COUNT": strconv.Itoa(n), + }) + var out strings.Builder + cmd.Stdout = &out + cmd.Stderr = &out + require.NoError(t, cmd.Start()) + + got := 0 + for got < n { + wctx, cancel := context.WithTimeout(ctx, 15*time.Second) + note, werr := listener.WaitForNotification(wctx) + cancel() + require.NoErrorf(t, werr, "received %d/%d cross-process notifications before timeout\nworker output:\n%s", got, n, out.String()) + require.Equal(t, channel, note.Channel) + got++ + } + + require.NoErrorf(t, cmd.Wait(), "publisher process failed\nworker output:\n%s", out.String()) + assert.Contains(t, out.String(), "WORKER_RESULT: PUBLISHED") +} + +// TestWorker_NotifyPublisher is the child entrypoint for the cross-process NOTIFY +// test. It connects independently and publishes N notifications on the channel. +func TestWorker_NotifyPublisher(t *testing.T) { + dsn := os.Getenv("SCION_TEST_WORKER_DSN") + if dsn == "" { + t.Skip("worker entrypoint; launched only by a parent multi-process test") + } + channel := os.Getenv("SCION_TEST_WORKER_CHANNEL") + n, err := strconv.Atoi(os.Getenv("SCION_TEST_WORKER_COUNT")) + require.NoError(t, err) + ctx := context.Background() + + conn, err := pgx.Connect(ctx, dsn) + if err != nil { + fmt.Println("WORKER_RESULT: ERROR connect:", err) + t.Fatal(err) + } + defer conn.Close(ctx) + + for i := 0; i < n; i++ { + if _, err := conn.Exec(ctx, "SELECT pg_notify($1, $2)", channel, strconv.Itoa(i)); err != nil { + fmt.Println("WORKER_RESULT: ERROR notify:", err) + t.Fatal(err) + } + } + fmt.Println("WORKER_RESULT: PUBLISHED") +} diff --git a/pkg/store/integrationtest/notify_test.go b/pkg/store/integrationtest/notify_test.go new file mode 100644 index 000000000..3ad4bbf81 --- /dev/null +++ b/pkg/store/integrationtest/notify_test.go @@ -0,0 +1,219 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//go:build integration + +// Category 4 — LISTEN/NOTIFY under load. These exercise the raw Postgres +// asynchronous-notification primitive that the hub's PostgresEventPublisher is +// built on: ordered burst delivery without drops, the hard 8000-byte payload +// limit that motivates the publisher's reference-and-refetch offload, listener +// reconnect after a backend is terminated, and strict per-channel isolation. +// +// The higher-level publisher behaviors (reference-and-refetch round-trip, +// automatic resubscribe, NATS-style pattern fan-out) are covered against a live +// database in pkg/hub/events_postgres_test.go. Here we pin the underlying +// database guarantees those features depend on. +package integrationtest + +import ( + "context" + "strconv" + "strings" + "testing" + "time" + + "github.com/jackc/pgx/v5" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/GoogleCloudPlatform/scion/pkg/store/enttest" +) + +// pgConnect opens a raw pgx connection to the per-package database and registers +// cleanup. LISTEN/NOTIFY channels are database-global (not schema-scoped), so the +// schema in the DSN is irrelevant here; tests use unique channel names to stay +// isolated from one another on the shared database. +func pgConnect(t *testing.T, dsn string) *pgx.Conn { + t.Helper() + conn, err := pgx.Connect(context.Background(), dsn) + require.NoError(t, err, "pgx connect") + t.Cleanup(func() { _ = conn.Close(context.Background()) }) + return conn +} + +// uniqueChannel returns a Postgres channel name safe to use unquoted (hex + '_'). +func uniqueChannel(prefix string) string { return "itest_" + prefix + "_" + shortID() } + +// TestNotify_BurstDeliveredInOrderNoDrops fires a rapid burst of N notifications +// on one channel and asserts the listener receives all N, in publish order. +// Postgres guarantees ordered, lossless delivery of committed notifications to a +// session that was LISTENing before they were sent. +func TestNotify_BurstDeliveredInOrderNoDrops(t *testing.T) { + requirePG(t) + dsn := enttest.NewSchemaURL(t) + ctx := context.Background() + + listener := pgConnect(t, dsn) + channel := uniqueChannel("burst") + _, err := listener.Exec(ctx, "LISTEN "+channel) + require.NoError(t, err) + + notifier := pgConnect(t, dsn) + const n = 200 + for i := 0; i < n; i++ { + _, err := notifier.Exec(ctx, "SELECT pg_notify($1, $2)", channel, strconv.Itoa(i)) + require.NoErrorf(t, err, "publishing notification %d", i) + } + + got := make([]string, 0, n) + for len(got) < n { + wctx, cancel := context.WithTimeout(ctx, 5*time.Second) + note, err := listener.WaitForNotification(wctx) + cancel() + require.NoErrorf(t, err, "received only %d of %d notifications before timing out", len(got), n) + require.Equal(t, channel, note.Channel) + got = append(got, note.Payload) + } + + for i := 0; i < n; i++ { + require.Equalf(t, strconv.Itoa(i), got[i], "notification %d out of order or corrupted", i) + } +} + +// TestNotify_OversizedPayloadRejected pins the 8000-byte NOTIFY payload limit: +// a payload at/over the limit is rejected by the server (this is exactly why the +// PostgresEventPublisher offloads oversized events to the scion_event_payloads +// table and notifies a reference id instead), while a payload comfortably under +// the limit is delivered intact. +func TestNotify_OversizedPayloadRejected(t *testing.T) { + requirePG(t) + dsn := enttest.NewSchemaURL(t) + ctx := context.Background() + + listener := pgConnect(t, dsn) + channel := uniqueChannel("size") + _, err := listener.Exec(ctx, "LISTEN "+channel) + require.NoError(t, err) + + notifier := pgConnect(t, dsn) + + // At/over 8000 bytes Postgres rejects the NOTIFY. + oversized := strings.Repeat("x", 8000) + _, err = notifier.Exec(ctx, "SELECT pg_notify($1, $2)", channel, oversized) + require.Error(t, err, "Postgres must reject a NOTIFY payload of 8000 bytes") + + // Comfortably under the limit (matching the publisher's 7000-byte threshold) + // is accepted and delivered intact. + underLimit := strings.Repeat("y", 7000) + _, err = notifier.Exec(ctx, "SELECT pg_notify($1, $2)", channel, underLimit) + require.NoError(t, err) + + wctx, cancel := context.WithTimeout(ctx, 5*time.Second) + note, err := listener.WaitForNotification(wctx) + cancel() + require.NoError(t, err) + assert.Equal(t, underLimit, note.Payload, "under-limit payload must arrive intact") +} + +// TestNotify_ListenerReconnectResumes terminates a listener's backend mid-stream +// (simulating a dropped CloudSQL connection) and verifies that a freshly +// reconnected listener which re-issues LISTEN resumes receiving notifications. +// This is the database-level guarantee the publisher's automatic resubscribe +// loop relies on. +func TestNotify_ListenerReconnectResumes(t *testing.T) { + requirePG(t) + dsn := enttest.NewSchemaURL(t) + ctx := context.Background() + + channel := uniqueChannel("reconnect") + notifier := pgConnect(t, dsn) + + listener1, err := pgx.Connect(ctx, dsn) + require.NoError(t, err) + _, err = listener1.Exec(ctx, "LISTEN "+channel) + require.NoError(t, err) + + var pid uint32 + require.NoError(t, listener1.QueryRow(ctx, "SELECT pg_backend_pid()").Scan(&pid)) + + // Sanity: delivery works before the drop. + _, err = notifier.Exec(ctx, "SELECT pg_notify($1, $2)", channel, "before") + require.NoError(t, err) + wctx, cancel := context.WithTimeout(ctx, 5*time.Second) + note, err := listener1.WaitForNotification(wctx) + cancel() + require.NoError(t, err) + require.Equal(t, "before", note.Payload) + + // Drop the listener's backend from another session. + _, err = notifier.Exec(ctx, "SELECT pg_terminate_backend($1)", pid) + require.NoError(t, err) + + // The dead connection now errors instead of hanging. + wctx, cancel = context.WithTimeout(ctx, 3*time.Second) + _, err = listener1.WaitForNotification(wctx) + cancel() + assert.Error(t, err, "a terminated backend must surface an error to its listener") + _ = listener1.Close(ctx) + + // Reconnect, re-LISTEN, and confirm delivery resumes. + listener2 := pgConnect(t, dsn) + _, err = listener2.Exec(ctx, "LISTEN "+channel) + require.NoError(t, err) + _, err = notifier.Exec(ctx, "SELECT pg_notify($1, $2)", channel, "after") + require.NoError(t, err) + wctx, cancel = context.WithTimeout(ctx, 5*time.Second) + note, err = listener2.WaitForNotification(wctx) + cancel() + require.NoError(t, err, "reconnected listener must resume receiving notifications") + assert.Equal(t, "after", note.Payload) +} + +// TestNotify_CrossChannelIsolation verifies notifications are strictly scoped to +// their channel: a listener subscribed only to channel A never observes an event +// published on channel B. +func TestNotify_CrossChannelIsolation(t *testing.T) { + requirePG(t) + dsn := enttest.NewSchemaURL(t) + ctx := context.Background() + + channelA := uniqueChannel("a") + channelB := uniqueChannel("b") + + listener := pgConnect(t, dsn) + _, err := listener.Exec(ctx, "LISTEN "+channelA) // only A + require.NoError(t, err) + + notifier := pgConnect(t, dsn) + // Publish on B first (must be ignored), then on A. + _, err = notifier.Exec(ctx, "SELECT pg_notify($1, $2)", channelB, "leak-from-B") + require.NoError(t, err) + _, err = notifier.Exec(ctx, "SELECT pg_notify($1, $2)", channelA, "expected-from-A") + require.NoError(t, err) + + wctx, cancel := context.WithTimeout(ctx, 5*time.Second) + note, err := listener.WaitForNotification(wctx) + cancel() + require.NoError(t, err) + assert.Equal(t, channelA, note.Channel, "must only receive channel A") + assert.Equal(t, "expected-from-A", note.Payload) + + // Nothing else should arrive — the B notification must not leak through. + wctx, cancel = context.WithTimeout(ctx, 500*time.Millisecond) + leak, err := listener.WaitForNotification(wctx) + cancel() + if err == nil { + t.Fatalf("listener received an unexpected notification on channel %q: %q", leak.Channel, leak.Payload) + } +} diff --git a/pkg/store/integrationtest/pool_test.go b/pkg/store/integrationtest/pool_test.go new file mode 100644 index 000000000..c05e555bc --- /dev/null +++ b/pkg/store/integrationtest/pool_test.go @@ -0,0 +1,212 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//go:build integration + +// Category 3 — Connection pool stress. These open stores with a deliberately +// small MaxOpenConns and then saturate, block, and forcibly drop connections to +// verify the database/sql pool behaves under pressure: queued requests are served +// once capacity frees up, a saturated pool honors the caller's context deadline +// instead of hanging forever, a long-running transaction does not starve short +// queries on the remaining connections, and the pool transparently heals after +// its backends are killed server-side. +package integrationtest + +import ( + "context" + "sync" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/GoogleCloudPlatform/scion/pkg/ent/entc" + "github.com/GoogleCloudPlatform/scion/pkg/store/entadapter" + "github.com/GoogleCloudPlatform/scion/pkg/store/enttest" +) + +// openPoolStore opens a CompositeStore against a fresh schema with an explicit +// pool size and optional application_name (used by the recovery test to target +// only its own backends for termination). +func openPoolStore(t *testing.T, maxOpen int, appName string) *entadapter.CompositeStore { + t.Helper() + dsn := enttest.NewSchemaURL(t) + if appName != "" { + var err error + dsn, err = enttest.WithConnParam(dsn, "application_name", appName) + require.NoError(t, err) + } + client, err := entc.OpenPostgres(dsn, entc.PoolConfig{MaxOpenConns: maxOpen, MaxIdleConns: maxOpen}) + require.NoError(t, err) + cs := entadapter.NewCompositeStore(client) + t.Cleanup(func() { _ = cs.Close() }) + return cs +} + +// waitFor polls cond until it returns true or the timeout elapses. +func waitFor(t *testing.T, timeout time.Duration, cond func() bool) bool { + t.Helper() + deadline := time.Now().Add(timeout) + for time.Now().Before(deadline) { + if cond() { + return true + } + time.Sleep(5 * time.Millisecond) + } + return cond() +} + +// TestPool_ExhaustedRequestsEventuallySucceed launches far more concurrent +// connection-holding queries than the pool has slots and asserts every one +// eventually completes: requests beyond MaxOpenConns queue and are served as +// connections free up, rather than erroring. +func TestPool_ExhaustedRequestsEventuallySucceed(t *testing.T) { + cs := newStoreWithPool(t, 4) + ctx := context.Background() + db := cs.DB() + require.NotNil(t, db) + require.Equal(t, 4, db.Stats().MaxOpenConnections, "pool sized to 4") + + const tasks = 32 // 8x the pool: most requests must queue + errs := make(chan error, tasks) + runConcurrently(tasks, func(int) { + // pg_sleep holds the connection checked out long enough to force queueing. + _, err := db.ExecContext(ctx, `SELECT pg_sleep(0.05)`) + errs <- err + }) + close(errs) + for err := range errs { + require.NoError(t, err, "a queued request failed instead of waiting for a free connection") + } +} + +// TestPool_SaturatedPoolRespectsContextDeadline checks out every connection in a +// 2-connection pool and holds them, then issues a query with a short deadline. +// With no connection available the acquire must fail with the context deadline +// (a clean, bounded failure) rather than blocking forever; once the held +// connections are released, queries succeed again. +func TestPool_SaturatedPoolRespectsContextDeadline(t *testing.T) { + cs := newStoreWithPool(t, 2) + ctx := context.Background() + db := cs.DB() + require.NotNil(t, db) + + release := make(chan struct{}) + var holding sync.WaitGroup + for i := 0; i < 2; i++ { + holding.Add(1) + go func() { + defer holding.Done() + conn, err := db.Conn(ctx) + if err != nil { + return + } + defer conn.Close() + // Touch the connection so it is genuinely checked out, then hold it. + _, _ = conn.ExecContext(ctx, `SELECT 1`) + <-release + }() + } + + require.True(t, waitFor(t, 5*time.Second, func() bool { return db.Stats().InUse == 2 }), + "both pool connections should be checked out") + + shortCtx, cancel := context.WithTimeout(ctx, 300*time.Millisecond) + _, err := db.ExecContext(shortCtx, `SELECT 1`) + cancel() + require.Error(t, err, "query on a saturated pool must not hang; it should fail on the deadline") + assert.ErrorIs(t, err, context.DeadlineExceeded) + + close(release) + holding.Wait() + + _, err = db.ExecContext(ctx, `SELECT 1`) + require.NoError(t, err, "pool must serve queries again once connections are released") +} + +// TestPool_LongTxnDoesNotStarveShortQueries holds one connection of a 4-connection +// pool inside a long (1s) transaction and asserts a batch of short queries on the +// other connections all complete quickly, well before the long transaction does. +func TestPool_LongTxnDoesNotStarveShortQueries(t *testing.T) { + cs := newStoreWithPool(t, 4) + ctx := context.Background() + db := cs.DB() + require.NotNil(t, db) + + longDone := make(chan error, 1) + go func() { + tx, err := db.BeginTx(ctx, nil) + if err != nil { + longDone <- err + return + } + if _, err := tx.ExecContext(ctx, `SELECT pg_sleep(1)`); err != nil { + _ = tx.Rollback() + longDone <- err + return + } + longDone <- tx.Commit() + }() + + // Let the long transaction grab its connection first. + require.True(t, waitFor(t, 5*time.Second, func() bool { return db.Stats().InUse >= 1 }), + "long transaction should have checked out a connection") + + start := time.Now() + for i := 0; i < 10; i++ { + shortCtx, cancel := context.WithTimeout(ctx, 500*time.Millisecond) + var one int + err := db.QueryRowContext(shortCtx, `SELECT 1`).Scan(&one) + cancel() + require.NoErrorf(t, err, "short query %d starved by the long transaction", i) + require.Equal(t, 1, one) + } + elapsed := time.Since(start) + assert.Lessf(t, elapsed, 900*time.Millisecond, + "10 short queries took %s — they appear to be waiting on the 1s transaction", elapsed) + + require.NoError(t, <-longDone, "long transaction itself must commit") +} + +// TestPool_RecoveryAfterConnectionDrop warms several pooled connections, then +// terminates them server-side with pg_terminate_backend (simulating a CloudSQL +// connection reset / failover). The pool must transparently discard the dead +// connections and open fresh ones so subsequent queries succeed. +func TestPool_RecoveryAfterConnectionDrop(t *testing.T) { + appName := "scion_pooltest_" + shortID() + cs := openPoolStore(t, 4, appName) + ctx := context.Background() + db := cs.DB() + require.NotNil(t, db) + + // Warm up multiple connections so there are siblings to kill. + runConcurrently(4, func(int) { _, _ = db.ExecContext(ctx, `SELECT pg_sleep(0.05)`) }) + + // Kill every backend with our application_name except the one running this + // statement, scoping the blast radius to this test's own pool. + _, err := db.ExecContext(ctx, + `SELECT pg_terminate_backend(pid) FROM pg_stat_activity + WHERE application_name = $1 AND pid <> pg_backend_pid()`, appName) + require.NoError(t, err) + + // The pool must heal: each query that lands on a dead connection is retried on + // a freshly opened one by database/sql. + for i := 0; i < 20; i++ { + var one int + err := db.QueryRowContext(ctx, `SELECT 1`).Scan(&one) + require.NoErrorf(t, err, "query %d failed; pool did not heal after backend termination", i) + require.Equal(t, 1, one) + } +} diff --git a/pkg/store/integrationtest/schema_test.go b/pkg/store/integrationtest/schema_test.go new file mode 100644 index 000000000..1561100d9 --- /dev/null +++ b/pkg/store/integrationtest/schema_test.go @@ -0,0 +1,176 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//go:build integration + +// Category 6 — Schema / type edge cases. Postgres is strictly typed where SQLite +// is loose, so these pin behaviors that can silently differ between the two +// backends: NULL semantics for nullable columns, exact round-tripping of +// Unicode/emoji and JSON (including nested structures and special characters), +// large text values that must not be truncated, and TIMESTAMPTZ microsecond +// precision (vs SQLite's text timestamps). +package integrationtest + +import ( + "context" + "strings" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/GoogleCloudPlatform/scion/pkg/store" +) + +// TestSchema_NullableRoundTrip verifies nullable columns store and read back as +// genuine SQL NULL (not a zero-value sentinel) and transition correctly when set. +func TestSchema_NullableRoundTrip(t *testing.T) { + cs := newStore(t) + ctx := context.Background() + db := cs.DB() + require.NotNil(t, db) + project := seedProject(t, cs) + + // A pending scheduled event has a NULL fired_at. + evt := makeScheduledEvent(project.ID) + require.NoError(t, cs.CreateScheduledEvent(ctx, evt)) + got, err := cs.GetScheduledEvent(ctx, evt.ID) + require.NoError(t, err) + assert.Nil(t, got.FiredAt, "pending event must have nil FiredAt") + + var firedIsNull bool + require.NoError(t, db.QueryRowContext(ctx, + `SELECT fired_at IS NULL FROM scheduled_events WHERE id=$1`, evt.ID).Scan(&firedIsNull)) + assert.True(t, firedIsNull, "fired_at must be SQL NULL, not a zero timestamp") + + // Claiming sets fired_at to a real value. + won, err := cs.ClaimScheduledEvent(ctx, evt.ID, store.ScheduledEventFired) + require.NoError(t, err) + require.True(t, won) + got, err = cs.GetScheduledEvent(ctx, evt.ID) + require.NoError(t, err) + require.NotNil(t, got.FiredAt, "claimed event must have a non-nil FiredAt") + + require.NoError(t, db.QueryRowContext(ctx, + `SELECT fired_at IS NULL FROM scheduled_events WHERE id=$1`, evt.ID).Scan(&firedIsNull)) + assert.False(t, firedIsNull, "fired_at must no longer be NULL after a claim") + + // An agent created without an owner has a NULL owner_id (not an empty string). + ag := makeAgent(project.ID, "null-owner-"+shortID()) + require.NoError(t, cs.CreateAgent(ctx, ag)) + var ownerIsNull bool + require.NoError(t, db.QueryRowContext(ctx, + `SELECT owner_id IS NULL FROM agents WHERE id=$1`, ag.ID).Scan(&ownerIsNull)) + assert.True(t, ownerIsNull, "unset owner_id must be SQL NULL") + reread, err := cs.GetAgent(ctx, ag.ID) + require.NoError(t, err) + assert.Equal(t, "", reread.OwnerID, "NULL owner_id must read back as empty string") +} + +// TestSchema_UnicodeAndEmoji verifies multibyte Unicode and emoji round-trip +// byte-for-byte through text columns. +func TestSchema_UnicodeAndEmoji(t *testing.T) { + cs := newStore(t) + ctx := context.Background() + + const fancy = "项目 🚀 café ☃ Ω≈ç √∫ — “quotes” 𝔘𝔫𝔦𝔠𝔬𝔡𝔢" + p := makeProject("unicode-" + shortID()) + p.Name = fancy + require.NoError(t, cs.CreateProject(ctx, p)) + + got, err := cs.GetProject(ctx, p.ID) + require.NoError(t, err) + assert.Equal(t, fancy, got.Name, "Unicode/emoji must round-trip exactly") +} + +// TestSchema_NestedJSONAndSpecialChars verifies JSON-bearing columns preserve +// nested objects, arrays, and special characters exactly. ScheduledEvent.Payload +// is stored verbatim; Agent.Labels is marshaled to a JSON column. +func TestSchema_NestedJSONAndSpecialChars(t *testing.T) { + cs := newStore(t) + ctx := context.Background() + project := seedProject(t, cs) + + // Verbatim JSON text column. + const payload = `{"nested":{"arr":[1,2,3],"flag":true,"s":"emoji 🎉 \"quoted\" back\\slash tab\there","null":null},"unicode":"café"}` + evt := makeScheduledEvent(project.ID) + evt.Payload = payload + require.NoError(t, cs.CreateScheduledEvent(ctx, evt)) + gotEvt, err := cs.GetScheduledEvent(ctx, evt.ID) + require.NoError(t, err) + assert.Equal(t, payload, gotEvt.Payload, "nested JSON payload must round-trip verbatim") + + // JSON-marshaled map column with special-character values. + labels := map[string]string{ + "emoji": "🔥💧", + "quotes": `he said "hi"`, + "unicode": "naïve café", + "nl": "line1\nline2\ttabbed", + } + ag := makeAgent(project.ID, "json-labels-"+shortID()) + ag.Labels = labels + require.NoError(t, cs.CreateAgent(ctx, ag)) + gotAg, err := cs.GetAgent(ctx, ag.ID) + require.NoError(t, err) + assert.Equal(t, labels, gotAg.Labels, "JSON label map must round-trip exactly, special chars included") +} + +// TestSchema_LargeTextNoTruncation verifies a large string in a text column is +// stored and returned without truncation. Ent maps Go strings to unbounded +// Postgres TEXT, so there is no VARCHAR(n) boundary to silently clip at. +func TestSchema_LargeTextNoTruncation(t *testing.T) { + cs := newStore(t) + ctx := context.Background() + project := seedProject(t, cs) + + large := strings.Repeat("A", 100*1024) // 100 KiB + evt := makeScheduledEvent(project.ID) + evt.Payload = large + require.NoError(t, cs.CreateScheduledEvent(ctx, evt)) + + got, err := cs.GetScheduledEvent(ctx, evt.ID) + require.NoError(t, err) + assert.Equal(t, len(large), len(got.Payload), "large text must not be truncated") + assert.Equal(t, large, got.Payload) +} + +// TestSchema_TimestampPrecision verifies TIMESTAMPTZ preserves sub-second +// precision to the microsecond (Postgres' resolution) and a stable instant. +// Nanoseconds below the microsecond are truncated by Postgres — that truncation +// is the documented behavior, not data loss to the second as a naive text-based +// (SQLite) representation might do. +func TestSchema_TimestampPrecision(t *testing.T) { + cs := newStore(t) + ctx := context.Background() + project := seedProject(t, cs) + + // A time carrying both microsecond and stray nanosecond components. + in := time.Date(2026, 6, 2, 13, 14, 15, 123456789, time.UTC) + wantMicro := in.Truncate(time.Microsecond) // 123456000 ns + + evt := makeScheduledEvent(project.ID) + evt.FireAt = in + require.NoError(t, cs.CreateScheduledEvent(ctx, evt)) + + got, err := cs.GetScheduledEvent(ctx, evt.ID) + require.NoError(t, err) + + assert.True(t, got.FireAt.UTC().Equal(wantMicro), + "fire_at must preserve the microsecond instant: got %v, want %v", got.FireAt.UTC(), wantMicro) + assert.NotEqual(t, in.Truncate(time.Second), got.FireAt.UTC(), + "sub-second precision must be retained (not truncated to whole seconds)") + assert.Zero(t, got.FireAt.Nanosecond()%1000, + "Postgres resolution is microseconds; nanosecond remainder must be zero") +} diff --git a/pkg/store/models.go b/pkg/store/models.go index 7007ef2de..8fb0f8f2d 100644 --- a/pkg/store/models.go +++ b/pkg/store/models.go @@ -184,11 +184,53 @@ const ( // When a git project has the workspace mode label set to "shared", it uses a // single shared clone mounted by all agents instead of per-agent clones. const ( - LabelWorkspaceMode = "scion.dev/workspace-mode" - WorkspaceModeShared = "shared" - WorkspaceModePerAgent = "per-agent" + LabelWorkspaceMode = "scion.dev/workspace-mode" + WorkspaceModeShared = "shared" + WorkspaceModePerAgent = "per-agent" + WorkspaceModeWorktreePerAgent = "worktree-per-agent" ) +// WorkspaceSharingMode is the canonical set of workspace sharing modes from the +// glossary. These three modes govern how workspaces are allocated to agents and +// determine which storage backend is used (NFS-shared vs node-local). +type WorkspaceSharingMode string + +const ( + // SharingModeSharedPlain: one workspace directory mounted into every agent, + // no per-agent isolation. Used for plain/non-git projects. + // Maps from label value "shared". + SharingModeSharedPlain WorkspaceSharingMode = "shared-plain" + + // SharingModeClonePerAgent: each agent gets its own full git clone. + // Nothing is shared, so this stays on node-local storage (NOT NFS). + // Maps from label value "per-agent". + SharingModeClonePerAgent WorkspaceSharingMode = "clone-per-agent" + + // SharingModeWorktreePerAgent: each agent gets its own git worktree over + // one shared checkout. The shared checkout + all worktrees live on NFS. + // Maps from label value "worktree-per-agent". + // Note: not yet on Hub-managed projects — reserved for Phase 1+. + SharingModeWorktreePerAgent WorkspaceSharingMode = "worktree-per-agent" +) + +// ResolveWorkspaceSharingMode maps a workspace mode label value (wire format) to +// the canonical WorkspaceSharingMode. Empty or unknown values default to +// SharingModeSharedPlain for backward compatibility (existing projects without +// an explicit label are treated as shared). +func ResolveWorkspaceSharingMode(label string) WorkspaceSharingMode { + switch label { + case WorkspaceModeShared, "shared-plain": + return SharingModeSharedPlain + case WorkspaceModePerAgent, "clone-per-agent": + return SharingModeClonePerAgent + case WorkspaceModeWorktreePerAgent: + return SharingModeWorktreePerAgent + default: + // Empty or unrecognized: default to shared-plain. + return SharingModeSharedPlain + } +} + // Project represents a project/agent group in the Hub database. type Project struct { // Identity @@ -283,6 +325,12 @@ func (p *Project) IsSharedWorkspace() bool { return p.GitRemote != "" && p.Labels[LabelWorkspaceMode] == WorkspaceModeShared } +// IsWorktreePerAgent returns true if this is a git project configured to use +// per-agent git worktrees over a shared base clone. +func (p *Project) IsWorktreePerAgent() bool { + return p.GitRemote != "" && p.Labels[LabelWorkspaceMode] == WorkspaceModeWorktreePerAgent +} + // RuntimeBroker represents a compute node in the Hub database. type RuntimeBroker struct { // Identity @@ -308,6 +356,11 @@ type RuntimeBroker struct { Labels map[string]string `json:"labels,omitempty"` Annotations map[string]string `json:"annotations,omitempty"` + // Affinity — which hub instance currently holds the control-channel socket + ConnectedHubID *string `json:"connectedHubId,omitempty"` + ConnectedSessionID *string `json:"connectedSessionId,omitempty"` + ConnectedAt *time.Time `json:"connectedAt,omitempty"` + // Network endpoint (for direct HTTP mode) Endpoint string `json:"endpoint,omitempty"` @@ -418,9 +471,7 @@ type Template struct { // Inheritance BaseTemplate string `json:"baseTemplate,omitempty"` // Parent template ID (for inheritance) - // Protection - Locked bool `json:"locked,omitempty"` // Prevent modifications (global templates) - Status string `json:"status"` // pending, active, archived + Status string `json:"status"` // pending, active, archived // Ownership OwnerID string `json:"ownerId,omitempty"` @@ -513,9 +564,7 @@ type HarnessConfig struct { // File manifest Files []TemplateFile `json:"files,omitempty"` // Manifest of harness config files (reuses TemplateFile) - // Protection - Locked bool `json:"locked,omitempty"` // Prevent modifications - Status string `json:"status"` // pending, active, archived + Status string `json:"status"` // pending, active, archived // Ownership OwnerID string `json:"ownerId,omitempty"` @@ -747,6 +796,42 @@ const ( BrokerStatusDegraded = "degraded" ) +// BrokerDispatch is the durable intent for a lifecycle/create-time command +// targeted at a broker (design §5.2). The socket-holding node reconciles it: +// CAS-claim (pending->in_progress) → run the local tunnel op → mark done/failed. +type BrokerDispatch struct { + ID string `json:"id"` + BrokerID string `json:"brokerId"` + AgentID string `json:"agentId,omitempty"` // empty for project-scoped ops + AgentSlug string `json:"agentSlug,omitempty"` + ProjectID string `json:"projectId,omitempty"` // empty if unknown/none + Op string `json:"op"` // start|stop|restart|delete|finalize_env|check_prompt|create|message + Args string `json:"args,omitempty"` // JSON + State string `json:"state"` // pending|in_progress|done|failed + Result string `json:"result,omitempty"` // JSON + ClaimedBy string `json:"claimedBy,omitempty"` // hub instanceID that reconciled it + Attempts int `json:"attempts"` + Error string `json:"error,omitempty"` + CreatedAt time.Time `json:"createdAt"` + UpdatedAt time.Time `json:"updatedAt"` + DeadlineAt *time.Time `json:"deadlineAt,omitempty"` +} + +// BrokerDispatch.State values. +const ( + DispatchStatePending = "pending" + DispatchStateInProgress = "in_progress" + DispatchStateDone = "done" + DispatchStateFailed = "failed" +) + +// Message.DispatchState values (the message row is its own dispatch intent). +const ( + MessageDispatchPending = "pending" + MessageDispatchDispatched = "dispatched" + MessageDispatchFailed = "failed" +) + // ============================================================================= // Notifications (Agent Status Notification System) // ============================================================================= @@ -1350,6 +1435,11 @@ type Message struct { Channel string `json:"channel,omitempty"` ThreadID string `json:"threadId,omitempty"` CreatedAt time.Time `json:"createdAt"` + // DispatchState tracks cross-node delivery of the message to the broker: + // pending|dispatched|failed. The message row is its own durable dispatch + // intent (design §5.2/§6.1). + DispatchState string `json:"dispatchState,omitempty"` + DispatchedAt *time.Time `json:"dispatchedAt,omitempty"` } // MarshalJSON implements custom marshaling to support legacy groveId field. diff --git a/pkg/store/models_test.go b/pkg/store/models_test.go new file mode 100644 index 000000000..6a3729fd8 --- /dev/null +++ b/pkg/store/models_test.go @@ -0,0 +1,130 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package store + +import "testing" + +func TestResolveWorkspaceSharingMode(t *testing.T) { + tests := []struct { + label string + want WorkspaceSharingMode + }{ + // Canonical label values + {label: "shared", want: SharingModeSharedPlain}, + {label: "per-agent", want: SharingModeClonePerAgent}, + {label: "worktree-per-agent", want: SharingModeWorktreePerAgent}, + + // Canonical enum values (accepted as aliases) + {label: "shared-plain", want: SharingModeSharedPlain}, + {label: "clone-per-agent", want: SharingModeClonePerAgent}, + + // Empty → default (shared-plain) + {label: "", want: SharingModeSharedPlain}, + + // Unknown → default (shared-plain) + {label: "unknown-mode", want: SharingModeSharedPlain}, + {label: "SHARED", want: SharingModeSharedPlain}, // case-sensitive: unrecognized → default + } + + for _, tt := range tests { + t.Run("label="+tt.label, func(t *testing.T) { + got := ResolveWorkspaceSharingMode(tt.label) + if got != tt.want { + t.Errorf("ResolveWorkspaceSharingMode(%q) = %q, want %q", tt.label, got, tt.want) + } + }) + } +} + +func TestWorkspaceSharingMode_Constants(t *testing.T) { + // Verify the existing label constants are unchanged (lossless migration). + if WorkspaceModeShared != "shared" { + t.Errorf("WorkspaceModeShared = %q, want %q", WorkspaceModeShared, "shared") + } + if WorkspaceModePerAgent != "per-agent" { + t.Errorf("WorkspaceModePerAgent = %q, want %q", WorkspaceModePerAgent, "per-agent") + } + if LabelWorkspaceMode != "scion.dev/workspace-mode" { + t.Errorf("LabelWorkspaceMode = %q, want %q", LabelWorkspaceMode, "scion.dev/workspace-mode") + } + + // Verify the new typed constants have the expected string values. + if SharingModeSharedPlain != "shared-plain" { + t.Errorf("SharingModeSharedPlain = %q, want %q", SharingModeSharedPlain, "shared-plain") + } + if SharingModeClonePerAgent != "clone-per-agent" { + t.Errorf("SharingModeClonePerAgent = %q, want %q", SharingModeClonePerAgent, "clone-per-agent") + } + if SharingModeWorktreePerAgent != "worktree-per-agent" { + t.Errorf("SharingModeWorktreePerAgent = %q, want %q", SharingModeWorktreePerAgent, "worktree-per-agent") + } + if WorkspaceModeWorktreePerAgent != "worktree-per-agent" { + t.Errorf("WorkspaceModeWorktreePerAgent = %q, want %q", WorkspaceModeWorktreePerAgent, "worktree-per-agent") + } +} + +func TestProject_IsWorktreePerAgent(t *testing.T) { + tests := []struct { + name string + project Project + want bool + }{ + { + name: "worktree-per-agent git project", + project: Project{ + GitRemote: "github.com/test/repo", + Labels: map[string]string{LabelWorkspaceMode: WorkspaceModeWorktreePerAgent}, + }, + want: true, + }, + { + name: "shared git project", + project: Project{ + GitRemote: "github.com/test/repo", + Labels: map[string]string{LabelWorkspaceMode: WorkspaceModeShared}, + }, + want: false, + }, + { + name: "per-agent git project", + project: Project{ + GitRemote: "github.com/test/repo", + Labels: map[string]string{LabelWorkspaceMode: WorkspaceModePerAgent}, + }, + want: false, + }, + { + name: "worktree label but no git remote", + project: Project{ + Labels: map[string]string{LabelWorkspaceMode: WorkspaceModeWorktreePerAgent}, + }, + want: false, + }, + { + name: "no labels", + project: Project{GitRemote: "github.com/test/repo"}, + want: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := tt.project.IsWorktreePerAgent() + if got != tt.want { + t.Errorf("IsWorktreePerAgent() = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/pkg/store/sqlite/allow_list_invite_test.go b/pkg/store/sqlite/allow_list_invite_test.go deleted file mode 100644 index 37b3a44f1..000000000 --- a/pkg/store/sqlite/allow_list_invite_test.go +++ /dev/null @@ -1,236 +0,0 @@ -// Copyright 2026 Google LLC -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -//go:build !no_sqlite - -package sqlite - -import ( - "context" - "testing" - "time" - - "github.com/GoogleCloudPlatform/scion/pkg/store" - "github.com/google/uuid" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -func TestUpdateAllowListEntryInviteID(t *testing.T) { - s := setupTestStore(t) - ctx := context.Background() - - // Add an allow-list entry - entry := &store.AllowListEntry{ - ID: uuid.New().String(), - Email: "user@example.com", - Note: "test user", - AddedBy: "admin-1", - } - require.NoError(t, s.AddAllowListEntry(ctx, entry)) - - // Update its invite ID - inviteID := uuid.New().String() - err := s.UpdateAllowListEntryInviteID(ctx, "user@example.com", inviteID) - require.NoError(t, err) - - // Verify the update - got, err := s.GetAllowListEntry(ctx, "user@example.com") - require.NoError(t, err) - assert.Equal(t, inviteID, got.InviteID) -} - -func TestUpdateAllowListEntryInviteID_CaseInsensitive(t *testing.T) { - s := setupTestStore(t) - ctx := context.Background() - - entry := &store.AllowListEntry{ - ID: uuid.New().String(), - Email: "user@example.com", - Note: "test", - AddedBy: "admin-1", - } - require.NoError(t, s.AddAllowListEntry(ctx, entry)) - - inviteID := uuid.New().String() - err := s.UpdateAllowListEntryInviteID(ctx, "User@Example.COM", inviteID) - require.NoError(t, err) - - got, err := s.GetAllowListEntry(ctx, "user@example.com") - require.NoError(t, err) - assert.Equal(t, inviteID, got.InviteID) -} - -func TestUpdateAllowListEntryInviteID_NotFound(t *testing.T) { - s := setupTestStore(t) - ctx := context.Background() - - err := s.UpdateAllowListEntryInviteID(ctx, "nonexistent@example.com", "some-id") - assert.ErrorIs(t, err, store.ErrNotFound) -} - -func TestListAllowListEntriesWithInvites_NoInvite(t *testing.T) { - s := setupTestStore(t) - ctx := context.Background() - - entry := &store.AllowListEntry{ - ID: uuid.New().String(), - Email: "user@example.com", - Note: "test", - AddedBy: "admin-1", - } - require.NoError(t, s.AddAllowListEntry(ctx, entry)) - - result, err := s.ListAllowListEntriesWithInvites(ctx, store.ListOptions{Limit: 50}) - require.NoError(t, err) - require.Len(t, result.Items, 1) - - item := result.Items[0] - assert.Equal(t, "user@example.com", item.Email) - assert.Empty(t, item.InviteCodePrefix) - assert.Zero(t, item.InviteMaxUses) - assert.False(t, item.InviteRevoked) -} - -func TestListAllowListEntriesWithInvites_WithLinkedInvite(t *testing.T) { - s := setupTestStore(t) - ctx := context.Background() - - // Create an invite code - invite := &store.InviteCode{ - ID: uuid.New().String(), - CodeHash: "testhash123", - CodePrefix: "scion_inv_abcdefgh", - MaxUses: 1, - UseCount: 0, - ExpiresAt: time.Now().Add(24 * time.Hour), - CreatedBy: "admin-1", - Note: "test invite", - Created: time.Now(), - } - require.NoError(t, s.CreateInviteCode(ctx, invite)) - - // Create an allow-list entry linked to the invite - entry := &store.AllowListEntry{ - ID: uuid.New().String(), - Email: "user@example.com", - Note: "test", - AddedBy: "admin-1", - InviteID: invite.ID, - } - require.NoError(t, s.AddAllowListEntry(ctx, entry)) - - result, err := s.ListAllowListEntriesWithInvites(ctx, store.ListOptions{Limit: 50}) - require.NoError(t, err) - require.Len(t, result.Items, 1) - - item := result.Items[0] - assert.Equal(t, "user@example.com", item.Email) - assert.Equal(t, invite.ID, item.InviteID) - assert.Equal(t, "scion_inv_abcdefgh", item.InviteCodePrefix) - assert.Equal(t, 1, item.InviteMaxUses) - assert.Equal(t, 0, item.InviteUseCount) - assert.False(t, item.InviteRevoked) - assert.False(t, item.InviteExpiresAt.IsZero()) -} - -func TestListAllowListEntriesWithInvites_RevokedInvite(t *testing.T) { - s := setupTestStore(t) - ctx := context.Background() - - invite := &store.InviteCode{ - ID: uuid.New().String(), - CodeHash: "testhash456", - CodePrefix: "scion_inv_xyz12345", - MaxUses: 1, - ExpiresAt: time.Now().Add(24 * time.Hour), - Revoked: true, - CreatedBy: "admin-1", - Created: time.Now(), - } - require.NoError(t, s.CreateInviteCode(ctx, invite)) - - entry := &store.AllowListEntry{ - ID: uuid.New().String(), - Email: "revoked@example.com", - Note: "test", - AddedBy: "admin-1", - InviteID: invite.ID, - } - require.NoError(t, s.AddAllowListEntry(ctx, entry)) - - result, err := s.ListAllowListEntriesWithInvites(ctx, store.ListOptions{Limit: 50}) - require.NoError(t, err) - require.Len(t, result.Items, 1) - assert.True(t, result.Items[0].InviteRevoked) -} - -func TestListAllowListEntriesWithInvites_MixedEntries(t *testing.T) { - s := setupTestStore(t) - ctx := context.Background() - - // Create an invite - invite := &store.InviteCode{ - ID: uuid.New().String(), - CodeHash: "testhash789", - CodePrefix: "scion_inv_mixed123", - MaxUses: 5, - UseCount: 2, - ExpiresAt: time.Now().Add(48 * time.Hour), - CreatedBy: "admin-1", - Created: time.Now(), - } - require.NoError(t, s.CreateInviteCode(ctx, invite)) - - // Entry with invite - entry1 := &store.AllowListEntry{ - ID: uuid.New().String(), - Email: "with-invite@example.com", - Note: "has invite", - AddedBy: "admin-1", - InviteID: invite.ID, - } - require.NoError(t, s.AddAllowListEntry(ctx, entry1)) - - // Entry without invite - entry2 := &store.AllowListEntry{ - ID: uuid.New().String(), - Email: "no-invite@example.com", - Note: "no invite", - AddedBy: "admin-1", - } - require.NoError(t, s.AddAllowListEntry(ctx, entry2)) - - result, err := s.ListAllowListEntriesWithInvites(ctx, store.ListOptions{Limit: 50}) - require.NoError(t, err) - assert.Equal(t, 2, result.TotalCount) - require.Len(t, result.Items, 2) - - // Find the entry with invite - var withInvite, withoutInvite store.AllowListEntryWithInvite - for _, item := range result.Items { - if item.Email == "with-invite@example.com" { - withInvite = item - } else { - withoutInvite = item - } - } - - assert.Equal(t, "scion_inv_mixed123", withInvite.InviteCodePrefix) - assert.Equal(t, 5, withInvite.InviteMaxUses) - assert.Equal(t, 2, withInvite.InviteUseCount) - - assert.Empty(t, withoutInvite.InviteCodePrefix) - assert.Zero(t, withoutInvite.InviteMaxUses) -} diff --git a/pkg/store/sqlite/brokersecret.go b/pkg/store/sqlite/brokersecret.go deleted file mode 100644 index b215a4d5a..000000000 --- a/pkg/store/sqlite/brokersecret.go +++ /dev/null @@ -1,290 +0,0 @@ -// Copyright 2026 Google LLC -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -// Package sqlite provides a SQLite implementation of the Store interface. -package sqlite - -import ( - "context" - "database/sql" - "errors" - "fmt" - "strings" - "time" - - "github.com/GoogleCloudPlatform/scion/pkg/store" -) - -// ============================================================================ -// Broker Secret Operations -// ============================================================================ - -// CreateBrokerSecret creates a new broker secret record. -func (s *SQLiteStore) CreateBrokerSecret(ctx context.Context, secret *store.BrokerSecret) error { - if secret.BrokerID == "" { - return store.ErrInvalidInput - } - - now := time.Now() - if secret.CreatedAt.IsZero() { - secret.CreatedAt = now - } - if secret.Algorithm == "" { - secret.Algorithm = store.BrokerSecretAlgorithmHMACSHA256 - } - if secret.Status == "" { - secret.Status = store.BrokerSecretStatusActive - } - - _, err := s.db.ExecContext(ctx, ` - INSERT INTO broker_secrets ( - broker_id, secret_key, algorithm, - created_at, rotated_at, expires_at, status - ) VALUES (?, ?, ?, ?, ?, ?, ?) - `, - secret.BrokerID, secret.SecretKey, secret.Algorithm, - secret.CreatedAt, nullableTime(secret.RotatedAt), nullableTime(secret.ExpiresAt), secret.Status, - ) - if err != nil { - if strings.Contains(err.Error(), "UNIQUE constraint failed") { - return store.ErrAlreadyExists - } - if strings.Contains(err.Error(), "FOREIGN KEY constraint failed") { - return fmt.Errorf("broker %s does not exist: %w", secret.BrokerID, store.ErrNotFound) - } - return err - } - return nil -} - -// GetBrokerSecret retrieves a broker secret by broker ID. -func (s *SQLiteStore) GetBrokerSecret(ctx context.Context, brokerID string) (*store.BrokerSecret, error) { - secret := &store.BrokerSecret{} - var rotatedAt, expiresAt sql.NullTime - - err := s.db.QueryRowContext(ctx, ` - SELECT broker_id, secret_key, algorithm, - created_at, rotated_at, expires_at, status - FROM broker_secrets WHERE broker_id = ? - `, brokerID).Scan( - &secret.BrokerID, &secret.SecretKey, &secret.Algorithm, - &secret.CreatedAt, &rotatedAt, &expiresAt, &secret.Status, - ) - if err != nil { - if errors.Is(err, sql.ErrNoRows) { - return nil, store.ErrNotFound - } - return nil, err - } - - if rotatedAt.Valid { - secret.RotatedAt = rotatedAt.Time - } - if expiresAt.Valid { - secret.ExpiresAt = expiresAt.Time - } - - return secret, nil -} - -// GetActiveSecrets retrieves all active and deprecated secrets for a broker. -// This supports dual-secret validation during rotation grace periods. -func (s *SQLiteStore) GetActiveSecrets(ctx context.Context, brokerID string) ([]*store.BrokerSecret, error) { - rows, err := s.db.QueryContext(ctx, ` - SELECT broker_id, secret_key, algorithm, - created_at, rotated_at, expires_at, status - FROM broker_secrets - WHERE broker_id = ? AND status IN (?, ?) - ORDER BY created_at DESC - `, brokerID, store.BrokerSecretStatusActive, store.BrokerSecretStatusDeprecated) - if err != nil { - return nil, err - } - defer rows.Close() - - var secrets []*store.BrokerSecret - for rows.Next() { - secret := &store.BrokerSecret{} - var rotatedAt, expiresAt sql.NullTime - - if err := rows.Scan( - &secret.BrokerID, &secret.SecretKey, &secret.Algorithm, - &secret.CreatedAt, &rotatedAt, &expiresAt, &secret.Status, - ); err != nil { - return nil, err - } - - if rotatedAt.Valid { - secret.RotatedAt = rotatedAt.Time - } - if expiresAt.Valid { - secret.ExpiresAt = expiresAt.Time - } - - secrets = append(secrets, secret) - } - - if err := rows.Err(); err != nil { - return nil, err - } - - return secrets, nil -} - -// UpdateBrokerSecret updates an existing broker secret. -func (s *SQLiteStore) UpdateBrokerSecret(ctx context.Context, secret *store.BrokerSecret) error { - result, err := s.db.ExecContext(ctx, ` - UPDATE broker_secrets SET - secret_key = ?, - algorithm = ?, - rotated_at = ?, - expires_at = ?, - status = ? - WHERE broker_id = ? - `, - secret.SecretKey, secret.Algorithm, - nullableTime(secret.RotatedAt), nullableTime(secret.ExpiresAt), secret.Status, - secret.BrokerID, - ) - if err != nil { - return err - } - - rows, err := result.RowsAffected() - if err != nil { - return err - } - if rows == 0 { - return store.ErrNotFound - } - return nil -} - -// DeleteBrokerSecret removes a broker secret. -func (s *SQLiteStore) DeleteBrokerSecret(ctx context.Context, brokerID string) error { - result, err := s.db.ExecContext(ctx, ` - DELETE FROM broker_secrets WHERE broker_id = ? - `, brokerID) - if err != nil { - return err - } - - rows, err := result.RowsAffected() - if err != nil { - return err - } - if rows == 0 { - return store.ErrNotFound - } - return nil -} - -// ============================================================================ -// Broker Join Token Operations -// ============================================================================ - -// CreateJoinToken creates a new join token for broker registration. -func (s *SQLiteStore) CreateJoinToken(ctx context.Context, token *store.BrokerJoinToken) error { - if token.BrokerID == "" || token.TokenHash == "" { - return store.ErrInvalidInput - } - - now := time.Now() - if token.CreatedAt.IsZero() { - token.CreatedAt = now - } - - _, err := s.db.ExecContext(ctx, ` - INSERT INTO broker_join_tokens ( - broker_id, token_hash, expires_at, created_at, created_by - ) VALUES (?, ?, ?, ?, ?) - `, - token.BrokerID, token.TokenHash, token.ExpiresAt, token.CreatedAt, token.CreatedBy, - ) - if err != nil { - if strings.Contains(err.Error(), "UNIQUE constraint failed") { - return store.ErrAlreadyExists - } - if strings.Contains(err.Error(), "FOREIGN KEY constraint failed") { - return store.ErrNotFound - } - return err - } - return nil -} - -// GetJoinToken retrieves a join token by token hash. -func (s *SQLiteStore) GetJoinToken(ctx context.Context, tokenHash string) (*store.BrokerJoinToken, error) { - token := &store.BrokerJoinToken{} - - err := s.db.QueryRowContext(ctx, ` - SELECT broker_id, token_hash, expires_at, created_at, created_by - FROM broker_join_tokens WHERE token_hash = ? - `, tokenHash).Scan( - &token.BrokerID, &token.TokenHash, &token.ExpiresAt, &token.CreatedAt, &token.CreatedBy, - ) - if err != nil { - if errors.Is(err, sql.ErrNoRows) { - return nil, store.ErrNotFound - } - return nil, err - } - return token, nil -} - -// GetJoinTokenByBrokerID retrieves a join token by broker ID. -func (s *SQLiteStore) GetJoinTokenByBrokerID(ctx context.Context, brokerID string) (*store.BrokerJoinToken, error) { - token := &store.BrokerJoinToken{} - - err := s.db.QueryRowContext(ctx, ` - SELECT broker_id, token_hash, expires_at, created_at, created_by - FROM broker_join_tokens WHERE broker_id = ? - `, brokerID).Scan( - &token.BrokerID, &token.TokenHash, &token.ExpiresAt, &token.CreatedAt, &token.CreatedBy, - ) - if err != nil { - if errors.Is(err, sql.ErrNoRows) { - return nil, store.ErrNotFound - } - return nil, err - } - return token, nil -} - -// DeleteJoinToken removes a join token by broker ID. -func (s *SQLiteStore) DeleteJoinToken(ctx context.Context, brokerID string) error { - result, err := s.db.ExecContext(ctx, ` - DELETE FROM broker_join_tokens WHERE broker_id = ? - `, brokerID) - if err != nil { - return err - } - - rows, err := result.RowsAffected() - if err != nil { - return err - } - if rows == 0 { - return store.ErrNotFound - } - return nil -} - -// CleanExpiredJoinTokens removes all expired join tokens. -func (s *SQLiteStore) CleanExpiredJoinTokens(ctx context.Context) error { - _, err := s.db.ExecContext(ctx, ` - DELETE FROM broker_join_tokens WHERE expires_at < ? - `, time.Now()) - return err -} diff --git a/pkg/store/sqlite/brokersecret_test.go b/pkg/store/sqlite/brokersecret_test.go deleted file mode 100644 index de8fa2f16..000000000 --- a/pkg/store/sqlite/brokersecret_test.go +++ /dev/null @@ -1,334 +0,0 @@ -// Copyright 2026 Google LLC -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -//go:build !no_sqlite - -package sqlite - -import ( - "context" - "errors" - "testing" - "time" - - "github.com/GoogleCloudPlatform/scion/pkg/store" - "github.com/google/uuid" -) - -func TestBrokerSecretCRUD(t *testing.T) { - s := setupTestStore(t) - ctx := context.Background() - - // First create a runtime broker to satisfy FK constraint - brokerID := uuid.New().String() - broker := &store.RuntimeBroker{ - ID: brokerID, - Name: "test-host", - Slug: "test-host", - Status: store.BrokerStatusOnline, - Created: time.Now(), - Updated: time.Now(), - } - if err := s.CreateRuntimeBroker(ctx, broker); err != nil { - t.Fatalf("failed to create runtime broker: %v", err) - } - - // Test CreateBrokerSecret - secret := &store.BrokerSecret{ - BrokerID: brokerID, - SecretKey: []byte("test-secret-key-32-bytes-long!!"), - Algorithm: store.BrokerSecretAlgorithmHMACSHA256, - Status: store.BrokerSecretStatusActive, - } - if err := s.CreateBrokerSecret(ctx, secret); err != nil { - t.Fatalf("CreateBrokerSecret failed: %v", err) - } - - // Verify timestamps were set - if secret.CreatedAt.IsZero() { - t.Error("CreatedAt should be set automatically") - } - - // Test GetBrokerSecret - retrieved, err := s.GetBrokerSecret(ctx, brokerID) - if err != nil { - t.Fatalf("GetBrokerSecret failed: %v", err) - } - if retrieved.BrokerID != brokerID { - t.Errorf("BrokerID mismatch: got %s, want %s", retrieved.BrokerID, brokerID) - } - if string(retrieved.SecretKey) != string(secret.SecretKey) { - t.Error("SecretKey mismatch") - } - if retrieved.Algorithm != store.BrokerSecretAlgorithmHMACSHA256 { - t.Errorf("Algorithm mismatch: got %s, want %s", retrieved.Algorithm, store.BrokerSecretAlgorithmHMACSHA256) - } - if retrieved.Status != store.BrokerSecretStatusActive { - t.Errorf("Status mismatch: got %s, want %s", retrieved.Status, store.BrokerSecretStatusActive) - } - - // Test duplicate create returns error - if err := s.CreateBrokerSecret(ctx, secret); err != store.ErrAlreadyExists { - t.Errorf("Expected ErrAlreadyExists, got: %v", err) - } - - // Test UpdateBrokerSecret - newKey := []byte("new-secret-key-32-bytes-long!!!") - retrieved.SecretKey = newKey - retrieved.RotatedAt = time.Now() - retrieved.Status = store.BrokerSecretStatusDeprecated - - if err := s.UpdateBrokerSecret(ctx, retrieved); err != nil { - t.Fatalf("UpdateBrokerSecret failed: %v", err) - } - - // Verify update - updated, err := s.GetBrokerSecret(ctx, brokerID) - if err != nil { - t.Fatalf("GetBrokerSecret after update failed: %v", err) - } - if string(updated.SecretKey) != string(newKey) { - t.Error("SecretKey not updated") - } - if updated.Status != store.BrokerSecretStatusDeprecated { - t.Errorf("Status not updated: got %s, want %s", updated.Status, store.BrokerSecretStatusDeprecated) - } - if updated.RotatedAt.IsZero() { - t.Error("RotatedAt should be set") - } - - // Test DeleteBrokerSecret - if err := s.DeleteBrokerSecret(ctx, brokerID); err != nil { - t.Fatalf("DeleteBrokerSecret failed: %v", err) - } - - // Verify deletion - _, err = s.GetBrokerSecret(ctx, brokerID) - if err != store.ErrNotFound { - t.Errorf("Expected ErrNotFound after delete, got: %v", err) - } - - // Test delete non-existent returns error - if err := s.DeleteBrokerSecret(ctx, "non-existent"); err != store.ErrNotFound { - t.Errorf("Expected ErrNotFound for non-existent delete, got: %v", err) - } -} - -func TestBrokerSecretForeignKey(t *testing.T) { - s := setupTestStore(t) - ctx := context.Background() - - // Try to create secret for non-existent broker - secret := &store.BrokerSecret{ - BrokerID: "non-existent-host", - SecretKey: []byte("test-secret"), - Algorithm: store.BrokerSecretAlgorithmHMACSHA256, - Status: store.BrokerSecretStatusActive, - } - - err := s.CreateBrokerSecret(ctx, secret) - if err == nil { - t.Error("Expected error when creating secret for non-existent broker") - } - if !errors.Is(err, store.ErrNotFound) { - t.Errorf("Expected ErrNotFound for FK violation, got: %v", err) - } -} - -func TestBrokerJoinTokenCRUD(t *testing.T) { - s := setupTestStore(t) - ctx := context.Background() - - // First create a runtime broker to satisfy FK constraint - brokerID := uuid.New().String() - broker := &store.RuntimeBroker{ - ID: brokerID, - Name: "test-host-for-token", - Slug: "test-host-for-token", - Status: store.BrokerStatusOffline, - Created: time.Now(), - Updated: time.Now(), - } - if err := s.CreateRuntimeBroker(ctx, broker); err != nil { - t.Fatalf("failed to create runtime broker: %v", err) - } - - // Test CreateJoinToken - token := &store.BrokerJoinToken{ - BrokerID: brokerID, - TokenHash: "test-token-hash-abc123", - ExpiresAt: time.Now().Add(1 * time.Hour), - CreatedBy: "admin-user-id", - } - if err := s.CreateJoinToken(ctx, token); err != nil { - t.Fatalf("CreateJoinToken failed: %v", err) - } - - // Verify timestamps were set - if token.CreatedAt.IsZero() { - t.Error("CreatedAt should be set automatically") - } - - // Test GetJoinToken by hash - retrieved, err := s.GetJoinToken(ctx, "test-token-hash-abc123") - if err != nil { - t.Fatalf("GetJoinToken failed: %v", err) - } - if retrieved.BrokerID != brokerID { - t.Errorf("BrokerID mismatch: got %s, want %s", retrieved.BrokerID, brokerID) - } - if retrieved.TokenHash != "test-token-hash-abc123" { - t.Errorf("TokenHash mismatch: got %s, want %s", retrieved.TokenHash, "test-token-hash-abc123") - } - if retrieved.CreatedBy != "admin-user-id" { - t.Errorf("CreatedBy mismatch: got %s, want %s", retrieved.CreatedBy, "admin-user-id") - } - - // Test GetJoinTokenByBrokerID - byHost, err := s.GetJoinTokenByBrokerID(ctx, brokerID) - if err != nil { - t.Fatalf("GetJoinTokenByBrokerID failed: %v", err) - } - if byHost.TokenHash != "test-token-hash-abc123" { - t.Errorf("TokenHash mismatch: got %s, want %s", byHost.TokenHash, "test-token-hash-abc123") - } - - // Test duplicate create returns error - if err := s.CreateJoinToken(ctx, token); err != store.ErrAlreadyExists { - t.Errorf("Expected ErrAlreadyExists, got: %v", err) - } - - // Test DeleteJoinToken - if err := s.DeleteJoinToken(ctx, brokerID); err != nil { - t.Fatalf("DeleteJoinToken failed: %v", err) - } - - // Verify deletion - _, err = s.GetJoinToken(ctx, "test-token-hash-abc123") - if err != store.ErrNotFound { - t.Errorf("Expected ErrNotFound after delete, got: %v", err) - } - - // Test delete non-existent returns error - if err := s.DeleteJoinToken(ctx, "non-existent"); err != store.ErrNotFound { - t.Errorf("Expected ErrNotFound for non-existent delete, got: %v", err) - } -} - -func TestCleanExpiredJoinTokens(t *testing.T) { - s := setupTestStore(t) - ctx := context.Background() - - // Create two brokers - host1ID := uuid.New().String() - host2ID := uuid.New().String() - for i, id := range []string{host1ID, host2ID} { - broker := &store.RuntimeBroker{ - ID: id, - Name: "test-host-" + string(rune('a'+i)), - Slug: "test-host-" + string(rune('a'+i)), - Status: store.BrokerStatusOffline, - Created: time.Now(), - Updated: time.Now(), - } - if err := s.CreateRuntimeBroker(ctx, broker); err != nil { - t.Fatalf("failed to create runtime broker: %v", err) - } - } - - // Create an expired token and a valid token - expiredToken := &store.BrokerJoinToken{ - BrokerID: host1ID, - TokenHash: "expired-token-hash", - ExpiresAt: time.Now().Add(-1 * time.Hour), // Already expired - CreatedBy: "admin", - } - validToken := &store.BrokerJoinToken{ - BrokerID: host2ID, - TokenHash: "valid-token-hash", - ExpiresAt: time.Now().Add(1 * time.Hour), // Still valid - CreatedBy: "admin", - } - - if err := s.CreateJoinToken(ctx, expiredToken); err != nil { - t.Fatalf("CreateJoinToken (expired) failed: %v", err) - } - if err := s.CreateJoinToken(ctx, validToken); err != nil { - t.Fatalf("CreateJoinToken (valid) failed: %v", err) - } - - // Clean expired tokens - if err := s.CleanExpiredJoinTokens(ctx); err != nil { - t.Fatalf("CleanExpiredJoinTokens failed: %v", err) - } - - // Verify expired token is gone - _, err := s.GetJoinToken(ctx, "expired-token-hash") - if err != store.ErrNotFound { - t.Errorf("Expected expired token to be deleted, got: %v", err) - } - - // Verify valid token still exists - _, err = s.GetJoinToken(ctx, "valid-token-hash") - if err != nil { - t.Errorf("Expected valid token to still exist, got: %v", err) - } -} - -func TestBrokerSecretCascadeDelete(t *testing.T) { - s := setupTestStore(t) - ctx := context.Background() - - // Create a runtime broker - brokerID := uuid.New().String() - broker := &store.RuntimeBroker{ - ID: brokerID, - Name: "cascade-test-host", - Slug: "cascade-test-host", - Status: store.BrokerStatusOnline, - Created: time.Now(), - Updated: time.Now(), - } - if err := s.CreateRuntimeBroker(ctx, broker); err != nil { - t.Fatalf("failed to create runtime broker: %v", err) - } - - // Create a secret for the broker - secret := &store.BrokerSecret{ - BrokerID: brokerID, - SecretKey: []byte("test-secret"), - Algorithm: store.BrokerSecretAlgorithmHMACSHA256, - Status: store.BrokerSecretStatusActive, - } - if err := s.CreateBrokerSecret(ctx, secret); err != nil { - t.Fatalf("CreateBrokerSecret failed: %v", err) - } - - // Verify secret exists - _, err := s.GetBrokerSecret(ctx, brokerID) - if err != nil { - t.Fatalf("GetBrokerSecret failed: %v", err) - } - - // Delete the runtime broker - if err := s.DeleteRuntimeBroker(ctx, brokerID); err != nil { - t.Fatalf("DeleteRuntimeBroker failed: %v", err) - } - - // Verify secret was cascade deleted - _, err = s.GetBrokerSecret(ctx, brokerID) - if err != store.ErrNotFound { - t.Errorf("Expected secret to be cascade deleted, got: %v", err) - } -} diff --git a/pkg/store/sqlite/gcp_service_account.go b/pkg/store/sqlite/gcp_service_account.go deleted file mode 100644 index 6b9d9f9ed..000000000 --- a/pkg/store/sqlite/gcp_service_account.go +++ /dev/null @@ -1,189 +0,0 @@ -// Copyright 2026 Google LLC -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package sqlite - -import ( - "context" - "database/sql" - "strings" - "time" - - "github.com/GoogleCloudPlatform/scion/pkg/store" -) - -func (s *SQLiteStore) CreateGCPServiceAccount(ctx context.Context, sa *store.GCPServiceAccount) error { - if sa.CreatedAt.IsZero() { - sa.CreatedAt = time.Now() - } - - scopesStr := strings.Join(sa.DefaultScopes, ",") - - _, err := s.db.ExecContext(ctx, ` - INSERT INTO gcp_service_accounts (id, scope, scope_id, email, project_id, display_name, default_scopes, verified, verified_at, created_by, created_at, managed, managed_by) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, - sa.ID, sa.Scope, sa.ScopeID, sa.Email, sa.ProjectID, sa.DisplayName, - scopesStr, sa.Verified, nullableTime(sa.VerifiedAt), sa.CreatedBy, sa.CreatedAt, - sa.Managed, sa.ManagedBy, - ) - if err != nil { - if strings.Contains(err.Error(), "UNIQUE constraint failed") { - return store.ErrAlreadyExists - } - return err - } - return nil -} - -func (s *SQLiteStore) GetGCPServiceAccount(ctx context.Context, id string) (*store.GCPServiceAccount, error) { - var sa store.GCPServiceAccount - var scopesStr string - var verifiedAt sql.NullTime - - err := s.db.QueryRowContext(ctx, ` - SELECT id, scope, scope_id, email, project_id, display_name, default_scopes, verified, verified_at, created_by, created_at, managed, managed_by - FROM gcp_service_accounts WHERE id = ?`, id, - ).Scan(&sa.ID, &sa.Scope, &sa.ScopeID, &sa.Email, &sa.ProjectID, &sa.DisplayName, - &scopesStr, &sa.Verified, &verifiedAt, &sa.CreatedBy, &sa.CreatedAt, - &sa.Managed, &sa.ManagedBy, - ) - if err == sql.ErrNoRows { - return nil, store.ErrNotFound - } - if err != nil { - return nil, err - } - - if scopesStr != "" { - sa.DefaultScopes = strings.Split(scopesStr, ",") - } - if verifiedAt.Valid { - sa.VerifiedAt = verifiedAt.Time - } - - return &sa, nil -} - -func (s *SQLiteStore) UpdateGCPServiceAccount(ctx context.Context, sa *store.GCPServiceAccount) error { - scopesStr := strings.Join(sa.DefaultScopes, ",") - - result, err := s.db.ExecContext(ctx, ` - UPDATE gcp_service_accounts - SET email = ?, project_id = ?, display_name = ?, default_scopes = ?, verified = ?, verified_at = ?, managed = ?, managed_by = ? - WHERE id = ?`, - sa.Email, sa.ProjectID, sa.DisplayName, scopesStr, sa.Verified, nullableTime(sa.VerifiedAt), - sa.Managed, sa.ManagedBy, sa.ID, - ) - if err != nil { - return err - } - rows, _ := result.RowsAffected() - if rows == 0 { - return store.ErrNotFound - } - return nil -} - -func (s *SQLiteStore) DeleteGCPServiceAccount(ctx context.Context, id string) error { - result, err := s.db.ExecContext(ctx, `DELETE FROM gcp_service_accounts WHERE id = ?`, id) - if err != nil { - return err - } - rows, _ := result.RowsAffected() - if rows == 0 { - return store.ErrNotFound - } - return nil -} - -func (s *SQLiteStore) ListGCPServiceAccounts(ctx context.Context, filter store.GCPServiceAccountFilter) ([]store.GCPServiceAccount, error) { - query := `SELECT id, scope, scope_id, email, project_id, display_name, default_scopes, verified, verified_at, created_by, created_at, managed, managed_by FROM gcp_service_accounts WHERE 1=1` - var args []interface{} - - if filter.Scope != "" { - query += ` AND scope = ?` - args = append(args, filter.Scope) - } - if filter.ScopeID != "" { - query += ` AND scope_id = ?` - args = append(args, filter.ScopeID) - } - if filter.Email != "" { - query += ` AND email = ?` - args = append(args, filter.Email) - } - if filter.Managed != nil { - query += ` AND managed = ?` - args = append(args, *filter.Managed) - } - - query += ` ORDER BY created_at DESC` - - rows, err := s.db.QueryContext(ctx, query, args...) - if err != nil { - return nil, err - } - defer rows.Close() - - var results []store.GCPServiceAccount - for rows.Next() { - var sa store.GCPServiceAccount - var scopesStr string - var verifiedAt sql.NullTime - - if err := rows.Scan(&sa.ID, &sa.Scope, &sa.ScopeID, &sa.Email, &sa.ProjectID, &sa.DisplayName, - &scopesStr, &sa.Verified, &verifiedAt, &sa.CreatedBy, &sa.CreatedAt, - &sa.Managed, &sa.ManagedBy, - ); err != nil { - return nil, err - } - - if scopesStr != "" { - sa.DefaultScopes = strings.Split(scopesStr, ",") - } - if verifiedAt.Valid { - sa.VerifiedAt = verifiedAt.Time - } - - results = append(results, sa) - } - - return results, rows.Err() -} - -func (s *SQLiteStore) CountGCPServiceAccounts(ctx context.Context, filter store.GCPServiceAccountFilter) (int, error) { - query := `SELECT COUNT(*) FROM gcp_service_accounts WHERE 1=1` - var args []interface{} - - if filter.Scope != "" { - query += ` AND scope = ?` - args = append(args, filter.Scope) - } - if filter.ScopeID != "" { - query += ` AND scope_id = ?` - args = append(args, filter.ScopeID) - } - if filter.Email != "" { - query += ` AND email = ?` - args = append(args, filter.Email) - } - if filter.Managed != nil { - query += ` AND managed = ?` - args = append(args, *filter.Managed) - } - - var count int - err := s.db.QueryRowContext(ctx, query, args...).Scan(&count) - return count, err -} diff --git a/pkg/store/sqlite/gcp_service_account_test.go b/pkg/store/sqlite/gcp_service_account_test.go deleted file mode 100644 index 3d42cd4e4..000000000 --- a/pkg/store/sqlite/gcp_service_account_test.go +++ /dev/null @@ -1,230 +0,0 @@ -//go:build !no_sqlite - -// Copyright 2026 Google LLC -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package sqlite - -import ( - "context" - "testing" - "time" - - "github.com/GoogleCloudPlatform/scion/pkg/store" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -func TestGCPServiceAccount_CRUD(t *testing.T) { - s := setupTestStore(t) - ctx := context.Background() - - sa := &store.GCPServiceAccount{ - ID: "sa-1", - Scope: store.ScopeProject, - ScopeID: "project-1", - Email: "agent@project.iam.gserviceaccount.com", - ProjectID: "my-project", - DisplayName: "Agent Worker", - DefaultScopes: []string{"https://www.googleapis.com/auth/cloud-platform"}, - CreatedBy: "user-1", - } - - // Create - err := s.CreateGCPServiceAccount(ctx, sa) - require.NoError(t, err) - assert.False(t, sa.CreatedAt.IsZero()) - - // Get - got, err := s.GetGCPServiceAccount(ctx, "sa-1") - require.NoError(t, err) - assert.Equal(t, "agent@project.iam.gserviceaccount.com", got.Email) - assert.Equal(t, "my-project", got.ProjectID) - assert.Equal(t, "Agent Worker", got.DisplayName) - assert.Equal(t, []string{"https://www.googleapis.com/auth/cloud-platform"}, got.DefaultScopes) - assert.False(t, got.Verified) - assert.Equal(t, "user-1", got.CreatedBy) - - // Update (verify) - got.Verified = true - got.VerifiedAt = time.Now() - err = s.UpdateGCPServiceAccount(ctx, got) - require.NoError(t, err) - - got2, err := s.GetGCPServiceAccount(ctx, "sa-1") - require.NoError(t, err) - assert.True(t, got2.Verified) - assert.False(t, got2.VerifiedAt.IsZero()) - - // List - list, err := s.ListGCPServiceAccounts(ctx, store.GCPServiceAccountFilter{ - Scope: store.ScopeProject, - ScopeID: "project-1", - }) - require.NoError(t, err) - assert.Len(t, list, 1) - assert.Equal(t, "sa-1", list[0].ID) - - // List with email filter - list, err = s.ListGCPServiceAccounts(ctx, store.GCPServiceAccountFilter{ - Email: "agent@project.iam.gserviceaccount.com", - }) - require.NoError(t, err) - assert.Len(t, list, 1) - - // List with wrong filter - list, err = s.ListGCPServiceAccounts(ctx, store.GCPServiceAccountFilter{ - ScopeID: "project-999", - Scope: store.ScopeProject, - }) - require.NoError(t, err) - assert.Len(t, list, 0) - - // Delete - err = s.DeleteGCPServiceAccount(ctx, "sa-1") - require.NoError(t, err) - - _, err = s.GetGCPServiceAccount(ctx, "sa-1") - assert.ErrorIs(t, err, store.ErrNotFound) -} - -func TestGCPServiceAccount_DuplicateEmail(t *testing.T) { - s := setupTestStore(t) - ctx := context.Background() - - sa1 := &store.GCPServiceAccount{ - ID: "sa-1", - Scope: store.ScopeProject, - ScopeID: "project-1", - Email: "agent@project.iam.gserviceaccount.com", - ProjectID: "my-project", - CreatedBy: "user-1", - } - err := s.CreateGCPServiceAccount(ctx, sa1) - require.NoError(t, err) - - // Same email, same scope = should fail - sa2 := &store.GCPServiceAccount{ - ID: "sa-2", - Scope: store.ScopeProject, - ScopeID: "project-1", - Email: "agent@project.iam.gserviceaccount.com", - ProjectID: "my-project", - CreatedBy: "user-1", - } - err = s.CreateGCPServiceAccount(ctx, sa2) - assert.ErrorIs(t, err, store.ErrAlreadyExists) - - // Same email, different scope = should succeed - sa3 := &store.GCPServiceAccount{ - ID: "sa-3", - Scope: store.ScopeProject, - ScopeID: "project-2", - Email: "agent@project.iam.gserviceaccount.com", - ProjectID: "my-project", - CreatedBy: "user-1", - } - err = s.CreateGCPServiceAccount(ctx, sa3) - assert.NoError(t, err) -} - -func TestGCPServiceAccount_ManagedFields(t *testing.T) { - s := setupTestStore(t) - ctx := context.Background() - - sa := &store.GCPServiceAccount{ - ID: "sa-managed-1", - Scope: store.ScopeProject, - ScopeID: "project-1", - Email: "scion-abc123@hub-project.iam.gserviceaccount.com", - ProjectID: "hub-project", - Managed: true, - ManagedBy: "hub-instance-1", - CreatedBy: "user-1", - } - - err := s.CreateGCPServiceAccount(ctx, sa) - require.NoError(t, err) - - got, err := s.GetGCPServiceAccount(ctx, "sa-managed-1") - require.NoError(t, err) - assert.True(t, got.Managed) - assert.Equal(t, "hub-instance-1", got.ManagedBy) - - // List with managed filter - managed := true - list, err := s.ListGCPServiceAccounts(ctx, store.GCPServiceAccountFilter{ - Scope: store.ScopeProject, - ScopeID: "project-1", - Managed: &managed, - }) - require.NoError(t, err) - assert.Len(t, list, 1) - assert.True(t, list[0].Managed) - - // Create a non-managed SA - sa2 := &store.GCPServiceAccount{ - ID: "sa-byosa-1", - Scope: store.ScopeProject, - ScopeID: "project-1", - Email: "user-sa@other-project.iam.gserviceaccount.com", - ProjectID: "other-project", - Managed: false, - CreatedBy: "user-1", - } - require.NoError(t, s.CreateGCPServiceAccount(ctx, sa2)) - - // Filter managed=true should return only the managed one - list, err = s.ListGCPServiceAccounts(ctx, store.GCPServiceAccountFilter{ - Scope: store.ScopeProject, - ScopeID: "project-1", - Managed: &managed, - }) - require.NoError(t, err) - assert.Len(t, list, 1) - assert.Equal(t, "sa-managed-1", list[0].ID) - - // Filter managed=false should return only the BYOSA one - notManaged := false - list, err = s.ListGCPServiceAccounts(ctx, store.GCPServiceAccountFilter{ - Scope: store.ScopeProject, - ScopeID: "project-1", - Managed: ¬Managed, - }) - require.NoError(t, err) - assert.Len(t, list, 1) - assert.Equal(t, "sa-byosa-1", list[0].ID) - - // No managed filter should return both - list, err = s.ListGCPServiceAccounts(ctx, store.GCPServiceAccountFilter{ - Scope: store.ScopeProject, - ScopeID: "project-1", - }) - require.NoError(t, err) - assert.Len(t, list, 2) -} - -func TestGCPServiceAccount_NotFound(t *testing.T) { - s := setupTestStore(t) - ctx := context.Background() - - _, err := s.GetGCPServiceAccount(ctx, "nonexistent") - assert.ErrorIs(t, err, store.ErrNotFound) - - err = s.DeleteGCPServiceAccount(ctx, "nonexistent") - assert.ErrorIs(t, err, store.ErrNotFound) - - err = s.UpdateGCPServiceAccount(ctx, &store.GCPServiceAccount{ID: "nonexistent"}) - assert.ErrorIs(t, err, store.ErrNotFound) -} diff --git a/pkg/store/sqlite/github_installation.go b/pkg/store/sqlite/github_installation.go deleted file mode 100644 index 03322ba69..000000000 --- a/pkg/store/sqlite/github_installation.go +++ /dev/null @@ -1,180 +0,0 @@ -// Copyright 2026 Google LLC -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package sqlite - -import ( - "context" - "database/sql" - "time" - - "github.com/GoogleCloudPlatform/scion/pkg/store" -) - -func (s *SQLiteStore) CreateGitHubInstallation(ctx context.Context, installation *store.GitHubInstallation) error { - if installation.CreatedAt.IsZero() { - installation.CreatedAt = time.Now() - } - if installation.UpdatedAt.IsZero() { - installation.UpdatedAt = installation.CreatedAt - } - if installation.Status == "" { - installation.Status = store.GitHubInstallationStatusActive - } - - _, err := s.db.ExecContext(ctx, ` - INSERT OR IGNORE INTO github_installations (installation_id, account_login, account_type, app_id, repositories, status, created_at, updated_at) - VALUES (?, ?, ?, ?, ?, ?, ?, ?)`, - installation.InstallationID, installation.AccountLogin, installation.AccountType, - installation.AppID, marshalJSON(installation.Repositories), - installation.Status, installation.CreatedAt, installation.UpdatedAt, - ) - if err != nil { - return err - } - return nil -} - -func (s *SQLiteStore) GetGitHubInstallation(ctx context.Context, installationID int64) (*store.GitHubInstallation, error) { - var inst store.GitHubInstallation - var repos string - - err := s.db.QueryRowContext(ctx, ` - SELECT installation_id, account_login, account_type, app_id, repositories, status, created_at, updated_at - FROM github_installations WHERE installation_id = ?`, installationID, - ).Scan(&inst.InstallationID, &inst.AccountLogin, &inst.AccountType, - &inst.AppID, &repos, &inst.Status, &inst.CreatedAt, &inst.UpdatedAt, - ) - if err == sql.ErrNoRows { - return nil, store.ErrNotFound - } - if err != nil { - return nil, err - } - - unmarshalJSON(repos, &inst.Repositories) - return &inst, nil -} - -func (s *SQLiteStore) UpdateGitHubInstallation(ctx context.Context, installation *store.GitHubInstallation) error { - installation.UpdatedAt = time.Now() - - result, err := s.db.ExecContext(ctx, ` - UPDATE github_installations SET - account_login = ?, account_type = ?, app_id = ?, - repositories = ?, status = ?, updated_at = ? - WHERE installation_id = ?`, - installation.AccountLogin, installation.AccountType, installation.AppID, - marshalJSON(installation.Repositories), installation.Status, installation.UpdatedAt, - installation.InstallationID, - ) - if err != nil { - return err - } - - rowsAffected, err := result.RowsAffected() - if err != nil { - return err - } - if rowsAffected == 0 { - return store.ErrNotFound - } - return nil -} - -func (s *SQLiteStore) DeleteGitHubInstallation(ctx context.Context, installationID int64) error { - result, err := s.db.ExecContext(ctx, `DELETE FROM github_installations WHERE installation_id = ?`, installationID) - if err != nil { - return err - } - - rowsAffected, err := result.RowsAffected() - if err != nil { - return err - } - if rowsAffected == 0 { - return store.ErrNotFound - } - return nil -} - -func (s *SQLiteStore) GetInstallationForRepository(ctx context.Context, repoFullName string) (*store.GitHubInstallation, error) { - // Search active installations whose repositories JSON array contains the repo. - installations, err := s.ListGitHubInstallations(ctx, store.GitHubInstallationFilter{ - Status: store.GitHubInstallationStatusActive, - }) - if err != nil { - return nil, err - } - - for i := range installations { - for _, repo := range installations[i].Repositories { - if repo == repoFullName { - return &installations[i], nil - } - } - } - return nil, store.ErrNotFound -} - -func (s *SQLiteStore) ListGitHubInstallations(ctx context.Context, filter store.GitHubInstallationFilter) ([]store.GitHubInstallation, error) { - query := "SELECT installation_id, account_login, account_type, app_id, repositories, status, created_at, updated_at FROM github_installations WHERE 1=1" - var args []interface{} - - if filter.AccountLogin != "" { - query += " AND account_login = ?" - args = append(args, filter.AccountLogin) - } - if filter.Status != "" { - query += " AND status = ?" - args = append(args, filter.Status) - } - if filter.AppID != 0 { - query += " AND app_id = ?" - args = append(args, filter.AppID) - } - - query += " ORDER BY created_at ASC" - - rows, err := s.db.QueryContext(ctx, query, args...) - if err != nil { - return nil, err - } - defer rows.Close() - - var results []store.GitHubInstallation - for rows.Next() { - var inst store.GitHubInstallation - var repos string - - if err := rows.Scan(&inst.InstallationID, &inst.AccountLogin, &inst.AccountType, - &inst.AppID, &repos, &inst.Status, &inst.CreatedAt, &inst.UpdatedAt); err != nil { - return nil, err - } - - unmarshalJSON(repos, &inst.Repositories) - results = append(results, inst) - } - - if err := rows.Err(); err != nil { - return nil, err - } - - // Ensure we never return nil slice (return empty slice instead) - if results == nil { - results = []store.GitHubInstallation{} - } - - return results, nil -} diff --git a/pkg/store/sqlite/github_installation_test.go b/pkg/store/sqlite/github_installation_test.go deleted file mode 100644 index 5be418331..000000000 --- a/pkg/store/sqlite/github_installation_test.go +++ /dev/null @@ -1,184 +0,0 @@ -// Copyright 2026 Google LLC -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -//go:build !no_sqlite - -package sqlite - -import ( - "context" - "testing" - - "github.com/GoogleCloudPlatform/scion/pkg/store" -) - -func TestGitHubInstallation_CRUD(t *testing.T) { - s := setupTestStore(t) - ctx := context.Background() - - inst := &store.GitHubInstallation{ - InstallationID: 12345, - AccountLogin: "acme-org", - AccountType: "Organization", - AppID: 42, - Repositories: []string{"widgets", "api"}, - Status: store.GitHubInstallationStatusActive, - } - - // Create - if err := s.CreateGitHubInstallation(ctx, inst); err != nil { - t.Fatalf("CreateGitHubInstallation failed: %v", err) - } - - // Get - got, err := s.GetGitHubInstallation(ctx, 12345) - if err != nil { - t.Fatalf("GetGitHubInstallation failed: %v", err) - } - if got.AccountLogin != "acme-org" { - t.Errorf("expected account_login acme-org, got %s", got.AccountLogin) - } - if got.AccountType != "Organization" { - t.Errorf("expected account_type Organization, got %s", got.AccountType) - } - if got.AppID != 42 { - t.Errorf("expected app_id 42, got %d", got.AppID) - } - if len(got.Repositories) != 2 || got.Repositories[0] != "widgets" { - t.Errorf("expected repos [widgets, api], got %v", got.Repositories) - } - if got.Status != store.GitHubInstallationStatusActive { - t.Errorf("expected status active, got %s", got.Status) - } - - // Update - got.Status = store.GitHubInstallationStatusSuspended - got.Repositories = []string{"widgets"} - if err := s.UpdateGitHubInstallation(ctx, got); err != nil { - t.Fatalf("UpdateGitHubInstallation failed: %v", err) - } - - updated, err := s.GetGitHubInstallation(ctx, 12345) - if err != nil { - t.Fatalf("GetGitHubInstallation after update failed: %v", err) - } - if updated.Status != store.GitHubInstallationStatusSuspended { - t.Errorf("expected status suspended, got %s", updated.Status) - } - if len(updated.Repositories) != 1 { - t.Errorf("expected 1 repo, got %d", len(updated.Repositories)) - } - - // Delete - if err := s.DeleteGitHubInstallation(ctx, 12345); err != nil { - t.Fatalf("DeleteGitHubInstallation failed: %v", err) - } - - _, err = s.GetGitHubInstallation(ctx, 12345) - if err != store.ErrNotFound { - t.Errorf("expected ErrNotFound after delete, got %v", err) - } -} - -func TestGitHubInstallation_CreateIdempotent(t *testing.T) { - s := setupTestStore(t) - ctx := context.Background() - - inst := &store.GitHubInstallation{ - InstallationID: 11111, - AccountLogin: "alice", - AccountType: "User", - AppID: 42, - Status: store.GitHubInstallationStatusActive, - } - - if err := s.CreateGitHubInstallation(ctx, inst); err != nil { - t.Fatalf("first create failed: %v", err) - } - - // Second create should be a no-op (INSERT OR IGNORE) - if err := s.CreateGitHubInstallation(ctx, inst); err != nil { - t.Fatalf("second create should not fail: %v", err) - } -} - -func TestGitHubInstallation_List(t *testing.T) { - s := setupTestStore(t) - ctx := context.Background() - - // Create several installations - for i, login := range []string{"org-a", "org-b", "user-c"} { - inst := &store.GitHubInstallation{ - InstallationID: int64(100 + i), - AccountLogin: login, - AccountType: "Organization", - AppID: 42, - Status: store.GitHubInstallationStatusActive, - } - if login == "user-c" { - inst.AccountType = "User" - inst.Status = store.GitHubInstallationStatusSuspended - } - if err := s.CreateGitHubInstallation(ctx, inst); err != nil { - t.Fatalf("CreateGitHubInstallation failed for %s: %v", login, err) - } - } - - // List all - all, err := s.ListGitHubInstallations(ctx, store.GitHubInstallationFilter{}) - if err != nil { - t.Fatalf("ListGitHubInstallations failed: %v", err) - } - if len(all) != 3 { - t.Errorf("expected 3 installations, got %d", len(all)) - } - - // Filter by status - active, err := s.ListGitHubInstallations(ctx, store.GitHubInstallationFilter{Status: "active"}) - if err != nil { - t.Fatalf("ListGitHubInstallations with status filter failed: %v", err) - } - if len(active) != 2 { - t.Errorf("expected 2 active installations, got %d", len(active)) - } - - // Filter by account login - byAccount, err := s.ListGitHubInstallations(ctx, store.GitHubInstallationFilter{AccountLogin: "org-a"}) - if err != nil { - t.Fatalf("ListGitHubInstallations with account filter failed: %v", err) - } - if len(byAccount) != 1 { - t.Errorf("expected 1 installation for org-a, got %d", len(byAccount)) - } -} - -func TestGitHubInstallation_NotFound(t *testing.T) { - s := setupTestStore(t) - ctx := context.Background() - - _, err := s.GetGitHubInstallation(ctx, 99999) - if err != store.ErrNotFound { - t.Errorf("expected ErrNotFound, got %v", err) - } - - err = s.UpdateGitHubInstallation(ctx, &store.GitHubInstallation{InstallationID: 99999}) - if err != store.ErrNotFound { - t.Errorf("expected ErrNotFound on update, got %v", err) - } - - err = s.DeleteGitHubInstallation(ctx, 99999) - if err != store.ErrNotFound { - t.Errorf("expected ErrNotFound on delete, got %v", err) - } -} diff --git a/pkg/store/sqlite/maintenance.go b/pkg/store/sqlite/maintenance.go deleted file mode 100644 index 4d48fdbda..000000000 --- a/pkg/store/sqlite/maintenance.go +++ /dev/null @@ -1,269 +0,0 @@ -// Copyright 2026 Google LLC -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package sqlite - -import ( - "context" - "database/sql" - "time" - - "github.com/GoogleCloudPlatform/scion/pkg/store" -) - -// ============================================================================ -// Maintenance Operation Operations -// ============================================================================ - -// ListMaintenanceOperations returns all registered operations and migrations. -func (s *SQLiteStore) ListMaintenanceOperations(ctx context.Context) ([]store.MaintenanceOperation, error) { - rows, err := s.db.QueryContext(ctx, ` - SELECT id, key, title, description, category, status, - created_at, started_at, completed_at, started_by, result, metadata - FROM maintenance_operations - ORDER BY category, created_at - `) - if err != nil { - return nil, err - } - defer rows.Close() - - var ops []store.MaintenanceOperation - for rows.Next() { - var op store.MaintenanceOperation - var startedAt, completedAt sql.NullTime - var startedBy, result, metadata sql.NullString - - if err := rows.Scan( - &op.ID, &op.Key, &op.Title, &op.Description, &op.Category, &op.Status, - &op.CreatedAt, &startedAt, &completedAt, &startedBy, &result, &metadata, - ); err != nil { - return nil, err - } - - if startedAt.Valid { - op.StartedAt = &startedAt.Time - } - if completedAt.Valid { - op.CompletedAt = &completedAt.Time - } - op.StartedBy = startedBy.String - op.Result = result.String - op.Metadata = metadata.String - - ops = append(ops, op) - } - return ops, rows.Err() -} - -// GetMaintenanceOperation returns a single operation by key. -func (s *SQLiteStore) GetMaintenanceOperation(ctx context.Context, key string) (*store.MaintenanceOperation, error) { - op := &store.MaintenanceOperation{} - var startedAt, completedAt sql.NullTime - var startedBy, result, metadata sql.NullString - - err := s.db.QueryRowContext(ctx, ` - SELECT id, key, title, description, category, status, - created_at, started_at, completed_at, started_by, result, metadata - FROM maintenance_operations WHERE key = ? - `, key).Scan( - &op.ID, &op.Key, &op.Title, &op.Description, &op.Category, &op.Status, - &op.CreatedAt, &startedAt, &completedAt, &startedBy, &result, &metadata, - ) - if err == sql.ErrNoRows { - return nil, store.ErrNotFound - } - if err != nil { - return nil, err - } - - if startedAt.Valid { - op.StartedAt = &startedAt.Time - } - if completedAt.Valid { - op.CompletedAt = &completedAt.Time - } - op.StartedBy = startedBy.String - op.Result = result.String - op.Metadata = metadata.String - - return op, nil -} - -// UpdateMaintenanceOperation updates an operation's status and result fields. -func (s *SQLiteStore) UpdateMaintenanceOperation(ctx context.Context, op *store.MaintenanceOperation) error { - res, err := s.db.ExecContext(ctx, ` - UPDATE maintenance_operations - SET status = ?, started_at = ?, completed_at = ?, started_by = ?, result = ?, metadata = ? - WHERE key = ? - `, - op.Status, - nullableTime(timeFromPtr(op.StartedAt)), - nullableTime(timeFromPtr(op.CompletedAt)), - nullableString(op.StartedBy), - nullableString(op.Result), - nullableString(op.Metadata), - op.Key, - ) - if err != nil { - return err - } - n, _ := res.RowsAffected() - if n == 0 { - return store.ErrNotFound - } - return nil -} - -// CreateMaintenanceRun inserts a new run record. -func (s *SQLiteStore) CreateMaintenanceRun(ctx context.Context, run *store.MaintenanceOperationRun) error { - _, err := s.db.ExecContext(ctx, ` - INSERT INTO maintenance_operation_runs ( - id, operation_key, status, started_at, completed_at, started_by, result, log - ) VALUES (?, ?, ?, ?, ?, ?, ?, ?) - `, - run.ID, run.OperationKey, run.Status, run.StartedAt, - nullableTime(timeFromPtr(run.CompletedAt)), - nullableString(run.StartedBy), - nullableString(run.Result), - run.Log, - ) - return err -} - -// UpdateMaintenanceRun updates a run's status, result, and log. -func (s *SQLiteStore) UpdateMaintenanceRun(ctx context.Context, run *store.MaintenanceOperationRun) error { - res, err := s.db.ExecContext(ctx, ` - UPDATE maintenance_operation_runs - SET status = ?, completed_at = ?, result = ?, log = ? - WHERE id = ? - `, - run.Status, - nullableTime(timeFromPtr(run.CompletedAt)), - nullableString(run.Result), - run.Log, - run.ID, - ) - if err != nil { - return err - } - n, _ := res.RowsAffected() - if n == 0 { - return store.ErrNotFound - } - return nil -} - -// GetMaintenanceRun returns a single run by ID. -func (s *SQLiteStore) GetMaintenanceRun(ctx context.Context, id string) (*store.MaintenanceOperationRun, error) { - run := &store.MaintenanceOperationRun{} - var completedAt sql.NullTime - var startedBy, result sql.NullString - - err := s.db.QueryRowContext(ctx, ` - SELECT id, operation_key, status, started_at, completed_at, started_by, result, log - FROM maintenance_operation_runs WHERE id = ? - `, id).Scan( - &run.ID, &run.OperationKey, &run.Status, &run.StartedAt, - &completedAt, &startedBy, &result, &run.Log, - ) - if err == sql.ErrNoRows { - return nil, store.ErrNotFound - } - if err != nil { - return nil, err - } - - if completedAt.Valid { - run.CompletedAt = &completedAt.Time - } - run.StartedBy = startedBy.String - run.Result = result.String - - return run, nil -} - -// AbortRunningMaintenanceOps transitions any "running" operation runs and -// migrations to "failed" with an appropriate result message. This is called at -// server startup to clean up operations interrupted by a restart. -func (s *SQLiteStore) AbortRunningMaintenanceOps(ctx context.Context) (int64, int64, error) { - now := sql.NullTime{Time: time.Now(), Valid: true} - result := `{"error":"aborted: server was restarted while operation was running"}` - - // Abort stalled runs. - res, err := s.db.ExecContext(ctx, ` - UPDATE maintenance_operation_runs - SET status = 'failed', completed_at = ?, result = ? - WHERE status = 'running' - `, now, result) - if err != nil { - return 0, 0, err - } - runs, _ := res.RowsAffected() - - // Reset stalled migrations back to pending (they can be retried). - res, err = s.db.ExecContext(ctx, ` - UPDATE maintenance_operations - SET status = 'pending', started_at = NULL, completed_at = NULL, result = ? - WHERE status = 'running' AND category = 'migration' - `, result) - if err != nil { - return runs, 0, err - } - migrations, _ := res.RowsAffected() - - return runs, migrations, nil -} - -// ListMaintenanceRuns returns runs for a given operation key, ordered by started_at DESC. -func (s *SQLiteStore) ListMaintenanceRuns(ctx context.Context, operationKey string, limit int) ([]store.MaintenanceOperationRun, error) { - if limit <= 0 { - limit = 20 - } - - rows, err := s.db.QueryContext(ctx, ` - SELECT id, operation_key, status, started_at, completed_at, started_by, result, log - FROM maintenance_operation_runs - WHERE operation_key = ? - ORDER BY started_at DESC - LIMIT ? - `, operationKey, limit) - if err != nil { - return nil, err - } - defer rows.Close() - - var runs []store.MaintenanceOperationRun - for rows.Next() { - var run store.MaintenanceOperationRun - var completedAt sql.NullTime - var startedBy, result sql.NullString - - if err := rows.Scan( - &run.ID, &run.OperationKey, &run.Status, &run.StartedAt, - &completedAt, &startedBy, &result, &run.Log, - ); err != nil { - return nil, err - } - - if completedAt.Valid { - run.CompletedAt = &completedAt.Time - } - run.StartedBy = startedBy.String - run.Result = result.String - - runs = append(runs, run) - } - return runs, rows.Err() -} diff --git a/pkg/store/sqlite/maintenance_test.go b/pkg/store/sqlite/maintenance_test.go deleted file mode 100644 index 87d2a25dd..000000000 --- a/pkg/store/sqlite/maintenance_test.go +++ /dev/null @@ -1,209 +0,0 @@ -// Copyright 2026 Google LLC -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -//go:build !no_sqlite - -package sqlite - -import ( - "context" - "testing" - "time" - - "github.com/GoogleCloudPlatform/scion/pkg/api" - "github.com/GoogleCloudPlatform/scion/pkg/store" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -func TestMaintenanceOperationsSeeded(t *testing.T) { - s := setupTestStore(t) - ctx := context.Background() - - ops, err := s.ListMaintenanceOperations(ctx) - require.NoError(t, err) - require.Len(t, ops, 5, "expected 5 seeded operations (1 migration + 4 operations)") - - // Verify categories - var migrations, operations int - for _, op := range ops { - switch op.Category { - case store.MaintenanceCategoryMigration: - migrations++ - case store.MaintenanceCategoryOperation: - operations++ - } - } - assert.Equal(t, 1, migrations, "expected 1 migration") - assert.Equal(t, 4, operations, "expected 4 operations") -} - -func TestMaintenanceGetOperationByKey(t *testing.T) { - s := setupTestStore(t) - ctx := context.Background() - - op, err := s.GetMaintenanceOperation(ctx, "secret-hub-id-migration") - require.NoError(t, err) - assert.Equal(t, "Secret Hub ID Namespace Migration", op.Title) - assert.Equal(t, store.MaintenanceCategoryMigration, op.Category) - assert.Equal(t, store.MaintenanceStatusPending, op.Status) - - // Not found - _, err = s.GetMaintenanceOperation(ctx, "nonexistent") - assert.ErrorIs(t, err, store.ErrNotFound) -} - -func TestMaintenanceUpdateOperation(t *testing.T) { - s := setupTestStore(t) - ctx := context.Background() - - op, err := s.GetMaintenanceOperation(ctx, "secret-hub-id-migration") - require.NoError(t, err) - - now := time.Now().UTC().Truncate(time.Second) - op.Status = store.MaintenanceStatusCompleted - op.StartedAt = &now - op.CompletedAt = &now - op.StartedBy = "admin-user" - op.Result = `{"migrated": 5}` - - err = s.UpdateMaintenanceOperation(ctx, op) - require.NoError(t, err) - - updated, err := s.GetMaintenanceOperation(ctx, "secret-hub-id-migration") - require.NoError(t, err) - assert.Equal(t, store.MaintenanceStatusCompleted, updated.Status) - assert.Equal(t, "admin-user", updated.StartedBy) - assert.Equal(t, `{"migrated": 5}`, updated.Result) -} - -func TestMaintenanceRunCRUD(t *testing.T) { - s := setupTestStore(t) - ctx := context.Background() - - runID := api.NewUUID() - now := time.Now().UTC().Truncate(time.Second) - - run := &store.MaintenanceOperationRun{ - ID: runID, - OperationKey: "pull-images", - Status: store.MaintenanceStatusRunning, - StartedAt: now, - StartedBy: "admin-user", - Log: "Pulling images...", - } - - err := s.CreateMaintenanceRun(ctx, run) - require.NoError(t, err) - - // Get - got, err := s.GetMaintenanceRun(ctx, runID) - require.NoError(t, err) - assert.Equal(t, "pull-images", got.OperationKey) - assert.Equal(t, store.MaintenanceStatusRunning, got.Status) - assert.Equal(t, "Pulling images...", got.Log) - - // Update - completedAt := now.Add(30 * time.Second) - got.Status = store.MaintenanceStatusCompleted - got.CompletedAt = &completedAt - got.Result = `{"pulled": 3}` - got.Log = "Pulling images...\nDone." - - err = s.UpdateMaintenanceRun(ctx, got) - require.NoError(t, err) - - updated, err := s.GetMaintenanceRun(ctx, runID) - require.NoError(t, err) - assert.Equal(t, store.MaintenanceStatusCompleted, updated.Status) - assert.Equal(t, `{"pulled": 3}`, updated.Result) - - // List - runs, err := s.ListMaintenanceRuns(ctx, "pull-images", 10) - require.NoError(t, err) - require.Len(t, runs, 1) - assert.Equal(t, runID, runs[0].ID) - - // Not found - _, err = s.GetMaintenanceRun(ctx, "nonexistent") - assert.ErrorIs(t, err, store.ErrNotFound) -} - -func TestAbortRunningMaintenanceOps(t *testing.T) { - s := setupTestStore(t) - ctx := context.Background() - - // Set a migration to "running" to simulate an interrupted migration. - op, err := s.GetMaintenanceOperation(ctx, "secret-hub-id-migration") - require.NoError(t, err) - now := time.Now() - op.Status = store.MaintenanceStatusRunning - op.StartedAt = &now - op.StartedBy = "admin@example.com" - require.NoError(t, s.UpdateMaintenanceOperation(ctx, op)) - - // Create two "running" operation runs to simulate interrupted operations. - for i, key := range []string{"pull-images", "rebuild-server"} { - run := &store.MaintenanceOperationRun{ - ID: api.NewUUID(), - OperationKey: key, - Status: store.MaintenanceStatusRunning, - StartedAt: now.Add(time.Duration(i) * time.Second), - StartedBy: "admin@example.com", - } - require.NoError(t, s.CreateMaintenanceRun(ctx, run)) - } - - // Create a completed run that should NOT be affected. - completed := now.Add(-time.Hour) - completedRun := &store.MaintenanceOperationRun{ - ID: api.NewUUID(), - OperationKey: "pull-images", - Status: store.MaintenanceStatusCompleted, - StartedAt: completed, - CompletedAt: &completed, - StartedBy: "admin@example.com", - } - require.NoError(t, s.CreateMaintenanceRun(ctx, completedRun)) - - // Abort all running operations. - runs, migrations, err := s.AbortRunningMaintenanceOps(ctx) - require.NoError(t, err) - assert.Equal(t, int64(2), runs, "expected 2 stalled runs aborted") - assert.Equal(t, int64(1), migrations, "expected 1 stalled migration reset") - - // Verify the migration was reset to pending. - op, err = s.GetMaintenanceOperation(ctx, "secret-hub-id-migration") - require.NoError(t, err) - assert.Equal(t, store.MaintenanceStatusPending, op.Status) - assert.Nil(t, op.StartedAt) - - // Verify running runs were marked as failed. - allRuns, err := s.ListMaintenanceRuns(ctx, "pull-images", 10) - require.NoError(t, err) - for _, r := range allRuns { - if r.ID == completedRun.ID { - assert.Equal(t, store.MaintenanceStatusCompleted, r.Status, "completed run should be unchanged") - } else { - assert.Equal(t, store.MaintenanceStatusFailed, r.Status, "running run should be failed") - assert.NotNil(t, r.CompletedAt, "aborted run should have completedAt") - } - } - - // Running it again should be a no-op. - runs, migrations, err = s.AbortRunningMaintenanceOps(ctx) - require.NoError(t, err) - assert.Equal(t, int64(0), runs) - assert.Equal(t, int64(0), migrations) -} diff --git a/pkg/store/sqlite/messages.go b/pkg/store/sqlite/messages.go deleted file mode 100644 index 18dcf34ba..000000000 --- a/pkg/store/sqlite/messages.go +++ /dev/null @@ -1,226 +0,0 @@ -// Copyright 2026 Google LLC -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -// Package sqlite provides a SQLite implementation of the Store interface. -package sqlite - -import ( - "context" - "database/sql" - "errors" - "fmt" - "strings" - "time" - - "github.com/GoogleCloudPlatform/scion/pkg/store" -) - -// ============================================================================ -// Message Operations -// ============================================================================ - -// CreateMessage persists a new message. -func (s *SQLiteStore) CreateMessage(ctx context.Context, msg *store.Message) error { - if msg.ID == "" || msg.ProjectID == "" || msg.Msg == "" { - return store.ErrInvalidInput - } - if msg.CreatedAt.IsZero() { - msg.CreatedAt = time.Now() - } - - _, err := s.db.ExecContext(ctx, ` - INSERT INTO messages ( - id, project_id, sender, sender_id, recipient, recipient_id, - msg, type, urgent, broadcasted, read, agent_id, group_id, created_at - ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) - `, - msg.ID, msg.ProjectID, msg.Sender, msg.SenderID, msg.Recipient, msg.RecipientID, - msg.Msg, msg.Type, - boolToInt(msg.Urgent), boolToInt(msg.Broadcasted), boolToInt(msg.Read), - msg.AgentID, msg.GroupID, msg.CreatedAt, - ) - if err != nil { - if strings.Contains(err.Error(), "UNIQUE constraint failed") { - return store.ErrAlreadyExists - } - return err - } - return nil -} - -// GetMessage returns a single message by ID. -func (s *SQLiteStore) GetMessage(ctx context.Context, id string) (*store.Message, error) { - row := s.db.QueryRowContext(ctx, ` - SELECT id, project_id, sender, sender_id, recipient, recipient_id, - msg, type, urgent, broadcasted, read, agent_id, group_id, created_at - FROM messages - WHERE id = ? - `, id) - - var msg store.Message - var urgent, broadcasted, read int - if err := row.Scan( - &msg.ID, &msg.ProjectID, &msg.Sender, &msg.SenderID, &msg.Recipient, &msg.RecipientID, - &msg.Msg, &msg.Type, &urgent, &broadcasted, &read, - &msg.AgentID, &msg.GroupID, &msg.CreatedAt, - ); err != nil { - if errors.Is(err, sql.ErrNoRows) { - return nil, store.ErrNotFound - } - return nil, err - } - msg.Urgent = urgent != 0 - msg.Broadcasted = broadcasted != 0 - msg.Read = read != 0 - return &msg, nil -} - -// ListMessages returns messages matching the given filter, ordered by created_at DESC. -func (s *SQLiteStore) ListMessages(ctx context.Context, filter store.MessageFilter, opts store.ListOptions) (*store.ListResult[store.Message], error) { - var conditions []string - var args []interface{} - - if filter.ProjectID != "" { - conditions = append(conditions, "project_id = ?") - args = append(args, filter.ProjectID) - } - if filter.AgentID != "" { - conditions = append(conditions, "agent_id = ?") - args = append(args, filter.AgentID) - } - if filter.RecipientID != "" { - conditions = append(conditions, "recipient_id = ?") - args = append(args, filter.RecipientID) - } - if filter.SenderID != "" { - conditions = append(conditions, "sender_id = ?") - args = append(args, filter.SenderID) - } - if filter.ParticipantID != "" { - conditions = append(conditions, "(recipient_id = ? OR sender_id = ?)") - args = append(args, filter.ParticipantID, filter.ParticipantID) - } - if filter.OnlyUnread { - conditions = append(conditions, "read = 0") - } - if filter.Type != "" { - conditions = append(conditions, "type = ?") - args = append(args, filter.Type) - } - - whereClause := "" - if len(conditions) > 0 { - whereClause = "WHERE " + strings.Join(conditions, " AND ") - } - - var totalCount int - countQuery := fmt.Sprintf("SELECT COUNT(*) FROM messages %s", whereClause) - if err := s.db.QueryRowContext(ctx, countQuery, args...).Scan(&totalCount); err != nil { - return nil, err - } - - limit := opts.Limit - if limit <= 0 { - limit = 50 - } - if limit > 200 { - limit = 200 - } - - query := fmt.Sprintf(` - SELECT id, project_id, sender, sender_id, recipient, recipient_id, - msg, type, urgent, broadcasted, read, agent_id, group_id, created_at - FROM messages %s ORDER BY created_at DESC LIMIT ? - `, whereClause) - args = append(args, limit+1) - - rows, err := s.db.QueryContext(ctx, query, args...) - if err != nil { - return nil, err - } - defer rows.Close() - - var msgs []store.Message - for rows.Next() { - var msg store.Message - var urgent, broadcasted, read int - if err := rows.Scan( - &msg.ID, &msg.ProjectID, &msg.Sender, &msg.SenderID, &msg.Recipient, &msg.RecipientID, - &msg.Msg, &msg.Type, &urgent, &broadcasted, &read, - &msg.AgentID, &msg.GroupID, &msg.CreatedAt, - ); err != nil { - return nil, err - } - msg.Urgent = urgent != 0 - msg.Broadcasted = broadcasted != 0 - msg.Read = read != 0 - msgs = append(msgs, msg) - } - if err := rows.Err(); err != nil { - return nil, err - } - - result := &store.ListResult[store.Message]{ - Items: msgs, - TotalCount: totalCount, - } - if len(msgs) > limit { - result.Items = msgs[:limit] - result.NextCursor = msgs[limit-1].ID - } - return result, nil -} - -// MarkMessageRead marks a message as read. -func (s *SQLiteStore) MarkMessageRead(ctx context.Context, id string) error { - result, err := s.db.ExecContext(ctx, ` - UPDATE messages SET read = 1 WHERE id = ? - `, id) - if err != nil { - return err - } - n, err := result.RowsAffected() - if err != nil { - return err - } - if n == 0 { - return store.ErrNotFound - } - return nil -} - -// MarkAllMessagesRead marks all messages for a recipient as read. -func (s *SQLiteStore) MarkAllMessagesRead(ctx context.Context, recipientID string) error { - _, err := s.db.ExecContext(ctx, ` - UPDATE messages SET read = 1 WHERE recipient_id = ? - `, recipientID) - return err -} - -// PurgeOldMessages removes read messages older than readCutoff and unread messages -// older than unreadCutoff. Returns the number of messages removed. -func (s *SQLiteStore) PurgeOldMessages(ctx context.Context, readCutoff time.Time, unreadCutoff time.Time) (int, error) { - result, err := s.db.ExecContext(ctx, ` - DELETE FROM messages - WHERE (read = 1 AND created_at < ?) OR (read = 0 AND created_at < ?) - `, readCutoff, unreadCutoff) - if err != nil { - return 0, err - } - n, err := result.RowsAffected() - if err != nil { - return 0, err - } - return int(n), nil -} diff --git a/pkg/store/sqlite/messages_test.go b/pkg/store/sqlite/messages_test.go deleted file mode 100644 index 57dbd73f1..000000000 --- a/pkg/store/sqlite/messages_test.go +++ /dev/null @@ -1,251 +0,0 @@ -// Copyright 2026 Google LLC -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -//go:build !no_sqlite - -package sqlite - -import ( - "context" - "testing" - "time" - - "github.com/GoogleCloudPlatform/scion/pkg/api" - "github.com/GoogleCloudPlatform/scion/pkg/store" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -func newTestMessage(projectID, agentID string) *store.Message { - return &store.Message{ - ID: api.NewUUID(), - ProjectID: projectID, - Sender: "user:alice", - SenderID: "user-uuid-alice", - Recipient: "agent:coder", - RecipientID: agentID, - Msg: "Please fix the auth module.", - Type: "instruction", - AgentID: agentID, - CreatedAt: time.Now().UTC().Truncate(time.Second), - } -} - -func TestMessageCRUD(t *testing.T) { - s := setupTestStore(t) - ctx := context.Background() - - projectID, agentID := createTestProjectAndAgent(t, s) - msg := newTestMessage(projectID, agentID) - - // Create - require.NoError(t, s.CreateMessage(ctx, msg)) - - // Get - got, err := s.GetMessage(ctx, msg.ID) - require.NoError(t, err) - assert.Equal(t, msg.ID, got.ID) - assert.Equal(t, msg.ProjectID, got.ProjectID) - assert.Equal(t, msg.Sender, got.Sender) - assert.Equal(t, msg.Recipient, got.Recipient) - assert.Equal(t, msg.Msg, got.Msg) - assert.Equal(t, msg.Type, got.Type) - assert.Equal(t, msg.AgentID, got.AgentID) - assert.False(t, got.Read) - - // Duplicate create returns ErrAlreadyExists - err = s.CreateMessage(ctx, msg) - assert.ErrorIs(t, err, store.ErrAlreadyExists) - - // Not found - _, err = s.GetMessage(ctx, "nonexistent") - assert.ErrorIs(t, err, store.ErrNotFound) -} - -func TestMessageMarkRead(t *testing.T) { - s := setupTestStore(t) - ctx := context.Background() - - projectID, agentID := createTestProjectAndAgent(t, s) - msg := newTestMessage(projectID, agentID) - require.NoError(t, s.CreateMessage(ctx, msg)) - - // Mark single message as read - require.NoError(t, s.MarkMessageRead(ctx, msg.ID)) - got, err := s.GetMessage(ctx, msg.ID) - require.NoError(t, err) - assert.True(t, got.Read) - - // Mark not-found returns ErrNotFound - assert.ErrorIs(t, s.MarkMessageRead(ctx, "nonexistent"), store.ErrNotFound) -} - -func TestMessageMarkAllRead(t *testing.T) { - s := setupTestStore(t) - ctx := context.Background() - - projectID, agentID := createTestProjectAndAgent(t, s) - - // Create two messages for the same recipient - recipientID := agentID - msg1 := newTestMessage(projectID, agentID) - msg1.RecipientID = recipientID - msg2 := newTestMessage(projectID, agentID) - msg2.ID = api.NewUUID() - msg2.RecipientID = recipientID - require.NoError(t, s.CreateMessage(ctx, msg1)) - require.NoError(t, s.CreateMessage(ctx, msg2)) - - require.NoError(t, s.MarkAllMessagesRead(ctx, recipientID)) - - got1, err := s.GetMessage(ctx, msg1.ID) - require.NoError(t, err) - assert.True(t, got1.Read) - - got2, err := s.GetMessage(ctx, msg2.ID) - require.NoError(t, err) - assert.True(t, got2.Read) -} - -func TestListMessages(t *testing.T) { - s := setupTestStore(t) - ctx := context.Background() - - projectID, agentID := createTestProjectAndAgent(t, s) - - // Create unread message - unread := newTestMessage(projectID, agentID) - require.NoError(t, s.CreateMessage(ctx, unread)) - - // Create read message - read := newTestMessage(projectID, agentID) - read.ID = api.NewUUID() - require.NoError(t, s.CreateMessage(ctx, read)) - require.NoError(t, s.MarkMessageRead(ctx, read.ID)) - - // List all - result, err := s.ListMessages(ctx, store.MessageFilter{ProjectID: projectID}, store.ListOptions{}) - require.NoError(t, err) - assert.Equal(t, 2, result.TotalCount) - assert.Len(t, result.Items, 2) - - // List unread only - result, err = s.ListMessages(ctx, store.MessageFilter{ProjectID: projectID, OnlyUnread: true}, store.ListOptions{}) - require.NoError(t, err) - assert.Equal(t, 1, result.TotalCount) - assert.Len(t, result.Items, 1) - assert.Equal(t, unread.ID, result.Items[0].ID) - - // Filter by agent - result, err = s.ListMessages(ctx, store.MessageFilter{AgentID: agentID}, store.ListOptions{}) - require.NoError(t, err) - assert.Equal(t, 2, result.TotalCount) - - // Filter by type - result, err = s.ListMessages(ctx, store.MessageFilter{Type: "instruction"}, store.ListOptions{}) - require.NoError(t, err) - assert.Equal(t, 2, result.TotalCount) - - result, err = s.ListMessages(ctx, store.MessageFilter{Type: "input-needed"}, store.ListOptions{}) - require.NoError(t, err) - assert.Equal(t, 0, result.TotalCount) -} - -func TestListMessages_ParticipantID(t *testing.T) { - s := setupTestStore(t) - ctx := context.Background() - - projectID, agentID := createTestProjectAndAgent(t, s) - userID := "user-uuid-alice" - - // Inbound: user → agent. Sender=user, recipient=agent. - inbound := newTestMessage(projectID, agentID) - inbound.SenderID = userID - inbound.RecipientID = agentID - require.NoError(t, s.CreateMessage(ctx, inbound)) - - // Outbound: agent → user. Sender=agent, recipient=user. - outbound := &store.Message{ - ID: api.NewUUID(), - ProjectID: projectID, - Sender: "agent:coder", - SenderID: agentID, - Recipient: "user:alice", - RecipientID: userID, - Msg: "Done — here's the patch.", - Type: "assistant-reply", - AgentID: agentID, - CreatedAt: time.Now().UTC().Truncate(time.Second), - } - require.NoError(t, s.CreateMessage(ctx, outbound)) - - // Unrelated message in the same project/agent with a different user. - other := &store.Message{ - ID: api.NewUUID(), - ProjectID: projectID, - Sender: "user:bob", - SenderID: "user-uuid-bob", - Recipient: "agent:coder", - RecipientID: agentID, - Msg: "Bob's message", - Type: "instruction", - AgentID: agentID, - CreatedAt: time.Now().UTC().Truncate(time.Second), - } - require.NoError(t, s.CreateMessage(ctx, other)) - - // ParticipantID + AgentID returns both sides of the alice↔agent chat - // but not bob's message. - result, err := s.ListMessages(ctx, store.MessageFilter{ - AgentID: agentID, - ParticipantID: userID, - }, store.ListOptions{}) - require.NoError(t, err) - assert.Equal(t, 2, result.TotalCount) - gotIDs := map[string]bool{} - for _, m := range result.Items { - gotIDs[m.ID] = true - } - assert.True(t, gotIDs[inbound.ID], "inbound (user→agent) should match") - assert.True(t, gotIDs[outbound.ID], "outbound (agent→user) should match") - assert.False(t, gotIDs[other.ID], "bob's message should not match alice's participant filter") -} - -func TestPurgeOldMessages(t *testing.T) { - s := setupTestStore(t) - ctx := context.Background() - - projectID, agentID := createTestProjectAndAgent(t, s) - - old := newTestMessage(projectID, agentID) - old.CreatedAt = time.Now().Add(-40 * 24 * time.Hour) - require.NoError(t, s.CreateMessage(ctx, old)) - require.NoError(t, s.MarkMessageRead(ctx, old.ID)) - - recent := newTestMessage(projectID, agentID) - recent.ID = api.NewUUID() - require.NoError(t, s.CreateMessage(ctx, recent)) - - readCutoff := time.Now().Add(-30 * 24 * time.Hour) - unreadCutoff := time.Now().Add(-90 * 24 * time.Hour) - n, err := s.PurgeOldMessages(ctx, readCutoff, unreadCutoff) - require.NoError(t, err) - assert.Equal(t, 1, n) - - _, err = s.GetMessage(ctx, old.ID) - assert.ErrorIs(t, err, store.ErrNotFound) - - _, err = s.GetMessage(ctx, recent.ID) - assert.NoError(t, err) -} diff --git a/pkg/store/sqlite/notification.go b/pkg/store/sqlite/notification.go deleted file mode 100644 index 995b1cef6..000000000 --- a/pkg/store/sqlite/notification.go +++ /dev/null @@ -1,553 +0,0 @@ -// Copyright 2026 Google LLC -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -// Package sqlite provides a SQLite implementation of the Store interface. -package sqlite - -import ( - "context" - "database/sql" - "errors" - "fmt" - "strings" - "time" - - "github.com/GoogleCloudPlatform/scion/pkg/store" -) - -// ============================================================================ -// Notification Subscription Operations -// ============================================================================ - -// CreateNotificationSubscription creates a new notification subscription. -func (s *SQLiteStore) CreateNotificationSubscription(ctx context.Context, sub *store.NotificationSubscription) error { - if sub.ID == "" || sub.SubscriberID == "" || sub.ProjectID == "" { - return store.ErrInvalidInput - } - - // Default scope to agent for backward compatibility - if sub.Scope == "" { - sub.Scope = store.SubscriptionScopeAgent - } - - // Validate scope-specific constraints - switch sub.Scope { - case store.SubscriptionScopeAgent: - if sub.AgentID == "" { - return store.ErrInvalidInput - } - case store.SubscriptionScopeProject: - sub.AgentID = "" // Ensure no agent_id for project-scoped - default: - return fmt.Errorf("invalid scope %q: %w", sub.Scope, store.ErrInvalidInput) - } - - now := time.Now() - if sub.CreatedAt.IsZero() { - sub.CreatedAt = now - } - - _, err := s.db.ExecContext(ctx, ` - INSERT INTO notification_subscriptions ( - id, scope, agent_id, subscriber_type, subscriber_id, project_id, - trigger_activities, created_at, created_by - ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?) - `, - sub.ID, sub.Scope, nullableString(sub.AgentID), sub.SubscriberType, sub.SubscriberID, sub.ProjectID, - marshalJSON(sub.TriggerActivities), sub.CreatedAt, sub.CreatedBy, - ) - if err != nil { - if strings.Contains(err.Error(), "UNIQUE constraint failed") { - return store.ErrAlreadyExists - } - if strings.Contains(err.Error(), "FOREIGN KEY constraint failed") { - return fmt.Errorf("agent %s does not exist: %w", sub.AgentID, store.ErrInvalidInput) - } - return err - } - return nil -} - -// GetNotificationSubscription returns a single subscription by ID. -func (s *SQLiteStore) GetNotificationSubscription(ctx context.Context, id string) (*store.NotificationSubscription, error) { - row := s.db.QueryRowContext(ctx, ` - SELECT id, scope, agent_id, subscriber_type, subscriber_id, project_id, - trigger_activities, created_at, created_by - FROM notification_subscriptions - WHERE id = ? - `, id) - - var sub store.NotificationSubscription - var agentID sql.NullString - var triggerActivitiesJSON string - - if err := row.Scan( - &sub.ID, &sub.Scope, &agentID, &sub.SubscriberType, &sub.SubscriberID, &sub.ProjectID, - &triggerActivitiesJSON, &sub.CreatedAt, &sub.CreatedBy, - ); err != nil { - if errors.Is(err, sql.ErrNoRows) { - return nil, store.ErrNotFound - } - return nil, err - } - - if agentID.Valid { - sub.AgentID = agentID.String - } - unmarshalJSON(triggerActivitiesJSON, &sub.TriggerActivities) - return &sub, nil -} - -// GetNotificationSubscriptions returns all agent-scoped subscriptions for a watched agent. -func (s *SQLiteStore) GetNotificationSubscriptions(ctx context.Context, agentID string) ([]store.NotificationSubscription, error) { - rows, err := s.db.QueryContext(ctx, ` - SELECT id, scope, agent_id, subscriber_type, subscriber_id, project_id, - trigger_activities, created_at, created_by - FROM notification_subscriptions - WHERE agent_id = ? - ORDER BY created_at ASC - `, agentID) - if err != nil { - return nil, err - } - defer rows.Close() - - return scanSubscriptions(rows) -} - -// GetNotificationSubscriptionsByProject returns all subscriptions within a project (any scope). -func (s *SQLiteStore) GetNotificationSubscriptionsByProject(ctx context.Context, projectID string) ([]store.NotificationSubscription, error) { - rows, err := s.db.QueryContext(ctx, ` - SELECT id, scope, agent_id, subscriber_type, subscriber_id, project_id, - trigger_activities, created_at, created_by - FROM notification_subscriptions - WHERE project_id = ? - ORDER BY created_at ASC - `, projectID) - if err != nil { - return nil, err - } - defer rows.Close() - - return scanSubscriptions(rows) -} - -// GetNotificationSubscriptionsByProjectScope returns project-scoped subscriptions -// (scope='project') for a given project. -func (s *SQLiteStore) GetNotificationSubscriptionsByProjectScope(ctx context.Context, projectID string) ([]store.NotificationSubscription, error) { - rows, err := s.db.QueryContext(ctx, ` - SELECT id, scope, agent_id, subscriber_type, subscriber_id, project_id, - trigger_activities, created_at, created_by - FROM notification_subscriptions - WHERE project_id = ? AND scope = 'project' - ORDER BY created_at ASC - `, projectID) - if err != nil { - return nil, err - } - defer rows.Close() - - return scanSubscriptions(rows) -} - -// GetSubscriptionsForSubscriber returns all subscriptions owned by a subscriber. -func (s *SQLiteStore) GetSubscriptionsForSubscriber(ctx context.Context, subscriberType, subscriberID string) ([]store.NotificationSubscription, error) { - rows, err := s.db.QueryContext(ctx, ` - SELECT id, scope, agent_id, subscriber_type, subscriber_id, project_id, - trigger_activities, created_at, created_by - FROM notification_subscriptions - WHERE subscriber_type = ? AND subscriber_id = ? - ORDER BY created_at ASC - `, subscriberType, subscriberID) - if err != nil { - return nil, err - } - defer rows.Close() - - return scanSubscriptions(rows) -} - -// UpdateNotificationSubscriptionTriggers updates the trigger activities of a subscription. -func (s *SQLiteStore) UpdateNotificationSubscriptionTriggers(ctx context.Context, id string, triggerActivities []string) error { - if id == "" || len(triggerActivities) == 0 { - return store.ErrInvalidInput - } - - result, err := s.db.ExecContext(ctx, ` - UPDATE notification_subscriptions SET trigger_activities = ? WHERE id = ? - `, marshalJSON(triggerActivities), id) - if err != nil { - return err - } - - rows, err := result.RowsAffected() - if err != nil { - return err - } - if rows == 0 { - return store.ErrNotFound - } - return nil -} - -// DeleteNotificationSubscription deletes a subscription by ID. -func (s *SQLiteStore) DeleteNotificationSubscription(ctx context.Context, id string) error { - result, err := s.db.ExecContext(ctx, ` - DELETE FROM notification_subscriptions WHERE id = ? - `, id) - if err != nil { - return err - } - - rows, err := result.RowsAffected() - if err != nil { - return err - } - if rows == 0 { - return store.ErrNotFound - } - return nil -} - -// DeleteNotificationSubscriptionsForAgent deletes all subscriptions for a watched agent. -// No error on zero rows affected. -func (s *SQLiteStore) DeleteNotificationSubscriptionsForAgent(ctx context.Context, agentID string) error { - _, err := s.db.ExecContext(ctx, ` - DELETE FROM notification_subscriptions WHERE agent_id = ? - `, agentID) - return err -} - -// ============================================================================ -// Notification Operations -// ============================================================================ - -// CreateNotification creates a new notification record. -func (s *SQLiteStore) CreateNotification(ctx context.Context, notif *store.Notification) error { - if notif.ID == "" || notif.SubscriptionID == "" || notif.AgentID == "" { - return store.ErrInvalidInput - } - - now := time.Now() - if notif.CreatedAt.IsZero() { - notif.CreatedAt = now - } - - _, err := s.db.ExecContext(ctx, ` - INSERT INTO notifications ( - id, subscription_id, agent_id, project_id, - subscriber_type, subscriber_id, - status, message, dispatched, acknowledged, created_at - ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) - `, - notif.ID, notif.SubscriptionID, notif.AgentID, notif.ProjectID, - notif.SubscriberType, notif.SubscriberID, - notif.Status, notif.Message, - boolToInt(notif.Dispatched), boolToInt(notif.Acknowledged), - notif.CreatedAt, - ) - if err != nil { - if strings.Contains(err.Error(), "FOREIGN KEY constraint failed") { - return fmt.Errorf("subscription %s does not exist: %w", notif.SubscriptionID, store.ErrInvalidInput) - } - return err - } - return nil -} - -// GetNotifications returns notifications for a subscriber. -// If onlyUnacknowledged is true, only unacknowledged notifications are returned. -// Results are ordered by created_at DESC. -func (s *SQLiteStore) GetNotifications(ctx context.Context, subscriberType, subscriberID string, onlyUnacknowledged bool) ([]store.Notification, error) { - query := ` - SELECT id, subscription_id, agent_id, project_id, - subscriber_type, subscriber_id, - status, message, dispatched, acknowledged, created_at - FROM notifications - WHERE subscriber_type = ? AND subscriber_id = ? - ` - args := []interface{}{subscriberType, subscriberID} - - if onlyUnacknowledged { - query += ` AND acknowledged = 0` - } - - query += ` ORDER BY created_at DESC` - - rows, err := s.db.QueryContext(ctx, query, args...) - if err != nil { - return nil, err - } - defer rows.Close() - - return scanNotifications(rows) -} - -// GetNotificationsByAgent returns notifications for a subscriber filtered by agent ID. -// If onlyUnacknowledged is true, only unacknowledged notifications are returned. -// Results are ordered by created_at DESC. -func (s *SQLiteStore) GetNotificationsByAgent(ctx context.Context, agentID, subscriberType, subscriberID string, onlyUnacknowledged bool) ([]store.Notification, error) { - query := ` - SELECT id, subscription_id, agent_id, project_id, - subscriber_type, subscriber_id, - status, message, dispatched, acknowledged, created_at - FROM notifications - WHERE agent_id = ? AND subscriber_type = ? AND subscriber_id = ? - ` - args := []interface{}{agentID, subscriberType, subscriberID} - - if onlyUnacknowledged { - query += ` AND acknowledged = 0` - } - - query += ` ORDER BY created_at DESC` - - rows, err := s.db.QueryContext(ctx, query, args...) - if err != nil { - return nil, err - } - defer rows.Close() - - return scanNotifications(rows) -} - -// AcknowledgeNotification marks a notification as acknowledged. -func (s *SQLiteStore) AcknowledgeNotification(ctx context.Context, id string) error { - result, err := s.db.ExecContext(ctx, ` - UPDATE notifications SET acknowledged = 1 WHERE id = ? - `, id) - if err != nil { - return err - } - - rows, err := result.RowsAffected() - if err != nil { - return err - } - if rows == 0 { - return store.ErrNotFound - } - return nil -} - -// AcknowledgeAllNotifications marks all notifications for a subscriber as acknowledged. -// No error on zero rows affected. -func (s *SQLiteStore) AcknowledgeAllNotifications(ctx context.Context, subscriberType, subscriberID string) error { - _, err := s.db.ExecContext(ctx, ` - UPDATE notifications SET acknowledged = 1 - WHERE subscriber_type = ? AND subscriber_id = ? - `, subscriberType, subscriberID) - return err -} - -// MarkNotificationDispatched marks a notification as dispatched. -func (s *SQLiteStore) MarkNotificationDispatched(ctx context.Context, id string) error { - result, err := s.db.ExecContext(ctx, ` - UPDATE notifications SET dispatched = 1 WHERE id = ? - `, id) - if err != nil { - return err - } - - rows, err := result.RowsAffected() - if err != nil { - return err - } - if rows == 0 { - return store.ErrNotFound - } - return nil -} - -// GetLastNotificationStatus returns the status of the most recent notification -// for a given subscription. Returns ("", nil) if no notifications exist. -func (s *SQLiteStore) GetLastNotificationStatus(ctx context.Context, subscriptionID string) (string, error) { - var status string - err := s.db.QueryRowContext(ctx, ` - SELECT status FROM notifications - WHERE subscription_id = ? - ORDER BY created_at DESC - LIMIT 1 - `, subscriptionID).Scan(&status) - if err != nil { - if errors.Is(err, sql.ErrNoRows) { - return "", nil - } - return "", err - } - return status, nil -} - -// ============================================================================ -// Subscription Template Operations -// ============================================================================ - -// CreateSubscriptionTemplate creates a new subscription template. -func (s *SQLiteStore) CreateSubscriptionTemplate(ctx context.Context, tmpl *store.SubscriptionTemplate) error { - if tmpl.ID == "" || tmpl.Name == "" || len(tmpl.TriggerActivities) == 0 { - return store.ErrInvalidInput - } - - _, err := s.db.ExecContext(ctx, ` - INSERT INTO subscription_templates (id, name, scope, trigger_activities, project_id, created_by) - VALUES (?, ?, ?, ?, ?, ?) - `, tmpl.ID, tmpl.Name, tmpl.Scope, marshalJSON(tmpl.TriggerActivities), tmpl.ProjectID, tmpl.CreatedBy) - if err != nil { - if strings.Contains(err.Error(), "UNIQUE constraint failed") { - return store.ErrAlreadyExists - } - return err - } - return nil -} - -// GetSubscriptionTemplate returns a template by ID. -func (s *SQLiteStore) GetSubscriptionTemplate(ctx context.Context, id string) (*store.SubscriptionTemplate, error) { - row := s.db.QueryRowContext(ctx, ` - SELECT id, name, scope, trigger_activities, project_id, created_by - FROM subscription_templates WHERE id = ? - `, id) - - var tmpl store.SubscriptionTemplate - var triggersJSON string - if err := row.Scan(&tmpl.ID, &tmpl.Name, &tmpl.Scope, &triggersJSON, &tmpl.ProjectID, &tmpl.CreatedBy); err != nil { - if errors.Is(err, sql.ErrNoRows) { - return nil, store.ErrNotFound - } - return nil, err - } - unmarshalJSON(triggersJSON, &tmpl.TriggerActivities) - return &tmpl, nil -} - -// ListSubscriptionTemplates returns all templates. If projectID is non-empty, -// returns both global templates and project-specific templates. -func (s *SQLiteStore) ListSubscriptionTemplates(ctx context.Context, projectID string) ([]store.SubscriptionTemplate, error) { - var rows *sql.Rows - var err error - - if projectID != "" { - rows, err = s.db.QueryContext(ctx, ` - SELECT id, name, scope, trigger_activities, project_id, created_by - FROM subscription_templates - WHERE project_id = '' OR project_id = ? - ORDER BY project_id ASC, name ASC - `, projectID) - } else { - rows, err = s.db.QueryContext(ctx, ` - SELECT id, name, scope, trigger_activities, project_id, created_by - FROM subscription_templates - WHERE project_id = '' - ORDER BY name ASC - `) - } - if err != nil { - return nil, err - } - defer rows.Close() - - var templates []store.SubscriptionTemplate - for rows.Next() { - var tmpl store.SubscriptionTemplate - var triggersJSON string - if err := rows.Scan(&tmpl.ID, &tmpl.Name, &tmpl.Scope, &triggersJSON, &tmpl.ProjectID, &tmpl.CreatedBy); err != nil { - return nil, err - } - unmarshalJSON(triggersJSON, &tmpl.TriggerActivities) - templates = append(templates, tmpl) - } - return templates, rows.Err() -} - -// DeleteSubscriptionTemplate deletes a template by ID. -func (s *SQLiteStore) DeleteSubscriptionTemplate(ctx context.Context, id string) error { - result, err := s.db.ExecContext(ctx, ` - DELETE FROM subscription_templates WHERE id = ? - `, id) - if err != nil { - return err - } - rows, err := result.RowsAffected() - if err != nil { - return err - } - if rows == 0 { - return store.ErrNotFound - } - return nil -} - -// ============================================================================ -// Helpers -// ============================================================================ - -// boolToInt converts a bool to an int for SQLite storage. -func boolToInt(b bool) int { - if b { - return 1 - } - return 0 -} - -// scanSubscriptions scans rows into NotificationSubscription slices. -func scanSubscriptions(rows *sql.Rows) ([]store.NotificationSubscription, error) { - var subs []store.NotificationSubscription - for rows.Next() { - var sub store.NotificationSubscription - var agentID sql.NullString - var triggerActivitiesJSON string - - if err := rows.Scan( - &sub.ID, &sub.Scope, &agentID, &sub.SubscriberType, &sub.SubscriberID, &sub.ProjectID, - &triggerActivitiesJSON, &sub.CreatedAt, &sub.CreatedBy, - ); err != nil { - return nil, err - } - - if agentID.Valid { - sub.AgentID = agentID.String - } - unmarshalJSON(triggerActivitiesJSON, &sub.TriggerActivities) - subs = append(subs, sub) - } - if err := rows.Err(); err != nil { - return nil, err - } - return subs, nil -} - -// scanNotifications scans rows into Notification slices. -func scanNotifications(rows *sql.Rows) ([]store.Notification, error) { - var notifs []store.Notification - for rows.Next() { - var notif store.Notification - var dispatched, acknowledged int - - if err := rows.Scan( - ¬if.ID, ¬if.SubscriptionID, ¬if.AgentID, ¬if.ProjectID, - ¬if.SubscriberType, ¬if.SubscriberID, - ¬if.Status, ¬if.Message, &dispatched, &acknowledged, ¬if.CreatedAt, - ); err != nil { - return nil, err - } - - notif.Dispatched = dispatched != 0 - notif.Acknowledged = acknowledged != 0 - notifs = append(notifs, notif) - } - if err := rows.Err(); err != nil { - return nil, err - } - return notifs, nil -} diff --git a/pkg/store/sqlite/notification_test.go b/pkg/store/sqlite/notification_test.go deleted file mode 100644 index f5a1cdfa4..000000000 --- a/pkg/store/sqlite/notification_test.go +++ /dev/null @@ -1,878 +0,0 @@ -// Copyright 2026 Google LLC -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -//go:build !no_sqlite - -package sqlite - -import ( - "context" - "testing" - "time" - - "github.com/GoogleCloudPlatform/scion/pkg/agent/state" - "github.com/GoogleCloudPlatform/scion/pkg/api" - "github.com/GoogleCloudPlatform/scion/pkg/store" - "github.com/google/uuid" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -// createTestProjectAndAgent is a helper that creates a project and agent for notification tests. -func createTestProjectAndAgent(t *testing.T, s *SQLiteStore) (projectID, agentID string) { - t.Helper() - ctx := context.Background() - - projectID = api.NewUUID() - project := &store.Project{ - ID: projectID, - Name: "Notification Test Project", - Slug: "notif-project-" + projectID[:8], - Visibility: store.VisibilityPrivate, - } - require.NoError(t, s.CreateProject(ctx, project)) - - agentID = api.NewUUID() - agent := &store.Agent{ - ID: agentID, - Slug: "notif-agent-" + agentID[:8], - Name: "Notification Test Agent", - ProjectID: projectID, - Phase: string(state.PhaseRunning), - Visibility: store.VisibilityPrivate, - } - require.NoError(t, s.CreateAgent(ctx, agent)) - - return projectID, agentID -} - -func TestNotificationSubscriptionCRUD(t *testing.T) { - s := setupTestStore(t) - ctx := context.Background() - projectID, agentID := createTestProjectAndAgent(t, s) - - subID := uuid.New().String() - sub := &store.NotificationSubscription{ - ID: subID, - Scope: store.SubscriptionScopeAgent, - AgentID: agentID, - SubscriberType: store.SubscriberTypeAgent, - SubscriberID: "lead-agent", - ProjectID: projectID, - TriggerActivities: []string{"COMPLETED", "WAITING_FOR_INPUT", "LIMITS_EXCEEDED"}, - CreatedBy: "lead-agent", - } - - // Create - err := s.CreateNotificationSubscription(ctx, sub) - require.NoError(t, err) - assert.False(t, sub.CreatedAt.IsZero(), "CreatedAt should be set automatically") - - // Get by ID - got, err := s.GetNotificationSubscription(ctx, subID) - require.NoError(t, err) - assert.Equal(t, subID, got.ID) - assert.Equal(t, store.SubscriptionScopeAgent, got.Scope) - assert.Equal(t, agentID, got.AgentID) - assert.Equal(t, store.SubscriberTypeAgent, got.SubscriberType) - assert.Equal(t, "lead-agent", got.SubscriberID) - - // Get by ID not found - _, err = s.GetNotificationSubscription(ctx, "non-existent") - assert.ErrorIs(t, err, store.ErrNotFound) - - // Get by agent - subs, err := s.GetNotificationSubscriptions(ctx, agentID) - require.NoError(t, err) - require.Len(t, subs, 1) - assert.Equal(t, subID, subs[0].ID) - assert.Equal(t, store.SubscriptionScopeAgent, subs[0].Scope) - assert.Equal(t, agentID, subs[0].AgentID) - assert.Equal(t, store.SubscriberTypeAgent, subs[0].SubscriberType) - assert.Equal(t, "lead-agent", subs[0].SubscriberID) - assert.Equal(t, projectID, subs[0].ProjectID) - assert.Equal(t, []string{"COMPLETED", "WAITING_FOR_INPUT", "LIMITS_EXCEEDED"}, subs[0].TriggerActivities) - - // Get by project - subs, err = s.GetNotificationSubscriptionsByProject(ctx, projectID) - require.NoError(t, err) - require.Len(t, subs, 1) - assert.Equal(t, subID, subs[0].ID) - - // Delete - err = s.DeleteNotificationSubscription(ctx, subID) - require.NoError(t, err) - - // Verify deleted - subs, err = s.GetNotificationSubscriptions(ctx, agentID) - require.NoError(t, err) - assert.Empty(t, subs) - - // Delete not found - err = s.DeleteNotificationSubscription(ctx, "non-existent") - assert.ErrorIs(t, err, store.ErrNotFound) -} - -func TestNotificationSubscriptionScopeDefault(t *testing.T) { - s := setupTestStore(t) - ctx := context.Background() - projectID, agentID := createTestProjectAndAgent(t, s) - - // Create subscription without explicit scope — should default to "agent" - sub := &store.NotificationSubscription{ - ID: uuid.New().String(), - AgentID: agentID, - SubscriberType: store.SubscriberTypeAgent, - SubscriberID: "default-scope-agent", - ProjectID: projectID, - TriggerActivities: []string{"COMPLETED"}, - CreatedBy: "test", - } - require.NoError(t, s.CreateNotificationSubscription(ctx, sub)) - assert.Equal(t, store.SubscriptionScopeAgent, sub.Scope) - - got, err := s.GetNotificationSubscription(ctx, sub.ID) - require.NoError(t, err) - assert.Equal(t, store.SubscriptionScopeAgent, got.Scope) -} - -func TestProjectScopedSubscription(t *testing.T) { - s := setupTestStore(t) - ctx := context.Background() - projectID, agentID := createTestProjectAndAgent(t, s) - - // Create a project-scoped subscription - projectSub := &store.NotificationSubscription{ - ID: uuid.New().String(), - Scope: store.SubscriptionScopeProject, - SubscriberType: store.SubscriberTypeUser, - SubscriberID: "user-project-watcher", - ProjectID: projectID, - TriggerActivities: []string{"COMPLETED", "WAITING_FOR_INPUT"}, - CreatedBy: "user-project-watcher", - } - require.NoError(t, s.CreateNotificationSubscription(ctx, projectSub)) - - // Create an agent-scoped subscription in the same project - agentSub := &store.NotificationSubscription{ - ID: uuid.New().String(), - Scope: store.SubscriptionScopeAgent, - AgentID: agentID, - SubscriberType: store.SubscriberTypeUser, - SubscriberID: "user-agent-watcher", - ProjectID: projectID, - TriggerActivities: []string{"COMPLETED"}, - CreatedBy: "user-agent-watcher", - } - require.NoError(t, s.CreateNotificationSubscription(ctx, agentSub)) - - // GetNotificationSubscriptionsByProjectScope should return only project-scoped - projectSubs, err := s.GetNotificationSubscriptionsByProjectScope(ctx, projectID) - require.NoError(t, err) - require.Len(t, projectSubs, 1) - assert.Equal(t, projectSub.ID, projectSubs[0].ID) - assert.Equal(t, store.SubscriptionScopeProject, projectSubs[0].Scope) - assert.Empty(t, projectSubs[0].AgentID) - - // GetNotificationSubscriptionsByProject should return both - allSubs, err := s.GetNotificationSubscriptionsByProject(ctx, projectID) - require.NoError(t, err) - assert.Len(t, allSubs, 2) - - // GetNotificationSubscriptions (by agent) should return only agent-scoped - agentSubs, err := s.GetNotificationSubscriptions(ctx, agentID) - require.NoError(t, err) - require.Len(t, agentSubs, 1) - assert.Equal(t, agentSub.ID, agentSubs[0].ID) -} - -func TestGetSubscriptionsForSubscriber(t *testing.T) { - s := setupTestStore(t) - ctx := context.Background() - projectID, agentID := createTestProjectAndAgent(t, s) - - // Create project-scoped subscription for user - sub1 := &store.NotificationSubscription{ - ID: uuid.New().String(), - Scope: store.SubscriptionScopeProject, - SubscriberType: store.SubscriberTypeUser, - SubscriberID: "sub-user", - ProjectID: projectID, - TriggerActivities: []string{"COMPLETED"}, - CreatedBy: "sub-user", - } - require.NoError(t, s.CreateNotificationSubscription(ctx, sub1)) - - // Create agent-scoped subscription for same user - sub2 := &store.NotificationSubscription{ - ID: uuid.New().String(), - Scope: store.SubscriptionScopeAgent, - AgentID: agentID, - SubscriberType: store.SubscriberTypeUser, - SubscriberID: "sub-user", - ProjectID: projectID, - TriggerActivities: []string{"COMPLETED"}, - CreatedBy: "sub-user", - } - require.NoError(t, s.CreateNotificationSubscription(ctx, sub2)) - - // Create subscription for different user - sub3 := &store.NotificationSubscription{ - ID: uuid.New().String(), - Scope: store.SubscriptionScopeAgent, - AgentID: agentID, - SubscriberType: store.SubscriberTypeUser, - SubscriberID: "other-user", - ProjectID: projectID, - TriggerActivities: []string{"COMPLETED"}, - CreatedBy: "other-user", - } - require.NoError(t, s.CreateNotificationSubscription(ctx, sub3)) - - // Get for sub-user - subs, err := s.GetSubscriptionsForSubscriber(ctx, store.SubscriberTypeUser, "sub-user") - require.NoError(t, err) - assert.Len(t, subs, 2) - - // Get for other-user - subs, err = s.GetSubscriptionsForSubscriber(ctx, store.SubscriberTypeUser, "other-user") - require.NoError(t, err) - assert.Len(t, subs, 1) - - // Get for non-existent - subs, err = s.GetSubscriptionsForSubscriber(ctx, store.SubscriberTypeUser, "nobody") - require.NoError(t, err) - assert.Empty(t, subs) -} - -func TestSubscriptionUniqueConstraint(t *testing.T) { - s := setupTestStore(t) - ctx := context.Background() - projectID, agentID := createTestProjectAndAgent(t, s) - - // Create first subscription - sub1 := &store.NotificationSubscription{ - ID: uuid.New().String(), - Scope: store.SubscriptionScopeAgent, - AgentID: agentID, - SubscriberType: store.SubscriberTypeUser, - SubscriberID: "unique-user", - ProjectID: projectID, - TriggerActivities: []string{"COMPLETED"}, - CreatedBy: "unique-user", - } - require.NoError(t, s.CreateNotificationSubscription(ctx, sub1)) - - // Duplicate should fail with ErrAlreadyExists - sub2 := &store.NotificationSubscription{ - ID: uuid.New().String(), - Scope: store.SubscriptionScopeAgent, - AgentID: agentID, - SubscriberType: store.SubscriberTypeUser, - SubscriberID: "unique-user", - ProjectID: projectID, - TriggerActivities: []string{"COMPLETED", "WAITING_FOR_INPUT"}, - CreatedBy: "unique-user", - } - err := s.CreateNotificationSubscription(ctx, sub2) - assert.ErrorIs(t, err, store.ErrAlreadyExists) - - // Same subscriber with different scope should succeed - sub3 := &store.NotificationSubscription{ - ID: uuid.New().String(), - Scope: store.SubscriptionScopeProject, - SubscriberType: store.SubscriberTypeUser, - SubscriberID: "unique-user", - ProjectID: projectID, - TriggerActivities: []string{"COMPLETED"}, - CreatedBy: "unique-user", - } - require.NoError(t, s.CreateNotificationSubscription(ctx, sub3)) -} - -func TestProjectScopedValidation(t *testing.T) { - s := setupTestStore(t) - ctx := context.Background() - projectID, _ := createTestProjectAndAgent(t, s) - - // project-scoped with agent_id should clear agent_id - sub := &store.NotificationSubscription{ - ID: uuid.New().String(), - Scope: store.SubscriptionScopeProject, - AgentID: "should-be-cleared", - SubscriberType: store.SubscriberTypeUser, - SubscriberID: "validation-user", - ProjectID: projectID, - TriggerActivities: []string{"COMPLETED"}, - CreatedBy: "validation-user", - } - require.NoError(t, s.CreateNotificationSubscription(ctx, sub)) - assert.Empty(t, sub.AgentID) // Should have been cleared - - // agent-scoped without agent_id should fail - sub2 := &store.NotificationSubscription{ - ID: uuid.New().String(), - Scope: store.SubscriptionScopeAgent, - AgentID: "", - SubscriberType: store.SubscriberTypeUser, - SubscriberID: "validation-user2", - ProjectID: projectID, - TriggerActivities: []string{"COMPLETED"}, - CreatedBy: "validation-user2", - } - err := s.CreateNotificationSubscription(ctx, sub2) - assert.ErrorIs(t, err, store.ErrInvalidInput) -} - -func TestNotificationSubscriptionFKConstraint(t *testing.T) { - s := setupTestStore(t) - ctx := context.Background() - - // Try to create subscription with non-existent agent - sub := &store.NotificationSubscription{ - ID: uuid.New().String(), - Scope: store.SubscriptionScopeAgent, - AgentID: "non-existent-agent", - SubscriberType: store.SubscriberTypeAgent, - SubscriberID: "lead-agent", - ProjectID: "some-project", - TriggerActivities: []string{"COMPLETED"}, - CreatedBy: "lead-agent", - } - - err := s.CreateNotificationSubscription(ctx, sub) - assert.Error(t, err) - assert.ErrorIs(t, err, store.ErrInvalidInput) -} - -func TestNotificationSubscriptionCascadeDelete(t *testing.T) { - s := setupTestStore(t) - ctx := context.Background() - projectID, agentID := createTestProjectAndAgent(t, s) - - // Create subscription - subID := uuid.New().String() - sub := &store.NotificationSubscription{ - ID: subID, - Scope: store.SubscriptionScopeAgent, - AgentID: agentID, - SubscriberType: store.SubscriberTypeAgent, - SubscriberID: "lead-agent", - ProjectID: projectID, - TriggerActivities: []string{"COMPLETED"}, - CreatedBy: "lead-agent", - } - require.NoError(t, s.CreateNotificationSubscription(ctx, sub)) - - // Create notification for this subscription - notifID := uuid.New().String() - notif := &store.Notification{ - ID: notifID, - SubscriptionID: subID, - AgentID: agentID, - ProjectID: projectID, - SubscriberType: store.SubscriberTypeAgent, - SubscriberID: "lead-agent", - Status: "COMPLETED", - Message: "agent completed", - } - require.NoError(t, s.CreateNotification(ctx, notif)) - - // Verify notification exists - notifs, err := s.GetNotifications(ctx, store.SubscriberTypeAgent, "lead-agent", false) - require.NoError(t, err) - require.Len(t, notifs, 1) - - // Delete the agent — should cascade to subscriptions and their notifications - err = s.DeleteAgent(ctx, agentID) - require.NoError(t, err) - - // Verify subscription is gone - subs, err := s.GetNotificationSubscriptions(ctx, agentID) - require.NoError(t, err) - assert.Empty(t, subs) - - // Verify notification is gone (cascaded from subscription) - notifs, err = s.GetNotifications(ctx, store.SubscriberTypeAgent, "lead-agent", false) - require.NoError(t, err) - assert.Empty(t, notifs) -} - -func TestBulkDeleteSubscriptions(t *testing.T) { - s := setupTestStore(t) - ctx := context.Background() - projectID, agentID := createTestProjectAndAgent(t, s) - - // Create multiple subscriptions - for i := 0; i < 3; i++ { - sub := &store.NotificationSubscription{ - ID: uuid.New().String(), - Scope: store.SubscriptionScopeAgent, - AgentID: agentID, - SubscriberType: store.SubscriberTypeAgent, - SubscriberID: "subscriber-" + uuid.New().String()[:8], - ProjectID: projectID, - TriggerActivities: []string{"COMPLETED"}, - CreatedBy: "test", - } - require.NoError(t, s.CreateNotificationSubscription(ctx, sub)) - } - - // Verify they exist - subs, err := s.GetNotificationSubscriptions(ctx, agentID) - require.NoError(t, err) - assert.Len(t, subs, 3) - - // Bulk delete - err = s.DeleteNotificationSubscriptionsForAgent(ctx, agentID) - require.NoError(t, err) - - // Verify all gone - subs, err = s.GetNotificationSubscriptions(ctx, agentID) - require.NoError(t, err) - assert.Empty(t, subs) - - // Repeat — no error on zero rows - err = s.DeleteNotificationSubscriptionsForAgent(ctx, agentID) - assert.NoError(t, err) -} - -func TestNotificationCRUD(t *testing.T) { - s := setupTestStore(t) - ctx := context.Background() - projectID, agentID := createTestProjectAndAgent(t, s) - - // Create subscription first - subID := uuid.New().String() - sub := &store.NotificationSubscription{ - ID: subID, - Scope: store.SubscriptionScopeAgent, - AgentID: agentID, - SubscriberType: store.SubscriberTypeUser, - SubscriberID: "user-123", - ProjectID: projectID, - TriggerActivities: []string{"COMPLETED", "WAITING_FOR_INPUT"}, - CreatedBy: "user-123", - } - require.NoError(t, s.CreateNotificationSubscription(ctx, sub)) - - // Create notification - notifID := uuid.New().String() - notif := &store.Notification{ - ID: notifID, - SubscriptionID: subID, - AgentID: agentID, - ProjectID: projectID, - SubscriberType: store.SubscriberTypeUser, - SubscriberID: "user-123", - Status: "COMPLETED", - Message: "agent has reached a state of COMPLETED", - } - err := s.CreateNotification(ctx, notif) - require.NoError(t, err) - assert.False(t, notif.CreatedAt.IsZero(), "CreatedAt should be set automatically") - - // Get notifications for subscriber - notifs, err := s.GetNotifications(ctx, store.SubscriberTypeUser, "user-123", false) - require.NoError(t, err) - require.Len(t, notifs, 1) - assert.Equal(t, notifID, notifs[0].ID) - assert.Equal(t, subID, notifs[0].SubscriptionID) - assert.Equal(t, agentID, notifs[0].AgentID) - assert.Equal(t, "COMPLETED", notifs[0].Status) - assert.Equal(t, "agent has reached a state of COMPLETED", notifs[0].Message) - assert.False(t, notifs[0].Dispatched) - assert.False(t, notifs[0].Acknowledged) - - // Acknowledge - err = s.AcknowledgeNotification(ctx, notifID) - require.NoError(t, err) - - // Verify acknowledged - notifs, err = s.GetNotifications(ctx, store.SubscriberTypeUser, "user-123", false) - require.NoError(t, err) - require.Len(t, notifs, 1) - assert.True(t, notifs[0].Acknowledged) - - // Acknowledge not found - err = s.AcknowledgeNotification(ctx, "non-existent") - assert.ErrorIs(t, err, store.ErrNotFound) -} - -func TestNotificationFiltering(t *testing.T) { - s := setupTestStore(t) - ctx := context.Background() - projectID, agentID := createTestProjectAndAgent(t, s) - - subID := uuid.New().String() - sub := &store.NotificationSubscription{ - ID: subID, - Scope: store.SubscriptionScopeAgent, - AgentID: agentID, - SubscriberType: store.SubscriberTypeUser, - SubscriberID: "filter-user", - ProjectID: projectID, - TriggerActivities: []string{"COMPLETED"}, - CreatedBy: "filter-user", - } - require.NoError(t, s.CreateNotificationSubscription(ctx, sub)) - - // Create two notifications — one acknowledged, one not - notif1 := &store.Notification{ - ID: uuid.New().String(), - SubscriptionID: subID, - AgentID: agentID, - ProjectID: projectID, - SubscriberType: store.SubscriberTypeUser, - SubscriberID: "filter-user", - Status: "COMPLETED", - Message: "first notification", - CreatedAt: time.Now().Add(-2 * time.Second), - } - notif2 := &store.Notification{ - ID: uuid.New().String(), - SubscriptionID: subID, - AgentID: agentID, - ProjectID: projectID, - SubscriberType: store.SubscriberTypeUser, - SubscriberID: "filter-user", - Status: "COMPLETED", - Message: "second notification", - CreatedAt: time.Now(), - } - require.NoError(t, s.CreateNotification(ctx, notif1)) - require.NoError(t, s.CreateNotification(ctx, notif2)) - - // Acknowledge the first one - require.NoError(t, s.AcknowledgeNotification(ctx, notif1.ID)) - - // Get all — should return both, ordered by created_at DESC - all, err := s.GetNotifications(ctx, store.SubscriberTypeUser, "filter-user", false) - require.NoError(t, err) - require.Len(t, all, 2) - assert.Equal(t, notif2.ID, all[0].ID, "most recent should be first") - assert.Equal(t, notif1.ID, all[1].ID) - - // Get only unacknowledged — should return only the second - unacked, err := s.GetNotifications(ctx, store.SubscriberTypeUser, "filter-user", true) - require.NoError(t, err) - require.Len(t, unacked, 1) - assert.Equal(t, notif2.ID, unacked[0].ID) -} - -func TestMarkNotificationDispatched(t *testing.T) { - s := setupTestStore(t) - ctx := context.Background() - projectID, agentID := createTestProjectAndAgent(t, s) - - subID := uuid.New().String() - sub := &store.NotificationSubscription{ - ID: subID, - Scope: store.SubscriptionScopeAgent, - AgentID: agentID, - SubscriberType: store.SubscriberTypeAgent, - SubscriberID: "dispatch-target", - ProjectID: projectID, - TriggerActivities: []string{"COMPLETED"}, - CreatedBy: "test", - } - require.NoError(t, s.CreateNotificationSubscription(ctx, sub)) - - notifID := uuid.New().String() - notif := &store.Notification{ - ID: notifID, - SubscriptionID: subID, - AgentID: agentID, - ProjectID: projectID, - SubscriberType: store.SubscriberTypeAgent, - SubscriberID: "dispatch-target", - Status: "COMPLETED", - Message: "dispatched test", - } - require.NoError(t, s.CreateNotification(ctx, notif)) - - // Initially not dispatched - notifs, err := s.GetNotifications(ctx, store.SubscriberTypeAgent, "dispatch-target", false) - require.NoError(t, err) - require.Len(t, notifs, 1) - assert.False(t, notifs[0].Dispatched) - - // Mark dispatched - err = s.MarkNotificationDispatched(ctx, notifID) - require.NoError(t, err) - - // Verify dispatched - notifs, err = s.GetNotifications(ctx, store.SubscriberTypeAgent, "dispatch-target", false) - require.NoError(t, err) - require.Len(t, notifs, 1) - assert.True(t, notifs[0].Dispatched) - - // Not found - err = s.MarkNotificationDispatched(ctx, "non-existent") - assert.ErrorIs(t, err, store.ErrNotFound) -} - -func TestAcknowledgeAllNotifications(t *testing.T) { - s := setupTestStore(t) - ctx := context.Background() - projectID, agentID := createTestProjectAndAgent(t, s) - - subID := uuid.New().String() - sub := &store.NotificationSubscription{ - ID: subID, - Scope: store.SubscriptionScopeAgent, - AgentID: agentID, - SubscriberType: store.SubscriberTypeUser, - SubscriberID: "ack-all-user", - ProjectID: projectID, - TriggerActivities: []string{"COMPLETED"}, - CreatedBy: "ack-all-user", - } - require.NoError(t, s.CreateNotificationSubscription(ctx, sub)) - - // Create multiple notifications - for i := 0; i < 3; i++ { - notif := &store.Notification{ - ID: uuid.New().String(), - SubscriptionID: subID, - AgentID: agentID, - ProjectID: projectID, - SubscriberType: store.SubscriberTypeUser, - SubscriberID: "ack-all-user", - Status: "COMPLETED", - Message: "notification", - } - require.NoError(t, s.CreateNotification(ctx, notif)) - } - - // All unacknowledged - unacked, err := s.GetNotifications(ctx, store.SubscriberTypeUser, "ack-all-user", true) - require.NoError(t, err) - assert.Len(t, unacked, 3) - - // Acknowledge all - err = s.AcknowledgeAllNotifications(ctx, store.SubscriberTypeUser, "ack-all-user") - require.NoError(t, err) - - // Verify all acknowledged - unacked, err = s.GetNotifications(ctx, store.SubscriberTypeUser, "ack-all-user", true) - require.NoError(t, err) - assert.Empty(t, unacked) - - // All should still be retrievable - all, err := s.GetNotifications(ctx, store.SubscriberTypeUser, "ack-all-user", false) - require.NoError(t, err) - assert.Len(t, all, 3) - - // Repeat — no error on zero rows - err = s.AcknowledgeAllNotifications(ctx, store.SubscriberTypeUser, "ack-all-user") - assert.NoError(t, err) -} - -func TestGetLastNotificationStatus(t *testing.T) { - s := setupTestStore(t) - ctx := context.Background() - projectID, agentID := createTestProjectAndAgent(t, s) - - subID := uuid.New().String() - sub := &store.NotificationSubscription{ - ID: subID, - Scope: store.SubscriptionScopeAgent, - AgentID: agentID, - SubscriberType: store.SubscriberTypeAgent, - SubscriberID: "last-status-agent", - ProjectID: projectID, - TriggerActivities: []string{"COMPLETED", "WAITING_FOR_INPUT"}, - CreatedBy: "test", - } - require.NoError(t, s.CreateNotificationSubscription(ctx, sub)) - - // No notifications yet — should return empty string, no error - status, err := s.GetLastNotificationStatus(ctx, subID) - require.NoError(t, err) - assert.Equal(t, "", status) - - // Create first notification - notif1 := &store.Notification{ - ID: uuid.New().String(), - SubscriptionID: subID, - AgentID: agentID, - ProjectID: projectID, - SubscriberType: store.SubscriberTypeAgent, - SubscriberID: "last-status-agent", - Status: "WAITING_FOR_INPUT", - Message: "waiting", - CreatedAt: time.Now().Add(-1 * time.Second), - } - require.NoError(t, s.CreateNotification(ctx, notif1)) - - status, err = s.GetLastNotificationStatus(ctx, subID) - require.NoError(t, err) - assert.Equal(t, "WAITING_FOR_INPUT", status) - - // Create second notification (more recent) - notif2 := &store.Notification{ - ID: uuid.New().String(), - SubscriptionID: subID, - AgentID: agentID, - ProjectID: projectID, - SubscriberType: store.SubscriberTypeAgent, - SubscriberID: "last-status-agent", - Status: "COMPLETED", - Message: "done", - CreatedAt: time.Now(), - } - require.NoError(t, s.CreateNotification(ctx, notif2)) - - status, err = s.GetLastNotificationStatus(ctx, subID) - require.NoError(t, err) - assert.Equal(t, "COMPLETED", status) -} - -func TestMatchesActivity(t *testing.T) { - sub := &store.NotificationSubscription{ - TriggerActivities: []string{"COMPLETED", "WAITING_FOR_INPUT"}, - } - - // Case-insensitive matching - assert.True(t, sub.MatchesActivity("COMPLETED")) - assert.True(t, sub.MatchesActivity("completed")) - assert.True(t, sub.MatchesActivity("Completed")) - assert.True(t, sub.MatchesActivity("waiting_for_input")) - assert.True(t, sub.MatchesActivity("WAITING_FOR_INPUT")) - - // Non-matching - assert.False(t, sub.MatchesActivity("RUNNING")) - assert.False(t, sub.MatchesActivity("error")) - assert.False(t, sub.MatchesActivity("")) - - // Empty trigger list - emptySub := &store.NotificationSubscription{ - TriggerActivities: []string{}, - } - assert.False(t, emptySub.MatchesActivity("COMPLETED")) - - // Nil trigger list - nilSub := &store.NotificationSubscription{} - assert.False(t, nilSub.MatchesActivity("COMPLETED")) -} - -func TestGetNotificationsByAgent(t *testing.T) { - s := setupTestStore(t) - ctx := context.Background() - - // Create project and two agents - projectID, agent1ID := createTestProjectAndAgent(t, s) - agent2ID := api.NewUUID() - agent2 := &store.Agent{ - ID: agent2ID, - Slug: "notif-agent2-" + agent2ID[:8], - Name: "Second Agent", - ProjectID: projectID, - Phase: string(state.PhaseRunning), - Visibility: store.VisibilityPrivate, - } - require.NoError(t, s.CreateAgent(ctx, agent2)) - - // Create subscriptions for both agents - sub1ID := uuid.New().String() - sub1 := &store.NotificationSubscription{ - ID: sub1ID, - Scope: store.SubscriptionScopeAgent, - AgentID: agent1ID, - SubscriberType: store.SubscriberTypeUser, - SubscriberID: "user-by-agent", - ProjectID: projectID, - TriggerActivities: []string{"COMPLETED"}, - CreatedBy: "user-by-agent", - } - require.NoError(t, s.CreateNotificationSubscription(ctx, sub1)) - - sub2ID := uuid.New().String() - sub2 := &store.NotificationSubscription{ - ID: sub2ID, - Scope: store.SubscriptionScopeAgent, - AgentID: agent2ID, - SubscriberType: store.SubscriberTypeUser, - SubscriberID: "user-by-agent", - ProjectID: projectID, - TriggerActivities: []string{"COMPLETED"}, - CreatedBy: "user-by-agent", - } - require.NoError(t, s.CreateNotificationSubscription(ctx, sub2)) - - // Create notifications for agent1 (2 notifications, one acked) - n1 := &store.Notification{ - ID: uuid.New().String(), - SubscriptionID: sub1ID, - AgentID: agent1ID, - ProjectID: projectID, - SubscriberType: store.SubscriberTypeUser, - SubscriberID: "user-by-agent", - Status: "COMPLETED", - Message: "agent1 completed first", - CreatedAt: time.Now().Add(-2 * time.Second), - } - n2 := &store.Notification{ - ID: uuid.New().String(), - SubscriptionID: sub1ID, - AgentID: agent1ID, - ProjectID: projectID, - SubscriberType: store.SubscriberTypeUser, - SubscriberID: "user-by-agent", - Status: "COMPLETED", - Message: "agent1 completed second", - CreatedAt: time.Now(), - } - require.NoError(t, s.CreateNotification(ctx, n1)) - require.NoError(t, s.CreateNotification(ctx, n2)) - require.NoError(t, s.AcknowledgeNotification(ctx, n1.ID)) - - // Create notification for agent2 - n3 := &store.Notification{ - ID: uuid.New().String(), - SubscriptionID: sub2ID, - AgentID: agent2ID, - ProjectID: projectID, - SubscriberType: store.SubscriberTypeUser, - SubscriberID: "user-by-agent", - Status: "COMPLETED", - Message: "agent2 completed", - } - require.NoError(t, s.CreateNotification(ctx, n3)) - - // GetNotificationsByAgent for agent1 — all - notifs, err := s.GetNotificationsByAgent(ctx, agent1ID, store.SubscriberTypeUser, "user-by-agent", false) - require.NoError(t, err) - assert.Len(t, notifs, 2) - assert.Equal(t, n2.ID, notifs[0].ID, "most recent first") - assert.Equal(t, n1.ID, notifs[1].ID) - - // GetNotificationsByAgent for agent1 — only unacknowledged - notifs, err = s.GetNotificationsByAgent(ctx, agent1ID, store.SubscriberTypeUser, "user-by-agent", true) - require.NoError(t, err) - assert.Len(t, notifs, 1) - assert.Equal(t, n2.ID, notifs[0].ID) - - // GetNotificationsByAgent for agent2 - notifs, err = s.GetNotificationsByAgent(ctx, agent2ID, store.SubscriberTypeUser, "user-by-agent", false) - require.NoError(t, err) - assert.Len(t, notifs, 1) - assert.Equal(t, n3.ID, notifs[0].ID) - - // GetNotificationsByAgent for non-existent agent - notifs, err = s.GetNotificationsByAgent(ctx, "no-such-agent", store.SubscriberTypeUser, "user-by-agent", false) - require.NoError(t, err) - assert.Empty(t, notifs) -} diff --git a/pkg/store/sqlite/project_sync_state.go b/pkg/store/sqlite/project_sync_state.go deleted file mode 100644 index 8a6b9ce2d..000000000 --- a/pkg/store/sqlite/project_sync_state.go +++ /dev/null @@ -1,142 +0,0 @@ -// Copyright 2026 Google LLC -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package sqlite - -import ( - "context" - "database/sql" - - "github.com/GoogleCloudPlatform/scion/pkg/store" -) - -// ============================================================================ -// Project Sync State Operations -// ============================================================================ - -// UpsertProjectSyncState creates or updates sync state for a project. -func (s *SQLiteStore) UpsertProjectSyncState(ctx context.Context, state *store.ProjectSyncState) error { - if state.ProjectID == "" { - return store.ErrInvalidInput - } - - _, err := s.db.ExecContext(ctx, ` - INSERT INTO project_sync_state (project_id, broker_id, last_sync_time, last_commit_sha, file_count, total_bytes) - VALUES (?, ?, ?, ?, ?, ?) - ON CONFLICT(project_id, broker_id) DO UPDATE SET - last_sync_time = excluded.last_sync_time, - last_commit_sha = excluded.last_commit_sha, - file_count = excluded.file_count, - total_bytes = excluded.total_bytes - `, state.ProjectID, state.BrokerID, - nullableTimePtr(state.LastSyncTime), - nullableString(state.LastCommitSHA), - state.FileCount, state.TotalBytes, - ) - return err -} - -// GetProjectSyncState retrieves sync state for a project and optional broker. -func (s *SQLiteStore) GetProjectSyncState(ctx context.Context, projectID, brokerID string) (*store.ProjectSyncState, error) { - state := &store.ProjectSyncState{} - var lastSyncTime sql.NullTime - var lastCommitSHA sql.NullString - - err := s.db.QueryRowContext(ctx, ` - SELECT project_id, broker_id, last_sync_time, last_commit_sha, file_count, total_bytes - FROM project_sync_state - WHERE project_id = ? AND broker_id = ? - `, projectID, brokerID).Scan( - &state.ProjectID, &state.BrokerID, - &lastSyncTime, &lastCommitSHA, - &state.FileCount, &state.TotalBytes, - ) - if err != nil { - if err == sql.ErrNoRows { - return nil, store.ErrNotFound - } - return nil, err - } - - if lastSyncTime.Valid { - state.LastSyncTime = &lastSyncTime.Time - } - if lastCommitSHA.Valid { - state.LastCommitSHA = lastCommitSHA.String - } - - return state, nil -} - -// ListProjectSyncStates returns all sync states for a project. -func (s *SQLiteStore) ListProjectSyncStates(ctx context.Context, projectID string) ([]store.ProjectSyncState, error) { - rows, err := s.db.QueryContext(ctx, ` - SELECT project_id, broker_id, last_sync_time, last_commit_sha, file_count, total_bytes - FROM project_sync_state - WHERE project_id = ? - ORDER BY broker_id - `, projectID) - if err != nil { - return nil, err - } - defer rows.Close() - - var states []store.ProjectSyncState - for rows.Next() { - var state store.ProjectSyncState - var lastSyncTime sql.NullTime - var lastCommitSHA sql.NullString - - if err := rows.Scan( - &state.ProjectID, &state.BrokerID, - &lastSyncTime, &lastCommitSHA, - &state.FileCount, &state.TotalBytes, - ); err != nil { - return nil, err - } - - if lastSyncTime.Valid { - state.LastSyncTime = &lastSyncTime.Time - } - if lastCommitSHA.Valid { - state.LastCommitSHA = lastCommitSHA.String - } - - states = append(states, state) - } - - if states == nil { - states = []store.ProjectSyncState{} - } - return states, rows.Err() -} - -// DeleteProjectSyncState removes sync state for a project and optional broker. -func (s *SQLiteStore) DeleteProjectSyncState(ctx context.Context, projectID, brokerID string) error { - result, err := s.db.ExecContext(ctx, ` - DELETE FROM project_sync_state WHERE project_id = ? AND broker_id = ? - `, projectID, brokerID) - if err != nil { - return err - } - - rows, err := result.RowsAffected() - if err != nil { - return err - } - if rows == 0 { - return store.ErrNotFound - } - return nil -} diff --git a/pkg/store/sqlite/project_sync_state_test.go b/pkg/store/sqlite/project_sync_state_test.go deleted file mode 100644 index 4ce823d82..000000000 --- a/pkg/store/sqlite/project_sync_state_test.go +++ /dev/null @@ -1,138 +0,0 @@ -// Copyright 2026 Google LLC -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -//go:build !no_sqlite - -package sqlite - -import ( - "context" - "testing" - "time" - - "github.com/GoogleCloudPlatform/scion/pkg/store" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -func TestProjectSyncStateCRUD(t *testing.T) { - s := setupTestStore(t) - ctx := context.Background() - projectID := createTestProject(t, s) - - now := time.Now().UTC().Truncate(time.Second) - - // Upsert (create) - state := &store.ProjectSyncState{ - ProjectID: projectID, - BrokerID: "", - LastSyncTime: &now, - LastCommitSHA: "abc123", - FileCount: 42, - TotalBytes: 123456, - } - err := s.UpsertProjectSyncState(ctx, state) - require.NoError(t, err) - - // Get - got, err := s.GetProjectSyncState(ctx, projectID, "") - require.NoError(t, err) - assert.Equal(t, projectID, got.ProjectID) - assert.Equal(t, "", got.BrokerID) - assert.NotNil(t, got.LastSyncTime) - assert.Equal(t, now, *got.LastSyncTime) - assert.Equal(t, "abc123", got.LastCommitSHA) - assert.Equal(t, 42, got.FileCount) - assert.Equal(t, int64(123456), got.TotalBytes) - - // Upsert (update) - later := now.Add(5 * time.Minute) - state.LastSyncTime = &later - state.FileCount = 50 - state.TotalBytes = 200000 - err = s.UpsertProjectSyncState(ctx, state) - require.NoError(t, err) - - got, err = s.GetProjectSyncState(ctx, projectID, "") - require.NoError(t, err) - assert.Equal(t, later, *got.LastSyncTime) - assert.Equal(t, 50, got.FileCount) - assert.Equal(t, int64(200000), got.TotalBytes) - - // Add a broker-scoped state - brokerState := &store.ProjectSyncState{ - ProjectID: projectID, - BrokerID: "broker-1", - FileCount: 10, - TotalBytes: 5000, - } - err = s.UpsertProjectSyncState(ctx, brokerState) - require.NoError(t, err) - - // List - states, err := s.ListProjectSyncStates(ctx, projectID) - require.NoError(t, err) - assert.Len(t, states, 2) - - // Delete hub-managed state - err = s.DeleteProjectSyncState(ctx, projectID, "") - require.NoError(t, err) - - // Verify only broker state remains - states, err = s.ListProjectSyncStates(ctx, projectID) - require.NoError(t, err) - assert.Len(t, states, 1) - assert.Equal(t, "broker-1", states[0].BrokerID) - - // Get not found - _, err = s.GetProjectSyncState(ctx, projectID, "") - assert.ErrorIs(t, err, store.ErrNotFound) - - // Delete not found - err = s.DeleteProjectSyncState(ctx, projectID, "nonexistent") - assert.ErrorIs(t, err, store.ErrNotFound) -} - -func TestProjectSyncStateValidation(t *testing.T) { - s := setupTestStore(t) - ctx := context.Background() - - // Empty project ID - err := s.UpsertProjectSyncState(ctx, &store.ProjectSyncState{}) - assert.ErrorIs(t, err, store.ErrInvalidInput) -} - -func TestProjectSyncStateCascadeDelete(t *testing.T) { - s := setupTestStore(t) - ctx := context.Background() - projectID := createTestProject(t, s) - - now := time.Now().UTC().Truncate(time.Second) - state := &store.ProjectSyncState{ - ProjectID: projectID, - LastSyncTime: &now, - FileCount: 5, - TotalBytes: 1000, - } - err := s.UpsertProjectSyncState(ctx, state) - require.NoError(t, err) - - // Delete the project (project) - sync state should cascade - err = s.DeleteProject(ctx, projectID) - require.NoError(t, err) - - states, err := s.ListProjectSyncStates(ctx, projectID) - require.NoError(t, err) - assert.Empty(t, states) -} diff --git a/pkg/store/sqlite/schedule.go b/pkg/store/sqlite/schedule.go deleted file mode 100644 index 4349cf96e..000000000 --- a/pkg/store/sqlite/schedule.go +++ /dev/null @@ -1,365 +0,0 @@ -// Copyright 2026 Google LLC -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package sqlite - -import ( - "context" - "database/sql" - "errors" - "fmt" - "strings" - "time" - - "github.com/GoogleCloudPlatform/scion/pkg/store" -) - -// ============================================================================ -// Schedule Operations (Recurring Schedules) -// ============================================================================ - -// CreateSchedule creates a new recurring schedule. -func (s *SQLiteStore) CreateSchedule(ctx context.Context, schedule *store.Schedule) error { - if schedule.ID == "" || schedule.ProjectID == "" || schedule.Name == "" || schedule.CronExpr == "" { - return store.ErrInvalidInput - } - - now := time.Now() - if schedule.CreatedAt.IsZero() { - schedule.CreatedAt = now - } - if schedule.UpdatedAt.IsZero() { - schedule.UpdatedAt = now - } - if schedule.Status == "" { - schedule.Status = store.ScheduleStatusActive - } - - _, err := s.db.ExecContext(ctx, ` - INSERT INTO schedules ( - id, project_id, name, cron_expr, event_type, payload, status, - next_run_at, last_run_at, last_run_status, last_run_error, - run_count, error_count, created_at, created_by, updated_at - ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) - `, - schedule.ID, schedule.ProjectID, schedule.Name, schedule.CronExpr, - schedule.EventType, schedule.Payload, schedule.Status, - nullableTime(timeFromNullablePtr(schedule.NextRunAt)), - nullableTime(timeFromNullablePtr(schedule.LastRunAt)), - nullableString(schedule.LastRunStatus), nullableString(schedule.LastRunError), - schedule.RunCount, schedule.ErrorCount, - schedule.CreatedAt, nullableString(schedule.CreatedBy), schedule.UpdatedAt, - ) - if err != nil { - if strings.Contains(err.Error(), "UNIQUE constraint failed") { - return store.ErrAlreadyExists - } - if strings.Contains(err.Error(), "FOREIGN KEY constraint failed") { - return fmt.Errorf("project %s does not exist: %w", schedule.ProjectID, store.ErrInvalidInput) - } - return err - } - return nil -} - -// GetSchedule retrieves a schedule by ID. -func (s *SQLiteStore) GetSchedule(ctx context.Context, id string) (*store.Schedule, error) { - schedule := &store.Schedule{} - var nextRunAt, lastRunAt sql.NullTime - var lastRunStatus, lastRunError, createdBy sql.NullString - - err := s.db.QueryRowContext(ctx, ` - SELECT id, project_id, name, cron_expr, event_type, payload, status, - next_run_at, last_run_at, last_run_status, last_run_error, - run_count, error_count, created_at, created_by, updated_at - FROM schedules WHERE id = ? - `, id).Scan( - &schedule.ID, &schedule.ProjectID, &schedule.Name, &schedule.CronExpr, - &schedule.EventType, &schedule.Payload, &schedule.Status, - &nextRunAt, &lastRunAt, &lastRunStatus, &lastRunError, - &schedule.RunCount, &schedule.ErrorCount, - &schedule.CreatedAt, &createdBy, &schedule.UpdatedAt, - ) - if err != nil { - if errors.Is(err, sql.ErrNoRows) { - return nil, store.ErrNotFound - } - return nil, err - } - - if nextRunAt.Valid { - schedule.NextRunAt = &nextRunAt.Time - } - if lastRunAt.Valid { - schedule.LastRunAt = &lastRunAt.Time - } - if lastRunStatus.Valid { - schedule.LastRunStatus = lastRunStatus.String - } - if lastRunError.Valid { - schedule.LastRunError = lastRunError.String - } - if createdBy.Valid { - schedule.CreatedBy = createdBy.String - } - - return schedule, nil -} - -// ListSchedules returns schedules matching the filter criteria. -func (s *SQLiteStore) ListSchedules(ctx context.Context, filter store.ScheduleFilter, opts store.ListOptions) (*store.ListResult[store.Schedule], error) { - var conditions []string - var args []interface{} - - if filter.ProjectID != "" { - conditions = append(conditions, "project_id = ?") - args = append(args, filter.ProjectID) - } - if filter.Status != "" { - conditions = append(conditions, "status = ?") - args = append(args, filter.Status) - } else { - // By default, exclude deleted schedules - conditions = append(conditions, "status != ?") - args = append(args, store.ScheduleStatusDeleted) - } - if filter.Name != "" { - conditions = append(conditions, "name = ?") - args = append(args, filter.Name) - } - - whereClause := "" - if len(conditions) > 0 { - whereClause = "WHERE " + strings.Join(conditions, " AND ") - } - - // Get total count - var totalCount int - countQuery := fmt.Sprintf("SELECT COUNT(*) FROM schedules %s", whereClause) - if err := s.db.QueryRowContext(ctx, countQuery, args...).Scan(&totalCount); err != nil { - return nil, err - } - - limit := opts.Limit - if limit <= 0 { - limit = 50 - } - if limit > 200 { - limit = 200 - } - - query := fmt.Sprintf(` - SELECT id, project_id, name, cron_expr, event_type, payload, status, - next_run_at, last_run_at, last_run_status, last_run_error, - run_count, error_count, created_at, created_by, updated_at - FROM schedules %s - ORDER BY created_at DESC - LIMIT ? - `, whereClause) - - queryArgs := append(args, limit+1) //nolint:gocritic - - rows, err := s.db.QueryContext(ctx, query, queryArgs...) - if err != nil { - return nil, err - } - defer rows.Close() - - schedules, err := scanSchedules(rows) - if err != nil { - return nil, err - } - - result := &store.ListResult[store.Schedule]{ - TotalCount: totalCount, - } - - if len(schedules) > limit { - result.Items = schedules[:limit] - result.NextCursor = schedules[limit-1].ID - } else { - result.Items = schedules - } - - return result, nil -} - -// UpdateSchedule updates an existing schedule. -func (s *SQLiteStore) UpdateSchedule(ctx context.Context, schedule *store.Schedule) error { - schedule.UpdatedAt = time.Now() - - result, err := s.db.ExecContext(ctx, ` - UPDATE schedules SET - name = ?, cron_expr = ?, event_type = ?, payload = ?, - status = ?, next_run_at = ?, updated_at = ? - WHERE id = ? - `, - schedule.Name, schedule.CronExpr, schedule.EventType, schedule.Payload, - schedule.Status, nullableTime(timeFromNullablePtr(schedule.NextRunAt)), - schedule.UpdatedAt, schedule.ID, - ) - if err != nil { - return err - } - rows, err := result.RowsAffected() - if err != nil { - return err - } - if rows == 0 { - return store.ErrNotFound - } - return nil -} - -// UpdateScheduleStatus updates only the status of a schedule. -func (s *SQLiteStore) UpdateScheduleStatus(ctx context.Context, id string, status string) error { - result, err := s.db.ExecContext(ctx, ` - UPDATE schedules SET status = ?, updated_at = ? WHERE id = ? - `, status, time.Now(), id) - if err != nil { - return err - } - rows, err := result.RowsAffected() - if err != nil { - return err - } - if rows == 0 { - return store.ErrNotFound - } - return nil -} - -// UpdateScheduleAfterRun updates a schedule after a run completes. -func (s *SQLiteStore) UpdateScheduleAfterRun(ctx context.Context, id string, ranAt time.Time, nextRunAt time.Time, errMsg string) error { - var query string - var args []interface{} - - if errMsg != "" { - query = ` - UPDATE schedules SET - last_run_at = ?, next_run_at = ?, last_run_status = ?, last_run_error = ?, - run_count = run_count + 1, error_count = error_count + 1, updated_at = ? - WHERE id = ? - ` - args = []interface{}{ranAt, nextRunAt, store.ScheduleRunError, errMsg, time.Now(), id} - } else { - query = ` - UPDATE schedules SET - last_run_at = ?, next_run_at = ?, last_run_status = ?, last_run_error = NULL, - run_count = run_count + 1, updated_at = ? - WHERE id = ? - ` - args = []interface{}{ranAt, nextRunAt, store.ScheduleRunSuccess, time.Now(), id} - } - - result, err := s.db.ExecContext(ctx, query, args...) - if err != nil { - return err - } - rows, err := result.RowsAffected() - if err != nil { - return err - } - if rows == 0 { - return store.ErrNotFound - } - return nil -} - -// DeleteSchedule removes a schedule by ID (hard delete). -func (s *SQLiteStore) DeleteSchedule(ctx context.Context, id string) error { - result, err := s.db.ExecContext(ctx, "DELETE FROM schedules WHERE id = ?", id) - if err != nil { - return err - } - rows, err := result.RowsAffected() - if err != nil { - return err - } - if rows == 0 { - return store.ErrNotFound - } - return nil -} - -// ListDueSchedules returns active schedules whose next_run_at has passed. -func (s *SQLiteStore) ListDueSchedules(ctx context.Context, now time.Time) ([]store.Schedule, error) { - rows, err := s.db.QueryContext(ctx, ` - SELECT id, project_id, name, cron_expr, event_type, payload, status, - next_run_at, last_run_at, last_run_status, last_run_error, - run_count, error_count, created_at, created_by, updated_at - FROM schedules - WHERE status = ? AND next_run_at IS NOT NULL AND next_run_at <= ? - ORDER BY next_run_at ASC - `, store.ScheduleStatusActive, now) - if err != nil { - return nil, err - } - defer rows.Close() - - return scanSchedules(rows) -} - -// ============================================================================ -// Helpers -// ============================================================================ - -// timeFromNullablePtr returns the time from a pointer, or zero time if nil. -func timeFromNullablePtr(t *time.Time) time.Time { - if t == nil { - return time.Time{} - } - return *t -} - -// scanSchedules scans rows into Schedule slices. -func scanSchedules(rows *sql.Rows) ([]store.Schedule, error) { - var schedules []store.Schedule - for rows.Next() { - var schedule store.Schedule - var nextRunAt, lastRunAt sql.NullTime - var lastRunStatus, lastRunError, createdBy sql.NullString - - if err := rows.Scan( - &schedule.ID, &schedule.ProjectID, &schedule.Name, &schedule.CronExpr, - &schedule.EventType, &schedule.Payload, &schedule.Status, - &nextRunAt, &lastRunAt, &lastRunStatus, &lastRunError, - &schedule.RunCount, &schedule.ErrorCount, - &schedule.CreatedAt, &createdBy, &schedule.UpdatedAt, - ); err != nil { - return nil, err - } - - if nextRunAt.Valid { - schedule.NextRunAt = &nextRunAt.Time - } - if lastRunAt.Valid { - schedule.LastRunAt = &lastRunAt.Time - } - if lastRunStatus.Valid { - schedule.LastRunStatus = lastRunStatus.String - } - if lastRunError.Valid { - schedule.LastRunError = lastRunError.String - } - if createdBy.Valid { - schedule.CreatedBy = createdBy.String - } - schedules = append(schedules, schedule) - } - if err := rows.Err(); err != nil { - return nil, err - } - return schedules, nil -} diff --git a/pkg/store/sqlite/schedule_test.go b/pkg/store/sqlite/schedule_test.go deleted file mode 100644 index f79c7e5e6..000000000 --- a/pkg/store/sqlite/schedule_test.go +++ /dev/null @@ -1,290 +0,0 @@ -// Copyright 2026 Google LLC -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -//go:build !no_sqlite - -package sqlite - -import ( - "context" - "testing" - "time" - - "github.com/GoogleCloudPlatform/scion/pkg/api" - "github.com/GoogleCloudPlatform/scion/pkg/store" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -func TestScheduleCRUD(t *testing.T) { - s := setupTestStore(t) - ctx := context.Background() - projectID := createTestProject(t, s) - - scheduleID := api.NewUUID() - nextRun := time.Now().Add(1 * time.Hour).UTC().Truncate(time.Second) - - sched := &store.Schedule{ - ID: scheduleID, - ProjectID: projectID, - Name: "daily-standup", - CronExpr: "0 9 * * 1-5", - EventType: "message", - Payload: `{"agentName":"all","message":"Status update please"}`, - NextRunAt: &nextRun, - CreatedBy: "user-123", - } - - // Create - err := s.CreateSchedule(ctx, sched) - require.NoError(t, err) - assert.False(t, sched.CreatedAt.IsZero()) - assert.Equal(t, store.ScheduleStatusActive, sched.Status) - - // Get - got, err := s.GetSchedule(ctx, scheduleID) - require.NoError(t, err) - assert.Equal(t, scheduleID, got.ID) - assert.Equal(t, projectID, got.ProjectID) - assert.Equal(t, "daily-standup", got.Name) - assert.Equal(t, "0 9 * * 1-5", got.CronExpr) - assert.Equal(t, "message", got.EventType) - assert.Equal(t, store.ScheduleStatusActive, got.Status) - assert.Equal(t, "user-123", got.CreatedBy) - assert.Equal(t, 0, got.RunCount) - assert.Equal(t, 0, got.ErrorCount) - assert.NotNil(t, got.NextRunAt) - - // Update - got.Name = "weekly-standup" - got.CronExpr = "0 9 * * 1" - err = s.UpdateSchedule(ctx, got) - require.NoError(t, err) - - updated, err := s.GetSchedule(ctx, scheduleID) - require.NoError(t, err) - assert.Equal(t, "weekly-standup", updated.Name) - assert.Equal(t, "0 9 * * 1", updated.CronExpr) - - // Update status - err = s.UpdateScheduleStatus(ctx, scheduleID, store.ScheduleStatusPaused) - require.NoError(t, err) - - paused, err := s.GetSchedule(ctx, scheduleID) - require.NoError(t, err) - assert.Equal(t, store.ScheduleStatusPaused, paused.Status) - - // Delete - err = s.DeleteSchedule(ctx, scheduleID) - require.NoError(t, err) - - _, err = s.GetSchedule(ctx, scheduleID) - assert.ErrorIs(t, err, store.ErrNotFound) -} - -func TestSchedule_DuplicateName(t *testing.T) { - s := setupTestStore(t) - ctx := context.Background() - projectID := createTestProject(t, s) - - sched1 := &store.Schedule{ - ID: api.NewUUID(), - ProjectID: projectID, - Name: "duplicate-name", - CronExpr: "0 * * * *", - EventType: "message", - Payload: "{}", - } - require.NoError(t, s.CreateSchedule(ctx, sched1)) - - sched2 := &store.Schedule{ - ID: api.NewUUID(), - ProjectID: projectID, - Name: "duplicate-name", - CronExpr: "0 * * * *", - EventType: "message", - Payload: "{}", - } - err := s.CreateSchedule(ctx, sched2) - assert.ErrorIs(t, err, store.ErrAlreadyExists) -} - -func TestSchedule_List(t *testing.T) { - s := setupTestStore(t) - ctx := context.Background() - projectID := createTestProject(t, s) - - // Create 3 schedules - for i, name := range []string{"sched-a", "sched-b", "sched-c"} { - status := store.ScheduleStatusActive - if i == 2 { - status = store.ScheduleStatusPaused - } - sched := &store.Schedule{ - ID: api.NewUUID(), - ProjectID: projectID, - Name: name, - CronExpr: "0 * * * *", - EventType: "message", - Payload: "{}", - Status: status, - } - require.NoError(t, s.CreateSchedule(ctx, sched)) - } - - // List all (excludes deleted) - result, err := s.ListSchedules(ctx, store.ScheduleFilter{ProjectID: projectID}, store.ListOptions{}) - require.NoError(t, err) - assert.Equal(t, 3, result.TotalCount) - assert.Len(t, result.Items, 3) - - // Filter by status - result, err = s.ListSchedules(ctx, store.ScheduleFilter{ProjectID: projectID, Status: store.ScheduleStatusActive}, store.ListOptions{}) - require.NoError(t, err) - assert.Equal(t, 2, result.TotalCount) - - result, err = s.ListSchedules(ctx, store.ScheduleFilter{ProjectID: projectID, Status: store.ScheduleStatusPaused}, store.ListOptions{}) - require.NoError(t, err) - assert.Equal(t, 1, result.TotalCount) -} - -func TestSchedule_UpdateAfterRun(t *testing.T) { - s := setupTestStore(t) - ctx := context.Background() - projectID := createTestProject(t, s) - - nextRun := time.Now().Add(-1 * time.Minute).UTC().Truncate(time.Second) - sched := &store.Schedule{ - ID: api.NewUUID(), - ProjectID: projectID, - Name: "run-test", - CronExpr: "0 * * * *", - EventType: "message", - Payload: "{}", - NextRunAt: &nextRun, - } - require.NoError(t, s.CreateSchedule(ctx, sched)) - - // Successful run - ranAt := time.Now().UTC().Truncate(time.Second) - newNextRun := time.Now().Add(1 * time.Hour).UTC().Truncate(time.Second) - err := s.UpdateScheduleAfterRun(ctx, sched.ID, ranAt, newNextRun, "") - require.NoError(t, err) - - got, err := s.GetSchedule(ctx, sched.ID) - require.NoError(t, err) - assert.Equal(t, 1, got.RunCount) - assert.Equal(t, 0, got.ErrorCount) - assert.Equal(t, store.ScheduleRunSuccess, got.LastRunStatus) - assert.Empty(t, got.LastRunError) - assert.NotNil(t, got.LastRunAt) - assert.NotNil(t, got.NextRunAt) - - // Error run - err = s.UpdateScheduleAfterRun(ctx, sched.ID, ranAt, newNextRun, "agent not found") - require.NoError(t, err) - - got, err = s.GetSchedule(ctx, sched.ID) - require.NoError(t, err) - assert.Equal(t, 2, got.RunCount) - assert.Equal(t, 1, got.ErrorCount) - assert.Equal(t, store.ScheduleRunError, got.LastRunStatus) - assert.Equal(t, "agent not found", got.LastRunError) -} - -func TestSchedule_ListDue(t *testing.T) { - s := setupTestStore(t) - ctx := context.Background() - projectID := createTestProject(t, s) - - now := time.Now().UTC() - - // Create a due schedule (next_run_at in the past) - pastRun := now.Add(-5 * time.Minute) - dueSchedule := &store.Schedule{ - ID: api.NewUUID(), - ProjectID: projectID, - Name: "due-schedule", - CronExpr: "0 * * * *", - EventType: "message", - Payload: "{}", - NextRunAt: &pastRun, - } - require.NoError(t, s.CreateSchedule(ctx, dueSchedule)) - - // Create a future schedule (not due yet) - futureRun := now.Add(1 * time.Hour) - futureSchedule := &store.Schedule{ - ID: api.NewUUID(), - ProjectID: projectID, - Name: "future-schedule", - CronExpr: "0 * * * *", - EventType: "message", - Payload: "{}", - NextRunAt: &futureRun, - } - require.NoError(t, s.CreateSchedule(ctx, futureSchedule)) - - // Create a paused schedule (should not be listed even if due) - pausedSchedule := &store.Schedule{ - ID: api.NewUUID(), - ProjectID: projectID, - Name: "paused-schedule", - CronExpr: "0 * * * *", - EventType: "message", - Payload: "{}", - Status: store.ScheduleStatusPaused, - NextRunAt: &pastRun, - } - require.NoError(t, s.CreateSchedule(ctx, pausedSchedule)) - - // List due schedules - dueSchedules, err := s.ListDueSchedules(ctx, now) - require.NoError(t, err) - assert.Len(t, dueSchedules, 1) - assert.Equal(t, "due-schedule", dueSchedules[0].Name) -} - -func TestScheduledEvent_WithScheduleID(t *testing.T) { - s := setupTestStore(t) - ctx := context.Background() - projectID := createTestProject(t, s) - - scheduleID := api.NewUUID() - eventID := api.NewUUID() - - evt := &store.ScheduledEvent{ - ID: eventID, - ProjectID: projectID, - EventType: "message", - FireAt: time.Now().UTC(), - Payload: `{"message":"test"}`, - ScheduleID: scheduleID, - } - require.NoError(t, s.CreateScheduledEvent(ctx, evt)) - - // Verify schedule_id is persisted - got, err := s.GetScheduledEvent(ctx, eventID) - require.NoError(t, err) - assert.Equal(t, scheduleID, got.ScheduleID) - - // Filter by schedule_id - result, err := s.ListScheduledEvents(ctx, store.ScheduledEventFilter{ - ProjectID: projectID, - ScheduleID: scheduleID, - }, store.ListOptions{}) - require.NoError(t, err) - assert.Len(t, result.Items, 1) - assert.Equal(t, eventID, result.Items[0].ID) -} diff --git a/pkg/store/sqlite/scheduled_event.go b/pkg/store/sqlite/scheduled_event.go deleted file mode 100644 index ad78b29c2..000000000 --- a/pkg/store/sqlite/scheduled_event.go +++ /dev/null @@ -1,316 +0,0 @@ -// Copyright 2026 Google LLC -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package sqlite - -import ( - "context" - "database/sql" - "errors" - "fmt" - "strings" - "time" - - "github.com/GoogleCloudPlatform/scion/pkg/store" -) - -// ============================================================================ -// Scheduled Event Operations -// ============================================================================ - -// CreateScheduledEvent creates a new scheduled event. -func (s *SQLiteStore) CreateScheduledEvent(ctx context.Context, event *store.ScheduledEvent) error { - if event.ID == "" || event.ProjectID == "" || event.EventType == "" { - return store.ErrInvalidInput - } - - now := time.Now() - if event.CreatedAt.IsZero() { - event.CreatedAt = now - } - if event.Status == "" { - event.Status = store.ScheduledEventPending - } - - _, err := s.db.ExecContext(ctx, ` - INSERT INTO scheduled_events ( - id, project_id, event_type, fire_at, payload, status, - created_at, created_by, fired_at, error, schedule_id - ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) - `, - event.ID, event.ProjectID, event.EventType, event.FireAt, event.Payload, event.Status, - event.CreatedAt, nullableString(event.CreatedBy), nullableTime(timeFromPtr(event.FiredAt)), nullableString(event.Error), - nullableString(event.ScheduleID), - ) - if err != nil { - if strings.Contains(err.Error(), "UNIQUE constraint failed") { - return store.ErrAlreadyExists - } - if strings.Contains(err.Error(), "FOREIGN KEY constraint failed") { - return fmt.Errorf("project %s does not exist: %w", event.ProjectID, store.ErrInvalidInput) - } - return err - } - return nil -} - -// GetScheduledEvent retrieves a scheduled event by ID. -func (s *SQLiteStore) GetScheduledEvent(ctx context.Context, id string) (*store.ScheduledEvent, error) { - event := &store.ScheduledEvent{} - var createdBy sql.NullString - var firedAt sql.NullTime - var errMsg sql.NullString - var scheduleID sql.NullString - - err := s.db.QueryRowContext(ctx, ` - SELECT id, project_id, event_type, fire_at, payload, status, - created_at, created_by, fired_at, error, schedule_id - FROM scheduled_events WHERE id = ? - `, id).Scan( - &event.ID, &event.ProjectID, &event.EventType, &event.FireAt, &event.Payload, &event.Status, - &event.CreatedAt, &createdBy, &firedAt, &errMsg, &scheduleID, - ) - if err != nil { - if errors.Is(err, sql.ErrNoRows) { - return nil, store.ErrNotFound - } - return nil, err - } - - if createdBy.Valid { - event.CreatedBy = createdBy.String - } - if firedAt.Valid { - event.FiredAt = &firedAt.Time - } - if errMsg.Valid { - event.Error = errMsg.String - } - if scheduleID.Valid { - event.ScheduleID = scheduleID.String - } - - return event, nil -} - -// ListPendingScheduledEvents returns all events with status "pending", -// ordered by fire_at ASC. -func (s *SQLiteStore) ListPendingScheduledEvents(ctx context.Context) ([]store.ScheduledEvent, error) { - rows, err := s.db.QueryContext(ctx, ` - SELECT id, project_id, event_type, fire_at, payload, status, - created_at, created_by, fired_at, error, schedule_id - FROM scheduled_events - WHERE status = ? - ORDER BY fire_at ASC - `, store.ScheduledEventPending) - if err != nil { - return nil, err - } - defer rows.Close() - - return scanScheduledEvents(rows) -} - -// UpdateScheduledEventStatus updates the status and optional error for an event. -func (s *SQLiteStore) UpdateScheduledEventStatus(ctx context.Context, id string, status string, firedAt *time.Time, errMsg string) error { - _, err := s.db.ExecContext(ctx, ` - UPDATE scheduled_events SET status = ?, fired_at = ?, error = ? - WHERE id = ? - `, status, nullableTime(timeFromPtr(firedAt)), nullableString(errMsg), id) - return err -} - -// CancelScheduledEvent marks an event as cancelled. -// Returns ErrNotFound if the event doesn't exist or is not pending. -func (s *SQLiteStore) CancelScheduledEvent(ctx context.Context, id string) error { - result, err := s.db.ExecContext(ctx, ` - UPDATE scheduled_events SET status = ? - WHERE id = ? AND status = ? - `, store.ScheduledEventCancelled, id, store.ScheduledEventPending) - if err != nil { - return err - } - - rows, err := result.RowsAffected() - if err != nil { - return err - } - if rows == 0 { - return store.ErrNotFound - } - return nil -} - -// ListScheduledEvents returns events matching the filter criteria. -func (s *SQLiteStore) ListScheduledEvents(ctx context.Context, filter store.ScheduledEventFilter, opts store.ListOptions) (*store.ListResult[store.ScheduledEvent], error) { - var conditions []string - var args []interface{} - - if filter.ProjectID != "" { - conditions = append(conditions, "project_id = ?") - args = append(args, filter.ProjectID) - } - if filter.EventType != "" { - conditions = append(conditions, "event_type = ?") - args = append(args, filter.EventType) - } - if filter.Status != "" { - conditions = append(conditions, "status = ?") - args = append(args, filter.Status) - } - if filter.ScheduleID != "" { - conditions = append(conditions, "schedule_id = ?") - args = append(args, filter.ScheduleID) - } - - whereClause := "" - if len(conditions) > 0 { - whereClause = "WHERE " + strings.Join(conditions, " AND ") - } - - // Get total count - var totalCount int - countQuery := fmt.Sprintf("SELECT COUNT(*) FROM scheduled_events %s", whereClause) - if err := s.db.QueryRowContext(ctx, countQuery, args...).Scan(&totalCount); err != nil { - return nil, err - } - - // Apply pagination - limit := opts.Limit - if limit <= 0 { - limit = 50 - } - if limit > 200 { - limit = 200 - } - - query := fmt.Sprintf(` - SELECT id, project_id, event_type, fire_at, payload, status, - created_at, created_by, fired_at, error, schedule_id - FROM scheduled_events %s - ORDER BY created_at DESC - LIMIT ? - `, whereClause) - - queryArgs := append(args, limit+1) //nolint:gocritic // intentional append to copy - - if opts.Cursor != "" { - query = fmt.Sprintf(` - SELECT id, project_id, event_type, fire_at, payload, status, - created_at, created_by, fired_at, error, schedule_id - FROM scheduled_events %s AND id < ? - ORDER BY created_at DESC - LIMIT ? - `, whereClause) - if whereClause == "" { - query = ` - SELECT id, project_id, event_type, fire_at, payload, status, - created_at, created_by, fired_at, error, schedule_id - FROM scheduled_events WHERE id < ? - ORDER BY created_at DESC - LIMIT ? - ` - } - queryArgs = append(args, opts.Cursor, limit+1) //nolint:gocritic - } - - rows, err := s.db.QueryContext(ctx, query, queryArgs...) - if err != nil { - return nil, err - } - defer rows.Close() - - events, err := scanScheduledEvents(rows) - if err != nil { - return nil, err - } - - result := &store.ListResult[store.ScheduledEvent]{ - TotalCount: totalCount, - } - - if len(events) > limit { - result.Items = events[:limit] - result.NextCursor = events[limit-1].ID - } else { - result.Items = events - } - - return result, nil -} - -// PurgeOldScheduledEvents removes non-pending events older than cutoff. -func (s *SQLiteStore) PurgeOldScheduledEvents(ctx context.Context, cutoff time.Time) (int, error) { - result, err := s.db.ExecContext(ctx, - "DELETE FROM scheduled_events WHERE status != ? AND created_at < ?", - store.ScheduledEventPending, cutoff, - ) - if err != nil { - return 0, err - } - rowsAffected, err := result.RowsAffected() - if err != nil { - return 0, err - } - return int(rowsAffected), nil -} - -// ============================================================================ -// Helpers -// ============================================================================ - -// timeFromPtr returns the time from a pointer, or zero time if nil. -func timeFromPtr(t *time.Time) time.Time { - if t == nil { - return time.Time{} - } - return *t -} - -// scanScheduledEvents scans rows into ScheduledEvent slices. -func scanScheduledEvents(rows *sql.Rows) ([]store.ScheduledEvent, error) { - var events []store.ScheduledEvent - for rows.Next() { - var event store.ScheduledEvent - var createdBy sql.NullString - var firedAt sql.NullTime - var errMsg sql.NullString - var scheduleID sql.NullString - - if err := rows.Scan( - &event.ID, &event.ProjectID, &event.EventType, &event.FireAt, &event.Payload, &event.Status, - &event.CreatedAt, &createdBy, &firedAt, &errMsg, &scheduleID, - ); err != nil { - return nil, err - } - - if createdBy.Valid { - event.CreatedBy = createdBy.String - } - if firedAt.Valid { - event.FiredAt = &firedAt.Time - } - if errMsg.Valid { - event.Error = errMsg.String - } - if scheduleID.Valid { - event.ScheduleID = scheduleID.String - } - events = append(events, event) - } - if err := rows.Err(); err != nil { - return nil, err - } - return events, nil -} diff --git a/pkg/store/sqlite/scheduled_event_test.go b/pkg/store/sqlite/scheduled_event_test.go deleted file mode 100644 index 0447bc636..000000000 --- a/pkg/store/sqlite/scheduled_event_test.go +++ /dev/null @@ -1,372 +0,0 @@ -// Copyright 2026 Google LLC -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -//go:build !no_sqlite - -package sqlite - -import ( - "context" - "testing" - "time" - - "github.com/GoogleCloudPlatform/scion/pkg/api" - "github.com/GoogleCloudPlatform/scion/pkg/store" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -// createTestProject creates a project for scheduled event tests. -func createTestProject(t *testing.T, s *SQLiteStore) string { - t.Helper() - ctx := context.Background() - - projectID := api.NewUUID() - project := &store.Project{ - ID: projectID, - Name: "Scheduled Event Test Project", - Slug: "sched-project-" + projectID[:8], - Visibility: store.VisibilityPrivate, - } - require.NoError(t, s.CreateProject(ctx, project)) - return projectID -} - -func TestScheduledEventCRUD(t *testing.T) { - s := setupTestStore(t) - ctx := context.Background() - projectID := createTestProject(t, s) - - eventID := api.NewUUID() - fireAt := time.Now().Add(1 * time.Hour).UTC().Truncate(time.Second) - - evt := &store.ScheduledEvent{ - ID: eventID, - ProjectID: projectID, - EventType: "message", - FireAt: fireAt, - Payload: `{"text":"hello"}`, - CreatedBy: "user-123", - } - - // Create - err := s.CreateScheduledEvent(ctx, evt) - require.NoError(t, err) - assert.False(t, evt.CreatedAt.IsZero(), "CreatedAt should be set automatically") - assert.Equal(t, store.ScheduledEventPending, evt.Status) - - // Get - got, err := s.GetScheduledEvent(ctx, eventID) - require.NoError(t, err) - assert.Equal(t, eventID, got.ID) - assert.Equal(t, projectID, got.ProjectID) - assert.Equal(t, "message", got.EventType) - assert.Equal(t, fireAt, got.FireAt.UTC().Truncate(time.Second)) - assert.Equal(t, `{"text":"hello"}`, got.Payload) - assert.Equal(t, store.ScheduledEventPending, got.Status) - assert.Equal(t, "user-123", got.CreatedBy) - assert.Nil(t, got.FiredAt) - assert.Empty(t, got.Error) - - // Get not found - _, err = s.GetScheduledEvent(ctx, "nonexistent") - assert.ErrorIs(t, err, store.ErrNotFound) -} - -func TestScheduledEventCreateValidation(t *testing.T) { - s := setupTestStore(t) - ctx := context.Background() - - // Missing ID - err := s.CreateScheduledEvent(ctx, &store.ScheduledEvent{ - ProjectID: "project-1", - EventType: "message", - }) - assert.ErrorIs(t, err, store.ErrInvalidInput) - - // Missing ProjectID - err = s.CreateScheduledEvent(ctx, &store.ScheduledEvent{ - ID: api.NewUUID(), - EventType: "message", - }) - assert.ErrorIs(t, err, store.ErrInvalidInput) - - // Missing EventType - err = s.CreateScheduledEvent(ctx, &store.ScheduledEvent{ - ID: api.NewUUID(), - ProjectID: "project-1", - }) - assert.ErrorIs(t, err, store.ErrInvalidInput) - - // Non-existent project (FK constraint) - err = s.CreateScheduledEvent(ctx, &store.ScheduledEvent{ - ID: api.NewUUID(), - ProjectID: "nonexistent-project", - EventType: "message", - Payload: "{}", - }) - assert.ErrorIs(t, err, store.ErrInvalidInput) -} - -func TestScheduledEventListPending(t *testing.T) { - s := setupTestStore(t) - ctx := context.Background() - projectID := createTestProject(t, s) - - // Create events with different statuses - pending1 := &store.ScheduledEvent{ - ID: api.NewUUID(), - ProjectID: projectID, - EventType: "message", - FireAt: time.Now().Add(2 * time.Hour), - Payload: "{}", - Status: store.ScheduledEventPending, - } - pending2 := &store.ScheduledEvent{ - ID: api.NewUUID(), - ProjectID: projectID, - EventType: "message", - FireAt: time.Now().Add(1 * time.Hour), // Fires sooner - Payload: "{}", - Status: store.ScheduledEventPending, - } - require.NoError(t, s.CreateScheduledEvent(ctx, pending1)) - require.NoError(t, s.CreateScheduledEvent(ctx, pending2)) - - // Mark one as fired to exclude it - now := time.Now() - require.NoError(t, s.UpdateScheduledEventStatus(ctx, pending1.ID, store.ScheduledEventFired, &now, "")) - - // ListPending should only return the pending one - events, err := s.ListPendingScheduledEvents(ctx) - require.NoError(t, err) - assert.Len(t, events, 1) - assert.Equal(t, pending2.ID, events[0].ID) -} - -func TestScheduledEventListPendingOrderByFireAt(t *testing.T) { - s := setupTestStore(t) - ctx := context.Background() - projectID := createTestProject(t, s) - - // Create events in reverse fire_at order - later := &store.ScheduledEvent{ - ID: api.NewUUID(), - ProjectID: projectID, - EventType: "message", - FireAt: time.Now().Add(3 * time.Hour), - Payload: "{}", - } - sooner := &store.ScheduledEvent{ - ID: api.NewUUID(), - ProjectID: projectID, - EventType: "message", - FireAt: time.Now().Add(1 * time.Hour), - Payload: "{}", - } - require.NoError(t, s.CreateScheduledEvent(ctx, later)) - require.NoError(t, s.CreateScheduledEvent(ctx, sooner)) - - events, err := s.ListPendingScheduledEvents(ctx) - require.NoError(t, err) - require.Len(t, events, 2) - // Should be ordered by fire_at ASC (sooner first) - assert.Equal(t, sooner.ID, events[0].ID) - assert.Equal(t, later.ID, events[1].ID) -} - -func TestScheduledEventUpdateStatus(t *testing.T) { - s := setupTestStore(t) - ctx := context.Background() - projectID := createTestProject(t, s) - - eventID := api.NewUUID() - evt := &store.ScheduledEvent{ - ID: eventID, - ProjectID: projectID, - EventType: "message", - FireAt: time.Now().Add(1 * time.Hour), - Payload: "{}", - } - require.NoError(t, s.CreateScheduledEvent(ctx, evt)) - - // Update to fired with firedAt - now := time.Now().UTC().Truncate(time.Second) - err := s.UpdateScheduledEventStatus(ctx, eventID, store.ScheduledEventFired, &now, "") - require.NoError(t, err) - - got, err := s.GetScheduledEvent(ctx, eventID) - require.NoError(t, err) - assert.Equal(t, store.ScheduledEventFired, got.Status) - require.NotNil(t, got.FiredAt) - assert.Equal(t, now, got.FiredAt.UTC().Truncate(time.Second)) - assert.Empty(t, got.Error) - - // Update with error - err = s.UpdateScheduledEventStatus(ctx, eventID, store.ScheduledEventExpired, &now, "handler failed") - require.NoError(t, err) - - got, err = s.GetScheduledEvent(ctx, eventID) - require.NoError(t, err) - assert.Equal(t, store.ScheduledEventExpired, got.Status) - assert.Equal(t, "handler failed", got.Error) -} - -func TestScheduledEventCancel(t *testing.T) { - s := setupTestStore(t) - ctx := context.Background() - projectID := createTestProject(t, s) - - eventID := api.NewUUID() - evt := &store.ScheduledEvent{ - ID: eventID, - ProjectID: projectID, - EventType: "message", - FireAt: time.Now().Add(1 * time.Hour), - Payload: "{}", - } - require.NoError(t, s.CreateScheduledEvent(ctx, evt)) - - // Cancel pending event - err := s.CancelScheduledEvent(ctx, eventID) - require.NoError(t, err) - - got, err := s.GetScheduledEvent(ctx, eventID) - require.NoError(t, err) - assert.Equal(t, store.ScheduledEventCancelled, got.Status) - - // Cancel again (not pending anymore) — should return ErrNotFound - err = s.CancelScheduledEvent(ctx, eventID) - assert.ErrorIs(t, err, store.ErrNotFound) - - // Cancel non-existent event - err = s.CancelScheduledEvent(ctx, "nonexistent") - assert.ErrorIs(t, err, store.ErrNotFound) -} - -func TestScheduledEventListWithFilter(t *testing.T) { - s := setupTestStore(t) - ctx := context.Background() - projectID1 := createTestProject(t, s) - projectID2 := createTestProject(t, s) - - // Create events across projects and types - events := []*store.ScheduledEvent{ - {ID: api.NewUUID(), ProjectID: projectID1, EventType: "message", FireAt: time.Now().Add(1 * time.Hour), Payload: "{}"}, - {ID: api.NewUUID(), ProjectID: projectID1, EventType: "status_update", FireAt: time.Now().Add(2 * time.Hour), Payload: "{}"}, - {ID: api.NewUUID(), ProjectID: projectID2, EventType: "message", FireAt: time.Now().Add(3 * time.Hour), Payload: "{}"}, - } - for _, evt := range events { - require.NoError(t, s.CreateScheduledEvent(ctx, evt)) - } - - // Filter by project - result, err := s.ListScheduledEvents(ctx, store.ScheduledEventFilter{ProjectID: projectID1}, store.ListOptions{}) - require.NoError(t, err) - assert.Len(t, result.Items, 2) - assert.Equal(t, 2, result.TotalCount) - - // Filter by event type - result, err = s.ListScheduledEvents(ctx, store.ScheduledEventFilter{EventType: "message"}, store.ListOptions{}) - require.NoError(t, err) - assert.Len(t, result.Items, 2) - - // Filter by status - result, err = s.ListScheduledEvents(ctx, store.ScheduledEventFilter{Status: store.ScheduledEventPending}, store.ListOptions{}) - require.NoError(t, err) - assert.Len(t, result.Items, 3) - - // No results - result, err = s.ListScheduledEvents(ctx, store.ScheduledEventFilter{Status: store.ScheduledEventFired}, store.ListOptions{}) - require.NoError(t, err) - assert.Len(t, result.Items, 0) -} - -func TestScheduledEventPurge(t *testing.T) { - s := setupTestStore(t) - ctx := context.Background() - projectID := createTestProject(t, s) - - // Create events: one pending, one fired (old), one cancelled (old), one fired (recent) - pendingEvt := &store.ScheduledEvent{ - ID: api.NewUUID(), ProjectID: projectID, EventType: "message", - FireAt: time.Now().Add(1 * time.Hour), Payload: "{}", - } - firedOldEvt := &store.ScheduledEvent{ - ID: api.NewUUID(), ProjectID: projectID, EventType: "message", - FireAt: time.Now().Add(-48 * time.Hour), Payload: "{}", - CreatedAt: time.Now().Add(-48 * time.Hour), - } - cancelledOldEvt := &store.ScheduledEvent{ - ID: api.NewUUID(), ProjectID: projectID, EventType: "message", - FireAt: time.Now().Add(-48 * time.Hour), Payload: "{}", - CreatedAt: time.Now().Add(-48 * time.Hour), - } - firedRecentEvt := &store.ScheduledEvent{ - ID: api.NewUUID(), ProjectID: projectID, EventType: "message", - FireAt: time.Now().Add(-1 * time.Hour), Payload: "{}", - } - - require.NoError(t, s.CreateScheduledEvent(ctx, pendingEvt)) - require.NoError(t, s.CreateScheduledEvent(ctx, firedOldEvt)) - require.NoError(t, s.CreateScheduledEvent(ctx, cancelledOldEvt)) - require.NoError(t, s.CreateScheduledEvent(ctx, firedRecentEvt)) - - // Mark statuses - now := time.Now() - require.NoError(t, s.UpdateScheduledEventStatus(ctx, firedOldEvt.ID, store.ScheduledEventFired, &now, "")) - require.NoError(t, s.UpdateScheduledEventStatus(ctx, cancelledOldEvt.ID, store.ScheduledEventCancelled, nil, "")) - require.NoError(t, s.UpdateScheduledEventStatus(ctx, firedRecentEvt.ID, store.ScheduledEventFired, &now, "")) - - // Purge events older than 24 hours - cutoff := time.Now().Add(-24 * time.Hour) - purged, err := s.PurgeOldScheduledEvents(ctx, cutoff) - require.NoError(t, err) - // Should purge firedOldEvt and cancelledOldEvt (non-pending, created > 24h ago) - assert.Equal(t, 2, purged) - - // Pending event should still exist - _, err = s.GetScheduledEvent(ctx, pendingEvt.ID) - assert.NoError(t, err) - - // Recently fired event should still exist - _, err = s.GetScheduledEvent(ctx, firedRecentEvt.ID) - assert.NoError(t, err) - - // Old events should be gone - _, err = s.GetScheduledEvent(ctx, firedOldEvt.ID) - assert.ErrorIs(t, err, store.ErrNotFound) - _, err = s.GetScheduledEvent(ctx, cancelledOldEvt.ID) - assert.ErrorIs(t, err, store.ErrNotFound) -} - -func TestScheduledEventOptionalCreatedBy(t *testing.T) { - s := setupTestStore(t) - ctx := context.Background() - projectID := createTestProject(t, s) - - evt := &store.ScheduledEvent{ - ID: api.NewUUID(), - ProjectID: projectID, - EventType: "message", - FireAt: time.Now().Add(1 * time.Hour), - Payload: "{}", - // No CreatedBy - } - require.NoError(t, s.CreateScheduledEvent(ctx, evt)) - - got, err := s.GetScheduledEvent(ctx, evt.ID) - require.NoError(t, err) - assert.Empty(t, got.CreatedBy) -} diff --git a/pkg/store/sqlite/secret_type_test.go b/pkg/store/sqlite/secret_type_test.go deleted file mode 100644 index e038d14c6..000000000 --- a/pkg/store/sqlite/secret_type_test.go +++ /dev/null @@ -1,219 +0,0 @@ -// Copyright 2026 Google LLC -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -//go:build !no_sqlite - -package sqlite - -import ( - "context" - "testing" - - "github.com/GoogleCloudPlatform/scion/pkg/store" - "github.com/google/uuid" -) - -func TestSecretCRUDWithType(t *testing.T) { - s := setupTestStore(t) - ctx := context.Background() - - t.Run("create secret with default type", func(t *testing.T) { - secret := &store.Secret{ - ID: uuid.New().String(), - Key: "API_KEY", - EncryptedValue: "encrypted-value", - Scope: store.ScopeUser, - ScopeID: "user-1", - } - if err := s.CreateSecret(ctx, secret); err != nil { - t.Fatalf("CreateSecret failed: %v", err) - } - if secret.SecretType != store.SecretTypeEnvironment { - t.Errorf("expected default SecretType %q, got %q", store.SecretTypeEnvironment, secret.SecretType) - } - if secret.Target != "API_KEY" { - t.Errorf("expected default Target %q, got %q", "API_KEY", secret.Target) - } - }) - - t.Run("get secret returns type and target", func(t *testing.T) { - got, err := s.GetSecret(ctx, "API_KEY", store.ScopeUser, "user-1") - if err != nil { - t.Fatalf("GetSecret failed: %v", err) - } - if got.SecretType != store.SecretTypeEnvironment { - t.Errorf("expected SecretType %q, got %q", store.SecretTypeEnvironment, got.SecretType) - } - if got.Target != "API_KEY" { - t.Errorf("expected Target %q, got %q", "API_KEY", got.Target) - } - }) - - t.Run("create file secret with explicit type and target", func(t *testing.T) { - secret := &store.Secret{ - ID: uuid.New().String(), - Key: "TLS_CERT", - EncryptedValue: "cert-data", - SecretType: store.SecretTypeFile, - Target: "/etc/ssl/certs/cert.pem", - Scope: store.ScopeUser, - ScopeID: "user-1", - } - if err := s.CreateSecret(ctx, secret); err != nil { - t.Fatalf("CreateSecret failed: %v", err) - } - - got, err := s.GetSecret(ctx, "TLS_CERT", store.ScopeUser, "user-1") - if err != nil { - t.Fatalf("GetSecret failed: %v", err) - } - if got.SecretType != store.SecretTypeFile { - t.Errorf("expected SecretType %q, got %q", store.SecretTypeFile, got.SecretType) - } - if got.Target != "/etc/ssl/certs/cert.pem" { - t.Errorf("expected Target %q, got %q", "/etc/ssl/certs/cert.pem", got.Target) - } - }) - - t.Run("create variable secret", func(t *testing.T) { - secret := &store.Secret{ - ID: uuid.New().String(), - Key: "CONFIG_JSON", - EncryptedValue: `{"key":"value"}`, - SecretType: store.SecretTypeVariable, - Target: "config", - Scope: store.ScopeUser, - ScopeID: "user-1", - } - if err := s.CreateSecret(ctx, secret); err != nil { - t.Fatalf("CreateSecret failed: %v", err) - } - - got, err := s.GetSecret(ctx, "CONFIG_JSON", store.ScopeUser, "user-1") - if err != nil { - t.Fatalf("GetSecret failed: %v", err) - } - if got.SecretType != store.SecretTypeVariable { - t.Errorf("expected SecretType %q, got %q", store.SecretTypeVariable, got.SecretType) - } - if got.Target != "config" { - t.Errorf("expected Target %q, got %q", "config", got.Target) - } - }) - - t.Run("update secret preserves type and target", func(t *testing.T) { - got, err := s.GetSecret(ctx, "TLS_CERT", store.ScopeUser, "user-1") - if err != nil { - t.Fatalf("GetSecret failed: %v", err) - } - - got.EncryptedValue = "updated-cert-data" - got.Target = "/etc/ssl/certs/new-cert.pem" - if err := s.UpdateSecret(ctx, got); err != nil { - t.Fatalf("UpdateSecret failed: %v", err) - } - - updated, err := s.GetSecret(ctx, "TLS_CERT", store.ScopeUser, "user-1") - if err != nil { - t.Fatalf("GetSecret after update failed: %v", err) - } - if updated.SecretType != store.SecretTypeFile { - t.Errorf("expected SecretType %q after update, got %q", store.SecretTypeFile, updated.SecretType) - } - if updated.Target != "/etc/ssl/certs/new-cert.pem" { - t.Errorf("expected Target %q after update, got %q", "/etc/ssl/certs/new-cert.pem", updated.Target) - } - if updated.Version != 2 { - t.Errorf("expected Version 2, got %d", updated.Version) - } - }) - - t.Run("list secrets returns type and target", func(t *testing.T) { - secrets, err := s.ListSecrets(ctx, store.SecretFilter{ - Scope: store.ScopeUser, - ScopeID: "user-1", - }) - if err != nil { - t.Fatalf("ListSecrets failed: %v", err) - } - if len(secrets) != 3 { - t.Fatalf("expected 3 secrets, got %d", len(secrets)) - } - - // Secrets should be ordered by key - for _, s := range secrets { - if s.SecretType == "" { - t.Errorf("secret %q has empty SecretType", s.Key) - } - if s.Target == "" { - t.Errorf("secret %q has empty Target", s.Key) - } - } - }) - - t.Run("list secrets with type filter", func(t *testing.T) { - secrets, err := s.ListSecrets(ctx, store.SecretFilter{ - Scope: store.ScopeUser, - ScopeID: "user-1", - Type: store.SecretTypeFile, - }) - if err != nil { - t.Fatalf("ListSecrets with type filter failed: %v", err) - } - if len(secrets) != 1 { - t.Fatalf("expected 1 file secret, got %d", len(secrets)) - } - if secrets[0].Key != "TLS_CERT" { - t.Errorf("expected key %q, got %q", "TLS_CERT", secrets[0].Key) - } - }) - - t.Run("upsert secret with type", func(t *testing.T) { - secret := &store.Secret{ - ID: uuid.New().String(), - Key: "NEW_SECRET", - EncryptedValue: "value", - SecretType: store.SecretTypeVariable, - Target: "new_key", - Scope: store.ScopeUser, - ScopeID: "user-1", - } - - created, err := s.UpsertSecret(ctx, secret) - if err != nil { - t.Fatalf("UpsertSecret (create) failed: %v", err) - } - if !created { - t.Error("expected UpsertSecret to create a new secret") - } - - // Upsert again (update) - secret.EncryptedValue = "updated-value" - created, err = s.UpsertSecret(ctx, secret) - if err != nil { - t.Fatalf("UpsertSecret (update) failed: %v", err) - } - if created { - t.Error("expected UpsertSecret to update existing secret") - } - - got, err := s.GetSecret(ctx, "NEW_SECRET", store.ScopeUser, "user-1") - if err != nil { - t.Fatalf("GetSecret after upsert failed: %v", err) - } - if got.SecretType != store.SecretTypeVariable { - t.Errorf("expected SecretType %q, got %q", store.SecretTypeVariable, got.SecretType) - } - }) -} diff --git a/pkg/store/sqlite/sqlite.go b/pkg/store/sqlite/sqlite.go deleted file mode 100644 index d69d9f4ae..000000000 --- a/pkg/store/sqlite/sqlite.go +++ /dev/null @@ -1,6021 +0,0 @@ -// Copyright 2026 Google LLC -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -// Package sqlite provides a SQLite implementation of the Store interface. -package sqlite - -import ( - "context" - "database/sql" - "encoding/json" - "errors" - "fmt" - "strconv" - "strings" - "sync/atomic" - "time" - - "github.com/GoogleCloudPlatform/scion/pkg/store" -) - -var memDBCounter atomic.Int64 - -// SQLiteStore implements the Store interface using SQLite. -type SQLiteStore struct { - db *sql.DB -} - -// New creates a new SQLite store with the given database path. -// Use ":memory:" for an in-memory database. -func New(dbPath string) (*SQLiteStore, error) { - dsn := buildDSN(dbPath) - db, err := sql.Open("sqlite", dsn) - if err != nil { - if strings.Contains(err.Error(), "unknown driver") { - return nil, fmt.Errorf("sqlite driver not registered; was the binary built with -tags no_sqlite? %w", err) - } - return nil, fmt.Errorf("failed to open database: %w", err) - } - - // WAL mode allows concurrent readers alongside a single writer. - // PRAGMAs are applied per-connection via DSN _pragma parameters, - // so each pooled connection gets them automatically. - db.SetMaxOpenConns(4) - db.SetMaxIdleConns(4) - - return &SQLiteStore{db: db}, nil -} - -// buildDSN converts a database path into a file: URI with per-connection -// PRAGMA parameters for the modernc.org/sqlite driver. -func buildDSN(dbPath string) string { - pragmas := "_pragma=busy_timeout(5000)&_pragma=foreign_keys(1)&_pragma=journal_mode(WAL)" - - switch { - case dbPath == ":memory:": - id := memDBCounter.Add(1) - return fmt.Sprintf("file:memdb%d?mode=memory&cache=shared&%s", id, pragmas) - case strings.HasPrefix(dbPath, "file:"): - if strings.Contains(dbPath, "?") { - return dbPath + "&" + pragmas - } - return dbPath + "?" + pragmas - default: - return "file:" + dbPath + "?" + pragmas - } -} - -// Close closes the database connection. -func (s *SQLiteStore) Close() error { - return s.db.Close() -} - -// DB returns the underlying *sql.DB for direct access in tests. -func (s *SQLiteStore) DB() *sql.DB { - return s.db -} - -// Ping checks database connectivity. -func (s *SQLiteStore) Ping(ctx context.Context) error { - return s.db.PingContext(ctx) -} - -// Migrate applies database migrations. -func (s *SQLiteStore) Migrate(ctx context.Context) error { - migrations := []any{ - migrationV1, - migrationV2, - migrationV3, - migrationV4, - migrationV5, - migrationV6, - migrationV7, - migrationV8, - migrationV9, - migrationV10, - migrationV11, - migrationV12, - migrationV13, - migrationV14, - migrationV15, - migrationV16, - migrationV17, - migrationV18, - migrationV19, - migrationV20, - migrationV21, - migrationV22, - migrationV23, - migrationV24, - migrationV25, - migrationV26, - migrationV27, - migrationV28, - migrationV29, - migrationV30, - migrationV31, - migrationV32, - migrationV33, - migrationV34, - migrationV35, - migrationV36, - migrationV37, - migrationV38, - migrationV39, - migrationV40, - migrationV41, - migrationV42, - migrationV43, - migrationV44, - migrationV45, - migrationV46, - migrationV47, - migrationV48, - migrationV49, - migrateV50, - migrationV51, - migrationV52, - migrationV53, - } - - // Create migrations table if not exists - if _, err := s.db.ExecContext(ctx, ` - CREATE TABLE IF NOT EXISTS schema_migrations ( - version INTEGER PRIMARY KEY, - applied_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP - ) - `); err != nil { - return fmt.Errorf("failed to create migrations table: %w", err) - } - - // Get current version - var currentVersion int - err := s.db.QueryRowContext(ctx, "SELECT COALESCE(MAX(version), 0) FROM schema_migrations").Scan(¤tVersion) - if err != nil { - return fmt.Errorf("failed to get current schema version: %w", err) - } - - // Migrations that require PRAGMA foreign_keys=OFF around the transaction. - // SQLite ignores PRAGMA changes inside transactions, so we must disable - // foreign keys before BeginTx and re-enable after Commit. Without this, - // DROP TABLE on a parent table triggers ON DELETE CASCADE on child tables. - foreignKeysOffMigrations := map[int]bool{ - 40: true, // V40 drops and recreates the projects table - } - - // Apply pending migrations - for i, migration := range migrations { - version := i + 1 - if version <= currentVersion { - continue - } - - switch m := migration.(type) { - case string: - needsFKOff := foreignKeysOffMigrations[version] - - if needsFKOff { - if err := s.applyMigrationWithFKOff(ctx, version, m); err != nil { - return err - } - continue - } - - tx, err := s.db.BeginTx(ctx, nil) - if err != nil { - return fmt.Errorf("failed to start transaction for migration %d: %w", version, err) - } - - if _, err := tx.ExecContext(ctx, m); err != nil { - tx.Rollback() - return fmt.Errorf("failed to apply migration %d: %w", version, err) - } - - if _, err := tx.ExecContext(ctx, "INSERT INTO schema_migrations (version) VALUES (?)", version); err != nil { - tx.Rollback() - return fmt.Errorf("failed to record migration %d: %w", version, err) - } - - if err := tx.Commit(); err != nil { - return fmt.Errorf("failed to commit migration %d: %w", version, err) - } - - case func(ctx context.Context, tx *sql.Tx) error: - tx, err := s.db.BeginTx(ctx, nil) - if err != nil { - return fmt.Errorf("failed to start transaction for migration %d: %w", version, err) - } - - if err := m(ctx, tx); err != nil { - tx.Rollback() - return fmt.Errorf("failed to apply migration %d: %w", version, err) - } - - if _, err := tx.ExecContext(ctx, "INSERT INTO schema_migrations (version) VALUES (?)", version); err != nil { - tx.Rollback() - return fmt.Errorf("failed to record migration %d: %w", version, err) - } - - if err := tx.Commit(); err != nil { - return fmt.Errorf("failed to commit migration %d: %w", version, err) - } - - default: - return fmt.Errorf("migration %d: unsupported type %T", version, migration) - } - } - - return nil -} - -// applyMigrationWithFKOff runs a migration that requires PRAGMA -// foreign_keys=OFF. It pins a single pooled connection to ensure the -// PRAGMA, transaction, and PRAGMA-restore all share the same connection. -func (s *SQLiteStore) applyMigrationWithFKOff(ctx context.Context, version int, migration string) error { - conn, err := s.db.Conn(ctx) - if err != nil { - return fmt.Errorf("failed to get connection for migration %d: %w", version, err) - } - defer conn.Close() - - if _, err := conn.ExecContext(ctx, "PRAGMA foreign_keys=OFF"); err != nil { - return fmt.Errorf("failed to disable foreign keys for migration %d: %w", version, err) - } - defer conn.ExecContext(ctx, "PRAGMA foreign_keys=ON") //nolint:errcheck - - tx, err := conn.BeginTx(ctx, nil) - if err != nil { - return fmt.Errorf("failed to start transaction for migration %d: %w", version, err) - } - - if _, err := tx.ExecContext(ctx, migration); err != nil { - tx.Rollback() - return fmt.Errorf("failed to apply migration %d: %w", version, err) - } - - if _, err := tx.ExecContext(ctx, "INSERT INTO schema_migrations (version) VALUES (?)", version); err != nil { - tx.Rollback() - return fmt.Errorf("failed to record migration %d: %w", version, err) - } - - if err := tx.Commit(); err != nil { - return fmt.Errorf("failed to commit migration %d: %w", version, err) - } - - return nil -} - -// Migration V1: Initial schema -const migrationV1 = ` --- Projects table -CREATE TABLE IF NOT EXISTS groves ( - id TEXT PRIMARY KEY, - name TEXT NOT NULL, - slug TEXT NOT NULL, - git_remote TEXT UNIQUE, - labels TEXT, - annotations TEXT, - created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, - updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, - created_by TEXT, - owner_id TEXT, - visibility TEXT NOT NULL DEFAULT 'private' -); -CREATE INDEX IF NOT EXISTS idx_groves_slug ON groves(slug); -CREATE INDEX IF NOT EXISTS idx_groves_git_remote ON groves(git_remote); -CREATE INDEX IF NOT EXISTS idx_groves_owner ON groves(owner_id); - --- Runtime brokers table -CREATE TABLE IF NOT EXISTS runtime_brokers ( - id TEXT PRIMARY KEY, - name TEXT NOT NULL, - slug TEXT NOT NULL, - type TEXT NOT NULL, - mode TEXT NOT NULL DEFAULT 'connected', - version TEXT, - status TEXT NOT NULL DEFAULT 'offline', - connection_state TEXT DEFAULT 'disconnected', - last_heartbeat TIMESTAMP, - capabilities TEXT, - supported_harnesses TEXT, - resources TEXT, - runtimes TEXT, - labels TEXT, - annotations TEXT, - endpoint TEXT, - created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, - updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP -); -CREATE INDEX IF NOT EXISTS idx_runtime_brokers_slug ON runtime_brokers(slug); -CREATE INDEX IF NOT EXISTS idx_runtime_brokers_status ON runtime_brokers(status); - --- Project contributors (many-to-many relationship) -CREATE TABLE IF NOT EXISTS grove_contributors ( - grove_id TEXT NOT NULL, - broker_id TEXT NOT NULL, - broker_name TEXT NOT NULL, - mode TEXT NOT NULL DEFAULT 'connected', - status TEXT NOT NULL DEFAULT 'offline', - profiles TEXT, - last_seen TIMESTAMP, - PRIMARY KEY (grove_id, broker_id), - FOREIGN KEY (grove_id) REFERENCES groves(id) ON DELETE CASCADE, - FOREIGN KEY (broker_id) REFERENCES runtime_brokers(id) ON DELETE CASCADE -); - --- Agents table -CREATE TABLE IF NOT EXISTS agents ( - id TEXT PRIMARY KEY, - agent_id TEXT NOT NULL, - name TEXT NOT NULL, - template TEXT NOT NULL, - grove_id TEXT NOT NULL, - labels TEXT, - annotations TEXT, - status TEXT NOT NULL DEFAULT 'pending', - connection_state TEXT DEFAULT 'unknown', - container_status TEXT, - session_status TEXT, - runtime_state TEXT, - image TEXT, - detached INTEGER NOT NULL DEFAULT 1, - runtime TEXT, - runtime_broker_id TEXT, - web_pty_enabled INTEGER NOT NULL DEFAULT 0, - task_summary TEXT, - applied_config TEXT, - created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, - updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, - last_seen TIMESTAMP, - created_by TEXT, - owner_id TEXT, - visibility TEXT NOT NULL DEFAULT 'private', - state_version INTEGER NOT NULL DEFAULT 1, - FOREIGN KEY (grove_id) REFERENCES groves(id) ON DELETE CASCADE, - FOREIGN KEY (runtime_broker_id) REFERENCES runtime_brokers(id) ON DELETE SET NULL -); --- Use (agent_id, grove_id) order to match Ent schema's (slug, project_id) -CREATE UNIQUE INDEX IF NOT EXISTS idx_agents_grove_slug ON agents(agent_id, grove_id); -CREATE INDEX IF NOT EXISTS idx_agents_grove ON agents(grove_id); -CREATE INDEX IF NOT EXISTS idx_agents_status ON agents(status); -CREATE INDEX IF NOT EXISTS idx_agents_runtime_broker ON agents(runtime_broker_id); - --- Templates table -CREATE TABLE IF NOT EXISTS templates ( - id TEXT PRIMARY KEY, - name TEXT NOT NULL, - slug TEXT NOT NULL, - harness TEXT NOT NULL, - image TEXT, - config TEXT, - scope TEXT NOT NULL DEFAULT 'global', - grove_id TEXT, - storage_uri TEXT, - owner_id TEXT, - visibility TEXT NOT NULL DEFAULT 'private', - created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, - updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, - FOREIGN KEY (grove_id) REFERENCES groves(id) ON DELETE CASCADE -); -CREATE INDEX IF NOT EXISTS idx_templates_slug_scope ON templates(slug, scope); -CREATE INDEX IF NOT EXISTS idx_templates_harness ON templates(harness); - --- Users table -CREATE TABLE IF NOT EXISTS users ( - id TEXT PRIMARY KEY, - email TEXT UNIQUE NOT NULL, - display_name TEXT NOT NULL, - avatar_url TEXT, - role TEXT NOT NULL DEFAULT 'member', - status TEXT NOT NULL DEFAULT 'active', - preferences TEXT, - created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, - last_login TIMESTAMP -); -CREATE INDEX IF NOT EXISTS idx_users_email ON users(email); -` - -// Migration V2: Add default_runtime_broker_id to groves -const migrationV2 = ` --- Add default runtime broker to groves -ALTER TABLE groves ADD COLUMN default_runtime_broker_id TEXT REFERENCES runtime_brokers(id) ON DELETE SET NULL; -CREATE INDEX IF NOT EXISTS idx_groves_default_runtime_broker ON groves(default_runtime_broker_id); -` - -// Migration V3: Add local_path to grove_contributors -const migrationV3 = ` --- Add local_path column to grove_contributors for tracking filesystem paths per broker -ALTER TABLE grove_contributors ADD COLUMN local_path TEXT; -` - -// Migration V4: Add environment variables and secrets tables -const migrationV4 = ` --- Environment variables table -CREATE TABLE IF NOT EXISTS env_vars ( - id TEXT PRIMARY KEY, - key TEXT NOT NULL, - value TEXT NOT NULL, - scope TEXT NOT NULL, - scope_id TEXT NOT NULL, - description TEXT, - sensitive INTEGER NOT NULL DEFAULT 0, - created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, - updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, - created_by TEXT -); -CREATE UNIQUE INDEX IF NOT EXISTS idx_env_vars_key_scope ON env_vars(key, scope, scope_id); -CREATE INDEX IF NOT EXISTS idx_env_vars_scope ON env_vars(scope, scope_id); - --- Secrets table -CREATE TABLE IF NOT EXISTS secrets ( - id TEXT PRIMARY KEY, - key TEXT NOT NULL, - encrypted_value TEXT NOT NULL, - scope TEXT NOT NULL, - scope_id TEXT NOT NULL, - description TEXT, - version INTEGER NOT NULL DEFAULT 1, - created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, - updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, - created_by TEXT, - updated_by TEXT -); -CREATE UNIQUE INDEX IF NOT EXISTS idx_secrets_key_scope ON secrets(key, scope, scope_id); -CREATE INDEX IF NOT EXISTS idx_secrets_scope ON secrets(scope, scope_id); -` - -// Migration V5: Groups and Policies (Hub Permissions System) -const migrationV5 = ` --- Groups table -CREATE TABLE IF NOT EXISTS groups ( - id TEXT PRIMARY KEY, - name TEXT NOT NULL, - slug TEXT UNIQUE NOT NULL, - description TEXT, - parent_id TEXT REFERENCES groups(id) ON DELETE SET NULL, - labels TEXT, - annotations TEXT, - created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, - updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, - created_by TEXT, - owner_id TEXT -); -CREATE INDEX IF NOT EXISTS idx_groups_slug ON groups(slug); -CREATE INDEX IF NOT EXISTS idx_groups_parent ON groups(parent_id); -CREATE INDEX IF NOT EXISTS idx_groups_owner ON groups(owner_id); - --- Group members table (users and nested groups) -CREATE TABLE IF NOT EXISTS group_members ( - group_id TEXT NOT NULL, - member_type TEXT NOT NULL, -- 'user' or 'group' - member_id TEXT NOT NULL, - role TEXT NOT NULL DEFAULT 'member', - added_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, - added_by TEXT, - PRIMARY KEY (group_id, member_type, member_id), - FOREIGN KEY (group_id) REFERENCES groups(id) ON DELETE CASCADE -); -CREATE INDEX IF NOT EXISTS idx_group_members_member ON group_members(member_type, member_id); - --- Policies table -CREATE TABLE IF NOT EXISTS policies ( - id TEXT PRIMARY KEY, - name TEXT NOT NULL, - description TEXT, - scope_type TEXT NOT NULL, - scope_id TEXT, - resource_type TEXT NOT NULL DEFAULT '*', - resource_id TEXT, - actions TEXT NOT NULL, -- JSON array - effect TEXT NOT NULL, - conditions TEXT, -- JSON object - priority INTEGER NOT NULL DEFAULT 0, - labels TEXT, - annotations TEXT, - created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, - updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, - created_by TEXT -); -CREATE INDEX IF NOT EXISTS idx_policies_scope ON policies(scope_type, scope_id); -CREATE INDEX IF NOT EXISTS idx_policies_effect ON policies(effect); -CREATE INDEX IF NOT EXISTS idx_policies_priority ON policies(priority DESC); - --- Policy bindings table -CREATE TABLE IF NOT EXISTS policy_bindings ( - policy_id TEXT NOT NULL, - principal_type TEXT NOT NULL, -- 'user' or 'group' - principal_id TEXT NOT NULL, - PRIMARY KEY (policy_id, principal_type, principal_id), - FOREIGN KEY (policy_id) REFERENCES policies(id) ON DELETE CASCADE -); -CREATE INDEX IF NOT EXISTS idx_policy_bindings_principal ON policy_bindings(principal_type, principal_id); -` - -// Migration V6: Extend templates table for hosted template management -const migrationV6 = ` --- Add new columns to templates table -ALTER TABLE templates ADD COLUMN display_name TEXT; -ALTER TABLE templates ADD COLUMN description TEXT; -ALTER TABLE templates ADD COLUMN content_hash TEXT; -ALTER TABLE templates ADD COLUMN scope_id TEXT; -ALTER TABLE templates ADD COLUMN storage_bucket TEXT; -ALTER TABLE templates ADD COLUMN storage_path TEXT; -ALTER TABLE templates ADD COLUMN files TEXT; -ALTER TABLE templates ADD COLUMN base_template TEXT; -ALTER TABLE templates ADD COLUMN locked INTEGER NOT NULL DEFAULT 0; -ALTER TABLE templates ADD COLUMN status TEXT NOT NULL DEFAULT 'active'; -ALTER TABLE templates ADD COLUMN created_by TEXT; -ALTER TABLE templates ADD COLUMN updated_by TEXT; - --- Add indexes for new columns -CREATE INDEX IF NOT EXISTS idx_templates_status ON templates(status); -CREATE INDEX IF NOT EXISTS idx_templates_content_hash ON templates(content_hash); -CREATE INDEX IF NOT EXISTS idx_templates_scope_id ON templates(scope, scope_id); -` - -const migrationV7 = ` --- Add API keys table -CREATE TABLE IF NOT EXISTS api_keys ( - id TEXT PRIMARY KEY, - user_id TEXT NOT NULL, - name TEXT NOT NULL, - prefix TEXT NOT NULL, - key_hash TEXT NOT NULL UNIQUE, - scopes TEXT, - revoked INTEGER NOT NULL DEFAULT 0, - expires_at TIMESTAMP, - last_used TIMESTAMP, - created_at TIMESTAMP NOT NULL, - FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE -); - --- Add indexes for API keys -CREATE INDEX IF NOT EXISTS idx_api_keys_user_id ON api_keys(user_id); -CREATE INDEX IF NOT EXISTS idx_api_keys_key_hash ON api_keys(key_hash); -CREATE INDEX IF NOT EXISTS idx_api_keys_prefix ON api_keys(prefix); -` - -const migrationV8 = ` --- Add message column to agents table -ALTER TABLE agents ADD COLUMN message TEXT; -` - -// Migration V9: Broker secrets and join tokens for Runtime Broker authentication -const migrationV9 = ` --- Broker secrets table for HMAC-based authentication -CREATE TABLE IF NOT EXISTS broker_secrets ( - broker_id TEXT PRIMARY KEY, - secret_key BLOB NOT NULL, - algorithm TEXT NOT NULL DEFAULT 'hmac-sha256', - created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, - rotated_at TIMESTAMP, - expires_at TIMESTAMP, - status TEXT NOT NULL DEFAULT 'active', - FOREIGN KEY (broker_id) REFERENCES runtime_brokers(id) ON DELETE CASCADE -); - --- Broker join tokens table for registration bootstrap -CREATE TABLE IF NOT EXISTS broker_join_tokens ( - broker_id TEXT PRIMARY KEY, - token_hash TEXT NOT NULL UNIQUE, - expires_at TIMESTAMP NOT NULL, - created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, - created_by TEXT NOT NULL, - FOREIGN KEY (broker_id) REFERENCES runtime_brokers(id) ON DELETE CASCADE -); -CREATE INDEX IF NOT EXISTS idx_broker_join_tokens_hash ON broker_join_tokens(token_hash); -CREATE INDEX IF NOT EXISTS idx_broker_join_tokens_expires ON broker_join_tokens(expires_at); -` - -// Migration V10: Add user tracking to grove_contributors and runtime_brokers -const migrationV10 = ` --- Add linked_by and linked_at columns to grove_contributors for tracking who linked a broker -ALTER TABLE grove_contributors ADD COLUMN linked_by TEXT; -ALTER TABLE grove_contributors ADD COLUMN linked_at TIMESTAMP; - --- Add created_by column to runtime_brokers for tracking who registered the broker -ALTER TABLE runtime_brokers ADD COLUMN created_by TEXT; -` - -// Migration V11: Add auto_provide column to runtime_brokers -const migrationV11 = ` --- Add auto_provide column to runtime_brokers for automatic project provider registration -ALTER TABLE runtime_brokers ADD COLUMN auto_provide INTEGER NOT NULL DEFAULT 0; -` - -// Migration V12: Add injection_mode and secret columns to env_vars -const migrationV12 = ` -ALTER TABLE env_vars ADD COLUMN injection_mode TEXT NOT NULL DEFAULT 'as_needed'; -ALTER TABLE env_vars ADD COLUMN secret INTEGER NOT NULL DEFAULT 0; -` - -const migrationV13 = ` -ALTER TABLE secrets ADD COLUMN secret_type TEXT NOT NULL DEFAULT 'environment'; -ALTER TABLE secrets ADD COLUMN target TEXT; -` - -const migrationV14 = ` -ALTER TABLE secrets ADD COLUMN secret_ref TEXT; -` - -const migrationV15 = ` -UPDATE agents SET status = session_status WHERE session_status IS NOT NULL AND session_status != ''; -ALTER TABLE agents DROP COLUMN session_status; -` - -// Migration V16: Add harness_configs table -const migrationV16 = ` -CREATE TABLE IF NOT EXISTS harness_configs ( - id TEXT PRIMARY KEY, - name TEXT NOT NULL, - slug TEXT NOT NULL, - display_name TEXT, - description TEXT, - harness TEXT NOT NULL, - config TEXT, - content_hash TEXT, - scope TEXT NOT NULL DEFAULT 'global', - scope_id TEXT, - storage_uri TEXT, - storage_bucket TEXT, - storage_path TEXT, - files TEXT, - locked INTEGER NOT NULL DEFAULT 0, - status TEXT NOT NULL DEFAULT 'active', - owner_id TEXT, - created_by TEXT, - updated_by TEXT, - visibility TEXT NOT NULL DEFAULT 'private', - created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, - updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP -); -CREATE INDEX IF NOT EXISTS idx_harness_configs_slug_scope ON harness_configs(slug, scope); -CREATE INDEX IF NOT EXISTS idx_harness_configs_harness ON harness_configs(harness); -CREATE INDEX IF NOT EXISTS idx_harness_configs_status ON harness_configs(status); -CREATE INDEX IF NOT EXISTS idx_harness_configs_content_hash ON harness_configs(content_hash); -CREATE INDEX IF NOT EXISTS idx_harness_configs_scope_id ON harness_configs(scope, scope_id); -` - -// Migration V17: Add deleted_at column to agents for soft-delete support -const migrationV17 = ` -ALTER TABLE agents ADD COLUMN deleted_at TIMESTAMP; -CREATE INDEX IF NOT EXISTS idx_agents_deleted ON agents(status, deleted_at) WHERE status = 'deleted'; -` - -// Migration V18: Notification subscriptions and notifications tables -const migrationV18 = ` -CREATE TABLE IF NOT EXISTS notification_subscriptions ( - id TEXT PRIMARY KEY, - agent_id TEXT NOT NULL, - subscriber_type TEXT NOT NULL DEFAULT 'agent', - subscriber_id TEXT NOT NULL, - grove_id TEXT NOT NULL, - trigger_statuses TEXT NOT NULL, - created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, - created_by TEXT NOT NULL, - FOREIGN KEY (agent_id) REFERENCES agents(id) ON DELETE CASCADE -); -CREATE INDEX IF NOT EXISTS idx_notification_subs_agent ON notification_subscriptions(agent_id); -CREATE INDEX IF NOT EXISTS idx_notification_subs_project ON notification_subscriptions(grove_id); - -CREATE TABLE IF NOT EXISTS notifications ( - id TEXT PRIMARY KEY, - subscription_id TEXT NOT NULL, - agent_id TEXT NOT NULL, - grove_id TEXT NOT NULL, - subscriber_type TEXT NOT NULL, - subscriber_id TEXT NOT NULL, - status TEXT NOT NULL, - message TEXT NOT NULL, - dispatched INTEGER NOT NULL DEFAULT 0, - acknowledged INTEGER NOT NULL DEFAULT 0, - created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, - FOREIGN KEY (subscription_id) REFERENCES notification_subscriptions(id) ON DELETE CASCADE -); -CREATE INDEX IF NOT EXISTS idx_notifications_subscriber ON notifications(subscriber_type, subscriber_id); -CREATE INDEX IF NOT EXISTS idx_notifications_project ON notifications(grove_id); -` - -const migrationV19 = ` -CREATE TABLE IF NOT EXISTS scheduled_events ( - id TEXT PRIMARY KEY, - grove_id TEXT NOT NULL, - event_type TEXT NOT NULL, - fire_at TIMESTAMP NOT NULL, - payload TEXT NOT NULL, - status TEXT NOT NULL DEFAULT 'pending', - created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, - created_by TEXT, - fired_at TIMESTAMP, - error TEXT, - - FOREIGN KEY (grove_id) REFERENCES groves(id) ON DELETE CASCADE -); -CREATE INDEX IF NOT EXISTS idx_scheduled_events_status ON scheduled_events(status); -CREATE INDEX IF NOT EXISTS idx_scheduled_events_fire_at ON scheduled_events(fire_at) WHERE status = 'pending'; -CREATE INDEX IF NOT EXISTS idx_scheduled_events_project ON scheduled_events(grove_id); -` - -const migrationV20 = ` -ALTER TABLE agents ADD COLUMN phase TEXT NOT NULL DEFAULT 'created'; -ALTER TABLE agents ADD COLUMN activity TEXT DEFAULT ''; -ALTER TABLE agents ADD COLUMN tool_name TEXT DEFAULT ''; - --- Backfill phase/activity from existing status values -UPDATE agents SET phase = 'created' WHERE status IN ('created', 'pending'); -UPDATE agents SET phase = 'provisioning' WHERE status = 'provisioning'; -UPDATE agents SET phase = 'cloning' WHERE status = 'cloning'; -UPDATE agents SET phase = 'running', activity = 'idle' WHERE status = 'running'; -UPDATE agents SET phase = 'stopped' WHERE status = 'stopped'; -UPDATE agents SET phase = 'error' WHERE status = 'error'; -UPDATE agents SET phase = 'running', activity = 'thinking' WHERE status = 'busy'; -UPDATE agents SET phase = 'running', activity = 'idle' WHERE status = 'idle'; -UPDATE agents SET phase = 'running', activity = 'waiting_for_input' WHERE status = 'waiting_for_input'; -UPDATE agents SET phase = 'running', activity = 'completed' WHERE status = 'completed'; -UPDATE agents SET phase = 'running', activity = 'limits_exceeded' WHERE status = 'limits_exceeded'; -UPDATE agents SET phase = 'stopped' WHERE status IN ('deleted', 'restored'); -UPDATE agents SET phase = 'running', activity = 'offline' WHERE status = 'undetermined'; - -CREATE INDEX IF NOT EXISTS idx_agents_phase ON agents(phase); -` - -// Migration V21: Remove legacy status column from agents table. -// Phase 6 of the agent state refactor — the status column is superseded by -// the phase/activity columns added in V20. -const migrationV21 = ` --- Backfill any remaining agents where phase was not set -UPDATE agents SET phase = status WHERE (phase = '' OR phase IS NULL) AND status IN ('created','provisioning','cloning','starting','running','stopping','stopped','error'); -UPDATE agents SET phase = 'created' WHERE (phase = '' OR phase IS NULL) AND status = 'pending'; -UPDATE agents SET phase = 'stopped' WHERE (phase = '' OR phase IS NULL) AND status = 'deleted'; - --- Backfill activity from status for running agents -UPDATE agents SET activity = status WHERE phase = 'running' AND (activity = '' OR activity IS NULL) AND status IN ('idle','waiting_for_input','completed','limits_exceeded','offline'); -UPDATE agents SET activity = 'thinking' WHERE phase = 'running' AND (activity = '' OR activity IS NULL) AND status = 'busy'; - --- Update soft-delete index: rely on deleted_at instead of status -DROP INDEX IF EXISTS idx_agents_deleted; -CREATE INDEX IF NOT EXISTS idx_agents_deleted ON agents(deleted_at) WHERE deleted_at IS NOT NULL; - --- Drop the status index before dropping the column -DROP INDEX IF EXISTS idx_agents_status; - --- Drop the status column (SQLite supports this from 3.35.0+) -ALTER TABLE agents DROP COLUMN status; -` - -// Migration V22: Rename trigger_statuses to trigger_activities in notification_subscriptions. -const migrationV22 = ` -ALTER TABLE notification_subscriptions RENAME COLUMN trigger_statuses TO trigger_activities; -` - -// Migration V23: Add injection_mode column to secrets -const migrationV23 = ` -ALTER TABLE secrets ADD COLUMN injection_mode TEXT NOT NULL DEFAULT 'as_needed'; -` - -// Migration V24: Add last_activity_event column to agents for stalled detection. -// Backfills existing agents to prevent false positives on upgrade. -const migrationV24 = ` -ALTER TABLE agents ADD COLUMN last_activity_event TIMESTAMP; -UPDATE agents SET last_activity_event = COALESCE(last_seen, updated_at, created_at); -` - -// Migration V25: Add stalled_from_activity column for stalled detection. -// Records the activity that was active when the agent was marked stalled, -// so heartbeats can distinguish "still stuck" from "genuinely recovered". -const migrationV25 = ` -ALTER TABLE agents ADD COLUMN stalled_from_activity TEXT DEFAULT ''; -` - -// Migration V26: Add limits tracking columns to agents table. -// These fields are updated by sciontool status reports from inside the container. -const migrationV26 = ` -ALTER TABLE agents ADD COLUMN current_turns INTEGER DEFAULT 0; -ALTER TABLE agents ADD COLUMN current_model_calls INTEGER DEFAULT 0; -ALTER TABLE agents ADD COLUMN started_at TIMESTAMP; -` - -const migrationV27 = ` -ALTER TABLE users ADD COLUMN last_seen TIMESTAMP; -` - -// Migration V28: Add shared_dirs column to groves table. -// Stores project-level shared directory configuration as JSON. -const migrationV28 = ` -ALTER TABLE groves ADD COLUMN shared_dirs TEXT DEFAULT ''; -` - -// Migration V29: Add group_type and grove_id columns to groups table. -// These enable filtering groups by type and project association. -const migrationV29 = ` -ALTER TABLE groups ADD COLUMN group_type TEXT NOT NULL DEFAULT 'explicit'; -ALTER TABLE groups ADD COLUMN grove_id TEXT DEFAULT ''; -CREATE INDEX IF NOT EXISTS idx_groups_project ON groups(grove_id); -` - -// Migration V30: Create gcp_service_accounts table for GCP identity management. -const migrationV30 = ` -CREATE TABLE IF NOT EXISTS gcp_service_accounts ( - id TEXT PRIMARY KEY, - scope TEXT NOT NULL, - scope_id TEXT NOT NULL, - email TEXT NOT NULL, - grove_id TEXT NOT NULL, - display_name TEXT NOT NULL DEFAULT '', - default_scopes TEXT NOT NULL DEFAULT '', - verified INTEGER NOT NULL DEFAULT 0, - verified_at TIMESTAMP, - created_by TEXT NOT NULL DEFAULT '', - created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, - UNIQUE(email, scope, scope_id) -); -CREATE INDEX IF NOT EXISTS idx_gcp_sa_scope ON gcp_service_accounts(scope, scope_id); -` - -// Migration V31: Add scope column to notification_subscriptions and make agent_id nullable. -// Enables project-scoped subscriptions (watch all agents in a project) in addition to -// agent-scoped subscriptions. Adds unique constraint for deduplication. -const migrationV31 = ` --- SQLite doesn't support ALTER COLUMN, so we recreate the table. -CREATE TABLE notification_subscriptions_new ( - id TEXT PRIMARY KEY, - scope TEXT NOT NULL DEFAULT 'agent', - agent_id TEXT, - subscriber_type TEXT NOT NULL DEFAULT 'agent', - subscriber_id TEXT NOT NULL, - grove_id TEXT NOT NULL, - trigger_activities TEXT NOT NULL, - created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, - created_by TEXT NOT NULL, - FOREIGN KEY (agent_id) REFERENCES agents(id) ON DELETE CASCADE -); - --- Copy existing data (all existing subscriptions are agent-scoped) -INSERT INTO notification_subscriptions_new - (id, scope, agent_id, subscriber_type, subscriber_id, grove_id, trigger_activities, created_at, created_by) -SELECT id, 'agent', agent_id, subscriber_type, subscriber_id, grove_id, trigger_activities, created_at, created_by -FROM notification_subscriptions; - -DROP TABLE notification_subscriptions; -ALTER TABLE notification_subscriptions_new RENAME TO notification_subscriptions; - --- Recreate indexes -CREATE INDEX IF NOT EXISTS idx_notification_subs_agent ON notification_subscriptions(agent_id); -CREATE INDEX IF NOT EXISTS idx_notification_subs_project ON notification_subscriptions(grove_id); -CREATE INDEX IF NOT EXISTS idx_notification_subs_subscriber ON notification_subscriptions(subscriber_type, subscriber_id); - --- Unique constraint: one subscription per (scope, target, subscriber, project) -CREATE UNIQUE INDEX IF NOT EXISTS idx_notification_subs_unique - ON notification_subscriptions(scope, COALESCE(agent_id, ''), subscriber_type, subscriber_id, grove_id); -` - -// Migration V32: Recurring schedules table and schedule_id FK on scheduled_events. -const migrationV32 = ` -CREATE TABLE IF NOT EXISTS schedules ( - id TEXT PRIMARY KEY, - grove_id TEXT NOT NULL, - name TEXT NOT NULL, - cron_expr TEXT NOT NULL, - event_type TEXT NOT NULL, - payload TEXT NOT NULL DEFAULT '{}', - status TEXT NOT NULL DEFAULT 'active', - next_run_at TIMESTAMP, - last_run_at TIMESTAMP, - last_run_status TEXT, - last_run_error TEXT, - run_count INTEGER NOT NULL DEFAULT 0, - error_count INTEGER NOT NULL DEFAULT 0, - created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, - created_by TEXT, - updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, - FOREIGN KEY (grove_id) REFERENCES groves(id) ON DELETE CASCADE, - UNIQUE(grove_id, name) -); -CREATE INDEX IF NOT EXISTS idx_schedules_project ON schedules(grove_id); -CREATE INDEX IF NOT EXISTS idx_schedules_next_run ON schedules(next_run_at) WHERE status = 'active'; - -ALTER TABLE scheduled_events ADD COLUMN schedule_id TEXT DEFAULT ''; -` - -// Migration V33: Subscription templates table. -const migrationV33 = ` -CREATE TABLE IF NOT EXISTS subscription_templates ( - id TEXT PRIMARY KEY, - name TEXT NOT NULL, - scope TEXT NOT NULL DEFAULT 'project', - trigger_activities TEXT NOT NULL, - grove_id TEXT NOT NULL DEFAULT '', - created_by TEXT NOT NULL, - UNIQUE(grove_id, name) -); -CREATE INDEX IF NOT EXISTS idx_sub_templates_project ON subscription_templates(grove_id); -` - -// Migration V34: User access tokens table (replaces api_keys). -const migrationV34 = ` -CREATE TABLE IF NOT EXISTS user_access_tokens ( - id TEXT PRIMARY KEY, - user_id TEXT NOT NULL, - name TEXT NOT NULL, - prefix TEXT NOT NULL, - key_hash TEXT NOT NULL UNIQUE, - grove_id TEXT NOT NULL, - scopes TEXT NOT NULL, - revoked INTEGER NOT NULL DEFAULT 0, - expires_at TIMESTAMP, - last_used TIMESTAMP, - created_at TIMESTAMP NOT NULL, - FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE, - FOREIGN KEY (grove_id) REFERENCES groves(id) ON DELETE CASCADE -); -CREATE INDEX IF NOT EXISTS idx_uat_user_id ON user_access_tokens(user_id); -CREATE INDEX IF NOT EXISTS idx_uat_key_hash ON user_access_tokens(key_hash); -` - -// Migration V35: GitHub App installations and project GitHub App fields. -const migrationV35 = ` -CREATE TABLE IF NOT EXISTS github_installations ( - installation_id INTEGER PRIMARY KEY, - account_login TEXT NOT NULL, - account_type TEXT NOT NULL DEFAULT 'Organization', - app_id INTEGER NOT NULL, - repositories TEXT NOT NULL DEFAULT '[]', - status TEXT NOT NULL DEFAULT 'active', - created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, - updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP -); -CREATE INDEX IF NOT EXISTS idx_github_installations_account ON github_installations(account_login); -CREATE INDEX IF NOT EXISTS idx_github_installations_status ON github_installations(status); - -ALTER TABLE groves ADD COLUMN github_installation_id INTEGER; -ALTER TABLE groves ADD COLUMN github_permissions TEXT; -ALTER TABLE groves ADD COLUMN github_app_status TEXT; -` - -// Migration V36: Git identity configuration for commit attribution. -const migrationV36 = ` -ALTER TABLE groves ADD COLUMN git_identity TEXT; -` - -// Migration V37: Add ancestry column for transitive access control. -const migrationV37 = ` -ALTER TABLE agents ADD COLUMN ancestry TEXT; -` - -// Migration V38: Backfill ancestry for existing agents from created_by. -const migrationV38 = ` -UPDATE agents SET ancestry = json_array(created_by) -WHERE created_by IS NOT NULL AND created_by != '' AND ancestry IS NULL; -` - -// Migration V39: Messages table for bidirectional human-agent messaging. -const migrationV39 = ` -CREATE TABLE IF NOT EXISTS messages ( - id TEXT PRIMARY KEY, - grove_id TEXT NOT NULL, - sender TEXT NOT NULL, - sender_id TEXT NOT NULL DEFAULT '', - recipient TEXT NOT NULL, - recipient_id TEXT NOT NULL DEFAULT '', - msg TEXT NOT NULL, - type TEXT NOT NULL DEFAULT 'instruction', - urgent INTEGER NOT NULL DEFAULT 0, - broadcasted INTEGER NOT NULL DEFAULT 0, - read INTEGER NOT NULL DEFAULT 0, - agent_id TEXT NOT NULL DEFAULT '', - created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP -); - -CREATE INDEX IF NOT EXISTS idx_messages_project ON messages(grove_id); -CREATE INDEX IF NOT EXISTS idx_messages_recipient ON messages(recipient_id, read); -CREATE INDEX IF NOT EXISTS idx_messages_agent ON messages(agent_id); -CREATE INDEX IF NOT EXISTS idx_messages_sender ON messages(sender_id); -CREATE INDEX IF NOT EXISTS idx_messages_created ON messages(created_at DESC); -` - -// Migration V40: Allow multiple groves per git remote (drop UNIQUE on git_remote), -// and enforce slug uniqueness (add UNIQUE on slug). Requires table recreation -// because SQLite does not support ALTER TABLE DROP CONSTRAINT. -// -// IMPORTANT: This migration requires foreign_keys=OFF around the DROP TABLE. -// SQLite ignores PRAGMA changes inside transactions, so the migration runner -// handles this via the foreignKeysOffMigrations set. The PRAGMA statements are -// intentionally NOT included in the SQL string. -const migrationV40 = ` -CREATE TABLE IF NOT EXISTS groves_new ( - id TEXT PRIMARY KEY, - name TEXT NOT NULL, - slug TEXT NOT NULL UNIQUE, - git_remote TEXT, - labels TEXT, - annotations TEXT, - created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, - updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, - created_by TEXT, - owner_id TEXT, - visibility TEXT NOT NULL DEFAULT 'private', - default_runtime_broker_id TEXT REFERENCES runtime_brokers(id) ON DELETE SET NULL, - shared_dirs TEXT, - github_installation_id INTEGER REFERENCES github_installations(installation_id), - github_permissions TEXT, - github_app_status TEXT, - git_identity TEXT -); - -INSERT OR IGNORE INTO groves_new SELECT - id, name, slug, git_remote, labels, annotations, - created_at, updated_at, created_by, owner_id, visibility, - default_runtime_broker_id, shared_dirs, - github_installation_id, github_permissions, github_app_status, - git_identity -FROM groves; - -DROP TABLE IF EXISTS groves; -ALTER TABLE groves_new RENAME TO groves; - -CREATE INDEX IF NOT EXISTS idx_groves_slug ON groves(slug); -CREATE INDEX IF NOT EXISTS idx_groves_git_remote ON groves(git_remote); -CREATE INDEX IF NOT EXISTS idx_groves_owner ON groves(owner_id); -CREATE INDEX IF NOT EXISTS idx_groves_default_runtime_broker ON groves(default_runtime_broker_id); -` - -// Migration V41: Maintenance operations tables for the admin maintenance panel. -// Tracks one-time migrations and repeatable operations with execution history. -const migrationV41 = ` -CREATE TABLE IF NOT EXISTS maintenance_operations ( - id TEXT PRIMARY KEY, - key TEXT NOT NULL UNIQUE, - title TEXT NOT NULL, - description TEXT NOT NULL DEFAULT '', - category TEXT NOT NULL, - status TEXT NOT NULL DEFAULT 'pending', - created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, - started_at TIMESTAMP, - completed_at TIMESTAMP, - started_by TEXT, - result TEXT, - metadata TEXT NOT NULL DEFAULT '{}' -); -CREATE INDEX IF NOT EXISTS idx_maintenance_ops_category ON maintenance_operations(category); -CREATE INDEX IF NOT EXISTS idx_maintenance_ops_status ON maintenance_operations(status); - -CREATE TABLE IF NOT EXISTS maintenance_operation_runs ( - id TEXT PRIMARY KEY, - operation_key TEXT NOT NULL, - status TEXT NOT NULL DEFAULT 'running', - started_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, - completed_at TIMESTAMP, - started_by TEXT, - result TEXT, - log TEXT NOT NULL DEFAULT '', - FOREIGN KEY (operation_key) REFERENCES maintenance_operations(key) -); -CREATE INDEX IF NOT EXISTS idx_maintenance_runs_key ON maintenance_operation_runs(operation_key); -CREATE INDEX IF NOT EXISTS idx_maintenance_runs_started ON maintenance_operation_runs(started_at DESC); - --- Seed: one-time migrations -INSERT INTO maintenance_operations (id, key, title, description, category, status) -VALUES ( - lower(hex(randomblob(4)) || '-' || hex(randomblob(2)) || '-4' || substr(hex(randomblob(2)),2) || '-' || substr('89ab', abs(random()) % 4 + 1, 1) || substr(hex(randomblob(2)),2) || '-' || hex(randomblob(6))), - 'secret-hub-id-migration', - 'Secret Hub ID Namespace Migration', - 'Migrates hub-scoped secrets from the legacy fixed "hub" scope ID to the per-instance hub ID. Required when upgrading a hub that was created before the hub ID namespacing feature. Only needed for GCP Secret Manager backend.', - 'migration', - 'pending' -); - --- Seed: repeatable operations -INSERT INTO maintenance_operations (id, key, title, description, category, status) -VALUES ( - lower(hex(randomblob(4)) || '-' || hex(randomblob(2)) || '-4' || substr(hex(randomblob(2)),2) || '-' || substr('89ab', abs(random()) % 4 + 1, 1) || substr(hex(randomblob(2)),2) || '-' || hex(randomblob(6))), - 'pull-images', - 'Pull Container Images', - 'Pulls the latest container images for all configured harnesses from the image registry.', - 'operation', - 'pending' -); - -INSERT INTO maintenance_operations (id, key, title, description, category, status) -VALUES ( - lower(hex(randomblob(4)) || '-' || hex(randomblob(2)) || '-4' || substr(hex(randomblob(2)),2) || '-' || substr('89ab', abs(random()) % 4 + 1, 1) || substr(hex(randomblob(2)),2) || '-' || hex(randomblob(6))), - 'rebuild-server', - 'Rebuild Server from Git', - 'Pulls latest code from the repository, rebuilds the server binary and web assets, then restarts the hub service. Equivalent to the fast-deploy mode of gce-start-hub.sh.', - 'operation', - 'pending' -); - -INSERT INTO maintenance_operations (id, key, title, description, category, status) -VALUES ( - lower(hex(randomblob(4)) || '-' || hex(randomblob(2)) || '-4' || substr(hex(randomblob(2)),2) || '-' || substr('89ab', abs(random()) % 4 + 1, 1) || substr(hex(randomblob(2)),2) || '-' || hex(randomblob(6))), - 'rebuild-web', - 'Rebuild Web Frontend', - 'Rebuilds only the web frontend assets from source without restarting the server binary. Changes take effect on the next page load.', - 'operation', - 'pending' -); -` - -const migrationV42 = ` -CREATE TABLE IF NOT EXISTS grove_sync_state ( - grove_id TEXT NOT NULL, - broker_id TEXT NOT NULL DEFAULT '', - last_sync_time TIMESTAMP, - last_commit_sha TEXT, - file_count INTEGER NOT NULL DEFAULT 0, - total_bytes INTEGER NOT NULL DEFAULT 0, - PRIMARY KEY (grove_id, broker_id), - FOREIGN KEY (grove_id) REFERENCES groves(id) ON DELETE CASCADE -); -CREATE INDEX IF NOT EXISTS idx_grove_sync_state_project ON grove_sync_state(grove_id); -` - -// migrationV43 fixes pre-existing signing key secrets that were stored with -// the default secret_type ('environment' or ”) instead of 'internal'. Without -// this, stale rows created before the fix would still be resolved and injected -// into agent containers. -const migrationV43 = ` -UPDATE secrets SET secret_type = 'internal' -WHERE key IN ('agent_signing_key', 'user_signing_key') - AND scope = 'hub' - AND secret_type != 'internal'; -` - -// Migration V44: Add managed and managed_by columns to gcp_service_accounts table -// for hub-minted service accounts. -const migrationV44 = ` -ALTER TABLE gcp_service_accounts ADD COLUMN managed INTEGER NOT NULL DEFAULT 0; -ALTER TABLE gcp_service_accounts ADD COLUMN managed_by TEXT NOT NULL DEFAULT ''; -` - -// Migration V45: Add allow_progeny column to secrets table -const migrationV45 = ` -ALTER TABLE secrets ADD COLUMN allow_progeny INTEGER NOT NULL DEFAULT 0; -` - -const migrationV46 = ` -ALTER TABLE templates ADD COLUMN default_harness_config TEXT; -` - -const migrationV47 = ` -INSERT INTO maintenance_operations (id, key, title, description, category, status) -VALUES ( - lower(hex(randomblob(4)) || '-' || hex(randomblob(2)) || '-4' || substr(hex(randomblob(2)),2) || '-' || substr('89ab', abs(random()) % 4 + 1, 1) || substr(hex(randomblob(2)),2) || '-' || hex(randomblob(6))), - 'rebuild-container-binaries', - 'Rebuild Container Binaries', - 'Rebuilds scion and sciontool binaries for Linux containers (make container-binaries). Only available when SCION_DEV_BINARIES is set. Binaries are written to .build/container/ in the source checkout.', - 'operation', - 'pending' -); -` - -const migrationV48 = ` -CREATE TABLE IF NOT EXISTS allow_list ( - id TEXT PRIMARY KEY, - email TEXT NOT NULL UNIQUE COLLATE NOCASE, - note TEXT NOT NULL DEFAULT '', - added_by TEXT NOT NULL, - invite_id TEXT NOT NULL DEFAULT '', - created DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP -); -` - -const migrationV49 = ` -CREATE TABLE IF NOT EXISTS invite_codes ( - id TEXT PRIMARY KEY, - code_hash TEXT NOT NULL UNIQUE, - code_prefix TEXT NOT NULL, - max_uses INTEGER NOT NULL DEFAULT 1, - use_count INTEGER NOT NULL DEFAULT 0, - expires_at DATETIME NOT NULL, - revoked INTEGER NOT NULL DEFAULT 0, - created_by TEXT NOT NULL, - note TEXT NOT NULL DEFAULT '', - created DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP -); -CREATE INDEX IF NOT EXISTS idx_invite_codes_expires ON invite_codes(expires_at); -` - -// migrateV50 renames 'grove' entities to 'project' idempotently. -// This is Phase 4 of the grove-to-project rename strategy. -// Each rename operation checks whether the old name still exists before -// attempting the rename, so the migration can be re-run safely on databases -// that partially applied an earlier (non-idempotent) version of V50. -func migrateV50(ctx context.Context, tx *sql.Tx) error { - // 1. Rename Tables (check before renaming) - tableRenames := [][2]string{ - {"groves", "projects"}, - {"grove_contributors", "project_contributors"}, - {"grove_sync_state", "project_sync_state"}, - } - for _, r := range tableRenames { - exists, err := tableExists(ctx, tx, r[0]) - if err != nil { - return fmt.Errorf("checking table %s: %w", r[0], err) - } - if exists { - if _, err := tx.ExecContext(ctx, fmt.Sprintf("ALTER TABLE %s RENAME TO %s", r[0], r[1])); err != nil { - return fmt.Errorf("renaming table %s to %s: %w", r[0], r[1], err) - } - } - } - - // 2. Rename Columns (check before renaming) - // After step 1, tables are at their new names. If step 1 was already - // applied in a prior run, the tables are also at their new names. - columnRenames := [][3]string{ - {"project_contributors", "grove_id", "project_id"}, - {"project_sync_state", "grove_id", "project_id"}, - {"agents", "grove_id", "project_id"}, - {"templates", "grove_id", "project_id"}, - {"notification_subscriptions", "grove_id", "project_id"}, - {"notifications", "grove_id", "project_id"}, - {"scheduled_events", "grove_id", "project_id"}, - {"schedules", "grove_id", "project_id"}, - {"subscription_templates", "grove_id", "project_id"}, - {"user_access_tokens", "grove_id", "project_id"}, - {"messages", "grove_id", "project_id"}, - {"groups", "grove_id", "project_id"}, - {"gcp_service_accounts", "grove_id", "project_id"}, - } - for _, r := range columnRenames { - exists, err := columnExists(ctx, tx, r[0], r[1]) - if err != nil { - return fmt.Errorf("checking column %s.%s: %w", r[0], r[1], err) - } - if exists { - if _, err := tx.ExecContext(ctx, fmt.Sprintf("ALTER TABLE %s RENAME COLUMN %s TO %s", r[0], r[1], r[2])); err != nil { - return fmt.Errorf("renaming column %s.%s to %s: %w", r[0], r[1], r[2], err) - } - } - } - - // 3. Update Data Values (already idempotent — UPDATE WHERE is a no-op - // when the old value no longer exists) - dataUpdates := ` -UPDATE env_vars SET scope = 'project' WHERE scope = 'grove'; -UPDATE secrets SET scope = 'project' WHERE scope = 'grove'; -UPDATE policies SET scope_type = 'project' WHERE scope_type = 'grove'; -UPDATE gcp_service_accounts SET scope = 'project' WHERE scope = 'grove'; -UPDATE groups SET group_type = 'project_agents' WHERE group_type = 'grove_agents'; -UPDATE notification_subscriptions SET scope = 'project' WHERE scope = 'grove'; -UPDATE subscription_templates SET scope = 'project' WHERE scope = 'grove'; -UPDATE templates SET scope = 'project' WHERE scope = 'grove'; -UPDATE harness_configs SET scope = 'project' WHERE scope = 'grove'; -` - if _, err := tx.ExecContext(ctx, dataUpdates); err != nil { - return fmt.Errorf("updating data values: %w", err) - } - - // 4. Rename/Recreate Indexes (already idempotent — DROP IF EXISTS / CREATE IF NOT EXISTS) - indexSQL := ` -DROP INDEX IF EXISTS idx_groves_slug; -CREATE UNIQUE INDEX IF NOT EXISTS idx_projects_slug ON projects(slug); -DROP INDEX IF EXISTS idx_groves_git_remote; -CREATE INDEX IF NOT EXISTS idx_projects_git_remote ON projects(git_remote); -DROP INDEX IF EXISTS idx_groves_owner; -CREATE INDEX IF NOT EXISTS idx_projects_owner ON projects(owner_id); -DROP INDEX IF EXISTS idx_groves_default_runtime_broker; -CREATE INDEX IF NOT EXISTS idx_projects_default_runtime_broker ON projects(default_runtime_broker_id); - -DROP INDEX IF EXISTS idx_agents_grove_slug; -DROP INDEX IF EXISTS idx_agents_project_slug; -CREATE UNIQUE INDEX IF NOT EXISTS idx_agents_project_slug ON agents(agent_id, project_id); -DROP INDEX IF EXISTS idx_agents_grove; -CREATE INDEX IF NOT EXISTS idx_agents_project ON agents(project_id); - -DROP INDEX IF EXISTS idx_grove_sync_state_grove; -CREATE INDEX IF NOT EXISTS idx_project_sync_state_project ON project_sync_state(project_id); - -DROP INDEX IF EXISTS idx_notification_subs_grove; -CREATE INDEX IF NOT EXISTS idx_notification_subs_project ON notification_subscriptions(project_id); - -DROP INDEX IF EXISTS idx_notifications_grove; -CREATE INDEX IF NOT EXISTS idx_notifications_project ON notifications(project_id); - -DROP INDEX IF EXISTS idx_scheduled_events_grove; -CREATE INDEX IF NOT EXISTS idx_scheduled_events_project ON scheduled_events(project_id); - -DROP INDEX IF EXISTS idx_schedules_grove; -CREATE INDEX IF NOT EXISTS idx_schedules_project ON schedules(project_id); - -DROP INDEX IF EXISTS idx_sub_templates_grove; -CREATE INDEX IF NOT EXISTS idx_sub_templates_project ON subscription_templates(project_id); - -DROP INDEX IF EXISTS idx_messages_grove; -CREATE INDEX IF NOT EXISTS idx_messages_project ON messages(project_id); - -DROP INDEX IF EXISTS idx_groups_grove; -CREATE INDEX IF NOT EXISTS idx_groups_project ON groups(project_id); - -DROP INDEX IF EXISTS idx_gcp_sa_grove; -CREATE INDEX IF NOT EXISTS idx_gcp_sa_project ON gcp_service_accounts(project_id); -` - if _, err := tx.ExecContext(ctx, indexSQL); err != nil { - return fmt.Errorf("updating indexes: %w", err) - } - - return nil -} - -// migrationV51 adds group_id to messages for correlating set[] deliveries. -const migrationV51 = ` -ALTER TABLE messages ADD COLUMN group_id TEXT NOT NULL DEFAULT ''; -` - -// migrationV52 renames the idle activity to working for clearer agent state reporting. -const migrationV52 = ` -UPDATE agents SET activity = 'working' WHERE activity = 'idle'; -UPDATE agents SET stalled_from_activity = 'working' WHERE stalled_from_activity = 'idle'; -` - -// migrationV53 adds an index on (created, id) to allow_list for efficient keyset pagination. -// It also ensures the allow_list table exists, because databases created before V48/V49 were -// inserted into the migration sequence already have version 48 recorded with different content -// (the grove-to-project rename that is now V50). On those databases V48 is skipped, so the -// allow_list table was never created. -const migrationV53 = ` -CREATE TABLE IF NOT EXISTS allow_list ( - id TEXT PRIMARY KEY, - email TEXT NOT NULL UNIQUE COLLATE NOCASE, - note TEXT NOT NULL DEFAULT '', - added_by TEXT NOT NULL, - invite_id TEXT NOT NULL DEFAULT '', - created DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP -); -CREATE TABLE IF NOT EXISTS invite_codes ( - id TEXT PRIMARY KEY, - code_hash TEXT NOT NULL UNIQUE, - code_prefix TEXT NOT NULL, - max_uses INTEGER NOT NULL DEFAULT 1, - use_count INTEGER NOT NULL DEFAULT 0, - expires_at DATETIME NOT NULL, - revoked INTEGER NOT NULL DEFAULT 0, - created_by TEXT NOT NULL, - note TEXT NOT NULL DEFAULT '', - created DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP -); -CREATE INDEX IF NOT EXISTS idx_invite_codes_expires ON invite_codes(expires_at); -CREATE INDEX IF NOT EXISTS idx_allow_list_created_id ON allow_list (created DESC, id DESC); -` - -// tableExists checks whether a table with the given name exists in the database. -func tableExists(ctx context.Context, tx *sql.Tx, tableName string) (bool, error) { - var name string - err := tx.QueryRowContext(ctx, - "SELECT name FROM sqlite_master WHERE type='table' AND name=?", tableName, - ).Scan(&name) - if errors.Is(err, sql.ErrNoRows) { - return false, nil - } - if err != nil { - return false, err - } - return true, nil -} - -// columnExists checks whether a column with the given name exists in the specified table. -func columnExists(ctx context.Context, tx *sql.Tx, tableName, columnName string) (bool, error) { - rows, err := tx.QueryContext(ctx, fmt.Sprintf("PRAGMA table_info(%s)", tableName)) - if err != nil { - return false, err - } - defer rows.Close() - - for rows.Next() { - var cid int - var name, ctype string - var notnull int - var dfltValue sql.NullString - var pk int - if err := rows.Scan(&cid, &name, &ctype, ¬null, &dfltValue, &pk); err != nil { - return false, err - } - if name == columnName { - return true, nil - } - } - return false, rows.Err() -} - -// Helper functions for JSON marshaling/unmarshaling -func marshalJSON(v interface{}) string { - if v == nil { - return "" - } - data, err := json.Marshal(v) - if err != nil { - return "" - } - return string(data) -} - -func unmarshalJSON[T any](data string, v *T) { - if data == "" { - return - } - json.Unmarshal([]byte(data), v) -} - -// nullableString returns a sql.NullString for database insertion. -// Empty strings become NULL, which is important for UNIQUE and FK constraints. -func nullableString(s string) sql.NullString { - if s == "" { - return sql.NullString{Valid: false} - } - return sql.NullString{String: s, Valid: true} -} - -// nullableTime returns a sql.NullTime for database insertion. -// Zero time values become NULL. -func nullableTime(t time.Time) sql.NullTime { - if t.IsZero() { - return sql.NullTime{Valid: false} - } - return sql.NullTime{Time: t, Valid: true} -} - -// nullableInt64 returns a sql.NullInt64 for database insertion. -// Nil pointers become NULL. -func nullableInt64(v *int64) sql.NullInt64 { - if v == nil { - return sql.NullInt64{Valid: false} - } - return sql.NullInt64{Int64: *v, Valid: true} -} - -// marshalJSONPtr marshals a pointer value to JSON string, returning empty string for nil pointers. -// Unlike marshalJSON, this correctly detects nil typed pointers. -func marshalJSONPtr[T any](v *T) string { - if v == nil { - return "" - } - data, err := json.Marshal(v) - if err != nil { - return "" - } - return string(data) -} - -// ============================================================================ -// Agent Operations -// ============================================================================ - -func (s *SQLiteStore) CreateAgent(ctx context.Context, agent *store.Agent) error { - now := time.Now() - agent.Created = now - agent.Updated = now - agent.StateVersion = 1 - - _, err := s.db.ExecContext(ctx, ` - INSERT INTO agents ( - id, agent_id, name, template, project_id, - labels, annotations, - phase, activity, tool_name, - connection_state, container_status, runtime_state, - stalled_from_activity, - image, detached, runtime, runtime_broker_id, web_pty_enabled, task_summary, message, - applied_config, - created_at, updated_at, last_seen, last_activity_event, deleted_at, - created_by, owner_id, visibility, state_version, ancestry - ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) - `, - agent.ID, agent.Slug, agent.Name, agent.Template, agent.ProjectID, - marshalJSON(agent.Labels), marshalJSON(agent.Annotations), - agent.Phase, agent.Activity, agent.ToolName, - agent.ConnectionState, agent.ContainerStatus, agent.RuntimeState, - agent.StalledFromActivity, - agent.Image, agent.Detached, agent.Runtime, nullableString(agent.RuntimeBrokerID), agent.WebPTYEnabled, agent.TaskSummary, agent.Message, - marshalJSON(agent.AppliedConfig), - agent.Created, agent.Updated, nullableTime(agent.LastSeen), nullableTime(agent.LastActivityEvent), nullableTime(agent.DeletedAt), - agent.CreatedBy, agent.OwnerID, agent.Visibility, agent.StateVersion, marshalJSON(agent.Ancestry), - ) - if err != nil { - if strings.Contains(err.Error(), "UNIQUE constraint failed") { - return store.ErrAlreadyExists - } - return err - } - return nil -} - -func (s *SQLiteStore) GetAgent(ctx context.Context, id string) (*store.Agent, error) { - agent := &store.Agent{} - var labels, annotations, appliedConfig string - var lastSeen, lastActivityEvent, deletedAt, startedAt sql.NullTime - var runtimeBrokerID, message, toolName, ancestry sql.NullString - - err := s.db.QueryRowContext(ctx, ` - SELECT id, agent_id, name, template, project_id, - labels, annotations, - phase, activity, tool_name, - connection_state, container_status, runtime_state, - stalled_from_activity, - current_turns, current_model_calls, - image, detached, runtime, runtime_broker_id, web_pty_enabled, task_summary, message, - applied_config, - created_at, updated_at, last_seen, last_activity_event, deleted_at, started_at, - created_by, owner_id, visibility, state_version, ancestry - FROM agents WHERE id = ? - `, id).Scan( - &agent.ID, &agent.Slug, &agent.Name, &agent.Template, &agent.ProjectID, - &labels, &annotations, - &agent.Phase, &agent.Activity, &toolName, - &agent.ConnectionState, &agent.ContainerStatus, &agent.RuntimeState, - &agent.StalledFromActivity, - &agent.CurrentTurns, &agent.CurrentModelCalls, - &agent.Image, &agent.Detached, &agent.Runtime, &runtimeBrokerID, &agent.WebPTYEnabled, &agent.TaskSummary, &message, - &appliedConfig, - &agent.Created, &agent.Updated, &lastSeen, &lastActivityEvent, &deletedAt, &startedAt, - &agent.CreatedBy, &agent.OwnerID, &agent.Visibility, &agent.StateVersion, &ancestry, - ) - if err != nil { - if errors.Is(err, sql.ErrNoRows) { - return nil, store.ErrNotFound - } - return nil, err - } - - unmarshalJSON(labels, &agent.Labels) - unmarshalJSON(annotations, &agent.Annotations) - unmarshalJSON(appliedConfig, &agent.AppliedConfig) - unmarshalJSON(ancestry.String, &agent.Ancestry) - if lastSeen.Valid { - agent.LastSeen = lastSeen.Time - } - if lastActivityEvent.Valid { - agent.LastActivityEvent = lastActivityEvent.Time - } - if deletedAt.Valid { - agent.DeletedAt = deletedAt.Time - } - if startedAt.Valid { - agent.StartedAt = startedAt.Time - } - if runtimeBrokerID.Valid { - agent.RuntimeBrokerID = runtimeBrokerID.String - } - if message.Valid { - agent.Message = message.String - } - if toolName.Valid { - agent.ToolName = toolName.String - } - - return agent, nil -} - -func (s *SQLiteStore) GetAgentBySlug(ctx context.Context, projectID, slug string) (*store.Agent, error) { - agent := &store.Agent{} - var labels, annotations, appliedConfig string - var lastSeen, lastActivityEvent, deletedAt, startedAt sql.NullTime - var runtimeBrokerID, message, toolName, ancestry sql.NullString - - err := s.db.QueryRowContext(ctx, ` - SELECT id, agent_id, name, template, project_id, - labels, annotations, - phase, activity, tool_name, - connection_state, container_status, runtime_state, - stalled_from_activity, - current_turns, current_model_calls, - image, detached, runtime, runtime_broker_id, web_pty_enabled, task_summary, message, - applied_config, - created_at, updated_at, last_seen, last_activity_event, deleted_at, started_at, - created_by, owner_id, visibility, state_version, ancestry - FROM agents WHERE project_id = ? AND agent_id = ? - `, projectID, slug).Scan( - &agent.ID, &agent.Slug, &agent.Name, &agent.Template, &agent.ProjectID, - &labels, &annotations, - &agent.Phase, &agent.Activity, &toolName, - &agent.ConnectionState, &agent.ContainerStatus, &agent.RuntimeState, - &agent.StalledFromActivity, - &agent.CurrentTurns, &agent.CurrentModelCalls, - &agent.Image, &agent.Detached, &agent.Runtime, &runtimeBrokerID, &agent.WebPTYEnabled, &agent.TaskSummary, &message, - &appliedConfig, - &agent.Created, &agent.Updated, &lastSeen, &lastActivityEvent, &deletedAt, &startedAt, - &agent.CreatedBy, &agent.OwnerID, &agent.Visibility, &agent.StateVersion, &ancestry, - ) - if err != nil { - if errors.Is(err, sql.ErrNoRows) { - return nil, store.ErrNotFound - } - return nil, err - } - - unmarshalJSON(labels, &agent.Labels) - unmarshalJSON(annotations, &agent.Annotations) - unmarshalJSON(appliedConfig, &agent.AppliedConfig) - unmarshalJSON(ancestry.String, &agent.Ancestry) - if lastSeen.Valid { - agent.LastSeen = lastSeen.Time - } - if lastActivityEvent.Valid { - agent.LastActivityEvent = lastActivityEvent.Time - } - if deletedAt.Valid { - agent.DeletedAt = deletedAt.Time - } - if startedAt.Valid { - agent.StartedAt = startedAt.Time - } - if runtimeBrokerID.Valid { - agent.RuntimeBrokerID = runtimeBrokerID.String - } - if message.Valid { - agent.Message = message.String - } - if toolName.Valid { - agent.ToolName = toolName.String - } - - return agent, nil -} - -func (s *SQLiteStore) UpdateAgent(ctx context.Context, agent *store.Agent) error { - agent.Updated = time.Now() - newVersion := agent.StateVersion + 1 - - result, err := s.db.ExecContext(ctx, ` - UPDATE agents SET - agent_id = ?, name = ?, template = ?, - labels = ?, annotations = ?, - phase = ?, activity = ?, tool_name = ?, - connection_state = ?, container_status = ?, runtime_state = ?, - stalled_from_activity = ?, - image = ?, detached = ?, runtime = ?, runtime_broker_id = ?, web_pty_enabled = ?, task_summary = ?, message = ?, - applied_config = ?, - updated_at = ?, last_seen = ?, last_activity_event = ?, deleted_at = ?, - owner_id = ?, visibility = ?, state_version = ? - WHERE id = ? AND state_version = ? - `, - agent.Slug, agent.Name, agent.Template, - marshalJSON(agent.Labels), marshalJSON(agent.Annotations), - agent.Phase, agent.Activity, agent.ToolName, - agent.ConnectionState, agent.ContainerStatus, agent.RuntimeState, - agent.StalledFromActivity, - agent.Image, agent.Detached, agent.Runtime, nullableString(agent.RuntimeBrokerID), agent.WebPTYEnabled, agent.TaskSummary, agent.Message, - marshalJSON(agent.AppliedConfig), - agent.Updated, nullableTime(agent.LastSeen), nullableTime(agent.LastActivityEvent), nullableTime(agent.DeletedAt), - agent.OwnerID, agent.Visibility, newVersion, - agent.ID, agent.StateVersion, - ) - if err != nil { - return err - } - - rowsAffected, err := result.RowsAffected() - if err != nil { - return err - } - if rowsAffected == 0 { - // Check if agent exists - var exists bool - s.db.QueryRowContext(ctx, "SELECT 1 FROM agents WHERE id = ?", agent.ID).Scan(&exists) - if !exists { - return store.ErrNotFound - } - return store.ErrVersionConflict - } - - agent.StateVersion = newVersion - return nil -} - -func (s *SQLiteStore) DeleteAgent(ctx context.Context, id string) error { - result, err := s.db.ExecContext(ctx, "DELETE FROM agents WHERE id = ?", id) - if err != nil { - return err - } - rowsAffected, err := result.RowsAffected() - if err != nil { - return err - } - if rowsAffected == 0 { - return store.ErrNotFound - } - return nil -} - -func (s *SQLiteStore) ListAgents(ctx context.Context, filter store.AgentFilter, opts store.ListOptions) (*store.ListResult[store.Agent], error) { - var conditions []string - var args []interface{} - - if len(filter.MemberOrOwnerProjectIDs) > 0 { - // Combine project_id membership with owner_id match using OR - placeholders := make([]string, len(filter.MemberOrOwnerProjectIDs)) - for i, id := range filter.MemberOrOwnerProjectIDs { - placeholders[i] = "?" - args = append(args, id) - } - orParts := []string{"project_id IN (" + strings.Join(placeholders, ",") + ")"} - if filter.OwnerID != "" { - orParts = append(orParts, "owner_id = ?") - args = append(args, filter.OwnerID) - } - conditions = append(conditions, "("+strings.Join(orParts, " OR ")+")") - } else if len(filter.MemberProjectIDs) > 0 { - placeholders := make([]string, len(filter.MemberProjectIDs)) - for i, id := range filter.MemberProjectIDs { - placeholders[i] = "?" - args = append(args, id) - } - conditions = append(conditions, "project_id IN ("+strings.Join(placeholders, ",")+")") - } else if filter.OwnerID != "" { - conditions = append(conditions, "owner_id = ?") - args = append(args, filter.OwnerID) - } - if filter.ExcludeOwnerID != "" { - conditions = append(conditions, "owner_id != ?") - args = append(args, filter.ExcludeOwnerID) - } - if filter.ProjectID != "" { - conditions = append(conditions, "project_id = ?") - args = append(args, filter.ProjectID) - } - if filter.RuntimeBrokerID != "" { - conditions = append(conditions, "runtime_broker_id = ?") - args = append(args, filter.RuntimeBrokerID) - } - if filter.Phase != "" { - conditions = append(conditions, "phase = ?") - args = append(args, filter.Phase) - } - if filter.AncestorID != "" { - conditions = append(conditions, "EXISTS (SELECT 1 FROM json_each(ancestry) WHERE json_each.value = ?)") - args = append(args, filter.AncestorID) - } - - // Exclude soft-deleted agents unless explicitly requested - if !filter.IncludeDeleted { - conditions = append(conditions, "deleted_at IS NULL") - } - - whereClause := "" - if len(conditions) > 0 { - whereClause = "WHERE " + strings.Join(conditions, " AND ") - } - - // Get total count - var totalCount int - countQuery := fmt.Sprintf("SELECT COUNT(*) FROM agents %s", whereClause) - if err := s.db.QueryRowContext(ctx, countQuery, args...).Scan(&totalCount); err != nil { - return nil, err - } - - // Apply pagination - limit := opts.Limit - if limit <= 0 { - limit = 50 - } - if limit > 200 { - limit = 200 - } - - query := fmt.Sprintf(` - SELECT id, agent_id, name, template, project_id, - labels, annotations, - phase, activity, tool_name, - connection_state, container_status, runtime_state, - stalled_from_activity, - current_turns, current_model_calls, - image, detached, runtime, runtime_broker_id, web_pty_enabled, task_summary, message, - applied_config, - created_at, updated_at, last_seen, last_activity_event, deleted_at, started_at, - created_by, owner_id, visibility, state_version, ancestry - FROM agents %s ORDER BY created_at DESC LIMIT ? - `, whereClause) - args = append(args, limit+1) // Fetch one extra to determine if there's a next page - - rows, err := s.db.QueryContext(ctx, query, args...) - if err != nil { - return nil, err - } - defer rows.Close() - - var agents []store.Agent - for rows.Next() { - var agent store.Agent - var labels, annotations, appliedConfig string - var lastSeen, lastActivityEvent, deletedAt, startedAt sql.NullTime - var runtimeBrokerID, message, toolName, ancestry sql.NullString - - if err := rows.Scan( - &agent.ID, &agent.Slug, &agent.Name, &agent.Template, &agent.ProjectID, - &labels, &annotations, - &agent.Phase, &agent.Activity, &toolName, - &agent.ConnectionState, &agent.ContainerStatus, &agent.RuntimeState, - &agent.StalledFromActivity, - &agent.CurrentTurns, &agent.CurrentModelCalls, - &agent.Image, &agent.Detached, &agent.Runtime, &runtimeBrokerID, &agent.WebPTYEnabled, &agent.TaskSummary, &message, - &appliedConfig, - &agent.Created, &agent.Updated, &lastSeen, &lastActivityEvent, &deletedAt, &startedAt, - &agent.CreatedBy, &agent.OwnerID, &agent.Visibility, &agent.StateVersion, &ancestry, - ); err != nil { - return nil, err - } - - unmarshalJSON(labels, &agent.Labels) - unmarshalJSON(annotations, &agent.Annotations) - unmarshalJSON(appliedConfig, &agent.AppliedConfig) - unmarshalJSON(ancestry.String, &agent.Ancestry) - if lastSeen.Valid { - agent.LastSeen = lastSeen.Time - } - if lastActivityEvent.Valid { - agent.LastActivityEvent = lastActivityEvent.Time - } - if deletedAt.Valid { - agent.DeletedAt = deletedAt.Time - } - if startedAt.Valid { - agent.StartedAt = startedAt.Time - } - if runtimeBrokerID.Valid { - agent.RuntimeBrokerID = runtimeBrokerID.String - } - if message.Valid { - agent.Message = message.String - } - if toolName.Valid { - agent.ToolName = toolName.String - } - - agents = append(agents, agent) - } - - result := &store.ListResult[store.Agent]{ - Items: agents, - TotalCount: totalCount, - } - - // Handle pagination - if len(agents) > limit { - result.Items = agents[:limit] - result.NextCursor = agents[limit-1].ID - } - - return result, nil -} - -func (s *SQLiteStore) UpdateAgentStatus(ctx context.Context, id string, su store.AgentStatusUpdate) error { - now := time.Now() - - // When activity is being updated to something other than "executing", - // clear tool_name (it's only meaningful during execution). - // We signal this by setting the activity-provided flag. - activityProvided := su.Activity != "" - - // Prepare nullable values for limits tracking fields - var currentTurnsProvided bool - var currentTurnsVal int - if su.CurrentTurns != nil { - currentTurnsProvided = true - currentTurnsVal = *su.CurrentTurns - } - var currentModelCallsProvided bool - var currentModelCallsVal int - if su.CurrentModelCalls != nil { - currentModelCallsProvided = true - currentModelCallsVal = *su.CurrentModelCalls - } - - result, err := s.db.ExecContext(ctx, ` - UPDATE agents SET - phase = COALESCE(NULLIF(?, ''), phase), - activity = CASE WHEN ? != '' THEN - CASE WHEN phase = 'stopped' - AND activity IN ('crashed', 'limits_exceeded') - AND ? NOT IN ('crashed', 'limits_exceeded') - THEN activity ELSE ? END - ELSE activity END, - tool_name = CASE WHEN ? THEN ? ELSE tool_name END, - message = COALESCE(NULLIF(?, ''), message), - connection_state = COALESCE(NULLIF(?, ''), connection_state), - container_status = COALESCE(NULLIF(?, ''), container_status), - runtime_state = COALESCE(NULLIF(?, ''), runtime_state), - task_summary = COALESCE(NULLIF(?, ''), task_summary), - stalled_from_activity = CASE WHEN ? != '' THEN '' ELSE stalled_from_activity END, - last_activity_event = CASE WHEN ? != '' THEN ? ELSE last_activity_event END, - current_turns = CASE WHEN ? THEN ? ELSE current_turns END, - current_model_calls = CASE WHEN ? THEN ? ELSE current_model_calls END, - started_at = COALESCE(NULLIF(?, ''), started_at), - updated_at = ?, - last_seen = ? - WHERE id = ? - `, - su.Phase, - su.Activity, su.Activity, su.Activity, - activityProvided, su.ToolName, - su.Message, su.ConnectionState, su.ContainerStatus, - su.RuntimeState, su.TaskSummary, - su.Activity, - su.Activity, now, - currentTurnsProvided, currentTurnsVal, - currentModelCallsProvided, currentModelCallsVal, - su.StartedAt, - now, now, id, - ) - if err != nil { - return err - } - - rowsAffected, err := result.RowsAffected() - if err != nil { - return err - } - if rowsAffected == 0 { - return store.ErrNotFound - } - return nil -} - -func (s *SQLiteStore) PurgeDeletedAgents(ctx context.Context, cutoff time.Time) (int, error) { - result, err := s.db.ExecContext(ctx, - "DELETE FROM agents WHERE deleted_at IS NOT NULL AND deleted_at < ?", - cutoff, - ) - if err != nil { - return 0, err - } - rowsAffected, err := result.RowsAffected() - if err != nil { - return 0, err - } - return int(rowsAffected), nil -} - -func (s *SQLiteStore) MarkStaleAgentsOffline(ctx context.Context, threshold time.Time) ([]store.Agent, error) { - tx, err := s.db.BeginTx(ctx, nil) - if err != nil { - return nil, err - } - defer tx.Rollback() - - now := time.Now() - - // Update stale agents to offline activity. - // Only affects agents that: - // - Have reported at least one heartbeat (last_seen IS NOT NULL) - // - Are in the running phase - // - Are not already in a terminal/sticky activity (completed, limits_exceeded, offline) - _, err = tx.ExecContext(ctx, ` - UPDATE agents SET - activity = 'offline', - updated_at = ? - WHERE last_seen < ? - AND last_seen IS NOT NULL - AND phase = 'running' - AND activity NOT IN ('completed', 'limits_exceeded', 'blocked', 'offline') - `, now, threshold) - if err != nil { - return nil, err - } - - // Fetch the agents that were just updated. - rows, err := tx.QueryContext(ctx, ` - SELECT id, agent_id, name, template, project_id, - labels, annotations, - phase, activity, tool_name, - connection_state, container_status, runtime_state, - stalled_from_activity, - current_turns, current_model_calls, - image, detached, runtime, runtime_broker_id, web_pty_enabled, task_summary, message, - applied_config, - created_at, updated_at, last_seen, last_activity_event, deleted_at, started_at, - created_by, owner_id, visibility, state_version, ancestry - FROM agents - WHERE activity = 'offline' AND updated_at = ? - AND last_seen < ? - AND last_seen IS NOT NULL - AND phase = 'running' - `, now, threshold) - if err != nil { - return nil, err - } - defer rows.Close() - - var agents []store.Agent - for rows.Next() { - var agent store.Agent - var labels, annotations, appliedConfig string - var lastSeen, lastActivityEvent, deletedAt, startedAt sql.NullTime - var runtimeBrokerID, message, toolName, ancestry sql.NullString - - if err := rows.Scan( - &agent.ID, &agent.Slug, &agent.Name, &agent.Template, &agent.ProjectID, - &labels, &annotations, - &agent.Phase, &agent.Activity, &toolName, - &agent.ConnectionState, &agent.ContainerStatus, &agent.RuntimeState, - &agent.StalledFromActivity, - &agent.CurrentTurns, &agent.CurrentModelCalls, - &agent.Image, &agent.Detached, &agent.Runtime, &runtimeBrokerID, &agent.WebPTYEnabled, &agent.TaskSummary, &message, - &appliedConfig, - &agent.Created, &agent.Updated, &lastSeen, &lastActivityEvent, &deletedAt, &startedAt, - &agent.CreatedBy, &agent.OwnerID, &agent.Visibility, &agent.StateVersion, &ancestry, - ); err != nil { - return nil, err - } - - unmarshalJSON(labels, &agent.Labels) - unmarshalJSON(annotations, &agent.Annotations) - unmarshalJSON(appliedConfig, &agent.AppliedConfig) - unmarshalJSON(ancestry.String, &agent.Ancestry) - if lastSeen.Valid { - agent.LastSeen = lastSeen.Time - } - if lastActivityEvent.Valid { - agent.LastActivityEvent = lastActivityEvent.Time - } - if deletedAt.Valid { - agent.DeletedAt = deletedAt.Time - } - if startedAt.Valid { - agent.StartedAt = startedAt.Time - } - if runtimeBrokerID.Valid { - agent.RuntimeBrokerID = runtimeBrokerID.String - } - if message.Valid { - agent.Message = message.String - } - if toolName.Valid { - agent.ToolName = toolName.String - } - - agents = append(agents, agent) - } - - if err := tx.Commit(); err != nil { - return nil, err - } - - return agents, nil -} - -func (s *SQLiteStore) MarkStalledAgents(ctx context.Context, activityThreshold, heartbeatRecency time.Time) ([]store.Agent, error) { - tx, err := s.db.BeginTx(ctx, nil) - if err != nil { - return nil, err - } - defer tx.Rollback() - - now := time.Now() - - // Update agents to stalled activity. - // Only affects agents that: - // - Have a stale last_activity_event (older than activityThreshold) - // - Have a recent heartbeat (last_seen >= heartbeatRecency) — process is alive - // - Are in the running phase - // - Are not already in a terminal/sticky/waiting activity or already stalled/offline - _, err = tx.ExecContext(ctx, ` - UPDATE agents SET - stalled_from_activity = activity, - activity = 'stalled', - updated_at = ? - WHERE last_activity_event < ? - AND last_activity_event IS NOT NULL - AND last_seen >= ? - AND last_seen IS NOT NULL - AND phase = 'running' - AND activity NOT IN ('completed', 'limits_exceeded', 'blocked', 'stalled', 'offline', 'waiting_for_input') - `, now, activityThreshold, heartbeatRecency) - if err != nil { - return nil, err - } - - // Fetch the agents that were just updated. - rows, err := tx.QueryContext(ctx, ` - SELECT id, agent_id, name, template, project_id, - labels, annotations, - phase, activity, tool_name, - connection_state, container_status, runtime_state, - stalled_from_activity, - current_turns, current_model_calls, - image, detached, runtime, runtime_broker_id, web_pty_enabled, task_summary, message, - applied_config, - created_at, updated_at, last_seen, last_activity_event, deleted_at, started_at, - created_by, owner_id, visibility, state_version, ancestry - FROM agents - WHERE activity = 'stalled' AND updated_at = ? - AND last_activity_event < ? - AND last_activity_event IS NOT NULL - AND last_seen >= ? - AND last_seen IS NOT NULL - AND phase = 'running' - `, now, activityThreshold, heartbeatRecency) - if err != nil { - return nil, err - } - defer rows.Close() - - var agents []store.Agent - for rows.Next() { - var agent store.Agent - var labels, annotations, appliedConfig string - var lastSeen, lastActivityEvent, deletedAt, startedAt sql.NullTime - var runtimeBrokerID, message, toolName, ancestry sql.NullString - - if err := rows.Scan( - &agent.ID, &agent.Slug, &agent.Name, &agent.Template, &agent.ProjectID, - &labels, &annotations, - &agent.Phase, &agent.Activity, &toolName, - &agent.ConnectionState, &agent.ContainerStatus, &agent.RuntimeState, - &agent.StalledFromActivity, - &agent.CurrentTurns, &agent.CurrentModelCalls, - &agent.Image, &agent.Detached, &agent.Runtime, &runtimeBrokerID, &agent.WebPTYEnabled, &agent.TaskSummary, &message, - &appliedConfig, - &agent.Created, &agent.Updated, &lastSeen, &lastActivityEvent, &deletedAt, &startedAt, - &agent.CreatedBy, &agent.OwnerID, &agent.Visibility, &agent.StateVersion, &ancestry, - ); err != nil { - return nil, err - } - - unmarshalJSON(labels, &agent.Labels) - unmarshalJSON(annotations, &agent.Annotations) - unmarshalJSON(appliedConfig, &agent.AppliedConfig) - unmarshalJSON(ancestry.String, &agent.Ancestry) - if lastSeen.Valid { - agent.LastSeen = lastSeen.Time - } - if lastActivityEvent.Valid { - agent.LastActivityEvent = lastActivityEvent.Time - } - if deletedAt.Valid { - agent.DeletedAt = deletedAt.Time - } - if startedAt.Valid { - agent.StartedAt = startedAt.Time - } - if runtimeBrokerID.Valid { - agent.RuntimeBrokerID = runtimeBrokerID.String - } - if message.Valid { - agent.Message = message.String - } - if toolName.Valid { - agent.ToolName = toolName.String - } - - agents = append(agents, agent) - } - - if err := tx.Commit(); err != nil { - return nil, err - } - - return agents, nil -} - -// ============================================================================ -// Project Operations -// ============================================================================ - -func (s *SQLiteStore) CreateProject(ctx context.Context, project *store.Project) error { - now := time.Now() - project.Created = now - project.Updated = now - - _, err := s.db.ExecContext(ctx, ` - INSERT INTO projects (id, name, slug, git_remote, default_runtime_broker_id, labels, annotations, shared_dirs, created_at, updated_at, created_by, owner_id, visibility, github_installation_id, github_permissions, github_app_status, git_identity) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) - `, - project.ID, project.Name, project.Slug, nullableString(project.GitRemote), nullableString(project.DefaultRuntimeBrokerID), - marshalJSON(project.Labels), marshalJSON(project.Annotations), marshalJSON(project.SharedDirs), - project.Created, project.Updated, project.CreatedBy, project.OwnerID, project.Visibility, - nullableInt64(project.GitHubInstallationID), marshalJSONPtr(project.GitHubPermissions), marshalJSONPtr(project.GitHubAppStatus), - marshalJSONPtr(project.GitIdentity), - ) - if err != nil { - if strings.Contains(err.Error(), "UNIQUE constraint failed") { - return store.ErrAlreadyExists - } - return err - } - return nil -} - -func (s *SQLiteStore) GetProject(ctx context.Context, id string) (*store.Project, error) { - project := &store.Project{} - var labels, annotations, sharedDirs string - var gitRemote, defaultRuntimeBrokerID sql.NullString - var githubInstallationID sql.NullInt64 - var githubPermissions, githubAppStatus, gitIdentity string - - err := s.db.QueryRowContext(ctx, ` - SELECT id, name, slug, git_remote, default_runtime_broker_id, labels, annotations, shared_dirs, created_at, updated_at, created_by, owner_id, visibility, github_installation_id, COALESCE(github_permissions, ''), COALESCE(github_app_status, ''), COALESCE(git_identity, '') - FROM projects WHERE id = ? - `, id).Scan( - &project.ID, &project.Name, &project.Slug, &gitRemote, &defaultRuntimeBrokerID, - &labels, &annotations, &sharedDirs, - &project.Created, &project.Updated, &project.CreatedBy, &project.OwnerID, &project.Visibility, - &githubInstallationID, &githubPermissions, &githubAppStatus, &gitIdentity, - ) - if err != nil { - if errors.Is(err, sql.ErrNoRows) { - return nil, store.ErrNotFound - } - return nil, err - } - - if gitRemote.Valid { - project.GitRemote = gitRemote.String - } - if defaultRuntimeBrokerID.Valid { - project.DefaultRuntimeBrokerID = defaultRuntimeBrokerID.String - } - if githubInstallationID.Valid { - id := githubInstallationID.Int64 - project.GitHubInstallationID = &id - } - unmarshalJSON(labels, &project.Labels) - unmarshalJSON(annotations, &project.Annotations) - unmarshalJSON(sharedDirs, &project.SharedDirs) - if githubPermissions != "" { - project.GitHubPermissions = &store.GitHubTokenPermissions{} - unmarshalJSON(githubPermissions, project.GitHubPermissions) - } - if githubAppStatus != "" { - project.GitHubAppStatus = &store.GitHubAppProjectStatus{} - unmarshalJSON(githubAppStatus, project.GitHubAppStatus) - } - if gitIdentity != "" { - project.GitIdentity = &store.GitIdentityConfig{} - unmarshalJSON(gitIdentity, project.GitIdentity) - } - - // Populate computed fields - s.db.QueryRowContext(ctx, "SELECT COUNT(*) FROM agents WHERE project_id = ?", id).Scan(&project.AgentCount) - s.db.QueryRowContext(ctx, ` - SELECT (SELECT COUNT(*) FROM project_contributors WHERE project_id = ? AND status = 'online') - + (SELECT COUNT(*) FROM runtime_brokers WHERE auto_provide = 1 AND status = 'online' - AND id NOT IN (SELECT broker_id FROM project_contributors WHERE project_id = ?)) - `, id, id).Scan(&project.ActiveBrokerCount) - s.populateProjectType(ctx, project) - - return project, nil -} - -// populateProjectType sets the computed ProjectType field based on how the project was established. -// Type is "linked" (pre-existing local project linked to Hub) or "hub-managed" (created via Hub). -// Whether a project is git-backed is orthogonal — indicated by the GitRemote field. -func (s *SQLiteStore) populateProjectType(ctx context.Context, project *store.Project) { - // Check if any provider has a local_path not under ~/.scion/projects/ (i.e. broker-linked) - var linkedCount int - s.db.QueryRowContext(ctx, - "SELECT COUNT(*) FROM project_contributors WHERE project_id = ? AND local_path != '' AND local_path NOT LIKE '%/.scion/projects/%'", - project.ID).Scan(&linkedCount) - if linkedCount > 0 { - project.ProjectType = store.ProjectTypeLinked - return - } - project.ProjectType = store.ProjectTypeHubManaged -} - -func (s *SQLiteStore) GetProjectBySlug(ctx context.Context, slug string) (*store.Project, error) { - var id string - err := s.db.QueryRowContext(ctx, "SELECT id FROM projects WHERE slug = ?", slug).Scan(&id) - if err != nil { - if errors.Is(err, sql.ErrNoRows) { - return nil, store.ErrNotFound - } - return nil, err - } - return s.GetProject(ctx, id) -} - -func (s *SQLiteStore) GetProjectBySlugCaseInsensitive(ctx context.Context, slug string) (*store.Project, error) { - var id string - err := s.db.QueryRowContext(ctx, "SELECT id FROM projects WHERE LOWER(slug) = LOWER(?)", slug).Scan(&id) - if err != nil { - if errors.Is(err, sql.ErrNoRows) { - return nil, store.ErrNotFound - } - return nil, err - } - return s.GetProject(ctx, id) -} - -func (s *SQLiteStore) GetProjectsByGitRemote(ctx context.Context, gitRemote string) ([]*store.Project, error) { - rows, err := s.db.QueryContext(ctx, "SELECT id FROM projects WHERE git_remote = ? ORDER BY created_at ASC", gitRemote) - if err != nil { - return nil, err - } - - // Collect all IDs first, then close the cursor before calling GetProject - // (SQLite single-connection can't serve a new query while rows are open). - var ids []string - for rows.Next() { - var id string - if err := rows.Scan(&id); err != nil { - rows.Close() - return nil, err - } - ids = append(ids, id) - } - if err := rows.Err(); err != nil { - rows.Close() - return nil, err - } - rows.Close() - - projects := make([]*store.Project, 0, len(ids)) - for _, id := range ids { - project, err := s.GetProject(ctx, id) - if err != nil { - return nil, err - } - projects = append(projects, project) - } - return projects, nil -} - -func (s *SQLiteStore) NextAvailableSlug(ctx context.Context, baseSlug string) (string, error) { - // Check if the base slug is available - var count int - if err := s.db.QueryRowContext(ctx, "SELECT COUNT(*) FROM projects WHERE slug = ?", baseSlug).Scan(&count); err != nil { - return "", err - } - if count == 0 { - return baseSlug, nil - } - - // Find the next available serial suffix - for i := 1; ; i++ { - candidate := fmt.Sprintf("%s-%d", baseSlug, i) - if err := s.db.QueryRowContext(ctx, "SELECT COUNT(*) FROM projects WHERE slug = ?", candidate).Scan(&count); err != nil { - return "", err - } - if count == 0 { - return candidate, nil - } - } -} - -func (s *SQLiteStore) UpdateProject(ctx context.Context, project *store.Project) error { - project.Updated = time.Now() - - result, err := s.db.ExecContext(ctx, ` - UPDATE projects SET - name = ?, slug = ?, git_remote = ?, default_runtime_broker_id = ?, - labels = ?, annotations = ?, shared_dirs = ?, - updated_at = ?, owner_id = ?, visibility = ?, - github_installation_id = ?, github_permissions = ?, github_app_status = ?, - git_identity = ? - WHERE id = ? - `, - project.Name, project.Slug, nullableString(project.GitRemote), nullableString(project.DefaultRuntimeBrokerID), - marshalJSON(project.Labels), marshalJSON(project.Annotations), marshalJSON(project.SharedDirs), - project.Updated, project.OwnerID, project.Visibility, - nullableInt64(project.GitHubInstallationID), marshalJSONPtr(project.GitHubPermissions), marshalJSONPtr(project.GitHubAppStatus), - marshalJSONPtr(project.GitIdentity), - project.ID, - ) - if err != nil { - return err - } - - rowsAffected, err := result.RowsAffected() - if err != nil { - return err - } - if rowsAffected == 0 { - return store.ErrNotFound - } - return nil -} - -func (s *SQLiteStore) DeleteProject(ctx context.Context, id string) error { - result, err := s.db.ExecContext(ctx, "DELETE FROM projects WHERE id = ?", id) - if err != nil { - return err - } - rowsAffected, err := result.RowsAffected() - if err != nil { - return err - } - if rowsAffected == 0 { - return store.ErrNotFound - } - return nil -} - -func (s *SQLiteStore) ListProjects(ctx context.Context, filter store.ProjectFilter, opts store.ListOptions) (*store.ListResult[store.Project], error) { - var conditions []string - var args []interface{} - - if len(filter.MemberOrOwnerIDs) > 0 { - // Combine owner_id match with project ID membership using OR - placeholders := make([]string, len(filter.MemberOrOwnerIDs)) - for i, id := range filter.MemberOrOwnerIDs { - placeholders[i] = "?" - args = append(args, id) - } - orParts := []string{"id IN (" + strings.Join(placeholders, ",") + ")"} - if filter.OwnerID != "" { - orParts = append(orParts, "owner_id = ?") - args = append(args, filter.OwnerID) - } - conditions = append(conditions, "("+strings.Join(orParts, " OR ")+")") - } else if len(filter.MemberProjectIDs) > 0 { - // Strict project ID membership (no owner OR) - placeholders := make([]string, len(filter.MemberProjectIDs)) - for i, id := range filter.MemberProjectIDs { - placeholders[i] = "?" - args = append(args, id) - } - conditions = append(conditions, "id IN ("+strings.Join(placeholders, ",")+")") - } else if filter.OwnerID != "" { - conditions = append(conditions, "owner_id = ?") - args = append(args, filter.OwnerID) - } - if filter.ExcludeOwnerID != "" { - conditions = append(conditions, "owner_id != ?") - args = append(args, filter.ExcludeOwnerID) - } - if filter.Visibility != "" { - conditions = append(conditions, "visibility = ?") - args = append(args, filter.Visibility) - } - if filter.GitRemote != "" { - conditions = append(conditions, "git_remote = ?") - args = append(args, filter.GitRemote) - } else if filter.GitRemotePrefix != "" { - conditions = append(conditions, "git_remote LIKE ?") - args = append(args, filter.GitRemotePrefix+"%") - } - if filter.BrokerID != "" { - conditions = append(conditions, "id IN (SELECT project_id FROM project_contributors WHERE broker_id = ?)") - args = append(args, filter.BrokerID) - } - if filter.Name != "" { - conditions = append(conditions, "LOWER(name) = LOWER(?)") - args = append(args, filter.Name) - } - if filter.Slug != "" { - conditions = append(conditions, "LOWER(slug) = LOWER(?)") - args = append(args, filter.Slug) - } - - whereClause := "" - if len(conditions) > 0 { - whereClause = "WHERE " + strings.Join(conditions, " AND ") - } - - var totalCount int - countQuery := fmt.Sprintf("SELECT COUNT(*) FROM projects %s", whereClause) - if err := s.db.QueryRowContext(ctx, countQuery, args...).Scan(&totalCount); err != nil { - return nil, err - } - - limit := opts.Limit - if limit <= 0 { - limit = 50 - } - - query := fmt.Sprintf(` - SELECT id, name, slug, git_remote, default_runtime_broker_id, labels, annotations, shared_dirs, created_at, updated_at, created_by, owner_id, visibility, - github_installation_id, COALESCE(github_permissions, ''), COALESCE(github_app_status, ''), COALESCE(git_identity, '') - FROM projects %s ORDER BY created_at DESC LIMIT ? - `, whereClause) - args = append(args, limit) - - rows, err := s.db.QueryContext(ctx, query, args...) - if err != nil { - return nil, err - } - defer rows.Close() - - var projects []store.Project - type projectRow struct { - project store.Project - labels string - annotations string - sharedDirs string - gitRemote sql.NullString - brokerID sql.NullString - githubInstallationID sql.NullInt64 - githubPermissions string - githubAppStatus string - gitIdentity string - } - var rowData []projectRow - - for rows.Next() { - var r projectRow - if err := rows.Scan( - &r.project.ID, &r.project.Name, &r.project.Slug, &r.gitRemote, &r.brokerID, - &r.labels, &r.annotations, &r.sharedDirs, - &r.project.Created, &r.project.Updated, &r.project.CreatedBy, &r.project.OwnerID, &r.project.Visibility, - &r.githubInstallationID, &r.githubPermissions, &r.githubAppStatus, &r.gitIdentity, - ); err != nil { - return nil, err - } - rowData = append(rowData, r) - } - rows.Close() // Close early to release connection for nested queries - - for _, r := range rowData { - project := r.project - if r.gitRemote.Valid { - project.GitRemote = r.gitRemote.String - } - if r.brokerID.Valid { - project.DefaultRuntimeBrokerID = r.brokerID.String - } - if r.githubInstallationID.Valid { - id := r.githubInstallationID.Int64 - project.GitHubInstallationID = &id - } - unmarshalJSON(r.labels, &project.Labels) - unmarshalJSON(r.annotations, &project.Annotations) - unmarshalJSON(r.sharedDirs, &project.SharedDirs) - if r.githubPermissions != "" { - project.GitHubPermissions = &store.GitHubTokenPermissions{} - unmarshalJSON(r.githubPermissions, project.GitHubPermissions) - } - if r.githubAppStatus != "" { - project.GitHubAppStatus = &store.GitHubAppProjectStatus{} - unmarshalJSON(r.githubAppStatus, project.GitHubAppStatus) - } - if r.gitIdentity != "" { - project.GitIdentity = &store.GitIdentityConfig{} - unmarshalJSON(r.gitIdentity, project.GitIdentity) - } - - // Populate computed fields - these now have a connection available - s.db.QueryRowContext(ctx, "SELECT COUNT(*) FROM agents WHERE project_id = ?", project.ID).Scan(&project.AgentCount) - s.db.QueryRowContext(ctx, ` - SELECT (SELECT COUNT(*) FROM project_contributors WHERE project_id = ? AND status = 'online') - + (SELECT COUNT(*) FROM runtime_brokers WHERE auto_provide = 1 AND status = 'online' - AND id NOT IN (SELECT broker_id FROM project_contributors WHERE project_id = ?)) - `, project.ID, project.ID).Scan(&project.ActiveBrokerCount) - s.populateProjectType(ctx, &project) - - projects = append(projects, project) - } - - return &store.ListResult[store.Project]{ - Items: projects, - TotalCount: totalCount, - }, nil -} - -// ============================================================================ -// RuntimeBroker Operations -// ============================================================================ - -func (s *SQLiteStore) CreateRuntimeBroker(ctx context.Context, broker *store.RuntimeBroker) error { - now := time.Now() - broker.Created = now - broker.Updated = now - - _, err := s.db.ExecContext(ctx, ` - INSERT INTO runtime_brokers ( - id, name, slug, type, mode, version, - status, connection_state, last_heartbeat, - capabilities, supported_harnesses, resources, runtimes, - labels, annotations, endpoint, - created_at, updated_at, created_by, auto_provide - ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) - `, - broker.ID, broker.Name, broker.Slug, "", "", broker.Version, - broker.Status, broker.ConnectionState, broker.LastHeartbeat, - marshalJSON(broker.Capabilities), "[]", - "{}", marshalJSON(broker.Profiles), - marshalJSON(broker.Labels), marshalJSON(broker.Annotations), broker.Endpoint, - broker.Created, broker.Updated, nullableString(broker.CreatedBy), broker.AutoProvide, - ) - if err != nil { - if strings.Contains(err.Error(), "UNIQUE constraint failed") { - return store.ErrAlreadyExists - } - return err - } - return nil -} - -func (s *SQLiteStore) GetRuntimeBroker(ctx context.Context, id string) (*store.RuntimeBroker, error) { - broker := &store.RuntimeBroker{} - var capabilities, profiles, labels, annotations string - var brokerType, brokerMode, harnesses, resources string // unused columns kept for schema compatibility - var lastHeartbeat sql.NullTime - var createdBy sql.NullString - - err := s.db.QueryRowContext(ctx, ` - SELECT id, name, slug, type, mode, version, - status, connection_state, last_heartbeat, - capabilities, supported_harnesses, resources, runtimes, - labels, annotations, endpoint, - created_at, updated_at, created_by, auto_provide - FROM runtime_brokers WHERE id = ? - `, id).Scan( - &broker.ID, &broker.Name, &broker.Slug, &brokerType, &brokerMode, &broker.Version, - &broker.Status, &broker.ConnectionState, &lastHeartbeat, - &capabilities, &harnesses, &resources, &profiles, - &labels, &annotations, &broker.Endpoint, - &broker.Created, &broker.Updated, &createdBy, &broker.AutoProvide, - ) - if err != nil { - if errors.Is(err, sql.ErrNoRows) { - return nil, store.ErrNotFound - } - return nil, err - } - - if lastHeartbeat.Valid { - broker.LastHeartbeat = lastHeartbeat.Time - } - if createdBy.Valid { - broker.CreatedBy = createdBy.String - } - unmarshalJSON(capabilities, &broker.Capabilities) - unmarshalJSON(profiles, &broker.Profiles) - unmarshalJSON(labels, &broker.Labels) - unmarshalJSON(annotations, &broker.Annotations) - - return broker, nil -} - -func (s *SQLiteStore) GetRuntimeBrokerByName(ctx context.Context, name string) (*store.RuntimeBroker, error) { - var id string - err := s.db.QueryRowContext(ctx, "SELECT id FROM runtime_brokers WHERE LOWER(name) = LOWER(?)", name).Scan(&id) - if err != nil { - if errors.Is(err, sql.ErrNoRows) { - return nil, store.ErrNotFound - } - return nil, err - } - return s.GetRuntimeBroker(ctx, id) -} - -func (s *SQLiteStore) UpdateRuntimeBroker(ctx context.Context, broker *store.RuntimeBroker) error { - broker.Updated = time.Now() - - result, err := s.db.ExecContext(ctx, ` - UPDATE runtime_brokers SET - name = ?, slug = ?, type = ?, version = ?, - status = ?, connection_state = ?, last_heartbeat = ?, - capabilities = ?, supported_harnesses = ?, resources = ?, runtimes = ?, - labels = ?, annotations = ?, endpoint = ?, - updated_at = ?, auto_provide = ? - WHERE id = ? - `, - broker.Name, broker.Slug, "", broker.Version, - broker.Status, broker.ConnectionState, broker.LastHeartbeat, - marshalJSON(broker.Capabilities), "[]", - "{}", marshalJSON(broker.Profiles), - marshalJSON(broker.Labels), marshalJSON(broker.Annotations), broker.Endpoint, - broker.Updated, broker.AutoProvide, - broker.ID, - ) - if err != nil { - return err - } - - rowsAffected, err := result.RowsAffected() - if err != nil { - return err - } - if rowsAffected == 0 { - return store.ErrNotFound - } - return nil -} - -func (s *SQLiteStore) DeleteRuntimeBroker(ctx context.Context, id string) error { - result, err := s.db.ExecContext(ctx, "DELETE FROM runtime_brokers WHERE id = ?", id) - if err != nil { - return err - } - rowsAffected, err := result.RowsAffected() - if err != nil { - return err - } - if rowsAffected == 0 { - return store.ErrNotFound - } - return nil -} - -func (s *SQLiteStore) ListRuntimeBrokers(ctx context.Context, filter store.RuntimeBrokerFilter, opts store.ListOptions) (*store.ListResult[store.RuntimeBroker], error) { - var conditions []string - var args []interface{} - - if filter.Status != "" { - conditions = append(conditions, "status = ?") - args = append(args, filter.Status) - } - if filter.ProjectID != "" { - conditions = append(conditions, "(id IN (SELECT broker_id FROM project_contributors WHERE project_id = ?) OR auto_provide = 1)") - args = append(args, filter.ProjectID) - } - if filter.Name != "" { - conditions = append(conditions, "LOWER(name) = LOWER(?)") - args = append(args, filter.Name) - } - if filter.AutoProvide != nil { - conditions = append(conditions, "auto_provide = ?") - args = append(args, *filter.AutoProvide) - } - - whereClause := "" - if len(conditions) > 0 { - whereClause = "WHERE " + strings.Join(conditions, " AND ") - } - - var totalCount int - countQuery := fmt.Sprintf("SELECT COUNT(*) FROM runtime_brokers %s", whereClause) - if err := s.db.QueryRowContext(ctx, countQuery, args...).Scan(&totalCount); err != nil { - return nil, err - } - - limit := opts.Limit - if limit <= 0 { - limit = 50 - } - - query := fmt.Sprintf(` - SELECT id, name, slug, type, mode, version, - status, connection_state, last_heartbeat, - capabilities, supported_harnesses, resources, runtimes, - labels, annotations, endpoint, - created_at, updated_at, created_by, auto_provide - FROM runtime_brokers %s ORDER BY created_at DESC LIMIT ? - `, whereClause) - args = append(args, limit) - - rows, err := s.db.QueryContext(ctx, query, args...) - if err != nil { - return nil, err - } - defer rows.Close() - - var hosts []store.RuntimeBroker - for rows.Next() { - var broker store.RuntimeBroker - var capabilities, profiles, labels, annotations string - var brokerType, brokerMode, harnesses, resources string // unused columns kept for schema compatibility - var lastHeartbeat sql.NullTime - var createdBy sql.NullString - - if err := rows.Scan( - &broker.ID, &broker.Name, &broker.Slug, &brokerType, &brokerMode, &broker.Version, - &broker.Status, &broker.ConnectionState, &lastHeartbeat, - &capabilities, &harnesses, &resources, &profiles, - &labels, &annotations, &broker.Endpoint, - &broker.Created, &broker.Updated, &createdBy, &broker.AutoProvide, - ); err != nil { - return nil, err - } - - if lastHeartbeat.Valid { - broker.LastHeartbeat = lastHeartbeat.Time - } - if createdBy.Valid { - broker.CreatedBy = createdBy.String - } - unmarshalJSON(capabilities, &broker.Capabilities) - unmarshalJSON(profiles, &broker.Profiles) - unmarshalJSON(labels, &broker.Labels) - unmarshalJSON(annotations, &broker.Annotations) - - hosts = append(hosts, broker) - } - - return &store.ListResult[store.RuntimeBroker]{ - Items: hosts, - TotalCount: totalCount, - }, nil -} - -func (s *SQLiteStore) UpdateRuntimeBrokerHeartbeat(ctx context.Context, id string, status string) error { - now := time.Now() - - result, err := s.db.ExecContext(ctx, ` - UPDATE runtime_brokers SET - status = ?, - last_heartbeat = ?, - updated_at = ? - WHERE id = ? - `, status, now, now, id) - if err != nil { - return err - } - - rowsAffected, err := result.RowsAffected() - if err != nil { - return err - } - if rowsAffected == 0 { - return store.ErrNotFound - } - return nil -} - -// ============================================================================ -// Template Operations -// ============================================================================ - -func (s *SQLiteStore) CreateTemplate(ctx context.Context, template *store.Template) error { - now := time.Now() - template.Created = now - template.Updated = now - - // Set default status if not provided - if template.Status == "" { - template.Status = store.TemplateStatusActive - } - - _, err := s.db.ExecContext(ctx, ` - INSERT INTO templates ( - id, name, slug, display_name, description, harness, default_harness_config, image, config, - content_hash, scope, scope_id, project_id, - storage_uri, storage_bucket, storage_path, files, - base_template, locked, status, - owner_id, created_by, updated_by, visibility, - created_at, updated_at - ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) - `, - template.ID, template.Name, template.Slug, nullableString(template.DisplayName), nullableString(template.Description), - template.Harness, nullableString(template.DefaultHarnessConfig), template.Image, marshalJSON(template.Config), - nullableString(template.ContentHash), template.Scope, nullableString(template.ScopeID), nullableString(template.ProjectID), - nullableString(template.StorageURI), nullableString(template.StorageBucket), nullableString(template.StoragePath), marshalJSON(template.Files), - nullableString(template.BaseTemplate), template.Locked, template.Status, - nullableString(template.OwnerID), nullableString(template.CreatedBy), nullableString(template.UpdatedBy), template.Visibility, - template.Created, template.Updated, - ) - if err != nil { - if strings.Contains(err.Error(), "UNIQUE constraint failed") { - return store.ErrAlreadyExists - } - return err - } - return nil -} - -func (s *SQLiteStore) GetTemplate(ctx context.Context, id string) (*store.Template, error) { - template := &store.Template{} - var config, files string - var displayName, description, contentHash, scopeID, projectID sql.NullString - var storageURI, storageBucket, storagePath, baseTemplate sql.NullString - var createdBy, updatedBy, ownerID, visibility sql.NullString - var defaultHarnessConfig sql.NullString - - err := s.db.QueryRowContext(ctx, ` - SELECT id, name, slug, display_name, description, harness, default_harness_config, image, config, - content_hash, scope, scope_id, project_id, - storage_uri, storage_bucket, storage_path, files, - base_template, locked, status, - owner_id, created_by, updated_by, visibility, - created_at, updated_at - FROM templates WHERE id = ? - `, id).Scan( - &template.ID, &template.Name, &template.Slug, &displayName, &description, - &template.Harness, &defaultHarnessConfig, &template.Image, &config, - &contentHash, &template.Scope, &scopeID, &projectID, - &storageURI, &storageBucket, &storagePath, &files, - &baseTemplate, &template.Locked, &template.Status, - &ownerID, &createdBy, &updatedBy, &visibility, - &template.Created, &template.Updated, - ) - if err != nil { - if errors.Is(err, sql.ErrNoRows) { - return nil, store.ErrNotFound - } - return nil, err - } - - if displayName.Valid { - template.DisplayName = displayName.String - } - if description.Valid { - template.Description = description.String - } - if defaultHarnessConfig.Valid { - template.DefaultHarnessConfig = defaultHarnessConfig.String - } - if contentHash.Valid { - template.ContentHash = contentHash.String - } - if scopeID.Valid { - template.ScopeID = scopeID.String - } - if projectID.Valid { - template.ProjectID = projectID.String - } - if storageURI.Valid { - template.StorageURI = storageURI.String - } - if storageBucket.Valid { - template.StorageBucket = storageBucket.String - } - if storagePath.Valid { - template.StoragePath = storagePath.String - } - if baseTemplate.Valid { - template.BaseTemplate = baseTemplate.String - } - if ownerID.Valid { - template.OwnerID = ownerID.String - } - if createdBy.Valid { - template.CreatedBy = createdBy.String - } - if updatedBy.Valid { - template.UpdatedBy = updatedBy.String - } - if visibility.Valid { - template.Visibility = visibility.String - } - unmarshalJSON(config, &template.Config) - unmarshalJSON(files, &template.Files) - - return template, nil -} - -func (s *SQLiteStore) GetTemplateBySlug(ctx context.Context, slug, scope, scopeID string) (*store.Template, error) { - var id string - var err error - - if scope == "project" && scopeID != "" { - // Try scope_id first, then fall back to project_id for backwards compatibility - err = s.db.QueryRowContext(ctx, "SELECT id FROM templates WHERE slug = ? AND scope = ? AND (scope_id = ? OR project_id = ?)", slug, scope, scopeID, scopeID).Scan(&id) - } else if scope == "user" && scopeID != "" { - err = s.db.QueryRowContext(ctx, "SELECT id FROM templates WHERE slug = ? AND scope = ? AND scope_id = ?", slug, scope, scopeID).Scan(&id) - } else { - err = s.db.QueryRowContext(ctx, "SELECT id FROM templates WHERE slug = ? AND scope = ?", slug, scope).Scan(&id) - } - - if err != nil { - if errors.Is(err, sql.ErrNoRows) { - return nil, store.ErrNotFound - } - return nil, err - } - return s.GetTemplate(ctx, id) -} - -func (s *SQLiteStore) UpdateTemplate(ctx context.Context, template *store.Template) error { - template.Updated = time.Now() - - result, err := s.db.ExecContext(ctx, ` - UPDATE templates SET - name = ?, slug = ?, display_name = ?, description = ?, - harness = ?, default_harness_config = ?, image = ?, config = ?, - content_hash = ?, scope = ?, scope_id = ?, project_id = ?, - storage_uri = ?, storage_bucket = ?, storage_path = ?, files = ?, - base_template = ?, locked = ?, status = ?, - owner_id = ?, updated_by = ?, visibility = ?, - updated_at = ? - WHERE id = ? - `, - template.Name, template.Slug, nullableString(template.DisplayName), nullableString(template.Description), - template.Harness, nullableString(template.DefaultHarnessConfig), template.Image, marshalJSON(template.Config), - nullableString(template.ContentHash), template.Scope, nullableString(template.ScopeID), nullableString(template.ProjectID), - nullableString(template.StorageURI), nullableString(template.StorageBucket), nullableString(template.StoragePath), marshalJSON(template.Files), - nullableString(template.BaseTemplate), template.Locked, template.Status, - nullableString(template.OwnerID), nullableString(template.UpdatedBy), template.Visibility, - template.Updated, - template.ID, - ) - if err != nil { - return err - } - - rowsAffected, err := result.RowsAffected() - if err != nil { - return err - } - if rowsAffected == 0 { - return store.ErrNotFound - } - return nil -} - -func (s *SQLiteStore) DeleteTemplate(ctx context.Context, id string) error { - result, err := s.db.ExecContext(ctx, "DELETE FROM templates WHERE id = ?", id) - if err != nil { - return err - } - rowsAffected, err := result.RowsAffected() - if err != nil { - return err - } - if rowsAffected == 0 { - return store.ErrNotFound - } - return nil -} - -func (s *SQLiteStore) DeleteTemplatesByScope(ctx context.Context, scope, scopeID string) (int, error) { - result, err := s.db.ExecContext(ctx, "DELETE FROM templates WHERE scope = ? AND scope_id = ?", scope, scopeID) - if err != nil { - return 0, err - } - n, err := result.RowsAffected() - if err != nil { - return 0, err - } - return int(n), nil -} - -func (s *SQLiteStore) ListTemplates(ctx context.Context, filter store.TemplateFilter, opts store.ListOptions) (*store.ListResult[store.Template], error) { - var conditions []string - var args []interface{} - - if filter.Name != "" { - // Exact match on name or slug - conditions = append(conditions, "(name = ? OR slug = ?)") - args = append(args, filter.Name, filter.Name) - } - if filter.Scope != "" { - conditions = append(conditions, "scope = ?") - args = append(args, filter.Scope) - } - if filter.ScopeID != "" { - conditions = append(conditions, "(scope_id = ? OR project_id = ?)") - args = append(args, filter.ScopeID, filter.ScopeID) - } else if filter.ProjectID != "" && filter.Scope == "" { - // When projectId is set without scope, return global + project-scoped templates for this project - conditions = append(conditions, "(scope = 'global' OR (scope = 'project' AND (scope_id = ? OR project_id = ?)))") - args = append(args, filter.ProjectID, filter.ProjectID) - } else if filter.ProjectID != "" { - // Backwards compatibility: projectId with explicit scope - conditions = append(conditions, "(scope_id = ? OR project_id = ?)") - args = append(args, filter.ProjectID, filter.ProjectID) - } - if filter.Harness != "" { - conditions = append(conditions, "harness = ?") - args = append(args, filter.Harness) - } - if filter.OwnerID != "" { - conditions = append(conditions, "owner_id = ?") - args = append(args, filter.OwnerID) - } - if filter.Status != "" { - conditions = append(conditions, "status = ?") - args = append(args, filter.Status) - } - if filter.Search != "" { - conditions = append(conditions, "(name LIKE ? OR description LIKE ?)") - searchPattern := "%" + filter.Search + "%" - args = append(args, searchPattern, searchPattern) - } - - whereClause := "" - if len(conditions) > 0 { - whereClause = "WHERE " + strings.Join(conditions, " AND ") - } - - var totalCount int - countQuery := fmt.Sprintf("SELECT COUNT(*) FROM templates %s", whereClause) - if err := s.db.QueryRowContext(ctx, countQuery, args...).Scan(&totalCount); err != nil { - return nil, err - } - - limit := opts.Limit - if limit <= 0 { - limit = 50 - } - - query := fmt.Sprintf(` - SELECT id, name, slug, display_name, description, harness, default_harness_config, image, config, - content_hash, scope, scope_id, project_id, - storage_uri, storage_bucket, storage_path, files, - base_template, locked, status, - owner_id, created_by, updated_by, visibility, - created_at, updated_at - FROM templates %s ORDER BY created_at DESC LIMIT ? - `, whereClause) - args = append(args, limit) - - rows, err := s.db.QueryContext(ctx, query, args...) - if err != nil { - return nil, err - } - defer rows.Close() - - var templates []store.Template - for rows.Next() { - var template store.Template - var config, files string - var displayName, description, contentHash, scopeID, projectID sql.NullString - var storageURI, storageBucket, storagePath, baseTemplate sql.NullString - var createdBy, updatedBy, ownerID, visibility sql.NullString - var defaultHarnessConfig sql.NullString - - if err := rows.Scan( - &template.ID, &template.Name, &template.Slug, &displayName, &description, - &template.Harness, &defaultHarnessConfig, &template.Image, &config, - &contentHash, &template.Scope, &scopeID, &projectID, - &storageURI, &storageBucket, &storagePath, &files, - &baseTemplate, &template.Locked, &template.Status, - &ownerID, &createdBy, &updatedBy, &visibility, - &template.Created, &template.Updated, - ); err != nil { - return nil, err - } - - if displayName.Valid { - template.DisplayName = displayName.String - } - if description.Valid { - template.Description = description.String - } - if defaultHarnessConfig.Valid { - template.DefaultHarnessConfig = defaultHarnessConfig.String - } - if contentHash.Valid { - template.ContentHash = contentHash.String - } - if scopeID.Valid { - template.ScopeID = scopeID.String - } - if projectID.Valid { - template.ProjectID = projectID.String - } - if storageURI.Valid { - template.StorageURI = storageURI.String - } - if storageBucket.Valid { - template.StorageBucket = storageBucket.String - } - if storagePath.Valid { - template.StoragePath = storagePath.String - } - if baseTemplate.Valid { - template.BaseTemplate = baseTemplate.String - } - if ownerID.Valid { - template.OwnerID = ownerID.String - } - if createdBy.Valid { - template.CreatedBy = createdBy.String - } - if updatedBy.Valid { - template.UpdatedBy = updatedBy.String - } - if visibility.Valid { - template.Visibility = visibility.String - } - unmarshalJSON(config, &template.Config) - unmarshalJSON(files, &template.Files) - - templates = append(templates, template) - } - - return &store.ListResult[store.Template]{ - Items: templates, - TotalCount: totalCount, - }, nil -} - -// ============================================================================ -// HarnessConfig Operations -// ============================================================================ - -func (s *SQLiteStore) CreateHarnessConfig(ctx context.Context, hc *store.HarnessConfig) error { - now := time.Now() - hc.Created = now - hc.Updated = now - - if hc.Status == "" { - hc.Status = store.HarnessConfigStatusActive - } - - _, err := s.db.ExecContext(ctx, ` - INSERT INTO harness_configs ( - id, name, slug, display_name, description, harness, config, - content_hash, scope, scope_id, - storage_uri, storage_bucket, storage_path, files, - locked, status, - owner_id, created_by, updated_by, visibility, - created_at, updated_at - ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) - `, - hc.ID, hc.Name, hc.Slug, nullableString(hc.DisplayName), nullableString(hc.Description), - hc.Harness, marshalJSON(hc.Config), - nullableString(hc.ContentHash), hc.Scope, nullableString(hc.ScopeID), - nullableString(hc.StorageURI), nullableString(hc.StorageBucket), nullableString(hc.StoragePath), marshalJSON(hc.Files), - hc.Locked, hc.Status, - nullableString(hc.OwnerID), nullableString(hc.CreatedBy), nullableString(hc.UpdatedBy), hc.Visibility, - hc.Created, hc.Updated, - ) - if err != nil { - if strings.Contains(err.Error(), "UNIQUE constraint failed") { - return store.ErrAlreadyExists - } - return err - } - return nil -} - -func (s *SQLiteStore) GetHarnessConfig(ctx context.Context, id string) (*store.HarnessConfig, error) { - hc := &store.HarnessConfig{} - var configJSON, filesJSON string - var displayName, description, contentHash, scopeID sql.NullString - var storageURI, storageBucket, storagePath sql.NullString - var createdBy, updatedBy, ownerID, visibility sql.NullString - - err := s.db.QueryRowContext(ctx, ` - SELECT id, name, slug, display_name, description, harness, config, - content_hash, scope, scope_id, - storage_uri, storage_bucket, storage_path, files, - locked, status, - owner_id, created_by, updated_by, visibility, - created_at, updated_at - FROM harness_configs WHERE id = ? - `, id).Scan( - &hc.ID, &hc.Name, &hc.Slug, &displayName, &description, - &hc.Harness, &configJSON, - &contentHash, &hc.Scope, &scopeID, - &storageURI, &storageBucket, &storagePath, &filesJSON, - &hc.Locked, &hc.Status, - &ownerID, &createdBy, &updatedBy, &visibility, - &hc.Created, &hc.Updated, - ) - if err != nil { - if errors.Is(err, sql.ErrNoRows) { - return nil, store.ErrNotFound - } - return nil, err - } - - if displayName.Valid { - hc.DisplayName = displayName.String - } - if description.Valid { - hc.Description = description.String - } - if contentHash.Valid { - hc.ContentHash = contentHash.String - } - if scopeID.Valid { - hc.ScopeID = scopeID.String - } - if storageURI.Valid { - hc.StorageURI = storageURI.String - } - if storageBucket.Valid { - hc.StorageBucket = storageBucket.String - } - if storagePath.Valid { - hc.StoragePath = storagePath.String - } - if ownerID.Valid { - hc.OwnerID = ownerID.String - } - if createdBy.Valid { - hc.CreatedBy = createdBy.String - } - if updatedBy.Valid { - hc.UpdatedBy = updatedBy.String - } - if visibility.Valid { - hc.Visibility = visibility.String - } - unmarshalJSON(configJSON, &hc.Config) - unmarshalJSON(filesJSON, &hc.Files) - - return hc, nil -} - -func (s *SQLiteStore) GetHarnessConfigBySlug(ctx context.Context, slug, scope, scopeID string) (*store.HarnessConfig, error) { - var id string - var err error - - if scopeID != "" { - err = s.db.QueryRowContext(ctx, "SELECT id FROM harness_configs WHERE slug = ? AND scope = ? AND scope_id = ?", slug, scope, scopeID).Scan(&id) - } else { - err = s.db.QueryRowContext(ctx, "SELECT id FROM harness_configs WHERE slug = ? AND scope = ?", slug, scope).Scan(&id) - } - - if err != nil { - if errors.Is(err, sql.ErrNoRows) { - return nil, store.ErrNotFound - } - return nil, err - } - return s.GetHarnessConfig(ctx, id) -} - -func (s *SQLiteStore) UpdateHarnessConfig(ctx context.Context, hc *store.HarnessConfig) error { - hc.Updated = time.Now() - - result, err := s.db.ExecContext(ctx, ` - UPDATE harness_configs SET - name = ?, slug = ?, display_name = ?, description = ?, - harness = ?, config = ?, - content_hash = ?, scope = ?, scope_id = ?, - storage_uri = ?, storage_bucket = ?, storage_path = ?, files = ?, - locked = ?, status = ?, - owner_id = ?, updated_by = ?, visibility = ?, - updated_at = ? - WHERE id = ? - `, - hc.Name, hc.Slug, nullableString(hc.DisplayName), nullableString(hc.Description), - hc.Harness, marshalJSON(hc.Config), - nullableString(hc.ContentHash), hc.Scope, nullableString(hc.ScopeID), - nullableString(hc.StorageURI), nullableString(hc.StorageBucket), nullableString(hc.StoragePath), marshalJSON(hc.Files), - hc.Locked, hc.Status, - nullableString(hc.OwnerID), nullableString(hc.UpdatedBy), hc.Visibility, - hc.Updated, - hc.ID, - ) - if err != nil { - return err - } - - rowsAffected, err := result.RowsAffected() - if err != nil { - return err - } - if rowsAffected == 0 { - return store.ErrNotFound - } - return nil -} - -func (s *SQLiteStore) DeleteHarnessConfig(ctx context.Context, id string) error { - result, err := s.db.ExecContext(ctx, "DELETE FROM harness_configs WHERE id = ?", id) - if err != nil { - return err - } - rowsAffected, err := result.RowsAffected() - if err != nil { - return err - } - if rowsAffected == 0 { - return store.ErrNotFound - } - return nil -} - -func (s *SQLiteStore) DeleteHarnessConfigsByScope(ctx context.Context, scope, scopeID string) (int, error) { - result, err := s.db.ExecContext(ctx, "DELETE FROM harness_configs WHERE scope = ? AND scope_id = ?", scope, scopeID) - if err != nil { - return 0, err - } - n, err := result.RowsAffected() - if err != nil { - return 0, err - } - return int(n), nil -} - -func (s *SQLiteStore) ListHarnessConfigs(ctx context.Context, filter store.HarnessConfigFilter, opts store.ListOptions) (*store.ListResult[store.HarnessConfig], error) { - var conditions []string - var args []interface{} - - if filter.Name != "" { - conditions = append(conditions, "(name = ? OR slug = ?)") - args = append(args, filter.Name, filter.Name) - } - if filter.Scope != "" { - conditions = append(conditions, "scope = ?") - args = append(args, filter.Scope) - } - if filter.ScopeID != "" { - conditions = append(conditions, "scope_id = ?") - args = append(args, filter.ScopeID) - } else if filter.ProjectID != "" && filter.Scope == "" { - // When projectId is set without scope, return global + project-scoped configs for this project - conditions = append(conditions, "(scope = 'global' OR (scope = 'project' AND scope_id = ?))") - args = append(args, filter.ProjectID) - } else if (filter.Scope == "project" || filter.Scope == "grove") && filter.ProjectID != "" { - // projectId combined with an explicit scope filter — narrow to that project. - conditions = append(conditions, "scope_id = ?") - args = append(args, filter.ProjectID) - } - if filter.Harness != "" { - conditions = append(conditions, "harness = ?") - args = append(args, filter.Harness) - } - if filter.OwnerID != "" { - conditions = append(conditions, "owner_id = ?") - args = append(args, filter.OwnerID) - } - if filter.Status != "" { - conditions = append(conditions, "status = ?") - args = append(args, filter.Status) - } - if filter.Search != "" { - conditions = append(conditions, "(name LIKE ? OR description LIKE ?)") - searchPattern := "%" + filter.Search + "%" - args = append(args, searchPattern, searchPattern) - } - - whereClause := "" - if len(conditions) > 0 { - whereClause = "WHERE " + strings.Join(conditions, " AND ") - } - - var totalCount int - countQuery := fmt.Sprintf("SELECT COUNT(*) FROM harness_configs %s", whereClause) - if err := s.db.QueryRowContext(ctx, countQuery, args...).Scan(&totalCount); err != nil { - return nil, err - } - - limit := opts.Limit - if limit <= 0 { - limit = 50 - } - - query := fmt.Sprintf(` - SELECT id, name, slug, display_name, description, harness, config, - content_hash, scope, scope_id, - storage_uri, storage_bucket, storage_path, files, - locked, status, - owner_id, created_by, updated_by, visibility, - created_at, updated_at - FROM harness_configs %s ORDER BY created_at DESC LIMIT ? - `, whereClause) - args = append(args, limit) - - rows, err := s.db.QueryContext(ctx, query, args...) - if err != nil { - return nil, err - } - defer rows.Close() - - var harnessConfigs []store.HarnessConfig - for rows.Next() { - var hc store.HarnessConfig - var configJSON, filesJSON string - var displayName, description, contentHash, scopeID sql.NullString - var storageURI, storageBucket, storagePath sql.NullString - var createdBy, updatedBy, ownerID, visibility sql.NullString - - if err := rows.Scan( - &hc.ID, &hc.Name, &hc.Slug, &displayName, &description, - &hc.Harness, &configJSON, - &contentHash, &hc.Scope, &scopeID, - &storageURI, &storageBucket, &storagePath, &filesJSON, - &hc.Locked, &hc.Status, - &ownerID, &createdBy, &updatedBy, &visibility, - &hc.Created, &hc.Updated, - ); err != nil { - return nil, err - } - - if displayName.Valid { - hc.DisplayName = displayName.String - } - if description.Valid { - hc.Description = description.String - } - if contentHash.Valid { - hc.ContentHash = contentHash.String - } - if scopeID.Valid { - hc.ScopeID = scopeID.String - } - if storageURI.Valid { - hc.StorageURI = storageURI.String - } - if storageBucket.Valid { - hc.StorageBucket = storageBucket.String - } - if storagePath.Valid { - hc.StoragePath = storagePath.String - } - if ownerID.Valid { - hc.OwnerID = ownerID.String - } - if createdBy.Valid { - hc.CreatedBy = createdBy.String - } - if updatedBy.Valid { - hc.UpdatedBy = updatedBy.String - } - if visibility.Valid { - hc.Visibility = visibility.String - } - unmarshalJSON(configJSON, &hc.Config) - unmarshalJSON(filesJSON, &hc.Files) - - harnessConfigs = append(harnessConfigs, hc) - } - - // When querying by ProjectID without explicit Scope, the query returns both - // global and project-scoped configs. Deduplicate by slug, preferring the more - // specific scope (project > global). - if filter.ProjectID != "" && filter.Scope == "" { - seen := make(map[string]int, len(harnessConfigs)) - deduped := make([]store.HarnessConfig, 0, len(harnessConfigs)) - for _, hc := range harnessConfigs { - if idx, exists := seen[hc.Slug]; exists { - if hc.Scope == "project" && deduped[idx].Scope == "global" { - deduped[idx] = hc - } - } else { - seen[hc.Slug] = len(deduped) - deduped = append(deduped, hc) - } - } - harnessConfigs = deduped - totalCount = len(deduped) - } - - return &store.ListResult[store.HarnessConfig]{ - Items: harnessConfigs, - TotalCount: totalCount, - }, nil -} - -// ============================================================================ -// User Operations -// ============================================================================ - -func (s *SQLiteStore) CreateUser(ctx context.Context, user *store.User) error { - now := time.Now() - user.Created = now - - _, err := s.db.ExecContext(ctx, ` - INSERT INTO users (id, email, display_name, avatar_url, role, status, preferences, created_at, last_login) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?) - `, - user.ID, user.Email, user.DisplayName, user.AvatarURL, user.Role, user.Status, - marshalJSON(user.Preferences), user.Created, user.LastLogin, - ) - if err != nil { - if strings.Contains(err.Error(), "UNIQUE constraint failed") { - return store.ErrAlreadyExists - } - return err - } - return nil -} - -func (s *SQLiteStore) GetUser(ctx context.Context, id string) (*store.User, error) { - user := &store.User{} - var preferences string - var lastLogin, lastSeen sql.NullTime - - err := s.db.QueryRowContext(ctx, ` - SELECT id, email, display_name, avatar_url, role, status, preferences, created_at, last_login, last_seen - FROM users WHERE id = ? - `, id).Scan( - &user.ID, &user.Email, &user.DisplayName, &user.AvatarURL, &user.Role, &user.Status, - &preferences, &user.Created, &lastLogin, &lastSeen, - ) - if err != nil { - if errors.Is(err, sql.ErrNoRows) { - return nil, store.ErrNotFound - } - return nil, err - } - - if lastLogin.Valid { - user.LastLogin = lastLogin.Time - } - if lastSeen.Valid { - user.LastSeen = lastSeen.Time - } - unmarshalJSON(preferences, &user.Preferences) - - return user, nil -} - -func (s *SQLiteStore) GetUserByEmail(ctx context.Context, email string) (*store.User, error) { - var id string - err := s.db.QueryRowContext(ctx, "SELECT id FROM users WHERE email = ?", email).Scan(&id) - if err != nil { - if errors.Is(err, sql.ErrNoRows) { - return nil, store.ErrNotFound - } - return nil, err - } - return s.GetUser(ctx, id) -} - -func (s *SQLiteStore) UpdateUser(ctx context.Context, user *store.User) error { - result, err := s.db.ExecContext(ctx, ` - UPDATE users SET - email = ?, display_name = ?, avatar_url = ?, - role = ?, status = ?, preferences = ?, last_login = ?, last_seen = ? - WHERE id = ? - `, - user.Email, user.DisplayName, user.AvatarURL, - user.Role, user.Status, marshalJSON(user.Preferences), user.LastLogin, user.LastSeen, - user.ID, - ) - if err != nil { - return err - } - - rowsAffected, err := result.RowsAffected() - if err != nil { - return err - } - if rowsAffected == 0 { - return store.ErrNotFound - } - return nil -} - -func (s *SQLiteStore) UpdateUserLastSeen(ctx context.Context, id string, t time.Time) error { - _, err := s.db.ExecContext(ctx, `UPDATE users SET last_seen = ? WHERE id = ?`, t, id) - return err -} - -func (s *SQLiteStore) DeleteUser(ctx context.Context, id string) error { - result, err := s.db.ExecContext(ctx, "DELETE FROM users WHERE id = ?", id) - if err != nil { - return err - } - rowsAffected, err := result.RowsAffected() - if err != nil { - return err - } - if rowsAffected == 0 { - return store.ErrNotFound - } - return nil -} - -func (s *SQLiteStore) ListUsers(ctx context.Context, filter store.UserFilter, opts store.ListOptions) (*store.ListResult[store.User], error) { - var conditions []string - var args []interface{} - - if filter.Role != "" { - conditions = append(conditions, "role = ?") - args = append(args, filter.Role) - } - if filter.Status != "" { - conditions = append(conditions, "status = ?") - args = append(args, filter.Status) - } - if filter.Search != "" { - pattern := "%" + filter.Search + "%" - conditions = append(conditions, "(email LIKE ? OR display_name LIKE ?)") - args = append(args, pattern, pattern) - } - - whereClause := "" - if len(conditions) > 0 { - whereClause = "WHERE " + strings.Join(conditions, " AND ") - } - - var totalCount int - countQuery := fmt.Sprintf("SELECT COUNT(*) FROM users %s", whereClause) - if err := s.db.QueryRowContext(ctx, countQuery, args...).Scan(&totalCount); err != nil { - return nil, err - } - - limit := opts.Limit - if limit <= 0 { - limit = 50 - } - - if limit > 200 { - limit = 200 - } - - offset := 0 - if opts.Cursor != "" { - if parsed, err := strconv.Atoi(opts.Cursor); err == nil && parsed > 0 { - offset = parsed - } - } - - // Map sort field to column name (whitelist to prevent SQL injection) - orderColumn := "created_at" - orderDir := "DESC" - switch opts.SortBy { - case "name": - orderColumn = "display_name" - orderDir = "ASC" // default ascending for name - case "lastSeen": - orderColumn = "last_seen" - case "created": - orderColumn = "created_at" - } - if opts.SortDir == "asc" { - orderDir = "ASC" - } else if opts.SortDir == "desc" { - orderDir = "DESC" - } - - query := fmt.Sprintf(` - SELECT id, email, display_name, avatar_url, role, status, preferences, created_at, last_login, last_seen - FROM users %s ORDER BY %s %s LIMIT ? OFFSET ? - `, whereClause, orderColumn, orderDir) - args = append(args, limit+1, offset) - - rows, err := s.db.QueryContext(ctx, query, args...) - if err != nil { - return nil, err - } - defer rows.Close() - - var users []store.User - for rows.Next() { - var user store.User - var preferences string - var lastLogin, lastSeen sql.NullTime - - if err := rows.Scan( - &user.ID, &user.Email, &user.DisplayName, &user.AvatarURL, &user.Role, &user.Status, - &preferences, &user.Created, &lastLogin, &lastSeen, - ); err != nil { - return nil, err - } - - if lastLogin.Valid { - user.LastLogin = lastLogin.Time - } - if lastSeen.Valid { - user.LastSeen = lastSeen.Time - } - unmarshalJSON(preferences, &user.Preferences) - - users = append(users, user) - } - - result := &store.ListResult[store.User]{ - Items: users, - TotalCount: totalCount, - } - - // Handle pagination: if we got more than limit, there's a next page - if len(users) > limit { - result.Items = users[:limit] - result.NextCursor = strconv.Itoa(offset + limit) - } - - return result, nil -} - -// ============================================================================ -// Allow List Operations -// ============================================================================ - -func (s *SQLiteStore) AddAllowListEntry(ctx context.Context, entry *store.AllowListEntry) error { - if entry.Created.IsZero() { - entry.Created = time.Now() - } - entry.Email = strings.ToLower(entry.Email) - - _, err := s.db.ExecContext(ctx, ` - INSERT INTO allow_list (id, email, note, added_by, invite_id, created) - VALUES (?, ?, ?, ?, ?, ?) - `, entry.ID, entry.Email, entry.Note, entry.AddedBy, entry.InviteID, entry.Created) - if err != nil { - if strings.Contains(err.Error(), "UNIQUE constraint failed") { - return store.ErrAlreadyExists - } - return err - } - return nil -} - -func (s *SQLiteStore) RemoveAllowListEntry(ctx context.Context, email string) error { - result, err := s.db.ExecContext(ctx, "DELETE FROM allow_list WHERE email = ?", strings.ToLower(email)) - if err != nil { - return err - } - rowsAffected, err := result.RowsAffected() - if err != nil { - return err - } - if rowsAffected == 0 { - return store.ErrNotFound - } - return nil -} - -func (s *SQLiteStore) GetAllowListEntry(ctx context.Context, email string) (*store.AllowListEntry, error) { - entry := &store.AllowListEntry{} - err := s.db.QueryRowContext(ctx, ` - SELECT id, email, note, added_by, invite_id, created - FROM allow_list WHERE email = ? - `, strings.ToLower(email)).Scan( - &entry.ID, &entry.Email, &entry.Note, &entry.AddedBy, &entry.InviteID, &entry.Created, - ) - if err != nil { - if errors.Is(err, sql.ErrNoRows) { - return nil, store.ErrNotFound - } - return nil, err - } - return entry, nil -} - -func (s *SQLiteStore) ListAllowListEntries(ctx context.Context, opts store.ListOptions) (*store.ListResult[store.AllowListEntry], error) { - var totalCount int - if err := s.db.QueryRowContext(ctx, "SELECT COUNT(*) FROM allow_list").Scan(&totalCount); err != nil { - return nil, err - } - - limit := opts.Limit - if limit <= 0 { - limit = 50 - } - - var conditions []string - var args []interface{} - - if opts.Cursor != "" { - var cursorCreated time.Time - if err := s.db.QueryRowContext(ctx, "SELECT created FROM allow_list WHERE id = ?", opts.Cursor).Scan(&cursorCreated); err != nil { - return nil, fmt.Errorf("invalid cursor: %w", err) - } - conditions = append(conditions, `(created < ? OR (created = ? AND id < ?))`) - args = append(args, cursorCreated, cursorCreated, opts.Cursor) - } - - whereClause := "" - if len(conditions) > 0 { - whereClause = "WHERE " + strings.Join(conditions, " AND ") - } - - query := fmt.Sprintf(` - SELECT id, email, note, added_by, invite_id, created - FROM allow_list %s ORDER BY created DESC, id DESC LIMIT ? - `, whereClause) - args = append(args, limit+1) - - rows, err := s.db.QueryContext(ctx, query, args...) - if err != nil { - return nil, err - } - defer rows.Close() - - var entries []store.AllowListEntry - for rows.Next() { - var entry store.AllowListEntry - if err := rows.Scan(&entry.ID, &entry.Email, &entry.Note, &entry.AddedBy, &entry.InviteID, &entry.Created); err != nil { - return nil, err - } - entries = append(entries, entry) - } - if err := rows.Err(); err != nil { - return nil, err - } - if entries == nil { - entries = []store.AllowListEntry{} - } - - var nextCursor string - if len(entries) > limit { - nextCursor = entries[limit-1].ID - entries = entries[:limit] - } - - return &store.ListResult[store.AllowListEntry]{ - Items: entries, - TotalCount: totalCount, - NextCursor: nextCursor, - }, nil -} - -func (s *SQLiteStore) IsEmailAllowListed(ctx context.Context, email string) (bool, error) { - var count int - err := s.db.QueryRowContext(ctx, "SELECT COUNT(*) FROM allow_list WHERE email = ?", strings.ToLower(email)).Scan(&count) - if err != nil { - return false, err - } - return count > 0, nil -} - -func (s *SQLiteStore) UpdateAllowListEntryInviteID(ctx context.Context, email string, inviteID string) error { - result, err := s.db.ExecContext(ctx, - "UPDATE allow_list SET invite_id = ? WHERE email = ?", - inviteID, strings.ToLower(email)) - if err != nil { - return err - } - rowsAffected, err := result.RowsAffected() - if err != nil { - return err - } - if rowsAffected == 0 { - return store.ErrNotFound - } - return nil -} - -func (s *SQLiteStore) ListAllowListEntriesWithInvites(ctx context.Context, opts store.ListOptions) (*store.ListResult[store.AllowListEntryWithInvite], error) { - var totalCount int - if err := s.db.QueryRowContext(ctx, "SELECT COUNT(*) FROM allow_list").Scan(&totalCount); err != nil { - return nil, err - } - - limit := opts.Limit - if limit <= 0 { - limit = 50 - } - - var conditions []string - var args []interface{} - - if opts.Cursor != "" { - var cursorCreated time.Time - if err := s.db.QueryRowContext(ctx, "SELECT created FROM allow_list WHERE id = ?", opts.Cursor).Scan(&cursorCreated); err != nil { - return nil, fmt.Errorf("invalid cursor: %w", err) - } - conditions = append(conditions, `(a.created < ? OR (a.created = ? AND a.id < ?))`) - args = append(args, cursorCreated, cursorCreated, opts.Cursor) - } - - whereClause := "" - if len(conditions) > 0 { - whereClause = "WHERE " + strings.Join(conditions, " AND ") - } - - query := fmt.Sprintf(` - SELECT a.id, a.email, a.note, a.added_by, a.invite_id, a.created, - i.code_prefix, i.max_uses, i.use_count, i.expires_at, i.revoked - FROM allow_list a - LEFT JOIN invite_codes i ON a.invite_id = i.id AND a.invite_id != '' - %s ORDER BY a.created DESC, a.id DESC LIMIT ? - `, whereClause) - args = append(args, limit+1) - - rows, err := s.db.QueryContext(ctx, query, args...) - if err != nil { - return nil, err - } - defer rows.Close() - - var entries []store.AllowListEntryWithInvite - for rows.Next() { - var entry store.AllowListEntryWithInvite - var codePrefix sql.NullString - var maxUses, useCount, revoked sql.NullInt64 - var expiresAt sql.NullTime - if err := rows.Scan( - &entry.ID, &entry.Email, &entry.Note, &entry.AddedBy, &entry.InviteID, &entry.Created, - &codePrefix, &maxUses, &useCount, &expiresAt, &revoked, - ); err != nil { - return nil, err - } - if codePrefix.Valid { - entry.InviteCodePrefix = codePrefix.String - } - if maxUses.Valid { - entry.InviteMaxUses = int(maxUses.Int64) - } - if useCount.Valid { - entry.InviteUseCount = int(useCount.Int64) - } - if expiresAt.Valid { - entry.InviteExpiresAt = expiresAt.Time - } - if revoked.Valid { - entry.InviteRevoked = revoked.Int64 != 0 - } - entries = append(entries, entry) - } - if err := rows.Err(); err != nil { - return nil, err - } - if entries == nil { - entries = []store.AllowListEntryWithInvite{} - } - - var nextCursor string - if len(entries) > limit { - nextCursor = entries[limit-1].ID - entries = entries[:limit] - } - - return &store.ListResult[store.AllowListEntryWithInvite]{ - Items: entries, - TotalCount: totalCount, - NextCursor: nextCursor, - }, nil -} - -func (s *SQLiteStore) BulkAddAllowListEntries(ctx context.Context, entries []*store.AllowListEntry) (int, int, error) { - tx, err := s.db.BeginTx(ctx, nil) - if err != nil { - return 0, 0, err - } - defer tx.Rollback() - - stmt, err := tx.PrepareContext(ctx, ` - INSERT OR IGNORE INTO allow_list (id, email, note, added_by, invite_id, created) - VALUES (?, ?, ?, ?, ?, ?) - `) - if err != nil { - return 0, 0, err - } - defer stmt.Close() - - added := 0 - skipped := 0 - now := time.Now() - - for _, entry := range entries { - entry.Email = strings.ToLower(entry.Email) - if entry.Created.IsZero() { - entry.Created = now - } - result, err := stmt.ExecContext(ctx, entry.ID, entry.Email, entry.Note, entry.AddedBy, entry.InviteID, entry.Created) - if err != nil { - return added, skipped, err - } - rows, _ := result.RowsAffected() - if rows > 0 { - added++ - } else { - skipped++ - } - } - - if err := tx.Commit(); err != nil { - return 0, 0, err - } - return added, skipped, nil -} - -func (s *SQLiteStore) ListEmailDomains(ctx context.Context) ([]string, error) { - rows, err := s.db.QueryContext(ctx, ` - SELECT DISTINCT SUBSTR(email, INSTR(email, '@') + 1) AS domain - FROM users - WHERE email LIKE '%@%' - ORDER BY domain - `) - if err != nil { - return nil, err - } - defer rows.Close() - - var domains []string - for rows.Next() { - var domain string - if err := rows.Scan(&domain); err != nil { - return nil, err - } - domains = append(domains, domain) - } - return domains, rows.Err() -} - -// ============================================================================ -// Invite Code Operations -// ============================================================================ - -func (s *SQLiteStore) CreateInviteCode(ctx context.Context, invite *store.InviteCode) error { - if invite.Created.IsZero() { - invite.Created = time.Now() - } - - _, err := s.db.ExecContext(ctx, ` - INSERT INTO invite_codes (id, code_hash, code_prefix, max_uses, use_count, expires_at, revoked, created_by, note, created) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?) - `, invite.ID, invite.CodeHash, invite.CodePrefix, invite.MaxUses, invite.UseCount, - invite.ExpiresAt, invite.Revoked, invite.CreatedBy, invite.Note, invite.Created) - if err != nil { - if strings.Contains(err.Error(), "UNIQUE constraint failed") { - return store.ErrAlreadyExists - } - return err - } - return nil -} - -func (s *SQLiteStore) GetInviteCodeByHash(ctx context.Context, codeHash string) (*store.InviteCode, error) { - invite := &store.InviteCode{} - var revoked int - err := s.db.QueryRowContext(ctx, ` - SELECT id, code_hash, code_prefix, max_uses, use_count, expires_at, revoked, created_by, note, created - FROM invite_codes WHERE code_hash = ? - `, codeHash).Scan( - &invite.ID, &invite.CodeHash, &invite.CodePrefix, &invite.MaxUses, &invite.UseCount, - &invite.ExpiresAt, &revoked, &invite.CreatedBy, &invite.Note, &invite.Created, - ) - if err != nil { - if errors.Is(err, sql.ErrNoRows) { - return nil, store.ErrNotFound - } - return nil, err - } - invite.Revoked = revoked != 0 - return invite, nil -} - -func (s *SQLiteStore) GetInviteCode(ctx context.Context, id string) (*store.InviteCode, error) { - invite := &store.InviteCode{} - var revoked int - err := s.db.QueryRowContext(ctx, ` - SELECT id, code_hash, code_prefix, max_uses, use_count, expires_at, revoked, created_by, note, created - FROM invite_codes WHERE id = ? - `, id).Scan( - &invite.ID, &invite.CodeHash, &invite.CodePrefix, &invite.MaxUses, &invite.UseCount, - &invite.ExpiresAt, &revoked, &invite.CreatedBy, &invite.Note, &invite.Created, - ) - if err != nil { - if errors.Is(err, sql.ErrNoRows) { - return nil, store.ErrNotFound - } - return nil, err - } - invite.Revoked = revoked != 0 - return invite, nil -} - -func (s *SQLiteStore) ListInviteCodes(ctx context.Context, opts store.ListOptions) (*store.ListResult[store.InviteCode], error) { - var totalCount int - if err := s.db.QueryRowContext(ctx, "SELECT COUNT(*) FROM invite_codes").Scan(&totalCount); err != nil { - return nil, err - } - - limit := opts.Limit - if limit <= 0 { - limit = 50 - } - - var conditions []string - var args []interface{} - - if opts.Cursor != "" { - conditions = append(conditions, `(created < (SELECT created FROM invite_codes WHERE id = ?) - OR (created = (SELECT created FROM invite_codes WHERE id = ?) AND id < ?))`) - args = append(args, opts.Cursor, opts.Cursor, opts.Cursor) - } - - whereClause := "" - if len(conditions) > 0 { - whereClause = "WHERE " + strings.Join(conditions, " AND ") - } - - query := fmt.Sprintf(` - SELECT id, code_prefix, max_uses, use_count, expires_at, revoked, created_by, note, created - FROM invite_codes %s ORDER BY created DESC, id DESC LIMIT ? - `, whereClause) - args = append(args, limit+1) - - rows, err := s.db.QueryContext(ctx, query, args...) - if err != nil { - return nil, err - } - defer rows.Close() - - var invites []store.InviteCode - for rows.Next() { - var invite store.InviteCode - var revoked int - if err := rows.Scan( - &invite.ID, &invite.CodePrefix, &invite.MaxUses, &invite.UseCount, - &invite.ExpiresAt, &revoked, &invite.CreatedBy, &invite.Note, &invite.Created, - ); err != nil { - return nil, err - } - invite.Revoked = revoked != 0 - invites = append(invites, invite) - } - if err := rows.Err(); err != nil { - return nil, err - } - if invites == nil { - invites = []store.InviteCode{} - } - - var nextCursor string - if len(invites) > limit { - nextCursor = invites[limit-1].ID - invites = invites[:limit] - } - - return &store.ListResult[store.InviteCode]{ - Items: invites, - TotalCount: totalCount, - NextCursor: nextCursor, - }, nil -} - -func (s *SQLiteStore) IncrementInviteUseCount(ctx context.Context, id string) error { - result, err := s.db.ExecContext(ctx, ` - UPDATE invite_codes SET use_count = use_count + 1 - WHERE id = ? AND revoked = 0 AND expires_at > datetime('now') - AND (max_uses = 0 OR use_count < max_uses) - `, id) - if err != nil { - return err - } - rowsAffected, err := result.RowsAffected() - if err != nil { - return err - } - if rowsAffected == 0 { - return store.ErrNotFound - } - return nil -} - -func (s *SQLiteStore) RevokeInviteCode(ctx context.Context, id string) error { - result, err := s.db.ExecContext(ctx, "UPDATE invite_codes SET revoked = 1 WHERE id = ?", id) - if err != nil { - return err - } - rowsAffected, err := result.RowsAffected() - if err != nil { - return err - } - if rowsAffected == 0 { - return store.ErrNotFound - } - return nil -} - -func (s *SQLiteStore) DeleteInviteCode(ctx context.Context, id string) error { - result, err := s.db.ExecContext(ctx, "DELETE FROM invite_codes WHERE id = ?", id) - if err != nil { - return err - } - rowsAffected, err := result.RowsAffected() - if err != nil { - return err - } - if rowsAffected == 0 { - return store.ErrNotFound - } - return nil -} - -func (s *SQLiteStore) GetInviteStats(ctx context.Context) (*store.InviteStats, error) { - stats := &store.InviteStats{} - - // Count pending (active, not expired, not exhausted) invites - err := s.db.QueryRowContext(ctx, ` - SELECT COUNT(*) FROM invite_codes - WHERE revoked = 0 - AND expires_at > datetime('now') - AND (max_uses = 0 OR use_count < max_uses) - `).Scan(&stats.PendingInvites) - if err != nil { - return nil, err - } - - // Total redemptions across all invites - err = s.db.QueryRowContext(ctx, ` - SELECT COALESCE(SUM(use_count), 0) FROM invite_codes - `).Scan(&stats.TotalRedemptions) - if err != nil { - return nil, err - } - - // Allow list count - err = s.db.QueryRowContext(ctx, `SELECT COUNT(*) FROM allow_list`).Scan(&stats.AllowListCount) - if err != nil { - return nil, err - } - - // Recent invites that have been redeemed (use_count > 0), ordered by most recently created - rows, err := s.db.QueryContext(ctx, ` - SELECT id, code_prefix, use_count, max_uses, expires_at, note, created - FROM invite_codes - WHERE use_count > 0 - ORDER BY created DESC - LIMIT 10 - `) - if err != nil { - return nil, err - } - defer rows.Close() - - for rows.Next() { - var info store.InviteCodeInfo - if err := rows.Scan(&info.ID, &info.CodePrefix, &info.UseCount, &info.MaxUses, &info.ExpiresAt, &info.Note, &info.Created); err != nil { - return nil, err - } - stats.RecentRedemptions = append(stats.RecentRedemptions, info) - } - if stats.RecentRedemptions == nil { - stats.RecentRedemptions = []store.InviteCodeInfo{} - } - - return stats, rows.Err() -} - -// ============================================================================ -// ProjectProvider Operations - -// ============================================================================ - -func (s *SQLiteStore) AddProjectProvider(ctx context.Context, provider *store.ProjectProvider) error { - // Set LinkedAt to now if not already set - if provider.LinkedAt.IsZero() && provider.LinkedBy != "" { - provider.LinkedAt = time.Now() - } - - _, err := s.db.ExecContext(ctx, ` - INSERT OR REPLACE INTO project_contributors (project_id, broker_id, broker_name, local_path, mode, status, profiles, last_seen, linked_by, linked_at) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?) - `, - provider.ProjectID, provider.BrokerID, provider.BrokerName, provider.LocalPath, "", provider.Status, - "[]", provider.LastSeen, // profiles column kept for schema compat but no longer used - nullableString(provider.LinkedBy), nullableTime(provider.LinkedAt), - ) - return err -} - -func (s *SQLiteStore) RemoveProjectProvider(ctx context.Context, projectID, brokerID string) error { - result, err := s.db.ExecContext(ctx, "DELETE FROM project_contributors WHERE project_id = ? AND broker_id = ?", projectID, brokerID) - if err != nil { - return err - } - rowsAffected, err := result.RowsAffected() - if err != nil { - return err - } - if rowsAffected == 0 { - return store.ErrNotFound - } - return nil -} - -func (s *SQLiteStore) GetProjectProvider(ctx context.Context, projectID, brokerID string) (*store.ProjectProvider, error) { - var provider store.ProjectProvider - var localPath, linkedBy sql.NullString - var providerMode, profiles string // unused columns kept for schema compat - var lastSeen, linkedAt sql.NullTime - - err := s.db.QueryRowContext(ctx, ` - SELECT project_id, broker_id, broker_name, local_path, mode, status, profiles, last_seen, linked_by, linked_at - FROM project_contributors WHERE project_id = ? AND broker_id = ? - `, projectID, brokerID).Scan( - &provider.ProjectID, &provider.BrokerID, &provider.BrokerName, &localPath, &providerMode, &provider.Status, - &profiles, &lastSeen, &linkedBy, &linkedAt, - ) - if err != nil { - if errors.Is(err, sql.ErrNoRows) { - return nil, store.ErrNotFound - } - return nil, err - } - - if localPath.Valid { - provider.LocalPath = localPath.String - } - if lastSeen.Valid { - provider.LastSeen = lastSeen.Time - } - if linkedBy.Valid { - provider.LinkedBy = linkedBy.String - } - if linkedAt.Valid { - provider.LinkedAt = linkedAt.Time - } - // profiles column no longer used - lookup from RuntimeBroker.Profiles instead - - return &provider, nil -} - -func (s *SQLiteStore) GetProjectProviders(ctx context.Context, projectID string) ([]store.ProjectProvider, error) { - rows, err := s.db.QueryContext(ctx, ` - SELECT project_id, broker_id, broker_name, local_path, mode, status, profiles, last_seen, linked_by, linked_at - FROM project_contributors WHERE project_id = ? - `, projectID) - if err != nil { - return nil, err - } - defer rows.Close() - - var providers []store.ProjectProvider - for rows.Next() { - var provider store.ProjectProvider - var localPath, linkedBy sql.NullString - var providerMode, profiles string // unused columns kept for schema compat - var lastSeen, linkedAt sql.NullTime - - if err := rows.Scan( - &provider.ProjectID, &provider.BrokerID, &provider.BrokerName, &localPath, &providerMode, &provider.Status, - &profiles, &lastSeen, &linkedBy, &linkedAt, - ); err != nil { - return nil, err - } - - if localPath.Valid { - provider.LocalPath = localPath.String - } - if lastSeen.Valid { - provider.LastSeen = lastSeen.Time - } - if linkedBy.Valid { - provider.LinkedBy = linkedBy.String - } - if linkedAt.Valid { - provider.LinkedAt = linkedAt.Time - } - // profiles column no longer used - lookup from RuntimeBroker.Profiles instead - - providers = append(providers, provider) - } - - return providers, nil -} - -func (s *SQLiteStore) GetBrokerProjects(ctx context.Context, brokerID string) ([]store.ProjectProvider, error) { - rows, err := s.db.QueryContext(ctx, ` - SELECT project_id, broker_id, broker_name, local_path, mode, status, profiles, last_seen, linked_by, linked_at - FROM project_contributors WHERE broker_id = ? - `, brokerID) - if err != nil { - return nil, err - } - defer rows.Close() - - var providers []store.ProjectProvider - for rows.Next() { - var provider store.ProjectProvider - var localPath, linkedBy sql.NullString - var providerMode, profiles string // unused columns kept for schema compat - var lastSeen, linkedAt sql.NullTime - - if err := rows.Scan( - &provider.ProjectID, &provider.BrokerID, &provider.BrokerName, &localPath, &providerMode, &provider.Status, - &profiles, &lastSeen, &linkedBy, &linkedAt, - ); err != nil { - return nil, err - } - - if localPath.Valid { - provider.LocalPath = localPath.String - } - if lastSeen.Valid { - provider.LastSeen = lastSeen.Time - } - if linkedBy.Valid { - provider.LinkedBy = linkedBy.String - } - if linkedAt.Valid { - provider.LinkedAt = linkedAt.Time - } - // profiles column no longer used - lookup from RuntimeBroker.Profiles instead - - providers = append(providers, provider) - } - - return providers, nil -} - -func (s *SQLiteStore) UpdateProviderStatus(ctx context.Context, projectID, brokerID, status string) error { - now := time.Now() - - result, err := s.db.ExecContext(ctx, ` - UPDATE project_contributors SET status = ?, last_seen = ? WHERE project_id = ? AND broker_id = ? - `, status, now, projectID, brokerID) - if err != nil { - return err - } - - rowsAffected, err := result.RowsAffected() - if err != nil { - return err - } - if rowsAffected == 0 { - return store.ErrNotFound - } - return nil -} - -// ============================================================================ -// EnvVar Operations -// ============================================================================ - -func (s *SQLiteStore) CreateEnvVar(ctx context.Context, envVar *store.EnvVar) error { - now := time.Now() - envVar.Created = now - envVar.Updated = now - - _, err := s.db.ExecContext(ctx, ` - INSERT INTO env_vars (id, key, value, scope, scope_id, description, sensitive, injection_mode, secret, created_at, updated_at, created_by) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) - `, - envVar.ID, envVar.Key, envVar.Value, envVar.Scope, envVar.ScopeID, - envVar.Description, envVar.Sensitive, envVar.InjectionMode, envVar.Secret, - envVar.Created, envVar.Updated, envVar.CreatedBy, - ) - if err != nil { - if strings.Contains(err.Error(), "UNIQUE constraint failed") { - return store.ErrAlreadyExists - } - return err - } - return nil -} - -func (s *SQLiteStore) GetEnvVar(ctx context.Context, key, scope, scopeID string) (*store.EnvVar, error) { - envVar := &store.EnvVar{} - - err := s.db.QueryRowContext(ctx, ` - SELECT id, key, value, scope, scope_id, description, sensitive, injection_mode, secret, created_at, updated_at, created_by - FROM env_vars WHERE key = ? AND scope = ? AND scope_id = ? - `, key, scope, scopeID).Scan( - &envVar.ID, &envVar.Key, &envVar.Value, &envVar.Scope, &envVar.ScopeID, - &envVar.Description, &envVar.Sensitive, &envVar.InjectionMode, &envVar.Secret, - &envVar.Created, &envVar.Updated, &envVar.CreatedBy, - ) - if err != nil { - if errors.Is(err, sql.ErrNoRows) { - return nil, store.ErrNotFound - } - return nil, err - } - - return envVar, nil -} - -func (s *SQLiteStore) UpdateEnvVar(ctx context.Context, envVar *store.EnvVar) error { - envVar.Updated = time.Now() - - result, err := s.db.ExecContext(ctx, ` - UPDATE env_vars SET - value = ?, description = ?, sensitive = ?, injection_mode = ?, secret = ?, updated_at = ? - WHERE key = ? AND scope = ? AND scope_id = ? - `, - envVar.Value, envVar.Description, envVar.Sensitive, envVar.InjectionMode, envVar.Secret, envVar.Updated, - envVar.Key, envVar.Scope, envVar.ScopeID, - ) - if err != nil { - return err - } - - rowsAffected, err := result.RowsAffected() - if err != nil { - return err - } - if rowsAffected == 0 { - return store.ErrNotFound - } - return nil -} - -func (s *SQLiteStore) UpsertEnvVar(ctx context.Context, envVar *store.EnvVar) (bool, error) { - now := time.Now() - envVar.Updated = now - - // Check if it already exists - existing, err := s.GetEnvVar(ctx, envVar.Key, envVar.Scope, envVar.ScopeID) - if err != nil && err != store.ErrNotFound { - return false, err - } - - if existing != nil { - // Update existing - envVar.ID = existing.ID - envVar.Created = existing.Created - envVar.CreatedBy = existing.CreatedBy - if err := s.UpdateEnvVar(ctx, envVar); err != nil { - return false, err - } - return false, nil - } - - // Create new - envVar.Created = now - if err := s.CreateEnvVar(ctx, envVar); err != nil { - return false, err - } - return true, nil -} - -func (s *SQLiteStore) DeleteEnvVar(ctx context.Context, key, scope, scopeID string) error { - result, err := s.db.ExecContext(ctx, "DELETE FROM env_vars WHERE key = ? AND scope = ? AND scope_id = ?", key, scope, scopeID) - if err != nil { - return err - } - rowsAffected, err := result.RowsAffected() - if err != nil { - return err - } - if rowsAffected == 0 { - return store.ErrNotFound - } - return nil -} - -func (s *SQLiteStore) DeleteEnvVarsByScope(ctx context.Context, scope, scopeID string) (int, error) { - result, err := s.db.ExecContext(ctx, "DELETE FROM env_vars WHERE scope = ? AND scope_id = ?", scope, scopeID) - if err != nil { - return 0, err - } - n, err := result.RowsAffected() - if err != nil { - return 0, err - } - return int(n), nil -} - -func (s *SQLiteStore) ListEnvVars(ctx context.Context, filter store.EnvVarFilter) ([]store.EnvVar, error) { - var conditions []string - var args []interface{} - - if filter.Scope != "" { - conditions = append(conditions, "scope = ?") - args = append(args, filter.Scope) - } - if filter.ScopeID != "" { - conditions = append(conditions, "scope_id = ?") - args = append(args, filter.ScopeID) - } - if filter.Key != "" { - conditions = append(conditions, "key = ?") - args = append(args, filter.Key) - } - - whereClause := "" - if len(conditions) > 0 { - whereClause = "WHERE " + strings.Join(conditions, " AND ") - } - - query := fmt.Sprintf(` - SELECT id, key, value, scope, scope_id, description, sensitive, injection_mode, secret, created_at, updated_at, created_by - FROM env_vars %s ORDER BY key - `, whereClause) - - rows, err := s.db.QueryContext(ctx, query, args...) - if err != nil { - return nil, err - } - defer rows.Close() - - var envVars []store.EnvVar - for rows.Next() { - var envVar store.EnvVar - if err := rows.Scan( - &envVar.ID, &envVar.Key, &envVar.Value, &envVar.Scope, &envVar.ScopeID, - &envVar.Description, &envVar.Sensitive, &envVar.InjectionMode, &envVar.Secret, - &envVar.Created, &envVar.Updated, &envVar.CreatedBy, - ); err != nil { - return nil, err - } - envVars = append(envVars, envVar) - } - - return envVars, nil -} - -// ============================================================================ -// Secret Operations -// ============================================================================ - -func (s *SQLiteStore) CreateSecret(ctx context.Context, secret *store.Secret) error { - now := time.Now() - secret.Created = now - secret.Updated = now - secret.Version = 1 - - if secret.SecretType == "" { - secret.SecretType = store.SecretTypeEnvironment - } - if secret.Target == "" { - secret.Target = secret.Key - } - if secret.InjectionMode == "" { - secret.InjectionMode = store.InjectionModeAsNeeded - } - - _, err := s.db.ExecContext(ctx, ` - INSERT INTO secrets (id, key, encrypted_value, secret_ref, secret_type, target, scope, scope_id, description, injection_mode, allow_progeny, version, created_at, updated_at, created_by, updated_by) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) - `, - secret.ID, secret.Key, secret.EncryptedValue, nullableString(secret.SecretRef), - secret.SecretType, nullableString(secret.Target), - secret.Scope, secret.ScopeID, - secret.Description, secret.InjectionMode, boolToInt(secret.AllowProgeny), secret.Version, - secret.Created, secret.Updated, secret.CreatedBy, secret.UpdatedBy, - ) - if err != nil { - if strings.Contains(err.Error(), "UNIQUE constraint failed") { - return store.ErrAlreadyExists - } - return err - } - return nil -} - -func (s *SQLiteStore) GetSecret(ctx context.Context, key, scope, scopeID string) (*store.Secret, error) { - secret := &store.Secret{} - var target sql.NullString - var secretRef sql.NullString - - var allowProgeny int - err := s.db.QueryRowContext(ctx, ` - SELECT id, key, encrypted_value, secret_ref, secret_type, COALESCE(target, key), scope, scope_id, description, injection_mode, allow_progeny, version, created_at, updated_at, created_by, updated_by - FROM secrets WHERE key = ? AND scope = ? AND scope_id = ? - `, key, scope, scopeID).Scan( - &secret.ID, &secret.Key, &secret.EncryptedValue, &secretRef, - &secret.SecretType, &target, - &secret.Scope, &secret.ScopeID, - &secret.Description, &secret.InjectionMode, &allowProgeny, &secret.Version, - &secret.Created, &secret.Updated, &secret.CreatedBy, &secret.UpdatedBy, - ) - if err != nil { - if errors.Is(err, sql.ErrNoRows) { - return nil, store.ErrNotFound - } - return nil, err - } - - if target.Valid { - secret.Target = target.String - } - if secretRef.Valid { - secret.SecretRef = secretRef.String - } - secret.AllowProgeny = allowProgeny != 0 - - return secret, nil -} - -func (s *SQLiteStore) UpdateSecret(ctx context.Context, secret *store.Secret) error { - secret.Updated = time.Now() - secret.Version++ // Increment version on each update - - if secret.SecretType == "" { - secret.SecretType = store.SecretTypeEnvironment - } - if secret.Target == "" { - secret.Target = secret.Key - } - if secret.InjectionMode == "" { - secret.InjectionMode = store.InjectionModeAsNeeded - } - - result, err := s.db.ExecContext(ctx, ` - UPDATE secrets SET - encrypted_value = ?, secret_ref = ?, secret_type = ?, target = ?, description = ?, injection_mode = ?, allow_progeny = ?, version = ?, updated_at = ?, updated_by = ? - WHERE key = ? AND scope = ? AND scope_id = ? - `, - secret.EncryptedValue, nullableString(secret.SecretRef), - secret.SecretType, nullableString(secret.Target), - secret.Description, secret.InjectionMode, boolToInt(secret.AllowProgeny), secret.Version, secret.Updated, secret.UpdatedBy, - secret.Key, secret.Scope, secret.ScopeID, - ) - if err != nil { - return err - } - - rowsAffected, err := result.RowsAffected() - if err != nil { - return err - } - if rowsAffected == 0 { - return store.ErrNotFound - } - return nil -} - -func (s *SQLiteStore) UpsertSecret(ctx context.Context, secret *store.Secret) (bool, error) { - now := time.Now() - secret.Updated = now - - // Check if it already exists - existing, err := s.GetSecret(ctx, secret.Key, secret.Scope, secret.ScopeID) - if err != nil && err != store.ErrNotFound { - return false, err - } - - if existing != nil { - // Update existing - secret.ID = existing.ID - secret.Created = existing.Created - secret.CreatedBy = existing.CreatedBy - secret.Version = existing.Version // Will be incremented in UpdateSecret - if err := s.UpdateSecret(ctx, secret); err != nil { - return false, err - } - return false, nil - } - - // Create new - secret.Created = now - if err := s.CreateSecret(ctx, secret); err != nil { - return false, err - } - return true, nil -} - -func (s *SQLiteStore) DeleteSecret(ctx context.Context, key, scope, scopeID string) error { - result, err := s.db.ExecContext(ctx, "DELETE FROM secrets WHERE key = ? AND scope = ? AND scope_id = ?", key, scope, scopeID) - if err != nil { - return err - } - rowsAffected, err := result.RowsAffected() - if err != nil { - return err - } - if rowsAffected == 0 { - return store.ErrNotFound - } - return nil -} - -func (s *SQLiteStore) DeleteSecretsByScope(ctx context.Context, scope, scopeID string) (int, error) { - result, err := s.db.ExecContext(ctx, "DELETE FROM secrets WHERE scope = ? AND scope_id = ?", scope, scopeID) - if err != nil { - return 0, err - } - n, err := result.RowsAffected() - if err != nil { - return 0, err - } - return int(n), nil -} - -func (s *SQLiteStore) ListSecrets(ctx context.Context, filter store.SecretFilter) ([]store.Secret, error) { - var conditions []string - var args []interface{} - - if filter.Scope != "" { - conditions = append(conditions, "scope = ?") - args = append(args, filter.Scope) - } - if filter.ScopeID != "" { - conditions = append(conditions, "scope_id = ?") - args = append(args, filter.ScopeID) - } - if filter.Key != "" { - conditions = append(conditions, "key = ?") - args = append(args, filter.Key) - } - if filter.Type != "" { - conditions = append(conditions, "secret_type = ?") - args = append(args, filter.Type) - } - - whereClause := "" - if len(conditions) > 0 { - whereClause = "WHERE " + strings.Join(conditions, " AND ") - } - - // Note: We do NOT select encrypted_value for listing - query := fmt.Sprintf(` - SELECT id, key, secret_ref, secret_type, COALESCE(target, key), scope, scope_id, description, injection_mode, allow_progeny, version, created_at, updated_at, created_by, updated_by - FROM secrets %s ORDER BY key - `, whereClause) - - rows, err := s.db.QueryContext(ctx, query, args...) - if err != nil { - return nil, err - } - defer rows.Close() - - var secrets []store.Secret - for rows.Next() { - var secret store.Secret - var target sql.NullString - var secretRef sql.NullString - var allowProgeny int - if err := rows.Scan( - &secret.ID, &secret.Key, &secretRef, &secret.SecretType, &target, - &secret.Scope, &secret.ScopeID, - &secret.Description, &secret.InjectionMode, &allowProgeny, &secret.Version, - &secret.Created, &secret.Updated, &secret.CreatedBy, &secret.UpdatedBy, - ); err != nil { - return nil, err - } - if target.Valid { - secret.Target = target.String - } - if secretRef.Valid { - secret.SecretRef = secretRef.String - } - secret.AllowProgeny = allowProgeny != 0 - secrets = append(secrets, secret) - } - - return secrets, nil -} - -func (s *SQLiteStore) ListProgenySecrets(ctx context.Context, ancestorIDs []string) ([]store.Secret, error) { - if len(ancestorIDs) == 0 { - return nil, nil - } - - // Build placeholder list for IN clause - placeholders := make([]string, len(ancestorIDs)) - args := make([]interface{}, len(ancestorIDs)) - for i, id := range ancestorIDs { - placeholders[i] = "?" - args[i] = id - } - - query := fmt.Sprintf(` - SELECT id, key, secret_ref, secret_type, COALESCE(target, key), scope, scope_id, description, injection_mode, allow_progeny, version, created_at, updated_at, created_by, updated_by - FROM secrets - WHERE scope = 'user' AND allow_progeny = 1 AND created_by IN (%s) - ORDER BY key - `, strings.Join(placeholders, ", ")) - - rows, err := s.db.QueryContext(ctx, query, args...) - if err != nil { - return nil, err - } - defer rows.Close() - - var secrets []store.Secret - for rows.Next() { - var secret store.Secret - var target sql.NullString - var secretRef sql.NullString - var allowProgeny int - if err := rows.Scan( - &secret.ID, &secret.Key, &secretRef, &secret.SecretType, &target, - &secret.Scope, &secret.ScopeID, - &secret.Description, &secret.InjectionMode, &allowProgeny, &secret.Version, - &secret.Created, &secret.Updated, &secret.CreatedBy, &secret.UpdatedBy, - ); err != nil { - return nil, err - } - if target.Valid { - secret.Target = target.String - } - if secretRef.Valid { - secret.SecretRef = secretRef.String - } - secret.AllowProgeny = allowProgeny != 0 - secrets = append(secrets, secret) - } - - return secrets, nil -} - -func (s *SQLiteStore) GetSecretValue(ctx context.Context, key, scope, scopeID string) (string, error) { - var encryptedValue string - - err := s.db.QueryRowContext(ctx, ` - SELECT encrypted_value FROM secrets WHERE key = ? AND scope = ? AND scope_id = ? - `, key, scope, scopeID).Scan(&encryptedValue) - if err != nil { - if errors.Is(err, sql.ErrNoRows) { - return "", store.ErrNotFound - } - return "", err - } - - return encryptedValue, nil -} - -// ============================================================================ -// Group Operations -// ============================================================================ - -func (s *SQLiteStore) CreateGroup(ctx context.Context, group *store.Group) error { - now := time.Now() - group.Created = now - group.Updated = now - if group.GroupType == "" { - group.GroupType = store.GroupTypeExplicit - } - - _, err := s.db.ExecContext(ctx, ` - INSERT INTO groups (id, name, slug, description, group_type, project_id, parent_id, labels, annotations, created_at, updated_at, created_by, owner_id) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) - `, - group.ID, group.Name, group.Slug, group.Description, - group.GroupType, nullableString(group.ProjectID), - nullableString(group.ParentID), - marshalJSON(group.Labels), marshalJSON(group.Annotations), - group.Created, group.Updated, group.CreatedBy, group.OwnerID, - ) - if err != nil { - if strings.Contains(err.Error(), "UNIQUE constraint failed") { - return store.ErrAlreadyExists - } - return err - } - return nil -} - -func (s *SQLiteStore) GetGroup(ctx context.Context, id string) (*store.Group, error) { - group := &store.Group{} - var labels, annotations string - var parentID, projectID sql.NullString - - err := s.db.QueryRowContext(ctx, ` - SELECT id, name, slug, description, group_type, project_id, parent_id, labels, annotations, created_at, updated_at, created_by, owner_id - FROM groups WHERE id = ? - `, id).Scan( - &group.ID, &group.Name, &group.Slug, &group.Description, - &group.GroupType, &projectID, - &parentID, - &labels, &annotations, - &group.Created, &group.Updated, &group.CreatedBy, &group.OwnerID, - ) - if err != nil { - if errors.Is(err, sql.ErrNoRows) { - return nil, store.ErrNotFound - } - return nil, err - } - - if parentID.Valid { - group.ParentID = parentID.String - } - if projectID.Valid { - group.ProjectID = projectID.String - } - unmarshalJSON(labels, &group.Labels) - unmarshalJSON(annotations, &group.Annotations) - if group.GroupType == "" { - group.GroupType = store.GroupTypeExplicit - } - - return group, nil -} - -func (s *SQLiteStore) GetGroupBySlug(ctx context.Context, slug string) (*store.Group, error) { - var id string - err := s.db.QueryRowContext(ctx, "SELECT id FROM groups WHERE slug = ?", slug).Scan(&id) - if err != nil { - if errors.Is(err, sql.ErrNoRows) { - return nil, store.ErrNotFound - } - return nil, err - } - return s.GetGroup(ctx, id) -} - -func (s *SQLiteStore) UpdateGroup(ctx context.Context, group *store.Group) error { - group.Updated = time.Now() - - result, err := s.db.ExecContext(ctx, ` - UPDATE groups SET - name = ?, slug = ?, description = ?, group_type = ?, project_id = ?, - parent_id = ?, labels = ?, annotations = ?, - updated_at = ?, owner_id = ? - WHERE id = ? - `, - group.Name, group.Slug, group.Description, - group.GroupType, nullableString(group.ProjectID), - nullableString(group.ParentID), - marshalJSON(group.Labels), marshalJSON(group.Annotations), - group.Updated, group.OwnerID, - group.ID, - ) - if err != nil { - return err - } - - rowsAffected, err := result.RowsAffected() - if err != nil { - return err - } - if rowsAffected == 0 { - return store.ErrNotFound - } - return nil -} - -func (s *SQLiteStore) DeleteGroup(ctx context.Context, id string) error { - result, err := s.db.ExecContext(ctx, "DELETE FROM groups WHERE id = ?", id) - if err != nil { - return err - } - rowsAffected, err := result.RowsAffected() - if err != nil { - return err - } - if rowsAffected == 0 { - return store.ErrNotFound - } - return nil -} - -func (s *SQLiteStore) ListGroups(ctx context.Context, filter store.GroupFilter, opts store.ListOptions) (*store.ListResult[store.Group], error) { - var conditions []string - var args []interface{} - - if filter.OwnerID != "" { - conditions = append(conditions, "owner_id = ?") - args = append(args, filter.OwnerID) - } - if filter.ParentID != "" { - conditions = append(conditions, "parent_id = ?") - args = append(args, filter.ParentID) - } - if filter.GroupType != "" { - conditions = append(conditions, "group_type = ?") - args = append(args, filter.GroupType) - } - if filter.ProjectID != "" { - conditions = append(conditions, "project_id = ?") - args = append(args, filter.ProjectID) - } - - whereClause := "" - if len(conditions) > 0 { - whereClause = "WHERE " + strings.Join(conditions, " AND ") - } - - var totalCount int - countQuery := fmt.Sprintf("SELECT COUNT(*) FROM groups %s", whereClause) - if err := s.db.QueryRowContext(ctx, countQuery, args...).Scan(&totalCount); err != nil { - return nil, err - } - - limit := opts.Limit - if limit <= 0 { - limit = 50 - } - - query := fmt.Sprintf(` - SELECT id, name, slug, description, group_type, project_id, parent_id, labels, annotations, created_at, updated_at, created_by, owner_id - FROM groups %s ORDER BY created_at DESC LIMIT ? - `, whereClause) - args = append(args, limit) - - rows, err := s.db.QueryContext(ctx, query, args...) - if err != nil { - return nil, err - } - defer rows.Close() - - var groups []store.Group - for rows.Next() { - var group store.Group - var labels, annotations string - var parentID, projectID sql.NullString - - if err := rows.Scan( - &group.ID, &group.Name, &group.Slug, &group.Description, - &group.GroupType, &projectID, - &parentID, - &labels, &annotations, - &group.Created, &group.Updated, &group.CreatedBy, &group.OwnerID, - ); err != nil { - return nil, err - } - - if parentID.Valid { - group.ParentID = parentID.String - } - if projectID.Valid { - group.ProjectID = projectID.String - } - unmarshalJSON(labels, &group.Labels) - unmarshalJSON(annotations, &group.Annotations) - if group.GroupType == "" { - group.GroupType = store.GroupTypeExplicit - } - - groups = append(groups, group) - } - - return &store.ListResult[store.Group]{ - Items: groups, - TotalCount: totalCount, - }, nil -} - -func (s *SQLiteStore) AddGroupMember(ctx context.Context, member *store.GroupMember) error { - if member.AddedAt.IsZero() { - member.AddedAt = time.Now() - } - - _, err := s.db.ExecContext(ctx, ` - INSERT INTO group_members (group_id, member_type, member_id, role, added_at, added_by) - VALUES (?, ?, ?, ?, ?, ?) - `, - member.GroupID, member.MemberType, member.MemberID, member.Role, member.AddedAt, member.AddedBy, - ) - if err != nil { - if strings.Contains(err.Error(), "UNIQUE constraint failed") || strings.Contains(err.Error(), "PRIMARY KEY constraint failed") { - return store.ErrAlreadyExists - } - return err - } - return nil -} - -func (s *SQLiteStore) UpdateGroupMemberRole(ctx context.Context, groupID, memberType, memberID, newRole string) error { - result, err := s.db.ExecContext(ctx, - `UPDATE group_members SET role = ? WHERE group_id = ? AND member_type = ? AND member_id = ?`, - newRole, groupID, memberType, memberID, - ) - if err != nil { - return err - } - rows, _ := result.RowsAffected() - if rows == 0 { - return store.ErrNotFound - } - return nil -} - -func (s *SQLiteStore) RemoveGroupMember(ctx context.Context, groupID, memberType, memberID string) error { - result, err := s.db.ExecContext(ctx, - "DELETE FROM group_members WHERE group_id = ? AND member_type = ? AND member_id = ?", - groupID, memberType, memberID, - ) - if err != nil { - return err - } - rowsAffected, err := result.RowsAffected() - if err != nil { - return err - } - if rowsAffected == 0 { - return store.ErrNotFound - } - return nil -} - -func (s *SQLiteStore) GetGroupMembers(ctx context.Context, groupID string) ([]store.GroupMember, error) { - rows, err := s.db.QueryContext(ctx, ` - SELECT group_id, member_type, member_id, role, added_at, added_by - FROM group_members WHERE group_id = ? - `, groupID) - if err != nil { - return nil, err - } - defer rows.Close() - - var members []store.GroupMember - for rows.Next() { - var member store.GroupMember - if err := rows.Scan( - &member.GroupID, &member.MemberType, &member.MemberID, &member.Role, &member.AddedAt, &member.AddedBy, - ); err != nil { - return nil, err - } - members = append(members, member) - } - - return members, nil -} - -func (s *SQLiteStore) GetUserGroups(ctx context.Context, userID string) ([]store.GroupMember, error) { - rows, err := s.db.QueryContext(ctx, ` - SELECT group_id, member_type, member_id, role, added_at, added_by - FROM group_members WHERE member_type = 'user' AND member_id = ? - `, userID) - if err != nil { - return nil, err - } - defer rows.Close() - - var memberships []store.GroupMember - for rows.Next() { - var member store.GroupMember - if err := rows.Scan( - &member.GroupID, &member.MemberType, &member.MemberID, &member.Role, &member.AddedAt, &member.AddedBy, - ); err != nil { - return nil, err - } - memberships = append(memberships, member) - } - - return memberships, nil -} - -func (s *SQLiteStore) GetGroupMembership(ctx context.Context, groupID, memberType, memberID string) (*store.GroupMember, error) { - member := &store.GroupMember{} - - err := s.db.QueryRowContext(ctx, ` - SELECT group_id, member_type, member_id, role, added_at, added_by - FROM group_members WHERE group_id = ? AND member_type = ? AND member_id = ? - `, groupID, memberType, memberID).Scan( - &member.GroupID, &member.MemberType, &member.MemberID, &member.Role, &member.AddedAt, &member.AddedBy, - ) - if err != nil { - if errors.Is(err, sql.ErrNoRows) { - return nil, store.ErrNotFound - } - return nil, err - } - - return member, nil -} - -// WouldCreateCycle checks if adding memberGroupID as a member of groupID would create a cycle. -// A cycle exists if groupID is reachable from memberGroupID by following the containment relationship. -// Example: if A contains B, and we try to add A as member of B, we'd have A->B->A (cycle). -func (s *SQLiteStore) WouldCreateCycle(ctx context.Context, groupID, memberGroupID string) (bool, error) { - // If they're the same, it's a direct cycle - if groupID == memberGroupID { - return true, nil - } - - // Check if groupID is reachable from memberGroupID by traversing DOWN the containment graph - // (i.e., checking what groups memberGroupID contains, and what those contain, etc.) - visited := make(map[string]bool) - return s.hasPathDown(ctx, memberGroupID, groupID, visited) -} - -// hasPathDown checks if 'target' is reachable from 'current' by following containment. -// It looks at what groups 'current' contains as members. -func (s *SQLiteStore) hasPathDown(ctx context.Context, current, target string, visited map[string]bool) (bool, error) { - if current == target { - return true, nil - } - if visited[current] { - return false, nil - } - visited[current] = true - - // Get all groups that 'current' contains (groups where current is the group_id) - rows, err := s.db.QueryContext(ctx, - "SELECT member_id FROM group_members WHERE member_type = 'group' AND group_id = ?", current) - if err != nil { - return false, err - } - defer rows.Close() - - for rows.Next() { - var childGroupID string - if err := rows.Scan(&childGroupID); err != nil { - return false, err - } - found, err := s.hasPathDown(ctx, childGroupID, target, visited) - if err != nil { - return false, err - } - if found { - return true, nil - } - } - - return false, nil -} - -// GetEffectiveGroups returns all groups a user belongs to, including transitive memberships. -func (s *SQLiteStore) GetEffectiveGroups(ctx context.Context, userID string) ([]string, error) { - // Start with direct group memberships - directMemberships, err := s.GetUserGroups(ctx, userID) - if err != nil { - return nil, err - } - - effectiveGroups := make(map[string]bool) - for _, m := range directMemberships { - effectiveGroups[m.GroupID] = true - // Add transitive group memberships - if err := s.addTransitiveGroups(ctx, m.GroupID, effectiveGroups); err != nil { - return nil, err - } - } - - result := make([]string, 0, len(effectiveGroups)) - for groupID := range effectiveGroups { - result = append(result, groupID) - } - - return result, nil -} - -// addTransitiveGroups recursively adds all groups that contain the given group. -func (s *SQLiteStore) addTransitiveGroups(ctx context.Context, groupID string, visited map[string]bool) error { - // Find all groups where this group is a member - rows, err := s.db.QueryContext(ctx, - "SELECT group_id FROM group_members WHERE member_type = 'group' AND member_id = ?", groupID) - if err != nil { - return err - } - - // Collect all parent group IDs first, then close rows before recursing - // This avoids issues with SQLite connections during recursive queries - var parentGroupIDs []string - for rows.Next() { - var parentGroupID string - if err := rows.Scan(&parentGroupID); err != nil { - rows.Close() - return err - } - parentGroupIDs = append(parentGroupIDs, parentGroupID) - } - rows.Close() - - // Now recurse after rows are closed - for _, parentGroupID := range parentGroupIDs { - if !visited[parentGroupID] { - visited[parentGroupID] = true - if err := s.addTransitiveGroups(ctx, parentGroupID, visited); err != nil { - return err - } - } - } - - return nil -} - -// GetGroupByProjectID retrieves the project_agents group associated with a project. -func (s *SQLiteStore) GetGroupByProjectID(ctx context.Context, projectID string) (*store.Group, error) { - var id string - err := s.db.QueryRowContext(ctx, "SELECT id FROM groups WHERE project_id = ? AND group_type = ? LIMIT 1", - projectID, store.GroupTypeProjectAgents).Scan(&id) - if err != nil { - if errors.Is(err, sql.ErrNoRows) { - return nil, store.ErrNotFound - } - return nil, err - } - return s.GetGroup(ctx, id) -} - -// GetEffectiveGroupsForAgent returns all groups an agent belongs to. -func (s *SQLiteStore) GetEffectiveGroupsForAgent(ctx context.Context, agentID string) ([]string, error) { - return nil, nil -} - -// CheckDelegatedAccess is a stub for the SQLite store. Delegation resolution -// is implemented in the Ent adapter. -func (s *SQLiteStore) CheckDelegatedAccess(ctx context.Context, agentID string, conditions *store.PolicyConditions) (bool, error) { - return false, nil -} - -// GetGroupsByIDs is a stub for the SQLite store. Group retrieval by IDs -// is implemented in the Ent adapter. -func (s *SQLiteStore) GetGroupsByIDs(ctx context.Context, ids []string) ([]store.Group, error) { - if len(ids) == 0 { - return nil, nil - } - - placeholders := make([]string, len(ids)) - args := make([]interface{}, len(ids)) - for i, id := range ids { - placeholders[i] = "?" - args[i] = id - } - - rows, err := s.db.QueryContext(ctx, - `SELECT id, name, slug, description, group_type, project_id, parent_id, labels, annotations, created_at, updated_at, created_by, owner_id - FROM groups WHERE id IN (`+strings.Join(placeholders, ",")+`)`, args...) - if err != nil { - return nil, err - } - defer rows.Close() - - var groups []store.Group - for rows.Next() { - var g store.Group - var labels, annotations string - var parentID, projectID sql.NullString - if err := rows.Scan( - &g.ID, &g.Name, &g.Slug, &g.Description, - &g.GroupType, &projectID, - &parentID, - &labels, &annotations, - &g.Created, &g.Updated, &g.CreatedBy, &g.OwnerID, - ); err != nil { - return nil, err - } - if parentID.Valid { - g.ParentID = parentID.String - } - if projectID.Valid { - g.ProjectID = projectID.String - } - unmarshalJSON(labels, &g.Labels) - unmarshalJSON(annotations, &g.Annotations) - if g.GroupType == "" { - g.GroupType = store.GroupTypeExplicit - } - groups = append(groups, g) - } - - return groups, rows.Err() -} - -func (s *SQLiteStore) CountGroupMembersByRole(ctx context.Context, groupID, role string) (int, error) { - var count int - err := s.db.QueryRowContext(ctx, - `SELECT COUNT(*) FROM group_members WHERE group_id = ? AND role = ?`, - groupID, role, - ).Scan(&count) - if err != nil { - return 0, err - } - return count, nil -} - -// ============================================================================ -// Policy Operations -// ============================================================================ - -func (s *SQLiteStore) CreatePolicy(ctx context.Context, policy *store.Policy) error { - now := time.Now() - policy.Created = now - policy.Updated = now - - _, err := s.db.ExecContext(ctx, ` - INSERT INTO policies (id, name, description, scope_type, scope_id, resource_type, resource_id, actions, effect, conditions, priority, labels, annotations, created_at, updated_at, created_by) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) - `, - policy.ID, policy.Name, policy.Description, policy.ScopeType, policy.ScopeID, - policy.ResourceType, policy.ResourceID, - marshalJSON(policy.Actions), policy.Effect, marshalJSON(policy.Conditions), - policy.Priority, marshalJSON(policy.Labels), marshalJSON(policy.Annotations), - policy.Created, policy.Updated, policy.CreatedBy, - ) - if err != nil { - if strings.Contains(err.Error(), "UNIQUE constraint failed") { - return store.ErrAlreadyExists - } - return err - } - return nil -} - -func (s *SQLiteStore) GetPolicy(ctx context.Context, id string) (*store.Policy, error) { - policy := &store.Policy{} - var actions, conditions, labels, annotations string - - err := s.db.QueryRowContext(ctx, ` - SELECT id, name, description, scope_type, scope_id, resource_type, resource_id, actions, effect, conditions, priority, labels, annotations, created_at, updated_at, created_by - FROM policies WHERE id = ? - `, id).Scan( - &policy.ID, &policy.Name, &policy.Description, &policy.ScopeType, &policy.ScopeID, - &policy.ResourceType, &policy.ResourceID, - &actions, &policy.Effect, &conditions, - &policy.Priority, &labels, &annotations, - &policy.Created, &policy.Updated, &policy.CreatedBy, - ) - if err != nil { - if errors.Is(err, sql.ErrNoRows) { - return nil, store.ErrNotFound - } - return nil, err - } - - unmarshalJSON(actions, &policy.Actions) - unmarshalJSON(conditions, &policy.Conditions) - unmarshalJSON(labels, &policy.Labels) - unmarshalJSON(annotations, &policy.Annotations) - - return policy, nil -} - -func (s *SQLiteStore) UpdatePolicy(ctx context.Context, policy *store.Policy) error { - policy.Updated = time.Now() - - result, err := s.db.ExecContext(ctx, ` - UPDATE policies SET - name = ?, description = ?, scope_type = ?, scope_id = ?, - resource_type = ?, resource_id = ?, - actions = ?, effect = ?, conditions = ?, - priority = ?, labels = ?, annotations = ?, - updated_at = ? - WHERE id = ? - `, - policy.Name, policy.Description, policy.ScopeType, policy.ScopeID, - policy.ResourceType, policy.ResourceID, - marshalJSON(policy.Actions), policy.Effect, marshalJSON(policy.Conditions), - policy.Priority, marshalJSON(policy.Labels), marshalJSON(policy.Annotations), - policy.Updated, - policy.ID, - ) - if err != nil { - return err - } - - rowsAffected, err := result.RowsAffected() - if err != nil { - return err - } - if rowsAffected == 0 { - return store.ErrNotFound - } - return nil -} - -func (s *SQLiteStore) DeletePolicy(ctx context.Context, id string) error { - result, err := s.db.ExecContext(ctx, "DELETE FROM policies WHERE id = ?", id) - if err != nil { - return err - } - rowsAffected, err := result.RowsAffected() - if err != nil { - return err - } - if rowsAffected == 0 { - return store.ErrNotFound - } - return nil -} - -func (s *SQLiteStore) ListPolicies(ctx context.Context, filter store.PolicyFilter, opts store.ListOptions) (*store.ListResult[store.Policy], error) { - var conditions []string - var args []interface{} - - if filter.Name != "" { - conditions = append(conditions, "name = ?") - args = append(args, filter.Name) - } - if filter.ScopeType != "" { - conditions = append(conditions, "scope_type = ?") - args = append(args, filter.ScopeType) - } - if filter.ScopeID != "" { - conditions = append(conditions, "scope_id = ?") - args = append(args, filter.ScopeID) - } - if filter.ResourceType != "" { - conditions = append(conditions, "resource_type = ?") - args = append(args, filter.ResourceType) - } - if filter.Effect != "" { - conditions = append(conditions, "effect = ?") - args = append(args, filter.Effect) - } - - whereClause := "" - if len(conditions) > 0 { - whereClause = "WHERE " + strings.Join(conditions, " AND ") - } - - var totalCount int - countQuery := fmt.Sprintf("SELECT COUNT(*) FROM policies %s", whereClause) - if err := s.db.QueryRowContext(ctx, countQuery, args...).Scan(&totalCount); err != nil { - return nil, err - } - - limit := opts.Limit - if limit <= 0 { - limit = 50 - } - - query := fmt.Sprintf(` - SELECT id, name, description, scope_type, scope_id, resource_type, resource_id, actions, effect, conditions, priority, labels, annotations, created_at, updated_at, created_by - FROM policies %s ORDER BY priority DESC, created_at DESC LIMIT ? - `, whereClause) - args = append(args, limit) - - rows, err := s.db.QueryContext(ctx, query, args...) - if err != nil { - return nil, err - } - defer rows.Close() - - var policies []store.Policy - for rows.Next() { - var policy store.Policy - var actions, conditions, labels, annotations string - - if err := rows.Scan( - &policy.ID, &policy.Name, &policy.Description, &policy.ScopeType, &policy.ScopeID, - &policy.ResourceType, &policy.ResourceID, - &actions, &policy.Effect, &conditions, - &policy.Priority, &labels, &annotations, - &policy.Created, &policy.Updated, &policy.CreatedBy, - ); err != nil { - return nil, err - } - - unmarshalJSON(actions, &policy.Actions) - unmarshalJSON(conditions, &policy.Conditions) - unmarshalJSON(labels, &policy.Labels) - unmarshalJSON(annotations, &policy.Annotations) - - policies = append(policies, policy) - } - - return &store.ListResult[store.Policy]{ - Items: policies, - TotalCount: totalCount, - }, nil -} - -func (s *SQLiteStore) AddPolicyBinding(ctx context.Context, binding *store.PolicyBinding) error { - _, err := s.db.ExecContext(ctx, ` - INSERT INTO policy_bindings (policy_id, principal_type, principal_id) - VALUES (?, ?, ?) - `, - binding.PolicyID, binding.PrincipalType, binding.PrincipalID, - ) - if err != nil { - if strings.Contains(err.Error(), "UNIQUE constraint failed") || strings.Contains(err.Error(), "PRIMARY KEY constraint failed") { - return store.ErrAlreadyExists - } - return err - } - return nil -} - -func (s *SQLiteStore) RemovePolicyBinding(ctx context.Context, policyID, principalType, principalID string) error { - result, err := s.db.ExecContext(ctx, - "DELETE FROM policy_bindings WHERE policy_id = ? AND principal_type = ? AND principal_id = ?", - policyID, principalType, principalID, - ) - if err != nil { - return err - } - rowsAffected, err := result.RowsAffected() - if err != nil { - return err - } - if rowsAffected == 0 { - return store.ErrNotFound - } - return nil -} - -func (s *SQLiteStore) GetPolicyBindings(ctx context.Context, policyID string) ([]store.PolicyBinding, error) { - rows, err := s.db.QueryContext(ctx, ` - SELECT policy_id, principal_type, principal_id - FROM policy_bindings WHERE policy_id = ? - `, policyID) - if err != nil { - return nil, err - } - defer rows.Close() - - var bindings []store.PolicyBinding - for rows.Next() { - var binding store.PolicyBinding - if err := rows.Scan(&binding.PolicyID, &binding.PrincipalType, &binding.PrincipalID); err != nil { - return nil, err - } - bindings = append(bindings, binding) - } - - return bindings, nil -} - -func (s *SQLiteStore) GetPoliciesForPrincipal(ctx context.Context, principalType, principalID string) ([]store.Policy, error) { - rows, err := s.db.QueryContext(ctx, ` - SELECT p.id, p.name, p.description, p.scope_type, p.scope_id, p.resource_type, p.resource_id, p.actions, p.effect, p.conditions, p.priority, p.labels, p.annotations, p.created_at, p.updated_at, p.created_by - FROM policies p - INNER JOIN policy_bindings pb ON p.id = pb.policy_id - WHERE pb.principal_type = ? AND pb.principal_id = ? - ORDER BY p.priority DESC, p.created_at DESC - `, principalType, principalID) - if err != nil { - return nil, err - } - defer rows.Close() - - var policies []store.Policy - for rows.Next() { - var policy store.Policy - var actions, conditions, labels, annotations string - - if err := rows.Scan( - &policy.ID, &policy.Name, &policy.Description, &policy.ScopeType, &policy.ScopeID, - &policy.ResourceType, &policy.ResourceID, - &actions, &policy.Effect, &conditions, - &policy.Priority, &labels, &annotations, - &policy.Created, &policy.Updated, &policy.CreatedBy, - ); err != nil { - return nil, err - } - - unmarshalJSON(actions, &policy.Actions) - unmarshalJSON(conditions, &policy.Conditions) - unmarshalJSON(labels, &policy.Labels) - unmarshalJSON(annotations, &policy.Annotations) - - policies = append(policies, policy) - } - - return policies, nil -} - -func (s *SQLiteStore) GetPoliciesForPrincipals(ctx context.Context, principals []store.PrincipalRef) ([]store.Policy, error) { - if len(principals) == 0 { - return nil, nil - } - - // Build dynamic OR clauses for each principal - var clauses []string - var args []interface{} - for _, p := range principals { - clauses = append(clauses, "(pb.principal_type = ? AND pb.principal_id = ?)") - args = append(args, p.Type, p.ID) - } - - query := ` - SELECT DISTINCT p.id, p.name, p.description, p.scope_type, p.scope_id, p.resource_type, p.resource_id, p.actions, p.effect, p.conditions, p.priority, p.labels, p.annotations, p.created_at, p.updated_at, p.created_by - FROM policies p - INNER JOIN policy_bindings pb ON p.id = pb.policy_id - WHERE ` + strings.Join(clauses, " OR ") + ` - ORDER BY - CASE p.scope_type WHEN 'hub' THEN 0 WHEN 'project' THEN 1 WHEN 'resource' THEN 2 END, - p.priority ASC - ` - - rows, err := s.db.QueryContext(ctx, query, args...) - if err != nil { - return nil, err - } - defer rows.Close() - - var policies []store.Policy - for rows.Next() { - var policy store.Policy - var actions, conditions, labels, annotations string - - if err := rows.Scan( - &policy.ID, &policy.Name, &policy.Description, &policy.ScopeType, &policy.ScopeID, - &policy.ResourceType, &policy.ResourceID, - &actions, &policy.Effect, &conditions, - &policy.Priority, &labels, &annotations, - &policy.Created, &policy.Updated, &policy.CreatedBy, - ); err != nil { - return nil, err - } - - unmarshalJSON(actions, &policy.Actions) - unmarshalJSON(conditions, &policy.Conditions) - unmarshalJSON(labels, &policy.Labels) - unmarshalJSON(annotations, &policy.Annotations) - - policies = append(policies, policy) - } - - return policies, nil -} - -// ============================================================================ -// User Access Token Operations -// ============================================================================ - -func (s *SQLiteStore) CreateUserAccessToken(ctx context.Context, token *store.UserAccessToken) error { - _, err := s.db.ExecContext(ctx, ` - INSERT INTO user_access_tokens ( - id, user_id, name, prefix, key_hash, project_id, scopes, - revoked, expires_at, last_used, created_at - ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) - `, - token.ID, token.UserID, token.Name, token.Prefix, token.KeyHash, - token.ProjectID, marshalJSON(token.Scopes), - token.Revoked, nullableTimePtr(token.ExpiresAt), nullableTimePtr(token.LastUsed), token.Created, - ) - if err != nil { - if strings.Contains(err.Error(), "UNIQUE constraint failed") { - return store.ErrAlreadyExists - } - if strings.Contains(err.Error(), "FOREIGN KEY constraint failed") { - return store.ErrInvalidInput - } - return err - } - return nil -} - -func (s *SQLiteStore) GetUserAccessToken(ctx context.Context, id string) (*store.UserAccessToken, error) { - token := &store.UserAccessToken{} - var scopes string - var expiresAt, lastUsed sql.NullTime - - err := s.db.QueryRowContext(ctx, ` - SELECT id, user_id, name, prefix, key_hash, project_id, scopes, - revoked, expires_at, last_used, created_at - FROM user_access_tokens WHERE id = ? - `, id).Scan( - &token.ID, &token.UserID, &token.Name, &token.Prefix, &token.KeyHash, - &token.ProjectID, &scopes, - &token.Revoked, &expiresAt, &lastUsed, &token.Created, - ) - if err != nil { - if errors.Is(err, sql.ErrNoRows) { - return nil, store.ErrNotFound - } - return nil, err - } - - unmarshalJSON(scopes, &token.Scopes) - if expiresAt.Valid { - token.ExpiresAt = &expiresAt.Time - } - if lastUsed.Valid { - token.LastUsed = &lastUsed.Time - } - return token, nil -} - -func (s *SQLiteStore) GetUserAccessTokenByHash(ctx context.Context, hash string) (*store.UserAccessToken, error) { - token := &store.UserAccessToken{} - var scopes string - var expiresAt, lastUsed sql.NullTime - - err := s.db.QueryRowContext(ctx, ` - SELECT id, user_id, name, prefix, key_hash, project_id, scopes, - revoked, expires_at, last_used, created_at - FROM user_access_tokens WHERE key_hash = ? - `, hash).Scan( - &token.ID, &token.UserID, &token.Name, &token.Prefix, &token.KeyHash, - &token.ProjectID, &scopes, - &token.Revoked, &expiresAt, &lastUsed, &token.Created, - ) - if err != nil { - if errors.Is(err, sql.ErrNoRows) { - return nil, store.ErrNotFound - } - return nil, err - } - - unmarshalJSON(scopes, &token.Scopes) - if expiresAt.Valid { - token.ExpiresAt = &expiresAt.Time - } - if lastUsed.Valid { - token.LastUsed = &lastUsed.Time - } - return token, nil -} - -func (s *SQLiteStore) UpdateUserAccessTokenLastUsed(ctx context.Context, id string) error { - _, err := s.db.ExecContext(ctx, - "UPDATE user_access_tokens SET last_used = ? WHERE id = ?", - time.Now(), id, - ) - return err -} - -func (s *SQLiteStore) RevokeUserAccessToken(ctx context.Context, id string) error { - result, err := s.db.ExecContext(ctx, - "UPDATE user_access_tokens SET revoked = 1 WHERE id = ?", id, - ) - if err != nil { - return err - } - rowsAffected, err := result.RowsAffected() - if err != nil { - return err - } - if rowsAffected == 0 { - return store.ErrNotFound - } - return nil -} - -func (s *SQLiteStore) DeleteUserAccessToken(ctx context.Context, id string) error { - result, err := s.db.ExecContext(ctx, "DELETE FROM user_access_tokens WHERE id = ?", id) - if err != nil { - return err - } - rowsAffected, err := result.RowsAffected() - if err != nil { - return err - } - if rowsAffected == 0 { - return store.ErrNotFound - } - return nil -} - -func (s *SQLiteStore) ListUserAccessTokens(ctx context.Context, userID string) ([]store.UserAccessToken, error) { - rows, err := s.db.QueryContext(ctx, ` - SELECT id, user_id, name, prefix, project_id, scopes, - revoked, expires_at, last_used, created_at - FROM user_access_tokens WHERE user_id = ? - ORDER BY created_at DESC - `, userID) - if err != nil { - return nil, err - } - defer rows.Close() - - var tokens []store.UserAccessToken - for rows.Next() { - var token store.UserAccessToken - var scopes string - var expiresAt, lastUsed sql.NullTime - - if err := rows.Scan( - &token.ID, &token.UserID, &token.Name, &token.Prefix, - &token.ProjectID, &scopes, - &token.Revoked, &expiresAt, &lastUsed, &token.Created, - ); err != nil { - return nil, err - } - - unmarshalJSON(scopes, &token.Scopes) - if expiresAt.Valid { - token.ExpiresAt = &expiresAt.Time - } - if lastUsed.Valid { - token.LastUsed = &lastUsed.Time - } - tokens = append(tokens, token) - } - return tokens, nil -} - -func (s *SQLiteStore) CountUserAccessTokens(ctx context.Context, userID string) (int, error) { - var count int - err := s.db.QueryRowContext(ctx, - "SELECT COUNT(*) FROM user_access_tokens WHERE user_id = ? AND revoked = 0", - userID, - ).Scan(&count) - return count, err -} - -// nullableTimePtr returns a sql.NullTime for a time pointer. -func nullableTimePtr(t *time.Time) sql.NullTime { - if t == nil { - return sql.NullTime{Valid: false} - } - return sql.NullTime{Time: *t, Valid: true} -} - -// Ensure SQLiteStore implements Store interface -var _ store.Store = (*SQLiteStore)(nil) diff --git a/pkg/store/sqlite/sqlite_test.go b/pkg/store/sqlite/sqlite_test.go deleted file mode 100644 index 3c588e1da..000000000 --- a/pkg/store/sqlite/sqlite_test.go +++ /dev/null @@ -1,2927 +0,0 @@ -// Copyright 2026 Google LLC -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -//go:build !no_sqlite - -package sqlite - -import ( - "context" - "database/sql" - "fmt" - "strings" - "testing" - "time" - - "github.com/GoogleCloudPlatform/scion/pkg/agent/state" - "github.com/GoogleCloudPlatform/scion/pkg/api" - "github.com/GoogleCloudPlatform/scion/pkg/store" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -func setupTestStore(t *testing.T) *SQLiteStore { - t.Helper() - s, err := New(":memory:") - require.NoError(t, err) - - err = s.Migrate(context.Background()) - require.NoError(t, err) - - t.Cleanup(func() { - s.Close() - }) - - return s -} - -// ============================================================================ -// Agent Tests -// ============================================================================ - -func TestAgentCRUD(t *testing.T) { - s := setupTestStore(t) - ctx := context.Background() - - // First create a project for the agent - project := &store.Project{ - ID: api.NewUUID(), - Name: "Test Project", - Slug: "test-project", - Visibility: store.VisibilityPrivate, - } - require.NoError(t, s.CreateProject(ctx, project)) - - // Create agent - agent := &store.Agent{ - ID: api.NewUUID(), - Slug: "test-agent", - Name: "Test Agent", - Template: "claude", - ProjectID: project.ID, - Phase: string(state.PhaseCreated), - Visibility: store.VisibilityPrivate, - Labels: map[string]string{"env": "test"}, - } - - err := s.CreateAgent(ctx, agent) - require.NoError(t, err) - assert.NotZero(t, agent.Created) - assert.Equal(t, int64(1), agent.StateVersion) - - // Get agent - retrieved, err := s.GetAgent(ctx, agent.ID) - require.NoError(t, err) - assert.Equal(t, agent.ID, retrieved.ID) - assert.Equal(t, agent.Slug, retrieved.Slug) - assert.Equal(t, agent.Name, retrieved.Name) - assert.Equal(t, agent.Template, retrieved.Template) - assert.Equal(t, "test", retrieved.Labels["env"]) - - // Get by slug - retrieved, err = s.GetAgentBySlug(ctx, project.ID, "test-agent") - require.NoError(t, err) - assert.Equal(t, agent.ID, retrieved.ID) - - // Update agent - retrieved.Name = "Updated Agent" - retrieved.Phase = string(state.PhaseRunning) - err = s.UpdateAgent(ctx, retrieved) - require.NoError(t, err) - assert.Equal(t, int64(2), retrieved.StateVersion) - - // Verify update - retrieved, err = s.GetAgent(ctx, agent.ID) - require.NoError(t, err) - assert.Equal(t, "Updated Agent", retrieved.Name) - assert.Equal(t, string(state.PhaseRunning), retrieved.Phase) - - // Test version conflict - oldVersion := retrieved.StateVersion - retrieved.StateVersion = 1 // Use old version - err = s.UpdateAgent(ctx, retrieved) - assert.ErrorIs(t, err, store.ErrVersionConflict) - - // Restore correct version for delete - retrieved.StateVersion = oldVersion - - // Delete agent - err = s.DeleteAgent(ctx, agent.ID) - require.NoError(t, err) - - // Verify deleted - _, err = s.GetAgent(ctx, agent.ID) - assert.ErrorIs(t, err, store.ErrNotFound) -} - -func TestAgentList(t *testing.T) { - s := setupTestStore(t) - ctx := context.Background() - - // Create project - project := &store.Project{ - ID: api.NewUUID(), - Name: "Test Project", - Slug: "test-project", - Visibility: store.VisibilityPrivate, - } - require.NoError(t, s.CreateProject(ctx, project)) - - // Create multiple agents - for i := 0; i < 5; i++ { - agent := &store.Agent{ - ID: api.NewUUID(), - Slug: api.Slugify("agent-" + string(rune('a'+i))), - Name: "Agent " + string(rune('A'+i)), - Template: "claude", - ProjectID: project.ID, - Phase: string(state.PhaseRunning), - Visibility: store.VisibilityPrivate, - } - if i%2 == 0 { - agent.Phase = string(state.PhaseStopped) - } - require.NoError(t, s.CreateAgent(ctx, agent)) - } - - // List all - result, err := s.ListAgents(ctx, store.AgentFilter{}, store.ListOptions{}) - require.NoError(t, err) - assert.Equal(t, 5, result.TotalCount) - assert.Len(t, result.Items, 5) - - // List by status - result, err = s.ListAgents(ctx, store.AgentFilter{Phase: string(state.PhaseRunning)}, store.ListOptions{}) - require.NoError(t, err) - assert.Equal(t, 2, result.TotalCount) - - // List by project - result, err = s.ListAgents(ctx, store.AgentFilter{ProjectID: project.ID}, store.ListOptions{}) - require.NoError(t, err) - assert.Equal(t, 5, result.TotalCount) - - // Test pagination - result, err = s.ListAgents(ctx, store.AgentFilter{}, store.ListOptions{Limit: 2}) - require.NoError(t, err) - assert.Len(t, result.Items, 2) -} - -func TestAgentAncestry(t *testing.T) { - s := setupTestStore(t) - ctx := context.Background() - - project := &store.Project{ - ID: api.NewUUID(), Name: "Ancestry Project", Slug: "ancestry-project", - Visibility: store.VisibilityPrivate, - } - require.NoError(t, s.CreateProject(ctx, project)) - - userID := "user-root-123" - - // Agent A: created by user (ancestry = [userID]) - agentA := &store.Agent{ - ID: api.NewUUID(), Slug: "agent-a", Name: "Agent A", - Template: "claude", ProjectID: project.ID, - Phase: string(state.PhaseRunning), Visibility: store.VisibilityPrivate, - CreatedBy: userID, OwnerID: userID, - Ancestry: []string{userID}, - } - require.NoError(t, s.CreateAgent(ctx, agentA)) - - // Agent B: created by Agent A (ancestry = [userID, agentA.ID]) - agentB := &store.Agent{ - ID: api.NewUUID(), Slug: "agent-b", Name: "Agent B", - Template: "claude", ProjectID: project.ID, - Phase: string(state.PhaseRunning), Visibility: store.VisibilityPrivate, - CreatedBy: agentA.ID, OwnerID: agentA.ID, - Ancestry: []string{userID, agentA.ID}, - } - require.NoError(t, s.CreateAgent(ctx, agentB)) - - // Agent C: created by Agent B (ancestry = [userID, agentA.ID, agentB.ID]) - agentC := &store.Agent{ - ID: api.NewUUID(), Slug: "agent-c", Name: "Agent C", - Template: "claude", ProjectID: project.ID, - Phase: string(state.PhaseRunning), Visibility: store.VisibilityPrivate, - CreatedBy: agentB.ID, OwnerID: agentB.ID, - Ancestry: []string{userID, agentA.ID, agentB.ID}, - } - require.NoError(t, s.CreateAgent(ctx, agentC)) - - // Verify ancestry is persisted and retrieved correctly - t.Run("GetAgent preserves ancestry", func(t *testing.T) { - retrieved, err := s.GetAgent(ctx, agentC.ID) - require.NoError(t, err) - assert.Equal(t, []string{userID, agentA.ID, agentB.ID}, retrieved.Ancestry) - }) - - t.Run("GetAgentBySlug preserves ancestry", func(t *testing.T) { - retrieved, err := s.GetAgentBySlug(ctx, project.ID, "agent-b") - require.NoError(t, err) - assert.Equal(t, []string{userID, agentA.ID}, retrieved.Ancestry) - }) - - t.Run("ListAgents preserves ancestry", func(t *testing.T) { - result, err := s.ListAgents(ctx, store.AgentFilter{ProjectID: project.ID}, store.ListOptions{}) - require.NoError(t, err) - assert.Len(t, result.Items, 3) - for _, agent := range result.Items { - assert.NotEmpty(t, agent.Ancestry, "agent %s should have ancestry", agent.Slug) - } - }) - - t.Run("AncestorID filter - user sees all descendants", func(t *testing.T) { - result, err := s.ListAgents(ctx, store.AgentFilter{AncestorID: userID}, store.ListOptions{}) - require.NoError(t, err) - assert.Equal(t, 3, result.TotalCount) - }) - - t.Run("AncestorID filter - agentA sees B and C", func(t *testing.T) { - result, err := s.ListAgents(ctx, store.AgentFilter{AncestorID: agentA.ID}, store.ListOptions{}) - require.NoError(t, err) - assert.Equal(t, 2, result.TotalCount) - }) - - t.Run("AncestorID filter - agentB sees only C", func(t *testing.T) { - result, err := s.ListAgents(ctx, store.AgentFilter{AncestorID: agentB.ID}, store.ListOptions{}) - require.NoError(t, err) - assert.Equal(t, 1, result.TotalCount) - assert.Equal(t, agentC.ID, result.Items[0].ID) - }) - - t.Run("AncestorID filter - agentC sees none", func(t *testing.T) { - result, err := s.ListAgents(ctx, store.AgentFilter{AncestorID: agentC.ID}, store.ListOptions{}) - require.NoError(t, err) - assert.Equal(t, 0, result.TotalCount) - }) - - t.Run("AncestorID filter - unknown user sees none", func(t *testing.T) { - result, err := s.ListAgents(ctx, store.AgentFilter{AncestorID: "unknown-user"}, store.ListOptions{}) - require.NoError(t, err) - assert.Equal(t, 0, result.TotalCount) - }) - - t.Run("nil ancestry persists as empty", func(t *testing.T) { - agentNoAnc := &store.Agent{ - ID: api.NewUUID(), Slug: "agent-no-anc", Name: "No Ancestry", - Template: "claude", ProjectID: project.ID, - Phase: string(state.PhaseRunning), Visibility: store.VisibilityPrivate, - } - require.NoError(t, s.CreateAgent(ctx, agentNoAnc)) - retrieved, err := s.GetAgent(ctx, agentNoAnc.ID) - require.NoError(t, err) - assert.Nil(t, retrieved.Ancestry) - }) - - t.Run("NULL ancestry column does not crash scan", func(t *testing.T) { - // Create agent normally, then set ancestry to NULL to simulate pre-migration state - agentNullAnc := &store.Agent{ - ID: api.NewUUID(), Slug: "agent-null-anc", Name: "Null Ancestry", - Template: "claude", ProjectID: project.ID, - Phase: string(state.PhaseRunning), Visibility: store.VisibilityPrivate, - Ancestry: []string{"some-user"}, - } - require.NoError(t, s.CreateAgent(ctx, agentNullAnc)) - _, err := s.db.ExecContext(ctx, `UPDATE agents SET ancestry = NULL WHERE id = ?`, agentNullAnc.ID) - require.NoError(t, err) - agentID := agentNullAnc.ID - - retrieved, err := s.GetAgent(ctx, agentID) - require.NoError(t, err) - assert.Nil(t, retrieved.Ancestry) - - retrievedBySlug, err := s.GetAgentBySlug(ctx, project.ID, "agent-null-anc") - require.NoError(t, err) - assert.Nil(t, retrievedBySlug.Ancestry) - - result, err := s.ListAgents(ctx, store.AgentFilter{ProjectID: project.ID}, store.ListOptions{}) - require.NoError(t, err) - assert.True(t, result.TotalCount > 0) - }) -} - -func TestAgentStatusUpdate(t *testing.T) { - s := setupTestStore(t) - ctx := context.Background() - - // Create project and agent - project := &store.Project{ - ID: api.NewUUID(), - Name: "Test Project", - Slug: "test-project", - Visibility: store.VisibilityPrivate, - } - require.NoError(t, s.CreateProject(ctx, project)) - - agent := &store.Agent{ - ID: api.NewUUID(), - Slug: "test-agent", - Name: "Test Agent", - Template: "claude", - ProjectID: project.ID, - Phase: string(state.PhaseCreated), - Visibility: store.VisibilityPrivate, - } - require.NoError(t, s.CreateAgent(ctx, agent)) - - // Legacy path: update flat status only (backward compat) - err := s.UpdateAgentStatus(ctx, agent.ID, store.AgentStatusUpdate{ - Phase: string(state.PhaseRunning), - ContainerStatus: "Up 5 minutes", - }) - require.NoError(t, err) - - retrieved, err := s.GetAgent(ctx, agent.ID) - require.NoError(t, err) - assert.Equal(t, string(state.PhaseRunning), retrieved.Phase) - assert.Equal(t, "Up 5 minutes", retrieved.ContainerStatus) - - // Structured path: set phase + activity - err = s.UpdateAgentStatus(ctx, agent.ID, store.AgentStatusUpdate{ - Phase: "running", - Activity: "thinking", - }) - require.NoError(t, err) - - retrieved, err = s.GetAgent(ctx, agent.ID) - require.NoError(t, err) - assert.Equal(t, "running", retrieved.Phase) - assert.Equal(t, "thinking", retrieved.Activity) - - // Set activity=executing with toolName - err = s.UpdateAgentStatus(ctx, agent.ID, store.AgentStatusUpdate{ - Phase: "running", - Activity: "executing", - ToolName: "Bash", - }) - require.NoError(t, err) - - retrieved, err = s.GetAgent(ctx, agent.ID) - require.NoError(t, err) - assert.Equal(t, "executing", retrieved.Activity) - assert.Equal(t, "Bash", retrieved.ToolName) - - // Change activity from executing to working → toolName is cleared - err = s.UpdateAgentStatus(ctx, agent.ID, store.AgentStatusUpdate{ - Phase: "running", - Activity: "working", - }) - require.NoError(t, err) - - retrieved, err = s.GetAgent(ctx, agent.ID) - require.NoError(t, err) - assert.Equal(t, "working", retrieved.Activity) - assert.Equal(t, "", retrieved.ToolName, "toolName should be cleared when activity changes away from executing") - - // Set only activity (phase preserved from previous update) - err = s.UpdateAgentStatus(ctx, agent.ID, store.AgentStatusUpdate{ - Activity: "waiting_for_input", - }) - require.NoError(t, err) - - retrieved, err = s.GetAgent(ctx, agent.ID) - require.NoError(t, err) - assert.Equal(t, "running", retrieved.Phase, "phase should be preserved") - assert.Equal(t, "waiting_for_input", retrieved.Activity) - - // Non-running phase - err = s.UpdateAgentStatus(ctx, agent.ID, store.AgentStatusUpdate{ - Phase: "stopped", - }) - require.NoError(t, err) - - retrieved, err = s.GetAgent(ctx, agent.ID) - require.NoError(t, err) - assert.Equal(t, "stopped", retrieved.Phase) -} - -func TestAgentStatusUpdate_PhaseActivityRoundTrip(t *testing.T) { - s := setupTestStore(t) - ctx := context.Background() - - project := &store.Project{ - ID: api.NewUUID(), - Name: "Test Project", - Slug: "test-project-rt", - Visibility: store.VisibilityPrivate, - } - require.NoError(t, s.CreateProject(ctx, project)) - - // Create agent with initial phase/activity - agent := &store.Agent{ - ID: api.NewUUID(), - Slug: "roundtrip-agent", - Name: "Roundtrip Agent", - Template: "claude", - ProjectID: project.ID, - Phase: "running", - Activity: "working", - Visibility: store.VisibilityPrivate, - } - require.NoError(t, s.CreateAgent(ctx, agent)) - - // Verify round-trip through Get - retrieved, err := s.GetAgent(ctx, agent.ID) - require.NoError(t, err) - assert.Equal(t, "running", retrieved.Phase) - assert.Equal(t, "working", retrieved.Activity) - - // Verify round-trip through GetBySlug - retrieved, err = s.GetAgentBySlug(ctx, project.ID, "roundtrip-agent") - require.NoError(t, err) - assert.Equal(t, "running", retrieved.Phase) - assert.Equal(t, "working", retrieved.Activity) - - // Verify round-trip through List - result, err := s.ListAgents(ctx, store.AgentFilter{ProjectID: project.ID}, store.ListOptions{}) - require.NoError(t, err) - require.Len(t, result.Items, 1) - assert.Equal(t, "running", result.Items[0].Phase) - assert.Equal(t, "working", result.Items[0].Activity) -} - -func TestSoftDeleteFilterExclusion(t *testing.T) { - s := setupTestStore(t) - ctx := context.Background() - - // Create project - project := &store.Project{ - ID: api.NewUUID(), - Name: "Test Project", - Slug: "test-project-sd", - Visibility: store.VisibilityPrivate, - } - require.NoError(t, s.CreateProject(ctx, project)) - - // Create 3 agents: 2 running, 1 soft-deleted - for i := 0; i < 3; i++ { - agent := &store.Agent{ - ID: api.NewUUID(), - Slug: api.Slugify("sd-agent-" + string(rune('a'+i))), - Name: "SD Agent " + string(rune('A'+i)), - Template: "claude", - ProjectID: project.ID, - Phase: string(state.PhaseRunning), - Visibility: store.VisibilityPrivate, - } - if i == 2 { - agent.DeletedAt = time.Now() - } - require.NoError(t, s.CreateAgent(ctx, agent)) - } - - // List without IncludeDeleted: should see 2 - result, err := s.ListAgents(ctx, store.AgentFilter{}, store.ListOptions{}) - require.NoError(t, err) - assert.Equal(t, 2, result.TotalCount) - assert.Len(t, result.Items, 2) - for _, a := range result.Items { - assert.True(t, a.DeletedAt.IsZero(), "non-deleted agent should have zero DeletedAt") - } - - // List with IncludeDeleted: should see 3 - result, err = s.ListAgents(ctx, store.AgentFilter{IncludeDeleted: true}, store.ListOptions{}) - require.NoError(t, err) - assert.Equal(t, 3, result.TotalCount) - assert.Len(t, result.Items, 3) - - // List with IncludeDeleted: should see all 3 (including the deleted one) - // Verify we can find the soft-deleted agent - var deletedCount int - for _, a := range result.Items { - if !a.DeletedAt.IsZero() { - deletedCount++ - } - } - assert.Equal(t, 1, deletedCount, "should have exactly one soft-deleted agent") -} - -func TestPurgeDeletedAgents(t *testing.T) { - s := setupTestStore(t) - ctx := context.Background() - - project := &store.Project{ - ID: api.NewUUID(), - Name: "Test Project", - Slug: "test-project-purge", - Visibility: store.VisibilityPrivate, - } - require.NoError(t, s.CreateProject(ctx, project)) - - now := time.Now() - - // Create 2 deleted agents: one expired (old), one recent - oldAgent := &store.Agent{ - ID: api.NewUUID(), - Slug: "old-deleted", - Name: "Old Deleted", - Template: "claude", - ProjectID: project.ID, - Phase: string(state.PhaseStopped), - DeletedAt: now.Add(-48 * time.Hour), - Visibility: store.VisibilityPrivate, - } - recentAgent := &store.Agent{ - ID: api.NewUUID(), - Slug: "recent-deleted", - Name: "Recent Deleted", - Template: "claude", - ProjectID: project.ID, - Phase: string(state.PhaseStopped), - DeletedAt: now.Add(-1 * time.Hour), - Visibility: store.VisibilityPrivate, - } - activeAgent := &store.Agent{ - ID: api.NewUUID(), - Slug: "active-agent", - Name: "Active Agent", - Template: "claude", - ProjectID: project.ID, - Phase: string(state.PhaseRunning), - Visibility: store.VisibilityPrivate, - } - require.NoError(t, s.CreateAgent(ctx, oldAgent)) - require.NoError(t, s.CreateAgent(ctx, recentAgent)) - require.NoError(t, s.CreateAgent(ctx, activeAgent)) - - // Purge with cutoff of 24h ago: should only purge the old one - cutoff := now.Add(-24 * time.Hour) - purged, err := s.PurgeDeletedAgents(ctx, cutoff) - require.NoError(t, err) - assert.Equal(t, 1, purged) - - // Old agent should be gone - _, err = s.GetAgent(ctx, oldAgent.ID) - assert.ErrorIs(t, err, store.ErrNotFound) - - // Recent deleted agent should still exist - _, err = s.GetAgent(ctx, recentAgent.ID) - require.NoError(t, err) - - // Active agent should still exist - _, err = s.GetAgent(ctx, activeAgent.ID) - require.NoError(t, err) -} - -func TestDeletedAtPersistence(t *testing.T) { - s := setupTestStore(t) - ctx := context.Background() - - project := &store.Project{ - ID: api.NewUUID(), - Name: "Test Project", - Slug: "test-project-dat", - Visibility: store.VisibilityPrivate, - } - require.NoError(t, s.CreateProject(ctx, project)) - - // Create and soft-delete an agent - agent := &store.Agent{ - ID: api.NewUUID(), - Slug: "soft-del-test", - Name: "Soft Delete Test", - Template: "claude", - ProjectID: project.ID, - Phase: string(state.PhaseRunning), - Visibility: store.VisibilityPrivate, - } - require.NoError(t, s.CreateAgent(ctx, agent)) - - // Verify DeletedAt is zero initially - retrieved, err := s.GetAgent(ctx, agent.ID) - require.NoError(t, err) - assert.True(t, retrieved.DeletedAt.IsZero()) - - // Soft-delete - deletedAt := time.Now().Truncate(time.Second) - retrieved.DeletedAt = deletedAt - retrieved.Updated = time.Now() - require.NoError(t, s.UpdateAgent(ctx, retrieved)) - - // Retrieve and verify DeletedAt is set - retrieved2, err := s.GetAgent(ctx, agent.ID) - require.NoError(t, err) - assert.False(t, retrieved2.DeletedAt.IsZero(), "soft-deleted agent should have non-zero DeletedAt") - assert.WithinDuration(t, deletedAt, retrieved2.DeletedAt, time.Second) - - // Verify GetAgentBySlug also returns DeletedAt - bySlug, err := s.GetAgentBySlug(ctx, project.ID, "soft-del-test") - require.NoError(t, err) - assert.False(t, bySlug.DeletedAt.IsZero(), "soft-deleted agent fetched by slug should have non-zero DeletedAt") - - // Verify restore clears DeletedAt - bySlug.DeletedAt = time.Time{} - bySlug.Updated = time.Now() - require.NoError(t, s.UpdateAgent(ctx, bySlug)) - - restored, err := s.GetAgent(ctx, agent.ID) - require.NoError(t, err) - assert.True(t, restored.DeletedAt.IsZero(), "restored agent should have zero DeletedAt") -} - -// ============================================================================ -// Project Tests -// ============================================================================ - -func TestProjectCRUD(t *testing.T) { - s := setupTestStore(t) - ctx := context.Background() - - // Create project - project := &store.Project{ - ID: api.NewUUID(), - Name: "My Project", - Slug: "my-project", - GitRemote: "github.com/org/repo", - Visibility: store.VisibilityPrivate, - Labels: map[string]string{"team": "platform"}, - } - - err := s.CreateProject(ctx, project) - require.NoError(t, err) - assert.NotZero(t, project.Created) - - // Get project - retrieved, err := s.GetProject(ctx, project.ID) - require.NoError(t, err) - assert.Equal(t, project.Name, retrieved.Name) - assert.Equal(t, project.GitRemote, retrieved.GitRemote) - assert.Equal(t, "platform", retrieved.Labels["team"]) - - // Get by slug - retrieved, err = s.GetProjectBySlug(ctx, "my-project") - require.NoError(t, err) - assert.Equal(t, project.ID, retrieved.ID) - - // Get by git remote (plural) - projects, err := s.GetProjectsByGitRemote(ctx, "github.com/org/repo") - require.NoError(t, err) - require.Len(t, projects, 1) - assert.Equal(t, project.ID, projects[0].ID) - - // Duplicate git remotes are now allowed (slug must still be unique) - duplicate := &store.Project{ - ID: api.NewUUID(), - Name: "Duplicate", - Slug: "duplicate", - GitRemote: "github.com/org/repo", - Visibility: store.VisibilityPrivate, - } - err = s.CreateProject(ctx, duplicate) - require.NoError(t, err) - - // Verify both projects are returned - projects, err = s.GetProjectsByGitRemote(ctx, "github.com/org/repo") - require.NoError(t, err) - assert.Len(t, projects, 2) - - // Update project - retrieved.Name = "Updated Project" - err = s.UpdateProject(ctx, retrieved) - require.NoError(t, err) - - // Verify update - retrieved, err = s.GetProject(ctx, project.ID) - require.NoError(t, err) - assert.Equal(t, "Updated Project", retrieved.Name) - - // Delete project - err = s.DeleteProject(ctx, project.ID) - require.NoError(t, err) - - // Verify deleted - _, err = s.GetProject(ctx, project.ID) - assert.ErrorIs(t, err, store.ErrNotFound) -} - -func TestMultiProjectPerGitRemote(t *testing.T) { - s := setupTestStore(t) - ctx := context.Background() - - remote := "github.com/acme/widgets" - - // Create 3 projects with the same git remote but different slugs - slugs := []string{"acme-widgets", "acme-widgets-1", "acme-widgets-2"} - for i, slug := range slugs { - project := &store.Project{ - ID: api.NewUUID(), - Name: fmt.Sprintf("acme-widgets project %d", i), - Slug: slug, - GitRemote: remote, - Visibility: store.VisibilityPrivate, - } - require.NoError(t, s.CreateProject(ctx, project)) - } - - projects, err := s.GetProjectsByGitRemote(ctx, remote) - require.NoError(t, err) - assert.Len(t, projects, 3) - - // Verify ordering is by created_at ASC - assert.Equal(t, "acme-widgets", projects[0].Slug) - assert.Equal(t, "acme-widgets-1", projects[1].Slug) - assert.Equal(t, "acme-widgets-2", projects[2].Slug) -} - -func TestGetProjectsByGitRemoteEmpty(t *testing.T) { - s := setupTestStore(t) - ctx := context.Background() - - projects, err := s.GetProjectsByGitRemote(ctx, "github.com/nonexistent/repo") - require.NoError(t, err) - assert.Empty(t, projects) -} - -func TestSlugUniqueness(t *testing.T) { - s := setupTestStore(t) - ctx := context.Background() - - project1 := &store.Project{ - ID: api.NewUUID(), Name: "Test", Slug: "test-project", - Visibility: store.VisibilityPrivate, - } - require.NoError(t, s.CreateProject(ctx, project1)) - - // Duplicate slug should fail - project2 := &store.Project{ - ID: api.NewUUID(), Name: "Test 2", Slug: "test-project", - Visibility: store.VisibilityPrivate, - } - err := s.CreateProject(ctx, project2) - assert.ErrorIs(t, err, store.ErrAlreadyExists) -} - -func TestNextAvailableSlug(t *testing.T) { - s := setupTestStore(t) - ctx := context.Background() - - // Base slug available - slug, err := s.NextAvailableSlug(ctx, "acme-widgets") - require.NoError(t, err) - assert.Equal(t, "acme-widgets", slug) - - // Create the base slug - require.NoError(t, s.CreateProject(ctx, &store.Project{ - ID: api.NewUUID(), Name: "acme-widgets", Slug: "acme-widgets", - Visibility: store.VisibilityPrivate, - })) - - // Should get -1 - slug, err = s.NextAvailableSlug(ctx, "acme-widgets") - require.NoError(t, err) - assert.Equal(t, "acme-widgets-1", slug) - - // Create -1 - require.NoError(t, s.CreateProject(ctx, &store.Project{ - ID: api.NewUUID(), Name: "acme-widgets (1)", Slug: "acme-widgets-1", - Visibility: store.VisibilityPrivate, - })) - - // Should get -2 - slug, err = s.NextAvailableSlug(ctx, "acme-widgets") - require.NoError(t, err) - assert.Equal(t, "acme-widgets-2", slug) -} - -func TestGetInstallationForRepository(t *testing.T) { - s := setupTestStore(t) - ctx := context.Background() - - // Create an installation with repos - inst := &store.GitHubInstallation{ - InstallationID: 12345, - AccountLogin: "acme", - AccountType: "Organization", - AppID: 100, - Repositories: []string{"acme/widgets", "acme/gizmos"}, - Status: store.GitHubInstallationStatusActive, - } - require.NoError(t, s.CreateGitHubInstallation(ctx, inst)) - - // Look up by repo - found, err := s.GetInstallationForRepository(ctx, "acme/widgets") - require.NoError(t, err) - assert.Equal(t, int64(12345), found.InstallationID) - assert.Contains(t, found.Repositories, "acme/widgets") - - // Look up non-existent repo - _, err = s.GetInstallationForRepository(ctx, "acme/nonexistent") - assert.ErrorIs(t, err, store.ErrNotFound) - - // Suspended installation should not match - inst2 := &store.GitHubInstallation{ - InstallationID: 67890, - AccountLogin: "other", - AccountType: "User", - AppID: 100, - Repositories: []string{"other/project"}, - Status: store.GitHubInstallationStatusSuspended, - } - require.NoError(t, s.CreateGitHubInstallation(ctx, inst2)) - - _, err = s.GetInstallationForRepository(ctx, "other/project") - assert.ErrorIs(t, err, store.ErrNotFound) -} - -func TestDisplayNameWithSerial(t *testing.T) { - assert.Equal(t, "acme-widgets", api.DisplayNameWithSerial("acme-widgets", "acme-widgets", "acme-widgets")) - assert.Equal(t, "acme-widgets (1)", api.DisplayNameWithSerial("acme-widgets", "acme-widgets-1", "acme-widgets")) - assert.Equal(t, "acme-widgets (2)", api.DisplayNameWithSerial("acme-widgets", "acme-widgets-2", "acme-widgets")) - assert.Equal(t, "My Project (3)", api.DisplayNameWithSerial("My Project", "my-project-3", "my-project")) -} - -func TestProjectList(t *testing.T) { - s := setupTestStore(t) - ctx := context.Background() - - // Create a broker for ActiveBrokerCount - broker := &store.RuntimeBroker{ - ID: api.NewUUID(), - Name: "Test Broker", - Slug: "test-broker", - Status: store.BrokerStatusOnline, - } - require.NoError(t, s.CreateRuntimeBroker(ctx, broker)) - - // Create projects - for i := 0; i < 3; i++ { - project := &store.Project{ - ID: api.NewUUID(), - Name: "Project " + string(rune('A'+i)), - Slug: "project-" + string(rune('a'+i)), - Visibility: store.VisibilityPrivate, - } - if i == 0 { - project.Visibility = store.VisibilityPublic - } - require.NoError(t, s.CreateProject(ctx, project)) - - // Add an agent to the first project - if i == 0 { - agent := &store.Agent{ - ID: api.NewUUID(), - Slug: "test-agent", - Name: "Test Agent", - ProjectID: project.ID, - Phase: string(state.PhaseRunning), - } - require.NoError(t, s.CreateAgent(ctx, agent)) - - // Link the broker to the first project - require.NoError(t, s.AddProjectProvider(ctx, &store.ProjectProvider{ - ProjectID: project.ID, - BrokerID: broker.ID, - BrokerName: broker.Name, - Status: store.BrokerStatusOnline, - })) - } - } - - // List all - result, err := s.ListProjects(ctx, store.ProjectFilter{}, store.ListOptions{}) - require.NoError(t, err) - assert.Equal(t, 3, result.TotalCount) - - // Verify computed fields on the first project (index 2 due to DESC sort by created_at) - var firstProject store.Project - for _, g := range result.Items { - if g.Name == "Project A" { - firstProject = g - break - } - } - assert.Equal(t, 1, firstProject.AgentCount) - assert.Equal(t, 1, firstProject.ActiveBrokerCount) - - // List by visibility - result, err = s.ListProjects(ctx, store.ProjectFilter{Visibility: store.VisibilityPublic}, store.ListOptions{}) - require.NoError(t, err) - assert.Equal(t, 1, result.TotalCount) - assert.Equal(t, "Project A", result.Items[0].Name) -} - -// ============================================================================ -// RuntimeBroker Tests -// ============================================================================ - -func TestProjectLookupCaseInsensitive(t *testing.T) { - s := setupTestStore(t) - ctx := context.Background() - - // Create a project with mixed case name - project := &store.Project{ - ID: api.NewUUID(), - Name: "Global", - Slug: "global", - Visibility: store.VisibilityPrivate, - } - require.NoError(t, s.CreateProject(ctx, project)) - - // Look up with exact case - should work - retrieved, err := s.GetProjectBySlugCaseInsensitive(ctx, "global") - require.NoError(t, err) - assert.Equal(t, project.ID, retrieved.ID) - - // Look up with different case - should still work - retrieved, err = s.GetProjectBySlugCaseInsensitive(ctx, "GLOBAL") - require.NoError(t, err) - assert.Equal(t, project.ID, retrieved.ID) - - // Look up with mixed case - should still work - retrieved, err = s.GetProjectBySlugCaseInsensitive(ctx, "Global") - require.NoError(t, err) - assert.Equal(t, project.ID, retrieved.ID) - - // Look up non-existent - should return ErrNotFound - _, err = s.GetProjectBySlugCaseInsensitive(ctx, "nonexistent") - assert.ErrorIs(t, err, store.ErrNotFound) -} - -func TestProjectListBySlug(t *testing.T) { - s := setupTestStore(t) - ctx := context.Background() - - // Create two projects with distinct slugs - project1 := &store.Project{ - ID: api.NewUUID(), - Name: "Alpha Project", - Slug: "alpha-project", - Visibility: store.VisibilityPrivate, - } - project2 := &store.Project{ - ID: api.NewUUID(), - Name: "Beta Project", - Slug: "beta-project", - Visibility: store.VisibilityPrivate, - } - require.NoError(t, s.CreateProject(ctx, project1)) - require.NoError(t, s.CreateProject(ctx, project2)) - - // Filter by slug — exact match - result, err := s.ListProjects(ctx, store.ProjectFilter{Slug: "alpha-project"}, store.ListOptions{}) - require.NoError(t, err) - assert.Equal(t, 1, result.TotalCount) - assert.Equal(t, project1.ID, result.Items[0].ID) - - // Filter by slug — case-insensitive - result, err = s.ListProjects(ctx, store.ProjectFilter{Slug: "ALPHA-PROJECT"}, store.ListOptions{}) - require.NoError(t, err) - assert.Equal(t, 1, result.TotalCount) - assert.Equal(t, project1.ID, result.Items[0].ID) - - // Filter by slug — no match - result, err = s.ListProjects(ctx, store.ProjectFilter{Slug: "nonexistent"}, store.ListOptions{}) - require.NoError(t, err) - assert.Equal(t, 0, result.TotalCount) -} - -func TestListProjectsByGitRemoteExactMatch(t *testing.T) { - s := setupTestStore(t) - ctx := context.Background() - - project1 := &store.Project{ - ID: api.NewUUID(), - Name: "Repo", - Slug: "repo", - GitRemote: "github.com/org/repo", - Visibility: store.VisibilityPrivate, - } - project2 := &store.Project{ - ID: api.NewUUID(), - Name: "Repo Clone", - Slug: "repo-clone", - GitRemote: "github.com/org/repo-clone", - Visibility: store.VisibilityPrivate, - } - require.NoError(t, s.CreateProject(ctx, project1)) - require.NoError(t, s.CreateProject(ctx, project2)) - - // Exact match should return only the exact project, not the one with repo-clone - result, err := s.ListProjects(ctx, store.ProjectFilter{GitRemote: "github.com/org/repo"}, store.ListOptions{}) - require.NoError(t, err) - assert.Equal(t, 1, result.TotalCount) - assert.Equal(t, project1.ID, result.Items[0].ID) - - // Exact match on the clone URL should return only that project - result, err = s.ListProjects(ctx, store.ProjectFilter{GitRemote: "github.com/org/repo-clone"}, store.ListOptions{}) - require.NoError(t, err) - assert.Equal(t, 1, result.TotalCount) - assert.Equal(t, project2.ID, result.Items[0].ID) -} - -func TestListProjectsSharedScope(t *testing.T) { - s := setupTestStore(t) - ctx := context.Background() - - ownerID := api.NewUUID() - otherOwnerID := api.NewUUID() - - ownedProject := &store.Project{ - ID: api.NewUUID(), - Name: "Owned Project", - Slug: "owned-project", - OwnerID: ownerID, - Visibility: store.VisibilityPrivate, - } - sharedProject := &store.Project{ - ID: api.NewUUID(), - Name: "Shared Project", - Slug: "shared-project", - OwnerID: otherOwnerID, - Visibility: store.VisibilityPrivate, - } - unrelatedProject := &store.Project{ - ID: api.NewUUID(), - Name: "Unrelated Project", - Slug: "unrelated-project", - OwnerID: otherOwnerID, - Visibility: store.VisibilityPrivate, - } - require.NoError(t, s.CreateProject(ctx, ownedProject)) - require.NoError(t, s.CreateProject(ctx, sharedProject)) - require.NoError(t, s.CreateProject(ctx, unrelatedProject)) - - // scope=mine: only projects owned by the user - result, err := s.ListProjects(ctx, store.ProjectFilter{OwnerID: ownerID}, store.ListOptions{}) - require.NoError(t, err) - assert.Equal(t, 1, result.TotalCount) - assert.Equal(t, ownedProject.ID, result.Items[0].ID) - - // scope=shared: MemberProjectIDs includes both owned and shared project IDs, - // but ExcludeOwnerID removes the owned one - result, err = s.ListProjects(ctx, store.ProjectFilter{ - MemberProjectIDs: []string{ownedProject.ID, sharedProject.ID}, - ExcludeOwnerID: ownerID, - }, store.ListOptions{}) - require.NoError(t, err) - assert.Equal(t, 1, result.TotalCount) - assert.Equal(t, sharedProject.ID, result.Items[0].ID) - - // MemberProjectIDs without ExcludeOwnerID returns all matched projects - result, err = s.ListProjects(ctx, store.ProjectFilter{ - MemberProjectIDs: []string{ownedProject.ID, sharedProject.ID}, - }, store.ListOptions{}) - require.NoError(t, err) - assert.Equal(t, 2, result.TotalCount) - - // Empty MemberProjectIDs with ExcludeOwnerID is a no-op on membership filter - result, err = s.ListProjects(ctx, store.ProjectFilter{ - ExcludeOwnerID: ownerID, - }, store.ListOptions{}) - require.NoError(t, err) - // Returns all projects not owned by ownerID - assert.Equal(t, 2, result.TotalCount) -} - -func TestListProjectsSharedScopeTransitiveGroup(t *testing.T) { - s := setupTestStore(t) - ctx := context.Background() - - userID := "user_transitive" - otherOwnerID := api.NewUUID() - - // Create a project owned by someone else - sharedProject := &store.Project{ - ID: api.NewUUID(), - Name: "Transitively Shared Project", - Slug: "trans-shared-project", - OwnerID: otherOwnerID, - Visibility: store.VisibilityPrivate, - } - require.NoError(t, s.CreateProject(ctx, sharedProject)) - - // Create a project_agents group linked to the project (simulates the project - // membership group that is created when a project gains members). - projectGroup := &store.Group{ - ID: api.NewUUID(), - Name: "Project Agents", - Slug: "project-agents-trans", - GroupType: "project_agents", - ProjectID: sharedProject.ID, - Created: time.Now(), - Updated: time.Now(), - } - // Create an intermediate parent group that is a member of the project group - parentGroup := &store.Group{ - ID: api.NewUUID(), - Name: "Team Alpha", - Slug: "team-alpha", - Created: time.Now(), - Updated: time.Now(), - } - // Create the child group the user is a direct member of - childGroup := &store.Group{ - ID: api.NewUUID(), - Name: "Sub-Team", - Slug: "sub-team", - Created: time.Now(), - Updated: time.Now(), - } - - for _, g := range []*store.Group{projectGroup, parentGroup, childGroup} { - require.NoError(t, s.CreateGroup(ctx, g)) - } - - // parentGroup is a member of projectGroup (admin access to the project) - require.NoError(t, s.AddGroupMember(ctx, &store.GroupMember{ - GroupID: projectGroup.ID, - MemberType: "group", - MemberID: parentGroup.ID, - Role: "admin", - AddedAt: time.Now(), - })) - - // childGroup is a member of parentGroup - require.NoError(t, s.AddGroupMember(ctx, &store.GroupMember{ - GroupID: parentGroup.ID, - MemberType: "group", - MemberID: childGroup.ID, - Role: "member", - AddedAt: time.Now(), - })) - - // User is a direct member of childGroup only - require.NoError(t, s.AddGroupMember(ctx, &store.GroupMember{ - GroupID: childGroup.ID, - MemberType: "user", - MemberID: userID, - Role: "member", - AddedAt: time.Now(), - })) - - // GetEffectiveGroups should return all three groups (child, parent, project) - effectiveGroupIDs, err := s.GetEffectiveGroups(ctx, userID) - require.NoError(t, err) - assert.Len(t, effectiveGroupIDs, 3) - - // Resolve project IDs from effective groups (mirrors resolveUserProjectIDs) - groups, err := s.GetGroupsByIDs(ctx, effectiveGroupIDs) - require.NoError(t, err) - - var projectIDs []string - for _, g := range groups { - if g.ProjectID != "" { - projectIDs = append(projectIDs, g.ProjectID) - } - } - require.Len(t, projectIDs, 1, "should find project via transitive group membership") - assert.Equal(t, sharedProject.ID, projectIDs[0]) - - // Using the resolved project IDs in a shared scope filter should return the project - result, err := s.ListProjects(ctx, store.ProjectFilter{ - MemberProjectIDs: projectIDs, - ExcludeOwnerID: userID, - }, store.ListOptions{}) - require.NoError(t, err) - assert.Equal(t, 1, result.TotalCount) - assert.Equal(t, sharedProject.ID, result.Items[0].ID) -} - -func TestRuntimeBrokerLookupByName(t *testing.T) { - s := setupTestStore(t) - ctx := context.Background() - - // Create a broker - broker := &store.RuntimeBroker{ - ID: api.NewUUID(), - Name: "My-Laptop", - Slug: "my-laptop", - Status: store.BrokerStatusOnline, - } - require.NoError(t, s.CreateRuntimeBroker(ctx, broker)) - - // Look up with exact case - should work - retrieved, err := s.GetRuntimeBrokerByName(ctx, "My-Laptop") - require.NoError(t, err) - assert.Equal(t, broker.ID, retrieved.ID) - - // Look up with different case - should still work (case-insensitive) - retrieved, err = s.GetRuntimeBrokerByName(ctx, "my-laptop") - require.NoError(t, err) - assert.Equal(t, broker.ID, retrieved.ID) - - // Look up with all caps - should still work - retrieved, err = s.GetRuntimeBrokerByName(ctx, "MY-LAPTOP") - require.NoError(t, err) - assert.Equal(t, broker.ID, retrieved.ID) - - // Look up non-existent - should return ErrNotFound - _, err = s.GetRuntimeBrokerByName(ctx, "nonexistent") - assert.ErrorIs(t, err, store.ErrNotFound) -} - -// ============================================================================ -// RuntimeBroker Tests -// ============================================================================ - -func TestRuntimeBrokerCRUD(t *testing.T) { - s := setupTestStore(t) - ctx := context.Background() - - // Create broker with CreatedBy tracking - broker := &store.RuntimeBroker{ - ID: api.NewUUID(), - Name: "Dev Laptop", - Slug: "dev-laptop", - Version: "1.0.0", - Status: store.BrokerStatusOnline, - Capabilities: &store.BrokerCapabilities{ - WebPTY: true, - Sync: true, - Attach: true, - }, - Profiles: []store.BrokerProfile{ - {Name: "default", Type: "docker", Available: true}, - }, - CreatedBy: "admin-user-456", - } - - err := s.CreateRuntimeBroker(ctx, broker) - require.NoError(t, err) - assert.NotZero(t, broker.Created) - - // Get broker - retrieved, err := s.GetRuntimeBroker(ctx, broker.ID) - require.NoError(t, err) - assert.Equal(t, broker.Name, retrieved.Name) - assert.True(t, retrieved.Capabilities.WebPTY) - assert.Len(t, retrieved.Profiles, 1) - assert.Equal(t, "docker", retrieved.Profiles[0].Type) - assert.Equal(t, "admin-user-456", retrieved.CreatedBy) - - // Update broker - retrieved.Status = store.BrokerStatusOffline - err = s.UpdateRuntimeBroker(ctx, retrieved) - require.NoError(t, err) - - // Verify update - retrieved, err = s.GetRuntimeBroker(ctx, broker.ID) - require.NoError(t, err) - assert.Equal(t, store.BrokerStatusOffline, retrieved.Status) - - // Update heartbeat - err = s.UpdateRuntimeBrokerHeartbeat(ctx, broker.ID, store.BrokerStatusOnline) - require.NoError(t, err) - - // Verify heartbeat - retrieved, err = s.GetRuntimeBroker(ctx, broker.ID) - require.NoError(t, err) - assert.Equal(t, store.BrokerStatusOnline, retrieved.Status) - assert.NotZero(t, retrieved.LastHeartbeat) - - // Delete broker - err = s.DeleteRuntimeBroker(ctx, broker.ID) - require.NoError(t, err) - - _, err = s.GetRuntimeBroker(ctx, broker.ID) - assert.ErrorIs(t, err, store.ErrNotFound) -} - -func TestRuntimeBrokerList(t *testing.T) { - s := setupTestStore(t) - ctx := context.Background() - - // Create brokers - for i := 0; i < 3; i++ { - broker := &store.RuntimeBroker{ - ID: api.NewUUID(), - Name: "Host " + string(rune('A'+i)), - Slug: "host-" + string(rune('a'+i)), - Status: store.BrokerStatusOnline, - Profiles: []store.BrokerProfile{ - {Name: "default", Type: "docker", Available: true}, - }, - } - if i == 0 { - broker.Status = store.BrokerStatusOffline - } - require.NoError(t, s.CreateRuntimeBroker(ctx, broker)) - } - - // List all - result, err := s.ListRuntimeBrokers(ctx, store.RuntimeBrokerFilter{}, store.ListOptions{}) - require.NoError(t, err) - assert.Equal(t, 3, result.TotalCount) - - // List by status - result, err = s.ListRuntimeBrokers(ctx, store.RuntimeBrokerFilter{Status: store.BrokerStatusOffline}, store.ListOptions{}) - require.NoError(t, err) - assert.Equal(t, 1, result.TotalCount) - - // List by name (exact match, case-insensitive) - result, err = s.ListRuntimeBrokers(ctx, store.RuntimeBrokerFilter{Name: "Host A"}, store.ListOptions{}) - require.NoError(t, err) - assert.Equal(t, 1, result.TotalCount) - assert.Equal(t, "Host A", result.Items[0].Name) - - // List by name (case-insensitive) - result, err = s.ListRuntimeBrokers(ctx, store.RuntimeBrokerFilter{Name: "host b"}, store.ListOptions{}) - require.NoError(t, err) - assert.Equal(t, 1, result.TotalCount) - assert.Equal(t, "Host B", result.Items[0].Name) - - // List by name (no match) - result, err = s.ListRuntimeBrokers(ctx, store.RuntimeBrokerFilter{Name: "nonexistent"}, store.ListOptions{}) - require.NoError(t, err) - assert.Equal(t, 0, result.TotalCount) -} - -func TestRuntimeBrokerListByProjectIncludesAutoProvide(t *testing.T) { - s := setupTestStore(t) - ctx := context.Background() - - // Create a project - project := &store.Project{ - ID: "project-autoprovide-test", - Slug: "autoprovide-test", - Name: "AutoProvide Test", - Created: time.Now(), - Updated: time.Now(), - } - require.NoError(t, s.CreateProject(ctx, project)) - - // Create a regular broker explicitly linked to the project - linkedBroker := &store.RuntimeBroker{ - ID: "broker-linked", - Name: "Linked Broker", - Slug: "linked-broker", - Status: store.BrokerStatusOnline, - } - require.NoError(t, s.CreateRuntimeBroker(ctx, linkedBroker)) - require.NoError(t, s.AddProjectProvider(ctx, &store.ProjectProvider{ - ProjectID: project.ID, - BrokerID: linkedBroker.ID, - BrokerName: linkedBroker.Name, - Status: store.BrokerStatusOnline, - })) - - // Create an auto-provide broker (NOT explicitly linked to the project) - autoBroker := &store.RuntimeBroker{ - ID: "broker-auto", - Name: "Auto Broker", - Slug: "auto-broker", - Status: store.BrokerStatusOnline, - AutoProvide: true, - } - require.NoError(t, s.CreateRuntimeBroker(ctx, autoBroker)) - - // Create a regular broker NOT linked to the project - unlinkedBroker := &store.RuntimeBroker{ - ID: "broker-unlinked", - Name: "Unlinked Broker", - Slug: "unlinked-broker", - Status: store.BrokerStatusOnline, - } - require.NoError(t, s.CreateRuntimeBroker(ctx, unlinkedBroker)) - - // List brokers for the project — should include linked + auto-provide, but not unlinked - result, err := s.ListRuntimeBrokers(ctx, store.RuntimeBrokerFilter{ProjectID: project.ID}, store.ListOptions{}) - require.NoError(t, err) - assert.Equal(t, 2, result.TotalCount) - - ids := make(map[string]bool) - for _, b := range result.Items { - ids[b.ID] = true - } - assert.True(t, ids["broker-linked"], "linked broker should be included") - assert.True(t, ids["broker-auto"], "auto-provide broker should be included") - assert.False(t, ids["broker-unlinked"], "unlinked broker should not be included") -} - -// ============================================================================ -// Template Tests -// ============================================================================ - -func TestTemplateCRUD(t *testing.T) { - s := setupTestStore(t) - ctx := context.Background() - - // Create template - template := &store.Template{ - ID: api.NewUUID(), - Name: "Claude Default", - Slug: "claude-default", - Harness: "claude", - Image: "scion-claude:latest", - Scope: "global", - Visibility: store.VisibilityPublic, - Config: &store.TemplateConfig{ - Harness: "claude", - Detached: true, - }, - } - - err := s.CreateTemplate(ctx, template) - require.NoError(t, err) - assert.NotZero(t, template.Created) - - // Get template - retrieved, err := s.GetTemplate(ctx, template.ID) - require.NoError(t, err) - assert.Equal(t, template.Name, retrieved.Name) - assert.Equal(t, template.Harness, retrieved.Harness) - assert.True(t, retrieved.Config.Detached) - - // Get by slug - retrieved, err = s.GetTemplateBySlug(ctx, "claude-default", "global", "") - require.NoError(t, err) - assert.Equal(t, template.ID, retrieved.ID) - - // Update template - retrieved.Image = "scion-claude:v2" - err = s.UpdateTemplate(ctx, retrieved) - require.NoError(t, err) - - // Verify update - retrieved, err = s.GetTemplate(ctx, template.ID) - require.NoError(t, err) - assert.Equal(t, "scion-claude:v2", retrieved.Image) - - // Delete template - err = s.DeleteTemplate(ctx, template.ID) - require.NoError(t, err) - - _, err = s.GetTemplate(ctx, template.ID) - assert.ErrorIs(t, err, store.ErrNotFound) -} - -func TestTemplateList(t *testing.T) { - s := setupTestStore(t) - ctx := context.Background() - - // Create templates - for i := 0; i < 3; i++ { - template := &store.Template{ - ID: api.NewUUID(), - Name: "Template " + string(rune('A'+i)), - Slug: "template-" + string(rune('a'+i)), - Harness: "claude", - Scope: "global", - Visibility: store.VisibilityPublic, - } - if i == 0 { - template.Harness = "gemini" - } - require.NoError(t, s.CreateTemplate(ctx, template)) - } - - // List all - result, err := s.ListTemplates(ctx, store.TemplateFilter{}, store.ListOptions{}) - require.NoError(t, err) - assert.Equal(t, 3, result.TotalCount) - - // List by harness - result, err = s.ListTemplates(ctx, store.TemplateFilter{Harness: "gemini"}, store.ListOptions{}) - require.NoError(t, err) - assert.Equal(t, 1, result.TotalCount) -} - -// ============================================================================ -// HarnessConfig Tests -// ============================================================================ - -func TestHarnessConfigCRUD(t *testing.T) { - s := setupTestStore(t) - ctx := context.Background() - - // Create harness config - hc := &store.HarnessConfig{ - ID: api.NewUUID(), - Name: "Claude Default", - Slug: "claude-default", - Harness: "claude", - Scope: "global", - Visibility: store.VisibilityPublic, - Config: &store.HarnessConfigData{ - Harness: "claude", - Image: "scion-claude:latest", - }, - } - - err := s.CreateHarnessConfig(ctx, hc) - require.NoError(t, err) - assert.NotZero(t, hc.Created) - - // Get harness config - retrieved, err := s.GetHarnessConfig(ctx, hc.ID) - require.NoError(t, err) - assert.Equal(t, hc.Name, retrieved.Name) - assert.Equal(t, hc.Harness, retrieved.Harness) - assert.Equal(t, "claude", retrieved.Config.Harness) - assert.Equal(t, "scion-claude:latest", retrieved.Config.Image) - - // Get by slug - retrieved, err = s.GetHarnessConfigBySlug(ctx, "claude-default", "global", "") - require.NoError(t, err) - assert.Equal(t, hc.ID, retrieved.ID) - - // Update harness config - retrieved.Description = "Updated description" - err = s.UpdateHarnessConfig(ctx, retrieved) - require.NoError(t, err) - - // Verify update - retrieved, err = s.GetHarnessConfig(ctx, hc.ID) - require.NoError(t, err) - assert.Equal(t, "Updated description", retrieved.Description) - - // Delete harness config - err = s.DeleteHarnessConfig(ctx, hc.ID) - require.NoError(t, err) - - _, err = s.GetHarnessConfig(ctx, hc.ID) - assert.ErrorIs(t, err, store.ErrNotFound) -} - -func TestHarnessConfigList(t *testing.T) { - s := setupTestStore(t) - ctx := context.Background() - - // Create harness configs - for i := 0; i < 3; i++ { - hc := &store.HarnessConfig{ - ID: api.NewUUID(), - Name: "HC " + string(rune('A'+i)), - Slug: "hc-" + string(rune('a'+i)), - Harness: "claude", - Scope: "global", - Visibility: store.VisibilityPublic, - } - if i == 0 { - hc.Harness = "gemini" - } - require.NoError(t, s.CreateHarnessConfig(ctx, hc)) - } - - // List all - result, err := s.ListHarnessConfigs(ctx, store.HarnessConfigFilter{}, store.ListOptions{}) - require.NoError(t, err) - assert.Equal(t, 3, result.TotalCount) - - // List by harness - result, err = s.ListHarnessConfigs(ctx, store.HarnessConfigFilter{Harness: "gemini"}, store.ListOptions{}) - require.NoError(t, err) - assert.Equal(t, 1, result.TotalCount) -} - -func TestHarnessConfigListDeduplication(t *testing.T) { - s := setupTestStore(t) - ctx := context.Background() - projectID := "project-dedup-test" - - globalHC := &store.HarnessConfig{ - ID: api.NewUUID(), - Name: "gemini", - Slug: "gemini", - Harness: "gemini", - Scope: "global", - } - projectHC := &store.HarnessConfig{ - ID: api.NewUUID(), - Name: "gemini", - Slug: "gemini", - Harness: "gemini", - Scope: "project", - ScopeID: projectID, - } - globalOnly := &store.HarnessConfig{ - ID: api.NewUUID(), - Name: "claude", - Slug: "claude", - Harness: "claude", - Scope: "global", - } - projectOnly := &store.HarnessConfig{ - ID: api.NewUUID(), - Name: "opencode", - Slug: "opencode", - Harness: "opencode", - Scope: "project", - ScopeID: projectID, - } - - for _, hc := range []*store.HarnessConfig{globalHC, projectHC, globalOnly, projectOnly} { - require.NoError(t, s.CreateHarnessConfig(ctx, hc)) - } - - // Without ProjectID filter: returns all 4 records (no dedup) - result, err := s.ListHarnessConfigs(ctx, store.HarnessConfigFilter{}, store.ListOptions{}) - require.NoError(t, err) - assert.Equal(t, 4, result.TotalCount) - - // With ProjectID filter: deduplicates, preferring project-scoped - result, err = s.ListHarnessConfigs(ctx, store.HarnessConfigFilter{ProjectID: projectID}, store.ListOptions{}) - require.NoError(t, err) - assert.Equal(t, 3, result.TotalCount) - - slugs := map[string]string{} - for _, hc := range result.Items { - slugs[hc.Slug] = hc.Scope - } - assert.Equal(t, "project", slugs["gemini"], "project-scoped should win over global") - assert.Equal(t, "global", slugs["claude"], "global-only config should still appear") - assert.Equal(t, "project", slugs["opencode"], "project-only config should still appear") -} - -// ============================================================================ -// User Tests -// ============================================================================ - -func TestUserCRUD(t *testing.T) { - s := setupTestStore(t) - ctx := context.Background() - - // Create user - user := &store.User{ - ID: api.NewUUID(), - Email: "test@example.com", - DisplayName: "Test User", - Role: store.UserRoleMember, - Status: "active", - Preferences: &store.UserPreferences{ - Theme: "dark", - }, - } - - err := s.CreateUser(ctx, user) - require.NoError(t, err) - assert.NotZero(t, user.Created) - - // Get user - retrieved, err := s.GetUser(ctx, user.ID) - require.NoError(t, err) - assert.Equal(t, user.Email, retrieved.Email) - assert.Equal(t, "dark", retrieved.Preferences.Theme) - - // Get by email - retrieved, err = s.GetUserByEmail(ctx, "test@example.com") - require.NoError(t, err) - assert.Equal(t, user.ID, retrieved.ID) - - // Test unique constraint on email - duplicate := &store.User{ - ID: api.NewUUID(), - Email: "test@example.com", - DisplayName: "Duplicate User", - Role: store.UserRoleMember, - Status: "active", - } - err = s.CreateUser(ctx, duplicate) - assert.ErrorIs(t, err, store.ErrAlreadyExists) - - // Update user - retrieved.DisplayName = "Updated User" - retrieved.LastLogin = time.Now() - err = s.UpdateUser(ctx, retrieved) - require.NoError(t, err) - - // Verify update - retrieved, err = s.GetUser(ctx, user.ID) - require.NoError(t, err) - assert.Equal(t, "Updated User", retrieved.DisplayName) - assert.NotZero(t, retrieved.LastLogin) - - // Delete user - err = s.DeleteUser(ctx, user.ID) - require.NoError(t, err) - - _, err = s.GetUser(ctx, user.ID) - assert.ErrorIs(t, err, store.ErrNotFound) -} - -func TestUserList(t *testing.T) { - s := setupTestStore(t) - ctx := context.Background() - - // Create users - for i := 0; i < 3; i++ { - user := &store.User{ - ID: api.NewUUID(), - Email: "user" + string(rune('a'+i)) + "@example.com", - DisplayName: "User " + string(rune('A'+i)), - Role: store.UserRoleMember, - Status: "active", - } - if i == 0 { - user.Role = store.UserRoleAdmin - } - require.NoError(t, s.CreateUser(ctx, user)) - } - - // List all - result, err := s.ListUsers(ctx, store.UserFilter{}, store.ListOptions{}) - require.NoError(t, err) - assert.Equal(t, 3, result.TotalCount) - - // List by role - result, err = s.ListUsers(ctx, store.UserFilter{Role: store.UserRoleAdmin}, store.ListOptions{}) - require.NoError(t, err) - assert.Equal(t, 1, result.TotalCount) -} - -func TestUserListSorting(t *testing.T) { - s := setupTestStore(t) - ctx := context.Background() - - // Create users with distinct names so sort order is deterministic - names := []string{"Charlie", "Alice", "Bob"} - for _, name := range names { - user := &store.User{ - ID: api.NewUUID(), - Email: strings.ToLower(name) + "@example.com", - DisplayName: name, - Role: store.UserRoleMember, - Status: "active", - } - require.NoError(t, s.CreateUser(ctx, user)) - } - - // Sort by name ascending - result, err := s.ListUsers(ctx, store.UserFilter{}, store.ListOptions{SortBy: "name", SortDir: "asc"}) - require.NoError(t, err) - require.Len(t, result.Items, 3) - assert.Equal(t, "Alice", result.Items[0].DisplayName) - assert.Equal(t, "Bob", result.Items[1].DisplayName) - assert.Equal(t, "Charlie", result.Items[2].DisplayName) - - // Sort by name descending - result, err = s.ListUsers(ctx, store.UserFilter{}, store.ListOptions{SortBy: "name", SortDir: "desc"}) - require.NoError(t, err) - require.Len(t, result.Items, 3) - assert.Equal(t, "Charlie", result.Items[0].DisplayName) - assert.Equal(t, "Bob", result.Items[1].DisplayName) - assert.Equal(t, "Alice", result.Items[2].DisplayName) - - // Sort by created descending (default) — most recent first - result, err = s.ListUsers(ctx, store.UserFilter{}, store.ListOptions{SortBy: "created", SortDir: "desc"}) - require.NoError(t, err) - require.Len(t, result.Items, 3) - // Last created should be first - assert.Equal(t, "Bob", result.Items[0].DisplayName) - - // Sorting should work across pages: page 1 with limit 2, sorted by name asc - result, err = s.ListUsers(ctx, store.UserFilter{}, store.ListOptions{Limit: 2, SortBy: "name", SortDir: "asc"}) - require.NoError(t, err) - require.Len(t, result.Items, 2) - assert.Equal(t, "Alice", result.Items[0].DisplayName) - assert.Equal(t, "Bob", result.Items[1].DisplayName) - assert.NotEmpty(t, result.NextCursor) - - // Page 2 should have the remaining user - result, err = s.ListUsers(ctx, store.UserFilter{}, store.ListOptions{Limit: 2, Cursor: result.NextCursor, SortBy: "name", SortDir: "asc"}) - require.NoError(t, err) - require.Len(t, result.Items, 1) - assert.Equal(t, "Charlie", result.Items[0].DisplayName) - assert.Empty(t, result.NextCursor) -} - -// ============================================================================ -// ProjectProvider Tests -// ============================================================================ - -func TestProjectProviders(t *testing.T) { - s := setupTestStore(t) - ctx := context.Background() - - // Create project - project := &store.Project{ - ID: api.NewUUID(), - Name: "Test Project", - Slug: "test-project", - Visibility: store.VisibilityPrivate, - } - require.NoError(t, s.CreateProject(ctx, project)) - - // Create brokers - broker1 := &store.RuntimeBroker{ - ID: api.NewUUID(), - Name: "Host 1", - Slug: "host-1", - Status: store.BrokerStatusOnline, - Profiles: []store.BrokerProfile{ - {Name: "docker", Type: "docker", Available: true}, - {Name: "dev", Type: "docker", Available: true}, - }, - } - require.NoError(t, s.CreateRuntimeBroker(ctx, broker1)) - - broker2 := &store.RuntimeBroker{ - ID: api.NewUUID(), - Name: "Host 2", - Slug: "host-2", - Status: store.BrokerStatusOnline, - Profiles: []store.BrokerProfile{ - {Name: "k8s-prod", Type: "kubernetes", Available: true}, - }, - } - require.NoError(t, s.CreateRuntimeBroker(ctx, broker2)) - - // Add providers with user tracking - provider1 := &store.ProjectProvider{ - ProjectID: project.ID, - BrokerID: broker1.ID, - BrokerName: broker1.Name, - Status: store.BrokerStatusOnline, - LinkedBy: "user-123", - } - require.NoError(t, s.AddProjectProvider(ctx, provider1)) - - provider2 := &store.ProjectProvider{ - ProjectID: project.ID, - BrokerID: broker2.ID, - BrokerName: broker2.Name, - Status: store.BrokerStatusOnline, - } - require.NoError(t, s.AddProjectProvider(ctx, provider2)) - - // Get project providers - providers, err := s.GetProjectProviders(ctx, project.ID) - require.NoError(t, err) - assert.Len(t, providers, 2) - - // Verify user tracking fields are stored - for _, p := range providers { - if p.BrokerID == broker1.ID { - assert.Equal(t, "user-123", p.LinkedBy) - assert.False(t, p.LinkedAt.IsZero(), "LinkedAt should be set") - } - } - - // Verify GetProjectProvider also returns user tracking fields - provider, err := s.GetProjectProvider(ctx, project.ID, broker1.ID) - require.NoError(t, err) - assert.Equal(t, "user-123", provider.LinkedBy) - assert.False(t, provider.LinkedAt.IsZero(), "LinkedAt should be set") - - // Get broker projects - projects, err := s.GetBrokerProjects(ctx, broker1.ID) - require.NoError(t, err) - assert.Len(t, projects, 1) - assert.Equal(t, project.ID, projects[0].ProjectID) - - // Update provider status - err = s.UpdateProviderStatus(ctx, project.ID, broker1.ID, store.BrokerStatusOffline) - require.NoError(t, err) - - // Verify update - providers, err = s.GetProjectProviders(ctx, project.ID) - require.NoError(t, err) - for _, p := range providers { - if p.BrokerID == broker1.ID { - assert.Equal(t, store.BrokerStatusOffline, p.Status) - } - } - - // Verify project's active broker count - retrievedProject, err := s.GetProject(ctx, project.ID) - require.NoError(t, err) - assert.Equal(t, 1, retrievedProject.ActiveBrokerCount) // Only broker2 is online - - // Remove provider - err = s.RemoveProjectProvider(ctx, project.ID, broker1.ID) - require.NoError(t, err) - - providers, err = s.GetProjectProviders(ctx, project.ID) - require.NoError(t, err) - assert.Len(t, providers, 1) - assert.Equal(t, broker2.ID, providers[0].BrokerID) -} - -// ============================================================================ -// Migration Tests -// ============================================================================ - -func TestMigration(t *testing.T) { - s, err := New(":memory:") - require.NoError(t, err) - defer s.Close() - - ctx := context.Background() - - // Run migrations - err = s.Migrate(ctx) - require.NoError(t, err) - - // Run again (should be idempotent) - err = s.Migrate(ctx) - require.NoError(t, err) - - // Verify tables exist by inserting data - project := &store.Project{ - ID: api.NewUUID(), - Name: "Test", - Slug: "test", - Visibility: store.VisibilityPrivate, - } - err = s.CreateProject(ctx, project) - require.NoError(t, err) -} - -func TestDropTableCascadesWithForeignKeysOn(t *testing.T) { - // Demonstrates the root cause: DROP TABLE with foreign_keys=ON - // triggers ON DELETE CASCADE, removing all child rows. - s, err := New(":memory:") - require.NoError(t, err) - defer s.Close() - - ctx := context.Background() - err = s.Migrate(ctx) - require.NoError(t, err) - - projectID := api.NewUUID() - err = s.CreateProject(ctx, &store.Project{ - ID: projectID, Name: "G", Slug: "g-cascade-test", Visibility: store.VisibilityPrivate, - }) - require.NoError(t, err) - - agentID := api.NewUUID() - err = s.CreateAgent(ctx, &store.Agent{ - ID: agentID, Slug: "a", Name: "A", ProjectID: projectID, Visibility: store.VisibilityPrivate, - }) - require.NoError(t, err) - - // With foreign_keys ON (default), DROP TABLE cascades - _, err = s.db.ExecContext(ctx, ` - CREATE TABLE projects_copy AS SELECT * FROM projects; - DROP TABLE projects; - ALTER TABLE projects_copy RENAME TO projects; - `) - require.NoError(t, err) - - // Agent was cascade-deleted — this is the bug V40 originally triggered - _, err = s.GetAgent(ctx, agentID) - assert.ErrorIs(t, err, store.ErrNotFound, "agent should be cascade-deleted when FK is ON") -} - -func TestMigrationV40PreservesAgents(t *testing.T) { - // Regression test: V40 drops and recreates the projects table. Without - // PRAGMA foreign_keys=OFF (which must be set OUTSIDE the transaction), - // DROP TABLE projects triggers ON DELETE CASCADE on agents, deleting all rows. - s, err := New(":memory:") - require.NoError(t, err) - defer s.Close() - - ctx := context.Background() - - err = s.Migrate(ctx) - require.NoError(t, err) - - // Create a project and an agent - projectID := api.NewUUID() - err = s.CreateProject(ctx, &store.Project{ - ID: projectID, - Name: "TestProject", - Slug: "test-project", - Visibility: store.VisibilityPrivate, - }) - require.NoError(t, err) - - agentID := api.NewUUID() - err = s.CreateAgent(ctx, &store.Agent{ - ID: agentID, - Slug: "test-agent", - Name: "Test Agent", - ProjectID: projectID, - Visibility: store.VisibilityPrivate, - }) - require.NoError(t, err) - - // Verify agent exists - agent, err := s.GetAgent(ctx, agentID) - require.NoError(t, err) - assert.Equal(t, "Test Agent", agent.Name) - - // Simulate re-running V40 by dropping and recreating projects table - // using the same pattern as the migration, with proper FK handling. - _, err = s.db.ExecContext(ctx, "PRAGMA foreign_keys=OFF") - require.NoError(t, err) - - _, err = s.db.ExecContext(ctx, ` - CREATE TABLE projects_new2 AS SELECT * FROM projects; - DROP TABLE projects; - ALTER TABLE projects_new2 RENAME TO projects; - `) - require.NoError(t, err) - - _, err = s.db.ExecContext(ctx, "PRAGMA foreign_keys=ON") - require.NoError(t, err) - - // Agent must still exist after table recreation - agent, err = s.GetAgent(ctx, agentID) - require.NoError(t, err) - assert.Equal(t, "Test Agent", agent.Name) -} - -func TestMigrationV53_AllowListMissing(t *testing.T) { - // Regression test: V48 and V49 were inserted into the migration sequence, - // pushing the grove-to-project rename from V48 to V50. Databases that - // already applied the old V48 (the rename) have version 48 recorded in - // schema_migrations, so the new V48 (CREATE TABLE allow_list) is skipped. - // V53 must create the allow_list table if it doesn't exist before adding - // the index. - s, err := New(":memory:") - require.NoError(t, err) - defer s.Close() - - ctx := context.Background() - - // Run all migrations normally first. - err = s.Migrate(ctx) - require.NoError(t, err) - - // Simulate the bug: drop allow_list (as if V48 was a different migration - // when it was originally applied) and roll back schema_migrations so V53 - // will re-run. - _, err = s.db.ExecContext(ctx, ` - DROP TABLE IF EXISTS allow_list; - DELETE FROM schema_migrations WHERE version >= 53; - `) - require.NoError(t, err) - - // Verify allow_list doesn't exist. - var tableName string - err = s.db.QueryRowContext(ctx, - "SELECT name FROM sqlite_master WHERE type='table' AND name='allow_list'", - ).Scan(&tableName) - require.ErrorIs(t, err, sql.ErrNoRows, - "allow_list should not exist before re-migration") - - // Re-run Migrate. V53 should succeed by creating the allow_list table - // before adding the index. - err = s.Migrate(ctx) - require.NoError(t, err, "Migrate must succeed even when allow_list was never created by V48") - - // Verify allow_list table now exists and is usable. - _, err = s.db.ExecContext(ctx, - "INSERT INTO allow_list (id, email, added_by) VALUES ('test-id', 'test@example.com', 'admin')") - require.NoError(t, err, "allow_list table should be usable after migration") - - // Verify the index exists. - var indexName string - err = s.db.QueryRowContext(ctx, - "SELECT name FROM sqlite_master WHERE type='index' AND name='idx_allow_list_created_id'", - ).Scan(&indexName) - require.NoError(t, err, "idx_allow_list_created_id index should exist") -} - -func TestPing(t *testing.T) { - s := setupTestStore(t) - ctx := context.Background() - - err := s.Ping(ctx) - require.NoError(t, err) -} - -// ============================================================================ -// Error Cases -// ============================================================================ - -func TestNotFoundErrors(t *testing.T) { - s := setupTestStore(t) - ctx := context.Background() - - nonExistentID := api.NewUUID() - - // Agent - _, err := s.GetAgent(ctx, nonExistentID) - assert.ErrorIs(t, err, store.ErrNotFound) - - err = s.DeleteAgent(ctx, nonExistentID) - assert.ErrorIs(t, err, store.ErrNotFound) - - // Project - _, err = s.GetProject(ctx, nonExistentID) - assert.ErrorIs(t, err, store.ErrNotFound) - - err = s.DeleteProject(ctx, nonExistentID) - assert.ErrorIs(t, err, store.ErrNotFound) - - // RuntimeBroker - _, err = s.GetRuntimeBroker(ctx, nonExistentID) - assert.ErrorIs(t, err, store.ErrNotFound) - - err = s.DeleteRuntimeBroker(ctx, nonExistentID) - assert.ErrorIs(t, err, store.ErrNotFound) - - // Template - _, err = s.GetTemplate(ctx, nonExistentID) - assert.ErrorIs(t, err, store.ErrNotFound) - - err = s.DeleteTemplate(ctx, nonExistentID) - assert.ErrorIs(t, err, store.ErrNotFound) - - // User - _, err = s.GetUser(ctx, nonExistentID) - assert.ErrorIs(t, err, store.ErrNotFound) - - err = s.DeleteUser(ctx, nonExistentID) - assert.ErrorIs(t, err, store.ErrNotFound) -} - -func TestCascadeDelete(t *testing.T) { - s := setupTestStore(t) - ctx := context.Background() - - // Create project with agent - project := &store.Project{ - ID: api.NewUUID(), - Name: "Test Project", - Slug: "test-project", - Visibility: store.VisibilityPrivate, - } - require.NoError(t, s.CreateProject(ctx, project)) - - agent := &store.Agent{ - ID: api.NewUUID(), - Slug: "test-agent", - Name: "Test Agent", - Template: "claude", - ProjectID: project.ID, - Phase: string(state.PhaseRunning), - Visibility: store.VisibilityPrivate, - } - require.NoError(t, s.CreateAgent(ctx, agent)) - - // Delete project - err := s.DeleteProject(ctx, project.ID) - require.NoError(t, err) - - // Verify agent was cascade deleted - _, err = s.GetAgent(ctx, agent.ID) - assert.ErrorIs(t, err, store.ErrNotFound) -} - -func TestCascadeDeleteEnvVarsSecrets(t *testing.T) { - s := setupTestStore(t) - ctx := context.Background() - - projectID := api.NewUUID() - require.NoError(t, s.CreateProject(ctx, &store.Project{ - ID: projectID, Name: "Cascade EV/S", Slug: "cascade-ev-s", - Visibility: store.VisibilityPrivate, - })) - - // Create project-scoped env vars - require.NoError(t, s.CreateEnvVar(ctx, &store.EnvVar{ - ID: api.NewUUID(), Key: "A", Value: "1", - Scope: store.ScopeProject, ScopeID: projectID, - })) - require.NoError(t, s.CreateEnvVar(ctx, &store.EnvVar{ - ID: api.NewUUID(), Key: "B", Value: "2", - Scope: store.ScopeProject, ScopeID: projectID, - })) - - // Create project-scoped secrets - require.NoError(t, s.CreateSecret(ctx, &store.Secret{ - ID: api.NewUUID(), Key: "S1", EncryptedValue: "enc1", - Scope: store.ScopeProject, ScopeID: projectID, Version: 1, - })) - - // Create a hub-scoped env var (should not be deleted) - require.NoError(t, s.CreateEnvVar(ctx, &store.EnvVar{ - ID: api.NewUUID(), Key: "HUB_VAR", Value: "hub", - Scope: store.ScopeHub, ScopeID: "test-hub-id", - })) - - // Delete by scope - n, err := s.DeleteEnvVarsByScope(ctx, store.ScopeProject, projectID) - require.NoError(t, err) - assert.Equal(t, 2, n) - - n, err = s.DeleteSecretsByScope(ctx, store.ScopeProject, projectID) - require.NoError(t, err) - assert.Equal(t, 1, n) - - // Verify project-scoped are gone - envVars, err := s.ListEnvVars(ctx, store.EnvVarFilter{Scope: store.ScopeProject, ScopeID: projectID}) - require.NoError(t, err) - assert.Empty(t, envVars) - - secrets, err := s.ListSecrets(ctx, store.SecretFilter{Scope: store.ScopeProject, ScopeID: projectID}) - require.NoError(t, err) - assert.Empty(t, secrets) - - // Verify hub-scoped env var still exists - hubVars, err := s.ListEnvVars(ctx, store.EnvVarFilter{Scope: store.ScopeHub, ScopeID: "test-hub-id"}) - require.NoError(t, err) - assert.Len(t, hubVars, 1) - - // Delete with no matches returns 0, no error - n, err = s.DeleteEnvVarsByScope(ctx, store.ScopeProject, "nonexistent") - require.NoError(t, err) - assert.Equal(t, 0, n) -} - -func TestDeleteHarnessConfigsByScope(t *testing.T) { - s := setupTestStore(t) - ctx := context.Background() - - projectID := api.NewUUID() - require.NoError(t, s.CreateProject(ctx, &store.Project{ - ID: projectID, Name: "HC Cascade", Slug: "hc-cascade", - Visibility: store.VisibilityPrivate, - })) - - require.NoError(t, s.CreateHarnessConfig(ctx, &store.HarnessConfig{ - ID: api.NewUUID(), Name: "hc1", Slug: "hc1", - Harness: "claude", Scope: store.ScopeProject, ScopeID: projectID, - Status: store.HarnessConfigStatusActive, Visibility: store.VisibilityPrivate, - })) - - n, err := s.DeleteHarnessConfigsByScope(ctx, store.ScopeProject, projectID) - require.NoError(t, err) - assert.Equal(t, 1, n) - - result, err := s.ListHarnessConfigs(ctx, store.HarnessConfigFilter{Scope: store.ScopeProject, ScopeID: projectID}, store.ListOptions{}) - require.NoError(t, err) - assert.Empty(t, result.Items) -} - -func TestDeleteTemplatesByScope(t *testing.T) { - s := setupTestStore(t) - ctx := context.Background() - - projectID := api.NewUUID() - require.NoError(t, s.CreateProject(ctx, &store.Project{ - ID: projectID, Name: "Tmpl Cascade", Slug: "tmpl-cascade", - Visibility: store.VisibilityPrivate, - })) - - require.NoError(t, s.CreateTemplate(ctx, &store.Template{ - ID: api.NewUUID(), Name: "tmpl1", Slug: "tmpl1", - Harness: "claude", Scope: store.ScopeProject, ScopeID: projectID, - Status: store.TemplateStatusActive, Visibility: store.VisibilityPrivate, - })) - require.NoError(t, s.CreateTemplate(ctx, &store.Template{ - ID: api.NewUUID(), Name: "tmpl2", Slug: "tmpl2", - Harness: "gemini", Scope: store.ScopeProject, ScopeID: projectID, - Status: store.TemplateStatusActive, Visibility: store.VisibilityPrivate, - })) - - n, err := s.DeleteTemplatesByScope(ctx, store.ScopeProject, projectID) - require.NoError(t, err) - assert.Equal(t, 2, n) - - result, err := s.ListTemplates(ctx, store.TemplateFilter{Scope: store.ScopeProject, ScopeID: projectID}, store.ListOptions{}) - require.NoError(t, err) - assert.Empty(t, result.Items) -} - -// ============================================================================ -// MarkStaleAgentsOffline Tests -// ============================================================================ - -func TestMarkStaleAgentsOffline(t *testing.T) { - s := setupTestStore(t) - ctx := context.Background() - - project := &store.Project{ - ID: api.NewUUID(), - Name: "Heartbeat Project", - Slug: "heartbeat-project", - Visibility: store.VisibilityPrivate, - } - require.NoError(t, s.CreateProject(ctx, project)) - - staleTime := time.Now().Add(-5 * time.Minute) - threshold := time.Now().Add(-2 * time.Minute) - - // These agents have phase=running with non-sticky activities → should be marked offline - activeActivities := []string{"working", "thinking", "executing", "waiting_for_input"} - - var expectedIDs []string - for i, activity := range activeActivities { - agent := &store.Agent{ - ID: api.NewUUID(), - Slug: "active-agent-" + activity, - Name: "Active Agent " + activity, - Template: "claude", - ProjectID: project.ID, - Phase: string(state.PhaseCreated), - Visibility: store.VisibilityPrivate, - } - require.NoError(t, s.CreateAgent(ctx, agent)) - - // Set to running phase with activity - err := s.UpdateAgentStatus(ctx, agent.ID, store.AgentStatusUpdate{ - Phase: "running", - Activity: activity, - }) - require.NoError(t, err, "setting activity for agent %d", i) - - // Manually set last_seen to stale time - _, err = s.db.ExecContext(ctx, "UPDATE agents SET last_seen = ? WHERE id = ?", staleTime, agent.ID) - require.NoError(t, err) - - expectedIDs = append(expectedIDs, agent.ID) - } - - // These agents should NOT be marked offline - - // Sticky activity: completed (phase=running) - completedAgent := &store.Agent{ - ID: api.NewUUID(), Slug: "completed-agent", Name: "Completed Agent", - Template: "claude", ProjectID: project.ID, Phase: string(state.PhaseCreated), - Visibility: store.VisibilityPrivate, - } - require.NoError(t, s.CreateAgent(ctx, completedAgent)) - require.NoError(t, s.UpdateAgentStatus(ctx, completedAgent.ID, store.AgentStatusUpdate{ - Phase: "running", Activity: "completed", - })) - _, err := s.db.ExecContext(ctx, "UPDATE agents SET last_seen = ? WHERE id = ?", staleTime, completedAgent.ID) - require.NoError(t, err) - - // Sticky activity: limits_exceeded (phase=running) - limitsAgent := &store.Agent{ - ID: api.NewUUID(), Slug: "limits-agent", Name: "Limits Agent", - Template: "claude", ProjectID: project.ID, Phase: string(state.PhaseCreated), - Visibility: store.VisibilityPrivate, - } - require.NoError(t, s.CreateAgent(ctx, limitsAgent)) - require.NoError(t, s.UpdateAgentStatus(ctx, limitsAgent.ID, store.AgentStatusUpdate{ - Phase: "running", Activity: "limits_exceeded", - })) - _, err = s.db.ExecContext(ctx, "UPDATE agents SET last_seen = ? WHERE id = ?", staleTime, limitsAgent.ID) - require.NoError(t, err) - - // Non-running phase: stopped - stoppedAgent := &store.Agent{ - ID: api.NewUUID(), Slug: "stopped-agent", Name: "Stopped Agent", - Template: "claude", ProjectID: project.ID, Phase: string(state.PhaseStopped), - Visibility: store.VisibilityPrivate, - } - require.NoError(t, s.CreateAgent(ctx, stoppedAgent)) - _, err = s.db.ExecContext(ctx, "UPDATE agents SET last_seen = ? WHERE id = ?", staleTime, stoppedAgent.ID) - require.NoError(t, err) - - // Recent heartbeat (should not be affected) - recentAgent := &store.Agent{ - ID: api.NewUUID(), Slug: "recent-agent", Name: "Recent Agent", - Template: "claude", ProjectID: project.ID, Phase: string(state.PhaseCreated), - Visibility: store.VisibilityPrivate, - } - require.NoError(t, s.CreateAgent(ctx, recentAgent)) - require.NoError(t, s.UpdateAgentStatus(ctx, recentAgent.ID, store.AgentStatusUpdate{ - Phase: "running", Activity: "working", - })) - // last_seen is set to now by UpdateAgentStatus, which is within the threshold - - // Execute - agents, err := s.MarkStaleAgentsOffline(ctx, threshold) - require.NoError(t, err) - assert.Len(t, agents, len(activeActivities), "should only mark running stale agents with non-sticky activities") - - // Verify the returned agents - returnedIDs := make(map[string]bool) - for _, a := range agents { - returnedIDs[a.ID] = true - assert.Equal(t, "offline", a.Activity, "returned agent should have offline activity") - assert.Equal(t, "running", a.Phase, "returned agent should still have running phase") - assert.Equal(t, string(state.ActivityOffline), a.Activity, "returned agent should have offline activity") - } - for _, id := range expectedIDs { - assert.True(t, returnedIDs[id], "expected agent %s to be in returned set", id) - } - - // Verify sticky activities were NOT affected - a, err := s.GetAgent(ctx, completedAgent.ID) - require.NoError(t, err) - assert.Equal(t, "completed", a.Activity) - - a, err = s.GetAgent(ctx, limitsAgent.ID) - require.NoError(t, err) - assert.Equal(t, "limits_exceeded", a.Activity) - - // Verify stopped agent was NOT affected - a, err = s.GetAgent(ctx, stoppedAgent.ID) - require.NoError(t, err) - assert.Equal(t, "stopped", a.Phase) - - // Verify recent agent was NOT affected - a, err = s.GetAgent(ctx, recentAgent.ID) - require.NoError(t, err) - assert.Equal(t, "working", a.Activity) -} - -func TestMarkStaleAgentsOffline_Idempotent(t *testing.T) { - s := setupTestStore(t) - ctx := context.Background() - - project := &store.Project{ - ID: api.NewUUID(), - Name: "Idempotent Project", - Slug: "idempotent-project", - Visibility: store.VisibilityPrivate, - } - require.NoError(t, s.CreateProject(ctx, project)) - - staleTime := time.Now().Add(-5 * time.Minute) - threshold := time.Now().Add(-2 * time.Minute) - - agent := &store.Agent{ - ID: api.NewUUID(), Slug: "stale-agent", Name: "Stale Agent", - Template: "claude", ProjectID: project.ID, Phase: string(state.PhaseCreated), - Visibility: store.VisibilityPrivate, - } - require.NoError(t, s.CreateAgent(ctx, agent)) - require.NoError(t, s.UpdateAgentStatus(ctx, agent.ID, store.AgentStatusUpdate{ - Phase: "running", Activity: "working", - })) - _, err := s.db.ExecContext(ctx, "UPDATE agents SET last_seen = ? WHERE id = ?", staleTime, agent.ID) - require.NoError(t, err) - - // First call should mark it offline - agents, err := s.MarkStaleAgentsOffline(ctx, threshold) - require.NoError(t, err) - assert.Len(t, agents, 1) - - // Second call should return empty (already offline) - agents, err = s.MarkStaleAgentsOffline(ctx, threshold) - require.NoError(t, err) - assert.Len(t, agents, 0, "should not re-mark already offline agents") -} - -func TestMarkStaleAgentsOffline_NoStaleAgents(t *testing.T) { - s := setupTestStore(t) - ctx := context.Background() - - threshold := time.Now().Add(-2 * time.Minute) - - // No agents at all - agents, err := s.MarkStaleAgentsOffline(ctx, threshold) - require.NoError(t, err) - assert.Len(t, agents, 0) -} - -// ============================================================================ -// Stalled Agent Detection Tests -// ============================================================================ - -func TestMarkStalledAgents(t *testing.T) { - s := setupTestStore(t) - ctx := context.Background() - - project := &store.Project{ - ID: api.NewUUID(), - Name: "Stalled Project", - Slug: "stalled-project", - Visibility: store.VisibilityPrivate, - } - require.NoError(t, s.CreateProject(ctx, project)) - - staleActivityTime := time.Now().Add(-10 * time.Minute) - recentHeartbeat := time.Now().Add(-30 * time.Second) - activityThreshold := time.Now().Add(-5 * time.Minute) - heartbeatRecency := time.Now().Add(-2 * time.Minute) - - // --- Should be marked stalled: stale activity + recent heartbeat --- - stalledActivities := []string{"thinking", "executing", "working"} - var expectedIDs []string - for _, activity := range stalledActivities { - agent := &store.Agent{ - ID: api.NewUUID(), Slug: "stalled-" + activity, Name: "Stalled " + activity, - Template: "claude", ProjectID: project.ID, Phase: string(state.PhaseCreated), - Visibility: store.VisibilityPrivate, - } - require.NoError(t, s.CreateAgent(ctx, agent)) - require.NoError(t, s.UpdateAgentStatus(ctx, agent.ID, store.AgentStatusUpdate{ - Phase: "running", Activity: activity, - })) - // Manually set stale activity time + recent heartbeat - _, err := s.db.ExecContext(ctx, - "UPDATE agents SET last_activity_event = ?, last_seen = ? WHERE id = ?", - staleActivityTime, recentHeartbeat, agent.ID) - require.NoError(t, err) - expectedIDs = append(expectedIDs, agent.ID) - } - - // --- Should NOT be marked stalled --- - - // Recent activity (within threshold) - recentAgent := &store.Agent{ - ID: api.NewUUID(), Slug: "recent-activity", Name: "Recent Activity", - Template: "claude", ProjectID: project.ID, Phase: string(state.PhaseCreated), - Visibility: store.VisibilityPrivate, - } - require.NoError(t, s.CreateAgent(ctx, recentAgent)) - require.NoError(t, s.UpdateAgentStatus(ctx, recentAgent.ID, store.AgentStatusUpdate{ - Phase: "running", Activity: "working", - })) - // last_activity_event is set to now by UpdateAgentStatus, which is within threshold - - // Stale activity + stale heartbeat (offline territory, not stalled) - offlineAgent := &store.Agent{ - ID: api.NewUUID(), Slug: "offline-territory", Name: "Offline Territory", - Template: "claude", ProjectID: project.ID, Phase: string(state.PhaseCreated), - Visibility: store.VisibilityPrivate, - } - require.NoError(t, s.CreateAgent(ctx, offlineAgent)) - require.NoError(t, s.UpdateAgentStatus(ctx, offlineAgent.ID, store.AgentStatusUpdate{ - Phase: "running", Activity: "working", - })) - staleHeartbeat := time.Now().Add(-5 * time.Minute) - _, err := s.db.ExecContext(ctx, - "UPDATE agents SET last_activity_event = ?, last_seen = ? WHERE id = ?", - staleActivityTime, staleHeartbeat, offlineAgent.ID) - require.NoError(t, err) - - // Completed activity (sticky — should not be stalled) - completedAgent := &store.Agent{ - ID: api.NewUUID(), Slug: "completed-stall", Name: "Completed Stall", - Template: "claude", ProjectID: project.ID, Phase: string(state.PhaseCreated), - Visibility: store.VisibilityPrivate, - } - require.NoError(t, s.CreateAgent(ctx, completedAgent)) - require.NoError(t, s.UpdateAgentStatus(ctx, completedAgent.ID, store.AgentStatusUpdate{ - Phase: "running", Activity: "completed", - })) - _, err = s.db.ExecContext(ctx, - "UPDATE agents SET last_activity_event = ?, last_seen = ? WHERE id = ?", - staleActivityTime, recentHeartbeat, completedAgent.ID) - require.NoError(t, err) - - // limits_exceeded activity (sticky) - limitsAgent := &store.Agent{ - ID: api.NewUUID(), Slug: "limits-stall", Name: "Limits Stall", - Template: "claude", ProjectID: project.ID, Phase: string(state.PhaseCreated), - Visibility: store.VisibilityPrivate, - } - require.NoError(t, s.CreateAgent(ctx, limitsAgent)) - require.NoError(t, s.UpdateAgentStatus(ctx, limitsAgent.ID, store.AgentStatusUpdate{ - Phase: "running", Activity: "limits_exceeded", - })) - _, err = s.db.ExecContext(ctx, - "UPDATE agents SET last_activity_event = ?, last_seen = ? WHERE id = ?", - staleActivityTime, recentHeartbeat, limitsAgent.ID) - require.NoError(t, err) - - // Stopped phase (not running) - stoppedAgent := &store.Agent{ - ID: api.NewUUID(), Slug: "stopped-stall", Name: "Stopped Stall", - Template: "claude", ProjectID: project.ID, Phase: string(state.PhaseStopped), - Visibility: store.VisibilityPrivate, - } - require.NoError(t, s.CreateAgent(ctx, stoppedAgent)) - _, err = s.db.ExecContext(ctx, - "UPDATE agents SET last_activity_event = ?, last_seen = ? WHERE id = ?", - staleActivityTime, recentHeartbeat, stoppedAgent.ID) - require.NoError(t, err) - - // Already offline - alreadyOfflineAgent := &store.Agent{ - ID: api.NewUUID(), Slug: "already-offline", Name: "Already Offline", - Template: "claude", ProjectID: project.ID, Phase: string(state.PhaseCreated), - Visibility: store.VisibilityPrivate, - } - require.NoError(t, s.CreateAgent(ctx, alreadyOfflineAgent)) - require.NoError(t, s.UpdateAgentStatus(ctx, alreadyOfflineAgent.ID, store.AgentStatusUpdate{ - Phase: "running", Activity: "offline", - })) - _, err = s.db.ExecContext(ctx, - "UPDATE agents SET last_activity_event = ?, last_seen = ? WHERE id = ?", - staleActivityTime, recentHeartbeat, alreadyOfflineAgent.ID) - require.NoError(t, err) - - // waiting_for_input activity (sticky waiting state — must NOT stall) - waitingAgent := &store.Agent{ - ID: api.NewUUID(), Slug: "waiting-for-input", Name: "Waiting For Input", - Template: "claude", ProjectID: project.ID, Phase: string(state.PhaseCreated), - Visibility: store.VisibilityPrivate, - } - require.NoError(t, s.CreateAgent(ctx, waitingAgent)) - require.NoError(t, s.UpdateAgentStatus(ctx, waitingAgent.ID, store.AgentStatusUpdate{ - Phase: "running", Activity: "waiting_for_input", - })) - _, err = s.db.ExecContext(ctx, - "UPDATE agents SET last_activity_event = ?, last_seen = ? WHERE id = ?", - staleActivityTime, recentHeartbeat, waitingAgent.ID) - require.NoError(t, err) - - // Execute - agents, err := s.MarkStalledAgents(ctx, activityThreshold, heartbeatRecency) - require.NoError(t, err) - assert.Len(t, agents, len(stalledActivities), "should only mark running agents with stale activity and recent heartbeat") - - // Verify the returned agents - returnedIDs := make(map[string]bool) - // Build a map from ID to pre-stall activity for validation - expectedPreStall := make(map[string]string) - for i, id := range expectedIDs { - expectedPreStall[id] = stalledActivities[i] - } - for _, a := range agents { - returnedIDs[a.ID] = true - assert.Equal(t, "stalled", a.Activity, "returned agent should have stalled activity") - assert.Equal(t, "running", a.Phase, "returned agent should still have running phase") - if expected, ok := expectedPreStall[a.ID]; ok { - assert.Equal(t, expected, a.StalledFromActivity, - "stalled_from_activity should record the pre-stall activity for agent %s", a.Slug) - } - } - for _, id := range expectedIDs { - assert.True(t, returnedIDs[id], "expected agent %s to be in returned set", id) - } - - // Verify excluded agents were NOT affected - a, err := s.GetAgent(ctx, recentAgent.ID) - require.NoError(t, err) - assert.Equal(t, "working", a.Activity) - - a, err = s.GetAgent(ctx, offlineAgent.ID) - require.NoError(t, err) - assert.Equal(t, "working", a.Activity, "stale heartbeat agent should not be stalled") - - a, err = s.GetAgent(ctx, completedAgent.ID) - require.NoError(t, err) - assert.Equal(t, "completed", a.Activity) - - a, err = s.GetAgent(ctx, limitsAgent.ID) - require.NoError(t, err) - assert.Equal(t, "limits_exceeded", a.Activity) - - a, err = s.GetAgent(ctx, stoppedAgent.ID) - require.NoError(t, err) - assert.Equal(t, string(state.PhaseStopped), a.Phase) - - a, err = s.GetAgent(ctx, alreadyOfflineAgent.ID) - require.NoError(t, err) - assert.Equal(t, "offline", a.Activity) - - a, err = s.GetAgent(ctx, waitingAgent.ID) - require.NoError(t, err) - assert.Equal(t, "waiting_for_input", a.Activity, "waiting_for_input agent should not be marked stalled") -} - -func TestMarkStalledAgents_Idempotent(t *testing.T) { - s := setupTestStore(t) - ctx := context.Background() - - project := &store.Project{ - ID: api.NewUUID(), - Name: "Idempotent Stalled Project", - Slug: "idempotent-stalled", - Visibility: store.VisibilityPrivate, - } - require.NoError(t, s.CreateProject(ctx, project)) - - staleActivityTime := time.Now().Add(-10 * time.Minute) - recentHeartbeat := time.Now().Add(-30 * time.Second) - activityThreshold := time.Now().Add(-5 * time.Minute) - heartbeatRecency := time.Now().Add(-2 * time.Minute) - - agent := &store.Agent{ - ID: api.NewUUID(), Slug: "stalled-idem", Name: "Stalled Idem", - Template: "claude", ProjectID: project.ID, Phase: string(state.PhaseCreated), - Visibility: store.VisibilityPrivate, - } - require.NoError(t, s.CreateAgent(ctx, agent)) - require.NoError(t, s.UpdateAgentStatus(ctx, agent.ID, store.AgentStatusUpdate{ - Phase: "running", Activity: "thinking", - })) - _, err := s.db.ExecContext(ctx, - "UPDATE agents SET last_activity_event = ?, last_seen = ? WHERE id = ?", - staleActivityTime, recentHeartbeat, agent.ID) - require.NoError(t, err) - - // First call should mark it stalled - agents, err := s.MarkStalledAgents(ctx, activityThreshold, heartbeatRecency) - require.NoError(t, err) - assert.Len(t, agents, 1) - - // Second call should return empty (already stalled) - agents, err = s.MarkStalledAgents(ctx, activityThreshold, heartbeatRecency) - require.NoError(t, err) - assert.Len(t, agents, 0, "should not re-mark already stalled agents") -} - -func TestMarkStalledAgents_NoAgents(t *testing.T) { - s := setupTestStore(t) - ctx := context.Background() - - activityThreshold := time.Now().Add(-5 * time.Minute) - heartbeatRecency := time.Now().Add(-2 * time.Minute) - - agents, err := s.MarkStalledAgents(ctx, activityThreshold, heartbeatRecency) - require.NoError(t, err) - assert.Len(t, agents, 0) -} - -func TestUpdateAgentStatus_SetsLastActivityEvent(t *testing.T) { - s := setupTestStore(t) - ctx := context.Background() - - project := &store.Project{ - ID: api.NewUUID(), - Name: "Activity Event Project", - Slug: "activity-event-project", - Visibility: store.VisibilityPrivate, - } - require.NoError(t, s.CreateProject(ctx, project)) - - agent := &store.Agent{ - ID: api.NewUUID(), Slug: "activity-tracker", Name: "Activity Tracker", - Template: "claude", ProjectID: project.ID, Phase: string(state.PhaseCreated), - Visibility: store.VisibilityPrivate, - } - require.NoError(t, s.CreateAgent(ctx, agent)) - - // Activity update should set last_activity_event - require.NoError(t, s.UpdateAgentStatus(ctx, agent.ID, store.AgentStatusUpdate{ - Phase: "running", Activity: "thinking", - })) - - a, err := s.GetAgent(ctx, agent.ID) - require.NoError(t, err) - assert.False(t, a.LastActivityEvent.IsZero(), "last_activity_event should be set after activity update") - activityTime := a.LastActivityEvent - - // Heartbeat-only update (no activity) should NOT change last_activity_event - // Manually set last_activity_event to a known past time first - pastTime := time.Now().Add(-10 * time.Minute) - _, err = s.db.ExecContext(ctx, "UPDATE agents SET last_activity_event = ? WHERE id = ?", pastTime, agent.ID) - require.NoError(t, err) - - require.NoError(t, s.UpdateAgentStatus(ctx, agent.ID, store.AgentStatusUpdate{ - Heartbeat: true, - })) - - a, err = s.GetAgent(ctx, agent.ID) - require.NoError(t, err) - // last_activity_event should still be the past time, not updated - assert.True(t, a.LastActivityEvent.Before(activityTime), - "heartbeat-only update should not change last_activity_event") - - // Another activity update should update last_activity_event - require.NoError(t, s.UpdateAgentStatus(ctx, agent.ID, store.AgentStatusUpdate{ - Activity: "executing", - })) - - a, err = s.GetAgent(ctx, agent.ID) - require.NoError(t, err) - assert.True(t, a.LastActivityEvent.After(pastTime), - "activity update should update last_activity_event") -} - -func TestUpdateAgentStatus_ProtectsTerminalActivity(t *testing.T) { - s := setupTestStore(t) - ctx := context.Background() - - grove := &store.Project{ - ID: api.NewUUID(), - Name: "Terminal Guard Grove", - Slug: "terminal-guard-grove", - Visibility: store.VisibilityPrivate, - } - require.NoError(t, s.CreateProject(ctx, grove)) - - agent := &store.Agent{ - ID: api.NewUUID(), Slug: "terminal-guard", Name: "Terminal Guard", - Template: "claude", ProjectID: grove.ID, Phase: string(state.PhaseStopped), - Activity: string(state.ActivityCrashed), - Visibility: store.VisibilityPrivate, - } - require.NoError(t, s.CreateAgent(ctx, agent)) - - // Non-terminal activity should not overwrite crashed - require.NoError(t, s.UpdateAgentStatus(ctx, agent.ID, store.AgentStatusUpdate{ - Activity: string(state.ActivityWorking), - })) - - a, err := s.GetAgent(ctx, agent.ID) - require.NoError(t, err) - assert.Equal(t, string(state.ActivityCrashed), a.Activity, - "non-terminal activity should not overwrite crashed") - - // Another terminal activity should overwrite - require.NoError(t, s.UpdateAgentStatus(ctx, agent.ID, store.AgentStatusUpdate{ - Activity: string(state.ActivityLimitsExceeded), - })) - - a, err = s.GetAgent(ctx, agent.ID) - require.NoError(t, err) - assert.Equal(t, string(state.ActivityLimitsExceeded), a.Activity, - "terminal activity should be able to overwrite another terminal activity") - - // Empty activity should keep current (standard behavior) - require.NoError(t, s.UpdateAgentStatus(ctx, agent.ID, store.AgentStatusUpdate{ - Heartbeat: true, - })) - - a, err = s.GetAgent(ctx, agent.ID) - require.NoError(t, err) - assert.Equal(t, string(state.ActivityLimitsExceeded), a.Activity, - "empty activity should keep current terminal activity") -} - -// ============================================================================ -// DSN Construction Tests -// ============================================================================ - -func TestBuildDSN(t *testing.T) { - tests := []struct { - name string - input string - wantExact string - wantRegex string - }{ - { - name: "memory", - input: ":memory:", - wantRegex: `^file:memdb\d+\?mode=memory&cache=shared&_pragma=busy_timeout\(5000\)&_pragma=foreign_keys\(1\)&_pragma=journal_mode\(WAL\)$`, - }, - { - name: "plain path", - input: "/data/scion.db", - wantExact: "file:/data/scion.db?_pragma=busy_timeout(5000)&_pragma=foreign_keys(1)&_pragma=journal_mode(WAL)", - }, - { - name: "file URI without params", - input: "file:/data/scion.db", - wantExact: "file:/data/scion.db?_pragma=busy_timeout(5000)&_pragma=foreign_keys(1)&_pragma=journal_mode(WAL)", - }, - { - name: "file URI with existing params", - input: "file:/data/scion.db?mode=rwc", - wantExact: "file:/data/scion.db?mode=rwc&_pragma=busy_timeout(5000)&_pragma=foreign_keys(1)&_pragma=journal_mode(WAL)", - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - got := buildDSN(tt.input) - if tt.wantExact != "" { - assert.Equal(t, tt.wantExact, got) - } else { - assert.Regexp(t, tt.wantRegex, got) - } - }) - } -} - -func TestConcurrentReadDuringWrite(t *testing.T) { - s := setupTestStore(t) - ctx := context.Background() - - project := &store.Project{ - ID: api.NewUUID(), - Name: "Concurrency Test", - Slug: "concurrency-test", - } - require.NoError(t, s.CreateProject(ctx, project)) - - // Create several agents to write to - const agentCount = 10 - agentIDs := make([]string, agentCount) - for i := range agentCount { - slug := fmt.Sprintf("agent-%d", i) - agent := &store.Agent{ - ID: api.NewUUID(), - Slug: slug, - Name: slug, - ProjectID: project.ID, - } - require.NoError(t, s.CreateAgent(ctx, agent)) - agentIDs[i] = agent.ID - } - - // Simulate heartbeat: write status updates for all agents sequentially - writerDone := make(chan struct{}) - go func() { - defer close(writerDone) - for _, id := range agentIDs { - _ = s.UpdateAgentStatus(ctx, id, store.AgentStatusUpdate{ - Phase: "running", - Activity: "thinking", - Heartbeat: true, - }) - } - }() - - // Concurrent reader should not block behind the writer - readerDone := make(chan struct{}) - go func() { - defer close(readerDone) - _, _ = s.GetAgent(ctx, agentIDs[0]) - }() - - // Both should complete quickly — if the reader blocks behind all - // writes (old MaxOpenConns=1 behavior), this would be much slower. - select { - case <-readerDone: - case <-time.After(5 * time.Second): - t.Fatal("concurrent read timed out — likely blocked behind writes") - } - - <-writerDone -} diff --git a/pkg/store/store.go b/pkg/store/store.go index 22e93e471..b148c488b 100644 --- a/pkg/store/store.go +++ b/pkg/store/store.go @@ -49,6 +49,9 @@ type Store interface { // RuntimeBroker operations RuntimeBrokerStore + // BrokerDispatch operations (multi-node command dispatch) + BrokerDispatchStore + // Template operations TemplateStore @@ -302,6 +305,36 @@ type RuntimeBrokerStore interface { // UpdateRuntimeBrokerHeartbeat updates the last heartbeat and status. UpdateRuntimeBrokerHeartbeat(ctx context.Context, id string, status string) error + + // ClaimRuntimeBrokerConnection records this hub instance as the owner of the + // broker's live control-channel socket. The newest connection wins + // (unconditional claim): it sets connected_hub_id/connected_session_id/ + // connected_at and, in the same write, bumps status to online and refreshes + // last_heartbeat. + ClaimRuntimeBrokerConnection(ctx context.Context, brokerID, hubInstanceID, sessionID string) error + + // ReleaseRuntimeBrokerConnection clears the broker's affinity ONLY IF it still + // names (hubInstanceID, sessionID) — a compare-and-clear. It returns + // cleared=true when this caller owned the affinity and it was cleared; it + // returns cleared=false (a no-op) when affinity has already moved to another + // hub/session, in which case the caller MUST NOT stamp the broker offline. + // It does not change status (the caller decides offline based on cleared). + ReleaseRuntimeBrokerConnection(ctx context.Context, brokerID, hubInstanceID, sessionID string) (cleared bool, err error) + + // ReleaseAndMarkBrokerOffline atomically clears broker affinity AND stamps + // status=offline, ONLY IF affinity still names (hubInstanceID, sessionID). + // This prevents a stale disconnect callback from clobbering a concurrent + // reconnect's online status — the session check and the offline stamp happen + // in the same CAS write with no TOCTOU window. + // Returns cleared=true when affinity matched and the broker was stamped offline. + // Returns cleared=false (no-op) when affinity has already moved. + ReleaseAndMarkBrokerOffline(ctx context.Context, brokerID, hubInstanceID, sessionID string) (cleared bool, err error) + + // ReapStaleBrokerAffinity clears connected_hub_id/connected_session_id/ + // connected_at for brokers whose last_heartbeat is older than staleBefore + // and whose connected_hub_id is not NULL (i.e. they still claim affinity). + // Returns the number of rows cleared. Does not change broker status. + ReapStaleBrokerAffinity(ctx context.Context, staleBefore time.Time) (cleared int, err error) } // RuntimeBrokerFilter defines criteria for filtering runtime brokers. @@ -312,6 +345,49 @@ type RuntimeBrokerFilter struct { AutoProvide *bool // Filter by auto-provide flag (nil = no filter) } +// BrokerDispatchStore defines persistence for the durable broker-dispatch intent +// table and the message dispatch-state CAS helpers (multi-node command dispatch). +type BrokerDispatchStore interface { + // InsertBrokerDispatch persists a new dispatch intent (state defaults pending). + InsertBrokerDispatch(ctx context.Context, d *BrokerDispatch) error + + // ClaimBrokerDispatch CAS-transitions a dispatch pending->in_progress for the + // given hub instance. Returns claimed=false if it was not pending (so exactly + // one node executes a given intent). + ClaimBrokerDispatch(ctx context.Context, id, hubInstanceID string) (claimed bool, err error) + + // CompleteBrokerDispatch marks a dispatch done with an optional result JSON. + CompleteBrokerDispatch(ctx context.Context, id, result string) error + + // FailBrokerDispatch marks a dispatch failed, records the error, bumps attempts. + FailBrokerDispatch(ctx context.Context, id, errMsg string) error + + // GetBrokerDispatch returns a single dispatch row by ID (used by the + // originator to read the result after the owner completes it). + GetBrokerDispatch(ctx context.Context, id string) (*BrokerDispatch, error) + + // ListPendingDispatch returns pending intents for a broker (drain query). + ListPendingDispatch(ctx context.Context, brokerID string) ([]BrokerDispatch, error) + + // MarkMessageDispatched CAS-flips a message pending->dispatched (dedupes drains). + MarkMessageDispatched(ctx context.Context, id string) (dispatched bool, err error) + + // ListPendingMessages returns pending messages whose target agent is on the broker. + ListPendingMessages(ctx context.Context, brokerID string) ([]Message, error) + + // ReapStuckDispatch re-drives or fails in_progress dispatches that have gone + // stale (updated_at < stuckBefore). Dispatches with attempts < maxAttempts + // are reset to pending (re-driven); those at or above the limit are failed. + // Returns counts of re-driven and failed rows. + ReapStuckDispatch(ctx context.Context, stuckBefore time.Time, maxAttempts int) (requeued, failed int, err error) + + // CountStuckPendingMessages returns the number of messages still in + // dispatch_state='pending' whose created timestamp is before the given + // cutoff. Used by the stuck-message sweep (B5-2) to surface messages that + // have not been dispatched within the expected window. + CountStuckPendingMessages(ctx context.Context, before time.Time) (int, error) +} + // TemplateStore defines template persistence operations. type TemplateStore interface { // CreateTemplate creates a new template record. diff --git a/pkg/store/storetest/domains.go b/pkg/store/storetest/domains.go new file mode 100644 index 000000000..84ec742bb --- /dev/null +++ b/pkg/store/storetest/domains.go @@ -0,0 +1,593 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package storetest + +import ( + "context" + "fmt" + "testing" + "time" + + "github.com/GoogleCloudPlatform/scion/pkg/store" + "github.com/google/uuid" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +// missingID returns a syntactically valid identifier that is guaranteed not to +// exist in a freshly created store. It is a UUID so backends that parse IDs +// (e.g. the Ent adapter) accept it and report ErrNotFound rather than a parse +// error. +func missingID() string { + return uuid.NewString() +} + +// RunStoreSuite runs the full CRUD-parity suite for every currently supported +// domain against stores produced by factory. As new domains are ported to the +// shared store interface, add their Domain descriptor here and they are covered +// automatically across all backends. +func RunStoreSuite(t *testing.T, factory Factory) { + t.Helper() + RunDomain(t, factory, GroupDomain()) + RunDomain(t, factory, PolicyDomain()) + RunDomain(t, factory, GCPServiceAccountDomain()) + RunDomain(t, factory, SubscriptionTemplateDomain()) + RunDomain(t, factory, NotificationSubscriptionDomain()) + RunDomain(t, factory, ProjectDomain()) + RunDomain(t, factory, RuntimeBrokerDomain()) + RunDomain(t, factory, BrokerSecretDomain()) + RunDomain(t, factory, BrokerJoinTokenDomain()) + RunDomain(t, factory, TemplateDomain()) + RunDomain(t, factory, HarnessConfigDomain()) + RunDomain(t, factory, SecretDomain()) + RunDomain(t, factory, EnvVarDomain()) + RunDomain(t, factory, AgentDomain()) + RunDomain(t, factory, UserDomain()) + RunDomain(t, factory, AllowListDomain()) + RunDomain(t, factory, InviteCodeDomain()) + + // Agent optimistic locking is not expressible through the generic CRUD + // categories, so it gets a dedicated backend-agnostic check. + t.Run("agent/OptimisticLock", func(t *testing.T) { runAgentOptimisticLock(t, factory) }) +} + +func listFrom[T any](items []T, err error) (*store.ListResult[T], error) { + if err != nil { + return nil, err + } + return &store.ListResult[T]{Items: items, TotalCount: len(items)}, nil +} + +// GroupDomain describes the group entity for the CRUD-parity oracle. +func GroupDomain() Domain[store.Group] { + return Domain[store.Group]{ + Name: "group", + Make: func(seq int) *store.Group { + id := uuid.NewString() + return &store.Group{ + ID: id, + Name: fmt.Sprintf("Group %d", seq), + Slug: fmt.Sprintf("group-%d-%s", seq, id[:8]), + Description: fmt.Sprintf("description %d", seq), + GroupType: store.GroupTypeExplicit, + Labels: map[string]string{"seq": fmt.Sprintf("%d", seq)}, + } + }, + GetID: func(g *store.Group) string { return g.ID }, + Create: func(ctx context.Context, s store.Store, g *store.Group) error { + return s.CreateGroup(ctx, g) + }, + Get: func(ctx context.Context, s store.Store, id string) (*store.Group, error) { + return s.GetGroup(ctx, id) + }, + List: func(ctx context.Context, s store.Store, opts store.ListOptions) (*store.ListResult[store.Group], error) { + return s.ListGroups(ctx, store.GroupFilter{}, opts) + }, + VerifyEqual: func(t *testing.T, want, got *store.Group) { + assert.Equal(t, want.ID, got.ID) + assert.Equal(t, want.Name, got.Name) + assert.Equal(t, want.Slug, got.Slug) + assert.Equal(t, want.Description, got.Description) + assert.Equal(t, store.GroupTypeExplicit, got.GroupType) + assert.False(t, got.Created.IsZero(), "Created timestamp should be set") + }, + Mutate: func(g *store.Group) { + g.Name = "Renamed " + g.Name + g.Description = "updated description" + }, + Update: func(ctx context.Context, s store.Store, g *store.Group) error { + return s.UpdateGroup(ctx, g) + }, + VerifyMutated: func(t *testing.T, got *store.Group) { + assert.Contains(t, got.Name, "Renamed ") + assert.Equal(t, "updated description", got.Description) + }, + Delete: func(ctx context.Context, s store.Store, id string) error { + return s.DeleteGroup(ctx, id) + }, + // Groups are hard-deleted (no SoftDelete spec). + Filters: []FilterCase[store.Group]{ + { + Name: "ByGroupType", + Seed: func(t *testing.T, ctx context.Context, s store.Store) { + require.NoError(t, s.CreateGroup(ctx, &store.Group{ + ID: uuid.NewString(), Name: "Explicit", Slug: "explicit-" + uuid.NewString()[:8], + GroupType: store.GroupTypeExplicit, + })) + require.NoError(t, s.CreateGroup(ctx, &store.Group{ + ID: uuid.NewString(), Name: "Project Agents", Slug: "project-agents-" + uuid.NewString()[:8], + GroupType: store.GroupTypeProjectAgents, + })) + }, + List: func(ctx context.Context, s store.Store) (*store.ListResult[store.Group], error) { + return s.ListGroups(ctx, store.GroupFilter{GroupType: store.GroupTypeExplicit}, store.ListOptions{}) + }, + WantCount: 1, + }, + }, + } +} + +// PolicyDomain describes the policy entity for the CRUD-parity oracle. +func PolicyDomain() Domain[store.Policy] { + return Domain[store.Policy]{ + Name: "policy", + Make: func(seq int) *store.Policy { + return &store.Policy{ + ID: uuid.NewString(), + Name: fmt.Sprintf("Policy %d", seq), + Description: fmt.Sprintf("policy description %d", seq), + ScopeType: store.PolicyScopeHub, + ResourceType: "agent", + Actions: []string{"read"}, + Effect: store.PolicyEffectAllow, + Priority: seq, + } + }, + GetID: func(p *store.Policy) string { return p.ID }, + Create: func(ctx context.Context, s store.Store, p *store.Policy) error { + return s.CreatePolicy(ctx, p) + }, + Get: func(ctx context.Context, s store.Store, id string) (*store.Policy, error) { + return s.GetPolicy(ctx, id) + }, + List: func(ctx context.Context, s store.Store, opts store.ListOptions) (*store.ListResult[store.Policy], error) { + return s.ListPolicies(ctx, store.PolicyFilter{}, opts) + }, + VerifyEqual: func(t *testing.T, want, got *store.Policy) { + assert.Equal(t, want.ID, got.ID) + assert.Equal(t, want.Name, got.Name) + assert.Equal(t, want.ScopeType, got.ScopeType) + assert.Equal(t, want.ResourceType, got.ResourceType) + assert.Equal(t, want.Actions, got.Actions) + assert.Equal(t, want.Effect, got.Effect) + assert.False(t, got.Created.IsZero(), "Created timestamp should be set") + }, + Mutate: func(p *store.Policy) { + p.Name = "Renamed " + p.Name + p.Actions = []string{"read", "update"} + }, + Update: func(ctx context.Context, s store.Store, p *store.Policy) error { + return s.UpdatePolicy(ctx, p) + }, + VerifyMutated: func(t *testing.T, got *store.Policy) { + assert.Contains(t, got.Name, "Renamed ") + assert.Equal(t, []string{"read", "update"}, got.Actions) + }, + Delete: func(ctx context.Context, s store.Store, id string) error { + return s.DeletePolicy(ctx, id) + }, + // Policies are hard-deleted (no SoftDelete spec). + Filters: []FilterCase[store.Policy]{ + { + Name: "ByEffect", + Seed: func(t *testing.T, ctx context.Context, s store.Store) { + require.NoError(t, s.CreatePolicy(ctx, &store.Policy{ + ID: uuid.NewString(), Name: "Allow", ScopeType: store.PolicyScopeHub, + ResourceType: "*", Actions: []string{"*"}, Effect: store.PolicyEffectAllow, + })) + require.NoError(t, s.CreatePolicy(ctx, &store.Policy{ + ID: uuid.NewString(), Name: "Deny", ScopeType: store.PolicyScopeHub, + ResourceType: "*", Actions: []string{"*"}, Effect: store.PolicyEffectDeny, + })) + }, + List: func(ctx context.Context, s store.Store) (*store.ListResult[store.Policy], error) { + return s.ListPolicies(ctx, store.PolicyFilter{Effect: store.PolicyEffectDeny}, store.ListOptions{}) + }, + WantCount: 1, + }, + }, + } +} + +// GCPServiceAccountDomain describes the GCP service account entity for the +// CRUD-parity oracle. The store's List methods are unpaginated, so the generic +// List (pagination) category is omitted and listing is exercised via filters. +func GCPServiceAccountDomain() Domain[store.GCPServiceAccount] { + return Domain[store.GCPServiceAccount]{ + Name: "gcp_service_account", + Make: func(seq int) *store.GCPServiceAccount { + id := uuid.NewString() + return &store.GCPServiceAccount{ + ID: id, + Scope: "project", + ScopeID: uuid.NewString(), + Email: fmt.Sprintf("sa-%d-%s@proj.iam.gserviceaccount.com", seq, id[:8]), + ProjectID: uuid.NewString(), + DisplayName: fmt.Sprintf("SA %d", seq), + DefaultScopes: []string{"https://www.googleapis.com/auth/cloud-platform"}, + CreatedBy: "tester", + } + }, + GetID: func(sa *store.GCPServiceAccount) string { return sa.ID }, + Create: func(ctx context.Context, s store.Store, sa *store.GCPServiceAccount) error { + return s.CreateGCPServiceAccount(ctx, sa) + }, + Get: func(ctx context.Context, s store.Store, id string) (*store.GCPServiceAccount, error) { + return s.GetGCPServiceAccount(ctx, id) + }, + VerifyEqual: func(t *testing.T, want, got *store.GCPServiceAccount) { + assert.Equal(t, want.ID, got.ID) + assert.Equal(t, want.Email, got.Email) + assert.Equal(t, want.Scope, got.Scope) + assert.Equal(t, want.ScopeID, got.ScopeID) + assert.Equal(t, want.DefaultScopes, got.DefaultScopes) + assert.False(t, got.CreatedAt.IsZero(), "CreatedAt should be set") + }, + Mutate: func(sa *store.GCPServiceAccount) { + sa.DisplayName = "Renamed " + sa.DisplayName + sa.Verified = true + }, + Update: func(ctx context.Context, s store.Store, sa *store.GCPServiceAccount) error { + return s.UpdateGCPServiceAccount(ctx, sa) + }, + VerifyMutated: func(t *testing.T, got *store.GCPServiceAccount) { + assert.Contains(t, got.DisplayName, "Renamed ") + assert.True(t, got.Verified) + }, + Delete: func(ctx context.Context, s store.Store, id string) error { + return s.DeleteGCPServiceAccount(ctx, id) + }, + // GCP service accounts are hard-deleted (no SoftDelete spec). + Filters: []FilterCase[store.GCPServiceAccount]{ + { + Name: "ByManaged", + Seed: func(t *testing.T, ctx context.Context, s store.Store) { + require.NoError(t, s.CreateGCPServiceAccount(ctx, &store.GCPServiceAccount{ + ID: uuid.NewString(), Scope: "project", ScopeID: uuid.NewString(), + Email: "managed-" + uuid.NewString()[:8] + "@p.iam.gserviceaccount.com", + ProjectID: uuid.NewString(), Managed: true, CreatedBy: "t", + })) + require.NoError(t, s.CreateGCPServiceAccount(ctx, &store.GCPServiceAccount{ + ID: uuid.NewString(), Scope: "project", ScopeID: uuid.NewString(), + Email: "byosa-" + uuid.NewString()[:8] + "@p.iam.gserviceaccount.com", + ProjectID: uuid.NewString(), Managed: false, CreatedBy: "t", + })) + }, + List: func(ctx context.Context, s store.Store) (*store.ListResult[store.GCPServiceAccount], error) { + managed := true + return listFrom(s.ListGCPServiceAccounts(ctx, store.GCPServiceAccountFilter{Managed: &managed})) + }, + WantCount: 1, + }, + }, + } +} + +// SubscriptionTemplateDomain describes the subscription template entity for the +// CRUD-parity oracle. Templates have no update method and an unpaginated list, +// so only Create/Read/Delete plus a filter scenario are exercised. +func SubscriptionTemplateDomain() Domain[store.SubscriptionTemplate] { + return Domain[store.SubscriptionTemplate]{ + Name: "subscription_template", + Make: func(seq int) *store.SubscriptionTemplate { + id := uuid.NewString() + return &store.SubscriptionTemplate{ + ID: id, + Name: fmt.Sprintf("template-%d-%s", seq, id[:8]), + Scope: "project", + TriggerActivities: []string{"COMPLETED", "FAILED"}, + CreatedBy: "tester", + } + }, + GetID: func(tmpl *store.SubscriptionTemplate) string { return tmpl.ID }, + Create: func(ctx context.Context, s store.Store, tmpl *store.SubscriptionTemplate) error { + return s.CreateSubscriptionTemplate(ctx, tmpl) + }, + Get: func(ctx context.Context, s store.Store, id string) (*store.SubscriptionTemplate, error) { + return s.GetSubscriptionTemplate(ctx, id) + }, + VerifyEqual: func(t *testing.T, want, got *store.SubscriptionTemplate) { + assert.Equal(t, want.ID, got.ID) + assert.Equal(t, want.Name, got.Name) + assert.Equal(t, want.Scope, got.Scope) + assert.Equal(t, want.TriggerActivities, got.TriggerActivities) + }, + Delete: func(ctx context.Context, s store.Store, id string) error { + return s.DeleteSubscriptionTemplate(ctx, id) + }, + // Templates are hard-deleted (no SoftDelete spec). + Filters: []FilterCase[store.SubscriptionTemplate]{ + { + Name: "GlobalOnly", + Seed: func(t *testing.T, ctx context.Context, s store.Store) { + require.NoError(t, s.CreateSubscriptionTemplate(ctx, &store.SubscriptionTemplate{ + ID: uuid.NewString(), Name: "global-" + uuid.NewString()[:8], + Scope: "project", TriggerActivities: []string{"COMPLETED"}, CreatedBy: "t", + })) + require.NoError(t, s.CreateSubscriptionTemplate(ctx, &store.SubscriptionTemplate{ + ID: uuid.NewString(), Name: "scoped-" + uuid.NewString()[:8], + Scope: "project", TriggerActivities: []string{"FAILED"}, + ProjectID: uuid.NewString(), CreatedBy: "t", + })) + }, + List: func(ctx context.Context, s store.Store) (*store.ListResult[store.SubscriptionTemplate], error) { + return listFrom(s.ListSubscriptionTemplates(ctx, "")) + }, + WantCount: 1, + }, + }, + } +} + +// NotificationSubscriptionDomain describes the notification subscription entity +// for the CRUD-parity oracle. Project-scoped subscriptions are used so the +// fixtures do not depend on a pre-existing agent (agent-scoped subscriptions +// carry a foreign key to agents). The store's list methods are unpaginated, so +// the generic pagination category is omitted. +func NotificationSubscriptionDomain() Domain[store.NotificationSubscription] { + return Domain[store.NotificationSubscription]{ + Name: "notification_subscription", + Make: func(seq int) *store.NotificationSubscription { + return &store.NotificationSubscription{ + ID: uuid.NewString(), + Scope: store.SubscriptionScopeProject, + SubscriberType: "user", + SubscriberID: fmt.Sprintf("user-%d", seq), + ProjectID: uuid.NewString(), + TriggerActivities: []string{"COMPLETED"}, + CreatedBy: "tester", + } + }, + GetID: func(sub *store.NotificationSubscription) string { return sub.ID }, + Create: func(ctx context.Context, s store.Store, sub *store.NotificationSubscription) error { + return s.CreateNotificationSubscription(ctx, sub) + }, + Get: func(ctx context.Context, s store.Store, id string) (*store.NotificationSubscription, error) { + return s.GetNotificationSubscription(ctx, id) + }, + VerifyEqual: func(t *testing.T, want, got *store.NotificationSubscription) { + assert.Equal(t, want.ID, got.ID) + assert.Equal(t, want.Scope, got.Scope) + assert.Equal(t, want.SubscriberID, got.SubscriberID) + assert.Equal(t, want.TriggerActivities, got.TriggerActivities) + assert.False(t, got.CreatedAt.IsZero(), "CreatedAt should be set") + }, + Mutate: func(sub *store.NotificationSubscription) { + sub.TriggerActivities = []string{"COMPLETED", "FAILED"} + }, + Update: func(ctx context.Context, s store.Store, sub *store.NotificationSubscription) error { + return s.UpdateNotificationSubscriptionTriggers(ctx, sub.ID, sub.TriggerActivities) + }, + VerifyMutated: func(t *testing.T, got *store.NotificationSubscription) { + assert.Equal(t, []string{"COMPLETED", "FAILED"}, got.TriggerActivities) + }, + Delete: func(ctx context.Context, s store.Store, id string) error { + return s.DeleteNotificationSubscription(ctx, id) + }, + // Notification subscriptions are hard-deleted (no SoftDelete spec). + Filters: []FilterCase[store.NotificationSubscription]{ + { + Name: "ByProjectScope", + Seed: func(t *testing.T, ctx context.Context, s store.Store) { + projectID := uuid.NewString() + require.NoError(t, s.CreateNotificationSubscription(ctx, &store.NotificationSubscription{ + ID: uuid.NewString(), Scope: store.SubscriptionScopeProject, + SubscriberType: "user", SubscriberID: "u1", ProjectID: projectID, + TriggerActivities: []string{"COMPLETED"}, CreatedBy: "t", + })) + require.NoError(t, s.CreateNotificationSubscription(ctx, &store.NotificationSubscription{ + ID: uuid.NewString(), Scope: store.SubscriptionScopeProject, + SubscriberType: "user", SubscriberID: "u2", ProjectID: projectID, + TriggerActivities: []string{"FAILED"}, CreatedBy: "t", + })) + }, + List: func(ctx context.Context, s store.Store) (*store.ListResult[store.NotificationSubscription], error) { + // Both seeded subscriptions are project-scoped under distinct + // projects; query project scope for the first project only. + all, err := s.GetSubscriptionsForSubscriber(ctx, "user", "u1") + return listFrom(all, err) + }, + WantCount: 1, + }, + }, + } +} + +// agentDomainProjectID is the project every agent oracle entity references. It +// is seeded by AgentDomain.Prepare so the required project foreign key resolves +// across backends. +const agentDomainProjectID = "30000000-0000-0000-0000-0000000000d1" + +// seedAgentProject creates the shared project agents reference. It is called +// once per fresh store before the agent categories run. +func seedAgentProject(t *testing.T, ctx context.Context, s store.Store) { + t.Helper() + require.NoError(t, s.CreateProject(ctx, &store.Project{ + ID: agentDomainProjectID, + Name: "agent-oracle-project", + Slug: "agent-oracle-" + agentDomainProjectID[:8], + Visibility: "private", + })) +} + +// newOracleAgent builds a minimal valid agent referencing the seeded project. +func newOracleAgent(slug string) *store.Agent { + id := uuid.NewString() + return &store.Agent{ + ID: id, + Slug: slug + "-" + id[:8], + Name: slug, + Template: "default", + ProjectID: agentDomainProjectID, + Phase: "running", + Visibility: "private", + } +} + +// seedLiveAndDeleted inserts one live agent and one soft-deleted agent. +func seedLiveAndDeleted(t *testing.T, ctx context.Context, s store.Store) { + t.Helper() + live := newOracleAgent("live") + require.NoError(t, s.CreateAgent(ctx, live)) + + gone := newOracleAgent("gone") + require.NoError(t, s.CreateAgent(ctx, gone)) + gone.DeletedAt = time.Now() + require.NoError(t, s.UpdateAgent(ctx, gone)) +} + +// AgentDomain describes the agent entity for the CRUD-parity oracle. Beyond the +// standard categories it covers the agent-specific behaviors that must hold +// identically across backends: the ancestry membership filter, soft-delete +// exclusion, and (via runAgentOptimisticLock) state_version conflict handling. +func AgentDomain() Domain[store.Agent] { + return Domain[store.Agent]{ + Name: "agent", + Prepare: func(t *testing.T, ctx context.Context, s store.Store) { + seedAgentProject(t, ctx, s) + }, + Make: func(seq int) *store.Agent { + id := uuid.NewString() + return &store.Agent{ + ID: id, + Slug: fmt.Sprintf("agent-%d-%s", seq, id[:8]), + Name: fmt.Sprintf("Agent %d", seq), + Template: "default", + ProjectID: agentDomainProjectID, + Phase: "running", + Activity: "thinking", + Visibility: "private", + Labels: map[string]string{"seq": fmt.Sprintf("%d", seq)}, + } + }, + GetID: func(a *store.Agent) string { return a.ID }, + Create: func(ctx context.Context, s store.Store, a *store.Agent) error { + return s.CreateAgent(ctx, a) + }, + Get: func(ctx context.Context, s store.Store, id string) (*store.Agent, error) { + return s.GetAgent(ctx, id) + }, + List: func(ctx context.Context, s store.Store, opts store.ListOptions) (*store.ListResult[store.Agent], error) { + return s.ListAgents(ctx, store.AgentFilter{}, opts) + }, + VerifyEqual: func(t *testing.T, want, got *store.Agent) { + assert.Equal(t, want.ID, got.ID) + assert.Equal(t, want.Slug, got.Slug) + assert.Equal(t, want.Name, got.Name) + assert.Equal(t, want.ProjectID, got.ProjectID) + assert.Equal(t, want.Phase, got.Phase) + assert.Equal(t, int64(1), got.StateVersion, "CreateAgent should initialize state_version to 1") + assert.False(t, got.Created.IsZero(), "Created timestamp should be set") + }, + Mutate: func(a *store.Agent) { + a.Name = "Renamed " + a.Name + a.Phase = "stopped" + }, + Update: func(ctx context.Context, s store.Store, a *store.Agent) error { + return s.UpdateAgent(ctx, a) + }, + VerifyMutated: func(t *testing.T, got *store.Agent) { + assert.Contains(t, got.Name, "Renamed ") + assert.Equal(t, "stopped", got.Phase) + assert.Equal(t, int64(2), got.StateVersion, "UpdateAgent should bump state_version") + }, + // DeleteAgent is a hard delete; soft-delete (deleted_at via UpdateAgent) + // is covered by the filter cases below. + Delete: func(ctx context.Context, s store.Store, id string) error { + return s.DeleteAgent(ctx, id) + }, + Filters: []FilterCase[store.Agent]{ + { + // Ancestry membership: only agents whose ancestry chain contains + // the queried principal are returned. + Name: "ByAncestor", + Seed: func(t *testing.T, ctx context.Context, s store.Store) { + child := newOracleAgent("child") + child.Ancestry = []string{"root-user", "mid-agent"} + require.NoError(t, s.CreateAgent(ctx, child)) + + sibling := newOracleAgent("sibling") + sibling.Ancestry = []string{"root-user"} + require.NoError(t, s.CreateAgent(ctx, sibling)) + + orphan := newOracleAgent("orphan") + require.NoError(t, s.CreateAgent(ctx, orphan)) + }, + List: func(ctx context.Context, s store.Store) (*store.ListResult[store.Agent], error) { + return s.ListAgents(ctx, store.AgentFilter{AncestorID: "root-user"}, store.ListOptions{}) + }, + WantCount: 2, + }, + { + // Soft-deleted agents are excluded from the default listing. + Name: "ExcludeSoftDeleted", + Seed: seedLiveAndDeleted, + List: func(ctx context.Context, s store.Store) (*store.ListResult[store.Agent], error) { + return s.ListAgents(ctx, store.AgentFilter{}, store.ListOptions{}) + }, + WantCount: 1, + }, + { + // ... but reappear when explicitly included. + Name: "IncludeSoftDeleted", + Seed: seedLiveAndDeleted, + List: func(ctx context.Context, s store.Store) (*store.ListResult[store.Agent], error) { + return s.ListAgents(ctx, store.AgentFilter{IncludeDeleted: true}, store.ListOptions{}) + }, + WantCount: 2, + }, + }, + } +} + +// runAgentOptimisticLock verifies that a stale UpdateAgent (one carrying an +// out-of-date StateVersion) is rejected with ErrVersionConflict rather than +// silently overwriting a concurrent winner. +func runAgentOptimisticLock(t *testing.T, factory Factory) { + ctx := context.Background() + s := factory(t) + seedAgentProject(t, ctx, s) + + a := newOracleAgent("locked") + require.NoError(t, s.CreateAgent(ctx, a)) + + first, err := s.GetAgent(ctx, a.ID) + require.NoError(t, err) + second, err := s.GetAgent(ctx, a.ID) + require.NoError(t, err) + + // First writer wins, advancing the version. + first.Name = "Winner" + require.NoError(t, s.UpdateAgent(ctx, first)) + + // Second writer holds the now-stale version and must conflict. + second.Name = "Loser" + assert.ErrorIs(t, s.UpdateAgent(ctx, second), store.ErrVersionConflict) + + final, err := s.GetAgent(ctx, a.ID) + require.NoError(t, err) + assert.Equal(t, "Winner", final.Name) +} diff --git a/pkg/store/storetest/domains_project_broker.go b/pkg/store/storetest/domains_project_broker.go new file mode 100644 index 000000000..9af66c8dc --- /dev/null +++ b/pkg/store/storetest/domains_project_broker.go @@ -0,0 +1,258 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package storetest + +import ( + "context" + "fmt" + "testing" + "time" + + "github.com/GoogleCloudPlatform/scion/pkg/store" + "github.com/google/uuid" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +// ProjectDomain describes the project (grove) entity for the CRUD-parity oracle. +func ProjectDomain() Domain[store.Project] { + return Domain[store.Project]{ + Name: "project", + Make: func(seq int) *store.Project { + id := uuid.NewString() + return &store.Project{ + ID: id, + Name: fmt.Sprintf("Project %d", seq), + Slug: fmt.Sprintf("project-%d-%s", seq, id[:8]), + Visibility: store.VisibilityPrivate, + Labels: map[string]string{"seq": fmt.Sprintf("%d", seq)}, + } + }, + GetID: func(p *store.Project) string { return p.ID }, + Create: func(ctx context.Context, s store.Store, p *store.Project) error { + return s.CreateProject(ctx, p) + }, + Get: func(ctx context.Context, s store.Store, id string) (*store.Project, error) { + return s.GetProject(ctx, id) + }, + List: func(ctx context.Context, s store.Store, opts store.ListOptions) (*store.ListResult[store.Project], error) { + return s.ListProjects(ctx, store.ProjectFilter{}, opts) + }, + VerifyEqual: func(t *testing.T, want, got *store.Project) { + assert.Equal(t, want.ID, got.ID) + assert.Equal(t, want.Name, got.Name) + assert.Equal(t, want.Slug, got.Slug) + assert.Equal(t, want.Visibility, got.Visibility) + assert.False(t, got.Created.IsZero(), "Created timestamp should be set") + }, + Mutate: func(p *store.Project) { + p.Name = "Renamed " + p.Name + p.Visibility = "public" + }, + Update: func(ctx context.Context, s store.Store, p *store.Project) error { + return s.UpdateProject(ctx, p) + }, + VerifyMutated: func(t *testing.T, got *store.Project) { + assert.Contains(t, got.Name, "Renamed ") + assert.Equal(t, "public", got.Visibility) + }, + Delete: func(ctx context.Context, s store.Store, id string) error { + return s.DeleteProject(ctx, id) + }, + Filters: []FilterCase[store.Project]{ + { + Name: "ByVisibility", + Seed: func(t *testing.T, ctx context.Context, s store.Store) { + require.NoError(t, s.CreateProject(ctx, &store.Project{ + ID: uuid.NewString(), Name: "Public", Slug: "public-" + uuid.NewString()[:8], Visibility: "public", + })) + require.NoError(t, s.CreateProject(ctx, &store.Project{ + ID: uuid.NewString(), Name: "Private", Slug: "private-" + uuid.NewString()[:8], Visibility: store.VisibilityPrivate, + })) + }, + List: func(ctx context.Context, s store.Store) (*store.ListResult[store.Project], error) { + return s.ListProjects(ctx, store.ProjectFilter{Visibility: "public"}, store.ListOptions{}) + }, + WantCount: 1, + }, + }, + } +} + +// RuntimeBrokerDomain describes the runtime broker entity for the oracle. +func RuntimeBrokerDomain() Domain[store.RuntimeBroker] { + return Domain[store.RuntimeBroker]{ + Name: "runtime_broker", + Make: func(seq int) *store.RuntimeBroker { + id := uuid.NewString() + return &store.RuntimeBroker{ + ID: id, + Name: fmt.Sprintf("Broker %d", seq), + Slug: fmt.Sprintf("broker-%d-%s", seq, id[:8]), + Version: "1.0.0", + Status: store.BrokerStatusOffline, + } + }, + GetID: func(b *store.RuntimeBroker) string { return b.ID }, + Create: func(ctx context.Context, s store.Store, b *store.RuntimeBroker) error { + return s.CreateRuntimeBroker(ctx, b) + }, + Get: func(ctx context.Context, s store.Store, id string) (*store.RuntimeBroker, error) { + return s.GetRuntimeBroker(ctx, id) + }, + List: func(ctx context.Context, s store.Store, opts store.ListOptions) (*store.ListResult[store.RuntimeBroker], error) { + return s.ListRuntimeBrokers(ctx, store.RuntimeBrokerFilter{}, opts) + }, + VerifyEqual: func(t *testing.T, want, got *store.RuntimeBroker) { + assert.Equal(t, want.ID, got.ID) + assert.Equal(t, want.Name, got.Name) + assert.Equal(t, want.Slug, got.Slug) + assert.Equal(t, want.Version, got.Version) + assert.False(t, got.Created.IsZero(), "Created timestamp should be set") + }, + Mutate: func(b *store.RuntimeBroker) { + b.Name = "Renamed " + b.Name + b.Version = "2.0.0" + b.Status = store.BrokerStatusOnline + }, + Update: func(ctx context.Context, s store.Store, b *store.RuntimeBroker) error { + return s.UpdateRuntimeBroker(ctx, b) + }, + VerifyMutated: func(t *testing.T, got *store.RuntimeBroker) { + assert.Contains(t, got.Name, "Renamed ") + assert.Equal(t, "2.0.0", got.Version) + }, + Delete: func(ctx context.Context, s store.Store, id string) error { + return s.DeleteRuntimeBroker(ctx, id) + }, + Filters: []FilterCase[store.RuntimeBroker]{ + { + Name: "ByStatus", + Seed: func(t *testing.T, ctx context.Context, s store.Store) { + require.NoError(t, s.CreateRuntimeBroker(ctx, &store.RuntimeBroker{ + ID: uuid.NewString(), Name: "Online", Slug: "online-" + uuid.NewString()[:8], Status: store.BrokerStatusOnline, + })) + require.NoError(t, s.CreateRuntimeBroker(ctx, &store.RuntimeBroker{ + ID: uuid.NewString(), Name: "Offline", Slug: "offline-" + uuid.NewString()[:8], Status: store.BrokerStatusOffline, + })) + }, + List: func(ctx context.Context, s store.Store) (*store.ListResult[store.RuntimeBroker], error) { + return s.ListRuntimeBrokers(ctx, store.RuntimeBrokerFilter{Status: store.BrokerStatusOnline}, store.ListOptions{}) + }, + WantCount: 1, + }, + }, + } +} + +// ensureBroker creates the runtime_brokers row a broker-scoped entity references +// via foreign key (broker_secrets / broker_join_tokens on the SQLite backend). +// It is idempotent: an already-existing broker is not an error. It keeps these +// domains self-contained without relying on a shared Prepare hook. +func ensureBroker(ctx context.Context, s store.Store, brokerID string) error { + err := s.CreateRuntimeBroker(ctx, &store.RuntimeBroker{ + ID: brokerID, + Name: "fk-broker-" + brokerID[:8], + Slug: "fk-broker-" + brokerID[:8], + }) + if err != nil && err != store.ErrAlreadyExists { + return err + } + return nil +} + +// BrokerSecretDomain describes the broker secret entity for the oracle. It has +// no List operation (one secret per broker, keyed on broker_id), so the +// pagination and filter categories are skipped. +func BrokerSecretDomain() Domain[store.BrokerSecret] { + return Domain[store.BrokerSecret]{ + Name: "broker_secret", + Make: func(seq int) *store.BrokerSecret { + return &store.BrokerSecret{ + BrokerID: uuid.NewString(), + SecretKey: []byte(fmt.Sprintf("hmac-key-%d", seq)), + Algorithm: store.BrokerSecretAlgorithmHMACSHA256, + Status: store.BrokerSecretStatusActive, + } + }, + GetID: func(b *store.BrokerSecret) string { return b.BrokerID }, + Create: func(ctx context.Context, s store.Store, b *store.BrokerSecret) error { + if err := ensureBroker(ctx, s, b.BrokerID); err != nil { + return err + } + return s.CreateBrokerSecret(ctx, b) + }, + Get: func(ctx context.Context, s store.Store, id string) (*store.BrokerSecret, error) { + return s.GetBrokerSecret(ctx, id) + }, + VerifyEqual: func(t *testing.T, want, got *store.BrokerSecret) { + assert.Equal(t, want.BrokerID, got.BrokerID) + assert.Equal(t, want.SecretKey, got.SecretKey) + assert.Equal(t, want.Algorithm, got.Algorithm) + assert.Equal(t, want.Status, got.Status) + assert.False(t, got.CreatedAt.IsZero(), "CreatedAt timestamp should be set") + }, + Mutate: func(b *store.BrokerSecret) { + b.SecretKey = []byte("rotated-key") + b.Status = store.BrokerSecretStatusDeprecated + }, + Update: func(ctx context.Context, s store.Store, b *store.BrokerSecret) error { + return s.UpdateBrokerSecret(ctx, b) + }, + VerifyMutated: func(t *testing.T, got *store.BrokerSecret) { + assert.Equal(t, []byte("rotated-key"), got.SecretKey) + assert.Equal(t, store.BrokerSecretStatusDeprecated, got.Status) + }, + Delete: func(ctx context.Context, s store.Store, id string) error { + return s.DeleteBrokerSecret(ctx, id) + }, + } +} + +// BrokerJoinTokenDomain describes the broker join token entity for the oracle. +// Join tokens are immutable (no Update) and have no List, so only the Create, +// Read and Delete categories apply. +func BrokerJoinTokenDomain() Domain[store.BrokerJoinToken] { + return Domain[store.BrokerJoinToken]{ + Name: "broker_join_token", + Make: func(seq int) *store.BrokerJoinToken { + return &store.BrokerJoinToken{ + BrokerID: uuid.NewString(), + TokenHash: fmt.Sprintf("token-hash-%d-%s", seq, uuid.NewString()), + ExpiresAt: time.Now().Add(time.Hour), + CreatedBy: uuid.NewString(), + } + }, + GetID: func(tok *store.BrokerJoinToken) string { return tok.BrokerID }, + Create: func(ctx context.Context, s store.Store, tok *store.BrokerJoinToken) error { + if err := ensureBroker(ctx, s, tok.BrokerID); err != nil { + return err + } + return s.CreateJoinToken(ctx, tok) + }, + Get: func(ctx context.Context, s store.Store, id string) (*store.BrokerJoinToken, error) { + return s.GetJoinTokenByBrokerID(ctx, id) + }, + VerifyEqual: func(t *testing.T, want, got *store.BrokerJoinToken) { + assert.Equal(t, want.BrokerID, got.BrokerID) + assert.Equal(t, want.TokenHash, got.TokenHash) + assert.Equal(t, want.CreatedBy, got.CreatedBy) + assert.False(t, got.CreatedAt.IsZero(), "CreatedAt timestamp should be set") + }, + Delete: func(ctx context.Context, s store.Store, id string) error { + return s.DeleteJoinToken(ctx, id) + }, + } +} diff --git a/pkg/store/storetest/domains_secret_template.go b/pkg/store/storetest/domains_secret_template.go new file mode 100644 index 000000000..6c650b322 --- /dev/null +++ b/pkg/store/storetest/domains_secret_template.go @@ -0,0 +1,317 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package storetest + +import ( + "context" + "fmt" + "testing" + + "github.com/GoogleCloudPlatform/scion/pkg/store" + "github.com/google/uuid" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +// secretTestScopeID is a fixed scope identifier shared by all entities a single +// Secret/EnvVar domain run creates, so the harness's (key, scope, scope_id) +// lookups resolve consistently within one store. +const secretTestScopeID = "00000000-0000-0000-0000-0000000000aa" + +// listResultFrom wraps a plain slice from a non-paginated list method into a +// ListResult so it can satisfy a FilterCase. TotalCount mirrors the slice +// length, which is the contract the filter oracle checks. +func listResultFrom[T any](items []T, err error) (*store.ListResult[T], error) { + if err != nil { + return nil, err + } + return &store.ListResult[T]{Items: items, TotalCount: len(items)}, nil +} + +// TemplateDomain describes the template entity for the CRUD-parity oracle. +func TemplateDomain() Domain[store.Template] { + return Domain[store.Template]{ + Name: "template", + Make: func(seq int) *store.Template { + id := uuid.NewString() + return &store.Template{ + ID: id, + Name: fmt.Sprintf("Template %d", seq), + Slug: fmt.Sprintf("template-%d-%s", seq, id[:8]), + Harness: "claude", + Image: "img:latest", + Scope: store.TemplateScopeGlobal, + Visibility: "private", + Status: store.TemplateStatusActive, + ContentHash: fmt.Sprintf("hash-%d", seq), + } + }, + GetID: func(e *store.Template) string { return e.ID }, + Create: func(ctx context.Context, s store.Store, e *store.Template) error { + return s.CreateTemplate(ctx, e) + }, + Get: func(ctx context.Context, s store.Store, id string) (*store.Template, error) { + return s.GetTemplate(ctx, id) + }, + List: func(ctx context.Context, s store.Store, opts store.ListOptions) (*store.ListResult[store.Template], error) { + return s.ListTemplates(ctx, store.TemplateFilter{}, opts) + }, + VerifyEqual: func(t *testing.T, want, got *store.Template) { + assert.Equal(t, want.ID, got.ID) + assert.Equal(t, want.Name, got.Name) + assert.Equal(t, want.Slug, got.Slug) + assert.Equal(t, want.Harness, got.Harness) + assert.Equal(t, want.Scope, got.Scope) + assert.False(t, got.Created.IsZero(), "Created timestamp should be set") + }, + Mutate: func(e *store.Template) { + e.Name = "Renamed " + e.Name + e.Status = store.TemplateStatusArchived + }, + Update: func(ctx context.Context, s store.Store, e *store.Template) error { + return s.UpdateTemplate(ctx, e) + }, + VerifyMutated: func(t *testing.T, got *store.Template) { + assert.Contains(t, got.Name, "Renamed ") + assert.Equal(t, store.TemplateStatusArchived, got.Status) + }, + Delete: func(ctx context.Context, s store.Store, id string) error { + return s.DeleteTemplate(ctx, id) + }, + Filters: []FilterCase[store.Template]{ + { + Name: "ByHarness", + Seed: func(t *testing.T, ctx context.Context, s store.Store) { + require.NoError(t, s.CreateTemplate(ctx, &store.Template{ + ID: uuid.NewString(), Name: "Claude", Slug: "claude-" + uuid.NewString()[:8], + Harness: "claude", Scope: store.TemplateScopeGlobal, Status: store.TemplateStatusActive, + })) + require.NoError(t, s.CreateTemplate(ctx, &store.Template{ + ID: uuid.NewString(), Name: "Gemini", Slug: "gemini-" + uuid.NewString()[:8], + Harness: "gemini", Scope: store.TemplateScopeGlobal, Status: store.TemplateStatusActive, + })) + }, + List: func(ctx context.Context, s store.Store) (*store.ListResult[store.Template], error) { + return s.ListTemplates(ctx, store.TemplateFilter{Harness: "gemini"}, store.ListOptions{}) + }, + WantCount: 1, + }, + }, + } +} + +// HarnessConfigDomain describes the harness config entity for the CRUD-parity oracle. +func HarnessConfigDomain() Domain[store.HarnessConfig] { + return Domain[store.HarnessConfig]{ + Name: "harness_config", + Make: func(seq int) *store.HarnessConfig { + id := uuid.NewString() + return &store.HarnessConfig{ + ID: id, + Name: fmt.Sprintf("Harness %d", seq), + Slug: fmt.Sprintf("harness-%d-%s", seq, id[:8]), + Harness: "claude", + Scope: store.HarnessConfigScopeGlobal, + Visibility: "private", + Status: store.HarnessConfigStatusActive, + ContentHash: fmt.Sprintf("hash-%d", seq), + } + }, + GetID: func(e *store.HarnessConfig) string { return e.ID }, + Create: func(ctx context.Context, s store.Store, e *store.HarnessConfig) error { + return s.CreateHarnessConfig(ctx, e) + }, + Get: func(ctx context.Context, s store.Store, id string) (*store.HarnessConfig, error) { + return s.GetHarnessConfig(ctx, id) + }, + List: func(ctx context.Context, s store.Store, opts store.ListOptions) (*store.ListResult[store.HarnessConfig], error) { + return s.ListHarnessConfigs(ctx, store.HarnessConfigFilter{}, opts) + }, + VerifyEqual: func(t *testing.T, want, got *store.HarnessConfig) { + assert.Equal(t, want.ID, got.ID) + assert.Equal(t, want.Name, got.Name) + assert.Equal(t, want.Slug, got.Slug) + assert.Equal(t, want.Harness, got.Harness) + assert.Equal(t, want.Scope, got.Scope) + assert.False(t, got.Created.IsZero(), "Created timestamp should be set") + }, + Mutate: func(e *store.HarnessConfig) { + e.Name = "Renamed " + e.Name + e.Status = store.HarnessConfigStatusArchived + }, + Update: func(ctx context.Context, s store.Store, e *store.HarnessConfig) error { + return s.UpdateHarnessConfig(ctx, e) + }, + VerifyMutated: func(t *testing.T, got *store.HarnessConfig) { + assert.Contains(t, got.Name, "Renamed ") + assert.Equal(t, store.HarnessConfigStatusArchived, got.Status) + }, + Delete: func(ctx context.Context, s store.Store, id string) error { + return s.DeleteHarnessConfig(ctx, id) + }, + Filters: []FilterCase[store.HarnessConfig]{ + { + Name: "ByHarness", + Seed: func(t *testing.T, ctx context.Context, s store.Store) { + require.NoError(t, s.CreateHarnessConfig(ctx, &store.HarnessConfig{ + ID: uuid.NewString(), Name: "Claude", Slug: "claude-" + uuid.NewString()[:8], + Harness: "claude", Scope: store.HarnessConfigScopeGlobal, Status: store.HarnessConfigStatusActive, + })) + require.NoError(t, s.CreateHarnessConfig(ctx, &store.HarnessConfig{ + ID: uuid.NewString(), Name: "Gemini", Slug: "gemini-" + uuid.NewString()[:8], + Harness: "gemini", Scope: store.HarnessConfigScopeGlobal, Status: store.HarnessConfigStatusActive, + })) + }, + List: func(ctx context.Context, s store.Store) (*store.ListResult[store.HarnessConfig], error) { + return s.ListHarnessConfigs(ctx, store.HarnessConfigFilter{Harness: "gemini"}, store.ListOptions{}) + }, + WantCount: 1, + }, + }, + } +} + +// SecretDomain describes the secret entity for the CRUD-parity oracle. Secrets +// are addressed by (key, scope, scope_id) rather than a surrogate ID, so the +// harness's id parameter carries the key and a fixed scope/scope_id pair is used +// throughout a run. Listing is non-paginated, so only the filter category (not +// pagination) is exercised here. +func SecretDomain() Domain[store.Secret] { + return Domain[store.Secret]{ + Name: "secret", + Make: func(seq int) *store.Secret { + return &store.Secret{ + ID: uuid.NewString(), + Key: fmt.Sprintf("SECRET_%d", seq), + EncryptedValue: fmt.Sprintf("enc-%d", seq), + Scope: store.ScopeUser, + ScopeID: secretTestScopeID, + Description: fmt.Sprintf("secret %d", seq), + } + }, + GetID: func(e *store.Secret) string { return e.Key }, + Create: func(ctx context.Context, s store.Store, e *store.Secret) error { + return s.CreateSecret(ctx, e) + }, + Get: func(ctx context.Context, s store.Store, id string) (*store.Secret, error) { + return s.GetSecret(ctx, id, store.ScopeUser, secretTestScopeID) + }, + VerifyEqual: func(t *testing.T, want, got *store.Secret) { + assert.Equal(t, want.Key, got.Key) + assert.Equal(t, want.EncryptedValue, got.EncryptedValue) + assert.Equal(t, want.Scope, got.Scope) + assert.Equal(t, 1, got.Version, "new secret starts at version 1") + assert.False(t, got.Created.IsZero(), "Created timestamp should be set") + }, + Mutate: func(e *store.Secret) { + e.EncryptedValue = "rotated" + e.Description = "changed" + }, + Update: func(ctx context.Context, s store.Store, e *store.Secret) error { + return s.UpdateSecret(ctx, e) + }, + VerifyMutated: func(t *testing.T, got *store.Secret) { + assert.Equal(t, "rotated", got.EncryptedValue) + assert.GreaterOrEqual(t, got.Version, 2, "update should bump version") + }, + Delete: func(ctx context.Context, s store.Store, id string) error { + return s.DeleteSecret(ctx, id, store.ScopeUser, secretTestScopeID) + }, + Filters: []FilterCase[store.Secret]{ + { + Name: "ByType", + Seed: func(t *testing.T, ctx context.Context, s store.Store) { + require.NoError(t, s.CreateSecret(ctx, &store.Secret{ + ID: uuid.NewString(), Key: "ENV_SECRET", EncryptedValue: "v", + SecretType: store.SecretTypeEnvironment, Scope: store.ScopeUser, ScopeID: secretTestScopeID, + })) + require.NoError(t, s.CreateSecret(ctx, &store.Secret{ + ID: uuid.NewString(), Key: "FILE_SECRET", EncryptedValue: "v", + SecretType: store.SecretTypeFile, Target: "/etc/x", Scope: store.ScopeUser, ScopeID: secretTestScopeID, + })) + }, + List: func(ctx context.Context, s store.Store) (*store.ListResult[store.Secret], error) { + return listResultFrom(s.ListSecrets(ctx, store.SecretFilter{ + Scope: store.ScopeUser, ScopeID: secretTestScopeID, Type: store.SecretTypeFile, + })) + }, + WantCount: 1, + }, + }, + } +} + +// EnvVarDomain describes the env var entity for the CRUD-parity oracle. Like +// secrets, env vars are addressed by (key, scope, scope_id); see SecretDomain. +func EnvVarDomain() Domain[store.EnvVar] { + return Domain[store.EnvVar]{ + Name: "env_var", + Make: func(seq int) *store.EnvVar { + return &store.EnvVar{ + ID: uuid.NewString(), + Key: fmt.Sprintf("ENV_%d", seq), + Value: fmt.Sprintf("val-%d", seq), + Scope: store.ScopeUser, + ScopeID: secretTestScopeID, + InjectionMode: store.InjectionModeAsNeeded, + Description: fmt.Sprintf("env %d", seq), + } + }, + GetID: func(e *store.EnvVar) string { return e.Key }, + Create: func(ctx context.Context, s store.Store, e *store.EnvVar) error { + return s.CreateEnvVar(ctx, e) + }, + Get: func(ctx context.Context, s store.Store, id string) (*store.EnvVar, error) { + return s.GetEnvVar(ctx, id, store.ScopeUser, secretTestScopeID) + }, + VerifyEqual: func(t *testing.T, want, got *store.EnvVar) { + assert.Equal(t, want.Key, got.Key) + assert.Equal(t, want.Value, got.Value) + assert.Equal(t, want.Scope, got.Scope) + assert.False(t, got.Created.IsZero(), "Created timestamp should be set") + }, + Mutate: func(e *store.EnvVar) { + e.Value = "updated" + }, + Update: func(ctx context.Context, s store.Store, e *store.EnvVar) error { + return s.UpdateEnvVar(ctx, e) + }, + VerifyMutated: func(t *testing.T, got *store.EnvVar) { + assert.Equal(t, "updated", got.Value) + }, + Delete: func(ctx context.Context, s store.Store, id string) error { + return s.DeleteEnvVar(ctx, id, store.ScopeUser, secretTestScopeID) + }, + Filters: []FilterCase[store.EnvVar]{ + { + Name: "ByKey", + Seed: func(t *testing.T, ctx context.Context, s store.Store) { + require.NoError(t, s.CreateEnvVar(ctx, &store.EnvVar{ + ID: uuid.NewString(), Key: "KEEP", Value: "v", Scope: store.ScopeUser, ScopeID: secretTestScopeID, + })) + require.NoError(t, s.CreateEnvVar(ctx, &store.EnvVar{ + ID: uuid.NewString(), Key: "OTHER", Value: "v", Scope: store.ScopeUser, ScopeID: secretTestScopeID, + })) + }, + List: func(ctx context.Context, s store.Store) (*store.ListResult[store.EnvVar], error) { + return listResultFrom(s.ListEnvVars(ctx, store.EnvVarFilter{ + Scope: store.ScopeUser, ScopeID: secretTestScopeID, Key: "KEEP", + })) + }, + WantCount: 1, + }, + }, + } +} diff --git a/pkg/store/storetest/domains_user.go b/pkg/store/storetest/domains_user.go new file mode 100644 index 000000000..94dd3a263 --- /dev/null +++ b/pkg/store/storetest/domains_user.go @@ -0,0 +1,201 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package storetest + +import ( + "context" + "fmt" + "testing" + "time" + + "github.com/GoogleCloudPlatform/scion/pkg/store" + "github.com/google/uuid" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +// UserDomain describes the user entity for the CRUD-parity oracle. +// +// Add `RunDomain(t, factory, UserDomain())` to RunStoreSuite (in domains.go) +// to cover it across all backends. It is kept in this separate file so the +// user/allowlist port can land without contending on domains.go. +func UserDomain() Domain[store.User] { + return Domain[store.User]{ + Name: "user", + Make: func(seq int) *store.User { + id := uuid.NewString() + return &store.User{ + ID: id, + Email: fmt.Sprintf("user-%d-%s@example.com", seq, id[:8]), + DisplayName: fmt.Sprintf("User %d", seq), + Role: store.UserRoleMember, + Status: "active", + } + }, + GetID: func(u *store.User) string { return u.ID }, + Create: func(ctx context.Context, s store.Store, u *store.User) error { + return s.CreateUser(ctx, u) + }, + Get: func(ctx context.Context, s store.Store, id string) (*store.User, error) { + return s.GetUser(ctx, id) + }, + List: func(ctx context.Context, s store.Store, opts store.ListOptions) (*store.ListResult[store.User], error) { + return s.ListUsers(ctx, store.UserFilter{}, opts) + }, + VerifyEqual: func(t *testing.T, want, got *store.User) { + assert.Equal(t, want.ID, got.ID) + assert.Equal(t, want.Email, got.Email) + assert.Equal(t, want.DisplayName, got.DisplayName) + assert.Equal(t, want.Role, got.Role) + assert.Equal(t, want.Status, got.Status) + assert.False(t, got.Created.IsZero(), "Created timestamp should be set") + }, + Mutate: func(u *store.User) { + u.DisplayName = "Renamed " + u.DisplayName + u.Role = store.UserRoleAdmin + }, + Update: func(ctx context.Context, s store.Store, u *store.User) error { + return s.UpdateUser(ctx, u) + }, + VerifyMutated: func(t *testing.T, got *store.User) { + assert.Contains(t, got.DisplayName, "Renamed ") + assert.Equal(t, store.UserRoleAdmin, got.Role) + }, + Delete: func(ctx context.Context, s store.Store, id string) error { + return s.DeleteUser(ctx, id) + }, + // Users are hard-deleted (no SoftDelete spec). + Filters: []FilterCase[store.User]{ + { + Name: "ByRole", + Seed: func(t *testing.T, ctx context.Context, s store.Store) { + require.NoError(t, s.CreateUser(ctx, &store.User{ + ID: uuid.NewString(), Email: "admin-" + uuid.NewString()[:8] + "@example.com", + DisplayName: "Admin", Role: store.UserRoleAdmin, Status: "active", + })) + require.NoError(t, s.CreateUser(ctx, &store.User{ + ID: uuid.NewString(), Email: "member-" + uuid.NewString()[:8] + "@example.com", + DisplayName: "Member", Role: store.UserRoleMember, Status: "active", + })) + }, + List: func(ctx context.Context, s store.Store) (*store.ListResult[store.User], error) { + return s.ListUsers(ctx, store.UserFilter{Role: store.UserRoleAdmin}, store.ListOptions{}) + }, + WantCount: 1, + }, + { + Name: "ByStatus", + Seed: func(t *testing.T, ctx context.Context, s store.Store) { + require.NoError(t, s.CreateUser(ctx, &store.User{ + ID: uuid.NewString(), Email: "active-" + uuid.NewString()[:8] + "@example.com", + DisplayName: "Active", Role: store.UserRoleMember, Status: "active", + })) + require.NoError(t, s.CreateUser(ctx, &store.User{ + ID: uuid.NewString(), Email: "suspended-" + uuid.NewString()[:8] + "@example.com", + DisplayName: "Suspended", Role: store.UserRoleMember, Status: "suspended", + })) + }, + List: func(ctx context.Context, s store.Store) (*store.ListResult[store.User], error) { + return s.ListUsers(ctx, store.UserFilter{Status: "suspended"}, store.ListOptions{}) + }, + WantCount: 1, + }, + }, + } +} + +// AllowListDomain describes the email allow-list entry for the CRUD-parity +// oracle. The allow list is keyed by email rather than ID: Get and Delete +// operate on the (normalized) email address, so GetID returns the email. +func AllowListDomain() Domain[store.AllowListEntry] { + return Domain[store.AllowListEntry]{ + Name: "allowlist", + Make: func(seq int) *store.AllowListEntry { + id := uuid.NewString() + return &store.AllowListEntry{ + ID: id, + Email: fmt.Sprintf("allow-%d-%s@example.com", seq, id[:8]), + Note: fmt.Sprintf("note %d", seq), + AddedBy: "admin", + } + }, + GetID: func(e *store.AllowListEntry) string { return e.Email }, + Create: func(ctx context.Context, s store.Store, e *store.AllowListEntry) error { + return s.AddAllowListEntry(ctx, e) + }, + Get: func(ctx context.Context, s store.Store, email string) (*store.AllowListEntry, error) { + return s.GetAllowListEntry(ctx, email) + }, + List: func(ctx context.Context, s store.Store, opts store.ListOptions) (*store.ListResult[store.AllowListEntry], error) { + return s.ListAllowListEntries(ctx, opts) + }, + VerifyEqual: func(t *testing.T, want, got *store.AllowListEntry) { + assert.Equal(t, want.ID, got.ID) + assert.Equal(t, want.Email, got.Email) + assert.Equal(t, want.Note, got.Note) + assert.Equal(t, want.AddedBy, got.AddedBy) + assert.False(t, got.Created.IsZero(), "Created timestamp should be set") + }, + // AllowListStore has no general-purpose update (only invite-id linking), + // so the Update category is skipped. + Delete: func(ctx context.Context, s store.Store, email string) error { + return s.RemoveAllowListEntry(ctx, email) + }, + // Allow-list entries are hard-deleted (no SoftDelete spec). + } +} + +// InviteCodeDomain describes the invite-code entity for the CRUD-parity oracle. +func InviteCodeDomain() Domain[store.InviteCode] { + return Domain[store.InviteCode]{ + Name: "invitecode", + Make: func(seq int) *store.InviteCode { + id := uuid.NewString() + return &store.InviteCode{ + ID: id, + CodeHash: fmt.Sprintf("hash-%d-%s", seq, id), + CodePrefix: fmt.Sprintf("scion_in%d", seq), + MaxUses: 5, + UseCount: 0, + ExpiresAt: time.Now().Add(24 * time.Hour), + CreatedBy: "admin", + Note: fmt.Sprintf("invite %d", seq), + } + }, + GetID: func(i *store.InviteCode) string { return i.ID }, + Create: func(ctx context.Context, s store.Store, i *store.InviteCode) error { + return s.CreateInviteCode(ctx, i) + }, + Get: func(ctx context.Context, s store.Store, id string) (*store.InviteCode, error) { + return s.GetInviteCode(ctx, id) + }, + List: func(ctx context.Context, s store.Store, opts store.ListOptions) (*store.ListResult[store.InviteCode], error) { + return s.ListInviteCodes(ctx, opts) + }, + VerifyEqual: func(t *testing.T, want, got *store.InviteCode) { + assert.Equal(t, want.ID, got.ID) + assert.Equal(t, want.CodePrefix, got.CodePrefix) + assert.Equal(t, want.MaxUses, got.MaxUses) + assert.Equal(t, want.CreatedBy, got.CreatedBy) + assert.False(t, got.Created.IsZero(), "Created timestamp should be set") + }, + // InviteCodeStore exposes targeted mutators (revoke, increment) rather + // than a general update, so the Update category is skipped. + Delete: func(ctx context.Context, s store.Store, id string) error { + return s.DeleteInviteCode(ctx, id) + }, + // Invite codes are hard-deleted (no SoftDelete spec). + } +} diff --git a/pkg/store/storetest/main_test.go b/pkg/store/storetest/main_test.go new file mode 100644 index 000000000..063bf8bba --- /dev/null +++ b/pkg/store/storetest/main_test.go @@ -0,0 +1,34 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//go:build !no_sqlite + +package storetest_test + +import ( + "os" + "testing" + + "github.com/GoogleCloudPlatform/scion/pkg/store/enttest" +) + +// TestMain wires the enttest backend lifecycle so the Postgres integration +// backend can create and drop its per-package ephemeral database. Both calls are +// no-ops in the default SQLite build. +func TestMain(m *testing.M) { + enttest.MainSetup() + code := m.Run() + enttest.MainTeardown() + os.Exit(code) +} diff --git a/pkg/store/storetest/storetest.go b/pkg/store/storetest/storetest.go new file mode 100644 index 000000000..160402e07 --- /dev/null +++ b/pkg/store/storetest/storetest.go @@ -0,0 +1,296 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Package storetest provides a backend-agnostic, table-driven CRUD-parity test +// oracle for implementations of store.Store. +// +// The harness is the cornerstone of the Postgres integration effort: every +// store backend (SQLite today, Postgres later) must produce identical +// observable results for the same operations. Rather than re-write the same +// CRUD assertions per backend, a test provides a Factory that returns a fresh, +// migrated store.Store, and the harness drives the standardized test categories +// against it: +// +// - Create: insert an entity, verify the returned/persisted fields. +// - Read: get by ID, verify all fields; missing ID -> ErrNotFound. +// - Update: modify fields, verify the change is persisted. +// - Delete: delete an entity, verify it is excluded from the default +// list and Get returns ErrNotFound. For domains that support +// soft-delete, additionally verify it is still returned when +// deleted entities are explicitly included. +// - List-paginate: insert N entities, verify limit/pagination behavior. +// - List-filter: verify filtering returns only matching entities. +// +// Each entity type is described by a generic Domain[T]. Because every domain +// has different method signatures on store.Store, the Domain captures each +// operation as a closure. This keeps the harness itself entity-agnostic while +// letting new domains be onboarded by adding a single Domain descriptor (see +// domains.go for the group and policy examples). +package storetest + +import ( + "context" + "testing" + + "github.com/GoogleCloudPlatform/scion/pkg/store" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +// Factory builds a fresh, migrated store.Store for a single test. It receives +// the subtest's *testing.T so the implementation can register cleanup (closing +// connections, dropping temp databases) via t.Cleanup. Each test category gets +// its own store so cases are isolated from one another. +type Factory func(t *testing.T) store.Store + +// Domain describes how to exercise one entity type (T) through the standardized +// CRUD test categories. Required fields must be set; optional fields enable +// additional categories when the entity supports them. +type Domain[T any] struct { + // Name is the entity name, used as the subtest group name (e.g. "group"). + Name string + + // Make builds a fresh, valid entity. seq is a monotonically increasing + // counter the implementation should weave into unique fields (slug, name) + // so that many entities can be created in the same store without collisions. + Make func(seq int) *T + + // GetID returns the primary identifier used for Get/Delete. + GetID func(*T) string + + // Prepare, when non-nil, runs once against each fresh store before a test + // category exercises it. It seeds prerequisite rows that entities of this + // domain depend on (e.g. the project an agent references via a required + // foreign key). It must be idempotent with respect to a fresh store. + Prepare func(t *testing.T, ctx context.Context, s store.Store) + + // Create persists a new entity. + Create func(ctx context.Context, s store.Store, e *T) error + + // Get retrieves an entity by ID. It must return store.ErrNotFound when the + // entity does not exist. + Get func(ctx context.Context, s store.Store, id string) (*T, error) + + // List returns entities honoring ListOptions (in particular Limit). For the + // Delete and Paginate categories this is the default, non-filtered listing. + List func(ctx context.Context, s store.Store, opts store.ListOptions) (*store.ListResult[T], error) + + // VerifyEqual asserts that a freshly-read entity (got) matches the one that + // was created (want). Implementations compare the fields they care about. + VerifyEqual func(t *testing.T, want, got *T) + + // --- Optional: Update category --- + + // Mutate applies an in-place modification used to verify Update persists. + // When nil (together with Update), the Update category is skipped. + Mutate func(e *T) + + // Update persists modifications to an existing entity. + Update func(ctx context.Context, s store.Store, e *T) error + + // VerifyMutated asserts that the change applied by Mutate is present on a + // freshly-read entity. + VerifyMutated func(t *testing.T, got *T) + + // --- Optional: Delete category --- + + // Delete removes an entity by ID. When nil, the Delete category is skipped. + Delete func(ctx context.Context, s store.Store, id string) error + + // SoftDelete, when non-nil, marks the domain as soft-deleting: after Delete + // the entity is excluded from the default List but must still be returned + // when deleted entities are explicitly included. + SoftDelete *SoftDeleteSpec[T] + + // --- Optional: List-filter category --- + + // Filters enumerates filter scenarios to verify. When empty, the + // List-filter category is skipped. + Filters []FilterCase[T] +} + +// SoftDeleteSpec captures the extra behavior of domains that soft-delete rather +// than hard-delete. ListIncludeDeleted must list entities including any that +// have been soft-deleted. +type SoftDeleteSpec[T any] struct { + ListIncludeDeleted func(ctx context.Context, s store.Store, opts store.ListOptions) (*store.ListResult[T], error) +} + +// FilterCase describes one List-filter scenario. Seed inserts a known mix of +// entities into a fresh store; List applies the filter under test; WantCount is +// the number of entities expected to match. +type FilterCase[T any] struct { + Name string + Seed func(t *testing.T, ctx context.Context, s store.Store) + List func(ctx context.Context, s store.Store) (*store.ListResult[T], error) + WantCount int +} + +// RunDomain runs every applicable test category for a single domain against +// stores produced by factory. Each category obtains its own fresh store. +func RunDomain[T any](t *testing.T, factory Factory, d Domain[T]) { + t.Helper() + t.Run(d.Name, func(t *testing.T) { + t.Run("Create", func(t *testing.T) { testCreate(t, factory, d) }) + t.Run("Read", func(t *testing.T) { testRead(t, factory, d) }) + + if d.Update != nil && d.Mutate != nil { + t.Run("Update", func(t *testing.T) { testUpdate(t, factory, d) }) + } + if d.Delete != nil { + t.Run("Delete", func(t *testing.T) { testDelete(t, factory, d) }) + } + if d.List != nil { + t.Run("ListPaginate", func(t *testing.T) { testPaginate(t, factory, d) }) + } + if len(d.Filters) > 0 { + t.Run("ListFilter", func(t *testing.T) { testFilter(t, factory, d) }) + } + }) +} + +// prepareStore runs the domain's Prepare hook (if any) against a fresh store. +func prepareStore[T any](t *testing.T, ctx context.Context, s store.Store, d Domain[T]) { + if d.Prepare != nil { + d.Prepare(t, ctx, s) + } +} + +func testCreate[T any](t *testing.T, factory Factory, d Domain[T]) { + ctx := context.Background() + s := factory(t) + prepareStore(t, ctx, s, d) + + e := d.Make(1) + require.NoError(t, d.Create(ctx, s, e), "Create should succeed") + + // The created entity must be retrievable and have the fields we set. + got, err := d.Get(ctx, s, d.GetID(e)) + require.NoError(t, err, "Get after Create should succeed") + d.VerifyEqual(t, e, got) +} + +func testRead[T any](t *testing.T, factory Factory, d Domain[T]) { + ctx := context.Background() + s := factory(t) + prepareStore(t, ctx, s, d) + + e := d.Make(1) + require.NoError(t, d.Create(ctx, s, e)) + + got, err := d.Get(ctx, s, d.GetID(e)) + require.NoError(t, err) + d.VerifyEqual(t, e, got) + + // A non-existent ID must surface as ErrNotFound across all backends. + _, err = d.Get(ctx, s, missingID()) + assert.ErrorIs(t, err, store.ErrNotFound, "Get of missing entity should return ErrNotFound") +} + +func testUpdate[T any](t *testing.T, factory Factory, d Domain[T]) { + ctx := context.Background() + s := factory(t) + prepareStore(t, ctx, s, d) + + e := d.Make(1) + require.NoError(t, d.Create(ctx, s, e)) + + d.Mutate(e) + require.NoError(t, d.Update(ctx, s, e), "Update should succeed") + + got, err := d.Get(ctx, s, d.GetID(e)) + require.NoError(t, err) + d.VerifyMutated(t, got) +} + +func testDelete[T any](t *testing.T, factory Factory, d Domain[T]) { + ctx := context.Background() + s := factory(t) + prepareStore(t, ctx, s, d) + + e := d.Make(1) + require.NoError(t, d.Create(ctx, s, e)) + id := d.GetID(e) + + require.NoError(t, d.Delete(ctx, s, id), "Delete should succeed") + + // Excluded from the default listing. + if d.List != nil { + res, err := d.List(ctx, s, store.ListOptions{}) + require.NoError(t, err) + assert.False(t, containsID(d, res.Items, id), "deleted entity must be excluded from default List") + } + + if d.SoftDelete != nil { + // Soft-deleted: Get still treats it as gone by default, but it must + // remain visible when deleted entities are explicitly included. + incl, err := d.SoftDelete.ListIncludeDeleted(ctx, s, store.ListOptions{}) + require.NoError(t, err) + assert.True(t, containsID(d, incl.Items, id), "soft-deleted entity must be returned with IncludeDeleted") + } else { + // Hard-deleted: Get must report ErrNotFound. + _, err := d.Get(ctx, s, id) + assert.ErrorIs(t, err, store.ErrNotFound, "Get of hard-deleted entity should return ErrNotFound") + } +} + +func testPaginate[T any](t *testing.T, factory Factory, d Domain[T]) { + ctx := context.Background() + s := factory(t) + prepareStore(t, ctx, s, d) + + const n = 5 + for i := 0; i < n; i++ { + require.NoError(t, d.Create(ctx, s, d.Make(i+1))) + } + + // No limit: all entities returned, TotalCount reflects the full set. + all, err := d.List(ctx, s, store.ListOptions{}) + require.NoError(t, err) + assert.Len(t, all.Items, n, "unbounded List should return every entity") + assert.Equal(t, n, all.TotalCount, "TotalCount should reflect the full set") + + // Limited: at most `limit` items, but TotalCount still reflects the full set. + const limit = 2 + page, err := d.List(ctx, s, store.ListOptions{Limit: limit}) + require.NoError(t, err) + assert.Len(t, page.Items, limit, "limited List should return exactly `limit` items") + assert.Equal(t, n, page.TotalCount, "TotalCount should be independent of Limit") +} + +func testFilter[T any](t *testing.T, factory Factory, d Domain[T]) { + ctx := context.Background() + for _, fc := range d.Filters { + fc := fc + t.Run(fc.Name, func(t *testing.T) { + s := factory(t) + prepareStore(t, ctx, s, d) + fc.Seed(t, ctx, s) + res, err := fc.List(ctx, s) + require.NoError(t, err) + assert.Equal(t, fc.WantCount, len(res.Items), "filtered List item count") + assert.Equal(t, fc.WantCount, res.TotalCount, "filtered List TotalCount") + }) + } +} + +// containsID reports whether any item in items has the given ID. +func containsID[T any](d Domain[T], items []T, id string) bool { + for i := range items { + if d.GetID(&items[i]) == id { + return true + } + } + return false +} diff --git a/pkg/store/storetest/storetest_test.go b/pkg/store/storetest/storetest_test.go new file mode 100644 index 000000000..d72ccd041 --- /dev/null +++ b/pkg/store/storetest/storetest_test.go @@ -0,0 +1,48 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//go:build !no_sqlite + +package storetest_test + +import ( + "testing" + + "github.com/GoogleCloudPlatform/scion/pkg/store" + "github.com/GoogleCloudPlatform/scion/pkg/store/entadapter" + "github.com/GoogleCloudPlatform/scion/pkg/store/enttest" + "github.com/GoogleCloudPlatform/scion/pkg/store/storetest" +) + +// compositeFactory returns a Factory that builds the production-shaped +// CompositeStore: a single Ent-managed database serving every domain. This is +// exactly the single-database layout used by the hub today (see +// cmd/server_foreground.go:initStore), so a green run proves the oracle works +// against the current backend. +// +// The backend (SQLite by default, Postgres under -tags integration with +// SCION_TEST_POSTGRES_URL set) is selected by enttest.NewClient, so the same +// oracle asserts identical observable behavior across both backends. +func compositeFactory(t *testing.T) store.Store { + t.Helper() + + cs := entadapter.NewCompositeStore(enttest.NewClient(t)) + return cs +} + +// TestCompositeStore_CRUDParity runs the full CRUD-parity oracle against the +// current CompositeStore across all ported domains. +func TestCompositeStore_CRUDParity(t *testing.T) { + storetest.RunStoreSuite(t, compositeFactory) +} diff --git a/pr-nn-review-v5.md b/pr-nn-review-v5.md deleted file mode 100644 index 930e3bd00..000000000 --- a/pr-nn-review-v5.md +++ /dev/null @@ -1,100 +0,0 @@ -# Code Review v5: Grove-to-Project Rename Strategy - -## Review Summary - -**Verdict:** REQUEST CHANGES - -**Overview:** While the rename strategy has progressed significantly, this review identified several **CRITICAL** bugs that break core functionality (settings loading, A2A messaging) and several **HIGH** severity omissions in backward compatibility for the Hub API. The branch is currently unstable as evidenced by multiple test failures in `pkg/config`. - -**Findings Count:** -- CRITICAL: 2 -- HIGH: 4 -- MEDIUM: 1 -- LOW/INFO: 2 - ---- - -### Critical Issues - -#### 1. Settings Loading for Project ID is Broken [CRITICAL] -- **File:** `pkg/config/koanf.go` (Multiple lines) -- **Issue:** The remapping logic in `LoadSettingsKoanf` still uses `grove_id` and `hub.groveId` as target keys, but the `Settings` and `HubClientConfig` structs have been updated to use `project_id` and `projectId` tags. -- **Impact:** Project IDs are not correctly loaded from `.scion/project-id` files or environment variables like `SCION_HUB_GROVE_ID`. This causes `scion` to fail to recognize linked projects. -- **Evidence:** Multiple tests in `pkg/config/koanf_test.go` fail with `expected ProjectID ..., got ''`. -- **Suggested Fix:** - - Update `koanf.go` to use `project_id` and `hub.projectId` in all `confmap.Provider` remapping calls. - - Ensure `SCION_HUB_PROJECT_ID` environment variable is also supported. - -#### 2. A2A Bridge Messaging is Broken [CRITICAL] -- **File:** `extras/scion-a2a-bridge/internal/bridge/bridge.go:270, 935` -- **Issue:** The A2A bridge was updated to subscribe to `scion.project.*` topics, but the Hub and Broker (in `pkg/broker/broker.go`) still publish exclusively to `scion.grove.*` topics. -- **Impact:** The A2A bridge will never receive messages from agents, breaking the entire protocol flow. -- **Suggested Fix:** - - Update `pkg/broker/broker.go` to dual-publish to both `scion.project` and `scion.grove` topics, or keep the bridge on `scion.grove` until the wire protocol is officially migrated. - ---- - -### High Issues - -#### 3. Hub API Missing Backward Compatibility for Projects & Agents [HIGH] -- **File:** `pkg/store/models.go` -- **Issue:** The core `Project` and `Agent` structs are missing `MarshalJSON` implementations to provide legacy `groveId`, `groveName`, and `grove` fields. -- **Impact:** REST clients (like older CLI versions) expecting these fields in Hub responses will break or show missing data. -- **Evidence:** Custom integration test `pkg/hub/compat_test.go` failed (Response missing `groveId` and `grove`). -- **Suggested Fix:** Implement `MarshalJSON` and `UnmarshalJSON` for `store.Project` and `store.Agent` similar to how it was done for `Schedule` and `Message`. - -#### 4. Project Registration Broken for Old Clients [HIGH] -- **File:** `pkg/hub/handlers.go:2864` -- **Issue:** `RegisterProjectRequest` struct uses `json:"id"` but lacks custom unmarshaling for the legacy `groveId` or `grove_id` keys. -- **Impact:** Old CLI versions performing `hub register` will fail to pass the project ID to the Hub. -- **Suggested Fix:** Add custom `UnmarshalJSON` to `RegisterProjectRequest`. - -#### 5. ProjectProvider Backward Compatibility Missing [HIGH] -- **File:** `pkg/store/models.go:266` -- **Issue:** `ProjectProvider` (formerly `GroveContributor`) uses `json:"projectId"` but lacks custom marshaling for the legacy `groveId` field. -- **Impact:** Inconsistency in API responses for broker/project links. - -#### 6. Project Initialization Botches V1 Settings [HIGH] -- **File:** `pkg/config/init.go:516` -- **Issue:** `writeProjectSettings` writes `project_id` into the `hub` section for V1 settings, but `V1HubClientConfig` (in `pkg/config/settings_v1.go`) still uses `koanf:"grove_id"`. -- **Impact:** Newly initialized V1 projects won't load their Hub Project ID correctly. -- **Suggested Fix:** Synchronize the key name used in `init.go` with the tags in `settings_v1.go`. - ---- - -### Medium Issues - -#### 7. Redundant Index Operations in Migration V48 [MEDIUM] -- **File:** `pkg/store/sqlite/sqlite.go:1155` -- **Issue:** Migration V48 attempts to `DROP INDEX IF EXISTS ..._grove` and `CREATE INDEX IF NOT EXISTS ..._project` for several tables (e.g., `messages`, `groups`) where the indexes were ALREADY named `..._project` in previous migrations (e.g., V18, V31). -- **Impact:** The `DROP` fails silently (correct), and the `CREATE` does nothing because the index exists. While harmless, it's confusing and sloppy. -- **Suggested Fix:** Verify index names in earlier migrations and only drop/recreate those that actually contain "grove" in the DB schema. - ---- - -### Low / Info Issues - -#### 8. Bug in ResolvedSecret.MarshalJSON [LOW] -- **File:** `pkg/api/types.go:680` (added in commit `c4b316ce`) -- **Issue:** The code sets `grove = "project"` if `s.Source == "project"`. -- **Suggested Fix:** It should set `grove = "grove"`. - -#### 9. Leftover 'g' Receivers [LOW] -- **File:** `pkg/store/models.go:204` -- **Issue:** Method `IsSharedWorkspace` still uses `g *Project` as receiver. - ---- - -### What's Done Well - -- **Comprehensive CLI Aliases:** The addition of `cd-grove` and persistence of `-g` flag for `--project` is a great UX touch. -- **Dual Event Publishing:** Internal Hub events (`events.go`) correctly dual-publish to both `project.*` and `grove.*` subjects. -- **A2A Bridge Porting:** The rename within the A2A bridge (except for the topic mismatch) was very thorough, covering metrics and internal state. - ---- - -### Verification Story -- **Tests reviewed:** YES. Identified multiple failures in `pkg/config/koanf_test.go` confirming regression. -- **Build verified:** YES. Verified compilation of Hub and CLI. -- **API Compatibility:** YES. Verified via a custom test (`pkg/hub/compat_test.go`) that `Project` and `Agent` objects are missing legacy fields. -- **Messaging checked:** YES. Identified topic mismatch between A2A bridge and Hub Broker. diff --git a/pr-nn-review-v8.md b/pr-nn-review-v8.md deleted file mode 100644 index 79c4428fd..000000000 --- a/pr-nn-review-v8.md +++ /dev/null @@ -1,68 +0,0 @@ -# Code Review: Hub-Broker Protocol Mismatch Fixes - -## Executive Summary -**Verdict:** REQUEST CHANGES -**Risk Level:** HIGH (due to backward compatibility regressions in heartbeat and agent listing) - -This PR addresses several critical protocol mismatches between the Hub and Runtime Broker introduced during the "grove" to "project" rename. The implementation of dual-field support via custom JSON marshaling is a step in the right direction. However, the backward compatibility is currently asymmetrical: while the system is now capable of emitting both new and legacy fields, it frequently fails to correctly **unmarshal** incoming legacy fields from older system components. This will lead to broken agent status tracking (heartbeats) and broken project listings when communicating with un-migrated brokers. - -## Critical Issues - -### 1. Broken Backward Compatibility for Incoming Heartbeats -**Files:** `pkg/hubclient/runtime_brokers.go`, `pkg/hub/handlers.go` - -The `BrokerHeartbeat` and `ProjectHeartbeat` structs in `hubclient` (used by Brokers) and the `brokerHeartbeatRequest` in the Hub's handlers have been updated to use `projects` and `projectId` JSON tags. While `hubclient` now includes `MarshalJSON` to emit both keys, it lacks `UnmarshalJSON` to read from older Brokers that only send `groves` and `groveId`. - -* **Impact:** A newer Hub will fail to process heartbeats from an older Broker. The `Projects` slice will be empty upon unmarshaling, causing the Hub to ignore all agent status updates. This breaks real-time observability across the entire platform. -* **Suggested Fix:** Implement `UnmarshalJSON` for `ListBrokerProjectsResponse`, `BrokerHeartbeat`, and `ProjectHeartbeat` in `pkg/hubclient/runtime_brokers.go` and for `brokerHeartbeatRequest` in `pkg/hub/handlers.go`. - -### 2. `listAgents` Missing `projectId` Query Parameter Support -**File:** `pkg/runtimebroker/handlers.go:217` - -While `handleAgentByID` and `handleAgentAttach` were correctly updated to support both `projectId` and `groveId` query parameters, the `listAgents` handler was overlooked. - -* **Impact:** Clients attempting to list agents using the new `projectId` parameter will receive a full list of all agents on the broker instead of a filtered list. This is a functional regression and a potential security concern regarding project isolation. -* **Suggested Fix:** - ```go - // pkg/runtimebroker/handlers.go - projectID := query.Get("projectId") - if projectID == "" { - projectID = query.Get("groveId") - } - if projectID != "" { - filter["scion.project_id"] = projectID - } - ``` - -## Important Issues - -### 3. Missing `projectId` JSON Tag Migration in Templates -**File:** `pkg/hubclient/templates.go` - -Unlike `tokens.go` and `notifications.go`, the structs in `templates.go` (e.g., `CreateTemplateRequest`, `CloneTemplateRequest`) still use `json:"groveId"` as their primary tag and have not been updated to `json:"projectId"` with a compatibility shim. - -* **Impact:** Inconsistent API across `hubclient`. New code attempting to use `projectId` in JSON for template operations will fail. -* **Suggested Fix:** Rename tags to `projectId` and implement custom `MarshalJSON`/`UnmarshalJSON` to maintain `groveId` support, consistent with the rest of the `hubclient` package. - -### 4. `ListBrokerProjectsResponse` Missing `UnmarshalJSON` -**File:** `pkg/hubclient/runtime_brokers.go:81` - -The Hub calls `ListProjects` on the Broker using this struct. Because it lacks `UnmarshalJSON`, it will return an empty list when communicating with an older Broker that returns `groves`. - -* **Impact:** Broken project discovery for older brokers. -* **Suggested Fix:** Add `UnmarshalJSON` to `ListBrokerProjectsResponse`. - -## Observations - -- **MessageRequest Implementation:** The custom unmarshaling for `MessageRequest` in `pkg/runtimebroker/types.go` is well-implemented, supporting `project_id`, `projectId`, and `grove_id`. This should serve as the template for other compatibility shims. -- **Route Aliasing:** The addition of the `/api/v1/workspace/project-upload` route as an alias for `grove-upload` in `pkg/runtimebroker/server.go` is a robust and clean way to handle the route migration. - -## Positive Feedback - -- **Comprehensive Query Param Fallbacks:** The implementation of dual query parameter support in `handleAgentByID` and `handleAgentAttach` ensures a smooth transition for the core agent control paths. -- **Client-Side Robustness:** `hubclient` now correctly sends both `projectId` and `groveId` in all `List` operations, ensuring it can talk to any version of the Hub/Broker. - -## Final Verdict -The PR addresses the immediate protocol mismatch bugs but introduces several regressions in backward compatibility by failing to handle incoming legacy payloads. These must be addressed to ensure a zero-downtime migration of the Scion fleet. - -**Status: REQUEST CHANGES** diff --git a/pr-nn-review.md b/pr-nn-review.md deleted file mode 100644 index 041caf3f8..000000000 --- a/pr-nn-review.md +++ /dev/null @@ -1,33 +0,0 @@ -## Review Summary - -**Verdict:** APPROVE - -**Overview:** The changes successfully address the "grove" to "project" rename by providing robust JSON marshaling/unmarshaling for Hub response types and heartbeats. The implementation correctly handles legacy fields and avoids shadowing issues using established Go patterns. - -### Critical Issues -- None - -### Important Issues -- None - -### Suggestions - -- **File: pkg/hub/response_types.go:42, 87, 115, ...** - **Consistency in `omitempty` for legacy fields:** Legacy fields such as `groveId`, `groveName`, and `grove` are marked `omitempty` in some types (e.g., `TemplateWithCapabilities`, `GroupWithCapabilities`) but are mandatory in others (e.g., `AgentWithCapabilities`, `ProjectWithCapabilities`). - *Suggested Fix:* Use `omitempty` consistently for all legacy fields to ensure that if for some reason the source field is empty, we don't send an explicit empty string for the legacy key. - -- **File: pkg/hub/response_types.go:50, 94** - **Performance (Double Unmarshaling):** `AgentWithCapabilities` and `ProjectWithCapabilities` unmarshal the same JSON input twice. While this correctly leverages the embedded model's `UnmarshalJSON`, it is slightly less efficient than a single-pass approach. - *Suggested Fix:* This is acceptable for readability, but consider if the volume of these requests justifies optimizing into a single pass using a combined alias struct. - -### What's Done Well -- **Correct Pattern for Embedded Marshaling:** The use of `type Alias T` to bypass the embedded type's `MarshalJSON`/`UnmarshalJSON` methods is exactly the right way to avoid infinite recursion and ensure wrapper fields are included. -- **Robust Heartbeat Support:** The Hub's heartbeat handler correctly supports both the new `projects` key and the legacy `groves` key, ensuring older brokers continue to work during the transition. -- **Comprehensive Legacy Coverage:** The PR goes beyond just renaming and ensures that all relevant API entities (Policies, Groups, Templates) maintain backward compatibility. -- **Solid Test Suite:** The addition of `heartbeat_legacy_test.go` and updates to `runtime_brokers_test.go` provide good confidence in the bidirectional compatibility. - -### Verification Story -- Tests reviewed: Yes. `TestBrokerHeartbeatRequest_UnmarshalJSON`, `TestBrokerProjectHeartbeat_UnmarshalJSON`, and `ListBrokerProjectsResponse` tests verify the legacy mapping. -- Build verified: Yes. `go build ./...` passes. -- Lint/static analysis clean: Yes. The code follows standard Go idioms for JSON handling. -- Security checked: Yes. No unsanitized inputs or insecure handling identified in these JSON transformations. diff --git a/scratch/apitest/main.go b/scratch/apitest/main.go new file mode 100644 index 000000000..79c625d6a --- /dev/null +++ b/scratch/apitest/main.go @@ -0,0 +1,238 @@ +// Command apitest drives API-level multi-hub integration/stress traffic against +// two running Scion hubs that share one CloudSQL Postgres instance. It validates +// the connection-pool / keepalive fixes and multi-replica behavior through the +// real HTTP API. Run it ON a hub VM so it reaches both hubs over the fast +// internal network. Not part of the product. +// +// Env: +// +// A_BASE, B_BASE base URLs (e.g. http://localhost:8080, http://10.128.15.241:8080) +// A_TOK, B_TOK admin bearer tokens (per-hub signing keys) +package main + +import ( + "bytes" + "encoding/json" + "fmt" + "io" + "net/http" + "os" + "sort" + "sync" + "sync/atomic" + "time" + + "github.com/google/uuid" +) + +type hub struct { + name string + base string + tok string +} + +var client = &http.Client{Timeout: 35 * time.Second} + +func req(h hub, method, path string, body any) (int, []byte, time.Duration) { + var rdr io.Reader + if body != nil { + b, _ := json.Marshal(body) + rdr = bytes.NewReader(b) + } + r, _ := http.NewRequest(method, h.base+path, rdr) + r.Header.Set("Authorization", "Bearer "+h.tok) + if body != nil { + r.Header.Set("Content-Type", "application/json") + } + start := time.Now() + resp, err := client.Do(r) + d := time.Since(start) + if err != nil { + return 0, []byte(err.Error()), d + } + defer resp.Body.Close() + rb, _ := io.ReadAll(resp.Body) + return resp.StatusCode, rb, d +} + +func pct(ds []time.Duration, p float64) time.Duration { + if len(ds) == 0 { + return 0 + } + sort.Slice(ds, func(i, j int) bool { return ds[i] < ds[j] }) + i := int(float64(len(ds)) * p) + if i >= len(ds) { + i = len(ds) - 1 + } + return ds[i] +} + +func main() { + A := hub{"A", os.Getenv("A_BASE"), os.Getenv("A_TOK")} + B := hub{"B", os.Getenv("B_BASE"), os.Getenv("B_TOK")} + hubs := []hub{A, B} + + // ---- Phase 1: concurrent CRUD storm across both hubs ---- + fmt.Println("== Phase 1: concurrent project CRUD storm (both hubs) ==") + const workers, iters = 24, 30 + var ok, fail, stalls int64 + latMu := sync.Mutex{} + lat := map[string][]time.Duration{"A": {}, "B": {}} + var wg sync.WaitGroup + t0 := time.Now() + for w := 0; w < workers; w++ { + wg.Add(1) + go func(w int) { + defer wg.Done() + h := hubs[w%2] + for i := 0; i < iters; i++ { + name := fmt.Sprintf("stress-%d-%d-%s", w, i, uuid.NewString()[:8]) + st, body, d := req(h, "POST", "/api/v1/projects", map[string]string{"name": name}) + if d > 2*time.Second { + atomic.AddInt64(&stalls, 1) + } + if st != 201 && st != 200 { + atomic.AddInt64(&fail, 1) + if i == 0 { + fmt.Printf(" [%s] create failed st=%d body=%.120s\n", h.name, st, body) + } + continue + } + var pr struct { + ID string `json:"id"` + } + json.Unmarshal(body, &pr) + req(h, "GET", "/api/v1/projects/"+pr.ID, nil) + req(h, "GET", "/api/v1/projects?limit=5", nil) + dst, _, dd := req(h, "DELETE", "/api/v1/projects/"+pr.ID, nil) + if dd > 2*time.Second { + atomic.AddInt64(&stalls, 1) + } + if dst >= 200 && dst < 300 { + atomic.AddInt64(&ok, 1) + } else { + atomic.AddInt64(&fail, 1) + } + latMu.Lock() + lat[h.name] = append(lat[h.name], d) + latMu.Unlock() + } + }(w) + } + wg.Wait() + dur := time.Since(t0) + total := int64(workers * iters) + fmt.Printf(" full CRUD cycles ok=%d fail=%d of %d in %s (%.0f cycles/s), stalls(>2s)=%d\n", + ok, fail, total, dur.Truncate(time.Millisecond), float64(total)/dur.Seconds(), stalls) + for _, n := range []string{"A", "B"} { + fmt.Printf(" hub %s create-latency p50=%s p95=%s max=%s (n=%d)\n", + n, pct(lat[n], 0.5), pct(lat[n], 0.95), pct(lat[n], 1.0), len(lat[n])) + } + + // ---- Phase 2: cross-replica read-after-write (create A, read B) ---- + fmt.Println("== Phase 2: cross-replica read-after-write (create on A, GET on B) ==") + const rw = 40 + var immediate, delayed, miss int + for i := 0; i < rw; i++ { + name := "raw-" + uuid.NewString()[:10] + st, body, _ := req(A, "POST", "/api/v1/projects", map[string]string{"name": name}) + if st != 201 && st != 200 { + miss++ + continue + } + var pr struct { + ID string `json:"id"` + } + json.Unmarshal(body, &pr) + got := false + for attempt := 0; attempt < 10; attempt++ { + s2, _, _ := req(B, "GET", "/api/v1/projects/"+pr.ID, nil) + if s2 == 200 { + if attempt == 0 { + immediate++ + } else { + delayed++ + } + got = true + break + } + time.Sleep(50 * time.Millisecond) + } + if !got { + miss++ + } + req(A, "DELETE", "/api/v1/projects/"+pr.ID, nil) + } + fmt.Printf(" read-after-write: immediate=%d delayed=%d miss=%d of %d\n", immediate, delayed, miss, rw) + + // ---- Phase 3: conflict -> HTTP 409 (concurrent duplicate-ID creates) ---- + fmt.Println("== Phase 3: concurrent duplicate-ID create -> expect exactly one 201, rest 409 ==") + const rounds = 25 + var created, conflict, other int + for i := 0; i < rounds; i++ { + id := uuid.NewString() + name := "dup-" + id[:8] + var c201, c409, cother int64 + var w2 sync.WaitGroup + // 4 concurrent creators (2 per hub) racing on the same explicit ID. + for k := 0; k < 4; k++ { + w2.Add(1) + go func(k int) { + defer w2.Done() + h := hubs[k%2] + st, _, _ := req(h, "POST", "/api/v1/projects", map[string]any{"id": id, "name": name}) + switch { + case st == 201 || st == 200: + atomic.AddInt64(&c201, 1) + case st == 409: + atomic.AddInt64(&c409, 1) + default: + atomic.AddInt64(&cother, 1) + } + }(k) + } + w2.Wait() + created += int(c201) + conflict += int(c409) + other += int(cother) + req(A, "DELETE", "/api/v1/projects/"+id, nil) + } + fmt.Printf(" over %d rounds (4 racers each): 201=%d 409=%d other=%d (ideal: 201==%d, 409==%d)\n", + rounds, created, conflict, other, rounds, rounds*3) + + // ---- Phase 4: idle-then-burst (the stale-connection scenario) ---- + idleStr := os.Getenv("IDLE_SECONDS") + idle := 75 + fmt.Sscanf(idleStr, "%d", &idle) + fmt.Printf("== Phase 4: idle %ds then burst (validates keepalive/idle-recycle fix) ==\n", idle) + for _, h := range hubs { // warm the pools + for i := 0; i < 5; i++ { + req(h, "GET", "/api/v1/projects?limit=1", nil) + } + } + fmt.Printf(" pools warm; sleeping %ds to force idle...\n", idle) + time.Sleep(time.Duration(idle) * time.Second) + for _, h := range hubs { + var first time.Duration + var maxd time.Duration + for i := 0; i < 10; i++ { + st, _, d := req(h, "GET", "/api/v1/projects?limit=1", nil) + if i == 0 { + first = d + } + if d > maxd { + maxd = d + } + if st != 200 { + fmt.Printf(" [%s] burst req %d unexpected st=%d\n", h.name, i, st) + } + } + verdict := "OK" + if first > 2*time.Second { + verdict = "STALL (likely dead idle conn)" + } + fmt.Printf(" hub %s post-idle first-request=%s max=%s -> %s\n", + h.name, first.Truncate(time.Millisecond), maxd.Truncate(time.Millisecond), verdict) + } + fmt.Println("== done ==") +} diff --git a/scratch/dbdiag/main.go b/scratch/dbdiag/main.go new file mode 100644 index 000000000..8cc7d92fc --- /dev/null +++ b/scratch/dbdiag/main.go @@ -0,0 +1,42 @@ +// Command dbdiag prints CloudSQL connection usage for diagnosing pool +// saturation. Not part of the product. +package main + +import ( + "context" + "fmt" + "os" + + "github.com/jackc/pgx/v5" +) + +func main() { + ctx := context.Background() + conn, err := pgx.Connect(ctx, os.Getenv("PG_DSN")) + if err != nil { + fmt.Fprintln(os.Stderr, "connect:", err) + os.Exit(1) + } + defer conn.Close(ctx) + + var maxc, used int + conn.QueryRow(ctx, "SHOW max_connections").Scan(&maxc) + conn.QueryRow(ctx, "SELECT count(*) FROM pg_stat_activity WHERE datname='scion_test'").Scan(&used) + fmt.Printf("max_connections=%d total_on_scion_test=%d\n", maxc, used) + + rows, _ := conn.Query(ctx, `SELECT COALESCE(application_name,'(none)'), state, count(*) + FROM pg_stat_activity WHERE datname='scion_test' + GROUP BY 1,2 ORDER BY 3 DESC`) + defer rows.Close() + fmt.Printf("%-32s %-20s %s\n", "application_name", "state", "count") + for rows.Next() { + var app, state string + var n int + rows.Scan(&app, &state, &n) + fmt.Printf("%-32s %-20s %d\n", app, state, n) + } + // Advisory locks currently held. + var locks int + conn.QueryRow(ctx, "SELECT count(*) FROM pg_locks WHERE locktype='advisory'").Scan(&locks) + fmt.Printf("advisory_locks_held=%d\n", locks) +} diff --git a/scratch/dbdiag2/main.go b/scratch/dbdiag2/main.go new file mode 100644 index 000000000..c6645041f --- /dev/null +++ b/scratch/dbdiag2/main.go @@ -0,0 +1,31 @@ +package main + +import ( + "context" + "fmt" + "github.com/jackc/pgx/v5" + "os" + "time" +) + +func main() { + ctx := context.Background() + c, _ := pgx.Connect(ctx, os.Getenv("PG_DSN")) + defer c.Close(ctx) + for i := 0; i < 14; i++ { + rows, _ := c.Query(ctx, `SELECT client_addr::text, state, count(*) FROM pg_stat_activity WHERE datname='scion_test' AND client_addr IS NOT NULL GROUP BY 1,2 ORDER BY 1,2`) + m := map[string]int{} + for rows.Next() { + var a, s string + var n int + rows.Scan(&a, &s, &n) + m[a+"/"+s] = n + } + rows.Close() + var locks, waiting int + c.QueryRow(ctx, "SELECT count(*) FROM pg_locks WHERE locktype='advisory'").Scan(&locks) + c.QueryRow(ctx, "SELECT count(*) FROM pg_stat_activity WHERE wait_event_type='Client' AND datname='scion_test'").Scan(&waiting) + fmt.Printf("t+%2ds locks=%d %v\n", i*5, locks, m) + time.Sleep(5 * time.Second) + } +} diff --git a/scratch/minttoken/main.go b/scratch/minttoken/main.go new file mode 100644 index 000000000..705957868 --- /dev/null +++ b/scratch/minttoken/main.go @@ -0,0 +1,61 @@ +// Command minttoken mints a user access-token JWT for API-level integration +// testing against the running hubs. It looks up an existing (preferably admin) +// user in the shared Postgres DB and signs a token with the per-hub signing key +// read from Secret Manager. Not part of the product; used only for test driving. +package main + +import ( + "context" + "encoding/base64" + "fmt" + "os" + + "github.com/jackc/pgx/v5" + + "github.com/GoogleCloudPlatform/scion/pkg/hub" +) + +func main() { + dsn := os.Getenv("PG_DSN") + keyB64 := os.Getenv("SIGNING_KEY_B64") + if dsn == "" || keyB64 == "" { + fmt.Fprintln(os.Stderr, "PG_DSN and SIGNING_KEY_B64 required") + os.Exit(1) + } + key, err := base64.StdEncoding.DecodeString(keyB64) + if err != nil { + fmt.Fprintln(os.Stderr, "decode key:", err) + os.Exit(1) + } + + ctx := context.Background() + conn, err := pgx.Connect(ctx, dsn) + if err != nil { + fmt.Fprintln(os.Stderr, "db connect:", err) + os.Exit(1) + } + defer conn.Close(ctx) + + var id, email, displayName, role string + // Prefer an admin; fall back to any user. + err = conn.QueryRow(ctx, `SELECT id::text, email, display_name, role FROM users + ORDER BY (role = 'admin') DESC, created ASC LIMIT 1`).Scan(&id, &email, &displayName, &role) + if err != nil { + fmt.Fprintln(os.Stderr, "user lookup:", err) + os.Exit(1) + } + + svc, err := hub.NewUserTokenService(hub.UserTokenConfig{SigningKey: key}) + if err != nil { + fmt.Fprintln(os.Stderr, "token service:", err) + os.Exit(1) + } + // CLI client type → long (30-day) validity so the token outlives the test run. + token, _, err := svc.GenerateAccessToken(id, email, displayName, role, hub.ClientTypeCLI) + if err != nil { + fmt.Fprintln(os.Stderr, "mint:", err) + os.Exit(1) + } + fmt.Fprintf(os.Stderr, "user=%s email=%s role=%s\n", id, email, role) + fmt.Println(token) +} diff --git a/scratch/nm2-test-pod-a.yaml b/scratch/nm2-test-pod-a.yaml new file mode 100644 index 000000000..cab1f7892 --- /dev/null +++ b/scratch/nm2-test-pod-a.yaml @@ -0,0 +1,76 @@ +apiVersion: v1 +kind: Pod +metadata: + name: nm2-test-agent-a + namespace: scion-agents + labels: + test: nm2-scenario-a + scion.dev/project-id: test-project-alpha +spec: + securityContext: + runAsUser: 1000 + runAsGroup: 1000 + fsGroup: 1000 + initContainers: + - name: workspace-provision + image: alpine/git:latest + command: + - sh + - -c + - | + set -e + SENTINEL="/workspace/.scion-provisioned" + if [ -f "$SENTINEL" ]; then + echo "PROVISION: sentinel found, skipping clone" + exit 0 + fi + echo "PROVISION: cloning workspace..." + git clone --depth 1 https://github.com/ptone/scion.git /workspace + echo "$(date -u +%Y-%m-%dT%H:%M:%SZ)" > "$SENTINEL" + echo "PROVISION: clone complete, sentinel written" + volumeMounts: + - name: workspace + mountPath: /workspace + subPath: projects/test-project-alpha/workspace + resources: + requests: + cpu: 250m + memory: 512Mi + containers: + - name: agent + image: busybox:1.36 + command: + - sh + - -c + - | + echo "=== WORKSPACE CONTENTS ===" + ls -la /workspace/ + echo "=== SENTINEL CHECK ===" + cat /workspace/.scion-provisioned 2>/dev/null && echo "SENTINEL: present" || echo "SENTINEL: missing" + echo "=== ISOLATION CHECK ===" + echo "Attempting to access parent dir..." + ls /workspace/../ 2>&1 || echo "ISOLATION: cannot traverse up" + echo "=== WORKSPACE MOUNT INFO ===" + mount | grep workspace || echo "mount info unavailable in busybox" + echo "=== UID/GID CHECK ===" + id + echo "=== TEST COMPLETE ===" + sleep 30 + volumeMounts: + - name: workspace + mountPath: /workspace + subPath: projects/test-project-alpha/workspace + resources: + requests: + cpu: 250m + memory: 256Mi + volumes: + - name: workspace + persistentVolumeClaim: + claimName: scion-workspaces + restartPolicy: Never + tolerations: + - key: "kubernetes.io/arch" + operator: "Equal" + value: "amd64" + effect: "NoSchedule" diff --git a/scratch/nm2-test-pod-b1.yaml b/scratch/nm2-test-pod-b1.yaml new file mode 100644 index 000000000..fc29aa3a4 --- /dev/null +++ b/scratch/nm2-test-pod-b1.yaml @@ -0,0 +1,71 @@ +apiVersion: v1 +kind: Pod +metadata: + name: nm2-test-agent-b1 + namespace: scion-agents + labels: + test: nm2-scenario-b + scion.dev/project-id: test-project-beta +spec: + securityContext: + runAsUser: 1000 + runAsGroup: 1000 + fsGroup: 1000 + initContainers: + - name: workspace-provision + image: alpine/git:latest + command: + - sh + - -c + - | + set -e + SENTINEL="/workspace/.scion-provisioned" + if [ -f "$SENTINEL" ]; then + echo "PROVISION: sentinel found at $(cat $SENTINEL), skipping clone" + exit 0 + fi + echo "PROVISION: cloning workspace for project-beta (agent b1)..." + git clone --depth 1 https://github.com/ptone/scion.git /workspace + echo "b1:$(date -u +%Y-%m-%dT%H:%M:%SZ)" > "$SENTINEL" + echo "PROVISION: clone complete, sentinel written by b1" + volumeMounts: + - name: workspace + mountPath: /workspace + subPath: projects/test-project-beta/workspace + resources: + requests: + cpu: 250m + memory: 512Mi + containers: + - name: agent + image: busybox:1.36 + command: + - sh + - -c + - | + echo "=== AGENT B1 WORKSPACE ===" + ls -la /workspace/ + echo "=== SENTINEL ===" + cat /workspace/.scion-provisioned + echo "=== go.mod (identity check) ===" + head -3 /workspace/go.mod 2>/dev/null || echo "go.mod not found" + echo "=== TEST COMPLETE (b1) ===" + sleep 60 + volumeMounts: + - name: workspace + mountPath: /workspace + subPath: projects/test-project-beta/workspace + resources: + requests: + cpu: 250m + memory: 256Mi + volumes: + - name: workspace + persistentVolumeClaim: + claimName: scion-workspaces + restartPolicy: Never + tolerations: + - key: "kubernetes.io/arch" + operator: "Equal" + value: "amd64" + effect: "NoSchedule" diff --git a/scratch/nm2-test-pod-b2.yaml b/scratch/nm2-test-pod-b2.yaml new file mode 100644 index 000000000..e2b3647ed --- /dev/null +++ b/scratch/nm2-test-pod-b2.yaml @@ -0,0 +1,71 @@ +apiVersion: v1 +kind: Pod +metadata: + name: nm2-test-agent-b2 + namespace: scion-agents + labels: + test: nm2-scenario-b + scion.dev/project-id: test-project-beta +spec: + securityContext: + runAsUser: 1000 + runAsGroup: 1000 + fsGroup: 1000 + initContainers: + - name: workspace-provision + image: alpine/git:latest + command: + - sh + - -c + - | + set -e + SENTINEL="/workspace/.scion-provisioned" + if [ -f "$SENTINEL" ]; then + echo "PROVISION: sentinel found at $(cat $SENTINEL), skipping clone" + exit 0 + fi + echo "PROVISION: cloning workspace for project-beta (agent b2)..." + git clone --depth 1 https://github.com/ptone/scion.git /workspace + echo "b2:$(date -u +%Y-%m-%dT%H:%M:%SZ)" > "$SENTINEL" + echo "PROVISION: clone complete, sentinel written by b2" + volumeMounts: + - name: workspace + mountPath: /workspace + subPath: projects/test-project-beta/workspace + resources: + requests: + cpu: 250m + memory: 512Mi + containers: + - name: agent + image: busybox:1.36 + command: + - sh + - -c + - | + echo "=== AGENT B2 WORKSPACE ===" + ls -la /workspace/ + echo "=== SENTINEL ===" + cat /workspace/.scion-provisioned + echo "=== go.mod (identity check) ===" + head -3 /workspace/go.mod 2>/dev/null || echo "go.mod not found" + echo "=== TEST COMPLETE (b2) ===" + sleep 60 + volumeMounts: + - name: workspace + mountPath: /workspace + subPath: projects/test-project-beta/workspace + resources: + requests: + cpu: 250m + memory: 256Mi + volumes: + - name: workspace + persistentVolumeClaim: + claimName: scion-workspaces + restartPolicy: Never + tolerations: + - key: "kubernetes.io/arch" + operator: "Equal" + value: "amd64" + effect: "NoSchedule" diff --git a/scratch/nm2-test-pod-e.yaml b/scratch/nm2-test-pod-e.yaml new file mode 100644 index 000000000..856f923fc --- /dev/null +++ b/scratch/nm2-test-pod-e.yaml @@ -0,0 +1,97 @@ +apiVersion: v1 +kind: Pod +metadata: + name: nm2-test-agent-e + namespace: scion-agents + labels: + test: nm2-scenario-e + scion.dev/project-id: test-project-epsilon +spec: + securityContext: + runAsUser: 1000 + runAsGroup: 1000 + fsGroup: 1000 + initContainers: + - name: workspace-provision + image: alpine/git:latest + command: + - sh + - -c + - | + set -e + SENTINEL="/workspace/.scion-provisioned" + if [ -f "$SENTINEL" ]; then + echo "PROVISION: sentinel found, skipping clone" + exit 0 + fi + echo "PROVISION: cloning workspace..." + git clone --depth 1 https://github.com/ptone/scion.git /workspace + echo "$(date -u +%Y-%m-%dT%H:%M:%SZ)" > "$SENTINEL" + echo "PROVISION: clone complete" + volumeMounts: + - name: workspace + mountPath: /workspace + subPath: projects/test-project-epsilon/workspace + resources: + requests: + cpu: 250m + memory: 512Mi + - name: shared-dir-provision + image: busybox:1.36 + command: + - sh + - -c + - | + echo "SHARED-DIR: ensuring directory exists..." + mkdir -p /shared/test-data + echo "shared-dir-test-content" > /shared/test-data/readme.txt + ls -la /shared/ + echo "SHARED-DIR: provisioned" + volumeMounts: + - name: shared-dir-0 + mountPath: /shared + subPath: projects/test-project-epsilon/shared-dirs/test-data + resources: + requests: + cpu: 250m + memory: 128Mi + containers: + - name: agent + image: busybox:1.36 + command: + - sh + - -c + - | + echo "=== WORKSPACE ===" + ls -la /workspace/ | head -10 + echo "=== SHARED DIR (/scion-volumes/test-data) ===" + ls -la /scion-volumes/test-data/ + cat /scion-volumes/test-data/readme.txt + echo "=== MOUNT VERIFICATION ===" + echo "Workspace and shared dir are on same PVC with different subPaths" + echo "=== TEST COMPLETE (e) ===" + sleep 30 + volumeMounts: + - name: workspace + mountPath: /workspace + subPath: projects/test-project-epsilon/workspace + - name: shared-dir-0 + mountPath: /scion-volumes/test-data + subPath: projects/test-project-epsilon/shared-dirs/test-data + resources: + requests: + cpu: 250m + memory: 256Mi + volumes: + - name: workspace + persistentVolumeClaim: + claimName: scion-workspaces + - name: shared-dir-0 + persistentVolumeClaim: + claimName: scion-workspaces + restartPolicy: Never + tolerations: + - key: "kubernetes.io/arch" + operator: "Equal" + value: "amd64" + effect: "NoSchedule" diff --git a/scratch/scion-nfs-pv.yaml b/scratch/scion-nfs-pv.yaml new file mode 100644 index 000000000..0fed9b6a9 --- /dev/null +++ b/scratch/scion-nfs-pv.yaml @@ -0,0 +1,31 @@ +apiVersion: v1 +kind: Namespace +metadata: + name: scion-agents +--- +apiVersion: v1 +kind: PersistentVolume +metadata: + name: scion-workspaces +spec: + capacity: + storage: 1Ti + accessModes: [ReadWriteMany] + nfs: + server: 10.45.255.170 + path: /scion_share + mountOptions: [vers=3, hard, nconnect=4] + persistentVolumeReclaimPolicy: Retain +--- +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: scion-workspaces + namespace: scion-agents +spec: + accessModes: [ReadWriteMany] + storageClassName: "" + volumeName: scion-workspaces + resources: + requests: + storage: 1Ti diff --git a/scripts/cloudrun/Dockerfile b/scripts/cloudrun/Dockerfile new file mode 100644 index 000000000..6c5dad500 --- /dev/null +++ b/scripts/cloudrun/Dockerfile @@ -0,0 +1,67 @@ +# Scion Hub — Cloud Run container image +# Multi-stage build: web frontend → Go binary → slim runtime + +# --------------------------------------------------------------------------- +# Stage 1: Build web frontend +# --------------------------------------------------------------------------- +FROM node:20-slim AS web-builder + +WORKDIR /src/web +COPY web/package.json web/package-lock.json ./ +RUN npm ci --ignore-scripts +COPY web/ ./ +RUN npm run build + +# --------------------------------------------------------------------------- +# Stage 2: Build Go binary (with embedded web assets) +# --------------------------------------------------------------------------- +FROM golang:1.25 AS go-builder + +WORKDIR /src +COPY go.mod go.sum ./ +RUN go mod download + +COPY . . +COPY --from=web-builder /src/web/dist/client ./web/dist/client + +RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 \ + go build -buildvcs=false \ + -ldflags "-X github.com/GoogleCloudPlatform/scion/pkg/version.BuildTime=$(date -u +%Y-%m-%dT%H:%M:%SZ)" \ + -o /scion ./cmd/scion + +# --------------------------------------------------------------------------- +# Stage 3: Runtime +# --------------------------------------------------------------------------- +FROM debian:bookworm-slim + +RUN apt-get update && apt-get install -y --no-install-recommends \ + ca-certificates \ + git \ + openssh-client \ + curl \ + apt-transport-https \ + gnupg \ + && echo "deb [signed-by=/usr/share/keyrings/cloud.google.gpg] https://packages.cloud.google.com/apt cloud-sdk main" \ + > /etc/apt/sources.list.d/google-cloud-sdk.list \ + && curl -fsSL https://packages.cloud.google.com/apt/doc/apt-key.gpg \ + | gpg --dearmor -o /usr/share/keyrings/cloud.google.gpg \ + && apt-get update \ + && apt-get install -y --no-install-recommends google-cloud-cli-gke-gcloud-auth-plugin \ + && apt-get clean && rm -rf /var/lib/apt/lists/* + +RUN useradd -m -d /home/scion -s /bin/bash -u 1000 scion \ + && mkdir -p /home/scion/.kube /run/secrets \ + && chown -R scion:scion /home/scion /run/secrets + +COPY --from=go-builder /scion /usr/local/bin/scion +COPY scripts/cloudrun/entrypoint.sh /usr/local/bin/entrypoint.sh + +ENV HOME=/home/scion +ENV KUBECONFIG=/home/scion/.kube/config + +USER scion +WORKDIR /home/scion + +EXPOSE 8080 + +ENTRYPOINT ["/usr/local/bin/entrypoint.sh"] diff --git a/scripts/cloudrun/README.md b/scripts/cloudrun/README.md new file mode 100644 index 000000000..6e5f9ab9c --- /dev/null +++ b/scripts/cloudrun/README.md @@ -0,0 +1,114 @@ +# Scion Hub — Cloud Run Deployment + +Deploys the Scion hub as a single Cloud Run instance with IAP authentication +and a co-located GKE broker targeting `scion-demo-cluster`. + +## Architecture + +``` +User → Cloud Run (built-in IAP) → Hub container +┌──────────────────────────┐ +│ scion server (combo) │ +│ ├─ Hub API :8080 │ +│ ├─ Web UI :8080 │ +│ └─ Broker :9810 │──▶ GKE Autopilot (scion-demo-cluster) +│ SQLite: /tmp/scion.db│ namespace: scion-agents +└──────────────────────────┘ +``` + +- **IAP-protected** — Cloud Run's native IAP integration secures all ingress + paths (including the default `*.run.app` URL). No load balancer required. +- **Authenticated HTTPS only** (`--no-allow-unauthenticated`) +- **SQLite (ephemeral)** — lost on instance restart, acceptable for demo +- **GKE auth via ADC** — Cloud Run service account → Workload Identity → GKE + +## Prerequisites + +- `gcloud` CLI, authenticated with project `deploy-demo-test` +- `docker` CLI, authenticated to Artifact Registry +- `kubectl` with access to `scion-demo-cluster` (for namespace creation only) +- `openssl` (for session secret generation) +- IAP API enabled (`gcloud services enable iap.googleapis.com`) + +## Quick Start + +```bash +# Full deploy (build + push + secrets + Cloud Run + IAP) +./scripts/cloudrun/deploy.sh + +# Redeploy without rebuilding the image +./scripts/cloudrun/deploy.sh --skip-build +``` + +## Configuration + +Environment variables override defaults: + +| Variable | Default | Description | +|------------------------|----------------------|---------------------------------| +| `SCION_PROJECT` | `deploy-demo-test` | GCP project ID | +| `SCION_REGION` | `us-central1` | GCP region | +| `SCION_SERVICE` | `scion-hub` | Cloud Run service name | +| `SCION_GKE_CLUSTER` | `scion-demo-cluster` | Target GKE cluster | +| `SCION_SA_NAME` | `scion-hub-sa` | Service account name | +| `SCION_REPO` | `scion` | Artifact Registry repo name | +| `SCION_SESSION_SECRET` | *(auto-generated)* | JWT session secret (hex string) | + +## What the Deploy Script Does + +1. Creates a dedicated service account with `container.admin` and + `secretmanager.secretAccessor` roles (if it doesn't exist) +2. Creates a transport service account for agent IAP traversal (Phase 2) +3. Builds and pushes the container image to Artifact Registry +4. Fetches GKE cluster endpoint + CA cert and generates a kubeconfig +5. Computes the IAP audience (`/projects/NUM/locations/REGION/services/NAME`) +6. Generates hub settings from the template (injects session secret, IAP audience) +7. Stores kubeconfig and settings as Secret Manager secrets +8. Ensures the `scion-agents` namespace exists in GKE +9. Deploys the Cloud Run service with `--iap` flag and secrets mounted as files +10. Grants the IAP service agent `roles/run.invoker` on the service +11. Grants the transport SA `roles/iap.httpsResourceAccessor` for agent callbacks + +## Verification + +```bash +# Get the service URL +URL=$(gcloud run services describe scion-hub \ + --region us-central1 --project deploy-demo-test \ + --format="value(status.url)") + +# Verify IAP is enabled +gcloud run services describe scion-hub \ + --region us-central1 --project deploy-demo-test \ + | grep "Iap Enabled" + +# Direct health check (bypasses IAP via identity token) +curl -H "Authorization: Bearer $(gcloud auth print-identity-token)" "${URL}/health" + +# Visit the service URL in a browser — should redirect to Google sign-in +``` + +## Files + +| File | Purpose | +|-------------------------------|---------------------------------------------| +| `Dockerfile` | Multi-stage build: web + Go → slim runtime | +| `deploy.sh` | End-to-end deploy script | +| `hub-settings-template.yaml` | Hub settings (IAP audience, transport SA) | +| `README.md` | This file | + +## Notes + +- The Cloud Run instance uses `--timeout 3600` for long-lived WebSocket + connections from agent control channels. +- `--min-instances 1` keeps the instance warm. SQLite state is lost on cold + starts, so a warm instance is critical. +- The `gke-gcloud-auth-plugin` is installed in the image for robustness, but + `pkg/k8s/client.go` also has a `fallbackToGCEAuth()` path that uses ADC + directly if the plugin fails. +- Session secret is stored in Secret Manager and injected into settings at + deploy time, so it survives instance restarts. +- Cloud Run's native IAP protects all ingress paths without a load balancer, + managed cert, or static IP. The `*.run.app` URL is directly IAP-protected. +- Agent IAP traversal (transport SA) requires Phase 2 transport token code + which is not yet merged. The infrastructure and IAM bindings are in place. diff --git a/scripts/cloudrun/deploy.sh b/scripts/cloudrun/deploy.sh new file mode 100755 index 000000000..e35d206a6 --- /dev/null +++ b/scripts/cloudrun/deploy.sh @@ -0,0 +1,288 @@ +#!/usr/bin/env bash +# Deploy Scion hub as a Cloud Run service with IAP enabled directly. +# +# Architecture: +# User → Cloud Run (with built-in IAP) → Hub container +# +# Cloud Run's native IAP integration protects all ingress paths (including +# the default *.run.app URL) without requiring a load balancer, NEG, or +# managed certificate. +# +# Prerequisites: +# - gcloud CLI authenticated with sufficient permissions +# - docker CLI authenticated to Artifact Registry +# - kubectl configured for scion-demo-cluster (for namespace setup only) +# - IAP API enabled in the project (gcloud services enable iap.googleapis.com) +# +# Usage: +# ./scripts/cloudrun/deploy.sh # full deploy +# ./scripts/cloudrun/deploy.sh --skip-build # redeploy without rebuilding image + +set -euo pipefail + +# ── Configuration ──────────────────────────────────────────────────────────── + +PROJECT="${SCION_PROJECT:-deploy-demo-test}" +REGION="${SCION_REGION:-us-central1}" +SERVICE_NAME="${SCION_SERVICE:-scion-hub}" +GKE_CLUSTER="${SCION_GKE_CLUSTER:-scion-demo-cluster}" +SA_NAME="${SCION_SA_NAME:-scion-hub-sa}" +REPO="${SCION_REPO:-scion}" +IMAGE="us-central1-docker.pkg.dev/${PROJECT}/${REPO}/hub:latest" +K8S_NAMESPACE="scion-agents" + +# Optional: custom OAuth client for IAP (needed for external users) +IAP_CLIENT_ID="${SCION_IAP_CLIENT_ID:-}" +IAP_CLIENT_SECRET="${SCION_IAP_CLIENT_SECRET:-}" + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +REPO_ROOT="$(cd "${SCRIPT_DIR}/../.." && pwd)" + +SKIP_BUILD=false +for arg in "$@"; do + case "$arg" in + --skip-build) SKIP_BUILD=true ;; + esac +done + +# ── Helpers ────────────────────────────────────────────────────────────────── + +log() { echo "==> $*"; } +die() { echo "ERROR: $*" >&2; exit 1; } + +ensure_secret() { + local name="$1" + local data="$2" + if gcloud secrets describe "$name" --project="$PROJECT" &>/dev/null; then + log "Updating secret ${name}" + echo "$data" | gcloud secrets versions add "$name" --data-file=- --project="$PROJECT" + else + log "Creating secret ${name}" + echo "$data" | gcloud secrets create "$name" --data-file=- --project="$PROJECT" \ + --replication-policy=automatic + fi +} + +# ── 0. Validate ────────────────────────────────────────────────────────────── + +command -v gcloud >/dev/null || die "gcloud CLI not found" +command -v docker >/dev/null || die "docker CLI not found" + +# ── 1. Service account (hub) ──────────────────────────────────────────────── + +SA_EMAIL="${SA_NAME}@${PROJECT}.iam.gserviceaccount.com" + +if ! gcloud iam service-accounts describe "$SA_EMAIL" --project="$PROJECT" &>/dev/null; then + log "Creating service account ${SA_NAME}" + gcloud iam service-accounts create "$SA_NAME" \ + --display-name="Scion Hub (Cloud Run)" \ + --project="$PROJECT" + + for role in roles/container.admin roles/secretmanager.secretAccessor; do + gcloud projects add-iam-policy-binding "$PROJECT" \ + --member="serviceAccount:${SA_EMAIL}" \ + --role="$role" \ + --condition=None \ + --quiet + done +fi + +# ── 1b. Transport service account (for agent → hub IAP traversal) ─────────── + +TRANSPORT_SA_NAME="${SA_NAME}-transport" +TRANSPORT_SA_EMAIL="${TRANSPORT_SA_NAME}@${PROJECT}.iam.gserviceaccount.com" + +if ! gcloud iam service-accounts describe "$TRANSPORT_SA_EMAIL" --project="$PROJECT" &>/dev/null; then + log "Creating transport service account ${TRANSPORT_SA_NAME}" + gcloud iam service-accounts create "$TRANSPORT_SA_NAME" \ + --display-name="Scion Transport (IAP traversal)" \ + --project="$PROJECT" + + # Hub SA needs to mint tokens as the transport SA + gcloud iam service-accounts add-iam-policy-binding "$TRANSPORT_SA_EMAIL" \ + --member="serviceAccount:${SA_EMAIL}" \ + --role="roles/iam.serviceAccountTokenCreator" \ + --project="$PROJECT" \ + --quiet +fi + +# ── 2. Build & push image ─────────────────────────────────────────────────── + +if [[ "$SKIP_BUILD" == false ]]; then + log "Building container image" + docker build -f "${SCRIPT_DIR}/Dockerfile" -t "$IMAGE" "$REPO_ROOT" + + log "Pushing image to Artifact Registry" + docker push "$IMAGE" +else + log "Skipping build (--skip-build)" +fi + +# ── 3. Generate kubeconfig from live cluster info ──────────────────────────── + +log "Fetching GKE cluster details" +read -r ENDPOINT CA_CERT < <(gcloud container clusters describe "$GKE_CLUSTER" \ + --region "$REGION" --project "$PROJECT" \ + --format="value(endpoint,masterAuth.clusterCaCertificate)") + +[[ -n "$ENDPOINT" ]] || die "Could not fetch cluster endpoint" +[[ -n "$CA_CERT" ]] || die "Could not fetch cluster CA certificate" + +KUBECONFIG_CONTENT="apiVersion: v1 +kind: Config +clusters: +- cluster: + certificate-authority-data: ${CA_CERT} + server: https://${ENDPOINT} + name: ${GKE_CLUSTER} +contexts: +- context: + cluster: ${GKE_CLUSTER} + user: ${GKE_CLUSTER} + namespace: ${K8S_NAMESPACE} + name: ${GKE_CLUSTER} +current-context: ${GKE_CLUSTER} +users: +- name: ${GKE_CLUSTER} + user: + exec: + apiVersion: client.authentication.k8s.io/v1beta1 + command: gke-gcloud-auth-plugin + installHint: Install gke-gcloud-auth-plugin for use with kubectl by following https://cloud.google.com/kubernetes-engine/docs/how-to/cluster-access-for-kubectl#install_plugin + provideClusterInfo: true" + +# ── 4. Derive IAP audience ────────────────────────────────────────────────── +# For Cloud Run's direct IAP integration, the audience is: +# /projects/PROJECT_NUMBER/locations/REGION/services/SERVICE_NAME +# No backend service needs to exist — the audience is deterministic. + +PROJECT_NUMBER=$(gcloud projects describe "$PROJECT" --format="value(projectNumber)") +IAP_AUDIENCE="/projects/${PROJECT_NUMBER}/locations/${REGION}/services/${SERVICE_NAME}" +log "IAP audience: ${IAP_AUDIENCE}" + +# ── 5. Generate hub settings ──────────────────────────────────────────────── + +SESSION_SECRET="${SCION_SESSION_SECRET:-$(openssl rand -hex 32)}" + +SETTINGS_CONTENT=$(sed \ + -e "s|__SESSION_SECRET__|${SESSION_SECRET}|" \ + -e "s|__IAP_AUDIENCE__|${IAP_AUDIENCE}|" \ + -e "s|__TRANSPORT_SA_EMAIL__|${TRANSPORT_SA_EMAIL}|" \ + "${SCRIPT_DIR}/hub-settings-template.yaml") + +# ── 6. Store secrets ──────────────────────────────────────────────────────── + +log "Storing secrets in Secret Manager" +ensure_secret "${SERVICE_NAME}-kubeconfig" "$KUBECONFIG_CONTENT" +ensure_secret "${SERVICE_NAME}-settings" "$SETTINGS_CONTENT" + +# ── 7. Ensure K8s namespace ───────────────────────────────────────────────── + +log "Ensuring namespace ${K8S_NAMESPACE} exists in ${GKE_CLUSTER}" +LOCAL_KUBECONFIG=$(mktemp) +echo "$KUBECONFIG_CONTENT" > "$LOCAL_KUBECONFIG" +KUBECONFIG="$LOCAL_KUBECONFIG" kubectl create namespace "$K8S_NAMESPACE" --dry-run=client -o yaml | KUBECONFIG="$LOCAL_KUBECONFIG" kubectl apply -f - || true +rm -f "$LOCAL_KUBECONFIG" + +# ── 8. Create Artifact Registry repo (if needed) ──────────────────────────── + +if ! gcloud artifacts repositories describe "$REPO" \ + --location="$REGION" --project="$PROJECT" &>/dev/null; then + log "Creating Artifact Registry repository ${REPO}" + gcloud artifacts repositories create "$REPO" \ + --repository-format=docker \ + --location="$REGION" \ + --project="$PROJECT" +fi + +# ── 9. Deploy Cloud Run service with IAP enabled ──────────────────────────── + +log "Deploying Cloud Run service ${SERVICE_NAME} with IAP" +gcloud run deploy "$SERVICE_NAME" \ + --image "$IMAGE" \ + --region "$REGION" \ + --project "$PROJECT" \ + --min-instances 1 \ + --max-instances 1 \ + --no-allow-unauthenticated \ + --iap \ + --no-cpu-throttling \ + --service-account "$SA_EMAIL" \ + --port 8080 \ + --memory 1Gi \ + --cpu 1 \ + --timeout 3600 \ + --set-secrets "/home/scion/.kube/config=${SERVICE_NAME}-kubeconfig:latest,/run/secrets/settings.yaml=${SERVICE_NAME}-settings:latest" \ + --set-env-vars "HOME=/home/scion,KUBECONFIG=/home/scion/.kube/config" + +SERVICE_URL=$(gcloud run services describe "$SERVICE_NAME" \ + --region "$REGION" --project "$PROJECT" \ + --format="value(status.url)") + +# ── 10. Grant IAP service agent invoker permission ────────────────────────── +# The IAP service agent needs roles/run.invoker to forward authenticated +# requests to the Cloud Run service. + +log "Granting IAP service agent invoker permission" +gcloud run services add-iam-policy-binding "$SERVICE_NAME" \ + --region "$REGION" \ + --project "$PROJECT" \ + --member "serviceAccount:service-${PROJECT_NUMBER}@gcp-sa-iap.iam.gserviceaccount.com" \ + --role "roles/run.invoker" + +# ── 11. Configure custom OAuth client (if provided) ───────────────────────── +# By default, Cloud Run IAP uses a Google-managed OAuth client. If custom +# credentials are provided (needed for external users), configure them via +# IAP settings. + +if [[ -n "$IAP_CLIENT_ID" && -n "$IAP_CLIENT_SECRET" ]]; then + log "Configuring custom OAuth client for IAP" + IAP_SETTINGS_FILE=$(mktemp) + cat > "$IAP_SETTINGS_FILE" < "$HOME/.scion/settings.yaml" +fi +exec scion server start \ + --foreground --production --dev-auth \ + --enable-hub --enable-runtime-broker --enable-web --web-port 8080 \ + --auto-provide --global diff --git a/scripts/cloudrun/hub-settings-template.yaml b/scripts/cloudrun/hub-settings-template.yaml new file mode 100644 index 000000000..4d938fc26 --- /dev/null +++ b/scripts/cloudrun/hub-settings-template.yaml @@ -0,0 +1,30 @@ +schema_version: "1" +image_registry: "us-central1-docker.pkg.dev/deploy-demo-test/public-docker" +active_profile: default +server: + database: + driver: sqlite + url: /tmp/scion.db + auth: + session_secret: "__SESSION_SECRET__" + mode: proxy + proxy: + provider: iap + iap: + audience: "__IAP_AUDIENCE__" + transport: + mode: iap + oidcAudience: "__IAP_AUDIENCE__" + platformAuthSA: "__TRANSPORT_SA_EMAIL__" + runtimeBroker: + port: 9810 +profiles: + default: + runtime: kubernetes +runtimes: + kubernetes: + type: kubernetes + gke: true + context: scion-demo-cluster + namespace: scion-agents + list_all_namespaces: false diff --git a/scripts/starter-hub/README.md b/scripts/starter-hub/README.md index c8bf8b8bf..e1bf26c41 100644 --- a/scripts/starter-hub/README.md +++ b/scripts/starter-hub/README.md @@ -17,6 +17,9 @@ file paths from two primary variables: | `HUB_NAME` | `demo` | Deployment name — drives GCE instance, SA, firewall rule, cluster, and DNS names | | `BASE_DOMAIN` | `scion-ai.dev` | Root domain — combined with `HUB_NAME` to form `hub..` | | `ENABLE_GKE` | `false` | Set to `true` to provision a GKE cluster, grant `container.admin`, configure credentials, and use Kubernetes as the default runtime. | +| `REGION` | `us-central1` | GCP region for GKE and resource locations | +| `ZONE` | `us-central1-a` | GCP zone for the GCE VM instance | +| `MACHINE_TYPE` | *(derived)* | Compute machine type to use (overrides `SIZE_CHOICE`) | To stand up a second hub (e.g., "staging"): diff --git a/scripts/starter-hub/gce-demo-provision.sh b/scripts/starter-hub/gce-demo-provision.sh index 2ff455b5f..b22f5a4db 100755 --- a/scripts/starter-hub/gce-demo-provision.sh +++ b/scripts/starter-hub/gce-demo-provision.sh @@ -99,22 +99,24 @@ fi # Prompt for size (only needed if creating the instance) if [[ "${INSTANCE_EXISTS}" == "false" ]]; then - if [[ -z "${SIZE_CHOICE:-}" ]]; then - echo "Choose instance size:" - echo "1) Small (10s of agents) - e2-standard-4 (4 vCPU, 16GB)" - echo "2) Medium (~50 agents) - n2-standard-16 (16 vCPU, 64GB)" - echo "3) Large (100s of agents) - n2-standard-32 (32 vCPU, 128GB)" - echo "4) XLarge (~1000 agents) - n2-standard-128 (128 vCPU, 512GB)" - read -p "Select [1-4]: " SIZE_CHOICE - fi + if [[ -z "${MACHINE_TYPE:-}" ]]; then + if [[ -z "${SIZE_CHOICE:-}" ]]; then + echo "Choose instance size:" + echo "1) Small (10s of agents) - e2-standard-4 (4 vCPU, 16GB)" + echo "2) Medium (~50 agents) - n2-standard-16 (16 vCPU, 64GB)" + echo "3) Large (100s of agents) - n2-standard-32 (32 vCPU, 128GB)" + echo "4) XLarge (~1000 agents) - n2-standard-128 (128 vCPU, 512GB)" + read -p "Select [1-4]: " SIZE_CHOICE + fi - case $SIZE_CHOICE in - 1) MACHINE_TYPE="e2-standard-4" ;; - 2) MACHINE_TYPE="n2-standard-16" ;; - 3) MACHINE_TYPE="n2-standard-32" ;; - 4) MACHINE_TYPE="n2-standard-128" ;; - *) echo "Invalid choice: $SIZE_CHOICE"; exit 1 ;; - esac + case $SIZE_CHOICE in + 1) MACHINE_TYPE="e2-standard-4" ;; + 2) MACHINE_TYPE="n2-standard-16" ;; + 3) MACHINE_TYPE="n2-standard-32" ;; + 4) MACHINE_TYPE="n2-standard-128" ;; + *) echo "Invalid choice: $SIZE_CHOICE"; exit 1 ;; + esac + fi echo "Selected Machine Type: ${MACHINE_TYPE}" fi diff --git a/scripts/starter-hub/gce-demo-setup-repo.sh b/scripts/starter-hub/gce-demo-setup-repo.sh index df0553277..01d4fd167 100755 --- a/scripts/starter-hub/gce-demo-setup-repo.sh +++ b/scripts/starter-hub/gce-demo-setup-repo.sh @@ -61,7 +61,7 @@ gcloud compute ssh "${INSTANCE_NAME}" \ echo \"Repository /home/scion/scion already exists, fetching latest...\" sudo -u scion sh -c 'cd /home/scion/scion && git fetch origin && git reset --hard origin/HEAD' else - if [ -e \"/home/scion/scion\" ]; then + if sudo test -e \"/home/scion/scion\"; then echo \"Removing existing non-git path /home/scion/scion...\" sudo rm -rf /home/scion/scion fi diff --git a/shared-dirs/scratchpad/coordinator.md b/shared-dirs/scratchpad/coordinator.md new file mode 100644 index 000000000..de67981d2 --- /dev/null +++ b/shared-dirs/scratchpad/coordinator.md @@ -0,0 +1,75 @@ +# Coordinator Agent Workflow Instructions + +## Role +You are a coordinator agent. Your primary role is to manage agents using the Scion CLI and communicate with the user via `scion message`. You do not implement code yourself. You are acting as the product manager and are here to ensure that the project is completed completely and at high quality. + + +## Communication +- Always communicate with the user via `scion message --non-interactive ""` — direct text output is not visible to them. +- Report agent progress, and summaries proactively. + +## Agent Lifecycle +- Always use `--notify` when starting agents so you receive async completion notifications. +- After starting an agent, signal blocked status with `sciontool status blocked ""` and wait for the notification — do not poll or sleep. +- Stop agents after their work is complete to free resources. + + +## The `.scratch/` Directory +- `.scratch/` is gitignored — use it for agent briefs, investigation notes, and throwaway docs. +- Keep briefs concise: problem statement, not full analysis. + +## Project Tracking +- Maintain `/scion-volumes/scratchpad/projects.md` as a running index of all project work. +- When an agent completes work (bug fix, design doc, feature), add or update the entry in projects.md with: title, 1-3 line description, branch link, PR link (if any), and status. + +## Context Management +- Keep your coordinator context lean — delegate both investigation and implementation to engineerig managers to assign to developers. +- Don't run Explore agents or do detailed code analysis as the coordinator when you're going to assign an agent anyway. + +## Design Docs +- Design agents should write docs for smaller features should be written to `/scion-volumes/scratchpad/` and larger features to `/workspace/.design/` in the repo. + +## Agent Start Command +- The CLI syntax is `scion start [task...] [flags]` — there is no `--name` or `--instructions` flag. The agent name is a positional arg, and the task/instructions are passed as trailing positional args. +- When the default broker is unavailable, specify `--broker scion-gteam` explicitly (check existing agents with `scion list` to find the broker name). + +## Notification Behavior +- State-change notifications (COMPLETED, STALLED, etc.) fire for agent **subtask** completions too, not just the full job. Always check `scion look` before assuming the agent is done — verify the agent's task list and final output. +- Don't report completion to the user until you've confirmed the agent actually finished all its work. + +## Agent Cleanup +- Always stop then delete agents after their work is confirmed complete: `scion stop --non-interactive && scion delete --non-interactive` +- Clean up stalled agents too — a STALLED notification on a completed agent just means it went idle after finishing. + +## Autonomy & Progress +- **Never block on user availability.** You are the project driver — make decisions, keep moving. +- **Status updates should not pause work.** Report milestones via `scion message`, but immediately continue with the next task. Don't wait for acknowledgement. +- **Own the project direction.** You decide what to build next based on the design doc, security findings, integration results, etc. Only escalate genuine blockers (e.g., access, credentials, architectural ambiguity the design doc doesn't resolve). + +## Delegation Model +- **Never implement code directly.** All coding goes to eng-manager agents with clear, specific task descriptions. +- Use eng-manager agents for: feature implementation, bug fixes, security hardening, test writing, Dockerfile changes. +- Use specialized agents (e.g., sec-review-*) for: code audits, security reviews, focused analysis. +- The coordinator's job: plan phases, write agent briefs, review results, verify commits compile/pass tests, coordinate sequencing, report to Preston. + +## Waiting for Agents (Notification-Based, Not Polling) +- After starting an agent with `--notify`, call `sciontool status blocked ""` and **stop**. Do not create polling crons, sleep loops, or `scion look` checks. +- The scion system will deliver a notification message when the agent's state changes (completed, stalled, etc.). +- Only after receiving the notification, use `scion look` to verify the agent fully finished (subtask completions can also trigger notifications). + + +## Accumulated Tips +- When the user refers to "scratchpad", they mean `/scion-volumes/scratchpad/` — the directory where this instructions document lives. +- Messages typed directly into the coordinator's terminal (not via Scion) don't need a `scion message` reply — just respond inline. Only use `scion message` to reply to named users who sent a Scion message. +- Primary user is `ptone@google.com` (Preston). Use this identifier for `scion message`. +- The user appreciates concise status updates and proactive reporting of agent results (key findings, branch names, GitHub URLs). +- Subtask completion notifications can fire before the agent is truly done — always `scion look` to confirm all tasks are finished before acting on the result. +- When delegating security fixes, provide specific file paths, line numbers, and the exact vulnerability description from the review report — vague instructions lead to incomplete fixes. +- Clean up completed security review agents and old eng-managers once their work is confirmed merged or committed. +- **Multi-user independence:** Other users (e.g. ghchinoy@google.com) may message the coordinator. Reply to them directly. Do NOT notify Preston when you reply to other users — handle each independently. +- **eng-manager slug collision:** Only one eng-manager can run at a time — they share the same slug. Starting a second while one is running silently disrupts both and neither produces work. Always run eng-manager agents sequentially. +- **Agent task size limit:** Passing large briefs inline via `$(cat file.md)` causes the agent to abort silently if the content is too large (~5KB+). Fix: commit the brief to the repo (e.g. `.tasks/phase-N-name.md`) and pass a short pointer task like "Read and implement .tasks/phase-N-name.md". This reliably works. +- **`scion look` fails on stopped containers:** After an agent stops, `scion look` returns a docker exec error. Use `git log --oneline` and `git diff HEAD~1..HEAD --stat` to verify what was committed instead. +- **Plan approval timing:** eng-manager agents enter WAITING_FOR_INPUT for plan approval shortly after starting. If you go `sciontool status blocked` immediately, you may miss that notification and the agent will time out. Either wait ~30–45s and check the agent is still running before going blocked, or check the list quickly after blocking to confirm it hasn't already stopped. +- **Verify agent is actually running before going blocked:** After starting an agent and before calling `sciontool status blocked`, do a quick `sleep 30 && scion list` check to confirm the agent is still in `running` phase. If it stopped immediately, investigate before blocking. +- **`scion look` during active run:** `scion look` works fine while the agent is running but fails after it stops. Use it proactively to check plan approval prompts, not retrospectively. \ No newline at end of file diff --git a/shared-dirs/scratchpad/instance-interaction.md b/shared-dirs/scratchpad/instance-interaction.md new file mode 100644 index 000000000..129b461d4 --- /dev/null +++ b/shared-dirs/scratchpad/instance-interaction.md @@ -0,0 +1,88 @@ +You are an AI agent whose primary role is to manage and interact with a GCP VM via `gcloud compute ssh --zone "us-central1-a" "scion-aiopm" --project "deploy-demo-test"` + +If you do not have the ssh command already installed in your environment, you will need to install it with apt. You have sudo in this environment, and on the scion-aiopm GCE VM. + +Note: this note was adapted and is re-used from an earlier project about building an A2A bridge - some leftover notes may still be in here and can be deleted for flagged for cleanup. + +## VM Details + +- **Instance**: `scion-aiopm` +- **Zone**: `us-central1-a` +- **Project**: `deploy-demo-test` +- **SSH user**: Logs in as a service account (`sa_*`), not as `scion`. Use `sudo -u scion bash -c '...'` to run commands as the scion user, or `sudo` for root-level operations. + +## Repository Configuration + +The scion repo is checked out at `/home/scion/scion` on the VM. + +- **Remote**: `https://github.com/ptone/scion.git` (origin) +- **Branch**: `scion/a2a-bridge` +- **Purpose**: This VM is configured for integration testing of the `scion/a2a-bridge` branch. Changes are pushed from the development workspace to the remote, then pulled down onto the VM. + +## Hub Service + +- **Service**: `scion-hub` (systemd) +- **Config directory**: `/home/scion/.scion/` +- **Environment file**: `/home/scion/.scion/hub.env` +- **Settings**: `/home/scion/.scion/settings.yaml` +- **Database**: `/home/scion/.scion/hub.db` +- **Service file**: `/etc/systemd/system/scion-hub.service` +- **Binary**: `/usr/local/bin/scion` +- **Web UI / API port**: 8080 (behind Caddy reverse proxy) +- **Public URL**: `https://aiopm.projects.scion-ai.dev` +- **Caddy config**: `/etc/caddy/Caddyfile` (serves `aiopm.projects.scion-ai.dev`) + +### Key hub.env settings +- `SCION_MAINTENANCE_REPO_PATH="/home/scion/scion"` — points rebuild operations at the local checkout +- `SCION_MAINTENANCE_REPO_BRANCH=scion/chat-tee` — pins rebuilds to this branch + +## Common Operations + +### Check service status +```bash +gcloud compute ssh --zone "us-central1-a" "scion-aiopm" --project "deploy-demo-test" --command "sudo systemctl status scion-hub" +``` + +### Pull latest code on VM +```bash +gcloud compute ssh --zone "us-central1-a" "scion-aiopm" --project "deploy-demo-test" --command "sudo -u scion bash -c 'cd /home/scion/scion && git pull origin scion/chat-tee'" +``` + +### Rebuild and restart hub +```bash +gcloud compute ssh --zone "us-central1-a" "scion-aiopm" --project "deploy-demo-test" --command " +sudo -u scion bash -c 'cd /home/scion/scion && git pull origin scion/a2a-bridge && make web && /usr/local/go/bin/go build -o scion ./cmd/scion' +sudo systemctl stop scion-hub +sudo mv /home/scion/scion/scion /usr/local/bin/scion +sudo chmod +x /usr/local/bin/scion +sudo systemctl start scion-hub +" +``` + +### View recent logs +```bash +gcloud compute ssh --zone "us-central1-a" "scion-aiopm" --project "deploy-demo-test" --command "sudo journalctl -u scion-hub -n 50 --no-pager" +``` + +### Health check +```bash +gcloud compute ssh --zone "us-central1-a" "scion-aiopm" --project "deploy-demo-test" --command "curl -s http://localhost:8080/healthz" +``` + +## Integration Testing Workflow + +1. Make changes in the development workspace on branch `scion/a2a-bridge` +2. Push to remote: `git push origin scion/a2a-bridge` +3. Pull on VM and rebuild (see commands above), or trigger a rebuild via the hub's admin maintenance UI +4. Test against `https://integration.projects.scion-ai.dev` + + +## SSH Notes + +- **Do NOT use `--tunnel-through-iap`** — the VM has an external IP (35.232.118.211) and OS Login. Direct SSH works fine. +- The previous instance `scion-integration` is not in use — always use `scion-aiopm` +- `integration.projects.scion-ai.dev` (136.111.240.153) is the OLD VM — do not use +- `aiopm.projects.scion-ai.dev` (35.232.118.211) is THIS VM +- The hub can also self-rebuild via its admin maintenance page (rebuild-server / rebuild-web tasks), which respect the `SCION_MAINTENANCE_REPO_BRANCH` setting + + diff --git a/test_json.go b/test_json.go deleted file mode 100644 index cef87b7d9..000000000 --- a/test_json.go +++ /dev/null @@ -1,32 +0,0 @@ -package main - -import ( - "encoding/json" - "fmt" -) - -type E struct{ Name string } - -func (e *E) UnmarshalJSON(data []byte) error { - fmt.Println("E.UnmarshalJSON called") - e.Name = "unmarshaled" - return nil -} - -type T struct{ E } - -func (t *T) UnmarshalJSON(data []byte) error { - fmt.Println("T.UnmarshalJSON called") - type Alias T - var a Alias - if err := json.Unmarshal(data, &a); err != nil { - return err - } - t.E = a.E - return nil -} -func main() { - var t T - json.Unmarshal([]byte("{\"Name\":\"test\"}"), &t) - fmt.Println(t.Name) -} diff --git a/web/src/client/main.ts b/web/src/client/main.ts index 97aeb854c..afc0892e3 100644 --- a/web/src/client/main.ts +++ b/web/src/client/main.ts @@ -147,6 +147,7 @@ const ROUTES: RouteConfig[] = [ { pattern: /^\/profile\/settings$/, tag: 'scion-page-profile-settings', load: () => import('../components/pages/profile-settings.js') }, { pattern: /^\/profile\/tokens$/, tag: 'scion-page-profile-tokens', load: () => import('../components/pages/profile-tokens.js') }, { pattern: /^\/profile\/telegram$/, tag: 'scion-page-profile-telegram', load: () => import('../components/pages/profile-telegram.js') }, + { pattern: /^\/profile\/discord$/, tag: 'scion-page-profile-discord', load: () => import('../components/pages/profile-discord.js') }, { pattern: /^\/profile$/, tag: 'scion-page-profile-env-vars', load: () => import('../components/pages/profile-env-vars.js') }, { pattern: /^\/github-app\/installed$/, tag: 'scion-page-github-app-setup', load: () => import('../components/pages/github-app-setup.js') }, { pattern: /^\/projects\/new$/, tag: 'scion-page-project-create', load: () => import('../components/pages/project-create.js') }, @@ -169,7 +170,7 @@ const STANDALONE_ROUTES = new Set(['scion-login-page', 'scion-page-invite']); /** * Routes that render inside the profile shell instead of the main app shell */ -const PROFILE_ROUTES = new Set(['scion-page-profile-env-vars', 'scion-page-profile-secrets', 'scion-page-profile-settings', 'scion-page-profile-tokens', 'scion-page-profile-telegram']); +const PROFILE_ROUTES = new Set(['scion-page-profile-env-vars', 'scion-page-profile-secrets', 'scion-page-profile-settings', 'scion-page-profile-tokens', 'scion-page-profile-telegram', 'scion-page-profile-discord']); /** * Routes that require admin role. Non-admin users are redirected to dashboard. diff --git a/web/src/components/pages/admin-maintenance.ts b/web/src/components/pages/admin-maintenance.ts index 8b93c23f1..103694c11 100644 --- a/web/src/components/pages/admin-maintenance.ts +++ b/web/src/components/pages/admin-maintenance.ts @@ -126,6 +126,14 @@ export class ScionPageAdminMaintenance extends LitElement { @state() private viewingRun: MaintenanceRun | null = null; + /** Whether the bulk reset-auth request is in-flight. */ + @state() + private resetAuthAllLoading = false; + + /** Result of the last bulk reset-auth request. */ + @state() + private resetAuthAllResult: { succeeded: { id: string; name: string }[]; failed: { id: string; name: string; error: string }[]; total: number } | null = null; + /** Update check state for rebuild-server. */ @state() private updateCheckLoading = false; @@ -946,11 +954,84 @@ export class ScionPageAdminMaintenance extends LitElement { private renderContent() { return html` ${this.renderMaintenanceMode()} + ${this.renderQuickActions()} ${this.renderMigrations()} ${this.renderOperations()} `; } + private renderQuickActions() { + return html` +
+

Quick Actions

+

+ One-off administrative actions across all agents. +

+
+
+
+ + Reset Auth — All Running Agents + ${this.resetAuthAllLoading + ? html`Running...` + : html` + this.handleResetAuthAll()} + > + + Reset Auth + + `} +
+
+ Inject a fresh auth token into every running agent without restarting them. + The token refresh loop in each agent will pick up the new credentials automatically. +
+ ${this.resetAuthAllResult + ? html` +
+ + Total: ${this.resetAuthAllResult.total} · + Succeeded: ${this.resetAuthAllResult.succeeded?.length ?? 0} · + Failed: ${this.resetAuthAllResult.failed?.length ?? 0} + +
+ ${(this.resetAuthAllResult.failed?.length ?? 0) > 0 + ? html` +
${this.resetAuthAllResult.failed + .map((f) => `${f.name || f.id}: ${f.error}`) + .join('\n')}
+ ` + : nothing} + ` + : nothing} +
+
+
+ `; + } + + private async handleResetAuthAll() { + this.resetAuthAllLoading = true; + this.resetAuthAllResult = null; + try { + const response = await apiFetch('/api/v1/admin/agents/reset-auth-all', { + method: 'POST', + }); + if (!response.ok) { + const errMsg = await extractApiError(response, `HTTP ${response.status}`); + throw new Error(errMsg); + } + this.resetAuthAllResult = await response.json(); + } catch (err) { + alert(err instanceof Error ? err.message : 'Failed to reset auth for all agents'); + } finally { + this.resetAuthAllLoading = false; + } + } + private renderMaintenanceMode() { return html`
diff --git a/web/src/components/pages/agent-create.ts b/web/src/components/pages/agent-create.ts index 39b9122e0..ce592a36f 100644 --- a/web/src/components/pages/agent-create.ts +++ b/web/src/components/pages/agent-create.ts @@ -1130,7 +1130,8 @@ private selectBrokerForProject(): void { ` ) - : html` + : // Fallback: all known/installable harnesses (incl. opt-in), not the default-install set. + html` Gemini Claude OpenCode diff --git a/web/src/components/pages/agent-detail.ts b/web/src/components/pages/agent-detail.ts index 564393ae0..7695fba85 100644 --- a/web/src/components/pages/agent-detail.ts +++ b/web/src/components/pages/agent-detail.ts @@ -756,7 +756,7 @@ export class ScionPageAgentDetail extends LitElement { } private async handleAction( - action: 'start' | 'stop' | 'suspend' | 'resume' | 'delete', + action: 'start' | 'stop' | 'suspend' | 'resume' | 'delete' | 'reset-auth', event?: MouseEvent ): Promise { if (!this.agent) return; @@ -786,6 +786,28 @@ export class ScionPageAgentDetail extends LitElement { return; } + if (action === 'reset-auth') { + this.actionLoading = { ...this.actionLoading, 'reset-auth': true }; + try { + const response = await apiFetch( + `/api/v1/agents/${this.agentId}/reset-auth`, + { method: 'POST' } + ); + if (!response.ok) { + throw new Error( + await extractApiError(response, 'Failed to reset auth') + ); + } + this.backgroundRefresh(); + } catch (err) { + console.error('Failed to reset auth:', err); + alert(err instanceof Error ? err.message : 'Failed to reset auth'); + } finally { + this.actionLoading = { ...this.actionLoading, 'reset-auth': false }; + } + return; + } + const optimisticPhase: Record = { start: 'starting', stop: 'stopping', @@ -1080,6 +1102,23 @@ export class ScionPageAgentDetail extends LitElement { ` : nothing} + ${isAgentRunning(agent) + ? html` + + this.handleAction('reset-auth')} + > + + Reset Auth + + + ` + : nothing} ${can(agent._capabilities, 'delete') ? html` a.phase === this.phaseFilter); + } + const sorted = [...list]; + sorted.sort((a, b) => { + let cmp = 0; + switch (this.sortField) { + case 'name': + cmp = (a.name || '').localeCompare(b.name || ''); + break; + case 'status': + cmp = getAgentDisplayStatus(a).localeCompare(getAgentDisplayStatus(b)); + break; + case 'created': + cmp = (a.created || '').localeCompare(b.created || ''); + break; + case 'updated': + cmp = (a.updated || '').localeCompare(b.updated || ''); + break; + } + return this.sortDir === 'asc' ? cmp : -cmp; + }); + return sorted; + } + + private formatRelativeTime(isoString: string): string { + const date = new Date(isoString); + if (isNaN(date.getTime())) return '—'; + const now = Date.now(); + const diffMs = now - date.getTime(); + if (diffMs < 0) return 'just now'; + const seconds = Math.floor(diffMs / 1000); + if (seconds < 60) return 'just now'; + const minutes = Math.floor(seconds / 60); + if (minutes < 60) return `${minutes}m ago`; + const hours = Math.floor(minutes / 60); + if (hours < 24) return `${hours}h ago`; + const days = Math.floor(hours / 24); + return `${days}d ago`; + } + + private setPhaseFilter(phase: AgentPhase | ''): void { + if (this.phaseFilter === phase) return; + this.phaseFilter = phase; + if (phase) { + localStorage.setItem('scion-filter-agents-phase', phase); + } else { + localStorage.removeItem('scion-filter-agents-phase'); + } + } + + private toggleSort(field: AgentSortField): void { + if (this.sortField === field) { + this.sortDir = this.sortDir === 'asc' ? 'desc' : 'asc'; + } else { + this.sortField = field; + this.sortDir = field === 'name' ? 'asc' : 'desc'; + } + localStorage.setItem('scion-sort-agents', JSON.stringify({ field: this.sortField, dir: this.sortDir })); + } + + private sortIndicator(field: AgentSortField): string { + return this.sortField === field ? (this.sortDir === 'asc' ? '▲' : '▼') : '▲'; + } + private setScope(scope: 'all' | 'mine' | 'shared'): void { if (this.agentScope === scope) return; this.agentScope = scope; @@ -538,7 +699,10 @@ export class ScionPageAgents extends LitElement {
- ${this.loading ? this.renderLoading() : this.error ? this.renderError() : this.renderAgents()} + ${this.loading ? this.renderLoading() : this.error ? this.renderError() : html` + ${this.renderFilterBar()} + ${this.renderAgents()} + `} `; } @@ -566,6 +730,50 @@ export class ScionPageAgents extends LitElement { `; } + private renderFilterBar() { + return html` +
+ Status: +
+ + + + + +
+ ${this.viewMode === 'grid' ? html` + + + + Sort: ${this.sortField} + + ) => this.toggleSort(e.detail.item.value as AgentSortField)}> + Name + Status + Created + Updated + + + ` : nothing} +
+ `; + } + private renderAgents() { if (this.agents.length === 0) { if (this.agentScope === 'mine') { @@ -589,6 +797,17 @@ export class ScionPageAgents extends LitElement { return this.renderEmptyState(); } + const filtered = this.displayAgents; + if (filtered.length === 0 && this.phaseFilter) { + return html` +
+ +

No Matching Agents

+

No agents match the current filter. Try changing the status filter.

+
+ `; + } + return this.viewMode === 'grid' ? this.renderGrid() : this.renderTable(); } @@ -614,13 +833,116 @@ export class ScionPageAgents extends LitElement { private renderGrid() { return html` -
${this.agents.map((agent) => this.renderAgentCard(agent))}
+
${this.displayAgents.map((agent) => this.renderAgentCard(agent))}
`; } - private renderAgentCard(agent: Agent) { + private renderActionButtons(agent: Agent) { const isLoading = this.actionLoading[agent.id] || false; + return html` + ${can(agent._capabilities, 'attach') ? html` + + + + + + + + ` : nothing} + ${isAgentRunning(agent) + ? can(agent._capabilities, 'stop') ? html` + ${agent.harnessCapabilities?.resume?.support !== 'no' ? html` + + this.handleAgentAction(agent.id, 'suspend')} + aria-label="Suspend" + > + + + + ` : nothing} + + this.handleAgentAction(agent.id, 'stop')} + aria-label="Stop" + > + + + + ` : nothing + : agent.phase === 'suspended' + ? can(agent._capabilities, 'start') ? html` + + this.handleAgentAction(agent.id, 'resume')} + aria-label="Resume" + > + + + + ` : nothing + : can(agent._capabilities, 'start') ? html` + + this.handleAgentAction(agent.id, 'start')} + aria-label="Start" + > + + + + ` : nothing} + ${can(agent._capabilities, 'delete') ? html` + + this.handleAgentAction(agent.id, 'delete', e)} + aria-label="Delete" + > + + + + ` : nothing} + `; + } + + private renderAgentCard(agent: Agent) { return html`
@@ -655,83 +977,7 @@ export class ScionPageAgents extends LitElement { ${agent.taskSummary ? html`
${agent.taskSummary}
` : ''}
- ${can(agent._capabilities, 'attach') ? html` - - - Terminal - - ` : nothing} - ${isAgentRunning(agent) - ? can(agent._capabilities, 'stop') ? html` - ${agent.harnessCapabilities?.resume?.support !== 'no' ? html` - this.handleAgentAction(agent.id, 'suspend')} - > - - Suspend - - ` : nothing} - this.handleAgentAction(agent.id, 'stop')} - > - - Stop - - ` : nothing - : agent.phase === 'suspended' - ? can(agent._capabilities, 'start') ? html` - this.handleAgentAction(agent.id, 'resume')} - > - - Resume - - ` : nothing - : can(agent._capabilities, 'start') ? html` - this.handleAgentAction(agent.id, 'start')} - > - - Start - - ` : nothing} - ${can(agent._capabilities, 'delete') ? html` - this.handleAgentAction(agent.id, 'delete', e)} - > - - - ` : nothing} + ${this.renderActionButtons(agent)}
`; @@ -743,16 +989,26 @@ export class ScionPageAgents extends LitElement { - + - + + - ${this.agents.map((agent) => this.renderAgentRow(agent))} + ${this.displayAgents.map((agent) => this.renderAgentRow(agent))}
Name this.toggleSort('name')} + >Name ${this.sortIndicator('name')} Project TemplateStatus this.toggleSort('status')} + >Status ${this.sortIndicator('status')} this.toggleSort('updated')} + >Updated ${this.sortIndicator('updated')} Task Actions
@@ -760,8 +1016,6 @@ export class ScionPageAgents extends LitElement { } private renderAgentRow(agent: Agent) { - const isLoading = this.actionLoading[agent.id] || false; - return html` @@ -779,83 +1033,13 @@ export class ScionPageAgents extends LitElement { size="small" > + ${agent.updated ? this.formatRelativeTime(agent.updated) : '\u2014'} ${agent.taskSummary || '\u2014'} - ${can(agent._capabilities, 'attach') ? html` - - - - ` : nothing} - ${isAgentRunning(agent) - ? can(agent._capabilities, 'stop') ? html` - ${agent.harnessCapabilities?.resume?.support !== 'no' ? html` - this.handleAgentAction(agent.id, 'suspend')} - > - - - ` : nothing} - this.handleAgentAction(agent.id, 'stop')} - > - - - ` : nothing - : agent.phase === 'suspended' - ? can(agent._capabilities, 'start') ? html` - this.handleAgentAction(agent.id, 'resume')} - > - - - ` : nothing - : can(agent._capabilities, 'start') ? html` - this.handleAgentAction(agent.id, 'start')} - > - - - ` : nothing} - ${can(agent._capabilities, 'delete') ? html` - this.handleAgentAction(agent.id, 'delete', e)} - > - - - ` : nothing} + ${this.renderActionButtons(agent)} diff --git a/web/src/components/pages/profile-discord.ts b/web/src/components/pages/profile-discord.ts new file mode 100644 index 000000000..7a7c05ebe --- /dev/null +++ b/web/src/components/pages/profile-discord.ts @@ -0,0 +1,323 @@ +/** + * Copyright 2026 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * Profile Discord linking page + * + * Allows users to link their Discord account by entering a 6-character + * code provided by the Discord bot. + */ + +import { LitElement, html, css, nothing } from 'lit'; +import { customElement, state } from 'lit/decorators.js'; +import { apiFetch } from '../../client/api.js'; + +@customElement('scion-page-profile-discord') +export class ScionPageProfileDiscord extends LitElement { + @state() + private _code = ''; + + @state() + private _status: 'idle' | 'submitting' | 'success' | 'error' = 'idle'; + + @state() + private _message = ''; + + @state() + private _discordUsername = ''; + + override connectedCallback(): void { + super.connectedCallback(); + const params = new URLSearchParams(window.location.search); + const code = params.get('code'); + const userName = params.get('user_name'); + if (userName) { + this._discordUsername = userName; + } + if (code) { + this._code = code.toUpperCase().replace(/[^A-Z0-9]/g, '').slice(0, 6); + if (this._code.length === 6) { + this._autoSubmit(); + } + } + } + + private async _autoSubmit(): Promise { + this._status = 'submitting'; + this._message = 'Linking your account…'; + + try { + const resp = await apiFetch('/api/v1/discord/link/verify', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ code: this._code }), + }); + + if (resp.ok) { + this._status = 'success'; + this._message = 'Discord account linked successfully! You can close this page and return to Discord.'; + this._code = ''; + } else { + const errData = (await resp.json().catch(() => null)) as { + message?: string; + } | null; + this._status = 'error'; + this._message = errData?.message || 'Code not found or expired. Please try again with a new code from the bot.'; + } + } catch { + this._status = 'error'; + this._message = 'Failed to connect to the server. Please try again.'; + } + } + + static override styles = css` + :host { + display: block; + } + + .page-header { + display: flex; + align-items: flex-start; + justify-content: space-between; + margin-bottom: 1.5rem; + gap: 1rem; + } + + .page-header-info h1 { + font-size: 1.5rem; + font-weight: 700; + color: var(--scion-text, #1e293b); + margin: 0 0 0.25rem 0; + } + + .page-header-info p { + color: var(--scion-text-muted, #64748b); + font-size: 0.875rem; + margin: 0; + } + + .settings-card { + background: var(--scion-surface, #ffffff); + border: 1px solid var(--scion-border, #e2e8f0); + border-radius: 0.75rem; + padding: 1.5rem; + margin-bottom: 1.5rem; + } + + .section-title { + font-size: 1rem; + font-weight: 600; + color: var(--scion-text, #1e293b); + margin: 0 0 1rem 0; + display: flex; + align-items: center; + gap: 0.5rem; + } + + .section-title sl-icon { + font-size: 1.125rem; + color: var(--scion-text-muted, #64748b); + } + + .instructions { + font-size: 0.875rem; + color: var(--scion-text-muted, #64748b); + margin: 0 0 1.25rem 0; + line-height: 1.6; + } + + .instructions ol { + margin: 0.5rem 0; + padding-left: 1.25rem; + } + + .instructions li { + margin-bottom: 0.375rem; + } + + .discord-user { + font-size: 0.875rem; + color: var(--scion-text, #1e293b); + margin: 0 0 1rem 0; + } + + .discord-user strong { + color: var(--scion-primary, #6366f1); + } + + .code-form { + display: flex; + align-items: flex-end; + gap: 0.75rem; + } + + .code-input { + flex: 0 0 auto; + } + + .code-input sl-input::part(input) { + font-family: monospace; + font-size: 1.25rem; + letter-spacing: 0.25em; + text-transform: uppercase; + text-align: center; + } + + .result-message { + display: flex; + align-items: center; + gap: 0.5rem; + margin-top: 1rem; + padding: 0.625rem 0.875rem; + border-radius: 0.375rem; + font-size: 0.8125rem; + } + + .result-message sl-icon { + font-size: 1rem; + flex-shrink: 0; + } + + .result-success { + background: var(--sl-color-success-50, #f0fdf4); + color: var(--sl-color-success-700, #15803d); + border: 1px solid var(--sl-color-success-200, #bbf7d0); + } + + .result-error { + background: var(--sl-color-danger-50, #fef2f2); + color: var(--sl-color-danger-700, #b91c1c); + border: 1px solid var(--sl-color-danger-200, #fecaca); + } + `; + + private _handleInput(e: Event): void { + const input = e.target as HTMLInputElement & { value: string }; + this._code = input.value.toUpperCase().replace(/[^A-Z0-9]/g, '').slice(0, 6); + input.value = this._code; + } + + private async _handleSubmit(e: Event): Promise { + e.preventDefault(); + + if (this._code.length !== 6) { + this._status = 'error'; + this._message = 'Please enter the full 6-character code.'; + return; + } + + this._status = 'submitting'; + this._message = ''; + + try { + const resp = await apiFetch('/api/v1/discord/link/verify', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ code: this._code }), + }); + + if (resp.ok) { + this._status = 'success'; + this._message = 'Discord account linked successfully! You can close this page and return to Discord.'; + this._code = ''; + } else { + const errData = (await resp.json().catch(() => null)) as { + message?: string; + } | null; + this._status = 'error'; + this._message = errData?.message || 'Code not found or expired. Please try again with a new code from the bot.'; + } + } catch { + this._status = 'error'; + this._message = 'Failed to connect to the server. Please try again.'; + } + } + + override render() { + return html` + + +
+

+ + Link Discord Account +

+ +
+
    +
  1. Open a channel with the Scion Discord bot
  2. +
  3. Use the /scion register command
  4. +
  5. Enter the 6-character code below
  6. +
+
+ + ${this._discordUsername + ? html`

Linking as Discord user: ${this._discordUsername}

` + : nothing} + +
+
+ +
+ + Link Account + +
+ + ${this._status === 'success' + ? html` +
+ + ${this._message} +
+ ` + : nothing} + ${this._status === 'error' + ? html` +
+ + ${this._message} +
+ ` + : nothing} +
+ `; + } +} + +declare global { + interface HTMLElementTagNameMap { + 'scion-page-profile-discord': ScionPageProfileDiscord; + } +} diff --git a/web/src/components/pages/project-settings.ts b/web/src/components/pages/project-settings.ts index 8c3e2b0d1..756553d58 100644 --- a/web/src/components/pages/project-settings.ts +++ b/web/src/components/pages/project-settings.ts @@ -1438,7 +1438,8 @@ export class ScionPageProjectSettings extends LitElement { ` ) - : html` + : // Fallback: all known/installable harnesses (incl. opt-in), not the default-install set. + html` Gemini Claude OpenCode @@ -1768,7 +1769,7 @@ export class ScionPageProjectSettings extends LitElement { ?canImport=${canSync} allowWorkspace gitRemote=${this.project?.gitRemote ?? ''} - @resource-imported=${() => { + @resource-changed=${() => { this.refreshTemplatesList(); void this.loadDropdownTemplates(); }} @@ -1779,6 +1780,13 @@ export class ScionPageProjectSettings extends LitElement { scope="project" .scopeId=${this.projectId} detailBasePath="/projects/${this.projectId}" + ?canClone=${canSync} + ?canDelete=${can(this.project!._capabilities, 'delete') || can(this.project!._capabilities, 'manage')} + ?cloneFromGlobal=${canSync} + @resource-changed=${() => { + this.refreshTemplatesList(); + void this.loadDropdownTemplates(); + }} > `; } @@ -1801,7 +1809,7 @@ export class ScionPageProjectSettings extends LitElement { ?canImport=${canSync} allowWorkspace gitRemote=${this.project?.gitRemote ?? ''} - @resource-imported=${() => this.refreshHarnessConfigsList()} + @resource-changed=${() => this.refreshHarnessConfigsList()} > this.refreshHarnessConfigsList()} > `; } diff --git a/web/src/components/pages/settings.ts b/web/src/components/pages/settings.ts index b95964e46..1197f0701 100644 --- a/web/src/components/pages/settings.ts +++ b/web/src/components/pages/settings.ts @@ -160,13 +160,16 @@ export class ScionPageSettings extends LitElement { kind="template" scope="global" canImport - @resource-imported=${() => this.refreshList('templates-list')} + @resource-changed=${() => this.refreshList('templates-list')} > this.refreshList('templates-list')} > @@ -178,13 +181,16 @@ export class ScionPageSettings extends LitElement { kind="harness-config" scope="global" canImport - @resource-imported=${() => this.refreshList('harness-configs-list')} + @resource-changed=${() => this.refreshList('harness-configs-list')} > this.refreshList('harness-configs-list')} > diff --git a/web/src/components/pages/terminal.ts b/web/src/components/pages/terminal.ts index f3413d83a..f619d369b 100644 --- a/web/src/components/pages/terminal.ts +++ b/web/src/components/pages/terminal.ts @@ -67,6 +67,9 @@ export class ScionPageTerminal extends LitElement { @state() private connected = false; + @state() + private wasConnected = false; + @state() private error: string | null = null; @@ -228,12 +231,43 @@ export class ScionPageTerminal extends LitElement { opacity: 0.4; } - .terminal-container { + .terminal-wrapper { flex: 1; position: relative; overflow: hidden; } + .terminal-container { + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + } + + .disconnected-overlay { + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: rgba(0, 0, 0, 0.5); + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + z-index: 10; + pointer-events: none; + } + + .disconnected-overlay .overlay-text { + color: #ef4444; + font-size: 2rem; + font-weight: 700; + letter-spacing: 0.15em; + text-shadow: 0 2px 8px rgba(0, 0, 0, 0.6); + } + .loading-state, .error-state { display: flex; @@ -588,6 +622,7 @@ export class ScionPageTerminal extends LitElement { this.socket.onopen = () => { console.debug('[Terminal] WebSocket connected'); this.connected = true; + this.wasConnected = true; this.error = null; // Re-fit now that the connection is live so tmux gets accurate dimensions if (this.fitAddon) { @@ -683,6 +718,7 @@ export class ScionPageTerminal extends LitElement { } this.fitAddon = null; this.clipboardAddon = null; + this.wasConnected = false; } /** @@ -809,7 +845,14 @@ export class ScionPageTerminal extends LitElement { ` : ''} -
+
+
+ ${!this.connected && this.wasConnected + ? html`
+ DISCONNECTED +
` + : ''} +
`; } } diff --git a/web/src/components/shared/resource-import.ts b/web/src/components/shared/resource-import.ts index 0496ad516..4a979ef6c 100644 --- a/web/src/components/shared/resource-import.ts +++ b/web/src/components/shared/resource-import.ts @@ -323,8 +323,8 @@ export class ScionResourceImport extends LitElement { if (summary.failed.length > 0) msg += ` ${summary.failed.length} failed.`; this.success = msg; this.dispatchEvent( - new CustomEvent('resource-imported', { - detail: { count }, + new CustomEvent('resource-changed', { + detail: { action: 'imported', kind: this.kind, count }, bubbles: true, composed: true, }) diff --git a/web/src/components/shared/resource-list.ts b/web/src/components/shared/resource-list.ts index 516511cfb..5dbf5f7f7 100644 --- a/web/src/components/shared/resource-list.ts +++ b/web/src/components/shared/resource-list.ts @@ -26,10 +26,10 @@ * e.g. template import) are rendered by the host page around this list. */ -import { LitElement, html, css } from 'lit'; +import { LitElement, html, css, nothing } from 'lit'; import { customElement, property, state } from 'lit/decorators.js'; -import { apiFetch } from '../../client/api.js'; +import { apiFetch, extractApiError } from '../../client/api.js'; export type ResourceKind = 'template' | 'harness-config'; @@ -63,10 +63,34 @@ export class ScionResourceList extends LitElement { @property({ type: String }) detailBasePath = ''; + /** Whether to show the Clone action on each row. */ + @property({ type: Boolean }) + canClone = false; + + /** Whether to show the Delete action on each row. */ + @property({ type: Boolean }) + canDelete = false; + + /** When true, show a "Clone from Global" button above the list. */ + @property({ type: Boolean }) + cloneFromGlobal = false; + @state() private items: ResourceItem[] = []; @state() private loading = true; @state() private error: string | null = null; + @state() private cloneTarget: ResourceItem | null = null; + @state() private deleteTarget: ResourceItem | null = null; + @state() private actionInProgress = false; + @state() private actionError = ''; + @state() private cloneName = ''; + @state() private deleteFiles = true; + + @state() private globalPickerOpen = false; + @state() private globalItems: ResourceItem[] = []; + @state() private globalLoading = false; + @state() private globalError = ''; + static override styles = css` :host { display: block; @@ -78,23 +102,31 @@ export class ScionResourceList extends LitElement { gap: 0.5rem; } - .resource-item { + .resource-row { display: flex; align-items: center; - gap: 0.75rem; - padding: 0.75rem 1rem; + gap: 0; background: var(--scion-bg-subtle, #f8fafc); border: 1px solid var(--scion-border, #e2e8f0); border-radius: var(--scion-radius, 0.5rem); - text-decoration: none; - color: inherit; - cursor: pointer; } - .resource-item:hover { + .resource-row:hover { border-color: var(--scion-primary, #3b82f6); } + .resource-item { + display: flex; + align-items: center; + gap: 0.75rem; + padding: 0.75rem 1rem; + flex: 1; + min-width: 0; + text-decoration: none; + color: inherit; + cursor: pointer; + } + .resource-item > sl-icon { color: var(--scion-primary, #3b82f6); font-size: 1.125rem; @@ -128,6 +160,15 @@ export class ScionResourceList extends LitElement { white-space: nowrap; } + .row-actions { + flex-shrink: 0; + padding-right: 0.5rem; + } + + .menu-item-danger::part(base) { + color: var(--sl-color-danger-600, #dc2626); + } + .empty { text-align: center; padding: 2rem 1rem; @@ -148,6 +189,72 @@ export class ScionResourceList extends LitElement { background: var(--sl-color-danger-50, #fef2f2); border-radius: var(--scion-radius, 0.5rem); } + + .dialog-error { + color: var(--sl-color-danger-600, #dc2626); + font-size: 0.8125rem; + margin-top: 0.5rem; + } + + .dialog-warning { + display: flex; + align-items: center; + gap: 0.5rem; + font-size: 0.8125rem; + color: var(--sl-color-danger-600, #dc2626); + margin-top: 0.75rem; + } + + .clone-global-btn { + margin-bottom: 0.75rem; + } + + .global-picker-list { + display: flex; + flex-direction: column; + gap: 0.25rem; + max-height: 400px; + overflow-y: auto; + } + + .global-picker-item { + display: flex; + align-items: center; + gap: 0.75rem; + padding: 0.625rem 0.75rem; + border: 1px solid var(--scion-border, #e2e8f0); + border-radius: var(--scion-radius, 0.5rem); + cursor: pointer; + background: var(--scion-surface, #ffffff); + } + + .global-picker-item:hover { + border-color: var(--scion-primary, #3b82f6); + background: var(--scion-bg-subtle, #f8fafc); + } + + .global-picker-item sl-icon { + color: var(--scion-primary, #3b82f6); + font-size: 1rem; + flex-shrink: 0; + } + + .global-picker-info { + flex: 1; + min-width: 0; + } + + .global-picker-name { + font-weight: 600; + font-size: 0.8125rem; + color: var(--scion-text, #1e293b); + } + + .global-picker-desc { + font-size: 0.75rem; + color: var(--scion-text-muted, #64748b); + margin-top: 0.125rem; + } `; override connectedCallback(): void { @@ -173,6 +280,10 @@ export class ScionResourceList extends LitElement { return this.kind === 'template' ? 'file-earmark-code' : 'sliders'; } + private get kindLabel(): string { + return this.kind === 'template' ? 'template' : 'harness config'; + } + /** Public method to refresh the list. */ async load(): Promise { this.loading = true; @@ -200,6 +311,148 @@ export class ScionResourceList extends LitElement { } } + private emitChanged(action: string, id: string, sourceId?: string) { + this.dispatchEvent( + new CustomEvent('resource-changed', { + detail: { action, kind: this.kind, id, sourceId }, + bubbles: true, + composed: true, + }) + ); + } + + // ── Delete ────────────────────────────────────────────────────────── + + private openDeleteDialog(item: ResourceItem) { + this.deleteTarget = item; + this.deleteFiles = true; + this.actionError = ''; + this.actionInProgress = false; + } + + private closeDeleteDialog() { + this.deleteTarget = null; + this.actionError = ''; + } + + private async confirmDelete(): Promise { + if (!this.deleteTarget) return; + this.actionInProgress = true; + this.actionError = ''; + try { + const params = new URLSearchParams({ deleteFiles: String(this.deleteFiles) }); + const response = await apiFetch( + `/api/v1/${this.apiResource}/${this.deleteTarget.id}?${params.toString()}`, + { method: 'DELETE' } + ); + if (!response.ok && response.status !== 204) { + throw new Error( + await extractApiError(response, `Failed to delete: HTTP ${response.status}`) + ); + } + const deletedId = this.deleteTarget.id; + this.closeDeleteDialog(); + await this.load(); + this.emitChanged('deleted', deletedId); + } catch (err) { + this.actionError = err instanceof Error ? err.message : 'Delete failed'; + } finally { + this.actionInProgress = false; + } + } + + // ── Clone ─────────────────────────────────────────────────────────── + + private openCloneDialog(item: ResourceItem) { + this.cloneTarget = item; + this.cloneName = `${item.name}-copy`; + this.actionError = ''; + this.actionInProgress = false; + } + + private closeCloneDialog() { + this.cloneTarget = null; + this.actionError = ''; + } + + private async confirmClone(): Promise { + if (!this.cloneTarget) return; + this.actionInProgress = true; + this.actionError = ''; + try { + const body: Record = { name: this.cloneName }; + if (this.scope) body.scope = this.scope; + if (this.scope === 'project' && this.scopeId) body.scopeId = this.scopeId; + + const response = await apiFetch( + `/api/v1/${this.apiResource}/${this.cloneTarget.id}/clone`, + { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(body), + } + ); + if (response.status === 409) { + this.actionError = 'A resource with this slug already exists. Choose a different name.'; + this.actionInProgress = false; + return; + } + if (!response.ok) { + throw new Error( + await extractApiError(response, `Failed to clone: HTTP ${response.status}`) + ); + } + const created = (await response.json()) as { id?: string }; + const sourceId = this.cloneTarget.id; + this.closeCloneDialog(); + await this.load(); + this.emitChanged('cloned', created.id ?? '', sourceId); + } catch (err) { + this.actionError = err instanceof Error ? err.message : 'Clone failed'; + } finally { + this.actionInProgress = false; + } + } + + // ── Clone from global ────────────────────────────────────────────── + + private async openGlobalPicker(): Promise { + this.globalPickerOpen = true; + this.globalError = ''; + this.globalLoading = true; + this.globalItems = []; + try { + const params = new URLSearchParams({ status: 'active', scope: 'global', limit: '100' }); + const response = await apiFetch(`/api/v1/${this.apiResource}?${params.toString()}`); + if (!response.ok) { + throw new Error(`HTTP ${response.status}`); + } + const data = (await response.json()) as Record; + const list = this.kind === 'template' ? data.templates : data.harnessConfigs; + this.globalItems = Array.isArray(list) ? list : []; + } catch (err) { + this.globalError = err instanceof Error ? err.message : 'Failed to load global resources'; + } finally { + this.globalLoading = false; + } + } + + private closeGlobalPicker() { + this.globalPickerOpen = false; + this.globalItems = []; + this.globalError = ''; + } + + private selectGlobalItem(item: ResourceItem) { + this.closeGlobalPicker(); + this.cloneTarget = item; + this.cloneName = `${item.name}-copy`; + this.actionError = ''; + this.actionInProgress = false; + } + + // ── Render ───────────────────────────────────────────────────────── + override render() { if (this.loading) { return html`
`; @@ -207,29 +460,91 @@ export class ScionResourceList extends LitElement { if (this.error) { return html`
${this.error}
`; } - if (this.items.length === 0) { - return this.renderEmpty(); - } + + const hasActions = this.canClone || this.canDelete; return html` -
${this.items.map((item) => this.renderItem(item))}
+ ${this.cloneFromGlobal && this.canClone + ? html` +
+ this.openGlobalPicker()}> + + Clone from Global + +
+ ` + : nothing} + ${this.items.length === 0 + ? this.renderEmpty() + : html` +
+ ${this.items.map((item) => this.renderItem(item, hasActions))} +
+ `} + ${this.renderDeleteDialog()} ${this.renderCloneDialog()} + ${this.renderGlobalPickerDialog()} `; } - private renderItem(item: ResourceItem) { + private renderItem(item: ResourceItem, hasActions: boolean) { + if (!hasActions) { + return html` + + +
+
${item.displayName || item.name}
+ ${item.description ? html`
${item.description}
` : ''} +
+ ${item.harness ? html`${item.harness}` : ''} + +
+ `; + } + return html` - - -
-
${item.displayName || item.name}
- ${item.description ? html`
${item.description}
` : ''} +
+ + +
+
${item.displayName || item.name}
+ ${item.description ? html`
${item.description}
` : ''} +
+ ${item.harness ? html`${item.harness}` : ''} + +
+
+ + + + + + ${this.canClone + ? html` + this.openCloneDialog(item)}> + + Clone + + ` + : nothing} + ${this.canClone && this.canDelete ? html`` : nothing} + ${this.canDelete + ? html` + this.openDeleteDialog(item)}> + + Delete + + ` + : nothing} + +
- ${item.harness ? html`${item.harness}` : ''} - - +
`; } @@ -242,6 +557,154 @@ export class ScionResourceList extends LitElement {
`; } + + // ── Dialogs ──────────────────────────────────────────────────────── + + private renderDeleteDialog() { + if (!this.deleteTarget) return nothing; + return html` + { + if (this.actionInProgress) e.preventDefault(); + else this.closeDeleteDialog(); + }} + > +

+ Are you sure you want to delete + ${this.deleteTarget.displayName || this.deleteTarget.name}? +

+ { + this.deleteFiles = (e.target as HTMLInputElement).checked; + }} + > + Also delete stored files + +
+ + This action cannot be undone. +
+ ${this.actionError ? html`
${this.actionError}
` : nothing} +
+ this.closeDeleteDialog()} + > + Cancel + + this.confirmDelete()} + > + Delete + +
+
+ `; + } + + private renderCloneDialog() { + if (!this.cloneTarget) return nothing; + + const isFromGlobal = + this.cloneFromGlobal && this.scope === 'project' && !this.items.find((i) => i.id === this.cloneTarget!.id); + + return html` + { + if (this.actionInProgress) e.preventDefault(); + else this.closeCloneDialog(); + }} + > +

+ Clone ${this.cloneTarget.displayName || this.cloneTarget.name} + ${isFromGlobal ? html` from global into this project` : nothing}. +

+ { + this.cloneName = (e.target as HTMLInputElement).value; + }} + > + ${this.actionError ? html`
${this.actionError}
` : nothing} +
+ this.closeCloneDialog()} + > + Cancel + + this.confirmClone()} + > + Clone + +
+
+ `; + } + + private renderGlobalPickerDialog() { + if (!this.globalPickerOpen) return nothing; + const label = this.kind === 'template' ? 'templates' : 'harness configs'; + return html` + this.closeGlobalPicker()} + > +

Select a global ${this.kindLabel} to clone into this project.

+ ${this.globalLoading + ? html`
` + : this.globalError + ? html`
${this.globalError}
` + : this.globalItems.length === 0 + ? html`
No global ${label} available.
` + : html` +
+ ${this.globalItems.map( + (item) => html` +
this.selectGlobalItem(item)} + > + +
+
+ ${item.displayName || item.name} +
+ ${item.description + ? html`
${item.description}
` + : nothing} +
+ ${item.harness + ? html`${item.harness}` + : nothing} +
+ ` + )} +
+ `} +
+ `; + } } declare global { diff --git a/web/src/components/shared/resource-styles.ts b/web/src/components/shared/resource-styles.ts index 15cb77e50..f2e7a2323 100644 --- a/web/src/components/shared/resource-styles.ts +++ b/web/src/components/shared/resource-styles.ts @@ -640,10 +640,12 @@ export const listPageStyles = css` } .resource-table-container .task-cell { - max-width: 250px; + display: -webkit-box; + -webkit-line-clamp: 2; + -webkit-box-orient: vertical; overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; + max-width: 250px; + white-space: normal; color: var(--scion-text-muted, #64748b); font-size: 0.8125rem; } diff --git a/web/test-scripts/screenshot-debug.js b/web/test-scripts/screenshot-debug.js deleted file mode 100644 index 1c363b98f..000000000 --- a/web/test-scripts/screenshot-debug.js +++ /dev/null @@ -1,86 +0,0 @@ -/** - * Copyright 2026 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -/** - * Debug Screenshot Tool - * - * Takes a screenshot of a URL while capturing console logs, network errors, - * and response statuses. Useful for diagnosing why a page is blank or - * misbehaving in a headless environment. - * - * Usage: - * node screenshot-debug.js [url] [output-path] - * - * Examples: - * node screenshot-debug.js http://localhost:8080/ /tmp/debug.png - * node screenshot-debug.js http://localhost:8080/projects/abc123 /tmp/project.png - * - * Prerequisites: - * - Playwright + Chromium: cd /tmp && npm install playwright - */ -const { chromium } = require('playwright'); - -const CHROMIUM_PATH = process.env.CHROMIUM || '/usr/bin/chromium'; - -async function debug(url, outputPath) { - const browser = await chromium.launch({ - executablePath: CHROMIUM_PATH, - args: ['--no-sandbox', '--disable-setuid-sandbox'], - }); - const context = await browser.newContext({ viewport: { width: 1280, height: 800 } }); - const page = await context.newPage(); - - // Capture console messages - page.on('console', (msg) => console.log(`[CONSOLE ${msg.type()}] ${msg.text()}`)); - page.on('pageerror', (err) => console.log(`[PAGE ERROR] ${err.message}`)); - - // Capture network failures - page.on('requestfailed', (req) => - console.log(`[NET FAIL] ${req.url()} - ${req.failure()?.errorText}`) - ); - - // Capture responses of interest (errors, assets, API, SSE) - page.on('response', (resp) => { - if ( - resp.status() >= 400 || - resp.url().includes('.js') || - resp.url().includes('.css') || - resp.url().includes('/events') || - resp.url().includes('/api/') - ) { - console.log(`[RESPONSE] ${resp.status()} ${resp.url()}`); - } - }); - - await page - .goto(url, { waitUntil: 'networkidle', timeout: 15000 }) - .catch((e) => console.log(`[NAV ERROR] ${e.message}`)); - await page.waitForTimeout(3000); - - // Print page HTML summary - const html = await page.content(); - console.log(`\n[HTML length] ${html.length}`); - console.log(`[HTML snippet] ${html.substring(0, 500)}`); - - await page.screenshot({ path: outputPath, fullPage: false }); - console.log(`\nScreenshot saved to ${outputPath}`); - - await browser.close(); -} - -const url = process.argv[2] || 'http://localhost:8080'; -const output = process.argv[3] || '/tmp/screenshot-debug.png'; -debug(url, output);