Skip to content

Commit b7db6e5

Browse files
Merge pull request #2 from mikelv702/1-multiple-state-files
Multiple state files
2 parents a71755a + 1858846 commit b7db6e5

File tree

3 files changed

+85
-39
lines changed

3 files changed

+85
-39
lines changed

README.md

+3-10
Original file line numberDiff line numberDiff line change
@@ -40,22 +40,15 @@ fastapi dev main.py
4040
```hcl
4141
terraform {
4242
backend "http" {
43-
address = "http://localhost:8000/tfstate"
44-
lock_address = "http://localhost:8000/tfstate/lock"
45-
unlock_address = "http://localhost:8000/tfstate/lock"
43+
address = "http://localhost:8000/tfstate/${PROJECT_ID}"
44+
lock_address = "http://localhost:8000/tfstate/${PROJECT_ID}/lock"
45+
unlock_address = "http://localhost:8000/tfstate/${PROJECT_ID}/lock"
4646
}
4747
}
4848
```
4949

5050
3. Use Terraform as normal. The state will be stored and retrieved from your FastAPI backend.
5151

52-
## API ENDPOINTS
53-
54-
- POST /tfstate: Update Terraform state
55-
- GET /tfstate: Retrieve Terraform state
56-
- LOCK /tfstate/lock: Acquire a state lock
57-
- UNLOCK /tfstate/lock: Release a state lock
58-
- GET /tfstate/lock: Retrieve current lock info
5952

6053
## Note
6154

src/helpers/local_file_handler.py

+51-5
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,10 @@
77
settings.configure_logging()
88
logger = logging.getLogger(__name__)
99

10-
def save_file(json_data) -> None:
10+
def save_file(json_data, project_id) -> None:
1111
logger.debug("Saving to disk")
1212
try:
13-
directory_path = os.path.join(os.getcwd(), settings.FILE_STORAGE_PATH)
13+
directory_path = os.path.join(os.getcwd(), settings.FILE_STORAGE_PATH, project_id)
1414
if not os.path.exists(directory_path):
1515
os.makedirs(directory_path)
1616
logger.info(f"Created directory: {directory_path}")
@@ -21,13 +21,59 @@ def save_file(json_data) -> None:
2121
except FileNotFoundError:
2222
logger.warn(f"File not found {file_path}")
2323

24-
def read_file():
24+
def read_file(project_id):
2525
try:
26-
directory_path = os.path.join(os.getcwd(), settings.FILE_STORAGE_PATH)
26+
directory_path = os.path.join(os.getcwd(),
27+
settings.FILE_STORAGE_PATH,
28+
project_id)
2729
file_path = os.path.join(directory_path, 'data.json')
2830
f = open(file_path)
2931
json_data = json.load(f)
3032
return json_data
3133
except FileNotFoundError:
3234
print("File not found, returning empty")
33-
return None
35+
return None
36+
37+
def save_lock_file(project_id: int, lock):
38+
logger.debug("Saving lock to disk")
39+
try:
40+
directory_path = os.path.join(os.getcwd(),
41+
settings.FILE_STORAGE_PATH,
42+
str(project_id))
43+
if not os.path.exists(directory_path):
44+
os.makedirs(directory_path)
45+
logger.info(f"Created directory: {directory_path}")
46+
file_path = os.path.join(directory_path, 'lock.json')
47+
logger.info(f"Saving to {file_path}")
48+
with open(file_path, 'w', encoding='utf-8') as f:
49+
json.dump(lock, f, ensure_ascii=False)
50+
except FileNotFoundError:
51+
logger.warn(f"File not found {file_path}")
52+
53+
def read_lock_file(project_id):
54+
try:
55+
directory_path = os.path.join(os.getcwd(),
56+
settings.FILE_STORAGE_PATH,
57+
str(project_id))
58+
file_path = os.path.join(directory_path, 'lock.json')
59+
f = open(file_path)
60+
json_data = json.load(f)
61+
return json_data
62+
except FileNotFoundError:
63+
print("File not found, returning empty")
64+
return None
65+
66+
def delete_lock_file(project_id):
67+
try:
68+
directory_path = os.path.join(os.getcwd(),
69+
settings.FILE_STORAGE_PATH,
70+
str(project_id))
71+
file_path = os.path.join(directory_path, 'lock.json')
72+
os.remove(file_path)
73+
return True
74+
except FileNotFoundError:
75+
logger.warn("Lock not found")
76+
return True
77+
except Exception as e:
78+
logger.error(f"Unable to remove lock project: {project_id}, {e}")
79+
return False

