Skip to content

Commit e3f211e

Browse files
author
Gunther Klessinger
committed
feat flux install
with pass key insert when not existing, after create with age
1 parent 1a65bd5 commit e3f211e

12 files changed

+195
-24
lines changed

environ

+1
Original file line numberDiff line numberDiff line change
@@ -27,3 +27,4 @@ GITOPS_OWNER="company"
2727
GITOPS_PATH="clusters/staging"
2828
GITOPS_REPO="k8s"
2929
GITOPS_TOKEN="py:keyval"
30+
GITOPS_FLUX_PRIV_SECRET="py:keyval"

justfile

+2-2
Original file line numberDiff line numberDiff line change
@@ -53,8 +53,8 @@ port-forward:
5353
just p do port_forward
5454

5555

56-
install-gitops:
57-
just p gitops install
56+
install-flux:
57+
just p flux install
5858

5959

6060
test:

keyval.py

+3
Original file line numberDiff line numberDiff line change
@@ -19,3 +19,6 @@
1919
GITOPS_TOKEN = sec.get('GITOPS_TOKEN', '...')
2020
HCLOUD_TOKEN = sec.get('HCLOUD_TOKEN', '...')
2121
HCLOUD_TOKEN_WRITE = sec.get('HCLOUD_TOKEN_WRITE', '...')
22+
23+
# delivering a pass value is also possible:
24+
GITOPS_FLUX_PRIV_SECRET = sec.get('GITOPS_FLUX_PRIV_SECRET', 'pass:my/flux_priv_secret')

pyproject.toml

