Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1c1f2b1d44 |
No files matched your search
+108
@@ -0,0 +1,108 @@
|
|||||||
|
package e2e
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"crypto/ed25519"
|
||||||
|
"crypto/rand"
|
||||||
|
"fmt"
|
||||||
|
"net"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"git.veillax.com/Veillax/bamboo/sshd"
|
||||||
|
"git.veillax.com/Veillax/blacktip"
|
||||||
|
"git.veillax.com/Veillax/reef/model"
|
||||||
|
"golang.org/x/crypto/ssh"
|
||||||
|
)
|
||||||
|
|
||||||
|
func newSSHSigner(t *testing.T) ssh.Signer {
|
||||||
|
t.Helper()
|
||||||
|
_, priv, err := ed25519.GenerateKey(rand.Reader)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
s, err := ssh.NewSignerFromKey(priv)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
// A TCP port on the edge whose target is Bamboo's SSH server at the site reaches that server
|
||||||
|
// with an ordinary ssh client: the path is client, edge, WireGuard tunnel, Bamboo, sshd.
|
||||||
|
func TestSSHThroughTheEdgeToBamboosServer(t *testing.T) {
|
||||||
|
w := setup(t, blacktip.TraefikConfig{})
|
||||||
|
dir := t.TempDir()
|
||||||
|
signer := newSSHSigner(t)
|
||||||
|
keys := filepath.Join(dir, "authorized_keys")
|
||||||
|
if err := os.WriteFile(keys, ssh.MarshalAuthorizedKey(signer.PublicKey()), 0o600); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
srv, err := sshd.New(sshd.Config{AuthorizedKeys: keys, HostKey: filepath.Join(dir, "ssh_host_ed25519_key")})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
ln, err := net.Listen("tcp", "127.0.0.1:0")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
|
t.Cleanup(cancel)
|
||||||
|
go srv.Serve(ctx, ln)
|
||||||
|
|
||||||
|
edgePort := freePort(t)
|
||||||
|
w.open(model.Mapping{Proto: model.ProtoTCP, Port: edgePort, Name: "ssh", Targets: tgt(w.site, ln.Addr().(*net.TCPAddr).Port)})
|
||||||
|
addr := "127.0.0.1:" + strconv.Itoa(edgePort)
|
||||||
|
|
||||||
|
connect := func(auth ssh.AuthMethod) (*ssh.Client, error) {
|
||||||
|
return ssh.Dial("tcp", addr, &ssh.ClientConfig{User: "alex", Auth: []ssh.AuthMethod{auth}, Timeout: 5 * time.Second,
|
||||||
|
// the key the client sees through the tunnel must be Bamboo's, not something in between
|
||||||
|
HostKeyCallback: func(_ string, _ net.Addr, k ssh.PublicKey) error {
|
||||||
|
if got := ssh.FingerprintSHA256(k); got != srv.Fingerprint() {
|
||||||
|
return fmt.Errorf("host key %s is not the Bamboo server's %s", got, srv.Fingerprint())
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}})
|
||||||
|
}
|
||||||
|
var client *ssh.Client
|
||||||
|
var last error
|
||||||
|
for i := 0; i < 60 && client == nil; i++ {
|
||||||
|
if client, last = connect(ssh.PublicKeys(signer)); client == nil {
|
||||||
|
time.Sleep(250 * time.Millisecond)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if client == nil {
|
||||||
|
t.Fatalf("could not sign in through the edge: %v", last)
|
||||||
|
}
|
||||||
|
defer client.Close()
|
||||||
|
|
||||||
|
if sshd.Supported {
|
||||||
|
sess, err := client.NewSession()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
out, err := sess.Output("echo through-the-tunnel")
|
||||||
|
sess.Close()
|
||||||
|
if err != nil || strings.TrimSpace(string(out)) != "through-the-tunnel" {
|
||||||
|
t.Fatalf("running a command: %q, %v", out, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// the same narrow rules hold through the tunnel
|
||||||
|
if c, err := connect(ssh.PublicKeys(newSSHSigner(t))); err == nil {
|
||||||
|
c.Close()
|
||||||
|
t.Error("a key that is not in authorized_keys signed in")
|
||||||
|
}
|
||||||
|
if c, err := connect(ssh.Password("hunter2")); err == nil {
|
||||||
|
c.Close()
|
||||||
|
t.Error("a password signed in")
|
||||||
|
}
|
||||||
|
if c, err := client.Dial("tcp", "127.0.0.1:1"); err == nil {
|
||||||
|
c.Close()
|
||||||
|
t.Error("port forwarding through the SSH server is allowed")
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,7 +3,7 @@ module git.veillax.com/Veillax/hammerhead
|
|||||||
go 1.27.3
|
go 1.27.3
|
||||||
|
|
||||||
require (
|
require (
|
||||||
git.veillax.com/Veillax/bamboo v0.1.7
|
git.veillax.com/Veillax/bamboo v0.1.8
|
||||||
git.veillax.com/Veillax/blacktip v0.1.10
|
git.veillax.com/Veillax/blacktip v0.1.10
|
||||||
git.veillax.com/Veillax/reef v0.1.11
|
git.veillax.com/Veillax/reef v0.1.11
|
||||||
github.com/emersion/go-msgauth v0.7.0
|
github.com/emersion/go-msgauth v0.7.0
|
||||||
@@ -13,6 +13,7 @@ require (
|
|||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/Microsoft/go-winio v0.6.4 // indirect
|
github.com/Microsoft/go-winio v0.6.4 // indirect
|
||||||
|
github.com/creack/pty v1.1.24 // indirect
|
||||||
github.com/dustin/go-humanize v1.1.0 // indirect
|
github.com/dustin/go-humanize v1.1.0 // indirect
|
||||||
github.com/google/btree v1.1.2 // indirect
|
github.com/google/btree v1.1.2 // indirect
|
||||||
github.com/google/uuid v1.6.0 // indirect
|
github.com/google/uuid v1.6.0 // indirect
|
||||||
|
|||||||
@@ -1,11 +1,13 @@
|
|||||||
git.veillax.com/Veillax/bamboo v0.1.7 h1:men+7c+MyAbpQmNZKxwdO7yPdRgLtec2KuU4EQ4/EFA=
|
git.veillax.com/Veillax/bamboo v0.1.8 h1:mOdkGBzM8CjFVjR3784/WAYGt3U4bSdOaAJqFWMVh3I=
|
||||||
git.veillax.com/Veillax/bamboo v0.1.7/go.mod h1:hUMWRFuDIiTNaqjpx2F7RxOg7ussrbVZAQIhzhPF780=
|
git.veillax.com/Veillax/bamboo v0.1.8/go.mod h1:hUMWRFuDIiTNaqjpx2F7RxOg7ussrbVZAQIhzhPF780=
|
||||||
git.veillax.com/Veillax/blacktip v0.1.10 h1:2e1wUbMCp4z/DvQjAJCRmLyqu4PX+qV3mo5WBliqGiE=
|
git.veillax.com/Veillax/blacktip v0.1.10 h1:2e1wUbMCp4z/DvQjAJCRmLyqu4PX+qV3mo5WBliqGiE=
|
||||||
git.veillax.com/Veillax/blacktip v0.1.10/go.mod h1:tarAhdKU9hx5pdufe9wG3dRWYkWgPYZvawKM5DWgt6o=
|
git.veillax.com/Veillax/blacktip v0.1.10/go.mod h1:tarAhdKU9hx5pdufe9wG3dRWYkWgPYZvawKM5DWgt6o=
|
||||||
git.veillax.com/Veillax/reef v0.1.11 h1:kbjgxJpmpp1HRckbW1tqJUm5/ftgvSlmUHAhFXugOlY=
|
git.veillax.com/Veillax/reef v0.1.11 h1:kbjgxJpmpp1HRckbW1tqJUm5/ftgvSlmUHAhFXugOlY=
|
||||||
git.veillax.com/Veillax/reef v0.1.11/go.mod h1:ZBUd/BARO5kkpUNTP81TTY1aQXLHFR6bKDhCxckjHso=
|
git.veillax.com/Veillax/reef v0.1.11/go.mod h1:ZBUd/BARO5kkpUNTP81TTY1aQXLHFR6bKDhCxckjHso=
|
||||||
github.com/Microsoft/go-winio v0.6.4 h1:kJF2pamtYCWXf0xAT8sAkwKC0pMl2HYBrRPHAqyUc+g=
|
github.com/Microsoft/go-winio v0.6.4 h1:kJF2pamtYCWXf0xAT8sAkwKC0pMl2HYBrRPHAqyUc+g=
|
||||||
github.com/Microsoft/go-winio v0.6.4/go.mod h1:1xl5JXG4ze/m74GH6Dc4Mwxfc3VL5siRVQjIAlHH07U=
|
github.com/Microsoft/go-winio v0.6.4/go.mod h1:1xl5JXG4ze/m74GH6Dc4Mwxfc3VL5siRVQjIAlHH07U=
|
||||||
|
github.com/creack/pty v1.1.24 h1:bJrF4RRfyJnbTJqzRLHzcGaZK1NeM5kTC9jGgovnR1s=
|
||||||
|
github.com/creack/pty v1.1.24/go.mod h1:08sCNb52WyoAwi2QDyzUCTgcvVFhUzewun7wtTfvcwE=
|
||||||
github.com/dustin/go-humanize v1.1.0 h1:dbKTrvD0klcbBV/h4AWJdMuZogJACoMlvWIWZ5b2xWg=
|
github.com/dustin/go-humanize v1.1.0 h1:dbKTrvD0klcbBV/h4AWJdMuZogJACoMlvWIWZ5b2xWg=
|
||||||
github.com/dustin/go-humanize v1.1.0/go.mod h1:hc1CvRkJMsgxqjmjMQF3QNRAZBwY8AXBAzKYoSX9sFI=
|
github.com/dustin/go-humanize v1.1.0/go.mod h1:hc1CvRkJMsgxqjmjMQF3QNRAZBwY8AXBAzKYoSX9sFI=
|
||||||
github.com/emersion/go-msgauth v0.7.0 h1:vj2hMn6KhFtW41kshIBTXvp6KgYSqpA/ZN9Pv4g1INc=
|
github.com/emersion/go-msgauth v0.7.0 h1:vj2hMn6KhFtW41kshIBTXvp6KgYSqpA/ZN9Pv4g1INc=
|
||||||
@@ -34,6 +36,8 @@ golang.org/x/sync v0.24.0 h1:iqiDrGoDgyuvAKrhLQthUEzU/R378N78gXbBjxed7oA=
|
|||||||
golang.org/x/sync v0.24.0/go.mod h1:sUUOizhqBxiL6pEWpqNLUiaJn1ShEbZ6BBqskPbjZm0=
|
golang.org/x/sync v0.24.0/go.mod h1:sUUOizhqBxiL6pEWpqNLUiaJn1ShEbZ6BBqskPbjZm0=
|
||||||
golang.org/x/sys v0.49.0 h1:XbzkgYJHdqh/8m2Uu0W/dQv8nktxx4BFHp1M0gROTXA=
|
golang.org/x/sys v0.49.0 h1:XbzkgYJHdqh/8m2Uu0W/dQv8nktxx4BFHp1M0gROTXA=
|
||||||
golang.org/x/sys v0.49.0/go.mod h1:hNLxWAXmnKAxqDtdwIYC4bM9oQPEecfsnNMuSxOs3og=
|
golang.org/x/sys v0.49.0/go.mod h1:hNLxWAXmnKAxqDtdwIYC4bM9oQPEecfsnNMuSxOs3og=
|
||||||
|
golang.org/x/term v0.47.0 h1:cpyOwQkr2Nzxe2p6KnrTG7Ei2wtzjsuSjhpXXepZ/BM=
|
||||||
|
golang.org/x/term v0.47.0/go.mod h1:efAfAuG6RKABvg1ArGebRkvX6LXul14SM67o2e4mUCY=
|
||||||
golang.org/x/text v0.43.0 h1:1QivrlhwAsnMOcqgOdl9my7s9OyZl8O5/UGUTJVXoWc=
|
golang.org/x/text v0.43.0 h1:1QivrlhwAsnMOcqgOdl9my7s9OyZl8O5/UGUTJVXoWc=
|
||||||
golang.org/x/text v0.43.0/go.mod h1:hbSIYA/amXcRXTFSZqh8tWHEoqRvvbKPJ2uz/KU+a+s=
|
golang.org/x/text v0.43.0/go.mod h1:hbSIYA/amXcRXTFSZqh8tWHEoqRvvbKPJ2uz/KU+a+s=
|
||||||
golang.org/x/time v0.7.0 h1:ntUhktv3OPE6TgYxXWv9vKvUSJyIFJlyohwbkEwPrKQ=
|
golang.org/x/time v0.7.0 h1:ntUhktv3OPE6TgYxXWv9vKvUSJyIFJlyohwbkEwPrKQ=
|
||||||
|
|||||||
+77
-6
@@ -102,6 +102,8 @@
|
|||||||
.grid > label.chk { margin-top:calc(var(--text-sm) * var(--leading) + var(--s1) + var(--s2)); }
|
.grid > label.chk { margin-top:calc(var(--text-sm) * var(--leading) + var(--s1) + var(--s2)); }
|
||||||
.sect-h { padding:var(--s3) var(--s4); font-weight:600; border-bottom:var(--border-w) solid var(--border); }
|
.sect-h { padding:var(--s3) var(--s4); font-weight:600; border-bottom:var(--border-w) solid var(--border); }
|
||||||
.scroll { overflow-x:auto; }
|
.scroll { overflow-x:auto; }
|
||||||
|
td.wrap { overflow-wrap:anywhere; min-width:150px; max-width:260px; }
|
||||||
|
.ports { display:flex; flex-wrap:wrap; gap:var(--s1); }
|
||||||
.actions { display:flex; gap:var(--s2); flex-wrap:wrap; }
|
.actions { display:flex; gap:var(--s2); flex-wrap:wrap; }
|
||||||
.form-actions { justify-content:flex-end; margin-top:var(--s2); }
|
.form-actions { justify-content:flex-end; margin-top:var(--s2); }
|
||||||
.toolbar { display:flex; align-items:center; gap:var(--s3); margin-bottom:var(--s3); flex-wrap:wrap; }
|
.toolbar { display:flex; align-items:center; gap:var(--s3); margin-bottom:var(--s3); flex-wrap:wrap; }
|
||||||
@@ -179,7 +181,7 @@
|
|||||||
@keyframes shimmer { to { background-position:-200% 0; } }
|
@keyframes shimmer { to { background-position:-200% 0; } }
|
||||||
|
|
||||||
/* dialogs, tabs, toasts */
|
/* dialogs, tabs, toasts */
|
||||||
.shade { position:fixed; inset:0; background:var(--overlay); display:grid; place-items:start center; padding:var(--s7) var(--s4); overflow:auto; z-index:var(--z-overlay); }
|
.shade { position:fixed; inset:0; background:var(--overlay); display:grid; grid-template-columns:minmax(0,1fr); place-items:start center; padding:var(--s7) var(--s4); overflow:auto; z-index:var(--z-overlay); }
|
||||||
.modal { background:var(--surface); border-radius:var(--radius-lg); box-shadow:var(--shadow); padding:var(--s5); width:min(480px,100%); }
|
.modal { background:var(--surface); border-radius:var(--radius-lg); box-shadow:var(--shadow); padding:var(--s5); width:min(480px,100%); }
|
||||||
.modal.wide { width:min(780px,100%); }
|
.modal.wide { width:min(780px,100%); }
|
||||||
.modal h2 { margin:0 0 var(--s3); font-size:var(--text-lg); }
|
.modal h2 { margin:0 0 var(--s3); font-size:var(--text-lg); }
|
||||||
@@ -1028,6 +1030,77 @@ async function shareDialog(m) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ----- sites -----
|
// ----- sites -----
|
||||||
|
// What an agent reports about its network: Docker containers (-docker) and listening services (-discover).
|
||||||
|
async function siteFound(s) {
|
||||||
|
const [list, found] = await Promise.all([api('GET', O(`/sites/${s.id}/containers`)), api('GET', O(`/sites/${s.id}/discovered`))]);
|
||||||
|
// a published port is "8777→8777"; TCP and UDP on the same port share one entry
|
||||||
|
const portChips = ports => {
|
||||||
|
const by = new Map();
|
||||||
|
for (const p of ports) { const k = (p.public ? p.public + '→' : '') + p.private; by.set(k, [...(by.get(k) || []), p.type]); }
|
||||||
|
return h('div', {class:'ports'}, [...by].map(([k, types]) => h('span', {class:'tag mono'}, k + '/' + [...new Set(types)].join(','))));
|
||||||
|
};
|
||||||
|
const section = (title, count, body, empty) => h('div', {}, h('h3', {style:'margin:0 0 var(--s2)'}, title + (count ? ` (${count})` : '')), count ? h('div', {class:'scroll'}, body) : h('p', {class:'dim', style:'margin:0'}, empty));
|
||||||
|
const containers = h('table', {}, h('thead', {}, h('tr', {}, ['Name', 'Image', 'Ports'].map(t => h('th', {}, t)))),
|
||||||
|
h('tbody', {}, list.map(c => h('tr', {}, h('td', {class:'wrap'}, c.name), h('td', {class:'dim wrap'}, c.image), h('td', {}, portChips(c.ports))))));
|
||||||
|
const services = h('table', {}, h('thead', {}, h('tr', {}, ['Service', 'Address', 'Found', 'Detail'].map(t => h('th', {}, t)))),
|
||||||
|
h('tbody', {}, found.map(d => h('tr', {}, h('td', {}, d.service || 'tcp'), h('td', {class:'mono small'}, d.host + ':' + d.port),
|
||||||
|
h('td', {class:'dim'}, d.source === 'local' ? 'Listening here' : 'Network scan'), h('td', {class:'dim small wrap'}, d.detail || '')))));
|
||||||
|
const close = modal('Found by ' + s.name, h('div', {class:'stack'},
|
||||||
|
h('p', {class:'dim', style:'margin:0'}, 'These show up as suggestions when you pick a target for a resource. Nothing is shared until you choose it.'),
|
||||||
|
section('Docker containers', list.length, containers, 'None reported. Start the agent with -docker (BAMBOO_DOCKER=1, or BLACKTIP_DOCKER=1 for an edge) on a machine where Docker is running.'),
|
||||||
|
section('Services on the network', found.length, services, 'None found. Start the agent with -discover (BAMBOO_DISCOVER=1) for services listening on its machine, and add -discover-subnets (BAMBOO_DISCOVER_SUBNETS) to scan other addresses.'),
|
||||||
|
h('div', {class:'actions form-actions'}, h('button', {class:'btn', onclick:() => close()}, 'Close'))), {wide:true});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Bamboo's built-in SSH server (started with -ssh) listens here on the site by default.
|
||||||
|
const SSH_TARGET = '127.0.0.1:2222';
|
||||||
|
|
||||||
|
// Reach a site's Bamboo SSH server with an ordinary ssh client: a TCP port on an edge whose target is that server.
|
||||||
|
function sshAccess(s) {
|
||||||
|
const st = S.st;
|
||||||
|
const [tHost, tPort] = [SSH_TARGET.split(':')[0], +SSH_TARGET.split(':')[1]];
|
||||||
|
const isSSH = m => m.proto === 'tcp' && !m.redirectTo && m.targets.some(t => t.siteId === s.id && (t.port === tPort || (m.tags || []).includes('ssh')));
|
||||||
|
let ports = st.mappings.filter(isSSH);
|
||||||
|
// what to type after the @: a domain pointed at the address when there is one, else the address itself
|
||||||
|
const edgeHost = (e, m) => {
|
||||||
|
if (m.bindIp && m.bindIp !== '0.0.0.0') { const d = st.domains.find(x => x.ip === m.bindIp); return d ? d.name : m.bindIp; }
|
||||||
|
const d = st.domains.find(x => x.ip && (!x.edgeId || x.edgeId === m.edgeId));
|
||||||
|
return d ? d.name : ((e && e.ips) || [])[0] || 'your-edge-address';
|
||||||
|
};
|
||||||
|
const cmds = h('div', {class:'stack'});
|
||||||
|
const connectBox = h('div', {}, h('h3', {style:'margin:0 0 var(--s2)'}, 'Connect with'), cmds, h('p', {class:'dim small'}, 'The user name is ignored: you get a shell as the user Bamboo runs as.'));
|
||||||
|
const draw = () => {
|
||||||
|
connectBox.style.display = ports.length ? '' : 'none';
|
||||||
|
cmds.replaceChildren(...ports.map(m => {
|
||||||
|
const cmd = `ssh -p ${m.port} you@${edgeHost(byId(st.edges, m.edgeId), m)}`;
|
||||||
|
return h('div', {}, h('div', {class:'small dim'}, m.name + (m.enabled ? '' : ' (paused)')), cmdBox(cmd),
|
||||||
|
h('button', {class:'ghost', type:'button', onclick: act(async () => { await navigator.clipboard.writeText(cmd); toast('Copied'); })}, 'Copy'));
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
draw();
|
||||||
|
const edgeSel = h('select', {}, st.edges.map(e => h('option', {value:e.id}, e.name + (e.online ? '' : ' (offline)'))));
|
||||||
|
const ipSel = h('select', {});
|
||||||
|
const refreshIPs = () => { const e = byId(st.edges, edgeSel.value); ipSel.replaceChildren(h('option', {value:'0.0.0.0'}, 'Every address on the edge'), ...((e && e.ips) || []).map(ip => h('option', {value:ip}, ip))); };
|
||||||
|
edgeSel.onchange = refreshIPs; refreshIPs();
|
||||||
|
const port = h('input', {type:'number', min:1, max:65535, value:2222, placeholder:'2222'});
|
||||||
|
const target = h('input', {value:SSH_TARGET});
|
||||||
|
const create = act(async () => {
|
||||||
|
const [h2, p2] = target.value.trim().split(':');
|
||||||
|
if (!st.edges.length) throw new Error('Add an edge first');
|
||||||
|
const m = await api('POST', O('/mappings'), {name:'SSH to ' + s.name, proto:'tcp', edgeId:edgeSel.value, bindIp:ipSel.value || '0.0.0.0', port:+port.value, tags:['ssh'], enabled:true,
|
||||||
|
targets:[{siteId:s.id, host:h2 || tHost, port:+p2 || tPort}]});
|
||||||
|
ports = [...ports, m]; draw(); toast('SSH port opened'); refresh();
|
||||||
|
});
|
||||||
|
const close = modal('SSH into ' + s.name, h('div', {class:'stack'},
|
||||||
|
h('p', {class:'dim', style:'margin:0'}, 'Bamboo has a small SSH server, so you can open a shell on this machine from anywhere without installing sshd. Start Bamboo with -ssh (BAMBOO_SSH=1) and put the public keys that may sign in in authorized_keys in its data folder. Only keys are accepted, never passwords.'),
|
||||||
|
connectBox,
|
||||||
|
h('fieldset', {}, h('legend', {}, 'Open an SSH port'), h('div', {class:'stack'},
|
||||||
|
h('div', {class:'grid'}, field('Edge', edgeSel), field('Public IP', ipSel), field('Public port', port, 'Anyone can reach it, but only listed keys sign in')),
|
||||||
|
field('Bamboo\'s SSH server on the site', target, 'Leave as is unless you changed -ssh-listen'),
|
||||||
|
h('div', {class:'actions form-actions'}, h('button', {class:'btn', type:'button', onclick:create}, 'Open SSH port')))),
|
||||||
|
h('div', {class:'actions form-actions'}, h('button', {class:'btn', onclick:() => close()}, 'Close'))), {wide:true});
|
||||||
|
}
|
||||||
|
|
||||||
views.sites = async () => {
|
views.sites = async () => {
|
||||||
const st = S.st, name = h('input', {placeholder:'e.g. Home lab'});
|
const st = S.st, name = h('input', {placeholder:'e.g. Home lab'});
|
||||||
const add = act(async () => { const s = await api('POST', O('/sites'), {name:name.value}); name.value = ''; showCmd(s); refresh(); });
|
const add = act(async () => { const s = await api('POST', O('/sites'), {name:name.value}); name.value = ''; showCmd(s); refresh(); });
|
||||||
@@ -1047,17 +1120,15 @@ views.sites = async () => {
|
|||||||
};
|
};
|
||||||
return h('div', {},
|
return h('div', {},
|
||||||
tagBar('sites', tags),
|
tagBar('sites', tags),
|
||||||
card('', h('table', {}, h('thead', {}, h('tr', {}, ['Status', 'Name', 'Kind', 'Tunnels to', 'Version', 'Containers', ''].map(t => h('th', {}, t)))),
|
card('', h('table', {}, h('thead', {}, h('tr', {}, ['Status', 'Name', 'Kind', 'Tunnels to', 'Version', 'Discovered', ''].map(t => h('th', {}, t)))),
|
||||||
h('tbody', {}, st.sites.filter(s => hasTag(s, TAGSEL.sites)).map(s => h('tr', {},
|
h('tbody', {}, st.sites.filter(s => hasTag(s, TAGSEL.sites)).map(s => h('tr', {},
|
||||||
h('td', {}, st.online[s.id] ? badge('Online', 'success') : badge('Offline', 'danger')), h('td', {}, s.name, tagList(s)),
|
h('td', {}, st.online[s.id] ? badge('Online', 'success') : badge('Offline', 'danger')), h('td', {}, s.name, tagList(s)),
|
||||||
h('td', {}, s.local ? 'This machine' : 'Bamboo agent'),
|
h('td', {}, s.local ? 'This machine' : 'Bamboo agent'),
|
||||||
h('td', {}, s.local ? 'Direct' : ((s.connected || []).map(id => (byId(st.edges, id) || {name:id}).name).join(', ') || 'nothing connected')),
|
h('td', {}, s.local ? 'Direct' : ((s.connected || []).map(id => (byId(st.edges, id) || {name:id}).name).join(', ') || 'nothing connected')),
|
||||||
s.local ? h('td', {class:'dim'}, 'Same as its edge') : versionCell(s, latest.bamboo, !!s.updateId, O(`/sites/${s.id}/update`), O(`/sites/${s.id}/update/log`)),
|
s.local ? h('td', {class:'dim'}, 'Same as its edge') : versionCell(s, latest.bamboo, !!s.updateId, O(`/sites/${s.id}/update`), O(`/sites/${s.id}/update/log`)),
|
||||||
h('td', {}, h('button', {class:'ghost', onclick: act(async () => {
|
h('td', {}, h('button', {class:'ghost', onclick: act(() => siteFound(s))}, 'View')),
|
||||||
const l = await api('GET', O(`/sites/${s.id}/containers`));
|
|
||||||
modal('Containers on ' + s.name, l.length ? h('table', {}, h('thead', {}, h('tr', {}, ['Name', 'Image', 'Ports'].map(t => h('th', {}, t)))), h('tbody', {}, l.map(c => h('tr', {}, h('td', {}, c.name), h('td', {class:'dim'}, c.image), h('td', {class:'mono small'}, c.ports.map(p => (p.public ? p.public + '→' : '') + p.private + '/' + p.type).join(', ')))))) :
|
|
||||||
h('p', {class:'dim'}, 'None reported. Start the agent with --docker (or BLACKTIP_DOCKER=1 / BAMBOO_DOCKER=1) on a machine where Docker is running.')); })}, 'View')),
|
|
||||||
h('td', {}, s.local ? null : h('div', {class:'actions'},
|
h('td', {}, s.local ? null : h('div', {class:'actions'},
|
||||||
|
h('button', {class:'ghost', onclick:() => sshAccess(s)}, 'SSH'),
|
||||||
h('button', {class:'ghost', onclick:() => editSite(s)}, 'Edit'),
|
h('button', {class:'ghost', onclick:() => editSite(s)}, 'Edit'),
|
||||||
h('button', {class:'ghost', onclick:() => showCmd(s)}, 'Show command'),
|
h('button', {class:'ghost', onclick:() => showCmd(s)}, 'Show command'),
|
||||||
h('button', {class:'ghost danger', onclick: ask({title:`Delete ${s.name}?`, body:'Every port that uses only this site is deleted with it, and its agent loses its connection. This cannot be undone.', confirm:'Delete site'}, async () => { await api('DELETE', O('/sites/' + s.id)); refresh(); })}, 'Delete')))))))));
|
h('button', {class:'ghost danger', onclick: ask({title:`Delete ${s.name}?`, body:'Every port that uses only this site is deleted with it, and its agent loses its connection. This cannot be undone.', confirm:'Delete site'}, async () => { await api('DELETE', O('/sites/' + s.id)); refresh(); })}, 'Delete')))))))));
|
||||||
|
|||||||
Reference in new issue
Block a user