fastlyctl 1.0.13 → 1.0.18

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: c2ae1f51d01ae7427213464df500005c82a6f7e88539189b4ac930a4251c5334
4
- data.tar.gz: 487ee8363712ca396b5c02b1305000a0136336227ec18bc2531bc706123e7f93
3
+ metadata.gz: d6d98fb4345d1cbfc02e52a6ab8565af95f7fb6db2c607ae204ef100735fde6e
4
+ data.tar.gz: b27abed6021946a60d7adc5473696eae49047813f02424d56ed459c38a9ccd01
5
5
  SHA512:
6
- metadata.gz: 15a29375aced1dc02313e6aef32539200529b64414b3812588bed4b74f0c55746bf5f8c1a9a0136e2d3d57a10e3eac091f02bc42cabf72921499517474c8fe70
7
- data.tar.gz: fc607eed5bbbba636be71737750e4a5cdb6cd1fd108df8cac3c7f02bac73394b2430d3b7b594e95811c4e6f4e4cc478c6e8093180dc32e51f8c6fca81ca9e720
6
+ metadata.gz: 0c72072eeb95cabce04393ca9c060c00d353d6a94fbcbf2c4e1a2206c9d2c51c0e23af643ab8189d8736d00bb6827c2b98932175f63d92cb184848135b275cc8
7
+ data.tar.gz: 57560a7c0a4e8c7a73daf08f4d14ba107378eb26c9e70dae9709e900920b460067127df3d1c1767b2bd4c7da36b64937ebfd6528fa6e17bb7d2e3e929dab7ddf
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- fastlyctl (1.0.13)
4
+ fastlyctl (1.0.14)
5
5
  diffy (~> 3.2.1)
6
6
  launchy (~> 2.4.3, >= 2.4.3)
7
7
  thor (~> 0.19.4)
data/README.md CHANGED
@@ -321,6 +321,22 @@ Flags:
321
321
  * --d: When used with the create command, specifies that the snippet should be dynamic.
322
322
  * --y: Answer yes to all prompts
323
323
 
324
+ ### tls
325
+
326
+ #### managed
327
+
328
+ Usage:
329
+
330
+ ```
331
+ fastlyctl tls managed [subcommand] [domain]
332
+ ```
333
+
334
+ Available Subcommands:
335
+ * create: Create a Managed TLS Subscription for `[domain]`
336
+ * status: Print the status of all Managed TLS Subscriptions
337
+ * challenges: Print the challenges available for the verification of a certificate for `[domain]`
338
+ * delete: Delete a Managed TLS Subscription for `[domain]`
339
+
324
340
  ### token
325
341
 
326
342
  Manipulate tokens for an account.
@@ -371,10 +387,6 @@ Flags:
371
387
 
372
388
  The `--debug` flag is available on any command. Using it will cause fastlyctl to print the libcurl output for any requests it makes.
373
389
 
374
- ## Contributing
375
-
376
- Submit a pull request. Don't break anything.
377
-
378
390
  ## License
379
391
 
380
392
  The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).
@@ -34,4 +34,5 @@ Gem::Specification.new do |spec|
34
34
  spec.add_runtime_dependency "thor", "~> 0.19.4"
35
35
  spec.add_runtime_dependency 'diffy', '~> 3.2.1'
36
36
  spec.add_runtime_dependency 'launchy', '~> 2.4.3', '>= 2.4.3'
37
+ spec.add_runtime_dependency 'openssl', '~> 2.1.2', '>= 2.1.2'
37
38
  end
@@ -6,23 +6,23 @@ require "uri"
6
6
  require "launchy"
7
7
  require "erb"
8
8
  require "pp"
9
+ require "openssl"
9
10
 
10
11
  require "fastlyctl/version"
11
12
  require "fastlyctl/fetcher"
12
13
  require "fastlyctl/clone_utils"
13
14
  require "fastlyctl/utils"
15
+ require "fastlyctl/subcommand_patch"
14
16
  require "fastlyctl/cli"
15
17
 
16
18
  include ERB::Util
17
19
 
18
20
  module FastlyCTL
19
- COOKIE_JAR = ENV['HOME'] + "/.fastlyctl_cookie_jar"
20
21
  TOKEN_FILE = ENV['HOME'] + "/.fastlyctl_token"
21
22
  FASTLY_API = "https://api.fastly.com"
22
- FASTLY_APP = "https://manage.fastly.com"
23
+ FASTLY_RT_API = "https://rt.fastly.com"
23
24
  TANGO_PATH = "/configure/services/"
24
25
 
25
- Cookies = File.exist?(FastlyCTL::COOKIE_JAR) ? JSON.parse(File.read(FastlyCTL::COOKIE_JAR)) : {}
26
26
  # Don't allow header splitting with the key
27
- Token = File.exist?(FastlyCTL::TOKEN_FILE) ? File.read(FastlyCTL::TOKEN_FILE) : false
27
+ Token = File.exist?(FastlyCTL::TOKEN_FILE) ? File.read(FastlyCTL::TOKEN_FILE) : (ENV['FASTLYCLI_TOKEN'] ? ENV['FASTLYCLI_TOKEN'] : false)
28
28
  end
@@ -16,6 +16,8 @@ require "fastlyctl/commands/snippet"
16
16
  require "fastlyctl/commands/acl"
