kraaken 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 51ab5760351b020ed64ef73708af90f370666a9a7fb4abcd7f756925fe1ac46f
4
+ data.tar.gz: ed7f5cef0b9bbbc8a826c67c63c014043c6319436b94f8da791a6a953d6e6e9d
5
+ SHA512:
6
+ metadata.gz: 528e8155b5d9217c3a383e8d13c70f3aca0b4e9b1c439b186329c6d26a4b9cd03c878b513fc669de57a9dd01f357204e9b8c802ec0e190a2a2fe0ed5b589d9c2
7
+ data.tar.gz: eba0df01f94934a5e801bcf01ce6f4e65fd64a558e86ac0d4c28c4a88e82811e05b9d54dca32e199030c6ec6c5c3de2a59e549e9ce93ad826bee9ef96b9fb57f
data/.editorconfig ADDED
@@ -0,0 +1,12 @@
1
+ ; EditorConfig is awesome: http://EditorConfig.org
2
+
3
+ ; top-most EditorConfig file
4
+ root = true
5
+
6
+ ; Unix-style newlines with a newline ending every file
7
+ [*]
8
+ indent_style = spaces
9
+ end_of_line = lf
10
+ insert_final_newline = true
11
+ trim_trailing_whitespace = true
12
+ indent_size = 2
data/.rubocop.yml ADDED
@@ -0,0 +1,9 @@
1
+ AllCops:
2
+ TargetRubyVersion: 3.2
3
+ SuggestExtensions: false
4
+ Exclude:
5
+ - "scratchpad.rb"
6
+ - "vendor/**/*"
7
+
8
+ inherit_gem:
9
+ shimmer: config/rubocop_base.yml
data/.ruby-version ADDED
@@ -0,0 +1 @@
1
+ 3.2.2
@@ -0,0 +1,84 @@
1
+ # Contributor Covenant Code of Conduct
2
+
3
+ ## Our Pledge
4
+
5
+ We as members, contributors, and leaders pledge to make participation in our community a harassment-free experience for everyone, regardless of age, body size, visible or invisible disability, ethnicity, sex characteristics, gender identity and expression, level of experience, education, socio-economic status, nationality, personal appearance, race, religion, or sexual identity and orientation.
6
+
7
+ We pledge to act and interact in ways that contribute to an open, welcoming, diverse, inclusive, and healthy community.
8
+
9
+ ## Our Standards
10
+
11
+ Examples of behavior that contributes to a positive environment for our community include:
12
+
13
+ * Demonstrating empathy and kindness toward other people
14
+ * Being respectful of differing opinions, viewpoints, and experiences
15
+ * Giving and gracefully accepting constructive feedback
16
+ * Accepting responsibility and apologizing to those affected by our mistakes, and learning from the experience
17
+ * Focusing on what is best not just for us as individuals, but for the overall community
18
+
19
+ Examples of unacceptable behavior include:
20
+
21
+ * The use of sexualized language or imagery, and sexual attention or
22
+ advances of any kind
23
+ * Trolling, insulting or derogatory comments, and personal or political attacks
24
+ * Public or private harassment
25
+ * Publishing others' private information, such as a physical or email
26
+ address, without their explicit permission
27
+ * Other conduct which could reasonably be considered inappropriate in a
28
+ professional setting
29
+
30
+ ## Enforcement Responsibilities
31
+
32
+ Community leaders are responsible for clarifying and enforcing our standards of acceptable behavior and will take appropriate and fair corrective action in response to any behavior that they deem inappropriate, threatening, offensive, or harmful.
33
+
34
+ Community leaders have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, and will communicate reasons for moderation decisions when appropriate.
35
+
36
+ ## Scope
37
+
38
+ This Code of Conduct applies within all community spaces, and also applies when an individual is officially representing the community in public spaces. Examples of representing our community include using an official e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event.
39
+
40
+ ## Enforcement
41
+
42
+ Instances of abusive, harassing, or otherwise unacceptable behavior may be reported to the community leaders responsible for enforcement at jens@nerdgeschoss.de. All complaints will be reviewed and investigated promptly and fairly.
43
+
44
+ All community leaders are obligated to respect the privacy and security of the reporter of any incident.
45
+
46
+ ## Enforcement Guidelines
47
+
48
+ Community leaders will follow these Community Impact Guidelines in determining the consequences for any action they deem in violation of this Code of Conduct:
49
+
50
+ ### 1. Correction
51
+
52
+ **Community Impact**: Use of inappropriate language or other behavior deemed unprofessional or unwelcome in the community.
53
+
54
+ **Consequence**: A private, written warning from community leaders, providing clarity around the nature of the violation and an explanation of why the behavior was inappropriate. A public apology may be requested.
55
+
56
+ ### 2. Warning
57
+
58
+ **Community Impact**: A violation through a single incident or series of actions.
59
+
60
+ **Consequence**: A warning with consequences for continued behavior. No interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, for a specified period of time. This includes avoiding interactions in community spaces as well as external channels like social media. Violating these terms may lead to a temporary or permanent ban.
61
+
62
+ ### 3. Temporary Ban
63
+
64
+ **Community Impact**: A serious violation of community standards, including sustained inappropriate behavior.
65
+
66
+ **Consequence**: A temporary ban from any sort of interaction or public communication with the community for a specified period of time. No public or private interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, is allowed during this period. Violating these terms may lead to a permanent ban.
67
+
68
+ ### 4. Permanent Ban
69
+
70
+ **Community Impact**: Demonstrating a pattern of violation of community standards, including sustained inappropriate behavior, harassment of an individual, or aggression toward or disparagement of classes of individuals.
71
+
72
+ **Consequence**: A permanent ban from any sort of public interaction within the community.
73
+
74
+ ## Attribution
75
+
76
+ This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 2.0,
77
+ available at https://www.contributor-covenant.org/version/2/0/code_of_conduct.html.
78
+
79
+ Community Impact Guidelines were inspired by [Mozilla's code of conduct enforcement ladder](https://github.com/mozilla/diversity).
80
+
81
+ [homepage]: https://www.contributor-covenant.org
82
+
83
+ For answers to common questions about this code of conduct, see the FAQ at
84
+ https://www.contributor-covenant.org/faq. Translations are available at https://www.contributor-covenant.org/translations.
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2023 Jens Ravens
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
data/Rakefile ADDED
@@ -0,0 +1,4 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bundler/gem_tasks"
4
+ task default: []
data/exe/kraaken ADDED
@@ -0,0 +1,17 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ # Prevent failures from being reported twice.
5
+ Thread.report_on_exception = false
6
+
7
+ require_relative "../lib/kraaken"
8
+
9
+ begin
10
+ Kraaken::Cli::Main.start(ARGV)
11
+ rescue => e
12
+ puts " \e[31mERROR (#{e.class}): #{e.message}\e[0m"
13
+ puts e.backtrace
14
+ exit 1
15
+ rescue Interrupt
16
+ exit 0
17
+ end
data/kraaken.gemspec ADDED
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "lib/kraaken/version"
4
+
5
+ Gem::Specification.new do |spec|
6
+ spec.name = "kraaken"
7
+ spec.version = Kraaken::VERSION
8
+ spec.authors = ["Jens Ravens"]
9
+ spec.email = ["jens@nerdgeschoss.de"]
10
+
11
+ spec.summary = "Deploy stuff with docker. The easy way."
12
+ spec.description = "Kraaken helps you deploy your applications with docker, traefik and cloudflare tunnels."
13
+ spec.homepage = "https://github.com/nerdgeschoss/kraaken"
14
+ spec.license = "MIT"
15
+ spec.required_ruby_version = ">= 3.1.0"
16
+
17
+ spec.metadata["homepage_uri"] = spec.homepage
18
+ spec.metadata["source_code_uri"] = spec.homepage
19
+
20
+ # Specify which files should be added to the gem when it is released.
21
+ # The `git ls-files -z` loads the files in the RubyGem that have been added into git.
22
+ spec.files = Dir.chdir(__dir__) do
23
+ `git ls-files -z`.split("\x0").reject do |f|
24
+ (File.expand_path(f) == __FILE__) ||
25
+ f.start_with?("bin/", "test/", "spec/", "features/", ".git", ".circleci", "appveyor", "Gemfile")
26
+ end
27
+ end
28
+ spec.bindir = "exe"
29
+ spec.executables = spec.files.grep(%r{\Aexe/}) { |f| File.basename(f) }
30
+ spec.require_paths = ["lib"]
31
+
32
+ spec.add_dependency "activesupport", ">= 7.0"
33
+ spec.add_dependency "net-ssh", "~> 7.0"
34
+ spec.add_dependency "thor", "~> 1.2"
35
+ spec.add_dependency "hcloud", "~> 1.2"
36
+ spec.add_dependency "faraday", "~> 2.7"
37
+ spec.add_dependency "zeitwerk", "~> 2.5"
38
+ spec.add_dependency "ruby-progressbar", "~> 1.13"
39
+ end
@@ -0,0 +1,18 @@
1
+ #cloud-config
2
+ users:
3
+ - name: nerd
4
+ groups: users, admin
5
+ sudo: ALL=(ALL) NOPASSWD:ALL
6
+ shell: /bin/bash
7
+ ssh_authorized_keys: <%= keys.map { _1.public_key } %>
8
+ runcmd:
9
+ - ufw allow OpenSSH
10
+ - ufw enable
11
+ - sed -i -e '/^\(#\|\)PermitRootLogin/s/^.*$/PermitRootLogin no/' /etc/ssh/sshd_config
12
+ - sed -i -e '/^\(#\|\)PasswordAuthentication/s/^.*$/PasswordAuthentication no/' /etc/ssh/sshd_config
13
+ - sed -i -e '/^\(#\|\)X11Forwarding/s/^.*$/X11Forwarding no/' /etc/ssh/sshd_config
14
+ - sed -i -e '/^\(#\|\)MaxAuthTries/s/^.*$/MaxAuthTries 10/' /etc/ssh/sshd_config
15
+ - sed -i -e '/^\(#\|\)AllowTcpForwarding/s/^.*$/AllowTcpForwarding no/' /etc/ssh/sshd_config
16
+ - sed -i -e '/^\(#\|\)AllowAgentForwarding/s/^.*$/AllowAgentForwarding no/' /etc/ssh/sshd_config
17
+ - sed -i -e '/^\(#\|\)AuthorizedKeysFile/s/^.*$/AuthorizedKeysFile .ssh\/authorized_keys/' /etc/ssh/sshd_config
18
+ - sed -i '$a AllowUsers nerd' /etc/ssh/sshd_config
@@ -0,0 +1,36 @@
1
+ version: "3"
2
+
3
+ services:
4
+ traefik:
5
+ # The official v2 Traefik docker image
6
+ image: traefik:v2.10
7
+ # Enables the web UI and tells Traefik to listen to docker
8
+ command: --api.insecure=true --providers.docker
9
+ restart: always
10
+ container_name: traefik
11
+ networks:
12
+ - cftunnel-transport
13
+ - cloudflaretunnel
14
+ ports:
15
+ # The HTTP port
16
+ - "80:80"
17
+ # The Web UI (enabled by --api.insecure=true)
18
+ - "8080:8080"
19
+ volumes:
20
+ # So that Traefik can listen to the Docker events
21
+ - /var/run/docker.sock:/var/run/docker.sock
22
+
23
+ tunnel:
24
+ container_name: cloudflared-tunnel
25
+ image: cloudflare/cloudflared
26
+ restart: unless-stopped
27
+ command: tunnel run
28
+ environment:
29
+ - TUNNEL_TOKEN=<%= tunnel_token %>
30
+ networks:
31
+ - cftunnel-transport
32
+
33
+ networks:
34
+ cftunnel-transport:
35
+ cloudflaretunnel:
36
+ external: true
@@ -0,0 +1,71 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Kraaken::App
4
+ attr_reader :name, :server, :destination
5
+
6
+ def initialize(name:, destination:, server:, config:)
7
+ @name = name
8
+ @destination = destination
9
+ @server = server
10
+ @config = config
11
+ end
12
+
13
+ def environment
14
+ create
15
+ ssh.connect(server) do |ssh|
16
+ ssh.read_file("~/#{full_name}/.env")
17
+ end
18
+ end
19
+
20
+ def environment=(content)
21
+ ssh.connect(server) do |ssh|
22
+ ssh.write_file("~/#{full_name}/.env", content)
23
+ end
24
+ end
25
+
26
+ def create
27
+ ssh.connect(server) do |ssh|
28
+ ssh.run("mkdir -p ~/#{full_name}")
29
+ ssh.run("touch ~/#{full_name}/.env")
30
+ end
31
+ end
32
+
33
+ def deploy(file)
34
+ ssh.connect(server) do |ssh|
35
+ ssh.run <<~BASH
36
+ mkdir -p ~/#{full_name}
37
+ cd ~/#{full_name}
38
+ touch .env
39
+ BASH
40
+ ssh.write_file("~/#{full_name}/docker-compose.yml", config.load_template(file.path, app: self))
41
+ ssh.run <<~BASH
42
+ cd ~/#{full_name}
43
+ docker-compose pull
44
+ docker-compose up -d
45
+ BASH
46
+ end
47
+ end
48
+
49
+ def destroy
50
+ ssh.connect(server) do |ssh|
51
+ ssh.run("cd ~/#{full_name} && docker-compose down --volumes --remove-orphans")
52
+ ssh.run("rm -rf ~/#{full_name}")
53
+ end
54
+ end
55
+
56
+ def logs
57
+ ssh.connect(server) do |ssh|
58
+ ssh.run("cd ~/#{full_name} && docker-compose logs -f")
59
+ end
60
+ end
61
+
62
+ def full_name
63
+ "#{name}-#{destination}"
64
+ end
65
+
66
+ private
67
+
68
+ attr_reader :config
69
+
70
+ delegate :ssh, to: :config
71
+ end
@@ -0,0 +1,53 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Kraaken::Cli::App < Kraaken::Cli::Base
4
+ include Thor::Actions
5
+
6
+ class_option :server, aliases: "-s", desc: "server to run the command on", default: "app-1"
7
+ class_option :app, aliases: "-a", desc: "app to run the command on"
8
+ class_option :destination, aliases: "-d", desc: "destination environment", default: "production"
9
+
10
+ desc "credentials", "opens the credentials file"
11
+ def credentials
12
+ old_content = new_content = app.environment.strip
13
+ Tempfile.create(app.name) do |file|
14
+ file.write old_content
15
+ file.flush
16
+ logger.info "Opening #{file.path}"
17
+ run "code --wait #{file.path}"
18
+ file.rewind
19
+ new_content = file.read.strip
20
+ end
21
+ if new_content != old_content
22
+ logger.info "Updating #{app.name} credentials"
23
+ app.environment = new_content
24
+ end
25
+ end
26
+
27
+ desc "deploy", "deploys the app"
28
+ option :file, aliases: "-f", desc: "docker-compose file to deploy"
29
+ def deploy
30
+ default_path = File.expand_path("config/deploy/docker-compose.yml", destination_root)
31
+ file = File.new(options[:file]) if options[:file].present? && File.exist?(options[:file])
32
+ file = File.new(default_path) if !file && File.exist?(default_path)
33
+ logger.error "No docker-compose file found" and return unless file
34
+ app.deploy(file)
35
+ end
36
+
37
+ desc "destroy", "destroys the app"
38
+ def destroy
39
+ app.destroy
40
+ end
41
+
42
+ desc "logs", "shows the logs of the app"
43
+ def logs
44
+ app.logs
45
+ rescue IOError # prevent error when terminating the cli
46
+ end
47
+
48
+ private
49
+
50
+ def app
51
+ @app ||= Kraaken::App.new(server: options[:server], name: options[:app].presence || File.basename(destination_root), config:, destination: options[:destination])
52
+ end
53
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Kraaken::Cli::Base < Thor
4
+ def self.exit_on_failure?
5
+ true
6
+ end
7
+
8
+ protected
9
+
10
+ def config
11
+ @config ||= Kraaken::Config.new
12
+ end
13
+
14
+ def logger
15
+ config.logger
16
+ end
17
+ end
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Kraaken::Cli::Main < Kraaken::Cli::Base
4
+ desc "server", "Provision and manage servers"
5
+ subcommand "server", Kraaken::Cli::Server
6
+
7
+ desc "ssh", "Manage ssh connections and keys"
8
+ subcommand "ssh", Kraaken::Cli::Ssh
9
+
10
+ desc "app", "Manage apps"
11
+ subcommand "app", Kraaken::Cli::App
12
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Kraaken::Cli::Server < Kraaken::Cli::Base
4
+ desc "provision NAME", "Provision a new server"
5
+ method_option :group, aliases: "-g", desc: "assign the server to an access group, default: admin"
6
+ def provision(name)
7
+ groups = [options[:group] || "admin", "admin"].uniq
8
+ config.cloud.provision(name, groups:)
9
+ end
10
+
11
+ desc "list", "Lists all current servers"
12
+ def list
13
+ config.cloud.servers.each do |server|
14
+ logger.info "#{server.name} (#{server.status}) #{server.ip} #{server.public_ip}"
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Kraaken::Cli::Ssh < Kraaken::Cli::Base
4
+ desc "config", "updates the local ssh config file"
5
+ def config
6
+ super.ssh.regenerate_config
7
+ end
8
+ end
@@ -0,0 +1,4 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Kraaken::Cli
4
+ end
@@ -0,0 +1,65 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "hcloud"
4
+
5
+ class Kraaken::Cloud::Hetzner < Kraaken::Cloud
6
+ def provision(name, groups:)
7
+ logger.with_progress(total: 100) do
8
+ logger.info "Provisioning server #{name}"
9
+ keys = client.ssh_keys
10
+ user_data = config.load_template("cloud-config.yml", keys:)
11
+ options = {
12
+ name:,
13
+ server_type: "cax41",
14
+ image: "ubuntu-22.04",
15
+ labels: {group: groups.join(".")},
16
+ location: "nbg1",
17
+ networks: [client.networks.first.id],
18
+ ssh_keys: keys.map(&:id),
19
+ user_data:
20
+ }
21
+ logger.increment_progress by: 5
22
+ logger.info "Creating server"
23
+ action, server = client.servers.create(**options)
24
+ await_action action
25
+ logger.increment_progress by: 10
26
+ server = await_startup server
27
+ logger.info "Server started #{server.public_net.dig("ipv4", "ip")}"
28
+ logger.increment_progress by: 10
29
+ logger.info "Regenrating ssh config"
30
+ config.ssh.regenerate_config
31
+ logger.increment_progress by: 5
32
+ sleep 10
33
+ logger.increment_progress by: 10
34
+ logger.info "Rebooting server after applying cloud-config"
35
+ await_action server.reboot
36
+ logger.increment_progress by: 10
37
+ prepare name
38
+ end
39
+ end
40
+
41
+ def servers
42
+ client.servers.map { Kraaken::Cloud::Server.new(name: _1.name, ip: _1.private_net.first&.ip, public_ip: _1.public_net.dig("ipv4", "ip"), status: _1.status) }
43
+ end
44
+
45
+ private
46
+
47
+ def client
48
+ @client ||= Hcloud::Client.new(token: config.credentials.password("hetzner"))
49
+ end
50
+
51
+ def await_action(action)
52
+ while action.status == "running"
53
+ sleep 2
54
+ action = client.actions.find(action.id)
55
+ end
56
+ end
57
+
58
+ def await_startup(server)
59
+ while server.status == "initializing" || server.status == "starting" || server.status == "off"
60
+ sleep 3
61
+ server = client.servers.find(server.id)
62
+ end
63
+ server
64
+ end
65
+ end
@@ -0,0 +1,48 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Kraaken::Cloud
4
+ Server = Struct.new(:name, :ip, :public_ip, :status) do
5
+ def jump?
6
+ name == "jump"
7
+ end
8
+ end
9
+
10
+ def initialize(config:)
11
+ @config = config
12
+ end
13
+
14
+ def provision(name)
15
+ raise NotImplementedError
16
+ end
17
+
18
+ def prepare(name)
19
+ new_relic = config.credentials.credential("new-relic")
20
+ config.ssh.connect(name) do |ssh|
21
+ ssh.run <<~BASH
22
+ sudo apt update
23
+ sudo apt upgrade -y
24
+ sudo apt install -y docker.io docker-compose
25
+ sudo docker network create cloudflaretunnel
26
+ sudo gpasswd -a $USER docker
27
+ curl -Ls https://download.newrelic.com/install/newrelic-cli/scripts/install.sh | bash && sudo NEW_RELIC_API_KEY=#{new_relic.password} NEW_RELIC_ACCOUNT_ID=#{new_relic.username} NEW_RELIC_REGION=EU /usr/local/bin/newrelic install -y
28
+ BASH
29
+ end
30
+ logger.increment_progress by: 25
31
+ config.ssh.connect(name) do |ssh|
32
+ ssh.run "mkdir -p ~/traefik"
33
+ ssh.write_file "~/traefik/docker-compose.yml", config.load_template("traefik-compose.yml", tunnel_token: config.ingress.tunnel_token_for_name(name))
34
+ logger.increment_progress by: 20
35
+ ssh.run "cd ~/traefik && docker-compose up -d"
36
+ ssh.run "echo #{config.credentials.password("docker-registry")} | docker login ghcr.io -u USERNAME --password-stdin", log: false
37
+ end
38
+ end
39
+
40
+ def servers
41
+ raise NotImplementedError
42
+ end
43
+
44
+ protected
45
+
46
+ attr_reader :config
47
+ delegate :logger, to: :config
48
+ end
@@ -0,0 +1,96 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "faraday"
4
+
5
+ class Kraaken::Cloudflare
6
+ Tunnel = Struct.new(:id, :name, :status, :cloudflare, :_token) do
7
+ def token
8
+ _token || cloudflare.tunnel_token(id:)
9
+ end
10
+ end
11
+
12
+ def initialize(config:)
13
+ @config = config
14
+ end
15
+
16
+ def tunnels
17
+ client.get("/client/v4/accounts/#{credential.username}/cfd_tunnel").body["result"]&.map do |tunnel|
18
+ Tunnel.new(tunnel["id"], tunnel["name"], tunnel["status"], self)
19
+ end || raise("Could not fetch tunnels")
20
+ end
21
+
22
+ def create_tunnel(name)
23
+ tunnel_secret = Base64.strict_encode64 SecureRandom.hex(32)
24
+ body = {name:, tunnel_secret:, config_src: "cloudflare"}
25
+ config = {
26
+ config: {
27
+ ingress: [
28
+ {
29
+ hostname: "#{name}.server.nerdgeschoss.de",
30
+ service: "http://traefik:8080"
31
+ },
32
+ {
33
+ hostname: "*.#{name}.nerdgeschoss.de",
34
+ service: "http://traefik"
35
+ },
36
+ {
37
+ service: "http_status:404"
38
+ }
39
+ ]
40
+ }
41
+ }
42
+ res = client.post("/client/v4/accounts/#{credential.username}/cfd_tunnel", body).body["result"]
43
+ tunnel = Tunnel.new(res["id"], res["name"], res["status"], self, res["token"])
44
+ logger.info "Creating tunnel #{name} with id #{tunnel.id}"
45
+ client.put("/client/v4/accounts/#{credential.username}/cfd_tunnel/#{tunnel.id}/configurations", config)
46
+ server_dns = {
47
+ type: "CNAME",
48
+ proxied: true,
49
+ name: "#{name}.server.nerdgeschoss.de",
50
+ content: "#{tunnel.id}.cfargotunnel.com"
51
+ }
52
+ logger.info "Creating DNS record for #{name}.server.nerdgeschoss.de"
53
+ client.post("/client/v4/zones/#{zone_id}/dns_records", server_dns)
54
+ app_dns = {
55
+ type: "CNAME",
56
+ proxied: true,
57
+ name: "*.#{name}.nerdgeschoss.de",
58
+ content: "#{tunnel.id}.cfargotunnel.com"
59
+ }
60
+ logger.info "Creating DNS record for #{name}.server.nerdgeschoss.de"
61
+ client.post("/client/v4/zones/#{zone_id}/dns_records", app_dns)
62
+ logger.info "Tunnel #{name} created"
63
+ tunnel
64
+ end
65
+
66
+ def tunnel_token(id:)
67
+ client.get("/client/v4/accounts/#{credential.username}/cfd_tunnel/#{id}/token").body["result"] || raise("Could not fetch tunnel token")
68
+ end
69
+
70
+ def tunnel_token_for_name(name)
71
+ (tunnels.find { _1.name == name } || create_tunnel(name)).token
72
+ end
73
+
74
+ private
75
+
76
+ attr_reader :config
77
+
78
+ delegate :logger, to: :config
79
+
80
+ def client
81
+ @client ||= Faraday.new(url: "https://api.cloudflare.com") do |f|
82
+ f.request :authorization, "Bearer", credential.password
83
+ f.request :json
84
+ f.response :json
85
+ f.response :raise_error
86
+ end
87
+ end
88
+
89
+ def credential
90
+ @credential ||= config.credentials.credential("cloudflare")
91
+ end
92
+
93
+ def zone_id
94
+ config.credentials.password("cloudflare-zone")
95
+ end
96
+ end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Kraaken::Config
4
+ def credentials
5
+ @credentials ||= Kraaken::Credentials::OnePassword.new
6
+ end
7
+
8
+ def cloud
9
+ @cloud ||= Kraaken::Cloud::Hetzner.new(config: self)
10
+ end
11
+
12
+ def ssh
13
+ @ssh ||= Kraaken::Ssh.new(config: self)
14
+ end
15
+
16
+ def ingress
17
+ @ingress ||= Kraaken::Cloudflare.new(config: self)
18
+ end
19
+
20
+ def logger
21
+ @logger ||= Kraaken::Logger.new
22
+ end
23
+
24
+ def load_template(name, **locals)
25
+ locals[:config] = self
26
+ name = File.expand_path("../config/#{name}", __dir__) unless name.start_with?("/")
27
+ ERB.new(File.read(name)).result_with_hash(locals)
28
+ end
29
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Kraaken::Credentials::OnePassword < Kraaken::Credentials
4
+ def credential(name)
5
+ username = retrieve(name, "username")
6
+ password = retrieve(name)
7
+ Credential.new(username:, password:)
8
+ end
9
+
10
+ def password(name)
11
+ retrieve name
12
+ end
13
+
14
+ private
15
+
16
+ def retrieve(name, field = "password")
17
+ stdout_str, stderr_str, exit_code = Open3.capture3("op read 'op://server/#{name}/#{field}'")
18
+ raise StandardError.new(stderr_str) unless exit_code.success?
19
+ stdout_str.strip.presence
20
+ end
21
+ end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Kraaken::Credentials
4
+ Credential = Struct.new(:username, :password)
5
+
6
+ def credential(name)
7
+ raise "Not implemented"
8
+ end
9
+
10
+ def password(name)
11
+ credential(name).password
12
+ end
13
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Kraaken::Logger::Color
4
+ CLEAR = "\e[0m"
5
+ BOLD = "\e[1m"
6
+
7
+ # Colors
8
+ BLACK = "\e[30m"
9
+ RED = "\e[31m"
10
+ GREEN = "\e[32m"
11
+ YELLOW = "\e[33m"
12
+ BLUE = "\e[34m"
13
+ MAGENTA = "\e[35m"
14
+ CYAN = "\e[36m"
15
+ WHITE = "\e[37m"
16
+ GRAY = "\e[90m"
17
+
18
+ def color(text, color:, bold: false)
19
+ color = self.class.const_get(color.to_s.upcase) if color.is_a?(Symbol)
20
+ bold = bold ? BOLD : ""
21
+ "#{bold}#{color}#{text}#{CLEAR}"
22
+ end
23
+ end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Kraaken::Logger::Formatter < Logger::Formatter
4
+ def initialize(color)
5
+ @color = color
6
+ end
7
+
8
+ def call(severity, time, progname, msg)
9
+ colors = {
10
+ "DEBUG" => :blue,
11
+ "INFO" => :white,
12
+ "WARN" => :yellow,
13
+ "ERROR" => :red,
14
+ "FATAL" => :red
15
+ }
16
+ @color.color(msg.strip, color: colors[severity] || :white) + "\n"
17
+ end
18
+ end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Kraaken::Logger::LogDevice
4
+ def initialize(progress_proc)
5
+ @progress_proc = progress_proc
6
+ end
7
+
8
+ def write(data)
9
+ if (bar = progress_bar)
10
+ bar.log(data)
11
+ else
12
+ $stdout.write(data)
13
+ end
14
+ end
15
+
16
+ def close
17
+ end
18
+
19
+ private
20
+
21
+ def progress_bar
22
+ @progress_proc.call
23
+ end
24
+ end
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "ruby-progressbar"
4
+
5
+ class Kraaken::Logger < ActiveSupport::Logger
6
+ def initialize
7
+ super Kraaken::Logger::LogDevice.new(-> { @progress_bar })
8
+ @painter = Kraaken::Logger::Color.new
9
+ self.formatter = Kraaken::Logger::Formatter.new(@painter)
10
+ end
11
+
12
+ def with_progress(total:, title: nil, &block)
13
+ @progress_bar = ProgressBar.create(
14
+ total:,
15
+ title:,
16
+ format: "%t |#{color("%B", color: :blue)}|"
17
+ )
18
+
19
+ block.call
20
+
21
+ @progress_bar.finish
22
+ @progress_bar = nil
23
+ end
24
+
25
+ def increment_progress(by: 1)
26
+ @progress_bar&.progress += by
27
+ end
28
+
29
+ delegate :color, to: :@painter
30
+ end
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Kraaken::Ssh::Connection
4
+ def initialize(ssh, logger:)
5
+ @logger = logger
6
+ @ssh = ssh
7
+ end
8
+
9
+ def run(positional_script = nil, log: true, script: nil)
10
+ script ||= positional_script
11
+ output = []
12
+ logger.info script if log
13
+ ssh.exec!(script) do |channel, stream, data|
14
+ logger.debug data if log
15
+ output << data
16
+ rescue Encoding::UndefinedConversionError
17
+ end
18
+ output.join("\n")
19
+ end
20
+
21
+ def read_file(path)
22
+ run "cat #{path}", log: false
23
+ end
24
+
25
+ def write_file(path, content)
26
+ run log: false, script: <<~BASH
27
+ cat <<'EOT' > #{path}
28
+ #{content}
29
+ EOT
30
+ BASH
31
+ end
32
+
33
+ private
34
+
35
+ attr_reader :ssh, :logger
36
+ end
@@ -0,0 +1,47 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "net/ssh"
4
+
5
+ class Kraaken::Ssh
6
+ def initialize(config:)
7
+ @config = config
8
+ end
9
+
10
+ def regenerate_config
11
+ servers = config.cloud.servers
12
+ jump = servers.find(&:jump?)
13
+ servers.reject!(&:jump?)
14
+ config = <<~SSH
15
+ Host jump
16
+ User root
17
+ HostName #{jump.public_ip}
18
+ ForwardAgent yes
19
+ SSH
20
+ servers.each do |server|
21
+ config += <<~SSH
22
+
23
+ Host #{server.name}
24
+ User nerd
25
+ HostName #{server.ip}
26
+ ProxyJump jump
27
+ SSH
28
+ end
29
+ File.write(File.join(Dir.home, ".ssh", "cloud_config"), config)
30
+ config_path = File.join(Dir.home, ".ssh", "config")
31
+ unless File.read(config_path).include?("Include cloud_config")
32
+ File.write(config_path, "Include cloud_config\n\n" + File.read(config_path))
33
+ end
34
+ end
35
+
36
+ def connect(name)
37
+ result = nil
38
+ Net::SSH.start(name) do |ssh|
39
+ result = yield Kraaken::Ssh::Connection.new(ssh, logger: config.logger) if block_given?
40
+ end
41
+ result
42
+ end
43
+
44
+ private
45
+
46
+ attr_reader :config
47
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Kraaken
4
+ VERSION = "0.0.1"
5
+ end
data/lib/kraaken.rb ADDED
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Kraaken
4
+ class Error < StandardError; end
5
+ end
6
+
7
+ require "active_support"
8
+ require "active_support/core_ext"
9
+ require "zeitwerk"
10
+ require "thor"
11
+ require "open3"
12
+ require "tempfile"
13
+
14
+ loader = Zeitwerk::Loader.for_gem
15
+ loader.setup
16
+ loader.eager_load
metadata ADDED
@@ -0,0 +1,176 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: kraaken
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ platform: ruby
6
+ authors:
7
+ - Jens Ravens
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2023-11-04 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: activesupport
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '7.0'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: '7.0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: net-ssh
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '7.0'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '7.0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: thor
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '1.2'
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '1.2'
55
+ - !ruby/object:Gem::Dependency
56
+ name: hcloud
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: '1.2'
62
+ type: :runtime
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
67
+ - !ruby/object:Gem::Version
68
+ version: '1.2'
69
+ - !ruby/object:Gem::Dependency
70
+ name: faraday
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - "~>"
74
+ - !ruby/object:Gem::Version
75
+ version: '2.7'
76
+ type: :runtime
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - "~>"
81
+ - !ruby/object:Gem::Version
82
+ version: '2.7'
83
+ - !ruby/object:Gem::Dependency
84
+ name: zeitwerk
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - "~>"
88
+ - !ruby/object:Gem::Version
89
+ version: '2.5'
90
+ type: :runtime
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - "~>"
95
+ - !ruby/object:Gem::Version
96
+ version: '2.5'
97
+ - !ruby/object:Gem::Dependency
98
+ name: ruby-progressbar
99
+ requirement: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - "~>"
102
+ - !ruby/object:Gem::Version
103
+ version: '1.13'
104
+ type: :runtime
105
+ prerelease: false
106
+ version_requirements: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - "~>"
109
+ - !ruby/object:Gem::Version
110
+ version: '1.13'
111
+ description: Kraaken helps you deploy your applications with docker, traefik and cloudflare
112
+ tunnels.
113
+ email:
114
+ - jens@nerdgeschoss.de
115
+ executables:
116
+ - kraaken
117
+ extensions: []
118
+ extra_rdoc_files: []
119
+ files:
120
+ - ".editorconfig"
121
+ - ".rubocop.yml"
122
+ - ".ruby-version"
123
+ - CODE_OF_CONDUCT.md
124
+ - LICENSE.txt
125
+ - Rakefile
126
+ - exe/kraaken
127
+ - kraaken.gemspec
128
+ - lib/config/cloud-config.yml
129
+ - lib/config/traefik-compose.yml
130
+ - lib/kraaken.rb
131
+ - lib/kraaken/app.rb
132
+ - lib/kraaken/cli.rb
133
+ - lib/kraaken/cli/app.rb
134
+ - lib/kraaken/cli/base.rb
135
+ - lib/kraaken/cli/main.rb
136
+ - lib/kraaken/cli/server.rb
137
+ - lib/kraaken/cli/ssh.rb
138
+ - lib/kraaken/cloud.rb
139
+ - lib/kraaken/cloud/hetzner.rb
140
+ - lib/kraaken/cloudflare.rb
141
+ - lib/kraaken/config.rb
142
+ - lib/kraaken/credentials.rb
143
+ - lib/kraaken/credentials/one_password.rb
144
+ - lib/kraaken/logger.rb
145
+ - lib/kraaken/logger/color.rb
146
+ - lib/kraaken/logger/formatter.rb
147
+ - lib/kraaken/logger/log_device.rb
148
+ - lib/kraaken/ssh.rb
149
+ - lib/kraaken/ssh/connection.rb
150
+ - lib/kraaken/version.rb
151
+ homepage: https://github.com/nerdgeschoss/kraaken
152
+ licenses:
153
+ - MIT
154
+ metadata:
155
+ homepage_uri: https://github.com/nerdgeschoss/kraaken
156
+ source_code_uri: https://github.com/nerdgeschoss/kraaken
157
+ post_install_message:
158
+ rdoc_options: []
159
+ require_paths:
160
+ - lib
161
+ required_ruby_version: !ruby/object:Gem::Requirement
162
+ requirements:
163
+ - - ">="
164
+ - !ruby/object:Gem::Version
165
+ version: 3.1.0
166
+ required_rubygems_version: !ruby/object:Gem::Requirement
167
+ requirements:
168
+ - - ">="
169
+ - !ruby/object:Gem::Version
170
+ version: '0'
171
+ requirements: []
172
+ rubygems_version: 3.4.10
173
+ signing_key:
174
+ specification_version: 4
175
+ summary: Deploy stuff with docker. The easy way.
176
+ test_files: []