├── .fmf └── version ├── test ├── reference-image ├── browser │ ├── main.fmf │ ├── run-test.sh │ └── browser.sh ├── vm.install ├── run └── check-application ├── .eslintignore ├── src ├── app.scss ├── manifest.json ├── index.tsx ├── index.html ├── types.ts └── app.tsx ├── docs └── screenshot.png ├── plans └── all.fmf ├── .gitignore ├── .gitpod └── Dockerfile ├── .codesandbox ├── Dockerfile └── tailscaled ├── tsconfig.json ├── org.cockpit-project.tailscale.metainfo.xml ├── .cirrus.yml ├── stylePaths.js ├── .gitpod.yml ├── .github └── workflows │ ├── npm-update-pf.yml │ ├── npm-update.yml │ ├── cockpit-lib-update.yml │ └── release.yml.disabled ├── po └── de.po ├── .stylelintrc.json ├── packaging └── cockpit-tailscale.spec.in ├── .eslintrc.json ├── package.json ├── packit.yaml ├── webpack.config.js ├── README.md ├── Makefile ├── @types └── cockpitjs │ └── index.d.ts ├── LICENSE └── yarn.lock /.fmf/version: -------------------------------------------------------------------------------- 1 | 1 2 | -------------------------------------------------------------------------------- /test/reference-image: -------------------------------------------------------------------------------- 1 | fedora-35 2 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules/* 2 | pkg/lib/* 3 | -------------------------------------------------------------------------------- /src/app.scss: -------------------------------------------------------------------------------- 1 | @use "page.scss"; 2 | 3 | p { 4 | font-weight: bold; 5 | } 6 | -------------------------------------------------------------------------------- /docs/screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gbraad-cockpit/cockpit-tailscale/HEAD/docs/screenshot.png -------------------------------------------------------------------------------- /plans/all.fmf: -------------------------------------------------------------------------------- 1 | summary: 2 | Run all tests 3 | discover: 4 | how: fmf 5 | execute: 6 | how: tmt 7 | -------------------------------------------------------------------------------- /src/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "requires": { 3 | "cockpit": "137" 4 | }, 5 | 6 | "tools": { 7 | "index": { 8 | "label": "Tailscale" 9 | } 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *~ 2 | *.retry 3 | *.tar.xz 4 | *.rpm 5 | node_modules/ 6 | dist/ 7 | /*.spec 8 | /.vagrant 9 | package-lock.json 10 | Test*FAIL* 11 | /bots 12 | test/common/ 13 | test/images/ 14 | pkg 15 | *.pot 16 | POTFILES* 17 | tmp/ 18 | /po/LINGUAS 19 | /tools 20 | -------------------------------------------------------------------------------- /test/browser/main.fmf: -------------------------------------------------------------------------------- 1 | summary: 2 | Run browser integration tests on the host 3 | require: 4 | - cockpit-starter-kit 5 | - cockpit-ws 6 | - cockpit-system 7 | - bzip2 8 | - git-core 9 | - glibc-langpack-de 10 | - libvirt-python3 11 | - make 12 | - npm 13 | - python3 14 | test: ./browser.sh 15 | duration: 60m 16 | -------------------------------------------------------------------------------- /test/vm.install: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # image-customize script to prepare a bots VM for testing this application 3 | # The application package will be installed separately 4 | set -eu 5 | 6 | # don't force https:// (self-signed cert) 7 | printf "[WebService]\\nAllowUnencrypted=true\\n" > /etc/cockpit/cockpit.conf 8 | 9 | if type firewall-cmd >/dev/null 2>&1; then 10 | firewall-cmd --add-service=cockpit --permanent 11 | fi 12 | systemctl enable cockpit.socket 13 | -------------------------------------------------------------------------------- /test/run: -------------------------------------------------------------------------------- 1 | #! /bin/sh 2 | set -eu 3 | 4 | # This is the expected entry point for Cockpit CI; will be called without 5 | # arguments but with an appropriate $TEST_OS, and optionally $TEST_SCENARIO 6 | 7 | TEST_SCENARIO="${TEST_SCENARIO:-}" 8 | [ "${TEST_SCENARIO}" = "${TEST_SCENARIO##firefox}" ] || export TEST_BROWSER=firefox 9 | export RUN_TESTS_OPTIONS=--track-naughties 10 | 11 | # linters are off by default for production builds, but we want to run them in CI 12 | export LINT=1 13 | 14 | make check 15 | -------------------------------------------------------------------------------- /.gitpod/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM --platform=linux/amd64 ghcr.io/gbraad-devenv/fedora/base:38 2 | 3 | USER root 4 | 5 | # Add gitpod user with the expected ID (automated setup does not work atm) 6 | RUN useradd -l -u 33333 -G wheel -md /home/gitpod -s /usr/bin/zsh -p gitpod gitpod 7 | 8 | RUN dnf install -y \ 9 | docker \ 10 | cockpit \ 11 | passwd \ 12 | make \ 13 | npm \ 14 | rpm-build \ 15 | && dnf clean all \ 16 | && rm -rf /var/cache/yum 17 | 18 | USER gitpod 19 | -------------------------------------------------------------------------------- /src/index.tsx: -------------------------------------------------------------------------------- 1 | import "cockpit-dark-theme"; 2 | //import "@patternfly/patternfly/patternfly-base.scss"; 3 | import '@patternfly/react-core/dist/styles/base.css'; 4 | 5 | import React from 'react'; 6 | import ReactDOM from 'react-dom'; 7 | import { Application } from './app'; 8 | 9 | //import "patternfly/patternfly-5-overrides.scss"; 10 | 11 | document.addEventListener("DOMContentLoaded", function () { 12 | ReactDOM.render( 13 | , 14 | document.getElementById("app"), 15 | ) 16 | }); 17 | 18 | -------------------------------------------------------------------------------- /.codesandbox/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM --platform=linux/amd64 ghcr.io/gbraad-devenv/fedora/base:38 2 | 3 | USER root 4 | 5 | RUN dnf install -y \ 6 | docker \ 7 | cockpit \ 8 | passwd \ 9 | make \ 10 | npm \ 11 | rpm-build \ 12 | && dnf clean all \ 13 | && rm -rf /var/cache/yum 14 | 15 | RUN npm install -g yarn 16 | 17 | COPY .codesandbox/tailscaled /etc/init.d/taiscaled 18 | 19 | USER gbraad 20 | 21 | RUN git clone https://github.com/gbraad/dotfiles ~/.dotfiles \ 22 | && ~/.dotfiles/install.sh 23 | -------------------------------------------------------------------------------- /src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Tailscale 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 |
17 | 18 | 19 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "baseUrl": ".", 4 | "rootDir": ".", 5 | "target": "esnext", 6 | "module": "commonjs", 7 | "moduleResolution": "node", 8 | "typeRoots": ["@types"], 9 | "allowJs": true, 10 | "declaration": true, 11 | "sourceMap": true, 12 | "outDir": "dist", 13 | "esModuleInterop": true, 14 | "forceConsistentCasingInFileNames": true, 15 | "strict": false, 16 | "skipLibCheck": true, 17 | "lib": ["es6", "dom"], 18 | "jsx": "react", 19 | "paths": { 20 | "@app/*": ["src/*"], 21 | "@assets/*": ["node_modules/@patternfly/react-core/dist/styles/assets/*"] 22 | } 23 | }, 24 | "include": ["src"], 25 | "exclude": ["node_modules"] 26 | } 27 | -------------------------------------------------------------------------------- /org.cockpit-project.tailscale.metainfo.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | org.cockpit_project.tailscale 4 | CC0-1.0 5 | Tailscale 6 | Tailscale application 7 | 8 |

9 | Tailscale application for Cockpit 10 |

11 |
12 | org.cockpit_project.tailscale 13 | tailscale 14 | https://github.com/spotsnel/tailscale-cockpit 15 | https://github.com/spotsnel/tailscale-cockpit/issues 16 | me@gbraad.nl 17 |
18 | -------------------------------------------------------------------------------- /.cirrus.yml: -------------------------------------------------------------------------------- 1 | container: 2 | # official cockpit CI container, with cockpit related build and test dependencies 3 | # if you want to use your own, see the documentation about required packages: 4 | # https://github.com/cockpit-project/cockpit/blob/main/HACKING.md#getting-the-development-dependencies 5 | image: quay.io/cockpit/tasks 6 | kvm: true 7 | # increase this if you have many tests that benefit from parallelism 8 | cpu: 1 9 | 10 | test_task: 11 | env: 12 | matrix: 13 | - TEST_OS: fedora-37 14 | - TEST_OS: centos-8-stream 15 | 16 | fix_kvm_script: sudo chmod 666 /dev/kvm 17 | 18 | # test PO template generation 19 | pot_build_script: make po/tailscale.pot 20 | 21 | # chromium has too little /dev/shm, and we can't make that bigger 22 | check_script: TEST_BROWSER=firefox TEST_JOBS=$(nproc) TEST_OS=$TEST_OS make check 23 | -------------------------------------------------------------------------------- /.codesandbox/tailscaled: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | set -e 4 | test -x /usr/sbin/tailscaled || exit 0 5 | umask 022 6 | 7 | export PATH="${PATH:+$PATH:}/usr/sbin:/sbin" 8 | 9 | case "$1" in 10 | start) 11 | echo "Starting Tailscale VPN" 12 | tailscaled --tun=userspace-networking \ 13 | --socks5-server=localhost:3215 \ 14 | --outbound-http-proxy-listen=localhost:3214 \ 15 | --state=/var/lib/tailscale/tailscaled.state \ 16 | --socket=/run/tailscale/tailscaled.sock \ 17 | --port 41641 \ 18 | 2>/dev/null & 19 | tailscale up --authkey=${TAILSCALE_AUTHKEY} \ 20 | --netfilter-mode=off \ 21 | --ssh 22 | ;; 23 | *) 24 | echo "Usage: /etc/init.d/tailscaled {start}" 25 | exit 1 26 | esac 27 | 28 | exit 0 -------------------------------------------------------------------------------- /stylePaths.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | module.exports = { 3 | stylePaths: [ 4 | path.resolve(__dirname, 'src'), 5 | path.resolve(__dirname, 'node_modules/patternfly'), 6 | path.resolve(__dirname, 'node_modules/@patternfly/patternfly'), 7 | path.resolve(__dirname, 'node_modules/@patternfly/react-styles/css'), 8 | path.resolve(__dirname, 'node_modules/@patternfly/react-core/dist/styles/base.css'), 9 | path.resolve(__dirname, 'node_modules/@patternfly/react-core/dist/esm/@patternfly/patternfly'), 10 | path.resolve(__dirname, 'node_modules/@patternfly/react-core/node_modules/@patternfly/react-styles/css'), 11 | path.resolve(__dirname, 'node_modules/@patternfly/react-table/node_modules/@patternfly/react-styles/css'), 12 | path.resolve(__dirname, 'node_modules/@patternfly/react-inline-edit-extension/node_modules/@patternfly/react-styles/css') 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /.gitpod.yml: -------------------------------------------------------------------------------- 1 | image: 2 | file: /.gitpod/Dockerfile 3 | 4 | tasks: 5 | - name: dotfiles 6 | command: | 7 | if [ ! -d "~/.dotfiles" ]; then 8 | cd /tmp 9 | curl -sSL https://raw.githubusercontent.com/gbraad/dotfiles/master/install.sh -o /tmp/install.sh && 10 | rm -f ~/.zshrc && 11 | sh /tmp/install.sh 12 | fi 13 | mv ~/.bashrc-nochsh ~/.bashrc 14 | - name: sshd 15 | command: | 16 | sudo ssh-keygen -A && sudo /usr/sbin/sshd 17 | curl https://github.com/gbraad.keys | tee -a ~/.ssh/authorized_keys 18 | - name: tailscale 19 | command: | 20 | sudo --preserve-env=TAILSCALE_AUTHKEY /etc/init.d/tailscaled start 21 | 22 | ports: 23 | - port: 22 24 | onOpen: ignore 25 | - port: 6080 26 | onOpen: open-preview 27 | - port: 9090 28 | onOpen: open-preview 29 | 30 | vscode: 31 | extensions: 32 | - ms-vscode.Theme-TomorrowKit 33 | - tailscale.vscode-tailscale -------------------------------------------------------------------------------- /.github/workflows/npm-update-pf.yml: -------------------------------------------------------------------------------- 1 | name: npm-update-pf 2 | on: 3 | schedule: 4 | - cron: '0 2 * * 1' 5 | # can be run manually on https://github.com/cockpit-project/starter-kit/actions 6 | workflow_dispatch: 7 | jobs: 8 | npm-update: 9 | runs-on: ubuntu-latest 10 | permissions: 11 | pull-requests: write 12 | contents: write 13 | steps: 14 | - name: Set up dependencies 15 | run: | 16 | sudo apt update 17 | sudo apt install -y npm make 18 | 19 | - name: Set up configuration and secrets 20 | run: | 21 | printf '[user]\n\tname = Cockpit Project\n\temail=cockpituous@gmail.com\n' > ~/.gitconfig 22 | echo '${{ secrets.GITHUB_TOKEN }}' > ~/.config/github-token 23 | 24 | - name: Clone repository 25 | uses: actions/checkout@v3 26 | 27 | - name: Run npm-update bot 28 | run: | 29 | make bots 30 | bots/npm-update @patternfly 31 | -------------------------------------------------------------------------------- /.github/workflows/npm-update.yml: -------------------------------------------------------------------------------- 1 | name: npm-update 2 | on: 3 | schedule: 4 | - cron: '0 2 * * 2,4,6' 5 | # can be run manually on https://github.com/cockpit-project/starter-kit/actions 6 | workflow_dispatch: 7 | jobs: 8 | npm-update: 9 | runs-on: ubuntu-latest 10 | permissions: 11 | pull-requests: write 12 | contents: write 13 | steps: 14 | - name: Set up dependencies 15 | run: | 16 | sudo apt update 17 | sudo apt install -y npm make 18 | 19 | - name: Set up configuration and secrets 20 | run: | 21 | printf '[user]\n\tname = Cockpit Project\n\temail=cockpituous@gmail.com\n' > ~/.gitconfig 22 | echo '${{ secrets.GITHUB_TOKEN }}' > ~/.config/github-token 23 | 24 | - name: Clone repository 25 | uses: actions/checkout@v3 26 | 27 | - name: Run npm-update bot 28 | run: | 29 | make bots 30 | bots/npm-update ~@patternfly 31 | -------------------------------------------------------------------------------- /.github/workflows/cockpit-lib-update.yml: -------------------------------------------------------------------------------- 1 | name: cockpit-lib-update 2 | on: 3 | schedule: 4 | - cron: '0 2 * * 4' 5 | # can be run manually on https://github.com/cockpit-project/starter-kit/actions 6 | workflow_dispatch: 7 | jobs: 8 | cockpit-lib-update: 9 | runs-on: ubuntu-latest 10 | permissions: 11 | pull-requests: write 12 | contents: write 13 | steps: 14 | - name: Set up dependencies 15 | run: | 16 | sudo apt update 17 | sudo apt install -y make 18 | 19 | - name: Set up configuration and secrets 20 | run: | 21 | printf '[user]\n\tname = Cockpit Project\n\temail=cockpituous@gmail.com\n' > ~/.gitconfig 22 | echo '${{ secrets.GITHUB_TOKEN }}' > ~/.config/github-token 23 | 24 | - name: Clone repository 25 | uses: actions/checkout@v3 26 | 27 | - name: Run cockpit-lib-update 28 | run: | 29 | make bots 30 | bots/cockpit-lib-update 31 | -------------------------------------------------------------------------------- /po/de.po: -------------------------------------------------------------------------------- 1 | # starter-kit German translations 2 | #, fuzzy 3 | msgid "" 4 | msgstr "" 5 | "Project-Id-Version: starter-kit 1.0\n" 6 | "Report-Msgid-Bugs-To: \n" 7 | "POT-Creation-Date: 2022-03-09 16:09+0100\n" 8 | "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" 9 | "Last-Translator: FULL NAME \n" 10 | "Language-Team: LANGUAGE \n" 11 | "Language: de\n" 12 | "MIME-Version: 1.0\n" 13 | "Content-Type: text/plain; charset=UTF-8\n" 14 | "Content-Transfer-Encoding: 8bit\n" 15 | "Plural-Forms: nplurals=2; plural=n != 1\n" 16 | 17 | #: src/index.html:20 18 | msgid "Cockpit Starter Kit" 19 | msgstr "Cockpit Bausatz" 20 | 21 | #: src/app.jsx:43 22 | msgid "Running on $0" 23 | msgstr "Läuft auf $0" 24 | 25 | #: org.cockpit-project.starter-kit.metainfo.xml:6 26 | msgid "Scaffolding for a cockpit module" 27 | msgstr "Gerüst für ein Cockpit-Modul" 28 | 29 | #: org.cockpit-project.starter-kit.metainfo.xml:8 30 | msgid "Scaffolding for a cockpit module." 31 | msgstr "Gerüst für ein Cockpit-Modul." 32 | 33 | #: src/manifest.json:0 org.cockpit-project.starter-kit.metainfo.xml:5 34 | msgid "Starter Kit" 35 | msgstr "Bausatz" 36 | 37 | #: src/app.jsx:29 38 | msgid "Unknown" 39 | msgstr "Unbekannt" 40 | -------------------------------------------------------------------------------- /test/browser/run-test.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | set -eux 3 | 4 | # tests need cockpit's bots/ libraries and test infrastructure 5 | cd $SOURCE 6 | git init 7 | rm -f bots # common local case: existing bots symlink 8 | make bots test/common 9 | 10 | # support running from clean git tree 11 | if [ ! -d node_modules/chrome-remote-interface ]; then 12 | # copy package.json temporarily otherwise npm might try to install the dependencies from it 13 | rm -f package-lock.json # otherwise the command below installs *everything*, argh 14 | mv package.json .package.json 15 | # only install a subset to save time/space 16 | npm install chrome-remote-interface sizzle 17 | mv .package.json package.json 18 | fi 19 | 20 | # disable detection of affected tests; testing takes too long as there is no parallelization 21 | mv .git dot-git 22 | 23 | . /etc/os-release 24 | export TEST_OS="${ID}-${VERSION_ID/./-}" 25 | export TEST_AUDIT_NO_SELINUX=1 26 | 27 | if [ "${TEST_OS#centos-}" != "$TEST_OS" ]; then 28 | TEST_OS="${TEST_OS}-stream" 29 | fi 30 | 31 | EXCLUDES="" 32 | 33 | RC=0 34 | test/common/run-tests --nondestructive --machine 127.0.0.1:22 --browser 127.0.0.1:9090 $EXCLUDES || RC=$? 35 | 36 | echo $RC > "$LOGS/exitcode" 37 | cp --verbose Test* "$LOGS" || true 38 | # deliver test result via exitcode file 39 | exit 0 40 | -------------------------------------------------------------------------------- /.github/workflows/release.yml.disabled: -------------------------------------------------------------------------------- 1 | # Create a GitHub upstream release. Replace "TARNAME" with your project tarball 2 | # name and enable this by dropping the ".disabled" suffix from the file name. 3 | # See README.md. 4 | name: release 5 | on: 6 | push: 7 | tags: 8 | # this is a glob, not a regexp 9 | - '[0-9]*' 10 | jobs: 11 | source: 12 | runs-on: ubuntu-latest 13 | container: 14 | image: ghcr.io/cockpit-project/unit-tests 15 | options: --user root 16 | permissions: 17 | # create GitHub release 18 | contents: write 19 | steps: 20 | - name: Clone repository 21 | uses: actions/checkout@v3 22 | with: 23 | fetch-depth: 0 24 | 25 | # https://github.blog/2022-04-12-git-security-vulnerability-announced/ 26 | - name: Pacify git's permission check 27 | run: git config --global --add safe.directory /__w/ 28 | 29 | - name: Workaround for https://github.com/actions/checkout/pull/697 30 | run: git fetch --force origin $(git describe --tags):refs/tags/$(git describe --tags) 31 | 32 | - name: Build release 33 | run: make dist 34 | 35 | - name: Publish GitHub release 36 | uses: cockpit-project/action-release@88d994da62d1451c7073e26748c18413fcdf46e9 37 | with: 38 | filename: "TARNAME-${{ github.ref_name }}.tar.xz" 39 | -------------------------------------------------------------------------------- /.stylelintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "stylelint-config-standard-scss", 3 | "rules": { 4 | "declaration-colon-newline-after": null, 5 | "selector-list-comma-newline-after": null, 6 | 7 | "at-rule-empty-line-before": null, 8 | "declaration-colon-space-before": null, 9 | "declaration-empty-line-before": null, 10 | "custom-property-empty-line-before": null, 11 | "comment-empty-line-before": null, 12 | "scss/double-slash-comment-empty-line-before": null, 13 | "scss/dollar-variable-colon-space-after": null, 14 | 15 | "custom-property-pattern": null, 16 | "declaration-block-no-duplicate-properties": null, 17 | "declaration-block-no-redundant-longhand-properties": null, 18 | "declaration-block-no-shorthand-property-overrides": null, 19 | "declaration-block-single-line-max-declarations": null, 20 | "font-family-no-duplicate-names": null, 21 | "function-url-quotes": null, 22 | "indentation": null, 23 | "keyframes-name-pattern": null, 24 | "max-line-length": null, 25 | "no-descending-specificity": null, 26 | "no-duplicate-selectors": null, 27 | "scss/at-extend-no-missing-placeholder": null, 28 | "scss/at-import-partial-extension": null, 29 | "scss/at-mixin-pattern": null, 30 | "scss/comment-no-empty": null, 31 | "scss/dollar-variable-pattern": null, 32 | "scss/double-slash-comment-whitespace-inside": null, 33 | "scss/no-global-function-names": null, 34 | "scss/operator-no-unspaced": null, 35 | "selector-class-pattern": null, 36 | "selector-id-pattern": null 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /packaging/cockpit-tailscale.spec.in: -------------------------------------------------------------------------------- 1 | Name: cockpit-tailscale 2 | Version: %{VERSION} 3 | Release: 1%{?dist} 4 | Summary: Cockpit Tailscale 5 | License: LGPLv2+ 6 | 7 | Source0: https://github.com/spotsnel/tailscale-cockpit/releases/download/%{version}/%{name}-%{version}.tar.xz 8 | Source1: https://github.com/spotsnel/tailscale-cockpit/releases/download/%{version}/%{name}-node-%{version}.tar.xz 9 | BuildArch: noarch 10 | ExclusiveArch: %{nodejs_arches} noarch 11 | BuildRequires: nodejs 12 | BuildRequires: make 13 | BuildRequires: libappstream-glib 14 | BuildRequires: gettext 15 | %if 0%{?rhel} && 0%{?rhel} <= 8 16 | BuildRequires: libappstream-glib-devel 17 | %endif 18 | 19 | Requires: cockpit-bridge 20 | 21 | %{NPM_PROVIDES} 22 | 23 | %description 24 | Tailscale management for Cockpit 25 | 26 | %prep 27 | %autosetup -n %{name} -a 1 28 | # ignore pre-built bundle in release tarball and rebuild it 29 | # but keep it in RHEL/CentOS-8, as that has a too old nodejs 30 | %if ! 0%{?rhel} || 0%{?rhel} >= 9 31 | rm -rf dist 32 | %endif 33 | 34 | %build 35 | ESLINT=0 NODE_ENV=production make 36 | 37 | %install 38 | %make_install PREFIX=/usr 39 | 40 | # drop source maps, they are large and just for debugging 41 | find %{buildroot}%{_datadir}/cockpit/ -name '*.map' | xargs --no-run-if-empty rm --verbose 42 | 43 | %check 44 | appstream-util validate-relax --nonet %{buildroot}/%{_datadir}/metainfo/* 45 | 46 | # this can't be meaningfully tested during package build; tests happen through 47 | # FMF (see plans/all.fmf) during package gating 48 | 49 | %files 50 | %doc README.md 51 | %license LICENSE dist/bundle.js.LICENSE.txt 52 | %{_datadir}/cockpit/* 53 | %{_datadir}/metainfo/* 54 | 55 | %changelog 56 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "env": { 4 | "browser": true, 5 | "es6": true 6 | }, 7 | "extends": ["eslint:recommended", "standard", "standard-jsx", "standard-react"], 8 | "parserOptions": { 9 | "ecmaVersion": "2022", 10 | "sourceType": "module" 11 | }, 12 | "plugins": ["flowtype", "react", "react-hooks"], 13 | "rules": { 14 | "indent": ["error", 4, 15 | { 16 | "ObjectExpression": "first", 17 | "CallExpression": {"arguments": "first"}, 18 | "MemberExpression": 2, 19 | "ignoredNodes": [ "JSXAttribute" ] 20 | }], 21 | "newline-per-chained-call": ["error", { "ignoreChainWithDepth": 2 }], 22 | "no-var": "error", 23 | "lines-between-class-members": ["error", "always", { "exceptAfterSingleLine": true }], 24 | "prefer-promise-reject-errors": ["error", { "allowEmptyReject": true }], 25 | "react/jsx-indent": ["error", 4], 26 | "semi": ["error", "always", { "omitLastInOneLineBlock": true }], 27 | 28 | "react-hooks/rules-of-hooks": "error", 29 | "react-hooks/exhaustive-deps": "error", 30 | 31 | "camelcase": "off", 32 | "comma-dangle": "off", 33 | "curly": "off", 34 | "jsx-quotes": "off", 35 | "key-spacing": "off", 36 | "no-console": "off", 37 | "quotes": "off", 38 | "react/jsx-curly-spacing": "off", 39 | "react/jsx-indent-props": "off", 40 | "react/prop-types": "off", 41 | "space-before-function-paren": "off", 42 | "standard/no-callback-literal": "off" 43 | }, 44 | "globals": { 45 | "require": false, 46 | "module": false 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /test/browser/browser.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | set -eux 3 | 4 | TESTS="$(realpath $(dirname "$0"))" 5 | SOURCE="$(realpath $TESTS/../..)" 6 | LOGS="$(pwd)/logs" 7 | mkdir -p "$LOGS" 8 | chmod a+w "$LOGS" 9 | 10 | # HACK: https://bugzilla.redhat.com/show_bug.cgi?id=2033020 11 | dnf update -y pam || true 12 | 13 | # install firefox (available everywhere in Fedora and RHEL) 14 | # we don't need the H.264 codec, and it is sometimes not available (rhbz#2005760) 15 | dnf install --disablerepo=fedora-cisco-openh264 -y --setopt=install_weak_deps=False firefox 16 | 17 | # nodejs 10 is too old for current Cockpit test API 18 | if grep -q platform:el8 /etc/os-release; then 19 | dnf module switch-to -y nodejs:16 20 | fi 21 | 22 | # create user account for logging in 23 | if ! id admin 2>/dev/null; then 24 | useradd -c Administrator -G wheel admin 25 | echo admin:foobar | chpasswd 26 | fi 27 | 28 | # set root's password 29 | echo root:foobar | chpasswd 30 | 31 | # avoid sudo lecture during tests 32 | su -c 'echo foobar | sudo --stdin whoami' - admin 33 | 34 | # create user account for running the test 35 | if ! id runtest 2>/dev/null; then 36 | useradd -c 'Test runner' runtest 37 | # allow test to set up things on the machine 38 | mkdir -p /root/.ssh 39 | curl https://raw.githubusercontent.com/cockpit-project/bots/main/machine/identity.pub >> /root/.ssh/authorized_keys 40 | chmod 600 /root/.ssh/authorized_keys 41 | fi 42 | chown -R runtest "$SOURCE" 43 | 44 | # disable core dumps, we rather investigate them upstream where test VMs are accessible 45 | echo core > /proc/sys/kernel/core_pattern 46 | 47 | systemctl enable --now cockpit.socket 48 | 49 | # Run tests as unprivileged user 50 | su - -c "env TEST_BROWSER=firefox SOURCE=$SOURCE LOGS=$LOGS $TESTS/run-test.sh" runtest 51 | 52 | RC=$(cat $LOGS/exitcode) 53 | exit ${RC:-1} 54 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "tailscale", 3 | "description": "Tailscale application for Cockpit", 4 | "main": "dist/index.js", 5 | "repository": "git@github.com:spotsnel/tailscale-cockpit.git", 6 | "author": "Gerard Braad ", 7 | "version": "v0.0.6", 8 | "license": "LGPL-2.1", 9 | "scripts": { 10 | "build": "webpack --mode production", 11 | "dev": "webpack --mode development", 12 | "rpm": "make rpm", 13 | "watch": "", 14 | "link": "mkdir -p ~/.local/share/cockpit && ln -s $PWD/dist ~/.local/share/cockpit/tailscale", 15 | "unlink": "rm ~/.local/share/cockpit/tailscale", 16 | "linkusr": "sudo mkdir -p /usr/local/share/cockpit && sudo ln -s $PWD/dist /usr/local/share/cockpit/tailscale", 17 | "unlinkusr": "sudo rm ~/.local/share/cockpit/tailscale", 18 | "cockpit": "sudo runuser -u cockpit-wsinstance -- /usr/libexec/cockpit-ws --port=9090 --for-tls-proxy" 19 | }, 20 | "devDependencies": { 21 | "@types/react": "^18.2.12", 22 | "@types/react-dom": "^18.2.5", 23 | "argparse": "^2.0.1", 24 | "chrome-remote-interface": "^0.32.1", 25 | "copy-webpack-plugin": "^9.0.0", 26 | "css-loader": "^5.2.6", 27 | "file-loader": "^6.2.0", 28 | "html-webpack-plugin": "^5.5.3", 29 | "htmlparser": "^1.7.7", 30 | "jed": "^1.1.1", 31 | "mini-css-extract-plugin": "^1.6.0", 32 | "po2json": "^1.0.0-alpha", 33 | "qunit": "^2.9.3", 34 | "sass": "^1.63.6", 35 | "sass-loader": "^13.3.2", 36 | "sizzle": "^2.3.3", 37 | "ts-loader": "^9.4.3", 38 | "typescript": "^4.8.4", 39 | "webpack": "^5.88.0", 40 | "webpack-cli": "^5.1.4", 41 | "webpack-dev-server": "^4.15.1" 42 | }, 43 | "dependencies": { 44 | "@patternfly/patternfly": "5.0.0-prerelease.10", 45 | "@patternfly/react-core": "5.0.0-prerelease.13", 46 | "@patternfly/react-icons": "5.0.0-prerelease.7", 47 | "@patternfly/react-styles": "5.0.0-prerelease.5", 48 | "@patternfly/react-table": "5.0.0-prerelease.13", 49 | "react": "17.0.2", 50 | "react-dom": "17.0.2", 51 | "yarn": "^1.22.19" 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /packit.yaml: -------------------------------------------------------------------------------- 1 | # Enable RPM builds and running integration tests in PRs through https://packit.dev/ 2 | # To use this, enable Packit-as-a-service in GitHub: https://packit.dev/docs/packit-as-a-service/ 3 | # See https://packit.dev/docs/configuration/ for the format of this file 4 | 5 | specfile_path: cockpit-tailscale.spec 6 | # use the nicely formatted release description from our upstream release, instead of git shortlog 7 | copy_upstream_release_description: true 8 | 9 | srpm_build_deps: 10 | - make 11 | - nodejs-npm 12 | 13 | actions: 14 | post-upstream-clone: 15 | - make cockpit-tailscale.spec 16 | # replace Source1 manually, as create-archive: can't handle multiple tarballs 17 | - make node-cache 18 | - sh -c 'sed -i "/^Source1:/ s/https:.*/$(ls *-node*.tar.xz)/" cockpit-*.spec' 19 | create-archive: make dist 20 | # starter-kit.git has no release tags; your project can drop this once you have a release 21 | get-current-version: make print-version 22 | 23 | jobs: 24 | - job: copr_build 25 | trigger: pull_request 26 | targets: 27 | - fedora-all 28 | - fedora-latest-aarch64 29 | - centos-stream-8 30 | - centos-stream-9 31 | - centos-stream-9-aarch64 32 | 33 | - job: tests 34 | trigger: pull_request 35 | targets: 36 | - fedora-all 37 | - fedora-latest-aarch64 38 | - centos-stream-8 39 | - centos-stream-9 40 | - centos-stream-9-aarch64 41 | 42 | # Build releases in COPR: https://packit.dev/docs/configuration/#copr_build 43 | #- job: copr_build 44 | # trigger: release 45 | # owner: your_copr_login 46 | # project: your_copr_project 47 | # preserve_project: True 48 | # targets: 49 | # - fedora-all 50 | # - centos-stream-9-x86_64 51 | 52 | # Build releases in Fedora: https://packit.dev/docs/configuration/#propose_downstream 53 | #- job: propose_downstream 54 | # trigger: release 55 | # dist_git_branches: 56 | # - fedora-all 57 | 58 | #- job: koji_build 59 | # trigger: commit 60 | # dist_git_branches: 61 | # - fedora-all 62 | 63 | #- job: bodhi_update 64 | # trigger: commit 65 | # dist_git_branches: 66 | # # rawhide updates are created automatically 67 | # - fedora-branched 68 | -------------------------------------------------------------------------------- /src/types.ts: -------------------------------------------------------------------------------- 1 | // BackendState 2 | // Keep in sync with https://github.com/tailscale/tailscale/blob/main/ipn/backend.go 3 | export type TailscaleBackendState = 4 | | 'NoState' 5 | | 'NeedsMachineAuth' 6 | | 'NeedsLogin' 7 | | 'InUseOtherUser' 8 | | 'Stopped' 9 | | 'Starting' 10 | | 'Running'; 11 | 12 | export type TailscaleVersion = { 13 | majorMinorPatch: string; 14 | short: string; 15 | long: string; 16 | gitCommit: string; 17 | extraGitCommit: string; 18 | cap: number; 19 | } 20 | 21 | export enum OS { 22 | Android = "android", 23 | IOS = "iOS", 24 | Linux = "linux", 25 | MACOS = "macOS", 26 | Windows = "windows", 27 | } 28 | 29 | export type TailscalePeer = { 30 | Self: boolean; 31 | ID: string; 32 | PublicKey: string; 33 | HostName: string; 34 | DNSName: string; 35 | OS: OS; 36 | UserID: string; 37 | TailscaleIPs: string[] 38 | Tags?: string[]; 39 | Capabilities?: string[]; 40 | Relay: string; 41 | RxBytes: number; 42 | TxBytes: number; 43 | Created: Date; 44 | LastWrite: Date; 45 | LastSeen: Date; 46 | LastHandshake: Date; 47 | Online: boolean; 48 | KeepAlive: boolean; 49 | ExitNode: boolean; 50 | ExitNodeOption: boolean; 51 | Active: boolean; 52 | CurAddr: string; 53 | ShareeNode?: boolean; // funnel-ingress-node, device-of-shared-to-user 54 | } 55 | 56 | export interface TailscaleExitNodeStatus { 57 | ID: string; 58 | Online: boolean; 59 | TailscaleIPs: string[]; 60 | } 61 | 62 | export type TailscaleStatus = { 63 | BackendState: TailscaleBackendState; 64 | AuthURL: string; 65 | Self: TailscalePeer, 66 | CurrentTailnet: { 67 | Name: string; 68 | MagicDNSSuffix: string; 69 | MagicDNSEnabled: boolean; 70 | } | null; 71 | ExitNodeStatus: TailscaleExitNodeStatus | null; 72 | User: Record | null; 73 | Peer: Record | null; 74 | }; 75 | 76 | export type TailscaleUser = { 77 | ID: number; 78 | LoginName: string; 79 | DisplayName: string; 80 | ProfilePicURL: string; 81 | Roles: string[]; 82 | }; 83 | 84 | export type TailscaleUp = { 85 | BackendState: TailscaleBackendState; 86 | AuthURL?: string; 87 | QR?: string; 88 | }; -------------------------------------------------------------------------------- /test/check-application: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 -cimport os, sys; os.execv(os.path.dirname(sys.argv[1]) + "/common/pywrap", sys.argv) 2 | 3 | # Run this with --help to see available options for tracing and debugging 4 | # See https://github.com/cockpit-project/cockpit/blob/main/test/common/testlib.py 5 | # "class Browser" and "class MachineCase" for the available API. 6 | 7 | import testlib 8 | 9 | # Nondestructive tests all run in the same running VM. This allows them to run in Packit, Fedora, and RHEL dist-git gating 10 | # They must not permanently change any file or configuration on the system in a way that influences other tests. 11 | @testlib.nondestructive 12 | class TestApplication(testlib.MachineCase): 13 | def testBasic(self): 14 | b = self.browser 15 | m = self.machine 16 | 17 | self.login_and_go("/starter-kit") 18 | # verify expected heading 19 | b.wait_text(".pf-v5-c-card__title", "Starter Kit") 20 | 21 | # verify expected host name 22 | hostname = m.execute("cat /etc/hostname").strip() 23 | b.wait_in_text(".pf-v5-c-alert__title", "Running on " + hostname) 24 | 25 | # change current hostname 26 | self.write_file("/etc/hostname", "new-" + hostname) 27 | # verify new hostname name 28 | b.wait_in_text(".pf-v5-c-alert__title", "Running on new-" + hostname) 29 | 30 | # change language to German 31 | b.switch_to_top() 32 | # the menu and dialog changed several times 33 | b.click("#toggle-menu") 34 | b.click(".display-language-menu") 35 | b.wait_popup('display-language-modal') 36 | b.click("#display-language-modal [data-value='de-de'] button") 37 | b.click("#display-language-modal button.pf-m-primary") 38 | b.wait_visible("#content") 39 | # menu label (from manifest) should be translated 40 | b.wait_text("#host-apps a[href='/starter-kit']", "Bausatz") 41 | # window title should be translated; this is not considered as "visible" 42 | self.assertIn("Bausatz", b.call_js_func("ph_text", "head title")) 43 | 44 | b.go("/starter-kit") 45 | b.enter_page("/starter-kit") 46 | # page label (from js) should be translated 47 | b.wait_in_text(".pf-v5-c-alert__title", "Läuft auf") 48 | 49 | 50 | if __name__ == '__main__': 51 | testlib.test_main() 52 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const CopyPlugin = require("copy-webpack-plugin"); 2 | const HtmlWebpackPlugin = require('html-webpack-plugin'); 3 | const MiniCssExtractPlugin = require('mini-css-extract-plugin'); 4 | const path = require('path'); 5 | const { stylePaths } = require('./stylePaths'); 6 | 7 | const copy_files = [ 8 | "./src/manifest.json", 9 | ]; 10 | 11 | module.exports = { 12 | devtool: "source-map", 13 | entry: './src/index.tsx', 14 | module: { 15 | rules: [ 16 | { 17 | test: /\.(ts|tsx|jsx)?$/, 18 | use: [ 19 | { 20 | loader: 'ts-loader', 21 | options: { 22 | transpileOnly: true, 23 | experimentalWatchApi: true, 24 | }, 25 | } 26 | ] 27 | }, 28 | { 29 | test: /\.s[ac]ss$/i, 30 | use: [ 31 | "css-loader", 32 | "sass-loader", 33 | ], 34 | }, 35 | { 36 | test: /\.css$/, 37 | include: [...stylePaths], 38 | use: [MiniCssExtractPlugin.loader, 'css-loader'], 39 | }, { 40 | test: /\.(svg|ttf|eot|woff|woff2)$/, 41 | // only process modules with this loader 42 | // if they live under a 'fonts' or 'pficon' directory 43 | include: [ 44 | path.resolve(__dirname, 'node_modules/patternfly/dist/fonts'), 45 | path.resolve(__dirname, 'node_modules/@patternfly/react-core/dist/styles/assets/fonts'), 46 | path.resolve(__dirname, 'node_modules/@patternfly/react-core/dist/styles/assets/pficon'), 47 | path.resolve(__dirname, 'node_modules/@patternfly/patternfly/assets/fonts'), 48 | path.resolve(__dirname, 'node_modules/@patternfly/patternfly/assets/pficon'), 49 | ], 50 | use: { 51 | loader: 'file-loader', 52 | options: { 53 | // Limit at 50k. larger files emited into separate files 54 | limit: 5000, 55 | outputPath: 'fonts', 56 | name: '[name].[ext]', 57 | }, 58 | }, 59 | }, 60 | { 61 | test: /\.svg$/, 62 | include: (input) => input.indexOf('background-filter.svg') > 1, 63 | use: [ 64 | { 65 | loader: 'url-loader', 66 | options: { 67 | limit: 5000, 68 | outputPath: 'svgs', 69 | name: '[name].[ext]', 70 | }, 71 | }, 72 | ], 73 | }, 74 | { 75 | test: /\.(jpg|jpeg|png|gif)$/i, 76 | include: [ 77 | path.resolve(__dirname, 'src'), 78 | path.resolve(__dirname, 'node_modules/patternfly'), 79 | path.resolve(__dirname, 'node_modules/@patternfly/patternfly/assets/images'), 80 | path.resolve(__dirname, 'node_modules/@patternfly/react-styles/css/assets/images'), 81 | path.resolve(__dirname, 'node_modules/@patternfly/react-core/dist/styles/assets/images'), 82 | path.resolve( 83 | __dirname, 84 | 'node_modules/@patternfly/react-core/node_modules/@patternfly/react-styles/css/assets/images' 85 | ), 86 | path.resolve( 87 | __dirname, 88 | 'node_modules/@patternfly/react-table/node_modules/@patternfly/react-styles/css/assets/images' 89 | ), 90 | path.resolve( 91 | __dirname, 92 | 'node_modules/@patternfly/react-inline-edit-extension/node_modules/@patternfly/react-styles/css/assets/images' 93 | ), 94 | ], 95 | use: [ 96 | { 97 | loader: 'url-loader', 98 | options: { 99 | limit: 5000, 100 | outputPath: 'images', 101 | name: '[name].[ext]', 102 | }, 103 | }, 104 | ], 105 | }, 106 | ], 107 | }, 108 | resolve: { 109 | extensions: ['.tsx', '.ts', '.js'], 110 | modules: ["node_modules", 'pkg/lib'], 111 | alias: { 'font-awesome': 'font-awesome-sass/assets/stylesheets' }, 112 | }, 113 | resolveLoader: { 114 | modules: ["node_modules", 'pkg/lib'], 115 | }, 116 | plugins: [ 117 | new CopyPlugin({ 118 | patterns: copy_files 119 | }), 120 | new HtmlWebpackPlugin({ 121 | template: path.resolve(__dirname, 'src', 'index.html'), 122 | }), 123 | new MiniCssExtractPlugin({ 124 | filename: 'bundle.css', 125 | }), 126 | ], 127 | output: { 128 | filename: 'bundle.js', 129 | path: path.resolve(__dirname, 'dist'), 130 | }, 131 | devServer: { 132 | static: path.join(__dirname, "dist"), 133 | compress: true, 134 | port: 4000, 135 | }, 136 | }; 137 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Cockpit application to manage Tailscale 2 | ======================================= 3 | 4 | !["Prompt"](https://raw.githubusercontent.com/gbraad/assets/gh-pages/icons/prompt-icon-64.png) 5 | 6 | 7 | A Cockpit application to manage Tailscale 8 | 9 | ![Screenshot](./docs/screenshot.png) 10 | 11 | 12 | 13 | Development 14 | ----------- 15 | 16 | This repository includes deployment scripts for the Cocpit Tailscale development environment. 17 | The easiest to get started is by using the following cloud development environments: 18 | 19 | * Open in [Gitpod workspace](https://gitpod.io/#https://github.com/spotsnel/cockpit-tailscale) 20 | * Open in [CodeSandbox](https://codesandbox.io/p/github/spotsnel/cockpit-tailscale) 21 | 22 | or you can either use a local `devsys`/`almsys`, as published here: 23 | 24 | * https://github.com/gbraad-devenv/fedora 25 | * https://github.com/gbraad-devenv/almalinux 26 | 27 | 28 | 29 | ### Preparation 30 | 31 | Install the following packages to develop and build: 32 | 33 | ```bash 34 | $ sudo dnf install -y make npm 35 | ``` 36 | 37 | and to make the RPM you need: 38 | 39 | ```bash 40 | $ sudo dnf install -y rpm-build gettext libappstream-glib 41 | ``` 42 | 43 | 44 | #### Cockpit user 45 | 46 | If you want to run Cockpit, you need a user with a password: 47 | 48 | ```bash 49 | $ sudo dnf install -y passwd 50 | $ sudo passwd gbraad 51 | ``` 52 | 53 | After which you can use this user to log in to Cockpit. 54 | 55 | 56 | ### Build 57 | 58 | To perform a development build: 59 | ```bash 60 | $ npm run dev 61 | ```` 62 | 63 | To perform a production build: 64 | ```bash 65 | $ npm run build 66 | ``` 67 | 68 | For the RPM package: 69 | ```bash 70 | $ npm run rpm 71 | ``` 72 | 73 | 74 | ### Cockpit 75 | 76 | After the build, copy contents to `/usr/share/cockpit/tailscale`, `/usr/share/local/cockpit/tailscale` or `~/.local/share/cockpit/tailscale`. 77 | 78 | #### Link development 79 | 80 | For convenience, you can also create a symlink to `~/.local/share/cockpit/tailscale` to `$PWD/dist`. However, you will need to log out and log in because Cockpit caches the page and assets. 81 | 82 | To create a link: 83 | 84 | ```bash 85 | $ npm run link 86 | ``` 87 | 88 | And to remove: 89 | 90 | ```bash 91 | $ npm run unlink 92 | ``` 93 | 94 | Note: this only works when the current user also logs in. Otherwise, use the tasks 95 | `linkusr` and `unlinkusr` which uses `sudo` to create the link in `/usr/local/share/cockpit`. 96 | 97 | 98 | #### Run Cockpit 99 | 100 | You can run Cockpit in a container or remote development environment with the following command: 101 | 102 | ```bash 103 | $ npm run cockpit 104 | ``` 105 | 106 | You will need to use an account with a password to log in. 107 | 108 | 109 | #### Origins 110 | 111 | If the login fails and you see `bad Origin` errors, you need to modify the `/etc/cockpit/cockpit.conf` file and add something like: 112 | 113 | ```ini 114 | [WebService] 115 | Origins=https://jqgnyj-9090.csb.app 116 | ``` 117 | 118 | The example shows CodeSandbox. For Gitpod this might look like this: 119 | ```ini 120 | [WebService] 121 | Origins=https://9090-spotsnel-cockpittailsca-57e5sbbb0zb.ws-us100.gitpod.io 122 | ``` 123 | 124 | 125 | ### Tailscale systemd image 126 | You can run this as part of [spotsnel/tailscale-systemd](https://github.com/spotsnel/tailscale-systemd) container image to deploy this inside a Podman machine or similar: 127 | ```bash 128 | $ tailscale ssh podmandesktop / podman exec -it tailscale-system bash 129 | # dnf install -y cockpit passwd 130 | # systemctl enable --now cockpit.socket 131 | # curl -L https://github.com/spotsnel/cockpit-tailscale/releases/download/v0.0.1/cockpit-tailscale-v0.0.1.tar.gz -o dist.tar.gz 132 | # tar zxvf dist.tar.gz 133 | # mkdir /usr/local/share/cockpit 134 | # mv dist /usr/local/share/cockpit/tailscale 135 | # passwd root 136 | # tailscale up --ssh 137 | ``` 138 | 139 | Now you can access the remote cockpit from another host by 'add new host'. 140 | Note: remote hosts get authenticated over SSH. If you have conflicts, like on WSL, you can serve on `localhost` instead. 141 | 142 | `/etc/systemd/system/cockpit.socket.d/listen.conf` 143 | ```ini 144 | [Socket] 145 | ListenStream= 146 | ListenStream=127.0.0.1:9090 147 | FreeBind=yes 148 | ``` 149 | 150 | Note: the blank `ListenStream` is intentional as it resets the parameter. 151 | 152 | Now set up the forward from the Tailscale client to open port `9090`: 153 | 154 | ```bash 155 | # tailscale serve tcp:9090 tcp://localhost:9090 156 | # systemctl daemon-reload 157 | # systemctl restart cockpit.socket 158 | ``` 159 | 160 | Now you can navigate to the Tailscale IP: 161 | ``` 162 | # tailscale ip -4 163 | 100.113.113.114 164 | ``` 165 | 166 | Open https://100.113.113.114:9090. 167 | 168 | 169 | Authors 170 | ------- 171 | 172 | | [!["Gerard Braad"](http://gravatar.com/avatar/e466994eea3c2a1672564e45aca844d0.png?s=60)](http://gbraad.nl "Gerard Braad ") | 173 | |---| 174 | | [@gbraad](https://gbraad.nl/social) | 175 | -------------------------------------------------------------------------------- /src/app.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import { TailscaleBackendState, TailscalePeer, TailscaleStatus, TailscaleUp, TailscaleVersion } from './types'; 4 | import { Icon } from '@patternfly/react-core'; 5 | import ExclamationCircleIcon from '@patternfly/react-icons/dist/esm/icons/exclamation-circle-icon'; 6 | import CheckCircleIcon from '@patternfly/react-icons/dist/esm/icons/check-circle-icon'; 7 | import InfoCircleIcon from '@patternfly/react-icons/dist/esm/icons/info-circle-icon'; 8 | import { 9 | ExpandableRowContent, 10 | Table, Caption, Thead, Tbody, Tr, Th, Td, 11 | SortByDirection, 12 | } from '@patternfly/react-table'; 13 | 14 | type ApplicationProps = { 15 | } 16 | 17 | type ApplicationState = { 18 | Status: TailscaleStatus 19 | Version: TailscaleVersion 20 | } 21 | 22 | export class Application extends React.Component { 23 | state: ApplicationState = { 24 | Status: null, 25 | Version: null 26 | } 27 | 28 | constructor(props: ApplicationProps) { 29 | super(props); 30 | 31 | cockpit 32 | .spawn(['tailscale', 'version', '--json']) 33 | .done(content => { 34 | const version: TailscaleVersion = JSON.parse(content) 35 | this.setState(state => ({ Version: version })); 36 | }); 37 | 38 | cockpit 39 | .spawn(['tailscale', 'status', '--json']) 40 | .done(content => { 41 | const status: TailscaleStatus = JSON.parse(content) 42 | this.setState(state => ({ Status: status })); 43 | }); 44 | } 45 | 46 | render() { 47 | 48 | function isNotSharee(peer: TailscalePeer): peer is TailscalePeer { 49 | return peer.DNSName !== ""; // ShareeNode doesn't work? 50 | } 51 | 52 | return ( 53 | <> 54 | { 55 | this.state.Status != null 56 | ?
57 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | {this.state.Status.Self.Self = true} 76 | 77 | { 78 | Object.values(this.state.Status.Peer) 79 | .filter(isNotSharee) 80 | .map(peer => { 81 | return 82 | } 83 | ) 84 | } 85 | 86 |
Tailscale peers
IPHostnameNetworkTagsStateExit nodeOSTraffic
87 | 88 |
Tailscale {this.state.Version.majorMinorPatch}
89 |
90 | 91 | :