17
17
  require "fastlyctl/commands/copy"
18
18
  require "fastlyctl/commands/logging"
19
+ require "fastlyctl/commands/condition"
20
+ require "fastlyctl/commands/tls"
19
21
 
20
22
 
21
23
  module FastlyCTL
@@ -23,7 +25,7 @@ module FastlyCTL
23
25
  class_option :debug, :desc => 'Enabled debug mode output'
24
26
 
25
27
  def initialize(a,b,c)
26
- unless File.exist?(FastlyCTL::TOKEN_FILE)
28
+ unless File.exist?(FastlyCTL::TOKEN_FILE) || ENV['FASTLYCLI_TOKEN']
27
29
  if yes?("Unable to locate API token. Would you like to login first?")
28
30
  self.login
29
31
  end
@@ -16,7 +16,6 @@ module FastlyCTL
16
16
  "vcl" => {},
17
17
  "snippet" => {},
18
18
  "logging/s3" => {},
19
- "logging/s3canary" => {},
20
19
  "logging/azureblob" => {},
21
20
  "logging/cloudfiles" => {},
22
21
  "logging/digitalocean" => {},
@@ -60,12 +59,12 @@ module FastlyCTL
60
59
 
61
60
  if main === true
62
61
  # the "main-ness" of the vcl does not get carried over during creation. must explicitly set main
63
- FastlyCTL::Fetcher.api_request(:put, "/service/#{sid}/version/#{version}/vcl/#{URI.escape(obj["name"])}/main")
62
+ FastlyCTL::Fetcher.api_request(:put, "/service/#{sid}/version/#{version}/vcl/#{FastlyCTL::Utils.percent_encode(obj["name"])}/main")
64
63
  end
65
64
 
66
65
  if type == "director"
67
66
  backends.each do |b|
68
- FastlyCTL::Fetcher.api_request(:post, "/service/#{sid}/version/#{version}/director/#{URI.escape(obj["name"])}/backend/#{b}", body: obj )
67
+ FastlyCTL::Fetcher.api_request(:post, "/service/#{sid}/version/#{version}/director/#{FastlyCTL::Utils.percent_encode(obj["name"])}/backend/#{b}", body: obj )
69
68
  end
70
69
  end
71
70
 
@@ -20,7 +20,7 @@ module FastlyCTL
20
20
  version = FastlyCTL::Fetcher.get_writable_version(id) unless options[:version]
21
21
  version ||= options[:version]
22
22
 
23
- encoded_name = URI.escape(name) if name
23
+ encoded_name = FastlyCTL::Utils.percent_encode(name) if name
24
24
 
25
25
  case action
26
26
  when "create"
@@ -0,0 +1,92 @@
1
+ module FastlyCTL
2
+ class CLI < Thor
3
+ desc "condition ACTION NAME", "Manipulate conditions on a service. Available actions are create, delete, update, show and list. NAME parameter not required for list ACTION"
4
+ method_option :service, :aliases => ["--s"]
5
+ method_option :version, :aliases => ["--v"]
6
+ method_option :type, :aliases => ["--t"]
7
+ method_option :yes, :aliases => ["--y"]
8
+ method_option :priority, :aliases => ["--p"]
9
+ method_option :statement, :aliases => ["--st"]
10
+ method_option :comment, :aliases => ["--c"]
11
+
12
+ def self.print_condition_header
13
+ puts
14
+ puts "Name".ljust(40) + " | " + "Priority".ljust(8) + " | " + "Type".ljust(10) + " | " + "Statement".ljust(20)
15
+ puts "-------------------------------------------------------------------------------------------------------"
16
+ end
17
+
18
+ def self.print_conditions(conditions)
19
+ self.print_condition_header
20
+
21
+ conditions.each { |c|
22
+ puts "%s | %s | %s | %s " % [c["name"].ljust(40), c["priority"].ljust(8), c["type"].ljust(10), c["statement"].ljust(20)]
23
+ }
24
+ end
25
+
26
+ def condition(action,name=false)
27
+ id = FastlyCTL::Utils.parse_directory unless options[:service]
28
+ id ||= options[:service]
29
+
30
+ abort "Could not parse service id from directory. Use --s <service> to specify, vcl download, then try again." unless id
31
+
32
+ version = FastlyCTL::Fetcher.get_writable_version(id) unless options[:version]
33
+ version ||= options[:version].to_i
34
+
35
+ encoded_name = FastlyCTL::Utils.percent_encode(name) if name
36
+
37
+ case action
38
+ when "list"
39
+ conditions = FastlyCTL::Fetcher.api_request(:get,"/service/#{id}/version/#{version}/condition")
40
+ CLI.print_conditions(conditions)
41
+
42
+ when "create"
43
+ abort "Must supply a condition name as second parameter" unless name
44
+ abort "Must supply a statement to create a condition" unless options[:statement]
45
+
46
+ params = {}
47
+ params[:name] = name
48
+ params[:statement] = options[:statement]
49
+
50
+ params[:priority] = options[:priority] if options.key?(:priority)
51
+ params[:type] = options[:type] if options.key?(:type)
52
+ params[:comment] = options[:comment] if options.key?(:comment)
53
+
54
+ FastlyCTL::Fetcher.api_request(:post,"/service/#{id}/version/#{version}/condition",{
55
+ params: params
56
+ })
57
+ say("Condition #{name} created on #{id} version #{version}")
58
+
59
+ when "update"
60
+ abort "Must supply a condition name as second parameter" unless name
61
+
62
+ if options.key?(:type)
63
+ puts "WARNING: Can not change the TYPE of a condition, you must delete and re-create, type parameter is ignored in update method.\n"
64
+ end
65
+
66
+ params = {}
67
+ params[:statement] = options[:statement] if options.key?(:statement)
68
+ params[:priority] = options[:priority] if options.key?(:priority)
69
+ params[:comment] = options[:comment] if options.key?(:comment)
70
+
71
+ FastlyCTL::Fetcher.api_request(:put,"/service/#{id}/version/#{version}/condition/#{encoded_name}",{
72
+ params: params
73
+ })
74
+ say("Condition #{name} updated on #{id} version #{version}")
75
+
76
+ when "show"
77
+ abort "Must supply a condition name as second parameter" unless name
78
+
79
+ c = FastlyCTL::Fetcher.api_request(:get,"/service/#{id}/version/#{version}/condition/#{encoded_name}")
80
+ CLI.print_conditions([c])
81
+
82
+ when "delete"
83
+ abort "Must supply a condition name as second parameter" unless name
84
+
85
+ c = FastlyCTL::Fetcher.api_request(:delete,"/service/#{id}/version/#{version}/condition/#{encoded_name}")
86
+ say("Condition #{name} deleted on #{id} version #{version}")
87
+
88
+ end
89
+ end
90
+
91
+ end
92
+ end
@@ -19,7 +19,7 @@ module FastlyCTL
19
19
  path += "/#{obj_name}" unless obj_type == "settings"
