environments

package
v0.0.0-...-c6c7719 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Nov 14, 2026 License: Apache-2.0 Imports: 20 Imported by: 0

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

View Source
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

View Source
var ErrEnvironmentsUnavailable = core.Unavailable("environments store not configured")

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

func NotEmptyError(kind, id string) error

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

func (s *Service) Create(ctx context.Context, projectID, name string) (EnvironmentView, error)

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

func (s *Service) Delete(ctx context.Context, id string) error

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

func (s *Service) DeleteIfEmpty(ctx context.Context, id string) error

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) Get

func (s *Service) Get(ctx context.Context, id string) (EnvironmentView, error)

Get returns a single environment by id.

func (*Service) GraphQLMutation

func (s *Service) GraphQLMutation() graphql.Fields

GraphQLMutation contributes the environment write verbs to the root Mutation.

func (*Service) GraphQLQuery

func (s *Service) GraphQLQuery() graphql.Fields

GraphQLQuery contributes the environments reads to the root Query.

func (*Service) List

func (s *Service) List(ctx context.Context, projectID string) ([]EnvironmentView, error)

List returns every environment under a project, each with its current service/database/key-value membership.

func (*Service) ListWorkspace

func (s *Service) ListWorkspace(ctx context.Context, workspaceID string) ([]EnvironmentView, error)

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

func (s *Service) OwnerWorkspace(ctx context.Context, id string) (string, error)

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

func (s *Service) RegisterMCP(srv *mcp.Server)

RegisterMCP adds the environment management tools to the shared MCP server.

func (*Service) RegisterREST

func (s *Service) RegisterREST(mux *http.ServeMux)

RegisterREST mounts the environment CRUD endpoints.

func (*Service) Rename

func (s *Service) Rename(ctx context.Context, id, name string) (EnvironmentView, error)

Rename renames an environment.

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.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL
⌂ 换网址