Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add secrets subcommand for CLI #73

Open
wants to merge 4 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions cli/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
# (usually `~/.cache/tenzir-platform`) needs to be mapped to an external volume,
# to be able to persist the login information between runs.

FROM python:3.11-slim as builder
FROM python:3.11-slim AS builder

RUN pip install poetry
RUN mkdir -p /app
Expand All @@ -15,7 +15,7 @@ WORKDIR /app
RUN poetry config virtualenvs.in-project true --local
RUN poetry install --without dev

FROM python:3.11-slim as base
FROM python:3.11-slim AS base

COPY --from=builder /app /app

Expand Down
64 changes: 34 additions & 30 deletions cli/poetry.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions cli/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ types-docopt = "^0.6.11.4"
pydantic-settings = "^2.2.1"
pyjwt = {extras = ["crypto"], version = "^2.8.0"}
pytimeparse2 = "^1.7.1"
cryptography = "^44.0.1"

[tool.poetry.group.dev.dependencies]
black = "^24.1.1"
Expand Down
3 changes: 2 additions & 1 deletion cli/tenzir_platform/helpers/oidc.py
Original file line number Diff line number Diff line change
Expand Up @@ -181,6 +181,7 @@ def load_id_token(self, interactive: bool = True) -> str:
token = f.read()
self.validate_token(token)
return token
except Exception:
except Exception as e:
print(e)
print("could not load valid token from cache, reauthenticating")
return self.reauthenticate_token(interactive=interactive)
1 change: 1 addition & 0 deletions cli/tenzir_platform/subcommand_admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
tenzir-platform admin update-workspace <workspace_id> [--name=<workspace_name>] [--icon-url=<icon_url>] [--owner-namespace=<namespace>] [--owner-id=<owner_id>] [--category=<workspace_category>]
tenzir-platform admin list-global-workspaces
tenzir-platform admin spawn-node <workspace_id> <image> [--lifetime=<lifetime>]
tenzir-platform admin change-secret-store <workspace_id> <store_type> <store_config>

Options:
-d,--dry-run Do not add the rule, only print a JSON representation.
Expand Down
156 changes: 156 additions & 0 deletions cli/tenzir_platform/subcommand_secret.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
# SPDX-FileCopyrightText: (c) 2024 The Tenzir Contributors
# SPDX-License-Identifier: BSD-3-Clause

"""Usage:
tenzir-platform secret set <name> [--file=<file>] [--value=<value>] [--env]
tenzir-platform secret delete <name>
tenzir-platform secret list

tenzir-platform secret add-store aws --region=<region> --assumed-role-arn=<assumed_role_arn> [--name=<name>]
tenzir-platform secret delete-store <store>
tenzir-platform secret list-stores

Options:
<name> The name of the secret.

Description:
tenzir-platform secret set <name> [--file <file>] [--value <value>] [--env]
Add or update a secret. By default, reads the secret value
interactively from stdin.
The `--file` option can be used to read the secret value from a file instead.
The `--value` option can be used to pass the secret value as a command-line argument instead.
The `--env` option can be used to read the secret value from the environment variable with the same name.
Only one of these options can be specified.

tenzir-platform secret delete <name>
Delete the specified secret.

tenzir-platform secret list
List all configured secrets.
"""

# TODO: We probably also want to add the equivalent of these options (from `gh secret`)
# in the future.
# -o, --org organization Set organization secret
# -r, --repos repositories List of repositories that can access an organization or user secret
# -u, --user Set a secret for your user
# -v, --visibility string Set visibility for an organization secret: {all|private|selected} (default "private")

from tenzir_platform.helpers.cache import load_current_workspace
from tenzir_platform.helpers.client import AppClient
from tenzir_platform.helpers.environment import PlatformEnvironment
from pydantic import BaseModel
from docopt import docopt
from typing import Optional, List
from requests import HTTPError
from pytimeparse2 import parse as parse_duration
import json
import time
import tempfile
import os
import subprocess
import re
import random
import datetime

def set(
client: AppClient, workspace_id: str, name: str, file: Optional[str], value: Optional[str], env: bool = False
):
if sum(bool(x) for x in [file, value, env]) > 1:
print("Error: Only one of --file, --value, or --env can be specified.")
return

secret_value = None
if file:
with open(file, 'r') as f:
secret_value = f.read().strip()
elif value:
secret_value = value
elif env:
secret_value = os.getenv(name)
else:
secret_value = input("Enter secret value: ").strip()

resp = client.post(
"secrets/upsert",
json={
"tenant_id": workspace_id,
"name": name,
"value": secret_value,
},
)
resp.raise_for_status()
print(json.dumps(resp.json()))


def delete(client: AppClient, workspace_id: str, name: str):
resp = client.post(
"secrets/remove",
json={
"tenant_id": workspace_id,
"secret_id": name,
},
)
resp.raise_for_status()
print(f"deleted secret {name}")


def list(
client: AppClient,
workspace_id: str,
):
resp = client.post(
"secrets/list",
json={
"tenant_id": workspace_id,
},
)
resp.raise_for_status()
secrets = resp.json()["secrets"]
if len(secrets) == 0:
print("no secrets configured")
return
print("Secrets")
for secret in secrets:
name = secret['name']
print(f"{name}")


def secret_subcommand(platform: PlatformEnvironment, argv):
args = docopt(__doc__, argv=argv)
try:
workspace_id, user_key = load_current_workspace(platform)
client = AppClient(platform=platform)
client.workspace_login(user_key)
except Exception as e:
print(f"error: {e}")
print(
"Failed to load current workspace, please run 'tenzir-platform workspace select' first"
)
exit(1)

try:
if args["set"]:
name = args["<name>"]
file = args["--file"]
value = args["--value"]
env = args["--env"]
set(client, workspace_id, name, file, value, env)
elif args["delete"]:
name = args["<name>"]
delete(client, workspace_id, name)
elif args["list"]:
list(client, workspace_id)
elif args["add-store"]:
pass # FIXME
elif args["delete-store"]:
pass # FIXME
elif args["list-stores"]:
pass # FIXME
except HTTPError as e:
if e.response.status_code == 403:
print(
"Access denied. Please try re-authenticating by running 'tenzir-platform workspace select'"
)
else:
print(f"Error communicating with the platform: {e}")
4 changes: 4 additions & 0 deletions cli/tenzir_platform/tenzir_platform.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
node Interact with nodes
alert Configure alerts for disconnected nodes.
admin Administer local on-prem platform infrastructure.
secret Manage secrets.

See 'tenzir-platform <command> --help' for more information on a specific command.
"""
Expand All @@ -34,6 +35,7 @@
from tenzir_platform.subcommand_workspace import workspace_subcommand
from tenzir_platform.subcommand_node import node_subcommand
from tenzir_platform.subcommand_admin import admin_subcommand
from tenzir_platform.subcommand_secret import secret_subcommand
from tenzir_platform.helpers.environment import PlatformEnvironment

version = importlib.metadata.version("tenzir-platform")
Expand All @@ -57,6 +59,8 @@ def main():
node_subcommand(platform, argv)
elif arguments["<command>"] == "admin":
admin_subcommand(platform, argv)
elif arguments["<command>"] == "secret":
secret_subcommand(platform, argv)
else:
print("unknown subcommand, see 'tenzir-platform --help' for usage")

Expand Down
2 changes: 1 addition & 1 deletion components/tenant-manager