20
20
  obj = FastlyCTL::Fetcher.api_request(:get, path)
21
21
 
22
- encoded_name = URI.escape(obj_name)
22
+ encoded_name = FastlyCTL::Utils.percent_encode(obj_name)
23
23
 
24
24
  if (obj_type == "settings")
25
25
  puts "Copying settings from #{id} version #{source_version} to #{target_id} version #{target_version}..."
@@ -20,7 +20,7 @@ module FastlyCTL
20
20
  version = FastlyCTL::Fetcher.get_writable_version(id) unless options[:version]
21
21
  version ||= options[:version]
22
22
 
23
- encoded_name = URI.escape(name) if name
23
+ encoded_name = FastlyCTL::Utils.percent_encode(name) if name
24
24
 
25
25
  case action
26
26
  when "create"
@@ -49,14 +49,14 @@ module FastlyCTL
49
49
  abort "Must specify name for dictionary" unless name
50
50
  abort "Must specify key and value for dictionary item" unless (key && value)
51
51
  dict = FastlyCTL::Fetcher.api_request(:get, "/service/#{id}/version/#{version}/dictionary/#{encoded_name}")
52
- FastlyCTL::Fetcher.api_request(:put, "/service/#{id}/dictionary/#{dict["id"]}/item/#{key}", params: { item_value: value })
52
+ FastlyCTL::Fetcher.api_request(:put, "/service/#{id}/dictionary/#{dict["id"]}/item/#{FastlyCTL::Utils.percent_encode(key)}", params: { item_value: value })
53
53
 
54
54
  say("Dictionary item #{key} set to #{value}.")
55
55
  when "remove"
56
56
  abort "Must specify name for dictionary" unless name
57
57
  abort "Must specify key for dictionary item" unless key
58
58
  dict = FastlyCTL::Fetcher.api_request(:get, "/service/#{id}/version/#{version}/dictionary/#{encoded_name}")
59
- FastlyCTL::Fetcher.api_request(:delete, "/service/#{id}/dictionary/#{dict["id"]}/item/#{key}")
59
+ FastlyCTL::Fetcher.api_request(:delete, "/service/#{id}/dictionary/#{dict["id"]}/item/#{FastlyCTL::Utils.percent_encode(key)}")
60
60
 
61
61
  say("Item #{key} removed from dictionary #{name}.")
62
62
  when "list_items"
@@ -13,23 +13,21 @@ module FastlyCTL
13
13
 
14
14
  say("Proceeding with username/password login...")
15
15
 
16
- login_results = FastlyCTL::Fetcher.login
16
+ username = ask("Username: ")
17
+ password = ask("Password: ", :echo => false)
18
+ say("")
17
19
 
18
- File.open(FastlyCTL::COOKIE_JAR , 'w+') {|f| f.write(JSON.dump(FastlyCTL::Cookies)) }
19
- File.chmod(0600, FastlyCTL::COOKIE_JAR)
20
-
21
- say("Creating root scoped token...")
22
-
23
- if login_results[:user].include?("@fastly.com") && !login_results[:user].include?("+")
20
+ if username.include?("@fastly.com") && !username.include?("+")
24
21
  scope = "root"
25
22
  else
26
23
  scope = "global"
27
24
  end
28
25
 
26
+ say("Creating #{scope} scope token...")
27
+
29
28
  o = {
30
- user: login_results[:user],
31
- pass: login_results[:pass],
32
- code: login_results[:code],
29
+ username: username,
30
+ password: password,
33
31
  scope: scope,
34
32
  name: "fastlyctl_token"
35
33
  }