Loading...

92 | } 93 | 94 | ); 95 | } 96 | } 97 | 98 | 99 | class Peer extends React.Component { 100 | render() { 101 | 102 | const name = this.props.DNSName.split('.'); 103 | const hostName = name[0]; 104 | const network = name[1] + '.' + 'ts.net'; 105 | 106 | var tags = "-" 107 | if (this.props.Tags) { 108 | const mapped_items = this.props.Tags?.map(t => { return t.split(":")[1] }) 109 | tags = mapped_items.join(', ') 110 | } 111 | 112 | return ( 113 | 114 | 115 | {this.props.Online 116 | ? 117 | : 118 | } 119 | {this.props.TailscaleIPs[0]} 120 | {hostName} 121 | {network} 122 | { 123 | tags 124 | } 125 | 126 | {this.props.Self 127 | ? "Self" 128 | : this.props.Active 129 | ? this.props.CurAddr != "" 130 | ? "Direct" 131 | : "Relay: " + this.props.Relay 132 | : this.props.Online 133 | ? "Idle" 134 | : "-" 135 | } 136 | {this.props.ExitNode 137 | ? "Current" 138 | : this.props.ExitNodeOption 139 | ? "Yes" 140 | : "-" 141 | } 142 | {this.props.OS} 143 | {this.props.Self 144 | ? "-" 145 | : "" + this.props.TxBytes + " / " + this.props.RxBytes 146 | } 147 | ); 148 | } 149 | } 150 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | # extract name from package.json 2 | PACKAGE_NAME := $(shell awk '/"name":/ {gsub(/[",]/, "", $$2); print $$2}' package.json) 3 | RPM_NAME := cockpit-$(PACKAGE_NAME) 4 | VERSION := $(shell T=$$(git describe 2>/dev/null) || T=1; echo $$T | tr '-' '.') 5 | ifeq ($(TEST_OS),) 6 | TEST_OS = centos-8-stream 7 | endif 8 | export TEST_OS 9 | TARFILE=$(RPM_NAME)-$(VERSION).tar.xz 10 | NODE_CACHE=$(RPM_NAME)-node-$(VERSION).tar.xz 11 | SPEC=$(RPM_NAME).spec 12 | PREFIX ?= /usr/local 13 | APPSTREAMFILE=org.cockpit-project.$(PACKAGE_NAME).metainfo.xml 14 | VM_IMAGE=$(CURDIR)/test/images/$(TEST_OS) 15 | # stamp file to check for node_modules/ 16 | NODE_MODULES_TEST=package-lock.json 17 | # one example file in dist/ from bundler to check if that already ran 18 | DIST_TEST=dist/manifest.json 19 | # one example file in pkg/lib to check if it was already checked out 20 | COCKPIT_REPO_STAMP=pkg/lib/cockpit-po-plugin.js 21 | # common arguments for tar, mostly to make the generated tarballs reproducible 22 | TAR_ARGS = --sort=name --mtime "@$(shell git show --no-patch --format='%at')" --mode=go=rX,u+rw,a-s --numeric-owner --owner=0 --group=0 23 | 24 | all: $(DIST_TEST) 25 | 26 | # checkout common files from Cockpit repository required to build this project; 27 | # this has no API stability guarantee, so check out a stable tag when you start 28 | # a new project, use the latest release, and update it from time to time 29 | COCKPIT_REPO_FILES = \ 30 | pkg/lib \ 31 | test/common \ 32 | $(NULL) 33 | 34 | COCKPIT_REPO_URL = https://github.com/cockpit-project/cockpit.git 35 | COCKPIT_REPO_COMMIT = 536834c40ad3e2390a52fb87583f07302e2a29a4 # 294 + 14 commits 36 | 37 | $(COCKPIT_REPO_FILES): $(COCKPIT_REPO_STAMP) 38 | COCKPIT_REPO_TREE = '$(strip $(COCKPIT_REPO_COMMIT))^{tree}' 39 | $(COCKPIT_REPO_STAMP): Makefile 40 | @git rev-list --quiet --objects $(COCKPIT_REPO_TREE) -- 2>/dev/null || \ 41 | git fetch --no-tags --no-write-fetch-head --depth=1 $(COCKPIT_REPO_URL) $(COCKPIT_REPO_COMMIT) 42 | git archive $(COCKPIT_REPO_TREE) -- $(COCKPIT_REPO_FILES) | tar x 43 | 44 | # 45 | # i18n 46 | # 47 | 48 | LINGUAS=$(basename $(notdir $(wildcard po/*.po))) 49 | 50 | po/$(PACKAGE_NAME).js.pot: 51 | xgettext --default-domain=$(PACKAGE_NAME) --output=$@ --language=C --keyword= \ 52 | --keyword=_:1,1t --keyword=_:1c,2,2t --keyword=C_:1c,2 \ 53 | --keyword=N_ --keyword=NC_:1c,2 \ 54 | --keyword=gettext:1,1t --keyword=gettext:1c,2,2t \ 55 | --keyword=ngettext:1,2,3t --keyword=ngettext:1c,2,3,4t \ 56 | --keyword=gettextCatalog.getString:1,3c --keyword=gettextCatalog.getPlural:2,3,4c \ 57 | --from-code=UTF-8 $$(find src/ -name '*.js' -o -name '*.jsx') 58 | 59 | po/$(PACKAGE_NAME).html.pot: $(NODE_MODULES_TEST) $(COCKPIT_REPO_STAMP) 60 | pkg/lib/html2po.js -o $@ $$(find src -name '*.html') 61 | 62 | po/$(PACKAGE_NAME).manifest.pot: $(NODE_MODULES_TEST) $(COCKPIT_REPO_STAMP) 63 | pkg/lib/manifest2po.js src/manifest.json -o $@ 64 | 65 | po/$(PACKAGE_NAME).metainfo.pot: $(APPSTREAMFILE) 66 | xgettext --default-domain=$(PACKAGE_NAME) --output=$@ $< 67 | 68 | po/$(PACKAGE_NAME).pot: po/$(PACKAGE_NAME).html.pot po/$(PACKAGE_NAME).js.pot po/$(PACKAGE_NAME).manifest.pot po/$(PACKAGE_NAME).metainfo.pot 69 | msgcat --sort-output --output-file=$@ $^ 70 | 71 | po/LINGUAS: 72 | echo $(LINGUAS) | tr ' ' '\n' > $@ 73 | 74 | # 75 | # Build/Install/dist 76 | # 77 | 78 | $(SPEC): packaging/$(SPEC).in $(NODE_MODULES_TEST) 79 | provides=$$(npm ls --omit dev --package-lock-only --depth=Infinity | grep -Eo '[^[:space:]]+@[^[:space:]]+' | sort -u | sed 's/^/Provides: bundled(npm(/; s/\(.*\)@/\1)) = /'); \ 80 | awk -v p="$$provides" '{gsub(/%{VERSION}/, "$(VERSION)"); gsub(/%{NPM_PROVIDES}/, p)}1' $< > $@ 81 | 82 | $(DIST_TEST): $(NODE_MODULES_TEST) $(COCKPIT_REPO_STAMP) $(shell find src/ -type f) package.json 83 | NODE_ENV=$(NODE_ENV) npm run build 84 | 85 | watch: $(NODE_MODULES_TEST) $(COCKPIT_REPO_STAMP) 86 | NODE_ENV=$(NODE_ENV) npm run watch 87 | 88 | clean: 89 | rm -rf dist/ 90 | rm -f $(SPEC) 91 | rm -f po/LINGUAS 92 | 93 | install: $(DIST_TEST) po/LINGUAS 94 | mkdir -p $(DESTDIR)$(PREFIX)/share/cockpit/$(PACKAGE_NAME) 95 | cp -r dist/* $(DESTDIR)$(PREFIX)/share/cockpit/$(PACKAGE_NAME) 96 | mkdir -p $(DESTDIR)$(PREFIX)/share/metainfo/ 97 | msgfmt --xml -d po \ 98 | --template $(APPSTREAMFILE) \ 99 | -o $(DESTDIR)$(PREFIX)/share/metainfo/$(APPSTREAMFILE) 100 | 101 | # this requires a built source tree and avoids having to install anything system-wide 102 | devel-install: $(DIST_TEST) 103 | mkdir -p ~/.local/share/cockpit 104 | ln -s `pwd`/dist ~/.local/share/cockpit/$(PACKAGE_NAME) 105 | 106 | # assumes that there was symlink set up using the above devel-install target, 107 | # and removes it 108 | devel-uninstall: 109 | rm -f ~/.local/share/cockpit/$(PACKAGE_NAME) 110 | 111 | print-version: 112 | @echo "$(VERSION)" 113 | 114 | dist: $(TARFILE) 115 | @ls -1 $(TARFILE) 116 | 117 | # when building a distribution tarball, call bundler with a 'production' environment 118 | # we don't ship node_modules for license and compactness reasons; we ship a 119 | # pre-built dist/ (so it's not necessary) and ship package-lock.json (so that 120 | # node_modules/ can be reconstructed if necessary) 121 | $(TARFILE): export NODE_ENV=production 122 | $(TARFILE): $(DIST_TEST) $(SPEC) 123 | if type appstream-util >/dev/null 2>&1; then appstream-util validate-relax --nonet *.metainfo.xml; fi 124 | tar --xz $(TAR_ARGS) -cf $(TARFILE) --transform 's,^,$(RPM_NAME)/,' \ 125 | --exclude packaging/$(SPEC).in --exclude node_modules \ 126 | $$(git ls-files) $(COCKPIT_REPO_FILES) $(NODE_MODULES_TEST) $(SPEC) dist/ 127 | 128 | $(NODE_CACHE): $(NODE_MODULES_TEST) 129 | tar --xz $(TAR_ARGS) -cf $@ node_modules 130 | 131 | node-cache: $(NODE_CACHE) 132 | 133 | # convenience target for developers 134 | srpm: $(TARFILE) $(NODE_CACHE) $(SPEC) 135 | rpmbuild -bs \ 136 | --define "_sourcedir `pwd`" \ 137 | --define "_srcrpmdir `pwd`" \ 138 | $(SPEC) 139 | 140 | # convenience target for developers 141 | rpm: $(TARFILE) $(NODE_CACHE) $(SPEC) 142 | mkdir -p "`pwd`/output" 143 | mkdir -p "`pwd`/rpmbuild" 144 | rpmbuild -bb \ 145 | --define "_sourcedir `pwd`" \ 146 | --define "_specdir `pwd`" \ 147 | --define "_builddir `pwd`/rpmbuild" \ 148 | --define "_srcrpmdir `pwd`" \ 149 | --define "_rpmdir `pwd`/output" \ 150 | --define "_buildrootdir `pwd`/build" \ 151 | $(SPEC) 152 | find `pwd`/output -name '*.rpm' -printf '%f\n' -exec mv {} . \; 153 | rm -r "`pwd`/rpmbuild" 154 | rm -r "`pwd`/output" "`pwd`/build" 155 | 156 | ifeq ("$(TEST_SCENARIO)","pybridge") 157 | COCKPIT_PYBRIDGE_REF = main 158 | COCKPIT_WHEEL = cockpit-0-py3-none-any.whl 159 | 160 | $(COCKPIT_WHEEL): 161 | pip wheel git+https://github.com/cockpit-project/cockpit.git@${COCKPIT_PYBRIDGE_REF} 162 | 163 | VM_DEPENDS = $(COCKPIT_WHEEL) 164 | VM_CUSTOMIZE_FLAGS = --install $(COCKPIT_WHEEL) 165 | endif 166 | 167 | # build a VM with locally built distro pkgs installed 168 | # disable networking, VM images have mock/pbuilder with the common build dependencies pre-installed 169 | $(VM_IMAGE): $(TARFILE) $(NODE_CACHE) bots test/vm.install $(VM_DEPENDS) 170 | bots/image-customize --no-network --fresh \ 171 | $(VM_CUSTOMIZE_FLAGS) \ 172 | --upload $(NODE_CACHE):/var/tmp/ --build $(TARFILE) \ 173 | --script $(CURDIR)/test/vm.install $(TEST_OS) 174 | 175 | # convenience target for the above 176 | vm: $(VM_IMAGE) 177 | @echo $(VM_IMAGE) 178 | 179 | # convenience target to print the filename of the test image 180 | print-vm: 181 | @echo $(VM_IMAGE) 182 | 183 | # convenience target to setup all the bits needed for the integration tests 184 | # without actually running them 185 | prepare-check: $(NODE_MODULES_TEST) $(VM_IMAGE) test/common 186 | 187 | # run the browser integration tests; skip check for SELinux denials 188 | # this will run all tests/check-* and format them as TAP 189 | check: prepare-check 190 | TEST_AUDIT_NO_SELINUX=1 test/common/run-tests ${RUN_TESTS_OPTIONS} 191 | 192 | # checkout Cockpit's bots for standard test VM images and API to launch them 193 | bots: $(COCKPIT_REPO_STAMP) 194 | test/common/make-bots 195 | 196 | $(NODE_MODULES_TEST): package.json 197 | # if it exists already, npm install won't update it; force that so that we always get up-to-date packages 198 | rm -f package-lock.json 199 | # unset NODE_ENV, skips devDependencies otherwise 200 | env -u NODE_ENV npm install --ignore-scripts 201 | env -u NODE_ENV npm prune 202 | 203 | .PHONY: all clean install devel-install devel-uninstall print-version dist node-cache rpm prepare-check check vm print-vm 204 | -------------------------------------------------------------------------------- /@types/cockpitjs/index.d.ts: -------------------------------------------------------------------------------- 1 | declare namespace Cockpit { 2 | /********************************** 3 | * Some helpful primitive typedefs 4 | *********************************/ 5 | 6 | type integer = number; //A typedef for an integer. Doesn't actually prevent compilation, but provides an IDE hint 7 | 8 | /********************************** 9 | * Cockpit D-Bus 10 | * http://cockpit-project.org/guide/latest/cockpit-dbus.html 11 | *********************************/ 12 | 13 | type BYTE = number; 14 | type BOOLEAN = boolean; 15 | type INT16 = number; 16 | type UINT16 = number; 17 | type INT32 = number; 18 | type UINT32 = number; 19 | type INT64 = number; 20 | type UINT64 = number; 21 | type DOUBLE = number; 22 | type STRING = string; 23 | type OBJECT_PATH = string; 24 | type SIGNATURE = string; 25 | type ARRAY_BYTE = string[]; 26 | type ARRAY_DICT_ENTRY_STRING = object; 27 | type ARRAY_DICT_ENTRY_OTHER = object; 28 | type ARRAY_OTHER = any[]; 29 | interface VARIANT { 30 | "t": STRING, 31 | "v": any 32 | } 33 | //TODO - Not sure on specifics for handle 34 | type HANDLE = object; 35 | 36 | interface DBusOptions { 37 | "bus" : string 38 | "host" : string 39 | "superuser" : string 40 | "track" : string 41 | } 42 | 43 | interface DBusProxy { 44 | client : string 45 | path : string 46 | iface : string 47 | valid : boolean 48 | data : object 49 | 50 | } 51 | 52 | //Todo unfinished 53 | interface DBusClient { 54 | 55 | } 56 | 57 | /********************************** 58 | * Cockpit File Access 59 | * http://cockpit-project.org/guide/latest/cockpit-file.html 60 | **********************************/ 61 | 62 | interface ParsingFunction { 63 | (data: string) : string 64 | } 65 | 66 | interface StringifyingFunction { 67 | (data: string) : string 68 | } 69 | 70 | interface SyntaxObject { 71 | parse: ParsingFunction 72 | stringify: StringifyingFunction 73 | } 74 | 75 | interface FileAccessOptions { 76 | syntax?: SyntaxObject, 77 | binary?: boolean, 78 | max_read_size?: integer, 79 | superuser?: string, 80 | host?: string 81 | } 82 | 83 | interface FileReadDoneCallback { 84 | (content: string, tag: string) : void 85 | } 86 | 87 | interface FileReadFailCallback { 88 | (error: string) : void 89 | } 90 | 91 | interface FileReadPromise { 92 | done (callback : FileReadDoneCallback) : FileReadPromise 93 | fail (callback : FileReadFailCallback) : FileReadPromise 94 | } 95 | 96 | interface FileReplaceDoneCallback { 97 | (newTag: string) : void 98 | } 99 | 100 | interface FileReplaceFailCallback { 101 | (error: string) : void 102 | } 103 | 104 | interface FileReplacePromise { 105 | done (callback : FileReplaceDoneCallback) : FileReplacePromise 106 | fail (callback : FileReplaceFailCallback) : FileReplacePromise 107 | } 108 | 109 | interface FileModifyDoneCallback { 110 | (newContent : string, newTag: string) : void 111 | } 112 | 113 | interface FileModifyFailCallback { 114 | (error: string) : void 115 | } 116 | 117 | interface FileModifyPromise { 118 | done (callback : FileModifyDoneCallback) : FileModifyPromise 119 | fail (callback : FileModifyFailCallback) : FileModifyPromise 120 | } 121 | 122 | interface FileWatchCallback { 123 | content : string, 124 | tag : string, 125 | error? : any //TODO - what is the error content? 126 | } 127 | 128 | interface File { 129 | read () : FileReadPromise 130 | replace (content : string, expected_tag?: string) : FileReplacePromise 131 | modify (callback : any, initial_content?: string, initial_tag?: string) : FileModifyPromise 132 | watch (callback : FileWatchCallback) : void 133 | close () : void 134 | } 135 | 136 | /********************************** 137 | * Cockpit Processes 138 | * http://cockpit-project.org/guide/latest/cockpit-spawn.html 139 | **********************************/ 140 | 141 | interface ProcessFailureException { 142 | message?: string 143 | problem?: string 144 | exit_status?: integer 145 | exit_signal?: string 146 | } 147 | 148 | enum ProcessProblemCodes { 149 | "access-denied", //"The user is not permitted to perform the action in question." 150 | "authentication-failed", //"User authentication failed." 151 | "internal-error", //"An unexpected internal error without further info. This should not happen during the normal course of operations." 152 | "no-cockpit", //"The system does not have a compatible version of Cockpit installed or installed properly." 153 | "no-session", //"Cockpit is not logged in." 154 | "not-found", //"Something specifically requested was not found, such as a file, executable etc." 155 | "terminated", //"Something was terminated forcibly, such as a connection, process session, etc." 156 | "timeout", //"Something timed out." 157 | "unknown-hostkey", //"The remote host had an unexpected or unknown key." 158 | "no-forwarding" //"Could not forward authentication credentials to the remote host." 159 | } 160 | 161 | interface ProcessPromiseDoneCallback { 162 | (data: string, message?: string) : void 163 | } 164 | 165 | interface ProcessPromiseFailCallback { 166 | (exception: ProcessFailureException, data?: string) : void 167 | } 168 | 169 | interface ProcessPromiseStreamCallback { 170 | (data: string) : void 171 | } 172 | 173 | interface ProcessPromise { 174 | done( callback: ProcessPromiseDoneCallback ) : ProcessPromise, 175 | fail( callback: ProcessPromiseFailCallback ) : ProcessPromise, 176 | stream( callback: ProcessPromiseStreamCallback ) : ProcessPromise, 177 | input( data: string, stream?: boolean ) : ProcessPromise, 178 | close( problem?: ProcessProblemCodes ) : ProcessPromise, 179 | } 180 | 181 | /********************************** 182 | * Cockpit User Session 183 | * http://cockpit-project.org/guide/latest/cockpit-login.html 184 | **********************************/ 185 | 186 | interface UserSessionPermission { 187 | allowed : boolean 188 | onChanged : any //TODO need to see how to do events in TS 189 | close() : void 190 | } 191 | 192 | interface UserSessionObject { 193 | onchanged : any 194 | } 195 | 196 | interface UserSessionDetails { 197 | "id" : string //This is unix user id. 198 | "name" : string //This is the unix user name like "root". 199 | "full_name" : string //This is a readable name for the user. 200 | "groups" : string //This is an array of group names to which the user belongs. 201 | "home" : string //This is user's home directory. 202 | "shell" : string //This is unix user shell. 203 | } 204 | 205 | interface UserSessionPromiseDoneCallback { 206 | (user: UserSessionDetails) : void 207 | } 208 | 209 | interface UserSessionPromiseFailCallback { //Todo - is this defined? 210 | 211 | } 212 | 213 | interface UserSessionPromise { 214 | 215 | } 216 | 217 | /********************************** 218 | * Cockpit Object 219 | * Generally brought into your app in the root HTML file via a