Skip to content

Commit

Permalink
(Feature) Add PupEnt tool with puppet access and code
Browse files Browse the repository at this point in the history
Adds a new executable to Bolt: 'pupent', which replaces the existing
implementations of the puppet-access and puppet-code cli tools for
Puppet Enterprise.
  • Loading branch information
mcdonaldseanp committed Sep 8, 2023
1 parent 7a10680 commit b970b04
Show file tree
Hide file tree
Showing 7 changed files with 682 additions and 0 deletions.
16 changes: 16 additions & 0 deletions exe/pupent
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
#!/usr/bin/env ruby
# frozen_string_literal: true

require 'pupent/cli'
require 'bolt/logger'
require 'bolt/error'

begin
cli = PupEnt::CLI.new(ARGV)
exitcode = cli.execute
exit exitcode
rescue PupEnt::CLIExit
exit
rescue Bolt::Error => e
exit e.error_code
end
59 changes: 59 additions & 0 deletions lib/pupent/access.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
# frozen_string_literal: true

require 'fileutils'
require 'io/console'

module PupEnt
class Access
RBAC_PREFIX = ":4433/rbac-api"

def initialize(token_location)
@token_location = token_location
@logger = Bolt::Logger.logger(self)
end

def login(client, lifetime)
$stderr.print("Enter your Puppet Enterprise credentials.\n")
$stderr.print("Username: ")
username = $stdin.gets.to_s.chomp
$stderr.print("Password: ")
# noecho ensures we don't print anything to the console
# while the user is typing the password
password = $stdin.noecho(&:gets).to_s.chomp!
$stderr.puts

body = {
login: username,
password: password,
lifetime: lifetime
}
response, = client.pe_post("/v1/auth/token", body, sensitive: true)
FileUtils.mkdir_p(File.dirname(token_location))
File.open(token_location, File::CREAT | File::WRONLY) do |fd|
fd.write(response['token'])
end
end

def show
File.open(token_location, File::RDONLY).read
rescue Errno::ENOENT
msg = "No token file!"
@logger.error(msg)
raise Bolt::Error.new(msg, 'bolt/no-token')
end

def delete_token
@logger.info("Deleting token...")
File.delete(token_location)
@logger.info("Done.")
rescue Errno::ENOENT
msg = "No token file!"
@logger.error(msg)
raise Bolt::Error.new(msg, 'bolt/no-token')
end

def token_location
@token_location ||= File.join(ENV['HOME'], '.pupent', 'token')
end
end
end
91 changes: 91 additions & 0 deletions lib/pupent/cli.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
# frozen_string_literal: true

require_relative '../pupent/pupent_option_parser'
require_relative '../pupent/config'
require_relative '../pupent/http_client'
require_relative '../pupent/access'
require_relative '../pupent/code'

module PupEnt
class CLI
def initialize(argv)
@command, @action, @object, cli_options = PupEntOptionParser.parse(argv)
# This merge is done in a specific order on purpose:
#
# The final parsed options are read from the config options on disk first, then
# we merge anything sent over the CLI. This forces the precedence that CLI options
# override anything set on disk.
@parsed_options = Config.read_config(cli_options[:config_file]).merge(cli_options)
Bolt::Logger.initialize_logging
Bolt::Logger.logger(:root).add_appenders Logging.appenders.stderr(
'console',
layout: Bolt::Logger.console_layout(true),
level: @parsed_options[:log_level]
)
@pe_host_url = parse_pe_host_url(@command, @parsed_options[:pe_host], @parsed_options[:service_url])
@ca_cert = @parsed_options[:ca_cert]
if @parsed_options[:save_config]
require_relative '../pupent/config'
Config.save_config(@parsed_options)
end
end

def parse_pe_host_url(command, pe_host, service_url)
if pe_host
case command
when 'access'
"https://" + pe_host + Access::RBAC_PREFIX
when 'code'
"https://" + pe_host + Code::CODE_MANAGER_PREFIX
end
elsif service_url
service_url
end
end

# Only create a client when we need to
def new_client
HttpClient.new(@pe_host_url, @ca_cert)
end

def execute
case @command
when 'access'
case @action
when 'login'
Access.new(
@parsed_options[:token_file]
).login(new_client, @parsed_options[:lifetime], @parsed_options)
0
when 'show'
$stdout.puts Access.new(@parsed_options[:token_file]).show
0
when 'delete-token-file'
Access.new(@parsed_options[:token_file]).delete_token
0
end
when 'code'
case @action
when 'deploy'
$stdout.puts Code.new(
@parsed_options[:token_file],
new_client
).deploy(@object, @parsed_options[:wait], @parsed_options[:all])
0
when 'status'
$stdout.puts Code.new(
@parsed_options[:token_file],
new_client
).status
0
when 'deploy-status'
$stdout.puts Code.new(
@parsed_options[:token_file],
new_client
).deploy_status(@object)
0
end
end
end
end
end
45 changes: 45 additions & 0 deletions lib/pupent/code.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
# frozen_string_literal: true

require 'json'
require_relative "../pupent/access"

module PupEnt
class Code
CODE_MANAGER_PREFIX = ":8170/code-manager"

def initialize(token_location, http_client)
@token = Access.new(token_location).show
@client = http_client
end

def deploy(environments, wait, all)
body = {}
if all
body["deploy-all"] = true
else
body[:environments] = environments.split(',')
end

if wait
body[:wait] = true
end
response, = @client.pe_post('/v1/deploys', body, headers: { "X-Authentication": @token })
JSON.pretty_generate(response)
end

def status
response, = @client.pe_get('/v1/status', headers: { "X-Authentication": @token })
JSON.pretty_generate(response)
end

def deploy_status(deploy_id)
url_path = if deploy_id && !deploy_id.empty?
"/v1/deploys/status?id=#{deploy_id}"
else
"/v1/deploys/status"
end
response, = @client.pe_get(url_path, headers: { "X-Authentication": @token })
JSON.pretty_generate(response)
end
end
end
41 changes: 41 additions & 0 deletions lib/pupent/config.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
# frozen_string_literal: true

require 'json'
require 'fileutils'

module PupEnt
module Config
DEFAULT_PUPENT_LOCATION = File.join(ENV['HOME'], ".puppetlabs", "pupent")
DEFAULTS = {
token_file: File.join(DEFAULT_PUPENT_LOCATION, "token"),
log_level: "info",
# pupent access defaults
lifetime: "15m"
}.freeze

def self.read_config(file_location)
FileUtils.mkdir_p(DEFAULT_PUPENT_LOCATION)
file_location ||= File.join(DEFAULT_PUPENT_LOCATION, "config.json")
parsed_data = nil
# Use a+ so it won't fail if the config file doesn't exist, just create an empty one
File.open(file_location, 'a+') do |fd|
config_data = fd.read
parsed_data = if config_data && !config_data.empty?
JSON.parse(config_data)
else
{}
end
end
parsed_data.transform_keys! { |key| key.to_s.downcase.gsub("-", "_").to_sym }
DEFAULTS.merge(parsed_data)
end

def self.save_config(parsed_options)
file_location ||= File.join(DEFAULT_PUPENT_LOCATION, "config.json")
File.open(file_location, 'w') do |fd|
config_to_save = parsed_options.slice(:token_file, :ca_cert, :pe_host, :service_url)
fd.write(JSON.pretty_generate(config_to_save))
end
end
end
end
Loading

0 comments on commit b970b04

Please sign in to comment.