@@ -3,11 +3,10 @@ module FastlyCTL
3
3
  desc "purge_all", "Purge all content from a service."
4
4
  method_option :service, :aliases => ["--s"]
5
5
  def purge_all
6
- parsed_id = FastlyCTL::Utils.parse_directory
6
+ id = FastlyCTL::Utils.parse_directory unless options[:service]
7
+ id ||= options[:service]
7
8
 
8
- id = FastlyCTL::Utils.parse_directory
9
-
10
- abort "Could not parse service id from directory. Use --s <service> to specify, vcl download, then try again." unless (id || options[:service])
9
+ abort "Could not parse service id from directory. Use --s <service> to specify, vcl download, then try again." unless id
11
10
 
12
11
  FastlyCTL::Fetcher.api_request(:post, "/service/#{id}/purge_all")
13
12
 
@@ -18,7 +18,7 @@ module FastlyCTL
18
18
  version = FastlyCTL::Fetcher.get_writable_version(id) unless options[:version]
19
19
  version ||= options[:version].to_i
20
20
 
21
- encoded_name = URI.escape(name) if name
21
+ encoded_name = FastlyCTL::Utils.percent_encode(name) if name
22
22
 
23
23
  filename = options.key?(:filename) ? options[:filename] : "#{name}.snippet"
24
24
 