src/main.py

+31-24
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,11 @@
66
from fastapi.responses import JSONResponse
77

88

9-
from .helpers.local_file_handler import save_file, read_file
9+
from .helpers.local_file_handler import (save_file,
10+
read_file,
11+
read_lock_file,
12+
delete_lock_file,
13+
save_lock_file)
1014

1115
from .settings import settings
1216

@@ -45,45 +49,41 @@ class TerraformState(BaseModel):
4549

4650
class Config:
4751
extra = "allow"
48-
@app.api_route("/tfstate/lock", methods=["LOCK"])
49-
async def lock_state(lock: LockInfo):
50-
global lock_info
52+
53+
@app.api_route("/tfstate/{project_id}/lock", methods=["LOCK"])
54+
async def lock_state(project_id: int, lock: LockInfo):
55+
lock_info = read_lock_file(project_id)
5156
if lock_info is None:
5257
lock_info = lock
58+
save_lock_file(project_id, lock_info.model_dump())
5359
return JSONResponse(content=lock.model_dump(), status_code=200)
5460
else:
5561
return JSONResponse(
5662
content={"error": "State already locked", "locked_by": lock_info.dict()},
5763
status_code=423
5864
)
5965

60-
@app.api_route("/tfstate/lock", methods=["UNLOCK"])
61-
async def unlock_state(lock: LockInfo):
62-
global lock_info
66+
@app.api_route("/tfstate/{project_id}/lock", methods=["UNLOCK"])
67+
async def unlock_state(project_id: int, lock: LockInfo):
68+
lock_info = read_lock_file(project_id)
6369
if lock_info is None:
6470
raise HTTPException(status_code=404, detail="State is not locked")
65-
if lock_info.ID != lock.ID:
71+
if lock_info["ID"] != lock.ID:
6672
raise HTTPException(status_code=403, detail="Lock ID does not match")
67-
lock_info = None
73+
delete_lock_file(project_id)
6874
return Response(status_code=200)
6975

70-
@app.get("/tfstate/lock")
71-
async def get_lock():
76+
@app.get("/tfstate/{project_id}/lock")
77+
async def get_lock(project_id: int):
78+
lock_info = read_lock_file(project_id)
7279
if lock_info is None:
7380
return Response(status_code=404)
7481
return JSONResponse(content=lock_info.model_dump(), status_code=200)
75-
76-
@app.post("/tfstate")
77-
async def update_terraform_state(ID: str, state: TerraformState):
78-
logging.info(f"Update state file {ID}")
79-
save_file(state.model_dump())
80-
logging.debug(state)
81-
return JSONResponse(content=state.model_dump())
82-
83-
@app.get("/tfstate")
84-
async def get_terraform_state():
85-
logging.info("Requesting State File")
86-
state_returned = read_file()
82+
83+
@app.get("/tfstate/{project_id}")
84+
async def get_terraform_state_project(project_id:int):
85+
logging.info(f"Requesting State File for project {project_id}")
86+
state_returned = read_file(str(project_id))
8787
if state_returned is None:
8888
dummy_state = TerraformState(
8989
version=4,
@@ -96,4 +96,11 @@ async def get_terraform_state():
9696
return dummy_state
9797
else:
9898
returned_state = TerraformState(**state_returned)
99-
return JSONResponse(content=returned_state.model_dump())
99+
return JSONResponse(content=returned_state.model_dump())
100+
101+
@app.post("/tfstate/{project_id}")
102+
async def update_terraform_state_project(project_id: int, state: TerraformState):
103+
logging.info(f"Update state file for project {project_id}")
104+
save_file(state.model_dump(), str(project_id))
105+
logging.debug(state)
106+
return JSONResponse(content=state.model_dump())

0 commit comments

Comments
 (0)