Documentation
¶
Overview ¶
Package environments implements named environments — a subset of a project's services (e.g. staging/production) — layered on top of w1/m31's projects feature (internal/projects). An environment is a project-scoped label; services opt-in by being assigned to one, which also assigns them to the environment's project. Three surfaces: REST, GraphQL, MCP — mirroring internal/projects' own shape so the two grouping features read as one consistent system rather than two independently-designed ones.
Index ¶
- Constants
- Variables
- func EnvironmentOccupied(v EnvironmentView) bool
- func ExitGuards(s *Service) core.ExitGuardLoader
- func NewCreateResolver(service *Service) core.EnvironmentResolver
- func NotEmptyError(kind, id string) error
- type CreateEnvironmentRequest
- type DatabaseIndex
- type EnvGroupIndex
- type EnvironmentPatch
- type EnvironmentStore
- type EnvironmentView
- type KeyValueIndex
- type ProjectMemberClearer
- func (c *ProjectMemberClearer) ClearMembersForProject(ctx context.Context, projectID string) error
- func (c *ProjectMemberClearer) ClearServiceEnvironmentLayer(ctx context.Context, serviceNames []string) error
- func (c *ProjectMemberClearer) EnvironmentsOccupied(ctx context.Context, projectID string) (bool, error)
- type Service
- func (s *Service) ChangeResources(ctx context.Context, id string, resourceIDs []string, add bool) (EnvironmentView, error)
- func (s *Service) Create(ctx context.Context, projectID, name string) (EnvironmentView, error)
- func (s *Service) CreateWithACL(ctx context.Context, req CreateEnvironmentRequest) (EnvironmentView, error)
- func (s *Service) Delete(ctx context.Context, id string) error
- func (s *Service) DeleteIfEmpty(ctx context.Context, id string) error
- func (s *Service) Get(ctx context.Context, id string) (EnvironmentView, error)
- func (s *Service) GraphQLMutation() graphql.Fields
- func (s *Service) GraphQLQuery() graphql.Fields
- func (s *Service) List(ctx context.Context, projectID string) ([]EnvironmentView, error)
- func (s *Service) ListWorkspace(ctx context.Context, workspaceID string) ([]EnvironmentView, error)
- func (s *Service) OwnerWorkspace(ctx context.Context, id string) (string, error)
- func (s *Service) RegisterMCP(srv *mcp.Server)
- func (s *Service) RegisterREST(mux *http.ServeMux)
- func (s *Service) Rename(ctx context.Context, id, name string) (EnvironmentView, error)
- func (s *Service) SetACL(ctx context.Context, id, protectedStatus string, networkIsolationEnabled bool, ...) (EnvironmentView, error)
- func (s *Service) SetDatabases(ctx context.Context, id string, databaseIDs []string) (EnvironmentView, error)
- func (s *Service) SetEnvGroups(ctx context.Context, id string, envGroupIDs []string) (EnvironmentView, error)
- func (s *Service) SetKeyValues(ctx context.Context, id string, keyValueIDs []string) (EnvironmentView, error)
- func (s *Service) SetServices(ctx context.Context, id string, serviceIDs []string) (EnvironmentView, error)
- func (s *Service) Update(ctx context.Context, id string, patch EnvironmentPatch) (EnvironmentView, error)
Constants ¶
const ( ProtectedStatusProtected = core.ProtectedStatusProtected ProtectedStatusUnprotected = core.ProtectedStatusUnprotected )
Render's protectedStatus enum (w6/m19) — aliased from core (the shared leaf both this feature and apps.Service's protection guard, apps/protection.go, import) rather than defined here, so the two never drift and neither feature needs to import the other's package for two string constants. Unprotected is the default; on protected, the guard blocks unguarded delete/suspend/direct-deploy-override verbs on member Apps unless the caller echoes back apps.ProtectedConfirmation.
Variables ¶
ErrEnvironmentsUnavailable is returned when the control-plane store is not wired (BEX_CP_DB_URI unset). Environments have no CR-only equivalent.
Functions ¶
func EnvironmentOccupied ¶
func EnvironmentOccupied(v EnvironmentView) bool
EnvironmentOccupied reports whether v holds any resource.
func ExitGuards ¶
func ExitGuards(s *Service) core.ExitGuardLoader
ExitGuards is s's exit check as the loader the projects, env groups and Blueprint writers are wired with (w4/m232).
func NewCreateResolver ¶
func NewCreateResolver(service *Service) core.EnvironmentResolver
NewCreateResolver returns the shared resource-create assignment seam. Its method is kept off Service because it is plumbing after the parent create verb's authorization, not an independently callable API verb.
func NotEmptyError ¶
NotEmptyError is the 409 REST answers when a delete would orphan resources, as Render's API does; the dashboard and GraphQL keep their confirmed detach-and-delete flow.
Types ¶
type CreateEnvironmentRequest ¶
type CreateEnvironmentRequest struct {
Name string `json:"name"`
ProjectID string `json:"projectId"`
ProtectedStatus string `json:"protectedStatus,omitempty"`
NetworkIsolationEnabled bool `json:"networkIsolationEnabled,omitempty"`
IPAllowList []core.IPAllowListEntry `json:"ipAllowList,omitempty"`
}
CreateEnvironmentRequest is the neutral create input shared by REST, GraphQL, and MCP. The ACL triple is optional as a unit: omitted fields use the store defaults (unprotected, unisolated, open).
type DatabaseIndex ¶
type DatabaseIndex interface {
ListPostgres(ctx context.Context, ownerID string) ([]postgres.PostgresView, error)
SetEnvironmentID(ctx context.Context, name, expectedEnvironmentID, environmentID string) error
ClearEnvironmentID(ctx context.Context, name, expectedEnvironmentID string) error
// SetProjectID joins a newly-assigned Database to the environment's
// project (mirroring SetEnvironmentServices' apps.project_id stamp — see
// SetDatabases).
SetProjectID(ctx context.Context, name, expectedEnvironmentID, projectID string) error
SetEnvironmentIPAllowList(ctx context.Context, name, expectedEnvironmentID string, cidrs []string) error
// SetEnvironmentIsolation stamps or clears core.LabelNetworkIsolation (w8/m136).
SetEnvironmentIsolation(ctx context.Context, name, expectedEnvironmentID string, isolated bool) error
}
DatabaseIndex is the narrow contract environments needs from managed Postgres to group Database CRs into an environment (w6/m20 extension) — mirroring internal/projects.DatabaseIndex. Unlike services, Databases have no control-plane row, so membership is a label (core.LabelEnvironment), read via ListPostgres and written via SetEnvironmentID. SetEnvironmentIPAllowList (w4/m28) is the ACL fan-out target: it projects the ENVIRONMENT rule layer onto a member, never touching the Database's own ipAllowList (the pre-m28 fan-out full-replaced it — that clobber is retired). *postgres.Service satisfies this structurally.
type EnvGroupIndex ¶
type EnvGroupIndex interface {
ListEnvironmentMemberships(ctx context.Context, ownerID string) ([]envgroups.EnvironmentMembership, error)
SetEnvironmentID(ctx context.Context, id, environmentID string) error
// DetachEnvironment unscopes a group still in expectedEnvironmentID during
// that environment's teardown (w4/m187).
DetachEnvironment(ctx context.Context, id, expectedEnvironmentID string) error
}
EnvGroupIndex is the narrow cross-feature contract environments needs to expose Render's Environment.envGroupIds membership. Environment membership lives in each env group's KV metadata, so reads list the owning workspace's groups and writes update only groups proven to belong to that workspace. *envgroups.Service satisfies it structurally.
type EnvironmentPatch ¶
type EnvironmentPatch struct {
Name *string
ProtectedStatus *string
NetworkIsolationEnabled *bool
IPAllowList *[]core.IPAllowListEntry
}
EnvironmentPatch is the neutral partial-update input shared by REST PATCH, GraphQL updateEnvironment, and MCP update_environment (w4/m30) — pointer fields distinguish "absent" (nil, leave unchanged) from a zero value (networkIsolationEnabled: false must be appliable), mirroring postgres.PostgresPatch. The ACL fields merge into the environment's current triple rather than replacing it wholesale — SetACL/applyACL remain the full-replace verb the bex-native /acl route uses.
type EnvironmentStore ¶
type EnvironmentStore interface {
GetProject(ctx context.Context, id string) (store.Project, error)
CreateEnvironment(ctx context.Context, projectID, tenantID, name string) (store.Environment, error)
GetEnvironment(ctx context.Context, id string) (store.Environment, error)
ListEnvironments(ctx context.Context, projectID string) ([]store.Environment, error)
ListWorkspaceEnvironments(ctx context.Context, tenantID string) ([]store.Environment, error)
RenameEnvironment(ctx context.Context, id, name string) error
DeleteEnvironment(ctx context.Context, id string) error
SetEnvironmentServices(ctx context.Context, environmentID, projectID, tenantID string, serviceNames []string, exit core.EnvironmentExitGuard) ([]core.ServicePlacementChange, error)
ListEnvironmentServices(ctx context.Context, environmentID, projectID string) ([]string, error)
ListWorkspaceEnvironmentServices(ctx context.Context, tenantID string) (map[string][]string, error)
// SetEnvironmentACL replaces the protected-environment ACL triple (w6/m19).
SetEnvironmentACL(ctx context.Context, id, protectedStatus string, networkIsolationEnabled bool, ipAllowList []core.IPAllowListEntry) error
}
EnvironmentStore is the narrow persistence contract this feature needs. *store.PGStore satisfies it structurally — GetProject is the one method borrowed from the sibling projects feature's own store surface (needed to resolve a project id to its tenant for authorization and to create/list environments under it), not a new capability.
type EnvironmentView ¶
type EnvironmentView struct {
ID string `json:"id"`
ProjectID string `json:"projectId"`
Name string `json:"name"`
OwnerID string `json:"ownerId"`
CreatedAt time.Time `json:"createdAt"`
ServiceIDs []string `json:"serviceIds"`
DatabaseIDs []string `json:"databaseIds"`
KeyValueIDs []string `json:"keyValueIds"`
EnvGroupIDs []string `json:"envGroupIds"`
ProtectedStatus string `json:"protectedStatus"`
NetworkIsolationEnabled bool `json:"networkIsolationEnabled"`
// IPAllowList is Render's [{cidrBlock, description}] objects — descriptions
// persist in the store row since w4/m24.
IPAllowList []core.IPAllowListEntry `json:"ipAllowList"`
// IPAllowListProxiedDomains is a bex extension (w1/m171): the member
// services' Cloudflare-proxied custom domains while IPAllowList restricts
// traffic. On those hosts the environment layer matches Cloudflare's edge
// address, not the client's. Omitted when there are none.
IPAllowListProxiedDomains []string `json:"ipAllowListProxiedDomains,omitempty"`
}
EnvironmentView is the API shape for an environment — all three surfaces return this.
type KeyValueIndex ¶
type KeyValueIndex interface {
ListKeyValues(ctx context.Context, ownerID string) ([]keyvalue.KeyValueView, error)
SetEnvironmentID(ctx context.Context, name, expectedEnvironmentID, environmentID string) error
ClearEnvironmentID(ctx context.Context, name, expectedEnvironmentID string) error
SetProjectID(ctx context.Context, name, expectedEnvironmentID, projectID string) error
SetEnvironmentIPAllowList(ctx context.Context, name, expectedEnvironmentID string, cidrs []string) error
// SetEnvironmentIsolation stamps or clears core.LabelNetworkIsolation (w8/m136).
SetEnvironmentIsolation(ctx context.Context, name, expectedEnvironmentID string, isolated bool) error
}
KeyValueIndex is DatabaseIndex's KeyValue-CR counterpart. *keyvalue.Service satisfies this structurally.
type ProjectMemberClearer ¶
type ProjectMemberClearer struct{ *Service }
ProjectMemberClearer adapts Service to the projects feature's EnvironmentIndex seam (satisfied structurally — this package never imports projects), mirroring apps.WorkspacePurger's shape: an internal system operation invoked after the calling verb (projects.Service.SetServices/ Delete) has already authorized, with no separate caller identity of its own to check against — never call s.Authorize/AuthorizeOn in these methods (w4/m32).
func (*ProjectMemberClearer) ClearMembersForProject ¶
func (c *ProjectMemberClearer) ClearMembersForProject(ctx context.Context, projectID string) error
ClearMembersForProject clears the environment-projected layer from every member (App, Database, KeyValue, environment group) of every environment under projectID — the seam projects.Service.Delete uses BEFORE the project row is deleted, while the environment rows this needs to enumerate still exist. The DB cascade (environments.project_id ON DELETE CASCADE, then apps.environment_id ON DELETE SET NULL transitively) already leaves the store rows consistent on its own; only the k8s CRs need this explicit fan-out.
func (*ProjectMemberClearer) ClearServiceEnvironmentLayer ¶
func (c *ProjectMemberClearer) ClearServiceEnvironmentLayer(ctx context.Context, serviceNames []string) error
ClearServiceEnvironmentLayer clears the environment-projected fields (core.LabelNetworkIsolation, spec.environmentIPAllowList) on every named App CR, with no environment row involved — the seam projects.Service uses when a service leaving its project also carries a stale apps.environment_id: the store NULLs the column, but nothing else re-syncs the already-existing k8s CR after that raw write, and environments' own verbs never see the departure (the app already dropped out of ListEnvironmentServices by the time this runs).
func (*ProjectMemberClearer) EnvironmentsOccupied ¶
func (c *ProjectMemberClearer) EnvironmentsOccupied(ctx context.Context, projectID string) (bool, error)
EnvironmentsOccupied reports whether any environment under projectID holds a resource — Render's precondition for deleting a project (w8/188). It reads membership only; the caller (the REST delete) has already authorized the project.
type Service ¶
type Service struct {
*core.Base
Store EnvironmentStore
Databases DatabaseIndex
KeyValues KeyValueIndex
EnvGroups EnvGroupIndex
// Services deletes an environment's services when the environment is
// deleted, as Render's dashboard does (w4/m264). nil keeps the former
// detach-only behavior (test fakes).
Services core.ServiceDeleter
// PostgresDeleter and KeyValueDeleter delete the environment's datastores
// with it, as Render's dashboard deletes all its services (w6/m290). nil
// keeps the detach-only behavior (test fakes). Env groups are always
// detached: Render lists them apart from services.
PostgresDeleter core.PostgresDeleter
KeyValueDeleter core.KeyValueDeleter
// MaxGroupings is the workspace's durable grouping quota
// (BEX_MAX_BLUEPRINT_GROUPINGS, default 1000) — the same abuse bound the
// Blueprint apply loop enforces, applied to DIRECT environment creates too
// (codex-security round 12, finding 5). 0 disables, matching the Blueprint
// path.
MaxGroupings int
// ProxiedHosts detects member services' custom domains that resolve into
// Cloudflare's edge, where the environment's inbound allowlist layer sees
// Cloudflare instead of the client (w1/m171). nil detects nothing.
ProxiedHosts *core.ProxiedHostDetector
}
Service is the environments feature service. Store nil => ErrEnvironmentsUnavailable. Databases/KeyValues nil => the corresponding *Ids field resolves empty, SetDatabases/SetKeyValues report ErrEnvironmentsUnavailable, and SetACL's ipAllowList change is not propagated to any Database/KeyValue (matching internal/projects' own nil-degrades pattern) — everything else about environments still works.
func (*Service) ChangeResources ¶
func (s *Service) ChangeResources(ctx context.Context, id string, resourceIDs []string, add bool) (EnvironmentView, error)
ChangeResources adds (or removes) the listed resources to (from) the environment, leaving every other member as it is. A malformed id refuses the whole request before anything changes; the kinds then apply in turn, and a failure restores the kinds already changed so the request is all-or-nothing as far as the individual membership verbs allow.
func (*Service) Create ¶
Create creates a new environment under the named project, authorized once in the project's own workspace (scopeProject).
func (*Service) CreateWithACL ¶
func (s *Service) CreateWithACL(ctx context.Context, req CreateEnvironmentRequest) (EnvironmentView, error)
CreateWithACL creates an environment and its optional protected-environment ACL in one cross-surface verb, authorized in the project's own workspace as Create is. Validation happens before the row is written, so malformed status/CIDR input cannot leave an orphan environment.
func (*Service) Delete ¶
Delete deletes the environment's services and datastores (Render: "Deleting an environment deletes all of its associated services", w4/m264, w6/m290), then clears the remaining members' environment membership and inherited rules before removing the environment. Env groups are detached, not deleted.
func (*Service) DeleteIfEmpty ¶
DeleteIfEmpty is Render's delete (w8/188): the environment must hold no resources, otherwise the caller gets a 409 and nothing changes. Authorization is Delete's, ahead of the occupancy answer.
func (*Service) GraphQLMutation ¶
GraphQLMutation contributes the environment write verbs to the root Mutation.
func (*Service) GraphQLQuery ¶
GraphQLQuery contributes the environments reads to the root Query.
func (*Service) List ¶
List returns every environment under a project, each with its current service/database/key-value membership.
func (*Service) ListWorkspace ¶
ListWorkspace returns every Environment in an explicitly authorized workspace. Both rows and service membership are fetched in batches, while the label-backed resource scans remain one-per-kind for the whole workspace. This is the backend for the dashboard's one-shot scope index.
func (*Service) OwnerWorkspace ¶
OwnerWorkspace returns the workspace that owns environment id, under exactly the scoping and authorization Get applies. It is for a caller that only needs to place another resource in the environment: Get also composes the full view, which lists every database, Key Value and env group of the workspace to report membership the caller would discard (w4/m206).
func (*Service) RegisterMCP ¶
RegisterMCP adds the environment management tools to the shared MCP server.
func (*Service) RegisterREST ¶
RegisterREST mounts the environment CRUD endpoints.
func (*Service) SetACL ¶
func (s *Service) SetACL(ctx context.Context, id, protectedStatus string, networkIsolationEnabled bool, ipAllowList []core.IPAllowListEntry) (EnvironmentView, error)
SetACL replaces an environment's full protected-environment ACL triple (w6/m19) — full-replace, not a merge, matching every other Set verb in this codebase (SetServices, postgres/keyvalue's SetIPAllowList): a caller changing one field sends the current value of the other two.
- protectedStatus arms/disarms apps.Service's destructive-verb guard on every member App (checked at guard time via the store, not projected onto any CR — see apps/protection.go).
- networkIsolationEnabled is projected onto every member App CR as core.LabelNetworkIsolation (present) or its absence — the operator's signal to scope that App's NetworkPolicy to same-environment peers (t004).
- ipAllowList is fanned out to every Database/KeyValue whose core.LabelEnvironment (w6/m20) names this environment (propagateEnvironmentLayer).
func (*Service) SetDatabases ¶
func (s *Service) SetDatabases(ctx context.Context, id string, databaseIDs []string) (EnvironmentView, error)
SetDatabases replaces the full list of managed Postgres databases in an environment. databaseIDs are immutable Database CR names (normally dpg-...), not store rows — unlike SetServices' apps.environment_id column, a Database's membership is purely the core.LabelEnvironment label (w6/m20 extension), so this diffs the current label state against the wanted set and calls SetEnvironmentID per change instead of a single bulk store UPDATE (mirroring internal/projects.Service.SetDatabases). A newly-assigned Database also joins the environment's project (SetProjectID), mirroring SetEnvironmentServices' apps.project_id stamp — Render's model treats "in an environment" as "in that project." Removing a Database from the environment does NOT clear its project membership, matching store.SetEnvironmentServices' own asymmetry for services.
func (*Service) SetEnvGroups ¶
func (s *Service) SetEnvGroups(ctx context.Context, id string, envGroupIDs []string) (EnvironmentView, error)
SetEnvGroups replaces the full list of environment groups assigned to an Environment. Every requested group must belong to the Environment's own workspace; an unknown or foreign id is refused instead of silently ignored. Groups already assigned to another Environment in the same workspace move in one update, while groups omitted from this Environment are unassigned.
func (*Service) SetKeyValues ¶
func (s *Service) SetKeyValues(ctx context.Context, id string, keyValueIDs []string) (EnvironmentView, error)
SetKeyValues is SetDatabases' KeyValue-CR counterpart.
func (*Service) SetServices ¶
func (s *Service) SetServices(ctx context.Context, id string, serviceIDs []string) (EnvironmentView, error)
SetServices replaces the full list of services in an environment. serviceIDs are the public ids shown by list_services (normally srv-...), matching internal/projects.Service.SetServices exactly. Legacy service names remain accepted by the store during the stable-id transition. The assigned services also join the environment's project (see store.SetEnvironmentServices). core.LabelNetworkIsolation is synced on every affected App CR: cleared on services leaving, set (if the environment's networkIsolationEnabled) on services now in it — the operator's signal for environment-scoped NetworkPolicy (t004).
func (*Service) Update ¶
func (s *Service) Update(ctx context.Context, id string, patch EnvironmentPatch) (EnvironmentView, error)
Update applies a partial update (Render's PATCH /environments/{id} semantics — only fields set in patch change; everything else is left alone) in one authorize + one fetch, replacing the REST adapter's former sequence of independently-authorized Rename/Get/SetACL calls. A rename and an ACL merge in the same patch apply together against the one fetched row; the ACL half reuses applyACL (SetACL's post-authorization body) so the merge-then-full-replace semantics stay identical to the standalone /acl route.