@@ -0,0 +1,70 @@
1
+ require "fastlyctl/commands/tls/managed"
2
+
3
+ module FastlyCTL
4
+ class TLSSubCmd < SubCommandBase
5
+ SubcommandPrefix = "tls"
6
+
7
+ desc "managed SUBCOMMAND ...ARGS", "Interface with Fastly Managed TLS Subscriptions (lets-encrypt)"
8
+ subcommand "managed", TLSManagedSubCmd
9
+ end
10
+
11
+ class CLI < Thor
12
+ desc "tls SUBCOMMAND ...ARGS", "Interface with Fastly TLS"
13
+ subcommand "tls", TLSSubCmd
14
+ end
15
+
16
+ module TLSUtils
17
+ def self.get_tls_configs
18
+ data = FastlyCTL::Fetcher.api_request(:get,"/tls/configurations",{use_vnd:true})["data"]
19
+ if data.length == 0
20
+ thor = Thor::Shell::Basic.new
21
+ thor.say "No TLS Configurations found. You may need to upgrade to a paid account if you are using a free account."
22
+ thor.say "If you need assistance, please contact support@fastly.com."
23
+ if (thor.yes?("Would you like to open the TLS configuration page in the Fastly app?"))
24
+ FastlyCTL::Utils.open_app_path("/network/domains")
25
+ end
26
+ abort
27
+ end
28
+
29
+ return data
30
+ end
31
+
32
+ def self.select_tls_config(configs)
33
+ thor = Thor::Shell::Basic.new
34
+ if configs.length == 1
35
+ thor.say "Using TLS Configuration #{configs[0]["id"]} - #{configs[0]["name"]}"
36
+ return configs[0]
37
+ end
38
+
39
+ loop do
40
+ i = 1
41
+ configs.each do |c|
42
+ bulk = c["attributes"]["bulk"] ? " [Platform TLS]" : ""
43
+ thor.say("[#{i}]#{bulk} #{c["id"]} - #{c["name"]}\n")
44
+ i += 1
45
+ end
46
+
47
+ selected = thor.ask("Which TLS Configuration would you like to use? Please type the number next to the configuration(s) above.").to_i
48
+ if selected > 0 && selected <= (configs.length+1)
49
+ selected -= 1
50
+ thor.say "Using TLS Configuration #{configs[selected]["id"]} - #{configs[selected]["name"]}"
51
+ return configs[selected]
52
+ end
53
+
54
+ thor.say "#{selcted} is in invalid selection. Please try again."
55
+ end
56
+ end
57
+
58
+ def self.print_challenges(tls_authorization)
59
+ thor = Thor::Shell::Basic.new
60
+ thor.say "\nIn order to verify your ownership of the domain, the Certificate Authority provided the following challenges:"
61
+ tls_authorization["attributes"]["challenges"].each do |challenge|
62
+ thor.say("\n#{challenge["type"]}: Create #{challenge["record_type"]} record for #{challenge["record_name"]} with value(s) of:")
63
+ challenge["values"].each do |val|
64
+ thor.say(" #{val}")
65
+ end
66
+ end
67
+ thor.say("\nNote: If you don't want to move all traffic to Fastly right now, use the managed-dns option. The other options result in traffic for that hostname being directed to Fastly.")
68
+ end
69
+ end
70
+ end
@@ -0,0 +1,119 @@
1
+ module FastlyCTL
2
+ class TLSManagedSubCmd < SubCommandBase
3
+ SubcommandPrefix = "tls managed"
4
+ DomainRegex = /(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z0-9][a-z0-9-]{0,61}[a-z0-9]/
5
+
6
+ desc "create <domain>", "Create a Fastly Managed TLS Subscription for [domain]. A Certificate will be requested from lets-encrypt once you satisfy one of the challenges. You can learn more about the challenge types here: https://letsencrypt.org/docs/challenge-types/ and Fastly's API documentation here: https://docs.fastly.com/api/tls-subscriptions."
7
+ def create(domain)
8
+ abort "Must specify valid domain name" unless domain =~ DomainRegex
9
+
10
+ tls_configs = FastlyCTL::TLSUtils.get_tls_configs
11
+ tls_config = FastlyCTL::TLSUtils.select_tls_config(tls_configs)
12
+
13
+ payload = {
14
+ data: {
15
+ type: "tls_subscription",
16
+ attributes: {
17
+ certificate_authority: "lets-encrypt"
18
+ },
19
+ relationships: {
20
+ tls_domains: {
21
+ data: [
22
+ {
23
+ type: "tls_domain",
24
+ id: domain
25
+ }
26
+ ]
27
+ },
28
+ tls_configuration: {
29
+ data: {
30
+ type: "tls_configuration",
31
+ id: tls_config["id"]
32
+ }
33
+ }
34
+ }
35
+ }
36
+ }
37
+
38
+ subscription = FastlyCTL::Fetcher.api_request(:post,"/tls/subscriptions", {
39
+ body: payload.to_json,
40
+ use_vnd: true
41
+ })
42
+
43
+ tls_authorization = FastlyCTL::Utils.filter_vnd(subscription["included"],"tls_authorization")
44
+ abort "Unable to fetch TLS Authorization for the domain." unless tls_authorization.length > 0
45
+ FastlyCTL::TLSUtils.print_challenges(tls_authorization[0])
46
+ end
47
+
48
+ desc "status", "Print status of Fastly Managed TLS Subscriptions"
49
+ def status
50
+ subscriptions = FastlyCTL::Fetcher.api_request(:get,"/tls/subscriptions", {
51
+ use_vnd: true
52
+ })
53
+
54
+ if subscriptions["data"].length == 0
55
+ say("No Fastly Managed TLS Subscriptions found.")
56
+ abort
57
+ end
58
+
59
+ subscriptions["data"].each do |subscription|
60
+ output = subscription["relationships"]["tls_domains"]["data"][0]["id"]
61
+ output += " - " + subscription["attributes"]["certificate_authority"]
62
+ output += " - " + subscription["attributes"]["state"]
63
+ say(output)
64
+ end
65
+ end
66
+
67
+ desc "challenges", "Print challenges available for a domain's verification."
68
+ def challenges(domain)
69
+ abort "Must specify valid domain name" unless domain =~ DomainRegex
70
+
71
+ domains = FastlyCTL::Fetcher.api_request(:get,"/tls/domains?include=tls_subscriptions.tls_authorizations", {
72
+ use_vnd: true
73
+ })
74
+
75
+ tls_authorizations = FastlyCTL::Utils.filter_vnd(domains["included"],"tls_authorization")
76
+
77
+ tls_authorizations.each do |tls_authorization|
78
+ tls_authorization["attributes"]["challenges"].each do |challenge|
79
+ if challenge["record_name"] == domain
80
+ FastlyCTL::TLSUtils.print_challenges(tls_authorization)
81
+ abort
82
+ end
83
+ end
84
+ end
85
+
86
+ say("#{domain} not found in domain list.")
87
+ end
88
+
89
+ desc "delete", "Delete a Fastly Managed TLS Subscription"
90
+ def delete(domain)
91
+ abort "Must specify valid domain name" unless domain =~ DomainRegex
92
+
93
+ activation = FastlyCTL::Fetcher.api_request(:get,"/tls/activations?filter[tls_domain.id]=#{domain}", {use_vnd: true})
94
+
95
+ if activation["data"].length >= 1
96
+ say("TLS is currently active for #{domain}. If you proceed, Fastly will no longer be able to serve TLS requests to clients for #{domain}.")
97
+ answer = ask("Please type the name of the domain to confirm deactivation and deletion of the Fastly Managed TLS subscription: ")
98
+ abort "Supplied domain does not match the domain requested for deletion--aborting." unless answer == domain
99
+
100
+ FastlyCTL::Fetcher.api_request(:delete,"/tls/activations/#{activation["data"][0]["id"]}",{use_vnd:true})
101
+ end
102
+
103
+ subscriptions = FastlyCTL::Fetcher.api_request(:get,"/tls/subscriptions", {
104
+ use_vnd: true
105
+ })
106
+
107
+ subscriptions["data"].each do |subscription|
108
+ next unless subscription["relationships"]["tls_domains"]["data"][0]["id"] == domain
109
+
110
+ FastlyCTL::Fetcher.api_request(:delete,"/tls/subscriptions/#{subscription["id"]}",{use_vnd:true})
111
+
112
+ say("TLS Subscription for #{domain} has been deleted.")
113
+ abort
114
+ end
115
+
116
+ say("No TLS Subscription found for #{domain}...")
117
+ end
118
+ end
119
+ end
@@ -20,25 +20,27 @@ module FastlyCTL
20
20
  scope = options[:scope]
21
21
  scope ||= "global"
22
22
 
23
- say("You must login again to create tokens.")
23
+ say("You must authenticate again to create tokens.")
24
24
 
25
- login_results = FastlyCTL::Fetcher.login
26
-
27
- name = ask("What would you like to name your token?")
25
+ username = ask("Username: ")
26
+ password = ask("Password: ", :echo => false)
27
+ say("")
28
28
 
