diff --git a/.gitignore b/.gitignore index ad80b11..1106bb2 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ .vscode/ +.opencode/ config.pkl diff --git a/.mise.toml b/.mise.toml index f1b9820..12465d1 100644 --- a/.mise.toml +++ b/.mise.toml @@ -29,6 +29,11 @@ description = "Render tfvars and show what would change" dir = "{{ config_root }}/ansible" run = "ansible-playbook deploy.yml --tags=render,init,plan" +[tasks.undeploy-mobile] +description = "Tear down constructor-mobile Terraform resources" +dir = "{{ config_root }}/ansible" +run = "ansible-playbook undeploy-mobile.yml" + [settings] lockfile = true experimental = true diff --git a/ansible/deploy.yml b/ansible/deploy.yml index 403d81e..4745ece 100644 --- a/ansible/deploy.yml +++ b/ansible/deploy.yml @@ -20,12 +20,14 @@ loop_control: loop_var: project label: "{{ project.name }}" + when: project.name != mobile_project or (mobile_enabled | bool) tags: [sync, upstream] - import_tasks: tasks/03-bootstrap-ba.yml tags: [bootstrap, build, ba] - import_tasks: tasks/03b-bootstrap-mobile.yml + when: mobile_enabled | bool tags: [bootstrap, build, mobile] # background-agents must be initialized/applied before constructor-mobile. @@ -43,10 +45,13 @@ # constructor-mobile consumes the BA control-plane URL, so keep all mobile # Terraform work after BA readiness is verified. - import_tasks: tasks/06-capture-ba-outputs.yml + when: mobile_enabled | bool tags: [mobile, capture, terraform] - import_tasks: tasks/06a-terraform-init-mobile.yml + when: mobile_enabled | bool tags: [mobile, init, terraform] - import_tasks: tasks/06b-terraform-apply-mobile.yml + when: mobile_enabled | bool tags: [mobile, apply] diff --git a/ansible/group_vars/all.yml b/ansible/group_vars/all.yml index c7131c7..d229aef 100644 --- a/ansible/group_vars/all.yml +++ b/ansible/group_vars/all.yml @@ -9,6 +9,7 @@ config_render_root: "{{ config_root }}/render" deploy_dir: "{{ repo_root }}/deploy" inputs_json: "{{ deploy_dir }}/inputs.json" mobile_inputs_json: "{{ deploy_dir }}/mobile-inputs.json" +mobile_enabled: false state_json: "{{ deploy_dir }}/state.json" projects_json: "{{ deploy_dir }}/projects.json" diff --git a/ansible/tasks/01-render-config.yml b/ansible/tasks/01-render-config.yml index b7d02be..d66fdce 100644 --- a/ansible/tasks/01-render-config.yml +++ b/ansible/tasks/01-render-config.yml @@ -19,3 +19,11 @@ loop_control: label: "{{ item.dst | basename }}" changed_when: true + +- name: Load rendered mobile settings + ansible.builtin.set_fact: + mobile_config: "{{ lookup('file', mobile_inputs_json) | from_json }}" + +- name: Set mobile provisioning flag + ansible.builtin.set_fact: + mobile_enabled: "{{ mobile_config.enabled | default(false) | bool }}" diff --git a/ansible/tasks/04-terraform-init-stack.yml b/ansible/tasks/04-terraform-init-stack.yml index 277e008..eadc0c0 100644 --- a/ansible/tasks/04-terraform-init-stack.yml +++ b/ansible/tasks/04-terraform-init-stack.yml @@ -9,7 +9,7 @@ dest: "{{ terraform_stack_tf_dir }}/{{ terraform_var_file }}" mode: "0600" content: | - {% for key, value in (terraform_inputs | combine(terraform_stack_extra_tfvars | default({}))).items() %} + {% for key, value in (terraform_inputs | dict2items | rejectattr('key', 'equalto', 'enabled') | items2dict | combine(terraform_stack_extra_tfvars | default({}))).items() %} {% if value is string and '\n' in value %} {{ key }} = <<-EOF {{ value }} diff --git a/ansible/tasks/06-capture-ba-outputs.yml b/ansible/tasks/06-capture-ba-outputs.yml index 9076b53..f4f111d 100644 --- a/ansible/tasks/06-capture-ba-outputs.yml +++ b/ansible/tasks/06-capture-ba-outputs.yml @@ -1,8 +1,6 @@ --- -- name: Load b-a inputs (for deterministic URL construction) - ansible.builtin.set_fact: - ba_inputs: "{{ lookup('file', inputs_json) | from_json }}" - tf_state: "{{ lookup('file', state_json) | from_json }}" +- name: Compute b-a URLs + ansible.builtin.import_tasks: 06-compute-ba-urls.yml - name: Verify background-agents has been deployed ansible.builtin.command: @@ -19,15 +17,6 @@ 'cloudflare_worker' not in ba_state_list.stdout or 'cloudflare_workers_deployment' not in ba_state_list.stdout -- name: Compute b-a control plane host - ansible.builtin.set_fact: - ba_control_plane_host: "open-inspect-control-plane-{{ ba_inputs.deployment_name }}.{{ ba_inputs.cloudflare_worker_subdomain }}.workers.dev" - -- name: Derive b-a URLs from host - ansible.builtin.set_fact: - ba_control_plane_url: "https://{{ ba_control_plane_host }}" - ba_ws_url: "wss://{{ ba_control_plane_host }}" - - name: Confirm captured values ansible.builtin.debug: msg: diff --git a/ansible/tasks/06-compute-ba-urls.yml b/ansible/tasks/06-compute-ba-urls.yml new file mode 100644 index 0000000..09b24e2 --- /dev/null +++ b/ansible/tasks/06-compute-ba-urls.yml @@ -0,0 +1,14 @@ +--- +- name: Load b-a inputs and state + ansible.builtin.set_fact: + ba_inputs: "{{ lookup('file', inputs_json) | from_json }}" + tf_state: "{{ lookup('file', state_json) | from_json }}" + +- name: Compute b-a control plane host + ansible.builtin.set_fact: + ba_control_plane_host: "open-inspect-control-plane-{{ ba_inputs.deployment_name }}.{{ ba_inputs.cloudflare_worker_subdomain }}.workers.dev" + +- name: Derive b-a URLs from host + ansible.builtin.set_fact: + ba_control_plane_url: "https://{{ ba_control_plane_host }}" + ba_ws_url: "wss://{{ ba_control_plane_host }}" diff --git a/ansible/tasks/07-terraform-destroy-mobile.yml b/ansible/tasks/07-terraform-destroy-mobile.yml new file mode 100644 index 0000000..c9786df --- /dev/null +++ b/ansible/tasks/07-terraform-destroy-mobile.yml @@ -0,0 +1,19 @@ +--- +- name: Define mobile terraform environment + ansible.builtin.set_fact: + mobile_tf_env: + AWS_ACCESS_KEY_ID: "{{ tf_state.access_key }}" + AWS_SECRET_ACCESS_KEY: "{{ tf_state.secret_key }}" + AWS_REGION: "{{ tf_state.region }}" + +- name: Terraform destroy (mobile) + ansible.builtin.command: + cmd: "terraform destroy {{ terraform_apply_args }}" + chdir: "{{ mobile_tf_dir }}" + environment: "{{ mobile_tf_env }}" + register: mobile_destroy + changed_when: "'Destroy complete' in mobile_destroy.stdout" + +- name: Destroy summary + ansible.builtin.debug: + msg: "constructor-mobile teardown finished" diff --git a/ansible/undeploy-mobile.yml b/ansible/undeploy-mobile.yml new file mode 100644 index 0000000..26d0823 --- /dev/null +++ b/ansible/undeploy-mobile.yml @@ -0,0 +1,32 @@ +--- +- name: Undeploy constructor-mobile stack + hosts: local + connection: local + gather_facts: false + + tasks: + - import_tasks: tasks/00-preflight.yml + tags: [preflight] + + - import_tasks: tasks/01-render-config.yml + tags: [render, config] + + - name: Sync constructor-mobile project + ansible.builtin.include_tasks: + file: tasks/02-sync-upstream.yml + apply: + tags: [sync, upstream, mobile] + loop: "{{ projects | selectattr('name', 'equalto', mobile_project) }}" + loop_control: + loop_var: project + label: "{{ project.name }}" + tags: [sync, upstream, mobile] + + - import_tasks: tasks/06-compute-ba-urls.yml + tags: [mobile, terraform] + + - import_tasks: tasks/06a-terraform-init-mobile.yml + tags: [mobile, init, terraform] + + - import_tasks: tasks/07-terraform-destroy-mobile.yml + tags: [mobile, destroy] diff --git a/config.example.pkl b/config.example.pkl index db35dee..8294f6a 100644 --- a/config.example.pkl +++ b/config.example.pkl @@ -126,12 +126,16 @@ deployment { } mobile { + // Set true to provision constructor-mobile during `mise run deploy`. + enabled = false + // Defaults are fine; uncomment to customize // nameSuffix = "prod" // customDomain = "gateway.example.com" // pushCron = new Listing { "*/2 * * * *" } - appJwtSigningKey = "REPLACE_ME_base64_from_openssl_rand_base64_32" // openssl rand -base64 32 + // Required only when enabled = true. + // appJwtSigningKey = "REPLACE_ME_base64_from_openssl_rand_base64_32" // openssl rand -base64 32 } // ─── Optional per-project overrides ─────────────────────────────────── diff --git a/config/render/mobile-inputs.pkl b/config/render/mobile-inputs.pkl index c23679c..50b404d 100644 --- a/config/render/mobile-inputs.pkl +++ b/config/render/mobile-inputs.pkl @@ -13,7 +13,11 @@ import "../../config.pkl" as c local m = c.mobile local d = c.deployment +local defaultAppJwtSigningKey = "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=" + local baseFields = new Mapping { + ["enabled"] = m.enabled + // Cloudflare (reused from b-a's config) ["cloudflare_account_id"] = d.cloudflare.accountId ["cloudflare_api_token"] = d.cloudflare.apiToken @@ -30,7 +34,7 @@ local baseFields = new Mapping { ["github_oauth_client_secret"] = d.github.app.clientSecret // Mobile-only secret - ["app_jwt_signing_key"] = m.appJwtSigningKey + ["app_jwt_signing_key"] = if (m.appJwtSigningKey != null) m.appJwtSigningKey else defaultAppJwtSigningKey // Shared secret: must match b-a's value ["internal_callback_secret"] = d.security.internalCallbackSecret diff --git a/config/schema/Root.pkl b/config/schema/Root.pkl index 2e67619..442185b 100644 --- a/config/schema/Root.pkl +++ b/config/schema/Root.pkl @@ -9,7 +9,7 @@ import "../defaults/projects.pkl" as ProjectDefaults projects: Mapping = ProjectDefaults.projects deployment: Deployment.Deployment -mobile: Mobile.Mobile +mobile: Mobile.Mobile = new {} state: State.State local sandboxError: String? = @@ -43,6 +43,11 @@ local githubBotError: String? = "github.bot is enabled but webhookSecret or username is missing" else null +local mobileError: String? = + if (mobile.enabled && mobile.appJwtSigningKey == null) + "mobile is enabled but appJwtSigningKey is missing" + else null + hidden unsupportedTweakcnThemeProjects: Listing = new Listing { for (key, value in projects.toMap()) { when (key != "background-agents" && value.tweakcnThemeUrl != null) { @@ -63,6 +68,7 @@ hidden _errors: List = List( webError, accessControlError, githubBotError, + mobileError, tweakcnThemeError ) diff --git a/config/schema/mobile/Mobile.pkl b/config/schema/mobile/Mobile.pkl index 40c0dc4..c48b294 100644 --- a/config/schema/mobile/Mobile.pkl +++ b/config/schema/mobile/Mobile.pkl @@ -6,6 +6,9 @@ typealias Base64_32 = String( ) class Mobile { + /// Set true to provision constructor-mobile during deploy. + enabled: Boolean = false + /// Suffix for resource names (e.g. "prod", "staging") nameSuffix: String(length > 0) = "prod" @@ -23,5 +26,5 @@ class Mobile { /// Gateway-owned key used to sign short-lived app-session JWTs. /// Generate: openssl rand -base64 32 - appJwtSigningKey: Base64_32 + appJwtSigningKey: Base64_32? = null }