| 1 |
{% extends "base.html" %} |
| 2 |
{% import "repo_tabs.html" as m %} |
| 3 |
{% block title %}Terraform state · {{ repo.owner }}/{{ repo.name }}{% endblock %} |
| 4 |
{% block bodyattrs %} data-repo="{{ repo.url }}"{% endblock %} |
| 5 |
{% block tabs %}{% call m::tabs(repo, "terraform") %}{% endblock %} |
| 6 |
{% block content %} |
| 7 |
<div class="head"><h1>Terraform state <small>{{ states.len() }}</small></h1></div> |
| 8 |
<p>The hub keeps this repository's Terraform and OpenTofu state, sealed at rest, with its last {{ crate::terraform::VERSIONS_KEPT }} versions, outside the repository's history. Point a configuration at it with this backend block, naming the state in place of <code>NAME</code> (a repository may keep several):</p> |
| 9 |
<pre class="snippet">terraform { |
| 10 |
backend "http" { |
| 11 |
address = "{{ shell.base_url }}/api/v1/repos/{{ repo.owner }}/{{ repo.name }}/terraform/state/NAME" |
| 12 |
lock_address = "{{ shell.base_url }}/api/v1/repos/{{ repo.owner }}/{{ repo.name }}/terraform/state/NAME" |
| 13 |
unlock_address = "{{ shell.base_url }}/api/v1/repos/{{ repo.owner }}/{{ repo.name }}/terraform/state/NAME" |
| 14 |
username = "flow" |
| 15 |
} |
| 16 |
}</pre> |
| 17 |
<h2>From a pipeline</h2> |
| 18 |
<p>A job uses it with an identity token of this hub's, so it needs no cloud credential for the state. Give the job <code>identity "{{ shell.base_url }}/terraform" {% raw %}{{% endraw %} mount-path "/run/secrets/tf" {% raw %}}{% endraw %}</code>, run <code>export TF_HTTP_PASSWORD=$(cat /run/secrets/tf)</code> before <code>tofu init</code>, and let its sandbox reach this hub (<code>network "egress" {% raw %}{{% endraw %} allow "{{ shell.base_url|host }}" {% raw %}}{% endraw %}</code>, beside whatever else it reaches).</p> |
| 19 |
<h2>From your machine</h2> |
| 20 |
<p>Set <code>TF_HTTP_PASSWORD</code> to one of your tokens with <code>repo:write</code>, as someone with the write role here. Reading a state needs that too: states hold secrets.</p> |
| 21 |
<h2>States</h2> |
| 22 |
{% if let Some(n) = notice %}<p class="note">{{ n }}</p>{% endif %} |
| 23 |
{% if let Some(e) = error %}<p class="error">{{ e }}</p>{% endif %} |
| 24 |
{% if !repo.can(crate::auth::Access::Write) %}<p class="mute">The states are shown to people who can write to this repository.</p> |
| 25 |
{% else if states.is_empty() %}<p class="empty">No state yet: the first <code>tofu apply</code> through the backend makes one.</p>{% else %} |
| 26 |
<div class="table"><table> |
| 27 |
<tr><th>State</th><th>Serial</th><th>Last written</th><th>Lock</th><th>Versions kept</th><th></th></tr> |
| 28 |
{% for v in states %}<tr><td><strong>{{ v.s.name }}</strong></td><td>{% if v.s.body.is_empty() %}<span class="mute">deleted</span>{% else %}{{ v.s.serial }}{% endif %}</td><td class="small">{% if v.s.updated_by.is_empty() %}<span class="mute">never</span>{% else %}by {{ v.s.updated_by }} <time title="{{ v.s.updated_ms|when_i }}">{{ v.s.updated_ms|ago_i }}</time>{% endif %}</td><td class="small">{% if v.s.lock_id.is_empty() %}<span class="mute">free</span>{% else %}held by {{ v.s.locked_by }}{% if let Some(t) = v.s.locked_ms %} <time title="{{ t|when_i }}">{{ t|ago_i }}</time>{% endif %}{% endif %}</td><td>{{ v.versions.len() }}</td><td class="small">{% if !v.s.body.is_empty() %}<a href="{{ repo.url }}/terraform/{{ v.s.name|urlencode }}/download">download</a>{% endif %}</td></tr>{% endfor %} |
| 29 |
</table></div> |
| 30 |
{% for v in states %}{% if !v.versions.is_empty() %} |
| 31 |
<details class="tf-versions"> |
| 32 |
<summary><strong>{{ v.s.name }}</strong>: {{ v.versions.len() }} kept {% if v.versions.len() == 1 %}version{% else %}versions{% endif %}</summary> |
| 33 |
<p class="small mute">Each write keeps the state it replaced (the last {{ crate::terraform::VERSIONS_KEPT }}). Restoring one writes it again with the next serial, so OpenTofu and Terraform take it, and keeps the current state as a version: restoring never loses anything, and a later version can be restored in turn.</p> |
| 34 |
<div class="table"><table> |
| 35 |
<tr><th>Serial</th><th>Written by</th><th>When</th><th></th></tr> |
| 36 |
{% for k in v.versions %}<tr><td>{{ k.serial }}</td><td class="small">{{ k.created_by }}</td><td class="small"><time title="{{ k.created_ms|when_i }}">{{ k.created_ms|ago_i }}</time></td><td class="small"><a href="{{ repo.url }}/terraform/{{ v.s.name|urlencode }}/download?version={{ k.id }}">download</a>{% if repo.can_maintain() %} <form class="tiny" method="post" action="{{ repo.url }}/terraform/{{ v.s.name|urlencode }}/restore" data-confirm="Restore {{ v.s.name }} to serial {{ k.serial }}? It becomes the current state; the current one is kept as a version."><input type="hidden" name="_csrf" value="{{ shell.csrf }}"><input type="hidden" name="version" value="{{ k.id }}"><button>restore</button></form>{% endif %}</td></tr>{% endfor %} |
| 37 |
</table></div> |
| 38 |
</details> |
| 39 |
{% endif %}{% endfor %} |
| 40 |
{% endif %} |
| 41 |
{% endblock %} |