29
+ name = ask("What would you like to name your token? (enter here):")
29
30
  o = {
30
- user: login_results[:user],
31
- pass: login_results[:pass],
32
- code: login_results[:code],
31
+ username: username,
32
+ password: password,
33
33
  scope: scope,
34
- name: name
35
- }
36
-
37
- o[:services] = options[:services].split(",") if options[:services]
34
+ name: name || "fastlyctl_token"
35
+ }.compare_by_identity
38
36
 
37
+ options[:services].split(",").each do |v|
38
+ o["services[]"] = v
39
+ end if options[:services]
39
40
  o[:customer] = options[:customer] if options[:customer]
40
41
 
41
42
  resp = FastlyCTL::Fetcher.create_token(o)
43
+ say("token: #{resp["access_token"]}")
42
44
 
43
45
  when "delete"
44
46
  id = ask("What is the ID of the token you'd like to delete?")
@@ -13,7 +13,7 @@ module FastlyCTL
13
13
  pop = pop.upcase if pop
14
14
 
15
15
  while true
16
- data = FastlyCTL::Fetcher.api_request(:get,"/rt/v1/channel/#{service}/ts/#{ts ? ts : 'h/limit/120'}", :endpoint => :app)
16
+ data = FastlyCTL::Fetcher.api_request(:get,"/rt/v1/channel/#{service}/ts/#{ts ? ts : 'h/limit/120'}", :endpoint => :rt)
17
17
 
18
18
  unless data["Data"].length > 0
19
19
  say("No data to display!")
@@ -33,7 +33,8 @@ module FastlyCTL
33
33
  # gbps
34
34
  uncacheable = agg["pass"] + agg["synth"] + agg["errors"]
35
35
  bw = ((agg["resp_header_bytes"] + agg["resp_body_bytes"]).to_f * 8.0) / 1000000000.0
36
- hit_rate = (1.0 - ((agg["miss"] - agg["shield"]).to_f / ((agg["requests"] - uncacheable).to_f))) * 100.0
36
+ shield = agg["shield"] || 0
37
+ hit_rate = (1.0 - ((agg["miss"] - shield).to_f / ((agg["requests"] - uncacheable).to_f))) * 100.0
37
38
  passes = agg["pass"]
38
39
  miss_time = agg["miss"] > 0 ? ((agg["miss_time"] / agg["miss"]) * 1000).round(0) : 0
39
40
  synth = agg["synth"]
@@ -5,28 +5,32 @@ module FastlyCTL
5
5
  options[:params] ||= {}
6
6
  options[:headers] ||= {}
7
7
  options[:body] ||= nil
8
- options[:force_session] ||= false
8
+ options[:disable_token] ||= false
9
9
  options[:expected_responses] ||= [200]
10
+ options[:use_vnd] ||= false
10
11
 
11
12
  headers = {"Accept" => "application/json", "Connection" => "close", "User-Agent" => "FastlyCTL: https://github.com/fastly/fastlyctl"}
12
13
 
13
14
  if options[:endpoint] == :app
14
15
  headers["Referer"] = FastlyCTL::FASTLY_APP
15
- headers["X-CSRF-Token"] = FastlyCTL::Cookies["fastly.csrf"] if FastlyCTL::Cookies["fastly.csrf"]
16
16
  headers["Fastly-API-Request"] = "true"
17
17
  end
18
18
 
19
- if FastlyCTL::Token && !options[:force_session]
19
+ if FastlyCTL::Token && !options[:disable_token]
20
20
  headers["Fastly-Key"] = FastlyCTL::Token
21
- else
22
- headers["Cookie"] = "" if FastlyCTL::Cookies.length > 0
23
- FastlyCTL::Cookies.each do |k,v|
24
- headers["Cookie"] << "#{k}=#{v};"
25
- end
26
21
  end
27
22
 
28
23
  headers["Content-Type"] = "application/x-www-form-urlencoded" if (method == :post || method == :put)
29
24
 
25
+ if options[:use_vnd]
26
+ headers["Accept"] = "application/vnd.api+json"
27
+
28
+ if (method == :post || method == :put)
29
+ headers["Content-Type"] = "application/vnd.api+json"
30
+ end
31
+ options[:expected_responses].push(*[201,202,203,204])
32
+ end
33
+
30
34
  headers.merge!(options[:headers]) if options[:headers].count > 0
31
35
 
32
36
  # dont allow header splitting on anything
@@ -34,7 +38,7 @@ module FastlyCTL
34
38
  headers[k] = v.gsub(/\r|\n/,'')
35
39
  end
36
40
 
37
- url = "#{options[:endpoint] == :api ? FastlyCTL::FASTLY_API : FastlyCTL::FASTLY_APP}#{path}"
41
+ url = "#{options[:endpoint] == :api ? FastlyCTL::FASTLY_API : FastlyCTL::FASTLY_RT_API}#{path}"
38
42
 
39
43
  response = Typhoeus::Request.new(
40
44
  url,
@@ -44,35 +48,43 @@ module FastlyCTL
44
48
  body: options[:body]
45
49
  ).run
46
50
 
47
- if options[:expected_responses].include?(response.response_code)
48
- if response.headers["Set-Cookie"]
49
- response.headers["Set-Cookie"] = [response.headers["Set-Cookie"]] if response.headers["Set-Cookie"].is_a? String
50
- response.headers["Set-Cookie"].each do |c|
51
- name, value = c.match(/^([^=]*)=([^;]*).*/i).captures
52
- FastlyCTL::Cookies[name] = value
53
- end
54
- end
55
- else
51
+ if !options[:expected_responses].include?(response.response_code)
56
52
  case response.response_code