+1
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ extend-select = ["Q"]
5757
select = ["E", "F", "B"] # Enable flake8-bugbear (`B`) rules.
5858
ignore = [
5959
"E501", # Never enforce `E501` (line length violations).
60+
"E713", # not in condition
6061
"E741", # short var names
6162
"E731", # no lambda
6263
"B006", # mutables in signature

src/pyhk3/assets/kubectl.py

+19
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
T_NS = """
2+
---
3+
apiVersion: v1
4+
kind: Namespace
5+
metadata:
6+
name: "%(namespace)s"
7+
"""
8+
9+
10+
T_SECRET = """
11+
---
12+
apiVersion: v1
13+
kind: Secret
14+
metadata:
15+
name: "%(name)s"
16+
namespace: "%(namespace)s"
17+
data:
18+
%(data)s
19+
"""

src/pyhk3/cli.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
from inspect import signature as sig
55
from functools import partial
66
from .create import create, hk3s
7-
from .gitops import gitops
7+
from .flux import flux
88
from .do import do, recover
99
from rich.console import Console
1010
from rich.tree import Tree
@@ -17,7 +17,7 @@ class pyhk3:
1717
do = do
1818
recover = recover
1919
hk3s = hk3s
20-
gitops = gitops
20+
flux = flux
2121

2222

2323
console = Console()

src/pyhk3/defaults.py

+4-1
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,19 @@
11
binenv_custom_base = 'https://github.com/axgkl/binaries/raw/master'
22
dflt_img = 'ubuntu-24.04'
33
dflt_type = 'cx22'
4+
tools_proxy = 'helm 3.16.3 kubectl 1.31.3 hetzner-k3s 2.2.3 btop 1.4.0'
5+
tools_local = 'flux 2.4.0 yq 4.44.5 age-keygen 1.2.0 sops 3.9.1'
46

57

68
class envdefaults:
7-
BINENV_TOOLS_PROXY = 'helm 3.16.3 kubectl 1.31.3 hetzner-k3s 2.2.3 btop 1.4.0'
9+
BINENV_TOOLS_PROXY = tools_proxy
810
DNS_API_TOKEN = 'your-token-to-add-a-wildcard-dns-entry-at-provider'
911
DNS_PROVIDER = 'digitalocean' # see dns.py for others
1012
DNS_TTL = 60
1113
DOMAIN = 'k8s.mycompany.net'
1214
1315
FN_SSH_KEY = '$HOME/.ssh/hetzner-cluster' # created if not exists, also on hcloud
16+
GITOPS_FLUX_PRIV_SECRET = ''
1417
GITOPS_BRANCH = 'main'
1518
GITOPS_HOST = 'gitlab.mycompany.com'
1619
GITOPS_OWNER = 'company'

src/pyhk3/flux.py

+59
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
from .ssh import ensure_forward
2+
from .tools import run, log, die, need_env as E, read_file, env_key_on_missing
3+
from .tools import add_to_pass
4+
from .kubectl import ensure_namespace
5+
import os
6+
import sh
7+
# from kubernetes import client, config
8+
# # Load the kubeconfig file
9+
# config.load_kube_config()
10+
# # Create a V1Namespace object
11+
# namespace = client.V1Namespace(metadata=client.V1ObjectMeta(name='my-namespace'))
12+
# # Create the namespace using the CoreV1Api
13+
# v1 = client.CoreV1Api()
14+
# breakpoint() # FIXME BREAKPOINT
15+
# v1.create_namespace(namespace)
16+
17+
18+
def install():
19+
"""To uninstall use: flux uninstall."""
20+
host = E('GITOPS_HOST')
21+
os.environ['GITLAB_TOKEN'] = E('GITOPS_TOKEN')
22+
if not 'gitlab' in host:
23+
die('Only gitlab is supported at this time')
24+
ensure_forward()
25+
run('flux check --pre')
26+
ensure_namespace('flux-system')
27+
ns = ('--namespace', 'flux-system')
28+
have = sh.kubectl.get('secrets', *ns)
29+
if 'sops-age' in have:
30+
die('Secret already exists', hint='run: flux check, possibly then flux uninstall')
31+
S = 'AGE-SECRET-KEY'
32+
s = priv = E('GITOPS_FLUX_PRIV_SECRET', env_key_on_missing)
33+
if S in s:
34+
log.info('Using existing secret')
35+
else:
36+
log.warn('No GITOPS_FLUX_PRIV_SECRET - gen.ing new one using age')
37+
priv = sh.age_keygen().strip().split('\n')[-1]
38+
assert priv.startswith(S)
39+
40+
_ = '--from-file=age.agekey=/dev/stdin'
41+
sh.kubectl.create.secret.generic('sops-age', _, *ns, _in=priv)
42+
cmd = [
43+
'flux',
44+
'bootstrap',
45+
'gitlab',
46+
f'--owner={E("GITOPS_OWNER")}',
47+
f'--path={E("GITOPS_PATH")}',
48+
f'--repository={E("GITOPS_REPO")}',
49+
f'--hostname={host}',
50+
f'--branch={E("GITOPS_BRANCH")}',
51+
'--token-auth',
52+
]
53+
run(cmd)
54+
if s.startswith('pass:'):
55+
add_to_pass(s, priv)
56+
57+
58+
class flux:
59+
install = install

src/pyhk3/gitops.py

-7
This file was deleted.

src/pyhk3/kubectl.py

+17
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
from .assets.kubectl import T_NS, T_SECRET
2+
from .tools import run
3+
import sh
4+
5+
6+
def ensure_namespace(namespace: str):
7+
try:
8+
sh.kubectl('get', 'namespace', namespace)
9+
return
10+
except sh.ErrorReturnCode_1:
11+
n = T_NS % {'namespace': namespace}
12+
sh.kubectl('apply', '-f', '-', _in=n)
13+
14+
15+
def add_secret(name: str, namespace: str, data: str):
16+
breakpoint() # FIXME BREAKPOINT
17+
run(f'kubectl create namespace {name}')

src/pyhk3/ssh.py

+44-5
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,13 @@
11
from ipaddress import ip_address
22
from .hapi import ips
3-
from .tools import log, ssh, need_env as E
3+
from .tools import run, log, ssh, die, need_env as E
4+
from subprocess import Popen
45
from functools import partial
56
import sh
67

8+
from time import sleep
9+
import sys
10+
711

812
def ips_of_host(name):
913
"""name either ip or hostname"""
@@ -31,12 +35,36 @@ def ssh_add_no_hostkey_check(args):
3135
return
3236

3337

34-
def port_forward():
38+
def ensure_forward(_chck=False):
39+
m = f'{E("NAME")}-master1'
40+
try:
41+
r = run('kubectl get nodes', no_fail=True, capture_output=True).decode()
42+
if m in r:
43+
return True
44+
die('Wrong kubernetes cluster', notfound=m, output=r)
45+
except Exception as _:
46+
if _chck:
47+
return 0
48+
port_forward(nohup=True)
49+
for _ in range(10):
50+
sleep(1)
51+
print('.', file=sys.stderr, end='')
52+
if ensure_forward(_chck=True):
53+
print('')
54+
return
55+
die('No tunnel')
56+
57+
58+
def port_forward(nohup=False):
59+
log.info('Port forward', kubecfg_fwd_port=kubecfg_fwd_port)
60+
c = ['htop', '-d', '50']
61+
if nohup:
62+
c = ['sleep', '3600']
3563
fwd = f'{kubecfg_fwd_port}:127.0.0.1:6443'
36-
run_remote('master1', 'htop', '-d', '50', _term=True, _fwd=fwd)
64+
run_remote('master1', *c, _term=True, _fwd=fwd, _nohup=nohup)
3765

3866

39-
def run_remote(name, *cmd, _fwd=None, _term=False, _fg=True):
67+
def run_remote(name, *cmd, _fwd=None, _term=False, _fg=True, _nohup=False):
4068
"""ssh to servers, e.g. ssh proxy [cmd]. autovia via proxy built in."""
4169
log.debug('Run remote', name=name, cmd=cmd)
4270
ip_pub, ip = ips_of_host(name)
@@ -52,7 +80,18 @@ def run_remote(name, *cmd, _fwd=None, _term=False, _fg=True):
5280
args.insert(0, _fwd)
5381
args.insert(0, '-L')
5482
args.extend(list(cmd))
55-
return sh.ssh(args, _fg=_fg) # this can be redirected w/o probs
83+
try:
84+
if _nohup:
85+
ssh_command = ' '.join(f'"{i}"' for i in args)
86+
# sh.nohup not working
87+
return Popen(
88+
f'nohup ssh {ssh_command} > /dev/null 2> /dev/null &', shell=True
89+
)
90+
else:
91+
r = sh.ssh(args, _fg=_fg)
92+
except Exception as ex:
93+
die('ssh failed', name=name, cmd=cmd, ex=ex)
94+
return r
5695

5796

5897
kubecfg_fwd_port = int(E('HK_HOST_NETWORK')) + 6443

src/pyhk3/tools.py

+43-7
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import structlog
22
import os
33
import sys
4+
import sh
45
import json
56
import subprocess
67
import importlib.util
@@ -77,6 +78,9 @@ def pyval(k, fn, dflt=None):
7778
return v
7879

7980

81+
env_key_on_missing = '__env_key_on_missing__'
82+
83+
8084
def env(key, dflt=None):
8185
v = os.environ.get(key, nil)
8286
if v == nil:
@@ -87,12 +91,33 @@ def env(key, dflt=None):
8791
if str(v).startswith('pass:'):
8892
x = _secrets.get(v, nil)
8993
if x == nil:
90-
x = run(['pass', 'show', v[5:]], capture_output=True, text=True) or dflt
94+
try:
95+
x = pass_(key).show(v[5:], _err=[1, 2]).strip()
96+
except Exception as _:
97+
# special case: allows to calc and set the value later:
98+
if dflt == env_key_on_missing:
99+
return v
91100
_secrets[v] = x
92101
v = x
93102
return v
94103

95104

105+
def add_to_pass(key, val):
106+
log.warn('Adding key to pass', n=key[5:])
107+
pass_(key).insert('-m', key[5:], _in=val)
108+
if not need_env(key) == val:
109+
die('Failed to update pass', key=key[5:], value=val)
110+
111+
112+
def pass_(key=''):
113+
p = getattr(sh, 'pass', None)
114+
h = 'Install pass: https://www.passwordstore.org/'
115+
h += '- or supply a wrapper, supporting show and insert [-m] methods, e.g. for reading/writing files'
116+
if p is None:
117+
die('pass utility not found', hint=h, required_for=key)
118+
return p
119+
120+
96121
def dt(_, __, e):
97122
e['timestamp'] = f'{now() - T0:>4}'
98123
return e
@@ -112,40 +137,51 @@ def dt(_, __, e):
112137
log = structlog.get_logger()
113138

114139

115-
def die(msg, **kw):
140+
def die(msg, only_raise=False, **kw):
116141
log.fatal(msg, **kw)
142+
if only_raise:
143+
raise Exception(msg)
117144
sys.exit(1)
118145

119146

120-
def run(cmd, bg=False, **kw):
147+
def run(cmd, bg=False, no_fail=False, **kw):
148+
i = kw.get('input')
149+
if i is not None:
150+
kw['input'] = i.encode() if isinstance(i, str) else i
121151
if isinstance(cmd, str):
122152
cmd = cmd.split()
123153
pipe = kw.get('pipe', '')
124154
pipe = pipe if not len(pipe) > 20 else f'{pipe[:20]}...'
125-
log.debug('⚙️ Cmd', cmd=cmd, pipe=pipe)
155+
lw = {}
156+
if pipe:
157+
lw['pipe'] = pipe
158+
log.debug(f'⚙️ {" ".join(cmd)}', **lw)
126159
if bg:
127160
r = subprocess.Popen(cmd, start_new_session=True)
128161
# r.communicate()
129162
return r
130163

131164
r = subprocess.run(cmd, **kw)
132165
if r.returncode != 0:
133-
die('Command failed', cmd=cmd, returncode=r.returncode)
166+
die('Command failed', cmd=cmd, returncode=r.returncode, only_raise=no_fail)
134167
return r.stdout.strip() if r.stdout else ''
135168

136169

137-
def need_env(k, dflt=None):
170+
def need_env(k, dflt=None, _home_repl=False):
138171
v = env(k, dflt)
139172
if v is None:
140173
die(f'Missing env var ${k}')
174+
if _home_repl:
175+
for k in '~', '$HOME':
176+
v = v.replace(k, os.environ['HOME'])
141177
return v
142178

143179

144180
def ssh(ip, port=None, cmd=None, input=None, send_env=None, capture_output=True, **kw):
145181
cmd = cmd if cmd is not None else 'bash -s'
146182
port = port if port is not None else int(env('SSH_PORT', 22))
147183
c = f'ssh -p {port} -o StrictHostKeyChecking=accept-new -i'.split()
148-
cmd = c + [need_env('FN_SSH_KEY'), f'root@{ip}', cmd]
184+
cmd = c + [need_env('FN_SSH_KEY', _home_repl=True), f'root@{ip}', cmd]
149185
kw['capture_output'] = capture_output
150186
for key in send_env or []:
151187
cmd.insert(1, f'SendEnv={key}')

0 commit comments

Comments
 (0)