Files
creator-hub/internal/environment/environment.go
T

1079 lines
45 KiB
Go

package environment
import (
"context"
"crypto/rand"
"database/sql"
"encoding/hex"
"encoding/json"
"errors"
"net"
"regexp"
"strings"
"time"
"git.ipao.vip/rogee/creator-hub/internal/taskstate"
)
var exitIDPattern = regexp.MustCompile(`^[A-Za-z0-9][A-Za-z0-9._/-]{0,127}$`)
var nodeIDPattern = regexp.MustCompile(`^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$`)
type NetworkExit struct {
ID string `json:"id"`
Protocol string `json:"protocol"`
Host string `json:"host"`
Port int `json:"port"`
Username string `json:"username"`
Password string `json:"password"`
ExpectedPublicIP string `json:"expected_public_ip,omitempty"`
ExpectedRegion string `json:"expected_region,omitempty"`
ObservedPublicIP string `json:"observed_public_ip,omitempty"`
ObservedRegion string `json:"observed_region,omitempty"`
HealthStatus string `json:"health_status"`
LastCheckReason string `json:"last_check_reason,omitempty"`
Version int64 `json:"version"`
LastCheckedAt *time.Time `json:"last_checked_at,omitempty"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
// NetworkExitAccess is the internal runtime view of a persisted network exit.
type NetworkExitAccess struct {
NetworkExit
}
type ExitObservation struct {
PublicIP string
Region string
}
type EnvironmentContext struct {
Env
AccountID string `json:"account_id"`
AccountStatus string `json:"account_status"`
AuthorizationStatus string `json:"authorization_status"`
BindingID string `json:"binding_id"`
BindingVersion int64 `json:"binding_version"`
RuntimeCleanupPending bool `json:"runtime_cleanup_pending,omitempty"`
RuntimeCleanupBindingVersion int64 `json:"runtime_cleanup_binding_version,omitempty"`
RuntimeCleanupInstanceID string `json:"runtime_cleanup_instance_id,omitempty"`
RuntimeCleanupRuntimeID string `json:"runtime_cleanup_runtime_id,omitempty"`
RuntimeCleanupNetworkID string `json:"runtime_cleanup_network_id,omitempty"`
Exit NetworkExit `json:"network_exit"`
RuntimeInstanceID string `json:"runtime_instance_id,omitempty"`
RuntimeID string `json:"runtime_id,omitempty"`
RuntimeNetworkID string `json:"runtime_network_id,omitempty"`
RuntimeNodeID string `json:"runtime_node_id,omitempty"`
}
type RuntimeUseLease struct {
Token string `json:"token"`
RuntimeInstanceID string `json:"runtime_instance_id"`
OwnerID string `json:"owner_id"`
Purpose string `json:"purpose"`
TaskID string `json:"task_id,omitempty"`
LeaseUntil time.Time `json:"lease_until"`
}
type EnvironmentAction struct {
OperationID string
Action string
AccountID string
BrowserEnvAlias string
NetworkExitID string
RuntimeInstanceID string
BindingVersion int64
OldBrowserVersion string
NewBrowserVersion string
Outcome string
ReasonCode string
}
func (s *Store) CreateNetworkExit(ctx context.Context, exit NetworkExit) (NetworkExit, error) {
exit.ID = "exit-" + newHubID()
exit.Protocol, exit.Host = strings.ToLower(strings.TrimSpace(exit.Protocol)), strings.TrimSpace(exit.Host)
exit.ExpectedPublicIP, exit.ExpectedRegion = strings.TrimSpace(exit.ExpectedPublicIP), strings.TrimSpace(exit.ExpectedRegion)
if !validNetworkExit(exit) {
return NetworkExit{}, ErrInvalid
}
row := s.db.QueryRowContext(ctx, `
INSERT INTO network_exit (id, protocol, host, port, username, password, expected_public_ip, expected_region)
VALUES ($1, $2, $3, $4, $5, $6, NULLIF($7, '')::inet, $8)
RETURNING id`, exit.ID, exit.Protocol, exit.Host, exit.Port, exit.Username, exit.Password, exit.ExpectedPublicIP, exit.ExpectedRegion)
if err := row.Scan(&exit.ID); err != nil {
return NetworkExit{}, publicDatabaseError(err)
}
return s.GetNetworkExit(ctx, exit.ID)
}
func validNetworkExit(exit NetworkExit) bool {
if exit.Protocol != "http" && exit.Protocol != "https" && exit.Protocol != "socks4" && exit.Protocol != "socks5" {
return false
}
if !validExitHost(exit.Host) || exit.Port < 1 || exit.Port > 65535 || !validExitCredentials(exit.Username, exit.Password) {
return false
}
if exit.ExpectedPublicIP != "" && net.ParseIP(exit.ExpectedPublicIP) == nil {
return false
}
return validOptionalRegion(exit.ExpectedRegion)
}
func validExitCredentials(username, password string) bool {
if len(username) > 255 || len(password) > 255 || (username == "" && password != "") {
return false
}
for _, value := range username + password {
if value < 0x20 || value == 0x7f {
return false
}
}
return true
}
func validExitHost(host string) bool {
if host == "" || len(host) > 253 || strings.ContainsAny(host, "@/[]?# \t\r\n") {
return false
}
if net.ParseIP(host) != nil {
return true
}
if strings.HasPrefix(host, ".") || strings.HasSuffix(host, ".") || strings.Contains(host, "..") {
return false
}
for _, label := range strings.Split(host, ".") {
if len(label) > 63 || strings.HasPrefix(label, "-") || strings.HasSuffix(label, "-") {
return false
}
for _, character := range label {
if (character < 'a' || character > 'z') && (character < 'A' || character > 'Z') &&
(character < '0' || character > '9') && character != '-' {
return false
}
}
}
return true
}
func validOptionalRegion(region string) bool {
if len(region) > 64 {
return false
}
for _, character := range region {
if character < 0x20 || character == 0x7f {
return false
}
}
return true
}
func (s *Store) UpdateNetworkExit(ctx context.Context, id string, input NetworkExit) (NetworkExit, error) {
if !exitIDPattern.MatchString(id) {
return NetworkExit{}, ErrInvalid
}
input.Protocol, input.Host = strings.ToLower(strings.TrimSpace(input.Protocol)), strings.TrimSpace(input.Host)
input.ExpectedPublicIP, input.ExpectedRegion = strings.TrimSpace(input.ExpectedPublicIP), strings.TrimSpace(input.ExpectedRegion)
if !validNetworkExit(input) {
return NetworkExit{}, ErrInvalid
}
tx, err := s.db.BeginTx(ctx, nil)
if err != nil {
return NetworkExit{}, errors.New("begin network exit update")
}
defer tx.Rollback()
var active bool
if err := tx.QueryRowContext(ctx, `SELECT EXISTS (SELECT 1 FROM environment_binding binding JOIN runtime_instance runtime ON runtime.binding_id=binding.id WHERE binding.network_exit_id=$1 AND runtime.released_at IS NULL AND runtime.lease_until > now()) OR EXISTS (SELECT 1 FROM environment_binding binding JOIN operation_task task ON task.account_id=binding.account_id WHERE binding.network_exit_id=$1 AND task.state='executing')`, id).Scan(&active); err != nil {
return NetworkExit{}, errors.New("check network exit activity")
}
if active {
return NetworkExit{}, ErrConflict
}
if _, err := tx.ExecContext(ctx, `UPDATE network_exit SET protocol=$2,host=$3,port=$4,username=$5,password=$6,expected_public_ip=NULLIF($7,'')::inet,expected_region=$8,health_status=CASE WHEN health_status='disabled' THEN 'disabled' ELSE 'unchecked' END,last_check_reason='exit_configuration_changed',version=version+1,updated_at=now() WHERE id=$1`, id, input.Protocol, input.Host, input.Port, input.Username, input.Password, input.ExpectedPublicIP, input.ExpectedRegion); err != nil {
return NetworkExit{}, rowError(err)
}
if err := tx.Commit(); err != nil {
return NetworkExit{}, err
}
return s.GetNetworkExit(ctx, id)
}
func (s *Store) EnableNetworkExit(ctx context.Context, id string) (NetworkExit, error) {
if !exitIDPattern.MatchString(id) {
return NetworkExit{}, ErrInvalid
}
if _, err := s.db.ExecContext(ctx, `UPDATE network_exit SET health_status='unchecked',last_check_reason='exit_enabled',version=version+1,updated_at=now() WHERE id=$1 AND health_status='disabled'`, id); err != nil {
return NetworkExit{}, rowError(err)
}
return s.GetNetworkExit(ctx, id)
}
func (s *Store) DeleteNetworkExit(ctx context.Context, id string) error {
if !exitIDPattern.MatchString(id) {
return ErrInvalid
}
tx, err := s.db.BeginTx(ctx, nil)
if err != nil {
return errors.New("begin network exit delete")
}
defer tx.Rollback()
var used bool
if err := tx.QueryRowContext(ctx, `SELECT EXISTS (SELECT 1 FROM environment_binding WHERE network_exit_id=$1) OR EXISTS (SELECT 1 FROM environment_binding binding JOIN runtime_instance runtime ON runtime.binding_id=binding.id WHERE binding.network_exit_id=$1 AND runtime.released_at IS NULL AND runtime.lease_until > now())`, id).Scan(&used); err != nil {
return errors.New("check network exit bindings")
}
if used {
return ErrConflict
}
result, err := tx.ExecContext(ctx, `DELETE FROM network_exit WHERE id=$1`, id)
if err != nil {
return rowError(err)
}
count, err := result.RowsAffected()
if err != nil {
return err
}
if count != 1 {
return ErrNotFound
}
return tx.Commit()
}
func (s *Store) ListNetworkExits(ctx context.Context) ([]NetworkExit, error) {
rows, err := s.db.QueryContext(ctx, networkExitSelect+` ORDER BY network.created_at, network.id`)
if err != nil {
return nil, errors.New("read network exits")
}
defer rows.Close()
exits := []NetworkExit{}
for rows.Next() {
exit, err := scanNetworkExit(rows)
if err != nil {
return nil, err
}
exits = append(exits, exit)
}
return exits, rows.Err()
}
func (s *Store) GetNetworkExit(ctx context.Context, id string) (NetworkExit, error) {
if !exitIDPattern.MatchString(id) {
return NetworkExit{}, ErrInvalid
}
return scanNetworkExit(s.db.QueryRowContext(ctx, networkExitSelect+` WHERE network.id = $1`, id))
}
func (s *Store) GetNetworkExitAccess(ctx context.Context, id string) (NetworkExitAccess, error) {
exit, err := s.GetNetworkExit(ctx, id)
return NetworkExitAccess{NetworkExit: exit}, err
}
const networkExitSelect = `
SELECT network.id, network.protocol, network.host, network.port, network.username, network.password,
COALESCE(host(network.expected_public_ip), ''), network.expected_region,
COALESCE(host(network.observed_public_ip), ''), network.observed_region,
network.health_status, COALESCE(network.last_check_reason, ''), network.version, network.last_checked_at,
network.created_at, network.updated_at
FROM network_exit network`
type rowScanner interface{ Scan(...any) error }
func scanNetworkExit(row rowScanner) (NetworkExit, error) {
var exit NetworkExit
var checked sql.NullTime
if err := row.Scan(&exit.ID, &exit.Protocol, &exit.Host, &exit.Port, &exit.Username, &exit.Password,
&exit.ExpectedPublicIP, &exit.ExpectedRegion, &exit.ObservedPublicIP, &exit.ObservedRegion,
&exit.HealthStatus, &exit.LastCheckReason, &exit.Version, &checked, &exit.CreatedAt, &exit.UpdatedAt); err != nil {
return NetworkExit{}, rowError(err)
}
if checked.Valid {
exit.LastCheckedAt = &checked.Time
}
return exit, nil
}
// RecordNetworkExitCheck stores only observed identity and a stable reason code.
func (s *Store) RecordNetworkExitCheck(ctx context.Context, id string, observation ExitObservation, failureReason string) (NetworkExit, string, error) {
if !exitIDPattern.MatchString(id) || !validOptionalRegion(observation.Region) ||
(observation.PublicIP != "" && net.ParseIP(observation.PublicIP) == nil) || !validExitFailureReason(failureReason) {
return NetworkExit{}, "invalid_observation", ErrInvalid
}
tx, err := s.db.BeginTx(ctx, nil)
if err != nil {
return NetworkExit{}, "persistence_failed", errors.New("begin network exit check")
}
defer tx.Rollback()
var expectedIP, expectedRegion, oldIP, oldRegion, oldStatus string
var version int64
if err := tx.QueryRowContext(ctx, `
SELECT COALESCE(host(expected_public_ip), ''), expected_region,
COALESCE(host(observed_public_ip), ''), observed_region, health_status, version
FROM network_exit WHERE id = $1 FOR UPDATE`, id).
Scan(&expectedIP, &expectedRegion, &oldIP, &oldRegion, &oldStatus, &version); err != nil {
return NetworkExit{}, "persistence_failed", rowError(err)
}
if oldStatus == "disabled" {
return NetworkExit{}, "exit_disabled", ErrConflict
}
reason, status := strings.TrimSpace(failureReason), "unhealthy"
if reason == "" && expectedIP != "" && !net.ParseIP(expectedIP).Equal(net.ParseIP(observation.PublicIP)) {
reason = "exit_ip_drift"
}
if reason == "" && expectedRegion != "" && !strings.EqualFold(expectedRegion, observation.Region) {
reason = "exit_region_drift"
}
if reason == "" {
reason, status = "exit_healthy", "healthy"
}
changed := !sameIP(oldIP, observation.PublicIP) || !strings.EqualFold(oldRegion, observation.Region) || oldStatus != status
if changed {
version++
}
if _, err := tx.ExecContext(ctx, `
UPDATE network_exit SET observed_public_ip = NULLIF($2, '')::inet, observed_region = $3,
health_status = $4, last_check_reason = $5, version = $6, last_checked_at = now(), updated_at = now()
WHERE id = $1`, id, observation.PublicIP, observation.Region, status, reason, version); err != nil {
return NetworkExit{}, "persistence_failed", errors.New("record network exit check")
}
var transitions []taskstate.Transition
if changed {
transitions, err = invalidateAccountsForExit(ctx, tx, id)
if err != nil {
return NetworkExit{}, "persistence_failed", err
}
}
if err := s.commitTransitions(tx, transitions); err != nil {
return NetworkExit{}, "persistence_failed", err
}
exit, err := s.GetNetworkExit(ctx, id)
return exit, reason, err
}
func validExitFailureReason(reason string) bool {
switch reason {
case "", "credential_unavailable", "credential_invalid", "proxy_auth_failed", "proxy_check_failed", "exit_observation_invalid":
return true
default:
return false
}
}
func sameIP(left, right string) bool {
if left == "" || right == "" {
return left == right
}
return net.ParseIP(left).Equal(net.ParseIP(right))
}
func (s *Store) DisableNetworkExit(ctx context.Context, id string) (NetworkExit, error) {
if !exitIDPattern.MatchString(id) {
return NetworkExit{}, ErrInvalid
}
tx, err := s.db.BeginTx(ctx, nil)
if err != nil {
return NetworkExit{}, errors.New("begin network exit disable")
}
defer tx.Rollback()
var oldStatus string
if err := tx.QueryRowContext(ctx, `SELECT health_status FROM network_exit WHERE id = $1 FOR UPDATE`, id).Scan(&oldStatus); err != nil {
return NetworkExit{}, rowError(err)
}
var active bool
if err := tx.QueryRowContext(ctx, `SELECT EXISTS (
SELECT 1 FROM environment_binding binding
JOIN runtime_instance runtime ON runtime.binding_id = binding.id
WHERE binding.network_exit_id = $1 AND runtime.released_at IS NULL AND runtime.lease_until > now()
) OR EXISTS (
SELECT 1 FROM environment_binding binding
JOIN operation_task task ON task.account_id = binding.account_id
WHERE binding.network_exit_id = $1 AND task.state = 'executing'
)`, id).Scan(&active); err != nil {
return NetworkExit{}, errors.New("check network exit activity")
}
if active {
return NetworkExit{}, ErrConflict
}
var transitions []taskstate.Transition
if oldStatus != "disabled" {
if _, err := tx.ExecContext(ctx, `
UPDATE network_exit SET health_status = 'disabled', last_check_reason = 'exit_disabled',
version = version + 1, updated_at = now()
WHERE id = $1`, id); err != nil {
return NetworkExit{}, errors.New("disable network exit")
}
transitions, err = invalidateAccountsForExit(ctx, tx, id)
if err != nil {
return NetworkExit{}, err
}
}
if err := s.commitTransitions(tx, transitions); err != nil {
return NetworkExit{}, err
}
return s.GetNetworkExit(ctx, id)
}
func invalidateAccountsForExit(ctx context.Context, tx *sql.Tx, exitID string) ([]taskstate.Transition, error) {
rows, err := tx.QueryContext(ctx, `
WITH changed AS (
UPDATE social_account account SET status = 'paused', paused_at = COALESCE(paused_at, now()),
version = account.version + 1, updated_at = now()
FROM environment_binding binding
WHERE binding.network_exit_id = $1 AND binding.account_id = account.id
RETURNING account.id
), held AS (
UPDATE operation_task task SET
state = CASE task.state WHEN 'executing' THEN 'needs_confirmation' ELSE 'policy_hold' END,
hold_reason = 'exit_unhealthy', verification_result = NULL, verified_at = NULL, verified_by = NULL,
lease_owner = NULL, lease_until = NULL, updated_at = now()
FROM changed WHERE task.account_id = changed.id AND task.state IN ('queued', 'executing')
RETURNING task.current_attempt_id, task.state, task.hold_reason, task.account_id, task.id
), finished AS (
UPDATE execution_attempt attempt SET finished_at = now(), outcome = 'uncertain'
FROM held WHERE held.state = 'needs_confirmation' AND attempt.id = held.current_attempt_id
AND attempt.finished_at IS NULL
RETURNING attempt.id
)
SELECT state, hold_reason, account_id, id FROM held`, exitID)
if err != nil {
return nil, errors.New("invalidate network exit accounts")
}
return scanTransitions(rows, "read invalidated network exit tasks")
}
func (s *Store) CreateBoundEnv(ctx context.Context, env Env, accountID, exitID string) (EnvironmentContext, bool, error) {
env.Alias, env.Name = strings.TrimSpace(env.Alias), strings.TrimSpace(env.Name)
if !aliasPattern.MatchString(env.Alias) || !validDisplayName(env.Name) || !aliasPattern.MatchString(accountID) ||
(exitID != "" && !exitIDPattern.MatchString(exitID)) || !gatewayNamePattern.MatchString(env.Gateway) ||
env.Fingerprint.ProxyServer != "" {
return EnvironmentContext{}, false, ErrInvalid
}
if err := env.Fingerprint.Validate(); err != nil {
return EnvironmentContext{}, false, ErrInvalid
}
encoded, _ := json.Marshal(env.Fingerprint)
tx, err := s.db.BeginTx(ctx, nil)
if err != nil {
return EnvironmentContext{}, false, errors.New("begin bound environment create")
}
defer tx.Rollback()
var existingAlias, existingExit string
err = tx.QueryRowContext(ctx, `SELECT browser_env_alias, COALESCE(network_exit_id, '') FROM environment_binding WHERE account_id = $1 FOR UPDATE`, accountID).
Scan(&existingAlias, &existingExit)
if err == nil {
if existingAlias != env.Alias || existingExit != exitID {
return EnvironmentContext{}, false, ErrConflict
}
if err := tx.Commit(); err != nil {
return EnvironmentContext{}, false, errors.New("commit existing environment lookup")
}
context, err := s.GetEnvironmentContext(ctx, env.Alias)
if err != nil || context.Name != env.Name || context.Gateway != env.Gateway || context.Fingerprint != env.Fingerprint {
return EnvironmentContext{}, false, ErrConflict
}
return context, false, nil
}
if !errors.Is(err, sql.ErrNoRows) {
return EnvironmentContext{}, false, publicDatabaseError(err)
}
var created string
if err := tx.QueryRowContext(ctx, `
INSERT INTO browser_env (alias, name, gateway_name, fingerprint)
SELECT $1, $2, $3, $4
FROM social_account account
WHERE account.id = $5 AND account.status = 'paused'
AND account.authorization_status = 'authorized'
AND ($6 = '' OR EXISTS (SELECT 1 FROM network_exit WHERE id = $6 AND health_status = 'healthy'))
RETURNING alias`, env.Alias, env.Name, env.Gateway, encoded, accountID, exitID).Scan(&created); err != nil {
return EnvironmentContext{}, false, rowError(err)
}
if _, err := tx.ExecContext(ctx, `
INSERT INTO environment_binding (id, account_id, browser_env_alias, network_exit_id)
VALUES ($1, $1, $2, NULLIF($3, ''))`, accountID, env.Alias, exitID); err != nil {
return EnvironmentContext{}, false, publicDatabaseError(err)
}
if _, err := tx.ExecContext(ctx, `UPDATE social_account SET version = version + 1, updated_at = now() WHERE id = $1`, accountID); err != nil {
return EnvironmentContext{}, false, errors.New("version bound account")
}
if err := commitHub(tx); err != nil {
return EnvironmentContext{}, false, err
}
context, err := s.GetEnvironmentContext(ctx, env.Alias)
return context, true, err
}
func (s *Store) GetEnvironmentContext(ctx context.Context, alias string) (EnvironmentContext, error) {
if !aliasPattern.MatchString(alias) {
return EnvironmentContext{}, ErrInvalid
}
tx, err := s.db.BeginTx(ctx, nil)
if err != nil {
return EnvironmentContext{}, errors.New("begin environment context read")
}
defer tx.Rollback()
var result EnvironmentContext
var encoded []byte
var expectedIP, observedIP string
var checked sql.NullTime
var runtimeInstanceID, runtimeID, runtimeNetworkID, cleanupInstanceID, cleanupRuntimeID, cleanupNetworkID sql.NullString
var cleanupBindingVersion sql.NullInt64
err = tx.QueryRowContext(ctx, `
SELECT environment.alias, environment.name, environment.gateway_name,
environment.fingerprint, environment.created_at, binding.account_id, account.status, account.authorization_status,
binding.id, binding.version,
binding.runtime_cleanup_pending, binding.runtime_cleanup_binding_version,
binding.runtime_cleanup_instance_id, binding.runtime_cleanup_runtime_id, binding.runtime_cleanup_network_id,
COALESCE(network.id, ''), COALESCE(network.protocol, ''), COALESCE(network.host, ''), COALESCE(network.port, 0),
COALESCE(host(network.expected_public_ip), ''), COALESCE(network.expected_region, ''),
COALESCE(host(network.observed_public_ip), ''), COALESCE(network.observed_region, ''),
COALESCE(network.health_status, 'unchecked'), COALESCE(network.last_check_reason, ''),
COALESCE(network.version, 0), network.last_checked_at,
COALESCE(network.created_at, to_timestamp(0)), COALESCE(network.updated_at, to_timestamp(0)),
runtime.id, runtime.runtime_id, runtime.network_id, COALESCE(runtime.node_id, '')
FROM browser_env environment
JOIN environment_binding binding ON binding.browser_env_alias = environment.alias
JOIN social_account account ON account.id = binding.account_id
LEFT JOIN network_exit network ON network.id = binding.network_exit_id
LEFT JOIN runtime_instance runtime ON runtime.binding_id = binding.id AND runtime.released_at IS NULL
WHERE environment.alias = $1`, alias).
Scan(&result.Alias, &result.Name, &result.Gateway, &encoded, &result.CreatedAt,
&result.AccountID, &result.AccountStatus, &result.AuthorizationStatus,
&result.BindingID, &result.BindingVersion, &result.RuntimeCleanupPending, &cleanupBindingVersion,
&cleanupInstanceID, &cleanupRuntimeID, &cleanupNetworkID,
&result.Exit.ID, &result.Exit.Protocol, &result.Exit.Host, &result.Exit.Port,
&expectedIP, &result.Exit.ExpectedRegion, &observedIP, &result.Exit.ObservedRegion,
&result.Exit.HealthStatus, &result.Exit.LastCheckReason, &result.Exit.Version, &checked, &result.Exit.CreatedAt, &result.Exit.UpdatedAt,
&runtimeInstanceID, &runtimeID, &runtimeNetworkID, &result.RuntimeNodeID)
if err != nil {
return EnvironmentContext{}, rowError(err)
}
if err := json.Unmarshal(encoded, &result.Fingerprint); err != nil {
return EnvironmentContext{}, errors.New("decode bound environment fingerprint")
}
result.Fingerprint.ProxyServer = ""
result.Fingerprint.DisableNonProxiedUDP = false
result.Exit.ExpectedPublicIP, result.Exit.ObservedPublicIP = expectedIP, observedIP
if checked.Valid {
result.Exit.LastCheckedAt = &checked.Time
}
result.RuntimeInstanceID, result.RuntimeID, result.RuntimeNetworkID = runtimeInstanceID.String, runtimeID.String, runtimeNetworkID.String
if result.RuntimeCleanupPending {
result.RuntimeCleanupBindingVersion = cleanupBindingVersion.Int64
result.RuntimeCleanupInstanceID, result.RuntimeCleanupRuntimeID = cleanupInstanceID.String, cleanupRuntimeID.String
result.RuntimeCleanupNetworkID = cleanupNetworkID.String
}
if err := commitHub(tx); err != nil {
return EnvironmentContext{}, err
}
return result, nil
}
func (s *Store) GetEnvironmentContextForAccount(ctx context.Context, accountID string) (EnvironmentContext, error) {
if !aliasPattern.MatchString(accountID) {
return EnvironmentContext{}, ErrInvalid
}
var alias string
if err := s.db.QueryRowContext(ctx, `
SELECT browser_env_alias FROM environment_binding WHERE account_id = $1`, accountID).Scan(&alias); err != nil {
return EnvironmentContext{}, rowError(err)
}
return s.GetEnvironmentContext(ctx, alias)
}
const runtimeUseLeaseDuration = time.Minute
func validRuntimeUse(purpose, ownerID, taskID string) bool {
if purpose != "task" && purpose != "listener" || ownerID == "" || len(ownerID) > 128 || !exitIDPattern.MatchString(ownerID) {
return false
}
return taskID == "" || exitIDPattern.MatchString(taskID)
}
// AcquireRuntimeUse reserves a short-lived task or long-lived listener right on
// an already running browser. It deliberately does not own the browser runtime
// itself; stopping that runtime revokes active use leases and callers observe
// the loss on their next renewal.
func (s *Store) AcquireRuntimeUse(ctx context.Context, alias, purpose, ownerID, taskID string) (RuntimeUseLease, error) {
if !aliasPattern.MatchString(alias) || !validRuntimeUse(purpose, ownerID, taskID) {
return RuntimeUseLease{}, ErrInvalid
}
tx, err := s.db.BeginTx(ctx, nil)
if err != nil {
return RuntimeUseLease{}, errors.New("begin runtime use acquisition")
}
defer tx.Rollback()
var runtimeInstanceID string
err = tx.QueryRowContext(ctx, `
SELECT runtime.id
FROM environment_binding binding
JOIN runtime_instance runtime ON runtime.binding_id = binding.id
JOIN social_account account ON account.id = binding.account_id
WHERE binding.browser_env_alias = $1 AND runtime.released_at IS NULL
AND runtime.lease_until > now() AND NOT binding.runtime_cleanup_pending
AND account.status = 'active' AND account.authorization_status = 'authorized'
FOR UPDATE OF binding, runtime`, alias).Scan(&runtimeInstanceID)
if errors.Is(err, sql.ErrNoRows) {
return RuntimeUseLease{}, ErrConflict
}
if err != nil {
return RuntimeUseLease{}, publicDatabaseError(err)
}
token := "runtime-use-" + newHubID()
var lease RuntimeUseLease
err = tx.QueryRowContext(ctx, `
INSERT INTO runtime_use_lease (token, runtime_instance_id, owner_id, purpose, task_id, lease_until)
VALUES ($1, $2, $3, $4, NULLIF($5, ''), now() + $6::interval)
RETURNING token, runtime_instance_id, owner_id, purpose, COALESCE(task_id, ''), lease_until`,
token, runtimeInstanceID, ownerID, purpose, taskID, runtimeUseLeaseDuration.String()).Scan(
&lease.Token, &lease.RuntimeInstanceID, &lease.OwnerID, &lease.Purpose, &lease.TaskID, &lease.LeaseUntil)
if err != nil {
return RuntimeUseLease{}, publicDatabaseError(err)
}
if err := commitHub(tx); err != nil {
return RuntimeUseLease{}, err
}
return lease, nil
}
func (s *Store) RenewRuntimeUse(ctx context.Context, token string) (RuntimeUseLease, error) {
if !exitIDPattern.MatchString(token) {
return RuntimeUseLease{}, ErrInvalid
}
var lease RuntimeUseLease
err := s.db.QueryRowContext(ctx, `
UPDATE runtime_use_lease use_lease
SET lease_until = now() + interval '1 minute'
FROM runtime_instance runtime
WHERE use_lease.token = $1 AND use_lease.released_at IS NULL
AND use_lease.lease_until > now() AND runtime.id = use_lease.runtime_instance_id
AND runtime.released_at IS NULL AND runtime.lease_until > now()
RETURNING use_lease.token, use_lease.runtime_instance_id, use_lease.owner_id,
use_lease.purpose, COALESCE(use_lease.task_id, ''), use_lease.lease_until`, token).Scan(
&lease.Token, &lease.RuntimeInstanceID, &lease.OwnerID, &lease.Purpose, &lease.TaskID, &lease.LeaseUntil)
if errors.Is(err, sql.ErrNoRows) {
return RuntimeUseLease{}, ErrConflict
}
if err != nil {
return RuntimeUseLease{}, publicDatabaseError(err)
}
return lease, nil
}
func (s *Store) ReleaseRuntimeUse(ctx context.Context, token string) error {
if !exitIDPattern.MatchString(token) {
return ErrInvalid
}
result, err := s.db.ExecContext(ctx, `
UPDATE runtime_use_lease SET released_at = now()
WHERE token = $1 AND released_at IS NULL`, token)
if err != nil {
return publicDatabaseError(err)
}
affected, err := result.RowsAffected()
if err != nil {
return err
}
if affected == 1 {
return nil
}
var exists bool
if err := s.db.QueryRowContext(ctx, `SELECT EXISTS (SELECT 1 FROM runtime_use_lease WHERE token = $1)`, token).Scan(&exists); err != nil {
return publicDatabaseError(err)
}
if !exists {
return ErrNotFound
}
return nil
}
func (s *Store) SetRuntimeNode(ctx context.Context, alias, runtimeID, nodeID string) error {
if !aliasPattern.MatchString(alias) || !exitIDPattern.MatchString(runtimeID) || !nodeIDPattern.MatchString(nodeID) {
return ErrInvalid
}
result, err := s.db.ExecContext(ctx, `
UPDATE runtime_instance runtime
SET node_id = $3
FROM environment_binding binding
WHERE binding.browser_env_alias = $1 AND runtime.id = $2 AND runtime.binding_id = binding.id
AND runtime.released_at IS NULL`, alias, runtimeID, nodeID)
if err != nil {
return publicDatabaseError(err)
}
affected, err := result.RowsAffected()
if err != nil {
return err
}
if affected != 1 {
return ErrConflict
}
return nil
}
func releaseRuntimeUseLeases(ctx context.Context, tx *sql.Tx, runtimeInstanceID string) error {
if runtimeInstanceID == "" {
return nil
}
_, err := tx.ExecContext(ctx, `UPDATE runtime_use_lease SET released_at = now() WHERE runtime_instance_id = $1 AND released_at IS NULL`, runtimeInstanceID)
return err
}
func releaseExpiredRuntime(ctx context.Context, tx *sql.Tx, bindingID string) error {
var accountID, alias, runtimeInstanceID string
var exitID sql.NullString
var bindingVersion int64
err := tx.QueryRowContext(ctx, `
UPDATE runtime_instance runtime SET released_at = now()
FROM environment_binding binding
WHERE binding.id = $1 AND runtime.binding_id = binding.id
AND runtime.released_at IS NULL AND runtime.lease_until <= now()
RETURNING runtime.account_id, binding.browser_env_alias, binding.network_exit_id,
runtime.id, runtime.binding_version`, bindingID).
Scan(&accountID, &alias, &exitID, &runtimeInstanceID, &bindingVersion)
if errors.Is(err, sql.ErrNoRows) {
return nil
}
if err != nil {
return err
}
if err := releaseRuntimeUseLeases(ctx, tx, runtimeInstanceID); err != nil {
return errors.New("release runtime use leases")
}
return appendRuntimeAudit(ctx, tx, "runtime_released", accountID, alias, exitID.String, runtimeInstanceID, bindingVersion)
}
func validateEnvironmentRebind(ctx context.Context, tx *sql.Tx, alias, exitID string, expectedBindingVersion int64) (string, string, error) {
var accountID, bindingID string
var bindingVersion int64
err := tx.QueryRowContext(ctx, `
SELECT binding.account_id, binding.id, binding.version
FROM environment_binding binding
JOIN social_account account ON account.id = binding.account_id
WHERE binding.browser_env_alias = $1 AND account.status = 'paused'
AND account.authorization_status = 'authorized'
AND NOT binding.runtime_cleanup_pending
FOR UPDATE OF binding, account`, alias).Scan(&accountID, &bindingID, &bindingVersion)
if errors.Is(err, sql.ErrNoRows) {
return "", "", ErrConflict
}
if err != nil {
return "", "", publicDatabaseError(err)
}
if bindingVersion != expectedBindingVersion {
return "", "", ErrConflict
}
if err := releaseExpiredRuntime(ctx, tx, bindingID); err != nil {
return "", "", errors.New("expire runtime before rebind")
}
var allowed bool
if err := tx.QueryRowContext(ctx, `
SELECT EXISTS (SELECT 1 FROM network_exit WHERE id = $1 AND health_status = 'healthy')
AND NOT EXISTS (SELECT 1 FROM operation_task WHERE account_id = $2 AND state = 'executing')
AND NOT EXISTS (SELECT 1 FROM runtime_instance WHERE binding_id = $3 AND released_at IS NULL)`,
exitID, accountID, bindingID).Scan(&allowed); err != nil {
return "", "", errors.New("check environment rebind")
}
if !allowed {
return "", "", ErrConflict
}
return accountID, bindingID, nil
}
func (s *Store) ValidateEnvironmentRebind(ctx context.Context, alias, exitID string, expectedBindingVersion int64) error {
if !aliasPattern.MatchString(alias) || !exitIDPattern.MatchString(exitID) || expectedBindingVersion < 1 {
return ErrInvalid
}
tx, err := s.db.BeginTx(ctx, nil)
if err != nil {
return errors.New("begin environment rebind validation")
}
defer tx.Rollback()
if _, _, err := validateEnvironmentRebind(ctx, tx, alias, exitID, expectedBindingVersion); err != nil {
return err
}
if err := commitHub(tx); err != nil {
return err
}
return nil
}
func (s *Store) RebindEnvironment(ctx context.Context, alias, exitID, runtimeID string, expectedBindingVersion int64, networkIDs ...string) (EnvironmentContext, error) {
networkID := ""
if len(networkIDs) == 1 {
networkID = networkIDs[0]
}
if !aliasPattern.MatchString(alias) || !exitIDPattern.MatchString(exitID) ||
(runtimeID != "" && !exitIDPattern.MatchString(runtimeID)) || (networkID != "" && !exitIDPattern.MatchString(networkID)) ||
len(networkIDs) > 1 || expectedBindingVersion < 1 {
return EnvironmentContext{}, ErrInvalid
}
tx, err := s.db.BeginTx(ctx, nil)
if err != nil {
return EnvironmentContext{}, errors.New("begin environment rebind")
}
defer tx.Rollback()
accountID, bindingID, err := validateEnvironmentRebind(ctx, tx, alias, exitID, expectedBindingVersion)
if err != nil {
return EnvironmentContext{}, err
}
if _, err := tx.ExecContext(ctx, `UPDATE environment_binding SET network_exit_id = $2, version = version + 1, updated_at = now() WHERE browser_env_alias = $1`, alias, exitID); err != nil {
return EnvironmentContext{}, errors.New("update environment binding")
}
if _, err := tx.ExecContext(ctx, `UPDATE social_account SET version = version + 1, updated_at = now() WHERE id = $1`, accountID); err != nil {
return EnvironmentContext{}, errors.New("version rebound account")
}
if runtimeID != "" {
runtimeInstanceID := "runtime-" + newHubID()
if _, err := tx.ExecContext(ctx, `
INSERT INTO runtime_instance (id, account_id, binding_id, binding_version, runtime_id, network_id, owner_id, purpose, lease_token, node_id, lease_until)
VALUES ($1, $2, $3, $4, $5, NULLIF($6, ''), 'control-plane', 'session', 'runtime-lease-' || $1, '', now() + interval '1 minute')`,
runtimeInstanceID, accountID, bindingID, expectedBindingVersion+1, runtimeID, networkID); err != nil {
return EnvironmentContext{}, publicDatabaseError(err)
}
if err := appendRuntimeAudit(ctx, tx, "runtime_bound", accountID, alias, exitID, runtimeInstanceID, expectedBindingVersion+1); err != nil {
return EnvironmentContext{}, err
}
}
if err := commitHub(tx); err != nil {
return EnvironmentContext{}, err
}
return s.GetEnvironmentContext(ctx, alias)
}
func (s *Store) ActivateRuntime(ctx context.Context, alias, runtimeID string, bindingVersion int64, exitID string, networkIDs ...string) (EnvironmentContext, error) {
networkID := ""
if len(networkIDs) == 1 {
networkID = networkIDs[0]
}
if !aliasPattern.MatchString(alias) || !exitIDPattern.MatchString(runtimeID) || bindingVersion < 1 ||
(exitID != "" && !exitIDPattern.MatchString(exitID)) || !exitIDPattern.MatchString(networkID) || len(networkIDs) != 1 {
return EnvironmentContext{}, ErrInvalid
}
tx, err := s.db.BeginTx(ctx, nil)
if err != nil {
return EnvironmentContext{}, errors.New("begin runtime activation")
}
defer tx.Rollback()
var accountID, bindingID, currentExitID, accountStatus, authorizationStatus string
var currentBindingVersion int64
var cleanupPending bool
err = tx.QueryRowContext(ctx, `
SELECT binding.account_id, binding.id, binding.version, COALESCE(binding.network_exit_id, ''), binding.runtime_cleanup_pending,
account.status, account.authorization_status
FROM environment_binding binding
JOIN social_account account ON account.id = binding.account_id
WHERE binding.browser_env_alias = $1 FOR UPDATE OF binding, account`, alias).
Scan(&accountID, &bindingID, &currentBindingVersion, &currentExitID, &cleanupPending, &accountStatus, &authorizationStatus)
if err != nil {
return EnvironmentContext{}, rowError(err)
}
if cleanupPending || accountStatus != "active" || authorizationStatus != "authorized" ||
currentBindingVersion != bindingVersion || currentExitID != exitID {
return EnvironmentContext{}, ErrConflict
}
if err := releaseExpiredRuntime(ctx, tx, bindingID); err != nil {
return EnvironmentContext{}, errors.New("expire runtime before activation")
}
var existingInstanceID, existingRuntimeID, existingNetworkID string
var existingBindingVersion int64
if err := tx.QueryRowContext(ctx, `
SELECT COALESCE(id, ''), COALESCE(runtime_id, ''), COALESCE(network_id, ''), COALESCE(binding_version, 0) FROM runtime_instance
WHERE binding_id = $1 AND released_at IS NULL`, bindingID).Scan(&existingInstanceID, &existingRuntimeID, &existingNetworkID, &existingBindingVersion); err != nil && !errors.Is(err, sql.ErrNoRows) {
return EnvironmentContext{}, publicDatabaseError(err)
}
if existingInstanceID != "" && (existingRuntimeID != runtimeID || existingNetworkID != networkID || existingBindingVersion != bindingVersion) {
return EnvironmentContext{}, ErrConflict
}
if existingInstanceID == "" {
existingInstanceID = "runtime-" + newHubID()
if _, err := tx.ExecContext(ctx, `
INSERT INTO runtime_instance (id, account_id, binding_id, binding_version, runtime_id, network_id, owner_id, purpose, lease_token, node_id, lease_until)
VALUES ($1, $2, $3, $4, $5, NULLIF($6, ''), 'control-plane', 'session', 'runtime-lease-' || $1, '', now() + interval '1 minute')`,
existingInstanceID, accountID, bindingID, bindingVersion, runtimeID, networkID); err != nil {
return EnvironmentContext{}, publicDatabaseError(err)
}
if err := appendRuntimeAudit(ctx, tx, "runtime_bound", accountID, alias, exitID, existingInstanceID, bindingVersion); err != nil {
return EnvironmentContext{}, err
}
} else if _, err := tx.ExecContext(ctx, `UPDATE runtime_instance SET owner_id = 'control-plane', purpose = 'session', lease_until = now() + interval '1 minute' WHERE id = $1`, existingInstanceID); err != nil {
return EnvironmentContext{}, errors.New("renew environment runtime")
}
if err := commitHub(tx); err != nil {
return EnvironmentContext{}, err
}
return s.GetEnvironmentContext(ctx, alias)
}
func (s *Store) ReleaseRuntime(ctx context.Context, environment EnvironmentContext) error {
if !aliasPattern.MatchString(environment.Alias) || !exitIDPattern.MatchString(environment.BindingID) ||
environment.BindingVersion < 1 || (environment.RuntimeInstanceID != "" && !exitIDPattern.MatchString(environment.RuntimeInstanceID)) {
return ErrInvalid
}
if environment.RuntimeInstanceID == "" {
return nil
}
tx, err := s.db.BeginTx(ctx, nil)
if err != nil {
return errors.New("begin runtime release")
}
defer tx.Rollback()
var accountID, alias, runtimeInstanceID string
var exitID sql.NullString
var bindingVersion int64
err = tx.QueryRowContext(ctx, `
UPDATE runtime_instance runtime SET released_at = now()
FROM environment_binding binding
WHERE binding.browser_env_alias = $1 AND binding.id = $2 AND binding.version = $3
AND runtime.binding_id = binding.id AND runtime.binding_version = binding.version
AND runtime.id = $4 AND runtime.released_at IS NULL
RETURNING runtime.account_id, binding.browser_env_alias, binding.network_exit_id, runtime.id, runtime.binding_version`,
environment.Alias, environment.BindingID, environment.BindingVersion, environment.RuntimeInstanceID).
Scan(&accountID, &alias, &exitID, &runtimeInstanceID, &bindingVersion)
if errors.Is(err, sql.ErrNoRows) {
return ErrConflict
}
if err != nil {
return errors.New("release environment runtime")
}
if err := releaseRuntimeUseLeases(ctx, tx, runtimeInstanceID); err != nil {
return errors.New("release runtime use leases")
}
if err := appendRuntimeAudit(ctx, tx, "runtime_released", accountID, alias, exitID.String, runtimeInstanceID, bindingVersion); err != nil {
return err
}
return commitHub(tx)
}
func (s *Store) SetRuntimeCleanupPending(ctx context.Context, environment EnvironmentContext, pending bool) error {
if !aliasPattern.MatchString(environment.Alias) || !exitIDPattern.MatchString(environment.BindingID) ||
environment.BindingVersion < 1 || environment.RuntimeCleanupBindingVersion < 1 ||
(pending && environment.RuntimeCleanupRuntimeID == "") ||
(environment.RuntimeCleanupInstanceID != "" && !exitIDPattern.MatchString(environment.RuntimeCleanupInstanceID)) ||
(environment.RuntimeCleanupRuntimeID != "" && !exitIDPattern.MatchString(environment.RuntimeCleanupRuntimeID)) ||
(environment.RuntimeCleanupNetworkID != "" && !exitIDPattern.MatchString(environment.RuntimeCleanupNetworkID)) {
return ErrInvalid
}
tx, err := s.db.BeginTx(ctx, nil)
if err != nil {
return errors.New("begin runtime cleanup state update")
}
defer tx.Rollback()
var currentPending bool
var accountID, alias string
var exitID sql.NullString
var cleanupBindingVersion sql.NullInt64
var cleanupInstanceID, cleanupRuntimeID, cleanupNetworkID sql.NullString
if err := tx.QueryRowContext(ctx, `
SELECT runtime_cleanup_pending, runtime_cleanup_binding_version,
runtime_cleanup_instance_id, runtime_cleanup_runtime_id, runtime_cleanup_network_id,
account_id, browser_env_alias, network_exit_id
FROM environment_binding
WHERE browser_env_alias = $1 AND id = $2 AND version = $3 FOR UPDATE`, environment.Alias,
environment.BindingID, environment.BindingVersion).
Scan(&currentPending, &cleanupBindingVersion, &cleanupInstanceID, &cleanupRuntimeID, &cleanupNetworkID,
&accountID, &alias, &exitID); errors.Is(err, sql.ErrNoRows) {
return ErrConflict
} else if err != nil {
return publicDatabaseError(err)
}
if currentPending {
if cleanupBindingVersion.Int64 != environment.RuntimeCleanupBindingVersion ||
cleanupInstanceID.String != environment.RuntimeCleanupInstanceID || cleanupRuntimeID.String != environment.RuntimeCleanupRuntimeID ||
cleanupNetworkID.String != environment.RuntimeCleanupNetworkID {
return ErrConflict
}
if pending {
return commitHub(tx)
}
} else if !pending {
return commitHub(tx)
}
if pending && !currentPending {
var runtimeInstanceID string
err := tx.QueryRowContext(ctx, `
SELECT id FROM runtime_instance
WHERE binding_id = $1 AND binding_version = $2 AND released_at IS NULL`, environment.BindingID,
environment.BindingVersion).Scan(&runtimeInstanceID)
if errors.Is(err, sql.ErrNoRows) {
runtimeInstanceID = ""
} else if err != nil {
return publicDatabaseError(err)
}
if runtimeInstanceID != environment.RuntimeCleanupInstanceID {
return ErrConflict
}
if runtimeInstanceID != "" {
result, err := tx.ExecContext(ctx, `
UPDATE runtime_instance SET released_at = now()
WHERE id = $1 AND binding_id = $2 AND binding_version = $3 AND released_at IS NULL`, runtimeInstanceID,
environment.BindingID, environment.BindingVersion)
if err != nil {
return errors.New("release runtime for pending cleanup")
}
if affected, err := result.RowsAffected(); err != nil || affected != 1 {
return ErrConflict
}
if err := releaseRuntimeUseLeases(ctx, tx, runtimeInstanceID); err != nil {
return errors.New("release runtime use leases")
}
if err := appendRuntimeAudit(ctx, tx, "runtime_released", accountID, alias, exitID.String, runtimeInstanceID, environment.BindingVersion); err != nil {
return err
}
}
}
if pending {
_, err = tx.ExecContext(ctx, `
UPDATE environment_binding SET runtime_cleanup_pending = true,
runtime_cleanup_binding_version = $2, runtime_cleanup_instance_id = NULLIF($3, ''),
runtime_cleanup_runtime_id = NULLIF($4, ''), runtime_cleanup_network_id = NULLIF($5, ''), updated_at = now()
WHERE id = $1`, environment.BindingID, environment.RuntimeCleanupBindingVersion,
environment.RuntimeCleanupInstanceID, environment.RuntimeCleanupRuntimeID, environment.RuntimeCleanupNetworkID)
} else {
_, err = tx.ExecContext(ctx, `
UPDATE environment_binding SET runtime_cleanup_pending = false,
runtime_cleanup_binding_version = NULL, runtime_cleanup_instance_id = NULL,
runtime_cleanup_runtime_id = NULL, runtime_cleanup_network_id = NULL, updated_at = now()
WHERE id = $1`, environment.BindingID)
}
if err != nil {
return errors.New("update runtime cleanup state")
}
return commitHub(tx)
}
func appendRuntimeAudit(ctx context.Context, tx *sql.Tx, eventType, accountID, alias, exitID, runtimeInstanceID string, bindingVersion int64) error {
_, err := tx.ExecContext(ctx, `
INSERT INTO audit_event
(event_type, account_id, browser_env_alias, network_exit_id, runtime_instance_id, binding_version, actor, reason_code)
VALUES ($1, $2, $3, NULLIF($4, ''), $5, $6, 'local-user', $1)`,
eventType, accountID, alias, exitID, runtimeInstanceID, bindingVersion)
if err != nil {
return errors.New("append runtime audit event")
}
return nil
}
func (s *Store) AppendEnvironmentAction(ctx context.Context, eventType string, action EnvironmentAction) error {
if (eventType != "environment_action_requested" && eventType != "environment_action_finished") ||
!exitIDPattern.MatchString(action.OperationID) || action.Action == "" || action.ReasonCode == "" ||
(eventType == "environment_action_finished" && action.Outcome != "succeeded" && action.Outcome != "failed" && action.Outcome != "unknown") {
return ErrInvalid
}
_, err := s.db.ExecContext(ctx, `
INSERT INTO audit_event
(event_type, account_id, browser_env_alias, network_exit_id, runtime_instance_id,
binding_version, actor, reason_code, operation_id, action, outcome)
VALUES ($1, NULLIF($2, ''), NULLIF($3, ''), NULLIF($4, ''), NULLIF($5, ''),
NULLIF($6, 0), 'local-user', $7, $8, $9, NULLIF($10, ''))`,
eventType, action.AccountID, action.BrowserEnvAlias, action.NetworkExitID, action.RuntimeInstanceID,
action.BindingVersion, action.ReasonCode, action.OperationID, action.Action, action.Outcome)
if err != nil {
return errors.New("append environment action")
}
return nil
}
func NewOperationID() string { return "operation-" + newHubID() }
func newHubID() string {
var value [12]byte
_, _ = rand.Read(value[:])
return hex.EncodeToString(value[:])
}