57
- when 400
58
- error = "400: Bad API request--got bad request response."
59
- when 403
60
- error = "403: Access Denied by API. Run login command to authenticate."
61
- when 404
62
- error = "404: Service does not exist or bad path requested."
63
- when 503
64
- error = "503: API is offline."
65
- else
66
- error = "API responded with status #{response.response_code}."
53
+ when 400
54
+ error = "400: Bad API request--something was wrong with the request made by FastlyCTL."
55
+ when 403
56
+ error = "403: Access Denied by API. Run login command to authenticate."
57
+ when 404
58
+ error = "404: Service does not exist or bad path requested."
59
+ when 503
60
+ error = "503: Error from Fastly API--see details below."
61
+ when 0
62
+ error = "0: Network connection error occurred."
63
+ else
64
+ error = "API responded with status #{response.response_code}."
67
65
  end
68
66
 
69
67
  error += " Method: #{method.to_s.upcase}, Path: #{path}\n"
70
- error += "Message from API: #{response.response_body}"
68
+
69
+ if (options[:use_vnd])
70
+ begin
71
+ error_resp = JSON.parse(response.response_body)
72
+ rescue JSON::ParserError
73
+ error_resp = {"errors" => [{"title" => "Error parsing response JSON","details" => "No further information available. Please file a github issue at https://github.com/fastly/fastlyctl"}]}
74
+ end
75
+
76
+ error_resp["errors"].each do |e|
77
+ next unless e.key?("title") && e.key?("detail")
78
+ error += e["title"] + " --- " + e["detail"] + "\n"
79
+ end
80
+ else
81
+ error += "Message from API: #{response.response_body}"
82
+ end
71
83
 
72
84
  abort error
73
85
  end
74
86
 
75
- return response.response_body if (response.headers["Content-Type"] != "application/json")
87
+ return response.response_body unless (response.headers["Content-Type"] =~ /json$/)
76
88
 
77
89
  if response.response_body.length > 1
78
90
  begin
@@ -161,7 +173,7 @@ module FastlyCTL
161
173
  end
162
174
 
163
175
  def self.upload_snippet(service,version,content,name)
164
- return FastlyCTL::Fetcher.api_request(:put, "/service/#{service}/version/#{version}/snippet/#{name}", {:endpoint => :api, body: {
176
+ return FastlyCTL::Fetcher.api_request(:put, "/service/#{service}/version/#{version}/snippet/#{FastlyCTL::Utils.percent_encode(name)}", {:endpoint => :api, body: {
165
177
  content: content
166
178
  }
167
179
  })
@@ -178,7 +190,7 @@ module FastlyCTL
178
190
  end
179
191
  end
180
192
 
181
- response = FastlyCTL::Fetcher.api_request(:put, "/service/#{service}/version/#{version}/vcl/#{name}", {:endpoint => :api, body: params, expected_responses: [200,404]})
193
+ response = FastlyCTL::Fetcher.api_request(:put, "/service/#{service}/version/#{version}/vcl/#{FastlyCTL::Utils.percent_encode(name)}", {:endpoint => :api, body: params, expected_responses: [200,404]})
182
194
 
183
195
  # The VCL got deleted so recreate it.
184
196
  if response["msg"] == "Record not found"
@@ -186,61 +198,33 @@ module FastlyCTL
186
198
  end
187
199
  end
188
200
 
189
- def self.login
190
- thor = Thor::Shell::Basic.new
191
-
192
- user = thor.ask("Username: ")
193
- pass = thor.ask("Password: ", :echo => false)
194
-
195
- resp = FastlyCTL::Fetcher.api_request(:post, "/login", { :endpoint => :app, params: { user: user, password: pass}})
196
-
197
- if resp["needs_two_factor_auth"]
198
- two_factor = true
199
-
200
- thor.say("\nTwo factor auth enabled on account, second factor needed.")
201
- code = thor.ask('Please enter verification code:', echo: false)
202
-
203
- resp = FastlyCTL::Fetcher.api_request(:post, "/two_factor_auth/verify", {force_session: true, :endpoint => :app, params: { token: code }} )
204
- else
205
- thor.say("\nTwo factor auth is NOT enabled. You should go do that immediately.")
206
- end
207
-
208
- thor.say("Login successful!")
209
-
210
- return { user: user, pass: pass, two_factor: two_factor, code: code }
211
- end
212
-
213
201
  def self.create_token(options)
214
202
  thor = Thor::Shell::Basic.new
215
203
 
216
204
  headers = {}
217
- headers["Fastly-OTP"] = options[:code] if options[:code]
218
-
219
- FastlyCTL::Fetcher.api_request(:post, "/sudo", {
220
- force_session: true,
205
+ resp = FastlyCTL::Fetcher.api_request(:post, "/tokens", {
206
+ disable_token: true,
221
207
  endpoint: :api,
222
- params: {
223
- user: options[:user],
224
- password: options[:pass]
225
- },
226
- headers: headers
208
+ body: options,
209
+ headers: headers,
210
+ expected_responses: [200,400]
227
211
  })
228
212
 
229
- params = {
230
- name: options[:name],
231
- scope: options[:scope],
232
- user: options[:user],
233
- password: options[:pass]
234
- }
235
-
236
- params[:services] = options[:services] if options[:services]
213
+ if resp.has_key?("msg") && resp["msg"] == "2fa.verify"
214
+ thor.say("\nTwo factor auth enabled on account, second factor needed.")
215
+ code = thor.ask('Please enter verification code:', echo: false)
237
216
 
238
- resp = FastlyCTL::Fetcher.api_request(:post, "/tokens", {
239
- force_session: true,
240
- endpoint: :api,
241
- params: params,
242
- headers: headers
243
- })
217
+ headers = {}
218
+ headers["Fastly-OTP"] = code
219
+ resp = FastlyCTL::Fetcher.api_request(:post, "/tokens", {
220
+ disable_token: true,
221
+ endpoint: :api,
222
+ body: options,
223
+ headers: headers
224
+ })
225
+ elsif resp.has_key?("msg")
226
+ abort "ERROR: #{resp}"
227
+ end
244
228
 
245
229
  thor.say("\n#{resp["id"]} created.")
246
230
 
@@ -0,0 +1,5 @@
1
+ class SubCommandBase < Thor
2
+ def self.banner(command, namespace = nil, subcommand = false)
3
+ basename + " " + self::SubcommandPrefix + " " + command.usage
4
+ end
5
+ end
@@ -4,6 +4,10 @@ module FastlyCTL
4
4
  Launchy.open(FastlyCTL::FASTLY_APP + FastlyCTL::TANGO_PATH + id)
5
5
  end
6
6
 
7
+ def self.open_app_path(path)
8
+ Launchy.open(FastlyCTL::FASTLY_APP + path)
9
+ end
10
+
7
11
  def self.parse_directory(path=false)
8
12
  directory = Dir.pwd unless path
9
13
  directory = path if path
@@ -68,5 +72,21 @@ module FastlyCTL
68
72
 
69
73
  return diff
70
74
  end
75
+
76
+ def self.percent_encode(string)
77
+ # CGI.escape replace whitespace to "+" which is "%20" in a percent-encoding manner
78
+ CGI.escape(string).gsub('+', '%20')
79
+ end
80
+
81
+ def self.filter_vnd(haystack,needle)
82
+ results = []
83
+ haystack.each do |i|
84
+ next unless i["type"] == needle
85
+
86
+ results.push(i)
87
+ end
88
+
89
+ return results
90
+ end
71
91
  end
72
92
  end
@@ -1,3 +1,3 @@
1
1
  module FastlyCTL
2
- VERSION = "1.0.13"
2
+ VERSION = "1.0.18"
3
3
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: fastlyctl
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.13
4
+ version: 1.0.18
5
5
  platform: ruby
6
6
  authors:
7
7
  - Stephen Basile
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2019-09-16 00:00:00.000000000 Z
11
+ date: 2020-07-28 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: typhoeus
@@ -72,6 +72,26 @@ dependencies:
72
72
  - - "~>"
73
73
  - !ruby/object:Gem::Version
74
74
  version: 2.4.3
75
+ - !ruby/object:Gem::Dependency
76
+ name: openssl
77
+ requirement: !ruby/object:Gem::Requirement
78
+ requirements:
79
+ - - ">="
80
+ - !ruby/object:Gem::Version
81
+ version: 2.1.2
82
+ - - "~>"
83
+ - !ruby/object:Gem::Version
84
+ version: 2.1.2
85
+ type: :runtime
86
+ prerelease: false
87
+ version_requirements: !ruby/object:Gem::Requirement
88
+ requirements:
89
+ - - ">="
90
+ - !ruby/object:Gem::Version
91
+ version: 2.1.2
92
+ - - "~>"
93
+ - !ruby/object:Gem::Version
94
+ version: 2.1.2
75
95
  description: This gem provides a CLI for managing Fastly configurations
76
96
  email:
77
97
  - stephen@fastly.com
@@ -97,6 +117,7 @@ files:
97
117
  - lib/fastlyctl/commands/acl.rb
98
118
  - lib/fastlyctl/commands/activate.rb
99
119
  - lib/fastlyctl/commands/clone.rb
120
+ - lib/fastlyctl/commands/condition.rb
100
121
  - lib/fastlyctl/commands/copy.rb
101
122
  - lib/fastlyctl/commands/create_service.rb
102
123
  - lib/fastlyctl/commands/dictionary.rb
@@ -110,10 +131,13 @@ files:
110
131
  - lib/fastlyctl/commands/purge_all.rb
111
132
  - lib/fastlyctl/commands/skeleton.rb
112
133
  - lib/fastlyctl/commands/snippet.rb
134
+ - lib/fastlyctl/commands/tls.rb
135
+ - lib/fastlyctl/commands/tls/managed.rb
113
136
  - lib/fastlyctl/commands/token.rb
114
137
  - lib/fastlyctl/commands/upload.rb
115
138
  - lib/fastlyctl/commands/watch.rb
116
139
  - lib/fastlyctl/fetcher.rb
140
+ - lib/fastlyctl/subcommand_patch.rb
117
141
  - lib/fastlyctl/utils.rb
118
142
  - lib/fastlyctl/version.rb
119
143
  homepage: http://www.github.com/fastly/fastlyctl